hejianhao
2026-04-26 fe844ba68369ec2cf7b5c2f62d1b74c4deedb60b
feat(websocket): 增加应用前后台状态管理并优化连接健壮性

- 新增 onAppShow/onAppHide 方法,在应用前后台切换时暂停/恢复心跳与重连
- 引入连接代次机制,防止旧连接回调污染新连接状态
- 优化心跳机制,增加连续丢失检测与后台防误判
- 改进重连策略,使用指数退避算法并支持前台延迟重连
- 增强 UID 校验,避免无效 UID 导致连接异常
2个文件已修改
956 ■■■■■ 已修改文件
src/app.tsx 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/websocket.ts 953 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/app.tsx
@@ -16,6 +16,7 @@
import { getStorage } from './utils/tools';
import { jumpTo } from './utils/jump';
import { clearSubscribeCount } from './utils/subscribe';
import wsManager from './utils/websocket';
import './app.less';
import { dispatchPreRouterUpdate } from './actions/router';
@@ -93,6 +94,7 @@
  }
  componentDidShow() {
    wsManager.onAppShow();
    if (ENV === 'weapp' && Taro.canIUse('onBeforePageUnload') && Taro.canIUse('offBeforePageUnload')) {
      // @ts-ignore
      wx.onBeforePageUnload(this.beforePageUnload);
@@ -126,6 +128,7 @@
  }
  componentDidHide() {
    wsManager.onAppHide();
    if (ENV === 'weapp' && Taro.canIUse('offBeforePageUnload')) {
      // @ts-ignore
      wx.offBeforePageUnload(this.beforePageUnload);
src/utils/websocket.ts
@@ -5,27 +5,36 @@
type MessageCallback = (data: any) => void;
enum SocketState {
  IDLE = 'idle',
  CONNECTING = 'connecting',
  CONNECTED = 'connected',
}
class WebSocketManager {
  private static instance: WebSocketManager;
  private socketTask: Taro.SocketTask | null = null;
  private url: string = '';
  private isConnected: boolean = false;
  private isConnecting: boolean = false;
  private isManualClose: boolean = false;
  private reconnectAttempts: number = 0;
  private maxReconnectAttempts: number = 5;
  private reconnectTimer: any = null;
  private heartbeatTimer: any = null;
  private heartbeatTimeoutTimer: any = null; // 用于检测心跳超时的定时器
  private heartbeatInterval: number = 10000; // 10秒心跳
  private heartbeatTimeout: number = 3000;   // 3秒收不到响应即视为掉线
  private listeners: Map<string, MessageCallback[]> = new Map();
  private globalEventsBound: boolean = false;
  private isHeartbeatMessage(data: any) {
    const type = String(data?.type || '').toLowerCase();
    return type === 'ping' || type === 'pong';
  }
  private socketTask: Taro.SocketTask | null = null; // 当前生效的 socketTask
  private url: string = ''; // 当前连接地址
  private socketState: SocketState = SocketState.IDLE; // 当前连接状态
  private isConnected: boolean = false; // 对外兼容的连接态
  private isConnecting: boolean = false; // 对外兼容的连接中标记
  private isManualClose: boolean = false; // 是否手动关闭
  private isAppActive: boolean = true; // 应用是否处于前台
  private shouldReconnectWhenActive: boolean = false; // 回前台后是否需要补重连
  private reconnectAttempts: number = 0; // 当前连续重连次数
  private reconnectTimer: any = null; // 重连定时器
  private heartbeatTimer: any = null; // 心跳发送定时器
  private heartbeatTimeoutTimer: any = null; // 单次等待 pong 的超时定时器
  private heartbeatInterval: number = 15000; // 心跳间隔
  private heartbeatTimeout: number = 10000; // 单次心跳等待 pong 的超时时间
  private maxHeartbeatMisses: number = 2; // 连续丢失 pong 的最大次数
  private listeners: Map<string, MessageCallback[]> = new Map(); // 事件监听器
  private globalEventsBound: boolean = false; // 全局事件是否已绑定
  private connectionSeed: number = 0; // 连接代次种子
  private activeConnectionId: number = 0; // 当前生效连接代次
  private heartbeatSeq: number = 0; // 心跳序号
  private pendingHeartbeatSeq: number | null = null; // 当前等待 pong 的心跳序号
  private missedHeartbeatCount: number = 0; // 连续未收到 pong 的次数
  private constructor() { }
@@ -37,339 +46,154 @@
  }
  /**
   * 初始化连接
   * @param url WebSocket地址
   * 应用回到前台时恢复心跳或补发重连。
   */
  public onAppShow() {
    this.isAppActive = true;
    this.shouldReconnectWhenActive = false;
    if (this.isConnected && this.activeConnectionId > 0) {
      this.startHeartbeat(this.activeConnectionId);
      return;
    }
    if (!this.isManualClose && this.url) {
      this.connect(this.url);
    }
  }
  /**
   * 应用进入后台时暂停心跳,避免后台定时器抖动误判掉线。
   */
  public onAppHide() {
    this.isAppActive = false;
    this.stopHeartbeat();
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
      this.shouldReconnectWhenActive = true;
    }
  }
  /**
   * 初始化连接。
   * @param url WebSocket 地址
   */
  public async connect(url: string) {
    if (this.socketTask || this.isConnected || this.isConnecting) {
      console.log('WebSocket 已连接或正在连接中');
    if (!url) {
      return;
    }
    if (this.isConnected && this.url === url) {
      return;
    }
    if (this.isConnecting && this.url === url) {
      return;
    }
    this.url = url;
    this.isConnecting = true;
    this.isManualClose = false;
    console.log(`正在连接 WebSocket: ${url}`);
    this.shouldReconnectWhenActive = false;
    this.isConnecting = true;
    this.socketState = SocketState.CONNECTING;
    this.clearReconnectTimer();
    const uid = await getStorage(_UID) || '-1';
    const task = Taro.connectSocket({
      header: {
        'XP-Uid': uid.toString(),
      },
      url: this.url,
      success: () => { },
      fail: (err) => {
        console.error('WebSocket 连接失败:', err);
        this.isConnecting = false;
        this.handleReconnect();
    const connectionId = this.createConnectionId();
    console.log(`正在连接 WebSocket: ${url},连接代次:${connectionId}`);
    try {
      const uid = await this.getValidUid();
      if (!this.isCurrentConnection(connectionId)) {
        return;
      }
    });
    const maybeTask: any = task as any;
    if (maybeTask && typeof maybeTask.onOpen === 'function') {
      this.socketTask = maybeTask;
      this.isConnecting = false;
      this.initEventListeners();
      return;
    }
    if (maybeTask && typeof maybeTask.then === 'function') {
      maybeTask
        .then((resolvedTask) => {
          this.socketTask = resolvedTask;
          this.isConnecting = false;
          this.initEventListeners();
        })
        .catch((err) => {
      if (!uid) {
        console.warn('WebSocket 连接已取消,UID 无效');
        this.markConnectionClosed(connectionId);
        return;
      }
      const task = Taro.connectSocket({
        header: {
          'XP-Uid': uid,
        },
        url: this.url,
        success: () => { },
        fail: (err) => {
          if (!this.isCurrentConnection(connectionId)) {
            return;
          }
          console.error('WebSocket 连接失败:', err);
          this.isConnecting = false;
          this.handleReconnect();
        });
      return;
          this.markConnectionClosed(connectionId);
          this.scheduleReconnect(connectionId, 'connect_fail');
        }
      });
      const maybeTask: any = task as any;
      if (maybeTask && typeof maybeTask.onOpen === 'function') {
        this.attachTask(connectionId, maybeTask);
        return;
      }
      if (maybeTask && typeof maybeTask.then === 'function') {
        maybeTask
          .then((resolvedTask: Taro.SocketTask) => {
            if (!this.isCurrentConnection(connectionId)) {
              this.closeTaskSilently(resolvedTask, '过期连接任务关闭');
              return;
            }
            this.attachTask(connectionId, resolvedTask);
          })
          .catch((err: any) => {
            if (!this.isCurrentConnection(connectionId)) {
              return;
            }
            console.error('WebSocket 连接失败:', err);
            this.markConnectionClosed(connectionId);
            this.scheduleReconnect(connectionId, 'connect_promise_fail');
          });
        return;
      }
      this.bindGlobalEventListeners();
    } catch (error) {
      if (!this.isCurrentConnection(connectionId)) {
        return;
      }
      console.error('初始化 WebSocket 连接失败:', error);
      this.markConnectionClosed(connectionId);
      this.scheduleReconnect(connectionId, 'connect_exception');
    }
    this.socketTask = null;
    this.isConnecting = false;
    this.initEventListeners();
  }
  /**
   * 对外暴露当前连接状态。
   */
  public isConnectedStatus() {
    return this.isConnected;
  }
  /**
   * 初始化事件监听
   */
  private initEventListeners() {
    if (this.socketTask && typeof this.socketTask.onOpen === 'function') {
      this.socketTask.onOpen(async () => {
        console.log('WebSocket 连接已打开');
        this.isConnected = true;
        this.isConnecting = false;
        this.reconnectAttempts = 0;
        this.startHeartbeat();
        const uid = await getStorage(_UID) || '-1';
        this.send({ type: 'auth', appUserId: uid.toString(), timestamp: Date.now() });
        this.notifyListeners('open', { status: 'connected' });
      });
      this.socketTask.onMessage((res) => {
        this.resetHeartbeatTimeout();
        let data: any = {};
        try {
          data = safeParse(res.data as string, {});
          console.log('收到 WebSocket 消息:', res.data);
        } catch (e) {
          console.warn('解析 WebSocket data 失败:', res.data);
          this.notifyListeners('message', res.data);
          return;
        }
        const content = safeParse(data.content || '{}', {});
        try {
          this.handleIncomingMessage(data, content);
          if (data.type) {
            this.notifyListeners(data.type, data);
          }
          this.notifyListeners('message', data);
        } catch (e) {
          console.error('处理 WebSocket 消息出错:', e);
        }
      });
      this.socketTask.onClose((res) => {
        console.log('WebSocket 连接已关闭', res);
        this.isConnected = false;
        this.isConnecting = false;
        this.stopHeartbeat();
        this.socketTask = null;
        if (!this.isManualClose) {
          this.handleReconnect();
        }
        this.notifyListeners('close', res);
      });
      this.socketTask.onError((err) => {
        console.error('WebSocket 错误:', err);
        this.isConnected = false;
        this.isConnecting = false;
        this.socketTask = null;
        this.stopHeartbeat();
        if (!this.isManualClose) {
          this.handleReconnect();
        }
        this.notifyListeners('error', err);
      });
      return;
    }
    if (this.globalEventsBound) return;
    this.globalEventsBound = true;
    Taro.onSocketOpen(async () => {
      console.log('WebSocket 连接已打开');
      this.isConnected = true;
      this.isConnecting = false;
      this.reconnectAttempts = 0;
      this.startHeartbeat();
      const uid = await getStorage(_UID) || '-1';
      this.send({ type: 'auth', appUserId: uid.toString(), timestamp: Date.now() });
      this.notifyListeners('open', { status: 'connected' });
    });
    Taro.onSocketMessage((res) => {
      this.resetHeartbeatTimeout();
      let data: any = {};
      try {
        data = safeParse(res.data as string, {});
        console.log('收到 WebSocket 消息:', res.data);
      } catch (e) {
        console.warn('解析 WebSocket data 失败:', res.data);
        this.notifyListeners('message', res.data);
        return;
      }
      const content = safeParse(data.content || '{}', {});
      try {
        this.handleIncomingMessage(data, content);
        if (data.type) {
          this.notifyListeners(data.type, data);
        }
        this.notifyListeners('message', data);
      } catch (e) {
        console.error('处理 WebSocket 消息出错:', e);
      }
    });
    Taro.onSocketClose((res) => {
      console.log('WebSocket 连接已关闭', res);
      this.isConnected = false;
      this.isConnecting = false;
      this.stopHeartbeat();
      this.socketTask = null;
      if (!this.isManualClose) {
        this.handleReconnect();
      }
      this.notifyListeners('close', res);
    });
    Taro.onSocketError((err) => {
      console.error('WebSocket 错误:', err);
      this.isConnected = false;
      this.isConnecting = false;
      this.socketTask = null;
      this.stopHeartbeat();
      if (!this.isManualClose) {
        this.handleReconnect();
      }
      this.notifyListeners('error', err);
    });
  }
  /**
   * 发送消息
   * 发送消息。
   * @param data 消息内容
   */
  public send(data: any) {
    if (!this.isConnected) {
      console.warn('WebSocket 未连接,消息未发送:', data);
      return;
    }
    const message = typeof data === 'string' ? data : JSON.stringify(data);
    if (this.socketTask) {
      this.socketTask.send({
        data: message,
        fail: (err) => {
          console.error('发送消息失败:', err);
        }
      });
    } else {
      Taro.sendSocketMessage({
        data: message,
        fail: (err) => {
          console.error('发送消息失败:', err);
        }
      });
    }
    this.sendInternal(data, { silentWhenDisconnected: false });
  }
  /**
   * 关闭连接
   * 主动关闭连接。
   */
  public close() {
    const closedConnectionId = this.activeConnectionId;
    this.isManualClose = true;
    this.shouldReconnectWhenActive = false;
    this.clearReconnectTimer();
    this.stopHeartbeat();
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
    if (this.socketTask) {
      this.socketTask.close({
        code: 1000,
        reason: '用户主动关闭连接'
      });
      this.socketTask = null;
    }
    this.isConnected = false;
    this.isConnecting = false;
    this.invalidateCurrentConnection();
    this.closeTaskSilently(this.socketTask, '用户主动关闭连接');
    this.socketTask = null;
    this.markConnectionClosed(closedConnectionId);
  }
  /**
   * 处理重连
   */
  private handleReconnect() {
    if (this.reconnectTimer) return;
    // 固定约 3 秒重连一次,并加上 0~1 秒的随机抖动
    const delay = 3000 + Math.floor(Math.random() * 1000);
    console.log(`正在尝试重连,延迟 ${delay}ms... (第 ${this.reconnectAttempts + 1} 次尝试)`);
    this.reconnectTimer = setTimeout(() => {
      // 防止数字溢出(无限重试时)
      if (this.reconnectAttempts < 9999) {
        this.reconnectAttempts++;
      }
      this.reconnectTimer = null;
      this.connect(this.url);
    }, delay);
  }
  /**
   * 开启心跳
   */
  private startHeartbeat() {
    this.stopHeartbeat();
    this.heartbeatTimer = setInterval(() => {
      if (this.isConnected) {
        this.send({ type: 'ping', timestamp: Date.now() });
        // 发送 ping 后,启动一个超时检测
        // 如果在配置的时间内没有收到任何消息(pong 或业务消息),则认为连接已假死
        if (this.heartbeatTimeoutTimer) {
          clearTimeout(this.heartbeatTimeoutTimer);
        }
        this.heartbeatTimeoutTimer = setTimeout(() => {
          console.warn('心跳超时,未收到服务器响应,主动断开 WebSocket 连接');
          this.closeAndReconnect();
        }, this.heartbeatTimeout);
      }
    }, this.heartbeatInterval);
  }
  /**
   * 停止心跳
   */
  private stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
    if (this.heartbeatTimeoutTimer) {
      clearTimeout(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }
  }
  /**
   * 收到消息时重置心跳超时检测
   */
  private resetHeartbeatTimeout() {
    if (this.heartbeatTimeoutTimer) {
      clearTimeout(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }
  }
  /**
   * 主动关闭并重连 (处理假死状态)
   */
  private closeAndReconnect() {
    // 强制关闭当前连接
    if (this.socketTask) {
      try {
        this.socketTask.close({
          code: 1000,
          reason: '心跳超时主动关闭'
        });
      } catch (e) {
        console.error('强制关闭 Socket 失败:', e);
      }
      this.socketTask = null;
    }
    this.isConnected = false;
    this.isConnecting = false;
    this.stopHeartbeat();
    // 立即触发重连
    if (!this.isManualClose) {
      this.handleReconnect();
    }
  }
  /**
   * 注册事件监听
   * 注册事件监听。
   * @param event 事件名称
   * @param callback 回调函数
   */
@@ -384,7 +208,7 @@
  }
  /**
   * 移除事件监听
   * 移除事件监听。
   * @param event 事件名称
   * @param callback 回调函数
   */
@@ -400,7 +224,472 @@
  }
  /**
   * 通知监听器
   * 创建新的连接代次,并让旧连接回调全部失效。
   */
  private createConnectionId() {
    this.connectionSeed += 1;
    this.activeConnectionId = this.connectionSeed;
    return this.activeConnectionId;
  }
  /**
   * 当前连接失效后让旧回调不再影响全局状态。
   */
  private invalidateCurrentConnection() {
    this.connectionSeed += 1;
    this.activeConnectionId = this.connectionSeed;
    this.pendingHeartbeatSeq = null;
  }
  /**
   * 判断回调是否仍然属于当前连接代次。
   */
  private isCurrentConnection(connectionId: number) {
    return connectionId > 0 && connectionId === this.activeConnectionId;
  }
  /**
   * 绑定当前 socketTask,并注册基于连接代次的回调。
   */
  private attachTask(connectionId: number, task: Taro.SocketTask) {
    if (!this.isCurrentConnection(connectionId)) {
      this.closeTaskSilently(task, '过期连接任务关闭');
      return;
    }
    this.socketTask = task;
    this.bindTaskEventListeners(connectionId, task);
  }
  /**
   * 绑定 task 级别事件,确保旧连接回调不会污染新连接。
   */
  private bindTaskEventListeners(connectionId: number, task: Taro.SocketTask) {
    task.onOpen(() => {
      this.handleSocketOpen(connectionId);
    });
    task.onMessage((res) => {
      this.handleSocketMessage(connectionId, res.data);
    });
    task.onClose((res) => {
      this.handleSocketClose(connectionId, res);
    });
    task.onError((err) => {
      this.handleSocketError(connectionId, err);
    });
  }
  /**
   * 兼容无 task 级事件的运行环境。
   */
  private bindGlobalEventListeners() {
    if (this.globalEventsBound) return;
    this.globalEventsBound = true;
    Taro.onSocketOpen(() => {
      this.handleSocketOpen(this.activeConnectionId);
    });
    Taro.onSocketMessage((res) => {
      this.handleSocketMessage(this.activeConnectionId, res.data);
    });
    Taro.onSocketClose((res) => {
      this.handleSocketClose(this.activeConnectionId, res);
    });
    Taro.onSocketError((err) => {
      this.handleSocketError(this.activeConnectionId, err);
    });
  }
  /**
   * 处理连接建立成功。
   */
  private async handleSocketOpen(connectionId: number) {
    if (!this.isCurrentConnection(connectionId)) {
      return;
    }
    console.log(`WebSocket 连接已打开,连接代次:${connectionId}`);
    this.isConnected = true;
    this.isConnecting = false;
    this.socketState = SocketState.CONNECTED;
    this.reconnectAttempts = 0;
    this.resetHeartbeatState();
    if (this.isAppActive) {
      this.startHeartbeat(connectionId);
    }
    const uid = await this.getValidUid();
    if (!this.isCurrentConnection(connectionId)) {
      return;
    }
    if (!uid) {
      console.warn('WebSocket 鉴权已取消,UID 无效');
      const expiredTask = this.socketTask;
      this.stopHeartbeat();
      this.invalidateCurrentConnection();
      this.socketTask = null;
      this.closeTaskSilently(expiredTask, 'UID 无效,关闭未鉴权连接');
      this.markConnectionClosed(connectionId);
      return;
    }
    this.sendInternal(
      { type: 'auth', appUserId: uid, timestamp: Date.now() },
      { silentWhenDisconnected: true }
    );
    this.notifyListeners('open', { status: 'connected' });
  }
  /**
   * 处理服务端消息。
   */
  private handleSocketMessage(connectionId: number, rawData: any) {
    if (!this.isCurrentConnection(connectionId)) {
      return;
    }
    console.log('收到 WebSocket 消息:', rawData);
    const data = this.normalizeIncomingMessage(rawData);
    if (this.isPongMessage(data)) {
      this.handlePong(connectionId);
    }
    const content = safeParse(data?.content || '{}', {});
    try {
      this.handleIncomingMessage(data, content);
      if (data?.type) {
        this.notifyListeners(data.type, data);
      }
      this.notifyListeners('message', data);
    } catch (error) {
      console.error('处理 WebSocket 消息出错:', error);
    }
  }
  /**
   * 处理连接关闭。
   */
  private handleSocketClose(connectionId: number, res: any) {
    if (!this.isCurrentConnection(connectionId)) {
      console.log('忽略过期连接关闭回调', res);
      return;
    }
    console.log('WebSocket 连接已关闭', res);
    this.markConnectionClosed(connectionId);
    this.notifyListeners('close', res);
    if (!this.isManualClose) {
      this.scheduleReconnect(connectionId, 'socket_close');
    }
  }
  /**
   * 处理连接异常。
   */
  private handleSocketError(connectionId: number, err: any) {
    if (!this.isCurrentConnection(connectionId)) {
      console.log('忽略过期连接错误回调', err);
      return;
    }
    console.error('WebSocket 错误:', err);
    this.markConnectionClosed(connectionId);
    this.notifyListeners('error', err);
    if (!this.isManualClose) {
      this.scheduleReconnect(connectionId, 'socket_error');
    }
  }
  /**
   * 标准化消息结构,兼容纯字符串 pong。
   */
  private normalizeIncomingMessage(rawData: any) {
    const parsedData = safeParse(rawData, rawData);
    if (typeof parsedData === 'string') {
      return {
        type: parsedData,
        raw: rawData,
      };
    }
    return parsedData || {};
  }
  /**
   * 获取有效 UID,约束连接头和鉴权消息都不能使用 -1 等无效值。
   */
  private async getValidUid() {
    const uid = await getStorage(_UID);
    return this.normalizeValidUid(uid);
  }
  /**
   * 标准化并校验 UID。
   */
  private normalizeValidUid(uid: any) {
    const normalizedUid = String(uid ?? '').trim();
    if (!normalizedUid || normalizedUid === '-1' || normalizedUid === 'undefined' || normalizedUid === 'null') {
      return null;
    }
    return normalizedUid;
  }
  /**
   * 判断消息是否为服务端 pong。
   */
  private isPongMessage(data: any) {
    const type = String(data?.type || '').toLowerCase();
    return type === 'pong';
  }
  /**
   * 根据 pong 清理等待状态,并恢复心跳健康计数。
   */
  private handlePong(connectionId: number) {
    if (!this.isCurrentConnection(connectionId)) {
      return;
    }
    this.pendingHeartbeatSeq = null;
    this.missedHeartbeatCount = 0;
    if (this.heartbeatTimeoutTimer) {
      clearTimeout(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }
  }
  /**
   * 启动心跳轮询。
   */
  private startHeartbeat(connectionId: number) {
    this.stopHeartbeat();
    if (!this.isCurrentConnection(connectionId) || !this.isAppActive) {
      return;
    }
    this.heartbeatTimer = setInterval(() => {
      if (!this.isCurrentConnection(connectionId)) {
        return;
      }
      if (!this.isConnected || this.socketState !== SocketState.CONNECTED || !this.isAppActive) {
        return;
      }
      if (this.pendingHeartbeatSeq !== null) {
        return;
      }
      this.sendHeartbeat(connectionId);
    }, this.heartbeatInterval);
  }
  /**
   * 发送一轮 ping,并等待服务端回 pong。
   */
  private sendHeartbeat(connectionId: number) {
    if (!this.isCurrentConnection(connectionId) || !this.isAppActive) {
      return;
    }
    const currentHeartbeatSeq = ++this.heartbeatSeq;
    this.pendingHeartbeatSeq = currentHeartbeatSeq;
    const hasSent = this.sendInternal(
      { type: 'ping', timestamp: Date.now(), seq: currentHeartbeatSeq },
      { silentWhenDisconnected: true }
    );
    if (!hasSent) {
      this.handleHeartbeatMiss(connectionId, currentHeartbeatSeq, 'ping_send_fail');
      return;
    }
    this.heartbeatTimeoutTimer = setTimeout(() => {
      this.handleHeartbeatMiss(connectionId, currentHeartbeatSeq, 'pong_timeout');
    }, this.heartbeatTimeout);
  }
  /**
   * 处理单轮心跳未收到 pong 的场景。
   */
  private handleHeartbeatMiss(connectionId: number, heartbeatSeq: number, reason: string) {
    if (!this.isCurrentConnection(connectionId)) {
      return;
    }
    if (!this.isAppActive) {
      this.pendingHeartbeatSeq = null;
      return;
    }
    if (this.pendingHeartbeatSeq !== heartbeatSeq) {
      return;
    }
    this.pendingHeartbeatSeq = null;
    if (this.heartbeatTimeoutTimer) {
      clearTimeout(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }
    this.missedHeartbeatCount += 1;
    console.warn(`WebSocket 心跳未收到 pong,原因:${reason},连续失败 ${this.missedHeartbeatCount} 次`);
    if (this.missedHeartbeatCount < this.maxHeartbeatMisses) {
      return;
    }
    this.safeReconnect(connectionId, reason);
  }
  /**
   * 安全关闭当前连接并发起重连,不让旧连接回调影响新连接。
   */
  private safeReconnect(connectionId: number, reason: string) {
    if (!this.isCurrentConnection(connectionId)) {
      return;
    }
    console.warn(`WebSocket 准备重连,原因:${reason},连接代次:${connectionId}`);
    const expiredTask = this.socketTask;
    this.stopHeartbeat();
    this.markConnectionClosed(connectionId);
    this.invalidateCurrentConnection();
    this.socketTask = null;
    this.closeTaskSilently(expiredTask, `重连前关闭旧连接:${reason}`);
    this.scheduleReconnect(this.activeConnectionId, reason);
  }
  /**
   * 停止心跳及其超时检测。
   */
  private stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
    if (this.heartbeatTimeoutTimer) {
      clearTimeout(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }
    this.pendingHeartbeatSeq = null;
  }
  /**
   * 重置心跳健康状态。
   */
  private resetHeartbeatState() {
    this.pendingHeartbeatSeq = null;
    this.missedHeartbeatCount = 0;
    if (this.heartbeatTimeoutTimer) {
      clearTimeout(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }
  }
  /**
   * 调度下一次重连,后台期间延后到前台再处理。
   */
  private scheduleReconnect(connectionId: number, reason: string) {
    if (!this.isCurrentConnection(connectionId) || this.isManualClose || !this.url) {
      return;
    }
    if (!this.isAppActive) {
      this.shouldReconnectWhenActive = true;
      return;
    }
    if (this.reconnectTimer) {
      return;
    }
    const delay = this.getReconnectDelay();
    console.log(`正在尝试重连,原因:${reason},延迟 ${delay}ms... (第 ${this.reconnectAttempts + 1} 次尝试)`);
    this.reconnectTimer = setTimeout(() => {
      this.reconnectTimer = null;
      if (this.isManualClose || !this.isAppActive || !this.url) {
        return;
      }
      this.reconnectAttempts += 1;
      this.connect(this.url);
    }, delay);
  }
  /**
   * 获取退避后的重连延迟。
   */
  private getReconnectDelay() {
    const retryLevel = Math.min(this.reconnectAttempts, 4);
    const baseDelay = Math.min(1000 * Math.pow(2, retryLevel), 15000);
    const jitter = Math.floor(Math.random() * 500);
    return baseDelay + jitter;
  }
  /**
   * 清理已有重连任务。
   */
  private clearReconnectTimer() {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
  }
  /**
   * 发送消息的底层实现。
   */
  private sendInternal(data: any, options: { silentWhenDisconnected: boolean }) {
    if (!this.isConnected || this.socketState !== SocketState.CONNECTED) {
      if (!options.silentWhenDisconnected) {
        console.warn('WebSocket 未连接,消息未发送:', data);
      }
      return false;
    }
    const message = typeof data === 'string' ? data : JSON.stringify(data);
    if (this.socketTask) {
      this.socketTask.send({
        data: message,
        fail: (err) => {
          console.error('发送消息失败:', err);
        }
      });
      return true;
    }
    Taro.sendSocketMessage({
      data: message,
      fail: (err) => {
        console.error('发送消息失败:', err);
      }
    });
    return true;
  }
  /**
   * 关闭指定 task,不影响当前代次判断。
   */
  private closeTaskSilently(task: Taro.SocketTask | null | undefined, reason: string) {
    if (!task) {
      return;
    }
    try {
      task.close({
        code: 1000,
        reason,
      });
    } catch (error) {
      console.error('关闭 SocketTask 失败:', error);
    }
  }
  /**
   * 统一更新关闭态。
   */
  private markConnectionClosed(connectionId: number) {
    if (this.isCurrentConnection(connectionId)) {
      this.socketTask = null;
    }
    this.isConnected = false;
    this.isConnecting = false;
    this.socketState = SocketState.IDLE;
    this.stopHeartbeat();
  }
  /**
   * 通知监听器。
   * @param event 事件名称
   * @param data 数据
   */
@@ -417,7 +706,9 @@
    }
  }
  // 处理传入消息
  /**
   * 处理传入消息。
   */
  private handleIncomingMessage(data: any, content: any) {
    if (data?.type === 'ORDER_STATUS') {
      const status = content?.status;
@@ -483,15 +774,23 @@
    }
  }
  // 归一化路线点
  /**
   * 归一化路线点。
   */
  private normalizeRoutePoints(content: any) {
    return this.normalizeRoutePointsByKey(content, 'polyline');
  }
  /**
   * 归一化已行驶路线点。
   */
  private normalizeServedPoints(content: any) {
    return this.normalizeRoutePointsByKey(content, 'served');
  }
  /**
   * 根据指定 key 归一化路线点。
   */
  private normalizeRoutePointsByKey(content: any, key: 'polyline' | 'served') {
    const route = safeParse(content?.route ?? '{}', {});
    const rawPoints = route?.[key] ?? [];
@@ -509,7 +808,9 @@
      .filter(Boolean);
  }
  // 映射订单状态到行程状态
  /**
   * 映射订单状态到行程状态。
   */
  private mapOrderStatusToTripStatus(status: number) {
    switch (status) {
      case 0: