feat: 增加dataSheet提供通用的excel import功能

This commit is contained in:
lilong 2024-03-19 18:09:18 +08:00
parent d49b863f78
commit 9af69dbf95
6 changed files with 1268 additions and 0 deletions

14
pom.xml
View File

@ -20,6 +20,8 @@
<revision>2.0.0-SNAPSHOT</revision>
<axzo-bom.version>2.0.0-SNAPSHOT</axzo-bom.version>
<axzo-dependencies.version>2.0.0-SNAPSHOT</axzo-dependencies.version>
<poi.version>5.2.2</poi.version>
<easyexcel.version>3.3.3</easyexcel.version>
</properties>
<dependencyManagement>
@ -113,6 +115,18 @@
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
</dependency>
</dependencies>
<repositories>

View File

@ -0,0 +1,608 @@
package cn.axzo.pokonyan.client;
import cn.axzo.pokonyan.exception.BizResultCode;
import cn.axzo.pokonyan.exception.BusinessException;
import cn.axzo.pokonyan.exception.VarParamFormatter;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONPath;
import com.alibaba.fastjson.util.TypeUtils;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static cn.axzo.pokonyan.client.DataSheetClient.CellMeta.EXT_KEY_DATETIME_FORMAT;
import static cn.axzo.pokonyan.client.DataSheetClient.CellMeta.EXT_KEY_RANGE_LOWER_TYPE;
import static cn.axzo.pokonyan.client.DataSheetClient.CellMeta.EXT_KEY_RANGE_UPPER_TYPE;
/**
* 用于发送sms & email验证码, 或者获取图片验证码. 并提供校验验证码是否正确的能力
*/
public interface DataSheetClient {
/**
* 数据导入的builder
*
* @return
*/
ImporterBuilder importBuilder();
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(fluent = true)
abstract class ImporterBuilder {
/**
* 导入的场景名称, 必填
*/
@NonNull
private String scene;
/**
* 导入格式
*/
@NonNull
private ImportFormat format;
/**
* EXCEL格式的sheetName
*/
private String tableName;
private Meta meta;
private boolean debugEnabled;
/**
* 解析结果ImportResp中的lines是否包含解析失败的行
* true: 解析失败的时候不会抛异常会在每行的JSONObject中添加
* "errors":[{"columnIndex":1,"columnKey":"","columnName":"","rawValue":"","errorCode":"","errorMsg":""}]
* false: 解析失败的时候抛第一个异常
*/
private boolean includeLineErrors;
/**
* 允许最大的导入行数
*/
private Integer allowMaxLineCount;
public abstract Importer build();
}
interface Importer {
/**
* 所有字段读取为String类型
*
* @param inputStream
* @return
*/
ImportResp<JSONObject> readAll(InputStream inputStream);
}
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
class ImportResp<T> {
/**
* 导入的场景名称
*/
private String scene;
/**
* 模版对应的code以及版本
*/
private String templateCode;
private String version;
/**
* headers
*/
private List<String> headers;
/**
* 每一行的数据
*/
private List<T> lines;
/**
* header的行数
*/
private Integer headerRowCount;
/**
* 导入数据总行数, 不包含header
*/
private Integer rowCount;
/**
* 导入数据列数
*/
private Integer columnCount;
/**
* Meta数据
*/
private Meta meta;
/**
* 消耗的时间
*/
private Long elapsedMillis;
}
@AllArgsConstructor
@Getter
enum ImportFormat {
EXCEL("xlsx"),
// TODO: 支持其他类型
// CSV("csv")
;
private String suffix;
}
@Data
class ExportField {
private String column;
private String header;
private CellMeta.Type type;
private Boolean mandatory;
private String lowerType;
private String upperType;
private String dateTimeFormat;
private List<Object> options;
private BiFunction<JSONObject, Integer, Object> reader;
@Builder
public ExportField(String column, String header, CellMeta.Type type, Boolean mandatory,
Function<String, Object> resultConverter, BiFunction<JSONObject, Integer, Object> reader,
List<Object> options) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(column), "column不能为空");
Preconditions.checkArgument(!Strings.isNullOrEmpty(header), "header不能为空");
Preconditions.checkArgument(!(resultConverter != null && reader != null), "reader & resultConverter不能同时存在");
this.column = column;
this.header = header;
this.type = Optional.ofNullable(type).orElse(CellMeta.Type.STRING);
this.mandatory = Optional.ofNullable(mandatory).orElse(Boolean.FALSE);
if (reader != null) {
this.reader = reader;
} else {
this.reader = Optional.ofNullable(resultConverter)
.map(e -> (BiFunction<JSONObject, Integer, Object>) (row, integer) -> e.apply(Strings.nullToEmpty(row.getString(column))))
.orElseGet(() -> DataSheetClient.stringCellReader(column));
}
this.options = options;
}
public CellMeta toCellMeta() {
JSONObject params = new JSONObject();
if (CellMeta.Type.RANGE == type) {
params.put(EXT_KEY_RANGE_LOWER_TYPE, lowerType);
params.put(EXT_KEY_RANGE_UPPER_TYPE, upperType);
}
if (CellMeta.Type.DATE == type || CellMeta.Type.DATETIME == type) {
params.put(EXT_KEY_DATETIME_FORMAT, dateTimeFormat);
}
return CellMeta.builder()
.key(column).name(header).mandatory(mandatory).type(type)
.params(params).options(options).build();
}
}
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
class Meta {
public static final String PATTERN_VERSION_OPEN = "$V{";
public static final String PATTERN_CELL_OPEN = "$C{";
public static final String PATTERN_IGNORE_OPEN = "$I{";
public static final String PATTERN_EXTRA_OPEN = "$E{";
public static final String PATTERN_CLOSE = "}";
public static final String RANGE_TYPE_OPEN_OPEN = "1";
public static final String RANGE_TYPE_OPEN_CLOSED = "2";
public static final String RANGE_TYPE_CLOSED_OPEN = "3";
public static final String RANGE_TYPE_CLOSED_CLOSED = "4";
public static final String IGNORE_ROW_KEY = "r";
public static final String IGNORE_COLUMN_KEY = "c";
public static final String IGNORE_ROW_AND_COLUMN_KEY = "b";
private String templateCode;
private String version;
private List<CellMeta> cellMetas;
/**
* 忽略掉的行号
*/
private List<Integer> ignoreRowIndexes;
/**
* 忽略掉的列号
*/
private List<Integer> ignoreColumnIndexes;
public List<Integer> getIgnoreRowIndexes() {
return Optional.ofNullable(ignoreRowIndexes).orElseGet(ImmutableList::of);
}
public List<Integer> getIgnoreColumnIndexes() {
return Optional.ofNullable(ignoreColumnIndexes).orElseGet(ImmutableList::of);
}
}
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
class CellMeta {
public static final String EXT_KEY_DATETIME_FORMAT = "dateTimeFormat";
public static final String EXT_KEY_RANGE_LOWER_TYPE = "lowerType";
public static final String EXT_KEY_RANGE_UPPER_TYPE = "upperType";
public static final String RANGE_BOUND_TYPE_OPEN = "open";
public static final String RANGE_BOUND_TYPE_CLOSED = "closed";
public static final Set<String> RANGE_TYPES = ImmutableSet.of(RANGE_BOUND_TYPE_OPEN, RANGE_BOUND_TYPE_CLOSED);
private static final List<String> DEFAULT_DATE_FORMATS = ImmutableList.of(
"yyyy-MM-dd",
"yyyy-MM-dd HH:mm",
"yyyy-MM-dd HH:mm:ss",
"yyyy.MM.dd",
"yyyy.MM.dd HH:mm",
"yyyy.MM.dd HH:mm:ss",
"yyyy年MM月dd日",
"yyyy年MM月dd日 HH:mm",
"yyyy年MM月dd日 HH:mm:ss",
"yyyy年MM月dd日 HH时mm分",
"yyyy年MM月dd日 HH时mm分ss秒",
"yyyy/MM/dd",
"yyyy/MM/dd HH:mm",
"yyyy/MM/dd HH:mm:ss",
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd'T'HH:mm:ssZ",
"yyyy-MM-dd'T'HH:mm:ssz",
"yyyy-MM-dd'T'HH:mm:ss",
"MM/dd/yyyy HH:mm:ss a",
"yyyyMMddHHmmss",
"yyyyMMdd"
);
private static final Map<String, Boolean> BOOLEAN_MAP = ImmutableMap.<String, Boolean>builder()
.put("", Boolean.TRUE)
.put("yes", Boolean.TRUE)
.put("y", Boolean.TRUE)
.put("1", Boolean.TRUE)
.put("", Boolean.FALSE)
.put("no", Boolean.FALSE)
.put("n", Boolean.FALSE)
.put("0", Boolean.FALSE)
.build();
/**
* cell的key
*/
private String key;
/**
* cell的header一般为中文
*/
private String name;
/**
* 类型
*/
private Type type;
/**
* 是否必需
*/
private Boolean mandatory;
/**
* 可选值
*/
private List<Object> options;
/**
* 存放一些和type相关的参数例如DateTime的pattern格式Range的开闭区间
*/
private JSONObject params;
/**
* 存放一些额外的信息
*/
private JSONObject ext;
/**
* 导入的时候将原始的String转换为特定的类型
*/
private Function<String, Object> importConverter;
public void validate() {
if (type == Type.RANGE
&& (!RANGE_TYPES.contains(params.getString(EXT_KEY_RANGE_LOWER_TYPE))
|| !RANGE_TYPES.contains(params.getString(EXT_KEY_RANGE_UPPER_TYPE)))) {
throw ResultCode.IMPORT_PARSE_CELL_META_RANGE_TYPE_ERROR.toException();
}
}
public Object convertType(String value) {
if (importConverter != null) {
return importConverter.apply(value);
}
if (Boolean.TRUE.equals(mandatory) && Strings.isNullOrEmpty(value)) {
throw ResultCode.IMPORT_CELL_META_MISSING_MANDATORY_VALUE.toException();
}
// 非必填的字段如果为空直接返回
if (value == null) {
return type == Type.STRING ? StringUtils.EMPTY : null;
}
switch (type) {
case INT:
return Integer.valueOf(value);
case LONG:
return Long.valueOf(value);
case FLOAT:
return Float.valueOf(value);
case DOUBLE:
return Double.valueOf(value);
case BIG_DECIMAL:
return new BigDecimal(value);
case DATE:
case DATETIME:
return parseDate(value, DEFAULT_DATE_FORMATS);
case RANGE:
// 格式 100-200
List<Integer> values = Splitter.on("-").omitEmptyStrings().trimResults().splitToList(value)
.stream()
.map(Integer::valueOf)
.collect(Collectors.toList());
if (values.size() != 2) {
throw ResultCode.IMPORT_CELL_RANGE_FORMAT_ERROR.toException();
}
if (values.get(0) >= values.get(1)) {
throw ResultCode.IMPORT_CELL_RANGE_VALUE_ERROR.toException();
}
return new JSONObject()
.fluentPut("lower", values.get(0))
.fluentPut("upper", values.get(1))
.fluentPut(EXT_KEY_RANGE_LOWER_TYPE, params.getString(EXT_KEY_RANGE_LOWER_TYPE))
.fluentPut(EXT_KEY_RANGE_UPPER_TYPE, params.getString(EXT_KEY_RANGE_UPPER_TYPE));
case BOOLEAN:
return Optional.ofNullable(BOOLEAN_MAP.get(value.toLowerCase()))
.orElseThrow(ResultCode.IMPORT_CELL_BOOLEAN_VALUE_ERROR::toException);
default:
return Strings.nullToEmpty(value);
}
}
private LocalDateTime parseDate(String dateStr, List<String> formats) {
try {
Date date = DateUtils.parseDate(dateStr, formats.toArray(new String[0]));
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
} catch (Exception e) {
throw ResultCode.IMPORT_CELL_DATETIME_CONVERT_FAILED.toException("不支持的日期格式{}", dateStr);
}
}
@AllArgsConstructor
public enum Type {
INT("00"),
LONG("01"),
FLOAT("02"),
DOUBLE("03"),
STRING("04"),
BIG_DECIMAL("05"),
DATE("06"),
DATETIME("07"),
RANGE("08"),
BOOLEAN("09")
;
@Getter
private String code;
private static Map<String, Type> map = Stream.of(Type.values())
.collect(Collectors.toMap(Type::getCode, Function.identity()));
public static Type from(String code) {
return map.get(code);
}
}
}
@AllArgsConstructor
@Getter
enum ExportFormat {
EXCEL("application/vnd.ms-excel", ".xlsx"),
CSV("text/csv", ".csv");
private String contentType;
private String suffix;
}
static BiFunction<JSONObject, Integer, Object> indexCellReader() {
return (row, index) -> String.valueOf(index + 1);
}
static BiFunction<JSONObject, Integer, Object> stringCellReader(String columnName) {
return (row, index) -> Strings.nullToEmpty(row.getString(columnName));
}
static BiFunction<JSONObject, Integer, Object> dateTimeCellReader(String columnName, String pattern) {
return (row, index) -> {
Long epochMillis = row.getLong(columnName);
if (epochMillis == null) {
return StringUtils.EMPTY;
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
Instant instant = Instant.ofEpochMilli(epochMillis);
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
return formatter.format(localDateTime);
};
}
static BiFunction<JSONObject, Integer, Object> bigDecimalCellReader(String columnName) {
return bigDecimalCellReader(columnName, 2);
}
static BiFunction<JSONObject, Integer, Object> bigDecimalCellReader(String columnName, int scale) {
return (row, index) -> {
BigDecimal number = row.getBigDecimal(columnName);
if (number == null) {
number = BigDecimal.ZERO;
}
return number.stripTrailingZeros().setScale(scale, RoundingMode.HALF_UP).toPlainString();
};
}
static BiFunction<JSONObject, Integer, Object> jsonPathCellReader(String jsonPath) {
return (row, index) -> JSONPath.eval(row, jsonPath);
}
static BiFunction<JSONObject, Integer, Object> jsonPathBigDecimalCellReader(String jsonPath) {
return jsonPathBigDecimalCellReader(jsonPath, 2);
}
static BiFunction<JSONObject, Integer, Object> jsonPathBigDecimalCellReader(String jsonPath, int scale) {
return (row, index) -> {
Object val = JSONPath.eval(row, jsonPath);
if (val == null) {
return BigDecimal.ZERO;
}
return TypeUtils.castToBigDecimal(val).stripTrailingZeros().setScale(scale, RoundingMode.HALF_UP).toPlainString();
};
}
@Getter
class DataSheetException extends BusinessException {
private String subErrorCode;
/** 异常相关的行号列号0开始 */
private Integer rowIndex;
private Integer columnIndex;
/** 异常相关的列名称,中文 */
private List<String> columnNames;
public DataSheetException(ResultCode resultCode) {
super(BizResultCode.SYSTEM_PARAM_NOT_VALID_EXCEPTION.getErrorCode(), resultCode.getMessage());
this.subErrorCode = resultCode.getSubBizCode();
}
public DataSheetException(ResultCode resultCode, Integer rowIndex, Integer columnIndex) {
super(BizResultCode.SYSTEM_PARAM_NOT_VALID_EXCEPTION.getErrorCode(), resultCode.getMessage());
this.subErrorCode = resultCode.getSubBizCode();
this.rowIndex = rowIndex;
this.columnIndex = columnIndex;
}
public DataSheetException(ResultCode resultCode, List<String> columnNames) {
super(BizResultCode.SYSTEM_PARAM_NOT_VALID_EXCEPTION.getErrorCode(), resultCode.getMessage());
this.subErrorCode = resultCode.getSubBizCode();
this.columnNames = columnNames;
}
public DataSheetException(ResultCode resultCode, String message) {
super(BizResultCode.SYSTEM_PARAM_NOT_VALID_EXCEPTION.getErrorCode(), message);
this.subErrorCode = resultCode.getSubBizCode();
}
public DataSheetException(String subErrorCode, String errorMsg, Integer rowIndex, Integer columnIndex) {
super(BizResultCode.SYSTEM_PARAM_NOT_VALID_EXCEPTION.getErrorCode(), errorMsg);
this.subErrorCode = subErrorCode;
this.rowIndex = rowIndex;
this.columnIndex = columnIndex;
}
public DataSheetException(String errorCode, String subErrorCode, String errorMsg) {
super(errorCode, errorMsg);
this.subErrorCode = subErrorCode;
}
}
@AllArgsConstructor
@Getter
enum ResultCode {
/** 解析Excel的批注时相关的errorCode*/
IMPORT_PARSE_MISSING_VERSION("C00", "批注中没有找到版本信息"),
IMPORT_PARSE_VERSION_FORMAT_ERROR("C01", "批注中的版本格式不对"),
IMPORT_PARSE_CELL_META_MISSING_TYPE("C02", "批注中的字段没有找到类型信息"),
IMPORT_PARSE_CELL_META_FORMAT_ERROR("C03", "批注中的字段格式不对"),
IMPORT_PARSE_CELL_META_RANGE_FORMAT_ERROR("C04", "批注中的范围类型字段的格式不对"),
IMPORT_PARSE_CELL_META_RANGE_TYPE_ERROR("C05", "批注中的范围类型字段的区间类型不对"),
/** 导入数据时相关的errorCode*/
IMPORT_LINES_REACHED_LIMIT("C20", "导入的数据超过了最大行数"),
IMPORT_COLUMN_RANGE_MISSING_TYPES("C21", "范围类型缺少区间类型的定义:开区间还是闭区间"),
IMPORT_COLUMN_DUPLICATED_NAME("C22", "列的名称不能重复"),
IMPORT_COLUMN_MISSING_CELL_META("C23", "字段缺少类型定义"),
IMPORT_COLUMN_NAME_NOT_MATCHED("C24", "字段的名称与类型定义的名称不一致"),
IMPORT_CELL_RANGE_FORMAT_ERROR("C25", "范围类型的格式不对"),
IMPORT_CELL_RANGE_VALUE_ERROR("C26", "范围类型的下限值必须小于上限值"),
IMPORT_CELL_DATETIME_CONVERT_FAILED("C27", "时间类型解析失败"),
IMPORT_CELL_CONVERT_FAILED("C28", "类型转换失败"),
IMPORT_CELL_META_MISSING_MANDATORY_VALUE("C29", "必填字段不能为空"),
IMPORT_CELL_BOOLEAN_VALUE_ERROR("C30", "布尔类型的值不支持"),
;
private String subBizCode;
private String message;
public DataSheetException toException() {
return new DataSheetException(this);
}
public DataSheetException toException(String message) {
return new DataSheetException(this, message);
}
public DataSheetException toException(List<String> columnNames) {
return new DataSheetException(this, columnNames);
}
public DataSheetException toException(Integer rowIndex, Integer columnIndex) {
return new DataSheetException(this, rowIndex, columnIndex);
}
public DataSheetException toException(String customMsg, Object... objects) {
if (objects == null) {
return toException(customMsg);
}
String msg = VarParamFormatter.format(customMsg, objects);
//如果最后一个参数是Throwable. 则将SimpleName附加到msg中
if (objects[objects.length - 1] instanceof Throwable) {
Throwable throwable = (Throwable) objects[objects.length - 1];
msg = String.format("%s (%s)", msg, throwable.getClass().getSimpleName());
}
return toException(msg);
}
}
}

View File

@ -0,0 +1,54 @@
package cn.axzo.pokonyan.client.impl;
import cn.axzo.pokonyan.client.DataSheetClient;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
@Slf4j
public class DataSheetClientImpl implements DataSheetClient {
@Override
public ImporterBuilder importBuilder() {
return new DataSheetImporter();
}
@Data
@lombok.Builder
@NoArgsConstructor
@AllArgsConstructor
private static class ReportReq {
String appName;
String scene;
ReportReq.Action action;
Map resp;
Long rowCount;
Long elapsedMillis;
String filePath;
String operatorId;
String operatorName;
String operatorTenantId;
public enum Action {
IMPORT,
EXPORT;
}
}
public static Builder builder() {
return new Builder();
}
@Data
public static class Builder {
public DataSheetClient build() {
return new DataSheetClientImpl();
}
}
}

View File

@ -0,0 +1,536 @@
package cn.axzo.pokonyan.client.impl;
import cn.axzo.pokonyan.client.DataSheetClient;
import cn.axzo.pokonyan.exception.BusinessException;
import cn.axzo.pokonyan.util.Regex;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.enums.CellExtraTypeEnum;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.metadata.CellExtra;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.io.InputStream;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static cn.axzo.pokonyan.client.DataSheetClient.CellMeta.EXT_KEY_RANGE_LOWER_TYPE;
import static cn.axzo.pokonyan.client.DataSheetClient.CellMeta.EXT_KEY_RANGE_UPPER_TYPE;
import static cn.axzo.pokonyan.client.DataSheetClient.CellMeta.RANGE_BOUND_TYPE_CLOSED;
import static cn.axzo.pokonyan.client.DataSheetClient.CellMeta.RANGE_BOUND_TYPE_OPEN;
import static cn.axzo.pokonyan.client.DataSheetClient.Meta.IGNORE_COLUMN_KEY;
import static cn.axzo.pokonyan.client.DataSheetClient.Meta.IGNORE_ROW_AND_COLUMN_KEY;
import static cn.axzo.pokonyan.client.DataSheetClient.Meta.IGNORE_ROW_KEY;
import static cn.axzo.pokonyan.client.DataSheetClient.Meta.PATTERN_CELL_OPEN;
import static cn.axzo.pokonyan.client.DataSheetClient.Meta.PATTERN_CLOSE;
import static cn.axzo.pokonyan.client.DataSheetClient.Meta.PATTERN_EXTRA_OPEN;
import static cn.axzo.pokonyan.client.DataSheetClient.Meta.PATTERN_IGNORE_OPEN;
import static cn.axzo.pokonyan.client.DataSheetClient.Meta.PATTERN_VERSION_OPEN;
import static cn.axzo.pokonyan.client.DataSheetClient.Meta.RANGE_TYPE_CLOSED_CLOSED;
import static cn.axzo.pokonyan.client.DataSheetClient.Meta.RANGE_TYPE_CLOSED_OPEN;
import static cn.axzo.pokonyan.client.DataSheetClient.Meta.RANGE_TYPE_OPEN_CLOSED;
import static cn.axzo.pokonyan.client.DataSheetClient.Meta.RANGE_TYPE_OPEN_OPEN;
import static cn.axzo.pokonyan.client.DataSheetClient.ResultCode.IMPORT_CELL_CONVERT_FAILED;
/**
* 使用ali的EasyExcel来读入excel/csv文件
*
* @author yuanyi
*/
@Slf4j
public class DataSheetImporter extends DataSheetClient.ImporterBuilder {
private static final String LINE_KEY_ERRORS = "errors";
private static final String LINE_KEY_ROW_INDEX = "rowIndex";
/**
* 完成后处理事件.
*/
@Setter
private Consumer<DataSheetClient.ImportResp> onCompleted;
@Override
public DataSheetClient.Importer build() {
Preconditions.checkArgument(this.scene() != null, "scene不能为空");
Preconditions.checkArgument(this.format() != null, "format不能为空");
// TODO: 当支持更多format的时候需要生成对应的Importer实例
return ExcelImporter.builder()
.scene(scene())
.tableName(tableName())
.meta(meta())
.debugEnabled(debugEnabled())
.allowMaxLineCount(allowMaxLineCount())
.includeLineErrors(includeLineErrors())
.onCompleted(onCompleted)
.build();
}
@Builder
private static class ExcelImporter implements DataSheetClient.Importer {
private static final Map<String, List<String>> rangeBoundTypes = ImmutableMap.of(
RANGE_TYPE_OPEN_OPEN, ImmutableList.of(RANGE_BOUND_TYPE_OPEN, RANGE_BOUND_TYPE_OPEN),
RANGE_TYPE_OPEN_CLOSED, ImmutableList.of(RANGE_BOUND_TYPE_OPEN, RANGE_BOUND_TYPE_CLOSED),
RANGE_TYPE_CLOSED_OPEN, ImmutableList.of(RANGE_BOUND_TYPE_CLOSED, RANGE_BOUND_TYPE_OPEN),
RANGE_TYPE_CLOSED_CLOSED, ImmutableList.of(RANGE_BOUND_TYPE_CLOSED, RANGE_BOUND_TYPE_CLOSED));
private String scene;
private String tableName;
private DataSheetClient.Meta meta;
private boolean debugEnabled;
private Integer allowMaxLineCount;
private boolean includeLineErrors;
private Consumer<DataSheetClient.ImportResp> onCompleted;
@Override
public DataSheetClient.ImportResp<JSONObject> readAll(InputStream inputStream) {
Stopwatch stopwatch = Stopwatch.createStarted();
NoModelDataListener dataListener = new NoModelDataListener();
EasyExcel.read(inputStream, dataListener).extraRead(CellExtraTypeEnum.COMMENT).sheet(tableName).doRead();
if (allowMaxLineCount != null && dataListener.lines.size() > allowMaxLineCount) {
throw DataSheetClient.ResultCode.IMPORT_LINES_REACHED_LIMIT
.toException("导入的数据超过了最大行数" + allowMaxLineCount);
}
// 聚合来自入参和文件批注的meta信息如果都存在使用入参的meta覆盖文件批注的meta
this.meta = mergeMeta(this.meta, parseMetaFromData(dataListener));
filterHeadMap(dataListener);
filterLines(dataListener);
validateHeaders(dataListener);
validateMeta(dataListener);
List<String> headers = parseHeaders(dataListener);
List<JSONObject> lines = parseLines(dataListener);
if (!includeLineErrors) {
// 没有设置includeLineErrors时抛第一个发现的异常
Optional<JSONObject> errorLine = lines.stream()
.filter(line -> line.containsKey(LINE_KEY_ERRORS))
.findFirst();
if (errorLine.isPresent()) {
JSONObject error = errorLine.get().getJSONArray(LINE_KEY_ERRORS).getJSONObject(0);
Integer rowIndex = lines.indexOf(errorLine.get());
Integer columnIndex = error.getInteger("columnIndex");
String errMsg = String.format("第%d行, 第%d列字段[%s]%s", rowIndex + 1, columnIndex + 1,
error.getString("columnName"), error.getString("errorMsg"));
throw new DataSheetClient.DataSheetException(error.getString("subErrorCode"), errMsg, rowIndex, columnIndex);
}
}
DataSheetClient.ImportResp resp = DataSheetClient.ImportResp.<JSONObject>builder()
.scene(scene)
.templateCode(Optional.ofNullable(meta).map(DataSheetClient.Meta::getTemplateCode).orElse(null))
.version(Optional.ofNullable(meta).map(DataSheetClient.Meta::getVersion).orElse(null))
.headers(headers)
.lines(lines)
.headerRowCount(1)
.rowCount(lines.size())
.columnCount(headers.size())
.meta(meta)
.elapsedMillis(stopwatch.elapsed(TimeUnit.MILLISECONDS))
.build();
if (null != onCompleted) {
onCompleted.accept(resp);
}
return resp;
}
private void filterHeadMap(NoModelDataListener dataListener) {
if (meta == null) {
return;
}
Map<Integer, String> headMap = dataListener.getHeadMap();
meta.getIgnoreColumnIndexes().forEach(headMap::remove);
}
private void filterLines(NoModelDataListener dataListener) {
if (meta == null) {
return;
}
Set<Integer> toRemoveLines = ImmutableSet.copyOf(meta.getIgnoreRowIndexes());
List<Map<Integer, String>> lines = dataListener.getLines();
dataListener.setLines(IntStream.range(0, lines.size())
// 这里要加1是因为removeIndex是整个文档的行数来计算包括了header
// 但是lines的数据已经排除了header
.filter(index -> !toRemoveLines.contains(index + 1))
.mapToObj(lines::get)
.collect(Collectors.toList()));
}
private DataSheetClient.Meta parseMetaFromData(NoModelDataListener dataListener) {
Map<Integer, String> headMap = dataListener.getHeadMap();
List<CellExtra> headComments = dataListener.getCellComments().stream()
// 获取第一行Head的批注信息
.filter(cellExtra -> cellExtra.getRowIndex() == 0)
// 过滤带有Cell参数配置的批注信息
.filter(cellExtra -> !Strings.isNullOrEmpty(StringUtils.substringBetween(cellExtra.getText(),
PATTERN_CELL_OPEN, PATTERN_CLOSE)))
// 排序方便找到第一列获取templateCode与version
.sorted(Comparator.comparing(CellExtra::getColumnIndex))
.collect(Collectors.toList());
// 没有批注信息直接返回null会以String来解析值
if (headComments.isEmpty()) {
return null;
}
// 从文件中解析meta信息
JSONObject codeAndVersion = parseTemplateCodeAndVersion(headComments);
List<DataSheetClient.CellMeta> cellMetas = headComments.stream()
.map(cellExtra -> parseCellMeta(cellExtra.getText(), headMap.get(cellExtra.getColumnIndex())))
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (cellMetas.isEmpty()) {
return null;
}
return DataSheetClient.Meta.builder()
.templateCode(codeAndVersion.getString("templateCode"))
.version(codeAndVersion.getString("version"))
.cellMetas(cellMetas)
.ignoreColumnIndexes(parseIgnoreColumns(dataListener))
.ignoreRowIndexes(parseIgnoreRows(dataListener))
.build();
}
private DataSheetClient.Meta mergeMeta(DataSheetClient.Meta metaFromParam, DataSheetClient.Meta metaFromHeader) {
if (metaFromHeader == null || metaFromParam == null) {
return metaFromHeader == null ? metaFromParam : metaFromHeader;
}
// 在两者都存在的时候以文件的meta为准仅仅将入参的CellMetas覆盖文件的CellMetas
if (metaFromHeader.getCellMetas() == null) {
metaFromHeader.setCellMetas(metaFromParam.getCellMetas());
return metaFromHeader;
}
// 使用入参中定义的列信息覆盖文件的cellMetas
Map<String, DataSheetClient.CellMeta> cellMetaMap =
Maps.uniqueIndex(metaFromParam.getCellMetas(), DataSheetClient.CellMeta::getKey);
List<DataSheetClient.CellMeta> cellMetas = metaFromHeader.getCellMetas().stream()
.map(cellMeta -> cellMetaMap.getOrDefault(cellMeta.getKey(), cellMeta))
.collect(Collectors.toList());
metaFromHeader.setCellMetas(cellMetas);
return metaFromHeader;
}
private List<Integer> parseIgnoreRows(NoModelDataListener dataListener) {
return dataListener.getCellComments().stream()
.map(cellExtra -> {
String ignoreKey = StringUtils.substringBetween(cellExtra.getText(),
PATTERN_IGNORE_OPEN, PATTERN_CLOSE);
if (IGNORE_ROW_KEY.equals(ignoreKey) || IGNORE_ROW_AND_COLUMN_KEY.equals(ignoreKey)) {
return cellExtra.getRowIndex();
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
private List<Integer> parseIgnoreColumns(NoModelDataListener dataListener) {
return dataListener.getCellComments().stream()
.map(cellExtra -> {
String ignoreKey = StringUtils.substringBetween(cellExtra.getText(),
PATTERN_IGNORE_OPEN, PATTERN_CLOSE);
if (IGNORE_COLUMN_KEY.equals(ignoreKey) || IGNORE_ROW_AND_COLUMN_KEY.equals(ignoreKey)) {
return cellExtra.getColumnIndex();
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
public JSONObject parseTemplateCodeAndVersion(List<CellExtra> headComments) {
// 从整个头部的批注获取模版code和版本信息
Optional<String> parsedText = headComments.stream()
.map(comment -> StringUtils.substringBetween(comment.getText(), PATTERN_VERSION_OPEN, PATTERN_CLOSE))
.filter(str -> !Strings.isNullOrEmpty(str))
.findFirst();
if (!parsedText.isPresent()) {
throw DataSheetClient.ResultCode.IMPORT_PARSE_MISSING_VERSION.toException();
}
// 格式为"CODE_VERSION"
List<String> values = Splitter.on("_").splitToList(parsedText.get());
if (values.size() != 2) {
throw DataSheetClient.ResultCode.IMPORT_PARSE_VERSION_FORMAT_ERROR.toException();
}
return new JSONObject()
.fluentPut("templateCode", values.get(0))
.fluentPut("version", values.get(1));
}
private DataSheetClient.CellMeta parseCellMeta(String text, String name) {
String value = StringUtils.substringBetween(text, PATTERN_CELL_OPEN, PATTERN_CLOSE);
if (Strings.isNullOrEmpty(value)) {
throw DataSheetClient.ResultCode.IMPORT_PARSE_CELL_META_MISSING_TYPE
.toException("没有找到列[{}]的类型信息", name);
}
List<String> values = Splitter.on("_").splitToList(value);
// 格式 key_type_mandatory_params...
if (values.size() < 3) {
throw DataSheetClient.ResultCode.IMPORT_PARSE_CELL_META_FORMAT_ERROR
.toException("列[{}]类型的批注格式不对[{}]", name, text);
}
DataSheetClient.CellMeta.Type type = DataSheetClient.CellMeta.Type.from(values.get(1));
JSONObject params = new JSONObject();
if (type == DataSheetClient.CellMeta.Type.RANGE) {
if (values.size() != 4 || Strings.isNullOrEmpty(values.get(3))) {
throw DataSheetClient.ResultCode.IMPORT_PARSE_CELL_META_RANGE_FORMAT_ERROR
.toException("列[{}]范围类型的批注格式不对[{}]", name, text);
}
List<String> boundType = rangeBoundTypes.get(values.get(3));
if (boundType == null) {
throw DataSheetClient.ResultCode.IMPORT_PARSE_CELL_META_RANGE_TYPE_ERROR
.toException(String.format("列[{}]范围类型的值不对", name));
}
params.put(EXT_KEY_RANGE_LOWER_TYPE, boundType.get(0));
params.put(EXT_KEY_RANGE_UPPER_TYPE, boundType.get(1));
}
JSONObject ext = new JSONObject();
value = StringUtils.substringBetween(text, PATTERN_EXTRA_OPEN, PATTERN_CLOSE);
if (!Strings.isNullOrEmpty(value)) {
ext.putAll(Splitter.on("&").withKeyValueSeparator("=").split(value));
}
return DataSheetClient.CellMeta.builder()
.key(values.get(0))
.name(name)
.type(type)
.mandatory("1".equals(values.get(2)))
.params(params)
.ext(ext)
.build();
}
private List<String> parseHeaders(NoModelDataListener dataListener) {
return dataListener.getHeadMap().keySet().stream()
.sorted()
.map(key -> Strings.nullToEmpty(dataListener.getHeadMap().get(key)))
.collect(Collectors.toList());
}
private List<JSONObject> parseLines(NoModelDataListener dataListener) {
// 如果没有找到meta信息按照key=header(很可能是中文), 类型就为String
if (this.meta == null) {
Map<Integer, String> headerMap = dataListener.getHeadMap();
return dataListener.getLines().stream()
.map(line -> new JSONObject().fluentPutAll(line.entrySet().stream()
.map(entry -> Pair.of(headerMap.get(entry.getKey()), Strings.nullToEmpty(entry.getValue())))
.collect(Collectors.toMap(Pair::getKey, Pair::getValue))))
.collect(Collectors.toList());
}
// 根据meta来校验cell的类型
Map<Integer, String> headerMap = dataListener.getHeadMap();
Map<String, DataSheetClient.CellMeta> cellMetaMap = Maps.uniqueIndex(meta.getCellMetas(),
DataSheetClient.CellMeta::getName);
List<Map<Integer, String>> lines = dataListener.getLines();
return IntStream.range(0, lines.size())
.mapToObj(lineIndex -> {
Map<Integer, String> line = lines.get(lineIndex);
// 收集每一行每一列的转换结果
Map<Boolean, List<ColumnConvertResp>> convertRespMap = headerMap.entrySet().stream()
.map(entry -> {
Integer columnIndex = entry.getKey();
String header = entry.getValue();
DataSheetClient.CellMeta cellMeta = cellMetaMap.get(header);
return convertType(cellMeta, line.get(columnIndex), lineIndex, columnIndex);
})
.collect(Collectors.groupingBy(ColumnConvertResp::getSuccess));
JSONObject convertedLine = new JSONObject()
.fluentPutAll(convertRespMap.getOrDefault(Boolean.TRUE, ImmutableList.of()).stream()
// convertValue可能为null, 有非必需的字段
.collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getConvertedValue()), HashMap::putAll));
if (convertRespMap.get(Boolean.FALSE) != null) {
// 转换失败的将失败信息放到errors字段中
convertedLine.put(LINE_KEY_ERRORS, convertRespMap.get(Boolean.FALSE).stream()
.map(ColumnConvertResp::getError)
.collect(Collectors.toList()));
}
convertedLine.put(LINE_KEY_ROW_INDEX, lineIndex);
return convertedLine;
})
.collect(Collectors.toList());
}
private ColumnConvertResp convertType(DataSheetClient.CellMeta cellMeta, String rawValue, int rowIndex, int columnIndex) {
try {
return ColumnConvertResp.builder()
.success(true).cellMeta(cellMeta)
.rawValue(rawValue).convertedValue(cellMeta.convertType(rawValue))
.build();
} catch (BusinessException e) {
log.error("failed to convert type, cellMeta={}, rawValue={}, rowIndex={}, columnIndex={}",
cellMeta, rawValue, rowIndex, columnIndex, e);
String subErrorCode = null;
if (e instanceof DataSheetClient.DataSheetException) {
subErrorCode = ((DataSheetClient.DataSheetException) e).getSubErrorCode();
}
return ColumnConvertResp.builder()
.success(false).cellMeta(cellMeta).rawValue(rawValue)
.columnIndex(columnIndex)
.errorCode(e.getErrorCode())
.errorMsg(e.getErrorMsg())
.subErrorCode(subErrorCode)
.build();
} catch (Exception e) {
log.error("failed to convert type, cellMeta={}, rawValue={}, rowIndex={}, columnIndex={}",
cellMeta, rawValue, rowIndex, columnIndex, e);
String errMsg = String.format("第%d行, 第%d列字段[%s]%s", rowIndex + 1, columnIndex + 1,
cellMeta.getName(), Optional.ofNullable(e.getMessage())
.orElse(IMPORT_CELL_CONVERT_FAILED.getMessage()));
throw new DataSheetClient.DataSheetException(IMPORT_CELL_CONVERT_FAILED.getSubBizCode(), errMsg, rowIndex, columnIndex);
}
}
private void validateHeaders(NoModelDataListener dataListener) {
Map<Integer, String> headMap = dataListener.getHeadMap();
Set<String> headerNames = ImmutableSet.copyOf(headMap.values());
if (headerNames.size() != headMap.size()) {
List<String> columnNames = headMap.values().stream()
.collect(Collectors.groupingBy(Function.identity()))
.entrySet().stream()
.filter(grouped -> grouped.getValue().size() > 1)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
throw DataSheetClient.ResultCode.IMPORT_COLUMN_DUPLICATED_NAME.toException(columnNames);
}
}
private void validateMeta(NoModelDataListener dataListener) {
if (this.meta == null) {
return;
}
if (meta.getCellMetas().size() != dataListener.getHeadMap().size()) {
throw DataSheetClient.ResultCode.IMPORT_COLUMN_MISSING_CELL_META.toException();
}
Set<String> headerNames = ImmutableSet.copyOf(dataListener.getHeadMap().values());
Set<String> cellNames = meta.getCellMetas().stream()
.map(DataSheetClient.CellMeta::getName)
.collect(Collectors.toSet());
if (!headerNames.equals(cellNames)) {
Set<String> missingNames = Sets.difference(cellNames, headerNames);
Set<String> redundantNames = Sets.difference(headerNames, cellNames);
List<String> columnNames = Stream.of(missingNames, redundantNames)
.flatMap(Set::stream)
.collect(Collectors.toList());
throw DataSheetClient.ResultCode.IMPORT_COLUMN_NAME_NOT_MATCHED.toException(columnNames);
}
meta.getCellMetas().forEach(DataSheetClient.CellMeta::validate);
}
@Slf4j
@Data
private static class NoModelDataListener extends AnalysisEventListener<Map<Integer, String>> {
private Map<Integer, String> headMap = Maps.newHashMap();
private List<Map<Integer, String>> lines = Lists.newArrayList();
private List<CellExtra> cellComments = Lists.newArrayList();
@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
lines.add(strip(data));
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
this.headMap.putAll(strip(headMap));
}
private Map<Integer, String> strip(Map<Integer, String> data) {
return data.entrySet().stream()
.map(entry -> Maps.immutableEntry(entry.getKey(), StringUtils.strip(entry.getValue(), Regex.WHITESPACE_CHARS)))
// value有可能为null不能直接用Collectors.toMap
.collect(Maps::newHashMap, (m, v) -> m.put(v.getKey(), v.getValue()), HashMap::putAll);
}
/**
* extra是在整个文件被解析后才会调用
*
* @param extra
* @param context
*/
@Override
public void extra(CellExtra extra, AnalysisContext context) {
if (extra.getType() == CellExtraTypeEnum.COMMENT) {
cellComments.add(extra);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
private static class ColumnConvertResp {
private Boolean success;
private DataSheetClient.CellMeta cellMeta;
private String rawValue;
private Object convertedValue;
private Integer columnIndex;
private String errorCode;
private String errorMsg;
private String subErrorCode;
public String getKey() {
return cellMeta.getKey();
}
public JSONObject getError() {
return new JSONObject()
.fluentPut("columnIndex", columnIndex)
.fluentPut("columnKey", cellMeta.getKey())
.fluentPut("columnName", cellMeta.getName())
.fluentPut("rawValue", rawValue)
.fluentPut("errorCode", errorCode)
.fluentPut("errorMsg", errorMsg)
.fluentPut("subErrorCode", subErrorCode);
}
}
}
}

View File

@ -0,0 +1,16 @@
package cn.axzo.pokonyan.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum BizResultCode implements ResultCode {
SYSTEM_PARAM_NOT_VALID_EXCEPTION("001", "参与异常"),;
private String errorCode;
private String errorMessage;
}

View File

@ -0,0 +1,40 @@
package cn.axzo.pokonyan.util;
import org.apache.commons.lang3.StringUtils;
import java.util.regex.Pattern;
/**
* 重用的数据格式验证正则表达式
*/
public class Regex {
public final static String MOBILE_REGEX = "^1\\d{10}$";
public final static String MOBILE_REGEX_MESSAGE = "手机号格式不正确";
public final static Pattern MOBILE_PATTERN = Pattern.compile(MOBILE_REGEX);
public static final String ID_NO_REGEX = "^(\\d{15}$|^\\d{18}$|^\\d{17}(\\d|X|x))$";
public final static String ID_NO_REGEX_MESSAGE = "身份证格式不正确";
public final static Pattern ID_NO_PATTERN = Pattern.compile(ID_NO_REGEX);
public static final String COMPANY_LICENSE_NO_REGEX = "^(\\w{15}|\\w{18})$";
public final static String COMPANY_LICENSE_NO_REGEX_MESSAGE = "营业执照号格式不正确";
public final static Pattern COMPANY_LICENSE_NO_PATTERN = Pattern.compile(COMPANY_LICENSE_NO_REGEX);
public static final String TEL_REGEX = "^((0\\d{2,3})-?)(\\d{7,8})?$";
public final static String TEL_REGEX_MESSAGE = "固定电话格式不正确";
public final static Pattern TEL_PATTERN = Pattern.compile(TEL_REGEX);
public static final String EMAIL_REGEX = "^([-|\\w])+(\\.[-|\\w]+)*@(\\w)+((\\.\\w+)+)$";
public final static String EMAIL_REGEX_MESSAGE = "邮件格式不正确";
public final static Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);
public static final String HTTP_REGEX = "^(http|https)://([\\w.]+/?)\\S*$";
public static final String REAL_NAME_REGEX = "^[\\u4E00-\\u9FA5]{2,32}$|^$";
public static final String REAL_NAME_REGEX_MESSAGE = "姓名格式不正确";
public final static Pattern REAL_NAME_PATTERN = Pattern.compile(REAL_NAME_REGEX);
/**常用的制表符*/
public static final String WHITESPACE_CHARS = "\r\n\0\t\b" + StringUtils.SPACE;
}