feat(commission): 添加分账成功后佣金处理功能

添加了新的方法来处理分账成功后的佣金结算:
- 新增 runCommissionRewardByOrderWithoutWallet 方法用于分账成功时标记已结算
- 新增 runCommissionRewardWithoutWallet 方法处理不入余额的佣金奖励
- 避免在分账成功时重复将佣金计入用户余额

feat(regional_agent): 添加区域代理分账处理逻辑

- 新增 settleByRegionalOrderWithoutWallet 方法处理区域代理分账
- 新增 settleRewardWithoutWallet 方法处理不入钱包的奖励结算
- 完善区域代理佣金处理流程

refactor(split): 优化分账执行器逻辑

- 修改 SplitExecutor 类以支持延迟钱包结算
- 添加异常处理和事务管理
- 支持根据支付网关类型决定是否延迟钱包结算

feat(split): 实现分账完成后钱包结算机制

- 添加 SplitWalletSettlement 类来管理分账与钱包结算的协调
- 支持在分账成功后延迟钱包入账,失败时回退到钱包
- 修复汇付分账与佣金结算的时间冲突问题

refactor(hook): 调整分账钩子事件处理

- 修改 SplitHook 类的事件参数传递
- 更新各个订单状态变更事件的分账触发逻辑
- 确保分账执行时能正确获取事件类型

docs(tests): 更新分账佣金验证说明

- 更新测试文件中的注释说明
- 明确汇付/微信分账成功后的佣金处理方式
```
这个提交包含在:
可乐加冰
2026-09-29 12:37:53 +08:00
父节点 10c35237b3
当前提交 73d65c3f0c
共有 8 个文件被更改,包括 362 次插入 和 90 次删除

查看文件

@@ -91,6 +91,79 @@ class Reward
return false; 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; return false;
} }
/** /**
* 扣除/取消 分销订单 * 扣除/取消 分销订单
* *

查看文件

@@ -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\Level as LevelModel;
use addons\shopro\model\regional_agent\Order as RegionalOrderModel; use addons\shopro\model\regional_agent\Order as RegionalOrderModel;
use addons\shopro\model\regional_agent\Reward as RewardModel; 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); $agent->setInc('order_count', 1);
if ($this->_config->getCommissionEvent() === 'payed') { if ($this->_config->getCommissionEvent() === 'payed') {
(new Reward())->settleByRegionalOrder($regionalOrder->id, 'payed'); if (!SplitWalletSettlement::shouldDeferRegionalWallet($order, 'payed')) {
(new Reward())->settleByRegionalOrder($regionalOrder->id, 'payed');
}
} }
return $regionalOrder; return $regionalOrder;

查看文件

@@ -38,6 +38,28 @@ class Reward
return true; 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) public function settleReward($reward, $event)
{ {
if ($reward['status'] !== self::STATUS_WAITING) { 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) public function backByRegionalOrder($regionalOrderId, $event)
{ {
$regionalOrder = RegionalOrderModel::get($regionalOrderId); $regionalOrder = RegionalOrderModel::get($regionalOrderId);

查看文件

@@ -22,93 +22,106 @@ class SplitExecutor
public function executeByOrderId($orderId, $event = 'payed') public function executeByOrderId($orderId, $event = 'payed')
{ {
if (!$this->config->isEnable()) { $order = Order::get($orderId);
return false; $deferWallet = $order && SplitWalletSettlement::shouldDeferForGatewaySplit($order, $event);
} $ok = false;
if (class_exists('\\addons\\shopro_huifu\\library\\Bridge')) { try {
$hookParams = [ if (!$this->config->isEnable()) {
'order_id' => $orderId, return false;
'event' => $event, }
];
$handled = \think\Hook::listen('shopro_split_execute', $hookParams, null, true); if (class_exists('\\addons\\shopro_huifu\\library\\Bridge')) {
if ($handled === true) { $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; return true;
} }
}
$orderCheck = Order::get($orderId); $order = Order::get($orderId);
if ($orderCheck && $orderCheck['pay_type'] === 'huifu_wechat') { if (!$order || empty($order['transaction_id'])) {
return false; 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'],
];
} }
$result = $this->service->createOrder([ $items = $this->calculator->calculate($orderId);
'transaction_id' => $order['transaction_id'], if (empty($items)) {
'out_order_no' => $outOrderNo, return false;
'receivers' => $receivers, }
'unfreeze_unsplit' => true,
]);
$splitOrder->wx_result = is_array($result) ? json_encode($result, JSON_UNESCAPED_UNICODE) : json_encode($result); $outOrderNo = 'PS' . date('YmdHis') . $orderId;
$splitOrder->status = 2; $splitAmount = array_sum(array_column($items, 'amount'));
$splitOrder->save();
SplitOrderReceiver::where('split_order_id', $splitOrder->id)->update(['status' => 1]); Db::startTrans();
Db::commit(); try {
return true; $splitOrder = SplitOrder::create([
} catch (\Exception $e) { 'order_id' => $orderId,
Db::rollback(); 'order_sn' => $order['order_sn'],
$this->markFailed($orderId, $outOrderNo ?? ('PS' . $orderId), $order, $items ?? [], $e->getMessage()); 'transaction_id' => $order['transaction_id'],
return false; '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);
}
} }
} }

查看文件

@@ -0,0 +1,137 @@
<?php
namespace addons\shopro\library\split;
use addons\shopro\library\commission\Config as CommissionConfig;
use addons\shopro\library\commission\Reward as CommissionRewardLibrary;
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\regional_agent\Order as RegionalOrderModel;
use addons\shopro\model\split\SplitOrder;
/**
* 支付通道分账与钱包佣金互斥:分账成功则不入余额,失败则回退入余额
*/
class SplitWalletSettlement
{
public static function shouldDeferCommissionWallet($order, $event)
{
if (!self::shouldDeferForGatewaySplit($order, $event)) {
return false;
}
$commissionEvent = (new CommissionConfig())->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);
}
}
}
}

查看文件

@@ -5,6 +5,7 @@ namespace addons\shopro\listener\commission;
use addons\shopro\library\commission\Agent as AgentLibrary; use addons\shopro\library\commission\Agent as AgentLibrary;
use addons\shopro\library\commission\Commission as CommissionLibrary; use addons\shopro\library\commission\Commission as CommissionLibrary;
use addons\shopro\library\commission\Reward as RewardLibrary; use addons\shopro\library\commission\Reward as RewardLibrary;
use addons\shopro\library\split\SplitWalletSettlement;
use addons\shopro\model\Order; use addons\shopro\model\Order;
use addons\shopro\model\OrderItem; use addons\shopro\model\OrderItem;
@@ -48,8 +49,10 @@ class CommissionHook
$commissionOrder = $commission->addCommissionOrder(); $commissionOrder = $commission->addCommissionOrder();
// 执行奖金计划 // 执行奖金计划
$commission->runCommissionPlan($commissionOrder); $commission->runCommissionPlan($commissionOrder);
// 支付后拨款 // 支付后拨款(汇付分账同事件时延至分账结果后再结算)
(new RewardLibrary())->runCommissionRewardByOrder('payed', $commissionOrder->id); if (!SplitWalletSettlement::shouldDeferCommissionWallet($order, 'payed')) {
(new RewardLibrary())->runCommissionRewardByOrder('payed', $commissionOrder->id);
}
} }
} }
} }

查看文件

@@ -4,7 +4,6 @@ namespace addons\shopro\listener\split;
use addons\shopro\library\split\Config; use addons\shopro\library\split\Config;
use addons\shopro\library\split\SplitExecutor; use addons\shopro\library\split\SplitExecutor;
class SplitHook class SplitHook
{ {
protected function shouldRun($event) protected function shouldRun($event)
@@ -21,7 +20,7 @@ class SplitHook
if (!$this->shouldRun('payed')) { if (!$this->shouldRun('payed')) {
return; return;
} }
$this->runSplit($params); $this->runSplit($params, 'payed');
} }
public function orderConfirmAfter(&$params) public function orderConfirmAfter(&$params)
@@ -29,7 +28,7 @@ class SplitHook
if (!$this->shouldRun('confirm')) { if (!$this->shouldRun('confirm')) {
return; return;
} }
$this->runSplit($params); $this->runSplit($params, 'confirm');
} }
public function orderFinish(&$params) public function orderFinish(&$params)
@@ -37,7 +36,7 @@ class SplitHook
if (!$this->shouldRun('finish')) { if (!$this->shouldRun('finish')) {
return; return;
} }
$this->runSplit($params); $this->runSplit($params, 'finish');
} }
public function orderRefundAfter(&$params) public function orderRefundAfter(&$params)
@@ -52,12 +51,12 @@ class SplitHook
} }
} }
protected function runSplit($params) protected function runSplit($params, $event)
{ {
$order = $params['order'] ?? null; $order = $params['order'] ?? null;
if (!$order) { if (!$order) {
return; return;
} }
(new SplitExecutor())->executeByOrderId($order['id']); (new SplitExecutor())->executeByOrderId($order['id'], $event);
} }
} }

查看文件

@@ -197,7 +197,7 @@ function printSummary()
echo " · 网关分账金额来自 commission/regional reward 或 split_rule\n"; echo " · 网关分账金额来自 commission/regional reward 或 split_rule\n";
echo " · 汇付:推广员/区域 huifu_id 收 div_amt,余款进 platform_huifu_id 或主商户 huifu_id\n"; echo " · 汇付:推广员/区域 huifu_id 收 div_amt,余款进 platform_huifu_id 或主商户 huifu_id\n";
echo " · 业务佣金结算仍写 shopro_user_wallet_log(commission_income / regional_agent_income)\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; $since = time() - 90 * 86400;
$stats = Db::name('shopro_split_order')->alias('so') $stats = Db::name('shopro_split_order')->alias('so')