feat(wallet): 钱包系统替代配额系统,新增支付与退款机制

- 新增钱包余额管理、充值、扣费、退款完整流程
- 新增支付网关抽象层(支持手动/支付宝/微信)
- 用量计费从配额系统迁移到钱包余额扣费
- 新增管理员钱包管理与支付订单管理页面
- 新增用户钱包中心页面
- 移除独立 Key 锁定机制,统一由钱包余额控制
- 新增相关 API 路由、序列化器与数据库迁移
- 新增钱包、支付、退款相关测试
This commit is contained in:
LewisPen
2026-03-08 00:05:48 +08:00
committed by fawney19
parent 9cdcce1b5f
commit 783f654953
108 changed files with 13152 additions and 3372 deletions

View File

@@ -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 列表")

View File

@@ -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="初始赠款USDnull 表示使用系统默认初始赠款"
)
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仅用于独立KeyNone = 无限制"
)
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

View File

@@ -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) # 当前余额USDNULL 表示无限制
# 钱包体系:余额/额度由 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(
# 活跃钱包必须归属唯一 ownerowner 被删除后允许双 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)