一、需求分析
本次需要实现三个核心功能:
- Excel 导出:根据分页、查询条件(用户名、小区 ID、手机号)查询居民信息,生成 Excel 文件并返回文件名。
- Excel 上传:接收前端上传的 Excel 文件,保存到服务器指定路径,返回生成的唯一文件名。
- 数据导入:读取已上传的 Excel 文件,解析数据并验证,最终将有效数据存入数据库。
二、技术选型
- 框架:Spring Boot 2.x
- Excel 处理:Apache POI(用于操作 Excel 文件,支持 xls 格式)
- 文件命名:UUID(生成唯一文件名,避免冲突)
- 数据校验:MyBatis-Plus(简化数据库操作与查询)
- 依赖管理:Maven
核心依赖(Apache POI 相关,已包含在 easypoi-starter 中):
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.2.0</version>
</dependency>
三、Excel 导出实现
1. 实现思路
- 准备 Excel 模板(含固定表头,如居民 ID、小区名称等)
- 根据查询条件获取居民数据
- 复制模板文件(避免修改原模板),生成带时间戳的新文件
- 使用 Apache POI 向新文件写入数据
- 返回生成的文件名
2. 代码实现
(1)Controller 层
接收前端参数,调用服务层查询数据,再通过工具类生成 Excel:
/**
* 文件导出接口
* @param personListForm 包含分页和查询条件的表单
* @return 包含生成的Excel文件名的响应
*/
@GetMapping("/exportExcel")
public Result exportExcel(PersonListForm personListForm){
// 调用服务层查询居民数据(分页)
PageVO pageVO = this.personService.personList(personListForm);
List<PersonVO> personList = pageVO.getList();
// 调用工具类生成Excel,excel为预设的文件存储路径
String fileName = ExcelUtil.ExpPersonInfo(personList, excel);
return Result.ok().put("data", fileName);
}
(2)Excel 工具类(核心)
负责复制模板、写入数据、设置样式:
public class ExcelUtil {
/**
* 导出居民信息到Excel
* @param personList 居民数据列表
* @param basePath 文件存储根路径
* @return 生成的Excel文件名
*/
public static String ExpPersonInfo(List<PersonVO> personList, String basePath){
POIFSFileSystem fs = null;
// 表头所在行数(模板中前2行为表头,从第3行开始写入数据)
int headRow = 2;
String generatedFileName = null;
try {
// 1. 复制模板文件(避免修改原模板)
String templatePath = basePath + "personInfo.xls"; // 模板路径
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String timeStamp = sdf.format(date);
generatedFileName = timeStamp + ".xls"; // 生成带时间戳的新文件名
String targetPath = basePath + generatedFileName;
// 复制文件流
FileInputStream fis = new FileInputStream(templatePath);
FileOutputStream fos = new FileOutputStream(targetPath);
byte[] buffer = new byte[1024 * 4];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fis.close();
fos.close();
// 2. 向新文件写入数据
fs = new POIFSFileSystem(new FileInputStream(targetPath));
HSSFWorkbook workbook = new HSSFWorkbook(fs); // 打开Excel工作簿
HSSFSheet sheet = workbook.getSheetAt(0); // 获取第一个工作表
// 设置单元格样式(边框)
HSSFCellStyle cellStyle = workbook.createCellStyle();
cellStyle.setBorderLeft(BorderStyle.THIN);
cellStyle.setBorderRight(BorderStyle.THIN);
cellStyle.setBorderTop(BorderStyle.THIN);
cellStyle.setBorderBottom(BorderStyle.THIN);
// 遍历数据列表,写入行
for (int i = 0; i < personList.size(); i++) {
PersonVO person = personList.get(i);
HSSFRow row = sheet.createRow(i + headRow); // 从表头后开始创建行
int col = 0;
// 按顺序写入单元格(与模板表头对应)
HSSFCell cell = row.createCell(col++);
cell.setCellStyle(cellStyle);
cell.setCellValue(person.getPersonId());
cell = row.createCell(col++);
cell.setCellStyle(cellStyle);
cell.setCellValue(person.getCommunityName());
// 省略其他字段(小区名称、楼栋、房号、姓名、性别等)...
}
// 3. 保存文件
FileOutputStream out = new FileOutputStream(targetPath);
workbook.write(out);
out.close();
} catch (Exception e) {
e.printStackTrace();
}
return generatedFileName;
}
}
3. 关键说明
- 模板复用:通过复制模板文件,避免每次导出修改原模板,保证模板稳定性。
- 样式统一:设置单元格边框样式,使导出的 Excel 格式更规范。
- 文件名唯一:使用时间戳命名,确保每次导出的文件不重复。
四、Excel 上传实现
1. 实现思路
- 接收前端上传的 MultipartFile 文件
- 验证文件是否为空
- 生成 UUID 唯一文件名(避免重名)
- 保存文件到服务器指定路径
- 返回生成的文件名(用于后续导入操作)
2. 代码实现
/**
* Excel上传接口
* @param file 上传的Excel文件
* @return 包含生成的文件名的响应
* @throws Exception 异常处理
*/
@PostMapping("/excelUpload")
public Result excelUpload(@RequestParam("uploadExcel") MultipartFile file) throws Exception {
// 验证文件是否为空
if (file.getOriginalFilename().isEmpty()) {
return Result.error("未选中上传文件");
}
// 生成唯一文件名(UUID + 原文件后缀)
String originalFileName = file.getOriginalFilename();
String fileSuffix = originalFileName.substring(originalFileName.lastIndexOf(".")); // 获取后缀(如.xls)
String newFileName = UUID.randomUUID().toString() + fileSuffix;
// 保存文件到指定路径(excel为预设的存储路径)
File targetFile = new File(excel, newFileName);
file.transferTo(targetFile);
return Result.ok().put("data", newFileName);
}
3. 关键说明
- 文件验证:防止空文件上传导致的错误。
- 唯一命名:使用 UUID 避免文件名冲突,确保文件存储安全。
- 路径管理:建议将文件存储路径配置在 application.properties 中,便于维护。
五、数据导入实现
1. 实现思路
- 从 Session 获取当前登录用户(用于记录数据创建者)
- 根据上传时返回的文件名,读取服务器上的 Excel 文件
- 使用 Apache POI 解析 Excel 数据(跳过表头行)
- 验证数据合法性(如小区名称是否存在)
- 将有效数据存入数据库
- 返回导入结果(成功 / 失败信息)
2. 代码实现
/**
* 数据导入接口
* @param fileName 上传的Excel文件名
* @param session 用于获取当前登录用户
* @return 导入结果响应
*/
@LogAnnotation("导入数据")
@PostMapping("/parsefile/{fileName}")
public Result parsefile(@PathVariable("fileName") String fileName, HttpSession session){
// 获取当前登录用户(用于设置数据创建者)
User currentUser = (User) session.getAttribute("user");
if (currentUser == null) {
return Result.error("请先登录");
}
POIFSFileSystem fs = null;
HSSFWorkbook workbook = null;
try {
// 读取服务器上的Excel文件
String filePath = excel + fileName;
fs = new POIFSFileSystem(new FileInputStream(filePath));
workbook = new HSSFWorkbook(fs);
} catch (Exception e) {
e.printStackTrace();
return Result.error("文件读取失败");
}
// 获取第一个工作表
HSSFSheet sheet = workbook.getSheetAt(0);
int totalRows = sheet.getLastRowNum() + 1; // 总行数(含表头)
int totalCols = sheet.getRow(0).getLastCellNum(); // 总列数
int headRow = 2; // 表头行数(前2行为表头,从第3行开始读取数据)
// 存储解析后的数据
Object[][] data = new Object[totalRows - headRow][totalCols];
DataFormatter dataFormatter = new DataFormatter(); // 用于格式化单元格数据为字符串
// 解析Excel数据
for (int i = headRow; i < totalRows; i++) {
HSSFRow row = sheet.getRow(i);
for (int j = 0; j < totalCols; j++) {
HSSFCell cell = row.getCell(j);
if (cell == null) {
data[i - headRow][j] = "";
continue;
}
// 格式化单元格数据(处理数字、日期等类型)
data[i - headRow][j] = dataFormatter.formatCellValue(cell);
}
}
// 验证并保存数据到数据库
String errorInfo = "";
int headRowNum = 3; // 错误提示中的行号(Excel实际行号,从1开始)
for (int i = 0; i < data.length; i++) {
Person person = new Person();
person.setState(1); // 默认状态为启用
person.setFaceUrl(""); // 头像默认为空
person.setCreater(currentUser.getUsername()); // 设置创建者
try {
int col = 1; // 从第2列开始读取(对应模板中的小区名称)
// 验证小区名称是否存在
String communityName = data[i][col++].toString();
QueryWrapper<Community> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("community_name", communityName);
Community community = communityService.getOne(queryWrapper);
if (community == null) {
errorInfo += "第" + (i + headRowNum) + "行:小区名称不存在;";
continue; // 跳过当前行
}
person.setCommunityId(community.getCommunityId());
// 填充其他字段(楼栋、房号、姓名、性别、手机号等)
person.setTermName(data[i][col++].toString());
person.setHouseNo(data[i][col++].toString());
person.setUserName(data[i][col++].toString());
person.setSex(data[i][col++].toString());
person.setMobile(data[i][col++].toString());
person.setPersonType(data[i][col++].toString());
person.setRemark(data[i][col++].toString());
// 保存到数据库
personService.save(person);
} catch (Exception e) {
errorInfo += "第" + (i + headRowNum) + "行:数据格式错误;";
e.printStackTrace();
}
}
if (!errorInfo.isEmpty()) {
return Result.ok().put("status", "fail").put("data", errorInfo);
} else {
return Result.ok().put("status", "success").put("data", "数据导入完成!");
}
}
3. 关键说明
- 数据解析:使用 DataFormatter 处理单元格数据,避免数字、日期等类型转换错误。
- 数据验证:导入前验证小区名称是否存在,避免无效数据入库。
- 错误处理:记录错误行号和原因,便于用户修正后重新导入。
- 用户关联:通过 Session 获取当前用户,记录数据创建者,便于数据溯源。