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 { ContractSigningStatus } from '../../domain/value-objects';
|
||||||
import { EventPublisherService } from '../../infrastructure/kafka/event-publisher.service';
|
import { EventPublisherService } from '../../infrastructure/kafka/event-publisher.service';
|
||||||
import { UnitOfWork, UNIT_OF_WORK } from '../../infrastructure/persistence/unit-of-work';
|
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)
|
@Inject(UNIT_OF_WORK)
|
||||||
private readonly unitOfWork: UnitOfWork,
|
private readonly unitOfWork: UnitOfWork,
|
||||||
private readonly eventPublisher: EventPublisherService,
|
private readonly eventPublisher: EventPublisherService,
|
||||||
|
private readonly identityClient: IdentityServiceClient,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -160,6 +162,57 @@ export class ContractSigningService {
|
||||||
return tasks.map((t) => this.toDto(t));
|
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 'package:flutter/foundation.dart';
|
||||||
import 'contract_signing_service.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 {
|
class ContractCheckService {
|
||||||
final ContractSigningService _contractSigningService;
|
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 {
|
Future<int> getPendingContractCount() async {
|
||||||
try {
|
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 {
|
class ContractSigningService {
|
||||||
final ApiClient _apiClient;
|
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 {
|
Future<List<ContractSigningTask>> getPendingTasks() async {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../../../../core/theme/app_dimensions.dart';
|
import '../../../../core/theme/app_dimensions.dart';
|
||||||
import '../../../../core/di/injection_container.dart';
|
import '../../../../core/di/injection_container.dart';
|
||||||
|
import '../../../../core/services/contract_check_service.dart';
|
||||||
import '../../../../routes/route_paths.dart';
|
import '../../../../routes/route_paths.dart';
|
||||||
import '../../../../bootstrap.dart';
|
import '../../../../bootstrap.dart';
|
||||||
import '../widgets/bottom_nav_bar.dart';
|
import '../widgets/bottom_nav_bar.dart';
|
||||||
|
|
@ -33,7 +34,7 @@ class _HomeShellPageState extends ConsumerState<HomeShellPage>
|
||||||
// 首次进入时检查更新和未签署合同
|
// 首次进入时检查更新和未签署合同
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_checkForUpdateIfNeeded();
|
_checkForUpdateIfNeeded();
|
||||||
_checkPendingContracts();
|
_checkContractsAndKyc();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,27 +64,109 @@ class _HomeShellPageState extends ConsumerState<HomeShellPage>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查待签署合同
|
/// 综合检查:待签署合同和 KYC 需求
|
||||||
Future<void> _checkPendingContracts() async {
|
Future<void> _checkContractsAndKyc() async {
|
||||||
// 每次会话只检查一次
|
// 每次会话只检查一次
|
||||||
if (_hasCheckedContracts) return;
|
if (_hasCheckedContracts) return;
|
||||||
_hasCheckedContracts = true;
|
_hasCheckedContracts = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final contractCheckService = ref.read(contractCheckServiceProvider);
|
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 表示必须签署后才能继续使用
|
// forceSign=true 表示必须签署后才能继续使用
|
||||||
context.push(RoutePaths.pendingContracts, extra: true);
|
context.push(RoutePaths.pendingContracts, extra: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 处理需要 KYC 的情况
|
||||||
|
if (result.requiresKyc) {
|
||||||
|
await _showKycRequiredDialog(result);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[HomeShellPage] 检查待签署合同失败: $e');
|
debugPrint('[HomeShellPage] 检查合同和KYC失败: $e');
|
||||||
// 检查失败不阻止用户使用 App
|
// 检查失败不阻止用户使用 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() {
|
static void resetContractCheckState() {
|
||||||
_hasCheckedContracts = false;
|
_hasCheckedContracts = false;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue