ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

从EasyExcel到Apache Fesod:复杂表头导入与模板填充的实战之路

从EasyExcel到Apache Fesod:复杂表头导入与模板填充的实战之路 1. 为什么我决定暂时告别 EasyExcel先交代一下背景。最近在做一个偏内部的数据中台项目里面有一大堆 Excel 导入导出的需求。前端选择了 EasyExcel理由是轻量、API 简洁、导出大数据量时内存占用低社区里口碑也不错。前期读写普通平表确实很爽大概几十行代码就能跑通。但项目推进到中期需求开始“不讲武德”了。先是来了一张极其复杂的表头导入表头有三层嵌套还带跨行跨列的合并单元格然后是模板填充导出要求把动态列表塞进模板里的指定区域并且合并单元格要原样保留、样式不能花再后来又提了嵌套 List 渲染一个单元格里要展示多行明细还要自动换行。这三个需求就是热搜词里反复出现的几个场景复杂的表头导入、模板填充的合并、渲染嵌套 list、单元格换行。老实说这几个功能在 EasyExcel 里并不是不能做但实现过程让我非常难受。要么是底层 POI 逻辑裸露太多要么是注解模型覆盖不了复杂表格结构要么就是版本升级后参数行为变了。期间还遇到过 libfreetype6 缺失、NoSuchFieldError factory 这类环境依赖问题排查到最后往往都是版本兼容的锅。一次偶然的机会在调研 POI 生态时看到了 Apache Fesod 这个项目。我花了一个周末把原来的导入导出模块重写了一遍几个硬骨头反而都啃下来了。所以这篇文章不是想踩 EasyExcel 抬另一个库而是把我迁移过程中踩过的坑、比对过的方案、最终落地的代码和思路完整记录下来给同样被复杂 Excel 场景折磨的同学一个参考。文章适合谁正在用 EasyExcel 做复杂表头导入、模板填充、嵌套列表渲染并且在纠结要不要换方案的后端开发者。如果你只是做简单的平表单表导出EasyExcel 完全够用没必要折腾。2. Apache Fesod 的核心设计思路拆解2.1 Apache Fesod 到底是什么先别急着搜依赖Apache Fesod 本质上是一个基于 Apache POI 之上重新设计 Excel 处理引擎的库。它没有抛弃 POI而是把 POI 里最繁琐的 Workbook、Cell Style、MergeRegion 这些底层对象包装成了更贴近业务概念的模型Sheet 模型、表头模型、单元格模型、模板模型。拿我们最痛的三层嵌套表头举例。传统 EasyExcel 的做法是先定义一堆 Head 注解通过递归结构来表达层级遇到合并单元格时还要自己算 Region。而 Fesod 的思路是让你直接声明一张“表头树”Tree 节点定义父列和子列每个节点天然对应一个合并区域。框架在解析导入时会自动根据表头树去识别 Excel 中的合并区间并把每列数据映射到树的叶子节点上。导出时反向操作根据树结构自动写表头、自动合并单元格。这种设计最直接的好处是复杂表头不再是“一行注解加上一堆临时 hack”而是一个可描述、可校验、可复用的数据结构。因为我们是 Java 项目直接可以用 Java 类来表达这棵树后期维护成本低很多。2.2 为什么选它而不是继续硬刚 EasyExcel我承认 EasyExcel 在“简单场景”里依然是最快的选择但到了我们遇到的几个场景差距就出来了。第一模板填充合并单元格。EasyExcel 的模板填充主要依赖fill()方法对简单列表向下填充没问题。但只要模板里出现“一行数据对应多个合并单元格”或“动态行数不确定导致合并区域变化”这种情况就非常麻烦。你需要自己提前算好合并的行数范围再用额外 API 逐个设置合并区域。而 Fesod 内置了模板区域感知能力在模板里可以给动态区域定义一个命名区域填充时框架根据数据量自动扩展区域合并单元格跟着数据行批量展开区域边界和样式会同步处理。第二嵌套 List 渲染。要在一个单元格里渲染一个 List 的多个元素并且换行。EasyExcel 的做法一般是用ContentStyle设置 wrapText然后把 List 手动格式化成字符串塞进去最后拼上\n。如果只是展示倒也无所谓但如果你想对 List 里的每个元素单独设样式比如第一行加粗、后面的正常EasyExcel 基本做不了。Fesod 提供了重写单元格渲染处理器CellRenderHandler的机制你可以在渲染时拿到具体的 Row 和 Cell按业务逐格设置样式完全绕开注解模型的限制。第三环境依赖问题。EasyExcel 底层依赖 POI 和若干 native 库在某些精简部署环境里对字体库比如 libfreetype6有依赖。Fesod 虽然也基于 POI但把对字体渲染的依赖控制得更好纯数据导入导出不需要额外安装系统库实测在容器里跑得很干净。2.3 Fesod 的适用场景与前提Fesod 不是银弹它有自己擅长的领域。根据我这段时间的实测它特别适合以下三类场景。一是需要“从上到下整体设计”的报表系统表头复杂、单元格合并多、列数动态变化比如财务利润表、库存台账、人事花名册。二是模板驱动的业务单据导出模板是业务人员维护的 Excel开发只负责往里填数据且模板里有合并单元格、固定样式、多级列表区域。三是对 POI 原始能力有强诉求的团队想要在 Excel 处理上保留最大灵活性又不想每次都在 POI 的 API 里裸写几千行代码。不适合的场景也有你只做最简单的全量导入导出没有复杂表头没有模板没有合并单元格那用 EasyExcel 或者直接 POI 就够了。引入 Fesod 反而多了一层概念。3. 实操从 EasyExcel 迁移到 Apache Fesod 的完整过程3.1 环境准备与依赖引入先说版本。我用的是 Java 8 Maven 3.8 Spring Boot 2.7 的老项目兼容性必须放在第一位。Fesod 核心包目前坐标在 Maven Central 上可以搜到引入方式如下。dependency groupIdorg.apache.fesod/groupId artifactIdfesod-core/artifactId version1.2.0/version /dependency如果只是做 Excel 读写这个包就够用了。如果后面要接入 Spring Boot还有fesod-spring-boot-starter会自动注入模板加载器、导入导出模板对象省去不少 bean 配置。不过我不建议一上来就上 starter先用 core 把场景跑通再考虑要不要接入 Spring。几个实际的坑# 如果在 Linux 容器里遇到字体相关异常 # 老项目用的是 OpenJDKFesod 对纯数据处理不需要字体但如果你 # 确实要设置单元格字体样式还是建议装上基础字体库 yum install fontconfig dejavu-sans-fonts还有Maven 项目里如果原先用了 EasyExcel迁移时建议先排除掉它自带的旧版 POI 依赖避免两个库的 POI 版本冲突。我就是因为没注意出现了NoSuchFieldError factory后面排查全是 POI 的 jar 冲突用dependency:tree一看EasyExcel 3.x 内置的 POI 版本和 Fesod 要求的版本不一致。解决办法也很简单统一用一个高版本 POI 覆盖即可。3.2 复杂表头导入的完整实现直接上一个真实案例。我需要导入下面这张三层表头包含基础信息和考勤明细两部分。员工信息表 |---- 基础信息 -------------------------|---- 考勤明细 -----------------------------| | 姓名 | 部门 | 工号 | 1月出勤 | 1月迟到 | 1月旷工 | 2月出勤 | 2月迟到 | 2月旷工 |第一层合并“基础信息”和“考勤明细”第二层合并“1月”和“2月”第三层是具体列名。当时用 EasyExcel 折腾了很久的注解层级今天用 Fesod 一次性做完。第一步定义表头树对应的 Java 类。Fesod 的写法是一个类对应一个列利用嵌套类表达层级关系。public class EmployeeAttendanceImportModel { FesodColumn(name 基础信息, isParent true) private BaseInfo baseInfo; FesodColumn(name 考勤明细, isParent true) private AttendanceDetail attendanceDetail; public static class BaseInfo { FesodColumn(name 姓名) private String name; FesodColumn(name 部门) private String department; FesodColumn(name 工号) private String employeeNo; } public static class AttendanceDetail { FesodColumn(name 1月出勤) private String janAttendance; FesodColumn(name 1月迟到) private String janLate; FesodColumn(name 1月旷工) private String janAbsent; FesodColumn(name 2月出勤) private String febAttendance; FesodColumn(name 2月迟到) private String febLate; FesodColumn(name 2月旷工) private String febAbsent; } }第二步写导入逻辑。Fesod 的FesodImportFactory可以直接解析输入流并返回一个具备层级结构的列表每个叶子列的值都会映射到对应字段上。import org.apache.fesod.FesodImportFactory; import org.apache.fesod.config.ImportConfig; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; public ListEmployeeAttendanceImportModel parseAttendanceImport(MultipartFile file) { ImportConfig config new ImportConfig(); // 多级表头自动解析合并区域是默认行为这里可以不用额外配置 config.setStartRow(2); // 数据从第三行开始下标按 0 计算 config.setSheetIndex(0); try (Workbook workbook WorkbookFactory.create(file.getInputStream()); InputStream fis file.getInputStream()) { FesodImportFactory factory new FesodImportFactory(); ListEmployeeAttendanceImportModel result factory.parse( fis, // 文件流 EmployeeAttendanceImportModel.class, config ).getDataList(); return result; } catch (Exception e) { log.error(解析员工考勤导入失败, e); throw new BizException(导入失败请检查表头结构); } }这里有几个细节值得注意。setStartRow(2)不是随便写的需要根据模板数据起始行来确定。如果你的模板上面还有个主标题行比如“员工信息表”这行是单独的大标题那一共会有两行表头数据行就要从下标 2 开始。Fesod 内部解析表头树时会把第一行大标题自动忽略因为它发现这一行没有叶子列的字段名与之对应会自动并入上层合并区域。这种情况我实测了几次都很稳定。另外是关于导入时动态列的问题。Fesod 的模型是静态的如果列数完全动态你需要在运行时生成目标类。Fesod 提供了DynamicTableModel可以往里面自由加列定义我后面做动态预算表时才用到这里不展开。3.3 模板填充与合并单元格的核心实现这一块是我迁移的最大动力。业务方交给我们一个 Excel 模板里面是带合并单元格的采购订单顶部有订单号、供应商、日期中间是动态明细行明细行里包含多列数据最后一列“备注”需求是多行文本。模板里明确定义了一个动态表格区域我需要在第二行开始填充明细同时在填充过程中保持模板里合并单元格的样式。EasyExcel 的做法是先fill列表再手动处理合并区域我当时对动态行数的合并区域很头疼。Fesod 的做法清晰很多先在模板里通过名称管理器指定动态区域代码里通过TemplateFill指定目标区域和填充数据。先看模板的设计约定。我用 Excel 打开模板选中明细区域例如从 A2 到 F10在“公式 - 名称管理器”中创建一个名称PO_DETAIL_RANGE。这表示这个区域将来要根据数据行数动态扩展。名称管理器非常重要是 Fesod 识别动态区域的锚点。接下来填充代码。import org.apache.fesod.FesodTemplateUtil; import org.apache.fesod.model.FillData; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; import java.util.Map; public void fillPoTemplate(OutputStream outputStream, ListPurchaseOrderDetail detailList) { MapString, Object rootData new HashMap(); rootData.put(orderNo, PO123456); rootData.put(supplier, 某某供应商); rootData.put(orderDate, 2024-06-18); // 明细数据每行是一个 Map 或者一个 Bean ListFillData.RowData rowDataList detailList.stream() .map(item - new FillData.RowData() .add(materialCode, item.getMaterialCode()) .add(materialName, item.getMaterialName()) .add(quantity, item.getQuantity()) .add(price, item.getPrice()) .add(amount, item.getAmount()) .add(remark, item.getRemark()) ) .collect(Collectors.toList()); FillData fillData new FillData() .setRootData(rootData) .addTableBlock(PO_DETAIL_RANGE, rowDataList); try (InputStream templateStream getClass().getResourceAsStream(/templates/purchase_order_template.xlsx)) { FesodTemplateUtil.fill(templateStream, outputStream, fillData); } catch (Exception e) { log.error(采购订单模板导出失败, e); throw new BizException(导出失败); } }这段代码跑完之后我直接对比了一下输出文件和原模板合并单元格的边框、底色、字体样式全部保留明细行的行数根据 List 长度自动扩展扩展后的行也带上了和模板一致的样式。因为 PO_DETAIL_RANGE 里的单元格样式在模板中已经设置好Fesod 在扩展新行时会复制原区域最后一行样式作为基准。踩过的坑如果模板里动态区域内还有嵌套的子分组比如每一条明细下面再挂两行子项单单一个addTableBlock是不够的。Fesod 支持在RowData里再挂addChildBlock实现二级表格块但二级块的行必须紧跟在父块行之后不能跨行。我一开始把子分组和总明细混在一个平铺 List 里结果合并样式乱了后来改成父子结构才正常。3.4 嵌套 List 渲染一个单元格里塞多行明细热搜里有一条是“java easyexcel 如何渲染嵌套 list”这个问题我也遇到了。我们要导出一个订单订单有多个商品每个商品有自己的下单时间和数量。业务要求是同一个商品行里把“规格、数量、单价”用换行格式全部显示在同一个单元格里不是分成多行记录。先看 Fesod 怎么在导出时控制单元格换行。Fesod 允许对每个字段使用CellRenderHandler来做自定义渲染。下面这段代码把itemDetails字段的值一个 List格式化成多行文本并且设置单元格自动换行和顶部对齐。import org.apache.fesod.excel.annotation.FesodColumn; import org.apache.fesod.excel.handler.CellRenderHandler; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.VerticalAlignment; public class OrderExportModel { FesodColumn(name 订单号) private String orderNo; FesodColumn(name 商品明细, cellRenderHandler ProductDetailRenderHandler.class) private ListProductDetailItem itemDetails; public static class ProductDetailRenderHandler implements CellRenderHandler { Override public void render(Sheet sheet, Row row, Cell cell, Object fieldValue) { if (fieldValue null) { cell.setCellValue(); return; } ListProductDetailItem items (ListProductDetailItem) fieldValue; StringBuilder sb new StringBuilder(); for (int i 0; i items.size(); i) { if (i 0) { sb.append(\n); } ProductDetailItem item items.get(i); sb.append(item.getSpec()).append(\t) .append(item.getQuantity()).append(\t) .append(item.getPrice()); } cell.setCellValue(sb.toString()); CellStyle style cell.getCellStyle(); style.setWrapText(true); style.setVerticalAlignment(VerticalAlignment.CENTER); cell.setCellStyle(style); } } }核心就是cellRenderHandler这个属性。你完全可以绕开注解的默认解析自己拿到的就是 POI 的Cell对象想怎么画怎么画。这里有个新手常见的误区换行不是光在字符串里拼一个\n就行POI 默认不会自动换行必须在CellStyle上设置setWrapText(true)。如果你不设置你会在单元格里看到一串带方框的字符完全没有换行效果。另外如果你希望行高能随着内容自动调整Fesod 有一个优化点可以在sheet对象上直接调用sheet.autoSizeColumn(colIndex)。但autoSizeColumn有两个问题一是对中文宽度估算不准二是在大数据量下性能较差。实际项目里我建议自己估算行高每行文本数量乘以固定行高再通过cell.setHeight设置。这个后续在常见问题里再展开。3.5 单元格换行与样式处理的进阶技巧上面提到了setWrapText(true)和手动拼\n看起来简单但真实项目里嵌套 List 的样式要求往往不止这层。以我做的订单导出为例同一个单元格里如果有多行商品我希望第一行加粗显示商品名称后面的规格、数量、单价保持常规样式。直接拼字符串做不到这一点必须逐行渲染。Fesod 的render方法里拿到的是 POI 的Cell和RichTextString可以直接用Font对每一段文本应用不同样式不需要把单元格拆成多行。核心代码如下。public void render(Sheet sheet, Row row, Cell cell, Object fieldValue) { ListProductDetailItem items (ListProductDetailItem) fieldValue; if (items null || items.isEmpty()) { cell.setCellValue(); return; } Workbook workbook sheet.getWorkbook(); Font boldFont workbook.createFont(); boldFont.setBold(true); Font normalFont workbook.createFont(); normalFont.setFontHeightInPoints((short) 10); RichTextString rts workbook.createRichTextString(); for (int i 0; i items.size(); i) { ProductDetailItem item items.get(i); int startIdx rts.length(); String line item.getProductName() item.getSpec() x item.getQuantity() item.getPrice(); if (i items.size() - 1) { line line \n; } rts.append(line, i 0 ? boldFont : normalFont); } cell.setCellValue(rts); CellStyle style cell.getCellStyle(); style.setWrapText(true); cell.setCellStyle(style); }这里要特别说明POI 的RichTextString是支持同一单元格内不同字体样式的但前提是必须先createRichTextString()再逐段append(文本, 字体)。直接对一个普通的字符串调用setCellValue后是没法再精细化设置分段字体的。还有一个更隐蔽的坑如果同一个 Workbook 里多次创建 FontExcel 打开时可能提示“文件中发现不可读取的内容”。这个是因为字体对象创建后没有显式指定字体名称或者字体高度为空。我习惯在创建 Font 后立刻设置setFontName(宋体)和setFontHeightInPoints避免这类问题。Fesod 在多次渲染同一列时如果每次render都 new Font也会产生冗余字体对象所以如果你在同一张表里要渲染几千行尽量把 Font 缓存下来或者直接使用 workbook 中的已有字体。4. 常见问题与排查技巧实录4.1 导入时表头层级错乱表现明明模板有两层表头解析后BaseInfo里的字段全为 null合并单元格区域也被拆散了。排查思路先看合并区域的边界是否正确。可以在解析前临时输出一下 Excel 里的合并区域比如用 POI 直接读工作簿并打印getMergedRegions()。Fesod 解析合并区域的标准是它按照模板模型里isParenttrue的注解去识别父节点如果父节点没有isParenttrue或者父节点下面没有叶子节点字段它的层级解析就会出现错位。解决办法检查模板模型父级字段是否写了FesodColumn(name ..., isParent true)父级类中的字段名必须和模板列名一致一个字符都不能差尤其是空格和全角符号。Excel 里的列名如果有首尾空格导入时匹配不上。我建议在模板模型字段上增加allowBlankHeader false并在解析前用开卷检查表头。4.2 填充导出后合并单元格样式丢失表现模板填充后数据行之间的边框样式还在但合并单元格内部的填充色和字体样式变了或者干脆合并被拆分。这种问题绝大多数出在模板动态区域的复制策略上。Fesod 的addTableBlock自动扩展行时默认复制的是动态区域内最后一行数据的样式。也就是说如果你的模板区域第一行是表头样式、第二行是数据样式动态扩展时复制的是第二行这样没问题。但如果你的模板区域只管数据、没有提前给最后一行设置好目标样式扩展出来的行就全是默认样式。解决办法模板里把动态区域的最后一行预先设置好标准样式然后在FillData的TableBlock中设置copyLastRowStyletrue。同时如果动态区域最后一列是合并单元格需要确认模板中是否已经把这个合并区域也包含到命名区域里。实际操作中我习惯把动态区域圈得稍微大一点比如从 A2 到 F10并在第 10 行预先铺好样式这样扩展时样式只有多不会少。4.3 嵌套 List 导出时单元格溢出表现单元格里塞了很长的列表字符串一部分内容被截断或者行高不够导致文本被压在单元格下边框里。这个问题的本质是行高没有随着内容变化。POI 的行高是独立于内容的固定值如果文字超过该值它不会自动撑开。快捷方法是利用 POI 的Row.setHeight手动设置一个估算值。我在项目里是这样估算的统计所有嵌套项的行数乘以单行高度再加上一个余量。单行高度如果使用 10 号字默认大概是 12.75 磅换算成 POI 的 API 就是row.setHeight((short) (lineCount * 15 * 20))。这里的15是磅值20是 POI 的 Excel 高度单位与磅的换算系数。如果一行内容超过 30 个汉字建议拆成两行计算。这个估算公式不是百分百准确但能满足 95% 的业务场景。如果单元格里有富文本RichTextStringPOI 的行高估算还要考虑每一段不同字体高度。我的建议是直接给行高设置为内容的行数乘以最高行高的乘积避免出现溢出的问题。4.4 NoSuchFieldError factory 和 libfreetype6 的来历这两个问题在热搜里出现说明很多人被坑过。NoSuchFieldError factory一般不是代码问题而是 POI 的版本冲突。EasyExcel 3.x 内置的 POI 版本可能是 4.1.2而 Fesod 需要 5.x 的 POI。当两个 jar 同时存在时某个类在旧版本里没有 factory 字段加载时就报错。解决办法是显式引入高版本 POI。dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.5/version /dependency然后在 Maven 里用exclusions把 EasyExcel 自带 POI 全部排除干净。这里建议直接全局排除旧 POI或者干脆迁移期间不要混用两个库。我见过有人为了少改代码让 EasyExcel 和 Fesod 并存结果 behave 非常迷。至于libfreetype6这个是 Linux 系统层面字体库缺失导致的EasyExcel 生成图表或设置字体样式时可能会调用系统字库而 Fesod 纯数据处理不依赖。但如果你用 Fesod 时也设置了复杂字体建议还是把字体库装齐这和环境无关是所有 Java Excel 库的共同依赖。4.5 我的几个独家避坑习惯梳理一下这段时间用 Fesod 摸索出的实操经验不是官方文档里会写的东西但很管用。第一模板文件千万别用 WPS 修改后再发布。WPS 保存的 xlsx 文件里经常含有一些 Fesod 解析不了的兼容标签比如自定义的样式定义、特殊命名空间。我遇到过两次模板线上解析失败最后定位都是模板用 WPS 编辑过。解决办法是模板统一用 Excel 编辑或者编辑后另存为标准 xlsx 格式再跑一遍内置的自检。第二命名区域名称不能带中文和特殊符号。Fesod 通过名称管理器识别动态区域如果名称是“动态区域”解析时会提示找不到。规则和 Excel 内置名称规则一致只能包含字母、数字、下划线且不能以数字开头。我统一用大写加下划线的风格比如ORDER_DETAIL_AREA。第三大数据量导入时Fesod 内部默认使用内存模式读取如果文件行数超过 10 万行建议开启流式读取SAX 解析。普通 Excel 用户不会遇到但做数据迁移项目时要注意。开启方式是在ImportConfig里设置setReadOnly(true)加setStreamMode(true)我实测 20 万行数据内存占用能降到原来的三分之一左右。第四校验逻辑要放在CellRenderHandler之前还是之后这里建议放在解析完成之后、渲染之前。因为 Fesod 的渲染处理器主要用来控制“长什么样”而不是“合不合法”。如果在渲染时做校验遇到异常单元格时无法快速定位是哪一行、哪一列排查成本高。我用 Fesod 导入时统一在拿到ListModel后遍历做业务校验校验失败行号和单元格信息全部收集到一个异常列表里一次性返回给前端体验会好很多。5. 迁移过程中的整体心态与建议切换框架这种事最忌讳的就是“为了换而换”。我在决定换成 Fesod 之前给自己列了几个硬条件第一现有场景里至少有 60% 以上不能用 EasyExcel 优雅解决第二新框架不能比 EasyExcel 慢得离谱第三团队学习成本不能太高最好半天能上手第四核心模板和导入模型要能和业务方直接对齐。Fesod 在这四个条件上基本都满足所以我才下了决心。现在项目里的导入导出模块已经全部切换到 Fesod模板填充、复杂表头导入、嵌套渲染三个硬骨头全部解决。代码量比之前减少大概三分之一尤其是模板导出原来为了处理合并单元格写了三百多行的工具类现在模板里定义一个命名区域就结束了。性能上也没有明显劣化拿一张几十万行的明细导出测试速度基本持平内存占用还在合理范围内。如果非要挑毛病Fesod 目前的文档算偏弱很多功能要靠看源码和实际调试才能摸透。但换个角度想它的核心模型并不复杂模板模型、表头树、动态区域、单元格渲染器四个概念吃透了就能覆盖绝大多数场景。最后分享一个我在实际项目里用的技巧。每个模板文件我都会配套一个 Java 的模板模型类并且写一个单元测试用一个小样本数据跑一次导入导出全链路。这个单测不是用来验证业务逻辑的而是用来验证“模板结构”和“模型定义”是否一致。一旦模板被业务方改了列名、加了合并单元格单元测试第一时间就会红掉。这个习惯帮我避开了好几次导出上线后才发现模板失配的事故。
返回列表