| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- <?php
- namespace app\common\service {
- // Initialization must never enter either side-effecting dependency.
- final class WsClientService { public function __construct() { throw new \LogicException('Initialization attempted a network connection'); } }
- final class JisuSettlementService { public function __construct() { throw new \LogicException('Initialization attempted settlement'); } }
- }
- namespace {
- error_reporting(E_ALL & ~E_DEPRECATED);
- date_default_timezone_set('Asia/Shanghai');
- $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());
- $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 rejects(callable $action, string $text): void {
- global $checks; ++$checks;
- try { $action(); } catch (\Throwable $error) {
- if (!str_contains($error->getMessage(), $text)) throw $error;
- return;
- }
- throw new \RuntimeException('Expected rejection: ' . $text);
- }
- function period(array $extra = []): array {
- return $extra + ['type' => 1, 'issue' => '34122211', 'status' => 3,
- 'open_time' => 1787567760, 'next_open_time' => 1787567835, 'next_issue' => 34122212,
- 'open_code' => '1,2,3,4,5,6,7,8,9,10', 'advance_code' => 'private-existing-value', 'is_settlement' => 1,
- 'created_at' => '2026-08-24 18:34:46', 'updated_at' => '2026-08-24 18:34:46'];
- }
- function resetPeriods(array $rows = []): void {
- global $db;
- $db->execute('DROP TABLE IF EXISTS bot_jisu_lottery');
- $db->execute('CREATE TABLE bot_jisu_lottery (id INTEGER PRIMARY KEY AUTOINCREMENT,type INTEGER,issue TEXT,status INTEGER,open_time INTEGER,next_open_time INTEGER,next_issue INTEGER,open_code TEXT,advance_code TEXT,is_settlement INTEGER DEFAULT 0,created_at TEXT,updated_at TEXT,UNIQUE(issue,type))');
- foreach ($rows as $row) \think\facade\Db::table('bot_jisu_lottery')->insert($row);
- $db->execute("CREATE TRIGGER protect_closed_update BEFORE UPDATE ON bot_jisu_lottery WHEN OLD.status = 3 BEGIN SELECT RAISE(ABORT,'closed history changed'); END");
- $db->execute("CREATE TRIGGER protect_period_delete BEFORE DELETE ON bot_jisu_lottery BEGIN SELECT RAISE(ABORT,'history deleted'); END");
- }
- function snapshot(): array { return \think\facade\Db::table('bot_jisu_lottery')->order('id')->select()->toArray(); }
- function finances(): array {
- $result = [];
- foreach (['bot_wallets', 'bot_lhc_order', 'bot_balance_logs'] as $table) $result[$table] = \think\facade\Db::table($table)->order('id')->select()->toArray();
- return $result;
- }
- 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']);
- foreach (['INSERT', 'UPDATE', 'DELETE'] as $action) $db->execute('CREATE TRIGGER forbid_' . $table . '_' . $action . ' BEFORE ' . $action . ' ON ' . $table . " BEGIN SELECT RAISE(ABORT,'initialization changed finances'); END");
- }
- $financialBefore = finances();
- $now = (new \DateTimeImmutable('2026-10-25 15:18:55'))->getTimestamp();
- $service = \app\common\service\JisuLotteryPeriodService::class;
- check(\app\api\command\JisuLottery::COUN_TDOWN, $service::INTERVAL, 'Initialization interval matches the ongoing draw loop');
- check(\app\api\command\JisuLottery::FENG_PAN, $service::CLOSE_BEFORE, 'Initialization closing window matches the ongoing draw loop');
- $rows = [];
- foreach ([1, 2, 3, 6] as $type) $rows[] = period(['type' => $type, 'is_settlement' => $type === 6 ? 0 : 1]);
- foreach ([4, 5, 7] as $type) $rows[] = period(['type' => $type, 'issue' => '20261025184', 'status' => 1, 'open_time' => $now + 300]);
- resetPeriods($rows);
- $before = snapshot();
- foreach ([1, 2, 3, 6] as $type) {
- $result = $service::ensure($type, $now);
- check($result['created'], true, 'One next period created');
- check($result['issue'], 34122212, 'Continue the observed issue instead of using the seed');
- check($result['open_time'], $now + 75, 'Long outage resumes a full future period');
- check($result['next_open_time'], $now + 150, 'Next period is 75 seconds later');
- check($result['next_issue'], 34122213, 'Next issue increments');
- check($result['rescheduled'], true, 'Expired schedule recovery is explicit');
- check($result['status'], 1, 'Future period opens normally');
- $stored = \think\facade\Db::table('bot_jisu_lottery')->where('type', $type)->where('issue', '34122212')->find();
- check($stored['open_time'], $now + 75, 'Timestamp saved as an integer');
- check($stored['open_code'], null, 'No draw result is invented');
- check($stored['advance_code'], null, 'Previous advance code is never copied');
- check($stored['is_settlement'], 0, 'New period is not settled');
- }
- $after = snapshot();
- check(count($after), count($before) + 4, 'Only four future local periods added');
- check(array_slice($after, 0, count($before)), $before, 'All closed history and SG rows stay byte-for-byte unchanged');
- $command = new \app\api\command\JisuLottery();
- $output = new \think\console\Output('buffer');
- (new \ReflectionProperty(\think\console\Command::class, 'output'))->setValue($command, $output);
- $command->init(); $command->init();
- check(snapshot(), $after, 'Repeated real command initialization is idempotent');
- check(str_contains($output->fetch(), '保留已有待开奖期次'), true, 'Initialization reports that existing periods are retained');
- check(finances(), $financialBefore, 'Recovery does not change wallets, orders or ledger');
- foreach ([$now + 150, (string)($now + 150), date('Y-m-d H:i:s', $now + 150)] as $future) {
- $plan = $service::plan(period(['next_open_time' => $future]), $now);
- check($plan['open_time'], $now + 150, 'Integer, numeric-string and datetime future schedules are preserved');
- check($plan['rescheduled'], false, 'Valid future schedule is not shifted');
- check($plan['issue'], 34122212, 'Future plan still increments issue');
- }
- foreach ([$now - 1, $now] as $expired) {
- $plan = $service::plan(period(['next_open_time' => $expired]), $now);
- check($plan['open_time'], $now + 75, 'Expired and exact-now schedules move to the future');
- check($plan['rescheduled'], true, 'Expired schedule is marked as resumed');
- }
- check($service::plan(period(['next_open_time' => $now + 15]), $now)['status'], 2, 'A retained future schedule inside the closing window starts closed');
- check($service::plan(period(['next_open_time' => $now + 16]), $now)['status'], 1, 'Future schedule outside closing window remains open');
- check($service::plan(null, $now)['issue'], 34038100, 'Seed is used only for a truly empty game');
- check($service::plan(null, $now)['open_time'], $now + 75, 'Empty game starts a full future period');
- foreach ([0, 1, 2] as $status) {
- resetPeriods([period(['status' => $status, 'open_time' => $now - 100])]);
- $pendingBefore = snapshot();
- check($service::ensure(1, $now)['created'], false, 'Existing pending status is retained: ' . $status);
- check(snapshot(), $pendingBefore, 'Pending rows are never moved or replaced');
- }
- resetPeriods([period(), period(['issue' => '34038100', 'status' => 1])]);
- $pendingBefore = snapshot();
- check($service::ensure(1, $now)['created'], false, 'Lower-number pending row also blocks creating another active period');
- check(snapshot(), $pendingBefore, 'Out-of-order pending rows remain untouched for audit');
- resetPeriods();
- $fresh = $service::ensure(1, $now);
- check($fresh['issue'], 34038100, 'Fresh database keeps original first issue');
- check($service::ensure(1, $now)['created'], false, 'Fresh initialization is also idempotent');
- foreach ([null, '', 'invalid-date', [], -1, 0] as $invalid) {
- resetPeriods([period(['next_open_time' => is_array($invalid) ? null : $invalid])]);
- $invalidBefore = snapshot();
- rejects(static fn() => $service::plan(period(['next_open_time' => $invalid]), $now), 'next_open_time');
- rejects(static fn() => $service::ensure(1, $now), 'next_open_time');
- check(snapshot(), $invalidBefore, 'Invalid timestamp never creates a guessed period');
- }
- foreach (['', 'unknown', '0', '-1', str_repeat('9', 50)] as $invalid) {
- resetPeriods([period(['issue' => $invalid])]);
- $invalidBefore = snapshot();
- rejects(static fn() => $service::ensure(1, $now), '期号无效');
- check(snapshot(), $invalidBefore, 'Invalid issue never falls back to the seed');
- }
- foreach ([0, 1, 2, null] as $status) rejects(static fn() => $service::plan(period(['status' => $status]), $now), '尚未确认开奖');
- foreach ([0, 4, 5, 7, 8] as $type) rejects(static fn() => $service::ensure($type, $now), '仅支持');
- resetPeriods([period()]);
- $failedBefore = snapshot();
- $db->execute("CREATE TRIGGER injected_insert_failure BEFORE INSERT ON bot_jisu_lottery BEGIN SELECT RAISE(ABORT,'injected period failure'); END");
- rejects(static fn() => $service::ensure(1, $now), 'injected period failure');
- check(snapshot(), $failedBefore, 'Failed insertion rolls back without altering history');
- $db->execute('DROP TRIGGER injected_insert_failure');
- check($service::ensure(1, $now)['created'], true, 'Initialization can retry after a rolled-back failure');
- check(finances(), $financialBefore, 'All validation, retry and failure cases preserve finances');
- echo 'PASS ' . $checks . " jisu initialization assertions (isolated SQLite, no settlement or network)\n";
- }
|