""" 管理后台 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]