diff --git a/frontend/mobile-app/lib/core/di/injection_container.dart b/frontend/mobile-app/lib/core/di/injection_container.dart index 4dd3fb78..bdb0ceff 100644 --- a/frontend/mobile-app/lib/core/di/injection_container.dart +++ b/frontend/mobile-app/lib/core/di/injection_container.dart @@ -166,10 +166,14 @@ final contractSigningServiceProvider = Provider((ref) { return ContractSigningService(apiClient: apiClient); }); -// Contract Check Service Provider (用于启动时检查未签署合同) +// Contract Check Service Provider (用于启动时检查未签署合同,含预种待签合并) final contractCheckServiceProvider = Provider((ref) { final contractSigningService = ref.watch(contractSigningServiceProvider); - return ContractCheckService(contractSigningService: contractSigningService); + final prePlantingService = ref.watch(prePlantingServiceProvider); + return ContractCheckService( + contractSigningService: contractSigningService, + prePlantingService: prePlantingService, + ); }); // KYC Service Provider (调用 identity-service) 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 eefa60b6..be66b3f6 100644 --- a/frontend/mobile-app/lib/core/services/contract_check_service.dart +++ b/frontend/mobile-app/lib/core/services/contract_check_service.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'contract_signing_service.dart'; +import 'pre_planting_service.dart'; /// 合同检查结果 class ContractCheckResult { @@ -15,11 +16,15 @@ class ContractCheckResult { /// 已付款但未完成 KYC 的订单数量 final int paidOrderCount; + /// 待签署的预种合并合同编号(有则跳到合并详情页) + final String? pendingPrePlantingMergeNo; + ContractCheckResult({ required this.hasPendingContracts, required this.requiresKyc, this.kycMessage, this.paidOrderCount = 0, + this.pendingPrePlantingMergeNo, }); /// 是否需要强制操作(有待签署合同或需要 KYC) @@ -30,21 +35,23 @@ class ContractCheckResult { /// 用于在 App 启动时检查用户是否有未签署的合同或需要完成 KYC class ContractCheckService { final ContractSigningService _contractSigningService; + final PrePlantingService _prePlantingService; ContractCheckService({ required ContractSigningService contractSigningService, - }) : _contractSigningService = contractSigningService; + required PrePlantingService prePlantingService, + }) : _contractSigningService = contractSigningService, + _prePlantingService = prePlantingService; - /// 检查是否有待签署的合同 + /// 检查是否有待签署的合同(含预种合并合同) /// 返回 true 表示有待签署合同,需要强制签署 Future hasPendingContracts() async { try { - // 获取未签署任务(包括待签署和超时未签署的) final unsignedTasks = await _contractSigningService.getUnsignedTasks(); - return unsignedTasks.isNotEmpty; + if (unsignedTasks.isNotEmpty) return true; + final mergeNo = await _getPendingPrePlantingMergeNo(); + return mergeNo != null; } catch (e) { - // debugPrint('[ContractCheckService] 检查待签署合同失败: $e'); - // 检查失败时不阻止用户使用 App return false; } } @@ -52,23 +59,29 @@ class ContractCheckService { /// 综合检查:待签署合同和 KYC 需求 /// /// 返回检查结果,包含: - /// - 是否有待签署的合同 + /// - 是否有待签署的合同(含预种合并合同) /// - 是否需要先完成 KYC(有已付款订单但未完成 KYC) Future checkAll() async { try { - // 1. 检查未签署的合同 + // 1. 检查普通认种未签署合同 final unsignedTasks = await _contractSigningService.getUnsignedTasks(); - final hasPending = unsignedTasks.isNotEmpty; - - // 2. 如果有待签署的合同,直接返回(不需要再检查 KYC) - if (hasPending) { - // debugPrint('[ContractCheckService] 检测到 ${unsignedTasks.length} 个待签署合同'); + if (unsignedTasks.isNotEmpty) { return ContractCheckResult( hasPendingContracts: true, requiresKyc: false, ); } + // 2. 检查预种待签合并合同 + final pendingMergeNo = await _getPendingPrePlantingMergeNo(); + if (pendingMergeNo != null) { + return ContractCheckResult( + hasPendingContracts: true, + requiresKyc: false, + pendingPrePlantingMergeNo: pendingMergeNo, + ); + } + // 3. 没有待签署合同,检查是否有需要 KYC 的情况 final kycResult = await _contractSigningService.checkKycRequirement(); @@ -79,8 +92,6 @@ class ContractCheckService { paidOrderCount: kycResult.paidOrderCount, ); } catch (e) { - // debugPrint('[ContractCheckService] 综合检查失败: $e'); - // 检查失败时不阻止用户使用 App return ContractCheckResult( hasPendingContracts: false, requiresKyc: false, @@ -92,10 +103,24 @@ class ContractCheckService { Future getPendingContractCount() async { try { final unsignedTasks = await _contractSigningService.getUnsignedTasks(); - return unsignedTasks.length; + if (unsignedTasks.isNotEmpty) return unsignedTasks.length; + final mergeNo = await _getPendingPrePlantingMergeNo(); + return mergeNo != null ? 1 : 0; } catch (e) { - // debugPrint('[ContractCheckService] 获取待签署合同数量失败: $e'); return 0; } } + + /// 查找第一个待签署的预种合并合同编号 + Future _getPendingPrePlantingMergeNo() async { + try { + final merges = await _prePlantingService.getMerges(); + final pending = merges.where( + (m) => m.contractStatus == PrePlantingContractStatus.pending, + ); + return pending.isEmpty ? null : pending.first.mergeNo; + } catch (e) { + return null; + } + } } diff --git a/frontend/mobile-app/lib/core/services/pre_planting_service.dart b/frontend/mobile-app/lib/core/services/pre_planting_service.dart index 2cfe440a..9b4ab55b 100644 --- a/frontend/mobile-app/lib/core/services/pre_planting_service.dart +++ b/frontend/mobile-app/lib/core/services/pre_planting_service.dart @@ -230,12 +230,16 @@ class CreatePrePlantingOrderResponse { final int portionCount; final double totalAmount; final double pricePerPortion; + final bool merged; + final String? mergeNo; CreatePrePlantingOrderResponse({ required this.orderNo, required this.portionCount, required this.totalAmount, required this.pricePerPortion, + this.merged = false, + this.mergeNo, }); factory CreatePrePlantingOrderResponse.fromJson(Map json) { @@ -244,6 +248,8 @@ class CreatePrePlantingOrderResponse { portionCount: json['portionCount'] ?? 1, totalAmount: (json['totalAmount'] ?? 3566).toDouble(), pricePerPortion: (json['pricePerPortion'] ?? 3566).toDouble(), + merged: json['merged'] == true, + mergeNo: json['mergeNo']?.toString(), ); } } diff --git a/frontend/mobile-app/lib/features/contract_signing/presentation/pages/pending_contracts_page.dart b/frontend/mobile-app/lib/features/contract_signing/presentation/pages/pending_contracts_page.dart index 120f1bf3..feb5438e 100644 --- a/frontend/mobile-app/lib/features/contract_signing/presentation/pages/pending_contracts_page.dart +++ b/frontend/mobile-app/lib/features/contract_signing/presentation/pages/pending_contracts_page.dart @@ -3,10 +3,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/di/injection_container.dart'; import '../../../../core/services/contract_signing_service.dart'; +import '../../../../core/services/pre_planting_service.dart'; import '../../../../routes/route_paths.dart'; /// 待签署合同列表页面 /// 用于 App 启动时检查并强制用户签署未完成的合同 +/// 包含:普通认种合同 + 预种合并合同 class PendingContractsPage extends ConsumerStatefulWidget { /// 是否为强制模式(不允许跳过) final bool forceSign; @@ -22,6 +24,7 @@ class PendingContractsPage extends ConsumerStatefulWidget { class _PendingContractsPageState extends ConsumerState { List _unsignedTasks = []; + List _pendingMerges = []; bool _isLoading = true; String? _errorMessage; @@ -38,18 +41,29 @@ class _PendingContractsPageState extends ConsumerState { }); try { - final service = ref.read(contractSigningServiceProvider); + final contractService = ref.read(contractSigningServiceProvider); + final prePlantingService = ref.read(prePlantingServiceProvider); - // 获取所有未签署任务(包括待签署和超时未签署) - final tasks = await service.getUnsignedTasks(); + // 并行加载:普通认种未签合同 + 预种待签合并 + final results = await Future.wait([ + contractService.getUnsignedTasks(), + prePlantingService.getMerges(), + ]); + + final tasks = results[0] as List; + final allMerges = results[1] as List; + final pendingMerges = allMerges + .where((m) => m.contractStatus == PrePlantingContractStatus.pending) + .toList(); setState(() { _unsignedTasks = tasks; + _pendingMerges = pendingMerges; _isLoading = false; }); - // 如果没有待签署任务,自动返回 - if (_unsignedTasks.isEmpty) { + // 如果没有任何待签署任务,自动返回 + if (_unsignedTasks.isEmpty && _pendingMerges.isEmpty) { if (mounted) { context.pop(true); } @@ -62,7 +76,7 @@ class _PendingContractsPageState extends ConsumerState { } } - /// 签署合同 + /// 签署普通认种合同 Future _signContract(ContractSigningTask task) async { final result = await context.push( '${RoutePaths.contractSigning}/${task.orderNo}', @@ -74,6 +88,13 @@ class _PendingContractsPageState extends ConsumerState { } } + /// 签署预种合并合同(跳转到合并详情页) + Future _signPrePlantingMerge(PrePlantingMerge merge) async { + await context.push('${RoutePaths.prePlantingMergeDetail}/${merge.mergeNo}'); + // 返回后刷新,合并可能已签署 + _loadTasks(); + } + /// 跳过(仅非强制模式) void _skip() { if (!widget.forceSign) { @@ -177,7 +198,7 @@ class _PendingContractsPageState extends ConsumerState { ); } - if (_unsignedTasks.isEmpty) { + if (_unsignedTasks.isEmpty && _pendingMerges.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -200,10 +221,12 @@ class _PendingContractsPageState extends ConsumerState { return RefreshIndicator( onRefresh: _loadTasks, color: const Color(0xFFD4AF37), - child: ListView.builder( + child: ListView( padding: const EdgeInsets.all(16), - itemCount: _unsignedTasks.length, - itemBuilder: (context, index) => _buildTaskCard(_unsignedTasks[index]), + children: [ + ..._unsignedTasks.map((task) => _buildTaskCard(task)), + ..._pendingMerges.map((merge) => _buildMergeCard(merge)), + ], ), ); } @@ -289,95 +312,34 @@ class _PendingContractsPageState extends ConsumerState { ], ), const SizedBox(height: 12), - // 状态提示 if (isTimeout) - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFFFFEBEE), - borderRadius: BorderRadius.circular(8), - ), - child: const Row( - children: [ - Icon(Icons.warning_amber_rounded, color: Color(0xFFE53935), size: 16), - SizedBox(width: 8), - Expanded( - child: Text( - '签署已超时,请尽快补签', - style: TextStyle( - fontSize: 12, - color: Color(0xFFE53935), - ), - ), - ), - ], - ), + _buildStatusHint( + color: const Color(0xFFFFEBEE), + icon: Icons.warning_amber_rounded, + iconColor: const Color(0xFFE53935), + text: '签署已超时,请尽快补签', + textColor: const Color(0xFFE53935), ) else if (isUrgent) - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFFFFF3E0), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - const Icon(Icons.access_time, color: Color(0xFFFF9800), size: 16), - const SizedBox(width: 8), - Expanded( - child: Text( - '剩余 ${_formatRemainingTime(task.remainingSeconds)}', - style: const TextStyle( - fontSize: 12, - color: Color(0xFFE65100), - ), - ), - ), - ], - ), + _buildStatusHint( + color: const Color(0xFFFFF3E0), + icon: Icons.access_time, + iconColor: const Color(0xFFFF9800), + text: '剩余 ${_formatRemainingTime(task.remainingSeconds)}', + textColor: const Color(0xFFE65100), ) else - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFFE3F2FD), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - const Icon(Icons.timer, color: Color(0xFF1976D2), size: 16), - const SizedBox(width: 8), - Expanded( - child: Text( - '剩余 ${_formatRemainingTime(task.remainingSeconds)}', - style: const TextStyle( - fontSize: 12, - color: Color(0xFF1976D2), - ), - ), - ), - ], - ), + _buildStatusHint( + color: const Color(0xFFE3F2FD), + icon: Icons.timer, + iconColor: const Color(0xFF1976D2), + text: '剩余 ${_formatRemainingTime(task.remainingSeconds)}', + textColor: const Color(0xFF1976D2), ), const SizedBox(height: 12), - // 签署按钮 - Container( - width: double.infinity, - height: 44, - decoration: BoxDecoration( - color: isTimeout ? const Color(0xFFE53935) : const Color(0xFFD4AF37), - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: Text( - isTimeout ? '立即补签' : '立即签署', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), + _buildSignButton( + label: isTimeout ? '立即补签' : '立即签署', + color: isTimeout ? const Color(0xFFE53935) : const Color(0xFFD4AF37), ), ], ), @@ -386,6 +348,142 @@ class _PendingContractsPageState extends ConsumerState { ); } + /// 预种合并待签卡片 + Widget _buildMergeCard(PrePlantingMerge merge) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + border: Border.all(color: const Color(0xFF4CAF50), width: 1), + ), + child: InkWell( + onTap: () => _signPrePlantingMerge(merge), + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '合并编号: ${merge.mergeNo}', + style: const TextStyle( + fontSize: 14, + color: Color(0xFF666666), + ), + ), + const SizedBox(height: 4), + Text( + '${merge.treeCount} 棵预种合并树', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF5D4037), + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFE8F5E9), + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + '待签署', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF2E7D32), + ), + ), + ), + ], + ), + const SizedBox(height: 12), + _buildStatusHint( + color: const Color(0xFFE8F5E9), + icon: Icons.park_outlined, + iconColor: const Color(0xFF4CAF50), + text: '5 份预种份额已合并,请签署合同以开启挖矿', + textColor: const Color(0xFF2E7D32), + ), + const SizedBox(height: 12), + _buildSignButton( + label: '立即签署', + color: const Color(0xFF4CAF50), + ), + ], + ), + ), + ), + ); + } + + Widget _buildStatusHint({ + required Color color, + required IconData icon, + required Color iconColor, + required String text, + required Color textColor, + }) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon(icon, color: iconColor, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + text, + style: TextStyle(fontSize: 12, color: textColor), + ), + ), + ], + ), + ); + } + + Widget _buildSignButton({required String label, required Color color}) { + return Container( + width: double.infinity, + height: 44, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + label, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ); + } + Widget _buildStatusBadge(ContractSigningTask task) { Color bgColor; Color textColor; diff --git a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_purchase_page.dart b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_purchase_page.dart index e6177a97..5bd3a4ca 100644 --- a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_purchase_page.dart +++ b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_purchase_page.dart @@ -368,7 +368,8 @@ class _PrePlantingPurchasePageState debugPrint( '[PrePlantingPurchase] 购买成功: orderNo=${response.orderNo}, ' - 'portions=${response.portionCount}, total=${response.totalAmount}', + 'portions=${response.portionCount}, total=${response.totalAmount}, ' + 'merged=${response.merged}, mergeNo=${response.mergeNo}', ); if (mounted) { @@ -382,8 +383,13 @@ class _PrePlantingPurchasePageState ), ); - // 返回上一页(Profile 页面会自动刷新) - context.pop(true); + // 如果触发了合并(5 份凑满 1 棵树),直接跳转到合并详情页签合同 + if (response.merged && response.mergeNo != null) { + context.push('${RoutePaths.prePlantingMergeDetail}/${response.mergeNo}'); + } else { + // 返回上一页(Profile 页面会自动刷新) + context.pop(true); + } } } catch (e, stack) { debugPrint('[PrePlantingPurchase] ★ 购买失败 type=${e.runtimeType}: $e');