321 lines
8.2 KiB
Python
321 lines
8.2 KiB
Python
"""
|
||
余额与交易模型
|
||
|
||
定义用户余额、余额交易记录相关的数据表结构。
|
||
|
||
设计说明:
|
||
- 余额内部以无符号整数存储(单位额度),避免浮点精度问题
|
||
- 1.00 显示余额 = 1000 单位额度(精度 0.001)
|
||
- 所有金额操作都在单位额度层面进行,只在展示时转换
|
||
"""
|
||
|
||
from datetime import datetime, timezone
|
||
from enum import Enum
|
||
from typing import TYPE_CHECKING
|
||
from uuid import uuid4
|
||
|
||
from sqlalchemy import (
|
||
BigInteger,
|
||
DateTime,
|
||
Enum as SQLEnum,
|
||
ForeignKey,
|
||
Index,
|
||
String,
|
||
Text,
|
||
func,
|
||
)
|
||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||
|
||
from app.database import Base
|
||
|
||
if TYPE_CHECKING:
|
||
from app.models.user import User
|
||
|
||
|
||
def generate_uuid() -> str:
|
||
"""生成 UUID 字符串"""
|
||
return str(uuid4())
|
||
|
||
|
||
def utc_now() -> datetime:
|
||
"""获取当前 UTC 时间"""
|
||
return datetime.now(timezone.utc)
|
||
|
||
|
||
class TransactionType(str, Enum):
|
||
"""交易类型枚举"""
|
||
|
||
RECHARGE = "recharge" # 充值(兑换码等)
|
||
DEDUCTION = "deduction" # 扣款(API 调用等)
|
||
REFUND = "refund" # 退款
|
||
ADJUSTMENT = "adjustment" # 管理员调整
|
||
TRANSFER_IN = "transfer_in" # 转入
|
||
TRANSFER_OUT = "transfer_out" # 转出
|
||
|
||
|
||
class TransactionStatus(str, Enum):
|
||
"""交易状态枚举"""
|
||
|
||
PENDING = "pending" # 待处理
|
||
COMPLETED = "completed" # 已完成
|
||
FAILED = "failed" # 失败
|
||
CANCELLED = "cancelled" # 已取消
|
||
|
||
|
||
class UserBalance(Base):
|
||
"""
|
||
用户余额模型
|
||
|
||
独立的余额表,便于扩展(如多币种、账户类型)和锁管理
|
||
"""
|
||
|
||
__tablename__ = "user_balances"
|
||
|
||
# 主键
|
||
id: Mapped[str] = mapped_column(
|
||
String(36),
|
||
primary_key=True,
|
||
default=generate_uuid,
|
||
comment="余额记录唯一标识",
|
||
)
|
||
|
||
# 关联用户(一对一)
|
||
user_id: Mapped[str] = mapped_column(
|
||
String(36),
|
||
ForeignKey("users.id", ondelete="CASCADE"),
|
||
unique=True,
|
||
nullable=False,
|
||
index=True,
|
||
comment="关联用户 ID",
|
||
)
|
||
|
||
# 余额信息(内部以整数单位存储)
|
||
balance: Mapped[int] = mapped_column(
|
||
BigInteger,
|
||
default=0,
|
||
nullable=False,
|
||
comment="当前余额(单位额度,1000 = 1.00 显示余额)",
|
||
)
|
||
|
||
# 冻结金额(用于处理中的交易)
|
||
frozen_balance: Mapped[int] = mapped_column(
|
||
BigInteger,
|
||
default=0,
|
||
nullable=False,
|
||
comment="冻结余额(单位额度)",
|
||
)
|
||
|
||
# 累计统计
|
||
total_recharged: Mapped[int] = mapped_column(
|
||
BigInteger,
|
||
default=0,
|
||
nullable=False,
|
||
comment="累计充值(单位额度)",
|
||
)
|
||
total_consumed: Mapped[int] = mapped_column(
|
||
BigInteger,
|
||
default=0,
|
||
nullable=False,
|
||
comment="累计消费(单位额度)",
|
||
)
|
||
|
||
# 乐观锁版本号
|
||
version: Mapped[int] = mapped_column(
|
||
BigInteger,
|
||
default=0,
|
||
nullable=False,
|
||
comment="版本号(乐观锁)",
|
||
)
|
||
|
||
# 时间戳
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True),
|
||
default=utc_now,
|
||
server_default=func.now(),
|
||
nullable=False,
|
||
comment="创建时间",
|
||
)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True),
|
||
default=utc_now,
|
||
onupdate=utc_now,
|
||
server_default=func.now(),
|
||
nullable=False,
|
||
comment="更新时间",
|
||
)
|
||
|
||
# 关系
|
||
user: Mapped["User"] = relationship(
|
||
"User",
|
||
back_populates="balance_account",
|
||
lazy="selectin",
|
||
)
|
||
transactions: Mapped[list["BalanceTransaction"]] = relationship(
|
||
"BalanceTransaction",
|
||
back_populates="balance_account",
|
||
lazy="selectin",
|
||
order_by="desc(BalanceTransaction.created_at)",
|
||
)
|
||
|
||
@property
|
||
def available_balance(self) -> int:
|
||
"""可用余额(总余额 - 冻结余额)"""
|
||
return self.balance - self.frozen_balance
|
||
|
||
@property
|
||
def display_balance(self) -> str:
|
||
"""显示余额(2 位小数)"""
|
||
return f"{self.balance / 1000:.2f}"
|
||
|
||
@property
|
||
def display_available_balance(self) -> str:
|
||
"""显示可用余额(2 位小数)"""
|
||
return f"{self.available_balance / 1000:.2f}"
|
||
|
||
def __repr__(self) -> str:
|
||
return f"<UserBalance(user_id={self.user_id!r}, balance={self.display_balance})>"
|
||
|
||
|
||
class BalanceTransaction(Base):
|
||
"""
|
||
余额交易记录模型
|
||
|
||
记录所有余额变动,用于审计和对账
|
||
"""
|
||
|
||
__tablename__ = "balance_transactions"
|
||
__table_args__ = (
|
||
Index("ix_balance_transactions_user_created", "user_id", "created_at"),
|
||
Index("ix_balance_transactions_type_status", "transaction_type", "status"),
|
||
)
|
||
|
||
# 主键
|
||
id: Mapped[str] = mapped_column(
|
||
String(36),
|
||
primary_key=True,
|
||
default=generate_uuid,
|
||
comment="交易记录唯一标识",
|
||
)
|
||
|
||
# 关联
|
||
user_id: Mapped[str] = mapped_column(
|
||
String(36),
|
||
ForeignKey("users.id", ondelete="CASCADE"),
|
||
nullable=False,
|
||
index=True,
|
||
comment="关联用户 ID",
|
||
)
|
||
balance_account_id: Mapped[str] = mapped_column(
|
||
String(36),
|
||
ForeignKey("user_balances.id", ondelete="CASCADE"),
|
||
nullable=False,
|
||
index=True,
|
||
comment="关联余额账户 ID",
|
||
)
|
||
|
||
# 交易信息
|
||
transaction_type: Mapped[TransactionType] = mapped_column(
|
||
SQLEnum(TransactionType),
|
||
nullable=False,
|
||
comment="交易类型",
|
||
)
|
||
status: Mapped[TransactionStatus] = mapped_column(
|
||
SQLEnum(TransactionStatus),
|
||
default=TransactionStatus.COMPLETED,
|
||
nullable=False,
|
||
comment="交易状态",
|
||
)
|
||
|
||
# 金额(整数单位)
|
||
amount: Mapped[int] = mapped_column(
|
||
BigInteger,
|
||
nullable=False,
|
||
comment="交易金额(单位额度,正数表示收入,负数表示支出)",
|
||
)
|
||
balance_before: Mapped[int] = mapped_column(
|
||
BigInteger,
|
||
nullable=False,
|
||
comment="交易前余额(单位额度)",
|
||
)
|
||
balance_after: Mapped[int] = mapped_column(
|
||
BigInteger,
|
||
nullable=False,
|
||
comment="交易后余额(单位额度)",
|
||
)
|
||
|
||
# 业务关联
|
||
reference_type: Mapped[str | None] = mapped_column(
|
||
String(64),
|
||
nullable=True,
|
||
index=True,
|
||
comment="关联业务类型(如 redeem_code、api_call)",
|
||
)
|
||
reference_id: Mapped[str | None] = mapped_column(
|
||
String(64),
|
||
nullable=True,
|
||
index=True,
|
||
comment="关联业务 ID",
|
||
)
|
||
|
||
# 描述
|
||
description: Mapped[str | None] = mapped_column(
|
||
String(255),
|
||
nullable=True,
|
||
comment="交易描述",
|
||
)
|
||
remark: Mapped[str | None] = mapped_column(
|
||
Text,
|
||
nullable=True,
|
||
comment="备注(内部使用)",
|
||
)
|
||
|
||
# 操作人(管理员调整时记录)
|
||
operator_id: Mapped[str | None] = mapped_column(
|
||
String(36),
|
||
ForeignKey("users.id", ondelete="SET NULL"),
|
||
nullable=True,
|
||
comment="操作人 ID(管理员调整时)",
|
||
)
|
||
|
||
# 幂等键(防止重复提交)
|
||
idempotency_key: Mapped[str | None] = mapped_column(
|
||
String(64),
|
||
unique=True,
|
||
nullable=True,
|
||
comment="幂等键",
|
||
)
|
||
|
||
# 时间戳
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True),
|
||
default=utc_now,
|
||
server_default=func.now(),
|
||
nullable=False,
|
||
comment="创建时间",
|
||
)
|
||
|
||
# 关系
|
||
user: Mapped["User"] = relationship(
|
||
"User",
|
||
foreign_keys=[user_id],
|
||
lazy="selectin",
|
||
)
|
||
balance_account: Mapped["UserBalance"] = relationship(
|
||
"UserBalance",
|
||
back_populates="transactions",
|
||
lazy="selectin",
|
||
)
|
||
|
||
@property
|
||
def display_amount(self) -> str:
|
||
"""显示金额(带符号,2 位小数)"""
|
||
return f"{self.amount / 1000:+.2f}"
|
||
|
||
def __repr__(self) -> str:
|
||
return (
|
||
f"<BalanceTransaction(id={self.id!r}, "
|
||
f"type={self.transaction_type.value}, "
|
||
f"amount={self.display_amount})>"
|
||
)
|
||
|