package com.inspect.nvr.service.camera; import com.inspect.nvr.domain.Infrared.Camera; import com.inspect.nvr.domain.Infrared.TemperatureData; import com.inspect.nvr.enums.CameraEnum; import lombok.extern.slf4j.Slf4j; import java.nio.file.Files; import java.nio.file.Path; /** * 品牌相机服务抽象基类。 * 统一抓图、云台预置位、测温的并发控制与重试模板,子类只实现品牌 SDK 差异钩子。 * 并发控制、统一重试等基础设施见 AbstractCameraSupport。 */ @Slf4j public abstract class AbstractCameraService extends AbstractCameraSupport implements BrandCameraService { // ------------------------------------------------------------------ // 品牌差异钩子 // ------------------------------------------------------------------ /** * 品牌对应的摄像头枚举,brandName/fileFlag 均由它派生。 */ @Override public abstract CameraEnum cameraType(); /** * 日志品牌名:取枚举 name 字段(中文名),如 大华/海康/朗驰。 */ @Override protected String brandName() { return cameraType().getName(); } /** * 抓图文件名前缀:取枚举 value,如 dahua/hik/linc。 */ protected String fileFlag() { return cameraType().getValue(); } /** * 单次 SDK 抓图,失败返回 null 或抛异常,由模板负责重试与落盘。 */ protected abstract byte[] doCapture(Camera camera) throws Exception; /** * 单次 SDK 预置位跳转,成功 true / 失败 false。 */ protected abstract boolean doGotoPreset(Camera camera) throws Exception; /** * 单次 SDK 测温,失败返回 null 或抛异常,由模板负责重试。各品牌必须实现。 */ protected abstract TemperatureData doMeasureTemperature(Camera camera) throws Exception; /** * Digest 认证抓图 URL 模板;不支持 Digest 兜底的品牌返回 null。 */ protected String digestUrlTemplate() { return null; } /** * 是否支持 DLT664 抓图,默认不支持,支持的品牌重写为 true。 */ protected boolean supportsDlt664() { return false; } // ------------------------------------------------------------------ // 抓图模板 // ------------------------------------------------------------------ @Override public byte[] capture(Camera camera) { return withConcurrencyControl(camera, () -> captureWithRetry(camera)); } private byte[] captureWithRetry(Camera camera) { Path fullPath = getFullPath(fileFlag(), camera.getIp(), camera.getChannel()); // 抓图与预置位/测温共用统一重试模板:取图+落盘在一次尝试内,返回 null 触发重试 byte[] imageBytes = withRetry(camera, "抓图", () -> { byte[] data = doCapture(camera); if (data == null) { return null; } Files.write(fullPath, data); log.info("[{}]抓图成功:{}", brandName(), fullPath); return data; }); if (imageBytes != null) { return imageBytes; } // SDK 抓图全部失败后,尝试 Digest 认证抓图兜底(3参静态方法,避免重复进入并发控制) String digestUrlTemplate = digestUrlTemplate(); if (digestUrlTemplate != null) { byte[] digestBytes = captureDigest(camera, digestUrlTemplate, fullPath); if (digestBytes != null) { log.info("[{}]digest抓图成功,图片地址:{}", brandName(), fullPath); return digestBytes; } } writeCaptureFailedImage(fullPath); log.error("[{}]抓图失败,图片地址:{}", brandName(), fullPath); return null; } /** * Digest 认证抓图入口;不支持 Digest 的品牌返回 null。 */ public byte[] captureDigest(Camera camera) { String digestUrlTemplate = digestUrlTemplate(); if (digestUrlTemplate == null) { log.warn("[{}]不支持Digest认证抓图", brandName()); return null; } Path fullPath = getFullPath(fileFlag() + "_digest", camera.getIp(), camera.getChannel()); return withConcurrencyControl(camera, () -> captureDigest(camera, digestUrlTemplate, fullPath)); } /** * DLT664 抓图:不支持时返回 null;支持的品牌其 doCapture 已内嵌温度数据,直接复用抓图模板。 */ @Override public byte[] captureDlt664(Camera camera) { if (!supportsDlt664()) { log.warn("[{}]不支持DLT664抓图", brandName()); return null; } return capture(camera); } // ------------------------------------------------------------------ // 云台模板 // ------------------------------------------------------------------ @Override public boolean gotoPreset(Camera camera) { return withConcurrencyControl(camera, () -> { Boolean result = withRetry(camera, "预置位跳转", () -> doGotoPreset(camera)); return Boolean.TRUE.equals(result); }); } /** * 组合操作模板:一次预置位跳转 + 一次测温视为一次尝试,任一步失败则整对重试。 */ @Override public TemperatureData gotoPresetAndMeasure(Camera ptzCamera, Camera thermalCamera) { Camera lockCamera = ptzCamera != null ? ptzCamera : thermalCamera; return withConcurrencyControl(lockCamera, () -> withRetry(lockCamera, "预置位跳转+测温", () -> doGotoPresetAndMeasure(ptzCamera, thermalCamera))); } /** * 单次“预置位跳转 + 测温”,普通品牌无需重写;双模在复合实现中分别委托两个品牌。 */ protected TemperatureData doGotoPresetAndMeasure(Camera ptzCamera, Camera thermalCamera) throws Exception { if (!doGotoPreset(ptzCamera)) { throw new RuntimeException("预置位跳转失败"); } if (presetSettleMillis > 0) { Thread.sleep(presetSettleMillis); } TemperatureData data = doMeasureTemperature(thermalCamera); if (data == null) { throw new RuntimeException("测温返回空"); } return data; } // ------------------------------------------------------------------ // 测温模板 // ------------------------------------------------------------------ @Override public TemperatureData measureTemperature(Camera camera) { return withConcurrencyControl(camera, () -> withRetry(camera, "测温", () -> doMeasureTemperature(camera))); } }