rwadurian/frontend/mobile-app/lib/features/kyc/data/kyc_service.dart

758 lines
22 KiB
Dart

import 'package:flutter/foundation.dart';
import '../../../core/network/api_client.dart';
import '../../../core/errors/exceptions.dart';
/// KYC 状态枚举 (综合状态)
enum KycStatusType {
notStarted, // 未开始
realNameVerified, // 层级1完成: 实名认证通过
faceVerified, // 层级2完成: 实人认证通过
kycVerified, // 层级3完成: KYC认证通过
completed, // 所有层级完成
rejected, // 被拒绝
}
/// KYC 配置响应
class KycConfigResponse {
final bool level1Enabled; // 实名认证开关
final bool level2Enabled; // 实人认证开关
final bool level3Enabled; // KYC证件照开关
KycConfigResponse({
required this.level1Enabled,
required this.level2Enabled,
required this.level3Enabled,
});
factory KycConfigResponse.fromJson(Map<String, dynamic> json) {
return KycConfigResponse(
level1Enabled: json['level1Enabled'] as bool? ?? true,
level2Enabled: json['level2Enabled'] as bool? ?? true,
level3Enabled: json['level3Enabled'] as bool? ?? true,
);
}
}
/// 层级1状态
class KycLevel1Status {
final bool enabled;
final bool verified;
final DateTime? verifiedAt;
final String? realName;
final String? idCardNumber;
KycLevel1Status({
required this.enabled,
required this.verified,
this.verifiedAt,
this.realName,
this.idCardNumber,
});
factory KycLevel1Status.fromJson(Map<String, dynamic> json) {
return KycLevel1Status(
enabled: json['enabled'] as bool? ?? true,
verified: json['verified'] as bool? ?? false,
verifiedAt: json['verifiedAt'] != null
? DateTime.parse(json['verifiedAt'] as String)
: null,
realName: json['realName'] as String?,
idCardNumber: json['idCardNumber'] as String?,
);
}
}
/// 层级2状态
class KycLevel2Status {
final bool enabled;
final bool verified;
final DateTime? verifiedAt;
final bool canStart;
KycLevel2Status({
required this.enabled,
required this.verified,
this.verifiedAt,
required this.canStart,
});
factory KycLevel2Status.fromJson(Map<String, dynamic> json) {
return KycLevel2Status(
enabled: json['enabled'] as bool? ?? true,
verified: json['verified'] as bool? ?? false,
verifiedAt: json['verifiedAt'] != null
? DateTime.parse(json['verifiedAt'] as String)
: null,
canStart: json['canStart'] as bool? ?? false,
);
}
}
/// 层级3状态
class KycLevel3Status {
final bool enabled;
final bool verified;
final DateTime? verifiedAt;
final bool hasIdCardFront;
final bool hasIdCardBack;
final bool canStart;
KycLevel3Status({
required this.enabled,
required this.verified,
this.verifiedAt,
required this.hasIdCardFront,
required this.hasIdCardBack,
required this.canStart,
});
factory KycLevel3Status.fromJson(Map<String, dynamic> json) {
return KycLevel3Status(
enabled: json['enabled'] as bool? ?? true,
verified: json['verified'] as bool? ?? false,
verifiedAt: json['verifiedAt'] != null
? DateTime.parse(json['verifiedAt'] as String)
: null,
hasIdCardFront: json['hasIdCardFront'] as bool? ?? false,
hasIdCardBack: json['hasIdCardBack'] as bool? ?? false,
canStart: json['canStart'] as bool? ?? false,
);
}
}
/// KYC 完整状态响应 (支持三层认证)
class KycStatusResponse {
final KycConfigResponse config;
final List<String> requiredSteps;
// 层级1: 实名认证
final KycLevel1Status level1;
// 层级2: 实人认证
final KycLevel2Status level2;
// 层级3: KYC
final KycLevel3Status level3;
// 综合状态
final String kycStatus;
final bool isCompleted;
final String? rejectedReason;
final String? phoneNumber;
final bool phoneVerified;
KycStatusResponse({
required this.config,
required this.requiredSteps,
required this.level1,
required this.level2,
required this.level3,
required this.kycStatus,
required this.isCompleted,
this.rejectedReason,
this.phoneNumber,
required this.phoneVerified,
});
factory KycStatusResponse.fromJson(Map<String, dynamic> json) {
return KycStatusResponse(
config: KycConfigResponse.fromJson(
json['config'] as Map<String, dynamic>? ?? {},
),
requiredSteps: (json['requiredSteps'] as List?)?.cast<String>() ?? [],
level1: KycLevel1Status.fromJson(
json['level1'] as Map<String, dynamic>? ?? {},
),
level2: KycLevel2Status.fromJson(
json['level2'] as Map<String, dynamic>? ?? {},
),
level3: KycLevel3Status.fromJson(
json['level3'] as Map<String, dynamic>? ?? {},
),
kycStatus: json['kycStatus'] as String? ?? 'NOT_STARTED',
isCompleted: json['isCompleted'] as bool? ?? false,
rejectedReason: json['rejectedReason'] as String?,
phoneNumber: json['phoneNumber'] as String?,
phoneVerified: json['phoneVerified'] as bool? ?? false,
);
}
KycStatusType get statusType {
switch (kycStatus) {
case 'REAL_NAME_VERIFIED':
return KycStatusType.realNameVerified;
case 'FACE_VERIFIED':
return KycStatusType.faceVerified;
case 'KYC_VERIFIED':
return KycStatusType.kycVerified;
case 'COMPLETED':
return KycStatusType.completed;
case 'REJECTED':
return KycStatusType.rejected;
default:
return KycStatusType.notStarted;
}
}
/// 兼容旧代码
String? get realName => level1.realName;
String? get idCardNumber => level1.idCardNumber;
DateTime? get kycVerifiedAt => level3.verifiedAt ?? level2.verifiedAt ?? level1.verifiedAt;
bool get needsIdVerification => !level1.verified && level1.enabled;
}
/// 实名认证响应
class RealNameVerifyResponse {
final bool success;
final int level;
final String? status;
final String? message;
final String? errorMessage;
RealNameVerifyResponse({
required this.success,
required this.level,
this.status,
this.message,
this.errorMessage,
});
factory RealNameVerifyResponse.fromJson(Map<String, dynamic> json) {
return RealNameVerifyResponse(
success: json['success'] as bool? ?? false,
level: json['level'] as int? ?? 1,
status: json['status'] as String?,
message: json['message'] as String?,
errorMessage: json['errorMessage'] as String?,
);
}
}
/// 人脸认证初始化响应
class FaceVerifyInitResponse {
final bool success;
final String? certifyId;
final String? certifyUrl;
FaceVerifyInitResponse({
required this.success,
this.certifyId,
this.certifyUrl,
});
factory FaceVerifyInitResponse.fromJson(Map<String, dynamic> json) {
return FaceVerifyInitResponse(
success: json['success'] as bool? ?? false,
certifyId: json['certifyId'] as String?,
certifyUrl: json['certifyUrl'] as String?,
);
}
}
/// 人脸认证查询响应
class FaceVerifyQueryResponse {
final bool success;
final bool passed;
final String status;
final String? errorMessage;
FaceVerifyQueryResponse({
required this.success,
required this.passed,
required this.status,
this.errorMessage,
});
factory FaceVerifyQueryResponse.fromJson(Map<String, dynamic> json) {
return FaceVerifyQueryResponse(
success: json['success'] as bool? ?? false,
passed: json['passed'] as bool? ?? false,
status: json['status'] as String? ?? 'PENDING',
errorMessage: json['errorMessage'] as String?,
);
}
}
/// 证件照上传响应
class IdCardUploadResponse {
final bool success;
final String side;
final String? imageUrl;
final IdCardOcrResult? ocrResult;
IdCardUploadResponse({
required this.success,
required this.side,
this.imageUrl,
this.ocrResult,
});
factory IdCardUploadResponse.fromJson(Map<String, dynamic> json) {
return IdCardUploadResponse(
success: json['success'] as bool? ?? false,
side: json['side'] as String? ?? '',
imageUrl: json['imageUrl'] as String?,
ocrResult: json['ocrResult'] != null
? IdCardOcrResult.fromJson(json['ocrResult'] as Map<String, dynamic>)
: null,
);
}
}
/// OCR 识别结果
class IdCardOcrResult {
final String? name;
final String? idNumber;
final String? address;
final String? issueAuthority;
final String? validPeriod;
IdCardOcrResult({
this.name,
this.idNumber,
this.address,
this.issueAuthority,
this.validPeriod,
});
factory IdCardOcrResult.fromJson(Map<String, dynamic> json) {
return IdCardOcrResult(
name: json['name'] as String?,
idNumber: json['idNumber'] as String?,
address: json['address'] as String?,
issueAuthority: json['issueAuthority'] as String?,
validPeriod: json['validPeriod'] as String?,
);
}
}
/// KYC 确认响应
class KycConfirmResponse {
final bool success;
final int level;
final String status;
final String message;
KycConfirmResponse({
required this.success,
required this.level,
required this.status,
required this.message,
});
factory KycConfirmResponse.fromJson(Map<String, dynamic> json) {
return KycConfirmResponse(
success: json['success'] as bool? ?? false,
level: json['level'] as int? ?? 3,
status: json['status'] as String? ?? '',
message: json['message'] as String? ?? '',
);
}
}
/// 手机号状态响应
class PhoneStatusResponse {
final bool isBound;
final bool isVerified;
final String? phoneNumber;
final DateTime? verifiedAt;
PhoneStatusResponse({
required this.isBound,
required this.isVerified,
this.phoneNumber,
this.verifiedAt,
});
factory PhoneStatusResponse.fromJson(Map<String, dynamic> json) {
return PhoneStatusResponse(
isBound: json['isBound'] as bool? ?? false,
isVerified: json['isVerified'] as bool? ?? false,
phoneNumber: json['phoneNumber'] as String?,
verifiedAt: json['verifiedAt'] != null
? DateTime.parse(json['verifiedAt'] as String)
: null,
);
}
}
/// 验证旧手机响应
class VerifyOldPhoneResponse {
final String changePhoneToken;
final bool wasAlreadyVerified;
VerifyOldPhoneResponse({
required this.changePhoneToken,
required this.wasAlreadyVerified,
});
factory VerifyOldPhoneResponse.fromJson(Map<String, dynamic> json) {
return VerifyOldPhoneResponse(
changePhoneToken: json['changePhoneToken'] as String,
wasAlreadyVerified: json['wasAlreadyVerified'] as bool? ?? true,
);
}
}
/// KYC 服务 - 支持三层认证
/// 层级1: 实名认证 (二要素: 姓名+身份证号)
/// 层级2: 实人认证 (人脸活体检测)
/// 层级3: KYC (证件照上传验证)
class KycService {
static const String _tag = '[KycService]';
final ApiClient _apiClient;
KycService(this._apiClient);
// ============ 获取状态和配置 ============
/// 获取 KYC 配置(三层认证开关)
Future<KycConfigResponse> getKycConfig() async {
debugPrint('$_tag getKycConfig() - 获取 KYC 配置');
try {
final response = await _apiClient.get('/user/kyc/config');
debugPrint('$_tag getKycConfig() - 响应: ${response.statusCode}');
if (response.data == null) {
throw const ApiException('获取 KYC 配置失败: 空响应');
}
final responseData = response.data as Map<String, dynamic>;
final data = responseData['data'] as Map<String, dynamic>;
return KycConfigResponse.fromJson(data);
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag getKycConfig() - 异常: $e');
throw ApiException('获取 KYC 配置失败: $e');
}
}
/// 获取 KYC 完整状态(包含三层认证详情)
Future<KycStatusResponse> getKycStatus() async {
debugPrint('$_tag getKycStatus() - 获取 KYC 状态');
try {
final response = await _apiClient.get('/user/kyc/status');
debugPrint('$_tag getKycStatus() - 响应: ${response.statusCode}');
if (response.data == null) {
throw const ApiException('获取 KYC 状态失败: 空响应');
}
final responseData = response.data as Map<String, dynamic>;
// 后端返回格式: { success, data: { code, message, data: {...实际数据...} } }
final outerData = responseData['data'] as Map<String, dynamic>;
final data = outerData['data'] as Map<String, dynamic>;
return KycStatusResponse.fromJson(data);
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag getKycStatus() - 异常: $e');
throw ApiException('获取 KYC 状态失败: $e');
}
}
// ============ 层级1: 实名认证 ============
/// 提交实名认证(二要素验证)
Future<RealNameVerifyResponse> submitRealNameVerification({
required String realName,
required String idCardNumber,
}) async {
debugPrint('$_tag submitRealNameVerification() - 提交实名认证');
try {
final response = await _apiClient.post(
'/user/kyc/level1/submit',
data: {
'realName': realName,
'idCardNumber': idCardNumber,
},
);
debugPrint('$_tag submitRealNameVerification() - 响应: ${response.statusCode}');
if (response.data == null) {
throw const ApiException('提交失败: 空响应');
}
final responseData = response.data as Map<String, dynamic>;
// 后端返回格式: { success, data: { code, message, data: {...实际数据...} } }
final outerData = responseData['data'] as Map<String, dynamic>;
final data = outerData['data'] as Map<String, dynamic>;
return RealNameVerifyResponse.fromJson(data);
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag submitRealNameVerification() - 异常: $e');
throw ApiException('实名认证失败: $e');
}
}
// ============ 层级2: 实人认证 ============
/// 初始化人脸活体检测
Future<FaceVerifyInitResponse> initFaceVerification({String? metaInfo}) async {
debugPrint('$_tag initFaceVerification() - 初始化人脸认证');
try {
final response = await _apiClient.post(
'/user/kyc/level2/init',
data: {
if (metaInfo != null) 'metaInfo': metaInfo,
},
);
debugPrint('$_tag initFaceVerification() - 响应: ${response.statusCode}');
if (response.data == null) {
throw const ApiException('初始化失败: 空响应');
}
final responseData = response.data as Map<String, dynamic>;
final data = responseData['data'] as Map<String, dynamic>;
return FaceVerifyInitResponse.fromJson(data);
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag initFaceVerification() - 异常: $e');
throw ApiException('初始化人脸认证失败: $e');
}
}
/// 查询人脸认证结果
Future<FaceVerifyQueryResponse> queryFaceVerification(String certifyId) async {
debugPrint('$_tag queryFaceVerification() - 查询人脸认证结果');
try {
final response = await _apiClient.get(
'/user/kyc/level2/query',
queryParameters: {'certifyId': certifyId},
);
debugPrint('$_tag queryFaceVerification() - 响应: ${response.statusCode}');
if (response.data == null) {
throw const ApiException('查询失败: 空响应');
}
final responseData = response.data as Map<String, dynamic>;
final data = responseData['data'] as Map<String, dynamic>;
return FaceVerifyQueryResponse.fromJson(data);
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag queryFaceVerification() - 异常: $e');
throw ApiException('查询人脸认证结果失败: $e');
}
}
// ============ 层级3: KYC 证件照 ============
/// 上传身份证照片
Future<IdCardUploadResponse> uploadIdCardPhoto({
required String side, // 'front' 或 'back'
required List<int> imageBytes,
required String fileName,
}) async {
debugPrint('$_tag uploadIdCardPhoto() - 上传证件照 side=$side');
try {
final response = await _apiClient.uploadFile(
'/user/kyc/level3/upload/$side',
imageBytes,
fileName,
fieldName: 'file',
);
debugPrint('$_tag uploadIdCardPhoto() - 响应: ${response.statusCode}');
if (response.data == null) {
throw const ApiException('上传失败: 空响应');
}
final responseData = response.data as Map<String, dynamic>;
final data = responseData['data'] as Map<String, dynamic>;
return IdCardUploadResponse.fromJson(data);
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag uploadIdCardPhoto() - 异常: $e');
throw ApiException('上传证件照失败: $e');
}
}
/// 确认提交 KYC
Future<KycConfirmResponse> confirmKycSubmission() async {
debugPrint('$_tag confirmKycSubmission() - 确认提交 KYC');
try {
final response = await _apiClient.post('/user/kyc/level3/confirm');
debugPrint('$_tag confirmKycSubmission() - 响应: ${response.statusCode}');
if (response.data == null) {
throw const ApiException('提交失败: 空响应');
}
final responseData = response.data as Map<String, dynamic>;
final data = responseData['data'] as Map<String, dynamic>;
return KycConfirmResponse.fromJson(data);
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag confirmKycSubmission() - 异常: $e');
throw ApiException('提交 KYC 失败: $e');
}
}
// ============ 手机号验证相关 (用于跳过验证的用户补充验证) ============
/// 发送手机号验证短信
Future<void> sendKycVerifySms() async {
debugPrint('$_tag sendKycVerifySms() - 发送 KYC 手机验证短信');
try {
final response = await _apiClient.post('/user/kyc/send-phone-sms');
debugPrint('$_tag sendKycVerifySms() - 响应: ${response.statusCode}');
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag sendKycVerifySms() - 异常: $e');
throw ApiException('发送验证码失败: $e');
}
}
/// 验证手机号验证码
Future<void> verifyPhoneForKyc(String smsCode) async {
debugPrint('$_tag verifyPhoneForKyc() - 验证手机号');
try {
final response = await _apiClient.post(
'/user/kyc/verify-phone',
data: {'smsCode': smsCode},
);
debugPrint('$_tag verifyPhoneForKyc() - 响应: ${response.statusCode}');
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag verifyPhoneForKyc() - 异常: $e');
throw ApiException('验证失败: $e');
}
}
// ============ 更换手机号相关 ============
/// 获取手机号状态
Future<PhoneStatusResponse> getPhoneStatus() async {
debugPrint('$_tag getPhoneStatus() - 获取手机号状态');
try {
final response = await _apiClient.get('/user/phone-status');
debugPrint('$_tag getPhoneStatus() - 响应: ${response.statusCode}');
if (response.data == null) {
throw const ApiException('获取手机号状态失败: 空响应');
}
final responseData = response.data as Map<String, dynamic>;
final data = responseData['data'] as Map<String, dynamic>;
return PhoneStatusResponse.fromJson(data);
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag getPhoneStatus() - 异常: $e');
throw ApiException('获取手机号状态失败: $e');
}
}
/// 发送旧手机验证码
Future<void> sendOldPhoneCode() async {
debugPrint('$_tag sendOldPhoneCode() - 发送旧手机验证码');
try {
final response = await _apiClient.post('/user/change-phone/send-old-code');
debugPrint('$_tag sendOldPhoneCode() - 响应: ${response.statusCode}');
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag sendOldPhoneCode() - 异常: $e');
throw ApiException('发送验证码失败: $e');
}
}
/// 验证旧手机验证码
/// 返回包含 changePhoneToken 和 wasAlreadyVerified 的响应
Future<VerifyOldPhoneResponse> verifyOldPhoneCode(String smsCode) async {
debugPrint('$_tag verifyOldPhoneCode() - 验证旧手机验证码');
try {
final response = await _apiClient.post(
'/user/change-phone/verify-old',
data: {'smsCode': smsCode},
);
debugPrint('$_tag verifyOldPhoneCode() - 响应: ${response.statusCode}');
if (response.data == null) {
throw const ApiException('验证失败: 空响应');
}
final responseData = response.data as Map<String, dynamic>;
final data = responseData['data'] as Map<String, dynamic>;
return VerifyOldPhoneResponse.fromJson(data);
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag verifyOldPhoneCode() - 异常: $e');
throw ApiException('验证失败: $e');
}
}
/// 发送新手机验证码
Future<void> sendNewPhoneCode({
required String newPhoneNumber,
required String changePhoneToken,
}) async {
debugPrint('$_tag sendNewPhoneCode() - 发送新手机验证码');
try {
final response = await _apiClient.post(
'/user/change-phone/send-new-code',
data: {
'newPhoneNumber': newPhoneNumber,
'changePhoneToken': changePhoneToken,
},
);
debugPrint('$_tag sendNewPhoneCode() - 响应: ${response.statusCode}');
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag sendNewPhoneCode() - 异常: $e');
throw ApiException('发送验证码失败: $e');
}
}
/// 确认更换手机号
Future<void> confirmChangePhone({
required String newPhoneNumber,
required String smsCode,
required String changePhoneToken,
}) async {
debugPrint('$_tag confirmChangePhone() - 确认更换手机号');
try {
final response = await _apiClient.post(
'/user/change-phone/confirm',
data: {
'newPhoneNumber': newPhoneNumber,
'smsCode': smsCode,
'changePhoneToken': changePhoneToken,
},
);
debugPrint('$_tag confirmChangePhone() - 响应: ${response.statusCode}');
} on ApiException {
rethrow;
} catch (e) {
debugPrint('$_tag confirmChangePhone() - 异常: $e');
throw ApiException('更换手机号失败: $e');
}
}
}