From 532be9a5612527a3254568d5e6c68583ece08d7f Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 3 Mar 2026 02:58:34 -0800 Subject: [PATCH] =?UTF-8?q?fix(pre-planting):=20=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E6=98=BE=E7=A4=BA=E5=AE=9E=E9=99=85=E4=BB=BD?= =?UTF-8?q?=E6=95=B0=E5=92=8C=E9=87=91=E9=A2=9D=EF=BC=8C=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:合并详情页"合并份数"显示订单条数(7)而非实际份数(10), "总价值"硬编码 订单数×1887,每笔订单金额也硬编码 1,887。 修复: 后端 getMergeDetail: - 新增 sourceOrders[] 含每笔订单的 portionCount + totalAmount - 新增 totalPortions(总份数)和 totalAmount(总金额) 前端 PrePlantingMerge model: - 新增 MergeSourceOrder 类 - 新增 sourceOrders/totalPortions/totalAmount 字段 前端合并详情页: - "合并份数"用 totalPortions 替代 sourceOrderNos.length - "总价值"用 totalAmount 替代硬编码计算 - 来源订单列表显示每笔实际金额和份数(多份时显示"N份"前缀) Co-Authored-By: Claude Opus 4.6 --- .../pre-planting-application.service.ts | 22 +++ .../core/services/pre_planting_service.dart | 36 +++- .../pages/pre_planting_merge_detail_page.dart | 157 ++++++++++++------ 3 files changed, 166 insertions(+), 49 deletions(-) diff --git a/backend/services/planting-service/src/pre-planting/application/services/pre-planting-application.service.ts b/backend/services/planting-service/src/pre-planting/application/services/pre-planting-application.service.ts index 0bcf7c9f..46c7fbc5 100644 --- a/backend/services/planting-service/src/pre-planting/application/services/pre-planting-application.service.ts +++ b/backend/services/planting-service/src/pre-planting/application/services/pre-planting-application.service.ts @@ -397,6 +397,9 @@ export class PrePlantingApplicationService { ): Promise<{ mergeNo: string; sourceOrderNos: string[]; + sourceOrders: { orderNo: string; portionCount: number; totalAmount: number }[]; + totalPortions: number; + totalAmount: number; treeCount: number; contractStatus: string; contractSignedAt: Date | null; @@ -408,9 +411,28 @@ export class PrePlantingApplicationService { if (!merge || merge.userId !== userId) { throw new NotFoundException(`合并记录不存在: ${mergeNo}`); } + + // 查询源订单的份数和金额 + const orderRecords = await tx.prePlantingOrder.findMany({ + where: { orderNo: { in: merge.sourceOrderNos } }, + select: { orderNo: true, portionCount: true, totalAmount: true }, + orderBy: { createdAt: 'asc' }, + }); + + const sourceOrders = orderRecords.map((o) => ({ + orderNo: o.orderNo, + portionCount: o.portionCount, + totalAmount: Number(o.totalAmount), + })); + const totalPortions = sourceOrders.reduce((sum, o) => sum + o.portionCount, 0); + const totalAmount = sourceOrders.reduce((sum, o) => sum + o.totalAmount, 0); + return { mergeNo: merge.mergeNo, sourceOrderNos: merge.sourceOrderNos, + sourceOrders, + totalPortions, + totalAmount, treeCount: merge.treeCount, contractStatus: merge.contractStatus, contractSignedAt: merge.contractSignedAt, 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 1390cc26..d1241ce2 100644 --- a/frontend/mobile-app/lib/core/services/pre_planting_service.dart +++ b/frontend/mobile-app/lib/core/services/pre_planting_service.dart @@ -165,9 +165,33 @@ class PrePlantingOrder { } /// 预种合并记录(5 份 → 1 棵树) +/// 合并来源订单详情 +class MergeSourceOrder { + final String orderNo; + final int portionCount; + final double totalAmount; + + MergeSourceOrder({ + required this.orderNo, + required this.portionCount, + required this.totalAmount, + }); + + factory MergeSourceOrder.fromJson(Map json) { + return MergeSourceOrder( + orderNo: json['orderNo'] ?? '', + portionCount: json['portionCount'] ?? 1, + totalAmount: (json['totalAmount'] ?? 0).toDouble(), + ); + } +} + class PrePlantingMerge { final String mergeNo; - final List sourceOrderNos; // 5 笔来源订单号 + final List sourceOrderNos; // 来源订单号列表 + final List sourceOrders; // 来源订单详情(含份数和金额) + final int totalPortions; // 合并总份数(应为 10) + final double totalAmount; // 合并总金额(各订单实际支付之和) final int treeCount; // 合并产生的树数(固定 1) final PrePlantingContractStatus contractStatus; final DateTime? contractSignedAt; @@ -179,6 +203,9 @@ class PrePlantingMerge { PrePlantingMerge({ required this.mergeNo, required this.sourceOrderNos, + this.sourceOrders = const [], + this.totalPortions = 0, + this.totalAmount = 0, required this.treeCount, required this.contractStatus, this.contractSignedAt, @@ -195,12 +222,19 @@ class PrePlantingMerge { bool get isMiningEnabled => miningEnabledAt != null; factory PrePlantingMerge.fromJson(Map json) { + final sourceOrders = (json['sourceOrders'] as List?) + ?.map((e) => MergeSourceOrder.fromJson(e as Map)) + .toList() ?? + []; return PrePlantingMerge( mergeNo: json['mergeNo'] ?? '', sourceOrderNos: (json['sourceOrderNos'] as List?) ?.map((e) => e.toString()) .toList() ?? [], + sourceOrders: sourceOrders, + totalPortions: json['totalPortions'] ?? sourceOrders.fold(0, (sum, o) => sum + o.portionCount), + totalAmount: (json['totalAmount'] ?? sourceOrders.fold(0.0, (sum, o) => sum + o.totalAmount)).toDouble(), treeCount: json['treeCount'] ?? 1, contractStatus: _parseContractStatus(json['contractStatus']), contractSignedAt: json['contractSignedAt'] != null diff --git a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_merge_detail_page.dart b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_merge_detail_page.dart index 4754d1e5..decc72e1 100644 --- a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_merge_detail_page.dart +++ b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_merge_detail_page.dart @@ -320,11 +320,11 @@ class _PrePlantingMergeDetailPageState // 信息行 _buildDetailRow('合并时间', _formatDateTime(merge.mergedAt)), const SizedBox(height: 8), - _buildDetailRow('合并份数', '${merge.sourceOrderNos.length} 份 → 1 棵树'), + _buildDetailRow('合并份数', '${merge.totalPortions > 0 ? merge.totalPortions : merge.sourceOrderNos.length} 份 → 1 棵树'), const SizedBox(height: 8), _buildDetailRow( '总价值', - '${(merge.sourceOrderNos.length * 1887).toString()} 绿积分', + '${merge.totalAmount > 0 ? _formatNumber(merge.totalAmount) : _formatNumber(merge.sourceOrderNos.length * 1887.0)} 绿积分', ), if (merge.selectedProvince != null) ...[ const SizedBox(height: 8), @@ -458,7 +458,7 @@ class _PrePlantingMergeDetailPageState crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '来源订单(${merge.sourceOrderNos.length} 笔)', + '来源订单(${merge.sourceOrderNos.length} 笔,共 ${merge.totalPortions > 0 ? merge.totalPortions : merge.sourceOrderNos.length} 份)', style: const TextStyle( fontSize: 16, fontFamily: 'Inter', @@ -467,57 +467,104 @@ class _PrePlantingMergeDetailPageState ), ), const SizedBox(height: 12), - ...merge.sourceOrderNos.asMap().entries.map((entry) { - final index = entry.key; - final orderNo = entry.value; - return Padding( - padding: EdgeInsets.only( - bottom: index < merge.sourceOrderNos.length - 1 ? 8 : 0, - ), - child: Row( - children: [ - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: - const Color(0xFFD4AF37).withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(12), - ), - child: Center( - child: Text( - '${index + 1}', - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Color(0xFFD4AF37), + // 优先使用 sourceOrders(含份数和金额),降级用 sourceOrderNos + if (merge.sourceOrders.isNotEmpty) + ...merge.sourceOrders.asMap().entries.map((entry) { + final index = entry.key; + final order = entry.value; + return Padding( + padding: EdgeInsets.only( + bottom: index < merge.sourceOrders.length - 1 ? 8 : 0, + ), + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: + const Color(0xFFD4AF37).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Text( + '${index + 1}', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFFD4AF37), + ), ), ), ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - orderNo, - style: const TextStyle( - fontSize: 14, - fontFamily: 'Inter', - color: Color(0xFF5D4037), + const SizedBox(width: 8), + Expanded( + child: Text( + order.orderNo, + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + color: Color(0xFF5D4037), + ), + overflow: TextOverflow.ellipsis, ), - overflow: TextOverflow.ellipsis, ), - ), - const Text( - '1,887 绿积分', - style: TextStyle( - fontSize: 13, - color: Color(0xFF745D43), + Text( + '${order.portionCount > 1 ? "${order.portionCount}份 " : ""}${_formatNumber(order.totalAmount)} 绿积分', + style: const TextStyle( + fontSize: 13, + color: Color(0xFF745D43), + ), ), - ), - ], - ), - ); - }), + ], + ), + ); + }) + else + ...merge.sourceOrderNos.asMap().entries.map((entry) { + final index = entry.key; + final orderNo = entry.value; + return Padding( + padding: EdgeInsets.only( + bottom: index < merge.sourceOrderNos.length - 1 ? 8 : 0, + ), + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: + const Color(0xFFD4AF37).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Text( + '${index + 1}', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFFD4AF37), + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + orderNo, + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + color: Color(0xFF5D4037), + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + }), ], ), ); @@ -650,6 +697,20 @@ class _PrePlantingMergeDetailPageState ); } + /// 格式化金额(整数不带小数点,带千分位) + String _formatNumber(double value) { + if (value == value.truncateToDouble()) { + return value.toInt().toString().replaceAllMapped( + RegExp(r'(\d)(?=(\d{3})+(?!\d))'), + (m) => '${m[1]},', + ); + } + return value.toStringAsFixed(2).replaceAllMapped( + RegExp(r'(\d)(?=(\d{3})+(?!\d))'), + (m) => '${m[1]},', + ); + } + /// 格式化日期时间 String _formatDateTime(DateTime dt) { return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-'