From 59e9cddf5b2f2c63d21609b6eed04fdf72f54bf7 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 24 Dec 2025 21:10:55 -0800 Subject: [PATCH] =?UTF-8?q?feat(planting):=20=E8=AE=A4=E7=A7=8D=E6=88=90?= =?UTF-8?q?=E5=8A=9F=E5=90=8E=E6=A3=80=E6=9F=A5=E5=AE=9E=E5=90=8D=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当CONTRACT_SIGNING_ENABLED=true时,认种成功后检查用户是否已完成实名认证: - 如果未完成实名认证,显示提示弹窗引导用户去认证 - 如果已完成或功能未启用,按原有流程返回个人中心 新增: - KycRequiredDialog 实名认证提示弹窗组件 - ContractSigningConfig 配置类和getConfig()方法 - kycServiceProvider 依赖注入 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../lib/core/di/injection_container.dart | 7 + .../services/contract_signing_service.dart | 24 +++ .../pages/planting_location_page.dart | 49 ++++- .../widgets/kyc_required_dialog.dart | 200 ++++++++++++++++++ 4 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 frontend/mobile-app/lib/features/planting/presentation/widgets/kyc_required_dialog.dart diff --git a/frontend/mobile-app/lib/core/di/injection_container.dart b/frontend/mobile-app/lib/core/di/injection_container.dart index 4f93ba11..de6f96dc 100644 --- a/frontend/mobile-app/lib/core/di/injection_container.dart +++ b/frontend/mobile-app/lib/core/di/injection_container.dart @@ -14,6 +14,7 @@ import '../services/notification_service.dart'; import '../services/system_config_service.dart'; import '../services/contract_signing_service.dart'; import '../services/contract_check_service.dart'; +import '../../features/kyc/data/kyc_service.dart'; // Storage Providers final secureStorageProvider = Provider((ref) { @@ -107,6 +108,12 @@ final contractCheckServiceProvider = Provider((ref) { return ContractCheckService(contractSigningService: contractSigningService); }); +// KYC Service Provider (调用 identity-service) +final kycServiceProvider = Provider((ref) { + final apiClient = ref.watch(apiClientProvider); + return KycService(apiClient: apiClient); +}); + // Override provider with initialized instance ProviderContainer createProviderContainer(LocalStorage localStorage) { return ProviderContainer( 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 a1bff286..1d729684 100644 --- a/frontend/mobile-app/lib/core/services/contract_signing_service.dart +++ b/frontend/mobile-app/lib/core/services/contract_signing_service.dart @@ -153,6 +153,30 @@ class ContractSigningService { ContractSigningService({required ApiClient apiClient}) : _apiClient = apiClient; + /// 获取合同签署配置(公开接口,不需要认证) + /// 用于判断是否需要在认种前检查实名认证 + Future getConfig() async { + try { + debugPrint('[ContractSigningService] 获取合同签署配置'); + + final response = await _apiClient.get('/planting/contract-signing/config'); + + if (response.statusCode == 200) { + final data = response.data as Map; + if (data['success'] == true && data['data'] != null) { + return ContractSigningConfig.fromJson(data['data']); + } + } + + // 默认启用合同签署 + return ContractSigningConfig(contractSigningEnabled: true); + } catch (e) { + debugPrint('[ContractSigningService] 获取配置失败: $e'); + // 获取失败时默认启用合同签署 + return ContractSigningConfig(contractSigningEnabled: true); + } + } + /// 获取待签署任务列表 Future> getPendingTasks() async { try { diff --git a/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart b/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart index 7718f42e..f881282d 100644 --- a/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart +++ b/frontend/mobile-app/lib/features/planting/presentation/pages/planting_location_page.dart @@ -3,7 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:city_pickers/city_pickers.dart'; import '../widgets/planting_confirm_dialog.dart'; +import '../widgets/kyc_required_dialog.dart'; import '../../../../core/di/injection_container.dart'; +import '../../../../routes/route_paths.dart'; /// 认种省市选择页面参数 class PlantingLocationParams { @@ -208,6 +210,37 @@ class _PlantingLocationPageState extends ConsumerState { ); } + /// 检查是否需要实名认证 + /// 仅在合同签署功能启用时才需要检查 + Future _checkKycRequirement() async { + try { + // 1. 获取合同签署配置 + final contractSigningService = ref.read(contractSigningServiceProvider); + final config = await contractSigningService.getConfig(); + + // 如果合同签署功能未启用,不需要检查实名认证 + if (!config.contractSigningEnabled) { + debugPrint('[PlantingLocationPage] 合同签署功能未启用,跳过实名认证检查'); + return false; + } + + // 2. 合同签署功能启用时,检查实名认证状态 + final kycService = ref.read(kycServiceProvider); + final kycStatus = await kycService.getKycStatus(); + + // 检查层级1(实名认证)是否完成 + final isVerified = kycStatus.level1.verified; + debugPrint('[PlantingLocationPage] 合同签署功能启用,实名认证状态: $isVerified'); + + // 返回是否需要去做实名认证(未完成实名认证则需要) + return !isVerified; + } catch (e) { + debugPrint('[PlantingLocationPage] 检查实名认证状态失败: $e'); + // 检查失败时默认不阻止用户,让后端处理 + return false; + } + } + /// 提交认种请求 Future _submitPlanting() async { setState(() => _isSubmitting = true); @@ -243,8 +276,22 @@ class _PlantingLocationPageState extends ConsumerState { ), ); + // 5. 认种成功后,检查是否需要实名认证(仅在合同签署功能启用时) + final needsKyc = await _checkKycRequirement(); + if (needsKyc && mounted) { + // 显示实名认证提示弹窗 + final goToKyc = await KycRequiredDialog.show(context: context); + if (goToKyc == true && mounted) { + // 跳转到实名认证页面 + context.push(RoutePaths.kycEntry); + return; + } + } + // 返回到个人中心 - context.go('/profile'); + if (mounted) { + context.go('/profile'); + } } } catch (e) { debugPrint('认种失败: $e'); diff --git a/frontend/mobile-app/lib/features/planting/presentation/widgets/kyc_required_dialog.dart b/frontend/mobile-app/lib/features/planting/presentation/widgets/kyc_required_dialog.dart new file mode 100644 index 00000000..56331c9e --- /dev/null +++ b/frontend/mobile-app/lib/features/planting/presentation/widgets/kyc_required_dialog.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; + +/// 未实名认证提示弹窗 +/// 当用户未完成实名认证但尝试确认认种时显示 +class KycRequiredDialog extends StatelessWidget { + const KycRequiredDialog({super.key}); + + /// 显示提示弹窗 + /// 返回 true 表示用户选择去实名认证,false 表示取消 + static Future show({required BuildContext context}) { + return showDialog( + context: context, + barrierDismissible: true, + barrierColor: const Color(0x80000000), + builder: (context) => const KycRequiredDialog(), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 24), + child: Container( + width: double.infinity, + constraints: const BoxConstraints(maxWidth: 360), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Color(0x40000000), + blurRadius: 50, + offset: Offset(0, 25), + spreadRadius: -12, + ), + ], + ), + child: Stack( + children: [ + // 主内容 + Padding( + padding: const EdgeInsets.fromLTRB(24, 32, 24, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 图标 + _buildIcon(), + const SizedBox(height: 16), + // 标题 + _buildTitle(), + const SizedBox(height: 12), + // 说明文字 + _buildDescription(), + const SizedBox(height: 24), + // 按钮 + _buildButtons(context), + ], + ), + ), + // 右上角关闭按钮 + Positioned( + top: 8, + right: 8, + child: GestureDetector( + onTap: () => Navigator.pop(context, false), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: const Color(0x0D000000), + borderRadius: BorderRadius.circular(16), + ), + child: const Icon( + Icons.close, + size: 18, + color: Color(0xFF8B5A2B), + ), + ), + ), + ), + ], + ), + ), + ); + } + + /// 构建图标 + Widget _buildIcon() { + return Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: const Color(0xFFFFF7E6), + borderRadius: BorderRadius.circular(32), + ), + child: const Center( + child: Icon( + Icons.verified_user_outlined, + color: Color(0xFFD4AF37), + size: 32, + ), + ), + ); + } + + /// 构建标题 + Widget _buildTitle() { + return const Text( + '需要先完成实名认证', + style: TextStyle( + fontSize: 20, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 1.3, + color: Color(0xFF5D4037), + ), + textAlign: TextAlign.center, + ); + } + + /// 构建说明文字 + Widget _buildDescription() { + return const Text( + '为了保障您的权益,认种前需要完成实名认证。\n完成认证后即可继续认种。', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xFF745D43), + ), + textAlign: TextAlign.center, + ); + } + + /// 构建按钮 + Widget _buildButtons(BuildContext context) { + return Column( + children: [ + // 去实名认证按钮 + SizedBox( + width: double.infinity, + child: GestureDetector( + onTap: () => Navigator.pop(context, true), + child: Container( + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFD4AF37), + borderRadius: BorderRadius.circular(8), + ), + child: const Center( + child: Text( + '去实名认证', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + height: 1.5, + color: Colors.white, + ), + ), + ), + ), + ), + ), + const SizedBox(height: 12), + // 稍后再说按钮 + SizedBox( + width: double.infinity, + child: GestureDetector( + onTap: () => Navigator.pop(context, false), + child: Container( + height: 48, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFFD4AF37), + width: 1, + ), + ), + child: const Center( + child: Text( + '稍后再说', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + height: 1.5, + color: Color(0xFFD4AF37), + ), + ), + ), + ), + ), + ), + ], + ); + } +}