| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554 |
- /**
- * 支付页面
- * 处理微信支付流程和状态管理
- */
- import Taro, { useRouter } from '@tarojs/taro'
- import { useState, useEffect } from 'react'
- import { View, Text } from '@tarojs/components'
- import { useQuery, useMutation } from '@tanstack/react-query'
- import { Button } from '@/components/ui/button'
- import { Navbar } from '@/components/ui/navbar'
- import {
- requestWechatPayment,
- PaymentStatus,
- PaymentStateManager,
- PaymentRateLimiter,
- retryPayment
- } from '@/utils/payment'
- import { paymentClient, creditBalanceClient } from '@/api'
- interface PaymentData {
- timeStamp: string
- nonceStr: string
- package: string
- signType: string
- paySign: string
- }
- interface CreditBalanceData {
- totalLimit: number
- usedAmount: number
- availableAmount: number
- isEnabled: boolean
- }
- enum PaymentMethod {
- WECHAT = 'wechat',
- CREDIT = 'credit'
- }
- const PaymentPage = () => {
- const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.PENDING)
- const [isProcessing, setIsProcessing] = useState(false)
- const [errorMessage, setErrorMessage] = useState('')
- const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod>(PaymentMethod.WECHAT)
- const [creditBalance, setCreditBalance] = useState<CreditBalanceData | null>(null)
- const [isCreditBalanceLoading, setIsCreditBalanceLoading] = useState(false)
- // 使用useRouter钩子获取路由参数
- const router = useRouter()
- const routerParams = router.params
- const orderId = routerParams?.orderId ? parseInt(routerParams.orderId) : 0
- const amount = routerParams?.amount ? parseFloat(routerParams.amount) : 0
- const orderNo = routerParams?.orderNo
- // 获取用户额度信息
- const fetchCreditBalance = async () => {
- setIsCreditBalanceLoading(true)
- try {
- const response = await creditBalanceClient.me.$get({})
- if (response.status === 200) {
- const balanceData = await response.json()
- setCreditBalance(balanceData)
- } else if (response.status === 404) {
- // 用户没有额度记录,创建默认额度
- setCreditBalance({
- totalLimit: 0,
- usedAmount: 0,
- availableAmount: 0,
- isEnabled: false
- })
- } else {
- // 其他错误情况
- setCreditBalance({
- totalLimit: 0,
- usedAmount: 0,
- availableAmount: 0,
- isEnabled: false
- })
- }
- } catch (error) {
- console.error('获取用户额度失败:', error)
- setCreditBalance({
- totalLimit: 0,
- usedAmount: 0,
- availableAmount: 0,
- isEnabled: false
- })
- } finally {
- setIsCreditBalanceLoading(false)
- }
- }
- // 组件加载时获取额度信息
- useEffect(() => {
- fetchCreditBalance()
- }, [])
- // 获取微信支付参数(手动触发)
- const {
- mutateAsync: fetchWechatPaymentParams,
- data: paymentData,
- isLoading: paymentLoading,
- error: paymentError
- } = useMutation({
- mutationFn: async () => {
- if (!orderId) throw new Error('订单ID无效')
- // 调用后端API获取微信支付参数
- const response = await paymentClient.payment.$post({
- json: {
- orderId: orderId,
- totalAmount: Math.round(amount * 100), // 转换为分
- description: `订单支付 - ${orderNo || `ORD${orderId}`}`
- }
- })
- if (response.status !== 200) {
- throw new Error(`获取支付参数失败: ${response.status}`)
- }
- const responseData = await response.json()
- // 转换响应数据格式
- const paymentData: PaymentData = {
- timeStamp: responseData.timeStamp,
- nonceStr: responseData.nonceStr,
- package: responseData.package,
- signType: responseData.signType,
- paySign: responseData.paySign
- }
- return paymentData
- }
- })
- // 支付状态管理
- const paymentStateManager = PaymentStateManager.getInstance()
- const rateLimiter = PaymentRateLimiter.getInstance()
- // 处理额度支付
- const handleCreditPayment = async () => {
- if (!orderId) {
- setErrorMessage('订单信息不完整')
- return
- }
- if (!creditBalance?.isEnabled) {
- setErrorMessage('额度支付未启用')
- return
- }
- // 检查额度是否足够(使用订单金额检查)
- if (creditBalance.availableAmount < amount) {
- setErrorMessage(`额度不足,可用额度: ¥${creditBalance.availableAmount.toFixed(2)}`)
- return
- }
- setIsProcessing(true)
- setErrorMessage('')
- setPaymentStatus(PaymentStatus.PROCESSING)
- try {
- const response = await creditBalanceClient.payment.$post({
- json: {
- referenceId: orderId.toString(), // 传递订单ID而不是订单号
- remark: `订单支付 - ${orderNo || `ORD${orderId}`}`
- }
- })
- if (response.status === 200) {
- // 额度支付成功
- setPaymentStatus(PaymentStatus.SUCCESS)
- // 更新本地额度信息
- const updatedBalance = await response.json()
- setCreditBalance(updatedBalance)
- // 跳转到支付成功页面
- setTimeout(() => {
- Taro.redirectTo({
- url: `/pages/payment-success/index?orderId=${orderId}&amount=${amount}&paymentMethod=credit`
- })
- }, 1500)
- } else {
- const errorData = await response.json()
- setPaymentStatus(PaymentStatus.FAILED)
- setErrorMessage(errorData.message || '额度支付失败')
- }
- } catch (error: any) {
- console.error('额度支付处理异常:', error)
- setPaymentStatus(PaymentStatus.FAILED)
- setErrorMessage(error.message || '额度支付异常')
- } finally {
- setIsProcessing(false)
- }
- }
- // 处理支付
- const handlePayment = async () => {
- if (selectedPaymentMethod === PaymentMethod.CREDIT) {
- await handleCreditPayment()
- return
- }
- // 微信支付逻辑
- if (!orderId) {
- setErrorMessage('订单信息不完整')
- return
- }
- // 检查频率限制
- const rateLimit = rateLimiter.isRateLimited(orderId)
- if (rateLimit.limited) {
- setErrorMessage(`支付频率过高,请${Math.ceil(rateLimit.remainingTime! / 1000)}秒后重试`)
- return
- }
- setIsProcessing(true)
- setErrorMessage('')
- setPaymentStatus(PaymentStatus.PROCESSING)
- try {
- // 先获取微信支付参数
- const wechatPaymentData = await fetchWechatPaymentParams()
- if (!wechatPaymentData) {
- setPaymentStatus(PaymentStatus.FAILED)
- setErrorMessage('获取支付参数失败')
- return
- }
- paymentStateManager.setPaymentState(orderId, PaymentStatus.PROCESSING)
- // 记录支付尝试
- rateLimiter.recordAttempt(orderId)
- // 调用微信支付
- const paymentResult = await requestWechatPayment(wechatPaymentData)
- if (paymentResult.success) {
- // 支付成功
- setPaymentStatus(PaymentStatus.SUCCESS)
- paymentStateManager.setPaymentState(orderId, PaymentStatus.SUCCESS)
- // 清除频率限制记录
- rateLimiter.clearAttempts(orderId)
- // 跳转到支付成功页面
- setTimeout(() => {
- Taro.redirectTo({
- url: `/pages/payment-success/index?orderId=${orderId}&amount=${amount}`
- })
- }, 1500)
- } else {
- // 支付失败
- setPaymentStatus(PaymentStatus.FAILED)
- paymentStateManager.setPaymentState(orderId, PaymentStatus.FAILED)
- if (paymentResult.type === 'cancel') {
- setErrorMessage('用户取消支付')
- } else {
- setErrorMessage(paymentResult.message || '支付失败')
- }
- }
- } catch (error: any) {
- console.error('支付处理异常:', error)
- setPaymentStatus(PaymentStatus.FAILED)
- paymentStateManager.setPaymentState(orderId, PaymentStatus.FAILED)
- setErrorMessage(error.message || '支付异常')
- } finally {
- setIsProcessing(false)
- }
- }
- // 重试支付
- const handleRetryPayment = async () => {
- if (selectedPaymentMethod === PaymentMethod.CREDIT) {
- await handleCreditPayment()
- return
- }
- if (!orderId) return
- setIsProcessing(true)
- setErrorMessage('')
- try {
- // 先获取微信支付参数
- const wechatPaymentData = await fetchWechatPaymentParams()
- if (!wechatPaymentData) {
- setPaymentStatus(PaymentStatus.FAILED)
- setErrorMessage('获取支付参数失败')
- return
- }
- const retryResult = await retryPayment(
- () => requestWechatPayment(wechatPaymentData),
- 3,
- 1000
- )
- if (retryResult.success) {
- setPaymentStatus(PaymentStatus.SUCCESS)
- paymentStateManager.setPaymentState(orderId, PaymentStatus.SUCCESS)
- // 跳转到支付成功页面
- setTimeout(() => {
- Taro.redirectTo({
- url: `/pages/payment-success/index?orderId=${orderId}&amount=${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 (orderId) {
- paymentStateManager.clearPaymentState(orderId)
- rateLimiter.clearAttempts(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 (!orderId || !amount) {
- 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">
- {/* 导航栏 */}
- <Navbar
- title="支付订单"
- leftIcon=""
- onClickLeft={() => {}}
- />
- <View className="p-5">
- {/* 头部 */}
- <View className="text-center py-6 bg-white rounded-2xl mb-5">
- <Text className="text-2xl font-bold text-gray-800" data-testid="payment-page-title">支付订单</Text>
- </View>
- {/* 订单信息 */}
- <View className="bg-white rounded-2xl p-6 mb-5" data-testid="order-info">
- <View className="flex justify-between items-center mb-4">
- <Text className="text-sm text-gray-600">订单号:</Text>
- <Text className="text-sm text-gray-800" data-testid="order-no">{orderNo || `ORD${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" data-testid="payment-amount">¥{amount.toFixed(2)}</Text>
- </View>
- </View>
- {/* 支付方式选择 */}
- <View className="bg-white rounded-2xl p-6 mb-5">
- <Text className="text-sm font-bold text-gray-800 block mb-4">选择支付方式</Text>
- {/* 微信支付选项 */}
- <View
- className={`flex items-center justify-between p-4 mb-3 rounded-xl border-2 ${
- selectedPaymentMethod === PaymentMethod.WECHAT
- ? 'border-blue-500 bg-blue-50'
- : 'border-gray-200'
- }`}
- onClick={() => setSelectedPaymentMethod(PaymentMethod.WECHAT)}
- data-testid="wechat-payment-option"
- >
- <View className="flex items-center">
- <View className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center mr-3">
- <Text className="text-green-600 text-lg">💰</Text>
- </View>
- <View>
- <Text className="text-sm font-bold text-gray-800">微信支付</Text>
- <Text className="text-xs text-gray-500">使用微信支付完成付款</Text>
- </View>
- </View>
- {selectedPaymentMethod === PaymentMethod.WECHAT && (
- <View className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center" data-testid="wechat-selected">
- <Text className="text-white text-xs">✓</Text>
- </View>
- )}
- </View>
- {/* 额度支付选项 - 只在额度满足时才显示 */}
- {creditBalance?.isEnabled && creditBalance?.availableAmount >= amount && (
- <View
- className={`flex items-center justify-between p-4 rounded-xl border-2 ${
- selectedPaymentMethod === PaymentMethod.CREDIT
- ? 'border-blue-500 bg-blue-50'
- : 'border-gray-200'
- }`}
- onClick={() => setSelectedPaymentMethod(PaymentMethod.CREDIT)}
- data-testid="credit-payment-option"
- >
- <View className="flex items-center">
- <View className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center mr-3">
- <Text className="text-purple-600 text-lg">💳</Text>
- </View>
- <View>
- <Text className="text-sm font-bold text-gray-800">额度支付</Text>
- <Text className="text-xs text-gray-500" data-testid="available-amount-text">
- 使用信用额度支付
- </Text>
- </View>
- </View>
- {selectedPaymentMethod === PaymentMethod.CREDIT && (
- <View className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center" data-testid="credit-selected">
- <Text className="text-white text-xs">✓</Text>
- </View>
- )}
- </View>
- )}
- {/* 额度支付说明 */}
- {selectedPaymentMethod === PaymentMethod.CREDIT && creditBalance && (
- <View className="mt-4 p-3 bg-blue-50 rounded-lg" data-testid="credit-payment-details">
- <Text className="text-xs text-blue-700">
- • 使用信用额度支付,无需立即付款
- </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 || (selectedPaymentMethod === PaymentMethod.CREDIT && (!creditBalance?.isEnabled || creditBalance?.availableAmount < amount))}
- className={`w-full h-22 ${
- selectedPaymentMethod === PaymentMethod.CREDIT
- ? 'bg-gradient-to-r from-purple-500 to-purple-400'
- : 'bg-gradient-to-r from-orange-500 to-orange-400'
- } text-white rounded-full text-lg font-bold ${
- isProcessing ? 'bg-gray-400' : ''
- }`}
- data-testid="pay-button"
- >
- {isProcessing ? '支付中...' :
- selectedPaymentMethod === PaymentMethod.CREDIT
- ? `额度支付 ¥${amount.toFixed(2)}`
- : `微信支付 ¥${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>
- </View>
- )
- }
- export default PaymentPage
|