diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/config/RemoteFileAutoConfiguration.java b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/config/RemoteFileAutoConfiguration.java new file mode 100644 index 000000000..a6ca2d6ee --- /dev/null +++ b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/config/RemoteFileAutoConfiguration.java @@ -0,0 +1,24 @@ +package com.ruoyi.system.api.config; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import com.ruoyi.common.core.utils.poi.ExcelImageHandler; +import com.ruoyi.system.api.RemoteFileService; +import com.ruoyi.system.api.handler.RemoteExcelImageHandler; + +/** + * Excel图片导入处理器 + * + * @author ruoyi + */ +@AutoConfiguration +@ConditionalOnBean(RemoteFileService.class) +public class RemoteFileAutoConfiguration +{ + @Bean + public ExcelImageHandler excelImageHandler(RemoteFileService remoteFileService) + { + return new RemoteExcelImageHandler(remoteFileService); + } +} diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/handler/RemoteExcelImageHandler.java b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/handler/RemoteExcelImageHandler.java new file mode 100644 index 000000000..ca7942a29 --- /dev/null +++ b/ruoyi-api/ruoyi-api-system/src/main/java/com/ruoyi/system/api/handler/RemoteExcelImageHandler.java @@ -0,0 +1,128 @@ +package com.ruoyi.system.api.handler; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.exception.ServiceException; +import com.ruoyi.common.core.utils.poi.ExcelImageHandler; +import com.ruoyi.system.api.RemoteFileService; +import com.ruoyi.system.api.domain.SysFile; + +/** + * Excel图片导入处理器 + * + * 将Excel中内嵌的图片字节封装为 {@link MultipartFile},通过 Feign 调用 ruoyi-file + * 服务的 /upload 接口上传,返回文件服务生成的图片访问地址,最终写入实体的图片字段 + * + * @author ruoyi + */ +public class RemoteExcelImageHandler implements ExcelImageHandler +{ + private static final Logger log = LoggerFactory.getLogger(RemoteExcelImageHandler.class); + + private final RemoteFileService remoteFileService; + + public RemoteExcelImageHandler(RemoteFileService remoteFileService) + { + this.remoteFileService = remoteFileService; + } + + @Override + public String upload(byte[] data, String extension) + { + String fileName = "excel_image" + "." + extension; + MultipartFile file = new InMemoryMultipartFile(fileName, data, "image/" + extension); + R result; + try + { + result = remoteFileService.upload(file); + } + catch (Exception e) + { + log.error("Excel图片上传调用文件服务异常", e); + throw new ServiceException("图片上传失败:文件服务调用异常"); + } + if (result == null || result.getCode() != R.SUCCESS || result.getData() == null) + { + String msg = result == null ? "文件服务无响应" : result.getMsg(); + throw new ServiceException("图片上传失败:" + msg); + } + return result.getData().getUrl(); + } + + /** + * 基于字节数组的内存MultipartFile实现,用于Feign以multipart/form-data方式上传字节数据 + */ + public static class InMemoryMultipartFile implements MultipartFile + { + /** 表单参数名,与 ruoyi-file 上传接口的参数名保持一致 */ + private final String name = "file"; + + private final String originalFilename; + + private final String contentType; + + private final byte[] content; + + public InMemoryMultipartFile(String originalFilename, byte[] content, String contentType) + { + this.originalFilename = originalFilename; + this.content = content; + this.contentType = contentType; + } + + @Override + public String getName() + { + return name; + } + + @Override + public String getOriginalFilename() + { + return originalFilename; + } + + @Override + public String getContentType() + { + return contentType; + } + + @Override + public boolean isEmpty() + { + return content == null || content.length == 0; + } + + @Override + public long getSize() + { + return content.length; + } + + @Override + public byte[] getBytes() throws IOException + { + return content; + } + + @Override + public InputStream getInputStream() throws IOException + { + return new ByteArrayInputStream(content); + } + + @Override + public void transferTo(File dest) throws IOException, IllegalStateException + { + Files.copy(getInputStream(), dest.toPath()); + } + } +} diff --git a/ruoyi-api/ruoyi-api-system/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-api/ruoyi-api-system/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 9212ec047..8fffc241a 100644 --- a/ruoyi-api/ruoyi-api-system/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/ruoyi-api/ruoyi-api-system/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,4 @@ com.ruoyi.system.api.factory.RemoteUserFallbackFactory com.ruoyi.system.api.factory.RemoteLogFallbackFactory com.ruoyi.system.api.factory.RemoteFileFallbackFactory +com.ruoyi.system.api.config.RemoteFileAutoConfiguration diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/poi/ExcelImageHandler.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/poi/ExcelImageHandler.java new file mode 100644 index 000000000..f171732a6 --- /dev/null +++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/poi/ExcelImageHandler.java @@ -0,0 +1,19 @@ +package com.ruoyi.common.core.utils.poi; + +/** + * Excel导入图片处理器 + * 文件存储由独立的 ruoyi-file 服务提供 + * + * @author ruoyi + */ +public interface ExcelImageHandler +{ + /** + * 上传Excel中内嵌的图片,返回图片访问地址 + * + * @param data 图片字节数组 + * @param extension 图片扩展名(不含点,如 jpg、png、gif、bmp) + * @return 图片上传后的访问地址 + */ + String upload(byte[] data, String extension); +} diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/poi/ExcelUtil.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/poi/ExcelUtil.java index 25ecf2564..b6b8df08e 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/poi/ExcelUtil.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/poi/ExcelUtil.java @@ -23,6 +23,13 @@ import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.RegExUtils; import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.poi.hssf.usermodel.HSSFClientAnchor; +import org.apache.poi.hssf.usermodel.HSSFPicture; +import org.apache.poi.hssf.usermodel.HSSFPictureData; +import org.apache.poi.hssf.usermodel.HSSFShape; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ooxml.POIXMLDocumentPart; import org.apache.poi.ss.usermodel.BorderStyle; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; @@ -39,6 +46,7 @@ import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.HorizontalAlignment; import org.apache.poi.ss.usermodel.IndexedColors; import org.apache.poi.ss.usermodel.Name; +import org.apache.poi.ss.usermodel.PictureData; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.VerticalAlignment; @@ -50,6 +58,12 @@ import org.apache.poi.util.IOUtils; import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFClientAnchor; import org.apache.poi.xssf.usermodel.XSSFDataValidation; +import org.apache.poi.xssf.usermodel.XSSFDrawing; +import org.apache.poi.xssf.usermodel.XSSFPicture; +import org.apache.poi.xssf.usermodel.XSSFShape; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTMarker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.ruoyi.common.core.annotation.Excel; @@ -59,6 +73,7 @@ import com.ruoyi.common.core.annotation.Excels; import com.ruoyi.common.core.exception.UtilException; import com.ruoyi.common.core.text.Convert; import com.ruoyi.common.core.utils.DateUtils; +import com.ruoyi.common.core.utils.SpringUtils; import com.ruoyi.common.core.utils.StringUtils; import com.ruoyi.common.core.utils.file.FileTypeUtils; import com.ruoyi.common.core.utils.file.ImageUtils; @@ -179,11 +194,36 @@ public class ExcelUtil */ public String[] excludeFields; + /** + * 图片导入处理器(用于把Excel内嵌图片上传到文件服务,返回访问地址) + */ + private ExcelImageHandler imageHandler; + public ExcelUtil(Class clazz) { this.clazz = clazz; } + /** + * 获取图片导入处理器 + * + * @return 图片导入处理器 + */ + private ExcelImageHandler getImageHandler() + { + if (imageHandler == null) + { + try + { + imageHandler = SpringUtils.getBean(ExcelImageHandler.class); + } + catch (Exception e) + { + } + } + return imageHandler; + } + /** * 仅在Excel中显示列属性 * @@ -339,6 +379,17 @@ public class ExcelUtil throw new IOException("文件sheet不存在"); } + boolean isXSSFWorkbook = !(wb instanceof HSSFWorkbook); + Map> pictures = null; + if (isXSSFWorkbook) + { + pictures = getSheetPictures07((XSSFSheet) sheet, (XSSFWorkbook) wb); + } + else + { + pictures = getSheetPictures03((HSSFSheet) sheet, (HSSFWorkbook) wb); + } + // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1 int rows = sheet.getLastRowNum(); if (rows > 0) @@ -463,6 +514,10 @@ public class ExcelUtil { val = dataFormatHandlerAdapter(val, attr, null); } + else if (ColumnType.IMAGE == attr.cellType()) + { + val = getImportImageValue(row, entry.getKey(), pictures, val); + } ReflectUtils.invokeSetter(entity, propertyName, val); } } @@ -1027,6 +1082,91 @@ public class ExcelUtil return Workbook.PICTURE_TYPE_JPEG; } + /** + * 获取导入单元格中的内嵌图片 + * + * @param row 当前行 + * @param column 当前列号 + * @param pictures 工作表图片映射(key: 行号_列号) + * @param defaultVal 单元格原有值 + * @return 图片访问地址或单元格原值 + */ + private Object getImportImageValue(Row row, int column, Map> pictures, Object defaultVal) + { + List images = pictures == null ? null : pictures.get(row.getRowNum() + "_" + column); + if (images == null || images.isEmpty()) + { + return defaultVal; + } + ExcelImageHandler imageHandler = getImageHandler(); + StringBuilder propertyString = new StringBuilder(); + for (PictureData picture : images) + { + byte[] data = picture.getData(); + String extension = FileTypeUtils.getFileExtendName(data).toLowerCase(); + propertyString.append(imageHandler.upload(data, extension)).append(SEPARATOR); + } + return StringUtils.stripEnd(propertyString.toString(), SEPARATOR); + } + + /** + * 获取Excel2003图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map> getSheetPictures03(HSSFSheet sheet, HSSFWorkbook workbook) + { + Map> sheetIndexPicMap = new HashMap<>(); + List pictures = workbook.getAllPictures(); + if (!pictures.isEmpty() && sheet.getDrawingPatriarch() != null) + { + for (HSSFShape shape : sheet.getDrawingPatriarch().getChildren()) + { + if (shape instanceof HSSFPicture) + { + HSSFPicture pic = (HSSFPicture) shape; + HSSFClientAnchor anchor = (HSSFClientAnchor) pic.getAnchor(); + String picIndex = anchor.getRow1() + "_" + anchor.getCol1(); + sheetIndexPicMap.computeIfAbsent(picIndex, k -> new ArrayList<>()).add(pic.getPictureData()); + } + } + } + return sheetIndexPicMap; + } + + /** + * 获取Excel2007图片 + * + * @param sheet 当前sheet对象 + * @param workbook 工作簿对象 + * @return Map key:图片单元格索引(1_1)String,value:图片流PictureData + */ + public static Map> getSheetPictures07(XSSFSheet sheet, XSSFWorkbook workbook) + { + Map> sheetIndexPicMap = new HashMap<>(); + for (POIXMLDocumentPart dr : sheet.getRelations()) + { + if (dr instanceof XSSFDrawing) + { + XSSFDrawing drawing = (XSSFDrawing) dr; + for (XSSFShape shape : drawing.getShapes()) + { + if (shape instanceof XSSFPicture) + { + XSSFPicture pic = (XSSFPicture) shape; + XSSFClientAnchor anchor = pic.getPreferredSize(); + CTMarker ctMarker = anchor.getFrom(); + String picIndex = ctMarker.getRow() + "_" + ctMarker.getCol(); + sheetIndexPicMap.computeIfAbsent(picIndex, k -> new ArrayList<>()).add(pic.getPictureData()); + } + } + } + } + return sheetIndexPicMap; + } + /** * 创建表格样式 */