1.修复自动创建下一期bug
This commit is contained in:
@@ -68,6 +68,23 @@ final class GameHotDataLock
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员强制操作前释放可能残留的互斥锁(仅删 key,不校验 token)。
|
||||||
|
*/
|
||||||
|
public static function forceRelease(string $type, string $resourceKey): void
|
||||||
|
{
|
||||||
|
if ($resourceKey === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$client = Redis::connection()->client();
|
||||||
|
if (is_object($client) && method_exists($client, 'del')) {
|
||||||
|
$client->del(self::lockKey($type, $resourceKey));
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function release(string $type, string $resourceKey, ?string $token, bool $redisLock): void
|
public static function release(string $type, string $resourceKey, ?string $token, bool $redisLock): void
|
||||||
{
|
{
|
||||||
if ($resourceKey === '' || !$redisLock || $token === null || $token === '') {
|
if ($resourceKey === '' || !$redisLock || $token === null || $token === '') {
|
||||||
|
|||||||
@@ -572,10 +572,10 @@ final class GameLiveService
|
|||||||
return ['ok' => false, 'msg' => __('Failed to save scheduled draw number; please try again')];
|
return ['ok' => false, 'msg' => __('Failed to save scheduled draw number; please try again')];
|
||||||
}
|
}
|
||||||
GameHotDataCoordinator::afterGameRecordCommitted($rid);
|
GameHotDataCoordinator::afterGameRecordCommitted($rid);
|
||||||
self::publishSnapshot(null);
|
|
||||||
} finally {
|
} finally {
|
||||||
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
|
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
|
||||||
}
|
}
|
||||||
|
self::publishSnapshot(null);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
@@ -610,6 +610,11 @@ final class GameLiveService
|
|||||||
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')];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @var null|callable(): void */
|
||||||
|
$notifyAfterLock = null;
|
||||||
|
$result = ['ok' => false, 'msg' => __('Game live: settlement error')];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
self::ensureAiLocked($rid);
|
self::ensureAiLocked($rid);
|
||||||
|
|
||||||
@@ -620,102 +625,122 @@ final class GameLiveService
|
|||||||
$payoutUntil = 0;
|
$payoutUntil = 0;
|
||||||
$periodNo = '';
|
$periodNo = '';
|
||||||
$now = time();
|
$now = time();
|
||||||
|
$drawCommitted = false;
|
||||||
|
|
||||||
Db::startTrans();
|
Db::startTrans();
|
||||||
try {
|
try {
|
||||||
$record = self::loadRecordRowFromDb($rid, true);
|
$record = self::loadRecordRowFromDb($rid, true);
|
||||||
if (!$record) {
|
if (!$record) {
|
||||||
Db::rollback();
|
Db::rollback();
|
||||||
return ['ok' => false, 'msg' => __('No active game in progress')];
|
$result = ['ok' => false, 'msg' => __('No active game in progress')];
|
||||||
|
} else {
|
||||||
|
$st = (int) ($record['status'] ?? -1);
|
||||||
|
$existingResult = filter_var($record['result_number'] ?? 0, FILTER_VALIDATE_INT);
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
$result = [
|
||||||
|
'ok' => true,
|
||||||
|
'msg' => __('Draw completed; paying out'),
|
||||||
|
'result_number' => $existingResult,
|
||||||
|
'estimated_loss' => '0.00',
|
||||||
|
'payout_until' => (int) ($record['payout_until'] ?? 0),
|
||||||
|
];
|
||||||
|
} elseif (!in_array($st, [0, 1], true)) {
|
||||||
|
Db::rollback();
|
||||||
|
$result = ['ok' => false, 'msg' => __('Current game status does not allow drawing')];
|
||||||
|
} else {
|
||||||
|
$elapsedLocked = max(0, $now - (int) ($record['period_start_at'] ?? $now));
|
||||||
|
if ($elapsedLocked < $betSeconds || $elapsedLocked < $periodSeconds) {
|
||||||
|
Db::rollback();
|
||||||
|
$result = ['ok' => false, 'msg' => __('Period countdown has not ended; cannot draw yet')];
|
||||||
|
} else {
|
||||||
|
[$finalNumber, $drawMode] = self::resolveFinalDrawNumber($record, $manualNumber);
|
||||||
|
$bets = Db::name('bet_order')->where('period_id', $rid)->select()->toArray();
|
||||||
|
$finalLoss = self::estimateLossForNumber($bets, $finalNumber);
|
||||||
|
$payoutUntil = $now + self::getPayoutGraceSeconds();
|
||||||
|
$periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : '';
|
||||||
|
|
||||||
|
Db::name('game_record')->where('id', $rid)->update([
|
||||||
|
'status' => 3,
|
||||||
|
'result_number' => $finalNumber,
|
||||||
|
'draw_mode' => $drawMode,
|
||||||
|
'pending_draw_number' => null,
|
||||||
|
'payout_until' => $payoutUntil,
|
||||||
|
'update_time' => $now,
|
||||||
|
]);
|
||||||
|
$settleOut = GameBetSettleService::settleBetsForDraw($rid, $finalNumber);
|
||||||
|
Db::commit();
|
||||||
|
$drawCommitted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$st = (int) ($record['status'] ?? -1);
|
|
||||||
$existingResult = filter_var($record['result_number'] ?? 0, FILTER_VALIDATE_INT);
|
|
||||||
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);
|
|
||||||
self::publishSnapshot($rid);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'ok' => true,
|
|
||||||
'msg' => __('Draw completed; paying out'),
|
|
||||||
'result_number' => $existingResult,
|
|
||||||
'estimated_loss' => '0.00',
|
|
||||||
'payout_until' => (int) ($record['payout_until'] ?? 0),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!in_array($st, [0, 1], true)) {
|
|
||||||
Db::rollback();
|
|
||||||
return ['ok' => false, 'msg' => __('Current game status does not allow drawing')];
|
|
||||||
}
|
|
||||||
|
|
||||||
$elapsedLocked = max(0, $now - (int) ($record['period_start_at'] ?? $now));
|
|
||||||
if ($elapsedLocked < $betSeconds || $elapsedLocked < $periodSeconds) {
|
|
||||||
Db::rollback();
|
|
||||||
return ['ok' => false, 'msg' => __('Period countdown has not ended; cannot draw yet')];
|
|
||||||
}
|
|
||||||
|
|
||||||
[$finalNumber, $drawMode] = self::resolveFinalDrawNumber($record, $manualNumber);
|
|
||||||
$bets = Db::name('bet_order')->where('period_id', $rid)->select()->toArray();
|
|
||||||
$finalLoss = self::estimateLossForNumber($bets, $finalNumber);
|
|
||||||
$payoutUntil = $now + self::getPayoutGraceSeconds();
|
|
||||||
$periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : '';
|
|
||||||
|
|
||||||
Db::name('game_record')->where('id', $rid)->update([
|
|
||||||
'status' => 3,
|
|
||||||
'result_number' => $finalNumber,
|
|
||||||
'draw_mode' => $drawMode,
|
|
||||||
'pending_draw_number' => null,
|
|
||||||
'payout_until' => $payoutUntil,
|
|
||||||
'update_time' => $now,
|
|
||||||
]);
|
|
||||||
$settleOut = GameBetSettleService::settleBetsForDraw($rid, $finalNumber);
|
|
||||||
Db::commit();
|
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Db::rollback();
|
Db::rollback();
|
||||||
return ['ok' => false, 'msg' => __('Game live: settlement error') . ': ' . $e->getMessage()];
|
$result = ['ok' => false, 'msg' => __('Game live: settlement error') . ': ' . $e->getMessage()];
|
||||||
}
|
}
|
||||||
|
|
||||||
GameBetSettleService::publishSettlementWinsAfterCommit(
|
if ($drawCommitted) {
|
||||||
$settleOut,
|
GameBetSettleService::publishSettlementWinsAfterCommit(
|
||||||
$rid,
|
$settleOut,
|
||||||
$periodNo,
|
$rid,
|
||||||
$finalNumber
|
$periodNo,
|
||||||
);
|
$finalNumber
|
||||||
|
);
|
||||||
|
|
||||||
GameHotDataCoordinator::afterGameRecordCommitted($rid);
|
GameHotDataCoordinator::afterGameRecordCommitted($rid);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
GameRecordStatService::refreshForRecordId($rid);
|
GameRecordStatService::refreshForRecordId($rid);
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
$notifyAfterLock = static function () use (
|
||||||
|
$rid,
|
||||||
|
$periodNo,
|
||||||
|
$finalNumber,
|
||||||
|
$drawMode,
|
||||||
|
$payoutUntil,
|
||||||
|
$now,
|
||||||
|
$settleOut
|
||||||
|
): void {
|
||||||
|
self::publishPublicPeriodOpened($periodNo, $finalNumber, $drawMode, $now);
|
||||||
|
self::publishPublicPeriodPayout($rid, $periodNo, $finalNumber, $payoutUntil, $now);
|
||||||
|
$jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : [];
|
||||||
|
GameWebSocketEventBus::publish('admin.live.opened', [
|
||||||
|
'period_id' => $rid,
|
||||||
|
'period_no' => $periodNo,
|
||||||
|
'result_number' => $finalNumber,
|
||||||
|
'draw_mode' => $drawMode,
|
||||||
|
'payout_until' => $payoutUntil,
|
||||||
|
'jackpot_hits' => $jackpotHits,
|
||||||
|
'server_time' => $now,
|
||||||
|
]);
|
||||||
|
self::publishSnapshot(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'ok' => true,
|
||||||
|
'msg' => __('Draw completed; paying out'),
|
||||||
|
'result_number' => $finalNumber,
|
||||||
|
'draw_mode' => $drawMode,
|
||||||
|
'estimated_loss' => $finalLoss,
|
||||||
|
'payout_until' => $payoutUntil,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
self::publishPublicPeriodOpened($periodNo, $finalNumber, $drawMode, $now);
|
|
||||||
self::publishPublicPeriodPayout($rid, $periodNo, $finalNumber, $payoutUntil, $now);
|
|
||||||
$jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : [];
|
|
||||||
GameWebSocketEventBus::publish('admin.live.opened', [
|
|
||||||
'period_id' => $rid,
|
|
||||||
'period_no' => $periodNo,
|
|
||||||
'result_number' => $finalNumber,
|
|
||||||
'draw_mode' => $drawMode,
|
|
||||||
'payout_until' => $payoutUntil,
|
|
||||||
'jackpot_hits' => $jackpotHits,
|
|
||||||
'server_time' => $now,
|
|
||||||
]);
|
|
||||||
self::publishSnapshot(null);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'ok' => true,
|
|
||||||
'msg' => __('Draw completed; paying out'),
|
|
||||||
'result_number' => $finalNumber,
|
|
||||||
'draw_mode' => $drawMode,
|
|
||||||
'estimated_loss' => $finalLoss,
|
|
||||||
'payout_until' => $payoutUntil,
|
|
||||||
];
|
|
||||||
} finally {
|
} finally {
|
||||||
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
|
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($notifyAfterLock !== null) {
|
||||||
|
$notifyAfterLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -739,6 +764,10 @@ final class GameLiveService
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @var null|callable(): void */
|
||||||
|
$notifyAfterLock = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$periodNo = is_string($row['period_no'] ?? null) ? (string) $row['period_no'] : '';
|
$periodNo = is_string($row['period_no'] ?? null) ? (string) $row['period_no'] : '';
|
||||||
$resultNumber = filter_var($row['result_number'] ?? 0, FILTER_VALIDATE_INT);
|
$resultNumber = filter_var($row['result_number'] ?? 0, FILTER_VALIDATE_INT);
|
||||||
@@ -747,6 +776,7 @@ final class GameLiveService
|
|||||||
}
|
}
|
||||||
$newPeriodNo = null;
|
$newPeriodNo = null;
|
||||||
$now = time();
|
$now = time();
|
||||||
|
$finalized = false;
|
||||||
Db::startTrans();
|
Db::startTrans();
|
||||||
try {
|
try {
|
||||||
$affected = Db::name('game_record')
|
$affected = Db::name('game_record')
|
||||||
@@ -759,16 +789,19 @@ final class GameLiveService
|
|||||||
]);
|
]);
|
||||||
if ($affected < 1) {
|
if ($affected < 1) {
|
||||||
Db::rollback();
|
Db::rollback();
|
||||||
|
} else {
|
||||||
return;
|
Db::commit();
|
||||||
|
$finalized = true;
|
||||||
}
|
}
|
||||||
Db::commit();
|
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Db::rollback();
|
Db::rollback();
|
||||||
Log::warning('finalizePayoutGrace failed: ' . $e->getMessage(), ['record_id' => $id]);
|
Log::warning('finalizePayoutGrace failed: ' . $e->getMessage(), ['record_id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$finalized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GameRecordService::isAutoCreateEnabled()) {
|
if (GameRecordService::isAutoCreateEnabled()) {
|
||||||
try {
|
try {
|
||||||
$newPeriodNo = GameRecordService::createNextRecordAfterDraw();
|
$newPeriodNo = GameRecordService::createNextRecordAfterDraw();
|
||||||
@@ -781,27 +814,41 @@ final class GameLiveService
|
|||||||
}
|
}
|
||||||
GameHotDataCoordinator::afterGameRecordCommitted($id);
|
GameHotDataCoordinator::afterGameRecordCommitted($id);
|
||||||
GameRecordStatService::refreshForRecordId($id);
|
GameRecordStatService::refreshForRecordId($id);
|
||||||
self::publishPublicPeriodFinished($id, $periodNo, $resultNumber);
|
|
||||||
GameWebSocketEventBus::publish('admin.live.finalized', [
|
$runtimeEnabled = GameRecordService::isLiveRuntimeEnabled();
|
||||||
'period_id' => $id,
|
$notifyAfterLock = static function () use (
|
||||||
'period_no' => $periodNo,
|
$id,
|
||||||
'result_number' => $resultNumber,
|
$periodNo,
|
||||||
'runtime_enabled' => GameRecordService::isLiveRuntimeEnabled(),
|
$resultNumber,
|
||||||
'maintenance_ui' => !GameRecordService::isLiveRuntimeEnabled()
|
$now,
|
||||||
&& !GameRecordService::hasActiveRecord(),
|
$runtimeEnabled,
|
||||||
'server_time' => $now,
|
$newPeriodNo
|
||||||
]);
|
): void {
|
||||||
self::publishSnapshot(null);
|
self::publishPublicPeriodFinished($id, $periodNo, $resultNumber);
|
||||||
if (!GameRecordService::isLiveRuntimeEnabled()) {
|
GameWebSocketEventBus::publish('admin.live.finalized', [
|
||||||
self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize();
|
'period_id' => $id,
|
||||||
GameHotDataRedis::gameRecordRefreshAggregateCaches();
|
'period_no' => $periodNo,
|
||||||
|
'result_number' => $resultNumber,
|
||||||
|
'runtime_enabled' => $runtimeEnabled,
|
||||||
|
'maintenance_ui' => !$runtimeEnabled && !GameRecordService::hasActiveRecord(),
|
||||||
|
'server_time' => $now,
|
||||||
|
]);
|
||||||
self::publishSnapshot(null);
|
self::publishSnapshot(null);
|
||||||
} elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') {
|
if (!$runtimeEnabled) {
|
||||||
self::publishImmediateBettingTickAfterFinalize();
|
self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize();
|
||||||
}
|
GameHotDataRedis::gameRecordRefreshAggregateCaches();
|
||||||
|
self::publishSnapshot(null);
|
||||||
|
} elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') {
|
||||||
|
self::publishImmediateBettingTickAfterFinalize();
|
||||||
|
}
|
||||||
|
};
|
||||||
} 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']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($notifyAfterLock !== null) {
|
||||||
|
$notifyAfterLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function tickAutoDraw(): void
|
public static function tickAutoDraw(): void
|
||||||
@@ -919,7 +966,7 @@ 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 = GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, (string) $recordId, 3000);
|
$lock = self::acquireRecordLockForAdminMutation((string) $recordId, 3000);
|
||||||
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')];
|
||||||
}
|
}
|
||||||
@@ -1592,6 +1639,26 @@ final class GameLiveService
|
|||||||
return $max;
|
return $max;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员作废等操作:等待锁失败时清除残留锁再试一次,避免 ticker/开奖持锁导致无法作废。
|
||||||
|
*
|
||||||
|
* @return array{acquired: bool, token: ?string, redis_lock: bool}
|
||||||
|
*/
|
||||||
|
private static function acquireRecordLockForAdminMutation(string $recordId, int $waitMs): array
|
||||||
|
{
|
||||||
|
if ($recordId === '') {
|
||||||
|
return ['acquired' => false, 'token' => null, 'redis_lock' => false];
|
||||||
|
}
|
||||||
|
$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]);
|
||||||
|
|
||||||
|
return GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, $recordId, 800);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 封盘至本期完全结束前均展示赔付预估(含已开奖/派彩中),供后台实时对局页保留表格数据。
|
* 封盘至本期完全结束前均展示赔付预估(含已开奖/派彩中),供后台实时对局页保留表格数据。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
>
|
>
|
||||||
{{ t('game.live.void_btn') }}
|
{{ t('game.live.void_btn') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button :loading="loading" :disabled="asideOperationLocked" @click="loadSnapshot({ force: true })">{{ t('Refresh') }}</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,12 +276,7 @@ const serverSkewSeconds = ref(0)
|
|||||||
/** 每秒递增,驱动派彩剩余秒本地刷新 */
|
/** 每秒递增,驱动派彩剩余秒本地刷新 */
|
||||||
const clockTick = ref(0)
|
const clockTick = ref(0)
|
||||||
let clockTimer: number | null = null
|
let clockTimer: number | null = null
|
||||||
let payoutStuckRefreshTimer: number | null = null
|
|
||||||
let drawStuckRefreshTimer: number | null = null
|
|
||||||
let drawStuckSeconds = 0
|
|
||||||
let payoutPhaseStuckSeconds = 0
|
|
||||||
let fallbackPollTimer: number | null = null
|
let fallbackPollTimer: number | null = null
|
||||||
let betStreamRefreshTimer: number | null = null
|
|
||||||
/** 合并并发 snapshot 请求,避免 axios 重复请求取消导致控制台报错 */
|
/** 合并并发 snapshot 请求,避免 axios 重复请求取消导致控制台报错 */
|
||||||
let snapshotLoadPromise: Promise<void> | null = null
|
let snapshotLoadPromise: Promise<void> | null = null
|
||||||
|
|
||||||
@@ -350,16 +344,23 @@ function handleWsPayload(raw: unknown): void {
|
|||||||
snapshot.result_number = opened.result_number
|
snapshot.result_number = opened.result_number
|
||||||
calcResultNumber.value = opened.result_number
|
calcResultNumber.value = opened.result_number
|
||||||
}
|
}
|
||||||
void loadSnapshot({ force: true })
|
if (typeof opened.payout_until === 'number' && opened.payout_until > 0 && snapshot.record) {
|
||||||
|
snapshot.record.payout_until = opened.payout_until
|
||||||
|
snapshot.is_payout_phase = true
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event === 'admin.live.finalized' && parsed.data && typeof parsed.data === 'object') {
|
if (event === 'admin.live.finalized' && parsed.data && typeof parsed.data === 'object') {
|
||||||
const fin = parsed.data as anyObj
|
const fin = parsed.data as anyObj
|
||||||
|
snapshot.is_payout_phase = false
|
||||||
|
snapshot.payout_remaining_seconds = 0
|
||||||
if (toBool(fin.maintenance_ui) === true) {
|
if (toBool(fin.maintenance_ui) === true) {
|
||||||
snapshot.is_payout_phase = false
|
snapshot.maintenance_ui = true
|
||||||
snapshot.payout_remaining_seconds = 0
|
snapshot.record = null
|
||||||
|
}
|
||||||
|
if (toBool(fin.runtime_enabled) !== null) {
|
||||||
|
snapshot.runtime_enabled = toBool(fin.runtime_enabled) === true
|
||||||
}
|
}
|
||||||
void loadSnapshot({ force: true })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event === 'period.payout' && parsed.data && typeof parsed.data === 'object') {
|
if (event === 'period.payout' && parsed.data && typeof parsed.data === 'object') {
|
||||||
@@ -393,30 +394,7 @@ function handleWsPayload(raw: unknown): void {
|
|||||||
}
|
}
|
||||||
if (event === 'period.tick' && parsed.data && typeof parsed.data === 'object') {
|
if (event === 'period.tick' && parsed.data && typeof parsed.data === 'object') {
|
||||||
handlePeriodTickEvent(parsed.data as anyObj)
|
handlePeriodTickEvent(parsed.data as anyObj)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if (event === 'bet.accepted' && parsed.data && typeof parsed.data === 'object') {
|
|
||||||
const betData = parsed.data as anyObj
|
|
||||||
const periodNo = typeof betData.period_no === 'string' ? betData.period_no : ''
|
|
||||||
const currentNo = typeof snapshot.record?.period_no === 'string' ? snapshot.record.period_no : ''
|
|
||||||
if (periodNo !== '' && periodNo === currentNo) {
|
|
||||||
scheduleBetStreamRefresh()
|
|
||||||
} else if (!wsConnected.value) {
|
|
||||||
void loadSnapshot({ force: true })
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 有新下注时防抖拉取快照,补全 WS 每秒快照之间的下注列表 */
|
|
||||||
function scheduleBetStreamRefresh(): void {
|
|
||||||
if (betStreamRefreshTimer !== null) {
|
|
||||||
window.clearTimeout(betStreamRefreshTimer)
|
|
||||||
}
|
|
||||||
betStreamRefreshTimer = window.setTimeout(() => {
|
|
||||||
betStreamRefreshTimer = null
|
|
||||||
void loadSnapshot({ force: true })
|
|
||||||
}, 600)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 用 period.tick 轻量字段刷新倒计时(不触发 HTTP,避免与 WS 每秒 snapshot 冲突) */
|
/** 用 period.tick 轻量字段刷新倒计时(不触发 HTTP,避免与 WS 每秒 snapshot 冲突) */
|
||||||
@@ -457,18 +435,9 @@ function handlePeriodTickEvent(periodData: anyObj): void {
|
|||||||
if (runtimeOff && (status === 'betting' || status === 'locked')) {
|
if (runtimeOff && (status === 'betting' || status === 'locked')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (status === 'betting' && periodNo !== '' && periodNo !== currentNo) {
|
|
||||||
void loadSnapshot({ force: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (status === 'finished') {
|
if (status === 'finished') {
|
||||||
snapshot.is_payout_phase = false
|
snapshot.is_payout_phase = false
|
||||||
snapshot.payout_remaining_seconds = 0
|
snapshot.payout_remaining_seconds = 0
|
||||||
void loadSnapshot({ force: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (currentNo === '' && periodNo !== '' && !runtimeOff) {
|
|
||||||
void loadSnapshot({ force: true })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,96 +984,17 @@ const countdownParts = computed(() => {
|
|||||||
return { bet, draw, payout }
|
return { bet, draw, payout }
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 派彩倒计时从 >0 变为 0 时主动拉 HTTP 快照 */
|
|
||||||
watch(payoutRemainingLive, (remain, prev) => {
|
|
||||||
if (!snapshot.is_payout_phase || remain !== 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (prev !== null && prev !== undefined && prev > 0) {
|
|
||||||
schedulePayoutEndRefresh(400)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function schedulePayoutEndRefresh(delayMs: number): void {
|
|
||||||
if (payoutStuckRefreshTimer !== null) {
|
|
||||||
window.clearTimeout(payoutStuckRefreshTimer)
|
|
||||||
}
|
|
||||||
payoutStuckRefreshTimer = window.setTimeout(() => {
|
|
||||||
payoutStuckRefreshTimer = null
|
|
||||||
if (!snapshot.is_payout_phase) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
void loadSnapshot({ force: true })
|
|
||||||
}, delayMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 下注/开奖倒计时均已归零但仍未进入派彩(常见于 live ticker 未跑或开奖锁竞争失败) */
|
|
||||||
function isPrePayoutDrawStuck(): boolean {
|
|
||||||
if (snapshot.is_payout_phase || !snapshot.can_calculate) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const bet = snapshot.bet_remaining_seconds ?? 0
|
|
||||||
const draw = snapshot.remaining_seconds ?? 0
|
|
||||||
if (bet > 0 || draw > 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const st = numberValue(snapshot.record?.status)
|
|
||||||
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 {
|
|
||||||
if (!isPrePayoutDrawStuck()) {
|
|
||||||
drawStuckSeconds = 0
|
|
||||||
if (drawStuckRefreshTimer !== null) {
|
|
||||||
window.clearTimeout(drawStuckRefreshTimer)
|
|
||||||
drawStuckRefreshTimer = null
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
drawStuckSeconds++
|
|
||||||
if (drawStuckSeconds < 8 || drawStuckRefreshTimer !== null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
drawStuckRefreshTimer = window.setTimeout(() => {
|
|
||||||
drawStuckRefreshTimer = null
|
|
||||||
drawStuckSeconds = 0
|
|
||||||
if (isPrePayoutDrawStuck()) {
|
|
||||||
void loadSnapshot({ force: true })
|
|
||||||
}
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
updateIsMobile()
|
updateIsMobile()
|
||||||
window.addEventListener('resize', updateIsMobile)
|
window.addEventListener('resize', updateIsMobile)
|
||||||
clockTimer = window.setInterval(() => {
|
clockTimer = window.setInterval(() => {
|
||||||
clockTick.value++
|
clockTick.value++
|
||||||
tickPrePayoutDrawStuckRecovery()
|
|
||||||
tickPayoutPhaseStuckRecovery()
|
|
||||||
}, 1000)
|
}, 1000)
|
||||||
fallbackPollTimer = window.setInterval(() => {
|
fallbackPollTimer = window.setInterval(() => {
|
||||||
if (!wsConnected.value) {
|
if (!wsConnected.value) {
|
||||||
void loadSnapshot({ force: true })
|
void loadSnapshot({ force: true })
|
||||||
}
|
}
|
||||||
}, 3000)
|
}, 15000)
|
||||||
await loadSnapshot({ force: true })
|
await loadSnapshot({ force: true })
|
||||||
await reloadWsConfig()
|
await reloadWsConfig()
|
||||||
connectWs()
|
connectWs()
|
||||||
@@ -1121,18 +1011,6 @@ onUnmounted(() => {
|
|||||||
window.clearInterval(fallbackPollTimer)
|
window.clearInterval(fallbackPollTimer)
|
||||||
fallbackPollTimer = null
|
fallbackPollTimer = null
|
||||||
}
|
}
|
||||||
if (payoutStuckRefreshTimer !== null) {
|
|
||||||
window.clearTimeout(payoutStuckRefreshTimer)
|
|
||||||
payoutStuckRefreshTimer = null
|
|
||||||
}
|
|
||||||
if (drawStuckRefreshTimer !== null) {
|
|
||||||
window.clearTimeout(drawStuckRefreshTimer)
|
|
||||||
drawStuckRefreshTimer = null
|
|
||||||
}
|
|
||||||
if (betStreamRefreshTimer !== null) {
|
|
||||||
window.clearTimeout(betStreamRefreshTimer)
|
|
||||||
betStreamRefreshTimer = null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user