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

Merge branch 'clear-cg' into dev

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

+ 4 - 0
app-ghs/controllers/BaseController.php

@@ -31,6 +31,10 @@ class BaseController extends PublicController
         //定义平台类型 1花店 2供货商
         Yii::$app->params['ptStyle'] = dict::getDict('ptStyle', 'ghs');
 
+        // 客户端协议版本:2=旧版「待结+余额」双字段;3=仅净余额 balance(见 AccountMoneyClass)
+        $headers = Yii::$app->getRequest()->getHeaders();
+        Yii::$app->params['clientAppVersion'] = (int)$headers->get('appVersion', 2);
+
         // $headers = Yii::$app->getRequest()->getHeaders();
         // $requestVersion = $headers->get('appVersion');
         // $currentVersion = dict::getDict('appVersion');

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

@@ -780,7 +780,8 @@ class CustomController extends BaseController
         } else if ($type == 2) {
             //欠款客户
             $where['isDebt'] = 1;
-            $sort = '(debtAmount-balance) DESC';
+            // 欠款客户排序:净余额越小(越负)越靠前(原 (debtAmount-balance) DESC)
+            $sort = 'balance ASC';
         } else if ($type == 3) {
             //大客户
             $where['level'] = 2;

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

@@ -29,6 +29,10 @@ class BaseController extends PublicController
         //定义平台类型 1花店 2供货商 3商城
         Yii::$app->params['ptStyle'] = dict::getDict('ptStyle', 'hd');
 
+        // 客户端协议版本:2=旧端金额字段合成;3=净余额(与 ghs 侧 AccountMoneyClass 一致)
+        $headers = Yii::$app->getRequest()->getHeaders();
+        Yii::$app->params['clientAppVersion'] = (int)$headers->get('appVersion', 2);
+
         //token验证 
         $adminId = jwt::getLoginId();
 

+ 8 - 6
biz-ghs/cg/services/CgRefundService.php

@@ -237,10 +237,12 @@ class CgRefundService extends BaseService
         if (empty($ghs)) {
             util::fail('没有找到供货商');
         }
-        $ghs->debtAmount = bcsub($ghs->debtAmount, $refundPrice, 2);
-        $ghs->save();
+        // 采购退款:净 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,
@@ -248,15 +250,15 @@ class CgRefundService extends BaseService
             'ptStyle' => 2,
             'capitalType' => $capitalType,
             'amount' => $refundPrice,
-            'balance' => $ghs->debtAmount,
-            'io' => 0,
+            'balance' => $ghsNewBalance,
+            'io' => 1,
             'staffId' => $refund->shopAdminId,
             'staffName' => $refund->shopAdminName,
             'event' => '采购单 ' . $order->orderSn . ' 申请售后 ' . $orderSn,
             'sjId' => $sjId,
             'shopId' => $shopId,
         ];
-        GhsDebtRecordClass::add($debtData);
+        \bizGhs\ghs\classes\GhsBalanceChangeClass::add($debtData, true);
 
         return $refund;
     }

+ 15 - 12
biz-ghs/clear/classes/ClearClass.php

@@ -179,13 +179,14 @@ class ClearClass extends BaseClass
         if (!$lock) {
             util::fail("系统繁忙,请重试");
         }
-        $currentAmount = bcsub($ghs->debtAmount, $order->prePrice, 2);
-        $ghs->debtAmount = $currentAmount;
+        // 采购单结账:供货商净 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 = $currentAmount > 0 ? 2 : 1;
-        $ghs->save();
+        $ghs->debt = bccomp($ghsNewBalance, '0', 2) < 0 ? 2 : 1;
+        $ghs->save(false, ['balance', 'debtNum', 'debt']);
 
-        //减少欠款变动记录
         $capitalType = dict::getDict('capitalType', 'ghsCgOrderClear', 'id');
         $debtData = [
             'ghsId' => $ghsId,
@@ -195,13 +196,13 @@ class ClearClass extends BaseClass
             'amount' => $order->prePrice,
             'staffId' => $order->customShopAdminId,
             'staffName' => $order->customShopAdminName,
-            'balance' => $ghs->debtAmount,
-            'io' => 0,
+            'balance' => $ghsNewBalance,
+            'io' => 1,
             'event' => '采购单结账 ' . $order->orderSn,
             'sjId' => $order->sjId,
             'shopId' => $order->shopId,
         ];
-        GhsDebtRecordClass::add($debtData);
+        \bizGhs\ghs\classes\GhsBalanceChangeClass::add($debtData, true);
         util::unlock($ghsAddDebtKey);
 
         $shopId = $ghs->shopId;
@@ -233,11 +234,13 @@ class ClearClass extends BaseClass
         if (empty($custom)) {
             util::fail('没有找到客户信息');
         }
-        $remain = bcsub($custom->debtAmount, $order->actPrice, 2);
-        $custom->debtAmount = $remain;
+        // 客户侧同步:净 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 = $remain > 0 ? 1 : 0;
-        $custom->save();
+        $custom->isDebt = bccomp($customNewBalance, '0', 2) < 0 ? 1 : 0;
+        $custom->save(false, ['balance', 'debtNum', 'isDebt']);
         if ($custom->debtNum == 0) {
             $main->mayGatheringNum -= 1;
             $main->save();

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

@@ -0,0 +1,380 @@
+<?php
+
+namespace bizGhs\custom\classes;
+
+use biz\ghs\classes\GhsClass as BizGhsClass;
+use bizGhs\ghs\classes\GhsBalanceChangeClass;
+use common\components\dict;
+use Yii;
+
+/**
+ * 客户/供货商账户金额(表 xhGhsCustom、xhGhs)
+ *
+ * 背景:原 debtAmount=挂账累计、balance=充值余额,现统一为净 balance(正=有余额,负=待结)。
+ * 合并:balance = 原 balance - 原 debtAmount,debtAmount 置 0,balanceMerged=1 表示已合并且勿重复写说明流水。
+ * 写业务前调 ensureCustomMoneyReady / ensureGhsMoneyReady;列表/详情返回前调 formatMoneyForClient。
+ * 旧 App(appVersion<3)由 formatMoneyForClient 拆回待结+余额双字段;新 App 只读净 balance。
+ */
+class AccountMoneyClass
+{
+    /**
+     * 新端协议版本号:请求头 appVersion >= 此值时,接口只返回净 balance,不再拆分待结/余额两列。
+     * 与前端 ghsApp/ghs/hdApp/ghsPad 的 request.js 中 appVersion 保持一致。
+     */
+    const APP_VERSION_NET_BALANCE = 3;
+
+    /**
+     * 【用途】获取当前 HTTP 请求里的客户端版本号,供 formatMoneyForClient 决定返回字段形态。
+     * 【调用时机】一般无需直接调;formatMoneyForClient 内部会自动读取。
+     * 【返回值】整数,默认 2(旧端);3 表示新端净余额协议。
+     */
+    public static function getClientAppVersion()
+    {
+        if (isset(Yii::$app->params['clientAppVersion'])) {
+            return (int)Yii::$app->params['clientAppVersion'];
+        }
+        $headers = Yii::$app->request->headers ?? null;
+        $ver = $headers ? (int)$headers->get('appVersion', 2) : 2;
+        Yii::$app->params['clientAppVersion'] = $ver;
+        return $ver;
+    }
+
+    /**
+     * 【用途】判断数据行是否带有 balanceMerged 字段(是否已执行 SQL 迁移)。
+     * 【调用时机】内部判断幂等标记;未迁移库时靠 debtAmount 是否为 0 代替。
+     * 【参数】$row 数组或 ActiveRecord
+     * 【返回值】true=表结构含 balanceMerged 列
+     */
+    public static function hasMergeFlagField($row)
+    {
+        if (is_array($row)) {
+            return array_key_exists('balanceMerged', $row);
+        }
+        return is_object($row) && method_exists($row, 'hasAttribute') && $row->hasAttribute('balanceMerged');
+    }
+
+    /**
+     * 【用途】判断该客户/供货商账户是否已经完成「挂账并入余额」。
+     * 【调用时机】合并前检查,避免重复合并、重复写说明流水。
+     * 【参数】$row 含 balanceMerged、debtAmount 的数组或模型
+     * 【返回值】true=已合并(或本无挂账)
+     */
+    public static function isMerged($row)
+    {
+        if (!self::hasMergeFlagField($row)) {
+            return bccomp(self::rawDebtAmount($row), '0', 2) == 0;
+        }
+        if (is_array($row)) {
+            return !empty($row['balanceMerged']);
+        }
+        return !empty($row->balanceMerged);
+    }
+
+    /**
+     * 【用途】内部取 debtAmount 原始值,供 isMerged / calcNetBalance 使用。
+     * 【说明】外部业务请用 getNetBalanceFromRow,不要直接读 debtAmount。
+     */
+    protected static function rawDebtAmount($row)
+    {
+        if (is_array($row)) {
+            return $row['debtAmount'] ?? '0.00';
+        }
+        return $row->debtAmount ?? '0.00';
+    }
+
+    /**
+     * 【用途】根据库里的 balance、debtAmount 计算「净账户余额」(合并前后均可用)。
+     * 【规则】未合并:净额 = balance - debtAmount(等价于旧逻辑下「余额减待结」后的真实资金位置);
+     *        已合并:净额 = balance(debtAmount 应为 0)。
+     * 【调用时机】统计总余额、采购列表算实欠、需要与旧 remainDebtAmount 对齐时。
+     * 【参数】$balance 充值余额字段;$debtAmount 挂账累计;$merged 是否已执行过合并
+     * 【返回值】字符串金额,bc 精度 2 位
+     */
+    public static function calcNetBalance($balance, $debtAmount, $merged = false)
+    {
+        $balance = $balance ?? '0.00';
+        $debtAmount = $debtAmount ?? '0.00';
+        if ($merged || bccomp($debtAmount, '0', 2) == 0) {
+            return $balance;
+        }
+        return bcsub($balance, $debtAmount, 2);
+    }
+
+    /**
+     * 【用途】从一条客户或供货商记录中取出净账户余额(自动识别是否已合并)。
+     * 【调用时机】导出汇总、showTotalBalance、任何仍持有双字段模型的读逻辑。
+     * 【参数】$row 客户/供货商数组或 AR
+     * 【返回值】净 balance 字符串
+     */
+    public static function getNetBalanceFromRow($row)
+    {
+        if (is_array($row)) {
+            return self::calcNetBalance(
+                $row['balance'] ?? 0,
+                $row['debtAmount'] ?? 0,
+                self::isMerged($row)
+            );
+        }
+        return self::calcNetBalance(
+            $row->balance ?? 0,
+            $row->debtAmount ?? 0,
+            self::isMerged($row)
+        );
+    }
+
+    /**
+     * 【用途】得到当前账户「待结欠款」数额(正数),用于挂账额度校验、下单前是否超限。
+     * 【规则】净余额 >= 0 时返回 0;净余额 < 0 时返回 |净余额|(替代原 custom.debtAmount 与额度比较)。
+     * 【调用时机】客户自助下单校验 debtLimit;OrderService 欠款超限判断。
+     * 【返回值】待结金额字符串,无欠款为 '0.00'
+     */
+    public static function getOutstandingDebt($row)
+    {
+        $net = self::getNetBalanceFromRow($row);
+        if (bccomp($net, '0', 2) >= 0) {
+            return '0.00';
+        }
+        return bcmul($net, '-1', 2);
+    }
+
+    /**
+     * 【用途】把接口返回给前端的金额字段,按 appVersion 转成旧版或新版形态(多端平滑过渡的核心)。
+     * 【调用时机】CustomClass::groupBaseInfo 组装列表/详情后,对每条记录调用一次。
+     * 【行为】
+     *   - appVersion >= 3:balance=净额,debtAmount=0,remainDebtAmount 辅助展示;
+     *   - appVersion < 3:拆成旧「待结 debtAmount + 余额 balance + remainDebtAmount」,未升级 App 无需改 UI。
+     * 【参数】$row 须含 balance/debtAmount/balanceMerged;$appVersion 可空则自动读请求头
+     * 【返回值】追加 displayBalance 后的同一数组
+     */
+    public static function formatMoneyForClient(array $row, $appVersion = null)
+    {
+        if ($appVersion === null) {
+            $appVersion = self::getClientAppVersion();
+        }
+        $net = self::getNetBalanceFromRow($row);
+        $row['displayBalance'] = $net;
+
+        if ($appVersion >= self::APP_VERSION_NET_BALANCE) {
+            $row['balance'] = $net;
+            $row['debtAmount'] = '0.00';
+            $remain = bccomp($net, '0', 2) < 0 ? bcmul($net, '-1', 2) : '0.00';
+            if (bccomp($net, '0', 2) > 0) {
+                $remain = bcsub('0', $net, 2);
+            }
+            $row['remainDebtAmount'] = $remain;
+            return $row;
+        }
+
+        if (bccomp($net, '0', 2) < 0) {
+            $row['debtAmount'] = bcmul($net, '-1', 2);
+            $row['balance'] = '0.00';
+            $row['remainDebtAmount'] = $row['debtAmount'];
+        } else {
+            $row['debtAmount'] = '0.00';
+            $row['balance'] = $net;
+            $row['remainDebtAmount'] = bccomp($net, '0', 2) > 0 ? bcsub('0', $net, 2) : '0.00';
+        }
+        return $row;
+    }
+
+    /**
+     * 【用途】对 xhGhsCustom(客户)执行一次性「挂账并入余额」:balance -= debtAmount,debtAmount 置 0,打标 balanceMerged。
+     * 【调用时机】一般由 ensureCustomMoneyReady 调用;不要绕过 ensure 直接改 balance。
+     * 【参数】$custom 客户 AR;$writeBalanceChange true 时写一条「账户合并」余额变动(仅首次)
+     * 【返回值】合并后的客户对象
+     */
+    public static function mergeCustomDebtIntoBalanceIfNeeded($custom, $writeBalanceChange = true)
+    {
+        return self::mergeRowDebtIntoBalance($custom, 'custom', $writeBalanceChange);
+    }
+
+    /**
+     * 【用途】对 xhGhs(花店↔供货商关系行)执行与上相同的挂账并入余额。
+     * 【调用时机】ensureCustomMoneyReady 会顺带处理 custom.ghsId;hd 采购-only 场景用 ensureGhsMoneyReady。
+     * 【说明】充值等场景要求 custom 与 ghs 两边净额一致,故成对合并。
+     */
+    public static function mergeGhsDebtIntoBalanceIfNeeded($ghs, $writeBalanceChange = true)
+    {
+        return self::mergeRowDebtIntoBalance($ghs, 'ghs', $writeBalanceChange);
+    }
+
+    /**
+     * 【用途】真正写库的单行合并实现(客户或供货商一行)。
+     * 【说明】已合并则只清理残留 debtAmount;无挂账则只打标;有挂账则改 balance 并可选记流水。
+     * 【注意】须在 getLockById 之后、业务改金额之前调用。
+     */
+    protected static function mergeRowDebtIntoBalance($row, $type, $writeBalanceChange)
+    {
+        if (empty($row)) {
+            return $row;
+        }
+        if (self::isMerged($row)) {
+            if (bccomp($row->debtAmount ?? '0', '0', 2) != 0) {
+                $row->debtAmount = '0.00';
+                $row->save(false, ['debtAmount']);
+            }
+            return $row;
+        }
+
+        $debtAmount = $row->debtAmount ?? '0.00';
+        if (bccomp($debtAmount, '0', 2) == 0) {
+            if (self::hasMergeFlagField($row)) {
+                $row->balanceMerged = 1;
+                $row->save(false, ['balanceMerged']);
+            }
+            return $row;
+        }
+
+        $oldBalance = $row->balance ?? '0.00';
+        $newBalance = bcsub($oldBalance, $debtAmount, 2);
+        $row->balance = $newBalance;
+        $row->debtAmount = '0.00';
+        $saveAttrs = ['balance', 'debtAmount'];
+        if (self::hasMergeFlagField($row)) {
+            $row->balanceMerged = 1;
+            $saveAttrs[] = 'balanceMerged';
+        }
+
+        if ($type === 'custom') {
+            $row->isDebt = bccomp($newBalance, '0', 2) < 0 ? CustomClass::IS_DEBT_YES : CustomClass::IS_DEBT_NO;
+            $saveAttrs[] = 'isDebt';
+            $row->save(false, $saveAttrs);
+        } else {
+            $row->debt = bccomp($newBalance, '0', 2) < 0 ? 2 : 1;
+            $saveAttrs[] = 'debt';
+            $row->save(false, $saveAttrs);
+        }
+
+        if ($writeBalanceChange) {
+            self::addMergeBalanceChange($row, $type, $debtAmount, $newBalance);
+        }
+
+        return $row;
+    }
+
+    /**
+     * 【用途】首次合并时往「余额变动明细」插一条说明,方便财务对账;不写挂账变动表。
+     * 【流水】capitalType=balanceMerge(81),事项如「账户合并:原挂账 X 元并入余额」。
+     */
+    protected static function addMergeBalanceChange($row, $type, $mergedDebt, $newBalance)
+    {
+        $capitalType = dict::getDict('capitalType', 'balanceMerge', 'id');
+        $event = '账户合并:原挂账 ' . floatval($mergedDebt) . ' 元并入余额';
+        if ($type === 'custom') {
+            $cbData = [
+                'customId' => $row->id ?? 0,
+                'customName' => $row->name ?? '',
+                'relateId' => 0,
+                'onlinePay' => 0,
+                'ptStyle' => dict::getDict('ptStyle', 'ghs'),
+                'capitalType' => $capitalType,
+                'amount' => $mergedDebt,
+                'balance' => $newBalance,
+                'staffId' => 0,
+                'staffName' => '',
+                'io' => 0,
+                'side' => 0,
+                'payWay' => 0,
+                'fromType' => dict::getDict('fromType', 'shop'),
+                'event' => $event,
+                'mainId' => $row->ownMainId ?? ($row->mainId ?? 0),
+                'shopId' => $row->ownShopId ?? ($row->shopId ?? 0),
+                'sjId' => $row->sjId ?? 0,
+                'remark' => '系统自动合并挂账与余额',
+            ];
+            CustomBalanceChangeClass::add($cbData, true);
+            return;
+        }
+
+        $gbData = [
+            'ghsId' => $row->id ?? 0,
+            'relateId' => 0,
+            'ptStyle' => dict::getDict('ptStyle', 'ghs'),
+            'capitalType' => $capitalType,
+            'amount' => $mergedDebt,
+            'balance' => $newBalance,
+            'io' => 0,
+            'side' => 0,
+            'onlinePay' => 0,
+            'payWay' => 0,
+            'fromType' => dict::getDict('fromType', 'shop'),
+            'event' => $event,
+            'mainId' => $row->ownMainId ?? ($row->mainId ?? 0),
+            'shopId' => $row->ownShopId ?? ($row->shopId ?? 0),
+            'sjId' => $row->sjId ?? 0,
+            'remark' => '系统自动合并挂账与余额',
+        ];
+        if (class_exists('\bizHd\ghs\classes\GhsBalanceChangeClass')) {
+            \bizHd\ghs\classes\GhsBalanceChangeClass::add($gbData, true);
+        } else {
+            GhsBalanceChangeClass::add($gbData, true);
+        }
+    }
+
+    /**
+     * 【用途】客户侧金额业务的前置入口(最重要,绝大多数写操作应先调此方法)。
+     * 【做什么】行锁客户 → 未合并则合并客户 → 再合并其 ghsId 对应 xhGhs 行。
+     * 【调用时机】开单挂账、结账、退款、充值、余额消费结账、清分改客户金额等。
+     * 【参数】$writeMergeChange false 可关闭合并说明流水(极少用);默认 true
+     * 【返回值】已合并、可安全读写 balance 的客户 AR
+     */
+    public static function ensureCustomMoneyReady($custom, $writeMergeChange = true)
+    {
+        if (empty($custom)) {
+            return $custom;
+        }
+        $customId = is_array($custom) ? ($custom['id'] ?? 0) : ($custom->id ?? 0);
+        if (!is_object($custom) || !self::isMerged($custom)) {
+            $custom = CustomClass::getLockById($customId);
+        }
+        if (empty($custom)) {
+            return $custom;
+        }
+        self::mergeCustomDebtIntoBalanceIfNeeded($custom, $writeMergeChange);
+
+        $ghsId = $custom->ghsId ?? 0;
+        if ($ghsId > 0) {
+            $ghs = BizGhsClass::getLockById($ghsId);
+            if (!empty($ghs)) {
+                self::mergeGhsDebtIntoBalanceIfNeeded($ghs, $writeMergeChange);
+            }
+        }
+        return $custom;
+    }
+
+    /**
+     * 【用途】仅处理 xhGhs 关系行(无 custom 对象时),逻辑同 ensureCustomMoneyReady 的 ghs 部分。
+     * 【调用时机】hd 采购挂账/结账/退款、ghs 采购单改供货商金额、CgRefund 等只拿到 $ghs 的场景。
+     */
+    public static function ensureGhsMoneyReady($ghs, $writeMergeChange = true)
+    {
+        if (empty($ghs)) {
+            return $ghs;
+        }
+        $ghsId = is_array($ghs) ? ($ghs['id'] ?? 0) : ($ghs->id ?? 0);
+        if (!is_object($ghs) || !self::isMerged($ghs)) {
+            $ghs = BizGhsClass::getLockById($ghsId);
+        }
+        if (empty($ghs)) {
+            return $ghs;
+        }
+        return self::mergeGhsDebtIntoBalanceIfNeeded($ghs, $writeMergeChange);
+    }
+
+    /**
+     * 【用途】客户列表分页时,对「仍有 debtAmount 未合并」的单户触发懒合并(读路径补刀)。
+     * 【调用时机】CustomClass::groupBaseInfo 循环里发现 debtAmount>0 且 balanceMerged=0。
+     * 【说明】避免客户长期无交易一直显示旧双字段;合并后会刷新该行再 formatMoneyForClient。
+     * 【参数】$customId 客户主键
+     */
+    public static function mergeCustomRowFromListIfNeeded($customId)
+    {
+        if (empty($customId)) {
+            return;
+        }
+        $custom = CustomClass::getLockById($customId);
+        if (!empty($custom)) {
+            self::ensureCustomMoneyReady($custom, true);
+        }
+    }
+}

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

@@ -195,9 +195,12 @@ class CustomClass extends BaseClass
         BookCustomClass::updateByCondition(['bookSn' => $bookSn, 'customId' => $customId], ['seatSn' => $seatSn]);
     }
 
-    //消费余额 ssh 20240311
+    // 使用账户余额结账(扣减净 balance)ssh 20240311
     public static function clearConsumeBalance($amount, $custom, $ghs, $shop, $staff, $clear)
     {
+        // 先合并历史挂账到 balance,再扣减(只记余额变动)
+        AccountMoneyClass::ensureCustomMoneyReady($custom, true);
+        AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
         $customId = $custom->id ?? 0;
         $customName = $custom->name ?? '';
         $ghsId = $ghs->id ?? 0;
@@ -282,11 +285,14 @@ class CustomClass extends BaseClass
         $clear->save();
     }
 
-    //充值金额,有多个地方要同步修改,请搜索关键词custom_ghs_recharge ssh 20240310
+    // 充值/返充(增加净 balance);有多个地方要同步修改,请搜索关键词 custom_ghs_recharge ssh 20240310
     public static function rechargeBalance($custom, $amount, $shop, $staff, $payWay, $params = [])
     {
+        // 充值前合并;之后对 balance 做 bcadd(只记余额变动)
+        AccountMoneyClass::ensureCustomMoneyReady($custom, true);
         $ghsId = $custom->ghsId ?? 0;
         $ghsInfo = GhsClass::getLockById($ghsId);
+        AccountMoneyClass::ensureGhsMoneyReady($ghsInfo, true);
         if (empty($ghsInfo)) {
             util::fail('没有找到供货商');
         }
@@ -515,10 +521,18 @@ class CustomClass extends BaseClass
             $level = $val['level'] ?? 1;
             $levelName = $levelMap[$level] ?? '';
             $list[$key]['levelName'] = $levelName;
-            $debtAmount = $val['debtAmount'] ?? 0;
-            $balance = $val['balance'] ?? 0;
-            $remainDebtAmount = bcsub($debtAmount, $balance, 2);
-            $list[$key]['remainDebtAmount'] = $remainDebtAmount;
+            // 列表读:未合并的挂账先懒合并,再按 appVersion 格式化金额字段
+            $customId = $val['id'] ?? 0;
+            if (!empty($customId) && bccomp($val['debtAmount'] ?? '0', '0', 2) != 0 && empty($val['balanceMerged'])) {
+                AccountMoneyClass::mergeCustomRowFromListIfNeeded($customId);
+                $fresh = self::getById($customId);
+                if (!empty($fresh)) {
+                    $val = is_array($fresh) ? $fresh : $fresh->getAttributes();
+                    $list[$key] = array_merge($list[$key], $val);
+                }
+            }
+            // 按 appVersion 输出净 balance 或旧版待结+余额字段
+            $list[$key] = AccountMoneyClass::formatMoneyForClient($list[$key]);
 
             //前端使用,走播,预订客户添加新客户
             $list[$key]['zbNum'] = '';
@@ -591,28 +605,35 @@ class CustomClass extends BaseClass
         }
     }
 
-    //客户采购欠款金额增加 ssh 20220306
+    /**
+     * 挂账开单:增加客户待结(改造后 balance 减少,只记余额变动,不再记挂账变动)
+     * ssh 20220306
+     */
     public static function cgDebtAmountAdd($custom, $order, $bookOrder = false)
     {
+        // 挂账开单:合并后 balance 减少,不再写 CustomDebtChange
+        AccountMoneyClass::ensureCustomMoneyReady($custom, true);
         $amount = $order->remainDebtPrice ?? 0;
+        if (bccomp($amount, '0', 2) <= 0) {
+            return;
+        }
+        $beforeBalance = $custom->balance ?? '0.00';
         $custom->isDebt = CustomClass::IS_DEBT_YES;
         $debtLimit = $custom->debtLimit ?? 0;
         if (!$bookOrder) {
             if (isset($order->shopAdminId) && $order->shopAdminId == 0) {
-                //客户自己下单要受额度限制
-                if ($custom->debtAmount > $debtLimit) {
-                    //虚拟供货商和客户关系不受影响
+                $outstanding = AccountMoneyClass::getOutstandingDebt($custom);
+                if (bccomp($outstanding, $debtLimit, 2) > 0) {
                     if ($custom->live == 1) {
-                        util::fail("已欠款{$custom->debtAmount},请先结账哦");
+                        util::fail("已欠款{$outstanding},请先结账哦");
                     }
                 }
             }
         }
-        $balance = bcadd($custom->debtAmount, $amount, 2);
+        $newBalance = bcsub($beforeBalance, $amount, 2);
 
         $mustNotice = false;
         if (getenv('YII_ENV') == 'production') {
-            //王品牛排 黄先生
             if ($custom->id == 11726) {
                 $mustNotice = true;
             }
@@ -624,12 +645,12 @@ class CustomClass extends BaseClass
 
         if (getenv('YII_ENV', 'local') == 'production') {
             if ($mustNotice) {
-                noticeUtil::push("【开单动作 | 客户】变化前欠款:{$custom->debtAmount} 变化金额:{$amount} 变化后欠款:{$balance}", '15280215347');
+                noticeUtil::push("【开单动作 | 客户】变化前余额:{$beforeBalance} 变化金额:{$amount} 变化后余额:{$newBalance}", '15280215347');
             }
         }
 
-        $custom->debtAmount = $balance;
-        $custom->save();
+        $custom->balance = $newBalance;
+        $custom->save(false, ['balance', 'isDebt']);
 
         $relateId = $order->id ?? 0;
         $customId = $custom->id ?? 0;
@@ -648,15 +669,20 @@ class CustomClass extends BaseClass
             'ptStyle' => dict::getDict('ptStyle', 'ghs'),
             'capitalType' => $capitalType,
             'amount' => $amount,
-            'balance' => $balance,
-            'io' => 1,
+            'balance' => $newBalance,
+            'io' => 0,
             'payWay' => $order->payWay,
             'event' => $event,
             'sjId' => $order->sjId,
             'shopId' => $order->shopId,
             'mainId' => $order->mainId,
+            'staffId' => 0,
+            'staffName' => '',
+            'side' => 0,
+            'fromType' => dict::getDict('fromType', 'shop'),
+            'remark' => '',
         ];
-        CustomDebtChangeClass::addChange($change);
+        CustomBalanceChangeClass::add($change, true);
 
         $mainId = $order->mainId ?? 0;
         $main = MainClass::getLockById($mainId);
@@ -682,18 +708,23 @@ class CustomClass extends BaseClass
 
     }
 
-    //客户结账欠款金额减少 ssh 20220306
+    /**
+     * 结账收回挂账:balance 增加(原 debtAmount 减少逻辑)
+     * ssh 20220306
+     */
     public static function clearDebtAmountReduce($custom, $ghs, $amount, $clear)
     {
-        $debtAmount = $custom->debtAmount ?? 0.00;
-        $remain = bcsub($debtAmount, $amount, 2);
-        if ($remain < 0) {
-            noticeUtil::push("客户欠款总金额:{$custom->debtAmount} 本次结账金额:{$amount} 客户ID:{$custom->id} 【里外金额有问题】", '15280215347');
+        // 结账收回挂账:balance 增加,只记余额变动
+        AccountMoneyClass::ensureCustomMoneyReady($custom, true);
+        $beforeBalance = $custom->balance ?? '0.00';
+        $newBalance = bcadd($beforeBalance, $amount, 2);
+        if (bccomp($beforeBalance, '0', 2) < 0 && bccomp($newBalance, '0', 2) > 0) {
+            noticeUtil::push("客户账户余额:{$beforeBalance} 本次结账金额:{$amount} 客户ID:{$custom->id} 【里外金额有问题】", '15280215347');
             util::fail('收款失败,结账后欠款金额会出现负数');
         }
-        $custom->isDebt = $remain <= 0 ? 0 : 1;
-        $custom->debtAmount = $remain;
-        $custom->save();
+        $custom->isDebt = bccomp($newBalance, '0', 2) < 0 ? CustomClass::IS_DEBT_YES : CustomClass::IS_DEBT_NO;
+        $custom->balance = $newBalance;
+        $custom->save(false, ['balance', 'isDebt']);
         $relateId = $clear->id ?? 0;
         $customId = $custom->id ?? 0;
         $customName = $custom->name ?? '';
@@ -707,15 +738,20 @@ class CustomClass extends BaseClass
             'ptStyle' => dict::getDict('ptStyle', 'ghs'),
             'capitalType' => $capitalType,
             'amount' => $amount,
-            'balance' => $remain,
-            'io' => 0,
+            'balance' => $newBalance,
+            'io' => 1,
             'payWay' => 0,
             'event' => "结账,单号:{$orderSn}",
             'sjId' => $ghs->sjId,
             'shopId' => $ghs->shopId,
             'mainId' => $ghs->mainId,
+            'staffId' => 0,
+            'staffName' => '',
+            'side' => 0,
+            'fromType' => dict::getDict('fromType', 'shop'),
+            'remark' => '',
         ];
-        CustomDebtChangeClass::addChange($change);
+        CustomBalanceChangeClass::add($change, true);
 
         $mainId = $ghs->mainId ?? 0;
         $main = MainClass::getLockById($mainId);
@@ -734,13 +770,22 @@ class CustomClass extends BaseClass
 
     }
 
-    //客户退款欠款金额减少 ssh 20220306
+    /**
+     * 售后退款:减少待结(balance 增加),只记余额变动
+     * ssh 20220306
+     */
     public static function refundDebtAmountReduce($custom, $refund, $order)
     {
+        // 退款减待结:balance 增加,只记余额变动
+        AccountMoneyClass::ensureCustomMoneyReady($custom, true);
         $refundPrice = $refund->refundPrice ?? 0;
-        $balance = bcsub($custom->debtAmount, $refundPrice, 2);
-        $custom->debtAmount = $balance;
-        $custom->save();
+        if (bccomp($refundPrice, '0', 2) <= 0) {
+            return;
+        }
+        $newBalance = bcadd($custom->balance ?? '0.00', $refundPrice, 2);
+        $custom->balance = $newBalance;
+        $custom->isDebt = bccomp($newBalance, '0', 2) < 0 ? CustomClass::IS_DEBT_YES : CustomClass::IS_DEBT_NO;
+        $custom->save(false, ['balance', 'isDebt']);
 
         $relateId = $refund->id ?? 0;
         $customId = $custom->id ?? 0;
@@ -755,16 +800,21 @@ class CustomClass extends BaseClass
             'ptStyle' => dict::getDict('ptStyle', 'ghs'),
             'capitalType' => $capitalType,
             'amount' => $refundPrice,
-            'balance' => $balance,
-            'io' => 0,
+            'balance' => $newBalance,
+            'io' => 1,
             'payWay' => 0,
             'event' => "订单号:{$saleOrderSn} 退款",
             'sjId' => $refund->sjId,
             'shopId' => $refund->shopId,
             'mainId' => $order->mainId,
             'payTime' => $payTime,
+            'staffId' => 0,
+            'staffName' => '',
+            'side' => 0,
+            'fromType' => dict::getDict('fromType', 'shop'),
+            'remark' => '',
         ];
-        CustomDebtChangeClass::addChange($change);
+        CustomBalanceChangeClass::add($change, true);
 
         $mainId = $order->mainId ?? 0;
         $main = MainClass::getLockById($mainId);
@@ -905,9 +955,7 @@ class CustomClass extends BaseClass
             $custom = CustomClass::getAllByCondition(['ownShopId' => $pfShopId], null, '*', null, true);
             if (!empty($custom)) {
                 foreach ($custom as $c) {
-                    $balance = $c->balance ?? 0;
-                    $debtAmount = $c->debtAmount ?? 0;
-                    $new = bcsub($balance, $debtAmount, 2);
+                    $new = AccountMoneyClass::getNetBalanceFromRow($c);
                     $pfAmount = bcadd($pfAmount, $new, 2);
                 }
             }

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

@@ -708,16 +708,17 @@ class PurchaseOrderClass extends BaseClass
             util::fail('没有找到供货商');
         }
 
-        //锁定当前增加资产的运作,有多处,请搜索关键词 ghsAddDebtAction
+        // 采购挂账:合并后 balance 减少,记 GhsBalanceChange 而非 GhsDebtRecord
+        \bizGhs\custom\classes\AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
         $ghs->debt = GhsClass::DEBT_YES;
-        $ghs->debtAmount = bcadd($ghs->debtAmount, $currentPrice, 2);
+        $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();
+        $ghs->save(false, ['debt', 'balance', 'debtNum', 'expendAmount', 'expendNum']);
 
-        //增加欠款变动记录
         $capitalType = dict::getDict('capitalType', 'ghsPurchase', 'id');
         $debtData = [
             'ghsId' => $ghsId,
@@ -725,15 +726,15 @@ class PurchaseOrderClass extends BaseClass
             'ptStyle' => 2,
             'capitalType' => $capitalType,
             'amount' => $currentPrice,
-            'balance' => $ghs->debtAmount,
+            'balance' => $ghsNewBalance,
             'staffId' => $cg->shopAdminId,
             'staffName' => $cg->shopAdminName,
-            'io' => 1,
+            'io' => 0,
             'event' => '新增采购 ' . $orderSn,
             'sjId' => $sjId,
             'shopId' => $shopId,
         ];
-        GhsDebtRecordClass::add($debtData);
+        \bizGhs\ghs\classes\GhsBalanceChangeClass::add($debtData, true);
 
         //采购统计
         $cgNum = $order->itemNum ?? 0;
@@ -761,8 +762,9 @@ class PurchaseOrderClass extends BaseClass
         if (empty($custom)) {
             util::fail('没有找到客户信息');
         }
+        \bizGhs\custom\classes\AccountMoneyClass::ensureCustomMoneyReady($custom, true);
         $custom->isDebt = CustomClass::IS_DEBT_YES;
-        $custom->debtAmount = bcadd($custom->debtAmount, $currentPrice, 2);
+        $custom->balance = bcsub($custom->balance ?? '0.00', $currentPrice, 2); // 客户待结增加=净 balance 减少
         if ($custom->debtNum == 0) {
             $ghsMain->mayGatheringNum += 1;
         }
@@ -1192,13 +1194,14 @@ class PurchaseOrderClass extends BaseClass
                 if (empty($ghs)) {
                     util::fail('没有找到供货商');
                 }
+                \bizGhs\custom\classes\AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
                 $ghs->debt = GhsClass::DEBT_YES;
-                $ghs->debtAmount = bcadd($ghs->debtAmount, $currentPrice, 2);
+                $ghs->balance = bcsub($ghs->balance ?? '0.00', $currentPrice, 2);
 
                 $ghs->debtNum += 1;
                 $ghs->expendAmount = bcadd($ghs->expendAmount, $currentPrice, 2);
                 $ghs->expendNum += 1;
-                $ghs->save();
+                $ghs->save(false, ['debt', 'balance', 'debtNum', 'expendAmount', 'expendNum']);
 
                 //采购统计
                 $cgNum = $order->itemNum ?? 0;
@@ -1226,8 +1229,9 @@ class PurchaseOrderClass extends BaseClass
                 if (empty($custom)) {
                     util::fail('没有找到客户信息');
                 }
+                \bizGhs\custom\classes\AccountMoneyClass::ensureCustomMoneyReady($custom, true);
                 $custom->isDebt = CustomClass::IS_DEBT_YES;
-                $custom->debtAmount = bcadd($custom->debtAmount, $currentPrice, 2);
+                $custom->balance = bcsub($custom->balance ?? '0.00', $currentPrice, 2);
                 if ($custom->debtNum == 0) {
                     $ghsMain->mayGatheringNum += 1;
                 }

+ 4 - 3
biz-ghs/order/services/OrderService.php

@@ -297,10 +297,11 @@ class OrderService extends BaseService
                 if (isset($shop->overAllowDebt) && $shop->overAllowDebt == 0) {
                     $debtLimit = $custom['debtLimit'] ?? 0;
                     $debtLimit = floatval($debtLimit);
-                    $debtAmount = $custom['debtAmount'] ?? 0;
+                    // 待结额度校验:用净余额欠款额,不再读 debtAmount
+                    $outstanding = \bizGhs\custom\classes\AccountMoneyClass::getOutstandingDebt($custom);
                     $currentPrice = $returnOrder->actPrice ?? 0;
-                    $newDebt = bcadd($currentPrice, $debtAmount, 2);
-                    if ($newDebt > $debtLimit) {
+                    $newDebt = bcadd($currentPrice, $outstanding, 2);
+                    if (bccomp($newDebt, $debtLimit, 2) > 0) {
                         util::fail('欠款已超' . $debtLimit . ',请先清账');
                     }
                 }

+ 47 - 27
biz-hd/ghs/classes/GhsClass.php

@@ -2,6 +2,13 @@
 
 namespace bizHd\ghs\classes;
 
+/**
+ * 花店侧 xhGhs 关系账户(与 xhGhsCustom 成对,custom.ghsId 关联)
+ *
+ * 金额与 bizGhs\custom\classes\AccountMoneyClass 一致:写前 ensureGhsMoneyReady;
+ * 挂账=balance 减少,结账/退款=balance 增加;只记 GhsBalanceChange,不再新增 GhsDebtChange(历史仍可查)。
+ */
+use bizGhs\custom\classes\AccountMoneyClass;
 use common\components\dict;
 use common\components\noticeUtil;
 use common\components\util;
@@ -13,14 +20,20 @@ class GhsClass extends BaseClass
 
     public static $baseFile = '\bizHd\ghs\models\Ghs';
 
-    //供货商销售欠款金额增加 ssh 20220306
+    /** 采购挂账开单:balance 减少(原 debtAmount 增加) */
     public static function cgDebtAmountAdd($ghs, $order, $bookOrder = false)
     {
+        // hd 采购挂账:净 balance 减少
+        AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
         $amount = $order->remainDebtPrice ?? 0;
-        $balance = bcadd($ghs->debtAmount, $order->remainDebtPrice, 2);
+        if (bccomp($amount, '0', 2) <= 0) {
+            return;
+        }
+        $beforeBalance = $ghs->balance ?? '0.00';
         $ghs->debt = 2;
-        $ghs->debtAmount = $balance;
-        $ghs->save();
+        $newBalance = bcsub($beforeBalance, $amount, 2);
+        $ghs->balance = $newBalance;
+        $ghs->save(false, ['balance', 'debt']);
 
         $relateId = $order->id ?? 0;
         $orderSn = $order->orderSn ?? '';
@@ -37,27 +50,30 @@ class GhsClass extends BaseClass
             'ptStyle' => dict::getDict('ptStyle', 'hd'),
             'capitalType' => $capitalType,
             'amount' => $amount,
-            'balance' => $balance,
-            'io' => 1,
+            'balance' => $newBalance,
+            'io' => 0,
             'payWay' => $order->payWay,
             'event' => $event,
             'sjId' => $order->sjId ?? 0,
             'shopId' => $order->shopId ?? 0,
         ];
-        GhsDebtChangeClass::addChange($change);
+        GhsBalanceChangeClass::add($change, true);
     }
 
-    //供货商结账欠款金额减少 ssh 20220306
+    /** 采购结账:balance 增加(原 debtAmount 减少) */
     public static function clearDebtAmountReduce($ghs, $custom, $amount, $clear)
     {
-        $remainAmount = bcsub($ghs->debtAmount, $amount, 2);
-        if ($remainAmount < 0) {
-            noticeUtil::push("结账没有成功,欠供货商总金额:{$ghs->debtAmount} 本次结账金额:{$amount} 供货商ID:{$ghs->id}", '15280215347');
+        // hd 采购结账:净 balance 增加
+        AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
+        $beforeBalance = $ghs->balance ?? '0.00';
+        $newBalance = bcadd($beforeBalance, $amount, 2);
+        if (bccomp($beforeBalance, '0', 2) < 0 && bccomp($newBalance, '0', 2) > 0) {
+            noticeUtil::push("结账没有成功,账户余额:{$beforeBalance} 本次结账金额:{$amount} 供货商ID:{$ghs->id}", '15280215347');
             util::fail('结账没有成功');
         }
-        $ghs->debtAmount = $remainAmount;
-        $ghs->debt = $remainAmount > 0 ? 2 : 1;
-        $ghs->save();
+        $ghs->balance = $newBalance;
+        $ghs->debt = bccomp($newBalance, '0', 2) < 0 ? 2 : 1;
+        $ghs->save(false, ['balance', 'debt']);
 
         $relateId = $clear->id ?? 0;
         $ghsId = $ghs->id ?? 0;
@@ -70,26 +86,30 @@ class GhsClass extends BaseClass
             'ptStyle' => dict::getDict('ptStyle', 'hd'),
             'capitalType' => $capitalType,
             'amount' => $amount,
-            'balance' => $remainAmount,
-            'io' => 0,
+            'balance' => $newBalance,
+            'io' => 1,
             'payWay' => 1,
             'event' => "结账,单号:{$orderSn}",
             'sjId' => $custom->sjId ?? 0,
             'shopId' => $custom->shopId ?? 0,
         ];
-        GhsDebtChangeClass::addChange($change);
-
+        GhsBalanceChangeClass::add($change, true);
     }
 
-    //供货商退款欠款金额减少 ssh 20220306
+    /** 采购退款:balance 增加 */
     public static function refundDebtAmountReduce($ghs, $refund, $cg)
     {
+        // hd 采购退款:净 balance 增加
+        AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
         $refundPrice = $refund->refundPrice ?? 0;
-        $currentDebtAmount = bcsub($ghs->debtAmount, $refundPrice, 2);
-        $ghs->debtAmount = $currentDebtAmount;
-        $ghs->save();
+        if (bccomp($refundPrice, '0', 2) <= 0) {
+            return;
+        }
+        $newBalance = bcadd($ghs->balance ?? '0.00', $refundPrice, 2);
+        $ghs->balance = $newBalance;
+        $ghs->save(false, ['balance']);
 
-        $relateId = $order->id ?? 0;
+        $relateId = $refund->id ?? 0;
         $ghsId = $ghs->id ?? 0;
         $capitalType = dict::getDict('capitalType', 'cgRefund', 'id');
         $cgOrderSn = $cg->orderSn ?? '';
@@ -100,14 +120,14 @@ class GhsClass extends BaseClass
             'ptStyle' => dict::getDict('ptStyle', 'hd'),
             'capitalType' => $capitalType,
             'amount' => $refundPrice,
-            'balance' => $currentDebtAmount,
-            'io' => 0,
+            'balance' => $newBalance,
+            'io' => 1,
             'payWay' => 0,
             'event' => "采购单:{$cgOrderSn} 退款",
             'sjId' => $refund->sjId ?? 0,
             'shopId' => $refund->shopId ?? 0,
         ];
-        GhsDebtChangeClass::addChange($change);
+        GhsBalanceChangeClass::add($change, true);
     }
 
     //验证供货商 ssh 20220307
@@ -122,4 +142,4 @@ class GhsClass extends BaseClass
         return true;
     }
 
-}
+}

+ 10 - 3
biz-hd/purchase/classes/PurchaseClass.php

@@ -1372,13 +1372,20 @@ class PurchaseClass extends BaseClass
             $list[$key]['supplierAddress'] = $ghsInfo[$ghsId]['address'] ?? '';
             $list[$key]['debtAmount'] = $ghsInfo[$ghsId]['debtAmount'] ?? '';
 
-            //剩余欠款
+            // 供货商卡片:统一为净余额(与 AccountMoneyClass 一致,appVersion=3 时接口 balance 已是净额)
             $balance = $ghsInfo[$ghsId]['balance'] ?? 0;
             $debtAmount = $ghsInfo[$ghsId]['debtAmount'] ?? 0;
+            $balanceMerged = $ghsInfo[$ghsId]['balanceMerged'] ?? 0;
             $ghsSalt = $ghsInfo[$ghsId]['salt'] ?? '';
-            $remainDebtAmount = bcsub($debtAmount, $balance, 2);
+            $net = \bizGhs\custom\classes\AccountMoneyClass::calcNetBalance($balance, $debtAmount, !empty($balanceMerged));
+            $remainDebtAmount = bccomp($net, '0', 2) < 0 ? bcmul($net, '-1', 2) : '0.00';
+            if (bccomp($net, '0', 2) > 0) {
+                $remainDebtAmount = bcsub('0', $net, 2);
+            }
             $list[$key]['remainDebtAmount'] = floatval($remainDebtAmount);
-            $list[$key]['inMyGhsBalance'] = floatval($balance);
+            $list[$key]['inMyGhsBalance'] = floatval($net);
+            $list[$key]['balance'] = floatval($net);
+            $list[$key]['displayBalance'] = floatval($net);
             $list[$key]['ghsSalt'] = $ghsSalt;
 
             $list[$key]['lat'] = $ghsInfo[$ghsId]['lat'] ?? '';

+ 3 - 0
common/components/dict.php

@@ -421,6 +421,8 @@ class dict
             'system' => ['id' => 78, 'name' => 'system'],
             'scanPay' => ['id' => 79, 'name' => 'scanPay'],
             'scanPayRefund' => ['id' => 80, 'name' => 'scanPayRefund'],
+            // AccountMoneyClass 首次合并挂账入 balance 时写入余额变动的事项类型
+            'balanceMerge' => ['id' => 81, 'name' => 'balanceMerge'],
         ],
         "capitalTypeList" => [//流水类型的对应链接,后台收支明细查看时跳转的链接
             0 => ['link' => '/capital/order-detail', 'name' => '网店', 'orderLink' => '/order/detail', 'id' => 0,],
@@ -473,6 +475,7 @@ class dict
             78 => ['link' => '', 'name' => '系统操作', 'id' => 78,],
             79 => ['link' => '', 'name' => '扫码付款', 'id' => 79,],
             80 => ['link' => '', 'name' => '扫码退款', 'id' => 80,],
+            81 => ['link' => '', 'name' => '账户合并', 'id' => 81,],
         ],
 
         //用户来源

+ 95 - 0
console/controllers/BalanceMergeController.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace console\controllers;
+
+use biz\ghs\classes\GhsClass as BizGhsClass;
+use bizGhs\custom\classes\AccountMoneyClass;
+use bizGhs\custom\classes\CustomClass;
+use yii\console\Controller;
+
+/**
+ * 挂账并入余额 — 控制台补漏(休眠账户批量处理)
+ *
+ * 【用途】处理长期未触发「开单/充值/结账」等入口、仍未 merge 的历史账户。
+ * 【与线上一致】内部调用 AccountMoneyClass::ensureCustomMoneyReady / ensureGhsMoneyReady。
+ *
+ * 用法:php yii balance-merge/run  或  --dryRun=1 仅预览
+ */
+class BalanceMergeController extends Controller
+{
+    /** 1=只打印将处理的 id 与金额,不写库 */
+    public $dryRun = 0;
+
+    public function options($actionID)
+    {
+        return array_merge(parent::options($actionID), ['dryRun']);
+    }
+
+    /**
+     * 【用途】执行或预览补漏:先客户表 xhGhsCustom,再关系表 xhGhs。
+     */
+    public function actionRun()
+    {
+        $dry = (int)$this->dryRun === 1;
+        echo $dry ? "【预览模式】\n" : "【执行合并】\n";
+        $customCount = $this->mergeCustomRows($dry);
+        $ghsCount = $this->mergeGhsRows($dry);
+        echo "完成:客户 {$customCount} 条,供货商关系 {$ghsCount} 条,dryRun=" . ($dry ? '1' : '0') . PHP_EOL;
+    }
+
+    /**
+     * 【用途】扫 xhGhsCustom:debtAmount>0 且(有 balanceMerged 列时)balanceMerged=0,逐条 ensureCustomMoneyReady。
+     */
+    protected function mergeCustomRows($dry)
+    {
+        $count = 0;
+        $where = ['and', ['>', 'debtAmount', 0]];
+        if ($this->columnExists('xhGhsCustom', 'balanceMerged')) {
+            $where = ['and', ['>', 'debtAmount', 0], ['balanceMerged' => 0]];
+        }
+        $list = CustomClass::getAllByCondition($where, null, '*', null, true);
+        foreach ($list as $custom) {
+            if ($dry) {
+                echo "[客户] id={$custom->id} debtAmount={$custom->debtAmount} balance={$custom->balance}" . PHP_EOL;
+                $count++;
+                continue;
+            }
+            AccountMoneyClass::ensureCustomMoneyReady($custom, true);
+            $count++;
+        }
+        return $count;
+    }
+
+    /**
+     * 【用途】扫 xhGhs:条件同上,逐条 ensureGhsMoneyReady(不自动带 custom,休眠 ghs 单独处理)。
+     */
+    protected function mergeGhsRows($dry)
+    {
+        $count = 0;
+        $where = ['and', ['>', 'debtAmount', 0]];
+        if ($this->columnExists('xhGhs', 'balanceMerged')) {
+            $where = ['and', ['>', 'debtAmount', 0], ['balanceMerged' => 0]];
+        }
+        $list = BizGhsClass::getAllByCondition($where, null, '*', null, true);
+        foreach ($list as $ghs) {
+            if ($dry) {
+                echo "[关系] id={$ghs->id} debtAmount={$ghs->debtAmount} balance={$ghs->balance}" . PHP_EOL;
+                $count++;
+                continue;
+            }
+            AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
+            $count++;
+        }
+        return $count;
+    }
+
+    /**
+     * 【用途】判断迁移 SQL 是否已执行,避免 where balanceMerged 报错。
+     */
+    protected function columnExists($table, $column)
+    {
+        $db = \Yii::$app->db;
+        $schema = $db->getTableSchema($table, true);
+        return $schema && isset($schema->columns[$column]);
+    }
+}

+ 30 - 0
scripts/sql/add_balance_merged.sql

@@ -0,0 +1,30 @@
+-- =============================================================================
+-- debtAmount 并入 balance — 加字段 balanceMerged(在线 DDL,尽量避免锁表)
+-- =============================================================================
+-- 执行前确认列不存在(已存在则跳过该表):
+--   SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.COLUMNS
+--   WHERE TABLE_SCHEMA = DATABASE()
+--     AND TABLE_NAME IN ('xhGhsCustom', 'xhGhs')
+--     AND COLUMN_NAME = 'balanceMerged';
+--
+-- 少锁表做法:
+--   · 列加在表末尾(不写 AFTER debtAmount),避免为「插中间列」触发拷表
+--   · NOT NULL DEFAULT 0,无需 UPDATE 回填
+--   · ALGORITHM=INPLACE, LOCK=NONE:允许并发 DML(MySQL 5.7+/8.0 常见)
+--   · 8.0 若支持 INSTANT,可把 INPLACE 换成 INSTANT(变更更快、锁更短)
+--   · 极大表或仍阻塞时,用 pt-online-schema-change / gh-ost 加同名列
+--
+-- 业务合并:bizGhs\custom\classes\AccountMoneyClass
+-- =============================================================================
+
+SET SESSION lock_wait_timeout = 10;
+
+ALTER TABLE `xhGhsCustom`
+    ADD COLUMN `balanceMerged` tinyint(1) NOT NULL DEFAULT 0 COMMENT '挂账已并入余额:0否1是',
+    ALGORITHM = INPLACE,
+    LOCK = NONE;
+
+ALTER TABLE `xhGhs`
+    ADD COLUMN `balanceMerged` tinyint(1) NOT NULL DEFAULT 0 COMMENT '挂账已并入余额:0否1是',
+    ALGORITHM = INPLACE,
+    LOCK = NONE;