功能: - 激活码管理 (Pro/Auto 两种类型) - 账号池管理 - 设备绑定记录 - 使用日志 - 搜索/筛选功能 - 禁用/启用功能 (支持退款参考) - 全局设置 (换号间隔、额度消耗等) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
583 lines
19 KiB
Python
583 lines
19 KiB
Python
"""
|
||
管理后台 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¬_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]
|