""" 余额相关 Schema 定义余额数据的验证和序列化规则。 设计说明: - 内部存储使用整数单位(units),外部显示使用小数(display) - 1.00 显示余额 = 1000 单位额度 """ from datetime import datetime from typing import Annotated from pydantic import Field, field_validator, computed_field from app.schemas.base import BaseSchema, PaginatedResponse from app.models.balance import TransactionType, TransactionStatus # ============================================================ # 余额单位转换工具 # ============================================================ UNITS_PER_DISPLAY = 1000 # 1.00 显示余额 = 1000 单位额度 def display_to_units(display_amount: float) -> int: """将显示金额转换为单位额度""" return int(round(display_amount * UNITS_PER_DISPLAY)) def units_to_display(units: int) -> float: """将单位额度转换为显示金额""" return units / UNITS_PER_DISPLAY def format_display(units: int) -> str: """格式化显示金额(2 位小数)""" return f"{units / UNITS_PER_DISPLAY:.2f}" # ============================================================ # 余额账户 Schema # ============================================================ class BalanceResponse(BaseSchema): """余额信息响应""" user_id: str balance_units: Annotated[ int, Field(description="当前余额(单位额度)"), ] frozen_units: Annotated[ int, Field(description="冻结余额(单位额度)"), ] total_recharged_units: Annotated[ int, Field(description="累计充值(单位额度)"), ] total_consumed_units: Annotated[ int, Field(description="累计消费(单位额度)"), ] @computed_field @property def balance(self) -> str: """显示余额(2 位小数)""" return format_display(self.balance_units) @computed_field @property def available_balance(self) -> str: """显示可用余额(2 位小数)""" return format_display(self.balance_units - self.frozen_units) @computed_field @property def frozen_balance(self) -> str: """显示冻结余额(2 位小数)""" return format_display(self.frozen_units) @computed_field @property def total_recharged(self) -> str: """显示累计充值(2 位小数)""" return format_display(self.total_recharged_units) @computed_field @property def total_consumed(self) -> str: """显示累计消费(2 位小数)""" return format_display(self.total_consumed_units) class BalanceSummaryResponse(BaseSchema): """余额简要信息响应(用于嵌入用户信息)""" balance: str = Field(description="当前余额") available_balance: str = Field(description="可用余额") # ============================================================ # 交易记录 Schema # ============================================================ class TransactionResponse(BaseSchema): """交易记录响应""" id: str transaction_type: TransactionType status: TransactionStatus amount_units: Annotated[ int, Field(description="交易金额(单位额度,正数收入,负数支出)"), ] balance_before_units: Annotated[ int, Field(description="交易前余额(单位额度)"), ] balance_after_units: Annotated[ int, Field(description="交易后余额(单位额度)"), ] reference_type: str | None reference_id: str | None description: str | None created_at: datetime @computed_field @property def amount(self) -> str: """显示交易金额(带符号,2 位小数)""" return f"{self.amount_units / UNITS_PER_DISPLAY:+.2f}" @computed_field @property def balance_before(self) -> str: """显示交易前余额(2 位小数)""" return format_display(self.balance_before_units) @computed_field @property def balance_after(self) -> str: """显示交易后余额(2 位小数)""" return format_display(self.balance_after_units) class TransactionListResponse(PaginatedResponse[TransactionResponse]): """交易记录列表响应""" pass # ============================================================ # 扣款请求 Schema # ============================================================ class DeductionRequest(BaseSchema): """扣款请求""" amount: Annotated[ float, Field( gt=0, description="扣款金额(显示金额,如 1.00)", examples=[1.00, 0.50], ), ] reference_type: Annotated[ str | None, Field( default=None, max_length=64, description="关联业务类型", examples=["api_call", "service_fee"], ), ] reference_id: Annotated[ str | None, Field( default=None, max_length=64, description="关联业务 ID", ), ] description: Annotated[ str | None, Field( default=None, max_length=255, description="交易描述", ), ] idempotency_key: Annotated[ str | None, Field( default=None, max_length=64, description="幂等键(防止重复扣款)", ), ] @field_validator("amount") @classmethod def validate_amount(cls, v: float) -> float: """验证金额精度""" # 最小精度 0.001(即 1 单位额度) if round(v, 3) != v: raise ValueError("金额精度不能超过 3 位小数") return v @property def amount_units(self) -> int: """转换为单位额度""" return display_to_units(self.amount) class DeductionResponse(BaseSchema): """扣款响应""" transaction_id: str amount: str = Field(description="扣款金额") balance_before: str = Field(description="扣款前余额") balance_after: str = Field(description="扣款后余额") # ============================================================ # 管理员操作 Schema # ============================================================ class AdminAdjustmentRequest(BaseSchema): """管理员余额调整请求""" user_id: Annotated[ str, Field(description="目标用户 ID"), ] amount: Annotated[ float, Field( description="调整金额(正数增加,负数减少)", examples=[10.00, -5.00], ), ] reason: Annotated[ str, Field( min_length=1, max_length=255, description="调整原因", ), ] @field_validator("amount") @classmethod def validate_amount(cls, v: float) -> float: """验证金额精度""" if round(v, 3) != v: raise ValueError("金额精度不能超过 3 位小数") if v == 0: raise ValueError("调整金额不能为 0") return v @property def amount_units(self) -> int: """转换为单位额度""" return display_to_units(self.amount) class AdminBalanceResponse(BaseSchema): """管理员查看的余额信息(包含更多细节)""" user_id: str username: str balance: str available_balance: str frozen_balance: str total_recharged: str total_consumed: str version: int created_at: datetime updated_at: datetime