Files
ai-agent-admin/backend-fastapi/core/device/api.py
T
2026-06-08 18:14:59 +08:00

250 lines
7.6 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
设备管理 API
"""
from fastapi import APIRouter, Depends, HTTPException, status, Request
from utils.redis import RedisClient
from utils.client_info import get_device_id, get_client_info
from utils.security import get_current_user
from app.base_schema import ResponseModel
from core.device.schema import DeviceInfo, DeviceListResponse, DeviceRenameRequest
from datetime import datetime, timezone
router = APIRouter(prefix="/devices", tags=["设备管理"])
# Redis中存储refresh token的key前缀
REFRESH_TOKEN_PREFIX = "refresh_token:"
# Redis中存储设备信息的key前缀
DEVICE_INFO_PREFIX = "device_info:"
@router.get("", response_model=DeviceListResponse, summary="获取设备列表")
async def get_devices(
request: Request,
current_user=Depends(get_current_user)
):
"""
获取当前用户的所有登录设备
返回:
- current_device: 当前设备信息
- online_devices: 其他在线设备列表
- total_count: 设备总数
"""
user_id = current_user.id
current_device_id = get_device_id(request)
redis = await RedisClient.get_client()
# 查找该用户的所有refresh token
pattern = f"{REFRESH_TOKEN_PREFIX}{user_id}:*"
cursor = 0
devices = []
while True:
cursor, keys = await redis.scan(cursor, match=pattern, count=100)
for key in keys:
# 提取device_id
device_id = key.split(":")[-1]
# 检查refresh token是否存在
token_exists = await redis.exists(key)
if not token_exists:
continue
# 获取设备信息
device_info_key = f"{DEVICE_INFO_PREFIX}{user_id}:{device_id}"
device_data = await redis.hgetall(device_info_key)
# 检查 access token 是否存在(判断在线状态)
access_token_key = f"access_token:{user_id}:{device_id}"
is_online = await redis.exists(access_token_key)
# 构建设备信息
device = DeviceInfo(
device_id=device_id,
device_name=device_data.get("device_name"),
device_type=device_data.get("device_type"),
browser_type=device_data.get("browser_type"),
os_type=device_data.get("os_type"),
ip_address=device_data.get("ip_address"),
last_active_time=datetime.fromisoformat(device_data["last_active_time"]) if device_data.get("last_active_time") else None,
is_current=(device_id == current_device_id),
is_online=bool(is_online)
)
devices.append(device)
if cursor == 0:
break
# 分离当前设备和其他设备
current_device = None
online_devices = []
for device in devices:
if device.is_current:
current_device = device
else:
online_devices.append(device)
# 按最后活跃时间排序
online_devices.sort(key=lambda x: x.last_active_time or datetime.min, reverse=True)
return DeviceListResponse(
current_device=current_device,
online_devices=online_devices,
total_count=len(devices)
)
@router.delete("/{device_id}", response_model=ResponseModel, summary="强制登出指定设备")
async def logout_device(
device_id: str,
request: Request,
current_user=Depends(get_current_user)
):
"""
强制登出指定设备
删除该设备的refresh token,使其下次刷新时失效
"""
user_id = current_user.id
current_device_id = get_device_id(request)
# 不能登出当前设备
if device_id == current_device_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="不能登出当前设备,请使用登出功能"
)
redis = await RedisClient.get_client()
# 删除指定设备的 refresh token 和 access token
token_key = f"{REFRESH_TOKEN_PREFIX}{user_id}:{device_id}"
access_token_key = f"access_token:{user_id}:{device_id}"
deleted = await redis.delete(token_key)
await redis.delete(access_token_key)
if deleted == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在或已登出"
)
# 删除设备信息
device_info_key = f"{DEVICE_INFO_PREFIX}{user_id}:{device_id}"
await redis.delete(device_info_key)
return ResponseModel(message="设备已强制登出")
@router.delete("/logout-others", response_model=ResponseModel, summary="登出其他所有设备")
async def logout_other_devices(
request: Request,
current_user=Depends(get_current_user)
):
"""
登出除当前设备外的所有其他设备
"""
user_id = current_user.id
current_device_id = get_device_id(request)
redis = await RedisClient.get_client()
# 查找该用户的所有refresh token
pattern = f"{REFRESH_TOKEN_PREFIX}{user_id}:*"
cursor = 0
deleted_count = 0
while True:
cursor, keys = await redis.scan(cursor, match=pattern, count=100)
for key in keys:
# 提取device_id
device_id = key.split(":")[-1]
# 跳过当前设备
if device_id == current_device_id:
continue
# 删除其他设备的 refresh token 和 access token
await redis.delete(key)
access_token_key = f"access_token:{user_id}:{device_id}"
await redis.delete(access_token_key)
# 删除设备信息
device_info_key = f"{DEVICE_INFO_PREFIX}{user_id}:{device_id}"
await redis.delete(device_info_key)
deleted_count += 1
if cursor == 0:
break
return ResponseModel(message=f"已登出 {deleted_count} 台其他设备")
@router.post("/{device_id}/rename", response_model=ResponseModel, summary="重命名设备")
async def rename_device(
device_id: str,
data: DeviceRenameRequest,
current_user=Depends(get_current_user)
):
"""
为设备设置自定义名称
"""
user_id = current_user.id
redis = await RedisClient.get_client()
# 检查设备是否存在
token_key = f"{REFRESH_TOKEN_PREFIX}{user_id}:{device_id}"
exists = await redis.exists(token_key)
if not exists:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
# 更新设备名称
device_info_key = f"{DEVICE_INFO_PREFIX}{user_id}:{device_id}"
await redis.hset(device_info_key, "device_name", data.device_name)
return ResponseModel(message="设备名称已更新")
@router.get("/statistics", response_model=ResponseModel, summary="获取设备统计信息")
async def get_device_statistics(
current_user=Depends(get_current_user)
):
"""
获取设备统计信息
"""
user_id = current_user.id
redis = await RedisClient.get_client()
# 统计在线设备数量
pattern = f"{REFRESH_TOKEN_PREFIX}{user_id}:*"
cursor = 0
online_count = 0
while True:
cursor, keys = await redis.scan(cursor, match=pattern, count=100)
online_count += len(keys)
if cursor == 0:
break
return ResponseModel(
message="获取成功",
data={
"online_count": online_count,
"total_count": online_count # 暂时只统计在线设备
}
)