""" 余额与交易模型 定义用户余额、余额交易记录相关的数据表结构。 设计说明: - 余额内部以无符号整数存储(单位额度),避免浮点精度问题 - 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"" 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"" )