分析时间:2026-05-07 分析方法:代码级追踪所有状态下定义、赋值、流转、显示的全链路
| 编号 | 状态字段 | 代码定义 | 数据库 | 实际流转路径 | 状态数 |
|---|---|---|---|---|---|
| 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 |
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
[用户点击"生成音频"] [后端处理]
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 在 两个地方 被设置:
audioStatus.value = 'generating'(本地响应式)data: { audioStatus: 'generating' }(数据库)但批量生成(generateAllChaptersAudio)的 generating 状态只在后端设置(store.ts:775),前端的 getBookAudioStatus() 函数通过轮询 getAudioStatus API 来感知状态变化。
[用户点击"生成视频"] [后端处理]
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设置。
[开始生成]
Book.status = 'generating'
生成大纲 → chapters[i].status = 'completed'
逐章生成内容 →
chapters[i].contentStatus = 'generating' → 'completed' / 'failed'
全部完成 →
Book.status = 'completed', Book.progress = 100
失败(超过重试次数)→
Book.status = 'failed', Book.errorMsg = ...
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
null vs pending 语义冲突 ★★★contentStatus: null = 未开始, pending = 待生成(两者实际上等价)
audioStatus: null = 未开始(但 pending 也在注释中列出但从不使用)
videoStatus: null = 未开始(同上)
后果:
audioStatus === null 时显示"生成音频"按钮(chapter-detail.vue:105)contentStatus === null 和 contentStatus === 'pending' 含义不同却又混用IS NULL 和 = 'pending'唯一出现在代码中的 pending 赋值:
// test-dual-status.js:49 — 仅在测试脚本中使用!
// 后端 store.ts 中从未设置 contentStatus = 'pending'
→ 结论:pending 状态值定义但从未写入数据库,后端代码直接跳到 generating
// player.service.ts 中合并音频后
await prisma.bookChapter.update({
where: { id: chapterId },
data: { audioUrl: mergedUrl, audioDuration: totalDuration },
// ← 没有更新 audioStatus!
});
合并后的结果音频写入 audioUrl 字段,但 audioStatus 保持为合并前的状态(可能是 null 或 generating)。前端 getBookAudioStatus() 依赖 audioUrl 和 audioStatus 混合判断,导致状态显示不一致。
interrupted 死代码 ★★// types.ts:9
export type BookStatus = 'draft' | 'planning' | 'generating' | 'completed' | 'failed' | 'interrupted';
interrupted 在类型定义中存在,但后端代码中没有任何赋值,前端也没有任何显示逻辑。测试脚本 check-book-status.js 中有 if (book.status === 'interrupted') 的判断,但实际永远不会有值。
book-generator/index.vue:170-183 的 getBookAudioStatus() 函数:
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 未更新,会出现计数错误。
pending 状态在音频/视频中完全缺失 ★★音频/视频的 generating 状态只在即将开始生成时才设置。没有 排队中(queued) 状态。这意味着:
null,但前端没有任何"排队等待"的反馈planning 状态在 Book 中名存实亡 ★Book.status: draft, planning, generating, completed, failed, interrupted
^^^^^^^^
Book 的 status 类型中定义了 planning,但后端代码:
create() → status: 'draft'langgraph-controller.ts → 返回 status: 'generating'status = 'planning'发现:planning 从未被使用,注释里写了"规划中"但没有实际代码路径。
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,增加了前端显示逻辑的复杂度。
detail.vue:383-401 的 getNodeStatusClass() 函数:
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,也会显示"生成中"。这种隐式优先级容易导致显示不一致。
创建书籍 开始生成 全部完成
│ │ │
▼ ▼ ▼
┌──────┐ POST /generate ┌──────────┐ 逐章完成 ┌───────────┐
│draft │ ────────────────→ │generating│ ───────────→ │ completed │
└──────┘ └──────────┘ └───────────┘
│ ↑ │
│ └ 容错自动恢复 │
│ │
▼ ▼
failed failed
(超过重试次数) (生成出错)
未使用:planning → 无代码路径
死代码:interrupted → 定义了但永不赋值
内容生成:
null ─→ generating ─→ completed
│ │
│ ├ 手动编辑保存 → completed
│ │
└──→ failed
音频生成(依赖内容完成):
null ─→ generating ─→ completed
│ │
│ ├ 音频合并 → audioUrl 写入但 audioStatus 不更新
│ │
└──→ failed
视频生成(依赖内容完成):
null ─→ generating ─→ completed
│ │
│ ├ 视频合并 → videoUrl 写入但 videoStatus 不更新
│ │
└──→ failed
// 统一生成状态
export type GenStatus =
| 'idle' // 未开始(替代 null)
| 'queued' // 排队中(新增)
| 'running' // 运行中(替代 generating/processing/running)
| 'retrying' // 重试中(新增)
| 'completed' // 已完成(替代 completed/success)
| 'failed' // 失败
| 'cancelled'; // 已取消(替代 interrupted,新增)
| 优先级 | 问题 | 改动 | 涉及文件 |
|---|---|---|---|
| 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.success → completed 重命名 |
Prisma + 后端 | schema.prisma |
| 优先级 | 改动 | 说明 |
|---|---|---|
| P0 | 章节树状态图标增加 audioStatus/videoStatus 标注 |
当前只显示 contentStatus |
| P1 | 批量生成页的进度显示增加"排队中"状态 | 利用 getBookAudioStatus 接口 |
| P1 | 合并后重新加载书籍数据刷新状态 | 现有轮询逻辑增强 |
| 维度 | 现状 | 建议 |
|---|---|---|
| 状态枚举数量 | 10 个独立体系,5 种不同命名风格 | 统一为 1 个 GenStatus 枚举 |
| 未使用的状态值 | interrupted, planning, pending(对 audio/video) |
清理或使用 |
| 状态更新遗漏 | 音频/视频合并后不更新状态字段 | 补充 audioStatus='completed' |
| 前端状态推断 | 混合使用 audioUrl 存在性和 audioStatus 字段 |
全部改为直读 audioStatus |
| 排队反馈 | 无 queued 状态,点击后直接变 generating |
增加排队中状态 |