""" 管理后台 API """ 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.schemas import ( AccountCreate, AccountUpdate, AccountResponse, AccountImport, KeyCreate, KeyUpdate, KeyResponse, DashboardStats, Token, LoginRequest, GlobalSettingsResponse, GlobalSettingsUpdate, BatchExtendRequest, BatchExtendResponse, ExternalBatchUpload, ExternalBatchResponse, AnnouncementCreate, AnnouncementUpdate ) 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) 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) # ========== 外部系统API (Token认证) ========== def verify_api_token(x_api_token: str = Header(..., alias="X-API-Token")): """验证外部系统API Token""" if x_api_token != settings.API_TOKEN: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Token" ) return True @router.post("/external/accounts/batch", response_model=ExternalBatchResponse) async def external_batch_upload( data: ExternalBatchUpload, db: Session = Depends(get_db), _: bool = Depends(verify_api_token) ): """外部系统批量上传账号 使用方法: POST /admin/external/accounts/batch Headers: X-API-Token: your-api-token Body: { "accounts": [ { "email": "user@example.com", "access_token": "xxx", "refresh_token": "xxx", "workos_session_token": "xxx", "membership_type": "free", // free=auto账号, pro=高级账号 "remark": "备注" } ], "update_existing": true // 是否更新已存在的账号 } """ created = 0 updated = 0 failed = 0 errors = [] for item in data.accounts: try: # 转换membership_type (free/auto -> AUTO, pro -> PRO) # 注意:mt 变量暂未使用,因为 CursorAccount 模型中 membership_type 是从 Cursor API 分析得出的 existing = AccountService.get_by_email(db, item.email) if existing: if data.update_existing: # 更新已存在的账号 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, remark=item.remark or existing.remark ) updated += 1 else: failed += 1 errors.append(f"{item.email}: 账号已存在") else: # 创建新账号 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, password=None, remark=item.remark ) created += 1 except Exception as e: failed += 1 errors.append(f"{item.email}: {str(e)}") return ExternalBatchResponse( success=failed == 0, total=len(data.accounts), created=created, updated=updated, failed=failed, errors=errors[:20] # 只返回前20个错误 ) @router.get("/external/accounts/stats") async def external_account_stats( db: Session = Depends(get_db), _: bool = Depends(verify_api_token) ): """外部系统获取账号统计""" stats = AccountService.count(db) pro_count = db.query(CursorAccount).filter(CursorAccount.account_type == AccountType.PRO).count() return { "total": stats["total"], "active": stats["available"] + stats["in_use"], "pro": pro_count, "free": stats["total"] - pro_count } @router.delete("/external/accounts/batch") async def external_batch_delete( emails: List[str], db: Session = Depends(get_db), _: bool = Depends(verify_api_token) ): """外部系统批量删除账号""" deleted = 0 failed = 0 for email in emails: try: account = AccountService.get_by_email(db, email) if account: AccountService.delete(db, account.id) deleted += 1 else: failed += 1 except Exception: failed += 1 return { "success": failed == 0, "deleted": deleted, "failed": failed } # ========== 仪表盘 ========== @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["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 ) # ========== 账号管理 ========== @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, 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) 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, 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, password=account.password, remark=account.remark ) else: 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 errors.append(f"{account.email}: {str(e)}") return { "success": success, "failed": failed, "errors": errors[:10] # 只返回前10个错误 } @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"]} @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.post("/accounts/{account_id}/toggle-status") async def toggle_account_status( account_id: int, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """快捷切换账号状态 切换逻辑: - 使用中(in_use) -> 可用(active) 释放账号 - 可用(active) -> 禁用(disabled) - 禁用(disabled) -> 可用(active) - 过期(expired) -> 可用(active) """ account = db.query(CursorAccount).filter(CursorAccount.id == account_id).first() if not account: raise HTTPException(status_code=404, detail="账号不存在") old_status = account.status # 根据当前状态切换 if account.status == AccountStatus.IN_USE: 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.AVAILABLE elif account.status == AccountStatus.EXHAUSTED: account.status = AccountStatus.AVAILABLE db.commit() return { "success": True, "old_status": old_status.value, "new_status": account.status.value, "message": f"状态已从 {old_status.value} 切换为 {account.status.value}" } @router.post("/accounts/{account_id}/release") async def release_account( account_id: int, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """释放账号(从使用中变为可用)""" 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.AVAILABLE account.locked_by_key_id = None account.locked_at = None db.commit() return {"success": True, "message": "账号已释放"} @router.post("/accounts/batch-enable") async def batch_enable_accounts( account_ids: List[int], db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """批量启用账号""" success = 0 failed = 0 for account_id in account_ids: try: account = db.query(CursorAccount).filter(CursorAccount.id == account_id).first() if account: account.status = AccountStatus.AVAILABLE success += 1 else: failed += 1 except Exception: failed += 1 db.commit() return { "success": success, "failed": failed, "message": f"成功启用 {success} 个账号" } @router.post("/accounts/batch-disable") async def batch_disable_accounts( account_ids: List[int], db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """批量禁用账号""" success = 0 failed = 0 for account_id in account_ids: try: account = db.query(CursorAccount).filter(CursorAccount.id == account_id).first() if account: account.status = AccountStatus.DISABLED success += 1 else: failed += 1 except Exception: failed += 1 db.commit() return { "success": success, "failed": failed, "message": f"成功禁用 {success} 个账号" } @router.post("/accounts/batch-delete") async def batch_delete_accounts( account_ids: List[int], db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """批量删除账号""" success = 0 failed = 0 for account_id in account_ids: try: if AccountService.delete(db, account_id): success += 1 else: failed += 1 except Exception: failed += 1 return { "success": success, "failed": failed, "message": f"成功删除 {success} 个账号" } # ========== 激活码管理 ========== @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[str] = Query(None, description="是否已激活: true/false"), membership_type: Optional[str] = Query(None, description="套餐类型: pro/free"), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """获取激活码列表(支持搜索和筛选)""" query = db.query(ActivationKey).order_by(ActivationKey.id.desc()) # 搜索激活码 if search: query = query.filter(ActivationKey.key.contains(search)) # 状态筛选 if status: status_enum = KeyStatus.ACTIVE if status == "active" else KeyStatus.DISABLED query = query.filter(ActivationKey.status == status_enum) # 是否已激活 if activated and activated == "true": query = query.filter(ActivationKey.first_activated_at != None) elif activated and activated == "false": query = query.filter(ActivationKey.first_activated_at == None) # 套餐类型筛选 (free/auto -> AUTO, pro -> PRO) if membership_type: 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() @router.get("/keys/count") async def get_keys_count( search: Optional[str] = Query(None, description="搜索激活码"), status: Optional[str] = Query(None, description="状态筛选: active/disabled"), activated: Optional[str] = Query(None, description="是否已激活: true/false"), membership_type: Optional[str] = Query(None, description="套餐类型: pro/free"), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """获取激活码总数(支持筛选)""" query = db.query(ActivationKey) # 搜索激活码 if search: query = query.filter(ActivationKey.key.contains(search)) # 状态筛选 if status: status_enum = KeyStatus.ACTIVE if status == "active" else KeyStatus.DISABLED query = query.filter(ActivationKey.status == status_enum) # 是否已激活 if activated and activated == "true": query = query.filter(ActivationKey.first_activated_at != None) elif activated and activated == "false": query = query.filter(ActivationKey.first_activated_at == None) # 套餐类型筛选 (free/auto -> AUTO, pro -> PRO) if membership_type: mt = KeyMembershipType.PRO if membership_type.lower() == "pro" else KeyMembershipType.AUTO query = query.filter(ActivationKey.membership_type == mt) total = query.count() return {"total": total} @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, 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) 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.post("/keys/{key_id}/revoke") async def revoke_key( key_id: int, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """撤销激活码(从主密钥扣除资源)""" success, message = KeyService.revoke_key(db, key_id) if not success: raise HTTPException(status_code=400, detail=message) return {"success": True, "message": message} @router.get("/keys/by-device/{device_id}") async def get_keys_by_device( device_id: str, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """获取设备的所有密钥(管理后台用)""" keys_info = KeyService.get_device_keys(db, device_id) result = { "device_id": device_id, "auto": None, "pro": None } # Auto 密钥组 if keys_info["auto"]: auto_data = keys_info["auto"] master = auto_data["master"] merged_keys = auto_data["merged_keys"] all_keys = [{ "id": master.id, "key": master.key, "is_master": True, "status": master.status.value, "duration_days": master.duration_days, "activated_at": master.first_activated_at.strftime("%Y-%m-%d %H:%M:%S") if master.first_activated_at else None }] for k in merged_keys: all_keys.append({ "id": k.id, "key": k.key, "is_master": False, "status": k.status.value, "duration_days": k.duration_days, "merged_at": k.merged_at.strftime("%Y-%m-%d %H:%M:%S") if k.merged_at else None }) result["auto"] = { "total_keys": len(all_keys), "expire_at": master.expire_at.strftime("%Y-%m-%d %H:%M:%S") if master.expire_at else None, "current_account": master.current_account.email if master.current_account else None, "keys": all_keys } # Pro 密钥组 if keys_info["pro"]: pro_data = keys_info["pro"] master = pro_data["master"] merged_keys = pro_data["merged_keys"] all_keys = [{ "id": master.id, "key": master.key, "is_master": True, "status": master.status.value, "quota_contribution": master.quota_contribution, "activated_at": master.first_activated_at.strftime("%Y-%m-%d %H:%M:%S") if master.first_activated_at else None }] for k in merged_keys: all_keys.append({ "id": k.id, "key": k.key, "is_master": False, "status": k.status.value, "quota_contribution": k.quota_contribution, "merged_at": k.merged_at.strftime("%Y-%m-%d %H:%M:%S") if k.merged_at else None }) result["pro"] = { "total_keys": len(all_keys), "quota": pro_data["quota"], "quota_used": pro_data["quota_used"], "quota_remaining": pro_data["quota_remaining"], "current_account": master.current_account.email if master.current_account else None, "keys": all_keys } return result @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) ): """禁用激活码(返回使用信息供客服参考)""" 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) ): """启用激活码""" 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/batch-enable") async def batch_enable_keys( key_ids: List[int], db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """批量启用激活码""" success = 0 failed = 0 for key_id in key_ids: try: key = KeyService.get_by_id(db, key_id) if key: key.status = KeyStatus.ACTIVE success += 1 else: failed += 1 except Exception: failed += 1 db.commit() return { "success": success, "failed": failed, "message": f"成功启用 {success} 个激活码" } @router.post("/keys/batch-disable") async def batch_disable_keys( key_ids: List[int], db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """批量禁用激活码""" success = 0 failed = 0 for key_id in key_ids: try: key = KeyService.get_by_id(db, key_id) if key: key.status = KeyStatus.DISABLED success += 1 else: failed += 1 except Exception: failed += 1 db.commit() return { "success": success, "failed": failed, "message": f"成功禁用 {success} 个激活码" } @router.post("/keys/batch-delete") async def batch_delete_keys( key_ids: List[int], db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """批量删除激活码""" success = 0 failed = 0 for key_id in key_ids: try: if KeyService.delete(db, key_id): success += 1 else: failed += 1 except Exception: failed += 1 return { "success": success, "failed": failed, "message": f"成功删除 {success} 个激活码" } @router.get("/keys/count") async def get_keys_count( search: Optional[str] = Query(None, description="搜索激活码"), status: Optional[str] = Query(None, description="状态筛选: active/disabled"), activated: Optional[str] = Query(None, description="是否已激活: true/false"), membership_type: Optional[str] = Query(None, description="套餐类型: pro/free"), db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """获取激活码总数(支持筛选)""" query = db.query(ActivationKey) # 搜索激活码 if search: query = query.filter(ActivationKey.key.contains(search)) # 状态筛选 if status: status_enum = KeyStatus.ACTIVE if status == "active" else KeyStatus.DISABLED query = query.filter(ActivationKey.status == status_enum) # 是否已激活 if activated and activated == "true": query = query.filter(ActivationKey.first_activated_at != None) elif activated and activated == "false": query = query.filter(ActivationKey.first_activated_at == None) # 套餐类型筛选 (free/auto -> AUTO, pro -> PRO) if membership_type: mt = KeyMembershipType.PRO if membership_type.lower() == "pro" else KeyMembershipType.AUTO query = query.filter(ActivationKey.membership_type == mt) total = query.count() return {"total": total} @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/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="延长天数"), 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=auto&activated_before=2024-12-05¬_expired_on=2024-12-04&extend_days=1 """ mt = None if membership_type: 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 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/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), current_user: dict = Depends(get_current_user) ): """预览补偿 - 查看符合条件的密钥数量(不执行)""" mt = None if membership_type: 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 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] # ========== 账号分析 (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 "公告已禁用" }