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:
hailin 2025-12-25 21:14:53 -08:00
parent dbeef0a80b
commit f62a96f3f1
5 changed files with 287 additions and 7 deletions

View File

@ -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,
};
}
/**
*
*/

View File

@ -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 };
}
}
/**
*
*/

View File

@ -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 {

View File

@ -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 {

View File

@ -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;