/** * Payment Service 单元测试 * 测试订单创建、支付回调、会员激活 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { generateOrderNo } from '@/modules/payment/payment.service'; // 直接 Mock prisma 模型 const mockOrderFindFirst = vi.fn(); const mockOrderFindUnique = vi.fn(); const mockOrderUpdate = vi.fn(); const mockOrderCreate = vi.fn(); const mockUserUpdate = vi.fn(); const mockUserFindUnique = vi.fn(); const mockPlanFindUnique = vi.fn(); const mockSubscriptionFindFirst = vi.fn(); const mockSubscriptionCreate = vi.fn(); const mockSubscriptionUpdate = vi.fn(); const mockTokenBalanceFindUnique = vi.fn(); const mockTokenBalanceUpdate = vi.fn(); const mockTokenBalanceCreate = vi.fn(); vi.mock('@/models', () => ({ prisma: { order: { findFirst: (...args: any[]) => mockOrderFindFirst(...args), findUnique: (...args: any[]) => mockOrderFindUnique(...args), update: (...args: any[]) => mockOrderUpdate(...args), create: (...args: any[]) => mockOrderCreate(...args), }, user: { update: (...args: any[]) => mockUserUpdate(...args), findUnique: (...args: any[]) => mockUserFindUnique(...args), }, subscriptionPlan: { findUnique: (...args: any[]) => mockPlanFindUnique(...args), }, subscription: { findFirst: (...args: any[]) => mockSubscriptionFindFirst(...args), create: (...args: any[]) => mockSubscriptionCreate(...args), update: (...args: any[]) => mockSubscriptionUpdate(...args), }, tokenBalance: { findUnique: (...args: any[]) => mockTokenBalanceFindUnique(...args), update: (...args: any[]) => mockTokenBalanceUpdate(...args), create: (...args: any[]) => mockTokenBalanceCreate(...args), }, }, })); describe('generateOrderNo', () => { it('生成格式正确的订单号', () => { const orderNo = generateOrderNo(); expect(orderNo).toMatch(/^PAY\d{8}[A-Z0-9]{6}$/); }); it('每次调用生成不同的订单号', () => { const orderNo1 = generateOrderNo(); const orderNo2 = generateOrderNo(); expect(orderNo1).not.toBe(orderNo2); }); it('订单号包含当前日期', () => { const orderNo = generateOrderNo(); const today = new Date().toISOString().slice(0, 10).replace(/-/g, ''); expect(orderNo).toContain(today); }); }); describe('createPaymentOrder', () => { beforeEach(() => { vi.clearAllMocks(); }); it('套餐不存在时抛出错误', async () => { const { createPaymentOrder } = await import('@/modules/payment/payment.service'); mockPlanFindUnique.mockResolvedValue(null); await expect( createPaymentOrder(1, 999, 'mock') ).rejects.toThrow('套餐不存在'); }); it('成功创建 mock 订单', async () => { const { createPaymentOrder } = await import('@/modules/payment/payment.service'); mockPlanFindUnique.mockResolvedValue({ id: 1, name: '专业版', priceMonthly: 59, priceYearly: 590 }); mockOrderCreate.mockResolvedValue({ id: 1, status: 'pending' }); const result = await createPaymentOrder(1, 1, 'mock'); expect(result.planName).toBe('专业版'); expect(result.amount).toBe(59); expect(result.orderNo).toBeDefined(); }); it('月付使用 monthly 价格', async () => { const { createPaymentOrder } = await import('@/modules/payment/payment.service'); mockPlanFindUnique.mockResolvedValue({ id: 1, name: '专业版', priceMonthly: 59, priceYearly: 590 }); mockOrderCreate.mockResolvedValue({ id: 1, status: 'pending' }); const result = await createPaymentOrder(1, 1, 'mock', 'monthly'); expect(result.amount).toBe(59); }); it('年付使用 yearly 价格', async () => { const { createPaymentOrder } = await import('@/modules/payment/payment.service'); mockPlanFindUnique.mockResolvedValue({ id: 1, name: '专业版', priceMonthly: 59, priceYearly: 590 }); mockOrderCreate.mockResolvedValue({ id: 1, status: 'pending' }); const result = await createPaymentOrder(1, 1, 'mock', 'yearly'); expect(result.amount).toBe(590); }); }); describe('handlePaymentCallback', () => { beforeEach(() => { vi.clearAllMocks(); }); it('订单不存在时抛出错误', async () => { const { handlePaymentCallback } = await import('@/modules/payment/payment.service'); mockOrderFindFirst.mockResolvedValue(null); await expect( handlePaymentCallback('NONEXISTENT', 'PAY123', 'success') ).rejects.toThrow('订单不存在'); }); it('订单已支付时返回幂等成功', async () => { const { handlePaymentCallback } = await import('@/modules/payment/payment.service'); mockOrderFindFirst.mockResolvedValue({ id: 1, orderNo: 'PAY20260519000001', status: 'paid', userId: 1, }); const result = await handlePaymentCallback('PAY20260519000001', 'PAY123', 'success'); expect(result.success).toBe(true); expect(result.message).toContain('已支付'); }); it('支付失败时更新订单状态', async () => { const { handlePaymentCallback } = await import('@/modules/payment/payment.service'); mockOrderFindFirst .mockResolvedValueOnce({ id: 1, orderNo: 'PAY20260519000001', status: 'pending', userId: 1, }) .mockResolvedValueOnce({ id: 1, orderNo: 'PAY20260519000001', status: 'failed', userId: 1, }); mockOrderUpdate.mockResolvedValue({ id: 1, status: 'failed' }); const result = await handlePaymentCallback('PAY20260519000001', 'PAY123', 'failed'); expect(result.success).toBe(false); expect(result.message).toContain('失败'); }); it('支付成功时调用 activateSubscription', async () => { const { handlePaymentCallback } = await import('@/modules/payment/payment.service'); mockOrderFindFirst .mockResolvedValueOnce({ id: 1, orderNo: 'PAY20260519000001', status: 'pending', userId: 1, planId: 2, productType: 'monthly', }) .mockResolvedValueOnce({ id: 1, orderNo: 'PAY20260519000001', status: 'paid', userId: 1, planId: 2, }); mockOrderUpdate.mockResolvedValue({}); mockPlanFindUnique.mockResolvedValue({ id: 2, level: 2, monthlyTokens: 15000 }); mockSubscriptionFindFirst.mockResolvedValue(null); mockUserUpdate.mockResolvedValue({}); mockTokenBalanceFindUnique.mockResolvedValue(null); mockTokenBalanceCreate.mockResolvedValue({}); const result = await handlePaymentCallback('PAY20260519000001', 'PAY123', 'success'); expect(result.success).toBe(true); expect(result.message).toContain('成功'); }); });