diff --git a/addons/shopro_huifu/command/DiagnoseFreezeFlow.php b/addons/shopro_huifu/command/DiagnoseFreezeFlow.php new file mode 100644 index 0000000..743acda --- /dev/null +++ b/addons/shopro_huifu/command/DiagnoseFreezeFlow.php @@ -0,0 +1,319 @@ +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; + } +} diff --git a/addons/shopro_huifu/docs/diagnose-freeze-flow.sql b/addons/shopro_huifu/docs/diagnose-freeze-flow.sql new file mode 100644 index 0000000..e7841c1 --- /dev/null +++ b/addons/shopro_huifu/docs/diagnose-freeze-flow.sql @@ -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; diff --git a/application/command.php b/application/command.php index afc97df..ea61c8a 100644 --- a/application/command.php +++ b/application/command.php @@ -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',