You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

6.9 KiB

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 表。

新表设计

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

相关决策


参考资料


决策日期:2026-02-01
实施状态:进行中
预计完成:2026-02-15