基于Spring Boot的医院预约挂号系统:从数据库设计到高并发抢号实战
简介这份资源是面向计算机专业学生与Java初学者的一套医院预约挂号系统毕业设计完整资料包含论文与可运行源码适合需要完成课程设计、毕业设计或想通过真实项目巩固Java Web开发技能的人群。压缩包共约2000个文件整体86.78MB以gif图片、xml配置、htm与aspx页面、css样式、js脚本及cs代码文件为主另含doc、docx论文文档、sql数据库文件mdf、ldf与少量dll组件覆盖前端页面、后端逻辑与数据库脚本等完整工程结构。目前已有208人学习下载。读者可从中获得一套可直接参考的挂号系统实现方案包括预约挂号、科室医生管理、用户信息维护等核心模块的代码与设计文档并借助论文理清需求分析、系统设计与实现思路便于二次开发或作为答辩材料使用。1. 从一份课程设计说起医院预约挂号系统到底在解决什么问题很多计算机专业的学生在毕业设计阶段都会碰到这个题目——基于Java的医院预约挂号系统。听起来像是老生常谈但真正动手做的时候你会发现它远比想象中复杂。它要解决的核心问题其实很朴素让患者不用凌晨去窗口排队让号源分配更公平让医生排班和患者需求之间尽量匹配。技术层面它涉及用户认证、排班管理、并发抢号、订单状态流转、数据一致性这几个硬骨头。适合谁看正在做课程设计或毕业设计的同学想拿它练手Spring Boot全栈的开发者以及需要快速搭一套预约类系统原型的小团队。这篇文章不讲空话直接按可落地的路径拆开讲。2. 技术选型与数据库设计为什么用Spring Boot而不是Servlet2.1 后端框架的取舍逻辑做这个系统第一道选择题就是后端框架。十年前大家用ServletJSP现在主流做法是Spring Boot。原因不复杂预约挂号的核心业务涉及大量的REST接口、事务控制和权限校验Spring Boot的自动配置和starter依赖能省掉大量XML配置。我一般会选Spring Boot 2.7.x或3.x搭配MyBatis-Plus前者生态最稳后者对单表CRUD几乎零SQL。如果你对JPA更熟用Spring Data JPA也行但在排班查询这种多条件动态拼接的场景下MyBatis-Plus的Wrapper会更顺手。前端方面如果只是课程设计Thymeleaf或VueElement UI都够用。想省事就用Thymeleaf服务端渲染想练前后端分离就上Vue3。数据库选MySQL 8.0缓存用Redis做号源库存扣减这是最经典的组合。2.2 核心表结构设计与字段说明表设计是这个系统的地基设计不好后面全是坑。最少需要这几张表用户表、医生表、科室表、排班表、预约订单表。下面给出关键表的建表语句。-- 用户表患者和医生共用用role区分 CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, role TINYINT NOT NULL DEFAULT 0 COMMENT 0患者 1医生 2管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 排班表核心表决定号源 CREATE TABLE schedule ( id BIGINT NOT NULL AUTO_INCREMENT, doctor_id BIGINT NOT NULL COMMENT 医生ID, dept_id BIGINT NOT NULL COMMENT 科室ID, work_date DATE NOT NULL COMMENT 出诊日期, time_slot TINYINT NOT NULL COMMENT 0上午 1下午, total_quota INT NOT NULL COMMENT 总号源数, remain_quota INT NOT NULL COMMENT 剩余号源, fee DECIMAL(10,2) NOT NULL COMMENT 挂号费, PRIMARY KEY (id), UNIQUE KEY uk_doctor_date_slot (doctor_id,work_date,time_slot) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医生排班表; -- 预约订单表 CREATE TABLE appointment ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, user_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待就诊 1已就诊 2已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id), KEY idx_schedule (schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约订单表;逻辑说明schedule表的uk_doctor_date_slot唯一索引保证同一个医生同一天同一时段只有一条排班记录避免重复排班。remain_quota字段是扣减号源的依据。appointment表的order_no唯一索引防止重复提交产生重复订单。参数说明time_slot用TINYINT而不是字符串省空间且查询快。fee用DECIMAL而不是FLOAT金额计算不能有精度丢失。status字段用TINYINT枚举比VARCHAR更省索引空间。2.3 号源扣减的两种实现路径号源扣减是并发最高的地方。常见做法有两种一是直接UPDATE schedule SET remain_quota remain_quota - 1 WHERE id ? AND remain_quota 0靠数据库行锁保证原子性二是用Redis的DECR命令预扣减再异步落库。前者实现简单适合课程设计后者性能高但要处理Redis和MySQL的数据一致性问题。我一般建议先用第一种把功能跑通压测发现瓶颈再引入Redis。3. 核心功能实现从登录到抢号的完整链路3.1 登录认证与权限拦截登录用Spring Security或自己写拦截器都行。课程设计里我倾向自己写一个简单的JWT拦截器代码量少且容易讲清楚。核心逻辑是登录成功后签发Token后续请求在Header里带Token拦截器校验并解析出用户ID和角色。// JWT工具类核心方法 public class JwtUtil { private static final String SECRET your-secret-key-must-be-long-enough; private static final long EXPIRE 24 * 60 * 60 * 1000L; // 24小时 public static String createToken(Long userId, Integer role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }逻辑说明createToken把用户ID和角色写进payload设置过期时间后用HS256签名。parseToken校验签名并返回Claims如果Token被篡改或过期会抛异常拦截器捕获后返回401。参数说明SECRET长度要够太短容易被暴力破解。EXPIRE根据场景调整课程设计24小时够用生产环境建议2小时并配合刷新Token机制。3.2 预约挂号的完整流程与代码预约流程分四步查排班、校验资格、扣号源、生成订单。这四步必须在一个事务里否则会出现号扣了但订单没生成的情况。Service public class AppointmentService { Autowired private ScheduleMapper scheduleMapper; Autowired private AppointmentMapper appointmentMapper; Transactional(rollbackFor Exception.class) public String book(Long userId, Long scheduleId) { // 1. 扣减号源利用行锁和remain_quota 0条件保证不超卖 int affected scheduleMapper.decreaseQuota(scheduleId); if (affected 0) { throw new BizException(号源已抢完); } // 2. 校验用户是否已挂过该排班的号防止重复挂号 Long count appointmentMapper.countByUserAndSchedule(userId, scheduleId); if (count 0) { throw new BizException(您已预约过该号源); } // 3. 生成订单 Appointment appt new Appointment(); appt.setOrderNo(generateOrderNo()); appt.setUserId(userId); appt.setScheduleId(scheduleId); appt.setStatus(0); appointmentMapper.insert(appt); return appt.getOrderNo(); } }对应的Mapper方法update iddecreaseQuota UPDATE schedule SET remain_quota remain_quota - 1 WHERE id #{scheduleId} AND remain_quota 0 /update逻辑说明decreaseQuota的WHERE条件里带remain_quota 0这是防超卖的关键。数据库在执行UPDATE时会加行锁多个请求同时到达时只有一个能成功扣减其余返回0行受影响。Transactional保证扣号和插订单要么都成功要么都回滚。参数说明rollbackFor Exception.class确保所有异常都触发回滚默认只回滚RuntimeException。generateOrderNo建议用时间戳用户ID后四位随机数保证唯一且可读。3.3 排班查询与分页展示患者查号时通常按科室和日期筛选。用MyBatis-Plus的Wrapper可以动态拼接条件。public PageScheduleVO querySchedules(Integer deptId, String date, Integer page, Integer size) { LambdaQueryWrapperSchedule wrapper new LambdaQueryWrapper(); wrapper.eq(deptId ! null, Schedule::getDeptId, deptId) .eq(StringUtils.hasText(date), Schedule::getWorkDate, date) .gt(Schedule::getRemainQuota, 0) // 只显示有号的 .orderByAsc(Schedule::getWorkDate); PageSchedule p scheduleMapper.selectPage(new Page(page, size), wrapper); // 转换为VO补充医生姓名、科室名称等 return p.convert(this::toVO); }逻辑说明eq方法的第一个参数是条件开关条件为false时不拼接该条件这样一套代码支持多种查询组合。gt(remainQuota, 0)过滤掉已约满的排班避免患者点进去才发现没号。参数说明page从1开始size建议不超过20太大影响响应速度。orderByAsc按日期升序让最近的排班排在前面。4. 避坑与排查那些年我们踩过的雷4.1 号源超卖现象、原因与解决现象压测时发现剩余号源变成负数或者同一个号被两个人约到。原因扣减号源的SQL没有加remain_quota 0条件或者用了先查后扣的两步操作中间有时间窗口。解决必须用UPDATE ... WHERE remain_quota 0的单条原子操作不要先SELECT再UPDATE。如果用了Redis预扣减要确保Redis扣减和MySQL扣减的最终一致性常见做法是Redis扣减成功后发消息到MQ异步落库失败则回补Redis。4.2 重复挂号同一个患者约了同一个号现象用户快速点击提交按钮生成两条订单。原因前端没做防抖后端没做唯一性校验。解决前端按钮点击后置灰后端在appointment表加uk_user_schedule唯一索引或者在插入前先查一次。更稳妥的做法是两者都做前端防误触后端兜底。4.3 事务失效扣了号但订单没生成现象号源减少了但订单表里查不到记录。原因Transactional注解失效常见情况是方法被同类内部调用、方法不是public、异常被catch后没重新抛出。解决确保事务方法通过代理调用异常要么不catch要么catch后throw new RuntimeException(e)。另外注意rollbackFor要设成Exception.class。4.4 时间字段的时区问题现象排班日期显示比实际少一天或多一天。原因数据库连接URL没设时区或者Java的Date和LocalDate转换时出错。解决JDBC URL加上serverTimezoneAsia/Shanghai实体类用LocalDate和LocalDateTime替代java.util.DateMyBatis-Plus对Java 8时间类型支持很好。4.5 密码明文存储现象数据库里能看到用户密码明文。原因注册时直接存了原始密码。解决用BCrypt加密Spring Security提供了BCryptPasswordEncoder每次加密结果不同但校验时能匹配。不要用MD5MD5已经被彩虹表攻破。5. 进阶技巧让系统更接近真实可用5.1 用Redis缓存排班数据排班数据读多写少非常适合缓存。做法是查排班时先查Redis没有则查MySQL并写入Redis设置5分钟过期。扣号源时同步更新Redis里的剩余号源避免缓存和数据库不一致。注意缓存穿透问题对不存在的排班ID也缓存一个空值过期时间设短一点。5.2 定时任务处理过期订单患者预约后如果一直不就诊号源就浪费了。可以加一个定时任务每天凌晨把前一天状态为“待就诊”且未就诊的订单自动取消并把号源回补到schedule表。用Spring的Scheduled注解就能实现。Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void cancelExpiredOrders() { ListAppointment expired appointmentMapper.selectExpired(); for (Appointment appt : expired) { appt.setStatus(2); // 已取消 appointmentMapper.updateById(appt); scheduleMapper.increaseQuota(appt.getScheduleId()); // 回补号源 } }逻辑说明cron表达式0 0 2 * * ?表示每天2点执行。先查出过期订单逐个取消并回补号源。回补号源的SQL是UPDATE schedule SET remain_quota remain_quota 1 WHERE id ?。参数说明cron的六个字段分别是秒、分、时、日、月、周?表示不指定。回补号源时要注意不要超过total_quota可以在SQL里加AND remain_quota total_quota条件。5.3 接口限流防止恶意抢号有人用脚本高频请求抢号需要在网关或拦截器里做限流。简单做法是用Redis的INCR命令每个用户每分钟最多请求10次预约接口。public boolean allowRequest(Long userId) { String key limit:book: userId; Long count redisTemplate.opsForValue().increment(key); if (count 1) { redisTemplate.expire(key, 1, TimeUnit.MINUTES); } return count 10; }逻辑说明每次请求对key自增第一次设置1分钟过期。超过10次返回false拦截器直接拒绝。这个方案简单有效适合课程设计。生产环境可以用Guava RateLimiter或Sentinel做更细粒度的控制。参数说明expire必须在increment返回1时设置否则每次都会刷新过期时间导致限流失效。阈值10根据实际并发调整太小误伤正常用户太大起不到防护作用。5.4 用状态机管理订单流转订单状态有“待就诊、已就诊、已取消”三种状态之间的流转是有规则的待就诊可以变成已就诊或已取消已就诊和已取消不能再变。用枚举加状态判断比一堆if-else清晰得多。public enum AppointmentStatus { PENDING(0, 待就诊), VISITED(1, 已就诊), CANCELLED(2, 已取消); private final int code; private final String desc; AppointmentStatus(int code, String desc) { this.code code; this.desc desc; } public static boolean canTransfer(int from, int to) { if (from PENDING.code) { return to VISITED.code || to CANCELLED.code; } return false; } }逻辑说明canTransfer方法定义了合法的状态流转路径只有待就诊状态才能流转到已就诊或已取消。在更新订单状态前先调用这个方法校验不合法就抛异常。参数说明code和数据库里的status字段对应。枚举的desc用于前端展示避免前端硬编码中文。做这个系统最大的教训是不要一开始就追求大而全。先把登录、排班、预约、取消这四个核心流程跑通再考虑缓存、限流、定时任务这些锦上添花的东西。我见过太多人卡在表设计阶段反复修改结果 deadline 到了连一个能跑的 Demo 都拿不出来。先把最小闭环做出来哪怕代码丑一点跑起来之后再重构。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

Java仓库管理系统源码解析:从跑通到二次开发实战

Java仓库管理系统源码解析:从跑通到二次开发实战

简介:这是一套面向Java Web初学者与课程设计者的仓库管理系统完整项目源码,围绕库存管理、进出货流程与供应链数据展开,可用于毕业设计、实训作业或框架学习。压缩包共70个文件,约8.52MB,以43张jpg界面素材、16个java源…

2026/10/12 2:38:28 阅读更多 →
黑苹果安装工具包全解析:镜像、引导、驱动与配置实战指南

黑苹果安装工具包全解析:镜像、引导、驱动与配置实战指南

简介:这份工具包面向希望在非苹果硬件上安装运行 macOS 的进阶用户,汇集黑苹果安装全流程所需的引导、驱动与配置工具,帮助解决硬件兼容性识别、系统引导和驱动匹配等核心难题。包内共 20 个文件,以 exe、7z、zip 等压缩与可执行文…

2026/10/12 2:38:28 阅读更多 →
C++策略模式高级应用:生命周期、组合与配置驱动实战指南

C++策略模式高级应用:生命周期、组合与配置驱动实战指南

策略模式大概是所有C开发者最早接触的那几个设计模式之一。我见过大量项目在三层继承之上堆了十几个策略类,把多态玩得很花哨,可一旦遇到组合策略、运行时装配、状态管理这些真实工程问题,代码就开始失控。今天想聊的不是“面向接口编程”的入…

2026/10/12 2:38:28 阅读更多 →

最新新闻

Streamyfin 后台下载模块(Background Downloader)深入解析:从 TypeScript API 到 iOS / Android 原生实现

Streamyfin 后台下载模块(Background Downloader)深入解析:从 TypeScript API 到 iOS / Android 原生实现

音视频移动开发 【免费下载链接】streamyfin A modern Jellyfin client built with Expo 项目地址: https://gitcode.com/gh_mirrors/st/streamyfin 点击查看 免费下载 本篇技术指南以 modules/background-downloader/README.md 为骨架,结合 Streamyfin…

2026/10/13 6:00:40 阅读更多 →
10 分钟跑通私有本地 AI 聊天:Open WebUI 从零部署到第一句对话

10 分钟跑通私有本地 AI 聊天:Open WebUI 从零部署到第一句对话

10 分钟跑通私有本地 AI 聊天:Open WebUI 从零部署到第一句对话 【免费下载链接】open-webui User-friendly AI Interface (Supports Ollama, OpenAI API, ...) 项目地址: https://gitcode.com/GitHub_Trending/op/open-webui Open WebUI 是一个开源的自托管…

2026/10/13 6:00:40 阅读更多 →
Kener 状态徽章实战指南:status、uptime 与 Dot 徽章的 URL 语法、查询参数与源码实现解析

Kener 状态徽章实战指南:status、uptime 与 Dot 徽章的 URL 语法、查询参数与源码实现解析

可观测性后端前端运维 【免费下载链接】kener Stunning status pages, batteries included! 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ke/kener 点击查看 免费下载 Kener 内置了三种可直接以 <img> 引入的状态徽章&#xff08;Badge&#xff09;&#xff1…

2026/10/13 6:00:40 阅读更多 →
AUV协同定位与故障检测一体化MATLAB实现

AUV协同定位与故障检测一体化MATLAB实现

简介&#xff1a;本资源是一份面向海洋机器人与水下自主系统研究者的AUV协同定位算法实现方案&#xff0c;聚焦多AUV编队中高精度定位与实时故障检测两大核心挑战&#xff0c;适用于自动化、控制工程及海洋探测方向的研究生、科研人员与算法工程师。压缩包为RAR格式&#xff0c…

2026/10/13 6:00:40 阅读更多 →
Pomotroid 计时器时长键盘输入:从整分钟滑块到 MM:SS 精确设置的完整改造指南

Pomotroid 计时器时长键盘输入:从整分钟滑块到 MM:SS 精确设置的完整改造指南

【免费下载链接】pomotroid :tomato: Simple and visually-pleasing Pomodoro timer 项目地址&#xff1a; https://gitcode.com/gh_mirrors/po/pomotroid 点击查看 免费下载 Pomotroid 是一款基于 Tauri&#xff08;Rust Svelte&#xff09;的番茄钟应用。本篇技术指南围绕其…

2026/10/13 6:00:40 阅读更多 →
OpenClaw 跨平台部署指南:Windows/macOS/Linux 环境搭建避坑全记录

OpenClaw 跨平台部署指南:Windows/macOS/Linux 环境搭建避坑全记录

如果你准备把 OpenClaw 真正用起来&#xff0c;第一道门槛往往不是模型接入&#xff0c;也不是技能配置&#xff0c;而是环境搭建。我见过太多人卡在“装好了但跑不动”“这台能跑换台就崩”的阶段&#xff0c;尤其是在 Windows、macOS、Linux 三套系统之间反复横跳的时候&…

2026/10/13 5:59:40 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目&#xff0c;我最大的感受是&#xff0c;大部分坑不是出在技术前沿&#xff0c;而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节&#xff0c;最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了&#xff0c;很难不碰到舵机控制这道坎。做智能小车要转方向&#xff0c;做机械臂要抬关节&#xff0c;做云台要偏航俯仰&#xff0c;背后都是同一个东西在干活&#xff1a;舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”&#xff1f;——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台&#xff1f;或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜&#xff1f;又或者&#xff0c;在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 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/12 21:05:09 阅读更多 →
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/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练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/12 18:11:17 阅读更多 →