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.
 

28 KiB

ADR 04: 移除废弃的 storyboard_resources 表

状态

提议中(Proposed)

背景

项目中存在一个命名冲突设计废弃的问题:

问题 1:命名冲突

存在两个不同用途的 storyboard_resources

  1. 废弃的关联表(数据库表)

    • 表名:storyboard_resources
    • 模型:StoryboardResource(在 project_resource.py 中)
    • 用途:分镜与项目素材的多对多关联(已被 storyboard_items 替代)
  2. 正在使用的 API 路由

    • 路由前缀:/api/v1/storyboard-resources/*
    • 模块:storyboard_resources.py
    • 用途:管理分镜生成的资源(图片、视频、对白、配音)
    • 实际表:storyboard_images, storyboard_videos, storyboard_dialogues, storyboard_voiceovers

问题 2:设计演进

原设计(已废弃):

storyboard_resources 表
├── storyboard_id (分镜ID)
├── project_resource_id (项目素材ID)
├── resource_type (素材类型:角色/场景/道具/实拍)
└── display_order (显示顺序)

新设计(正在使用):

storyboard_items 表
├── storyboard_id (分镜ID)
├── item_type (元素类型:1=ElementTag, 2=Resource)
├── element_tag_id (剧本元素标签ID,item_type=1时使用)
├── resource_id (项目素材ID,item_type=2时使用)
├── element_name (元素名称,冗余字段)
├── tag_label (标签名称,冗余字段)
├── cover_url (封面URL,冗余字段)
├── is_visible (是否在画面内)
├── spatial_position (画面位置)
├── action_description (动作描述)
├── display_order (显示顺序)
└── z_index (视觉层级)

核心差异

  • storyboard_items 支持多态关联(ElementTag + Resource)
  • storyboard_items 包含视觉属性(位置、层级、动作)
  • storyboard_items 通过 ItemType 枚举区分元素类型

依赖分析

已迁移到 storyboard_items 的功能

  1. Storyboard Servicestoryboard_service.py

    • add_element_to_storyboard() - 使用 StoryboardItem
    • update_element_metadata() - 使用 StoryboardItem
    • get_storyboard_items() - 使用 StoryboardItem
    • reorder_items() - 使用 StoryboardItem
  2. Screenplay Servicescreenplay_service.py

    • _create_storyboards_from_scenes() - 创建分镜时使用 StoryboardItem
  3. API 路由storyboards.py

    • POST /storyboards/{storyboard_id}/items - 添加元素
    • PATCH /items/{item_id} - 更新元素
    • GET /storyboards/{storyboard_id}/items - 获取元素列表
    • POST /storyboards/{storyboard_id}/items/reorder - 重新排序

⚠️ 仍依赖 storyboard_resources 的代码

1. StoryboardResourceService(storyboard_project_resource_service.py

状态⚠️ 完全废弃,但代码未删除

方法列表

  • add_resource_to_storyboard() - 添加素材到分镜
  • remove_resource_from_storyboard() - 从分镜移除素材
  • get_storyboard_resources() - 获取分镜的所有素材
  • get_resource_storyboards() - 获取素材关联的分镜
  • batch_add_resources() - 批量添加素材
  • batch_remove_resources() - 批量移除素材

影响:所有方法都操作 storyboard_resources

2. API 端点(project_resources.py

状态⚠️ 路由存在,但功能已失效

端点列表

POST   /api/v1/storyboards/{storyboard_id}/resources/batch      # 批量添加
DELETE /api/v1/storyboards/{storyboard_id}/resources/batch      # 批量移除
POST   /api/v1/storyboards/{storyboard_id}/resources/{resource_id}  # 单个添加
DELETE /api/v1/storyboards/{storyboard_id}/resources/{resource_id}  # 单个移除
GET    /api/v1/storyboards/{storyboard_id}/resources            # 获取分镜素材
GET    /api/v1/resources/{resource_id}/storyboards              # 获取素材关联的分镜

3. AI Conversation Service(ai_conversation_service.py

状态⚠️ 关键功能受影响

问题代码

async def _get_storyboard_resources(
    self,
    storyboard_id: UUID,
    resource_type: Optional[str] = None,
    search: Optional[str] = None
) -> List[Dict[str, Any]]:
    """获取分镜的可提及资源"""
    from app.repositories.storyboard_resource_repository import StoryboardResourceRepository
    
    storyboard_resource_repo = StoryboardResourceRepository(self.db)
    
    # ❌ 调用已废弃的方法
    storyboard_resources = await storyboard_resource_repo.get_by_storyboard(storyboard_id)
    # ...

当前实现

# StoryboardResourceRepository.get_by_storyboard()
async def get_by_storyboard(self, storyboard_id: UUID) -> List:
    """获取分镜的所有资源(用于 AI Conversation 提及功能)
    
    注意:这是一个简化的实现,返回空列表
    实际应该查询 storyboard_items 表获取关联的资源
    """
    # TODO: 实现完整的分镜资源查询逻辑
    return []

影响:AI 对话中无法提及分镜关联的资源

验证逻辑

# 验证资源是否关联到分镜
is_linked = await storyboard_resource_repo.is_linked(
    conversation.target_id,
    resource_id
)
if not is_linked:
    raise ValidationError("资源未关联到该分镜")

影响:资源关联验证失效

4. Screenplay Service(screenplay_service.py

状态⚠️ 方法存在但未使用

问题代码

async def _sync_storyboard_resources(
    self,
    screenplay_id: UUID,
    project_id: UUID,
    tag_id_maps: Dict[str, Dict[str, UUID]]
) -> int:
    """为标签创建占位符项目资源记录"""
    # 该方法创建 project_resources 记录
    # 但不涉及 storyboard_resources 表
    # 命名误导,实际是创建项目资源

影响:命名误导,但不依赖废弃表

📊 测试代码

状态⚠️ 测试 fixtures 和集成测试依赖废弃表

文件列表

  • tests/conftest.py - test_storyboard_resources fixture
  • tests/integration/test_storyboard_project_resource_api.py - 完整的 API 测试套件
  • tests/unit/services/test_storyboard_project_resource_service.py - Service 单元测试

🎨 前端代码

状态⚠️ 前端严重依赖废弃的 API 端点

1. API 服务层(client/src/services/api/storyboards.ts

问题代码

const syncStoryboardResources = async (
  storyboardId: string, 
  resources: StoryboardResources
) => {
  const desiredIds = collectResourceIds(resources);
  
  // ❌ 调用废弃的 API 端点
  const existing = await apiClient.get(
    `/storyboards/${storyboardId}/resources`
  );
  
  // ❌ 批量添加(废弃端点)
  if (toAdd.length > 0) {
    await apiClient.post(
      `/storyboards/${storyboardId}/resources/batch`,
      { resource_ids: toAdd }
    );
  }
  
  // ❌ 批量移除(废弃端点)
  if (toRemove.length > 0) {
    await apiClient.delete(
      `/storyboards/${storyboardId}/resources/batch`,
      { data: { resource_ids: toRemove } }
    );
  }
};

// 在更新分镜时调用
async update(id: string, data: UpdateStoryboardDto) {
  // ...
  if (data.resources) {
    await syncStoryboardResources(id, data.resources);
  }
  // ...
}

影响

  • 更新分镜时无法同步资源关联
  • 前端 UI 显示的资源列表可能不准确

2. 类型定义(client/src/types/storyboard.ts

问题代码

export interface StoryboardResources {
  characters: StoryboardResourceItem[];
  locations: StoryboardResourceItem[];
  props: StoryboardResourceItem[];
  footages?: StoryboardResourceItem[];
}

export interface Storyboard {
  // ...
  /** 关联的资源(详情接口返回) */
  resources?: StoryboardResources;  // ❌ 废弃字段
  /** v3.0 统一关联表 - 分镜元素关联项 */
  items?: StoryboardItem[];  // ✅ 新字段
  // ...
}

影响

  • ⚠️ 前端同时支持两种数据结构(resourcesitems
  • ⚠️ 造成代码复杂度增加

3. UI 组件依赖

受影响的组件

  1. VideoGenerationPrecheck.tsx

    const storyboardCharacters = storyboard.resources?.characters || [];
    const storyboardLocations = storyboard.resources?.locations || [];
    const storyboardProps = storyboard.resources?.props || [];
    
    • 用途:视频生成前的资源检查
    • 影响:无法正确检查资源完整性
  2. StoryboardResourcesPreview.tsx

    if (storyboard.resources) {
      characters = getResourceWithTag(storyboard.resources.characters, 'character');
      locations = getResourceWithTag(storyboard.resources.locations, 'location');
      props = getResourceWithTag(storyboard.resources.props, 'prop');
      footages = getResourceWithTag(storyboard.resources.footages, 'footage');
    }
    
    • 用途:分镜资源预览面板
    • 影响:资源列表显示不完整
  3. StoryboardList.tsx

    characters: getResourceNames(s.resources?.characters),
    locations: getResourceNames(s.resources?.locations),
    props: getResourceNames(s.resources?.props),
    
    • 用途:分镜列表展示
    • 影响:列表中资源信息缺失
  4. StoryboardEditForm.tsx

    • 用途:分镜编辑表单
    • 影响:资源选择器功能失效
  5. ParseFlowDialog.tsx

    const characters = item.resources?.characters
      ?.map((c) => resourceMap.get(c.resourceId) || '')
      .filter(Boolean) || [];
    
    • 用途:剧本解析流程
    • 影响:资源映射失败
  6. StoryboardBoardItem.tsx

    processV2Items(storyboard.resources.characters);
    processV2Items(storyboard.resources.locations);
    processV2Items(storyboard.resources.props);
    processV2Items(storyboard.resources.footages);
    
    • 用途:分镜看板项
    • 影响:看板显示异常

4. Hooks 依赖

受影响的 Hooks

  1. usePreviewActions.ts

    const existingResources = currentStoryboard.resources || {
      characters: [],
      locations: [],
      props: [],
      footages: []
    };
    
    • 用途:预览操作逻辑
    • 影响:资源添加/移除功能失效
  2. useStoryboardBoardLogic.ts

    const updatedResources = {
      ...(storyboard.resources || { 
        characters: [], 
        locations: [], 
        props: [], 
        footages: [] 
      }),
    };
    
    • 用途:分镜看板逻辑
    • 影响:看板资源同步失败

5. 工具函数依赖

受影响的工具函数

  1. storyboard-board-status.ts
    if (storyboard.resources) {
      storyboard.resources.characters?.forEach((item) => 
        relatedResourceIds.add(item.resourceId)
      );
      storyboard.resources.locations?.forEach((item) => 
        relatedResourceIds.add(item.resourceId)
      );
      storyboard.resources.props?.forEach((item) => 
        relatedResourceIds.add(item.resourceId)
      );
      storyboard.resources.footages?.forEach((item) => 
        relatedResourceIds.add(item.resourceId)
      );
    }
    
    • 用途:分镜状态计算
    • 影响:状态判断不准确

6. Mock 数据

受影响的文件

  • client/src/services/mockApi.ts
  • client/src/services/mock/storyboardApi.ts
  • client/src/mocks/storyboard-board.ts

影响:Mock 数据结构与实际 API 不一致

决策

移除 storyboard_resources 表及相关代码,并修复依赖功能。

理由

  1. 设计已废弃storyboard_items 提供更强大的多态关联能力
  2. 命名冲突:与正在使用的 API 路由同名,造成混淆
  3. 功能重复storyboard_items 已完全覆盖原有功能
  4. 代码冗余:Service 和 API 代码存在但无实际作用
  5. 维护成本:保留废弃代码增加维护负担
  6. 前端依赖严重:前端大量组件依赖废弃的 API,导致功能失效

影响范围总结

层级 受影响模块 严重程度 状态
后端 - 数据库 storyboard_resources 🔴 废弃但未删除
后端 - 模型 StoryboardResource 🔴 废弃但未删除
后端 - Service StoryboardResourceService 🔴 完全废弃
后端 - API 6 个废弃端点 🔴 路由存在但失效
后端 - Repository StoryboardResourceRepository.get_by_storyboard() 🟡 返回空列表
后端 - AI Service _get_storyboard_resources() 🟡 功能失效
后端 - 测试 3 个测试文件 🟡 依赖废弃表
前端 - API 服务 syncStoryboardResources() 🔴 调用废弃端点
前端 - 类型定义 StoryboardResources 接口 🟡 废弃字段
前端 - UI 组件 6 个组件 🔴 资源显示/操作失效
前端 - Hooks 2 个 Hooks 🔴 资源同步失败
前端 - 工具函数 状态计算函数 🟡 计算不准确

总计

  • 🔴 高优先级:9 项
  • 🟡 中优先级:5 项
  • 已迁移:4 项

实施方案

Phase 1: 修复 AI Conversation 功能

目标:使用 storyboard_items 实现资源提及功能

步骤

  1. 修改 StoryboardResourceRepository

    async def get_by_storyboard(self, storyboard_id: UUID) -> List[Dict]:
        """获取分镜的所有关联资源(基于 storyboard_items)"""
        from app.models.storyboard import StoryboardItem, ItemType
        from app.models.project_resource import ProjectResource
        from app.models.project_element_tag import ProjectElementTag
    
        stmt = (
            select(StoryboardItem)
            .where(StoryboardItem.storyboard_id == storyboard_id)
            .order_by(StoryboardItem.display_order)
        )
        result = await self.session.execute(stmt)
        items = result.scalars().all()
    
        resources = []
        for item in items:
            if item.item_type == ItemType.ELEMENT_TAG and item.element_tag_id:
                # 查询 ElementTag 关联的资源
                tag = await self.session.get(ProjectElementTag, item.element_tag_id)
                if tag and tag.resource_id:
                    resource = await self.session.get(ProjectResource, tag.resource_id)
                    if resource:
                        resources.append({
                            'item_id': item.item_id,
                            'item_type': 'element_tag',
                            'element_tag_id': item.element_tag_id,
                            'element_name': item.element_name,
                            'tag_label': item.tag_label,
                            'resource_id': resource.project_resource_id,
                            'resource_url': resource.file_url,
                            'cover_url': item.cover_url or resource.thumbnail_url,
                            'is_visible': item.is_visible,
                            'spatial_position': item.spatial_position,
                            'action_description': item.action_description
                        })
    
            elif item.item_type == ItemType.RESOURCE and item.resource_id:
                # 直接关联的项目素材
                resource = await self.session.get(ProjectResource, item.resource_id)
                if resource:
                    resources.append({
                        'item_id': item.item_id,
                        'item_type': 'resource',
                        'resource_id': resource.project_resource_id,
                        'resource_url': resource.file_url,
                        'cover_url': item.cover_url or resource.thumbnail_url,
                        'is_visible': item.is_visible,
                        'spatial_position': item.spatial_position,
                        'action_description': item.action_description
                    })
    
        return resources
    
  2. 添加 is_linked 方法

    async def is_linked(
        self,
        storyboard_id: UUID,
        resource_id: UUID
    ) -> bool:
        """验证资源是否关联到分镜(基于 storyboard_items)"""
        from app.models.storyboard import StoryboardItem, ItemType
    
        # 检查直接关联
        stmt = select(StoryboardItem).where(
            StoryboardItem.storyboard_id == storyboard_id,
            StoryboardItem.item_type == ItemType.RESOURCE,
            StoryboardItem.resource_id == resource_id
        )
        result = await self.session.execute(stmt)
        if result.scalar_one_or_none():
            return True
    
        # 检查通过 ElementTag 间接关联
        stmt = (
            select(StoryboardItem)
            .join(
                ProjectElementTag,
                StoryboardItem.element_tag_id == ProjectElementTag.element_tag_id
            )
            .where(
                StoryboardItem.storyboard_id == storyboard_id,
                StoryboardItem.item_type == ItemType.ELEMENT_TAG,
                ProjectElementTag.resource_id == resource_id
            )
        )
        result = await self.session.execute(stmt)
        return result.scalar_one_or_none() is not None
    
  3. 更新 AI Conversation Service

    • 保持现有调用方式不变
    • 内部实现已切换到 storyboard_items

Phase 2: 修复前端依赖

目标:将前端从 resources 字段迁移到 items 字段

2.1 修改 API 服务层

文件client/src/services/api/storyboards.ts

步骤

  1. 删除 syncStoryboardResources 函数

    • 该函数调用废弃的 API 端点
    • 资源关联应通过 storyboard_items API 管理
  2. 修改 update 方法

    // 删除这段代码
    if (data.resources) {
      await syncStoryboardResources(id, data.resources);
    }
    
  3. 添加新的资源管理方法(如果需要)

    // 使用 storyboard_items API
    async addItem(storyboardId: string, item: StoryboardItemCreate) {
      return apiClient.post(`/storyboards/${storyboardId}/items`, item);
    }
    
    async updateItem(itemId: string, data: StoryboardItemUpdate) {
      return apiClient.patch(`/items/${itemId}`, data);
    }
    
    async deleteItem(itemId: string) {
      return apiClient.delete(`/items/${itemId}`);
    }
    

2.2 修改类型定义

文件client/src/types/storyboard.ts

步骤

  1. 标记 resources 字段为废弃

    export interface Storyboard {
      // ...
      /** @deprecated 使用 items 字段替代 */
      resources?: StoryboardResources;
      /** v3.0 统一关联表 - 分镜元素关联项 */
      items?: StoryboardItem[];
      // ...
    }
    
  2. 保留 StoryboardResources 接口(向后兼容)

    • 短期内保留,逐步迁移
    • 最终删除

2.3 修改 UI 组件

策略:优先使用 items 字段,resources 作为降级方案

通用工具函数

// client/src/utils/storyboard-items.ts
export function getResourcesFromItems(
  items?: StoryboardItem[]
): StoryboardResources {
  const resources: StoryboardResources = {
    characters: [],
    locations: [],
    props: [],
    footages: []
  };
  
  if (!items) return resources;
  
  items.forEach(item => {
    if (item.itemType === 1 && item.elementTagId) {
      // ElementTag 类型,根据 elementName 判断类型
      // 需要配合后端返回的 metadata 或其他字段
    } else if (item.itemType === 2 && item.resourceId) {
      // Resource 类型,直接使用
      const resourceItem = {
        resourceId: item.resourceId,
        tagId: item.elementTagId,
        tagLabel: item.tagLabel,
        displayOrder: item.displayOrder
      };
      
      // 根据 metadata 或其他字段判断资源类型
      // 暂时放入 footages(需要后端提供类型信息)
      resources.footages?.push(resourceItem);
    }
  });
  
  return resources;
}

修改组件

  1. VideoGenerationPrecheck.tsx

    const resources = storyboard.items 
      ? getResourcesFromItems(storyboard.items)
      : storyboard.resources || { characters: [], locations: [], props: [] };
    
    const storyboardCharacters = resources.characters || [];
    const storyboardLocations = resources.locations || [];
    const storyboardProps = resources.props || [];
    
  2. StoryboardResourcesPreview.tsx

    const resources = useMemo(() => {
      if (storyboard.items) {
        return getResourcesFromItems(storyboard.items);
      }
      return storyboard.resources || { 
        characters: [], 
        locations: [], 
        props: [], 
        footages: [] 
      };
    }, [storyboard]);
    
  3. 其他组件:类似修改

2.4 修改 Hooks

文件

  • client/src/hooks/usePreviewActions.ts
  • client/src/hooks/useStoryboardBoardLogic.ts

修改策略:同 UI 组件

2.5 修改工具函数

文件client/src/utils/storyboard-board-status.ts

修改

// 优先使用 items
if (storyboard.items) {
  storyboard.items.forEach(item => {
    if (item.resourceId) {
      relatedResourceIds.add(item.resourceId);
    }
  });
} else if (storyboard.resources) {
  // 降级方案
  storyboard.resources.characters?.forEach(item => 
    relatedResourceIds.add(item.resourceId)
  );
  // ...
}

2.6 更新 Mock 数据

文件

  • client/src/services/mockApi.ts
  • client/src/services/mock/storyboardApi.ts
  • client/src/mocks/storyboard-board.ts

修改

  • 添加 items 字段
  • 保留 resources 字段(向后兼容)

Phase 3: 删除后端废弃代码

步骤

  1. 删除 Service

    • 删除 server/app/services/storyboard_project_resource_service.py
  2. 删除 API 端点

    • server/app/api/v1/project_resources.py 中删除相关端点:
      # 删除以下端点
      POST   /storyboards/{storyboard_id}/resources/batch
      DELETE /storyboards/{storyboard_id}/resources/batch
      POST   /storyboards/{storyboard_id}/resources/{resource_id}
      DELETE /storyboards/{storyboard_id}/resources/{resource_id}
      GET    /storyboards/{storyboard_id}/resources
      GET    /resources/{resource_id}/storyboards
      
    • 删除 get_storyboard_resource_service 依赖注入函数
  3. 删除 Schema

    • 删除 server/app/schemas/storyboard_project_resource.py
  4. 删除测试

    • 删除 tests/integration/test_storyboard_project_resource_api.py
    • 删除 tests/unit/services/test_storyboard_project_resource_service.py
    • tests/conftest.py 删除 test_storyboard_resources fixture
  5. 删除模型

    • server/app/models/project_resource.py 删除 StoryboardResource
  6. 清理导入

    • server/app/api/v1/__init__.py 中移除相关导入(如果有)
    • 检查其他文件是否有残留导入

Phase 4: 数据库迁移

步骤

  1. 创建迁移文件

    docker exec jointo-server-app alembic revision -m "drop_storyboard_resources_table"
    
  2. 编写迁移脚本

    """drop storyboard_resources table
    
    Revision ID: 20260210_xxxx
    Revises: <previous_revision>
    Create Date: 2026-02-10
    
    说明:
    - 删除废弃的 storyboard_resources 表
    - 该表已被 storyboard_items 替代
    """
    from alembic import op
    import sqlalchemy as sa
    from sqlalchemy import inspect
    
    revision = '20260210_xxxx'
    down_revision = '<previous_revision>'
    branch_labels = None
    depends_on = None
    
    def table_exists(table_name: str) -> bool:
        """检查表是否存在"""
        conn = op.get_bind()
        inspector = inspect(conn)
        return table_name in inspector.get_table_names()
    
    def upgrade() -> None:
        """删除 storyboard_resources 表"""
        if table_exists('storyboard_resources'):
            op.drop_table('storyboard_resources')
            print("✅ 已删除 storyboard_resources 表")
        else:
            print("⚠️  storyboard_resources 表不存在,跳过删除")
    
    def downgrade() -> None:
        """回滚:重新创建 storyboard_resources 表"""
        if not table_exists('storyboard_resources'):
            op.create_table(
                'storyboard_resources',
                sa.Column('storyboard_resource_id', sa.UUID(), nullable=False),
                sa.Column('storyboard_id', sa.UUID(), nullable=False),
                sa.Column('project_resource_id', sa.UUID(), nullable=False),
                sa.Column('resource_type', sa.SmallInteger(), nullable=False),
                sa.Column('display_order', sa.Integer(), nullable=False, server_default='0'),
                sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.text('now()')),
                sa.PrimaryKeyConstraint('storyboard_resource_id'),
                sa.UniqueConstraint('storyboard_id', 'project_resource_id', name='storyboard_resources_unique')
            )
            op.create_index('idx_storyboard_resources_storyboard_id', 'storyboard_resources', ['storyboard_id'])
            op.create_index('idx_storyboard_resources_project_resource_id', 'storyboard_resources', ['project_resource_id'])
            op.create_index('idx_storyboard_resources_type', 'storyboard_resources', ['resource_type'])
            print("✅ 已重新创建 storyboard_resources 表")
    
  3. 执行迁移

    docker exec jointo-server-app alembic upgrade head
    

Phase 5: 验证

检查清单

后端验证

  • AI Conversation 资源提及功能正常
  • AI Conversation 资源关联验证正常
  • Storyboard Items API 正常工作
  • 所有单元测试通过
  • 所有集成测试通过
  • 数据库迁移成功
  • 无残留的 storyboard_resources 引用

前端验证

  • 分镜列表正常显示资源信息
  • 分镜详情页资源预览正常
  • 分镜编辑表单资源选择器正常
  • 视频生成预检查功能正常
  • 分镜看板资源显示正常
  • 剧本解析流程资源映射正常
  • 预览操作(添加/移除资源)正常
  • 分镜状态计算准确

集成验证

  • 创建分镜 → 添加资源 → 查看详情(完整流程)
  • 更新分镜资源 → 刷新页面(数据持久化)
  • 删除分镜 → 资源关联清理(级联删除)
  • AI 对话提及资源 → 验证关联(权限检查)

风险评估

低风险

  • storyboard_items 已在生产环境稳定运行
  • 后端废弃代码未被实际使用
  • 有完整的测试覆盖

中风险

  • ⚠️ AI Conversation 功能需要重新实现
  • ⚠️ 需要验证所有资源关联场景
  • ⚠️ 前端需要大量组件修改

高风险

  • 🔴 前端严重依赖废弃的 API 端点
  • 🔴 多个核心功能受影响(视频生成、资源预览、分镜看板)
  • 🔴 需要前后端协同修改

缓解措施

技术措施

  1. 分阶段部署

    • Phase 1: 修复后端 AI Conversation(不影响前端)
    • Phase 2: 修复前端依赖(向后兼容)
    • Phase 3: 删除后端废弃代码
    • Phase 4: 数据库迁移
    • Phase 5: 删除前端废弃代码
  2. 向后兼容策略

    • 前端同时支持 resourcesitems 字段
    • 优先使用 itemsresources 作为降级方案
    • 逐步迁移,避免一次性破坏
  3. 测试策略

    • 在测试环境充分验证
    • 编写端到端测试覆盖关键流程
    • 保留数据库迁移的 downgrade 脚本
  4. 监控措施

    • 添加日志记录资源关联操作
    • 监控 API 调用失败率
    • 设置告警阈值

回滚方案

  1. 数据库回滚

    docker exec jointo-server-app alembic downgrade -1
    
  2. 代码回滚

    • Git revert 相关提交
    • 重新部署前一版本
  3. 数据恢复

    • 如果有数据迁移,从备份恢复
    • 验证数据完整性

应急预案

  1. 前端降级

    • 如果 items 字段有问题,回退到 resources 字段
    • 临时恢复废弃的 API 端点
  2. 功能降级

    • 暂时禁用受影响的功能
    • 显示维护提示
  3. 快速修复

    • 准备 hotfix 分支
    • 快速发布补丁版本

后续工作

  1. 重命名 Screenplay Service 方法

    • _sync_storyboard_resources()_create_project_resources_from_tags()
    • 更准确反映其功能
  2. 文档更新

    • 更新 API 文档
    • 更新架构图
    • 更新开发指南
  3. 性能优化

    • storyboard_items 添加复合索引
    • 优化资源查询的 JOIN 性能

参考