1.修复自动创建下一期bug

This commit is contained in:
2026-05-26 17:35:45 +08:00
parent 827da2058f
commit 5f8f5aa4ca
3 changed files with 110 additions and 13 deletions

View File

@@ -52,6 +52,7 @@ class Live extends Backend
$topics = [ $topics = [
'admin.live.snapshot', 'admin.live.snapshot',
'admin.live.opened', 'admin.live.opened',
'admin.live.finalized',
'jackpot.hit', 'jackpot.hit',
'period.tick', 'period.tick',
'period.locked', 'period.locked',

View File

@@ -252,12 +252,16 @@ final class GameLiveService
$now = time(); $now = time();
Db::startTrans(); Db::startTrans();
try { try {
Db::name('game_record')->where('id', $recordId)->where('status', 3)->update([ $affected = Db::name('game_record')->where('id', $recordId)->where('status', 3)->update([
'status' => 4, 'status' => 4,
'payout_until' => null, 'payout_until' => null,
'update_time' => $now, 'update_time' => $now,
]); ]);
$newPeriodNo = GameRecordService::createNextRecordAfterDraw(); if ($affected < 1) {
Db::rollback();
return;
}
Db::commit(); Db::commit();
} catch (Throwable $e) { } catch (Throwable $e) {
Db::rollback(); Db::rollback();
@@ -267,6 +271,17 @@ final class GameLiveService
]); ]);
return; return;
} }
$newPeriodNo = null;
if (GameRecordService::isAutoCreateEnabled()) {
try {
$newPeriodNo = GameRecordService::createNextRecordAfterDraw();
} catch (Throwable $e) {
Log::warning('game live startup create next record failed', [
'record_id' => $recordId,
'error' => $e->getMessage(),
]);
}
}
GameHotDataCoordinator::afterGameRecordCommitted($recordId); GameHotDataCoordinator::afterGameRecordCommitted($recordId);
try { try {
GameRecordStatService::refreshForRecordId($recordId); GameRecordStatService::refreshForRecordId($recordId);
@@ -283,6 +298,9 @@ final class GameLiveService
public static function buildSnapshot(?int $recordId = null): array public static function buildSnapshot(?int $recordId = null): array
{ {
// HTTP/WS 拉快照时也尝试结单,避免仅 gameLiveTicker 未跑时派彩倒计时归零后长期卡住
self::finalizePayoutGrace();
$record = self::resolveRecord($recordId); $record = self::resolveRecord($recordId);
if ($record) { if ($record) {
$periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30); $periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30);
@@ -717,6 +735,8 @@ final class GameLiveService
$id = (int) $row['id']; $id = (int) $row['id'];
$lock = GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, (string) $id, 2000); $lock = GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, (string) $id, 2000);
if (!$lock['acquired']) { if (!$lock['acquired']) {
Log::warning('finalizePayoutGrace: lock not acquired', ['record_id' => $id]);
return; return;
} }
try { try {
@@ -726,14 +746,22 @@ final class GameLiveService
$resultNumber = null; $resultNumber = null;
} }
$newPeriodNo = null; $newPeriodNo = null;
$now = time();
Db::startTrans(); Db::startTrans();
try { try {
Db::name('game_record')->where('id', $id)->update([ $affected = Db::name('game_record')
'status' => 4, ->where('id', $id)
'payout_until' => null, ->where('status', 3)
'update_time' => time(), ->update([
]); 'status' => 4,
$newPeriodNo = GameRecordService::createNextRecordAfterDraw(); 'payout_until' => null,
'update_time' => $now,
]);
if ($affected < 1) {
Db::rollback();
return;
}
Db::commit(); Db::commit();
} catch (Throwable $e) { } catch (Throwable $e) {
Db::rollback(); Db::rollback();
@@ -741,13 +769,33 @@ final class GameLiveService
return; return;
} }
if (GameRecordService::isAutoCreateEnabled()) {
try {
$newPeriodNo = GameRecordService::createNextRecordAfterDraw();
} catch (Throwable $e) {
Log::warning('finalizePayoutGrace: create next record failed', [
'record_id' => $id,
'error' => $e->getMessage(),
]);
}
}
GameHotDataCoordinator::afterGameRecordCommitted($id); GameHotDataCoordinator::afterGameRecordCommitted($id);
GameRecordStatService::refreshForRecordId($id); GameRecordStatService::refreshForRecordId($id);
self::publishPublicPeriodFinished($id, $periodNo, $resultNumber); self::publishPublicPeriodFinished($id, $periodNo, $resultNumber);
GameWebSocketEventBus::publish('admin.live.finalized', [
'period_id' => $id,
'period_no' => $periodNo,
'result_number' => $resultNumber,
'runtime_enabled' => GameRecordService::isLiveRuntimeEnabled(),
'maintenance_ui' => !GameRecordService::isLiveRuntimeEnabled()
&& !GameRecordService::hasActiveRecord(),
'server_time' => $now,
]);
self::publishSnapshot(null); self::publishSnapshot(null);
if (!GameRecordService::isLiveRuntimeEnabled()) { if (!GameRecordService::isLiveRuntimeEnabled()) {
self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize(); self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize();
GameHotDataRedis::gameRecordRefreshAggregateCaches(); GameHotDataRedis::gameRecordRefreshAggregateCaches();
self::publishSnapshot(null);
} elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') { } elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') {
self::publishImmediateBettingTickAfterFinalize(); self::publishImmediateBettingTickAfterFinalize();
} }

View File

@@ -280,6 +280,7 @@ let clockTimer: number | null = null
let payoutStuckRefreshTimer: number | null = null let payoutStuckRefreshTimer: number | null = null
let drawStuckRefreshTimer: number | null = null let drawStuckRefreshTimer: number | null = null
let drawStuckSeconds = 0 let drawStuckSeconds = 0
let payoutPhaseStuckSeconds = 0
let fallbackPollTimer: number | null = null let fallbackPollTimer: number | null = null
let betStreamRefreshTimer: number | null = null let betStreamRefreshTimer: number | null = null
/** 合并并发 snapshot 请求,避免 axios 重复请求取消导致控制台报错 */ /** 合并并发 snapshot 请求,避免 axios 重复请求取消导致控制台报错 */
@@ -352,6 +353,25 @@ function handleWsPayload(raw: unknown): void {
void loadSnapshot({ force: true }) void loadSnapshot({ force: true })
return return
} }
if (event === 'admin.live.finalized' && parsed.data && typeof parsed.data === 'object') {
const fin = parsed.data as anyObj
if (toBool(fin.maintenance_ui) === true) {
snapshot.is_payout_phase = false
snapshot.payout_remaining_seconds = 0
}
void loadSnapshot({ force: true })
return
}
if (event === 'period.payout' && parsed.data && typeof parsed.data === 'object') {
mergePeriodPayoutTick(parsed.data as anyObj)
const payoutData = parsed.data as anyObj
if (typeof payoutData.result_number === 'number') {
snapshot.result_number = payoutData.result_number
calcResultNumber.value = payoutData.result_number
}
snapshot.is_payout_phase = true
return
}
if (event === 'bet.win' && parsed.data && typeof parsed.data === 'object') { if (event === 'bet.win' && parsed.data && typeof parsed.data === 'object') {
const winData = parsed.data as anyObj const winData = parsed.data as anyObj
if (winData.is_jackpot === true) { if (winData.is_jackpot === true) {
@@ -419,7 +439,11 @@ function mergePeriodPayoutTick(data: anyObj): void {
const remain = numberValue(data.payout_remaining_seconds) const remain = numberValue(data.payout_remaining_seconds)
if (remain !== null) { if (remain !== null) {
snapshot.payout_remaining_seconds = Math.max(0, remain) snapshot.payout_remaining_seconds = Math.max(0, remain)
snapshot.is_payout_phase = remain > 0 || snapshot.is_payout_phase === true snapshot.is_payout_phase = remain > 0
}
const until = readPayoutUntilUnix(data)
if (until !== null && snapshot.record && typeof snapshot.record === 'object') {
snapshot.record.payout_until = until
} }
} }
@@ -437,10 +461,10 @@ function handlePeriodTickEvent(periodData: anyObj): void {
void loadSnapshot({ force: true }) void loadSnapshot({ force: true })
return return
} }
if (status === 'finished' && snapshot.is_payout_phase) { if (status === 'finished') {
if (!wsConnected.value) { snapshot.is_payout_phase = false
void loadSnapshot() snapshot.payout_remaining_seconds = 0
} void loadSnapshot({ force: true })
return return
} }
if (currentNo === '' && periodNo !== '' && !runtimeOff) { if (currentNo === '' && periodNo !== '' && !runtimeOff) {
@@ -709,6 +733,11 @@ function mergeLiveSnapshot(data: anyObj): void {
const serverMaintenance = data.maintenance_ui === true const serverMaintenance = data.maintenance_ui === true
if (runtimeOff && serverMaintenance) { if (runtimeOff && serverMaintenance) {
snapshot.record = null snapshot.record = null
snapshot.is_payout_phase = false
snapshot.payout_remaining_seconds = 0
snapshot.can_calculate = false
snapshot.can_draw = false
snapshot.can_schedule_draw = false
} else { } else {
snapshot.record = data.record snapshot.record = data.record
} }
@@ -1023,6 +1052,24 @@ function isPrePayoutDrawStuck(): boolean {
return st === 0 || st === 1 return st === 0 || st === 1
} }
/** 派彩倒计时已为 0 但 is_payout_phase 仍为 true关服排水时常见 */
function tickPayoutPhaseStuckRecovery(): void {
if (!snapshot.is_payout_phase) {
payoutPhaseStuckSeconds = 0
return
}
const remain = payoutRemainingLive.value
if (remain === null || remain > 0) {
payoutPhaseStuckSeconds = 0
return
}
payoutPhaseStuckSeconds++
if (payoutPhaseStuckSeconds >= 4) {
payoutPhaseStuckSeconds = 0
void loadSnapshot({ force: true })
}
}
function tickPrePayoutDrawStuckRecovery(): void { function tickPrePayoutDrawStuckRecovery(): void {
if (!isPrePayoutDrawStuck()) { if (!isPrePayoutDrawStuck()) {
drawStuckSeconds = 0 drawStuckSeconds = 0
@@ -1051,6 +1098,7 @@ onMounted(async () => {
clockTimer = window.setInterval(() => { clockTimer = window.setInterval(() => {
clockTick.value++ clockTick.value++
tickPrePayoutDrawStuckRecovery() tickPrePayoutDrawStuckRecovery()
tickPayoutPhaseStuckRecovery()
}, 1000) }, 1000)
fallbackPollTimer = window.setInterval(() => { fallbackPollTimer = window.setInterval(() => {
if (!wsConnected.value) { if (!wsConnected.value) {