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

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 的兼容性问题:

  1. SQLModel 的 UUID 字段在 ORM 对象中是 uuid.UUID 类型
  2. Pydantic v2 在验证时对 UUID 类型有严格的类型检查
  3. 使用 from_orm()model_validate() 时,UUID 对象无法正确转换
  4. 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_validatormode='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 - 修改 CreditConsumptionResponse validator

测试结果

所有 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

经验总结

  1. SQLModel + Pydantic v2 兼容性:在 API 层返回响应时,需要手动处理 UUID 字段
  2. 最佳实践:对于复杂的响应模型,手动构造字典比使用 from_orm() 更可靠
  3. model_validator 时机mode='after' 在字段验证后执行,更适合计算派生字段
  4. 测试驱动:集成测试能够及时发现 ORM 到 Pydantic 的序列化问题

相关文档