""" 管理后台 API """ from datetime import datetime from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status, Query, Header 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 import ( AccountCreate, AccountUpdate, AccountResponse, AccountImport, KeyCreate, KeyUpdate, KeyResponse, DashboardStats, Token, LoginRequest, GlobalSettingsResponse, GlobalSettingsUpdate, BatchExtendRequest, BatchExtendResponse, ExternalBatchUpload, ExternalBatchResponse ) 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) # ========== 外部系统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 mt = MembershipType.FREE if item.membership_type == "free" else MembershipType.PRO existing = AccountService.get_by_email(db, item.email) if existing: if data.update_existing: # 更新已存在的账号 AccountService.update( db, existing.id, access_token=item.access_token, refresh_token=item.refresh_token, workos_session_token=item.workos_session_token, membership_type=mt, remark=item.remark or existing.remark ) updated += 1 else: failed += 1 errors.append(f"{item.email}: 账号已存在") else: # 创建新账号 account_data = AccountCreate( email=item.email, access_token=item.access_token, refresh_token=item.refresh_token, workos_session_token=item.workos_session_token, membership_type=mt, remark=item.remark ) AccountService.create(db, account_data) 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) return { "total": stats["total"], "active": stats["active"], "pro": stats["pro"], "free": stats["total"] - stats["pro"] } @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["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/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) """ from app.models import AccountStatus, Account account = db.query(Account).filter(Account.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.ACTIVE account.current_key_id = None # 释放绑定 elif account.status == AccountStatus.ACTIVE: account.status = AccountStatus.DISABLED elif account.status == AccountStatus.DISABLED: account.status = AccountStatus.ACTIVE elif account.status == AccountStatus.EXPIRED: account.status = AccountStatus.ACTIVE 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) ): """释放账号(从使用中变为可用)""" from app.models import AccountStatus, Account account = db.query(Account).filter(Account.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.ACTIVE account.current_key_id = 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) ): """批量启用账号""" from app.models import AccountStatus, Account success = 0 failed = 0 for account_id in account_ids: try: account = db.query(Account).filter(Account.id == account_id).first() if account: account.status = AccountStatus.ACTIVE 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) ): """批量禁用账号""" from app.models import AccountStatus, Account success = 0 failed = 0 for account_id in account_ids: try: account = db.query(Account).filter(Account.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("/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("/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) ): """获取激活码列表(支持搜索和筛选)""" 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: 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) # 套餐类型筛选 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.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) ): """获取激活码总数(支持筛选)""" from app.models import KeyStatus 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) # 套餐类型筛选 if membership_type: mt = MembershipType.PRO if membership_type.lower() == "pro" else MembershipType.FREE 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, 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.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) ): """禁用激活码(返回使用信息供客服参考)""" 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/batch-enable") async def batch_enable_keys( key_ids: List[int], db: Session = Depends(get_db), current_user: dict = Depends(get_current_user) ): """批量启用激活码""" from app.models import KeyStatus 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) ): """批量禁用激活码""" from app.models import KeyStatus 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) ): """获取激活码总数(支持筛选)""" from app.models import KeyStatus 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) # 套餐类型筛选 if membership_type: mt = MembershipType.PRO if membership_type.lower() == "pro" else MembershipType.FREE 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/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]