|
|
@@ -154,7 +154,7 @@ export function calculateTokenCost(textLength: number, model: string = 'deepseek
|
|
|
};
|
|
|
}
|
|
|
|
|
|
-// 默认套餐配置(基于TTS成本,2026-04-12更新)
|
|
|
+// 默认套餐配置(基于音频时长,2026-04-12更新)
|
|
|
export const DEFAULT_PLANS = [
|
|
|
{
|
|
|
name: '免费版',
|
|
|
@@ -164,30 +164,28 @@ export const DEFAULT_PLANS = [
|
|
|
description: '适合轻度体验',
|
|
|
features: JSON.stringify([
|
|
|
'每天3次生成',
|
|
|
- '每次最多2000字',
|
|
|
+ '每月30分钟音频',
|
|
|
+ '每次最多2分钟',
|
|
|
'基础音色5种',
|
|
|
'标准音质',
|
|
|
- '科大讯飞免费额度(10万/月永久)',
|
|
|
- 'Token成本:¥17.5/月(平台补贴)'
|
|
|
+ '科大讯飞TTS(免费额度)'
|
|
|
]),
|
|
|
isRecommended: false,
|
|
|
sortOrder: 0,
|
|
|
dailyGenerations: 3,
|
|
|
- perGenerationLimit: 2000,
|
|
|
- monthlyTokens: 5000, // AI+基础TTS成本:¥17.5
|
|
|
+ perGenerationLimit: 300, // ~2分钟音频
|
|
|
+ monthlyTokens: 3000, // 文本配额
|
|
|
+ monthlyMinutes: 30, // 音频时长配额(新增)
|
|
|
yearlyTokens: null,
|
|
|
voiceOptions: 5,
|
|
|
audioQuality: 'standard',
|
|
|
apiAccess: false,
|
|
|
batchProcessing: false,
|
|
|
teamManagement: false,
|
|
|
- // 成本分析
|
|
|
costAnalysis: {
|
|
|
- tokenCost: 17.5, // Token成本
|
|
|
- platformSubsidy: true, // 平台补贴
|
|
|
- profitMargin: 0,
|
|
|
- ttsProvider: '科大讯飞-基础版',
|
|
|
- aiModel: 'DeepSeek V3.2'
|
|
|
+ minutesCost: 10.2, // 30分钟 × ¥0.34
|
|
|
+ platformSubsidy: true,
|
|
|
+ strategy: '平台补贴引流'
|
|
|
}
|
|
|
},
|
|
|
{
|
|
|
@@ -197,31 +195,30 @@ export const DEFAULT_PLANS = [
|
|
|
priceYearly: 99,
|
|
|
description: '适合日常使用',
|
|
|
features: JSON.stringify([
|
|
|
- '每月10000 Token',
|
|
|
- '每次最多5000字',
|
|
|
+ '每天10次生成',
|
|
|
+ '每月100分钟音频',
|
|
|
+ '每次最多5分钟',
|
|
|
'全部音色',
|
|
|
'高清音质',
|
|
|
- '百度基础版TTS(¥3/千字符)',
|
|
|
- 'Token成本:¥35/月'
|
|
|
+ '百度基础版TTS'
|
|
|
]),
|
|
|
isRecommended: false,
|
|
|
sortOrder: 1,
|
|
|
dailyGenerations: 10,
|
|
|
- perGenerationLimit: 5000,
|
|
|
- monthlyTokens: 10000, // AI+基础TTS成本:¥35
|
|
|
+ perGenerationLimit: 750, // ~5分钟音频
|
|
|
+ monthlyTokens: 15000,
|
|
|
+ monthlyMinutes: 100, // 音频时长配额(新增)
|
|
|
yearlyTokens: null,
|
|
|
voiceOptions: -1,
|
|
|
audioQuality: 'high',
|
|
|
apiAccess: false,
|
|
|
batchProcessing: false,
|
|
|
teamManagement: false,
|
|
|
- // 成本分析(亏损,用于获客转化)
|
|
|
costAnalysis: {
|
|
|
- tokenCost: 35.0,
|
|
|
+ minutesCost: 100, // 100分钟 × ¥1.0
|
|
|
monthlyPrice: 9.9,
|
|
|
- loss: -25.1, // 每单亏损,获客用
|
|
|
- strategy: '获客转化',
|
|
|
- ttsProvider: '百度智能云-基础版'
|
|
|
+ loss: -90.1,
|
|
|
+ strategy: '亏损获客转化'
|
|
|
}
|
|
|
},
|
|
|
{
|
|
|
@@ -231,33 +228,31 @@ export const DEFAULT_PLANS = [
|
|
|
priceYearly: 490,
|
|
|
description: '适合内容创作者 ⭐推荐',
|
|
|
features: JSON.stringify([
|
|
|
- '每月30000 Token',
|
|
|
- '每次最多20000字',
|
|
|
+ '无限制生成',
|
|
|
+ '每月500分钟音频',
|
|
|
+ '每次最多20分钟',
|
|
|
'全部音色+定制音色',
|
|
|
- '高清音质',
|
|
|
- '百度精品版TTS(¥15/千字符)',
|
|
|
- 'VIP优先队列',
|
|
|
- 'Token成本:¥105/月'
|
|
|
+ '无损音质',
|
|
|
+ '百度精品版TTS',
|
|
|
+ 'VIP优先队列'
|
|
|
]),
|
|
|
isRecommended: true,
|
|
|
sortOrder: 2,
|
|
|
dailyGenerations: -1,
|
|
|
- perGenerationLimit: 20000,
|
|
|
- monthlyTokens: 30000, // AI+精品TTS成本:¥105
|
|
|
+ perGenerationLimit: 3000, // ~20分钟音频
|
|
|
+ monthlyTokens: 75000,
|
|
|
+ monthlyMinutes: 500, // 音频时长配额(新增)
|
|
|
yearlyTokens: null,
|
|
|
voiceOptions: -1,
|
|
|
audioQuality: 'lossless',
|
|
|
apiAccess: false,
|
|
|
batchProcessing: false,
|
|
|
teamManagement: false,
|
|
|
- // 成本分析(部分补贴,核心盈利)
|
|
|
costAnalysis: {
|
|
|
- tokenCost: 105.0,
|
|
|
+ minutesCost: 500, // 500分钟 × ¥1.0
|
|
|
monthlyPrice: 49,
|
|
|
- loss: -56.0, // 部分补贴
|
|
|
- profitMargin: 0, // 保本微亏
|
|
|
- strategy: '核心转化产品',
|
|
|
- ttsProvider: '百度智能云-精品版'
|
|
|
+ loss: -451,
|
|
|
+ strategy: '亏损转化,盈利靠续费'
|
|
|
}
|
|
|
},
|
|
|
{
|
|
|
@@ -267,35 +262,34 @@ export const DEFAULT_PLANS = [
|
|
|
priceYearly: 1999,
|
|
|
description: '适合企业用户',
|
|
|
features: JSON.stringify([
|
|
|
- '每月100000 Token',
|
|
|
- '每次最多100000字',
|
|
|
+ '无限制生成',
|
|
|
+ '每月2000分钟音频',
|
|
|
+ '每次最多60分钟',
|
|
|
'全部功能',
|
|
|
'无损音质',
|
|
|
- '火山引擎豆包TTS(¥50/千字符)',
|
|
|
+ '火山引擎豆包TTS',
|
|
|
'专属技术支持',
|
|
|
'批量处理',
|
|
|
- '团队管理',
|
|
|
- 'Token成本:¥350/月'
|
|
|
+ '团队管理'
|
|
|
]),
|
|
|
isRecommended: false,
|
|
|
sortOrder: 3,
|
|
|
- dailyGenerations: -1, // 无限制
|
|
|
- perGenerationLimit: 100000,
|
|
|
- monthlyTokens: 100000, // AI+高端TTS成本:¥350
|
|
|
+ dailyGenerations: -1,
|
|
|
+ perGenerationLimit: 9000, // ~60分钟音频
|
|
|
+ monthlyTokens: 300000,
|
|
|
+ monthlyMinutes: 2000, // 音频时长配额(新增)
|
|
|
yearlyTokens: null,
|
|
|
voiceOptions: -1,
|
|
|
audioQuality: 'lossless',
|
|
|
apiAccess: true,
|
|
|
batchProcessing: true,
|
|
|
teamManagement: true,
|
|
|
- // 成本分析(盈利)
|
|
|
costAnalysis: {
|
|
|
- tokenCost: 350.0,
|
|
|
+ minutesCost: 680, // 2000分钟 × ¥0.34
|
|
|
monthlyPrice: 199,
|
|
|
- profit: -151.0, // 仍有亏损,建议涨价
|
|
|
- profitMargin: 0,
|
|
|
- suggestion: '建议定价¥399-499/月',
|
|
|
- ttsProvider: '火山引擎豆包'
|
|
|
+ loss: -481,
|
|
|
+ suggestion: '建议定价¥699-999/月',
|
|
|
+ strategy: '高端定位,低价策略'
|
|
|
}
|
|
|
}
|
|
|
];
|
|
|
@@ -516,3 +510,231 @@ export async function checkQuota(userId: number, requiredTokens: number) {
|
|
|
|
|
|
return { allowed: true, reason: null };
|
|
|
}
|
|
|
+
|
|
|
+// ============================================
|
|
|
+// 音频时长计费系统(2026-04-12 新增)
|
|
|
+// ============================================
|
|
|
+
|
|
|
+export const AUDIO_BILLING_CONFIG = {
|
|
|
+ // 语速配置(字/分钟)
|
|
|
+ speakingRate: 150, // 平均语速150字/分钟
|
|
|
+
|
|
|
+ // 音频时长单价(元/分钟)
|
|
|
+ pricePerMinute: {
|
|
|
+ standard: 0.5, // 标准音质:¥0.5/分钟
|
|
|
+ high: 1.0, // 高清音质:¥1.0/分钟
|
|
|
+ lossless: 2.0 // 无损音质:¥2.0/分钟
|
|
|
+ },
|
|
|
+
|
|
|
+ // 各等级每月音频时长配额
|
|
|
+ monthlyMinutes: {
|
|
|
+ 0: 30, // 免费版:30分钟/月
|
|
|
+ 1: 100, // 入门版:100分钟/月
|
|
|
+ 2: 500, // 专业版:500分钟/月
|
|
|
+ 3: 2000 // 旗舰版:2000分钟/月
|
|
|
+ },
|
|
|
+
|
|
|
+ // 各等级单次生成最大时长(分钟)
|
|
|
+ maxMinutesPerGeneration: {
|
|
|
+ 0: 2, // 免费版:2分钟/次
|
|
|
+ 1: 5, // 入门版:5分钟/次
|
|
|
+ 2: 20, // 专业版:20分钟/次
|
|
|
+ 3: 60 // 旗舰版:60分钟/次
|
|
|
+ },
|
|
|
+
|
|
|
+ // 成本计算(用于内部核算)
|
|
|
+ costPerMinute: {
|
|
|
+ standard: 0.34, // AI¥0.2 + TTS¥0.14 = ¥0.34/分钟
|
|
|
+ high: 1.0, // AI¥0.2 + TTS¥0.8 = ¥1.0/分钟
|
|
|
+ lossless: 3.4 // AI¥0.4 + TTS¥3.0 = ¥3.4/分钟
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 根据文本长度计算音频时长
|
|
|
+export function calculateAudioDuration(textLength: number): number {
|
|
|
+ return Math.ceil(textLength / AUDIO_BILLING_CONFIG.speakingRate);
|
|
|
+}
|
|
|
+
|
|
|
+// 计算音频生成费用
|
|
|
+export function calculateAudioCost(
|
|
|
+ textLength: number,
|
|
|
+ quality: 'standard' | 'high' | 'lossless' = 'high'
|
|
|
+): {
|
|
|
+ textLength: number;
|
|
|
+ audioMinutes: number;
|
|
|
+ pricePerMinute: number;
|
|
|
+ totalPrice: number;
|
|
|
+ estimatedCost: number;
|
|
|
+ remainingMinutes: number;
|
|
|
+} {
|
|
|
+ const audioMinutes = calculateAudioDuration(textLength);
|
|
|
+ const pricePerMinute = AUDIO_BILLING_CONFIG.pricePerMinute[quality] || 1.0;
|
|
|
+ const totalPrice = Math.round(audioMinutes * pricePerMinute * 100) / 100;
|
|
|
+
|
|
|
+ return {
|
|
|
+ textLength,
|
|
|
+ audioMinutes,
|
|
|
+ pricePerMinute,
|
|
|
+ totalPrice,
|
|
|
+ estimatedCost: AUDIO_BILLING_CONFIG.costPerMinute[quality] * audioMinutes,
|
|
|
+ remainingMinutes: 0 // 需要查询用户余额才能确定
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 获取用户音频时长余额
|
|
|
+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] || 30;
|
|
|
+
|
|
|
+ // 检查是否需要重置月度配额
|
|
|
+ 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 {
|
|
|
+ totalMinutes,
|
|
|
+ usedMinutes,
|
|
|
+ remainingMinutes: totalMinutes - usedMinutes,
|
|
|
+ isUnlimited: memberLevel >= 3, // 旗舰版及以上无限制
|
|
|
+ resetDate: user.subscriptionResetDate
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 检查音频生成配额
|
|
|
+export async function checkAudioQuota(
|
|
|
+ userId: number,
|
|
|
+ textLength: number,
|
|
|
+ quality: 'standard' | 'high' | 'lossless' = 'high'
|
|
|
+) {
|
|
|
+ const audioMinutes = calculateAudioDuration(textLength);
|
|
|
+ const balance = await getUserAudioBalance(userId);
|
|
|
+ const maxMinutes = AUDIO_BILLING_CONFIG.maxMinutesPerGeneration[balance.isUnlimited ? 3 : 0] || 2;
|
|
|
+
|
|
|
+ // 检查单次限制
|
|
|
+ if (audioMinutes > maxMinutes) {
|
|
|
+ return {
|
|
|
+ allowed: false,
|
|
|
+ reason: `单次生成不能超过${maxMinutes}分钟`,
|
|
|
+ audioMinutes,
|
|
|
+ maxMinutes
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查月度配额
|
|
|
+ if (!balance.isUnlimited && balance.remainingMinutes < audioMinutes) {
|
|
|
+ return {
|
|
|
+ allowed: false,
|
|
|
+ reason: `音频时长不足,需要${audioMinutes}分钟,剩余${balance.remainingMinutes}分钟`,
|
|
|
+ audioMinutes,
|
|
|
+ remainingMinutes: balance.remainingMinutes,
|
|
|
+ totalMinutes: balance.totalMinutes
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ allowed: true,
|
|
|
+ reason: null,
|
|
|
+ audioMinutes,
|
|
|
+ remainingMinutes: balance.remainingMinutes - audioMinutes,
|
|
|
+ totalMinutes: balance.totalMinutes
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 扣除音频时长(生成完成后调用)
|
|
|
+export async function consumeAudioMinutes(
|
|
|
+ userId: number,
|
|
|
+ textLength: number,
|
|
|
+ quality: 'standard' | 'high' | 'lossless' = 'high',
|
|
|
+ description?: string
|
|
|
+) {
|
|
|
+ const audioMinutes = calculateAudioDuration(textLength);
|
|
|
+ const cost = calculateAudioCost(textLength, quality);
|
|
|
+
|
|
|
+ // 更新用户已使用时长
|
|
|
+ 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}分钟${quality}音质音频`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ usageId: usage.id,
|
|
|
+ audioMinutes,
|
|
|
+ textLength,
|
|
|
+ quality,
|
|
|
+ cost: cost.totalPrice
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 获取音频生成预估(用于UI展示)
|
|
|
+export async function getAudioEstimate(
|
|
|
+ userId: number,
|
|
|
+ textLength: number,
|
|
|
+ quality: 'standard' | 'high' | 'lossless' = 'high'
|
|
|
+) {
|
|
|
+ const cost = calculateAudioCost(textLength, quality);
|
|
|
+ const quota = await checkAudioQuota(userId, textLength, quality);
|
|
|
+ const balance = await getUserAudioBalance(userId);
|
|
|
+
|
|
|
+ return {
|
|
|
+ // 预估信息
|
|
|
+ textLength,
|
|
|
+ audioMinutes: cost.audioMinutes,
|
|
|
+ pricePerMinute: cost.pricePerMinute,
|
|
|
+ estimatedPrice: cost.totalPrice,
|
|
|
+ estimatedCost: cost.estimatedCost,
|
|
|
+
|
|
|
+ // 用户配额
|
|
|
+ remainingMinutes: quota.allowed
|
|
|
+ ? quota.remainingMinutes
|
|
|
+ : balance.remainingMinutes,
|
|
|
+ totalMinutes: balance.totalMinutes,
|
|
|
+
|
|
|
+ // 是否允许生成
|
|
|
+ canGenerate: quota.allowed,
|
|
|
+ reason: quota.reason,
|
|
|
+
|
|
|
+ // 显示文本
|
|
|
+ displayText: quota.allowed
|
|
|
+ ? `预计${cost.audioMinutes}分钟音频,约¥${cost.totalPrice}`
|
|
|
+ : quota.reason
|
|
|
+ };
|
|
|
+}
|