| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- <?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, '[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";
- }
|