Browse Source

Merge remote-tracking branch 'origin/redesign‌-260706' into redesign‌-260706

ouyang 1 day ago
parent
commit
40df6e1eb7
40 changed files with 809 additions and 208 deletions
  1. 4 4
      app-ghs/controllers/CgRefundController.php
  2. 2 2
      app-ghs/controllers/ConsoleController.php
  3. 1 1
      app-ghs/controllers/CustomController.php
  4. 4 4
      app-ghs/controllers/OrderController.php
  5. 2 2
      app-ghs/controllers/PurchaseOrderController.php
  6. 4 4
      app-ghs/controllers/RefundController.php
  7. 3 3
      app-ghs/controllers/StatCgGhsController.php
  8. 28 9
      app-hd/controllers/CgRefundController.php
  9. 4 3
      app-hd/controllers/CustomController.php
  10. 1 1
      app-hd/controllers/OrderController.php
  11. 37 4
      app-hd/controllers/RefundController.php
  12. 5 1
      app-mall/controllers/RefundController.php
  13. 7 7
      biz-ghs/cg/classes/CgPurchaseNextDayClass.php
  14. 2 2
      biz-ghs/cg/services/CgRefundService.php
  15. 19 9
      biz-ghs/custom/classes/CustomClass.php
  16. 39 6
      biz-ghs/custom/services/GhsRechargeSettleService.php
  17. 5 5
      biz-ghs/order/classes/NextDayRefundClass.php
  18. 12 3
      biz-ghs/order/classes/OrderClass.php
  19. 3 3
      biz-ghs/order/classes/OrderClearClass.php
  20. 52 11
      biz-ghs/order/classes/PurchaseOrderClass.php
  21. 3 3
      biz-ghs/order/classes/RefundOrderClass.php
  22. 1 1
      biz-ghs/order/models/OrderItem.php
  23. 6 1
      biz-ghs/order/models/PurchaseOrder.php
  24. 1 1
      biz-ghs/order/models/PurchaseOrderItem.php
  25. 1 1
      biz-ghs/order/services/OrderService.php
  26. 5 5
      biz-ghs/order/services/RefundOrderService.php
  27. 4 4
      biz-ghs/stat/classes/StatCgClass.php
  28. 3 3
      biz-ghs/stat/classes/StatKdClass.php
  29. 11 11
      biz-ghs/stat/classes/StatSaleClass.php
  30. 3 3
      biz-hd/cg/classes/CgRefundClass.php
  31. 180 12
      biz-hd/custom/classes/CustomClass.php
  32. 51 7
      biz-hd/homePageConfig/classes/HomePageDisplayClass.php
  33. 5 5
      biz-hd/order/classes/OrderClass.php
  34. 2 2
      biz-hd/purchase/models/PurchaseItem.php
  35. 52 12
      biz-hd/refund/classes/HdNextDayRefundClass.php
  36. 2 2
      biz-hd/refund/classes/HdRefundClass.php
  37. 204 34
      biz-hd/refund/classes/HdRefundMallClass.php
  38. 39 15
      biz-hd/refund/services/HdRefundService.php
  39. 1 1
      biz-hd/shop/classes/ShopTemplateClass.php
  40. 1 1
      biz-mall/order/services/OrderService.php

+ 4 - 4
app-ghs/controllers/CgRefundController.php

@@ -17,7 +17,7 @@ class CgRefundController extends BaseController
 {
 
     /**
-     * 采购原单是否可走天售后(售后页预检)
+     * 采购原单是否可走天售后(售后页预检)
      * GET: orderId
      */
     public function actionCheckNextDayEligible()
@@ -111,7 +111,7 @@ class CgRefundController extends BaseController
         if ($status > -1) {
             $where['status'] = $status;
         }
-        // sameDay:-1全部 1当天售后 0天售后(与销售售后列表对齐)
+        // sameDay:-1全部 1当天售后 0天售后(与销售售后列表对齐)
         $sameDay = isset($get['sameDay']) ? intval($get['sameDay']) : -1;
         if ($sameDay === 0 || $sameDay === 1) {
             $where['sameDay'] = $sameDay;
@@ -157,7 +157,7 @@ class CgRefundController extends BaseController
         util::success($respond);
     }
 
-    //退款 ssh 20220909;支持天售后(sameDay=0,不减 actPrice)
+    //退款 ssh 20220909;支持天售后(sameDay=0,不减 actPrice)
     public function actionCreateOrder()
     {
 
@@ -238,7 +238,7 @@ class CgRefundController extends BaseController
                 $transaction->rollBack();
             }
             Yii::info("退款申请出错了,报错信息:" . $exception->getMessage());
-            // 透出业务失败原因,便于天售后排错
+            // 透出业务失败原因,便于天售后排错
             util::fail($exception->getMessage() ?: '操作失败');
         }
     }

+ 2 - 2
app-ghs/controllers/ConsoleController.php

@@ -322,8 +322,8 @@ class ConsoleController extends BaseController
 
         $warning = '';
         $warningUrl = '';
-        $warning = '挂账结清,支持售后了(重大更新,必看)';
-        $warningUrl = '/admin/notice/warning';
+        //$warning = '挂账结清,支持售后了(重大更新,必看)';
+        //$warningUrl = '/admin/notice/warning';
 
         util::success([
             'asset' => $this->main,

+ 1 - 1
app-ghs/controllers/CustomController.php

@@ -120,7 +120,7 @@ class CustomController extends BaseController
     }
 
     /**
-     * 客户向供货商充值的小程序码(天售后返余额海报用)
+     * 客户向供货商充值的小程序码(天售后返余额海报用)
      * GET: id=customId
      */
     public function actionGetGatheringMiniCode()

+ 4 - 4
app-ghs/controllers/OrderController.php

@@ -2976,7 +2976,7 @@ class OrderController extends BaseController
 
     /**
      * 应收统计:按支付日汇总。
-     * originAmount=未扣隔天;nextDayAmount=天售后;amount=已扣隔天(净额)。
+     * originAmount=未扣隔天;nextDayAmount=天售后;amount=已扣隔天(净额)。
      */
     public function actionAccountList()
     {
@@ -3039,7 +3039,7 @@ class OrderController extends BaseController
             }
         }
 
-        // 天售后:只扣 amount,originAmount 保持未扣口径
+        // 天售后:只扣 amount,originAmount 保持未扣口径
         $fwdMap = NextDayRefundClass::sumAmountGroupByCustom($mainId, $currentStartTime, $currentEndTime);
         foreach ($fwdMap as $cid => $amt) {
             if (bccomp((string)$amt, '0', 2) == 0) {
@@ -3125,7 +3125,7 @@ class OrderController extends BaseController
 
 
     /**
-     * 客户业绩(PC):按支付日汇总开单;天售后按通过日扣实际金额并计入退款。
+     * 客户业绩(PC):按支付日汇总开单;天售后按通过日扣实际金额并计入退款。
      * 与小程序 statCustomCg 同步,搜索关键词 customYj。
      */
     public function actionYj()
@@ -3310,7 +3310,7 @@ class OrderController extends BaseController
             }
         }
 
-        // 天售后:按通过日扣实际金额、计入退款;无当日开单也建负行;订单笔数不减
+        // 天售后:按通过日扣实际金额、计入退款;无当日开单也建负行;订单笔数不减
         $fwdMap = NextDayRefundClass::sumAmountGroupByCustom($mainId, $currentStartTime, $currentEndTime);
         $nextDayDeduct = '0.00';
         foreach ($fwdMap as $cid => $amt) {

+ 2 - 2
app-ghs/controllers/PurchaseOrderController.php

@@ -942,7 +942,7 @@ class PurchaseOrderController extends BaseController
 
     /**
      * 应付统计:按入库日汇总。
-     * originAmount=未扣隔天;nextDayAmount=天售后;amount=已扣隔天(净额)。
+     * originAmount=未扣隔天;nextDayAmount=天售后;amount=已扣隔天(净额)。
      */
     public function actionDueList()
     {
@@ -996,7 +996,7 @@ class PurchaseOrderController extends BaseController
             }
         }
 
-        // 天售后:只扣 amount,originAmount 保持未扣口径
+        // 天售后:只扣 amount,originAmount 保持未扣口径
         $nextMap = CgPurchaseNextDayClass::sumAmountGroupByGhs($mainId, $currentStartTime, $currentEndTime);
         foreach ($nextMap as $ghsId => $amt) {
             if (bccomp((string)$amt, '0', 2) <= 0) {

+ 4 - 4
app-ghs/controllers/RefundController.php

@@ -27,7 +27,7 @@ class RefundController extends BaseController
 {
 
     /**
-     * 原单是否可走天售后(售后页预检)
+     * 原单是否可走天售后(售后页预检)
      * GET: orderId
      */
     public function actionCheckNextDayEligible()
@@ -467,7 +467,7 @@ class RefundController extends BaseController
         if ($freeOnly === 1) {
             $where['relateOrderSn'] = '';
         }
-        // sameDay:-1全部 1当天售后 0天售后
+        // sameDay:-1全部 1当天售后 0天售后
         $sameDay = isset($get['sameDay']) ? intval($get['sameDay']) : -1;
         if ($sameDay === 0 || $sameDay === 1) {
             $where['sameDay'] = $sameDay;
@@ -751,7 +751,7 @@ class RefundController extends BaseController
                     RefundOrderService::passRefund($refundInfo);
 
                     $transaction->commit();
-                    // 天售后:返回海报/结果页所需字段
+                    // 天售后:返回海报/结果页所需字段
                     $refundInfo = RefundOrderClass::getById($refundId, true);
                     $order = OrderClass::getById($id, true);
                     $custom = null;
@@ -788,7 +788,7 @@ class RefundController extends BaseController
             if (util::isDbConcurrencyException($exception)) {
                 util::fail('系统繁忙中,请稍后再试');
             } else {
-                // 透出业务失败原因,便于天售后排错
+                // 透出业务失败原因,便于天售后排错
                 util::fail($exception->getMessage() ?: '操作失败');
             }
         }

+ 3 - 3
app-ghs/controllers/StatCgGhsController.php

@@ -18,7 +18,7 @@ class StatCgGhsController extends BaseController
 
     /**
      * 供货业绩:入库日累加 actPrice(当天售后已减实付);
-     * 天售后按通过日扣金额(可为负,笔数不减),对齐销售客户业绩口径。
+     * 天售后按通过日扣金额(可为负,笔数不减),对齐销售客户业绩口径。
      */
     public function actionProfile()
     {
@@ -144,7 +144,7 @@ class StatCgGhsController extends BaseController
             }
         }
 
-        // 天售后:按通过日扣供货商金额;无当日入库也建负行(笔数不减)
+        // 天售后:按通过日扣供货商金额;无当日入库也建负行(笔数不减)
         $nextDayDeduct = CgPurchaseNextDayClass::sumAmountByMainAndTime($mainId, $currentStartTime, $currentEndTime);
         $nextMap = CgPurchaseNextDayClass::sumAmountGroupByGhs($mainId, $currentStartTime, $currentEndTime);
         foreach ($nextMap as $ghsId => $amt) {
@@ -307,4 +307,4 @@ class StatCgGhsController extends BaseController
         util::success(['list' => $cg]);
     }
 
-}
+}

+ 28 - 9
app-hd/controllers/CgRefundController.php

@@ -76,16 +76,14 @@ class CgRefundController extends BaseController
         if ($cg->status == PurchaseClass::STATUS_UN_PAY) {
             util::fail("待付款订单,不能申请售后");
         }
-        if ($cg->status == PurchaseClass::STATUS_UN_SEND) {
-            util::fail("待发货订单,不能申请售后");
-        }
-        if ($cg->status == PurchaseClass::STATUS_SENDING) {
-            util::fail("订单配送中,不能申请售后");
-        }
         if ($cg->status == PurchaseClass::STATUS_CANCEL) {
             util::fail("订单已取消,不能申请售后");
         }
-        // 配对销售单:无支付时间不能售后;非当天/已结清走隔天售后
+        // 未完成时不在此拦截:提交申请时再「仅入库不改价」收货;预订单未发货仍不可售后
+        if (intval($cg->status) === PurchaseClass::STATUS_UN_SEND && intval($cg->book ?? 0) === 1) {
+            util::fail('预订单发货后才能申请售后');
+        }
+        // 配对销售单:无支付时间不能售后;非当天/已结清走跨天售后
         $saleOrder = OrderClass::getById(intval($cg->saleId ?? 0), true);
         if (empty($saleOrder)) {
             util::fail('没有订单信息');
@@ -125,7 +123,28 @@ class CgRefundController extends BaseController
         $connection = Yii::$app->db;
         $transaction = $connection->beginTransaction();
         try {
-
+            // 发起售后且采购单未完成:自动「仅入库不改价」收货,再创建售后单
+            $cg = PurchaseClass::getLockById($id);
+            if (empty($cg)) {
+                util::fail('没有找到采购单');
+            }
+            if (intval($cg->status) !== PurchaseClass::STATUS_COMPLETE
+                && intval($cg->status) !== PurchaseClass::STATUS_CANCEL
+                && intval($cg->status) !== PurchaseClass::STATUS_UN_PAY
+            ) {
+                if (intval($cg->status) === PurchaseClass::STATUS_UN_SEND && intval($cg->book ?? 0) === 1) {
+                    util::fail('预订单发货后才能申请售后');
+                }
+                PurchaseClass::confirmTake($cg);
+                $cg = PurchaseClass::getById($id, true);
+                if (empty($cg) || intval($cg->status) !== PurchaseClass::STATUS_COMPLETE) {
+                    util::fail('自动确认收货失败,请稍后重试');
+                }
+                $saleOrder = OrderClass::getById(intval($cg->saleId ?? 0), true);
+                if (empty($saleOrder)) {
+                    util::fail('没有订单信息');
+                }
+            }
 
             if ($refundType == CgRefundClass::REFUND_TYPE_MONEY_GOOD) {
                 //花材列表结构
@@ -147,7 +166,7 @@ class CgRefundController extends BaseController
             if ($post['price'] <= 0) {
                 util::fail("退款金额不能小于0");
             }
-            // 天售后不减实付:上限用 actPrice - 已隔天累计;当天仍对比 realPrice
+            // 天售后不减实付:上限用 actPrice - 已隔天累计;当天仍对比 realPrice
             if ($forceNextDay) {
                 $remainNext = bcsub(
                     (string)($saleOrder->actPrice ?? $cg->actPrice ?? '0'),

+ 4 - 3
app-hd/controllers/CustomController.php

@@ -668,8 +668,8 @@ class CustomController extends BaseController
     }
 
     /**
-     * 客户余额页小程序码(隔天/无原单返余额海报用)
-     * GET: id=customId
+     * 客户余额/充值页小程序码(隔天/无原单返余额海报用)
+     * GET: id=customId;扫码打开商城 pages/member/recharge,scene 带门店 s=
      */
     public function actionGetGatheringMiniCode()
     {
@@ -683,7 +683,8 @@ class CustomController extends BaseController
             util::fail('不是你的客户');
         }
         $merchant = \bizHd\wx\classes\WxOpenClass::getMallWxInfo();
-        $page = 'pages/home/user';
+        // 与扫码充值码一致:进充值页;scene.s 展开为 account
+        $page = 'pages/member/recharge';
         $ptStyle = \common\components\dict::getDict('ptStyle', 'mall');
         $scene = 's=' . $this->shopId;
         $envVersion = \common\components\miniUtil::normalizeMiniEnvVersion($get['env_version'] ?? 'release');

+ 1 - 1
app-hd/controllers/OrderController.php

@@ -1435,7 +1435,7 @@ class OrderController extends BaseController
         OrderService::valid($detail, $this->mainId);
 
         $main = $this->main;
-        // 是否须走天售后(前端提示)
+        // 是否须走天售后(前端提示)
         $ret = OrderClass::ifShPay($detail, $main, 0);
         $shAddPay = $ret['shAddPay'];
         $shAddPayReason = $ret['shAddPayReason'];

+ 37 - 4
app-hd/controllers/RefundController.php

@@ -60,7 +60,7 @@ class RefundController extends BaseController
         if ($customId > 0) {
             $where['customId'] = $customId;
         }
-        // sameDay:-1全部 1当天售后 0天售后
+        // sameDay:-1全部 1当天售后 0天售后
         $sameDay = isset($get['sameDay']) ? intval($get['sameDay']) : -1;
         if ($sameDay === 0 || $sameDay === 1) {
             $where['sameDay'] = $sameDay;
@@ -107,9 +107,37 @@ class RefundController extends BaseController
             }
         }
         $info['statusText'] = HdRefundMallClass::statusText($refund->status);
+        // 顾客上传图:展开为小图/大图,供审核页预览(对齐 ghs refundDetail)
+        $info = HdRefundMallClass::attachImgDisplayFields($info);
         util::success(['info' => $info, 'itemList' => $itemList, 'goodsList' => $goodsList, 'productList' => $productList]);
     }
 
+    /**
+     * 原单是否可走跨天售后(售后页预检,对齐 ghs check-next-day-eligible)
+     */
+    public function actionCheckNextDayEligible()
+    {
+        $orderId = Yii::$app->request->get('orderId', 0);
+        $order = OrderClass::getById($orderId, true);
+        if (empty($order) || intval($order->mainId) !== intval($this->mainId)) {
+            util::fail('没有找到订单');
+        }
+        $main = $this->main;
+        $check = HdNextDayRefundClass::checkEligible($order, $main);
+        $check['hasPayTime'] = HdNextDayRefundClass::hasValidPayTime($order) ? 1 : 0;
+        $check['hasNextDay'] = bccomp((string)($order->nextDayTkPrice ?? '0'), '0', 2) > 0 ? 1 : 0;
+        $check['nextDayTkPrice'] = $order->nextDayTkPrice ?? '0.00';
+        $check['tkPrice'] = $order->tkPrice ?? '0.00';
+        $check['actPrice'] = $order->actPrice ?? '0.00';
+        $check['payWay'] = $order->payWay ?? 0;
+        $check['onlinePay'] = $order->onlinePay ?? 0;
+        $check['debtPrice'] = $order->debtPrice ?? '0.00';
+        $check['remainDebtPrice'] = $order->remainDebtPrice ?? '0.00';
+        $check['clearId'] = intval($order->clearId ?? 0);
+        $check['payTime'] = $order->payTime ?? '';
+        util::success($check);
+    }
+
     //退款 ssh 20220427
     public function actionCreateOrder()
     {
@@ -191,7 +219,7 @@ class RefundController extends BaseController
             $post['shopAdminName'] = $adminName;
 
             $main = $this->main;
-            // 非付款日 / 已结清等:强制天售后写入(sameDay=0)
+            // 非付款日 / 已结清等:强制天售后写入(sameDay=0)
             if (HdNextDayRefundClass::mustUseNextDay($order, $main)) {
                 $post['sameDay'] = HdRefundClass::SAME_DAY_NO;
             } else {
@@ -389,6 +417,7 @@ class RefundController extends BaseController
 
     /**
      * 商城顾客售后审核通过:执行真实退款
+     * 成功返回资金落地字段,供前端跳转 forwardResult(线下选手选 / 已返充凭证)
      */
     public function actionPass()
     {
@@ -417,11 +446,15 @@ class RefundController extends BaseController
                 'sjId' => (int)$this->sjId,
                 'mainId' => (int)$this->mainId,
             ];
-            HdRefundMallClass::approve($refund, $order, $admin);
+            $refund = HdRefundMallClass::approve($refund, $order, $admin);
             $orderId = (int)$refund->orderId;
             $transaction->commit();
             DistributionCommissionClass::tryCalcDistributionAfterPay($orderId);
-            util::complete('审核通过,退款已处理');
+
+            // 对齐即时售后:返回 needFundAction 等,线下/现金等可选手选落地
+            $refund = HdRefundClass::getById((int)$refund->id, true);
+            $payload = HdNextDayRefundClass::buildFundResultPayload($refund);
+            util::success($payload, '审核通过,退款已处理');
         } catch (\Exception $e) {
             $transaction->rollBack();
             Yii::info('商城售后审核通过失败:' . $e->getMessage());

+ 5 - 1
app-mall/controllers/RefundController.php

@@ -37,6 +37,8 @@ class RefundController extends BaseController
             if (!empty($apply['product']) && is_string($apply['product'])) {
                 $apply['productList'] = json_decode($apply['product'], true) ?: [];
             }
+            // 进度页可预览已传售后图
+            $apply = HdRefundMallClass::attachImgDisplayFields($apply);
         }
 
         // 分销售后截止提示(仅提示,真正拦截在提交时)
@@ -145,6 +147,7 @@ class RefundController extends BaseController
     /**
      * 顾客售后申请列表(mallApp 退款售后 Tab)
      * status:all 全部;0/pending 处理中;1/refunded 已退款;closed 已关闭(驳回+取消)
+     * orderId:可选,订单详情「申请记录」只看本单申请
      */
     public function actionList()
     {
@@ -152,7 +155,8 @@ class RefundController extends BaseController
         if ($tab === '' || $tab === null) {
             $tab = 'all';
         }
-        $list = HdRefundMallClass::getCustomApplyList((int)$this->userId, (string)$tab);
+        $orderId = (int)Yii::$app->request->get('orderId', 0);
+        $list = HdRefundMallClass::getCustomApplyList((int)$this->userId, (string)$tab, $orderId);
         util::success($list);
     }
 

+ 7 - 7
biz-ghs/cg/classes/CgPurchaseNextDayClass.php

@@ -1,6 +1,6 @@
 <?php
 /**
- * 上游采购天售后业务类(biz-ghs/cg/classes)
+ * 上游采购天售后业务类(biz-ghs/cg/classes)
  * 用途:按入库日/结清状态判定当天 vs 隔天;写支付快照;隔天累加 nextDayTkPrice 并买方加余额;
  * 统计辅助:入库日净量、通过日按供货商/采购人/花材扣减。
  * 调用方:CgRefundService、CgRefundController、StatCgClass、StatCgGhsController。
@@ -52,7 +52,7 @@ class CgPurchaseNextDayClass
     }
 
     /**
-     * 原单是否必须走天售后:
+     * 原单是否必须走天售后:
      * - 入库日非当天;或
      * - 已结清 debt=2;或
      * - 已进结账单 clearId>0
@@ -87,7 +87,7 @@ class CgPurchaseNextDayClass
         if ($isToday && !$debtCleared && !$orderCleared) {
             return [
                 'ok' => false,
-                'reason' => '当天待结采购单请走普通售后;已结清/已结账/跨入库日可天售后',
+                'reason' => '当天待结采购单请走普通售后;已结清/已结账/跨入库日可天售后',
                 'sameDay' => self::SAME_DAY_YES,
                 'orderCleared' => 0,
                 'hasEntryTime' => 1,
@@ -103,7 +103,7 @@ class CgPurchaseNextDayClass
     }
 
     /**
-     * 创建时是否必须按天售后(sameDay=0)
+     * 创建时是否必须按天售后(sameDay=0)
      */
     public static function mustUseNextDay($order)
     {
@@ -197,7 +197,7 @@ class CgPurchaseNextDayClass
     }
 
     /**
-     * 按时段汇总成功天售后总金额(各统计页「隔天扣减」提示用)
+     * 按时段汇总成功天售后总金额(各统计页「隔天扣减」提示用)
      * @return string
      */
     public static function sumAmountByMainAndTime($mainId, $startTime, $endTime)
@@ -218,7 +218,7 @@ class CgPurchaseNextDayClass
     }
 
     /**
-     * 按供货商汇总天售后金额(供货业绩 cgStat 通过日扣减)
+     * 按供货商汇总天售后金额(供货业绩 cgStat 通过日扣减)
      * @return array ghsId => amount
      */
     public static function sumAmountGroupByGhs($mainId, $startTime, $endTime)
@@ -244,7 +244,7 @@ class CgPurchaseNextDayClass
     }
 
     /**
-     * 按原采购单采购人汇总天售后金额(采购人业绩 cgYj 通过日扣减,笔数不减)
+     * 按原采购单采购人汇总天售后金额(采购人业绩 cgYj 通过日扣减,笔数不减)
      * @return array cgStaffId => [amount, cgStaffName]
      */
     public static function sumAmountGroupByCgStaff($mainId, $startTime, $endTime)

+ 2 - 2
biz-ghs/cg/services/CgRefundService.php

@@ -50,7 +50,7 @@ class CgRefundService extends BaseService
         if ($sameDay === CgPurchaseNextDayClass::SAME_DAY_NO) {
             $check = CgPurchaseNextDayClass::checkEligible($order);
             if (empty($check['ok'])) {
-                util::fail($check['reason'] ?? '当前订单不可天售后');
+                util::fail($check['reason'] ?? '当前订单不可天售后');
             }
             $couldRefund = CgPurchaseNextDayClass::getCouldRefundPrice($order);
             if (bccomp((string)$refundPrice, $couldRefund, 2) === 1) {
@@ -62,7 +62,7 @@ class CgRefundService extends BaseService
                 util::fail('订单没有被结清才能申请当天退货退款');
             }
             if (!empty($order->clearId)) {
-                util::fail('已结账订单请走天售后');
+                util::fail('已结账订单请走天售后');
             }
             if (bccomp((string)$refundPrice, (string)($order->realPrice ?? '0'), 2) === 1) {
                 util::fail('退款金额超过订单金额');

+ 19 - 9
biz-ghs/custom/classes/CustomClass.php

@@ -698,8 +698,8 @@ class CustomClass extends BaseClass
     }
 
     /**
-     * 天售后/无原单返充余额:成对账户加余额,不改订单 actPrice;流水挂售后单。
-     * 入账后按本次金额 FIFO 销挂账(与客户详情充值消欠一致)
+     * 天售后/无原单返充余额:成对账户加余额,不改订单 actPrice;流水挂售后单。
+     * 入账后按本次金额销挂账:有原单则先结本单(可部分结),剩余再 FIFO。
      * 调用方须已 lockAccountPair。
      */
     public static function nextDayRefundReturnBalance($custom, $ghs, $refund, $order)
@@ -722,7 +722,7 @@ class CustomClass extends BaseClass
         $saleOrderSn = !empty($order) ? ($order->orderSn ?? '') : '';
         // 无原单退款流水只挂售后单号,避免「原单:」空串误导
         $event = $saleOrderSn !== ''
-            ? "隔天售后返充余额,售后单:{$refundSn},原单:{$saleOrderSn}"
+            ? "售后返充余额,售后单:{$refundSn},原单:{$saleOrderSn}"
             : "无原单退款返充余额,售后单:{$refundSn}";
         $capitalTypeCustom = dict::getDict('capitalType', 'ghsHelpCustomRechargeReturn', 'id');
         CustomBalanceChangeClass::add([
@@ -747,8 +747,8 @@ class CustomClass extends BaseClass
             'remark' => '',
         ], true);
         self::addGhsRefundBalanceChange($custom, $ghs, $refund, $order, $refundPrice, $newBalance);
-        // 有挂账欠款时:本次返充金额去消欠(与充值 merchantRechargeWithAutoClear 一致
-        self::fifoClearAfterRefundCredit($custom, $ghs, $refund, $refundPrice, $balanceBefore);
+        // 有挂账欠款时:本次返充金额去消欠(优先当前售后原单,剩余再 FIFO
+        self::fifoClearAfterRefundCredit($custom, $ghs, $refund, $refundPrice, $balanceBefore, $order);
         $custom = CustomClass::getById(intval($custom->id ?? 0), true);
         return bcadd((string)($custom->balance ?? $newBalance), '0', 2);
     }
@@ -801,14 +801,22 @@ class CustomClass extends BaseClass
         ];
         CustomBalanceChangeClass::add($change, true);
         self::addGhsRefundBalanceChange($custom, $ghs, $refund, $order, $refundPrice, $newBalance);
-        // 余额付原路退回 = 加余额,有挂账时一并消欠
-        self::fifoClearAfterRefundCredit($custom, $ghs, $refund, $refundPrice, $balanceBefore);
+        // 余额付原路退回 = 加余额,有挂账时一并消欠(优先当前售后原单)
+        self::fifoClearAfterRefundCredit($custom, $ghs, $refund, $refundPrice, $balanceBefore, $order);
     }
 
     /**
-     * 售后加余额后的 FIFO 销挂账封装:组装门店/操作人后交给 GhsRechargeSettleService。
+     * 售后加余额后的销挂账封装:优先结当前售后原单(可部分结),剩余按 id FIFO。
+     * 无原单退款时 $order 为空,整笔走原 FIFO。
+     *
+     * @param object $custom
+     * @param object|null $ghs
+     * @param object $refund
+     * @param string|float $amount
+     * @param string $balanceBefore 入账前净余额
+     * @param object|null $order 售后关联销售单
      */
-    protected static function fifoClearAfterRefundCredit($custom, $ghs, $refund, $amount, $balanceBefore)
+    protected static function fifoClearAfterRefundCredit($custom, $ghs, $refund, $amount, $balanceBefore, $order = null)
     {
         $shopId = intval($refund->shopId ?? 0);
         $shop = $shopId > 0 ? ShopClass::getById($shopId, true) : null;
@@ -816,9 +824,11 @@ class CustomClass extends BaseClass
             'id' => intval($refund->shopAdminId ?? 0),
             'name' => (string)($refund->shopAdminName ?? ''),
         ];
+        $preferOrderId = !empty($order) ? intval($order->id ?? 0) : 0;
         GhsRechargeSettleService::afterCreditFifoClear($custom, $ghs, $amount, $shop, $staff, [
             'balanceBeforeRecharge' => $balanceBefore,
             'payWay' => dict::getDict('payWay', 'balancePay'),
+            'preferOrderId' => $preferOrderId,
         ]);
     }
 

+ 39 - 6
biz-ghs/custom/services/GhsRechargeSettleService.php

@@ -20,7 +20,8 @@ use common\components\util;
  * 规则(仅此一套,无旧三件套):
  * 1. 净 balance:正=有余额,负=待结;历史挂账已并入 balance(ensure 幂等)。
  * 2. 来款:balance += 金额,记一条充值流水(amount=来款)。
- * 3. 销单:仅用「本次资金池」按订单 id FIFO 减少 remainDebtPrice,不再扣 balance。
+ * 3. 销单:仅用「本次资金池」减少 remainDebtPrice,不再扣 balance。
+ *    默认按订单 id FIFO;售后返充可传 preferOrderId,先结当前售后原单(可部分结),剩余再 FIFO。
  * 4. 用账面正余额销账:无来款,FIFO 后 balance -= 实销额,可记「结账」流水。
  *
  * 入口:merchantRechargeWithAutoClear / onlinePayFifoClear / confirmClearBillWithIncoming /
@@ -189,7 +190,7 @@ class GhsRechargeSettleService
      * @param string|float $amount 本次入账金额
      * @param object|null $shop 供货商门店;空则取 ghs.shopId
      * @param object|null $staff 操作人(结账单记录用)
-     * @param array $options balanceBeforeRecharge|payWay
+     * @param array $options balanceBeforeRecharge|payWay|preferOrderId(售后原单优先消欠)
      * @return array 与 fifoSettleWithPool 相同结构
      */
     public static function afterCreditFifoClear($custom, $ghs, $amount, $shop = null, $staff = null, $options = [])
@@ -231,11 +232,15 @@ class GhsRechargeSettleService
     }
 
     /**
-     * FIFO 分配计划:按订单 id 升序,资金池用尽即停止。
+     * 销挂账分配计划:可选优先本单,剩余按订单 id 升序 FIFO。
+     * 售后返充:preferOrderId=当前售后原单,先冲本单 remain(可部分结),再消其它挂账单。
      *
+     * @param int $customId 客户 id
+     * @param string|float $poolAmount 本次可用金额
+     * @param int $preferOrderId 优先消欠的销售单 id,0=不优先
      * @return array<int, array{orderId:int, clearAmount:string, orderSn:string}>
      */
-    public static function planFifoByPool($customId, $poolAmount)
+    public static function planFifoByPool($customId, $poolAmount, $preferOrderId = 0)
     {
         $pool = self::money($poolAmount);
         if (bccomp($pool, '0', 2) <= 0) {
@@ -249,12 +254,39 @@ class GhsRechargeSettleService
 
         $plan = [];
         $used = '0.00';
+        $preferOrderId = intval($preferOrderId);
+
+        // 1)有售后原单且仍挂账:先结本单(金额不够则部分结)
+        if ($preferOrderId > 0) {
+            foreach ($orderList as $order) {
+                if (intval($order->id ?? 0) !== $preferOrderId) {
+                    continue;
+                }
+                $remain = self::money($order->remainDebtPrice ?? 0);
+                if (bccomp($remain, '0', 2) <= 0) {
+                    break;
+                }
+                $clearAmount = bccomp($remain, $pool, 2) <= 0 ? $remain : $pool;
+                $plan[] = [
+                    'orderId' => $preferOrderId,
+                    'clearAmount' => $clearAmount,
+                    'orderSn' => $order->orderSn ?? '',
+                ];
+                $used = bcadd($used, $clearAmount, 2);
+                break;
+            }
+        }
 
+        // 2)剩余金额按 id 升序 FIFO(跳过已优先消过的本单)
         foreach ($orderList as $order) {
             $left = bcsub($pool, $used, 2);
             if (bccomp($left, '0', 2) <= 0) {
                 break;
             }
+            $oid = intval($order->id ?? 0);
+            if ($preferOrderId > 0 && $oid === $preferOrderId) {
+                continue;
+            }
 
             $remain = self::money($order->remainDebtPrice ?? 0);
             if (bccomp($remain, '0', 2) <= 0) {
@@ -263,7 +295,7 @@ class GhsRechargeSettleService
 
             $clearAmount = bccomp($remain, $left, 2) <= 0 ? $remain : $left;
             $plan[] = [
-                'orderId' => (int)($order->id ?? 0),
+                'orderId' => $oid,
                 'clearAmount' => $clearAmount,
                 'orderSn' => $order->orderSn ?? '',
             ];
@@ -282,8 +314,9 @@ class GhsRechargeSettleService
         $ghs = $pair['ghs'];
         $customId = $custom->id ?? 0;
         $pool = self::money($poolAmount);
+        $preferOrderId = intval($options['preferOrderId'] ?? 0);
 
-        $plan = self::planFifoByPool($customId, $pool);
+        $plan = self::planFifoByPool($customId, $pool, $preferOrderId);
         if (empty($plan)) {
             self::refreshDebtFlags($custom, $ghs);
             return self::emptyAllocateResult($custom, $ghs);

+ 5 - 5
biz-ghs/order/classes/NextDayRefundClass.php

@@ -1,6 +1,6 @@
 <?php
 /**
- * 天售后业务类(biz-ghs/order/classes)
+ * 天售后业务类(biz-ghs/order/classes)
  * 用途:资格校验、支付快照与资金三态、隔天金额/返余额、统计净销量辅助。
  * 调用方:RefundOrderClass、CgRefundClass、统计类、售后 Controller/Service。
  */
@@ -54,7 +54,7 @@ class NextDayRefundClass
     }
 
     /**
-     * 原单是否可走天售后(即原「冲销」路径):
+     * 原单是否可走天售后(即原「冲销」路径):
      * - 非当天支付(仅看 payTime);或
      * - 挂账已结清;或
      * - 已结账单(当天已结账也按冲销处理,不走普通售后)
@@ -100,7 +100,7 @@ class NextDayRefundClass
     }
 
     /**
-     * 创建/审核时是否必须按天售后(sameDay=0):
+     * 创建/审核时是否必须按天售后(sameDay=0):
      * 与 checkEligible 对齐——非当天 / 已结账单 / 挂账已结清,均不可再走当天售后。
      * 无支付时间返回 false(须先走 assertCanRefund)。
      */
@@ -365,7 +365,7 @@ class NextDayRefundClass
             $custom = CustomClass::getById($customId, true);
             $balance = bcadd((string)($custom->balance ?? '0'), '0', 2);
         } elseif (self::isOnlinePayOrder($refund) || self::isOnlinePayOrder($order)) {
-            // 线上付收款时加过门店可提现余额,天售后也要在 ghs 端扣回(拉卡拉原路仍在采购侧发起)
+            // 线上付收款时加过门店可提现余额,天售后也要在 ghs 端扣回(拉卡拉原路仍在采购侧发起)
             $shopId = intval($order->shopId ?? ($refund->shopId ?? 0));
             $mainId = intval($order->mainId ?? ($refund->mainId ?? 0));
             $shop = ShopClass::getLockById($shopId);
@@ -533,7 +533,7 @@ class NextDayRefundClass
     }
 
     /**
-     * 按时段汇总成功天售后金额(收入/销量统计扣减用)
+     * 按时段汇总成功天售后金额(收入/销量统计扣减用)
      */
     public static function sumAmountByMainAndTime($mainId, $startTime, $endTime)
     {

+ 12 - 3
biz-ghs/order/classes/OrderClass.php

@@ -2244,8 +2244,17 @@ class OrderClass extends BaseClass
                 $content .= '退款金额:' . floatval($orderInfo['tkPrice']) . '<BR>';
             }
             $content .= '<B>金额:' . floatval($respond['realPrice']) . '</B><BR>';
-            if (isset($respond['debt']) && $respond['debt'] == 1) {
-                $content .= '实付金额:0<BR>';
+            if (isset($orderInfo['debtPrice']) && $orderInfo['debtPrice'] > 0) {
+                $remainDebt = bcadd((string)($orderInfo['remainDebtPrice'] ?? '0'), '0', 2);
+                $totalRefund = bcadd((string)($orderInfo['tkPrice'] ?? '0'), (string)($orderInfo['nextDayTkPrice'] ?? '0'), 2);
+                if ($totalRefund > 0) {
+                    $content .= '退款:' . floatval($totalRefund) . '<BR>';
+                }
+                if ($remainDebt > 0) {
+                    $content .= '<B>待结:' . floatval($remainDebt) . '</B><BR>';
+                } else {
+                    $content .= '<B>本单已结清!</B><BR>';
+                }
             } else {
                 $content .= '实付金额:' . floatval($respond['realPrice']) . '<BR>';
             }
@@ -2896,7 +2905,7 @@ XL;
         $objPHPExcel->getActiveSheet()->setCellValue('A1', 'ID');
         $objPHPExcel->getActiveSheet()->setCellValue('B1', '名称');
         $objPHPExcel->getActiveSheet()->setCellValue('C1', '未扣隔天金额');
-        $objPHPExcel->getActiveSheet()->setCellValue('D1', '天售后金额');
+        $objPHPExcel->getActiveSheet()->setCellValue('D1', '天售后金额');
         $objPHPExcel->getActiveSheet()->setCellValue('E1', '已扣隔天金额');
         $objPHPExcel->getActiveSheet()->setCellValue('F1', '订单数');
 

+ 3 - 3
biz-ghs/order/classes/OrderClearClass.php

@@ -263,7 +263,7 @@ class OrderClearClass extends BaseClass
      *
      * 订单 clearId 约定:
      * - 足额结清:写真实清账单 id
-     * - 部分销账且原 clearId=0:写 1,表示「有结账痕迹」(充值消欠 / 天售后返充 FIFO 共用)
+     * - 部分销账且原 clearId=0:写 1,表示「有结账痕迹」(充值消欠 / 天售后返充 FIFO 共用)
      */
     public static function confirmClear($clear, $payWay, $options = [])
     {
@@ -352,8 +352,8 @@ class OrderClearClass extends BaseClass
                 // 足额结清:写真实清账单 id,便于追溯
                 $orderUpdate['clearId'] = $clearId;
             } elseif (intval($order->clearId ?? 0) === 0) {
-                // 部分销账也打结账痕迹:clearId=1(有清账单参与过),供天售后/已结账判断
-                // 覆盖:客户详情充值消欠、天售后返充余额后的 FIFO 销账等
+                // 部分销账也打结账痕迹:clearId=1(有清账单参与过),供天售后/已结账判断
+                // 覆盖:客户详情充值消欠、天售后返充余额后的 FIFO 销账等
                 $orderUpdate['clearId'] = 1;
             }
             OrderClass::updateById($orderId, $orderUpdate);

+ 52 - 11
biz-ghs/order/classes/PurchaseOrderClass.php

@@ -100,7 +100,7 @@ class PurchaseOrderClass extends BaseClass
 
     /**
      * 减少本单剩余待结;减到 ≤0 则标已结清。不改 debtPrice。
-     * 消欠成功时若 clearId==0 则写 1(结账痕迹,供天售后判定)。
+     * 消欠成功时若 clearId==0 则写 1(结账痕迹,供天售后判定)。
      * @return bool 是否本单因此从待结变为已结清
      */
     public static function reduceRemainDebtPrice($order, $amount)
@@ -205,17 +205,18 @@ class PurchaseOrderClass extends BaseClass
     }
 
     /**
-     * 【用途】采购售后返充后 FIFO 消欠并生成结账单,回写余额流水 clear 字段。
-     * 【为什么】对齐销售侧 afterCreditFifoClear,便于余额变动展示「本单销」并可跳转结账单
+     * 【用途】采购售后返充后消欠并生成结账单,回写余额流水 clear 字段。
+     * 【为什么】对齐销售侧:先结当前售后采购单(可部分结),剩余再按 id FIFO
      *
      * @param object $ghs 买方供货商(已锁、余额已加完)
      * @param string|float $amount 本次返充金额
      * @param object $refund 采购售后单
      * @param int $sjId
      * @param int $shopId
+     * @param object|null $preferOrder 当前售后关联采购单,空则整笔 FIFO
      * @return array{clearId:int,clearSn:string,clearAmount:string}
      */
-    public static function fifoClearRemainAfterCreditWithBill($ghs, $amount, $refund, $sjId, $shopId)
+    public static function fifoClearRemainAfterCreditWithBill($ghs, $amount, $refund, $sjId, $shopId, $preferOrder = null)
     {
         $empty = ['clearId' => 0, 'clearSn' => '', 'clearAmount' => '0.00'];
         $pool = bcadd((string)$amount, '0', 2);
@@ -235,11 +236,42 @@ class PurchaseOrderClass extends BaseClass
         }
         $plan = [];
         $used = '0.00';
+        $preferOrderId = !empty($preferOrder) ? intval($preferOrder->id ?? 0) : 0;
+
+        // 1)优先结当前售后采购单(可部分结)
+        if ($preferOrderId > 0) {
+            foreach ($orderList as $order) {
+                if (intval($order->id ?? 0) !== $preferOrderId) {
+                    continue;
+                }
+                $remain = bcadd((string)($order->remainDebtPrice ?? '0'), '0', 2);
+                if (bccomp($remain, '0', 2) <= 0) {
+                    $remain = bcadd((string)($order->actPrice ?? '0'), '0', 2);
+                }
+                if (bccomp($remain, '0', 2) <= 0) {
+                    break;
+                }
+                $cut = bccomp($remain, $pool, 2) <= 0 ? $remain : $pool;
+                $plan[] = [
+                    'cgId' => $preferOrderId,
+                    'clearAmount' => $cut,
+                    'orderSn' => (string)($order->orderSn ?? ''),
+                ];
+                $used = bcadd($used, $cut, 2);
+                break;
+            }
+        }
+
+        // 2)剩余金额按 id 升序 FIFO(跳过已优先的本单)
         foreach ($orderList as $order) {
             $left = bcsub($pool, $used, 2);
             if (bccomp($left, '0', 2) <= 0) {
                 break;
             }
+            $oid = intval($order->id ?? 0);
+            if ($preferOrderId > 0 && $oid === $preferOrderId) {
+                continue;
+            }
             $remain = bcadd((string)($order->remainDebtPrice ?? '0'), '0', 2);
             if (bccomp($remain, '0', 2) <= 0) {
                 $remain = bcadd((string)($order->actPrice ?? '0'), '0', 2);
@@ -249,7 +281,7 @@ class PurchaseOrderClass extends BaseClass
             }
             $cut = bccomp($remain, $left, 2) <= 0 ? $remain : $left;
             $plan[] = [
-                'cgId' => intval($order->id ?? 0),
+                'cgId' => $oid,
                 'clearAmount' => $cut,
                 'orderSn' => (string)($order->orderSn ?? ''),
             ];
@@ -420,9 +452,9 @@ class PurchaseOrderClass extends BaseClass
             }
         }
 
-        // 隔天等:入账后 FIFO 消挂账并生成结账单;当天已减本单 remain 则不再 FIFO,避免重复消欠
+        // 隔天等:入账后消挂账并生成结账单(优先当前售后采购单);当天已减本单 remain 则不再消,避免重复
         if ($doFifo) {
-            self::fifoClearRemainAfterCreditWithBill($ghs, $amount, $refund, $sjId, $shopId);
+            self::fifoClearRemainAfterCreditWithBill($ghs, $amount, $refund, $sjId, $shopId, $order);
         }
 
         return $ghs;
@@ -866,9 +898,18 @@ class PurchaseOrderClass extends BaseClass
         if (isset($orderInfo['tkPrice']) && $orderInfo['tkPrice'] > 0) {
             $content .= '退款金额:' . floatval($orderInfo['tkPrice']) . '元<BR>';
         }
-        $content .= '<B>订单金额:' . floatval($orderInfo['realPrice']) . '</B><BR>';
-        if (isset($orderInfo['debt']) && $orderInfo['debt'] == 1) {
-            $content .= '实付金额:0元<BR>';
+        $content .= '<B>金额:' . floatval($orderInfo['realPrice']) . '</B><BR>';
+        if (isset($orderInfo['debtPrice']) && $orderInfo['debtPrice'] > 0) {
+            $remainDebt = bcadd((string)($orderInfo['remainDebtPrice'] ?? '0'), '0', 2);
+            $totalRefund = bcadd((string)($orderInfo['tkPrice'] ?? '0'), (string)($orderInfo['nextDayTkPrice'] ?? '0'), 2);
+            if($totalRefund > 0) {
+                $content .= '退款:' . floatval($totalRefund) . '元<BR>';
+            }
+            if($remainDebt > 0) {
+                $content .= '<B>待结:' . floatval($remainDebt) . '</B><BR>';   
+            }else{
+                $content .= '<B>本单已结清!</B><BR>';
+            }
         } else {
             $content .= '实付金额:' . floatval($orderInfo['realPrice']) . '元<BR>';
         }
@@ -1920,7 +1961,7 @@ class PurchaseOrderClass extends BaseClass
         $objPHPExcel->getActiveSheet()->setCellValue('A1', 'ID');
         $objPHPExcel->getActiveSheet()->setCellValue('B1', '名称');
         $objPHPExcel->getActiveSheet()->setCellValue('C1', '未扣隔天金额');
-        $objPHPExcel->getActiveSheet()->setCellValue('D1', '天售后金额');
+        $objPHPExcel->getActiveSheet()->setCellValue('D1', '天售后金额');
         $objPHPExcel->getActiveSheet()->setCellValue('E1', '已扣隔天金额');
         $objPHPExcel->getActiveSheet()->setCellValue('F1', '订单数');
 

+ 3 - 3
biz-ghs/order/classes/RefundOrderClass.php

@@ -38,7 +38,7 @@ class RefundOrderClass extends BaseClass
     const REFUND_TYPE_MONEY_GOOD = 1; //退款退货
     const REFUND_TYPE_MONEY = 2; // 仅退款
 
-    /** 1当天售后(默认) 0天售后 */
+    /** 1当天售后(默认) 0天售后 */
     const SAME_DAY_YES = 1;
     const SAME_DAY_NO = 0;
 
@@ -136,7 +136,7 @@ class RefundOrderClass extends BaseClass
         $refundSn = $refund->orderSn ?? '';
         $refundPrice = $refund->refundPrice ?? 0;
         $refundType = $refund->refundType ?? 1;
-        // 提前读取:退货累加 nextRefundNum 时要用(天售后数量单独记账)
+        // 提前读取:退货累加 nextRefundNum 时要用(天售后数量单独记账)
         $sameDay = intval($refund->sameDay ?? self::SAME_DAY_YES);
         if ($refundType == RefundOrderClass::REFUND_TYPE_MONEY_GOOD) {
             $refundItemList = RefundOrderItemClass::getAllByCondition(['orderSn' => $refundSn], null, '*', null, true);
@@ -250,7 +250,7 @@ class RefundOrderClass extends BaseClass
         $main->totalRefund = $totalRefund;
         $main->save();
 
-        // 天售后:累计 nextDayTkPrice,不改 actPrice;buyAmount/线上门店可提现余额在 applyGhsAmountAndFund
+        // 天售后:累计 nextDayTkPrice,不改 actPrice;buyAmount/线上门店可提现余额在 applyGhsAmountAndFund
         if ($sameDay === self::SAME_DAY_NO) {
             \bizGhs\order\classes\NextDayRefundClass::applyGhsAmountAndFund($refund, $order);
             return;

+ 1 - 1
biz-ghs/order/models/OrderItem.php

@@ -6,7 +6,7 @@ use bizGhs\base\models\Base;
 
 /**
  * 供货商销售单明细 xhGhsOrderItem
- * nextRefundNum:天售后已退数量;当天已退 = refundNum - nextRefundNum
+ * nextRefundNum:天售后已退数量;当天已退 = refundNum - nextRefundNum
  */
 class OrderItem extends Base
 {

+ 6 - 1
biz-ghs/order/models/PurchaseOrder.php

@@ -3,11 +3,16 @@ namespace bizGhs\order\models;
 use bizGhs\base\models\Base;
 /**
  * 采购订单 xhGhsCgOrder
- * nextDayTkPrice:天售后累计退款,不改 actPrice;可退上限=actPrice-nextDayTkPrice
+ * nextDayTkPrice:天售后累计退款,不改 actPrice;可退上限=actPrice-nextDayTkPrice
  * debtPrice:挂账原欠;remainDebtPrice:剩余待结(结账/FIFO 消欠只减本字段)
  */
 class PurchaseOrder extends Base
 {
+	/**
+	 * 内存标记(非表字段):当天售后是否刚把本单挂账结清。
+	 * CgRefundService 写入,供 PurchaseOrderClass::applyPurchaseRefundOnAccounts 减 debtNum。
+	 */
+	public $_purchaseDebtClearedByRefund = false;
 
 	public static function tableName()
 	{

+ 1 - 1
biz-ghs/order/models/PurchaseOrderItem.php

@@ -3,7 +3,7 @@ namespace bizGhs\order\models;
 use bizGhs\base\models\Base;
 /**
  * 采购订单明细 xhGhsCgOrderItem
- * nextRefundNum:天售后已退数量;当天已退 = refundNum - nextRefundNum
+ * nextRefundNum:天售后已退数量;当天已退 = refundNum - nextRefundNum
  */
 class PurchaseOrderItem extends Base
 {

+ 1 - 1
biz-ghs/order/services/OrderService.php

@@ -939,7 +939,7 @@ class OrderService extends BaseService
         // 无支付时间不能售后
         \bizGhs\order\classes\NextDayRefundClass::assertCanRefund($order);
         $sameDay = intval($post['sameDay'] ?? \bizGhs\order\classes\RefundOrderClass::SAME_DAY_YES);
-        // 非当天 / 已结账 / 挂账结清 → 强制天售后(sameDay=0),不再拦截为「不能售后」
+        // 非当天 / 已结账 / 挂账结清 → 强制天售后(sameDay=0),不再拦截为「不能售后」
         if (\bizGhs\order\classes\NextDayRefundClass::mustUseNextDay($order)) {
             $sameDay = \bizGhs\order\classes\RefundOrderClass::SAME_DAY_NO;
             $post['sameDay'] = $sameDay;

+ 5 - 5
biz-ghs/order/services/RefundOrderService.php

@@ -57,7 +57,7 @@ class RefundOrderService extends BaseService
         }
         // 无支付时间不能售后;仅用 payTime 判是否转隔天
         NextDayRefundClass::assertCanRefund($order);
-        // 审核时若已非当天或已结清:自动转为天售后(不再要求驳回)
+        // 审核时若已非当天或已结清:自动转为天售后(不再要求驳回)
         $sameDay = intval($refund->sameDay ?? RefundOrderClass::SAME_DAY_YES);
         if ($sameDay === RefundOrderClass::SAME_DAY_YES && NextDayRefundClass::mustUseNextDay($order)) {
             self::convertRefundToNextDay($refund, $order);
@@ -72,7 +72,7 @@ class RefundOrderService extends BaseService
         if (strtotime($addTime) <= strtotime('2022-03-30 00:00:00')) {
             util::fail('历史订单,不能发起退款哦');
         }
-        // 天售后不减 actPrice,上限用 actPrice - 已隔天累计;当天仍对比 realPrice
+        // 天售后不减 actPrice,上限用 actPrice - 已隔天累计;当天仍对比 realPrice
         if ($sameDay === RefundOrderClass::SAME_DAY_NO) {
             $remainNext = bcsub(
                 (string)($order->actPrice ?? '0'),
@@ -102,7 +102,7 @@ class RefundOrderService extends BaseService
                 }
             }
             $currentClearId = OrderCgClearClass::getPendingClearIdByOrderId($currentOrderId, $customId);
-            // 天售后不要求先取消结账单
+            // 天售后不要求先取消结账单
             if ($currentClearId > 0 && $sameDay === RefundOrderClass::SAME_DAY_YES) {
 				//有结账单要取消才能售后
                 util::success(['clearId' => $currentClearId, 'error' => 'hasUnClearOrder']);
@@ -133,7 +133,7 @@ class RefundOrderService extends BaseService
     }
 
     /**
-     * 将当天售后单转为天售后类型(审核跨日/已结清时调用)
+     * 将当天售后单转为天售后类型(审核跨日/已结清时调用)
      * 同步写销售/采购售后单的 sameDay + 支付快照 + 资金三态。
      */
     public static function convertRefundToNextDay($refund, $order)
@@ -286,7 +286,7 @@ class RefundOrderService extends BaseService
         if ($sameDay === RefundOrderClass::SAME_DAY_NO) {
             $check = NextDayRefundClass::checkEligible($order);
             if (empty($check['ok'])) {
-                util::fail($check['reason'] ?? '当前订单不可天售后');
+                util::fail($check['reason'] ?? '当前订单不可天售后');
             }
         }
         $snap = NextDayRefundClass::buildRefundPaySnapshot(

+ 4 - 4
biz-ghs/stat/classes/StatCgClass.php

@@ -1,7 +1,7 @@
 <?php
 /**
  * 采购统计:花材排行 / 采购人业绩。
- * 口径对齐销售天售后:入库日用 nextRefundNum 算净量;通过日再扣隔天金额/数量(可为负,笔数不减)。
+ * 口径对齐销售天售后:入库日用 nextRefundNum 算净量;通过日再扣隔天金额/数量(可为负,笔数不减)。
  */
 namespace bizGhs\stat\classes;
 
@@ -105,7 +105,7 @@ class StatCgClass extends BaseClass
             }
         }
 
-        // 天售后:按通过日扣花材数量/金额;无当日入库也建负行
+        // 天售后:按通过日扣花材数量/金额;无当日入库也建负行
         self::applyNextDayAdjustToCgItemRows($arr, $productInfo, $mainId, $currentStartTime, $currentEndTime);
         $nextDayDeduct = CgPurchaseNextDayClass::sumAmountByMainAndTime($mainId, $currentStartTime, $currentEndTime);
 
@@ -242,7 +242,7 @@ class StatCgClass extends BaseClass
     }
 
     /**
-     * 采购人业绩:入库日按 actPrice(当天售后已减实付);天售后按通过日扣金额,笔数不减。
+     * 采购人业绩:入库日按 actPrice(当天售后已减实付);天售后按通过日扣金额,笔数不减。
      * @param int $mainId
      * @return array{staffCg:array}
      */
@@ -287,7 +287,7 @@ class StatCgClass extends BaseClass
             }
         }
 
-        // 天售后:按原单采购人扣金额;无当日入库也建负行(笔数不减)
+        // 天售后:按原单采购人扣金额;无当日入库也建负行(笔数不减)
         $nextDayDeduct = CgPurchaseNextDayClass::sumAmountByMainAndTime($mainId, $currentStartTime, $currentEndTime);
         $nextMap = CgPurchaseNextDayClass::sumAmountGroupByCgStaff($mainId, $currentStartTime, $currentEndTime);
         foreach ($nextMap as $sid => $row) {

+ 3 - 3
biz-ghs/stat/classes/StatKdClass.php

@@ -412,7 +412,7 @@ class StatKdClass extends BaseClass
                 $staffAmountList[$staffId] = ['num' => 1, 'amount' => $currentAmount, 'staffName' => $staffName];
             }
         }
-        // 天售后:按原单入账渠道扣减金额(只减金额不减笔数);批发 pf + 零售 ls
+        // 天售后:按原单入账渠道扣减金额(只减金额不减笔数);批发 pf + 零售 ls
         self::deductNextDayFromChannelIncome($incomeList, $mainId, $currentStartTime, $currentEndTime);
 
         return ['incomeList' => $incomeList, 'staffAmountList' => $staffAmountList, 'payCodeIncome' => $payCodeIncome, 'lsAfterSale' => $lsAfterSale];
@@ -420,7 +420,7 @@ class StatKdClass extends BaseClass
     }
 
     /**
-     * 渠道收入对冲天售后金额(ghs 批发 + hd 零售共用财务)
+     * 渠道收入对冲天售后金额(ghs 批发 + hd 零售共用财务)
      * 挂账已结扣 debtClear 等;批发记 pf、零售记 ls
      */
     protected static function deductNextDayFromChannelIncome(&$incomeList, $mainId, $startTime, $endTime)
@@ -440,7 +440,7 @@ class StatKdClass extends BaseClass
     }
 
     /**
-     * 将天售后行按渠道扣减金额
+     * 将天售后行按渠道扣减金额
      * @param array $incomeList
      * @param array $rows listForChannelIncomeDeduct 结果
      * @param string $categoryKey pf|ls

+ 11 - 11
biz-ghs/stat/classes/StatSaleClass.php

@@ -203,7 +203,7 @@ class StatSaleClass extends BaseClass
                 }
             }
         }
-        // 天售后:按通过日扣金额/扎数;无当日开单也建负行(笔数不减)
+        // 天售后:按通过日扣金额/扎数;无当日开单也建负行(笔数不减)
         $fwdMap = NextDayRefundClass::sumAmountGroupByCustom($mainId, $currentStartTime, $currentEndTime);
         $qtyMap = NextDayRefundClass::sumItemQtyByCustom($mainId, $currentStartTime, $currentEndTime);
         $cidKeys = array_unique(array_merge(array_keys($fwdMap), array_keys($qtyMap)));
@@ -253,7 +253,7 @@ class StatSaleClass extends BaseClass
 
     /**
      * 经营利润总表:收入 − 支出。
-     * 采购天售后:不改「花材采购」入库口径;通过日单独「采购隔天退货」(退货并退款+仅退款合计)计入收入。
+     * 采购天售后:不改「花材采购」入库口径;通过日单独「采购隔天退货」(退货并退款+仅退款合计)计入收入。
      */
     public static function profile($mainId)
     {
@@ -366,14 +366,14 @@ class StatSaleClass extends BaseClass
                 $totalOrderNum++;
             }
         }
-        // 天售后扣减批发收入(笔数不变)
+        // 天售后扣减批发收入(笔数不变)
         $pfIncome = bcsub(
             (string)$pfIncome,
             NextDayRefundClass::sumAmountByMainAndTime($mainId, $currentStartTime, $currentEndTime),
             2
         );
 
-        // 采购天售后(退货并退款+仅退款):通过日合计,单列收入,不改花材采购
+        // 采购天售后(退货并退款+仅退款):通过日合计,单列收入,不改花材采购
         $cgNextDayRefund = CgPurchaseNextDayClass::sumAmountByMainAndTime(
             $mainId,
             $currentStartTime,
@@ -462,7 +462,7 @@ class StatSaleClass extends BaseClass
             ['name' => '批发', 'id' => 'pf', 'amount' => floatval($pfIncome)],
             ['name' => '零售', 'id' => 'ls', 'amount' => floatval($lsIncome)],
             ['name' => '调拨出库', 'id' => 'allot', 'amount' => floatval($stockOut)],
-            // 采购天售后冲回(含退货并退款、仅退款);后续若细分再拆行
+            // 采购天售后冲回(含退货并退款、仅退款);后续若细分再拆行
             ['name' => '采购退款', 'id' => 'cgNextDay', 'amount' => floatval($cgNextDayRefund)],
         ];
         $expend = [
@@ -609,7 +609,7 @@ class StatSaleClass extends BaseClass
             }
         }
 
-        // 采购天售后:入库日已用 nextRefundNum;通过日再扣花材成本/数量;仅退款进「采购仅退款」
+        // 采购天售后:入库日已用 nextRefundNum;通过日再扣花材成本/数量;仅退款进「采购仅退款」
         $onlyRefundCg = bcadd(
             (string)$onlyRefundCg,
             self::applyNextDayAdjustToProductExpendRows(
@@ -719,7 +719,7 @@ class StatSaleClass extends BaseClass
             }
         }
 
-        // 天售后:支付日已用 nextRefundNum;通过日再扣数量/金额;仅退款并入销售仅退款
+        // 天售后:支付日已用 nextRefundNum;通过日再扣数量/金额;仅退款并入销售仅退款
         $onlyRefund = bcadd(
             (string)$onlyRefund,
             self::applyNextDayAdjustToProductIncomeRows(
@@ -978,7 +978,7 @@ class StatSaleClass extends BaseClass
             }
         }
 
-        // 采购天售后:入库日已用 nextRefundNum;通过日再扣分类成本;仅退款进「采购仅退款」
+        // 采购天售后:入库日已用 nextRefundNum;通过日再扣分类成本;仅退款进「采购仅退款」
         $productClassMapForCg = ProductClass::getAllByCondition(['mainId' => $mainId], null, 'id,classId,name', 'id');
         $applyCgClassDelta = function ($pidMap, $sign) use (&$arr, &$classList, $productClassMapForCg, &$tbCgIncome) {
             foreach ($pidMap as $pid => $row) {
@@ -1119,7 +1119,7 @@ class StatSaleClass extends BaseClass
             }
         }
 
-        // 天售后:支付日已用 nextRefundNum;通过日再按售后单扣分类收入;仅退款进「销售仅退款」
+        // 天售后:支付日已用 nextRefundNum;通过日再按售后单扣分类收入;仅退款进「销售仅退款」
         $productClassMap = ProductClass::getAllByCondition(['mainId' => $mainId], null, 'id,classId', 'id');
         $applyClassDelta = function ($pidMap, $sign) use (&$arr, &$classList, $productClassMap) {
             foreach ($pidMap as $pid => $row) {
@@ -1598,7 +1598,7 @@ class StatSaleClass extends BaseClass
                 }
             }
         }
-        // 天售后:数量已用 nextRefundNum;通过日再扣(无当日配送也建负行)
+        // 天售后:数量已用 nextRefundNum;通过日再扣(无当日配送也建负行)
         self::applyNextDayAdjustToSendStaffRows(
             $arr,
             $mainId,
@@ -1696,7 +1696,7 @@ class StatSaleClass extends BaseClass
                 }
             }
         }
-        // 天售后按客户归到片区扣金额/扎数;无当日开单的片区也建负行
+        // 天售后按客户归到片区扣金额/扎数;无当日开单的片区也建负行
         $fwdMap = NextDayRefundClass::sumAmountGroupByCustom($mainId, $currentStartTime, $currentEndTime);
         $qtyMap = NextDayRefundClass::sumItemQtyByCustom($mainId, $currentStartTime, $currentEndTime);
         $cidKeys = array_unique(array_merge(array_keys($fwdMap), array_keys($qtyMap)));

+ 3 - 3
biz-hd/cg/classes/CgRefundClass.php

@@ -40,7 +40,7 @@ class CgRefundClass extends BaseClass
     const SAME_DAY_NO = 0;
 
     /**
-     * 天售后原路退:走拉卡拉退款,不改采购单 actPrice
+     * 天售后原路退:走拉卡拉退款,不改采购单 actPrice
      * 优先读售后单资金快照 onlinePay/payWay,缺省回落采购原单
      */
     public static function nextDayOriginalOnlineRefund($cgRefund, $cg)
@@ -82,7 +82,7 @@ class CgRefundClass extends BaseClass
             'refundSn' => $refundOrderSn,
             'orderSn' => $cg->orderSn ?? '',
             'refundAmount' => $refundFee,
-            'refundReason' => '隔天售后原路退回',
+            'refundReason' => '订单售后,原路退回',
             'thirdNo' => $cg->thirdNo ?? '',
         ];
         $response = $laResource->refund($aliParams);
@@ -302,7 +302,7 @@ class CgRefundClass extends BaseClass
             }
         }
 
-        // 天售后:累计 nextDayTkPrice,不改 actPrice;原路退在此发起
+        // 天售后:累计 nextDayTkPrice,不改 actPrice;原路退在此发起
         if ($sameDay === self::SAME_DAY_NO) {
             \bizGhs\order\classes\NextDayRefundClass::applyCgAmountAndFund($cgRefund, $cg);
             return;

+ 180 - 12
biz-hd/custom/classes/CustomClass.php

@@ -621,6 +621,8 @@ class CustomClass extends BaseClass
     /**
      * 隔天/无原单售后返充余额(不依赖销售单 balanceTk 拆分)
      * 用途:成功页「返充到客户余额」;无原单时 $order 可为空。
+     * 入账规则:实付退回算本金,同步 +balance 与 +balancePay,并写总流水 + 充值余额流水;
+     * 不进赠送余额(赠送仅来自充值活动),以保持 balance ≈ balancePay + balanceGive。
      */
     public static function nextDayRefundReturnBalance($custom, $amount, $refund, $order = null)
     {
@@ -633,6 +635,8 @@ class CustomClass extends BaseClass
         if (empty($hd)) {
             util::fail('花店信息缺失,编号5363');
         }
+
+        // —— 总余额 ——
         $customBalance = bcadd((string)($custom->balance ?? '0'), $amount, 2);
         $custom->balance = $customBalance;
         $custom->isDebt = bccomp($customBalance, '0', 2) < 0 ? 1 : 0;
@@ -647,6 +651,17 @@ class CustomClass extends BaseClass
             util::fail('余额错误,请联系管理员,编号' . $hdId);
         }
 
+        // —— 充值本金:线下/无原单返充视为实付退回,进 balancePay(不进赠送)——
+        $customBalancePay = bcadd((string)($custom->balancePay ?? '0'), $amount, 2);
+        $custom->balancePay = $customBalancePay;
+        $custom->save(false, ['balancePay']);
+        $hdBalancePay = bcadd((string)($hd->balancePay ?? '0'), $amount, 2);
+        $hd->balancePay = $hdBalancePay;
+        $hd->save(false, ['balancePay']);
+        if (floatval($customBalancePay) !== floatval($hdBalancePay)) {
+            util::fail('充值余额错误,请联系管理员,编号' . $hdId);
+        }
+
         $refundSn = $refund->refundSn ?? '';
         $orderId = intval($refund->orderId ?? 0);
         $saleOrderSn = !empty($order) ? ($order->orderSn ?? '') : '';
@@ -655,32 +670,50 @@ class CustomClass extends BaseClass
         }
         // 无原单流水只挂售后单号,避免「原单:」空串误导
         $event = ($orderId > 0 && $saleOrderSn !== '')
-            ? "天售后返充余额,售后单:{$refundSn},原单:{$saleOrderSn}"
+            ? "天售后返充余额,售后单:{$refundSn},原单:{$saleOrderSn}"
             : "无原单退款返充余额,售后单:{$refundSn}";
         $capitalType = dict::getDict('capitalType', 'hdRefund', 'id');
-        $change = [
-            'customId' => $custom->id ?? 0,
-            'customName' => $custom->name ?? '',
+        $customId = intval($custom->id ?? 0);
+        $customName = $custom->name ?? '';
+        $staffId = intval($refund->shopAdminId ?? 0);
+        $staffName = $refund->shopAdminName ?? '';
+        $shopId = intval($refund->shopId ?? 0);
+        $mainId = intval($refund->mainId ?? 0);
+        $changeBase = [
+            'customId' => $customId,
+            'customName' => $customName,
             'relateId' => $refund->id ?? 0,
             'relateSn' => $refundSn,
             'onlinePay' => 1,
             'capitalType' => $capitalType,
-            'amount' => $amount,
-            'balance' => $customBalance,
             'io' => 1,
             'side' => 0,
             'payWay' => 2,
             'event' => $event,
-            'staffId' => $refund->shopAdminId ?? 0,
-            'staffName' => $refund->shopAdminName ?? '',
-            'shopId' => $refund->shopId ?? 0,
-            'mainId' => $refund->mainId ?? 0,
+            'staffId' => $staffId,
+            'staffName' => $staffName,
+            'shopId' => $shopId,
+            'mainId' => $mainId,
             'refundOrderId' => $orderId,
             'refundOrderSn' => $saleOrderSn,
             'remark' => '',
         ];
-        BalanceChangeClass::add($change, true);
-        return $customBalance;
+        // 总余额流水(balanceChange 页)
+        BalanceChangeClass::add(array_merge($changeBase, [
+            'amount' => $amount,
+            'balance' => $customBalance,
+        ]), true);
+        // 充值本金流水(balancePayChange / 商城余额变动页)
+        BalancePayChangeClass::add(array_merge($changeBase, [
+            'amount' => $amount,
+            'balance' => $hdBalancePay,
+        ]), true);
+
+        // 返充入账后:优先结当前售后原单挂账,剩余再 FIFO(不再二次扣余额)
+        $balanceBefore = bcsub($customBalance, $amount, 2);
+        self::fifoClearAfterRefundCredit($custom, $hd, $refund, $amount, $balanceBefore, $order);
+        $custom = CustomClass::getById($customId, true);
+        return bcadd((string)($custom->balance ?? $customBalance), '0', 2);
     }
 
     //欠款和余额支付的售后 ssh 20250803
@@ -691,6 +724,10 @@ class CustomClass extends BaseClass
         if (empty($hd)) {
             util::fail('花店信息缺失,编号5362');
         }
+        // 入账前净余额,供结账单 payWay 判定
+        $balanceBefore = bcadd((string)($custom->balance ?? '0'), '0', 2);
+        // 原始支付方式:后面流水会改写成余额付,FIFO 判定要用原值
+        $originPayWay = intval($payWay);
         $customBalance = bcadd($custom->balance, $amount, 2);
         $custom->balance = $customBalance;
         if ($custom->balance < 0) {
@@ -852,6 +889,137 @@ class CustomClass extends BaseClass
             }
             $order->save();
         }
+
+        // 当天挂账售后已先减 remainDebtPrice,再 FIFO 优先本单会重复销账;仅隔天/返充类或余额付原路才消欠
+        $sameDay = intval($refund->sameDay ?? 1);
+        $debtPay = intval(dict::getDict('payWay', 'debtPay'));
+        $skipFifo = ($originPayWay === $debtPay && $sameDay === 1);
+        if (!$skipFifo) {
+            self::fifoClearAfterRefundCredit($custom, $hd, $refund, $amount, $balanceBefore, $order);
+        }
+    }
+
+    /**
+     * 售后返充/原路退余额后销挂账:优先当前售后原单 remainDebtPrice(可部分结),再按 id 升序 FIFO。
+     * 调用时机:余额已入账;本方法只用本次金额作资金池,不再扣 balance。
+     *
+     * @param object $custom 客户(已加锁)
+     * @param object $hd 花店账户
+     * @param object $refund 售后单
+     * @param string|float $amount 本次入账金额
+     * @param string|float $balanceBefore 入账前净余额
+     * @param object|null $order 售后关联销售单
+     */
+    protected static function fifoClearAfterRefundCredit($custom, $hd, $refund, $amount, $balanceBefore, $order = null)
+    {
+        $pool = bcadd((string)$amount, '0', 2);
+        if (bccomp($pool, '0', 2) <= 0) {
+            return;
+        }
+        $customId = intval($custom->id ?? 0);
+        if ($customId <= 0) {
+            return;
+        }
+
+        $preferOrderId = !empty($order) ? intval($order->id ?? 0) : intval($refund->orderId ?? 0);
+        $plan = self::planFifoClearByPool($customId, $pool, $preferOrderId);
+        if (empty($plan)) {
+            return;
+        }
+
+        $shopId = intval($refund->shopId ?? ($custom->shopId ?? 0));
+        $shop = $shopId > 0 ? ShopClass::getById($shopId, true) : null;
+        if (empty($shop)) {
+            // 无门店无法建结账单,跳过销账(余额已入账,不阻断售后)
+            return;
+        }
+
+        $settleParams = [
+            'staffId' => intval($refund->shopAdminId ?? 0),
+            'staffName' => (string)($refund->shopAdminName ?? ''),
+        ];
+        $set = SettleClass::addSettle($plan, $shop, $custom, $hd, $settleParams);
+        if (!empty($set)) {
+            SettleClass::clearSettle($set);
+        }
+        // 刷新客户欠款标记(有未结挂账单则为欠款户)
+        $leftDebt = OrderClass::getAllByCondition(['customId' => $customId, 'debt' => 1], null, 'id', null, true);
+        $customFresh = self::getLockById($customId);
+        if (!empty($customFresh)) {
+            $customFresh->isDebt = !empty($leftDebt) ? 1 : 0;
+            $customFresh->save(false, ['isDebt']);
+        }
+    }
+
+    /**
+     * 销挂账分配:可选优先本单,剩余按订单 id 升序 FIFO。
+     * prefer 路径只用 remainDebtPrice,不用 actPrice 兜底,避免脏数据多销。
+     *
+     * @param int $customId
+     * @param string|float $poolAmount
+     * @param int $preferOrderId
+     * @return array<int, array{orderId:int, clearAmount:string, orderSn:string}>
+     */
+    protected static function planFifoClearByPool($customId, $poolAmount, $preferOrderId = 0)
+    {
+        $pool = bcadd((string)$poolAmount, '0', 2);
+        if (bccomp($pool, '0', 2) <= 0) {
+            return [];
+        }
+        $orderList = OrderClass::getAllByCondition(['customId' => $customId, 'debt' => 1], 'id asc', '*', null, true);
+        if (empty($orderList)) {
+            return [];
+        }
+
+        $plan = [];
+        $used = '0.00';
+        $preferOrderId = intval($preferOrderId);
+
+        // 1)售后原单仍挂账:先结本单(不够则部分结)
+        if ($preferOrderId > 0) {
+            foreach ($orderList as $ord) {
+                if (intval($ord->id ?? 0) !== $preferOrderId) {
+                    continue;
+                }
+                $remain = bcadd((string)($ord->remainDebtPrice ?? '0'), '0', 2);
+                if (bccomp($remain, '0', 2) <= 0) {
+                    break;
+                }
+                $clearAmount = bccomp($remain, $pool, 2) <= 0 ? $remain : $pool;
+                $plan[] = [
+                    'orderId' => $preferOrderId,
+                    'clearAmount' => $clearAmount,
+                    'orderSn' => $ord->orderSn ?? '',
+                ];
+                $used = bcadd($used, $clearAmount, 2);
+                break;
+            }
+        }
+
+        // 2)剩余金额 FIFO(跳过已优先处理的本单)
+        foreach ($orderList as $ord) {
+            $left = bcsub($pool, $used, 2);
+            if (bccomp($left, '0', 2) <= 0) {
+                break;
+            }
+            $oid = intval($ord->id ?? 0);
+            if ($preferOrderId > 0 && $oid === $preferOrderId) {
+                continue;
+            }
+            $remain = bcadd((string)($ord->remainDebtPrice ?? '0'), '0', 2);
+            if (bccomp($remain, '0', 2) <= 0) {
+                continue;
+            }
+            $clearAmount = bccomp($remain, $left, 2) <= 0 ? $remain : $left;
+            $plan[] = [
+                'orderId' => $oid,
+                'clearAmount' => $clearAmount,
+                'orderSn' => $ord->orderSn ?? '',
+            ];
+            $used = bcadd($used, $clearAmount, 2);
+        }
+
+        return $plan;
     }
 
     //添加客户 ssh 2021.2.28

+ 51 - 7
biz-hd/homePageConfig/classes/HomePageDisplayClass.php

@@ -3,13 +3,15 @@
 namespace bizHd\homePageConfig\classes;
 
 use biz\shop\classes\ShopClass;
+use bizHd\goods\classes\GoodsClass;
+use bizHd\goods\classes\GoodsSettingClass;
 use common\components\business;
 use common\components\util;
 
 /**
  * 门店首页"展示数据"组装类
  * 把 HomePageConfigClass/HomePageModuleClass 存的原始配置,格式化为可直接渲染的结构
- * (拼好图片完整URL、过滤未开启/未上架内容、解析出真实商品列表等)。
+ * (拼好图片完整URL、过滤未开启/未上架内容、解析出真实商品列表、普通商品套上商城涨价等)。
  * 供 app-mall(顾客端商城首页)与 app-hd(商家端配置预览)共用同一套数据,
  * 保证"预览"看到的效果与顾客端真实首页完全一致。
  */
@@ -24,6 +26,8 @@ class HomePageDisplayClass
      */
     public static function buildHome($mainId, $shopId)
     {
+        // 三个普通商品模块共用一次涨价配置,避免重复查库
+        $rise = GoodsSettingClass::getRise($mainId);
         return [
             'modules' => HomePageConfigClass::getModules($mainId, $shopId),
             'topNav' => self::formatTopNav($mainId, HomePageConfigClass::getTopNav($mainId, $shopId)),
@@ -31,9 +35,9 @@ class HomePageDisplayClass
             'navGrid' => self::formatNavGrid(HomePageModuleClass::getNavGrid($mainId, $shopId)),
             'seckill' => self::formatActivity(HomePageModuleClass::getSeckill($mainId, $shopId, true)),
             'groupBuy' => self::formatActivity(HomePageModuleClass::getGroupBuy($mainId, $shopId, true)),
-            'hot' => self::formatGoodsSection($mainId, $shopId, 'hot'),
-            'new' => self::formatGoodsSection($mainId, $shopId, 'new'),
-            'pullGoods' => self::formatGoodsSection($mainId, $shopId, 'pullGoods'),
+            'hot' => self::formatGoodsSection($mainId, $shopId, 'hot', $rise),
+            'new' => self::formatGoodsSection($mainId, $shopId, 'new', $rise),
+            'pullGoods' => self::formatGoodsSection($mainId, $shopId, 'pullGoods', $rise),
         ];
     }
 
@@ -173,13 +177,15 @@ class HomePageDisplayClass
 
     /**
      * 格式化热门推荐/今日上新/下拉商品:关闭时清空商品;开启时按真实商品数量自动推导列数与展示条数
+     * 输出价格为商城涨价后的售价(与详情页 obeyRiseRule 一致);秒杀/团购不走本方法
      *
      * @param int $mainId
      * @param int $shopId
      * @param string $moduleKey hot|new|pullGoods
+     * @param array|null $rise 已查好的涨价配置;单模块接口未传时内部再查一次
      * @return array
      */
-    public static function formatGoodsSection($mainId, $shopId, $moduleKey)
+    public static function formatGoodsSection($mainId, $shopId, $moduleKey, $rise = null)
     {
         $data = HomePageModuleClass::getGoodsSection($mainId, $shopId, $moduleKey);
         if (empty($data['enabled'])) {
@@ -195,7 +201,8 @@ class HomePageDisplayClass
         if ($layout['displayCount'] > 0 && $layout['displayCount'] < $total) {
             $rows = array_slice($rows, 0, $layout['displayCount']);
         }
-        $data['goods'] = HomePageModuleClass::formatGoodsRows($rows);
+        // 首页列表必须走与详情页相同的涨价公式,否则顾客看到原价、点进详情才变价
+        $data['goods'] = self::applyMallRisePrice(HomePageModuleClass::formatGoodsRows($rows), $mainId, $rise);
         $data['goodsTotal'] = $total;
         return $data;
     }
@@ -257,7 +264,8 @@ class HomePageDisplayClass
                     $page,
                     $pageSize
                 );
-                $list = $paged['list'];
+                // 「更多」列表与首页模块同源,同样补涨价;秒杀/团购走活动价,上面分支不套涨价
+                $list = self::applyMallRisePrice($paged['list'], $mainId);
                 $total = $paged['total'];
             }
         }
@@ -277,4 +285,40 @@ class HomePageDisplayClass
             'endTime' => $activityEndTime,
         ];
     }
+
+    /**
+     * 给首页普通商品套上店铺涨价后的售价
+     * 只走 obeyRiseRule 纯计算,不走 getFinalPrice(后者每条商品 Redis 查聊天报价,
+     * 首页游客 userId=0 必 miss,下拉商品全量时会拖慢约 1 秒)。
+     * 无价商品(priceType=0)不涨价;秒杀/团购走活动价,不要调用本方法。
+     *
+     * @param array $goods formatGoodsRows 产出的商品列表
+     * @param int $mainId 商户 mainId,未传入 $rise 时用来查涨价配置
+     * @param array|null $rise GoodsSettingClass::getRise 的返回值,传入则不再查库
+     * @return array
+     */
+    public static function applyMallRisePrice($goods, $mainId, $rise = null)
+    {
+        if (empty($goods) || !is_array($goods)) {
+            return $goods;
+        }
+        if (!is_array($rise)) {
+            $rise = intval($mainId) > 0 ? GoodsSettingClass::getRise($mainId) : [];
+        }
+        $riseSwitch = intval($rise['riseSwitch'] ?? 0);
+        $riseType = intval($rise['riseType'] ?? 0);
+        $riseAmount = floatval($rise['riseAmount'] ?? 0);
+        // 未开启涨价时直接返回原价,避免对全量下拉商品做无意义循环
+        if ($riseSwitch != 1 || $riseAmount <= 0) {
+            return $goods;
+        }
+        foreach ($goods as $key => $item) {
+            // 无价商品不参与涨价,与详情页 modifyAndRisePrice 一致
+            if (isset($item['priceType']) && intval($item['priceType']) === 0) {
+                continue;
+            }
+            $goods[$key] = GoodsClass::obeyRiseRule($item, $riseSwitch, $riseType, $riseAmount, [], []);
+        }
+        return $goods;
+    }
 }

+ 5 - 5
biz-hd/order/classes/OrderClass.php

@@ -82,8 +82,8 @@ class OrderClass extends BaseClass
     ];
 
     /**
-     * 是否必须走天售后(挂账已结清/超额、或可售后时限已过)
-     * @return array{shAddPay:int,shAddPayReason:int} shAddPay=1 表示天售后;reason:0欠款已结清/超额,1可售后时限已过
+     * 是否必须走天售后(挂账已结清/超额、或可售后时限已过)
+     * @return array{shAddPay:int,shAddPayReason:int} shAddPay=1 表示天售后;reason:0欠款已结清/超额,1可售后时限已过
      */
     public static function ifShPay($order, $main, $refundAmount = 0)
     {
@@ -93,11 +93,11 @@ class OrderClass extends BaseClass
         if ($payStatus == 1) {
             if ($order['payWay'] == 3) {
                 if ($order['remainDebtPrice'] <= 0) {
-                    // 挂账已结清:走天售后(写 nextRefundNum/nextDayTkPrice)
+                    // 挂账已结清:走天售后(写 nextRefundNum/nextDayTkPrice)
                     $shAddPay = 1;
                 } else {
                     if ($refundAmount > 0 && $refundAmount > $order['remainDebtPrice']) {
-                        // 售后金额大于剩余欠款:走天售后
+                        // 售后金额大于剩余欠款:走天售后
                         $shAddPay = 1;
                     }
                 }
@@ -105,7 +105,7 @@ class OrderClass extends BaseClass
             $cRet = OrderClass::couldRefund($order, $main);
             $could = $cRet['could'];
             if (!$could) {
-                // 可售后时限已过:走天售后
+                // 可售后时限已过:走天售后
                 $shAddPay = 1;
                 $shAddPayReason = 1;
             }

+ 2 - 2
biz-hd/purchase/models/PurchaseItem.php

@@ -6,7 +6,7 @@ use bizHd\base\models\Base;
 
 /**
  * 花店采购单明细 xhCgItem
- * nextRefundNum:天售后已退数量;当天已退 = refundNum - nextRefundNum
+ * nextRefundNum:天售后已退数量;当天已退 = refundNum - nextRefundNum
  */
 class PurchaseItem extends Base
 {
@@ -16,4 +16,4 @@ class PurchaseItem extends Base
 		return 'xhCgItem';
 	}
 	
-}
+}

+ 52 - 12
biz-hd/refund/classes/HdNextDayRefundClass.php

@@ -1,6 +1,6 @@
 <?php
 /**
- * HD 零售天售后 / 无原单退款业务类
+ * HD 零售天售后 / 无原单退款业务类
  * 用途:sameDay(当天/隔天)判定、支付快照与资金三态、隔天金额写入、无原单通过与返充;
  *       以及收入/销量统计用的隔天扣减查询(对齐 ghs NextDayRefundClass)。
  * 说明:sameDay 只区分当天/隔天,有无原单看 orderId;无原单常复用 sameDay=0 资金路径。
@@ -52,33 +52,73 @@ class HdNextDayRefundClass
     }
 
     /**
-     * 是否必须按天售后(sameDay=0):
+     * 是否必须按天售后(sameDay=0):
      * 非付款日 / 挂账已结清 / 已过可售后时限
      * @param object $order
      * @param object|null $main
      */
     public static function mustUseNextDay($order, $main = null)
     {
+        $check = self::checkEligible($order, $main);
+        return !empty($check['ok']);
+    }
+
+    /**
+     * 原单是否可走跨天售后(售后页预检,对齐 ghs NextDayRefundClass::checkEligible)
+     * @return array{ok:bool,reason:string,sameDay:int,orderCleared:int,hasPayTime:int}
+     */
+    public static function checkEligible($order, $main = null)
+    {
+        if (empty($order)) {
+            return [
+                'ok' => false,
+                'reason' => '没有原订单',
+                'sameDay' => self::SAME_DAY_YES,
+                'orderCleared' => 0,
+                'hasPayTime' => 0,
+            ];
+        }
         if (!self::hasValidPayTime($order)) {
-            return false;
+            return [
+                'ok' => false,
+                'reason' => '订单无支付时间,不能售后',
+                'sameDay' => self::SAME_DAY_YES,
+                'orderCleared' => 0,
+                'hasPayTime' => 0,
+            ];
         }
         $payTime = $order->payTime;
-        if (date('Y-m-d', strtotime($payTime)) !== date('Y-m-d')) {
-            return true;
-        }
+        $isToday = date('Y-m-d', strtotime($payTime)) === date('Y-m-d');
         $debtPay = intval(dict::getDict('payWay', 'debtPay'));
+        $debtCleared = false;
         if (intval($order->payWay ?? 0) === $debtPay) {
-            if (bccomp((string)($order->remainDebtPrice ?? '0'), '0', 2) <= 0) {
-                return true;
-            }
+            $debtCleared = bccomp((string)($order->remainDebtPrice ?? '0'), '0', 2) <= 0;
         }
+        $orderCleared = !empty($order->clearId);
+        $overTimeLimit = false;
         if (!empty($main)) {
             $cRet = OrderClass::couldRefund($order, $main);
             if (empty($cRet['could'])) {
-                return true;
+                $overTimeLimit = true;
             }
         }
-        return false;
+        // 当天且未结清、未超时 → 普通当天售后
+        if ($isToday && !$debtCleared && !$orderCleared && !$overTimeLimit) {
+            return [
+                'ok' => false,
+                'reason' => '当天未结账订单请走普通售后;已结账/挂账结清/超时/隔天可售后',
+                'sameDay' => self::SAME_DAY_YES,
+                'orderCleared' => 0,
+                'hasPayTime' => 1,
+            ];
+        }
+        return [
+            'ok' => true,
+            'reason' => '',
+            'sameDay' => self::SAME_DAY_NO,
+            'orderCleared' => ($orderCleared || $debtCleared) ? 1 : 0,
+            'hasPayTime' => 1,
+        ];
     }
 
     /**
@@ -402,7 +442,7 @@ class HdNextDayRefundClass
     }
 
     /**
-     * 按时段汇总成功天售后金额(收入统计扣减用,对齐 ghs NextDayRefundClass)
+     * 按时段汇总成功天售后金额(收入统计扣减用,对齐 ghs NextDayRefundClass)
      */
     public static function sumAmountByMainAndTime($mainId, $startTime, $endTime)
     {

+ 2 - 2
biz-hd/refund/classes/HdRefundClass.php

@@ -39,7 +39,7 @@ class HdRefundClass extends BaseClass
 
     /** 1当天售后(默认) */
     const SAME_DAY_YES = 1;
-    /** 0天售后(与有无原单无关;无原单看 orderId/orderSn) */
+    /** 0天售后(与有无原单无关;无原单看 orderId/orderSn) */
     const SAME_DAY_NO = 0;
 
     public static function onlyRefundAmount($shop, $order)
@@ -176,4 +176,4 @@ class HdRefundClass extends BaseClass
         return !empty($row);
     }
 
-}
+}

+ 204 - 34
biz-hd/refund/classes/HdRefundMallClass.php

@@ -15,6 +15,7 @@ use bizHd\order\classes\OrderGoodsClass;
 use bizHd\order\classes\OrderItemClass;
 use bizHd\refund\services\HdRefundService;
 use bizHd\shop\classes\ShopClass;
+use biz\shop\classes\MainClass;
 use biz\shop\classes\ShopClass as SharedShopClass;
 use common\components\imgUtil;
 use common\components\orderSn;
@@ -66,8 +67,17 @@ class HdRefundMallClass
             util::fail('退款金额超过可退金额');
         }
 
+        // 申请时写入原单支付快照,待审核详情/列表即可展示退回方式;通过时 addRefund 会再按原单刷新
+        $snap = HdNextDayRefundClass::buildRefundPaySnapshot($order, true);
+        // 预判当天/封账退款口径,便于待审核展示;最终以审核通过时 mustUseNextDay 为准
+        $mainId = (int)($extra['mainId'] ?? $order->mainId ?? 0);
+        $main = $mainId > 0 ? MainClass::getById($mainId, true) : null;
+        $sameDay = HdNextDayRefundClass::mustUseNextDay($order, $main)
+            ? HdRefundClass::SAME_DAY_NO
+            : HdRefundClass::SAME_DAY_YES;
+
         $data = [
-            'mainId' => (int)($extra['mainId'] ?? $order->mainId ?? 0),
+            'mainId' => $mainId,
             'shopId' => (int)($extra['shopId'] ?? $order->shopId ?? 0),
             'sjId' => (int)($extra['sjId'] ?? $order->sjId ?? 0),
             'hdId' => (int)($extra['hdId'] ?? $order->hdId ?? 0),
@@ -79,6 +89,8 @@ class HdRefundMallClass
             'refundSn' => orderSn::getHdRefundSn(),
             'refundType' => $refundType,
             'product' => !empty($productList) ? json_encode($productList, JSON_UNESCAPED_UNICODE) : '',
+            // 顾客上传售后图短路径 JSON,审核页展开为 smallImgList
+            'imgList' => self::normalizeImgListJson($post['imgList'] ?? ''),
             'refundPrice' => round((float)$refundPrice, 2),
             'remark' => trim((string)($post['remark'] ?? '')),
             'status' => HdRefundClass::STATUS_PENDING,
@@ -86,9 +98,12 @@ class HdRefundMallClass
             'shopAdminName' => '',
             'passTime' => null,
             'rejectReason' => '',
-            'hasReturn' => 0,
-            'returnBalance' => 0,
-            'couldReturn' => 1,
+            'sameDay' => $sameDay,
+            'onlinePay' => $snap['onlinePay'],
+            'payWay' => $snap['payWay'],
+            'hasReturn' => $snap['hasReturn'],
+            'returnBalance' => $snap['returnBalance'],
+            'couldReturn' => $snap['couldReturn'],
         ];
 
         // 撤销后重新申请:复用已取消记录,避免同一订单堆多条取消单
@@ -101,6 +116,85 @@ class HdRefundMallClass
         return HdRefundClass::add($data, true);
     }
 
+    /**
+     * 规范化售后图片字段:只保留短路径字符串列表再 JSON 编码
+     * @param string|array $imgList 前端 JSON 或数组
+     * @return string 空串或 JSON 数组字符串
+     */
+    public static function normalizeImgListJson($imgList)
+    {
+        $arr = [];
+        if (is_string($imgList)) {
+            $imgList = trim($imgList);
+            if ($imgList === '') {
+                return '';
+            }
+            $decoded = json_decode($imgList, true);
+            $arr = is_array($decoded) ? $decoded : [];
+        } elseif (is_array($imgList)) {
+            $arr = $imgList;
+        }
+        $paths = [];
+        foreach ($arr as $item) {
+            if (!is_string($item)) {
+                continue;
+            }
+            $path = trim($item);
+            // 若误传完整 URL,尽量截成短路径
+            if (strpos($path, 'http://') === 0 || strpos($path, 'https://') === 0) {
+                $parts = parse_url($path);
+                $path = ltrim((string)($parts['path'] ?? ''), '/');
+            }
+            if ($path === '') {
+                continue;
+            }
+            $paths[] = $path;
+            if (count($paths) >= 6) {
+                break;
+            }
+        }
+        if (empty($paths)) {
+            return '';
+        }
+        return json_encode($paths, JSON_UNESCAPED_UNICODE);
+    }
+
+    /**
+     * 给售后行挂上可预览的缩略图/大图列表(对齐 ghs refundDetail)
+     * @param array $row
+     * @return array
+     */
+    public static function attachImgDisplayFields($row)
+    {
+        if (!is_array($row)) {
+            return $row;
+        }
+        $imgString = $row['imgList'] ?? '';
+        $imgArr = [];
+        if (is_string($imgString) && $imgString !== '') {
+            $decoded = json_decode($imgString, true);
+            if (is_array($decoded)) {
+                $imgArr = $decoded;
+            }
+        } elseif (is_array($imgString)) {
+            $imgArr = $imgString;
+        }
+        $smallImgList = [];
+        $bigImgList = [];
+        foreach ($imgArr as $image) {
+            if (!is_string($image) || trim($image) === '') {
+                continue;
+            }
+            $short = trim($image);
+            $smallImgList[] = imgUtil::groupImg($short) . '?x-oss-process=image/resize,m_fill,h_130,w_130';
+            $bigImgList[] = imgUtil::groupImg($short) . '?x-oss-process=image/resize,m_fill,h_700,w_700';
+        }
+        $row['imgList'] = $imgArr;
+        $row['smallImgList'] = $smallImgList;
+        $row['bigImgList'] = $bigImgList;
+        return $row;
+    }
+
     /**
      * 将已取消申请改回待审核
      * 顾客撤销后再次提交时复用同一条记录,刷新申请内容并清掉审核痕迹
@@ -352,16 +446,21 @@ class HdRefundMallClass
     }
 
     /**
-     * 剩余可退现金:实付合计 orderPrice 减已退 tkPrice
+     * 剩余可退现金:实付合计 orderPrice 减当天已退 tkPrice 与封账退款 nextDayTkPrice
      * 红包抵扣已计入 orderPrice,所以红包全额单这里为 0
      * @param array|object $order
      * @return string 两位小数字符串,不会为负
      */
     public static function remainRefundCash($order)
     {
+        $already = bcadd(
+            self::toMoney(self::orderField($order, 'tkPrice', 0)),
+            self::toMoney(self::orderField($order, 'nextDayTkPrice', 0)),
+            2
+        );
         $could = bcsub(
             self::toMoney(self::orderField($order, 'orderPrice', 0)),
-            self::toMoney(self::orderField($order, 'tkPrice', 0)),
+            $already,
             2
         );
         if (bccomp($could, '0', 2) < 0) {
@@ -477,24 +576,28 @@ class HdRefundMallClass
         // 复用待审核行写明细/回库/资金,避免再插一条
         $refund = HdRefundService::addRefund($post, $order, $refund);
 
-        $custom = CustomClass::getLockById($order->customId);
-        if (!empty($custom)) {
-            $custom->buyAmount = bcsub((string)$custom->buyAmount, (string)$refund->refundPrice, 2);
-            $customName = $custom->name ?? $order->customName ?? '';
-            $orderSnText = $order->orderSn ?? '';
-            $eventPrefix = $customName !== '' ? $customName : '客户';
-            $custom->eventRemark = $eventPrefix . '商城售后' . $orderSnText;
-            $custom->relateId = $order->id;
-            $custom->relateType = 1;
-            $custom->staffId = (int)($shopAdmin['id'] ?? 0);
-            $custom->staffName = $shopAdmin['name'] ?? '';
-            $custom->mainId = (int)($shopAdmin['mainId'] ?? $refund->mainId);
-            $custom->save();
+        // 当天售后才在此回退累计消费;封账/隔天已在 applyHdAmountAndFund 扣过,避免重复
+        $sameDay = intval($refund->sameDay ?? HdRefundClass::SAME_DAY_YES);
+        if ($sameDay === HdRefundClass::SAME_DAY_YES) {
+            $custom = CustomClass::getLockById($order->customId);
+            if (!empty($custom)) {
+                $custom->buyAmount = bcsub((string)$custom->buyAmount, (string)$refund->refundPrice, 2);
+                $customName = $custom->name ?? $order->customName ?? '';
+                $orderSnText = $order->orderSn ?? '';
+                $eventPrefix = $customName !== '' ? $customName : '客户';
+                $custom->eventRemark = $eventPrefix . '商城售后' . $orderSnText;
+                $custom->relateId = $order->id;
+                $custom->relateType = 1;
+                $custom->staffId = (int)($shopAdmin['id'] ?? 0);
+                $custom->staffName = $shopAdmin['name'] ?? '';
+                $custom->mainId = (int)($shopAdmin['mainId'] ?? $refund->mainId);
+                $custom->save();
 
-            $hd = HdClass::getLockById($custom->hdId);
-            if (!empty($hd)) {
-                $hd->expendAmount = bcsub((string)$hd->expendAmount, (string)$refund->refundPrice, 2);
-                $hd->save();
+                $hd = HdClass::getLockById($custom->hdId);
+                if (!empty($hd)) {
+                    $hd->expendAmount = bcsub((string)$hd->expendAmount, (string)$refund->refundPrice, 2);
+                    $hd->save();
+                }
             }
         }
 
@@ -548,15 +651,21 @@ class HdRefundMallClass
     }
 
     /**
-     * 顾客端售后列表(mallApp 退款售后页,跨店)
+     * 顾客端售后列表(mallApp 退款售后页,跨店;可按订单收窄
      * @param int $userId 当前登录商城用户
      * @param string $tab all|pending|0|refunded|1|closed|2|3
+     * @param int $orderId 可选,>0 时只返回该订单的顾客申请(订单详情「申请记录」)
      * @return array 分页 list + pendingCount
      */
-    public static function getCustomApplyList($userId, $tab = 'all')
+    public static function getCustomApplyList($userId, $tab = 'all', $orderId = 0)
     {
         $userId = (int)$userId;
+        $orderId = (int)$orderId;
         $where = ['userId' => $userId];
+        // 订单详情进入时只看本单,避免混入其它订单售后
+        if ($orderId > 0) {
+            $where['orderId'] = $orderId;
+        }
         $tab = (string)$tab;
         if ($tab === 'pending' || $tab === '0') {
             $where['status'] = HdRefundClass::STATUS_PENDING;
@@ -569,10 +678,14 @@ class HdRefundMallClass
         }
 
         $data = HdRefundClass::getList('*', $where, 'addTime DESC');
-        $data['pendingCount'] = (int)HdRefundClass::getCount([
+        $pendingWhere = [
             'userId' => $userId,
             'status' => HdRefundClass::STATUS_PENDING,
-        ]);
+        ];
+        if ($orderId > 0) {
+            $pendingWhere['orderId'] = $orderId;
+        }
+        $data['pendingCount'] = (int)HdRefundClass::getCount($pendingWhere);
         if (!empty($data['list'])) {
             $data['list'] = self::formatCustomList($data['list']);
         } else {
@@ -589,7 +702,9 @@ class HdRefundMallClass
     public static function formatCustomDetail($row)
     {
         $list = self::formatCustomList([$row]);
-        return $list[0] ?? $row;
+        $detail = $list[0] ?? $row;
+        // 详情再补图片预览字段,列表可不带大图
+        return self::attachImgDisplayFields($detail);
     }
 
     /**
@@ -610,11 +725,13 @@ class HdRefundMallClass
 
     /**
      * 列表卡片主状态文案,对齐设计稿「退款中·待商家处理 / 退款成功」
+     * 已返充时用「退款成功·已返充」,列表右上角也能看清
      * @param int $status
      * @param int $refundType 1退货退款 2仅退款
+     * @param array $row 可选,用于通过后按资金态细化标题
      * @return string
      */
-    public static function displayStatusTitle($status, $refundType)
+    public static function displayStatusTitle($status, $refundType, $row = [])
     {
         $status = (int)$status;
         if ($status === HdRefundClass::STATUS_PENDING) {
@@ -623,6 +740,15 @@ class HdRefundMallClass
                 : '退款中·待商家处理';
         }
         if ($status === HdRefundClass::STATUS_PASS) {
+            if ((int)($row['returnBalance'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
+                return '退款成功·已返充';
+            }
+            if ((int)($row['hasReturn'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
+                return '退款成功';
+            }
+            if ((int)($row['couldReturn'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
+                return '退款成功·待退还';
+            }
             return '退款成功';
         }
         if ($status === HdRefundClass::STATUS_REJECT) {
@@ -635,7 +761,31 @@ class HdRefundMallClass
     }
 
     /**
-     * 卡片底部提示:处理中给审核时效,通过后带审核时间
+     * 资金落地文案:返充 / 原路 / 待商家退还(供列表 tip 与详情「退回方式」)
+     * @param array $row
+     * @return string
+     */
+    public static function fundStatusText($row)
+    {
+        $status = (int)($row['status'] ?? 0);
+        if ($status !== HdRefundClass::STATUS_PASS) {
+            return '';
+        }
+        if ((int)($row['returnBalance'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
+            return '已返充到余额';
+        }
+        if ((int)($row['hasReturn'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
+            return '已原路退回';
+        }
+        // 审核通过但线下等尚未选手选落地
+        if ((int)($row['couldReturn'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
+            return '等待商家退还';
+        }
+        return '退款处理中';
+    }
+
+    /**
+     * 卡片底部提示:处理中给审核时效;通过后按返充/原路/待退还区分,避免一律写「原路退回」
      * @param array $row
      * @return string
      */
@@ -646,11 +796,23 @@ class HdRefundMallClass
             return '商家审核中·预计 24 小时内处理';
         }
         if ($status === HdRefundClass::STATUS_PASS) {
+            $fundText = self::fundStatusText($row);
             $time = trim((string)($row['passTime'] ?? ''));
-            if ($time !== '' && $time !== '0000-00-00 00:00:00') {
-                return '退款已到账·原路退回 ' . $time;
+            $timeOk = $time !== '' && $time !== '0000-00-00 00:00:00';
+            if ((int)($row['returnBalance'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
+                return $timeOk
+                    ? ('退款已到账·已返充到余额 ' . $time)
+                    : '退款已到账·已返充到余额';
+            }
+            if ((int)($row['hasReturn'] ?? 0) === HdNextDayRefundClass::FLAG_YES) {
+                return $timeOk
+                    ? ('退款已到账·原路退回 ' . $time)
+                    : '退款已到账·原路退回';
+            }
+            if ($fundText !== '') {
+                return $timeOk ? ($fundText . ' ' . $time) : $fundText;
             }
-            return '退款已到账·原路退回';
+            return '退款已通过';
         }
         if ($status === HdRefundClass::STATUS_REJECT) {
             $reason = trim((string)($row['rejectReason'] ?? ''));
@@ -810,13 +972,21 @@ class HdRefundMallClass
         $row['hdName'] = $displayName !== '' ? $displayName : '花店';
         $row['hdAvatar'] = self::coverUrl($shop['avatar'] ?? '');
         $row['statusText'] = self::statusText($status);
-        $row['statusTitle'] = self::displayStatusTitle($status, $refundType);
+        $row['statusTitle'] = self::displayStatusTitle($status, $refundType, $row);
         $row['statusTip'] = self::displayStatusTip($row);
+        $row['fundStatusText'] = self::fundStatusText($row);
         $row['statusTone'] = self::statusTone($status);
         $row['refundTypeText'] = self::refundTypeText($refundType);
         $row['reasonText'] = self::reasonText($row);
         $row['canModify'] = $status === HdRefundClass::STATUS_PENDING ? 1 : 0;
         $row['refundPrice'] = round((float)($row['refundPrice'] ?? 0), 2);
+        // 详情页「审核时间」读 auditTime;库字段是 passTime
+        $passTime = trim((string)($row['passTime'] ?? ''));
+        $row['auditTime'] = ($passTime !== '' && $passTime !== '0000-00-00 00:00:00') ? $passTime : '';
+        // 资金三态显式转 int,前端判断返充/原路更稳
+        $row['returnBalance'] = (int)($row['returnBalance'] ?? 0);
+        $row['hasReturn'] = (int)($row['hasReturn'] ?? 0);
+        $row['couldReturn'] = (int)($row['couldReturn'] ?? 0);
         return $row;
     }
 

+ 39 - 15
biz-hd/refund/services/HdRefundService.php

@@ -1,7 +1,7 @@
 <?php
 /**
  * HD 零售售后服务
- * 用途:有原单当天/天售后、无原单退款;隔天写 nextRefundNum/nextDayTkPrice,对齐资金三态。
+ * 用途:有原单当天/天售后、无原单退款;隔天写 nextRefundNum/nextDayTkPrice,对齐资金三态。
  */
 namespace bizHd\refund\services;
 
@@ -65,29 +65,28 @@ class HdRefundService extends BaseService
             $sameDay = HdRefundClass::SAME_DAY_NO;
         }
 
-        // 可退金额:当天看 tkPrice;隔天看 tkPrice+nextDayTkPrice 合计不超过 mainPay
-        $mainPay = bcadd((string)($order->mainPay ?? '0'), '0', 2);
+        // 可退上限与商城申请 remainRefundCash 对齐:用 orderPrice(实付合计),
+        // 不要只卡 mainPay——组合付 / 会员折后 mainPay 与 orderPrice 不一致时,申请能过、审核会误报「可退款金额不足」
         $alreadyTk = bcadd((string)($order->tkPrice ?? '0'), '0', 2);
         $alreadyNext = bcadd((string)($order->nextDayTkPrice ?? '0'), '0', 2);
+        $payCeiling = self::resolveRefundPayCeiling($order);
+        $totalRefunded = bcadd(bcadd($alreadyTk, $alreadyNext, 2), $refundPrice, 2);
+        if (bccomp($totalRefunded, $payCeiling, 2) > 0) {
+            util::fail('可退款金额不足');
+        }
+
         if ($sameDay === HdRefundClass::SAME_DAY_YES) {
-            $currentTkPrice = bcadd($alreadyTk, $refundPrice, 2);
-            if (bccomp($currentTkPrice, $mainPay, 2) > 0) {
-                util::fail('可退款金额不足');
-            }
-            $order->tkPrice = $currentTkPrice;
+            $order->tkPrice = bcadd($alreadyTk, $refundPrice, 2);
             $order->refund = OrderClass::REFUND_YES;
+            // 当天售后扣减 actPrice;若历史数据 actPrice 略小于应退额,钳到 0(上限已用 orderPrice 校验过)
             $currentActPrice = bcsub((string)($order->actPrice ?? '0'), $refundPrice, 2);
             if (bccomp($currentActPrice, '0', 2) < 0) {
-                util::fail('可退款金额不足!');
+                $currentActPrice = '0.00';
             }
             $order->actPrice = $currentActPrice;
             $order->realPrice = $currentActPrice;
             $order->save();
         } else {
-            $totalRefunded = bcadd(bcadd($alreadyTk, $alreadyNext, 2), $refundPrice, 2);
-            if (bccomp($totalRefunded, $mainPay, 2) > 0) {
-                util::fail('可退款金额不足');
-            }
             // 隔天金额在通过资金段 applyHdAmountAndFund 写入 nextDayTkPrice
             $order->refund = OrderClass::REFUND_YES;
             $order->save(false, ['refund']);
@@ -368,6 +367,31 @@ class HdRefundService extends BaseService
         return HdRefundClass::getById($refund->id, true);
     }
 
+    /**
+     * 售后可退金额上限(与商城申请 remainRefundCash 同一口径)
+     * 优先 orderPrice;为空时回退 mainPay+cash,避免组合付/折后主付字段与实付合计不一致
+     * @param object $order
+     * @return string 两位小数字符串
+     */
+    protected static function resolveRefundPayCeiling($order)
+    {
+        $orderPrice = bcadd((string)($order->orderPrice ?? '0'), '0', 2);
+        if (bccomp($orderPrice, '0', 2) > 0) {
+            return $orderPrice;
+        }
+        $mainPay = bcadd((string)($order->mainPay ?? '0'), '0', 2);
+        $cash = bcadd((string)($order->cash ?? '0'), '0', 2);
+        $sum = bcadd($mainPay, $cash, 2);
+        if (bccomp($sum, '0', 2) > 0) {
+            return $sum;
+        }
+        // 再回退 actPrice + 已退(还原下单实付)
+        $act = bcadd((string)($order->actPrice ?? '0'), '0', 2);
+        $tk = bcadd((string)($order->tkPrice ?? '0'), '0', 2);
+        $next = bcadd((string)($order->nextDayTkPrice ?? '0'), '0', 2);
+        return bcadd(bcadd($act, $tk, 2), $next, 2);
+    }
+
     /**
      * 拉卡拉线上原路退款
      */
@@ -406,7 +430,7 @@ class HdRefundService extends BaseService
 
     /**
      * 无原单退款(工作台「退款」):以 orderId=0、空 orderSn 标识无原单;
-     * sameDay 写 0 表示走天售后资金路径(字段本身不表示无原单),立即完成,资金留给成功页。
+     * sameDay 写 0 表示走天售后资金路径(字段本身不表示无原单),立即完成,资金留给成功页。
      * @param array $post
      * @return array
      */
@@ -463,7 +487,7 @@ class HdRefundService extends BaseService
             'remark' => $remark,
             'refundPrice' => $refundPrice,
             'refundType' => $refundType,
-            // 无原单复用天售后资金路径,故 sameDay=0;是否无原单以 orderId=0 为准
+            // 无原单复用天售后资金路径,故 sameDay=0;是否无原单以 orderId=0 为准
             'sameDay' => HdRefundClass::SAME_DAY_NO,
             'onlinePay' => $snap['onlinePay'],
             'payWay' => $snap['payWay'],

+ 1 - 1
biz-hd/shop/classes/ShopTemplateClass.php

@@ -159,6 +159,7 @@ class ShopTemplateClass extends BaseClass
 
     /**
      * 复制商品:先主记录再子规格,销量清零,masterId/kindId 按映射重写
+     * 商品按 mainId 归属,xhGoods 没有 shopId,不能按门店过滤
      * @param int $tplMainId
      * @param int $tplShopId
      * @param array $newShop
@@ -170,7 +171,6 @@ class ShopTemplateClass extends BaseClass
         $map = [];
         $list = GoodsClass::getAllByCondition([
             'mainId' => $tplMainId,
-            'shopId' => $tplShopId,
             'delStatus' => 0,
         ], 'id ASC');
         if (empty($list)) {

+ 1 - 1
biz-mall/order/services/OrderService.php

@@ -228,7 +228,7 @@ class OrderService extends BaseService
     public static function getOrderList($where)
     {
         // customId:订单列表「联系客服」进 chatPage 所需
-        $fields = 'id, shopId, hdId, customId, orderSn, groupBuyId, orderType, actPrice, goodsNum, reachDate, reachPeriod, status, addTime';
+        $fields = 'id, shopId, hdId, customId, orderSn, groupBuyId, orderType, actPrice,orderPrice, goodsNum, reachDate, reachPeriod, status, addTime';
         $data = self::getList($fields, $where, 'addTime DESC');
         if (empty($data['list'])) {
             return $data;