```
feat(shopro_huifu): 新增汇付冻结链路诊断命令 新增 DiagnoseFreezeFlow 控制台命令用于诊断小程序发货管理 + 汇付延时分账的冻结/解冻链路问题。该命令提供只读诊断功能, 包含以下检查项: - 商城自动确认收货配置检查 - 汇付延时分账时机配置检查 - 微信交易合规插件状态检查 - 微信发货/确认同步统计 - 最近失败的微信同步记录 - 已发货但未成功上传物流信息的订单 - 已收货但未成功确认的订单 - 分账失败/待重试记录 - 分账时机与分销结算时机匹配性检查 同时添加了对应的 SQL 诊断脚本和命令注册。 ```
这个提交包含在:
@@ -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\ProbeWechatOnboarding',
|
||||
'addons\shopro_huifu\command\QuerySplitConfig',
|
||||
'addons\shopro_huifu\command\DiagnoseFreezeFlow',
|
||||
'addons\shopro_huifu\command\OnboardPoll',
|
||||
'addons\shopro_huifu\command\OnboardAutoSubmit',
|
||||
'addons\shopro_huifu\command\ImportBankBranch',
|
||||
|
||||
在新工单中引用
屏蔽一个用户