```
feat(shopro): 实现汇付混合分账功能支持按人区分通道和钱包结算 - 新增汇付延时分账混合模式:已进件用户走支付通道,未进件用户入钱包 - 添加 shouldSettleHuifuPerReceiver 方法判断是否使用混合结算 - 实现 settleOrderHybridHuifuGatewayAndWallet 方法处理混合分账逻辑 - 新增 collectGatewaySplitUserIds 方法收集分账用户ID列表 - 添加 markCommissionOrderSettledIfDone 和 markRegionalOrderSettledIfDone 辅助方法 fix(shopro_huifu): 优化延时分账确认前置拦截逻辑 - 修复 getDelayTransBlockReason 方法参数传递问题 - 新增 getNonDelayAcctBlockReason 方法检查交易非延迟入账状态 - 添加 2300004 错误码拦截实时入账交易的延迟分账操作 docs(shopro_wechat_trade): 更新命令行工具文档 - 添加 resync-shipping 命令用于重新同步指定订单的配送信息 - 更新重传说明文档,明确 retry-failed 不会重试已成功的记录 ```
这个提交包含在:
@@ -8,11 +8,15 @@ 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;
|
||||
|
||||
/**
|
||||
* 支付通道分账与钱包佣金互斥:分账成功则不入余额,失败则回退入余额
|
||||
* 支付通道分账与钱包佣金:汇付可按人混合(已进件走通道、未进件进钱包);其它通道整单成功则不入余额
|
||||
*/
|
||||
class SplitWalletSettlement
|
||||
{
|
||||
@@ -74,7 +78,7 @@ class SplitWalletSettlement
|
||||
}
|
||||
|
||||
/**
|
||||
* 分账尝试结束后结算佣金(成功=仅记账不入钱包,失败=入钱包)
|
||||
* 分账尝试结束后结算佣金(汇付成功=按人混合;其它成功=整单不入钱包;失败=整单入钱包)
|
||||
*/
|
||||
public static function finalizeAfterSplitAttempt($orderId, $event, $splitExecutedOk = false)
|
||||
{
|
||||
@@ -83,12 +87,157 @@ class SplitWalletSettlement
|
||||
}
|
||||
|
||||
if (self::isSplitSuccessful($orderId)) {
|
||||
self::settleOrderWithoutWallet($orderId, $event);
|
||||
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) {
|
||||
if ($commissionOrder['commission_event'] !== $event && $event !== 'admin') {
|
||||
continue;
|
||||
}
|
||||
$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($event, $reward);
|
||||
} else {
|
||||
$commissionLib->runCommissionReward($event, $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) {
|
||||
if ($regionalOrder['commission_event'] !== $event) {
|
||||
continue;
|
||||
}
|
||||
$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, $event);
|
||||
} else {
|
||||
$regionalLib->settleReward($reward, $event);
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -68,7 +68,7 @@ class HuifuSplitService
|
||||
|
||||
$outOrderNo = 'HFPS' . date('YmdHis') . $orderId;
|
||||
$ext = HuifuPayService::getPayExt($orderId);
|
||||
$blockReason = $this->getDelayFundsBlockReason($order['order_sn']);
|
||||
$blockReason = $this->getDelayTransBlockReason($order['order_sn'], $orderId);
|
||||
if ($blockReason !== null) {
|
||||
$this->markFailed($orderId, $outOrderNo, $order, $items, $blockReason);
|
||||
return false;
|
||||
@@ -298,10 +298,47 @@ class HuifuSplitService
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序 + 延时分账:通道侧资金可能处于 FREEZE,需发货/确认收货或等待解冻后才能 delaytrans/confirm。
|
||||
* 延时分账 confirm 前置拦截(非延迟入账 / 微信 FREEZE)
|
||||
*
|
||||
* @return string|null 不可 confirm 时返回说明文案,否则 null
|
||||
*/
|
||||
protected function getDelayTransBlockReason($orderSn, $orderId)
|
||||
{
|
||||
$nonDelay = $this->getNonDelayAcctBlockReason($orderSn, $orderId);
|
||||
if ($nonDelay !== null) {
|
||||
return $nonDelay;
|
||||
}
|
||||
return $this->getDelayFundsBlockReason($orderSn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 23000004「交易非延迟入账」:支付时未带 delay_acct_flag=Y,不能调 delaytrans/confirm
|
||||
*/
|
||||
protected function getNonDelayAcctBlockReason($orderSn, $orderId)
|
||||
{
|
||||
$ext = HuifuPayService::getPayExt($orderId);
|
||||
$local = strtoupper(trim((string) ($ext['delay_acct_flag'] ?? '')));
|
||||
if ($local === 'N') {
|
||||
return '该笔支付为实时入账(delay_acct_flag=N),不支持 delaytrans/confirm(23000004 交易非延迟入账)。'
|
||||
. '请在分账配置开启「延时分账」后重新下单;历史订单无法补打延迟标记,勿反复重试分账。';
|
||||
}
|
||||
|
||||
try {
|
||||
$query = (new HuifuQueryService())->queryByOrderSn($orderSn);
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
$remote = strtoupper(trim((string) ($this->queryFieldRecursive($query, 'delay_acct_flag') ?? '')));
|
||||
if ($remote !== '' && $remote !== 'Y') {
|
||||
return '汇付查单 delay_acct_flag=' . $remote . '(非延迟入账),不能延时分账确认(23000004)。'
|
||||
. ' 请核对下单是否 setDelayAcctFlag(Y)、商户 delay_flag 是否开通。';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序 + 延时分账:通道侧资金可能处于 FREEZE,需发货/确认收货或等待解冻后才能 delaytrans/confirm。
|
||||
*/
|
||||
protected function getDelayFundsBlockReason($orderSn)
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -55,6 +55,7 @@ php think shopro_wechat_trade:verify
|
||||
php think shopro_wechat_trade:init-path
|
||||
php think shopro_wechat_trade:backfill
|
||||
php think shopro_wechat_trade:retry-failed
|
||||
php think shopro_wechat_trade:resync-shipping --sn=订单号
|
||||
|
||||
# 指定订单:商城确认收货 + 微信 confirm(截图里分账 FREEZE 的单可批量处理)
|
||||
php think shopro_wechat_trade:confirm-orders --sn=202608500438103091004500,202606584688620091002100
|
||||
@@ -72,7 +73,7 @@ php think shopro_wechat_trade:confirm-orders --sn=202606584688620091002100
|
||||
- 不再要求快递单号/快递公司,用订单号作 `tracking_no`
|
||||
- **不改变**商城订单的配送方式(快递单仍可在后台照常发货)
|
||||
|
||||
已同步过「快递」的旧单若需按自提重传,在同步日志里对该单重新触发发货同步,或 `php think shopro_wechat_trade:retry-failed`。
|
||||
已同步过「快递」的旧单若需按自提重传:`php think shopro_wechat_trade:resync-shipping --sn=订单号`(`retry-failed` 不会重试已是 success 的记录)。
|
||||
|
||||
已有库可执行:
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户