Browse Source

修改红外ptzcontroller

master
王寅 2 weeks ago
parent
commit
d7c0a3dd91
10 changed files with 1370 additions and 305 deletions
  1. +6
    -3
      src/main/java/com/inspect/nvr/config/HikLibConfig.java
  2. +7
    -0
      src/main/java/com/inspect/nvr/controller/IvsCameraController.java
  3. +1
    -0
      src/main/java/com/inspect/nvr/hik/controller/HikNvrPlaybackExceptionHandler.java
  4. +3
    -0
      src/main/java/com/inspect/nvr/hik/domain/HikNvrLiveStreamResponse.java
  5. +3
    -0
      src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackErrorResponse.java
  6. +62
    -8
      src/main/java/com/inspect/nvr/hik/exception/HikNvrPlaybackException.java
  7. +6
    -6
      src/main/java/com/inspect/nvr/hik/service/HikIvsVendorService.java
  8. +1012
    -206
      src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java
  9. +18
    -3
      src/main/java/com/inspect/nvr/hik/vo/HikNvrLiveStreamConfigVo.java
  10. +252
    -79
      src/main/java/com/inspect/nvr/service/HikLoginService.java

+ 6
- 3
src/main/java/com/inspect/nvr/config/HikLibConfig.java View File

@ -9,6 +9,7 @@ import com.inspect.nvr.service.impl.HikLoginResultCallBack;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
@ -18,8 +19,9 @@ import org.springframework.stereotype.Component;
@Component
public class HikLibConfig {
/** ClientDemo 使用的连接超时时间,避免失联设备长时间占用控制请求。 */
private static final int SDK_CONNECT_TIMEOUT_MILLIS = 5000;
/** 海康 SDK 建连超时,失联 NVR 必须在实时预览总超时内快速返回。 */
@Value("${hik.sdk.connect-timeout-millis:1000}")
private int sdkConnectTimeoutMillis;
/** 海康 Linux64 SDK 6.1.11.5 随包提供的 OpenSSL 加密库文件名。 */
private static final String LINUX_CRYPTO_LIBRARY_NAME = "libcrypto.so.3";
@ -101,7 +103,8 @@ public class HikLibConfig {
//启动SDK写日志
hcNetSDK.NET_DVR_SetLogToFile(3, "./sdkLog", false);
//设置连接时间与重连时间
hcNetSDK.NET_DVR_SetConnectTime(SDK_CONNECT_TIMEOUT_MILLIS, 1);
// SDK 单次建连限制在可配置的短超时内避免断网时占满 Tomcat 请求线程
hcNetSDK.NET_DVR_SetConnectTime(Math.max(500, sdkConnectTimeoutMillis), 1);
hcNetSDK.NET_DVR_SetReconnect(100000,true);
//连接所有nvr


+ 7
- 0
src/main/java/com/inspect/nvr/controller/IvsCameraController.java View File

@ -63,6 +63,13 @@ public class IvsCameraController {
return ResponseEntity.ok().body(ivsOpenApiService.ptzControl(param));
}
@PostMapping({"/v1/device/ptzcontrol"})
public ResponseEntity<PtzControlResult> ptzControl(@RequestBody PtzControlParam param) {
log.info("Ptz control request param: {}", param);
return ResponseEntity
.ok()
.body(ivsCameraService.ptzControl(param));
}
/**
* 接收前端视频框选坐标并按框选方向执行 3D 定位放大或缩小
*


+ 1
- 0
src/main/java/com/inspect/nvr/hik/controller/HikNvrPlaybackExceptionHandler.java View File

@ -26,6 +26,7 @@ public class HikNvrPlaybackExceptionHandler {
// 组装错误码和中文错误消息
HikNvrPlaybackErrorResponse response = HikNvrPlaybackErrorResponse.builder()
.code(exception.getCode())
.errorCode(exception.getErrorCode())
.message(exception.getMessage())
.build();
// 使用异常携带的 HTTP 状态码返回错误响应


+ 3
- 0
src/main/java/com/inspect/nvr/hik/domain/HikNvrLiveStreamResponse.java View File

@ -29,6 +29,9 @@ public class HikNvrLiveStreamResponse {
/** 会话状态。 */
private String status;
/** 启动失败时返回的稳定字符串错误码。 */
private String errorCode;
/** HLS 播放地址。 */
private String hlsUrl;


+ 3
- 0
src/main/java/com/inspect/nvr/hik/domain/HikNvrPlaybackErrorResponse.java View File

@ -14,6 +14,9 @@ public class HikNvrPlaybackErrorResponse {
/** 参数错误或海康 SDK 错误码。 */
private int code;
/** 可供前端和运维直接判断故障类型的稳定字符串错误码。 */
private String errorCode;
/** 中文错误说明。 */
private String message;
}

+ 62
- 8
src/main/java/com/inspect/nvr/hik/exception/HikNvrPlaybackException.java View File

@ -6,29 +6,34 @@ public class HikNvrPlaybackException extends RuntimeException {
/** 对外返回的业务或 SDK 错误码。 */
private final int code;
/** 对外返回的稳定字符串错误码;旧接口异常未分类时为空。 */
private final String errorCode;
/** 对应的 HTTP 响应状态。 */
private final HttpStatus status;
/** 保存统一异常的数值码、字符串码、HTTP 状态和原始原因。 */
private HikNvrPlaybackException(
int code,
String errorCode,
HttpStatus status,
String message,
Throwable cause) {
super(message, cause);
this.code = code;
this.errorCode = errorCode;
this.status = status;
}
/** 创建参数错误异常。 */
public static HikNvrPlaybackException badRequest(String message) {
// 创建 HTTP 400 参数错误
return new HikNvrPlaybackException(-1, HttpStatus.BAD_REQUEST, message, null);
return new HikNvrPlaybackException(-1, null, HttpStatus.BAD_REQUEST, message, null);
}
/** 创建不带原始原因的 SDK/设备错误异常。 */
public static HikNvrPlaybackException sdkFailure(int code, String message) {
// 创建 HTTP 502 厂商 SDK/设备调用错误
return new HikNvrPlaybackException(code, HttpStatus.BAD_GATEWAY, message, null);
return new HikNvrPlaybackException(code, null, HttpStatus.BAD_GATEWAY, message, null);
}
/** 创建带原始原因的 SDK/设备错误异常。 */
@ -37,37 +42,80 @@ public class HikNvrPlaybackException extends RuntimeException {
String message,
Throwable cause) {
// 创建带原始异常原因的 HTTP 502 设备错误
return new HikNvrPlaybackException(code, HttpStatus.BAD_GATEWAY, message, cause);
return new HikNvrPlaybackException(code, null, HttpStatus.BAD_GATEWAY, message, cause);
}
/** 创建带稳定字符串错误码的 HTTP 502 NVR/通道异常。 */
public static HikNvrPlaybackException sdkFailure(
String errorCode,
String message,
Throwable cause) {
// 保留实时预览历史 502 语义同时增加可机读故障分类
return new HikNvrPlaybackException(
-1, errorCode, HttpStatus.BAD_GATEWAY, message, cause);
}
/** 创建带稳定字符串错误码且不含原始原因的 HTTP 502 NVR/通道异常。 */
public static HikNvrPlaybackException sdkFailure(
String errorCode,
String message) {
// 没有底层异常时仍保留可机读故障分类
return sdkFailure(errorCode, message, null);
}
/** 创建带原始原因的媒体链路错误异常。 */
public static HikNvrPlaybackException mediaFailure(String message, Throwable cause) {
// 创建 HTTP 503 媒体链路错误
return new HikNvrPlaybackException(-3, HttpStatus.SERVICE_UNAVAILABLE, message, cause);
return new HikNvrPlaybackException(-3, null, HttpStatus.SERVICE_UNAVAILABLE, message, cause);
}
/** 创建不带原始原因的媒体链路错误异常。 */
public static HikNvrPlaybackException mediaFailure(String message) {
// 创建不带原始异常的 HTTP 503 媒体错误
return new HikNvrPlaybackException(-3, HttpStatus.SERVICE_UNAVAILABLE, message, null);
return new HikNvrPlaybackException(-3, null, HttpStatus.SERVICE_UNAVAILABLE, message, null);
}
/** 创建带稳定字符串错误码的媒体链路异常。 */
public static HikNvrPlaybackException mediaFailure(
String errorCode,
String message,
Throwable cause) {
// 实时流调用方依赖字符串码区分 NVR通道与 ZLMediaKit 故障
return new HikNvrPlaybackException(
-3, errorCode, HttpStatus.SERVICE_UNAVAILABLE, message, cause);
}
/** 创建带稳定字符串错误码且不含原始原因的媒体链路异常。 */
public static HikNvrPlaybackException mediaFailure(
String errorCode,
String message) {
// 没有底层异常时仍保留可机读错误类型
return mediaFailure(errorCode, message, null);
}
/** 创建资源不存在异常。 */
public static HikNvrPlaybackException notFound(String message) {
// 创建 HTTP 404 资源不存在错误
return new HikNvrPlaybackException(-4, HttpStatus.NOT_FOUND, message, null);
return new HikNvrPlaybackException(-4, null, HttpStatus.NOT_FOUND, message, null);
}
/** 创建状态冲突异常。 */
public static HikNvrPlaybackException conflict(String message) {
// 创建 HTTP 409 状态冲突错误
return new HikNvrPlaybackException(-5, HttpStatus.CONFLICT, message, null);
return new HikNvrPlaybackException(-5, null, HttpStatus.CONFLICT, message, null);
}
/** 创建带稳定字符串错误码的状态冲突异常。 */
public static HikNvrPlaybackException conflict(String errorCode, String message) {
// 起流队列满时用 409 快速释放 Tomcat 请求线程
return new HikNvrPlaybackException(
-5, errorCode, HttpStatus.CONFLICT, message, null);
}
/** 创建云台控制锁被占用异常,提示持锁角色和剩余秒数。 */
public static HikNvrPlaybackException operationLocked(String holderRoleName, int remainSeconds) {
// 创建 HTTP 423 控制权被占错误
return new HikNvrPlaybackException(-423, HttpStatus.LOCKED,
return new HikNvrPlaybackException(-423, null, HttpStatus.LOCKED,
holderRoleName + "正在进行操作,操作锁定剩余时间" + Math.max(remainSeconds, 0) + "秒,请稍后重试", null);
}
@ -77,6 +125,12 @@ public class HikNvrPlaybackException extends RuntimeException {
return code;
}
/** 返回可供前端稳定判断故障类型的字符串错误码。 */
public String getErrorCode() {
// 未分类的历史异常保持为空兼容原有响应
return errorCode;
}
/** 返回 HTTP 状态。 */
public HttpStatus getStatus() {
// 返回 HTTP 状态码


+ 6
- 6
src/main/java/com/inspect/nvr/hik/service/HikIvsVendorService.java View File

@ -686,8 +686,8 @@ public class HikIvsVendorService implements IvsVendorAdapter {
if (request == null) {
throw HikNvrPlaybackException.badRequest("预置位请求不能为空");
}
// 校验完整摄像机编码确保新增动作落到正确的 NVR 通道
validateFullCameraCode(request.getCameraCode());
// 校验摄像机编码兼容完整设备编码#域编码和仅设备编码两种调用方式
validatePtzCameraCode(request.getCameraCode());
// 读取名称并去掉首尾空白统一设备端保存的文本
String name = request.getPresetName() == null
? "" : request.getPresetName().trim();
@ -752,8 +752,8 @@ public class HikIvsVendorService implements IvsVendorAdapter {
if (request == null || !hasText(request.getCameraCode())) {
throw HikNvrPlaybackException.badRequest("cameraCode不能为空");
}
// 校验完整 cameraCode 格式确保能解析出设备域和通道信息
validateFullCameraCode(request.getCameraCode());
// 校验 cameraCode 格式兼容完整设备编码#域编码和仅设备编码两种调用方式
validatePtzCameraCode(request.getCameraCode());
// 读取预置位对象后续从其中获取编号名称和协议保留字段
IvsPresetModifyRequest.PtzPresetInfo info = request.getPtzPresetInfo();
// 预置位对象或编号缺失时无法确定要修改哪一个预置位
@ -915,9 +915,9 @@ public class HikIvsVendorService implements IvsVendorAdapter {
}
/**
* 校验云台控制允许使用的摄像机编码格式
* 校验云台和预置位接口允许使用的摄像机编码格式
*
* <p>云台控制兼容完整的设备编码#域编码和仅含设备编码的调用方式
* <p>云台控制与预置位增改兼容完整的设备编码#域编码和仅含设备编码的调用方式
* 不带域编码时后续根据设备编码前缀匹配唯一的 NVR</p>
*
* @param cameraCode 完整摄像机编码或不带域后缀的设备编码


+ 1012
- 206
src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java
File diff suppressed because it is too large
View File


+ 18
- 3
src/main/java/com/inspect/nvr/hik/vo/HikNvrLiveStreamConfigVo.java View File

@ -18,7 +18,22 @@ public class HikNvrLiveStreamConfigVo {
/** ZLMediaKit HTTP/HLS 基础地址。 */
@Value("${hik.playback.hls.http-base-url:http://127.0.0.1:18081}")
private String httpBaseUrl = "http://127.0.0.1:18081";
/** 实时流启动探测超时时间。 */
@Value("${hik.playback.live.startup-timeout-millis:20000}")
private long startupTimeoutMillis = 20000L;
/** 服务端访问 ZLMediaKit HTTP API 的基础地址。 */
@Value("${hik.playback.hls.api-base-url:http://127.0.0.1:18081}")
private String apiBaseUrl = "http://127.0.0.1:18081";
/** ZLMediaKit API 密钥,仅用于服务端就绪探测。 */
@Value("${hik.playback.hls.api-secret:}")
private String apiSecret = "";
/** 实时流启动探测超时时间。现场实测流注册需 2~4 秒,网络闪断后更慢,默认 10 秒。 */
@Value("${hik.playback.live.startup-timeout-millis:10000}")
private long startupTimeoutMillis = 10000L;
/** 同一 NVR 相邻 RTSP 握手的最小间隔。 */
@Value("${hik.playback.live.start-spacing-millis:600}")
private long startSpacingMillis = 600L;
/** 每台 NVR 等待起流的最大任务数,不含正在执行的任务。 */
@Value("${hik.playback.live.max-start-queue-size:8}")
private int maxStartQueueSize = 8;
/** 单次 ZLMediaKit API 连接和读取超时时间。 */
@Value("${hik.playback.live.zlm-api-timeout-millis:300}")
private int zlmApiTimeoutMillis = 300;
}

+ 252
- 79
src/main/java/com/inspect/nvr/service/HikLoginService.java View File

@ -10,147 +10,320 @@ import com.inspect.nvr.hikVision.utils.jna.HikVisionUtils;
import com.inspect.nvr.utils.redis.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* 海康登录服务统一管理
* 统一管理海康登录会话负责按 NVR 复用失效健康检查和自动重登
*/
@Slf4j
@Service
public class HikLoginService {
/** Redis 中记录 SDK 登出失败事件的有序集合键。 */
private static final String ERROR_LOGOUT_KEY = "hik:error";
/** 登出失败时记录原始 SDK 错误,Redis 不可用不能阻断会话释放。 */
@Resource
private RedisService redisService;
/** 当前进程共享的海康 SDK 实例。 */
@Autowired
private HCNetSDK hcNetSDK;
// 使用 Caffeine 缓存10分钟未访问自动移除并登出
/** 保存已登录 NVR 的服务端连接参数,供健康检查失败后自动重登。 */
private final Map<String, NvrInfo> loginTargets = new ConcurrentHashMap<>();
/**
* NVR IP 缓存登录句柄同一个 key 的加载由 Caffeine 原子串行
* 不同 NVR 的首次登录可以并行
*/
private final Cache<String, HikLoginSession> sessionCache =
Caffeine.<String, HikLoginSession>newBuilder()
// 10分钟未被get()就过期
.expireAfterAccess(10, TimeUnit.MINUTES)
.removalListener((String ip, HikLoginSession session, RemovalCause cause) -> {
if (session != null
&& (cause == RemovalCause.EXPIRED || cause == RemovalCause.SIZE)) {
log.info("[海康]会话超时自动登出,ip: {},userID: {}",
ip, session.getUserId());
doLogout(ip, session.getUserId());
}
}).build();
.expireAfterAccess(10, TimeUnit.MINUTES)
.removalListener((String ip, HikLoginSession session, RemovalCause cause) -> {
// 只有缓存自然过期或容量淘汰时由监听器负责 SDK 登出
if (session != null
&& (cause == RemovalCause.EXPIRED
|| cause == RemovalCause.SIZE)) {
loginTargets.remove(ip);
log.info("[海康]会话超时自动登出,ip: {},userID: {}",
ip, session.getUserId());
// 释放 NVR 端登录连接避免缓存淘汰后遗留设备会话
doLogout(ip, session.getUserId());
}
}).build();
/**
* 登录
* 登录目标 NVR 并返回 SDK 用户句柄
*
* @param nvrInfo 数据库读取的 NVR 连接信息
* @return 海康 SDK 用户句柄
*/
public int login(NvrInfo nvrInfo) {
// 统一经过会话对象入口确保通道布局也被缓存
return loginSession(nvrInfo).getUserId();
}
/**
* 登录并返回包含设备通道布局的会话
* 登录并返回包含设备通道布局的会话同一 NVR 的并发首次登录只执行一次
*
* @param nvrInfo 数据库读取的 NVR 连接信息
* @return 可复用的登录会话
*/
public HikLoginSession loginSession(NvrInfo nvrInfo) {
String ip = nvrInfo.getNvrIp();
// 登录参数缺失时无法创建稳定缓存键也不能安全调用原生 SDK
if (nvrInfo == null || !hasText(nvrInfo.getNvrIp())) {
throw new IllegalArgumentException("NVR IP不能为空");
}
String ip = nvrInfo.getNvrIp().trim();
// 保存最近一次数据库连接参数健康检查发现僵死句柄后用它自动重登
loginTargets.put(ip, nvrInfo);
HikLoginSession existingSession = sessionCache.getIfPresent(ip);
// 缓存命中时直接返回周期健康检查和起流失败重试负责剔除僵死句柄
if (existingSession != null) {
log.info("[海康]登录命中缓存,ip: {},userID: {}",
ip, existingSession.getUserId());
return existingSession;
}
// 缓存命中不再经过整个 synchronized 方法只有真正建立新会话时串行化
// 避免多个并发 PTZ 请求在缓存命中时互相排队
synchronized (this) {
existingSession = sessionCache.getIfPresent(ip);
if (existingSession != null) {
log.info("[海康]登录命中缓存,ip: {},userID: {}",
ip, existingSession.getUserId());
return existingSession;
}
// Caffeine 对同一 IP 的映射计算只执行一次避免瞬时并发重复登录设备
return sessionCache.get(ip, ignored -> createSession(nvrInfo, ip));
}
// 执行登录
HCNetSDK.NET_DVR_USER_LOGIN_INFO m_strLoginInfo = HikVisionUtils.login_V40(nvrInfo.getNvrIp(), nvrInfo.getServerPort().shortValue(), nvrInfo.getAccount(), nvrInfo.getPassword());
HCNetSDK.NET_DVR_DEVICEINFO_V40 m_strDeviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40();
int userID = hcNetSDK.NET_DVR_Login_V40(m_strLoginInfo, m_strDeviceInfo);
if (userID < 0) {
int errorCode = hcNetSDK.NET_DVR_GetLastError();
throw new RuntimeException("登录失败,错误码:" + errorCode);
/**
* 主动注销旧句柄并重新登录一次供起流失败自愈使用
*
* @param nvrInfo 数据库读取的 NVR 连接信息
* @return 新建立的登录会话
*/
public HikLoginSession relogin(NvrInfo nvrInfo) {
// 先失效缓存并立即登出设备端旧句柄避免重新登录仍命中僵死会话
logout(nvrInfo == null ? null : nvrInfo.getNvrIp());
// 使用同一份数据库凭据重新建立会话成功后重新进入缓存
return loginSession(nvrInfo);
}
/** 定时用轻量工作状态接口检查缓存句柄,失败时自动注销并重登。 */
@Scheduled(fixedDelayString = "${hik.login.health-check-interval-millis:60000}")
public void healthCheckSessions() {
// 遍历已知登录目标而非仅遍历会话缓存断网重登失败后仍会在下一轮继续恢复
for (Map.Entry<String, NvrInfo> entry
: new ArrayList<>(loginTargets.entrySet())) {
String ip = entry.getKey();
NvrInfo target = entry.getValue();
HikLoginSession session = sessionCache.getIfPresent(ip);
// 当前存在且健康的句柄继续复用不制造额外登录连接
if (session != null && isSessionHealthy(ip, session)) {
continue;
}
log.warn("[海康]登录会话健康检查失败,准备自动重登,ip: {},userID: {}",
ip, session == null ? null : session.getUserId());
// 先释放缓存和设备端旧句柄确保后续登录不会再次命中失效对象
logout(ip);
// 没有可用数据库连接参数时只能完成失效等待下一次业务请求重新提供参数
if (target == null) {
continue;
}
m_strDeviceInfo.read();
HCNetSDK.NET_DVR_DEVICEINFO_V30 deviceInfo = m_strDeviceInfo.struDeviceV30;
HikLoginSession session = new HikLoginSession(
userID,
Byte.toUnsignedInt(deviceInfo.byChanNum),
Byte.toUnsignedInt(deviceInfo.byStartChan),
Byte.toUnsignedInt(deviceInfo.byIPChanNum)
+ Byte.toUnsignedInt(deviceInfo.byHighDChanNum) * 256,
Byte.toUnsignedInt(deviceInfo.byStartDChan));
// 放入缓存自动开始计时10分钟
sessionCache.put(nvrInfo.getNvrIp(), session);
log.info("[海康]登录成功,ip:{},userID:{}", ip, userID);
return session;
try {
// 自动重登恢复会话设备网络恢复后无需重启容器
loginSession(target);
log.info("[海康]登录会话自动重登成功,ip: {}", ip);
} catch (RuntimeException failure) {
// 重登失败保留告警并等待下一轮健康检查或业务请求重试
log.warn("[海康]登录会话自动重登失败,ip: {},原因: {}",
ip, failure.getMessage());
}
}
}
/**
* 对指定缓存会话调用海康轻量工作状态接口
*
* @param ip NVR IP仅用于诊断日志
* @param session 待检查的缓存会话
* @return SDK 确认句柄有效时返回 true
*/
private boolean isSessionHealthy(String ip, HikLoginSession session) {
// 空会话或负数句柄必定无效不再调用原生接口
if (session == null || session.getUserId() < 0) {
return false;
}
HCNetSDK.NET_DVR_WORKSTATE_V30 workState =
new HCNetSDK.NET_DVR_WORKSTATE_V30();
// 把结构体内存同步给 JNA SDK 写入设备工作状态
workState.write();
boolean healthy;
try {
// 轻量查询验证登录句柄和 NVR 连接是否仍然有效
healthy = hcNetSDK.NET_DVR_GetDVRWorkState_V30(
session.getUserId(), workState);
} catch (RuntimeException sdkFailure) {
// 原生 SDK 抛异常同样视为句柄失效让定时任务继续执行注销和重登
log.warn("[海康]会话健康检查调用异常,ip: {},userID: {},原因: {}",
ip, session.getUserId(), sdkFailure.getMessage());
return false;
}
// 健康检查失败时记录原始 SDK 错误码便于区分网络与句柄失效
if (!healthy) {
log.warn("[海康]会话失效,ip: {},userID: {},错误码: {}",
ip, session.getUserId(), hcNetSDK.NET_DVR_GetLastError());
return false;
}
return true;
}
/**
* 记录注销失败信息到 Redis
* 调用海康 SDK 建立新会话并读取设备通道布局
*
* @param nvrInfo 数据库读取的 NVR 连接信息
* @param ip 已规范化的缓存键
* @return 新登录会话
*/
private void recordLogoutError(String ip, Integer userID, int errorCode) {
JSONObject json = new JSONObject();
json.put("ip", ip);
json.put("userID", userID);
json.put("errorCode", errorCode);
json.put("time", System.currentTimeMillis());
redisService.redisTemplate.opsForZSet().add(ERROR_LOGOUT_KEY, json.toJSONString(), System.currentTimeMillis());
private HikLoginSession createSession(NvrInfo nvrInfo, String ip) {
// 组装海康 V40 登录结构体账号密码只在服务端内存和 SDK 调用中使用
HCNetSDK.NET_DVR_USER_LOGIN_INFO loginInfo = HikVisionUtils.login_V40(
ip,
nvrInfo.getServerPort().shortValue(),
nvrInfo.getAccount(),
nvrInfo.getPassword());
HCNetSDK.NET_DVR_DEVICEINFO_V40 deviceInfoV40 =
new HCNetSDK.NET_DVR_DEVICEINFO_V40();
// 发起 SDK 登录并取得后续设备调用使用的用户句柄
int userId = hcNetSDK.NET_DVR_Login_V40(loginInfo, deviceInfoV40);
// 负数表示 SDK 登录失败原始错误码保留在异常消息中供上层分类
if (userId < 0) {
int errorCode = hcNetSDK.NET_DVR_GetLastError();
throw new RuntimeException("登录失败,错误码:" + errorCode);
}
// 从原生内存读取 NVR 返回的模拟与数字通道布局
deviceInfoV40.read();
HCNetSDK.NET_DVR_DEVICEINFO_V30 deviceInfo = deviceInfoV40.struDeviceV30;
HikLoginSession session = new HikLoginSession(
userId,
Byte.toUnsignedInt(deviceInfo.byChanNum),
Byte.toUnsignedInt(deviceInfo.byStartChan),
Byte.toUnsignedInt(deviceInfo.byIPChanNum)
+ Byte.toUnsignedInt(deviceInfo.byHighDChanNum) * 256,
Byte.toUnsignedInt(deviceInfo.byStartDChan));
log.info("[海康]登录成功,ip: {},userID: {}", ip, userId);
return session;
}
/**
* 登出具体实现
* 记录注销失败信息到 RedisRedis 异常不能影响会话释放主流程
*
* @param ip NVR IP
* @param userId SDK 用户句柄
* @param errorCode SDK 原始错误码
*/
public void doLogout(String ip, Integer userID) {
if (userID != null) {
// 调用登出SDK
boolean isLogout = hcNetSDK.NET_DVR_Logout(userID);
if (isLogout) {
log.info("[海康]登出成功,ip: {},userID: {}", ip, userID);
} else {
int errorCode = hcNetSDK.NET_DVR_GetLastError();
log.error("[海康]登出失败,ip: {},userID: {},错误码: {}", ip, userID, errorCode);
// 登出失败日志记录到Redis中
recordLogoutError(ip, userID, errorCode);
}
private void recordLogoutError(String ip, Integer userId, int errorCode) {
// 单元测试或降级运行没有 Redis 时只保留应用日志
if (redisService == null || redisService.redisTemplate == null) {
return;
}
try {
JSONObject json = new JSONObject();
json.put("ip", ip);
json.put("userID", userId);
json.put("errorCode", errorCode);
json.put("time", System.currentTimeMillis());
// 按发生时间写入有序集合供运维追查设备端未释放句柄
redisService.redisTemplate.opsForZSet().add(
ERROR_LOGOUT_KEY, json.toJSONString(), System.currentTimeMillis());
} catch (RuntimeException failure) {
// Redis 暂时不可用时不反向阻塞 SDK 会话清理
log.warn("[海康]记录登出失败事件到Redis失败,ip: {},原因: {}",
ip, failure.getMessage());
}
}
/**
* 登出
* 立即调用 SDK 登出指定用户句柄
*
* @param ip NVR IP
* @param userId SDK 用户句柄
*/
public synchronized void logout(String ip) {
HikLoginSession session = sessionCache.getIfPresent(ip);
if (session != null) {
sessionCache.invalidate(ip);
public void doLogout(String ip, Integer userId) {
// 空句柄没有可释放的设备会话
if (userId == null) {
return;
}
boolean logoutSucceeded;
try {
// 调用海康 SDK 断开设备端登录连接
logoutSucceeded = hcNetSDK.NET_DVR_Logout(userId);
} catch (RuntimeException sdkFailure) {
// 原生注销异常不能阻断缓存失效或后续重新登录
log.error("[海康]登出调用异常,ip: {},userID: {},原因: {}",
ip, userId, sdkFailure.getMessage());
return;
}
// SDK 确认成功时记录完整会话生命周期
if (logoutSucceeded) {
log.info("[海康]登出成功,ip: {},userID: {}", ip, userId);
return;
}
int errorCode = hcNetSDK.NET_DVR_GetLastError();
log.error("[海康]登出失败,ip: {},userID: {},错误码: {}",
ip, userId, errorCode);
// 保留设备端登出失败证据 Redis 故障不影响当前调用返回
recordLogoutError(ip, userId, errorCode);
}
/**
* 登出所有用户
* 失效并立即登出指定 NVR 的缓存会话
*
* @param ip NVR IP
*/
public void logout(String ip) {
// IP 不能定位缓存会话按幂等操作直接返回
if (!hasText(ip)) {
return;
}
String normalizedIp = ip.trim();
// 原子移除会话并取得旧句柄避免只删缓存却遗留设备端连接
HikLoginSession session = sessionCache.asMap().remove(normalizedIp);
loginTargets.remove(normalizedIp);
// 缓存没有该 NVR 时保持幂等不调用无效 SDK 句柄
if (session == null) {
return;
}
// 显式注销必须立即释放设备端登录会话
doLogout(normalizedIp, session.getUserId());
}
/** 登出当前进程缓存的所有海康用户。 */
public void logoutAll() {
// 获取所有缓存的IP和userID
sessionCache.asMap().forEach((ip, session) -> {
doLogout(ip, session.getUserId());
});
// 清空整个缓存
sessionCache.invalidateAll();
// 使用 IP 快照逐个走显式登出确保每个设备句柄都真正释放
for (String ip : new ArrayList<>(sessionCache.asMap().keySet())) {
logout(ip);
}
log.info("[海康]所有用户已登出");
}
/**
* 检查是否已登录同时刷新过期时间
* 检查指定 NVR 是否存在缓存会话同时刷新访问过期时间
*
* @param ip NVR IP
* @return 缓存中存在会话时返回 true
*/
public boolean isLoggedIn(String ip) {
return sessionCache.getIfPresent(ip) != null;
// IP 不可能命中登录缓存
if (!hasText(ip)) {
return false;
}
return sessionCache.getIfPresent(ip.trim()) != null;
}
/** 判断文本是否包含非空白内容。 */
private boolean hasText(String value) {
// 同时覆盖 null空串和纯空白输入
return value != null && !value.trim().isEmpty();
}
}

Loading…
Cancel
Save