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.7 KiB

修复文件夹 API Schema 驼峰命名支持

日期: 2026-01-29
类型: Bug 修复
影响范围: 文件夹 API、Schema 定义

问题描述

文件夹 API 测试失败,返回 400 错误:

POST /api/v1/folders completed with status 400

根本原因

  1. Schema 字段命名不一致

    • 前端/测试使用驼峰命名:folderCategory, parentFolderId, coverImageId
    • 后端 Schema 定义使用蛇形命名:folder_category, parent_folder_id, cover_image_id
    • Pydantic 没有配置 aliaspopulate_by_name,导致无法识别驼峰命名
  2. API 响应序列化错误

    • UUID 对象未转换为字符串
    • 缺少 folderCategoryName 字段

解决方案

1. 修复 Schema 定义

为所有文件夹相关 Schema 添加 aliaspopulate_by_name 配置:

FolderCreate

class FolderCreate(BaseModel):
    """创建文件夹请求"""
    name: str = Field(..., min_length=1, max_length=255, description="文件夹名称")
    description: Optional[str] = Field(None, description="文件夹描述")
    parent_folder_id: Optional[UUID] = Field(None, alias="parentFolderId", description="父文件夹ID")
    folder_category: Optional[int] = Field(
        None, 
        alias="folderCategory",
        ge=1, 
        le=2, 
        description="文件夹分类: 1=我的项目, 2=协作项目"
    )
    color: Optional[str] = Field(
        None, 
        pattern=r"^#[0-9A-Fa-f]{6}$",
        description="文件夹颜色(十六进制)"
    )
    icon: Optional[str] = Field(None, max_length=50, description="文件夹图标")
    cover_image_id: Optional[UUID] = Field(None, alias="coverImageId", description="封面图片ID")
    
    model_config = {
        "populate_by_name": True  # 支持驼峰和蛇形命名
    }

FolderUpdate

class FolderUpdate(BaseModel):
    """更新文件夹请求"""
    name: Optional[str] = Field(None, min_length=1, max_length=255)
    description: Optional[str] = None
    color: Optional[str] = Field(None, pattern=r"^#[0-9A-Fa-f]{6}$")
    icon: Optional[str] = Field(None, max_length=50)
    cover_image_id: Optional[UUID] = Field(None, alias="coverImageId")
    sort_order: Optional[int] = Field(None, alias="sortOrder")
    
    model_config = {
        "populate_by_name": True
    }

FolderMove

class FolderMove(BaseModel):
    """移动文件夹请求"""
    parent_folder_id: Optional[UUID] = Field(None, alias="parentFolderId", description="目标父文件夹ID")
    
    model_config = {
        "populate_by_name": True
    }

2. 修复 API 响应

修改 POST /api/v1/folders 端点,确保:

  • UUID 转换为字符串
  • 添加 folderCategoryName 字段
@router.post("", response_model=ApiResponse[FolderResponse], summary="创建文件夹")
async def create_folder(
    folder_data: FolderCreate,
    current_user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session)
):
    service = FolderService(session)
    folder = await service.create_folder(current_user.user_id, folder_data)
    
    return success_response(data={
        "id": str(folder.id),  # UUID → 字符串
        "name": folder.name,
        "description": folder.description,
        "parentFolderId": str(folder.parent_folder_id) if folder.parent_folder_id else None,
        "ownerId": str(folder.owner_id),  # UUID → 字符串
        "path": folder.path,
        "level": folder.level,
        "folderCategory": folder.folder_category,
        "folderCategoryName": FolderCategory.get_display_name(folder.folder_category),  # 新增
        "sortOrder": folder.sort_order,
        "color": folder.color,
        "icon": folder.icon,
        "isShared": folder.is_shared,
        "createdAt": folder.created_at,
        "updatedAt": folder.updated_at,
    })

3. 添加导入

from app.models.folder import FolderCategory

修改文件

修改文件

  • server/app/schemas/folder.py

    • FolderCreate, FolderUpdate, FolderMove 添加 aliasmodel_config
  • server/app/api/v1/folders.py

    • 修复 create_folder 响应序列化
    • 添加 FolderCategory 导入

验证结果

测试结果

$ docker exec jointo-server-app pytest tests/integration/test_folder_api.py::TestFolderCRUD::test_create_root_folder -v
PASSED ✅

API 请求示例

请求(驼峰命名):

POST /api/v1/folders
{
  "name": "我的项目",
  "description": "根文件夹",
  "folderCategory": 1
}

响应

{
  "code": 200,
  "message": "Success",
  "data": {
    "id": "019c07ec-72a7-7152-a888-cfe3159ce317",
    "name": "我的项目",
    "description": "根文件夹",
    "parentFolderId": null,
    "ownerId": "019c07eb-70d8-7191-be3e-ba0e6099ef70",
    "path": "/我的项目",
    "level": 0,
    "folderCategory": 1,
    "folderCategoryName": "我的项目",
    "sortOrder": 0,
    "color": null,
    "icon": null,
    "isShared": false,
    "createdAt": "2026-01-29T04:04:22.350207+00:00",
    "updatedAt": "2026-01-29T04:04:22.350214+00:00"
  }
}

影响

正面影响

  • 支持前端使用驼峰命名调用 API
  • 保持后端代码使用 Python 蛇形命名规范
  • 提高 API 的易用性和一致性

API 兼容性

  • 同时支持驼峰和蛇形命名(populate_by_name=True
  • 向后兼容,不影响现有代码

最佳实践

Pydantic Schema 命名规范

  1. 字段定义使用蛇形命名(Python 规范):

    folder_category: Optional[int]
    parent_folder_id: Optional[UUID]
    
  2. 添加驼峰命名 alias(前端友好):

    folder_category: Optional[int] = Field(None, alias="folderCategory")
    parent_folder_id: Optional[UUID] = Field(None, alias="parentFolderId")
    
  3. 启用双向命名支持

    model_config = {
        "populate_by_name": True  # 同时接受 alias 和原始字段名
    }
    

API 响应规范

  1. UUID 必须转换为字符串

    "id": str(folder.id)
    
  2. 枚举值提供显示名称

    "folderCategory": folder.folder_category,
    "folderCategoryName": FolderCategory.get_display_name(folder.folder_category)
    

相关文档

后续工作

  • 为其他文件夹 API 端点添加相同的响应格式
  • 补充缺失的测试 fixture(test_folder_id, test_other_user_id 等)
  • 实现完整的文件夹 CRUD、移动、共享、导出功能
  • 运行完整的集成测试套件