Files
yjearth/src/main/java/com/yj/earth/business/controller/DeviceController.java
2025-11-25 14:27:10 +08:00

325 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.yj.earth.business.controller;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.util.StringUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yj.earth.business.domain.Device;
import com.yj.earth.business.service.DeviceService;
import com.yj.earth.common.util.ApiResponse;
import com.yj.earth.common.util.NetUtils;
import com.yj.earth.vo.AddDeviceDto;
import com.yj.earth.dto.device.ImportDeviceDto;
import com.yj.earth.dto.device.UpdateDeviceDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@Tag(name = "设备信息管理")
@RestController
@RequestMapping("/device")
public class DeviceController {
@Resource
private DeviceService deviceService;
public static final String TYPE_DAHUA = "大华";
public static final String TYPE_HIKVISION = "海康";
// FLV地址拼接规则、key=设备类型、value=地址模板
public static final Map<String, String> FLV_URL_RULES = new HashMap<String, String>() {{
put(TYPE_DAHUA, "http://{ip}:{port}/cam/realmonitor?channel={channel}&subtype=0&proto=flv");
put(TYPE_HIKVISION, "http://{ip}:{port}/Streaming/Channels/{channel}/flv");
}};
/**
* 生成FLV地址
*/
public static String generateFlvUrl(String type, String ip, Integer port, Integer channel) {
return FLV_URL_RULES.get(type)
.replace("{ip}", ip)
.replace("{port}", port.toString())
.replace("{channel}", channel.toString());
}
@PostMapping("/add")
@Operation(summary = "新增设备信息")
public ApiResponse addDevice(@RequestBody AddDeviceDto addDeviceDto) {
Device device = new Device();
BeanUtils.copyProperties(addDeviceDto, device);
// 生成FLV地址
String flvUrl = generateFlvUrl(
addDeviceDto.getType(),
addDeviceDto.getIp(),
addDeviceDto.getPort(),
addDeviceDto.getChannel()
);
device.setFlvUrl(flvUrl);
deviceService.save(device);
return ApiResponse.success(null);
}
@GetMapping("/list")
@Operation(summary = "查询设备信息(含在线状态)")
public ApiResponse listDevice(
@Parameter(description = "分页数量") Integer pageNum,
@Parameter(description = "分页大小") Integer pageSize,
@Parameter(description = "设备名称") String cameraName
) {
LambdaQueryWrapper<Device> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(cameraName)) {
queryWrapper.like(Device::getCameraName, cameraName);
}
// 执行标准的分页查询
Page<Device> devicePage = deviceService.page(new Page<>(pageNum, pageSize), queryWrapper);
List<Device> deviceList = devicePage.getRecords();
if (CollectionUtils.isEmpty(deviceList)) {
// 如果没有数据,直接返回空分页
return ApiResponse.success(devicePage);
}
try {
// 提取所有设备的 IP 地址、用于批量检测
List<String> ipAddresses = deviceList.stream()
.map(Device::getIp)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 异步并发检测所有 IP 的在线状态
Map<String, Boolean> reachabilityMap = NetUtils.checkReachabilityAsync(ipAddresses);
// 更新设备状态:遍历设备列表,根据 IP 设置 status 字段
deviceList.forEach(device -> {
String ip = device.getIp();
if (ip != null) {
Boolean isOnline = reachabilityMap.get(ip);
// 根据 Ping 结果设置 status: 1 为在线0 为离线
device.setStatus(isOnline != null && isOnline ? 1 : 0);
} else {
// 如果 IP 地址为空,直接设置为离线
device.setStatus(0);
}
});
// 直接返回更新后的分页对象
return ApiResponse.success(devicePage);
} catch (ExecutionException | InterruptedException e) {
// 恢复线程中断状态
Thread.currentThread().interrupt();
return ApiResponse.success(devicePage);
}
}
@GetMapping("/delete")
@Operation(summary = "删除设备信息")
public ApiResponse deleteDevice(@Parameter(description = "设备ID") @RequestParam String id) {
deviceService.removeById(id);
return ApiResponse.success(null);
}
@GetMapping("/getById")
@Operation(summary = "查询设备信息")
public ApiResponse getDevice(@Parameter(description = "设备ID") @RequestParam String id) {
return ApiResponse.success(deviceService.getById(id));
}
@PostMapping("/update")
@Operation(summary = "更新设备信息")
public ApiResponse updateDevice(@RequestBody UpdateDeviceDto updateDeviceDto) {
Device device = new Device();
BeanUtils.copyProperties(updateDeviceDto, device);
// 如果更新了影响FLV URL的字段、重新生成FLV URL
if (updateDeviceDto.getType() != null || updateDeviceDto.getIp() != null ||
updateDeviceDto.getPort() != null || updateDeviceDto.getChannel() != null) {
// 如果有任何一个字段为null、从数据库获取原始值
Device original = deviceService.getById(updateDeviceDto.getId());
String type = updateDeviceDto.getType() != null ? updateDeviceDto.getType() : original.getType();
String ip = updateDeviceDto.getIp() != null ? updateDeviceDto.getIp() : original.getIp();
Integer port = updateDeviceDto.getPort() != null ? updateDeviceDto.getPort() : original.getPort();
Integer channel = updateDeviceDto.getChannel() != null ? updateDeviceDto.getChannel() : original.getChannel();
device.setFlvUrl(generateFlvUrl(type, ip, port, channel));
}
deviceService.updateById(device);
return ApiResponse.success(null);
}
@PostMapping("/import")
@Operation(summary = "导入设备信息")
@Transactional(rollbackFor = Exception.class)
public ApiResponse importDevices(@Parameter(description = "设备Excel文件路径") @RequestParam String filePath) {
// 验证文件路径不为空
if (StringUtils.isBlank(filePath)) {
return ApiResponse.failure("文件路径不能为空");
}
// 验证文件是否存在
File file = new File(filePath);
if (!file.exists() || !file.isFile()) {
return ApiResponse.failure("文件不存在或不是有效文件");
}
// 验证文件格式
String fileName = file.getName();
if (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls")) {
return ApiResponse.failure("请上传Excel格式文件.xls或.xlsx");
}
// 验证文件大小
long fileSize = file.length();
if (fileSize > 10 * 1024 * 1024) {
return ApiResponse.failure("文件大小不能超过10MB");
}
try (InputStream inputStream = new FileInputStream(file)) {
List<ImportDeviceDto> deviceDtoList = EasyExcel.read(inputStream)
.head(ImportDeviceDto.class)
.sheet()
.doReadSync();
if (CollectionUtils.isEmpty(deviceDtoList)) {
return ApiResponse.failure("导入数据为空");
}
List<String> errorMessages = validateImportData(deviceDtoList);
if (!errorMessages.isEmpty()) {
return ApiResponse.failure("导入数据校验失败:" + String.join("", errorMessages));
}
List<String> importIps = deviceDtoList.stream()
.map(ImportDeviceDto::getIp)
.collect(Collectors.toList());
LambdaQueryWrapper<Device> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Device::getIp, importIps);
List<Device> existDevices = deviceService.list(queryWrapper);
Set<String> existIps = existDevices.stream()
.map(Device::getIp)
.collect(Collectors.toSet());
List<Device> newDevices = deviceDtoList.stream()
.filter(dto -> !existIps.contains(dto.getIp()))
.map(dto -> {
Device device = new Device();
BeanUtils.copyProperties(dto, device);
// 生成FLV地址
String flvUrl = generateFlvUrl(
dto.getType(),
dto.getIp(),
dto.getPort(),
dto.getChannel()
);
device.setFlvUrl(flvUrl);
return device;
})
.collect(Collectors.toList());
int total = deviceDtoList.size();
int skipped = total - newDevices.size();
int saved = 0;
if (!newDevices.isEmpty()) {
boolean saveResult = deviceService.saveBatch(newDevices);
saved = saveResult ? newDevices.size() : 0;
}
Map<String, Object> resultMap = new HashMap<>(3);
resultMap.put("total", total);
resultMap.put("saved", saved);
resultMap.put("skipped", skipped);
return ApiResponse.success(resultMap);
} catch (IOException e) {
return ApiResponse.failure("文件读取失败:" + e.getMessage());
} catch (Exception e) {
return ApiResponse.failure("导入异常:" + e.getMessage());
}
}
/**
* 校验导入数据的合法性
*/
private List<String> validateImportData(List<ImportDeviceDto> deviceList) {
List<String> errorMessages = new ArrayList<>();
for (int i = 0; i < deviceList.size(); i++) {
ImportDeviceDto dto = deviceList.get(i);
int rowNum = i + 2;
// 校验必填字段
if (StringUtils.isBlank(dto.getCameraName())) {
errorMessages.add(String.format("第%d行设备名称不能为空", rowNum));
}
if (StringUtils.isBlank(dto.getIp())) {
errorMessages.add(String.format("第%d行设备IP不能为空", rowNum));
} else if (!isValidIp(dto.getIp())) {
errorMessages.add(String.format("第%d行设备IP格式不正确", rowNum));
}
if (dto.getPort() == null || dto.getPort() <= 0 || dto.getPort() > 65535) {
errorMessages.add(String.format("第%d行设备端口无效", rowNum));
}
if (StringUtils.isBlank(dto.getUsername())) {
errorMessages.add(String.format("第%d行用户名不能为空", rowNum));
}
if (StringUtils.isBlank(dto.getType())) {
errorMessages.add(String.format("第%d行设备类型不能为空", rowNum));
} else if (!FLV_URL_RULES.containsKey(dto.getType())) {
errorMessages.add(String.format("第%d行不支持的设备类型", rowNum));
}
if (dto.getChannel() == null || dto.getChannel() <= 0) {
errorMessages.add(String.format("第%d行设备通道无效", rowNum));
}
}
return errorMessages;
}
/**
* 简单校验IP地址格式
*/
private boolean isValidIp(String ip) {
if (ip == null || ip.isEmpty()) {
return false;
}
String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
return ip.matches(regex);
}
@GetMapping("/import/template")
@Operation(summary = "下载导入模板")
public void downloadTemplate(HttpServletResponse response) throws IOException {
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("设备信息导入模板", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=" + fileName + ".xlsx");
// 写入模板表头(通过空数据列表触发表头生成)
EasyExcel.write(response.getOutputStream(), ImportDeviceDto.class)
.sheet("设备信息")
.doWrite(Collections.emptyList());
}
}