|
@@ -154,7 +154,7 @@ export function calculateTokenCost(textLength: number, model: string = 'deepseek
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 默认套餐配置(基于音频时长,2026-04-12更新)
|
|
|
|
|
|
|
+// 默认套餐配置(包月批发+按量零售,2026-04-12定稿)
|
|
|
export const DEFAULT_PLANS = [
|
|
export const DEFAULT_PLANS = [
|
|
|
{
|
|
{
|
|
|
name: '免费版',
|
|
name: '免费版',
|
|
@@ -164,132 +164,164 @@ export const DEFAULT_PLANS = [
|
|
|
description: '适合轻度体验',
|
|
description: '适合轻度体验',
|
|
|
features: JSON.stringify([
|
|
features: JSON.stringify([
|
|
|
'每天3次生成',
|
|
'每天3次生成',
|
|
|
- '每月30分钟音频',
|
|
|
|
|
|
|
+ '每月10分钟音频',
|
|
|
'每次最多2分钟',
|
|
'每次最多2分钟',
|
|
|
'基础音色5种',
|
|
'基础音色5种',
|
|
|
- '标准音质',
|
|
|
|
|
- '科大讯飞TTS(免费额度)'
|
|
|
|
|
|
|
+ '标准音质'
|
|
|
]),
|
|
]),
|
|
|
isRecommended: false,
|
|
isRecommended: false,
|
|
|
sortOrder: 0,
|
|
sortOrder: 0,
|
|
|
dailyGenerations: 3,
|
|
dailyGenerations: 3,
|
|
|
perGenerationLimit: 300, // ~2分钟音频
|
|
perGenerationLimit: 300, // ~2分钟音频
|
|
|
- monthlyTokens: 3000, // 文本配额
|
|
|
|
|
- monthlyMinutes: 30, // 音频时长配额(新增)
|
|
|
|
|
|
|
+ monthlyTokens: 1500,
|
|
|
|
|
+ monthlyMinutes: 10, // 音频时长配额
|
|
|
yearlyTokens: null,
|
|
yearlyTokens: null,
|
|
|
voiceOptions: 5,
|
|
voiceOptions: 5,
|
|
|
audioQuality: 'standard',
|
|
audioQuality: 'standard',
|
|
|
apiAccess: false,
|
|
apiAccess: false,
|
|
|
batchProcessing: false,
|
|
batchProcessing: false,
|
|
|
teamManagement: false,
|
|
teamManagement: false,
|
|
|
- costAnalysis: {
|
|
|
|
|
- minutesCost: 10.2, // 30分钟 × ¥0.34
|
|
|
|
|
- platformSubsidy: true,
|
|
|
|
|
- strategy: '平台补贴引流'
|
|
|
|
|
|
|
+ overageEnabled: false, // 不支持超出配额
|
|
|
|
|
+ overagePrice: 0,
|
|
|
|
|
+ pricing: {
|
|
|
|
|
+ monthlyPricePerMinute: 5.0, // ¥5/分钟(包月)
|
|
|
|
|
+ overagePricePerMinute: null // 不支持按量
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
name: '入门版',
|
|
name: '入门版',
|
|
|
level: 1,
|
|
level: 1,
|
|
|
- priceMonthly: 9.9,
|
|
|
|
|
- priceYearly: 99,
|
|
|
|
|
- description: '适合日常使用',
|
|
|
|
|
|
|
+ priceMonthly: 19,
|
|
|
|
|
+ priceYearly: 190,
|
|
|
|
|
+ description: '适合轻度使用',
|
|
|
features: JSON.stringify([
|
|
features: JSON.stringify([
|
|
|
'每天10次生成',
|
|
'每天10次生成',
|
|
|
- '每月100分钟音频',
|
|
|
|
|
|
|
+ '每月30分钟音频',
|
|
|
'每次最多5分钟',
|
|
'每次最多5分钟',
|
|
|
'全部音色',
|
|
'全部音色',
|
|
|
'高清音质',
|
|
'高清音质',
|
|
|
- '百度基础版TTS'
|
|
|
|
|
|
|
+ '超出¥8/分钟'
|
|
|
]),
|
|
]),
|
|
|
isRecommended: false,
|
|
isRecommended: false,
|
|
|
sortOrder: 1,
|
|
sortOrder: 1,
|
|
|
dailyGenerations: 10,
|
|
dailyGenerations: 10,
|
|
|
perGenerationLimit: 750, // ~5分钟音频
|
|
perGenerationLimit: 750, // ~5分钟音频
|
|
|
- monthlyTokens: 15000,
|
|
|
|
|
- monthlyMinutes: 100, // 音频时长配额(新增)
|
|
|
|
|
|
|
+ monthlyTokens: 4500,
|
|
|
|
|
+ monthlyMinutes: 30,
|
|
|
yearlyTokens: null,
|
|
yearlyTokens: null,
|
|
|
voiceOptions: -1,
|
|
voiceOptions: -1,
|
|
|
audioQuality: 'high',
|
|
audioQuality: 'high',
|
|
|
apiAccess: false,
|
|
apiAccess: false,
|
|
|
batchProcessing: false,
|
|
batchProcessing: false,
|
|
|
teamManagement: false,
|
|
teamManagement: false,
|
|
|
- costAnalysis: {
|
|
|
|
|
- minutesCost: 100, // 100分钟 × ¥1.0
|
|
|
|
|
- monthlyPrice: 9.9,
|
|
|
|
|
- loss: -90.1,
|
|
|
|
|
- strategy: '亏损获客转化'
|
|
|
|
|
|
|
+ overageEnabled: true,
|
|
|
|
|
+ overagePrice: 8.0, // 超出¥8/分钟(零售价)
|
|
|
|
|
+ pricing: {
|
|
|
|
|
+ monthlyPricePerMinute: 5.0, // ¥5/分钟(批发)
|
|
|
|
|
+ overagePricePerMinute: 8.0 // ¥8/分钟(零售)
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
name: '专业版',
|
|
name: '专业版',
|
|
|
level: 2,
|
|
level: 2,
|
|
|
- priceMonthly: 49,
|
|
|
|
|
- priceYearly: 490,
|
|
|
|
|
|
|
+ priceMonthly: 59,
|
|
|
|
|
+ priceYearly: 590,
|
|
|
description: '适合内容创作者 ⭐推荐',
|
|
description: '适合内容创作者 ⭐推荐',
|
|
|
features: JSON.stringify([
|
|
features: JSON.stringify([
|
|
|
'无限制生成',
|
|
'无限制生成',
|
|
|
- '每月500分钟音频',
|
|
|
|
|
- '每次最多20分钟',
|
|
|
|
|
|
|
+ '每月100分钟音频',
|
|
|
|
|
+ '每次最多10分钟',
|
|
|
'全部音色+定制音色',
|
|
'全部音色+定制音色',
|
|
|
- '无损音质',
|
|
|
|
|
- '百度精品版TTS',
|
|
|
|
|
- 'VIP优先队列'
|
|
|
|
|
|
|
+ '高清音质',
|
|
|
|
|
+ '超出¥7/分钟'
|
|
|
]),
|
|
]),
|
|
|
isRecommended: true,
|
|
isRecommended: true,
|
|
|
sortOrder: 2,
|
|
sortOrder: 2,
|
|
|
dailyGenerations: -1,
|
|
dailyGenerations: -1,
|
|
|
- perGenerationLimit: 3000, // ~20分钟音频
|
|
|
|
|
- monthlyTokens: 75000,
|
|
|
|
|
- monthlyMinutes: 500, // 音频时长配额(新增)
|
|
|
|
|
|
|
+ perGenerationLimit: 1500, // ~10分钟音频
|
|
|
|
|
+ monthlyTokens: 15000,
|
|
|
|
|
+ monthlyMinutes: 100,
|
|
|
yearlyTokens: null,
|
|
yearlyTokens: null,
|
|
|
voiceOptions: -1,
|
|
voiceOptions: -1,
|
|
|
- audioQuality: 'lossless',
|
|
|
|
|
|
|
+ audioQuality: 'high',
|
|
|
apiAccess: false,
|
|
apiAccess: false,
|
|
|
batchProcessing: false,
|
|
batchProcessing: false,
|
|
|
teamManagement: false,
|
|
teamManagement: false,
|
|
|
- costAnalysis: {
|
|
|
|
|
- minutesCost: 500, // 500分钟 × ¥1.0
|
|
|
|
|
- monthlyPrice: 49,
|
|
|
|
|
- loss: -451,
|
|
|
|
|
- strategy: '亏损转化,盈利靠续费'
|
|
|
|
|
|
|
+ overageEnabled: true,
|
|
|
|
|
+ overagePrice: 7.0, // 超出¥7/分钟
|
|
|
|
|
+ pricing: {
|
|
|
|
|
+ monthlyPricePerMinute: 5.0,
|
|
|
|
|
+ overagePricePerMinute: 7.0
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
name: '旗舰版',
|
|
name: '旗舰版',
|
|
|
level: 3,
|
|
level: 3,
|
|
|
priceMonthly: 199,
|
|
priceMonthly: 199,
|
|
|
- priceYearly: 1999,
|
|
|
|
|
|
|
+ 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: '适合企业用户',
|
|
description: '适合企业用户',
|
|
|
features: JSON.stringify([
|
|
features: JSON.stringify([
|
|
|
'无限制生成',
|
|
'无限制生成',
|
|
|
- '每月2000分钟音频',
|
|
|
|
|
|
|
+ '每月1500分钟音频',
|
|
|
'每次最多60分钟',
|
|
'每次最多60分钟',
|
|
|
'全部功能',
|
|
'全部功能',
|
|
|
- '无损音质',
|
|
|
|
|
- '火山引擎豆包TTS',
|
|
|
|
|
|
|
+ '高清音质',
|
|
|
'专属技术支持',
|
|
'专属技术支持',
|
|
|
'批量处理',
|
|
'批量处理',
|
|
|
- '团队管理'
|
|
|
|
|
|
|
+ '团队管理',
|
|
|
|
|
+ '超出¥5/分钟'
|
|
|
]),
|
|
]),
|
|
|
isRecommended: false,
|
|
isRecommended: false,
|
|
|
- sortOrder: 3,
|
|
|
|
|
|
|
+ sortOrder: 4,
|
|
|
dailyGenerations: -1,
|
|
dailyGenerations: -1,
|
|
|
perGenerationLimit: 9000, // ~60分钟音频
|
|
perGenerationLimit: 9000, // ~60分钟音频
|
|
|
- monthlyTokens: 300000,
|
|
|
|
|
- monthlyMinutes: 2000, // 音频时长配额(新增)
|
|
|
|
|
|
|
+ monthlyTokens: 225000,
|
|
|
|
|
+ monthlyMinutes: 1500,
|
|
|
yearlyTokens: null,
|
|
yearlyTokens: null,
|
|
|
voiceOptions: -1,
|
|
voiceOptions: -1,
|
|
|
- audioQuality: 'lossless',
|
|
|
|
|
|
|
+ audioQuality: 'high',
|
|
|
apiAccess: true,
|
|
apiAccess: true,
|
|
|
batchProcessing: true,
|
|
batchProcessing: true,
|
|
|
teamManagement: true,
|
|
teamManagement: true,
|
|
|
- costAnalysis: {
|
|
|
|
|
- minutesCost: 680, // 2000分钟 × ¥0.34
|
|
|
|
|
- monthlyPrice: 199,
|
|
|
|
|
- loss: -481,
|
|
|
|
|
- suggestion: '建议定价¥699-999/月',
|
|
|
|
|
- strategy: '高端定位,低价策略'
|
|
|
|
|
|
|
+ overageEnabled: true,
|
|
|
|
|
+ overagePrice: 5.0, // 超出¥5/分钟(最低零售价)
|
|
|
|
|
+ pricing: {
|
|
|
|
|
+ monthlyPricePerMinute: 5.0,
|
|
|
|
|
+ overagePricePerMinute: 5.0
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
];
|
|
];
|
|
@@ -512,41 +544,53 @@ export async function checkQuota(userId: number, requiredTokens: number) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ============================================
|
|
// ============================================
|
|
|
-// 音频时长计费系统(2026-04-12 新增)
|
|
|
|
|
|
|
+// 音频时长计费系统(2026-04-12 定稿)
|
|
|
// ============================================
|
|
// ============================================
|
|
|
|
|
|
|
|
export const AUDIO_BILLING_CONFIG = {
|
|
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, // 平均语速150字/分钟
|
|
|
|
|
|
|
+ speakingRate: 150,
|
|
|
|
|
|
|
|
- // 音频时长单价(元/分钟)
|
|
|
|
|
- pricePerMinute: {
|
|
|
|
|
- standard: 0.5, // 标准音质:¥0.5/分钟
|
|
|
|
|
- high: 1.0, // 高清音质:¥1.0/分钟
|
|
|
|
|
- lossless: 2.0 // 无损音质:¥2.0/分钟
|
|
|
|
|
|
|
+ // 各等级每月音频时长配额(分钟)
|
|
|
|
|
+ monthlyMinutes: {
|
|
|
|
|
+ 0: 10, // 免费版:10分钟/月
|
|
|
|
|
+ 1: 30, // 入门版:30分钟/月
|
|
|
|
|
+ 2: 100, // 专业版:100分钟/月
|
|
|
|
|
+ 3: 400, // 旗舰版:400分钟/月
|
|
|
|
|
+ 4: 1500 // 企业版:1500分钟/月
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- // 各等级每月音频时长配额
|
|
|
|
|
- monthlyMinutes: {
|
|
|
|
|
- 0: 30, // 免费版:30分钟/月
|
|
|
|
|
- 1: 100, // 入门版:100分钟/月
|
|
|
|
|
- 2: 500, // 专业版:500分钟/月
|
|
|
|
|
- 3: 2000 // 旗舰版:2000分钟/月
|
|
|
|
|
|
|
+ // 各等级超出配额单价(元/分钟)
|
|
|
|
|
+ overagePrice: {
|
|
|
|
|
+ 0: null, // 免费版:不支持超出
|
|
|
|
|
+ 1: 8.0, // 入门版:¥8/分钟
|
|
|
|
|
+ 2: 7.0, // 专业版:¥7/分钟
|
|
|
|
|
+ 3: 6.0, // 旗舰版:¥6/分钟
|
|
|
|
|
+ 4: 5.0 // 企业版:¥5/分钟
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
// 各等级单次生成最大时长(分钟)
|
|
// 各等级单次生成最大时长(分钟)
|
|
|
maxMinutesPerGeneration: {
|
|
maxMinutesPerGeneration: {
|
|
|
0: 2, // 免费版:2分钟/次
|
|
0: 2, // 免费版:2分钟/次
|
|
|
1: 5, // 入门版:5分钟/次
|
|
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/分钟
|
|
|
|
|
|
|
+ 2: 10, // 专业版:10分钟/次
|
|
|
|
|
+ 3: 20, // 旗舰版:20分钟/次
|
|
|
|
|
+ 4: 60 // 企业版:60分钟/次
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -555,29 +599,49 @@ export function calculateAudioDuration(textLength: number): number {
|
|
|
return Math.ceil(textLength / AUDIO_BILLING_CONFIG.speakingRate);
|
|
return Math.ceil(textLength / AUDIO_BILLING_CONFIG.speakingRate);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 计算音频生成费用
|
|
|
|
|
|
|
+// 计算音频生成费用(支持包月配额和按量计费)
|
|
|
export function calculateAudioCost(
|
|
export function calculateAudioCost(
|
|
|
textLength: number,
|
|
textLength: number,
|
|
|
- quality: 'standard' | 'high' | 'lossless' = 'high'
|
|
|
|
|
|
|
+ memberLevel: number = 0,
|
|
|
|
|
+ usedMinutes: number = 0
|
|
|
): {
|
|
): {
|
|
|
textLength: number;
|
|
textLength: number;
|
|
|
audioMinutes: number;
|
|
audioMinutes: number;
|
|
|
- pricePerMinute: number;
|
|
|
|
|
|
|
+ inQuotaMinutes: number;
|
|
|
|
|
+ overageMinutes: number;
|
|
|
|
|
+ inQuotaPrice: number; // 配额内价格
|
|
|
|
|
+ overagePrice: number; // 超额价格
|
|
|
totalPrice: number;
|
|
totalPrice: number;
|
|
|
estimatedCost: number;
|
|
estimatedCost: number;
|
|
|
- remainingMinutes: number;
|
|
|
|
|
} {
|
|
} {
|
|
|
const audioMinutes = calculateAudioDuration(textLength);
|
|
const audioMinutes = calculateAudioDuration(textLength);
|
|
|
- const pricePerMinute = AUDIO_BILLING_CONFIG.pricePerMinute[quality] || 1.0;
|
|
|
|
|
- const totalPrice = Math.round(audioMinutes * pricePerMinute * 100) / 100;
|
|
|
|
|
|
|
+ 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 {
|
|
return {
|
|
|
textLength,
|
|
textLength,
|
|
|
audioMinutes,
|
|
audioMinutes,
|
|
|
- pricePerMinute,
|
|
|
|
|
|
|
+ inQuotaMinutes,
|
|
|
|
|
+ overageMinutes,
|
|
|
|
|
+ inQuotaPrice: Math.round(inQuotaPrice * 100) / 100,
|
|
|
|
|
+ overagePrice: Math.round(overagePrice * 100) / 100,
|
|
|
totalPrice,
|
|
totalPrice,
|
|
|
- estimatedCost: AUDIO_BILLING_CONFIG.costPerMinute[quality] * audioMinutes,
|
|
|
|
|
- remainingMinutes: 0 // 需要查询用户余额才能确定
|
|
|
|
|
|
|
+ estimatedCost: audioMinutes * AUDIO_BILLING_CONFIG.costPerMinute
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -592,7 +656,9 @@ export async function getUserAudioBalance(userId: number) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const memberLevel = user.memberLevel as MemberLevel;
|
|
const memberLevel = user.memberLevel as MemberLevel;
|
|
|
- const monthlyMinutes = AUDIO_BILLING_CONFIG.monthlyMinutes[memberLevel] || 30;
|
|
|
|
|
|
|
+ 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 now = new Date();
|
|
@@ -618,10 +684,12 @@ export async function getUserAudioBalance(userId: number) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
|
|
+ memberLevel,
|
|
|
totalMinutes,
|
|
totalMinutes,
|
|
|
usedMinutes,
|
|
usedMinutes,
|
|
|
remainingMinutes: totalMinutes - usedMinutes,
|
|
remainingMinutes: totalMinutes - usedMinutes,
|
|
|
- isUnlimited: memberLevel >= 3, // 旗舰版及以上无限制
|
|
|
|
|
|
|
+ overageEnabled,
|
|
|
|
|
+ overagePrice: overagePrice || 0,
|
|
|
resetDate: user.subscriptionResetDate
|
|
resetDate: user.subscriptionResetDate
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
@@ -629,12 +697,11 @@ export async function getUserAudioBalance(userId: number) {
|
|
|
// 检查音频生成配额
|
|
// 检查音频生成配额
|
|
|
export async function checkAudioQuota(
|
|
export async function checkAudioQuota(
|
|
|
userId: number,
|
|
userId: number,
|
|
|
- textLength: number,
|
|
|
|
|
- quality: 'standard' | 'high' | 'lossless' = 'high'
|
|
|
|
|
|
|
+ textLength: number
|
|
|
) {
|
|
) {
|
|
|
const audioMinutes = calculateAudioDuration(textLength);
|
|
const audioMinutes = calculateAudioDuration(textLength);
|
|
|
const balance = await getUserAudioBalance(userId);
|
|
const balance = await getUserAudioBalance(userId);
|
|
|
- const maxMinutes = AUDIO_BILLING_CONFIG.maxMinutesPerGeneration[balance.isUnlimited ? 3 : 0] || 2;
|
|
|
|
|
|
|
+ const maxMinutes = AUDIO_BILLING_CONFIG.maxMinutesPerGeneration[balance.memberLevel] || 2;
|
|
|
|
|
|
|
|
// 检查单次限制
|
|
// 检查单次限制
|
|
|
if (audioMinutes > maxMinutes) {
|
|
if (audioMinutes > maxMinutes) {
|
|
@@ -647,13 +714,14 @@ export async function checkAudioQuota(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 检查月度配额
|
|
// 检查月度配额
|
|
|
- if (!balance.isUnlimited && balance.remainingMinutes < audioMinutes) {
|
|
|
|
|
|
|
+ if (balance.remainingMinutes < audioMinutes && !balance.overageEnabled) {
|
|
|
return {
|
|
return {
|
|
|
allowed: false,
|
|
allowed: false,
|
|
|
- reason: `音频时长不足,需要${audioMinutes}分钟,剩余${balance.remainingMinutes}分钟`,
|
|
|
|
|
|
|
+ reason: `音频时长不足,本月配额已用完`,
|
|
|
audioMinutes,
|
|
audioMinutes,
|
|
|
remainingMinutes: balance.remainingMinutes,
|
|
remainingMinutes: balance.remainingMinutes,
|
|
|
- totalMinutes: balance.totalMinutes
|
|
|
|
|
|
|
+ totalMinutes: balance.totalMinutes,
|
|
|
|
|
+ overageEnabled: false
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -661,8 +729,12 @@ export async function checkAudioQuota(
|
|
|
allowed: true,
|
|
allowed: true,
|
|
|
reason: null,
|
|
reason: null,
|
|
|
audioMinutes,
|
|
audioMinutes,
|
|
|
- remainingMinutes: balance.remainingMinutes - audioMinutes,
|
|
|
|
|
- totalMinutes: balance.totalMinutes
|
|
|
|
|
|
|
+ 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
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -670,11 +742,11 @@ export async function checkAudioQuota(
|
|
|
export async function consumeAudioMinutes(
|
|
export async function consumeAudioMinutes(
|
|
|
userId: number,
|
|
userId: number,
|
|
|
textLength: number,
|
|
textLength: number,
|
|
|
- quality: 'standard' | 'high' | 'lossless' = 'high',
|
|
|
|
|
description?: string
|
|
description?: string
|
|
|
) {
|
|
) {
|
|
|
const audioMinutes = calculateAudioDuration(textLength);
|
|
const audioMinutes = calculateAudioDuration(textLength);
|
|
|
- const cost = calculateAudioCost(textLength, quality);
|
|
|
|
|
|
|
+ const balance = await getUserAudioBalance(userId);
|
|
|
|
|
+ const cost = calculateAudioCost(textLength, balance.memberLevel, balance.usedMinutes);
|
|
|
|
|
|
|
|
// 更新用户已使用时长
|
|
// 更新用户已使用时长
|
|
|
await prisma.user.update({
|
|
await prisma.user.update({
|
|
@@ -691,7 +763,7 @@ export async function consumeAudioMinutes(
|
|
|
type: 'audio_generation',
|
|
type: 'audio_generation',
|
|
|
amount: audioMinutes,
|
|
amount: audioMinutes,
|
|
|
contentLength: textLength,
|
|
contentLength: textLength,
|
|
|
- description: description || `${audioMinutes}分钟${quality}音质音频`
|
|
|
|
|
|
|
+ description: description || `${audioMinutes}分钟音频`
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -699,34 +771,42 @@ export async function consumeAudioMinutes(
|
|
|
usageId: usage.id,
|
|
usageId: usage.id,
|
|
|
audioMinutes,
|
|
audioMinutes,
|
|
|
textLength,
|
|
textLength,
|
|
|
- quality,
|
|
|
|
|
- cost: cost.totalPrice
|
|
|
|
|
|
|
+ inQuotaMinutes: cost.inQuotaMinutes,
|
|
|
|
|
+ overageMinutes: cost.overageMinutes,
|
|
|
|
|
+ inQuotaPrice: cost.inQuotaPrice,
|
|
|
|
|
+ overagePrice: cost.overagePrice,
|
|
|
|
|
+ totalPrice: cost.totalPrice
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 获取音频生成预估(用于UI展示)
|
|
// 获取音频生成预估(用于UI展示)
|
|
|
export async function getAudioEstimate(
|
|
export async function getAudioEstimate(
|
|
|
userId: number,
|
|
userId: number,
|
|
|
- textLength: number,
|
|
|
|
|
- quality: 'standard' | 'high' | 'lossless' = 'high'
|
|
|
|
|
|
|
+ textLength: number
|
|
|
) {
|
|
) {
|
|
|
- const cost = calculateAudioCost(textLength, quality);
|
|
|
|
|
- const quota = await checkAudioQuota(userId, textLength, quality);
|
|
|
|
|
const balance = await getUserAudioBalance(userId);
|
|
const balance = await getUserAudioBalance(userId);
|
|
|
|
|
+ const cost = calculateAudioCost(textLength, balance.memberLevel, balance.usedMinutes);
|
|
|
|
|
+ const quota = await checkAudioQuota(userId, textLength);
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
// 预估信息
|
|
// 预估信息
|
|
|
textLength,
|
|
textLength,
|
|
|
audioMinutes: cost.audioMinutes,
|
|
audioMinutes: cost.audioMinutes,
|
|
|
- pricePerMinute: cost.pricePerMinute,
|
|
|
|
|
|
|
+ inQuotaMinutes: cost.inQuotaMinutes,
|
|
|
|
|
+ overageMinutes: cost.overageMinutes,
|
|
|
|
|
+ inQuotaPrice: cost.inQuotaPrice,
|
|
|
|
|
+ overagePrice: cost.overagePrice,
|
|
|
estimatedPrice: cost.totalPrice,
|
|
estimatedPrice: cost.totalPrice,
|
|
|
estimatedCost: cost.estimatedCost,
|
|
estimatedCost: cost.estimatedCost,
|
|
|
|
|
|
|
|
// 用户配额
|
|
// 用户配额
|
|
|
- remainingMinutes: quota.allowed
|
|
|
|
|
- ? quota.remainingMinutes
|
|
|
|
|
- : balance.remainingMinutes,
|
|
|
|
|
|
|
+ remainingMinutes: balance.remainingMinutes,
|
|
|
totalMinutes: balance.totalMinutes,
|
|
totalMinutes: balance.totalMinutes,
|
|
|
|
|
+ usedMinutes: balance.usedMinutes,
|
|
|
|
|
+
|
|
|
|
|
+ // 计费策略
|
|
|
|
|
+ overageEnabled: balance.overageEnabled,
|
|
|
|
|
+ overagePrice: balance.overagePrice,
|
|
|
|
|
|
|
|
// 是否允许生成
|
|
// 是否允许生成
|
|
|
canGenerate: quota.allowed,
|
|
canGenerate: quota.allowed,
|
|
@@ -734,7 +814,9 @@ export async function getAudioEstimate(
|
|
|
|
|
|
|
|
// 显示文本
|
|
// 显示文本
|
|
|
displayText: quota.allowed
|
|
displayText: quota.allowed
|
|
|
- ? `预计${cost.audioMinutes}分钟音频,约¥${cost.totalPrice}`
|
|
|
|
|
|
|
+ ? cost.overageMinutes > 0
|
|
|
|
|
+ ? `预计${cost.audioMinutes}分钟(配额${cost.inQuotaMinutes}分钟+超出${cost.overageMinutes}分钟),约¥${cost.totalPrice}`
|
|
|
|
|
+ : `预计${cost.audioMinutes}分钟,约¥${cost.totalPrice}`
|
|
|
: quota.reason
|
|
: quota.reason
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|