CursorPro 后台管理系统 v1.0

功能:
- 激活码管理 (Pro/Auto 两种类型)
- 账号池管理
- 设备绑定记录
- 使用日志
- 搜索/筛选功能
- 禁用/启用功能 (支持退款参考)
- 全局设置 (换号间隔、额度消耗等)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ccdojox-crypto
2025-12-16 20:54:44 +08:00
commit 9e2333c90c
62 changed files with 9567 additions and 0 deletions

582
backend/app/api/admin.py Normal file
View File

@@ -0,0 +1,582 @@
"""
管理后台 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]