330 lines
12 KiB
Python
330 lines
12 KiB
Python
import asyncio
|
||
import json
|
||
from datetime import date
|
||
|
||
from fastapi import APIRouter, Query, HTTPException, Request, Path
|
||
from mysql.connector import Error as MySQLError
|
||
from ds.db import db
|
||
from encryption.encrypt_decorator import encrypt_response
|
||
from schema.device_schema import (
|
||
DeviceCreateRequest, DeviceResponse, DeviceListResponse,
|
||
DeviceStatusHistoryResponse, DeviceStatusHistoryListResponse
|
||
)
|
||
from schema.response_schema import APIResponse
|
||
from service.device_service import update_online_status_by_ip
|
||
from ws.ws import get_current_time_str, aes_encrypt, is_client_connected
|
||
|
||
router = APIRouter(
|
||
prefix="/api/devices",
|
||
tags=["设备管理"]
|
||
)
|
||
|
||
|
||
# 创建设备信息接口
|
||
@router.post("/add", response_model=APIResponse, summary="创建设备信息")
|
||
@encrypt_response()
|
||
async def create_device(device_data: DeviceCreateRequest, request: Request):
|
||
conn = None
|
||
cursor = None
|
||
try:
|
||
conn = db.get_connection()
|
||
cursor = conn.cursor(dictionary=True)
|
||
|
||
# 检查设备是否已存在
|
||
cursor.execute("SELECT * FROM devices WHERE client_ip = %s", (device_data.ip,))
|
||
existing_device = cursor.fetchone()
|
||
if existing_device:
|
||
# 更新设备为在线状态
|
||
from service.device_service import update_online_status_by_ip
|
||
update_online_status_by_ip(client_ip=device_data.ip, online_status=1)
|
||
return APIResponse(
|
||
code=200,
|
||
message=f"设备IP {device_data.ip} 已存在、返回已有设备信息",
|
||
data=DeviceResponse(**existing_device)
|
||
)
|
||
|
||
# 通过 User-Agent 判断设备类型
|
||
user_agent = request.headers.get("User-Agent", "").lower()
|
||
device_type = "unknown"
|
||
if user_agent == "default":
|
||
device_type = device_data.params.get("os") if (device_data.params and isinstance(device_data.params, dict)) else "unknown"
|
||
elif "windows" in user_agent:
|
||
device_type = "windows"
|
||
elif "android" in user_agent:
|
||
device_type = "android"
|
||
elif "linux" in user_agent:
|
||
device_type = "linux"
|
||
|
||
device_params_json = json.dumps(device_data.params) if device_data.params else None
|
||
|
||
# 插入新设备
|
||
insert_query = """
|
||
INSERT INTO devices
|
||
(client_ip, hostname, device_online_status, device_type, alarm_count, params, is_need_handler)
|
||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||
"""
|
||
cursor.execute(insert_query, (
|
||
device_data.ip,
|
||
device_data.hostname,
|
||
0,
|
||
device_type,
|
||
0,
|
||
device_params_json,
|
||
0
|
||
))
|
||
conn.commit()
|
||
|
||
# 获取新设备并返回
|
||
device_id = cursor.lastrowid
|
||
cursor.execute("SELECT * FROM devices WHERE id = %s", (device_id,))
|
||
new_device = cursor.fetchone()
|
||
|
||
return APIResponse(
|
||
code=200,
|
||
message="设备创建成功",
|
||
data=DeviceResponse(**new_device)
|
||
)
|
||
|
||
except MySQLError as e:
|
||
if conn:
|
||
conn.rollback()
|
||
raise Exception(f"创建设备失败: {str(e)}") from e
|
||
except json.JSONDecodeError as e:
|
||
raise Exception(f"设备参数JSON序列化失败: {str(e)}") from e
|
||
except Exception as e:
|
||
if conn:
|
||
conn.rollback()
|
||
raise e
|
||
finally:
|
||
db.close_connection(conn, cursor)
|
||
|
||
|
||
# ------------------------------
|
||
# 获取设备列表接口
|
||
# ------------------------------
|
||
@router.get("/", response_model=APIResponse, summary="获取设备列表(支持筛选分页)")
|
||
@encrypt_response()
|
||
async def get_device_list(
|
||
page: int = Query(1, ge=1, description="页码、默认第1页"),
|
||
page_size: int = Query(10, ge=1, le=100, description="每页条数、1-100之间"),
|
||
device_type: str = Query(None, description="按设备类型筛选"),
|
||
online_status: int = Query(None, ge=0, le=1, description="按在线状态筛选")
|
||
):
|
||
conn = None
|
||
cursor = None
|
||
try:
|
||
conn = db.get_connection()
|
||
cursor = conn.cursor(dictionary=True)
|
||
|
||
where_clause = []
|
||
params = []
|
||
|
||
if device_type:
|
||
where_clause.append("device_type = %s")
|
||
params.append(device_type)
|
||
if online_status is not None:
|
||
where_clause.append("device_online_status = %s")
|
||
params.append(online_status)
|
||
|
||
# 统计总数
|
||
count_query = "SELECT COUNT(*) AS total FROM devices"
|
||
if where_clause:
|
||
count_query += " WHERE " + " AND ".join(where_clause)
|
||
cursor.execute(count_query, params)
|
||
total = cursor.fetchone()["total"]
|
||
|
||
# 分页查询列表
|
||
offset = (page - 1) * page_size
|
||
list_query = "SELECT * FROM devices"
|
||
if where_clause:
|
||
list_query += " WHERE " + " AND ".join(where_clause)
|
||
list_query += " ORDER BY id DESC LIMIT %s OFFSET %s"
|
||
params.extend([page_size, offset])
|
||
|
||
cursor.execute(list_query, params)
|
||
device_list = cursor.fetchall()
|
||
|
||
return APIResponse(
|
||
code=200,
|
||
message="获取设备列表成功",
|
||
data=DeviceListResponse(
|
||
total=total,
|
||
devices=[DeviceResponse(**device) for device in device_list]
|
||
)
|
||
)
|
||
|
||
except MySQLError as e:
|
||
raise Exception(f"获取设备列表失败: {str(e)}") from e
|
||
finally:
|
||
db.close_connection(conn, cursor)
|
||
|
||
|
||
# ------------------------------
|
||
# 获取设备上下线记录接口
|
||
# ------------------------------
|
||
@router.get("/status-history", response_model=APIResponse, summary="获取设备上下线记录")
|
||
@encrypt_response()
|
||
async def get_device_status_history(
|
||
client_ip: str = Query(None, description="客户端IP地址(非必填,为空时返回所有设备记录)"),
|
||
page: int = Query(1, ge=1, description="页码、默认第1页"),
|
||
page_size: int = Query(10, ge=1, le=100, description="每页条数、1-100之间"),
|
||
start_date: date = Query(None, description="开始日期、格式YYYY-MM-DD"),
|
||
end_date: date = Query(None, description="结束日期、格式YYYY-MM-DD")
|
||
):
|
||
conn = None
|
||
cursor = None
|
||
try:
|
||
conn = db.get_connection()
|
||
cursor = conn.cursor(dictionary=True)
|
||
|
||
# 1. 检查设备是否存在(仅传IP时):强制指定Collation
|
||
if client_ip is not None:
|
||
# 关键调整1:WHERE条件中给d.client_ip指定Collation(与da一致或反之)
|
||
check_query = """
|
||
SELECT id FROM devices
|
||
WHERE client_ip COLLATE utf8mb4_general_ci = %s COLLATE utf8mb4_general_ci
|
||
"""
|
||
cursor.execute(check_query, (client_ip,))
|
||
device = cursor.fetchone()
|
||
if not device:
|
||
raise HTTPException(status_code=404, detail=f"客户端IP为 {client_ip} 的设备不存在")
|
||
|
||
# 2. 构建WHERE条件
|
||
where_clause = []
|
||
params = []
|
||
|
||
# 关键调整2:传IP时,强制指定da.client_ip的Collation
|
||
if client_ip is not None:
|
||
where_clause.append("da.client_ip COLLATE utf8mb4_general_ci = %s COLLATE utf8mb4_general_ci")
|
||
params.append(client_ip)
|
||
if start_date:
|
||
where_clause.append("DATE(da.created_at) >= %s")
|
||
params.append(start_date.strftime("%Y-%m-%d"))
|
||
if end_date:
|
||
where_clause.append("DATE(da.created_at) <= %s")
|
||
params.append(end_date.strftime("%Y-%m-%d"))
|
||
|
||
# 3. 统计总数:JOIN时强制统一Collation
|
||
count_query = """
|
||
SELECT COUNT(*) AS total
|
||
FROM device_action da
|
||
LEFT JOIN devices d
|
||
ON da.client_ip COLLATE utf8mb4_general_ci = d.client_ip COLLATE utf8mb4_general_ci
|
||
"""
|
||
if where_clause:
|
||
count_query += " WHERE " + " AND ".join(where_clause)
|
||
cursor.execute(count_query, params)
|
||
total = cursor.fetchone()["total"]
|
||
|
||
# 4. 分页查询:JOIN时强制统一Collation
|
||
offset = (page - 1) * page_size
|
||
list_query = """
|
||
SELECT da.*, d.id AS device_id
|
||
FROM device_action da
|
||
LEFT JOIN devices d
|
||
ON da.client_ip COLLATE utf8mb4_general_ci = d.client_ip COLLATE utf8mb4_general_ci
|
||
"""
|
||
if where_clause:
|
||
list_query += " WHERE " + " AND ".join(where_clause)
|
||
list_query += " ORDER BY da.created_at DESC LIMIT %s OFFSET %s"
|
||
params.extend([page_size, offset])
|
||
cursor.execute(list_query, params)
|
||
history_list = cursor.fetchall()
|
||
|
||
# 后续格式化响应逻辑不变...
|
||
formatted_history = []
|
||
for item in history_list:
|
||
formatted_item = {
|
||
"id": item["id"],
|
||
"device_id": item["device_id"], # 可能为None(IP无对应设备)
|
||
"client_ip": item["client_ip"],
|
||
"status": item["action"],
|
||
"status_time": item["created_at"]
|
||
}
|
||
formatted_history.append(formatted_item)
|
||
|
||
return APIResponse(
|
||
code=200,
|
||
message="获取设备上下线记录成功",
|
||
data=DeviceStatusHistoryListResponse(
|
||
total=total,
|
||
history=[DeviceStatusHistoryResponse(**item) for item in formatted_history]
|
||
)
|
||
)
|
||
|
||
except MySQLError as e:
|
||
raise Exception(f"获取设备上下线记录失败: {str(e)}") from e
|
||
finally:
|
||
db.close_connection(conn, cursor)
|
||
|
||
# ------------------------------
|
||
# 通过客户端IP设置设备is_need_handler为0接口
|
||
# ------------------------------
|
||
@router.post("/need-handler/reset", response_model=APIResponse, summary="解封客户端")
|
||
@encrypt_response()
|
||
async def reset_device_need_handler(
|
||
client_ip: str = Query(..., description="目标设备的客户端IP地址(必填)")
|
||
):
|
||
try:
|
||
from service.device_service import update_is_need_handler_by_client_ip
|
||
success = update_is_need_handler_by_client_ip(
|
||
client_ip=client_ip,
|
||
is_need_handler=0 # 固定设置为0(不需要处理)
|
||
)
|
||
|
||
if success:
|
||
online_status = is_client_connected(client_ip)
|
||
|
||
# 如果设备在线,则发送消息给前端
|
||
if online_status:
|
||
# 调用 ws 发送一个消息给前端、告诉他已解锁
|
||
unlock_msg = {
|
||
"type": "unlock",
|
||
"timestamp": get_current_time_str(),
|
||
"client_ip": client_ip
|
||
}
|
||
from ws.ws import send_message_to_client
|
||
await send_message_to_client(client_ip, json.dumps(unlock_msg))
|
||
|
||
# 休眠 100 ms
|
||
await asyncio.sleep(0.1)
|
||
|
||
frame_permit_msg = {
|
||
"type": "frame",
|
||
"timestamp": get_current_time_str(),
|
||
"client_ip": client_ip
|
||
}
|
||
await send_message_to_client(client_ip, json.dumps(frame_permit_msg))
|
||
|
||
# 更新设备在线状态为1
|
||
update_online_status_by_ip(client_ip, 1)
|
||
|
||
return APIResponse(
|
||
code=200,
|
||
message=f"设备已解封",
|
||
data={
|
||
"client_ip": client_ip,
|
||
"is_need_handler": 0,
|
||
"status_desc": "设备已解封"
|
||
}
|
||
)
|
||
|
||
# 捕获工具方法抛出的业务异常(如IP为空、设备不存在)
|
||
except ValueError as e:
|
||
# 业务异常返回400/404状态码(与现有接口异常规范一致)
|
||
raise HTTPException(
|
||
status_code=404 if "设备不存在" in str(e) else 400,
|
||
detail=str(e)
|
||
) from e
|
||
|
||
# 捕获数据库层面异常(如连接失败、SQL执行错误)
|
||
except MySQLError as e:
|
||
raise Exception(f"设置is_need_handler失败:数据库操作异常 - {str(e)}") from e
|
||
|
||
# 捕获其他未知异常
|
||
except Exception as e:
|
||
raise Exception(f"设置is_need_handler失败:未知错误 - {str(e)}") from e
|
||
|
||
|
||
|