Просмотр исходного кода

fix(refund): 服务端重算退货退款金额

- 秒杀与拼团商品按订单活动价计算退款金额
- 普通商品按会员折扣、满减与红包比例分摊优惠

API: 退货退款申请不再采用客户端 price,以服务端计算结果为准
shizhongqi 1 день назад
Родитель
Сommit
f67f10f647

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

@@ -7,7 +7,6 @@ use bizHd\custom\classes\CustomClass;
 use bizHd\custom\classes\HdClass;
 use bizHd\distribution\classes\DistributionCommissionClass;
 use bizHd\hb\classes\HbClass;
-use bizHd\member\classes\MemberLevelClass;
 use bizHd\order\classes\OrderClass;
 use bizHd\refund\classes\HdNextDayRefundClass;
 use bizHd\refund\classes\HdRefundClass;
@@ -108,7 +107,6 @@ class RefundController extends BaseController
         $connection = Yii::$app->db;
         $transaction = $connection->beginTransaction();
         try {
-
             $order = OrderClass::getById($id, true);
             OrderClass::valid($order, $this->mainId);
             if ($order->payStatus == 0) {

+ 136 - 13
biz-hd/refund/services/MallRefundApplyService.php

@@ -3,6 +3,7 @@
  * 用途:商城顾客售后申请服务
  * 谁用:app-mall 提交/撤销/重新申请、app-hd 审核通过/驳回
  * 解决:申请只落待审核记录;撤销后可把已取消记录改回待审核;审核通过时复用 HdRefundService 执行真实退款
+ * 退货并退款金额由服务端重算:秒杀/团购按活动价,普通商品分摊会员折/满减/红包;不信任前端 price
  */
 
 namespace bizHd\refund\services;
@@ -34,17 +35,6 @@ class MallRefundApplyService extends BaseService
             util::fail('退款方式不正确');
         }
 
-        $refundPrice = $post['price'] ?? 0;
-        if (!is_numeric($refundPrice) || bccomp((string)$refundPrice, '0', 2) <= 0) {
-            util::fail('退款金额必须大于0');
-        }
-
-        // 可退上限:订单金额 - 已退累计
-        $couldRefund = bcsub((string)($order->orderPrice ?? 0), (string)($order->tkPrice ?? 0), 2);
-        if (bccomp((string)$refundPrice, $couldRefund, 2) === 1) {
-            util::fail('退款金额超过可退金额');
-        }
-
         $productList = [];
         if ($refundType === MallRefundApplyClass::REFUND_TYPE_MONEY_GOOD) {
             $productJson = $post['product'] ?? '';
@@ -57,6 +47,19 @@ class MallRefundApplyService extends BaseService
                 util::fail('请选择要退的商品');
             }
             $productList = self::validateAndEnrichProduct($productList, $order);
+            // 退货并退款由服务端按秒杀/团购活动价 + 普通商品分摊优惠重算,不信任前端 price
+            $refundPrice = self::calcReturnRefundPrice($productList, $order);
+        } else {
+            $refundPrice = $post['price'] ?? 0;
+        }
+        if (!is_numeric($refundPrice) || bccomp((string)$refundPrice, '0', 2) <= 0) {
+            util::fail('退款金额必须大于0');
+        }
+
+        // 可退上限:订单金额 - 已退累计
+        $couldRefund = bcsub((string)($order->orderPrice ?? 0), (string)($order->tkPrice ?? 0), 2);
+        if (bccomp((string)$refundPrice, $couldRefund, 2) === 1) {
+            util::fail('退款金额超过可退金额');
         }
 
         $data = [
@@ -134,10 +137,10 @@ class MallRefundApplyService extends BaseService
     }
 
     /**
-     * 校验退货数量不超过可退数量,并补齐商品名快照
+     * 校验退货数量不超过可退数量,并补齐商品名、订单行单价快照
      * @param array $productList [{productId,num,unitPrice,property,name?}] property 0商品 1花材
      * @param object $order
-     * @return array 补齐 name 后的列表
+     * @return array 补齐 name/unitPrice 后的列表
      */
     protected static function validateAndEnrichProduct($productList, $order)
     {
@@ -176,6 +179,9 @@ class MallRefundApplyService extends BaseService
                     util::fail("{$name}超过可退数量");
                 }
                 $row['name'] = $row['name'] ?? ($line->name ?? '');
+                // 单价以订单行为准:秒杀/团购行即活动价,普通行再分摊整单优惠
+                $row['unitPrice'] = $line->unitPrice ?? ($row['unitPrice'] ?? 0);
+                $row['seckillGoodsId'] = (int)($line->seckillGoodsId ?? 0);
                 // 封面写入快照,售后列表不必再回查订单行
                 if (empty($row['cover'])) {
                     $row['cover'] = $line->cover ?? '';
@@ -194,6 +200,7 @@ class MallRefundApplyService extends BaseService
                     util::fail("{$name}超过可退数量");
                 }
                 $row['name'] = $row['name'] ?? ($line->name ?? '');
+                $row['unitPrice'] = $line->unitPrice ?? ($row['unitPrice'] ?? 0);
                 if (empty($row['unitName'])) {
                     $row['unitName'] = $line->unitName ?? '';
                 }
@@ -206,6 +213,122 @@ class MallRefundApplyService extends BaseService
         return $enriched;
     }
 
+    /**
+     * 退货并退款应退金额:秒杀/团购按行上活动单价;普通商品再按整单优惠占比扣除会员折、满减、红包
+     * 会员折/满减/红包下单时打在非秒杀应付上,分摊基数要剔除整单活动商品原价
+     * @param array $productList 已用订单行单价、seckillGoodsId 补齐的勾选商品
+     * @param object $order xhOrder
+     * @return float 不超过可退上限的应退金额
+     */
+    protected static function calcReturnRefundPrice($productList, $order)
+    {
+        $isGroupBuy = (int)($order->groupBuyId ?? 0) > 0;
+        $selectedActivity = 0.0;
+        $selectedNormal = 0.0;
+        foreach ($productList as $row) {
+            $num = $row['num'] ?? 0;
+            $unitPrice = $row['unitPrice'] ?? 0;
+            if (!is_numeric($num) || !is_numeric($unitPrice)) {
+                continue;
+            }
+            $line = (float)$num * (float)$unitPrice;
+            // 拼团整单,或花束行带 seckillGoodsId:按活动价、不摊会员折/满减
+            if ($isGroupBuy || (int)($row['seckillGoodsId'] ?? 0) > 0) {
+                $selectedActivity += $line;
+            } else {
+                $selectedNormal += $line;
+            }
+        }
+
+        $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);
+        }
+
+        $activityOriginal = self::sumOrderActivityOriginal($order, $isGroupBuy);
+        $normalBase = (float)$allocBase - (float)$activityOriginal;
+        if ($normalBase < 0) {
+            $normalBase = 0;
+        }
+
+        $discountTotal = self::toMoney($order->discountAmount ?? 0);
+        $discountTotal = bcadd($discountTotal, self::toMoney($order->orderReachDiscountPrice ?? 0), 2);
+        $discountTotal = bcadd($discountTotal, $hbAmount, 2);
+
+        $normalPrice = $selectedNormal;
+        if ($selectedNormal > 0 && $normalBase > 0 && (float)$discountTotal > 0) {
+            // 与前端 toFixed(2) 同一套四舍五入
+            $normalPrice = $selectedNormal - ($selectedNormal * (float)$discountTotal / $normalBase);
+        }
+        $price = number_format(round($selectedActivity + $normalPrice, 2), 2, '.', '');
+        if (bccomp($price, '0', 2) < 0) {
+            $price = '0.00';
+        }
+
+        $couldRefund = bcsub(self::toMoney($order->orderPrice ?? 0), self::toMoney($order->tkPrice ?? 0), 2);
+        if (bccomp($couldRefund, '0', 2) < 0) {
+            $couldRefund = '0.00';
+        }
+        if (bccomp($price, $couldRefund, 2) === 1) {
+            $price = $couldRefund;
+        }
+        return round((float)$price, 2);
+    }
+
+    /**
+     * 整单秒杀/团购商品原价合计,供从优惠分摊基数中剔除
+     * @param object $order
+     * @param bool $isGroupBuy 拼团整单则商品总额都是活动价
+     * @return float
+     */
+    protected static function sumOrderActivityOriginal($order, $isGroupBuy)
+    {
+        if ($isGroupBuy) {
+            return (float)self::toMoney($order->goodsPrice ?? 0);
+        }
+        $orderSn = $order->orderSn ?? '';
+        if ($orderSn === '') {
+            return 0.0;
+        }
+        $goodsRows = \bizHd\order\classes\OrderGoodsClass::getAllByCondition(['orderSn' => $orderSn]) ?: [];
+        $total = 0.0;
+        foreach ($goodsRows as $goods) {
+            $seckillGoodsId = is_array($goods)
+                ? (int)($goods['seckillGoodsId'] ?? 0)
+                : (int)($goods->seckillGoodsId ?? 0);
+            if ($seckillGoodsId <= 0) {
+                continue;
+            }
+            $num = is_array($goods) ? ($goods['num'] ?? 0) : ($goods->num ?? 0);
+            $unitPrice = is_array($goods) ? ($goods['unitPrice'] ?? 0) : ($goods->unitPrice ?? 0);
+            if (!is_numeric($num) || !is_numeric($unitPrice)) {
+                continue;
+            }
+            $total += (float)$num * (float)$unitPrice;
+        }
+        return round($total, 2);
+    }
+
+    /**
+     * 金额转两位小数字符串,供 bcmath 分摊,避免浮点误差
+     * @param mixed $value
+     * @return string
+     */
+    protected static function toMoney($value)
+    {
+        if ($value === null || $value === '' || !is_numeric($value)) {
+            return '0.00';
+        }
+        return number_format((float)$value, 2, '.', '');
+    }
+
     /**
      * 商家审核通过:执行真实退款 + 红包按过期退回 + 消费回退 + 分销重算
      * @param object $apply MallRefundApply