280 lines
12 KiB
Java
280 lines
12 KiB
Java
|
|
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.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.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);
|
|||
|
|
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*=utf-8''" + fileName + ".xlsx");
|
|||
|
|
// 写入模板表头(通过空数据列表触发表头生成)
|
|||
|
|
EasyExcel.write(response.getOutputStream(), ImportDeviceDto.class)
|
|||
|
|
.sheet("设备信息")
|
|||
|
|
.doWrite(Collections.emptyList());
|
|||
|
|
}
|
|||
|
|
}
|