1. 从零开始为什么我们需要Excel模板导出与导入做后台管理系统尤其是像学校专业管理、客户信息录入、订单批量处理这类业务我敢说十个项目里有九个半都逃不开Excel的导入导出。我自己刚入行那会儿每次接到这种需求都头疼要么是导出的Excel格式乱七八糟业务部门看不懂要么是导入时数据校验不完整脏数据进了数据库还得手动去清理费时费力。后来用上了JeecgBoot我发现它虽然提供了强大的代码生成器和基础CRUD功能但在处理复杂的Excel交互时很多朋友还是有点无从下手。比如怎么导出一个带固定表头、甚至带下拉选择项的模板怎么在前端优雅地封装一个通用的导入组件而不是每个页面都写一遍上传逻辑这些实战中的“痒点”官方文档往往一笔带过但恰恰是决定开发效率和系统稳定性的关键。所以今天我就结合自己踩过的坑和优化后的经验跟你详细聊聊在JeecgBoot里如何高效、优雅地实现Excel模板导出和数据导入。我会把后端Controller的逻辑、前端的组件封装以及那些容易出错的细节掰开揉碎了讲清楚。目标就一个让你看完就能直接复制代码到自己的项目里用起来真正告别“百度一小时调试一整天”的窘境。2. 后端核心Controller层如何生成“聪明”的模板原始文章里给出的Controller代码是一个很好的起点但它只实现了最基础的导出——一个只有表头的空表格。在实际项目中我们往往需要更“聪明”的模板。2.1 基础模板导出不仅仅是表头我们先回顾并优化一下基础的导出代码。原始代码使用了HSSFWorkbook这是Apache POI库中处理老版本.xls格式的类。现在更推荐使用XSSFWorkbook来生成.xlsx格式它能支持更大的行数和更丰富的功能。/** * 导出专业名录模板 * param response */ RequestMapping(value /exportTemplate) public void exportMajorTemplate(HttpServletResponse response) { try { // 1. 设置响应头告诉浏览器这是一个Excel文件 String fileName 专业名录导入模板; response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(UTF-8); response.setHeader(Content-Disposition, attachment;filename URLEncoder.encode(fileName .xlsx, UTF-8)); // 2. 创建Excel工作簿和工作表 XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(专业信息); // 3. 创建标题行表头 String[] headers {专业大类*, 专业名称*, 专业介绍, 开设年份, 状态}; XSSFRow headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { XSSFCell cell headerRow.createCell(i); cell.setCellValue(headers[i]); // 设置列宽单位是1/256个字符宽度 sheet.setColumnWidth(i, 15 * 256); // 可以设置表头样式比如加粗、居中 XSSFCellStyle headerStyle workbook.createCellStyle(); XSSFFont font workbook.createFont(); font.setBold(true); headerStyle.setFont(font); cell.setCellStyle(headerStyle); } // 4. 可选添加一行示例数据指导用户填写 XSSFRow exampleRow sheet.createRow(1); exampleRow.createCell(0).setCellValue(例如工学); exampleRow.createCell(1).setCellValue(例如软件工程); exampleRow.createCell(2).setCellValue(例如学习编程语言、软件设计与开发...); exampleRow.createCell(3).setCellValue(例如2023); exampleRow.createCell(4).setCellValue(例如启用); // 可以将示例行的字体设置为灰色斜体以示区别 XSSFCellStyle exampleStyle workbook.createCellStyle(); XSSFFont exampleFont workbook.createFont(); exampleFont.setItalic(true); exampleFont.setColor(IndexedColors.GREY_50_PERCENT.getIndex()); exampleStyle.setFont(exampleFont); for (Cell cell : exampleRow) { cell.setCellStyle(exampleStyle); } // 5. 写出到响应流 workbook.write(response.getOutputStream()); workbook.close(); } catch (Exception e) { log.error(导出模板失败, e); // 更友好的错误处理可以返回JSON提示这里简单抛出异常 throw new RuntimeException(模板导出失败请稍后重试); } }这段代码做了几处关键改进使用了更新的XSSF库为表头添加了简单的样式使其更醒目额外添加了一行示例数据。这小小的改动对用户体验提升巨大用户一看就知道每列该填什么格式的内容避免了因理解偏差导致的数据错误。2.2 高级技巧为模板添加数据验证如果导出的模板能让用户直接在下拉列表里选择而不是手动输入那就能从根本上杜绝无效数据。Apache POI支持创建数据验证规则。// 在创建sheet和表头之后添加数据验证 private void addDataValidation(XSSFSheet sheet) { // 假设第4列索引3是“状态”我们提供“启用”、“停用”两个选项 XSSFDataValidationHelper dvHelper new XSSFDataValidationHelper(sheet); XSSFDataValidationConstraint dvConstraint (XSSFDataValidationConstraint) dvHelper.createExplicitListConstraint(new String[]{启用, 停用}); // 设置验证范围从第2行开始到第1000行第4列 CellRangeAddressList addressList new CellRangeAddressList(1, 1000, 3, 3); XSSFDataValidation validation (XSSFDataValidation) dvHelper.createValidation(dvConstraint, addressList); // 设置错误提示 validation.createErrorBox(输入错误, 请从下拉列表中选择“启用”或“停用”); validation.setShowErrorBox(true); sheet.addValidationData(validation); // 同理可以为“开设年份”列添加数字范围验证等 // DataValidationConstraint numericConstraint dvHelper.createNumericConstraint(DataValidationConstraint.OperatorType.BETWEEN, 2000, 2100); // CellRangeAddressList yearList new CellRangeAddressList(1, 1000, 3, 3); // DataValidation yearValidation dvHelper.createValidation(numericConstraint, yearList); // sheet.addValidationData(yearValidation); }把这个方法集成到上面的导出逻辑里用户下载的模板在“状态”这一列就会变成下拉选择框。这比在导入逻辑里写复杂的校验代码要直观和可靠得多。2.3 数据导入稳健的Service层处理逻辑原始文章的导入逻辑在Service层直接解析Excel并插入数据库这在实际生产环境中风险较高。我们需要更健壮的处理包括事务管理、数据校验和更清晰的错误反馈。Override Transactional(rollbackFor Exception.class) // 添加事务任何一条失败则全部回滚 public ResultObject importSchoolNewStuList(OrgFile orgFile) { MultipartFile file orgFile.getFile(); String userId orgFile.getUserId(); if (file.isEmpty()) { return Result.error(导入文件为空请选择文件); } // 校验文件类型 String originalFilename file.getOriginalFilename(); if (originalFilename null || (!originalFilename.endsWith(.xls) !originalFilename.endsWith(.xlsx))) { return Result.error(仅支持导入.xls或.xlsx格式的Excel文件); } ListMajorPo successList new ArrayList(); ListString errorMessages new ArrayList(); // 收集所有错误信息 try (InputStream is file.getInputStream()) { Workbook workbook WorkbookFactory.create(is); // 使用工厂方法自动识别xls/xlsx Sheet sheet workbook.getSheetAt(0); // 跳过表头行从第1行开始索引0是表头 for (int i 1; i sheet.getLastRowNum(); i) { Row row sheet.getRow(i); if (row null) { // 跳过空行 continue; } MajorPo po new MajorPo(); po.setNewOprUserId(userId); StringBuilder rowError new StringBuilder(); // 解析并校验每一列数据 // 专业大类必填 Cell cell0 row.getCell(0); if (cell0 null || cell0.getCellType() CellType.BLANK) { rowError.append(第).append(i1).append(行专业大类不能为空); } else { po.setParentName(getCellStringValue(cell0)); } // 专业名称必填 Cell cell1 row.getCell(1); if (cell1 null || cell1.getCellType() CellType.BLANK) { rowError.append(第).append(i1).append(行专业名称不能为空); } else { String majorName getCellStringValue(cell1); // 可以添加业务校验比如是否已存在 // if (majorService.existsByName(majorName)) { // rowError.append(专业名称“).append(majorName).append(”已存在); // } po.setMajorName(majorName); } // 专业介绍选填 Cell cell2 row.getCell(2); po.setMajorJs(cell2 ! null ? getCellStringValue(cell2) : ); // 状态校验如果模板有下拉框这里可以校验值是否合法 Cell cell4 row.getCell(4); if (cell4 ! null) { String status getCellStringValue(cell4); if (!启用.equals(status) !停用.equals(status)) { rowError.append(第).append(i1).append(行状态只能是“启用”或“停用”); } } if (rowError.length() 0) { // 本行有错误记录错误信息跳过插入 errorMessages.add(rowError.toString()); continue; } // 数据校验通过加入待插入列表 successList.add(po); } // 批量插入所有有效数据 if (!successList.isEmpty()) { // 这里假设你的Mapper支持批量插入效率远高于循环单条插入 // baseMapper.batchInsertRmMajor(successList); for (MajorPo po : successList) { baseMapper.insertRmMajor(po); } } workbook.close(); } catch (IOException e) { log.error(读取Excel文件失败, e); return Result.error(文件读取失败请检查文件格式是否正确。); } catch (Exception e) { log.error(导入过程发生未知错误, e); return Result.error(系统错误导入失败。); } // 构造导入结果信息 String message; if (errorMessages.isEmpty()) { message String.format(导入成功共处理%d条数据全部成功。, successList.size()); } else if (successList.isEmpty()) { message String.format(导入失败共处理%d行数据全部失败。失败原因%s, errorMessages.size(), String.join( , errorMessages)); } else { message String.format(导入部分成功共处理%d行数据成功%d条失败%d条。失败行及原因%s, successList.size() errorMessages.size(), successList.size(), errorMessages.size(), String.join( , errorMessages)); } return Result.OK(message); } // 一个安全的获取单元格字符串值的方法 private String getCellStringValue(Cell cell) { if (cell null) { return ; } // 根据单元格类型读取值 switch (cell.getCellType()) { case STRING: return cell.getStringCellValue().trim(); case NUMERIC: if (DateUtil.isCellDateFormatted(cell)) { return cell.getDateCellValue().toString(); } else { // 防止数字变成科学计数法转为BigDecimal再取整 double num cell.getNumericCellValue(); if (num (long) num) { return String.valueOf((long) num); } else { return String.valueOf(num); } } case BOOLEAN: return String.valueOf(cell.getBooleanCellValue()); case FORMULA: try { return cell.getStringCellValue(); } catch (Exception e) { try { return String.valueOf(cell.getNumericCellValue()); } catch (Exception ex) { return cell.getCellFormula(); } } case BLANK: default: return ; } }这个改进版的导入服务做了大量增强文件类型校验、逐行数据校验、事务管理确保数据一致性以及详细的错误信息收集和反馈。用户上传后能清晰地知道哪些行成功了哪些行失败了以及失败的原因而不是一个笼统的“导入失败”。3. 前端封装打造一个通用的Excel导入组件原始文章的前端代码已经展示了一个封装好的ExcelModal组件这是个非常好的实践。我们来深入分析并优化它让它更健壮、更易用。3.1 组件核心逻辑剖析与优化原始组件使用了Ant Design Vue的a-upload-dragger实现拖拽上传并监听了上传状态。我们可以在此基础上增加一些实用功能。首先优化Props定义让组件更灵活script setup langts import { ref, computed, watch } from vue; const props withDefaults(defineProps{ // 组件是否可见 visible: boolean; // 上传接口地址相对路径 excelUrl: string; // 业务类型可用于区分不同模块的上传 bizType?: string; // 是否显示“入学年份”等特定选择器解耦业务 showExtraOption?: boolean; // 额外上传参数 extraParams?: Recordstring, any; // 模板下载的接口地址或回调函数 templateDownloadUrl?: string; // 上传文件大小限制MB maxSize?: number; // 支持的文件类型 accept?: string; }(), { bizType: , showExtraOption: false, maxSize: 10, accept: .xls,.xlsx, }); const emit defineEmits{ // 上传成功回调可返回成功数据 (e: success, data: any): void; // 上传失败回调 (e: error, error: any): void; // 关闭模态框 (e: update:visible, value: boolean): void; // 触发下载模板 (e: downloadTemplate): void; }(); // 使用computed实现双向绑定优化父组件控制 const innerVisible computed({ get: () props.visible, set: (val) emit(update:visible, val) }); // 上传前的文件校验 const beforeUpload (file: File) { // 校验文件大小 const isLtMaxSize file.size / 1024 / 1024 props.maxSize; if (!isLtMaxSize) { message.error(文件大小不能超过${props.maxSize}MB); return false; } // 校验文件类型 const fileName file.name; const fileExt fileName.substring(fileName.lastIndexOf(.)).toLowerCase(); const acceptList props.accept.split(,).map(ext ext.trim().toLowerCase()); if (!acceptList.includes(fileExt)) { message.error(仅支持上传${props.accept}格式的文件); return false; } return true; }; // 处理上传状态变化 const handleChange (info: UploadChangeParam) { const { file } info; const status file.status; if (status uploading) { loading.value true; return; } if (status done) { loading.value false; const response file.response; if (response response.success) { message.success(response.message || 导入成功); // 清空文件列表为下一次上传做准备 fileList.value []; // 成功回调可以传递后端返回的数据比如成功/失败条数统计 emit(success, response.result); // 导入成功后自动关闭弹窗可根据需求调整 // innerVisible.value false; } else { message.error(response?.message || 导入失败请检查文件格式和数据。); emit(error, response); } } else if (status error) { loading.value false; message.error(${file.name} 上传失败可能是网络问题或服务器错误。); emit(error, file.error); } }; /script3.2 模板下载与用户体验增强原始文章里模板下载是通过触发父组件方法实现的。我们可以让组件自身集成这个功能并提供更友好的提示。template a-modal :width800 :footernull v-model:visibleinnerVisible title批量数据导入 :after-closehandleAfterClose !-- 步骤提示 -- a-steps :currentcurrentStep sizesmall stylemargin-bottom: 24px; a-step title下载模板 / a-step title填写数据 / a-step title上传文件 / /a-steps !-- 第一步下载模板区域 -- div classstep-section v-showcurrentStep 0 div classtemplate-download-card clickhandleDownloadTemplate div classtemplate-icon FileExcelOutlined stylefont-size: 48px; color: #1890ff; / /div div classtemplate-info h4下载导入模板/h4 p点击下载标准Excel模板文件请按照模板格式填写数据。/p p classtemplate-tips InfoCircleOutlined / 提示请勿修改表头带“*”列为必填项。 /p /div div classdownload-arrow DownloadOutlined / /div /div div styletext-align: center; margin-top: 20px; a-button typeprimary clickcurrentStep 1我已准备好数据下一步/a-button /div /div !-- 第二步上传区域 -- div classstep-section v-showcurrentStep 1 a-alert message上传注意事项 typeinfo show-icon stylemargin-bottom: 16px; template #description div1. 请确保文件为.xls或.xlsx格式/div div2. 单次导入建议不超过1000行文件大小不超过10MB/div div3. 请勿修改列顺序必填项不能为空/div div4. 如有错误系统会提示具体行号和原因。/div /template /a-alert a-upload-dragger namefile :multiplefalse :actionuploadAction :headersuploadHeaders :datauploadData :before-uploadbeforeUpload :file-listfileList changehandleChange :acceptprops.accept p classant-upload-drag-icon InboxOutlined / /p p classant-upload-text点击或拖拽文件到此区域/p p classant-upload-hint 支持单个文件上传文件大小不超过{{ props.maxSize }}MB /p /a-upload-dragger !-- 显示上传进度或结果 -- div v-ifuploadResult stylemargin-top: 20px; a-alert :messageuploadResult.title :typeuploadResult.type show-icon template #description div v-htmluploadResult.description/div /template /a-alert /div div styletext-align: center; margin-top: 20px; a-button stylemargin-right: 8px; clickcurrentStep 0上一步/a-button a-button typeprimary :loadingloading :disabledfileList.length 0 开始导入 /a-button /div /div /a-modal /template script setup langts // ... 其他逻辑 const currentStep ref(0); const uploadResult ref{type: success | error | info, title: string, description: string} | null(null); const handleDownloadTemplate () { // 如果有自定义的模板下载URL则使用它 if (props.templateDownloadUrl) { window.open(props.templateDownloadUrl, _blank); } else { // 否则触发事件由父组件处理保持灵活性 emit(downloadTemplate); } message.success(模板下载开始请稍候...); }; // 模态框关闭后的清理工作 const handleAfterClose () { currentStep.value 0; fileList.value []; uploadResult.value null; loading.value false; }; // 计算上传的完整URL和附加数据 const uploadAction computed(() { const baseUrl import.meta.env.VITE_API_BASE_URL || ; return ${baseUrl}${props.excelUrl}; }); const uploadData computed(() ({ bizType: props.bizType, ...props.extraParams })); // 设置上传请求头如Token const uploadHeaders computed(() { const token getToken(); // 假设有获取token的方法 return { Authorization: Bearer ${token}, X-Requested-With: XMLHttpRequest }; }); /script style scoped .step-section { padding: 0 20px; } .template-download-card { display: flex; align-items: center; padding: 24px; background: #fafafa; border: 1px dashed #d9d9d9; border-radius: 8px; cursor: pointer; transition: all 0.3s; } .template-download-card:hover { border-color: #1890ff; background: #e6f7ff; } .template-icon { margin-right: 16px; } .template-info { flex: 1; } .template-info h4 { margin-bottom: 8px; color: rgba(0, 0, 0, 0.85); } .template-info p { margin-bottom: 4px; color: rgba(0, 0, 0, 0.65); } .template-tips { color: #faad14 !important; font-size: 12px; } .download-arrow { font-size: 20px; color: #1890ff; } /style这个优化后的组件有几个亮点分步引导让用户操作更清晰上传前校验提前拦截错误更丰富的状态反馈以及关闭后自动重置状态避免下次打开时残留旧数据。这样的组件封装好后在整个项目的任何需要导入Excel的地方你只需要传递不同的接口地址和业务参数即可复用极大提升了开发效率。4. 实战避坑指南与性能优化光有代码还不够在实际项目中我踩过不少坑这里总结几个关键点希望能帮你绕过去。4.1 内存溢出大文件导入的“杀手”直接用WorkbookFactory.create(inputStream)把整个Excel文件读到内存当文件有几十兆、几万行时很容易导致内存溢出OOM。解决方案是使用POI的“事件模型”Event API或“流式读取”SXSSF。对于导入推荐使用SAX方式解析XSSF and SAX (Event API)它不会将整个文件加载到内存。public ResultObject importLargeExcel(MultipartFile file) { try (InputStream is file.getInputStream()) { OPCPackage pkg OPCPackage.open(is); XSSFReader reader new XSSFReader(pkg); // 获取SharedStringsTable用于处理单元格内字符串 SharedStringsTable sst reader.getSharedStringsTable(); XMLReader parser XMLReaderFactory.createXMLReader(); // 设置自定义的内容处理器 SheetHandler handler new SheetHandler(sst); parser.setContentHandler(handler); // 获取第一个sheet的数据流 InputStream sheetStream reader.getSheet(rId1); InputSource sheetSource new InputSource(sheetStream); parser.parse(sheetSource); sheetStream.close(); // handler.getData() 获取解析后的数据列表 ListMajorPo dataList handler.getData(); // 批量插入数据库... pkg.close(); return Result.OK(导入成功共处理 dataList.size() 条数据); } catch (Exception e) { log.error(导入大文件失败, e); return Result.error(文件过大或格式异常导入失败); } } // 需要实现一个自定义的DefaultHandler来处理XML事件代码略长但能有效控制内存对于导出超大数据则可以使用SXSSFWorkbook它采用滑动窗口机制只将一部分行保留在内存中。RequestMapping(/exportLargeData) public void exportLargeData(HttpServletResponse response) { // 创建一个SXSSFWorkbook指定在内存中保留的行数如100 SXSSFWorkbook workbook new SXSSFWorkbook(100); SXSSFSheet sheet workbook.createSheet(大数据); // 写入表头 Row headerRow sheet.createRow(0); // ... 设置表头 // 模拟写入大量数据 for (int i 1; i 100000; i) { Row row sheet.createRow(i); // ... 设置单元格数据 // 每写1000行手动将行刷出内存到临时文件SXSSF会自动处理但显式控制更好 if (i % 1000 0) { ((SXSSFSheet)sheet).flushRows(100); // 保留最近100行在内存 } } // 写出响应 workbook.write(response.getOutputStream()); workbook.dispose(); // 删除临时文件 workbook.close(); }4.2 数据校验的“双保险”策略在导入逻辑里做数据校验是必须的但有时光靠后端校验用户体验不够及时。我们可以采用“前端轻量校验 后端严格校验”的双保险策略。前端校验Vue组件内在上传前可以用XLSX或SheetJS这类轻量库在浏览器端预读文件进行基础校验。script setup langts import * as XLSX from xlsx; const validateExcelBeforeUpload (file: File): Promiseboolean { return new Promise((resolve, reject) { const reader new FileReader(); reader.onload (e) { const data e.target?.result; const workbook XLSX.read(data, { type: binary }); const firstSheet workbook.Sheets[workbook.SheetNames[0]]; const jsonData XLSX.utils.sheet_to_json(firstSheet, { header: 1 }); // 1. 检查表头是否正确 const headers jsonData[0]; const expectedHeaders [专业大类*, 专业名称*, 专业介绍]; const isHeaderValid expectedHeaders.every((h, i) headers[i] h); if (!isHeaderValid) { message.error(Excel表头格式不正确请下载最新模板); reject(false); return; } // 2. 检查数据行数是否超限比如前端限制1000行 if (jsonData.length - 1 1000) { message.error(单次导入数据不能超过1000行); reject(false); return; } // 3. 快速检查必填项是否有空值可选因为后端会做更精确的校验 for (let i 1; i jsonData.length; i) { const row jsonData[i]; if (!row[0] || !row[1]) { // 假设前两列是必填 message.warning(第${i1}行存在必填项为空请检查。); } } resolve(true); }; reader.readAsBinaryString(file); }); }; // 在beforeUpload中调用 const beforeUpload async (file: File) { // ... 大小、类型校验 try { await validateExcelBeforeUpload(file); return true; } catch { return false; } }; /script后端校验则如前文所述进行更严格、更全面的业务规则校验。这样大部分低级错误在前端就被拦截用户体验更好而后端则守住最后一道防线保证数据质量。4.3 异步导入与进度反馈对于非常大的文件导入操作可能耗时很长让用户干等着不是好体验。我们可以实现异步导入即用户上传文件后后端立即返回一个“任务已接收”的响应然后通过WebSocket或轮询告知用户处理进度。后端改造思路用户上传文件Controller接收后将文件保存到临时位置。立即生成一个唯一的taskId并放入消息队列如Redis或启动一个异步线程处理。返回Result.ok(taskId)给前端。前端轮询查询任务状态的接口如/import/task/{taskId}/status。后端异步任务处理Excel并实时更新该taskId对应的处理进度成功数、失败数、错误信息。前端根据轮询结果更新进度条并在完成后显示详细结果。虽然实现起来比同步导入复杂但对于需要处理数万行数据的后台系统这是提升用户体验的必备功能。JeecgBoot本身集成了Quartz和Redis实现异步任务并不困难。4.4 代码复用与维护性最后谈谈如何让这套导入导出代码更好维护。原始文章将逻辑写在具体的Service里如果多个模块都需要就会产生大量重复代码。我的做法是抽象一个通用的Excel工具类。Component Slf4j public class ExcelImportExportUtils { /** * 通用Excel导出模板方法 * param response HttpServletResponse * param fileName 文件名 * param headers 表头数组 * param exampleData 示例数据可选 */ public void exportTemplate(HttpServletResponse response, String fileName, String[] headers, ListString[] exampleData) throws IOException { // ... 封装之前的模板导出逻辑 } /** * 通用Excel数据导出方法 * param response HttpServletResponse * param fileName 文件名 * param headers 表头 * param dataList 数据列表List of Object数组或List of Map */ public void exportData(HttpServletResponse response, String fileName, String[] headers, List? dataList) throws IOException { // ... 封装数据导出逻辑 } /** * 安全的单元格值获取 */ public static String getSafeCellValue(Cell cell) { // ... 封装之前的getCellStringValue方法 } /** * 解析Excel文件为ListMapMap的key为列索引或列名 * param file 上传的文件 * param headerRowIndex 表头所在行索引从0开始 * return 数据列表 */ public ListMapString, String parseExcelToMap(MultipartFile file, int headerRowIndex) throws IOException { // ... 通用解析逻辑 } } // 在Controller中使用 Autowired private ExcelImportExportUtils excelUtils; RequestMapping(/exportTemplate) public void exportTemplate(HttpServletResponse response) { String[] headers {专业大类*, 专业名称*, 专业介绍}; ListString[] example Arrays.asList( new String[]{工学, 软件工程, 这是一个示例专业} ); excelUtils.exportTemplate(response, 专业模板, headers, example); }对于导入业务逻辑虽然无法完全通用但可以定义一个导入处理器接口不同业务模块实现自己的校验和保存逻辑。public interface ExcelImportHandlerT { /** * 处理单行数据 * param rowData 一行数据可能是Map或数组 * param rowIndex 行号用于错误提示 * return 处理结果包含实体对象和错误信息 */ ImportRowResultT handleRow(MapString, String rowData, int rowIndex); /** * 批量保存数据 * param validDataList 校验通过的数据列表 */ void batchSave(ListT validDataList); } // 在专业的Service中实现这个接口 Service public class MajorImportHandler implements ExcelImportHandlerMajorPo { Override public ImportRowResultMajorPo handleRow(MapString, String rowData, int rowIndex) { MajorPo po new MajorPo(); ListString errors new ArrayList(); // 专业名称校验 String majorName rowData.get(专业名称); if (StringUtils.isBlank(majorName)) { errors.add(专业名称不能为空); } else if (majorName.length() 50) { errors.add(专业名称长度不能超过50字符); } else { po.setMajorName(majorName); } // ... 其他字段校验和赋值 return new ImportRowResult(po, errors); } Override Transactional public void batchSave(ListMajorPo validDataList) { // 使用MyBatis Plus的saveBatch方法 majorService.saveBatch(validDataList); } } // 通用的导入服务 Service public class CommonImportService { Autowired private ExcelImportExportUtils excelUtils; public T ImportResultT doImport(MultipartFile file, ExcelImportHandlerT handler) { // 1. 使用工具类解析Excel为Map列表 ListMapString, String rawData excelUtils.parseExcelToMap(file, 0); ImportResultT result new ImportResult(); ListT successList new ArrayList(); ListRowError errorList new ArrayList(); // 2. 逐行调用业务处理器 for (int i 0; i rawData.size(); i) { ImportRowResultT rowResult handler.handleRow(rawData.get(i), i); if (rowResult.getErrors().isEmpty()) { successList.add(rowResult.getData()); } else { errorList.add(new RowError(i, rowResult.getErrors())); } } // 3. 批量保存成功数据 if (!successList.isEmpty()) { handler.batchSave(successList); } result.setSuccessCount(successList.size()); result.setErrorCount(errorList.size()); result.setErrors(errorList); return result; } }通过这样的抽象具体的业务Service只需要关注数据校验和转换规则复杂的文件解析、事务控制、错误收集都由通用服务完成。代码复用率极高新加一个导入功能几乎就是实现一个处理器接口那么简单。