import { ref } from 'vue' const EMPTY_BOARD = { week_start: '', week_end: '', sex_label: '同行', group_size: 7, member_count: 0, players: [], me: { count: 0, rank: 1, best_score: 0, distance: 0, is_first: true }, invite_code: '' } const PENDING_SYNC_KEY = 'tongji_endless_pending_sync_v1' function createSessionKey() { const random = Math.random().toString(36).slice(2, 12) return `game_${Date.now().toString(36)}_${random}` } function readPendingPayloads() { try { const stored = uni.getStorageSync(PENDING_SYNC_KEY) if (!Array.isArray(stored)) return [] return stored.filter(item => ( item && /^[A-Za-z0-9_-]{16,64}$/.test(String(item.session_key || '')) && Number(item.learned_count) >= 0 )).slice(-8) } catch (_) { return [] } } /** * 小游戏的平台连接层。复用主小程序 token,不在游戏里另建账号。 * 每次上报的是本局绝对值,后端按 session_key 去重,断网重试也不会重复加分。 */ export function useGamePlatform(proxy, ensureLoggedIn) { const connected = ref(false) const loading = ref(false) const syncStatus = ref('idle') const lastError = ref('') const leaderboard = ref({ ...EMPTY_BOARD }) const confirmedSessionLearned = ref(0) let sessionKey = createSessionKey() let syncTimer = null let syncing = false let queuedPayloads = readPendingPayloads() let connectPromise = null function persistPendingPayloads() { try { uni.setStorageSync(PENDING_SYNC_KEY, queuedPayloads.slice(-8)) } catch (_) {} } async function api(request) { if (!proxy?.apiUrl) throw new Error('平台接口未初始化') return proxy.apiUrl(request, false) } function applyLeaderboard(data, confirmedForSession = '') { if (!data || !Array.isArray(data.players)) return const inactiveSessionResponse = confirmedForSession && confirmedForSession !== sessionKey if (!inactiveSessionResponse) { leaderboard.value = { ...EMPTY_BOARD, ...data, me: { ...EMPTY_BOARD.me, ...(data.me || {}) }, players: data.players } } if (confirmedForSession === sessionKey && data.confirmed_session_learned != null) { confirmedSessionLearned.value = Number(data.confirmed_session_learned) || 0 } connected.value = true syncStatus.value = 'synced' lastError.value = '' } async function refreshLeaderboard() { const res = await api({ url: '/api/tcm/gameWeeklyLeaderboard', method: 'GET', data: { session_key: sessionKey } }) if (res?.code !== 1 || !res.data) { throw new Error(res?.msg || '同行榜加载失败') } applyLeaderboard(res.data, sessionKey) return res.data } async function acceptShare(inviteCode) { const code = String(inviteCode || '').trim().toUpperCase() if (!code) return try { await api({ url: '/api/tcm/gameAcceptShare', method: 'POST', data: { invite_code: code } }) } catch (_) { // 分享关系是辅助能力,不阻断进入游戏。 } } async function connect(inviteCode = '') { if (connectPromise) return connectPromise connectPromise = (async () => { loading.value = true try { const loggedIn = await ensureLoggedIn() if (!loggedIn) throw new Error('登录失败') await acceptShare(inviteCode) await refreshLeaderboard() if (queuedPayloads.length) flushProgress() return true } catch (error) { connected.value = false syncStatus.value = 'offline' lastError.value = String(error?.message || '平台连接失败,请点击重试') return false } finally { loading.value = false connectPromise = null } })() return connectPromise } function beginSession(existingSessionKey = '') { if (syncTimer) clearTimeout(syncTimer) const restoredKey = String(existingSessionKey || '').trim() sessionKey = /^[A-Za-z0-9_-]{16,64}$/.test(restoredKey) ? restoredKey : createSessionKey() confirmedSessionLearned.value = 0 syncStatus.value = queuedPayloads.length ? 'pending' : (connected.value ? 'synced' : 'offline') return sessionKey } function getSessionKey() { return sessionKey } function queueProgress(learnedCount, score, ended = false) { const nextPayload = { session_key: sessionKey, learned_count: Math.max(0, Number(learnedCount) || 0), score: Math.max(0, Number(score) || 0), ended: ended ? 1 : 0 } const existingIndex = queuedPayloads.findIndex(item => item.session_key === sessionKey) if (existingIndex >= 0) { const existing = queuedPayloads[existingIndex] queuedPayloads[existingIndex] = { ...nextPayload, learned_count: Math.max(existing.learned_count, nextPayload.learned_count), score: Math.max(existing.score, nextPayload.score), ended: Math.max(existing.ended, nextPayload.ended) } } else { queuedPayloads.push(nextPayload) } persistPendingPayloads() syncStatus.value = 'pending' if (syncTimer) clearTimeout(syncTimer) if (ended) { flushProgress() } else { syncTimer = setTimeout(flushProgress, 1000) } } async function flushProgress() { if (syncTimer) clearTimeout(syncTimer) syncTimer = null if (syncing || !queuedPayloads.length) return const payload = queuedPayloads[0] syncing = true syncStatus.value = 'syncing' try { if (!connected.value) { const loggedIn = await ensureLoggedIn() if (!loggedIn) throw new Error('未登录') } const res = await api({ url: '/api/tcm/gameSubmitProgress', method: 'POST', data: payload }) if (res?.code !== 1 || !res.data) { throw new Error(res?.msg || '成绩保存失败') } applyLeaderboard(res.data, payload.session_key) // 请求发出后玩家可能又完成了消除。只移除已经被本次请求覆盖的进度, // 不能按 session_key 整局删除,否则会丢失请求进行期间产生的新进度。 queuedPayloads = queuedPayloads.filter(item => ( item.session_key !== payload.session_key || Number(item.learned_count) > Number(payload.learned_count) || Number(item.score) > Number(payload.score) || Number(item.ended) > Number(payload.ended) )) persistPendingPayloads() } catch (error) { connected.value = false syncStatus.value = 'offline' lastError.value = String(error?.message || '成绩暂未保存,联网后会自动重试') // 队首保留原绝对值,下次连接或打开榜单时安全重试。 persistPendingPayloads() } finally { syncing = false if (queuedPayloads.length && connected.value) { setTimeout(flushProgress, 80) } } } async function syncAndRefresh(learnedCount, score) { queueProgress(learnedCount, score, false) await flushProgress() if (!connected.value) { await connect() if (queuedPayloads.length) await flushProgress() } else { try { await refreshLeaderboard() } catch (_) {} } } async function recordShare() { try { if (!connected.value && !(await connect())) return const res = await api({ url: '/api/tcm/gameRecordShare', method: 'POST' }) if (res?.code === 1 && res.data?.invite_code) { leaderboard.value = { ...leaderboard.value, invite_code: res.data.invite_code } } } catch (_) { // 分享本身仍可进行,统计失败不影响用户。 } } function dispose() { if (syncTimer) clearTimeout(syncTimer) syncTimer = null persistPendingPayloads() } return { connected, loading, syncStatus, lastError, leaderboard, confirmedSessionLearned, connect, beginSession, getSessionKey, queueProgress, flushProgress, syncAndRefresh, refreshLeaderboard, recordShare, dispose } }