1.优化后台/admin/game/live
This commit is contained in:
@@ -270,10 +270,6 @@ final class GameLiveService
|
||||
GameHotDataRedis::gameRecordRevalidateFromDbIfStale($record, $periodSeconds);
|
||||
$record = self::resolveRecord($recordId);
|
||||
}
|
||||
if ($record && GameHotDataRedis::isExpiredPayoutRecord($record)) {
|
||||
self::finalizePayoutGrace();
|
||||
$record = self::resolveRecord($recordId);
|
||||
}
|
||||
if (!$record) {
|
||||
return self::emptySnapshotPayload();
|
||||
}
|
||||
@@ -307,13 +303,13 @@ final class GameLiveService
|
||||
->where('bo.period_id', $rid)
|
||||
->order('bo.id', 'desc')
|
||||
->limit(200)
|
||||
->field('bo.id,bo.user_id,bo.period_no,bo.pick_numbers,bo.total_amount,bo.streak_at_bet,bo.create_time,gu.username as user_username')
|
||||
->field('bo.id,bo.user_id,bo.period_no,bo.pick_numbers,bo.total_amount,bo.streak_at_bet,bo.win_amount,bo.status as bet_status,bo.create_time,gu.username as user_username')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$candidates = [];
|
||||
$canCalculate = $elapsed >= $betSeconds && ($status === 0 || $status === 1);
|
||||
if ($canCalculate) {
|
||||
if (self::shouldBuildCandidateEstimates($status, $elapsed, $betSeconds)) {
|
||||
for ($n = 1; $n <= self::DRAW_NUMBER_MAX; $n++) {
|
||||
$loss = self::estimateLossForNumber($bets, $n);
|
||||
$candidates[] = [
|
||||
@@ -323,6 +319,8 @@ final class GameLiveService
|
||||
}
|
||||
}
|
||||
|
||||
$resultNumber = isset($record['result_number']) ? (int) $record['result_number'] : 0;
|
||||
|
||||
$aiLocked = $record['ai_locked_number'] ?? null;
|
||||
$aiDisplay = null;
|
||||
if ($aiLocked !== null && $aiLocked !== '' && is_numeric((string) $aiLocked)) {
|
||||
@@ -357,10 +355,14 @@ final class GameLiveService
|
||||
'pick_numbers' => $row['pick_numbers'],
|
||||
'total_amount' => (string) $row['total_amount'],
|
||||
'streak_at_bet' => (int) $row['streak_at_bet'],
|
||||
'win_amount' => (string) ($row['win_amount'] ?? '0.00'),
|
||||
'bet_status' => (int) ($row['bet_status'] ?? 0),
|
||||
'create_time' => (int) $row['create_time'],
|
||||
];
|
||||
}, $bets),
|
||||
'candidate_numbers' => $candidates,
|
||||
'result_number' => $resultNumber > 0 ? $resultNumber : null,
|
||||
'show_settlement_preview' => self::shouldBuildCandidateEstimates($status, $elapsed, $betSeconds),
|
||||
'ai_default_number' => $aiDisplay,
|
||||
'calc_number' => $aiDisplay,
|
||||
'pending_draw_number' => $pendingDraw,
|
||||
@@ -412,6 +414,8 @@ final class GameLiveService
|
||||
'can_calculate' => false,
|
||||
'can_draw' => false,
|
||||
'can_schedule_draw' => false,
|
||||
'result_number' => null,
|
||||
'show_settlement_preview' => false,
|
||||
'server_time' => time(),
|
||||
];
|
||||
}
|
||||
@@ -1312,6 +1316,18 @@ final class GameLiveService
|
||||
return $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* 封盘至本期完全结束前均展示赔付预估(含已开奖/派彩中),供后台实时对局页保留表格数据。
|
||||
*/
|
||||
private static function shouldBuildCandidateEstimates(int $status, int $elapsed, int $betSeconds): bool
|
||||
{
|
||||
if ($elapsed < $betSeconds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($status, [0, 1, 2, 3, 4], true);
|
||||
}
|
||||
|
||||
private static function estimateLossForNumber(array $bets, int $number): string
|
||||
{
|
||||
$payout = '0.00';
|
||||
|
||||
@@ -34,6 +34,7 @@ export default {
|
||||
username: 'Username',
|
||||
pick_numbers: 'Pick numbers',
|
||||
total_amount: 'Total bet amount',
|
||||
win_amount: 'Payout amount',
|
||||
streak_at_bet: 'Streak at bet',
|
||||
runtime_switch: 'Auto-create next round',
|
||||
countdown_maintenance: 'Maintenance',
|
||||
|
||||
@@ -34,6 +34,7 @@ export default {
|
||||
username: '用户名',
|
||||
pick_numbers: '下注号码',
|
||||
total_amount: '下注总额',
|
||||
win_amount: '派彩金额',
|
||||
streak_at_bet: '下注时连胜',
|
||||
runtime_switch: '自动创建下一局',
|
||||
countdown_maintenance: '维护中',
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<div class="calc-result-bar">
|
||||
<span class="calc-result-bar__item">
|
||||
<span class="calc-result-bar__k">{{ t('game.live.calc_result_number') }}</span>
|
||||
<span class="calc-result-bar__v">{{ calcResultNumber ?? '—' }}</span>
|
||||
<span class="calc-result-bar__v">{{ displayResultNumber ?? '—' }}</span>
|
||||
</span>
|
||||
<span class="calc-result-bar__item">
|
||||
<span class="calc-result-bar__k">{{ t('game.live.calc_estimated_loss') }}</span>
|
||||
@@ -160,6 +160,11 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="total_amount" :label="t('game.live.total_amount')" width="92" align="center" header-align="center" />
|
||||
<el-table-column prop="win_amount" :label="t('game.live.win_amount')" width="92" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<span class="mono">{{ formatWinAmount(scope.row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
@@ -229,6 +234,8 @@ interface Snapshot {
|
||||
runtime_enabled?: boolean
|
||||
/** 完整维护 UI:关服且当前无进行中/未结清对局(派彩已全部完成) */
|
||||
maintenance_ui?: boolean
|
||||
/** 已开奖号码(status≥2 时有值) */
|
||||
result_number?: number | null
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -252,6 +259,7 @@ const snapshot = reactive<Snapshot>({
|
||||
can_schedule_draw: false,
|
||||
runtime_enabled: true,
|
||||
maintenance_ui: false,
|
||||
result_number: null,
|
||||
})
|
||||
const calcLoading = ref(false)
|
||||
const drawLoading = ref(false)
|
||||
@@ -271,6 +279,7 @@ const clockTick = ref(0)
|
||||
let clockTimer: number | null = null
|
||||
let payoutStuckRefreshTimer: number | null = null
|
||||
let fallbackPollTimer: number | null = null
|
||||
let betStreamRefreshTimer: number | null = null
|
||||
/** 合并并发 snapshot 请求,避免 axios 重复请求取消导致控制台报错 */
|
||||
let snapshotLoadPromise: Promise<void> | null = null
|
||||
|
||||
@@ -333,6 +342,11 @@ function handleWsPayload(raw: unknown): void {
|
||||
return
|
||||
}
|
||||
if (event === 'admin.live.opened') {
|
||||
const opened = parsed.data as anyObj
|
||||
if (typeof opened.result_number === 'number') {
|
||||
snapshot.result_number = opened.result_number
|
||||
calcResultNumber.value = opened.result_number
|
||||
}
|
||||
void loadSnapshot({ force: true })
|
||||
return
|
||||
}
|
||||
@@ -360,13 +374,29 @@ function handleWsPayload(raw: unknown): void {
|
||||
return
|
||||
}
|
||||
if (event === 'bet.accepted' && parsed.data && typeof parsed.data === 'object') {
|
||||
if (!wsConnected.value) {
|
||||
void loadSnapshot()
|
||||
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 冲突) */
|
||||
function mergePeriodTickFields(periodData: anyObj): void {
|
||||
if (typeof periodData.server_time === 'number') {
|
||||
@@ -568,8 +598,49 @@ function isScheduledNumber(v: unknown): boolean {
|
||||
return snapshot.pending_draw_number === n
|
||||
}
|
||||
|
||||
const displayResultNumber = computed(() => {
|
||||
const fromSnap = numberValue(snapshot.result_number)
|
||||
if (fromSnap !== null) {
|
||||
return fromSnap
|
||||
}
|
||||
const fromRec = numberValue(snapshot.record?.result_number)
|
||||
if (fromRec !== null) {
|
||||
return fromRec
|
||||
}
|
||||
return calcResultNumber.value
|
||||
})
|
||||
|
||||
function updateCalcLossFromResultNumber(): void {
|
||||
const rn = displayResultNumber.value
|
||||
if (rn === null) {
|
||||
return
|
||||
}
|
||||
const row = snapshot.candidate_numbers.find((c) => numberValue(c?.number) === rn)
|
||||
if (row && row.estimated_loss !== undefined && row.estimated_loss !== null) {
|
||||
calcEstimatedLoss.value = String(row.estimated_loss)
|
||||
}
|
||||
}
|
||||
|
||||
function formatWinAmount(row: anyObj): string {
|
||||
const st = Number(row?.bet_status ?? 0)
|
||||
const win = row?.win_amount
|
||||
if (st === 2 || st === 5) {
|
||||
return win !== undefined && win !== null && String(win) !== '' ? String(win) : '0.00'
|
||||
}
|
||||
return '—'
|
||||
}
|
||||
|
||||
function candidateRowClassName(arg: { row: anyObj }): string {
|
||||
return isScheduledNumber(arg.row?.number) ? 'is-scheduled-row' : ''
|
||||
const classes: string[] = []
|
||||
if (isScheduledNumber(arg.row?.number)) {
|
||||
classes.push('is-scheduled-row')
|
||||
}
|
||||
const result = displayResultNumber.value
|
||||
const num = numberValue(arg.row?.number)
|
||||
if (result !== null && num !== null && num === result) {
|
||||
classes.push('is-result-row')
|
||||
}
|
||||
return classes.join(' ')
|
||||
}
|
||||
|
||||
async function onPickSwitchChange(val: boolean, rowNumber: unknown): Promise<void> {
|
||||
@@ -621,11 +692,29 @@ function toBool(v: unknown): boolean | null {
|
||||
}
|
||||
|
||||
function mergeLiveSnapshot(data: anyObj): void {
|
||||
const prevPeriodId = snapshot.record?.id != null ? Number(snapshot.record.id) : null
|
||||
let periodChanged = false
|
||||
|
||||
if (data.record !== undefined) {
|
||||
const nextId = data.record?.id != null ? Number(data.record.id) : null
|
||||
periodChanged = prevPeriodId !== null && nextId !== null && prevPeriodId !== nextId
|
||||
snapshot.record = data.record
|
||||
}
|
||||
snapshot.bets = data.bets || []
|
||||
snapshot.candidate_numbers = data.candidate_numbers || []
|
||||
|
||||
const incomingBets = Array.isArray(data.bets) ? data.bets : null
|
||||
if (incomingBets !== null) {
|
||||
if (incomingBets.length > 0 || periodChanged || prevPeriodId === null) {
|
||||
snapshot.bets = incomingBets
|
||||
}
|
||||
}
|
||||
|
||||
const incomingCandidates = Array.isArray(data.candidate_numbers) ? data.candidate_numbers : null
|
||||
if (incomingCandidates !== null) {
|
||||
if (incomingCandidates.length > 0 || periodChanged || prevPeriodId === null) {
|
||||
snapshot.candidate_numbers = incomingCandidates
|
||||
}
|
||||
}
|
||||
|
||||
snapshot.ai_default_number = data.ai_default_number ?? null
|
||||
snapshot.pending_draw_number = typeof data.pending_draw_number === 'number' ? data.pending_draw_number : null
|
||||
snapshot.period_seconds = data.period_seconds ?? 30
|
||||
@@ -654,6 +743,17 @@ function mergeLiveSnapshot(data: anyObj): void {
|
||||
snapshot.maintenance_ui = data.maintenance_ui
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data.result_number === 'number') {
|
||||
snapshot.result_number = data.result_number
|
||||
calcResultNumber.value = data.result_number
|
||||
} else if (periodChanged) {
|
||||
snapshot.result_number = null
|
||||
calcResultNumber.value = null
|
||||
calcEstimatedLoss.value = '0.00'
|
||||
}
|
||||
|
||||
updateCalcLossFromResultNumber()
|
||||
syncServerClock(data.server_time)
|
||||
}
|
||||
|
||||
@@ -931,6 +1031,10 @@ onUnmounted(() => {
|
||||
window.clearTimeout(payoutStuckRefreshTimer)
|
||||
payoutStuckRefreshTimer = null
|
||||
}
|
||||
if (betStreamRefreshTimer !== null) {
|
||||
window.clearTimeout(betStreamRefreshTimer)
|
||||
betStreamRefreshTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -1166,6 +1270,16 @@ onUnmounted(() => {
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.candidate-table :deep(.is-result-row td) {
|
||||
background: var(--el-color-success-light-9);
|
||||
}
|
||||
|
||||
.candidate-table :deep(.is-result-row .number-tag) {
|
||||
border-color: var(--el-color-success);
|
||||
color: var(--el-color-success);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.live-tables-row .el-col {
|
||||
margin-bottom: 12px;
|
||||
|
||||
Reference in New Issue
Block a user