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

View File

@@ -296,11 +296,17 @@ final class GameLiveService
self::publishSnapshot(null); self::publishSnapshot(null);
} }
/**
* 定时任务/WS 推送前:结单 + 超时自动开奖(勿在已持期号锁时调用)。
*/
public static function recoverLiveRoundState(): void
{
self::finalizePayoutGrace();
self::tickAutoDraw();
}
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);
@@ -639,9 +645,9 @@ final class GameLiveService
if ($existingResult !== false && $existingResult >= 1 && $existingResult <= self::DRAW_NUMBER_MAX && $st >= 2) { if ($existingResult !== false && $existingResult >= 1 && $existingResult <= self::DRAW_NUMBER_MAX && $st >= 2) {
Db::commit(); Db::commit();
$periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : ''; $periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : '';
GameHotDataCoordinator::afterGameRecordCommitted($rid);
$notifyAfterLock = static function () use ($rid): void { $notifyAfterLock = static function () use ($rid): void {
self::publishSnapshot($rid); GameHotDataCoordinator::afterGameRecordCommitted($rid);
self::publishSnapshot($rid, false);
}; };
$result = [ $result = [
'ok' => true, 'ok' => true,
@@ -685,20 +691,6 @@ final class GameLiveService
} }
if ($drawCommitted) { if ($drawCommitted) {
GameBetSettleService::publishSettlementWinsAfterCommit(
$settleOut,
$rid,
$periodNo,
$finalNumber
);
GameHotDataCoordinator::afterGameRecordCommitted($rid);
try {
GameRecordStatService::refreshForRecordId($rid);
} catch (Throwable) {
}
$notifyAfterLock = static function () use ( $notifyAfterLock = static function () use (
$rid, $rid,
$periodNo, $periodNo,
@@ -708,6 +700,17 @@ final class GameLiveService
$now, $now,
$settleOut $settleOut
): void { ): void {
GameBetSettleService::publishSettlementWinsAfterCommit(
$settleOut,
$rid,
$periodNo,
$finalNumber
);
GameHotDataCoordinator::afterGameRecordCommitted($rid);
try {
GameRecordStatService::refreshForRecordId($rid);
} catch (Throwable) {
}
self::publishPublicPeriodOpened($periodNo, $finalNumber, $drawMode, $now); self::publishPublicPeriodOpened($periodNo, $finalNumber, $drawMode, $now);
self::publishPublicPeriodPayout($rid, $periodNo, $finalNumber, $payoutUntil, $now); self::publishPublicPeriodPayout($rid, $periodNo, $finalNumber, $payoutUntil, $now);
$jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : []; $jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : [];
@@ -720,7 +723,7 @@ final class GameLiveService
'jackpot_hits' => $jackpotHits, 'jackpot_hits' => $jackpotHits,
'server_time' => $now, 'server_time' => $now,
]); ]);
self::publishSnapshot(null); self::publishSnapshot(null, false);
}; };
$result = [ $result = [
@@ -833,14 +836,13 @@ final class GameLiveService
'maintenance_ui' => !$runtimeEnabled && !GameRecordService::hasActiveRecord(), 'maintenance_ui' => !$runtimeEnabled && !GameRecordService::hasActiveRecord(),
'server_time' => $now, 'server_time' => $now,
]); ]);
self::publishSnapshot(null);
if (!$runtimeEnabled) { if (!$runtimeEnabled) {
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();
} }
self::publishSnapshot(null, false);
}; };
} finally { } finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $id, $lock['token'], $lock['redis_lock']); 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)) { if (!in_array($st, [0, 1], true)) {
return ['ok' => false, 'msg' => __('Current period cannot be voided')]; return ['ok' => false, 'msg' => __('Current period cannot be voided')];
} }
$lock = self::acquireRecordLockForAdminMutation((string) $recordId, 3000); $lock = self::acquireRecordLockForAdminMutation((string) $recordId, 1500);
if (!$lock['acquired']) { if (!$lock['acquired']) {
return ['ok' => false, 'msg' => __('Another operation is in progress for this period; please try again later')]; return ['ok' => false, 'msg' => __('Another operation is in progress for this period; please try again later')];
} }
$refundedUserIds = []; $refundedUserIds = [];
$refund = ['user_ids' => [], 'order_count' => 0, 'total_amount' => '0.00', 'order_ids' => []];
try { try {
$now = time(); $now = time();
Db::startTrans(); Db::startTrans();
@@ -990,20 +993,21 @@ final class GameLiveService
Db::rollback(); Db::rollback();
return ['ok' => false, 'msg' => __('Void failed') . ': ' . $e->getMessage()]; 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 { } finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $recordId, $lock['token'], $lock['redis_lock']); 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); GameRecordService::setAutoCreateEnabled(false);
GameHotDataCoordinator::afterGameConfigKeyCommitted(GameRecordService::KEY_AUTO_CREATE); GameHotDataCoordinator::afterGameConfigKeyCommitted(GameRecordService::KEY_AUTO_CREATE);
self::publishSnapshot(null);
$refund = $internal['refund'] ?? null; $refund = $internal['refund'] ?? null;
return [ return [
'ok' => true, 'ok' => true,
'msg' => __('Period voided'), 'msg' => __('Period voided'),
'record' => self::reloadRecord($rid),
'refund' => is_array($refund) ? $refund : null, '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> * @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); $snapshot = self::buildSnapshot($recordId);
self::publishPublicPeriodPayoutCountdown($snapshot); self::publishPublicPeriodPayoutCountdown($snapshot);
self::publishPublicPeriodTick($snapshot); self::publishPublicPeriodTick($snapshot);
@@ -1649,14 +1669,15 @@ final class GameLiveService
if ($recordId === '') { if ($recordId === '') {
return ['acquired' => false, 'token' => null, 'redis_lock' => false]; return ['acquired' => false, 'token' => null, 'redis_lock' => false];
} }
GameHotDataLock::forceRelease(GameHotDataLock::TYPE_GAME_RECORD, $recordId);
$lock = GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, $recordId, $waitMs); $lock = GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, $recordId, $waitMs);
if ($lock['acquired']) { if ($lock['acquired']) {
return $lock; return $lock;
} }
GameHotDataLock::forceRelease(GameHotDataLock::TYPE_GAME_RECORD, $recordId); 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(); GameLiveService::recoverAbnormalPeriodOnStartup();
Timer::add(1, static function (): void { Timer::add(1, static function (): void {
GameLiveService::finalizePayoutGrace();
GameLiveService::tickAutoDraw();
GameLiveService::publishSnapshot(null); GameLiveService::publishSnapshot(null);
}); });
} }

View File

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

View File

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