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/StringUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/StringUtils.java index a27ab625f..9ff317c4a 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/StringUtils.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/StringUtils.java @@ -1,8 +1,11 @@ package com.ruoyi.common.core.utils; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.springframework.util.AntPathMatcher; import com.ruoyi.common.core.constant.Constants; import com.ruoyi.common.core.text.StrFormatter; @@ -366,6 +369,69 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS); } + /** + * 字符串转set + * + * @param str 字符串 + * @param sep 分隔符 + * @return set集合 + */ + public static final Set str2Set(String str, String sep) + { + return new HashSet(str2List(str, sep, true, false)); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @return list集合 + */ + public static final List str2List(String str, String sep) + { + return str2List(str, sep, true, false); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @param filterBlank 过滤纯空白 + * @param trim 去掉首尾空白 + * @return list集合 + */ + public static final List str2List(String str, String sep, boolean filterBlank, boolean trim) + { + List list = new ArrayList(); + if (StringUtils.isEmpty(str)) + { + return list; + } + + // 过滤空白字符串 + if (filterBlank && StringUtils.isBlank(str)) + { + return list; + } + String[] split = str.split(sep); + for (String string : split) + { + if (filterBlank && StringUtils.isBlank(string)) + { + continue; + } + if (trim) + { + string = string.trim(); + } + list.add(string); + } + + return list; + } + /** * 判断给定的collection列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value * 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 6d062b182..82750832d 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 javax.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); } } @@ -487,7 +542,7 @@ public class ExcelUtil /** * 对list数据源将其里面的数据导入到excel表单 - * + * * @param response 返回数据 * @param list 导出数据集合 * @param sheetName 工作表的名称 @@ -591,7 +646,7 @@ public class ExcelUtil /** * 对list数据源将其里面的数据导入到excel表单 - * + * * @param sheetName 工作表的名称 * @param title 标题 * @return 结果 @@ -985,12 +1040,15 @@ public class ExcelUtil else if (ColumnType.IMAGE == attr.cellType()) { ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1); - String imagePath = Convert.toStr(value); - if (StringUtils.isNotEmpty(imagePath)) + String propertyValue = Convert.toStr(value); + if (StringUtils.isNotEmpty(propertyValue)) { - byte[] data = ImageUtils.getImage(imagePath); - getDrawingPatriarch(cell.getSheet()).createPicture(anchor, - cell.getSheet().getWorkbook().addPicture(data, getImageType(data))); + List imagePaths = StringUtils.str2List(propertyValue, SEPARATOR); + for (String imagePath : imagePaths) + { + byte[] data = ImageUtils.getImage(imagePath); + getDrawingPatriarch(cell.getSheet()).createPicture(anchor, cell.getSheet().getWorkbook().addPicture(data, getImageType(data))); + } } } } @@ -1024,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; + } + /** * 创建表格样式 */ @@ -1419,7 +1562,7 @@ public class ExcelUtil /** * 以类的属性的get方法方法形式获取值 - * + * * @param o * @param name * @return value