瀏覽代碼

增加中文说明

shish 1 月之前
父節點
當前提交
6cf118d6fc
共有 2 個文件被更改,包括 136 次插入27 次删除
  1. 5 1
      biz-ghs/custom/classes/CustomRechargeClass.php
  2. 131 26
      biz-ghs/custom/services/GhsRechargeSettleService.php

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

@@ -172,7 +172,11 @@ class CustomRechargeClass extends BaseClass
             ShopClass::customRechargeAddBalance($ghsMain, $ghsShop, $customRecharge);
         }
 
-        // 按「本次支付金额」FIFO 销挂账并回写 clearAmount(合并后净余额模型)
+        /**
+         * 线上充值自动销账(详见 GhsRechargeSettleService::onlinePayFifoClear)。
+         * 前置:balance 已 += amount;本步用「本次实付 amount」作资金池,FIFO 销欠款订单。
+         * $balanceBeforeRecharge:入账前 balance,决定结账单标线上付还是余额付。
+         */
         $ghsShop = ShopClass::getById($ghsShopId, true);
         if (!empty($ghsShop)) {
             GhsRechargeSettleService::onlinePayFifoClear(

+ 131 - 26
biz-ghs/custom/services/GhsRechargeSettleService.php

@@ -16,18 +16,23 @@ use common\components\util;
 /**
  * 批发净余额模型:来款、销挂账、流水(对齐 hd RechargeClass::complete)
  *
- * 规则(仅此一套,无旧三件套):
- * 1. 净 balance:正=有余额,负=待结;历史挂账已并入 balance(ensure 幂等)。
- * 2. 来款:balance += 金额,记一条充值流水(amount=来款)。
- * 3. 销单:仅用「本次资金池」按订单 id FIFO 减少 remainDebtPrice,不再扣 balance。
- * 4. 用账面正余额销账:无来款,FIFO 后 balance -= 实销额,可记「结账」流水。
+ * 【核心概念】
+ * - 净 balance:正数=客户有余额,负数=还欠供货商钱(历史挂账已并入 balance)。
+ * - 资金池 pool:本次可用于销账的金额(线上充值=实付金额;商家帮充=充值额;用余额销账=当前正余额)。
+ * - FIFO:按订单 id 从小到大,依次减少 remainDebtPrice,池子用完即停。
  *
- * 入口:merchantRechargeWithAutoClear / onlinePayFifoClear / confirmClearBillWithIncoming /
- *       consumePositiveBalanceFifo
+ * 【线上充值销账 onlinePayFifoClear 五步】
+ * 1. lockAccountPair — 加锁客户/供货商,校验两端 balance 一致
+ * 2. voidAwaitPayClearBills — 作废该客户「待付款」的旧结账单,避免重复结
+ * 3. fifoSettleWithPool — 用本次来款作资金池,FIFO 销挂账并生成 xhClear
+ * 4. bindRechargeToClear — 把结账单信息回写到充值单、余额变动流水
+ * 5. 返回 allocate(含 clear、实销金额等),无挂账则 clear 为 null
+ *
+ * 其他入口:merchantRechargeWithAutoClear / confirmClearBillWithIncoming / consumePositiveBalanceFifo
  */
 class GhsRechargeSettleService
 {
-    /** 结账单掺杂历史正余额时的 payWay(dict balancePay=2) */
+    /** 结账单 payWay=2 表示「余额支付」(掺杂历史正余额销账时用,见 dict balancePay) */
     const CLEAR_PAY_WAY_BALANCE = 2;
 
     /** 商家帮充 + 按来款 FIFO 销账(CustomController) */
@@ -63,27 +68,44 @@ class GhsRechargeSettleService
     }
 
     /**
-     * 线上支付已入账后的 FIFO 销账(CustomRechargeClass::thirdPay)。
-     * $balanceBeforeRecharge 为本次入账前客户净余额,用于 xhClear.onlinePay 判定。
+     * 线上支付回调入账后的自动销账(CustomRechargeClass::thirdPay 调用)。
+     *
+     * 场景:花店 App 向供货商线上充值,钱已加到 balance;若客户还有欠款订单,用本次来款按 FIFO 销账。
+     * 注意:入账(balance += amount)在调用本方法之前已完成,本方法只负责「分配来款销单」,不再扣 balance。
+     *
+     * @param object $custom 客户(可未加锁,内部会 lockAccountPair)
+     * @param object $ghs 供货商
+     * @param object $shop 收款供货商门店
+     * @param string|float $amount 本次线上实付金额(= 资金池大小)
+     * @param object $customRecharge 客户侧充值单 xhCustomRecharge
+     * @param object|null $ghsRecharge 供货商侧充值单 xhGhsRecharge
+     * @param string|null $balanceBeforeRecharge 入账前客户净 balance,用于判断结账单标「线上付」还是「余额付」
+     * @return array 销账结果:clear 结账单、realClearAmount 实销总额、clearId/clearSn 等;无欠款时 clear=null
      */
     public static function onlinePayFifoClear($custom, $ghs, $shop, $amount, $customRecharge, $ghsRecharge = null, $balanceBeforeRecharge = null)
     {
+        // ① 加锁并校验客户/供货商 balance 一致,同时 ensure 历史挂账已并入净余额
         $pair = self::lockAccountPair($custom, $ghs);
+        // ② 资金池 = 本次充值金额(两位小数)
         $pool = self::money($amount);
 
+        // ③ 作废该客户未付款的待结账单,防止与本次自动销账冲突
         self::voidAwaitPayClearBills($pair['custom']->id ?? 0);
 
+        // ④ 从充值单取支付方式、第三方流水号,供结账单标记
         $payWay = intval(is_object($customRecharge) ? ($customRecharge->payWay ?? dict::getDict('payWay', 'unPay')) : dict::getDict('payWay', 'unPay'));
         $thirdNo = is_object($customRecharge) ? (string)($customRecharge->returnCode ?? '') : '';
 
+        // ⑤ 核心:用资金池 FIFO 销挂账,生成 xhClear;deduct_balance=false 表示不再次扣 balance
         $allocate = self::fifoSettleWithPool($pair, $shop, null, $pool, [
             'payWay' => $payWay,
             'thirdNo' => $thirdNo,
             'balanceBeforeRecharge' => $balanceBeforeRecharge,
-            'rechargeSource' => 'online',
+            'rechargeSource' => 'online', // 标识 hdApp 线上充值,影响结账单 onlinePay 判定
             'deduct_balance' => false,
         ]);
 
+        // ⑥ 若确实销了账,把结账单 id/单号/金额写回充值单与余额流水,便于对账
         if (!empty($customRecharge) && !empty($allocate['clear'])) {
             self::bindRechargeToClear($customRecharge, $ghsRecharge, $allocate, $pair['custom'], $pair['ghs']);
         }
@@ -179,9 +201,13 @@ class GhsRechargeSettleService
     }
 
     /**
-     * FIFO 分配计划:按订单 id 升序,资金池用尽即停止。
+     * 制定 FIFO 销账计划:不真正写库,只算出「资金池能销哪些订单、各销多少」。
+     *
+     * 规则:查 customId 下 debt=1 的订单,按 id 升序;每笔取 min(订单剩余欠款, 池子剩余),直到池子用完。
      *
-     * @return array<int, array{orderId:int, clearAmount:string, orderSn:string}>
+     * @param int $customId 客户 id
+     * @param string|float $poolAmount 可用资金池金额
+     * @return array<int, array{orderId:int, clearAmount:string, orderSn:string}> 计划列表,空数组表示无挂账或池子为 0
      */
     public static function planFifoByPool($customId, $poolAmount)
     {
@@ -190,25 +216,27 @@ class GhsRechargeSettleService
             return [];
         }
 
+        // 只处理仍标记为欠款(debt=1)的订单,id 小=下单早,先销
         $orderList = OrderClass::getAllByCondition(['customId' => $customId, 'debt' => 1], 'id asc', '*', null, true);
         if (empty($orderList)) {
             return [];
         }
 
         $plan = [];
-        $used = '0.00';
+        $used = '0.00'; // 已分配进计划的金额
 
         foreach ($orderList as $order) {
             $left = bcsub($pool, $used, 2);
             if (bccomp($left, '0', 2) <= 0) {
-                break;
+                break; // 资金池已分完
             }
 
             $remain = self::money($order->remainDebtPrice ?? 0);
             if (bccomp($remain, '0', 2) <= 0) {
-                continue;
+                continue; // 该单已无剩余欠款,跳过
             }
 
+            // 本单销账额 = min(本单欠款, 池子剩余)
             $clearAmount = bccomp($remain, $left, 2) <= 0 ? $remain : $left;
             $plan[] = [
                 'orderId' => (int)($order->id ?? 0),
@@ -222,7 +250,17 @@ class GhsRechargeSettleService
     }
 
     /**
-     * @param array{custom:object, ghs:object} $pair
+     * 用资金池执行 FIFO 销账并生成结账单(merchant/online/余额销账共用内核)。
+     *
+     * 流程:planFifoByPool → 无计划则直接返回 → 创建 xhClear → applyNetBalanceClear 更新订单欠款 →
+     *       可选 deduct_balance 从 balance 扣实销额 → refreshDebtFlags 刷新 isDebt。
+     *
+     * @param array{custom:object, ghs:object} $pair lockAccountPair 返回的客户/供货商
+     * @param object $shop 操作门店
+     * @param object|null $staff 操作员工(线上回调无员工传 null)
+     * @param string|float $poolAmount 资金池
+     * @param array $options payWay/thirdNo/balanceBeforeRecharge/rechargeSource/deduct_balance/onlinePay 等
+     * @return array clear、realClearAmount、clearId、clearSn、custom、ghs
      */
     protected static function fifoSettleWithPool(array $pair, $shop, $staff, $poolAmount, $options = [])
     {
@@ -233,6 +271,7 @@ class GhsRechargeSettleService
 
         $plan = self::planFifoByPool($customId, $pool);
         if (empty($plan)) {
+            // 没有可销订单:只刷新欠款标记,不生成结账单
             self::refreshDebtFlags($custom, $ghs);
             return self::emptyAllocateResult($custom, $ghs);
         }
@@ -252,6 +291,8 @@ class GhsRechargeSettleService
         $actualPayWay = isset($options['payWay']) && is_numeric($options['payWay'])
             ? intval($options['payWay'])
             : intval(dict::getDict('payWay', 'unPay'));
+
+        // 决定结账单上的 onlinePay、payWay(线上纯来款 vs 掺杂历史余额 vs 纯余额销账)
         if (array_key_exists('balanceBeforeRecharge', $options)) {
             $payMeta = self::resolveClearPayMetaForRechargeFifo(
                 $options['balanceBeforeRecharge'],
@@ -271,6 +312,7 @@ class GhsRechargeSettleService
             $onlinePay = self::resolveOnlinePayByPayWay($payWay);
         }
 
+        // 按 plan 创建结账单 xhClear,并关联各订单销账明细
         $clear = OrderClearClass::clearWithAmountMap([
             'customId' => $customId,
             'ghsShopAdminId' => $staffId,
@@ -283,11 +325,13 @@ class GhsRechargeSettleService
             'remark' => '',
         ], $plan, $sjId, $shopId);
 
+        // 更新订单 remainDebtPrice、净 balance 等(充值链路 skipCashMoney,不再动门店现金)
         OrderClearClass::applyNetBalanceClear($clear, $payWay, [
             'skipCashMoney' => true,
             'onlinePay' => $onlinePay,
         ]);
 
+        // 线上支付:把拉卡拉/微信流水号写入结账单 thirdNo
         $clearId = intval($clear->id ?? 0);
         if ($clearId > 0 && !empty($options['thirdNo'])) {
             $clear = OrderClearClass::getLockById($clearId);
@@ -297,6 +341,7 @@ class GhsRechargeSettleService
             }
         }
 
+        // 校验:库内实销合计必须与计划一致,且不能超过资金池
         $settled = self::money(OrderClearClass::sumClearAmountByClearId($clear->id ?? 0));
         if (bccomp($settled, $planTotal, 2) !== 0) {
             util::fail('销账结果与计划不一致,请检查订单待结或联系技术员');
@@ -305,6 +350,7 @@ class GhsRechargeSettleService
             util::fail('销账金额超过本次可用金额' . floatval($pool) . '元,请检查订单待结或联系技术员');
         }
 
+        // 「用账面正余额销账」场景:FIFO 完成后要从 balance 扣掉实销额(线上充值场景 deduct_balance=false 不扣)
         if (!empty($options['deduct_balance']) && bccomp($settled, '0', 2) > 0) {
             $custom = CustomClass::getLockById($custom->id ?? 0);
             $ghs = BizGhsClass::getLockById($ghs->id ?? 0);
@@ -324,7 +370,15 @@ class GhsRechargeSettleService
         ];
     }
 
-    /** @return array{custom: object, ghs: object} */
+    /**
+     * 加锁客户与供货商账户,并校验两端 balance 必须相等。
+     *
+     * 副作用:ensureCustomMoneyReady / ensureGhsMoneyReady 可能把历史挂账并入净 balance(幂等)。
+     *
+     * @param object|array $custom 客户或含 id 的数组
+     * @param object|null $ghs 供货商,空则按 custom.ghsId 查
+     * @return array{custom: object, ghs: object}
+     */
     public static function lockAccountPair($custom, $ghs = null)
     {
         if (empty($custom)) {
@@ -354,11 +408,16 @@ class GhsRechargeSettleService
         return ['custom' => $custom, 'ghs' => $ghs];
     }
 
+    /**
+     * 作废客户名下「待付款」状态的结账单。
+     * 为什么:新来款/新销账前清掉旧草稿单,避免客户重复确认或数据打架。
+     */
     protected static function voidAwaitPayClearBills($customId)
     {
         OrderClearClass::expireAwaitPayClears(intval($customId));
     }
 
+    /** 汇总 FIFO 计划里各订单 clearAmount 之和 */
     protected static function sumPlan(array $plan)
     {
         $total = '0.00';
@@ -368,6 +427,10 @@ class GhsRechargeSettleService
         return $total;
     }
 
+    /**
+     * 按当前净 balance 刷新客户 isDebt、供货商 debt 标记。
+     * balance < 0 → 仍欠款;balance >= 0 → 不欠款。
+     */
     protected static function refreshDebtFlags($custom, $ghs)
     {
         $bal = self::money($custom->balance ?? 0);
@@ -380,6 +443,7 @@ class GhsRechargeSettleService
         }
     }
 
+    /** 无挂账可销时的空结果,clear=null、金额为 0 */
     protected static function emptyAllocateResult($custom, $ghs)
     {
         return [
@@ -393,6 +457,13 @@ class GhsRechargeSettleService
         ];
     }
 
+    /**
+     * 充值销账成功后,把结账单信息回写到充值单 + 余额变动流水。
+     *
+     * 写哪些表:
+     * - xhCustomRecharge / xhGhsRecharge:clearId、clearSn、clearAmount、balance
+     * - xhCustomBalanceChange / xhGhsBalanceChange:同 relateId 的入账流水补 clear 字段
+     */
     protected static function bindRechargeToClear($customRecharge, $ghsRecharge, $allocate, $custom = null, $ghs = null)
     {
         $clearId = $allocate['clearId'] ?? 0;
@@ -401,6 +472,7 @@ class GhsRechargeSettleService
         $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;
@@ -416,6 +488,7 @@ class GhsRechargeSettleService
         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,
@@ -433,6 +506,7 @@ class GhsRechargeSettleService
             self::patchBalanceChangeRow(GhsBalanceChangeClass::class, $gRelateId, $clearId, $clearSn, $clearAmount, [], $finalGhsBalance);
             $balanceGhsId = intval($ghsRecharge->ghsId ?? 0);
             if ($balanceGhsId > 0) {
+                // 供货商侧可能有多条同金额入账流水,按 ghsId+amount 再 patch 一条
                 self::patchBalanceChangeRow(GhsBalanceChangeClass::class, 0, $clearId, $clearSn, $clearAmount, [
                     'ghsId' => $balanceGhsId,
                     'amount' => self::money($ghsRecharge->amount ?? 0),
@@ -445,6 +519,9 @@ class GhsRechargeSettleService
         }
     }
 
+    /**
+     * 无充值单、仅确认结账单时:把 clear 信息 patch 到最近一条入账流水(客户+供货商各一条)。
+     */
     protected static function bindClearToLatestIncomeRow($custom, $ghs, $clear, $clearAmount)
     {
         $clearId = $clear->id ?? 0;
@@ -459,9 +536,16 @@ class GhsRechargeSettleService
         ]);
     }
 
+    /**
+     * 找最近一条匹配的余额变动流水,补上 clearId/clearSn/clearAmount。
+     *
+     * @param string $class CustomBalanceChangeClass 或 GhsBalanceChangeClass
+     * @param int $relateId 充值单 id,0 表示不按 relateId 筛
+     * @param array $extraWhere 额外条件如 customId、ghsId、amount
+     */
     protected static function patchBalanceChangeRow($class, $relateId, $clearId, $clearSn, $clearAmount, $extraWhere = [], $finalBalance = null)
     {
-        $where = array_merge(['io' => 1], $extraWhere);
+        $where = array_merge(['io' => 1], $extraWhere); // io=1 只 patch 入账流水
         if ($relateId > 0) {
             $where['relateId'] = $relateId;
         }
@@ -481,17 +565,22 @@ class GhsRechargeSettleService
         $row->save(false);
     }
 
+    /** 模型是否有某字段(兼容未迁移库) */
     protected static function modelHasAttr($row, $attr)
     {
         return is_object($row) && method_exists($row, 'hasAttribute') && $row->hasAttribute($attr);
     }
 
+    /** 金额统一保留两位小数的字符串,避免 float 精度问题 */
     protected static function money($value)
     {
         return bcadd((string)$value, '0', 2);
     }
 
-    /** 微信/支付宝视为线上付,其余为线下(非充值销账 FIFO 场景兜底) */
+    /**
+     * 按支付方式推断结账单是否线上付:仅微信/支付宝为线上。
+     * 用于非充值 FIFO 场景的兜底,充值链路走 resolveClearPayMetaForRechargeFifo。
+     */
     protected static function resolveOnlinePayByPayWay($payWay)
     {
         $payWay = intval($payWay);
@@ -504,9 +593,23 @@ class GhsRechargeSettleService
     }
 
     /**
-     * 充值 FIFO 结账单 onlinePay / payWay。
-     * 入账前无正余额:hd 线上充→线上+实付方式;商家手动充→线下+实付方式。
-     * 入账前仍有正余额:一律线下,payWay=2(余额支付)。
+     * 充值 FIFO 销账时,决定 xhClear 上的 onlinePay 与 payWay。
+     *
+     * 业务规则(简化理解):
+     * | 场景 | onlinePay | payWay |
+     * |------|-----------|--------|
+     * | 用账面余额销账 deduct_balance | 线下 | 2 余额 |
+     * | 充值前已有正余额(掺杂历史余额) | 线下 | 2 余额 |
+     * | hdApp 线上充,充前无正余额,实销≤来款 | 线上 | 微信/支付宝等 |
+     * | ghsApp 商家手动充 | 线下 | 所选方式 |
+     *
+     * @param string|null $balanceBeforeRecharge 入账前净 balance
+     * @param string $settledAmount 计划/实销总额
+     * @param string $poolAmount 本次资金池(来款额)
+     * @param int $actualPayWay 充值单上的支付方式
+     * @param string $rechargeSource 'online' | 'merchant'
+     * @param bool $deductBalance 是否「纯余额销账」
+     * @return array{onlinePay:int, payWay:int}
      */
     protected static function resolveClearPayMetaForRechargeFifo(
         $balanceBeforeRecharge,
@@ -520,6 +623,7 @@ class GhsRechargeSettleService
         $notOnline = intval(dict::getDict('onlinePay', 'not'));
         $yesOnline = intval(dict::getDict('onlinePay', 'yes'));
 
+        // 纯余额销账:一律线下 + payWay=余额
         if ($deductBalance) {
             return [
                 'onlinePay' => $notOnline,
@@ -532,7 +636,7 @@ class GhsRechargeSettleService
         $pool = bcadd((string)$poolAmount, '0', 2);
         $actualPayWay = intval($actualPayWay);
 
-        // 客户仍有正余额:掺杂历史余额,线下 + 余额支付
+        // 充值前客户已有正余额:本次来款+旧余额一起销,结账单标「余额支付」非纯线上
         if (bccomp($before, '0', 2) > 0) {
             return [
                 'onlinePay' => $notOnline,
@@ -540,6 +644,7 @@ class GhsRechargeSettleService
             ];
         }
 
+        // 无销账或销账超过来款:保守标线下+实付方式
         if (bccomp($settled, '0', 2) <= 0 || bccomp($settled, $pool, 2) > 0) {
             return [
                 'onlinePay' => $notOnline,
@@ -547,7 +652,7 @@ class GhsRechargeSettleService
             ];
         }
 
-        // 入账前无余额或负余额:hdApp 线上充值 → 线上 + 微信/支付宝等实
+        // hdApp 线上充值且充前无正余额:微信/支付宝可标线上
         if ($rechargeSource === 'online') {
             $wxPay = intval(dict::getDict('payWay', 'wxPay'));
             $aliPay = intval(dict::getDict('payWay', 'alipay'));
@@ -563,7 +668,7 @@ class GhsRechargeSettleService
             ];
         }
 
-        // ghsApp 商家手动充值:线下 + 所选支付方式
+        // ghsApp 商家柜台帮充:线下 + 所选支付方式
         return [
             'onlinePay' => $notOnline,
             'payWay' => $actualPayWay,