From df85ea459a81b7895f0628ccd823837676053771 Mon Sep 17 00:00:00 2001 From: WangGuangYuan Date: Fri, 16 Oct 2026 17:21:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=91=E5=8F=B0=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E6=94=B9=E6=88=90=E4=BA=BA=E5=B7=A5=E7=BB=B4=E6=8A=A4ptz=5Fsup?= =?UTF-8?q?ported?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/sql/ivs_camera.sql | 2 +- .../hik/domain/HikNvrPlaybackUiConfig.java | 8 +- .../hik/service/HikNvrDeviceTreeService.java | 5 +- .../com/inspect/nvr/ivs/pojo/IvsCamera.java | 2 +- .../nvr/ivs/service/IvsCameraSyncService.java | 44 +------ .../nvr/ivs/util/IvsNvrChannelClient.java | 122 +----------------- .../resources/mapper/ivs/IvsCameraMapper.xml | 3 - 7 files changed, 13 insertions(+), 173 deletions(-) diff --git a/docs/sql/ivs_camera.sql b/docs/sql/ivs_camera.sql index e5445bc3..7faa7457 100644 --- a/docs/sql/ivs_camera.sql +++ b/docs/sql/ivs_camera.sql @@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS `ivs_camera` ( `reserve` varchar(500) DEFAULT NULL COMMENT '预留字段', `custom_fields` text COMMENT '自定义字段', `function_type` varchar(255) DEFAULT NULL COMMENT '功能类型', - `ptz_supported` tinyint DEFAULT NULL COMMENT '云台能力:1支持,0不支持,NULL未探测', + `ptz_supported` tinyint DEFAULT NULL COMMENT '云台能力:1支持,0不支持,NULL未知', `monitor_direction` varchar(32) DEFAULT NULL COMMENT '监控方向', `owner_aps_id` varchar(128) DEFAULT NULL COMMENT '所属 APS 编码', `security_level_code_string` varchar(32) DEFAULT NULL COMMENT '安全等级编码', diff --git a/src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackUiConfig.java b/src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackUiConfig.java index 44b23bd1..1abf3ea7 100644 --- a/src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackUiConfig.java +++ b/src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackUiConfig.java @@ -94,13 +94,7 @@ public class HikNvrPlaybackUiConfig { /** 设备分组名称。 */ private String group; - /** - * 该通道是否支持云台控制,供前端对不可控通道置灰云台面板。 - * - *

按台账 basedata_eqpbook_channel.channel_type 静态判定,与设备树节点上的 - * 同名字段使用同一套规则:与镜头无关的 ptz、ng 通道不支持,台账查不到信息的 - * 通道保守放行。

- */ + /** 该通道是否支持云台控制,供前端对不可控通道置灰云台面板。 */ private Boolean ptzSupported; } } diff --git a/src/main/java/com/inspect/nvr/hik/service/HikNvrDeviceTreeService.java b/src/main/java/com/inspect/nvr/hik/service/HikNvrDeviceTreeService.java index 1887fde8..e087cbf0 100644 --- a/src/main/java/com/inspect/nvr/hik/service/HikNvrDeviceTreeService.java +++ b/src/main/java/com/inspect/nvr/hik/service/HikNvrDeviceTreeService.java @@ -171,9 +171,8 @@ public class HikNvrDeviceTreeService { /** * 把 ivs_camera 中落库的云台能力转换为页面布尔值。 * - *

能力值由同步任务向 NVR 探测得到:1 支持、0 不支持、NULL 未探测。未探测表示 - * 尚无结论(通道离线、探测失败、非海康厂商等),必须按"支持"返回,否则一次探测 - * 失败就会把云台面板误置灰。

+ *

能力值由人工维护:1 支持、0 不支持、NULL 未知。未知必须按"支持"返回, + * 否则尚未维护的通道会被整片误置灰。

* * @param capability ivs_camera.ptz_supported 落库值 * @return 支持云台控制返回 true diff --git a/src/main/java/com/inspect/nvr/ivs/pojo/IvsCamera.java b/src/main/java/com/inspect/nvr/ivs/pojo/IvsCamera.java index a86dae49..c20e566e 100644 --- a/src/main/java/com/inspect/nvr/ivs/pojo/IvsCamera.java +++ b/src/main/java/com/inspect/nvr/ivs/pojo/IvsCamera.java @@ -68,7 +68,7 @@ public class IvsCamera implements Serializable { private String customFields; /** 功能类型。 */ private String functionType; - /** 云台能力:1 支持,0 不支持,NULL 未探测(通道离线或探测失败)。 */ + /** 云台能力:1 支持,0 不支持,NULL 未知;人工维护,同步任务不回写该列。 */ private Integer ptzSupported; /** 监控方向。 */ private String monitorDirection; diff --git a/src/main/java/com/inspect/nvr/ivs/service/IvsCameraSyncService.java b/src/main/java/com/inspect/nvr/ivs/service/IvsCameraSyncService.java index 7da0011a..9b288694 100644 --- a/src/main/java/com/inspect/nvr/ivs/service/IvsCameraSyncService.java +++ b/src/main/java/com/inspect/nvr/ivs/service/IvsCameraSyncService.java @@ -15,10 +15,8 @@ import org.springframework.scheduling.annotation.Scheduled; import javax.annotation.Resource; import java.security.SecureRandom; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.UUID; /** @@ -91,20 +89,10 @@ public class IvsCameraSyncService { ivsNvrMapper.updateChannelCount(current.getNvrId(), channels.size()); current.setChannelCount(channels.size()); } - // 读取本台 NVR 已落库的云台能力结论,避免每轮同步重复探测设备。 - Map knownPtzSupport = loadKnownPtzSupport(current); // 将每个通道映射成 IVS 摄像机并按 code 插入或更新。 for (IvsNvrChannelClient.Channel channel : channels) { // 构造摄像机表字段并执行 upsert。 - IvsCamera camera = toCamera(current, channel); - // 已有结论直接沿用,保证通道离线或探测失败时不会丢失结论。 - camera.setPtzSupported(knownPtzSupport.get(camera.getCode())); - // 仅对"尚无结论且当前在线"的通道向 NVR 探测云台能力,控制同步开销。 - if (camera.getPtzSupported() == null && channel.isOnline()) { - camera.setPtzSupported( - channelClient.probePtzSupported(current, channel.getId())); - } - ivsCameraMapper.upsert(camera); + ivsCameraMapper.upsert(toCamera(current, channel)); result.setCameraCount(result.getCameraCount() + 1); } result.setSuccessNvrCount(result.getSuccessNvrCount() + 1); @@ -159,36 +147,6 @@ public class IvsCameraSyncService { } } - /** - * 读取本台 NVR 已落库的云台能力结论。 - * - *

同步任务每五分钟执行一次,已有结论的通道不再重复探测,否则每轮都会对每个 - * 通道发一次 ISAPI 请求。通道离线、探测失败或需要重新探测(手工把 - * {@code ivs_camera.ptz_supported} 置空)时才会再次探测。

- * - * @param nvr 当前同步的 NVR - * @return 摄像机编码到已落库云台能力的映射,未落库或查询无结果时对应编码不出现 - */ - private Map loadKnownPtzSupport(IvsNvr nvr) { - // 尚未落库的 NVR 无需查询,直接按全部未探测处理。 - if (nvr == null || nvr.getNvrIp() == null) { - return Collections.emptyMap(); - } - List existing = ivsCameraMapper.selectByNvrIp(nvr.getNvrIp()); - if (existing == null || existing.isEmpty()) { - return Collections.emptyMap(); - } - Map known = new HashMap<>(existing.size()); - for (IvsCamera camera : existing) { - // 空记录或空编码无法与通道对应。 - if (camera == null || camera.getCode() == null) { - continue; - } - known.put(camera.getCode().trim(), camera.getPtzSupported()); - } - return known; - } - /** 确保 NVR 具有符合 IVS 规则且全局唯一的编码。 */ private IvsNvr ensureNvrCode(IvsNvr nvr) { // NVR 记录及主键不能为空,否则无法安全回写编码。 diff --git a/src/main/java/com/inspect/nvr/ivs/util/IvsNvrChannelClient.java b/src/main/java/com/inspect/nvr/ivs/util/IvsNvrChannelClient.java index cb5cff58..057c95ec 100644 --- a/src/main/java/com/inspect/nvr/ivs/util/IvsNvrChannelClient.java +++ b/src/main/java/com/inspect/nvr/ivs/util/IvsNvrChannelClient.java @@ -44,14 +44,6 @@ public class IvsNvrChannelClient { private static final Timeout CONNECT_TIMEOUT = Timeout.ofSeconds(2); /** NVR ISAPI 响应读取超时时间。 */ private static final Timeout RESPONSE_TIMEOUT = Timeout.ofSeconds(5); - /** 海康云台能力查询路径,通道号由调用方填入。 */ - private static final String PTZ_CAPABILITIES_PATH = "/ISAPI/PTZCtrl/channels/%d/capabilities"; - /** 云台能力结构标签:出现即代表该通道具备云台能力。 */ - private static final String PTZ_CAPABILITY_TAG = "= 300) { - throw new IllegalStateException("NVR ISAPI 返回 HTTP " + response.status - + ",IP=" + nvr.getNvrIp()); - } - return response.body; - } - - /** - * 使用 Digest/Basic 兼容认证请求 NVR 的 ISAPI 接口,并返回状态码与响应体。 - * - *

云台能力探测需要区分"设备明确拒绝"与"接口本身不可用",因此不能像 - * {@link #request} 那样把非 2xx 一律当异常抛出。

- * - * @param nvr NVR 连接与凭据 - * @param path ISAPI 路径(含前导斜杠) - * @return 状态码与解码后的响应体 - */ - private IsapiResponse exchange(IvsNvr nvr, String path) { // 请求必须提供 NVR IP。 if (nvr == null || isBlank(nvr.getNvrIp())) { throw new IllegalArgumentException("NVR IP 不能为空"); @@ -172,8 +144,13 @@ public class IvsNvrChannelClient { // 读取响应体原始字节,避免 HTTP 客户端使用平台默认字符集破坏中文名称。 byte[] bytes = response.getEntity() == null ? new byte[0] : EntityUtils.toByteArray(response.getEntity()); - // 将设备响应解码成 XML 文本;非 2xx 的响应体同样解码,交由调用方判定。 - return new IsapiResponse(status, decode(bytes)); + // 非 2xx 状态表示 NVR 通道接口调用失败。 + if (status < 200 || status >= 300) { + throw new IllegalStateException("NVR ISAPI 返回 HTTP " + status + + ",IP=" + nvr.getNvrIp()); + } + // 将设备响应解码成 XML 文本。 + return decode(bytes); }); } catch (Exception ex) { // 保留已知业务异常,其他异常包装为通道读取失败。 @@ -185,91 +162,6 @@ public class IvsNvrChannelClient { } } - /** - * 向 NVR 探测指定通道是否支持云台控制。 - * - *

海康 ISAPI 的云台能力接口只对可云台通道返回设备能力结构;固定摄像机(枪机、 - * 半球等)由设备明确拒绝。因此判定分三种结果,其中"未探测"绝不能当成"不支持", - * 否则接口本身不可用或凭据异常时会把整台 NVR 的通道全部置灰。

- * - * @param nvr NVR 连接与凭据 - * @param channelId NVR 通道号,空值或无意义通道号直接返回未探测 - * @return 1 支持,0 不支持,null 未探测(接口不可用、鉴权失败或响应无法判定) - */ - public Integer probePtzSupported(IvsNvr nvr, Integer channelId) { - // 云台能力接口由海康 ISAPI 提供,其他厂商保持"未探测",不发无效请求。 - if (nvr == null || !"HIKVISION".equals(normalizeVendor(nvr.getVendor()))) { - log.info("该厂商暂不支持云台能力探测,nvrIp={},vendor={}", - nvr == null ? null : nvr.getNvrIp(), - nvr == null ? null : nvr.getVendor()); - return null; - } - // 通道号缺失时无法拼出能力查询地址,保持"未探测"而不是让同步任务失败。 - if (channelId == null || channelId <= 0) { - log.info("云台能力探测缺少有效通道号,保持未探测,nvrIp={},channel={}", - nvr.getNvrIp(), channelId); - return null; - } - IsapiResponse response; - try { - response = exchange(nvr, String.format(PTZ_CAPABILITIES_PATH, channelId)); - } catch (Exception ex) { - // 网络异常或参数异常只记录原因,保持"未探测",下一轮同步重试。 - log.error("云台能力探测失败,nvrIp={},channel={}:{}", - nvr.getNvrIp(), channelId, ex.getMessage()); - return null; - } - String body = response.body == null ? "" : response.body; - // 能力接口返回设备能力结构即代表该通道可云台控制。 - if ((response.status >= 200 && response.status < 300) - || body.contains(PTZ_CAPABILITY_TAG)) { - log.info("云台能力探测:支持云台,nvrIp={},channel={}", - nvr.getNvrIp(), channelId); - return 1; - } - // 设备用标准 ISAPI 错误体明确拒绝时判为不支持;鉴权失败必须排除在外。 - if (response.status != 401 && !body.contains(UNAUTHORIZED_TAG) - && body.contains(ISAPI_ERROR_TAG)) { - log.info("云台能力探测:不支持云台,nvrIp={},channel={},http={},statusString={}", - nvr.getNvrIp(), channelId, response.status, extractStatusString(body)); - return 0; - } - // 其余情况(接口不存在、网关错误、空响应等)无法判定,保持"未探测"。 - log.info("云台能力探测结果无法判定,保持未探测,nvrIp={},channel={},http={},body={}", - nvr.getNvrIp(), channelId, response.status, abbreviate(body)); - return null; - } - - /** 提取 ISAPI 错误体中的 statusString,便于日志直接看出设备拒绝原因。 */ - private String extractStatusString(String body) { - int start = body.indexOf(""); - int end = body.indexOf(""); - if (start < 0 || end <= start) { - return ""; - } - return body.substring(start + "".length(), end).trim(); - } - - /** 截断过长响应体,避免无法判定的设备响应把日志刷爆。 */ - private String abbreviate(String body) { - String singleLine = body.replaceAll("\\s+", " ").trim(); - return singleLine.length() <= 200 ? singleLine : singleLine.substring(0, 200) + "..."; - } - - /** ISAPI 响应:状态码与解码后的响应体。 */ - private static final class IsapiResponse { - - /** HTTP 状态码。 */ - private final int status; - /** 解码后的响应体,空响应体为空串。 */ - private final String body; - - private IsapiResponse(int status, String body) { - this.status = status; - this.body = body == null ? "" : body; - } - } - /** 解析通道 XML,并合并通道在线状态。 */ private List parseChannels(String xml, Map statuses) { Document document = parseXml(xml); diff --git a/src/main/resources/mapper/ivs/IvsCameraMapper.xml b/src/main/resources/mapper/ivs/IvsCameraMapper.xml index cada9514..9b7da4e6 100644 --- a/src/main/resources/mapper/ivs/IvsCameraMapper.xml +++ b/src/main/resources/mapper/ivs/IvsCameraMapper.xml @@ -65,7 +65,6 @@ reserve, custom_fields, function_type, - ptz_supported, monitor_direction, owner_aps_id, security_level_code_string, @@ -97,7 +96,6 @@ #{reserve}, #{customFields}, #{functionType}, - #{ptzSupported}, #{monitorDirection}, #{ownerApsId}, #{securityLevelCodeString}, @@ -129,7 +127,6 @@ reserve = VALUES(reserve), custom_fields = VALUES(custom_fields), function_type = VALUES(function_type), - ptz_supported = COALESCE(VALUES(ptz_supported), ptz_supported), monitor_direction = VALUES(monitor_direction), owner_aps_id = VALUES(owner_aps_id), security_level_code_string = VALUES(security_level_code_string),