From 228c1e07d117d1f7f11dec8381eb951d92bcf2e8 Mon Sep 17 00:00:00 2001
From: liujie <1793218484@qq.com>
Date: 星期三, 22 七月 2026 09:50:09 +0800
Subject: [PATCH] 监控模块

---
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/VideoMonitorControlRequest.java  |   42 ++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/EzvizOpenApiUtils.java            |  338 ++++++++++++++++++++++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TDeviceService.java            |    2 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TDeviceController.java      |   78 ++++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/VideoMonitorPageQuery.java       |   31 ++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TDeviceServiceImpl.java   |    6 
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/VideoMonitorController.java |  263 +++++++++++++++++
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/DevicePageList.java              |   12 
 ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TDevice.java                |   22 +
 ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/VideoMonitorDeviceVO.java           |   54 +++
 ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TDeviceMapper.xml                    |   23 +
 11 files changed, 848 insertions(+), 23 deletions(-)

diff --git a/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TDevice.java b/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TDevice.java
index d3e8aaf..5bc60b1 100644
--- a/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TDevice.java
+++ b/ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TDevice.java
@@ -32,12 +32,16 @@
     private String deviceName;
 
     @ApiModelProperty(value = "设备序列号")
-    @TableField("serial_no")
-    private String serialNo;
+    @TableField("device_serial")
+    private String deviceSerial;
 
-    @ApiModelProperty(value = "设备型号")
-    @TableField("equipment_model")
-    private String equipmentModel;
+    @ApiModelProperty(value = "设备品牌(1=大华,2=海康,3=萤石)")
+    @TableField("device_brand")
+    private Integer deviceBrand;
+
+    @ApiModelProperty(value = "设备验证码")
+    @TableField("validate_code")
+    private String validateCode;
 
     @ApiModelProperty(value = "合作商类型")
     @TableField("partner_type")
@@ -62,4 +66,12 @@
     @ApiModelProperty(value = "在线状态(0=不在线,1=在线)")
     @TableField("online_status")
     private Integer onlineStatus;
+
+    @ApiModelProperty(value = "下发状态(0=未下发,1=已下发,2=下发失败)")
+    @TableField("issue_status")
+    private Integer issueStatus;
+
+    @ApiModelProperty(value = "下发结果")
+    @TableField("issue_msg")
+    private String issueMsg;
 }
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TDeviceController.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TDeviceController.java
index ef482e6..41402e4 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TDeviceController.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TDeviceController.java
@@ -1,5 +1,6 @@
 package com.ruoyi.other.controller;
 
+import com.alibaba.fastjson2.JSONObject;
 import com.ruoyi.common.core.web.domain.AjaxResult;
 import com.ruoyi.common.core.web.page.PageInfo;
 import com.ruoyi.common.log.annotation.Log;
@@ -9,6 +10,7 @@
 import com.ruoyi.other.api.domain.TDevice;
 import com.ruoyi.other.query.DevicePageList;
 import com.ruoyi.other.service.TDeviceService;
+import com.ruoyi.other.util.EzvizOpenApiUtils;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.*;
@@ -27,6 +29,9 @@
 
     @Resource
     private TDeviceService deviceService;
+
+    @Resource
+    private EzvizOpenApiUtils ezvizOpenApiUtils;
 
     @RequiresPermissions(value = {"/deviceManagement/list"}, logical = Logical.OR)
     @ResponseBody
@@ -49,6 +54,9 @@
         }
         if (device.getOnlineStatus() == null) {
             device.setOnlineStatus(1);
+        }
+        if (device.getIssueStatus() == null) {
+            device.setIssueStatus(0);
         }
         deviceService.save(device);
         return AjaxResult.success();
@@ -79,6 +87,40 @@
         return AjaxResult.success();
     }
 
+    @RequiresPermissions(value = {"/deviceManagement/issue"}, logical = Logical.OR)
+    @ResponseBody
+    @PostMapping("/issueDevice/{id}")
+    @ApiOperation(value = "下发设备到萤石平台", tags = {"管理后台-设备管理"})
+    @Log(title = "【设备管理】下发设备到萤石平台", businessType = BusinessType.UPDATE)
+    public AjaxResult issueDevice(@PathVariable Integer id) {
+        TDevice device = deviceService.getById(id);
+        if (device == null) {
+            return AjaxResult.error("设备不存在");
+        }
+        if (Integer.valueOf(1).equals(device.getIssueStatus())) {
+            return AjaxResult.success("设备已下发");
+        }
+        JSONObject ezvizResult;
+        try {
+            ezvizResult = ezvizOpenApiUtils.addDevice(device.getDeviceSerial(), device.getValidateCode());
+        } catch (Exception e) {
+            device.setIssueStatus(2);
+            device.setIssueMsg("萤石设备下发失败:" + e.getMessage());
+            deviceService.updateById(device);
+            return AjaxResult.error(device.getIssueMsg());
+        }
+        if (!isEzvizSuccess(ezvizResult)) {
+            device.setIssueStatus(2);
+            device.setIssueMsg(ezvizErrorMessage("萤石设备下发失败", ezvizResult));
+            deviceService.updateById(device);
+            return AjaxResult.error(device.getIssueMsg());
+        }
+        device.setIssueStatus(1);
+        device.setIssueMsg("下发成功");
+        deviceService.updateById(device);
+        return AjaxResult.success();
+    }
+
     @RequiresPermissions(value = {"/deviceManagement/del"}, logical = Logical.OR)
     @ResponseBody
     @DeleteMapping("/delDevice")
@@ -87,6 +129,19 @@
     public AjaxResult<TDevice> delDevice(Integer[] id) {
         if (id == null || id.length == 0) {
             return AjaxResult.error("请选择要删除的设备");
+        }
+        for (TDevice device : deviceService.listByIds(Arrays.asList(id))) {
+            if (Integer.valueOf(1).equals(device.getIssueStatus())) {
+                JSONObject ezvizResult;
+                try {
+                    ezvizResult = ezvizOpenApiUtils.deleteDevice(device.getDeviceSerial());
+                } catch (Exception e) {
+                    return AjaxResult.error("萤石设备删除失败:" + e.getMessage());
+                }
+                if (!isEzvizSuccess(ezvizResult)) {
+                    return AjaxResult.error(ezvizErrorMessage("萤石设备删除失败", ezvizResult));
+                }
+            }
         }
         deviceService.removeByIds(Arrays.asList(id));
         return AjaxResult.success();
@@ -99,18 +154,33 @@
         if (!StringUtils.hasText(device.getDeviceName())) {
             return AjaxResult.error("设备名称不能为空");
         }
-        if (!StringUtils.hasText(device.getSerialNo())) {
+        if (!StringUtils.hasText(device.getDeviceSerial())) {
             return AjaxResult.error("设备序列号不能为空");
         }
-        if (!StringUtils.hasText(device.getEquipmentModel())) {
-            return AjaxResult.error("设备型号不能为空");
+        if (device.getDeviceBrand() == null) {
+            return AjaxResult.error("设备品牌不能为空");
+        }
+        if (!StringUtils.hasText(device.getValidateCode())) {
+            return AjaxResult.error("设备验证码不能为空");
         }
         if (device.getPartnerId() == null) {
             return AjaxResult.error("合作商不能为空");
         }
-        if (deviceService.existsBySerialNo(device.getSerialNo(), excludeId)) {
+        if (deviceService.existsByDeviceSerial(device.getDeviceSerial(), excludeId)) {
             return AjaxResult.error("设备序列号已存在");
         }
         return null;
     }
+
+    private boolean isEzvizSuccess(JSONObject result) {
+        return result != null && "200".equals(result.getString("code"));
+    }
+
+    private String ezvizErrorMessage(String defaultMessage, JSONObject result) {
+        if (result == null) {
+            return defaultMessage;
+        }
+        String message = result.getString("msg");
+        return StringUtils.hasText(message) ? message : defaultMessage;
+    }
 }
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/VideoMonitorController.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/VideoMonitorController.java
new file mode 100644
index 0000000..5689735
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/VideoMonitorController.java
@@ -0,0 +1,263 @@
+package com.ruoyi.other.controller;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.common.core.web.domain.AjaxResult;
+import com.ruoyi.common.core.web.page.PageInfo;
+import com.ruoyi.other.api.domain.TDevice;
+import com.ruoyi.other.query.VideoMonitorControlRequest;
+import com.ruoyi.other.query.VideoMonitorPageQuery;
+import com.ruoyi.other.service.TDeviceService;
+import com.ruoyi.other.util.EzvizOpenApiUtils;
+import com.ruoyi.other.vo.VideoMonitorDeviceVO;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.BeanUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 视频监控控制器
+ *
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/video-monitor")
+public class VideoMonitorController {
+
+    private static final String SUCCESS_CODE = "200";
+
+    @Resource
+    private TDeviceService deviceService;
+
+    @Resource
+    private EzvizOpenApiUtils ezvizOpenApiUtils;
+
+    @ResponseBody
+    @GetMapping("/pageList")
+    @ApiOperation(value = "获取视频监控列表", tags = {"管理后台-视频监控"})
+    public AjaxResult<PageInfo<VideoMonitorDeviceVO>> pageList(VideoMonitorPageQuery query) {
+        Page<TDevice> page = deviceService.lambdaQuery()
+                .eq(TDevice::getIssueStatus, 1)
+                .like(StringUtils.hasText(query.getDeviceName()), TDevice::getDeviceName, query.getDeviceName())
+                .eq(query.getPartnerType() != null, TDevice::getPartnerType, query.getPartnerType())
+                .eq(query.getPartnerId() != null, TDevice::getPartnerId, query.getPartnerId())
+                .eq(query.getSiteId() != null && query.getSiteId() != 0, TDevice::getSiteId, query.getSiteId())
+                .orderByDesc(TDevice::getCreateTime)
+                .page(Page.of(query.getPageCurr(), query.getPageSize()));
+
+        List<VideoMonitorDeviceVO> records = page.getRecords().stream()
+                .map(this::toMonitorVO)
+                .peek(this::fillOnlineStatus)
+                .filter(item -> query.getOnlineStatus() == null || query.getOnlineStatus().equals(item.getOnlineStatus()))
+                .collect(Collectors.toList());
+
+        PageInfo<VideoMonitorDeviceVO> pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize());
+        pageInfo.setTotal(page.getTotal());
+        pageInfo.setRecords(records);
+        return AjaxResult.success(pageInfo);
+    }
+
+    @ResponseBody
+    @GetMapping("/detail/{id}")
+    @ApiOperation(value = "获取视频监控详情", tags = {"管理后台-视频监控"})
+    public AjaxResult<VideoMonitorDeviceVO> detail(@PathVariable Integer id) {
+        TDevice device = getIssuedDevice(id);
+        if (device == null) {
+            return AjaxResult.error("设备不存在或未下发");
+        }
+
+        VideoMonitorDeviceVO vo = toMonitorVO(device);
+        fillOnlineStatus(vo);
+        fillLiveAddress(vo, device);
+        return AjaxResult.success(vo);
+    }
+
+    @ResponseBody
+    @PostMapping("/ptz")
+    @ApiOperation(value = "视频监控云台控制", tags = {"管理后台-视频监控"})
+    public AjaxResult ptz(@RequestBody VideoMonitorControlRequest request) {
+        TDevice device = getIssuedDevice(request.getId());
+        if (device == null) {
+            return AjaxResult.error("设备不存在或未下发");
+        }
+
+        JSONObject result = ezvizOpenApiUtils.ptzControl(
+                device.getDeviceSerial(),
+                StringUtils.hasText(request.getAction()) ? request.getAction() : "start",
+                request.getCommand(),
+                request.getSpeed()
+        );
+        return buildEzvizResult(result, "云台控制失败");
+    }
+
+    @ResponseBody
+    @PostMapping("/sceneSwitch")
+    @ApiOperation(value = "视频监控镜头开关", tags = {"管理后台-视频监控"})
+    public AjaxResult sceneSwitch(@RequestBody VideoMonitorControlRequest request) {
+        TDevice device = getIssuedDevice(request.getId());
+        if (device == null) {
+            return AjaxResult.error("设备不存在或未下发");
+        }
+
+        JSONObject result = ezvizOpenApiUtils.setSceneSwitch(
+                device.getDeviceSerial(),
+                defaultChannelNo(request.getChannelNo()),
+                request.getEnable()
+        );
+        return buildEzvizResult(result, "摄像头开关控制失败");
+    }
+
+    @ResponseBody
+    @PostMapping("/focus")
+    @ApiOperation(value = "视频监控焦距控制", tags = {"管理后台-视频监控"})
+    public AjaxResult focus(@RequestBody VideoMonitorControlRequest request) {
+        TDevice device = getIssuedDevice(request.getId());
+        if (device == null) {
+            return AjaxResult.error("设备不存在或未下发");
+        }
+
+        JSONObject result = ezvizOpenApiUtils.ptzControl(
+                device.getDeviceSerial(),
+                "start",
+                request.getCommand(),
+                request.getSpeed()
+        );
+        if (isEzvizSuccess(result)) {
+            ezvizOpenApiUtils.ptzControl(device.getDeviceSerial(), "stop", request.getCommand(), null);
+        }
+        return buildEzvizResult(result, "焦距控制失败");
+    }
+
+    @ResponseBody
+    @PostMapping("/playback")
+    @ApiOperation(value = "获取视频监控回放地址", tags = {"管理后台-视频监控"})
+    public AjaxResult<VideoMonitorDeviceVO> playback(@RequestBody VideoMonitorControlRequest request) {
+        TDevice device = getIssuedDevice(request.getId());
+        if (device == null) {
+            return AjaxResult.error("设备不存在或未下发");
+        }
+
+        JSONObject result;
+        if (Integer.valueOf(2).equals(request.getType())) {
+            result = ezvizOpenApiUtils.getLocalPlaybackAddress(
+                    device.getDeviceSerial(),
+                    device.getValidateCode(),
+                    defaultChannelNo(request.getChannelNo()),
+                    request.getStartTime(),
+                    request.getStopTime()
+            );
+        } else {
+            result = ezvizOpenApiUtils.getCloudPlaybackAddress(
+                    device.getDeviceSerial(),
+                    device.getValidateCode(),
+                    defaultChannelNo(request.getChannelNo()),
+                    request.getStartTime(),
+                    request.getStopTime()
+            );
+        }
+
+        if (!isEzvizSuccess(result)) {
+            return AjaxResult.error(ezvizErrorMessage("获取回放地址失败", result));
+        }
+        VideoMonitorDeviceVO vo = toMonitorVO(device);
+        vo.setPlaybackAddress(resolveVideoUrl(result));
+        return AjaxResult.success(vo);
+    }
+
+    private TDevice getIssuedDevice(Integer id) {
+        if (id == null) {
+            return null;
+        }
+        TDevice device = deviceService.getById(id);
+        if (device == null || !Integer.valueOf(1).equals(device.getIssueStatus())) {
+            return null;
+        }
+        return device;
+    }
+
+    private VideoMonitorDeviceVO toMonitorVO(TDevice device) {
+        VideoMonitorDeviceVO vo = new VideoMonitorDeviceVO();
+        BeanUtils.copyProperties(device, vo);
+        return vo;
+    }
+
+    private void fillOnlineStatus(VideoMonitorDeviceVO vo) {
+        try {
+            JSONObject result = ezvizOpenApiUtils.getDeviceInfo(vo.getDeviceSerial());
+            if (!isEzvizSuccess(result)) {
+                result = ezvizOpenApiUtils.getDeviceStatus(vo.getDeviceSerial());
+            }
+            if (!isEzvizSuccess(result)) {
+                vo.setOnlineStatus(0);
+                vo.setStatusMsg(ezvizErrorMessage("获取在线状态失败", result));
+                return;
+            }
+            JSONObject data = result.getJSONObject("data");
+            Integer status = data == null ? null : data.getInteger("status");
+            if (status == null && data != null) {
+                status = data.getInteger("onlineStatus");
+            }
+            vo.setOnlineStatus(Integer.valueOf(1).equals(status) ? 1 : 0);
+            vo.setStatusMsg(Integer.valueOf(1).equals(vo.getOnlineStatus()) ? "在线" : "不在线");
+        } catch (Exception e) {
+            vo.setOnlineStatus(0);
+            vo.setStatusMsg("获取在线状态失败:" + e.getMessage());
+        }
+    }
+
+    private void fillLiveAddress(VideoMonitorDeviceVO vo, TDevice device) {
+        try {
+            JSONObject result = ezvizOpenApiUtils.getLiveAddress(
+                    device.getDeviceSerial(),
+                    device.getValidateCode(),
+                    1
+            );
+            if (isEzvizSuccess(result)) {
+                vo.setLiveAddress(resolveVideoUrl(result));
+            } else {
+                vo.setStatusMsg(ezvizErrorMessage("获取直播地址失败", result));
+            }
+        } catch (Exception e) {
+            vo.setStatusMsg("获取直播地址失败:" + e.getMessage());
+        }
+    }
+
+    private AjaxResult buildEzvizResult(JSONObject result, String defaultMessage) {
+        if (isEzvizSuccess(result)) {
+            return AjaxResult.success(result);
+        }
+        return AjaxResult.error(ezvizErrorMessage(defaultMessage, result));
+    }
+
+    private boolean isEzvizSuccess(JSONObject result) {
+        return result != null && SUCCESS_CODE.equals(result.getString("code"));
+    }
+
+    private String ezvizErrorMessage(String defaultMessage, JSONObject result) {
+        if (result == null) {
+            return defaultMessage;
+        }
+        String message = result.getString("msg");
+        return StringUtils.hasText(message) ? message : defaultMessage;
+    }
+
+    private String resolveVideoUrl(JSONObject result) {
+        JSONObject data = result.getJSONObject("data");
+        if (data == null) {
+            return null;
+        }
+        String url = data.getString("url");
+        if (StringUtils.hasText(url)) {
+            return url;
+        }
+        return data.getString("ezopen");
+    }
+
+    private Integer defaultChannelNo(Integer channelNo) {
+        return channelNo == null ? 1 : channelNo;
+    }
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/DevicePageList.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/DevicePageList.java
index 90ce33c..0c8d55b 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/DevicePageList.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/DevicePageList.java
@@ -18,10 +18,13 @@
     private String deviceName;
 
     @ApiModelProperty(value = "设备序列号")
-    private String serialNo;
+    private String deviceSerial;
 
-    @ApiModelProperty(value = "设备型号")
-    private String equipmentModel;
+    @ApiModelProperty(value = "设备品牌(1=大华,2=海康,3=萤石)")
+    private Integer deviceBrand;
+
+    @ApiModelProperty(value = "设备验证码")
+    private String validateCode;
 
     @ApiModelProperty(value = "合作商类型")
     private Integer partnerType;
@@ -34,4 +37,7 @@
 
     @ApiModelProperty(value = "在线状态(0=不在线,1=在线)")
     private Integer onlineStatus;
+
+    @ApiModelProperty(value = "下发状态(0=未下发,1=已下发,2=下发失败)")
+    private Integer issueStatus;
 }
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/VideoMonitorControlRequest.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/VideoMonitorControlRequest.java
new file mode 100644
index 0000000..a70bd54
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/VideoMonitorControlRequest.java
@@ -0,0 +1,42 @@
+package com.ruoyi.other.query;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 视频监控控制请求
+ *
+ * @author ruoyi
+ */
+@Data
+@ApiModel
+public class VideoMonitorControlRequest {
+
+    @ApiModelProperty(value = "设备ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "通道号")
+    private Integer channelNo;
+
+    @ApiModelProperty(value = "控制动作:start/stop")
+    private String action;
+
+    @ApiModelProperty(value = "控制命令")
+    private String command;
+
+    @ApiModelProperty(value = "速度")
+    private Integer speed;
+
+    @ApiModelProperty(value = "开关值:1=开启,0=关闭")
+    private Integer enable;
+
+    @ApiModelProperty(value = "回放类型:2=本地录像,3=云存储")
+    private Integer type;
+
+    @ApiModelProperty(value = "回放开始时间")
+    private String startTime;
+
+    @ApiModelProperty(value = "回放结束时间")
+    private String stopTime;
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/VideoMonitorPageQuery.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/VideoMonitorPageQuery.java
new file mode 100644
index 0000000..0660d40
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/VideoMonitorPageQuery.java
@@ -0,0 +1,31 @@
+package com.ruoyi.other.query;
+
+import com.ruoyi.common.core.web.page.BasePage;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 视频监控分页查询
+ *
+ * @author ruoyi
+ */
+@Data
+@ApiModel
+public class VideoMonitorPageQuery extends BasePage {
+
+    @ApiModelProperty(value = "合作商类型")
+    private Integer partnerType;
+
+    @ApiModelProperty(value = "合作商ID")
+    private Integer partnerId;
+
+    @ApiModelProperty(value = "归属电站ID")
+    private Integer siteId;
+
+    @ApiModelProperty(value = "设备名称")
+    private String deviceName;
+
+    @ApiModelProperty(value = "在线状态(0=不在线,1=在线)")
+    private Integer onlineStatus;
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TDeviceService.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TDeviceService.java
index 94ecb03..c9ae9b6 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TDeviceService.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TDeviceService.java
@@ -14,5 +14,5 @@
 
     PageInfo<TDevice> pageList(DevicePageList pageList);
 
-    boolean existsBySerialNo(String serialNo, Integer excludeId);
+    boolean existsByDeviceSerial(String deviceSerial, Integer excludeId);
 }
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TDeviceServiceImpl.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TDeviceServiceImpl.java
index 612d6c8..22c2446 100644
--- a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TDeviceServiceImpl.java
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TDeviceServiceImpl.java
@@ -28,12 +28,12 @@
     }
 
     @Override
-    public boolean existsBySerialNo(String serialNo, Integer excludeId) {
-        if (!StringUtils.hasText(serialNo)) {
+    public boolean existsByDeviceSerial(String deviceSerial, Integer excludeId) {
+        if (!StringUtils.hasText(deviceSerial)) {
             return false;
         }
         return this.lambdaQuery()
-                .eq(TDevice::getSerialNo, serialNo)
+                .eq(TDevice::getDeviceSerial, deviceSerial)
                 .ne(excludeId != null, TDevice::getId, excludeId)
                 .count() > 0;
     }
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/EzvizOpenApiUtils.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/EzvizOpenApiUtils.java
new file mode 100644
index 0000000..eff5ce9
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/EzvizOpenApiUtils.java
@@ -0,0 +1,338 @@
+package com.ruoyi.other.util;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 萤石开放平台对接工具类。
+ *
+ * 配置项:
+ * ezviz.open.base-url=https://open.ys7.com
+ * ezviz.open.app-key=应用AppKey
+ * ezviz.open.app-secret=应用Secret
+ */
+@Service
+public class EzvizOpenApiUtils {
+
+    private static final String SUCCESS_CODE = "200";
+    private static final String TOKEN_EXPIRED_CODE = "10002";
+    private static final long TOKEN_EXPIRE_ADVANCE_MS = 5 * 60 * 1000L;
+
+    @Value("${ezviz.open.base-url:https://open.ys7.com}")
+    private String baseUrl;
+
+    @Value("${ezviz.open.app-key:}")
+    private String appKey;
+
+    @Value("${ezviz.open.app-secret:}")
+    private String appSecret;
+
+    private volatile String accessToken;
+
+    private volatile long accessTokenExpireTime;
+
+    /**
+     * 获取并缓存 accessToken。
+     */
+    public String getAccessToken() {
+        if (StringUtils.hasText(accessToken) && System.currentTimeMillis() < accessTokenExpireTime) {
+            return accessToken;
+        }
+        return refreshAccessToken();
+    }
+
+    /**
+     * 添加萤石设备到当前账号下。
+     */
+    public JSONObject addDevice(String deviceSerial, String validateCode) {
+        requireText(deviceSerial, "deviceSerial");
+        requireText(validateCode, "validateCode");
+
+        Map<String, Object> form = new LinkedHashMap<>();
+        form.put("deviceSerial", deviceSerial);
+        form.put("validateCode", validateCode);
+        return postFormWithToken("/api/lapp/device/add", form);
+    }
+
+    /**
+     * 删除当前账号下的萤石设备。
+     */
+    public JSONObject deleteDevice(String deviceSerial) {
+        requireText(deviceSerial, "deviceSerial");
+
+        Map<String, Object> form = new LinkedHashMap<>();
+        form.put("deviceSerial", deviceSerial);
+        return postFormWithToken("/api/lapp/device/delete", form);
+    }
+
+    /**
+     * 获取设备基础信息,返回内容中包含设备在线状态。
+     */
+    public JSONObject getDeviceInfo(String deviceSerial) {
+        requireText(deviceSerial, "deviceSerial");
+
+        Map<String, Object> form = new LinkedHashMap<>();
+        form.put("deviceSerial", deviceSerial);
+        return postFormWithToken("/api/lapp/device/info", form);
+    }
+
+    /**
+     * 获取设备状态信息。
+     */
+    public JSONObject getDeviceStatus(String deviceSerial) {
+        requireText(deviceSerial, "deviceSerial");
+
+        Map<String, Object> form = new LinkedHashMap<>();
+        form.put("deviceSerial", deviceSerial);
+        return postFormWithToken("/api/lapp/device/status/get", form);
+    }
+
+    /**
+     * 云台角度控制。
+     *
+     * @param action start=开始,stop=结束
+     * @param command up/down/left/right/upleft/downleft/upright/downright
+     * @param speed 速度 1-7,为空时按萤石默认值
+     */
+    public JSONObject ptzControl(String deviceSerial, String action, String command, Integer speed) {
+        requireText(deviceSerial, "deviceSerial");
+        requireText(action, "action");
+
+        Map<String, Object> body = new LinkedHashMap<>();
+        body.put("timeStamp", String.valueOf(System.currentTimeMillis()));
+        body.put("action", action);
+        body.put("control", "ptz");
+        putIfNotNull(body, "command", command);
+        putIfNotNull(body, "speed", speed);
+
+        return otapAction(
+                deviceSerial,
+                "0",
+                "global",
+                "PTZManualCtrl",
+                "ModifyPTZCtrl",
+                body
+        );
+    }
+
+    /**
+     * 开始云台控制。
+     */
+    public JSONObject startPtz(String deviceSerial, String command, Integer speed) {
+        return ptzControl(deviceSerial, "start", command, speed);
+    }
+
+    /**
+     * 停止云台控制。
+     */
+    public JSONObject stopPtz(String deviceSerial, String command) {
+        return ptzControl(deviceSerial, "stop", command, null);
+    }
+
+    /**
+     * 通用设备开关/动作控制。用于后续按萤石具体功能文档传入 domainIdentifier/actionIdentifier。
+     */
+    public JSONObject otapAction(String deviceSerial,
+                                 String localIndex,
+                                 String resourceCategory,
+                                 String domainIdentifier,
+                                 String actionIdentifier,
+                                 Map<String, Object> body) {
+        requireText(deviceSerial, "deviceSerial");
+        requireText(localIndex, "localIndex");
+        requireText(resourceCategory, "resourceCategory");
+        requireText(domainIdentifier, "domainIdentifier");
+        requireText(actionIdentifier, "actionIdentifier");
+
+        Map<String, String> headers = new LinkedHashMap<>();
+        headers.put("accessToken", getAccessToken());
+        headers.put("deviceSerial", deviceSerial);
+        headers.put("localIndex", localIndex);
+        headers.put("resourceCategory", resourceCategory);
+        headers.put("domainIdentifier", domainIdentifier);
+        headers.put("actionIdentifier", actionIdentifier);
+
+        JSONObject result = putJson("/api/v3/device/otap/action", headers, body);
+        if (isTokenExpired(result)) {
+            headers.put("accessToken", refreshAccessToken());
+            result = putJson("/api/v3/device/otap/action", headers, body);
+        }
+        return result;
+    }
+
+    /**
+     * 布撤防开关。isDefence:1=布防,0=撤防。
+     */
+    public JSONObject setDefence(String deviceSerial, Integer channelNo, Integer isDefence) {
+        requireText(deviceSerial, "deviceSerial");
+        if (isDefence == null) {
+            throw new IllegalArgumentException("isDefence不能为空");
+        }
+
+        Map<String, Object> form = new LinkedHashMap<>();
+        form.put("deviceSerial", deviceSerial);
+        form.put("isDefence", isDefence);
+        putIfNotNull(form, "channelNo", channelNo);
+        return postFormWithToken("/api/lapp/device/defence/set", form);
+    }
+
+    /**
+     * 镜头遮蔽开关。enable:1=开启遮蔽,0=关闭遮蔽。
+     */
+    public JSONObject setSceneSwitch(String deviceSerial, Integer channelNo, Integer enable) {
+        requireText(deviceSerial, "deviceSerial");
+        if (enable == null) {
+            throw new IllegalArgumentException("enable不能为空");
+        }
+
+        Map<String, Object> form = new LinkedHashMap<>();
+        form.put("deviceSerial", deviceSerial);
+        form.put("enable", enable);
+        putIfNotNull(form, "channelNo", channelNo);
+        return postFormWithToken("/api/lapp/device/scene/switch/set", form);
+    }
+
+    /**
+     * 获取直播/回放取流地址。
+     *
+     * @param type 1=预览,2=本地录像回放,3=云存储录像回放
+     */
+    public JSONObject getVideoAddress(String deviceSerial,
+                                      String validateCode,
+                                      Integer channelNo,
+                                      Integer type,
+                                      String startTime,
+                                      String stopTime,
+                                      Integer protocol,
+                                      Integer quality,
+                                      Integer expireTime) {
+        requireText(deviceSerial, "deviceSerial");
+
+        Map<String, Object> form = new LinkedHashMap<>();
+        form.put("deviceSerial", deviceSerial);
+        putIfNotNull(form, "code", validateCode);
+        putIfNotNull(form, "channelNo", channelNo);
+        putIfNotNull(form, "type", type);
+        putIfNotNull(form, "startTime", startTime);
+        putIfNotNull(form, "stopTime", stopTime);
+        putIfNotNull(form, "protocol", protocol);
+        putIfNotNull(form, "quality", quality);
+        putIfNotNull(form, "expireTime", expireTime);
+        return postFormWithToken("/api/lapp/v2/live/address/get", form);
+    }
+
+    /**
+     * 获取本地录像回放地址。
+     */
+    public JSONObject getLocalPlaybackAddress(String deviceSerial,
+                                              String validateCode,
+                                              Integer channelNo,
+                                              String startTime,
+                                              String stopTime) {
+        return getVideoAddress(deviceSerial, validateCode, channelNo, 2, startTime, stopTime, 2, null, null);
+    }
+
+    /**
+     * 获取云存储录像回放地址。
+     */
+    public JSONObject getCloudPlaybackAddress(String deviceSerial,
+                                              String validateCode,
+                                              Integer channelNo,
+                                              String startTime,
+                                              String stopTime) {
+        return getVideoAddress(deviceSerial, validateCode, channelNo, 3, startTime, stopTime, 2, null, null);
+    }
+
+    /**
+     * 获取直播地址。
+     */
+    public JSONObject getLiveAddress(String deviceSerial, String validateCode, Integer channelNo) {
+        return getVideoAddress(deviceSerial, validateCode, channelNo, 1, null, null, 2, null, null);
+    }
+
+    private synchronized String refreshAccessToken() {
+        requireText(appKey, "ezviz.open.app-key");
+        requireText(appSecret, "ezviz.open.app-secret");
+
+        Map<String, Object> form = new LinkedHashMap<>();
+        form.put("appKey", appKey);
+        form.put("appSecret", appSecret);
+
+        JSONObject result = postForm("/api/lapp/token/get", form);
+        if (!SUCCESS_CODE.equals(result.getString("code"))) {
+            throw new IllegalStateException("获取萤石accessToken失败:" + result.toJSONString());
+        }
+
+        JSONObject data = result.getJSONObject("data");
+        if (data == null || !StringUtils.hasText(data.getString("accessToken"))) {
+            throw new IllegalStateException("萤石accessToken响应为空:" + result.toJSONString());
+        }
+
+        accessToken = data.getString("accessToken");
+        Long expireTime = data.getLong("expireTime");
+        accessTokenExpireTime = expireTime == null
+                ? System.currentTimeMillis() + 60 * 60 * 1000L
+                : expireTime - TOKEN_EXPIRE_ADVANCE_MS;
+        return accessToken;
+    }
+
+    private JSONObject postFormWithToken(String path, Map<String, Object> form) {
+        form.put("accessToken", getAccessToken());
+        JSONObject result = postForm(path, form);
+        if (isTokenExpired(result)) {
+            form.put("accessToken", refreshAccessToken());
+            result = postForm(path, form);
+        }
+        return result;
+    }
+
+    private JSONObject postForm(String path, Map<String, Object> form) {
+        HttpResponse response = HttpRequest.post(url(path))
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .form(form)
+                .timeout(10000)
+                .execute();
+        return JSON.parseObject(response.body());
+    }
+
+    private JSONObject putJson(String path, Map<String, String> headers, Map<String, Object> body) {
+        HttpRequest request = HttpRequest.put(url(path))
+                .header("Content-Type", "application/json;charset=UTF-8")
+                .timeout(10000);
+        for (Map.Entry<String, String> entry : headers.entrySet()) {
+            request.header(entry.getKey(), entry.getValue());
+        }
+        HttpResponse response = request.body(JSON.toJSONString(body == null ? new LinkedHashMap<>() : body)).execute();
+        return JSON.parseObject(response.body());
+    }
+
+    private boolean isTokenExpired(JSONObject result) {
+        return result != null && TOKEN_EXPIRED_CODE.equals(result.getString("code"));
+    }
+
+    private String url(String path) {
+        String normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
+        String normalizedPath = path.startsWith("/") ? path : "/" + path;
+        return normalizedBaseUrl + normalizedPath;
+    }
+
+    private void requireText(String value, String name) {
+        if (!StringUtils.hasText(value)) {
+            throw new IllegalArgumentException(name + "不能为空");
+        }
+    }
+
+    private void putIfNotNull(Map<String, Object> map, String key, Object value) {
+        if (value != null) {
+            map.put(key, value);
+        }
+    }
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/VideoMonitorDeviceVO.java b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/VideoMonitorDeviceVO.java
new file mode 100644
index 0000000..cb3d59e
--- /dev/null
+++ b/ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/VideoMonitorDeviceVO.java
@@ -0,0 +1,54 @@
+package com.ruoyi.other.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 视频监控设备展示对象
+ *
+ * @author ruoyi
+ */
+@Data
+@ApiModel
+public class VideoMonitorDeviceVO {
+
+    @ApiModelProperty(value = "设备ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "设备名称")
+    private String deviceName;
+
+    @ApiModelProperty(value = "设备序列号")
+    private String deviceSerial;
+
+    @ApiModelProperty(value = "设备品牌(1=大华,2=海康,3=萤石)")
+    private Integer deviceBrand;
+
+    @ApiModelProperty(value = "合作商类型")
+    private Integer partnerType;
+
+    @ApiModelProperty(value = "合作商ID")
+    private Integer partnerId;
+
+    @ApiModelProperty(value = "合作商名称")
+    private String partnerName;
+
+    @ApiModelProperty(value = "归属电站ID")
+    private Integer siteId;
+
+    @ApiModelProperty(value = "归属电站名称")
+    private String siteName;
+
+    @ApiModelProperty(value = "在线状态(0=不在线,1=在线)")
+    private Integer onlineStatus;
+
+    @ApiModelProperty(value = "状态说明")
+    private String statusMsg;
+
+    @ApiModelProperty(value = "直播地址")
+    private String liveAddress;
+
+    @ApiModelProperty(value = "回放地址")
+    private String playbackAddress;
+}
diff --git a/ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TDeviceMapper.xml b/ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TDeviceMapper.xml
index dc36409..6f0f091 100644
--- a/ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TDeviceMapper.xml
+++ b/ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TDeviceMapper.xml
@@ -5,20 +5,23 @@
     <resultMap id="BaseResultMap" type="com.ruoyi.other.api.domain.TDevice">
         <id column="id" property="id" />
         <result column="device_name" property="deviceName" />
-        <result column="serial_no" property="serialNo" />
-        <result column="equipment_model" property="equipmentModel" />
+        <result column="device_serial" property="deviceSerial" />
+        <result column="device_brand" property="deviceBrand" />
+        <result column="validate_code" property="validateCode" />
         <result column="partner_type" property="partnerType" />
         <result column="partner_id" property="partnerId" />
         <result column="partner_name" property="partnerName" />
         <result column="site_id" property="siteId" />
         <result column="site_name" property="siteName" />
         <result column="online_status" property="onlineStatus" />
+        <result column="issue_status" property="issueStatus" />
+        <result column="issue_msg" property="issueMsg" />
         <result column="del_flag" property="delFlag" />
         <result column="create_time" property="createTime" />
     </resultMap>
 
     <sql id="Base_Column_List">
-        id, device_name, serial_no, equipment_model, partner_type, partner_id, partner_name, site_id, site_name, online_status, del_flag, create_time
+        id, device_name, device_serial, device_brand, validate_code, partner_type, partner_id, partner_name, site_id, site_name, online_status, issue_status, issue_msg, del_flag, create_time
     </sql>
 
     <select id="pageList" resultMap="BaseResultMap">
@@ -29,11 +32,14 @@
         <if test="null != req.deviceName and '' != req.deviceName">
             and device_name like CONCAT('%', #{req.deviceName}, '%')
         </if>
-        <if test="null != req.serialNo and '' != req.serialNo">
-            and serial_no like CONCAT('%', #{req.serialNo}, '%')
+        <if test="null != req.deviceSerial and '' != req.deviceSerial">
+            and device_serial like CONCAT('%', #{req.deviceSerial}, '%')
         </if>
-        <if test="null != req.equipmentModel and '' != req.equipmentModel">
-            and equipment_model like CONCAT('%', #{req.equipmentModel}, '%')
+        <if test="null != req.deviceBrand">
+            and device_brand = #{req.deviceBrand}
+        </if>
+        <if test="null != req.validateCode and '' != req.validateCode">
+            and validate_code like CONCAT('%', #{req.validateCode}, '%')
         </if>
         <if test="null != req.partnerType">
             and partner_type = #{req.partnerType}
@@ -47,6 +53,9 @@
         <if test="null != req.onlineStatus">
             and online_status = #{req.onlineStatus}
         </if>
+        <if test="null != req.issueStatus">
+            and issue_status = #{req.issueStatus}
+        </if>
         order by create_time desc, id desc
     </select>
 </mapper>

--
Gitblit v1.7.1