liaof
2026-07-15 f9b95ff6e2ddbbda4686656178ffb390ad909d7d
Merge branch 'feature_v2.3_payment' into 'feature_v2.3'

fix: 信用下单完整流程

See merge request robotaxi1/xp-robotaxi-miniprogram!150
2个文件已添加
16个文件已修改
706 ■■■■■ 已修改文件
src/robotaxi/api/begin-route.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/types/begin-route.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/wechat-pay-score-loading.less 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-be-being.tsx 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/wechat-pay-score-loading.tsx 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-credit-pay-return.ts 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-actions.shared.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-cancel.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-payment.ts 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-submit.ts 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-status-socket-handler.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 260 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/order/index.tsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/personal-center/index.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts
@@ -40,7 +40,8 @@
    UnlockVehiclePayload,
    UnlockVehicleResponse,
    OrderPaymentResponse,
    QueryOrderPaymentResultResponse
    QueryOrderPaymentResultResponse,
    QueryLedgerCreditOrderResultResponse
} from './types/begin-route';
// 导航-获取路线和费用
@@ -116,7 +117,8 @@
    return request({
        url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/creditPay/createOrder`,
        method: 'POST',
        payload
        payload,
        showToast: false
    }, true);
}
@@ -322,6 +324,19 @@
    }, true);
}
// 查询信用分授权结果
export const queryLedgerCreditOrderResult = async (orderId: OrderId): Promise<QueryLedgerCreditOrderResultResponse> => {
    return request({
        url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/queryLedgerCreditOrderResult`,
        method: 'POST',
        payload: {
            orderId: String(orderId)
        },
        skipException: true,
        showToast: false
    }, true);
}
// 订单查询实时轨迹路线
export const queryOrderRoute = async (
    payload: QueryOrderRoutePayload
src/robotaxi/api/types/begin-route.ts
@@ -605,6 +605,12 @@
/** 订单支付结果查询返回值。 */
export type QueryOrderPaymentResultResponse = {
    status: RobotaxiOrderStatus | number;
    orderId: string;
    payStatus: number;
    payType: number;
    payTime: string;
};
// 信用分授权结果查询响应
export type QueryLedgerCreditOrderResultResponse = {
    creditOrderStatus: 'DOING' | 'REVOKED' | 'EXPIRED' | string;
};
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
@@ -25,6 +25,12 @@
}: Props) {
    const [showDetail, setShowDetail] = useState(false); // 费用明细展开状态
    const canOpenCouponSelect = tripStatus === TripStatus.TripFinished && typeof onOpenCouponSelect === 'function'; // 仅待支付支持切换优惠券
    const isCreditPayProcessing = (
        tripStatus === TripStatus.TripFinished
        && !!orderId
        && String(Taro.getStorageSync('creditPayOrderId') || '') === String(orderId)
    ); // 信用下单结束后,展示免密支付中并阻止重复点击
    const payButtonText = isCreditPayProcessing ? '免密支付中' : '立即支付';
    // 外部重置版本变化时收起明细
    useEffect(() => {
@@ -210,7 +216,15 @@
                            :
                            <View className="pay-box-free">员工内部测试</View>
                    }{/*  */}
                    <View onClick={handlePay} className="pay-box-btn">立即支付</View>
                    <View className="pay-box-btn-wrapper">
                        <View
                            onClick={isCreditPayProcessing ? undefined : handlePay}
                            className="pay-box-btn"
                        >
                            {payButtonText}
                        </View>
                        {isCreditPayProcessing && <View className="pay-box-btn-mask" />}
                    </View>
                </View>
            }
        </View>
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -708,6 +708,12 @@
    }
  }
  &-btn-wrapper {
    width: 296px;
    height: 94px;
    position: relative;
  }
  &-btn {
    width: 296px;
    height: 94px;
@@ -720,6 +726,16 @@
    letter-spacing: normal;
    color: #FFFFFF;
  }
  &-btn-mask {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    border-radius: 20px;
    background: rgba(255, 255, 255, 0.4);
  }
}
.trip-action-bar-item-hover {
src/robotaxi/pages/begin-route/components/css-folder/wechat-pay-score-loading.less
New file
@@ -0,0 +1,70 @@
.wechat-pay-score-loading-mask {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: 999999;
  display: flex;
  align-items: center;
  justify-content: center;
  /* 背景全透明,只有中间的盒子有颜色 */
  background: transparent;
}
.wechat-pay-score-loading-box {
  width: 260px;
  height: 260px;
  background-color: rgba(50, 50, 50, 0.95);
  border-radius: 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: #fff;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.wechat-pay-score-icon {
  width: 88px;
  height: 88px;
  margin-bottom: 24px;
}
.wechat-pay-score-text {
  font-size: 30px;
  font-weight: 400;
  margin-bottom: 32px;
  line-height: 1;
}
.wechat-pay-score-dots {
  display: flex;
  gap: 12px;
}
.wechat-pay-score-dot {
  width: 12px;
  height: 12px;
  background-color: #fff;
  border-radius: 50%;
  opacity: 0.3;
  animation: wechat-pay-score-dot-blink 1.4s infinite ease-in-out both;
}
.wechat-pay-score-dot:nth-child(1) {
  animation-delay: -0.32s;
}
.wechat-pay-score-dot:nth-child(2) {
  animation-delay: -0.16s;
}
@keyframes wechat-pay-score-dot-blink {
  0%, 80%, 100% {
    opacity: 0.3;
  }
  40% {
    opacity: 1;
  }
}
src/robotaxi/pages/begin-route/components/release-be-being.tsx
@@ -1,16 +1,19 @@
import Taro from "@tarojs/taro";
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Image, View } from "@tarojs/components";
import './css-folder/release-be-being.less'
type Props = {
    handleOpenCancelPopup?: () => void;
    onFinish?: () => void;
    finishBlocked?: boolean;
}
export default function ReleaseBeBeing({ handleOpenCancelPopup, onFinish }: Props) {
export default function ReleaseBeBeing({ handleOpenCancelPopup, onFinish, finishBlocked = false }: Props) {
    const [progress, setProgress] = useState(0); // 发布进度条
    const [completedCount, setCompletedCount] = useState(0); // 已完成步骤数
    const [finishReady, setFinishReady] = useState(false); // 发布动画是否已走完
    const finishTriggeredRef = useRef(false); // 防止 onFinish 重复触发
    const startSite = Taro.getStorageSync('startSite') || {};
    const endSite = Taro.getStorageSync('endSite') || {};
    const startSiteName = startSite?.name || startSite?.address || '-';
@@ -27,9 +30,9 @@
        const step1 = setTimeout(() => setCompletedCount(1), 1000);
        const step2 = setTimeout(() => setCompletedCount(2), 2000);
        // 3秒后进入呼叫车中状态
        // 3 秒后标记动画完成;是否真正切页由 finishBlocked 控制
        const step3 = setTimeout(() => {
            onFinish?.();
            setFinishReady(true);
        }, 3000);
        return () => {
@@ -40,6 +43,15 @@
        };
    }, []);
    useEffect(() => {
        // 信用分下单时,需要等待用户从支付分确认页返回后再推进到“呼叫车中”。
        if (!finishReady || finishBlocked || finishTriggeredRef.current) {
            return;
        }
        finishTriggeredRef.current = true;
        onFinish?.();
    }, [finishBlocked, finishReady, onFinish]);
    return (
        <View className='release-be-being'>
            <View className="top-box">
src/robotaxi/pages/begin-route/components/wechat-pay-score-loading.tsx
New file
@@ -0,0 +1,30 @@
import { View, Image } from '@tarojs/components';
import React from 'react';
import './css-folder/wechat-pay-score-loading.less';
// 模拟微信支付分的图标 SVG Base64
const PAY_SCORE_ICON_BASE64 = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDQiIGhlaWdodD0iNDQiIHZpZXdCb3g9IjAgMCA0NCA0NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cGF0aCBkPSJNMjIgM0wyNiA3SDMyVjEzTDM2IDE3TDI2IDIxVjI3SDI2TDIyIDMxTDE4IDI3SDEyVjIxTDggMTdMMTIgMTNWN0gxOEwyMiAzWiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyLjUiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KICA8Y2lyY2xlIGN4PSIyMiIgY3k9IjE0IiByPSI0IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIuNSIvPgogIDxwYXRoIGQ9Ik0xNSAyNUMxNSAyMS4xMzQgMTguMTM0IDE4IDIyIDE4QzI1Ljg2NiAxOCAyOSAyMS4xMzQgMjkgMjUiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KICA8cGF0aCBkPSJNMjQgMjdMMjggMzFMMzggMjEiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPg==';
type Props = {
  visible: boolean;
};
const WechatPayScoreLoading: React.FC<Props> = ({ visible }) => {
  if (!visible) return null;
  return (
    <View className="wechat-pay-score-loading-mask">
      <View className="wechat-pay-score-loading-box">
        <Image className="wechat-pay-score-icon" src={PAY_SCORE_ICON_BASE64} mode="aspectFit" />
        <View className="wechat-pay-score-text">微信支付分</View>
        <View className="wechat-pay-score-dots">
          <View className="wechat-pay-score-dot" />
          <View className="wechat-pay-score-dot" />
          <View className="wechat-pay-score-dot" />
        </View>
      </View>
    </View>
  );
};
export default React.memo(WechatPayScoreLoading);
src/robotaxi/pages/begin-route/hooks/use-credit-pay-return.ts
@@ -1,9 +1,16 @@
import { useCallback, useEffect, useRef } from 'react';
import Taro from '@tarojs/taro';
import { queryLedgerCreditOrderResult } from '../../../api/begin-route';
type Deps = {
  /** 支付分页面返回后的回调。 */
  onReturn: () => void;
  /** 当前正在下单的订单 ID,用于查询授权结果。 */
  orderId?: string;
  /** 支付分授权成功后的回调。 */
  onAuthorizedReturn: (queryId: string) => void | Promise<void>;
  /** 支付分未授权后的回调。 */
  onUnauthorizedReturn: (orderId?: string) => void | Promise<void>;
  /** 支付分页面拉起失败后的回调。 */
  onLaunchFailed: (orderId?: string) => void | Promise<void>;
};
const PAY_SCORE_REFERRER_APP_ID = 'wxd8f3793ea3b935b8';
@@ -28,22 +35,25 @@
  /**
   * 拉起微信支付分订单确认页。
   */
  const openCreditPayScoreUse = useCallback((packageValue: string) => {
  const openCreditPayScoreUse = useCallback((packageValue: string, targetOrderId?: string) => {
    const queryOrderId = targetOrderId || deps.orderId;
    if (!packageValue) {
      Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' });
      return false;
      void deps.onLaunchFailed(queryOrderId);
      return true;
    }
    // @ts-ignore
    if (!wx.openBusinessView) {
      Taro.showToast({ title: '请升级微信版本后重试', icon: 'none' });
      return false;
      void deps.onLaunchFailed(queryOrderId);
      return true;
    }
    clearAppShowListener();
    /** 支付分页面返回监听函数。 */
    const handleAppShow = (res) => {
    const handleAppShow = async (res) => {
      if (res?.scene !== 1038) {
        return;
      }
@@ -53,8 +63,88 @@
        return;
      }
      const queryId = String(res?.referrerInfo?.extraData?.query_id || '').trim();
      console.log('[信用分授权返回][AppShow]', {
        scene: res?.scene,
        referrerInfo: res?.referrerInfo,
        queryId
      });
      clearAppShowListener();
      deps.onReturn();
      if (!queryId) {
        void deps.onUnauthorizedReturn(queryOrderId);
        return;
      }
      // 根据 query_id 轮询查询真实的授权结果
      if (!queryOrderId) {
        console.log('[信用分授权查询失败] 缺失 orderId');
        void deps.onUnauthorizedReturn(queryOrderId);
        return;
      }
      Taro.showLoading({ title: '授权结果查询中', mask: true });
      const maxPollingCount = 120;
      let currentPollingCount = 0;
      const pollStatus = async () => {
        try {
          const result = await queryLedgerCreditOrderResult(queryOrderId);
          const status = result?.data?.creditOrderStatus;
          console.log(`[信用分授权查询结果] 第${currentPollingCount + 1}次`, result);
          if (status === 'REVOKED' || status === 'EXPIRED') {
            Taro.hideLoading();
            void deps.onUnauthorizedReturn(queryOrderId);
            return;
          }
          if (status === 'DOING' || result?.code === 20710) {
            Taro.hideLoading();
            void deps.onAuthorizedReturn(queryId);
            return;
          }
          if (status === 'CREATED') {
            // CREATED 状态也当作中间状态继续轮询
            currentPollingCount++;
            if (currentPollingCount >= maxPollingCount) {
              Taro.hideLoading();
              console.log('[信用分授权查询超时] CREATED 状态持续2分钟');
              void deps.onUnauthorizedReturn(queryOrderId);
              return;
            }
            setTimeout(pollStatus, 1000);
            return;
          }
          // 其他状态,继续轮询
          currentPollingCount++;
          if (currentPollingCount >= maxPollingCount) {
            Taro.hideLoading();
            console.log('[信用分授权查询超时]');
            void deps.onUnauthorizedReturn(queryOrderId);
            return;
          }
          setTimeout(pollStatus, 1000);
        } catch (error: any) {
          const errorCode = String(error?.code || error?.data?.code || '');
          if (errorCode === '20710') {
            console.log(`[信用分授权查询遇到20710] 第${currentPollingCount + 1}次,当作授权成功处理`);
            Taro.hideLoading();
            void deps.onAuthorizedReturn(queryId);
            return;
          }
          Taro.hideLoading();
          console.log('[信用分授权查询失败]', error);
          void deps.onUnauthorizedReturn(queryOrderId);
        }
      };
      await pollStatus();
    };
    appShowListenerRef.current = handleAppShow;
@@ -66,14 +156,19 @@
      extraData: {
        package: packageValue
      },
      success: (res) => {
        console.log('[信用分授权拉起][openBusinessView]', res);
      },
      fail: () => {
        clearAppShowListener();
        console.log('[信用分授权拉起失败][openBusinessView]');
        Taro.showToast({ title: '拉起支付分订单失败', icon: 'none' });
        void deps.onLaunchFailed(queryOrderId);
      }
    });
    return true;
  }, [clearAppShowListener, deps.onReturn]);
  }, [clearAppShowListener, deps.onAuthorizedReturn, deps.onLaunchFailed, deps.onUnauthorizedReturn, deps.orderId]);
  useEffect(() => clearAppShowListener, [clearAppShowListener]);
src/robotaxi/pages/begin-route/hooks/use-order-actions.shared.ts
@@ -126,7 +126,9 @@
  /** 未签约态支付处理。 */
  handleUnsignedPayment: (token: string) => void | Promise<void>;
  /** 已签约态支付处理。 */
  handleSignedCreditPayment: (value: SignedOrderPaymentValue) => void | Promise<void>;
  handleSignedCreditPayment: (value: SignedOrderPaymentValue, targetOrderId?: string) => boolean | Promise<boolean>;
  /** 结束行程后进入待支付态时,触发信用扣款结果查询。 */
  handleCreditPayTripFinished?: (targetOrderId: string) => void | Promise<void>;
  /** 同步上车点。 */
  setStartSite?: (site: any) => void;
};
src/robotaxi/pages/begin-route/hooks/use-order-cancel.ts
@@ -50,6 +50,7 @@
    deps.setVehicleInfo({});
    clearOrderTripStorage();
    Taro.removeStorageSync('pickupEndTime');
    Taro.removeStorageSync('creditPayOrderId');
    Taro.removeStorageSync(WAITING_TIMER_START_STORAGE_KEY);
    Taro.removeStorageSync(WAITING_TIMER_ELAPSED_STORAGE_KEY);
  };
src/robotaxi/pages/begin-route/hooks/use-order-payment.ts
@@ -92,7 +92,10 @@
    } else {
      syncTripFinishedStatus();
    }
    return orderDetail;
    return {
      orderDetail,
      nextTripStatus
    };
  };
  /**
@@ -145,7 +148,11 @@
    try {
      const endOrderResult: any = await endOrder(targetOrderId);
      const orderDetail = await syncPostEndOrderTripStatus(targetOrderId);
      const { orderDetail, nextTripStatus } = await syncPostEndOrderTripStatus(targetOrderId);
      if (nextTripStatus === TripStatus.TripFinished) {
        // 结束行程后若仍待支付,立即触发信用扣款结果查询,避免依赖页面 effect 才能进入。
        await deps.handleCreditPayTripFinished?.(targetOrderId);
      }
      reportTripPayStartEvent(orderDetail);
      if (endOrderResult) {
        Taro.showToast({
@@ -171,7 +178,11 @@
      await endOrder(targetOrderId);
    } catch (error) {
    }
    await syncPostEndOrderTripStatus(targetOrderId);
    const { nextTripStatus } = await syncPostEndOrderTripStatus(targetOrderId);
    if (nextTripStatus === TripStatus.TripFinished) {
      // 倒计时自动结束行程时也复用同一套信用扣款结果查询逻辑。
      await deps.handleCreditPayTripFinished?.(targetOrderId);
    }
  };
  /**
@@ -192,35 +203,34 @@
      mask: false
    });
    try {
      await getOrderPayment({ orderId: targetOrderId });
      syncPaymentSuccessStatus();
      // await getOrderPayment({ orderId: targetOrderId });
      // syncPaymentSuccessStatus();
      // const paymentResult = await getOrderPayment({ orderId: targetOrderId });
      // const paymentStatus = Number(paymentResult?.status);
      const paymentResult = await getOrderPayment({ orderId: targetOrderId });
      const paymentStatus = Number(paymentResult?.status);
      // if (paymentStatus === RobotaxiPaymentEntryStatus.Unsigned) {
      //   const cashierToken = paymentResult?.value?.token;
      //   if (!cashierToken) {
      //     Taro.showToast({ title: '支付凭证缺失,请稍后重试', icon: 'none' });
      //     return;
      //   }
      //   await deps.handleUnsignedPayment(cashierToken);
      //   return;
      // }
      if (paymentStatus === RobotaxiPaymentEntryStatus.Unsigned) {
        const cashierToken = paymentResult?.value?.token;
        if (!cashierToken) {
          Taro.showToast({ title: '支付凭证缺失,请稍后重试', icon: 'none' });
          return;
        }
        await deps.handleUnsignedPayment(cashierToken);
        return;
      }
      // if (paymentStatus === RobotaxiPaymentEntryStatus.Signed) {
      //   const signedPaymentValue = paymentResult?.value as SignedOrderPaymentValue;
      //   if (!signedPaymentValue?.packageValue) {
      //     Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' });
      //     return;
      //   }
      //   await deps.handleSignedCreditPayment(signedPaymentValue);
      //   return;
      // }
      if (paymentStatus === RobotaxiPaymentEntryStatus.Signed) {
        const signedPaymentValue = paymentResult?.value as SignedOrderPaymentValue;
        if (!signedPaymentValue?.packageValue) {
          Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' });
          return;
        }
        await deps.handleSignedCreditPayment(signedPaymentValue);
        return;
      }
      // Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' });
      Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' });
    } catch (error) {
      Taro.showToast({ title: '支付失败,请重试', icon: 'none' });
    } finally {
src/robotaxi/pages/begin-route/hooks/use-order-submit.ts
@@ -51,6 +51,9 @@
  callImmediately: () => void;
};
/** 当前路线是否为载客测试免费。 */
const isOperateFreeRoute = (selectedRoute: any) => Number(selectedRoute?.operateFree) === 1;
/** 标准化文本,兼容普通空格与不可见空格差异。 */
const normalizeText = (value: any) => String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
@@ -465,9 +468,13 @@
  /**
   * 下单前校验免密状态。
   */
  const checkCreditPayReadyBeforeOrder = useCallback(async () => {
  const checkCreditPayReadyBeforeOrder = useCallback(async (selectedRoute: any) => {
    if (isOperateFreeRoute(selectedRoute)) {
      // 载客测试免费单直接走普通下单,不拉起微信信用下单弹窗。
      return 'normal_submit' as CreditPayCheckResult;
    }
    // 免密状态校验,先默认返回正常下单,后续根据校验结果再处理。
    return 'normal_submit' as CreditPayCheckResult;
    // return 'normal_submit' as CreditPayCheckResult;
    try {
      const hasEnabledCreditPay = await getPayPwd();
      if (hasEnabledCreditPay === true) {
@@ -565,7 +572,7 @@
    let submitMode: CreditPayCheckResult = 'normal_submit';
    if (!options.skipCreditPayCheck) {
      submitMode = await checkCreditPayReadyBeforeOrder();
      submitMode = await checkCreditPayReadyBeforeOrder(selectedRoute);
      if (submitMode === 'blocked') {
        return;
      }
@@ -575,34 +582,60 @@
    await waitForOrderSuccessSubscribePopup();
    try {
      if (submitMode === 'credit_pay_submit') {
        Taro.showLoading({ title: '加载中', mask: true });
      }
      const submitResult = submitMode === 'credit_pay_submit'
        ? await submitCreditPayOrder(startSite, openCityId, selectedRoute)
        : await submitOrder(startSite, openCityId);
      if (!submitResult?.orderId) {
        Taro.hideLoading();
        stopCallSubmitting();
        return;
      }
      deps.setOrderId(submitResult.orderId);
      deps.setTripStatus(TripStatus.Publishing);
      deps.bindSocketListeners();
      void subscribeAfterOrderSuccess(submitResult.orderId);
      if (submitMode !== 'credit_pay_submit') {
        Taro.removeStorageSync('creditPayOrderId');
      }
      if (submitMode === 'credit_pay_submit') {
        Taro.setStorageSync('creditPayOrderId', submitResult.orderId);
        const creditPayPackageValue = String(submitResult.creditPayPackageValue || '').trim();
        if (!creditPayPackageValue) {
          Taro.hideLoading();
          Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' });
          return;
        }
        // 复用现有支付分确认页拉起与回跳查询链路。
        void deps.handleSignedCreditPayment({
        const launched = await deps.handleSignedCreditPayment({
          businessOrderId: '',
          authOrderId: '',
          channelOrderId: '',
          payType: 13,
          packageValue: creditPayPackageValue
        });
        }, submitResult.orderId);
        Taro.hideLoading();
        // 只有拉起失败才立刻进入发单状态(或者依靠 abort 逻辑终止)
        if (!launched) {
          deps.setTripStatus(TripStatus.Publishing);
        }
        return;
      }
      deps.setTripStatus(TripStatus.Publishing);
    } catch (error) {
      reportPayResultEvent(resolvePayResultStatus((error as any)?.msg || (error as any)?.message || error));
      Taro.hideLoading();
      const errorMessage = String((error as any)?.msg || (error as any)?.message || '下单失败,请重试');
      if (submitMode === 'credit_pay_submit') {
        Taro.showToast({ title: errorMessage, icon: 'none' });
      }
      reportPayResultEvent(resolvePayResultStatus(errorMessage));
      stopCallSubmitting();
    }
  };
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -61,6 +61,8 @@
  endSite: any;
  /** 发单 */
  callImmediately: () => void;
  /** 是否正在等待信用分确认页返回结果 */
  isWaitingSignedCreditPayResult?: boolean;
  /** 预估态选择上下车点 */
  handleEstimateSelectSite: (pageType: "start" | "end") => void;
  /** 费用信息 */
@@ -84,6 +86,8 @@
  handlePickupTimeoutAutoCancel: () => void;
  /** 行程状态设置 */
  setTripStatus: (value: TripStatus) => void;
  /** 发布中动画结束 */
  handlePublishingAnimationFinish?: () => void;
  /** 完成倒计时启用 */
  finishCountdownEnabled: boolean;
  /** 订单 ID */
@@ -695,7 +699,12 @@
          handleOpenCancelPopup={() =>
            deps.handleOpenCancelPopupByTripStatus(TripStatus.Publishing)
          }
          finishBlocked={Boolean(deps.isWaitingSignedCreditPayResult)}
          onFinish={() => {
            if (deps.handlePublishingAnimationFinish) {
              deps.handlePublishingAnimationFinish();
              return;
            }
            deps.setTripStatus(TripStatus.CallingCar);
            Taro.setStorageSync("tripStatus", TripStatus.CallingCar);
          }}
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -17,6 +17,10 @@
export type UseWebSocketEventsDeps = {
    tripStatusRef: React.MutableRefObject<TripStatus>;
    isWaitingSignedCreditPayResultRef?: React.MutableRefObject<boolean>;
    pendingSignedCreditPayTripStatusRef?: React.MutableRefObject<TripStatus | null>;
    isHoldingTripStatusDuringPublishingAnimationRef?: React.MutableRefObject<boolean>;
    pendingTripStatusAfterPublishingAnimationRef?: React.MutableRefObject<TripStatus | null>;
    pickupArrivedReportedOrderIdRef: React.MutableRefObject<string>;
    tripInProgressReportedOrderIdRef: React.MutableRefObject<string>;
    arrivedDestinationReportedOrderIdRef: React.MutableRefObject<string>;
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-status-socket-handler.ts
@@ -96,6 +96,14 @@
  if (currentTripStatus === mappedTripStatus) {
    return mappedTripStatus;
  }
  if (deps.isWaitingSignedCreditPayResultRef?.current) {
    deps.pendingSignedCreditPayTripStatusRef && (deps.pendingSignedCreditPayTripStatusRef.current = mappedTripStatus);
    return currentTripStatus;
  }
  if (deps.isHoldingTripStatusDuringPublishingAnimationRef?.current && currentTripStatus === TripStatus.Publishing) {
    deps.pendingTripStatusAfterPublishingAnimationRef && (deps.pendingTripStatusAfterPublishingAnimationRef.current = mappedTripStatus);
    return currentTripStatus;
  }
  deps.setTripStatus(mappedTripStatus);
  deps.tripStatusRef.current = mappedTripStatus;
  Taro.setStorageSync('tripStatus', mappedTripStatus);
src/robotaxi/pages/begin-route/index.tsx
@@ -60,11 +60,12 @@
import useBluetoothCarControl from './hooks/use-bluetooth-car-control';
import { toTripStatus, TripStatus } from '../../constants/trip-status';
import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from '../../constants/modify-destination';
import { selectNavigationRoute } from '../../api/begin-route';
import { cancelOrder, selectNavigationRoute } from '../../api/begin-route';
import { getAddressByLocation, getDistance, reportEvent } from '../../../utils/Robotaxi-public-method';
import { CurrentUserLocation, RouteDistanceValue } from './hooks/use-route-manager.shared';
import { MODIFY_DESTINATION_SESSION_KEY } from '../../constants/modify-destination';
import { getOrderId } from '../../utils/order';
import { getOrderDetail } from '../../api/order';
import { _ROBOTAXI_WS_HOST } from '../../../constants/api';
import { RobotaxiSignedPayType, SignedOrderPaymentValue } from '../../api/types/begin-route';
@@ -305,6 +306,7 @@
  const [pageDidShowVersion, setPageDidShowVersion] = useState(0); // 页面展示版本号,用于触发子模块重查
  const [resetDetailVersion, setResetDetailVersion] = useState(0); // 重置详情版本号,用于触发组件更新
  const [priceUpdateVersion, setPriceUpdateVersion] = useState(0); // 价格更新版本号
  const [, setCreditPayFallbackRefreshVersion] = useState(0); // 信用扣款回落普通支付后,触发页面重渲染
  const [evaluateSubmitVersion, setEvaluateSubmitVersion] = useState(0); // 评价提交版本号
  const [evaluateCloseVersion, setEvaluateCloseVersion] = useState(0); // 评价关闭版本号
  const [evaluateOrderRating, setEvaluateOrderRating] = useState(1); // 当前评价弹窗对应评分
@@ -315,7 +317,16 @@
  const [pickupExceptionInfo, setPickupExceptionInfo] = useState<PickupExceptionInfo | null>(null); // 接驾异常信息
  const [showPickupExceptionPopup, setShowPickupExceptionPopup] = useState(false); // 接驾异常弹窗展示状态
  const [showCreditPayPopup, setShowCreditPayPopup] = useState(false); // 免密支付开通弹窗
  const [isWaitingSignedCreditPayResult, setIsWaitingSignedCreditPayResult] = useState(false); // 信用分确认页已拉起,等待用户返回结果
  const creditPayResolverRef = useRef<((value: boolean) => void) | null>(null); // 免密弹窗的 Promise resolve 回调
  const isWaitingSignedCreditPayResultRef = useRef(false); // 信用分确认结果等待态引用,供 socket 状态同步拦截使用
  const pendingSignedCreditPayTripStatusRef = useRef<TripStatus | null>(null); // 等待信用分结果期间暂存的最新行程状态
  const pendingPublishingAfterSignedCreditPayRef = useRef(false); // 信用分下单成功后,等待结果返回再进入发布态
  const isHoldingTripStatusDuringPublishingAnimationRef = useRef(false); // 信用分返回后,发布中 3 秒动画期间暂存 socket 状态
  const pendingTripStatusAfterPublishingAnimationRef = useRef<TripStatus | null>(null); // 发布中动画结束后待恢复的最新状态
  const clearActiveOrderRuntimeRef = useRef<(() => void) | null>(null); // 信用分取消发单后复用订单清理逻辑
  const restartPlanningRouteRef = useRef<(() => void) | null>(null); // 信用分取消发单后复用发单前页面恢复逻辑
  const stopCallSubmittingRef = useRef<(() => void) | null>(null); // 信用分中止发单后复位“立即呼叫”提交态
  const [orderRouteMetrics, setOrderRouteMetrics] = useState<OrderRouteMetrics>({
    distanceMeters: null,
    durationSeconds: null
@@ -1357,6 +1368,52 @@
  }, []);
  /**
   * 统一维护“等待信用分结果”状态,同时同步 ref,供异步回调和 socket 读取。
   */
  const syncWaitingSignedCreditPayResult = useCallback((value: boolean) => {
    isWaitingSignedCreditPayResultRef.current = value;
    setIsWaitingSignedCreditPayResult(value);
  }, []);
  /**
   * 信用分结果返回后,恢复页面状态推进。
   */
  const resumeTripStatusAfterSignedCreditPayReturn = useCallback(() => {
    syncWaitingSignedCreditPayResult(false);
    const deferredTripStatus = pendingSignedCreditPayTripStatusRef.current;
    pendingSignedCreditPayTripStatusRef.current = null;
    if (pendingPublishingAfterSignedCreditPayRef.current) {
      pendingPublishingAfterSignedCreditPayRef.current = false;
      pendingTripStatusAfterPublishingAnimationRef.current = deferredTripStatus;
      isHoldingTripStatusDuringPublishingAnimationRef.current = true;
      setTripStatus(TripStatus.Publishing);
      tripStatusRef.current = TripStatus.Publishing;
      Taro.setStorageSync('tripStatus', TripStatus.Publishing);
      return;
    }
    if (deferredTripStatus !== null) {
      setTripStatus(deferredTripStatus);
      tripStatusRef.current = deferredTripStatus;
      Taro.setStorageSync('tripStatus', deferredTripStatus);
    }
  }, [syncWaitingSignedCreditPayResult]);
  /**
   * 发布中 3 秒动画结束后,再恢复信用分结果等待期间暂存的最新状态。
   */
  const handlePublishingAnimationFinish = useCallback(() => {
    const deferredTripStatus = pendingTripStatusAfterPublishingAnimationRef.current;
    pendingTripStatusAfterPublishingAnimationRef.current = null;
    isHoldingTripStatusDuringPublishingAnimationRef.current = false;
    const nextTripStatus = deferredTripStatus ?? TripStatus.CallingCar;
    setTripStatus(nextTripStatus);
    tripStatusRef.current = nextTripStatus;
    Taro.setStorageSync('tripStatus', nextTripStatus);
  }, []);
  /**
   * 确认开通免密支付。
   */
  const handleConfirmCreditPayPopup = useCallback(() => {
@@ -1390,6 +1447,10 @@
  const { bindSocketListeners, ensureSocketListeners } = useWebSocketEvents({
    tripStatusRef,
    isWaitingSignedCreditPayResultRef,
    pendingSignedCreditPayTripStatusRef,
    isHoldingTripStatusDuringPublishingAnimationRef,
    pendingTripStatusAfterPublishingAnimationRef,
    pickupArrivedReportedOrderIdRef,
    tripInProgressReportedOrderIdRef,
    arrivedDestinationReportedOrderIdRef,
@@ -1491,9 +1552,155 @@
    onPaySuccess: handlePaymentSuccess
  });
  // 信用分结束行程 loading 状态
  const creditPayLoadingTimerRef = useRef<any>(null);
  const creditPayFallbackCheckTimerRef = useRef<any>(null);
  const creditPayFallbackActiveOrderIdRef = useRef('');
  const clearCreditPayFallbackRuntime = useCallback((hideLoading = true) => {
    if (creditPayLoadingTimerRef.current) {
      clearTimeout(creditPayLoadingTimerRef.current);
      creditPayLoadingTimerRef.current = null;
    }
    if (creditPayFallbackCheckTimerRef.current) {
      clearInterval(creditPayFallbackCheckTimerRef.current);
      creditPayFallbackCheckTimerRef.current = null;
    }
    creditPayFallbackActiveOrderIdRef.current = '';
    if (hideLoading) {
      Taro.hideLoading();
    }
  }, []);
  const handleCreditPayTripFinished = useCallback(async (targetOrderId: string) => {
    const resolvedOrderId = String(targetOrderId || '').trim();
    const creditPayOrderId = String(Taro.getStorageSync('creditPayOrderId') || '').trim();
    if (!resolvedOrderId || !creditPayOrderId || creditPayOrderId !== resolvedOrderId) {
      return;
    }
    if (
      creditPayFallbackActiveOrderIdRef.current === resolvedOrderId
      && (creditPayLoadingTimerRef.current || creditPayFallbackCheckTimerRef.current)
    ) {
      return;
    }
    clearCreditPayFallbackRuntime(false);
    creditPayFallbackActiveOrderIdRef.current = resolvedOrderId;
    Taro.showLoading({ title: '扣款结果查询中,请勿重复支付', mask: true });
    const checkCreditPayFallback = async () => {
      try {
        const orderDetail: any = await getOrderDetail(resolvedOrderId);
        if (Number(orderDetail?.payType) === 1) {
          Taro.removeStorageSync('creditPayOrderId');
          clearCreditPayFallbackRuntime();
          setCreditPayFallbackRefreshVersion((prev) => prev + 1);
          Taro.showToast({ title: '免密扣款失败,请手动支付', icon: 'none' });
        }
      } catch (error) {
      }
    };
    // 先立即查一次,再轮询订单详情感知 payType 是否从信用支付回落到普通支付。
    await checkCreditPayFallback();
    if (creditPayFallbackActiveOrderIdRef.current !== resolvedOrderId) {
      return;
    }
    creditPayFallbackCheckTimerRef.current = setInterval(() => {
      void checkCreditPayFallback();
    }, 1000);
    creditPayLoadingTimerRef.current = setTimeout(() => {
      clearCreditPayFallbackRuntime();
      Taro.redirectTo({
        url: '/pages/robotaxi/home/index',
      });
    }, 10000);
  }, [clearCreditPayFallbackRuntime]);
  useEffect(() => {
    if (tripStatus === TripStatus.TripFinished) {
      void handleCreditPayTripFinished(orderId);
      return () => {
        clearCreditPayFallbackRuntime();
      };
    } else {
      clearCreditPayFallbackRuntime();
    }
    return () => {
      clearCreditPayFallbackRuntime();
    };
  }, [tripStatus, orderId, handleCreditPayTripFinished, clearCreditPayFallbackRuntime]);
  /**
   * 信用分流程未完成时,统一取消订单并终止发单。
   */
  const abortSignedCreditPayFlow = useCallback(async (reason: 'unauthorized' | 'launch_failed', fallbackOrderId?: string) => {
    const targetOrderId = getOrderId(orderId) || fallbackOrderId;
    console.log('[信用分流程中止]', {
      reason,
      orderId: targetOrderId
    });
    stopCallSubmittingRef.current?.();
    syncWaitingSignedCreditPayResult(false);
    pendingPublishingAfterSignedCreditPayRef.current = false;
    pendingSignedCreditPayTripStatusRef.current = null;
    pendingTripStatusAfterPublishingAnimationRef.current = null;
    isHoldingTripStatusDuringPublishingAnimationRef.current = false;
    if (!targetOrderId) {
      clearActiveOrderRuntimeRef.current?.();
      restartPlanningRouteRef.current?.();
      return;
    }
    try {
      await cancelOrder(targetOrderId);
      // Taro.showToast({
      //   title: reason === 'launch_failed' ? '拉起信用分失败,已取消发单' : '未完成信用分授权,已取消发单',
      //   icon: 'none'
      // });
      clearActiveOrderRuntimeRef.current?.();
      if (restartPlanningRouteRef.current) {
        restartPlanningRouteRef.current();
        return;
      }
      setTripStatus(TripStatus.PublishRoute);
      tripStatusRef.current = TripStatus.PublishRoute;
      Taro.setStorageSync('tripStatus', TripStatus.PublishRoute);
    } catch (error) {
      console.log('[信用分流程中止][取消订单失败]', {
        reason,
        error
      });
      Taro.showToast({ title: '取消订单失败,请重试', icon: 'none' });
    }
  }, [orderId, syncWaitingSignedCreditPayResult]);
  /**
   * 信用分页面返回但未拿到 query_id,视为未授权。
   */
  const handleSignedCreditPayUnauthorizedReturn = useCallback((targetOrderId?: string) => {
    return abortSignedCreditPayFlow('unauthorized', targetOrderId);
  }, [abortSignedCreditPayFlow]);
  /**
   * 信用分页面拉起失败,直接终止本次发单。
   */
  const handleSignedCreditPayLaunchFailed = useCallback((targetOrderId?: string) => {
    return abortSignedCreditPayFlow('launch_failed', targetOrderId);
  }, [abortSignedCreditPayFlow]);
  const { openCreditPayScoreUse } = useCreditPayReturn({
    // 信用下单链路回跳后仅关闭监听,不再主动查询支付结果。
    onReturn: () => {}
    orderId,
    // 信用下单链路回跳后,有 query_id 且结果为 DOING 或已确认时,继续正常发单。
    onAuthorizedReturn: () => resumeTripStatusAfterSignedCreditPayReturn(),
    // 授权结果为失败/撤销等,立即取消订单并终止发单。
    onUnauthorizedReturn: handleSignedCreditPayUnauthorizedReturn,
    // 拉起失败同样取消订单并终止发单。
    onLaunchFailed: handleSignedCreditPayLaunchFailed
  });
  useCashierPayListener({
@@ -1513,14 +1720,30 @@
  /**
   * 处理已签约态支付分支付。
   */
  const handleSignedCreditPayment = useCallback((value: SignedOrderPaymentValue) => {
  const handleSignedCreditPayment = useCallback((value: SignedOrderPaymentValue, targetOrderId?: string) => {
    if (Number(value?.payType) !== RobotaxiSignedPayType.WechatCredit) {
      Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' });
      return;
      return false;
    }
    openCreditPayScoreUse(value.packageValue);
  }, [openCreditPayScoreUse]);
    pendingPublishingAfterSignedCreditPayRef.current = true;
    pendingSignedCreditPayTripStatusRef.current = null;
    syncWaitingSignedCreditPayResult(true);
    const launched = openCreditPayScoreUse(value.packageValue, targetOrderId);
    if (!launched) {
      pendingPublishingAfterSignedCreditPayRef.current = false;
      syncWaitingSignedCreditPayResult(false);
    }
    return launched;
  }, [openCreditPayScoreUse, syncWaitingSignedCreditPayResult]);
  useEffect(() => {
    if (tripStatus !== TripStatus.Publishing) {
      syncWaitingSignedCreditPayResult(false);
      isHoldingTripStatusDuringPublishingAnimationRef.current = false;
      pendingTripStatusAfterPublishingAnimationRef.current = null;
    }
  }, [tripStatus, syncWaitingSignedCreditPayResult]);
  const orderActions = useOrderActions({
    tripStatus,
@@ -1559,6 +1782,7 @@
    routeRequestRouteAndFare,
    handleUnsignedPayment,
    handleSignedCreditPayment,
    handleCreditPayTripFinished,
    setStartSite
  });
@@ -1570,8 +1794,14 @@
    handlePickupTimeoutAutoCancel,
    handleConfirmOverRoute,
    handleFinishCountdownTimeout,
    handlePay
    handlePay,
    stopCallSubmitting,
    clearActiveOrderRuntime,
    restartPlanningRoute
  } = orderActions;
  stopCallSubmittingRef.current = stopCallSubmitting;
  clearActiveOrderRuntimeRef.current = clearActiveOrderRuntime;
  restartPlanningRouteRef.current = restartPlanningRoute;
  const {
    authSuccess: bluetoothAuthSuccess,
    startAuth: startBluetoothAuth,
@@ -1623,6 +1853,14 @@
  useDidShow(() => {
    setPageDidShowVersion((v) => v + 1);
    setDoorUnlockedInCurrentPage(false);
    // 信用分结果页返回后,需要先保留“发布中”动画,不要立刻走页面恢复链路把状态顶到最新。
    if (
      isWaitingSignedCreditPayResultRef.current
      || isHoldingTripStatusDuringPublishingAnimationRef.current
    ) {
      updateMapViewportPadding();
      return;
    }
    handleDidShow(router.params?.skipPageFlag, router.params?.fromModifyDestination);
    updateMapViewportPadding();
    // 完成态下隐藏小程序再恢复时,受控中心点可能与原生地图实际中心脱钩,主动触发一次
@@ -1759,7 +1997,7 @@
      // (causedBy='update') 的 end 不应清除标记,否则会在用户仍在操作时提前恢复补帧导致弹回。
      userGestureActiveRef.current = false;
    }
    if(eventType === 'begin' && isUserGesture) {
      notifyUserMapGesture();
@@ -1784,7 +2022,7 @@
          recordLongitude(lng);
        }
    }
    // useSpeedBasedScale.performFit 调用 includePoints 后会置 pendingFitSyncRef=true,
    // globalFitSyncRef.current = true 表示全局其他地方(如 PublishRoute 的 includePoints)
    // 等本次 fit 动画真正结束(type==='end')时直接从事件 detail 读取实际 scale/center
@@ -2134,6 +2372,7 @@
    startSite,
    endSite: effectiveEndSite,
    callImmediately: handleCallImmediately,
    isWaitingSignedCreditPayResult,
    handleEstimateSelectSite,
    fareEstimate,
    handleOpenCouponSelect,
@@ -2144,6 +2383,7 @@
    handleOpenCancelPopupWithType,
    handlePickupTimeoutAutoCancel,
    setTripStatus,
    handlePublishingAnimationFinish,
    finishCountdownEnabled,
    orderId,
    vehicleInfo,
src/robotaxi/pages/order/index.tsx
@@ -225,6 +225,15 @@
    if (status == 10) {
      return "已完成";
    }
    if (status == 11) {
      return "退款中";
    }
    if (status == 12) {
      return "已退款";
    }
    if (status == 13) {
      return "退款失败";
    }
  };
  const getOrderStatusClassName = (status: number) => {
@@ -302,7 +311,7 @@
                        </View>
                      )}
                      {/* 发票功能暂未上线,屏蔽入口 */}
                      {false && ["已完成", "待评价"].includes(
                      { ["已完成", "待评价"].includes(
                        formatOrderStatus(item.status)
                      ) &&
                        item.invoiceStatus != 2 &&
@@ -321,7 +330,7 @@
                            </View>
                          </View>
                        )}
                      {["已完成", "已取消", "待评价"].includes(
                      {["已完成", "已取消", "待评价", "退款中", "已退款", "退款失败"].includes(
                        formatOrderStatus(item.status)
                      ) &&
                        canAgainOrder(item) && (
src/robotaxi/pages/personal-center/index.tsx
@@ -33,8 +33,8 @@
const toolMenuList: ToolMenuItem[] = [
  { key: "service", label: "联系客服" },
  // { key: "invoice", label: "开发票" }, // 暂未上线,屏蔽入口
  { key: "payment", label: "支付设置" },
  { key: "invoice", label: "开发票" }, // 暂未上线,屏蔽入口
  // { key: "payment", label: "支付设置" },
  { key: "favorite", label: "收藏站点" }
];