diff --git a/app/admin/controller/game/Live.php b/app/admin/controller/game/Live.php index f5ab7cf..07a8d1a 100644 --- a/app/admin/controller/game/Live.php +++ b/app/admin/controller/game/Live.php @@ -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); } } diff --git a/app/common/service/GameLiveService.php b/app/common/service/GameLiveService.php index 9759320..fcb8481 100644 --- a/app/common/service/GameLiveService.php +++ b/app/common/service/GameLiveService.php @@ -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 + */ + public static function buildSnapshotAfterVoid(): array + { + GameHotDataRedis::gameRecordRefreshAggregateCaches(); + $snapshot = self::buildSnapshot(null); + $snapshot['runtime_enabled'] = false; + $snapshot['maintenance_ui'] = true; + + return $snapshot; + } + /** * @return list */ @@ -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); } /** diff --git a/app/process/GameLiveTicker.php b/app/process/GameLiveTicker.php index b93efc4..d4e1388 100644 --- a/app/process/GameLiveTicker.php +++ b/app/process/GameLiveTicker.php @@ -15,8 +15,6 @@ class GameLiveTicker GameLiveService::recoverAbnormalPeriodOnStartup(); Timer::add(1, static function (): void { - GameLiveService::finalizePayoutGrace(); - GameLiveService::tickAutoDraw(); GameLiveService::publishSnapshot(null); }); } diff --git a/app/process/GameWebSocketServer.php b/app/process/GameWebSocketServer.php index 1405ab2..c93c158 100644 --- a/app/process/GameWebSocketServer.php +++ b/app/process/GameWebSocketServer.php @@ -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', diff --git a/web/src/views/backend/game/live/index.vue b/web/src/views/backend/game/live/index.vue index c35ae14..05bfdd6 100644 --- a/web/src/views/backend/game/live/index.vue +++ b/web/src/views/backend/game/live/index.vue @@ -899,6 +899,7 @@ async function submitVoidPeriod(): Promise { record_id: snapshot.record.id, void_reason: reason, }, + timeout: 60 * 1000, showSuccessMessage: true, }) if (res.code === 1 && res.data) {