Browse Source

fix: 退款金额使用 money/productNum 代替 price×quantity,消除 1 分钱舍入差

问题:
- haha 平台 ORDER 回调中 price 和 money 存在 1 分钱舍入差
  例:price=2.13, product_num=2 → price×2=4.26, 但 money=4.25(实付)
- 前端退款页和后台 RefundServiceImpl 均用 price×quantity 计算退款金额
  导致退 2 件时计算为 4.26 ≠ 实付 4.25,无法全额退款

修复:
- 前端 hook.tsx: 退款金额显示用 money/productNum×quantity 代替 price×quantity
- 前端 hook.tsx: 提交退款时传 money 和 productNum 给后端
- RefundDTO.RefundProductDTO: 新增 money、productNum 字段
- OrderController: 透传 money、productNum 到退款参数
- RefundServiceImpl.calculateRefundAmount: 优先用 money/productNum×quantity
- RefundServiceImpl.createRefundRecord: 退款明细金额同上逻辑

Co-Authored-By: Claude <noreply@anthropic.com>
skyline 2 tuần trước cách đây
mục cha
commit
e1dc2f8b21

+ 11 - 5
haha-admin-web/src/views/order/utils/hook.tsx

@@ -463,8 +463,8 @@ export function useOrder(tableRef: Ref) {
                     align="right"
                     v-slots={{
                       default: ({ row }: any) => {
-                        const subtotal = row.price && row.productNum ? (row.price * row.productNum) : 0;
-                        return <span style="font-weight: 500">¥{subtotal.toFixed(2)}</span>;
+                        const subtotal = row.money ? row.money : (row.price && row.productNum ? (row.price * row.productNum) : 0);
+                        return <span style="font-weight: 500">¥{Number(subtotal).toFixed(2)}</span>;
                       }
                     }}
                   />
@@ -508,7 +508,11 @@ export function useOrder(tableRef: Ref) {
       if (!refundState.orderDetail?.products) return 0;
       return refundState.selectedProducts.reduce((sum, i) => {
         const p = refundState.orderDetail.products[i];
-        return sum + (p.price || 0) * (refundState.refundQuantities[i] || 1);
+        const qty = refundState.refundQuantities[i] || 1;
+        // 以 money(行总计) / productNum(购买数量) 为实际单价,避免 ORDER 回调 price 的 1 分钱舍入差
+        // 例如: money=4.25, productNum=2 → 单价=2.125, 退2件=4.25, 而非 price(2.13)×2=4.26
+        const actualUnitPrice = p.productNum > 0 ? (p.money || 0) / p.productNum : (p.price || 0);
+        return sum + actualUnitPrice * qty;
       }, 0);
     });
 
@@ -624,7 +628,7 @@ export function useOrder(tableRef: Ref) {
                     default: ({ $index, row: r }: any) => (
                       refundState.selectedProducts.includes($index) ? (
                         <span style="color: #e6a23c; font-weight: 600; font-size: 14px;">
-                          ¥{((r.price || 0) * (refundState.refundQuantities[$index] || 1)).toFixed(2)}
+                          ¥{((r.productNum > 0 ? (r.money || 0) / r.productNum : (r.price || 0)) * (refundState.refundQuantities[$index] || 1)).toFixed(2)}
                         </span>
                       ) : <span style="color: #999">¥0.00</span>
                     )
@@ -709,7 +713,9 @@ export function useOrder(tableRef: Ref) {
               productId: p.productId || p.id,
               productName: p.productName,
               quantity: refundState.refundQuantities[i] || 1,
-              price: p.price || 0
+              price: p.price || 0,
+              money: p.money || 0,
+              productNum: p.productNum || 1
             };
           });
         } else if (refundState.customAmount && refundState.customAmount > 0) {

+ 4 - 0
haha-admin/src/main/java/com/haha/admin/controller/OrderController.java

@@ -125,6 +125,10 @@ public class OrderController {
                 m.put("productName", p.getProductName());
                 m.put("quantity", p.getQuantity());
                 m.put("price", p.getPrice());
+                // 传入 money 和 productNum 用于精确计算退款金额(money/productNum*quantity)
+                // 避免 price 的 1 分钱舍入差导致退款金额与实付金额不一致
+                if (p.getMoney() != null) m.put("money", p.getMoney());
+                if (p.getProductNum() != null) m.put("productNum", p.getProductNum());
                 return m;
             }).collect(Collectors.toList());
             params.put("products", productMaps);

+ 5 - 0
haha-entity/src/main/java/com/haha/entity/dto/RefundDTO.java

@@ -23,6 +23,11 @@ public class RefundDTO {
         private Long productId;
         private String productName;
         private Integer quantity;
+        /** 单价(haha平台返回,含1分钱舍入差) */
         private BigDecimal price;
+        /** 行总计金额(haha平台返回,无舍入差) */
+        private BigDecimal money;
+        /** 购买数量(用于计算实际单价 money/productNum) */
+        private Integer productNum;
     }
 }

+ 39 - 4
haha-service/src/main/java/com/haha/service/impl/RefundServiceImpl.java

@@ -147,7 +147,22 @@ public class RefundServiceImpl extends ServiceImpl<RefundMapper, Refund> impleme
                         ? Integer.valueOf(p.get("quantity").toString()) : 1);
                 item.setPrice(p.get("price") != null
                         ? new BigDecimal(p.get("price").toString()) : BigDecimal.ZERO);
-                item.setRefundAmount(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
+                // 精确计算退款金额:优先用 money/productNum*quantity,避免 price 的 1 分钱舍入差
+                if (p.get("money") != null && p.get("productNum") != null) {
+                    BigDecimal money = new BigDecimal(p.get("money").toString());
+                    int originalNum = Integer.parseInt(p.get("productNum").toString());
+                    int refundQty = item.getQuantity();
+                    if (originalNum > 0 && refundQty == originalNum) {
+                        item.setRefundAmount(money);
+                    } else if (originalNum > 0) {
+                        item.setRefundAmount(money.divide(BigDecimal.valueOf(originalNum), 4, BigDecimal.ROUND_HALF_UP)
+                                .multiply(BigDecimal.valueOf(refundQty)));
+                    } else {
+                        item.setRefundAmount(money);
+                    }
+                } else {
+                    item.setRefundAmount(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
+                }
                 item.setCreateTime(LocalDateTime.now());
                 items.add(item);
             }
@@ -299,11 +314,31 @@ public class RefundServiceImpl extends ServiceImpl<RefundMapper, Refund> impleme
         }
         BigDecimal total = BigDecimal.ZERO;
         for (Map<String, Object> p : products) {
-            BigDecimal price = p.get("price") != null
-                    ? new BigDecimal(p.get("price").toString()) : BigDecimal.ZERO;
             int quantity = p.get("quantity") != null
                     ? Integer.parseInt(p.get("quantity").toString()) : 1;
-            total = total.add(price.multiply(BigDecimal.valueOf(quantity)));
+            // 优先使用 money/productNum 计算实际单价,避免 haha 平台 price 的 1 分钱舍入差
+            // 例如: money=4.25, productNum=2, quantity=2 → 4.25(正确)
+            //       price=2.13, quantity=2 → 4.26(多1分钱)
+            BigDecimal itemAmount;
+            if (p.get("money") != null && p.get("productNum") != null) {
+                BigDecimal money = new BigDecimal(p.get("money").toString());
+                int originalNum = Integer.parseInt(p.get("productNum").toString());
+                if (originalNum > 0 && quantity == originalNum) {
+                    // 全量退款 → 直接用 money
+                    itemAmount = money;
+                } else if (originalNum > 0) {
+                    // 部分退款 → money / productNum × quantity
+                    itemAmount = money.divide(BigDecimal.valueOf(originalNum), 4, BigDecimal.ROUND_HALF_UP)
+                            .multiply(BigDecimal.valueOf(quantity));
+                } else {
+                    itemAmount = money;
+                }
+            } else {
+                BigDecimal price = p.get("price") != null
+                        ? new BigDecimal(p.get("price").toString()) : BigDecimal.ZERO;
+                itemAmount = price.multiply(BigDecimal.valueOf(quantity));
+            }
+            total = total.add(itemAmount);
         }
         return total;
     }