633 lines
25 KiB
Java
633 lines
25 KiB
Java
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 <T> Excel导入导出对象类型
|
||
*/
|
||
public class ExcelUtil<T> {
|
||
private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class);
|
||
|
||
/**
|
||
* 实体类型
|
||
*/
|
||
private Class<T> clazz;
|
||
|
||
/**
|
||
* 初始化
|
||
*
|
||
* @param clazz Excel导入导出对象类型
|
||
*/
|
||
public ExcelUtil(Class<T> 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<T> 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<Field> 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<T> importExcel(InputStream is) throws Exception {
|
||
List<T> 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<Integer, Field> fieldMap = new HashMap<>();
|
||
Map<Integer, Excel> excelAnnotationMap = new HashMap<>();
|
||
Map<String, Field> fieldNameMap = new HashMap<>();
|
||
Map<String, Excel> 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;
|
||
}
|
||
} |