diff --git a/api/docs/数据迁移指南.md b/api/docs/数据迁移指南.md new file mode 100644 index 0000000..2e2d37f --- /dev/null +++ b/api/docs/数据迁移指南.md @@ -0,0 +1,258 @@ +# 生产环境迁移操作指南 + +本文档描述新表结构(`chapter_sections`、`memoir_images`)的完整迁移步骤,适用于生产环境。 + +--- + +## 一、迁移概览 + +| 阶段 | 内容 | 前置条件 | +|------|------|----------| +| 1 | 创建 `chapter_sections` 表、`chapters.cover_image` 列 | 无 | +| 2 | 将 `chapters.content` + `chapters.images` 迁移到 `chapter_sections`,并删除旧列 | 阶段 1 完成 | +| 3 | 创建 `memoir_images` 表 | 阶段 2 完成(依赖 `chapter_sections`) | +| 4 | 将 `chapters.cover_image`、`chapter_sections.image` 迁移到 `memoir_images` | 阶段 3 完成 | +| 5 | (可选)添加 `chapter_sections.image_id` 外键,删除 `chapter_sections.image` | 阶段 4 完成 | + +--- + +## 二、前置条件检查 + +### 2.1 环境要求 + +- Python 3.x,已安装项目依赖(`uv sync` 或 `pip install -r requirements.txt`) +- 数据库:PostgreSQL +- 环境变量:`.env` 中配置正确的 `DATABASE_URL` + +### 2.2 数据库状态检查 + +执行前确认: + +```sql +-- 1. chapters 表存在且包含 content、images 列(迁移前应有) +SELECT column_name FROM information_schema.columns +WHERE table_schema = 'public' AND table_name = 'chapters' +AND column_name IN ('content', 'images', 'cover_image'); +-- 预期:content、images 存在;cover_image 可能不存在 + +-- 2. chapter_sections 表在阶段 1 前应不存在 +SELECT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = 'chapter_sections' +); +-- 预期:false(首次迁移) + +-- 3. memoir_images 表在阶段 3 前应不存在 +SELECT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = 'memoir_images' +); +-- 预期:false(首次迁移) +``` + +### 2.3 备份(强烈建议) + +```bash +# 生产环境务必先备份 +pg_dump -U -d -F c -f backup_$(date +%Y%m%d_%H%M%S).dump +``` + +--- + +## 三、迁移步骤 + +### 阶段 1:执行 SQL 建表/加列(chapter_sections) + +**前置 SQL 条件**:无。`chapters` 表需已存在。 + +**方式 A:使用 psql 手动执行** + +```bash +cd /path/to/life-echo +psql -U -d -f api/migrations/add_chapter_sections.sql +``` + +**方式 B:使用一键脚本(推荐)** + +一键脚本 `run_chapter_sections_migration` 会自动执行该 SQL,然后执行数据迁移,无需单独执行本阶段。 + +若选择方式 A,则需在阶段 2 使用 `migrate_chapters_to_sections`;若选择方式 B,可跳过阶段 1 和 2 的拆分,直接执行 `run_chapter_sections_migration`。 + +--- + +### 阶段 2:数据迁移(chapters → chapter_sections) + +**前置 SQL 条件**:已执行 `api/migrations/add_chapter_sections.sql`,即: + +- `chapter_sections` 表已创建 +- `chapters.cover_image` 列已添加(若不存在会自动添加) + +**执行命令**(在 `api` 目录下): + +```bash +cd /path/to/life-echo/api +uv run python -m scripts.migrate_chapters_to_sections +``` + +或使用一键脚本(包含阶段 1 + 2): + +```bash +cd /path/to/life-echo/api +uv run python -m scripts.run_chapter_sections_migration +``` + +**脚本行为**: + +- 若 `chapters.content` 已不存在,则跳过迁移 +- 将 `chapters.content` + `chapters.images` 解析为 sections,写入 `chapter_sections` +- 将每章首张图写入 `chapters.cover_image` +- 最后删除 `chapters.content`、`chapters.images` + +--- + +### 阶段 3:执行 SQL 建表(memoir_images) + +**前置 SQL 条件**:`chapter_sections` 表已存在(`memoir_images` 的 `section_id` 外键依赖它)。 + +**方式 A:使用 psql 手动执行** + +```bash +cd /path/to/life-echo +psql -U -d -f api/migrations/add_memoir_images_table.sql +``` + +**方式 B:使用 memoir_images 迁移脚本** + +`run_memoir_images_migration` 会先执行该 SQL,再执行数据迁移。若 SQL 文件不存在会给出警告。 + +--- + +### 阶段 4:数据迁移(cover_image / section.image → memoir_images) + +**前置 SQL 条件**:已执行 `api/migrations/add_memoir_images_table.sql`,即 `memoir_images` 表已创建。 + +**执行命令**(在项目根目录或 `api` 目录下): + +```bash +cd /path/to/life-echo +uv run python -m api.scripts.run_memoir_images_migration +``` + +或: + +```bash +cd /path/to/life-echo/api +uv run python -m scripts.run_memoir_images_migration +``` + +**脚本行为**: + +- 先执行 `add_memoir_images_table.sql`(若存在) +- 迁移 `chapters.cover_image` → `memoir_images`(`section_id` 为空表示封面) +- 迁移 `chapter_sections.image` → `memoir_images`(`section_id` 非空表示段落配图) +- 已存在的记录会跳过,支持幂等执行 + +--- + +### 阶段 5:(可选)添加 image_id 外键并删除旧 JSON 列 + +**前置 SQL 条件**:`memoir_images` 表已有数据,且 `chapter_sections` 中对应 section 的配图已迁移完成。 + +**执行**: + +```bash +cd /path/to/life-echo +psql -U -d -f api/migrations/add_section_image_id_fk.sql +``` + +**该 SQL 会**: + +1. 为 `chapter_sections` 添加 `image_id` 列(外键指向 `memoir_images`) +2. 回填:将 `memoir_images` 中 `section_id` 指向本行的记录的 `id` 写入 `image_id` +3. 删除 `chapter_sections.image` 列 + +--- + +## 四、推荐生产执行顺序(精简版) + +### 方案 A:分步执行(便于排查问题) + +```bash +# 1. 备份 +pg_dump -U -d -F c -f backup_$(date +%Y%m%d).dump + +# 2. chapter_sections 迁移(SQL + 数据) +cd /path/to/life-echo/api +psql -U -d -f migrations/add_chapter_sections.sql +uv run python -m scripts.migrate_chapters_to_sections + +# 3. memoir_images 迁移(SQL + 数据) +cd /path/to/life-echo +psql -U -d -f api/migrations/add_memoir_images_table.sql +uv run python -m api.scripts.run_memoir_images_migration + +# 4. (可选)image_id 外键 +psql -U -d -f api/migrations/add_section_image_id_fk.sql +``` + +### 方案 B:使用一键脚本(更简洁) + +```bash +# 1. 备份 +pg_dump -U -d -F c -f backup_$(date +%Y%m%d).dump + +# 2. chapter_sections 一键迁移(内含 SQL + 数据) +cd /path/to/life-echo/api +uv run python -m scripts.run_chapter_sections_migration + +# 3. memoir_images 一键迁移(内含 SQL + 数据) +cd /path/to/life-echo +uv run python -m api.scripts.run_memoir_images_migration + +# 4. (可选)image_id 外键 +psql -U -d -f api/migrations/add_section_image_id_fk.sql +``` + +--- + +## 五、前置 SQL 条件汇总 + +| 脚本 / SQL 文件 | 前置条件 | +|-----------------|----------| +| `add_chapter_sections.sql` | `chapters` 表已存在 | +| `migrate_chapters_to_sections` / `run_chapter_sections_migration` | 已执行 `add_chapter_sections.sql` | +| `add_memoir_images_table.sql` | `chapter_sections` 表已存在 | +| `run_memoir_images_migration` | 已执行 `add_memoir_images_table.sql`(或脚本会自动执行) | +| `add_section_image_id_fk.sql` | `memoir_images` 表已存在且数据已迁移完成 | + +--- + +## 六、回滚说明 + +- 迁移会删除 `chapters.content`、`chapters.images`,以及(若执行阶段 5)`chapter_sections.image` +- 回滚需从备份恢复,或根据业务自行编写反向迁移脚本 +- 建议在测试环境完整跑通后再在生产执行 + +--- + +## 七、验证 + +```sql +-- 1. chapter_sections 有数据 +SELECT COUNT(*) FROM chapter_sections; + +-- 2. chapters 无 content/images +SELECT column_name FROM information_schema.columns +WHERE table_schema = 'public' AND table_name = 'chapters' +AND column_name IN ('content', 'images'); +-- 预期:无结果 + +-- 3. memoir_images 有数据 +SELECT COUNT(*) FROM memoir_images; + +-- 4. 封面与段落配图数量合理 +SELECT + COUNT(*) FILTER (WHERE section_id IS NULL) AS cover_count, + COUNT(*) FILTER (WHERE section_id IS NOT NULL) AS section_count +FROM memoir_images; +```