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.
7.0 KiB
7.0 KiB
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
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
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
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
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 |
参考图数量 |
测试验证
单元测试
docker exec jointo-server-app pytest tests/unit/repositories/test_ai_conversation_message_repository.py -v
结果:✅ 17 passed
集成测试
docker exec jointo-server-app pytest tests/integration/test_ai_conversation_api.py -v
结果:✅ 23 passed
完整测试套件
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 响应示例
发送消息响应(修复后)
{
"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"
}
}
消息列表响应(修复后)
{
"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)
相关文档
总结
✅ 修复完成
- 所有响应 Schema 已添加 camelCase alias
- 通过 40 个测试(17 单元 + 23 集成)
- 保持向后兼容性
- 符合项目技术栈规范