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:
2026-03-09 19:58:05 +08:00
parent 73a71f198f
commit ac19d029da
20 changed files with 3341 additions and 1440 deletions

View File

@@ -1,37 +1,67 @@
from pydantic import BaseModel, EmailStr
from typing import Optional, List
from pydantic import BaseModel, EmailStr, Field, model_validator
from typing import Optional, List, Any
from datetime import datetime
from app.models.models import MembershipType, AccountStatus, KeyStatus
from app.models.models import KeyMembershipType, AccountStatus, KeyStatus
# ========== 账号相关 ==========
class AccountBase(BaseModel):
"""账号基础信息 (用于创建/更新)"""
email: str
access_token: str
refresh_token: Optional[str] = None
workos_session_token: Optional[str] = None
membership_type: MembershipType = MembershipType.PRO
token: Optional[str] = Field(None, description="兼容旧字段: user_id::jwt")
access_token: Optional[str] = Field(None, description="Access Token")
refresh_token: Optional[str] = Field(None, description="Refresh Token")
workos_session_token: Optional[str] = Field(None, description="WorkosCursorSessionToken")
password: Optional[str] = None
remark: Optional[str] = None
class AccountCreate(AccountBase):
pass
"""创建账号 (兼容旧字段)"""
@model_validator(mode='before')
@classmethod
def ensure_token(cls, data: Any) -> Any:
"""确保至少提供一个 Token"""
if isinstance(data, dict):
if not data.get('token'):
for field in ("workos_session_token", "access_token"):
if data.get(field):
data['token'] = data[field]
break
return data
class AccountUpdate(BaseModel):
email: Optional[str] = None
token: Optional[str] = None
access_token: Optional[str] = None
refresh_token: Optional[str] = None
workos_session_token: Optional[str] = None
membership_type: Optional[MembershipType] = None
password: Optional[str] = None
status: Optional[AccountStatus] = None
remark: Optional[str] = None
class AccountResponse(AccountBase):
class AccountResponse(BaseModel):
"""账号响应 (匹配 CursorAccount 模型)"""
id: int
email: str
token: str
access_token: Optional[str] = None
refresh_token: Optional[str] = None
workos_session_token: Optional[str] = None
password: Optional[str] = None
status: AccountStatus
usage_count: int
last_used_at: Optional[datetime] = None
current_key_id: Optional[int] = None
account_type: Optional[str] = None
membership_type: Optional[str] = None
trial_days_remaining: int = 0
usage_limit: int = 0
usage_used: int = 0
usage_remaining: int = 0
usage_percent: float = 0
total_requests: int = 0
locked_by_key_id: Optional[int] = None
last_analyzed_at: Optional[datetime] = None
remark: Optional[str] = None
created_at: datetime
updated_at: datetime
@@ -51,7 +81,7 @@ class ExternalAccountItem(BaseModel):
access_token: str
refresh_token: Optional[str] = None
workos_session_token: Optional[str] = None
membership_type: Optional[str] = "free" # free/pro, 默认free(auto账号)
membership_type: Optional[str] = "free" # Cursor账号类型: free/free_trial/pro/business
remark: Optional[str] = None
class ExternalBatchUpload(BaseModel):
@@ -72,7 +102,7 @@ class ExternalBatchResponse(BaseModel):
# ========== 激活码相关 ==========
class KeyBase(BaseModel):
membership_type: MembershipType = MembershipType.PRO # pro=高级模型, free=无限auto
membership_type: KeyMembershipType = KeyMembershipType.PRO # pro=高级模型, auto=无限换号
quota: int = 500 # 总额度 (仅Pro有效)
valid_days: int = 30 # 有效天数0表示永久 (仅Auto有效)
max_devices: int = 2 # 最大设备数
@@ -83,7 +113,7 @@ class KeyCreate(KeyBase):
count: int = 1 # 批量生成数量
class KeyUpdate(BaseModel):
membership_type: Optional[MembershipType] = None
membership_type: Optional[KeyMembershipType] = None
quota: Optional[int] = None
valid_days: Optional[int] = None
max_devices: Optional[int] = None
@@ -98,15 +128,15 @@ class KeyResponse(BaseModel):
id: int
key: str
status: KeyStatus
membership_type: MembershipType
membership_type: KeyMembershipType
quota: int
quota_used: int
quota_remaining: Optional[int] = None # 剩余额度(计算字段)
valid_days: int
valid_days: int = 30 # 有效天数 (映射自 duration_days)
first_activated_at: Optional[datetime] = None
expire_at: Optional[datetime] = None
max_devices: int
switch_count: int
switch_count: int = 0
last_switch_at: Optional[datetime] = None
current_account_id: Optional[int] = None
remark: Optional[str] = None
@@ -184,17 +214,43 @@ class LoginRequest(BaseModel):
class GlobalSettingsResponse(BaseModel):
"""全局设置响应"""
# Auto密钥设置
auto_switch_interval_minutes: int = 20 # 换号最小间隔(分钟)
auto_max_switches_per_day: int = 50 # 每天最大换号次数
# Pro密钥设置
pro_quota_cost: int = 50 # 每次换号扣除额度
# ===== 密钥策略 =====
key_max_devices: int = 2 # 主密钥最大设备数
auto_merge_enabled: bool = True # 是否启用同类型密钥自动合并
# ===== 自动检测开关 =====
auto_analyze_enabled: bool = False # 是否启用自动账号分析
auto_switch_enabled: bool = True # 是否启用自动换号
# ===== 账号分析设置 =====
account_analyze_interval: int = 300 # 账号分析间隔(秒)
account_analyze_batch_size: int = 10 # 每批分析账号数量
# ===== 换号阈值 =====
auto_switch_threshold: int = 98 # Auto池自动换号阈值(用量百分比)
pro_switch_threshold: int = 98 # Pro池自动换号阈值(用量百分比)
# ===== 换号限制 =====
max_switch_per_day: int = 50 # 每日最大换号次数
auto_daily_switches: int = 999 # Auto密钥每日换号次数限制
auto_switch_interval: int = 0 # Auto密钥换号冷却时间(分钟), 0表示无限制
pro_quota_per_switch: int = 1 # Pro密钥每次换号消耗积分
class GlobalSettingsUpdate(BaseModel):
"""更新全局设置"""
auto_switch_interval_minutes: Optional[int] = None
auto_max_switches_per_day: Optional[int] = None
pro_quota_cost: Optional[int] = None
# ===== 密钥策略 =====
key_max_devices: Optional[int] = None
auto_merge_enabled: Optional[bool] = None
# ===== 自动检测开关 =====
auto_analyze_enabled: Optional[bool] = None
auto_switch_enabled: Optional[bool] = None
# ===== 账号分析设置 =====
account_analyze_interval: Optional[int] = None
account_analyze_batch_size: Optional[int] = None
# ===== 换号阈值 =====
auto_switch_threshold: Optional[int] = None
pro_switch_threshold: Optional[int] = None
# ===== 换号限制 =====
max_switch_per_day: Optional[int] = None
auto_daily_switches: Optional[int] = None
auto_switch_interval: Optional[int] = None
pro_quota_per_switch: Optional[int] = None
# ========== 批量操作相关 ==========
@@ -210,3 +266,20 @@ class BatchExtendResponse(BaseModel):
success: int
failed: int
errors: List[str] = []
# ========== 公告相关 ==========
class AnnouncementCreate(BaseModel):
"""创建公告"""
title: str
content: str
type: str = "info" # info/warning/error/success
is_active: bool = True
class AnnouncementUpdate(BaseModel):
"""更新公告"""
title: Optional[str] = None
content: Optional[str] = None
type: Optional[str] = None
is_active: Optional[bool] = None