| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573 |
- <?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\dict;
- use common\components\noticeUtil;
- 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
- {
- /** 结账单掺杂历史正余额时的 payWay(dict balancePay=2) */
- const CLEAR_PAY_WAY_BALANCE = 2;
- /** 商家帮充 + 按来款 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);
- // 入账前净余额:决定结账单线上/线下及 payWay
- $balanceBeforeRecharge = bcadd((string)($custom->balance ?? '0'), '0', 2);
- $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, [
- 'payWay' => $payWay,
- 'balanceBeforeRecharge' => $balanceBeforeRecharge,
- 'rechargeSource' => 'merchant',
- 'deduct_balance' => false,
- ]);
- if (!empty($customRecharge) && !empty($allocate['clear'])) {
- self::bindRechargeToClear($customRecharge, $respond['ghsRecharge'] ?? null, $allocate, $custom, $ghs);
- }
- return $respond;
- }
- /**
- * 线上支付已入账后的 FIFO 销账(CustomRechargeClass::thirdPay)。
- * $balanceBeforeRecharge 为本次入账前客户净余额,用于 xhClear.onlinePay 判定。
- */
- public static function onlinePayFifoClear($custom, $ghs, $shop, $amount, $customRecharge, $ghsRecharge = null, $balanceBeforeRecharge = null)
- {
- $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 ?? '') : '';
- $allocate = self::fifoSettleWithPool($pair, $shop, null, $pool, [
- 'payWay' => $payWay,
- 'thirdNo' => $thirdNo,
- 'balanceBeforeRecharge' => $balanceBeforeRecharge,
- 'rechargeSource' => 'online',
- '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;
- }
- // 商家「线下已转账」确认:微信/支付宝亦为线下,不得被 confirmClear 兜底标为线上
- OrderClearClass::applyNetBalanceClear($clear, $payWay, [
- 'skipCashMoney' => true,
- 'onlinePay' => intval(dict::getDict('onlinePay', 'not')),
- ]);
- $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;
- }
- $balanceBeforeRecharge = self::money($pair['custom']->balance ?? 0);
- $allocate = self::fifoSettleWithPool($pair, $shop, $staff, $pool, [
- 'deduct_balance' => true,
- 'balanceBeforeRecharge' => $balanceBeforeRecharge,
- 'rechargeSource' => 'merchant',
- ]);
- 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;
- $actualPayWay = isset($options['payWay']) && is_numeric($options['payWay'])
- ? intval($options['payWay'])
- : intval(dict::getDict('payWay', 'unPay'));
- if (array_key_exists('balanceBeforeRecharge', $options)) {
- $payMeta = self::resolveClearPayMetaForRechargeFifo(
- $options['balanceBeforeRecharge'],
- $planTotal,
- $pool,
- $actualPayWay,
- $options['rechargeSource'] ?? 'merchant',
- !empty($options['deduct_balance'])
- );
- $payWay = $payMeta['payWay'];
- $onlinePay = $payMeta['onlinePay'];
- } elseif (isset($options['onlinePay']) && is_numeric($options['onlinePay'])) {
- $payWay = $actualPayWay;
- $onlinePay = intval($options['onlinePay']);
- } else {
- $payWay = $actualPayWay;
- $onlinePay = self::resolveOnlinePayByPayWay($payWay);
- }
- $clear = OrderClearClass::clearWithAmountMap([
- 'customId' => $customId,
- 'ghsShopAdminId' => $staffId,
- 'ghsShopId' => $shopId,
- 'ghsShopAdminName' => $staffName,
- 'modifyPrice' => $planTotal,
- 'payWay' => $payWay,
- 'onlinePay' => $onlinePay,
- 'complete' => 0,
- 'remark' => '',
- ], $plan, $sjId, $shopId);
- OrderClearClass::applyNetBalanceClear($clear, $payWay, [
- 'skipCashMoney' => true,
- 'onlinePay' => $onlinePay,
- ]);
- $clearId = intval($clear->id ?? 0);
- if ($clearId > 0 && !empty($options['thirdNo'])) {
- $clear = OrderClearClass::getLockById($clearId);
- if (!empty($clear) && self::modelHasAttr($clear, 'thirdNo')) {
- $clear->thirdNo = (string)$options['thirdNo'];
- $clear->save(false, ['thirdNo']);
- }
- }
- $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) {
- noticeUtil::push("客户的余额,和供货商端不一致,客户 ID:" . $custom->id . " 余额:" . $custom->balance . " 供货商 ID:" . $ghsId . ' 余额:' . $ghs->balance, '15280215347');
- 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);
- }
- /** 微信/支付宝视为线上付,其余为线下(非充值销账 FIFO 场景兜底) */
- protected static function resolveOnlinePayByPayWay($payWay)
- {
- $payWay = intval($payWay);
- $wxPay = intval(dict::getDict('payWay', 'wxPay'));
- $aliPay = intval(dict::getDict('payWay', 'alipay'));
- if (in_array($payWay, [$wxPay, $aliPay], true)) {
- return intval(dict::getDict('onlinePay', 'yes'));
- }
- return intval(dict::getDict('onlinePay', 'not'));
- }
- /**
- * 充值 FIFO 结账单 onlinePay / payWay。
- * 入账前无正余额:hd 线上充→线上+实付方式;商家手动充→线下+实付方式。
- * 入账前仍有正余额:一律线下,payWay=2(余额支付)。
- */
- protected static function resolveClearPayMetaForRechargeFifo(
- $balanceBeforeRecharge,
- $settledAmount,
- $poolAmount,
- $actualPayWay,
- $rechargeSource,
- $deductBalance = false
- )
- {
- $notOnline = intval(dict::getDict('onlinePay', 'not'));
- $yesOnline = intval(dict::getDict('onlinePay', 'yes'));
- if ($deductBalance) {
- return [
- 'onlinePay' => $notOnline,
- 'payWay' => self::CLEAR_PAY_WAY_BALANCE,
- ];
- }
- $before = bcadd((string)$balanceBeforeRecharge, '0', 2);
- $settled = bcadd((string)$settledAmount, '0', 2);
- $pool = bcadd((string)$poolAmount, '0', 2);
- $actualPayWay = intval($actualPayWay);
- // 客户仍有正余额:掺杂历史余额,线下 + 余额支付
- if (bccomp($before, '0', 2) > 0) {
- return [
- 'onlinePay' => $notOnline,
- 'payWay' => self::CLEAR_PAY_WAY_BALANCE,
- ];
- }
- if (bccomp($settled, '0', 2) <= 0 || bccomp($settled, $pool, 2) > 0) {
- return [
- 'onlinePay' => $notOnline,
- 'payWay' => $actualPayWay,
- ];
- }
- // 入账前无余额或负余额:hdApp 线上充值 → 线上 + 微信/支付宝等实付
- if ($rechargeSource === 'online') {
- $wxPay = intval(dict::getDict('payWay', 'wxPay'));
- $aliPay = intval(dict::getDict('payWay', 'alipay'));
- if (in_array($actualPayWay, [$wxPay, $aliPay], true)) {
- return [
- 'onlinePay' => $yesOnline,
- 'payWay' => $actualPayWay,
- ];
- }
- return [
- 'onlinePay' => $notOnline,
- 'payWay' => $actualPayWay,
- ];
- }
- // ghsApp 商家手动充值:线下 + 所选支付方式
- return [
- 'onlinePay' => $notOnline,
- 'payWay' => $actualPayWay,
- ];
- }
- }
|