index.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. /**
  2. * 支付页面
  3. * 处理微信支付流程和状态管理
  4. */
  5. import Taro from '@tarojs/taro'
  6. import { useState } from 'react'
  7. import { View, Text } from '@tarojs/components'
  8. import { useQuery } from '@tanstack/react-query'
  9. import { Button } from '@/components/ui/button'
  10. import {
  11. requestWechatPayment,
  12. PaymentStatus,
  13. PaymentStateManager,
  14. PaymentRateLimiter,
  15. retryPayment
  16. } from '@/utils/payment'
  17. import { paymentClient } from '@/api'
  18. interface PaymentData {
  19. timeStamp: string
  20. nonceStr: string
  21. package: string
  22. signType: string
  23. paySign: string
  24. }
  25. const PaymentPage = () => {
  26. const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.PENDING)
  27. const [isProcessing, setIsProcessing] = useState(false)
  28. const [errorMessage, setErrorMessage] = useState('')
  29. // 获取页面参数 - 参照 goods-detail 页面的写法
  30. const routerParams = Taro.getCurrentInstance().router?.params
  31. const orderId = routerParams?.orderId ? parseInt(routerParams.orderId) : 0
  32. const amount = routerParams?.amount ? parseFloat(routerParams.amount) : 0
  33. const orderNo = routerParams?.orderNo
  34. // 获取支付参数
  35. const { data: paymentData, isLoading: paymentLoading } = useQuery({
  36. queryKey: ['payment-params', orderId],
  37. queryFn: async () => {
  38. if (!orderId) throw new Error('订单ID无效')
  39. // 调用后端API获取微信支付参数
  40. const response = await paymentClient.payment.$post({
  41. json: {
  42. orderId: orderId,
  43. totalAmount: Math.round(amount * 100), // 转换为分
  44. description: `订单支付 - ${orderNo || `ORD${orderId}`}`
  45. }
  46. })
  47. if (response.status !== 200) {
  48. throw new Error(`获取支付参数失败: ${response.status}`)
  49. }
  50. const responseData = await response.json()
  51. // 转换响应数据格式
  52. const paymentData: PaymentData = {
  53. timeStamp: responseData.timeStamp,
  54. nonceStr: responseData.nonceStr,
  55. package: responseData.package,
  56. signType: responseData.signType,
  57. paySign: responseData.paySign
  58. }
  59. return paymentData
  60. },
  61. enabled: !!orderId && paymentStatus === PaymentStatus.PENDING
  62. })
  63. // 支付状态管理
  64. const paymentStateManager = PaymentStateManager.getInstance()
  65. const rateLimiter = PaymentRateLimiter.getInstance()
  66. // 处理支付
  67. const handlePayment = async () => {
  68. if (!paymentData || !orderId) {
  69. setErrorMessage('支付参数不完整')
  70. return
  71. }
  72. // 检查频率限制
  73. const rateLimit = rateLimiter.isRateLimited(orderId)
  74. if (rateLimit.limited) {
  75. setErrorMessage(`支付频率过高,请${Math.ceil(rateLimit.remainingTime! / 1000)}秒后重试`)
  76. return
  77. }
  78. setIsProcessing(true)
  79. setErrorMessage('')
  80. setPaymentStatus(PaymentStatus.PROCESSING)
  81. paymentStateManager.setPaymentState(orderId, PaymentStatus.PROCESSING)
  82. try {
  83. // 记录支付尝试
  84. rateLimiter.recordAttempt(orderId)
  85. // 调用微信支付
  86. const paymentResult = await requestWechatPayment(paymentData)
  87. if (paymentResult.success) {
  88. // 支付成功
  89. setPaymentStatus(PaymentStatus.SUCCESS)
  90. paymentStateManager.setPaymentState(orderId, PaymentStatus.SUCCESS)
  91. // 清除频率限制记录
  92. rateLimiter.clearAttempts(orderId)
  93. // 跳转到支付成功页面
  94. setTimeout(() => {
  95. Taro.redirectTo({
  96. url: `/pages/payment-success/index?orderId=${orderId}&amount=${amount}`
  97. })
  98. }, 1500)
  99. } else {
  100. // 支付失败
  101. setPaymentStatus(PaymentStatus.FAILED)
  102. paymentStateManager.setPaymentState(orderId, PaymentStatus.FAILED)
  103. if (paymentResult.type === 'cancel') {
  104. setErrorMessage('用户取消支付')
  105. } else {
  106. setErrorMessage(paymentResult.message || '支付失败')
  107. }
  108. }
  109. } catch (error: any) {
  110. console.error('支付处理异常:', error)
  111. setPaymentStatus(PaymentStatus.FAILED)
  112. paymentStateManager.setPaymentState(orderId, PaymentStatus.FAILED)
  113. setErrorMessage(error.message || '支付异常')
  114. } finally {
  115. setIsProcessing(false)
  116. }
  117. }
  118. // 重试支付
  119. const handleRetryPayment = async () => {
  120. if (!paymentData || !orderId) return
  121. setIsProcessing(true)
  122. setErrorMessage('')
  123. try {
  124. const retryResult = await retryPayment(
  125. () => requestWechatPayment(paymentData),
  126. 3,
  127. 1000
  128. )
  129. if (retryResult.success) {
  130. setPaymentStatus(PaymentStatus.SUCCESS)
  131. paymentStateManager.setPaymentState(orderId, PaymentStatus.SUCCESS)
  132. // 跳转到支付成功页面
  133. setTimeout(() => {
  134. Taro.redirectTo({
  135. url: `/pages/payment-success/index?orderId=${orderId}&amount=${amount}`
  136. })
  137. }, 1500)
  138. } else {
  139. setPaymentStatus(PaymentStatus.FAILED)
  140. setErrorMessage(retryResult.message || '支付重试失败')
  141. }
  142. } catch (error: any) {
  143. console.error('支付重试异常:', error)
  144. setPaymentStatus(PaymentStatus.FAILED)
  145. setErrorMessage(error.message || '支付重试异常')
  146. } finally {
  147. setIsProcessing(false)
  148. }
  149. }
  150. // 取消支付
  151. const handleCancelPayment = () => {
  152. if (orderId) {
  153. paymentStateManager.clearPaymentState(orderId)
  154. rateLimiter.clearAttempts(orderId)
  155. }
  156. // 返回上一页
  157. Taro.navigateBack()
  158. }
  159. // 渲染支付状态
  160. const renderPaymentStatus = () => {
  161. switch (paymentStatus) {
  162. case PaymentStatus.PENDING:
  163. return (
  164. <View>
  165. <Text className="text-xl font-bold text-orange-500 block mb-2">待支付</Text>
  166. <Text className="text-sm text-gray-600 block">请确认支付信息</Text>
  167. </View>
  168. )
  169. case PaymentStatus.PROCESSING:
  170. return (
  171. <View>
  172. <Text className="text-xl font-bold text-blue-500 block mb-2">支付中...</Text>
  173. <Text className="text-sm text-gray-600 block">请稍候</Text>
  174. </View>
  175. )
  176. case PaymentStatus.SUCCESS:
  177. return (
  178. <View>
  179. <Text className="text-xl font-bold text-green-500 block mb-2">支付成功</Text>
  180. <Text className="text-sm text-gray-600 block">正在跳转...</Text>
  181. </View>
  182. )
  183. case PaymentStatus.FAILED:
  184. return (
  185. <View>
  186. <Text className="text-xl font-bold text-red-500 block mb-2">支付失败</Text>
  187. <Text className="text-sm text-gray-600 block">{errorMessage}</Text>
  188. </View>
  189. )
  190. default:
  191. return null
  192. }
  193. }
  194. if (!orderId || !amount) {
  195. return (
  196. <View className="min-h-screen bg-gray-50 flex flex-col items-center justify-center">
  197. <Text className="text-xl text-red-500 mb-8">参数错误</Text>
  198. <Button onClick={() => Taro.navigateBack()} className="w-48 h-18 bg-blue-500 text-white rounded-full text-sm">
  199. 返回
  200. </Button>
  201. </View>
  202. )
  203. }
  204. return (
  205. <View className="min-h-screen bg-gray-50 p-5">
  206. {/* 头部 */}
  207. <View className="text-center py-6 bg-white rounded-2xl mb-5">
  208. <Text className="text-2xl font-bold text-gray-800">支付订单</Text>
  209. </View>
  210. {/* 订单信息 */}
  211. <View className="bg-white rounded-2xl p-6 mb-5">
  212. <View className="flex justify-between items-center mb-4">
  213. <Text className="text-sm text-gray-600">订单号:</Text>
  214. <Text className="text-sm text-gray-800">{orderNo || `ORD${orderId}`}</Text>
  215. </View>
  216. <View className="flex justify-between items-center">
  217. <Text className="text-sm text-gray-600">支付金额:</Text>
  218. <Text className="text-2xl font-bold text-orange-500">¥{amount.toFixed(2)}</Text>
  219. </View>
  220. </View>
  221. {/* 支付状态 */}
  222. <View className="bg-white rounded-2xl p-8 mb-5 text-center">
  223. {renderPaymentStatus()}
  224. </View>
  225. {/* 支付按钮 */}
  226. <View className="mb-5">
  227. {paymentStatus === PaymentStatus.PENDING && (
  228. <Button
  229. onClick={handlePayment}
  230. disabled={isProcessing || paymentLoading}
  231. className={`w-full h-22 bg-gradient-to-r from-orange-500 to-orange-400 text-white rounded-full text-lg font-bold ${
  232. isProcessing ? 'bg-gray-400' : ''
  233. }`}
  234. >
  235. {isProcessing ? '支付中...' : `确认支付 ¥${amount.toFixed(2)}`}
  236. </Button>
  237. )}
  238. {paymentStatus === PaymentStatus.FAILED && (
  239. <View className="flex gap-4">
  240. <Button onClick={handleRetryPayment} className="flex-1 h-22 bg-blue-500 text-white rounded-full text-sm">
  241. 重试支付
  242. </Button>
  243. <Button onClick={handleCancelPayment} className="flex-1 h-22 bg-gray-100 text-gray-600 border border-gray-300 rounded-full text-sm">
  244. 取消支付
  245. </Button>
  246. </View>
  247. )}
  248. {paymentStatus === PaymentStatus.PROCESSING && (
  249. <Button disabled className="w-full h-22 bg-gray-100 text-gray-500 rounded-full text-sm">
  250. 支付处理中...
  251. </Button>
  252. )}
  253. {paymentStatus === PaymentStatus.SUCCESS && (
  254. <Button disabled className="w-full h-22 bg-gray-100 text-gray-500 rounded-full text-sm">
  255. 支付成功
  256. </Button>
  257. )}
  258. </View>
  259. {/* 支付说明 */}
  260. <View className="bg-white rounded-2xl p-6">
  261. <Text className="text-sm font-bold text-gray-800 block mb-4">支付说明</Text>
  262. <Text className="text-xs text-gray-600 leading-relaxed whitespace-pre-line">
  263. • 请确保网络连接正常
  264. {'\n'}
  265. • 支付过程中请勿关闭页面
  266. {'\n'}
  267. • 如遇支付问题,请尝试重新支付
  268. {'\n'}
  269. • 支付成功后会自动跳转
  270. </Text>
  271. </View>
  272. </View>
  273. )
  274. }
  275. export default PaymentPage