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

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_id
  • idx_storyboard_items_target
  • idx_storyboard_items_order
  • idx_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