Files
SatoNano/app/schemas/balance.py
2026-01-06 23:49:23 +08:00

286 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
余额相关 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