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
4.6 KiB
Credit API Pydantic UUID 兼容性修复
日期: 2026-01-28
类型: Bug Fix
影响范围: Credit API - 积分消耗端点
问题描述
POST /api/v1/credits/consume 端点在返回响应时出现 Pydantic 验证错误:
pydantic_core._pydantic_core.ValidationError: 2 validation errors for CreditConsumptionResponse
consumption_id
UUID input should be a string, bytes or UUID object [type=uuid_type, input_value=UUID('...'), input_type=UUID]
user_id
UUID input should be a string, bytes or UUID object [type=uuid_type, input_value=UUID('...'), input_type=UUID]
根本原因
SQLModel + Pydantic v2 的兼容性问题:
- SQLModel 的 UUID 字段在 ORM 对象中是
uuid.UUID类型 - Pydantic v2 在验证时对 UUID 类型有严格的类型检查
- 使用
from_orm()或model_validate()时,UUID 对象无法正确转换 model_validator(mode='before')在接收 UUID 对象时也会报错
解决方案
在 API 层手动构造响应字典,将 UUID 对象转换为字符串:
# server/app/api/v1/credits.py
@router.post("/consume", response_model=SuccessResponse[CreditConsumptionResponse])
async def consume_credits(...):
consumption_log = await service.consume_credits(...)
# 手动构造响应字典,避免 SQLModel + Pydantic v2 的 UUID 兼容性问题
from app.services.credit_service import FeatureType, TaskStatus
response_dict = {
"consumptionId": str(consumption_log.consumption_id),
"userId": str(consumption_log.user_id),
"featureType": consumption_log.feature_type,
"featureTypeName": FeatureType.to_name(consumption_log.feature_type),
"creditsConsumed": consumption_log.credits_consumed,
"taskId": consumption_log.task_id,
"taskStatus": consumption_log.task_status,
"taskStatusName": TaskStatus.to_name(consumption_log.task_status),
"aiJobId": str(consumption_log.ai_job_id) if consumption_log.ai_job_id else None,
"resourceId": str(consumption_log.resource_id) if consumption_log.resource_id else None,
"resourceType": consumption_log.resource_type,
"createdAt": consumption_log.created_at,
"completedAt": consumption_log.completed_at
}
response_data = CreditConsumptionResponse(**response_dict)
return SuccessResponse(data=response_data)
同时修改 schema 的 model_validator 从 mode='before' 改为 mode='after':
# server/app/schemas/credit.py
class CreditConsumptionResponse(BaseModel):
@model_validator(mode='after')
def compute_names(self) -> 'CreditConsumptionResponse':
"""计算枚举名称字段(在验证后执行)"""
from app.services.credit_service import FeatureType, TaskStatus
if not self.feature_type_name:
self.feature_type_name = FeatureType.to_name(self.feature_type)
if not self.task_status_name:
self.task_status_name = TaskStatus.to_name(self.task_status)
return self
修改文件
server/app/api/v1/credits.py- 修改consume_credits端点server/app/schemas/credit.py- 修改CreditConsumptionResponsevalidator
测试结果
✅ 所有 9 个 Credit API 集成测试通过:
tests/integration/test_credit_api.py::TestCreditBalanceAPI::test_get_balance PASSED
tests/integration/test_credit_api.py::TestCreditTransactionAPI::test_get_transactions PASSED
tests/integration/test_credit_api.py::TestCreditTransactionAPI::test_get_transactions_with_filter PASSED
tests/integration/test_credit_api.py::TestCreditConsumptionAPI::test_consume_credits PASSED
tests/integration/test_credit_api.py::TestCreditConsumptionAPI::test_consume_credits_insufficient PASSED
tests/integration/test_credit_api.py::TestCreditConsumptionAPI::test_get_consumption_logs PASSED
tests/integration/test_credit_api.py::TestCreditPackageAPI::test_get_packages PASSED
tests/integration/test_credit_api.py::TestCreditPackageAPI::test_get_package_detail PASSED
tests/integration/test_credit_api.py::TestCreditCalculationAPI::test_calculate_credits PASSED
经验总结
- SQLModel + Pydantic v2 兼容性:在 API 层返回响应时,需要手动处理 UUID 字段
- 最佳实践:对于复杂的响应模型,手动构造字典比使用
from_orm()更可靠 - model_validator 时机:
mode='after'在字段验证后执行,更适合计算派生字段 - 测试驱动:集成测试能够及时发现 ORM 到 Pydantic 的序列化问题