| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- 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
|