feat(commission): 完善分销订单支付时数据同步机制

- 新增 syncExistingCommissionOrderOnPay 方法处理已存在分销单的支付同步场景
- 添加 encodeCommissionRulesSnapshot 和 commissionRulesSnapshotEquals 辅助方法
- 优化佣金规则快照存储方式,统一JSON编码格式
- 增加已入账奖励数量检查,防止对已结算订单进行错误同步

fix(wallet-settlement): 调整分账和佣金结算的余额处理逻辑

- 新增 shouldSettleCommissionViaWalletHook 和 shouldSettleRegionalViaWalletHook
  方法控制分账成功后的钱包入账行为
- 修改 shouldDeferRegionalWallet 调用为 shouldSettleRegionalViaWalletHook
- 修复分账成功时仍可能重复入钱包的问题
- 统一佣金和区域代理的钩子结算判断逻辑

refactor(hook): 优化佣金钩子触发条件和订单查询逻辑

- 在 CommissionHook 中使用新的钱包结算判断方法
- 修正 orderFinish 事件中的订单项遍历逻辑
- 增加订单有效性检查,避免空值异常
- 统一各事件中的结算条件判断

test(sql): 更新分账佣金验证SQL脚本

- 修正注释说明,明确分账成功不入余额的控制逻辑
- 补充区域代理佣金缺失汇付接收方的查询
- 重构单笔订单深度核验脚本,提供更详细的订单分析信息
- 增加分账成功与钱包流水的对比检查逻辑
```
这个提交包含在:
可乐加冰
2026-10-22 11:44:24 +08:00
父节点 b457b9b434
当前提交 b5142ff780
共有 5 个文件被更改,包括 345 次插入 和 43 次删除

查看文件

@@ -14,6 +14,8 @@ use addons\shopro\model\commission\Order as OrderModel;
use addons\shopro\model\commission\Reward as RewardModel;
use addons\shopro\library\commission\Reward as RewardLibrary;
/**
@@ -193,7 +195,7 @@ class Commission
if ($commissionOrder) {
return $commissionOrder;
return $this->syncExistingCommissionOrderOnPay($commissionOrder);
}
@@ -225,17 +227,7 @@ class Commission
'commission_price_type' => $this->_config->getCommissionPriceType(),
'commission_rules' => json_encode([
'rules' => $this->goods->getCommissionRules(),
'differential_enable' => $this->goods->isDifferentialEnable() ? 1 : 0,
'management_enable' => $this->goods->isManagementEnable() ? 1 : 0,
'repurchase_enable' => $this->goods->isRepurchaseEnable() ? 1 : 0,
]),
'commission_rules' => $this->encodeCommissionRulesSnapshot(),
'commission_event' => $this->_config->getCommissionEvent(),
@@ -279,6 +271,128 @@ class Commission
/**
* 支付时若已存在分销单(测试/脏数据/提前写入),与当前订单项不一致则同步并重算待发佣金
*/
protected function syncExistingCommissionOrderOnPay($commissionOrder)
{
$agentId = $this->selfBuy
? $this->buyer->user->id
: ($this->buyer->getParentAgentId() ?: $this->buyer->user->id);
$newAmount = round(floatval($this->amount), 2);
$oldAmount = round(floatval($commissionOrder->amount), 2);
$newPriceType = $this->_config->getCommissionPriceType();
$newRulesJson = $this->encodeCommissionRulesSnapshot();
$needsSync = (
abs($newAmount - $oldAmount) >= 0.01
|| intval($commissionOrder->order_id) !== intval($this->item['order_id'])
|| intval($commissionOrder->agent_id) !== intval($agentId)
|| (string) $commissionOrder->commission_price_type !== (string) $newPriceType
|| !$this->commissionRulesSnapshotEquals($commissionOrder->commission_rules, $newRulesJson)
);
if (!$needsSync) {
return $commissionOrder;
}
$accountedCount = RewardModel::where('commission_order_id', $commissionOrder->id)
->where('status', RewardLibrary::COMMISSION_REWARD_STATUS_ACCOUNTED)
->count();
if ($accountedCount > 0) {
\think\Log::warning(sprintf(
'[commission] skip sync commission_order id=%d order_item_id=%d: %d accounted reward(s), old_amount=%s new_amount=%s',
$commissionOrder->id,
$this->item['id'],
$accountedCount,
$oldAmount,
$newAmount
));
return $commissionOrder;
}
RewardModel::where('commission_order_id', $commissionOrder->id)
->where('status', RewardLibrary::COMMISSION_REWARD_STATUS_WAITING)
->delete();
if (
intval($commissionOrder->commission_order_status) === OrderModel::COMMISSION_ORDER_STATUS_YES
&& abs($newAmount - $oldAmount) >= 0.01
) {
$orderAgent = new Agent($commissionOrder->agent_id);
if ($orderAgent->isAgentAvaliable()) {
$delta = bcsub((string) $newAmount, (string) $oldAmount, 2);
if (bccomp($delta, '0', 2) > 0) {
$orderAgent->agent->setInc('order_money', $delta);
} elseif (bccomp($delta, '0', 2) < 0) {
$dec = ltrim($delta, '-');
if (bccomp((string) $orderAgent->agent->order_money, $dec, 2) < 0) {
$orderAgent->agent->order_money = 0;
} else {
$orderAgent->agent->setDec('order_money', $dec);
}
}
$orderAgent->agent->save();
}
}
$commissionOrder->self_buy = intval($this->selfBuy);
$commissionOrder->order_id = intval($this->item['order_id']);
$commissionOrder->buyer_id = $this->buyer->user->id;
$commissionOrder->goods_id = $this->item['goods_id'];
$commissionOrder->agent_id = intval($agentId);
$commissionOrder->amount = $newAmount;
$commissionOrder->commission_price_type = $newPriceType;
$commissionOrder->commission_rules = $newRulesJson;
$commissionOrder->commission_event = $this->_config->getCommissionEvent();
if (intval($commissionOrder->commission_reward_status) !== RewardLibrary::COMMISSION_REWARD_STATUS_WAITING) {
$commissionOrder->commission_reward_status = RewardLibrary::COMMISSION_REWARD_STATUS_WAITING;
$commissionOrder->commission_time = null;
}
$commissionOrder->save();
\think\Log::info(sprintf(
'[commission] synced commission_order id=%d order_item_id=%d amount %s -> %s',
$commissionOrder->id,
$this->item['id'],
$oldAmount,
$newAmount
));
return $commissionOrder;
}
protected function encodeCommissionRulesSnapshot()
{
return json_encode([
'rules' => $this->goods->getCommissionRules(),
'differential_enable' => $this->goods->isDifferentialEnable() ? 1 : 0,
'management_enable' => $this->goods->isManagementEnable() ? 1 : 0,
'repurchase_enable' => $this->goods->isRepurchaseEnable() ? 1 : 0,
], JSON_UNESCAPED_UNICODE);
}
protected function commissionRulesSnapshotEquals($stored, $newJson)
{
$left = json_decode($stored ?: '{}', true);
$right = json_decode($newJson ?: '{}', true);
if (!is_array($left)) {
$left = [];
}
if (!is_array($right)) {
$right = [];
}
return json_encode($left, JSON_UNESCAPED_UNICODE) === json_encode($right, JSON_UNESCAPED_UNICODE);
}
/**
* 执行分销计划:关系链、级差、管理奖可同时发放

查看文件

@@ -292,7 +292,7 @@ class Commission
$agent->setInc('order_count', 1);
if ($this->_config->getCommissionEvent() === 'payed') {
if (!SplitWalletSettlement::shouldDeferRegionalWallet($order, 'payed')) {
if (SplitWalletSettlement::shouldSettleRegionalViaWalletHook($order, 'payed')) {
(new Reward())->settleByRegionalOrder($regionalOrder->id, 'payed');
}
}
@@ -311,7 +311,11 @@ class Commission
$rewardLib = new Reward();
foreach ($regionalOrders as $regionalOrder) {
if ($regionalOrder['commission_event'] === $event) {
if ($regionalOrder['commission_event'] !== $event) {
continue;
}
$order = \addons\shopro\model\Order::get($regionalOrder->order_id);
if ($order && SplitWalletSettlement::shouldSettleRegionalViaWalletHook($order, $event)) {
$rewardLib->settleByRegionalOrder($regionalOrder->id, $event);
}
}

查看文件

@@ -38,6 +38,38 @@ class SplitWalletSettlement
return self::shouldDeferWalletUntilGatewaySplitResolves($order);
}
/**
* 分销钩子是否应把待发佣金写入余额(false = 等分账 finalize 或已走通道,避免与汇付/微信分账重复)
*/
public static function shouldSettleCommissionViaWalletHook($order, $event)
{
$commissionEvent = (new CommissionConfig())->getCommissionEvent();
if ($commissionEvent !== $event && $event !== 'admin') {
return false;
}
if (self::shouldDeferWalletUntilGatewaySplitResolves($order)) {
return false;
}
return true;
}
/**
* 区域代理钩子是否应把待发佣金写入余额
*/
public static function shouldSettleRegionalViaWalletHook($order, $event)
{
$regionalEvent = (new RegionalConfig())->getCommissionEvent();
if ($regionalEvent !== $event && $event !== 'admin') {
return false;
}
if (self::shouldDeferWalletUntilGatewaySplitResolves($order)) {
return false;
}
return true;
}
/**
* 分销/区域结算事件(如 payed)早于 split_timing(如 confirm)时,仍应等分账结束再入钱包
*/
@@ -54,8 +86,9 @@ class SplitWalletSettlement
if (!$config->isEnable() || !$config->shouldSkipWallet()) {
return false;
}
// 分账已成功:佣金应由 finalizeAfterSplitAttempt 处理(或已标记通道到账),钩子不得再入余额
if (self::isSplitSuccessful($order['id'])) {
return false;
return true;
}
if (class_exists('\\addons\\shopro_huifu\\library\\Bridge')