Files
iot-device-management-service/app/services/camera_name_service.py
16337 6996423f7d refactor(alarm): 模块化摄像头名称格式化服务
问题:
- 硬编码字段映射(gbName、name、app)
- 逻辑重复散落多处
- 格式写死无法配置
- 未基于数据库实际表结构
- 可扩展性差

重构方案:
1. 创建配置类 CameraNameConfig
   - 显示格式模板(支持变量:{camera_code}, {name}, {stream})
   - 字段优先级配置
   - WVP API配置
   - 查询超时配置

2. 创建服务类 CameraNameService
   - 查询摄像头信息(get_camera_info)
   - 提取名称字段(extract_name)
   - 格式化显示名称(format_display_name)
   - 一站式方法(get_display_name)

3. 重构路由层
   - 移除硬编码逻辑
   - 使用camera_name_service统一处理
   - 删除旧的_get_camera_info函数
   - 简化代码结构

架构优势:
- 配置驱动:格式通过环境变量控制
- 单一职责:服务只负责名称处理
- 可扩展:新增格式无需改代码
- 可测试:服务独立易于测试
- 模块化:逻辑集中便于维护

配置示例:
```bash
WVP_API_BASE=http://localhost:18080
CAMERA_NAME_FORMAT={camera_code} {name}/{stream}
CAMERA_QUERY_TIMEOUT=5
```

修改文件:
+ app/config.py - 添加CameraNameConfig配置
+ app/services/camera_name_service.py - 新建服务
+ docs/camera_name_config.md - 配置文档
~ app/routers/yudao_aiot_alarm.py - 使用新服务

测试结果:
- 告警列表: cam_1f0e3dad9990 → cam_1f0e3dad9990 大堂吧台3/012 ✓
- 设备汇总: cam_c51ce410c124 → cam_c51ce410c124 大堂吧台1/008 ✓
2026-02-24 13:59:13 +08:00

196 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
摄像头名称格式化服务
功能:
1. 从 WVP 查询摄像头信息
2. 根据配置提取摄像头名称
3. 按配置模板格式化显示名称
设计原则:
- 配置驱动:所有格式和字段映射通过配置文件控制
- 单一职责:只负责摄像头名称的查询和格式化
- 可扩展:新增格式只需修改配置,不需改代码
- 可测试:不依赖全局状态,便于单元测试
"""
from typing import Optional, Dict
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:
logger.warning(f"使用遗留格式 app/stream: {device_id},建议使用 camera_code 格式")
return None # app/stream 格式需要token暂不支持无认证查询
return None
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
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: 设备IDfallback值
camera_info: 摄像头信息字典(可选)
Returns:
格式化后的显示名称
示例:
配置模板:"{camera_code} {name}/{stream}"
camera_info: {cameraCode: "cam_123", gbName: "大堂/", stream: "012"}
返回: "cam_123 大堂/012"
配置模板:"{name}"
返回: "大堂"
"""
# 如果没有摄像头信息直接返回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 camera_code or not name or not stream:
logger.warning(
f"摄像头信息不完整: camera_code={camera_code}, "
f"name={name}, stream={stream}, 使用fallback"
)
return device_id
# 按模板格式化
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 device_id
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)
# 全局单例(依赖注入)
_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