文件
xionggeshop/addons/shopro/tests/split_commission_verify.php
2026-09-29 11:44:50 +08:00

314 行
12 KiB
PHP

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

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

<?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]
*/
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;
default:
echo "未知命令: {$command}\n";
echo "可用: summary | anomalies | order {order_sn} | recent-success [limit]\n";
exit(1);
}
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 " · Config::shouldSkipWallet() 未接入 Reward,分账开启时仍可能同时入账钱包 → 用 anomalies 命令排查\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";
}
}
}
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 = Db::name('shopro_regional_agent_reward')->where('order_id', $oid)->select();
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);
}
}
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";
}
}