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.
 

5.5 KiB

Changelog: 修复剧本解析接口项目ID问题

日期: 2026-02-11
类型: Bug Fix
影响范围: 剧本解析、项目资源管理

问题描述

剧本解析接口 /api/v1/screenplays/{screenplay_id}/parse 在存储角色、场景、道具到项目级别表时,使用的是剧本所属的 project_id

当剧本属于子项目时,screenplay.project_id 是子项目ID,但 project_characters/locations/props 表的 project_id 字段设计为存储"父级项目ID"(parent_project_id = NULL 的项目),导致数据存储到错误的项目。

根本原因

数据库设计约定:

  • project_characters/locations/props 表的 project_id 字段存储父级项目ID
  • 所有子项目共享父项目的角色/场景/道具资源
  • 但代码实现中直接使用了 screenplay.project_id,未判断是否为子项目

解决方案

1. 新增辅助方法 _get_parent_project_id

ScreenplayService 中添加辅助方法,用于获取父级项目ID:

async def _get_parent_project_id(self, project_id: UUID) -> UUID:
    """获取父级项目ID
    
    如果项目是子项目(parent_project_id != NULL),返回其父项目ID
    如果项目是父项目(parent_project_id == NULL),返回自身ID
    """
    project_repo = ProjectRepository(self.db)
    project = await project_repo.get_by_id(str(project_id))
    
    if not project:
        raise Exception(f"项目不存在: {project_id}")
    
    # 如果是子项目,返回父项目ID;否则返回自身ID
    if project.parent_project_id:
        return project.parent_project_id
    else:
        return project.id

2. 修改 store_parsed_elements 方法

在存储解析结果前,先获取父级项目ID:

# 0. 获取剧本和父级项目 ID
screenplay = await self.repository.get_by_id(screenplay_id)
if not screenplay:
    raise Exception(f"剧本不存在: {screenplay_id}")

# ✅ 获取父级项目ID(如果是子项目,需要获取其父项目ID)
parent_project_id = await self._get_parent_project_id(screenplay.project_id)
logger.info("剧本所属项目 ID: %s, 父级项目 ID: %s", screenplay.project_id, parent_project_id)

3. 更新所有资源创建调用

将所有 project_id 参数替换为 parent_project_id

  • 创建项目角色:char_repo.create_or_update(project_id=parent_project_id, ...)
  • 创建项目场景:loc_repo.create_or_update(project_id=parent_project_id, ...)
  • 创建项目道具:prop_repo.create_or_update(project_id=parent_project_id, ...)
  • 创建元素标签:ProjectElementTag(project_id=parent_project_id, ...)
  • 创建分镜:_create_storyboards_from_ai(project_id=parent_project_id, ...)

影响范围

修改文件

  • server/app/services/screenplay_service.py
    • 新增方法:_get_parent_project_id
    • 修改方法:store_parsed_elements

影响功能

  • 剧本解析(AI 提取角色/场景/道具)
  • 项目资源管理(角色/场景/道具列表)
  • 分镜创建(元素关联)

测试建议

1. 测试场景1:父项目剧本解析

# 1. 创建父项目
POST /api/v1/projects
{
  "name": "测试父项目",
  "type": "mine"
}

# 2. 上传剧本到父项目(不自动创建子项目)
POST /api/v1/screenplays/upload
{
  "project_id": "<父项目ID>",
  "name": "测试剧本",
  "auto_create_subproject": false,
  "file": <剧本文件>
}

# 3. 触发解析
POST /api/v1/screenplays/{screenplay_id}/parse

# 4. 验证:角色/场景/道具的 project_id 应该是父项目ID
SELECT * FROM project_characters WHERE project_id = '<父项目ID>';

2. 测试场景2:子项目剧本解析

# 1. 创建父项目
POST /api/v1/projects
{
  "name": "测试父项目",
  "type": "mine"
}

# 2. 上传剧本并自动创建子项目
POST /api/v1/screenplays/upload
{
  "project_id": "<父项目ID>",
  "name": "测试剧本",
  "auto_create_subproject": true,
  "file": <剧本文件>
}

# 3. 触发解析
POST /api/v1/screenplays/{screenplay_id}/parse

# 4. 验证:角色/场景/道具的 project_id 应该是父项目ID(不是子项目ID)
SELECT * FROM project_characters WHERE project_id = '<父项目ID>';
SELECT * FROM projects WHERE parent_project_id = '<父项目ID>';

3. 验证SQL

-- 验证角色/场景/道具是否存储到父项目
SELECT 
    pc.character_id,
    pc.name AS character_name,
    pc.project_id,
    p.name AS project_name,
    p.parent_project_id
FROM project_characters pc
JOIN projects p ON pc.project_id = p.id
WHERE pc.project_id = '<父项目ID>';

-- 验证分镜是否关联到正确的项目
SELECT 
    s.storyboard_id,
    s.title,
    s.project_id,
    p.name AS project_name,
    p.parent_project_id
FROM storyboards s
JOIN projects p ON s.project_id = p.id
WHERE s.project_id IN ('<父项目ID>', '<子项目ID>');

向后兼容性

完全向后兼容

  • 对于父项目剧本:行为不变(parent_project_id 返回自身ID)
  • 对于子项目剧本:修复了错误行为(现在正确返回父项目ID)

相关文档

  • 数据库迁移:server/alembic/versions/20260208_1000_add_project_level_resources.py
  • 架构决策:docs/architecture/adrs/003-screenplay-resource-architecture.md
  • 项目模型:server/app/models/project.py
  • 剧本服务:server/app/services/screenplay_service.py

总结

此次修复确保剧本解析接口正确处理子项目场景,将角色/场景/道具资源存储到父级项目,符合数据库设计约定和业务逻辑。