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; } }