feat: 联调充值和提现接口

This commit is contained in:
JiaJun
2026-05-21 13:40:32 +08:00
parent 6ac42cf35e
commit 44c984d59e
51 changed files with 3830 additions and 1478 deletions

View File

@@ -8,7 +8,13 @@ import {
import { getAuthDeviceId, useAuthStore } from '@/store/auth'
import { useGameRoundStore, useGameSessionStore } from '@/store/game'
import { getGameLobbyInit, normalizePeriodTickRound } from '../api/game-api'
import type { GameLobbyUserSnapshotDto, GamePeriodTickDto } from '../api/types'
import type { GamePeriodTickDto } from '../api/types'
type UserStreakMessageData = {
currentStreak: number
oddsFactor?: number
streakLevel?: number
}
const FALLBACK_POLL_INTERVAL_MS = 10_000
const GAME_SOCKET_TOPICS = {
@@ -36,6 +42,10 @@ const GAME_SOCKET_TOPICS = {
adminLiveOpened: 'admin.live.opened',
} as const
const GAME_SOCKET_TOPIC_VALUES = new Set<string>(
Object.values(GAME_SOCKET_TOPICS),
)
// 当前 H5 游戏页实际需要的用户侧事件。
// 后台专用事件保持在 GAME_SOCKET_TOPICS 中做口径对齐,但不在这里订阅。
const PLAYER_SOCKET_TOPICS = [
@@ -93,6 +103,22 @@ function getNestedRecord(
: null
}
function getMessageTopic(message: GameSocketMessage) {
const root = message as Record<string, unknown>
const event = typeof root.event === 'string' ? root.event : null
const topic = typeof root.topic === 'string' ? root.topic : null
if (event && GAME_SOCKET_TOPIC_VALUES.has(event)) {
return event
}
if (topic && GAME_SOCKET_TOPIC_VALUES.has(topic)) {
return topic
}
return event ?? topic
}
function extractServerTime(message: GameSocketMessage) {
const root = message as Record<string, unknown>
@@ -105,31 +131,22 @@ function extractServerTime(message: GameSocketMessage) {
return typeof data?.server_time === 'number' ? data.server_time : null
}
function extractUserSnapshot(
function extractUserStreakMessageData(
message: GameSocketMessage,
): GameLobbyUserSnapshotDto | null {
): UserStreakMessageData | null {
const direct = getNestedRecord(message, 'user_snapshot')
const nested = getNestedRecord(
getNestedRecord(message, 'data'),
'user_snapshot',
)
const source = direct ?? nested
const data = getNestedRecord(message, 'data')
const nested = getNestedRecord(data, 'user_snapshot')
const source = direct ?? nested ?? data
if (
!source ||
typeof source.coin !== 'string' ||
typeof source.current_streak !== 'number'
) {
if (!source || typeof source.current_streak !== 'number') {
return null
}
return {
coin: source.coin,
current_streak: source.current_streak,
is_jackpot:
typeof source.is_jackpot === 'boolean' ? source.is_jackpot : undefined,
odds_factor: toOptionalNumber(source.odds_factor),
streak_level: toOptionalNumber(source.streak_level),
currentStreak: source.current_streak,
oddsFactor: toOptionalNumber(source.odds_factor),
streakLevel: toOptionalNumber(source.streak_level),
}
}
@@ -169,6 +186,25 @@ function extractPeriodTick(
}
}
function extractWalletCoin(message: GameSocketMessage) {
const data = getNestedRecord(message, 'data')
const source = data ?? (message as Record<string, unknown>)
const coin = source.coin ?? source.balance ?? source.balance_after
if (typeof coin === 'string') {
return coin
}
return typeof coin === 'number' && Number.isFinite(coin) ? String(coin) : null
}
function extractJackpotStatus(message: GameSocketMessage) {
const data = getNestedRecord(message, 'data')
const source = data ?? (message as Record<string, unknown>)
return typeof source.is_jackpot === 'boolean' ? source.is_jackpot : true
}
function applyLobbySync(result: Awaited<ReturnType<typeof getGameLobbyInit>>) {
const currentRoundState = useGameRoundStore.getState()
const currentSessionState = useGameSessionStore.getState()
@@ -211,52 +247,122 @@ function applyLobbySync(result: Awaited<ReturnType<typeof getGameLobbyInit>>) {
}
}
function applyRealtimeMessage(message: GameSocketMessage) {
const serverTime = extractServerTime(message)
function applyPeriodMessage(
message: GameSocketMessage,
serverTime: number | null,
) {
const period = extractPeriodTick(message)
const userSnapshot = extractUserSnapshot(message)
if (period) {
const previousRound = useGameRoundStore.getState().round
const round = normalizePeriodTickRound(
{
...period,
server_time: serverTime ?? period.server_time,
},
previousRound,
)
useGameRoundStore.getState().syncRound({
bettingClosesAt: round.bettingClosesAt,
id: round.id,
phase: round.phase,
revealingAt: round.revealingAt,
settledAt: round.settledAt,
startedAt: round.startedAt,
winningCellId: round.winningCellId,
})
useGameSessionStore.getState().syncDashboard({
countdownMs: period.countdown * 1000,
updatedAt:
serverTime !== null
? toIsoFromUnixSeconds(serverTime)
: toIsoFromUnixSeconds(period.server_time),
})
if (!period) {
return
}
if (userSnapshot) {
const currentUser = useAuthStore.getState().currentUser
const previousRound = useGameRoundStore.getState().round
const round = normalizePeriodTickRound(
{
...period,
server_time: serverTime ?? period.server_time,
},
previousRound,
)
if (currentUser) {
useAuthStore.getState().setCurrentUser({
...currentUser,
coin: userSnapshot.coin,
currentStreak: userSnapshot.current_streak,
isJackpot: userSnapshot.is_jackpot,
oddsFactor: userSnapshot.odds_factor,
streakLevel: userSnapshot.streak_level,
})
}
useGameRoundStore.getState().syncRound({
bettingClosesAt: round.bettingClosesAt,
id: round.id,
phase: round.phase,
revealingAt: round.revealingAt,
settledAt: round.settledAt,
startedAt: round.startedAt,
winningCellId: round.winningCellId,
})
useGameSessionStore.getState().syncDashboard({
countdownMs: period.countdown * 1000,
updatedAt:
serverTime !== null
? toIsoFromUnixSeconds(serverTime)
: toIsoFromUnixSeconds(period.server_time),
})
}
function applyPeriodPhase(phase: 'locked' | 'revealing' | 'settled') {
useGameRoundStore.getState().setPhase(phase)
}
function applyUserStreakMessage(message: GameSocketMessage) {
const streakData = extractUserStreakMessageData(message)
const currentUser = useAuthStore.getState().currentUser
if (!streakData || !currentUser) {
return
}
useAuthStore.getState().setCurrentUser({
...currentUser,
currentStreak: streakData.currentStreak,
oddsFactor: streakData.oddsFactor,
streakLevel: streakData.streakLevel,
})
}
function applyWalletChangedMessage(message: GameSocketMessage) {
const coin = extractWalletCoin(message)
const currentUser = useAuthStore.getState().currentUser
if (coin === null || !currentUser) {
return
}
useAuthStore.getState().setCurrentUser({
...currentUser,
coin,
})
}
function applyJackpotHitMessage(message: GameSocketMessage) {
const currentUser = useAuthStore.getState().currentUser
if (!currentUser) {
return
}
useAuthStore.getState().setCurrentUser({
...currentUser,
isJackpot: extractJackpotStatus(message),
})
}
function applyRealtimeMessage(message: GameSocketMessage) {
const serverTime = extractServerTime(message)
const topic = getMessageTopic(message)
switch (topic) {
case GAME_SOCKET_TOPICS.periodTick:
applyPeriodMessage(message, serverTime)
break
case GAME_SOCKET_TOPICS.periodLocked:
applyPeriodMessage(message, serverTime)
applyPeriodPhase('locked')
break
case GAME_SOCKET_TOPICS.periodOpened:
applyPeriodMessage(message, serverTime)
applyPeriodPhase('revealing')
break
case GAME_SOCKET_TOPICS.periodPayout:
applyPeriodMessage(message, serverTime)
applyPeriodPhase('settled')
break
case GAME_SOCKET_TOPICS.userStreak:
applyUserStreakMessage(message)
break
case GAME_SOCKET_TOPICS.walletChanged:
applyWalletChangedMessage(message)
break
case GAME_SOCKET_TOPICS.jackpotHit:
applyJackpotHitMessage(message)
break
case GAME_SOCKET_TOPICS.betAccepted:
case GAME_SOCKET_TOPICS.autoSpinProgress:
break
}
useGameSessionStore.getState().syncConnection({