Jelajahi Sumber

Merge branch 'dev' of git.huaml.com:zhh/huahuibao into dev

shizhongqi 2 bulan lalu
induk
melakukan
0c6035fed9

+ 33 - 0
app-ghs/controllers/GhsBalanceChangeController.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace ghs\controllers;
+
+use bizGhs\ghs\classes\GhsBalanceChangeClass;
+use bizGhs\ghs\classes\GhsClass;
+use common\components\util;
+use Yii;
+
+class GhsBalanceChangeController extends BaseController
+{
+
+    /** 批发店查看供货商(内部采购)余额变动明细 */
+    public function actionList()
+    {
+        $get = Yii::$app->request->get();
+        $ghsId = $get['ghsId'] ?? 0;
+        if (empty($ghsId)) {
+            util::success([]);
+        }
+        $ghs = GhsClass::getById($ghsId, true);
+        if (empty($ghs)) {
+            util::success([]);
+        }
+        if ($ghs->ownMainId != $this->mainId) {
+            util::fail('不是你的供货商');
+        }
+        $where = ['ghsId' => $ghsId];
+        $list = GhsBalanceChangeClass::getChangeList($where);
+        util::success($list);
+    }
+
+}

+ 14 - 8
app-ghs/controllers/GhsController.php

@@ -12,8 +12,6 @@ use common\components\orderSn;
 use common\components\stringUtil;
 use Yii;
 use common\components\util;
-use bizGhs\order\classes\PurchaseOrderClass;
-
 class GhsController extends BaseController
 {
 
@@ -191,11 +189,15 @@ class GhsController extends BaseController
     {
         $get = Yii::$app->request->get();
         $name = isset($get['name']) && !empty($get['name']) ? $get['name'] : '';
-        $delStatus = isset($get['delStatus']) && is_numeric($get['delStatus']) ? $get['delStatus'] : 0;
+        $delStatus = isset($get['delStatus']) && is_numeric($get['delStatus']) ? (int)$get['delStatus'] : 0;
         $where = [
             'ownShopId' => $this->shopId,
-            'delStatus' => $delStatus
+            'delStatus' => $delStatus,
         ];
+        $sortType = $get['sortType'] ?? 'all';
+        if (!in_array($sortType, ['all', 'debt', 'expend'], true)) {
+            $sortType = 'all';
+        }
         if (!empty($name)) {
             if (stringUtil::isLetter($name)) {
                 $where['py'] = ['like', $name];
@@ -242,7 +244,11 @@ class GhsController extends BaseController
             }
         }
 
-        $list = GhsClass::getGhsList($where);
+        $list = GhsClass::getGhsList($where, $sortType);
+        $page = isset($get['page']) ? (int)$get['page'] : 1;
+        if ($page <= 1) {
+            $list['tabCounts'] = GhsClass::getSupplierTabCounts($this->shopId);
+        }
         $staff = $this->shopAdmin;
         if (!isset($staff->finance) || $staff->finance == 0) {
             $totalDebtAmount = 0;
@@ -253,8 +259,8 @@ class GhsController extends BaseController
                 }
             }
         } else {
-            //累计总欠款
-            $totalDebtAmount = PurchaseOrderClass::sum(['mainId' => $this->mainId, 'status' => 4, 'debt' => PurchaseOrderClass::DEBT_YES], 'actPrice');
+            // 总欠款:各供货商 balance 字段之和
+            $totalDebtAmount = GhsClass::sumTotalOutstandingDebt($this->shopId, 0);
         }
         $list['totalDebtAmount'] = $totalDebtAmount;
         util::success($list);
@@ -272,4 +278,4 @@ class GhsController extends BaseController
         util::success(['file' => $fileUrl, 'shortFile' => $shortFile]);
     }
 
-}
+}

+ 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();
             //充值销账通知

+ 12 - 1
app-ghs/controllers/OrderController.php

@@ -2477,7 +2477,18 @@ class OrderController extends BaseController
                 util::fail('请选时间');
             }
         }
-        $list = OrderClass::getAllByCondition($where, 'addTime DESC', ['id', 'orderSn', 'actPrice', 'addTime', 'remainDebtPrice', 'payTime']);
+        $list = OrderClass::getAllByCondition($where, 'addTime DESC', ['id', 'orderSn', 'actPrice', 'addTime', 'remainDebtPrice', 'debtPrice', 'payTime']);
+        if (!empty($list)) {
+            foreach ($list as $key => $value) {
+                $debtPrice = $value['debtPrice'] ?? 0;
+                $remainDebtPrice = $value['remainDebtPrice'] ?? 0;
+                $hasPayDebt = '0.00';
+                if (bccomp((string)$debtPrice, (string)$remainDebtPrice, 2) > 0) {
+                    $hasPayDebt = bcsub((string)$debtPrice, (string)$remainDebtPrice, 2);
+                }
+                $list[$key]['hasPayDebt'] = floatval($hasPayDebt);
+            }
+        }
         $result = ['customInfo' => $info, 'list' => $list];
         util::success($result);
     }

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

@@ -11,7 +11,7 @@ use bizGhs\cg\classes\CgOrderItemSendClass;
 use bizGhs\cg\classes\CgRefundClass;
 use bizGhs\cg\classes\CgRefundItemClass;
 use bizGhs\ghs\classes\GhsClass;
-use bizGhs\ghs\classes\GhsDebtRecordClass;
+use bizGhs\order\classes\PurchaseOrderClass;
 use bizGhs\order\classes\PurchaseOrderItemClass;
 use bizGhs\product\classes\ProductClass;
 use common\components\dict;
@@ -237,28 +237,7 @@ class CgRefundService extends BaseService
         if (empty($ghs)) {
             util::fail('没有找到供货商');
         }
-        // 采购退款:净 balance 增加
-        \bizGhs\custom\classes\AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
-        $ghsNewBalance = bcadd($ghs->balance ?? '0.00', $refundPrice, 2);
-        $ghs->balance = $ghsNewBalance;
-        $ghs->save(false, ['balance']);
-
-        $capitalType = dict::getDict('capitalType', 'ghsCgOrderRefund', 'id');
-        $debtData = [
-            'ghsId' => $ghsId,
-            'relateId' => $refund->id,
-            'ptStyle' => 2,
-            'capitalType' => $capitalType,
-            'amount' => $refundPrice,
-            'balance' => $ghsNewBalance,
-            'io' => 1,
-            'staffId' => $refund->shopAdminId,
-            'staffName' => $refund->shopAdminName,
-            'event' => '采购单 ' . $order->orderSn . ' 申请售后 ' . $orderSn,
-            'sjId' => $sjId,
-            'shopId' => $shopId,
-        ];
-        \bizGhs\ghs\classes\GhsBalanceChangeClass::add($debtData, true);
+        PurchaseOrderClass::applyPurchaseRefundOnAccounts($ghs, $refundPrice, $refund, $order, $sjId, $shopId);
 
         return $refund;
     }

+ 2 - 42
biz-ghs/clear/classes/ClearClass.php

@@ -6,7 +6,6 @@ use biz\ghs\classes\GhsClass;
 use biz\shop\classes\ShopClass;
 use bizGhs\base\classes\BaseClass;
 use bizGhs\custom\classes\CustomClass;
-use bizGhs\ghs\classes\GhsDebtRecordClass;
 use bizGhs\clear\classes\OrderCgClearClass;
 use bizGhs\order\classes\OrderClass;
 use bizGhs\order\classes\PurchaseOrderClass;
@@ -179,30 +178,7 @@ class ClearClass extends BaseClass
         if (!$lock) {
             util::fail("系统繁忙,请重试");
         }
-        // 采购单结账:供货商净 balance 增加(原减 debtAmount)
-        \bizGhs\custom\classes\AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
-        $ghsNewBalance = bcadd($ghs->balance ?? '0.00', $order->prePrice, 2);
-        $ghs->balance = $ghsNewBalance;
-        $ghs->debtNum -= count($arr);
-        $ghs->debt = bccomp($ghsNewBalance, '0', 2) < 0 ? 2 : 1;
-        $ghs->save(false, ['balance', 'debtNum', 'debt']);
-
-        $capitalType = dict::getDict('capitalType', 'ghsCgOrderClear', 'id');
-        $debtData = [
-            'ghsId' => $ghsId,
-            'relateId' => $order->id,
-            'ptStyle' => 2,
-            'capitalType' => $capitalType,
-            'amount' => $order->prePrice,
-            'staffId' => $order->customShopAdminId,
-            'staffName' => $order->customShopAdminName,
-            'balance' => $ghsNewBalance,
-            'io' => 1,
-            'event' => '采购单结账 ' . $order->orderSn,
-            'sjId' => $order->sjId,
-            'shopId' => $order->shopId,
-        ];
-        \bizGhs\ghs\classes\GhsBalanceChangeClass::add($debtData, true);
+        PurchaseOrderClass::applyPurchaseClearOnBuyerGhs($ghs, $order->prePrice, $order, count($arr));
         util::unlock($ghsAddDebtKey);
 
         $shopId = $ghs->shopId;
@@ -228,23 +204,7 @@ class ClearClass extends BaseClass
             $purchase->save();
         }
 
-        //客户欠款变化
-        $customId = $ghs->customId;
-        $custom = CustomClass::getLockById($customId);
-        if (empty($custom)) {
-            util::fail('没有找到客户信息');
-        }
-        // 客户侧同步:净 balance 增加
-        \bizGhs\custom\classes\AccountMoneyClass::ensureCustomMoneyReady($custom, true);
-        $customNewBalance = bcadd($custom->balance ?? '0.00', $order->actPrice, 2);
-        $custom->balance = $customNewBalance;
-        $custom->debtNum -= count($arr);
-        $custom->isDebt = bccomp($customNewBalance, '0', 2) < 0 ? 1 : 0;
-        $custom->save(false, ['balance', 'debtNum', 'isDebt']);
-        if ($custom->debtNum == 0) {
-            $main->mayGatheringNum -= 1;
-            $main->save();
-        }
+        $custom = PurchaseOrderClass::applyPurchaseClearOnSupplierCustom($ghs, $order->actPrice, count($arr), $main);
 
         //应付供货商款减少
         $customShopId = $custom->shopId;

+ 64 - 0
biz-ghs/clear/classes/OrderCgClearClass.php

@@ -186,6 +186,70 @@ class OrderCgClearClass extends BaseClass
         }
     }
 
+    /**
+     * 销售单已销账的结账单列表(部分销、多次销均含,对齐 hd orderSettleList)
+     *
+     * @return array<int, array{clearId:int, clearSn:string, amount:string}>
+     */
+    public static function listPaidClearRecordsForOrder($orderId)
+    {
+        $orderId = intval($orderId);
+        if ($orderId <= 0) {
+            return [];
+        }
+        $list = self::getAllByCondition([
+            'orderId' => $orderId,
+            'status' => self::STATUS_HAS_PAY,
+        ], 'id asc', '*', null, true);
+        return self::mapPaidClearRecordRows($list);
+    }
+
+    /**
+     * 采购单关联的已销结账单(仅本采购单在结账单中的销账额,勿与 saleId 查询合并以免重复累加)
+     */
+    public static function listPaidClearRecordsForPurchase($purchaseId, $saleOrderId = 0)
+    {
+        $purchaseId = intval($purchaseId);
+        $saleOrderId = intval($saleOrderId);
+
+        if ($purchaseId > 0) {
+            $cgList = self::getAllByCondition([
+                'cgId' => $purchaseId,
+                'status' => self::STATUS_HAS_PAY,
+            ], 'id asc', '*', null, true);
+            if (!empty($cgList)) {
+                return self::mapPaidClearRecordRows($cgList);
+            }
+        }
+
+        // 旧数据可能只有 orderId、未写 cgId
+        if ($saleOrderId > 0) {
+            return self::listPaidClearRecordsForOrder($saleOrderId);
+        }
+
+        return [];
+    }
+
+    protected static function mapPaidClearRecordRows($list)
+    {
+        if (empty($list)) {
+            return [];
+        }
+        $rows = [];
+        foreach ($list as $item) {
+            $clearId = intval(is_object($item) ? ($item->clearId ?? 0) : ($item['clearId'] ?? 0));
+            if ($clearId <= 0) {
+                continue;
+            }
+            $rows[] = [
+                'clearId' => $clearId,
+                'clearSn' => (string)(is_object($item) ? ($item->clearSn ?? '') : ($item['clearSn'] ?? '')),
+                'amount' => self::normalizeAmount(is_object($item) ? ($item->amount ?? 0) : ($item['amount'] ?? 0)),
+            ];
+        }
+        return $rows;
+    }
+
     public static function getOrderIdsByClearId($clearId)
     {
         $clearId = intval($clearId);

+ 16 - 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
 {
@@ -379,4 +379,19 @@ class AccountMoneyClass
             self::ensureCustomMoneyReady($custom, true);
         }
     }
+
+    /**
+     * 【用途】供货商列表分页时,对 xhGhs 仍有 debtAmount 未合并的单行触发懒合并(读路径补刀)。
+     * 【调用时机】bizGhs\ghs\classes\GhsClass::groupBaseInfo。
+     */
+    public static function mergeGhsRowFromListIfNeeded($ghsId)
+    {
+        if (empty($ghsId)) {
+            return;
+        }
+        $ghs = BizGhsClass::getLockById($ghsId);
+        if (!empty($ghs)) {
+            self::ensureGhsMoneyReady($ghs, true);
+        }
+    }
 }

+ 352 - 2
biz-ghs/custom/classes/CustomBalanceChangeClass.php

@@ -2,6 +2,11 @@
 
 namespace bizGhs\custom\classes;
 
+use bizGhs\ghs\classes\GhsBalanceChangeClass;
+use bizGhs\ghs\classes\GhsRechargeClass;
+use bizGhs\order\classes\OrderClearClass;
+use bizHd\purchase\classes\PurchaseClearClass;
+use common\components\dict;
 use common\components\util;
 use bizHd\base\classes\BaseClass;
 use Yii;
@@ -20,21 +25,366 @@ class CustomBalanceChangeClass extends BaseClass
         $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
+                $v = self::fillClearAuditFromRecharge($v);
+                $clearAmount = $v['clearAmount'] ?? 0;
                 $amount = $v['amount'] ?? 0;
                 $io = $v['io'] ?? 0;
                 $becomeBalance = 0;
                 if ($io == 1) {
-                    // 仅入账方向计算「充完销账后还剩多少余额」
                     $becomeBalance = bcsub($amount, $clearAmount, 2);
                     $becomeBalance = floatval($becomeBalance);
                 }
+                $data['list'][$k] = $v;
                 $data['list'][$k]['becomeBalance'] = $becomeBalance;
             }
         }
         return $data;
     }
 
+    /**
+     * 入账流水补全结账单(充值清账、选订单销账-客户已转账 等)
+     */
+    public static function fillClearAuditFromRecharge($row)
+    {
+        if (empty($row) || !is_array($row)) {
+            return $row;
+        }
+        if (intval($row['clearId'] ?? 0) > 0) {
+            return self::normalizeClearAuditRow($row);
+        }
+        if (intval($row['io'] ?? 0) !== 1) {
+            return $row;
+        }
+        $relateId = intval($row['relateId'] ?? 0);
+        if ($relateId <= 0) {
+            return $row;
+        }
+
+        // 旧流水:relateId 即结账单 id
+        if (self::isClearIncomeCapitalType($row['capitalType'] ?? 0)) {
+            return self::attachClearMetaFromClearId($row, $relateId);
+        }
+
+        if (!self::isRechargeCapitalType($row['capitalType'] ?? 0)
+            && !self::isGhsSideRechargeCapitalType($row['capitalType'] ?? 0)) {
+            return $row;
+        }
+
+        $recharge = self::resolveCustomRechargeForBalanceRow($row);
+        if (!empty($recharge)) {
+            $customId = intval(is_object($recharge) ? ($recharge->customId ?? 0) : ($recharge['customId'] ?? 0));
+            if ($customId > 0) {
+                $row['customId'] = $customId;
+            }
+            $clearId = intval(is_object($recharge) ? ($recharge->clearId ?? 0) : ($recharge['clearId'] ?? 0));
+            if ($clearId > 0) {
+                $row['clearId'] = $clearId;
+                $row['clearSn'] = is_object($recharge) ? ($recharge->clearSn ?? '') : ($recharge['clearSn'] ?? '');
+                $row['clearAmount'] = is_object($recharge) ? ($recharge->clearAmount ?? 0) : ($recharge['clearAmount'] ?? 0);
+                return self::normalizeClearAuditRow($row);
+            }
+        }
+
+        // 花店 xhGhsBalanceChange:禁止金额模糊匹配;再尝试 GhsRecharge 直连
+        if (self::isFloristGhsBalanceChangeRow($row)) {
+            if (empty($recharge)) {
+                $ghsRecharge = GhsRechargeClass::getById($relateId, true);
+                if (!empty($ghsRecharge)) {
+                    $recharge = self::findCustomRechargeByGhsRecharge($ghsRecharge);
+                }
+            }
+            if (!empty($recharge)) {
+                $clearId = intval(is_object($recharge) ? ($recharge->clearId ?? 0) : ($recharge['clearId'] ?? 0));
+                if ($clearId > 0) {
+                    return self::attachClearMetaFromClearId($row, $clearId);
+                }
+            }
+            return $row;
+        }
+
+        // 供货商侧 CustomBalanceChange:充值单未落库 clearId 时再按金额+时间匹配
+        return self::fillClearAuditByPaidClearMatch($row, $recharge ?? null);
+    }
+
+    /**
+     * 供货商帮充:CustomBalanceChange.relateId=CustomRecharge.id(无 ghsId 字段);
+     * 花店 xhGhsBalanceChange.relateId=GhsRecharge.id(有 ghsId)。
+     */
+    protected static function resolveCustomRechargeForBalanceRow(array $row)
+    {
+        $relateId = intval($row['relateId'] ?? 0);
+        if ($relateId <= 0) {
+            return null;
+        }
+
+        if (self::isFloristGhsBalanceChangeRow($row)) {
+            $ghsRecharge = GhsRechargeClass::getById($relateId, true);
+            if (empty($ghsRecharge)) {
+                return null;
+            }
+            return self::findCustomRechargeByGhsRecharge($ghsRecharge);
+        }
+
+        return CustomRechargeClass::getById($relateId, true);
+    }
+
+    /** 花店端 xhGhsBalanceChange:有 ghsId;relateId 对应 GhsRecharge(非 CustomRecharge.id) */
+    protected static function isFloristGhsBalanceChangeRow(array $row)
+    {
+        if (intval($row['ghsId'] ?? 0) <= 0) {
+            return false;
+        }
+        if (intval($row['customId'] ?? 0) <= 0) {
+            return true;
+        }
+        $relateId = intval($row['relateId'] ?? 0);
+        return $relateId > 0 && !empty(GhsRechargeClass::getById($relateId, true));
+    }
+
+    /**
+     * 根据 xhGhsRecharge.customRechargeId 取成对的 xhCustomRecharge。
+     *
+     * 一次充值应对写两张充值单并互相关联(linkCustomAndGhsRecharge / 花店建单):
+     * - 结账单 clearId 写在 CustomRecharge 上
+     * - 花店 GhsBalanceChange.relateId = GhsRecharge.id,只能通过 customRechargeId 找到 CustomRecharge
+     *
+     * 若 customRechargeId 为空或查无记录,视为写入阶段脏数据,不做金额/时间猜测匹配。
+     *
+     * @param object|array $ghsRecharge
+     * @return object|null
+     */
+    protected static function findCustomRechargeByGhsRecharge($ghsRecharge)
+    {
+        if (empty($ghsRecharge)) {
+            return null;
+        }
+        $customRechargeId = intval(is_object($ghsRecharge)
+            ? ($ghsRecharge->customRechargeId ?? 0)
+            : ($ghsRecharge['customRechargeId'] ?? 0));
+        if ($customRechargeId <= 0) {
+            return null;
+        }
+        return CustomRechargeClass::getById($customRechargeId, true);
+    }
+
+    /** 写入流水/充值单时带上结账单(有字段才写,对照 hd 充值即带 settleId) */
+    public static function mergeClearAuditIntoRowData(array $data, array $audit)
+    {
+        $clearId = intval($audit['clearId'] ?? 0);
+        if ($clearId <= 0) {
+            return $data;
+        }
+        $probe = new \bizGhs\custom\models\CustomBalanceChange();
+        if (!method_exists($probe, 'hasAttribute') || !$probe->hasAttribute('clearId')) {
+            return $data;
+        }
+        $data['clearId'] = $clearId;
+        $data['clearSn'] = (string)($audit['clearSn'] ?? '');
+        $data['clearAmount'] = bcadd((string)($audit['clearAmount'] ?? 0), '0', 2);
+        if ($probe->hasAttribute('giveAmount')) {
+            $data['giveAmount'] = '0.00';
+        }
+        return $data;
+    }
+
+    public static function mergeClearAuditIntoRechargeData(array $data, array $audit)
+    {
+        $clearId = intval($audit['clearId'] ?? 0);
+        if ($clearId <= 0) {
+            return $data;
+        }
+        $probe = new \bizGhs\custom\models\CustomRecharge();
+        if (!method_exists($probe, 'hasAttribute') || !$probe->hasAttribute('clearId')) {
+            return $data;
+        }
+        $data['clearId'] = $clearId;
+        $data['clearSn'] = (string)($audit['clearSn'] ?? '');
+        $data['clearAmount'] = bcadd((string)($audit['clearAmount'] ?? 0), '0', 2);
+        return $data;
+    }
+
+    protected static function attachClearMetaFromClearId(array $row, $clearId)
+    {
+        $clearId = intval($clearId);
+        if ($clearId <= 0) {
+            return $row;
+        }
+        $clear = OrderClearClass::getById($clearId, true);
+        if (empty($clear)) {
+            return $row;
+        }
+        $row['clearId'] = $clearId;
+        $row['clearSn'] = is_object($clear) ? ($clear->orderSn ?? '') : ($clear['orderSn'] ?? '');
+        $actPrice = is_object($clear) ? ($clear->actPrice ?? 0) : ($clear['actPrice'] ?? 0);
+        $row['clearAmount'] = bcadd((string)$actPrice, '0', 2);
+        return self::normalizeClearAuditRow($row);
+    }
+
+    /**
+     * 充值入账与结账单关联:同客户、同实收金额、付款时间接近的已付结账单
+     */
+    protected static function fillClearAuditByPaidClearMatch(array $row, $recharge = null)
+    {
+        $customId = intval($row['customId'] ?? 0);
+        if ($customId <= 0 && !empty($recharge)) {
+            $customId = intval(is_object($recharge) ? ($recharge->customId ?? 0) : ($recharge['customId'] ?? 0));
+            if ($customId > 0) {
+                $row['customId'] = $customId;
+            }
+        }
+        if ($customId <= 0) {
+            return $row;
+        }
+        $amount = bcadd((string)($row['amount'] ?? 0), '0', 2);
+        $anchorTime = (string)($row['addTime'] ?? '');
+        if (!empty($recharge)) {
+            $amount = bcadd((string)(is_object($recharge) ? ($recharge->amount ?? $amount) : ($recharge['amount'] ?? $amount)), '0', 2);
+            $payTime = is_object($recharge) ? ($recharge->payTime ?? '') : ($recharge['payTime'] ?? '');
+            if (!empty($payTime) && $payTime !== '0000-00-00 00:00:00') {
+                $anchorTime = $payTime;
+            }
+        }
+        $list = OrderClearClass::getAllByCondition([
+            'customId' => $customId,
+            'status' => PurchaseClearClass::STATUS_HAS_PAY,
+            'actPrice' => $amount,
+        ], 'payTime DESC,id DESC', '*', null, true);
+        if (empty($list)) {
+            return $row;
+        }
+        $best = null;
+        $bestDiff = null;
+        $anchorTs = !empty($anchorTime) ? strtotime($anchorTime) : 0;
+        foreach ($list as $clear) {
+            $clearId = intval(is_object($clear) ? ($clear->id ?? 0) : ($clear['id'] ?? 0));
+            if ($clearId <= 0) {
+                continue;
+            }
+            if ($anchorTs <= 0) {
+                $best = $clear;
+                break;
+            }
+            $payTime = is_object($clear) ? ($clear->payTime ?? '') : ($clear['payTime'] ?? '');
+            $payTs = !empty($payTime) ? strtotime($payTime) : 0;
+            if ($payTs <= 0) {
+                continue;
+            }
+            $diff = abs($anchorTs - $payTs);
+            if ($bestDiff === null || $diff < $bestDiff) {
+                $bestDiff = $diff;
+                $best = $clear;
+            }
+        }
+        if (empty($best) || $bestDiff === null || $bestDiff > 300) {
+            return $row;
+        }
+        $clearId = intval(is_object($best) ? ($best->id ?? 0) : ($best['id'] ?? 0));
+        return self::attachClearMetaFromClearId($row, $clearId);
+    }
+
+    protected static function normalizeClearAuditRow(array $row)
+    {
+        if (intval($row['clearId'] ?? 0) > 0 && (empty($row['clearSn']) || bccomp((string)($row['clearAmount'] ?? 0), '0', 2) <= 0)) {
+            $row = self::attachClearMetaFromClearId($row, intval($row['clearId']));
+        }
+        return $row;
+    }
+
+    /** 供货商帮充 / 线上充 / 售后返充 */
+    protected static function isRechargeCapitalType($capitalType)
+    {
+        $capitalType = intval($capitalType);
+        $ids = [
+            dict::getDict('capitalType', 'ghsHelpCustomRecharge', 'id'),
+            dict::getDict('capitalType', 'ghsHelpCustomRechargeReturn', 'id'),
+            dict::getDict('capitalType', 'customRechargeToGhs', 'id'),
+        ];
+        return in_array($capitalType, $ids, true);
+    }
+
+    /** 花店侧 xhGhsBalanceChange:relateId 为 GhsRecharge.id */
+    protected static function isGhsSideRechargeCapitalType($capitalType)
+    {
+        $capitalType = intval($capitalType);
+        $ids = [
+            dict::getDict('capitalType', 'customAskGhsRecharge', 'id'),
+            dict::getDict('capitalType', 'customAskGhsRechargeReturn', 'id'),
+        ];
+        return in_array($capitalType, $ids, true);
+    }
+
+    /** 结账类入账(relateId=结账单 id) */
+    protected static function isClearIncomeCapitalType($capitalType)
+    {
+        $capitalType = intval($capitalType);
+        $ids = [
+            dict::getDict('capitalType', 'ghsXsClear', 'id'),
+            dict::getDict('capitalType', 'ghsHelpCustomUseBalanceClear', 'id'),
+            dict::getDict('capitalType', 'customAskGhsUseBalanceClear', 'id'),
+        ];
+        return in_array($capitalType, $ids, true);
+    }
+
+    /**
+     * 充值销账后回写流水上的 clearId(GhsRechargeSettleService 调用)
+     */
+    public static function patchClearAuditByRechargeId($customId, $rechargeId, $clearId, $clearSn, $clearAmount, $finalBalance = null)
+    {
+        $customId = intval($customId);
+        $rechargeId = intval($rechargeId);
+        $clearId = intval($clearId);
+        if ($customId <= 0 || $rechargeId <= 0 || $clearId <= 0) {
+            return;
+        }
+        $row = self::getByCondition([
+            'customId' => $customId,
+            'relateId' => $rechargeId,
+            'io' => 1,
+        ], true, null, 'id DESC');
+        if (empty($row) || !method_exists($row, 'hasAttribute') || !$row->hasAttribute('clearId')) {
+            return;
+        }
+        $row->clearId = $clearId;
+        $row->clearSn = (string)$clearSn;
+        $row->clearAmount = bcadd((string)$clearAmount, '0', 2);
+        if ($finalBalance !== null && $row->hasAttribute('balance')) {
+            $row->balance = $finalBalance;
+        }
+        if ($row->hasAttribute('giveAmount')) {
+            $row->giveAmount = '0.00';
+        }
+        $row->save(false);
+    }
+
+    /**
+     * 花店线上充:回写 xhGhsBalanceChange(relateId=GhsRecharge.id)
+     */
+    public static function patchClearAuditByGhsRechargeId($ghsRechargeId, $clearId, $clearSn, $clearAmount, $finalBalance = null)
+    {
+        $ghsRechargeId = intval($ghsRechargeId);
+        $clearId = intval($clearId);
+        if ($ghsRechargeId <= 0 || $clearId <= 0) {
+            return;
+        }
+        $row = GhsBalanceChangeClass::getByCondition([
+            'relateId' => $ghsRechargeId,
+            'io' => 1,
+        ], true, null, 'id DESC');
+        if (empty($row) || !method_exists($row, 'hasAttribute') || !$row->hasAttribute('clearId')) {
+            return;
+        }
+        $row->clearId = $clearId;
+        $row->clearSn = (string)$clearSn;
+        $row->clearAmount = bcadd((string)$clearAmount, '0', 2);
+        if ($row->hasAttribute('giveAmount')) {
+            $row->giveAmount = '0.00';
+        }
+        if ($finalBalance !== null && $row->hasAttribute('balance')) {
+            $row->balance = $finalBalance;
+        }
+        $row->save(false);
+    }
+
 
     public static function exportData($respond, $mainId)
     {

+ 40 - 4
biz-ghs/custom/classes/CustomClass.php

@@ -196,12 +196,13 @@ class CustomClass extends BaseClass
     }
 
     /**
-     * 使用账户余额结账:扣减净 balance(合并后来款销单必调)
-     * 调用方:CustomPaymentAllocateService 在 confirmClear(paymentSettleOnly) 之后,
-     *         按实销金额扣回「已用于销挂账」部分,避免 balance 与订单欠款不一致
+     * 用账面正余额支付挂账:扣减净 balance 并记「结账」流水。
+     * 来款充值后的自动销单不要调本方法(对照 hd:来款已入账,销单只改订单)。
+     * 调用方:GhsRechargeSettleService::consumePositiveBalanceFifo
      */
-    public static function clearConsumeBalance($amount, $custom, $ghs, $shop, $staff, $clear)
+    public static function clearConsumeBalance($amount, $custom, $ghs, $shop, $staff, $clear, $options = [])
     {
+        $skipBalanceChangeRecord = !empty($options['skipBalanceChangeRecord']);
         // 先合并历史挂账到 balance,再扣减(只记余额变动)
         AccountMoneyClass::ensureCustomMoneyReady($custom, true);
         AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
@@ -234,6 +235,11 @@ class CustomClass extends BaseClass
         $custom->balance = $newCustomBalance;
         $custom->save();
 
+        // 充值销账:只记一条充值流水(附 clearAmount),不另记「结账 -X」行(对照 hd settleAmount)
+        if ($skipBalanceChangeRecord) {
+            return;
+        }
+
         $capitalType = dict::getDict('capitalType', 'ghsHelpCustomUseBalanceClear', 'id');
         $fromType = dict::getDict('fromType', 'shop');
         $cbData = [
@@ -292,6 +298,29 @@ class CustomClass extends BaseClass
         $clear->save();
     }
 
+    /** 供货商帮充与花店线上充:成对充值单互相关联,供余额明细展示结账单 */
+    protected static function linkCustomAndGhsRecharge($customRecharge, $ghsRecharge)
+    {
+        if (empty($customRecharge) || empty($ghsRecharge)) {
+            return;
+        }
+        $customRechargeId = intval(is_object($customRecharge) ? ($customRecharge->id ?? 0) : 0);
+        $ghsRechargeId = intval(is_object($ghsRecharge) ? ($ghsRecharge->id ?? 0) : 0);
+        if ($customRechargeId <= 0 || $ghsRechargeId <= 0) {
+            return;
+        }
+        $cProbe = new \bizGhs\custom\models\CustomRecharge();
+        if (method_exists($cProbe, 'hasAttribute') && $cProbe->hasAttribute('ghsRechargeId')) {
+            $customRecharge->ghsRechargeId = $ghsRechargeId;
+            $customRecharge->save(false, ['ghsRechargeId']);
+        }
+        $gProbe = new \bizGhs\ghs\models\GhsRecharge();
+        if (method_exists($gProbe, 'hasAttribute') && $gProbe->hasAttribute('customRechargeId')) {
+            $ghsRecharge->customRechargeId = $customRechargeId;
+            $ghsRecharge->save(false, ['customRechargeId']);
+        }
+    }
+
     // 充值/返充(增加净 balance);有多个地方要同步修改,请搜索关键词 custom_ghs_recharge ssh 20240310
     public static function rechargeBalance($custom, $amount, $shop, $staff, $payWay, $params = [])
     {
@@ -357,6 +386,9 @@ class CustomClass extends BaseClass
             'payTime' => $payTime,
             'rechargeType' => $rechargeType,
         ];
+        if (!empty($params['clearAudit']) && is_array($params['clearAudit'])) {
+            $cData = CustomBalanceChangeClass::mergeClearAuditIntoRechargeData($cData, $params['clearAudit']);
+        }
         $cRecharge = CustomRechargeClass::add($cData, true);
         $cRechargeId = $cRecharge->id ?? 0;
 
@@ -387,6 +419,9 @@ class CustomClass extends BaseClass
             'sjId' => $sjId,
             'remark' => $remark,
         ];
+        if (!empty($params['clearAudit']) && is_array($params['clearAudit'])) {
+            $cbData = CustomBalanceChangeClass::mergeClearAuditIntoRowData($cbData, $params['clearAudit']);
+        }
         CustomBalanceChangeClass::add($cbData, true);
 
         $ghsInfo->balance = bcadd($ghsInfo->balance, $amount, 2);
@@ -418,6 +453,7 @@ class CustomClass extends BaseClass
         ];
         $gRecharge = GhsRechargeClass::add($gData, true);
         $gRechargeId = $gRecharge->id ?? 0;
+        self::linkCustomAndGhsRecharge($cRecharge, $gRecharge);
         $capitalType = dict::getDict('capitalType', 'customAskGhsRecharge', 'id');
         if ($rechargeType == 1) {
             //售后返充

+ 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 - 409
biz-ghs/custom/services/CustomPaymentAllocateService.php

@@ -2,443 +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;
-
 /**
- * 批发客户来款分配销挂账(对齐零售 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
+ * 兼容层:批发充值销账已迁至 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 销单,再扣余额(见 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;
+        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' => '线上充值销账',
-        ]);
-
-        if (!empty($customRecharge) && !empty($allocate['clear'])) {
-            self::patchRechargeAudit($customRecharge, $ghsRecharge, $allocate);
-        }
-
-        return $allocate;
+        return GhsRechargeSettleService::onlinePayFifoClear($custom, $ghs, $shop, $amount, $customRecharge, $ghsRecharge);
     }
 
-    /**
-     * 【用途】待付结账单 — 供货商确认「客户已转账」(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);
-        $payCap = bcadd((string)($clear->actPrice ?? '0'), '0', 2);
-        if (bccomp($realClearAmount, $payCap, 2) > 0) {
-            $realClearAmount = $payCap;
-        }
-        // 从净余额扣掉实销金额(不超过结账单 actPrice,兼容手工抹零结账单)
-        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,
-        ];
+        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' => '余额结账',
-        ]);
-
-        $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;
-    }
-
-    /**
-     * 【用途】用指定来款池金额销挂账:建结账单 → 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);
-        $poolAmount = bcadd((string)$poolAmount, '0', 2);
-        if (bccomp($realClearAmount, $poolAmount, 2) > 0) {
-            util::fail('销账金额超过本次来款' . floatval($poolAmount) . '元,请检查订单待结或联系技术员');
-        }
-
-        // 入账后须把「用于销单」的部分从 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);
+        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'] ?? [];
     }
 

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

@@ -0,0 +1,434 @@
+<?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('结账单金额有误');
+        }
+
+        $clearId = intval($clear->id ?? 0);
+        $clearSn = $clear->orderSn ?? '';
+        $clearAudit = [
+            'clearId' => $clearId,
+            'clearSn' => $clearSn,
+            'clearAmount' => $pool,
+        ];
+
+        $ghsRecharge = null;
+        if (empty($customRecharge)) {
+            $respond = CustomClass::rechargeBalance($custom, $pool, $shop, $staff, $payWay, [
+                'remark' => $clear->remark ?? '',
+                'clearAudit' => $clearAudit,
+            ]);
+            $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($clearId));
+        if (bccomp($settled, $pool, 2) > 0) {
+            $settled = $pool;
+        }
+        $clearAudit['clearAmount'] = $settled;
+
+        self::refreshDebtFlags($custom, $ghs);
+
+        $allocate = [
+            'clear' => $clear,
+            'realClearAmount' => $settled,
+            'clearId' => $clearId,
+            'clearSn' => $clearSn,
+            '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;
+        $customId = is_object($custom) ? intval($custom->id ?? 0) : 0;
+        if ($customId <= 0 && is_object($customRecharge)) {
+            $customId = intval($customRecharge->customId ?? 0);
+        }
+        CustomBalanceChangeClass::patchClearAuditByRechargeId($customId, $relateId, $clearId, $clearSn, $clearAmount, $finalCustomBalance);
+        self::patchBalanceChangeRow(CustomBalanceChangeClass::class, $relateId, $clearId, $clearSn, $clearAmount, [
+            'customId' => $customId,
+        ], $finalCustomBalance);
+
+        if (!empty($ghsRecharge)) {
+            $gRelateId = intval($ghsRecharge->id ?? 0);
+            CustomBalanceChangeClass::patchClearAuditByGhsRechargeId(
+                $gRelateId,
+                $clearId,
+                $clearSn,
+                $clearAmount,
+                $finalGhsBalance
+            );
+            self::patchBalanceChangeRow(GhsBalanceChangeClass::class, $gRelateId, $clearId, $clearSn, $clearAmount, [], $finalGhsBalance);
+            $balanceGhsId = intval($ghsRecharge->ghsId ?? 0);
+            if ($balanceGhsId > 0) {
+                self::patchBalanceChangeRow(GhsBalanceChangeClass::class, 0, $clearId, $clearSn, $clearAmount, [
+                    'ghsId' => $balanceGhsId,
+                    'amount' => self::money($ghsRecharge->amount ?? 0),
+                ], $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);
+    }
+
+}

+ 6 - 0
biz-ghs/ghs/classes/GhsBalanceChangeClass.php

@@ -12,4 +12,10 @@ class GhsBalanceChangeClass extends BaseClass
 
     public static $baseFile = '\bizGhs\ghs\models\GhsBalanceChange';
 
+    /** 批发店供货商余额变动列表(xhGhsBalanceChange,按 ghsId) */
+    public static function getChangeList($where)
+    {
+        return self::getList('*', $where, 'addTime DESC,id DESC');
+    }
+
 }

+ 46 - 2
biz-ghs/ghs/classes/GhsClass.php

@@ -3,6 +3,7 @@
 namespace bizGhs\ghs\classes;
 
 use bizGhs\base\classes\BaseClass;
+use bizGhs\custom\classes\AccountMoneyClass;
 use bizGhs\order\classes\PurchaseOrderClass;
 use bizGhs\shop\classes\ShopClass;
 use common\components\imgUtil;
@@ -14,13 +15,44 @@ class GhsClass extends BaseClass
     public static $baseFile = '\bizGhs\ghs\models\Ghs';
 
     //供货商列表 ssh 20221220
-    public static function getGhsList($where)
+    public static function getGhsList($where, $sortType = 'all')
     {
-        $data = self::getList('*', $where, 'debtAmount DESC,inTurn DESC,addTime DESC');
+        $order = 'inTurn DESC,addTime DESC';
+        if ($sortType === 'debt') {
+            $order = 'balance ASC,inTurn DESC,addTime DESC';
+        } elseif ($sortType === 'expend') {
+            $order = 'expendAmount DESC,inTurn DESC,addTime DESC';
+        }
+        $data = self::getList('*', $where, $order);
         $data['list'] = self::groupBaseInfo($data['list']);
         return $data;
     }
 
+    /**
+     * 批发店采购:按 xhGhs 净 balance 汇总总待结(balance<0 部分之和,合并后模型)
+     */
+    /** 供货商列表 Tab 数量(一次返回,避免前端多次 list 请求) */
+    public static function getSupplierTabCounts($ownShopId)
+    {
+        if (empty($ownShopId)) {
+            return ['all' => 0, 'debtRank' => 0, 'expendRank' => 0];
+        }
+        $base = ['ownShopId' => $ownShopId, 'delStatus' => 0];
+        return [
+            'all' => (int)self::getCount($base),
+            'debtRank' => (int)self::getCount(array_merge($base, ['balance<' => 0])),
+            'expendRank' => (int)self::getCount($base),
+        ];
+    }
+
+    public static function sumTotalOutstandingDebt($shopId, $delStatus = 0)
+    {
+        if (empty($shopId)) {
+            return '0.00';
+        }
+        $sum = self::sum(['ownShopId' => $shopId, 'delStatus' => $delStatus], 'balance');
+        return bcadd($sum ?: '0', '0', 2);
+    }
 
     public static function groupBaseInfo($list)
     {
@@ -30,6 +62,18 @@ class GhsClass extends BaseClass
         $ids = array_column($list, 'shopId');
         $shopInfo = ShopClass::getByIds($ids, null, 'id');
         foreach ($list as $key => $val) {
+            // 列表读:xhGhs 未合并的挂账先懒合并(待结订单笔数/金额仍用采购单统计,见下方)
+            $ghsId = $val['id'] ?? 0;
+            if (!empty($ghsId) && bccomp($val['debtAmount'] ?? '0', '0', 2) != 0 && empty($val['balanceMerged'])) {
+                AccountMoneyClass::mergeGhsRowFromListIfNeeded($ghsId);
+                $fresh = self::getById($ghsId);
+                if (!empty($fresh)) {
+                    $freshAttrs = is_array($fresh) ? $fresh : $fresh->getAttributes();
+                    $list[$key] = array_merge($list[$key], $freshAttrs);
+                    $val = $list[$key];
+                }
+            }
+
             $avatar = $val['avatar'] ?? '';
             $list[$key]['shortAvatar'] = $avatar;
             $list[$key]['avatar'] = imgUtil::groupImg($avatar);

+ 45 - 22
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 = [])
     {
@@ -324,23 +334,10 @@ class OrderClearClass extends BaseClass
             }
             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));
-                }
-            }
+            self::syncHdPurchaseAfterOrderClear($orderId, $cgId, $mayClear, $clearId, $clearTime);
 
             $totalSettled = bcadd($totalSettled, $mayClear, 2);
+            $os->amount = $mayClear;
             $os->status = 1;
             $os->save();
         }
@@ -390,4 +387,30 @@ class OrderClearClass extends BaseClass
         OrderCgClearClass::markDoneByClearId($clearId);
     }
 
+    /**
+     * 批发订单销账后同步花店采购单待结(充值 FIFO 等);cgId 缺失时按 saleId 回查。
+     */
+    protected static function syncHdPurchaseAfterOrderClear($orderId, $cgId, $mayClear, $clearId, $clearTime)
+    {
+        $purchase = null;
+        if (intval($cgId) > 0) {
+            $purchase = PurchaseClass::getById($cgId, true);
+        } elseif (intval($orderId) > 0) {
+            $purchase = PurchaseClass::getByCondition(['saleId' => $orderId], true);
+        }
+        if (empty($purchase)) {
+            return;
+        }
+        $pRemain = bcsub((string)($purchase->remainDebtPrice ?? 0), (string)$mayClear, 2);
+        $pUpdate = [
+            'remainDebtPrice' => bccomp($pRemain, '0', 2) > 0 ? $pRemain : '0.00',
+            'clearTime' => $clearTime,
+        ];
+        if (bccomp($pRemain, '0', 2) <= 0) {
+            $pUpdate['debt'] = PurchaseClass::DEBT_NO;
+            $pUpdate['clearId'] = $clearId;
+        }
+        $purchase->save(false, array_keys($pUpdate));
+    }
+
 }

+ 166 - 71
biz-ghs/order/classes/PurchaseOrderClass.php

@@ -19,8 +19,9 @@ use bizGhs\book\classes\BookItemCustomClass;
 use bizGhs\book\classes\BookItemGhsClass;
 use bizGhs\cg\classes\CgOrderItemClass;
 use bizGhs\cg\classes\CgOrderItemSendClass;
+use bizGhs\custom\classes\AccountMoneyClass;
 use bizGhs\custom\classes\CustomClass;
-use bizGhs\ghs\classes\GhsDebtRecordClass;
+use bizGhs\ghs\classes\GhsBalanceChangeClass;
 use bizGhs\item\classes\CostChangeClass;
 use bizGhs\item\classes\ItemClassClass;
 use bizGhs\order\traits\OrderTrait;
@@ -70,6 +71,162 @@ class PurchaseOrderClass extends BaseClass
     const IN_TYPE_LATER = 0;
     const IN_TYPE_NOW = 1;
 
+    /**
+     * 采购挂账:买方 xhGhs 净 balance 减少,并记余额变动(合并后模型,不再写欠款字段)
+     */
+    public static function applyPurchaseDebtOnBuyerGhs($ghs, $amount, $purchase, $sjId, $shopId)
+    {
+        AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
+        $ghsNewBalance = bcsub($ghs->balance ?? '0.00', $amount, 2);
+        $ghs->debt = GhsClass::DEBT_YES;
+        $ghs->balance = $ghsNewBalance;
+        $ghs->debtNum += 1;
+        $ghs->expendAmount = bcadd($ghs->expendAmount ?? '0.00', $amount, 2);
+        $ghs->expendNum += 1;
+        $ghs->save(false, ['debt', 'balance', 'debtNum', 'expendAmount', 'expendNum']);
+
+        $orderSn = $purchase->orderSn ?? '';
+        $capitalType = dict::getDict('capitalType', 'ghsPurchase', 'id');
+        GhsBalanceChangeClass::add([
+            'ghsId' => $ghs->id ?? 0,
+            'relateId' => $purchase->id ?? 0,
+            'ptStyle' => 2,
+            'capitalType' => $capitalType,
+            'amount' => $amount,
+            'balance' => $ghsNewBalance,
+            'staffId' => $purchase->shopAdminId ?? 0,
+            'staffName' => $purchase->shopAdminName ?? '',
+            'io' => 0,
+            'event' => '新增采购 ' . $orderSn,
+            'sjId' => $sjId,
+            'shopId' => $shopId,
+        ], true);
+
+        return $ghs;
+    }
+
+    /**
+     * 采购挂账:供货方 custom 净 balance 同步减少(双边关系)
+     */
+    public static function applyPurchaseDebtOnSupplierCustom($ghs, $amount, $ghsMain = null)
+    {
+        $customId = $ghs->customId ?? 0;
+        $custom = CustomClass::getLockById($customId);
+        if (empty($custom)) {
+            util::fail('没有找到客户信息');
+        }
+        AccountMoneyClass::ensureCustomMoneyReady($custom, true);
+        $custom->isDebt = CustomClass::IS_DEBT_YES;
+        $custom->balance = bcsub($custom->balance ?? '0.00', $amount, 2);
+        if ($custom->debtNum == 0 && $ghsMain !== null) {
+            $ghsMain->mayGatheringNum += 1;
+        }
+        $custom->debtNum += 1;
+        $custom->buyNum += 1;
+        $custom->buyAmount = bcadd($custom->buyAmount ?? '0.00', $amount, 2);
+        $custom->save();
+
+        return $custom;
+    }
+
+    /**
+     * 采购售后退款:买方 xhGhs 净 balance 增加 + 流水;供货方 custom 同步
+     */
+    public static function applyPurchaseRefundOnAccounts($ghs, $amount, $refund, $order, $sjId, $shopId)
+    {
+        AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
+        $ghsNewBalance = bcadd($ghs->balance ?? '0.00', $amount, 2);
+        $ghs->balance = $ghsNewBalance;
+        $ghs->save(false, ['balance']);
+
+        $orderSn = $order->orderSn ?? '';
+        $refundSn = $refund->refundSn ?? '';
+        $capitalType = dict::getDict('capitalType', 'ghsCgOrderRefund', 'id');
+        GhsBalanceChangeClass::add([
+            'ghsId' => $ghs->id ?? 0,
+            'relateId' => $refund->id ?? 0,
+            'ptStyle' => 2,
+            'capitalType' => $capitalType,
+            'amount' => $amount,
+            'balance' => $ghsNewBalance,
+            'io' => 1,
+            'staffId' => $refund->shopAdminId ?? 0,
+            'staffName' => $refund->shopAdminName ?? '',
+            'event' => '采购单 ' . $orderSn . ' 申请售后 ' . $refundSn,
+            'sjId' => $sjId,
+            'shopId' => $shopId,
+        ], true);
+
+        $customId = $ghs->customId ?? 0;
+        if ($customId > 0) {
+            $custom = CustomClass::getLockById($customId);
+            if (!empty($custom)) {
+                AccountMoneyClass::ensureCustomMoneyReady($custom, true);
+                $customNewBalance = bcadd($custom->balance ?? '0.00', $amount, 2);
+                $custom->balance = $customNewBalance;
+                $custom->isDebt = bccomp($customNewBalance, '0', 2) < 0 ? CustomClass::IS_DEBT_YES : CustomClass::IS_DEBT_NO;
+                $custom->save(false, ['balance', 'isDebt']);
+            }
+        }
+
+        return $ghs;
+    }
+
+    /**
+     * 采购结账:买方 xhGhs 净 balance 增加 + 流水
+     */
+    public static function applyPurchaseClearOnBuyerGhs($ghs, $amount, $clear, $orderCount)
+    {
+        AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
+        $ghsNewBalance = bcadd($ghs->balance ?? '0.00', $amount, 2);
+        $ghs->balance = $ghsNewBalance;
+        $ghs->debtNum -= $orderCount;
+        $ghs->debt = bccomp($ghsNewBalance, '0', 2) < 0 ? GhsClass::DEBT_YES : GhsClass::DEBT_NO;
+        $ghs->save(false, ['balance', 'debtNum', 'debt']);
+
+        $capitalType = dict::getDict('capitalType', 'ghsCgOrderClear', 'id');
+        GhsBalanceChangeClass::add([
+            'ghsId' => $ghs->id ?? 0,
+            'relateId' => $clear->id ?? 0,
+            'ptStyle' => 2,
+            'capitalType' => $capitalType,
+            'amount' => $amount,
+            'balance' => $ghsNewBalance,
+            'staffId' => $clear->customShopAdminId ?? 0,
+            'staffName' => $clear->customShopAdminName ?? '',
+            'io' => 1,
+            'event' => '采购单结账 ' . ($clear->orderSn ?? ''),
+            'sjId' => $clear->sjId ?? 0,
+            'shopId' => $clear->shopId ?? 0,
+        ], true);
+
+        return $ghs;
+    }
+
+    /**
+     * 采购结账:供货方 custom 净 balance 同步增加
+     */
+    public static function applyPurchaseClearOnSupplierCustom($ghs, $amount, $orderCount, $ghsMain = null)
+    {
+        $customId = $ghs->customId ?? 0;
+        $custom = CustomClass::getLockById($customId);
+        if (empty($custom)) {
+            util::fail('没有找到客户信息');
+        }
+        AccountMoneyClass::ensureCustomMoneyReady($custom, true);
+        $customNewBalance = bcadd($custom->balance ?? '0.00', $amount, 2);
+        $custom->balance = $customNewBalance;
+        $custom->debtNum -= $orderCount;
+        $custom->isDebt = bccomp($customNewBalance, '0', 2) < 0 ? CustomClass::IS_DEBT_YES : CustomClass::IS_DEBT_NO;
+        $custom->save(false, ['balance', 'debtNum', 'isDebt']);
+        if ($custom->debtNum == 0 && $ghsMain !== null) {
+            $ghsMain->mayGatheringNum -= 1;
+            $ghsMain->save();
+        }
+
+        return $custom;
+    }
+
     public static function assign($cg, $shop, $params)
     {
         $bookSn = $cg->bookSn ?? 0;
@@ -708,36 +865,10 @@ class PurchaseOrderClass extends BaseClass
             util::fail('没有找到供货商');
         }
 
-        // 采购挂账:合并后 balance 减少,记 GhsBalanceChange 而非 GhsDebtRecord
-        \bizGhs\custom\classes\AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
-        $ghs->debt = GhsClass::DEBT_YES;
-        $ghsNewBalance = bcsub($ghs->balance ?? '0.00', $currentPrice, 2);
-        $ghs->balance = $ghsNewBalance;
-
-        $ghs->debtNum += 1;
-        $ghs->expendAmount = bcadd($ghs->expendAmount, $currentPrice, 2);
-        $ghs->expendNum += 1;
-        $ghs->save(false, ['debt', 'balance', 'debtNum', 'expendAmount', 'expendNum']);
-
-        $capitalType = dict::getDict('capitalType', 'ghsPurchase', 'id');
-        $debtData = [
-            'ghsId' => $ghsId,
-            'relateId' => $cg->id,
-            'ptStyle' => 2,
-            'capitalType' => $capitalType,
-            'amount' => $currentPrice,
-            'balance' => $ghsNewBalance,
-            'staffId' => $cg->shopAdminId,
-            'staffName' => $cg->shopAdminName,
-            'io' => 0,
-            'event' => '新增采购 ' . $orderSn,
-            'sjId' => $sjId,
-            'shopId' => $shopId,
-        ];
-        \bizGhs\ghs\classes\GhsBalanceChangeClass::add($debtData, true);
+        self::applyPurchaseDebtOnBuyerGhs($ghs, $currentPrice, $cg, $sjId, $shopId);
 
         //采购统计
-        $cgNum = $order->itemNum ?? 0;
+        $cgNum = $cg->itemNum ?? 0;
         StatCgClass::replace($main, $shop, $currentPrice, $cgNum);
         //采购按供货商统计
         StatCgGhsClass::ghsReplace($cg);
@@ -756,31 +887,14 @@ class PurchaseOrderClass extends BaseClass
         $currentMayGathering = bcadd($ghsMain->mayGathering, $currentPrice, 2);
         $ghsMain->mayGathering = $currentMayGathering;
 
-        //客户资产增加
-        $customId = $ghs->customId;
-        $custom = CustomClass::getLockById($customId);
-        if (empty($custom)) {
-            util::fail('没有找到客户信息');
-        }
-        \bizGhs\custom\classes\AccountMoneyClass::ensureCustomMoneyReady($custom, true);
-        $custom->isDebt = CustomClass::IS_DEBT_YES;
-        $custom->balance = bcsub($custom->balance ?? '0.00', $currentPrice, 2); // 客户待结增加=净 balance 减少
-        if ($custom->debtNum == 0) {
-            $ghsMain->mayGatheringNum += 1;
-        }
+        self::applyPurchaseDebtOnSupplierCustom($ghs, $currentPrice, $ghsMain);
         $ghsMain->save();
-        $custom->debtNum += 1;
-        $custom->buyNum += 1;
-        $custom->buyAmount = bcadd($custom->buyAmount, $currentPrice, 2);
-        $custom->save();
 
         //当天和当月支出统计
         StatOutClass::updateOrInsert($main, $shop, $currentPrice);
         //支出流水
         $payWay = dict::getDict('payWay', 'unknown');
         $capitalType = dict::getDict('capitalType', 'ghsPurchase', 'id');
-        $sjId = $order->sjId ?? 0;
-        $shopId = $order->shopId ?? 0;
         $event = '采购';
         $mainId = $shop->mainId ?? 0;
         $capitalData = [
@@ -1194,14 +1308,7 @@ class PurchaseOrderClass extends BaseClass
                 if (empty($ghs)) {
                     util::fail('没有找到供货商');
                 }
-                \bizGhs\custom\classes\AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
-                $ghs->debt = GhsClass::DEBT_YES;
-                $ghs->balance = bcsub($ghs->balance ?? '0.00', $currentPrice, 2);
-
-                $ghs->debtNum += 1;
-                $ghs->expendAmount = bcadd($ghs->expendAmount, $currentPrice, 2);
-                $ghs->expendNum += 1;
-                $ghs->save(false, ['debt', 'balance', 'debtNum', 'expendAmount', 'expendNum']);
+                self::applyPurchaseDebtOnBuyerGhs($ghs, $currentPrice, $order, $sjId, $shopId);
 
                 //采购统计
                 $cgNum = $order->itemNum ?? 0;
@@ -1223,23 +1330,11 @@ class PurchaseOrderClass extends BaseClass
                 $currentMayGathering = bcadd($ghsMain->mayGathering, $currentPrice, 2);
                 $ghsMain->mayGathering = $currentMayGathering;
 
-                //客户资产增加
-                $customId = $ghs->customId;
-                $custom = CustomClass::getLockById($customId);
-                if (empty($custom)) {
-                    util::fail('没有找到客户信息');
-                }
-                \bizGhs\custom\classes\AccountMoneyClass::ensureCustomMoneyReady($custom, true);
-                $custom->isDebt = CustomClass::IS_DEBT_YES;
-                $custom->balance = bcsub($custom->balance ?? '0.00', $currentPrice, 2);
-                if ($custom->debtNum == 0) {
-                    $ghsMain->mayGatheringNum += 1;
-                }
+                self::applyPurchaseDebtOnSupplierCustom($ghs, $currentPrice, $ghsMain);
                 $ghsMain->save();
-                $custom->debtNum += 1;
-                $custom->buyNum += 1;
-                $custom->buyAmount = bcadd($custom->buyAmount, $currentPrice, 2);
-                $custom->save();
+
+                $order->debt = self::DEBT_YES;
+                $order->save(false, ['debt']);
 
                 //当天和当月支出统计
                 StatOutClass::updateOrInsert($main, $shop, $currentPrice);

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

@@ -15,6 +15,7 @@ use bizGhs\base\services\BaseService;
 use bizGhs\book\classes\BookItemClass;
 use bizGhs\custom\classes\CustomClass;
 use bizGhs\custom\classes\CustomDebtChangeClass;
+use bizGhs\clear\classes\OrderCgClearClass;
 use bizGhs\order\classes\OrderClass;
 use bizGhs\order\classes\OrderExpressClass;
 use bizGhs\order\classes\OrderItemClass;
@@ -694,7 +695,10 @@ class OrderService extends BaseService
         }
         $info = $order->attributes;
         $respond = OrderClass::groupOrder([$info], $showButton, $showItem, $showExpress, $isList);
-        return current($respond);
+        $result = current($respond);
+        $orderId = intval($result['id'] ?? 0);
+        $result['orderClearList'] = $orderId > 0 ? OrderCgClearClass::listPaidClearRecordsForOrder($orderId) : [];
+        return $result;
     }
 
     //订单失效后的操作 ssh 2021.5.14

+ 60 - 1
biz-hd/balance/classes/BalanceChangeClass.php

@@ -2,6 +2,9 @@
 
 namespace bizHd\balance\classes;
 
+use bizHd\order\classes\SettleClass;
+use bizHd\recharge\classes\RechargeClass;
+use common\components\dict;
 use Yii;
 use bizHd\base\classes\BaseClass;
 
@@ -15,6 +18,8 @@ class BalanceChangeClass extends BaseClass
         $data = self::getList('*', $where, 'addTime DESC,id DESC');
         if (!empty($data['list'])) {
             foreach ($data['list'] as $k => $v) {
+                $v = is_array($v) ? $v : (array)$v;
+                $v = self::fillSettleAuditFromRecharge($v);
                 $settleAmount = $v['settleAmount'] ?? 0;
                 $amount = $v['amount'] ?? 0;
                 $io = $v['io'] ?? 0;
@@ -23,10 +28,64 @@ class BalanceChangeClass extends BaseClass
                     $becomeBalance = bcsub($amount, $settleAmount, 2);
                     $becomeBalance = floatval($becomeBalance);
                 }
-                $data['list'][$k]['becomeBalance'] = $becomeBalance;
+                $v['becomeBalance'] = $becomeBalance;
+                $data['list'][$k] = $v;
             }
         }
         return $data;
     }
 
+    /**
+     * 零售会员充值流水补全结账单(对照 ghs CustomBalanceChangeClass::fillClearAuditFromRecharge)
+     */
+    public static function fillSettleAuditFromRecharge($row)
+    {
+        if (empty($row) || !is_array($row)) {
+            return $row;
+        }
+        if (intval($row['settleId'] ?? 0) > 0) {
+            return self::normalizeSettleAuditRow($row);
+        }
+        if (intval($row['io'] ?? 0) !== 1) {
+            return $row;
+        }
+        $relateId = intval($row['relateId'] ?? 0);
+        if ($relateId <= 0 || !self::isRechargeCapitalType($row['capitalType'] ?? 0)) {
+            return $row;
+        }
+        $recharge = RechargeClass::getById($relateId, true);
+        if (!empty($recharge)) {
+            $settleId = intval(is_object($recharge) ? ($recharge->settleId ?? 0) : ($recharge['settleId'] ?? 0));
+            if ($settleId > 0) {
+                $row['settleId'] = $settleId;
+                $settle = SettleClass::getById($settleId, true);
+                if (!empty($settle)) {
+                    $row['settleNo'] = is_object($settle) ? ($settle->orderSn ?? '') : ($settle['orderSn'] ?? '');
+                    $row['settleAmount'] = is_object($settle) ? ($settle->actPrice ?? 0) : ($settle['actPrice'] ?? 0);
+                }
+                return self::normalizeSettleAuditRow($row);
+            }
+        }
+        return $row;
+    }
+
+    protected static function normalizeSettleAuditRow(array $row)
+    {
+        $settleId = intval($row['settleId'] ?? 0);
+        if ($settleId > 0 && (empty($row['settleNo']) || bccomp((string)($row['settleAmount'] ?? 0), '0', 2) <= 0)) {
+            $settle = SettleClass::getById($settleId, true);
+            if (!empty($settle)) {
+                $row['settleNo'] = is_object($settle) ? ($settle->orderSn ?? '') : ($settle['orderSn'] ?? '');
+                $row['settleAmount'] = is_object($settle) ? ($settle->actPrice ?? 0) : ($settle['actPrice'] ?? 0);
+            }
+        }
+        return $row;
+    }
+
+    protected static function isRechargeCapitalType($capitalType)
+    {
+        $capitalType = intval($capitalType);
+        return $capitalType === intval(dict::getDict('capitalType', 'xhRecharge', 'id'));
+    }
+
 }

+ 21 - 4
biz-hd/ghs/classes/GhsBalanceChangeClass.php

@@ -2,9 +2,7 @@
 
 namespace bizHd\ghs\classes;
 
-use common\components\stringUtil;
-use common\components\util;
-use Yii;
+use bizGhs\custom\classes\CustomBalanceChangeClass;
 use bizHd\base\classes\BaseClass;
 
 class GhsBalanceChangeClass extends BaseClass
@@ -12,10 +10,29 @@ class GhsBalanceChangeClass extends BaseClass
 
     public static $baseFile = '\bizHd\ghs\models\GhsBalanceChange';
 
+    /**
+     * 花店端查看供货商余额变动(与 ghs 客户余额明细一致:结账单、剩余可用)
+     */
     public static function getChangeList($where)
     {
         $data = self::getList('*', $where, 'addTime DESC,id DESC');
+        if (empty($data['list'])) {
+            return $data;
+        }
+        foreach ($data['list'] as $k => $row) {
+            $row = is_array($row) ? $row : (array)$row;
+            $row = CustomBalanceChangeClass::fillClearAuditFromRecharge($row);
+            $clearAmount = $row['clearAmount'] ?? 0;
+            $amount = $row['amount'] ?? 0;
+            $io = $row['io'] ?? 0;
+            $becomeBalance = 0;
+            if ($io == 1) {
+                $becomeBalance = floatval(bcsub((string)$amount, (string)$clearAmount, 2));
+            }
+            $row['becomeBalance'] = $becomeBalance;
+            $data['list'][$k] = $row;
+        }
         return $data;
     }
 
-}
+}

+ 90 - 3
biz-hd/purchase/services/PurchaseService.php

@@ -14,6 +14,7 @@ use bizGhs\order\classes\OrderClass;
 use bizGhs\order\services\OrderService;
 use bizHd\base\services\BaseService;
 use bizHd\cg\classes\CgRefundClass;
+use bizGhs\clear\classes\OrderCgClearClass;
 use bizHd\purchase\classes\PurchaseClass;
 use bizHd\purchase\classes\PurchaseItemClass;
 use bizHd\purchase\classes\PurchaseSnClass;
@@ -142,7 +143,11 @@ class PurchaseService extends BaseService
         }
         $info = $purchase->attributes;
         $respond = PurchaseClass::groupPurchase([$info], $showButton, $showProduct);
-        return current($respond);
+        $result = current($respond);
+        $purchaseId = intval($result['id'] ?? 0);
+        $saleId = intval($result['saleId'] ?? 0);
+        $result['orderClearList'] = OrderCgClearClass::listPaidClearRecordsForPurchase($purchaseId, $saleId);
+        return $result;
     }
 
     //检查采购单
@@ -181,8 +186,90 @@ class PurchaseService extends BaseService
     public static function getDebtList($where)
     {
         $list = PurchaseClass::getAllList('*', $where, 'addTime DESC');
-        $count = PurchaseClass::getCount($where);
-        return ['list' => $list, 'debtNum' => $count];
+        $synced = [];
+        $clearedSum = '0.00';
+        $remainSum = '0.00';
+        foreach ($list as $val) {
+            $row = is_array($val) ? $val : (array)$val;
+            $row = self::syncPurchaseDebtFromGhsOrder($row);
+            if (intval($row['debt'] ?? 0) != PurchaseClass::DEBT_YES) {
+                continue;
+            }
+            $row = self::enrichPurchaseDebtRow($row);
+            $synced[] = $row;
+            $clearedSum = bcadd($clearedSum, (string)($row['hasPayDebt'] ?? 0), 2);
+            $remainSum = bcadd($remainSum, (string)($row['remainDebtPrice'] ?? 0), 2);
+        }
+        return [
+            'list' => $synced,
+            'debtNum' => count($synced),
+            'clearedAmount' => floatval($clearedSum),
+            'remainDebtAmount' => floatval($remainSum),
+        ];
+    }
+
+    /** 列表展示:与 ghs order/debt-list 一致,按 debtPrice - remainDebtPrice 算已清 */
+    public static function enrichPurchaseDebtRow(array $row)
+    {
+        $debtPrice = bcadd((string)($row['debtPrice'] ?? '0'), '0', 2);
+        $remain = bcadd((string)($row['remainDebtPrice'] ?? '0'), '0', 2);
+        $hasPayDebt = '0.00';
+        if (bccomp($debtPrice, $remain, 2) > 0) {
+            $hasPayDebt = bcsub($debtPrice, $remain, 2);
+        }
+        $row['hasPayDebt'] = floatval($hasPayDebt);
+        return $row;
+    }
+
+    /**
+     * 充值销账后批发订单已更新,采购单可能滞后:按 saleId 对齐 remainDebtPrice / debt。
+     */
+    public static function syncPurchaseDebtFromGhsOrder(array $row)
+    {
+        $saleId = intval($row['saleId'] ?? 0);
+        if ($saleId <= 0) {
+            return $row;
+        }
+        $order = OrderClass::getById($saleId, true);
+        if (empty($order)) {
+            return $row;
+        }
+        $purchaseId = intval($row['id'] ?? 0);
+        if ($purchaseId <= 0) {
+            return $row;
+        }
+        $purchase = PurchaseClass::getById($purchaseId, true);
+        if (empty($purchase)) {
+            return $row;
+        }
+
+        $orderRemain = bcadd((string)($order->remainDebtPrice ?? '0'), '0', 2);
+        $orderDebt = bcadd((string)($order->debtPrice ?? $order->orderPrice ?? '0'), '0', 2);
+        $needSave = false;
+
+        if (bccomp((string)($purchase->remainDebtPrice ?? '0'), $orderRemain, 2) !== 0) {
+            $purchase->remainDebtPrice = bccomp($orderRemain, '0', 2) > 0 ? $orderRemain : '0.00';
+            $needSave = true;
+        }
+        if (intval($order->debt ?? 0) === 0 && intval($purchase->debt) === PurchaseClass::DEBT_YES) {
+            $purchase->debt = PurchaseClass::DEBT_NO;
+            if (!empty($order->clearId)) {
+                $purchase->clearId = $order->clearId;
+            }
+            $needSave = true;
+        }
+        if (bccomp((string)($purchase->debtPrice ?? '0'), '0', 2) <= 0 && bccomp($orderDebt, '0', 2) > 0) {
+            $purchase->debtPrice = $orderDebt;
+            $needSave = true;
+        }
+        if ($needSave) {
+            $purchase->save(false);
+        }
+
+        $row['remainDebtPrice'] = $purchase->remainDebtPrice;
+        $row['debtPrice'] = $purchase->debtPrice;
+        $row['debt'] = $purchase->debt;
+        return $row;
     }
 
     //采购单失效 ssh 2021.5.14