feat(planting): 已付款未KYC用户强制进入实名认证流程
后端 (planting-service): - 添加 /contract-signing/kyc-requirement 接口检查用户是否需要KYC - 检查已付款订单但无合同签署任务的情况 前端 (mobile-app): - ContractCheckService 新增 checkAll() 综合检查方法 - HomeShellPage 综合检查待签署合同和KYC需求 - 需要KYC时弹出强制认证弹窗,不可关闭 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
dbeef0a80b
commit
f62a96f3f1
|
|
@ -109,6 +109,30 @@ export class ContractSigningController {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否需要先完成 KYC
|
||||
*
|
||||
* 如果用户有已付款的认种订单但未完成 KYC,返回需要强制完成 KYC 的信息
|
||||
*/
|
||||
@Get('kyc-requirement')
|
||||
async checkKycRequirement(
|
||||
@Request() req: { user: { id: string; accountSequence?: string } },
|
||||
) {
|
||||
const userId = BigInt(req.user.id);
|
||||
// accountSequence 可能在 token 中,也可能需要从 userId 派生
|
||||
const accountSequence = req.user.accountSequence || req.user.id;
|
||||
|
||||
const result = await this.contractSigningService.checkKycRequirement(
|
||||
userId,
|
||||
accountSequence,
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签署任务详情
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { ContractSigningStatus } from '../../domain/value-objects';
|
||||
import { EventPublisherService } from '../../infrastructure/kafka/event-publisher.service';
|
||||
import { UnitOfWork, UNIT_OF_WORK } from '../../infrastructure/persistence/unit-of-work';
|
||||
import { IdentityServiceClient } from '../../infrastructure/external/identity-service.client';
|
||||
|
||||
/**
|
||||
* 创建签署任务的参数
|
||||
|
|
@ -79,6 +80,7 @@ export class ContractSigningService {
|
|||
@Inject(UNIT_OF_WORK)
|
||||
private readonly unitOfWork: UnitOfWork,
|
||||
private readonly eventPublisher: EventPublisherService,
|
||||
private readonly identityClient: IdentityServiceClient,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -160,6 +162,57 @@ export class ContractSigningService {
|
|||
return tasks.map((t) => this.toDto(t));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否需要先完成 KYC
|
||||
*
|
||||
* 条件:用户有已付款的认种订单,但没有对应的合同签署任务(因为未完成KYC)
|
||||
*
|
||||
* @returns 如果需要KYC返回相关信息,否则返回null
|
||||
*/
|
||||
async checkKycRequirement(
|
||||
userId: bigint,
|
||||
accountSequence: string,
|
||||
): Promise<{ requiresKyc: boolean; paidOrderCount: number; message?: string } | null> {
|
||||
try {
|
||||
// 1. 查找已付款但没有合同的订单
|
||||
const ordersWithoutContract = await this.orderRepo.findPaidOrdersWithoutContract(userId);
|
||||
|
||||
if (ordersWithoutContract.length === 0) {
|
||||
return { requiresKyc: false, paidOrderCount: 0 };
|
||||
}
|
||||
|
||||
// 2. 检查用户 KYC 状态
|
||||
const kycInfo = await this.identityClient.getUserKycInfo(accountSequence);
|
||||
|
||||
if (kycInfo) {
|
||||
// 用户已完成 KYC,但合同还没创建(可能是系统延迟)
|
||||
// 这种情况不需要强制 KYC,合同会通过 KYC 完成事件自动创建
|
||||
this.logger.log(
|
||||
`User ${accountSequence} has ${ordersWithoutContract.length} orders without contract, ` +
|
||||
`but KYC is complete. Contracts will be created automatically.`,
|
||||
);
|
||||
return { requiresKyc: false, paidOrderCount: ordersWithoutContract.length };
|
||||
}
|
||||
|
||||
// 3. 用户未完成 KYC,需要强制完成
|
||||
this.logger.log(
|
||||
`User ${accountSequence} has ${ordersWithoutContract.length} paid orders but no KYC. ` +
|
||||
`Requiring KYC verification.`,
|
||||
);
|
||||
|
||||
return {
|
||||
requiresKyc: true,
|
||||
paidOrderCount: ordersWithoutContract.length,
|
||||
message:
|
||||
'系统检测到您已认种了榴莲树,但尚未按国家法规完成实名认证,无法签署合同。请先完成实名认证后才能继续使用APP。',
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to check KYC requirement for user ${accountSequence}:`, error);
|
||||
// 出错时不阻止用户,返回不需要 KYC
|
||||
return { requiresKyc: false, paidOrderCount: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签署任务详情
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,8 +1,33 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'contract_signing_service.dart';
|
||||
|
||||
/// 合同检查结果
|
||||
class ContractCheckResult {
|
||||
/// 是否有待签署的合同
|
||||
final bool hasPendingContracts;
|
||||
|
||||
/// 是否需要先完成 KYC
|
||||
final bool requiresKyc;
|
||||
|
||||
/// 需要 KYC 时的提示消息
|
||||
final String? kycMessage;
|
||||
|
||||
/// 已付款但未完成 KYC 的订单数量
|
||||
final int paidOrderCount;
|
||||
|
||||
ContractCheckResult({
|
||||
required this.hasPendingContracts,
|
||||
required this.requiresKyc,
|
||||
this.kycMessage,
|
||||
this.paidOrderCount = 0,
|
||||
});
|
||||
|
||||
/// 是否需要强制操作(有待签署合同或需要 KYC)
|
||||
bool get requiresAction => hasPendingContracts || requiresKyc;
|
||||
}
|
||||
|
||||
/// 合同检查服务
|
||||
/// 用于在 App 启动时检查用户是否有未签署的合同
|
||||
/// 用于在 App 启动时检查用户是否有未签署的合同或需要完成 KYC
|
||||
class ContractCheckService {
|
||||
final ContractSigningService _contractSigningService;
|
||||
|
||||
|
|
@ -30,6 +55,48 @@ class ContractCheckService {
|
|||
}
|
||||
}
|
||||
|
||||
/// 综合检查:待签署合同和 KYC 需求
|
||||
///
|
||||
/// 返回检查结果,包含:
|
||||
/// - 是否有待签署的合同
|
||||
/// - 是否需要先完成 KYC(有已付款订单但未完成 KYC)
|
||||
Future<ContractCheckResult> checkAll() async {
|
||||
try {
|
||||
debugPrint('[ContractCheckService] 开始综合检查...');
|
||||
|
||||
// 1. 检查未签署的合同
|
||||
final unsignedTasks = await _contractSigningService.getUnsignedTasks();
|
||||
final hasPending = unsignedTasks.isNotEmpty;
|
||||
debugPrint('[ContractCheckService] 未签署合同数量: ${unsignedTasks.length}');
|
||||
|
||||
// 2. 如果有待签署的合同,直接返回(不需要再检查 KYC)
|
||||
if (hasPending) {
|
||||
return ContractCheckResult(
|
||||
hasPendingContracts: true,
|
||||
requiresKyc: false,
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 没有待签署合同,检查是否有需要 KYC 的情况
|
||||
final kycResult = await _contractSigningService.checkKycRequirement();
|
||||
debugPrint('[ContractCheckService] KYC 检查结果: requiresKyc=${kycResult.requiresKyc}, paidOrderCount=${kycResult.paidOrderCount}');
|
||||
|
||||
return ContractCheckResult(
|
||||
hasPendingContracts: false,
|
||||
requiresKyc: kycResult.requiresKyc,
|
||||
kycMessage: kycResult.message,
|
||||
paidOrderCount: kycResult.paidOrderCount,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[ContractCheckService] 综合检查失败: $e');
|
||||
// 检查失败时不阻止用户使用 App
|
||||
return ContractCheckResult(
|
||||
hasPendingContracts: false,
|
||||
requiresKyc: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取待签署合同数量
|
||||
Future<int> getPendingContractCount() async {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -150,6 +150,32 @@ class ContractSigningConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// KYC 需求检查结果
|
||||
class KycRequirementResult {
|
||||
/// 是否需要完成 KYC
|
||||
final bool requiresKyc;
|
||||
|
||||
/// 已付款但未完成 KYC 的订单数量
|
||||
final int paidOrderCount;
|
||||
|
||||
/// 提示消息
|
||||
final String? message;
|
||||
|
||||
KycRequirementResult({
|
||||
required this.requiresKyc,
|
||||
required this.paidOrderCount,
|
||||
this.message,
|
||||
});
|
||||
|
||||
factory KycRequirementResult.fromJson(Map<String, dynamic> json) {
|
||||
return KycRequirementResult(
|
||||
requiresKyc: json['requiresKyc'] ?? false,
|
||||
paidOrderCount: json['paidOrderCount'] ?? 0,
|
||||
message: json['message'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 合同签署服务
|
||||
class ContractSigningService {
|
||||
final ApiClient _apiClient;
|
||||
|
|
@ -180,6 +206,33 @@ class ContractSigningService {
|
|||
}
|
||||
}
|
||||
|
||||
/// 检查用户是否需要先完成 KYC
|
||||
///
|
||||
/// 如果用户有已付款的认种订单但未完成 KYC,返回需要强制完成 KYC 的信息
|
||||
Future<KycRequirementResult> checkKycRequirement() async {
|
||||
try {
|
||||
debugPrint('[ContractSigningService] 检查 KYC 需求');
|
||||
|
||||
final response = await _apiClient.get('/planting/contract-signing/kyc-requirement');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = response.data as Map<String, dynamic>;
|
||||
if (responseData['success'] == true && responseData['data'] != null) {
|
||||
final data = responseData['data'] as Map<String, dynamic>;
|
||||
debugPrint('[ContractSigningService] KYC 检查结果: requiresKyc=${data['requiresKyc']}, paidOrderCount=${data['paidOrderCount']}');
|
||||
return KycRequirementResult.fromJson(data);
|
||||
}
|
||||
}
|
||||
|
||||
// 默认不需要 KYC
|
||||
return KycRequirementResult(requiresKyc: false, paidOrderCount: 0);
|
||||
} catch (e) {
|
||||
debugPrint('[ContractSigningService] 检查 KYC 需求失败: $e');
|
||||
// 检查失败时不阻止用户
|
||||
return KycRequirementResult(requiresKyc: false, paidOrderCount: 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取待签署任务列表
|
||||
Future<List<ContractSigningTask>> getPendingTasks() async {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
|||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/theme/app_dimensions.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../core/services/contract_check_service.dart';
|
||||
import '../../../../routes/route_paths.dart';
|
||||
import '../../../../bootstrap.dart';
|
||||
import '../widgets/bottom_nav_bar.dart';
|
||||
|
|
@ -33,7 +34,7 @@ class _HomeShellPageState extends ConsumerState<HomeShellPage>
|
|||
// 首次进入时检查更新和未签署合同
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkForUpdateIfNeeded();
|
||||
_checkPendingContracts();
|
||||
_checkContractsAndKyc();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -63,27 +64,109 @@ class _HomeShellPageState extends ConsumerState<HomeShellPage>
|
|||
}
|
||||
}
|
||||
|
||||
/// 检查待签署合同
|
||||
Future<void> _checkPendingContracts() async {
|
||||
/// 综合检查:待签署合同和 KYC 需求
|
||||
Future<void> _checkContractsAndKyc() async {
|
||||
// 每次会话只检查一次
|
||||
if (_hasCheckedContracts) return;
|
||||
_hasCheckedContracts = true;
|
||||
|
||||
try {
|
||||
final contractCheckService = ref.read(contractCheckServiceProvider);
|
||||
final hasPending = await contractCheckService.hasPendingContracts();
|
||||
final result = await contractCheckService.checkAll();
|
||||
|
||||
if (hasPending && mounted) {
|
||||
if (!mounted) return;
|
||||
|
||||
// 1. 优先处理待签署合同
|
||||
if (result.hasPendingContracts) {
|
||||
// 有待签署合同,跳转到待签署列表页面
|
||||
// forceSign=true 表示必须签署后才能继续使用
|
||||
context.push(RoutePaths.pendingContracts, extra: true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 处理需要 KYC 的情况
|
||||
if (result.requiresKyc) {
|
||||
await _showKycRequiredDialog(result);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[HomeShellPage] 检查待签署合同失败: $e');
|
||||
debugPrint('[HomeShellPage] 检查合同和KYC失败: $e');
|
||||
// 检查失败不阻止用户使用 App
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示 KYC 必需弹窗
|
||||
Future<void> _showKycRequiredDialog(ContractCheckResult result) async {
|
||||
final message = result.kycMessage ??
|
||||
'系统检测到您已认种了榴莲树,但尚未按国家法规完成实名认证,无法签署合同。请先完成实名认证后才能继续使用APP。';
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false, // 不允许点击外部关闭
|
||||
builder: (context) => WillPopScope(
|
||||
onWillPop: () async => false, // 不允许返回键关闭
|
||||
child: AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, color: Colors.orange[700], size: 28),
|
||||
const SizedBox(width: 8),
|
||||
const Text('需要实名认证'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
message,
|
||||
style: const TextStyle(fontSize: 15, height: 1.5),
|
||||
),
|
||||
if (result.paidOrderCount > 0) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Colors.orange[700], size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'您有 ${result.paidOrderCount} 笔认种订单待签署合同',
|
||||
style: TextStyle(
|
||||
color: Colors.orange[800],
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
// 跳转到 KYC 入口页面
|
||||
context.push(RoutePaths.kycEntry);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text('立即认证', style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 重置合同检查状态(用于用户切换账号时)
|
||||
static void resetContractCheckState() {
|
||||
_hasCheckedContracts = false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue