mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(wallet): 钱包系统替代配额系统,新增支付与退款机制
- 新增钱包余额管理、充值、扣费、退款完整流程 - 新增支付网关抽象层(支持手动/支付宝/微信) - 用量计费从配额系统迁移到钱包余额扣费 - 新增管理员钱包管理与支付订单管理页面 - 新增用户钱包中心页面 - 移除独立 Key 锁定机制,统一由钱包余额控制 - 新增相关 API 路由、序列化器与数据库迁移 - 新增钱包、支付、退款相关测试
This commit is contained in:
@@ -689,7 +689,7 @@ class UpdateUserRequest(BaseModel):
|
||||
password: str | None = Field(
|
||||
None, min_length=6, max_length=128, description="新密码(留空保持不变)"
|
||||
)
|
||||
quota_usd: float | None = Field(None, ge=0)
|
||||
unlimited: bool | None = Field(None, description="是否无限制(true=无限制,false=有限制)")
|
||||
is_active: bool | None = None
|
||||
role: str | None = None
|
||||
allowed_providers: list[str] | None = Field(None, description="允许使用的提供商 ID 列表")
|
||||
|
||||
@@ -241,8 +241,10 @@ class CreateUserRequest(BaseModel):
|
||||
password: str = Field(..., min_length=6, max_length=128, description="密码")
|
||||
email: str | None = Field(None, max_length=255, description="邮箱地址(可选)")
|
||||
role: UserRole | None = Field(UserRole.USER, description="用户角色")
|
||||
quota_usd: float | None = Field(default=None, description="USD配额,null表示使用系统默认配额")
|
||||
unlimited: bool = Field(default=False, description="是否无限配额")
|
||||
initial_gift_usd: float | None = Field(
|
||||
default=None, description="初始赠款(USD),null 表示使用系统默认初始赠款"
|
||||
)
|
||||
unlimited: bool = Field(default=False, description="是否无限制")
|
||||
# 访问限制字段
|
||||
allowed_providers: list[str] | None = Field(
|
||||
default=None, description="允许使用的提供商ID列表,null表示无限制"
|
||||
@@ -254,16 +256,16 @@ class CreateUserRequest(BaseModel):
|
||||
default=None, description="允许使用的模型名称列表,null表示无限制"
|
||||
)
|
||||
|
||||
@field_validator("quota_usd", mode="before")
|
||||
@field_validator("initial_gift_usd", mode="before")
|
||||
@classmethod
|
||||
def validate_quota_usd(cls, v: Any) -> Any:
|
||||
"""验证配额值,null表示使用系统默认配额"""
|
||||
def validate_initial_gift_usd(cls, v: Any) -> Any:
|
||||
"""验证初始赠款金额,null 表示使用系统默认初始赠款。"""
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, (int, float)) and v >= 0 and v <= 10000:
|
||||
return float(v)
|
||||
if isinstance(v, (int, float)):
|
||||
raise ValueError("配额必须在 0-10000 范围内")
|
||||
raise ValueError("初始赠款必须在 0-10000 范围内")
|
||||
return v
|
||||
|
||||
@field_validator("email")
|
||||
@@ -337,10 +339,10 @@ class UpdateUserRequest(BaseModel):
|
||||
username: str | None = None
|
||||
password: str | None = None
|
||||
role: UserRole | None = None
|
||||
unlimited: bool | None = None
|
||||
allowed_providers: list[str] | None = None # 允许使用的提供商 ID 列表
|
||||
allowed_api_formats: list[str] | None = None # 允许使用的 API 格式列表
|
||||
allowed_models: list[str] | None = None # 允许使用的模型名称列表
|
||||
quota_usd: float | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
@field_validator("allowed_api_formats")
|
||||
@@ -349,18 +351,6 @@ class UpdateUserRequest(BaseModel):
|
||||
# 与 CreateUserRequest 保持一致
|
||||
return CreateUserRequest.validate_allowed_api_formats(v)
|
||||
|
||||
@field_validator("quota_usd", mode="before")
|
||||
@classmethod
|
||||
def validate_quota_usd(cls, v: Any) -> Any:
|
||||
"""验证配额值,允许null表示无限制"""
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, (int, float)) and v >= 0 and v <= 10000:
|
||||
return float(v)
|
||||
if isinstance(v, (int, float)):
|
||||
raise ValueError("配额必须在 0-10000 范围内")
|
||||
return v
|
||||
|
||||
|
||||
class CreateApiKeyRequest(BaseModel):
|
||||
"""创建API密钥请求"""
|
||||
@@ -370,11 +360,14 @@ class CreateApiKeyRequest(BaseModel):
|
||||
allowed_api_formats: list[str] | None = None # 允许使用的 API 格式列表
|
||||
allowed_models: list[str] | None = None # 允许使用的模型名称列表
|
||||
rate_limit: int | None = None # None = 无限制
|
||||
expire_days: int | None = None # None = 永不过期,数字 = 多少天后过期(兼容旧版)
|
||||
expire_days: int | None = None # None = 永不过期,数字 = 多少天后过期
|
||||
expires_at: str | None = None # ISO 日期字符串,如 "2025-12-31",优先于 expire_days
|
||||
initial_balance_usd: float | None = Field(
|
||||
None, description="初始余额(USD),仅用于独立Key,None = 无限制"
|
||||
)
|
||||
unlimited_balance: bool | None = Field(
|
||||
None, description="是否无限余额(编辑独立Key时用于切换额度模式)"
|
||||
)
|
||||
is_standalone: bool = Field(False, description="是否为独立余额Key(给非注册用户使用)")
|
||||
auto_delete_on_expiry: bool = Field(
|
||||
False, description="过期后是否自动删除(True=物理删除,False=仅禁用)"
|
||||
@@ -397,8 +390,7 @@ class UserResponse(BaseModel):
|
||||
allowed_providers: list[str] | None = None # 允许使用的提供商 ID 列表
|
||||
allowed_api_formats: list[str] | None = None # 允许使用的 API 格式列表
|
||||
allowed_models: list[str] | None = None # 允许使用的模型名称列表
|
||||
quota_usd: float
|
||||
used_usd: float
|
||||
unlimited: bool = False
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
@@ -421,8 +413,6 @@ class ApiKeyResponse(BaseModel):
|
||||
rate_limit: int
|
||||
is_active: bool
|
||||
expires_at: datetime | None = None
|
||||
balance_used_usd: float = 0.0
|
||||
current_balance_usd: float | None = None # NULL = 无限制
|
||||
is_standalone: bool = False
|
||||
force_capabilities: dict[str, bool] | None = None # 强制开启的能力
|
||||
created_at: datetime
|
||||
|
||||
@@ -25,6 +25,7 @@ from sqlalchemy import (
|
||||
Index,
|
||||
Integer,
|
||||
LargeBinary,
|
||||
Numeric,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
@@ -71,13 +72,13 @@ class User(Base):
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
# OAuth 用户可能没有邮箱;Postgres unique 允许多个 NULL
|
||||
email = Column(String(255), unique=True, index=True, nullable=True)
|
||||
# 注意:所有创建用户的入口必须显式写入 true/false,禁止依赖默认值
|
||||
email_verified = Column(Boolean, nullable=False)
|
||||
username = Column(String(100), unique=True, index=True, nullable=False)
|
||||
# OAuth 用户可能没有本地密码(v1 仅做字段兼容)
|
||||
# OAuth 用户可能没有本地密码
|
||||
password_hash = Column(String(255), nullable=True)
|
||||
role = Column(
|
||||
Enum(
|
||||
@@ -113,11 +114,6 @@ class User(Base):
|
||||
model_capability_settings = Column(JSON, nullable=True) # 用户针对特定模型的能力配置
|
||||
# 示例: {"claude-sonnet-4-20250514": {"cache_1h": true}}
|
||||
|
||||
# 配额管理
|
||||
quota_usd = Column(Float, nullable=True) # 美元配额(NULL 表示无限制)
|
||||
used_usd = Column(Float, default=0.0) # 当前周期已使用美元
|
||||
total_usd = Column(Float, default=0.0) # 累积消费总额
|
||||
|
||||
# 状态
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
is_deleted = Column(Boolean, default=False, nullable=False)
|
||||
@@ -142,9 +138,6 @@ class User(Base):
|
||||
preferences = relationship(
|
||||
"UserPreference", back_populates="user", cascade="all, delete-orphan", passive_deletes=True
|
||||
)
|
||||
quotas = relationship(
|
||||
"UserQuota", back_populates="user", cascade="all, delete-orphan", passive_deletes=True
|
||||
)
|
||||
announcement_reads = relationship(
|
||||
"AnnouncementRead",
|
||||
back_populates="user",
|
||||
@@ -154,6 +147,14 @@ class User(Base):
|
||||
|
||||
# 关系 - SET NULL: 保留历史记录,让数据库处理 SET NULL
|
||||
usage_records = relationship("Usage", back_populates="user", passive_deletes=True)
|
||||
wallet = relationship("Wallet", back_populates="user", uselist=False, passive_deletes=True)
|
||||
payment_orders = relationship("PaymentOrder", back_populates="user", passive_deletes=True)
|
||||
refund_requests = relationship(
|
||||
"RefundRequest",
|
||||
back_populates="user",
|
||||
passive_deletes=True,
|
||||
foreign_keys="RefundRequest.user_id",
|
||||
)
|
||||
authored_announcements = relationship(
|
||||
"Announcement",
|
||||
back_populates="author",
|
||||
@@ -179,8 +180,14 @@ class ApiKey(Base):
|
||||
"""API密钥模型"""
|
||||
|
||||
__tablename__ = "api_keys"
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"(NOT is_standalone) OR (NOT is_locked)",
|
||||
name="ck_api_keys_standalone_not_locked",
|
||||
),
|
||||
)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
key_hash = Column(String(64), unique=True, index=True, nullable=False) # API密钥的SHA256哈希
|
||||
key_encrypted = Column(Text, nullable=True) # 加密后的完整密钥,用于查看
|
||||
@@ -190,9 +197,7 @@ class ApiKey(Base):
|
||||
total_requests = Column(Integer, default=0)
|
||||
total_cost_usd = Column(Float, default=0.0)
|
||||
|
||||
# 余额管理(仅用于独立余额 Key)
|
||||
balance_used_usd = Column(Float, default=0.0) # 已使用余额(USD),用于统计
|
||||
current_balance_usd = Column(Float, nullable=True) # 当前余额(USD),NULL 表示无限制
|
||||
# 钱包体系:余额/额度由 wallets 表统一管理
|
||||
is_standalone = Column(
|
||||
Boolean, default=False, nullable=False
|
||||
) # 是否为独立余额 Key(给非注册用户使用)
|
||||
@@ -210,7 +215,7 @@ class ApiKey(Base):
|
||||
|
||||
# 状态
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
is_locked = Column(Boolean, default=False, nullable=False) # 管理员锁定,用户无法使用/操作
|
||||
is_locked = Column(Boolean, default=False, nullable=False) # 仅普通用户Key可锁定
|
||||
last_used_at = Column(DateTime(timezone=True), nullable=True)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=True) # 过期时间
|
||||
auto_delete_on_expiry = Column(Boolean, default=False, nullable=False) # 过期后是否自动删除
|
||||
@@ -229,6 +234,7 @@ class ApiKey(Base):
|
||||
# 关系
|
||||
user = relationship("User", back_populates="api_keys")
|
||||
usage_records = relationship("Usage", back_populates="api_key")
|
||||
wallet = relationship("Wallet", back_populates="api_key", uselist=False, passive_deletes=True)
|
||||
provider_mappings = relationship(
|
||||
"ApiKeyProviderMapping", back_populates="api_key", cascade="all, delete-orphan"
|
||||
)
|
||||
@@ -310,9 +316,10 @@ class Usage(Base):
|
||||
Index("idx_usage_provider_key", "provider_id", "provider_api_key_id"),
|
||||
)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
api_key_id = Column(String(36), ForeignKey("api_keys.id", ondelete="SET NULL"), nullable=True)
|
||||
wallet_id = Column(String(36), ForeignKey("wallets.id", ondelete="SET NULL"), nullable=True)
|
||||
|
||||
# 请求信息
|
||||
request_id = Column(String(100), unique=True, index=True, nullable=False)
|
||||
@@ -345,7 +352,7 @@ class Usage(Base):
|
||||
# 成本计算
|
||||
input_cost_usd = Column(Float, default=0.0)
|
||||
output_cost_usd = Column(Float, default=0.0)
|
||||
cache_cost_usd = Column(Float, default=0.0) # 总缓存成本(兼容旧数据)
|
||||
cache_cost_usd = Column(Float, default=0.0) # 总缓存成本
|
||||
cache_creation_cost_usd = Column(Float, default=0.0) # 缓存创建成本
|
||||
cache_read_cost_usd = Column(Float, default=0.0) # 缓存读取成本
|
||||
request_cost_usd = Column(Float, default=0.0) # 按次计费成本
|
||||
@@ -397,6 +404,12 @@ class Usage(Base):
|
||||
# - void: 作废(不收费,如任务未开始就取消)
|
||||
billing_status = Column(String(20), default="settled", nullable=False, index=True)
|
||||
finalized_at = Column(DateTime(timezone=True), nullable=True) # 结算完成时间(可选)
|
||||
wallet_balance_before = Column(Numeric(20, 8), nullable=True) # 结算前可用总余额快照
|
||||
wallet_balance_after = Column(Numeric(20, 8), nullable=True) # 结算后可用总余额快照
|
||||
wallet_recharge_balance_before = Column(Numeric(20, 8), nullable=True) # 结算前充值余额
|
||||
wallet_recharge_balance_after = Column(Numeric(20, 8), nullable=True) # 结算后充值余额
|
||||
wallet_gift_balance_before = Column(Numeric(20, 8), nullable=True) # 结算前赠款余额
|
||||
wallet_gift_balance_after = Column(Numeric(20, 8), nullable=True) # 结算后赠款余额
|
||||
|
||||
# 完整请求和响应记录
|
||||
request_headers = Column(JSON, nullable=True) # 客户端请求头
|
||||
@@ -428,6 +441,7 @@ class Usage(Base):
|
||||
# 关系
|
||||
user = relationship("User", back_populates="usage_records")
|
||||
api_key = relationship("ApiKey", back_populates="usage_records")
|
||||
wallet = relationship("Wallet", back_populates="usage_records")
|
||||
provider_obj = relationship("Provider") # 使用 provider_obj 避免与 provider 字段名冲突
|
||||
provider_endpoint = relationship("ProviderEndpoint")
|
||||
provider_api_key = relationship("ProviderAPIKey")
|
||||
@@ -473,31 +487,45 @@ class Usage(Base):
|
||||
return None
|
||||
|
||||
|
||||
class UserQuota(Base):
|
||||
"""用户配额历史记录"""
|
||||
class Wallet(Base):
|
||||
"""统一钱包模型(用户钱包 / 独立 API Key 钱包)"""
|
||||
|
||||
__tablename__ = "user_quotas"
|
||||
__tablename__ = "wallets"
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
# 活跃钱包必须归属唯一 owner;owner 被删除后允许双 NULL(孤立钱包由清理策略回收)。
|
||||
"(user_id IS NOT NULL AND api_key_id IS NULL) "
|
||||
"OR (user_id IS NULL AND api_key_id IS NOT NULL) "
|
||||
"OR (user_id IS NULL AND api_key_id IS NULL)",
|
||||
name="ck_wallet_single_owner",
|
||||
),
|
||||
CheckConstraint("gift_balance >= 0", name="ck_wallets_gift_balance_non_negative"),
|
||||
# user_id/api_key_id 的 unique=True 已隐含唯一索引,无需额外 Index
|
||||
Index("idx_wallets_status", "status"),
|
||||
)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = Column(
|
||||
String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, unique=True
|
||||
)
|
||||
api_key_id = Column(
|
||||
String(36), ForeignKey("api_keys.id", ondelete="SET NULL"), nullable=True, unique=True
|
||||
)
|
||||
|
||||
# 配额类型
|
||||
quota_type = Column(String(50), nullable=False) # monthly, daily, custom
|
||||
# balance: 充值余额(可退款余额)
|
||||
balance = Column(Numeric(20, 8), nullable=False, default=0)
|
||||
# gift_balance: 赠款余额(不可退款)
|
||||
gift_balance = Column(Numeric(20, 8), nullable=False, default=0)
|
||||
# finite: 按余额校验;unlimited: 忽略余额放行,但仍统计消费
|
||||
limit_mode = Column(String(20), nullable=False, default="finite")
|
||||
currency = Column(String(3), nullable=False, default="USD")
|
||||
status = Column(String(20), nullable=False, default="active")
|
||||
|
||||
# 配额值
|
||||
quota_usd = Column(Float, nullable=False)
|
||||
total_recharged = Column(Numeric(20, 8), nullable=False, default=0)
|
||||
total_consumed = Column(Numeric(20, 8), nullable=False, default=0)
|
||||
total_refunded = Column(Numeric(20, 8), nullable=False, default=0)
|
||||
total_adjusted = Column(Numeric(20, 8), nullable=False, default=0)
|
||||
|
||||
# 时间范围
|
||||
period_start = Column(DateTime(timezone=True), nullable=False)
|
||||
period_end = Column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
# 使用情况
|
||||
used_usd = Column(Float, default=0.0)
|
||||
|
||||
# 状态
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
|
||||
)
|
||||
@@ -508,8 +536,192 @@ class UserQuota(Base):
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
# 关系
|
||||
user = relationship("User", back_populates="quotas")
|
||||
user = relationship("User", back_populates="wallet")
|
||||
api_key = relationship("ApiKey", back_populates="wallet")
|
||||
usage_records = relationship("Usage", back_populates="wallet")
|
||||
transactions = relationship(
|
||||
"WalletTransaction", back_populates="wallet", cascade="all, delete-orphan"
|
||||
)
|
||||
payment_orders = relationship("PaymentOrder", back_populates="wallet")
|
||||
refund_requests = relationship("RefundRequest", back_populates="wallet")
|
||||
|
||||
|
||||
class WalletTransaction(Base):
|
||||
"""钱包资金流水(只记录资金动作,不重复记录每次请求消费)"""
|
||||
|
||||
__tablename__ = "wallet_transactions"
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"balance_before = recharge_balance_before + gift_balance_before",
|
||||
name="ck_wallet_tx_balance_before_consistent",
|
||||
),
|
||||
CheckConstraint(
|
||||
"balance_after = recharge_balance_after + gift_balance_after",
|
||||
name="ck_wallet_tx_balance_after_consistent",
|
||||
),
|
||||
Index("idx_wallet_tx_wallet_created", "wallet_id", "created_at"),
|
||||
Index("idx_wallet_tx_link", "link_type", "link_id"),
|
||||
Index("idx_wallet_tx_category_created", "category", "created_at"),
|
||||
Index("idx_wallet_tx_reason_created", "reason_code", "created_at"),
|
||||
)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
wallet_id = Column(String(36), ForeignKey("wallets.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
category = Column(String(20), nullable=False)
|
||||
reason_code = Column(String(40), nullable=False)
|
||||
amount = Column(Numeric(20, 8), nullable=False)
|
||||
# 总可用余额(充值+赠款)快照
|
||||
balance_before = Column(Numeric(20, 8), nullable=False)
|
||||
balance_after = Column(Numeric(20, 8), nullable=False)
|
||||
# 分账户快照(审计用)
|
||||
recharge_balance_before = Column(Numeric(20, 8), nullable=False)
|
||||
recharge_balance_after = Column(Numeric(20, 8), nullable=False)
|
||||
gift_balance_before = Column(Numeric(20, 8), nullable=False)
|
||||
gift_balance_after = Column(Numeric(20, 8), nullable=False)
|
||||
|
||||
link_type = Column(String(30), nullable=True)
|
||||
link_id = Column(String(100), nullable=True)
|
||||
operator_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
|
||||
)
|
||||
|
||||
wallet = relationship("Wallet", back_populates="transactions")
|
||||
operator = relationship("User")
|
||||
|
||||
|
||||
class PaymentOrder(Base):
|
||||
"""充值订单"""
|
||||
|
||||
__tablename__ = "payment_orders"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("order_no", name="uq_payment_orders_order_no"),
|
||||
Index("idx_payment_orders_wallet_created", "wallet_id", "created_at"),
|
||||
Index("idx_payment_orders_user_created", "user_id", "created_at"),
|
||||
Index("idx_payment_orders_status", "status"),
|
||||
Index("idx_payment_orders_gateway_order_id", "gateway_order_id"),
|
||||
)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
order_no = Column(String(64), nullable=False)
|
||||
wallet_id = Column(String(36), ForeignKey("wallets.id", ondelete="RESTRICT"), nullable=False)
|
||||
user_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
|
||||
amount_usd = Column(Numeric(20, 8), nullable=False)
|
||||
pay_amount = Column(Numeric(20, 2), nullable=True)
|
||||
pay_currency = Column(String(3), nullable=True)
|
||||
exchange_rate = Column(Numeric(18, 8), nullable=True)
|
||||
refunded_amount_usd = Column(Numeric(20, 8), nullable=False, default=0)
|
||||
refundable_amount_usd = Column(Numeric(20, 8), nullable=False, default=0)
|
||||
|
||||
payment_method = Column(String(30), nullable=False)
|
||||
gateway_order_id = Column(String(128), nullable=True)
|
||||
gateway_response = Column(JSONB, nullable=True)
|
||||
|
||||
status = Column(String(20), nullable=False, default="pending")
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
|
||||
)
|
||||
paid_at = Column(DateTime(timezone=True), nullable=True)
|
||||
credited_at = Column(DateTime(timezone=True), nullable=True)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
wallet = relationship("Wallet", back_populates="payment_orders")
|
||||
user = relationship("User", back_populates="payment_orders")
|
||||
callbacks = relationship("PaymentCallback", back_populates="payment_order")
|
||||
refund_requests = relationship("RefundRequest", back_populates="payment_order")
|
||||
|
||||
|
||||
class PaymentCallback(Base):
|
||||
"""支付回调日志(幂等与审计)"""
|
||||
|
||||
__tablename__ = "payment_callbacks"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("callback_key", name="uq_payment_callbacks_callback_key"),
|
||||
Index("idx_payment_callbacks_order", "order_no"),
|
||||
Index("idx_payment_callbacks_gateway_order", "gateway_order_id"),
|
||||
Index("idx_payment_callbacks_created", "created_at"),
|
||||
)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
payment_order_id = Column(
|
||||
String(36), ForeignKey("payment_orders.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
payment_method = Column(String(30), nullable=False)
|
||||
|
||||
callback_key = Column(String(128), nullable=False)
|
||||
order_no = Column(String(64), nullable=True)
|
||||
gateway_order_id = Column(String(128), nullable=True)
|
||||
payload_hash = Column(String(128), nullable=True)
|
||||
signature_valid = Column(Boolean, nullable=False, default=False)
|
||||
status = Column(String(20), nullable=False, default="received")
|
||||
payload = Column(JSONB, nullable=True)
|
||||
error_message = Column(Text, nullable=True)
|
||||
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
|
||||
)
|
||||
processed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
payment_order = relationship("PaymentOrder", back_populates="callbacks")
|
||||
|
||||
|
||||
class RefundRequest(Base):
|
||||
"""退款申请(原路退款 / 非原路人工打款)"""
|
||||
|
||||
__tablename__ = "refund_requests"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("refund_no", name="uq_refund_requests_refund_no"),
|
||||
UniqueConstraint("idempotency_key", name="uq_refund_requests_idempotency_key"),
|
||||
Index("idx_refund_wallet_created", "wallet_id", "created_at"),
|
||||
Index("idx_refund_user_created", "user_id", "created_at"),
|
||||
Index("idx_refund_status", "status"),
|
||||
)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
refund_no = Column(String(64), nullable=False)
|
||||
wallet_id = Column(String(36), ForeignKey("wallets.id", ondelete="RESTRICT"), nullable=False)
|
||||
user_id = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
payment_order_id = Column(
|
||||
String(36), ForeignKey("payment_orders.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
source_type = Column(String(30), nullable=False) # payment_order/manual_recharge/card_recharge
|
||||
source_id = Column(String(100), nullable=True)
|
||||
refund_mode = Column(String(30), nullable=False) # original_channel/offline_payout
|
||||
amount_usd = Column(Numeric(20, 8), nullable=False)
|
||||
|
||||
status = Column(String(30), nullable=False, default="pending_approval")
|
||||
reason = Column(Text, nullable=True)
|
||||
requested_by = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
approved_by = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
processed_by = Column(String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
|
||||
gateway_refund_id = Column(String(128), nullable=True)
|
||||
payout_method = Column(String(50), nullable=True)
|
||||
payout_reference = Column(String(255), nullable=True)
|
||||
payout_proof = Column(JSONB, nullable=True)
|
||||
failure_reason = Column(Text, nullable=True)
|
||||
idempotency_key = Column(String(128), nullable=True)
|
||||
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False
|
||||
)
|
||||
updated_at = Column(
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc),
|
||||
nullable=False,
|
||||
)
|
||||
processed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
wallet = relationship("Wallet", back_populates="refund_requests")
|
||||
user = relationship("User", back_populates="refund_requests", foreign_keys=[user_id])
|
||||
payment_order = relationship("PaymentOrder", back_populates="refund_requests")
|
||||
|
||||
|
||||
class SystemConfig(Base):
|
||||
@@ -517,7 +729,7 @@ class SystemConfig(Base):
|
||||
|
||||
__tablename__ = "system_configs"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
key = Column(String(100), unique=True, nullable=False)
|
||||
value = Column(JSON, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
@@ -703,7 +915,7 @@ class Provider(ExportMixin, Base):
|
||||
}
|
||||
)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
name = Column(String(100), unique=True, nullable=False, index=True) # 提供商名称(唯一)
|
||||
description = Column(Text, nullable=True) # 提供商描述
|
||||
website = Column(String(500), nullable=True) # 主站网站
|
||||
@@ -814,7 +1026,7 @@ class ProviderEndpoint(ExportMixin, Base):
|
||||
}
|
||||
)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
provider_id = Column(String(36), ForeignKey("providers.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
# API 格式和配置
|
||||
@@ -886,7 +1098,7 @@ class ProxyNode(Base):
|
||||
|
||||
__tablename__ = "proxy_nodes"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
name = Column(String(100), nullable=False) # 节点名
|
||||
ip = Column(String(512), nullable=False) # 公网 IP 或手动节点的主机名(含协议前缀)
|
||||
port = Column(Integer, nullable=False) # 代理端口
|
||||
@@ -1016,7 +1228,7 @@ class GlobalModel(ExportMixin, Base):
|
||||
}
|
||||
)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
name = Column(String(100), unique=True, nullable=False, index=True) # 统一模型名(唯一)
|
||||
display_name = Column(String(100), nullable=False)
|
||||
|
||||
@@ -1116,7 +1328,7 @@ class Model(ExportMixin, Base):
|
||||
}
|
||||
)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
provider_id = Column(String(36), ForeignKey("providers.id"), nullable=False)
|
||||
# 必须关联一个 GlobalModel
|
||||
global_model_id = Column(String(36), ForeignKey("global_models.id"), nullable=False, index=True)
|
||||
@@ -1540,7 +1752,7 @@ class ProviderAPIKey(ExportMixin, Base):
|
||||
}
|
||||
)
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
|
||||
# 外键关系 - 直接关联 Provider
|
||||
provider_id = Column(
|
||||
@@ -1548,7 +1760,7 @@ class ProviderAPIKey(ExportMixin, Base):
|
||||
)
|
||||
|
||||
# API 格式支持列表(核心字段)
|
||||
# None 表示支持所有格式(兼容历史数据),空列表 [] 表示不支持任何格式
|
||||
# None 表示支持所有格式,空列表 [] 表示不支持任何格式
|
||||
api_formats = Column(JSON, nullable=True, default=list) # ["claude:chat", "claude:cli"]
|
||||
|
||||
# 认证类型
|
||||
@@ -1799,7 +2011,7 @@ class UserPreference(Base):
|
||||
|
||||
__tablename__ = "user_preferences"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = Column(
|
||||
String(36), ForeignKey("users.id", ondelete="CASCADE"), unique=True, nullable=False
|
||||
)
|
||||
@@ -1840,7 +2052,7 @@ class Announcement(Base):
|
||||
|
||||
__tablename__ = "announcements"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
title = Column(String(200), nullable=False)
|
||||
content = Column(Text, nullable=False) # 支持 Markdown
|
||||
type = Column(String(20), default="info") # info, warning, maintenance, important
|
||||
@@ -1881,7 +2093,7 @@ class AnnouncementRead(Base):
|
||||
|
||||
__tablename__ = "announcement_reads"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
announcement_id = Column(String(36), ForeignKey("announcements.id"), nullable=False)
|
||||
read_at = Column(
|
||||
@@ -1945,7 +2157,7 @@ class ManagementToken(Base):
|
||||
TOKEN_PREFIX = "ae_"
|
||||
TOKEN_RANDOM_LENGTH = 40
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
# Token 信息
|
||||
@@ -2119,7 +2331,7 @@ class AuditLog(Base):
|
||||
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
event_type = Column(String(50), nullable=False, index=True)
|
||||
user_id = Column(
|
||||
String(36), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
|
||||
@@ -2696,7 +2908,7 @@ class GeminiFileMapping(Base):
|
||||
|
||||
__tablename__ = "gemini_file_mappings"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
|
||||
# 文件名(如 files/abc123xyz)
|
||||
file_name = Column(String(255), nullable=False, unique=True, index=True)
|
||||
|
||||
Reference in New Issue
Block a user