370 lines
15 KiB
Markdown
370 lines
15 KiB
Markdown
# 数据库设计重构方案 —— 问题分析、解决方案与变更影响
|
||
|
||
---
|
||
|
||
## 问题总览
|
||
|
||
| 编号 | 问题 | 解决类型 | 风险等级 |
|
||
|------|------|----------|----------|
|
||
| 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 脚本或编写临时迁移任务
|