From 663235eb7edb93841b831bdb05cbe666a2df3fc9 Mon Sep 17 00:00:00 2001 From: ZZX9599 <536509593@qq.com> Date: Wed, 22 Oct 2025 11:44:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 14 + .../java/com/yj/earth/FastFileEncryptor.java | 226 ++++++++++++++ .../business/controller/CsvController.java | 55 ++++ .../business/controller/DeviceController.java | 279 ++++++++++++++++++ .../controller/PbfInfoController.java | 7 +- .../business/controller/PoiController.java | 88 ++++++ .../business/controller/SystemController.java | 133 +++++---- .../business/controller/UserController.java | 54 ++-- .../controller/WebSourceController.java | 13 + .../com/yj/earth/business/domain/Device.java | 45 +++ .../com/yj/earth/business/domain/Role.java | 3 + .../com/yj/earth/business/domain/User.java | 3 + .../earth/business/mapper/DeviceMapper.java | 18 ++ .../earth/business/service/DeviceService.java | 16 + .../service/impl/DeviceServiceImpl.java | 20 ++ .../common/service/ServerInitService.java | 8 +- .../com/yj/earth/common/util/CodeUtil.java | 2 +- .../earth/common/util/GdalJsonConverter.java | 53 ++-- .../com/yj/earth/common/util/SdkUtil.java | 6 +- .../yj/earth/datasource/DatabaseManager.java | 5 +- src/main/java/com/yj/earth/design/Device.java | 34 +++ src/main/java/com/yj/earth/design/Role.java | 2 + src/main/java/com/yj/earth/design/User.java | 4 +- .../yj/earth/dto/device/ImportDeviceDto.java | 23 ++ .../yj/earth/dto/device/UpdateDeviceDto.java | 24 ++ .../dto/system/UpdateSystemServiceDto.java | 2 - .../com/yj/earth/dto/user/UpdateUserDto.java | 5 +- .../earth/dto/user/UpdateUserStatusDto.java | 12 + src/main/java/com/yj/earth/params/Gdb.java | 11 + .../java/com/yj/earth/vo/AddDeviceDto.java | 22 ++ src/main/java/com/yj/earth/vo/CsvField.java | 14 + src/main/resources/application.yml | 2 +- src/main/resources/mapper/DeviceMapper.xml | 25 ++ 33 files changed, 1099 insertions(+), 129 deletions(-) create mode 100644 src/main/java/com/yj/earth/FastFileEncryptor.java create mode 100644 src/main/java/com/yj/earth/business/controller/CsvController.java create mode 100644 src/main/java/com/yj/earth/business/controller/DeviceController.java create mode 100644 src/main/java/com/yj/earth/business/controller/PoiController.java create mode 100644 src/main/java/com/yj/earth/business/domain/Device.java create mode 100644 src/main/java/com/yj/earth/business/mapper/DeviceMapper.java create mode 100644 src/main/java/com/yj/earth/business/service/DeviceService.java create mode 100644 src/main/java/com/yj/earth/business/service/impl/DeviceServiceImpl.java create mode 100644 src/main/java/com/yj/earth/design/Device.java create mode 100644 src/main/java/com/yj/earth/dto/device/ImportDeviceDto.java create mode 100644 src/main/java/com/yj/earth/dto/device/UpdateDeviceDto.java create mode 100644 src/main/java/com/yj/earth/dto/user/UpdateUserStatusDto.java create mode 100644 src/main/java/com/yj/earth/params/Gdb.java create mode 100644 src/main/java/com/yj/earth/vo/AddDeviceDto.java create mode 100644 src/main/java/com/yj/earth/vo/CsvField.java create mode 100644 src/main/resources/mapper/DeviceMapper.xml diff --git a/pom.xml b/pom.xml index 37dd43c..2286389 100644 --- a/pom.xml +++ b/pom.xml @@ -194,6 +194,20 @@ gdal 3.11.4 + + + + com.alibaba + easyexcel + 3.3.2 + + + + + org.apache.commons + commons-csv + 1.9.0 + diff --git a/src/main/java/com/yj/earth/FastFileEncryptor.java b/src/main/java/com/yj/earth/FastFileEncryptor.java new file mode 100644 index 0000000..3d07fa9 --- /dev/null +++ b/src/main/java/com/yj/earth/FastFileEncryptor.java @@ -0,0 +1,226 @@ +package com.yj.earth; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.*; +import java.security.spec.KeySpec; +import java.util.Random; + +/** + * 文件快速加密解密工具类(仅加密文件开头部分,兼顾保护与效率) + */ +public class FastFileEncryptor { + // 加密算法参数 + private static final String ALGORITHM = "AES"; + private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; + private static final int KEY_SIZE = 256; // 需JCE无限制权限包 + private static final int IV_LENGTH = 16; // AES块大小 + private static final int ITERATION_COUNT = 65536; + private static final int SALT_LENGTH = 16; + // 只加密文件开头的1MB(可根据需求调整,建议512KB~2MB) + private static final int ENCRYPTED_LENGTH = 1024 * 1024; // 1MB + + + /** + * 加密文件(仅加密开头部分) + * @param sourceFilePath 源文件路径 + * @param encryptedFilePath 加密后文件路径 + * @param password 加密密码 + * @throws Exception 加密过程异常 + */ + public static void encryptFile(String sourceFilePath, String encryptedFilePath, String password) throws Exception { + File sourceFile = new File(sourceFilePath); + File encryptedFile = new File(encryptedFilePath); + + // 验证源文件 + if (!sourceFile.exists() || !sourceFile.isFile()) { + throw new FileNotFoundException("源文件不存在或不是文件: " + sourceFilePath); + } + // 检查磁盘空间 + if (!checkDiskSpace(encryptedFilePath, sourceFile.length())) { + throw new IOException("目标路径磁盘空间不足,至少需要 " + sourceFile.length() + " 字节"); + } + + // 生成盐值和IV向量 + byte[] salt = generateRandomBytes(SALT_LENGTH); + byte[] iv = generateRandomBytes(IV_LENGTH); + SecretKey secretKey = generateSecretKey(password, salt); + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv)); + + try ( + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sourceFile)); + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(encryptedFile)) + ) { + // 写入元数据:盐值(16) + IV(16) + 加密长度(4) + bos.write(salt); + bos.write(iv); + bos.write(intToBytes(ENCRYPTED_LENGTH)); + bos.flush(); + + // 加密开头指定长度的字节 + byte[] encryptBuffer = new byte[ENCRYPTED_LENGTH]; + int actualRead = bis.read(encryptBuffer); // 实际读取的字节数(可能文件小于1MB) + if (actualRead > 0) { + byte[] encryptedBytes = cipher.doFinal(encryptBuffer, 0, actualRead); + bos.write(encryptedBytes); + } + + // 直接复制剩余未加密的字节 + byte[] copyBuffer = new byte[1024 * 1024]; // 1MB缓冲区加速复制 + int bytesRead; + while ((bytesRead = bis.read(copyBuffer)) != -1) { + bos.write(copyBuffer, 0, bytesRead); + } + } + } + + + /** + * 解密文件(仅解密开头部分) + * @param encryptedFilePath 加密文件路径 + * @param decryptedFilePath 解密后文件路径 + * @param password 解密密码 + * @throws Exception 解密过程异常 + */ + public static void decryptFile(String encryptedFilePath, String decryptedFilePath, String password) throws Exception { + File encryptedFile = new File(encryptedFilePath); + File decryptedFile = new File(decryptedFilePath); + + // 验证加密文件 + if (!encryptedFile.exists() || !encryptedFile.isFile()) { + throw new FileNotFoundException("加密文件不存在或不是文件: " + encryptedFilePath); + } + // 检查磁盘空间(减去元数据字节数) + long requiredSize = encryptedFile.length() - SALT_LENGTH - IV_LENGTH - 4; + if (!checkDiskSpace(decryptedFilePath, requiredSize)) { + throw new IOException("目标路径磁盘空间不足,至少需要 " + requiredSize + " 字节"); + } + + try ( + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(encryptedFile)); + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(decryptedFile)) + ) { + // 读取元数据:盐值、IV、加密长度 + byte[] salt = new byte[SALT_LENGTH]; + byte[] iv = new byte[IV_LENGTH]; + byte[] lengthBytes = new byte[4]; + if (bis.read(salt) != SALT_LENGTH || bis.read(iv) != IV_LENGTH || bis.read(lengthBytes) != 4) { + throw new IOException("加密文件格式错误,元数据不完整"); + } + int encryptedLength = bytesToInt(lengthBytes); + + // 生成密钥并解密开头部分 + SecretKey secretKey = generateSecretKey(password, salt); + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); + + // 读取加密的开头数据(预留填充空间) + byte[] encryptedBytes = new byte[encryptedLength + 16]; // 最多额外16字节填充 + int actualEncryptedRead = bis.read(encryptedBytes); + if (actualEncryptedRead <= 0) { + throw new IOException("加密文件内容不完整"); + } + // 解密并写入(只取原始加密长度的字节,去除填充) + byte[] decryptedBytes = cipher.doFinal(encryptedBytes, 0, actualEncryptedRead); + bos.write(decryptedBytes, 0, Math.min(decryptedBytes.length, encryptedLength)); + + // 直接复制剩余未加密的字节 + byte[] copyBuffer = new byte[1024 * 1024]; + int bytesRead; + while ((bytesRead = bis.read(copyBuffer)) != -1) { + bos.write(copyBuffer, 0, bytesRead); + } + } + } + + + /** + * 生成随机字节数组(盐值或IV) + */ + private static byte[] generateRandomBytes(int length) { + byte[] bytes = new byte[length]; + new Random().nextBytes(bytes); + return bytes; + } + + + /** + * 基于密码和盐值生成AES密钥 + */ + private static SecretKey generateSecretKey(String password, byte[] salt) throws Exception { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_SIZE); + return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), ALGORITHM); + } + + + /** + * 检查目标路径磁盘空间 + */ + private static boolean checkDiskSpace(String targetPath, long requiredSize) { + File file = new File(targetPath); + File parent = file.getParentFile(); + if (parent == null) { + parent = new File(System.getProperty("user.dir")); + } + return parent.getFreeSpace() >= requiredSize; + } + + + /** + * int转4字节数组(大端序) + */ + private static byte[] intToBytes(int value) { + return new byte[]{ + (byte) (value >> 24), + (byte) (value >> 16), + (byte) (value >> 8), + (byte) value + }; + } + + + /** + * 4字节数组转int(大端序) + */ + private static int bytesToInt(byte[] bytes) { + return ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + (bytes[3] & 0xFF); + } + + + // 测试示例 + public static void main(String[] args) { + try { + // 测试参数(替换为实际文件路径) + String sourceFile = "E:\\yjearth\\poi\\poi.db"; // 原始大文件 + String encryptedFile = "E:\\yjearth\\poi\\poi_enc.db"; // 加密后文件 + String decryptedFile = "E:\\yjearth\\poi\\poi_dec.db"; // 解密后文件 + String password = "MySecretPassword123!"; // 密码 + + // 加密(仅加密开头1MB,速度极快) + System.out.println("开始加密文件..."); + long encryptStart = System.currentTimeMillis(); + encryptFile(sourceFile, encryptedFile, password); + long encryptEnd = System.currentTimeMillis(); + System.out.println("加密完成!耗时:" + (encryptEnd - encryptStart) + "ms,加密文件:" + encryptedFile); + + // 解密(仅解密开头1MB,速度极快) + System.out.println("开始解密文件..."); + long decryptStart = System.currentTimeMillis(); + decryptFile(encryptedFile, decryptedFile, password); + long decryptEnd = System.currentTimeMillis(); + System.out.println("解密完成!耗时:" + (decryptEnd - decryptStart) + "ms,解密文件:" + decryptedFile); + + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/yj/earth/business/controller/CsvController.java b/src/main/java/com/yj/earth/business/controller/CsvController.java new file mode 100644 index 0000000..82e4017 --- /dev/null +++ b/src/main/java/com/yj/earth/business/controller/CsvController.java @@ -0,0 +1,55 @@ +package com.yj.earth.business.controller; + +import com.yj.earth.common.util.ApiResponse; +import com.yj.earth.vo.CsvField; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Tag(name = "CSV文件解析") +@RestController +@RequestMapping("/csv") +public class CsvController { + @GetMapping("/parseCsv") + @Operation(summary = "解析CSV文件") + public ApiResponse parseCsv(@Parameter(description = "文件路径") @RequestParam String filePath) { + Map response = new HashMap<>(); + List fieldList = new ArrayList<>(); + + try { + File file = new File(filePath); + try (InputStreamReader reader = new InputStreamReader(new FileInputStream(file), "GBK"); + CSVParser parser = new CSVParser(reader, CSVFormat.DEFAULT)) { + // 遍历所有行(CSVRecord即一行数据) + for (CSVRecord record : parser) { + // 确保每行至少有3列(A、B、C列) + if (record.size() >= 3) { + String label = record.get(1).trim(); + String key = record.get(2).trim(); + fieldList.add(new CsvField(key, label)); + } + } + } + return ApiResponse.success(fieldList); + } catch (Exception e) { + return ApiResponse.failure(e.getMessage()); + } + } +} diff --git a/src/main/java/com/yj/earth/business/controller/DeviceController.java b/src/main/java/com/yj/earth/business/controller/DeviceController.java new file mode 100644 index 0000000..323dc8f --- /dev/null +++ b/src/main/java/com/yj/earth/business/controller/DeviceController.java @@ -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 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()); + } +} diff --git a/src/main/java/com/yj/earth/business/controller/PbfInfoController.java b/src/main/java/com/yj/earth/business/controller/PbfInfoController.java index 3a4d903..a1ff62b 100644 --- a/src/main/java/com/yj/earth/business/controller/PbfInfoController.java +++ b/src/main/java/com/yj/earth/business/controller/PbfInfoController.java @@ -10,10 +10,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; @@ -78,7 +75,7 @@ public class PbfInfoController { } @Operation(summary = "获取所有地图文件") - @PostMapping("/list") + @GetMapping("/list") public ApiResponse list() { LambdaQueryWrapper queryWrapper = new QueryWrapper().lambda(); // 把启用的排在最前面 diff --git a/src/main/java/com/yj/earth/business/controller/PoiController.java b/src/main/java/com/yj/earth/business/controller/PoiController.java new file mode 100644 index 0000000..7900e47 --- /dev/null +++ b/src/main/java/com/yj/earth/business/controller/PoiController.java @@ -0,0 +1,88 @@ +package com.yj.earth.business.controller; + +import com.yj.earth.common.util.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.Data; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.sql.*; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +@Tag(name = "全国POI管理") +@RestController +@RequestMapping("/poi") +public class PoiController { + private static Connection connection; + + static { + try { + Class.forName("org.sqlite.JDBC"); + String dbPath = System.getProperty("user.dir") + File.separator + "poi" + File.separator + "poi.db"; + connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath); + } catch (Exception e) { + throw new RuntimeException("SQLite连接初始化失败", e); + } + } + + @GetMapping("/data") + @Operation(summary = "查看数据") + public ApiResponse data(@Parameter(description = "分页页码") Integer pageNum, @Parameter(description = "分页大小") Integer pageSize, @Parameter(description = "名称搜索") String name) { + int offset = (pageNum - 1) * pageSize; + // 构建查询SQL + StringBuilder dataSql = new StringBuilder("SELECT id, name, address, lng, lat FROM tbl_pois WHERE 1=1"); + List params = new ArrayList<>(); + // 添加名称搜索条件 + if (name != null && !name.trim().isEmpty()) { + dataSql.append(" AND name LIKE ?"); + params.add("%" + name.trim() + "%"); + } + // 添加分页条件 + dataSql.append(" LIMIT ? OFFSET ?"); + try { + // 执行数据查询、获取List + List poiList = new ArrayList<>(); + long dataStartTime = System.currentTimeMillis(); + try (PreparedStatement dataPs = connection.prepareStatement(dataSql.toString())) { + // 设置参数(搜索条件 + 分页参数) + int paramIndex = 1; + for (Object param : params) { + dataPs.setObject(paramIndex++, param); + } + dataPs.setInt(paramIndex++, pageSize); + dataPs.setInt(paramIndex++, offset); + // 处理结果集、填充List + try (ResultSet dataRs = dataPs.executeQuery()) { + while (dataRs.next()) { + Poi poi = new Poi(); + poi.setId(dataRs.getLong("id")); + poi.setName(dataRs.getString("name")); + poi.setAddress(dataRs.getString("address")); + poi.setLng(dataRs.getString("lng")); + poi.setLat(dataRs.getString("lat")); + poiList.add(poi); + } + } + } + return ApiResponse.success(poiList); + } catch (SQLException e) { + return ApiResponse.failure("POI数据查询失败:" + e.getMessage()); + } + } + + @Data + private static class Poi { + private Long id; + private String name; + private String address; + private String lng; + private String lat; + } +} diff --git a/src/main/java/com/yj/earth/business/controller/SystemController.java b/src/main/java/com/yj/earth/business/controller/SystemController.java index 0b5be09..7ffbbca 100644 --- a/src/main/java/com/yj/earth/business/controller/SystemController.java +++ b/src/main/java/com/yj/earth/business/controller/SystemController.java @@ -1,21 +1,25 @@ package com.yj.earth.business.controller; import com.yj.earth.common.util.ApiResponse; +import com.yj.earth.datasource.DatabaseManager; import com.yj.earth.dto.system.UpdateSystemServiceDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Value; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; import java.io.*; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.sql.SQLException; import java.util.HashMap; import java.util.Map; @@ -23,35 +27,36 @@ import java.util.Map; @RestController @RequestMapping("/systemService") public class SystemController { - @Value("${server.host}") - private String serverHost; + @Value("${server.port}") private Integer serverPort; - // 配置文件路径(jar包所在目录的application.yml) private static final String CONFIG_FILE_PATH = "application.yml"; - // 重启脚本路径(app同级目录) - private static final String WINDOWS_REBOOT_SCRIPT = "../reboot.bat"; - private static final String LINUX_REBOOT_SCRIPT = "../reboot.sh"; - - @Operation(summary = "读取系统服务配置") + @Operation(summary = "读取系统服务端口配置") @PostMapping("/info") public ApiResponse readSystemServiceConfig() { - Map map = Map.of("serverHost", serverHost, "serverPort", serverPort); + // 只返回端口信息 + Map map = Map.of("serverPort", serverPort); return ApiResponse.success(map); } - @Operation(summary = "修改系统服务配置并重启") - @PostMapping("/update") - public ApiResponse updateSystemService(@RequestBody UpdateSystemServiceDto updateSystemServiceDto) { + @Operation(summary = "修改系统服务端口配置") + @PostMapping("/updatePort") + public ApiResponse updateSystemPort(@RequestBody UpdateSystemServiceDto updateSystemServiceDto) { try { + // 检查端口是否为空 + if (updateSystemServiceDto.getServerPort() == null) { + return ApiResponse.failure("端口号不能为空"); + } + // 读取并更新YAML配置 Yaml yaml = new Yaml(); File configFile = new File(CONFIG_FILE_PATH); if (!configFile.exists()) { return ApiResponse.failure("配置文件不存在"); } + // 解析YAML内容 Map configMap; try (InputStream in = Files.newInputStream(Paths.get(CONFIG_FILE_PATH))) { @@ -60,15 +65,12 @@ public class SystemController { configMap = new HashMap<>(); } } - // 更新配置 + + // 更新端口配置 Map serverMap = (Map) configMap.getOrDefault("server", new HashMap<>()); - if (updateSystemServiceDto.getServerHost() != null && !updateSystemServiceDto.getServerHost().isEmpty()) { - serverMap.put("host", updateSystemServiceDto.getServerHost()); - } - if (updateSystemServiceDto.getServerPort() != null) { - serverMap.put("port", updateSystemServiceDto.getServerPort()); - } + serverMap.put("port", updateSystemServiceDto.getServerPort()); configMap.put("server", serverMap); + // 保存配置 DumperOptions options = new DumperOptions(); options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); @@ -77,44 +79,71 @@ public class SystemController { try (FileWriter writer = new FileWriter(configFile, StandardCharsets.UTF_8)) { writeYaml.dump(configMap, writer); } - // 执行重启脚本 - executeRebootScript(); + return ApiResponse.success(null); } catch (IOException e) { - return ApiResponse.failure("更新配置失败:" + e.getMessage()); + return ApiResponse.failure("更新端口配置失败:" + e.getMessage()); } catch (Exception e) { return ApiResponse.failure("操作失败:" + e.getMessage()); } } - /** - * 执行重启脚本 - */ - private void executeRebootScript() throws IOException, InterruptedException { - String scriptPath; - ProcessBuilder processBuilder; - - // 判断操作系统类型、选择对应的脚本 - if (System.getProperty("os.name").toLowerCase().contains("win")) { - scriptPath = WINDOWS_REBOOT_SCRIPT; - // 检查Windows脚本是否存在 - if (!new File(scriptPath).exists()) { - throw new FileNotFoundException("Windows重启脚本不存在: " + scriptPath); - } - processBuilder = new ProcessBuilder(scriptPath); - } else { - scriptPath = LINUX_REBOOT_SCRIPT; - // 检查Linux脚本是否存在 - if (!new File(scriptPath).exists()) { - throw new FileNotFoundException("Linux重启脚本不存在: " + scriptPath); - } - // 给脚本添加执行权限 - new ProcessBuilder("chmod", "+x", scriptPath).start().waitFor(); - processBuilder = new ProcessBuilder(scriptPath); + @Operation(summary = "工程导出") + @GetMapping("/export") + public void exportProject(HttpServletResponse response) { + // 获取SQLite文件绝对路径 + String sqliteFilePath = DatabaseManager.getSqliteDbFilePath(); + // 校验路径与文件有效性 + if (sqliteFilePath == null || sqliteFilePath.isEmpty()) { + handleError(response, HttpStatus.BAD_REQUEST.value(), "SQLite数据库未初始化"); + return; } + File sqliteFile = new File(sqliteFilePath); + if (!sqliteFile.exists()) { + handleError(response, HttpStatus.NOT_FOUND.value(), "SQLite文件不存在"); + return; + } + if (!sqliteFile.canRead()) { + handleError(response, HttpStatus.FORBIDDEN.value(), "无权限读取SQLite文件"); + return; + } + // 配置下载响应头(让浏览器触发下载而非打开) + response.setContentType("application/octet-stream"); + response.setContentLengthLong(sqliteFile.length()); + // 处理文件名编码 + String fileName = URLEncoder.encode("app.db", StandardCharsets.UTF_8); + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + response.setBufferSize(1024 * 8); // 设置响应缓冲区(提升传输效率) + // 读取文件流并写入响应 + try (InputStream inputStream = new BufferedInputStream(new FileInputStream(sqliteFile)); + OutputStream outputStream = new BufferedOutputStream(response.getOutputStream())) { + byte[] buffer = new byte[1024 * 8]; + int len; + // 循环读取文件内容并写入响应流 + while ((len = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, len); + } + // 强制刷新缓冲区、确保所有数据发送到前端 + response.flushBuffer(); + } catch (IOException e) { + handleError(response, HttpStatus.INTERNAL_SERVER_ERROR.value(), "文件下载失败:" + e.getMessage()); + } + } - // 执行脚本(在新进程中运行、不阻塞当前请求) - processBuilder.start(); + + + /** + * 返回指定状态码与错误信息 + */ + private void handleError(HttpServletResponse response, int statusCode, String message) { + try { + response.setStatus(statusCode); + response.setContentType("text/plain; charset=utf-8"); + response.getWriter().write(message); + response.getWriter().flush(); + } catch (IOException e) { + e.printStackTrace(); + } } } diff --git a/src/main/java/com/yj/earth/business/controller/UserController.java b/src/main/java/com/yj/earth/business/controller/UserController.java index 0342f67..31d9593 100644 --- a/src/main/java/com/yj/earth/business/controller/UserController.java +++ b/src/main/java/com/yj/earth/business/controller/UserController.java @@ -4,6 +4,7 @@ import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpUtil; import cn.hutool.crypto.digest.BCrypt; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.yj.earth.annotation.CheckAuth; import com.yj.earth.annotation.EncryptResponse; @@ -13,10 +14,7 @@ import com.yj.earth.business.domain.Role; import com.yj.earth.business.domain.User; import com.yj.earth.business.service.RoleService; import com.yj.earth.dto.relation.UserBindOrUnBindRoleDto; -import com.yj.earth.dto.user.AddUserDto; -import com.yj.earth.dto.user.UpdatePasswordDto; -import com.yj.earth.dto.user.UpdateUserDto; -import com.yj.earth.dto.user.UserLoginDto; +import com.yj.earth.dto.user.*; import com.yj.earth.business.service.UserService; import com.yj.earth.common.util.ApiResponse; import io.swagger.v3.oas.annotations.Operation; @@ -38,7 +36,6 @@ public class UserController { @Resource private RoleService roleService; - @CheckAuth @Operation(summary = "新增用户") @PostMapping("/add") @RoleAccess(roleNames = "管理员") @@ -58,16 +55,6 @@ public class UserController { return ApiResponse.success(null); } - @CheckAuth - @Operation(summary = "删除用户") - @PostMapping("/delete") - @RoleAccess(roleNames = "管理员") - public ApiResponse delete(@Parameter(description = "用户ID") String id) { - userService.removeById(id); - return ApiResponse.success(null); - } - - @CheckAuth @Operation(summary = "更新信息") @PostMapping("/update") public ApiResponse update(@RequestBody UpdateUserDto updateUserDto) { @@ -77,7 +64,6 @@ public class UserController { return ApiResponse.success(null); } - @CheckAuth @Operation(summary = "更新密码") @PostMapping("/updatePassword") public ApiResponse updatePassword(@RequestBody UpdatePasswordDto updatePasswordDto) { @@ -93,19 +79,29 @@ public class UserController { return ApiResponse.success(null); } - @CheckAuth @Operation(summary = "用户详情") @GetMapping("/getById") public ApiResponse get(@Parameter(description = "用户ID") String id) { return ApiResponse.success(userService.getById(id)); } - @CheckAuth @Operation(summary = "用户列表") @GetMapping("/list") @RoleAccess(roleNames = "管理员") - public ApiResponse list(@Parameter(description = "分页数量") Integer pageNum, @Parameter(description = "分页大小") Integer pageSize) { - Page userPage = userService.page(new Page<>(pageNum, pageSize)); + public ApiResponse list(@Parameter(description = "分页数量") Integer pageNum, + @Parameter(description = "分页大小") Integer pageSize, + @Parameter(description = "搜索字段") String searchKey, + @Parameter(description = "角色ID") String roleId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + // 根据用户名或者昵称进行模糊搜索 + if (StringUtils.isNotBlank(searchKey)) { + wrapper.like(User::getUsername, searchKey).or().like(User::getNickname, searchKey); + } + // 根据角色ID等值搜索 + if (StringUtils.isNotBlank(roleId)) { + wrapper.eq(User::getRoleId, roleId); + } + Page userPage = userService.page(new Page<>(pageNum, pageSize), wrapper); return ApiResponse.success(userPage); } @@ -144,4 +140,22 @@ public class UserController { public ApiResponse getCurrentUserInfo() { return ApiResponse.success(userService.getById(StpUtil.getLoginIdAsString())); } + + @Operation(summary = "启用禁用用户数统计") + @GetMapping("/getUserStatusCount") + public ApiResponse getUserStatusCount() { + // 查询状态为1的用户数 + long useUserCount = userService.count(new LambdaQueryWrapper().eq(User::getStatus, 1)); + // 查询状态为0的用户数 + long bindUserCount = userService.count(new LambdaQueryWrapper().eq(User::getStatus, 0)); + return ApiResponse.success(Map.of("useUserCount", useUserCount, "bindUserCount", bindUserCount)); + } + + @Operation(summary = "删除用户") + @PostMapping("/deletes") + @RoleAccess(roleNames = "管理员") + public ApiResponse deletes(@RequestBody List ids) { + userService.removeByIds(ids); + return ApiResponse.success(null); + } } diff --git a/src/main/java/com/yj/earth/business/controller/WebSourceController.java b/src/main/java/com/yj/earth/business/controller/WebSourceController.java index 21d7fda..abb569f 100644 --- a/src/main/java/com/yj/earth/business/controller/WebSourceController.java +++ b/src/main/java/com/yj/earth/business/controller/WebSourceController.java @@ -4,8 +4,10 @@ import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.UUID; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.yj.earth.annotation.RoleAccess; +import com.yj.earth.business.domain.PbfInfo; import com.yj.earth.business.domain.WebSource; import com.yj.earth.business.service.ModelLibraryService; +import com.yj.earth.business.service.PbfInfoService; import com.yj.earth.business.service.SourceService; import com.yj.earth.business.service.WebSourceService; import com.yj.earth.common.util.ApiResponse; @@ -39,6 +41,8 @@ public class WebSourceController { private ModelLibraryController modelLibraryController; @Resource private SourceService sourceService; + @Resource + private PbfInfoService pbfInfoService; private static final List SUPPORT_EXTENSIONS = Arrays.asList("clt", "mbtiles", "pak", "pbf", "model"); @RoleAccess(roleNames = "管理员") @@ -102,6 +106,15 @@ public class WebSourceController { if (webSource.getType().equals("model")) { modelLibraryController.importModelLibrary(filePath); } + + // 如果是PBF文件则调用地图服务 + if (webSource.getType().equals("pbf")) { + PbfInfo pbfInfo = new PbfInfo(); + pbfInfo.setName(FileUtil.getName(filePath)); + pbfInfo.setPath(filePath); + pbfInfo.setIsEnable(0); + pbfInfoService.save(pbfInfo); + } } /** diff --git a/src/main/java/com/yj/earth/business/domain/Device.java b/src/main/java/com/yj/earth/business/domain/Device.java new file mode 100644 index 0000000..2011610 --- /dev/null +++ b/src/main/java/com/yj/earth/business/domain/Device.java @@ -0,0 +1,45 @@ +package com.yj.earth.business.domain; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import java.time.LocalDateTime; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Data +public class Device implements Serializable { + @TableId(value = "id", type = IdType.ASSIGN_UUID) + @Schema(description = "主键") + private String id; + @Schema(description = "设备名称") + private String cameraName; + @Schema(description = "设备类型") + private String type; + @Schema(description = "设备ip") + private String ip; + @Schema(description = "设备端口") + private Integer port; + @Schema(description = "设备用户名") + private String username; + @Schema(description = "设备密码") + private String password; + @Schema(description = "设备通道") + private Integer channel; + @Schema(description = "设备FLV地址") + private String flvUrl; + @Schema(description = "设备状态") + private Integer status; + @Schema(description = "创建时间") + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createdAt; + @Schema(description = "更新时间") + @TableField(fill = FieldFill.UPDATE) + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/yj/earth/business/domain/Role.java b/src/main/java/com/yj/earth/business/domain/Role.java index ce48a9f..9490ab7 100644 --- a/src/main/java/com/yj/earth/business/domain/Role.java +++ b/src/main/java/com/yj/earth/business/domain/Role.java @@ -25,6 +25,9 @@ public class Role implements Serializable { @Schema(description = "角色名称") private String roleName; + @Schema(description = "状态(0未启用、1启用)") + private Integer status; + @Schema(description = "角色描述") private String description; diff --git a/src/main/java/com/yj/earth/business/domain/User.java b/src/main/java/com/yj/earth/business/domain/User.java index 783d97f..acd9bbe 100644 --- a/src/main/java/com/yj/earth/business/domain/User.java +++ b/src/main/java/com/yj/earth/business/domain/User.java @@ -41,6 +41,9 @@ public class User implements Serializable { @Schema(description = "所属角色") private String roleId; + @Schema(description = "状态(0未启用、1启用)") + private Integer status; + @Schema(description = "创建时间") @TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt; diff --git a/src/main/java/com/yj/earth/business/mapper/DeviceMapper.java b/src/main/java/com/yj/earth/business/mapper/DeviceMapper.java new file mode 100644 index 0000000..7e10c76 --- /dev/null +++ b/src/main/java/com/yj/earth/business/mapper/DeviceMapper.java @@ -0,0 +1,18 @@ +package com.yj.earth.business.mapper; + +import com.yj.earth.business.domain.Device; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author 周志雄 + * @since 2025-10-14 + */ +@Mapper +public interface DeviceMapper extends BaseMapper { + +} diff --git a/src/main/java/com/yj/earth/business/service/DeviceService.java b/src/main/java/com/yj/earth/business/service/DeviceService.java new file mode 100644 index 0000000..d974cca --- /dev/null +++ b/src/main/java/com/yj/earth/business/service/DeviceService.java @@ -0,0 +1,16 @@ +package com.yj.earth.business.service; + +import com.yj.earth.business.domain.Device; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 服务类 + *

+ * + * @author 周志雄 + * @since 2025-10-14 + */ +public interface DeviceService extends IService { + +} diff --git a/src/main/java/com/yj/earth/business/service/impl/DeviceServiceImpl.java b/src/main/java/com/yj/earth/business/service/impl/DeviceServiceImpl.java new file mode 100644 index 0000000..89df251 --- /dev/null +++ b/src/main/java/com/yj/earth/business/service/impl/DeviceServiceImpl.java @@ -0,0 +1,20 @@ +package com.yj.earth.business.service.impl; + +import com.yj.earth.business.domain.Device; +import com.yj.earth.business.mapper.DeviceMapper; +import com.yj.earth.business.service.DeviceService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author 周志雄 + * @since 2025-10-14 + */ +@Service +public class DeviceServiceImpl extends ServiceImpl implements DeviceService { + +} diff --git a/src/main/java/com/yj/earth/common/service/ServerInitService.java b/src/main/java/com/yj/earth/common/service/ServerInitService.java index 5d76c70..868c3d5 100644 --- a/src/main/java/com/yj/earth/common/service/ServerInitService.java +++ b/src/main/java/com/yj/earth/common/service/ServerInitService.java @@ -28,9 +28,10 @@ public class ServerInitService { private RoleService roleService; @Resource private RoleSourceService roleSourceService; + public void init() { // 查询数据库所有需要加载的资源 - List list =sourceService.list(new LambdaQueryWrapper() + List list = sourceService.list(new LambdaQueryWrapper() .eq(Source::getSourceType, "terrain") .or().eq(Source::getSourceType, "layer") .or().eq(Source::getSourceType, "tileset")); @@ -48,18 +49,20 @@ public class ServerInitService { public void checkDefaultUserAndRole() { // 查询角色表和用户表是否有数据 - if(roleService.count() == 0 && userService.count() == 0) { + if (roleService.count() == 0 && userService.count() == 0) { log.info("初始化默认用户角色数据"); // 新增一个管理员角色 Role adminRole = new Role(); adminRole.setRoleName("管理员"); adminRole.setDescription("系统管理员"); + adminRole.setStatus(1); adminRole.setIsSuper(1); roleService.save(adminRole); // 新增一个默认角色 Role defaultRole = new Role(); defaultRole.setRoleName("默认角色"); defaultRole.setDescription("系统默认角色"); + defaultRole.setStatus(1); defaultRole.setIsSuper(0); roleService.save(defaultRole); // 新增一个用户 @@ -67,6 +70,7 @@ public class ServerInitService { user.setUsername("admin"); user.setPassword(BCrypt.hashpw("admin123", BCrypt.gensalt())); user.setNickname("管理员"); + user.setStatus(1); user.setRoleId(adminRole.getId()); user.setPhone("13888888888"); userService.save(user); diff --git a/src/main/java/com/yj/earth/common/util/CodeUtil.java b/src/main/java/com/yj/earth/common/util/CodeUtil.java index 920e159..67e2198 100644 --- a/src/main/java/com/yj/earth/common/util/CodeUtil.java +++ b/src/main/java/com/yj/earth/common/util/CodeUtil.java @@ -34,7 +34,7 @@ public class CodeUtil { } // 传入需要生成代码的表名 - Generation("pbf_info"); + Generation("device"); } public static void Generation(String... tableName) { diff --git a/src/main/java/com/yj/earth/common/util/GdalJsonConverter.java b/src/main/java/com/yj/earth/common/util/GdalJsonConverter.java index 033ee07..72b6b84 100644 --- a/src/main/java/com/yj/earth/common/util/GdalJsonConverter.java +++ b/src/main/java/com/yj/earth/common/util/GdalJsonConverter.java @@ -8,6 +8,7 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -15,34 +16,22 @@ import java.util.Map; import static org.gdal.ogr.ogrConstants.*; -/** - * GDAL地理数据转JSON工具类 - */ -import org.gdal.gdal.gdal; -import org.gdal.ogr.*; -import org.gdal.osr.SpatialReference; -import org.json.JSONArray; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * GDAL地理数据转JSON工具类 - */ public class GdalJsonConverter { static { - // 初始化GDAL + // 动态获取项目根目录下的lib文件夹路径 + String projectRoot = System.getProperty("user.dir"); + + // 拼接lib目录路径(项目根目录/lib) + String projLibPath = projectRoot + File.separator + "lib"; + + // 设置PROJ库路径(指向包含proj.db的lib目录) + gdal.SetConfigOption("PROJ_LIB", projLibPath); gdal.AllRegister(); } /** * 将地理数据文件转换为指定结构的JSON - * @param filePath 文件绝对路径 - * @return JSON字符串 */ public static String convertToJson(String filePath) { return convertToJson(filePath, ""); @@ -50,11 +39,8 @@ public class GdalJsonConverter { /** * 将地理数据文件转换为指定结构的JSON - * @param filePath 文件绝对路径 - * @param targetCrs 目标坐标系(如:"EPSG:4326"),为空则不转换 - * @return JSON字符串 */ - public static String convertToJson(String filePath, String targetCrs) { + private static String convertToJson(String filePath, String targetCrs) { try { // 打开数据源 DataSource dataSource = ogr.Open(filePath, 0); @@ -80,7 +66,7 @@ public class GdalJsonConverter { result.put("list", listArray); dataSource.delete(); - return result.toString(2); // 缩进2个空格,美化输出 + return result.toString(2); } catch (Exception e) { throw new RuntimeException("转换失败: " + e.getMessage(), e); @@ -88,7 +74,7 @@ public class GdalJsonConverter { } /** - * 创建FeatureCollection对象 + * 创建 FeatureCollection 对象 */ private static JSONObject createFeatureCollection(Layer layer, String layerName, String targetCrs) throws JSONException { JSONObject featureCollection = new JSONObject(); @@ -108,10 +94,10 @@ public class GdalJsonConverter { // 重置图层读取位置 layer.ResetReading(); - // 获取图层定义,用于字段信息 + // 获取图层定义、用于字段信息 FeatureDefn layerDefn = layer.GetLayerDefn(); - // 如果需要坐标转换,创建坐标转换对象 + // 如果需要坐标转换、创建坐标转换对象 SpatialReference sourceSrs = layer.GetSpatialRef(); SpatialReference targetSrs = null; CoordinateTransformation coordTrans = null; @@ -164,7 +150,7 @@ public class GdalJsonConverter { return proj4.trim(); } - // 如果PROJ4为空,尝试获取WKT + // 如果PROJ4为空、尝试获取WKT String wkt = srs.ExportToWkt(); if (wkt != null && !wkt.trim().isEmpty()) { return wkt.trim(); @@ -186,8 +172,7 @@ public class GdalJsonConverter { /** * 创建单个Feature对象 */ - private static JSONObject createFeature(Feature feature, FeatureDefn layerDefn, - CoordinateTransformation coordTrans, boolean transformCoords) throws JSONException { + private static JSONObject createFeature(Feature feature, FeatureDefn layerDefn, CoordinateTransformation coordTrans, boolean transformCoords) throws JSONException { JSONObject featureObj = new JSONObject(); featureObj.put("type", "Feature"); @@ -220,7 +205,7 @@ public class GdalJsonConverter { geomToUse = transformedGeom; } catch (Exception e) { System.err.println("坐标转换失败: " + e.getMessage()); - // 如果转换失败,使用原始几何 + // 如果转换失败、使用原始几何 geomToUse = geometry; } } @@ -233,7 +218,7 @@ public class GdalJsonConverter { geomToUse.delete(); } } else { - // 如果没有几何信息,创建空的MultiPolygon + // 如果没有几何信息、创建空的MultiPolygon featureObj.put("geometry", createEmptyMultiPolygon()); } @@ -452,11 +437,9 @@ public class GdalJsonConverter { layerInfo.put("name", layer.GetName()); layerInfo.put("featureCount", layer.GetFeatureCount()); layerInfo.put("geometryType", ogr.GeometryTypeToName(layer.GetGeomType())); - // 添加坐标系信息 String crs = getLayerCrs(layer); layerInfo.put("crs", crs != null ? crs : "未知"); - layers.add(layerInfo); } info.put("layers", layers); diff --git a/src/main/java/com/yj/earth/common/util/SdkUtil.java b/src/main/java/com/yj/earth/common/util/SdkUtil.java index 54998cc..cf912b8 100644 --- a/src/main/java/com/yj/earth/common/util/SdkUtil.java +++ b/src/main/java/com/yj/earth/common/util/SdkUtil.java @@ -171,11 +171,11 @@ public class SdkUtil { } } } - log.error("sdk配置文件中未配置server.port"); + log.error("sdk 配置文件中未配置 server.port"); } catch (IOException e) { - log.error("读取sdk配置文件失败", e); + log.error("读取 sdk 配置文件失败", e); } catch (ClassCastException e) { - log.error("sdk配置文件中server.port格式错误、应为数字类型", e); + log.error("sdk 配置文件中 server.port 格式错误、应为数字类型", e); } return null; } diff --git a/src/main/java/com/yj/earth/datasource/DatabaseManager.java b/src/main/java/com/yj/earth/datasource/DatabaseManager.java index e9a4d8a..e694826 100644 --- a/src/main/java/com/yj/earth/datasource/DatabaseManager.java +++ b/src/main/java/com/yj/earth/datasource/DatabaseManager.java @@ -53,6 +53,7 @@ public class DatabaseManager { classes.add(BusinessConfig.class); classes.add(WebSource.class); classes.add(PbfInfo.class); + classes.add(Device.class); ENTITY_CLASSES = Collections.unmodifiableList(classes); } @@ -192,10 +193,6 @@ public class DatabaseManager { } } - public static void createTablesForEntities(DatabaseType dbType, Class entityClass, Connection connection) throws SQLException { - createTableIfNotExists(connection, dbType, entityClass); - } - private static void createTableIfNotExists(Connection connection, DatabaseType dbType, Class entityClass) throws SQLException { String tableName = getUnderlineName(entityClass.getSimpleName()); diff --git a/src/main/java/com/yj/earth/design/Device.java b/src/main/java/com/yj/earth/design/Device.java new file mode 100644 index 0000000..b3d8076 --- /dev/null +++ b/src/main/java/com/yj/earth/design/Device.java @@ -0,0 +1,34 @@ +package com.yj.earth.design; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class Device { + @Schema(description = "主键") + private String id; + @Schema(description = "设备名称") + private String cameraName; + @Schema(description = "设备类型") + private String type; + @Schema(description = "设备ip") + private String ip; + @Schema(description = "设备端口") + private Integer port; + @Schema(description = "设备用户名") + private String username; + @Schema(description = "设备密码") + private String password; + @Schema(description = "设备通道") + private Integer channel; + @Schema(description = "设备FLV地址") + private String flvUrl; + @Schema(description = "设备状态") + private Integer status; + @Schema(description = "创建时间") + private LocalDateTime createdAt; + @Schema(description = "更新时间") + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/yj/earth/design/Role.java b/src/main/java/com/yj/earth/design/Role.java index 51d200d..835621b 100644 --- a/src/main/java/com/yj/earth/design/Role.java +++ b/src/main/java/com/yj/earth/design/Role.java @@ -13,6 +13,8 @@ public class Role { private String roleName; @Schema(description = "角色描述") private String description; + @Schema(description = "状态(0未启用、1启用)") + private Integer status; @Schema(description = "是否超级管理员") private Integer isSuper; @Schema(description = "创建时间") diff --git a/src/main/java/com/yj/earth/design/User.java b/src/main/java/com/yj/earth/design/User.java index b7567f7..fb2c4b3 100644 --- a/src/main/java/com/yj/earth/design/User.java +++ b/src/main/java/com/yj/earth/design/User.java @@ -6,7 +6,7 @@ import lombok.Data; import java.time.LocalDateTime; @Data -public class User{ +public class User { @Schema(description = "主键") private String id; @Schema(description = "用户名") @@ -19,6 +19,8 @@ public class User{ private String nickname; @Schema(description = "手机号") private String phone; + @Schema(description = "状态(0未启用、1启用)") + private Integer status; @Schema(description = "所属角色") private String roleId; @Schema(description = "创建时间") diff --git a/src/main/java/com/yj/earth/dto/device/ImportDeviceDto.java b/src/main/java/com/yj/earth/dto/device/ImportDeviceDto.java new file mode 100644 index 0000000..b4816f4 --- /dev/null +++ b/src/main/java/com/yj/earth/dto/device/ImportDeviceDto.java @@ -0,0 +1,23 @@ +package com.yj.earth.dto.device; + + +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +@Data +public class ImportDeviceDto { + @ExcelProperty(value = "名称", index = 0) + private String cameraName; + @ExcelProperty(value = "设备IP", index = 1) + private String ip; + @ExcelProperty(value = "设备端口", index = 2) + private Integer port; + @ExcelProperty(value = "用户名", index = 3) + private String username; + @ExcelProperty(value = "密码", index = 4) + private String password; + @ExcelProperty(value = "设备类型", index = 5) + private String type; + @ExcelProperty(value = "通道号", index = 6) + private Integer channel; +} diff --git a/src/main/java/com/yj/earth/dto/device/UpdateDeviceDto.java b/src/main/java/com/yj/earth/dto/device/UpdateDeviceDto.java new file mode 100644 index 0000000..7fca5a5 --- /dev/null +++ b/src/main/java/com/yj/earth/dto/device/UpdateDeviceDto.java @@ -0,0 +1,24 @@ +package com.yj.earth.dto.device; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class UpdateDeviceDto { + @Schema(description = "设备ID") + private String id; + @Schema(description = "设备名称") + private String cameraName; + @Schema(description = "设备类型") + private String type; + @Schema(description = "设备ip") + private String ip; + @Schema(description = "设备端口") + private Integer port; + @Schema(description = "设备用户名") + private String username; + @Schema(description = "设备密码") + private String password; + @Schema(description = "设备通道") + private Integer channel; +} diff --git a/src/main/java/com/yj/earth/dto/system/UpdateSystemServiceDto.java b/src/main/java/com/yj/earth/dto/system/UpdateSystemServiceDto.java index 9cad785..cf65023 100644 --- a/src/main/java/com/yj/earth/dto/system/UpdateSystemServiceDto.java +++ b/src/main/java/com/yj/earth/dto/system/UpdateSystemServiceDto.java @@ -5,8 +5,6 @@ import lombok.Data; @Data public class UpdateSystemServiceDto { - @Schema(description = "服务地址") - private String serverHost; @Schema(description = "服务端口") private Integer serverPort; } diff --git a/src/main/java/com/yj/earth/dto/user/UpdateUserDto.java b/src/main/java/com/yj/earth/dto/user/UpdateUserDto.java index 4acd740..c9572cd 100644 --- a/src/main/java/com/yj/earth/dto/user/UpdateUserDto.java +++ b/src/main/java/com/yj/earth/dto/user/UpdateUserDto.java @@ -8,13 +8,12 @@ import lombok.Data; public class UpdateUserDto { @Schema(description = "主键") private String id; - @Schema(description = "头像") private String avatar; - @Schema(description = "昵称") private String nickname; - @Schema(description = "手机号") private String phone; + @Schema(description = "用户状态") + private Integer status; } diff --git a/src/main/java/com/yj/earth/dto/user/UpdateUserStatusDto.java b/src/main/java/com/yj/earth/dto/user/UpdateUserStatusDto.java new file mode 100644 index 0000000..d4989c2 --- /dev/null +++ b/src/main/java/com/yj/earth/dto/user/UpdateUserStatusDto.java @@ -0,0 +1,12 @@ +package com.yj.earth.dto.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class UpdateUserStatusDto { + @Schema(description = "用户ID") + private String id; + @Schema(description = "用户状态") + private Integer status; +} diff --git a/src/main/java/com/yj/earth/params/Gdb.java b/src/main/java/com/yj/earth/params/Gdb.java new file mode 100644 index 0000000..22a3f32 --- /dev/null +++ b/src/main/java/com/yj/earth/params/Gdb.java @@ -0,0 +1,11 @@ +package com.yj.earth.params; + +import com.yj.earth.annotation.SourceType; +import lombok.Data; + +@Data +@SourceType("gdb") +public class Gdb { + private String richText; + private String sourcePath; +} diff --git a/src/main/java/com/yj/earth/vo/AddDeviceDto.java b/src/main/java/com/yj/earth/vo/AddDeviceDto.java new file mode 100644 index 0000000..4d99c5b --- /dev/null +++ b/src/main/java/com/yj/earth/vo/AddDeviceDto.java @@ -0,0 +1,22 @@ +package com.yj.earth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class AddDeviceDto { + @Schema(description = "设备名称") + private String cameraName; + @Schema(description = "设备类型") + private String type; + @Schema(description = "设备ip") + private String ip; + @Schema(description = "设备端口") + private Integer port; + @Schema(description = "设备用户名") + private String username; + @Schema(description = "设备密码") + private String password; + @Schema(description = "设备通道") + private Integer channel; +} diff --git a/src/main/java/com/yj/earth/vo/CsvField.java b/src/main/java/com/yj/earth/vo/CsvField.java new file mode 100644 index 0000000..971846d --- /dev/null +++ b/src/main/java/com/yj/earth/vo/CsvField.java @@ -0,0 +1,14 @@ +package com.yj.earth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class CsvField { + @Schema(description = "字段名称") + private String key; + @Schema(description = "字段描述") + private String label; +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f990055..da2bb7c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,5 @@ server: - host: 192.168.110.25 + host: 192.168.110.137 port: 8848 sdk: diff --git a/src/main/resources/mapper/DeviceMapper.xml b/src/main/resources/mapper/DeviceMapper.xml new file mode 100644 index 0000000..cced52d --- /dev/null +++ b/src/main/resources/mapper/DeviceMapper.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + id, camera_name, type, ip, port, user_name, password, channel, flv_url, created_at, updated_at + + +