backend v2.1: 公告管理功能 + 系统重构
- 新增 Announcement 数据模型,支持公告的增删改查 - 后台管理新增"公告管理"Tab(创建/编辑/删除/启用禁用) - 客户端 /api/announcement 改为从数据库读取 - 账号服务重构,新增无感换号、自动分析等功能 - 新增后台任务调度器、数据库迁移脚本 - Schema/Service/Config 全面升级至 v2.1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1,4 @@
|
||||
from app.models.models import CursorAccount, ActivationKey, KeyDevice, UsageLog, GlobalSettings, MembershipType, AccountStatus, KeyStatus
|
||||
from app.models.models import (
|
||||
CursorAccount, ActivationKey, KeyDevice, UsageLog, GlobalSettings, Announcement,
|
||||
AccountStatus, AccountType, KeyMembershipType, KeyStatus
|
||||
)
|
||||
|
||||
@@ -1,93 +1,218 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, ForeignKey, Enum
|
||||
"""
|
||||
蜂鸟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 MembershipType(str, enum.Enum):
|
||||
FREE = "free"
|
||||
PRO = "pro"
|
||||
|
||||
# ==================== 枚举类型 ====================
|
||||
|
||||
class AccountStatus(str, enum.Enum):
|
||||
ACTIVE = "active" # 可用
|
||||
IN_USE = "in_use" # 使用中
|
||||
DISABLED = "disabled" # 禁用
|
||||
EXPIRED = "expired" # 过期
|
||||
"""账号状态"""
|
||||
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" # 已激活(主密钥)
|
||||
MERGED = "merged" # 已合并到主密钥
|
||||
REVOKED = "revoked" # 已撤销
|
||||
DISABLED = "disabled" # 禁用
|
||||
EXPIRED = "expired" # 过期
|
||||
ACTIVE = "active" # 已激活
|
||||
EXPIRED = "expired" # 已过期
|
||||
DISABLED = "disabled" # 已禁用
|
||||
|
||||
|
||||
# ==================== 数据模型 ====================
|
||||
|
||||
class CursorAccount(Base):
|
||||
"""Cursor 账号池"""
|
||||
"""
|
||||
Cursor 账号表
|
||||
存储从Cursor API获取的账号信息和用量数据
|
||||
"""
|
||||
__tablename__ = "cursor_accounts"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
email = Column(String(255), unique=True, nullable=False, comment="邮箱")
|
||||
access_token = Column(Text, nullable=False, comment="访问令牌")
|
||||
refresh_token = Column(Text, nullable=True, comment="刷新令牌")
|
||||
workos_session_token = Column(Text, nullable=True, comment="WorkOS会话令牌")
|
||||
membership_type = Column(Enum(MembershipType), default=MembershipType.PRO, comment="会员类型")
|
||||
status = Column(Enum(AccountStatus), default=AccountStatus.ACTIVE, comment="状态")
|
||||
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")
|
||||
|
||||
# 使用统计
|
||||
usage_count = Column(Integer, default=0, comment="使用次数")
|
||||
last_used_at = Column(DateTime, nullable=True, comment="最后使用时间")
|
||||
current_key_id = Column(Integer, ForeignKey("activation_keys.id"), nullable=True, comment="当前使用的激活码")
|
||||
# 状态管理
|
||||
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, comment="状态")
|
||||
|
||||
# 状态
|
||||
status = Column(
|
||||
Enum(KeyStatus),
|
||||
default=KeyStatus.UNUSED,
|
||||
index=True,
|
||||
comment="状态"
|
||||
)
|
||||
|
||||
# 套餐类型
|
||||
membership_type = Column(Enum(MembershipType), default=MembershipType.PRO, comment="套餐类型: free=Auto池, pro=Pro池")
|
||||
membership_type = Column(
|
||||
Enum(KeyMembershipType),
|
||||
default=KeyMembershipType.PRO,
|
||||
index=True,
|
||||
comment="套餐类型: auto/pro"
|
||||
)
|
||||
|
||||
# 密钥合并关系
|
||||
master_key_id = Column(Integer, ForeignKey("activation_keys.id"), nullable=True, comment="主密钥ID(如果已合并)")
|
||||
# 密钥合并 (支持多密钥合并到主密钥)
|
||||
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")
|
||||
|
||||
# 该密钥贡献的资源 (创建时设置,不变)
|
||||
duration_days = Column(Integer, default=30, comment="Auto: 该密钥贡献的天数")
|
||||
quota_contribution = Column(Integer, default=500, comment="Pro: 该密钥贡献的积分")
|
||||
# ===== Auto密钥专属字段 =====
|
||||
duration_days = Column(Integer, default=30, comment="该密钥贡献的天数")
|
||||
expire_at = Column(DateTime, nullable=True, comment="到期时间 (首次激活时计算)")
|
||||
|
||||
# 额度系统 (仅主密钥使用,累计值)
|
||||
quota = Column(Integer, default=500, comment="Pro主密钥: 总额度(累加)")
|
||||
quota_used = Column(Integer, default=0, comment="Pro主密钥: 已用额度")
|
||||
# ===== Pro密钥专属字段 =====
|
||||
quota_contribution = Column(Integer, default=500, comment="该密钥贡献的积分")
|
||||
quota = Column(Integer, default=500, comment="总积分 (主密钥累加值)")
|
||||
quota_used = Column(Integer, default=0, comment="已用积分")
|
||||
|
||||
# 有效期 (仅主密钥使用)
|
||||
expire_at = Column(DateTime, nullable=True, comment="Auto主密钥: 到期时间(累加)")
|
||||
# ===== 无感换号 =====
|
||||
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="首次激活时间")
|
||||
merged_at = Column(DateTime, nullable=True, comment="合并时间")
|
||||
|
||||
# 设备限制 (可换设备,此字段保留但不强制)
|
||||
max_devices = Column(Integer, default=3, comment="最大设备数(可换设备)")
|
||||
|
||||
# 当前绑定的账号 (仅主密钥使用)
|
||||
current_account_id = Column(Integer, ForeignKey("cursor_accounts.id"), nullable=True)
|
||||
current_account = relationship("CursorAccount", foreign_keys=[current_account_id])
|
||||
|
||||
# 统计 (仅主密钥使用)
|
||||
switch_count = Column(Integer, default=0, comment="总换号次数")
|
||||
last_switch_at = Column(DateTime, nullable=True, comment="最后换号时间")
|
||||
merged_count = Column(Integer, default=0, comment="已合并的密钥数量")
|
||||
last_active_at = Column(DateTime, nullable=True, comment="最后活跃时间")
|
||||
|
||||
# 备注
|
||||
remark = Column(String(500), nullable=True, comment="备注")
|
||||
@@ -97,52 +222,174 @@ class ActivationKey(Base):
|
||||
|
||||
# 关系
|
||||
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 UsageLog(Base):
|
||||
"""使用日志"""
|
||||
__tablename__ = "usage_logs"
|
||||
|
||||
class Announcement(Base):
|
||||
"""
|
||||
公告表
|
||||
管理员发布的系统公告
|
||||
"""
|
||||
__tablename__ = "announcements"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
key_id = Column(Integer, ForeignKey("activation_keys.id"), nullable=False)
|
||||
account_id = Column(Integer, ForeignKey("cursor_accounts.id"), nullable=True)
|
||||
action = Column(String(50), nullable=False, comment="操作类型: verify/switch/seamless")
|
||||
ip_address = Column(String(50), nullable=True)
|
||||
user_agent = Column(String(500), nullable=True)
|
||||
success = Column(Boolean, default=True)
|
||||
message = Column(String(500), nullable=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())
|
||||
|
||||
key = relationship("ActivationKey")
|
||||
account = relationship("CursorAccount")
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
Reference in New Issue
Block a user