hejianhao
2026-05-12 18414b7d34dc91b7c61c3799dc247b7f7c5e7747
feat(begin-route): 实现行程发起页完整支付流程

- 新增收银台支付监听、订单支付结果查询、微信支付分回跳处理三类自定义Hook
- 扩展行程发起接口的枚举与数据类型,新增订单支付状态查询API
- 重构支付处理逻辑,兼容未签约收银台与已签约微信支付分两种支付场景
- 添加重复支付防重复触发机制,统一支付成功回调流程
3个文件已添加
3个文件已修改
327 ■■■■■ 已修改文件
src/robotaxi/api/begin-route.ts 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-cashier-pay-listener.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-credit-pay-return.ts 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-order-payment-result-query.ts 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts
@@ -182,8 +182,62 @@
    }, true);
}
// 获取订单支付方式
export const getOrderPayment = async (payload) => {
/** 支付入口状态:1=已签约,2=未签约。 */
export enum RobotaxiPaymentEntryStatus {
    Signed = 1,
    Unsigned = 2
}
/** 已签约态支付方式。 */
export enum RobotaxiSignedPayType {
    WechatCredit = 13
}
/** 订单状态枚举。 */
export enum RobotaxiOrderStatus {
    Cancelled = 0,
    PendingAccept = 1,
    PendingDeparture = 2,
    OnTheWayToPickup = 3,
    WaitingPassengerBoard = 4,
    PassengerBoarded = 5,
    InService = 6,
    WaitingPassengerDropoff = 7,
    PendingPayment = 8,
    PendingComment = 9,
    Completed = 10
}
/** 已签约态返回值。 */
export type SignedOrderPaymentValue = {
    businessOrderId: string;
    authOrderId: string;
    channelOrderId: string;
    payType: RobotaxiSignedPayType | number;
    packageValue: string;
};
/** 未签约态返回值。 */
export type UnsignedOrderPaymentValue = {
    bizOrderId: string;
    prePayId: string;
    url: string;
    token: string;
};
/** 订单支付入口返回值。 */
export type OrderPaymentResponse =
    | { status: RobotaxiPaymentEntryStatus.Signed; value: SignedOrderPaymentValue }
    | { status: RobotaxiPaymentEntryStatus.Unsigned; value: UnsignedOrderPaymentValue };
/** 订单支付结果查询返回值。 */
export type QueryOrderPaymentResultResponse = {
    status: RobotaxiOrderStatus | number;
    orderId: string;
};
// 获取订单支付入口信息
export const getOrderPayment = async (payload): Promise<OrderPaymentResponse> => {
    return request({
        url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/payment`,
        method: 'POST',
@@ -191,6 +245,17 @@
    }, true);
}
// 根据订单 ID 查询支付结果(服务端已完成内部轮询)
export const queryOrderPaymentResultFromOrderId = async (orderId: string | number): Promise<QueryOrderPaymentResultResponse> => {
    return request({
        url: _ROBOTAXI_HOST + `/order/api/v1/mini-program/order/queryOrderStatusFromOrderId`,
        method: 'GET',
        payload: {
            orderId: String(orderId)
        }
    }, true);
}
// 取消订单
export const cancelOrder = async (orderId: string | number) => {
    return request({
src/robotaxi/pages/begin-route/hooks/use-cashier-pay-listener.ts
New file
@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import Taro from '@tarojs/taro';
type Deps = {
  /** 收银台支付成功后的统一回调。 */
  onPaySuccess: () => void;
};
/**
 * 监听收银台支付成功事件。
 */
export default function useCashierPayListener(deps: Deps) {
  useEffect(() => {
    /** 收银台支付成功事件处理函数。 */
    const handleCashierPaySuccess = () => {
      deps.onPaySuccess();
    };
    Taro.eventCenter.on('cashier:paySuccess', handleCashierPaySuccess);
    return () => {
      Taro.eventCenter.off('cashier:paySuccess', handleCashierPaySuccess);
    };
  }, [deps.onPaySuccess]);
}
src/robotaxi/pages/begin-route/hooks/use-credit-pay-return.ts
New file
@@ -0,0 +1,83 @@
import { useCallback, useEffect, useRef } from 'react';
import Taro from '@tarojs/taro';
type Deps = {
  /** 支付分页面返回后的回调。 */
  onReturn: () => void;
};
const PAY_SCORE_REFERRER_APP_ID = 'wxd8f3793ea3b935b8';
/**
 * 处理微信支付分拉起与返回识别。
 */
export default function useCreditPayReturn(deps: Deps) {
  /** 当前 AppShow 监听函数。 */
  const appShowListenerRef = useRef<any>(null);
  /**
   * 清理 AppShow 监听。
   */
  const clearAppShowListener = useCallback(() => {
    if (appShowListenerRef.current) {
      Taro.offAppShow(appShowListenerRef.current);
      appShowListenerRef.current = null;
    }
  }, []);
  /**
   * 拉起微信支付分订单确认页。
   */
  const openCreditPayScoreUse = useCallback((packageValue: string) => {
    if (!packageValue) {
      Taro.showToast({ title: '支付参数缺失,请稍后重试', icon: 'none' });
      return false;
    }
    // @ts-ignore
    if (!wx.openBusinessView) {
      Taro.showToast({ title: '请升级微信版本后重试', icon: 'none' });
      return false;
    }
    clearAppShowListener();
    /** 支付分页面返回监听函数。 */
    const handleAppShow = (res) => {
      if (res?.scene !== 1038) {
        return;
      }
      const { appId } = res.referrerInfo || {};
      if (appId !== PAY_SCORE_REFERRER_APP_ID) {
        return;
      }
      clearAppShowListener();
      deps.onReturn();
    };
    appShowListenerRef.current = handleAppShow;
    Taro.onAppShow(handleAppShow);
    // @ts-ignore
    wx.openBusinessView({
      businessType: 'wxpayScoreUse',
      extraData: {
        package: packageValue
      },
      fail: () => {
        clearAppShowListener();
        Taro.showToast({ title: '拉起支付分订单失败', icon: 'none' });
      }
    });
    return true;
  }, [clearAppShowListener, deps.onReturn]);
  useEffect(() => clearAppShowListener, [clearAppShowListener]);
  return {
    openCreditPayScoreUse
  };
}
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts
@@ -1,6 +1,7 @@
import Taro from '@tarojs/taro';
import { useRef } from 'react';
import { formatDateTime, clearOrderTripStorage, debounce, reportEvent } from '../../../../utils/Robotaxi-public-method';
import { saveOrder, getOrderPayment, cancelOrder, endOrder, routeStaleCheck } from '../../../api/begin-route';
import { saveOrder, getOrderPayment, cancelOrder, endOrder, routeStaleCheck, RobotaxiPaymentEntryStatus, SignedOrderPaymentValue } from '../../../api/begin-route';
import { getAgreement } from '../../../api/home';
import { getOrderDistance } from '../../../api/select-map-site';
import { checkOtp } from '../../../../utils/tools';
@@ -70,6 +71,10 @@
    startPointId: string | number,
    force?: boolean
  ) => void;
  /** 未签约态支付处理。 */
  handleUnsignedPayment: (token: string) => void | Promise<void>;
  /** 已签约态支付处理。 */
  handleSignedCreditPayment: (value: SignedOrderPaymentValue) => void | Promise<void>;
};
/**
@@ -78,6 +83,7 @@
export default function useOrderActions(deps: Deps) {
  const ROUTE_STALE_NOTICE_TEXT = '路况已变,价格已更新'; // 路况变化提示文案
  const NOT_AGREED_TEXT = 'Not agreed to the agreement'; // 协议未同意返回文案关键字
  const paySubmittingRef = useRef(false); // 支付中标记,避免重复触发
  // 标准化文本,兼容普通空格与不可见空格差异
  const normalizeText = (value: any) => String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
@@ -441,19 +447,45 @@
   * 支付处理。
   */
  const handlePay = async () => {
    if (paySubmittingRef.current) {
      return;
    }
    const targetOrderId = deps.orderId || Taro.getStorageSync('orderId');
    if (!targetOrderId) {
      Taro.showToast({ title: '订单信息缺失,无法支付', icon: 'none' });
      return;
    }
    paySubmittingRef.current = true;
    try {
      await getOrderPayment({ orderId: targetOrderId });
      Taro.showToast({ title: '支付成功', icon: 'none' });
      deps.setTripStatus(TripStatus.PaymentSuccess);
      Taro.setStorageSync('tripStatus', TripStatus.PaymentSuccess);
      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.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' });
    } catch (error) {
      Taro.showToast({ title: '支付失败,请重试', icon: 'none' });
      return;
    } finally {
      paySubmittingRef.current = false;
    }
  };
src/robotaxi/pages/begin-route/hooks/use-order-payment-result-query.ts
New file
@@ -0,0 +1,51 @@
import { useCallback } from 'react';
import Taro from '@tarojs/taro';
import { queryOrderPaymentResultFromOrderId, RobotaxiOrderStatus } from '../../../api/begin-route';
type Deps = {
  /** 当前订单 ID。 */
  orderId: string;
  /** 支付成功后的统一回调。 */
  onPaySuccess: () => void;
};
/**
 * 查询订单支付结果。
 */
export default function useOrderPaymentResultQuery(deps: Deps) {
  /**
   * 按订单 ID 查询支付结果。
   */
  const queryPaymentResult = useCallback(async () => {
    if (!deps.orderId) {
      Taro.showToast({ title: '订单信息缺失,无法确认支付结果', icon: 'none' });
      return;
    }
    Taro.showLoading({ title: '支付结果查询中' });
    try {
      const result = await queryOrderPaymentResultFromOrderId(deps.orderId);
      const nextStatus = Number(result?.status);
      if ([RobotaxiOrderStatus.PendingComment, RobotaxiOrderStatus.Completed].includes(nextStatus)) {
        deps.onPaySuccess();
        return;
      }
      if (nextStatus === RobotaxiOrderStatus.Cancelled) {
        Taro.showToast({ title: '订单已取消', icon: 'none' });
        return;
      }
      Taro.showToast({ title: '订单状态异常,请稍后查看', icon: 'none' });
    } catch (error) {
      Taro.showToast({ title: '支付结果查询失败,请稍后重试', icon: 'none' });
    } finally {
      Taro.hideLoading();
    }
  }, [deps.onPaySuccess, deps.orderId]);
  return {
    queryPaymentResult
  };
}
src/robotaxi/pages/begin-route/index.tsx
@@ -1,5 +1,5 @@
import Taro, { useDidHide, useDidShow, useShareAppMessage } from "@tarojs/taro"
import { useEffect, useState, useRef, useMemo } from 'react';
import { useEffect, useState, useRef, useMemo, useCallback } from 'react';
import { View, Image } from '@tarojs/components';
import './index.less';
@@ -34,6 +34,12 @@
import useTripRuntime from './hooks/use-trip-runtime';
// 订单动作编排
import useOrderActions from './hooks/use-order-actions';
// 收银台支付成功监听
import useCashierPayListener from './hooks/use-cashier-pay-listener';
// 支付分回跳监听
import useCreditPayReturn from './hooks/use-credit-pay-return';
// 订单支付结果查询
import useOrderPaymentResultQuery from './hooks/use-order-payment-result-query';
// 视图委托渲染编排
import useViewDelegates from './hooks/use-view-delegates';
// 修改目的地流程编排
@@ -52,6 +58,8 @@
import originIcon from '../../../robotaxi/assets/origin-icon.png'
import packUpIcon from '../../../robotaxi/assets/pack-up.png'
import { _ROBOTAXI_WS_HOST } from '../../../constants/api';
import { RobotaxiSignedPayType, SignedOrderPaymentValue } from '../../api/begin-route';
import { jumpTo } from '../../../utils/jump';
const ROUTE_SUMMARY_MARKER_ID = 999001;
type StatusRule = {
@@ -681,6 +689,49 @@
    resetSmoothRouteMotion
  });
  /**
   * 支付成功后的统一收口。
   */
  const handlePaymentSuccess = useCallback(() => {
    setTripStatus(TripStatus.PaymentSuccess);
    Taro.setStorageSync('tripStatus', TripStatus.PaymentSuccess);
  }, []);
  const { queryPaymentResult: queryOrderPaymentResult } = useOrderPaymentResultQuery({
    orderId,
    onPaySuccess: handlePaymentSuccess
  });
  const { openCreditPayScoreUse } = useCreditPayReturn({
    onReturn: queryOrderPaymentResult
  });
  useCashierPayListener({
    onPaySuccess: handlePaymentSuccess
  });
  /**
   * 打开收银台处理未签约态支付。
   */
  const handleUnsignedPayment = useCallback((token: string) => {
    jumpTo({
      url: `/pages/cashier/index?token=${encodeURIComponent(token)}&from=miniapp`,
      type: 'nav'
    });
  }, []);
  /**
   * 处理已签约态支付分支付。
   */
  const handleSignedCreditPayment = useCallback((value: SignedOrderPaymentValue) => {
    if (Number(value?.payType) !== RobotaxiSignedPayType.WechatCredit) {
      Taro.showToast({ title: '暂不支持当前支付方式', icon: 'none' });
      return;
    }
    openCreditPayScoreUse(value.packageValue);
  }, [openCreditPayScoreUse]);
  const orderActions = useOrderActions({
    tripStatus,
    orderId,
@@ -708,7 +759,9 @@
    checkDistanceAndCityReady,
    bindSocketListeners,
    routeUpdateWalkingRouteForStart,
    routeRequestRouteAndFare
    routeRequestRouteAndFare,
    handleUnsignedPayment,
    handleSignedCreditPayment
  });
  const {