440 lines
14 KiB
Python
440 lines
14 KiB
Python
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 schema.device_schema import (
|
||
DeviceCreateRequest, DeviceResponse, DeviceListResponse,
|
||
DeviceStatusHistoryResponse, DeviceStatusHistoryListResponse
|
||
)
|
||
from schema.response_schema import APIResponse
|
||
|
||
router = APIRouter(
|
||
prefix="/devices",
|
||
tags=["设备管理"]
|
||
)
|
||
|
||
|
||
# ------------------------------
|
||
# 内部工具方法 - 记录设备状态变更历史
|
||
# ------------------------------
|
||
def record_status_change(client_ip: str, status: int) -> bool:
|
||
"""
|
||
记录设备状态变更历史(写入 device_action 表)
|
||
|
||
:param client_ip: 设备IP
|
||
:param status: 状态(1-在线、0-离线)
|
||
:return: 操作是否成功
|
||
"""
|
||
if not client_ip:
|
||
raise ValueError("客户端IP不能为空")
|
||
|
||
if status not in (0, 1):
|
||
raise ValueError("状态必须是0(离线)或1(在线)")
|
||
|
||
conn = None
|
||
cursor = None
|
||
try:
|
||
conn = db.get_connection()
|
||
cursor = conn.cursor(dictionary=True)
|
||
|
||
# 插入状态变更记录到 device_action
|
||
insert_query = """
|
||
INSERT INTO device_action
|
||
(client_ip, action, created_at, updated_at)
|
||
VALUES (%s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||
"""
|
||
cursor.execute(insert_query, (client_ip, status))
|
||
conn.commit()
|
||
|
||
return True
|
||
except MySQLError as e:
|
||
if conn:
|
||
conn.rollback()
|
||
raise Exception(f"记录设备状态变更失败: {str(e)}") from e
|
||
finally:
|
||
db.close_connection(conn, cursor)
|
||
|
||
|
||
# ------------------------------
|
||
# 内部工具方法 - 通过客户端IP增加设备报警次数
|
||
# ------------------------------
|
||
def increment_alarm_count_by_ip(client_ip: str) -> bool:
|
||
"""
|
||
通过客户端IP增加设备的报警次数
|
||
|
||
:param client_ip: 客户端IP地址
|
||
:return: 操作是否成功
|
||
"""
|
||
if not client_ip:
|
||
raise ValueError("客户端IP不能为空")
|
||
|
||
conn = None
|
||
cursor = None
|
||
try:
|
||
conn = db.get_connection()
|
||
cursor = conn.cursor(dictionary=True)
|
||
|
||
# 检查设备是否存在
|
||
cursor.execute("SELECT id FROM devices WHERE client_ip = %s", (client_ip,))
|
||
device = cursor.fetchone()
|
||
if not device:
|
||
raise ValueError(f"客户端IP为 {client_ip} 的设备不存在")
|
||
|
||
# 报警次数加1、并更新时间戳
|
||
update_query = """
|
||
UPDATE devices
|
||
SET alarm_count = alarm_count + 1,
|
||
updated_at = CURRENT_TIMESTAMP
|
||
WHERE client_ip = %s
|
||
"""
|
||
cursor.execute(update_query, (client_ip,))
|
||
conn.commit()
|
||
|
||
return True
|
||
except MySQLError as e:
|
||
if conn:
|
||
conn.rollback()
|
||
raise Exception(f"更新报警次数失败: {str(e)}") from e
|
||
finally:
|
||
db.close_connection(conn, cursor)
|
||
|
||
|
||
# ------------------------------
|
||
# 内部工具方法 - 通过客户端IP更新设备在线状态
|
||
# ------------------------------
|
||
def update_online_status_by_ip(client_ip: str, online_status: int) -> bool:
|
||
"""
|
||
通过客户端IP更新设备的在线状态
|
||
|
||
:param client_ip: 客户端IP地址
|
||
:param online_status: 在线状态(1-在线、0-离线)
|
||
:return: 操作是否成功
|
||
"""
|
||
if not client_ip:
|
||
raise ValueError("客户端IP不能为空")
|
||
|
||
if online_status not in (0, 1):
|
||
raise ValueError("在线状态必须是0(离线)或1(在线)")
|
||
|
||
conn = None
|
||
cursor = None
|
||
try:
|
||
conn = db.get_connection()
|
||
cursor = conn.cursor(dictionary=True)
|
||
|
||
# 检查设备是否存在并获取设备ID
|
||
cursor.execute("SELECT id, device_online_status FROM devices WHERE client_ip = %s", (client_ip,))
|
||
device = cursor.fetchone()
|
||
if not device:
|
||
raise ValueError(f"客户端IP为 {client_ip} 的设备不存在")
|
||
|
||
# 状态无变化则不操作
|
||
if device['device_online_status'] == online_status:
|
||
return True
|
||
|
||
# 更新在线状态和时间戳
|
||
update_query = """
|
||
UPDATE devices
|
||
SET device_online_status = %s,
|
||
updated_at = CURRENT_TIMESTAMP
|
||
WHERE client_ip = %s
|
||
"""
|
||
cursor.execute(update_query, (online_status, client_ip))
|
||
|
||
# 记录状态变更历史
|
||
record_status_change(client_ip, online_status)
|
||
|
||
conn.commit()
|
||
return True
|
||
except MySQLError as e:
|
||
if conn:
|
||
conn.rollback()
|
||
raise Exception(f"更新设备在线状态失败: {str(e)}") from e
|
||
finally:
|
||
db.close_connection(conn, cursor)
|
||
|
||
|
||
# ------------------------------
|
||
# 创建设备信息接口
|
||
# ------------------------------
|
||
@router.post("/add", response_model=APIResponse, summary="创建设备信息")
|
||
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:
|
||
# 更新设备为在线状态
|
||
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)
|
||
VALUES (%s, %s, %s, %s, %s, %s)
|
||
"""
|
||
cursor.execute(insert_query, (
|
||
device_data.ip,
|
||
device_data.hostname,
|
||
1,
|
||
device_type,
|
||
0,
|
||
device_params_json
|
||
))
|
||
conn.commit()
|
||
|
||
# 获取新设备并返回
|
||
device_id = cursor.lastrowid
|
||
cursor.execute("SELECT * FROM devices WHERE id = %s", (device_id,))
|
||
new_device = cursor.fetchone()
|
||
|
||
# 记录上线历史
|
||
record_status_change(device_data.ip, 1)
|
||
|
||
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="获取设备列表(支持筛选分页)")
|
||
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("/{device_id}/status-history", response_model=APIResponse, summary="获取设备上下线记录")
|
||
async def get_device_status_history(
|
||
device_id: int = Path(..., description="设备ID"),
|
||
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)
|
||
|
||
# 检查设备是否存在并获取 client_ip
|
||
cursor.execute("SELECT id, client_ip FROM devices WHERE id = %s", (device_id,))
|
||
device = cursor.fetchone()
|
||
if not device:
|
||
raise HTTPException(status_code=404, detail=f"设备ID为 {device_id} 的设备不存在")
|
||
client_ip = device['client_ip']
|
||
|
||
where_clause = ["client_ip = %s"]
|
||
params = [client_ip]
|
||
|
||
# 日期筛选
|
||
if start_date:
|
||
where_clause.append("DATE(created_at) >= %s")
|
||
params.append(start_date.strftime("%Y-%m-%d"))
|
||
if end_date:
|
||
where_clause.append("DATE(created_at) <= %s")
|
||
params.append(end_date.strftime("%Y-%m-%d"))
|
||
|
||
# 统计记录总数
|
||
count_query = "SELECT COUNT(*) AS total FROM device_action WHERE " + " AND ".join(where_clause)
|
||
cursor.execute(count_query, params)
|
||
total = cursor.fetchone()["total"]
|
||
|
||
# 分页查询记录
|
||
offset = (page - 1) * page_size
|
||
list_query = f"""
|
||
SELECT * FROM device_action
|
||
WHERE {' AND '.join(where_clause)}
|
||
ORDER BY 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": device_id,
|
||
"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)
|
||
|
||
|
||
# ------------------------------
|
||
# 手动更新设备在线状态接口
|
||
# ------------------------------
|
||
@router.put("/{device_id}/status", response_model=APIResponse, summary="更新设备在线状态")
|
||
async def update_device_status(
|
||
device_id: int = Path(..., description="设备ID"),
|
||
status: int = Query(..., ge=0, le=1, description="在线状态(1-在线、0-离线)")
|
||
):
|
||
conn = None
|
||
cursor = None
|
||
try:
|
||
conn = db.get_connection()
|
||
cursor = conn.cursor(dictionary=True)
|
||
|
||
# 获取设备 client_ip
|
||
cursor.execute("SELECT id, client_ip FROM devices WHERE id = %s", (device_id,))
|
||
device = cursor.fetchone()
|
||
if not device:
|
||
raise HTTPException(status_code=404, detail=f"设备ID为 {device_id} 的设备不存在")
|
||
|
||
# 更新状态
|
||
success = update_online_status_by_ip(device['client_ip'], status)
|
||
|
||
if success:
|
||
status_text = "在线" if status == 1 else "离线"
|
||
return APIResponse(
|
||
code=200,
|
||
message=f"设备已更新为{status_text}状态",
|
||
data={"device_id": device_id, "status": status, "status_text": status_text}
|
||
)
|
||
return APIResponse(
|
||
code=500,
|
||
message="更新设备状态失败",
|
||
data=None
|
||
)
|
||
|
||
except MySQLError as e:
|
||
raise Exception(f"更新设备状态失败: {str(e)}") from e
|
||
finally:
|
||
db.close_connection(conn, cursor)
|
||
|
||
|
||
# ------------------------------
|
||
# 获取所有去重的客户端IP列表
|
||
# ------------------------------
|
||
def get_unique_client_ips() -> list[str]:
|
||
"""获取所有去重的客户端IP列表"""
|
||
conn = None
|
||
cursor = None
|
||
try:
|
||
conn = db.get_connection()
|
||
cursor = conn.cursor(dictionary=True)
|
||
|
||
query = "SELECT DISTINCT client_ip FROM devices WHERE client_ip IS NOT NULL"
|
||
cursor.execute(query)
|
||
results = cursor.fetchall()
|
||
return [item['client_ip'] for item in results]
|
||
|
||
except MySQLError as e:
|
||
raise Exception(f"获取客户端IP列表失败: {str(e)}") from e
|
||
finally:
|
||
db.close_connection(conn, cursor) |