subscription.service.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  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. // 默认套餐配置(基于TTS成本,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. '每次最多2000字',
  154. '基础音色5种',
  155. '标准音质',
  156. '科大讯飞免费额度(10万/月永久)',
  157. 'Token成本:¥17.5/月(平台补贴)'
  158. ]),
  159. isRecommended: false,
  160. sortOrder: 0,
  161. dailyGenerations: 3,
  162. perGenerationLimit: 2000,
  163. monthlyTokens: 5000, // AI+基础TTS成本:¥17.5
  164. yearlyTokens: null,
  165. voiceOptions: 5,
  166. audioQuality: 'standard',
  167. apiAccess: false,
  168. batchProcessing: false,
  169. teamManagement: false,
  170. // 成本分析
  171. costAnalysis: {
  172. tokenCost: 17.5, // Token成本
  173. platformSubsidy: true, // 平台补贴
  174. profitMargin: 0,
  175. ttsProvider: '科大讯飞-基础版',
  176. aiModel: 'DeepSeek V3.2'
  177. }
  178. },
  179. {
  180. name: '入门版',
  181. level: 1,
  182. priceMonthly: 9.9,
  183. priceYearly: 99,
  184. description: '适合日常使用',
  185. features: JSON.stringify([
  186. '每月10000 Token',
  187. '每次最多5000字',
  188. '全部音色',
  189. '高清音质',
  190. '百度基础版TTS(¥3/千字符)',
  191. 'Token成本:¥35/月'
  192. ]),
  193. isRecommended: false,
  194. sortOrder: 1,
  195. dailyGenerations: 10,
  196. perGenerationLimit: 5000,
  197. monthlyTokens: 10000, // AI+基础TTS成本:¥35
  198. yearlyTokens: null,
  199. voiceOptions: -1,
  200. audioQuality: 'high',
  201. apiAccess: false,
  202. batchProcessing: false,
  203. teamManagement: false,
  204. // 成本分析(亏损,用于获客转化)
  205. costAnalysis: {
  206. tokenCost: 35.0,
  207. monthlyPrice: 9.9,
  208. loss: -25.1, // 每单亏损,获客用
  209. strategy: '获客转化',
  210. ttsProvider: '百度智能云-基础版'
  211. }
  212. },
  213. {
  214. name: '专业版',
  215. level: 2,
  216. priceMonthly: 49,
  217. priceYearly: 490,
  218. description: '适合内容创作者 ⭐推荐',
  219. features: JSON.stringify([
  220. '每月30000 Token',
  221. '每次最多20000字',
  222. '全部音色+定制音色',
  223. '高清音质',
  224. '百度精品版TTS(¥15/千字符)',
  225. 'VIP优先队列',
  226. 'Token成本:¥105/月'
  227. ]),
  228. isRecommended: true,
  229. sortOrder: 2,
  230. dailyGenerations: -1,
  231. perGenerationLimit: 20000,
  232. monthlyTokens: 30000, // AI+精品TTS成本:¥105
  233. yearlyTokens: null,
  234. voiceOptions: -1,
  235. audioQuality: 'lossless',
  236. apiAccess: false,
  237. batchProcessing: false,
  238. teamManagement: false,
  239. // 成本分析(部分补贴,核心盈利)
  240. costAnalysis: {
  241. tokenCost: 105.0,
  242. monthlyPrice: 49,
  243. loss: -56.0, // 部分补贴
  244. profitMargin: 0, // 保本微亏
  245. strategy: '核心转化产品',
  246. ttsProvider: '百度智能云-精品版'
  247. }
  248. },
  249. {
  250. name: '旗舰版',
  251. level: 3,
  252. priceMonthly: 199,
  253. priceYearly: 1999,
  254. description: '适合企业用户',
  255. features: JSON.stringify([
  256. '每月100000 Token',
  257. '每次最多100000字',
  258. '全部功能',
  259. '无损音质',
  260. '火山引擎豆包TTS(¥50/千字符)',
  261. '专属技术支持',
  262. '批量处理',
  263. '团队管理',
  264. 'Token成本:¥350/月'
  265. ]),
  266. isRecommended: false,
  267. sortOrder: 3,
  268. dailyGenerations: -1, // 无限制
  269. perGenerationLimit: 100000,
  270. monthlyTokens: 100000, // AI+高端TTS成本:¥350
  271. yearlyTokens: null,
  272. voiceOptions: -1,
  273. audioQuality: 'lossless',
  274. apiAccess: true,
  275. batchProcessing: true,
  276. teamManagement: true,
  277. // 成本分析(盈利)
  278. costAnalysis: {
  279. tokenCost: 350.0,
  280. monthlyPrice: 199,
  281. profit: -151.0, // 仍有亏损,建议涨价
  282. profitMargin: 0,
  283. suggestion: '建议定价¥399-499/月',
  284. ttsProvider: '火山引擎豆包'
  285. }
  286. }
  287. ];
  288. // 初始化套餐数据
  289. export async function initializePlans() {
  290. const existingPlans = await prisma.subscriptionPlan.count();
  291. if (existingPlans === 0) {
  292. console.log('初始化订阅套餐数据...');
  293. for (const plan of DEFAULT_PLANS) {
  294. await prisma.subscriptionPlan.create({ data: plan });
  295. }
  296. console.log('订阅套餐初始化完成');
  297. }
  298. }
  299. // 获取所有套餐列表
  300. export async function getPlans() {
  301. const plans = await prisma.subscriptionPlan.findMany({
  302. where: { isActive: true },
  303. orderBy: { sortOrder: 'asc' }
  304. });
  305. return plans.map(plan => ({
  306. ...plan,
  307. priceMonthly: Number(plan.priceMonthly),
  308. priceYearly: Number(plan.priceYearly),
  309. features: JSON.parse(plan.features || '[]')
  310. }));
  311. }
  312. // 获取单个套餐详情
  313. export async function getPlanById(planId: number) {
  314. const plan = await prisma.subscriptionPlan.findUnique({
  315. where: { id: planId }
  316. });
  317. if (!plan) {
  318. throw new Error('套餐不存在');
  319. }
  320. return {
  321. ...plan,
  322. priceMonthly: Number(plan.priceMonthly),
  323. priceYearly: Number(plan.priceYearly),
  324. features: JSON.parse(plan.features || '[]')
  325. };
  326. }
  327. // 获取用户当前订阅信息
  328. export async function getUserSubscription(userId: number) {
  329. const subscription = await prisma.subscription.findFirst({
  330. where: {
  331. userId,
  332. status: 'active',
  333. endDate: { gt: new Date() }
  334. },
  335. include: {
  336. plan: true
  337. },
  338. orderBy: { endDate: 'desc' }
  339. });
  340. return subscription;
  341. }
  342. // 获取用户Token余额
  343. export async function getUserTokenBalance(userId: number) {
  344. let balance = await prisma.tokenBalance.findUnique({
  345. where: { userId }
  346. });
  347. if (!balance) {
  348. // 初始化Token余额
  349. balance = await prisma.tokenBalance.create({
  350. data: {
  351. userId,
  352. totalTokens: 10000, // 默认免费额度
  353. usedTokens: 0
  354. }
  355. });
  356. }
  357. const remaining = balance.totalTokens - balance.usedTokens;
  358. return {
  359. totalTokens: balance.totalTokens,
  360. usedTokens: balance.usedTokens,
  361. remainingTokens: remaining,
  362. resetDate: balance.resetDate,
  363. isUnlimited: balance.totalTokens === -1
  364. };
  365. }
  366. // 获取Token使用记录
  367. export async function getTokenUsageList(userId: number, page: number = 1, pageSize: number = 20) {
  368. const where = { userId };
  369. const total = await prisma.tokenUsage.count({ where });
  370. const list = await prisma.tokenUsage.findMany({
  371. where,
  372. orderBy: { createdAt: 'desc' },
  373. skip: (page - 1) * pageSize,
  374. take: pageSize
  375. });
  376. return {
  377. list,
  378. total,
  379. page,
  380. pageSize,
  381. totalPages: Math.ceil(total / pageSize)
  382. };
  383. }
  384. // 消耗Token
  385. export async function consumeToken(
  386. userId: number,
  387. amount: number,
  388. type: string,
  389. contentLength: number,
  390. description?: string
  391. ) {
  392. const balance = await prisma.tokenBalance.findUnique({
  393. where: { userId }
  394. });
  395. if (!balance) {
  396. throw new Error('用户Token余额不存在');
  397. }
  398. if (balance.totalTokens !== -1 && balance.usedTokens + amount > balance.totalTokens) {
  399. throw new Error('Token余额不足');
  400. }
  401. // 更新余额
  402. await prisma.tokenBalance.update({
  403. where: { userId },
  404. data: {
  405. usedTokens: { increment: amount }
  406. }
  407. });
  408. // 记录使用
  409. const usage = await prisma.tokenUsage.create({
  410. data: {
  411. userId,
  412. type,
  413. amount,
  414. contentLength,
  415. description
  416. }
  417. });
  418. return usage;
  419. }
  420. // 获取用户配额(兼容旧接口)
  421. export async function getUserQuota(userId: number) {
  422. const user = await prisma.user.findUnique({
  423. where: { id: userId }
  424. });
  425. if (!user) {
  426. throw new Error('用户不存在');
  427. }
  428. const subscription = await getUserSubscription(userId);
  429. const tokenBalance = await getUserTokenBalance(userId);
  430. const memberLevel = user.memberLevel as MemberLevel;
  431. const levelNames = ['免费版', '基础版', '专业版', '旗舰版'];
  432. return {
  433. level: memberLevel,
  434. levelName: levelNames[memberLevel] || '免费版',
  435. isValid: subscription !== null,
  436. expireAt: subscription?.endDate,
  437. plan: subscription?.plan ? {
  438. name: subscription.plan.name,
  439. features: JSON.parse(subscription.plan.features || '[]')
  440. } : null,
  441. tokenBalance,
  442. limits: subscription?.plan ? {
  443. dailyGenerations: subscription.plan.dailyGenerations,
  444. perGenerationLimit: subscription.plan.perGenerationLimit,
  445. monthlyTokens: subscription.plan.monthlyTokens,
  446. voiceOptions: subscription.plan.voiceOptions,
  447. audioQuality: subscription.plan.audioQuality
  448. } : {
  449. dailyGenerations: 3,
  450. perGenerationLimit: 2000,
  451. monthlyTokens: 10000,
  452. voiceOptions: 5,
  453. audioQuality: 'standard'
  454. }
  455. };
  456. }
  457. // 检查用户配额
  458. export async function checkQuota(userId: number, requiredTokens: number) {
  459. const quota = await getUserQuota(userId);
  460. const balance = quota.tokenBalance;
  461. if (balance.isUnlimited) {
  462. return { allowed: true, reason: null };
  463. }
  464. if (balance.remainingTokens < requiredTokens) {
  465. return {
  466. allowed: false,
  467. reason: 'Token余额不足',
  468. required: requiredTokens,
  469. remaining: balance.remainingTokens
  470. };
  471. }
  472. return { allowed: true, reason: null };
  473. }