feat(shopro_huifu): 新增汇付冻结链路诊断命令

新增 DiagnoseFreezeFlow 控制台命令用于诊断小程序发货管理 +
汇付延时分账的冻结/解冻链路问题。该命令提供只读诊断功能,
包含以下检查项:
- 商城自动确认收货配置检查
- 汇付延时分账时机配置检查
- 微信交易合规插件状态检查
- 微信发货/确认同步统计
- 最近失败的微信同步记录
- 已发货但未成功上传物流信息的订单
- 已收货但未成功确认的订单
- 分账失败/待重试记录
- 分账时机与分销结算时机匹配性检查

同时添加了对应的 SQL 诊断脚本和命令注册。
```
这个提交包含在:
可乐加冰
2026-10-08 13:49:26 +08:00
父节点 fa8d57693b
当前提交 73efa1a7eb
共有 3 个文件被更改,包括 363 次插入 和 0 次删除

查看文件

@@ -0,0 +1,319 @@
<?php
namespace addons\shopro_huifu\command;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\Db;
/**
* 小程序发货管理 + 汇付延时分账:冻结/解冻链路只读诊断(需在已配置 .env 的服务器执行)
*/
class DiagnoseFreezeFlow extends Command
{
protected function configure()
{
$this->setName('shopro_huifu:diagnose-freeze-flow')
->addOption('days', null, Option::VALUE_OPTIONAL, '统计近 N 天订单,默认 30', 30)
->addOption('limit', null, Option::VALUE_OPTIONAL, '明细条数上限,默认 15', 15)
->setDescription('Diagnose order_auto_confirm, wechat shipping sync, split_timing vs FREEZE/23000004');
}
protected function execute(Input $input, Output $output)
{
$days = max(1, intval($input->getOption('days')));
$limit = max(5, intval($input->getOption('limit')));
$since = time() - ($days * 86400);
$output->writeln('=== 汇付/微信发货冻结链路诊断(只读)===');
$output->writeln('统计近 ' . $days . ' 天 · 明细最多 ' . $limit . ' 条');
$output->writeln('');
$this->sectionOrderAutoConfirm($output);
$this->sectionSplitTiming($output);
$this->sectionWechatTrade($output);
if ($this->tableExists('shopro_wechat_trade_sync')) {
$this->sectionWechatSyncStats($output, $since);
$this->sectionWechatSyncFailures($output, $since, $limit);
$this->sectionShippedNoWechatSync($output, $since, $limit);
$this->sectionConfirmGap($output, $since, $limit);
} else {
$output->writeln('[SKIP] 表 shopro_wechat_trade_sync 不存在 → 请安装/启用 shopro_wechat_trade');
$output->writeln(' php think shopro_wechat_trade:install');
}
if ($this->tableExists('shopro_split_order')) {
$this->sectionSplitFailures($output, $since, $limit);
$this->sectionTimingMismatch($output);
}
$output->writeln('');
$output->writeln('=== 解读提示 ===');
$output->writeln('· 小程序实物快递:须真实发货 + upload_shipping_info 成功,再 confirm(用户/自动收货)');
$output->writeln('· split_timing=payed + 延时分账 + 微信 FREEZE → 易出现 23000004,建议改为 confirm 或 finish');
$output->writeln('· 失败同步:php think shopro_wechat_trade:retry-failed');
$output->writeln('· 单笔汇付查单:后台分账订单详情或 split_commission_verify.php');
return 0;
}
protected function sectionOrderAutoConfirm(Output $output)
{
$output->writeln('--- 1. 商城自动确认收货 (shopro_config.name=order) ---');
if (!$this->tableExists('shopro_config')) {
$output->writeln('[FAIL] 无 shopro_config 表');
return;
}
$row = Db::name('shopro_config')->where('name', 'order')->find();
if (!$row) {
$output->writeln('[WARN] 未找到 order 配置,发货后默认 10 天自动确认(见 Send.php)');
return;
}
$cfg = json_decode($row['value'] ?? '', true);
if (!is_array($cfg)) {
$output->writeln('[WARN] order 配置非 JSON');
return;
}
$days = isset($cfg['order_auto_confirm']) ? trim((string) $cfg['order_auto_confirm']) : '';
$output->writeln('order_auto_confirm(天): ' . ($days !== '' ? $days : '(空,代码默认 10)'));
if ($days === '0' || $days === 0) {
$output->writeln('[WARN] 设为 0 不会立即确认,仍走默认 10 天');
}
$output->writeln('order_auto_close(天): ' . ($cfg['order_auto_close'] ?? '-'));
$output->writeln('');
}
protected function sectionSplitTiming(Output $output)
{
$output->writeln('--- 2. 汇付延时分账时机 ---');
if (!$this->tableExists('shopro_split_config')) {
$output->writeln('[SKIP] 无 shopro_split_config');
$output->writeln('');
return;
}
$names = [
'split_enable', 'split_mode', 'split_timing', 'delay_acct_enable', 'platform_huifu_id',
];
$rows = Db::name('shopro_split_config')->where('name', 'in', $names)->select();
$map = [];
foreach ($rows as $r) {
$map[$r['name']] = $r['value'];
}
foreach ($names as $n) {
$output->writeln($n . ': ' . ($map[$n] ?? '(未配置)'));
}
$timing = $map['split_timing'] ?? 'payed';
$delay = ($map['delay_acct_enable'] ?? '0') === '1';
$enable = ($map['split_enable'] ?? '0') === '1';
if ($enable && $delay && in_array($timing, ['payed', 'pay'], true)) {
$output->writeln('[WARN] split_timing=' . $timing . ' + delay_acct_enable=1 + 小程序 → 支付后立即分账易遇 FREEZE/23000004');
$output->writeln(' 建议:后台分账配置改为 confirm 或 finish');
}
if ($this->tableExists('shopro_huifu_config')) {
$hf = Db::name('shopro_huifu_config')->where('name', 'miniprogram_channel')->value('value');
$st = Db::name('shopro_huifu_config')->where('name', 'status')->value('value');
$output->writeln('huifu miniprogram_channel: ' . ($hf ?: '-'));
$output->writeln('huifu plugin status: ' . ($st ?: '-'));
}
$output->writeln('');
}
protected function sectionWechatTrade(Output $output)
{
$output->writeln('--- 3. 微信交易合规插件 ---');
$info = function_exists('get_addon_info') ? get_addon_info('shopro_wechat_trade') : [];
$state = intval($info['state'] ?? 0);
$output->writeln('插件 state: ' . ($state === 1 ? '1 (启用)' : $state . ' (未启用)'));
if ($this->tableExists('shopro_wechat_trade_config')) {
$en = Db::name('shopro_wechat_trade_config')->where('name', 'wechat_trade_enable')->value('value');
$output->writeln('wechat_trade_enable: ' . ($en ?? '-'));
}
$output->writeln('');
}
protected function sectionWechatSyncStats(Output $output, $since)
{
$output->writeln('--- 4. 微信发货/确认同步统计(createtime >= ' . date('Y-m-d', $since) . ')---');
$rows = Db::query(
'SELECT sync_type, status, COUNT(*) AS cnt FROM ' . $this->table('shopro_wechat_trade_sync')
. ' WHERE createtime >= ? GROUP BY sync_type, status ORDER BY sync_type, status',
[$since]
);
if (!$rows) {
$output->writeln('(无记录)');
} else {
foreach ($rows as $r) {
$output->writeln(sprintf(' %s / %s: %d', $r['sync_type'], $r['status'], $r['cnt']));
}
}
$output->writeln('');
}
protected function sectionWechatSyncFailures(Output $output, $since, $limit)
{
$output->writeln('--- 5. 最近失败的微信同步(最多 ' . $limit . ' 条)---');
$rows = Db::name('shopro_wechat_trade_sync')
->where('createtime', '>=', $since)
->where('status', '<>', 'success')
->order('id', 'desc')
->limit($limit)
->select();
if (!$rows) {
$output->writeln('(无失败/pending 记录)');
$output->writeln('');
return;
}
foreach ($rows as $r) {
$output->writeln(sprintf(
'#%d order_id=%s sn=%s type=%s status=%s err=%s %s',
$r['id'],
$r['order_id'],
$r['order_sn'],
$r['sync_type'],
$r['status'],
$r['errcode'] ?? '',
mb_substr((string) ($r['errmsg'] ?? ''), 0, 80)
));
}
$output->writeln('');
}
protected function sectionShippedNoWechatSync(Output $output, $since, $limit)
{
$output->writeln('--- 6. 已发货但未成功 upload_shipping(小程序汇付/微信,最多 ' . $limit . ' 单)---');
$prefix = config('database.prefix');
$sql = "SELECT o.id, o.order_sn, o.status, o.pay_type, MAX(oi.dispatch_status) AS max_dispatch
FROM {$prefix}shopro_order o
INNER JOIN {$prefix}shopro_order_item oi ON oi.order_id = o.id
LEFT JOIN {$prefix}shopro_wechat_trade_sync ws ON ws.order_id = o.id AND ws.sync_type = 'shipping' AND ws.status = 'success'
WHERE o.platform = 'wxMiniProgram'
AND o.pay_type IN ('wechat','huifu_wechat')
AND o.status > 0
AND o.createtime >= ?
AND oi.dispatch_status >= 1
AND ws.id IS NULL
GROUP BY o.id
ORDER BY o.id DESC
LIMIT " . intval($limit);
try {
$rows = Db::query($sql, [$since]);
} catch (\Exception $e) {
$output->writeln('[ERR] ' . $e->getMessage());
$output->writeln('');
return;
}
if (!$rows) {
$output->writeln('(未发现:已发货单均有 shipping success,或未命中条件)');
} else {
foreach ($rows as $r) {
$output->writeln(sprintf(' order#%s %s pay=%s status=%s', $r['id'], $r['order_sn'], $r['pay_type'], $r['status']));
}
$output->writeln('[ACTION] php think shopro_wechat_trade:backfill 或后台补发货同步');
}
$output->writeln('');
}
protected function sectionConfirmGap(Output $output, $since, $limit)
{
$output->writeln('--- 7. 已全部收货但未成功 notify_confirm(最多 ' . $limit . ' 单)---');
$prefix = config('database.prefix');
$got = 2; // DISPATCH_STATUS_GETED — 与 OrderItem 常量一致
$sql = "SELECT o.id, o.order_sn, o.pay_type
FROM {$prefix}shopro_order o
INNER JOIN {$prefix}shopro_order_item oi ON oi.order_id = o.id
LEFT JOIN {$prefix}shopro_wechat_trade_sync wc ON wc.order_id = o.id AND wc.sync_type = 'confirm' AND wc.status = 'success'
WHERE o.platform = 'wxMiniProgram'
AND o.pay_type IN ('wechat','huifu_wechat')
AND o.status > 0
AND o.createtime >= ?
AND wc.id IS NULL
GROUP BY o.id
HAVING MIN(oi.dispatch_status) >= {$got}
ORDER BY o.id DESC
LIMIT " . intval($limit);
try {
$rows = Db::query($sql, [$since]);
} catch (\Exception $e) {
$output->writeln('[ERR] ' . $e->getMessage());
$output->writeln('');
return;
}
if (!$rows) {
$output->writeln('(无缺口,或尚无全部收货订单)');
} else {
foreach ($rows as $r) {
$output->writeln(sprintf(' order#%s %s pay=%s', $r['id'], $r['order_sn'], $r['pay_type']));
}
$output->writeln('[ACTION] 确认 shopro_wechat_trade 已开;可在后台订单确认收货或 retry-failed');
}
$output->writeln('');
}
protected function sectionSplitFailures(Output $output, $since, $limit)
{
$output->writeln('--- 8. 分账失败/待重试(含 23000004 / FREEZE 关键词,最多 ' . $limit . ' 条)---');
$rows = Db::name('shopro_split_order')
->where('createtime', '>=', $since)
->where(function ($q) {
$q->where('status', 4)
->whereOr('wx_result', 'like', '%23000004%')
->whereOr('wx_result', 'like', '%FREEZE%')
->whereOr('wx_result', 'like', '%fund_freeze%');
})
->order('id', 'desc')
->limit($limit)
->select();
if (!$rows) {
$output->writeln('(近 ' . $limit . ' 条内无典型失败;可后台查看分账订单列表 status=4)');
$output->writeln('');
return;
}
foreach ($rows as $r) {
$snippet = mb_substr(preg_replace('/\s+/', ' ', (string) ($r['wx_result'] ?? '')), 0, 120);
$output->writeln(sprintf(
'#%d order_sn=%s status=%s amount=%s %s',
$r['id'],
$r['order_sn'] ?? '',
$r['status'] ?? '',
$r['split_amount'] ?? '',
$snippet
));
}
$output->writeln('');
}
protected function sectionTimingMismatch(Output $output)
{
$output->writeln('--- 9. 分账时机 vs 分销结算(是否「支付就分、佣金却等确认」)---');
if (!$this->tableExists('shopro_commission_config')) {
$output->writeln('[SKIP] 无 commission 配置表');
$output->writeln('');
return;
}
$splitTiming = Db::name('shopro_split_config')->where('name', 'split_timing')->value('value') ?: 'payed';
$commEvent = Db::name('shopro_commission_config')->where('name', 'commission_event')->value('value') ?: 'confirm';
$output->writeln('split_timing: ' . $splitTiming);
$output->writeln('commission_event: ' . $commEvent);
if (in_array($splitTiming, ['payed', 'pay'], true) && in_array($commEvent, ['confirm', 'finish', 'completed'], true)) {
$output->writeln('[INFO] 分账早于佣金结算事件,小程序 FREEZE 时更明显;建议 split_timing 与 commission_event 对齐为 confirm');
}
$output->writeln('');
}
protected function tableExists($name)
{
try {
Db::name($name)->limit(1)->select();
return true;
} catch (\Exception $e) {
return false;
}
}
protected function table($name)
{
return config('database.prefix') . $name;
}
}

查看文件

@@ -0,0 +1,43 @@
-- 小程序发货 + 汇付冻结链路 · 只读诊断(表前缀默认 fa_)
SET NAMES utf8mb4;
SELECT '=== order 自动确认收货 ===' AS section;
SELECT value FROM fa_shopro_config WHERE name = 'order' LIMIT 1;
SELECT '=== 分账/延时分账 ===' AS section;
SELECT name, value FROM fa_shopro_split_config
WHERE name IN ('split_enable','split_mode','split_timing','delay_acct_enable','platform_huifu_id')
ORDER BY name;
SELECT '=== 微信交易合规 ===' AS section;
SELECT name, value FROM fa_shopro_wechat_trade_config WHERE name = 'wechat_trade_enable';
SELECT '=== 近30天 微信同步统计 ===' AS section;
SELECT sync_type, status, COUNT(*) cnt
FROM fa_shopro_wechat_trade_sync
WHERE createtime >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))
GROUP BY sync_type, status;
SELECT '=== 近30天 已发货无 shipping success(前15单)===' AS section;
SELECT o.id, o.order_sn, o.pay_type
FROM fa_shopro_order o
INNER JOIN fa_shopro_order_item oi ON oi.order_id = o.id
LEFT JOIN fa_shopro_wechat_trade_sync ws
ON ws.order_id = o.id AND ws.sync_type = 'shipping' AND ws.status = 'success'
WHERE o.platform = 'wxMiniProgram'
AND o.pay_type IN ('wechat','huifu_wechat')
AND o.status > 0
AND o.createtime >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))
AND oi.dispatch_status >= 1
AND ws.id IS NULL
GROUP BY o.id
ORDER BY o.id DESC
LIMIT 15;
SELECT '=== 近30天 分账失败/23000004(前15条)===' AS section;
SELECT id, order_sn, status, split_amount, LEFT(wx_result, 200) AS wx_result_snip
FROM fa_shopro_split_order
WHERE createtime >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))
AND (status = 4 OR wx_result LIKE '%23000004%' OR wx_result LIKE '%FREEZE%')
ORDER BY id DESC
LIMIT 15;

查看文件

@@ -25,6 +25,7 @@ return [
'addons\shopro_huifu\command\TestPayConnection', 'addons\shopro_huifu\command\TestPayConnection',
'addons\shopro_huifu\command\ProbeWechatOnboarding', 'addons\shopro_huifu\command\ProbeWechatOnboarding',
'addons\shopro_huifu\command\QuerySplitConfig', 'addons\shopro_huifu\command\QuerySplitConfig',
'addons\shopro_huifu\command\DiagnoseFreezeFlow',
'addons\shopro_huifu\command\OnboardPoll', 'addons\shopro_huifu\command\OnboardPoll',
'addons\shopro_huifu\command\OnboardAutoSubmit', 'addons\shopro_huifu\command\OnboardAutoSubmit',
'addons\shopro_huifu\command\ImportBankBranch', 'addons\shopro_huifu\command\ImportBankBranch',