import env from '../config/env.js' const HEARTBEAT_CMD = { cmd: 1 } function safeJsonParse(data) { if (typeof data !== 'string') return data try { return JSON.parse(data) } catch (error) { return data } } function getHostFromBaseUrl(baseUrl) { return String(baseUrl || '') .replace(/^https?:\/\//, '') .replace(/\/+$/, '') } class WebSocketService { constructor() { this.socketTask = null this.isConnected = false this.isManualClose = false this.connectingPromise = null this.connectOptions = null this.reconnectAttempts = 0 this.maxReconnectAttempts = 5 this.reconnectInterval = 3000 this.reconnectTimer = null this.heartbeatInterval = 20000 this.heartbeatTimer = null this.listeners = {} this.useGlobalSocketApi = false this.globalSocketHandlers = null } on(eventName, handler) { if (!eventName || typeof handler !== 'function') return () => {} if (!this.listeners[eventName]) this.listeners[eventName] = new Set() this.listeners[eventName].add(handler) return () => this.off(eventName, handler) } off(eventName, handler) { if (!eventName || !this.listeners[eventName]) return if (!handler) { this.listeners[eventName].clear() return } this.listeners[eventName].delete(handler) } emit(eventName, payload) { const group = this.listeners[eventName] if (!group || !group.size) return group.forEach((fn) => { try { fn(payload) } catch (error) { console.error(`[WebSocket] listener error for ${eventName}:`, error) } }) } buildSocketUrl(options = {}) { const role = options.role || 'parent' const base = 'wss://yx.newfeifan.cn/btcSkt' // 开发地址(保留注释):const base = 'ws://192.168.220.13:8080/btcSkt' // 旧线上地址(保留注释):const base = 'wss://m.hfdcschool.com/btcSkt' // 原 env 写法(保留注释):const base = `wss://${getHostFromBaseUrl(env.baseUrl)}/btcSkt` if (role === 'device') { const ssDev = options.ssDev || '' if (!ssDev) throw new Error('缺少 ssDev') return `${base}?ssDev=${encodeURIComponent(ssDev)}` } const ssToken = options.ssToken || '' if (!ssToken) throw new Error('缺少 ssToken') return `${base}?ssToken=${encodeURIComponent(ssToken)}` } async connect(options = {}) { if (this.isConnected && this.socketTask && !options.forceReconnect) { return true } if (this.connectingPromise) { return this.connectingPromise } if (options.forceReconnect) { await this.disconnect() } const merged = { role: options.role || this.connectOptions?.role || 'parent', ssToken: options.ssToken || this.connectOptions?.ssToken || '', ssDev: options.ssDev || this.connectOptions?.ssDev || '', heartbeat: options.heartbeat ?? this.connectOptions?.heartbeat ?? false, heartbeatInterval: options.heartbeatInterval || this.connectOptions?.heartbeatInterval || this.heartbeatInterval, autoReconnect: options.autoReconnect ?? this.connectOptions?.autoReconnect ?? true, maxReconnectAttempts: options.maxReconnectAttempts || this.connectOptions?.maxReconnectAttempts || this.maxReconnectAttempts, reconnectInterval: options.reconnectInterval || this.connectOptions?.reconnectInterval || this.reconnectInterval } this.connectOptions = merged this.heartbeatInterval = merged.heartbeatInterval this.maxReconnectAttempts = merged.maxReconnectAttempts this.reconnectInterval = merged.reconnectInterval this.isManualClose = false const wsUrl = this.buildSocketUrl(merged) const socketApi = typeof wx !== 'undefined' ? wx : uni this.connectingPromise = new Promise((resolve, reject) => { let settled = false let openTimeout = null const resolveOnce = (value) => { if (settled) return settled = true resolve(value) } const rejectOnce = (error) => { if (settled) return settled = true reject(error) } const handleOpen = () => { if (openTimeout) clearTimeout(openTimeout) this.isConnected = true this.reconnectAttempts = 0 this.connectingPromise = null if (merged.heartbeat) { this.startHeartbeat() } else { this.stopHeartbeat() } this.emit('open', { url: wsUrl, options: merged }) resolveOnce(true) } const handleMessage = (res) => { const data = safeJsonParse(res?.data) this.emit('message', data) if (data && typeof data === 'object' && data.cmd !== undefined) { this.emit(`cmd:${data.cmd}`, data) if (data.cmd === 51) this.emit('refresh', data) } } const handleError = (error) => { this.emit('error', error) if (!this.isConnected && this.connectingPromise) { this.connectingPromise = null rejectOnce(error) } } const handleClose = (res) => { this.isConnected = false this.stopHeartbeat() this.emit('close', res) if (!this.isManualClose && this.connectOptions?.autoReconnect) { this.tryReconnect() } } let task = null try { task = socketApi.connectSocket({ url: wsUrl }) } catch (error) { this.connectingPromise = null rejectOnce(error) return } if (task && typeof task.onOpen === 'function') { this.useGlobalSocketApi = false this.socketTask = task task.onOpen(handleOpen) task.onMessage(handleMessage) task.onError(handleError) task.onClose(handleClose) return } if ( typeof socketApi.onSocketOpen !== 'function' || typeof socketApi.onSocketMessage !== 'function' || typeof socketApi.onSocketError !== 'function' || typeof socketApi.onSocketClose !== 'function' ) { this.connectingPromise = null rejectOnce(new Error('创建 WebSocket 失败')) return } this.useGlobalSocketApi = true this.socketTask = { __global: true } const openHandler = () => handleOpen() const messageHandler = (res) => handleMessage(res) const errorHandler = (err) => handleError(err) const closeHandler = (res) => handleClose(res) this.globalSocketHandlers = { openHandler, messageHandler, errorHandler, closeHandler } socketApi.onSocketOpen(openHandler) socketApi.onSocketMessage(messageHandler) socketApi.onSocketError(errorHandler) socketApi.onSocketClose(closeHandler) openTimeout = setTimeout(() => { this.connectingPromise = null rejectOnce(new Error('WebSocket 连接超时(未收到 onSocketOpen)')) }, 15000) }) return this.connectingPromise } tryReconnect() { if (this.reconnectTimer) return if (this.reconnectAttempts >= this.maxReconnectAttempts) return this.reconnectAttempts += 1 this.reconnectTimer = setTimeout(async () => { this.reconnectTimer = null try { await this.connect(this.connectOptions || {}) } catch (error) { this.tryReconnect() } }, this.reconnectInterval) } startHeartbeat() { this.stopHeartbeat() this.heartbeatTimer = setInterval(() => { if (!this.isConnected) return this.send(HEARTBEAT_CMD).catch(() => {}) }, this.heartbeatInterval) } stopHeartbeat() { if (!this.heartbeatTimer) return clearInterval(this.heartbeatTimer) this.heartbeatTimer = null } async disconnect() { this.isManualClose = true this.stopHeartbeat() if (this.reconnectTimer) { clearTimeout(this.reconnectTimer) this.reconnectTimer = null } const socketApi = typeof wx !== 'undefined' ? wx : uni return new Promise((resolve) => { if (!this.socketTask) { this.socketTask = null this.useGlobalSocketApi = false this.isConnected = false this.connectingPromise = null resolve() return } if (this.useGlobalSocketApi || this.socketTask.__global) { if (this.globalSocketHandlers) { const { openHandler, messageHandler, errorHandler, closeHandler } = this.globalSocketHandlers if (typeof socketApi.offSocketOpen === 'function') socketApi.offSocketOpen(openHandler) if (typeof socketApi.offSocketMessage === 'function') socketApi.offSocketMessage(messageHandler) if (typeof socketApi.offSocketError === 'function') socketApi.offSocketError(errorHandler) if (typeof socketApi.offSocketClose === 'function') socketApi.offSocketClose(closeHandler) this.globalSocketHandlers = null } socketApi.closeSocket({ complete: () => { this.socketTask = null this.useGlobalSocketApi = false this.isConnected = false this.connectingPromise = null resolve() } }) return } if (typeof this.socketTask.close !== 'function') { this.socketTask = null this.useGlobalSocketApi = false this.isConnected = false this.connectingPromise = null resolve() return } this.socketTask.close({ complete: () => { this.socketTask = null this.useGlobalSocketApi = false this.isConnected = false this.connectingPromise = null resolve() } }) }) } async send(payload) { if (!this.socketTask || !this.isConnected) { throw new Error('WebSocket 未连接') } const socketApi = typeof wx !== 'undefined' ? wx : uni return new Promise((resolve, reject) => { if (this.useGlobalSocketApi || this.socketTask.__global) { socketApi.sendSocketMessage({ data: JSON.stringify(payload), success: () => resolve(true), fail: (error) => reject(error) }) return } if (typeof this.socketTask.send !== 'function') { reject(new Error('WebSocket 发送不可用')) return } this.socketTask.send({ data: JSON.stringify(payload), success: () => resolve(true), fail: (error) => reject(error) }) }) } async ensureConnected(options = {}) { if (this.isConnected && this.socketTask) return true await this.connect(options) return true } } const websocketService = new WebSocketService() export default websocketService