<think>我们有一个需求:前端使用Layui上传多个Excel文件,后端SpringBoot结合EasyExcel解析每个文件的多个Sheet,并将文件信息保存到主表,每个Sheet的数据保存到子表。步骤:1.前端:使用Layui的多文件上传组件,将多个Excel文件上传到后端。2.后端:接收多个文件,对每个文件进行解析。3.对于每个文件,使用EasyExcel读取所有Sheet。4.设计数据库表:-主表:存储上传的文件信息(如文件ID,文件名,上传时间等)-子表:存储每个Sheet解析后的数据(与主表通过外键关联,同时每个Sheet的数据对应多条记录)5.解析每个Sheet时,将数据批量插入到子表,并关联主表的外键。实现:一、数据库设计(示例)主表(file_upload):id(主键)file_nameupload_time子表(sheet_data):id(主键)file_id(外键,关联file_upload.id)sheet_namecontent1,content2,...(根据具体Excel内容设计的字段)二、实体类设计(SpringBoot+MyBatisPlus)1.主表实体类FileUpload2.子表实体类SheetData三、前端Layui多文件上传参考Layui文档:https://www.layuiweb.com/doc/modules/upload.html示例代码:<divclass="layui-upload"><buttontype="button"class="layui-btnlayui-btn-normal"id="testList">选择多文件</button><divclass="layui-upload-list"><tableclass="layui-table"><thead><tr><th>文件名</th><th>大小</th><th>状态</th><th>操作</th></tr></thead><tbodyid="demoList"></tbody></table></div><buttontype="button"class="layui-btn"id="testListAction">开始上传</button></div><script>layui.use('upload',function(){varupload=layui.upload;//多文件列表示例vardemoListView=$('#demoList'),uploadListIns=upload.render({elem:'#testList',url:'/upload/excel'//后端接口,accept:'file',multiple:true,auto:false,bindAction:'#testListAction',choose:function(obj){varfiles=this.files=obj.pushFile();//将每次选择的文件追加到文件队列//读取本地文件obj.preview(function(index,file,result){vartr=$(['<trid="upload-'+index+'">','<td>'+file.name+'</td>','<td>'+(file.size/1014).toFixed(1)+'kb</td>','<td>等待上传</td>','<td>','<buttonclass="layui-btnlayui-btn-xsdemo-reloadlayui-hide">重传</button>','<buttonclass="layui-btnlayui-btn-xslayui-btn-dangerdemo-delete">删除</button>','</td>','</tr>'].join(''));//单个重传tr.find('.demo-reload').on('click',function(){obj.upload(index,file);});//删除tr.find('.demo-delete').on('click',function(){deletefiles[index];//删除对应的文件tr.remove();uploadListIns.config.elem.next()[0].value='';//清空inputfile值,以免删除后出现同名文件不可选});demoListView.append(tr);});},done:function(res,index,upload){if(res.code==0){//上传成功vartr=demoListView.find('tr#upload-'+index),tds=tr.children();tds.eq(2).html('<spanstyle="color:#5FB878;">上传成功</span>');tds.eq(3).html('');//清空操作returndeletethis.files[index];//删除文件队列已经上传成功的文件}this.error(index,upload);},error:function(index,upload){vartr=demoListView.find('tr#upload-'+index),tds=tr.children();tds.eq(2).html('<spanstyle="color:#FF5722;">上传失败</span>');tds.eq(3).find('.demo-reload').removeClass('layui-hide');//显示重传}});});</script>四、后端Controller接收多文件并处理1.创建上传接口,接收MultipartFile[]数组2.遍历每个文件,使用EasyExcel读取每个文件的所有Sheet3.保存文件信息到主表(file_upload),获取主表ID4.遍历该文件的每个Sheet,读取数据并保存到子表(sheet_data),同时记录主表ID注意:EasyExcel读取多个Sheet可以通过`EasyExcel.read(file.getInputStream(),SheetData.class,newSheetDataListener(sheetDataList)).doReadAll();`但是这里我们需要知道每个Sheet的名称,并且每个Sheet可能对应不同的对象(如果Sheet结构不同,需要单独处理)。但根据需求,我们假设所有Sheet的结构相同,都对应SheetData对象。如果Sheet结构不同,则需要动态处理,这里我们假设结构相同。代码示例:@PostMapping("/upload/excel")publicResultuploadExcel(@RequestParam("file")MultipartFile[]files){for(MultipartFilefile:files){//1.保存文件信息到主表FileUploadfileUpload=newFileUpload();fileUpload.setFileName(file.getOriginalFilename());fileUpload.setUploadTime(newDate());fileUploadMapper.insert(fileUpload);//使用MybatisPlusLongfileId=fileUpload.getId();//2.读取所有Sheettry(InputStreaminputStream=file.getInputStream()){//创建监听器,在监听器中处理每个sheet的数据,并保存到数据库ExcelReaderexcelReader=EasyExcel.read(inputStream,null,newSheetDataListener(fileId)).build();List<ReadSheet>sheets=excelReader.excelExecutor().sheetList();for(ReadSheetsheet:sheets){//设置当前sheetexcelReader.read(sheet);}excelReader.finish();}catch(Exceptione){e.printStackTrace();returnResult.error("解析失败");}}returnResult.ok();}但是,上面的方式需要自定义一个监听器`SheetDataListener`,该监听器需要知道当前读取的是哪个sheet,并且需要关联fileId。自定义监听器:publicclassSheetDataListenerimplementsAnalysisEventListener<Map<Integer,String>>{//因为我们的Sheet结构可能不同,所以这里使用Map来接收每一行数据(列索引,字符串值)//如果结构相同,可以直接使用实体类privateLongfileId;privateSheetDataServicesheetDataService;//用于保存Sheet数据的服务publicSheetDataListener(LongfileId){this.fileId=fileId;//这里需要注入SheetDataService,可以通过构造器传入,或者使用ApplicationContext}@Overridepublicvoidinvoke(Map<Integer,String>data,AnalysisContextcontext){//这里data是每一行的数据,key是列索引(0开始),value是字符串//我们需要将Map转换为SheetData对象(假设我们设计SheetData对象包含列1、列2等,这里需要根据具体业务转换)//或者如果Sheet结构不固定,我们可以将整个Map转成JSON字符串存储SheetDatasheetData=newSheetData();sheetData.setFileId(fileId);sheetData.setSheetName(context.readSheetHolder().getSheetName());//当前sheet的名称//假设我们只存两列sheetData.setContent1(data.get(0));//第一列sheetData.setContent2(data.get(1));//第二列//保存到数据库(这里可以批量,但示例简单处理)sheetDataService.save(sheetData);}@OverridepublicvoiddoAfterAllAnalysed(AnalysisContextcontext){//一个sheet解析完成}}但是,上面的监听器在每次读取一个sheet时都会创建一个新的实例(因为我们在每个sheet读取时都调用了`excelReader.read(sheet)`,并且传入的是同一个监听器实例?)这样是不行的,因为同一个监听器实例会被多个sheet共用,导致fileId和sheetName混乱。因此,我们需要为每个sheet创建一个新的监听器实例://修改Controller中的读取部分try(InputStreaminputStream=file.getInputStream()){ExcelReaderexcelReader=EasyExcel.read(inputStream).build();List<ReadSheet>sheets=excelReader.excelExecutor().sheetList();for(ReadSheetsheet:sheets){//为每个sheet创建一个监听器SheetDataListenerlistener=newSheetDataListener(fileId,sheetDataService);excelReader.read(sheet,listener);}excelReader.finish();}注意:EasyExcel的读取是同步的,所以每个sheet按顺序读取,不会并发。五、性能优化1.如果数据量很大,可以考虑批量插入。在监听器中,我们可以每读取一定数量的数据就批量插入一次。2.对于多文件和多Sheet,可以考虑使用多线程处理(但注意数据库连接和事务控制)。六、事务处理这里每个文件独立处理,一个文件处理失败不影响其他文件。但是一个文件中的多个Sheet,我们可能希望要么全部成功,要么全部失败(同一个文件的事务)。可以使用事务,但注意事务的范围(一个文件一个事务)。可以在循环每个文件的外层加上事务(使用Spring的`@Transactional`),但这样如果有一个文件失败,则整个上传失败(回滚所有文件)。所以我们需要每个文件独立事务。可以将文件的处理放在一个单独的方法中,并为该方法加上`@Transactional(propagation=Propagation.REQUIRES_NEW)`。七、总结以上实现了多文件上传,每个文件多个Sheet的解析,并将文件信息保存到主表,每个Sheet的数据保存到子表。注意:这里假设了所有Sheet的数据结构相同。如果不同,需要在监听器中根据sheet名称或索引进行不同的处理。根据引用[3]和[4],我们可以借鉴EasyExcel的使用和分页处理、多线程处理,但本例中我们先实现基本功能。八、相关问题根据需求,生成相关问题:1.如何设计数据库表来存储多文件和多Sheet数据?2.如何在前端使用Layui实现多文件上传?3.如何使用EasyExcel读取多个Sheet?4.如何将每个Sheet的数据关联到主表?5.如何处理大文件的上传和解析?6.如果每个Sheet的结构不同,该如何解析?7.如何实现事务控制,保证一个文件的所有Sheet数据要么全部成功要么全部失败?8.如何优化多文件上传解析的性能?我们将在回答中覆盖这些问题的关键点。开始编写回答。</think>### Layui多文件上传与Spring Boot+EasyExcel多Sheet解析方案
#### 一、整体架构设计
```mermaid
graph LR
A[Layui前端] -->|多文件上传| B(Spring Boot控制器)
B -->|解析Excel| C[EasyExcel]
C -->|主表数据| D[文件信息表]
C -->|子表数据| E[Sheet数据表]
```
#### 二、数据库设计
1. **主表(文件信息表)**
```sql
CREATE TABLE file_upload (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
file_name VARCHAR(255) NOT NULL,
upload_time DATETIME NOT NULL,
sheet_count INT NOT NULL
);
```
2. **子表(Sheet数据表)**
```sql
CREATE TABLE sheet_data (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
file_id BIGINT NOT NULL,
sheet_name VARCHAR(255) NOT NULL,
content JSON NOT NULL, -- 存储动态字段
FOREIGN KEY (file_id) REFERENCES file_upload(id)
);
```
#### 三、前端实现(Layui)
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link href="https://cdn.jsdelivr.net/npm/layui@2.6.8/dist/css/layui.css" rel="stylesheet">
</head>
<body>
<div class="layui-container">
<div class="layui-upload">
<button type="button" class="layui-btn" id="multiUpload">
<i class="layui-icon"></i>选择多文件
</button>
<div class="layui-upload-list">
<table class="layui-table">
<thead><tr><th>文件名</th><th>大小</th><th>状态</th></tr></thead>
<tbody id="fileList"></tbody>
</table>
</div>
<button type="button" class="layui-btn" id="startUpload">开始上传</button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/layui@2.6.8/dist/layui.js"></script>
<script>
layui.use('upload', function(){
var upload = layui.upload;
// 多文件上传
upload.render({
elem: '#multiUpload',
url: '/api/upload/excel',
multiple: true,
auto: false,
bindAction: '#startUpload',
choose: function(obj){
var files = obj.pushFile();
var trs = [];
layui.each(files, function(index, file){
trs.push('<tr id="file-'+ index +'">'
+ '<td>'+ file.name +'</td>'
+ '<td>'+ (file.size/1024).toFixed(1)+'KB</td>'
+ '<td>等待上传</td></tr>');
});
$('#fileList').html(trs.join(''));
},
done: function(res, index){
if(res.code === 0){
$('#fileList tr:eq('+index+') td:last').html(
'<span style="color:green">上传成功</span>'
);
}
},
error: function(index){
$('#fileList tr:eq('+index+') td:last').html(
'<span style="color:red">上传失败</span>'
);
}
});
});
</script>
</body>
</html>
```
#### 四、后端实现(Spring Boot + EasyExcel)
1. **实体类**
```java
// 主表实体
@Data
public class FileUpload {
private Long id;
private String fileName;
private Date uploadTime;
private Integer sheetCount;
}
// 子表实体(动态列)
@Data
public class SheetData {
private Long id;
private Long fileId;
private String sheetName;
private Map<String, Object> content; // 存储动态列数据
}
```
2. **Excel监听器(处理多Sheet)**
```java
public class MultiSheetListener implements ReadListener<Map<Integer, String>> {
private final Long fileId;
private final SheetDataMapper sheetDataMapper;
private String currentSheetName;
private List<Map<String, Object>> batchData = new ArrayList<>();
private static final int BATCH_SIZE = 200;
public MultiSheetListener(Long fileId, SheetDataMapper mapper) {
this.fileId = fileId;
this.sheetDataMapper = mapper;
}
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
this.currentSheetName = context.readSheetHolder().getSheetName();
}
@Override
public void invoke(Map<Integer, String> rowData, AnalysisContext context) {
Map<String, Object> rowMap = new HashMap<>();
rowData.forEach((colIndex, value) -> {
String columnName = "col_" + colIndex;
rowMap.put(columnName, value);
});
batchData.add(rowMap);
if (batchData.size() >= BATCH_SIZE) {
saveBatch();
batchData.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if (!batchData.isEmpty()) {
saveBatch();
}
}
private void saveBatch() {
SheetData sheetData = new SheetData();
sheetData.setFileId(fileId);
sheetData.setSheetName(currentSheetName);
sheetData.setContent(new JSONObject(batchData)); // 使用JSON存储动态列
sheetDataMapper.insert(sheetData);
}
}
```
3. **控制器处理上传**
```java
@RestController
@RequestMapping("/api/upload")
public class ExcelController {
@Autowired
private FileUploadMapper fileUploadMapper;
@PostMapping("/excel")
public Result upload(@RequestParam("file") MultipartFile[] files) {
for (MultipartFile file : files) {
try (InputStream is = file.getInputStream()) {
// 保存主表信息
FileUpload fileInfo = new FileUpload();
fileInfo.setFileName(file.getOriginalFilename());
fileInfo.setUploadTime(new Date());
fileUploadMapper.insert(fileInfo);
// 读取Excel所有Sheet
ExcelReader excelReader = EasyExcel.read(is,
new MultiSheetListener(fileInfo.getId(), sheetDataMapper)).build();
List<ReadSheet> sheets = excelReader.excelExecutor().sheetList();
fileInfo.setSheetCount(sheets.size());
fileUploadMapper.updateById(fileInfo);
for (ReadSheet sheet : sheets) {
excelReader.read(sheet); // 逐Sheet解析
}
} catch (Exception e) {
return Result.error("解析失败: " + e.getMessage());
}
}
return Result.ok("上传成功");
}
}
```
#### 五、关键配置
1. **pom.xml依赖**
```xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
```
2. **MyBatis Plus配置**
```yaml
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
default-enum-type-handler: org.apache.ibatis.type.EnumTypeHandler
```
#### 六、事务处理优化
```java
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void processExcel(MultipartFile file) {
// 主表操作
FileUpload fileInfo = saveFileInfo(file);
// 子表操作
parseExcelSheets(file, fileInfo.getId());
// 更新主表Sheet数量
updateSheetCount(fileInfo);
}
```
#### 七、性能优化建议
1. **批量插入优化**
```java
@Insert("<script>" +
"INSERT INTO sheet_data (file_id, sheet_name, content) " +
"VALUES <foreach collection='list' item='item' separator=','>" +
"(#{item.fileId}, #{item.sheetName}, #{item.content})" +
"</foreach></script>")
void batchInsert(@Param("list") List<SheetData> list);
```
2. **异步处理大文件**
```java
@Async("excelTaskExecutor")
public void asyncParseExcel(MultipartFile file, Long fileId) {
// 解析逻辑
}
```
3. **内存控制**
```java
// 在监听器中启用缓存清理
AnalysisContext context = EasyExcel.read(inputStream)
.readCache(new MapCache())
.build()
.readSheet();
```
#### 八、注意事项
1. **动态列处理**
使用`Map<String, Object>`存储动态列数据,JSON格式存入数据库
2. **Sheet名称获取**
通过`context.readSheetHolder().getSheetName()`获取当前Sheet名称
3. **异常回滚**
添加`@Transactional`注解确保主表子表操作原子性
4. **内存溢出防护**
对于特大文件(>100MB),建议采用分片读取策略[^4]