|
|
@@ -0,0 +1,428 @@
|
|
|
+package com.goldenwater.common.utils.poi;
|
|
|
+
|
|
|
+import java.io.InputStream;
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+import com.goldenwater.common.annotation.Excel;
|
|
|
+import com.goldenwater.common.utils.DateUtils;
|
|
|
+import com.goldenwater.common.utils.StringUtils;
|
|
|
+import com.goldenwater.common.utils.reflect.ReflectUtils;
|
|
|
+import org.apache.poi.ss.usermodel.*;
|
|
|
+import org.apache.poi.ss.usermodel.DateUtil;
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
|
|
+import org.apache.poi.ss.usermodel.WorkbookFactory;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Excel动态导入工具类
|
|
|
+ * 支持从Excel的指定行读取列名作为变量名,动态映射到实体类字段
|
|
|
+ */
|
|
|
+public class ExcelImportHelper {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(ExcelImportHelper.class);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从Excel文件导入数据,使用指定行作为列名行
|
|
|
+ *
|
|
|
+ * @param is 输入流
|
|
|
+ * @param clazz 目标实体类
|
|
|
+ * @param headerRow 作为列名的行号(从0开始,如2表示第3行)
|
|
|
+ * @param <T> 目标类型
|
|
|
+ * @return 实体列表
|
|
|
+ */
|
|
|
+ public static <T> List<T> importExcel(InputStream is, Class<T> clazz, int headerRow) {
|
|
|
+ List<T> list = new ArrayList<>();
|
|
|
+ try {
|
|
|
+ Workbook wb = WorkbookFactory.create(is);
|
|
|
+ Sheet sheet = wb.getSheetAt(0);
|
|
|
+ if (sheet == null) {
|
|
|
+ throw new RuntimeException("Excel文件sheet不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ int rows = sheet.getLastRowNum();
|
|
|
+ if (rows <= headerRow) {
|
|
|
+ throw new RuntimeException("Excel文件数据行不足");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取表头行(列名行)
|
|
|
+ Row header = sheet.getRow(headerRow);
|
|
|
+ if (header == null) {
|
|
|
+ throw new RuntimeException("Excel文件表头行为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建列名到字段的映射
|
|
|
+ // Map: 列索引 -> 实体字段
|
|
|
+ Map<Integer, Field> columnFieldMap = buildColumnFieldMap(header, clazz);
|
|
|
+
|
|
|
+ // 从headerRow+1开始读取数据行
|
|
|
+ for (int i = headerRow + 1; i <= rows; i++) {
|
|
|
+ Row row = sheet.getRow(i);
|
|
|
+ if (isRowEmpty(row)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ T entity = clazz.getDeclaredConstructor().newInstance();
|
|
|
+ boolean hasData = false;
|
|
|
+
|
|
|
+ for (Map.Entry<Integer, Field> entry : columnFieldMap.entrySet()) {
|
|
|
+ int columnIndex = entry.getKey();
|
|
|
+ Field field = entry.getValue();
|
|
|
+
|
|
|
+ try {
|
|
|
+ Object value = getCellValue(row, columnIndex);
|
|
|
+ if (value != null && !isEmptyValue(value)) {
|
|
|
+ // 根据字段类型转换值
|
|
|
+ Object convertedValue = convertValue(value, field.getType(), field);
|
|
|
+ if (convertedValue != null) {
|
|
|
+ ReflectUtils.invokeSetter(entity, field.getName(), convertedValue);
|
|
|
+ hasData = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("设置字段[" + field.getName() + "]失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (hasData) {
|
|
|
+ list.add(entity);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("Excel导入完成,共导入{}条数据", list.size());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("Excel导入失败", e);
|
|
|
+ throw new RuntimeException("Excel导入失败: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建列索引到实体字段的映射
|
|
|
+ * 支持通过字段名、@Excel注解的name属性进行匹配
|
|
|
+ */
|
|
|
+ private static Map<Integer, Field> buildColumnFieldMap(Row header, Class<?> clazz) {
|
|
|
+ Map<Integer, Field> map = new HashMap<>();
|
|
|
+ Field[] allFields = clazz.getDeclaredFields();
|
|
|
+
|
|
|
+ // 收集实体类所有字段(包括父类)
|
|
|
+ List<Field> allFieldsList = new ArrayList<>(Arrays.asList(allFields));
|
|
|
+ Class<?> superClass = clazz.getSuperclass();
|
|
|
+ while (superClass != null && superClass != Object.class) {
|
|
|
+ allFieldsList.addAll(Arrays.asList(superClass.getDeclaredFields()));
|
|
|
+ superClass = superClass.getSuperclass();
|
|
|
+ }
|
|
|
+
|
|
|
+ for (int colIndex = 0; colIndex < header.getLastCellNum(); colIndex++) {
|
|
|
+ Cell cell = header.getCell(colIndex);
|
|
|
+ if (cell == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String headerName = getCellStringValue(cell);
|
|
|
+ if (StringUtils.isEmpty(headerName)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 去除空格和特殊字符,用于匹配
|
|
|
+ String normalizedHeader = normalizeName(headerName);
|
|
|
+
|
|
|
+ // 尝试匹配字段
|
|
|
+ Field matchedField = findFieldByName(normalizedHeader, allFieldsList);
|
|
|
+ if (matchedField != null) {
|
|
|
+ map.put(colIndex, matchedField);
|
|
|
+ log.debug("列[{}] '{}' -> 字段 '{}'", colIndex, headerName, matchedField.getName());
|
|
|
+ } else {
|
|
|
+ log.debug("列[{}] '{}' 未匹配到字段", colIndex, headerName);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过名称查找字段
|
|
|
+ * 匹配优先级:
|
|
|
+ * 1. 字段名完全匹配(不区分大小写)
|
|
|
+ * 2. 字段名去除下划线匹配
|
|
|
+ * 3. @Excel注解的name属性匹配
|
|
|
+ */
|
|
|
+ private static Field findFieldByName(String headerName, List<Field> fields) {
|
|
|
+ // 1. 字段名精确匹配(不区分大小写)
|
|
|
+ for (Field field : fields) {
|
|
|
+ if (field.getName().equalsIgnoreCase(headerName)) {
|
|
|
+ return field;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 字段名去除下划线匹配
|
|
|
+ String headerNoUnderscore = headerName.replace("_", "");
|
|
|
+ for (Field field : fields) {
|
|
|
+ String fieldNameNoUnderscore = field.getName().replace("_", "");
|
|
|
+ if (fieldNameNoUnderscore.equalsIgnoreCase(headerNoUnderscore)) {
|
|
|
+ return field;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. @Excel注解的name属性匹配
|
|
|
+ for (Field field : fields) {
|
|
|
+ Excel excelAnnotation = field.getAnnotation(Excel.class);
|
|
|
+ if (excelAnnotation != null && StringUtils.isNotEmpty(excelAnnotation.name())) {
|
|
|
+ String annotationName = normalizeName(excelAnnotation.name());
|
|
|
+ if (annotationName.equalsIgnoreCase(headerName) ||
|
|
|
+ annotationName.replace("_", "").equalsIgnoreCase(headerNoUnderscore)) {
|
|
|
+ return field;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 规范化名称:去除空格、统一大小写
|
|
|
+ */
|
|
|
+ private static String normalizeName(String name) {
|
|
|
+ if (name == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ return name.trim().replaceAll("\\s+", "").toLowerCase();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断行是否为空
|
|
|
+ */
|
|
|
+ private static boolean isRowEmpty(Row row) {
|
|
|
+ if (row == null) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ for (int i = 0; i < row.getLastCellNum(); i++) {
|
|
|
+ Cell cell = row.getCell(i);
|
|
|
+ if (cell != null && cell.getCellType() != CellType.BLANK) {
|
|
|
+ String value = getCellStringValue(cell);
|
|
|
+ if (StringUtils.isNotEmpty(value)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取单元格的值(兼容不同类型)
|
|
|
+ */
|
|
|
+ private static Object getCellValue(Row row, int cellIndex) {
|
|
|
+ Cell cell = row.getCell(cellIndex);
|
|
|
+ if (cell == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (cell.getCellType()) {
|
|
|
+ case STRING:
|
|
|
+ return cell.getStringCellValue().trim();
|
|
|
+ case NUMERIC:
|
|
|
+ if (DateUtil.isCellDateFormatted(cell)) {
|
|
|
+ return cell.getDateCellValue();
|
|
|
+ }
|
|
|
+ double numericValue = cell.getNumericCellValue();
|
|
|
+ // 如果是整数,转为Long;否则转为Double
|
|
|
+ if (numericValue == Math.floor(numericValue) && !Double.isInfinite(numericValue)) {
|
|
|
+ return (long) numericValue;
|
|
|
+ }
|
|
|
+ return numericValue;
|
|
|
+ case BOOLEAN:
|
|
|
+ return cell.getBooleanCellValue();
|
|
|
+ case FORMULA:
|
|
|
+ try {
|
|
|
+ return cell.getStringCellValue();
|
|
|
+ } catch (Exception e) {
|
|
|
+ return cell.getNumericCellValue();
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取单元格的字符串值
|
|
|
+ */
|
|
|
+ private static String getCellStringValue(Cell cell) {
|
|
|
+ if (cell == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ switch (cell.getCellType()) {
|
|
|
+ case STRING:
|
|
|
+ return cell.getStringCellValue();
|
|
|
+ case NUMERIC:
|
|
|
+ if (DateUtil.isCellDateFormatted(cell)) {
|
|
|
+ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(cell.getDateCellValue());
|
|
|
+ }
|
|
|
+ double val = cell.getNumericCellValue();
|
|
|
+ if (val == Math.floor(val)) {
|
|
|
+ return String.valueOf((long) val);
|
|
|
+ }
|
|
|
+ return String.valueOf(val);
|
|
|
+ case BOOLEAN:
|
|
|
+ return String.valueOf(cell.getBooleanCellValue());
|
|
|
+ case FORMULA:
|
|
|
+ try {
|
|
|
+ return cell.getStringCellValue();
|
|
|
+ } catch (Exception e) {
|
|
|
+ return String.valueOf(cell.getNumericCellValue());
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 转换值到目标类型
|
|
|
+ */
|
|
|
+ private static Object convertValue(Object value, Class<?> targetType, Field field) {
|
|
|
+ if (value == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理String类型
|
|
|
+ if (targetType == String.class) {
|
|
|
+ String strVal = value.toString().trim();
|
|
|
+ // 如果是数字类型且以.0结尾,去除.0
|
|
|
+ if (strVal.matches("^\\d+\\.0$")) {
|
|
|
+ strVal = strVal.substring(0, strVal.length() - 2);
|
|
|
+ }
|
|
|
+ // 日期字符串转换
|
|
|
+ Excel excelAnnotation = field.getAnnotation(Excel.class);
|
|
|
+ if (excelAnnotation != null && StringUtils.isNotEmpty(excelAnnotation.dateFormat())) {
|
|
|
+ try {
|
|
|
+ return DateUtils.parseDate(strVal);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return strVal;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return strVal;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理BigDecimal类型
|
|
|
+ if (targetType == BigDecimal.class) {
|
|
|
+ if (value instanceof BigDecimal) {
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return new BigDecimal(value.toString());
|
|
|
+ }
|
|
|
+ String strVal = value.toString().trim();
|
|
|
+ if (StringUtils.isEmpty(strVal)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return new BigDecimal(strVal);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.warn("无法转换'{}'为BigDecimal", strVal);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理Date类型
|
|
|
+ if (targetType == java.util.Date.class) {
|
|
|
+ if (value instanceof java.util.Date) {
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ if (value instanceof String) {
|
|
|
+ String strVal = ((String) value).trim();
|
|
|
+ if (StringUtils.isEmpty(strVal)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return DateUtils.parseDate(strVal);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("无法转换'{}'为Date", strVal);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理Integer/int类型
|
|
|
+ if (targetType == Integer.class || targetType == int.class) {
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return ((Number) value).intValue();
|
|
|
+ }
|
|
|
+ String strVal = value.toString().trim();
|
|
|
+ if (StringUtils.isEmpty(strVal)) {
|
|
|
+ return targetType == int.class ? 0 : null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return (int) Double.parseDouble(strVal);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return targetType == int.class ? 0 : null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理Long/long类型
|
|
|
+ if (targetType == Long.class || targetType == long.class) {
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return ((Number) value).longValue();
|
|
|
+ }
|
|
|
+ String strVal = value.toString().trim();
|
|
|
+ if (StringUtils.isEmpty(strVal)) {
|
|
|
+ return targetType == long.class ? 0L : null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return (long) Double.parseDouble(strVal);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return targetType == long.class ? 0L : null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理Double/double类型
|
|
|
+ if (targetType == Double.class || targetType == double.class) {
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return ((Number) value).doubleValue();
|
|
|
+ }
|
|
|
+ String strVal = value.toString().trim();
|
|
|
+ if (StringUtils.isEmpty(strVal)) {
|
|
|
+ return targetType == double.class ? 0.0 : null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Double.parseDouble(strVal);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return targetType == double.class ? 0.0 : null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理Float/float类型
|
|
|
+ if (targetType == Float.class || targetType == float.class) {
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return ((Number) value).floatValue();
|
|
|
+ }
|
|
|
+ String strVal = value.toString().trim();
|
|
|
+ if (StringUtils.isEmpty(strVal)) {
|
|
|
+ return targetType == float.class ? 0.0f : null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Float.parseFloat(strVal);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return targetType == float.class ? 0.0f : null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认返回原值
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断值是否为空
|
|
|
+ */
|
|
|
+ private static boolean isEmptyValue(Object value) {
|
|
|
+ if (value == null) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (value instanceof String) {
|
|
|
+ return StringUtils.isEmpty((String) value);
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|