# 状态系统重构方案 — 线性阶段模型

## 一、核心变更

### 删除字段

| 实体 | 删除字段 | 原因 |
|------|---------|------|
| BookChapter | `status`（大纲） | 合并到 `genStage` |
| BookChapter | `contentStatus` | 合并到 `genStage` |
| BookChapter | `audioStatus` | 合并到 `genStage` |
| BookChapter | `videoStatus` | 合并到 `genStage` |

### 新增字段

| 实体 | 新增字段 | 类型 | 默认值 | 说明 |
|------|---------|------|--------|------|
| BookChapter | `genStage` | String | `"idle"` | 唯一阶段字段 |
| Book | `genStage` | String | `"draft"` | 唯一阶段字段（替代原 `status`） |
| Book | `failedStage` | String? | `null` | 记录失败时卡在哪个阶段（调试用） |

### 保留字段

- Book.**`progress`**（0-100）— 推进中不断更新
- BookChapter.**`content`** / **`wordCount`** — 实际数据
- BookChapter.**`audioUrl`** / **`audioDuration`** — 实际数据
- BookChapter.**`videoUrl`** / **`videoDuration`** — 实际数据
- BookChapter.**`isPublic`** — 独立于生成进程

---

## 二、Book.genStage 完整定义

```typescript
export type BookGenStage =
  | 'draft'               // 草稿，初始状态
  | 'outlining'           // 大纲生成中（LangGraph）
  | 'outline_completed'   // 大纲完成
  | 'content_generating'  // 逐章生成内容中
  | 'content_completed'   // 所有章节内容完成
  | 'audio_generating'    // 逐章生成音频中
  | 'audio_completed'     // 所有章节音频完成
  | 'video_generating'    // 逐章生成视频中
  | 'video_completed'     // 所有章节视频完成（终态）
  | 'failed';             // 失败（终态）
```

### Book 阶段索引（用于比较大小）

```typescript
const BOOK_STAGE_ORDER: BookGenStage[] = [
  'draft',               // 0
  'outlining',           // 1
  'outline_completed',   // 2
  'content_generating',  // 3
  'content_completed',   // 4
  'audio_generating',    // 5
  'audio_completed',     // 6
  'video_generating',    // 7
  'video_completed',     // 8
  'failed',              // 9（终态）
];

function bookStageIndex(s: BookGenStage): number {
  return BOOK_STAGE_ORDER.indexOf(s);
}
```

### Book 转移规则

| 规则 | 说明 |
|------|------|
| **前进** | 只能在阶段索引上正向推进，不允许跳跃 |
| **回退** | Book 不直接回退。当子章节回退导致"所有子章节 >= 某阶段"条件不再满足时自动回退 |
| **失败** | 任何阶段可进入 `failed` |

**前进允许的转移：**
```
draft → outlining
outlining → outline_completed
outline_completed → content_generating
content_generating → content_completed
content_completed → audio_generating
audio_generating → audio_completed
audio_completed → video_generating
video_generating → video_completed
任意 → failed
```

**回退通过 `checkAndRegressBook()` 触发（见 5.2 节）：**

当任意子章节因重新生成而回退到上游阶段后，Book 检查当前 `genStage` 所代表的"所有章节 >= minStage"条件是否仍满足。不满足时自动回退到实际满足的阶段。

---

## 三、Chapter.genStage 完整定义

```typescript
export type ChapterGenStage =
  | 'idle'                  // 初始状态
  | 'content_generating'    // 内容生成中
  | 'content_completed'     // 内容完成
  | 'audio_generating'      // 音频生成中
  | 'audio_completed'       // 音频完成
  | 'video_generating'      // 视频生成中
  | 'video_completed'       // 视频完成（终态）
  | 'failed';               // 失败（终态）
```

### Chapter 阶段索引

```typescript
const CHAPTER_STAGE_ORDER: ChapterGenStage[] = [
  'idle',                // 0
  'content_generating',  // 1
  'content_completed',   // 2
  'audio_generating',    // 3
  'audio_completed',     // 4
  'video_generating',    // 5
  'video_completed',     // 6
  'failed',              // 7（终态）
];

function chapterStageIndex(s: ChapterGenStage): number {
  return CHAPTER_STAGE_ORDER.indexOf(s);
}
```

### Chapter 转移规则

| 规则 | 说明 |
|------|------|
| **前进** | 只能按阶段索引正向推进，不可跳跃 |
| **回退** | **允许**，但必须满足：只回退到当前阶段和之前的阶段，不能跳到更后的阶段；回退会自动清除下游资源 |
| **重新生成** | 在 `content_completed` / `audio_completed` / `video_completed` 上点击"重新生成"视为"前进到对应的 generating 阶段 + 清除下游资源" |
| **失败** | 任何阶段可进入 `failed` |
| **失败重试** | 从 `failed` 回到对应的回调阶段前 |

### 完整的转移矩阵

```typescript
const CHAPTER_VALID_TRANSITIONS: Record<ChapterGenStage, ChapterGenStage[]> = {
  // idle → 内容生成
  'idle':                ['content_generating', 'failed'],

  // 内容生成中 → 内容完成 或 失败
  'content_generating':  [
    'content_completed',       // 前进
    'failed',
  ],

  // 内容完成 → 音频生成（前进）或 重新生成内容（回退清除 audio/video）
  'content_completed':   [
    'audio_generating',        // 前进到音频
    'content_generating',      // ← 重新生成内容（清除 audioUrl、videoUrl）
    'failed',
  ],

  // 音频生成中 → 音频完成 或 失败
  'audio_generating':    [
    'audio_completed',         // 前进
    'failed',
  ],

  // 音频完成 → 视频生成（前进）或 重新生成音频（清除 video）或 重新生成内容
  'audio_completed':     [
    'video_generating',        // 前进到视频
    'audio_generating',        // ← 重新生成音频（清除 videoUrl）
    'content_generating',      // ← 重新生成内容（清除 audioUrl、videoUrl）
    'failed',
  ],

  // 视频生成中 → 视频完成 或 失败
  'video_generating':    [
    'video_completed',         // 前进
    'failed',
  ],

  // 视频完成 → 重新生成 或 失败
  'video_completed':     [
    'video_generating',        // ← 重新生成视频
    'audio_generating',        // ← 从音频重新开始
    'content_generating',      // ← 从内容重新开始
    'failed',
  ],

  // 失败 → 重试到对应阶段前
  'failed':              [
    'content_generating',      // 内容生成失败，重试
    'audio_generating',        // 音频生成失败，重试
    'video_generating',        // 视频生成失败，重试
    'idle',                    // 完全重置
  ],
};
```

### 回退时的资源清理规则

```typescript
/**
 * 判断本次转移是否需要清理下游资源。
 * 如果目标阶段比某资源的阶段索引更小，则清除该资源。
 */
function getCleanupForRegression(
  targetStage: ChapterGenStage,
): { audioUrl?: null; audioDuration?: 0; videoUrl?: null; videoDuration?: 0 } {
  const targetIdx = chapterStageIndex(targetStage);
  const audioStageIdx = chapterStageIndex('audio_completed');    // 4
  const videoStageIdx = chapterStageIndex('video_completed');    // 6

  const clean: any = {};
  if (targetIdx < audioStageIdx) {
    clean.audioUrl = null;
    clean.audioDuration = 0;
  }
  if (targetIdx < videoStageIdx) {
    clean.videoUrl = null;
    clean.videoDuration = 0;
  }
  return clean;
}
```

---

## 四、安全转移函数

### 4.1 通用验证 + 乐观锁

```typescript
async function safeTransitionChapter(
  chapterId: number,
  currentStage: ChapterGenStage,
  targetStage: ChapterGenStage,
): Promise<void> {
  // ① 验证是否在转移矩阵中
  const allowed = CHAPTER_VALID_TRANSITIONS[currentStage];
  if (!allowed || !allowed.includes(targetStage)) {
    throw new Error(
      `非法状态转移：Chapter ${chapterId} 当前 ${currentStage}，` +
      `不允许转到 ${targetStage}。允许的转移：[${allowed?.join(', ') || '无'}]`
    );
  }

  // ② 判断是否需要清理下游资源
  const cleanData = getCleanupForRegression(targetStage);

  // ③ 乐观锁写入
  const result = await prisma.bookChapter.updateMany({
    where: { id: chapterId, genStage: currentStage },
    data: { genStage: targetStage, ...cleanData, updatedAt: new Date() },
  });
  if (result.count === 0) {
    throw new Error(
      `并发冲突：Chapter ${chapterId} 预期当前状态 ${currentStage}，` +
      `但数据库中已不是该状态`
    );
  }
}

async function safeTransitionBook(
  bookId: number,
  currentStage: BookGenStage,
  targetStage: BookGenStage,
): Promise<void> {
  const allowed = BOOK_VALID_TRANSITIONS[currentStage];
  if (!allowed || !allowed.includes(targetStage)) {
    throw new Error(
      `非法状态转移：Book ${bookId} 当前 ${currentStage}，` +
      `不允许转到 ${targetStage}。允许的转移：[${allowed?.join(', ') || '无'}]`
    );
  }
  const result = await prisma.book.updateMany({
    where: { id: bookId, genStage: currentStage },
    data: {
      genStage: targetStage,
      failedStage: targetStage === 'failed' ? currentStage : undefined,
      updatedAt: new Date(),
    },
  });
  if (result.count === 0) {
    throw new Error(
      `并发冲突：Book ${bookId} 预期当前状态 ${currentStage}，` +
      `但数据库中已不是该状态`
    );
  }
}
```

### 4.2 上层封装：`advanceChapter()` 和 `regenerateChapter()`

为了让调用方不用关心前进/回退的矩阵细节，提供两个命名语义明确的上层函数：

```typescript
/**
 * 前进：推进到下一个阶段。只能前进不能回退。
 * 例：content_completed → audio_generating
 */
async function advanceChapter(
  chapterId: number,
  targetStage: ChapterGenStage,
): Promise<void> {
  const chapter = await prisma.bookChapter.findUniqueOrThrow({
    where: { id: chapterId },
  });
  const cur = chapter.genStage as ChapterGenStage;
  const curIdx = chapterStageIndex(cur);
  const tgtIdx = chapterStageIndex(targetStage);
  if (tgtIdx <= curIdx) {
    throw new Error(
      `advanceChapter 不能用于回退：当前 ${cur}，目标 ${targetStage}。` +
      `请使用 regenerateChapter()`
    );
  }
  await safeTransitionChapter(chapterId, cur, targetStage);
}

/**
 * 重新生成：回退到上游阶段（或同级 generating 状态）以重新生成。
 * 自动清除下游资源。
 * 例：audio_completed → audio_generating（重新生成音频）
 *     audio_completed → content_generating（从内容重新开始）
 *     failed → audio_generating（失败重试）
 */
async function regenerateChapter(
  chapterId: number,
  targetStage: ChapterGenStage,
): Promise<void> {
  const chapter = await prisma.bookChapter.findUniqueOrThrow({
    where: { id: chapterId },
  });
  const cur = chapter.genStage as ChapterGenStage;
  await safeTransitionChapter(chapterId, cur, targetStage);
  // 回退后检查 Book 是否需要联动回退
  await checkAndRegressBook(chapter.bookId);
}
```

---

## 五、Book 推进与回退逻辑

### 5.1 Book 推进条件

Book 的 `genStage` 不代表"正在做这个操作"，而是 **"所有章节至少达到了某个阶段"**。

```typescript
/**
 * 检查所有叶子节点是否至少达到了某阶段。
 * 例如 allChaptersAtLeast('content_completed') = 所有章节都 >= content_completed。
 */
function allChaptersAtLeast(
  chapters: ChapterGenStage[],
  minStage: ChapterGenStage,
): boolean {
  const minIdx = chapterStageIndex(minStage);
  return chapters.every(c => chapterStageIndex(c) >= minIdx);
}
```

**推进只发生在 Book 的某个 generating 阶段，且所有子章节已满足条件时：**

```
Book = content_generating +
所有子章节 >= content_completed
    → Book 推进到 content_completed
    → 立即继续推进到 audio_generating

Book = audio_generating +
所有子章节 >= audio_completed
    → Book 推进到 audio_completed
    → 立即继续推进到 video_generating（如适用）
```

### 5.2 Book 回退机制（自动联动）

当任意子章节因 `regenerateChapter()` 回退到上游时，Book 阶段可能不再适用。**检查并自动回退**：

```typescript
async function checkAndRegressBook(bookId: number): Promise<void> {
  const book = await prisma.book.findUnique({
    where: { id: bookId },
    include: { chapters: { where: { level: { gte: 1 }, parentId: 0 } } },
  });
  if (!book) return;

  const curBookStage = book.genStage as BookGenStage;
  const chapterStages = book.chapters.map(c => c.genStage as ChapterGenStage);

  // 从当前 Book 阶段往前检查，找到第一个所有章节都满足的阶段
  let targetIdx = bookStageIndex(curBookStage);
  for (let i = targetIdx; i >= 0; i--) {
    const stage = BOOK_STAGE_ORDER[i];
    // failing 和 draft 是终态/初始态，不去检查子章节
    if (stage === 'failed' || stage === 'draft') continue;

    // 非 generating 阶段（completed 阶段）才检查子章节条件
    if (!stage.endsWith('_generating')) {
      const chStage = stageToChapterStage(stage);
      if (chStage && allChaptersAtLeast(chapterStages, chStage)) {
        targetIdx = i;
        break;
      }
    } else {
      // generating 阶段也检查
      targetIdx = i;
      break;
    }
  }

  const targetBookStage = BOOK_STAGE_ORDER[targetIdx];
  if (targetBookStage !== curBookStage) {
    // 直接写数据库（跳过 safeTransition，因为这是合法回退）
    await prisma.book.update({
      where: { id: bookId },
      data: { genStage: targetBookStage, updatedAt: new Date() },
    });
  }
}

/**
 * Book stage → Chapter stage 映射（对应关系）
 */
function stageToChapterStage(bookStage: BookGenStage): ChapterGenStage | null {
  const map: Partial<Record<BookGenStage, ChapterGenStage>> = {
    'content_completed': 'content_completed',
    'audio_completed':   'audio_completed',
    'video_completed':   'video_completed',
    'outline_completed': 'content_completed', // 大纲完成 ≈ 内容准备开始
  };
  return map[bookStage] || null;
}
```

### 5.3 示例：章节不同步时的流程

```
时间  Chapter 1         Chapter 2         Book.genStage
───  ──────────         ──────────         ─────────────
T1   content_completed  content_generating content_generating
     ↑ 已跑在前面       ↑ 还在生成中       ↑ 正确：还没全部完成

T2   audio_completed    content_generating content_generating
     ↑ 独立推进到音频   ↑ 仍在内容阶段     ↑ 还是 content_generating
                                           ↑ 因为条件 allChapters('content_completed') 不满足

T3   audio_completed    content_completed  content_completed → audio_generating
                     ↑ 刚完成内容          ↑ 检测到全部 >= content_completed
                                           ↑ 立即推进

T4   audio_completed    audio_generating   audio_generating
                        ↑ 正在生成         ↑ 正确推进到 audio 阶段
```

**每个章节独立推进自己的 genStage，不受 Book 影响。** Chapter 1 在 T2 时虽然 Book 还显示 content_generating，但用户依然可以操作 Chapter 1 的音频播放/下载。

---

## 六、前端改动方案

### 6.1 类型定义

```typescript
// book-generator-api.ts
export interface Book {
  genStage: BookGenStage;
  progress: number;
  failedStage?: string;
}

export interface Chapter {
  genStage: ChapterGenStage;
}
```

### 6.2 按钮显隐：单函数决策

```typescript
type ButtonState = 'hidden' | 'generate' | 'generating' | 'retry' | 'play' | 'preview';

function getAudioButtonState(genStage: ChapterGenStage): ButtonState {
  switch (genStage) {
    case 'content_completed': return 'generate';      // 显示"生成音频"
    case 'audio_generating':  return 'generating';     // 显示"生成中..."
    case 'audio_completed':
    case 'video_generating':
    case 'video_completed':   return 'play';           // 显示"播放"
    case 'failed':            return 'retry';          // 显示"重试"
    default:                  return 'hidden';
  }
}

function getVideoButtonState(genStage: ChapterGenStage): ButtonState {
  switch (genStage) {
    case 'audio_completed':   return 'generate';       // 显示"生成视频"
    case 'video_generating':  return 'generating';     // 显示"生成中..."
    case 'video_completed':   return 'preview';        // 显示"预览"
    case 'failed':            return 'retry';
    default:                  return 'hidden';
  }
}
```

### 6.3 重新生成按钮

```vue
<!-- 在播放/预览按钮附近 -->
<button v-if="genStage === 'audio_completed' || genStage === 'video_completed'"
        @click="handleRegenerate('audio')">
  🔄 重新生成
</button>
```

### 6.4 进度描述

```typescript
function getProgressDescription(
  genStage: BookGenStage,
  chapters: Chapter[],
): string {
  switch (genStage) {
    case 'draft':               return '草稿';
    case 'outlining':           return '正在生成大纲...';
    case 'outline_completed':   return '大纲已完成';
    case 'content_generating':  return describeStageCount(chapters, 'content_completed', '内容');
    case 'content_completed':   return '内容已完成';
    case 'audio_generating':    return describeStageCount(chapters, 'audio_completed', '音频');
    case 'audio_completed':     return '音频已完成';
    case 'video_generating':    return describeStageCount(chapters, 'video_completed', '视频');
    case 'video_completed':     return '全部完成 ✓';
    case 'failed':              return `生成失败（${book.failedStage}阶段）`;
  }
}

function describeStageCount(
  chapters: Chapter[],
  stage: ChapterGenStage,
  label: string,
): string {
  const targetIdx = chapterStageIndex(stage);
  const done = chapters.filter(c => chapterStageIndex(c.genStage) >= targetIdx).length;
  const total = chapters.length;
  // 附带上超过当前阶段的章节数
  const ahead = chapters.filter(c => chapterStageIndex(c.genStage) > targetIdx).length;
  const aheadText = ahead > 0 ? `，其中${ahead}章已进入下一阶段` : '';
  return `${label}生成中 (${done}/${total}${aheadText})`;
}
```

---

## 七、数据迁移方案

### 7.1 将旧字段合并为 genStage

```sql
-- Book 迁移
UPDATE Book SET genStage = 
  CASE status
    WHEN 'draft'       THEN 'draft'
    WHEN 'planning'    THEN 'outline_completed'
    WHEN 'generating'  THEN 'content_generating'
    WHEN 'completed'   THEN 
      CASE WHEN EXISTS (SELECT 1 FROM BookChapter WHERE bookId = Book.id AND videoUrl IS NOT NULL)
        THEN 'video_completed'
      WHEN EXISTS (SELECT 1 FROM BookChapter WHERE bookId = Book.id AND audioUrl IS NOT NULL)
        THEN 'audio_completed'
      ELSE 'content_completed'
      END
    WHEN 'failed'      THEN 'failed'
    WHEN 'interrupted' THEN 'failed'
    ELSE 'draft'
  END;

-- BookChapter 迁移
UPDATE BookChapter SET genStage = 
  CASE 
    WHEN videoStatus = 'completed' THEN 'video_completed'
    WHEN videoStatus = 'generating' THEN 'video_generating'
    WHEN audioStatus = 'completed' THEN 'audio_completed'
    WHEN audioStatus = 'generating' THEN 'audio_generating'
    WHEN contentStatus = 'completed' THEN 'content_completed'
    WHEN contentStatus = 'generating' THEN 'content_generating'
    WHEN status = 'completed' THEN 'content_completed'
    ELSE 'idle'
  END;
```

### 7.2 Prisma Schema

```prisma
model Book {
  // ... 原有字段 ...
  genStage        String    @default("draft")
  failedStage     String?   @db.VarChar(30)
  progress        Int       @default(0)
}

model BookChapter {
  // ... 原有字段（不含 status/contentStatus/audioStatus/videoStatus）...
  genStage        String    @default("idle")
}
```

---

## 八、实施步骤

| 步骤 | 内容 | 回滚？ |
|------|------|--------|
| 1 | 新建 `stage-manager.ts`（转移矩阵 + 安全函数） | ✅ 纯新增 |
| 2 | Prisma 新增 `genStage` + `failedStage`（旧字段保留） | ✅ 可逆 |
| 3 | 数据迁移脚本 | ✅ 可逆 |
| 4 | 逐文件替换后端赋值代码 | ⚠️ 需测试 |
| 5 | 更新前端 | ✅ 可逆 |
| 6 | 稳定后删除旧字段 | 24h 后执行 |

---

## 九、核心安全原则（终版）

```
1. 所有状态变更经过 safeTransitionChapter / safeTransitionBook
   → 自动验证转移矩阵 + 乐观锁 + 资源清理

2. 调用方只面向 advanceChapter() / regenerateChapter()
   → 前者的所有目标阶段索引必须大于当前
   → 后者的目标阶段可以等于或小于当前

3. 资源清理自动完成
   → 回退到 content_generating 时自动清除 audioUrl + videoUrl
   → 回退到 audio_generating 时自动清除 videoUrl
   → 永不出现"audioUrl 还在但内容重写了"的不一致

4. Book 阶段联动
   → 子章节推进时检查 Book 是否满足推进条件
   → 子章节回退时自动触发 checkAndRegressBook()
   → Book 阶段总是反映"所有子章节实际达到的最小进展"

5. 前端只读 genStage，永不推断
```

## 十、旧状态 → 新 genStage 映射表

### Book

| 旧 status | 实际情况 | 新 genStage |
|-----------|---------|-------------|
| `draft` | 任意 | `draft` |
| `planning` | 大纲完成 | `outline_completed` |
| `generating` | 部分章节内容未完成 | `content_generating` |
| `generating` | 内容全完成、部分音频未完成 | `audio_generating` |
| `generating` | 音频全完成、部分视频未完成 | `video_generating` |
| `completed` | 只有内容 | `content_completed` |
| `completed` | 有音频无视频 | `audio_completed` |
| `completed` | 有视频 | `video_completed` |
| `failed` / `interrupted` | 任意 | `failed` |

### Chapter

| 旧 contentStatus | 旧 audioStatus | 旧 videoStatus | 新 genStage |
|-----------------|---------------|---------------|-------------|
| `null` | `null` | `null` | `idle` |
| `generating` | `null` | `null` | `content_generating` |
| `completed` | `null` | `null` | `content_completed` |
| `completed` | `generating` | `null` | `audio_generating` |
| `completed` | `completed` | `null` | `audio_completed` |
| `completed` | `completed` | `generating` | `video_generating` |
| `completed` | `completed` | `completed` | `video_completed` |
| 任意失败 | 任意 | 任意 | `failed` |
