diff --git a/src/App.vue b/src/App.vue index 751b9d6..3a630ac 100644 --- a/src/App.vue +++ b/src/App.vue @@ -21,7 +21,10 @@ } = storeToRefs(store); const { updateUser, - updateOnline + updateOnline, + updateDevice, + updateGame, + updateRoomNumber } = store; watch( @@ -46,6 +49,22 @@ updateUser(value); } + function onSessionKickedOut() { + uni.removeStorageSync( + `${uni.getAccountInfoSync().miniProgram.envVersion}_token` + ); + updateUser(); + updateDevice("", ""); + updateOnline(false); + updateGame(false, ""); + updateRoomNumber(""); + uni.showModal({ + title: "提示", + content: "账号已在其他设备登录", + showCancel: false, + }); + } + async function emitUpdateOnline() { const data = await getDeviceBatteryAPI(); updateOnline(data.online); @@ -65,6 +84,7 @@ onShow(() => { uni.$on("update-user", emitUpdateUser); uni.$on("update-online", emitUpdateOnline); + uni.$on("session-kicked-out", onSessionKickedOut); const token = uni.getStorageSync( `${uni.getAccountInfoSync().miniProgram.envVersion}_token` ); @@ -77,6 +97,7 @@ onHide(() => { uni.$off("update-user", emitUpdateUser); uni.$off("update-online", emitUpdateOnline); + uni.$off("session-kicked-out", onSessionKickedOut); websocket.closeWebSocket(); }); @@ -289,4 +310,4 @@ font-style: normal; font-display: swap; } - \ No newline at end of file + diff --git a/src/apis.js b/src/apis.js index 0259878..242172d 100644 --- a/src/apis.js +++ b/src/apis.js @@ -46,6 +46,8 @@ function request(method, url, data = {}) { `${uni.getAccountInfoSync().miniProgram.envVersion}_token` ); uni.$emit("update-user"); + reject({ type: "AUTH_INVALID", message }); + return; } if (message === "ROOM_FULL") { resolve({full: true}); diff --git a/src/websocket.js b/src/websocket.js index ba202b3..639da84 100644 --- a/src/websocket.js +++ b/src/websocket.js @@ -1,61 +1,91 @@ import { MESSAGETYPES, getMessageTypeName } from "@/constants"; +import { getUserGameState } from "@/apis"; + let socket = null; let heartbeatInterval = null; let reconnectTimer = null; +let manualClose = false; +let checkingSession = false; +let kickedOut = false; +let isConnecting = false; -/** - * 建立 WebSocket 连接 - */ function createWebSocket(token, onMessage) { + if (!token) return; + if (kickedOut) kickedOut = false; + if (socket || isConnecting) return; + + manualClose = false; + isConnecting = true; + let url = "wss://api.shelingxingqiu.com/socket"; try { const accountInfo = uni.getAccountInfoSync(); const envVersion = accountInfo.miniProgram.envVersion; switch (envVersion) { - case "develop": // 开发版 + case "develop": // url = "ws://localhost:8000/socket"; url = "wss://apitest.shelingxingqiu.com/socket"; break; - case "trial": // 体验版 + case "trial": url = "wss://apitest.shelingxingqiu.com/socket"; break; - case "release": // 正式版 + case "release": url = "wss://api.shelingxingqiu.com/socket"; break; default: - // 保持默认值 break; } } catch (e) { - console.error("获取环境信息失败,使用默认正式环境", e); + console.error("获取 WebSocket 环境信息失败,使用默认正式环境", e); } + url += `?authorization=${token}`; - socket = uni.connectSocket({ + const socketTask = uni.connectSocket({ url, success: () => { - console.log("websocket 连接成功"); - // 启动心跳 - startHeartbeat(onMessage); + console.log("WebSocket 已发起连接"); }, - fail: () => { + fail: (err) => { + if (socket !== socketTask) return; + console.error("WebSocket 连接失败", err); + socket = null; + isConnecting = false; reconnect(onMessage); }, }); - // 接收消息 - uni.onSocketMessage((res) => { + socket = socketTask; + + socketTask.onOpen(() => { + if (socket !== socketTask) return; + console.log("WebSocket 连接成功"); + isConnecting = false; + startHeartbeat(onMessage); + }); + + socketTask.onMessage((res) => { + if (socket !== socketTask) return; + const { data, event } = JSON.parse(res.data); if (event === "pong") return; if (data.type) { - console.log("收到消息:", getMessageTypeName(data.type), data.data); + console.log( + "收到 WebSocket 消息", + getMessageTypeName(data.type), + data.data + ); if (onMessage) onMessage({ ...(data.data || {}), type: data.type }); return; } if (onMessage && data.updates) onMessage(data.updates); const msg = data.updates[0]; if (msg) { - console.log("收到消息:", getMessageTypeName(msg.constructor), msg); + console.log( + "收到 WebSocket 更新", + getMessageTypeName(msg.constructor), + msg + ); if (msg.constructor === MESSAGETYPES.RankUpdate) { uni.setStorageSync("latestRank", msg.lvl); } else if (msg.constructor === MESSAGETYPES.LvlUpdate) { @@ -68,84 +98,109 @@ function createWebSocket(token, onMessage) { } }); - // 错误处理 - uni.onSocketError((err) => { + socketTask.onError((err) => { + if (socket !== socketTask) return; console.error("WebSocket 错误", err); - reconnect(onMessage); }); - uni.onSocketClose((result) => { + socketTask.onClose(async (result) => { + if (socket !== socketTask) return; console.log("WebSocket 已关闭", result); stopHeartbeat(); - reconnect(onMessage); + socket = null; + isConnecting = false; + + if (manualClose || kickedOut) return; + await handleUnexpectedClose(onMessage); }); } -/** - * 重连机制 - */ -function reconnect(onMessage) { - reconnectTimer && clearTimeout(reconnectTimer); - closeWebSocket(); // 确保关闭旧连接 +async function handleUnexpectedClose(onMessage) { + if (checkingSession || manualClose || kickedOut) return; const token = uni.getStorageSync( `${uni.getAccountInfoSync().miniProgram.envVersion}_token` ); if (!token) return; + checkingSession = true; + try { + await getUserGameState(); + if (!manualClose && !kickedOut) reconnect(onMessage); + } catch (err) { + if (err?.type === "AUTH_INVALID") { + kickedOut = true; + manualClose = true; + reconnectTimer && clearTimeout(reconnectTimer); + uni.$emit("session-kicked-out"); + return; + } + if (!manualClose && !kickedOut) reconnect(onMessage); + } finally { + checkingSession = false; + } +} + +function reconnect(onMessage) { + reconnectTimer && clearTimeout(reconnectTimer); + + const token = uni.getStorageSync( + `${uni.getAccountInfoSync().miniProgram.envVersion}_token` + ); + if (!token || manualClose || kickedOut || socket || isConnecting) return; + reconnectTimer = setTimeout(() => { - console.log("reconnecting..."); + if (manualClose || kickedOut || socket || isConnecting) return; + console.log("WebSocket 正在重连..."); createWebSocket(token, onMessage); }, 1000); } -function closeWebSocket() { +function closeWebSocket(isManual = true) { + manualClose = isManual; + reconnectTimer && clearTimeout(reconnectTimer); + stopHeartbeat(); + isConnecting = false; + if (socket) { - reconnectTimer && clearTimeout(reconnectTimer); - stopHeartbeat(); + const currentSocket = socket; + socket = null; try { - socket.close(); + currentSocket.close(); } catch (err) { - console.error("关闭WebSocket连接失败", err); + console.error("关闭 WebSocket 失败", err); } - - socket = null; // 清除socket引用 } } function sendHeartbeat(onMessage) { - uni.sendSocketMessage({ + if (!socket) return; + + const currentSocket = socket; + currentSocket.send({ data: JSON.stringify({ event: "ping", data: {} }), - success: () => { - // console.log("发送心跳成功"); - }, + success: () => {}, fail: (err) => { - console.error("发送心跳失败", err); + if (socket !== currentSocket) return; + console.error("心跳发送失败", err); stopHeartbeat(); - closeWebSocket(); // 关闭失效的连接 - reconnect(onMessage); // 触发重连 + closeWebSocket(false); + reconnect(onMessage); }, }); } -/** - * 启动心跳 - */ function startHeartbeat(onMessage) { - stopHeartbeat(); // 防止重复启动 + stopHeartbeat(); heartbeatInterval = setInterval(() => { - if (socket && socket.readyState === 1) { - // 检查连接状态 + if (socket) { sendHeartbeat(onMessage); } }, 10000); } -/** - * 停止心跳 - */ function stopHeartbeat() { if (heartbeatInterval) { clearInterval(heartbeatInterval);