|
|
@@ -0,0 +1,153 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace app\common\service {
|
|
|
+ function env($key, $default = null) { return $GLOBALS['jisu_diagnostic_env'][$key] ?? $default; }
|
|
|
+ function httpRequest($url, $params, $headers, $proxy) {
|
|
|
+ $GLOBALS['jisu_diagnostic_http_calls'][] = [$url, $proxy];
|
|
|
+ return ['errorCode' => 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, '固定期号 34038100');
|
|
|
+ contains($text, 'next_open_time 是原始时间戳');
|
|
|
+ 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 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";
|
|
|
+}
|