hejianhao
2026-04-28 079ad31adf157435f2637f2c5495d18519580543
feat(begin-route): 增加WebSocket掉线兜底轮询与运行时状态同步统一

- 新增 `useSocketFallbackPolling` hook,在WebSocket断开且页面处于关键行程状态时启动轮询,补偿订单状态、车辆信息及路线数据
- 新增 `runtime-sync` 模块,统一订单快照与路线快照的数据结构及同步逻辑,供WebSocket事件、页面恢复和兜底轮询复用
- 重构 `useWebSocketEvents` 与 `usePageRestore`,使用新的快照同步方法替代分散的状态更新逻辑
- 在首页订单状态卡片展示条件中增加 `useMemo` 优化,避免真实数据返回前展示默认图
2个文件已添加
4个文件已修改
1013 ■■■■■ 已修改文件
src/pages/robotaxi/home/index.tsx 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/runtime-sync.ts 429 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-socket-fallback-polling.ts 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx
@@ -102,6 +102,7 @@
  const [orderData, setOrderData] = useState<any>({}); // 订单数据
  const [isLogin, setIsLogin] = useState(false); // 是否登录
  const [overlayAnchorBottom, setOverlayAnchorBottom] = useState<number | null>(null); // 浮层初始化锚点(距底部)
  const shouldShowOrderStatusCard = useMemo(() => tripStatus === 3 || Boolean(orderData?.id), [orderData?.id, tripStatus]); // 订单状态卡片展示时机,避免真实数据未返回时先展示默认图
  const {
    boxHeight,
    originOpacity,
@@ -539,12 +540,16 @@
                </View>
              </View>
            </View>
            <OrderStatusCard
              tripStatus={tripStatus}
              orderData={orderData}
              onViewDetail={toOrderDetail}
              orderStatusMap={ORDER_STATUS}
            />
            {
              shouldShowOrderStatusCard && (
                <OrderStatusCard
                  tripStatus={tripStatus}
                  orderData={orderData}
                  onViewDetail={toOrderDetail}
                  orderStatusMap={ORDER_STATUS}
                />
              )
            }
            {/* 运营活动 */}
            {bannerList.length > 0 && (
              <View className="activity">
src/robotaxi/pages/begin-route/hooks/runtime-sync.ts
New file
@@ -0,0 +1,429 @@
import Taro from '@tarojs/taro';
import type { MutableRefObject } from 'react';
import {
  mapOrderStatusToTripStatus,
  safeParse
} from '../../../../utils/Robotaxi-public-method';
import {
  normalizeDistanceKm,
  normalizeDurationSeconds
} from '../../../utils/format';
export enum TripStatus {
  CALLING = 3,
  CAR_ARRIVING = 4,
  CAR_ARRIVED = 5,
  IN_TRIP = 6,
  ARRIVED_DESTINATION = 7,
  FINISHED = 8,
  PAID = 9,
}
export enum VehiclePointMode {
  FIRST = 'first',
  LAST = 'last',
  SERVED_LAST_THEN_FIRST = 'served_last_then_first',
  FIRST_THEN_SERVED_LAST = 'first_then_served_last',
}
type RouteDistance = { value: number; unit: 'km' | 'm' };
export type RoutePoint = {
  latitude: number;
  longitude: number;
};
export type OrderSite = {
  id: string;
  name: string;
  latitude: number;
  longitude: number;
  electronicAreaId: string;
  time: number;
};
export type OrderSnapshot = {
  orderId: string;
  tripStatus: number | null;
  startSite?: OrderSite;
  endSite?: OrderSite;
  trunkStatus?: boolean;
  showCancelPopup?: boolean;
  showPassengerNotice?: boolean;
};
export type RouteSnapshot = {
  points: RoutePoint[];
  distance: RouteDistance | null;
  duration: number | null;
  direction: number | null;
  vehicleMoveAlongPoints: RoutePoint[];
  forceFullRedraw: boolean;
  eraseVehiclePointMode: VehiclePointMode;
  normalVehiclePointMode: VehiclePointMode;
};
export type ApplyOrderSnapshotDeps = {
  orderId: string;
  tripStatusRef: MutableRefObject<number>;
  setOrderId: (value: string) => void;
  setTripStatus: (value: number) => void;
  setStartSite: (value: OrderSite) => void;
  setEndSite: (value: OrderSite) => void;
  setTrunkStatus: (value: boolean) => void;
};
export type ApplyRouteSnapshotDeps = {
  tripStatusRef: MutableRefObject<number>;
  setRoutePoints: (points: RoutePoint[]) => void;
  setPolyline: (lines: any[]) => void;
  setVehicleDirection: (value: number) => void;
  setVehicleMoveAlongPoints?: (points: RoutePoint[]) => void;
  setRouteDistance: (value: RouteDistance | null) => void;
  setRouteDuration: (value: number | null) => void;
  updateVehicleLocationThrottled: (lat: number, lng: number, force?: boolean) => void;
  erasePassedRouteSegments: (points: RoutePoint[]) => void;
  applyRoutePoints: (points: RoutePoint[]) => boolean;
  polylineKeyRef: MutableRefObject<string>;
  lastPolylineUpdateAtRef: MutableRefObject<number>;
  lastRoutePointRef: MutableRefObject<RoutePoint | null>;
  lastRouteUpdateAtRef: MutableRefObject<number>;
  lastVehiclePointRef?: MutableRefObject<RoutePoint | null>;
  currentFullRouteRef: MutableRefObject<RoutePoint[]>;
  lastErasedIndexRef: MutableRefObject<number>;
  orderRouteInitializedRef: MutableRefObject<boolean>;
};
type ApplyOrderSnapshotOptions = {
  protectCriticalTripStatus?: boolean;
};
const CRITICAL_TRIP_STATUSES = [TripStatus.CAR_ARRIVING, TripStatus.IN_TRIP]; // 关键行程态不允许被旧消息回退
const TERMINAL_ROUTE_STATUSES = [TripStatus.FINISHED, TripStatus.PAID]; // 结束态只同步概要,不再消费路线点
const ROUTE_ERASE_EXCLUDED_STATUSES = [TripStatus.ARRIVED_DESTINATION, TripStatus.FINISHED, TripStatus.PAID]; // 这些状态不走擦线分支
const MOVE_ALONG_ACTIVE_STATUSES = [TripStatus.CAR_ARRIVING, TripStatus.IN_TRIP]; // 这些状态允许同步车辆沿线路径
/**
 * 将后端点位数组规范成前端统一路线点结构。
 */
function normalizeRoutePoints(rawPoints: any): RoutePoint[] {
  if (!Array.isArray(rawPoints)) {
    return [];
  }
  return rawPoints
    .map((item: any) => ({
      longitude: Number(item?.longitude),
      latitude: Number(item?.latitude),
    }))
    .filter((item: RoutePoint) => Number.isFinite(item.longitude) && Number.isFinite(item.latitude));
}
/**
 * 将方向值归一化到 0-359,避免不同来源出现负值或字符串。
 */
function normalizeDirection(rawDirection: any) {
  const direction = Number(rawDirection);
  if (!Number.isFinite(direction)) {
    return null;
  }
  return ((direction % 360) + 360) % 360;
}
/**
 * 构造统一上车点结构,供页面恢复和掉线轮询复用。
 */
export function buildStartSiteFromActiveOrder(activeOrder: any): OrderSite {
  return {
    id: String(activeOrder?.startPointId || ''),
    name: String(activeOrder?.startPointName || ''),
    latitude: Number(activeOrder?.startLat || 0),
    longitude: Number(activeOrder?.startLon || 0),
    electronicAreaId: String(activeOrder?.xpElectronicFenceAreaId || ''),
    time: Number(activeOrder?.startParkingTimeLimit || 0),
  };
}
/**
 * 构造统一下车点结构,供页面恢复和掉线轮询复用。
 */
export function buildEndSiteFromActiveOrder(activeOrder: any): OrderSite {
  return {
    id: String(activeOrder?.endPointId || ''),
    name: String(activeOrder?.endPointName || ''),
    latitude: Number(activeOrder?.endLat || 0),
    longitude: Number(activeOrder?.endLon || 0),
    electronicAreaId: String(activeOrder?.xpElectronicFenceAreaId || ''),
    time: Number(activeOrder?.endParkingTimeLimit || 0),
  };
}
/**
 * 归一化活跃订单接口数据,统一成页面快照结构。
 */
export function normalizeActiveOrderSnapshot(activeOrder: any, fallbackOrderId = ''): OrderSnapshot | null {
  if (!activeOrder) {
    return null;
  }
  return {
    orderId: String(activeOrder?.id || fallbackOrderId || ''),
    tripStatus: mapOrderStatusToTripStatus(activeOrder?.status || 0),
    startSite: buildStartSiteFromActiveOrder(activeOrder),
    endSite: buildEndSiteFromActiveOrder(activeOrder),
    trunkStatus: ![0, 2].includes(Number(activeOrder?.openTrunk)),
  };
}
/**
 * 归一化 socket 订单状态消息,只保留可复用的页面快照字段。
 */
export function normalizeSocketOrderStatusSnapshot(payload: any, fallbackOrderId = ''): OrderSnapshot {
  return {
    orderId: String(payload?.content?.orderId || payload?.orderId || fallbackOrderId || ''),
    tripStatus: typeof payload?.tripStatus === 'number' ? payload.tripStatus : null,
    showCancelPopup: payload?.showCancelPopup === true,
    showPassengerNotice: payload?.showPassengerNotice === true,
  };
}
/**
 * 将订单快照落到页面状态与 storage,保证 socket 和 API 走同一套同步规则。
 */
export function applyOrderSnapshot(
  snapshot: OrderSnapshot | null,
  deps: ApplyOrderSnapshotDeps,
  options: ApplyOrderSnapshotOptions = {}
) {
  if (!snapshot) {
    return {
      nextOrderId: String(deps.orderId || Taro.getStorageSync('orderId') || ''),
      nextTripStatus: Number(deps.tripStatusRef.current),
    };
  }
  const nextOrderId = String(snapshot.orderId || deps.orderId || Taro.getStorageSync('orderId') || '');
  const currentTripStatus = Number(deps.tripStatusRef.current);
  let nextTripStatus = typeof snapshot.tripStatus === 'number' ? snapshot.tripStatus : currentTripStatus;
  if (
    options.protectCriticalTripStatus
    && CRITICAL_TRIP_STATUSES.includes(currentTripStatus)
    && nextTripStatus < currentTripStatus
  ) {
    nextTripStatus = currentTripStatus;
  }
  if (nextOrderId && String(deps.orderId || '') !== nextOrderId) {
    deps.setOrderId(nextOrderId);
    Taro.setStorageSync('orderId', nextOrderId);
  }
  if (typeof nextTripStatus === 'number' && nextTripStatus !== currentTripStatus) {
    deps.setTripStatus(nextTripStatus);
    deps.tripStatusRef.current = nextTripStatus;
    Taro.setStorageSync('tripStatus', nextTripStatus);
  }
  if (snapshot.startSite) {
    deps.setStartSite(snapshot.startSite);
    Taro.setStorageSync('startSite', snapshot.startSite);
  }
  if (snapshot.endSite) {
    deps.setEndSite(snapshot.endSite);
    Taro.setStorageSync('endSite', snapshot.endSite);
  }
  if (typeof snapshot.trunkStatus === 'boolean') {
    deps.setTrunkStatus(snapshot.trunkStatus);
  }
  return {
    nextOrderId,
    nextTripStatus,
  };
}
/**
 * 归一化 socket 路线消息,统一路线点、概要和车辆移动点结构。
 */
export function normalizeSocketRouteSnapshot(payload: any): RouteSnapshot {
  const route = safeParse(payload?.content?.route, {});
  return {
    points: normalizeRoutePoints(payload?.points),
    distance: normalizeDistanceKm(Number(route?.distance)),
    duration: normalizeDurationSeconds(Number(route?.duration)),
    direction: normalizeDirection(payload?.content?.direction),
    vehicleMoveAlongPoints: normalizeRoutePoints(payload?.servedPoints),
    forceFullRedraw: payload?.forceFullRedraw === true,
    eraseVehiclePointMode: VehiclePointMode.FIRST_THEN_SERVED_LAST,
    normalVehiclePointMode: VehiclePointMode.SERVED_LAST_THEN_FIRST,
  };
}
/**
 * 归一化订单路线接口返回,抹平与 socket 路线消息的结构差异。
 */
export function normalizeApiOrderRouteSnapshot(response: any, tripStatus: number): RouteSnapshot {
  const isArrivedPickup = tripStatus === TripStatus.CAR_ARRIVED; // 到达上车点时车辆应贴近路线终点
  const isArrivedDestination = tripStatus === TripStatus.ARRIVED_DESTINATION; // 到达终点时车辆应贴近终点
  return {
    points: normalizeRoutePoints(response?.polyline),
    distance: normalizeDistanceKm(Number(response?.distance)),
    duration: normalizeDurationSeconds(Number(response?.duration)),
    direction: normalizeDirection(response?.direction),
    vehicleMoveAlongPoints: [],
    forceFullRedraw: false,
    eraseVehiclePointMode: isArrivedPickup ? VehiclePointMode.LAST : VehiclePointMode.FIRST,
    normalVehiclePointMode: isArrivedDestination ? VehiclePointMode.LAST : VehiclePointMode.FIRST,
  };
}
/**
 * 根据不同来源的策略解析当前应使用的车辆点位。
 */
function resolveVehiclePoint(
  points: RoutePoint[],
  vehicleMoveAlongPoints: RoutePoint[],
  mode: VehiclePointMode
) {
  const firstPoint = points?.[0] || null;
  const lastPoint = points.length > 0 ? points[points.length - 1] : null;
  const servedLastPoint = vehicleMoveAlongPoints.length > 0
    ? vehicleMoveAlongPoints[vehicleMoveAlongPoints.length - 1]
    : null;
  if (mode === VehiclePointMode.LAST) {
    return lastPoint;
  }
  if (mode === VehiclePointMode.SERVED_LAST_THEN_FIRST) {
    return servedLastPoint || firstPoint;
  }
  if (mode === VehiclePointMode.FIRST_THEN_SERVED_LAST) {
    return firstPoint || servedLastPoint;
  }
  return firstPoint;
}
/**
 * 同步路线概要,避免不同来源重复写距离、时长、方向更新逻辑。
 */
function applyRouteSummary(snapshot: RouteSnapshot, deps: ApplyRouteSnapshotDeps) {
  if (typeof snapshot.direction === 'number') {
    deps.setVehicleDirection(snapshot.direction);
  }
  if (snapshot.distance !== null) {
    deps.setRouteDistance(snapshot.distance);
  }
  if (snapshot.duration !== null) {
    deps.setRouteDuration(snapshot.duration);
  }
}
/**
 * 按统一规则同步车辆点位,保留终点态去抖,避免 marker 抖动。
 */
function syncVehiclePointWithRoute(
  snapshot: RouteSnapshot,
  deps: ApplyRouteSnapshotDeps,
  mode: VehiclePointMode
) {
  const targetPoint = resolveVehiclePoint(snapshot.points, snapshot.vehicleMoveAlongPoints, mode);
  if (!targetPoint) {
    return;
  }
  const currentTripStatus = Number(deps.tripStatusRef.current);
  if (currentTripStatus === TripStatus.ARRIVED_DESTINATION && deps.lastVehiclePointRef?.current) {
    const lastPoint = deps.lastVehiclePointRef.current;
    const latDiff = Math.abs(Number(lastPoint?.latitude || 0) - targetPoint.latitude);
    const lngDiff = Math.abs(Number(lastPoint?.longitude || 0) - targetPoint.longitude);
    const almostSamePoint = latDiff < 0.000003 && lngDiff < 0.000003;
    if (almostSamePoint) {
      return;
    }
  }
  deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true);
}
/**
 * 强制全量重绘时,先清运行时引用,再完整回填路线。
 */
function handleForceFullRouteRedraw(snapshot: RouteSnapshot, deps: ApplyRouteSnapshotDeps) {
  deps.setRoutePoints([]);
  deps.setPolyline([]);
  deps.orderRouteInitializedRef.current = false;
  deps.currentFullRouteRef.current = [];
  deps.lastErasedIndexRef.current = 0;
  deps.polylineKeyRef.current = '';
  deps.lastPolylineUpdateAtRef.current = 0;
  deps.lastRoutePointRef.current = null;
  deps.lastRouteUpdateAtRef.current = 0;
  Taro.nextTick(() => {
    deps.orderRouteInitializedRef.current = snapshot.points.length > 0;
    deps.currentFullRouteRef.current = [...snapshot.points];
    deps.lastErasedIndexRef.current = 0;
    deps.applyRoutePoints(snapshot.points);
    if (snapshot.points.length > 0) {
      syncVehiclePointWithRoute(snapshot, deps, snapshot.normalVehiclePointMode);
    }
  });
}
/**
 * 擦线态优先更新车辆点位,再交给前端差分擦线逻辑处理。
 */
function handleEraseRouteSnapshot(snapshot: RouteSnapshot, deps: ApplyRouteSnapshotDeps) {
  const targetPoint = resolveVehiclePoint(
    snapshot.points,
    snapshot.vehicleMoveAlongPoints,
    snapshot.eraseVehiclePointMode
  );
  if (targetPoint) {
    deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true);
  }
  if (deps.orderRouteInitializedRef.current && deps.currentFullRouteRef.current.length > 0) {
    deps.erasePassedRouteSegments(snapshot.points);
    return;
  }
  deps.orderRouteInitializedRef.current = true;
  deps.currentFullRouteRef.current = [...snapshot.points];
  deps.lastErasedIndexRef.current = 0;
  Taro.nextTick(() => {
    deps.applyRoutePoints(snapshot.points);
  });
}
/**
 * 普通路线态直接同步整条路线,并按策略更新车辆位置。
 */
function handleNormalRouteSnapshot(snapshot: RouteSnapshot, deps: ApplyRouteSnapshotDeps) {
  Taro.nextTick(() => {
    const updated = deps.applyRoutePoints(snapshot.points);
    if (!updated) {
      return;
    }
    if (![4, 5, 6, 7].includes(Number(deps.tripStatusRef.current))) {
      return;
    }
    syncVehiclePointWithRoute(snapshot, deps, snapshot.normalVehiclePointMode);
  });
}
/**
 * 将路线快照落到页面状态,让 socket 路线和 API 路线复用同一套渲染规则。
 */
export function applyRouteSnapshot(snapshot: RouteSnapshot, deps: ApplyRouteSnapshotDeps) {
  applyRouteSummary(snapshot, deps);
  const currentTripStatus = Number(deps.tripStatusRef.current);
  if (TERMINAL_ROUTE_STATUSES.includes(currentTripStatus)) {
    return;
  }
  if (
    MOVE_ALONG_ACTIVE_STATUSES.includes(currentTripStatus)
    && deps.setVehicleMoveAlongPoints
    && snapshot.vehicleMoveAlongPoints.length > 0
  ) {
    deps.setVehicleMoveAlongPoints(snapshot.vehicleMoveAlongPoints);
  }
  if (snapshot.forceFullRedraw) {
    handleForceFullRouteRedraw(snapshot, deps);
    return;
  }
  const isEraseStatus = !ROUTE_ERASE_EXCLUDED_STATUSES.includes(currentTripStatus);
  if (isEraseStatus) {
    handleEraseRouteSnapshot(snapshot, deps);
    return;
  }
  handleNormalRouteSnapshot(snapshot, deps);
}
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts
@@ -1,8 +1,11 @@
import Taro from '@tarojs/taro';
import wsManager from '../../../../utils/websocket';
import { mapOrderStatusToTripStatus } from '../../../../utils/Robotaxi-public-method';
import { getOrderActive, } from '../../../api/home';
import { queryVehicleByOrderId } from '../../../api/begin-route';
import {
  applyOrderSnapshot,
  normalizeActiveOrderSnapshot
} from './runtime-sync';
type Deps = {
  wsHost: string;
@@ -60,30 +63,16 @@
    getOrderActive()
      .then((res: any) => {
        if (!res) return;
        deps.setTrunkStatus(![0, 2].includes(res.openTrunk));
        const nextStartSite = {
          id: res.startPointId || '',
          name: res.startPointName || '',
          latitude: res.startLat || 0,
          longitude: res.startLon || 0,
          electronicAreaId: res.xpElectronicFenceAreaId || '',
          time: res.startParkingTimeLimit || 0,
        };
        const nextEndSite = {
          id: res.endPointId || '',
          name: res.endPointName || '',
          latitude: res.endLat || 0,
          longitude: res.endLon || 0,
          electronicAreaId: res.xpElectronicFenceAreaId || '',
          time: res.endParkingTimeLimit || 0,
        };
        const nextStatus = mapOrderStatusToTripStatus(res.status || 1);
        Taro.setStorageSync('tripStatus', nextStatus);
        deps.setTripStatus(nextStatus);
        Taro.setStorageSync('startSite', nextStartSite);
        Taro.setStorageSync('endSite', nextEndSite);
        deps.setStartSite(nextStartSite);
        deps.setEndSite(nextEndSite);
        const orderSnapshot = normalizeActiveOrderSnapshot(res);
        applyOrderSnapshot(orderSnapshot, {
          orderId: String(Taro.getStorageSync('orderId') || ''),
          tripStatusRef: { current: deps.getCurrentTripStatus() },
          setOrderId: () => { },
          setTripStatus: deps.setTripStatus,
          setStartSite: deps.setStartSite,
          setEndSite: deps.setEndSite,
          setTrunkStatus: deps.setTrunkStatus,
        });
      })
      .catch(() => { });
  };
src/robotaxi/pages/begin-route/hooks/use-socket-fallback-polling.ts
New file
@@ -0,0 +1,301 @@
import Taro, { useDidHide, useDidShow } from '@tarojs/taro';
import { useEffect, useRef, useState } from 'react';
import wsManager from '../../../../utils/websocket';
import { queryVehicleByOrderId } from '../../../api/begin-route';
import { getOrderActive } from '../../../api/home';
import {
  applyOrderSnapshot,
  applyRouteSnapshot,
  normalizeActiveOrderSnapshot,
  normalizeApiOrderRouteSnapshot,
  TripStatus
} from './runtime-sync';
type Deps = {
  /** 行程状态 */
  tripStatus: number;
  /** 行程状态引用 */
  tripStatusRef: React.MutableRefObject<number>;
  /** 订单 ID */
  orderId: string;
  /** 设置行程状态 */
  setTripStatus: (value: number) => void;
  /** 设置订单 ID */
  setOrderId: (value: string) => void;
  /** 设置车辆信息 */
  setVehicleInfo: (value: any) => void;
  /** 设置上车点 */
  setStartSite: (value: any) => void;
  /** 设置下车点 */
  setEndSite: (value: any) => void;
  /** 设置后备箱状态 */
  setTrunkStatus: (value: boolean) => void;
  /** 请求订单路线 */
  routeRequestOrderRouteApi: (orderId: string, status: number) => Promise<any>;
  /** 设置路线点 */
  setRoutePoints: (points: any[]) => void;
  /** 设置线段 */
  setPolyline: (lines: any[]) => void;
  /** 设置车辆方向 */
  setVehicleDirection: (value: number) => void;
  /** 设置车辆沿线移动点 */
  setVehicleMoveAlongPoints: (points: any[]) => void;
  /** 设置路线距离 */
  setRouteDistance: (value: any) => void;
  /** 设置路线时长 */
  setRouteDuration: (value: number | null) => void;
  /** 擦除已走路线 */
  erasePassedRouteSegments: (points: any[]) => void;
  /** 应用路线点 */
  applyRoutePoints: (points: any[]) => boolean;
  /** 更新车辆位置 */
  updateVehicleLocationThrottled: (lat: number, lng: number, force?: boolean) => void;
  /** 折线 key */
  polylineKeyRef: React.MutableRefObject<string>;
  /** 最近一次折线更新时间 */
  lastPolylineUpdateAtRef: React.MutableRefObject<number>;
  /** 最近一次路线首点 */
  lastRoutePointRef: React.MutableRefObject<any>;
  /** 最近一次路线更新时间 */
  lastRouteUpdateAtRef: React.MutableRefObject<number>;
  /** 最近一次车辆点 */
  lastVehiclePointRef: React.MutableRefObject<any>;
  /** 当前完整路线 */
  currentFullRouteRef: React.MutableRefObject<any[]>;
  /** 最近一次擦线索引 */
  lastErasedIndexRef: React.MutableRefObject<number>;
  /** 订单路线是否已初始化 */
  orderRouteInitializedRef: React.MutableRefObject<boolean>;
};
enum FallbackPollingConfig {
  DISCONNECT_CONFIRM_DELAY = 2000,
  STATUS_POLL_INTERVAL = 5000,
  ROUTE_POLL_INTERVAL = 6000,
  VEHICLE_POLL_INTERVAL = 10000,
}
const FALLBACK_ACTIVE_STATUSES = [
  TripStatus.CALLING,
  TripStatus.CAR_ARRIVING,
  TripStatus.CAR_ARRIVED,
  TripStatus.IN_TRIP,
  TripStatus.ARRIVED_DESTINATION
]; // 允许启动兜底轮询的状态
const FALLBACK_ROUTE_STATUSES = [
  TripStatus.CAR_ARRIVING,
  TripStatus.CAR_ARRIVED,
  TripStatus.IN_TRIP,
  TripStatus.ARRIVED_DESTINATION
]; // 需要补偿路线的状态
/**
 * begin-route 掉线兜底编排:负责感知 socket 失联,并在行程关键状态下补偿轮询状态与路线。
 */
export default function useSocketFallbackPolling(deps: Deps) {
  const [isSocketUnavailable, setIsSocketUnavailable] = useState(!wsManager.isConnectedStatus()); // socket 是否处于不可用状态
  const [isPageVisible, setIsPageVisible] = useState(true); // 页面当前是否可见
  const disconnectConfirmTimerRef = useRef<any>(null); // 掉线确认定时器
  const lastVehiclePollAtRef = useRef(0); // 最近一次车辆信息轮询时间
  const statusPollInFlightRef = useRef(false); // 状态轮询是否进行中
  /**
   * 清理掉线确认定时器。
   */
  const clearDisconnectConfirmTimer = () => {
    if (disconnectConfirmTimerRef.current) {
      clearTimeout(disconnectConfirmTimerRef.current);
      disconnectConfirmTimerRef.current = null;
    }
  };
  /**
   * 延迟确认 socket 是否真的掉线,避免和自动重连抖动互相打架。
   */
  const confirmSocketUnavailableWithDelay = () => {
    clearDisconnectConfirmTimer();
    disconnectConfirmTimerRef.current = setTimeout(() => {
      setIsSocketUnavailable(!wsManager.isConnectedStatus());
    }, FallbackPollingConfig.DISCONNECT_CONFIRM_DELAY);
  };
  /**
   * 同步活跃订单快照,保证轮询拿到的数据能驱动页面关键状态。
   */
  const syncActiveOrderSnapshot = (activeOrder: any) => {
    const orderSnapshot = normalizeActiveOrderSnapshot(activeOrder, deps.orderId);
    return applyOrderSnapshot(orderSnapshot, {
      orderId: deps.orderId,
      tripStatusRef: deps.tripStatusRef,
      setOrderId: deps.setOrderId,
      setTripStatus: deps.setTripStatus,
      setStartSite: deps.setStartSite,
      setEndSite: deps.setEndSite,
      setTrunkStatus: deps.setTrunkStatus,
    });
  };
  /**
   * 轮询车辆信息,掉线期间补齐车牌和车辆信息。
   */
  const pollVehicleInfo = (targetOrderId: string) => {
    const now = Date.now();
    const shouldSkipVehiclePolling = now - lastVehiclePollAtRef.current < FallbackPollingConfig.VEHICLE_POLL_INTERVAL;
    if (!targetOrderId || shouldSkipVehiclePolling) {
      return;
    }
    lastVehiclePollAtRef.current = now;
    queryVehicleByOrderId({ orderId: targetOrderId })
      .then((res: any) => {
        const latestOrderId = String(Taro.getStorageSync('orderId') || deps.orderId || '');
        if (!latestOrderId || latestOrderId !== String(targetOrderId)) {
          return;
        }
        deps.setVehicleInfo(res);
      })
      .catch(() => { });
  };
  /**
   * 轮询订单状态,并在需要时触发车辆信息补偿同步。
   */
  const pollOrderStatus = () => {
    if (statusPollInFlightRef.current) {
      return;
    }
    statusPollInFlightRef.current = true;
    getOrderActive()
      .then((activeOrder: any) => {
        if (!activeOrder) {
          return;
        }
        const orderApplyResult = syncActiveOrderSnapshot(activeOrder);
        const effectiveOrderId = String(orderApplyResult?.nextOrderId || activeOrder?.id || deps.orderId || '');
        pollVehicleInfo(effectiveOrderId);
      })
      .catch(() => { })
      .finally(() => {
        statusPollInFlightRef.current = false;
      });
  };
  /**
   * 轮询订单路线,掉线期间继续刷新车辆位置和轨迹。
   */
  const pollOrderRoute = () => {
    const currentTripStatus = Number(deps.tripStatusRef.current);
    const targetOrderId = String(Taro.getStorageSync('orderId') || deps.orderId || '');
    if (!targetOrderId || !FALLBACK_ROUTE_STATUSES.includes(currentTripStatus)) {
      return;
    }
    deps.routeRequestOrderRouteApi(targetOrderId, currentTripStatus)
      .then((res: any) => {
        const routeSnapshot = normalizeApiOrderRouteSnapshot(res, currentTripStatus);
        applyRouteSnapshot(routeSnapshot, {
          tripStatusRef: deps.tripStatusRef,
          setRoutePoints: deps.setRoutePoints,
          setPolyline: deps.setPolyline,
          setVehicleDirection: deps.setVehicleDirection,
          setVehicleMoveAlongPoints: deps.setVehicleMoveAlongPoints,
          setRouteDistance: deps.setRouteDistance,
          setRouteDuration: deps.setRouteDuration,
          updateVehicleLocationThrottled: deps.updateVehicleLocationThrottled,
          erasePassedRouteSegments: deps.erasePassedRouteSegments,
          applyRoutePoints: deps.applyRoutePoints,
          polylineKeyRef: deps.polylineKeyRef,
          lastPolylineUpdateAtRef: deps.lastPolylineUpdateAtRef,
          lastRoutePointRef: deps.lastRoutePointRef,
          lastRouteUpdateAtRef: deps.lastRouteUpdateAtRef,
          lastVehiclePointRef: deps.lastVehiclePointRef,
          currentFullRouteRef: deps.currentFullRouteRef,
          lastErasedIndexRef: deps.lastErasedIndexRef,
          orderRouteInitializedRef: deps.orderRouteInitializedRef,
        });
      })
      .catch(() => { });
  };
  useDidShow(() => {
    setIsPageVisible(true);
    setIsSocketUnavailable(!wsManager.isConnectedStatus());
  });
  useDidHide(() => {
    setIsPageVisible(false);
  });
  /**
   * 监听页面 socket 连接态,延迟确认掉线,避免与自动重连过程互相干扰。
   */
  useEffect(() => {
    /**
     * socket 恢复连接时关闭兜底状态。
     */
    const handleSocketOpen = () => {
      clearDisconnectConfirmTimer();
      setIsSocketUnavailable(false);
    };
    /**
     * socket 关闭时延迟确认掉线。
     */
    const handleSocketClose = () => {
      confirmSocketUnavailableWithDelay();
    };
    /**
     * socket 异常时延迟确认掉线。
     */
    const handleSocketError = () => {
      confirmSocketUnavailableWithDelay();
    };
    wsManager.on('open', handleSocketOpen);
    wsManager.on('close', handleSocketClose);
    wsManager.on('error', handleSocketError);
    return () => {
      clearDisconnectConfirmTimer();
      wsManager.off('open', handleSocketOpen);
      wsManager.off('close', handleSocketClose);
      wsManager.off('error', handleSocketError);
    };
  }, []);
  /**
   * 仅在页面可见且 socket 不可用时启动兜底轮询,socket 恢复后自动停止。
   */
  useEffect(() => {
    const targetOrderId = String(Taro.getStorageSync('orderId') || deps.orderId || '');
    const shouldStartFallbackPolling = (
      isPageVisible
      && isSocketUnavailable
      && !!targetOrderId
      && FALLBACK_ACTIVE_STATUSES.includes(deps.tripStatus)
    );
    if (!shouldStartFallbackPolling) {
      return;
    }
    pollOrderStatus();
    pollOrderRoute();
    const statusTimer = setInterval(() => {
      pollOrderStatus();
    }, FallbackPollingConfig.STATUS_POLL_INTERVAL);
    const routeTimer = setInterval(() => {
      pollOrderRoute();
    }, FallbackPollingConfig.ROUTE_POLL_INTERVAL);
    return () => {
      clearInterval(statusTimer);
      clearInterval(routeTimer);
    };
  }, [deps.orderId, deps.tripStatus, isPageVisible, isSocketUnavailable]);
  return {
    isSocketUnavailable
  };
}
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -2,8 +2,13 @@
import Taro from '@tarojs/taro';
import wsManager from '../../../../utils/websocket';
import { queryVehicleByOrderId } from '../../../api/begin-route';
import { clearOrderTripStorage, safeParse } from '../../../../utils/Robotaxi-public-method';
import { normalizeDistanceKm, normalizeDurationSeconds } from '../../../../robotaxi/utils/format';
import { clearOrderTripStorage } from '../../../../utils/Robotaxi-public-method';
import {
    applyOrderSnapshot,
    applyRouteSnapshot,
    normalizeSocketOrderStatusSnapshot,
    normalizeSocketRouteSnapshot
} from './runtime-sync';
type UseWebSocketEventsDeps = {
    tripStatusRef: any;
@@ -62,25 +67,6 @@
    deps.orderRouteInitializedRef.current = false;
};
const syncTripStatus = (payload: any, deps: UseWebSocketEventsDeps) => {
    const mappedTripStatus = payload?.tripStatus;
    if (typeof mappedTripStatus !== 'number') {
        return null;
    }
    const currentTripStatus = Number(deps.tripStatusRef.current);
    // 行程进行关键态不允许被回退,避免 4/6 被延迟状态覆盖导致路线链路重置
    if ([4, 6].includes(currentTripStatus) && mappedTripStatus < currentTripStatus) {
        return currentTripStatus;
    }
    if (currentTripStatus === mappedTripStatus) {
        return mappedTripStatus;
    }
    deps.setTripStatus(mappedTripStatus);
    deps.tripStatusRef.current = mappedTripStatus;
    Taro.setStorageSync('tripStatus', mappedTripStatus);
    return mappedTripStatus;
};
const syncVehicleInfo = (
    effectiveOrderId: string,
    deps: UseWebSocketEventsDeps,
@@ -107,8 +93,8 @@
        .catch(() => { });
};
const handleCancelPopupPush = (payload: any, changeStatusOld: number, deps: UseWebSocketEventsDeps) => {
    if (!payload?.showCancelPopup) return;
const handleCancelPopupPush = (showCancelPopup: boolean, changeStatusOld: number, deps: UseWebSocketEventsDeps) => {
    if (!showCancelPopup) return;
    if (changeStatusOld === 4 || changeStatusOld === 5) {
        deps.setOrderId('');
        deps.setVehicleInfo({});
@@ -148,159 +134,30 @@
        return;
    }
    const mappedTripStatus = syncTripStatus(payload, deps);
    const effectiveOrderId = resolveOrderId(payload, currentOrderId);
    syncVehicleInfo(effectiveOrderId, deps, lastFetchedVehicleOrderIdRef);
    handleStatusSideEffects(mappedTripStatus, changeStatusOld, deps);
    handleCancelPopupPush(payload, changeStatusOld, deps);
    const orderSnapshot = normalizeSocketOrderStatusSnapshot(payload, currentOrderId);
    const { nextOrderId, nextTripStatus } = applyOrderSnapshot(orderSnapshot, deps, {
        protectCriticalTripStatus: true
    });
    syncVehicleInfo(nextOrderId, deps, lastFetchedVehicleOrderIdRef);
    handleStatusSideEffects(nextTripStatus, changeStatusOld, deps);
    handleCancelPopupPush(orderSnapshot.showCancelPopup === true, changeStatusOld, deps);
    if (payload?.showPassengerNotice) {
    if (orderSnapshot.showPassengerNotice) {
        deps.checkAndShowPassengerNotice();
    }
};
// 统一解析方向值
const resolveDirectionValue = (payload: any) => {
    const rawDirection = payload?.content?.direction;
    const direction = Number(rawDirection);
    if (!Number.isFinite(direction)) {
        return null;
    }
    return ((direction % 360) + 360) % 360;
};
const updateRouteSummary = (payload: any, deps: UseWebSocketEventsDeps) => {
    const normalizedDirection = resolveDirectionValue(payload);
    if (typeof normalizedDirection === 'number') {
        deps.setVehicleDirection(normalizedDirection);
    }
    const route = safeParse(payload?.content?.route, {});
    const distanceKm = normalizeDistanceKm(Number(route?.distance));
    if (distanceKm !== null) {
        deps.setRouteDistance(distanceKm);
    }
    const durationSeconds = normalizeDurationSeconds(Number(route?.duration));
    if (durationSeconds !== null) {
        deps.setRouteDuration(durationSeconds);
    }
};
const resolveVehiclePoint = (points: any[], servedPoints: any[] = []) => {
    const servedLastPoint = Array.isArray(servedPoints) && servedPoints.length > 0
        ? servedPoints[servedPoints.length - 1]
        : null;
    if (servedLastPoint && typeof servedLastPoint.latitude === 'number' && typeof servedLastPoint.longitude === 'number') {
        return servedLastPoint;
    }
    const firstPoint = points?.[0];
    if (firstPoint && typeof firstPoint.latitude === 'number' && typeof firstPoint.longitude === 'number') {
        return firstPoint;
    }
    return null;
};
const resolveVehiclePointForErase = (points: any[], servedPoints: any[] = []) => {
    const firstPoint = points?.[0];
    if (firstPoint && typeof firstPoint.latitude === 'number' && typeof firstPoint.longitude === 'number') {
        return firstPoint;
    }
    return resolveVehiclePoint(points, servedPoints);
};
const syncVehiclePointWithRoute = (points: any[], deps: UseWebSocketEventsDeps, servedPoints: any[] = []) => {
    const targetPoint = resolveVehiclePoint(points, servedPoints);
    if (!targetPoint) {
        return;
    }
    const currentTripStatus = Number(deps.tripStatusRef.current);
    if (currentTripStatus === 7) {
        const lastPoint = deps.lastVehiclePointRef.current;
        const latDiff = Math.abs(Number(lastPoint?.latitude || 0) - targetPoint.latitude);
        const lngDiff = Math.abs(Number(lastPoint?.longitude || 0) - targetPoint.longitude);
        const almostSamePoint = latDiff < 0.000003 && lngDiff < 0.000003;
        if (almostSamePoint) {
            return;
        }
    }
    deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true);
};
const handleEraseRouteStatus = (points: any[], servedPoints: any[], deps: UseWebSocketEventsDeps) => {
    const targetPoint = resolveVehiclePointForErase(points, servedPoints);
    if (targetPoint) {
        deps.updateVehicleLocationThrottled(targetPoint.latitude, targetPoint.longitude, true);
    }
    if (deps.orderRouteInitializedRef.current && deps.currentFullRouteRef.current.length > 0) {
        deps.erasePassedRouteSegments(points);
    } else {
        deps.orderRouteInitializedRef.current = true;
        deps.currentFullRouteRef.current = [...points];
        deps.lastErasedIndexRef.current = 0;
        Taro.nextTick(() => {
            deps.applyRoutePoints(points);
        });
    }
};
const handleNormalRouteStatus = (points: any[], deps: UseWebSocketEventsDeps, servedPoints: any[] = []) => {
    Taro.nextTick(() => {
        const updated = deps.applyRoutePoints(points);
        if (!updated) return;
        if (![4, 5, 6, 7].includes(deps.tripStatusRef.current)) return;
        syncVehiclePointWithRoute(points, deps, servedPoints);
    });
};
const handleForceFullRouteRedraw = (points: any[], deps: UseWebSocketEventsDeps, servedPoints: any[] = []) => {
    deps.setRoutePoints([]);
    deps.setPolyline([]);
    deps.orderRouteInitializedRef.current = false;
    deps.currentFullRouteRef.current = [];
    deps.lastErasedIndexRef.current = 0;
    deps.polylineKeyRef.current = '';
    deps.lastPolylineUpdateAtRef.current = 0;
    deps.lastRoutePointRef.current = null;
    deps.lastRouteUpdateAtRef.current = 0;
    Taro.nextTick(() => {
        deps.orderRouteInitializedRef.current = points.length > 0;
        deps.currentFullRouteRef.current = [...points];
        deps.lastErasedIndexRef.current = 0;
        deps.applyRoutePoints(points);
        if (points.length > 0) {
            syncVehiclePointWithRoute(points, deps, servedPoints);
        }
    });
};
const handleOrderRoutePayload = (payload: any, deps: UseWebSocketEventsDeps) => {
    const points = payload?.points || [];
    const servedPoints = Array.isArray(payload?.servedPoints) ? payload.servedPoints : [];
    const forceFullRedraw = payload?.forceFullRedraw === true;
    const currentTripStatus = Number(deps.tripStatusRef.current);
    updateRouteSummary(payload, deps);
    // 行程结束态仅更新概要信息,不再消费路线点,避免视口反复重算造成跳动
    if ([8, 9].includes(currentTripStatus)) {
        return;
    }
    if ([4, 6].includes(currentTripStatus)) {
        deps.setVehicleMoveAlongPoints(servedPoints);
    }
    if (forceFullRedraw) {
        handleForceFullRouteRedraw(points, deps, servedPoints);
        return;
    }
    const isEraseStatus = ![7, 8, 9].includes(currentTripStatus);
    if (isEraseStatus) {
        handleEraseRouteStatus(points, servedPoints, deps);
        return;
    }
    handleNormalRouteStatus(points, deps, servedPoints);
    const routeSnapshot = normalizeSocketRouteSnapshot(payload);
    applyRouteSnapshot(routeSnapshot, deps);
};
export default function useWebSocketEvents(deps: UseWebSocketEventsDeps) {
    // 持有最新依赖,避免渲染过程反复解绑/重绑监听
    const depsRef = useRef(deps);
    /**
     * 同步最新 deps,避免 socket 监听因为渲染而反复解绑重绑。
     */
    useEffect(() => {
        depsRef.current = deps;
    }, [deps]);
@@ -325,6 +182,9 @@
    const lastFetchedVehicleOrderIdRef = useRef<string | null>(null);
    // 仅初始化一次事件处理器与解绑逻辑
    /**
     * 初始化 socket 业务事件处理器,并在卸载时统一解绑。
     */
    useEffect(() => {
        authSuccessHandlerRef.current = () => {
            if (!messageBound.current && messageHandlerRef.current) {
src/robotaxi/pages/begin-route/index.tsx
@@ -28,6 +28,8 @@
import useTripRuntime from './hooks/use-trip-runtime';
// 订单动作编排
import useOrderActions from './hooks/use-order-actions';
// socket 掉线兜底轮询编排
import useSocketFallbackPolling from './hooks/use-socket-fallback-polling';
// 视图委托渲染编排
import useViewDelegates from './hooks/use-view-delegates';
@@ -37,11 +39,17 @@
import suggest from '../../../robotaxi/assets/suggest.png'
import originIcon from '../../../robotaxi/assets/origin-icon.png'
import { _ROBOTAXI_WS_HOST } from '../../../constants/api';
import { queryOrderRoute, queryOrderTrajectory } from '../../api/begin-route';
export default function BeginRoute() {
  const WAITING_TIMER_START_KEY = 'waitingTimerStartAt'; // 等待计时开始时间
  const WAITING_TIMER_ELAPSED_KEY = 'waitingTimerElapsed'; // 等待计时已过秒数
  const REQUOTE_EXPIRE_SECONDS = 10 * 60; // 10分钟
  const requestOrderRouteApi = (targetOrderId: string, status: number) => { // 掉线兜底专用路线查询,直接返回接口结果供统一落地层消费
    const routeType = status <= 5 ? 1 : 2;
    const requestRoute = [8, 9].includes(status) ? queryOrderTrajectory : queryOrderRoute;
    return requestRoute({ orderId: targetOrderId, routeType });
  };
  const {
    showCustomerService,
@@ -386,6 +394,35 @@
    applyRoutePoints: routeApplyRoutePoints,
    updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled
  });
  useSocketFallbackPolling({
    tripStatus,
    tripStatusRef,
    orderId,
    setTripStatus,
    setOrderId,
    setVehicleInfo,
    setStartSite,
    setEndSite,
    setTrunkStatus,
    routeRequestOrderRouteApi: requestOrderRouteApi,
    setRoutePoints,
    setPolyline,
    setVehicleDirection,
    setVehicleMoveAlongPoints,
    setRouteDistance,
    setRouteDuration,
    erasePassedRouteSegments: routeErasePassedRouteSegments,
    applyRoutePoints: routeApplyRoutePoints,
    updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled,
    polylineKeyRef,
    lastPolylineUpdateAtRef,
    lastRoutePointRef,
    lastRouteUpdateAtRef,
    lastVehiclePointRef,
    currentFullRouteRef,
    lastErasedIndexRef,
    orderRouteInitializedRef
  });
  const orderActions = useOrderActions({
    tripStatus,