286 lines
7.6 KiB
Python
286 lines
7.6 KiB
Python
"""
|
||
余额相关 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
|
||
|