import json from datetime import datetime, 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)