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.
 

4.6 KiB

Changelog: 修复分镜 API metadata 字段命名格式

日期: 2026-02-11
类型: Bug 修复
影响范围: /api/v1/storyboards 接口

问题描述

分镜接口返回的 metadata 字段内部使用 snake_case 命名(如 screenplay_idcharacter_tags),不符合项目 API 规范(应使用 camelCase)。

问题示例

{
  "metadata": {
    "screenplay_id": "019c4b4b-c98f-76c3-9a9c-19cb6b125614",
    "character_tags": {"孙悟空": "youth"},
    "prop_tags": {}
  }
}

根本原因

  1. StoryboardResponse 使用手动 alias 定义字段别名,未使用 alias_generator
  2. metadata 字段直接从数据库 meta_data (JSONB) 读取,内部键名未转换

解决方案

1. 创建通用工具函数

文件: server/app/utils/case_converter.py

def to_camel(string: str) -> str:
    """将 snake_case 转换为 camelCase"""
    components = string.split('_')
    return components[0] + ''.join(x.title() for x in components[1:])

def convert_dict_to_camel(data: Any) -> Any:
    """递归转换字典键名从 snake_case 到 camelCase"""
    if isinstance(data, dict):
        return {
            to_camel(key): convert_dict_to_camel(value)
            for key, value in data.items()
        }
    elif isinstance(data, list):
        return [convert_dict_to_camel(item) for item in data]
    else:
        return data

2. 更新 Schema 定义

文件: server/app/schemas/storyboard.py

修改前

class StoryboardResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True, populate_by_name=True)
    
    storyboard_id: str = Field(..., alias="storyboardId")
    project_id: str = Field(..., alias="projectId")
    # ... 手动定义所有 alias
    metadata: Dict[str, Any] = Field(default_factory=dict)

修改后

from pydantic import model_serializer
from app.utils.case_converter import to_camel, convert_dict_to_camel

class StoryboardResponse(BaseModel):
    model_config = ConfigDict(
        from_attributes=True,
        populate_by_name=True,
        alias_generator=to_camel  # 自动生成 camelCase 别名
    )
    
    storyboard_id: str = Field(...)
    project_id: str = Field(...)
    # ... 无需手动定义 alias
    metadata: Dict[str, Any] = Field(default_factory=dict)
    
    @model_serializer
    def serialize_model(self) -> Dict[str, Any]:
        """序列化时转换 metadata 内部键名为 camelCase"""
        data = {to_camel(k): v for k, v in self.__dict__.items()}
        if 'metadata' in data and data['metadata']:
            data['metadata'] = convert_dict_to_camel(data['metadata'])
        return data

3. 同步更新的 Schema

  • StoryboardResponse
  • StoryboardItemResponse
  • StoryboardListResponse
  • StoryboardDurationStats
  • StoryboardReorderResponse

修复效果

修复后返回格式

{
  "metadata": {
    "screenplayId": "019c4b4b-c98f-76c3-9a9c-19cb6b125614",
    "characterTags": {"孙悟空": "youth"},
    "propTags": {},
    "locationTags": {}
  }
}

技术细节

alias_generator 工作原理

  1. 顶层字段: alias_generator 自动将 storyboard_id 转换为 storyboardId
  2. 嵌套字典: model_serializer 递归转换 metadata 内部的所有键名

参考实现

参考了 server/app/schemas/project_element.py 的实现模式:

  • 使用 alias_generator=to_camel 自动转换字段名
  • 使用 model_serializer 处理特殊序列化逻辑

影响评估

破坏性变更

⚠️ 前端需要同步更新

如果前端已经在使用 snake_case 访问 metadata 字段,需要更新为 camelCase:

// 修改前
const screenplayId = storyboard.metadata.screenplay_id;
const characterTags = storyboard.metadata.character_tags;

// 修改后
const screenplayId = storyboard.metadata.screenplayId;
const characterTags = storyboard.metadata.characterTags;

兼容性

  • 数据库无需变更(仍存储 snake_case)
  • 仅影响 API 响应格式
  • 不影响请求参数格式

测试建议

  1. 单元测试: 验证 convert_dict_to_camel 递归转换逻辑
  2. 集成测试: 验证 /api/v1/storyboards 接口返回格式
  3. 前端测试: 确认前端能正确解析新格式

相关文件

  • server/app/utils/case_converter.py (新增)
  • server/app/schemas/storyboard.py (修改)
  • server/app/api/v1/storyboards.py (无需修改)

后续优化

可考虑将其他 Schema 的 metadata 字段也统一使用此模式:

  • server/app/schemas/screenplay.py
  • server/app/schemas/project_element_tag.py
  • server/app/schemas/screenplay_tag.py