Files
iot-device-management-service/app/services/camera_name_service.py
16337 4bd369813e fix(alarm): 支持 app/stream 格式直接提取中文名称
问题:
- 警告日志:使用遗留格式 app/stream: 大堂吧台3/012
- app/stream 格式无法显示中文名称
- 旧逻辑返回 None 导致显示原始ID

根本原因:
对于 "大堂吧台3/012" 格式,app 部分本身就是中文名称,
但旧逻辑直接返回 None 不处理,完全没必要。

修复方案:
1. 新增 _parse_app_stream_format 方法
   - 直接解析 app/stream 格式
   - 构造虚拟 camera_info 对象
   - 无需查询 WVP API

2. 修改 get_camera_info 方法
   - camera_code 格式:查询 WVP
   - app/stream 格式:直接解析
   - 统一返回 camera_info

3. 修改 format_display_name 方法
   - app/stream 格式没有 camera_code
   - 直接返回 name,不使用模板
   - 避免字段缺失警告

4. 修改 get_camera_infos_batch 方法
   - 分类处理两种格式
   - camera_code:并发查询 WVP
   - app/stream:直接解析(无IO)

逻辑对比:
旧逻辑:
  cam_1f0e3dad9990 → 查询WVP → 大堂吧台3 ✓
  大堂吧台3/012 → 返回None → 大堂吧台3/012 ✗

新逻辑:
  cam_1f0e3dad9990 → 查询WVP → 大堂吧台3 ✓
  大堂吧台3/012 → 直接解析 → 大堂吧台3 ✓

测试结果:
✓ cam_1f0e3dad9990 → 大堂吧台3
✓ 大堂吧台3/012 → 大堂吧台3
✓ 一楼大堂吧台/008 → 一楼大堂吧台
✓ 无警告日志

性能提升:
- app/stream 格式无需 HTTP 查询
- 批量查询时性能更优
2026-02-24 14:26:44 +08:00

299 lines
9.1 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. 按配置模板格式化显示名称
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: 设备IDfallback值
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