Files
cursornew2026/backend/app/models/models.py
huangzhenpc ac19d029da backend v2.1: 公告管理功能 + 系统重构
- 新增 Announcement 数据模型,支持公告的增删改查
- 后台管理新增"公告管理"Tab(创建/编辑/删除/启用禁用)
- 客户端 /api/announcement 改为从数据库读取
- 账号服务重构,新增无感换号、自动分析等功能
- 新增后台任务调度器、数据库迁移脚本
- Schema/Service/Config 全面升级至 v2.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:58:05 +08:00

396 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
蜂鸟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())