# 数据库设计重构方案 —— 问题分析、解决方案与变更影响

---

## 问题总览

| 编号 | 问题 | 解决类型 | 风险等级 |
|------|------|----------|----------|
| 1 | 数据冗余（冗余归档表 + user_works 状态不同步） | 代码重构 + Flyway 迁移 | 中 |
| 2 | 模式文件不同步（schema.sql / init_database.sql / Flyway 冲突） | 文件清理 + 重命名 | 低 |
| 3 | 枚举类型重复定义（4个实体各自定义 TaskType/WorkType） | 代码重构 | 低 |
| 4 | Payment 字段命名混淆（orderId 实为业务订单号，非外键） | **已撤回，不做改动** | 无 |
| 5 | 用户名冗余存储（8张表都存 username，改名时无法同步） | 新增级联更新方法 | 低 |
| 6 | Base64 图片直接存数据库（storyboardImages 字段） | 保存时上传 COS | 低 |
| 7 | task_queue 与 task_status 职责边界模糊 | 仅文档注释 | 无 |

---

## 问题一：数据冗余

### 问题描述

1. `completed_tasks_archive` 表：将完成的任务复制到归档表，但原表数据并未删除，形成冗余副本
2. `failed_tasks_cleanup_log` 表：记录清理日志，但 `user_error_log` 表已有完整的错误记录，功能重复
3. `user_works` 状态与 `task_queue` / 业务任务表不同步：多条更新路径导致状态可能不一致

### 解决方案

**1A — 删除冗余归档表**

- 删除 JPA 实体：`CompletedTaskArchive.java`、`FailedTaskCleanupLog.java`
- 删除 Repository：`CompletedTaskArchiveRepository.java`、`FailedTaskCleanupLogRepository.java`
- 新增 Flyway 迁移：`V16__Drop_Redundant_Archive_Tables.sql`

```sql
DROP TABLE IF EXISTS completed_tasks_archive;
DROP TABLE IF EXISTS failed_tasks_cleanup_log;
```

**1B — 重构 TaskCleanupService**

- 移除所有归档/清理日志的写入逻辑
- 失败任务清理时改用 `userErrorLogService.logErrorAsync()` 记录，不再写单独的表

**1C — 增强 user_works 状态修复**

- 在 `TaskQueueService.repairUserWorkStatus()` 中增加对长时间停留在 `PROCESSING` 状态的任务的检测和修复
- 在 `TaskQueueScheduler` 中添加 5 分钟执行一次的定时任务，自动发现并修复不一致状态

### 变更影响

| 影响范围 | 说明 |
|----------|------|
| 数据库 | 删除 2 张表。已有数据丢失（归档记录），但原始数据仍在 task_queue / user_error_log 中 |
| TaskCleanupService | 大幅精简，从 ~150 行减到 ~60 行 |
| TaskQueueScheduler | 新增 `repairUserWorkStatus()` 定时任务 |
| 前端 | **无影响**，前端未直接查询这两张表 |

### 涉及文件

```
[已删除] model/CompletedTaskArchive.java
[已删除] model/FailedTaskCleanupLog.java
[已删除] repository/CompletedTaskArchiveRepository.java
[已删除] repository/FailedTaskCleanupLogRepository.java
[已修改] service/TaskCleanupService.java          — 移除归档逻辑
[已修改] service/TaskQueueService.java             — 增强 repairUserWorkStatus()
[已修改] scheduler/TaskQueueScheduler.java         — 新增状态修复定时任务
[已新增] db/migration/V16__Drop_Redundant_Archive_Tables.sql
```

---

## 问题二：模式文件不同步

### 问题描述

1. `schema.sql` 与 `init_database.sql` 内容不一致，且与 Flyway 迁移脚本重复
2. Flyway 迁移脚本存在版本号冲突：V2、V3、V7 各有两个同版本号的文件

### 解决方案

**2A — 删除过时的 schema.sql**

该文件仅在最初开发时使用，Flyway 已接管所有数据库变更。

**2B — 标注 init_database.sql**

在文件头部添加醒目注释，说明仅用于全新手动部署，不由 Flyway 管理。同时移除其中 `completed_tasks_archive` 和 `failed_tasks_cleanup_log` 的建表语句（与问题一保持一致）。

**2C — 修复 Flyway 版本号冲突**

| 原文件名 | 新文件名 |
|----------|----------|
| V2__add_indexes.sql | V2.1__add_indexes.sql |
| V2__add_task_status_cascade_triggers.sql | V2.2__add_task_status_cascade_triggers.sql |
| V3__Create_Task_Queue_Table.sql | V3.1__Create_Task_Queue_Table.sql |
| V3__fix_foreign_key_cascade.sql | V3.2__fix_foreign_key_cascade.sql |
| V7__Add_indexes_task_status.sql | V7.1__Add_indexes_task_status.sql |
| V7__Create_Task_Cleanup_Tables.sql | V7.2__Create_Task_Cleanup_Tables.sql |

`V7.2__Create_Task_Cleanup_Tables.sql` 内容改为空操作（注释说明已被 V16 取代）。

### 变更影响

| 影响范围 | 说明 |
|----------|------|
| 数据库 | **无影响**——仅重命名迁移文件，内容不变 |
| 应用启动 | Flyway 不再因版本冲突报错 |
| 开发者 | 只需关注 `db/migration/` 下的 Flyway 脚本 |

### 涉及文件

```
[已删除] src/main/resources/schema.sql
[已修改] init_database.sql                    — 添加注释头 + 移除冗余建表
[已重命名] V2/V3/V7 重复的迁移脚本（共 6 个文件）
[已修改] V7.2__Create_Task_Cleanup_Tables.sql — 改为 no-op
```

---

## 问题三：枚举类型重复定义

### 问题描述

4 个实体类各自在内部定义了功能相同的枚举：

- `TaskQueue.TaskType` — TEXT_TO_VIDEO / IMAGE_TO_VIDEO / STORYBOARD_VIDEO / STORYBOARD_IMAGE
- `TaskStatus.TaskType` — 同上
- `PointsFreezeRecord.TaskType` — 同上
- `UserWork.WorkType` — 同上（仅名称不同）

新增任务类型时需要改 4 个地方，容易遗漏。

### 解决方案

新建统一枚举 `com.example.demo.model.enums.CommonTaskType`：

```java
public enum CommonTaskType {
    TEXT_TO_VIDEO("文生视频"),
    IMAGE_TO_VIDEO("图生视频"),
    STORYBOARD_VIDEO("分镜视频"),
    STORYBOARD_IMAGE("分镜图");
}
```

删除 4 个实体类中的内部枚举，所有字段类型、构造方法参数、getter/setter 统一改为 `CommonTaskType`。

### 全项目扫描结果

| 扫描范围 | 旧枚举引用数 | 状态 |
|----------|-------------|------|
| `src/main/`（主代码，所有 service / model / controller） | 0 | 已全部替换 |
| `src/test/PointsRefundIntegrationTest.java` | 16 处 `PointsFreezeRecord.TaskType` | 已修复 → `CommonTaskType` |
| `frontend/src/`（前端 Vue 组件） | 使用字符串值 `'TEXT_TO_VIDEO'` 等 | 无需修改（枚举名不变，API 返回值不变） |

### 变更影响

| 影响范围 | 说明 |
|----------|------|
| 数据库 | **无影响**——JPA 使用 `@Enumerated(EnumType.STRING)`，数据库中存储的字符串值不变 |
| 编译 | 所有引用 `TaskQueue.TaskType`、`TaskStatus.TaskType`、`UserWork.WorkType`、`PointsFreezeRecord.TaskType` 的地方都需要改为 `CommonTaskType` |
| 测试代码 | `PointsRefundIntegrationTest.java` 中 16 处旧引用已同步更新 |
| 前端 | **无影响**——前端使用字符串值匹配，枚举名称未改变 |

### 涉及文件

```
[已新增] model/enums/CommonTaskType.java
[已修改] model/TaskQueue.java              — 删除内部枚举，字段改为 CommonTaskType
[已修改] model/TaskStatus.java             — 同上
[已修改] model/PointsFreezeRecord.java     — 同上
[已修改] model/UserWork.java               — 删除 WorkType，字段改为 CommonTaskType
[已修改] service/TaskQueueService.java     — 更新所有枚举引用
[已修改] service/TaskStatusPollingService.java — 更新 createTaskStatus / convertToTaskStatusType
[已修改] service/UserWorkService.java      — 更新 WorkType 引用
[已修改] test/service/PointsRefundIntegrationTest.java — 16 处旧枚举引用更新
```

---

## 问题四：Payment 字段命名混淆 —— 已撤回，不做改动

### 问题描述

`Payment.orderId` 存储的是业务订单号（如 `"ORD123456789"`），是一个 String 标识符，不是外键。命名为 `orderId` 可能被误读为外键。

### 评估结论：不需要改动

经过完整的支付链路分析，改名会导致以下问题：

**JSON 序列化会破坏前端**
- `Payment` 实体没有 `@JsonProperty` 注解，Jackson 序列化直接使用 Java 字段名
- 改名后 JSON key 从 `"orderId"` 变为 `"orderNumber"`
- 前端 `Payments.vue` 等多个组件通过 `payment.orderId` 访问该字段，改名后全部变成 `undefined`
- 修复需要同步改动前端 6+ 个 Vue 文件，涉及面过大

**原始问题不影响功能**
- `orderId` 命名虽然容易被误读为外键，但代码逻辑完全正确
- 支付回调（Alipay / LuluPay）通过 `out_trade_no` 查找，链路正常
- 这只是一个代码可读性问题，不值得冒支付功能出错的风险

### 涉及文件

```
无改动
```

---

## 问题五：用户名冗余存储

### 问题描述

8 张业务表都存储了 `username` 字段（冗余存储，非外键关联），用户改名时这些表无法同步更新，导致数据不一致。

涉及的表：`task_queue`、`task_status`、`text_to_video_tasks`、`image_to_video_tasks`、`storyboard_video_tasks`、`points_freeze_records`、`user_error_log`、`user_works`

### 解决方案

在 `UserService` 中新增 `cascadeUpdateUsername(oldUsername, newUsername)` 方法，在用户改名时通过 `JdbcTemplate` 批量更新所有相关表：

```java
private void cascadeUpdateUsername(String oldUsername, String newUsername) {
    String[] tables = {
        "task_queue", "task_status", "text_to_video_tasks",
        "image_to_video_tasks", "storyboard_video_tasks",
        "points_freeze_records", "user_error_log", "user_works"
    };
    for (String table : tables) {
        jdbcTemplate.update(
            "UPDATE " + table + " SET username = ? WHERE username = ?",
            newUsername, oldUsername);
    }
}
```

该方法在 `updateUsername()` 中被调用，确保改名操作是原子性的。

### 变更影响

| 影响范围 | 说明 |
|----------|------|
| 数据库 | **无结构变更**——仅在改名时批量 UPDATE 数据 |
| 性能 | 改名操作会执行 8 条 UPDATE 语句，但用户改名是低频操作，影响可忽略 |
| 索引 | 各表的 `username` 字段已有索引，UPDATE 不会全表扫描 |
| 前端 | **无影响** |

### 涉及文件

```
[已修改] service/UserService.java — 注入 JdbcTemplate，新增 cascadeUpdateUsername()
```

---

## 问题六：Base64 图片直接存数据库

### 问题描述

分镜图功能中，外部 AI API 返回 Base64 图片后：
- `resultUrl`（网格合并图）**已经在保存时上传 COS** ✓
- `storyboardImages`（单独分镜图片 JSON 数组）**直接将 Base64 存入数据库** ✗

一张 Base64 图片约 200KB-1MB，6 张分镜图意味着每条任务记录可能有 3-6MB 的文本存在 `LONGTEXT` 字段中，严重影响数据库性能和备份效率。

### 解决方案

**在保存时就将每张图上传 COS，存 COS 返回的链接，Base64 不进库。**

新增私有方法 `uploadStoryboardImagesToCos(taskId, storyboardImagesJson)`：

```java
private String uploadStoryboardImagesToCos(String taskId, String storyboardImagesJson) {
    // 1. 解析 JSON 数组
    // 2. 遍历每张图，如果是 data:image 开头的 Base64 就上传 COS
    // 3. 替换为 COS URL
    // 4. 上传失败时保留原值（不丢数据）
    // 5. 返回替换后的 JSON
}
```

两个保存路径都在写库之前调用此方法：
- `saveStoryboardImageResultWithTransactionTemplate()` — 异步事务保存
- `saveStoryboardImageResult()` — 同步事务保存

**不需要批量迁移**——修复源头后，新数据直接存 COS URL，不会再有 Base64 进库。

### 变更影响

| 影响范围 | 说明 |
|----------|------|
| 数据库 | 新数据 `storyboardImages` 字段从 ~3-6MB Base64 变为 ~500 字节的 URL JSON，**大幅减小存储** |
| COS | 每次生成分镜图会多上传 6 张图片到 COS（本就应该上传） |
| 前端 | **无影响**——前端通过 URL 加载图片，COS URL 和 Base64 Data URI 都能被 `<img>` 标签识别 |
| 视频生成 | 后续用分镜图生成视频时，需要从 COS URL 下载图片（代码中已有 `downloadImageAsBase64` 方法处理） |

### 涉及文件

```
[已修改] service/StoryboardVideoService.java — 新增 uploadStoryboardImagesToCos()，
                                               两个保存方法在存库前调用
[已删除] migrateBase64ToCos() 批量迁移方法（不再需要）
[已修改] scheduler/TaskQueueScheduler.java   — 删除 migrateBase64ToCos 定时任务
```

---

## 问题七：task_queue 与 task_status 职责边界模糊

### 问题描述

`task_queue` 和 `task_status` 两张表字段高度相似（都有 taskId、username、taskType、status），开发者容易误认为是重复表可以合并。但实际上它们职责完全不同：

- `task_queue` — **任务调度引擎**：管理内存 BlockingQueue 的持久化，控制并发执行
- `task_status` — **外部 API 轮询**：存储 externalTaskId，跟踪外部 AI API 的返回状态

合并会破坏现有的调度和轮询逻辑。

### 解决方案

**不做代码结构变更**，仅在关键位置添加 Javadoc 注释明确职责边界：

- `TaskQueue.java` — 类级注释说明"任务调度与执行引擎的持久化层"
- `TaskStatus.java` — 类级注释说明"跟踪已提交到外部 AI API 的任务轮询状态"
- `TaskQueueService.java` — 类级注释说明调度逻辑和调用关系
- `TaskStatusPollingService.java` — 类级注释说明轮询逻辑和级联更新路径

### 变更影响

| 影响范围 | 说明 |
|----------|------|
| 数据库 | **无影响** |
| 运行时 | **无影响**——仅添加注释 |
| 开发者 | 阅读代码时能快速理解两张表的区别，避免误合并 |

### 涉及文件

```
[已修改] model/TaskQueue.java                      — 添加 Javadoc
[已修改] model/TaskStatus.java                     — 添加 Javadoc
[已修改] service/TaskQueueService.java             — 添加 Javadoc
[已修改] service/TaskStatusPollingService.java     — 添加 Javadoc
```

---

## 部署注意事项

### Flyway 迁移

- 如果数据库是**全新部署**：Flyway 会自动按版本号顺序执行所有迁移脚本，包括 V16（删除冗余表）
- 如果数据库**已有数据**：
  - `V16__Drop_Redundant_Archive_Tables.sql` 会删除 `completed_tasks_archive` 和 `failed_tasks_cleanup_log`，其中数据会丢失
  - 原始任务数据仍保留在 `task_queue` 和 `user_error_log` 中，不受影响
  - 如果 Flyway 的 `flyway_schema_history` 中已有旧版本号的记录（V2、V3、V7），需要手动清理或设置 `flyway.baseline-on-migrate=true`

### COS 配置

- 问题六的修复依赖 COS 服务开启（`cosService.isEnabled() == true`）
- 如果 COS 未配置或不可用，代码会**回退保留 Base64**，不会导致数据丢失或报错

### 历史 Base64 数据

- 修复后新生成的分镜图会直接存 COS URL
- 已存在的历史 Base64 数据不会自动清理
- 如需清理历史数据，可手动执行一次性 SQL 脚本或编写临时迁移任务
