setName('shopro_huifu:verify') ->setDescription('Check shopro_huifu plugin installation (tables, menus, files)'); } protected function execute(Input $input, Output $output) { $prefix = config('database.prefix'); $root = ROOT_PATH; $pass = 0; $fail = 0; $checks = [ '入口文件 Shopro_huifu.php' => is_file($root . 'addons/shopro_huifu/Shopro_huifu.php'), 'info.ini' => is_file($root . 'addons/shopro_huifu/info.ini'), 'config/menu.php' => is_file($root . 'addons/shopro_huifu/config/menu.php'), '后台控制器(已部署)' => is_file($root . 'application/admin/controller/shopro_huifu/Config.php'), '后台控制器(插件包内)' => is_file($root . 'addons/shopro_huifu/application/admin/controller/shopro_huifu/Config.php'), '后台视图(已部署)' => is_file($root . 'application/admin/view/shopro_huifu/config/index.html'), '后台 JS(已部署)' => is_file($root . 'public/assets/js/backend/shopro_huifu/config.js'), '汇付 SDK init.php' => $this->hasSdk($root), ]; $output->writeln('=== 文件检查 ==='); foreach ($checks as $label => $ok) { $this->line($output, $label, $ok, $pass, $fail); } $output->writeln(''); $output->writeln('=== 数据库检查 ==='); $tables = ['shopro_huifu_config', 'shopro_huifu_pay_ext', 'shopro_huifu_notify_log']; foreach ($tables as $table) { $ok = $this->tableExists($prefix . $table); $this->line($output, '表 ' . $prefix . $table, $ok, $pass, $fail); } try { $cfgCount = Db::name('shopro_huifu_config')->count(); $this->line($output, '汇付配置项数量 >= 10', $cfgCount >= 10, $pass, $fail, (string) $cfgCount); } catch (\Exception $e) { $this->line($output, '汇付配置项', false, $pass, $fail, $e->getMessage()); } try { $orderTable = $prefix . 'shopro_order'; $rows = Db::query("SHOW COLUMNS FROM `{$orderTable}` LIKE 'pay_type'"); $type = $rows[0]['Type'] ?? ''; $supportsHuifu = stripos($type, 'huifu_wechat') !== false; $this->line( $output, 'shopro_order.pay_type 支持 huifu_wechat', $supportsHuifu, $pass, $fail, $type ?: '未找到 pay_type 字段' ); } catch (\Exception $e) { $this->line($output, 'shopro_order.pay_type 支持 huifu_wechat', false, $pass, $fail, $e->getMessage()); } $output->writeln(''); $output->writeln('=== 菜单与权限检查 ==='); $menuNames = [ 'shopro_huifu/config', 'shopro_huifu/config/index', 'shopro_huifu/config/save', 'shopro/split', 'shopro/split/config', 'shopro/split/config/index', 'shopro/split/receiver', 'shopro/split/rule', 'shopro/split/order', ]; foreach ($menuNames as $name) { $row = Db::name('auth_rule')->where('name', $name)->find(); $ok = (bool) $row; $extra = $row ? ('pid=' . $row['pid'] . ' status=' . $row['status'] . ' ismenu=' . $row['ismenu']) : '未找到'; $this->line($output, '菜单 ' . $name, $ok, $pass, $fail, $extra); } $legacyMenu = Db::name('auth_rule')->where('name', 'shopro_huifu')->find(); $this->line($output, '旧版菜单 shopro_huifu 已移除', !$legacyMenu, $pass, $fail, $legacyMenu ? '仍存在,需 repair' : '无'); foreach ([ 'shopro_huifu/config' => '汇付菜单父节点', 'shopro/split' => '分账菜单父节点', ] as $menuName => $label) { $row = Db::name('auth_rule')->where('name', $menuName)->find(); if ($row) { $parent = Db::name('auth_rule')->where('id', $row['pid'])->find(); $parentName = $parent['name'] ?? 'unknown'; $ok = in_array($parentName, ['shopro/config', 'shopro'], true); $this->line($output, $label . '正确', $ok, $pass, $fail, 'parent=' . $parentName); } else { $this->line($output, $label . '正确', false, $pass, $fail, '缺少 ' . $menuName); } } $splitConfig = Db::name('auth_rule')->where('name', 'shopro/split/config')->find(); if ($splitConfig && intval($splitConfig['ismenu']) === 1 && $splitConfig['status'] === 'normal') { $path = $this->getMenuPathTitles(intval($splitConfig['id'])); $this->line($output, '分账设置侧栏路径', !empty($path), $pass, $fail, implode(' -> ', $path)); } else { $this->line($output, '分账设置侧栏路径', false, $pass, $fail, '缺少 shopro/split/config'); } $huifuConfig = Db::name('auth_rule')->where('name', 'shopro_huifu/config')->find(); if ($huifuConfig && intval($huifuConfig['ismenu']) === 1 && $huifuConfig['status'] === 'normal') { $path = $this->getMenuPathTitles(intval($huifuConfig['id'])); $this->line($output, '汇付配置侧栏路径', !empty($path), $pass, $fail, implode(' -> ', $path)); } $indexRule = Db::name('auth_rule')->where('name', 'shopro_huifu/config/index')->find(); $superGroup = Db::name('auth_group')->where('rules', '*')->find(); if ($superGroup) { $this->line($output, '存在超级管理员权限组(*)', true, $pass, $fail, 'id=' . $superGroup['id']); } else { $this->line($output, '存在超级管理员权限组(*)', false, $pass, $fail); } if ($indexRule) { $groupsWithHuifu = 0; foreach (Db::name('auth_group')->where('status', 'normal')->select() as $group) { if ($group['rules'] === '*') { $groupsWithHuifu++; continue; } $owned = array_filter(array_map('trim', explode(',', (string) $group['rules']))); if (in_array((string) $indexRule['id'], $owned, true)) { $groupsWithHuifu++; } } $this->line($output, '至少1个管理组拥有汇付权限', $groupsWithHuifu > 0, $pass, $fail, (string) $groupsWithHuifu); } $iniFile = ADDON_PATH . 'shopro_huifu/info.ini'; $iniState = 0; if (is_file($iniFile)) { $ini = parse_ini_file($iniFile); $iniState = intval($ini['state'] ?? 0); } $cachedInfo = get_addon_info('shopro_huifu'); $cachedState = intval($cachedInfo['state'] ?? 0); $this->line( $output, '插件 info.ini state=1', $iniState === 1, $pass, $fail, 'info.ini state=' . $iniState ); if ($iniState === 1 && $cachedState !== 1) { $this->line( $output, 'get_addon_info 缓存与 info.ini 一致', false, $pass, $fail, '缓存 state=' . $cachedState . ',需刷新插件缓存' ); } else { $this->line( $output, 'get_addon_info 缓存与 info.ini 一致', true, $pass, $fail, '缓存 state=' . $cachedState ); } $addonsFile = ROOT_PATH . 'application/extra/addons.php'; $appInitHooks = []; if (is_file($addonsFile)) { $addonsConfig = include $addonsFile; $appInitHooks = (array) ($addonsConfig['hooks']['app_init'] ?? []); } $inAppInit = in_array('shopro_huifu', $appInitHooks, true); $this->line( $output, 'addons.php app_init 含 shopro_huifu', $iniState !== 1 || $inAppInit, $pass, $fail, $inAppInit ? '已注册' : '未注册(线上 Hook 可能不加载,需刷新插件缓存)' ); $output->writeln(''); $output->writeln('=== 支付通道路由检查(小程序 prepay 是否走汇付) ==='); $this->line( $output, '说明:收银台仍显示「微信支付」属正常,由后端路由决定实际通道', true, $pass, $fail ); try { $cfg = Db::name('shopro_huifu_config')->column('value', 'name'); $channel = $cfg['miniprogram_channel'] ?? 'yansongda'; $hfStatus = $cfg['status'] ?? '0'; $this->line( $output, 'miniprogram_channel=huifu', $channel === 'huifu', $pass, $fail, '当前=' . $channel ); $this->line( $output, '汇付通道开关 status=1', $hfStatus === '1', $pass, $fail, '当前=' . $hfStatus ); if ($channel === 'huifu') { $credOk = !empty($cfg['sys_id']) && !empty($cfg['product_id']) && !empty($cfg['huifu_id']); $this->line( $output, '汇付凭证 sys_id/product_id/huifu_id 已填', $credOk, $pass, $fail, $credOk ? '已填' : '缺商户号/系统号/产品号' ); $keyOk = !empty($cfg['merchant_private_key']) && !empty($cfg['huifu_public_key']); $this->line( $output, 'RSA 密钥对已填', $keyOk, $pass, $fail, $keyOk ? '已填' : '缺私钥或汇付公钥' ); if ($keyOk && class_exists('\\addons\\shopro_huifu\\library\\HuifuConfig')) { try { \addons\shopro_huifu\library\HuifuConfig::reload()->validateKeys(); $this->line($output, 'RSA 密钥格式可签名', true, $pass, $fail); } catch (\Exception $e) { $this->line($output, 'RSA 密钥格式可签名', false, $pass, $fail, $e->getMessage()); } } $payScene = trim((string) ($cfg['pay_scene'] ?? '')); $channelNo = trim((string) ($cfg['channel_no'] ?? '')); $scenePairOk = ($channelNo === '') || ($payScene !== '' && $channelNo !== ''); $this->line( $output, 'pay_scene 与 channel_no 成对配置', $scenePairOk, $pass, $fail, $scenePairOk ? 'OK' : '填了 channel_no 时必须同时填 pay_scene' ); } $mini = Db::name('shopro_config')->where('name', 'wxMiniProgram')->value('value'); $mini = json_decode($mini, true); $appId = $mini['app_id'] ?? ''; $this->line( $output, '商城小程序 AppID 已配置', $appId !== '', $pass, $fail, $appId !== '' ? $appId : 'shopro_config.wxMiniProgram 为空' ); } catch (\Exception $e) { $this->line($output, '支付通道配置可读', false, $pass, $fail, $e->getMessage()); } if (class_exists('\\addons\\shopro_huifu\\library\\Bridge')) { $bridgeEnabled = \addons\shopro_huifu\library\Bridge::isEnabled(); $this->line( $output, 'Bridge::isEnabled()', $bridgeEnabled, $pass, $fail, $bridgeEnabled ? 'true' : 'false(插件未启用或 status=0)' ); $huifuChannel = \addons\shopro_huifu\library\Bridge::isHuifuMiniProgramChannel('wxMiniProgram'); $this->line( $output, 'Bridge::isHuifuMiniProgramChannel(wxMiniProgram)', $huifuChannel, $pass, $fail, $huifuChannel ? 'true,prepay 应走汇付' : 'false,prepay 仍走 Yansongda' ); $shouldPay = \addons\shopro_huifu\library\Bridge::shouldHandlePay([ 'platform' => 'wxMiniProgram', 'payment' => 'wechat', 'prepay_type' => 'order', ]); $this->line( $output, 'Bridge::shouldHandlePay(小程序订单)', $shouldPay, $pass, $fail, $shouldPay ? 'true' : 'false' ); } $hookBehaviors = \think\Hook::get('shopro_pay_create'); $hookOk = !empty($hookBehaviors); $this->line( $output, 'Hook shopro_pay_create 已注册', $hookOk, $pass, $fail, $hookOk ? json_encode($hookBehaviors, JSON_UNESCAPED_UNICODE) : '未注册(插件 state=0 或未加载 appInit)' ); $output->writeln(''); $output->writeln("结果: 通过 {$pass} / 失败 {$fail}"); $output->writeln('汇付配置直达: /admin/shopro_huifu/config'); $output->writeln('分账设置直达: /admin/shopro/split/config'); if ($fail > 0) { $output->writeln('修复菜单/权限/缓存: php think shopro_huifu:repair-menu'); $output->writeln('修复后请退出后台重新登录'); } } protected function getMenuPathTitles($menuId) { $path = []; $currentId = $menuId; $guard = 0; while ($currentId && $guard < 20) { $row = Db::name('auth_rule')->where('id', $currentId)->find(); if (!$row) { break; } if (intval($row['ismenu']) === 1 && $row['status'] === 'normal') { array_unshift($path, $row['title']); } $currentId = intval($row['pid']); $guard++; } return $path; } protected function line(Output $output, $label, $ok, &$pass, &$fail, $extra = '') { if ($ok) { $pass++; $output->writeln('[OK] ' . $label . ($extra ? ' (' . $extra . ')' : '')); } else { $fail++; $output->writeln('[FAIL] ' . $label . ($extra ? ' (' . $extra . ')' : '')); } } protected function tableExists($table) { try { $rows = Db::query("SHOW TABLES LIKE '{$table}'"); return !empty($rows); } catch (\Exception $e) { return false; } } protected function hasSdk($root) { $paths = [ $root . 'addons/shopro_huifu/library/sdk/extracted/huifurepo-bspay-php-sdk-45b895b/BsPaySdk/init.php', $root . 'vendor/huifurepo/dg-php-sdk/BsPaySdk/init.php', ]; foreach ($paths as $p) { if (is_file($p)) { return true; } } return false; } }