提供基本前后端骨架
This commit is contained in:
28
app/models/__init__.py
Normal file
28
app/models/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""数据库模型"""
|
||||
|
||||
from app.models.user import User
|
||||
from app.models.balance import (
|
||||
UserBalance,
|
||||
BalanceTransaction,
|
||||
TransactionType,
|
||||
TransactionStatus,
|
||||
)
|
||||
from app.models.redeem_code import (
|
||||
RedeemCode,
|
||||
RedeemCodeBatch,
|
||||
RedeemCodeUsageLog,
|
||||
RedeemCodeStatus,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
"UserBalance",
|
||||
"BalanceTransaction",
|
||||
"TransactionType",
|
||||
"TransactionStatus",
|
||||
"RedeemCode",
|
||||
"RedeemCodeBatch",
|
||||
"RedeemCodeUsageLog",
|
||||
"RedeemCodeStatus",
|
||||
]
|
||||
|
||||
320
app/models/balance.py
Normal file
320
app/models/balance.py
Normal file
@@ -0,0 +1,320 @@
|
||||
"""
|
||||
余额与交易模型
|
||||
|
||||
定义用户余额、余额交易记录相关的数据表结构。
|
||||
|
||||
设计说明:
|
||||
- 余额内部以无符号整数存储(单位额度),避免浮点精度问题
|
||||
- 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})>"
|
||||
)
|
||||
|
||||
409
app/models/redeem_code.py
Normal file
409
app/models/redeem_code.py
Normal file
@@ -0,0 +1,409 @@
|
||||
"""
|
||||
兑换码模型
|
||||
|
||||
定义余额兑换码相关的数据表结构。
|
||||
|
||||
设计说明:
|
||||
- 兑换码支持批量生成、导入导出
|
||||
- 记录完整的使用日志
|
||||
- 支持设置有效期和使用限制
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import uuid4
|
||||
import secrets
|
||||
import string
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
Boolean,
|
||||
DateTime,
|
||||
Enum as SQLEnum,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
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)
|
||||
|
||||
|
||||
def generate_redeem_code(length: int = 16) -> str:
|
||||
"""
|
||||
生成兑换码
|
||||
|
||||
格式:大写字母和数字,分段显示(如 XXXX-XXXX-XXXX-XXXX)
|
||||
排除容易混淆的字符:0/O, 1/I/L
|
||||
"""
|
||||
# 可用字符集(排除易混淆字符)
|
||||
alphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
|
||||
code = "".join(secrets.choice(alphabet) for _ in range(length))
|
||||
# 每 4 个字符用连字符分隔
|
||||
return "-".join(code[i:i + 4] for i in range(0, len(code), 4))
|
||||
|
||||
|
||||
class RedeemCodeStatus(str, Enum):
|
||||
"""兑换码状态枚举"""
|
||||
|
||||
ACTIVE = "active" # 可用
|
||||
USED = "used" # 已使用
|
||||
DISABLED = "disabled" # 已禁用
|
||||
EXPIRED = "expired" # 已过期
|
||||
|
||||
|
||||
class RedeemCodeBatch(Base):
|
||||
"""
|
||||
兑换码批次模型
|
||||
|
||||
用于管理批量生成的兑换码
|
||||
"""
|
||||
|
||||
__tablename__ = "redeem_code_batches"
|
||||
|
||||
# 主键
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
primary_key=True,
|
||||
default=generate_uuid,
|
||||
comment="批次唯一标识",
|
||||
)
|
||||
|
||||
# 批次信息
|
||||
name: Mapped[str] = mapped_column(
|
||||
String(128),
|
||||
nullable=False,
|
||||
comment="批次名称",
|
||||
)
|
||||
description: Mapped[str | None] = mapped_column(
|
||||
Text,
|
||||
nullable=True,
|
||||
comment="批次描述",
|
||||
)
|
||||
|
||||
# 面值(单位额度)
|
||||
face_value: Mapped[int] = mapped_column(
|
||||
BigInteger,
|
||||
nullable=False,
|
||||
comment="面值(单位额度,1000 = 1.00)",
|
||||
)
|
||||
|
||||
# 数量统计
|
||||
total_count: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
default=0,
|
||||
nullable=False,
|
||||
comment="生成总数",
|
||||
)
|
||||
used_count: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
default=0,
|
||||
nullable=False,
|
||||
comment="已使用数量",
|
||||
)
|
||||
|
||||
# 创建者
|
||||
created_by: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
comment="创建者 ID",
|
||||
)
|
||||
|
||||
# 时间戳
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=utc_now,
|
||||
server_default=func.now(),
|
||||
nullable=False,
|
||||
comment="创建时间",
|
||||
)
|
||||
|
||||
# 关系
|
||||
codes: Mapped[list["RedeemCode"]] = relationship(
|
||||
"RedeemCode",
|
||||
back_populates="batch",
|
||||
lazy="selectin",
|
||||
)
|
||||
creator: Mapped["User"] = relationship(
|
||||
"User",
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
@property
|
||||
def display_face_value(self) -> str:
|
||||
"""显示面值(2 位小数)"""
|
||||
return f"{self.face_value / 1000:.2f}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<RedeemCodeBatch(id={self.id!r}, name={self.name!r})>"
|
||||
|
||||
|
||||
class RedeemCode(Base):
|
||||
"""
|
||||
兑换码模型
|
||||
|
||||
单个兑换码记录
|
||||
"""
|
||||
|
||||
__tablename__ = "redeem_codes"
|
||||
__table_args__ = (
|
||||
Index("ix_redeem_codes_status_expires", "status", "expires_at"),
|
||||
)
|
||||
|
||||
# 主键
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
primary_key=True,
|
||||
default=generate_uuid,
|
||||
comment="兑换码记录唯一标识",
|
||||
)
|
||||
|
||||
# 兑换码(唯一索引)
|
||||
code: Mapped[str] = mapped_column(
|
||||
String(32),
|
||||
unique=True,
|
||||
nullable=False,
|
||||
index=True,
|
||||
default=generate_redeem_code,
|
||||
comment="兑换码",
|
||||
)
|
||||
|
||||
# 批次关联(可选)
|
||||
batch_id: Mapped[str | None] = mapped_column(
|
||||
String(36),
|
||||
ForeignKey("redeem_code_batches.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment="关联批次 ID",
|
||||
)
|
||||
|
||||
# 面值(单位额度)
|
||||
face_value: Mapped[int] = mapped_column(
|
||||
BigInteger,
|
||||
nullable=False,
|
||||
comment="面值(单位额度,1000 = 1.00)",
|
||||
)
|
||||
|
||||
# 状态
|
||||
status: Mapped[RedeemCodeStatus] = mapped_column(
|
||||
SQLEnum(RedeemCodeStatus),
|
||||
default=RedeemCodeStatus.ACTIVE,
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="兑换码状态",
|
||||
)
|
||||
|
||||
# 使用限制
|
||||
max_uses: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
default=1,
|
||||
nullable=False,
|
||||
comment="最大使用次数",
|
||||
)
|
||||
used_count: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
default=0,
|
||||
nullable=False,
|
||||
comment="已使用次数",
|
||||
)
|
||||
|
||||
# 有效期
|
||||
expires_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=True,
|
||||
comment="过期时间",
|
||||
)
|
||||
|
||||
# 使用信息
|
||||
used_by: Mapped[str | None] = mapped_column(
|
||||
String(36),
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
comment="使用者 ID(最后使用)",
|
||||
)
|
||||
used_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=True,
|
||||
comment="使用时间(最后使用)",
|
||||
)
|
||||
|
||||
# 备注
|
||||
remark: Mapped[str | None] = mapped_column(
|
||||
Text,
|
||||
nullable=True,
|
||||
comment="备注",
|
||||
)
|
||||
|
||||
# 创建者
|
||||
created_by: Mapped[str | None] = mapped_column(
|
||||
String(36),
|
||||
ForeignKey("users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
comment="创建者 ID",
|
||||
)
|
||||
|
||||
# 时间戳
|
||||
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="更新时间",
|
||||
)
|
||||
|
||||
# 关系
|
||||
batch: Mapped["RedeemCodeBatch | None"] = relationship(
|
||||
"RedeemCodeBatch",
|
||||
back_populates="codes",
|
||||
lazy="selectin",
|
||||
)
|
||||
usage_logs: Mapped[list["RedeemCodeUsageLog"]] = relationship(
|
||||
"RedeemCodeUsageLog",
|
||||
back_populates="redeem_code",
|
||||
lazy="selectin",
|
||||
order_by="desc(RedeemCodeUsageLog.created_at)",
|
||||
)
|
||||
|
||||
@property
|
||||
def display_face_value(self) -> str:
|
||||
"""显示面值(2 位小数)"""
|
||||
return f"{self.face_value / 1000:.2f}"
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
"""检查兑换码是否有效"""
|
||||
if self.status != RedeemCodeStatus.ACTIVE:
|
||||
return False
|
||||
if self.used_count >= self.max_uses:
|
||||
return False
|
||||
if self.expires_at and self.expires_at < utc_now():
|
||||
return False
|
||||
return True
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<RedeemCode(code={self.code!r}, status={self.status.value})>"
|
||||
|
||||
|
||||
class RedeemCodeUsageLog(Base):
|
||||
"""
|
||||
兑换码使用日志模型
|
||||
|
||||
记录每次兑换的详细信息
|
||||
"""
|
||||
|
||||
__tablename__ = "redeem_code_usage_logs"
|
||||
__table_args__ = (
|
||||
Index("ix_redeem_usage_user_created", "user_id", "created_at"),
|
||||
)
|
||||
|
||||
# 主键
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
primary_key=True,
|
||||
default=generate_uuid,
|
||||
comment="日志唯一标识",
|
||||
)
|
||||
|
||||
# 关联
|
||||
redeem_code_id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
ForeignKey("redeem_codes.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="关联兑换码 ID",
|
||||
)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="使用者 ID",
|
||||
)
|
||||
transaction_id: Mapped[str | None] = mapped_column(
|
||||
String(36),
|
||||
ForeignKey("balance_transactions.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
comment="关联交易记录 ID",
|
||||
)
|
||||
|
||||
# 兑换信息快照
|
||||
code_snapshot: Mapped[str] = mapped_column(
|
||||
String(32),
|
||||
nullable=False,
|
||||
comment="兑换码快照",
|
||||
)
|
||||
face_value: Mapped[int] = mapped_column(
|
||||
BigInteger,
|
||||
nullable=False,
|
||||
comment="面值(单位额度)",
|
||||
)
|
||||
|
||||
# 客户端信息
|
||||
ip_address: Mapped[str | None] = mapped_column(
|
||||
String(64),
|
||||
nullable=True,
|
||||
comment="客户端 IP",
|
||||
)
|
||||
user_agent: Mapped[str | None] = mapped_column(
|
||||
String(512),
|
||||
nullable=True,
|
||||
comment="User Agent",
|
||||
)
|
||||
|
||||
# 时间戳
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=utc_now,
|
||||
server_default=func.now(),
|
||||
nullable=False,
|
||||
comment="使用时间",
|
||||
)
|
||||
|
||||
# 关系
|
||||
redeem_code: Mapped["RedeemCode"] = relationship(
|
||||
"RedeemCode",
|
||||
back_populates="usage_logs",
|
||||
lazy="selectin",
|
||||
)
|
||||
user: Mapped["User"] = relationship(
|
||||
"User",
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
@property
|
||||
def display_face_value(self) -> str:
|
||||
"""显示面值(2 位小数)"""
|
||||
return f"{self.face_value / 1000:.2f}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<RedeemCodeUsageLog(code={self.code_snapshot!r}, "
|
||||
f"user_id={self.user_id!r})>"
|
||||
)
|
||||
|
||||
141
app/models/user.py
Normal file
141
app/models/user.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
用户模型
|
||||
|
||||
定义用户数据表结构。
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.balance import UserBalance
|
||||
|
||||
|
||||
def generate_uuid() -> str:
|
||||
"""生成 UUID 字符串"""
|
||||
return str(uuid4())
|
||||
|
||||
|
||||
def utc_now() -> datetime:
|
||||
"""获取当前 UTC 时间"""
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""用户模型"""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
# 主键:使用 UUID 字符串
|
||||
id: Mapped[str] = mapped_column(
|
||||
String(36),
|
||||
primary_key=True,
|
||||
default=generate_uuid,
|
||||
comment="用户唯一标识",
|
||||
)
|
||||
|
||||
# 账户信息
|
||||
username: Mapped[str] = mapped_column(
|
||||
String(32),
|
||||
unique=True,
|
||||
index=True,
|
||||
nullable=False,
|
||||
comment="用户名",
|
||||
)
|
||||
email: Mapped[str | None] = mapped_column(
|
||||
String(255),
|
||||
unique=True,
|
||||
index=True,
|
||||
nullable=True,
|
||||
comment="邮箱地址",
|
||||
)
|
||||
hashed_password: Mapped[str | None] = mapped_column(
|
||||
String(255),
|
||||
nullable=True, # OAuth2 用户可能没有密码
|
||||
comment="密码哈希",
|
||||
)
|
||||
|
||||
# OAuth2 关联信息
|
||||
oauth_provider: Mapped[str | None] = mapped_column(
|
||||
String(32),
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment="OAuth2 提供商(如 linuxdo)",
|
||||
)
|
||||
oauth_user_id: Mapped[str | None] = mapped_column(
|
||||
String(128),
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment="OAuth2 用户 ID",
|
||||
)
|
||||
|
||||
# 用户状态
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean,
|
||||
default=True,
|
||||
nullable=False,
|
||||
comment="是否激活",
|
||||
)
|
||||
is_superuser: Mapped[bool] = mapped_column(
|
||||
Boolean,
|
||||
default=False,
|
||||
nullable=False,
|
||||
comment="是否为超级管理员",
|
||||
)
|
||||
|
||||
# 个人信息
|
||||
nickname: Mapped[str | None] = mapped_column(
|
||||
String(64),
|
||||
nullable=True,
|
||||
comment="昵称",
|
||||
)
|
||||
avatar_url: Mapped[str | None] = mapped_column(
|
||||
String(512),
|
||||
nullable=True,
|
||||
comment="头像 URL",
|
||||
)
|
||||
bio: Mapped[str | None] = mapped_column(
|
||||
Text,
|
||||
nullable=True,
|
||||
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="更新时间",
|
||||
)
|
||||
last_login_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=True,
|
||||
comment="最后登录时间",
|
||||
)
|
||||
|
||||
# 关系
|
||||
balance_account: Mapped["UserBalance | None"] = relationship(
|
||||
"UserBalance",
|
||||
back_populates="user",
|
||||
uselist=False,
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User(id={self.id!r}, username={self.username!r})>"
|
||||
|
||||
Reference in New Issue
Block a user