# 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 Service**(`storyboard_service.py`) - ✅ `add_element_to_storyboard()` - 使用 `StoryboardItem` - ✅ `update_element_metadata()` - 使用 `StoryboardItem` - ✅ `get_storyboard_items()` - 使用 `StoryboardItem` - ✅ `reorder_items()` - 使用 `StoryboardItem` 2. **Screenplay Service**(`screenplay_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`) **状态**:⚠️ 路由存在,但功能已失效 **端点列表**: ```python 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`) **状态**:⚠️ 关键功能受影响 **问题代码**: ```python 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) # ... ``` **当前实现**: ```python # StoryboardResourceRepository.get_by_storyboard() async def get_by_storyboard(self, storyboard_id: UUID) -> List: """获取分镜的所有资源(用于 AI Conversation 提及功能) 注意:这是一个简化的实现,返回空列表 实际应该查询 storyboard_items 表获取关联的资源 """ # TODO: 实现完整的分镜资源查询逻辑 return [] ``` **影响**:AI 对话中无法提及分镜关联的资源 **验证逻辑**: ```python # 验证资源是否关联到分镜 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`) **状态**:⚠️ 方法存在但未使用 **问题代码**: ```python 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`) **问题代码**: ```typescript 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`) **问题代码**: ```typescript export interface StoryboardResources { characters: StoryboardResourceItem[]; locations: StoryboardResourceItem[]; props: StoryboardResourceItem[]; footages?: StoryboardResourceItem[]; } export interface Storyboard { // ... /** 关联的资源(详情接口返回) */ resources?: StoryboardResources; // ❌ 废弃字段 /** v3.0 统一关联表 - 分镜元素关联项 */ items?: StoryboardItem[]; // ✅ 新字段 // ... } ``` **影响**: - ⚠️ 前端同时支持两种数据结构(`resources` 和 `items`) - ⚠️ 造成代码复杂度增加 #### 3. UI 组件依赖 **受影响的组件**: 1. **`VideoGenerationPrecheck.tsx`** ```typescript const storyboardCharacters = storyboard.resources?.characters || []; const storyboardLocations = storyboard.resources?.locations || []; const storyboardProps = storyboard.resources?.props || []; ``` - 用途:视频生成前的资源检查 - 影响:无法正确检查资源完整性 2. **`StoryboardResourcesPreview.tsx`** ```typescript 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`** ```typescript characters: getResourceNames(s.resources?.characters), locations: getResourceNames(s.resources?.locations), props: getResourceNames(s.resources?.props), ``` - 用途:分镜列表展示 - 影响:列表中资源信息缺失 4. **`StoryboardEditForm.tsx`** - 用途:分镜编辑表单 - 影响:资源选择器功能失效 5. **`ParseFlowDialog.tsx`** ```typescript const characters = item.resources?.characters ?.map((c) => resourceMap.get(c.resourceId) || '') .filter(Boolean) || []; ``` - 用途:剧本解析流程 - 影响:资源映射失败 6. **`StoryboardBoardItem.tsx`** ```typescript processV2Items(storyboard.resources.characters); processV2Items(storyboard.resources.locations); processV2Items(storyboard.resources.props); processV2Items(storyboard.resources.footages); ``` - 用途:分镜看板项 - 影响:看板显示异常 #### 4. Hooks 依赖 **受影响的 Hooks**: 1. **`usePreviewActions.ts`** ```typescript const existingResources = currentStoryboard.resources || { characters: [], locations: [], props: [], footages: [] }; ``` - 用途:预览操作逻辑 - 影响:资源添加/移除功能失效 2. **`useStoryboardBoardLogic.ts`** ```typescript const updatedResources = { ...(storyboard.resources || { characters: [], locations: [], props: [], footages: [] }), }; ``` - 用途:分镜看板逻辑 - 影响:看板资源同步失败 #### 5. 工具函数依赖 **受影响的工具函数**: 1. **`storyboard-board-status.ts`** ```typescript 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** ```python 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 方法** ```python 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` 方法** ```typescript // 删除这段代码 if (data.resources) { await syncStoryboardResources(id, data.resources); } ``` 3. **添加新的资源管理方法**(如果需要) ```typescript // 使用 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` 字段为废弃** ```typescript export interface Storyboard { // ... /** @deprecated 使用 items 字段替代 */ resources?: StoryboardResources; /** v3.0 统一关联表 - 分镜元素关联项 */ items?: StoryboardItem[]; // ... } ``` 2. **保留 `StoryboardResources` 接口**(向后兼容) - 短期内保留,逐步迁移 - 最终删除 #### 2.3 修改 UI 组件 **策略**:优先使用 `items` 字段,`resources` 作为降级方案 **通用工具函数**: ```typescript // 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`** ```typescript 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`** ```typescript 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` **修改**: ```typescript // 优先使用 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` 中删除相关端点: ```python # 删除以下端点 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. **创建迁移文件** ```bash docker exec jointo-server-app alembic revision -m "drop_storyboard_resources_table" ``` 2. **编写迁移脚本** ```python """drop storyboard_resources table Revision ID: 20260210_xxxx Revises: 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 = '' 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. **执行迁移** ```bash 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. **向后兼容策略** - 前端同时支持 `resources` 和 `items` 字段 - 优先使用 `items`,`resources` 作为降级方案 - 逐步迁移,避免一次性破坏 3. **测试策略** - 在测试环境充分验证 - 编写端到端测试覆盖关键流程 - 保留数据库迁移的 downgrade 脚本 4. **监控措施** - 添加日志记录资源关联操作 - 监控 API 调用失败率 - 设置告警阈值 #### 回滚方案 1. **数据库回滚** ```bash 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 性能 ## 参考 - [ADR 03: 分镜关键帧管理](./03-storyboard-keyframe-management.md) - [Storyboard Service 需求文档](../requirements/backend/04-services/project/storyboard-service.md) - [StoryboardItem 模型定义](../../server/app/models/storyboard.py)