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