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
6.9 KiB
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 表:统一关联表
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_ididx_storyboard_items_targetidx_storyboard_items_orderidx_storyboard_items_tag_id
2. Service 层变更
StoryboardService 新增方法
# 元素管理
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]
废弃方法
# 以下方法不再使用
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. 数据模型变更
新增模型
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
废弃模型
class StoryboardResource # 整个模型废弃
迁移指南
数据迁移(如果已有数据)
由于项目尚未开发,无需数据迁移。如果未来需要迁移,参考以下步骤:
# 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 调用)
// 获取分镜详情
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);
新代码(统一接口)
// 一次性获取所有元素
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 表:
# 角色名称变更时
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进行表分区(数据量大时)
相关文档
变更日期:2026-02-01
实施状态:进行中
预计完成:2026-02-15