fix-local-audio-urls.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. /**
  2. * 一次性数据修复脚本:把所有以 /uploads/... 开头的本地 audioUrl
  3. * 重新上传到 OSS 并更新 DB audioUrl。
  4. *
  5. * 背景:tts.service.ts 老版本对 book chapter 模式只写本地路径,
  6. * player.service.ts mergeChapterAudios 清理逻辑误删了小节本地副本,
  7. * 导致线上任何本地路径 audioUrl 全部 404。
  8. * 修复后 tts.service 已改为始终走 OSS,但存量 DB 仍含坏数据。
  9. *
  10. * 用法:cd server && npx tsx scripts/fix-local-audio-urls.ts [--dry-run]
  11. * 必须先在 .env.production 或运行 env 加载 STORAGE_TYPE=oss 等 OSS 配置,
  12. * 否则 storageService 会走 uploadToLocal,结果毫无变化。
  13. */
  14. import { prisma } from '../src/models';
  15. import { storageService } from '../src/services/storage.service';
  16. import { AudioMerger } from '../src/modules/tts/audio-merger';
  17. import path from 'path';
  18. import fs from 'fs';
  19. const LOCAL_PREFIX = '/uploads/';
  20. async function findLocalFile(audioUrl: string, chapterId: number): Promise<string | null> {
  21. // 优先用 audioUrl 拼出的本地路径
  22. const fromUrl = path.join(process.cwd(), audioUrl.replace(/^\//, ''));
  23. if (fs.existsSync(fromUrl)) return fromUrl;
  24. // 兜底: 标准本地副本路径
  25. const fallback = path.join(process.cwd(), 'uploads', 'chapters', String(chapterId), 'output.mp3');
  26. if (fs.existsSync(fallback)) return fallback;
  27. // 合并产物路径
  28. const merged = path.join(process.cwd(), 'uploads', 'merged', 'chapter', String(chapterId), 'merged.mp3');
  29. if (fs.existsSync(merged)) return merged;
  30. const mergedOld = path.join(process.cwd(), 'uploads', 'merged', String(chapterId), 'merged.mp3');
  31. if (fs.existsSync(mergedOld)) return mergedOld;
  32. return null;
  33. }
  34. async function main() {
  35. const dryRun = process.argv.includes('--dry-run');
  36. console.log(`[FixLocalUrls] mode=${dryRun ? 'DRY-RUN' : 'WRITE'}`);
  37. console.log(`[FixLocalUrls] storageType=${storageService.getStorageType()}`);
  38. if (storageService.getStorageType() !== 'oss' && !dryRun) {
  39. console.error('[FixLocalUrls] STORAGE_TYPE 不是 oss,先确认 OSS 链路可用');
  40. console.error(' 跑: STORAGE_TYPE=oss OSS_ACCESS_KEY_ID=... npx tsx scripts/fix-local-audio-urls.ts');
  41. process.exit(1);
  42. }
  43. const chapters = await prisma.bookChapter.findMany({
  44. where: { audioUrl: { startsWith: LOCAL_PREFIX } },
  45. select: { id: true, audioUrl: true, audioDuration: true, bookId: true, parentId: true, level: true },
  46. orderBy: { id: 'asc' },
  47. });
  48. console.log(`[FixLocalUrls] 扫描到 ${chapters.length} 个本地 audioUrl 章节`);
  49. let ok = 0;
  50. let missing = 0;
  51. let failed = 0;
  52. for (const ch of chapters) {
  53. const localFile = await findLocalFile(ch.audioUrl!, ch.id);
  54. if (!localFile) {
  55. console.warn(`[FixLocalUrls] ⚠️ 章节 ${ch.id} 本地文件不存在: ${ch.audioUrl}`);
  56. missing++;
  57. continue;
  58. }
  59. try {
  60. const size = fs.statSync(localFile).size;
  61. const duration = await AudioMerger.getDuration(localFile).catch(() => ch.audioDuration || 0);
  62. if (dryRun) {
  63. console.log(`[FixLocalUrls] [DRY] 章节 ${ch.id} 本地=${localFile} (${size}B) -> OSS`);
  64. ok++;
  65. continue;
  66. }
  67. // 走 storageService.uploadAudio 用 URL-based key(按 audioId),保持与其他上传路径一致
  68. // 这里直接用 ossService.uploadFile 是因为 chapter 没有 audioId 概念
  69. const { ossService } = await import('../src/services/oss.service');
  70. const objectKey = `audio/repair/chapter/${ch.id}/output.mp3`;
  71. const newUrl = await ossService.uploadFile(localFile, objectKey);
  72. await prisma.bookChapter.update({
  73. where: { id: ch.id },
  74. data: { audioUrl: newUrl, audioDuration: Math.round(duration) },
  75. });
  76. console.log(`[FixLocalUrls] ✅ 章节 ${ch.id}: ${ch.audioUrl} -> ${newUrl}`);
  77. ok++;
  78. } catch (e: any) {
  79. console.error(`[FixLocalUrls] ❌ 章节 ${ch.id} 失败: ${e.message}`);
  80. failed++;
  81. }
  82. }
  83. console.log(`\n[FixLocalUrls] 完成: 成功=${ok} 缺文件=${missing} 失败=${failed} 共=${chapters.length}`);
  84. await prisma.$disconnect();
  85. }
  86. main().catch(e => { console.error(e); process.exit(1); });