subscription.service.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. import { prisma } from '../../models';
  2. import { MemberLevel } from '../../types';
  3. // ============================================
  4. // TTS服务商成本配置(2026-04-12 更新)
  5. // ============================================
  6. export const TTS_COST_CONFIG = {
  7. // TTS服务商配置
  8. providers: {
  9. // 免费方案(科大讯飞基础版)
  10. iflytek_free: {
  11. name: '科大讯飞-基础版',
  12. costPerThousand: 0, // 免费
  13. quality: 'standard',
  14. voiceOptions: 10,
  15. freeQuota: 100000, // 10万/月(永久)
  16. novelRating: 3 // ★★★☆☆
  17. },
  18. // 性价比方案(百度基础版)
  19. baidu_standard: {
  20. name: '百度智能云-基础版',
  21. costPerThousand: 3.0, // ¥3/千字符
  22. quality: 'standard',
  23. voiceOptions: 20,
  24. freeQuota: 2000000, // 200万/月(首年)
  25. novelRating: 4 // ★★★★☆
  26. },
  27. // 高质量方案(百度精品版)
  28. baidu_premium: {
  29. name: '百度智能云-精品版',
  30. costPerThousand: 15.0, // ¥15/千字符
  31. quality: 'premium',
  32. voiceOptions: 50,
  33. freeQuota: 0,
  34. novelRating: 5 // ★★★★★
  35. },
  36. // 阿里云方案
  37. aliyun_plus: {
  38. name: '阿里云Qwen3-Plus',
  39. costPerThousand: 12.0, // ¥12/千字符
  40. quality: 'high',
  41. voiceOptions: 30,
  42. freeQuota: 0,
  43. novelRating: 5 // ★★★★★
  44. },
  45. // 企业方案(火山引擎)
  46. volcano_premium: {
  47. name: '火山引擎豆包-标准版',
  48. costPerThousand: 50.0, // ¥50/千字符
  49. quality: 'premium',
  50. voiceOptions: 40,
  51. freeQuota: 20000, // 2万/应用
  52. novelRating: 5 // ★★★★★
  53. }
  54. },
  55. // AI模型成本配置
  56. aiModels: {
  57. deepseek: {
  58. name: 'DeepSeek V3.2',
  59. inputCost: 2.0, // ¥2.0/千token
  60. outputCost: 8.0, // ¥8.0/千token
  61. quality: 'standard',
  62. default: true
  63. },
  64. qwen_plus: {
  65. name: '通义Qwen Plus',
  66. inputCost: 4.0, // ¥4.0/千token
  67. outputCost: 12.0, // ¥12.0/千token
  68. quality: 'high'
  69. },
  70. doubao_pro: {
  71. name: '豆包 Pro',
  72. inputCost: 2.0, // ¥2.0/千token
  73. outputCost: 6.0, // ¥6.0/千token
  74. quality: 'high'
  75. }
  76. },
  77. // 各等级默认TTS方案
  78. planTTSProvider: {
  79. 0: 'iflytek_free', // 免费版:科大讯飞免费
  80. 1: 'baidu_standard', // 入门版:百度基础
  81. 2: 'baidu_premium', // 专业版:百度精品
  82. 3: 'volcano_premium' // 旗舰版:火山引擎
  83. },
  84. // 默认AI模型
  85. defaultAIModel: 'deepseek'
  86. };
  87. // ============================================
  88. // Token成本配置(AI生成成本 + TTS成本)
  89. // ============================================
  90. export const TOKEN_COST_CONFIG = {
  91. // 基础Token成本(DeepSeek V3.2 + 百度基础TTS)
  92. baseCostPerThousand: 5.0, // AI: ¥2 + TTS: ¥3 = ¥5/千字符
  93. // 超额使用单价
  94. extraPricePerThousand: 6.0, // 略高于成本价
  95. // 模型选择
  96. models: {
  97. deepseek: {
  98. name: 'DeepSeek V3.2',
  99. inputCost: 2.0, // ¥2.0/千token
  100. outputCost: 8.0, // ¥8.0/千token
  101. quality: 'standard',
  102. default: true
  103. },
  104. qwen_plus: {
  105. name: '通义 Qwen Plus',
  106. inputCost: 4.0, // ¥4.0/千token
  107. outputCost: 12.0, // ¥12.0/千token
  108. quality: 'high',
  109. extraFee: 1.5 // 每千token额外收费
  110. },
  111. doubao_pro: {
  112. name: '豆包 Pro',
  113. inputCost: 2.0, // ¥2.0/千token
  114. outputCost: 6.0, // ¥6.0/千token
  115. quality: 'high'
  116. }
  117. }
  118. };
  119. // 计算Token生成成本
  120. export function calculateTokenCost(textLength: number, model: string = 'deepseek'): {
  121. inputCost: number;
  122. outputCost: number;
  123. totalCost: number;
  124. suggestedPrice: number;
  125. } {
  126. const modelConfig = TOKEN_COST_CONFIG.models[model] || TOKEN_COST_CONFIG.models.deepseek;
  127. // 输入成本
  128. const inputTokens = textLength; // 1字 ≈ 1 token
  129. const inputCost = (inputTokens / 1000) * modelConfig.inputCost;
  130. // 输出成本(音频合成)
  131. const outputCost = textLength * 0.001 * modelConfig.outputCost * 0.1;
  132. // 总成本
  133. const totalCost = inputCost + outputCost;
  134. // 建议售价(成本 × 1.5,保证50%利润)
  135. const suggestedPrice = totalCost * 1.5;
  136. return {
  137. inputCost: Math.round(inputCost * 100) / 100,
  138. outputCost: Math.round(outputCost * 100) / 100,
  139. totalCost: Math.round(totalCost * 100) / 100,
  140. suggestedPrice: Math.round(suggestedPrice * 100) / 100
  141. };
  142. }
  143. // 默认套餐配置(包月批发+按量零售,2026-04-12定稿)
  144. export const DEFAULT_PLANS = [
  145. {
  146. name: '免费版',
  147. level: 0,
  148. priceMonthly: 0,
  149. priceYearly: 0,
  150. description: '适合轻度体验',
  151. features: JSON.stringify([
  152. '每天3次生成',
  153. '每月10分钟音频',
  154. '每次最多2分钟',
  155. '基础音色5种',
  156. '标准音质'
  157. ]),
  158. isRecommended: false,
  159. sortOrder: 0,
  160. dailyGenerations: 3,
  161. perGenerationLimit: 300, // ~2分钟音频
  162. monthlyTokens: 1500,
  163. monthlyMinutes: 10, // 音频时长配额
  164. yearlyTokens: null,
  165. voiceOptions: 5,
  166. audioQuality: 'standard',
  167. apiAccess: false,
  168. batchProcessing: false,
  169. teamManagement: false,
  170. overageEnabled: false, // 不支持超出配额
  171. overagePrice: 0,
  172. pricing: {
  173. monthlyPricePerMinute: 5.0, // ¥5/分钟(包月)
  174. overagePricePerMinute: null // 不支持按量
  175. }
  176. },
  177. {
  178. name: '入门版',
  179. level: 1,
  180. priceMonthly: 19,
  181. priceYearly: 190,
  182. description: '适合轻度使用',
  183. features: JSON.stringify([
  184. '每天10次生成',
  185. '每月30分钟音频',
  186. '每次最多5分钟',
  187. '全部音色',
  188. '高清音质',
  189. '超出¥8/分钟'
  190. ]),
  191. isRecommended: false,
  192. sortOrder: 1,
  193. dailyGenerations: 10,
  194. perGenerationLimit: 750, // ~5分钟音频
  195. monthlyTokens: 4500,
  196. monthlyMinutes: 30,
  197. yearlyTokens: null,
  198. voiceOptions: -1,
  199. audioQuality: 'high',
  200. apiAccess: false,
  201. batchProcessing: false,
  202. teamManagement: false,
  203. overageEnabled: true,
  204. overagePrice: 8.0, // 超出¥8/分钟(零售价)
  205. pricing: {
  206. monthlyPricePerMinute: 5.0, // ¥5/分钟(批发)
  207. overagePricePerMinute: 8.0 // ¥8/分钟(零售)
  208. }
  209. },
  210. {
  211. name: '专业版',
  212. level: 2,
  213. priceMonthly: 59,
  214. priceYearly: 590,
  215. description: '适合内容创作者 ⭐推荐',
  216. features: JSON.stringify([
  217. '无限制生成',
  218. '每月100分钟音频',
  219. '每次最多10分钟',
  220. '全部音色+定制音色',
  221. '高清音质',
  222. '超出¥7/分钟'
  223. ]),
  224. isRecommended: true,
  225. sortOrder: 2,
  226. dailyGenerations: -1,
  227. perGenerationLimit: 1500, // ~10分钟音频
  228. monthlyTokens: 15000,
  229. monthlyMinutes: 100,
  230. yearlyTokens: null,
  231. voiceOptions: -1,
  232. audioQuality: 'high',
  233. apiAccess: false,
  234. batchProcessing: false,
  235. teamManagement: false,
  236. overageEnabled: true,
  237. overagePrice: 7.0, // 超出¥7/分钟
  238. pricing: {
  239. monthlyPricePerMinute: 5.0,
  240. overagePricePerMinute: 7.0
  241. }
  242. },
  243. {
  244. name: '旗舰版',
  245. level: 3,
  246. priceMonthly: 199,
  247. priceYearly: 1990,
  248. description: '适合重度使用',
  249. features: JSON.stringify([
  250. '无限制生成',
  251. '每月400分钟音频',
  252. '每次最多20分钟',
  253. '全部音色+定制音色',
  254. '高清音质',
  255. 'VIP优先队列',
  256. '超出¥6/分钟'
  257. ]),
  258. isRecommended: false,
  259. sortOrder: 3,
  260. dailyGenerations: -1,
  261. perGenerationLimit: 3000, // ~20分钟音频
  262. monthlyTokens: 60000,
  263. monthlyMinutes: 400,
  264. yearlyTokens: null,
  265. voiceOptions: -1,
  266. audioQuality: 'high',
  267. apiAccess: true,
  268. batchProcessing: false,
  269. teamManagement: false,
  270. overageEnabled: true,
  271. overagePrice: 6.0, // 超出¥6/分钟
  272. pricing: {
  273. monthlyPricePerMinute: 5.0,
  274. overagePricePerMinute: 6.0
  275. }
  276. },
  277. {
  278. name: '企业版',
  279. level: 4,
  280. priceMonthly: 599,
  281. priceYearly: 5990,
  282. description: '适合企业用户',
  283. features: JSON.stringify([
  284. '无限制生成',
  285. '每月1500分钟音频',
  286. '每次最多60分钟',
  287. '全部功能',
  288. '高清音质',
  289. '专属技术支持',
  290. '批量处理',
  291. '团队管理',
  292. '超出¥5/分钟'
  293. ]),
  294. isRecommended: false,
  295. sortOrder: 4,
  296. dailyGenerations: -1,
  297. perGenerationLimit: 9000, // ~60分钟音频
  298. monthlyTokens: 225000,
  299. monthlyMinutes: 1500,
  300. yearlyTokens: null,
  301. voiceOptions: -1,
  302. audioQuality: 'high',
  303. apiAccess: true,
  304. batchProcessing: true,
  305. teamManagement: true,
  306. overageEnabled: true,
  307. overagePrice: 5.0, // 超出¥5/分钟(最低零售价)
  308. pricing: {
  309. monthlyPricePerMinute: 5.0,
  310. overagePricePerMinute: 5.0
  311. }
  312. }
  313. ];
  314. // 初始化套餐数据
  315. export async function initializePlans() {
  316. const existingPlans = await prisma.subscriptionPlan.count();
  317. if (existingPlans === 0) {
  318. console.log('初始化订阅套餐数据...');
  319. for (const plan of DEFAULT_PLANS) {
  320. await prisma.subscriptionPlan.create({ data: plan });
  321. }
  322. console.log('订阅套餐初始化完成');
  323. }
  324. }
  325. // 获取所有套餐列表
  326. export async function getPlans() {
  327. const plans = await prisma.subscriptionPlan.findMany({
  328. where: { isActive: true },
  329. orderBy: { sortOrder: 'asc' }
  330. });
  331. return plans.map(plan => ({
  332. ...plan,
  333. priceMonthly: Number(plan.priceMonthly),
  334. priceYearly: Number(plan.priceYearly),
  335. features: JSON.parse(plan.features || '[]')
  336. }));
  337. }
  338. // 获取单个套餐详情
  339. export async function getPlanById(planId: number) {
  340. const plan = await prisma.subscriptionPlan.findUnique({
  341. where: { id: planId }
  342. });
  343. if (!plan) {
  344. throw new Error('套餐不存在');
  345. }
  346. return {
  347. ...plan,
  348. priceMonthly: Number(plan.priceMonthly),
  349. priceYearly: Number(plan.priceYearly),
  350. features: JSON.parse(plan.features || '[]')
  351. };
  352. }
  353. // 获取用户当前订阅信息
  354. export async function getUserSubscription(userId: number) {
  355. const subscription = await prisma.subscription.findFirst({
  356. where: {
  357. userId,
  358. status: 'active',
  359. endDate: { gt: new Date() }
  360. },
  361. include: {
  362. plan: true
  363. },
  364. orderBy: { endDate: 'desc' }
  365. });
  366. return subscription;
  367. }
  368. // 获取用户Token余额
  369. export async function getUserTokenBalance(userId: number) {
  370. let balance = await prisma.tokenBalance.findUnique({
  371. where: { userId }
  372. });
  373. if (!balance) {
  374. // 初始化Token余额
  375. balance = await prisma.tokenBalance.create({
  376. data: {
  377. userId,
  378. totalTokens: 10000, // 默认免费额度
  379. usedTokens: 0
  380. }
  381. });
  382. }
  383. const remaining = balance.totalTokens - balance.usedTokens;
  384. return {
  385. totalTokens: balance.totalTokens,
  386. usedTokens: balance.usedTokens,
  387. remainingTokens: remaining,
  388. resetDate: balance.resetDate,
  389. isUnlimited: balance.totalTokens === -1
  390. };
  391. }
  392. // 获取Token使用记录
  393. export async function getTokenUsageList(userId: number, page: number = 1, pageSize: number = 20) {
  394. const where = { userId };
  395. const total = await prisma.tokenUsage.count({ where });
  396. const list = await prisma.tokenUsage.findMany({
  397. where,
  398. orderBy: { createdAt: 'desc' },
  399. skip: (page - 1) * pageSize,
  400. take: pageSize
  401. });
  402. return {
  403. list,
  404. total,
  405. page,
  406. pageSize,
  407. totalPages: Math.ceil(total / pageSize)
  408. };
  409. }
  410. // 消耗Token
  411. export async function consumeToken(
  412. userId: number,
  413. amount: number,
  414. type: string,
  415. contentLength: number,
  416. description?: string
  417. ) {
  418. const balance = await prisma.tokenBalance.findUnique({
  419. where: { userId }
  420. });
  421. if (!balance) {
  422. throw new Error('用户Token余额不存在');
  423. }
  424. if (balance.totalTokens !== -1 && balance.usedTokens + amount > balance.totalTokens) {
  425. throw new Error('Token余额不足');
  426. }
  427. // 更新余额
  428. await prisma.tokenBalance.update({
  429. where: { userId },
  430. data: {
  431. usedTokens: { increment: amount }
  432. }
  433. });
  434. // 记录使用
  435. const usage = await prisma.tokenUsage.create({
  436. data: {
  437. userId,
  438. type,
  439. amount,
  440. contentLength,
  441. description
  442. }
  443. });
  444. return usage;
  445. }
  446. // 获取用户配额(兼容旧接口)
  447. export async function getUserQuota(userId: number) {
  448. const user = await prisma.user.findUnique({
  449. where: { id: userId }
  450. });
  451. if (!user) {
  452. throw new Error('用户不存在');
  453. }
  454. const subscription = await getUserSubscription(userId);
  455. const tokenBalance = await getUserTokenBalance(userId);
  456. const memberLevel = user.memberLevel as MemberLevel;
  457. const levelNames = ['免费版', '基础版', '专业版', '旗舰版'];
  458. return {
  459. level: memberLevel,
  460. levelName: levelNames[memberLevel] || '免费版',
  461. isValid: subscription !== null,
  462. expireAt: subscription?.endDate,
  463. plan: subscription?.plan ? {
  464. name: subscription.plan.name,
  465. features: JSON.parse(subscription.plan.features || '[]')
  466. } : null,
  467. tokenBalance,
  468. limits: subscription?.plan ? {
  469. dailyGenerations: subscription.plan.dailyGenerations,
  470. perGenerationLimit: subscription.plan.perGenerationLimit,
  471. monthlyTokens: subscription.plan.monthlyTokens,
  472. voiceOptions: subscription.plan.voiceOptions,
  473. audioQuality: subscription.plan.audioQuality
  474. } : {
  475. dailyGenerations: 3,
  476. perGenerationLimit: 2000,
  477. monthlyTokens: 10000,
  478. voiceOptions: 5,
  479. audioQuality: 'standard'
  480. }
  481. };
  482. }
  483. // 检查用户配额
  484. export async function checkQuota(userId: number, requiredTokens: number) {
  485. const quota = await getUserQuota(userId);
  486. const balance = quota.tokenBalance;
  487. if (balance.isUnlimited) {
  488. return { allowed: true, reason: null };
  489. }
  490. if (balance.remainingTokens < requiredTokens) {
  491. return {
  492. allowed: false,
  493. reason: 'Token余额不足',
  494. required: requiredTokens,
  495. remaining: balance.remainingTokens
  496. };
  497. }
  498. return { allowed: true, reason: null };
  499. }
  500. // ============================================
  501. // 音频时长计费系统(2026-04-12 定稿)
  502. // ============================================
  503. export const AUDIO_BILLING_CONFIG = {
  504. // 成本配置(综合TTS+AI成本)
  505. costPerMinute: 4.0, // 成本:¥4/分钟(AI文本生成+TTS合成)
  506. // 定价策略:包月=批发价,按量=零售价
  507. pricing: {
  508. // 包月(批发价)
  509. monthly: {
  510. basePricePerMinute: 5.0, // ¥5/分钟(30%利润)
  511. },
  512. // 按量(零售价)
  513. payAsYouGo: {
  514. pricePerMinute: 8.0, // ¥8/分钟(100%利润)
  515. }
  516. },
  517. // 语速配置(字/分钟)
  518. speakingRate: 150,
  519. // 各等级每月音频时长配额(分钟)
  520. monthlyMinutes: {
  521. 0: 10, // 免费版:10分钟/月
  522. 1: 30, // 入门版:30分钟/月
  523. 2: 100, // 专业版:100分钟/月
  524. 3: 400, // 旗舰版:400分钟/月
  525. 4: 1500 // 企业版:1500分钟/月
  526. },
  527. // 各等级超出配额单价(元/分钟)
  528. overagePrice: {
  529. 0: null, // 免费版:不支持超出
  530. 1: 8.0, // 入门版:¥8/分钟
  531. 2: 7.0, // 专业版:¥7/分钟
  532. 3: 6.0, // 旗舰版:¥6/分钟
  533. 4: 5.0 // 企业版:¥5/分钟
  534. },
  535. // 各等级单次生成最大时长(分钟)
  536. maxMinutesPerGeneration: {
  537. 0: 2, // 免费版:2分钟/次
  538. 1: 5, // 入门版:5分钟/次
  539. 2: 10, // 专业版:10分钟/次
  540. 3: 20, // 旗舰版:20分钟/次
  541. 4: 60 // 企业版:60分钟/次
  542. }
  543. };
  544. // 根据文本长度计算音频时长
  545. export function calculateAudioDuration(textLength: number): number {
  546. return Math.ceil(textLength / AUDIO_BILLING_CONFIG.speakingRate);
  547. }
  548. // 计算音频生成费用(支持包月配额和按量计费)
  549. export function calculateAudioCost(
  550. textLength: number,
  551. memberLevel: number = 0,
  552. usedMinutes: number = 0
  553. ): {
  554. textLength: number;
  555. audioMinutes: number;
  556. inQuotaMinutes: number;
  557. overageMinutes: number;
  558. inQuotaPrice: number; // 配额内价格
  559. overagePrice: number; // 超额价格
  560. totalPrice: number;
  561. estimatedCost: number;
  562. } {
  563. const audioMinutes = calculateAudioDuration(textLength);
  564. const monthlyMinutes = AUDIO_BILLING_CONFIG.monthlyMinutes[memberLevel] || 10;
  565. const overagePricePerMinute = AUDIO_BILLING_CONFIG.overagePrice[memberLevel];
  566. // 计算配额内和超额分钟数
  567. const remainingQuota = Math.max(0, monthlyMinutes - usedMinutes);
  568. const inQuotaMinutes = Math.min(audioMinutes, remainingQuota);
  569. const overageMinutes = Math.max(0, audioMinutes - inQuotaMinutes);
  570. // 配额内价格(包月批发价)
  571. const inQuotaPrice = inQuotaMinutes * AUDIO_BILLING_CONFIG.pricing.monthly.basePricePerMinute;
  572. // 超额价格(按量零售价)
  573. const overagePrice = overageMinutes > 0 && overagePricePerMinute !== null
  574. ? overageMinutes * overagePricePerMinute
  575. : 0;
  576. const totalPrice = Math.round((inQuotaPrice + overagePrice) * 100) / 100;
  577. return {
  578. textLength,
  579. audioMinutes,
  580. inQuotaMinutes,
  581. overageMinutes,
  582. inQuotaPrice: Math.round(inQuotaPrice * 100) / 100,
  583. overagePrice: Math.round(overagePrice * 100) / 100,
  584. totalPrice,
  585. estimatedCost: audioMinutes * AUDIO_BILLING_CONFIG.costPerMinute
  586. };
  587. }
  588. // 获取用户音频时长余额
  589. export async function getUserAudioBalance(userId: number) {
  590. const user = await prisma.user.findUnique({
  591. where: { id: userId }
  592. });
  593. if (!user) {
  594. throw new Error('用户不存在');
  595. }
  596. const memberLevel = user.memberLevel as MemberLevel;
  597. const monthlyMinutes = AUDIO_BILLING_CONFIG.monthlyMinutes[memberLevel] || 10;
  598. const overageEnabled = AUDIO_BILLING_CONFIG.overagePrice[memberLevel] !== null;
  599. const overagePrice = AUDIO_BILLING_CONFIG.overagePrice[memberLevel];
  600. // 检查是否需要重置月度配额
  601. const now = new Date();
  602. const resetDate = user.subscriptionResetDate;
  603. let totalMinutes = monthlyMinutes;
  604. let usedMinutes = user.usedAudioMinutes || 0;
  605. // 每月1日重置配额
  606. if (resetDate) {
  607. const lastReset = new Date(resetDate);
  608. if (now.getMonth() !== lastReset.getMonth() || now.getFullYear() !== lastReset.getFullYear()) {
  609. totalMinutes = monthlyMinutes;
  610. usedMinutes = 0;
  611. await prisma.user.update({
  612. where: { id: userId },
  613. data: {
  614. usedAudioMinutes: 0,
  615. subscriptionResetDate: now
  616. }
  617. });
  618. }
  619. }
  620. return {
  621. memberLevel,
  622. totalMinutes,
  623. usedMinutes,
  624. remainingMinutes: totalMinutes - usedMinutes,
  625. overageEnabled,
  626. overagePrice: overagePrice || 0,
  627. resetDate: user.subscriptionResetDate
  628. };
  629. }
  630. // 检查音频生成配额
  631. export async function checkAudioQuota(
  632. userId: number,
  633. textLength: number
  634. ) {
  635. const audioMinutes = calculateAudioDuration(textLength);
  636. const balance = await getUserAudioBalance(userId);
  637. const maxMinutes = AUDIO_BILLING_CONFIG.maxMinutesPerGeneration[balance.memberLevel] || 2;
  638. // 检查单次限制
  639. if (audioMinutes > maxMinutes) {
  640. return {
  641. allowed: false,
  642. reason: `单次生成不能超过${maxMinutes}分钟`,
  643. audioMinutes,
  644. maxMinutes
  645. };
  646. }
  647. // 检查月度配额
  648. if (balance.remainingMinutes < audioMinutes && !balance.overageEnabled) {
  649. return {
  650. allowed: false,
  651. reason: `音频时长不足,本月配额已用完`,
  652. audioMinutes,
  653. remainingMinutes: balance.remainingMinutes,
  654. totalMinutes: balance.totalMinutes,
  655. overageEnabled: false
  656. };
  657. }
  658. return {
  659. allowed: true,
  660. reason: null,
  661. audioMinutes,
  662. inQuotaMinutes: Math.min(audioMinutes, balance.remainingMinutes),
  663. overageMinutes: Math.max(0, audioMinutes - balance.remainingMinutes),
  664. remainingMinutes: balance.remainingMinutes,
  665. totalMinutes: balance.totalMinutes,
  666. overageEnabled: balance.overageEnabled,
  667. overagePrice: balance.overagePrice
  668. };
  669. }
  670. // 扣除音频时长(生成完成后调用)
  671. export async function consumeAudioMinutes(
  672. userId: number,
  673. textLength: number,
  674. description?: string
  675. ) {
  676. const audioMinutes = calculateAudioDuration(textLength);
  677. const balance = await getUserAudioBalance(userId);
  678. const cost = calculateAudioCost(textLength, balance.memberLevel, balance.usedMinutes);
  679. // 更新用户已使用时长
  680. await prisma.user.update({
  681. where: { id: userId },
  682. data: {
  683. usedAudioMinutes: { increment: audioMinutes }
  684. }
  685. });
  686. // 记录使用日志
  687. const usage = await prisma.tokenUsage.create({
  688. data: {
  689. userId,
  690. type: 'audio_generation',
  691. amount: audioMinutes,
  692. contentLength: textLength,
  693. description: description || `${audioMinutes}分钟音频`
  694. }
  695. });
  696. return {
  697. usageId: usage.id,
  698. audioMinutes,
  699. textLength,
  700. inQuotaMinutes: cost.inQuotaMinutes,
  701. overageMinutes: cost.overageMinutes,
  702. inQuotaPrice: cost.inQuotaPrice,
  703. overagePrice: cost.overagePrice,
  704. totalPrice: cost.totalPrice
  705. };
  706. }
  707. // 获取音频生成预估(用于UI展示)
  708. export async function getAudioEstimate(
  709. userId: number,
  710. textLength: number
  711. ) {
  712. const balance = await getUserAudioBalance(userId);
  713. const cost = calculateAudioCost(textLength, balance.memberLevel, balance.usedMinutes);
  714. const quota = await checkAudioQuota(userId, textLength);
  715. return {
  716. // 预估信息
  717. textLength,
  718. audioMinutes: cost.audioMinutes,
  719. inQuotaMinutes: cost.inQuotaMinutes,
  720. overageMinutes: cost.overageMinutes,
  721. inQuotaPrice: cost.inQuotaPrice,
  722. overagePrice: cost.overagePrice,
  723. estimatedPrice: cost.totalPrice,
  724. estimatedCost: cost.estimatedCost,
  725. // 用户配额
  726. remainingMinutes: balance.remainingMinutes,
  727. totalMinutes: balance.totalMinutes,
  728. usedMinutes: balance.usedMinutes,
  729. // 计费策略
  730. overageEnabled: balance.overageEnabled,
  731. overagePrice: balance.overagePrice,
  732. // 是否允许生成
  733. canGenerate: quota.allowed,
  734. reason: quota.reason,
  735. // 显示文本
  736. displayText: quota.allowed
  737. ? cost.overageMinutes > 0
  738. ? `预计${cost.audioMinutes}分钟(配额${cost.inQuotaMinutes}分钟+超出${cost.overageMinutes}分钟),约¥${cost.totalPrice}`
  739. : `预计${cost.audioMinutes}分钟,约¥${cost.totalPrice}`
  740. : quota.reason
  741. };
  742. }