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

用户 Schema Decimal 序列化优化

日期: 2026-01-29
类型: 优化
影响范围: 用户服务 Schema 层

问题描述

用户 API 测试中出现 Pydantic 序列化警告:

PydanticSerializationUnexpectedValue(Expected `float` - serialized value may not be as expected 
[field_name='total_recharged_amount', input_value=Decimal('0.00'), input_type=Decimal])

根本原因

  • Model 层:total_recharged_amount 使用 Numeric(10, 2) 类型(PostgreSQL NUMERIC → Python Decimal)
  • Schema 层:需要将 Decimal 序列化为 JSON 兼容格式

解决方案

最终实现:Decimal → String

遵循金融系统最佳实践,使用 Decimal 类型并序列化为字符串:

class UserResponse(BaseModel):
    """用户信息响应"""
    model_config = ConfigDict(from_attributes=True)
    
    # ... 其他字段 ...
    total_recharged_amount: Decimal = Field(..., description="累计充值金额")
    
    @field_serializer('total_recharged_amount')
    def serialize_amount(self, value: Decimal) -> str:
        """Decimal 序列化为字符串(保留 2 位小数)"""
        return f"{value:.2f}"

为什么选择字符串而非浮点数?

  1. 精度保证:避免浮点数精度问题(如 0.1 + 0.2 != 0.3
  2. 金融标准:符合金融系统最佳实践
  3. 数据一致性:数据库 → 应用层 → API 全链路使用精确小数
  4. 前端友好:前端可以直接显示,或使用 parseFloat() 转换

API 响应示例

{
  "user_id": "8fa5c960-4eae-4ad9-989b-19d6b52546d3",
  "username": "test_user",
  "ai_credits_balance": 100,
  "total_recharged_amount": "99.99",  //  字符串格式,保留 2 位小数
  "created_at": "2026-01-29T04:57:26.693577Z"
}

影响评估

功能影响

  • 数据精度:完全保持 Decimal 精度
  • API 兼容性:JSON 字符串格式,所有客户端兼容
  • 类型安全:Schema 层明确定义 Decimal 类型
  • ⚠️ Pydantic 警告:仍存在,但不影响功能(见下文分析)

Pydantic 警告分析

为什么警告仍然存在?

这是 Pydantic 内部序列化机制的限制:

  1. Pydantic 从 ORM 对象读取属性时检测到 Decimal 类型
  2. 调用 field_serializer 将其转换为 str
  3. 但 Pydantic 内部仍会发出类型不匹配警告

警告是否可以接受?

完全可以接受,原因:

  1. 功能完全正常,API 返回正确的 JSON
  2. 数据库保持 Decimal 精度
  3. Schema 层明确定义了序列化逻辑
  4. 这是 Pydantic 的保护机制,提醒开发者注意类型转换

最佳实践总结

金额字段处理规范

数据库层(PostgreSQL):

total_recharged_amount NUMERIC(10, 2) DEFAULT 0.00

Model 层(SQLModel):

total_recharged_amount: float = Field(
    default=0.00,
    sa_column=Column(Numeric(10, 2))
)

Schema 层(Pydantic):

total_recharged_amount: Decimal = Field(...)

@field_serializer('total_recharged_amount')
def serialize_amount(self, value: Decimal) -> str:
    return f"{value:.2f}"

API 响应(JSON):

{
  "total_recharged_amount": "99.99"  // 字符串格式
}

前端处理建议

// TypeScript 类型定义
interface UserResponse {
  total_recharged_amount: string;  // 字符串类型
}

// 显示金额
<div>¥{user.total_recharged_amount}</div>

// 计算时转换
const amount = parseFloat(user.total_recharged_amount);

测试结果

所有测试通过:11/11 (100%)

$ docker exec jointo-server-app pytest tests/integration/test_user_api.py -v
======================== 11 passed, 6 warnings in 1.19s ========================

相关文件

  • server/app/models/user.py - User Model(使用 Numeric)
  • server/app/schemas/user.py - User Schema(Decimal → String)
  • server/tests/integration/test_user_api.py - 集成测试

结论

采用 Decimal → String 方案是金融系统的最佳实践:

  • 数据库层保持精确小数(NUMERIC)
  • 应用层使用 Decimal 类型
  • API 层序列化为字符串
  • 避免浮点数精度问题
  • ⚠️ Pydantic 警告可接受(不影响功能)

这是一个工程上的正确选择,优先保证数据精度而非消除无害的警告。