导入excel:
1. 引入 EasyExcel 依赖
如果你还没有引入 EasyExcel,请先在 pom.xml 中加上:
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.4</version> <!-- 建议使用较新的稳定版本 -->
</dependency>
2. 为 Entity 加上 EasyExcel 注解
为了让 EasyExcel 能够自动把 Excel 的列和对象的属性对应起来 row-by-row 地解析,建议在你的 ProductEntity 属性上加上 @ExcelProperty 注解(匹配 Excel 的表头名称):
java
import com.alibaba.excel.annotation.ExcelProperty;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.util.Date;
@Data
@TableName("product_new")
public class ProductEntity {
@TableId(type = IdType.AUTO)
private Long productId;
@ExcelProperty("一级分类(业务板块)")
private String firstCategory;
@ExcelProperty("一级分类说明")
private String firstDesc;
@ExcelProperty("二级分类-行业/能源类型")
private String secondIndustry;
@ExcelProperty("二级分类-阶段")
private String secondStage;
@ExcelProperty("二级分类-业务类型")
private String secondType;
@ExcelProperty("二级分类说明")
private String secondDesc;
@ExcelProperty("三级分类")
private String thirdCategory;
@ExcelProperty("三级分类说明")
private String thirdDesc;
@ExcelProperty("四级分类(标段级别)")
private String fourthCategory;
@ExcelProperty("采购方式")
private String procurementMethod;
@ExcelProperty("评审办法")
private String reviewMethod;
@ExcelProperty("对应招标文件范本")
private String bidTemplate;
@ExcelProperty("对应合同范本")
private String contractTemplate;
private Date createTime;
private Date updateTime;
}
3. 核心:编写 Excel 解析监听器
EasyExcel 是基于事件驱动的,需要一个监听器来边读边处理数据。为了防止一次性读入十几万条数据撑爆内存,我们采用分批插入(每满 100 条存一次数据库)的方式。
java
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class ProductExcelListener implements ReadListener<ProductEntity> {
/**
* 每隔100条存储数据库,实际使用中可以1000条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;
private final List<ProductEntity> cachedDataList = new ArrayList<>(BATCH_COUNT);
// 注入 service 用来写入数据库
private final ProductService productService;
public ProductExcelListener(ProductService productService) {
this.productService = productService;
}
/**
* 每一条数据解析都会进来
*/
@Override
public void invoke(ProductEntity data, AnalysisContext context) {
log.info("解析到一条品类数据: {}", data);
cachedDataList.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止几万条数据在内存中导致OOM
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
cachedDataList.clear();
}
}
/**
* 所有数据解析完成了都会来调用
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存一下,确保最后遗留的数据(不满 BATCH_COUNT 条)也被存入数据库
saveData();
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
if (!cachedDataList.isEmpty()) {
log.info("{}条数据,开始存储数据库!", cachedDataList.size());
productService.saveBatch(cachedDataList);
log.info("存储数据库成功!");
}
}
}
4. Service 实现类 (ServiceImpl)
在 ProductServiceImpl 中编写调用 EasyExcel 的核心逻辑:
java
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity> implements ProductService {
@Override
public void importProductExcel(MultipartFile file) {
try {
// 这里 需要指定用哪个监听器去读,并且传入当前 service 实例
EasyExcel.read(file.getInputStream(), ProductEntity.class, new ProductExcelListener(this))
.sheet()
.doRead();
} catch (IOException e) {
log.error("读取 Excel 文件失败", e);
throw new RuntimeException("文件解析异常,请稍后再试");
}
}
}
(ProductService 接口中声明了 void importProductExcel(MultipartFile file); )
5. 控制层 (Controller)
最后,暴露出一个 HTTP POST 接口,供前端上传文件。
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping("/import")
public ResponseEntity<String> importExcel(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("上传的文件不能为空");
}
// 简单校验下文件后缀,防止传错格式
String fileName = file.getOriginalFilename();
if (fileName == null || (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx"))) {
return ResponseEntity.badRequest().body("请上传正确的 Excel 文件 (.xls 或 .xlsx)");
}
try {
productService.importProductExcel(file);
return ResponseEntity.ok("品类数据导入成功!");
} catch (Exception e) {
return ResponseEntity.status(500).body("导入失败: " + e.getMessage());
}
}
}
- 关于时间字段:数据库中的 create_time 和 update_time 已经设置了 DEFAULT CURRENT_TIMESTAMP。通过这种批量插入时,如果 Excel 里没有这两列,MyBatis-Plus 插入后数据库会自动生成当前时间,无需在前端或后端代码里手动 Set。
- 监听器不能交由 Spring 管理 :注意到 ProductExcelListener 是每次读取时由 new 出来的。不要把它声明为 @Component 类似单例,因为它内部持有 cachedDataList。如果并发导入,单例会导致数据错乱。用 new 并通过构造器把 Service 传进去是 EasyExcel 的标准推荐写法
下载模板:
1. Service 接口
首先在 ProductService 接口中声明下载模板的方法:
java
import javax.servlet.http.HttpServletResponse;
public interface ProductService extends IService<ProductEntity> {
// 前面已实现的导入方法
void importProductExcel(MultipartFile file);
// 新增:下载模板方法
void downloadTemplate(HttpServletResponse response);
}
2. ServiceImpl 实现类
在 ProductServiceImpl 中实现该方法。我们通过 EasyExcel.write() 并传入 ProductEntity.class,它会自动根据类上配置的 @ExcelProperty("表头名称") 注解来动态生成 Excel 的表头。
java
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, ProductEntity> implements ProductService {
@Override
public void importProductExcel(MultipartFile file) {
// ... 前面已实现的导入逻辑
}
@Override
public void downloadTemplate(HttpServletResponse response) {
try {
// 1. 设置响应内容类型和字符集
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 2. 防止中文文件名乱码
String fileName = URLEncoder.encode("品类数据导入模板", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 3. 使用 EasyExcel 将空列表(或者你可以放一条示例数据)写入响应流
// 传入 ProductEntity.class 后,EasyExcel 会自动根据 @ExcelProperty 生成表头
EasyExcel.write(response.getOutputStream(), ProductEntity.class)
.sheet("品类模板")
.doWrite(new ArrayList<>()); // 传入空列表,只生成表头;如果需要示例数据,可以New一个对象加进去
} catch (IOException e) {
log.error("下载模板失败", e);
throw new RuntimeException("生成下载模板异常,请稍后再试");
}
}
}
3. Controller 控制层
在 ProductController 中暴露出一个 GET 请求接口,供前端(或浏览器直接访问)下载:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping("/import")
public ResponseEntity<String> importExcel(@RequestParam("file") MultipartFile file) {
// ... 前面已实现的导入接口
}
/**
* 下载品类导入模板
* 注意:因为是直接往 response 流里写数据,所以方法返回值设为 void 即可
*/
@GetMapping("/download-template")
public void downloadTemplate(HttpServletResponse response) {
productService.downloadTemplate(response);
}
}
💡 提示:如何添加"示例数据"?
如果你希望用户下载打开模板时,里面自带一行模拟数据 作为参考,只需要修改 ServiceImpl 中 .doWrite() 传入的参数即可。例如:
java
// 在 ServiceImpl 的 downloadTemplate 方法中:
List<ProductEntity> demoData = new ArrayList<>();
ProductEntity demo = new ProductEntity();
demo.setFirstCategory("新能源板块");
demo.setFirstDesc("包含风电、光伏等业务");
demo.setSecondIndustry("陆上风电");
demo.setSecondStage("前期阶段");
demo.setSecondType("服务");
demo.setFourthCategory("某风电场一期标段");
// ... 可以根据需要继续补充其他字段的示例
demoData.add(demo);
// 写入时传入 demoData 即可
EasyExcel.write(response.getOutputStream(), ProductEntity.class)
.sheet("品类模板")
.doWrite(demoData);