EasyExcel批量导入实战:成功失败统计与失败数据回写导出
1. 批量导入这件事难的不是读Excel而是把失败的那部分说清楚做过后台管理系统的人大概都有过这种经历运营同事丢过来一个几百上千行的Excel说“帮忙导一下”你写了个接口用EasyExcel几行代码读进来批量插入数据库本地跑得挺顺。结果上线第一天运营又来了——“有几十条没导进去是哪几十条为什么没进去”这时候你才发现真正麻烦的根本不是“读Excel”而是怎么把成功了多少、失败了多少、每一条为什么失败讲清楚并且把失败的那些原样吐回去让人家改。这个需求听起来朴素但它是所有数据导入功能的“最后一公里”。一个只返回“导入成功”或“导入失败”的接口在生产环境里基本等于没做完。运营需要的是我这次导了500条成功480条失败20条失败的20条里有5条是手机号格式不对有8条是必填字段为空有7条是编码在系统里已经存在。而且这20条要能直接下载下来最好还带上失败原因列我改完再导一次。这篇内容就围绕这个场景展开讲清楚基于EasyExcel怎么把“读取—校验—入库—统计—失败回写导出”这一整条链路做扎实。关键词是EasyExcel读取、成功失败数量统计、失败原因回写、失败数据导出到系统路径。适合正在做后台导入功能的后端开发也适合想把这套逻辑抽象成通用组件的同学。我会把每一步为什么这么做、参数怎么定、坑在哪里都摊开讲代码给到能直接改改就用的程度。先说结论性的判断这个功能的核心设计思想是**“逐行独立处理 结果分类收集 失败行原样保留”**。不要试图用一次批量插入去赌全部成功也不要读完整个Excel再统一校验那样你根本定位不到具体是哪一行出的问题。逐行处理虽然看起来“笨”但它是唯一能精确归因的做法。2. 为什么监听器里不能直接写数据库以及正确的分层姿势2.1 EasyExcel的读取模型决定了你必须“边读边攒”EasyExcel读取的核心是ReadListener它按行回调invoke方法。很多人图省事直接在invoke里调Service做插入插一条算一条。小数据量没问题但一旦数据量上来或者插入逻辑里带了事务就会出大问题事务边界混乱、连接池被长时间占用、某一行抛异常导致整个读取中断。正确的姿势是监听器只负责“读”和“攒”业务处理放到读取完成之后统一做。具体来说监听器里把每一行解析成实体对象塞进一个List等doAfterAllAnalysed回调触发时说明整个文件读完了再把这个List交给业务层去逐条校验和入库。这样分层的好处很直接读取和业务解耦读取阶段不会因为业务异常而中断业务阶段可以自由控制事务粒度比如每条独立事务或者分批提交。public class ImportDataListenerT extends AnalysisEventListenerT { // 攒数据的容器注意用线程安全的或者每次new一个监听器 private final ListT dataList new ArrayList(); // 记录读取过程中的解析异常比如单元格类型不对 private final ListString parseErrors new ArrayList(); Override public void invoke(T data, AnalysisContext context) { // 这里只做最轻量的事收集 dataList.add(data); } Override public void doAfterAllAnalysed(AnalysisContext context) { // 读取完成什么都不做交给外部处理 } Override public void onException(Exception exception, AnalysisContext context) { // 单行解析异常不要直接抛记录下来继续读 parseErrors.add(第 context.readRowHolder().getRowIndex() 行解析异常); } public ListT getDataList() { return dataList; } public ListString getParseErrors() { return parseErrors; } }这里有个关键点onException方法一定要重写。默认情况下EasyExcel遇到解析异常会直接抛出整个读取就断了。重写之后把它记下来继续读才能保证“一行坏数据不影响其他行”。这是做批量导入的基本素养。2.2 监听器不要做成Spring单例Bean这是踩过坑才知道的事。如果你把ReadListener交给Spring管理成单例那dataList这个成员变量就是共享的。第一次导入攒了500条第二次导入又往同一个List里加数据就串了。更隐蔽的是并发场景两个用户同时导入数据直接混在一起。所以监听器要么每次new一个要么用Scope(prototype)声明成多例。我个人的习惯是每次读取时直接new简单直接不依赖Spring的作用域机制少一层心智负担。2.3 读取时的实体映射要留好“原始行号”失败数据导出时运营最想知道的是“我Excel里第几行出的问题”。EasyExcel的实体里默认没有行号但你可以通过AnalysisContext拿到rowIndex。做法是在实体里加一个rowNum字段在invoke里手动赋值Override public void invoke(T data, AnalysisContext context) { // rowIndex是从0开始的1变成Excel里看到的行号再1跳过表头 int rowIndex context.readRowHolder().getRowIndex(); if (data instanceof RowNumAware) { ((RowNumAware) data).setRowNum(rowIndex 1); } dataList.add(data); }定义一个RowNumAware接口让所有导入实体实现它这样行号信息就能一路带到失败导出环节。这个小小的设计能让运营的体验提升一大截——他们不用自己数行号。3. 逐行校验与入库成功失败怎么分桶事务怎么切3.1 校验和入库的顺序先校验全部还是边校验边入库两种做法各有取舍。先全部校验再统一入库的好处是如果校验阶段发现大量错误可以快速失败不用碰数据库坏处是校验通过的合法数据要再遍历一次。边校验边入库的好处是逻辑线性一条走完流程坏处是事务控制要小心。我的建议是校验和入库放在同一个循环里但每条数据用独立的事务。原因是导入场景下一条数据的失败不应该影响其他数据。如果用一个大事务包住整个循环任何一条失败都会导致全部回滚那“成功480条失败20条”就变成了“全部失败”完全违背需求。实现上把单条处理逻辑抽成一个方法加上Transactional(propagation Propagation.REQUIRES_NEW)或者用编程式事务手动控制。注意REQUIRES_NEW会挂起外层事务如果外层本来就没事务效果就是每条独立提交。public ImportResult importData(ListImportRow rows) { ImportResult result new ImportResult(); ListFailRow failRows new ArrayList(); int successCount 0; for (ImportRow row : rows) { try { // 校验 String error validate(row); if (error ! null) { failRows.add(buildFailRow(row, error)); continue; } // 入库独立事务 self.saveOne(row); successCount; } catch (Exception e) { // 数据库层面的异常比如唯一索引冲突 failRows.add(buildFailRow(row, 入库失败 e.getMessage())); } } result.setSuccessCount(successCount); result.setFailCount(failRows.size()); result.setFailRows(failRows); return result; }注意这里用了self.saveOne(row)self是注入的自己通过AopContext.currentProxy()或者注入自身目的是让Transactional生效。如果直接this.saveOne()Spring的代理不生效事务注解就是摆设。这个坑非常经典很多人栽在这里还以为事务配错了。3.2 校验规则要可配置、可累积校验最容易写成一坨if-else。更好的做法是把校验规则抽成一个个独立的校验器每个校验器返回一个错误信息多个错误可以累积。比如一行数据既手机号格式错、又编码重复运营希望一次看到两个问题而不是改完一个再发现另一个。public interface RowValidator { String validate(ImportRow row); } // 组合校验 ListRowValidator validators Arrays.asList( new RequiredFieldValidator(), new PhoneFormatValidator(), new DuplicateCodeValidator() ); ListString errors new ArrayList(); for (RowValidator v : validators) { String err v.validate(row); if (err ! null) errors.add(err); } if (!errors.isEmpty()) { failRows.add(buildFailRow(row, String.join(, errors))); continue; }这样设计还有一个好处校验规则可以按业务类型动态组装。比如导入用户和导入商品用的校验器集合不同但框架是同一套。3.3 数据库唯一约束冲突要单独处理有一种失败是校验阶段发现不了的并发导入时两条数据都通过了“编码不重复”的校验但入库时唯一索引冲突。这种异常必须捕获并且给出人话提示而不是把DuplicateKeyException的堆栈直接甩给运营。catch (DuplicateKeyException e) { failRows.add(buildFailRow(row, 编码已存在请检查是否重复导入)); }把常见的数据库异常映射成业务语言是导入功能专业度的体现。运营看不懂SQLIntegrityConstraintViolationException但看得懂“编码已存在”。4. 失败数据回写导出怎么把原始行和失败原因拼回一个Excel4.1 失败导出的本质是“原数据 一列原因”运营拿到失败文件后期望的是打开一看还是他原来那些列只是多了一列“失败原因”他改完直接再导。所以失败导出的结构应该是原始列 失败原因列。实现上有两种思路。一种是失败行实体里保留所有原始字段导出时用EasyExcel写这个实体另一种是动态列因为不同导入模板列不一样。如果导入模板是固定的用固定实体最省事如果要做通用组件就得用动态表头。固定实体的写法public class FailRow { // 原始字段和导入实体一致 private String name; private String phone; private String code; // 额外加的行号和原因 private Integer rowNum; private String failReason; }导出时用EasyExcel.write(outputStream, FailRow.class).sheet(失败数据).doWrite(failRows)几行搞定。4.2 导出到“系统路径”要考虑的几个现实问题需求里说“导出到系统路径中”这个“系统路径”通常指服务器上的某个目录比如/data/import/fail/。这里有几个必须想清楚的点第一文件名要唯一且可追溯。用时间戳加业务标识比如fail_20240520153012_用户导入.xlsx。如果多个用户同时导入文件名冲突会互相覆盖。第二目录要提前创建。File.mkdirs()别忘了否则写文件时直接报“路径不存在”。第三要考虑清理策略。失败文件堆在服务器上不会自己消失时间长了磁盘就满了。常见做法是保留7天或30天用定时任务清理或者按日期分目录删旧目录。第四返回给前端的应该是下载地址而不是文件流。既然已经落到系统路径了接口返回一个可访问的URL比如通过静态资源映射或者文件下载接口前端点一下就能下载比直接把流塞进响应体更灵活也方便运营事后重新下载。String dir /data/import/fail/; File dirFile new File(dir); if (!dirFile.exists()) dirFile.mkdirs(); String fileName fail_ System.currentTimeMillis() .xlsx; String fullPath dir fileName; try (OutputStream os new FileOutputStream(fullPath)) { EasyExcel.write(os, FailRow.class).sheet(失败数据).doWrite(failRows); } // 返回可下载的相对路径或URL result.setFailFilePath(/download/fail/ fileName);4.3 失败原因列要放在最后一列还是第一列这是个细节但影响体验。放在最后一列运营需要横向滚动才能看到原因尤其是列多的时候放在第一列运营一眼就能看到问题但会打乱他原来的列顺序改完再导时可能对不上。我的经验是放在最后一列但把行号放在第一列。行号是运营定位问题的关键放最前面方便他对照原文件失败原因放最后不干扰原始数据的阅读。如果失败原因很长可以在导出时设置列宽自适应或者干脆把原因列宽设大一点。5. 把结果封装成一个能直接返回给前端的对象5.1 返回结构设计数量、原因、文件路径一个都不能少前端拿到导入结果后要展示什么通常是三块成功多少条、失败多少条、失败文件下载链接。如果失败条数不多还可以直接把失败明细列表返回前端弹窗展示不用下载。public class ImportResult { private int totalCount; // 总条数 private int successCount; // 成功条数 private int failCount; // 失败条数 private String failFilePath; // 失败文件路径 private ListFailRow failRows; // 失败明细可选条数少时返回 }这里有个取舍失败明细要不要全量返回如果失败500条全塞进JSON响应体会很大。我的做法是失败条数小于50条时返回明细否则只返回文件路径。前端根据这个规则决定是弹窗展示还是提示下载。5.2 空文件和全成功全失败的边界处理边界情况最容易被忽略但线上偏偏就出这些空文件只有表头没有数据行。这时候dataList是空的应该直接返回“文件中没有数据”而不是走一遍流程返回0成功0失败。全成功failCount为0不生成失败文件failFilePath为null前端不显示下载按钮。全失败successCount为0失败文件包含所有行提示运营全部需要修改。文件格式错误比如上传的是.txt改了后缀EasyExcel读取时会抛异常要捕获并返回友好提示。if (dataList.isEmpty()) { throw new BizException(导入文件中没有数据请检查); }5.3 大文件的内存控制别让一个导入把服务搞挂如果运营传了一个10万行的Excel全攒在List里内存直接飙升。EasyExcel本身是流式读取的但你把所有行都攒起来就失去了流式的意义。对于大文件有两个策略。一是分批处理监听器里每攒够500条就交给业务层处理并清空List处理完再继续攒。二是限制单次导入条数超过阈值直接拒绝提示分批导入。实际项目中后者更常见也更稳妥因为导入本身就不该是高频大数据量的操作。private static final int BATCH_SIZE 500; private ListT batch new ArrayList(BATCH_SIZE); Override public void invoke(T data, AnalysisContext context) { batch.add(data); if (batch.size() BATCH_SIZE) { processBatch(batch); batch.clear(); } }注意分批处理后成功失败计数要累加失败文件要追加写入EasyExcel支持多次写入同一个sheet但要注意用ExcelWriter手动控制。如果嫌追加写入麻烦也可以每批生成一个临时文件最后合并但那样更复杂。我的建议是如果业务允许直接限制单次导入上限比如5000条超过就提示分批这是性价比最高的方案。6. 几个只有踩过才知道的坑6.1 日期和数字格式的解析陷阱Excel里的日期单元格EasyExcel默认可能读成数字或者字符串取决于单元格格式。如果实体字段是Date类型而单元格是文本格式的“2024-01-01”会解析失败。解决办法是在实体字段上加DateTimeFormat或者统一用String接收再手动转换。数字也是同理手机号这种长数字如果单元格是数值格式可能变成科学计数法用String接收最稳。6.2 表头不匹配时EasyExcel的静默行为如果Excel的表头和实体ExcelProperty配置对不上EasyExcel默认可能读不到数据但不报错你会得到一个空List然后困惑“为什么没数据”。建议开启表头校验或者在读取前先读一行表头做比对。EasyExcel的headRowNumber参数要设对默认是1如果文件有多行表头要调整。6.3 失败文件里的原始数据要“原样”不要二次加工导出失败数据时千万不要把原始值做格式化。比如手机号本来是13800138000你导出时格式化成138-0013-8000运营改完再导又对不上了。失败文件的原则是除了多一列失败原因其他列和原文件一模一样。这样才能保证“改完直接再导”的闭环。6.4 并发导入同一批数据时的重复校验两个运营同时导入同一份文件校验阶段都通过了入库时一个成功一个唯一键冲突。这不是bug是并发场景的正常表现。处理方式就是前面说的捕获DuplicateKeyException并给出友好提示。如果想更严格可以在导入前加分布式锁按业务键加锁但那样会牺牲并发一般没必要。7. 关于这套方案我自己的使用体会这套“逐行处理 结果分桶 失败回写”的模式我在好几个项目里都用过从用户导入、商品导入到订单导入结构基本不用大改换的只是校验器和实体。它最大的价值不是技术多高深而是把“导入”这件事从“黑盒”变成了“白盒”——运营知道哪条错了、为什么错、怎么改开发也不用被反复追问“到底哪条没进去”。如果让我给正在做这个功能的同学一句建议那就是先把失败路径想清楚再写成功路径。大多数人写导入都是先想着怎么把数据插进去最后才补失败处理结果补得手忙脚乱。反过来先把失败数据怎么收集、怎么导出、怎么返回想明白成功路径反而是水到渠成的事。另外失败文件建议保留一段时间比如7天别导入完就删。运营经常是当天没改过两天才回来找文件。留个定时清理任务既不会撑爆磁盘又能覆盖大多数回看需求。这个细节不起眼但能省掉很多“文件还能再发我一份吗”的沟通成本。

相关新闻

Spring Data MongoDB复合索引失效?@CompoundIndexes正确使用与排查指南

Spring Data MongoDB复合索引失效?@CompoundIndexes正确使用与排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 10:02:53 阅读更多 →
单因素方差分析全解读:从变异分解到MATLAB实现

单因素方差分析全解读:从变异分解到MATLAB实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 10:02:53 阅读更多 →
Android Camera HAL3架构拆解:从请求-响应模型到Pipeline实现

Android Camera HAL3架构拆解:从请求-响应模型到Pipeline实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 10:02:53 阅读更多 →

最新新闻

坚果云文件保护锁怎么用?防止协作中的意外覆盖与版本冲突

坚果云文件保护锁怎么用?防止协作中的意外覆盖与版本冲突

文章目录一、一个场景理解文件保护锁的价值二、文件保护锁的工作原理2.1 保护锁 vs 版本冲突:预防 vs 补救2.2 锁定后的具体限制2.3 保护锁的生命周期2.4 与 Git Lock 的对比三、操作指南:三端详细步骤3.1 Windows 端3.2 macOS 端3.3 网页端四、团队协作…

2026/10/17 10:41:07 阅读更多 →
增量同步技术原理详解:为什么坚果云比普通网盘快10倍?

增量同步技术原理详解:为什么坚果云比普通网盘快10倍?

文章目录一、一个场景引出核心问题二、增量同步的算法基础:从 Rsync 讲起2.1 全量同步为什么慢2.2 Rsync 算法的核心思想第一步:固定分块 双校验和第二步:客户端匹配第三步:生成差异指令2.3 一个具体的例子三、从 Rsync 到现代增…

2026/10/17 10:41:07 阅读更多 →
Burp Suite 保姆级安装+手动配置要点|抓包避坑指南:证书冲突终极解决

Burp Suite 保姆级安装+手动配置要点|抓包避坑指南:证书冲突终极解决

Burp Suite 保姆级安装手动配置要点|抓包避坑指南:证书冲突终极解决 最近从零配置 Burp 抓包环境,踩遍了证书重复、旧证书冲突、HTTPS 抓包失败、网页打不开等几乎所有的新手坑。这里整理了一套稳定可复现的实操流程,包含安装要点…

2026/10/17 10:41:07 阅读更多 →
知识点总结线性方程组解判定总梳理(齐次 + 非齐次,考研线代核心)

知识点总结线性方程组解判定总梳理(齐次 + 非齐次,考研线代核心)

线性方程组解判定总梳理(齐次 非齐次,考研线代核心)nnn 未知数的个数 AAA:系数矩阵;(A,b)(A,b)(A,b) 就是增广矩阵 A‾\overline AA一、非齐次线性方程组 Axb\boldsymbol{Axb}Axb {r(A)≠r(A,b)  ⟺  无解r(A)r(…

2026/10/17 10:41:07 阅读更多 →
企业同时用多个国产大模型,如何用 TaoToken 统一 Key 把接入成本压到最低?

企业同时用多个国产大模型,如何用 TaoToken 统一 Key 把接入成本压到最低?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 10:41:07 阅读更多 →
反转链表详解:三指针迭代与递归回溯,附高频变体

反转链表详解:三指针迭代与递归回溯,附高频变体

有一道题,看起来不超过十行,却常年挂在算法面试和在线判题平台的热门榜单上,那就是 反转链表 。我见过不少朋友在数组上玩得很溜,交换两个元素用中间变量一气呵成,可一碰到链表就莫名发怵:数组可以通过下…

2026/10/17 10:40:07 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →