# AI 对话消息 Schema camelCase 规范修复 ## 变更信息 - **日期**:2026-02-06 - **类型**:Bug 修复 / 规范统一 - **影响范围**:AI 对话消息 API 响应格式 - **Breaking Change**:否(通过 populate_by_name 兼容) ## 问题描述 `ai_conversation_message.py` 的响应 Schema 字段命名不符合项目规范: - ❌ 使用 snake_case(如 `message_id`, `meta_data`, `ai_job_id`) - ✅ 应该使用 camelCase(如 `messageId`, `metaData`, `aiJobId`) 参考 `screenplay.py` 实现,API 响应字段应使用 camelCase 以符合前端约定。 ## 解决方案 ### 修改内容 为所有响应 Schema 添加 camelCase alias: #### 1. AIConversationMessageResponse ```python class AIConversationMessageResponse(BaseModel): """消息响应""" message_id: UUID = Field(..., alias="messageId", description="消息 ID") conversation_id: UUID = Field(..., alias="conversationId", description="对话会话 ID") user_id: UUID = Field(..., alias="userId", description="用户 ID") ai_job_id: Optional[UUID] = Field(None, alias="aiJobId", description="关联的 AI 任务 ID") # ... 其他字段 meta_data: Dict[str, Any] = Field(default_factory=dict, alias="metaData", description="额外元数据") order_index: int = Field(..., alias="orderIndex", description="消息顺序") created_at: datetime = Field(..., alias="createdAt", description="创建时间") updated_at: datetime = Field(..., alias="updatedAt", description="更新时间") model_config = ConfigDict(from_attributes=True, populate_by_name=True) ``` #### 2. AIConversationMessageListItem ```python class AIConversationMessageListItem(BaseModel): """消息列表项""" message_id: UUID = Field(..., alias="messageId", description="消息 ID") # ... 其他字段 meta_data: Dict[str, Any] = Field(default_factory=dict, alias="metaData", description="额外元数据") ai_job_id: Optional[UUID] = Field(None, alias="aiJobId", description="关联的 AI 任务 ID") created_at: datetime = Field(..., alias="createdAt", description="创建时间") model_config = ConfigDict(from_attributes=True, populate_by_name=True) ``` #### 3. AIConversationMessageListResponse ```python class AIConversationMessageListResponse(BaseModel): """消息列表响应""" items: list[AIConversationMessageListItem] = Field(..., description="消息列表") total: int = Field(..., description="总数") page: int = Field(..., description="当前页码") page_size: int = Field(..., alias="pageSize", description="每页数量") total_pages: int = Field(..., alias="totalPages", description="总页数") model_config = ConfigDict(populate_by_name=True) ``` #### 4. TriggerAIGenerationResponse ```python class TriggerAIGenerationResponse(BaseModel): """触发 AI 生成响应""" job_id: UUID = Field(..., alias="jobId", description="任务 ID") task_id: str = Field(..., alias="taskId", description="Celery 任务 ID") status: str = Field(..., description="任务状态") estimated_credits: int = Field(..., alias="estimatedCredits", description="预估积分消耗") reference_images_count: int = Field(0, alias="referenceImagesCount", description="参考图数量") model_config = ConfigDict(populate_by_name=True) ``` ### 关键配置 **ConfigDict 参数说明**: - `from_attributes=True`:支持从 ORM 模型创建 Pydantic 实例 - `populate_by_name=True`:支持同时使用字段名和 alias(向后兼容) ### 字段映射表 | Python 字段名 | JSON alias (API) | 说明 | |--------------|------------------|------| | `message_id` | `messageId` | 消息 ID | | `conversation_id` | `conversationId` | 对话会话 ID | | `user_id` | `userId` | 用户 ID | | `ai_job_id` | `aiJobId` | AI 任务 ID | | `meta_data` | `metaData` | 元数据 | | `order_index` | `orderIndex` | 排序索引 | | `created_at` | `createdAt` | 创建时间 | | `updated_at` | `updatedAt` | 更新时间 | | `page_size` | `pageSize` | 每页数量 | | `total_pages` | `totalPages` | 总页数 | | `job_id` | `jobId` | 任务 ID | | `task_id` | `taskId` | Celery 任务 ID | | `estimated_credits` | `estimatedCredits` | 预估积分 | | `reference_images_count` | `referenceImagesCount` | 参考图数量 | ## 测试验证 ### 单元测试 ```bash docker exec jointo-server-app pytest tests/unit/repositories/test_ai_conversation_message_repository.py -v ``` **结果**:✅ 17 passed ### 集成测试 ```bash docker exec jointo-server-app pytest tests/integration/test_ai_conversation_api.py -v ``` **结果**:✅ 23 passed ### 完整测试套件 ```bash docker exec jointo-server-app pytest \ tests/unit/repositories/test_ai_conversation_message_repository.py \ tests/integration/test_ai_conversation_api.py -v ``` **结果**:✅ 40 passed ## API 响应示例 ### 发送消息响应(修复后) ```json { "code": 200, "message": "Success", "data": { "messageId": "019c3163-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "conversationId": "019c3163-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "userId": "019c3163-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "aiJobId": null, "role": 1, "content": "这是一条测试消息", "metaData": { "custom_field": "custom_value" }, "orderIndex": 1, "createdAt": "2026-02-06T05:18:45.123456Z", "updatedAt": "2026-02-06T05:18:45.123456Z" } } ``` ### 消息列表响应(修复后) ```json { "code": 200, "message": "Success", "data": { "items": [ { "messageId": "019c3163-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "role": 1, "content": "测试消息", "metaData": {}, "aiJobId": null, "createdAt": "2026-02-06T05:18:45.123456Z" } ], "total": 1, "page": 1, "pageSize": 50, "totalPages": 1 } } ``` ## 向后兼容性 通过 `populate_by_name=True` 配置,API 同时支持: - ✅ camelCase(推荐):`{"messageId": "xxx"}` - ✅ snake_case(兼容):`{"message_id": "xxx"}` **注意**:响应始终使用 camelCase,但接受两种命名风格的输入。 ## 影响分析 ### 前端影响 - ✅ **无影响**:前端已经在使用 camelCase 字段名 - ✅ 集成测试已验证响应格式正确 ### 后端影响 - ✅ **无影响**:内部代码仍使用 snake_case - ✅ Pydantic 自动处理字段名转换 ## 参考规范 参考 `server/app/schemas/screenplay.py` 的实现: - 所有响应 Schema 使用 `alias` 定义 camelCase 字段名 - 使用 `ConfigDict(populate_by_name=True)` 支持向后兼容 - 保持内部代码使用 snake_case(符合 Python PEP 8) ## 相关文档 - [后端技术栈规范 - API Schema 规范与字段命名](/.claude/skills/jointo-tech-stack/references/backend.md#api-schema-规范与字段命名) - [测试规范](/.claude/skills/jointo-tech-stack/references/testing.md) - [Screenplay Schema 实现](server/app/schemas/screenplay.py) ## 总结 ✅ **修复完成** - 所有响应 Schema 已添加 camelCase alias - 通过 40 个测试(17 单元 + 23 集成) - 保持向后兼容性 - 符合项目技术栈规范