1.修复自动创建下一期bug
This commit is contained in:
@@ -25,7 +25,6 @@ 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));
|
||||||
}
|
}
|
||||||
@@ -38,7 +37,6 @@ 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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ final class WebSocketConfigHelper
|
|||||||
public static function wsUrl(?Request $request = null): string
|
public static function wsUrl(?Request $request = null): string
|
||||||
{
|
{
|
||||||
$url = trim((string) env('H5_WEBSOCKET_URL', ''));
|
$url = trim((string) env('H5_WEBSOCKET_URL', ''));
|
||||||
|
if ($url !== '' && $request !== null && self::isLoopbackWsUrl($url) && !self::isLoopbackRequestHost($request)) {
|
||||||
|
$url = '';
|
||||||
|
}
|
||||||
if ($url !== '') {
|
if ($url !== '') {
|
||||||
return $url;
|
return $url;
|
||||||
}
|
}
|
||||||
@@ -36,5 +39,30 @@ final class WebSocketConfigHelper
|
|||||||
|
|
||||||
return 'ws://127.0.0.1:3131/ws/';
|
return 'ws://127.0.0.1:3131/ws/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function isLoopbackWsUrl(string $url): bool
|
||||||
|
{
|
||||||
|
$host = parse_url($url, PHP_URL_HOST);
|
||||||
|
if (!is_string($host) || $host === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$host = strtolower($host);
|
||||||
|
|
||||||
|
return in_array($host, ['127.0.0.1', 'localhost', '::1'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function isLoopbackRequestHost(Request $request): bool
|
||||||
|
{
|
||||||
|
$host = strtolower(trim((string) $request->host(true)));
|
||||||
|
if ($host === '') {
|
||||||
|
$host = strtolower(trim((string) $request->header('host', '')));
|
||||||
|
}
|
||||||
|
if ($host === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$hostOnly = preg_split('/:/', $host)[0] ?? $host;
|
||||||
|
|
||||||
|
return in_array($hostOnly, ['127.0.0.1', 'localhost', '::1'], true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -641,60 +641,103 @@ final class GameLiveService
|
|||||||
$now = time();
|
$now = time();
|
||||||
$drawCommitted = false;
|
$drawCommitted = false;
|
||||||
|
|
||||||
Db::startTrans();
|
$txResult = self::withShortInnodbLockWait(3, static function () use (
|
||||||
try {
|
$rid,
|
||||||
$record = self::loadRecordRowFromDb($rid, true);
|
$betSeconds,
|
||||||
if (!$record) {
|
$periodSeconds,
|
||||||
Db::rollback();
|
$manualNumber,
|
||||||
$result = ['ok' => false, 'msg' => __('No active game in progress')];
|
$now
|
||||||
} else {
|
): array {
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
$record = self::loadRecordRowFromDb($rid, true);
|
||||||
|
if (!$record) {
|
||||||
|
Db::rollback();
|
||||||
|
|
||||||
|
return ['ok' => false, 'draw_committed' => false, 'result' => ['ok' => false, 'msg' => __('No active game in progress')]];
|
||||||
|
}
|
||||||
$st = (int) ($record['status'] ?? -1);
|
$st = (int) ($record['status'] ?? -1);
|
||||||
$existingResult = filter_var($record['result_number'] ?? 0, FILTER_VALIDATE_INT);
|
$existingResult = filter_var($record['result_number'] ?? 0, FILTER_VALIDATE_INT);
|
||||||
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'] : '';
|
|
||||||
$postLockWork = static function () use ($rid): void {
|
|
||||||
GameHotDataCoordinator::afterGameRecordCommitted($rid);
|
|
||||||
self::publishSnapshot($rid, false);
|
|
||||||
};
|
|
||||||
$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([
|
return [
|
||||||
'status' => 3,
|
'ok' => true,
|
||||||
'result_number' => $finalNumber,
|
'draw_committed' => false,
|
||||||
'draw_mode' => $drawMode,
|
'existing_result' => $existingResult,
|
||||||
'pending_draw_number' => null,
|
'record' => $record,
|
||||||
'payout_until' => $payoutUntil,
|
];
|
||||||
'update_time' => $now,
|
|
||||||
]);
|
|
||||||
Db::commit();
|
|
||||||
$drawCommitted = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (!in_array($st, [0, 1], true)) {
|
||||||
|
Db::rollback();
|
||||||
|
|
||||||
|
return ['ok' => false, 'draw_committed' => false, 'result' => ['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, 'draw_committed' => false, 'result' => ['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,
|
||||||
|
]);
|
||||||
|
Db::commit();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'draw_committed' => true,
|
||||||
|
'final_number' => $finalNumber,
|
||||||
|
'draw_mode' => $drawMode,
|
||||||
|
'final_loss' => $finalLoss,
|
||||||
|
'payout_until' => $payoutUntil,
|
||||||
|
'period_no' => $periodNo,
|
||||||
|
];
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
Db::rollback();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'draw_committed' => false,
|
||||||
|
'result' => ['ok' => false, 'msg' => __('Game live: settlement error') . ': ' . $e->getMessage()],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
});
|
||||||
Db::rollback();
|
|
||||||
$result = ['ok' => false, 'msg' => __('Game live: settlement error') . ': ' . $e->getMessage()];
|
if (!($txResult['ok'] ?? false)) {
|
||||||
|
$result = is_array($txResult['result'] ?? null) ? $txResult['result'] : ['ok' => false, 'msg' => __('Game live: settlement error')];
|
||||||
|
} elseif (!empty($txResult['draw_committed'])) {
|
||||||
|
$drawCommitted = true;
|
||||||
|
$finalNumber = (int) ($txResult['final_number'] ?? 0);
|
||||||
|
$drawMode = (int) ($txResult['draw_mode'] ?? 0);
|
||||||
|
$finalLoss = is_string($txResult['final_loss'] ?? null) ? $txResult['final_loss'] : '0.00';
|
||||||
|
$payoutUntil = (int) ($txResult['payout_until'] ?? 0);
|
||||||
|
$periodNo = is_string($txResult['period_no'] ?? null) ? (string) $txResult['period_no'] : '';
|
||||||
|
} else {
|
||||||
|
$existingResult = (int) ($txResult['existing_result'] ?? 0);
|
||||||
|
$periodNo = is_string($txResult['record']['period_no'] ?? null) ? (string) $txResult['record']['period_no'] : '';
|
||||||
|
$postLockWork = static function () use ($rid): void {
|
||||||
|
GameHotDataCoordinator::afterGameRecordCommitted($rid);
|
||||||
|
self::publishSnapshot($rid, false);
|
||||||
|
};
|
||||||
|
$result = [
|
||||||
|
'ok' => true,
|
||||||
|
'msg' => __('Draw completed; paying out'),
|
||||||
|
'result_number' => $existingResult,
|
||||||
|
'estimated_loss' => '0.00',
|
||||||
|
'payout_until' => (int) ($txResult['record']['payout_until'] ?? 0),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($drawCommitted) {
|
if ($drawCommitted) {
|
||||||
@@ -882,34 +925,33 @@ final class GameLiveService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$rid = (int) $record['id'];
|
$rid = (int) $record['id'];
|
||||||
if (GameHotDataRedis::isStaleOpenPeriodRecord($record, $periodSeconds)) {
|
$st = (int) ($record['status'] ?? 0);
|
||||||
GameHotDataLock::forceRelease(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid);
|
$abnormalAfter = $periodSeconds + self::getPayoutGraceSeconds() + self::STARTUP_RECOVER_GRACE_SECONDS;
|
||||||
|
if ($elapsed > $abnormalAfter) {
|
||||||
|
$resultNumber = isset($record['result_number']) ? (int) $record['result_number'] : 0;
|
||||||
|
if ($resultNumber <= 0) {
|
||||||
|
GameHotDataLock::forceRelease(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid);
|
||||||
|
self::markAbnormalAndRefundOnStartup($rid, $st);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$out = self::drawResult($rid, null);
|
$out = self::drawResult($rid, null);
|
||||||
if ($out['ok'] ?? false) {
|
if ($out['ok'] ?? false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$msg = is_string($out['msg'] ?? null) ? (string) $out['msg'] : '';
|
$msg = is_string($out['msg'] ?? null) ? (string) $out['msg'] : '';
|
||||||
if (!str_contains($msg, 'Another operation is in progress')) {
|
if (
|
||||||
|
!str_contains($msg, 'Another operation is in progress')
|
||||||
|
&& !str_contains($msg, 'Lock wait timeout')
|
||||||
|
&& !str_contains($msg, '1205')
|
||||||
|
) {
|
||||||
Log::warning('tickAutoDraw: drawResult failed', [
|
Log::warning('tickAutoDraw: drawResult failed', [
|
||||||
'record_id' => $rid,
|
'record_id' => $rid,
|
||||||
'period_no' => $record['period_no'] ?? '',
|
'period_no' => $record['period_no'] ?? '',
|
||||||
'msg' => $msg,
|
'msg' => $msg,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!GameHotDataRedis::isStaleOpenPeriodRecord($record, $periodSeconds)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
GameHotDataLock::forceRelease(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid);
|
|
||||||
$retry = self::drawResult($rid, null);
|
|
||||||
if (!($retry['ok'] ?? false)) {
|
|
||||||
Log::warning('tickAutoDraw: drawResult failed after lock force-release', [
|
|
||||||
'record_id' => $rid,
|
|
||||||
'period_no' => $record['period_no'] ?? '',
|
|
||||||
'msg' => is_string($retry['msg'] ?? null) ? $retry['msg'] : '',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1871,4 +1913,28 @@ final class GameLiveService
|
|||||||
}
|
}
|
||||||
return $parsed;
|
return $parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开奖事务使用较短行锁等待,避免 HTTP/定时任务被 InnoDB 默认 50s 锁等待拖死。
|
||||||
|
*
|
||||||
|
* @template T
|
||||||
|
* @param callable(): T $fn
|
||||||
|
* @return T
|
||||||
|
*/
|
||||||
|
private static function withShortInnodbLockWait(int $seconds, callable $fn): mixed
|
||||||
|
{
|
||||||
|
$seconds = max(1, min(50, $seconds));
|
||||||
|
try {
|
||||||
|
Db::execute('SET SESSION innodb_lock_wait_timeout = ' . $seconds);
|
||||||
|
} catch (Throwable) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return $fn();
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
Db::execute('SET SESSION innodb_lock_wait_timeout = 50');
|
||||||
|
} catch (Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export default {
|
|||||||
push_connected: 'Realtime connection established',
|
push_connected: 'Realtime connection established',
|
||||||
push_disconnected: 'Polling mode enabled (push removed)',
|
push_disconnected: 'Polling mode enabled (push removed)',
|
||||||
ws_connected: 'Connected to real-time match',
|
ws_connected: 'Connected to real-time match',
|
||||||
ws_disconnected: 'Service unavailable, please check backend service',
|
ws_disconnected: 'Realtime push disconnected, using polling',
|
||||||
|
snapshot_load_failed: 'Failed to load match snapshot, please retry',
|
||||||
ws_panel_title: 'Admin WebSocket (vs. mobile lightweight stream)',
|
ws_panel_title: 'Admin WebSocket (vs. mobile lightweight stream)',
|
||||||
ws_reload_config: 'Load WS config',
|
ws_reload_config: 'Load WS config',
|
||||||
ws_connect: 'Connect WS',
|
ws_connect: 'Connect WS',
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export default {
|
|||||||
push_connected: '实时连接已建立',
|
push_connected: '实时连接已建立',
|
||||||
push_disconnected: '已切换为轮询模式(无推送)',
|
push_disconnected: '已切换为轮询模式(无推送)',
|
||||||
ws_connected: '已连接实时对局',
|
ws_connected: '已连接实时对局',
|
||||||
ws_disconnected: '服务异常,请检查服务端',
|
ws_disconnected: '实时推送未连接,已使用轮询刷新',
|
||||||
|
snapshot_load_failed: '对局快照加载失败,请稍后重试',
|
||||||
ws_panel_title: '后台 WebSocket 连接(区别于前端轻量流)',
|
ws_panel_title: '后台 WebSocket 连接(区别于前端轻量流)',
|
||||||
ws_reload_config: '加载WS配置',
|
ws_reload_config: '加载WS配置',
|
||||||
ws_connect: '连接WS',
|
ws_connect: '连接WS',
|
||||||
|
|||||||
@@ -825,6 +825,7 @@ async function loadSnapshot(options?: { force?: boolean }): Promise<void> {
|
|||||||
const res = await createAxios({
|
const res = await createAxios({
|
||||||
url: '/admin/game.Live/snapshot',
|
url: '/admin/game.Live/snapshot',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
timeout: 30 * 1000,
|
||||||
showCodeMessage: false,
|
showCodeMessage: false,
|
||||||
showErrorMessage: false,
|
showErrorMessage: false,
|
||||||
cancelDuplicateRequest: false,
|
cancelDuplicateRequest: false,
|
||||||
@@ -996,7 +997,11 @@ onMounted(async () => {
|
|||||||
void loadSnapshot({ force: true })
|
void loadSnapshot({ force: true })
|
||||||
}
|
}
|
||||||
}, 15000)
|
}, 15000)
|
||||||
await loadSnapshot({ force: true })
|
try {
|
||||||
|
await loadSnapshot({ force: true })
|
||||||
|
} catch {
|
||||||
|
ElMessage.warning(t('game.live.snapshot_load_failed'))
|
||||||
|
}
|
||||||
await reloadWsConfig()
|
await reloadWsConfig()
|
||||||
connectWs()
|
connectWs()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const viteConfig = ({ mode }: ConfigEnv): UserConfig => {
|
|||||||
'/admin': { target: 'http://localhost:7979', changeOrigin: true },
|
'/admin': { target: 'http://localhost:7979', changeOrigin: true },
|
||||||
'/install': { target: 'http://localhost:7979', changeOrigin: true },
|
'/install': { target: 'http://localhost:7979', changeOrigin: true },
|
||||||
'/plugin': { target: 'http://localhost:7979', changeOrigin: true },
|
'/plugin': { target: 'http://localhost:7979', changeOrigin: true },
|
||||||
|
'/ws': { target: 'http://localhost:3131', changeOrigin: true, ws: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Reference in New Issue
Block a user