This commit is contained in:
2025-10-22 11:44:18 +08:00
parent 0b6de3de4b
commit 663235eb7e
33 changed files with 1099 additions and 129 deletions

View File

@ -0,0 +1,279 @@
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());
}
}