|
@@ -1,65 +1,298 @@
|
|
|
package cn.iocoder.yudao.module.pms.hikvision;
|
|
package cn.iocoder.yudao.module.pms.hikvision;
|
|
|
|
|
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
-import cn.iocoder.yudao.module.pms.hikvision.vo.MonitorPointsPageReqVO;
|
|
|
|
|
|
|
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
|
|
|
|
+import cn.iocoder.yudao.module.pms.hikvision.vo.HikvisionTreeNodeRespVO;
|
|
|
import com.alibaba.fastjson.JSON;
|
|
import com.alibaba.fastjson.JSON;
|
|
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
import com.hikvision.artemis.sdk.ArtemisHttpUtil;
|
|
import com.hikvision.artemis.sdk.ArtemisHttpUtil;
|
|
|
import com.hikvision.artemis.sdk.config.ArtemisConfig;
|
|
import com.hikvision.artemis.sdk.config.ArtemisConfig;
|
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
|
|
|
+import io.swagger.v3.oas.annotations.Parameter;
|
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
-import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.validation.annotation.Validated;
|
|
import org.springframework.validation.annotation.Validated;
|
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
|
|
+import org.springframework.web.bind.annotation.RequestParam;
|
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
|
|
|
|
|
|
+import javax.validation.constraints.NotBlank;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Collections;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
|
|
|
|
|
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 海康 Artemis 区域和监控点接口。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 树形列表采用懒加载方式:前端首次传入 {@code -1},后端先获取根区域,再加载根区域的下一层节点;
|
|
|
|
|
+ * 用户继续展开某个区域时,传入该区域的 {@code indexCode}。后端分别查询该区域的直接子区域和直属监控点,
|
|
|
|
|
+ * 并将两种资源转换成统一的 {@link HikvisionTreeNodeRespVO} 返回给前端。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ */
|
|
|
@Tag(name = "管理后台 - hikvision 实时视频 录像相关接口")
|
|
@Tag(name = "管理后台 - hikvision 实时视频 录像相关接口")
|
|
|
@RequestMapping("/vms")
|
|
@RequestMapping("/vms")
|
|
|
@RestController
|
|
@RestController
|
|
|
@Validated
|
|
@Validated
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
public class HikvisionController {
|
|
public class HikvisionController {
|
|
|
|
|
|
|
|
- @Value("${hikvision.appKey}")
|
|
|
|
|
- private String appKey;
|
|
|
|
|
-
|
|
|
|
|
- @Value("${hikvision.appSecret}")
|
|
|
|
|
- private String appSecret;
|
|
|
|
|
-
|
|
|
|
|
- @Value("${hikvision.host}")
|
|
|
|
|
- private String host;
|
|
|
|
|
-
|
|
|
|
|
- @Value("${hikvision.artemisPath}")
|
|
|
|
|
- private String artemisPath;
|
|
|
|
|
-
|
|
|
|
|
- @Value("${hikvision.monitorPoints}")
|
|
|
|
|
- private String monitorPoints;
|
|
|
|
|
-
|
|
|
|
|
- @GetMapping("/monitorPoints")
|
|
|
|
|
- @Operation(summary = "查询监控点列表v2")
|
|
|
|
|
- public String monitorPoints(MonitorPointsPageReqVO pageReqVO) {
|
|
|
|
|
- ArtemisConfig config = new ArtemisConfig();
|
|
|
|
|
- config.setHost(host); // 代理API网关nginx服务器ip端口
|
|
|
|
|
- config.setAppKey(appKey); // 秘钥appkey
|
|
|
|
|
- config.setAppSecret(appSecret);// 秘钥appSecret
|
|
|
|
|
- Map<String, String> paramMap = new HashMap<String, String>();// post请求Form表单参数
|
|
|
|
|
- paramMap.put("pageNo", pageReqVO.getPageNo().toString());
|
|
|
|
|
- paramMap.put("pageSize", pageReqVO.getPageSize().toString());
|
|
|
|
|
- String body = JSON.toJSON(paramMap).toString();
|
|
|
|
|
- Map<String, String> path = new HashMap<String, String>(2) {
|
|
|
|
|
- {
|
|
|
|
|
- put("https://", monitorPoints);
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
- String result = StrUtil.EMPTY;
|
|
|
|
|
|
|
+ /** 获取根区域信息。 */
|
|
|
|
|
+ private static final String ROOT_ADDRESS = "/api/resource/v1/regions/root";
|
|
|
|
|
+ /** 根据父区域编号查询直接子区域,不会返回更深层的孙区域。 */
|
|
|
|
|
+ private static final String SUB_REGIONS_ADDRESS = "/api/resource/v2/regions/subRegions";
|
|
|
|
|
+ /** 根据区域编号查询直属资源,不会查询子区域中的资源。 */
|
|
|
|
|
+ private static final String SUB_RESOURCES_ADDRESS = "/api/irds/v2/resource/subResources";
|
|
|
|
|
+ /** 返回给前端的区域节点类型。 */
|
|
|
|
|
+ private static final String NODE_TYPE_REGION = "region";
|
|
|
|
|
+ /** 返回给前端的监控点节点类型。 */
|
|
|
|
|
+ private static final String NODE_TYPE_CAMERA = "camera";
|
|
|
|
|
+ /** 海康接口用于筛选监控点资源及其所属区域树的资源类型。 */
|
|
|
|
|
+ private static final String RESOURCE_TYPE_CAMERA = "camera";
|
|
|
|
|
+ /** 海康接口单页允许查询的最大数量。 */
|
|
|
|
|
+ private static final int MAX_PAGE_SIZE = 1000;
|
|
|
|
|
+
|
|
|
|
|
+ /** 包含 host、appKey、appSecret 的海康 SDK 单例配置。 */
|
|
|
|
|
+ private final ArtemisConfig artemisConfig;
|
|
|
|
|
+ /** 负责将环境配置的 artemisPath 与具体接口地址拼接成 SDK 请求路径。 */
|
|
|
|
|
+ private final HikvisionArtemisConfiguration artemisConfiguration;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取树形列表节点。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 当 {@code regionIndexCode=-1} 时,先调用根区域接口获得真实的根区域 {@code indexCode},
|
|
|
|
|
+ * 再查询根区域的直接子区域和直属监控点,最终返回一个带有 {@code children} 的根节点。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 当传入真实区域编号时,只返回该区域的下一层节点,供前端在用户展开区域时追加到对应节点。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param regionIndexCode 当前展开区域的唯一标识;传 {@code -1} 表示加载根区域
|
|
|
|
|
+ * @return 根节点及其下一层,或者指定区域的下一层节点
|
|
|
|
|
+ */
|
|
|
|
|
+ @GetMapping("/tree")
|
|
|
|
|
+ @Operation(summary = "获取海康区域和监控点懒加载树")
|
|
|
|
|
+ public CommonResult<List<HikvisionTreeNodeRespVO>> treeChildren(
|
|
|
|
|
+ @Parameter(description = "区域唯一标识;传 -1 加载根节点及其下一层", required = true)
|
|
|
|
|
+ @RequestParam("regionIndexCode")
|
|
|
|
|
+ @NotBlank(message = "区域编号不能为空") String regionIndexCode) {
|
|
|
try {
|
|
try {
|
|
|
- result = ArtemisHttpUtil.doPostStringArtemis(config, path, body, null, null, "application/json");
|
|
|
|
|
|
|
+ if ("-1".equals(regionIndexCode)) {
|
|
|
|
|
+ // 首次加载时保留根节点,并提前填充根节点的下一层 children。
|
|
|
|
|
+ return success(Collections.singletonList(queryRootWithChildren()));
|
|
|
|
|
+ }
|
|
|
|
|
+ return success(queryTreeChildren(regionIndexCode));
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- result = "接口异常";
|
|
|
|
|
|
|
+ log.error("查询海康区域树节点失败,regionIndexCode: {}", regionIndexCode, e);
|
|
|
|
|
+ return error(500, "查询海康区域树节点失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取根区域,并将根区域的下一层节点放入 children。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return 带有下一层节点的根区域
|
|
|
|
|
+ * @throws Exception Artemis SDK 请求异常,或根区域响应缺少 indexCode
|
|
|
|
|
+ */
|
|
|
|
|
+ private HikvisionTreeNodeRespVO queryRootWithChildren() throws Exception {
|
|
|
|
|
+ JSONObject root = parseArtemisData(invokeArtemis(ROOT_ADDRESS, Collections.emptyMap()));
|
|
|
|
|
+ String rootIndexCode = root.getString("indexCode");
|
|
|
|
|
+ if (StrUtil.isBlank(rootIndexCode)) {
|
|
|
|
|
+ throw new IllegalStateException("海康根区域响应缺少 indexCode");
|
|
|
|
|
+ }
|
|
|
|
|
+ List<HikvisionTreeNodeRespVO> children = queryTreeChildren(rootIndexCode);
|
|
|
|
|
+ return HikvisionTreeNodeRespVO.builder()
|
|
|
|
|
+ .indexCode(rootIndexCode)
|
|
|
|
|
+ .name(root.getString("name"))
|
|
|
|
|
+ .nodeType(NODE_TYPE_REGION)
|
|
|
|
|
+ // 根区域接口在部分版本中不返回 leaf,此时根据实际 children 判断。
|
|
|
|
|
+ .leaf(getBooleanOrDefault(root, "leaf", children.isEmpty()))
|
|
|
|
|
+ .parentIndexCode(root.getString("parentIndexCode"))
|
|
|
|
|
+ // 根区域接口在部分版本中不返回 available;能够成功查询到根区域时默认可用。
|
|
|
|
|
+ .available(getBooleanOrDefault(root, "available", true))
|
|
|
|
|
+ .children(children)
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询指定区域的下一层节点,返回顺序为“直接子区域在前、直属监控点在后”。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param regionIndexCode 当前区域唯一标识
|
|
|
|
|
+ * @return 当前区域的下一层树节点
|
|
|
|
|
+ * @throws Exception Artemis SDK 请求异常
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<HikvisionTreeNodeRespVO> queryTreeChildren(String regionIndexCode) throws Exception {
|
|
|
|
|
+ List<HikvisionTreeNodeRespVO> nodes = new ArrayList<>();
|
|
|
|
|
+ nodes.addAll(querySubRegions(regionIndexCode));
|
|
|
|
|
+ nodes.addAll(querySubCameras(regionIndexCode));
|
|
|
|
|
+ return nodes;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询父区域的所有直接子区域,并转换成统一树节点。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param parentIndexCode 父区域唯一标识
|
|
|
|
|
+ * @return 直接子区域节点
|
|
|
|
|
+ * @throws Exception Artemis SDK 请求异常
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<HikvisionTreeNodeRespVO> querySubRegions(String parentIndexCode) throws Exception {
|
|
|
|
|
+ Map<String, Object> paramMap = new HashMap<>();
|
|
|
|
|
+ paramMap.put("parentIndexCode", parentIndexCode);
|
|
|
|
|
+ // resourceType=camera 表示只返回当前账号具有监控点相关权限的区域树。
|
|
|
|
|
+ paramMap.put("resourceType", RESOURCE_TYPE_CAMERA);
|
|
|
|
|
+ List<JSONObject> regions = queryAllPages(SUB_REGIONS_ADDRESS, paramMap);
|
|
|
|
|
+ List<HikvisionTreeNodeRespVO> nodes = new ArrayList<>(regions.size());
|
|
|
|
|
+ for (JSONObject region : regions) {
|
|
|
|
|
+ nodes.add(HikvisionTreeNodeRespVO.builder()
|
|
|
|
|
+ .indexCode(region.getString("indexCode"))
|
|
|
|
|
+ .name(region.getString("name"))
|
|
|
|
|
+ .nodeType(NODE_TYPE_REGION)
|
|
|
|
|
+ .leaf(region.getBoolean("leaf"))
|
|
|
|
|
+ .parentIndexCode(region.getString("parentIndexCode"))
|
|
|
|
|
+ .available(region.getBoolean("available"))
|
|
|
|
|
+ .build());
|
|
|
|
|
+ }
|
|
|
|
|
+ return nodes;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询直接挂在指定区域下的所有监控点,并转换成统一树节点。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 使用 subResources 接口而不是包含子区域的查询接口,避免把孙区域中的监控点提前挂到当前区域,
|
|
|
|
|
+ * 从而保证懒加载树的父子层级正确且不会重复显示监控点。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param regionIndexCode 当前区域唯一标识
|
|
|
|
|
+ * @return 当前区域的直属监控点节点
|
|
|
|
|
+ * @throws Exception Artemis SDK 请求异常
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<HikvisionTreeNodeRespVO> querySubCameras(String regionIndexCode) throws Exception {
|
|
|
|
|
+ Map<String, Object> paramMap = new HashMap<>();
|
|
|
|
|
+ paramMap.put("regionIndexCode", regionIndexCode);
|
|
|
|
|
+ paramMap.put("resourceType", RESOURCE_TYPE_CAMERA);
|
|
|
|
|
+ List<JSONObject> cameras = queryAllPages(SUB_RESOURCES_ADDRESS, paramMap);
|
|
|
|
|
+ List<HikvisionTreeNodeRespVO> nodes = new ArrayList<>(cameras.size());
|
|
|
|
|
+ for (JSONObject camera : cameras) {
|
|
|
|
|
+ nodes.add(HikvisionTreeNodeRespVO.builder()
|
|
|
|
|
+ .indexCode(camera.getString("indexCode"))
|
|
|
|
|
+ .name(camera.getString("name"))
|
|
|
|
|
+ .nodeType(NODE_TYPE_CAMERA)
|
|
|
|
|
+ .parentIndexCode(camera.getString("regionIndexCode"))
|
|
|
|
|
+ // 某些平台版本会扩展返回 available;没有该字段时,已查询到的监控点默认可用。
|
|
|
|
|
+ .available(getBooleanOrDefault(camera, "available", true))
|
|
|
|
|
+ // 保留平台实际返回的在线状态;未返回时保持 null,不伪造在线或离线状态。
|
|
|
|
|
+ .onlineStatus(camera.getBoolean("onlineStatus"))
|
|
|
|
|
+ // camera 不允许继续懒加载,响应中不需要 children 字段。
|
|
|
|
|
+ .children(null)
|
|
|
|
|
+ .build());
|
|
|
|
|
+ }
|
|
|
|
|
+ return nodes;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自动分页查询海康列表接口,确保返回当前层的全部数据。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 调用方只需要提供业务查询条件,本方法负责补充 {@code pageNo} 和 {@code pageSize}。
|
|
|
|
|
+ * 每次使用海康允许的最大分页数量 1000;只要已累计数量仍小于响应中的 {@code total},
|
|
|
|
|
+ * 就继续查询下一页。这样即使某个区域下超过 1000 个节点,也不会漏数据。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param address 海康接口相对地址
|
|
|
|
|
+ * @param queryParams 不包含分页字段的业务查询条件
|
|
|
|
|
+ * @return 所有分页中的对象列表
|
|
|
|
|
+ * @throws Exception Artemis SDK 请求异常,或海康接口返回失败
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<JSONObject> queryAllPages(String address, Map<String, Object> queryParams) throws Exception {
|
|
|
|
|
+ List<JSONObject> result = new ArrayList<>();
|
|
|
|
|
+ int pageNo = 1;
|
|
|
|
|
+ while (true) {
|
|
|
|
|
+ // 每页复制一份参数,避免 pageNo/pageSize 修改调用方传入的原始 Map。
|
|
|
|
|
+ Map<String, Object> pageParams = new HashMap<>(queryParams);
|
|
|
|
|
+ pageParams.put("pageNo", pageNo);
|
|
|
|
|
+ pageParams.put("pageSize", MAX_PAGE_SIZE);
|
|
|
|
|
+ JSONObject data = parseArtemisData(invokeArtemis(address, pageParams));
|
|
|
|
|
+ JSONArray list = data.getJSONArray("list");
|
|
|
|
|
+ // 没有列表或列表为空,说明当前页及后续页均无可追加数据。
|
|
|
|
|
+ if (list == null || list.isEmpty()) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (int i = 0; i < list.size(); i++) {
|
|
|
|
|
+ result.add(list.getJSONObject(i));
|
|
|
|
|
+ }
|
|
|
|
|
+ Integer total = data.getInteger("total");
|
|
|
|
|
+ // 部分接口可能不返回 total,此时以当前页作为最终结果,避免无限请求。
|
|
|
|
|
+ if (total == null || result.size() >= total) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ pageNo++;
|
|
|
}
|
|
}
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 读取海康响应中的布尔字段,并在当前接口版本未返回该字段时使用业务默认值。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>{@link JSONObject#getBoolean(String)} 在字段不存在时返回 {@code null}。这里先保留接口的真实值,
|
|
|
|
|
+ * 只有返回值为 {@code null} 时才使用默认值,避免把接口明确返回的 {@code false} 错误覆盖为
|
|
|
|
|
+ * {@code true}。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param source 海康接口返回的单个对象
|
|
|
|
|
+ * @param fieldName 要读取的字段名称
|
|
|
|
|
+ * @param defaultValue 字段不存在时采用的默认值
|
|
|
|
|
+ * @return 接口字段值,或业务默认值
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean getBooleanOrDefault(JSONObject source, String fieldName, boolean defaultValue) {
|
|
|
|
|
+ Boolean value = source.getBoolean(fieldName);
|
|
|
|
|
+ return value != null ? value : defaultValue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 校验海康统一响应并取出 data 对象。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param response 海康接口原始 JSON 字符串
|
|
|
|
|
+ * @return 响应中的 data;接口未返回 data 时返回空对象
|
|
|
|
|
+ * @throws IllegalStateException 海康业务返回码不是 0
|
|
|
|
|
+ */
|
|
|
|
|
+ private JSONObject parseArtemisData(String response) {
|
|
|
|
|
+ JSONObject responseObject = JSON.parseObject(response);
|
|
|
|
|
+ // 海康成功码是字符串 "0";其他返回码都交由上层统一转换为本系统错误响应。
|
|
|
|
|
+ if (!"0".equals(responseObject.getString("code"))) {
|
|
|
|
|
+ throw new IllegalStateException("海康接口返回失败: " + responseObject.getString("msg"));
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject data = responseObject.getJSONObject("data");
|
|
|
|
|
+ return data != null ? data : new JSONObject();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 执行最底层 Artemis POST 请求。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 此方法不吞掉异常,由公开的树形接口统一记录日志并转换为 {@link CommonResult} 错误响应。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param address 不包含环境配置 artemisPath 的接口地址
|
|
|
|
|
+ * @param body 将被序列化为 JSON 的请求体
|
|
|
|
|
+ * @return 海康接口原始响应字符串
|
|
|
|
|
+ * @throws Exception Artemis SDK 请求异常
|
|
|
|
|
+ */
|
|
|
|
|
+ private String invokeArtemis(String address, Object body) throws Exception {
|
|
|
|
|
+ return ArtemisHttpUtil.doPostStringArtemis(
|
|
|
|
|
+ artemisConfig,
|
|
|
|
|
+ artemisConfiguration.buildArtemisPath(address),
|
|
|
|
|
+ JSON.toJSONString(body), null, null, "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|