- 新增 Announcement 数据模型,支持公告的增删改查 - 后台管理新增"公告管理"Tab(创建/编辑/删除/启用禁用) - 客户端 /api/announcement 改为从数据库读取 - 账号服务重构,新增无感换号、自动分析等功能 - 新增后台任务调度器、数据库迁移脚本 - Schema/Service/Config 全面升级至 v2.1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
396 lines
15 KiB
Python
396 lines
15 KiB
Python
"""
|
||
蜂鸟Pro 数据模型 v2.1
|
||
基于系统设计文档重构
|
||
"""
|
||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, ForeignKey, Enum, JSON, DECIMAL, BigInteger
|
||
from sqlalchemy.orm import relationship
|
||
from sqlalchemy.sql import func
|
||
from app.database import Base
|
||
import enum
|
||
|
||
|
||
# ==================== 枚举类型 ====================
|
||
|
||
class AccountStatus(str, enum.Enum):
|
||
"""账号状态"""
|
||
PENDING = "pending" # 待分析
|
||
ANALYZING = "analyzing" # 分析中
|
||
AVAILABLE = "available" # 可用
|
||
IN_USE = "in_use" # 使用中
|
||
EXHAUSTED = "exhausted" # 已耗尽
|
||
INVALID = "invalid" # Token无效
|
||
DISABLED = "disabled" # 已禁用
|
||
|
||
|
||
class AccountType(str, enum.Enum):
|
||
"""账号类型 (从Cursor API分析得出)"""
|
||
FREE_TRIAL = "free_trial" # 免费试用
|
||
PRO = "pro" # Pro会员
|
||
FREE = "free" # 免费版
|
||
BUSINESS = "business" # 商业版
|
||
UNKNOWN = "unknown" # 未知
|
||
|
||
|
||
class KeyMembershipType(str, enum.Enum):
|
||
"""密钥套餐类型"""
|
||
AUTO = "auto" # Auto池 - 按时间计费,无限换号
|
||
PRO = "pro" # Pro池 - 按积分计费
|
||
|
||
|
||
class KeyStatus(str, enum.Enum):
|
||
"""密钥状态"""
|
||
UNUSED = "unused" # 未使用
|
||
ACTIVE = "active" # 已激活
|
||
EXPIRED = "expired" # 已过期
|
||
DISABLED = "disabled" # 已禁用
|
||
|
||
|
||
# ==================== 数据模型 ====================
|
||
|
||
class CursorAccount(Base):
|
||
"""
|
||
Cursor 账号表
|
||
存储从Cursor API获取的账号信息和用量数据
|
||
"""
|
||
__tablename__ = "cursor_accounts"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
email = Column(String(255), nullable=False, comment="账号邮箱")
|
||
token = Column(Text, nullable=False, comment="认证Token (user_id::jwt)")
|
||
password = Column(String(255), nullable=True, comment="账号密码(可选)")
|
||
access_token = Column(Text, nullable=True, comment="Access Token (GraphQL/API)")
|
||
refresh_token = Column(Text, nullable=True, comment="Refresh Token")
|
||
workos_session_token = Column(Text, nullable=True, comment="Workos Session Token")
|
||
|
||
# 状态管理
|
||
status = Column(
|
||
Enum(AccountStatus),
|
||
default=AccountStatus.PENDING,
|
||
index=True,
|
||
comment="账号状态"
|
||
)
|
||
|
||
# 账号类型 (从Cursor API自动分析得出)
|
||
account_type = Column(
|
||
Enum(AccountType),
|
||
default=AccountType.UNKNOWN,
|
||
index=True,
|
||
comment="账号类型"
|
||
)
|
||
|
||
# 用量信息 (从Cursor API获取)
|
||
membership_type = Column(String(50), nullable=True, comment="会员类型原始值")
|
||
billing_cycle_start = Column(DateTime, nullable=True, comment="计费周期开始")
|
||
billing_cycle_end = Column(DateTime, nullable=True, comment="计费周期结束")
|
||
trial_days_remaining = Column(Integer, default=0, comment="试用剩余天数")
|
||
|
||
# 用量统计
|
||
usage_limit = Column(Integer, default=0, comment="用量上限")
|
||
usage_used = Column(Integer, default=0, comment="已用用量")
|
||
usage_remaining = Column(Integer, default=0, comment="剩余用量")
|
||
usage_percent = Column(DECIMAL(5, 2), default=0, comment="用量百分比")
|
||
|
||
# 详细用量 (从聚合API获取)
|
||
total_requests = Column(Integer, default=0, comment="总请求次数")
|
||
total_input_tokens = Column(BigInteger, default=0, comment="总输入Token")
|
||
total_output_tokens = Column(BigInteger, default=0, comment="总输出Token")
|
||
total_cost_cents = Column(DECIMAL(10, 2), default=0, comment="总花费(美分)")
|
||
|
||
# 锁定信息
|
||
locked_by_key_id = Column(
|
||
Integer,
|
||
ForeignKey("activation_keys.id"),
|
||
nullable=True,
|
||
index=True,
|
||
comment="被哪个激活码锁定"
|
||
)
|
||
locked_at = Column(DateTime, nullable=True, comment="锁定时间")
|
||
|
||
# 分析信息
|
||
last_analyzed_at = Column(DateTime, nullable=True, comment="最后分析时间")
|
||
analyze_error = Column(String(500), nullable=True, comment="分析错误信息")
|
||
|
||
# 元数据
|
||
remark = Column(String(500), nullable=True, comment="备注")
|
||
created_at = Column(DateTime, server_default=func.now())
|
||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||
|
||
# 关系
|
||
locked_by_key = relationship("ActivationKey", foreign_keys=[locked_by_key_id])
|
||
|
||
@property
|
||
def total_cost_usd(self):
|
||
"""总花费(美元)"""
|
||
if self.total_cost_cents:
|
||
return float(self.total_cost_cents) / 100
|
||
return 0.0
|
||
|
||
def to_dict(self):
|
||
"""转换为字典"""
|
||
return {
|
||
"id": self.id,
|
||
"email": self.email,
|
||
"status": self.status.value if self.status else None,
|
||
"account_type": self.account_type.value if self.account_type else None,
|
||
"membership_type": self.membership_type,
|
||
"trial_days_remaining": self.trial_days_remaining,
|
||
"usage_limit": self.usage_limit,
|
||
"usage_used": self.usage_used,
|
||
"usage_remaining": self.usage_remaining,
|
||
"usage_percent": float(self.usage_percent) if self.usage_percent else 0,
|
||
"total_requests": self.total_requests,
|
||
"total_cost_usd": self.total_cost_usd,
|
||
"last_analyzed_at": self.last_analyzed_at.isoformat() if self.last_analyzed_at else None,
|
||
"remark": self.remark
|
||
}
|
||
|
||
|
||
class ActivationKey(Base):
|
||
"""
|
||
激活码表
|
||
支持Auto/Pro双池,密钥合并,无感换号
|
||
"""
|
||
__tablename__ = "activation_keys"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
key = Column(String(64), unique=True, nullable=False, index=True, comment="激活码")
|
||
|
||
# 状态
|
||
status = Column(
|
||
Enum(KeyStatus),
|
||
default=KeyStatus.UNUSED,
|
||
index=True,
|
||
comment="状态"
|
||
)
|
||
|
||
# 套餐类型
|
||
membership_type = Column(
|
||
Enum(KeyMembershipType),
|
||
default=KeyMembershipType.PRO,
|
||
index=True,
|
||
comment="套餐类型: auto/pro"
|
||
)
|
||
|
||
# 密钥合并 (支持多密钥合并到主密钥)
|
||
master_key_id = Column(
|
||
Integer,
|
||
ForeignKey("activation_keys.id"),
|
||
nullable=True,
|
||
index=True,
|
||
comment="主密钥ID (如果已合并到其他密钥)"
|
||
)
|
||
merged_count = Column(Integer, default=0, comment="已合并的子密钥数量")
|
||
merged_at = Column(DateTime, nullable=True, comment="合并时间")
|
||
|
||
# 设备绑定
|
||
device_id = Column(String(255), nullable=True, index=True, comment="绑定的设备ID")
|
||
|
||
# ===== Auto密钥专属字段 =====
|
||
duration_days = Column(Integer, default=30, comment="该密钥贡献的天数")
|
||
expire_at = Column(DateTime, nullable=True, comment="到期时间 (首次激活时计算)")
|
||
|
||
# ===== Pro密钥专属字段 =====
|
||
quota_contribution = Column(Integer, default=500, comment="该密钥贡献的积分")
|
||
quota = Column(Integer, default=500, comment="总积分 (主密钥累加值)")
|
||
quota_used = Column(Integer, default=0, comment="已用积分")
|
||
|
||
# ===== 无感换号 =====
|
||
seamless_enabled = Column(Boolean, default=False, comment="是否启用无感换号")
|
||
current_account_id = Column(
|
||
Integer,
|
||
ForeignKey("cursor_accounts.id"),
|
||
nullable=True,
|
||
comment="当前使用的账号ID"
|
||
)
|
||
|
||
# ===== 统计 =====
|
||
switch_count = Column(Integer, default=0, comment="总换号次数")
|
||
last_switch_at = Column(DateTime, nullable=True, comment="最后换号时间")
|
||
|
||
# ===== 设备限制 =====
|
||
max_devices = Column(Integer, default=2, comment="最大设备数")
|
||
|
||
# 激活信息
|
||
first_activated_at = Column(DateTime, nullable=True, comment="首次激活时间")
|
||
last_active_at = Column(DateTime, nullable=True, comment="最后活跃时间")
|
||
|
||
# 备注
|
||
remark = Column(String(500), nullable=True, comment="备注")
|
||
|
||
created_at = Column(DateTime, server_default=func.now())
|
||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||
|
||
# 关系
|
||
master_key = relationship("ActivationKey", remote_side=[id], foreign_keys=[master_key_id])
|
||
current_account = relationship("CursorAccount", foreign_keys=[current_account_id])
|
||
|
||
@property
|
||
def valid_days(self):
|
||
"""兼容旧API: duration_days的别名"""
|
||
return self.duration_days or 0
|
||
|
||
@property
|
||
def quota_remaining(self):
|
||
"""剩余积分"""
|
||
return max(0, (self.quota or 0) - (self.quota_used or 0))
|
||
|
||
@property
|
||
def is_expired(self):
|
||
"""是否已过期"""
|
||
from datetime import datetime
|
||
if self.membership_type == KeyMembershipType.AUTO:
|
||
if self.expire_at:
|
||
return datetime.now() > self.expire_at
|
||
return False
|
||
elif self.membership_type == KeyMembershipType.PRO:
|
||
return self.quota_remaining <= 0
|
||
return False
|
||
|
||
def to_dict(self, include_account=False):
|
||
"""转换为字典"""
|
||
from datetime import datetime
|
||
|
||
data = {
|
||
"id": self.id,
|
||
"key": self.key,
|
||
"status": self.status.value if self.status else None,
|
||
"membership_type": self.membership_type.value if self.membership_type else None,
|
||
"seamless_enabled": self.seamless_enabled,
|
||
"switch_count": self.switch_count,
|
||
"first_activated_at": self.first_activated_at.isoformat() if self.first_activated_at else None,
|
||
"last_active_at": self.last_active_at.isoformat() if self.last_active_at else None,
|
||
}
|
||
|
||
# Auto密钥信息
|
||
if self.membership_type == KeyMembershipType.AUTO:
|
||
data["expire_at"] = self.expire_at.isoformat() if self.expire_at else None
|
||
if self.expire_at:
|
||
delta = self.expire_at - datetime.now()
|
||
data["days_remaining"] = max(0, delta.days)
|
||
else:
|
||
data["days_remaining"] = self.duration_days
|
||
|
||
# Pro密钥信息
|
||
if self.membership_type == KeyMembershipType.PRO:
|
||
data["quota"] = self.quota
|
||
data["quota_used"] = self.quota_used
|
||
data["quota_remaining"] = self.quota_remaining
|
||
|
||
# 当前账号信息
|
||
if include_account and self.current_account:
|
||
data["current_account"] = self.current_account.to_dict()
|
||
|
||
return data
|
||
|
||
|
||
class KeyDevice(Base):
|
||
"""
|
||
设备绑定表
|
||
记录激活码绑定的所有设备
|
||
"""
|
||
__tablename__ = "key_devices"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
key_id = Column(Integer, ForeignKey("activation_keys.id"), nullable=False)
|
||
device_id = Column(String(255), nullable=False, comment="设备标识")
|
||
device_name = Column(String(255), nullable=True, comment="设备名称")
|
||
platform = Column(String(50), nullable=True, comment="平台: windows/macos/linux")
|
||
last_active_at = Column(DateTime, nullable=True, comment="最后活跃时间")
|
||
created_at = Column(DateTime, server_default=func.now())
|
||
|
||
# 关系
|
||
key = relationship("ActivationKey")
|
||
|
||
class Meta:
|
||
unique_together = [("key_id", "device_id")]
|
||
|
||
|
||
class UsageLog(Base):
|
||
"""
|
||
使用日志表
|
||
记录所有操作
|
||
"""
|
||
__tablename__ = "usage_logs"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
key_id = Column(Integer, ForeignKey("activation_keys.id"), nullable=False, index=True)
|
||
account_id = Column(Integer, ForeignKey("cursor_accounts.id"), nullable=True)
|
||
|
||
action = Column(
|
||
String(50),
|
||
nullable=False,
|
||
index=True,
|
||
comment="操作类型: activate/verify/enable_seamless/disable_seamless/switch/auto_switch/release/merge"
|
||
)
|
||
|
||
success = Column(Boolean, default=True, comment="是否成功")
|
||
message = Column(String(500), nullable=True, comment="消息")
|
||
|
||
# 请求信息
|
||
ip_address = Column(String(50), nullable=True)
|
||
user_agent = Column(String(500), nullable=True)
|
||
device_id = Column(String(255), nullable=True)
|
||
|
||
# 用量快照 (换号时记录)
|
||
usage_snapshot = Column(JSON, nullable=True, comment="用量快照")
|
||
|
||
created_at = Column(DateTime, server_default=func.now(), index=True)
|
||
|
||
# 关系
|
||
key = relationship("ActivationKey")
|
||
account = relationship("CursorAccount")
|
||
|
||
|
||
class GlobalSettings(Base):
|
||
"""
|
||
全局设置表
|
||
存储系统配置
|
||
"""
|
||
__tablename__ = "global_settings"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
key = Column(String(100), unique=True, nullable=False, comment="设置键")
|
||
value = Column(String(500), nullable=False, comment="设置值")
|
||
value_type = Column(String(20), default="string", comment="值类型: string/int/float/bool/json")
|
||
description = Column(String(500), nullable=True, comment="描述")
|
||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||
|
||
@classmethod
|
||
def get_default_settings(cls):
|
||
"""默认设置"""
|
||
return [
|
||
# ===== 密钥策略 =====
|
||
{"key": "key_max_devices", "value": "2", "value_type": "int", "description": "主密钥最大设备数"},
|
||
{"key": "auto_merge_enabled", "value": "true", "value_type": "bool", "description": "是否启用同类型密钥自动合并"},
|
||
# ===== 自动检测开关 =====
|
||
{"key": "auto_analyze_enabled", "value": "false", "value_type": "bool", "description": "是否启用自动账号分析"},
|
||
{"key": "auto_switch_enabled", "value": "true", "value_type": "bool", "description": "是否启用自动换号"},
|
||
# ===== 账号分析设置 =====
|
||
{"key": "account_analyze_interval", "value": "300", "value_type": "int", "description": "账号分析间隔(秒)"},
|
||
{"key": "account_analyze_batch_size", "value": "10", "value_type": "int", "description": "每批分析账号数量"},
|
||
# ===== 换号阈值 =====
|
||
{"key": "auto_switch_threshold", "value": "98", "value_type": "int", "description": "Auto池自动换号阈值(用量百分比)"},
|
||
{"key": "pro_switch_threshold", "value": "98", "value_type": "int", "description": "Pro池自动换号阈值(用量百分比)"},
|
||
# ===== 换号限制 =====
|
||
{"key": "max_switch_per_day", "value": "50", "value_type": "int", "description": "每日最大换号次数"},
|
||
{"key": "auto_daily_switches", "value": "999", "value_type": "int", "description": "Auto密钥每日换号次数限制"},
|
||
{"key": "auto_switch_interval", "value": "0", "value_type": "int", "description": "Auto密钥换号冷却时间(分钟), 0表示无限制"},
|
||
{"key": "pro_quota_per_switch", "value": "1", "value_type": "int", "description": "Pro密钥每次换号消耗积分"},
|
||
]
|
||
|
||
|
||
class Announcement(Base):
|
||
"""
|
||
公告表
|
||
管理员发布的系统公告
|
||
"""
|
||
__tablename__ = "announcements"
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
title = Column(String(200), nullable=False, comment="公告标题")
|
||
content = Column(Text, nullable=False, comment="公告内容")
|
||
type = Column(String(20), default="info", comment="公告类型: info/warning/error/success")
|
||
is_active = Column(Boolean, default=True, comment="是否启用")
|
||
created_at = Column(DateTime, server_default=func.now())
|
||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|