WEB:导入设备数据

This commit is contained in:
2025-07-04 11:02:22 +08:00
parent e8aee3039a
commit d456236599
17 changed files with 1106 additions and 38 deletions

View File

@ -9,7 +9,6 @@ import org.springframework.boot.context.metrics.buffering.BufferingApplicationSt
*
* @author Lion Li
*/
@SpringBootApplication
public class DromaraApplication {

View File

@ -15,6 +15,10 @@
fys-common-core 核心模块
</description>
<properties>
<hutool.version>5.8.35</hutool.version>
</properties>
<dependencies>
<!-- Spring框架基本的核心工具 -->
<dependency>
@ -56,6 +60,13 @@
<artifactId>hutool-core</artifactId>
</dependency>
<!--工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
@ -66,6 +77,33 @@
<artifactId>hutool-extra</artifactId>
</dependency>
<!-- excel工具 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.4.0</version>
</dependency>
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>2.12.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-full</artifactId>
<version>5.4.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

View File

@ -0,0 +1,41 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fuyuanshen.common.core.exception;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
/**
* @author Zheng Jie
* @date 2018-11-23
* 统一异常处理
*/
@Getter
public class BadRequestException extends RuntimeException{
private Integer status = BAD_REQUEST.value();
public BadRequestException(String msg){
super(msg);
}
public BadRequestException(HttpStatus status,String msg){
super(msg);
this.status = status.value();
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fuyuanshen.common.core.exception;
import org.springframework.util.StringUtils;
/**
* @author Zheng Jie
* @date 2018-11-23
*/
public class EntityExistException extends RuntimeException {
public EntityExistException(Class clazz, String field, String val) {
super(EntityExistException.generateMessage(clazz.getSimpleName(), field, val));
}
private static String generateMessage1(String entity, String field, String val) {
return StringUtils.capitalize(entity) + " with " + field + " " + val + " existed";
}
private static String generateMessage(String entity, String field, String val) {
return field + " " + val + " 已存在";
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fuyuanshen.common.core.exception;
import org.springframework.util.StringUtils;
/**
* @author Zheng Jie
* @date 2018-11-23
*/
public class EntityNotFoundException extends RuntimeException {
public EntityNotFoundException(Class clazz, String field, String val) {
super(EntityNotFoundException.generateMessage(clazz.getSimpleName(), field, val));
}
private static String generateMessage(String entity, String field, String val) {
return StringUtils.capitalize(entity)
+ " with " + field + " "+ val + " does not exist";
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fuyuanshen.common.core.exception.handler;
import lombok.Data;
/**
* @author Zheng Jie
* @date 2018-11-23
*/
@Data
public class ApiError {
private Integer status = 400;
private Long timestamp;
private String message;
private ApiError() {
timestamp = System.currentTimeMillis();
}
public static ApiError error(String message){
ApiError apiError = new ApiError();
apiError.setMessage(message);
return apiError;
}
public static ApiError error(Integer status, String message){
ApiError apiError = new ApiError();
apiError.setStatus(status);
apiError.setMessage(message);
return apiError;
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fuyuanshen.common.core.utils;
import java.io.Closeable;
/**
* @author Zheng Jie
* @description 用于关闭各种连接,缺啥补啥
* @date 2021-03-05
**/
public class CloseUtil {
public static void close(Closeable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (Exception e) {
// 静默关闭
}
}
}
public static void close(AutoCloseable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (Exception e) {
// 静默关闭
}
}
}
}

View File

@ -0,0 +1,413 @@
/*
* Copyright 2019-2025 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fuyuanshen.common.core.utils.file;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.poi.excel.BigExcelWriter;
import cn.hutool.poi.excel.ExcelUtil;
import com.fuyuanshen.common.core.exception.BadRequestException;
import com.fuyuanshen.common.core.utils.CloseUtil;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* File工具类扩展 hutool 工具包
*
* @author Zheng Jie
* @date 2018-12-27
*/
@Slf4j
public class FileUtil extends cn.hutool.core.io.FileUtil {
/**
* 系统临时目录
* <br>
* windows 包含路径分割符但Linux 不包含,
* 在windows \\==\ 前提下,
* 为安全起见 同意拼装 路径分割符,
* <pre>
* java.io.tmpdir
* windows : C:\Users/xxx\AppData\Local\Temp\
* linux: /temp
* </pre>
*/
public static final String SYS_TEM_DIR = System.getProperty("java.io.tmpdir") + File.separator;
/**
* 定义GB的计算常量
*/
private static final int GB = 1024 * 1024 * 1024;
/**
* 定义MB的计算常量
*/
private static final int MB = 1024 * 1024;
/**
* 定义KB的计算常量
*/
private static final int KB = 1024;
/**
* 格式化小数
*/
private static final DecimalFormat DF = new DecimalFormat("0.00");
public static final String IMAGE = "图片";
public static final String TXT = "文档";
public static final String MUSIC = "音乐";
public static final String VIDEO = "视频";
public static final String OTHER = "其他";
/**
* MultipartFile转File
*/
public static File toFile(MultipartFile multipartFile) {
// 获取文件名
String fileName = multipartFile.getOriginalFilename();
// 获取文件后缀
String prefix = "." + getExtensionName(fileName);
File file = null;
try {
// 用uuid作为文件名防止生成的临时文件重复
file = new File(SYS_TEM_DIR + IdUtil.simpleUUID() + prefix);
// MultipartFile to File
multipartFile.transferTo(file);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return file;
}
/**
* 获取文件扩展名,不带 .
*/
public static String getExtensionName(String filename) {
if ((filename != null) && (!filename.isEmpty())) {
int dot = filename.lastIndexOf('.');
if ((dot > -1) && (dot < (filename.length() - 1))) {
return filename.substring(dot + 1);
}
}
return filename;
}
/**
* Java文件操作 获取不带扩展名的文件名
*/
public static String getFileNameNoEx(String filename) {
if ((filename != null) && (!filename.isEmpty())) {
int dot = filename.lastIndexOf('.');
if (dot > -1) {
return filename.substring(0, dot);
}
}
return filename;
}
/**
* 文件大小转换
*/
public static String getSize(long size) {
String resultSize;
if (size / GB >= 1) {
//如果当前Byte的值大于等于1GB
resultSize = DF.format(size / (float) GB) + "GB ";
} else if (size / MB >= 1) {
//如果当前Byte的值大于等于1MB
resultSize = DF.format(size / (float) MB) + "MB ";
} else if (size / KB >= 1) {
//如果当前Byte的值大于等于1KB
resultSize = DF.format(size / (float) KB) + "KB ";
} else {
resultSize = size + "B ";
}
return resultSize;
}
/**
* inputStream 转 File
*/
static File inputStreamToFile(InputStream ins, String name){
File file = new File(SYS_TEM_DIR + name);
if (file.exists()) {
return file;
}
OutputStream os = null;
try {
os = Files.newOutputStream(file.toPath());
int bytesRead;
int len = 8192;
byte[] buffer = new byte[len];
while ((bytesRead = ins.read(buffer, 0, len)) != -1) {
os.write(buffer, 0, bytesRead);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
CloseUtil.close(os);
CloseUtil.close(ins);
}
return file;
}
/**
* 将文件名解析成文件的上传路径
*/
public static File upload(MultipartFile file, String filePath) {
Date date = new Date();
SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmssS");
// 过滤非法文件名
String name = getFileNameNoEx(verifyFilename(file.getOriginalFilename()));
String suffix = getExtensionName(file.getOriginalFilename());
String nowStr = "-" + format.format(date);
try {
String fileName = name + nowStr + "." + suffix;
String path = filePath + fileName;
// getCanonicalFile 可解析正确各种路径
File dest = new File(path).getCanonicalFile();
// 检测是否存在目录
if (!dest.getParentFile().exists()) {
if (!dest.getParentFile().mkdirs()) {
System.out.println("was not successful.");
}
}
// 文件写入
file.transferTo(dest);
return dest;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
/**
* 导出excel
*/
public static void downloadExcel(List<Map<String, Object>> list, HttpServletResponse response) throws IOException {
String tempPath = SYS_TEM_DIR + IdUtil.fastSimpleUUID() + ".xlsx";
File file = new File(tempPath);
BigExcelWriter writer = ExcelUtil.getBigWriter(file);
// 处理数据以防止CSV注入
List<Map<String, Object>> sanitizedList = list.parallelStream().map(map -> {
Map<String, Object> sanitizedMap = new LinkedHashMap<>();
map.forEach((key, value) -> {
if (value instanceof String) {
String strValue = (String) value;
// 检查并处理以特殊字符开头的值
if (strValue.startsWith("=") || strValue.startsWith("+") || strValue.startsWith("-") || strValue.startsWith("@")) {
strValue = "'" + strValue; // 添加单引号前缀
}
sanitizedMap.put(key, strValue);
} else {
sanitizedMap.put(key, value);
}
});
return sanitizedMap;
}).collect(Collectors.toList());
// 一次性写出内容,使用默认样式,强制输出标题
writer.write(sanitizedList, true);
SXSSFSheet sheet = (SXSSFSheet)writer.getSheet();
//上面需要强转SXSSFSheet 不然没有trackAllColumnsForAutoSizing方法
sheet.trackAllColumnsForAutoSizing();
//列宽自适应
writer.autoSizeColumnAll();
//response为HttpServletResponse对象
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
//test.xls是弹出下载对话框的文件名不能为中文中文请自行编码
response.setHeader("Content-Disposition", "attachment;filename=file.xlsx");
ServletOutputStream out = response.getOutputStream();
// 终止后删除临时文件
file.deleteOnExit();
writer.flush(out, true);
//此处记得关闭输出Servlet流
IoUtil.close(out);
}
public static String getFileType(String type) {
String documents = "txt doc pdf ppt pps xlsx xls docx";
String music = "mp3 wav wma mpa ram ra aac aif m4a";
String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg";
String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg";
if (image.contains(type)) {
return IMAGE;
} else if (documents.contains(type)) {
return TXT;
} else if (music.contains(type)) {
return MUSIC;
} else if (video.contains(type)) {
return VIDEO;
} else {
return OTHER;
}
}
public static void checkSize(long maxSize, long size) {
// 1M
int len = 1024 * 1024;
if (size > (maxSize * len)) {
throw new BadRequestException("文件超出规定大小:" + maxSize + "MB");
}
}
/**
* 判断两个文件是否相同
*/
public static boolean check(File file1, File file2) {
String img1Md5 = getMd5(file1);
String img2Md5 = getMd5(file2);
if(img1Md5 != null){
return img1Md5.equals(img2Md5);
}
return false;
}
/**
* 判断两个文件是否相同
*/
public static boolean check(String file1Md5, String file2Md5) {
return file1Md5.equals(file2Md5);
}
private static byte[] getByte(File file) {
// 得到文件长度
byte[] b = new byte[(int) file.length()];
InputStream in = null;
try {
in = Files.newInputStream(file.toPath());
try {
System.out.println(in.read(b));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
} finally {
CloseUtil.close(in);
}
return b;
}
private static String getMd5(byte[] bytes) {
// 16进制字符
char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
MessageDigest mdTemp = MessageDigest.getInstance("MD5");
mdTemp.update(bytes);
byte[] md = mdTemp.digest();
int j = md.length;
char[] str = new char[j * 2];
int k = 0;
// 移位 输出字符串
for (byte byte0 : md) {
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
/**
* 下载文件
*
* @param request /
* @param response /
* @param file /
*/
public static void downloadFile(HttpServletRequest request, HttpServletResponse response, File file, boolean deleteOnExit) {
response.setCharacterEncoding(request.getCharacterEncoding());
response.setContentType("application/octet-stream");
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName());
IOUtils.copy(fis, response.getOutputStream());
response.flushBuffer();
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
if (fis != null) {
try {
fis.close();
if (deleteOnExit) {
file.deleteOnExit();
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
}
/**
* 验证并过滤非法的文件名
* @param fileName 文件名
* @return 文件名
*/
public static String verifyFilename(String fileName) {
// 过滤掉特殊字符
fileName = fileName.replaceAll("[\\\\/:*?\"<>|~\\s]", "");
// 去掉文件名开头和结尾的空格和点
fileName = fileName.trim().replaceAll("^[. ]+|[. ]+$", "");
// 不允许文件名超过255在Mac和Linux中或260在Windows中个字符
int maxFileNameLength = 255;
if (System.getProperty("os.name").startsWith("Windows")) {
maxFileNameLength = 260;
}
if (fileName.length() > maxFileNameLength) {
fileName = fileName.substring(0, maxFileNameLength);
}
// 过滤掉控制字符
fileName = fileName.replaceAll("[\\p{Cntrl}]", "");
// 过滤掉 ".." 路径
fileName = fileName.replaceAll("\\.{2,}", "");
// 去掉文件名开头的 ".."
fileName = fileName.replaceAll("^\\.+/", "");
// 保留文件名中最后一个 "." 字符,过滤掉其他 "."
fileName = fileName.replaceAll("^(.*)(\\.[^.]*)$", "$1").replaceAll("\\.", "") +
fileName.replaceAll("^(.*)(\\.[^.]*)$", "$2");
return fileName;
}
public static String getMd5(File file) {
return getMd5(getByte(file));
}
}

View File

@ -118,12 +118,12 @@
<version>3.3.1</version>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<version>1.4</version>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.54</version>
</dependency>
</dependencies>

View File

@ -1,23 +1,33 @@
package com.fuyuanshen.equipment.controller;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fuyuanshen.common.core.constant.ResponseMessageConstants;
import com.fuyuanshen.common.core.domain.ResponseVO;
import com.fuyuanshen.common.core.domain.model.LoginUser;
import com.fuyuanshen.common.core.utils.file.FileUtil;
import com.fuyuanshen.common.mybatis.core.page.TableDataInfo;
import com.fuyuanshen.common.satoken.utils.LoginHelper;
import com.fuyuanshen.customer.mapper.CustomerMapper;
import com.fuyuanshen.equipment.domain.Device;
import com.fuyuanshen.equipment.domain.dto.DeviceExcelImportDTO;
import com.fuyuanshen.equipment.domain.dto.ImportResult;
import com.fuyuanshen.equipment.domain.form.DeviceForm;
import com.fuyuanshen.equipment.domain.query.DeviceQueryCriteria;
import com.fuyuanshen.equipment.domain.vo.CustomerVo;
import com.fuyuanshen.equipment.excel.DeviceImportParams;
import com.fuyuanshen.equipment.excel.UploadDeviceDataListener;
import com.fuyuanshen.equipment.mapper.DeviceMapper;
import com.fuyuanshen.equipment.mapper.DeviceTypeMapper;
import com.fuyuanshen.equipment.service.DeviceService;
import com.fuyuanshen.equipment.service.impl.DeviceExportService;
import com.fuyuanshen.system.domain.vo.SysOssVo;
import com.fuyuanshen.system.service.ISysOssService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.undertow.util.BadRequestException;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -42,6 +52,9 @@ public class DeviceController {
private final ISysOssService ossService;
private final DeviceService deviceService;
private final DeviceMapper deviceMapper;
private final CustomerMapper customerMapper;
private final DeviceTypeMapper deviceTypeMapper;
private final DeviceExportService exportService;
@ -156,36 +169,34 @@ public class DeviceController {
@Operation(summary = "导入设备数据")
@PostMapping(value = "/import", consumes = "multipart/form-data")
public ResponseVO<ImportResult> importData(@Parameter(name = "文件", required = true) @RequestPart("file") MultipartFile file) {
public ResponseVO<ImportResult> importData(@Parameter(name = "文件", required = true) @RequestPart("file") MultipartFile file) throws BadRequestException {
SysOssVo upload = ossService.upload(file);
String suffix = FileUtil.getExtensionName(file.getOriginalFilename());
if (!("xlsx".equalsIgnoreCase(suffix))) {
throw new BadRequestException("只能上传Excel——xlsx格式文件");
}
// String suffix = FileUtil.getExtensionName(file.getOriginalFilename());
// if (!("xlsx".equalsIgnoreCase(suffix))) {
// throw new BadRequestException("只能上传Excel——xlsx格式文件");
// }
//
// ImportResult result = new ImportResult();
// try {
// User currentUser = userMapper.findByUsername(SecurityUtils.getCurrentUsername());
// DeviceImportParams params = DeviceImportParams.builder().ip(ip).deviceService(deviceService).tenantId(currentUser.getTenantId()).file(file).filePath(filePath).deviceMapper(deviceMapper).deviceAssignmentsService(deviceAssignmentsService).deviceTypeMapper(deviceTypeMapper).userId(currentUser.getId()).userMapper(userMapper).build();
// // 创建监听器
// UploadDeviceDataListener listener = new UploadDeviceDataListener(params);
// // 读取Excel
// EasyExcel.read(file.getInputStream(), DeviceExcelImportDTO.class, listener).sheet().doRead();
// // 获取导入结果
// result = listener.getImportResult();
// // 设置响应消息
// String message = String.format("成功导入 %d 条数据,失败 %d 条", result.getSuccessCount(), result.getFailureCount());
// // 返回带有正确泛型的响应
// return ResponseVO.<ImportResult>success(message, result);
// } catch (Exception e) {
// log.error("导入设备数据出错: {}", e.getMessage(), e);
// // 在异常情况下,设置默认结果
// String errorMessage = String.format("导入失败: %s。成功 %d 条,失败 %d 条", e.getMessage(), result.getSuccessCount(), result.getFailureCount());
// // 使用新方法确保类型正确
// return ResponseVO.<ImportResult>fail(errorMessage, result);
return null;
ImportResult result = new ImportResult();
try {
LoginUser loginUser = LoginHelper.getLoginUser();
DeviceImportParams params = DeviceImportParams.builder().ossService(ossService).deviceService(deviceService).tenantId(loginUser.getTenantId()).file(file).filePath("").deviceMapper(deviceMapper).deviceTypeMapper(deviceTypeMapper).userId(loginUser.getUserId()).customerMapper(customerMapper).build();
// 创建监听器
UploadDeviceDataListener listener = new UploadDeviceDataListener(params);
// 读取Excel
EasyExcel.read(file.getInputStream(), DeviceExcelImportDTO.class, listener).sheet().doRead();
// 获取导入结果
result = listener.getImportResult();
// 设置响应消息
String message = String.format("成功导入 %d 条数据,失败 %d 条", result.getSuccessCount(), result.getFailureCount());
// 返回带有正确泛型的响应
return ResponseVO.<ImportResult>success(message, result);
} catch (Exception e) {
log.error("导入设备数据出错: {}", e.getMessage(), e);
// 在异常情况下,设置默认结果
String errorMessage = String.format("导入失败: %s。成功 %d 条,失败 %d 条", e.getMessage(), result.getSuccessCount(), result.getFailureCount());
// 使用新方法确保类型正确
return ResponseVO.<ImportResult>fail(errorMessage, result);
}
}
}

View File

@ -60,7 +60,7 @@ public class DeviceQueryCriteria {
private Long currentOwnerId;
@Schema(name = "租户ID")
private Long tenantId;
private String tenantId;
@Schema(name = "通讯方式", example = "0:4G;1:蓝牙")
private Integer communicationMode;

View File

@ -0,0 +1,34 @@
package com.fuyuanshen.equipment.excel;
import com.fuyuanshen.customer.mapper.CustomerMapper;
import com.fuyuanshen.equipment.mapper.DeviceMapper;
import com.fuyuanshen.equipment.mapper.DeviceTypeMapper;
import com.fuyuanshen.equipment.service.DeviceService;
import com.fuyuanshen.system.service.ISysOssService;
import lombok.*;
import org.springframework.web.multipart.MultipartFile;
/**
* @author: 默苍璃
* @date: 2025-06-1414:56
*/
@AllArgsConstructor
@NoArgsConstructor(force = true)
@Getter
@Setter
@Builder
public class DeviceImportParams {
private DeviceService deviceService;
private DeviceMapper deviceMapper;
private CustomerMapper customerMapper;
private DeviceTypeMapper deviceTypeMapper;
private ISysOssService ossService;
private MultipartFile file;
private String filePath;
private String ip;
private String username;
private Long userId;
private String tenantId;
}

View File

@ -0,0 +1,260 @@
package com.fuyuanshen.equipment.excel;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fuyuanshen.common.core.domain.model.LoginUser;
import com.fuyuanshen.common.satoken.utils.LoginHelper;
import com.fuyuanshen.equipment.constants.DeviceConstants;
import com.fuyuanshen.equipment.domain.Device;
import com.fuyuanshen.equipment.domain.DeviceType;
import com.fuyuanshen.equipment.domain.dto.DeviceExcelImportDTO;
import com.fuyuanshen.equipment.domain.dto.ImportResult;
import com.fuyuanshen.equipment.domain.query.DeviceQueryCriteria;
import com.fuyuanshen.equipment.handler.ImageWriteHandler;
import com.fuyuanshen.system.domain.vo.SysOssVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.util.ZipSecureFile;
import org.apache.poi.xssf.usermodel.*;
import java.io.File;
import java.util.*;
@Slf4j
public class UploadDeviceDataListener implements ReadListener<DeviceExcelImportDTO> {
// 存储图片数据的映射
private final Map<Integer, byte[]> rowImageMap = new HashMap<>();
private final DeviceImportParams params;
private final Map<Integer, Device> rowDeviceMap = new HashMap<>();
private final Map<Integer, DeviceExcelImportDTO> rowDtoMap = new HashMap<>();
private final List<Integer> rowIndexList = new ArrayList<>();
private int successCount = 0;
private int failureCount = 0;
private final List<DeviceExcelImportDTO> failedRecords = new ArrayList<>();
public UploadDeviceDataListener(DeviceImportParams params) {
this.params = params;
}
public ImportResult getImportResult() {
ImportResult result = new ImportResult();
result.setSuccessCount(successCount);
result.setFailureCount(failureCount);
// 准备失败记录(包含图片数据)
List<DeviceExcelImportDTO> failedRecordsWithImages = new ArrayList<>();
for (DeviceExcelImportDTO failedRecord : failedRecords) {
// 获取原始行号
Integer rowIndex = null;
for (Map.Entry<Integer, DeviceExcelImportDTO> entry : rowDtoMap.entrySet()) {
if (entry.getValue() == failedRecord) {
rowIndex = entry.getKey();
break;
}
}
if (rowIndex != null) {
// 创建副本,避免修改原始数据
DeviceExcelImportDTO recordWithImage = new DeviceExcelImportDTO();
BeanUtil.copyProperties(failedRecord, recordWithImage);
// 设置图片数据
byte[] imageData = rowImageMap.get(rowIndex);
if (imageData != null) {
recordWithImage.setDevicePicFromBytes(imageData);
}
failedRecordsWithImages.add(recordWithImage);
} else {
failedRecordsWithImages.add(failedRecord);
}
}
result.setFailedRecords(failedRecordsWithImages);
// 生成错误报告
if (!failedRecordsWithImages.isEmpty()) {
try {
// 生成唯一的文件名
String fileName = "import_errors_" + System.currentTimeMillis() + ".xlsx";
// 创建错误报告目录
String errorDirPath = params.getFilePath() + DeviceConstants.ERROR_REPORT_DIR;
File errorDir = new File(errorDirPath);
if (!errorDir.exists() && !errorDir.mkdirs()) {
log.error("无法创建错误报告目录: {}", errorDirPath);
return result;
}
// 保存错误报告到文件(包含图片)
File errorFile = new File(errorDir, fileName);
EasyExcel.write(errorFile, DeviceExcelImportDTO.class).registerWriteHandler(new ImageWriteHandler()) // 添加图片处理
.sheet("失败数据").doWrite(failedRecordsWithImages);
// 生成访问URL
// String errorExcelUrl = params.getIp() + DeviceConstants.FILE_ACCESS_PREFIX + "/" + DeviceConstants.ERROR_REPORT_DIR + "/" + fileName;
SysOssVo upload = params.getOssService().upload(errorFile);
result.setErrorExcelUrl(upload.getUrl());
log.info("错误报告已保存: {}", errorFile.getAbsolutePath());
} catch (Exception e) {
log.error("生成错误报告失败", e);
}
}
return result;
}
@Override
public void invoke(DeviceExcelImportDTO data, AnalysisContext context) {
log.info("解析到一条数据: {}", JSON.toJSONString(data));
int rowIndex = context.readRowHolder().getRowIndex();
Device device = new Device();
BeanUtil.copyProperties(data, device, true);
rowDeviceMap.put(rowIndex, device);
rowDtoMap.put(rowIndex, data);
rowIndexList.add(rowIndex);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
try {
processImages();
processDataRowByRow();
log.info("数据处理完成!成功:{}条,失败:{}条", successCount, failureCount);
} catch (Exception e) {
log.error("数据处理失败:{}", e.getMessage(), e);
throw new RuntimeException("导入失败", e);
}
}
private void processDataRowByRow() {
LoginUser loginUser = LoginHelper.getLoginUser();
for (Integer rowIndex : rowIndexList) {
Device device = rowDeviceMap.get(rowIndex);
DeviceExcelImportDTO originalDto = rowDtoMap.get(rowIndex);
try {
DeviceQueryCriteria criteria = new DeviceQueryCriteria();
criteria.setDeviceMac(device.getDeviceMac());
criteria.setTenantId(params.getTenantId());
List<Device> deviceList = params.getDeviceMapper().findAll(criteria);
if (!deviceList.isEmpty()) {
throw new RuntimeException("设备MAC重复");
}
device.setTenantId(params.getTenantId());
// 设备类型
QueryWrapper<DeviceType> wrapper = new QueryWrapper<>();
wrapper.eq("type_name", device.getTypeName());
wrapper.eq("customer_id", params.getUserId());
List<DeviceType> deviceTypes = params.getDeviceTypeMapper().selectList(wrapper);
if (CollectionUtil.isNotEmpty(deviceTypes)) {
device.setDeviceType(deviceTypes.get(0).getId());
}
params.getDeviceService().save(device);
successCount++;
log.info("行 {} 数据插入成功", rowIndex);
} catch (Exception e) {
failureCount++;
failedRecords.add(originalDto);
log.error("行 {} 数据插入失败: {}", rowIndex, e.getMessage());
}
}
}
private void processImages() {
try (OPCPackage opcPackage = OPCPackage.open(params.getFile().getInputStream())) {
ZipSecureFile.setMinInflateRatio(-1.0d);
XSSFWorkbook workbook = new XSSFWorkbook(opcPackage);
XSSFSheet sheet = workbook.getSheetAt(0);
XSSFDrawing drawing = sheet.getDrawingPatriarch();
if (drawing == null) return;
for (XSSFShape shape : drawing.getShapes()) {
if (shape instanceof XSSFPicture) {
XSSFPicture picture = (XSSFPicture) shape;
XSSFClientAnchor anchor = picture.getPreferredSize();
int rowIndex = anchor.getRow1();
int colIndex = anchor.getCol1();
if (colIndex == 2) {
Device device = rowDeviceMap.get(rowIndex);
if (device != null) {
try {
byte[] imageData = picture.getPictureData().getData();
String extraValue = getCellValue(sheet, rowIndex, 4);
String imageUrl = uploadAndGenerateUrl(imageData, extraValue);
device.setDevicePic(imageUrl);
// 2. 保存图片数据到DTO用于错误报告
rowImageMap.put(rowIndex, imageData);
} catch (Exception e) {
log.error("行 {} 图片处理失败: {}", rowIndex, e.getMessage());
device.setDevicePic(null); // 设置为空,让插入继续
}
}
}
}
}
} catch (Exception e) {
log.error("图片处理失败:{}", e.getMessage(), e);
}
}
private String getCellValue(XSSFSheet sheet, int rowIndex, int colIndex) {
XSSFRow row = sheet.getRow(rowIndex);
if (row == null) return null;
XSSFCell cell = row.getCell(colIndex);
if (cell == null) return null;
return cell.toString();
}
private String uploadAndGenerateUrl(byte[] imageData, String deviceMac) {
if (imageData == null || imageData.length == 0) {
log.warn("图片数据为空");
return null;
}
try {
String fileExtension = "jpg";
String newFileName = "PS_" + new Random(8) + "." + fileExtension;
// String targetDirPath = params.getFilePath() + DeviceConstants.FILE_ACCESS_ISOLATION;
// File targetDir = new File(targetDirPath);
//
// if (!targetDir.exists() && !targetDir.mkdirs()) {
// log.error("无法创建目录: {}", targetDirPath);
// return null;
// }
//
// File newFile = new File(targetDir, newFileName);
// Files.write(newFile.toPath(), imageData);
//
// return params.getIp() + DeviceConstants.FILE_ACCESS_PREFIX + "/" + DeviceConstants.FILE_ACCESS_ISOLATION + "/" + newFileName;
SysOssVo upload = params.getOssService().upload(imageData, newFileName);
return upload.getUrl();
} catch (Exception e) {
log.error("保存图片失败", e);
return null;
}
}
}

View File

@ -0,0 +1,69 @@
package com.fuyuanshen.equipment.handler;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFDrawing;
import org.apache.poi.xssf.usermodel.XSSFSheet;
/**
* @author: 默苍璃
* @date: 2025-06-0718:05
*/
public class ImageWriteHandler implements SheetWriteHandler {
@Override
public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
// 不需要实现
}
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
Workbook workbook = writeWorkbookHolder.getWorkbook();
Sheet sheet = writeSheetHolder.getSheet();
// 获取设备图片列索引假设是第4列索引3
int imageColIndex = 3;
// 遍历所有行
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) { // 从第2行开始跳过标题
Row row = sheet.getRow(rowIndex);
if (row == null) continue;
Cell imageCell = row.getCell(imageColIndex);
if (imageCell == null) continue;
// 获取图片数据
byte[] imageData = null;
if (imageCell.getCellType() == CellType.STRING) {
// 处理Base64编码的图片如果需要
}
if (imageData != null && imageData.length > 0) {
try {
// 添加图片到工作表
int pictureIdx = workbook.addPicture(imageData, Workbook.PICTURE_TYPE_JPEG);
// 创建绘图对象
if (sheet instanceof XSSFSheet) {
XSSFSheet xssfSheet = (XSSFSheet) sheet;
XSSFDrawing drawing = xssfSheet.createDrawingPatriarch();
// 设置图片位置
XSSFClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, imageColIndex, rowIndex, imageColIndex + 1, rowIndex + 1);
// 创建图片
drawing.createPicture(anchor, pictureIdx);
}
// 清除单元格内容
imageCell.setBlank();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}

View File

@ -122,15 +122,18 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
// 保存图片并获取URL
// String imageUrl = saveDeviceImage(deviceForm.getFile(), deviceForm.getDeviceName());
SysOssVo upload = ossService.upload(deviceForm.getFile());
// 设置图片路径
deviceForm.setDevicePic(upload.getUrl());
if (deviceForm.getFile() != null) {
SysOssVo upload = ossService.upload(deviceForm.getFile());
// 设置图片路径
deviceForm.setDevicePic(upload.getUrl());
}
// 转换对象并插入数据库
Device device = new Device();
LoginUser loginUser = LoginHelper.getLoginUser();
device.setCurrentOwnerId(loginUser.getUserId());
device.setCreateByName(loginUser.getNickname());
device.setTypeName(deviceTypes.get(0).getTypeName());
BeanUtil.copyProperties(deviceForm, device, true);
deviceMapper.insert(device);

View File

@ -60,6 +60,9 @@ public interface ISysOssService {
*/
SysOssVo upload(File file);
public SysOssVo upload(byte[] data, String fileName);
/**
* 文件下载方法,支持一次性下载完整文件
*

View File

@ -219,6 +219,36 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
}
/**
* 上传二进制数据到对象存储服务,并保存文件信息到数据库
*
* @param data 要上传的二进制数据
* @param fileName 要上传的文件名(包括扩展名)
* @return 上传成功后的 SysOssVo 对象,包含文件信息
* @throws ServiceException 如果上传过程中发生异常,则抛出 ServiceException 异常
*/
@Override
public SysOssVo upload(byte[] data, String fileName) {
if (data == null || data.length == 0) {
throw new ServiceException("上传的数据为空");
}
if (StringUtils.isBlank(fileName)) {
throw new ServiceException("文件名不能为空");
}
String suffix = StringUtils.substring(fileName, fileName.lastIndexOf("."), fileName.length());
OssClient storage = OssFactory.instance();
UploadResult uploadResult;
uploadResult = storage.uploadSuffix(data, suffix, "image/jpeg"); // 假设是图片类型,可以根据实际需要修改
// 保存文件信息
return buildResultEntity(fileName, suffix, storage.getConfigKey(), uploadResult);
}
@NotNull
private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey, UploadResult uploadResult) {
SysOss oss = new SysOss();