|
|
@@ -0,0 +1,314 @@
|
|
|
+/**
|
|
|
+ * 支付页面
|
|
|
+ * 处理微信支付流程和状态管理
|
|
|
+ */
|
|
|
+
|
|
|
+import Taro from '@tarojs/taro'
|
|
|
+import { useEffect, useState } from 'react'
|
|
|
+import { View, Text, Button } from '@tarojs/components'
|
|
|
+import { useQuery } from '@tanstack/react-query'
|
|
|
+import {
|
|
|
+ requestWechatPayment,
|
|
|
+ PaymentStatus,
|
|
|
+ PaymentStateManager,
|
|
|
+ PaymentRateLimiter,
|
|
|
+ retryPayment
|
|
|
+} from '@/utils/payment'
|
|
|
+
|
|
|
+interface PaymentPageParams {
|
|
|
+ orderId: number
|
|
|
+ amount: number
|
|
|
+ orderNo?: string
|
|
|
+}
|
|
|
+
|
|
|
+interface PaymentData {
|
|
|
+ timeStamp: string
|
|
|
+ nonceStr: string
|
|
|
+ package: string
|
|
|
+ signType: string
|
|
|
+ paySign: string
|
|
|
+}
|
|
|
+
|
|
|
+const PaymentPage = () => {
|
|
|
+ const [params, setParams] = useState<PaymentPageParams | null>(null)
|
|
|
+ const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.PENDING)
|
|
|
+ const [isProcessing, setIsProcessing] = useState(false)
|
|
|
+ const [errorMessage, setErrorMessage] = useState('')
|
|
|
+
|
|
|
+ // 获取页面参数
|
|
|
+ useEffect(() => {
|
|
|
+ const currentPage = Taro.getCurrentPages().pop()
|
|
|
+ if (currentPage?.options) {
|
|
|
+ const { orderId, amount, orderNo } = currentPage.options
|
|
|
+ if (orderId && amount) {
|
|
|
+ setParams({
|
|
|
+ orderId: parseInt(orderId),
|
|
|
+ amount: parseFloat(amount),
|
|
|
+ orderNo
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, [])
|
|
|
+
|
|
|
+
|
|
|
+ // 获取支付参数
|
|
|
+ const { data: paymentData, isLoading: paymentLoading } = useQuery({
|
|
|
+ queryKey: ['payment-params', params?.orderId],
|
|
|
+ queryFn: async () => {
|
|
|
+ if (!params?.orderId) throw new Error('订单ID无效')
|
|
|
+
|
|
|
+ // 这里应该调用后端API获取微信支付参数
|
|
|
+ // 暂时模拟返回支付参数
|
|
|
+ const mockPaymentData: PaymentData = {
|
|
|
+ timeStamp: Math.floor(Date.now() / 1000).toString(),
|
|
|
+ nonceStr: Math.random().toString(36).substring(2, 15),
|
|
|
+ package: 'prepay_id=wx' + Math.random().toString(36).substring(2, 15),
|
|
|
+ signType: 'RSA',
|
|
|
+ paySign: 'mock_sign_' + Math.random().toString(36).substring(2, 15)
|
|
|
+ }
|
|
|
+
|
|
|
+ return mockPaymentData
|
|
|
+ },
|
|
|
+ enabled: !!params?.orderId && paymentStatus === PaymentStatus.PENDING
|
|
|
+ })
|
|
|
+
|
|
|
+ // 支付状态管理
|
|
|
+ const paymentStateManager = PaymentStateManager.getInstance()
|
|
|
+ const rateLimiter = PaymentRateLimiter.getInstance()
|
|
|
+
|
|
|
+ // 处理支付
|
|
|
+ const handlePayment = async () => {
|
|
|
+ if (!params || !paymentData) {
|
|
|
+ setErrorMessage('支付参数不完整')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查频率限制
|
|
|
+ const rateLimit = rateLimiter.isRateLimited(params.orderId)
|
|
|
+ if (rateLimit.limited) {
|
|
|
+ setErrorMessage(`支付频率过高,请${Math.ceil(rateLimit.remainingTime! / 1000)}秒后重试`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ setIsProcessing(true)
|
|
|
+ setErrorMessage('')
|
|
|
+ setPaymentStatus(PaymentStatus.PROCESSING)
|
|
|
+ paymentStateManager.setPaymentState(params.orderId, PaymentStatus.PROCESSING)
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 记录支付尝试
|
|
|
+ rateLimiter.recordAttempt(params.orderId)
|
|
|
+
|
|
|
+ // 调用微信支付
|
|
|
+ const paymentResult = await requestWechatPayment(paymentData)
|
|
|
+
|
|
|
+ if (paymentResult.success) {
|
|
|
+ // 支付成功
|
|
|
+ setPaymentStatus(PaymentStatus.SUCCESS)
|
|
|
+ paymentStateManager.setPaymentState(params.orderId, PaymentStatus.SUCCESS)
|
|
|
+
|
|
|
+ // 清除频率限制记录
|
|
|
+ rateLimiter.clearAttempts(params.orderId)
|
|
|
+
|
|
|
+ // 跳转到支付成功页面
|
|
|
+ setTimeout(() => {
|
|
|
+ Taro.redirectTo({
|
|
|
+ url: `/pages/payment-success/index?orderId=${params.orderId}&amount=${params.amount}`
|
|
|
+ })
|
|
|
+ }, 1500)
|
|
|
+ } else {
|
|
|
+ // 支付失败
|
|
|
+ setPaymentStatus(PaymentStatus.FAILED)
|
|
|
+ paymentStateManager.setPaymentState(params.orderId, PaymentStatus.FAILED)
|
|
|
+
|
|
|
+ if (paymentResult.type === 'cancel') {
|
|
|
+ setErrorMessage('用户取消支付')
|
|
|
+ } else {
|
|
|
+ setErrorMessage(paymentResult.message || '支付失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('支付处理异常:', error)
|
|
|
+ setPaymentStatus(PaymentStatus.FAILED)
|
|
|
+ paymentStateManager.setPaymentState(params.orderId, PaymentStatus.FAILED)
|
|
|
+ setErrorMessage(error.message || '支付异常')
|
|
|
+ } finally {
|
|
|
+ setIsProcessing(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重试支付
|
|
|
+ const handleRetryPayment = async () => {
|
|
|
+ if (!params || !paymentData) return
|
|
|
+
|
|
|
+ setIsProcessing(true)
|
|
|
+ setErrorMessage('')
|
|
|
+
|
|
|
+ try {
|
|
|
+ const retryResult = await retryPayment(
|
|
|
+ () => requestWechatPayment(paymentData),
|
|
|
+ 3,
|
|
|
+ 1000
|
|
|
+ )
|
|
|
+
|
|
|
+ if (retryResult.success) {
|
|
|
+ setPaymentStatus(PaymentStatus.SUCCESS)
|
|
|
+ paymentStateManager.setPaymentState(params.orderId, PaymentStatus.SUCCESS)
|
|
|
+
|
|
|
+ // 跳转到支付成功页面
|
|
|
+ setTimeout(() => {
|
|
|
+ Taro.redirectTo({
|
|
|
+ url: `/pages/payment-success/index?orderId=${params.orderId}&amount=${params.amount}`
|
|
|
+ })
|
|
|
+ }, 1500)
|
|
|
+ } else {
|
|
|
+ setPaymentStatus(PaymentStatus.FAILED)
|
|
|
+ setErrorMessage(retryResult.message || '支付重试失败')
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('支付重试异常:', error)
|
|
|
+ setPaymentStatus(PaymentStatus.FAILED)
|
|
|
+ setErrorMessage(error.message || '支付重试异常')
|
|
|
+ } finally {
|
|
|
+ setIsProcessing(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 取消支付
|
|
|
+ const handleCancelPayment = () => {
|
|
|
+ if (params?.orderId) {
|
|
|
+ paymentStateManager.clearPaymentState(params.orderId)
|
|
|
+ rateLimiter.clearAttempts(params.orderId)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 返回上一页
|
|
|
+ Taro.navigateBack()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染支付状态
|
|
|
+ const renderPaymentStatus = () => {
|
|
|
+ switch (paymentStatus) {
|
|
|
+ case PaymentStatus.PENDING:
|
|
|
+ return (
|
|
|
+ <View>
|
|
|
+ <Text className="text-xl font-bold text-orange-500 block mb-2">待支付</Text>
|
|
|
+ <Text className="text-sm text-gray-600 block">请确认支付信息</Text>
|
|
|
+ </View>
|
|
|
+ )
|
|
|
+ case PaymentStatus.PROCESSING:
|
|
|
+ return (
|
|
|
+ <View>
|
|
|
+ <Text className="text-xl font-bold text-blue-500 block mb-2">支付中...</Text>
|
|
|
+ <Text className="text-sm text-gray-600 block">请稍候</Text>
|
|
|
+ </View>
|
|
|
+ )
|
|
|
+ case PaymentStatus.SUCCESS:
|
|
|
+ return (
|
|
|
+ <View>
|
|
|
+ <Text className="text-xl font-bold text-green-500 block mb-2">支付成功</Text>
|
|
|
+ <Text className="text-sm text-gray-600 block">正在跳转...</Text>
|
|
|
+ </View>
|
|
|
+ )
|
|
|
+ case PaymentStatus.FAILED:
|
|
|
+ return (
|
|
|
+ <View>
|
|
|
+ <Text className="text-xl font-bold text-red-500 block mb-2">支付失败</Text>
|
|
|
+ <Text className="text-sm text-gray-600 block">{errorMessage}</Text>
|
|
|
+ </View>
|
|
|
+ )
|
|
|
+ default:
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!params) {
|
|
|
+ return (
|
|
|
+ <View className="min-h-screen bg-gray-50 flex flex-col items-center justify-center">
|
|
|
+ <Text className="text-xl text-red-500 mb-8">参数错误</Text>
|
|
|
+ <Button onClick={() => Taro.navigateBack()} className="w-48 h-18 bg-blue-500 text-white rounded-full text-sm">
|
|
|
+ 返回
|
|
|
+ </Button>
|
|
|
+ </View>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <View className="min-h-screen bg-gray-50 p-5">
|
|
|
+ {/* 头部 */}
|
|
|
+ <View className="text-center py-6 bg-white rounded-2xl mb-5">
|
|
|
+ <Text className="text-2xl font-bold text-gray-800">支付订单</Text>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {/* 订单信息 */}
|
|
|
+ <View className="bg-white rounded-2xl p-6 mb-5">
|
|
|
+ <View className="flex justify-between items-center mb-4">
|
|
|
+ <Text className="text-sm text-gray-600">订单号:</Text>
|
|
|
+ <Text className="text-sm text-gray-800">{params.orderNo || `ORD${params.orderId}`}</Text>
|
|
|
+ </View>
|
|
|
+ <View className="flex justify-between items-center">
|
|
|
+ <Text className="text-sm text-gray-600">支付金额:</Text>
|
|
|
+ <Text className="text-2xl font-bold text-orange-500">¥{params.amount.toFixed(2)}</Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {/* 支付状态 */}
|
|
|
+ <View className="bg-white rounded-2xl p-8 mb-5 text-center">
|
|
|
+ {renderPaymentStatus()}
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {/* 支付按钮 */}
|
|
|
+ <View className="mb-5">
|
|
|
+ {paymentStatus === PaymentStatus.PENDING && (
|
|
|
+ <Button
|
|
|
+ onClick={handlePayment}
|
|
|
+ disabled={isProcessing || paymentLoading}
|
|
|
+ className={`w-full h-22 bg-gradient-to-r from-orange-500 to-orange-400 text-white rounded-full text-lg font-bold ${
|
|
|
+ isProcessing ? 'bg-gray-400' : ''
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ {isProcessing ? '支付中...' : `确认支付 ¥${params.amount.toFixed(2)}`}
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {paymentStatus === PaymentStatus.FAILED && (
|
|
|
+ <View className="flex gap-4">
|
|
|
+ <Button onClick={handleRetryPayment} className="flex-1 h-22 bg-blue-500 text-white rounded-full text-sm">
|
|
|
+ 重试支付
|
|
|
+ </Button>
|
|
|
+ <Button onClick={handleCancelPayment} className="flex-1 h-22 bg-gray-100 text-gray-600 border border-gray-300 rounded-full text-sm">
|
|
|
+ 取消支付
|
|
|
+ </Button>
|
|
|
+ </View>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {paymentStatus === PaymentStatus.PROCESSING && (
|
|
|
+ <Button disabled className="w-full h-22 bg-gray-100 text-gray-500 rounded-full text-sm">
|
|
|
+ 支付处理中...
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {paymentStatus === PaymentStatus.SUCCESS && (
|
|
|
+ <Button disabled className="w-full h-22 bg-gray-100 text-gray-500 rounded-full text-sm">
|
|
|
+ 支付成功
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {/* 支付说明 */}
|
|
|
+ <View className="bg-white rounded-2xl p-6">
|
|
|
+ <Text className="text-sm font-bold text-gray-800 block mb-4">支付说明</Text>
|
|
|
+ <Text className="text-xs text-gray-600 leading-relaxed whitespace-pre-line">
|
|
|
+ • 请确保网络连接正常
|
|
|
+ {'\n'}
|
|
|
+ • 支付过程中请勿关闭页面
|
|
|
+ {'\n'}
|
|
|
+ • 如遇支付问题,请尝试重新支付
|
|
|
+ {'\n'}
|
|
|
+ • 支付成功后会自动跳转
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+export default PaymentPage
|