# Changelog: 分镜统一关联表重构 **日期**:2026-02-01 **类型**:架构重构 **影响范围**:数据库、后端 Service、API 接口 **破坏性变更**:是 --- ## 概述 将分镜与元素(角色、场景、道具、素材)的关联从 UUID[] 数组 + 独立表的混合模式,重构为统一的 `storyboard_items` 关联表。 --- ## 变更内容 ### 1. 数据库变更 #### 废弃内容 **storyboards 表**:移除以下字段 - `screenplay_character_ids UUID[]` - `screenplay_character_tag_ids UUID[]` - `screenplay_scene_ids UUID[]` - `screenplay_scene_tag_ids UUID[]` - `screenplay_prop_ids UUID[]` - `screenplay_prop_tag_ids UUID[]` **storyboard_resources 表**:整张表废弃 #### 新增内容 **storyboard_items 表**:统一关联表 ```sql CREATE TABLE storyboard_items ( item_id UUID PRIMARY KEY, storyboard_id UUID NOT NULL, item_type SMALLINT NOT NULL, target_id UUID NOT NULL, target_name TEXT, target_cover_url TEXT, 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 ); ``` **索引**: - `idx_storyboard_items_storyboard_id` - `idx_storyboard_items_target` - `idx_storyboard_items_order` - `idx_storyboard_items_tag_id` ### 2. Service 层变更 #### StoryboardService 新增方法 ```python # 元素管理 async def get_storyboard_items(user_id, storyboard_id) -> List[Dict] async def add_element_to_storyboard(user_id, storyboard_id, item_type, target_id, ...) -> Dict async def remove_element_from_storyboard(user_id, item_id) -> None async def update_element_metadata(user_id, item_id, ...) -> Dict # AI 解析剧本后的自动关联 async def create_storyboards_from_ai(project_id, screenplay_id, storyboards_data, element_id_maps, tag_id_maps) -> List[UUID] ``` #### 废弃方法 ```python # 以下方法不再使用 async def add_resource(user_id, storyboard_id, project_resource_id, resource_type, display_order) async def remove_resource(user_id, storyboard_id, project_resource_id, resource_type) ``` ### 3. API 接口变更 #### 新增接口 ``` GET /api/v1/storyboards/{storyboard_id}/items POST /api/v1/storyboards/{storyboard_id}/items PATCH /api/v1/storyboard-items/{item_id} DELETE /api/v1/storyboard-items/{item_id} POST /api/v1/storyboards/{storyboard_id}/items/reorder ``` #### 废弃接口 ``` POST /api/v1/storyboards/{storyboard_id}/resources DELETE /api/v1/storyboards/{storyboard_id}/resources/{project_resource_id} ``` ### 4. 数据模型变更 #### 新增模型 ```python class ItemType(IntEnum): CHARACTER = 1 LOCATION = 2 PROP = 3 RESOURCE = 4 class StoryboardItem(SQLModel, table=True): item_id: UUID storyboard_id: UUID item_type: int target_id: UUID target_name: Optional[str] target_cover_url: Optional[str] is_visible: bool = True spatial_position: Optional[str] action_description: Optional[str] tag_id: Optional[UUID] display_order: int = 0 z_index: int = 0 metadata: Dict[str, Any] = {} created_at: datetime ``` #### 废弃模型 ```python class StoryboardResource # 整个模型废弃 ``` --- ## 迁移指南 ### 数据迁移(如果已有数据) 由于项目尚未开发,无需数据迁移。如果未来需要迁移,参考以下步骤: ```python # 1. 从 storyboards 表的数组字段迁移 for storyboard in storyboards: for idx, char_id in enumerate(storyboard.screenplay_character_ids or []): # 获取角色信息 character = get_character(char_id) # 创建关联记录 create_item( storyboard_id=storyboard.id, item_type=ItemType.CHARACTER, target_id=char_id, target_name=character.name, target_cover_url=character.character_image_url, display_order=idx ) # 2. 从 storyboard_resources 表迁移 for resource in storyboard_resources: # 获取素材信息 res = get_resource(resource.project_resource_id) # 创建关联记录 create_item( storyboard_id=resource.storyboard_id, item_type=ItemType.RESOURCE, target_id=resource.project_resource_id, target_name=res.name, target_cover_url=res.cover_url, display_order=resource.display_order ) ``` ### 前端适配 #### 旧代码(需要多次 API 调用) ```typescript // 获取分镜详情 const storyboard = await getStoryboard(id); // 获取角色列表 const characters = await getCharactersByIds(storyboard.screenplay_character_ids); // 获取场景列表 const scenes = await getScenesByIds(storyboard.screenplay_scene_ids); // 获取素材列表 const resources = await getResourcesByStoryboard(id); ``` #### 新代码(统一接口) ```typescript // 一次性获取所有元素 const items = await getStoryboardItems(id); // 按类型分组 const characters = items.filter(item => item.item_type === 1); const locations = items.filter(item => item.item_type === 2); const props = items.filter(item => item.item_type === 3); const resources = items.filter(item => item.item_type === 4); ``` --- ## 优势 ### 1. 元数据能力 可以存储关联属性: - ✅ 动作描述:`action_description` - ✅ 画面位置:`spatial_position` - ✅ 可见性:`is_visible` - ✅ 视觉层级:`z_index` **业务价值**:为 AI 视频生成提供精准 Prompt。 ### 2. 统一接口 前端只需调用一个 API 获取所有元素,大幅简化逻辑。 ### 3. 高性能 - 冗余字段避免 JOIN - 正确的索引策略 - 读多写少的业务场景完美适配 ### 4. 易于扩展 未来可以轻松添加新的关联属性,无需修改表结构。 --- ## 注意事项 ### 1. 冗余字段同步 当元素名称或封面变更时,需要异步更新 `storyboard_items` 表: ```python # 角色名称变更时 async def on_character_name_changed(character_id: UUID, new_name: str): await db.execute( "UPDATE storyboard_items SET target_name = ? WHERE target_id = ? AND item_type = 1", new_name, character_id ) ``` ### 2. 数据一致性 Service 层负责验证所有引用关系: - 添加元素前检查 `target_id` 是否存在 - 添加标签前检查 `tag_id` 是否存在 - 防止重复关联 ### 3. 性能优化 - 为 `storyboard_id` 创建索引 - 为 `(item_type, target_id)` 创建复合索引 - 考虑按 `project_id` 进行表分区(数据量大时) --- ## 相关文档 - [ADR 008: 分镜统一关联表设计](../adrs/008-storyboard-unified-association-table.md) - [Storyboard Service 文档](../../requirements/backend/04-services/project/storyboard-service.md) --- **变更日期**:2026-02-01 **实施状态**:进行中 **预计完成**:2026-02-15