From 7f65404f28370aeba4825ea32a951b5d91d38372 Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Thu, 16 Oct 2025 11:25:09 +0800 Subject: [PATCH] =?UTF-8?q?serv-excel=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schoolNewsServ/common/common-util/pom.xml | 15 + .../utils/excel/ExcelColumnMapping.java | 208 +++++++ .../common/utils/excel/ExcelReadResult.java | 142 +++++ .../common/utils/excel/ExcelReaderUtils.java | 426 ++++++++++++++ .../common/utils/excel/ExcelUtilsExample.java | 429 ++++++++++++++ .../org/xyzh/common/utils/excel/README.md | 542 ++++++++++++++++++ schoolNewsServ/pom.xml | 16 + 7 files changed, 1778 insertions(+) create mode 100644 schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelColumnMapping.java create mode 100644 schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelReadResult.java create mode 100644 schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelReaderUtils.java create mode 100644 schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelUtilsExample.java create mode 100644 schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/README.md diff --git a/schoolNewsServ/common/common-util/pom.xml b/schoolNewsServ/common/common-util/pom.xml index 40be126..3724a5f 100644 --- a/schoolNewsServ/common/common-util/pom.xml +++ b/schoolNewsServ/common/common-util/pom.xml @@ -18,4 +18,19 @@ 21 + + + + org.apache.poi + poi + ${poi.version} + + + + org.apache.poi + poi-ooxml + ${poi.version} + + + \ No newline at end of file diff --git a/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelColumnMapping.java b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelColumnMapping.java new file mode 100644 index 0000000..fcfc146 --- /dev/null +++ b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelColumnMapping.java @@ -0,0 +1,208 @@ +package org.xyzh.common.utils.excel; + +import org.xyzh.common.utils.validation.ValidationParam; + +import java.util.ArrayList; +import java.util.List; + +/** + * @description Excel列映射配置 + * @filename ExcelColumnMapping.java + * @author yslg + * @copyright xyzh + * @since 2025-10-16 + */ +public class ExcelColumnMapping { + + /** + * @description Excel列名(表头名称) + */ + private String columnName; + + /** + * @description Excel列索引(从0开始,优先级高于列名) + */ + private Integer columnIndex; + + /** + * @description 对象字段名 + */ + private String fieldName; + + /** + * @description 字段类型 + */ + private Class fieldType; + + /** + * @description 是否必填 + */ + private boolean required = false; + + /** + * @description 默认值 + */ + private String defaultValue; + + /** + * @description 日期格式(当字段类型为Date时使用) + */ + private String dateFormat = "yyyy-MM-dd"; + + /** + * @description 校验参数列表 + */ + private List validationParams; + + private ExcelColumnMapping() { + this.validationParams = new ArrayList<>(); + } + + public String getColumnName() { + return columnName; + } + + public Integer getColumnIndex() { + return columnIndex; + } + + public String getFieldName() { + return fieldName; + } + + public Class getFieldType() { + return fieldType; + } + + public boolean isRequired() { + return required; + } + + public String getDefaultValue() { + return defaultValue; + } + + public String getDateFormat() { + return dateFormat; + } + + public List getValidationParams() { + return validationParams; + } + + /** + * @description Builder类 + */ + public static class Builder { + private ExcelColumnMapping mapping = new ExcelColumnMapping(); + + /** + * 设置Excel列名 + */ + public Builder columnName(String columnName) { + mapping.columnName = columnName; + return this; + } + + /** + * 设置Excel列索引(从0开始) + */ + public Builder columnIndex(int columnIndex) { + mapping.columnIndex = columnIndex; + return this; + } + + /** + * 设置对象字段名 + */ + public Builder fieldName(String fieldName) { + mapping.fieldName = fieldName; + return this; + } + + /** + * 设置字段类型 + */ + public Builder fieldType(Class fieldType) { + mapping.fieldType = fieldType; + return this; + } + + /** + * 设置是否必填 + */ + public Builder required(boolean required) { + mapping.required = required; + return this; + } + + /** + * 设置为必填 + */ + public Builder required() { + mapping.required = true; + return this; + } + + /** + * 设置默认值 + */ + public Builder defaultValue(String defaultValue) { + mapping.defaultValue = defaultValue; + return this; + } + + /** + * 设置日期格式 + */ + public Builder dateFormat(String dateFormat) { + mapping.dateFormat = dateFormat; + return this; + } + + /** + * 添加校验参数 + */ + public Builder addValidation(ValidationParam param) { + mapping.validationParams.add(param); + return this; + } + + /** + * 设置校验参数列表 + */ + public Builder validations(List params) { + mapping.validationParams = params; + return this; + } + + public ExcelColumnMapping build() { + if (mapping.fieldName == null || mapping.fieldName.isEmpty()) { + throw new IllegalArgumentException("字段名不能为空"); + } + if (mapping.columnName == null && mapping.columnIndex == null) { + throw new IllegalArgumentException("必须指定列名或列索引"); + } + if (mapping.fieldType == null) { + mapping.fieldType = String.class; + } + return mapping; + } + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public String toString() { + return "ExcelColumnMapping{" + + "columnName='" + columnName + '\'' + + ", columnIndex=" + columnIndex + + ", fieldName='" + fieldName + '\'' + + ", fieldType=" + (fieldType != null ? fieldType.getSimpleName() : "null") + + ", required=" + required + + '}'; + } +} + diff --git a/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelReadResult.java b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelReadResult.java new file mode 100644 index 0000000..34ee9cb --- /dev/null +++ b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelReadResult.java @@ -0,0 +1,142 @@ +package org.xyzh.common.utils.excel; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @description Excel读取结果 + * @filename ExcelReadResult.java + * @author yslg + * @copyright xyzh + * @since 2025-10-16 + */ +public class ExcelReadResult { + + /** + * @description 是否成功 + */ + private boolean success; + + /** + * @description 成功读取的数据列表 + */ + private List dataList; + + /** + * @description 失败的行数据(行号 -> 错误信息) + */ + private Map errorRowsMap; + + /** + * @description 总行数(不包括表头) + */ + private int totalRows; + + /** + * @description 成功行数 + */ + private int successRows; + + /** + * @description 失败行数 + */ + private int errorRowsCount; + + /** + * @description 错误信息 + */ + private String errorMessage; + + public ExcelReadResult() { + this.success = true; + this.dataList = new ArrayList<>(); + this.errorRowsMap = new HashMap<>(); + this.totalRows = 0; + this.successRows = 0; + this.errorRowsCount = 0; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public List getDataList() { + return dataList; + } + + public void setDataList(List dataList) { + this.dataList = dataList; + } + + public Map getErrorRows() { + return errorRowsMap; + } + + public void setErrorRows(Map errorRowsMap) { + this.errorRowsMap = errorRowsMap; + } + + public int getTotalRows() { + return totalRows; + } + + public void setTotalRows(int totalRows) { + this.totalRows = totalRows; + } + + public int getSuccessRows() { + return successRows; + } + + public void setSuccessRows(int successRows) { + this.successRows = successRows; + } + + public int getErrorRowsCount() { + return errorRowsCount; + } + + public void setErrorRowsCount(int errorRowsCount) { + this.errorRowsCount = errorRowsCount; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public void addData(T data) { + this.dataList.add(data); + this.successRows++; + } + + public void addError(int rowNum, String error) { + this.errorRowsMap.put(rowNum, error); + this.errorRowsCount++; + } + + public boolean hasErrors() { + return this.errorRowsCount > 0; + } + + @Override + public String toString() { + return "ExcelReadResult{" + + "success=" + success + + ", totalRows=" + totalRows + + ", successRows=" + successRows + + ", errorRows=" + errorRowsCount + + ", errorMessage='" + errorMessage + '\'' + + '}'; + } +} + diff --git a/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelReaderUtils.java b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelReaderUtils.java new file mode 100644 index 0000000..3790970 --- /dev/null +++ b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelReaderUtils.java @@ -0,0 +1,426 @@ +package org.xyzh.common.utils.excel; + +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.xyzh.common.utils.validation.ValidationParam; +import org.xyzh.common.utils.validation.ValidationResult; +import org.xyzh.common.utils.validation.ValidationUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * @description Excel读取工具类(非泛型版本) + * @filename ExcelReaderUtils.java + * @author yslg + * @copyright xyzh + * @since 2025-10-16 + */ +public class ExcelReaderUtils { + + /** + * @description 从文件读取Excel + * @param file Excel文件 + * @param targetClass 目标对象Class + * @param columnMappings Excel列映射配置列表 + * @return ExcelReadResult + */ + public static ExcelReadResult readExcel(File file, Class targetClass, List columnMappings) { + try (FileInputStream fis = new FileInputStream(file)) { + return readExcel(fis, file.getName(), targetClass, columnMappings, new HashMap<>()); + } catch (Exception e) { + ExcelReadResult result = new ExcelReadResult<>(); + result.setSuccess(false); + result.setErrorMessage("读取Excel文件失败: " + e.getMessage()); + return result; + } + } + + /** + * @description 从文件读取Excel(带配置) + * @param file Excel文件 + * @param targetClass 目标对象Class + * @param columnMappings Excel列映射配置列表 + * @param options 配置选项 + * @return ExcelReadResult + */ + public static ExcelReadResult readExcel(File file, Class targetClass, + List columnMappings, + Map options) { + try (FileInputStream fis = new FileInputStream(file)) { + return readExcel(fis, file.getName(), targetClass, columnMappings, options); + } catch (Exception e) { + ExcelReadResult result = new ExcelReadResult<>(); + result.setSuccess(false); + result.setErrorMessage("读取Excel文件失败: " + e.getMessage()); + return result; + } + } + + /** + * @description 从输入流读取Excel + * @param inputStream 输入流 + * @param fileName 文件名 + * @param targetClass 目标对象Class + * @param columnMappings Excel列映射配置列表 + * @return ExcelReadResult + */ + public static ExcelReadResult readExcel(InputStream inputStream, String fileName, + Class targetClass, + List columnMappings) { + return readExcel(inputStream, fileName, targetClass, columnMappings, new HashMap<>()); + } + + /** + * @description 从输入流读取Excel(带配置) + * @param inputStream 输入流 + * @param fileName 文件名 + * @param targetClass 目标对象Class + * @param columnMappings Excel列映射配置列表 + * @param options 配置选项(headerRowIndex, dataStartRowIndex, sheetIndex, sheetName, skipEmptyRow, maxRows, continueOnError) + * @return ExcelReadResult + */ + public static ExcelReadResult readExcel(InputStream inputStream, String fileName, + Class targetClass, + List columnMappings, + Map options) { + ExcelReadResult result = new ExcelReadResult<>(); + + try { + // 获取配置 + int headerRowIndex = (int) options.getOrDefault("headerRowIndex", 0); + int dataStartRowIndex = (int) options.getOrDefault("dataStartRowIndex", 1); + int sheetIndex = (int) options.getOrDefault("sheetIndex", 0); + String sheetName = (String) options.get("sheetName"); + boolean skipEmptyRow = (boolean) options.getOrDefault("skipEmptyRow", true); + int maxRows = (int) options.getOrDefault("maxRows", 0); + boolean continueOnError = (boolean) options.getOrDefault("continueOnError", true); + + // 创建Workbook + Workbook workbook = createWorkbook(inputStream, fileName); + + // 获取Sheet + Sheet sheet = getSheet(workbook, sheetIndex, sheetName); + if (sheet == null) { + result.setSuccess(false); + result.setErrorMessage("未找到指定的Sheet"); + return result; + } + + // 解析表头 + Map headerMap = parseHeader(sheet, headerRowIndex); + + // 读取数据 + readData(sheet, headerMap, targetClass, columnMappings, dataStartRowIndex, + skipEmptyRow, maxRows, continueOnError, result); + + workbook.close(); + + } catch (Exception e) { + result.setSuccess(false); + result.setErrorMessage("读取Excel失败: " + e.getMessage()); + } + + return result; + } + + /** + * @description 创建Workbook对象 + */ + private static Workbook createWorkbook(InputStream inputStream, String fileName) throws Exception { + if (fileName.endsWith(".xlsx")) { + return new XSSFWorkbook(inputStream); + } else if (fileName.endsWith(".xls")) { + return new HSSFWorkbook(inputStream); + } else { + throw new IllegalArgumentException("不支持的文件格式,仅支持.xls和.xlsx"); + } + } + + /** + * @description 获取Sheet + */ + private static Sheet getSheet(Workbook workbook, int sheetIndex, String sheetName) { + if (sheetName != null && !sheetName.isEmpty()) { + return workbook.getSheet(sheetName); + } else { + return workbook.getSheetAt(sheetIndex); + } + } + + /** + * @description 解析表头 + */ + private static Map parseHeader(Sheet sheet, int headerRowIndex) { + Map headerMap = new HashMap<>(); + Row headerRow = sheet.getRow(headerRowIndex); + + if (headerRow != null) { + for (Cell cell : headerRow) { + String headerName = getCellValue(cell).toString().trim(); + if (!headerName.isEmpty()) { + headerMap.put(headerName, cell.getColumnIndex()); + } + } + } + + return headerMap; + } + + /** + * @description 读取数据 + */ + private static void readData(Sheet sheet, Map headerMap, + Class targetClass, List columnMappings, + int startRow, boolean skipEmptyRow, int maxRows, + boolean continueOnError, ExcelReadResult result) { + int lastRowNum = sheet.getLastRowNum(); + int endRow = maxRows > 0 ? Math.min(startRow + maxRows, lastRowNum + 1) : lastRowNum + 1; + + for (int rowNum = startRow; rowNum < endRow; rowNum++) { + Row row = sheet.getRow(rowNum); + + // 跳过空行 + if (skipEmptyRow && isEmptyRow(row)) { + continue; + } + + result.setTotalRows(result.getTotalRows() + 1); + + try { + // 将行数据转换为对象 + Object data = convertRowToObject(row, headerMap, targetClass, columnMappings, rowNum + 1); + + // 数据校验 + List allValidations = new ArrayList<>(); + for (ExcelColumnMapping mapping : columnMappings) { + if (!mapping.getValidationParams().isEmpty()) { + allValidations.addAll(mapping.getValidationParams()); + } + } + + if (!allValidations.isEmpty()) { + ValidationResult validationResult = ValidationUtils.validate(data, allValidations); + if (!validationResult.isValid()) { + result.addError(rowNum + 1, validationResult.getFirstError()); + if (!continueOnError) { + break; + } + continue; + } + } + + result.addData(data); + + } catch (Exception e) { + result.addError(rowNum + 1, e.getMessage()); + if (!continueOnError) { + break; + } + } + } + } + + /** + * @description 判断是否为空行 + */ + private static boolean isEmptyRow(Row row) { + if (row == null) { + return true; + } + + for (Cell cell : row) { + if (cell != null && cell.getCellType() != CellType.BLANK) { + String value = getCellValue(cell).toString().trim(); + if (!value.isEmpty()) { + return false; + } + } + } + + return true; + } + + /** + * @description 将行数据转换为对象 + */ + private static Object convertRowToObject(Row row, Map headerMap, + Class targetClass, List columnMappings, + int rowNum) throws Exception { + Object obj = targetClass.getDeclaredConstructor().newInstance(); + + for (ExcelColumnMapping mapping : columnMappings) { + // 获取单元格 + Cell cell = getCell(row, mapping, headerMap); + + // 获取单元格值 + Object cellValue = getCellValue(cell); + + // 处理空值 + if (cellValue == null || cellValue.toString().trim().isEmpty()) { + if (mapping.isRequired()) { + throw new IllegalArgumentException("第" + rowNum + "行,字段[" + + (mapping.getColumnName() != null ? mapping.getColumnName() : "索引" + mapping.getColumnIndex()) + + "]不能为空"); + } + // 使用默认值 + if (mapping.getDefaultValue() != null && !mapping.getDefaultValue().isEmpty()) { + cellValue = mapping.getDefaultValue(); + } else { + continue; + } + } + + // 类型转换 + Object fieldValue = convertValue(cellValue, mapping.getFieldType(), mapping.getDateFormat()); + + // 设置字段值 + Field field = getField(targetClass, mapping.getFieldName()); + field.setAccessible(true); + field.set(obj, fieldValue); + } + + return obj; + } + + /** + * @description 获取字段(支持父类) + */ + private static Field getField(Class clazz, String fieldName) throws NoSuchFieldException { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + // 尝试从父类获取 + Class superClass = clazz.getSuperclass(); + if (superClass != null) { + return getField(superClass, fieldName); + } + throw e; + } + } + + /** + * @description 获取单元格 + */ + private static Cell getCell(Row row, ExcelColumnMapping mapping, Map headerMap) { + if (row == null) { + return null; + } + + // 优先使用索引 + if (mapping.getColumnIndex() != null) { + return row.getCell(mapping.getColumnIndex()); + } + + // 使用列名 + Integer columnIndex = headerMap.get(mapping.getColumnName()); + if (columnIndex != null) { + return row.getCell(columnIndex); + } + + return null; + } + + /** + * @description 获取单元格值 + */ + private static Object getCellValue(Cell cell) { + if (cell == null) { + return null; + } + + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue(); + } else { + return cell.getNumericCellValue(); + } + case BOOLEAN: + return cell.getBooleanCellValue(); + case FORMULA: + return cell.getCellFormula(); + case BLANK: + return ""; + default: + return cell.toString(); + } + } + + /** + * @description 类型转换 + */ + private static Object convertValue(Object value, Class targetType, String dateFormat) throws Exception { + if (value == null) { + return null; + } + + String strValue = value.toString().trim(); + + // String类型 + if (targetType == String.class) { + return strValue; + } + + // Integer类型 + if (targetType == Integer.class || targetType == int.class) { + if (value instanceof Number) { + return ((Number) value).intValue(); + } + return Integer.parseInt(strValue); + } + + // Long类型 + if (targetType == Long.class || targetType == long.class) { + if (value instanceof Number) { + return ((Number) value).longValue(); + } + return Long.parseLong(strValue); + } + + // Double类型 + if (targetType == Double.class || targetType == double.class) { + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + return Double.parseDouble(strValue); + } + + // Float类型 + if (targetType == Float.class || targetType == float.class) { + if (value instanceof Number) { + return ((Number) value).floatValue(); + } + return Float.parseFloat(strValue); + } + + // Boolean类型 + if (targetType == Boolean.class || targetType == boolean.class) { + if (value instanceof Boolean) { + return value; + } + return Boolean.parseBoolean(strValue) || "1".equals(strValue) || + "是".equals(strValue) || "true".equalsIgnoreCase(strValue); + } + + // Date类型 + if (targetType == Date.class) { + if (value instanceof Date) { + return value; + } + SimpleDateFormat sdf = new SimpleDateFormat(dateFormat); + return sdf.parse(strValue); + } + + // 其他类型 + return value; + } +} + diff --git a/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelUtilsExample.java b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelUtilsExample.java new file mode 100644 index 0000000..6de89e4 --- /dev/null +++ b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/ExcelUtilsExample.java @@ -0,0 +1,429 @@ +package org.xyzh.common.utils.excel; + +import org.xyzh.common.utils.validation.ValidationParam; +import org.xyzh.common.utils.validation.method.ValidateMethodType; + +import java.io.File; +import java.util.*; + +/** + * @description Excel工具使用示例 + * @filename ExcelUtilsExample.java + * @author yslg + * @copyright xyzh + * @since 2025-10-16 + */ +public class ExcelUtilsExample { + + /** + * 示例实体类:用户信息 + */ + public static class UserInfo { + private String name; + private Integer age; + private String phone; + private String email; + private String idCard; + private Date joinDate; + private Boolean active; + + // Getters and Setters + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public Integer getAge() { return age; } + public void setAge(Integer age) { this.age = age; } + + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getIdCard() { return idCard; } + public void setIdCard(String idCard) { this.idCard = idCard; } + + public Date getJoinDate() { return joinDate; } + public void setJoinDate(Date joinDate) { this.joinDate = joinDate; } + + public Boolean getActive() { return active; } + public void setActive(Boolean active) { this.active = active; } + + @Override + public String toString() { + return "UserInfo{" + + "name='" + name + '\'' + + ", age=" + age + + ", phone='" + phone + '\'' + + ", email='" + email + '\'' + + ", idCard='" + idCard + '\'' + + ", joinDate=" + joinDate + + ", active=" + active + + '}'; + } + } + + /** + * 示例1:基本使用 + */ + public static void example1_BasicUsage() { + System.out.println("========== 示例1: 基本使用 =========="); + + // 1. 定义列映射关系 + List columnMappings = Arrays.asList( + ExcelColumnMapping.builder() + .columnName("姓名") + .fieldName("name") + .fieldType(String.class) + .required() + .build(), + + ExcelColumnMapping.builder() + .columnName("年龄") + .fieldName("age") + .fieldType(Integer.class) + .required() + .build(), + + ExcelColumnMapping.builder() + .columnName("手机号") + .fieldName("phone") + .fieldType(String.class) + .required() + .build(), + + ExcelColumnMapping.builder() + .columnName("邮箱") + .fieldName("email") + .fieldType(String.class) + .validations(Arrays.asList( + ValidationParam.builder() + .fieldName("email") + .fieldLabel("邮箱") + .required() + .validateMethod(ValidateMethodType.EMAIL) + .build() + )) + .build(), + + ExcelColumnMapping.builder() + .columnName("入职日期") + .fieldName("joinDate") + .fieldType(Date.class) + .dateFormat("yyyy-MM-dd") + .build(), + + ExcelColumnMapping.builder() + .columnName("是否在职") + .fieldName("active") + .fieldType(Boolean.class) + .defaultValue("true") + .build() + ); + + // 2. 读取Excel + File file = new File("users.xlsx"); + ExcelReadResult result = ExcelReaderUtils.readExcel( + file, + UserInfo.class, + columnMappings + ); + + // 3. 处理结果 + if (result.isSuccess()) { + System.out.println("读取成功!"); + System.out.println("总行数: " + result.getTotalRows()); + System.out.println("成功行数: " + result.getSuccessRows()); + + for (Object obj : result.getDataList()) { + UserInfo user = (UserInfo) obj; + System.out.println(user); + } + } else { + System.out.println("读取失败: " + result.getErrorMessage()); + } + } + + /** + * 示例2:带数据校验 + */ + public static void example2_WithValidation() { + System.out.println("\n========== 示例2: 带数据校验 =========="); + + // 定义列映射关系(带校验) + List columnMappings = Arrays.asList( + ExcelColumnMapping.builder() + .columnName("姓名") + .fieldName("name") + .fieldType(String.class) + .required() + .addValidation( + ValidationParam.builder() + .fieldName("name") + .fieldLabel("姓名") + .required() + .minLength(2) + .maxLength(20) + .validateMethod(ValidateMethodType.CHINESE) + .build() + ) + .build(), + + ExcelColumnMapping.builder() + .columnName("年龄") + .fieldName("age") + .fieldType(Integer.class) + .required() + .addValidation( + ValidationParam.builder() + .fieldName("age") + .fieldLabel("年龄") + .required() + .customValidator(value -> { + Integer age = (Integer) value; + return age >= 18 && age <= 65; + }) + .customErrorMessage("年龄必须在18-65岁之间") + .build() + ) + .build(), + + ExcelColumnMapping.builder() + .columnName("手机号") + .fieldName("phone") + .fieldType(String.class) + .required() + .addValidation( + ValidationParam.builder() + .fieldName("phone") + .fieldLabel("手机号") + .required() + .validateMethod(ValidateMethodType.PHONE) + .build() + ) + .build(), + + ExcelColumnMapping.builder() + .columnName("邮箱") + .fieldName("email") + .fieldType(String.class) + .addValidation( + ValidationParam.builder() + .fieldName("email") + .fieldLabel("邮箱") + .required(false) + .validateMethod(ValidateMethodType.EMAIL) + .build() + ) + .build(), + + ExcelColumnMapping.builder() + .columnName("身份证号") + .fieldName("idCard") + .fieldType(String.class) + .addValidation( + ValidationParam.builder() + .fieldName("idCard") + .fieldLabel("身份证号") + .required(false) + .validateMethod(ValidateMethodType.ID_CARD) + .build() + ) + .build() + ); + + // 读取配置 + Map options = new HashMap<>(); + options.put("continueOnError", true); // 遇到错误继续读取 + + File file = new File("users.xlsx"); + ExcelReadResult result = ExcelReaderUtils.readExcel( + file, + UserInfo.class, + columnMappings, + options + ); + + // 处理结果 + System.out.println("读取完成!"); + System.out.println("总行数: " + result.getTotalRows()); + System.out.println("成功行数: " + result.getSuccessRows()); + System.out.println("失败行数: " + result.getErrorRowsCount()); + + // 显示错误信息 + if (result.hasErrors()) { + System.out.println("\n错误信息:"); + result.getErrorRows().forEach((rowNum, error) -> { + System.out.println("第" + rowNum + "行: " + error); + }); + } + } + + /** + * 示例3:使用列索引 + */ + public static void example3_UseColumnIndex() { + System.out.println("\n========== 示例3: 使用列索引 =========="); + + // 使用列索引而非列名 + List columnMappings = Arrays.asList( + ExcelColumnMapping.builder() + .columnIndex(0) // 第1列 + .fieldName("name") + .fieldType(String.class) + .required() + .build(), + + ExcelColumnMapping.builder() + .columnIndex(1) // 第2列 + .fieldName("age") + .fieldType(Integer.class) + .required() + .build(), + + ExcelColumnMapping.builder() + .columnIndex(2) // 第3列 + .fieldName("phone") + .fieldType(String.class) + .required() + .build() + ); + + File file = new File("users.xlsx"); + ExcelReadResult result = ExcelReaderUtils.readExcel( + file, + UserInfo.class, + columnMappings + ); + + System.out.println("成功读取: " + result.getSuccessRows() + " 行"); + } + + /** + * 示例4:自定义配置 + */ + public static void example4_CustomConfig() { + System.out.println("\n========== 示例4: 自定义配置 =========="); + + List columnMappings = Arrays.asList( + ExcelColumnMapping.builder() + .columnName("姓名") + .fieldName("name") + .fieldType(String.class) + .required() + .build(), + + ExcelColumnMapping.builder() + .columnName("年龄") + .fieldName("age") + .fieldType(Integer.class) + .required() + .build() + ); + + // 自定义配置 + Map options = new HashMap<>(); + options.put("sheetName", "员工信息"); // 指定Sheet名称 + options.put("headerRowIndex", 0); // 表头在第1行 + options.put("dataStartRowIndex", 1); // 数据从第2行开始 + options.put("skipEmptyRow", true); // 跳过空行 + options.put("maxRows", 1000); // 最多读取1000行 + options.put("continueOnError", true); // 遇到错误继续 + + File file = new File("users.xlsx"); + ExcelReadResult result = ExcelReaderUtils.readExcel( + file, + UserInfo.class, + columnMappings, + options + ); + + System.out.println("读取结果: " + result); + } + + /** + * 示例5:在Controller中使用 + */ + public static void example5_InController() { + System.out.println("\n========== 示例5: 在Controller中使用(代码示例)=========="); + System.out.println(""" + @PostMapping("/import") + public ResultDomain importUsers(@RequestParam("file") MultipartFile file) { + try { + // 1. 定义列映射关系 + List columnMappings = Arrays.asList( + ExcelColumnMapping.builder() + .columnName("姓名") + .fieldName("name") + .fieldType(String.class) + .required() + .addValidation( + ValidationParam.builder() + .fieldName("name") + .fieldLabel("姓名") + .required() + .validateMethod(ValidateMethodType.CHINESE) + .build() + ) + .build(), + + ExcelColumnMapping.builder() + .columnName("手机号") + .fieldName("phone") + .fieldType(String.class) + .required() + .addValidation( + ValidationParam.builder() + .fieldName("phone") + .fieldLabel("手机号") + .required() + .validateMethod(ValidateMethodType.PHONE) + .build() + ) + .build() + ); + + // 2. 读取Excel + ExcelReadResult result = ExcelReaderUtils.readExcel( + file.getInputStream(), + file.getOriginalFilename(), + UserInfo.class, + columnMappings + ); + + // 3. 处理结果 + if (result.hasErrors()) { + StringBuilder errorMsg = new StringBuilder(); + errorMsg.append("导入失败,共").append(result.getErrorRowsCount()).append("行数据有误:\\n"); + result.getErrorRows().forEach((rowNum, error) -> { + errorMsg.append("第").append(rowNum).append("行: ").append(error).append("\\n"); + }); + return ResultDomain.fail(errorMsg.toString()); + } + + // 4. 保存数据 + List users = new ArrayList<>(); + for (Object obj : result.getDataList()) { + users.add((UserInfo) obj); + } + userService.batchSave(users); + + return ResultDomain.success("导入成功,共导入" + result.getSuccessRows() + "条数据"); + + } catch (Exception e) { + return ResultDomain.fail("导入失败: " + e.getMessage()); + } + } + """); + } + + public static void main(String[] args) { + // 运行示例 + // example1_BasicUsage(); + // example2_WithValidation(); + // example3_UseColumnIndex(); + // example4_CustomConfig(); + example5_InController(); + } +} + diff --git a/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/README.md b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/README.md new file mode 100644 index 0000000..29d39b3 --- /dev/null +++ b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/excel/README.md @@ -0,0 +1,542 @@ +# Excel读取工具使用说明 + +## 概述 + +这是一个功能强大的Excel读取工具,通过配置 **Excel列对象** 来定义Excel列与Java对象字段的映射关系,支持自动构建对象并集成数据校验功能。 + +## 核心组件 + +### 1. ExcelColumnMapping - Excel列映射配置 +定义Excel列与对象字段的映射关系: +- **columnName** - Excel列名(表头名称) +- **columnIndex** - Excel列索引(从0开始,优先级高于列名) +- **fieldName** - 对象字段名 +- **fieldType** - 字段类型 +- **required** - 是否必填 +- **defaultValue** - 默认值 +- **dateFormat** - 日期格式 +- **validationParams** - 校验参数列表 + +### 2. ExcelReaderUtils - Excel读取工具类 +提供静态方法读取Excel: +- `readExcel(File, Class, List)` - 从文件读取 +- `readExcel(File, Class, List, Map)` - 从文件读取(带配置) +- `readExcel(InputStream, fileName, Class, List)` - 从输入流读取 +- `readExcel(InputStream, fileName, Class, List, Map)` - 从输入流读取(带配置) + +### 3. ExcelReadResult - 读取结果 +包含读取结果的详细信息: +- 成功数据列表 +- 错误行信息(行号 -> 错误消息) +- 统计信息(总行数、成功数、失败数) + +## 基本使用 + +### 1. 定义实体类 + +```java +public class UserInfo { + private String name; + private Integer age; + private String phone; + private String email; + private Date joinDate; + private Boolean active; + + // Getters and Setters... +} +``` + +### 2. 定义列映射关系 + +```java +List columnMappings = Arrays.asList( + ExcelColumnMapping.builder() + .columnName("姓名") // Excel列名 + .fieldName("name") // 对象字段名 + .fieldType(String.class) // 字段类型 + .required() // 必填 + .build(), + + ExcelColumnMapping.builder() + .columnName("年龄") + .fieldName("age") + .fieldType(Integer.class) + .required() + .build(), + + ExcelColumnMapping.builder() + .columnName("手机号") + .fieldName("phone") + .fieldType(String.class) + .required() + .build(), + + ExcelColumnMapping.builder() + .columnName("邮箱") + .fieldName("email") + .fieldType(String.class) + .build(), + + ExcelColumnMapping.builder() + .columnName("入职日期") + .fieldName("joinDate") + .fieldType(Date.class) + .dateFormat("yyyy-MM-dd") + .build(), + + ExcelColumnMapping.builder() + .columnName("是否在职") + .fieldName("active") + .fieldType(Boolean.class) + .defaultValue("true") + .build() +); +``` + +### 3. 读取Excel + +```java +// 读取文件 +File file = new File("users.xlsx"); +ExcelReadResult result = ExcelReaderUtils.readExcel( + file, + UserInfo.class, + columnMappings +); + +// 处理结果 +if (result.isSuccess()) { + for (Object obj : result.getDataList()) { + UserInfo user = (UserInfo) obj; + System.out.println(user); + } + System.out.println("成功读取: " + result.getSuccessRows() + " 行"); +} +``` + +### 4. 带数据校验的读取 + +```java +// 定义列映射关系(带校验) +List columnMappings = Arrays.asList( + ExcelColumnMapping.builder() + .columnName("姓名") + .fieldName("name") + .fieldType(String.class) + .required() + .addValidation( + ValidationParam.builder() + .fieldName("name") + .fieldLabel("姓名") + .required() + .minLength(2) + .maxLength(20) + .validateMethod(ValidateMethodType.CHINESE) + .build() + ) + .build(), + + ExcelColumnMapping.builder() + .columnName("年龄") + .fieldName("age") + .fieldType(Integer.class) + .required() + .addValidation( + ValidationParam.builder() + .fieldName("age") + .fieldLabel("年龄") + .required() + .customValidator(value -> { + Integer age = (Integer) value; + return age >= 18 && age <= 65; + }) + .customErrorMessage("年龄必须在18-65岁之间") + .build() + ) + .build(), + + ExcelColumnMapping.builder() + .columnName("手机号") + .fieldName("phone") + .fieldType(String.class) + .required() + .addValidation( + ValidationParam.builder() + .fieldName("phone") + .fieldLabel("手机号") + .required() + .validateMethod(ValidateMethodType.PHONE) + .build() + ) + .build(), + + ExcelColumnMapping.builder() + .columnName("邮箱") + .fieldName("email") + .fieldType(String.class) + .addValidation( + ValidationParam.builder() + .fieldName("email") + .fieldLabel("邮箱") + .required(false) + .validateMethod(ValidateMethodType.EMAIL) + .build() + ) + .build() +); + +// 配置选项 +Map options = new HashMap<>(); +options.put("continueOnError", true); // 遇到错误继续读取 + +// 读取Excel +File file = new File("users.xlsx"); +ExcelReadResult result = ExcelReaderUtils.readExcel( + file, + UserInfo.class, + columnMappings, + options +); + +// 处理结果 +System.out.println("总行数: " + result.getTotalRows()); +System.out.println("成功: " + result.getSuccessRows()); +System.out.println("失败: " + result.getErrorRowsCount()); + +// 显示错误 +if (result.hasErrors()) { + result.getErrorRows().forEach((rowNum, error) -> { + System.out.println("第" + rowNum + "行: " + error); + }); +} +``` + +### 5. 使用列索引而非列名 + +```java +// 使用列索引(从0开始) +List columnMappings = Arrays.asList( + ExcelColumnMapping.builder() + .columnIndex(0) // 第1列 + .fieldName("name") + .fieldType(String.class) + .required() + .build(), + + ExcelColumnMapping.builder() + .columnIndex(1) // 第2列 + .fieldName("age") + .fieldType(Integer.class) + .required() + .build(), + + ExcelColumnMapping.builder() + .columnIndex(2) // 第3列 + .fieldName("phone") + .fieldType(String.class) + .required() + .build() +); + +File file = new File("users.xlsx"); +ExcelReadResult result = ExcelReaderUtils.readExcel( + file, + UserInfo.class, + columnMappings +); +``` + +### 6. 自定义配置选项 + +以下是旧版本的校验代码,现在已经整合到ExcelColumnMapping中: + +```java +// 旧版本(已弃用) +List validationParams = Arrays.asList( + ValidationParam.builder() + .fieldName("name") + .fieldLabel("姓名") + .required() + .minLength(2) + .maxLength(20) + .validateMethod(ValidateMethodType.CHINESE) + .build(), + + ValidationParam.builder() + .fieldName("phone") + .fieldLabel("手机号") + .required() + .validateMethod(ValidateMethodType.PHONE) + .build(), + + ValidationParam.builder() + .fieldName("email") + .fieldLabel("邮箱") + .required(false) + .validateMethod(ValidateMethodType.EMAIL) + .build(), + + ValidationParam.builder() + .fieldName("age") + .fieldLabel("年龄") + .required() + .customValidator(value -> { + Integer age = (Integer) value; + return age >= 18 && age <= 65; + }) + .customErrorMessage("年龄必须在18-65岁之间") + .build() +); + +// 创建配置 +ExcelReaderConfig config = new ExcelReaderConfig<>(UserInfo.class) + .setValidationParams(validationParams) + .setContinueOnError(true); // 遇到错误继续读取 + +// 读取 +ExcelReader reader = ExcelReader.create(config); +ExcelReadResult result = reader.read(file); + +// 处理结果 +System.out.println("总行数: " + result.getTotalRows()); +System.out.println("成功: " + result.getSuccessRows()); +System.out.println("失败: " + result.getErrorRowsCount()); + +// 显示错误 +if (result.hasErrors()) { + result.getErrorRows().forEach((rowNum, error) -> { + System.out.println("第" + rowNum + "行: " + error); + }); +} +``` + +```java +List columnMappings = Arrays.asList( + // 列映射配置... +); + +// 自定义配置选项 +Map options = new HashMap<>(); +options.put("sheetName", "员工信息"); // 指定Sheet名称 +options.put("headerRowIndex", 0); // 表头在第1行 +options.put("dataStartRowIndex", 1); // 数据从第2行开始 +options.put("skipEmptyRow", true); // 跳过空行 +options.put("maxRows", 1000); // 最多读取1000行 +options.put("continueOnError", true); // 遇到错误继续 + +File file = new File("users.xlsx"); +ExcelReadResult result = ExcelReaderUtils.readExcel( + file, + UserInfo.class, + columnMappings, + options +); +``` + +## 在Controller中使用 + +```java +@PostMapping("/import") +public ResultDomain importUsers(@RequestParam("file") MultipartFile file) { + try { + // 1. 定义列映射关系(带校验) + List columnMappings = Arrays.asList( + ExcelColumnMapping.builder() + .columnName("姓名") + .fieldName("name") + .fieldType(String.class) + .required() + .addValidation( + ValidationParam.builder() + .fieldName("name") + .fieldLabel("姓名") + .required() + .validateMethod(ValidateMethodType.CHINESE) + .build() + ) + .build(), + + ExcelColumnMapping.builder() + .columnName("手机号") + .fieldName("phone") + .fieldType(String.class) + .required() + .addValidation( + ValidationParam.builder() + .fieldName("phone") + .fieldLabel("手机号") + .required() + .validateMethod(ValidateMethodType.PHONE) + .build() + ) + .build(), + + ExcelColumnMapping.builder() + .columnName("邮箱") + .fieldName("email") + .fieldType(String.class) + .addValidation( + ValidationParam.builder() + .fieldName("email") + .fieldLabel("邮箱") + .validateMethod(ValidateMethodType.EMAIL) + .build() + ) + .build() + ); + + // 2. 读取Excel + ExcelReadResult result = ExcelReaderUtils.readExcel( + file.getInputStream(), + file.getOriginalFilename(), + UserInfo.class, + columnMappings + ); + + // 3. 处理结果 + if (result.hasErrors()) { + StringBuilder errorMsg = new StringBuilder(); + errorMsg.append("导入失败,共").append(result.getErrorRowsCount()).append("行数据有误:\n"); + result.getErrorRows().forEach((rowNum, error) -> { + errorMsg.append("第").append(rowNum).append("行: ").append(error).append("\n"); + }); + return ResultDomain.fail(errorMsg.toString()); + } + + // 4. 保存数据 + List users = new ArrayList<>(); + for (Object obj : result.getDataList()) { + users.add((UserInfo) obj); + } + userService.batchSave(users); + + return ResultDomain.success("导入成功,共导入" + result.getSuccessRows() + "条数据"); + + } catch (Exception e) { + return ResultDomain.fail("导入失败: " + e.getMessage()); + } +} +``` + +## 支持的数据类型 + +- **String** - 字符串 +- **Integer/int** - 整数 +- **Long/long** - 长整数 +- **Double/double** - 双精度浮点数 +- **Float/float** - 单精度浮点数 +- **Boolean/boolean** - 布尔值(支持:true/false、1/0、是/否) +- **Date** - 日期(需指定dateFormat) + +## 配置选项 + +### ExcelColumnMapping Builder方法 + +| 方法 | 说明 | 必填 | +|------|------|------| +| columnName(String) | 设置Excel列名 | columnName和columnIndex至少一个 | +| columnIndex(int) | 设置Excel列索引(从0开始,优先级高于columnName) | columnName和columnIndex至少一个 | +| fieldName(String) | 设置对象字段名 | 是 | +| fieldType(Class) | 设置字段类型 | 否(默认String.class) | +| required() / required(boolean) | 设置是否必填 | 否(默认false) | +| defaultValue(String) | 设置默认值 | 否 | +| dateFormat(String) | 设置日期格式 | 否(默认"yyyy-MM-dd") | +| addValidation(ValidationParam) | 添加校验参数 | 否 | +| validations(List) | 设置校验参数列表 | 否 | + +### Options配置项 + +传递给 `readExcel` 方法的 `Map options` 支持以下选项: + +| 键名 | 类型 | 说明 | 默认值 | +|------|------|------|--------| +| headerRowIndex | int | 表头所在行(从0开始) | 0 | +| dataStartRowIndex | int | 数据起始行(从0开始) | 1 | +| sheetIndex | int | Sheet索引(从0开始) | 0 | +| sheetName | String | Sheet名称(优先级高于sheetIndex) | null | +| skipEmptyRow | boolean | 跳过空行 | true | +| maxRows | int | 最大读取行数(0=不限制) | 0 | +| continueOnError | boolean | 遇到错误继续读取 | true | + +## 核心特性 + +1. **配置驱动**:通过ExcelColumnMapping配置Excel列与对象字段的映射关系 +2. **无需注解**:不需要在实体类上添加注解,更加灵活 +3. **数据校验**:集成ValidationUtils和ValidateMethodType进行专业数据校验 +4. **灵活配置**:支持多种配置选项(Sheet选择、行范围、错误处理等) +5. **错误处理**:详细的错误信息和错误行记录 +6. **类型转换**:自动进行类型转换 +7. **空值处理**:支持默认值和空值校验 +8. **多Sheet支持**:可指定Sheet名称或索引 +9. **两种映射方式**:支持列名和列索引两种方式 + +## 注意事项 + +1. **实体类要求**:必须有无参构造函数 +2. **字段访问**:字段必须有setter方法或可访问(支持父类字段) +3. **列映射优先级**:columnIndex优先级高于columnName +4. **日期类型**:必须指定dateFormat +5. **布尔类型**:支持多种格式(true/false、1/0、是/否) +6. **错误处理**:根据continueOnError选项决定遇到错误时是否继续 +7. **文件格式**:支持.xls和.xlsx两种格式 +8. **类型转换**:自动转换支持String、Integer、Long、Double、Float、Boolean、Date + +## 依赖 + +```xml + + org.apache.poi + poi + 5.2.3 + + + + org.apache.poi + poi-ooxml + 5.2.3 + +``` + +## API说明 + +### ExcelReaderUtils 静态方法 + +```java +// 从文件读取(基本) +public static ExcelReadResult readExcel( + File file, + Class targetClass, + List columnMappings +) + +// 从文件读取(带配置) +public static ExcelReadResult readExcel( + File file, + Class targetClass, + List columnMappings, + Map options +) + +// 从输入流读取(基本) +public static ExcelReadResult readExcel( + InputStream inputStream, + String fileName, + Class targetClass, + List columnMappings +) + +// 从输入流读取(带配置) +public static ExcelReadResult readExcel( + InputStream inputStream, + String fileName, + Class targetClass, + List columnMappings, + Map options +) +``` + +## 完整示例 + +参考 `ExcelUtilsExample.java` 查看完整使用示例。 + diff --git a/schoolNewsServ/pom.xml b/schoolNewsServ/pom.xml index 436f547..efa33de 100644 --- a/schoolNewsServ/pom.xml +++ b/schoolNewsServ/pom.xml @@ -53,6 +53,9 @@ 3.5.4 + + 5.2.3 + 1.18.40 @@ -252,6 +255,19 @@ ${spring-data-redis.version} + + + org.apache.poi + poi + ${poi.version} + + + + org.apache.poi + poi-ooxml + ${poi.version} + +