一、添加依赖pom.xml
XML
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.0.1</version>
</dependency>
二、静态模板下载
直接去读取项目目录下的静态 Excel 模板文件,并以文件流的形式返回给前端。
java
/**
* 下载 Excel 导入模板
*/
@GetMapping("/downloadTemplate")
public void downloadTeamplate(HttpServletResponse response){
// 直接指定相对路径
String fileLocation = "template/基本仪器信息模板.xlsx";
// 调用父类/底层封装的下载方法,将文件写入 response 输出流
this.downloadTemplateFile(response,fileLocation);
}
三、PxFileUtils 通用工具类
Excel 的表头通常是中文(如"仪器名称"),而 Java 实体的属性是英文。需要在这个方法里维护一个巨大的字典,根据传入的表名返回对应的映射关系。
java
/**
* 获取表头中文与实体类英文属性的映射关系
*/
public static Map<String,String> getImportTitle(String tableType){
Map<String,String> titleMap = new HashMap<>();
if ("t_equipment".equals(tableType)){ // 仪器设备表映射
titleMap.put("固定资产编号","eqCode");
titleMap.put("仪器名称","eqName");
titleMap.put("部门名称","deptName");
titleMap.put("设备序列号","factoryNo");
titleMap.put("生产日期","manufactureDate");
// ... 添加更多字段
}
// 扩展其他表的映射...
else if ("t_user".equals(tableType)){
titleMap.put("登录名","userName");
titleMap.put("真实名称","realName");
// ...
}
return titleMap;
}
版本、解析表头、动态创建对象、反射赋值与类型强转
java
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.*;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
public class PxFileUtils {
/**
* 核心:解析Excel并转换为对应的 Java Bean 列表
* @param inputStream Excel文件流
* @param fileName 文件名(用于判断后缀)
* @param tableName 表名标志(用于获取映射字典)
* @param clazz 目标对象的 Class
* @param sheetNumer 要解析的 Sheet 页索引
*/
public static <T> List<T> getExcelDatasByTitle(InputStream inputStream, String fileName, String tableName, Class<T> clazz, Integer sheetNumer){
if (sheetNumer == null){
sheetNumer = 0;
}
// 1. 获取表头映射关系
Map<String,String> titleMap = getImportTitle(tableName);
Workbook wb;
InputStream is = null;
try {
is = new BufferedInputStream(inputStream);
String subfix = fileName.substring(fileName.lastIndexOf("."));
if (".xls".equalsIgnoreCase(subfix)) {
wb = new HSSFWorkbook(is); // 2003版本
} else if (".xlsx".equalsIgnoreCase(subfix)) {
wb = new XSSFWorkbook(is); // 2007版本
} else {
return new ArrayList<>();
}
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
DataFormatter formatter = new DataFormatter();
Sheet sheet = wb.getSheetAt(sheetNumer);
if (sheet == null) return null;
List<T> allDatas = new ArrayList<>();
Iterator<Row> rowIterator = sheet.iterator();
Map<Integer,String> columnMap = new HashMap<>();
int rowNum = 0;
int allCellCount = 0;
// 2. 逐行解析
while (rowIterator.hasNext()) {
Row row = rowIterator.next();
// 2.1 处理第一行表头
if(rowNum == 0){
if(row == null) throw new RuntimeException("表头不能为空!");
allCellCount = row.getLastCellNum();
// 将 Excel 列索引与 Java 属性名绑定
columnMap = getTitles(titleMap, row, dateFormat, formatter, allCellCount);
rowNum++;
continue;
}
// 2.2 处理数据行,通过反射实例化对象
T obj = clazz.getDeclaredConstructor().newInstance();
for (int colIndex = 0; colIndex < allCellCount; colIndex++) {
String header = columnMap.get(colIndex);
if (header == null || header.isEmpty()) continue;
String cellValue = getCellValue(row.getCell(colIndex), dateFormat, formatter);
if (cellValue == null || cellValue.isEmpty()) continue;
// 2.3 通过反射根据属性名设置对象值
Field field = getField(clazz, header);
if (field != null) {
field.setAccessible(true);
setFieldValue(obj, field, cellValue, dateTimeFormatter);
}
}
allDatas.add(obj);
}
return allDatas;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Excel解析失败!");
} finally {
if (is != null){
try { is.close(); } catch (IOException e) {}
}
}
}
// 内部方法:获取表头对应的 Java 属性名
private static Map<Integer,String> getTitles(Map<String, String> titleMap, Row row, DateFormat dateFormat, DataFormatter formatter, int allCellCount){
Map<Integer,String> maps = new HashMap<>();
for(int i = 0; i < allCellCount; i++){
Cell cell = row.getCell(i);
if(cell == null) continue;
String cellValue = getCellValue(cell, dateFormat, formatter);
// 处理类似 "规格型号(必填)" 带有括号的表头
if (cellValue != null && cellValue.contains("(")){
cellValue = cellValue.substring(0, cellValue.indexOf("("));
}
if(cellValue != null && !cellValue.isEmpty()){
String key = titleMap.get(cellValue);
if(key != null && !key.isEmpty()){
maps.put(i, key);
}
}
}
return maps;
}
// 内部方法:递归/下划线转驼峰获取 Field
private static Field getField(Class<?> clazz, String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
fieldName = convertToCamelCase(fieldName);
try { field = clazz.getDeclaredField(fieldName); } catch (NoSuchFieldException ignored) {}
}
return field;
}
// 内部方法:下划线转驼峰
private static String convertToCamelCase(String fieldName) {
StringBuilder camelCaseString = new StringBuilder();
boolean nextUpperCase = false;
for (int i = 0; i < fieldName.length(); i++) {
char c = fieldName.charAt(i);
if (c == '_') {
nextUpperCase = true;
} else {
camelCaseString.append(nextUpperCase ? Character.toUpperCase(c) : Character.toLowerCase(c));
nextUpperCase = false;
}
}
return camelCaseString.toString();
}
// 内部方法:根据字段类型强转并赋值
private static void setFieldValue(Object obj, Field field, String value, DateTimeFormatter dateTimeFormatter) throws IllegalAccessException {
Class<?> type = field.getType();
if (type == String.class) {
field.set(obj, value);
} else if (type == Integer.class || type == int.class) {
field.set(obj, Integer.parseInt(value));
} else if (type == Long.class || type == long.class) {
field.set(obj, Long.parseLong(value));
} else if (type == Double.class || type == double.class) {
field.set(obj, Double.parseDouble(value));
} else if (type == LocalDateTime.class) {
field.set(obj, LocalDateTime.parse(value, dateTimeFormatter));
} else if (type == LocalDate.class ) {
field.set(obj, LocalDate.parse(value));
} else {
field.set(obj, value);
}
}
// 内部方法:智能读取单元格内容,处理数字后面的 ".0"
private static String getCellValue(Cell cell, DateFormat dateFormat, DataFormatter formatter){
if(cell == null) return "";
String value;
switch (cell.getCellType()) {
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
value = dateFormat.format(cell.getDateCellValue());
} else {
value = formatter.formatCellValue(cell);
if(value != null && value.endsWith(".0")){
value = value.replace(".0", "");
}
}
break;
case BLANK:
value = null;
break;
default:
value = formatter.formatCellValue(cell);
}
return value == null ? null : value.replace("\n", "");
}
}
Service 层实现
java
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class EquipmentServiceImpl {
public JsonResult importExcel(MultipartFile file) {
try {
// 1. 调用引擎,一键获取解析后的实体列表
List<EquipmentBean> importList = PxFileUtils.getExcelDatasByTitle(
file.getInputStream(),
file.getOriginalFilename(),
"t_equipment", // 对应映射字典的 Key
EquipmentBean.class,
0
);
// 2. 基础数据校验
for (EquipmentBean bean : importList){
if (bean.getEqCode() == null || bean.getEqCode().isEmpty()){
throw new RuntimeException("【固定资产编号】不能为空!");
}
}
// 3. 性能优化:批量查询关联表,防止循环查库
// 提取所有涉及的部门名称去重
List<String> deptNames = importList.stream()
.map(EquipmentBean::getDeptName)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
Map<String, DepartmentBean> deptMap = new HashMap<>();
if(!deptNames.isEmpty()){
List<DepartmentBean> deptList = departmentService.getMapper().selectBeanByNames(deptNames);
deptList.forEach(t -> deptMap.put(t.getName(), t));
}
// 4. 数据补全与状态初始化
LocalDateTime now = LocalDateTime.now();
for (EquipmentBean bean : importList){
bean.setId(UUID.randomUUID().toString().replace("-", "")); // 生成主键
// 将中文部门名称翻译为系统的部门 ID
if (bean.getDeptName() != null){
DepartmentBean dept = deptMap.get(bean.getDeptName());
if (dept != null){
bean.setDeptId(dept.getId());
bean.setOrgId(dept.getOrgId());
}
}
bean.setCreateTime(now);
bean.setStatus("0"); // 默认状态
}
// 5. 批量插入数据库 (MyBatis-Plus 提供)
if (!importList.isEmpty()) {
getMapper().insertList(importList);
}
return new JsonResult(200, "成功导入" + importList.size() + "条数据!");
} catch (Exception e) {
e.printStackTrace();
return new JsonResult(1001, "导入失败:" + e.getMessage());
}
}
}