package com.eden.room.utils; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Field; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.io.IOException; import com.alibaba.excel.EasyExcel; import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.ss.usermodel.WorkbookFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import com.eden.room.utils.excel.Excel; import javax.servlet.http.HttpServletResponse; /** * Excel相关处理工具类 * * @param Excel导入导出对象类型 */ public class ExcelUtil { private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class); /** * 实体类型 */ private Class clazz; /** * 初始化 * * @param clazz Excel导入导出对象类型 */ public ExcelUtil(Class clazz) { this.clazz = clazz; } /** * 获取单元格的值 * * @param cell 单元格 * @param fieldType 字段类型 * @return 单元格值 */ private Object getCellValue(Cell cell, Class fieldType) { Object value = null; switch (cell.getCellType()) { case STRING: String cellValue = cell.getStringCellValue(); if (StringUtils.hasText(cellValue) && !cellValue.trim().isEmpty()) { value = convertValue(cellValue, fieldType); } break; case NUMERIC: if (DateUtil.isCellDateFormatted(cell)) { // 日期类型 value = cell.getDateCellValue(); } else { // 数值类型 double numericValue = cell.getNumericCellValue(); if (fieldType == BigDecimal.class) { value = BigDecimal.valueOf(numericValue); } else if (fieldType == Long.class || fieldType == long.class) { value = (long) numericValue; } else if (fieldType == Integer.class || fieldType == int.class) { value = (int) numericValue; } else if (fieldType == Double.class || fieldType == double.class) { value = numericValue; } else if (fieldType == Float.class || fieldType == float.class) { value = (float) numericValue; } else if (fieldType == String.class) { // 防止科学计数法 // 判断是否为整数 if (numericValue == Math.floor(numericValue) && !Double.isInfinite(numericValue)) { // 为整数,去掉小数点和小数部分 value = String.valueOf((long) numericValue); } else { // 为小数,按原样输出 value = String.valueOf(numericValue); } } else { value = numericValue; } } break; case BOOLEAN: value = cell.getBooleanCellValue(); break; case FORMULA: try { value = cell.getStringCellValue(); } catch (Exception e) { value = cell.getNumericCellValue(); } break; default: break; } return value; } /** * 转换字符串为指定类型 * * @param value 字符串值 * @param fieldType 目标类型 * @return 转换后的值 */ private Object convertValue(String value, Class fieldType) { if (fieldType == String.class) { // 检查字符串是否为数字表示 try { if (value.contains(".")) { // 尝试将其解析为double double doubleValue = Double.parseDouble(value); // 检查是否为整数 if (doubleValue == Math.floor(doubleValue) && !Double.isInfinite(doubleValue)) { // 为整数,去掉小数点和小数部分 return String.valueOf((long) doubleValue); } } } catch (NumberFormatException e) { // 不是数字,直接返回原字符串 } return value; } else if (fieldType == Integer.class || fieldType == int.class) { return Integer.parseInt(value); } else if (fieldType == Long.class || fieldType == long.class) { return Long.parseLong(value); } else if (fieldType == Double.class || fieldType == double.class) { return Double.parseDouble(value); } else if (fieldType == Float.class || fieldType == float.class) { return Float.parseFloat(value); } else if (fieldType == BigDecimal.class) { return new BigDecimal(value); } else if (fieldType == Boolean.class || fieldType == boolean.class) { return Boolean.parseBoolean(value); } return value; } /** * 导出Excel * * @param list 导出数据集合 * @param sheetName 工作表名称 * @param os 输出流 * @throws Exception 异常 */ public void exportExcel(List list, String sheetName, OutputStream os) throws Exception { // 创建工作簿 Workbook workbook = new XSSFWorkbook(); try { // 创建工作表 Sheet sheet = workbook.createSheet(sheetName); // 设置默认列宽 sheet.setDefaultColumnWidth(15); // 设置表头样式 CellStyle headerStyle = workbook.createCellStyle(); headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); headerStyle.setAlignment(HorizontalAlignment.CENTER); Font headerFont = workbook.createFont(); headerFont.setBold(true); headerStyle.setFont(headerFont); // 设置数据行样式 CellStyle dataStyle = workbook.createCellStyle(); dataStyle.setAlignment(HorizontalAlignment.CENTER); // 获取所有字段 Field[] fields = clazz.getDeclaredFields(); List exportFields = new ArrayList<>(); // 筛选带有@Excel注解的字段 for (Field field : fields) { Excel excel = field.getAnnotation(Excel.class); if (excel != null && excel.isExport()) { field.setAccessible(true); exportFields.add(field); } } // 按照sort排序 exportFields.sort((f1, f2) -> { Excel e1 = f1.getAnnotation(Excel.class); Excel e2 = f2.getAnnotation(Excel.class); return Integer.compare(e1.sort(), e2.sort()); }); // 创建标题行 Row headerRow = sheet.createRow(0); // 创建标题 for (int i = 0; i < exportFields.size(); i++) { Field field = exportFields.get(i); Excel excel = field.getAnnotation(Excel.class); Cell cell = headerRow.createCell(i); cell.setCellStyle(headerStyle); cell.setCellValue(excel.name()); } // 填充数据 if (list != null && !list.isEmpty()) { for (int i = 0; i < list.size(); i++) { Row row = sheet.createRow(i + 1); T entity = list.get(i); for (int j = 0; j < exportFields.size(); j++) { Field field = exportFields.get(j); Excel excel = field.getAnnotation(Excel.class); Cell cell = row.createCell(j); cell.setCellStyle(dataStyle); Object value = field.get(entity); if (value == null) { cell.setCellValue(""); continue; } // 处理日期类型 if (value instanceof Date) { if (excel.dateFormat().length() > 0) { cell.setCellValue(new java.text.SimpleDateFormat(excel.dateFormat()).format((Date) value)); } else { cell.setCellValue(new java.text.SimpleDateFormat("yyyy-MM-dd").format((Date) value)); } continue; } // 处理数值类型 if (excel.cellType() == Excel.ColumnType.NUMERIC) { if (value instanceof Integer) { cell.setCellValue(((Integer) value).doubleValue()); } else if (value instanceof Long) { cell.setCellValue(((Long) value).doubleValue()); } else if (value instanceof Double) { cell.setCellValue((Double) value); } else if (value instanceof Float) { cell.setCellValue(((Float) value).doubleValue()); } else if (value instanceof BigDecimal) { cell.setCellValue(((BigDecimal) value).doubleValue()); } // 添加后缀 if (excel.suffix().length() > 0) { String stringValue = value.toString() + excel.suffix(); cell.setCellValue(stringValue); } } else { // 处理枚举值转换 String cellValue = value.toString(); if (excel.readConverterExp().length() > 0) { cellValue = convertByExp(cellValue, excel.readConverterExp(), true); } cell.setCellValue(cellValue); } } } } // 写入输出流 workbook.write(os); } finally { if (workbook != null) { try { workbook.close(); } catch (IOException e) { log.error("关闭Excel工作簿异常", e); } } } } /** * 根据表达式转换值 * * @param value 原始值 * @param converterExp 转换表达式(例如:0=男,1=女,2=未知) * @param reverse 是否反向转换 * @return 转换后的值 */ private String convertByExp(String value, String converterExp, boolean reverse) { String[] convertSource = converterExp.split(","); for (String item : convertSource) { String[] itemArray = item.split("="); if (itemArray.length == 2) { if (reverse) { // 反向转换(从值转换为键) if (itemArray[1].equals(value)) { return itemArray[0]; } } else { // 正向转换(从键转换为值) if (itemArray[0].equals(value)) { return itemArray[1]; } } } } return value; } /** * 导入Excel数据 * * @param is Excel文件输入流 * @return 导入的数据列表 * @throws Exception 导入异常 */ public List importExcel(InputStream is) throws Exception { List list = new ArrayList<>(); Workbook workbook = null; try { log.info("开始解析Excel文件..."); // 尝试创建工作簿 try { workbook = WorkbookFactory.create(is); } catch (Exception e) { log.error("创建工作簿失败", e); throw new IOException("Excel文件格式错误或文件已损坏:" + e.getMessage()); } // 检查工作表 if (workbook.getNumberOfSheets() == 0) { throw new IOException("Excel文件中没有工作表"); } Sheet sheet = workbook.getSheetAt(0); log.info("工作表名称: {}, 总行数: {}", sheet.getSheetName(), sheet.getPhysicalNumberOfRows()); // 获取标题行 Row headerRow = sheet.getRow(1); if (headerRow == null) { throw new IOException("Excel文件格式错误,缺少标题行"); } log.info("标题行单元格数: {}", headerRow.getPhysicalNumberOfCells()); // 获取带有@Excel注解的字段 Field[] fields = clazz.getDeclaredFields(); Map fieldMap = new HashMap<>(); Map excelAnnotationMap = new HashMap<>(); Map fieldNameMap = new HashMap<>(); Map excelNameMap = new HashMap<>(); // 预先构建字段名称映射表 for (Field field : fields) { Excel excel = field.getAnnotation(Excel.class); if (excel != null) { field.setAccessible(true); fieldNameMap.put(excel.name().trim(), field); excelNameMap.put(excel.name().trim(), excel); log.debug("Excel注解字段: {}, 名称: {}", field.getName(), excel.name()); } } if (fieldNameMap.isEmpty()) { throw new IOException("未找到带有@Excel注解的字段,无法导入数据"); } // 解析标题行,将字段与列索引匹配 for (int i = 0; i < headerRow.getLastCellNum(); i++) { Cell cell = headerRow.getCell(i); if (cell == null) { continue; } String headerValue = ""; try { // 尝试获取标题值,处理不同类型的单元格 if (cell.getCellType() == CellType.STRING) { headerValue = cell.getStringCellValue().trim(); } else if (cell.getCellType() == CellType.NUMERIC) { headerValue = String.valueOf(cell.getNumericCellValue()).trim(); } else { headerValue = cell.toString().trim(); } } catch (Exception e) { log.warn("获取标题单元格值异常,跳过第{}列", i + 1, e); continue; } if (StringUtils.isEmpty(headerValue)) { continue; } // 查找匹配的字段 Field field = fieldNameMap.get(headerValue); Excel excel = excelNameMap.get(headerValue); if (field != null && excel != null) { fieldMap.put(i, field); excelAnnotationMap.put(i, excel); log.info("匹配到列: {}, 对应字段: {}", headerValue, field.getName()); } else { log.warn("列[{}]没有对应的字段注解", headerValue); } } if (fieldMap.isEmpty()) { throw new IOException("Excel文件中的列名与定义的@Excel注解不匹配,请检查模板格式"); } // 解析数据行 int rowCount = sheet.getLastRowNum() + 1; log.info("开始解析数据行,共{}行", rowCount - 1); for (int i = 3; i < rowCount; i++) { Row row = sheet.getRow(i); if (row == null) { log.warn("第{}行为空,已跳过", i + 1); continue; } // 检查是否为空白行(所有单元格都为空) if (isRowEmpty(row)) { log.warn("第{}行所有单元格为空,已跳过", i + 1); continue; } // 创建实例 T entity = clazz.newInstance(); boolean hasData = false; // 遍历列 for (int j = 0; j < row.getLastCellNum(); j++) { Cell cell = row.getCell(j); // 获取对应的字段 Field field = fieldMap.get(j); Excel excel = excelAnnotationMap.get(j); if (field != null && excel != null) { try { // 获取单元格值 Object value = null; if (cell != null) { // 特殊处理楼层字段 if (field.getName().equalsIgnoreCase("floorNumber") || (excel.name() != null && (excel.name().contains("楼层") || excel.name().contains("层数")))) { if (field.getType() == String.class) { // 如果是字符串类型的楼层字段,使用特殊处理方法 value = getIntegerStringFromCell(cell); log.debug("特殊处理楼层字段: {}, 原值: {}, 处理后: {}", field.getName(), cell, value); } else { value = getCellValue(cell, field.getType()); } } else { value = getCellValue(cell, field.getType()); } } // 必填字段校验 if (excel.required() && (value == null || value.toString().trim().isEmpty())) { throw new IOException("第" + (i + 1) + "行" + excel.name() + "不能为空"); } // 处理枚举转换 if (value != null && !StringUtils.isEmpty(excel.readConverterExp())) { try { value = convertByExp(value.toString(), excel.readConverterExp(), false); } catch (Exception e) { log.warn("第{}行{}列值[{}]转换异常", i + 1, excel.name(), value, e); } } // 设置字段值 if (value != null) { field.set(entity, value); hasData = true; log.debug("第{}行{}列设置值: {}", i + 1, excel.name(), value); } } catch (Exception e) { log.error("解析第{}行{}列发生异常", i + 1, excel.name(), e); throw new IOException("第" + (i + 1) + "行" + excel.name() + "解析失败: " + e.getMessage()); } } } // 添加到列表 if (hasData) { list.add(entity); log.debug("第{}行数据已添加", i + 1); } else { log.warn("第{}行没有有效数据,已跳过", i + 1); } } log.info("Excel导入完成,共解析{}条记录", list.size()); } catch (Exception e) { log.error("Excel导入异常", e); if (e instanceof IOException) { throw e; } else { throw new IOException("Excel导入失败: " + e.getMessage(), e); } } finally { if (workbook != null) { try { workbook.close(); } catch (IOException e) { log.error("关闭Excel工作簿异常", e); } } if (is != null) { try { is.close(); } catch (IOException e) { log.error("关闭输入流异常", e); } } } return list; } /** * 导出Excel到HttpServletResponse * * @param response HTTP响应对象 * @param fileName 文件名(不含扩展名) * @param sheetName 工作表名称 * @throws Exception 异常 */ public void exportExcel(HttpServletResponse response, String fileName, String sheetName) throws Exception { response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String downloadFileName = java.net.URLEncoder.encode(fileName, "UTF-8") + ".xlsx"; response.setHeader("Content-disposition", "attachment;filename=" + downloadFileName); // 创建一个空的列表,只导出表头 exportExcel(new ArrayList<>(), sheetName, response.getOutputStream()); } /** * 检查行是否为空 * * @param row 要检查的行 * @return 如果行所有单元格都为空,则返回true;否则返回false */ private boolean isRowEmpty(Row row) { for (int i = 0; i < row.getLastCellNum(); i++) { Cell cell = row.getCell(i); if (cell != null && cell.getCellType() != CellType.BLANK) { return false; } } return true; } /** * 处理数值型单元格为整数字符串 * 特别用于处理楼层编号等需要保持为整数形式的字段 * * @param cell 单元格 * @return 整数形式的字符串 */ private String getIntegerStringFromCell(Cell cell) { if (cell == null) { return null; } String result = null; switch (cell.getCellType()) { case STRING: String strValue = cell.getStringCellValue().trim(); if (!strValue.isEmpty()) { try { // 尝试解析为数字 double doubleValue = Double.parseDouble(strValue); if (doubleValue == Math.floor(doubleValue) && !Double.isInfinite(doubleValue)) { result = String.valueOf((long) doubleValue); } else { result = strValue; } } catch (NumberFormatException e) { // 不是数字,保持原样 result = strValue; } } break; case NUMERIC: double numValue = cell.getNumericCellValue(); if (numValue == Math.floor(numValue) && !Double.isInfinite(numValue)) { result = String.valueOf((long) numValue); } else { result = String.valueOf(numValue); } break; case FORMULA: try { String formulaValue = cell.getStringCellValue(); result = formulaValue; } catch (Exception e) { try { double numericValue = cell.getNumericCellValue(); if (numericValue == Math.floor(numericValue) && !Double.isInfinite(numericValue)) { result = String.valueOf((long) numericValue); } else { result = String.valueOf(numericValue); } } catch (Exception ex) { // 无法处理,返回null } } break; default: // 其他类型返回null } return result; } }