import { prisma } from '../../models'; import { MemberLevel } from '../../types'; // ============================================ // TTS服务商成本配置(2026-04-12 更新) // ============================================ export const TTS_COST_CONFIG = { // TTS服务商配置 providers: { // 免费方案(科大讯飞基础版) iflytek_free: { name: '科大讯飞-基础版', costPerThousand: 0, // 免费 quality: 'standard', voiceOptions: 10, freeQuota: 100000, // 10万/月(永久) novelRating: 3 // ★★★☆☆ }, // 性价比方案(百度基础版) baidu_standard: { name: '百度智能云-基础版', costPerThousand: 3.0, // ¥3/千字符 quality: 'standard', voiceOptions: 20, freeQuota: 2000000, // 200万/月(首年) novelRating: 4 // ★★★★☆ }, // 高质量方案(百度精品版) baidu_premium: { name: '百度智能云-精品版', costPerThousand: 15.0, // ¥15/千字符 quality: 'premium', voiceOptions: 50, freeQuota: 0, novelRating: 5 // ★★★★★ }, // 阿里云方案 aliyun_plus: { name: '阿里云Qwen3-Plus', costPerThousand: 12.0, // ¥12/千字符 quality: 'high', voiceOptions: 30, freeQuota: 0, novelRating: 5 // ★★★★★ }, // 企业方案(火山引擎) volcano_premium: { name: '火山引擎豆包-标准版', costPerThousand: 50.0, // ¥50/千字符 quality: 'premium', voiceOptions: 40, freeQuota: 20000, // 2万/应用 novelRating: 5 // ★★★★★ } }, // AI模型成本配置 aiModels: { deepseek: { name: 'DeepSeek V3.2', inputCost: 2.0, // ¥2.0/千token outputCost: 8.0, // ¥8.0/千token quality: 'standard', default: true }, qwen_plus: { name: '通义Qwen Plus', inputCost: 4.0, // ¥4.0/千token outputCost: 12.0, // ¥12.0/千token quality: 'high' }, doubao_pro: { name: '豆包 Pro', inputCost: 2.0, // ¥2.0/千token outputCost: 6.0, // ¥6.0/千token quality: 'high' } }, // 各等级默认TTS方案 planTTSProvider: { 0: 'iflytek_free', // 免费版:科大讯飞免费 1: 'baidu_standard', // 入门版:百度基础 2: 'baidu_premium', // 专业版:百度精品 3: 'volcano_premium' // 旗舰版:火山引擎 }, // 默认AI模型 defaultAIModel: 'deepseek' }; // ============================================ // Token成本配置(AI生成成本 + TTS成本) // ============================================ export const TOKEN_COST_CONFIG = { // 基础Token成本(DeepSeek V3.2 + 百度基础TTS) baseCostPerThousand: 5.0, // AI: ¥2 + TTS: ¥3 = ¥5/千字符 // 超额使用单价 extraPricePerThousand: 6.0, // 略高于成本价 // 模型选择 models: { deepseek: { name: 'DeepSeek V3.2', inputCost: 2.0, // ¥2.0/千token outputCost: 8.0, // ¥8.0/千token quality: 'standard', default: true }, qwen_plus: { name: '通义 Qwen Plus', inputCost: 4.0, // ¥4.0/千token outputCost: 12.0, // ¥12.0/千token quality: 'high', extraFee: 1.5 // 每千token额外收费 }, doubao_pro: { name: '豆包 Pro', inputCost: 2.0, // ¥2.0/千token outputCost: 6.0, // ¥6.0/千token quality: 'high' } } }; // 计算Token生成成本 export function calculateTokenCost(textLength: number, model: string = 'deepseek'): { inputCost: number; outputCost: number; totalCost: number; suggestedPrice: number; } { const modelConfig = TOKEN_COST_CONFIG.models[model] || TOKEN_COST_CONFIG.models.deepseek; // 输入成本 const inputTokens = textLength; // 1字 ≈ 1 token const inputCost = (inputTokens / 1000) * modelConfig.inputCost; // 输出成本(音频合成) const outputCost = textLength * 0.001 * modelConfig.outputCost * 0.1; // 总成本 const totalCost = inputCost + outputCost; // 建议售价(成本 × 1.5,保证50%利润) const suggestedPrice = totalCost * 1.5; return { inputCost: Math.round(inputCost * 100) / 100, outputCost: Math.round(outputCost * 100) / 100, totalCost: Math.round(totalCost * 100) / 100, suggestedPrice: Math.round(suggestedPrice * 100) / 100 }; } // 默认套餐配置(包月批发+按量零售,2026-04-12定稿) export const DEFAULT_PLANS = [ { name: '免费版', level: 0, priceMonthly: 0, priceYearly: 0, description: '适合轻度体验', features: JSON.stringify([ '每天3次生成', '每月10分钟音频', '每次最多2分钟', '基础音色5种', '标准音质' ]), isRecommended: false, sortOrder: 0, dailyGenerations: 3, perGenerationLimit: 300, // ~2分钟音频 monthlyTokens: 1500, monthlyMinutes: 10, // 音频时长配额 yearlyTokens: null, voiceOptions: 5, audioQuality: 'standard', apiAccess: false, batchProcessing: false, teamManagement: false, overageEnabled: false, // 不支持超出配额 overagePrice: 0, pricing: { monthlyPricePerMinute: 5.0, // ¥5/分钟(包月) overagePricePerMinute: null // 不支持按量 } }, { name: '入门版', level: 1, priceMonthly: 19, priceYearly: 190, description: '适合轻度使用', features: JSON.stringify([ '每天10次生成', '每月30分钟音频', '每次最多5分钟', '全部音色', '高清音质', '超出¥8/分钟' ]), isRecommended: false, sortOrder: 1, dailyGenerations: 10, perGenerationLimit: 750, // ~5分钟音频 monthlyTokens: 4500, monthlyMinutes: 30, yearlyTokens: null, voiceOptions: -1, audioQuality: 'high', apiAccess: false, batchProcessing: false, teamManagement: false, overageEnabled: true, overagePrice: 8.0, // 超出¥8/分钟(零售价) pricing: { monthlyPricePerMinute: 5.0, // ¥5/分钟(批发) overagePricePerMinute: 8.0 // ¥8/分钟(零售) } }, { name: '专业版', level: 2, priceMonthly: 59, priceYearly: 590, description: '适合内容创作者 ⭐推荐', features: JSON.stringify([ '无限制生成', '每月100分钟音频', '每次最多10分钟', '全部音色+定制音色', '高清音质', '超出¥7/分钟' ]), isRecommended: true, sortOrder: 2, dailyGenerations: -1, perGenerationLimit: 1500, // ~10分钟音频 monthlyTokens: 15000, monthlyMinutes: 100, yearlyTokens: null, voiceOptions: -1, audioQuality: 'high', apiAccess: false, batchProcessing: false, teamManagement: false, overageEnabled: true, overagePrice: 7.0, // 超出¥7/分钟 pricing: { monthlyPricePerMinute: 5.0, overagePricePerMinute: 7.0 } }, { name: '旗舰版', level: 3, priceMonthly: 199, priceYearly: 1990, description: '适合重度使用', features: JSON.stringify([ '无限制生成', '每月400分钟音频', '每次最多20分钟', '全部音色+定制音色', '高清音质', 'VIP优先队列', '超出¥6/分钟' ]), isRecommended: false, sortOrder: 3, dailyGenerations: -1, perGenerationLimit: 3000, // ~20分钟音频 monthlyTokens: 60000, monthlyMinutes: 400, yearlyTokens: null, voiceOptions: -1, audioQuality: 'high', apiAccess: true, batchProcessing: false, teamManagement: false, overageEnabled: true, overagePrice: 6.0, // 超出¥6/分钟 pricing: { monthlyPricePerMinute: 5.0, overagePricePerMinute: 6.0 } }, { name: '企业版', level: 4, priceMonthly: 599, priceYearly: 5990, description: '适合企业用户', features: JSON.stringify([ '无限制生成', '每月1500分钟音频', '每次最多60分钟', '全部功能', '高清音质', '专属技术支持', '批量处理', '团队管理', '超出¥5/分钟' ]), isRecommended: false, sortOrder: 4, dailyGenerations: -1, perGenerationLimit: 9000, // ~60分钟音频 monthlyTokens: 225000, monthlyMinutes: 1500, yearlyTokens: null, voiceOptions: -1, audioQuality: 'high', apiAccess: true, batchProcessing: true, teamManagement: true, overageEnabled: true, overagePrice: 5.0, // 超出¥5/分钟(最低零售价) pricing: { monthlyPricePerMinute: 5.0, overagePricePerMinute: 5.0 } } ]; // 初始化套餐数据 export async function initializePlans() { const existingPlans = await prisma.subscriptionPlan.count(); if (existingPlans === 0) { console.log('初始化订阅套餐数据...'); for (const plan of DEFAULT_PLANS) { await prisma.subscriptionPlan.create({ data: plan }); } console.log('订阅套餐初始化完成'); } } // 获取所有套餐列表 export async function getPlans() { const plans = await prisma.subscriptionPlan.findMany({ where: { isActive: true }, orderBy: { sortOrder: 'asc' } }); return plans.map(plan => ({ ...plan, priceMonthly: Number(plan.priceMonthly), priceYearly: Number(plan.priceYearly), features: JSON.parse(plan.features || '[]') })); } // 获取单个套餐详情 export async function getPlanById(planId: number) { const plan = await prisma.subscriptionPlan.findUnique({ where: { id: planId } }); if (!plan) { throw new Error('套餐不存在'); } return { ...plan, priceMonthly: Number(plan.priceMonthly), priceYearly: Number(plan.priceYearly), features: JSON.parse(plan.features || '[]') }; } // 获取用户当前订阅信息 export async function getUserSubscription(userId: number) { const subscription = await prisma.subscription.findFirst({ where: { userId, status: 'active', endDate: { gt: new Date() } }, include: { plan: true }, orderBy: { endDate: 'desc' } }); return subscription; } // 获取用户Token余额 export async function getUserTokenBalance(userId: number) { let balance = await prisma.tokenBalance.findUnique({ where: { userId } }); if (!balance) { // 初始化Token余额 balance = await prisma.tokenBalance.create({ data: { userId, totalTokens: 10000, // 默认免费额度 usedTokens: 0 } }); } const remaining = balance.totalTokens - balance.usedTokens; return { totalTokens: balance.totalTokens, usedTokens: balance.usedTokens, remainingTokens: remaining, resetDate: balance.resetDate, isUnlimited: balance.totalTokens === -1 }; } // 获取Token使用记录 export async function getTokenUsageList(userId: number, page: number = 1, pageSize: number = 20) { const where = { userId }; const total = await prisma.tokenUsage.count({ where }); const list = await prisma.tokenUsage.findMany({ where, orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize }); return { list, total, page, pageSize, totalPages: Math.ceil(total / pageSize) }; } // 消耗Token export async function consumeToken( userId: number, amount: number, type: string, contentLength: number, description?: string ) { const balance = await prisma.tokenBalance.findUnique({ where: { userId } }); if (!balance) { throw new Error('用户Token余额不存在'); } if (balance.totalTokens !== -1 && balance.usedTokens + amount > balance.totalTokens) { throw new Error('Token余额不足'); } // 更新余额 await prisma.tokenBalance.update({ where: { userId }, data: { usedTokens: { increment: amount } } }); // 记录使用 const usage = await prisma.tokenUsage.create({ data: { userId, type, amount, contentLength, description } }); return usage; } // 获取用户配额(兼容旧接口) export async function getUserQuota(userId: number) { const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new Error('用户不存在'); } const subscription = await getUserSubscription(userId); const tokenBalance = await getUserTokenBalance(userId); const memberLevel = user.memberLevel as MemberLevel; const levelNames = ['免费版', '基础版', '专业版', '旗舰版']; return { level: memberLevel, levelName: levelNames[memberLevel] || '免费版', isValid: subscription !== null, expireAt: subscription?.endDate, plan: subscription?.plan ? { name: subscription.plan.name, features: JSON.parse(subscription.plan.features || '[]') } : null, tokenBalance, limits: subscription?.plan ? { dailyGenerations: subscription.plan.dailyGenerations, perGenerationLimit: subscription.plan.perGenerationLimit, monthlyTokens: subscription.plan.monthlyTokens, voiceOptions: subscription.plan.voiceOptions, audioQuality: subscription.plan.audioQuality } : { dailyGenerations: 3, perGenerationLimit: 2000, monthlyTokens: 10000, voiceOptions: 5, audioQuality: 'standard' } }; } // 检查用户配额 export async function checkQuota(userId: number, requiredTokens: number) { const quota = await getUserQuota(userId); const balance = quota.tokenBalance; if (balance.isUnlimited) { return { allowed: true, reason: null }; } if (balance.remainingTokens < requiredTokens) { return { allowed: false, reason: 'Token余额不足', required: requiredTokens, remaining: balance.remainingTokens }; } return { allowed: true, reason: null }; } // ============================================ // 音频时长计费系统(2026-04-12 定稿) // ============================================ export const AUDIO_BILLING_CONFIG = { // 成本配置(综合TTS+AI成本) costPerMinute: 4.0, // 成本:¥4/分钟(AI文本生成+TTS合成) // 定价策略:包月=批发价,按量=零售价 pricing: { // 包月(批发价) monthly: { basePricePerMinute: 5.0, // ¥5/分钟(30%利润) }, // 按量(零售价) payAsYouGo: { pricePerMinute: 8.0, // ¥8/分钟(100%利润) } }, // 语速配置(字/分钟) speakingRate: 150, // 各等级每月音频时长配额(分钟) monthlyMinutes: { 0: 10, // 免费版:10分钟/月 1: 30, // 入门版:30分钟/月 2: 100, // 专业版:100分钟/月 3: 400, // 旗舰版:400分钟/月 4: 1500 // 企业版:1500分钟/月 }, // 各等级超出配额单价(元/分钟) overagePrice: { 0: null, // 免费版:不支持超出 1: 8.0, // 入门版:¥8/分钟 2: 7.0, // 专业版:¥7/分钟 3: 6.0, // 旗舰版:¥6/分钟 4: 5.0 // 企业版:¥5/分钟 }, // 各等级单次生成最大时长(分钟) maxMinutesPerGeneration: { 0: 2, // 免费版:2分钟/次 1: 5, // 入门版:5分钟/次 2: 10, // 专业版:10分钟/次 3: 20, // 旗舰版:20分钟/次 4: 60 // 企业版:60分钟/次 } }; // 根据文本长度计算音频时长 export function calculateAudioDuration(textLength: number): number { return Math.ceil(textLength / AUDIO_BILLING_CONFIG.speakingRate); } // 计算音频生成费用(支持包月配额和按量计费) export function calculateAudioCost( textLength: number, memberLevel: number = 0, usedMinutes: number = 0 ): { textLength: number; audioMinutes: number; inQuotaMinutes: number; overageMinutes: number; inQuotaPrice: number; // 配额内价格 overagePrice: number; // 超额价格 totalPrice: number; estimatedCost: number; } { const audioMinutes = calculateAudioDuration(textLength); const monthlyMinutes = AUDIO_BILLING_CONFIG.monthlyMinutes[memberLevel] || 10; const overagePricePerMinute = AUDIO_BILLING_CONFIG.overagePrice[memberLevel]; // 计算配额内和超额分钟数 const remainingQuota = Math.max(0, monthlyMinutes - usedMinutes); const inQuotaMinutes = Math.min(audioMinutes, remainingQuota); const overageMinutes = Math.max(0, audioMinutes - inQuotaMinutes); // 配额内价格(包月批发价) const inQuotaPrice = inQuotaMinutes * AUDIO_BILLING_CONFIG.pricing.monthly.basePricePerMinute; // 超额价格(按量零售价) const overagePrice = overageMinutes > 0 && overagePricePerMinute !== null ? overageMinutes * overagePricePerMinute : 0; const totalPrice = Math.round((inQuotaPrice + overagePrice) * 100) / 100; return { textLength, audioMinutes, inQuotaMinutes, overageMinutes, inQuotaPrice: Math.round(inQuotaPrice * 100) / 100, overagePrice: Math.round(overagePrice * 100) / 100, totalPrice, estimatedCost: audioMinutes * AUDIO_BILLING_CONFIG.costPerMinute }; } // 获取用户音频时长余额 export async function getUserAudioBalance(userId: number) { const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new Error('用户不存在'); } const memberLevel = user.memberLevel as MemberLevel; const monthlyMinutes = AUDIO_BILLING_CONFIG.monthlyMinutes[memberLevel] || 10; const overageEnabled = AUDIO_BILLING_CONFIG.overagePrice[memberLevel] !== null; const overagePrice = AUDIO_BILLING_CONFIG.overagePrice[memberLevel]; // 检查是否需要重置月度配额 const now = new Date(); const resetDate = user.subscriptionResetDate; let totalMinutes = monthlyMinutes; let usedMinutes = user.usedAudioMinutes || 0; // 每月1日重置配额 if (resetDate) { const lastReset = new Date(resetDate); if (now.getMonth() !== lastReset.getMonth() || now.getFullYear() !== lastReset.getFullYear()) { totalMinutes = monthlyMinutes; usedMinutes = 0; await prisma.user.update({ where: { id: userId }, data: { usedAudioMinutes: 0, subscriptionResetDate: now } }); } } return { memberLevel, totalMinutes, usedMinutes, remainingMinutes: totalMinutes - usedMinutes, overageEnabled, overagePrice: overagePrice || 0, resetDate: user.subscriptionResetDate }; } // 检查音频生成配额 export async function checkAudioQuota( userId: number, textLength: number ) { const audioMinutes = calculateAudioDuration(textLength); const balance = await getUserAudioBalance(userId); const maxMinutes = AUDIO_BILLING_CONFIG.maxMinutesPerGeneration[balance.memberLevel] || 2; // 检查单次限制 if (audioMinutes > maxMinutes) { return { allowed: false, reason: `单次生成不能超过${maxMinutes}分钟`, audioMinutes, maxMinutes }; } // 检查月度配额 if (balance.remainingMinutes < audioMinutes && !balance.overageEnabled) { return { allowed: false, reason: `音频时长不足,本月配额已用完`, audioMinutes, remainingMinutes: balance.remainingMinutes, totalMinutes: balance.totalMinutes, overageEnabled: false }; } return { allowed: true, reason: null, audioMinutes, inQuotaMinutes: Math.min(audioMinutes, balance.remainingMinutes), overageMinutes: Math.max(0, audioMinutes - balance.remainingMinutes), remainingMinutes: balance.remainingMinutes, totalMinutes: balance.totalMinutes, overageEnabled: balance.overageEnabled, overagePrice: balance.overagePrice }; } // 扣除音频时长(生成完成后调用) export async function consumeAudioMinutes( userId: number, textLength: number, description?: string ) { const audioMinutes = calculateAudioDuration(textLength); const balance = await getUserAudioBalance(userId); const cost = calculateAudioCost(textLength, balance.memberLevel, balance.usedMinutes); // 更新用户已使用时长 await prisma.user.update({ where: { id: userId }, data: { usedAudioMinutes: { increment: audioMinutes } } }); // 记录使用日志 const usage = await prisma.tokenUsage.create({ data: { userId, type: 'audio_generation', amount: audioMinutes, contentLength: textLength, description: description || `${audioMinutes}分钟音频` } }); return { usageId: usage.id, audioMinutes, textLength, inQuotaMinutes: cost.inQuotaMinutes, overageMinutes: cost.overageMinutes, inQuotaPrice: cost.inQuotaPrice, overagePrice: cost.overagePrice, totalPrice: cost.totalPrice }; } // 获取音频生成预估(用于UI展示) export async function getAudioEstimate( userId: number, textLength: number ) { const balance = await getUserAudioBalance(userId); const cost = calculateAudioCost(textLength, balance.memberLevel, balance.usedMinutes); const quota = await checkAudioQuota(userId, textLength); return { // 预估信息 textLength, audioMinutes: cost.audioMinutes, inQuotaMinutes: cost.inQuotaMinutes, overageMinutes: cost.overageMinutes, inQuotaPrice: cost.inQuotaPrice, overagePrice: cost.overagePrice, estimatedPrice: cost.totalPrice, estimatedCost: cost.estimatedCost, // 用户配额 remainingMinutes: balance.remainingMinutes, totalMinutes: balance.totalMinutes, usedMinutes: balance.usedMinutes, // 计费策略 overageEnabled: balance.overageEnabled, overagePrice: balance.overagePrice, // 是否允许生成 canGenerate: quota.allowed, reason: quota.reason, // 显示文本 displayText: quota.allowed ? cost.overageMinutes > 0 ? `预计${cost.audioMinutes}分钟(配额${cost.inQuotaMinutes}分钟+超出${cost.overageMinutes}分钟),约¥${cost.totalPrice}` : `预计${cost.audioMinutes}分钟,约¥${cost.totalPrice}` : quota.reason }; }