# 数据库设计重构方案 —— 问题分析、解决方案与变更影响 --- ## 问题总览 | 编号 | 问题 | 解决类型 | 风险等级 | |------|------|----------|----------| | 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 都能被 `` 标签识别 | | 视频生成 | 后续用分镜图生成视频时,需要从 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 脚本或编写临时迁移任务