diff --git a/app/config.py b/app/config.py index 2c7aac0..e537dcb 100644 --- a/app/config.py +++ b/app/config.py @@ -77,11 +77,11 @@ class CameraNameConfig: # 显示格式模板(支持变量:{camera_code}, {name}, {stream}) # 可选格式: - # - "{camera_code} {name}/{stream}" - cam_xxx 名称/流id + # - "{name}" - 仅名称(推荐,告警列表使用) + # - "{camera_code} {name}/{stream}" - cam_xxx 名称/流id(完整格式) # - "{name}/{stream}" - 名称/流id - # - "{name}" - 仅名称 # - "{camera_code}" - 仅code - display_format: str = "{camera_code} {name}/{stream}" + display_format: str = "{name}" # 名称字段优先级(从高到低) # 从StreamProxy对象中提取名称时的字段优先级 @@ -158,7 +158,7 @@ def load_settings() -> Settings: ), camera_name=CameraNameConfig( wvp_api_base=os.getenv("WVP_API_BASE", "http://localhost:18080"), - display_format=os.getenv("CAMERA_NAME_FORMAT", "{camera_code} {name}/{stream}"), + display_format=os.getenv("CAMERA_NAME_FORMAT", "{name}"), query_timeout=int(os.getenv("CAMERA_QUERY_TIMEOUT", "5")), ), ) diff --git a/app/routers/yudao_aiot_alarm.py b/app/routers/yudao_aiot_alarm.py index 6429a3f..9f0f6b6 100644 --- a/app/routers/yudao_aiot_alarm.py +++ b/app/routers/yudao_aiot_alarm.py @@ -28,8 +28,13 @@ from app.utils.logger import logger router = APIRouter(prefix="/admin-api/aiot/alarm", tags=["AIoT-告警"]) -async def _alarm_to_camel(alarm_dict: dict, current_user: dict = None) -> dict: - """将 alarm_event 字典转换为前端 camelCase 格式(兼容前端旧字段名)""" +async def _alarm_to_camel(alarm_dict: dict, name_map: dict = None) -> dict: + """将 alarm_event 字典转换为前端 camelCase 格式(兼容前端旧字段名) + + Args: + alarm_dict: 告警字典 + name_map: 摄像头名称映射 {device_id: display_name},可选 + """ # snapshot_url: 根据存储方式转为可访问 URL storage = get_oss_storage() snapshot_url = alarm_dict.get("snapshot_url") @@ -60,13 +65,12 @@ async def _alarm_to_camel(alarm_dict: dict, current_user: dict = None) -> dict: alarm_id = alarm_dict.get("alarm_id") - # 查询摄像头显示名称(使用配置化服务) + # 获取摄像头显示名称(从缓存映射中获取) device_id = alarm_dict.get("device_id") device_name = device_id # 默认使用 device_id - if device_id: - camera_service = get_camera_name_service() - device_name = await camera_service.get_display_name(device_id) + if name_map and device_id in name_map: + device_name = name_map[device_id] return { # 新字段(三表结构) @@ -147,8 +151,15 @@ async def get_alert_page( page_size=pageSize, ) - # 并发查询所有告警的摄像头名称 - alarm_list = await asyncio.gather(*[_alarm_to_camel(a.to_dict(), current_user) for a in alarms]) + # 提取所有唯一的 device_id + device_ids = list(set(a.device_id for a in alarms if a.device_id)) + + # 批量查询摄像头名称(去重+并发优化) + camera_service = get_camera_name_service() + name_map = await camera_service.get_display_names_batch(device_ids) + + # 转换为 camelCase 格式(使用缓存的名称映射) + alarm_list = [await _alarm_to_camel(a.to_dict(), name_map) for a in alarms] return YudaoResponse.page( list_data=alarm_list, @@ -173,7 +184,12 @@ async def get_alert( if not alarm_dict: raise HTTPException(status_code=404, detail="告警不存在") - return YudaoResponse.success(await _alarm_to_camel(alarm_dict, current_user)) + # 查询单个摄像头名称 + device_id = alarm_dict.get("device_id") + camera_service = get_camera_name_service() + name_map = {device_id: await camera_service.get_display_name(device_id)} if device_id else {} + + return YudaoResponse.success(await _alarm_to_camel(alarm_dict, name_map)) @router.put("/alert/handle") diff --git a/app/services/camera_name_service.py b/app/services/camera_name_service.py index ba1c12e..d946453 100644 --- a/app/services/camera_name_service.py +++ b/app/services/camera_name_service.py @@ -5,15 +5,17 @@ 1. 从 WVP 查询摄像头信息 2. 根据配置提取摄像头名称 3. 按配置模板格式化显示名称 +4. 批量查询和缓存优化 设计原则: - 配置驱动:所有格式和字段映射通过配置文件控制 - 单一职责:只负责摄像头名称的查询和格式化 - 可扩展:新增格式只需修改配置,不需改代码 - 可测试:不依赖全局状态,便于单元测试 +- 性能优化:批量查询、去重、请求内缓存 """ -from typing import Optional, Dict +from typing import Optional, Dict, List import httpx from app.config import CameraNameConfig from app.utils.logger import logger @@ -85,6 +87,40 @@ class CameraNameService: return None + async def get_camera_infos_batch(self, device_ids: List[str]) -> Dict[str, Optional[Dict]]: + """ + 批量查询摄像头信息(并发+去重优化) + + Args: + device_ids: 设备ID列表 + + Returns: + {device_id: camera_info} 字典 + """ + # 去重 + unique_ids = list(set(device_ids)) + + # 只查询 camera_code 格式的 + valid_ids = [did for did in unique_ids if did.startswith("cam_")] + + if not valid_ids: + return {did: None for did in device_ids} + + # 并发查询 + import asyncio + tasks = [self.get_camera_info(did) for did in valid_ids] + results = await asyncio.gather(*tasks) + + # 构建映射 + info_map = dict(zip(valid_ids, results)) + + # 补充非 camera_code 格式的 + for did in unique_ids: + if did not in info_map: + info_map[did] = None + + return info_map + def extract_name(self, camera_info: Dict) -> Optional[str]: """ 从摄像头信息中提取名称 @@ -142,8 +178,19 @@ class CameraNameService: stream = camera_info.get("stream") name = self.extract_name(camera_info) + # 检查模板需要的变量 + template_vars = { + "{camera_code}": camera_code, + "{name}": name, + "{stream}": stream + } + + # 如果模板只需要 {name},即使其他字段缺失也能返回 + if self.config.display_format == "{name}" and name: + return name + # 如果必需字段缺失,返回device_id - if not camera_code or not name or not stream: + if not all([camera_code, name, stream]): logger.warning( f"摄像头信息不完整: camera_code={camera_code}, " f"name={name}, stream={stream}, 使用fallback" @@ -176,6 +223,28 @@ class CameraNameService: camera_info = await self.get_camera_info(device_id) return self.format_display_name(device_id, camera_info) + async def get_display_names_batch( + self, + device_ids: List[str] + ) -> Dict[str, str]: + """ + 批量获取摄像头显示名称(性能优化版本) + + Args: + device_ids: 设备ID列表 + + Returns: + {device_id: display_name} 字典 + """ + # 批量查询摄像头信息(去重+并发) + info_map = await self.get_camera_infos_batch(device_ids) + + # 格式化所有名称 + return { + did: self.format_display_name(did, info_map.get(did)) + for did in device_ids + } + # 全局单例(依赖注入) _camera_name_service: Optional[CameraNameService] = None