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 FLV_URL_RULES = new HashMap() {{ 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 queryWrapper = new LambdaQueryWrapper<>(); if (StringUtils.isNotBlank(cameraName)) { queryWrapper.like(Device::getCameraName, cameraName); } Page 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 deviceDtoList = EasyExcel.read(inputStream) .head(ImportDeviceDto.class) .sheet() .doReadSync(); if (CollectionUtils.isEmpty(deviceDtoList)) { return ApiResponse.failure("导入数据为空"); } List errorMessages = validateImportData(deviceDtoList); if (!errorMessages.isEmpty()) { return ApiResponse.failure("导入数据校验失败:" + String.join(";", errorMessages)); } List importIps = deviceDtoList.stream() .map(ImportDeviceDto::getIp) .collect(Collectors.toList()); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.in(Device::getIp, importIps); List existDevices = deviceService.list(queryWrapper); Set existIps = existDevices.stream() .map(Device::getIp) .collect(Collectors.toSet()); List 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 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 validateImportData(List deviceList) { List 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()); } }