# ADR 008: 分镜统一关联表设计 **状态**:已接受 **日期**:2026-02-01 **决策者**:架构团队 **标签**:数据库设计, 架构重构, 分镜管理 --- ## 背景 在原始设计中,分镜与剧本元素(角色、场景、道具)的关联采用了两种方式: 1. **UUID[] 数组字段**:在 `storyboards` 表中使用数组字段存储关联 ID - `screenplay_character_ids UUID[]` - `screenplay_scene_ids UUID[]` - `screenplay_prop_ids UUID[]` - 以及对应的标签 ID 数组 2. **独立关联表**:`storyboard_resources` 表用于关联项目素材 这种设计在原型阶段看起来简单,但随着业务需求的深入,暴露出严重的架构问题。 ### 核心问题 #### 1. UUID[] 数组的致命缺陷 **无法存储关联元数据**(最大痛点): - 数组只能存储 `[id1, id2]` - 无法描述"角色在这个镜头里的动作、位置、状态" - 例如:无法表达"孙悟空在这个镜头里是'站着'还是'坐着'?是'正面'还是'背影'?是否在画面内?" - 这些元数据对 AI 视频生成至关重要,是生成精准 Prompt 的关键来源 **引用完整性难以维护**: - 删除角色时需要遍历所有分镜更新数组 - 应用层验证复杂,效率低下 **查询困难**: - 需要使用 `@>` 操作符,不如标准 JOIN 直观 - "查找所有包含'金箍棒'的分镜"需要特殊语法 **顺序管理困难**: - 数组中间插入/删除元素非常麻烦 - 无法灵活调整元素顺序 #### 2. 架构不一致 - UUID[] 数组用于剧本元素 - 独立表用于项目素材 - 两种方式并存,增加理解和维护成本 ### 业务需求 作为 AI 视频生成工作台,我们需要: 1. **精准的 Prompt 生成**: - "孙悟空(角色)拿着金箍棒(道具),动作是'挥舞',位置在画面左侧" - 这些细节信息是 AI 生成高质量视频的关键 2. **复杂的分镜制作需求**: - 元素的可见性控制(画外音、旁白) - 元素的空间位置(左/中/右/背景/前景) - 元素的视觉层级(用于合成时的图层顺序) - 元素的动作描述(用于动画生成) 3. **统一的前端接口**: - 前端需要一次性获取分镜的所有关联元素 - 不希望分别调用多个 API --- ## 决策 **采用统一的 `storyboard_items` 关联表**,废弃 UUID[] 数组和独立的 `storyboard_resources` 表。 ### 新表设计 ```sql CREATE TABLE storyboard_items ( item_id UUID PRIMARY KEY, storyboard_id UUID NOT NULL, -- 多态关联核心 item_type SMALLINT NOT NULL, -- 1=Character, 2=Location, 3=Prop, 4=Resource target_id UUID NOT NULL, -- 冗余字段(优化读性能,避免 JOIN) target_name TEXT, target_cover_url TEXT, -- 核心优势:关联属性(Metadata on Relation) is_visible BOOLEAN DEFAULT true, spatial_position TEXT, action_description TEXT, -- 标签关联 tag_id UUID, -- 排序与层级 display_order INTEGER DEFAULT 0, z_index INTEGER DEFAULT 0, -- 扩展字段 metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT now(), CONSTRAINT storyboard_items_unique UNIQUE (storyboard_id, target_id, tag_id) NULLS NOT DISTINCT ); ``` ### 设计原则 1. **多态关联**:`item_type` 区分不同类型的元素 2. **冗余字段**:`target_name`, `target_cover_url` 避免 JOIN,极大提升读性能 3. **关联属性**:在关联行上存储元数据(动作、位置、状态等) 4. **统一管理**:所有类型的元素使用同一张表 --- ## 后果 ### 优势 #### 1. 极强的元数据能力(核心价值) 可以在关联行上添加任意属性: - `action_description`:动作描述(如"大笑"、"奔跑"、"挥舞") - `spatial_position`:画面位置(left/center/right/background/foreground) - `is_visible`:是否在画面内(画外音设为 false) - `z_index`:视觉层级(用于合成时的图层顺序) **业务价值**:为 AI 视频生成提供精准的 Prompt 来源。 #### 2. 统一的 API 接口 前端只需调用一个 API: ``` GET /api/v1/storyboards/{id}/items ``` 返回该分镜的所有关联元素(角色、场景、道具、素材),前端统一渲染。 #### 3. 简化的排序管理 - `display_order` 字段支持拖拽调整顺序 - 数组中间插入/删除非常困难,关联表轻松实现 #### 4. 高效的删除逻辑 - 删除分镜:`DELETE FROM storyboard_items WHERE storyboard_id = ?` - 删除角色:`DELETE FROM storyboard_items WHERE target_id = ?` - 索引命中率极高 #### 5. 符合关系型数据库设计范式 - 符合第三范式(3NF) - 易于理解和维护 - 支持复杂查询 ### 劣势及解决方案 #### 1. 表数据量大 **问题**:千万级关联行 **解决方案**: - PostgreSQL 擅长处理亿级简单关联表 - 配合分区(按 `project_id` 哈希分区)轻松解决 - 正确的索引策略保证查询性能 #### 2. 多态关联的 JOIN 复杂性 **问题**:`target_id` 可能指向不同表,SQL 没法写死 JOIN **解决方案**: - 冗余字段(`target_name`, `target_cover_url`) - 读取时直接查 `storyboard_items` 表,完全不需要 JOIN 原表 - "空间换时间",对于读多写少的业务非常划算 #### 3. 数据一致性维护 **问题**:修改角色名称时需同步更新 **解决方案**: - 异步触发更新(PostgreSQL LISTEN/NOTIFY 或 Celery) - 虽然增加了写复杂度,但换来了极致的读性能 --- ## 实施计划 ### 阶段 1:数据库变更 1. 创建 `storyboard_items` 表 2. 创建必要的索引 3. 更新数据库迁移脚本 ### 阶段 2:Service 层改造 1. 更新 `StoryboardService` 2. 新增元素管理方法: - `get_storyboard_items()` - `add_element_to_storyboard()` - `remove_element_from_storyboard()` - `update_element_metadata()` ### 阶段 3:API 接口更新 1. 新增 API 端点: - `GET /api/v1/storyboards/{id}/items` - `POST /api/v1/storyboards/{id}/items` - `PATCH /api/v1/storyboard-items/{id}` - `DELETE /api/v1/storyboard-items/{id}` ### 阶段 4:前端适配 1. 更新前端 API 调用 2. 统一元素渲染逻辑 3. 支持元素元数据编辑 ### 阶段 5:文档更新 1. 更新 Storyboard Service 文档 2. 创建 ADR 文档(本文档) 3. 创建 Changelog --- ## 相关决策 - [ADR 001: UUID v7 迁移](./001-uuid-v7-migration.md) - [ADR 005: 标签系统重构](./005-variant-to-tag-system-refactor.md) --- ## 参考资料 - [Storyboard Service 文档](../../requirements/backend/04-services/project/storyboard-service.md) - [PostgreSQL 多态关联最佳实践](https://www.postgresql.org/docs/current/ddl-constraints.html) - [关系型数据库第三范式](https://en.wikipedia.org/wiki/Third_normal_form) --- **决策日期**:2026-02-01 **实施状态**:进行中 **预计完成**:2026-02-15