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:
@@ -4,23 +4,31 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Header
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.config import settings
|
||||
from app.services import AccountService, KeyService, LogService, GlobalSettingsService, BatchService, authenticate_admin, create_access_token, get_current_user
|
||||
from app.schemas import (
|
||||
from app.schemas.schemas import (
|
||||
AccountCreate, AccountUpdate, AccountResponse, AccountImport,
|
||||
KeyCreate, KeyUpdate, KeyResponse,
|
||||
DashboardStats, Token, LoginRequest,
|
||||
GlobalSettingsResponse, GlobalSettingsUpdate,
|
||||
BatchExtendRequest, BatchExtendResponse,
|
||||
ExternalBatchUpload, ExternalBatchResponse
|
||||
ExternalBatchUpload, ExternalBatchResponse,
|
||||
AnnouncementCreate, AnnouncementUpdate
|
||||
)
|
||||
from app.models import MembershipType, KeyDevice, UsageLog, ActivationKey
|
||||
from app.models.models import KeyMembershipType, KeyStatus, AccountStatus, AccountType, KeyDevice, UsageLog, ActivationKey, CursorAccount, Announcement
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["Admin API"])
|
||||
|
||||
|
||||
class AccountAnalyzeRequest(BaseModel):
|
||||
"""手动分析账号请求"""
|
||||
token: Optional[str] = None
|
||||
save_token: bool = False
|
||||
|
||||
|
||||
# ========== 认证 ==========
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
@@ -79,8 +87,8 @@ async def external_batch_upload(
|
||||
|
||||
for item in data.accounts:
|
||||
try:
|
||||
# 转换membership_type
|
||||
mt = MembershipType.FREE if item.membership_type == "free" else MembershipType.PRO
|
||||
# 转换membership_type (free/auto -> AUTO, pro -> PRO)
|
||||
# 注意:mt 变量暂未使用,因为 CursorAccount 模型中 membership_type 是从 Cursor API 分析得出的
|
||||
|
||||
existing = AccountService.get_by_email(db, item.email)
|
||||
if existing:
|
||||
@@ -88,10 +96,10 @@ async def external_batch_upload(
|
||||
# 更新已存在的账号
|
||||
AccountService.update(
|
||||
db, existing.id,
|
||||
token=item.workos_session_token or item.access_token,
|
||||
access_token=item.access_token,
|
||||
refresh_token=item.refresh_token,
|
||||
workos_session_token=item.workos_session_token,
|
||||
membership_type=mt,
|
||||
remark=item.remark or existing.remark
|
||||
)
|
||||
updated += 1
|
||||
@@ -100,15 +108,16 @@ async def external_batch_upload(
|
||||
errors.append(f"{item.email}: 账号已存在")
|
||||
else:
|
||||
# 创建新账号
|
||||
account_data = AccountCreate(
|
||||
AccountService.create(
|
||||
db,
|
||||
email=item.email,
|
||||
token=item.workos_session_token or item.access_token,
|
||||
access_token=item.access_token,
|
||||
refresh_token=item.refresh_token,
|
||||
workos_session_token=item.workos_session_token,
|
||||
membership_type=mt,
|
||||
password=None,
|
||||
remark=item.remark
|
||||
)
|
||||
AccountService.create(db, account_data)
|
||||
created += 1
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
@@ -131,11 +140,12 @@ async def external_account_stats(
|
||||
):
|
||||
"""外部系统获取账号统计"""
|
||||
stats = AccountService.count(db)
|
||||
pro_count = db.query(CursorAccount).filter(CursorAccount.account_type == AccountType.PRO).count()
|
||||
return {
|
||||
"total": stats["total"],
|
||||
"active": stats["active"],
|
||||
"pro": stats["pro"],
|
||||
"free": stats["total"] - stats["pro"]
|
||||
"active": stats["available"] + stats["in_use"],
|
||||
"pro": pro_count,
|
||||
"free": stats["total"] - pro_count
|
||||
}
|
||||
|
||||
|
||||
@@ -181,8 +191,8 @@ async def get_dashboard(
|
||||
|
||||
return DashboardStats(
|
||||
total_accounts=account_stats["total"],
|
||||
active_accounts=account_stats["active"],
|
||||
pro_accounts=account_stats["pro"],
|
||||
active_accounts=account_stats["available"] + account_stats["in_use"], # 可用+使用中
|
||||
pro_accounts=key_stats["pro"], # Pro密钥数量
|
||||
total_keys=key_stats["total"],
|
||||
active_keys=key_stats["active"],
|
||||
today_usage=today_usage
|
||||
@@ -212,7 +222,16 @@ async def create_account(
|
||||
existing = AccountService.get_by_email(db, account.email)
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="邮箱已存在")
|
||||
return AccountService.create(db, account)
|
||||
return AccountService.create(
|
||||
db,
|
||||
email=account.email,
|
||||
token=account.token,
|
||||
password=account.password,
|
||||
remark=account.remark,
|
||||
access_token=account.access_token,
|
||||
refresh_token=account.refresh_token,
|
||||
workos_session_token=account.workos_session_token
|
||||
)
|
||||
|
||||
|
||||
@router.post("/accounts/import", response_model=dict)
|
||||
@@ -233,13 +252,24 @@ async def import_accounts(
|
||||
# 更新已存在的账号
|
||||
AccountService.update(
|
||||
db, existing.id,
|
||||
token=account.token or account.workos_session_token or account.access_token,
|
||||
access_token=account.access_token,
|
||||
refresh_token=account.refresh_token,
|
||||
workos_session_token=account.workos_session_token,
|
||||
membership_type=account.membership_type
|
||||
password=account.password,
|
||||
remark=account.remark
|
||||
)
|
||||
else:
|
||||
AccountService.create(db, account)
|
||||
AccountService.create(
|
||||
db,
|
||||
email=account.email,
|
||||
token=account.token or account.workos_session_token or account.access_token,
|
||||
password=account.password,
|
||||
remark=account.remark,
|
||||
access_token=account.access_token,
|
||||
refresh_token=account.refresh_token,
|
||||
workos_session_token=account.workos_session_token
|
||||
)
|
||||
success += 1
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
@@ -315,8 +345,7 @@ async def toggle_account_status(
|
||||
- 禁用(disabled) -> 可用(active)
|
||||
- 过期(expired) -> 可用(active)
|
||||
"""
|
||||
from app.models import AccountStatus, Account
|
||||
account = db.query(Account).filter(Account.id == account_id).first()
|
||||
account = db.query(CursorAccount).filter(CursorAccount.id == account_id).first()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
|
||||
@@ -324,14 +353,15 @@ async def toggle_account_status(
|
||||
|
||||
# 根据当前状态切换
|
||||
if account.status == AccountStatus.IN_USE:
|
||||
account.status = AccountStatus.ACTIVE
|
||||
account.current_key_id = None # 释放绑定
|
||||
elif account.status == AccountStatus.ACTIVE:
|
||||
account.status = AccountStatus.AVAILABLE
|
||||
account.locked_by_key_id = None # 释放绑定
|
||||
account.locked_at = None
|
||||
elif account.status == AccountStatus.AVAILABLE:
|
||||
account.status = AccountStatus.DISABLED
|
||||
elif account.status == AccountStatus.DISABLED:
|
||||
account.status = AccountStatus.ACTIVE
|
||||
elif account.status == AccountStatus.EXPIRED:
|
||||
account.status = AccountStatus.ACTIVE
|
||||
account.status = AccountStatus.AVAILABLE
|
||||
elif account.status == AccountStatus.EXHAUSTED:
|
||||
account.status = AccountStatus.AVAILABLE
|
||||
|
||||
db.commit()
|
||||
|
||||
@@ -350,16 +380,16 @@ async def release_account(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""释放账号(从使用中变为可用)"""
|
||||
from app.models import AccountStatus, Account
|
||||
account = db.query(Account).filter(Account.id == account_id).first()
|
||||
account = db.query(CursorAccount).filter(CursorAccount.id == account_id).first()
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
|
||||
if account.status != AccountStatus.IN_USE:
|
||||
return {"success": False, "message": "账号不在使用中状态"}
|
||||
|
||||
account.status = AccountStatus.ACTIVE
|
||||
account.current_key_id = None
|
||||
account.status = AccountStatus.AVAILABLE
|
||||
account.locked_by_key_id = None
|
||||
account.locked_at = None
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "message": "账号已释放"}
|
||||
@@ -372,15 +402,14 @@ async def batch_enable_accounts(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""批量启用账号"""
|
||||
from app.models import AccountStatus, Account
|
||||
success = 0
|
||||
failed = 0
|
||||
|
||||
for account_id in account_ids:
|
||||
try:
|
||||
account = db.query(Account).filter(Account.id == account_id).first()
|
||||
account = db.query(CursorAccount).filter(CursorAccount.id == account_id).first()
|
||||
if account:
|
||||
account.status = AccountStatus.ACTIVE
|
||||
account.status = AccountStatus.AVAILABLE
|
||||
success += 1
|
||||
else:
|
||||
failed += 1
|
||||
@@ -402,13 +431,12 @@ async def batch_disable_accounts(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""批量禁用账号"""
|
||||
from app.models import AccountStatus, Account
|
||||
success = 0
|
||||
failed = 0
|
||||
|
||||
for account_id in account_ids:
|
||||
try:
|
||||
account = db.query(Account).filter(Account.id == account_id).first()
|
||||
account = db.query(CursorAccount).filter(CursorAccount.id == account_id).first()
|
||||
if account:
|
||||
account.status = AccountStatus.DISABLED
|
||||
success += 1
|
||||
@@ -451,15 +479,6 @@ async def batch_delete_accounts(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/accounts/count")
|
||||
async def get_accounts_count(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""获取账号总数"""
|
||||
stats = AccountService.count(db)
|
||||
return {"total": stats["total"]}
|
||||
|
||||
|
||||
# ========== 激活码管理 ==========
|
||||
|
||||
@@ -475,7 +494,6 @@ async def list_keys(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""获取激活码列表(支持搜索和筛选)"""
|
||||
from app.models import KeyStatus
|
||||
query = db.query(ActivationKey).order_by(ActivationKey.id.desc())
|
||||
|
||||
# 搜索激活码
|
||||
@@ -493,9 +511,9 @@ async def list_keys(
|
||||
elif activated and activated == "false":
|
||||
query = query.filter(ActivationKey.first_activated_at == None)
|
||||
|
||||
# 套餐类型筛选
|
||||
# 套餐类型筛选 (free/auto -> AUTO, pro -> PRO)
|
||||
if membership_type:
|
||||
mt = MembershipType.PRO if membership_type.lower() == "pro" else MembershipType.FREE
|
||||
mt = KeyMembershipType.PRO if membership_type.lower() == "pro" else KeyMembershipType.AUTO
|
||||
query = query.filter(ActivationKey.membership_type == mt)
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
@@ -511,7 +529,6 @@ async def get_keys_count(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""获取激活码总数(支持筛选)"""
|
||||
from app.models import KeyStatus
|
||||
query = db.query(ActivationKey)
|
||||
|
||||
# 搜索激活码
|
||||
@@ -529,9 +546,9 @@ async def get_keys_count(
|
||||
elif activated and activated == "false":
|
||||
query = query.filter(ActivationKey.first_activated_at == None)
|
||||
|
||||
# 套餐类型筛选
|
||||
# 套餐类型筛选 (free/auto -> AUTO, pro -> PRO)
|
||||
if membership_type:
|
||||
mt = MembershipType.PRO if membership_type.lower() == "pro" else MembershipType.FREE
|
||||
mt = KeyMembershipType.PRO if membership_type.lower() == "pro" else KeyMembershipType.AUTO
|
||||
query = query.filter(ActivationKey.membership_type == mt)
|
||||
|
||||
total = query.count()
|
||||
@@ -545,7 +562,15 @@ async def create_keys(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""创建激活码"""
|
||||
return KeyService.create(db, key_data)
|
||||
return KeyService.create(
|
||||
db,
|
||||
count=key_data.count,
|
||||
membership_type=key_data.membership_type,
|
||||
duration_days=key_data.valid_days,
|
||||
quota=key_data.quota,
|
||||
max_devices=key_data.max_devices,
|
||||
remark=key_data.remark
|
||||
)
|
||||
|
||||
|
||||
@router.get("/keys/{key_id}", response_model=KeyResponse)
|
||||
@@ -735,7 +760,6 @@ async def disable_key(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""禁用激活码(返回使用信息供客服参考)"""
|
||||
from app.models import KeyStatus
|
||||
key = KeyService.get_by_id(db, key_id)
|
||||
if not key:
|
||||
raise HTTPException(status_code=404, detail="激活码不存在")
|
||||
@@ -770,7 +794,6 @@ async def enable_key(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""启用激活码"""
|
||||
from app.models import KeyStatus
|
||||
key = KeyService.get_by_id(db, key_id)
|
||||
if not key:
|
||||
raise HTTPException(status_code=404, detail="激活码不存在")
|
||||
@@ -788,7 +811,6 @@ async def batch_enable_keys(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""批量启用激活码"""
|
||||
from app.models import KeyStatus
|
||||
success = 0
|
||||
failed = 0
|
||||
|
||||
@@ -818,7 +840,6 @@ async def batch_disable_keys(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""批量禁用激活码"""
|
||||
from app.models import KeyStatus
|
||||
success = 0
|
||||
failed = 0
|
||||
|
||||
@@ -877,7 +898,6 @@ async def get_keys_count(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""获取激活码总数(支持筛选)"""
|
||||
from app.models import KeyStatus
|
||||
query = db.query(ActivationKey)
|
||||
|
||||
# 搜索激活码
|
||||
@@ -895,9 +915,9 @@ async def get_keys_count(
|
||||
elif activated and activated == "false":
|
||||
query = query.filter(ActivationKey.first_activated_at == None)
|
||||
|
||||
# 套餐类型筛选
|
||||
# 套餐类型筛选 (free/auto -> AUTO, pro -> PRO)
|
||||
if membership_type:
|
||||
mt = MembershipType.PRO if membership_type.lower() == "pro" else MembershipType.FREE
|
||||
mt = KeyMembershipType.PRO if membership_type.lower() == "pro" else KeyMembershipType.AUTO
|
||||
query = query.filter(ActivationKey.membership_type == mt)
|
||||
|
||||
total = query.count()
|
||||
@@ -962,7 +982,7 @@ async def batch_extend_keys(
|
||||
|
||||
@router.post("/keys/batch-compensate")
|
||||
async def batch_compensate(
|
||||
membership_type: Optional[str] = Query(None, description="套餐类型: pro/free"),
|
||||
membership_type: Optional[str] = Query(None, description="套餐类型: pro/auto"),
|
||||
activated_before: Optional[str] = Query(None, description="在此日期之前激活 (YYYY-MM-DD)"),
|
||||
not_expired_on: Optional[str] = Query(None, description="在此日期时还未过期 (YYYY-MM-DD)"),
|
||||
extend_days: int = Query(0, description="延长天数"),
|
||||
@@ -981,11 +1001,11 @@ async def batch_compensate(
|
||||
- 如果卡已过期(但符合补偿条件):恢复使用,expire_at = 今天 + extend_days
|
||||
|
||||
例如: 补偿12月4号之前激活、12月4号还没过期的Auto密钥,延长1天
|
||||
POST /admin/keys/batch-compensate?membership_type=free&activated_before=2024-12-05¬_expired_on=2024-12-04&extend_days=1
|
||||
POST /admin/keys/batch-compensate?membership_type=auto&activated_before=2024-12-05¬_expired_on=2024-12-04&extend_days=1
|
||||
"""
|
||||
mt = None
|
||||
if membership_type:
|
||||
mt = MembershipType.PRO if membership_type.lower() == "pro" else MembershipType.FREE
|
||||
mt = KeyMembershipType.PRO if membership_type.lower() == "pro" else KeyMembershipType.AUTO
|
||||
|
||||
activated_before_dt = datetime.strptime(activated_before, "%Y-%m-%d") if activated_before else None
|
||||
not_expired_on_dt = datetime.strptime(not_expired_on, "%Y-%m-%d") if not_expired_on else None
|
||||
@@ -1003,7 +1023,7 @@ async def batch_compensate(
|
||||
|
||||
@router.get("/keys/preview-compensate")
|
||||
async def preview_compensate(
|
||||
membership_type: Optional[str] = Query(None, description="套餐类型: pro/free"),
|
||||
membership_type: Optional[str] = Query(None, description="套餐类型: pro/auto"),
|
||||
activated_before: Optional[str] = Query(None, description="在此日期之前激活 (YYYY-MM-DD)"),
|
||||
not_expired_on: Optional[str] = Query(None, description="在此日期时还未过期 (YYYY-MM-DD)"),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -1012,7 +1032,7 @@ async def preview_compensate(
|
||||
"""预览补偿 - 查看符合条件的密钥数量(不执行)"""
|
||||
mt = None
|
||||
if membership_type:
|
||||
mt = MembershipType.PRO if membership_type.lower() == "pro" else MembershipType.FREE
|
||||
mt = KeyMembershipType.PRO if membership_type.lower() == "pro" else KeyMembershipType.AUTO
|
||||
|
||||
activated_before_dt = datetime.strptime(activated_before, "%Y-%m-%d") if activated_before else None
|
||||
not_expired_on_dt = datetime.strptime(not_expired_on, "%Y-%m-%d") if not_expired_on else None
|
||||
@@ -1140,3 +1160,295 @@ async def get_key_logs(
|
||||
"message": log.message,
|
||||
"created_at": log.created_at.strftime("%Y-%m-%d %H:%M:%S") if log.created_at else None
|
||||
} for log in logs]
|
||||
|
||||
|
||||
# ========== 账号分析 (Analysis) ==========
|
||||
|
||||
@router.post("/accounts/{account_id}/analyze")
|
||||
async def analyze_account(
|
||||
account_id: int,
|
||||
payload: Optional[AccountAnalyzeRequest] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""手动触发单个账号分析"""
|
||||
from app.services import analyze_account_from_token
|
||||
|
||||
account = AccountService.get_by_id(db, account_id)
|
||||
if not account:
|
||||
raise HTTPException(status_code=404, detail="账号不存在")
|
||||
|
||||
override_token = payload.token.strip() if payload and payload.token else None
|
||||
token_to_use = override_token or account.token
|
||||
|
||||
if not token_to_use:
|
||||
raise HTTPException(status_code=400, detail="账号未保存Token,请提供检测Token")
|
||||
|
||||
# 执行分析
|
||||
result = await analyze_account_from_token(token_to_use)
|
||||
|
||||
if result.get("success"):
|
||||
# 更新账号信息
|
||||
if override_token and payload.save_token:
|
||||
AccountService.update(
|
||||
db,
|
||||
account_id,
|
||||
token=override_token,
|
||||
workos_session_token=override_token
|
||||
)
|
||||
AccountService.update_from_analysis(db, account_id, result)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "分析完成",
|
||||
"data": result
|
||||
}
|
||||
else:
|
||||
# 记录错误
|
||||
AccountService.update_from_analysis(db, account_id, {
|
||||
"success": False,
|
||||
"error": result.get("error", "分析失败")
|
||||
})
|
||||
return {
|
||||
"success": False,
|
||||
"message": result.get("error", "分析失败")
|
||||
}
|
||||
|
||||
|
||||
@router.post("/accounts/batch-analyze")
|
||||
async def batch_analyze_accounts(
|
||||
account_ids: List[int] = None,
|
||||
analyze_all_pending: bool = False,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""批量分析账号
|
||||
|
||||
参数:
|
||||
- account_ids: 指定账号ID列表
|
||||
- analyze_all_pending: 分析所有待分析的账号
|
||||
"""
|
||||
from app.services import analyze_account_from_token
|
||||
|
||||
accounts = []
|
||||
if analyze_all_pending:
|
||||
accounts = AccountService.get_pending_accounts(db, limit=50)
|
||||
elif account_ids:
|
||||
for aid in account_ids:
|
||||
acc = AccountService.get_by_id(db, aid)
|
||||
if acc:
|
||||
accounts.append(acc)
|
||||
|
||||
if not accounts:
|
||||
return {"success": 0, "failed": 0, "message": "没有找到要分析的账号"}
|
||||
|
||||
success = 0
|
||||
failed = 0
|
||||
results = []
|
||||
|
||||
for account in accounts:
|
||||
try:
|
||||
result = await analyze_account_from_token(account.token)
|
||||
if result.get("success"):
|
||||
AccountService.update_from_analysis(db, account.id, result)
|
||||
success += 1
|
||||
results.append({
|
||||
"id": account.id,
|
||||
"email": account.email,
|
||||
"success": True,
|
||||
"account_type": result.get("account_type"),
|
||||
"usage_percent": result.get("usage_percent")
|
||||
})
|
||||
else:
|
||||
AccountService.update_from_analysis(db, account.id, {
|
||||
"success": False,
|
||||
"error": result.get("error", "分析失败")
|
||||
})
|
||||
failed += 1
|
||||
results.append({
|
||||
"id": account.id,
|
||||
"email": account.email,
|
||||
"success": False,
|
||||
"error": result.get("error")
|
||||
})
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
results.append({
|
||||
"id": account.id,
|
||||
"email": account.email,
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"failed": failed,
|
||||
"total": len(accounts),
|
||||
"results": results[:20], # 只返回前20条
|
||||
"message": f"分析完成: {success} 成功, {failed} 失败"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/settings/toggle-auto-analyze")
|
||||
async def toggle_auto_analyze(
|
||||
enabled: bool,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""开启/关闭自动账号分析"""
|
||||
GlobalSettingsService.set(db, "auto_analyze_enabled", str(enabled).lower())
|
||||
return {
|
||||
"success": True,
|
||||
"auto_analyze_enabled": enabled,
|
||||
"message": f"自动分析已{'开启' if enabled else '关闭'}"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/settings/toggle-auto-switch")
|
||||
async def toggle_auto_switch(
|
||||
enabled: bool,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""开启/关闭自动换号"""
|
||||
GlobalSettingsService.set(db, "auto_switch_enabled", str(enabled).lower())
|
||||
return {
|
||||
"success": True,
|
||||
"auto_switch_enabled": enabled,
|
||||
"message": f"自动换号已{'开启' if enabled else '关闭'}"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/accounts/analysis-status")
|
||||
async def get_analysis_status(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""获取账号分析状态概览"""
|
||||
stats = AccountService.count(db)
|
||||
auto_analyze = GlobalSettingsService.get(db, "auto_analyze_enabled")
|
||||
auto_switch = GlobalSettingsService.get(db, "auto_switch_enabled")
|
||||
|
||||
return {
|
||||
"auto_analyze_enabled": auto_analyze == "true",
|
||||
"auto_switch_enabled": auto_switch == "true",
|
||||
"total_accounts": stats["total"],
|
||||
"pending_analysis": stats["pending"],
|
||||
"available": stats["available"],
|
||||
"in_use": stats["in_use"],
|
||||
"exhausted": stats["exhausted"],
|
||||
"invalid": stats["invalid"]
|
||||
}
|
||||
|
||||
|
||||
# ========== 公告管理 ==========
|
||||
|
||||
@router.get("/announcements")
|
||||
async def list_announcements(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""获取所有公告(含禁用的)"""
|
||||
announcements = db.query(Announcement).order_by(Announcement.id.desc()).all()
|
||||
return [
|
||||
{
|
||||
"id": a.id,
|
||||
"title": a.title,
|
||||
"content": a.content,
|
||||
"type": a.type,
|
||||
"is_active": a.is_active,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
"updated_at": a.updated_at.isoformat() if a.updated_at else None,
|
||||
}
|
||||
for a in announcements
|
||||
]
|
||||
|
||||
|
||||
@router.post("/announcements")
|
||||
async def create_announcement(
|
||||
data: AnnouncementCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""创建公告"""
|
||||
announcement = Announcement(
|
||||
title=data.title,
|
||||
content=data.content,
|
||||
type=data.type,
|
||||
is_active=data.is_active,
|
||||
)
|
||||
db.add(announcement)
|
||||
db.commit()
|
||||
db.refresh(announcement)
|
||||
return {
|
||||
"id": announcement.id,
|
||||
"title": announcement.title,
|
||||
"content": announcement.content,
|
||||
"type": announcement.type,
|
||||
"is_active": announcement.is_active,
|
||||
"created_at": announcement.created_at.isoformat() if announcement.created_at else None,
|
||||
"updated_at": announcement.updated_at.isoformat() if announcement.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/announcements/{announcement_id}")
|
||||
async def update_announcement(
|
||||
announcement_id: int,
|
||||
data: AnnouncementUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""编辑公告"""
|
||||
announcement = db.query(Announcement).filter(Announcement.id == announcement_id).first()
|
||||
if not announcement:
|
||||
raise HTTPException(status_code=404, detail="公告不存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(announcement, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(announcement)
|
||||
return {
|
||||
"id": announcement.id,
|
||||
"title": announcement.title,
|
||||
"content": announcement.content,
|
||||
"type": announcement.type,
|
||||
"is_active": announcement.is_active,
|
||||
"created_at": announcement.created_at.isoformat() if announcement.created_at else None,
|
||||
"updated_at": announcement.updated_at.isoformat() if announcement.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/announcements/{announcement_id}")
|
||||
async def delete_announcement(
|
||||
announcement_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""删除公告"""
|
||||
announcement = db.query(Announcement).filter(Announcement.id == announcement_id).first()
|
||||
if not announcement:
|
||||
raise HTTPException(status_code=404, detail="公告不存在")
|
||||
db.delete(announcement)
|
||||
db.commit()
|
||||
return {"success": True, "message": "公告已删除"}
|
||||
|
||||
|
||||
@router.post("/announcements/{announcement_id}/toggle")
|
||||
async def toggle_announcement(
|
||||
announcement_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""启用/禁用公告"""
|
||||
announcement = db.query(Announcement).filter(Announcement.id == announcement_id).first()
|
||||
if not announcement:
|
||||
raise HTTPException(status_code=404, detail="公告不存在")
|
||||
announcement.is_active = not announcement.is_active
|
||||
db.commit()
|
||||
db.refresh(announcement)
|
||||
return {
|
||||
"id": announcement.id,
|
||||
"is_active": announcement.is_active,
|
||||
"message": "公告已启用" if announcement.is_active else "公告已禁用"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user