文件
xionggeshop/addons/shopro/library/split/SplitWalletSettlement.php
可乐加冰 6a654070ed ```
feat(split): 新增分账订单线下结案功能

- 添加 SplitCloseService 类用于处理订单结案逻辑
- 实现订单结案状态检查和标记功能
- 在汇付分账重试逻辑中加入结案状态验证,防止已结案订单继续重试
- 更新分账手动服务,添加结案操作类型
- 扩展钱包结算类,增加结案相关诊断方法
- 修改测试脚本,支持批量标记订单为已结案状态
- 添加数据库升级脚本,增加结案相关字段和菜单权限
```
2026-10-24 21:18:48 +08:00

443 行
17 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?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\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;
use think\Db;
/**
* 支付通道分账与钱包佣金:汇付可按人混合(已进件走通道、未进件进钱包);其它通道整单成功则不入余额
*/
class SplitWalletSettlement
{
public static function shouldDeferCommissionWallet($order, $event)
{
$commissionEvent = (new CommissionConfig())->getCommissionEvent();
if ($commissionEvent !== $event && $event !== 'admin') {
return false;
}
return self::shouldDeferWalletUntilGatewaySplitResolves($order);
}
public static function shouldDeferRegionalWallet($order, $event)
{
$regionalEvent = (new RegionalConfig())->getCommissionEvent();
if ($regionalEvent !== $event && $event !== 'admin') {
return false;
}
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)时,仍应等分账结束再入钱包
*/
protected static function shouldDeferWalletUntilGatewaySplitResolves($order)
{
if (is_numeric($order)) {
$order = Order::get($order);
}
if (!$order) {
return false;
}
$config = new Config();
if (!$config->isEnable() || !$config->shouldSkipWallet()) {
return false;
}
// 分账已成功:佣金应由 finalizeAfterSplitAttempt 处理(或已标记通道到账),钩子不得再入余额
if (self::isSplitSuccessful($order['id'])) {
return true;
}
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 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)) {
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) {
$settleEvent = (string) ($commissionOrder['commission_event'] ?? $event);
$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($settleEvent, $reward);
} else {
$commissionLib->runCommissionReward($settleEvent, $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) {
$settleEvent = (string) ($regionalOrder['commission_event'] ?? $event);
$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, $settleEvent);
} else {
$regionalLib->settleReward($reward, $settleEvent);
}
}
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();
$commissionOrders = CommissionOrderModel::where('order_id', $orderId)
->where('commission_reward_status', CommissionRewardLibrary::COMMISSION_REWARD_STATUS_WAITING)
->select();
foreach ($commissionOrders as $commissionOrder) {
$settleEvent = (string) ($commissionOrder['commission_event'] ?? $event);
$commissionLib->runCommissionRewardByOrderWithoutWallet($settleEvent, $commissionOrder);
}
$regionalLib = new RegionalRewardLibrary();
$regionalOrders = RegionalOrderModel::where('order_id', $orderId)
->where('commission_reward_status', RegionalRewardLibrary::STATUS_WAITING)
->select();
foreach ($regionalOrders as $regionalOrder) {
$settleEvent = (string) ($regionalOrder['commission_event'] ?? $event);
$regionalLib->settleByRegionalOrderWithoutWallet($regionalOrder->id, $settleEvent);
}
}
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) {
$settleEvent = (string) ($commissionOrder['commission_event'] ?? $event);
$commissionLib->runCommissionRewardByOrder($settleEvent, $commissionOrder);
}
$regionalLib = new RegionalRewardLibrary();
$regionalOrders = RegionalOrderModel::where('order_id', $orderId)
->where('commission_reward_status', RegionalRewardLibrary::STATUS_WAITING)
->select();
foreach ($regionalOrders as $regionalOrder) {
$settleEvent = (string) ($regionalOrder['commission_event'] ?? $event);
$regionalLib->settleByRegionalOrder($regionalOrder->id, $settleEvent);
}
}
/**
* 汇付 confirm 重试前:佣金已入平台钱包则禁止再次分账(防双发)
*/
public static function getHuifuRetryBlockReason($orderId)
{
$orderId = intval($orderId);
if ($orderId <= 0) {
return null;
}
if (class_exists('\\addons\\shopro\\library\\split\\SplitCloseService')
&& (new SplitCloseService())->isClosed($orderId)) {
return '该订单已标记线下结案,不可重试汇付分账';
}
$wallet = self::sumOrderWalletCommission($orderId);
if (bccomp($wallet, '0.00', 2) <= 0) {
return null;
}
$splitOk = self::isSplitSuccessful($orderId);
if ($splitOk) {
return '网关分账已成功且存在钱包佣金流水,禁止重试(疑似双轨),请标记结案或人工对账';
}
return '佣金已入平台钱包兜底,禁止再次汇付 confirm(防双发);若需通道分账请先对账并标记结案';
}
/**
* @return array{settlement_source:string,settlement_source_text:string,wallet_sum:string,split_success:bool,retry_blocked:bool,retry_block_reason:string}
*/
public static function diagnoseOrderSettlement($orderId)
{
$orderId = intval($orderId);
$walletSum = self::sumOrderWalletCommission($orderId);
$splitOk = self::isSplitSuccessful($orderId);
$retryReason = self::getHuifuRetryBlockReason($orderId);
$source = 'pending';
if ($splitOk && bccomp($walletSum, '0.00', 2) > 0) {
$source = 'hybrid';
} elseif ($splitOk) {
$source = 'gateway';
} elseif (bccomp($walletSum, '0.00', 2) > 0) {
$source = 'wallet';
}
$textMap = [
'pending' => '待结算/无钱包流水',
'gateway' => '汇付通道(未入平台钱包)',
'wallet' => '平台钱包兜底',
'hybrid' => '通道+钱包混合(请核对)',
];
return [
'settlement_source' => $source,
'settlement_source_text' => $textMap[$source] ?? $source,
'wallet_sum' => $walletSum,
'split_success' => $splitOk,
'retry_blocked' => $retryReason !== null,
'retry_block_reason' => $retryReason ?: '',
];
}
protected static function sumOrderWalletCommission($orderId)
{
$orderId = intval($orderId);
if ($orderId <= 0) {
return '0.00';
}
$sum = '0.00';
$crIds = CommissionRewardModel::where('order_id', $orderId)
->where('status', CommissionRewardLibrary::COMMISSION_REWARD_STATUS_ACCOUNTED)
->column('id');
if ($crIds) {
$part = Db::name('shopro_user_wallet_log')
->where('item_id', 'in', $crIds)
->where('wallet_type', 'money')
->where('type', 'commission_income')
->sum('wallet');
$sum = bcadd($sum, number_format((float) $part, 2, '.', ''), 2);
}
$regionalOrderIds = RegionalOrderModel::where('order_id', $orderId)->column('id');
if ($regionalOrderIds) {
$rrIds = RegionalRewardModel::where('regional_order_id', 'in', $regionalOrderIds)
->where('status', RegionalRewardLibrary::STATUS_ACCOUNTED)
->column('id');
if ($rrIds) {
$part = Db::name('shopro_user_wallet_log')
->where('item_id', 'in', $rrIds)
->where('wallet_type', 'money')
->where('type', 'regional_agent_income')
->sum('wallet');
$sum = bcadd($sum, number_format((float) $part, 2, '.', ''), 2);
}
}
return $sum;
}
}