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