feat(aiot): 告警冷却时间调整 + 截图本地保留 + 中文路径修复

- 离岗检测冷却时间: 300s → 600s(10分钟)
- 入侵检测冷却时间: 120s → 300s(5分钟)
- 入侵告警级别改为高(alarm_level=3)
- COS 不可用时保留本地截图文件,不再上报后删除
- 修复 cv2.imwrite 中文路径失败,改用 imencode + write_bytes
- 配置订阅在 LOCAL 模式下跳过 Redis 连接

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 09:57:02 +08:00
parent e828f4e09b
commit 181623428a
8 changed files with 532 additions and 66 deletions

View File

@@ -44,9 +44,14 @@ class PendingCapture:
class ImageStorageManager:
"""图片存储管理器"""
_instance = None
_lock = threading.Lock()
@staticmethod
def _sanitize_filename(name: str) -> str:
"""清理文件名中的非法字符(/ \\ 等路径分隔符替换为下划线)"""
return name.replace("/", "_").replace("\\", "_")
def __new__(cls, config: Optional[CaptureConfig] = None):
if cls._instance is None:
@@ -103,17 +108,14 @@ class ImageStorageManager:
logger.error(f"图片保存异常: {e}")
def _save_image(self, capture: PendingCapture) -> Optional[str]:
"""保存单张图片"""
"""保存单张图片(使用 imencode+write_bytes 避免中文路径问题)"""
try:
image = capture.image
if image is None:
self._failed_count += 1
return None
if len(image.shape) == 3 and image.shape[2] == 3:
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
if image.shape[1] > self.config.max_width or image.shape[0] > self.config.max_height:
scale = min(
self.config.max_width / image.shape[1],
@@ -124,28 +126,33 @@ class ImageStorageManager:
int(image.shape[0] * scale)
)
image = cv2.resize(image, new_size, interpolation=cv2.INTER_AREA)
date_dir = capture.timestamp.strftime("%Y%m%d")
save_dir = Path(self.config.image_dir) / date_dir
save_dir.mkdir(parents=True, exist_ok=True)
filename = f"{capture.camera_id}_{capture.alert_id}{self.config.save_format}"
safe_camera_id = self._sanitize_filename(capture.camera_id)
filename = f"{safe_camera_id}_{capture.alert_id}{self.config.save_format}"
filepath = save_dir / filename
success = cv2.imwrite(
str(filepath),
# 使用 imencode + write_bytes 代替 imwrite
# 因为 cv2.imwrite 在 Windows 上无法处理中文路径
success, buffer = cv2.imencode(
self.config.save_format,
image,
[cv2.IMWRITE_JPEG_QUALITY, self.config.quality]
)
if success:
filepath.write_bytes(buffer.tobytes())
self._saved_count += 1
logger.debug(f"图片已保存: {filepath}")
logger.info(f"图片已保存: {filepath}")
return str(filepath)
else:
logger.warning(f"图片编码失败: {filepath}")
self._failed_count += 1
return None
except Exception as e:
logger.error(f"保存图片失败: {e}")
self._failed_count += 1
@@ -158,20 +165,28 @@ class ImageStorageManager:
alert_id: str,
timestamp: Optional[datetime] = None
) -> Optional[str]:
"""异步保存抓拍图片"""
"""异步保存抓拍图片,返回预计的文件路径"""
ts = timestamp or datetime.now()
capture = PendingCapture(
image=image,
camera_id=camera_id,
alert_id=alert_id,
timestamp=timestamp or datetime.now()
timestamp=ts,
)
self._save_queue.put(capture)
return f"<queued: {alert_id}>"
# 返回确定性的文件路径(与 _save_image 使用相同的命名规则)
date_dir = ts.strftime("%Y%m%d")
safe_camera_id = self._sanitize_filename(camera_id)
filename = f"{safe_camera_id}_{alert_id}{self.config.save_format}"
filepath = Path(self.config.image_dir) / date_dir / filename
return str(filepath)
def get_image_path(self, camera_id: str, alert_id: str) -> Optional[str]:
"""获取已保存图片路径"""
date_str = datetime.now().strftime("%Y%m%d")
filename = f"{camera_id}_{alert_id}{self.config.save_format}"
safe_camera_id = self._sanitize_filename(camera_id)
filename = f"{safe_camera_id}_{alert_id}{self.config.save_format}"
filepath = Path(self.config.image_dir) / date_str / filename
if filepath.exists():