GhsRechargeSettleService.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. <?php
  2. namespace bizGhs\custom\services;
  3. use biz\ghs\classes\GhsClass as BizGhsClass;
  4. use bizGhs\custom\classes\AccountMoneyClass;
  5. use bizGhs\custom\classes\CustomBalanceChangeClass;
  6. use bizGhs\custom\classes\CustomClass;
  7. use bizGhs\ghs\classes\GhsBalanceChangeClass;
  8. use bizGhs\order\classes\OrderClearClass;
  9. use bizGhs\order\classes\OrderClass;
  10. use common\components\dict;
  11. use common\components\noticeUtil;
  12. use common\components\util;
  13. /**
  14. * 批发净余额模型:来款、销挂账、流水(对齐 hd RechargeClass::complete)
  15. *
  16. * 【核心概念】
  17. * - 净 balance:正数=客户有余额,负数=还欠供货商钱(历史挂账已并入 balance)。
  18. * - 资金池 pool:本次可用于销账的金额(线上充值=实付金额;商家帮充=充值额;用余额销账=当前正余额)。
  19. * - FIFO:按订单 id 从小到大,依次减少 remainDebtPrice,池子用完即停。
  20. *
  21. * 【线上充值销账 onlinePayFifoClear 五步】
  22. * 1. lockAccountPair — 加锁客户/供货商,校验两端 balance 一致
  23. * 2. voidAwaitPayClearBills — 作废该客户「待付款」的旧结账单,避免重复结
  24. * 3. fifoSettleWithPool — 用本次来款作资金池,FIFO 销挂账并生成 xhClear
  25. * 4. bindRechargeToClear — 把结账单信息回写到充值单、余额变动流水
  26. * 5. 返回 allocate(含 clear、实销金额等),无挂账则 clear 为 null
  27. *
  28. * 其他入口:merchantRechargeWithAutoClear / confirmClearBillWithIncoming / consumePositiveBalanceFifo
  29. */
  30. class GhsRechargeSettleService
  31. {
  32. /** 结账单 payWay=2 表示「余额支付」(掺杂历史正余额销账时用,见 dict balancePay) */
  33. const CLEAR_PAY_WAY_BALANCE = 2;
  34. /** 商家帮充 + 按来款 FIFO 销账(CustomController) */
  35. public static function merchantRechargeWithAutoClear($custom, $amount, $shop, $staff, $payWay, $params = [])
  36. {
  37. $pair = self::lockAccountPair($custom, null);
  38. $custom = $pair['custom'];
  39. $ghs = $pair['ghs'];
  40. $pool = self::money($amount);
  41. self::voidAwaitPayClearBills($custom->id ?? 0);
  42. // 入账前净余额:决定结账单线上/线下及 payWay
  43. $balanceBeforeRecharge = bcadd((string)($custom->balance ?? '0'), '0', 2);
  44. $respond = CustomClass::rechargeBalance($custom, $pool, $shop, $staff, $payWay, $params);
  45. $custom = $respond['custom'] ?? $custom;
  46. $ghs = $respond['ghs'] ?? $ghs;
  47. $customRecharge = $respond['customRecharge'] ?? null;
  48. $allocate = self::fifoSettleWithPool($pair, $shop, $staff, $pool, [
  49. 'payWay' => $payWay,
  50. 'balanceBeforeRecharge' => $balanceBeforeRecharge,
  51. 'rechargeSource' => 'merchant',
  52. 'deduct_balance' => false,
  53. ]);
  54. if (!empty($customRecharge) && !empty($allocate['clear'])) {
  55. self::bindRechargeToClear($customRecharge, $respond['ghsRecharge'] ?? null, $allocate, $custom, $ghs);
  56. }
  57. return $respond;
  58. }
  59. /**
  60. * 线上支付回调入账后的自动销账(CustomRechargeClass::thirdPay 调用)。
  61. *
  62. * 场景:花店 App 向供货商线上充值,钱已加到 balance;若客户还有欠款订单,用本次来款按 FIFO 销账。
  63. * 注意:入账(balance += amount)在调用本方法之前已完成,本方法只负责「分配来款销单」,不再扣 balance。
  64. *
  65. * @param object $custom 客户(可未加锁,内部会 lockAccountPair)
  66. * @param object $ghs 供货商
  67. * @param object $shop 收款供货商门店
  68. * @param string|float $amount 本次线上实付金额(= 资金池大小)
  69. * @param object $customRecharge 客户侧充值单 xhCustomRecharge
  70. * @param object|null $ghsRecharge 供货商侧充值单 xhGhsRecharge
  71. * @param string|null $balanceBeforeRecharge 入账前客户净 balance,用于判断结账单标「线上付」还是「余额付」
  72. * @return array 销账结果:clear 结账单、realClearAmount 实销总额、clearId/clearSn 等;无欠款时 clear=null
  73. */
  74. public static function onlinePayFifoClear($custom, $ghs, $shop, $amount, $customRecharge, $ghsRecharge = null, $balanceBeforeRecharge = null)
  75. {
  76. // ① 加锁并校验客户/供货商 balance 一致,同时 ensure 历史挂账已并入净余额
  77. $pair = self::lockAccountPair($custom, $ghs);
  78. // ② 资金池 = 本次充值金额(两位小数)
  79. $pool = self::money($amount);
  80. // ③ 作废该客户未付款的待结账单,防止与本次自动销账冲突
  81. self::voidAwaitPayClearBills($pair['custom']->id ?? 0);
  82. // ④ 从充值单取支付方式、第三方流水号,供结账单标记
  83. $payWay = intval(is_object($customRecharge) ? ($customRecharge->payWay ?? dict::getDict('payWay', 'unPay')) : dict::getDict('payWay', 'unPay'));
  84. $thirdNo = is_object($customRecharge) ? (string)($customRecharge->returnCode ?? '') : '';
  85. // ⑤ 核心:用资金池 FIFO 销挂账,生成 xhClear;deduct_balance=false 表示不再次扣 balance
  86. $allocate = self::fifoSettleWithPool($pair, $shop, null, $pool, [
  87. 'payWay' => $payWay,
  88. 'thirdNo' => $thirdNo,
  89. 'balanceBeforeRecharge' => $balanceBeforeRecharge,
  90. 'rechargeSource' => 'online', // 标识 hdApp 线上充值,影响结账单 onlinePay 判定
  91. 'deduct_balance' => false,
  92. ]);
  93. // ⑥ 若确实销了账,把结账单 id/单号/金额写回充值单与余额流水,便于对账
  94. if (!empty($customRecharge) && !empty($allocate['clear'])) {
  95. self::bindRechargeToClear($customRecharge, $ghsRecharge, $allocate, $pair['custom'], $pair['ghs']);
  96. }
  97. return $allocate;
  98. }
  99. /** 待付结账单 — 客户已转账:来款入账 + 按本单销订单(OrderClearController) */
  100. public static function confirmClearBillWithIncoming($custom, $ghs, $shop, $staff, $clear, $payWay, $customRecharge = null)
  101. {
  102. $pair = self::lockAccountPair($custom, $ghs);
  103. $custom = $pair['custom'];
  104. $ghs = $pair['ghs'];
  105. $pool = self::money($clear->actPrice ?? 0);
  106. if (bccomp($pool, '0', 2) <= 0) {
  107. util::fail('结账单金额有误');
  108. }
  109. $clearId = intval($clear->id ?? 0);
  110. $clearSn = $clear->orderSn ?? '';
  111. $clearAudit = [
  112. 'clearId' => $clearId,
  113. 'clearSn' => $clearSn,
  114. 'clearAmount' => $pool,
  115. ];
  116. $ghsRecharge = null;
  117. if (empty($customRecharge)) {
  118. $respond = CustomClass::rechargeBalance($custom, $pool, $shop, $staff, $payWay, [
  119. 'remark' => $clear->remark ?? '',
  120. 'clearAudit' => $clearAudit,
  121. ]);
  122. $custom = $respond['custom'];
  123. $ghs = $respond['ghs'];
  124. $customRecharge = $respond['customRecharge'] ?? null;
  125. $ghsRecharge = $respond['ghsRecharge'] ?? null;
  126. }
  127. // 商家「线下已转账」确认:微信/支付宝亦为线下,不得被 confirmClear 兜底标为线上
  128. OrderClearClass::applyNetBalanceClear($clear, $payWay, [
  129. 'skipCashMoney' => true,
  130. 'onlinePay' => intval(dict::getDict('onlinePay', 'not')),
  131. ]);
  132. $settled = self::money(OrderClearClass::sumClearAmountByClearId($clearId));
  133. if (bccomp($settled, $pool, 2) > 0) {
  134. $settled = $pool;
  135. }
  136. $clearAudit['clearAmount'] = $settled;
  137. self::refreshDebtFlags($custom, $ghs);
  138. $allocate = [
  139. 'clear' => $clear,
  140. 'realClearAmount' => $settled,
  141. 'clearId' => $clearId,
  142. 'clearSn' => $clearSn,
  143. 'clearAmount' => $settled,
  144. ];
  145. if (!empty($customRecharge)) {
  146. self::bindRechargeToClear($customRecharge, $ghsRecharge, $allocate, $custom, $ghs);
  147. } else {
  148. self::bindClearToLatestIncomeRow($custom, $ghs, $clear, $settled);
  149. }
  150. return [
  151. 'custom' => $custom,
  152. 'ghs' => $ghs,
  153. 'customRecharge' => $customRecharge,
  154. 'allocate' => $allocate,
  155. ];
  156. }
  157. /** 用当前正余额 FIFO 销挂账(原 useBalanceClear) */
  158. public static function consumePositiveBalanceFifo($custom, $ghs, $shop, $staff)
  159. {
  160. $pair = self::lockAccountPair($custom, $ghs);
  161. $pool = self::money($pair['custom']->balance ?? 0);
  162. if (bccomp($pool, '0', 2) <= 0) {
  163. return true;
  164. }
  165. $balanceBeforeRecharge = self::money($pair['custom']->balance ?? 0);
  166. $allocate = self::fifoSettleWithPool($pair, $shop, $staff, $pool, [
  167. 'deduct_balance' => true,
  168. 'balanceBeforeRecharge' => $balanceBeforeRecharge,
  169. 'rechargeSource' => 'merchant',
  170. ]);
  171. return true;
  172. }
  173. /**
  174. * 制定 FIFO 销账计划:不真正写库,只算出「资金池能销哪些订单、各销多少」。
  175. *
  176. * 规则:查 customId 下 debt=1 的订单,按 id 升序;每笔取 min(订单剩余欠款, 池子剩余),直到池子用完。
  177. *
  178. * @param int $customId 客户 id
  179. * @param string|float $poolAmount 可用资金池金额
  180. * @return array<int, array{orderId:int, clearAmount:string, orderSn:string}> 计划列表,空数组表示无挂账或池子为 0
  181. */
  182. public static function planFifoByPool($customId, $poolAmount)
  183. {
  184. $pool = self::money($poolAmount);
  185. if (bccomp($pool, '0', 2) <= 0) {
  186. return [];
  187. }
  188. // 只处理仍标记为欠款(debt=1)的订单,id 小=下单早,先销
  189. $orderList = OrderClass::getAllByCondition(['customId' => $customId, 'debt' => 1], 'id asc', '*', null, true);
  190. if (empty($orderList)) {
  191. return [];
  192. }
  193. $plan = [];
  194. $used = '0.00'; // 已分配进计划的金额
  195. foreach ($orderList as $order) {
  196. $left = bcsub($pool, $used, 2);
  197. if (bccomp($left, '0', 2) <= 0) {
  198. break; // 资金池已分完
  199. }
  200. $remain = self::money($order->remainDebtPrice ?? 0);
  201. if (bccomp($remain, '0', 2) <= 0) {
  202. continue; // 该单已无剩余欠款,跳过
  203. }
  204. // 本单销账额 = min(本单欠款, 池子剩余)
  205. $clearAmount = bccomp($remain, $left, 2) <= 0 ? $remain : $left;
  206. $plan[] = [
  207. 'orderId' => (int)($order->id ?? 0),
  208. 'clearAmount' => $clearAmount,
  209. 'orderSn' => $order->orderSn ?? '',
  210. ];
  211. $used = bcadd($used, $clearAmount, 2);
  212. }
  213. return $plan;
  214. }
  215. /**
  216. * 用资金池执行 FIFO 销账并生成结账单(merchant/online/余额销账共用内核)。
  217. *
  218. * 流程:planFifoByPool → 无计划则直接返回 → 创建 xhClear → applyNetBalanceClear 更新订单欠款 →
  219. * 可选 deduct_balance 从 balance 扣实销额 → refreshDebtFlags 刷新 isDebt。
  220. *
  221. * @param array{custom:object, ghs:object} $pair lockAccountPair 返回的客户/供货商
  222. * @param object $shop 操作门店
  223. * @param object|null $staff 操作员工(线上回调无员工传 null)
  224. * @param string|float $poolAmount 资金池
  225. * @param array $options payWay/thirdNo/balanceBeforeRecharge/rechargeSource/deduct_balance/onlinePay 等
  226. * @return array clear、realClearAmount、clearId、clearSn、custom、ghs
  227. */
  228. protected static function fifoSettleWithPool(array $pair, $shop, $staff, $poolAmount, $options = [])
  229. {
  230. $custom = $pair['custom'];
  231. $ghs = $pair['ghs'];
  232. $customId = $custom->id ?? 0;
  233. $pool = self::money($poolAmount);
  234. $plan = self::planFifoByPool($customId, $pool);
  235. if (empty($plan)) {
  236. // 没有可销订单:只刷新欠款标记,不生成结账单
  237. self::refreshDebtFlags($custom, $ghs);
  238. return self::emptyAllocateResult($custom, $ghs);
  239. }
  240. $planTotal = self::sumPlan($plan);
  241. if (bccomp($planTotal, $pool, 2) > 0) {
  242. util::fail('销账计划超过本次可用金额,请联系技术员');
  243. }
  244. self::voidAwaitPayClearBills($customId);
  245. $staffId = $staff->id ?? 0;
  246. $staffName = $staff->name ?? '';
  247. $sjId = $shop->sjId ?? 0;
  248. $shopId = $shop->id ?? 0;
  249. $actualPayWay = isset($options['payWay']) && is_numeric($options['payWay'])
  250. ? intval($options['payWay'])
  251. : intval(dict::getDict('payWay', 'unPay'));
  252. // 决定结账单上的 onlinePay、payWay(线上纯来款 vs 掺杂历史余额 vs 纯余额销账)
  253. if (array_key_exists('balanceBeforeRecharge', $options)) {
  254. $payMeta = self::resolveClearPayMetaForRechargeFifo(
  255. $options['balanceBeforeRecharge'],
  256. $planTotal,
  257. $pool,
  258. $actualPayWay,
  259. $options['rechargeSource'] ?? 'merchant',
  260. !empty($options['deduct_balance'])
  261. );
  262. $payWay = $payMeta['payWay'];
  263. $onlinePay = $payMeta['onlinePay'];
  264. } elseif (isset($options['onlinePay']) && is_numeric($options['onlinePay'])) {
  265. $payWay = $actualPayWay;
  266. $onlinePay = intval($options['onlinePay']);
  267. } else {
  268. $payWay = $actualPayWay;
  269. $onlinePay = self::resolveOnlinePayByPayWay($payWay);
  270. }
  271. // 按 plan 创建结账单 xhClear,并关联各订单销账明细
  272. $clear = OrderClearClass::clearWithAmountMap([
  273. 'customId' => $customId,
  274. 'ghsShopAdminId' => $staffId,
  275. 'ghsShopId' => $shopId,
  276. 'ghsShopAdminName' => $staffName,
  277. 'modifyPrice' => $planTotal,
  278. 'payWay' => $payWay,
  279. 'onlinePay' => $onlinePay,
  280. 'complete' => 0,
  281. 'remark' => '',
  282. ], $plan, $sjId, $shopId);
  283. // 更新订单 remainDebtPrice、净 balance 等(充值链路 skipCashMoney,不再动门店现金)
  284. OrderClearClass::applyNetBalanceClear($clear, $payWay, [
  285. 'skipCashMoney' => true,
  286. 'onlinePay' => $onlinePay,
  287. ]);
  288. // 线上支付:把拉卡拉/微信流水号写入结账单 thirdNo
  289. $clearId = intval($clear->id ?? 0);
  290. if ($clearId > 0 && !empty($options['thirdNo'])) {
  291. $clear = OrderClearClass::getLockById($clearId);
  292. if (!empty($clear) && self::modelHasAttr($clear, 'thirdNo')) {
  293. $clear->thirdNo = (string)$options['thirdNo'];
  294. $clear->save(false, ['thirdNo']);
  295. }
  296. }
  297. // 校验:库内实销合计必须与计划一致,且不能超过资金池
  298. $settled = self::money(OrderClearClass::sumClearAmountByClearId($clear->id ?? 0));
  299. if (bccomp($settled, $planTotal, 2) !== 0) {
  300. util::fail('销账结果与计划不一致,请检查订单待结或联系技术员');
  301. }
  302. if (bccomp($settled, $pool, 2) > 0) {
  303. util::fail('销账金额超过本次可用金额' . floatval($pool) . '元,请检查订单待结或联系技术员');
  304. }
  305. // 「用账面正余额销账」场景:FIFO 完成后要从 balance 扣掉实销额(线上充值场景 deduct_balance=false 不扣)
  306. if (!empty($options['deduct_balance']) && bccomp($settled, '0', 2) > 0) {
  307. $custom = CustomClass::getLockById($custom->id ?? 0);
  308. $ghs = BizGhsClass::getLockById($ghs->id ?? 0);
  309. CustomClass::clearConsumeBalance($settled, $custom, $ghs, $shop, $staff, $clear);
  310. }
  311. self::refreshDebtFlags($custom, $ghs);
  312. return [
  313. 'clear' => $clear,
  314. 'realClearAmount' => $settled,
  315. 'clearId' => $clear->id ?? 0,
  316. 'clearSn' => $clear->orderSn ?? '',
  317. 'clearAmount' => $settled,
  318. 'custom' => $custom,
  319. 'ghs' => $ghs,
  320. ];
  321. }
  322. /**
  323. * 加锁客户与供货商账户,并校验两端 balance 必须相等。
  324. *
  325. * 副作用:ensureCustomMoneyReady / ensureGhsMoneyReady 可能把历史挂账并入净 balance(幂等)。
  326. *
  327. * @param object|array $custom 客户或含 id 的数组
  328. * @param object|null $ghs 供货商,空则按 custom.ghsId 查
  329. * @return array{custom: object, ghs: object}
  330. */
  331. public static function lockAccountPair($custom, $ghs = null)
  332. {
  333. if (empty($custom)) {
  334. util::fail('没有客户信息');
  335. }
  336. $customId = is_object($custom) ? ($custom->id ?? 0) : ($custom['id'] ?? 0);
  337. $custom = CustomClass::getLockById($customId);
  338. if (empty($custom)) {
  339. util::fail('没有找到客户');
  340. }
  341. AccountMoneyClass::ensureCustomMoneyReady($custom, true);
  342. $ghsId = $custom->ghsId ?? 0;
  343. if (empty($ghs)) {
  344. $ghs = BizGhsClass::getLockById($ghsId);
  345. }
  346. if (empty($ghs)) {
  347. util::fail('没有找到供货商');
  348. }
  349. AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
  350. if (bccomp(self::money($custom->balance), self::money($ghs->balance), 2) !== 0) {
  351. noticeUtil::push("客户的余额,和供货商端不一致,客户 ID:" . $custom->id . " 余额:" . $custom->balance . " 供货商 ID:" . $ghsId . ' 余额:' . $ghs->balance, '15280215347');
  352. util::fail('余额有问题,请联系管理员');
  353. }
  354. return ['custom' => $custom, 'ghs' => $ghs];
  355. }
  356. /**
  357. * 作废客户名下「待付款」状态的结账单。
  358. * 为什么:新来款/新销账前清掉旧草稿单,避免客户重复确认或数据打架。
  359. */
  360. protected static function voidAwaitPayClearBills($customId)
  361. {
  362. OrderClearClass::expireAwaitPayClears(intval($customId));
  363. }
  364. /** 汇总 FIFO 计划里各订单 clearAmount 之和 */
  365. protected static function sumPlan(array $plan)
  366. {
  367. $total = '0.00';
  368. foreach ($plan as $row) {
  369. $total = bcadd($total, self::money($row['clearAmount'] ?? 0), 2);
  370. }
  371. return $total;
  372. }
  373. /**
  374. * 按当前净 balance 刷新客户 isDebt、供货商 debt 标记。
  375. * balance < 0 → 仍欠款;balance >= 0 → 不欠款。
  376. */
  377. protected static function refreshDebtFlags($custom, $ghs)
  378. {
  379. $bal = self::money($custom->balance ?? 0);
  380. $custom->isDebt = bccomp($bal, '0', 2) < 0 ? CustomClass::IS_DEBT_YES : CustomClass::IS_DEBT_NO;
  381. $custom->save(false, ['isDebt']);
  382. if (!empty($ghs)) {
  383. $ghs->debt = bccomp($bal, '0', 2) < 0 ? 2 : 1;
  384. $ghs->save(false, ['debt']);
  385. }
  386. }
  387. /** 无挂账可销时的空结果,clear=null、金额为 0 */
  388. protected static function emptyAllocateResult($custom, $ghs)
  389. {
  390. return [
  391. 'clear' => null,
  392. 'realClearAmount' => '0.00',
  393. 'clearId' => 0,
  394. 'clearSn' => '',
  395. 'clearAmount' => '0.00',
  396. 'custom' => $custom,
  397. 'ghs' => $ghs,
  398. ];
  399. }
  400. /**
  401. * 充值销账成功后,把结账单信息回写到充值单 + 余额变动流水。
  402. *
  403. * 写哪些表:
  404. * - xhCustomRecharge / xhGhsRecharge:clearId、clearSn、clearAmount、balance
  405. * - xhCustomBalanceChange / xhGhsBalanceChange:同 relateId 的入账流水补 clear 字段
  406. */
  407. protected static function bindRechargeToClear($customRecharge, $ghsRecharge, $allocate, $custom = null, $ghs = null)
  408. {
  409. $clearId = $allocate['clearId'] ?? 0;
  410. $clearSn = $allocate['clearSn'] ?? '';
  411. $clearAmount = $allocate['clearAmount'] ?? '0.00';
  412. $finalCustomBalance = is_object($custom) ? ($custom->balance ?? null) : null;
  413. $finalGhsBalance = is_object($ghs) ? ($ghs->balance ?? null) : null;
  414. // 充值单上记录「这笔钱销了哪张结账单、销了多少」
  415. if (!empty($customRecharge) && self::modelHasAttr($customRecharge, 'clearId')) {
  416. $customRecharge->clearId = $clearId;
  417. $customRecharge->clearSn = $clearSn;
  418. $customRecharge->clearAmount = $clearAmount;
  419. if ($finalCustomBalance !== null) {
  420. $customRecharge->balance = $finalCustomBalance;
  421. }
  422. $customRecharge->save(false);
  423. }
  424. $relateId = $customRecharge->id ?? 0;
  425. $customId = is_object($custom) ? intval($custom->id ?? 0) : 0;
  426. if ($customId <= 0 && is_object($customRecharge)) {
  427. $customId = intval($customRecharge->customId ?? 0);
  428. }
  429. // 客户侧余额流水补结账单关联
  430. CustomBalanceChangeClass::patchClearAuditByRechargeId($customId, $relateId, $clearId, $clearSn, $clearAmount, $finalCustomBalance);
  431. self::patchBalanceChangeRow(CustomBalanceChangeClass::class, $relateId, $clearId, $clearSn, $clearAmount, [
  432. 'customId' => $customId,
  433. ], $finalCustomBalance);
  434. if (!empty($ghsRecharge)) {
  435. $gRelateId = intval($ghsRecharge->id ?? 0);
  436. CustomBalanceChangeClass::patchClearAuditByGhsRechargeId(
  437. $gRelateId,
  438. $clearId,
  439. $clearSn,
  440. $clearAmount,
  441. $finalGhsBalance
  442. );
  443. self::patchBalanceChangeRow(GhsBalanceChangeClass::class, $gRelateId, $clearId, $clearSn, $clearAmount, [], $finalGhsBalance);
  444. $balanceGhsId = intval($ghsRecharge->ghsId ?? 0);
  445. if ($balanceGhsId > 0) {
  446. // 供货商侧可能有多条同金额入账流水,按 ghsId+amount 再 patch 一条
  447. self::patchBalanceChangeRow(GhsBalanceChangeClass::class, 0, $clearId, $clearSn, $clearAmount, [
  448. 'ghsId' => $balanceGhsId,
  449. 'amount' => self::money($ghsRecharge->amount ?? 0),
  450. ], $finalGhsBalance);
  451. }
  452. if ($finalGhsBalance !== null) {
  453. $ghsRecharge->balance = $finalGhsBalance;
  454. $ghsRecharge->save(false, ['balance']);
  455. }
  456. }
  457. }
  458. /**
  459. * 无充值单、仅确认结账单时:把 clear 信息 patch 到最近一条入账流水(客户+供货商各一条)。
  460. */
  461. protected static function bindClearToLatestIncomeRow($custom, $ghs, $clear, $clearAmount)
  462. {
  463. $clearId = $clear->id ?? 0;
  464. $clearSn = $clear->orderSn ?? '';
  465. self::patchBalanceChangeRow(CustomBalanceChangeClass::class, 0, $clearId, $clearSn, $clearAmount, [
  466. 'customId' => $custom->id ?? 0,
  467. 'io' => 1,
  468. ]);
  469. self::patchBalanceChangeRow(GhsBalanceChangeClass::class, 0, $clearId, $clearSn, $clearAmount, [
  470. 'ghsId' => $ghs->id ?? 0,
  471. 'io' => 1,
  472. ]);
  473. }
  474. /**
  475. * 找最近一条匹配的余额变动流水,补上 clearId/clearSn/clearAmount。
  476. *
  477. * @param string $class CustomBalanceChangeClass 或 GhsBalanceChangeClass
  478. * @param int $relateId 充值单 id,0 表示不按 relateId 筛
  479. * @param array $extraWhere 额外条件如 customId、ghsId、amount
  480. */
  481. protected static function patchBalanceChangeRow($class, $relateId, $clearId, $clearSn, $clearAmount, $extraWhere = [], $finalBalance = null)
  482. {
  483. $where = array_merge(['io' => 1], $extraWhere); // io=1 只 patch 入账流水
  484. if ($relateId > 0) {
  485. $where['relateId'] = $relateId;
  486. }
  487. $row = $class::getByCondition($where, true, null, 'id DESC');
  488. if (empty($row) || !self::modelHasAttr($row, 'clearId')) {
  489. return;
  490. }
  491. $row->clearId = $clearId;
  492. $row->clearSn = $clearSn;
  493. $row->clearAmount = $clearAmount;
  494. if (self::modelHasAttr($row, 'giveAmount')) {
  495. $row->giveAmount = '0.00';
  496. }
  497. if ($finalBalance !== null && self::modelHasAttr($row, 'balance')) {
  498. $row->balance = $finalBalance;
  499. }
  500. $row->save(false);
  501. }
  502. /** 模型是否有某字段(兼容未迁移库) */
  503. protected static function modelHasAttr($row, $attr)
  504. {
  505. return is_object($row) && method_exists($row, 'hasAttribute') && $row->hasAttribute($attr);
  506. }
  507. /** 金额统一保留两位小数的字符串,避免 float 精度问题 */
  508. protected static function money($value)
  509. {
  510. return bcadd((string)$value, '0', 2);
  511. }
  512. /**
  513. * 按支付方式推断结账单是否线上付:仅微信/支付宝为线上。
  514. * 用于非充值 FIFO 场景的兜底,充值链路走 resolveClearPayMetaForRechargeFifo。
  515. */
  516. protected static function resolveOnlinePayByPayWay($payWay)
  517. {
  518. $payWay = intval($payWay);
  519. $wxPay = intval(dict::getDict('payWay', 'wxPay'));
  520. $aliPay = intval(dict::getDict('payWay', 'alipay'));
  521. if (in_array($payWay, [$wxPay, $aliPay], true)) {
  522. return intval(dict::getDict('onlinePay', 'yes'));
  523. }
  524. return intval(dict::getDict('onlinePay', 'not'));
  525. }
  526. /**
  527. * 充值 FIFO 销账时,决定 xhClear 上的 onlinePay 与 payWay。
  528. *
  529. * 业务规则(简化理解):
  530. * | 场景 | onlinePay | payWay |
  531. * |------|-----------|--------|
  532. * | 用账面余额销账 deduct_balance | 线下 | 2 余额 |
  533. * | 充值前已有正余额(掺杂历史余额) | 线下 | 2 余额 |
  534. * | hdApp 线上充,充前无正余额,实销≤来款 | 线上 | 微信/支付宝等 |
  535. * | ghsApp 商家手动充 | 线下 | 所选方式 |
  536. *
  537. * @param string|null $balanceBeforeRecharge 入账前净 balance
  538. * @param string $settledAmount 计划/实销总额
  539. * @param string $poolAmount 本次资金池(来款额)
  540. * @param int $actualPayWay 充值单上的支付方式
  541. * @param string $rechargeSource 'online' | 'merchant'
  542. * @param bool $deductBalance 是否「纯余额销账」
  543. * @return array{onlinePay:int, payWay:int}
  544. */
  545. protected static function resolveClearPayMetaForRechargeFifo(
  546. $balanceBeforeRecharge,
  547. $settledAmount,
  548. $poolAmount,
  549. $actualPayWay,
  550. $rechargeSource,
  551. $deductBalance = false
  552. )
  553. {
  554. $notOnline = intval(dict::getDict('onlinePay', 'not'));
  555. $yesOnline = intval(dict::getDict('onlinePay', 'yes'));
  556. // 纯余额销账:一律线下 + payWay=余额
  557. if ($deductBalance) {
  558. return [
  559. 'onlinePay' => $notOnline,
  560. 'payWay' => self::CLEAR_PAY_WAY_BALANCE,
  561. ];
  562. }
  563. $before = bcadd((string)$balanceBeforeRecharge, '0', 2);
  564. $settled = bcadd((string)$settledAmount, '0', 2);
  565. $pool = bcadd((string)$poolAmount, '0', 2);
  566. $actualPayWay = intval($actualPayWay);
  567. // 充值前客户已有正余额:本次来款+旧余额一起销,结账单标「余额支付」非纯线上
  568. if (bccomp($before, '0', 2) > 0) {
  569. return [
  570. 'onlinePay' => $notOnline,
  571. 'payWay' => self::CLEAR_PAY_WAY_BALANCE,
  572. ];
  573. }
  574. // 无销账或销账超过来款:保守标线下+实付方式
  575. if (bccomp($settled, '0', 2) <= 0 || bccomp($settled, $pool, 2) > 0) {
  576. return [
  577. 'onlinePay' => $notOnline,
  578. 'payWay' => $actualPayWay,
  579. ];
  580. }
  581. // hdApp 线上充值且充前无正余额:微信/支付宝可标线上付
  582. if ($rechargeSource === 'online') {
  583. $wxPay = intval(dict::getDict('payWay', 'wxPay'));
  584. $aliPay = intval(dict::getDict('payWay', 'alipay'));
  585. if (in_array($actualPayWay, [$wxPay, $aliPay], true)) {
  586. return [
  587. 'onlinePay' => $yesOnline,
  588. 'payWay' => $actualPayWay,
  589. ];
  590. }
  591. return [
  592. 'onlinePay' => $notOnline,
  593. 'payWay' => $actualPayWay,
  594. ];
  595. }
  596. // ghsApp 商家柜台帮充:线下 + 所选支付方式
  597. return [
  598. 'onlinePay' => $notOnline,
  599. 'payWay' => $actualPayWay,
  600. ];
  601. }
  602. }