Merge remote-tracking branch 'origin/dyf-device'

This commit is contained in:
2025-07-08 11:26:24 +08:00
17 changed files with 1106 additions and 38 deletions

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

@ -131,15 +131,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();