Quellcode durchsuchen

fix(payment-page): 优化额度显示和支付流程

- 移除可用额度显示,只在额度满足时显示额度支付按钮
- 将微信支付API调用从自动改为手动触发
- 更新相关测试文件

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname vor 3 Tagen
Ursprung
Commit
221c5a3733

+ 33 - 0
docs/stories/004.003.integrate-credit-payment.story.md

@@ -52,6 +52,8 @@ Ready for Review
   - [x] 实现额度支付选择逻辑:检查用户可用额度
   - [x] 额度为0的用户禁用额度支付选项
   - [x] 保持与微信支付选项的并行工作
+  - [x] **优化额度显示逻辑**:不显示可用额度,只在额度满足时显示额度支付按钮
+  - [x] **优化支付流程**:在显示额度按钮的情况下,不要默认调用微信支付接口(当前页面加载时自动调用微信支付API),等用户选择微信支付后再调用
 
 - [x] **在小程序个人中心显示欠款信息** (AC: 6)
   - [x] 检查小程序个人中心页面结构
@@ -375,6 +377,8 @@ mini/tests/
   - [ ] 测试额度为0时禁用额度支付选项
   - [ ] 测试额度支付选择逻辑
   - [ ] 测试与微信支付选项的并行工作
+  - [ ] **测试额度显示优化**:验证不显示可用额度,只在额度满足时显示额度支付按钮
+  - [ ] **测试支付流程优化**:验证在显示额度按钮的情况下,页面加载时不自动调用微信支付API,等用户选择微信支付后再调用
 
 - [ ] **创建个人中心欠款显示单元测试** (AC: 6)
   - [ ] 在 `mini/tests/unit/pages/profile/` 创建测试文件
@@ -441,6 +445,10 @@ Claude Code (d8d-model)
    - 更新小程序API客户端
      - 导入CreditBalanceRoutes类型
      - 添加creditBalanceClient导出
+   - **补充故事004.003任务**
+     - 添加"优化额度显示逻辑"任务:不显示可用额度,只在额度满足时显示额度支付按钮
+     - 添加"优化支付流程"任务:在显示额度按钮的情况下,不要默认调用微信支付接口(当前页面加载时自动调用微信支付API),等用户选择微信支付后再调用
+     - 更新测试任务,添加额度显示优化和支付流程优化的测试要求
 
 2. **已完成的工作**:
    - 在额度模块中添加`/api/credit-balance/me`路由,从上下文中获取当前用户ID
@@ -482,6 +490,21 @@ Claude Code (d8d-model)
      - **类型安全**: 已验证server包导出`CreditBalanceRoutes`类型,小程序API客户端使用该类型确保类型安全
      - **依赖关系**: 已验证订单模块和server包的package.json包含额度模块依赖
 
+7. **已完成的工作**:
+   - **优化额度显示逻辑**: 修改支付页面,不显示可用额度,只在额度满足时显示额度支付按钮
+     - 移除"可用额度: ¥X.XX"的显示
+     - 修改为只在`creditBalance?.availableAmount >= amount`时才显示额度支付选项
+     - 修改额度支付说明,移除可用额度的显示
+   - **优化支付流程**: 将微信支付API调用从自动改为手动触发
+     - 将`useQuery`改为`useMutation`,页面加载时不自动调用微信支付API
+     - 修改`handlePayment`函数,在用户选择微信支付并点击支付按钮时才调用API
+     - 更新`handleRetryPayment`函数,适配新的支付流程
+   - **更新测试文件**: 更新所有相关测试,验证新的额度和支付逻辑
+     - 更新额度显示相关的测试,验证不显示可用额度
+     - 添加测试验证页面加载时不自动调用微信支付API
+     - 添加测试验证选择微信支付并点击支付按钮时才调用API
+     - 所有测试通过
+
 ### File List
 **已修改的文件**:
 1. `packages/orders-module-mt/src/entities/order.mt.entity.ts` - 更新pay_type字段注释
@@ -512,5 +535,15 @@ Claude Code (d8d-model)
 2. `mini/tests/unit/pages/payment/credit-payment.test.tsx` - 修复文本匹配和定时器问题
 3. `mini/tests/integration/credit-payment-flow.test.tsx` - 修复文本匹配和按钮禁用检查
 
+**本次优化修改的文件**:
+1. `mini/src/pages/payment/index.tsx` - 优化额度显示逻辑和支付流程
+   - 移除可用额度的显示
+   - 只在额度满足时显示额度支付按钮
+   - 将微信支付API调用从自动改为手动触发
+2. `mini/tests/unit/pages/payment/credit-payment.test.tsx` - 更新测试以验证新的额度和支付逻辑
+   - 更新额度显示相关的测试
+   - 添加测试验证页面加载时不自动调用微信支付API
+   - 添加测试验证选择微信支付并点击支付按钮时才调用API
+
 ## QA Results
 *此部分由QA代理在审查完成后填写*

+ 60 - 50
mini/src/pages/payment/index.tsx

@@ -6,7 +6,7 @@
 import Taro, { useRouter } from '@tarojs/taro'
 import { useState, useEffect } from 'react'
 import { View, Text } from '@tarojs/components'
-import { useQuery } from '@tanstack/react-query'
+import { useQuery, useMutation } from '@tanstack/react-query'
 import { Button } from '@/components/ui/button'
 import { Navbar } from '@/components/ui/navbar'
 import {
@@ -98,10 +98,14 @@ const PaymentPage = () => {
     fetchCreditBalance()
   }, [])
 
-  // 获取支付参数
-  const { data: paymentData, isLoading: paymentLoading } = useQuery({
-    queryKey: ['payment-params', orderId],
-    queryFn: async () => {
+  // 获取微信支付参数(手动触发)
+  const {
+    mutateAsync: fetchWechatPaymentParams,
+    data: paymentData,
+    isLoading: paymentLoading,
+    error: paymentError
+  } = useMutation({
+    mutationFn: async () => {
       if (!orderId) throw new Error('订单ID无效')
 
       // 调用后端API获取微信支付参数
@@ -129,8 +133,7 @@ const PaymentPage = () => {
       }
 
       return paymentData
-    },
-    enabled: !!orderId && paymentStatus === PaymentStatus.PENDING
+    }
   })
 
   // 支付状态管理
@@ -203,8 +206,8 @@ const PaymentPage = () => {
     }
 
     // 微信支付逻辑
-    if (!paymentData || !orderId) {
-      setErrorMessage('支付参数不完整')
+    if (!orderId) {
+      setErrorMessage('订单信息不完整')
       return
     }
 
@@ -218,14 +221,24 @@ const PaymentPage = () => {
     setIsProcessing(true)
     setErrorMessage('')
     setPaymentStatus(PaymentStatus.PROCESSING)
-    paymentStateManager.setPaymentState(orderId, 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(paymentData)
+      const paymentResult = await requestWechatPayment(wechatPaymentData)
 
       if (paymentResult.success) {
         // 支付成功
@@ -269,14 +282,23 @@ const PaymentPage = () => {
       return
     }
 
-    if (!paymentData || !orderId) return
+    if (!orderId) return
 
     setIsProcessing(true)
     setErrorMessage('')
 
     try {
+      // 先获取微信支付参数
+      const wechatPaymentData = await fetchWechatPaymentParams()
+
+      if (!wechatPaymentData) {
+        setPaymentStatus(PaymentStatus.FAILED)
+        setErrorMessage('获取支付参数失败')
+        return
+      }
+
       const retryResult = await retryPayment(
-        () => requestWechatPayment(paymentData),
+        () => requestWechatPayment(wechatPaymentData),
         3,
         1000
       )
@@ -419,53 +441,41 @@ const PaymentPage = () => {
           )}
         </View>
 
-        {/* 额度支付选项 */}
-        <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'
-          } ${(!creditBalance?.isEnabled || creditBalance?.availableAmount <= 0 || creditBalance?.availableAmount < amount) ? 'opacity-50' : ''}`}
-          onClick={() => {
-            if (creditBalance?.isEnabled && creditBalance?.availableAmount > 0 && creditBalance?.availableAmount >= amount) {
-              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>
-              {isCreditBalanceLoading ? (
-                <Text className="text-xs text-gray-500">加载中...</Text>
-              ) : creditBalance?.isEnabled ? (
+        {/* 额度支付选项 - 只在额度满足时才显示 */}
+        {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">
-                  可用额度: ¥{creditBalance?.availableAmount?.toFixed(2) || '0.00'}
-                  {creditBalance?.availableAmount < amount && ` (不足)`}
+                  使用信用额度支付
                 </Text>
-              ) : (
-                <Text className="text-xs text-red-500" data-testid="credit-disabled-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 ? (
-            <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>
-          ) : (!creditBalance?.isEnabled || creditBalance?.availableAmount <= 0 || creditBalance?.availableAmount < amount) && (
-            <Text className="text-xs text-gray-400">不可用</Text>
-          )}
-        </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">
               • 使用信用额度支付,无需立即付款{'\n'}
-              • 可用额度: ¥{creditBalance.availableAmount.toFixed(2)}{'\n'}
               • 总额度: ¥{creditBalance.totalLimit.toFixed(2)}{'\n'}
               • 已用额度: ¥{creditBalance.usedAmount.toFixed(2)}
             </Text>

+ 115 - 30
mini/tests/unit/pages/payment/credit-payment.test.tsx

@@ -129,13 +129,18 @@ describe('支付页面额度支付功能测试', () => {
     expect(screen.getByTestId('order-no')).toHaveTextContent('ORD123456')
     expect(screen.getByTestId('payment-amount')).toHaveTextContent('¥100.00')
 
+    // 等待额度加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
+    })
+
     // 验证支付方式选项
     expect(screen.getByTestId('wechat-payment-option')).toBeInTheDocument()
     expect(screen.getByTestId('credit-payment-option')).toBeInTheDocument()
   })
 
-  test('应该显示额度支付选项和可用额度', async () => {
-    // Mock 额度查询返回正常数据
+  test('应该显示额度支付选项(只在额度满足时)', async () => {
+    // Mock 额度查询返回正常数据(额度足够)
     const mockCreditBalance = createTestCreditBalance({ availableAmount: 800 })
     ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
       status: 200,
@@ -150,15 +155,16 @@ describe('支付页面额度支付功能测试', () => {
 
     // 等待额度加载完成
     await waitFor(() => {
-      expect(screen.getByTestId('available-amount-text')).toHaveTextContent('可用额度: ¥800.00')
+      expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
     })
 
-    // 验证额度支付选项可用
+    // 验证额度支付选项显示
     const creditPaymentOption = screen.getByTestId('credit-payment-option')
+    expect(creditPaymentOption).toBeInTheDocument()
     expect(creditPaymentOption).not.toHaveClass('opacity-50')
   })
 
-  test('额度为0时应该禁用额度支付选项', async () => {
+  test('额度为0时不应该显示额度支付选项', async () => {
     // Mock 额度查询返回额度为0的数据
     const mockCreditBalance = createTestCreditBalance({
       totalLimit: 0,
@@ -177,17 +183,19 @@ describe('支付页面额度支付功能测试', () => {
       </TestWrapper>
     )
 
-    // 等待额度加载完成
+    // 等待额度加载完成 - 现在额度未启用时,额度支付选项根本不显示
+    // 所以没有特定的文本需要等待
     await waitFor(() => {
-      expect(screen.getByTestId('credit-disabled-text')).toHaveTextContent('额度未启用')
+      // 验证只有微信支付选项显示
+      expect(screen.getByTestId('wechat-payment-option')).toBeInTheDocument()
     })
 
-    // 验证额度支付选项被禁用
-    const creditPaymentOption = screen.getByTestId('credit-payment-option')
-    expect(creditPaymentOption).toHaveClass('opacity-50')
+    // 验证额度支付选项不显示
+    expect(screen.queryByTestId('credit-payment-option')).not.toBeInTheDocument()
+    expect(screen.queryByTestId('credit-disabled-text')).not.toBeInTheDocument()
   })
 
-  test('额度不足时应该显示不足提示', async () => {
+  test('额度不足时不应该显示额度支付选项', async () => {
     // Mock 额度查询返回额度不足的数据
     const mockCreditBalance = createTestCreditBalance({
       totalLimit: 50,
@@ -208,12 +216,12 @@ describe('支付页面额度支付功能测试', () => {
 
     // 等待额度加载完成
     await waitFor(() => {
-      expect(screen.getByTestId('available-amount-text')).toHaveTextContent('可用额度: ¥10.00 (不足)')
+      // 额度不足时,额度支付选项不应该显示
+      expect(screen.queryByTestId('credit-payment-option')).not.toBeInTheDocument()
     })
 
-    // 验证额度支付选项被禁用
-    const creditPaymentOption = screen.getByTestId('credit-payment-option')
-    expect(creditPaymentOption).toHaveClass('opacity-50')
+    // 验证只有微信支付选项显示
+    expect(screen.getByTestId('wechat-payment-option')).toBeInTheDocument()
   })
 
   test('应该可以切换支付方式', async () => {
@@ -232,7 +240,7 @@ describe('支付页面额度支付功能测试', () => {
 
     // 等待额度加载完成
     await waitFor(() => {
-      expect(screen.getByTestId('available-amount-text')).toHaveTextContent('可用额度: ¥800.00')
+      expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
     })
 
     // 初始应该是微信支付选中
@@ -254,7 +262,7 @@ describe('支付页面额度支付功能测试', () => {
     expect(screen.getByTestId('pay-button')).toHaveTextContent('额度支付 ¥100.00')
   })
 
-  test('选择额度支付时应该显示额度详情', async () => {
+  test('选择额度支付时应该显示额度详情(不显示可用额度)', async () => {
     // Mock 额度查询返回正常数据
     const mockCreditBalance = createTestCreditBalance()
     ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
@@ -270,24 +278,25 @@ describe('支付页面额度支付功能测试', () => {
 
     // 等待额度加载完成
     await waitFor(() => {
-      expect(screen.getByText('可用额度: ¥800.00')).toBeInTheDocument()
+      expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
     })
 
     // 点击额度支付选项
-    const creditOption = screen.getByText('额度支付').closest('[class*="border-gray-200"]')
-    fireEvent.click(creditOption!)
+    const creditOption = screen.getByTestId('credit-payment-option')
+    fireEvent.click(creditOption)
 
-    // 验证显示额度详情
+    // 验证显示额度详情(不包含可用额度)
     await waitFor(() => {
       // 使用data-testid查询额度详情容器
       const creditDetails = screen.getByTestId('credit-payment-details')
       expect(creditDetails).toBeInTheDocument()
 
-      // 验证容器中包含所有额度信息
+      // 验证容器中包含额度信息(不包含可用额度)
       expect(creditDetails).toHaveTextContent(/使用信用额度支付,无需立即付款/)
-      expect(creditDetails).toHaveTextContent(/可用额度: ¥800\.00/)
       expect(creditDetails).toHaveTextContent(/总额度: ¥1000\.00/)
       expect(creditDetails).toHaveTextContent(/已用额度: ¥200\.00/)
+      // 不应该包含可用额度
+      expect(creditDetails).not.toHaveTextContent(/可用额度:/)
     })
   })
 
@@ -314,12 +323,12 @@ describe('支付页面额度支付功能测试', () => {
 
     // 等待额度加载完成
     await waitFor(() => {
-      expect(screen.getByText('可用额度: ¥800.00')).toBeInTheDocument()
+      expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
     })
 
     // 点击额度支付选项
-    const creditOption = screen.getByText('额度支付').closest('[class*="border-gray-200"]')
-    fireEvent.click(creditOption!)
+    const creditOption = screen.getByTestId('credit-payment-option')
+    fireEvent.click(creditOption)
 
     // 点击支付按钮
     const payButton = screen.getByText('额度支付 ¥100.00')
@@ -368,12 +377,12 @@ describe('支付页面额度支付功能测试', () => {
 
     // 等待额度加载完成
     await waitFor(() => {
-      expect(screen.getByText('可用额度: ¥800.00')).toBeInTheDocument()
+      expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
     })
 
     // 点击额度支付选项
-    const creditOption = screen.getByText('额度支付').closest('[class*="border-gray-200"]')
-    fireEvent.click(creditOption!)
+    const creditOption = screen.getByTestId('credit-payment-option')
+    fireEvent.click(creditOption)
 
     // 点击支付按钮
     const payButton = screen.getByText('额度支付 ¥100.00')
@@ -408,7 +417,7 @@ describe('支付页面额度支付功能测试', () => {
 
     // 等待额度加载完成
     await waitFor(() => {
-      expect(screen.getByText('可用额度: ¥800.00')).toBeInTheDocument()
+      expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
     })
 
     // 验证两个支付选项都存在
@@ -440,4 +449,80 @@ describe('支付页面额度支付功能测试', () => {
       expect(screen.getByTestId('pay-button')).toHaveTextContent('微信支付 ¥100.00')
     })
   })
+
+  test('页面加载时不应该自动调用微信支付API', async () => {
+    // Mock 额度查询返回正常数据
+    const mockCreditBalance = createTestCreditBalance()
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(mockCreditBalance),
+    })
+
+    // 获取paymentClient mock
+    const { paymentClient } = require('@/api')
+
+    render(
+      <TestWrapper>
+        <PaymentPage />
+      </TestWrapper>
+    )
+
+    // 等待额度加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
+    })
+
+    // 验证微信支付API没有被调用(页面加载时不应该自动调用)
+    expect(paymentClient.payment.$post).not.toHaveBeenCalled()
+
+    // 验证支付按钮可用
+    const payButton = screen.getByTestId('pay-button')
+    expect(payButton).not.toBeDisabled()
+    expect(payButton).toHaveTextContent('微信支付 ¥100.00')
+  })
+
+  test('选择微信支付并点击支付按钮时才调用微信支付API', async () => {
+    // Mock 额度查询返回正常数据
+    const mockCreditBalance = createTestCreditBalance()
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(mockCreditBalance),
+    })
+
+    // Mock 微信支付参数
+    const { paymentClient } = require('@/api')
+    ;(paymentClient.payment.$post as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(createTestPaymentData()),
+    })
+
+    render(
+      <TestWrapper>
+        <PaymentPage />
+      </TestWrapper>
+    )
+
+    // 等待额度加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('available-amount-text')).toHaveTextContent('使用信用额度支付')
+    })
+
+    // 初始时微信支付API不应该被调用
+    expect(paymentClient.payment.$post).not.toHaveBeenCalled()
+
+    // 点击支付按钮(默认选中微信支付)
+    const payButton = screen.getByTestId('pay-button')
+    fireEvent.click(payButton)
+
+    // 验证微信支付API被调用
+    await waitFor(() => {
+      expect(paymentClient.payment.$post).toHaveBeenCalledWith({
+        json: {
+          orderId: 123,
+          totalAmount: 10000, // 100元转换为分
+          description: '订单支付 - ORD123456',
+        },
+      })
+    })
+  })
 })