feat(shopro): 完善汇付分账功能的错误处理和调试信息 - 添加主商户号验证逻辑,当platform_huifu_id为空时使用插件主商户 - 改进分账剩余金额显示逻辑,区分主商户留存和平台分账情况 - 更新错误提示信息,明确2200002错误的处理方式 - 新增acct_infos预览功能,便于调试分账配置 - 优化分账服务中的商户号处理,避免主商户重复分账 fix(shopro_huifu): 修复汇付分账配置错误提示和分账逻辑 - 更新22000002错误排查说明,简化表述并突出关键点 - 修复分账服务中构建分账信息的逻辑,确保不包含收款主商户 - 添加日志记录,当跳过分账方为自己时进行警告 - 优化剩余金额处理,当日志平台即收款主商户时剩余款项保留在主商户 - 增强分账验证,当无有效分账接收方时抛出异常 ```
716 行
29 KiB
PHP
716 行
29 KiB
PHP
<?php
|
||
/**
|
||
* 分账开关 & 已分账订单佣金去向 · CLI 核验
|
||
*
|
||
* 用法(项目根目录):
|
||
* php addons/shopro/tests/split_commission_verify.php summary
|
||
* php addons/shopro/tests/split_commission_verify.php anomalies
|
||
* php addons/shopro/tests/split_commission_verify.php order {order_sn}
|
||
* php addons/shopro/tests/split_commission_verify.php recent-success [limit=20]
|
||
* php addons/shopro/tests/split_commission_verify.php diagnose-missing [limit=20]
|
||
* php addons/shopro/tests/split_commission_verify.php try-split {order_sn} --execute # 补跑汇付分账(慎用)
|
||
*/
|
||
|
||
if (PHP_SAPI !== 'cli') {
|
||
http_response_code(403);
|
||
exit("请通过命令行运行\n");
|
||
}
|
||
|
||
define('APP_PATH', __DIR__ . '/../../../application/');
|
||
define('ROOT_PATH', __DIR__ . '/../../../');
|
||
|
||
if (!is_file(APP_PATH . 'admin/command/Install/install.lock')) {
|
||
fwrite(STDERR, "项目未安装,无法连接数据库。\n");
|
||
exit(1);
|
||
}
|
||
|
||
require ROOT_PATH . 'thinkphp/base.php';
|
||
\think\App::initCommon();
|
||
|
||
use think\Db;
|
||
|
||
$argv = $GLOBALS['argv'] ?? [];
|
||
$command = $argv[1] ?? 'summary';
|
||
|
||
switch ($command) {
|
||
case 'summary':
|
||
printSummary();
|
||
break;
|
||
case 'anomalies':
|
||
printAnomalies();
|
||
break;
|
||
case 'order':
|
||
$sn = $argv[2] ?? '';
|
||
if ($sn === '') {
|
||
fwrite(STDERR, "请指定订单号: order {order_sn}\n");
|
||
exit(1);
|
||
}
|
||
printOrderDetail($sn);
|
||
break;
|
||
case 'recent-success':
|
||
$limit = max(1, min(100, intval($argv[2] ?? 20)));
|
||
printRecentSuccess($limit);
|
||
break;
|
||
case 'diagnose-missing':
|
||
$limit = max(1, min(50, intval($argv[2] ?? 20)));
|
||
diagnoseMissingSplitOrders($limit);
|
||
break;
|
||
case 'try-split':
|
||
$sn = $argv[2] ?? '';
|
||
$execute = in_array('--execute', $argv, true);
|
||
trySplitOrder($sn, $execute);
|
||
break;
|
||
default:
|
||
echo "未知命令: {$command}\n";
|
||
echo "可用: summary | anomalies | order {order_sn} | recent-success [limit] | diagnose-missing [limit] | try-split {order_sn} --execute\n";
|
||
exit(1);
|
||
}
|
||
|
||
function tableHasColumn($table, $column)
|
||
{
|
||
static $cache = [];
|
||
$key = $table . '.' . $column;
|
||
if (array_key_exists($key, $cache)) {
|
||
return $cache[$key];
|
||
}
|
||
$full = config('database.prefix') . $table;
|
||
try {
|
||
$col = str_replace(['`', "'"], '', $column);
|
||
$rows = Db::query("SHOW COLUMNS FROM `{$full}` LIKE '{$col}'");
|
||
} catch (\Exception $e) {
|
||
$cache[$key] = false;
|
||
return false;
|
||
}
|
||
$cache[$key] = !empty($rows);
|
||
return $cache[$key];
|
||
}
|
||
|
||
/** 按订单查区域佣金(兼容无 order_id 字段的旧表结构) */
|
||
function selectRegionalRewardsByOrderId($orderId)
|
||
{
|
||
if (tableHasColumn('shopro_regional_agent_reward', 'order_id')) {
|
||
return Db::name('shopro_regional_agent_reward')->where('order_id', $orderId)->select();
|
||
}
|
||
return Db::name('shopro_regional_agent_reward')->alias('rr')
|
||
->join('shopro_regional_agent_order ro', 'ro.id = rr.regional_order_id')
|
||
->where('ro.order_id', $orderId)
|
||
->field('rr.*')
|
||
->select();
|
||
}
|
||
|
||
/** 订单是否存在分销/区域佣金(用于筛选) */
|
||
function sqlOrderHasAnyReward($orderIdExpr = 'o.id')
|
||
{
|
||
$pfx = config('database.prefix');
|
||
$dist = "EXISTS (SELECT 1 FROM {$pfx}shopro_commission_reward cr WHERE cr.order_id = {$orderIdExpr} AND cr.status >= 0)";
|
||
if (tableHasColumn('shopro_regional_agent_reward', 'order_id')) {
|
||
$reg = "EXISTS (SELECT 1 FROM {$pfx}shopro_regional_agent_reward rr WHERE rr.order_id = {$orderIdExpr} AND rr.status >= 0)";
|
||
} else {
|
||
$reg = "EXISTS (SELECT 1 FROM {$pfx}shopro_regional_agent_reward rr "
|
||
. "INNER JOIN {$pfx}shopro_regional_agent_order ro ON ro.id = rr.regional_order_id "
|
||
. "WHERE ro.order_id = {$orderIdExpr} AND rr.status >= 0)";
|
||
}
|
||
return "({$dist} OR {$reg})";
|
||
}
|
||
|
||
function splitConfigMap()
|
||
{
|
||
return Db::name('shopro_split_config')->column('value', 'name');
|
||
}
|
||
|
||
function huifuConfigMap()
|
||
{
|
||
if (!Db::query("SHOW TABLES LIKE '" . config('database.prefix') . "shopro_huifu_config'")) {
|
||
return [];
|
||
}
|
||
return Db::name('shopro_huifu_config')->column('value', 'name');
|
||
}
|
||
|
||
function inferSplitPath(array $sc, array $hf)
|
||
{
|
||
$enable = ($sc['split_enable'] ?? '0') === '1';
|
||
if (!$enable) {
|
||
return '未走支付分账(split_enable=0)';
|
||
}
|
||
$mode = $sc['split_mode'] ?? 'wechat';
|
||
if ($mode === 'wallet') {
|
||
return '未走支付分账(split_mode=wallet 仅钱包)';
|
||
}
|
||
$channel = $hf['miniprogram_channel'] ?? 'yansongda';
|
||
$hfOn = ($hf['status'] ?? '0') === '1';
|
||
$provider = $sc['split_provider'] ?? 'auto';
|
||
if ($hfOn && $channel === 'huifu') {
|
||
if (in_array($mode, ['huifu', 'both'], true) || in_array($provider, ['auto', 'huifu'], true)) {
|
||
return '汇付延时分账(huifu_wechat 订单,确认时 acct_split_bunch)';
|
||
}
|
||
}
|
||
if (in_array($mode, ['wechat', 'both'], true)) {
|
||
return '微信分账(pay_type=wechat 订单)';
|
||
}
|
||
return '分账已开,请结合 pay_type 与 split_mode/provider 人工确认';
|
||
}
|
||
|
||
function printSummary()
|
||
{
|
||
$sc = splitConfigMap();
|
||
$hf = huifuConfigMap();
|
||
$commEvent = Db::name('shopro_commission_config')->where('name', 'commission_event')->value('value');
|
||
$regEvent = Db::name('shopro_regional_agent_config')->where('name', 'commission_event')->value('value');
|
||
|
||
echo "========================================\n";
|
||
echo " 分账 & 佣金核验 · 摘要\n";
|
||
echo "========================================\n\n";
|
||
|
||
echo "【分账配置】\n";
|
||
foreach (['split_enable', 'split_mode', 'split_provider', 'split_timing', 'split_amount_source', 'split_max_rate', 'delay_acct_enable', 'platform_huifu_id'] as $k) {
|
||
echo " {$k}: " . ($sc[$k] ?? '(未配置)') . "\n";
|
||
}
|
||
echo "\n【汇付】\n";
|
||
foreach (['status', 'miniprogram_channel', 'huifu_id', 'delay_acct_enable'] as $k) {
|
||
echo " {$k}: " . ($hf[$k] ?? '(无表/未配置)') . "\n";
|
||
}
|
||
echo "\n【佣金结算事件】\n";
|
||
echo " 分销 commission_event: " . ($commEvent ?: 'confirm') . "\n";
|
||
echo " 区域 regional commission_event: " . ($regEvent ?: 'confirm') . "\n";
|
||
echo " 分账 split_timing: " . ($sc['split_timing'] ?? 'payed') . "\n";
|
||
if (($sc['split_timing'] ?? 'payed') !== ($commEvent ?: 'confirm')) {
|
||
echo " ⚠ 分账时机与分销结算事件不一致,可能出现「先网关分账、后钱包入账」\n";
|
||
}
|
||
|
||
echo "\n【推断】\n";
|
||
echo " " . inferSplitPath($sc, $hf) . "\n";
|
||
|
||
echo "\n【汇付延时分账支付标记(delay_acct_flag)】\n";
|
||
$delayEnable = ($sc['delay_acct_enable'] ?? '0') === '1';
|
||
$delayAtPay = $delayEnable && (
|
||
in_array($sc['split_mode'] ?? '', ['huifu', 'both'], true)
|
||
|| ($sc['split_provider'] ?? 'auto') === 'huifu'
|
||
);
|
||
echo " delay_acct_enable(分账配置): " . ($delayEnable ? '1' : '0') . "\n";
|
||
echo " 下单时 delay_acct_flag=Y: " . ($delayAtPay ? '是(HuifuConfig::shouldDelayAcct)' : '否') . "\n";
|
||
if (($sc['split_enable'] ?? '0') === '1' && ($hf['miniprogram_channel'] ?? '') === 'huifu' && !$delayAtPay) {
|
||
echo " ⚠ 小程序走汇付但支付未开延时分账:延时分账确认(acct_split_bunch)可能无法成功\n";
|
||
echo " 建议: delay_acct_enable=1,且 split_mode=huifu/both 或 split_provider=huifu\n";
|
||
}
|
||
|
||
echo "\n【代码行为说明】\n";
|
||
echo " · 网关分账金额来自 commission/regional reward 或 split_rule\n";
|
||
echo " · 汇付:推广员/区域 huifu_id 收 div_amt,余款进 platform_huifu_id 或主商户 huifu_id\n";
|
||
echo " · 业务佣金结算仍写 shopro_user_wallet_log(commission_income / regional_agent_income)\n";
|
||
echo " · 汇付/微信分账成功:佣金标记已结算且不入 user.money;分账失败则回退入钱包\n";
|
||
|
||
$since = time() - 90 * 86400;
|
||
$stats = Db::name('shopro_split_order')->alias('so')
|
||
->join('shopro_order o', 'o.id = so.order_id')
|
||
->where('o.paytime', '>=', $since)
|
||
->field('so.channel, so.status, COUNT(*) cnt, SUM(so.split_amount) split_sum')
|
||
->group('so.channel, so.status')
|
||
->select();
|
||
echo "\n【近90天分账订单】\n";
|
||
if (empty($stats)) {
|
||
echo " (无记录)\n";
|
||
} else {
|
||
foreach ($stats as $row) {
|
||
echo " channel={$row['channel']} status={$row['status']} cnt={$row['cnt']} split_sum={$row['split_sum']}\n";
|
||
}
|
||
}
|
||
}
|
||
|
||
function printAnomalies()
|
||
{
|
||
$since = time() - 90 * 86400;
|
||
echo "========================================\n";
|
||
echo " 异常检测(近90天)\n";
|
||
echo "========================================\n\n";
|
||
|
||
$pfx = config('database.prefix');
|
||
$double = Db::query("
|
||
SELECT so.order_sn, so.split_amount, wl.wallet_sum
|
||
FROM {$pfx}shopro_split_order so
|
||
INNER JOIN {$pfx}shopro_order o ON o.id = so.order_id
|
||
INNER JOIN (
|
||
SELECT order_id, SUM(wallet_sum) AS wallet_sum FROM (
|
||
SELECT cr.order_id, SUM(wl.wallet) AS wallet_sum
|
||
FROM {$pfx}shopro_commission_reward cr
|
||
INNER JOIN {$pfx}shopro_user_wallet_log wl
|
||
ON wl.item_id = cr.id AND wl.type = 'commission_income' AND wl.wallet_type = 'money'
|
||
WHERE cr.status = 1
|
||
GROUP BY cr.order_id
|
||
UNION ALL
|
||
SELECT rr.order_id, SUM(wl.wallet)
|
||
FROM {$pfx}shopro_regional_agent_reward rr
|
||
INNER JOIN {$pfx}shopro_user_wallet_log wl
|
||
ON wl.item_id = rr.id AND wl.type = 'regional_agent_income' AND wl.wallet_type = 'money'
|
||
WHERE rr.status = 1
|
||
GROUP BY rr.order_id
|
||
) t GROUP BY order_id
|
||
) wl ON wl.order_id = so.order_id
|
||
WHERE so.status IN (2,3) AND so.split_amount > 0 AND wl.wallet_sum > 0
|
||
AND o.paytime >= ?
|
||
ORDER BY so.id DESC
|
||
LIMIT 30
|
||
", [$since]);
|
||
|
||
echo "【可能双重发放】网关分账成功且钱包有佣金流水(最多30条)\n";
|
||
if (empty($double)) {
|
||
echo " ✓ 未发现\n\n";
|
||
} else {
|
||
foreach ($double as $r) {
|
||
echo " {$r['order_sn']} split={$r['split_amount']} wallet={$r['wallet_sum']}\n";
|
||
}
|
||
echo "\n";
|
||
}
|
||
|
||
$failed = Db::name('shopro_split_order')->where('status', 4)->order('id desc')->limit(15)->select();
|
||
echo "【分账失败 status=4】\n";
|
||
if (empty($failed)) {
|
||
echo " ✓ 无\n\n";
|
||
} else {
|
||
foreach ($failed as $r) {
|
||
$msg = mb_substr($r['wx_result'] ?? '', 0, 80);
|
||
echo " {$r['order_sn']} channel={$r['channel']} retry={$r['retry_count']} {$msg}\n";
|
||
}
|
||
echo "\n";
|
||
}
|
||
|
||
$missing = Db::name('shopro_order')->alias('o')
|
||
->where('o.pay_type', 'huifu_wechat')
|
||
->where('o.paytime', '>=', $since)
|
||
->whereNotExists(function ($q) {
|
||
$q->name('shopro_split_order')->alias('so')
|
||
->whereRaw('so.order_id = o.id')
|
||
->where('so.channel', 'huifu')
|
||
->where('so.status', 'in', [1, 2, 3]);
|
||
})
|
||
->whereExists(function ($q) {
|
||
$q->name('shopro_commission_reward')->alias('cr')
|
||
->whereRaw('cr.order_id = o.id')
|
||
->where('cr.status', '>=', 0);
|
||
})
|
||
->field('o.id, o.order_sn, o.pay_fee')
|
||
->limit(20)
|
||
->select();
|
||
|
||
echo "【汇付单有分销佣金但无成功/进行中分账记录】\n";
|
||
if (empty($missing)) {
|
||
echo " ✓ 未发现(或分账未开启/无佣金)\n";
|
||
} else {
|
||
foreach ($missing as $r) {
|
||
echo " {$r['order_sn']} pay_fee={$r['pay_fee']}\n";
|
||
}
|
||
echo "\n → 执行 diagnose-missing 查看每笔未分账原因\n";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @return array<int, array<string, mixed>>
|
||
*/
|
||
function fetchMissingSplitOrders($since, $limit = 20)
|
||
{
|
||
return Db::name('shopro_order')->alias('o')
|
||
->where('o.pay_type', 'huifu_wechat')
|
||
->where('o.paytime', '>=', $since)
|
||
->whereNotExists(function ($q) {
|
||
$q->name('shopro_split_order')->alias('so')
|
||
->whereRaw('so.order_id = o.id')
|
||
->where('so.channel', 'huifu')
|
||
->where('so.status', 'in', [1, 2, 3]);
|
||
})
|
||
->whereRaw(sqlOrderHasAnyReward('o.id'))
|
||
->field('o.id, o.order_sn, o.pay_fee, o.pay_type, o.paytime, o.transaction_id')
|
||
->order('o.id desc')
|
||
->limit($limit)
|
||
->select();
|
||
}
|
||
|
||
function diagnoseOrderSplitBlockers($orderId)
|
||
{
|
||
$reasons = [];
|
||
$order = Db::name('shopro_order')->where('id', $orderId)->find();
|
||
if (!$order) {
|
||
return ['订单不存在'];
|
||
}
|
||
if ($order['pay_type'] !== 'huifu_wechat') {
|
||
$reasons[] = "pay_type={$order['pay_type']},非 huifu_wechat,汇付分账不会执行";
|
||
}
|
||
$sc = splitConfigMap();
|
||
if (($sc['split_enable'] ?? '0') !== '1') {
|
||
$reasons[] = '当前 split_enable=0(若支付时也为关,则当时不会分账)';
|
||
}
|
||
|
||
$ext = Db::name('shopro_huifu_pay_ext')->where('order_id', $orderId)->find();
|
||
if (!$ext) {
|
||
$reasons[] = '无 shopro_huifu_pay_ext 记录,可能非汇付支付或扩展未写入';
|
||
} elseif (($ext['delay_acct_flag'] ?? 'N') !== 'Y') {
|
||
$reasons[] = '支付时 delay_acct_flag≠Y(历史配置未开延时分账),补跑 confirm 可能被汇付拒绝';
|
||
}
|
||
|
||
$calcItems = [];
|
||
if (class_exists('\\addons\\shopro\\library\\split\\SplitCalculator')) {
|
||
$calcItems = (new \addons\shopro\library\split\SplitCalculator())->calculate($orderId);
|
||
}
|
||
if (empty($calcItems)) {
|
||
$reasons[] = 'SplitCalculator 结果为空(最常见:推广员/区域无 channel=huifu 的分账接收方)';
|
||
$rewards = Db::name('shopro_commission_reward')
|
||
->where('order_id', $orderId)
|
||
->where('status', '>=', 0)
|
||
->field('id, commission, agent_id')
|
||
->select();
|
||
foreach ($rewards as $rw) {
|
||
// shopro_commission_agent 主键为 user_id,reward.agent_id 即分销商 user_id
|
||
$uid = intval($rw['agent_id']);
|
||
$recv = Db::name('shopro_split_receiver')
|
||
->where('role_type', 'promoter')
|
||
->where('role_id', $uid)
|
||
->where('channel', 'huifu')
|
||
->where('status', 'normal')
|
||
->find();
|
||
$onboard = Db::name('shopro_huifu_onboard')
|
||
->where('user_id', $uid)
|
||
->where('role_type', 'promoter')
|
||
->order('id desc')
|
||
->find();
|
||
$ob = $onboard ? ($onboard['onboard_status'] . '/' . ($onboard['huifu_id'] ?: '-')) : '无进件';
|
||
$rc = $recv ? $recv['receiver_account'] : '无 receiver';
|
||
$reasons[] = " 推广员 user_id={$uid} 佣金={$rw['commission']} 进件={$ob} split_receiver={$rc}";
|
||
}
|
||
$regRewards = selectRegionalRewardsByOrderId($orderId);
|
||
$regRewards = array_filter($regRewards, function ($rw) {
|
||
return intval($rw['status']) >= 0;
|
||
});
|
||
foreach ($regRewards as $rw) {
|
||
$uid = $rw['agent_user_id'];
|
||
$recv = Db::name('shopro_split_receiver')
|
||
->where('role_type', 'regional_agent')
|
||
->where('role_id', $uid)
|
||
->where('channel', 'huifu')
|
||
->where('status', 'normal')
|
||
->find();
|
||
$onboard = Db::name('shopro_huifu_onboard')
|
||
->where('user_id', $uid)
|
||
->where('role_type', 'regional_agent')
|
||
->order('id desc')
|
||
->find();
|
||
$ob = $onboard ? ($onboard['onboard_status'] . '/' . ($onboard['huifu_id'] ?: '-')) : '无进件';
|
||
$rc = $recv ? $recv['receiver_account'] : '无 receiver';
|
||
$reasons[] = " 区域 user_id={$uid} 佣金={$rw['commission']} 进件={$ob} split_receiver={$rc}";
|
||
}
|
||
} else {
|
||
$sum = array_sum(array_column($calcItems, 'amount'));
|
||
$maxRate = floatval($sc['split_max_rate'] ?? 30);
|
||
$reasons[] = '当前可算出分账明细 ' . count($calcItems) . ' 条,合计 ' . $sum . ' 元(受 split_max_rate=' . $maxRate . '% 与 pay_fee 上限约束)';
|
||
foreach ($calcItems as $item) {
|
||
$uid = intval($item['role_id'] ?? 0);
|
||
$acct = $item['receiver_account'] ?? '-';
|
||
$onboardHint = '';
|
||
if ($uid > 0 && ($item['role_type'] ?? '') === 'promoter') {
|
||
$onboard = Db::name('shopro_huifu_onboard')
|
||
->where('user_id', $uid)
|
||
->where('role_type', 'promoter')
|
||
->order('id desc')
|
||
->find();
|
||
if ($onboard) {
|
||
$onboardHint = ' 进件=' . ($onboard['onboard_status'] ?? '') . '/' . ($onboard['huifu_id'] ?: '-');
|
||
if (($onboard['huifu_id'] ?? '') !== '' && $onboard['huifu_id'] !== $acct) {
|
||
$onboardHint .= ' ⚠与 receiver 不一致';
|
||
}
|
||
} else {
|
||
$onboardHint = ' 进件=无';
|
||
}
|
||
}
|
||
$reasons[] = sprintf(
|
||
' → %s user_id=%s huifu_id=%s 金额=%s%s',
|
||
$item['role_type'] ?? '-',
|
||
$uid ?: '-',
|
||
$acct,
|
||
number_format((float) ($item['amount'] ?? 0), 2, '.', ''),
|
||
$onboardHint
|
||
);
|
||
}
|
||
$payFee = number_format((float) $order['pay_fee'], 2, '.', '');
|
||
$payHuifuId = trim((string) Db::name('shopro_huifu_config')->where('name', 'huifu_id')->value('value'));
|
||
$platformId = trim((string) ($sc['platform_huifu_id'] ?? ''));
|
||
if ($platformId === '') {
|
||
$platformId = $payHuifuId;
|
||
}
|
||
$remain = bcsub($payFee, number_format($sum, 2, '.', ''), 2);
|
||
if (bccomp($remain, '0.00', 2) > 0) {
|
||
if ($platformId !== '' && $platformId === $payHuifuId) {
|
||
$reasons[] = ' → 主商户留存(不写 acct_infos)huifu_id=' . $payHuifuId . ' 金额=' . $remain;
|
||
} else {
|
||
$reasons[] = ' → 平台分账 huifu_id=' . $platformId . ' 金额=' . $remain;
|
||
}
|
||
}
|
||
}
|
||
|
||
$walletCnt = 0;
|
||
$crIds = Db::name('shopro_commission_reward')->where('order_id', $orderId)->where('status', 1)->column('id');
|
||
$rrIds = array_column(array_filter(selectRegionalRewardsByOrderId($orderId), function ($r) {
|
||
return intval($r['status']) === 1;
|
||
}), 'id');
|
||
$allIds = array_merge($crIds ?: [], $rrIds ?: []);
|
||
if ($allIds) {
|
||
$walletCnt = Db::name('shopro_user_wallet_log')
|
||
->where('item_id', 'in', $allIds)
|
||
->where('wallet_type', 'money')
|
||
->where('type', 'in', ['commission_income', 'regional_agent_income'])
|
||
->count();
|
||
}
|
||
if ($walletCnt > 0) {
|
||
$reasons[] = "佣金已入钱包({$walletCnt} 条流水)→ 资金目前在用户余额,非汇付分账账户";
|
||
}
|
||
|
||
return $reasons;
|
||
}
|
||
|
||
function printTrySplitChannelHints(array $order)
|
||
{
|
||
if (!class_exists('\\addons\\shopro_huifu\\library\\HuifuQueryService')) {
|
||
return;
|
||
}
|
||
echo "\n【汇付查单】\n";
|
||
try {
|
||
$query = (new \addons\shopro_huifu\library\HuifuQueryService())->queryByOrderSn($order['order_sn']);
|
||
$keys = ['trans_stat', 'delay_acct_flag', 'unconfirm_amt', 'fund_freeze_stat', 'unfreeze_time'];
|
||
foreach ($keys as $key) {
|
||
if (isset($query[$key]) && $query[$key] !== '' && $query[$key] !== null) {
|
||
echo " · {$key}=" . $query[$key] . "\n";
|
||
}
|
||
}
|
||
$freeze = strtoupper(trim((string) ($query['fund_freeze_stat'] ?? '')));
|
||
if ($freeze === 'FREEZE') {
|
||
echo " → 仍 FREEZE:请先启用 shopro_wechat_trade 并完成发货/微信确认收货,勿 --execute\n";
|
||
}
|
||
} catch (\Throwable $e) {
|
||
echo ' · 查单失败: ' . $e->getMessage() . "\n";
|
||
}
|
||
|
||
$latest = Db::name('shopro_split_order')
|
||
->where('order_id', $order['id'])
|
||
->where('channel', 'huifu')
|
||
->order('id desc')
|
||
->find();
|
||
if ($latest && intval($latest['status']) === 4 && !empty($latest['wx_result'])) {
|
||
$raw = is_string($latest['wx_result']) ? $latest['wx_result'] : json_encode($latest['wx_result'], JSON_UNESCAPED_UNICODE);
|
||
if (strpos($raw, '22000002') !== false || strpos($raw, '分账配置') !== false) {
|
||
echo "\n【最近失败】id={$latest['id']} → 22000002:若已部署「主商户不写 acct_infos」仍失败,请在控台绑定子商户分账关系\n";
|
||
} elseif (strpos($raw, 'FREEZE') !== false) {
|
||
echo "\n【最近失败】id={$latest['id']} → 资金冻结,见上查单\n";
|
||
}
|
||
}
|
||
|
||
$payHuifuId = Db::name('shopro_huifu_config')->where('name', 'huifu_id')->value('value');
|
||
if ($payHuifuId) {
|
||
echo " · 插件支付 huifu_id={$payHuifuId}(应与 platform_huifu_id / 控台主商户一致)\n";
|
||
}
|
||
|
||
printHuifuAcctInfosPreview($order);
|
||
}
|
||
|
||
function printHuifuAcctInfosPreview(array $order)
|
||
{
|
||
if (!class_exists('\\addons\\shopro\\library\\split\\SplitCalculator')
|
||
|| !class_exists('\\addons\\shopro_huifu\\library\\HuifuSplitService')) {
|
||
return;
|
||
}
|
||
$items = (new \addons\shopro\library\split\SplitCalculator())->calculate($order['id']);
|
||
if (empty($items)) {
|
||
return;
|
||
}
|
||
$service = new \addons\shopro_huifu\library\HuifuSplitService();
|
||
$payFee = number_format((float) $order['pay_fee'], 2, '.', '');
|
||
$confirmBase = $service->previewConfirmPayFee($order['order_sn'], $payFee);
|
||
$ref = new \ReflectionClass($service);
|
||
$method = $ref->getMethod('buildAcctInfos');
|
||
$method->setAccessible(true);
|
||
try {
|
||
$acctInfos = $method->invoke($service, $items, $confirmBase);
|
||
} catch (\Throwable $e) {
|
||
echo "\n【acct_infos 预览】组装失败: " . $e->getMessage() . "\n";
|
||
return;
|
||
}
|
||
echo "\n【将提交汇付 acct_infos】confirm_base={$confirmBase}\n";
|
||
foreach ($acctInfos as $row) {
|
||
echo ' · huifu_id=' . ($row['huifu_id'] ?? '-') . ' div_amt=' . ($row['div_amt'] ?? '-') . "\n";
|
||
}
|
||
$splitSum = '0.00';
|
||
foreach ($acctInfos as $row) {
|
||
$splitSum = bcadd($splitSum, number_format((float) ($row['div_amt'] ?? 0), 2, '.', ''), 2);
|
||
}
|
||
$stay = bcsub($confirmBase, $splitSum, 2);
|
||
if (bccomp($stay, '0.00', 2) > 0) {
|
||
echo " · (其余 {$stay} 元留在收款主商户,不写入分账串)\n";
|
||
}
|
||
}
|
||
|
||
function diagnoseMissingSplitOrders($limit)
|
||
{
|
||
$since = time() - 90 * 86400;
|
||
$rows = fetchMissingSplitOrders($since, $limit);
|
||
echo "========================================\n";
|
||
echo " 未分账订单诊断(近90天,最多 {$limit} 笔)\n";
|
||
echo "========================================\n\n";
|
||
if (empty($rows)) {
|
||
echo "无匹配订单。\n";
|
||
return;
|
||
}
|
||
foreach ($rows as $row) {
|
||
echo str_repeat('-', 60) . "\n";
|
||
echo "订单: {$row['order_sn']} id={$row['id']} pay_fee={$row['pay_fee']}\n";
|
||
echo '支付: ' . date('Y-m-d H:i:s', $row['paytime']) . " pay_type={$row['pay_type']}\n";
|
||
foreach (diagnoseOrderSplitBlockers($row['id']) as $line) {
|
||
echo " · {$line}\n";
|
||
}
|
||
echo "\n";
|
||
}
|
||
echo "补跑(需 delay_acct_flag=Y 且接收方齐全):\n";
|
||
echo " php addons/shopro/tests/split_commission_verify.php try-split {order_sn} --execute\n";
|
||
}
|
||
|
||
function trySplitOrder($orderSn, $execute)
|
||
{
|
||
if ($orderSn === '') {
|
||
fwrite(STDERR, "请指定订单号\n");
|
||
exit(1);
|
||
}
|
||
$order = Db::name('shopro_order')->where('order_sn', $orderSn)->find();
|
||
if (!$order) {
|
||
fwrite(STDERR, "订单不存在\n");
|
||
exit(1);
|
||
}
|
||
echo "诊断:\n";
|
||
foreach (diagnoseOrderSplitBlockers($order['id']) as $line) {
|
||
echo " · {$line}\n";
|
||
}
|
||
printTrySplitChannelHints($order);
|
||
if (!$execute) {
|
||
echo "\n未执行分账。确认汇付控台分账关系 + 非 FREEZE 后再加 --execute\n";
|
||
echo "(本单若佣金已入钱包,--execute 成功可能造成测试单双轨,正式单请先对齐 split_timing 与分销结算)\n";
|
||
return;
|
||
}
|
||
if (!class_exists('\\addons\\shopro_huifu\\library\\HuifuSplitService')) {
|
||
fwrite(STDERR, "未安装 shopro_huifu\n");
|
||
exit(1);
|
||
}
|
||
$service = new \addons\shopro_huifu\library\HuifuSplitService();
|
||
$payFee = number_format((float) $order['pay_fee'], 2, '.', '');
|
||
$confirmBase = $service->previewConfirmPayFee($order['order_sn'], $payFee);
|
||
echo "\n汇付确认基数: pay_fee={$payFee} → confirm_base={$confirmBase}";
|
||
if (bccomp($confirmBase, $payFee, 2) < 0) {
|
||
echo "(已扣手续费/待确认金额)";
|
||
}
|
||
echo "\n";
|
||
|
||
try {
|
||
$query = (new \addons\shopro_huifu\library\HuifuQueryService())->queryByOrderSn($order['order_sn']);
|
||
$snippet = [
|
||
'trans_amt' => $query['trans_amt'] ?? null,
|
||
'unconfirm_amt' => $query['unconfirm_amt'] ?? null,
|
||
'settlement_amt' => $query['settlement_amt'] ?? null,
|
||
'delay_acct_flag' => $query['delay_acct_flag'] ?? null,
|
||
'fund_freeze_stat' => $query['fund_freeze_stat'] ?? null,
|
||
'freeze_time' => $query['freeze_time'] ?? null,
|
||
'unfreeze_time' => $query['unfreeze_time'] ?? null,
|
||
'trans_stat' => $query['trans_stat'] ?? null,
|
||
];
|
||
echo '查单摘要: ' . json_encode($snippet, JSON_UNESCAPED_UNICODE) . "\n";
|
||
} catch (\Throwable $e) {
|
||
echo '查单摘要: 失败 ' . $e->getMessage() . "\n";
|
||
}
|
||
|
||
$ok = $service->executeByOrderId($order['id']);
|
||
if ($ok) {
|
||
echo "\n分账执行返回: 成功\n";
|
||
return;
|
||
}
|
||
|
||
$latest = Db::name('shopro_split_order')
|
||
->where('order_id', $order['id'])
|
||
->where('channel', 'huifu')
|
||
->order('id', 'desc')
|
||
->find();
|
||
echo "\n分账执行返回: 失败\n";
|
||
if ($latest) {
|
||
echo "最新分账记录 id={$latest['id']} status={$latest['status']} split_amount={$latest['split_amount']}\n";
|
||
if (!empty($latest['wx_result'])) {
|
||
echo "wx_result: {$latest['wx_result']}\n";
|
||
}
|
||
} else {
|
||
echo "未写入 shopro_split_order(可能明细为空或 pay_type 非 huifu_wechat)\n";
|
||
}
|
||
}
|
||
|
||
function printOrderDetail($orderSn)
|
||
{
|
||
$order = Db::name('shopro_order')->where('order_sn', $orderSn)->find();
|
||
if (!$order) {
|
||
echo "订单不存在: {$orderSn}\n";
|
||
exit(1);
|
||
}
|
||
$oid = $order['id'];
|
||
echo "订单 {$orderSn} id={$oid} pay_type={$order['pay_type']} pay_fee={$order['pay_fee']}\n\n";
|
||
|
||
$splits = Db::name('shopro_split_order')->where('order_id', $oid)->select();
|
||
echo "【分账主表】\n";
|
||
print_r($splits);
|
||
|
||
if ($splits) {
|
||
$ids = array_column($splits, 'id');
|
||
$lines = Db::name('shopro_split_order_receiver')->alias('sor')
|
||
->join('shopro_split_receiver sr', 'sr.id = sor.receiver_id', 'LEFT')
|
||
->where('sor.split_order_id', 'in', $ids)
|
||
->field('sor.*, sr.receiver_account, sr.channel, sr.receiver_type')
|
||
->select();
|
||
echo "\n【分账明细 → 接收方账户(网关去向)】\n";
|
||
print_r($lines);
|
||
}
|
||
|
||
$cr = Db::name('shopro_commission_reward')->where('order_id', $oid)->select();
|
||
$rr = selectRegionalRewardsByOrderId($oid);
|
||
echo "\n【分销/区域佣金记录(业务层)】\n";
|
||
echo "commission_reward: " . count($cr) . " 条\n";
|
||
echo "regional_reward: " . count($rr) . " 条\n";
|
||
|
||
$rewardIds = array_merge(
|
||
array_column($cr, 'id'),
|
||
array_column($rr, 'id')
|
||
);
|
||
$wl = [];
|
||
if ($rewardIds) {
|
||
$wl = Db::name('shopro_user_wallet_log')
|
||
->where('item_id', 'in', $rewardIds)
|
||
->where('wallet_type', 'money')
|
||
->where('type', 'in', ['commission_income', 'regional_agent_income', 'commission_back', 'regional_agent_back'])
|
||
->select();
|
||
}
|
||
echo "\n【钱包流水(用户可提现余额来源)】\n";
|
||
print_r($wl);
|
||
|
||
$ext = Db::name('shopro_huifu_pay_ext')->where('order_id', $oid)->find();
|
||
if ($ext) {
|
||
echo "\n【汇付支付扩展】\n";
|
||
print_r($ext);
|
||
}
|
||
|
||
echo "\n【分账阻塞诊断】\n";
|
||
foreach (diagnoseOrderSplitBlockers($oid) as $line) {
|
||
echo " · {$line}\n";
|
||
}
|
||
}
|
||
|
||
function printRecentSuccess($limit)
|
||
{
|
||
$rows = Db::name('shopro_split_order')->alias('so')
|
||
->join('shopro_order o', 'o.id = so.order_id')
|
||
->where('so.status', 'in', [2, 3])
|
||
->field('so.order_sn, so.channel, so.total_amount, so.split_amount, so.createtime, o.pay_type')
|
||
->order('so.id desc')
|
||
->limit($limit)
|
||
->select();
|
||
echo "最近 {$limit} 笔分账成功订单:\n";
|
||
foreach ($rows as $r) {
|
||
$remain = round($r['total_amount'] - $r['split_amount'], 2);
|
||
echo date('Y-m-d H:i', $r['createtime']) . " {$r['order_sn']} {$r['channel']} pay={$r['total_amount']} split={$r['split_amount']} platform_remain≈{$remain}\n";
|
||
}
|
||
}
|