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
相关产品推荐
相关产品推荐

