You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native+Firebase支付流程异常场景处理方案咨询

React Native + Firebase 支付流程健壮性改造方案

适配工具包选型

  • 网络检测:@react-native-community/netinfo,官方维护的跨平台网络状态库,支持判断实际公网连通性、网络类型,无额外依赖
  • 本地缓存:@react-native-async-storage/async-storage,用于存储挂起的支付请求,网络恢复后自动触发对账
  • 提示组件:复用项目现有Toast/弹窗组件即可,无需额外引入重型组件库
  • 超时逻辑:直接使用原生Promise.race实现,不需要引入第三方请求库,保持包体积最小

核心改造原则

  • 支付前置校验:点击支付后先校验登录态、网络连通性,不满足条件直接提示并终止流程,不发起无效请求
  • 防重复提交:请求发起后立刻禁用支付按钮、隐藏可点击区域,直到流程完全结束再恢复,避免重复触发扣款
  • 全异常覆盖:必须覆盖网络中断、请求超时、服务端错误、业务支付失败四类场景,禁止出现loading永久卡死、无任何提示的情况
  • 不确定状态兜底:遇到超时、断网这类无法确认支付最终结果的场景,不要直接判定支付失败,本地记录待对账请求,后续网络恢复/重新进入页面时主动查询流水状态,避免用户已扣款但前端显示失败的资损问题
  • 超时阈值设置:Firebase Functions最长超时为300s,前端请求超时建议设置为25s(绝大多数支付通道正常响应时长在10s内,超过25s即可判定链路阻塞,进入待对账流程)

核心注意:支付场景所有最终状态必须以服务端支付流水为准,前端的超时、断网仅代表前端未收到响应,不代表支付请求未执行成功。

可直接落地的代码实现

React Native端改造代码

import NetInfo from '@react-native-community/netinfo';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Alert, TouchableOpacity, Text, ActivityIndicator } from 'react-native';
import firebase from '@react-native-firebase/app';
import '@react-native-firebase/functions';

// 超时请求封装
const withTimeout = (promise, timeoutMs = 25000) => {
  return Promise.race([
    promise,
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('PAYMENT_TIMEOUT')), timeoutMs)
    )
  ]);
};

const PaymentScreen = ({ navigation, route }) => {
  const [loading, setLoading] = useState(false);
  const [isBtnDisabled, setIsBtnDisabled] = useState(false);
  const { productsData } = route.params;

  // 页面加载时检查是否有未完成的待对账支付
  useEffect(() => {
    const checkPendingPayment = async () => {
      const pendingPay = await AsyncStorage.getItem('PENDING_PAYMENT');
      if (pendingPay) {
        const pendingData = JSON.parse(pendingPay);
        Alert.alert(
          '存在未完成支付',
          '检测到您有一笔正在处理的支付,是否查询结果?',
          [
            {text: '稍后查询', style: 'cancel'},
            {text: '立即查询', onPress: () => queryPaymentStatus(pendingData.requestId)}
          ]
        );
      }
    };
    checkPendingPayment();
  }, []);

  const onPay = async () => {
    // 防重复点击拦截
    if (isBtnDisabled || loading) return;
    setLoading(true);
    setIsBtnDisabled(true);

    try {
      // 前置网络连通性校验
      const netState = await NetInfo.fetch();
      if (!netState.isConnected || !netState.isInternetReachable) {
        Alert.alert('提示', '当前网络不可用,请检查网络后重试');
        return;
      }

      // 生成唯一请求ID,用于幂等校验和后续对账
      const requestId = `${firebase.auth().currentUser.uid}_${Date.now()}`;
      const payParams = { ...productsData, requestId };

      // 请求落本地存储,标记为待对账
      await AsyncStorage.setItem('PENDING_PAYMENT', JSON.stringify(payParams));

      // 发起带超时控制的支付请求
      const onPaymentCall = firebase.functions().httpsCallable('onPayment');
      const res = await withTimeout(onPaymentCall(payParams));

      if (res?.data?.statusCode === 200) {
        // 支付成功,清除本地待对账标记
        await AsyncStorage.removeItem('PENDING_PAYMENT');
        const paymentReceivedData = {
          transactionId: res.data.transactionId,
          paymentDate: Date.now(),
          totalPrice: res.data.totalPrice
        };
        navigation.replace('ReceiptScreen', { paymentReceivedData });
      } else {
        // 业务层面明确支付失败
        Alert.alert('支付失败', res?.data?.message || '请稍后重试');
      }
    } catch (error) {
      // 按错误类型返回对应提示
      let errMsg = '支付出现异常,请稍后在交易记录中查询结果,请勿重复支付';
      if (error?.message === 'PAYMENT_TIMEOUT') {
        errMsg = '支付请求超时,结果将在网络恢复后自动同步,请勿重复支付';
      } else if (error?.code === 'functions/network-error') {
        errMsg = '网络连接中断,请勿重复支付,结果将在网络恢复后同步';
      } else if (error?.code === 'functions/unauthenticated') {
        errMsg = '登录状态已失效,请重新登录后支付';
      } else if (error?.code === 'functions/deadline-exceeded') {
        errMsg = '服务处理超时,请勿重复支付,可稍后在交易记录查询结果';
      }
      Alert.alert('提示', errMsg);
    } finally {
      setLoading(false);
      setIsBtnDisabled(false);
    }
  };

  return (
    <TouchableOpacity 
      disabled={isBtnDisabled || loading}
      onPress={onPay}
      style={{opacity: isBtnDisabled || loading ? 0.6 : 1}}
    >
      {loading ? <ActivityIndicator /> : <Text>PAY</Text>}
    </TouchableOpacity>
  )
};

Firebase Functions端补充改造

export const onPayment = functions
    .runWith({ timeoutSeconds: 300 })
    .https.onCall(async (data: any, context: any) => {
      if (!context.auth) {
            throw new functions.https.HttpsError(
                'failed-precondition',
                'The function must be called while authenticated.'
            );
        }
      
      const { requestId } = data;
      // 幂等校验:查询当前请求ID是否已处理过,避免重复扣款
      const existPay = await admin.firestore()
        .collection('wallet')
        .where('requestId', '==', requestId)
        .limit(1)
        .get();
      if (!existPay.empty) {
        const existData = existPay.docs[0].data();
        return {
          statusCode: 200,
          totalPrice: existData.totalPrice,
          transactionId: existData.transactionId,
        };
      }

      try {
        const paymentResult = await paymentThroughApi(data);
        await admin.firestore().collection('wallet').add({
          ...paymentResult,
          requestId,
          uid: context.auth.uid,
          createdAt: admin.firestore.FieldValue.serverTimestamp()
        });
        return {
          statusCode: 200,
          totalPrice: paymentResult.totalPrice,
          transactionId: paymentResult.transactionId,
        };
      } catch (error) {
        // 按错误类型返回明确标识,供前端识别
        if (error.type === 'PAYMENT_GATEWAY_TIMEOUT') {
          throw new functions.https.HttpsError('deadline-exceeded', '支付网关处理超时');
        }
        if (error.type === 'PAYMENT_REJECTED') {
          return {
            statusCode: 400,
            message: error.message || '支付被拒绝,请检查支付方式',
            transactionId: null,
          };
        }
        throw new functions.https.HttpsError('internal', '支付服务异常');
      }

额外优化建议

  • 新增独立的支付状态查询云函数,供前端在待对账场景调用查询最终结果
  • 监听NetInfo的网络变化事件,网络恢复时自动触发待对账请求的状态查询
  • 支付请求发起后除了置灰按钮,可增加全屏不可点击的透明遮罩,彻底避免用户误触导致重复提交

内容的提问来源于stack exchange,提问作者dev1ce

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 21:39:06