shish 2 месяцев назад
Родитель
Сommit
c4bd3c9b7e

+ 2 - 3
app-ghs/controllers/OrderClearController.php

@@ -6,7 +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\custom\services\GhsRechargeSettleService;
 use bizGhs\order\classes\OrderClearClass;
 use bizGhs\shop\classes\ShopClass;
 use bizHd\purchase\classes\PurchaseClearClass;
@@ -257,8 +257,7 @@ class OrderClearController extends BaseController
             $custom = CustomClass::getLockById($customId);
             $shop = $this->shop;
             $staff = $this->shopAdmin;
-            // 客户已转账:来款入账 → 只销本结账单 → 扣净余额(CustomPaymentAllocateService,勿再 recharge+confirm+consume 三步)
-            $return = CustomPaymentAllocateService::linkedClearAfterRecharge($custom, $ghs, $shop, $staff, $clear, $payWay);
+            $return = GhsRechargeSettleService::confirmClearBillWithIncoming($custom, $ghs, $shop, $staff, $clear, $payWay);
             $customRecharge = $return['customRecharge'] ?? null;
             $transaction->commit();
             //充值销账通知

+ 1 - 1
biz-ghs/custom/classes/AccountMoneyClass.php

@@ -15,7 +15,7 @@ use Yii;
  * 写业务前调 ensureCustomMoneyReady / ensureGhsMoneyReady;列表/详情返回前调 formatMoneyForClient。
  * 旧 App(appVersion<3)由 formatMoneyForClient 拆回待结+余额双字段;新 App 只读净 balance。
  *
- * 充值销账(合并后来款 FIFO 销单):bizGhs\custom\services\CustomPaymentAllocateService
+ * 充值销账(合并后来款 FIFO 销单):bizGhs\custom\services\GhsRechargeSettleService
  */
 class AccountMoneyClass
 {

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

@@ -198,7 +198,7 @@ class CustomClass extends BaseClass
     /**
      * 用账面正余额支付挂账:扣减净 balance 并记「结账」流水。
      * 来款充值后的自动销单不要调本方法(对照 hd:来款已入账,销单只改订单)。
-     * 调用方:CustomPaymentAllocateService::allocateFromExistingBalance
+     * 调用方:GhsRechargeSettleService::consumePositiveBalanceFifo
      */
     public static function clearConsumeBalance($amount, $custom, $ghs, $shop, $staff, $clear, $options = [])
     {

+ 3 - 3
biz-ghs/custom/classes/CustomRechargeClass.php

@@ -4,7 +4,7 @@ namespace bizGhs\custom\classes;
 
 use biz\shop\classes\ShopClass;
 use bizGhs\custom\classes\AccountMoneyClass;
-use bizGhs\custom\services\CustomPaymentAllocateService;
+use bizGhs\custom\services\GhsRechargeSettleService;
 use bizGhs\ghs\classes\GhsRechargeClass;
 use bizGhs\shop\classes\MainClass;
 use bizHd\ghs\classes\GhsClass;
@@ -74,7 +74,7 @@ class CustomRechargeClass extends BaseClass
         $ghsRecharge->save();
 
         // 充值金额,有多个地方要同步修改,请搜索关键词 custom_ghs_recharge、custom_recharge_ghs_clear_action
-        // 合并后:先 ensure,入账后由 CustomPaymentAllocateService::allocateAfterOnlinePay 按本次来款 FIFO 销账(勿在 NoticeController 重复销账)
+        // 合并后:入账后由 GhsRechargeSettleService::onlinePayFifoClear 按本次来款 FIFO 销账
         $customRechargeId = $customRecharge->id ?? 0;
         $customId = $customRecharge->customId ?? 0;
         $ghsId = $customRecharge->ghsId ?? 0;
@@ -161,7 +161,7 @@ class CustomRechargeClass extends BaseClass
         // 按「本次支付金额」FIFO 销挂账并回写 clearAmount(合并后净余额模型)
         $ghsShop = ShopClass::getById($ghsShopId, true);
         if (!empty($ghsShop)) {
-            CustomPaymentAllocateService::allocateAfterOnlinePay($custom, $ghsInfo, $ghsShop, $amount, $customRecharge, $ghsRecharge);
+            GhsRechargeSettleService::onlinePayFifoClear($custom, $ghsInfo, $ghsShop, $amount, $customRecharge, $ghsRecharge);
         }
 
     }

+ 8 - 430
biz-ghs/custom/services/CustomPaymentAllocateService.php

@@ -2,464 +2,42 @@
 
 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;
-
 /**
- * 批发来款与销挂账(对齐 hd RechargeClass::complete,非照搬 ghs 旧三件套)
- *
- * ── 账户模型(合并后)──
- *   净 balance:正=有余额,负=待结。订单 remainDebtPrice 仍逐单记录待结。
- *   合并式:balance 已含历史挂账,勿再 clearDebtAmountReduce。
- *
- * ── hd 零售(对照)──
- *   balance / balancePay 与订单欠款分开记;充值 balance += 来款;
- *   销单只改订单 remainDebtPrice,不再减 balance;
- *   流水一条:amount=来款,settleAmount=用于销单,balance=充值+销单后的最终值。
- *
- * ── ghs 应对齐的行为 ──
- *   A. 来款充值销账(rechargeClear / 线上 thirdPay+allocate):
- *      来款已 rechargeBalance 入账 → FIFO 销单(仅改订单)→ 不再 clearConsumeBalance(否则 +1 又 -1)。
- *   B. 用已有正余额销账(allocateFromExistingBalance):
- *      无新来款 → 销单后须 clearConsumeBalance 扣减 balance → 可记一条「结账」流水。
- *   C. 待付结账单确认(linkedClearAfterRecharge):
- *      来款入账 + 按结账单销单 → 同 A,不二次扣 balance。
+ * 兼容层:批发充值销账已迁至 GhsRechargeSettleService,本类仅保留旧类名调用。
  *
- * 搜索关键词:ensure_before_recharge_clear、custom_ghs_recharge、custom_recharge_ghs_clear_action
+ * @see GhsRechargeSettleService
  */
 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'];
-
-        // 作废待付结账单,避免充值后误确认旧单(实付130+优惠106 与本次充值100 混在一起)
-        OrderClearClass::expireAwaitPayClears($custom->id ?? 0);
-
-        // 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 销单(只改订单,不二次扣 balance,对照 hd)
-        $allocate = self::applyPoolToDebtOrders($custom, $ghs, $shop, $staff, $amount, [
-            'remark' => $params['remark'] ?? '充值销账',
-            'funding' => 'recharge',
-        ]);
-
-        // 4. 回写充值单、余额流水上的 clearId/clearAmount(供 becomeBalance 展示)
-        if (!empty($customRecharge) && !empty($allocate['clear'])) {
-            self::patchRechargeAudit($customRecharge, $respond['ghsRecharge'] ?? null, $allocate, $custom, $ghs);
-        }
-
-        return $respond;
+        return GhsRechargeSettleService::merchantRechargeWithAutoClear($custom, $amount, $shop, $staff, $payWay, $params);
     }
 
-    /**
-     * 【用途】花店线上充值支付成功后的销账(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'];
-
-        OrderClearClass::expireAwaitPayClears($custom->id ?? 0);
-        self::syncDebtFlags($custom, $ghs);
-
-        // thirdPay 已入账,此处只按「本次支付金额」销挂账,勿在 NoticeController 再调 useBalanceClear
-        $allocate = self::applyPoolToDebtOrders($custom, $ghs, $shop, null, $amount, [
-            'remark' => '线上充值销账',
-            'funding' => 'recharge',
-        ]);
-
-        if (!empty($customRecharge) && !empty($allocate['clear'])) {
-            self::patchRechargeAudit($customRecharge, $ghsRecharge, $allocate, $custom, $ghs);
-        }
-
-        return $allocate;
+        return GhsRechargeSettleService::onlinePayFifoClear($custom, $ghs, $shop, $amount, $customRecharge, $ghsRecharge);
     }
 
-    /**
-     * 【用途】待付结账单 — 供货商确认「客户已转账」(OrderClearController::actionConfirmClear)。
-     * 【流程】rechargeBalance(来款) → confirmClear(paymentSettleOnly)(不 clearConsumeBalance,对照 hd)
-     */
     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);
-        $payCap = bcadd((string)($clear->actPrice ?? '0'), '0', 2);
-        if (bccomp($realClearAmount, $payCap, 2) > 0) {
-            $realClearAmount = $payCap;
-        }
-        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, $custom, $ghs);
-        } else {
-            self::patchRechargeAuditByClear($custom, $ghs, $clear, $realClearAmount);
-        }
-
-        return [
-            'custom' => $custom,
-            'ghs' => $ghs,
-            'customRecharge' => $customRecharge,
-            'allocate' => $allocate,
-        ];
+        return GhsRechargeSettleService::confirmClearBillWithIncoming($custom, $ghs, $shop, $staff, $clear, $payWay, $customRecharge);
     }
 
-    /**
-     * 【用途】用账户当前正余额 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' => '余额结账',
-            'funding' => 'existing_balance',
-        ]);
-
-        $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;
+        return GhsRechargeSettleService::consumePositiveBalanceFifo($custom, $ghs, $shop, $staff);
     }
 
-    /**
-     * 【用途】加锁客户/供货商并执行挂账并入净余额(幂等)。
-     * 【校验】合并后双方 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];
+        return GhsRechargeSettleService::lockAccountPair($custom, $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;
-    }
-
-    /**
-     * 用资金池 FIFO 销挂账。
-     * funding=recharge:来款已入账,只改订单,不 clearConsumeBalance(对照 hd)。
-     * funding=existing_balance:用账面正余额,销单后 clearConsumeBalance 扣减。
-     */
-    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);
-        $poolAmount = bcadd((string)$poolAmount, '0', 2);
-        if (bccomp($realClearAmount, $poolAmount, 2) > 0) {
-            util::fail('销账金额超过本次来款' . floatval($poolAmount) . '元,请检查订单待结或联系技术员');
-        }
-
-        $funding = $options['funding'] ?? 'recharge';
-        // 仅「用已有正余额」销账时才扣 balance;来款充值销账的来款已在 rechargeBalance 记入 balance
-        if ($funding === 'existing_balance' && 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);
-        } else {
-            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, $custom = null, $ghs = null)
-    {
-        $clearId = $allocate['clearId'] ?? 0;
-        $clearSn = $allocate['clearSn'] ?? '';
-        $clearAmount = $allocate['clearAmount'] ?? '0.00';
-        $finalCustomBalance = is_object($custom) ? ($custom->balance ?? null) : null;
-        $finalGhsBalance = is_object($ghs) ? ($ghs->balance ?? null) : null;
-
-        if (!empty($customRecharge)) {
-            if (self::modelHasAttr($customRecharge, 'clearId')) {
-                $customRecharge->clearId = $clearId;
-                $customRecharge->clearSn = $clearSn;
-                $customRecharge->clearAmount = $clearAmount;
-                if ($finalCustomBalance !== null) {
-                    $customRecharge->balance = $finalCustomBalance;
-                }
-                $customRecharge->save(false);
-            }
-        }
-
-        $relateId = $customRecharge->id ?? 0;
-        self::patchBalanceChangeRow(CustomBalanceChangeClass::class, $relateId, $clearId, $clearSn, $clearAmount, [], $finalCustomBalance);
-
-        if (!empty($ghsRecharge)) {
-            $gRelateId = $ghsRecharge->id ?? 0;
-            self::patchBalanceChangeRow(GhsBalanceChangeClass::class, $gRelateId, $clearId, $clearSn, $clearAmount, [], $finalGhsBalance);
-            if ($finalGhsBalance !== null) {
-                $ghsRecharge->balance = $finalGhsBalance;
-                $ghsRecharge->save(false, ['balance']);
-            }
-        }
-    }
-
-    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 = [], $finalBalance = null)
-    {
-        $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';
-        }
-        // 流水余额列展示充值+销账后的净余额(与 hd 单条充值流水一致)
-        if ($finalBalance !== null && self::modelHasAttr($row, 'balance')) {
-            $row->balance = $finalBalance;
-        }
-        $row->save(false);
-    }
-
-    protected static function modelHasAttr($row, $attr)
-    {
-        return is_object($row) && method_exists($row, 'hasAttribute') && $row->hasAttribute($attr);
+        return GhsRechargeSettleService::planFifoByPool($customId, $poolAmount);
     }
 
 }

+ 2 - 2
biz-ghs/custom/services/CustomService.php

@@ -14,11 +14,11 @@ class CustomService extends BaseService
 
     /**
      * 充值并自动销挂账(有多个地方要同步修改,请搜索关键词 custom_ghs_recharge)
-     * 实现见 CustomPaymentAllocateService::rechargeClear
+     * 实现见 GhsRechargeSettleService::merchantRechargeWithAutoClear
      */
     public static function rechargeClear($custom, $amount, $shop, $staff, $payWay, $params)
     {
-        $respond = CustomPaymentAllocateService::rechargeClear($custom, $amount, $shop, $staff, $payWay, $params);
+        $respond = GhsRechargeSettleService::merchantRechargeWithAutoClear($custom, $amount, $shop, $staff, $payWay, $params);
         return $respond['customRecharge'] ?? [];
     }
 

+ 403 - 0
biz-ghs/custom/services/GhsRechargeSettleService.php

@@ -0,0 +1,403 @@
+<?php
+
+namespace bizGhs\custom\services;
+
+use biz\ghs\classes\GhsClass as BizGhsClass;
+use bizGhs\custom\classes\AccountMoneyClass;
+use bizGhs\custom\classes\CustomBalanceChangeClass;
+use bizGhs\custom\classes\CustomClass;
+use bizGhs\ghs\classes\GhsBalanceChangeClass;
+use bizGhs\order\classes\OrderClearClass;
+use bizGhs\order\classes\OrderClass;
+use common\components\util;
+
+/**
+ * 批发净余额模型:来款、销挂账、流水(对齐 hd RechargeClass::complete)
+ *
+ * 规则(仅此一套,无旧三件套):
+ * 1. 净 balance:正=有余额,负=待结;历史挂账已并入 balance(ensure 幂等)。
+ * 2. 来款:balance += 金额,记一条充值流水(amount=来款)。
+ * 3. 销单:仅用「本次资金池」按订单 id FIFO 减少 remainDebtPrice,不再扣 balance。
+ * 4. 用账面正余额销账:无来款,FIFO 后 balance -= 实销额,可记「结账」流水。
+ *
+ * 入口:merchantRechargeWithAutoClear / onlinePayFifoClear / confirmClearBillWithIncoming /
+ *       consumePositiveBalanceFifo
+ */
+class GhsRechargeSettleService
+{
+
+    /** 商家帮充 + 按来款 FIFO 销账(CustomController) */
+    public static function merchantRechargeWithAutoClear($custom, $amount, $shop, $staff, $payWay, $params = [])
+    {
+        $pair = self::lockAccountPair($custom, null);
+        $custom = $pair['custom'];
+        $ghs = $pair['ghs'];
+        $pool = self::money($amount);
+
+        self::voidAwaitPayClearBills($custom->id ?? 0);
+
+        $respond = CustomClass::rechargeBalance($custom, $pool, $shop, $staff, $payWay, $params);
+        $custom = $respond['custom'] ?? $custom;
+        $ghs = $respond['ghs'] ?? $ghs;
+        $customRecharge = $respond['customRecharge'] ?? null;
+
+        $allocate = self::fifoSettleWithPool($pair, $shop, $staff, $pool, [
+            'remark' => $params['remark'] ?? '充值销账',
+            'deduct_balance' => false,
+        ]);
+
+        if (!empty($customRecharge) && !empty($allocate['clear'])) {
+            self::bindRechargeToClear($customRecharge, $respond['ghsRecharge'] ?? null, $allocate, $custom, $ghs);
+        }
+
+        return $respond;
+    }
+
+    /** 线上支付已入账后的 FIFO 销账(CustomRechargeClass::thirdPay) */
+    public static function onlinePayFifoClear($custom, $ghs, $shop, $amount, $customRecharge, $ghsRecharge = null)
+    {
+        $pair = self::lockAccountPair($custom, $ghs);
+        $pool = self::money($amount);
+
+        self::voidAwaitPayClearBills($pair['custom']->id ?? 0);
+
+        $allocate = self::fifoSettleWithPool($pair, $shop, null, $pool, [
+            'remark' => '线上充值销账',
+            'deduct_balance' => false,
+        ]);
+
+        if (!empty($customRecharge) && !empty($allocate['clear'])) {
+            self::bindRechargeToClear($customRecharge, $ghsRecharge, $allocate, $pair['custom'], $pair['ghs']);
+        }
+
+        return $allocate;
+    }
+
+    /** 待付结账单 — 客户已转账:来款入账 + 按本单销订单(OrderClearController) */
+    public static function confirmClearBillWithIncoming($custom, $ghs, $shop, $staff, $clear, $payWay, $customRecharge = null)
+    {
+        $pair = self::lockAccountPair($custom, $ghs);
+        $custom = $pair['custom'];
+        $ghs = $pair['ghs'];
+
+        $pool = self::money($clear->actPrice ?? 0);
+        if (bccomp($pool, '0', 2) <= 0) {
+            util::fail('结账单金额有误');
+        }
+
+        $ghsRecharge = null;
+        if (empty($customRecharge)) {
+            $respond = CustomClass::rechargeBalance($custom, $pool, $shop, $staff, $payWay, [
+                'remark' => $clear->remark ?? '',
+            ]);
+            $custom = $respond['custom'];
+            $ghs = $respond['ghs'];
+            $customRecharge = $respond['customRecharge'] ?? null;
+            $ghsRecharge = $respond['ghsRecharge'] ?? null;
+        }
+
+        OrderClearClass::applyNetBalanceClear($clear, $payWay, ['skipCashMoney' => true]);
+
+        $settled = self::money(OrderClearClass::sumClearAmountByClearId($clear->id ?? 0));
+        if (bccomp($settled, $pool, 2) > 0) {
+            $settled = $pool;
+        }
+
+        self::refreshDebtFlags($custom, $ghs);
+
+        $allocate = [
+            'clear' => $clear,
+            'realClearAmount' => $settled,
+            'clearId' => $clear->id ?? 0,
+            'clearSn' => $clear->orderSn ?? '',
+            'clearAmount' => $settled,
+        ];
+
+        if (!empty($customRecharge)) {
+            self::bindRechargeToClear($customRecharge, $ghsRecharge, $allocate, $custom, $ghs);
+        } else {
+            self::bindClearToLatestIncomeRow($custom, $ghs, $clear, $settled);
+        }
+
+        return [
+            'custom' => $custom,
+            'ghs' => $ghs,
+            'customRecharge' => $customRecharge,
+            'allocate' => $allocate,
+        ];
+    }
+
+    /** 用当前正余额 FIFO 销挂账(原 useBalanceClear) */
+    public static function consumePositiveBalanceFifo($custom, $ghs, $shop, $staff)
+    {
+        $pair = self::lockAccountPair($custom, $ghs);
+        $pool = self::money($pair['custom']->balance ?? 0);
+        if (bccomp($pool, '0', 2) <= 0) {
+            return true;
+        }
+
+        $allocate = self::fifoSettleWithPool($pair, $shop, $staff, $pool, [
+            'remark' => '余额结账',
+            'deduct_balance' => true,
+        ]);
+
+        return true;
+    }
+
+    /**
+     * FIFO 分配计划:按订单 id 升序,资金池用尽即停止。
+     *
+     * @return array<int, array{orderId:int, clearAmount:string, orderSn:string}>
+     */
+    public static function planFifoByPool($customId, $poolAmount)
+    {
+        $pool = self::money($poolAmount);
+        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';
+
+        foreach ($orderList as $order) {
+            $left = bcsub($pool, $used, 2);
+            if (bccomp($left, '0', 2) <= 0) {
+                break;
+            }
+
+            $remain = self::money($order->remainDebtPrice ?? 0);
+            if (bccomp($remain, '0', 2) <= 0) {
+                continue;
+            }
+
+            $clearAmount = bccomp($remain, $left, 2) <= 0 ? $remain : $left;
+            $plan[] = [
+                'orderId' => (int)($order->id ?? 0),
+                'clearAmount' => $clearAmount,
+                'orderSn' => $order->orderSn ?? '',
+            ];
+            $used = bcadd($used, $clearAmount, 2);
+        }
+
+        return $plan;
+    }
+
+    /**
+     * @param array{custom:object, ghs:object} $pair
+     */
+    protected static function fifoSettleWithPool(array $pair, $shop, $staff, $poolAmount, $options = [])
+    {
+        $custom = $pair['custom'];
+        $ghs = $pair['ghs'];
+        $customId = $custom->id ?? 0;
+        $pool = self::money($poolAmount);
+
+        $plan = self::planFifoByPool($customId, $pool);
+        if (empty($plan)) {
+            self::refreshDebtFlags($custom, $ghs);
+            return self::emptyAllocateResult($custom, $ghs);
+        }
+
+        $planTotal = self::sumPlan($plan);
+        if (bccomp($planTotal, $pool, 2) > 0) {
+            util::fail('销账计划超过本次可用金额,请联系技术员');
+        }
+
+        self::voidAwaitPayClearBills($customId);
+
+        $staffId = $staff->id ?? 0;
+        $staffName = $staff->name ?? '';
+        $sjId = $shop->sjId ?? 0;
+        $shopId = $shop->id ?? 0;
+
+        $clear = OrderClearClass::clearWithAmountMap([
+            'customId' => $customId,
+            'ghsShopAdminId' => $staffId,
+            'ghsShopId' => $shopId,
+            'ghsShopAdminName' => $staffName,
+            'modifyPrice' => $planTotal,
+            'payWay' => 0,
+            'complete' => 0,
+            'remark' => $options['remark'] ?? '充值销账',
+        ], $plan, $sjId, $shopId);
+
+        OrderClearClass::applyNetBalanceClear($clear, 0, ['skipCashMoney' => true]);
+
+        $settled = self::money(OrderClearClass::sumClearAmountByClearId($clear->id ?? 0));
+        if (bccomp($settled, $planTotal, 2) !== 0) {
+            util::fail('销账结果与计划不一致,请检查订单待结或联系技术员');
+        }
+        if (bccomp($settled, $pool, 2) > 0) {
+            util::fail('销账金额超过本次可用金额' . floatval($pool) . '元,请检查订单待结或联系技术员');
+        }
+
+        if (!empty($options['deduct_balance']) && bccomp($settled, '0', 2) > 0) {
+            $custom = CustomClass::getLockById($custom->id ?? 0);
+            $ghs = BizGhsClass::getLockById($ghs->id ?? 0);
+            CustomClass::clearConsumeBalance($settled, $custom, $ghs, $shop, $staff, $clear);
+        }
+
+        self::refreshDebtFlags($custom, $ghs);
+
+        return [
+            'clear' => $clear,
+            'realClearAmount' => $settled,
+            'clearId' => $clear->id ?? 0,
+            'clearSn' => $clear->orderSn ?? '',
+            'clearAmount' => $settled,
+            'custom' => $custom,
+            'ghs' => $ghs,
+        ];
+    }
+
+    /** @return array{custom: object, ghs: object} */
+    public static function lockAccountPair($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 (bccomp(self::money($custom->balance), self::money($ghs->balance), 2) !== 0) {
+            util::fail('客户与供货商余额不一致,请联系管理员');
+        }
+
+        return ['custom' => $custom, 'ghs' => $ghs];
+    }
+
+    protected static function voidAwaitPayClearBills($customId)
+    {
+        OrderClearClass::expireAwaitPayClears(intval($customId));
+    }
+
+    protected static function sumPlan(array $plan)
+    {
+        $total = '0.00';
+        foreach ($plan as $row) {
+            $total = bcadd($total, self::money($row['clearAmount'] ?? 0), 2);
+        }
+        return $total;
+    }
+
+    protected static function refreshDebtFlags($custom, $ghs)
+    {
+        $bal = self::money($custom->balance ?? 0);
+        $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']);
+        }
+    }
+
+    protected static function emptyAllocateResult($custom, $ghs)
+    {
+        return [
+            'clear' => null,
+            'realClearAmount' => '0.00',
+            'clearId' => 0,
+            'clearSn' => '',
+            'clearAmount' => '0.00',
+            'custom' => $custom,
+            'ghs' => $ghs,
+        ];
+    }
+
+    protected static function bindRechargeToClear($customRecharge, $ghsRecharge, $allocate, $custom = null, $ghs = null)
+    {
+        $clearId = $allocate['clearId'] ?? 0;
+        $clearSn = $allocate['clearSn'] ?? '';
+        $clearAmount = $allocate['clearAmount'] ?? '0.00';
+        $finalCustomBalance = is_object($custom) ? ($custom->balance ?? null) : null;
+        $finalGhsBalance = is_object($ghs) ? ($ghs->balance ?? null) : null;
+
+        if (!empty($customRecharge) && self::modelHasAttr($customRecharge, 'clearId')) {
+            $customRecharge->clearId = $clearId;
+            $customRecharge->clearSn = $clearSn;
+            $customRecharge->clearAmount = $clearAmount;
+            if ($finalCustomBalance !== null) {
+                $customRecharge->balance = $finalCustomBalance;
+            }
+            $customRecharge->save(false);
+        }
+
+        $relateId = $customRecharge->id ?? 0;
+        self::patchBalanceChangeRow(CustomBalanceChangeClass::class, $relateId, $clearId, $clearSn, $clearAmount, [], $finalCustomBalance);
+
+        if (!empty($ghsRecharge)) {
+            $gRelateId = $ghsRecharge->id ?? 0;
+            self::patchBalanceChangeRow(GhsBalanceChangeClass::class, $gRelateId, $clearId, $clearSn, $clearAmount, [], $finalGhsBalance);
+            if ($finalGhsBalance !== null) {
+                $ghsRecharge->balance = $finalGhsBalance;
+                $ghsRecharge->save(false, ['balance']);
+            }
+        }
+    }
+
+    protected static function bindClearToLatestIncomeRow($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,
+        ]);
+        self::patchBalanceChangeRow(GhsBalanceChangeClass::class, 0, $clearId, $clearSn, $clearAmount, [
+            'ghsId' => $ghs->id ?? 0,
+            'io' => 1,
+        ]);
+    }
+
+    protected static function patchBalanceChangeRow($class, $relateId, $clearId, $clearSn, $clearAmount, $extraWhere = [], $finalBalance = null)
+    {
+        $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';
+        }
+        if ($finalBalance !== null && self::modelHasAttr($row, 'balance')) {
+            $row->balance = $finalBalance;
+        }
+        $row->save(false);
+    }
+
+    protected static function modelHasAttr($row, $attr)
+    {
+        return is_object($row) && method_exists($row, 'hasAttribute') && $row->hasAttribute($attr);
+    }
+
+    protected static function money($value)
+    {
+        return bcadd((string)$value, '0', 2);
+    }
+
+}

+ 17 - 7
biz-ghs/order/classes/OrderClearClass.php

@@ -13,7 +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 bizGhs\custom\services\GhsRechargeSettleService;
 use bizHd\purchase\classes\PurchaseClass;
 use bizHd\purchase\classes\PurchaseClearClass;
 
@@ -23,12 +23,11 @@ class OrderClearClass extends BaseClass
     public static $baseFile = '\bizGhs\order\models\OrderClear';
 
     /**
-     * 使用账户正余额 FIFO 结挂账(委托 CustomPaymentAllocateService::allocateFromExistingBalance)。
-     * 合并后不再要求「充值后 balance 必须 >0」;资金池为当前正余额。
+     * 使用账户正余额 FIFO 结挂账(GhsRechargeSettleService::consumePositiveBalanceFifo)
      */
     public static function useBalanceClear($custom, $ghs, $shop, $staff)
     {
-        return CustomPaymentAllocateService::allocateFromExistingBalance($custom, $ghs, $shop, $staff);
+        return GhsRechargeSettleService::consumePositiveBalanceFifo($custom, $ghs, $shop, $staff);
     }
 
     /** 作废该客户所有待付结账单(status=待付),新建销账前调用 */
@@ -240,10 +239,21 @@ class OrderClearClass extends BaseClass
     }
 
     /**
-     * 确认结账 ssh 20220510
+     * 净余额模型销账:只减订单 remainDebtPrice,不调 clearDebtAmountReduce、不二次扣 balance。
+     * 充值/来款链路请只调本方法(GhsRechargeSettleService)。
+     */
+    public static function applyNetBalanceClear($clear, $payWay = 0, $options = [])
+    {
+        return self::confirmClear($clear, $payWay, array_merge([
+            'paymentSettleOnly' => true,
+        ], $options));
+    }
+
+    /**
+     * 确认结账(手工结账单等仍可能走完整链路;来款自动销账请用 applyNetBalanceClear)
      *
-     * $options['paymentSettleOnly'] 合并后来款销单:只减订单 remainDebtPrice,不调 clearDebtAmountReduce
-     * $options['skipCashMoney']      现金已在 rechargeBalance 记过,避免重复加门店现金余额
+     * $options['paymentSettleOnly'] 只减订单待结
+     * $options['skipCashMoney']      现金已在 rechargeBalance 记过
      */
     public static function confirmClear($clear, $payWay, $options = [])
     {