diff --git a/addons/shopro/library/split/SplitWalletSettlement.php b/addons/shopro/library/split/SplitWalletSettlement.php index 44ccdab..f26e0ce 100644 --- a/addons/shopro/library/split/SplitWalletSettlement.php +++ b/addons/shopro/library/split/SplitWalletSettlement.php @@ -8,11 +8,15 @@ use addons\shopro\library\regional_agent\Config as RegionalConfig; use addons\shopro\library\regional_agent\Reward as RegionalRewardLibrary; use addons\shopro\model\Order; use addons\shopro\model\commission\Order as CommissionOrderModel; +use addons\shopro\model\commission\Reward as CommissionRewardModel; use addons\shopro\model\regional_agent\Order as RegionalOrderModel; +use addons\shopro\model\regional_agent\Reward as RegionalRewardModel; +use addons\shopro\model\split\Receiver; use addons\shopro\model\split\SplitOrder; +use addons\shopro\model\split\SplitOrderReceiver; /** - * 支付通道分账与钱包佣金互斥:分账成功则不入余额,失败则回退入余额 + * 支付通道分账与钱包佣金:汇付可按人混合(已进件走通道、未进件进钱包);其它通道整单成功则不入余额 */ class SplitWalletSettlement { @@ -74,7 +78,7 @@ class SplitWalletSettlement } /** - * 分账尝试结束后结算佣金(成功=仅记账不入钱包,失败=入钱包) + * 分账尝试结束后结算佣金(汇付成功=按人混合;其它成功=整单不入钱包;失败=整单入钱包) */ public static function finalizeAfterSplitAttempt($orderId, $event, $splitExecutedOk = false) { @@ -83,12 +87,157 @@ class SplitWalletSettlement } if (self::isSplitSuccessful($orderId)) { - self::settleOrderWithoutWallet($orderId, $event); + if (self::shouldSettleHuifuPerReceiver($orderId)) { + self::settleOrderHybridHuifuGatewayAndWallet($orderId, $event); + } else { + self::settleOrderWithoutWallet($orderId, $event); + } } else { self::settleOrderToWallet($orderId, $event); } } + /** + * 汇付延时分账:仅出现在分账明细中的推广员/区域公司视为通道已分,其余佣金进钱包 + */ + public static function settleOrderHybridHuifuGatewayAndWallet($orderId, $event) + { + $gateway = self::collectGatewaySplitUserIds($orderId); + + $commissionLib = new CommissionRewardLibrary(); + $commissionOrders = CommissionOrderModel::where('order_id', $orderId) + ->where('commission_reward_status', CommissionRewardLibrary::COMMISSION_REWARD_STATUS_WAITING) + ->select(); + foreach ($commissionOrders as $commissionOrder) { + if ($commissionOrder['commission_event'] !== $event && $event !== 'admin') { + continue; + } + $rewards = CommissionRewardModel::where('commission_order_id', $commissionOrder['id']) + ->where('status', CommissionRewardLibrary::COMMISSION_REWARD_STATUS_WAITING) + ->select(); + foreach ($rewards as $reward) { + $userId = (int) $reward['agent_id']; + if ($userId > 0 && in_array($userId, $gateway['promoter'], true)) { + $commissionLib->runCommissionRewardWithoutWallet($event, $reward); + } else { + $commissionLib->runCommissionReward($event, $reward); + } + } + self::markCommissionOrderSettledIfDone($commissionOrder); + } + + $regionalLib = new RegionalRewardLibrary(); + $regionalOrders = RegionalOrderModel::where('order_id', $orderId) + ->where('commission_reward_status', RegionalRewardLibrary::STATUS_WAITING) + ->select(); + foreach ($regionalOrders as $regionalOrder) { + if ($regionalOrder['commission_event'] !== $event) { + continue; + } + $rewards = RegionalRewardModel::where('regional_order_id', $regionalOrder['id']) + ->where('status', RegionalRewardLibrary::STATUS_WAITING) + ->select(); + foreach ($rewards as $reward) { + $userId = (int) $reward['agent_user_id']; + if ($userId > 0 && in_array($userId, $gateway['regional_agent'], true)) { + $regionalLib->settleRewardWithoutWallet($reward, $event); + } else { + $regionalLib->settleReward($reward, $event); + } + } + self::markRegionalOrderSettledIfDone($regionalOrder); + } + } + + protected static function shouldSettleHuifuPerReceiver($orderId) + { + $order = Order::get($orderId); + if (!$order) { + return false; + } + return class_exists('\\addons\\shopro_huifu\\library\\Bridge') + && \addons\shopro_huifu\library\Bridge::shouldHandleSplit($order); + } + + /** + * @return array{promoter:int[],regional_agent:int[]} + */ + protected static function collectGatewaySplitUserIds($orderId) + { + $promoter = []; + $regional = []; + + $splitOrder = SplitOrder::where('order_id', $orderId) + ->where('status', 'in', [2, 3]) + ->order('id', 'desc') + ->find(); + if (!$splitOrder) { + return ['promoter' => [], 'regional_agent' => []]; + } + + $rows = SplitOrderReceiver::where('split_order_id', $splitOrder['id'])->select(); + foreach ($rows as $row) { + $roleType = (string) ($row['role_type'] ?? ''); + if (!in_array($roleType, ['promoter', 'regional_agent'], true)) { + continue; + } + $receiverId = (int) ($row['receiver_id'] ?? 0); + if ($receiverId <= 0) { + continue; + } + $receiver = Receiver::get($receiverId); + if (!$receiver) { + continue; + } + $roleUserId = (int) ($receiver['role_id'] ?? 0); + if ($roleUserId <= 0) { + continue; + } + if ($roleType === 'promoter') { + $promoter[$roleUserId] = $roleUserId; + } else { + $regional[$roleUserId] = $roleUserId; + } + } + + return [ + 'promoter' => array_values($promoter), + 'regional_agent' => array_values($regional), + ]; + } + + protected static function markCommissionOrderSettledIfDone($commissionOrder) + { + $waiting = CommissionRewardModel::where('commission_order_id', $commissionOrder['id']) + ->where('status', CommissionRewardLibrary::COMMISSION_REWARD_STATUS_WAITING) + ->count(); + if ($waiting > 0) { + return; + } + if ($commissionOrder['commission_reward_status'] !== CommissionRewardLibrary::COMMISSION_REWARD_STATUS_WAITING) { + return; + } + $commissionOrder->commission_reward_status = CommissionRewardLibrary::COMMISSION_REWARD_STATUS_ACCOUNTED; + $commissionOrder->commission_time = time(); + $commissionOrder->save(); + } + + protected static function markRegionalOrderSettledIfDone($regionalOrder) + { + $waiting = RegionalRewardModel::where('regional_order_id', $regionalOrder['id']) + ->where('status', RegionalRewardLibrary::STATUS_WAITING) + ->count(); + if ($waiting > 0) { + return; + } + if ($regionalOrder['commission_reward_status'] !== RegionalRewardLibrary::STATUS_WAITING) { + return; + } + $regionalOrder->commission_reward_status = RegionalRewardLibrary::STATUS_ACCOUNTED; + $regionalOrder->commission_time = time(); + $regionalOrder->save(); + } + public static function settleOrderWithoutWallet($orderId, $event) { $commissionLib = new CommissionRewardLibrary(); diff --git a/addons/shopro_huifu/library/HuifuSplitService.php b/addons/shopro_huifu/library/HuifuSplitService.php index e2ed388..d2194bc 100644 --- a/addons/shopro_huifu/library/HuifuSplitService.php +++ b/addons/shopro_huifu/library/HuifuSplitService.php @@ -68,7 +68,7 @@ class HuifuSplitService $outOrderNo = 'HFPS' . date('YmdHis') . $orderId; $ext = HuifuPayService::getPayExt($orderId); - $blockReason = $this->getDelayFundsBlockReason($order['order_sn']); + $blockReason = $this->getDelayTransBlockReason($order['order_sn'], $orderId); if ($blockReason !== null) { $this->markFailed($orderId, $outOrderNo, $order, $items, $blockReason); return false; @@ -298,10 +298,47 @@ class HuifuSplitService } /** - * 微信小程序 + 延时分账:通道侧资金可能处于 FREEZE,需发货/确认收货或等待解冻后才能 delaytrans/confirm。 + * 延时分账 confirm 前置拦截(非延迟入账 / 微信 FREEZE) * * @return string|null 不可 confirm 时返回说明文案,否则 null */ + protected function getDelayTransBlockReason($orderSn, $orderId) + { + $nonDelay = $this->getNonDelayAcctBlockReason($orderSn, $orderId); + if ($nonDelay !== null) { + return $nonDelay; + } + return $this->getDelayFundsBlockReason($orderSn); + } + + /** + * 23000004「交易非延迟入账」:支付时未带 delay_acct_flag=Y,不能调 delaytrans/confirm + */ + protected function getNonDelayAcctBlockReason($orderSn, $orderId) + { + $ext = HuifuPayService::getPayExt($orderId); + $local = strtoupper(trim((string) ($ext['delay_acct_flag'] ?? ''))); + if ($local === 'N') { + return '该笔支付为实时入账(delay_acct_flag=N),不支持 delaytrans/confirm(23000004 交易非延迟入账)。' + . '请在分账配置开启「延时分账」后重新下单;历史订单无法补打延迟标记,勿反复重试分账。'; + } + + try { + $query = (new HuifuQueryService())->queryByOrderSn($orderSn); + } catch (\Throwable $e) { + return null; + } + $remote = strtoupper(trim((string) ($this->queryFieldRecursive($query, 'delay_acct_flag') ?? ''))); + if ($remote !== '' && $remote !== 'Y') { + return '汇付查单 delay_acct_flag=' . $remote . '(非延迟入账),不能延时分账确认(23000004)。' + . ' 请核对下单是否 setDelayAcctFlag(Y)、商户 delay_flag 是否开通。'; + } + return null; + } + + /** + * 微信小程序 + 延时分账:通道侧资金可能处于 FREEZE,需发货/确认收货或等待解冻后才能 delaytrans/confirm。 + */ protected function getDelayFundsBlockReason($orderSn) { try { diff --git a/addons/shopro_wechat_trade/README.md b/addons/shopro_wechat_trade/README.md index 46544a0..d753c0f 100644 --- a/addons/shopro_wechat_trade/README.md +++ b/addons/shopro_wechat_trade/README.md @@ -55,6 +55,7 @@ php think shopro_wechat_trade:verify php think shopro_wechat_trade:init-path php think shopro_wechat_trade:backfill php think shopro_wechat_trade:retry-failed +php think shopro_wechat_trade:resync-shipping --sn=订单号 # 指定订单:商城确认收货 + 微信 confirm(截图里分账 FREEZE 的单可批量处理) php think shopro_wechat_trade:confirm-orders --sn=202608500438103091004500,202606584688620091002100 @@ -72,7 +73,7 @@ php think shopro_wechat_trade:confirm-orders --sn=202606584688620091002100 - 不再要求快递单号/快递公司,用订单号作 `tracking_no` - **不改变**商城订单的配送方式(快递单仍可在后台照常发货) -已同步过「快递」的旧单若需按自提重传,在同步日志里对该单重新触发发货同步,或 `php think shopro_wechat_trade:retry-failed`。 +已同步过「快递」的旧单若需按自提重传:`php think shopro_wechat_trade:resync-shipping --sn=订单号`(`retry-failed` 不会重试已是 success 的记录)。 已有库可执行: