卢春龙
2026-06-26 35b31094db5b0108c0e83d74d00cce2f36eee0db
Merge branch 'feature/liaof-bugfix' into 'feature_v2.0'

feat(temperature): 连环接驾改为后端字段判断

See merge request robotaxi1/xp-robotaxi-miniprogram!106
9个文件已修改
137 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/components/calling-car-modules/ac-storage.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.shared.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-request.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-status-socket-handler.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/ac-storage.ts
@@ -1,23 +1,26 @@
import Taro from '@tarojs/taro';
const AC_STORAGE_KEY_PREFIX = 'rt_control_';
const AC_STORAGE_KEY = 'rt_control';
export interface AcStorageData {
  acOpen: boolean;
  temperature: number;
}
function getStorageKey(orderId: string): string {
  return `${AC_STORAGE_KEY_PREFIX}${orderId}`;
interface AcStorageEntry {
  orderId: string;
  acOpen: boolean;
  temperature: number;
}
export function getAcStorage(orderId: string): AcStorageData | null {
  try {
    const raw = Taro.getStorageSync(getStorageKey(orderId));
    const raw = Taro.getStorageSync(AC_STORAGE_KEY);
    if (!raw) return null;
    const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
    const data: AcStorageEntry = typeof raw === 'string' ? JSON.parse(raw) : raw;
    if (String(data.orderId || '') !== orderId) return null;
    if (typeof data.acOpen === 'boolean' && typeof data.temperature === 'number') {
      return data;
      return { acOpen: data.acOpen, temperature: data.temperature };
    }
    return null;
  } catch {
@@ -27,16 +30,9 @@
export function saveAcStorage(orderId: string, data: AcStorageData): void {
  try {
    Taro.setStorageSync(getStorageKey(orderId), JSON.stringify(data));
    Taro.setStorageSync(AC_STORAGE_KEY, JSON.stringify({ orderId, acOpen: data.acOpen, temperature: data.temperature }));
  } catch {
    // 静默失败,不阻断交互
  }
}
export function removeAcStorage(orderId: string): void {
  try {
    Taro.removeStorageSync(getStorageKey(orderId));
  } catch {
    // 静默失败
  }
}
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx
@@ -13,7 +13,7 @@
import { getOrderId } from '../../../../utils/order';
import { TripStatus } from '../../../../constants/trip-status';
import { MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY } from '../../../../constants/modify-destination';
import { getAcStorage, saveAcStorage, removeAcStorage } from './ac-storage';
import { getAcStorage, saveAcStorage } from './ac-storage';
import useMinimumSpinLoading from './use-minimum-spin-loading';
const AC_SWITCH_OPEN_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-switch-open.png';
@@ -28,18 +28,15 @@
    tripStatus: TripStatus;
    orderId?: string;
    pageDidShowVersion?: number;
    /** 车是否还在执行上一单 */
    hasBeforeUserEndSite?: boolean;
    /** “上一单下车点”状态是否已由路线接口/Socket 确认 */
    beforeUserEndSiteResolved?: boolean;
    /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */
    hasPreviousOrder?: boolean;
};
export default function TemperatureControl({ tripStatus, orderId, pageDidShowVersion = 0, hasBeforeUserEndSite = false, beforeUserEndSiteResolved = false }: Props) {
export default function TemperatureControl({ tripStatus, orderId, pageDidShowVersion = 0, hasPreviousOrder = false }: Props) {
    const [tempStatus, setTempStatus] = useState<number | null>(null);
    const [acSwitchOn, setAcSwitchOn] = useState(false);
    const [acStatusLoaded, setAcStatusLoaded] = useState(false);
    const tempTimerRef = useRef<any>(null);
    const firstInitFallbackTimerRef = useRef<any>(null);
    const lastConfirmedTempRef = useRef<number | null>(null);
    const skipQueryOnThisCycleRef = useRef(false);
    const userActionSeqRef = useRef(0);
@@ -49,10 +46,6 @@
    const TEMP_MAX = 32.5;
    const TEMP_UPDATE_DEBOUNCE_MS = 500;
    const DEFAULT_FIRST_TEMP = 23.5;
    // 确定性版兜底超时:正常情况下进入接驾态会很快通过路线接口/Socket 确认“上一单下车点”状态(beforeUserEndSiteResolved 置真),
    // 届时立即下发默认温度,无固定延迟。此超时仅为路线数据异常迟迟未达(如接口失败)时的安全兜底,
    // 避免空调永久不初始化;正常 happy path 不会走到这里。
    const FIRST_INIT_RESOLVE_FALLBACK_MS = 3000;
    const isTogglingAc = acLoadingKey === 'ac';
    const canToggleSwitch = tripStatus === TripStatus.VehicleApproaching && !isTogglingAc;
@@ -69,15 +62,6 @@
    const increaseButtonIconSuffix = increaseButtonDisabled ? '-no' : '';
    const tempTextDisabledClassName = acSwitchOn ? '' : 'color-000-25';
    useEffect(() => {
        if (tripStatus === TripStatus.VehicleApproaching) {
            return;
        }
        const targetOrderId = getOrderId(orderId);
        if (targetOrderId) {
            removeAcStorage(targetOrderId);
        }
    }, [tripStatus, orderId]);
    useEffect(() => {
        const skipOnce = Number(Taro.getStorageSync(MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY)) === 1;
@@ -206,10 +190,10 @@
        if (tripStatus !== TripStatus.VehicleApproaching) {
            return;
        }
        // 连环派单场景:车辆仍在执行上一单(hasBeforeUserEndSite 为 true)时,
        // 当前用户的空调卡片尚未展示,不应初始化/下发温度,否则会把车内温度强制改回默认 23.5°C,
        // 覆盖上一单乘客的设置。待车辆完成上一单后该副作用会重新执行并正常初始化。
        if (hasBeforeUserEndSite) {
        // 连环派单场景:后端明确告知车辆仍在执行上一单(hasPreviousOrder 为 true)时,
        // 不应初始化/下发温度,否则会把车内温度强制改回默认 23.5°C,覆盖上一单乘客的设置。
        // 该字段与 tripStatus 原子下发、首次即正确,无竞态窗口。
        if (hasPreviousOrder) {
            return;
        }
        if (skipQueryOnThisCycleRef.current) {
@@ -223,36 +207,12 @@
        const storageData = getAcStorage(targetOrderId);
        if (storageData) {
            // 先不展示 storageData,等待校准(限定时间内接口返回 → 展示接口数据,超时 → 展示 storageData 并同步远程)
            handleCalibration(targetOrderId, storageData.acOpen, storageData.temperature);
            return;
        }
        // 首次初始化(无本地缓存):必须等到“上一单下车点”状态被确认后再决定是否下发默认温度。
        // 连环派单刚接单时,订单状态消息(推进到接驾中)通常早于订单线路消息(携带上一单下车点)到达,
        // 存在 hasBeforeUserEndSite 暂为 false 的竞态窗口,若此刻直接下发默认 23.5°C 会覆盖上一单乘客的温度设置。
        // 因此:已确认(resolved)→ 立即按 hasBeforeUserEndSite 据实初始化(普通单无固定延迟);
        //       未确认 → 仅挂兜底定时器,确认到达时本副作用会重跑并据实判断(命中上方 hasBeforeUserEndSite 守卫或下面的 resolved 分支)。
        if (beforeUserEndSiteResolved) {
            handleFirstTimeInit(targetOrderId);
            return;
        }
        firstInitFallbackTimerRef.current = setTimeout(() => {
            firstInitFallbackTimerRef.current = null;
            // 兜底触发前若已写入空调缓存(如用户手动操作/校准落库),不再强制下发默认温度。
            if (getAcStorage(targetOrderId)) {
                return;
            }
            handleFirstTimeInit(targetOrderId);
        }, FIRST_INIT_RESOLVE_FALLBACK_MS);
        return () => {
            if (firstInitFallbackTimerRef.current) {
                clearTimeout(firstInitFallbackTimerRef.current);
                firstInitFallbackTimerRef.current = null;
            }
        };
    }, [tripStatus, orderId, pageDidShowVersion, hasBeforeUserEndSite, beforeUserEndSiteResolved]);
        handleFirstTimeInit(targetOrderId);
    }, [tripStatus, orderId, pageDidShowVersion, hasPreviousOrder]);
    useEffect(() => {
        lastConfirmedTempRef.current = tempStatus;
@@ -378,8 +338,8 @@
        }
    };
    // 车还在上一单时不展示空调卡片
    if (tripStatus !== TripStatus.VehicleApproaching || hasBeforeUserEndSite) {
    // 连环单(车还在执行上一单)时不展示空调卡片
    if (tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) {
        return null;
    }
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -72,8 +72,8 @@
    orderId?: string;
    /** WebSocket 推送的上一单下车点坐标(有值表示车还在执行上一单) */
    hasBeforeUserEndSite?: boolean;
    /** “上一单下车点”状态是否已由路线接口/Socket 确认(用于规避连环派单接单瞬间温度被重置的竞态) */
    beforeUserEndSiteResolved?: boolean;
    /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */
    hasPreviousOrder?: boolean;
    orderTrunkStatus?: boolean;
    /** 当前页面内是否已经解锁过车门 */
    doorUnlockedInCurrentPage?: boolean;
@@ -123,7 +123,7 @@
    vehicleInfo,
    orderId,
    hasBeforeUserEndSite = false,
    beforeUserEndSiteResolved = false,
    hasPreviousOrder = false,
    orderTrunkStatus,
    doorUnlockedInCurrentPage = false,
    bluetoothStatusReady = false,
@@ -207,7 +207,7 @@
                <View className="car-card" style={{ marginBottom: tripStatus === TripStatus.TripFinished ? '70px' : '0px' }}>
                    <VehicleInfoCard vehicleInfo={vehicleInfo} />
                    <TemperatureControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasBeforeUserEndSite={hasBeforeUserEndSite} beforeUserEndSiteResolved={beforeUserEndSiteResolved} />
                    <TemperatureControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} />
                    <DoorTrunkControl
                        tripStatus={tripStatus}
src/robotaxi/pages/begin-route/hooks/use-route-manager.shared.ts
@@ -91,6 +91,8 @@
  setEndSite?: (site: any) => void;
  /** 设置上一单乘客下车点。 */
  setBeforeUserEndSite?: (site: { longitude: number; latitude: number } | null) => void;
  /** 设置后端推送的「是否仍在执行上一单」标识。 */
  setHasPreviousOrder?: (value: boolean) => void;
  /** 计算顶部安全区。 */
  resolveSafeTopInset: () => number;
  /** 计算底部视口内边距。 */
src/robotaxi/pages/begin-route/hooks/use-route-request.ts
@@ -543,6 +543,9 @@
          response?.endLatitude ?? response?.endlatitude
        );
        deps.setBeforeUserEndSite?.(beforeUserSite);
        const hasPreviousOrderValue = Boolean(response?.hasPreviousOrder);
        deps.setHasPreviousOrder?.(hasPreviousOrderValue);
        Taro.setStorageSync('hasPreviousOrder', { orderId: targetOrderId, value: hasPreviousOrderValue });
        const rawPoints = Array.isArray(response?.polyline) ? response.polyline : [];
        const points = rawPoints
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -47,8 +47,8 @@
  routeDuration: number | null;
  /** 是否存在上一单终点 */
  hasBeforeUserEndSite: boolean;
  /** “上一单下车点”状态是否已由路线接口/Socket 确认(用于规避连环派单接单瞬间温度被重置的竞态) */
  beforeUserEndSiteResolved: boolean;
  /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */
  hasPreviousOrder: boolean;
  /** 自定义气泡图标 */
  customCalloutIconSrc: string;
  /** 路线概览气泡背景图 */
@@ -523,7 +523,7 @@
          key={deps.tripStatus}
          orderId={deps.orderId}
          hasBeforeUserEndSite={deps.hasBeforeUserEndSite}
          beforeUserEndSiteResolved={deps.beforeUserEndSiteResolved}
          hasPreviousOrder={deps.hasPreviousOrder}
          tripStatus={deps.tripStatus}
          finishCountdownEnabled={deps.finishCountdownEnabled}
          vehicleInfo={deps.vehicleInfo}
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -70,6 +70,7 @@
        durationSeconds: number | null;
    }) => void;
    setBeforeUserEndSite: (site: { longitude: number; latitude: number } | null) => void;
    setHasPreviousOrder: (value: boolean) => void;
    isModifyDestinationReviewVisible?: boolean;
    hasLocalDestinationOverride?: boolean;
    isPickupRouteChangeVisible?: boolean;
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-status-socket-handler.ts
@@ -396,6 +396,12 @@
  if (!shouldKeepPickupExceptionPopup) {
    currentDeps.handlePickupException?.(normalizedPickupException);
  }
  const rawHasPreviousOrder = payload?.hasPreviousOrder ?? payload?.content?.hasPreviousOrder;
  if (rawHasPreviousOrder !== undefined) {
    const hasPreviousOrder = Boolean(rawHasPreviousOrder);
    currentDeps.setHasPreviousOrder(hasPreviousOrder);
    Taro.setStorageSync('hasPreviousOrder', { orderId: effectiveOrderId, value: hasPreviousOrder });
  }
  handleCancelPopupPush(payload, changeStatusOld, currentDeps, pendingCancelContext);
  if (payload?.showPassengerNotice) {
    currentDeps.checkAndShowPassengerNotice();
src/robotaxi/pages/begin-route/index.tsx
@@ -231,12 +231,14 @@
  const [tripStatus, setTripStatus] = useState<TripStatus>(toTripStatus(Taro.getStorageSync('tripStatus'))); // 当前行程状态
  const [elapsedSeconds, setElapsedSeconds] = useState(0); // 等待计时秒数
  const [beforeUserEndSite, setBeforeUserEndSite] = useState<BeforeUserEndSite | null>(null); // 上一单乘客下车点
  const [beforeUserEndSiteResolved, setBeforeUserEndSiteResolved] = useState(false); // 当前订单的“上一单下车点”状态是否已由路线接口/Socket 确认过(用于规避连环派单接单瞬间温度被重置的竞态)
  // 统一写入“上一单下车点”:在更新坐标的同时标记其已被确认,确保温度模块能在确认后再决定是否下发默认温度。
  const markBeforeUserEndSite = useCallback((site: BeforeUserEndSite | null) => {
    setBeforeUserEndSite(site);
    setBeforeUserEndSiteResolved(true);
  }, []);
  const [hasPreviousOrder, setHasPreviousOrder] = useState(() => {
    const cached = Taro.getStorageSync('hasPreviousOrder');
    const cachedOrderId = String(Taro.getStorageSync('orderId') || '');
    if (cached && cachedOrderId && String(cached.orderId || '') === cachedOrderId) {
      return Boolean(cached.value);
    }
    return false;
  }); // 后端推送:当前被指派车辆是否仍在执行上一单(连环单)
  const requoteTriggeredRef = useRef(false); // 重新报价触发标记,防止重复触发
  const [callSubmitting, setCallSubmitting] = useState(false); // 呼叫车辆提交中状态
  const [orderId, setOrderId] = useState<string>(''); // 订单ID
@@ -603,7 +605,8 @@
    endSite: effectiveEndSite,
    setStartSite,
    setEndSite,
    setBeforeUserEndSite: markBeforeUserEndSite,
    setBeforeUserEndSite,
    setHasPreviousOrder,
    updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled,
    vehicleMoveAlongPoints,
    setVehicleDirection,
@@ -1364,7 +1367,8 @@
    setRouteDuration,
    setModifyDestinationSocketStatus,
    setOrderRouteMetrics,
    setBeforeUserEndSite: markBeforeUserEndSite,
    setBeforeUserEndSite,
    setHasPreviousOrder,
    isModifyDestinationReviewVisible,
    hasLocalDestinationOverride,
    isPickupRouteChangeVisible,
@@ -1416,7 +1420,6 @@
   */
  useEffect(() => {
    setDoorUnlockedInCurrentPage(false);
    setBeforeUserEndSiteResolved(false); // 订单切换后需重新确认“上一单下车点”状态,避免沿用上个订单的判定造成温度误重置
  }, [orderId]);
  const { queryPaymentResult: queryOrderPaymentResult } = useOrderPaymentResultQuery({
@@ -1983,7 +1986,7 @@
    routeDistance,
    routeDuration,
    hasBeforeUserEndSite: hasValidBeforeUserEndSite,
    beforeUserEndSiteResolved,
    hasPreviousOrder,
    customCalloutIconSrc,
    routeSummaryBgSrc,
    showTrip1StyleEndpoint,