复杂Excel文件导入功能(使用AI快速实现)
1 前言
对复杂Excel文件的处理,可以根据不同依赖使用两种不同的处理方式:
- easyexcel(注解加拦截器实现)
- Apache POI(工具类实现)
文章采用方法是通过Apache POI
实现,使用的是Springboot+Vue
框架.提供讲解思路,在理解思路的基础后,处理Excel文件其实就是一个很重复繁琐的过程,此时推荐结合AI快速完成文件导入功能.并提供常见的Excel文件常见导入错误处理.
这里的
复杂
是指:
具有复杂表头
一个Excel文件中有多张表要读取
文章提供方法也适合简单的Excel表格导入,比如表头只占一行的.
2 实现思路
简单来说就下面几步:(后面有代码实现和具体例子)
- 创建Excel文件对应的数据库表格和对应实体类.
- 创建Excel表格处理工具类.
- 控制器调用工具类方法.
- 服务层实现业务逻辑(可选)
对于第一步,创建数据库表格和实体类与Excel表格相对应即可.
2.1 Excel表格处理工具类
思路是
- 获取输入流,读取上传的Excel文件内容.使用 Apache POI 的 WorkbookFactory 工具类,通过输入流创建一个 Workbook 对象。
- 因为有多张表格,需要一个循环读取到全部表格.workbook.getNumberOfSheets()方法可以获取表格数量用于循环条件的边界值判断.
- 每循环一次调用一次单表处理方法.
Excel表格处理上,步骤为:
- 读取表头,需要指定表头开始行数,同时获取表头列数作为循环边界值,把它读取到数组中作为
key
. - 读取表格内容,需要指定从第几行开始读取数据,避免读取无效信息.以sheet.getLastRowNum()方法获取记录的最后一行作为循环边界值.
- 将获取到的单元格cell值和对应的
key
放入map中,读取完一行值后,将map的值映射到实体类的对象中 - 将实体类对象的值插入数据库.
代码实现(已经添加相关注解)
映射字段(mapToEntity()方法)时不要一个个复制粘贴,直接输出读取到的表头,交给AI帮映射就行.注意创建数据库表格时给备注对应的字段信息后,AI会在生成工具类代码时自动帮助映射.
@Component
public class CandidatePoolInfoExcelImportUtil {
/**
* 导入指定工作表的数据
*
* @param file Excel文件
* @param sheetIndex 工作表索引
* @return CandidatePoolInfo列表
* @throws Exception 解析异常
*/
public static List<CandidatePoolInfo> importExcel(MultipartFile file, int sheetIndex) throws Exception {
InputStream inputStream = file.getInputStream();
Workbook workbook = WorkbookFactory.create(inputStream);
// 读取对应索引的工作表
Sheet sheet = workbook.getSheetAt(sheetIndex);
List<CandidatePoolInfo> resultList = importSheet(sheet);
workbook.close();
inputStream.close();
return resultList;
}
/**
* 导入Excel文件中所有工作表的数据
*
* @param file Excel文件
* @return 所有工作表的CandidatePoolInfo数据列表
* @throws Exception 解析异常
*/
public static List<CandidatePoolInfo> importExcelAllSheets(MultipartFile file) throws Exception {
List<CandidatePoolInfo> allResults = new ArrayList<>();
// 获取输入流,读取上传的Excel文件内容
InputStream inputStream = file.getInputStream();
// 创建工作簿
Workbook workbook = WorkbookFactory.create(inputStream);
// 遍历所有工作表
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
// 获取工作表
Sheet sheet = workbook.getSheetAt(i);
// 检查工作表是否存在数据,这个条件大于等于4,原因是表格标题一行,表头占两行,共三行,有数据的话就4行
if (sheet != null && sheet.getLastRowNum() >= 4) {
List<CandidatePoolInfo> sheetResults = importSheet(sheet);
allResults.addAll(sheetResults);
}
}
workbook.close();
inputStream.close();
return allResults;
}
/**
* 导入单个工作表数据
*
* @param sheet 工作表对象
* @return CandidatePoolInfo列表
* @throws Exception 解析异常
*/
private static List<CandidatePoolInfo> importSheet(Sheet sheet) throws Exception {
// 读取表头(第3,4行,索引为2)
Row headerRow1 = sheet.getRow(2);
Row headerRow2 = sheet.getRow(3);
// 获取表头所占列数
int colCount = headerRow2.getLastCellNum();
String[] headers = new String[colCount];
for (int i = 0; i < colCount; i++) {
// 获取Excel表头两行(第3、4行)中指定列的单元格内容,并转换为字符串。
// 若单元格为空,则返回空字符串,避免空指针异常。
String h1 = Optional.ofNullable(headerRow1.getCell(i)).map(Cell::toString).orElse("");
String h2 = Optional.ofNullable(headerRow2.getCell(i)).map(Cell::toString).orElse("");
// 上下两级表头之间用'-'连接
StringJoiner joiner = new StringJoiner("-");
// 如果有表头,则添加
if (!h1.isEmpty()) joiner.add(h1);
if (!h2.isEmpty() && !h2.equals(h1)) joiner.add(h2);
// 去除空格并去除前后空格
headers[i] = joiner.toString().replaceAll("\\s+", "").trim();
// 如果表头重复,添加列索引确保唯一性
if (Arrays.asList(headers).subList(0, i).contains(headers[i])) {
headers[i] = headers[i] + "-" + i;
}
System.out.println("Column " + i + ": " + headers[i]);
}
List<CandidatePoolInfo> resultList = new ArrayList<>();
// 从第5行开始读取数据(索引为4)
for (int i = 4; i <= sheet.getLastRowNum(); i++) {
// 获取当前行
Row row = sheet.getRow(i);
if (row == null) continue;
Map<String, String> dataMap = new HashMap<>();
for (int j = 0; j < colCount; j++) {
Cell cell = row.getCell(j);
// 读取单元格内容并去除空格
dataMap.put(headers[j], cell != null ? cell.toString().trim() : "");
}
if (dataMap.values().stream().allMatch(String::isBlank)) continue;
CandidatePoolInfo candidate = mapToEntity(dataMap);
resultList.add(candidate);
}
return resultList;
}
private static CandidatePoolInfo mapToEntity(Map<String, String> row) {
CandidatePoolInfo item = new CandidatePoolInfo();
// 映射字段到CandidatePoolInfo对象
item.setUnit(row.get("单位"));
item.setName(row.get("姓名"));
item.setGender(parseGender(row.get("性别")));
item.setRecommendedPosition(row.get("拟推荐职务"));
item.setBirthDate(parseDate(row.get("出生年月(岁)")));
// System.out.println("BirthDate: " + row.get("出生年月(岁)"));
// item.setAge(parseLong(row.get("年龄")));
item.setNativePlace(row.get("籍贯"));
item.setEthnicity(row.get("民族"));
item.setPartyAge(row.get("党龄"));
item.setFulltimeEducation(row.get("全日制教育-学历学位"));
item.setFulltimeSchoolMajor(row.get("毕业院校系及专业"));
item.setParttimeEducation(row.get("在职教育-学历学位"));
item.setParttimeSchoolMajor(row.get("毕业院校系及专业-12"));
try {
item.setWorkStartDate(parseDate(row.get("参加工作时间")));
} catch (Exception e) {
// 日期解析失败时保持为null
}
return item;
}
private static String parseGender(String value) {
if (value == null || value.trim().isEmpty()) return null;
String trimmedValue = value.trim();
if (Pattern.matches("(?i)男|male|man|m", trimmedValue)) {
return "0"; // 男
} else if (Pattern.matches("(?i)女|female|woman|f", trimmedValue)) {
return "1"; // 女
} else {
return "2"; // 未知
}
}
private static Long parseLong(String value) {
try {
if (value == null || value.trim().isEmpty()) return null;
double number = Double.parseDouble(value.trim());
return (long) number;
} catch (NumberFormatException e) {
return null;
}
}
private static Date parseDate(String value) {
if (value == null || value.trim().isEmpty()) return null;
try {
// 检查是否为Excel日期序列号(数字格式)
if (value.matches("\\d+([.]\\d+)?")) {
double excelDate = Double.parseDouble(value.trim());
if (excelDate > 1000) { // 判断是否可能是Excel日期序列号
// Excel日期序列号转换为Java日期
// Excel的日期起始点是1900年1月1日,Java的日期起始点是1970年1月1日
// Excel认为1900年是闰年(实际上不是),因此需要减去2天的偏差
// 1900年1月1日到1970年1月1日相差25569天
long javaTimestamp = Math.round((excelDate - 25569) * 24 * 60 * 60 * 1000);
return new Date(javaTimestamp);
}
}
} catch (Exception e) {
// 如果转换失败,继续尝试其他格式
}
// 扩展支持常见的日期格式
String[] patterns = {
"yyyy-MM-dd",
"yyyy/MM/dd",
"yyyy.MM.dd",
"dd-MMM-yyyy",
"dd-MMMM-yyyy",
"yyyy年MM月dd日",
"yyyy年M月d日"
};
for (String pattern : patterns) {
try {
SimpleDateFormat sdf = new SimpleDateFormat(pattern, Locale.CHINESE);
return sdf.parse(value.trim());
} catch (ParseException e) {
// 继续尝试下一个格式
}
}
return null;
}
}
2.2 控制器和服务层方法
ExcelImportUtil
中已经返回了一个对象数组,此时对于业务简单的场景可以直接把对象数组数据插入数据库就可以了,下面讲一些业务场景的实现.下面控制器中需要两个参数,一个是文件,updateSupport 参数的作用是控制是否支持更新已有数据,默认为false.同时获取用户名来记录这次操作.
/**
* 批量Excel文件导入
*/
@PreAuthorize("@ss.hasPermi('team:pool:disciplinePool:import')")
@Log(title = "人选池", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
try {
// 导入全部 sheet
List<CandidatePoolInfo> list = CandidatePoolInfoExcelImportUtil.importExcelAllSheets(file);
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
String operName = loginUser.getUsername();
String message = candidatePoolInfoService.importExcel(list, updateSupport, operName);
return AjaxResult.success(message);
} catch (Exception e) {
return AjaxResult.error("导入失败:" + e.getMessage());
}
}
在服务层实现上,可以使用两个参数来记录成功插入数据数success
和失败数failure
,用于一些业务逻辑判断.下面做法中,判断对象是否为空,是因为一些Excel表格只有序号没有记录,此时不过滤会插入空数据.对于返回信息中的failMsg,不推荐这样处理,返回消息会直接将报错信息返给用户.
@Override
public String importExcel(List<CandidatePoolInfo> list, boolean updateSupport, String operName) {
int success = 0, failure = 0;
StringBuilder failMsg = new StringBuilder();
for (CandidatePoolInfo s : list) {
try {
// 更严格的判断条件,检查 unit 是否有效
if (s.getUnit() != null && !s.getUnit().trim().isEmpty() && !s.getUnit().trim().equals(" ")) {
if (updateSupport) {
s.setUpdateBy(operName);
candidatePoolInfoMapper.updateCandidatePoolInfo(s);
success++;
} else {
s.setCreateBy(operName);
candidatePoolInfoMapper.insertCandidatePoolInfo(s);
success++;
}
}
// unit 为空的数据直接跳过,不计入成功或失败计数
} catch (Exception e) {
failure++;
failMsg.append("<br/>").append(s.getName() != null ? s.getName() : "未知人员").append(":").append(e.getMessage());
}
}
return "导入成功!共 " + success + " 条,失败" + failure + " 条,失败信息如下:" + failMsg.toString();
}
2.3 常见Excel文件导入错误处理
常见错误有:
❗️ 方法未报错同时参数正确传递,但是插入数据库中的数据为空.
检查数据库表格字段,非空字段是否都有值传递,如果主键不是自增的也需要传递值.否则及时方法执行成功和参数正确传递也无法插入值.
❗️数据插入后,有些字段插入成功有些字段插入失败.
如果插入失败字段为日期格式,需要将字符串转化为日期格式,同时接受日期数据的变量上注明日期格式,例如
@JsonFormat(pattern = "yyyy-MM-dd")
private Date learnDate;
同理,整形也需要将字符串转为整形.
如果是字符串类型字段导入数据失败,注意将Excel数据映射到实体类对象中时是否有问题,例如:
item.setDuration(row.get("培训时长"));//对应表头可能是"培训时长(小时)"
此时表头和获取的key不同就没法赋值.处理方法是直接将获取到的表头打印出来复制上去就行.
**注意:**插入数据是否成功看数据库表格是否有对应数据,前端页面上可能存在一些处理逻辑使得一些数据不能正常显示.
前端使用字典时,注意需要将对应数据转化为整数后再导入.
❗️数据读取开始位置不对/读取数据成功但是与表格内容不对应
表头处理和数据处理时,开始的行数和列数没设置正确,表头一般是x行0列开始,数据根据自己需求来确定开始位置.
3 通过AI快速实现
首先根据上面我提供的工具类生成你的工具类,推荐AI通义灵码,能够读取你的代码上下文,能够更准确生成代码,而且是免费的,付费插件可以用cursor.
提示词prompt为:(根据自己需求修改)
你是一名软件开发师,参考ExcelImportUtil模板代码,数据库表格创建语句和对应实体类,实现文件导入功能.
需求如下:
1.表头为第x行到第x行,内容从第x行开始.
2.性别一栏前端使用字典,需要把字符转为数字,0为男,1为女,2为未知.
3.只读取第x张表到第x张表
数据库表格创建语句为:
---
使用例子如图所示,使用的是idea的通义灵码插件,下面图中三个代码分别对应你要实现的工具类,导入的实体类和模板代码.输入框里是提示词.
就会得到可以直接使用的代码,直接插入使用即可

3.1 扩展
如果使用的是若依框架进行开发,控制器方法和服务层实现方法可以和我相同.
直接把上面的控制器方法粘贴到对应控制器中,选中这段代码,给通义灵码对应指令即可.

服务层方法同理,直接粘贴模板代码,选中后提供相关指令让通义灵码修改即可.生成代码后,右上角蓝色图标三个功能为插入代码
,复制代码
,文件保存
模板代码不一定用文章中的,可以用其它模板
