智慧社区(十一)——Spring Boot 实现 Excel 导出、上传与数据导入全流程详解

发布于:2025-08-12 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、需求分析

本次需要实现三个核心功能:

  1. Excel 导出:根据分页、查询条件(用户名、小区 ID、手机号)查询居民信息,生成 Excel 文件并返回文件名。
  2. Excel 上传:接收前端上传的 Excel 文件,保存到服务器指定路径,返回生成的唯一文件名。
  3. 数据导入:读取已上传的 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 获取当前用户,记录数据创建者,便于数据溯源。


网站公告

今日签到

点亮在社区的每一天
去签到