文件
可乐加冰 b5142ff780 ```
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

895 行
20 KiB
PHP

<?php
namespace addons\shopro\library\commission;
use addons\shopro\library\commission\Config;
use addons\shopro\model\commission\Level as LevelModel;
use addons\shopro\model\commission\Order as OrderModel;
use addons\shopro\model\commission\Reward as RewardModel;
use addons\shopro\library\commission\Reward as RewardLibrary;
/**
* 分佣业务
*/
class Commission
{
public $_config;
public $item;
public $buyer;
public $goods;
public $amount = 0;
public $commissionLevel = 0;
public $selfBuy;
/** @var array */
protected $agentLevelRulesCache = [];
const COMMISSION_CLOSE = 0;
public function __construct($item)
{
$this->buyer = new Agent($item['user_id']);
$this->item = $item;
$this->_config = new Config();
}
public function checkAndSetCommission()
{
if (!$this->item || !$this->buyer->user) {
return false;
}
$this->goods = new Goods($this->item['goods_id'], $this->item['goods_sku_price_id']);
if (!$this->goods->commissionGoods) {
return false;
}
if ($this->goods->getCommissionConfig()) {
$this->_config->setConfig($this->goods->getCommissionConfig());
}
$this->commissionLevel = $this->_config->getCommissionLevel();
if (self::COMMISSION_CLOSE === $this->commissionLevel) {
return false;
}
$this->selfBuy = $this->buyer->isAgentAvaliable() && $this->_config->isSelfBuy();
$hasParentOrSelfBuy = $this->buyer->getParentAgentId() || $this->selfBuy;
$hasChainCommission = $this->_config->isChainCommissionEnable()
&& $this->goods->getCommissionRules()
&& $hasParentOrSelfBuy;
$hasTierCommission = ($this->_config->isDifferentialCommissionEnable() && $this->goods->isDifferentialEnable())
|| ($this->_config->isManagementAwardEnable() && $this->goods->isManagementEnable());
$hasTierCommission = $hasTierCommission && $hasParentOrSelfBuy;
// 复购已在下单付款时直接减免,分销佣金奖池中不再发放买家本人复购返现
$hasRepurchaseSelf = false;
if (!$hasChainCommission && !$hasTierCommission && !$hasRepurchaseSelf) {
return false;
}
$this->amount = $this->getGoodsCommissionAmount();
if ($this->amount <= 0) {
return false;
}
return true;
}
public function getGoodsCommissionAmount()
{
$commissionType = $this->_config->getCommissionPriceType();
$amount = round(0, 2);
switch ($commissionType) {
case 'pay_price':
$amount = $this->item['pay_price'];
break;
case 'goods_price':
$amount = round($this->item['goods_price'] * $this->item['goods_num'], 2);
break;
}
return $amount;
}
public function addCommissionOrder()
{
$commissionOrder = OrderModel::where('order_item_id', $this->item['id'])->find();
if ($commissionOrder) {
return $this->syncExistingCommissionOrderOnPay($commissionOrder);
}
$agentId = $this->selfBuy
? $this->buyer->user->id
: ($this->buyer->getParentAgentId() ?: $this->buyer->user->id);
$commissionOrderParams = [
'self_buy' => intval($this->selfBuy),
'order_id' => $this->item['order_id'],
'order_item_id' => $this->item['id'],
'buyer_id' => $this->buyer->user->id,
'goods_id' => $this->item['goods_id'],
'agent_id' => $agentId,
'amount' => $this->amount,
'commission_price_type' => $this->_config->getCommissionPriceType(),
'commission_rules' => $this->encodeCommissionRulesSnapshot(),
'commission_event' => $this->_config->getCommissionEvent(),
'commission_order_status' => $this->goods->commissionGoods->commission_order_status,
];
$commissionOrder = OrderModel::create($commissionOrderParams);
$orderAgent = new Agent($commissionOrder->agent_id);
if ($orderAgent->isAgentAvaliable() && $commissionOrder->commission_order_status) {
$orderAgent->agent->setInc('order_money', $commissionOrder->amount);
$orderAgent->agent->setInc('order_count', 1);
Log::write($commissionOrder['agent_id'], 'order', [
'action' => 'payed',
'order' => $commissionOrder,
'order_item' => $this->item,
'buyer' => $this->buyer->user
], ['oper_type' => 'user', 'oper_id' => $this->buyer->user->id]);
$orderAgent->agent->save();
}
return $commissionOrder;
}
/**
* 支付时若已存在分销单(测试/脏数据/提前写入),与当前订单项不一致则同步并重算待发佣金
*/
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);
}
/**
* 执行分销计划:关系链、级差、管理奖可同时发放
*/
public function runCommissionPlan($commissionOrder, $currentAgent = null, $currentCommissionLevel = 1)
{
$skipReferrer = $this->isRepurchase() && !$this->_config->isRepurchaseReferrerEnable();
if (!$skipReferrer) {
if ($this->_config->isChainCommissionEnable() && $this->goods->getCommissionRules()) {
$this->runChainCommissionPlan($commissionOrder, $currentAgent, $currentCommissionLevel);
}
if ($this->_config->isDifferentialCommissionEnable() && $this->goods->isDifferentialEnable()) {
$this->runDifferentialCommissionPlan($commissionOrder);
}
if ($this->_config->isManagementAwardEnable() && $this->goods->isManagementEnable()) {
$this->runManagementAwardPlan($commissionOrder);
}
}
$this->runRepurchaseRewards($commissionOrder);
}
protected function runChainCommissionPlan($commissionOrder, $currentAgent = null, $currentCommissionLevel = 1)
{
if ($this->commissionLevel < $currentCommissionLevel) {
return true;
}
if ($currentCommissionLevel === 1) {
$currentAgent = new Agent($commissionOrder->agent_id);
}
if ($currentAgent && !empty($currentAgent->user)) {
$commissionReward = RewardModel::get([
'commission_order_id' => $commissionOrder->id,
'agent_id' => $currentAgent->user->id,
'commission_level' => $currentCommissionLevel,
]);
if (!$commissionReward) {
$currentAgentLevel = $currentAgent->getAgentLevel();
$currentCommissionLevelRule = $this->goods->getCommissionLevelRule($currentAgentLevel, $currentCommissionLevel);
if ($currentCommissionLevelRule) {
// repurchase_rate 已废弃:级差模式下无效;一级佣金为 0 时也不应再发关系链推广佣金
if (
$currentCommissionLevel === 1
&& $this->isRepurchase()
&& $this->_config->isRepurchaseEnable()
&& !$this->_config->isDifferentialMode()
) {
$baseRate = floatval($currentCommissionLevelRule['rate'] ?? 0);
if ($baseRate > 0) {
$repurchaseRate = $this->_config->getRepurchaseRate();
if ($repurchaseRate > 0) {
$currentCommissionLevelRule = ['rate' => $repurchaseRate];
}
}
}
$commission = $this->goods->caculateGoodsCommission($currentCommissionLevelRule, $this->amount, $this->item['goods_num']);
if ($commission > 0) {
$this->createCommissionReward($commissionOrder, $currentAgent, $currentCommissionLevel, $commission, $currentCommissionLevelRule, $currentAgentLevel, 'chain');
}
}
}
$currentCommissionLevel++;
if ($this->commissionLevel < $currentCommissionLevel) {
return true;
}
$parentAgentId = $currentAgent->getParentAgentId();
if ($parentAgentId) {
$parentAgent = new Agent($parentAgentId);
$this->runChainCommissionPlan($commissionOrder, $parentAgent, $currentCommissionLevel);
}
}
}
protected function runDifferentialCommissionPlan($commissionOrder)
{
$chain = $this->getAgentChain($commissionOrder);
if (!$chain) {
return;
}
$isSelfBuy = intval($commissionOrder->self_buy) === 1;
$buyerId = intval($commissionOrder->buyer_id);
$prevTier = 0;
if ($isSelfBuy) {
$buyerAgent = new Agent($buyerId);
if ($buyerAgent->isAgentAvaliable()) {
$prevTier = $this->getTierAmount($buyerAgent->getAgentLevel());
}
}
foreach ($chain as $item) {
$currentAgent = $item['agent'];
$commissionLevel = $item['level'];
if ($isSelfBuy && intval($currentAgent->user->id) === $buyerId) {
continue;
}
$currentAgentLevel = $currentAgent->getAgentLevel();
$tier = $this->getTierAmount($currentAgentLevel);
$payout = round(max(0, $tier - $prevTier), 2);
$prevTier = max($prevTier, $tier);
if ($payout > 0) {
$rule = [
'award_type' => 'differential',
'differential_amount' => $tier,
'payout' => $payout,
];
$this->createCommissionReward($commissionOrder, $currentAgent, $commissionLevel, $payout, $rule, $currentAgentLevel, 'differential');
}
}
}
protected function runManagementAwardPlan($commissionOrder)
{
$chain = $this->getAgentChain($commissionOrder);
if (!$chain) {
return;
}
$isSelfBuy = intval($commissionOrder->self_buy) === 1;
$buyerId = intval($commissionOrder->buyer_id);
if ($isSelfBuy) {
$buyerInChain = false;
foreach ($chain as $item) {
if (intval($item['agent']->user->id) === $buyerId) {
$buyerInChain = true;
break;
}
}
if (!$buyerInChain) {
$buyerAgent = new Agent($buyerId);
if ($buyerAgent->isAgentAvaliable()) {
array_unshift($chain, ['agent' => $buyerAgent, 'level' => 0]);
}
}
}
foreach ($chain as $item) {
$currentAgent = $item['agent'];
$commissionLevel = $item['level'] ?: 1;
$currentAgentLevel = $currentAgent->getAgentLevel();
$rate = $this->getManagementRate($currentAgentLevel);
if ($rate <= 0) {
continue;
}
$commission = round($this->amount * $rate * 0.01, 2);
if ($commission > 0) {
$rule = [
'award_type' => 'management',
'management_rate' => $rate,
];
$this->createCommissionReward($commissionOrder, $currentAgent, $commissionLevel, $commission, $rule, $currentAgentLevel, 'management');
}
}
}
protected function getAgentChain($commissionOrder)
{
$chain = [];
$depth = 1;
$agent = new Agent($commissionOrder->agent_id);
while ($agent && $agent->user && $depth <= $this->commissionLevel) {
if ($agent->isAgentAvaliable()) {
$chain[] = ['agent' => $agent, 'level' => $depth];
}
$parentId = $agent->getParentAgentId();
if (!$parentId) {
break;
}
$agent = new Agent($parentId);
$depth++;
}
return $chain;
}
protected function getTierAmount($agentLevel)
{
$rules = $this->getAgentLevelRules($agentLevel);
return floatval($rules['differential_amount'] ?? 0);
}
protected function getManagementRate($agentLevel)
{
$rules = $this->getAgentLevelRules($agentLevel);
return floatval($rules['management_rate'] ?? 0);
}
protected function getAgentLevelRules($agentLevel)
{
if (!isset($this->agentLevelRulesCache[$agentLevel])) {
$level = LevelModel::where('level', $agentLevel)->find();
$rules = $level ? ($level->commission_rules ?? []) : [];
$this->agentLevelRulesCache[$agentLevel] = is_array($rules) ? $rules : [];
}
return $this->agentLevelRulesCache[$agentLevel];
}
protected function runRepurchaseRewards($commissionOrder)
{
// 复购优惠已在商品下单付款时直接立减,此处不再发放买家本人复购返现佣金
return;
}
protected function isRepurchase()
{
$goodsId = $this->item['goods_id'] ?? 0;
$userId = $this->buyer->user->id ?? 0;
if (!$goodsId || !$userId) {
return false;
}
$count = \addons\shopro\model\OrderItem::alias('oi')
->join('shopro_order o', 'o.id = oi.order_id')
->where('o.user_id', $userId)
->where('o.status', 'in', [1, 2])
->where('oi.goods_id', $goodsId)
->where('o.id', '<', $this->item['order_id'])
->where('oi.refund_status', '<=', 0)
->count();
return $count > 0;
}
protected function createCommissionReward($commissionOrder, $currentAgent, $commissionLevel, $commission, $rule, $agentLevel, $awardType = 'chain')
{
if (!is_array($rule)) {
$rule = [];
}
$rule['award_type'] = $awardType;
$exists = RewardModel::where('commission_order_id', $commissionOrder->id)
->where('agent_id', $currentAgent->user->id)
->where('commission_level', $commissionLevel)
->select();
foreach ($exists as $item) {
$existRules = json_decode($item->commission_rules, true);
if (($existRules['award_type'] ?? 'chain') === $awardType) {
return;
}
}
$commissionReward = RewardModel::create([
'order_id' => $commissionOrder->order_id,
'order_item_id' => $commissionOrder->order_item_id,
'buyer_id' => $commissionOrder->buyer_id,
'commission_order_id' => $commissionOrder->id,
'agent_id' => $currentAgent->user->id,
'type' => 'money',
'commission' => $commission,
'status' => 0,
'original_commission' => $commission,
'commission_level' => $commissionLevel,
'commission_rules' => json_encode($rule),
'agent_level' => $agentLevel,
]);
Log::write($commissionReward['agent_id'], 'reward', [
'action' => 'payed',
'reward' => $commissionReward,
], ['oper_type' => 'user', 'oper_id' => $commissionOrder->buyer_id]);
}
}