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

This commit is contained in:
2026-05-26 17:57:42 +08:00
parent 5d33b13c6f
commit 6c4e935b6c
5 changed files with 68 additions and 49 deletions

View File

@@ -25,6 +25,8 @@ class Live extends Backend
{
$recordIdRaw = $this->request ? $this->request->get('record_id') : null;
$recordId = is_numeric((string) $recordIdRaw) ? (int) $recordIdRaw : null;
GameLiveService::recoverLiveRoundState();
return $this->success('', GameLiveService::buildSnapshot($recordId));
}
@@ -36,6 +38,8 @@ class Live extends Backend
}
$recordIdRaw = $request->get('record_id');
$recordId = is_numeric((string) $recordIdRaw) ? (int) $recordIdRaw : null;
GameLiveService::recoverLiveRoundState();
return $this->success('', GameLiveService::buildSnapshot($recordId));
}
@@ -173,15 +177,12 @@ class Live extends Backend
return $this->error(is_string($errMsg) ? $errMsg : __('Void failed'));
}
$okMsg = $res['msg'] ?? '';
$snapshot = GameLiveService::buildSnapshot(null);
// 作废本局后:必须关闭自动创建下一局开关(允许管理员后续手动重新开启)
$snapshot['runtime_enabled'] = false;
// 作废后一般不存在进行中的局,直接进入维护态(用于前端展示“维护中”倒计时)
$snapshot['maintenance_ui'] = true;
$snapshot = GameLiveService::buildSnapshotAfterVoid();
$refund = $res['refund'] ?? null;
if (is_array($refund)) {
$snapshot['void_refund'] = $refund;
}
return $this->success(is_string($okMsg) ? $okMsg : '', $snapshot);
}
}

View File

@@ -296,11 +296,17 @@ final class GameLiveService
self::publishSnapshot(null);
}
/**
* 定时任务/WS 推送前:结单 + 超时自动开奖(勿在已持期号锁时调用)。
*/
public static function recoverLiveRoundState(): void
{
self::finalizePayoutGrace();
self::tickAutoDraw();
}
public static function buildSnapshot(?int $recordId = null): array
{
// HTTP/WS 拉快照时也尝试结单,避免仅 gameLiveTicker 未跑时派彩倒计时归零后长期卡住
self::finalizePayoutGrace();
$record = self::resolveRecord($recordId);
if ($record) {
$periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30);
@@ -639,9 +645,9 @@ final class GameLiveService
if ($existingResult !== false && $existingResult >= 1 && $existingResult <= self::DRAW_NUMBER_MAX && $st >= 2) {
Db::commit();
$periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : '';
GameHotDataCoordinator::afterGameRecordCommitted($rid);
$notifyAfterLock = static function () use ($rid): void {
self::publishSnapshot($rid);
GameHotDataCoordinator::afterGameRecordCommitted($rid);
self::publishSnapshot($rid, false);
};
$result = [
'ok' => true,
@@ -685,20 +691,6 @@ final class GameLiveService
}
if ($drawCommitted) {
GameBetSettleService::publishSettlementWinsAfterCommit(
$settleOut,
$rid,
$periodNo,
$finalNumber
);
GameHotDataCoordinator::afterGameRecordCommitted($rid);
try {
GameRecordStatService::refreshForRecordId($rid);
} catch (Throwable) {
}
$notifyAfterLock = static function () use (
$rid,
$periodNo,
@@ -708,6 +700,17 @@ final class GameLiveService
$now,
$settleOut
): void {
GameBetSettleService::publishSettlementWinsAfterCommit(
$settleOut,
$rid,
$periodNo,
$finalNumber
);
GameHotDataCoordinator::afterGameRecordCommitted($rid);
try {
GameRecordStatService::refreshForRecordId($rid);
} catch (Throwable) {
}
self::publishPublicPeriodOpened($periodNo, $finalNumber, $drawMode, $now);
self::publishPublicPeriodPayout($rid, $periodNo, $finalNumber, $payoutUntil, $now);
$jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : [];
@@ -720,7 +723,7 @@ final class GameLiveService
'jackpot_hits' => $jackpotHits,
'server_time' => $now,
]);
self::publishSnapshot(null);
self::publishSnapshot(null, false);
};
$result = [
@@ -833,14 +836,13 @@ final class GameLiveService
'maintenance_ui' => !$runtimeEnabled && !GameRecordService::hasActiveRecord(),
'server_time' => $now,
]);
self::publishSnapshot(null);
if (!$runtimeEnabled) {
self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize();
GameHotDataRedis::gameRecordRefreshAggregateCaches();
self::publishSnapshot(null);
} elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') {
self::publishImmediateBettingTickAfterFinalize();
}
self::publishSnapshot(null, false);
};
} finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $id, $lock['token'], $lock['redis_lock']);
@@ -966,11 +968,12 @@ final class GameLiveService
if (!in_array($st, [0, 1], true)) {
return ['ok' => false, 'msg' => __('Current period cannot be voided')];
}
$lock = self::acquireRecordLockForAdminMutation((string) $recordId, 3000);
$lock = self::acquireRecordLockForAdminMutation((string) $recordId, 1500);
if (!$lock['acquired']) {
return ['ok' => false, 'msg' => __('Another operation is in progress for this period; please try again later')];
}
$refundedUserIds = [];
$refund = ['user_ids' => [], 'order_count' => 0, 'total_amount' => '0.00', 'order_ids' => []];
try {
$now = time();
Db::startTrans();
@@ -990,20 +993,21 @@ final class GameLiveService
Db::rollback();
return ['ok' => false, 'msg' => __('Void failed') . ': ' . $e->getMessage()];
}
GameHotDataCoordinator::afterGameRecordCommitted($recordId);
foreach ($refundedUserIds as $uid) {
if ($uid > 0) {
GameHotDataCoordinator::afterUserCommitted($uid);
}
}
return [
'ok' => true,
'msg' => __('Period voided'),
'refund' => $refund,
];
} finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $recordId, $lock['token'], $lock['redis_lock']);
}
GameHotDataCoordinator::afterGameRecordCommitted($recordId);
foreach ($refundedUserIds as $uid) {
if ($uid > 0) {
GameHotDataCoordinator::afterUserCommitted($uid);
}
}
return [
'ok' => true,
'msg' => __('Period voided'),
'refund' => $refund,
];
}
/**
@@ -1044,17 +1048,30 @@ final class GameLiveService
}
GameRecordService::setAutoCreateEnabled(false);
GameHotDataCoordinator::afterGameConfigKeyCommitted(GameRecordService::KEY_AUTO_CREATE);
self::publishSnapshot(null);
$refund = $internal['refund'] ?? null;
return [
'ok' => true,
'msg' => __('Period voided'),
'record' => self::reloadRecord($rid),
'refund' => is_array($refund) ? $refund : null,
];
}
/**
* 作废本局后返回给前端的轻量快照(不再触发 publishSnapshot避免 HTTP 超时)。
*
* @return array<string, mixed>
*/
public static function buildSnapshotAfterVoid(): array
{
GameHotDataRedis::gameRecordRefreshAggregateCaches();
$snapshot = self::buildSnapshot(null);
$snapshot['runtime_enabled'] = false;
$snapshot['maintenance_ui'] = true;
return $snapshot;
}
/**
* @return list<int>
*/
@@ -1148,8 +1165,11 @@ final class GameLiveService
];
}
public static function publishSnapshot(?int $recordId = null): void
public static function publishSnapshot(?int $recordId = null, bool $runRecovery = true): void
{
if ($runRecovery) {
self::recoverLiveRoundState();
}
$snapshot = self::buildSnapshot($recordId);
self::publishPublicPeriodPayoutCountdown($snapshot);
self::publishPublicPeriodTick($snapshot);
@@ -1649,14 +1669,15 @@ final class GameLiveService
if ($recordId === '') {
return ['acquired' => false, 'token' => null, 'redis_lock' => false];
}
GameHotDataLock::forceRelease(GameHotDataLock::TYPE_GAME_RECORD, $recordId);
$lock = GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, $recordId, $waitMs);
if ($lock['acquired']) {
return $lock;
}
GameHotDataLock::forceRelease(GameHotDataLock::TYPE_GAME_RECORD, $recordId);
Log::warning('admin record lock force-released before void', ['record_id' => $recordId]);
Log::warning('admin record lock force-released before void retry', ['record_id' => $recordId]);
return GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, $recordId, 800);
return GameHotDataLock::tryAcquire(GameHotDataLock::TYPE_GAME_RECORD, $recordId);
}
/**

View File

@@ -15,8 +15,6 @@ class GameLiveTicker
GameLiveService::recoverAbnormalPeriodOnStartup();
Timer::add(1, static function (): void {
GameLiveService::finalizePayoutGrace();
GameLiveService::tickAutoDraw();
GameLiveService::publishSnapshot(null);
});
}

View File

@@ -85,9 +85,7 @@ class GameWebSocketServer
if (!$hasAdminSubscriber) {
return;
}
// 与 GameLiveTicker 对齐:仅推快照时若 live ticker 未运行会导致倒计时归零但永不开奖
GameLiveService::finalizePayoutGrace();
GameLiveService::tickAutoDraw();
GameLiveService::recoverLiveRoundState();
$snapshot = GameLiveService::buildSnapshot(null);
$payload = json_encode([
'event' => 'admin.live.snapshot',

View File

@@ -899,6 +899,7 @@ async function submitVoidPeriod(): Promise<void> {
record_id: snapshot.record.id,
void_reason: reason,
},
timeout: 60 * 1000,
showSuccessMessage: true,
})
if (res.code === 1 && res.data) {