/** * Subscription Service 单元测试 * 测试配额计算、扣除、边界情况 * * 注意:2026-05-29 实施"统一定价中心重构"(BILLING 常量统一系数 2.0x 月价)。 * 测试期望值已与 BILLING.coefficients.monthly=2.0 对齐: * monthlyPrice = costPerMinute(0.016) × monthly(2.0) = 0.032/分钟 * overageBase = 0.032 × overage(1.5) = 0.048/分钟 * 配额分钟数(monthlyMinutes):0=100, 1=300, 2=1000, 3=4000, 4=15000 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { calculateAudioDuration, calculateAudioCost, AUDIO_BILLING_CONFIG, TOKEN_COST_CONFIG, calculateTokenCost, BILLING, } from '@/modules/subscription/subscription.service'; // Mock Prisma vi.mock('@/models', () => ({ prisma: { user: { findUnique: vi.fn(), update: vi.fn(), }, tokenUsage: { create: vi.fn(), count: vi.fn(), }, subscriptionPlan: { findUnique: vi.fn(), findMany: vi.fn(), }, subscription: { findFirst: vi.fn(), }, tokenBalance: { findUnique: vi.fn(), create: vi.fn(), update: vi.fn(), }, }, })); describe('AUDIO_BILLING_CONFIG', () => { it('包含所有会员等级的配置', () => { expect(AUDIO_BILLING_CONFIG.monthlyMinutes).toHaveProperty('0'); // 免费版 expect(AUDIO_BILLING_CONFIG.monthlyMinutes).toHaveProperty('1'); // 入门版 expect(AUDIO_BILLING_CONFIG.monthlyMinutes).toHaveProperty('2'); // 专业版 expect(AUDIO_BILLING_CONFIG.monthlyMinutes).toHaveProperty('3'); // 旗舰版 expect(AUDIO_BILLING_CONFIG.monthlyMinutes).toHaveProperty('4'); // 企业版 }); it('免费版不支持超额', () => { expect(AUDIO_BILLING_CONFIG.overagePrice[0]).toBeNull(); }); it('其他等级支持超额', () => { // 实际值:入门版¥0.048, 专业版¥0.042, 旗舰版¥0.036, 企业版¥0.032 expect(AUDIO_BILLING_CONFIG.overagePrice[1]).toBe(0.048); expect(AUDIO_BILLING_CONFIG.overagePrice[2]).toBe(0.042); expect(AUDIO_BILLING_CONFIG.overagePrice[3]).toBe(0.036); expect(AUDIO_BILLING_CONFIG.overagePrice[4]).toBe(0.032); }); }); describe('BILLING 统一定价中心', () => { it('月价系数为 2.0(成本 × 2)', () => { expect(BILLING.coefficients.monthly).toBe(2.0); expect(BILLING.monthlyPrice).toBeCloseTo(0.032, 3); }); it('超额基础价 = 月价 × 1.5', () => { expect(BILLING.coefficients.overage).toBe(1.5); expect(BILLING.overageBasePrice).toBeCloseTo(0.048, 3); }); }); describe('calculateAudioDuration', () => { it('按语速 150 字/分钟 计算', () => { expect(calculateAudioDuration(150)).toBe(1); // 150字 = 1分钟 expect(calculateAudioDuration(300)).toBe(2); // 300字 = 2分钟 expect(calculateAudioDuration(450)).toBe(3); // 450字 = 3分钟 }); it('向上取整', () => { expect(calculateAudioDuration(1)).toBe(1); // 不足1分钟按1分钟计 expect(calculateAudioDuration(151)).toBe(2); // 151字按2分钟计 expect(calculateAudioDuration(299)).toBe(2); // 299字按2分钟计 }); it('大文本计算正确', () => { expect(calculateAudioDuration(1500)).toBe(10); // 1500字 = 10分钟 expect(calculateAudioDuration(15000)).toBe(100); // 15000字 = 100分钟 }); }); describe('calculateAudioCost', () => { // 实际统一定价:¥0.032/分钟(成本¥0.016 × 2) const PRICE_PER_MIN = 0.032; describe('正常计算', () => { it('免费用户 0 已用,1500字生成(10分钟,配额内)', () => { const result = calculateAudioCost(1500, 0, 0); // 1500字,10分钟音频 expect(result.audioMinutes).toBe(10); expect(result.inQuotaMinutes).toBe(10); expect(result.overageMinutes).toBe(0); expect(result.inQuotaPrice).toBeCloseTo(10 * PRICE_PER_MIN, 4); expect(result.overagePrice).toBe(0); expect(result.totalPrice).toBeCloseTo(result.inQuotaPrice, 4); }); it('专业版用户 15000字生成(100分钟,专业版 1000 分钟配额内)', () => { const result = calculateAudioCost(15000, 2, 0); // 15000字,100分钟 expect(result.audioMinutes).toBe(100); // 专业版 1000 分钟配额,剩余 1000 分钟 expect(result.inQuotaMinutes).toBe(100); expect(result.overageMinutes).toBe(0); expect(result.inQuotaPrice).toBeCloseTo(100 * PRICE_PER_MIN, 3); expect(result.totalPrice).toBeCloseTo(result.inQuotaPrice, 3); }); }); describe('超额计费', () => { it('入门版用户:6000字(40分钟),已用 20 分钟,仍在 300 分钟配额内', () => { // 入门版 300 分钟配额,已用 20 分钟,剩余 280 分钟 // 40 分钟音频 远小于 280 分钟剩余,全部配额内 const result = calculateAudioCost(6000, 1, 20); expect(result.audioMinutes).toBe(40); expect(result.inQuotaMinutes).toBe(40); // 全部配额内 expect(result.overageMinutes).toBe(0); // 不超额 // 配额内价格 = 40 分钟 × ¥0.032 = ¥1.28 expect(result.inQuotaPrice).toBeCloseTo(1.28, 3); expect(result.overagePrice).toBe(0); expect(result.totalPrice).toBeCloseTo(1.28, 3); }); it('专业版用户:12000字(80分钟),已用 950 分钟 → 超额', () => { // 专业版 1000 分钟配额,已用 950 分钟,剩余 50 分钟 // 80 分钟音频 → 50 分钟配额内 + 30 分钟超额 const result = calculateAudioCost(12000, 2, 950); expect(result.audioMinutes).toBe(80); expect(result.inQuotaMinutes).toBe(50); // 剩余50分钟配额 expect(result.overageMinutes).toBe(30); // 超额30分钟 // 配额内价格 = 50 分钟 × ¥0.032 = ¥1.6 expect(result.inQuotaPrice).toBeCloseTo(1.6, 3); // 超额价格 = 30 分钟 × ¥0.042 = ¥1.26 expect(result.overagePrice).toBeCloseTo(1.26, 3); expect(result.totalPrice).toBeCloseTo(2.86, 3); // 1.6 + 1.26 }); }); describe('边界值', () => { it('刚好等于配额上限(免费版 100 分钟配额 → 100 分钟音频)', () => { const result = calculateAudioCost(15000, 0, 0); // 100 分钟音频 expect(result.audioMinutes).toBe(100); expect(result.inQuotaMinutes).toBe(100); expect(result.overageMinutes).toBe(0); expect(result.totalPrice).toBeCloseTo(100 * PRICE_PER_MIN, 3); }); it('超出配额 1 分钟(免费版 100 分钟配额 → 101 分钟音频)', () => { const result = calculateAudioCost(15150, 0, 0); // 101 分钟音频 expect(result.audioMinutes).toBe(101); expect(result.inQuotaMinutes).toBe(100); // 只能使用100分钟配额 expect(result.overageMinutes).toBe(1); // 超额1分钟 // 免费版不支持超额,所以超额价格为0 expect(result.overagePrice).toBe(0); }); it('配额已用尽(免费版 100 分钟配额 → 5 分钟音频,已用 100)', () => { const result = calculateAudioCost(750, 0, 100); expect(result.audioMinutes).toBe(5); expect(result.inQuotaMinutes).toBe(0); expect(result.overageMinutes).toBe(5); // 免费版不支持超额 expect(result.overagePrice).toBe(0); }); }); describe('免费版不支持超额', () => { it('免费版超额部分价格为零', () => { const result = calculateAudioCost(150000, 0, 0); // 1000分钟音频 expect(result.overageMinutes).toBe(900); // 1000 - 100 = 900 expect(result.overagePrice).toBe(0); // 免费版不支持超额 }); it('免费版总价为配额内价格', () => { const result = calculateAudioCost(150000, 0, 0); expect(result.totalPrice).toBeCloseTo(result.inQuotaPrice, 4); }); }); describe('estimatedCost 计算', () => { it('estimatedCost 基于成本价计算', () => { const result = calculateAudioCost(1500, 0, 0); // 10分钟 × ¥0.016/分钟(成本) = ¥0.16 expect(result.estimatedCost).toBeCloseTo(10 * AUDIO_BILLING_CONFIG.costPerMinute, 4); }); }); }); describe('calculateTokenCost', () => { it('默认使用 qwen_plus 模型计算', () => { const result = calculateTokenCost(1000); // 1000字 expect(result.aiInputCost).toBeGreaterThan(0); expect(result.aiOutputCost).toBeGreaterThan(0); const expected = result.aiInputCost + result.aiOutputCost + result.ttsCost; expect(result.totalCost).toBeCloseTo(expected, 4); expect(result.suggestedPrice).toBeGreaterThan(result.totalCost); // 有利润 }); it('不同模型成本不同', () => { const resultQwen = calculateTokenCost(1000, 'qwen_plus'); const resultQwenFlash = calculateTokenCost(1000, 'qwen3_flash'); const resultDeepseek = calculateTokenCost(1000, 'deepseek_v3'); // 各模型定价不同,成本应该不同 expect(resultQwen.totalCost).not.toBe(resultQwenFlash.totalCost); expect(resultQwen.totalCost).not.toBe(resultDeepseek.totalCost); expect(resultQwenFlash.totalCost).not.toBe(resultDeepseek.totalCost); }); it('大文本计算正确', () => { const result = calculateTokenCost(10000); expect(result.totalCost).toBeGreaterThan(0); expect(result.ttsCost).toBeGreaterThan(0); }); it('返回值格式正确', () => { const result = calculateTokenCost(1000); expect(result).toHaveProperty('aiInputCost'); expect(result).toHaveProperty('aiOutputCost'); expect(result).toHaveProperty('ttsCost'); expect(result).toHaveProperty('totalCost'); expect(result).toHaveProperty('suggestedPrice'); }); }); describe('配额计算边界情况', () => { it('零文本', () => { const result = calculateAudioCost(0, 0, 0); expect(result.audioMinutes).toBe(0); expect(result.totalPrice).toBe(0); }); it('负数文本返回0或负0', () => { const result = calculateAudioCost(-100, 0, 0); expect(result.audioMinutes).toBeLessThanOrEqual(0); }); it('未知会员等级使用默认配额', () => { const result = calculateAudioCost(1500, 99, 0); // 未知等级 expect(result.audioMinutes).toBe(10); }); it('所有金额精确到分', () => { const result = calculateAudioCost(333, 1, 0); // 不规则数字 // 检查金额没有精度问题 const strTotal = result.totalPrice.toString(); const decimals = strTotal.includes('.') ? strTotal.split('.')[1].length : 0; expect(decimals).toBeLessThanOrEqual(2); }); });