index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. /**
  2. * 支付页面
  3. * 处理微信支付流程和状态管理
  4. */
  5. import Taro, { useRouter } from '@tarojs/taro'
  6. import { useState, useEffect } from 'react'
  7. import { View, Text } from '@tarojs/components'
  8. import { useQuery, useMutation } from '@tanstack/react-query'
  9. import { Button } from '@/components/ui/button'
  10. import { Navbar } from '@/components/ui/navbar'
  11. import {
  12. requestWechatPayment,
  13. PaymentStatus,
  14. PaymentStateManager,
  15. PaymentRateLimiter,
  16. retryPayment
  17. } from '@/utils/payment'
  18. import { paymentClient, creditBalanceClient } from '@/api'
  19. interface PaymentData {
  20. timeStamp: string
  21. nonceStr: string
  22. package: string
  23. signType: string
  24. paySign: string
  25. }
  26. interface CreditBalanceData {
  27. totalLimit: number
  28. usedAmount: number
  29. availableAmount: number
  30. isEnabled: boolean
  31. }
  32. enum PaymentMethod {
  33. WECHAT = 'wechat',
  34. CREDIT = 'credit'
  35. }
  36. const PaymentPage = () => {
  37. const [paymentStatus, setPaymentStatus] = useState<PaymentStatus>(PaymentStatus.PENDING)
  38. const [isProcessing, setIsProcessing] = useState(false)
  39. const [errorMessage, setErrorMessage] = useState('')
  40. const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod>(PaymentMethod.WECHAT)
  41. const [creditBalance, setCreditBalance] = useState<CreditBalanceData | null>(null)
  42. const [isCreditBalanceLoading, setIsCreditBalanceLoading] = useState(false)
  43. // 使用useRouter钩子获取路由参数
  44. const router = useRouter()
  45. const routerParams = router.params
  46. const orderId = routerParams?.orderId ? parseInt(routerParams.orderId) : 0
  47. const amount = routerParams?.amount ? parseFloat(routerParams.amount) : 0
  48. const orderNo = routerParams?.orderNo
  49. // 获取用户额度信息
  50. const fetchCreditBalance = async () => {
  51. setIsCreditBalanceLoading(true)
  52. try {
  53. const response = await creditBalanceClient.me.$get({})
  54. if (response.status === 200) {
  55. const balanceData = await response.json()
  56. setCreditBalance(balanceData)
  57. } else if (response.status === 404) {
  58. // 用户没有额度记录,创建默认额度
  59. setCreditBalance({
  60. totalLimit: 0,
  61. usedAmount: 0,
  62. availableAmount: 0,
  63. isEnabled: false
  64. })
  65. } else {
  66. // 其他错误情况
  67. setCreditBalance({
  68. totalLimit: 0,
  69. usedAmount: 0,
  70. availableAmount: 0,
  71. isEnabled: false
  72. })
  73. }
  74. } catch (error) {
  75. console.error('获取用户额度失败:', error)
  76. setCreditBalance({
  77. totalLimit: 0,
  78. usedAmount: 0,
  79. availableAmount: 0,
  80. isEnabled: false
  81. })
  82. } finally {
  83. setIsCreditBalanceLoading(false)
  84. }
  85. }
  86. // 组件加载时获取额度信息
  87. useEffect(() => {
  88. fetchCreditBalance()
  89. }, [])
  90. // 获取微信支付参数(手动触发)
  91. const {
  92. mutateAsync: fetchWechatPaymentParams,
  93. data: paymentData,
  94. isLoading: paymentLoading,
  95. error: paymentError
  96. } = useMutation({
  97. mutationFn: async () => {
  98. if (!orderId) throw new Error('订单ID无效')
  99. // 调用后端API获取微信支付参数
  100. const response = await paymentClient.payment.$post({
  101. json: {
  102. orderId: orderId,
  103. totalAmount: Math.round(amount * 100), // 转换为分
  104. description: `订单支付 - ${orderNo || `ORD${orderId}`}`
  105. }
  106. })
  107. if (response.status !== 200) {
  108. throw new Error(`获取支付参数失败: ${response.status}`)
  109. }
  110. const responseData = await response.json()
  111. // 转换响应数据格式
  112. const paymentData: PaymentData = {
  113. timeStamp: responseData.timeStamp,
  114. nonceStr: responseData.nonceStr,
  115. package: responseData.package,
  116. signType: responseData.signType,
  117. paySign: responseData.paySign
  118. }
  119. return paymentData
  120. }
  121. })
  122. // 支付状态管理
  123. const paymentStateManager = PaymentStateManager.getInstance()
  124. const rateLimiter = PaymentRateLimiter.getInstance()
  125. // 处理额度支付
  126. const handleCreditPayment = async () => {
  127. if (!orderId) {
  128. setErrorMessage('订单信息不完整')
  129. return
  130. }
  131. if (!creditBalance?.isEnabled) {
  132. setErrorMessage('额度支付未启用')
  133. return
  134. }
  135. // 检查额度是否足够(使用订单金额检查)
  136. if (creditBalance.availableAmount < amount) {
  137. setErrorMessage(`额度不足,可用额度: ¥${creditBalance.availableAmount.toFixed(2)}`)
  138. return
  139. }
  140. setIsProcessing(true)
  141. setErrorMessage('')
  142. setPaymentStatus(PaymentStatus.PROCESSING)
  143. try {
  144. const response = await creditBalanceClient.payment.$post({
  145. json: {
  146. referenceId: orderId.toString(), // 传递订单ID而不是订单号
  147. remark: `订单支付 - ${orderNo || `ORD${orderId}`}`
  148. }
  149. })
  150. if (response.status === 200) {
  151. // 额度支付成功
  152. setPaymentStatus(PaymentStatus.SUCCESS)
  153. // 更新本地额度信息
  154. const updatedBalance = await response.json()
  155. setCreditBalance(updatedBalance)
  156. // 跳转到支付成功页面
  157. setTimeout(() => {
  158. Taro.redirectTo({
  159. url: `/pages/payment-success/index?orderId=${orderId}&amount=${amount}&paymentMethod=credit`
  160. })
  161. }, 1500)
  162. } else {
  163. const errorData = await response.json()
  164. setPaymentStatus(PaymentStatus.FAILED)
  165. setErrorMessage(errorData.message || '额度支付失败')
  166. }
  167. } catch (error: any) {
  168. console.error('额度支付处理异常:', error)
  169. setPaymentStatus(PaymentStatus.FAILED)
  170. setErrorMessage(error.message || '额度支付异常')
  171. } finally {
  172. setIsProcessing(false)
  173. }
  174. }
  175. // 处理支付
  176. const handlePayment = async () => {
  177. if (selectedPaymentMethod === PaymentMethod.CREDIT) {
  178. await handleCreditPayment()
  179. return
  180. }
  181. // 微信支付逻辑
  182. if (!orderId) {
  183. setErrorMessage('订单信息不完整')
  184. return
  185. }
  186. // 检查频率限制
  187. const rateLimit = rateLimiter.isRateLimited(orderId)
  188. if (rateLimit.limited) {
  189. setErrorMessage(`支付频率过高,请${Math.ceil(rateLimit.remainingTime! / 1000)}秒后重试`)
  190. return
  191. }
  192. setIsProcessing(true)
  193. setErrorMessage('')
  194. setPaymentStatus(PaymentStatus.PROCESSING)
  195. try {
  196. // 先获取微信支付参数
  197. const wechatPaymentData = await fetchWechatPaymentParams()
  198. if (!wechatPaymentData) {
  199. setPaymentStatus(PaymentStatus.FAILED)
  200. setErrorMessage('获取支付参数失败')
  201. return
  202. }
  203. paymentStateManager.setPaymentState(orderId, PaymentStatus.PROCESSING)
  204. // 记录支付尝试
  205. rateLimiter.recordAttempt(orderId)
  206. // 调用微信支付
  207. const paymentResult = await requestWechatPayment(wechatPaymentData)
  208. if (paymentResult.success) {
  209. // 支付成功
  210. setPaymentStatus(PaymentStatus.SUCCESS)
  211. paymentStateManager.setPaymentState(orderId, PaymentStatus.SUCCESS)
  212. // 清除频率限制记录
  213. rateLimiter.clearAttempts(orderId)
  214. // 跳转到支付成功页面
  215. setTimeout(() => {
  216. Taro.redirectTo({
  217. url: `/pages/payment-success/index?orderId=${orderId}&amount=${amount}`
  218. })
  219. }, 1500)
  220. } else {
  221. // 支付失败
  222. setPaymentStatus(PaymentStatus.FAILED)
  223. paymentStateManager.setPaymentState(orderId, PaymentStatus.FAILED)
  224. if (paymentResult.type === 'cancel') {
  225. setErrorMessage('用户取消支付')
  226. } else {
  227. setErrorMessage(paymentResult.message || '支付失败')
  228. }
  229. }
  230. } catch (error: any) {
  231. console.error('支付处理异常:', error)
  232. setPaymentStatus(PaymentStatus.FAILED)
  233. paymentStateManager.setPaymentState(orderId, PaymentStatus.FAILED)
  234. setErrorMessage(error.message || '支付异常')
  235. } finally {
  236. setIsProcessing(false)
  237. }
  238. }
  239. // 重试支付
  240. const handleRetryPayment = async () => {
  241. if (selectedPaymentMethod === PaymentMethod.CREDIT) {
  242. await handleCreditPayment()
  243. return
  244. }
  245. if (!orderId) return
  246. setIsProcessing(true)
  247. setErrorMessage('')
  248. try {
  249. // 先获取微信支付参数
  250. const wechatPaymentData = await fetchWechatPaymentParams()
  251. if (!wechatPaymentData) {
  252. setPaymentStatus(PaymentStatus.FAILED)
  253. setErrorMessage('获取支付参数失败')
  254. return
  255. }
  256. const retryResult = await retryPayment(
  257. () => requestWechatPayment(wechatPaymentData),
  258. 3,
  259. 1000
  260. )
  261. if (retryResult.success) {
  262. setPaymentStatus(PaymentStatus.SUCCESS)
  263. paymentStateManager.setPaymentState(orderId, PaymentStatus.SUCCESS)
  264. // 跳转到支付成功页面
  265. setTimeout(() => {
  266. Taro.redirectTo({
  267. url: `/pages/payment-success/index?orderId=${orderId}&amount=${amount}`
  268. })
  269. }, 1500)
  270. } else {
  271. setPaymentStatus(PaymentStatus.FAILED)
  272. setErrorMessage(retryResult.message || '支付重试失败')
  273. }
  274. } catch (error: any) {
  275. console.error('支付重试异常:', error)
  276. setPaymentStatus(PaymentStatus.FAILED)
  277. setErrorMessage(error.message || '支付重试异常')
  278. } finally {
  279. setIsProcessing(false)
  280. }
  281. }
  282. // 取消支付
  283. const handleCancelPayment = () => {
  284. if (orderId) {
  285. paymentStateManager.clearPaymentState(orderId)
  286. rateLimiter.clearAttempts(orderId)
  287. }
  288. // 返回上一页
  289. Taro.navigateBack()
  290. }
  291. // 渲染支付状态
  292. const renderPaymentStatus = () => {
  293. switch (paymentStatus) {
  294. case PaymentStatus.PENDING:
  295. return (
  296. <View>
  297. <Text className="text-xl font-bold text-orange-500 block mb-2">待支付</Text>
  298. <Text className="text-sm text-gray-600 block">请确认支付信息</Text>
  299. </View>
  300. )
  301. case PaymentStatus.PROCESSING:
  302. return (
  303. <View>
  304. <Text className="text-xl font-bold text-blue-500 block mb-2">支付中...</Text>
  305. <Text className="text-sm text-gray-600 block">请稍候</Text>
  306. </View>
  307. )
  308. case PaymentStatus.SUCCESS:
  309. return (
  310. <View>
  311. <Text className="text-xl font-bold text-green-500 block mb-2">支付成功</Text>
  312. <Text className="text-sm text-gray-600 block">正在跳转...</Text>
  313. </View>
  314. )
  315. case PaymentStatus.FAILED:
  316. return (
  317. <View>
  318. <Text className="text-xl font-bold text-red-500 block mb-2">支付失败</Text>
  319. <Text className="text-sm text-gray-600 block">{errorMessage}</Text>
  320. </View>
  321. )
  322. default:
  323. return null
  324. }
  325. }
  326. if (!orderId || !amount) {
  327. return (
  328. <View className="min-h-screen bg-gray-50 flex flex-col items-center justify-center">
  329. <Text className="text-xl text-red-500 mb-8">参数错误</Text>
  330. <Button onClick={() => Taro.navigateBack()} className="w-48 h-18 bg-blue-500 text-white rounded-full text-sm">
  331. 返回
  332. </Button>
  333. </View>
  334. )
  335. }
  336. return (
  337. <View className="min-h-screen bg-gray-50">
  338. {/* 导航栏 */}
  339. <Navbar
  340. title="支付订单"
  341. leftIcon=""
  342. onClickLeft={() => {}}
  343. />
  344. <View className="p-5">
  345. {/* 头部 */}
  346. <View className="text-center py-6 bg-white rounded-2xl mb-5">
  347. <Text className="text-2xl font-bold text-gray-800" data-testid="payment-page-title">支付订单</Text>
  348. </View>
  349. {/* 订单信息 */}
  350. <View className="bg-white rounded-2xl p-6 mb-5" data-testid="order-info">
  351. <View className="flex justify-between items-center mb-4">
  352. <Text className="text-sm text-gray-600">订单号:</Text>
  353. <Text className="text-sm text-gray-800" data-testid="order-no">{orderNo || `ORD${orderId}`}</Text>
  354. </View>
  355. <View className="flex justify-between items-center">
  356. <Text className="text-sm text-gray-600">支付金额:</Text>
  357. <Text className="text-2xl font-bold text-orange-500" data-testid="payment-amount">¥{amount.toFixed(2)}</Text>
  358. </View>
  359. </View>
  360. {/* 支付方式选择 */}
  361. <View className="bg-white rounded-2xl p-6 mb-5">
  362. <Text className="text-sm font-bold text-gray-800 block mb-4">选择支付方式</Text>
  363. {/* 微信支付选项 */}
  364. <View
  365. className={`flex items-center justify-between p-4 mb-3 rounded-xl border-2 ${
  366. selectedPaymentMethod === PaymentMethod.WECHAT
  367. ? 'border-blue-500 bg-blue-50'
  368. : 'border-gray-200'
  369. }`}
  370. onClick={() => setSelectedPaymentMethod(PaymentMethod.WECHAT)}
  371. data-testid="wechat-payment-option"
  372. >
  373. <View className="flex items-center">
  374. <View className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center mr-3">
  375. <Text className="text-green-600 text-lg">💰</Text>
  376. </View>
  377. <View>
  378. <Text className="text-sm font-bold text-gray-800">微信支付</Text>
  379. <Text className="text-xs text-gray-500">使用微信支付完成付款</Text>
  380. </View>
  381. </View>
  382. {selectedPaymentMethod === PaymentMethod.WECHAT && (
  383. <View className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center" data-testid="wechat-selected">
  384. <Text className="text-white text-xs">✓</Text>
  385. </View>
  386. )}
  387. </View>
  388. {/* 额度支付选项 - 只在额度满足时才显示 */}
  389. {creditBalance?.isEnabled && creditBalance?.availableAmount >= amount && (
  390. <View
  391. className={`flex items-center justify-between p-4 rounded-xl border-2 ${
  392. selectedPaymentMethod === PaymentMethod.CREDIT
  393. ? 'border-blue-500 bg-blue-50'
  394. : 'border-gray-200'
  395. }`}
  396. onClick={() => setSelectedPaymentMethod(PaymentMethod.CREDIT)}
  397. data-testid="credit-payment-option"
  398. >
  399. <View className="flex items-center">
  400. <View className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center mr-3">
  401. <Text className="text-purple-600 text-lg">💳</Text>
  402. </View>
  403. <View>
  404. <Text className="text-sm font-bold text-gray-800">额度支付</Text>
  405. <Text className="text-xs text-gray-500" data-testid="available-amount-text">
  406. 使用信用额度支付
  407. </Text>
  408. </View>
  409. </View>
  410. {selectedPaymentMethod === PaymentMethod.CREDIT && (
  411. <View className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center" data-testid="credit-selected">
  412. <Text className="text-white text-xs">✓</Text>
  413. </View>
  414. )}
  415. </View>
  416. )}
  417. {/* 额度支付说明 */}
  418. {selectedPaymentMethod === PaymentMethod.CREDIT && creditBalance && (
  419. <View className="mt-4 p-3 bg-blue-50 rounded-lg" data-testid="credit-payment-details">
  420. <Text className="text-xs text-blue-700">
  421. • 使用信用额度支付,无需立即付款
  422. </Text>
  423. </View>
  424. )}
  425. </View>
  426. {/* 支付状态 */}
  427. <View className="bg-white rounded-2xl p-8 mb-5 text-center">
  428. {renderPaymentStatus()}
  429. </View>
  430. {/* 支付按钮 */}
  431. <View className="mb-5">
  432. {paymentStatus === PaymentStatus.PENDING && (
  433. <Button
  434. onClick={handlePayment}
  435. disabled={isProcessing || paymentLoading || (selectedPaymentMethod === PaymentMethod.CREDIT && (!creditBalance?.isEnabled || creditBalance?.availableAmount < amount))}
  436. className={`w-full h-22 ${
  437. selectedPaymentMethod === PaymentMethod.CREDIT
  438. ? 'bg-gradient-to-r from-purple-500 to-purple-400'
  439. : 'bg-gradient-to-r from-orange-500 to-orange-400'
  440. } text-white rounded-full text-lg font-bold ${
  441. isProcessing ? 'bg-gray-400' : ''
  442. }`}
  443. data-testid="pay-button"
  444. >
  445. {isProcessing ? '支付中...' :
  446. selectedPaymentMethod === PaymentMethod.CREDIT
  447. ? `额度支付 ¥${amount.toFixed(2)}`
  448. : `微信支付 ¥${amount.toFixed(2)}`
  449. }
  450. </Button>
  451. )}
  452. {paymentStatus === PaymentStatus.FAILED && (
  453. <View className="flex gap-4">
  454. <Button onClick={handleRetryPayment} className="flex-1 h-22 bg-blue-500 text-white rounded-full text-sm">
  455. 重试支付
  456. </Button>
  457. <Button onClick={handleCancelPayment} className="flex-1 h-22 bg-gray-100 text-gray-600 border border-gray-300 rounded-full text-sm">
  458. 取消支付
  459. </Button>
  460. </View>
  461. )}
  462. {paymentStatus === PaymentStatus.PROCESSING && (
  463. <Button disabled className="w-full h-22 bg-gray-100 text-gray-500 rounded-full text-sm">
  464. 支付处理中...
  465. </Button>
  466. )}
  467. {paymentStatus === PaymentStatus.SUCCESS && (
  468. <Button disabled className="w-full h-22 bg-gray-100 text-gray-500 rounded-full text-sm">
  469. 支付成功
  470. </Button>
  471. )}
  472. </View>
  473. {/* 支付说明 */}
  474. <View className="bg-white rounded-2xl p-6">
  475. <Text className="text-sm font-bold text-gray-800 block mb-4">支付说明</Text>
  476. <Text className="text-xs text-gray-600 leading-relaxed whitespace-pre-line">
  477. • 请确保网络连接正常
  478. {'\n'}
  479. • 支付过程中请勿关闭页面
  480. {'\n'}
  481. • 如遇支付问题,请尝试重新支付
  482. {'\n'}
  483. • 支付成功后会自动跳转
  484. </Text>
  485. </View>
  486. </View>
  487. </View>
  488. )
  489. }
  490. export default PaymentPage