feat(kyc): 升级实名认证为三要素验证(姓名+身份证号+手机号)

- 后端 aliyun-kyc.provider.ts: 改用 ID_CARD_THREE 类型,添加 PhoneNumber 参数
- 后端 kyc-application.service.ts: 从用户账户获取手机号传递给 KYC provider
- 前端 kyc_id_page.dart: 更新文案为"三要素验证"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-24 23:54:13 -08:00
parent e4f27b3134
commit 181d11d656
3 changed files with 38 additions and 18 deletions

View File

@ -211,15 +211,16 @@ export class KycApplicationService {
/** /**
* ======================================== * ========================================
* 层级1: 实名认证 - * 层级1: 实名认证 -
* ======================================== * ========================================
* ++
*/ */
async submitRealNameVerification( async submitRealNameVerification(
userId: string, userId: string,
realName: string, realName: string,
idCardNumber: string, idCardNumber: string,
) { ) {
this.logger.log(`[KYC] [Level1] Submitting real name verification for user: ${userId}`); this.logger.log(`[KYC] [Level1] Submitting real name verification (3-factor) for user: ${userId}`);
const config = await this.getKycConfig(); const config = await this.getKycConfig();
if (!config.level1Enabled) { if (!config.level1Enabled) {
@ -231,6 +232,7 @@ export class KycApplicationService {
select: { select: {
realNameVerified: true, realNameVerified: true,
kycStatus: true, kycStatus: true,
phoneNumber: true,
}, },
}); });
@ -242,6 +244,10 @@ export class KycApplicationService {
throw new ApplicationError('实名认证已完成,无需重复提交'); throw new ApplicationError('实名认证已完成,无需重复提交');
} }
if (!user.phoneNumber) {
throw new ApplicationError('未绑定手机号,无法进行实名认证');
}
// 生成请求 ID // 生成请求 ID
const requestId = `REAL_NAME_${userId}_${Date.now()}`; const requestId = `REAL_NAME_${userId}_${Date.now()}`;
@ -249,22 +255,24 @@ export class KycApplicationService {
const attempt = await this.prisma.kycVerificationAttempt.create({ const attempt = await this.prisma.kycVerificationAttempt.create({
data: { data: {
userId: BigInt(userId), userId: BigInt(userId),
verificationType: 'ID_CARD', verificationType: 'ID_CARD_THREE',
provider: 'ALIYUN', provider: 'ALIYUN',
requestId, requestId,
inputData: { inputData: {
realName: this.maskName(realName), realName: this.maskName(realName),
idCardNumber: this.maskIdCard(idCardNumber), idCardNumber: this.maskIdCard(idCardNumber),
phoneNumber: this.maskPhoneNumber(user.phoneNumber),
}, },
status: 'PENDING', status: 'PENDING',
}, },
}); });
try { try {
// 调用阿里云要素验证 // 调用阿里云要素验证
const result = await this.aliyunKycProvider.verifyIdCard( const result = await this.aliyunKycProvider.verifyIdCard(
realName, realName,
idCardNumber, idCardNumber,
user.phoneNumber,
requestId, requestId,
); );

View File

@ -53,7 +53,7 @@ export interface IdCardOcrResult {
/** /**
* - * -
* *
* 层级1: 实名认证 - + * 层级1: 实名认证 - ++
* 层级2: 实人认证 - * 层级2: 实人认证 -
* 层级3: KYC - OCR识别验证 * 层级3: KYC - OCR识别验证
* *
@ -83,32 +83,34 @@ export class AliyunKycProvider {
/** /**
* ======================================== * ========================================
* 层级1: 实名认证 - * 层级1: 实名认证 -
* ======================================== * ========================================
* *
*/ */
async verifyIdCard( async verifyIdCard(
realName: string, realName: string,
idCardNumber: string, idCardNumber: string,
phoneNumber: string,
requestId: string, requestId: string,
): Promise<IdCardVerificationResult> { ): Promise<IdCardVerificationResult> {
this.logger.log(`[AliyunKYC] [Level1] Starting ID card verification, requestId: ${requestId}`); this.logger.log(`[AliyunKYC] [Level1] Starting ID card verification (3-factor), requestId: ${requestId}`);
// 开发/测试环境:模拟验证 // 开发/测试环境:模拟验证
if (!this.enabled) { if (!this.enabled) {
this.logger.warn('[AliyunKYC] KYC is disabled, using mock verification'); this.logger.warn('[AliyunKYC] KYC is disabled, using mock verification');
return this.mockIdCardVerification(realName, idCardNumber); return this.mockIdCardVerification(realName, idCardNumber, phoneNumber);
} }
try { try {
// 调用阿里云身份要素核验 API // 调用阿里云身份要素核验 API
const params = { const params = {
Action: 'VerifyMaterial', Action: 'VerifyMaterial',
Version: '2019-03-07', Version: '2019-03-07',
Format: 'JSON', Format: 'JSON',
BizType: 'ID_CARD_TWO', BizType: 'ID_CARD_THREE',
Name: realName, Name: realName,
IdCardNumber: idCardNumber, IdCardNumber: idCardNumber,
PhoneNumber: phoneNumber,
}; };
const response = await this.callAliyunApi(params); const response = await this.callAliyunApi(params);
@ -410,8 +412,8 @@ export class AliyunKycProvider {
// ============ Mock 方法 (开发/测试环境) ============ // ============ Mock 方法 (开发/测试环境) ============
private mockIdCardVerification(realName: string, idCardNumber: string): IdCardVerificationResult { private mockIdCardVerification(realName: string, idCardNumber: string, phoneNumber: string): IdCardVerificationResult {
this.logger.log('[AliyunKYC] Using mock ID card verification'); this.logger.log('[AliyunKYC] Using mock ID card verification (3-factor)');
// 基本格式验证 // 基本格式验证
if (!realName || realName.length < 2) { if (!realName || realName.length < 2) {
@ -441,9 +443,19 @@ export class AliyunKycProvider {
}; };
} }
// 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneNumber || !phoneRegex.test(phoneNumber)) {
return {
success: false,
errorMessage: '手机号格式不正确',
rawResponse: { mock: true, reason: 'invalid_phone' },
};
}
return { return {
success: true, success: true,
rawResponse: { mock: true, verifyTime: new Date().toISOString() }, rawResponse: { mock: true, verifyTime: new Date().toISOString(), verifyType: '3-factor' },
}; };
} }

View File

@ -4,7 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'kyc_entry_page.dart'; import 'kyc_entry_page.dart';
/// KYC 1: () /// KYC 1: (: ++)
class KycIdPage extends ConsumerStatefulWidget { class KycIdPage extends ConsumerStatefulWidget {
const KycIdPage({super.key}); const KycIdPage({super.key});
@ -143,7 +143,7 @@ class _KycIdPageState extends ConsumerState<KycIdPage> {
SizedBox(height: 24.h), SizedBox(height: 24.h),
Text( Text(
'要素验证', '要素验证',
style: TextStyle( style: TextStyle(
fontSize: 24.sp, fontSize: 24.sp,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@ -152,7 +152,7 @@ class _KycIdPageState extends ConsumerState<KycIdPage> {
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
Text( Text(
'请填写与身份证一致的信息', '请填写与身份证一致的信息,系统将自动使用您的注册手机号进行验证',
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: const Color(0xFF999999), color: const Color(0xFF999999),
@ -224,7 +224,7 @@ class _KycIdPageState extends ConsumerState<KycIdPage> {
SizedBox(width: 8.w), SizedBox(width: 8.w),
Expanded( Expanded(
child: Text( child: Text(
'您的身份信息将通过权威数据源进行验证,信息将被加密存储,不会泄露给任何第三方。', '您的身份信息(姓名+身份证号+手机号)将通过权威数据源进行三要素验证,信息将被加密存储,不会泄露给任何第三方。',
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: const Color(0xFFE65100), color: const Color(0xFFE65100),