news 2026/9/10 6:44:09

SpringBoot+MyBatis-Plus打造乡村儿童帮扶管理平台实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+MyBatis-Plus打造乡村儿童帮扶管理平台实战

1. 立项前的实地调研:乡村儿童帮扶场景里的真实痛点

1.1 信息散落在三套台账里,根本对不上

做这个平台之前,我在一个乡镇的志愿者团队里待过一段时间。当时大家管理乡村儿童信息的方式非常原始:村委有一份留守儿童花名册,学校有一份贫困生档案,镇民政所还有一份低保家庭台账。三套数据各记各的,字段口径也不一样,有的按年龄分,有的按年级分,有的按是否单亲分。真到了要帮扶的时候,发现同一户孩子的家庭情况在三个表里写的是三个版本,甚至连名字都可能因为同音字被录成不同写法。

这个痛点是导致项目启动的直接原因。基层志愿者不是不想把数据管好,而是没有一套统一的工具,平时填表、汇总、上报全靠Excel来回传,版本乱、易丢失、没法多人协同。所以这个帮扶平台的第一个核心定位就很清晰:先解决“一个孩子只有一份标准档案”的问题,让村干部、学校老师、志愿者在同一个系统里看到同一份信息。

1.2 帮扶过程“重结果、轻过程”,监督和追溯都很困难

乡村儿童帮扶和普通慈善捐赠有个很大的区别:帮扶是一个长期持续的过程,不是一次性给钱给物就结束了。一个志愿者结对了一个孩子,今天去家访,明天电话沟通,后天帮孩子辅导作业,这些过程都是帮扶成果的一部分。但在没有系统的时期,这些过程全靠志愿者的个人记录,写在笔记本上、存在手机备忘录里,或者干脆口头说一句,最后被统计成“帮扶次数:1次”。

问过几个公益组织的负责人,他们最想知道的不是志愿者去了几次,而是孩子这个月有没有持续受到关注、上次反馈的问题解决了没有、帮扶关系是否稳定。这种对“过程数据”的需求,靠Excel是管不出来的。这就决定了平台不能只做档案管理,还得有帮扶日志、结对关系、反馈跟进这类功能。

1.3 平台定位:管理工具为主,展示页面为辅

很多初学者拿到类似需求,第一反应就是把前台活动页面做得花里胡哨,轮播图、爱心动态、志愿者风采铺满首页。但我做这个项目的定位很明确:这是一个给乡镇志愿者和公益组织使用的内部管理工具,不是面向社会公众的宣传网站。前台可以有一个简单的信息展示,但核心价值全在后端管理功能上。

我不是说展示不重要,而是资源有限的时候必须排优先级。乡村儿童帮扶场景里,最缺的是“把账记清楚”“把事跟到底”的能力。所以这个SpringBoot项目的功能设计围绕四条业务线展开:儿童档案、结对帮扶、物资捐赠、公益活动。把这四条线做扎实了,平台才真正有用。

2. 技术选型与数据库设计:为什么SpringBoot这套组合最适合这种项目

2.1 技术选型逻辑:够用、好维护、团队上手快

这个项目从第一天就定下了技术基调:SpringBoot + MyBatis-Plus + MySQL + Thymeleaf。很多人会问为什么不用Spring Cloud、不用前后端分离、不用Vue,我的回答是:看场景。

乡村儿童帮扶平台的用户群体是乡镇干部、学校老师、志愿者,他们不会关心你用的是什么架构,只关心好不好用、稳不稳定。SpringBoot 2.7.18是目前比较稳妥的版本,既支持JDK8,又不至于像SpringBoot 3.x那样强制要求JDK17。对于部署在乡镇老旧服务器上的应用,JDK8的兼容性优势非常大。MyBatis-Plus则大幅简化了单表CRUD的开发量,少写大量XML。

前端没有用前后端分离,因为这种管理型系统页面交互复杂度不高,Thymeleaf服务端渲染就够了。它天然解决了一个前后端分离架构里很麻烦的问题:页面权限控制。后端在渲染模板时就可以根据当前用户角色决定显示哪些菜单、隐藏哪些按钮,不需要前端先拿一堆权限数据再去做判断。

2.2 核心表结构:用五张主表撑起整个业务闭环

数据库是这类管理系统的命脉。我设计表结构的时候,没有一开始就搞二十多张表,而是先梳理清楚业务主链路:一个孩子进来,被志愿者结对,得到物资帮助,参与公益活动,形成反馈记录。

第一张主表是child_info,存儿童基本信息。我在设计时特别注意了几个在乡村场景下容易忽略的字段:guardian_name(监护人姓名)、guardian_relation(与孩子关系)、family_type(家庭类型,如双亲在外、单亲、孤儿、事实无人抚养等)、poverty_level(困难程度)。这些字段直接决定了后面结对和捐赠时的优先级排序。

第二张表是pair_relation,记录志愿者和儿童的结对关系。这里有一个容易踩坑的地方:结对关系不是一次性建立的,它有一个完整的生命周期。我用了status字段来管理:待审核、帮扶中、已结束。start_dateend_date分别记录结对开始和结束时间,pair_reason记录结对理由。这样设计之后,统计每个志愿者累计帮扶了多少孩子、当前正在帮扶几个,一条SQL就能查出来。

第三张表是donation_record,记录捐赠行为。字段包括donor_name(捐赠人/单位)、donation_type(资金/物资)、amount(金额)、item_name(物资名称)、item_quantity(物资数量)、receiver_child_id(接收儿童ID)。这里我踩过一个坑,就是一开始把资金和物资放在两张表里,结果统计报表的时候需要反复合并查询,非常痛苦。后来干脆合并成一张表,通过donation_type区分,报表瞬间好写很多。

第四张表material_stock用于物资库存管理。做公益系统的人都懂,捐赠物资的出入库是最容易被诟病的地方,因为涉及钱物,必须账实相符。material_stock记录当前库存,同时用stock_in_recordstock_out_record两张流水表记录每一笔变动。这样任何一个批次物资从进库到出库,全程可追溯。

第五张表activity_info记录公益活动,配合activity_signup记录报名信息。活动表里我加了一个signup_start_timesignup_end_time字段,用于控制报名窗口。很多类似项目没有这个字段,导致活动都结束了志愿者还能报名,非常尴尬。

2.3 数据库设计的几个关键设计决策

在表结构设计上,有几个决策值得单独说。第一个是统一使用逻辑删除。乡村帮扶平台涉及儿童隐私数据,一旦误删很难恢复。我在所有业务表加了deleted字段,MyBatis-Plus的@TableLogic注解统一处理。删除操作都变成UPDATE,数据不会真正消失,审计和恢复都有余地。

第二个是创建时间和更新时间字段统一。每张表都有create_timeupdate_time两个字段,由MyBatis-Plus的自动填充功能处理,不需要业务代码手动维护。这个看似不起眼,但后期排查问题、统计数据的时候太重要了。

第三个是外键一律不建物理外键。很多用惯了MySQL Workbench图形界面的人喜欢把外键建上,实际生产中这是给自己挖坑。物理外键会导致插入、更新、删除时数据库频繁做关联校验,性能下降不说,业务上的灵活扩展也会受限。我的做法是只建普通索引,外键关系在应用层维护,比如查询儿童所在学校时,通过school_id去学校表查。

3. 核心功能模块的代码实现思路:从增删改查到业务闭环

3.1 儿童档案模块:批量导入比单人录入重要得多

儿童档案模块做起来不难,典型的CRUD。但这里有个很实在的经验:与其反复优化单人录入表单,不如做好Excel批量导入。

乡村场景下,学校、村委手里的数据本身就是Excel表格,志愿者一个个手工录入既慢又容易出错。我实现了一个基于EasyExcel的批量导入功能,模板里包含姓名、性别、出生日期、身份证号、监护人、家庭住址等核心字段。导入时后台逐行校验,身份证号格式不对、必填项为空、出生日期大于今天等问题的行都会被记录下来,最后返回一个错误提示列表,告诉用户第几行哪一列有问题。

批量导入的代码有一个关键点需要提醒:不要一次性把所有数据都读进内存。EasyExcel支持流式读取,配合逐行校验,几十万行数据也不会OOM。刚开始我图省事直接把整个Excel转成List再处理,三千条数据就跑得很吃力,换成流式读取后问题就解决了。

EasyExcel.read(inputStream, ChildImportDTO.class, new ChildDataListener(childService)) .sheet() .doRead();

ChildDataListener里重写invoke方法,每解析一行就调用一次childService.save,同时在内存里维护一个List<String> errorMsgList,解析完整个文件后一次性返回给前端。项目源码里这个类写得很完整,直接看代码比看描述更直观。

儿童档案的查询页也值得说。我没有用什么高级的搜索引擎,就是用MyBatis-Plus的LambdaQueryWrapper做多条件组合查询。姓名用like,家庭类型用eq,困难程度用gele组合成范围查询,创建时间用between。页面提供查询表单,用户选什么条件就拼什么条件,不选就默认查全部。

3.2 结对帮扶模块:状态机设计避免业务逻辑混乱

结对帮扶是平台里面业务状态最复杂的模块。我前面提到pair_relation表里有个status字段,这个字段配合一组状态流转规则,就是一个小状态机。

结对流程是这样的:志愿者在前台提交结对申请,填写想要结对的孩子和结对理由,状态变为PENDING(待审核)。管理员审核通过后,状态变为ING(帮扶中),系统自动记录start_date为当天。帮扶过程中志愿者可以填写帮扶日志,每次日志都会关联到结对记录上。如果志愿者因为特殊情况不能继续帮扶,可以发起终止申请,管理员确认后状态变为ENDEDend_date记录当天。

状态机的优势是让代码逻辑非常清晰。我把状态流转写在一个Service方法里,每次更新状态前先判断当前状态是否能流转到目标状态。比如PENDING状态只能流转到INGREJECTED,不能直接跳到ENDED。这样即使前端被绕过,接口被非法调用,后端状态也不会乱。这个设计逻辑在公益类管理系统里尤其重要,因为你并不知道哪个志愿者会手滑把状态改错。

帮扶日志表pair_log字段有pair_idlog_contentlog_type(家访/电话沟通/作业辅导/物资送达等)、next_follow_up_date(下次跟进日期)。next_follow_up_date这个字段是我后来加的,作用是配合定时任务,每天扫描一次,把当天需要跟进的帮扶记录推送给对应志愿者,防止帮扶中断。这个功能在实施过程中被志愿者广泛使用,因为人一旦忙起来,真的会忘记自己答应过孩子下周再去看看。

3.3 捐赠与物资管理:库存流水是防扯皮的关键

捐赠物资管理是这个平台里最容易起纠纷的模块。以前没有系统记录时,经常出现“捐了100本课外书,实际发给孩子的只有60本,剩下的去哪了”这种问题。所以我在设计时定了一条铁律:所有实物出入库必须写流水,没有流水就没有发言权

stock_in_record表里,我记录入库单号、来源捐赠记录ID、物资名称、入库数量、入库时间、经手人。在stock_out_record表里,记录出库单号、接收儿童ID或活动ID、物资名称、出库数量、出库时间、经手人。每次出库时后端先检查库存是否足够,不足则拒绝出库并提示当前剩余量。这样每一件物资的去向都有据可查,后面审计的时候再也不会有人来问“东西去哪了”。

由于项目里实现了库存扣减的并发控制,一开始我犯了个错误:直接用SELECT stock FROM material_stock WHERE id = 1查出当前库存,然后在Java代码里判断是否够扣,够则执行UPDATE。这个逻辑在并发情况下会出问题:如果两个出库请求同时读到库存为10,各自扣5,最后执行更新时两个事务都会把库存变成5,实际应该剩0。解决方法是使用乐观锁,在material_stock表加version字段,更新时带上WHERE version = 旧版本号,更新不成功说明数据已被别人改过,重新查再试。

3.4 活动与资讯模块:报名先锁名额,签到用二维码

活动模块相对简单,但报名环节有一个并发问题需要处理。一个活动名额可能很快就没了,多个志愿者同时点报名,如果不在数据库层面做限制,会出现超报。我的做法是使用MySQL的SELECT ... FOR UPDATE行锁:报名前先锁定活动记录行,查出当前已报名人数,判断是否小于名额上限,满足才插入报名记录。整个过程放在一个事务里,事务提交后释放锁。

活动签到我用了二维码方案。管理员在活动开始前生成一个签到的二维码,里面包含活动ID和一个随机token。志愿者用手机微信扫码,浏览器访问签到接口,后端校验token并记录签到时间。做这个功能时要注意token的过期时间,我设置的是活动开始前1小时到活动结束后2小时之间有效,防止二维码被提前泄露或事后补签。

4. 开发中最容易翻车的四个Java/SpringBoot细节

4.1 日期时间字段的序列化与传参陷阱

这个坑我记了很久。SpringBoot后端接收前端传来的日期字符串时,如果前端传的是2024-06-15,后端用DateLocalDate接收,大概率会报转换错误。原因很简单:SpringBoot默认的Jackson序列化器不认识这种格式。

解决方法是在实体日期字段上加@JsonFormat注解,或者在全局配置里统一指定格式:

@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") private LocalDate birthDate;

我还遇到过更隐蔽的问题:数据库存的是datetime类型,Java用了LocalDate接收,结果时分秒被丢弃,而且MyBatis在映射时可能因为类型不匹配报错。类似场景的教训是:只要字段涉及具体时间点,Java类型就用LocalDateTime;只要字段只关心日期,就用LocalDate,不要混用,混用必出问题。

4.2 文件上传的路径问题与静态资源映射

平台里有上传头像、上传活动图片的需求。我第一次实现时把保存路径写死为D:/upload/,在自己电脑上跑没问题,打包部署到服务器后就404了。后来改成相对路径:System.getProperty("user.dir") + "/upload/",至少解决了Linux下路径不存在的问题。

但还有个问题,SpringBoot默认只处理/static/下的静态资源,自定义上传目录不在处理范围内。需要在配置类里添加资源映射:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath); }

这里有个生产环境必须注意的点:不要把上传目录放在jar包所在目录。因为每次发布新版本,如果用了java -jar app.jar方式启动,jar包所在目录可能被运维脚本清空重建。我的做法是先启动一个专门存放上传文件的目录,如/data/help-platform/upload,然后在启动参数里通过--upload.path指定。这样哪怕重新部署,数据也不会丢。

4.3 事务失效的三种典型场景

SpringBoot里事务用起来很简单,但失效场景也很多。我在这个项目里遇到过三种情况。

第一种是this调用。一个Service类里,methodA()没有加@Transactional,它调用了同类中加@TransactionalmethodB(),这时候@Transactional是无效的。原因是Spring的声明式事务基于AOP代理,this调用走的是原始对象而不是代理对象,事务自然不生效。解决方法是把需要事务的方法拆到另一个Service里,或者自己注入代理对象。

第二种是异常被吞掉。@Transactional默认只在RuntimeException和Error时回滚,如果方法里捕获了异常并打印日志但没抛出,事务不会回滚。我后来在项目里养成了一个习惯:业务方法里不做try-catch,异常直接往上抛,由全局异常处理器统一处理。

第三种是数据库引擎问题。MySQL的MyISAM引擎不支持事务,只有InnoDB支持。这个问题在新项目里基本不会遇到,但如果是改造老项目,一定要确认建表语句里有ENGINE=InnoDB,否则配置再多@Transactional也是白搭。

4.4 列表查询的N+1问题与SQL性能优化

平台里的列表页面经常需要展示关联数据,比如儿童列表要显示所在学校名称、结对状态、最近帮扶时间。我刚写完的时候用的是for循环里逐个查询关联表的方式,一两百条数据页面响应要两三秒,数据量上来之后更慢。

后来发现这是典型的N+1查询问题。优化方案是在Mapper里写一个连表查询,一次性查出列表数据:

SELECT c.id, c.name, c.gender, c.birth_date, s.school_name, COUNT(p.id) AS pair_count FROM child_info c LEFT JOIN school_info s ON c.school_id = s.id LEFT JOIN pair_relation p ON c.id = p.child_id AND p.status = 'ING' WHERE c.deleted = 0 GROUP BY c.id ORDER BY c.create_time DESC LIMIT #{offset}, #{size}

这里用LEFT JOIN保证即使孩子没有学校、没有结对记录也能查出来。COUNT(p.id)GROUP BY配合下能顺便统计结对次数,一次查询解决列表展示的数据需求。优化后同样数据量下响应从两三秒降到了几十毫秒。

5. 部署到服务器后的运维优化:从能跑到跑得稳

5.1 配置文件区分环境与外部化

项目开发环境、测试环境、生产环境的数据库连接、上传路径、日志级别都不一样。我从一开始就不允许把配置写死在application.yml里,而是用spring.profiles.active切换环境配置。

生产环境部署时用java -jar help-platform.jar --spring.profiles.active=prod --upload.path=/data/help-platform/upload的方式启动。这样配置全部外置,换服务器迁移时只需要把jar包和upload目录拷过去即可,不用重新编译。

日志配置也值得专门说。我用了Logback的滚动日志策略,按天切割,保留最近30天。日志文件路径指向/data/help-platform/logs/,和jar包目录分开。以前图省事把日志放在当前目录,结果日志文件把磁盘撑爆,应用直接挂掉,这个教训很深刻。

5.2 定时任务:数据统计与库存预警

乡村儿童帮扶平台不是交易系统,没有高并发压力,但有两个场景很适合用定时任务。

第一个是每天凌晨统计帮扶数据。包括当前帮扶中结对数量、本月新增捐赠金额、本月活动参与人次等,写进一张statistics_daily表。页面统计报表直接查这张表,不需要实时去主表做聚合,大幅降低主表查询压力。这个功能初期没有,是后期迭代加上的,因为管理员每天早上一打开系统就想看到数字,实时统计太慢。

第二个是库存预警。用@Scheduled(cron = "0 0 9 * * ?")每天早上9点执行一次,扫描material_stock表中低于预警阈值的物资,把预警信息插入到notice_info表,管理后台首页展示。这样志愿者不用每天手动去看库存够不够,系统自动提醒该补货了。实现这个功能时注意@Scheduled默认是单线程执行的,多个任务放在同一个类里会互相阻塞,建议在启动类加@EnableScheduling,需要并发执行的定时任务通过@Async异步运行。

5.3 前端静态资源的缓存策略

Thymeleaf模板项目虽然不像Vue单页应用那样有打包优化的问题,但静态资源缓存仍然值得优化。浏览器默认会对CSS、JS文件做缓存,发布新版后发现用户还在用旧缓存,体验极差。

我的做法是给静态资源URL加版本号后缀:

<link th:href="@{/css/app.css?v=1.0.3}" rel="stylesheet">

或者用SpringBoot的静态资源版本策略,在配置里加上spring.web.resources.chain.strategy.content.enabled=true,框架会根据文件内容的MD5自动生成版本号。这样只要文件内容变了,URL就会变,浏览器就不会命中旧缓存。这个方法成本极低,但经常被忽视,我建议所有SpringBoot项目都配上。

6. 源码里值得反复读的几个关键类

6.1 ChildDataListener:批量导入的流式处理范例

有朋友拿到源码后私信我,说想看看Excel导入的实现。我觉得读源码可以从ChildDataListener看起,这个类几十行代码,但把EasyExcel的核心用法演示得很完整。它继承了AnalysisEventListener<ChildImportDTO>,重写了invokedoAfterAllAnalysed两个方法,前者处理每一行数据,后者在全部读取完成后做汇总处理。

里面有一个值得借鉴的小设计:数据校验失败的行不会立即抛异常,而是把错误信息收集起来,等全部解析完之后一起返回。这样用户拿到的是一个完整的错误清单,不用反复修改上传。如果某一行校验失败就抛异常中断,用户体验会非常差。

6.2 PairStateMachine:状态流转的逻辑集中管理

结对模块的状态逻辑我单独抽了一个PairStateMachine类,里面维护一个Map<PairState, List<PairState>>,清晰地声明每种状态可以流转到哪些状态。比如:

PENDING -> ING, REJECTED ING -> ENDED

所有状态流转都通过这个类校验,代码可以做到“不会出现非法的状态组合”。这是我在这个项目里比较满意的一个设计,虽然简单,但对于业务状态多的模块来说,复用的价值很高。

6.3 GlobalExceptionHandler:统一异常处理很关键

管理系统的后台接口如果直接返回500错误页面,会给用户留下“系统不稳定”的感觉。我在源码里实现了一个全局异常处理器@RestControllerAdvice,按异常类型分别处理。

业务异常,如“库存不足”“活动名额已满”,返回code: 400, msg: 具体提示;参数校验异常,如手机号格式错误,返回code: 422;未登录访问,返回code: 401提示去登录;兜底的Exception,返回code: 500, msg: 系统繁忙,请稍后重试。这样前端拿到什么code都可以统一处理,不用在Controller里写大量重复的try-catch。

6.4 权限拦截器与用户上下文

项目的权限控制用的是拦截器加注解的方式,没有引入Spring Security。对于这种内部管理系统,Spring Security太重了,配置量大,学习成本高。拦截器思路是:在preHandle方法里检查请求路径是否在放行列表(如登录接口、首页展示)中,不在则从Session中取出当前登录用户,取不到就重定向到登录页,取到了就放入ThreadLocal里的UserContext,供后续业务代码随时取用。

有点需要注意的是ThreadLocal在线程池场景下会串数据,但在本项目里拦截器后执行的业务代码都是同一个请求线程,用完在afterCompletionremove就好。这个思路在中小型管理系统里越用越香,值得试试。

截至写这篇文章的时候,平台已经在一个县城级别的公益组织里跑了两个多月。最大的感受是:技术本身不复杂,真正有价值的是把乡村儿童帮扶这件事的流程理清楚,再转化成代码逻辑。有次志愿者反馈,以前要花一上午整理的月报,现在打开系统一点导出就生成了,那种帮助到实际工作的成就感,比用上什么高深技术来得更真实。

如果你打算拿这个项目当毕设或者作为公益组织的内部工具,我建议你先别急着改功能,把结对帮扶的状态流转和物资出入库流水两块吃透。这两块是这个系统的骨架,也是真正有业务深度的部分。后面想扩展,无论是加个在线课堂模块,还是对接公众号消息推送,都不会破坏现有的架构。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 6:42:45

Hermes Python库:轻量嵌入式Agent集成方案

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

作者头像 李华
网站建设 2026/9/10 6:39:55

机器人关节模组选型指南:电机、减速器与驱动链路匹配实践

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

作者头像 李华
网站建设 2026/9/10 6:37:36

Matlab RSA图像加密:密钥生成、像素分块与模幂运算实现拆解

简介&#xff1a;这是一份面向图像加密入门者与Matlab开发者的RSA图像加密解密完整实现&#xff0c;可用于数字图像保密传输、教学实验与算法复现。代码基于Matlab 2019b编写&#xff0c;主程序main.m可直接运行&#xff0c;配套一系列功能函数完成密钥生成、像素级加密与解密还…

作者头像 李华
网站建设 2026/9/10 6:34:53

瑞数5代反爬破解:补环境与__rsc后缀生成实战指南

简介&#xff1a;本资源聚焦瑞数5代&#xff08;rs5&#xff09;动态反爬机制的实战破解&#xff0c;面向中高级Python爬虫开发者与Web安全研究者&#xff0c;解决rs5环境补全、动态Cookie生成及URL后缀算法还原等核心难点。压缩包共7个文件&#xff0c;含5个JavaScript脚本&am…

作者头像 李华
网站建设 2026/9/10 6:34:22

视频AI中台实战:Docker多架构镜像与K8s弹性调度全解析

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

作者头像 李华