From 53eefd901dce4a7e8c5e08006513ea22c7ece6fd Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Tue, 26 May 2026 16:23:04 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E5=A4=8D=E5=85=B3=E9=97=AD=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=88=9B=E5=BB=BA=E4=B8=8B=E4=B8=80=E5=B1=80=E5=90=8E?= =?UTF-8?q?=E8=BF=98=E8=87=AA=E5=8A=A8=E5=88=9B=E5=BB=BA=E4=B8=8B=E4=B8=80?= =?UTF-8?q?=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/common/service/GameLiveService.php | 12 ++++++ app/common/service/GameRecordService.php | 45 ++++++++++++++++++----- web/src/views/backend/game/live/index.vue | 19 +++++++--- 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/app/common/service/GameLiveService.php b/app/common/service/GameLiveService.php index fdae894..c209161 100644 --- a/app/common/service/GameLiveService.php +++ b/app/common/service/GameLiveService.php @@ -261,6 +261,7 @@ final class GameLiveService } if (!GameRecordService::isLiveRuntimeEnabled()) { self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize(); + GameHotDataRedis::gameRecordRefreshAggregateCaches(); } elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') { self::publishImmediateBettingTickAfterFinalize(); } @@ -716,6 +717,7 @@ final class GameLiveService self::publishSnapshot(null); if (!GameRecordService::isLiveRuntimeEnabled()) { self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize(); + GameHotDataRedis::gameRecordRefreshAggregateCaches(); } elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') { self::publishImmediateBettingTickAfterFinalize(); } @@ -726,6 +728,12 @@ final class GameLiveService public static function tickAutoDraw(): void { + if (!GameRecordService::isAutoCreateEnabled()) { + $openCount = (int) Db::name('game_record')->whereIn('status', [0, 1])->count(); + if ($openCount <= 0) { + return; + } + } $record = self::resolveRecord(null); if (!$record || !in_array((int) $record['status'], [0, 1], true)) { return; @@ -801,6 +809,7 @@ final class GameLiveService self::voidOpenPeriodInternal($rid, $reason); } GameHotDataRedis::gameRecordRefreshAggregateCaches(); + self::publishSnapshot(null); } /** @@ -1145,6 +1154,9 @@ final class GameLiveService if (!self::shouldPublishPeriodTick($status, $periodNo)) { return; } + if (empty($snapshot['runtime_enabled']) && in_array($status, ['betting', 'locked'], true)) { + return; + } GameWebSocketEventBus::publish(self::EVT_PERIOD_TICK, $payload); } diff --git a/app/common/service/GameRecordService.php b/app/common/service/GameRecordService.php index 4a82e64..c83eabc 100644 --- a/app/common/service/GameRecordService.php +++ b/app/common/service/GameRecordService.php @@ -23,6 +23,31 @@ final class GameRecordService return false; } $v = $row['config_value'] ?? ''; + return self::truthyConfigValue($v); + } + + /** + * 是否允许自动创建下一局(读库,避免 Redis 缓存仍为 1 时误开新期)。 + */ + public static function isAutoCreateEnabled(): bool + { + return self::getConfigBoolFromDb(self::KEY_AUTO_CREATE); + } + + private static function getConfigBoolFromDb(string $key): bool + { + if ($key === '') { + return false; + } + $row = Db::name('game_config')->where('config_key', $key)->find(); + if (!$row) { + return false; + } + return self::truthyConfigValue($row['config_value'] ?? ''); + } + + private static function truthyConfigValue(mixed $v): bool + { return $v === '1' || $v === 1; } @@ -51,7 +76,7 @@ final class GameRecordService public static function tickAutoCreate(): void { - if (!self::getConfigBool(self::KEY_AUTO_CREATE)) { + if (!self::isAutoCreateEnabled()) { return; } try { @@ -78,10 +103,6 @@ final class GameRecordService public static function createNextRecordAfterDraw(): ?string { - // 派彩结束后是否自动开新局:由 period_auto_create_enabled 控制 - if (!self::getConfigBool(self::KEY_AUTO_CREATE)) { - return null; - } return self::createNextRecordRowIfNoActive(); } @@ -90,7 +111,7 @@ final class GameRecordService */ public static function isLiveRuntimeEnabled(): bool { - return self::getConfigBool(self::KEY_AUTO_CREATE); + return self::isAutoCreateEnabled(); } public static function setLiveRuntimeEnabled(bool $enabled): void @@ -103,7 +124,7 @@ final class GameRecordService */ public static function bootstrapPeriodWhenRuntimeEnabled(): void { - if (!self::getConfigBool(self::KEY_AUTO_CREATE)) { + if (!self::isAutoCreateEnabled()) { return; } try { @@ -116,6 +137,7 @@ final class GameRecordService { $now = time(); $v = $enabled ? '1' : '0'; + GameHotDataRedis::gameConfigForget(self::KEY_AUTO_CREATE); self::upsertConfig(self::KEY_AUTO_CREATE, $v, 'int', '是否允许自动创建下一局(全局仅一局)', $now); } @@ -124,7 +146,7 @@ final class GameRecordService */ public static function createNextRecordRow(): string { - $created = self::createNextRecordRowIfNoActive(); + $created = self::createNextRecordRowIfNoActive(false); if ($created === null) { throw new \RuntimeException((string) __('There is an unfinished round; cannot create a new one')); } @@ -134,9 +156,14 @@ final class GameRecordService /** * 幂等插入下一期:有进行中局则不插入,返回 null。 + * + * @param bool $requireAutoCreate true=须开启自动开局(定时/派彩后);false=手动创建下一期 */ - public static function createNextRecordRowIfNoActive(): ?string + public static function createNextRecordRowIfNoActive(bool $requireAutoCreate = true): ?string { + if ($requireAutoCreate && !self::isAutoCreateEnabled()) { + return null; + } $lock = GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, self::AUTO_CREATE_LOCK_KEY, 1500); if (!$lock['acquired']) { return null; diff --git a/web/src/views/backend/game/live/index.vue b/web/src/views/backend/game/live/index.vue index 21298be..9a5657b 100644 --- a/web/src/views/backend/game/live/index.vue +++ b/web/src/views/backend/game/live/index.vue @@ -426,6 +426,11 @@ function handlePeriodTickEvent(periodData: anyObj): void { const status = typeof periodData.status === 'string' ? periodData.status : '' const periodNo = typeof periodData.period_no === 'string' ? periodData.period_no : '' const currentNo = typeof snapshot.record?.period_no === 'string' ? snapshot.record.period_no : '' + const runtimeOff = + toBool(snapshot.runtime_enabled) === false || toBool(periodData.runtime_enabled) === false + if (runtimeOff && (status === 'betting' || status === 'locked')) { + return + } if (status === 'betting' && periodNo !== '' && periodNo !== currentNo) { void loadSnapshot({ force: true }) return @@ -436,7 +441,7 @@ function handlePeriodTickEvent(periodData: anyObj): void { } return } - if (currentNo === '' && periodNo !== '') { + if (currentNo === '' && periodNo !== '' && !runtimeOff) { void loadSnapshot({ force: true }) } } @@ -698,7 +703,13 @@ function mergeLiveSnapshot(data: anyObj): void { if (data.record !== undefined) { const nextId = data.record?.id != null ? Number(data.record.id) : null periodChanged = prevPeriodId !== null && nextId !== null && prevPeriodId !== nextId - snapshot.record = data.record + const runtimeOff = toBool(data.runtime_enabled) === false || toBool(snapshot.runtime_enabled) === false + const serverMaintenance = data.maintenance_ui === true + if (runtimeOff && serverMaintenance) { + snapshot.record = null + } else { + snapshot.record = data.record + } } const incomingBets = Array.isArray(data.bets) ? data.bets : null @@ -835,9 +846,7 @@ async function loadSnapshot(options?: { force?: boolean }): Promise { async function onRuntimeSwitch(val: boolean | string | number): void { const on = toBool(val) === true - // 防止某些场景下 model-value 变化触发重复 change 事件,造成 runtime 接口循环调用 - const current = toBool(snapshot.runtime_enabled) === true - if (on === current) { + if (runtimeSwitchLoading.value) { return } // el-switch 为受控组件(model-value 来自 snapshot),接口返回前先乐观更新,避免点击后立刻回弹