feat(begin-route): 实现行程发起页完整支付流程
- 新增收银台支付监听、订单支付结果查询、微信支付分回跳处理三类自定义Hook
- 扩展行程发起接口的枚举与数据类型,新增订单支付状态查询API
- 重构支付处理逻辑,兼容未签约收银台与已签约微信支付分两种支付场景
- 添加重复支付防重复触发机制,统一支付成功回调流程
| | |
| | | }, 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', |
| | |
| | | }, 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({ |
| New file |
| | |
| | | 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]); |
| | | } |
| New file |
| | |
| | | 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 |
| | | }; |
| | | } |
| | |
| | | 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'; |
| | |
| | | startPointId: string | number, |
| | | force?: boolean |
| | | ) => void; |
| | | /** 未签约态支付处理。 */ |
| | | handleUnsignedPayment: (token: string) => void | Promise<void>; |
| | | /** 已签约态支付处理。 */ |
| | | handleSignedCreditPayment: (value: SignedOrderPaymentValue) => void | Promise<void>; |
| | | }; |
| | | |
| | | /** |
| | |
| | | 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(); |
| | |
| | | * 支付处理。 |
| | | */ |
| | | 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; |
| | | } |
| | | }; |
| | | |
| New file |
| | |
| | | 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 |
| | | }; |
| | | } |
| | |
| | | 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'; |
| | | |
| | |
| | | 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'; |
| | | // 修改目的地流程编排 |
| | |
| | | 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 = { |
| | |
| | | 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, |
| | |
| | | checkDistanceAndCityReady, |
| | | bindSocketListeners, |
| | | routeUpdateWalkingRouteForStart, |
| | | routeRequestRouteAndFare |
| | | routeRequestRouteAndFare, |
| | | handleUnsignedPayment, |
| | | handleSignedCreditPayment |
| | | }); |
| | | |
| | | const { |