liujie
2026-07-22 228c1e07d117d1f7f11dec8381eb951d92bcf2e8
监控模块
5个文件已添加
6个文件已修改
871 ■■■■■ 已修改文件
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TDevice.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TDeviceController.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/VideoMonitorController.java 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/DevicePageList.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/VideoMonitorControlRequest.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/VideoMonitorPageQuery.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TDeviceService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TDeviceServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/EzvizOpenApiUtils.java 338 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/VideoMonitorDeviceVO.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TDeviceMapper.xml 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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;
}
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;
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/VideoMonitorController.java
New file
@@ -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;
    }
}
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;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/VideoMonitorControlRequest.java
New file
@@ -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;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/VideoMonitorPageQuery.java
New file
@@ -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;
}
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);
}
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;
    }
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/EzvizOpenApiUtils.java
New file
@@ -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);
        }
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/VideoMonitorDeviceVO.java
New file
@@ -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;
}
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>