Просмотр исходного кода

✨ feat(mini): 合并信用支付功能分支并集成额度管理

- 合并 epic-004-credit-payment 分支到 mini-multi-tenant-mall
- 移除开发环境配置文件并更新 .gitignore
- 在 API 客户端中添加信用额度客户端支持
- 在个人中心页面集成用户信用额度查询与欠款信息显示
- 添加 Taro mock 的 redirectTo 方法支持
- 新增额度恢复集成测试和欠款显示单元测试
- 在支付模块中更新订单支付状态时记录支付类型
- 扩展订单模块支付类型定义,支持额度支付和微信支付
- 在订单取消时增加额度恢复逻辑
- 在服务端集成信用额度模块路由
- 在用户管理界面添加信用额度管理功能
- 更新依赖配置,添加信用额度相关模块
- 在管理后台初始化信用额度 API 客户端
yourname 1 месяц назад
Родитель
Сommit
1963549504

+ 0 - 10
mini/.env.development

@@ -1,10 +0,0 @@
-# 配置文档参考 https://taro-docs.jd.com/docs/next/env-mode-config
-# TARO_APP_ID="开发环境下的小程序 AppID"
-
-# API配置
-# 需换成当前项目的
-TARO_APP_API_BASE_URL=https://d8d-ai-vscode-8080-186-175-template-22-group.r.d8d.fun
-TARO_APP_API_VERSION=v1
-
-# 租户ID
-TARO_APP_TENANT_ID=1

+ 1 - 1
mini/.gitignore

@@ -6,5 +6,5 @@ node_modules/
 .DS_Store
 .swc
 *.local
-!.env.development
+.env.development
 !.env.production

+ 6 - 2
mini/src/api.ts

@@ -11,7 +11,8 @@ import type {
   MerchantRoutes,
   AreaRoutes,
   AdvertisementRoutes,
-  PaymentRoutes
+  PaymentRoutes,
+  CreditBalanceRoutes
 } from '@d8d/server'
 import { rpcClient } from './utils/rpc-client'
 
@@ -36,4 +37,7 @@ export const areaClient = rpcClient<AreaRoutes>().api.v1.areas
 export const advertisementClient = rpcClient<AdvertisementRoutes>().api.v1.advertisements
 
 // 支付客户端
-export const paymentClient = rpcClient<PaymentRoutes>().api.v1.payments
+export const paymentClient = rpcClient<PaymentRoutes>().api.v1.payments
+
+// 信用额度客户端
+export const creditBalanceClient = rpcClient<CreditBalanceRoutes>().api.v1['credit-balance']

+ 106 - 1
mini/src/pages/profile/index.tsx

@@ -1,6 +1,7 @@
-import { useState } from 'react'
+import { useState, useEffect } from 'react'
 import { View, Text, ScrollView } from '@tarojs/components'
 import Taro from '@tarojs/taro'
+import { useQuery } from '@tanstack/react-query'
 import { TabBarLayout } from '@/layouts/tab-bar-layout'
 import { useAuth } from '@/utils/auth'
 import { cn } from '@/utils/cn'
@@ -14,6 +15,7 @@ import TDesignCellGroup from '@/components/tdesign/cell-group'
 import TDesignCell from '@/components/tdesign/cell'
 import TDesignPopup from '@/components/tdesign/popup'
 import TDesignIcon from '@/components/tdesign/icon'
+import { creditBalanceClient } from '@/api'
 import './index.css'
 
 const ProfilePage: React.FC = () => {
@@ -21,6 +23,45 @@ const ProfilePage: React.FC = () => {
   const [updatingAvatar, setUpdatingAvatar] = useState(false)
   const [showCustomerService, setShowCustomerService] = useState(false)
 
+  // 查询用户信用额度
+  const {
+    data: creditBalance,
+    isLoading: creditBalanceLoading,
+    error: creditBalanceError,
+    refetch: refetchCreditBalance
+  } = useQuery({
+    queryKey: ['credit-balance', userProfile?.id],
+    queryFn: async () => {
+      if (!userProfile?.id) {
+        throw new Error('用户未登录')
+      }
+
+      try {
+        const response = await creditBalanceClient.me.$get({})
+
+        if (response.status === 200) {
+          const balanceData = await response.json()
+          return balanceData
+        } else if (response.status === 404) {
+          // 用户没有额度记录,返回默认值
+          return {
+            totalLimit: 0,
+            usedAmount: 0,
+            availableAmount: 0,
+            isEnabled: false
+          }
+        } else {
+          throw new Error(`获取额度失败: ${response.status}`)
+        }
+      } catch (error) {
+        console.error('查询用户额度失败:', error)
+        throw error
+      }
+    },
+    enabled: !!userProfile?.id,
+    retry: 1
+  })
+
   const handleLogout = async () => {
     try {
       Taro.showModal({
@@ -234,6 +275,70 @@ const ProfilePage: React.FC = () => {
           />
         </View>
 
+        {/* 欠款信息卡片 */}
+        {creditBalance && creditBalance.usedAmount > 0 && (
+          <View className="px-4 pt-4">
+            <View className="bg-white rounded-2xl overflow-hidden">
+              <View className="p-5 border-b border-gray-100">
+                <View className="flex items-center justify-between">
+                  <Text className="text-lg font-bold text-gray-800">欠款信息</Text>
+                  {creditBalanceLoading ? (
+                    <View className="i-heroicons-arrow-path-20-solid animate-spin w-5 h-5 text-blue-500" />
+                  ) : creditBalanceError ? (
+                    <Button
+                      size="sm"
+                      variant="ghost"
+                      onClick={() => refetchCreditBalance()}
+                      className="text-xs text-red-500"
+                    >
+                      重试
+                    </Button>
+                  ) : null}
+                </View>
+              </View>
+
+              <View className="p-5">
+                {creditBalanceLoading ? (
+                  <View className="flex items-center justify-center py-8">
+                    <View className="i-heroicons-arrow-path-20-solid animate-spin w-6 h-6 text-blue-500 mr-2" />
+                    <Text className="text-gray-500">加载中...</Text>
+                  </View>
+                ) : creditBalanceError ? (
+                  <View className="flex flex-col items-center justify-center py-8">
+                    <View className="i-heroicons-exclamation-circle-20-solid w-8 h-8 text-red-400 mb-2" />
+                    <Text className="text-gray-500 mb-2">加载失败</Text>
+                    <Button
+                      size="sm"
+                      variant="outline"
+                      onClick={() => refetchCreditBalance()}
+                    >
+                      重新加载
+                    </Button>
+                  </View>
+                ) : creditBalance && creditBalance.usedAmount > 0 ? (
+                  <View className="flex items-center justify-between">
+                    <View className="flex items-center">
+                      <View className="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center mr-3">
+                        <Text className="text-red-600 text-lg">📊</Text>
+                      </View>
+                      <View>
+                        <Text className="text-sm text-gray-600">累计欠款</Text>
+                        <Text className="text-2xl font-bold text-red-600">
+                          ¥{creditBalance.usedAmount.toFixed(2)}
+                        </Text>
+                      </View>
+                    </View>
+                    <View className="text-right">
+                      <Text className="text-xs text-gray-500">需结清金额</Text>
+                      <Text className="text-xs text-red-500 mt-1">请及时还款</Text>
+                    </View>
+                  </View>
+                ) : null}
+              </View>
+            </View>
+          </View>
+        )}
+
         {/* 功能菜单 */}
         <View className="px-4 pt-4">
           {menuData.map((group, groupIndex) => (

+ 3 - 0
mini/tests/__mocks__/taroMock.ts

@@ -22,6 +22,7 @@ export const mockUseShareTimeline = jest.fn()
 export const mockGetCurrentInstance = jest.fn()
 export const mockGetCurrentPages = jest.fn()
 export const mockGetNetworkType = jest.fn()
+export const mockRedirectTo = jest.fn()
 
 // 存储相关
 export const mockGetStorageSync = jest.fn()
@@ -54,6 +55,7 @@ export default {
   navigateBack: mockNavigateBack,
   switchTab: mockSwitchTab,
   reLaunch: mockReLaunch,
+  redirectTo: mockRedirectTo,
   useRouter: () => mockUseRouter(),
   useLoad: (callback: any) => mockUseLoad(callback),
 
@@ -103,6 +105,7 @@ export {
   mockNavigateBack as navigateBack,
   mockSwitchTab as switchTab,
   mockReLaunch as reLaunch,
+  mockRedirectTo as redirectTo,
   mockUseRouter as useRouter,
   mockUseLoad as useLoad,
   mockOpenCustomerServiceChat as openCustomerServiceChat,

+ 489 - 0
mini/tests/integration/credit-balance-restore.test.tsx

@@ -0,0 +1,489 @@
+/**
+ * 额度恢复集成测试
+ * 测试额度恢复逻辑
+ */
+
+import { render, screen, waitFor, fireEvent } from '@testing-library/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { creditBalanceClient } from '@/api'
+import { mockShowToast, mockShowModal } from '~/__mocks__/taroMock'
+
+// 由于额度恢复主要在服务端处理,这里测试前端相关的恢复逻辑
+// 包括额度查询、状态显示等
+
+// @tarojs/taro 已经在 jest.config.js 中通过 moduleNameMapper 重定向到 mock 文件
+// 不需要额外 mock
+
+// Mock API客户端
+jest.mock('@/api', () => ({
+  creditBalanceClient: {
+    me: {
+      $get: jest.fn(),
+    },
+    checkout: {
+      $post: jest.fn(),
+    },
+  },
+}))
+
+// 创建测试QueryClient
+const createTestQueryClient = () => new QueryClient({
+  defaultOptions: {
+    queries: { retry: false },
+    mutations: { retry: false },
+  },
+})
+
+// 测试包装器
+const TestWrapper = ({ children }: { children: React.ReactNode }) => (
+  <QueryClientProvider client={createTestQueryClient()}>
+    {children}
+  </QueryClientProvider>
+)
+
+// 测试数据工厂
+const createTestCreditBalance = (overrides = {}) => ({
+  totalLimit: 1000,
+  usedAmount: 200,
+  availableAmount: 800,
+  isEnabled: true,
+  ...overrides,
+})
+
+// 简单的测试组件,模拟额度显示和恢复操作
+const TestCreditBalanceComponent = () => {
+  const { data: creditBalance, isLoading, error, refetch } = useQuery({
+    queryKey: ['credit-balance-test'],
+    queryFn: async () => {
+      const response = await creditBalanceClient.me.$get({})
+      if (response.status === 200) {
+        return response.json()
+      }
+      throw new Error('查询失败')
+    },
+  })
+
+  const handleCheckout = async () => {
+    try {
+      const response = await creditBalanceClient.checkout.$post({
+        json: {
+          amount: 100,
+          referenceId: 'ORD123456',
+          remark: '结账恢复额度',
+        },
+      })
+
+      if (response.status === 200) {
+        mockShowToast({ title: '额度恢复成功', icon: 'success' })
+        refetch()
+      } else {
+        mockShowToast({ title: '额度恢复失败', icon: 'none' })
+      }
+    } catch (error) {
+      mockShowToast({ title: '额度恢复异常', icon: 'none' })
+    }
+  }
+
+  if (isLoading) {
+    return <div data-testid="loading">加载中...</div>
+  }
+
+  if (error) {
+    return (
+      <div data-testid="error">
+        <div>加载失败</div>
+        <button data-testid="retry-button" onClick={() => refetch()}>重试</button>
+      </div>
+    )
+  }
+
+  return (
+    <div data-testid="credit-balance-component">
+      <div data-testid="used-amount">已用额度: ¥{creditBalance?.usedAmount.toFixed(2)}</div>
+      <div data-testid="available-amount">可用额度: ¥{creditBalance?.availableAmount.toFixed(2)}</div>
+      <button data-testid="checkout-button" onClick={handleCheckout}>结账恢复额度</button>
+      <button data-testid="refresh-button" onClick={() => refetch()}>刷新额度</button>
+    </div>
+  )
+}
+
+// Mock useQuery
+import { useQuery } from '@tanstack/react-query'
+jest.mock('@tanstack/react-query', () => ({
+  ...jest.requireActual('@tanstack/react-query'),
+  useQuery: jest.fn(),
+}))
+
+describe('额度恢复集成测试', () => {
+  beforeEach(() => {
+    jest.clearAllMocks()
+  })
+
+  test('额度查询和显示功能', async () => {
+    // Mock 额度查询返回正常数据
+    const mockCreditBalance = createTestCreditBalance({ usedAmount: 200, availableAmount: 800 })
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(mockCreditBalance),
+    })
+
+    ;(useQuery as jest.Mock).mockReturnValue({
+      data: mockCreditBalance,
+      isLoading: false,
+      error: null,
+      refetch: jest.fn(),
+    })
+
+    render(
+      <TestWrapper>
+        <TestCreditBalanceComponent />
+      </TestWrapper>
+    )
+
+    // 验证额度信息显示
+    expect(screen.getByTestId('used-amount')).toHaveTextContent('已用额度: ¥200.00')
+    expect(screen.getByTestId('available-amount')).toHaveTextContent('可用额度: ¥800.00')
+    expect(screen.getByTestId('checkout-button')).toBeInTheDocument()
+    expect(screen.getByTestId('refresh-button')).toBeInTheDocument()
+  })
+
+  test('结账恢复额度操作', async () => {
+    // Mock 额度查询返回正常数据
+    const initialBalance = createTestCreditBalance({ usedAmount: 200, availableAmount: 800 })
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(initialBalance),
+    })
+
+    // Mock 结账恢复成功
+    const updatedBalance = createTestCreditBalance({ usedAmount: 100, availableAmount: 900 })
+    ;(creditBalanceClient.checkout.$post as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(updatedBalance),
+    })
+
+    const mockRefetch = jest.fn()
+    ;(useQuery as jest.Mock).mockReturnValue({
+      data: initialBalance,
+      isLoading: false,
+      error: null,
+      refetch: mockRefetch,
+    })
+
+    render(
+      <TestWrapper>
+        <TestCreditBalanceComponent />
+      </TestWrapper>
+    )
+
+    // 点击结账恢复按钮
+    const checkoutButton = screen.getByTestId('checkout-button')
+    fireEvent.click(checkoutButton)
+
+    // 验证调用了结账恢复API
+    await waitFor(() => {
+      expect(creditBalanceClient.checkout.$post).toHaveBeenCalledWith({
+        json: {
+          amount: 100,
+          referenceId: 'ORD123456',
+          remark: '结账恢复额度',
+        },
+      })
+    })
+
+    // 验证显示成功提示
+    await waitFor(() => {
+      expect(mockShowToast).toHaveBeenCalledWith({
+        title: '额度恢复成功',
+        icon: 'success',
+      })
+    })
+
+    // 验证重新查询额度
+    expect(mockRefetch).toHaveBeenCalled()
+  })
+
+  test('结账恢复额度失败处理', async () => {
+    // Mock 额度查询返回正常数据
+    const initialBalance = createTestCreditBalance({ usedAmount: 200, availableAmount: 800 })
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(initialBalance),
+    })
+
+    // Mock 结账恢复失败
+    ;(creditBalanceClient.checkout.$post as jest.Mock).mockResolvedValue({
+      status: 400,
+      json: () => Promise.resolve({ message: '恢复失败' }),
+    })
+
+    const mockRefetch = jest.fn()
+    ;(useQuery as jest.Mock).mockReturnValue({
+      data: initialBalance,
+      isLoading: false,
+      error: null,
+      refetch: mockRefetch,
+    })
+
+    render(
+      <TestWrapper>
+        <TestCreditBalanceComponent />
+      </TestWrapper>
+    )
+
+    // 点击结账恢复按钮
+    const checkoutButton = screen.getByTestId('checkout-button')
+    fireEvent.click(checkoutButton)
+
+    // 验证显示失败提示
+    await waitFor(() => {
+      expect(mockShowToast).toHaveBeenCalledWith({
+        title: '额度恢复失败',
+        icon: 'none',
+      })
+    })
+
+    // 验证没有重新查询额度
+    expect(mockRefetch).not.toHaveBeenCalled()
+  })
+
+  test('额度查询失败后的重试功能', async () => {
+    // Mock 额度查询第一次失败,第二次成功
+    let queryCallCount = 0
+    ;(creditBalanceClient.me.$get as jest.Mock).mockImplementation(() => {
+      queryCallCount++
+      if (queryCallCount === 1) {
+        return Promise.reject(new Error('网络错误'))
+      } else {
+        return Promise.resolve({
+          status: 200,
+          json: () => Promise.resolve(createTestCreditBalance({ usedAmount: 200 })),
+        })
+      }
+    })
+
+    const mockRefetch = jest.fn(() => {
+      // 模拟重试逻辑
+      return Promise.resolve()
+    })
+
+    // 第一次渲染:错误状态
+    ;(useQuery as jest.Mock).mockReturnValue({
+      data: null,
+      isLoading: false,
+      error: new Error('网络错误'),
+      refetch: mockRefetch,
+    })
+
+    const { rerender } = render(
+      <TestWrapper>
+        <TestCreditBalanceComponent />
+      </TestWrapper>
+    )
+
+    // 验证错误状态显示
+    expect(screen.getByTestId('error')).toBeInTheDocument()
+    expect(screen.getByTestId('retry-button')).toBeInTheDocument()
+
+    // 点击重试按钮
+    const retryButton = screen.getByTestId('retry-button')
+    fireEvent.click(retryButton)
+
+    // 验证调用了重试函数
+    expect(mockRefetch).toHaveBeenCalled()
+
+    // 模拟重试成功后的状态
+    ;(useQuery as jest.Mock).mockReturnValue({
+      data: createTestCreditBalance({ usedAmount: 200, availableAmount: 800 }),
+      isLoading: false,
+      error: null,
+      refetch: mockRefetch,
+    })
+
+    // 重新渲染
+    rerender(
+      <TestWrapper>
+        <TestCreditBalanceComponent />
+      </TestWrapper>
+    )
+
+    // 验证正常显示额度信息
+    expect(screen.getByTestId('used-amount')).toHaveTextContent('已用额度: ¥200.00')
+    expect(screen.getByTestId('available-amount')).toHaveTextContent('可用额度: ¥800.00')
+  })
+
+  test('额度恢复的幂等性验证(前端角度)', async () => {
+    // Mock 额度查询返回正常数据
+    const initialBalance = createTestCreditBalance({ usedAmount: 200, availableAmount: 800 })
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(initialBalance),
+    })
+
+    // Mock 结账恢复API,多次调用返回相同结果
+    const mockCheckoutResponse = {
+      status: 200,
+      json: () => Promise.resolve(createTestCreditBalance({ usedAmount: 100, availableAmount: 900 })),
+    }
+    ;(creditBalanceClient.checkout.$post as jest.Mock).mockResolvedValue(mockCheckoutResponse)
+
+    const mockRefetch = jest.fn()
+    ;(useQuery as jest.Mock).mockReturnValue({
+      data: initialBalance,
+      isLoading: false,
+      error: null,
+      refetch: mockRefetch,
+    })
+
+    render(
+      <TestWrapper>
+        <TestCreditBalanceComponent />
+      </TestWrapper>
+    )
+
+    // 连续点击多次结账恢复按钮
+    const checkoutButton = screen.getByTestId('checkout-button')
+    fireEvent.click(checkoutButton)
+    fireEvent.click(checkoutButton)
+    fireEvent.click(checkoutButton)
+
+    // 验证API被调用了3次
+    await waitFor(() => {
+      expect(creditBalanceClient.checkout.$post).toHaveBeenCalledTimes(3)
+    })
+
+    // 验证每次调用参数相同
+    expect(creditBalanceClient.checkout.$post).toHaveBeenNthCalledWith(1, {
+      json: {
+        amount: 100,
+        referenceId: 'ORD123456',
+        remark: '结账恢复额度',
+      },
+    })
+    expect(creditBalanceClient.checkout.$post).toHaveBeenNthCalledWith(2, {
+      json: {
+        amount: 100,
+        referenceId: 'ORD123456',
+        remark: '结账恢复额度',
+      },
+    })
+    expect(creditBalanceClient.checkout.$post).toHaveBeenNthCalledWith(3, {
+      json: {
+        amount: 100,
+        referenceId: 'ORD123456',
+        remark: '结账恢复额度',
+      },
+    })
+
+    // 验证重新查询额度被调用了3次
+    expect(mockRefetch).toHaveBeenCalledTimes(3)
+  })
+
+  test('额度数据刷新功能', async () => {
+    // Mock 额度查询
+    const initialBalance = createTestCreditBalance({ usedAmount: 200, availableAmount: 800 })
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(initialBalance),
+    })
+
+    const mockRefetch = jest.fn()
+    ;(useQuery as jest.Mock).mockReturnValue({
+      data: initialBalance,
+      isLoading: false,
+      error: null,
+      refetch: mockRefetch,
+    })
+
+    render(
+      <TestWrapper>
+        <TestCreditBalanceComponent />
+      </TestWrapper>
+    )
+
+    // 点击刷新按钮
+    const refreshButton = screen.getByTestId('refresh-button')
+    fireEvent.click(refreshButton)
+
+    // 验证调用了刷新函数
+    expect(mockRefetch).toHaveBeenCalled()
+  })
+
+  test('额度查询加载状态', () => {
+    // Mock 加载状态
+    ;(useQuery as jest.Mock).mockReturnValue({
+      data: null,
+      isLoading: true,
+      error: null,
+      refetch: jest.fn(),
+    })
+
+    render(
+      <TestWrapper>
+        <TestCreditBalanceComponent />
+      </TestWrapper>
+    )
+
+    // 验证加载状态显示
+    expect(screen.getByTestId('loading')).toHaveTextContent('加载中...')
+  })
+
+  test('额度恢复后的状态更新', async () => {
+    // 模拟额度恢复前后的状态变化
+    const beforeRestore = createTestCreditBalance({ usedAmount: 200, availableAmount: 800 })
+    const afterRestore = createTestCreditBalance({ usedAmount: 100, availableAmount: 900 })
+
+    let currentData = beforeRestore
+    const mockRefetch = jest.fn(() => {
+      currentData = afterRestore
+      return Promise.resolve()
+    })
+
+    // 第一次渲染:恢复前状态
+    ;(useQuery as jest.Mock).mockReturnValue({
+      data: currentData,
+      isLoading: false,
+      error: null,
+      refetch: mockRefetch,
+    })
+
+    const { rerender } = render(
+      <TestWrapper>
+        <TestCreditBalanceComponent />
+      </TestWrapper>
+    )
+
+    // 验证恢复前状态
+    expect(screen.getByTestId('used-amount')).toHaveTextContent('已用额度: ¥200.00')
+    expect(screen.getByTestId('available-amount')).toHaveTextContent('可用额度: ¥800.00')
+
+    // Mock 结账恢复API
+    ;(creditBalanceClient.checkout.$post as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(afterRestore),
+    })
+
+    // 点击结账恢复按钮
+    const checkoutButton = screen.getByTestId('checkout-button')
+    fireEvent.click(checkoutButton)
+
+    // 模拟恢复后的查询状态
+    ;(useQuery as jest.Mock).mockReturnValue({
+      data: afterRestore,
+      isLoading: false,
+      error: null,
+      refetch: mockRefetch,
+    })
+
+    // 重新渲染
+    rerender(
+      <TestWrapper>
+        <TestCreditBalanceComponent />
+      </TestWrapper>
+    )
+
+    // 验证恢复后状态
+    expect(screen.getByTestId('used-amount')).toHaveTextContent('已用额度: ¥100.00')
+    expect(screen.getByTestId('available-amount')).toHaveTextContent('可用额度: ¥900.00')
+  })
+})

+ 429 - 0
mini/tests/unit/pages/profile/credit-balance-display.test.tsx

@@ -0,0 +1,429 @@
+/**
+ * 个人中心欠款显示单元测试
+ * 测试欠款信息显示功能
+ */
+
+import { render, screen, waitFor, fireEvent } from '@testing-library/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import ProfilePage from '@/pages/profile/index'
+import { creditBalanceClient } from '@/api'
+import { useAuth } from '@/utils/auth'
+import { mockShowModal, mockShowLoading, mockHideLoading, mockShowToast, mockReLaunch } from '~/__mocks__/taroMock'
+
+// @tarojs/taro 已经在 jest.config.js 中通过 moduleNameMapper 重定向到 mock 文件
+// 不需要额外 mock
+
+// Mock API客户端
+jest.mock('@/api', () => ({
+  creditBalanceClient: {
+    me: {
+      $get: jest.fn(),
+    },
+  },
+}))
+
+// Mock 认证hook
+jest.mock('@/utils/auth', () => ({
+  useAuth: jest.fn(),
+}))
+
+// Mock TDesign组件
+jest.mock('@/components/tdesign/user-center-card', () => ({
+  __esModule: true,
+  default: ({ avatar, nickname, isLoggedIn, onUserEdit, className }: any) => (
+    <div data-testid="user-center-card" className={className}>
+      <div data-testid="avatar">{avatar}</div>
+      <div data-testid="nickname">{nickname}</div>
+      <div data-testid="is-logged-in">{isLoggedIn ? '已登录' : '未登录'}</div>
+      <button data-testid="edit-button" onClick={onUserEdit}>编辑</button>
+    </div>
+  ),
+}))
+
+jest.mock('@/components/tdesign/order-group', () => ({
+  __esModule: true,
+  default: ({ orderTagInfos, title, desc, onTopClick, onItemClick }: any) => (
+    <div data-testid="order-group">
+      <div data-testid="order-title">{title}</div>
+      <div data-testid="order-desc">{desc}</div>
+      <button data-testid="top-click" onClick={onTopClick}>查看全部</button>
+      {orderTagInfos.map((item: any, index: number) => (
+        <button key={index} data-testid={`order-item-${index}`} onClick={() => onItemClick(item)}>
+          {item.title}
+        </button>
+      ))}
+    </div>
+  ),
+}))
+
+jest.mock('@/components/tdesign/cell-group', () => ({
+  __esModule: true,
+  default: ({ children }: any) => (
+    <div data-testid="cell-group">{children}</div>
+  ),
+}))
+
+jest.mock('@/components/tdesign/cell', () => ({
+  __esModule: true,
+  default: ({ title, arrow, bordered, onClick, noteSlot }: any) => (
+    <div data-testid="cell" data-bordered={bordered}>
+      <div data-testid="cell-title">{title}</div>
+      <button data-testid="cell-click" onClick={onClick}>点击</button>
+      <div data-testid="cell-note">{noteSlot}</div>
+    </div>
+  ),
+}))
+
+jest.mock('@/components/tdesign/popup', () => ({
+  __esModule: true,
+  default: ({ visible, placement, onVisibleChange, onClose, children }: any) => (
+    visible ? (
+      <div data-testid="popup" data-placement={placement}>
+        {children}
+        <button data-testid="popup-close" onClick={onClose}>关闭</button>
+      </div>
+    ) : null
+  ),
+}))
+
+jest.mock('@/components/tdesign/icon', () => ({
+  __esModule: true,
+  default: ({ name, size, color }: any) => (
+    <div data-testid="icon" data-name={name} data-size={size} data-color={color}>图标</div>
+  ),
+}))
+
+// 创建测试QueryClient
+const createTestQueryClient = () => new QueryClient({
+  defaultOptions: {
+    queries: { retry: false },
+    mutations: { retry: false },
+  },
+})
+
+// Mock CartContext
+jest.mock('@/contexts/CartContext', () => ({
+  CartProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
+  useCart: () => ({
+    cart: {
+      items: [],
+      totalAmount: 0,
+      totalCount: 0,
+    },
+    addToCart: jest.fn(),
+    removeFromCart: jest.fn(),
+    updateQuantity: jest.fn(),
+    clearCart: jest.fn(),
+    isInCart: jest.fn(),
+    getItemQuantity: jest.fn(),
+    isLoading: false,
+  }),
+}))
+
+// 测试包装器
+const TestWrapper = ({ children }: { children: React.ReactNode }) => (
+  <QueryClientProvider client={createTestQueryClient()}>
+    {children}
+  </QueryClientProvider>
+)
+
+// 测试数据工厂
+const createTestUser = (overrides = {}) => ({
+  id: 1,
+  username: '测试用户',
+  avatarFile: { fullUrl: 'https://example.com/avatar.jpg' },
+  ...overrides,
+})
+
+const createTestCreditBalance = (overrides = {}) => ({
+  totalLimit: 1000,
+  usedAmount: 200,
+  availableAmount: 800,
+  isEnabled: true,
+  ...overrides,
+})
+
+describe('个人中心欠款显示功能测试', () => {
+  beforeEach(() => {
+    jest.clearAllMocks()
+
+    // 设置默认认证状态
+    ;(useAuth as jest.Mock).mockReturnValue({
+      user: createTestUser(),
+      logout: jest.fn(),
+      isLoading: false,
+      updateUser: jest.fn(),
+    })
+  })
+
+  test('应该正确渲染个人中心页面', async () => {
+    // Mock 额度查询返回正常数据
+    const mockCreditBalance = createTestCreditBalance({ usedAmount: 200 })
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(mockCreditBalance),
+    })
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 验证页面标题
+    await waitFor(() => {
+      expect(screen.getByText('个人中心')).toBeInTheDocument()
+    })
+
+    // 验证用户信息显示
+    expect(screen.getByTestId('user-center-card')).toBeInTheDocument()
+    expect(screen.getByTestId('nickname')).toHaveTextContent('测试用户')
+
+    // 验证订单组件
+    expect(screen.getByTestId('order-group')).toBeInTheDocument()
+    expect(screen.getByTestId('order-title')).toHaveTextContent('我的订单')
+  })
+
+  test('有欠款时应该显示欠款信息卡片', async () => {
+    // Mock 额度查询返回有欠款的数据
+    const mockCreditBalance = createTestCreditBalance({ usedAmount: 200 })
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(mockCreditBalance),
+    })
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 等待额度加载完成
+    await waitFor(() => {
+      expect(screen.getByText('欠款信息')).toBeInTheDocument()
+    })
+
+    // 验证欠款信息显示
+    expect(screen.getByText('累计欠款')).toBeInTheDocument()
+    expect(screen.getByText('¥200.00')).toBeInTheDocument()
+    expect(screen.getByText('需结清金额')).toBeInTheDocument()
+    expect(screen.getByText('请及时还款')).toBeInTheDocument()
+  })
+
+  test('没有欠款时不应该显示欠款信息卡片', async () => {
+    // Mock 额度查询返回没有欠款的数据
+    const mockCreditBalance = createTestCreditBalance({ usedAmount: 0 })
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(mockCreditBalance),
+    })
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 等待额度加载完成(如果有查询)
+    await waitFor(() => {
+      // 验证没有显示欠款信息卡片
+      expect(screen.queryByText('欠款信息')).not.toBeInTheDocument()
+      expect(screen.queryByText('累计欠款')).not.toBeInTheDocument()
+    })
+  })
+
+  test('额度查询加载中应该显示加载状态', async () => {
+    // Mock 额度查询延迟返回
+    let resolveQuery: any
+    const queryPromise = new Promise((resolve) => {
+      resolveQuery = resolve
+    })
+    ;(creditBalanceClient.me.$get as jest.Mock).mockReturnValue(queryPromise)
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 验证加载状态显示 - 使用更灵活的选择器
+    await waitFor(() => {
+      const loadingElement = screen.getByText(/加载中/)
+      expect(loadingElement).toBeInTheDocument()
+    })
+
+    // 解析查询
+    const mockCreditBalance = createTestCreditBalance({ usedAmount: 200 })
+    resolveQuery({
+      status: 200,
+      json: () => Promise.resolve(mockCreditBalance),
+    })
+
+    // 等待加载完成
+    await waitFor(() => {
+      expect(screen.queryByText(/加载中/)).not.toBeInTheDocument()
+      expect(screen.getByText('欠款信息')).toBeInTheDocument()
+    })
+  })
+
+  test('额度查询失败应该显示错误状态', async () => {
+    // Mock 额度查询失败
+    ;(creditBalanceClient.me.$get as jest.Mock).mockRejectedValue(new Error('网络错误'))
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 等待错误状态显示
+    await waitFor(() => {
+      expect(screen.getByText('加载失败')).toBeInTheDocument()
+      expect(screen.getByText('重新加载')).toBeInTheDocument()
+    })
+  })
+
+  test('额度查询失败后可以重试', async () => {
+    // Mock 额度查询第一次失败,第二次成功
+    let callCount = 0
+    ;(creditBalanceClient.me.$get as jest.Mock).mockImplementation(() => {
+      callCount++
+      if (callCount === 1) {
+        return Promise.reject(new Error('网络错误'))
+      } else {
+        return Promise.resolve({
+          status: 200,
+          json: () => Promise.resolve(createTestCreditBalance({ usedAmount: 200 })),
+        })
+      }
+    })
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 等待错误状态显示
+    await waitFor(() => {
+      expect(screen.getByText('加载失败')).toBeInTheDocument()
+    })
+
+    // 点击重新加载按钮
+    const retryButton = screen.getByText('重新加载')
+    fireEvent.click(retryButton)
+
+    // 等待重试成功
+    await waitFor(() => {
+      expect(screen.getByText('欠款信息')).toBeInTheDocument()
+      expect(screen.getByText('¥200.00')).toBeInTheDocument()
+    })
+  })
+
+  test('用户未登录时应该显示登录提示', async () => {
+    // Mock 用户未登录
+    ;(useAuth as jest.Mock).mockReturnValue({
+      user: null,
+      logout: jest.fn(),
+      isLoading: false,
+      updateUser: jest.fn(),
+    })
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 验证登录提示
+    expect(screen.getByText('请先登录')).toBeInTheDocument()
+    expect(screen.getByText('去登录')).toBeInTheDocument()
+
+    // 验证没有显示欠款信息
+    expect(screen.queryByText('欠款信息')).not.toBeInTheDocument()
+  })
+
+  test('额度查询返回404时不应该显示欠款信息', async () => {
+    // Mock 额度查询返回404(用户没有额度记录)
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 404,
+      json: () => Promise.resolve({ message: '用户信用额度记录不存在' }),
+    })
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 等待查询完成
+    await waitFor(() => {
+      // 验证没有显示欠款信息卡片
+      expect(screen.queryByText('欠款信息')).not.toBeInTheDocument()
+    })
+  })
+
+  test('欠款金额较大时应该正确格式化显示', async () => {
+    // Mock 额度查询返回大额欠款
+    const mockCreditBalance = createTestCreditBalance({ usedAmount: 12345.67 })
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(mockCreditBalance),
+    })
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 等待额度加载完成
+    await waitFor(() => {
+      expect(screen.getByText('欠款信息')).toBeInTheDocument()
+    })
+
+    // 验证金额正确格式化 - 使用正则表达式匹配
+    expect(screen.getByText(/¥12,345\.67/)).toBeInTheDocument()
+  })
+
+  test('应该正确显示欠款卡片的样式', async () => {
+    // Mock 额度查询返回有欠款的数据
+    const mockCreditBalance = createTestCreditBalance({ usedAmount: 200 })
+    ;(creditBalanceClient.me.$get as jest.Mock).mockResolvedValue({
+      status: 200,
+      json: () => Promise.resolve(mockCreditBalance),
+    })
+
+    render(
+      <TestWrapper>
+        <ProfilePage />
+      </TestWrapper>
+    )
+
+    // 等待额度加载完成
+    await waitFor(() => {
+      expect(screen.getByText('欠款信息')).toBeInTheDocument()
+    })
+
+    // 验证卡片结构
+    const card = screen.getByText('欠款信息').closest('.bg-white')
+    expect(card).toBeInTheDocument()
+    expect(card).toHaveClass('rounded-2xl')
+
+    // 验证标题区域
+    const titleSection = screen.getByText('欠款信息').closest('.border-b')
+    expect(titleSection).toBeInTheDocument()
+    expect(titleSection).toHaveClass('border-gray-100')
+
+    // 验证金额显示为大字体
+    const amountText = screen.getByText('¥200.00')
+    expect(amountText).toHaveClass('text-2xl')
+    expect(amountText).toHaveClass('font-bold')
+    expect(amountText).toHaveClass('text-red-600')
+
+    // 验证图标显示
+    const iconContainer = screen.getByText('📊').closest('.bg-red-100')
+    expect(iconContainer).toBeInTheDocument()
+    expect(iconContainer).toHaveClass('rounded-full')
+  })
+})

+ 1 - 0
packages/mini-payment-mt/package.json

@@ -57,6 +57,7 @@
     "@d8d/supplier-module-mt": "workspace:*",
     "@d8d/delivery-address-module-mt": "workspace:*",
     "@d8d/geo-areas-mt": "workspace:*",
+    "@d8d/goods-module-mt": "workspace:*",
     "@hono/zod-openapi": "^1.0.2",
     "typeorm": "^0.3.20",
     "wechatpay-node-v3": "2.1.8",

+ 12 - 9
packages/mini-payment-mt/src/services/payment.mt.service.ts

@@ -7,6 +7,7 @@ import { PaymentCreateResponse } from '../entities/payment.types.js';
 import { GenericCrudService } from '@d8d/shared-crud';
 import { SystemConfigServiceMt } from '@d8d/core-module-mt/system-config-module-mt';
 import { OrderMt } from '@d8d/orders-module-mt';
+import { PayStatus, PayType } from '@d8d/orders-module-mt';
 
 /**
  * 微信支付服务 - 多租户版本
@@ -308,16 +309,16 @@ export class PaymentMtService extends GenericCrudService<PaymentMtEntity> {
 
     try {
       if (payment.paymentStatus === PaymentStatus.PAID) {
-        // 支付成功,更新订单状态为已支付 (2)
-        await this.updateOrderPaymentStatus(payment.tenantId, payment.externalOrderId, 2);
+        // 支付成功,更新订单状态为已支付 (2),支付类型为微信支付 (4)
+        await this.updateOrderPaymentStatus(payment.tenantId, payment.externalOrderId, PayStatus.SUCCESS, PayType.WECHAT);
         console.debug(`[租户${payment.tenantId}] 订单状态更新为已支付,订单ID: ${payment.externalOrderId}`);
       } else if (payment.paymentStatus === PaymentStatus.FAILED) {
-        // 支付失败,更新订单状态为支付失败 (4)
-        await this.updateOrderPaymentStatus(payment.tenantId, payment.externalOrderId, 4);
+        // 支付失败,更新订单状态为支付失败 (4),支付类型为微信支付 (4)
+        await this.updateOrderPaymentStatus(payment.tenantId, payment.externalOrderId, PayStatus.FAILED, PayType.WECHAT);
         console.debug(`[租户${payment.tenantId}] 订单状态更新为支付失败,订单ID: ${payment.externalOrderId}`);
       } else if (payment.paymentStatus === PaymentStatus.REFUNDED) {
-        // 退款,更新订单状态为已退款 (3)
-        await this.updateOrderPaymentStatus(payment.tenantId, payment.externalOrderId, 3);
+        // 退款,更新订单状态为已退款 (3),支付类型为微信支付 (4)
+        await this.updateOrderPaymentStatus(payment.tenantId, payment.externalOrderId, PayStatus.REFUNDED, PayType.WECHAT);
         console.debug(`[租户${payment.tenantId}] 订单状态更新为已退款,订单ID: ${payment.externalOrderId}`);
       }
     } catch (error) {
@@ -403,9 +404,10 @@ export class PaymentMtService extends GenericCrudService<PaymentMtEntity> {
    * @param tenantId 租户ID
    * @param externalOrderId 外部订单ID
    * @param payState 支付状态 (0未支付、1支付中、2支付成功、3已退款、4支付失败、5订单关闭)
+   * @param payType 支付类型 (1积分、2礼券、3额度支付、4微信支付),可选,默认为微信支付
    */
-  async updateOrderPaymentStatus(tenantId: number, externalOrderId: number, payState: number): Promise<void> {
-    console.debug(`[租户${tenantId}] 开始更新订单支付状态,订单ID: ${externalOrderId}, 状态: ${payState}`);
+  async updateOrderPaymentStatus(tenantId: number, externalOrderId: number, payState: number, payType: number = PayType.WECHAT): Promise<void> {
+    console.debug(`[租户${tenantId}] 开始更新订单支付状态,订单ID: ${externalOrderId}, 支付状态: ${payState}, 支付类型: ${payType}`);
 
     try {
       // 直接使用数据源更新订单支付状态
@@ -427,6 +429,7 @@ export class PaymentMtService extends GenericCrudService<PaymentMtEntity> {
         { id: externalOrderId, tenantId },
         {
           payState,
+          payType,
           updatedAt: new Date()
         }
       );
@@ -436,7 +439,7 @@ export class PaymentMtService extends GenericCrudService<PaymentMtEntity> {
         throw new Error(`订单ID ${externalOrderId} 更新失败`);
       }
 
-      console.debug(`[租户${tenantId}] 订单支付状态更新成功,订单ID: ${externalOrderId}, 状态: ${payState}`);
+      console.debug(`[租户${tenantId}] 订单支付状态更新成功,订单ID: ${externalOrderId}, 支付状态: ${payState}, 支付类型: ${payType}`);
     } catch (error) {
       console.debug(`[租户${tenantId}] 订单支付状态更新失败,订单ID: ${externalOrderId}, 错误详情:`, {
         error: error instanceof Error ? error.message : '未知错误',

+ 19 - 9
packages/mini-payment-mt/tests/integration/payment-callback.integration.test.ts

@@ -10,12 +10,14 @@ import { PaymentStatus } from '../../src/entities/payment.types.js';
 import { UserEntityMt } from '@d8d/user-module-mt';
 import { RoleMt } from '@d8d/user-module-mt';
 import { FileMt } from '@d8d/file-module-mt';
-import { OrderMt } from '@d8d/orders-module-mt';
+import { OrderMt, OrderGoodsMt, OrderRefundMt } from '@d8d/orders-module-mt';
+import { PayStatus, PayType } from '@d8d/orders-module-mt';
 import { MerchantMt } from '@d8d/merchant-module-mt';
 import { SupplierMt } from '@d8d/supplier-module-mt';
 import { DeliveryAddressMt } from '@d8d/delivery-address-module-mt';
 import { AreaEntityMt } from '@d8d/geo-areas-mt';
 import { SystemConfigMt } from '@d8d/core-module-mt/system-config-module-mt/entities';
+import { GoodsMt, GoodsCategoryMt } from '@d8d/goods-module-mt';
 import { config } from 'dotenv';
 import { resolve } from 'path';
 // 导入微信支付SDK用于模拟
@@ -29,7 +31,10 @@ config({ path: resolve(process.cwd(), '.env.test') });
 vi.mock('wechatpay-node-v3')
 
 // 设置集成测试钩子
-setupIntegrationDatabaseHooksWithEntities([PaymentMtEntity, UserEntityMt, FileMt, RoleMt, OrderMt, MerchantMt, SupplierMt, DeliveryAddressMt, AreaEntityMt, SystemConfigMt])
+setupIntegrationDatabaseHooksWithEntities([
+  PaymentMtEntity, UserEntityMt, FileMt, RoleMt, OrderMt, OrderGoodsMt, OrderRefundMt,
+  MerchantMt, SupplierMt, DeliveryAddressMt, AreaEntityMt, SystemConfigMt, GoodsMt, GoodsCategoryMt
+])
 
 describe('支付回调API集成测试 - 多租户版本', () => {
   let client: ReturnType<typeof testClient<typeof PaymentMtRoutes>>;
@@ -112,7 +117,7 @@ describe('支付回调API集成测试 - 多租户版本', () => {
         const result = await response.text();
         expect(result).toBe('SUCCESS');
 
-        // 验证订单状态已更新为已支付 (2)
+        // 验证订单状态已更新为已支付 (2),支付类型为微信支付 (4)
         const dataSource = await IntegrationTestDatabase.getDataSource();
         const orderRepository = dataSource.getRepository(OrderMt);
         const updatedOrder = await orderRepository.findOne({
@@ -120,7 +125,8 @@ describe('支付回调API集成测试 - 多租户版本', () => {
         });
 
         expect(updatedOrder).toBeDefined();
-        expect(updatedOrder?.payState).toBe(2); // 已支付
+        expect(updatedOrder?.payState).toBe(PayStatus.SUCCESS); // 已支付
+        expect(updatedOrder?.payType).toBe(PayType.WECHAT); // 微信支付
       }
     });
 
@@ -154,7 +160,7 @@ describe('支付回调API集成测试 - 多租户版本', () => {
         const result = await response.text();
         expect(result).toBe('SUCCESS');
 
-        // 验证订单状态已更新为支付失败 (4)
+        // 验证订单状态已更新为支付失败 (4),支付类型为微信支付 (4)
         const dataSource = await IntegrationTestDatabase.getDataSource();
         const orderRepository = dataSource.getRepository(OrderMt);
         const updatedOrder = await orderRepository.findOne({
@@ -162,7 +168,8 @@ describe('支付回调API集成测试 - 多租户版本', () => {
         });
 
         expect(updatedOrder).toBeDefined();
-        expect(updatedOrder?.payState).toBe(4); // 支付失败
+        expect(updatedOrder?.payState).toBe(PayStatus.FAILED); // 支付失败
+        expect(updatedOrder?.payType).toBe(PayType.WECHAT); // 微信支付
       }
     });
 
@@ -204,7 +211,8 @@ describe('支付回调API集成测试 - 多租户版本', () => {
         });
 
         expect(updatedOrder).toBeDefined();
-        expect(updatedOrder?.payState).toBe(3); // 已退款
+        expect(updatedOrder?.payState).toBe(PayStatus.REFUNDED); // 已退款
+        expect(updatedOrder?.payType).toBe(PayType.WECHAT); // 微信支付
       }
     });
 
@@ -262,13 +270,15 @@ describe('支付回调API集成测试 - 多租户版本', () => {
       const updatedOrder1 = await orderRepository.findOne({
         where: { id: tenant1Data.order.id, tenantId: 1 }
       });
-      expect(updatedOrder1?.payState).toBe(2); // 已支付
+      expect(updatedOrder1?.payState).toBe(PayStatus.SUCCESS); // 已支付
+      expect(updatedOrder1?.payType).toBe(PayType.WECHAT); // 微信支付
 
       // 验证租户2的订单状态未受影响
       const updatedOrder2 = await orderRepository.findOne({
         where: { id: tenant2Data.order.id, tenantId: 2 }
       });
-      expect(updatedOrder2?.payState).toBe(0); // 仍为未支付
+      expect(updatedOrder2?.payState).toBe(PayStatus.UNPAID); // 仍为未支付
+      expect(updatedOrder2?.payType).toBe(0); // 支付类型未设置
     });
 
     it('应该处理无效的回调数据格式', async () => {

+ 12 - 17
packages/mini-payment-mt/tests/integration/payment-refund.integration.test.ts

@@ -51,26 +51,21 @@ vi.mock('@d8d/core-module-mt/system-config-module-mt', () => ({
   }))
 }));
 
-// 使用部分mock来保留 OrderMt 实体
-vi.mock('@d8d/orders-module-mt', async (importOriginal) => {
-  const actual = await importOriginal<typeof import('@d8d/orders-module-mt')>()
-  return {
-    ...actual,
-    OrderMtService: vi.fn().mockImplementation(() => ({})),
-  }
-});
+// 导入订单实体
+import { OrderMt, OrderGoodsMt, OrderRefundMt } from '@d8d/orders-module-mt';
+import { GoodsMt, GoodsCategoryMt } from '@d8d/goods-module-mt';
+import { UserEntityMt, RoleMt } from '@d8d/user-module-mt';
+import { FileMt } from '@d8d/file-module-mt';
+import { MerchantMt } from '@d8d/merchant-module-mt';
+import { SupplierMt } from '@d8d/supplier-module-mt';
+import { DeliveryAddressMt } from '@d8d/delivery-address-module-mt';
+import { AreaEntityMt } from '@d8d/geo-areas-mt';
+import { SystemConfigMt } from '@d8d/core-module-mt/system-config-module-mt/entities';
 
 // 设置集成测试钩子
 setupIntegrationDatabaseHooksWithEntities([
-  PaymentMtEntity,
-  OrderMt,
-  OrderGoodsMt,
-  UserEntityMt,
-  RoleMt,
-  MerchantMt,
-  SupplierMt,
-  DeliveryAddressMt,
-  FileMt
+  PaymentMtEntity, OrderMt, OrderGoodsMt, OrderRefundMt, GoodsMt, GoodsCategoryMt,
+  UserEntityMt, RoleMt, FileMt, MerchantMt, SupplierMt, DeliveryAddressMt, AreaEntityMt, SystemConfigMt
 ])
 
 describe('PaymentRefund Integration Tests', () => {

+ 13 - 3
packages/mini-payment-mt/tests/integration/payment.integration.test.ts

@@ -10,6 +10,13 @@ import { PaymentStatus } from '../../src/entities/payment.types.js';
 import { UserEntityMt } from '@d8d/user-module-mt';
 import { RoleMt } from '@d8d/user-module-mt';
 import { FileMt } from '@d8d/file-module-mt';
+import { OrderMt, OrderGoodsMt, OrderRefundMt } from '@d8d/orders-module-mt';
+import { MerchantMt } from '@d8d/merchant-module-mt';
+import { SupplierMt } from '@d8d/supplier-module-mt';
+import { DeliveryAddressMt } from '@d8d/delivery-address-module-mt';
+import { AreaEntityMt } from '@d8d/geo-areas-mt';
+import { SystemConfigMt } from '@d8d/core-module-mt/system-config-module-mt/entities';
+import { GoodsMt, GoodsCategoryMt } from '@d8d/goods-module-mt';
 import { JWTUtil } from '@d8d/shared-utils';
 import { config } from 'dotenv';
 import { resolve } from 'path';
@@ -22,7 +29,10 @@ config({ path: resolve(process.cwd(), '.env.test') });
 vi.mock('wechatpay-node-v3')
 
 // 设置集成测试钩子
-setupIntegrationDatabaseHooksWithEntities([PaymentMtEntity, UserEntityMt, FileMt, RoleMt])
+setupIntegrationDatabaseHooksWithEntities([
+  PaymentMtEntity, UserEntityMt, FileMt, RoleMt, OrderMt, OrderGoodsMt, OrderRefundMt,
+  MerchantMt, SupplierMt, DeliveryAddressMt, AreaEntityMt, SystemConfigMt, GoodsMt, GoodsCategoryMt
+])
 
 describe('支付API集成测试', () => {
   let client: ReturnType<typeof testClient<typeof PaymentMtRoutes>>;
@@ -37,7 +47,7 @@ describe('支付API集成测试', () => {
     // 创建测试用户并生成token
     const dataSource = await IntegrationTestDatabase.getDataSource();
 
-    const userRepository = dataSource.getRepository(UserEntity);
+    const userRepository = dataSource.getRepository(UserEntityMt);
     testUser = userRepository.create({
       username: `test_user_${Date.now()}`,
       password: 'test_password',
@@ -228,7 +238,7 @@ describe('支付API集成测试', () => {
     it('应该拒绝没有openid的用户支付', async () => {
       // 创建没有openid的测试用户
       const dataSource = await IntegrationTestDatabase.getDataSource();
-      const userRepository = dataSource.getRepository(UserEntity);
+      const userRepository = dataSource.getRepository(UserEntityMt);
 
       const userWithoutOpenid = userRepository.create({
         username: `test_user_no_openid_${Date.now()}`,

+ 1 - 0
packages/orders-module-mt/package.json

@@ -63,6 +63,7 @@
     "@d8d/file-module-mt": "workspace:*",
     "@d8d/geo-areas-mt": "workspace:*",
     "@d8d/mini-payment-mt": "workspace:*",
+    "@d8d/credit-balance-module-mt": "workspace:*",
     "@hono/zod-openapi": "^1.0.2",
     "typeorm": "^0.3.20",
     "zod": "^4.1.12"

+ 1 - 1
packages/orders-module-mt/src/entities/order.mt.entity.ts

@@ -67,7 +67,7 @@ export class OrderMt {
   @Column({ name: 'order_type', type: 'int', default: 1, comment: '订单类型 1实物订单 2虚拟订单' })
   orderType!: number;
 
-  @Column({ name: 'pay_type', type: 'int', default: 0, comment: '支付类型1积分2礼券' })
+  @Column({ name: 'pay_type', type: 'int', default: 0, comment: '支付类型1积分2礼券3额度支付' })
   payType!: number;
 
   @Column({ name: 'pay_state', type: 'int', default: 0, comment: '0未支付1支付中2支付成功3已退款4支付失败5订单关闭' })

+ 7 - 6
packages/orders-module-mt/src/schemas/order.mt.schema.ts

@@ -37,6 +37,7 @@ export const OrderType = {
 export const PayType = {
   POINTS: 1, // 积分
   COUPON: 2, // 礼券
+  CREDIT: 3, // 额度支付
 } as const;
 
 // 多租户订单基础Schema
@@ -113,8 +114,8 @@ export const OrderSchema = z.object({
     description: '订单类型 1实物订单 2虚拟订单',
     example: 1
   }),
-  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(2, '支付类型最大为2').default(0).openapi({
-    description: '支付类型1积分2礼券',
+  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(3, '支付类型最大为3').default(0).openapi({
+    description: '支付类型1积分2礼券3额度支付',
     example: 1
   }),
   payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').default(0).openapi({
@@ -338,8 +339,8 @@ export const CreateOrderDto = z.object({
     description: '订单类型 1实物订单 2虚拟订单',
     example: 1
   }),
-  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(2, '支付类型最大为2').default(0).openapi({
-    description: '支付类型1积分2礼券',
+  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(3, '支付类型最大为3').default(0).openapi({
+    description: '支付类型1积分2礼券3额度支付',
     example: 1
   }),
   payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').default(0).openapi({
@@ -494,8 +495,8 @@ export const UpdateOrderDto = z.object({
     description: '订单类型 1实物订单 2虚拟订单',
     example: 1
   }),
-  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(2, '支付类型最大为2').optional().openapi({
-    description: '支付类型1积分2礼券',
+  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(3, '支付类型最大为3').optional().openapi({
+    description: '支付类型1积分2礼券3额度支付',
     example: 1
   }),
   payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').optional().openapi({

+ 21 - 0
packages/orders-module-mt/src/services/order.mt.service.ts

@@ -6,6 +6,7 @@ import { OrderRefundMt } from '../entities/order-refund.mt.entity';
 import { GoodsMt } from '@d8d/goods-module-mt';
 import { DeliveryAddressMt } from '@d8d/delivery-address-module-mt';
 import { PaymentMtService } from '@d8d/mini-payment-mt';
+import { CreditBalanceService } from '@d8d/credit-balance-module-mt';
 import type { CreateOrderRequest } from '../schemas/create-order.schema';
 
 export class OrderMtService extends GenericCrudService<OrderMt> {
@@ -14,6 +15,7 @@ export class OrderMtService extends GenericCrudService<OrderMt> {
   private deliveryAddressRepository: Repository<DeliveryAddressMt>;
   private orderRefundRepository: Repository<OrderRefundMt>;
   private paymentMtService: PaymentMtService;
+  private creditBalanceService: CreditBalanceService;
 
   constructor(dataSource: DataSource) {
     super(dataSource, OrderMt, {
@@ -24,6 +26,7 @@ export class OrderMtService extends GenericCrudService<OrderMt> {
     this.deliveryAddressRepository = dataSource.getRepository(DeliveryAddressMt);
     this.orderRefundRepository = dataSource.getRepository(OrderRefundMt);
     this.paymentMtService = new PaymentMtService(dataSource);
+    this.creditBalanceService = new CreditBalanceService(dataSource);
   }
 
   /**
@@ -244,6 +247,24 @@ export class OrderMtService extends GenericCrudService<OrderMt> {
         updatedAt: new Date()
       });
 
+      // 如果是额度支付订单(payType === 3),需要恢复额度
+      if (order.payType === 3) {
+        console.debug(`[租户${tenantId}] 额度支付订单 ${orderId} 取消,开始恢复额度`);
+        try {
+          await this.creditBalanceService.restoreBalanceForCancelOrder(
+            tenantId,
+            userId,
+            order.orderNo,
+            order.payAmount,
+            userId
+          );
+          console.debug(`[租户${tenantId}] 额度恢复成功,订单号: ${order.orderNo}`);
+        } catch (error) {
+          console.debug(`[租户${tenantId}] 额度恢复失败,订单ID: ${orderId}, 错误:`, error);
+          // 额度恢复失败不影响订单取消,但记录错误信息
+        }
+      }
+
       // 如果订单是未支付状态,需要恢复商品库存
       if (order.payState === 0) {
         // 查询订单商品明细

+ 1 - 1
packages/orders-module/src/entities/order.entity.ts

@@ -57,7 +57,7 @@ export class Order {
   @Column({ name: 'order_type', type: 'int', default: 1, comment: '订单类型 1实物订单 2虚拟订单' })
   orderType!: number;
 
-  @Column({ name: 'pay_type', type: 'int', default: 0, comment: '支付类型1积分2礼券' })
+  @Column({ name: 'pay_type', type: 'int', default: 0, comment: '支付类型1积分2礼券3额度支付4微信支付' })
   payType!: number;
 
   @Column({ name: 'pay_state', type: 'int', default: 0, comment: '0未支付1支付中2支付成功3已退款4支付失败5订单关闭' })

+ 8 - 6
packages/orders-module/src/schemas/order.schema.ts

@@ -28,6 +28,8 @@ export const OrderType = {
 export const PayType = {
   POINTS: 1, // 积分
   COUPON: 2, // 礼券
+  CREDIT: 3, // 额度支付
+  WECHAT: 4, // 微信支付
 } as const;
 
 // 订单基础Schema
@@ -100,8 +102,8 @@ export const OrderSchema = z.object({
     description: '订单类型 1实物订单 2虚拟订单',
     example: 1
   }),
-  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(2, '支付类型最大为2').default(0).openapi({
-    description: '支付类型1积分2礼券',
+  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(4, '支付类型最大为4').default(0).openapi({
+    description: '支付类型1积分2礼券3额度支付4微信支付',
     example: 1
   }),
   payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').default(0).openapi({
@@ -280,8 +282,8 @@ export const CreateOrderDto = z.object({
     description: '订单类型 1实物订单 2虚拟订单',
     example: 1
   }),
-  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(2, '支付类型最大为2').default(0).openapi({
-    description: '支付类型1积分2礼券',
+  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(4, '支付类型最大为4').default(0).openapi({
+    description: '支付类型1积分2礼券3额度支付4微信支付',
     example: 1
   }),
   payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').default(0).openapi({
@@ -416,8 +418,8 @@ export const UpdateOrderDto = z.object({
     description: '订单类型 1实物订单 2虚拟订单',
     example: 1
   }),
-  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(2, '支付类型最大为2').optional().openapi({
-    description: '支付类型1积分2礼券',
+  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(4, '支付类型最大为4').optional().openapi({
+    description: '支付类型1积分2礼券3额度支付4微信支付',
     example: 1
   }),
   payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').optional().openapi({

+ 1 - 0
packages/server/package.json

@@ -46,6 +46,7 @@
     "@d8d/merchant-module-mt": "workspace:*",
     "@d8d/orders-module-mt": "workspace:*",
     "@d8d/supplier-module-mt": "workspace:*",
+    "@d8d/credit-balance-module-mt": "workspace:*",
     "@d8d/core-module-mt": "workspace:*",
     "axios": "^1.12.2",
     "bcrypt": "^6.0.0",

+ 6 - 1
packages/server/src/index.ts

@@ -22,6 +22,8 @@ import { GoodsMt, GoodsCategoryMt } from '@d8d/goods-module-mt'
 import { MerchantMt } from '@d8d/merchant-module-mt'
 import { OrderMt, OrderGoodsMt, OrderRefundMt } from '@d8d/orders-module-mt'
 import { SupplierMt } from '@d8d/supplier-module-mt'
+import { CreditBalanceMt, CreditBalanceLogMt } from '@d8d/credit-balance-module-mt'
+import { creditBalanceRoutes as creditBalanceModuleRoutes } from '@d8d/credit-balance-module-mt'
 
 initializeDataSource([
   // 已实现的包实体
@@ -33,7 +35,8 @@ initializeDataSource([
   GoodsMt, GoodsCategoryMt,
   MerchantMt,
   OrderMt, OrderGoodsMt, OrderRefundMt,
-  SupplierMt, SystemConfigMt
+  SupplierMt, SystemConfigMt,
+  CreditBalanceMt, CreditBalanceLogMt
 ])
 
 if(!AppDataSource || !AppDataSource.isInitialized) {
@@ -169,6 +172,7 @@ export const merchantApiRoutes = api.route('/api/v1/merchants', merchantRoutes)
 export const orderApiRoutes = api.route('/api/v1/orders', userOrderRoutes)
 // export const orderGoodsApiRoutes = api.route('/api/v1/orders-goods', userOrderItemsRoutes)
 // export const orderRefundApiRoutes = api.route('/api/v1/orders-refund', userRefundsRoutes)
+export const creditBalanceApiRoutes = api.route('/api/v1/credit-balance', creditBalanceModuleRoutes)
 
 export const adminOrderApiRoutes = api.route('/api/v1/admin/orders', adminOrderRoutes)
 export const adminOrderGoodsApiRoutes = api.route('/api/v1/admin/orders-goods', adminOrderItemsRoutes)
@@ -198,6 +202,7 @@ export type AdminOrderRefundRoutes = typeof adminOrderRefundApiRoutes
 export type AreaRoutes = typeof areaApiRoutes
 export type AdminAreaRoutes = typeof adminAreaApiRoutes
 export type PaymentRoutes = typeof paymentApiRoutes
+export type CreditBalanceRoutes = typeof creditBalanceApiRoutes
 
 app.route('/', api)
 export default app

+ 1 - 0
packages/user-management-ui-mt/package.json

@@ -43,6 +43,7 @@
     "@d8d/shared-types": "workspace:*",
     "@d8d/shared-ui-components": "workspace:*",
     "@d8d/user-module-mt": "workspace:*",
+    "@d8d/credit-balance-management-ui-mt": "workspace:*",
     "@hookform/resolvers": "^5.2.1",
     "@tanstack/react-query": "^5.90.9",
     "axios": "^1.7.9",

+ 40 - 6
packages/user-management-ui-mt/src/components/UserManagement.tsx

@@ -4,7 +4,6 @@ import { format } from 'date-fns';
 import { Plus, Search, Edit, Trash2, Filter, X } from 'lucide-react';
 import { userClient, userClientManager } from '../api/userClient';
 import type { InferRequestType, InferResponseType } from 'hono/client';
-import { z } from 'zod';
 import { Button } from '@d8d/shared-ui-components/components/ui/button';
 import { Input } from '@d8d/shared-ui-components/components/ui/input';
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
@@ -17,12 +16,14 @@ import { zodResolver } from '@hookform/resolvers/zod';
 import { toast } from 'sonner';
 import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
 import { Switch } from '@d8d/shared-ui-components/components/ui/switch';
-import { CreateUserDtoMt, UpdateUserDtoMt, RoleSchemaMt } from '@d8d/user-module-mt/schemas';
+import { CreateUserDtoMt, UpdateUserDtoMt } from '@d8d/user-module-mt/schemas';
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
 import { Popover, PopoverContent, PopoverTrigger } from '@d8d/shared-ui-components/components/ui/popover';
 import { Calendar } from '@d8d/shared-ui-components/components/ui/calendar';
 import { cn } from '@d8d/shared-ui-components/utils/cn';
 import { DisabledStatus } from '@d8d/shared-types';
+import { CreditBalanceDialog } from '@d8d/credit-balance-management-ui-mt';
+import { CreditCard } from 'lucide-react';
 
 // 使用RPC方式提取类型
 type CreateUserRequest = InferRequestType<typeof userClient.index.$post>['json'];
@@ -36,10 +37,10 @@ const updateUserFormSchema = UpdateUserDtoMt;
 type CreateUserFormData = CreateUserRequest;
 type UpdateUserFormData = UpdateUserRequest;
 // 适配后端返回的字符串日期格式
-type Role = Omit<z.infer<typeof RoleSchemaMt>, 'createdAt' | 'updatedAt'> & {
-  createdAt: string;
-  updatedAt: string;
-};
+// type Role = Omit<z.infer<typeof RoleSchemaMt>, 'createdAt' | 'updatedAt'> & {
+//   createdAt: string;
+//   updatedAt: string;
+// };
 
 
 export const UserManagement = () => {
@@ -58,6 +59,8 @@ export const UserManagement = () => {
   const [editingUser, setEditingUser] = useState<UserResponse | null>(null);
   const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
   const [userToDelete, setUserToDelete] = useState<number | null>(null);
+  const [creditBalanceDialogOpen, setCreditBalanceDialogOpen] = useState(false);
+  const [selectedUserForCredit, setSelectedUserForCredit] = useState<{ id: number; name: string } | null>(null);
 
   const [isCreateForm, setIsCreateForm] = useState(true);
 
@@ -196,6 +199,15 @@ export const UserManagement = () => {
     setIsModalOpen(true);
   };
 
+  // 打开信用额度管理对话框
+  const handleOpenCreditDialog = (user: UserResponse) => {
+    setSelectedUserForCredit({
+      id: user.id,
+      name: user.name || user.username
+    });
+    setCreditBalanceDialogOpen(true);
+  };
+
   // 打开编辑用户对话框
   const handleEditUser = (user: UserResponse) => {
     setEditingUser(user);
@@ -568,6 +580,15 @@ export const UserManagement = () => {
                       </TableCell>
                       <TableCell className="text-right">
                         <div className="flex justify-end gap-2">
+                          <Button
+                            variant="ghost"
+                            size="icon"
+                            onClick={() => handleOpenCreditDialog(user)}
+                            aria-label="管理信用额度"
+                            title="管理信用额度"
+                          >
+                            <CreditCard className="h-4 w-4" />
+                          </Button>
                           <Button
                             variant="ghost"
                             size="icon"
@@ -900,6 +921,19 @@ export const UserManagement = () => {
           </DialogFooter>
         </DialogContent>
       </Dialog>
+
+      {/* 信用额度管理对话框 */}
+      {selectedUserForCredit && (
+        <CreditBalanceDialog
+          userId={selectedUserForCredit.id}
+          userName={selectedUserForCredit.name}
+          open={creditBalanceDialogOpen}
+          onOpenChange={setCreditBalanceDialogOpen}
+          title={`${selectedUserForCredit.name}的信用额度管理`}
+          description={`管理用户 ${selectedUserForCredit.name} (ID: ${selectedUserForCredit.id}) 的信用额度`}
+          size="lg"
+        />
+      )}
     </div>
   );
 };

+ 133 - 0
pnpm-lock.yaml

@@ -1298,11 +1298,126 @@ importers:
         specifier: ^3.2.4
         version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(sass@1.94.1)(stylus@0.64.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
 
+  packages/credit-balance-management-ui-mt:
+    dependencies:
+      '@d8d/credit-balance-module-mt':
+        specifier: workspace:*
+        version: link:../credit-balance-module-mt
+      '@d8d/shared-types':
+        specifier: workspace:*
+        version: link:../shared-types
+      '@d8d/shared-ui-components':
+        specifier: workspace:*
+        version: link:../shared-ui-components
+      '@hookform/resolvers':
+        specifier: ^5.2.1
+        version: 5.2.2(react-hook-form@7.66.1(react@19.2.0))
+      '@tanstack/react-query':
+        specifier: ^5.90.9
+        version: 5.90.10(react@19.2.0)
+      axios:
+        specifier: ^1.7.9
+        version: 1.13.2(debug@4.4.3)
+      class-variance-authority:
+        specifier: ^0.7.1
+        version: 0.7.1
+      clsx:
+        specifier: ^2.1.1
+        version: 2.1.1
+      date-fns:
+        specifier: ^4.1.0
+        version: 4.1.0
+      dayjs:
+        specifier: ^1.11.13
+        version: 1.11.19
+      hono:
+        specifier: ^4.8.5
+        version: 4.8.5
+      lucide-react:
+        specifier: ^0.536.0
+        version: 0.536.0(react@19.2.0)
+      react:
+        specifier: ^19.1.0
+        version: 19.2.0
+      react-dom:
+        specifier: ^19.1.0
+        version: 19.2.0(react@19.2.0)
+      react-hook-form:
+        specifier: ^7.61.1
+        version: 7.66.1(react@19.2.0)
+      react-router:
+        specifier: ^7.1.3
+        version: 7.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      sonner:
+        specifier: ^2.0.7
+        version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      tailwind-merge:
+        specifier: ^3.3.1
+        version: 3.4.0
+      zod:
+        specifier: ^4.0.15
+        version: 4.1.12
+    devDependencies:
+      '@testing-library/jest-dom':
+        specifier: ^6.8.0
+        version: 6.9.1
+      '@testing-library/react':
+        specifier: ^16.3.0
+        version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@testing-library/user-event':
+        specifier: ^14.6.1
+        version: 14.6.1(@testing-library/dom@10.4.1)
+      '@types/node':
+        specifier: ^22.10.2
+        version: 22.19.1
+      '@types/react':
+        specifier: ^19.2.2
+        version: 19.2.6
+      '@types/react-dom':
+        specifier: ^19.2.3
+        version: 19.2.3(@types/react@19.2.6)
+      '@typescript-eslint/eslint-plugin':
+        specifier: ^8.18.1
+        version: 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
+      '@typescript-eslint/parser':
+        specifier: ^8.18.1
+        version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
+      eslint:
+        specifier: ^9.17.0
+        version: 9.39.1(jiti@2.6.1)
+      jsdom:
+        specifier: ^26.0.0
+        version: 26.1.0
+      typescript:
+        specifier: ^5.8.3
+        version: 5.8.3
+      unbuild:
+        specifier: ^3.4.0
+        version: 3.6.1(sass@1.94.1)(typescript@5.8.3)
+      vitest:
+        specifier: ^4.0.9
+        version: 4.0.10(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(sass@1.94.1)(stylus@0.64.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
+
   packages/credit-balance-module-mt:
     dependencies:
       '@d8d/core-module-mt':
         specifier: workspace:*
         version: link:../core-module-mt
+      '@d8d/delivery-address-module-mt':
+        specifier: workspace:*
+        version: link:../delivery-address-module-mt
+      '@d8d/geo-areas-mt':
+        specifier: workspace:*
+        version: link:../geo-areas-mt
+      '@d8d/goods-module-mt':
+        specifier: workspace:*
+        version: link:../goods-module-mt
+      '@d8d/merchant-module-mt':
+        specifier: workspace:*
+        version: link:../merchant-module-mt
+      '@d8d/orders-module-mt':
+        specifier: workspace:*
+        version: link:../orders-module-mt
       '@d8d/shared-crud':
         specifier: workspace:*
         version: link:../shared-crud
@@ -1312,6 +1427,9 @@ importers:
       '@d8d/shared-utils':
         specifier: workspace:*
         version: link:../shared-utils
+      '@d8d/supplier-module-mt':
+        specifier: workspace:*
+        version: link:../supplier-module-mt
       '@hono/zod-openapi':
         specifier: ^1.0.2
         version: 1.0.2(hono@4.8.5)(zod@4.1.12)
@@ -2930,6 +3048,9 @@ importers:
       '@d8d/geo-areas-mt':
         specifier: workspace:*
         version: link:../geo-areas-mt
+      '@d8d/goods-module-mt':
+        specifier: workspace:*
+        version: link:../goods-module-mt
       '@d8d/merchant-module-mt':
         specifier: workspace:*
         version: link:../merchant-module-mt
@@ -3258,6 +3379,9 @@ importers:
       '@d8d/auth-module-mt':
         specifier: workspace:*
         version: link:../auth-module-mt
+      '@d8d/credit-balance-module-mt':
+        specifier: workspace:*
+        version: link:../credit-balance-module-mt
       '@d8d/delivery-address-module-mt':
         specifier: workspace:*
         version: link:../delivery-address-module-mt
@@ -3340,6 +3464,9 @@ importers:
       '@d8d/core-module-mt':
         specifier: workspace:*
         version: link:../core-module-mt
+      '@d8d/credit-balance-module-mt':
+        specifier: workspace:*
+        version: link:../credit-balance-module-mt
       '@d8d/delivery-address-module-mt':
         specifier: workspace:*
         version: link:../delivery-address-module-mt
@@ -4415,6 +4542,9 @@ importers:
 
   packages/user-management-ui-mt:
     dependencies:
+      '@d8d/credit-balance-management-ui-mt':
+        specifier: workspace:*
+        version: link:../credit-balance-management-ui-mt
       '@d8d/shared-types':
         specifier: workspace:*
         version: link:../shared-types
@@ -4613,6 +4743,9 @@ importers:
       '@d8d/auth-module':
         specifier: workspace:*
         version: link:../packages/auth-module
+      '@d8d/credit-balance-management-ui-mt':
+        specifier: workspace:*
+        version: link:../packages/credit-balance-management-ui-mt
       '@d8d/delivery-address-management-ui-mt':
         specifier: workspace:*
         version: link:../packages/delivery-address-management-ui-mt

+ 1 - 0
web/package.json

@@ -63,6 +63,7 @@
     "@d8d/delivery-address-management-ui-mt": "workspace:*",
     "@d8d/advertisement-management-ui-mt": "workspace:*",
     "@d8d/system-config-management-ui-mt": "workspace:*",
+    "@d8d/credit-balance-management-ui-mt": "workspace:*",
     "@d8d/tenant-management-ui": "workspace:*",
     "@d8d/user-module": "workspace:*",
     "@heroicons/react": "^2.2.0",

+ 3 - 1
web/src/client/admin/api_init.ts

@@ -12,6 +12,7 @@ import { goodsCategoryClientManager } from '@d8d/goods-category-management-ui-mt
 import { deliveryAddressClientManager } from '@d8d/delivery-address-management-ui-mt/api';
 import { advertisementClientManager } from '@d8d/advertisement-management-ui-mt/api';
 import { systemConfigClientManager } from '@d8d/system-config-management-ui-mt/api';
+import { creditBalanceClientManager } from '@d8d/credit-balance-management-ui-mt/api';
 
 
 // 初始化所有多租户API客户端
@@ -27,4 +28,5 @@ goodsClientManager.init('/api/v1/admin/goods');
 goodsCategoryClientManager.init('/api/v1/admin/goods-categories');
 deliveryAddressClientManager.init('/api/v1/admin/delivery-addresses');
 advertisementClientManager.init('/api/v1/advertisements');
-systemConfigClientManager.init('/api/v1/admin/system-configs');
+systemConfigClientManager.init('/api/v1/admin/system-configs');
+creditBalanceClientManager.init('/api/v1/credit-balance');