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:
294
backend/app/tasks.py
Normal file
294
backend/app/tasks.py
Normal 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()
|
||||
Reference in New Issue
Block a user