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

294
backend/app/tasks.py Normal file
View File

@@ -0,0 +1,294 @@
"""
蜂鸟Pro 后台定时任务 v2.1
- 账号分析任务:定期从 Cursor API 获取账号用量数据
- 自动换号任务:检查用量超阈值的账号并自动换号
"""
import asyncio
import logging
from datetime import datetime, timedelta
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger
from app.database import SessionLocal
from app.services import (
AccountService, KeyService, GlobalSettingsService,
analyze_account_from_token
)
from app.models.models import AccountStatus, KeyStatus, KeyMembershipType
# 配置日志
logger = logging.getLogger("tasks")
logger.setLevel(logging.INFO)
# 调度器
scheduler = AsyncIOScheduler()
async def analyze_accounts_task():
"""
账号分析任务
定期扫描 pending/available 状态的账号,从 Cursor API 获取最新用量数据
执行频率:每 5 分钟
"""
db = SessionLocal()
try:
# 检查开关
enabled = GlobalSettingsService.get(db, "auto_analyze_enabled")
if not enabled or str(enabled).strip().lower() not in ("true", "1", "yes", "y", "on"):
logger.debug("[账号分析] 自动分析已关闭,跳过")
return
logger.info("[账号分析] 开始执行...")
# 获取需要分析的账号
accounts = AccountService.get_pending_accounts(db, limit=10)
if not accounts:
logger.info("[账号分析] 无需分析的账号")
return
logger.info(f"[账号分析] 发现 {len(accounts)} 个待分析账号")
success_count = 0
fail_count = 0
for account in accounts:
try:
# 调用 Cursor API 分析账号
analysis_data = await analyze_account_from_token(account.token)
# 更新账号信息
AccountService.update_from_analysis(db, account.id, analysis_data)
if analysis_data.get("success"):
success_count += 1
logger.info(
f"[账号分析] {account.email} 分析成功: "
f"类型={analysis_data.get('account_type')}, "
f"用量={analysis_data.get('usage_percent')}%"
)
else:
fail_count += 1
logger.warning(
f"[账号分析] {account.email} 分析失败: {analysis_data.get('error')}"
)
# 避免请求过于频繁
await asyncio.sleep(1)
except Exception as e:
fail_count += 1
logger.error(f"[账号分析] {account.email} 异常: {str(e)}")
logger.info(f"[账号分析] 完成: 成功 {success_count}, 失败 {fail_count}")
except Exception as e:
logger.error(f"[账号分析] 任务异常: {str(e)}")
finally:
db.close()
async def auto_switch_task():
"""
自动换号任务
检查已启用无感换号的密钥,如果当前账号用量超阈值则自动换号
执行频率:每 10 分钟
"""
db = SessionLocal()
try:
# 检查开关
enabled = GlobalSettingsService.get(db, "auto_switch_enabled")
if not enabled or str(enabled).strip().lower() not in ("true", "1", "yes", "y", "on"):
logger.debug("[自动换号] 自动换号已关闭,跳过")
return
logger.info("[自动换号] 开始执行...")
# 获取阈值设置
auto_threshold = GlobalSettingsService.get_int(db, "auto_switch_threshold") or 98
pro_threshold = GlobalSettingsService.get_int(db, "pro_switch_threshold") or 98
# 查找已启用无感的活跃密钥
from app.models.models import ActivationKey, CursorAccount
active_keys = db.query(ActivationKey).filter(
ActivationKey.status == KeyStatus.ACTIVE,
ActivationKey.seamless_enabled == True,
ActivationKey.current_account_id != None,
ActivationKey.master_key_id == None # 只处理主密钥
).all()
if not active_keys:
logger.info("[自动换号] 无需处理的密钥")
return
logger.info(f"[自动换号] 检查 {len(active_keys)} 个密钥")
switch_count = 0
for key in active_keys:
try:
# 获取当前账号
account = AccountService.get_by_id(db, key.current_account_id)
if not account:
continue
# 确定阈值
threshold = auto_threshold if key.membership_type == KeyMembershipType.AUTO else pro_threshold
# 检查是否需要换号
usage_percent = float(account.usage_percent) if account.usage_percent else 0
if usage_percent < threshold:
continue
logger.info(
f"[自动换号] 密钥 {key.key[:8]}**** 账号 {account.email} "
f"用量 {usage_percent}% >= {threshold}%, 触发换号"
)
# 执行换号
success, message, new_account = KeyService.switch_account(db, key)
if success:
switch_count += 1
logger.info(
f"[自动换号] 换号成功: {account.email} -> {new_account.email}"
)
# 记录日志
from app.services import LogService
LogService.log(
db, key.id, "auto_switch",
account_id=new_account.id,
success=True,
message=f"自动换号: {account.email} -> {new_account.email}",
usage_snapshot={
"old_account": account.to_dict(),
"new_account": new_account.to_dict(),
"trigger_usage_percent": usage_percent,
"threshold": threshold
}
)
else:
logger.warning(f"[自动换号] 换号失败: {message}")
except Exception as e:
logger.error(f"[自动换号] 密钥 {key.key[:8]}**** 处理异常: {str(e)}")
logger.info(f"[自动换号] 完成: 换号 {switch_count}")
except Exception as e:
logger.error(f"[自动换号] 任务异常: {str(e)}")
finally:
db.close()
async def cleanup_expired_keys_task():
"""
清理过期密钥任务
将过期的密钥状态更新为 expired释放关联的账号
执行频率:每小时
"""
db = SessionLocal()
try:
logger.info("[清理过期] 开始执行...")
from app.models.models import ActivationKey
# 查找需要检查的活跃密钥
active_keys = db.query(ActivationKey).filter(
ActivationKey.status == KeyStatus.ACTIVE
).all()
expired_count = 0
for key in active_keys:
if key.is_expired:
# 释放账号
if key.current_account_id:
account = AccountService.get_by_id(db, key.current_account_id)
if account:
AccountService.release_account(db, account)
# 更新状态
key.status = KeyStatus.EXPIRED
key.seamless_enabled = False
key.current_account_id = None
db.commit()
expired_count += 1
logger.info(f"[清理过期] 密钥 {key.key[:8]}**** 已过期")
logger.info(f"[清理过期] 完成: 处理 {expired_count} 个过期密钥")
except Exception as e:
logger.error(f"[清理过期] 任务异常: {str(e)}")
finally:
db.close()
async def init_global_settings_task():
"""
初始化全局设置任务
确保所有默认设置都存在
执行频率:启动时执行一次
"""
db = SessionLocal()
try:
logger.info("[初始化设置] 开始执行...")
GlobalSettingsService.init_settings(db)
logger.info("[初始化设置] 完成")
except Exception as e:
logger.error(f"[初始化设置] 任务异常: {str(e)}")
finally:
db.close()
def start_scheduler():
"""启动调度器"""
# 添加定时任务
scheduler.add_job(
analyze_accounts_task,
trigger=IntervalTrigger(minutes=5),
id="analyze_accounts",
name="账号分析任务",
replace_existing=True
)
scheduler.add_job(
auto_switch_task,
trigger=IntervalTrigger(minutes=10),
id="auto_switch",
name="自动换号任务",
replace_existing=True
)
scheduler.add_job(
cleanup_expired_keys_task,
trigger=IntervalTrigger(hours=1),
id="cleanup_expired",
name="清理过期密钥任务",
replace_existing=True
)
# 启动调度器
scheduler.start()
logger.info("[调度器] 后台任务调度器已启动")
def stop_scheduler():
"""停止调度器"""
if scheduler.running:
scheduler.shutdown()
logger.info("[调度器] 后台任务调度器已停止")
async def run_startup_tasks():
"""运行启动任务"""
await init_global_settings_task()
# 启动后立即执行一次账号分析
await analyze_accounts_task()