""" 摄像头名称格式化服务 功能: 1. 从 WVP 查询摄像头信息 2. 根据配置提取摄像头名称 3. 按配置模板格式化显示名称 4. 批量查询和缓存优化 设计原则: - 配置驱动:所有格式和字段映射通过配置文件控制 - 单一职责:只负责摄像头名称的查询和格式化 - 可扩展:新增格式只需修改配置,不需改代码 - 可测试:不依赖全局状态,便于单元测试 - 性能优化:批量查询、去重、请求内缓存 """ from typing import Optional, Dict, List import httpx from app.config import CameraNameConfig from app.utils.logger import logger class CameraNameService: """摄像头名称服务""" def __init__(self, config: CameraNameConfig): """ 初始化服务 Args: config: 摄像头名称配置 """ self.config = config async def get_camera_info(self, device_id: str) -> Optional[Dict]: """ 从 WVP 查询摄像头信息 Args: device_id: 设备ID,支持两种格式: - camera_code 格式:cam_xxxxxxxxxxxx - app/stream 格式:大堂吧台3/012 Returns: 摄像头信息字典,查询失败返回 None """ # camera_code 格式(推荐) if device_id.startswith("cam_"): return await self._query_by_camera_code(device_id) # app/stream 格式(遗留格式,直接解析) elif "/" in device_id: return self._parse_app_stream_format(device_id) return None def _parse_app_stream_format(self, device_id: str) -> Optional[Dict]: """ 解析 app/stream 格式的 device_id Args: device_id: 格式如 "大堂吧台3/012" Returns: 虚拟的摄像头信息字典 """ parts = device_id.split("/", 1) if len(parts) != 2: return None app, stream = parts # 构造虚拟的摄像头信息(兼容统一格式化逻辑) return { "app": app, # 中文名称 "stream": stream, # 流ID "gbName": app, # 兼容字段 "cameraCode": None, # app/stream 格式没有 camera_code } async def _query_by_camera_code(self, camera_code: str) -> Optional[Dict]: """ 通过 camera_code 查询摄像头信息 Args: camera_code: 摄像头编码,格式:cam_xxxxxxxxxxxx Returns: 摄像头信息字典,查询失败返回 None """ try: async with httpx.AsyncClient(timeout=self.config.query_timeout) as client: resp = await client.get( f"{self.config.wvp_api_base}/api/ai/camera/get", params={"cameraCode": camera_code}, ) if resp.status_code == 200: data = resp.json() if data.get("code") == 0: return data.get("data") else: logger.warning( f"WVP查询摄像头失败: camera_code={camera_code}, " f"status={resp.status_code}" ) except Exception as e: logger.error(f"WVP查询异常: camera_code={camera_code}, error={e}") 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 和 app/stream 格式 camera_code_ids = [did for did in unique_ids if did.startswith("cam_")] app_stream_ids = [did for did in unique_ids if "/" in did] # 初始化结果映射 info_map = {} # 并发查询 camera_code 格式 if camera_code_ids: import asyncio tasks = [self.get_camera_info(did) for did in camera_code_ids] results = await asyncio.gather(*tasks) info_map.update(dict(zip(camera_code_ids, results))) # 直接解析 app/stream 格式(无需查询) for did in app_stream_ids: info_map[did] = self._parse_app_stream_format(did) # 补充其他格式(未识别的) 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]: """ 从摄像头信息中提取名称 根据配置的字段优先级提取名称: 1. 按优先级遍历字段 2. 如果是 gbName 字段,自动去除 "/" 后缀 3. 返回第一个非空值 Args: camera_info: 摄像头信息字典 Returns: 提取的名称,失败返回 None """ for field in self.config.name_field_priority: value = camera_info.get(field) if value: # gb_name 可能包含 "/" 后缀,需要去除 if field == "gbName" and "/" in value: value = value.split("/")[0] return value return None def format_display_name( self, device_id: str, camera_info: Optional[Dict] = None ) -> str: """ 格式化摄像头显示名称 Args: device_id: 设备ID(fallback值) camera_info: 摄像头信息字典(可选) Returns: 格式化后的显示名称 示例: 配置模板:"{camera_code} {name}/{stream}" camera_info: {cameraCode: "cam_123", gbName: "大堂/", stream: "012"} 返回: "cam_123 大堂/012" 配置模板:"{name}" 返回: "大堂" app/stream格式: device_id: "大堂吧台3/012" camera_info: {app: "大堂吧台3", stream: "012", cameraCode: None} 返回: "大堂吧台3"(忽略模板,直接返回名称) """ # 如果没有摄像头信息,直接返回device_id if not camera_info: return device_id # 提取变量 camera_code = camera_info.get("cameraCode") or camera_info.get("camera_code") stream = camera_info.get("stream") name = self.extract_name(camera_info) # 如果没有提取到名称,返回device_id if not name: logger.warning(f"无法提取摄像头名称: device_id={device_id}") return device_id # 对于 app/stream 格式(没有 camera_code),直接返回名称 if not camera_code: return name # 对于 camera_code 格式,检查模板需要的变量 # 如果模板只需要 {name},直接返回名称 if self.config.display_format == "{name}": return name # 完整格式需要所有字段 if not stream: logger.warning( f"摄像头信息不完整: camera_code={camera_code}, " f"name={name}, stream={stream}, 使用fallback" ) return name # 至少返回名称 # 按模板格式化 try: return self.config.display_format.format( camera_code=camera_code, name=name, stream=stream ) except KeyError as e: logger.error(f"格式化模板变量错误: {e}, 模板={self.config.display_format}") return name # 出错时至少返回名称 async def get_display_name(self, device_id: str) -> str: """ 获取摄像头显示名称(一站式方法) 结合查询和格式化,返回最终显示名称 Args: device_id: 设备ID Returns: 格式化后的显示名称 """ 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 def get_camera_name_service() -> CameraNameService: """ 获取摄像头名称服务单例 Returns: CameraNameService 实例 """ global _camera_name_service if _camera_name_service is None: from app.config import settings _camera_name_service = CameraNameService(settings.camera_name) return _camera_name_service