forked from dyf/fys-Multi-tenant
fys
This commit is contained in:
30
fys-common/fys-common-excel/pom.xml
Normal file
30
fys-common/fys-common-excel/pom.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>com.fuyuanshen</groupId>
|
||||
<artifactId>fys-common</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>fys-common-excel</artifactId>
|
||||
|
||||
<description>
|
||||
fys-common-excel
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fuyuanshen</groupId>
|
||||
<artifactId>fys-common-json</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.idev.excel</groupId>
|
||||
<artifactId>fastexcel</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,29 @@
|
||||
package com.fuyuanshen.common.excel.annotation;
|
||||
|
||||
import com.fuyuanshen.common.excel.core.CellMergeStrategy;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* excel 列单元格合并(合并列相同项)
|
||||
*
|
||||
* 需搭配 {@link CellMergeStrategy} 策略使用
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
public @interface CellMerge {
|
||||
|
||||
/**
|
||||
* col index
|
||||
*/
|
||||
int index() default -1;
|
||||
|
||||
/**
|
||||
* 合并需要依赖的其他字段名称
|
||||
*/
|
||||
String[] mergeBy() default {};
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package com.fuyuanshen.common.excel.annotation;
|
||||
|
||||
import com.fuyuanshen.common.core.utils.StringUtils;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 字典格式化
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
public @interface ExcelDictFormat {
|
||||
|
||||
/**
|
||||
* 如果是字典类型,请设置字典的type值 (如: sys_user_sex)
|
||||
*/
|
||||
String dictType() default "";
|
||||
|
||||
/**
|
||||
* 读取内容转表达式 (如: 0=男,1=女,2=未知)
|
||||
*/
|
||||
String readConverterExp() default "";
|
||||
|
||||
/**
|
||||
* 分隔符,读取字符串组内容
|
||||
*/
|
||||
String separator() default StringUtils.SEPARATOR;
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package com.fuyuanshen.common.excel.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 枚举格式化
|
||||
*
|
||||
* @author Liang
|
||||
*/
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
public @interface ExcelEnumFormat {
|
||||
|
||||
/**
|
||||
* 字典枚举类型
|
||||
*/
|
||||
Class<? extends Enum<?>> enumClass();
|
||||
|
||||
/**
|
||||
* 字典枚举类中对应的code属性名称,默认为code
|
||||
*/
|
||||
String codeField() default "code";
|
||||
|
||||
/**
|
||||
* 字典枚举类中对应的text属性名称,默认为text
|
||||
*/
|
||||
String textField() default "text";
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.fuyuanshen.common.excel.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 批注
|
||||
* @author guzhouyanyu
|
||||
*/
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ExcelNotation {
|
||||
|
||||
/**
|
||||
* col index
|
||||
*/
|
||||
int index() default -1;
|
||||
/**
|
||||
* 批注内容
|
||||
*/
|
||||
String value() default "";
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.fuyuanshen.common.excel.annotation;
|
||||
|
||||
import org.apache.poi.ss.usermodel.IndexedColors;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 是否必填
|
||||
* @author guzhouyanyu
|
||||
*/
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ExcelRequired {
|
||||
|
||||
/**
|
||||
* col index
|
||||
*/
|
||||
int index() default -1;
|
||||
/**
|
||||
* 字体颜色
|
||||
*/
|
||||
IndexedColors fontColor() default IndexedColors.RED;
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package com.fuyuanshen.common.excel.convert;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.idev.excel.converters.Converter;
|
||||
import cn.idev.excel.enums.CellDataTypeEnum;
|
||||
import cn.idev.excel.metadata.GlobalConfiguration;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 大数值转换
|
||||
* Excel 数值长度位15位 大于15位的数值转换位字符串
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
public class ExcelBigNumberConvert implements Converter<Long> {
|
||||
|
||||
@Override
|
||||
public Class<Long> supportJavaTypeKey() {
|
||||
return Long.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
return CellDataTypeEnum.STRING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
return Convert.toLong(cellData.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteCellData<Object> convertToExcelData(Long object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
if (ObjectUtil.isNotNull(object)) {
|
||||
String str = Convert.toStr(object);
|
||||
if (str.length() > 15) {
|
||||
return new WriteCellData<>(str);
|
||||
}
|
||||
}
|
||||
WriteCellData<Object> cellData = new WriteCellData<>(new BigDecimal(object));
|
||||
cellData.setType(CellDataTypeEnum.NUMBER);
|
||||
return cellData;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package com.fuyuanshen.common.excel.convert;
|
||||
|
||||
import cn.hutool.core.annotation.AnnotationUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.idev.excel.converters.Converter;
|
||||
import cn.idev.excel.enums.CellDataTypeEnum;
|
||||
import cn.idev.excel.metadata.GlobalConfiguration;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import com.fuyuanshen.common.excel.annotation.ExcelDictFormat;
|
||||
import com.fuyuanshen.common.core.service.DictService;
|
||||
import com.fuyuanshen.common.core.utils.SpringUtils;
|
||||
import com.fuyuanshen.common.core.utils.StringUtils;
|
||||
import com.fuyuanshen.common.excel.utils.ExcelUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* 字典格式化转换处理
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
public class ExcelDictConvert implements Converter<Object> {
|
||||
|
||||
@Override
|
||||
public Class<Object> supportJavaTypeKey() {
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
ExcelDictFormat anno = getAnnotation(contentProperty.getField());
|
||||
String type = anno.dictType();
|
||||
String label = cellData.getStringValue();
|
||||
String value;
|
||||
if (StringUtils.isBlank(type)) {
|
||||
value = ExcelUtil.reverseByExp(label, anno.readConverterExp(), anno.separator());
|
||||
} else {
|
||||
value = SpringUtils.getBean(DictService.class).getDictValue(type, label, anno.separator());
|
||||
}
|
||||
return Convert.convert(contentProperty.getField().getType(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
if (ObjectUtil.isNull(object)) {
|
||||
return new WriteCellData<>("");
|
||||
}
|
||||
ExcelDictFormat anno = getAnnotation(contentProperty.getField());
|
||||
String type = anno.dictType();
|
||||
String value = Convert.toStr(object);
|
||||
String label;
|
||||
if (StringUtils.isBlank(type)) {
|
||||
label = ExcelUtil.convertByExp(value, anno.readConverterExp(), anno.separator());
|
||||
} else {
|
||||
label = SpringUtils.getBean(DictService.class).getDictLabel(type, value, anno.separator());
|
||||
}
|
||||
return new WriteCellData<>(label);
|
||||
}
|
||||
|
||||
private ExcelDictFormat getAnnotation(Field field) {
|
||||
return AnnotationUtil.getAnnotation(field, ExcelDictFormat.class);
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package com.fuyuanshen.common.excel.convert;
|
||||
|
||||
import cn.hutool.core.annotation.AnnotationUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.idev.excel.converters.Converter;
|
||||
import cn.idev.excel.enums.CellDataTypeEnum;
|
||||
import cn.idev.excel.metadata.GlobalConfiguration;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import com.fuyuanshen.common.core.utils.reflect.ReflectUtils;
|
||||
import com.fuyuanshen.common.excel.annotation.ExcelEnumFormat;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 枚举格式化转换处理
|
||||
*
|
||||
* @author Liang
|
||||
*/
|
||||
@Slf4j
|
||||
public class ExcelEnumConvert implements Converter<Object> {
|
||||
|
||||
@Override
|
||||
public Class<Object> supportJavaTypeKey() {
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
cellData.checkEmpty();
|
||||
// Excel中填入的是枚举中指定的描述
|
||||
Object textValue = switch (cellData.getType()) {
|
||||
case STRING, DIRECT_STRING, RICH_TEXT_STRING -> cellData.getStringValue();
|
||||
case NUMBER -> cellData.getNumberValue();
|
||||
case BOOLEAN -> cellData.getBooleanValue();
|
||||
default -> throw new IllegalArgumentException("单元格类型异常!");
|
||||
};
|
||||
// 如果是空值
|
||||
if (ObjectUtil.isNull(textValue)) {
|
||||
return null;
|
||||
}
|
||||
Map<Object, String> enumCodeToTextMap = beforeConvert(contentProperty);
|
||||
// 从Java输出至Excel是code转text
|
||||
// 因此从Excel转Java应该将text与code对调
|
||||
Map<Object, Object> enumTextToCodeMap = new HashMap<>();
|
||||
enumCodeToTextMap.forEach((key, value) -> enumTextToCodeMap.put(value, key));
|
||||
// 应该从text -> code中查找
|
||||
Object codeValue = enumTextToCodeMap.get(textValue);
|
||||
return Convert.convert(contentProperty.getField().getType(), codeValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
if (ObjectUtil.isNull(object)) {
|
||||
return new WriteCellData<>("");
|
||||
}
|
||||
Map<Object, String> enumValueMap = beforeConvert(contentProperty);
|
||||
String value = Convert.toStr(enumValueMap.get(object), "");
|
||||
return new WriteCellData<>(value);
|
||||
}
|
||||
|
||||
private Map<Object, String> beforeConvert(ExcelContentProperty contentProperty) {
|
||||
ExcelEnumFormat anno = getAnnotation(contentProperty.getField());
|
||||
Map<Object, String> enumValueMap = new HashMap<>();
|
||||
Enum<?>[] enumConstants = anno.enumClass().getEnumConstants();
|
||||
for (Enum<?> enumConstant : enumConstants) {
|
||||
Object codeValue = ReflectUtils.invokeGetter(enumConstant, anno.codeField());
|
||||
String textValue = ReflectUtils.invokeGetter(enumConstant, anno.textField());
|
||||
enumValueMap.put(codeValue, textValue);
|
||||
}
|
||||
return enumValueMap;
|
||||
}
|
||||
|
||||
private ExcelEnumFormat getAnnotation(Field field) {
|
||||
return AnnotationUtil.getAnnotation(field, ExcelEnumFormat.class);
|
||||
}
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
package com.fuyuanshen.common.excel.core;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.metadata.Head;
|
||||
import cn.idev.excel.write.handler.WorkbookWriteHandler;
|
||||
import cn.idev.excel.write.handler.context.WorkbookWriteHandlerContext;
|
||||
import cn.idev.excel.write.merge.AbstractMergeStrategy;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
import com.fuyuanshen.common.core.utils.reflect.ReflectUtils;
|
||||
import com.fuyuanshen.common.excel.annotation.CellMerge;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 列值重复合并策略
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler {
|
||||
|
||||
private final List<CellRangeAddress> cellList;
|
||||
private final boolean hasTitle;
|
||||
private int rowIndex;
|
||||
|
||||
public CellMergeStrategy(List<?> list, boolean hasTitle) {
|
||||
this.hasTitle = hasTitle;
|
||||
// 行合并开始下标
|
||||
this.rowIndex = hasTitle ? 1 : 0;
|
||||
this.cellList = handle(list, hasTitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
|
||||
//单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
|
||||
final int rowIndex = cell.getRowIndex();
|
||||
if (CollUtil.isNotEmpty(cellList)){
|
||||
for (CellRangeAddress cellAddresses : cellList) {
|
||||
final int firstRow = cellAddresses.getFirstRow();
|
||||
if (cellAddresses.isInRange(cell) && rowIndex != firstRow){
|
||||
cell.setBlank();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) {
|
||||
//当前表格写完后,统一写入
|
||||
if (CollUtil.isNotEmpty(cellList)){
|
||||
for (CellRangeAddress item : cellList) {
|
||||
context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
|
||||
List<CellRangeAddress> cellList = new ArrayList<>();
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return cellList;
|
||||
}
|
||||
Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));
|
||||
|
||||
// 有注解的字段
|
||||
List<Field> mergeFields = new ArrayList<>();
|
||||
List<Integer> mergeFieldsIndex = new ArrayList<>();
|
||||
for (int i = 0; i < fields.length; i++) {
|
||||
Field field = fields[i];
|
||||
if (field.isAnnotationPresent(CellMerge.class)) {
|
||||
CellMerge cm = field.getAnnotation(CellMerge.class);
|
||||
mergeFields.add(field);
|
||||
mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
|
||||
if (hasTitle) {
|
||||
ExcelProperty property = field.getAnnotation(ExcelProperty.class);
|
||||
rowIndex = Math.max(rowIndex, property.value().length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<Field, RepeatCell> map = new HashMap<>();
|
||||
// 生成两两合并单元格
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
for (int j = 0; j < mergeFields.size(); j++) {
|
||||
Field field = mergeFields.get(j);
|
||||
Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());
|
||||
|
||||
int colNum = mergeFieldsIndex.get(j);
|
||||
if (!map.containsKey(field)) {
|
||||
map.put(field, new RepeatCell(val, i));
|
||||
} else {
|
||||
RepeatCell repeatCell = map.get(field);
|
||||
Object cellValue = repeatCell.getValue();
|
||||
if (cellValue == null || "".equals(cellValue)) {
|
||||
// 空值跳过不合并
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!cellValue.equals(val)) {
|
||||
if ((i - repeatCell.getCurrent() > 1)) {
|
||||
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
|
||||
}
|
||||
map.put(field, new RepeatCell(val, i));
|
||||
} else if (i == list.size() - 1) {
|
||||
if (!isMerge(list, i, field)) {
|
||||
// 如果最后一行不能合并,检查之前的数据是否需要合并
|
||||
if (i - repeatCell.getCurrent() > 1) {
|
||||
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
|
||||
}
|
||||
} else if (i > repeatCell.getCurrent()) {
|
||||
// 如果最后一行可以合并,则直接合并到最后
|
||||
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
|
||||
}
|
||||
} else if (!isMerge(list, i, field)) {
|
||||
if ((i - repeatCell.getCurrent() > 1)) {
|
||||
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
|
||||
}
|
||||
map.put(field, new RepeatCell(val, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cellList;
|
||||
}
|
||||
|
||||
private boolean isMerge(List<?> list, int i, Field field) {
|
||||
boolean isMerge = true;
|
||||
CellMerge cm = field.getAnnotation(CellMerge.class);
|
||||
final String[] mergeBy = cm.mergeBy();
|
||||
if (StrUtil.isAllNotBlank(mergeBy)) {
|
||||
//比对当前list(i)和list(i - 1)的各个属性值一一比对 如果全为真 则为真
|
||||
for (String fieldName : mergeBy) {
|
||||
final Object valCurrent = ReflectUtil.getFieldValue(list.get(i), fieldName);
|
||||
final Object valPre = ReflectUtil.getFieldValue(list.get(i - 1), fieldName);
|
||||
if (!Objects.equals(valPre, valCurrent)) {
|
||||
//依赖字段如有任一不等值,则标记为不可合并
|
||||
isMerge = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isMerge;
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
static class RepeatCell {
|
||||
|
||||
private Object value;
|
||||
|
||||
private int current;
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package com.fuyuanshen.common.excel.core;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.idev.excel.context.AnalysisContext;
|
||||
import cn.idev.excel.event.AnalysisEventListener;
|
||||
import cn.idev.excel.exception.ExcelAnalysisException;
|
||||
import cn.idev.excel.exception.ExcelDataConvertException;
|
||||
import com.fuyuanshen.common.core.utils.StreamUtils;
|
||||
import com.fuyuanshen.common.core.utils.ValidatorUtils;
|
||||
import com.fuyuanshen.common.json.utils.JsonUtils;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Excel 导入监听
|
||||
*
|
||||
* @author Yjoioooo
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
@NoArgsConstructor
|
||||
public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements ExcelListener<T> {
|
||||
|
||||
/**
|
||||
* 是否Validator检验,默认为是
|
||||
*/
|
||||
private Boolean isValidate = Boolean.TRUE;
|
||||
|
||||
/**
|
||||
* excel 表头数据
|
||||
*/
|
||||
private Map<Integer, String> headMap;
|
||||
|
||||
/**
|
||||
* 导入回执
|
||||
*/
|
||||
private ExcelResult<T> excelResult;
|
||||
|
||||
public DefaultExcelListener(boolean isValidate) {
|
||||
this.excelResult = new DefaultExcelResult<>();
|
||||
this.isValidate = isValidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理异常
|
||||
*
|
||||
* @param exception ExcelDataConvertException
|
||||
* @param context Excel 上下文
|
||||
*/
|
||||
@Override
|
||||
public void onException(Exception exception, AnalysisContext context) throws Exception {
|
||||
String errMsg = null;
|
||||
if (exception instanceof ExcelDataConvertException excelDataConvertException) {
|
||||
// 如果是某一个单元格的转换异常 能获取到具体行号
|
||||
Integer rowIndex = excelDataConvertException.getRowIndex();
|
||||
Integer columnIndex = excelDataConvertException.getColumnIndex();
|
||||
errMsg = StrUtil.format("第{}行-第{}列-表头{}: 解析异常<br/>",
|
||||
rowIndex + 1, columnIndex + 1, headMap.get(columnIndex));
|
||||
if (log.isDebugEnabled()) {
|
||||
log.error(errMsg);
|
||||
}
|
||||
}
|
||||
if (exception instanceof ConstraintViolationException constraintViolationException) {
|
||||
Set<ConstraintViolation<?>> constraintViolations = constraintViolationException.getConstraintViolations();
|
||||
String constraintViolationsMsg = StreamUtils.join(constraintViolations, ConstraintViolation::getMessage, ", ");
|
||||
errMsg = StrUtil.format("第{}行数据校验异常: {}", context.readRowHolder().getRowIndex() + 1, constraintViolationsMsg);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.error(errMsg);
|
||||
}
|
||||
}
|
||||
excelResult.getErrorList().add(errMsg);
|
||||
throw new ExcelAnalysisException(errMsg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
|
||||
this.headMap = headMap;
|
||||
log.debug("解析到一条表头数据: {}", JsonUtils.toJsonString(headMap));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(T data, AnalysisContext context) {
|
||||
if (isValidate) {
|
||||
ValidatorUtils.validate(data);
|
||||
}
|
||||
excelResult.getList().add(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||
log.debug("所有数据解析完成!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExcelResult<T> getExcelResult() {
|
||||
return excelResult;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package com.fuyuanshen.common.excel.core;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 默认excel返回对象
|
||||
*
|
||||
* @author Yjoioooo
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class DefaultExcelResult<T> implements ExcelResult<T> {
|
||||
|
||||
/**
|
||||
* 数据对象list
|
||||
*/
|
||||
@Setter
|
||||
private List<T> list;
|
||||
|
||||
/**
|
||||
* 错误信息列表
|
||||
*/
|
||||
@Setter
|
||||
private List<String> errorList;
|
||||
|
||||
public DefaultExcelResult() {
|
||||
this.list = new ArrayList<>();
|
||||
this.errorList = new ArrayList<>();
|
||||
}
|
||||
|
||||
public DefaultExcelResult(List<T> list, List<String> errorList) {
|
||||
this.list = list;
|
||||
this.errorList = errorList;
|
||||
}
|
||||
|
||||
public DefaultExcelResult(ExcelResult<T> excelResult) {
|
||||
this.list = excelResult.getList();
|
||||
this.errorList = excelResult.getErrorList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> getList() {
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getErrorList() {
|
||||
return errorList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导入回执
|
||||
*
|
||||
* @return 导入回执
|
||||
*/
|
||||
@Override
|
||||
public String getAnalysis() {
|
||||
int successCount = list.size();
|
||||
int errorCount = errorList.size();
|
||||
if (successCount == 0) {
|
||||
return "读取失败,未解析到数据";
|
||||
} else {
|
||||
if (errorCount == 0) {
|
||||
return StrUtil.format("恭喜您,全部读取成功!共{}条", successCount);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
package com.fuyuanshen.common.excel.core;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import com.fuyuanshen.common.core.exception.ServiceException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* <h1>Excel下拉可选项</h1>
|
||||
* 注意:为确保下拉框解析正确,传值务必使用createOptionValue()做为值的拼接
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("unused")
|
||||
public class DropDownOptions {
|
||||
/**
|
||||
* 一级下拉所在列index,从0开始算
|
||||
*/
|
||||
private int index = 0;
|
||||
/**
|
||||
* 二级下拉所在的index,从0开始算,不能与一级相同
|
||||
*/
|
||||
private int nextIndex = 0;
|
||||
/**
|
||||
* 一级下拉所包含的数据
|
||||
*/
|
||||
private List<String> options = new ArrayList<>();
|
||||
/**
|
||||
* 二级下拉所包含的数据Map
|
||||
* <p>以每一个一级选项值为Key,每个一级选项对应的二级数据为Value</p>
|
||||
*/
|
||||
private Map<String, List<String>> nextOptions = new HashMap<>();
|
||||
/**
|
||||
* 分隔符
|
||||
*/
|
||||
private static final String DELIMITER = "_";
|
||||
|
||||
/**
|
||||
* 创建只有一级的下拉选
|
||||
*/
|
||||
public DropDownOptions(int index, List<String> options) {
|
||||
this.index = index;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>创建每个选项可选值</h2>
|
||||
* <p>注意:不能以数字,特殊符号开头,选项中不可以包含任何运算符号</p>
|
||||
*
|
||||
* @param vars 可选值内包含的参数
|
||||
* @return 合规的可选值
|
||||
*/
|
||||
public static String createOptionValue(Object... vars) {
|
||||
StringBuilder stringBuffer = new StringBuilder();
|
||||
String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
|
||||
for (int i = 0; i < vars.length; i++) {
|
||||
String var = StrUtil.trimToEmpty(String.valueOf(vars[i]));
|
||||
if (!var.matches(regex)) {
|
||||
throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
|
||||
}
|
||||
stringBuffer.append(var);
|
||||
if (i < vars.length - 1) {
|
||||
// 直至最后一个前,都以_作为切割线
|
||||
stringBuffer.append(DELIMITER);
|
||||
}
|
||||
}
|
||||
if (stringBuffer.toString().matches("^\\d_*$")) {
|
||||
throw new ServiceException("禁止以数字开头");
|
||||
}
|
||||
return stringBuffer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将处理后合理的可选值解析为原始的参数
|
||||
*
|
||||
* @param option 经过处理后的合理的可选项
|
||||
* @return 原始的参数
|
||||
*/
|
||||
public static List<String> analyzeOptionValue(String option) {
|
||||
return StrUtil.split(option, DELIMITER, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建级联下拉选项
|
||||
*
|
||||
* @param parentList 父实体可选项原始数据
|
||||
* @param parentIndex 父下拉选位置
|
||||
* @param sonList 子实体可选项原始数据
|
||||
* @param sonIndex 子下拉选位置
|
||||
* @param parentHowToGetIdFunction 父类如何获取唯一标识
|
||||
* @param sonHowToGetParentIdFunction 子类如何获取父类的唯一标识
|
||||
* @param howToBuildEveryOption 如何生成下拉选内容
|
||||
* @return 级联下拉选项
|
||||
*/
|
||||
public static <T> DropDownOptions buildLinkedOptions(List<T> parentList,
|
||||
int parentIndex,
|
||||
List<T> sonList,
|
||||
int sonIndex,
|
||||
Function<T, Number> parentHowToGetIdFunction,
|
||||
Function<T, Number> sonHowToGetParentIdFunction,
|
||||
Function<T, String> howToBuildEveryOption) {
|
||||
DropDownOptions parentLinkSonOptions = new DropDownOptions();
|
||||
// 先创建父类的下拉
|
||||
parentLinkSonOptions.setIndex(parentIndex);
|
||||
parentLinkSonOptions.setOptions(
|
||||
parentList.stream()
|
||||
.map(howToBuildEveryOption)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
// 提取父-子级联下拉
|
||||
Map<String, List<String>> sonOptions = new HashMap<>();
|
||||
// 父级依据自己的ID分组
|
||||
Map<Number, List<T>> parentGroupByIdMap =
|
||||
parentList.stream().collect(Collectors.groupingBy(parentHowToGetIdFunction));
|
||||
// 遍历每个子集,提取到Map中
|
||||
sonList.forEach(everySon -> {
|
||||
if (parentGroupByIdMap.containsKey(sonHowToGetParentIdFunction.apply(everySon))) {
|
||||
// 找到对应的上级
|
||||
T parentObj = parentGroupByIdMap.get(sonHowToGetParentIdFunction.apply(everySon)).get(0);
|
||||
// 提取名称和ID作为Key
|
||||
String key = howToBuildEveryOption.apply(parentObj);
|
||||
// Key对应的Value
|
||||
List<String> thisParentSonOptionList;
|
||||
if (sonOptions.containsKey(key)) {
|
||||
thisParentSonOptionList = sonOptions.get(key);
|
||||
} else {
|
||||
thisParentSonOptionList = new ArrayList<>();
|
||||
sonOptions.put(key, thisParentSonOptionList);
|
||||
}
|
||||
// 往Value中添加当前子集选项
|
||||
thisParentSonOptionList.add(howToBuildEveryOption.apply(everySon));
|
||||
}
|
||||
});
|
||||
parentLinkSonOptions.setNextIndex(sonIndex);
|
||||
parentLinkSonOptions.setNextOptions(sonOptions);
|
||||
return parentLinkSonOptions;
|
||||
}
|
||||
}
|
@ -0,0 +1,401 @@
|
||||
package com.fuyuanshen.common.excel.core;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.EnumUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.idev.excel.metadata.FieldCache;
|
||||
import cn.idev.excel.metadata.FieldWrapper;
|
||||
import cn.idev.excel.util.ClassUtils;
|
||||
import cn.idev.excel.write.handler.SheetWriteHandler;
|
||||
import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
|
||||
import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.ss.util.CellRangeAddressList;
|
||||
import org.apache.poi.ss.util.WorkbookUtil;
|
||||
import org.apache.poi.xssf.usermodel.XSSFDataValidation;
|
||||
import com.fuyuanshen.common.core.exception.ServiceException;
|
||||
import com.fuyuanshen.common.core.service.DictService;
|
||||
import com.fuyuanshen.common.core.utils.SpringUtils;
|
||||
import com.fuyuanshen.common.core.utils.StreamUtils;
|
||||
import com.fuyuanshen.common.core.utils.StringUtils;
|
||||
import com.fuyuanshen.common.excel.annotation.ExcelDictFormat;
|
||||
import com.fuyuanshen.common.excel.annotation.ExcelEnumFormat;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* <h1>Excel表格下拉选操作</h1>
|
||||
* 考虑到下拉选过多可能导致Excel打开缓慢的问题,只校验前1000行
|
||||
* <p>
|
||||
* 即只有前1000行的数据可以用下拉框,超出的自行通过限制数据量的形式,第二次输出
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
@Slf4j
|
||||
public class ExcelDownHandler implements SheetWriteHandler {
|
||||
|
||||
/**
|
||||
* Excel表格中的列名英文
|
||||
* 仅为了解析列英文,禁止修改
|
||||
*/
|
||||
private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
/**
|
||||
* 单选数据Sheet名
|
||||
*/
|
||||
private static final String OPTIONS_SHEET_NAME = "options";
|
||||
/**
|
||||
* 联动选择数据Sheet名的头
|
||||
*/
|
||||
private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions";
|
||||
/**
|
||||
* 下拉可选项
|
||||
*/
|
||||
private final List<DropDownOptions> dropDownOptions;
|
||||
private final DictService dictService;
|
||||
/**
|
||||
* 当前单选进度
|
||||
*/
|
||||
private int currentOptionsColumnIndex;
|
||||
/**
|
||||
* 当前联动选择进度
|
||||
*/
|
||||
private int currentLinkedOptionsSheetIndex;
|
||||
|
||||
public ExcelDownHandler(List<DropDownOptions> options) {
|
||||
this.dropDownOptions = options;
|
||||
this.currentOptionsColumnIndex = 0;
|
||||
this.currentLinkedOptionsSheetIndex = 0;
|
||||
this.dictService = SpringUtils.getBean(DictService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>开始创建下拉数据</h2>
|
||||
* 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项
|
||||
* 如果有且设置了value值,则将其直接置为下拉可选项
|
||||
* <p>
|
||||
* 2.或者在调用ExcelUtil时指定了可选项,将依据传入的可选项做下拉
|
||||
* <p>
|
||||
* 3.二者并存,注意调用方式
|
||||
*/
|
||||
@Override
|
||||
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
|
||||
Sheet sheet = writeSheetHolder.getSheet();
|
||||
// 开始设置下拉框 HSSFWorkbook
|
||||
DataValidationHelper helper = sheet.getDataValidationHelper();
|
||||
Workbook workbook = writeWorkbookHolder.getWorkbook();
|
||||
FieldCache fieldCache = ClassUtils.declaredFields(writeWorkbookHolder.getClazz(), writeWorkbookHolder);
|
||||
for (Map.Entry<Integer, FieldWrapper> entry : fieldCache.getSortedFieldMap().entrySet()) {
|
||||
Integer index = entry.getKey();
|
||||
FieldWrapper wrapper = entry.getValue();
|
||||
Field field = wrapper.getField();
|
||||
// 循环实体中的每个属性
|
||||
// 可选的下拉值
|
||||
List<String> options = new ArrayList<>();
|
||||
if (field.isAnnotationPresent(ExcelDictFormat.class)) {
|
||||
// 如果指定了@ExcelDictFormat,则使用字典的逻辑
|
||||
ExcelDictFormat format = field.getDeclaredAnnotation(ExcelDictFormat.class);
|
||||
String dictType = format.dictType();
|
||||
String converterExp = format.readConverterExp();
|
||||
if (StringUtils.isNotBlank(dictType)) {
|
||||
// 如果传递了字典名,则依据字典建立下拉
|
||||
Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
|
||||
.orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
|
||||
.values();
|
||||
options = new ArrayList<>(values);
|
||||
} else if (StringUtils.isNotBlank(converterExp)) {
|
||||
// 如果指定了确切的值,则直接解析确切的值
|
||||
List<String> strList = StringUtils.splitList(converterExp, format.separator());
|
||||
options = StreamUtils.toList(strList, s -> StringUtils.split(s, "=")[1]);
|
||||
}
|
||||
} else if (field.isAnnotationPresent(ExcelEnumFormat.class)) {
|
||||
// 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
|
||||
ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class);
|
||||
List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
|
||||
options = StreamUtils.toList(values, String::valueOf);
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(options)) {
|
||||
// 仅当下拉可选项不为空时执行
|
||||
if (options.size() > 20) {
|
||||
// 这里限制如果可选项大于20,则使用额外表形式
|
||||
dropDownWithSheet(helper, workbook, sheet, index, options);
|
||||
} else {
|
||||
// 否则使用固定值形式
|
||||
dropDownWithSimple(helper, sheet, index, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (CollUtil.isEmpty(dropDownOptions)) {
|
||||
return;
|
||||
}
|
||||
dropDownOptions.forEach(everyOptions -> {
|
||||
// 如果传递了下拉框选择器参数
|
||||
if (!everyOptions.getNextOptions().isEmpty()) {
|
||||
// 当二级选项不为空时,使用额外关联表的形式
|
||||
dropDownLinkedOptions(helper, workbook, sheet, everyOptions);
|
||||
} else if (everyOptions.getOptions().size() > 10) {
|
||||
// 当一级选项参数个数大于10,使用额外表的形式
|
||||
dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
|
||||
} else {
|
||||
// 否则使用默认形式
|
||||
dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>简单下拉框</h2>
|
||||
* 直接将可选项拼接为指定列的数据校验值
|
||||
*
|
||||
* @param celIndex 列index
|
||||
* @param value 下拉选可选值
|
||||
*/
|
||||
private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) {
|
||||
if (ObjectUtil.isEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>额外表格形式的级联下拉框</h2>
|
||||
*
|
||||
* @param options 额外表格形式存储的下拉可选项
|
||||
*/
|
||||
private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) {
|
||||
String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex);
|
||||
// 创建联动下拉数据表
|
||||
Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
|
||||
// 将下拉表隐藏
|
||||
workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
|
||||
// 选项数据
|
||||
List<String> firstOptions = options.getOptions();
|
||||
Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
|
||||
|
||||
// 采用按行填充数据的方式,避免出现数据无法写入的问题
|
||||
// Attempting to write a row in the range that is already written to disk
|
||||
|
||||
// 使用ArrayList记载数据,防止乱序
|
||||
List<String> columnNames = new ArrayList<>();
|
||||
// 写入第一行,即第一级的数据
|
||||
Row firstRow = linkedOptionsDataSheet.createRow(0);
|
||||
for (int columnIndex = 0; columnIndex < firstOptions.size(); columnIndex++) {
|
||||
String columnName = firstOptions.get(columnIndex);
|
||||
firstRow.createCell(columnIndex)
|
||||
.setCellValue(columnName);
|
||||
columnNames.add(columnName);
|
||||
}
|
||||
|
||||
// 创建名称管理器
|
||||
Name name = workbook.createName();
|
||||
// 设置名称管理器的别名
|
||||
name.setNameName(linkedOptionsSheetName);
|
||||
// 以横向第一行创建一级下拉拼接引用位置
|
||||
String firstOptionsFunction = String.format("%s!$%s$1:$%s$1",
|
||||
linkedOptionsSheetName,
|
||||
getExcelColumnName(0),
|
||||
getExcelColumnName(firstOptions.size())
|
||||
);
|
||||
// 设置名称管理器的引用位置
|
||||
name.setRefersToFormula(firstOptionsFunction);
|
||||
// 设置数据校验为序列模式,引用的是名称管理器中的别名
|
||||
this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
|
||||
|
||||
// 创建二级选项的名称管理器
|
||||
for (int columIndex = 0; columIndex < columnNames.size(); columIndex++) {
|
||||
// 列名
|
||||
String firstOptionsColumnName = getExcelColumnName(columIndex);
|
||||
// 对应的一级值
|
||||
String thisFirstOptionsValue = columnNames.get(columIndex);
|
||||
|
||||
// 以该一级选项值创建子名称管理器
|
||||
Name sonName = workbook.createName();
|
||||
// 设置名称管理器的别名
|
||||
sonName.setNameName(thisFirstOptionsValue);
|
||||
// 以第二行该列数据拼接引用位置
|
||||
String sonFunction = String.format("%s!$%s$2:$%s$%d",
|
||||
linkedOptionsSheetName,
|
||||
firstOptionsColumnName,
|
||||
firstOptionsColumnName,
|
||||
// 二级选项存在则设置为(选项个数+1)行,否则设置为2行
|
||||
Math.max(Optional.ofNullable(secoundOptionsMap.get(thisFirstOptionsValue))
|
||||
.orElseGet(ArrayList::new).size(), 1) + 1
|
||||
);
|
||||
// 设置名称管理器的引用位置
|
||||
sonName.setRefersToFormula(sonFunction);
|
||||
// 数据验证为序列模式,引用到每一个主表中的二级选项位置
|
||||
// 创建子项的名称管理器,只是为了使得Excel可以识别到数据
|
||||
String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex());
|
||||
for (int i = 0; i < 100; i++) {
|
||||
// 以一级选项对应的主体所在位置创建二级下拉
|
||||
String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1);
|
||||
// 二级只能主表每一行的每一列添加二级校验
|
||||
markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
|
||||
}
|
||||
}
|
||||
|
||||
// 将二级数据处理为按行区分
|
||||
Map<Integer, List<String>> columnValueMap = new HashMap<>();
|
||||
int currentRow = 1;
|
||||
while (currentRow >= 0) {
|
||||
boolean flag = false;
|
||||
List<String> rowData = new ArrayList<>();
|
||||
for (String columnName : columnNames) {
|
||||
List<String> data = secoundOptionsMap.get(columnName);
|
||||
if (CollUtil.isEmpty(data)) {
|
||||
// 添加空字符串填充位置
|
||||
rowData.add(" ");
|
||||
continue;
|
||||
}
|
||||
// 取第一个
|
||||
String str = data.get(0);
|
||||
rowData.add(str);
|
||||
// 通过移除的方式避免重复
|
||||
data.remove(0);
|
||||
// 设置可以继续
|
||||
flag = true;
|
||||
}
|
||||
columnValueMap.put(currentRow, rowData);
|
||||
// 可以继续,则增加行数,否则置为负数跳出循环
|
||||
if (flag) {
|
||||
currentRow++;
|
||||
} else {
|
||||
currentRow = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// 填充第二级选项数据
|
||||
columnValueMap.forEach((rowIndex, rowValues) -> {
|
||||
Row row = linkedOptionsDataSheet.createRow(rowIndex);
|
||||
for (int columnIndex = 0; columnIndex < rowValues.size(); columnIndex++) {
|
||||
String rowValue = rowValues.get(columnIndex);
|
||||
// 填充位置的部分不渲染
|
||||
if (StrUtil.isNotBlank(rowValue)) {
|
||||
row.createCell(columnIndex)
|
||||
.setCellValue(rowValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
currentLinkedOptionsSheetIndex++;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>额外表格形式的普通下拉框</h2>
|
||||
* 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉
|
||||
*
|
||||
* @param celIndex 下拉选
|
||||
* @param value 下拉选可选值
|
||||
*/
|
||||
private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) {
|
||||
//由于poi的写出相关问题,超过100个会被临时写进硬盘,导致后续内存合并会出Attempting to write a row[] in the range [] that is already written to disk
|
||||
String tmpOptionsSheetName = OPTIONS_SHEET_NAME + "_" + currentOptionsColumnIndex;
|
||||
// 创建下拉数据表
|
||||
Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(tmpOptionsSheetName)))
|
||||
.orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(tmpOptionsSheetName)));
|
||||
// 将下拉表隐藏
|
||||
workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
|
||||
// 完善纵向的一级选项数据表
|
||||
for (int i = 0; i < value.size(); i++) {
|
||||
int finalI = i;
|
||||
// 获取每一选项行,如果没有则创建
|
||||
Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
|
||||
.orElseGet(() -> simpleDataSheet.createRow(finalI));
|
||||
// 获取本级选项对应的选项列,如果没有则创建。上述采用多个sheet,默认索引为1列
|
||||
Cell cell = Optional.ofNullable(row.getCell(0))
|
||||
.orElseGet(() -> row.createCell(0));
|
||||
// 设置值
|
||||
cell.setCellValue(value.get(i));
|
||||
}
|
||||
|
||||
// 创建名称管理器
|
||||
Name name = workbook.createName();
|
||||
// 设置名称管理器的别名
|
||||
String nameName = String.format("%s_%d", tmpOptionsSheetName, celIndex);
|
||||
name.setNameName(nameName);
|
||||
// 以纵向第一列创建一级下拉拼接引用位置
|
||||
String function = String.format("%s!$%s$1:$%s$%d",
|
||||
tmpOptionsSheetName,
|
||||
getExcelColumnName(0),
|
||||
getExcelColumnName(0),
|
||||
value.size());
|
||||
// 设置名称管理器的引用位置
|
||||
name.setRefersToFormula(function);
|
||||
// 设置数据校验为序列模式,引用的是名称管理器中的别名
|
||||
this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName));
|
||||
currentOptionsColumnIndex++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂载下拉的列,仅限一级选项
|
||||
*/
|
||||
private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex,
|
||||
DataValidationConstraint constraint) {
|
||||
// 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
|
||||
CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
|
||||
markDataValidationToSheet(helper, sheet, constraint, addressList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂载下拉的列,仅限二级选项
|
||||
*/
|
||||
private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex,
|
||||
Integer celIndex, DataValidationConstraint constraint) {
|
||||
// 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
|
||||
CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex);
|
||||
markDataValidationToSheet(helper, sheet, constraint, addressList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用数据校验
|
||||
*/
|
||||
private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet,
|
||||
DataValidationConstraint constraint, CellRangeAddressList addressList) {
|
||||
// 数据有效性对象
|
||||
DataValidation dataValidation = helper.createValidation(constraint, addressList);
|
||||
// 处理Excel兼容性问题
|
||||
if (dataValidation instanceof XSSFDataValidation) {
|
||||
//数据校验
|
||||
dataValidation.setSuppressDropDownArrow(true);
|
||||
//错误提示
|
||||
dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
|
||||
dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致");
|
||||
dataValidation.setShowErrorBox(true);
|
||||
//选定提示
|
||||
dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败");
|
||||
dataValidation.setShowPromptBox(true);
|
||||
sheet.addValidationData(dataValidation);
|
||||
} else {
|
||||
dataValidation.setSuppressDropDownArrow(false);
|
||||
}
|
||||
sheet.addValidationData(dataValidation);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>依据列index获取列名英文</h2>
|
||||
* 依据列index转换为Excel中的列名英文
|
||||
* <p>例如第1列,index为0,解析出来为A列</p>
|
||||
* 第27列,index为26,解析为AA列
|
||||
* <p>第28列,index为27,解析为AB列</p>
|
||||
*
|
||||
* @param columnIndex 列index
|
||||
* @return 列index所在得英文名
|
||||
*/
|
||||
private String getExcelColumnName(int columnIndex) {
|
||||
// 26一循环的次数
|
||||
int columnCircleCount = columnIndex / 26;
|
||||
// 26一循环内的位置
|
||||
int thisCircleColumnIndex = columnIndex % 26;
|
||||
// 26一循环的次数大于0,则视为栏名至少两位
|
||||
String columnPrefix = columnCircleCount == 0
|
||||
? StrUtil.EMPTY
|
||||
: StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
|
||||
// 从26一循环内取对应的栏位名
|
||||
String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
|
||||
// 将二者拼接即为最终的栏位名
|
||||
return columnPrefix + columnNext;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.fuyuanshen.common.excel.core;
|
||||
|
||||
import cn.idev.excel.read.listener.ReadListener;
|
||||
|
||||
/**
|
||||
* Excel 导入监听
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public interface ExcelListener<T> extends ReadListener<T> {
|
||||
|
||||
ExcelResult<T> getExcelResult();
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.fuyuanshen.common.excel.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* excel返回对象
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public interface ExcelResult<T> {
|
||||
|
||||
/**
|
||||
* 对象列表
|
||||
*/
|
||||
List<T> getList();
|
||||
|
||||
/**
|
||||
* 错误列表
|
||||
*/
|
||||
List<String> getErrorList();
|
||||
|
||||
/**
|
||||
* 导入回执
|
||||
*/
|
||||
String getAnalysis();
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
package com.fuyuanshen.common.excel.handler;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.idev.excel.metadata.data.DataFormatData;
|
||||
import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.util.StyleUtil;
|
||||
import cn.idev.excel.write.handler.CellWriteHandler;
|
||||
import cn.idev.excel.write.handler.SheetWriteHandler;
|
||||
import cn.idev.excel.write.handler.context.CellWriteHandlerContext;
|
||||
import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
|
||||
import cn.idev.excel.write.metadata.style.WriteCellStyle;
|
||||
import cn.idev.excel.write.metadata.style.WriteFont;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
|
||||
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
|
||||
import com.fuyuanshen.common.core.utils.reflect.ReflectUtils;
|
||||
import com.fuyuanshen.common.excel.annotation.ExcelNotation;
|
||||
import com.fuyuanshen.common.excel.annotation.ExcelRequired;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 批注、必填
|
||||
*
|
||||
* @author guzhouyanyu
|
||||
*/
|
||||
public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
|
||||
|
||||
/**
|
||||
* 批注
|
||||
*/
|
||||
private final Map<Integer, String> notationMap;
|
||||
|
||||
/**
|
||||
* 头列字体颜色
|
||||
*/
|
||||
private final Map<Integer, Short> headColumnMap;
|
||||
|
||||
|
||||
public DataWriteHandler(Class<?> clazz) {
|
||||
notationMap = getNotationMap(clazz);
|
||||
headColumnMap = getRequiredMap(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCellDispose(CellWriteHandlerContext context) {
|
||||
if (CollUtil.isEmpty(notationMap) && CollUtil.isEmpty(headColumnMap)) {
|
||||
return;
|
||||
}
|
||||
WriteCellData<?> cellData = context.getFirstCellData();
|
||||
WriteCellStyle writeCellStyle = cellData.getOrCreateStyle();
|
||||
|
||||
DataFormatData dataFormatData = new DataFormatData();
|
||||
// 单元格设置为文本格式
|
||||
dataFormatData.setIndex((short) 49);
|
||||
writeCellStyle.setDataFormatData(dataFormatData);
|
||||
|
||||
if (context.getHead()) {
|
||||
Cell cell = context.getCell();
|
||||
WriteSheetHolder writeSheetHolder = context.getWriteSheetHolder();
|
||||
Sheet sheet = writeSheetHolder.getSheet();
|
||||
Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
|
||||
Drawing<?> drawing = sheet.createDrawingPatriarch();
|
||||
// 设置标题字体样式
|
||||
WriteFont headWriteFont = new WriteFont();
|
||||
// 加粗
|
||||
headWriteFont.setBold(true);
|
||||
if (CollUtil.isNotEmpty(headColumnMap) && headColumnMap.containsKey(cell.getColumnIndex())) {
|
||||
// 设置字体颜色
|
||||
headWriteFont.setColor(headColumnMap.get(cell.getColumnIndex()));
|
||||
}
|
||||
writeCellStyle.setWriteFont(headWriteFont);
|
||||
CellStyle cellStyle = StyleUtil.buildCellStyle(workbook, null, writeCellStyle);
|
||||
cell.setCellStyle(cellStyle);
|
||||
|
||||
if (CollUtil.isNotEmpty(notationMap) && notationMap.containsKey(cell.getColumnIndex())) {
|
||||
// 批注内容
|
||||
String notationContext = notationMap.get(cell.getColumnIndex());
|
||||
// 创建绘图对象
|
||||
Comment comment = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), 0, (short) 5, 5));
|
||||
comment.setString(new XSSFRichTextString(notationContext));
|
||||
cell.setCellComment(comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取必填列
|
||||
*/
|
||||
private static Map<Integer, Short> getRequiredMap(Class<?> clazz) {
|
||||
Map<Integer, Short> requiredMap = new HashMap<>();
|
||||
Field[] fields = clazz.getDeclaredFields();
|
||||
// 检查 fields 数组是否为空
|
||||
if (fields.length == 0) {
|
||||
return requiredMap;
|
||||
}
|
||||
Field[] filteredFields = ReflectUtils.getFields(clazz, field -> !"serialVersionUID".equals(field.getName()));
|
||||
|
||||
for (int i = 0; i < filteredFields.length; i++) {
|
||||
Field field = filteredFields[i];
|
||||
if (!field.isAnnotationPresent(ExcelRequired.class)) {
|
||||
continue;
|
||||
}
|
||||
ExcelRequired excelRequired = field.getAnnotation(ExcelRequired.class);
|
||||
int columnIndex = excelRequired.index() == -1 ? i : excelRequired.index();
|
||||
requiredMap.put(columnIndex, excelRequired.fontColor().getIndex());
|
||||
}
|
||||
return requiredMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取批注
|
||||
*/
|
||||
private static Map<Integer, String> getNotationMap(Class<?> clazz) {
|
||||
Map<Integer, String> notationMap = new HashMap<>();
|
||||
Field[] fields = clazz.getDeclaredFields();
|
||||
// 检查 fields 数组是否为空
|
||||
if (fields.length == 0) {
|
||||
return notationMap;
|
||||
}
|
||||
Field[] filteredFields = ReflectUtils.getFields(clazz, field -> !"serialVersionUID".equals(field.getName()));
|
||||
for (int i = 0; i < filteredFields.length; i++) {
|
||||
Field field = filteredFields[i];
|
||||
if (!field.isAnnotationPresent(ExcelNotation.class)) {
|
||||
continue;
|
||||
}
|
||||
ExcelNotation excelNotation = field.getAnnotation(ExcelNotation.class);
|
||||
int columnIndex = excelNotation.index() == -1 ? i : excelNotation.index();
|
||||
notationMap.put(columnIndex, excelNotation.value());
|
||||
}
|
||||
return notationMap;
|
||||
}
|
||||
}
|
@ -0,0 +1,440 @@
|
||||
package com.fuyuanshen.common.excel.utils;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.io.resource.ClassPathResource;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.idev.excel.FastExcel;
|
||||
import cn.idev.excel.ExcelWriter;
|
||||
import cn.idev.excel.write.builder.ExcelWriterSheetBuilder;
|
||||
import cn.idev.excel.write.metadata.WriteSheet;
|
||||
import cn.idev.excel.write.metadata.fill.FillConfig;
|
||||
import cn.idev.excel.write.metadata.fill.FillWrapper;
|
||||
import cn.idev.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import com.fuyuanshen.common.core.utils.StringUtils;
|
||||
import com.fuyuanshen.common.core.utils.file.FileUtils;
|
||||
import com.fuyuanshen.common.excel.convert.ExcelBigNumberConvert;
|
||||
import com.fuyuanshen.common.excel.core.*;
|
||||
import com.fuyuanshen.common.excel.handler.DataWriteHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Excel相关处理
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class ExcelUtil {
|
||||
|
||||
/**
|
||||
* 同步导入(适用于小数据量)
|
||||
*
|
||||
* @param is 输入流
|
||||
* @return 转换后集合
|
||||
*/
|
||||
public static <T> List<T> importExcel(InputStream is, Class<T> clazz) {
|
||||
return FastExcel.read(is).head(clazz).autoCloseStream(false).sheet().doReadSync();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 使用校验监听器 异步导入 同步返回
|
||||
*
|
||||
* @param is 输入流
|
||||
* @param clazz 对象类型
|
||||
* @param isValidate 是否 Validator 检验 默认为是
|
||||
* @return 转换后集合
|
||||
*/
|
||||
public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, boolean isValidate) {
|
||||
DefaultExcelListener<T> listener = new DefaultExcelListener<>(isValidate);
|
||||
FastExcel.read(is, clazz, listener).sheet().doRead();
|
||||
return listener.getExcelResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用自定义监听器 异步导入 自定义返回
|
||||
*
|
||||
* @param is 输入流
|
||||
* @param clazz 对象类型
|
||||
* @param listener 自定义监听器
|
||||
* @return 转换后集合
|
||||
*/
|
||||
public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, ExcelListener<T> listener) {
|
||||
FastExcel.read(is, clazz, listener).sheet().doRead();
|
||||
return listener.getExcelResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param response 响应体
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response) {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, false, os, null);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param response 响应体
|
||||
* @param options 级联下拉选
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response, List<DropDownOptions> options) {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, false, os, options);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param merge 是否合并单元格
|
||||
* @param response 响应体
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response) {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, merge, os, null);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param merge 是否合并单元格
|
||||
* @param response 响应体
|
||||
* @param options 级联下拉选
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response, List<DropDownOptions> options) {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, merge, os, options);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param os 输出流
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os) {
|
||||
exportExcel(list, sheetName, clazz, false, os, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param os 输出流
|
||||
* @param options 级联下拉选内容
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options) {
|
||||
exportExcel(list, sheetName, clazz, false, os, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param merge 是否合并单元格
|
||||
* @param os 输出流
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,
|
||||
OutputStream os, List<DropDownOptions> options) {
|
||||
ExcelWriterSheetBuilder builder = FastExcel.write(os, clazz)
|
||||
.autoCloseStream(false)
|
||||
// 自动适配
|
||||
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
|
||||
// 大数值自动转换 防止失真
|
||||
.registerConverter(new ExcelBigNumberConvert())
|
||||
.registerWriteHandler(new DataWriteHandler(clazz))
|
||||
.sheet(sheetName);
|
||||
if (merge) {
|
||||
// 合并处理器
|
||||
builder.registerWriteHandler(new CellMergeStrategy(list, true));
|
||||
}
|
||||
// 添加下拉框操作
|
||||
builder.registerWriteHandler(new ExcelDownHandler(options));
|
||||
builder.doWrite(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单表多数据模板导出 模板格式为 {.属性}
|
||||
*
|
||||
* @param filename 文件名
|
||||
* @param templatePath 模板路径 resource 目录下的路径包括模板文件名
|
||||
* 例如: excel/temp.xlsx
|
||||
* 重点: 模板文件必须放置到启动类对应的 resource 目录下
|
||||
* @param data 模板需要的数据
|
||||
* @param response 响应体
|
||||
*/
|
||||
public static <T> void exportTemplate(List<T> data, String filename, String templatePath, HttpServletResponse response) {
|
||||
try {
|
||||
if (CollUtil.isEmpty(data)) {
|
||||
throw new IllegalArgumentException("数据为空");
|
||||
}
|
||||
resetResponse(filename, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportTemplate(data, templatePath, os);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单表多数据模板导出 模板格式为 {.属性}
|
||||
*
|
||||
* @param templatePath 模板路径 resource 目录下的路径包括模板文件名
|
||||
* 例如: excel/temp.xlsx
|
||||
* 重点: 模板文件必须放置到启动类对应的 resource 目录下
|
||||
* @param data 模板需要的数据
|
||||
* @param os 输出流
|
||||
*/
|
||||
public static <T> void exportTemplate(List<T> data, String templatePath, OutputStream os) {
|
||||
ClassPathResource templateResource = new ClassPathResource(templatePath);
|
||||
ExcelWriter excelWriter = FastExcel.write(os)
|
||||
.withTemplate(templateResource.getStream())
|
||||
.autoCloseStream(false)
|
||||
// 大数值自动转换 防止失真
|
||||
.registerConverter(new ExcelBigNumberConvert())
|
||||
.registerWriteHandler(new DataWriteHandler(data.get(0).getClass()))
|
||||
.build();
|
||||
WriteSheet writeSheet = FastExcel.writerSheet().build();
|
||||
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
|
||||
// 单表多数据导出 模板格式为 {.属性}
|
||||
for (T d : data) {
|
||||
excelWriter.fill(d, fillConfig, writeSheet);
|
||||
}
|
||||
excelWriter.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* 多表多数据模板导出 模板格式为 {key.属性}
|
||||
*
|
||||
* @param filename 文件名
|
||||
* @param templatePath 模板路径 resource 目录下的路径包括模板文件名
|
||||
* 例如: excel/temp.xlsx
|
||||
* 重点: 模板文件必须放置到启动类对应的 resource 目录下
|
||||
* @param data 模板需要的数据
|
||||
* @param response 响应体
|
||||
*/
|
||||
public static void exportTemplateMultiList(Map<String, Object> data, String filename, String templatePath, HttpServletResponse response) {
|
||||
try {
|
||||
if (CollUtil.isEmpty(data)) {
|
||||
throw new IllegalArgumentException("数据为空");
|
||||
}
|
||||
resetResponse(filename, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportTemplateMultiList(data, templatePath, os);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多sheet模板导出 模板格式为 {key.属性}
|
||||
*
|
||||
* @param filename 文件名
|
||||
* @param templatePath 模板路径 resource 目录下的路径包括模板文件名
|
||||
* 例如: excel/temp.xlsx
|
||||
* 重点: 模板文件必须放置到启动类对应的 resource 目录下
|
||||
* @param data 模板需要的数据
|
||||
* @param response 响应体
|
||||
*/
|
||||
public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String filename, String templatePath, HttpServletResponse response) {
|
||||
try {
|
||||
if (CollUtil.isEmpty(data)) {
|
||||
throw new IllegalArgumentException("数据为空");
|
||||
}
|
||||
resetResponse(filename, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportTemplateMultiSheet(data, templatePath, os);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多表多数据模板导出 模板格式为 {key.属性}
|
||||
*
|
||||
* @param templatePath 模板路径 resource 目录下的路径包括模板文件名
|
||||
* 例如: excel/temp.xlsx
|
||||
* 重点: 模板文件必须放置到启动类对应的 resource 目录下
|
||||
* @param data 模板需要的数据
|
||||
* @param os 输出流
|
||||
*/
|
||||
public static void exportTemplateMultiList(Map<String, Object> data, String templatePath, OutputStream os) {
|
||||
ClassPathResource templateResource = new ClassPathResource(templatePath);
|
||||
ExcelWriter excelWriter = FastExcel.write(os)
|
||||
.withTemplate(templateResource.getStream())
|
||||
.autoCloseStream(false)
|
||||
// 大数值自动转换 防止失真
|
||||
.registerConverter(new ExcelBigNumberConvert())
|
||||
.build();
|
||||
WriteSheet writeSheet = FastExcel.writerSheet().build();
|
||||
for (Map.Entry<String, Object> map : data.entrySet()) {
|
||||
// 设置列表后续还有数据
|
||||
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
|
||||
if (map.getValue() instanceof Collection) {
|
||||
// 多表导出必须使用 FillWrapper
|
||||
excelWriter.fill(new FillWrapper(map.getKey(), (Collection<?>) map.getValue()), fillConfig, writeSheet);
|
||||
} else {
|
||||
excelWriter.fill(map.getValue(), fillConfig, writeSheet);
|
||||
}
|
||||
}
|
||||
excelWriter.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* 多sheet模板导出 模板格式为 {key.属性}
|
||||
*
|
||||
* @param templatePath 模板路径 resource 目录下的路径包括模板文件名
|
||||
* 例如: excel/temp.xlsx
|
||||
* 重点: 模板文件必须放置到启动类对应的 resource 目录下
|
||||
* @param data 模板需要的数据
|
||||
* @param os 输出流
|
||||
*/
|
||||
public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String templatePath, OutputStream os) {
|
||||
ClassPathResource templateResource = new ClassPathResource(templatePath);
|
||||
ExcelWriter excelWriter = FastExcel.write(os)
|
||||
.withTemplate(templateResource.getStream())
|
||||
.autoCloseStream(false)
|
||||
// 大数值自动转换 防止失真
|
||||
.registerConverter(new ExcelBigNumberConvert())
|
||||
.build();
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
WriteSheet writeSheet = FastExcel.writerSheet(i).build();
|
||||
for (Map.Entry<String, Object> map : data.get(i).entrySet()) {
|
||||
// 设置列表后续还有数据
|
||||
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
|
||||
if (map.getValue() instanceof Collection) {
|
||||
// 多表导出必须使用 FillWrapper
|
||||
excelWriter.fill(new FillWrapper(map.getKey(), (Collection<?>) map.getValue()), fillConfig, writeSheet);
|
||||
} else {
|
||||
excelWriter.fill(map.getValue(), writeSheet);
|
||||
}
|
||||
}
|
||||
}
|
||||
excelWriter.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置响应体
|
||||
*/
|
||||
private static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {
|
||||
String filename = encodingFilename(sheetName);
|
||||
FileUtils.setAttachmentResponseHeader(response, filename);
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析导出值 0=男,1=女,2=未知
|
||||
*
|
||||
* @param propertyValue 参数值
|
||||
* @param converterExp 翻译注解
|
||||
* @param separator 分隔符
|
||||
* @return 解析后值
|
||||
*/
|
||||
public static String convertByExp(String propertyValue, String converterExp, String separator) {
|
||||
StringBuilder propertyString = new StringBuilder();
|
||||
String[] convertSource = converterExp.split(StringUtils.SEPARATOR);
|
||||
for (String item : convertSource) {
|
||||
String[] itemArray = item.split("=");
|
||||
if (StringUtils.containsAny(propertyValue, separator)) {
|
||||
for (String value : propertyValue.split(separator)) {
|
||||
if (itemArray[0].equals(value)) {
|
||||
propertyString.append(itemArray[1] + separator);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (itemArray[0].equals(propertyValue)) {
|
||||
return itemArray[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return StringUtils.stripEnd(propertyString.toString(), separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 反向解析值 男=0,女=1,未知=2
|
||||
*
|
||||
* @param propertyValue 参数值
|
||||
* @param converterExp 翻译注解
|
||||
* @param separator 分隔符
|
||||
* @return 解析后值
|
||||
*/
|
||||
public static String reverseByExp(String propertyValue, String converterExp, String separator) {
|
||||
StringBuilder propertyString = new StringBuilder();
|
||||
String[] convertSource = converterExp.split(StringUtils.SEPARATOR);
|
||||
for (String item : convertSource) {
|
||||
String[] itemArray = item.split("=");
|
||||
if (StringUtils.containsAny(propertyValue, separator)) {
|
||||
for (String value : propertyValue.split(separator)) {
|
||||
if (itemArray[1].equals(value)) {
|
||||
propertyString.append(itemArray[0] + separator);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (itemArray[1].equals(propertyValue)) {
|
||||
return itemArray[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
return StringUtils.stripEnd(propertyString.toString(), separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码文件名
|
||||
*/
|
||||
public static String encodingFilename(String filename) {
|
||||
return IdUtil.fastSimpleUUID() + "_" + filename + ".xlsx";
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user