feat(pre-planting): 合并后走正常签合同流程,购买第5份直接跳合并详情页

- pre_planting_service: CreatePrePlantingOrderResponse 增加 merged/mergeNo 字段
- pre_planting_purchase_page: 购买成功若触发合并,直接跳转合并详情签合同
- contract_check_service: 注入 PrePlantingService,checkAll 增加预种待签合并检查
- pending_contracts_page: 同时展示普通合同和预种合并待签卡片,复用现有签合同弹窗流程
- injection_container: contractCheckServiceProvider 注入 prePlantingService

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-28 09:51:21 -08:00
parent 26dcd1d2de
commit b9b23c36d7
5 changed files with 253 additions and 114 deletions

View File

@ -166,10 +166,14 @@ final contractSigningServiceProvider = Provider<ContractSigningService>((ref) {
return ContractSigningService(apiClient: apiClient);
});
// Contract Check Service Provider ()
// Contract Check Service Provider ()
final contractCheckServiceProvider = Provider<ContractCheckService>((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)

View File

@ -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<bool> 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<ContractCheckResult> 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<int> 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<String?> _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;
}
}
}

View File

@ -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<String, dynamic> 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(),
);
}
}

View File

@ -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<PendingContractsPage> {
List<ContractSigningTask> _unsignedTasks = [];
List<PrePlantingMerge> _pendingMerges = [];
bool _isLoading = true;
String? _errorMessage;
@ -38,18 +41,29 @@ class _PendingContractsPageState extends ConsumerState<PendingContractsPage> {
});
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<ContractSigningTask>;
final allMerges = results[1] as List<PrePlantingMerge>;
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<PendingContractsPage> {
}
}
///
///
Future<void> _signContract(ContractSigningTask task) async {
final result = await context.push<bool>(
'${RoutePaths.contractSigning}/${task.orderNo}',
@ -74,6 +88,13 @@ class _PendingContractsPageState extends ConsumerState<PendingContractsPage> {
}
}
///
Future<void> _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<PendingContractsPage> {
);
}
if (_unsignedTasks.isEmpty) {
if (_unsignedTasks.isEmpty && _pendingMerges.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@ -200,10 +221,12 @@ class _PendingContractsPageState extends ConsumerState<PendingContractsPage> {
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<PendingContractsPage> {
],
),
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<PendingContractsPage> {
);
}
///
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;

View File

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