diff --git a/addons/shopro/library/commission/Reward.php b/addons/shopro/library/commission/Reward.php index a4fb5ac..01badcb 100644 --- a/addons/shopro/library/commission/Reward.php +++ b/addons/shopro/library/commission/Reward.php @@ -91,6 +91,79 @@ class Reward return false; } + /** + * 分账成功:标记已结算,累计收益,不入余额 + */ + public function runCommissionRewardByOrderWithoutWallet($event, $commissionOrder) + { + if (is_numeric($commissionOrder)) { + $commissionOrder = OrderModel::get($commissionOrder); + } + if (!$commissionOrder) { + return false; + } + if ($commissionOrder['commission_reward_status'] !== self::COMMISSION_REWARD_STATUS_WAITING) { + return false; + } + $commissionEvent = $commissionOrder['commission_event']; + if ($commissionEvent !== $event && $event !== 'admin') { + return false; + } + + $commissionOrder->commission_reward_status = self::COMMISSION_REWARD_STATUS_ACCOUNTED; + $commissionOrder->commission_time = time(); + $commissionOrder->save(); + + $commissionRewards = RewardModel::all([ + 'commission_order_id' => $commissionOrder['id'], + 'status' => self::COMMISSION_REWARD_STATUS_WAITING, + ]); + if (!$commissionRewards) { + return false; + } + foreach ($commissionRewards as $commissionReward) { + $this->runCommissionRewardWithoutWallet($event, $commissionReward); + } + return true; + } + + public function runCommissionRewardWithoutWallet($event, $commissionReward) + { + if (is_numeric($commissionReward)) { + $commissionReward = RewardModel::get($commissionReward); + } + if (!$commissionReward || $commissionReward->status != self::COMMISSION_REWARD_STATUS_WAITING) { + return false; + } + if ($this->isRepurchaseSelfReward($commissionReward)) { + $user = UserModel::get($commissionReward->agent_id); + if (!$user) { + return false; + } + $commissionReward->status = self::COMMISSION_REWARD_STATUS_ACCOUNTED; + $commissionReward->save(); + Log::write($commissionReward->agent_id, 'reward', [ + 'action' => $event, + 'reward' => $commissionReward, + 'wallet_skipped' => 'gateway_split', + ], $this->oper); + return true; + } + $rewardAgent = new Agent($commissionReward->agent_id); + if ($rewardAgent && $rewardAgent->isAgentAvaliable()) { + $rewardAgent->agent->setInc('total_income', $commissionReward->commission); + $commissionReward->status = self::COMMISSION_REWARD_STATUS_ACCOUNTED; + $commissionReward->save(); + Log::write($rewardAgent->user->id, 'reward', [ + 'action' => $event, + 'reward' => $commissionReward, + 'wallet_skipped' => 'gateway_split', + ], $this->oper); + return true; + } + return false; + } + /** * 执行奖金计划,直接派发佣金 * @@ -140,7 +213,6 @@ class Reward return false; } - /** * 扣除/取消 分销订单 * diff --git a/addons/shopro/library/regional_agent/Commission.php b/addons/shopro/library/regional_agent/Commission.php index 55151e5..ec0fdbd 100644 --- a/addons/shopro/library/regional_agent/Commission.php +++ b/addons/shopro/library/regional_agent/Commission.php @@ -8,6 +8,8 @@ use addons\shopro\model\regional_agent\Agent as AgentModel; use addons\shopro\model\regional_agent\Level as LevelModel; use addons\shopro\model\regional_agent\Order as RegionalOrderModel; use addons\shopro\model\regional_agent\Reward as RewardModel; +use addons\shopro\library\split\SplitWalletSettlement; +use addons\shopro\library\split\SplitWalletSettlement; /** * 区域代理分佣 @@ -291,7 +293,9 @@ class Commission $agent->setInc('order_count', 1); if ($this->_config->getCommissionEvent() === 'payed') { - (new Reward())->settleByRegionalOrder($regionalOrder->id, 'payed'); + if (!SplitWalletSettlement::shouldDeferRegionalWallet($order, 'payed')) { + (new Reward())->settleByRegionalOrder($regionalOrder->id, 'payed'); + } } return $regionalOrder; diff --git a/addons/shopro/library/regional_agent/Reward.php b/addons/shopro/library/regional_agent/Reward.php index 64b60f7..8695d1f 100644 --- a/addons/shopro/library/regional_agent/Reward.php +++ b/addons/shopro/library/regional_agent/Reward.php @@ -38,6 +38,28 @@ class Reward return true; } + public function settleByRegionalOrderWithoutWallet($regionalOrderId, $event) + { + $regionalOrder = RegionalOrderModel::get($regionalOrderId); + if (!$regionalOrder || $regionalOrder['commission_reward_status'] !== self::STATUS_WAITING) { + return false; + } + + $regionalOrder->commission_reward_status = self::STATUS_ACCOUNTED; + $regionalOrder->commission_time = time(); + $regionalOrder->save(); + + $rewards = RewardModel::where('regional_order_id', $regionalOrderId) + ->where('status', self::STATUS_WAITING) + ->select(); + + foreach ($rewards as $reward) { + $this->settleRewardWithoutWallet($reward, $event); + } + + return true; + } + public function settleReward($reward, $event) { if ($reward['status'] !== self::STATUS_WAITING) { @@ -68,6 +90,28 @@ class Reward } } + public function settleRewardWithoutWallet($reward, $event) + { + if ($reward['status'] !== self::STATUS_WAITING) { + return false; + } + + $agent = AgentModel::get($reward['agent_id']); + if (!$agent || $agent['status'] !== AgentModel::STATUS_NORMAL) { + return false; + } + + $agentUser = UserModel::get($reward['agent_user_id']); + if (!$agentUser) { + return false; + } + + $agent->setInc('total_income', $reward['commission']); + $reward->status = self::STATUS_ACCOUNTED; + $reward->save(); + return true; + } + public function backByRegionalOrder($regionalOrderId, $event) { $regionalOrder = RegionalOrderModel::get($regionalOrderId); diff --git a/addons/shopro/library/split/SplitExecutor.php b/addons/shopro/library/split/SplitExecutor.php index ebf365a..302b210 100644 --- a/addons/shopro/library/split/SplitExecutor.php +++ b/addons/shopro/library/split/SplitExecutor.php @@ -22,93 +22,106 @@ class SplitExecutor public function executeByOrderId($orderId, $event = 'payed') { - if (!$this->config->isEnable()) { - return false; - } + $order = Order::get($orderId); + $deferWallet = $order && SplitWalletSettlement::shouldDeferForGatewaySplit($order, $event); + $ok = false; - if (class_exists('\\addons\\shopro_huifu\\library\\Bridge')) { - $hookParams = [ - 'order_id' => $orderId, - 'event' => $event, - ]; - $handled = \think\Hook::listen('shopro_split_execute', $hookParams, null, true); - if ($handled === true) { + try { + if (!$this->config->isEnable()) { + return false; + } + + if (class_exists('\\addons\\shopro_huifu\\library\\Bridge')) { + $hookParams = [ + 'order_id' => $orderId, + 'event' => $event, + ]; + $handled = \think\Hook::listen('shopro_split_execute', $hookParams, null, true); + if ($handled === true) { + $ok = true; + return true; + } + } + + $orderCheck = Order::get($orderId); + if ($orderCheck && $orderCheck['pay_type'] === 'huifu_wechat') { + return false; + } + + $exists = SplitOrder::where('order_id', $orderId)->where('status', 'in', [1, 2])->find(); + if ($exists) { + $ok = true; return true; } - } - $orderCheck = Order::get($orderId); - if ($orderCheck && $orderCheck['pay_type'] === 'huifu_wechat') { - return false; - } - - $exists = SplitOrder::where('order_id', $orderId)->where('status', 'in', [1, 2])->find(); - if ($exists) { - return false; - } - - $order = Order::get($orderId); - if (!$order || empty($order['transaction_id'])) { - return false; - } - - $items = $this->calculator->calculate($orderId); - if (empty($items)) { - return false; - } - - $outOrderNo = 'PS' . date('YmdHis') . $orderId; - $splitAmount = array_sum(array_column($items, 'amount')); - - Db::startTrans(); - try { - $splitOrder = SplitOrder::create([ - 'order_id' => $orderId, - 'order_sn' => $order['order_sn'], - 'transaction_id' => $order['transaction_id'], - 'out_order_no' => $outOrderNo, - 'total_amount' => $order['pay_fee'], - 'split_amount' => $splitAmount, - 'status' => 1, - ]); - - $receivers = []; - foreach ($items as $item) { - SplitOrderReceiver::create([ - 'split_order_id' => $splitOrder->id, - 'receiver_id' => $item['receiver_id'], - 'role_type' => $item['role_type'], - 'amount' => $item['amount'], - 'description' => $item['description'], - 'status' => 0, - ]); - $receivers[] = [ - 'type' => $item['receiver_type'], - 'account' => $item['receiver_account'], - 'name' => $item['receiver_name'] ?? '', - 'amount' => intval(round($item['amount'] * 100)), - 'description' => $item['description'], - ]; + $order = Order::get($orderId); + if (!$order || empty($order['transaction_id'])) { + return false; } - $result = $this->service->createOrder([ - 'transaction_id' => $order['transaction_id'], - 'out_order_no' => $outOrderNo, - 'receivers' => $receivers, - 'unfreeze_unsplit' => true, - ]); + $items = $this->calculator->calculate($orderId); + if (empty($items)) { + return false; + } - $splitOrder->wx_result = is_array($result) ? json_encode($result, JSON_UNESCAPED_UNICODE) : json_encode($result); - $splitOrder->status = 2; - $splitOrder->save(); + $outOrderNo = 'PS' . date('YmdHis') . $orderId; + $splitAmount = array_sum(array_column($items, 'amount')); - SplitOrderReceiver::where('split_order_id', $splitOrder->id)->update(['status' => 1]); - Db::commit(); - return true; - } catch (\Exception $e) { - Db::rollback(); - $this->markFailed($orderId, $outOrderNo ?? ('PS' . $orderId), $order, $items ?? [], $e->getMessage()); - return false; + Db::startTrans(); + try { + $splitOrder = SplitOrder::create([ + 'order_id' => $orderId, + 'order_sn' => $order['order_sn'], + 'transaction_id' => $order['transaction_id'], + 'out_order_no' => $outOrderNo, + 'total_amount' => $order['pay_fee'], + 'split_amount' => $splitAmount, + 'status' => 1, + ]); + + $receivers = []; + foreach ($items as $item) { + SplitOrderReceiver::create([ + 'split_order_id' => $splitOrder->id, + 'receiver_id' => $item['receiver_id'], + 'role_type' => $item['role_type'], + 'amount' => $item['amount'], + 'description' => $item['description'], + 'status' => 0, + ]); + $receivers[] = [ + 'type' => $item['receiver_type'], + 'account' => $item['receiver_account'], + 'name' => $item['receiver_name'] ?? '', + 'amount' => intval(round($item['amount'] * 100)), + 'description' => $item['description'], + ]; + } + + $result = $this->service->createOrder([ + 'transaction_id' => $order['transaction_id'], + 'out_order_no' => $outOrderNo, + 'receivers' => $receivers, + 'unfreeze_unsplit' => true, + ]); + + $splitOrder->wx_result = is_array($result) ? json_encode($result, JSON_UNESCAPED_UNICODE) : json_encode($result); + $splitOrder->status = 2; + $splitOrder->save(); + + SplitOrderReceiver::where('split_order_id', $splitOrder->id)->update(['status' => 1]); + Db::commit(); + $ok = true; + return true; + } catch (\Exception $e) { + Db::rollback(); + $this->markFailed($orderId, $outOrderNo ?? ('PS' . $orderId), $order, $items ?? [], $e->getMessage()); + return false; + } + } finally { + if ($deferWallet) { + SplitWalletSettlement::finalizeAfterSplitAttempt($orderId, $event, $ok); + } } } diff --git a/addons/shopro/library/split/SplitWalletSettlement.php b/addons/shopro/library/split/SplitWalletSettlement.php new file mode 100644 index 0000000..44ccdab --- /dev/null +++ b/addons/shopro/library/split/SplitWalletSettlement.php @@ -0,0 +1,137 @@ +getCommissionEvent(); + return $commissionEvent === $event; + } + + public static function shouldDeferRegionalWallet($order, $event) + { + if (!self::shouldDeferForGatewaySplit($order, $event)) { + return false; + } + $regionalEvent = (new RegionalConfig())->getCommissionEvent(); + return $regionalEvent === $event; + } + + /** + * 当前订单是否应等分账结果再决定钱包入账 + */ + public static function shouldDeferForGatewaySplit($order, $event) + { + if (is_numeric($order)) { + $order = Order::get($order); + } + if (!$order) { + return false; + } + + $config = new Config(); + if (!$config->isEnable() || !$config->shouldSkipWallet()) { + return false; + } + if ($config->getTiming() !== $event) { + return false; + } + + if (class_exists('\\addons\\shopro_huifu\\library\\Bridge') + && \addons\shopro_huifu\library\Bridge::shouldHandleSplit($order)) { + return true; + } + + if (in_array($config->getMode(), ['wechat', 'both'], true) && $order['pay_type'] === 'wechat') { + return true; + } + + return false; + } + + public static function isSplitSuccessful($orderId) + { + return SplitOrder::where('order_id', $orderId) + ->where('status', 'in', [2, 3]) + ->count() > 0; + } + + /** + * 分账尝试结束后结算佣金(成功=仅记账不入钱包,失败=入钱包) + */ + public static function finalizeAfterSplitAttempt($orderId, $event, $splitExecutedOk = false) + { + if (!self::shouldDeferForGatewaySplit($orderId, $event)) { + return; + } + + if (self::isSplitSuccessful($orderId)) { + self::settleOrderWithoutWallet($orderId, $event); + } else { + self::settleOrderToWallet($orderId, $event); + } + } + + public static function settleOrderWithoutWallet($orderId, $event) + { + $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') { + $commissionLib->runCommissionRewardByOrderWithoutWallet($event, $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) { + $regionalLib->settleByRegionalOrderWithoutWallet($regionalOrder->id, $event); + } + } + } + + public static function settleOrderToWallet($orderId, $event) + { + $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') { + $commissionLib->runCommissionRewardByOrder($event, $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) { + $regionalLib->settleByRegionalOrder($regionalOrder->id, $event); + } + } + } +} diff --git a/addons/shopro/listener/commission/CommissionHook.php b/addons/shopro/listener/commission/CommissionHook.php index f44970c..4e652e0 100644 --- a/addons/shopro/listener/commission/CommissionHook.php +++ b/addons/shopro/listener/commission/CommissionHook.php @@ -5,6 +5,7 @@ namespace addons\shopro\listener\commission; use addons\shopro\library\commission\Agent as AgentLibrary; use addons\shopro\library\commission\Commission as CommissionLibrary; use addons\shopro\library\commission\Reward as RewardLibrary; +use addons\shopro\library\split\SplitWalletSettlement; use addons\shopro\model\Order; use addons\shopro\model\OrderItem; @@ -48,8 +49,10 @@ class CommissionHook $commissionOrder = $commission->addCommissionOrder(); // 执行奖金计划 $commission->runCommissionPlan($commissionOrder); - // 支付后拨款 - (new RewardLibrary())->runCommissionRewardByOrder('payed', $commissionOrder->id); + // 支付后拨款(汇付分账同事件时延至分账结果后再结算) + if (!SplitWalletSettlement::shouldDeferCommissionWallet($order, 'payed')) { + (new RewardLibrary())->runCommissionRewardByOrder('payed', $commissionOrder->id); + } } } } diff --git a/addons/shopro/listener/split/SplitHook.php b/addons/shopro/listener/split/SplitHook.php index 1a4d091..1b15d6e 100644 --- a/addons/shopro/listener/split/SplitHook.php +++ b/addons/shopro/listener/split/SplitHook.php @@ -4,7 +4,6 @@ namespace addons\shopro\listener\split; use addons\shopro\library\split\Config; use addons\shopro\library\split\SplitExecutor; - class SplitHook { protected function shouldRun($event) @@ -21,7 +20,7 @@ class SplitHook if (!$this->shouldRun('payed')) { return; } - $this->runSplit($params); + $this->runSplit($params, 'payed'); } public function orderConfirmAfter(&$params) @@ -29,7 +28,7 @@ class SplitHook if (!$this->shouldRun('confirm')) { return; } - $this->runSplit($params); + $this->runSplit($params, 'confirm'); } public function orderFinish(&$params) @@ -37,7 +36,7 @@ class SplitHook if (!$this->shouldRun('finish')) { return; } - $this->runSplit($params); + $this->runSplit($params, 'finish'); } public function orderRefundAfter(&$params) @@ -52,12 +51,12 @@ class SplitHook } } - protected function runSplit($params) + protected function runSplit($params, $event) { $order = $params['order'] ?? null; if (!$order) { return; } - (new SplitExecutor())->executeByOrderId($order['id']); + (new SplitExecutor())->executeByOrderId($order['id'], $event); } } diff --git a/addons/shopro/tests/split_commission_verify.php b/addons/shopro/tests/split_commission_verify.php index aecc2e4..e8f6ce6 100644 --- a/addons/shopro/tests/split_commission_verify.php +++ b/addons/shopro/tests/split_commission_verify.php @@ -197,7 +197,7 @@ function printSummary() echo " · 网关分账金额来自 commission/regional reward 或 split_rule\n"; echo " · 汇付:推广员/区域 huifu_id 收 div_amt,余款进 platform_huifu_id 或主商户 huifu_id\n"; echo " · 业务佣金结算仍写 shopro_user_wallet_log(commission_income / regional_agent_income)\n"; - echo " · Config::shouldSkipWallet() 未接入 Reward,分账开启时仍可能同时入账钱包 → 用 anomalies 命令排查\n"; + echo " · 汇付/微信分账成功:佣金标记已结算且不入 user.money;分账失败则回退入钱包\n"; $since = time() - 90 * 86400; $stats = Db::name('shopro_split_order')->alias('so')