#!/usr/bin/env python # -*- coding: utf-8 -*- """ 呼叫器设备API模块 提供呼叫器设备相关的API接口 """ from typing import Dict, List, Any, Optional from fastapi import APIRouter, Body, Query, Path, Request, File, UploadFile, Form, Response from pydantic import BaseModel from routes.common_api import format_response, error_response from utils.logger import get_logger from services.calldevice_service import CallDeviceService from routes.model.calldevice_model import CallDeviceModel, DeleteDeviceRequest, ExportDevicesRequest from utils.crypto_utils import CryptoUtils import time import random import json # 创建路由 router = APIRouter( prefix="/api/vwed-calldevice", tags=["VWED呼叫器设备"] ) # 设置日志 logger = get_logger("app.calldevice_api") @router.post("/add") async def add_call_device( call_device: CallDeviceModel = Body(..., description="呼叫器设备信息"), request: Request = Request ): """ 新增呼叫器设备 Args: call_device: 呼叫器设备信息,包括协议类型、品牌、IP、端口、设备名称、状态及按钮配置 request: 请求对象 Returns: 包含新增结果的响应 """ try: # 将按钮列表转换为字典列表,映射字段名称 buttons_dict = [] api_token = request.headers.get("x-access-token") if call_device.buttons: for button in call_device.buttons: button_dict = { "signal_name": button.signal_name, "signal_type": button.signal_type, "signal_length": button.signal_length, "register_address": button.register_address, "function_code": button.function_code, "remark": button.remark, "vwed_task_id": button.vwed_task_id, "long_vwed_task_id": button.long_vwed_task_id } buttons_dict.append(button_dict) result = await CallDeviceService.add_call_device( protocol=call_device.protocol, brand=call_device.brand, ip=call_device.ip, port=call_device.port, device_name=call_device.device_name, status=call_device.status, slave_id=call_device.slave_id, buttons=buttons_dict, api_token=api_token ) if not result.get("success", False): return error_response( message=result.get("message", "新增呼叫器设备失败"), code=400 ) return format_response( data=result.get("data", {}), message=result.get("message", "新增呼叫器设备成功") ) except Exception as e: logger.error(f"新增呼叫器设备异常: {str(e)}") return error_response(message=f"新增呼叫器设备失败: {str(e)}", code=500) @router.put("/update/{device_id}") async def update_call_device( device_id: str = Path(..., description="设备ID"), call_device: CallDeviceModel = Body(..., description="呼叫器设备信息") ): """ 更新呼叫器设备 Args: device_id: 设备ID call_device: 呼叫器设备信息,包括协议类型、品牌、IP、端口、设备名称、状态及按钮配置 Returns: 包含更新结果的响应 """ try: # 将按钮列表转换为字典列表,映射字段名称 buttons_dict = [] if call_device.buttons: for button in call_device.buttons: button_dict = { "signal_name": button.signal_name, "signal_type": button.signal_type, "signal_length": button.signal_length, "register_address": button.register_address, "function_code": button.function_code, "remark": button.remark, "vwed_task_id": button.vwed_task_id, "long_vwed_task_id": button.long_vwed_task_id } buttons_dict.append(button_dict) result = await CallDeviceService.update_call_device( device_id=device_id, protocol=call_device.protocol, brand=call_device.brand, ip=call_device.ip, port=call_device.port, device_name=call_device.device_name, status=call_device.status, slave_id=call_device.slave_id, buttons=buttons_dict ) if not result.get("success", False): return error_response( message=result.get("message", "更新呼叫器设备失败"), code=400 ) return format_response( data=result.get("data", {}), message=result.get("message", "更新呼叫器设备成功") ) except Exception as e: logger.error(f"更新呼叫器设备异常: {str(e)}") return error_response(message=f"更新呼叫器设备失败: {str(e)}", code=500) @router.post("/batch-delete") async def delete_call_device_batch( request: DeleteDeviceRequest = Body(..., description="批量删除设备请求") ): """ 删除呼叫器设备 Args: request: 包含要删除的设备ID列表的请求 Returns: 包含删除结果的响应 """ try: if not request.ids: return error_response( message="设备ID列表不能为空", code=400 ) result = await CallDeviceService.delete_call_device(device_ids=request.ids) if not result.get("success", False): return error_response( message=result.get("message", "批量删除呼叫器设备失败"), code=400 ) return format_response( data=result.get("data", {}), message=result.get("message", "批量删除呼叫器设备成功") ) except Exception as e: logger.error(f"批量删除呼叫器设备异常: {str(e)}") return error_response(message=f"批量删除呼叫器设备失败: {str(e)}", code=500) @router.get("/list") async def get_call_device_list( page: int = Query(1, description="页码", ge=1), page_size: int = Query(10, description="每页数量", ge=1, le=100), device_name: str = Query(None, description="设备名称搜索"), ip: str = Query(None, description="IP地址搜索"), protocol: str = Query(None, description="协议类型搜索"), status: int = Query(None, description="状态过滤(0:禁用,1:启用)") ): """ 获取呼叫器设备列表 Args: page: 页码 page_size: 每页数量 device_name: 设备名称搜索 ip: IP地址搜索 protocol: 协议类型搜索 status: 状态过滤(0:禁用,1:启用) Returns: 包含设备列表和分页信息的响应 """ try: result = await CallDeviceService.get_call_device_list( page=page, page_size=page_size, device_name=device_name, ip=ip, protocol=protocol, status=status ) if not result.get("success", False): return error_response( message=result.get("message", "获取呼叫器设备列表失败"), code=400 ) return format_response( data=result.get("data", {}), message=result.get("message", "获取呼叫器设备列表成功") ) except Exception as e: logger.error(f"获取呼叫器设备列表异常: {str(e)}") return error_response(message=f"获取呼叫器设备列表失败: {str(e)}", code=500) @router.get("/detail/{device_id}") async def get_call_device_detail( device_id: str = Path(..., description="设备ID") ): """ 获取呼叫器设备详情 Args: device_id: 设备ID Returns: 包含设备详情的响应 """ try: result = await CallDeviceService.get_call_device_detail(device_id=device_id) if not result.get("success", False): return error_response( message=result.get("message", "获取呼叫器设备详情失败"), code=400 ) return format_response( data=result.get("data", {}), message=result.get("message", "获取呼叫器设备详情成功") ) except Exception as e: logger.error(f"获取呼叫器设备详情异常: {str(e)}") return error_response(message=f"获取呼叫器设备详情失败: {str(e)}", code=500) @router.post("/export-batch") async def export_batch_calldevice( export_req: ExportDevicesRequest = Body(..., description="批量导出设备请求") ): """ 批量导出呼叫器设备 Args: export_req: 导出请求数据,包含要导出的设备ID列表 Returns: Response: 包含设备配置的加密专有格式文件 """ try: result = await CallDeviceService.export_call_devices(device_ids=export_req.ids) if not result.get("success", False): return error_response( message=result.get("message", "批量导出呼叫器设备失败"), code=400 ) # 获取设备数据 devices_data = result.get("data", []) # 加密数据并添加文件头、签名和校验和 encrypted_data = CryptoUtils.encrypt_data(devices_data) # 使用专有文件扩展名 .vdex (VWED Device Export) filename = f"calldevices_export_{len(export_req.ids)}.vdex" if len(export_req.ids) > 1 else f"calldevice_{export_req.ids[0]}.vdex" # 返回加密的二进制文件 headers = { 'Content-Disposition': f'attachment; filename={filename}', 'Content-Type': 'application/octet-stream', 'X-Content-Type-Options': 'nosniff', # 防止浏览器嗅探文件类型 "Access-Control-Expose-Headers": "Content-Disposition" } return Response(content=encrypted_data, media_type='application/octet-stream', headers=headers) except Exception as e: logger.error(f"批量导出呼叫器设备异常: {str(e)}") return error_response(message=f"批量导出呼叫器设备失败: {str(e)}", code=500) @router.post("/import") async def import_calldevice( file: UploadFile = File(..., description="呼叫器设备配置文件,加密专有格式") ): """ 导入呼叫器设备 Args: file: 设备配置文件,加密专有格式 Returns: 包含导入结果的响应 """ try: # 读取文件内容 content = await file.read() try: # 解密并验证数据 devices_data = CryptoUtils.decrypt_data(content) # 生成唯一后缀以避免名称冲突 timestamp = int(time.time()) rand_num = random.randint(1000, 9999) rename_suffix = f"-备份-{timestamp}{rand_num}" # 导入设备 result = await CallDeviceService.import_call_devices( devices_data=devices_data, rename_suffix=rename_suffix ) if not result.get("success", False): return error_response( message=result.get("message", "导入呼叫器设备失败"), code=400 ) return format_response( data=result.get("data", {}), message=result.get("message", "导入呼叫器设备成功") ) except ValueError as e: logger.error(f"文件格式无效或已损坏: {str(e)}") return error_response(f"无法导入呼叫器设备: {str(e)}", 400) except Exception as e: logger.error(f"解析呼叫器设备数据失败: {str(e)}") return error_response(f"解析呼叫器设备数据失败: {str(e)}", 400) except Exception as e: logger.error(f"导入呼叫器设备异常: {str(e)}") return error_response(message=f"导入呼叫器设备失败: {str(e)}", code=500) @router.post("/monitor/start/{device_id}") async def start_device_monitor( device_id: str = Path(..., description="设备ID"), request: Request = Request ): """ 启动设备监控 开启一个监控线程,持续监控设备按钮状态,当检测到按钮按下时触发对应任务 Args: device_id: 设备ID Returns: 包含启动结果的响应 """ try: # 获取token(用于同步到系统任务) tf_api_token = request.headers.get("x-access-token") result = await CallDeviceService.start_device_monitor(device_id, tf_api_token) if not result.get("success", False): return error_response( message=result.get("message", "启动设备监控失败"), code=400 ) return format_response( data=result.get("data", {}), message=result.get("message", "启动设备监控成功") ) except Exception as e: logger.error(f"启动设备监控异常: {str(e)}") return error_response(message=f"启动设备监控失败: {str(e)}", code=500) @router.post("/monitor/stop/{device_id}") async def stop_device_monitor( device_id: str = Path(..., description="设备ID") ): """ 停止设备监控 停止设备的监控线程,不再监控按钮状态 Args: device_id: 设备ID Returns: 包含停止结果的响应 """ try: result = await CallDeviceService.stop_device_monitor(device_id) if not result.get("success", False): return error_response( message=result.get("message", "停止设备监控失败"), code=400 ) return format_response( data=result.get("data", {}), message=result.get("message", "停止设备监控成功") ) except Exception as e: logger.error(f"停止设备监控异常: {str(e)}") return error_response(message=f"停止设备监控失败: {str(e)}", code=500) @router.get("/monitor/status/{device_id}") async def get_device_monitor_status( device_id: str = Path(..., description="设备ID") ): """ 获取设备监控状态 获取指定设备的监控状态信息 Args: device_id: 设备ID Returns: 包含设备监控状态的响应 """ try: result = await CallDeviceService.get_device_monitor_status(device_id) if not result.get("success", False): return error_response( message=result.get("message", "获取设备监控状态失败"), code=400 ) return format_response( data=result.get("data", {}), message=result.get("message", "获取设备监控状态成功") ) except Exception as e: logger.error(f"获取设备监控状态异常: {str(e)}") return error_response(message=f"获取设备监控状态失败: {str(e)}", code=500)