jisu_lottery_diagnostics.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. <?php
  2. namespace app\common\service {
  3. function env($key, $default = null) { return $GLOBALS['jisu_diagnostic_env'][$key] ?? $default; }
  4. function httpRequest($url, $params, $headers, $proxy) {
  5. $GLOBALS['jisu_diagnostic_http_calls'][] = [$url, $proxy];
  6. return ['errorCode' => 0, 'result' => ['data' => [
  7. 'drawIssue' => '12345', 'drawTime' => '2026-10-25 12:00:00', 'preDrawCode' => '1,2,3', 'preDrawIssue' => '12344',
  8. ]]];
  9. }
  10. final class WsClientService {
  11. public function connect() { ++$GLOBALS['jisu_diagnostic_ws_calls']; return true; }
  12. public function close() { ++$GLOBALS['jisu_diagnostic_ws_closes']; }
  13. }
  14. }
  15. namespace {
  16. error_reporting(E_ALL & ~E_DEPRECATED);
  17. $repo = getenv('JISU_SITE_REPO') ?: dirname(__DIR__, 3) . '/melbet_sport-api';
  18. $stage = getenv('JISU_SITE_STAGE') ?: $repo;
  19. $loader = require $repo . '/vendor/autoload.php';
  20. $loader->setPsr4('app\\', [$stage . '/app/', $repo . '/app/']);
  21. $app = new \think\App($repo . '/');
  22. \think\Container::setInstance($app);
  23. $db = new \think\DbManager();
  24. $db->setConfig(['default' => 'isolated', 'connections' => ['isolated' => [
  25. 'type' => 'sqlite', 'database' => ':memory:', 'prefix' => '', 'fields_strict' => true, 'debug' => false,
  26. ]], 'auto_timestamp' => false]);
  27. $app->instance(\think\DbManager::class, $db);
  28. $app->instance('log', new \Psr\Log\NullLogger());
  29. $GLOBALS['jisu_diagnostic_env'] = ['app.http_proxy' => 'http://user:proxy-secret@proxy.test:8080'];
  30. $GLOBALS['jisu_diagnostic_http_calls'] = [];
  31. $GLOBALS['jisu_diagnostic_ws_calls'] = $GLOBALS['jisu_diagnostic_ws_closes'] = 0;
  32. $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)');
  33. foreach (['bot_wallets', 'bot_lhc_order', 'bot_balance_logs'] as $table) {
  34. $db->execute('CREATE TABLE ' . $table . ' (id INTEGER PRIMARY KEY,amount TEXT)');
  35. \think\facade\Db::table($table)->insert(['id' => 1, 'amount' => '123.45']);
  36. }
  37. $now = time();
  38. $old = $now - 60 * 86400;
  39. \think\facade\Db::table('bot_jisu_lottery')->insertAll([
  40. ['id' => 1, 'type' => 1, 'issue' => 34040000, 'status' => 3, 'open_time' => $old, 'next_open_time' => $old + 75, 'updated_at' => '2026-08-24 18:36:00'],
  41. ['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'],
  42. ['id' => 3, 'type' => 2, 'issue' => 34040000, 'status' => 3, 'open_time' => $old, 'next_open_time' => $old + 75, 'updated_at' => '2026-08-24 18:36:00'],
  43. ['id' => 4, 'type' => 4, 'issue' => 12345, 'status' => 1, 'open_time' => $now + 3600, 'next_open_time' => null, 'updated_at' => '2026-10-25 12:00:00'],
  44. ]);
  45. $tables = ['bot_jisu_lottery', 'bot_wallets', 'bot_lhc_order', 'bot_balance_logs'];
  46. foreach ($tables as $table) {
  47. foreach (['INSERT', 'UPDATE', 'DELETE'] as $action) {
  48. $db->execute('CREATE TRIGGER forbid_' . $table . '_' . $action . ' BEFORE ' . $action . ' ON ' . $table . " BEGIN SELECT RAISE(ABORT,'diagnosis attempted a write'); END");
  49. }
  50. }
  51. function snapshot(): array {
  52. global $tables;
  53. $result = [];
  54. foreach ($tables as $table) $result[$table] = \think\facade\Db::table($table)->order('id')->select()->toArray();
  55. return $result;
  56. }
  57. $checks = 0;
  58. function check($actual, $expected, string $message): void {
  59. global $checks; ++$checks;
  60. if ($actual !== $expected) throw new \RuntimeException($message . ': actual=' . json_encode($actual) . ' expected=' . json_encode($expected));
  61. }
  62. function contains(string $text, string $part): void { check(str_contains($text, $part), true, 'Missing output: ' . $part); }
  63. $before = snapshot();
  64. $output = new \think\console\Output('buffer');
  65. $service = new \app\common\service\JisuLotteryDiagnostics();
  66. check($service->run($output), 0, 'Database-only diagnostics complete');
  67. $text = $output->fetch();
  68. contains($text, 'pending=1 overdue=1');
  69. contains($text, 'api_last_drawn: id=1 issue=34040000 status=3');
  70. contains($text, 'last_inserted: id=2 issue=34038100 status=1');
  71. contains($text, '最新插入期号小于最大期号');
  72. contains($text, '没有未开奖期次');
  73. contains($text, '[RECOVERY PLAN] issue=34040001');
  74. contains($text, 'mode=resume_future');
  75. check(str_contains($text, '当前初始化使用固定期号'), false, 'Diagnostics do not report a bug that has been fixed');
  76. check(str_contains($text, '当前初始化使用 strtotime'), false, 'Diagnostics use the corrected raw-timestamp plan');
  77. contains($text, '此类型没有任何期次数据');
  78. contains($text, '检查结束');
  79. check($GLOBALS['jisu_diagnostic_http_calls'], [], 'Database diagnostics never contact upstream');
  80. check($GLOBALS['jisu_diagnostic_ws_calls'], 0, 'Database diagnostics never connect WebSocket');
  81. check(snapshot(), $before, 'Database-only diagnostics preserve all rows');
  82. $output = new \think\console\Output('buffer');
  83. check($service->run($output, true), 0, 'Default network probes use read-only adapters');
  84. $text = $output->fetch();
  85. foreach ([4, 5, 7] as $type) contains($text, '[SG ' . $type . ' OK]');
  86. contains($text, '[WS OK]');
  87. check(count($GLOBALS['jisu_diagnostic_http_calls']), 3, 'Every SG feed is probed once');
  88. check($GLOBALS['jisu_diagnostic_ws_calls'], 1, 'WebSocket handshake tested once');
  89. check($GLOBALS['jisu_diagnostic_ws_closes'], 1, 'WebSocket client always closed');
  90. check(str_contains($text, 'proxy-secret'), false, 'Proxy credentials are not printed');
  91. check(snapshot(), $before, 'Network probes do not insert or settle any data');
  92. $calls = 0; $wsCalls = 0;
  93. $broken = new \app\common\service\JisuLotteryDiagnostics(
  94. static function () use (&$calls) {
  95. ++$calls;
  96. if ($calls === 1) return null;
  97. if ($calls === 2) return ['code' => 500, 'msg' => 'request failed http://user:proxy-secret@proxy.test:8080'];
  98. return ['errorCode' => 0, 'result' => ['data' => ['drawIssue' => 12345]]];
  99. },
  100. static function () use (&$wsCalls) { ++$wsCalls; throw new \RuntimeException('certificate verify failed ssl://user:ws-secret@host.test/?token=secret-token'); }
  101. );
  102. $output = new \think\console\Output('buffer');
  103. check($broken->run($output, true), 1, 'Network failures produce a nonzero diagnostic result');
  104. $text = $output->fetch();
  105. foreach ([4, 5, 7] as $type) contains($text, '[SG ' . $type . ' FAIL]');
  106. contains($text, '返回空内容或非 JSON');
  107. contains($text, 'code=500');
  108. contains($text, '缺少字段:drawTime,preDrawCode,preDrawIssue');
  109. contains($text, 'certificate verify failed');
  110. check($calls, 3, 'Upstream failure does not skip other feed probes');
  111. check($wsCalls, 1, 'Upstream failure does not skip WebSocket diagnostics');
  112. foreach (['proxy-secret', 'ws-secret', 'secret-token'] as $secret) check(str_contains($text, $secret), false, 'Secret is redacted');
  113. check(snapshot(), $before, 'Failures never change lottery, orders, wallet or ledger');
  114. $malformed = new \app\common\service\JisuLotteryDiagnostics(
  115. static fn() => ['errorCode' => 0, 'result' => ['data' => [
  116. 'drawIssue' => [], 'drawTime' => null, 'preDrawCode' => '1,2,3', 'preDrawIssue' => '12344',
  117. ]]],
  118. static fn() => false
  119. );
  120. $output = new \think\console\Output('buffer');
  121. check($malformed->run($output, true), 1, 'Malformed upstream fields and false WebSocket result are diagnosed');
  122. $text = $output->fetch();
  123. contains($text, '字段内容异常:drawIssue,drawTime');
  124. contains($text, '[WS FAIL]');
  125. check(snapshot(), $before, 'Malformed responses do not change data');
  126. final class GuardedJisuCommand extends \app\api\command\JisuLottery {
  127. public function init() { throw new \LogicException('Read-only mode entered mutation initialization'); }
  128. public function sgKaiJiang($type) { throw new \LogicException('Read-only mode entered draw/settlement'); }
  129. }
  130. foreach ([['--diagnose'], ['--diagnose', '--probe-network']] as $args) {
  131. $command = new GuardedJisuCommand();
  132. $input = new \think\console\Input($args); $input->setInteractive(false);
  133. $output = new \think\console\Output('buffer');
  134. check($command->run($input, $output), 0, 'Real CLI option routing bypasses normal execution');
  135. contains($output->fetch(), '[DIAG] 只读诊断');
  136. check(snapshot(), $before, 'Real command diagnostic branch is read-only');
  137. }
  138. $command = new GuardedJisuCommand();
  139. $input = new \think\console\Input(['--probe-network']); $input->setInteractive(false);
  140. $output = new \think\console\Output('buffer');
  141. check($command->run($input, $output), 1, 'Network flag alone cannot start the mutating worker');
  142. contains($output->fetch(), '请同时传入 --diagnose');
  143. $db->execute('DROP TRIGGER forbid_bot_jisu_lottery_UPDATE');
  144. \think\facade\Db::table('bot_jisu_lottery')->where('type', 2)->update(['next_open_time' => null]);
  145. $blockedBefore = snapshot();
  146. $output = new \think\console\Output('buffer');
  147. check($service->run($output), 1, 'Invalid archived schedule blocks the recovery preview with a nonzero result');
  148. contains($output->fetch(), '[RECOVERY BLOCKED]');
  149. check(snapshot(), $blockedBefore, 'A blocked recovery preview never edits the invalid data');
  150. $db->execute('DROP TABLE bot_jisu_lottery');
  151. $output = new \think\console\Output('buffer');
  152. check($service->run($output), 1, 'Database errors are reported as failures');
  153. contains($output->fetch(), '[DB FAIL]');
  154. echo 'PASS ' . $checks . " jisu diagnostics assertions (isolated SQLite, fake HTTP/WS, no live tasks)\n";
  155. }