Quellcode durchsuchen

fix(refund): 修正商城售后优惠分摊与红包退回

- 将跑腿费排除在会员折、满减和红包的退款分摊基数之外
- 审核通过后按现金退清或商品全部退完自动退回未过期红包

API: 商城售后审核通过不再依赖前端 refundHb 参数
Database: 无结构变更;满足退回条件时更新订单红包占用状态
shizhongqi vor 6 Stunden
Ursprung
Commit
d2219b84f0
2 geänderte Dateien mit 116 neuen und 16 gelöschten Zeilen
  1. 2 1
      app-hd/controllers/RefundController.php
  2. 114 15
      biz-hd/refund/classes/HdRefundMallClass.php

+ 2 - 1
app-hd/controllers/RefundController.php

@@ -416,7 +416,8 @@ class RefundController extends BaseController
     }
 
     /**
-     * 商城顾客售后审核通过:执行真实退款
+     * 商城顾客售后审核通过:独立于商家即时售后(不读 refundHb)
+     * 执行真实退款后由 HdRefundMallClass::approve 自行判断是否全额/商品退完并退回红包
      * 成功返回资金落地字段,供前端跳转 forwardResult(线下选手选 / 已返充凭证)
      */
     public function actionPass()

+ 114 - 15
biz-hd/refund/classes/HdRefundMallClass.php

@@ -3,6 +3,8 @@
  * 用途:商城顾客售后申请(落 xhHdRefund 待审核)
  * 谁用:app-mall RefundController 提交/列表/撤销、app-hd RefundController 审核通过/驳回
  * 解决:申请只落待审核记录不动资金库存;审核通过时复用 HdRefundService 执行真实退款
+ * 退货并退款金额:普通商品按勾选原价分摊会员折/满减/红包(跑腿费不参与分摊);秒杀、团购按活动单价退
+ * 审核通过后退红包:现金已退清(含隔天 nextDayTkPrice)或商品/花材已全部退完,不依赖前端 refundHb
  */
 
 namespace bizHd\refund\classes;
@@ -319,6 +321,7 @@ class HdRefundMallClass
 
     /**
      * 退货并退款应退金额:秒杀/团购按行上活动单价;普通商品再按整单优惠占比扣除会员折、满减、红包
+     * 跑腿费不参与分摊,与 mallApp refund.vue、下单 wholeDiscount 口径一致
      * @param array $productList 已用订单行单价、seckillGoodsId 补齐的勾选商品
      * @param object $order xhOrder
      * @return float 不超过可退上限的应退金额
@@ -342,18 +345,7 @@ class HdRefundMallClass
             }
         }
 
-        $hbAmount = self::toMoney($order->hbAmount ?? 0);
-        $prePrice = self::toMoney($order->prePrice ?? 0);
-        if (bccomp($prePrice, '0', 2) > 0) {
-            $allocBase = bcadd($prePrice, $hbAmount, 2);
-        } else {
-            $allocBase = self::toMoney($order->goodsPrice ?? 0);
-            $allocBase = bcadd($allocBase, self::toMoney($order->sendCost ?? 0), 2);
-            $allocBase = bcadd($allocBase, self::toMoney($order->packingFee ?? 0), 2);
-            $allocBase = bcadd($allocBase, self::toMoney($order->labourCost ?? 0), 2);
-            $allocBase = bcadd($allocBase, self::toMoney($order->serviceFee ?? 0), 2);
-        }
-
+        $allocBase = self::getDiscountAllocBase($order);
         $activityOriginal = self::sumOrderActivityOriginal($order, $isGroupBuy);
         $normalBase = (float)$allocBase - (float)$activityOriginal;
         if ($normalBase < 0) {
@@ -362,7 +354,7 @@ class HdRefundMallClass
 
         $discountTotal = self::toMoney($order->discountAmount ?? 0);
         $discountTotal = bcadd($discountTotal, self::toMoney($order->orderReachDiscountPrice ?? 0), 2);
-        $discountTotal = bcadd($discountTotal, $hbAmount, 2);
+        $discountTotal = bcadd($discountTotal, self::toMoney($order->hbAmount ?? 0), 2);
 
         $normalPrice = $selectedNormal;
         if ($selectedNormal > 0 && $normalBase > 0 && (float)$discountTotal > 0) {
@@ -380,6 +372,49 @@ class HdRefundMallClass
         return round((float)$price, 2);
     }
 
+    /**
+     * 跑腿费(sendType=2 的 sendCost):下单时红包不抵运费、会员折/满减也不打在跑腿费上
+     * 退货分摊时要从基数里剔除,否则会把优惠摊到跑腿费上,商品应退被抬高
+     * @param object $order xhOrder
+     * @return string 两位小数字符串
+     */
+    protected static function getRunnerSendCost($order)
+    {
+        if ((int)($order->sendType ?? 0) !== 2) {
+            return '0.00';
+        }
+        return self::toMoney($order->sendCost ?? 0);
+    }
+
+    /**
+     * 整单优惠分摊基数:红包只抵商品;会员折/满减按「商品+非跑腿运费/包装-红包」
+     * prePrice 已扣红包且含跑腿费,这里加回红包再剔除跑腿费,与下单 wholeDiscount 口径一致
+     * @param object $order xhOrder
+     * @return string 两位小数字符串
+     */
+    protected static function getDiscountAllocBase($order)
+    {
+        $hbAmount = self::toMoney($order->hbAmount ?? 0);
+        $prePrice = self::toMoney($order->prePrice ?? 0);
+        $runnerCost = self::getRunnerSendCost($order);
+        if (bccomp($prePrice, '0', 2) > 0) {
+            $base = bcsub(bcadd($prePrice, $hbAmount, 2), $runnerCost, 2);
+            if (bccomp($base, '0', 2) < 0) {
+                return '0.00';
+            }
+            return $base;
+        }
+        $allocBase = self::toMoney($order->goodsPrice ?? 0);
+        // 非跑腿才把运费计入分摊基数;跑腿费已在 getRunnerSendCost 口径下排除
+        if ((int)($order->sendType ?? 0) !== 2) {
+            $allocBase = bcadd($allocBase, self::toMoney($order->sendCost ?? 0), 2);
+        }
+        $allocBase = bcadd($allocBase, self::toMoney($order->packingFee ?? 0), 2);
+        $allocBase = bcadd($allocBase, self::toMoney($order->labourCost ?? 0), 2);
+        $allocBase = bcadd($allocBase, self::toMoney($order->serviceFee ?? 0), 2);
+        return $allocBase;
+    }
+
     /**
      * 整单秒杀/团购商品原价合计,供从优惠分摊基数中剔除
      * @param object $order
@@ -601,14 +636,78 @@ class HdRefundMallClass
             }
         }
 
-        $updatedOrder = OrderClass::getById($order->id, true, 'id, actPrice, hbId, hbAmount');
-        if (!empty($updatedOrder) && (int)$updatedOrder->hbId > 0 && bccomp((string)$updatedOrder->actPrice, '0', 2) <= 0) {
+        $updatedOrder = OrderClass::getById($order->id, true);
+        if (self::shouldReturnHbAfterApprove($updatedOrder)) {
             HbClass::hbBackIfNotExpired($updatedOrder);
         }
 
         return $refund;
     }
 
+    /**
+     * 审核通过后是否退回红包。与商家端即时售后的 refundHb 勾选无关,由服务端自行判断:
+     * 1)现金已退清:remainRefundCash=0(当天 tkPrice + 隔天 nextDayTkPrice >= orderPrice)
+     *    不能只看 actPrice——隔天售后只加 nextDayTkPrice、不扣 actPrice,全额退完 actPrice 仍可能 >0
+     * 2)商品/花材已全部退完:红包只抵商品,退货并退款退光商品后即使还留跑腿费也应退红包
+     * @param object|null $order 审核写完金额后的订单
+     * @return bool
+     */
+    protected static function shouldReturnHbAfterApprove($order)
+    {
+        if (empty($order) || (int)($order->hbId ?? 0) <= 0) {
+            return false;
+        }
+        if (bccomp(self::remainRefundCash($order), '0', 2) === 0) {
+            return true;
+        }
+        return self::isAllOrderProductsReturned($order);
+    }
+
+    /**
+     * 订单商品和花材是否都已退完(refundNum >= num)
+     * 只退款不写退货数量时返回 false,避免把部分退款误判成全额退货
+     * @param object $order
+     * @return bool
+     */
+    protected static function isAllOrderProductsReturned($order)
+    {
+        $orderSn = trim((string)($order->orderSn ?? ''));
+        if ($orderSn === '') {
+            return false;
+        }
+        $goodsRows = OrderGoodsClass::getAllByCondition(['orderSn' => $orderSn]) ?: [];
+        $itemRows = OrderItemClass::getAllByCondition(['orderSn' => $orderSn]) ?: [];
+        if (empty($goodsRows) && empty($itemRows)) {
+            return false;
+        }
+        foreach ($goodsRows as $goods) {
+            if (!self::isLineFullyReturned($goods)) {
+                return false;
+            }
+        }
+        foreach ($itemRows as $item) {
+            if (!self::isLineFullyReturned($item)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 单行是否已退完数量
+     * @param array|object $line 订单商品或花材行
+     * @return bool
+     */
+    protected static function isLineFullyReturned($line)
+    {
+        $num = is_array($line) ? ($line['num'] ?? 0) : ($line->num ?? 0);
+        $refundNum = is_array($line) ? ($line['refundNum'] ?? 0) : ($line->refundNum ?? 0);
+        if (!is_numeric($num) || bccomp((string)$num, '0', 2) <= 0) {
+            return true;
+        }
+        return bccomp((string)$refundNum, (string)$num, 2) >= 0;
+    }
+
     /**
      * 商家驳回申请
      * @param object $refund