0, 'result' => ['data' => [ 'drawIssue' => '12345', 'drawTime' => '2026-10-25 12:00:00', 'preDrawCode' => '1,2,3', 'preDrawIssue' => '12344', ]]]; } final class WsClientService { public function connect() { ++$GLOBALS['jisu_diagnostic_ws_calls']; return true; } public function close() { ++$GLOBALS['jisu_diagnostic_ws_closes']; } } } namespace { error_reporting(E_ALL & ~E_DEPRECATED); $repo = getenv('JISU_SITE_REPO') ?: dirname(__DIR__, 3) . '/melbet_sport-api'; $stage = getenv('JISU_SITE_STAGE') ?: $repo; $loader = require $repo . '/vendor/autoload.php'; $loader->setPsr4('app\\', [$stage . '/app/', $repo . '/app/']); $app = new \think\App($repo . '/'); \think\Container::setInstance($app); $db = new \think\DbManager(); $db->setConfig(['default' => 'isolated', 'connections' => ['isolated' => [ 'type' => 'sqlite', 'database' => ':memory:', 'prefix' => '', 'fields_strict' => true, 'debug' => false, ]], 'auto_timestamp' => false]); $app->instance(\think\DbManager::class, $db); $app->instance('log', new \Psr\Log\NullLogger()); $GLOBALS['jisu_diagnostic_env'] = ['app.http_proxy' => 'http://user:proxy-secret@proxy.test:8080']; $GLOBALS['jisu_diagnostic_http_calls'] = []; $GLOBALS['jisu_diagnostic_ws_calls'] = $GLOBALS['jisu_diagnostic_ws_closes'] = 0; $db->execute('CREATE TABLE bot_jisu_lottery (id INTEGER PRIMARY KEY,type INTEGER,issue INTEGER,status INTEGER,open_time INTEGER,next_open_time INTEGER,updated_at TEXT)'); foreach (['bot_wallets', 'bot_lhc_order', 'bot_balance_logs'] as $table) { $db->execute('CREATE TABLE ' . $table . ' (id INTEGER PRIMARY KEY,amount TEXT)'); \think\facade\Db::table($table)->insert(['id' => 1, 'amount' => '123.45']); } $now = time(); $old = $now - 60 * 86400; \think\facade\Db::table('bot_jisu_lottery')->insertAll([ ['id' => 1, 'type' => 1, 'issue' => 34040000, 'status' => 3, 'open_time' => $old, 'next_open_time' => $old + 75, 'updated_at' => '2026-08-24 18:36:00'], ['id' => 2, 'type' => 1, 'issue' => 34038100, 'status' => 1, 'open_time' => $now - 60, 'next_open_time' => $now + 15, 'updated_at' => '2026-10-25 12:00:00'], ['id' => 3, 'type' => 2, 'issue' => 34040000, 'status' => 3, 'open_time' => $old, 'next_open_time' => $old + 75, 'updated_at' => '2026-08-24 18:36:00'], ['id' => 4, 'type' => 4, 'issue' => 12345, 'status' => 1, 'open_time' => $now + 3600, 'next_open_time' => null, 'updated_at' => '2026-10-25 12:00:00'], ]); $tables = ['bot_jisu_lottery', 'bot_wallets', 'bot_lhc_order', 'bot_balance_logs']; foreach ($tables as $table) { foreach (['INSERT', 'UPDATE', 'DELETE'] as $action) { $db->execute('CREATE TRIGGER forbid_' . $table . '_' . $action . ' BEFORE ' . $action . ' ON ' . $table . " BEGIN SELECT RAISE(ABORT,'diagnosis attempted a write'); END"); } } function snapshot(): array { global $tables; $result = []; foreach ($tables as $table) $result[$table] = \think\facade\Db::table($table)->order('id')->select()->toArray(); return $result; } $checks = 0; function check($actual, $expected, string $message): void { global $checks; ++$checks; if ($actual !== $expected) throw new \RuntimeException($message . ': actual=' . json_encode($actual) . ' expected=' . json_encode($expected)); } function contains(string $text, string $part): void { check(str_contains($text, $part), true, 'Missing output: ' . $part); } $before = snapshot(); $output = new \think\console\Output('buffer'); $service = new \app\common\service\JisuLotteryDiagnostics(); check($service->run($output), 0, 'Database-only diagnostics complete'); $text = $output->fetch(); contains($text, 'pending=1 overdue=1'); contains($text, 'api_last_drawn: id=1 issue=34040000 status=3'); contains($text, 'last_inserted: id=2 issue=34038100 status=1'); contains($text, '最新插入期号小于最大期号'); contains($text, '没有未开奖期次'); contains($text, '[RECOVERY PLAN] issue=34040001'); contains($text, 'mode=resume_future'); check(str_contains($text, '当前初始化使用固定期号'), false, 'Diagnostics do not report a bug that has been fixed'); check(str_contains($text, '当前初始化使用 strtotime'), false, 'Diagnostics use the corrected raw-timestamp plan'); contains($text, '此类型没有任何期次数据'); contains($text, '检查结束'); check($GLOBALS['jisu_diagnostic_http_calls'], [], 'Database diagnostics never contact upstream'); check($GLOBALS['jisu_diagnostic_ws_calls'], 0, 'Database diagnostics never connect WebSocket'); check(snapshot(), $before, 'Database-only diagnostics preserve all rows'); $output = new \think\console\Output('buffer'); check($service->run($output, true), 0, 'Default network probes use read-only adapters'); $text = $output->fetch(); foreach ([4, 5, 7] as $type) contains($text, '[SG ' . $type . ' OK]'); contains($text, '[WS OK]'); check(count($GLOBALS['jisu_diagnostic_http_calls']), 3, 'Every SG feed is probed once'); check($GLOBALS['jisu_diagnostic_ws_calls'], 1, 'WebSocket handshake tested once'); check($GLOBALS['jisu_diagnostic_ws_closes'], 1, 'WebSocket client always closed'); check(str_contains($text, 'proxy-secret'), false, 'Proxy credentials are not printed'); check(snapshot(), $before, 'Network probes do not insert or settle any data'); $calls = 0; $wsCalls = 0; $broken = new \app\common\service\JisuLotteryDiagnostics( static function () use (&$calls) { ++$calls; if ($calls === 1) return null; if ($calls === 2) return ['code' => 500, 'msg' => 'request failed http://user:proxy-secret@proxy.test:8080']; return ['errorCode' => 0, 'result' => ['data' => ['drawIssue' => 12345]]]; }, static function () use (&$wsCalls) { ++$wsCalls; throw new \RuntimeException('certificate verify failed ssl://user:ws-secret@host.test/?token=secret-token'); } ); $output = new \think\console\Output('buffer'); check($broken->run($output, true), 1, 'Network failures produce a nonzero diagnostic result'); $text = $output->fetch(); foreach ([4, 5, 7] as $type) contains($text, '[SG ' . $type . ' FAIL]'); contains($text, '返回空内容或非 JSON'); contains($text, 'code=500'); contains($text, '缺少字段:drawTime,preDrawCode,preDrawIssue'); contains($text, 'certificate verify failed'); check($calls, 3, 'Upstream failure does not skip other feed probes'); check($wsCalls, 1, 'Upstream failure does not skip WebSocket diagnostics'); foreach (['proxy-secret', 'ws-secret', 'secret-token'] as $secret) check(str_contains($text, $secret), false, 'Secret is redacted'); check(snapshot(), $before, 'Failures never change lottery, orders, wallet or ledger'); $malformed = new \app\common\service\JisuLotteryDiagnostics( static fn() => ['errorCode' => 0, 'result' => ['data' => [ 'drawIssue' => [], 'drawTime' => null, 'preDrawCode' => '1,2,3', 'preDrawIssue' => '12344', ]]], static fn() => false ); $output = new \think\console\Output('buffer'); check($malformed->run($output, true), 1, 'Malformed upstream fields and false WebSocket result are diagnosed'); $text = $output->fetch(); contains($text, '字段内容异常:drawIssue,drawTime'); contains($text, '[WS FAIL]'); check(snapshot(), $before, 'Malformed responses do not change data'); final class GuardedJisuCommand extends \app\api\command\JisuLottery { public function init() { throw new \LogicException('Read-only mode entered mutation initialization'); } public function sgKaiJiang($type) { throw new \LogicException('Read-only mode entered draw/settlement'); } } foreach ([['--diagnose'], ['--diagnose', '--probe-network']] as $args) { $command = new GuardedJisuCommand(); $input = new \think\console\Input($args); $input->setInteractive(false); $output = new \think\console\Output('buffer'); check($command->run($input, $output), 0, 'Real CLI option routing bypasses normal execution'); contains($output->fetch(), '[DIAG] 只读诊断'); check(snapshot(), $before, 'Real command diagnostic branch is read-only'); } $command = new GuardedJisuCommand(); $input = new \think\console\Input(['--probe-network']); $input->setInteractive(false); $output = new \think\console\Output('buffer'); check($command->run($input, $output), 1, 'Network flag alone cannot start the mutating worker'); contains($output->fetch(), '请同时传入 --diagnose'); $db->execute('DROP TRIGGER forbid_bot_jisu_lottery_UPDATE'); \think\facade\Db::table('bot_jisu_lottery')->where('type', 2)->update(['next_open_time' => null]); $blockedBefore = snapshot(); $output = new \think\console\Output('buffer'); check($service->run($output), 1, 'Invalid archived schedule blocks the recovery preview with a nonzero result'); contains($output->fetch(), '[RECOVERY BLOCKED]'); check(snapshot(), $blockedBefore, 'A blocked recovery preview never edits the invalid data'); $db->execute('DROP TABLE bot_jisu_lottery'); $output = new \think\console\Output('buffer'); check($service->run($output), 1, 'Database errors are reported as failures'); contains($output->fetch(), '[DB FAIL]'); echo 'PASS ' . $checks . " jisu diagnostics assertions (isolated SQLite, fake HTTP/WS, no live tasks)\n"; }