Bladeren bron

fix: 添加 /albums/:id/chapters 路由别名修复线上404

前端使用 /albums/ 路径,但后端仅定义了 /books/ 路由,
导致 https://book.rrbrr.com/api/book-generator/albums/18/chapters 返回404。
将 handler 提取为命名函数并注册到两条路径。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MyFramework User 3 maanden geleden
bovenliggende
commit
5e40f2aaf0
1 gewijzigde bestanden met toevoegingen van 17 en 41 verwijderingen
  1. 17 41
      server/src/modules/book-generator/album-controller.ts

+ 17 - 41
server/src/modules/book-generator/album-controller.ts

@@ -233,17 +233,15 @@ router.post('/books/:id', optionalAuth, async (ctx: Context) => {
 });
 });
 
 
 /**
 /**
- * GET /api/book-generator/books/:id/chapters
- * 获取专辑章节列表
+ * 获取专辑章节列表 handler
  * 返回章(level=1)的列表,每个章包含其下所有小节的合并音频
  * 返回章(level=1)的列表,每个章包含其下所有小节的合并音频
  * 过滤规则:公开的音频 + 当前用户自己的音频
  * 过滤规则:公开的音频 + 当前用户自己的音频
  */
  */
-router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
+async function getChapters(ctx: Context) {
   try {
   try {
     const bookId = ctx.params.id as string;
     const bookId = ctx.params.id as string;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
     const userId = ctx.state.user?.userId || TEST_USER_ID;
 
 
-    // 获取专辑信息
     const book = await prisma.book.findUnique({
     const book = await prisma.book.findUnique({
       where: { id: parseInt(bookId) },
       where: { id: parseInt(bookId) },
     });
     });
@@ -254,20 +252,16 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
       return;
       return;
     }
     }
 
 
-    // 判断是否为所有者(userId为null时也视为所有者,这样无需登录也能访问自己的音频)
     const isOwner = book.userId === null || book.userId === parseInt(userId);
     const isOwner = book.userId === null || book.userId === parseInt(userId);
 
 
-    // 获取所有章节
     const allChapters = await prisma.bookChapter.findMany({
     const allChapters = await prisma.bookChapter.findMany({
       where: { bookId: parseInt(bookId) },
       where: { bookId: parseInt(bookId) },
       orderBy: { number: 'asc' },
       orderBy: { number: 'asc' },
     });
     });
 
 
-    // 构建章(1) -> 节(2) -> 小节(3) 的树形结构
     const chapterMap = new Map<number, any>();
     const chapterMap = new Map<number, any>();
     const sectionMap = new Map<number, any>();
     const sectionMap = new Map<number, any>();
 
 
-    // 先遍历,构建映射
     allChapters.forEach(c => {
     allChapters.forEach(c => {
       if (c.level === 1) {
       if (c.level === 1) {
         chapterMap.set(c.id, {
         chapterMap.set(c.id, {
@@ -283,7 +277,7 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
           videoDuration: c.videoDuration || 0,
           videoDuration: c.videoDuration || 0,
           isPublic: c.isPublic,
           isPublic: c.isPublic,
           level: c.level,
           level: c.level,
-          subsections: [], // 节和小节列表
+          subsections: [],
         });
         });
       } else if (c.level === 2) {
       } else if (c.level === 2) {
         sectionMap.set(c.id, {
         sectionMap.set(c.id, {
@@ -295,10 +289,7 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
       }
       }
     });
     });
 
 
-    // 收集所有小节及其音频,按章分组
     const subsectionsByChapter = new Map<number, { audioUrl: string; audioDuration: number; title: string; isPublic: boolean }[]>();
     const subsectionsByChapter = new Map<number, { audioUrl: string; audioDuration: number; title: string; isPublic: boolean }[]>();
-
-    // 先把3级小节挂到2级节下
     const sectionSubsections = new Map<number, { audioUrl: string; audioDuration: number; title: string; isPublic: boolean }[]>();
     const sectionSubsections = new Map<number, { audioUrl: string; audioDuration: number; title: string; isPublic: boolean }[]>();
     allChapters.forEach(c => {
     allChapters.forEach(c => {
       if (c.level === 3 && c.parentId && c.audioUrl && c.audioUrl.trim() !== '') {
       if (c.level === 3 && c.parentId && c.audioUrl && c.audioUrl.trim() !== '') {
@@ -314,7 +305,6 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
       }
       }
     });
     });
 
 
-    // 把2级节挂到1级章下,并收集小节音频
     chapterMap.forEach((chapter, chapterId) => {
     chapterMap.forEach((chapter, chapterId) => {
       subsectionsByChapter.set(chapterId, []);
       subsectionsByChapter.set(chapterId, []);
     });
     });
@@ -324,10 +314,8 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
         const chapter = chapterMap.get(c.parentId);
         const chapter = chapterMap.get(c.parentId);
         const sectionData = sectionMap.get(c.id);
         const sectionData = sectionMap.get(c.id);
         if (chapter && sectionData) {
         if (chapter && sectionData) {
-          // 获取这个小节的所有小节音频
           const subs = sectionSubsections.get(c.id) || [];
           const subs = sectionSubsections.get(c.id) || [];
           chapter.subsections.push({ ...sectionData, subsections: subs });
           chapter.subsections.push({ ...sectionData, subsections: subs });
-          // 把小节音频按章汇总
           subs.forEach(s => {
           subs.forEach(s => {
             subsectionsByChapter.get(c.parentId)!.push(s);
             subsectionsByChapter.get(c.parentId)!.push(s);
           });
           });
@@ -335,41 +323,27 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
       }
       }
     });
     });
 
 
-    // 为每个章设置audioUrl
-    // 优先级:1. 章自身已有的合并音频(_merged) > 2. 章自身的audioUrl > 3. 仅汇总子节时长
-    // 注意:不再用 subs[0].audioUrl 覆盖章的 audioUrl,因为章的合并音频已在 mergeChapterAudios 中正确写入
     chapterMap.forEach((chapter, chapterId) => {
     chapterMap.forEach((chapter, chapterId) => {
       const subs = subsectionsByChapter.get(chapterId) || [];
       const subs = subsectionsByChapter.get(chapterId) || [];
       if (subs.length > 0 && (!chapter.audioUrl || chapter.audioUrl.trim() === '')) {
       if (subs.length > 0 && (!chapter.audioUrl || chapter.audioUrl.trim() === '')) {
-        // 章自身没有音频(短文章以外的情况),仅汇总子节时长,不覆盖audioUrl
-        // 前端播放章级别时会通过 player 接口自动触发合并
         chapter.audioDuration = subs.reduce((acc, s) => acc + s.audioDuration, 0);
         chapter.audioDuration = subs.reduce((acc, s) => acc + s.audioDuration, 0);
       } else if (subs.length > 0) {
       } else if (subs.length > 0) {
-        // 章已有音频(可能是合并后的 _merged 音频),保留它,只汇总时长
         chapter.audioDuration = subs.reduce((acc, s) => acc + s.audioDuration, 0);
         chapter.audioDuration = subs.reduce((acc, s) => acc + s.audioDuration, 0);
       }
       }
-      // 否则保持章自己的audioUrl(短文章直接生成在章上)
     });
     });
 
 
-    // 转换为数组并应用过滤规则
     let chapters = Array.from(chapterMap.values());
     let chapters = Array.from(chapterMap.values());
 
 
-    // 应用公开过滤(只影响非所有者)
     if (!isOwner) {
     if (!isOwner) {
       chapters = chapters.map(chapter => {
       chapters = chapters.map(chapter => {
-        // 检查是否有公开的小节音频
         const hasPublicSubsection = chapter.subsections?.some((s: any) =>
         const hasPublicSubsection = chapter.subsections?.some((s: any) =>
           s.subsections?.some((sub: any) => sub.isPublic === true && sub.audioUrl)
           s.subsections?.some((sub: any) => sub.isPublic === true && sub.audioUrl)
         );
         );
-        // 检查章节本身是否有公开音频
         const hasOwnPublicAudio = chapter.audioUrl &&
         const hasOwnPublicAudio = chapter.audioUrl &&
           chapter.audioUrl.trim() !== '' &&
           chapter.audioUrl.trim() !== '' &&
           chapter.isPublic === true;
           chapter.isPublic === true;
         if (!hasPublicSubsection && !hasOwnPublicAudio) {
         if (!hasPublicSubsection && !hasOwnPublicAudio) {
-          return {
-            ...chapter,
-            audioUrl: null,
-          };
+          return { ...chapter, audioUrl: null };
         }
         }
         return chapter;
         return chapter;
       });
       });
@@ -378,17 +352,18 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
     ctx.body = {
     ctx.body = {
       code: 0,
       code: 0,
       message: 'success',
       message: 'success',
-      data: {
-        chapters,
-        isOwner,
-      },
+      data: { chapters, isOwner },
     };
     };
   } catch (error) {
   } catch (error) {
     console.error('查询失败:', error);
     console.error('查询失败:', error);
     ctx.status = 500;
     ctx.status = 500;
     ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' };
     ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' };
   }
   }
-});
+}
+
+// GET /api/book-generator/books/:id/chapters 和 /albums/:id/chapters
+router.get('/books/:id/chapters', optionalAuth, getChapters);
+router.get('/albums/:id/chapters', optionalAuth, getChapters);
 
 
 /**
 /**
  * POST /api/book-generator/books/:id/chapters/:chapterId/merge-audio
  * POST /api/book-generator/books/:id/chapters/:chapterId/merge-audio
@@ -478,14 +453,11 @@ router.post('/books/:id/chapters/:chapterId/merge-audio', optionalAuth, async (c
 });
 });
 
 
 /**
 /**
- * GET /api/book-generator/books/chapters/:id
- * 获取单个章节详情
+ * 获取单个章节详情 handler
  */
  */
-router.get('/books/chapters/:id', optionalAuth, async (ctx: Context) => {
+async function getChapterDetail(ctx: Context) {
   try {
   try {
     const chapterId = parseInt(ctx.params.id as string);
     const chapterId = parseInt(ctx.params.id as string);
-
-    // 获取章节详情
     const chapter = await prisma.bookChapter.findUnique({
     const chapter = await prisma.bookChapter.findUnique({
       where: { id: chapterId },
       where: { id: chapterId },
     });
     });
@@ -521,6 +493,10 @@ router.get('/books/chapters/:id', optionalAuth, async (ctx: Context) => {
     ctx.status = 500;
     ctx.status = 500;
     ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' };
     ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' };
   }
   }
-});
+}
+
+// GET /api/book-generator/books/chapters/:id 和 /albums/chapters/:id
+router.get('/books/chapters/:id', optionalAuth, getChapterDetail);
+router.get('/albums/chapters/:id', optionalAuth, getChapterDetail);
 
 
 export default router;
 export default router;