瀏覽代碼

结账流程优化

shish 1 月之前
父節點
當前提交
53d5ba79e7

+ 4 - 15
app-ghs/controllers/OrderClearController.php

@@ -6,6 +6,7 @@ use biz\ghs\classes\GhsClass;
 use biz\wx\classes\WxMessageClass;
 use bizGhs\clear\classes\OrderCgClearClass;
 use bizGhs\custom\classes\CustomClass;
+use bizGhs\custom\services\CustomPaymentAllocateService;
 use bizGhs\order\classes\OrderClearClass;
 use bizGhs\shop\classes\ShopClass;
 use bizHd\purchase\classes\PurchaseClearClass;
@@ -128,10 +129,6 @@ class OrderClearController extends BaseController
         if ($ghs->shopId != $this->shopId) {
             util::fail('没有获得授权');
         }
-        if ($ghs->balance > 0) {
-            util::mistake('客户有余额,不能通过订单销账,请走充值。有疑问请联系技术员', false, 'none', 3000);
-        }
-
         $staff = $this->shopAdmin;
         if ($staff->finance == 0) {
             $speShopList = [4608, 17118, 72366, 78556];
@@ -256,21 +253,13 @@ class OrderClearController extends BaseController
             $clear->payRemark = $payRemark;
             $clear->save();
 
-            $amount = $clear->actPrice ?? 0;
             $customId = $clear->customId ?? 0;
             $custom = CustomClass::getLockById($customId);
             $shop = $this->shop;
             $staff = $this->shopAdmin;
-            //充值
-            $return = CustomClass::rechargeBalance($custom, $amount, $shop, $staff, $payWay);
-            $custom = $return['custom'];
-            $ghs = $return['ghs'];
-            $customRecharge = $return['customRecharge'];
-            //结账(现金已在 rechargeBalance 记过,不再重复入账)
-            OrderClearClass::confirmClear($clear, $payWay, ['skipCashMoney' => true]);
-            $clearInfo = OrderClearClass::getLockById($id);
-            //结账消余额
-            CustomClass::clearConsumeBalance($amount, $custom, $ghs, $shop, $staff, $clearInfo);
+            // 客户已转账:来款入账 → 只销本结账单 → 扣净余额(CustomPaymentAllocateService,勿再 recharge+confirm+consume 三步)
+            $return = CustomPaymentAllocateService::linkedClearAfterRecharge($custom, $ghs, $shop, $staff, $clear, $payWay);
+            $customRecharge = $return['customRecharge'] ?? null;
             $transaction->commit();
             //充值销账通知
             WxMessageClass::customRechargeClearInform($shop, $customRecharge);

+ 4 - 0
app-hd/controllers/CustomRechargeController.php

@@ -2,6 +2,7 @@
 
 namespace hd\controllers;
 
+use bizGhs\custom\classes\AccountMoneyClass;
 use bizGhs\custom\classes\CustomClass;
 use bizGhs\custom\classes\CustomRechargeClass;
 use bizGhs\ghs\classes\GhsRechargeClass;
@@ -46,6 +47,9 @@ class CustomRechargeController extends BaseController
         if (empty($custom)) {
             util::fail('供货商信息不完整那');
         }
+        // 花店自助充值建单前:挂账并入净 balance
+        AccountMoneyClass::ensureCustomMoneyReady($custom, true);
+        AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
         $capitalType = dict::getDict('capitalType', 'customRechargeToGhs', 'id');
 
         $customOrderSn = orderSn::getGhsCustomRechargeSn();

+ 2 - 3
app-hd/controllers/NoticeController.php

@@ -7,7 +7,6 @@ use biz\shop\classes\ShopClass;
 use bizGhs\custom\classes\CustomClass;
 use bizGhs\custom\classes\CustomRechargeClass;
 use bizGhs\express\classes\GhsDeliveryOrderClass;
-use bizGhs\order\classes\OrderClearClass;
 use bizGhs\order\classes\RefundOrderClass;
 use bizHd\cg\classes\CgRefundClass;
 use bizHd\ghs\classes\GhsClass;
@@ -132,7 +131,7 @@ class NoticeController extends PublicController
                 $ghsShopId = $ghs->shopId ?? 0;
                 $custom = CustomClass::getLockById($customId);
                 $shop = ShopClass::getById($ghsShopId, true);
-                OrderClearClass::useBalanceClear($custom, $ghs, $shop, null);
+                // 销账已在 CustomRechargeClass::thirdPay 内按本次来款 FIFO 完成,勿再 useBalanceClear
                 //充值销账通知
                 WxMessageClass::customRechargeClearInform($shop, $customRecharge);
             }
@@ -347,7 +346,7 @@ class NoticeController extends PublicController
                 $ghsShopId = $ghs->shopId ?? 0;
                 $custom = CustomClass::getLockById($customId);
                 $shop = ShopClass::getById($ghsShopId, true);
-                OrderClearClass::useBalanceClear($custom, $ghs, $shop, null);
+                // 销账已在 CustomRechargeClass::thirdPay 内按本次来款 FIFO 完成,勿再 useBalanceClear
                 //充值销账通知
                 WxMessageClass::customRechargeClearInform($shop, $customRecharge);
             }

+ 3 - 0
app-hd/controllers/PurchaseClearController.php

@@ -3,6 +3,7 @@
 namespace hd\controllers;
 
 use biz\ghs\classes\GhsClass;
+use bizGhs\custom\classes\AccountMoneyClass;
 use bizHd\purchase\classes\OrderCgClearClass;
 use bizHd\purchase\classes\PurchaseClearClass;
 use bizHd\shop\classes\ShopClass;
@@ -28,6 +29,8 @@ class PurchaseClearController extends BaseController
         if (empty($ghs)) {
             util::fail('没有供货商');
         }
+        // 花店向供货商结账建单前:挂账并入净 balance
+        AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
         //解决重复请求问题
         $staffId = $this->shopAdminId ?? 0;
         $cacheKey = 'hd_clear_order_pay_' . $ghsId . '_' . $staffId;

+ 2 - 0
biz-ghs/custom/classes/AccountMoneyClass.php

@@ -14,6 +14,8 @@ use Yii;
  * 合并:balance = 原 balance - 原 debtAmount,debtAmount 置 0,balanceMerged=1 表示已合并且勿重复写说明流水。
  * 写业务前调 ensureCustomMoneyReady / ensureGhsMoneyReady;列表/详情返回前调 formatMoneyForClient。
  * 旧 App(appVersion<3)由 formatMoneyForClient 拆回待结+余额双字段;新 App 只读净 balance。
+ *
+ * 充值销账(合并后来款 FIFO 销单):bizGhs\custom\services\CustomPaymentAllocateService
  */
 class AccountMoneyClass
 {

+ 18 - 0
biz-ghs/custom/classes/CustomBalanceChangeClass.php

@@ -11,9 +11,27 @@ class CustomBalanceChangeClass extends BaseClass
 
     public static $baseFile = '\bizGhs\custom\models\CustomBalanceChange';
 
+    /**
+     * 客户余额变动列表(供货商端 CustomBalanceChangeController)
+     * becomeBalance:本笔入账后「成为可用余额」的部分 = amount - clearAmount(对照零售 hd)
+     */
     public static function getChangeList($where)
     {
         $data = self::getList('*', $where, 'addTime DESC,id DESC');
+        if (!empty($data['list'])) {
+            foreach ($data['list'] as $k => $v) {
+                $clearAmount = $v['clearAmount'] ?? 0; // 本笔来款中用于销挂账的金额,列见 add_clear_audit_fields.sql
+                $amount = $v['amount'] ?? 0;
+                $io = $v['io'] ?? 0;
+                $becomeBalance = 0;
+                if ($io == 1) {
+                    // 仅入账方向计算「充完销账后还剩多少余额」
+                    $becomeBalance = bcsub($amount, $clearAmount, 2);
+                    $becomeBalance = floatval($becomeBalance);
+                }
+                $data['list'][$k]['becomeBalance'] = $becomeBalance;
+            }
+        }
         return $data;
     }
 

+ 5 - 1
biz-ghs/custom/classes/CustomClass.php

@@ -195,7 +195,11 @@ class CustomClass extends BaseClass
         BookCustomClass::updateByCondition(['bookSn' => $bookSn, 'customId' => $customId], ['seatSn' => $seatSn]);
     }
 
-    // 使用账户余额结账(扣减净 balance)ssh 20240311
+    /**
+     * 使用账户余额结账:扣减净 balance(合并后来款销单必调)
+     * 调用方:CustomPaymentAllocateService 在 confirmClear(paymentSettleOnly) 之后,
+     *         按实销金额扣回「已用于销挂账」部分,避免 balance 与订单欠款不一致。
+     */
     public static function clearConsumeBalance($amount, $custom, $ghs, $shop, $staff, $clear)
     {
         // 先合并历史挂账到 balance,再扣减(只记余额变动)

+ 12 - 1
biz-ghs/custom/classes/CustomRechargeClass.php

@@ -3,6 +3,8 @@
 namespace bizGhs\custom\classes;
 
 use biz\shop\classes\ShopClass;
+use bizGhs\custom\classes\AccountMoneyClass;
+use bizGhs\custom\services\CustomPaymentAllocateService;
 use bizGhs\ghs\classes\GhsRechargeClass;
 use bizGhs\shop\classes\MainClass;
 use bizHd\ghs\classes\GhsClass;
@@ -71,12 +73,15 @@ class CustomRechargeClass extends BaseClass
         $ghsRecharge->onlinePay = dict::getDict('onlinePay', 'yes');
         $ghsRecharge->save();
 
-        //充值金额,有多个地方要同步修改,请搜索关键词custom_ghs_recharge ssh 20240310
+        // 充值金额,有多个地方要同步修改,请搜索关键词 custom_ghs_recharge、custom_recharge_ghs_clear_action
+        // 合并后:先 ensure,入账后由 CustomPaymentAllocateService::allocateAfterOnlinePay 按本次来款 FIFO 销账(勿在 NoticeController 重复销账)
         $customRechargeId = $customRecharge->id ?? 0;
         $customId = $customRecharge->customId ?? 0;
         $ghsId = $customRecharge->ghsId ?? 0;
         $custom = CustomClass::getLockById($customId);
         $ghsInfo = GhsClass::getLockById($ghsId);
+        AccountMoneyClass::ensureCustomMoneyReady($custom, true);
+        AccountMoneyClass::ensureGhsMoneyReady($ghsInfo, true);
         $rechargeEvent = '线上充值';
         $customMainId = $custom->mainId ?? 0;
         $customShopId = $custom->shopId ?? 0;
@@ -153,6 +158,12 @@ class CustomRechargeClass extends BaseClass
             ShopClass::customRechargeAddBalance($ghsMain, $ghsShop, $customRecharge);
         }
 
+        // 按「本次支付金额」FIFO 销挂账并回写 clearAmount(合并后净余额模型)
+        $ghsShop = ShopClass::getById($ghsShopId, true);
+        if (!empty($ghsShop)) {
+            CustomPaymentAllocateService::allocateAfterOnlinePay($custom, $ghsInfo, $ghsShop, $amount, $customRecharge, $ghsRecharge);
+        }
+
     }
 
 }

+ 432 - 0
biz-ghs/custom/services/CustomPaymentAllocateService.php

@@ -0,0 +1,432 @@
+<?php
+
+namespace bizGhs\custom\services;
+
+use biz\ghs\classes\GhsClass as BizGhsClass;
+use biz\shop\classes\ShopClass;
+use bizGhs\custom\classes\AccountMoneyClass;
+use bizGhs\custom\classes\CustomBalanceChangeClass;
+use bizGhs\custom\classes\CustomClass;
+use bizGhs\custom\classes\CustomRechargeClass;
+use bizGhs\ghs\classes\GhsBalanceChangeClass;
+use bizGhs\order\classes\OrderClearClass;
+use bizGhs\order\classes\OrderClass;
+use bizHd\purchase\classes\PurchaseClearClass;
+use common\components\util;
+
+/**
+ * 批发客户来款分配销挂账(对齐零售 RechargeClass::complete + SettleClass::clearSettle)
+ *
+ * 【背景】挂账已并入净 balance 后,不能再走「clearDebtAmountReduce + 整单 remainDebtPrice=0」旧销账,
+ *        也不能「充值后 balance 必须 >0 才 useBalanceClear」。
+ * 【原则】
+ *   1. 写账前:ensureCustomMoneyReady + ensureGhsMoneyReady(见 AccountMoneyClass)
+ *   2. 来款先入账:balance += 本次金额(rechargeBalance / thirdPay)
+ *   3. 仅用「本次来款」作资金池 FIFO 销 debt=1 订单(末单可部分销,见 buildNeedClearMap)
+ *   4. confirmClear(['paymentSettleOnly'=>true]) 只改订单欠款
+ *   5. clearConsumeBalance(实销合计) 扣减净余额,避免余额与订单状态不一致
+ *   6. 回写充值单/流水 clearId、clearSn、clearAmount;列表 becomeBalance = amount - clearAmount
+ *
+ * 搜索关键词:ensure_before_recharge_clear、custom_ghs_recharge、custom_recharge_ghs_clear_action
+ */
+class CustomPaymentAllocateService
+{
+
+    /**
+     * 【用途】商家帮客户充值并自动按本次来款销挂账(app-ghs CustomController 充值清账)。
+     * 【流程】ensure → rechargeBalance → applyPoolToDebtOrders → patchRechargeAudit
+     * 【参数】$params 可含 remark、rechargeType(售后返充)
+     */
+    public static function rechargeClear($custom, $amount, $shop, $staff, $payWay, $params = [])
+    {
+        // 1. 挂账并入净余额(幂等)
+        $pair = self::ensurePairReady($custom, null);
+        $custom = $pair['custom'];
+        $ghs = $pair['ghs'];
+
+        // 2. 来款入账(balance += 本次充值额)
+        $respond = CustomClass::rechargeBalance($custom, $amount, $shop, $staff, $payWay, $params);
+        $custom = $respond['custom'] ?? $custom;
+        $ghs = $respond['ghs'] ?? $ghs;
+        $customRecharge = $respond['customRecharge'] ?? null;
+
+        self::syncDebtFlags($custom, $ghs);
+
+        // 3. 仅用「本次充值额」作资金池 FIFO 销单,再扣余额(见 applyPoolToDebtOrders)
+        $allocate = self::applyPoolToDebtOrders($custom, $ghs, $shop, $staff, $amount, [
+            'remark' => $params['remark'] ?? '充值销账',
+        ]);
+
+        // 4. 回写充值单、余额流水上的 clearId/clearAmount(供 becomeBalance 展示)
+        if (!empty($customRecharge) && !empty($allocate['clear'])) {
+            self::patchRechargeAudit($customRecharge, $respond['ghsRecharge'] ?? null, $allocate);
+        }
+
+        return $respond;
+    }
+
+    /**
+     * 【用途】花店线上充值支付成功后的销账(CustomRechargeClass::thirdPay 末尾调用)。
+     * 【注意】余额已在 thirdPay 写入,此处不再 recharge;NoticeController 勿再调 useBalanceClear。
+     * 【参数】$amount 须与回调金额、本次来款池一致(通常为充值单 amount)
+     */
+    public static function allocateAfterOnlinePay($custom, $ghs, $shop, $amount, $customRecharge, $ghsRecharge = null)
+    {
+        $pair = self::ensurePairReady($custom, $ghs);
+        $custom = $pair['custom'];
+        $ghs = $pair['ghs'];
+
+        self::syncDebtFlags($custom, $ghs);
+
+        // thirdPay 已入账,此处只按「本次支付金额」销挂账,勿在 NoticeController 再调 useBalanceClear
+        $allocate = self::applyPoolToDebtOrders($custom, $ghs, $shop, null, $amount, [
+            'remark' => '线上充值销账',
+        ]);
+
+        if (!empty($customRecharge) && !empty($allocate['clear'])) {
+            self::patchRechargeAudit($customRecharge, $ghsRecharge, $allocate);
+        }
+
+        return $allocate;
+    }
+
+    /**
+     * 【用途】待付结账单 — 供货商确认「客户已转账」(OrderClearController::actionConfirmClear)。
+     * 【流程】rechargeBalance(结账单 actPrice) → confirmClear(paymentSettleOnly) → clearConsumeBalance(实销)
+     * 【替代】旧逻辑「充值 + confirmClear(clearDebt) + clearConsumeBalance(整单 actPrice)」会在合并后双计挂账。
+     */
+    public static function linkedClearAfterRecharge($custom, $ghs, $shop, $staff, $clear, $payWay, $customRecharge = null)
+    {
+        $pair = self::ensurePairReady($custom, $ghs);
+        $custom = $pair['custom'];
+        $ghs = $pair['ghs'];
+
+        $amount = $clear->actPrice ?? 0;
+        if (bccomp($amount, '0', 2) <= 0) {
+            util::fail('结账单金额有误');
+        }
+
+        // 来款按结账单实收金额入账
+        if (empty($customRecharge)) {
+            $respond = CustomClass::rechargeBalance($custom, $amount, $shop, $staff, $payWay, [
+                'remark' => $clear->remark ?? '',
+            ]);
+            $custom = $respond['custom'];
+            $ghs = $respond['ghs'];
+            $customRecharge = $respond['customRecharge'] ?? null;
+            $ghsRecharge = $respond['ghsRecharge'] ?? null;
+        } else {
+            $ghsRecharge = null;
+        }
+
+        self::syncDebtFlags($custom, $ghs);
+
+        // 只改订单 remainDebtPrice,不调 clearDebtAmountReduce(合并后挂账已在 balance 里)
+        OrderClearClass::confirmClear($clear, $payWay, [
+            'skipCashMoney' => true,
+            'paymentSettleOnly' => true,
+        ]);
+
+        $realClearAmount = OrderClearClass::sumClearAmountByClearId($clear->id ?? 0);
+        // 从净余额扣掉实销金额(替代旧逻辑里对整单 actPrice 的 consume + clearDebt 双计)
+        if (bccomp($realClearAmount, '0', 2) > 0) {
+            $custom = CustomClass::getLockById($custom->id ?? 0);
+            $ghs = BizGhsClass::getLockById($ghs->id ?? 0);
+            CustomClass::clearConsumeBalance($realClearAmount, $custom, $ghs, $shop, $staff, $clear);
+            self::syncDebtFlags($custom, $ghs);
+        }
+        $allocate = [
+            'clear' => $clear,
+            'realClearAmount' => $realClearAmount,
+            'clearId' => $clear->id ?? 0,
+            'clearSn' => $clear->orderSn ?? '',
+            'clearAmount' => $realClearAmount,
+        ];
+
+        if (!empty($customRecharge)) {
+            self::patchRechargeAudit($customRecharge, $ghsRecharge ?? null, $allocate);
+        } else {
+            self::patchRechargeAuditByClear($custom, $ghs, $clear, $realClearAmount);
+        }
+
+        return [
+            'custom' => $custom,
+            'ghs' => $ghs,
+            'customRecharge' => $customRecharge,
+            'allocate' => $allocate,
+        ];
+    }
+
+    /**
+     * 【用途】用账户当前正余额 FIFO 销挂账(原 OrderClearClass::useBalanceClear)。
+     * 【区别】资金池 = 当前 balance 正数部分,不是某笔充值单金额;销完后 clearConsumeBalance。
+     */
+    public static function allocateFromExistingBalance($custom, $ghs, $shop, $staff)
+    {
+        $pair = self::ensurePairReady($custom, $ghs);
+        $custom = $pair['custom'];
+        $ghs = $pair['ghs'];
+
+        $pool = $custom->balance ?? '0.00';
+        if (bccomp($pool, '0', 2) <= 0) {
+            return true;
+        }
+
+        $allocate = self::applyPoolToDebtOrders($custom, $ghs, $shop, $staff, $pool, [
+            'remark' => '余额结账',
+        ]);
+
+        $realClearAmount = $allocate['realClearAmount'] ?? '0.00';
+        if (bccomp($realClearAmount, '0', 2) > 0 && !empty($allocate['clear'])) {
+            CustomClass::clearConsumeBalance($realClearAmount, $custom, $ghs, $shop, $staff, $allocate['clear']);
+            self::syncDebtFlags($custom, $ghs);
+        }
+
+        return true;
+    }
+
+    /**
+     * 【用途】加锁客户/供货商并执行挂账并入净余额(幂等)。
+     * 【校验】合并后双方 balance 必须一致,否则 fail。
+     *
+     * @return array{custom: object, ghs: object}
+     */
+    public static function ensurePairReady($custom, $ghs = null)
+    {
+        if (empty($custom)) {
+            util::fail('没有客户信息');
+        }
+        $customId = is_object($custom) ? ($custom->id ?? 0) : ($custom['id'] ?? 0);
+        $custom = CustomClass::getLockById($customId);
+        if (empty($custom)) {
+            util::fail('没有找到客户');
+        }
+        AccountMoneyClass::ensureCustomMoneyReady($custom, true);
+
+        $ghsId = $custom->ghsId ?? 0;
+        if (empty($ghs)) {
+            $ghs = BizGhsClass::getLockById($ghsId);
+        }
+        if (empty($ghs)) {
+            util::fail('没有找到供货商');
+        }
+        AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
+
+        if (floatval($custom->balance) != floatval($ghs->balance)) {
+            util::fail('客户与供货商余额不一致,请联系管理员');
+        }
+
+        return ['custom' => $custom, 'ghs' => $ghs];
+    }
+
+    /**
+     * 【用途】按本次来款池 FIFO 选出待销订单(逻辑移植自 hd RechargeClass::complete 约 324–364 行)。
+     * 【规则】按 id 升序累加 remainDebtPrice;超过池金额时仅末单 partial clear;部分商家 mainId 遇超池即 break。
+     *
+     * @return array<int, array{orderId:int, clearAmount:string, orderSn:string}>
+     */
+    public static function buildNeedClearMap($customId, $poolAmount, $shop = null)
+    {
+        $poolAmount = bcadd((string)$poolAmount, '0', 2);
+        if (bccomp($poolAmount, '0', 2) <= 0) {
+            return [];
+        }
+
+        $orderList = OrderClass::getAllByCondition(['customId' => $customId, 'debt' => 1], 'id asc', '*', null, true);
+        if (empty($orderList)) {
+            return [];
+        }
+
+        $lastRemain = '0.00';
+        $need = [];
+        $hasOver = false;
+        $mainId = is_object($shop) ? ($shop->mainId ?? 0) : 0;
+
+        foreach ($orderList as $order) {
+            $orderId = (int)($order->id ?? 0);
+            $orderSn = $order->orderSn ?? '';
+            $remainDebtPrice = bcadd((string)($order->remainDebtPrice ?? 0), '0', 2);
+            if (bccomp($remainDebtPrice, '0', 2) <= 0) {
+                continue;
+            }
+
+            $currentTotal = bcadd($lastRemain, $remainDebtPrice, 2);
+            // 累计欠款超过来款池:首单超额时只销「池子剩余」部分(部分销末单)
+            if (bccomp($currentTotal, $poolAmount, 2) > 0) {
+                if (!$hasOver) {
+                    $lastClear = bcsub($poolAmount, $lastRemain, 2);
+                    if (bccomp($lastClear, '0', 2) > 0) {
+                        $need[] = [
+                            'orderId' => $orderId,
+                            'clearAmount' => $lastClear,
+                            'orderSn' => $orderSn,
+                        ];
+                    }
+                }
+                $hasOver = true;
+                if (getenv('YII_ENV') == 'production' && in_array($mainId, [23390])) {
+                    break;
+                }
+                continue;
+            }
+
+            $lastRemain = $currentTotal;
+            $need[] = [
+                'orderId' => $orderId,
+                'clearAmount' => $remainDebtPrice,
+                'orderSn' => $orderSn,
+            ];
+        }
+
+        return $need;
+    }
+
+    /**
+     * 【用途】用指定来款池金额销挂账:建结账单 → paymentSettleOnly 确认 → clearConsumeBalance。
+     * 【参数】$poolAmount 本次来款(充值额或正余额),不是账户历史累计余额。
+     */
+    protected static function applyPoolToDebtOrders($custom, $ghs, $shop, $staff, $poolAmount, $options = [])
+    {
+        $customId = $custom->id ?? 0;
+        $need = self::buildNeedClearMap($customId, $poolAmount, $shop);
+        if (empty($need)) {
+            return [
+                'clear' => null,
+                'realClearAmount' => '0.00',
+                'clearId' => 0,
+                'clearSn' => '',
+                'clearAmount' => '0.00',
+            ];
+        }
+
+        $staffId = $staff->id ?? 0;
+        $staffName = $staff->name ?? '';
+        $sjId = $shop->sjId ?? 0;
+        $shopId = $shop->id ?? 0;
+
+        $totalClear = '0.00';
+        foreach ($need as $row) {
+            $totalClear = bcadd($totalClear, $row['clearAmount'], 2);
+        }
+
+        // 新建销账结账单前,作废该客户其它待付结账单(与零售一致)
+        OrderClearClass::expireAwaitPayClears($customId);
+
+        $clearData = [
+            'customId' => $customId,
+            'ghsShopAdminId' => $staffId,
+            'ghsShopId' => $shopId,
+            'ghsShopAdminName' => $staffName,
+            'modifyPrice' => $totalClear,
+            'payWay' => 0,
+            'complete' => 0,
+            'remark' => $options['remark'] ?? '充值销账',
+        ];
+
+        $clear = OrderClearClass::clearWithAmountMap($clearData, $need, $sjId, $shopId);
+        // 合并后:只更新订单欠款字段,不 reduce debtAmount
+        OrderClearClass::confirmClear($clear, 0, [
+            'skipCashMoney' => true,
+            'paymentSettleOnly' => true,
+        ]);
+
+        $realClearAmount = OrderClearClass::sumClearAmountByClearId($clear->id ?? 0);
+
+        // 入账后须把「用于销单」的部分从 balance 扣回,净余额才与订单状态一致
+        if (bccomp($realClearAmount, '0', 2) > 0) {
+            $custom = CustomClass::getLockById($custom->id ?? 0);
+            $ghs = BizGhsClass::getLockById($ghs->id ?? 0);
+            CustomClass::clearConsumeBalance($realClearAmount, $custom, $ghs, $shop, $staff, $clear);
+            self::syncDebtFlags($custom, $ghs);
+        }
+
+        return [
+            'clear' => $clear,
+            'realClearAmount' => $realClearAmount,
+            'clearId' => $clear->id ?? 0,
+            'clearSn' => $clear->orderSn ?? '',
+            'clearAmount' => $realClearAmount,
+            'custom' => $custom,
+            'ghs' => $ghs,
+        ];
+    }
+
+    /** 按净 balance 刷新 isDebt / ghs.debt 标记(负余额=仍有待结) */
+    protected static function syncDebtFlags($custom, $ghs)
+    {
+        $bal = $custom->balance ?? '0.00';
+        $custom->isDebt = bccomp($bal, '0', 2) < 0 ? CustomClass::IS_DEBT_YES : CustomClass::IS_DEBT_NO;
+        $custom->save(false, ['isDebt']);
+
+        if (!empty($ghs)) {
+            $ghs->debt = bccomp($bal, '0', 2) < 0 ? 2 : 1;
+            $ghs->save(false, ['debt']);
+        }
+    }
+
+    /** 回写 xhCustomRecharge、xhCustomBalanceChange 的 clearId/clearSn/clearAmount(需先执行 add_clear_audit_fields.sql) */
+    protected static function patchRechargeAudit($customRecharge, $ghsRecharge, $allocate)
+    {
+        $clearId = $allocate['clearId'] ?? 0;
+        $clearSn = $allocate['clearSn'] ?? '';
+        $clearAmount = $allocate['clearAmount'] ?? '0.00';
+
+        if (!empty($customRecharge)) {
+            if (self::modelHasAttr($customRecharge, 'clearId')) {
+                $customRecharge->clearId = $clearId;
+                $customRecharge->clearSn = $clearSn;
+                $customRecharge->clearAmount = $clearAmount;
+                $customRecharge->save(false, ['clearId', 'clearSn', 'clearAmount']);
+            }
+        }
+
+        $relateId = $customRecharge->id ?? 0;
+        self::patchBalanceChangeRow(CustomBalanceChangeClass::class, $relateId, $clearId, $clearSn, $clearAmount);
+
+        if (!empty($ghsRecharge)) {
+            $gRelateId = $ghsRecharge->id ?? 0;
+            self::patchBalanceChangeRow(GhsBalanceChangeClass::class, $gRelateId, $clearId, $clearSn, $clearAmount);
+        }
+    }
+
+    protected static function patchRechargeAuditByClear($custom, $ghs, $clear, $clearAmount)
+    {
+        $clearId = $clear->id ?? 0;
+        $clearSn = $clear->orderSn ?? '';
+        self::patchBalanceChangeRow(CustomBalanceChangeClass::class, 0, $clearId, $clearSn, $clearAmount, [
+            'customId' => $custom->id ?? 0,
+            'io' => 1,
+            'limit' => 1,
+        ]);
+        self::patchBalanceChangeRow(GhsBalanceChangeClass::class, 0, $clearId, $clearSn, $clearAmount, [
+            'ghsId' => $ghs->id ?? 0,
+            'io' => 1,
+            'limit' => 1,
+        ]);
+    }
+
+    protected static function patchBalanceChangeRow($class, $relateId, $clearId, $clearSn, $clearAmount, $extraWhere = [])
+    {
+        $where = array_merge(['io' => 1], $extraWhere);
+        if ($relateId > 0) {
+            $where['relateId'] = $relateId;
+        }
+        $row = $class::getByCondition($where, true, null, 'id DESC');
+        if (empty($row) || !self::modelHasAttr($row, 'clearId')) {
+            return;
+        }
+        $row->clearId = $clearId;
+        $row->clearSn = $clearSn;
+        $row->clearAmount = $clearAmount;
+        if (self::modelHasAttr($row, 'giveAmount')) {
+            $row->giveAmount = '0.00';
+        }
+        $row->save(false);
+    }
+
+    protected static function modelHasAttr($row, $attr)
+    {
+        return is_object($row) && method_exists($row, 'hasAttribute') && $row->hasAttribute($attr);
+    }
+
+}

+ 6 - 15
biz-ghs/custom/services/CustomService.php

@@ -4,31 +4,22 @@ namespace bizGhs\custom\services;
 
 use biz\ghs\classes\GhsClass;
 use biz\sj\classes\SjClass;
-use bizGhs\order\classes\OrderClearClass;
 use bizHd\base\services\BaseService;
 use bizGhs\custom\classes\CustomClass;
-use common\components\util;
 
 class CustomService extends BaseService
 {
 
     public static $baseFile = '\bizGhs\custom\classes\CustomClass';
 
-    //充值和结账,有多个地方要同步修改,请搜索关键词custom_ghs_recharge ssh 20240309
+    /**
+     * 充值并自动销挂账(有多个地方要同步修改,请搜索关键词 custom_ghs_recharge)
+     * 实现见 CustomPaymentAllocateService::rechargeClear
+     */
     public static function rechargeClear($custom, $amount, $shop, $staff, $payWay, $params)
     {
-        $respond = CustomClass::rechargeBalance($custom, $amount, $shop, $staff, $payWay, $params);
-        $custom = $respond['custom'] ?? [];
-        $ghs = $respond['ghs'] ?? [];
-        $customRecharge = $respond['customRecharge'] ?? [];
-        if (empty($custom)) {
-            util::fail('没有客户信息');
-        }
-        if (empty($ghs)) {
-            util::fail('没有供货商信息');
-        }
-        OrderClearClass::useBalanceClear($custom, $ghs, $shop, $staff);
-        return $customRecharge;
+        $respond = CustomPaymentAllocateService::rechargeClear($custom, $amount, $shop, $staff, $payWay, $params);
+        return $respond['customRecharge'] ?? [];
     }
 
     //分享建立客户和供货商关系 ssh 2021.2.28

+ 144 - 89
biz-ghs/order/classes/OrderClearClass.php

@@ -13,6 +13,7 @@ use common\components\orderSn;
 use common\components\util;
 use bizGhs\base\classes\BaseClass;
 use bizGhs\clear\classes\OrderCgClearClass;
+use bizGhs\custom\services\CustomPaymentAllocateService;
 use bizHd\purchase\classes\PurchaseClass;
 use bizHd\purchase\classes\PurchaseClearClass;
 
@@ -21,90 +22,80 @@ class OrderClearClass extends BaseClass
 
     public static $baseFile = '\bizGhs\order\models\OrderClear';
 
-    //使用余额自助结欠款 ssh 20240310
+    /**
+     * 使用账户正余额 FIFO 结挂账(委托 CustomPaymentAllocateService::allocateFromExistingBalance)。
+     * 合并后不再要求「充值后 balance 必须 >0」;资金池为当前正余额。
+     */
     public static function useBalanceClear($custom, $ghs, $shop, $staff)
     {
-        $staffId = $staff->id ?? 0;
-        $staffName = $staff->name ?? '';
-        $shopId = $shop->id ?? 0;
-        $sjId = $shop->sjId ?? 0;
-        $customId = $custom->id ?? 0;
-        $ghsId = $ghs->id ?? 0;
-        $customBalance = $custom->balance ?? 0;
-        $ghsBalance = $ghs->balance ?? 0;
-        if (floatval($customBalance) != floatval($ghsBalance)) {
-            noticeUtil::push("客户{$customId} {$customBalance} 供货商{$ghsId} {$ghsBalance} 的余额有问题", '15280215347');
-            util::fail('系统故障');
-        }
-        if ($customBalance < 0) {
-            noticeUtil::push("客户{$customId} 供货商{$ghsId} 的余额小于0", '15280215347');
-            util::fail('系统故障哦');
+        return CustomPaymentAllocateService::allocateFromExistingBalance($custom, $ghs, $shop, $staff);
+    }
+
+    /** 作废该客户所有待付结账单(status=待付),新建销账前调用 */
+    public static function expireAwaitPayClears($customId)
+    {
+        $list = self::getAllByCondition(['customId' => $customId, 'status' => PurchaseClearClass::STATUS_AWAIT_PAY], null, '*', null, true);
+        if (empty($list)) {
+            return;
         }
-        if (floatval($customBalance) == 0) {
-            return true;
+        foreach ($list as $item) {
+            $item->status = PurchaseClearClass::STATUS_EXPIRE;
+            $item->save();
+            OrderCgClearClass::markExpireByClearId($item->id);
         }
-        $where = ['customId' => $customId, 'debt' => 1];
-        $orderList = OrderClass::getAllByCondition($where, 'id asc', '*', null, true);
-        if (empty($orderList)) {
-            return true;
+    }
+
+    /** 汇总结账单下各 OrderCgClear.amount(实销合计,支持部分销单) */
+    public static function sumClearAmountByClearId($clearId)
+    {
+        $clearId = intval($clearId);
+        if ($clearId <= 0) {
+            return '0.00';
         }
-        $totalAmount = 0;
-        $need = [];
-        foreach ($orderList as $order) {
-            $orderId = $order->id ?? 0;
-            $remainDebtPrice = $order->remainDebtPrice ?? 0;
-            $currentTotal = bcadd($totalAmount, $remainDebtPrice, 2);
-            if ($currentTotal > $customBalance) {
-                if (getenv('YII_ENV') == 'production') {
-                    //小向需要先结旧账
-                    if (in_array($shop->mainId, [23390])) {
-                        break;
-                    } else {
-                        continue;
-                    }
-                } else {
-                    if (in_array($shop->mainId, [])) {
-                        break;
-                    } else {
-                        continue;
-                    }
-                }
+        $list = OrderCgClearClass::getAllByCondition(['clearId' => $clearId], null, '*', null, true);
+        $total = '0.00';
+        if (!empty($list)) {
+            foreach ($list as $row) {
+                $total = bcadd($total, $row->amount ?? 0, 2);
             }
-            $totalAmount = bcadd($totalAmount, $remainDebtPrice, 2);
-            $need[] = ['id' => $orderId];
         }
-        if (empty($need)) {
-            return true;
+        return $total;
+    }
+
+    /**
+     * 按每单指定 clearAmount 建结账单(充值 FIFO 销账、末单可部分销 remainDebtPrice)
+     *
+     * @param array $post 同 clear()
+     * @param array $orderRows [['orderId'=>,'clearAmount'=>,'orderSn'=>], ...]
+     */
+    public static function clearWithAmountMap($post, $orderRows, $sjId, $shopId)
+    {
+        if (empty($orderRows)) {
+            util::fail('请选择订单');
         }
-        $where = ['customId' => $customId, 'status' => PurchaseClearClass::STATUS_AWAIT_PAY];
-        $list = OrderClearClass::getAllByCondition($where, null, '*', null, true);
-        if (!empty($list)) {
-            foreach ($list as $item) {
-                //只要新建账单,老账单就取消掉
-                $item->status = PurchaseClearClass::STATUS_EXPIRE;
-                $item->save();
-                OrderCgClearClass::markExpireByClearId($item->id);
+        $ids = [];
+        $amountMap = [];
+        foreach ($orderRows as $row) {
+            $orderId = intval($row['orderId'] ?? 0);
+            if ($orderId <= 0) {
+                continue;
             }
+            $ids[] = $orderId;
+            $amountMap[$orderId] = bcadd((string)($row['clearAmount'] ?? 0), '0', 2);
         }
-
-        $idText = json_encode($need);
-        $clearData = [
-            'customId' => $customId,
-            'ghsShopAdminId' => $staffId,
-            'ghsShopId' => $shopId,
-            'ghsShopAdminName' => $staffName,
-            'modifyPrice' => $totalAmount,
-            'payWay' => 0,
-            'complete' => 1,
-            'remark' => '充值余额结账',
-        ];
-        $clearInfo = self::clear($clearData, $idText, $sjId, $shopId);
-        //结账消费余额
-        CustomClass::clearConsumeBalance($totalAmount, $custom, $ghs, $shop, $staff, $clearInfo);
+        if (empty($ids)) {
+            util::fail('请选择订单');
+        }
+        $idText = json_encode(array_map(function ($id) {
+            return ['id' => $id];
+        }, $ids));
+        $post['modifyPrice'] = $post['modifyPrice'] ?? array_sum(array_map('floatval', $amountMap));
+        $clear = self::clear($post, $idText, $sjId, $shopId, $amountMap);
+        return $clear;
     }
 
     //订单结算 ssh 2021.3.14
-    public static function clear($post, $idText, $sjId, $shopId)
+    public static function clear($post, $idText, $sjId, $shopId, $orderAmountMap = [])
     {
         $customId = $post['customId'] ?? 0;
         $data = json_decode($idText, true);
@@ -137,12 +128,15 @@ class OrderClearClass extends BaseClass
         if (empty($custom)) {
             util::fail('没有找到客户');
         }
+        // 建结账单前:历史挂账并入净 balance(关键词 ensure_before_recharge_clear)
+        \bizGhs\custom\classes\AccountMoneyClass::ensureCustomMoneyReady($custom, true);
 
         $ghsId = $custom->ghsId;
         $ghs = GhsClass::getLockById($ghsId);
         if (empty($ghs)) {
             util::fail('没有找到供货商');
         }
+        \bizGhs\custom\classes\AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
         $ghsShopId = $ghs['shopId'] ?? 0;
         $ghsShop = ShopClass::getById($ghsShopId, true);
         $ghsShopName = $ghsShop->shopName ?? '';
@@ -217,6 +211,15 @@ class OrderClearClass extends BaseClass
         }
         $return = PurchaseClearClass::add($clearData, true);
         $pairs = OrderCgClearClass::buildPairsFromOrders($list);
+        // 充值销账:每单销账金额可小于 remainDebtPrice(部分销)
+        if (!empty($orderAmountMap)) {
+            foreach ($pairs as $k => $pair) {
+                $oid = intval($pair['orderId'] ?? 0);
+                if ($oid > 0 && isset($orderAmountMap[$oid])) {
+                    $pairs[$k]['amount'] = $orderAmountMap[$oid];
+                }
+            }
+        }
         OrderCgClearClass::bindRelations($return, $pairs);
         $complete = $post['complete'] ?? 0;
         if ($complete == 1) {
@@ -225,7 +228,12 @@ class OrderClearClass extends BaseClass
         return $return;
     }
 
-    //确认结账 ssh 20220510
+    /**
+     * 确认结账 ssh 20220510
+     *
+     * $options['paymentSettleOnly'] 合并后来款销单:只减订单 remainDebtPrice,不调 clearDebtAmountReduce
+     * $options['skipCashMoney']      现金已在 rechargeBalance 记过,避免重复加门店现金余额
+     */
     public static function confirmClear($clear, $payWay, $options = [])
     {
         if ($clear->status == 2) {
@@ -256,28 +264,75 @@ class OrderClearClass extends BaseClass
         if (empty($ghs)) {
             util::fail('没有找到供货商');
         }
-        $amount = $clear->prePrice ?? 0;
-
-        $ids = OrderCgClearClass::getOrderIdsByClearId($clearId);
-        if (empty($ids)) {
-            util::fail('数据错误');
-        }
-        $purchaseArr = OrderCgClearClass::getCgIdsByClearId($clearId);
-        if (empty($purchaseArr)) {
+        // true=净余额模型下的销单;false=旧链路(仍调 clearDebtAmountReduce,complete=1 当场结清等)
+        $paymentSettleOnly = !empty($options['paymentSettleOnly']);
+        $orderSettleRows = OrderCgClearClass::getAllByCondition(['clearId' => $clearId], null, '*', null, true);
+        if (empty($orderSettleRows)) {
             util::fail('数据错误');
         }
 
-        //客户欠款减少
-        CustomClass::clearDebtAmountReduce($custom, $ghs, $amount, $clear);
-
         $clearTime = date("Y-m-d H:i:s");
-        // 销售单状态变更
-        OrderClass::updateByIds($ids, ['debt' => 0, 'clearId' => $clearId, 'remainDebtPrice' => 0, 'clearTime' => $clearTime]);
-        // 采购单状态变更
-        PurchaseClass::updateByIds($purchaseArr, ['debt' => 2, 'clearId' => $clearId, 'remainDebtPrice' => 0, 'clearTime' => $clearTime]);
+        $totalSettled = '0.00';
 
-        //供货商结账欠款减少
-        \bizHd\ghs\classes\GhsClass::clearDebtAmountReduce($ghs, $custom, $amount, $clear);
+        foreach ($orderSettleRows as $os) {
+            $orderId = intval($os->orderId ?? 0);
+            $cgId = intval($os->cgId ?? 0);
+            $mayClear = bcadd((string)($os->amount ?? 0), '0', 2);
+            if ($orderId <= 0 || bccomp($mayClear, '0', 2) <= 0) {
+                continue;
+            }
+
+            $order = OrderClass::getById($orderId, true);
+            if (empty($order) || ($order->debt ?? 0) == 0) {
+                continue;
+            }
+
+            // 按 OrderCgClear.amount 部分或足额减少待结,不是一律置 remainDebtPrice=0
+            $newRemain = bcsub((string)($order->remainDebtPrice ?? 0), $mayClear, 2);
+            $orderUpdate = [
+                'remainDebtPrice' => bccomp($newRemain, '0', 2) > 0 ? $newRemain : '0.00',
+                'clearTime' => $clearTime,
+            ];
+            if (bccomp($newRemain, '0', 2) <= 0) {
+                $orderUpdate['debt'] = 0;
+                $orderUpdate['clearId'] = $clearId;
+            }
+            OrderClass::updateById($orderId, $orderUpdate);
+
+            if ($cgId > 0) {
+                $purchase = PurchaseClass::getById($cgId, true);
+                if (!empty($purchase)) {
+                    $pRemain = bcsub((string)($purchase->remainDebtPrice ?? 0), $mayClear, 2);
+                    $pUpdate = [
+                        'remainDebtPrice' => bccomp($pRemain, '0', 2) > 0 ? $pRemain : '0.00',
+                        'clearTime' => $clearTime,
+                    ];
+                    if (bccomp($pRemain, '0', 2) <= 0) {
+                        $pUpdate['debt'] = 2;
+                        $pUpdate['clearId'] = $clearId;
+                    }
+                    $purchase->save(false, array_keys($pUpdate));
+                }
+            }
+
+            $totalSettled = bcadd($totalSettled, $mayClear, 2);
+            $os->status = 1;
+            $os->save();
+        }
+
+        if (!$paymentSettleOnly) {
+            // 未合并或旧当场结清:仍减少 debtAmount 字段(合并后新业务勿走此分支)
+            $amount = $clear->prePrice ?? 0;
+            CustomClass::clearDebtAmountReduce($custom, $ghs, $amount, $clear);
+            \bizHd\ghs\classes\GhsClass::clearDebtAmountReduce($ghs, $custom, $amount, $clear);
+        } else {
+            // 挂账已在 balance,只根据净余额刷新欠款标记;扣款在 clearConsumeBalance
+            $amount = $totalSettled;
+            $custom->isDebt = bccomp($custom->balance ?? '0', '0', 2) < 0 ? CustomClass::IS_DEBT_YES : CustomClass::IS_DEBT_NO;
+            $custom->save(false, ['isDebt']);
+            $ghs->debt = bccomp($custom->balance ?? '0', '0', 2) < 0 ? 2 : 1;
+            $ghs->save(false, ['debt']);
+        }
 
         //现金余额增加(充值销账链路里现金已在 rechargeBalance 记过,需 skipCashMoney)
         $skipCashMoney = !empty($options['skipCashMoney']);

+ 41 - 0
scripts/sql/add_clear_audit_fields.sql

@@ -0,0 +1,41 @@
+-- =============================================================================
+-- 充值销账审计字段 — 对照零售 xhBalanceChange.settleId / settleAmount
+-- =============================================================================
+-- 【用途】批发充值、余额变动可记录「本笔来款中有多少用于销挂账」,接口返回 becomeBalance。
+-- 【业务类】CustomPaymentAllocateService::patchRechargeAudit
+-- 【列表】CustomBalanceChangeClass::getChangeList 计算 becomeBalance
+--
+-- 执行前确认列不存在(已存在则跳过该表):
+--   SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.COLUMNS
+--   WHERE TABLE_SCHEMA = DATABASE()
+--     AND TABLE_NAME IN ('xhCustomBalanceChange','xhGhsBalanceChange','xhCustomRecharge')
+--     AND COLUMN_NAME IN ('clearId','clearSn','clearAmount','giveAmount');
+--
+-- 依赖:建议先执行 add_balance_merged.sql(挂账已并入净 balance)
+-- 锁表:ALGORITHM=INPLACE, LOCK=NONE(同 add_balance_merged.sql)
+-- =============================================================================
+
+SET SESSION lock_wait_timeout = 10;
+
+ALTER TABLE `xhCustomBalanceChange`
+    ADD COLUMN `clearId` int(11) NOT NULL DEFAULT 0 COMMENT '关联结账单ID(批发OrderClear)',
+    ADD COLUMN `clearSn` varchar(64) NOT NULL DEFAULT '' COMMENT '结账单号',
+    ADD COLUMN `clearAmount` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '本笔来款中用于销挂账的金额',
+    ADD COLUMN `giveAmount` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '赠送金额(批发预留,现多为0)',
+    ALGORITHM = INPLACE,
+    LOCK = NONE;
+
+ALTER TABLE `xhGhsBalanceChange`
+    ADD COLUMN `clearId` int(11) NOT NULL DEFAULT 0 COMMENT '关联结账单ID',
+    ADD COLUMN `clearSn` varchar(64) NOT NULL DEFAULT '' COMMENT '结账单号',
+    ADD COLUMN `clearAmount` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '本笔来款中用于销挂账的金额',
+    ADD COLUMN `giveAmount` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '赠送金额(批发预留)',
+    ALGORITHM = INPLACE,
+    LOCK = NONE;
+
+ALTER TABLE `xhCustomRecharge`
+    ADD COLUMN `clearId` int(11) NOT NULL DEFAULT 0 COMMENT '关联结账单ID',
+    ADD COLUMN `clearSn` varchar(64) NOT NULL DEFAULT '' COMMENT '结账单号',
+    ADD COLUMN `clearAmount` decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT '本笔充值中用于销挂账的金额',
+    ALGORITHM = INPLACE,
+    LOCK = NONE;