| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- import { BASE_URL, TOKEN_KEY } from '@/config/api'
- import { request, parseResponseJson } from '@/utils/request'
- import { emit } from '@/utils/bus'
- const STORAGE_PRICE_VERSION_KEY = 'sq_active_price_version'
- const STORAGE_PRICE_LIST_ID_KEY = 'sq_active_price_list_id'
- const WS_LOG_PREFIX = '[price-ws]'
- function wsLog(...args) {
- try {
- console.log(WS_LOG_PREFIX, ...args)
- } catch (e) {
- // ignore
- }
- }
- function getStoredVersion() {
- const v = uni.getStorageSync(STORAGE_PRICE_VERSION_KEY)
- const n = Number(v)
- return Number.isFinite(n) ? n : 0
- }
- function setStoredVersion(version, priceListId) {
- uni.setStorageSync(STORAGE_PRICE_VERSION_KEY, Number(version) || 0)
- if (priceListId) uni.setStorageSync(STORAGE_PRICE_LIST_ID_KEY, String(priceListId))
- }
- function getStoredPriceListId() {
- return String(uni.getStorageSync(STORAGE_PRICE_LIST_ID_KEY) || '').trim()
- }
- function buildWsUrl(token) {
- const base = String(BASE_URL || '').trim()
- const origin = base.replace(/\/api\/?$/, '').replace(/\/$/, '')
- const scheme = origin.startsWith('https://') ? 'wss://' : 'ws://'
- const host = origin.replace(/^https?:\/\//, '')
- return `${scheme}${host}/ws/mini?token=${encodeURIComponent(token)}`
- }
- let socketTask = null
- let reconnectTimer = null
- let reconnectDelayMs = 1000
- let isConnecting = false
- let h5ListenersBound = false
- let shouldReconnect = true
- let socketOpened = false
- let pollTimer = null
- const FALLBACK_POLL_INTERVAL_MS = 5000
- const WS_ENABLED = true
- function safeCloseSocket(task, reason = 'client-close') {
- // 仅在连接已建立后主动关闭,避免小程序端在未连接状态 close 抛错
- if (!socketOpened) {
- wsLog('skip close: socket not opened yet', { reason })
- return false
- }
- // 微信小程序仅允许 1000 或 3000-4999 的关闭码
- const closeOptions = { code: 1000, reason }
- if (task && typeof task.close === 'function') {
- wsLog('closing socket', { reason, code: 1000 })
- // #ifdef MP-WEIXIN
- task.close({ code: 1000 })
- // #endif
- // #ifndef MP-WEIXIN
- task.close(closeOptions)
- // #endif
- return true
- }
- // 无有效 task 时不调用 uni.closeSocket,避免出现“WebSocket is not connected / invalid code”噪音
- return false
- }
- function bindSocketListeners() {
- const onOpen = () => {
- wsLog('socket open')
- socketOpened = true
- clearFallbackPoll()
- reconnectDelayMs = 1000
- void checkActiveVersionAndEmitIfChanged()
- }
- const onMessage = (evt) => {
- wsLog('socket message received')
- handleWsMessage(evt?.data)
- }
- const onClose = () => {
- wsLog('socket close', { shouldReconnect })
- socketOpened = false
- socketTask = null
- ensureFallbackPollRunning()
- if (shouldReconnect) scheduleReconnect()
- }
- const onError = () => {
- wsLog('socket error', { shouldReconnect })
- try {
- // 错误回调中不主动 close,交给 onClose,避免重复关闭触发小程序异常
- } catch (e) {
- // ignore
- }
- socketOpened = false
- socketTask = null
- ensureFallbackPollRunning()
- if (shouldReconnect) scheduleReconnect()
- }
- // App/小程序端:SocketTask 事件方法(每次重连都会产生新的 SocketTask,需要重新绑定监听)
- if (socketTask && typeof socketTask.onOpen === 'function') {
- socketTask.onOpen(onOpen)
- socketTask.onMessage(onMessage)
- socketTask.onClose(onClose)
- socketTask.onError(onError)
- return
- }
- // H5 端:uni 全局 socket 事件(全局监听只需注册一次)
- if (h5ListenersBound) return
- if (typeof uni.onSocketOpen === 'function') uni.onSocketOpen(onOpen)
- if (typeof uni.onSocketMessage === 'function') uni.onSocketMessage(onMessage)
- if (typeof uni.onSocketClose === 'function') uni.onSocketClose(onClose)
- if (typeof uni.onSocketError === 'function') uni.onSocketError(onError)
- h5ListenersBound = true
- }
- function clearReconnect() {
- if (reconnectTimer) clearTimeout(reconnectTimer)
- reconnectTimer = null
- }
- function clearFallbackPoll() {
- if (pollTimer) clearInterval(pollTimer)
- pollTimer = null
- }
- function ensureFallbackPollRunning() {
- if (pollTimer) return
- wsLog('start fallback polling', { intervalMs: FALLBACK_POLL_INTERVAL_MS })
- pollTimer = setInterval(() => {
- void checkActiveVersionAndEmitIfChanged()
- }, FALLBACK_POLL_INTERVAL_MS)
- }
- function scheduleReconnect() {
- if (reconnectTimer) return
- const delay = reconnectDelayMs
- wsLog('schedule reconnect', { delay })
- reconnectDelayMs = Math.min(reconnectDelayMs * 2, 30000)
- reconnectTimer = setTimeout(() => {
- reconnectTimer = null
- void ensurePriceRealtimeConnected()
- }, delay)
- }
- async function checkActiveVersionAndEmitIfChanged() {
- try {
- const res = await request({
- url: '/mini/client/price-list/active-version',
- method: 'GET'
- })
- const body = parseResponseJson(res)
- const data = body && body.code === 200 ? body.data : null
- const newVersion = Number(data?.priceVersion || 0) || 0
- const oldVersion = getStoredVersion()
- wsLog('poll active-version', { oldVersion, newVersion, priceListId: data?.priceListId || '' })
- if (newVersion > 0 && newVersion !== oldVersion) {
- setStoredVersion(newVersion, data?.priceListId)
- emit('price_list_updated', data)
- }
- } catch (e) {
- // ignore
- }
- }
- function handleWsMessage(raw) {
- let msg = null
- try {
- msg = typeof raw === 'string' ? JSON.parse(raw) : raw
- } catch (e) {
- msg = null
- }
- if (!msg || msg.event !== 'price_list_updated') return
- const data = msg.data || {}
- const newVersion = Number(data.priceVersion || 0) || 0
- const oldVersion = getStoredVersion()
- const oldPriceListId = getStoredPriceListId()
- const newPriceListId = String(data.priceListId || '').trim()
- wsLog('price_list_updated event', {
- oldVersion,
- newVersion,
- oldPriceListId,
- newPriceListId
- })
- // WebSocket 推送属于“已发生更新”的强信号:
- // 即便版本号缺失/相同,也触发页面刷新,避免界面停留旧价。
- const nextVersion = newVersion > 0 ? Math.max(newVersion, oldVersion) : oldVersion
- setStoredVersion(nextVersion, newPriceListId || oldPriceListId)
- // 若版本与清单都完全没变化,仍允许刷新一次,确保客户端与服务端最终一致。
- emit('price_list_updated', data)
- try {
- uni.$emit && uni.$emit('price_list_updated', data)
- } catch (e) {
- // ignore
- }
- }
- export async function ensurePriceRealtimeConnected() {
- const token = String(uni.getStorageSync(TOKEN_KEY) || '').trim()
- if (!token) {
- wsLog('skip connect: empty token')
- ensureFallbackPollRunning()
- return
- }
- if (isConnecting) {
- wsLog('skip connect: already connecting')
- return
- }
- if (socketTask) {
- wsLog('skip connect: socketTask exists')
- return
- }
- isConnecting = true
- shouldReconnect = true
- socketOpened = false
- try {
- clearReconnect()
- ensureFallbackPollRunning()
- const url = buildWsUrl(token)
- wsLog('connect socket', { url })
- if (WS_ENABLED) {
- socketTask = uni.connectSocket({ url })
- bindSocketListeners()
- }
- } finally {
- isConnecting = false
- }
- }
- export function stopPriceRealtime() {
- wsLog('stop realtime')
- clearReconnect()
- clearFallbackPoll()
- reconnectDelayMs = 1000
- shouldReconnect = false
- if (socketTask) {
- try {
- safeCloseSocket(socketTask, 'manual-stop')
- } catch (e) {
- // ignore
- }
- }
- socketOpened = false
- socketTask = null
- }
- export async function checkActivePriceVersionOnce() {
- await checkActiveVersionAndEmitIfChanged()
- }
|