status-system-analysis.md 15 KB

项目状态管理系统深度分析报告

分析时间:2026-05-07 分析方法:代码级追踪所有状态下定义、赋值、流转、显示的全链路


一、状态定义全景图

1.1 当前所有独立状态

编号 状态字段 代码定义 数据库 实际流转路径 状态数
S1 Book.status types.ts:9 @default("draft") draft → planning(?) → generating → completed / failed 6
S2 BookChapter.status prisma:204 @default("pending") pending → completed / failed 3
S3 BookChapter.contentStatus prisma:207 String? (null) null → pending(?) → generating → completed / failed 5
S4 BookChapter.audioStatus prisma:215 String? (null) null → generating → completed / failed 4
S5 BookChapter.videoStatus prisma:218 String? (null) null → generating → completed / failed 4
S6 GenerateTask.status types.ts:112 内存 pending → running → completed / failed 4
S7 VideoProject.status prisma:263 @default("draft") draft → processing → completed / failed 4
S8 Order.status prisma:51 @default("pending") pending → paid / failed / refunded 4
S9 PublishTask.status prisma:482 @default("pending") pending → uploading → success / failed 4
S10 AudioRecord.status prisma:441 @default("processing") processing → completed / failed 3

1.2 各自的状态取值

S1 Book:      draft ─→ generating ─→ completed
                │                    │
                └── planning(?) ─────┘
                                     └── failed / interrupted(dead)
                                     
S2 Chapter.status:   pending ─→ completed / failed

S3 contentStatus:  null ─→ generating ─→ completed / failed
                     │        ↑
                     └── pending(dead)

S4 audioStatus:    null ─→ generating ─→ completed / failed

S5 videoStatus:    null ─→ generating ─→ completed / failed

S6 GenerateTask:   pending ─→ running ─→ completed / failed

S7 VideoProject:   draft ─→ processing ─→ completed / failed

S8 Order:          pending ─→ paid / failed / refunded

S9 PublishTask:    pending ─→ uploading ─→ success / failed

S10 AudioRecord:   processing ─→ completed / failed

二、关键流转代码位置(后端)

2.1 音频状态流转

[用户点击"生成音频"]                  [后端处理]
  chapter-detail.vue                  book-generator.store.ts:780
  POST /api/book-generator/audio      generateAudio()
                                        │
  audioStatus = 'generating'  ←────  chapter.store.ts:775
  (chapter-detail.vue:828)         set audioStatus='generating'
                                        │
                                    [TTS API 异步回调]
                                        │
  audioStatus = 'completed'  ←────  chapter.store.ts:788
  + audioUrl + audioDuration       set audioStatus='completed'
                                        │
                                    失败时 → store.ts:802
                                    set audioStatus='failed'

发现:音频状态的 generating两个地方 被设置:

  1. 前端 chapter-detail.vue:828 — audioStatus.value = 'generating'(本地响应式)
  2. 后端 book-generator.store.ts:775 — data: { audioStatus: 'generating' }(数据库)

批量生成(generateAllChaptersAudio)的 generating 状态只在后端设置(store.ts:775),前端的 getBookAudioStatus() 函数通过轮询 getAudioStatus API 来感知状态变化。

2.2 视频状态流转

[用户点击"生成视频"]                  [后端处理]
  POST /api/book-generator/video      langgraph-controller.ts:1724
                                        │
  videoStatus = 'generating'  ←────  set videoStatus='generating'
  (chapter-detail.vue:865)             │
                                    [FFmpeg 异步处理]
                                        │
  videoStatus = 'completed'  ←────  langgraph-controller.ts:1746
  + videoUrl                       set videoStatus='completed'
                                        │
                                    失败时 → controller.ts:1752
                                    set videoStatus='failed'

发现:视频状态 也是两处设置:前端本地 + 后端数据库。前端handleGenerateVideo()设置,后端langgraph-controller.ts设置。

2.3 内容生成状态流转(LangGraph)

[开始生成]
  Book.status = 'generating'
  
  生成大纲 → chapters[i].status = 'completed'
  
  逐章生成内容 → 
    chapters[i].contentStatus = 'generating' → 'completed' / 'failed'
  
  全部完成 →
    Book.status = 'completed', Book.progress = 100
  
  失败(超过重试次数)→
    Book.status = 'failed', Book.errorMsg = ...

2.4 批量编排状态流转(BatchGenerationOrchestrator)

book-generator.service.ts

execute():
  1. 内容生成阶段
     → 逐章生成 content
     → contentStatus = 'generating' → 'completed' / 'failed'
  
  2. 音频生成阶段(所有章节)
     → 逐小节点生成 audio
     → audioStatus = 'generating' → 'completed' / 'failed'
  
  3. 音频合并阶段(可选)
     → mergeChapterAudios()
  
  4. 视频生成阶段(所有章节)
     → 逐小节点生成 video
     → videoStatus = 'generating' → 'completed' / 'failed'
  
  5. 视频合并阶段(可选)
     → mergeChapterVideos()
  
  6. 完成
     → Book.status = 'completed', progress = 100

三、深刻问题分析

问题 1:null vs pending 语义冲突 ★★★

contentStatus:   null = 未开始, pending = 待生成(两者实际上等价)
audioStatus:     null = 未开始(但 pending 也在注释中列出但从不使用)
videoStatus:     null = 未开始(同上)

后果

  • 前端判断 audioStatus === null 时显示"生成音频"按钮(chapter-detail.vue:105)
  • contentStatus === nullcontentStatus === 'pending' 含义不同却又混用
  • 数据库查询时需要同时处理 IS NULL= 'pending'

唯一出现在代码中的 pending 赋值

// test-dual-status.js:49 — 仅在测试脚本中使用!
// 后端 store.ts 中从未设置 contentStatus = 'pending'

结论pending 状态值定义但从未写入数据库,后端代码直接跳到 generating

问题 2:音频合并后状态不更新 ★★★

// player.service.ts 中合并音频后
await prisma.bookChapter.update({
  where: { id: chapterId },
  data: { audioUrl: mergedUrl, audioDuration: totalDuration },
  // ← 没有更新 audioStatus!
});

合并后的结果音频写入 audioUrl 字段,但 audioStatus 保持为合并前的状态(可能是 nullgenerating)。前端 getBookAudioStatus() 依赖 audioUrlaudioStatus 混合判断,导致状态显示不一致。

问题 3:interrupted 死代码 ★★

// types.ts:9
export type BookStatus = 'draft' | 'planning' | 'generating' | 'completed' | 'failed' | 'interrupted';

interrupted 在类型定义中存在,但后端代码中没有任何赋值,前端也没有任何显示逻辑。测试脚本 check-book-status.js 中有 if (book.status === 'interrupted') 的判断,但实际永远不会有值。

问题 4:前端音频状态推断混乱 ★★

book-generator/index.vue:170-183getBookAudioStatus() 函数:

function getBookAudioStatus(book: Book) {
  const leafNodes = getBookLeafNodes(book, leafLevel);
  const total = leafNodes.length;
  const completed = leafNodes.filter(n => {
    if (n.audioStatus === 'completed') return true;
    return !n.audioStatus && n.audioUrl; // 兼容旧数据
  }).length;

问题:它同时依赖 audioStatus 字段和 audioUrl 存在性来推断"已完成"。如果有音频合并后将 audioUrl 写入父章节但 audioStatus 未更新,会出现计数错误。

问题 5:pending 状态在音频/视频中完全缺失 ★★

音频/视频的 generating 状态只在即将开始生成时才设置。没有 排队中(queued) 状态。这意味着:

  • 用户点击"生成全部音频"后,系统需要先检查内容是否完成(store.ts:767)
  • 如果未完成,返回 null,但前端没有任何"排队等待"的反馈
  • 用户看到的只是按钮从"生成全部音频"立即变为"生成中..."
  • 但实际上可能还没开始(还在检查前置条件)

问题 6:planning 状态在 Book 中名存实亡 ★

Book.status: draft, planning, generating, completed, failed, interrupted
                            ^^^^^^^^

Book 的 status 类型中定义了 planning,但后端代码:

  • create()status: 'draft'
  • langgraph-controller.ts → 返回 status: 'generating'
  • 没有任何代码设置 status = 'planning'

发现planning 从未被使用,注释里写了"规划中"但没有实际代码路径。

问题 7:order.status 使用 success 不一致 ★

Order.status:    pending, paid, failed, refunded  ✓(统一使用其他枚举风格)
PublishTask:     pending, uploading, success, failed  ✗(success 而非 completed)
VideoProject:    draft, processing, completed, failed  ✓
AudioRecord:     processing, completed, failed  ✓

PublishTask.status 使用 success 而其他所有模块使用 completed,增加了前端显示逻辑的复杂度。

问题 8:章节树状态优先级逻辑模糊 ★

detail.vue:383-401getNodeStatusClass() 函数:

function getNodeStatusClass(node: any): string {
  if (node.contentStatus === 'completed') return 'status-completed';
  if (node.contentStatus === 'generating') return 'status-generating';
  if (node.contentStatus === 'failed') return 'status-failed';
  if (node.status === 'completed') return 'status-completed';
  if (node.status === 'pending') return 'status-pending';
  return 'status-pending';
}

问题contentStatus 优先级高于 status。一个章节如果大纲状态是 pending 但内容状态是 completed(比如手动编辑),会显示"已完成"。但如果大纲状态是 completed 而内容状态是 generating,也会显示"生成中"。这种隐式优先级容易导致显示不一致。


四、状态流转完整图表

4.1 书籍全生命周期状态图

创建书籍                 开始生成                   全部完成
  │                        │                          │
  ▼                        ▼                          ▼
┌──────┐   POST /generate  ┌──────────┐   逐章完成    ┌───────────┐
│draft │ ────────────────→ │generating│ ───────────→ │ completed │
└──────┘                   └──────────┘               └───────────┘
                              │    ↑                      │
                              │    └ 容错自动恢复          │
                              │                           │
                              ▼                           ▼
                          failed                      failed
                          (超过重试次数)               (生成出错)

  未使用:planning → 无代码路径
  死代码:interrupted → 定义了但永不赋值

4.2 章节内容/音频/视频状态流转图

内容生成:
null ─→ generating ─→ completed
                  │       │
                  │       ├ 手动编辑保存 → completed
                  │       │
                  └──→ failed

音频生成(依赖内容完成):
null ─→ generating ─→ completed
                  │       │
                  │       ├ 音频合并 → audioUrl 写入但 audioStatus 不更新
                  │       │
                  └──→ failed

视频生成(依赖内容完成):
null ─→ generating ─→ completed
                  │       │
                  │       ├ 视频合并 → videoUrl 写入但 videoStatus 不更新
                  │       │
                  └──→ failed

五、建议优化方案

方案 A:统一枚举(推荐,需 schema 变更)

// 统一生成状态
export type GenStatus = 
  | 'idle'       // 未开始(替代 null)
  | 'queued'     // 排队中(新增)
  | 'running'    // 运行中(替代 generating/processing/running)
  | 'retrying'   // 重试中(新增)
  | 'completed'  // 已完成(替代 completed/success)
  | 'failed'     // 失败
  | 'cancelled'; // 已取消(替代 interrupted,新增)

方案 B:最小改动(不改 schema,只修复代码逻辑)

优先级 问题 改动 涉及文件
P0 音频合并后缺 audioStatus='completed' 更新 新增一行 player.service.ts
P0 视频合并后缺 videoStatus='completed' 更新 新增一行 player.service.ts
P0 前端章节树 getNodeStatusClass 优先级重排 调整顺序 detail.vue
P1 interrupted 清理(类型+前端显示) 类型删除 types.ts
P1 批量生成前设置 queued(可选) 新增逻辑 store.ts
P2 Service 层 pending 改为 null 统一 注释+清理 types.ts
P2 PublishTask.successcompleted 重命名 Prisma + 后端 schema.prisma

方案 C:纯前端展示优化(无后端风险)

优先级 改动 说明
P0 章节树状态图标增加 audioStatus/videoStatus 标注 当前只显示 contentStatus
P1 批量生成页的进度显示增加"排队中"状态 利用 getBookAudioStatus 接口
P1 合并后重新加载书籍数据刷新状态 现有轮询逻辑增强

六、总结

维度 现状 建议
状态枚举数量 10 个独立体系,5 种不同命名风格 统一为 1 个 GenStatus 枚举
未使用的状态值 interrupted, planning, pending(对 audio/video) 清理或使用
状态更新遗漏 音频/视频合并后不更新状态字段 补充 audioStatus='completed'
前端状态推断 混合使用 audioUrl 存在性和 audioStatus 字段 全部改为直读 audioStatus
排队反馈 queued 状态,点击后直接变 generating 增加排队中状态