diff --git a/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts b/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts index 1945ecc1..c7e2377c 100644 --- a/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts +++ b/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts @@ -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, + }; + } + /** * 获取签署任务详情 */ diff --git a/backend/services/planting-service/src/application/services/contract-signing.service.ts b/backend/services/planting-service/src/application/services/contract-signing.service.ts index 52fa660d..f2d3a9e1 100644 --- a/backend/services/planting-service/src/application/services/contract-signing.service.ts +++ b/backend/services/planting-service/src/application/services/contract-signing.service.ts @@ -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 }; + } + } + /** * 获取签署任务详情 */ diff --git a/frontend/mobile-app/lib/core/services/contract_check_service.dart b/frontend/mobile-app/lib/core/services/contract_check_service.dart index 74ad8fed..9d393137 100644 --- a/frontend/mobile-app/lib/core/services/contract_check_service.dart +++ b/frontend/mobile-app/lib/core/services/contract_check_service.dart @@ -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 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 getPendingContractCount() async { try { diff --git a/frontend/mobile-app/lib/core/services/contract_signing_service.dart b/frontend/mobile-app/lib/core/services/contract_signing_service.dart index 14ba980a..d3bfaad5 100644 --- a/frontend/mobile-app/lib/core/services/contract_signing_service.dart +++ b/frontend/mobile-app/lib/core/services/contract_signing_service.dart @@ -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 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 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; + if (responseData['success'] == true && responseData['data'] != null) { + final data = responseData['data'] as Map; + 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> getPendingTasks() async { try { diff --git a/frontend/mobile-app/lib/features/home/presentation/pages/home_shell_page.dart b/frontend/mobile-app/lib/features/home/presentation/pages/home_shell_page.dart index f513bdd1..448dbc96 100644 --- a/frontend/mobile-app/lib/features/home/presentation/pages/home_shell_page.dart +++ b/frontend/mobile-app/lib/features/home/presentation/pages/home_shell_page.dart @@ -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 // 首次进入时检查更新和未签署合同 WidgetsBinding.instance.addPostFrameCallback((_) { _checkForUpdateIfNeeded(); - _checkPendingContracts(); + _checkContractsAndKyc(); }); } @@ -63,27 +64,109 @@ class _HomeShellPageState extends ConsumerState } } - /// 检查待签署合同 - Future _checkPendingContracts() async { + /// 综合检查:待签署合同和 KYC 需求 + Future _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 _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;