Files
cursornew2026/backend/app/api/admin.py
ccdojox-crypto 9e2333c90c CursorPro 后台管理系统 v1.0
功能:
- 激活码管理 (Pro/Auto 两种类型)
- 账号池管理
- 设备绑定记录
- 使用日志
- 搜索/筛选功能
- 禁用/启用功能 (支持退款参考)
- 全局设置 (换号间隔、额度消耗等)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 20:54:44 +08:00

583 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
管理后台 API
"""
from datetime import datetime
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.services import AccountService, KeyService, LogService, GlobalSettingsService, BatchService, authenticate_admin, create_access_token, get_current_user
from app.schemas import (
AccountCreate, AccountUpdate, AccountResponse, AccountImport,
KeyCreate, KeyUpdate, KeyResponse,
DashboardStats, Token, LoginRequest,
GlobalSettingsResponse, GlobalSettingsUpdate,
BatchExtendRequest, BatchExtendResponse
)
from app.models import MembershipType, KeyDevice, UsageLog, ActivationKey
router = APIRouter(prefix="/admin", tags=["Admin API"])
# ========== 认证 ==========
@router.post("/login", response_model=Token)
async def login(request: LoginRequest):
"""管理员登录"""
if not authenticate_admin(request.username, request.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误"
)
access_token = create_access_token(data={"sub": request.username})
return Token(access_token=access_token)
# ========== 仪表盘 ==========
@router.get("/dashboard", response_model=DashboardStats)
async def get_dashboard(
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取仪表盘统计"""
account_stats = AccountService.count(db)
key_stats = KeyService.count(db)
today_usage = LogService.get_today_count(db)
return DashboardStats(
total_accounts=account_stats["total"],
active_accounts=account_stats["active"],
pro_accounts=account_stats["pro"],
total_keys=key_stats["total"],
active_keys=key_stats["active"],
today_usage=today_usage
)
# ========== 账号管理 ==========
@router.get("/accounts", response_model=List[AccountResponse])
async def list_accounts(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取账号列表"""
return AccountService.get_all(db, skip, limit)
@router.post("/accounts", response_model=AccountResponse)
async def create_account(
account: AccountCreate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""创建账号"""
existing = AccountService.get_by_email(db, account.email)
if existing:
raise HTTPException(status_code=400, detail="邮箱已存在")
return AccountService.create(db, account)
@router.post("/accounts/import", response_model=dict)
async def import_accounts(
data: AccountImport,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""批量导入账号"""
success = 0
failed = 0
errors = []
for account in data.accounts:
try:
existing = AccountService.get_by_email(db, account.email)
if existing:
# 更新已存在的账号
AccountService.update(
db, existing.id,
access_token=account.access_token,
refresh_token=account.refresh_token,
workos_session_token=account.workos_session_token,
membership_type=account.membership_type
)
else:
AccountService.create(db, account)
success += 1
except Exception as e:
failed += 1
errors.append(f"{account.email}: {str(e)}")
return {
"success": success,
"failed": failed,
"errors": errors[:10] # 只返回前10个错误
}
@router.get("/accounts/{account_id}", response_model=AccountResponse)
async def get_account(
account_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取账号详情"""
account = AccountService.get_by_id(db, account_id)
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
return account
@router.put("/accounts/{account_id}", response_model=AccountResponse)
async def update_account(
account_id: int,
account: AccountUpdate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""更新账号"""
updated = AccountService.update(db, account_id, **account.model_dump(exclude_unset=True))
if not updated:
raise HTTPException(status_code=404, detail="账号不存在")
return updated
@router.delete("/accounts/{account_id}")
async def delete_account(
account_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""删除账号"""
if not AccountService.delete(db, account_id):
raise HTTPException(status_code=404, detail="账号不存在")
return {"message": "删除成功"}
# ========== 激活码管理 ==========
@router.get("/keys", response_model=List[KeyResponse])
async def list_keys(
skip: int = 0,
limit: int = 100,
search: Optional[str] = Query(None, description="搜索激活码"),
status: Optional[str] = Query(None, description="状态筛选: active/disabled"),
activated: Optional[bool] = Query(None, description="是否已激活"),
membership_type: Optional[str] = Query(None, description="套餐类型: pro/free"),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取激活码列表(支持搜索和筛选)"""
from app.models import KeyStatus
query = db.query(ActivationKey).order_by(ActivationKey.id.desc())
# 搜索激活码
if search:
query = query.filter(ActivationKey.key.contains(search))
# 状态筛选
if status:
query = query.filter(ActivationKey.status == status)
# 是否已激活
if activated is True:
query = query.filter(ActivationKey.first_activated_at != None)
elif activated is False:
query = query.filter(ActivationKey.first_activated_at == None)
# 套餐类型筛选
if membership_type:
mt = MembershipType.PRO if membership_type.lower() == "pro" else MembershipType.FREE
query = query.filter(ActivationKey.membership_type == mt)
return query.offset(skip).limit(limit).all()
@router.post("/keys", response_model=List[KeyResponse])
async def create_keys(
key_data: KeyCreate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""创建激活码"""
return KeyService.create(db, key_data)
@router.get("/keys/{key_id}", response_model=KeyResponse)
async def get_key(
key_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取激活码详情"""
key = KeyService.get_by_id(db, key_id)
if not key:
raise HTTPException(status_code=404, detail="激活码不存在")
return key
@router.put("/keys/{key_id}", response_model=KeyResponse)
async def update_key(
key_id: int,
key_data: KeyUpdate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""更新激活码"""
updated = KeyService.update(db, key_id, **key_data.model_dump(exclude_unset=True))
if not updated:
raise HTTPException(status_code=404, detail="激活码不存在")
return updated
@router.delete("/keys/{key_id}")
async def delete_key(
key_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""删除激活码"""
if not KeyService.delete(db, key_id):
raise HTTPException(status_code=404, detail="激活码不存在")
return {"message": "删除成功"}
@router.get("/keys/{key_id}/usage-info")
async def get_key_usage_info(
key_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取激活码使用信息(用于禁用/退款参考)"""
key = KeyService.get_by_id(db, key_id)
if not key:
raise HTTPException(status_code=404, detail="激活码不存在")
now = datetime.now()
usage_info = {
"key": key.key,
"membership_type": key.membership_type.value,
"status": key.status.value,
"is_activated": key.first_activated_at is not None,
"first_activated_at": key.first_activated_at.strftime("%Y-%m-%d %H:%M:%S") if key.first_activated_at else None,
"expire_at": key.expire_at.strftime("%Y-%m-%d %H:%M:%S") if key.expire_at else None,
"valid_days": key.valid_days,
"used_days": 0,
"remaining_days": 0,
"switch_count": key.switch_count,
"quota": key.quota,
"quota_used": key.quota_used,
"quota_remaining": key.quota - key.quota_used,
"device_count": db.query(KeyDevice).filter(KeyDevice.key_id == key_id).count(),
"max_devices": key.max_devices,
}
# 计算使用天数
if key.first_activated_at:
used_delta = now - key.first_activated_at
usage_info["used_days"] = used_delta.days
if key.expire_at:
if key.expire_at > now:
remaining_delta = key.expire_at - now
usage_info["remaining_days"] = remaining_delta.days
else:
usage_info["remaining_days"] = 0
usage_info["is_expired"] = True
return usage_info
@router.post("/keys/{key_id}/disable")
async def disable_key(
key_id: int,
db: Session = Depends(get_db),
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="激活码不存在")
now = datetime.now()
# 计算使用信息
used_days = 0
if key.first_activated_at:
used_delta = now - key.first_activated_at
used_days = used_delta.days
# 禁用
key.status = KeyStatus.DISABLED
db.commit()
return {
"message": "激活码已禁用",
"key": key.key,
"membership_type": key.membership_type.value,
"used_days": used_days,
"switch_count": key.switch_count,
"quota_used": key.quota_used,
"first_activated_at": key.first_activated_at.strftime("%Y-%m-%d %H:%M:%S") if key.first_activated_at else "未激活"
}
@router.post("/keys/{key_id}/enable")
async def enable_key(
key_id: int,
db: Session = Depends(get_db),
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="激活码不存在")
key.status = KeyStatus.ACTIVE
db.commit()
return {"message": "激活码已启用"}
@router.post("/keys/{key_id}/add-quota", response_model=KeyResponse)
async def add_key_quota(
key_id: int,
add_quota: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""叠加额度(只加额度不加时间)"""
key = KeyService.get_by_id(db, key_id)
if not key:
raise HTTPException(status_code=404, detail="激活码不存在")
KeyService.add_quota(db, key, add_quota)
db.refresh(key)
return key
# ========== 全局设置 ==========
@router.get("/settings", response_model=GlobalSettingsResponse)
async def get_settings(
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取全局设置"""
# 初始化默认设置(如果不存在)
GlobalSettingsService.init_settings(db)
return GlobalSettingsService.get_all(db)
@router.put("/settings", response_model=GlobalSettingsResponse)
async def update_settings(
settings: GlobalSettingsUpdate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""更新全局设置"""
GlobalSettingsService.update_all(db, **settings.model_dump(exclude_unset=True))
return GlobalSettingsService.get_all(db)
# ========== 批量操作 ==========
@router.post("/keys/batch-extend", response_model=BatchExtendResponse)
async def batch_extend_keys(
request: BatchExtendRequest,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""批量延长密钥指定ID列表
- extend_days: 延长天数Auto和Pro都可用
- add_quota: 增加额度仅Pro有效
"""
result = BatchService.extend_keys(db, request.key_ids, request.extend_days, request.add_quota)
return BatchExtendResponse(**result)
@router.post("/keys/batch-compensate")
async def batch_compensate(
membership_type: Optional[str] = Query(None, description="套餐类型: pro/free"),
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="延长天数"),
add_quota: int = Query(0, description="增加额度(仅Pro)"),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""批量补偿 - 根据条件筛选密钥并补偿
筛选条件:
- activated_before: 在此日期之前激活的
- not_expired_on: 在此日期时还未过期的
补偿逻辑:
- 如果卡当前还没过期expire_at += extend_days
- 如果卡已过期但符合补偿条件恢复使用expire_at = 今天 + extend_days
例如: 补偿12月4号之前激活、12月4号还没过期的Auto密钥延长1天
POST /admin/keys/batch-compensate?membership_type=free&activated_before=2024-12-05&not_expired_on=2024-12-04&extend_days=1
"""
mt = None
if membership_type:
mt = MembershipType.PRO if membership_type.lower() == "pro" else MembershipType.FREE
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
result = BatchService.batch_compensate(
db,
membership_type=mt,
activated_before=activated_before_dt,
not_expired_on=not_expired_on_dt,
extend_days=extend_days,
add_quota=add_quota
)
return result
@router.get("/keys/preview-compensate")
async def preview_compensate(
membership_type: Optional[str] = Query(None, description="套餐类型: pro/free"),
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),
current_user: dict = Depends(get_current_user)
):
"""预览补偿 - 查看符合条件的密钥数量(不执行)"""
mt = None
if membership_type:
mt = MembershipType.PRO if membership_type.lower() == "pro" else MembershipType.FREE
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
keys = BatchService.get_keys_for_compensation(
db,
membership_type=mt,
activated_before=activated_before_dt,
not_expired_on=not_expired_on_dt
)
now = datetime.now()
return {
"total_matched": len(keys),
"keys": [{"id": k.id, "key": k.key[:8] + "...", "membership_type": k.membership_type.value,
"expire_at": k.expire_at.strftime("%Y-%m-%d %H:%M") if k.expire_at else "永久",
"activated_at": k.first_activated_at.strftime("%Y-%m-%d") if k.first_activated_at else "-",
"is_expired": k.expire_at < now if k.expire_at else False} for k in keys[:20]],
"message": f"共找到 {len(keys)} 个符合条件的密钥" + ("仅显示前20个" if len(keys) > 20 else "")
}
# ========== 设备记录 ==========
@router.get("/keys/{key_id}/devices")
async def get_key_devices(
key_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取激活码绑定的设备列表"""
key = KeyService.get_by_id(db, key_id)
if not key:
raise HTTPException(status_code=404, detail="激活码不存在")
devices = db.query(KeyDevice).filter(KeyDevice.key_id == key_id).order_by(KeyDevice.created_at.desc()).all()
return [{
"id": d.id,
"device_id": d.device_id,
"device_name": d.device_name,
"last_active_at": d.last_active_at.strftime("%Y-%m-%d %H:%M:%S") if d.last_active_at else None,
"created_at": d.created_at.strftime("%Y-%m-%d %H:%M:%S") if d.created_at else None
} for d in devices]
@router.delete("/devices/{device_id}")
async def delete_device(
device_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""删除设备绑定"""
device = db.query(KeyDevice).filter(KeyDevice.id == device_id).first()
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
db.delete(device)
db.commit()
return {"message": "删除成功"}
# ========== 使用日志 (Usage Logs) ==========
@router.get("/logs")
async def get_logs(
key_id: Optional[int] = Query(None, description="按激活码ID筛选"),
action: Optional[str] = Query(None, description="操作类型: verify/switch"),
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取使用日志"""
query = db.query(UsageLog).order_by(UsageLog.created_at.desc())
if key_id:
query = query.filter(UsageLog.key_id == key_id)
if action:
query = query.filter(UsageLog.action == action)
logs = query.offset(skip).limit(limit).all()
# 获取关联的激活码信息
key_ids = list(set(log.key_id for log in logs))
keys_map = {}
if key_ids:
keys = db.query(ActivationKey).filter(ActivationKey.id.in_(key_ids)).all()
keys_map = {k.id: k.key[:8] + "..." for k in keys}
return [{
"id": log.id,
"key_id": log.key_id,
"key_preview": keys_map.get(log.key_id, "-"),
"account_id": log.account_id,
"action": log.action,
"ip_address": log.ip_address,
"success": log.success,
"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]
@router.get("/keys/{key_id}/logs")
async def get_key_logs(
key_id: int,
skip: int = 0,
limit: int = 50,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取指定激活码的使用日志"""
key = KeyService.get_by_id(db, key_id)
if not key:
raise HTTPException(status_code=404, detail="激活码不存在")
logs = db.query(UsageLog).filter(
UsageLog.key_id == key_id
).order_by(UsageLog.created_at.desc()).offset(skip).limit(limit).all()
return [{
"id": log.id,
"action": log.action,
"account_id": log.account_id,
"ip_address": log.ip_address,
"success": log.success,
"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]