hejianhao
2026-04-25 ed87d93aa10f3e5bcc6dc0c5ab9a8e8cd00b3203
fix(robotaxi): 修复蓝牙并发鉴权问题并适配多路线接口

- 修复蓝牙鉴权并发触发问题,使用引用缓存状态和任务锁
- 适配后端多路线查询接口,统一路线数据格式处理
- 修复路线过期校验逻辑,补充快照ID和指纹参数
- 优化首页营销弹窗显示逻辑,等待登录完成后再判断
- 修复行程状态描述组件的底部间距计算逻辑
8个文件已修改
238 ■■■■■ 已修改文件
src/pages/robotaxi/home/hooks/use-home-bootstrap.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.tsx 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-bluetooth-car-control.ts 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/hooks/use-home-bootstrap.ts
@@ -83,7 +83,7 @@
  const syncAuthAndOrder = async () => {
    const otp = await deps.checkOtp(false);
    deps.setIsLogin(!otp);
    if (!otp) return;
    if (!otp) return false;
    wsManager.connect(deps.wsHost);
    const agreement = await deps.getAgreement();
    if (agreement === false) {
@@ -100,7 +100,7 @@
      Taro.removeStorageSync('endSite');
      deps.setTripStatus(0);
      deps.setOrderData({});
      return;
      return true;
    }
    deps.setOrderData(activeOrder || {});
    const mappedStatus = toHomeTripStatus(mapOrderStatusToTripStatus(activeOrder.status || 0));
@@ -127,6 +127,7 @@
        time: activeOrder.endParkingTimeLimit || 0,
      });
    }
    return true;
  };
  /**
@@ -218,7 +219,7 @@
      setIsAdsorbed: deps.setIsAdsorbed
    });
    deps.setTripStatus(toHomeTripStatus(Taro.getStorageSync('tripStatus')));
    syncAuthAndOrder();
    const authSyncTask = syncAuthAndOrder();
    Taro.getSetting({
      success: (res) => {
        const auth = res.authSetting['scope.userLocation'];
@@ -226,6 +227,7 @@
        processLocationSetting(auth, startSite);
      }
    });
    return authSyncTask;
  };
  return {
src/pages/robotaxi/home/index.tsx
@@ -405,11 +405,21 @@
  }, [selectedCity]);
  useDidShow(() => {
    bootstrap.onDidShow();
    const authSyncTask = bootstrap.onDidShow();
    refreshQuickCapsule();
    firstEnter()
      .then((res: any) => {
        setShowMarketing(resolveMarketingVisible(res));
    authSyncTask
      .then((hasLogin) => {
        if (!hasLogin) {
          setShowMarketing(false);
          return;
        }
        return firstEnter()
          .then((res: any) => {
            setShowMarketing(resolveMarketingVisible(res));
          })
          .catch(() => {
            setShowMarketing(false);
          });
      })
      .catch(() => {
        setShowMarketing(false);
@@ -577,7 +587,7 @@
            <Image className="select-city-icon" src={selectCityIcon} />
          </View>
          {weatherData && (
            <View className="weather-info" onClick={() => setShowMarketing(true)}>
            <View className="weather-info">
              <Image className="weather-icon" src={getWeatherIconByDesc(weatherData.weather)} />
              <Text>{weatherData.temperature}°C</Text>
            </View>
src/robotaxi/api/begin-route.ts
@@ -4,7 +4,7 @@
// 导航-获取路线和费用
export const queryRouteAndFare = async (payload) => {
    return request({
        url: _ROBOTAXI_HOST + '/itinerary/base/v1/mini-program/navigation/route-and-fare',
        url: _ROBOTAXI_HOST + '/itinerary/base/v1/mini-program/navigation/multi-route-and-fare',
        method: 'POST',
        payload
    }, true);
@@ -29,10 +29,11 @@
}
// 导航-下单前校验路况是否过期
export const routeStaleCheck = async () => {
export const routeStaleCheck = async (payload?) => {
    return request({
        url: _ROBOTAXI_HOST + '/itinerary/base/v1/mini-program/navigation/route-stale-check',
        method: 'POST'
        method: 'POST',
        payload
    }, true);
}
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
@@ -120,8 +120,7 @@
                    <View className='calling-car-content-title'>{titleMap[tripStatus]}</View>
                )}
                {descMap[tripStatus] && (
                    // <View style={{ paddingBottom: tripStatus === TripStatus.CallingCar ? '20px' : '0px' }} className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View>
                    <View className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View>
                    <View style={{ paddingBottom: tripStatus === TripStatus.CallingCar && !queueStatus ? '20px' : '0px' }} className={descMap[tripStatus]?.className}>{descMap[tripStatus]?.text}</View>
                )}
                {tripStatus === TripStatus.CallingCar && queueStatus && (
                    <View className='calling-car-content-desc'>
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts
@@ -1,4 +1,4 @@
import Taro, { useDidShow } from '@tarojs/taro';
import Taro from '@tarojs/taro';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { TripStatus } from '../constants/trip-status';
@@ -35,6 +35,11 @@
  const adapterStateHandlerRef = useRef<((res: any) => void) | null>(null); // 蓝牙适配器状态监听函数引用
  const openSettingPollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null); // 去开启后的短轮询定时器
  const openSettingPollingCountRef = useRef(0); // 去开启后的短轮询计数
  const startBluetoothAuthRef = useRef(startBluetoothAuth); // 蓝牙鉴权动作引用,避免函数引用变化导致状态 effect 反复触发
  useEffect(() => {
    startBluetoothAuthRef.current = startBluetoothAuth;
  }, [startBluetoothAuth]);
  /**
   * 停止“去开启”后的短轮询。
@@ -118,9 +123,9 @@
    setStatusReady(true);
    stopOpenSettingPolling();
    if (nextStatus === 'authorized') {
      startBluetoothAuth();
      startBluetoothAuthRef.current?.();
    }
  }, [startBluetoothAuth, stopOpenSettingPolling, openBluetoothAdapterSafe]);
  }, [stopOpenSettingPolling, openBluetoothAdapterSafe]);
  /**
   * 开启“去开启”后的短轮询兜底,处理未触发页面 show 的场景。
@@ -183,13 +188,6 @@
    setShowAuthBluetoothPopup(false);
    setShowSuccessAuthPopup(false);
  }, [tripStatus, refreshBluetoothState, startBluetoothStateWatch, stopBluetoothStateWatch, stopOpenSettingPolling]);
  // 页面回前台后同步蓝牙状态,避免设置页返回后状态滞后
  useDidShow(() => {
    if ([TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus)) {
      refreshBluetoothState();
    }
  });
  // 跳转系统蓝牙设置,并在返回后触发一次状态刷新
  const handleOpenSystemBluetoothSetting = useCallback(async () => {
src/robotaxi/pages/begin-route/hooks/use-bluetooth-car-control.ts
@@ -67,6 +67,7 @@
export default function useBluetoothCarControl({ orderId, tripStatus }: Deps) {
  const managerRef = useRef<any>(null); // 蓝牙管理器实例引用
  const lastStartKeyRef = useRef(''); // 最近一次启动鉴权的参数签名
  const startAuthTaskRef = useRef<Promise<void> | null>(null); // 当前进行中的鉴权任务,防止并发重复请求
  const candidateMacsRef = useRef<string[]>([]); // 蓝牙地址候选列表(主 + 从)
  const currentMacIndexRef = useRef(0); // 当前尝试的 MAC 索引
  const certificateRef = useRef(''); // 当前会话证书缓存
@@ -75,6 +76,8 @@
  const [authSuccess, setAuthSuccess] = useState(false); // RtAuth 鉴权是否成功
  const [isAuthorizing, setIsAuthorizing] = useState(false); // 鉴权流程进行中
  const [authError, setAuthError] = useState<string>(''); // 鉴权流程错误信息
  const authSuccessRef = useRef(false); // 鉴权成功状态引用,避免并发期间闭包值滞后
  const isAuthorizingRef = useRef(false); // 鉴权中状态引用,避免并发期间闭包值滞后
  const showUnlockToast = useCallback((title: string) => {
    Taro.showToast({
      title,
@@ -117,6 +120,8 @@
        console.log('[蓝牙] 连接状态变更:', status);
        setConnectStatus(status);
        if (status === 3) {
          authSuccessRef.current = true;
          isAuthorizingRef.current = false;
          setAuthSuccess(true);
          setAuthError('');
          setIsAuthorizing(false);
@@ -127,20 +132,24 @@
        if (isRetryableConnectError(error) && tryFallbackMacConnection()) {
          return;
        }
        isAuthorizingRef.current = false;
        setAuthError(error?.msg || '蓝牙连接失败');
        setIsAuthorizing(false);
      },
      onRtAuthResult: (result: any) => {
        console.log('[蓝牙] 鉴权结果:', result);
        if (result?.success) {
          authSuccessRef.current = true;
          setAuthSuccess(true);
          setAuthError('');
          setConnectStatus(3);
        } else {
          authSuccessRef.current = false;
          setAuthSuccess(false);
          setAuthError(RETRY_UNLOCK_MESSAGE);
          showUnlockToast(RETRY_UNLOCK_MESSAGE);
        }
        isAuthorizingRef.current = false;
        setIsAuthorizing(false);
      },
      onRtNotification: (result: any) => {
@@ -172,6 +181,9 @@
    if (managerRef.current) {
      managerRef.current.closeConnection();
    }
    authSuccessRef.current = false;
    isAuthorizingRef.current = false;
    startAuthTaskRef.current = null;
    setConnectStatus(0);
    setAuthSuccess(false);
    setIsAuthorizing(false);
@@ -181,50 +193,66 @@
   * 触发蓝牙连接与 RtAuth 鉴权。
   */
  const startAuth = useCallback(async () => {
    console.log('[蓝牙] 开始鉴权入参:', { tripStatus, orderId, isAuthorizing, authSuccess });
    console.log('[蓝牙] 开始鉴权入参:', {
      tripStatus,
      orderId,
      isAuthorizing: isAuthorizingRef.current,
      authSuccess: authSuccessRef.current
    });
    if (![TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus)) return;
    if (!orderId) return;
    if (startAuthTaskRef.current) return startAuthTaskRef.current;
    const startKey = `${orderId}`;
    if (isAuthorizing || (authSuccess && lastStartKeyRef.current === startKey)) return;
    if (isAuthorizingRef.current || (authSuccessRef.current && lastStartKeyRef.current === startKey)) return;
    lastStartKeyRef.current = startKey;
    isAuthorizingRef.current = true;
    setIsAuthorizing(true);
    setAuthError('');
    try {
      const tokenRes = await getBluetoothUnlockToken(orderId);
      const tokenPayload: BluetoothTokenPayload = typeof tokenRes === 'object' && tokenRes !== null ? tokenRes as BluetoothTokenPayload : {};
      const certificate = typeof tokenPayload.token === 'string' ? tokenPayload.token : String(tokenPayload.token || '');
      const candidateMacs = buildMacCandidates(tokenPayload.macAddress, tokenPayload.slaveMacList);
      console.log('[蓝牙] 获取凭证与地址成功:', { tokenLength: certificate?.length || 0, macCount: candidateMacs.length });
      if (!certificate) {
        setAuthError('蓝牙鉴权凭证为空');
    const runTask = async () => {
      try {
        const tokenRes = await getBluetoothUnlockToken(orderId);
        const tokenPayload: BluetoothTokenPayload = typeof tokenRes === 'object' && tokenRes !== null ? tokenRes as BluetoothTokenPayload : {};
        const certificate = typeof tokenPayload.token === 'string' ? tokenPayload.token : String(tokenPayload.token || '');
        const candidateMacs = buildMacCandidates(tokenPayload.macAddress, tokenPayload.slaveMacList);
        console.log('[蓝牙] 获取凭证与地址成功:', { tokenLength: certificate?.length || 0, macCount: candidateMacs.length });
        if (!certificate) {
          isAuthorizingRef.current = false;
          setAuthError('蓝牙鉴权凭证为空');
          setIsAuthorizing(false);
          return;
        }
        if (candidateMacs.length === 0) {
          isAuthorizingRef.current = false;
          setAuthError('蓝牙地址为空');
          setIsAuthorizing(false);
          return;
        }
        certificateRef.current = certificate;
        candidateMacsRef.current = candidateMacs;
        currentMacIndexRef.current = 0;
        const manager = ensureManager();
        console.log('[蓝牙] 更新车辆配置(主地址):', { macAddress: candidateMacs[0], deviceModel: deviceModelRef.current });
        manager.updateCarConfig({
          macAddress: candidateMacs[0],
          certificate,
          deviceModel: deviceModelRef.current
        });
        console.log('[蓝牙] 准备打开蓝牙适配器');
        manager.openBluetoothAdapter();
      } catch (error: any) {
        console.log('[蓝牙] 鉴权流程异常:', error);
        isAuthorizingRef.current = false;
        setAuthError(error?.msg || '获取蓝牙鉴权凭证失败');
        setIsAuthorizing(false);
        return;
      } finally {
        startAuthTaskRef.current = null;
      }
      if (candidateMacs.length === 0) {
        setAuthError('蓝牙地址为空');
        setIsAuthorizing(false);
        return;
      }
      certificateRef.current = certificate;
      candidateMacsRef.current = candidateMacs;
      currentMacIndexRef.current = 0;
      const manager = ensureManager();
      console.log('[蓝牙] 更新车辆配置(主地址):', { macAddress: candidateMacs[0], deviceModel: deviceModelRef.current });
      manager.updateCarConfig({
        macAddress: candidateMacs[0],
        certificate,
        deviceModel: deviceModelRef.current
      });
      console.log('[蓝牙] 准备打开蓝牙适配器');
      manager.openBluetoothAdapter();
    } catch (error: any) {
      console.log('[蓝牙] 鉴权流程异常:', error);
      setAuthError(error?.msg || '获取蓝牙鉴权凭证失败');
      setIsAuthorizing(false);
    }
  }, [tripStatus, orderId, isAuthorizing, authSuccess, ensureManager]);
    };
    const task = runTask();
    startAuthTaskRef.current = task;
    return task;
  }, [tripStatus, orderId, ensureManager]);
  /**
   * 重试蓝牙鉴权流程。
@@ -235,6 +263,7 @@
      startAuth();
      return;
    }
    isAuthorizingRef.current = true;
    setIsAuthorizing(true);
    setAuthError('');
    managerRef.current.retryConnection();
@@ -248,6 +277,9 @@
      candidateMacsRef.current = [];
      currentMacIndexRef.current = 0;
      certificateRef.current = '';
      startAuthTaskRef.current = null;
      authSuccessRef.current = false;
      isAuthorizingRef.current = false;
      setAuthError('');
    }
  }, [tripStatus, closeConnection]);
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts
@@ -167,6 +167,16 @@
  };
  /**
   * 获取当前选中的路线。
   */
  const getSelectedRoute = () => {
    if (!Array.isArray(deps.routeOptions) || typeof deps.selectedRouteIndex !== 'number') {
      return null;
    }
    return deps.routeOptions[deps.selectedRouteIndex] || null;
  };
  /**
   * 读取下单起点。
   */
  const getStartSiteForCall = () => {
@@ -182,9 +192,31 @@
  /**
   * 下单前校验路线是否过期,命中过期时刷新路线并终止本次下单。
   */
  const checkRouteStaleBeforeOrder = async (startSite: any) => {
  const checkRouteStaleBeforeOrder = async (startSite: any, selectedRoute: any) => {
    const snapshotId = selectedRoute?.snapshotId; // 路线快照 ID
    const pathFingerprint = selectedRoute?.pathFingerprint ?? selectedRoute?.route?.pathFingerprint; // 路线指纹
    const clientSnapshotMillis = Number(selectedRoute?.generatedAtMillis); // 客户端路线生成时间戳
    const clientFinalFare = Number(selectedRoute?.fareEstimate?.finalFare ?? deps.fareEstimate?.finalFare); // 客户端路线预估价
    const staleCheckPayload = {
      snapshotId,
      pathFingerprint,
      clientSnapshotMillis,
      clientFinalFare
    }; // 路况过期校验请求参数
    if (!snapshotId || !pathFingerprint || !Number.isFinite(clientSnapshotMillis) || !Number.isFinite(clientFinalFare)) {
      Taro.showToast({ title: '路线信息已更新,请稍后重试', icon: 'none' });
      deps.routeRequestRouteAndFare(
        Number(startSite?.latitude),
        Number(startSite?.longitude),
        Number(deps.endSite?.latitude),
        Number(deps.endSite?.longitude),
        true
      );
      stopCallSubmitting();
      return false;
    }
    try {
      const staleCheckRes: any = await routeStaleCheck();
      const staleCheckRes: any = await routeStaleCheck(staleCheckPayload);
      const needRefreshNotice = staleCheckRes?.needRefreshNotice === true; // 服务端是否要求刷新路线
      if (!needRefreshNotice) {
        return true;
@@ -231,9 +263,15 @@
    if (!routeReady) return;
    const s = getStartSiteForCall();
    if (!s) return;
    const selectedRoute = getSelectedRoute();
    if (!selectedRoute) {
      Taro.showToast({ title: '路线信息缺失,请重试', icon: 'none' });
      stopCallSubmitting();
      return;
    }
    const openCityId = await deps.checkDistanceAndCityReady(s);
    if (!openCityId) return;
    const routeStalePassed = await checkRouteStaleBeforeOrder(s);
    const routeStalePassed = await checkRouteStaleBeforeOrder(s, selectedRoute);
    if (!routeStalePassed) return;
    try {
      const nextOrderId = await submitOrder(s, openCityId);
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts
@@ -396,25 +396,41 @@
      setRouteAndFareError(false);
      const responseRoutes = Array.isArray(res?.routes) ? res.routes : []; // 新版接口路线集合
      const responseSnapshotId = res?.snapshotId; // 新版接口快照 ID
      const responseGeneratedAtMillis = res?.generatedAtMillis; // 新版接口报价生成时间戳
      const normalizeRouteOption = (item: any) => ({
        ...(item || {}),
        snapshotId: item?.snapshotId ?? responseSnapshotId,
        generatedAtMillis: item?.generatedAtMillis ?? responseGeneratedAtMillis
      }); // 统一补齐路线校验所需的快照信息
      let routeData = res;
      let options: any[] = [];
      // Handle both array and single object response for backward compatibility
      // 兼容新版对象结构(res.routes)与历史结构
      let allPointsArr: any[][] = [];
      if (Array.isArray(res) && res.length > 0) {
        options = res;
        setRouteOptions(res);
        routeData = res[0];
      if (responseRoutes.length > 0) {
        options = responseRoutes.map(normalizeRouteOption);
        setRouteOptions(options);
        routeData = options[0];
        setSelectedRouteIndex(0);
        allPointsArr = options.map(item => Array.isArray(item?.route?.polyline) ? item.route.polyline : []);
        setAllRoutePoints(allPointsArr);
      } else if (Array.isArray(res) && res.length > 0) {
        options = res.map(normalizeRouteOption);
        setRouteOptions(options);
        routeData = options[0];
        setSelectedRouteIndex(0);
        // 解析所有路线的点集
        allPointsArr = res.map(item => Array.isArray(item?.route?.polyline) ? item.route.polyline : []);
        allPointsArr = options.map(item => Array.isArray(item?.route?.polyline) ? item.route.polyline : []);
        setAllRoutePoints(allPointsArr);
      } else if (res) {
        options = [res];
        setRouteOptions([res]);
        routeData = res;
        options = [normalizeRouteOption(res)];
        setRouteOptions(options);
        routeData = options[0];
        setSelectedRouteIndex(0);
        const pts = Array.isArray(res?.route?.polyline) ? res.route.polyline : [];
        const pts = Array.isArray(routeData?.route?.polyline) ? routeData.route.polyline : [];
        allPointsArr = [pts];
        setAllRoutePoints(allPointsArr);
      } else {