尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
宠物医院预约小程序课设源码:Java后端调用链路全解析
简介基于Java开发的宠物医院预约微信小程序完整源码与数据库文件面向计算机相关专业的在校学生和教师。资源围绕宠物医院预约业务场景包含完整代码与数据表脚本适合作为课程设计、期末大作业或毕业设计的基础方案也可用于Java及小程序开发的进阶练习。压缩包约95KB共包含49个文件。核心由35个Java源文件组成承担后端业务逻辑与接口实现同时提供SQL数据库脚本、YAML启动配置、XML配置、HTML页面、JAR依赖、properties配置及说明文档覆盖运行与二次开发所需的关键内容目录结构清晰导入IDE后即可对照源码查阅。该资源已有171人学习或下载代码功能已经过验证可稳定运行。除了直接使用外还可在其基础上做二次开发例如扩展宠物档案管理、在线支付或评价功能从而快速搭建满足毕设或课设要求的完整项目。1. 宠物医院预约小程序课设源码一份能讲清调用链路的 Java 全家桶拿到一份基于 Java 开发的宠物医院预约微信小程序课设项目源码我最先看的不是页面而是后端那套 Controller-Service-Mapper 链路。因为这类系统真正决定答辩成败的地方不在小程序长什么样而在预约单从提交到落库之间的每一步逻辑。这份压缩包里有完整的 Java 后端工程、petsdn.sql 数据库初始化脚本、Maven Wrapper 和项目文档覆盖用户登录、宠物档案、医生排班、在线预约等核心模块。适合计科、软工、大数据、人工智能等专业的毕业设计、期末大作业和课程设计也适合想快速上手 Spring Boot 加小程序联调的人。下面按我拆项目的顺序先盘结构再跑数据库最后说清楚联调里必须绕开的坑。2. 先拆工程再动手这份 Java 源码的模块划分与文件用途2.1 Maven 工程里的六个文件分别管什么解压后第一件事不是急着打开 IDE而是先把目录结构过一遍。这里没有复杂的微服务拆分就是一个标准 Maven 单工程但每个文件都有明确职责。我按自己排查的顺序列了一张清单文件/目录在项目里的作用拿到后应该确认什么pom.xmlMaven 依赖与构建配置父依赖版本、Spring Boot 起步依赖、MySQL 驱动和 MyBatis 是否齐全src/main/java后端业务代码按 controller/service/mapper/entity 分包启动类位置、接口是否带统一前缀src/main/resourcesapplication.yml 配置、XML Mapper、静态资源数据源配置、端口、日志级别src/test冒烟测试与接口测试有没有 SpringBootTest测试库连接.mvn/wrapperMaven Wrapper 配置与本地 jar决定项目用哪个 Maven 版本能不能离线跑petsdn.sql建库建表与初始化数据表引擎、字符集、预约状态字段项目.md项目说明文档与代码是否同步有没有过期配置很多同学拿到课设源码习惯直接开 IDEA 然后等索引结果跑不起来就怀疑包有问题。实际上大部分起不来的原因都能在这张表里找到要么 pom.xml 依赖没拉全要么 application.yml 里数据库密码还是原作者的要么 petsdn.sql 压根没导入。我一般先打开项目.md 和 pom.xml花十分钟确认技术栈再决定后面怎么跑。这份承重结构里pom.xml 决定你能不能编过。常见误用是本地 Maven 版本太老或者 JDK 版本和 Spring Boot 不匹配。打开 pom.xml 先看父依赖和插件版本例如parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.x.x/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.x.x/version /dependency /dependencies这里最需要注意的是 spring-boot-starter-parent 的版本要和 JDK 匹配。Spring Boot 2.x 对应 JDK 8 或 11Spring Boot 3.x 对应 JDK 17。如果本地环境是 JDK 8却强行用 3.x编译直接报错。MySQL 驱动这里用的是 runtime scope意思是编译期不需要运行期才加载所以报驱动找不到时优先查本地仓库有没有拉成功。2.2 后端包的调用链路从 Controller 到 Mapper看代码的顺序我习惯沿着一次预约请求走。小程序端发起 wx.request请求打到后端 ControllerController 不写业务只做参数接收真正的判断在 Service落库在 Mapper。下面这个预约接口的写法在课设里很典型RestController RequestMapping(/api/appointment) public class AppointmentController { Autowired private AppointmentService appointmentService; PostMapping(/create) public Result create(RequestBody AppointmentCreateDTO dto) { // 参数校验放 Controller业务规则放 Service if (dto.getDoctorId() null || dto.getTimeSlot() null) { return Result.error(医生和时段不能为空); } return appointmentService.createAppointment(dto); } }这个接口做的事情很纯粹接收 JSON、做非空校验、交给 Service。注意 RequestBody 要求小程序端必须设置 Content-Type 为 application/json很多联调失败就死在这里。DTO 字段用驼峰命名和小程序端传参保持一致省去一堆字段映射的麻烦。Service 层才是这份源码里值得抄的部分。预约的核心是冲突判断同一个医生同一个时段不能出现两条有效预约。Service 里要先查排班状态再查是否已有未取消的记录最后才 insert。这种先查后插的方式在并发量低的课设场景完全够用也比直接甩一个唯一索引更容易在答辩时讲清楚思路。Service public class AppointmentServiceImpl implements AppointmentService { Autowired private AppointmentMapper appointmentMapper; Override Transactional public Result createAppointment(AppointmentCreateDTO dto) { // 1. 查排班表确认该时段可预约 Schedule schedule scheduleMapper.selectByDoctorAndSlot( dto.getDoctorId(), dto.getAppointmentDate(), dto.getTimeSlot()); if (schedule null || schedule.getSlotStatus() ! 1) { return Result.error(该时段不可预约); } // 2. 查预约表避免重复预约 int count appointmentMapper.countByDoctorAndSlot( dto.getDoctorId(), dto.getAppointmentDate(), dto.getTimeSlot()); if (count 0) { return Result.error(该时段已被预约); } // 3. 插入预约单 return Result.success(appointmentMapper.insert(dto.toEntity())); } }这里有两个参数值得注意。Transactional 保证第 2 步到第 3 步之间出错时事务回滚不会出现查到了但没插入的脏状态。slotStatus 用 int 而不是 boolean是为了后续扩展比如 0 代表停诊1 代表可约2 代表约满int 比 boolean 好在改状态时不用改表结构。如果你要把这个课设升级成毕设这段逻辑就是状态机扩展的起点。2.3 小程序端与后端的同名映射字段命名不一致是联调第一坑后端接口写好了小程序端还得能对得上。原生微信小程序的请求封装一般长这样// utils/request.js const BASE_URL http://localhost:8080; function request(path, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json }, success: (res) { if (res.statusCode 200) { resolve(res.data); } else { reject(res); } }, fail: (err) reject(err) }); }); } module.exports { request, BASE_URL };BASE_URL 是联调第一个要改的地方。用微信开发者工具调试时localhost 指向你自己的电脑没问题但真机预览时手机访问不到电脑的 localhost必须改成局域网 IP比如 http://192.168.x.x:8080。这里有个细节微信开发者工具默认会校验合法域名课设阶段直接在工具右上角关闭域名校验即可不需要配 HTTPS。每次改完 BASE_URL要重新编译小程序才会生效这个坑我踩过不止一次。3. 数据库先行petsdn.sql 的表结构、初始化数据与预约约束3.1 六张核心表的字段设计与外键关系petsdn.sql 是这份资源里含金量最高的部分因为预约类系统的复杂度主要在数据关系上。表结构按业务拆成六张核心表用户表、宠物档案表、医生表、排班表、服务项目表、预约单表。我简化后的核心字段如下表名关键字段说明userid, openid, nick_name, phone, create_timeopenid 关联微信登录petid, user_id, pet_name, pet_type, breed, birth_date一个用户多只宠物doctorid, name, title, department, intro医生基本资料scheduleid, doctor_id, work_date, time_slot, slot_status排班时段slot_status 1 可约service_itemid, name, price, duration服务项目如疫苗、绝育appointmentid, appointment_no, user_id, pet_id, doctor_id, schedule_id, service_item_id, appointment_date, time_slot, status, remark预约主表status 记录业务状态外键关系要能讲清楚用户和宠物是一对多医生和排班是一对多预约单同时关联用户、宠物、医生、排班和服务项目。这张预约表是典型的多对多关联表答辩时老师最喜欢问“为什么 appointment 表有这么多外键”答案就是预约行为本身关联了五个业务主体。3.2 预约时段的前后端一致性为什么排班表要单独建排班表单独建比直接在预约表里存 doctor_id 和 time_slot 更合理。因为排班是医生维度的基础数据护士或管理员需要提前维护一周的排班预约时再按排班状态判断能不能约。如果把排班信息揉进预约表就没法回答“这个医生下周一有没有班”这种问题了。CREATE TABLE schedule ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, work_date DATE NOT NULL, time_slot VARCHAR(20) NOT NULL COMMENT 时段如 09:00-10:00, slot_status TINYINT DEFAULT 1 COMMENT 0 停诊 1 可约 2 约满, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_doctor_slot (doctor_id, work_date, time_slot) );这个建表语句的精华在唯一索引 uk_doctor_slot它从数据库层面保证同一个医生同一天同一个时段只能存在一条排班记录。注意 time_slot 用字符串存而不是用 int 枚举虽然查询效率略低但可读性高小程序端展示时段不需要再做翻译。预约状态字段 status 我建议用 TINYINT 加注释0 待确认、1 已确认、2 已完成、3 已取消。千万不要用中文直接存否则后续想统计完成率、取消率时SQL 会写得非常痛苦。如果你下载到的脚本里 status 已经在用 int保持原样就好不要中途改类型改类型容易引发前后端字段类型不一致。3.3 SQL 脚本落库的三个细节导入脚本时用命令行最省事不需要打开可视化工具mysql -u root -p petsdn.sql执行前先确认三件事。第一脚本开头有没有 CREATE DATABASE如果没有需要手动建库再指定库导入mysql -u root -p -e CREATE DATABASE IF NOT EXISTS petsdn DEFAULT CHARSET utf8mb4 mysql -u root -p petsdn petsdn.sql第二统一用 utf8mb4不要用 utf8。utf8 在 MySQL 里最多存 3 字节遇到 emoji 宠物昵称会直接报错utf8mb4 才是完整的 UTF-8。第三看初始化数据里的时间字段。如果脚本里写的是2024-05-20 10:00:00这种固定时间导入后不影响功能但如果写的是CURRENT_TIMESTAMP就不用管。真正坑的是硬编码了过去的日期作为排班数据导入后查不到可约时段小程序列表为空这个在第 5 章会专门展开。4. 跑通前后端Maven 启动、MySQL 配置与接口验证4.1 Maven Wrapper为什么我不建议你依赖本机 Maven项目里有 .mvn/wrapper 目录和 maven-wrapper.properties这意味着可以用 Maven Wrapper 跑构建。Maven Wrapper 的作用是锁定 Maven 版本让项目在哪个环境构建都用同一个 Maven。这样做的最大好处是换电脑不翻车团队协作也不存在“我本地能跑你本地不能跑”的玄学问题。构建命令很简单./mvnw clean package -DskipTestsWindows 环境用mvnw.cmd代替注意首次执行会下载 Maven 发行版耗时取决于网络。如果你本机已经装了 Maven也可以直接用 mvn 命令但版本最好和 maven-wrapper.properties 里指定的 distributionUrl 一致否则可能出现依赖解析方式不同导致 build 失败。这是个很隐蔽的坑Maven 3.6 和 3.9 在插件兼容性上有些差别。4.2 application.yml 里的三处必改参数后端跑起来前必须改 application.yml。课设源码里几乎都会残留原开发者的数据库配置不改密码你连库都连不上。下面这份配置包含了所有必改项server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/petsdn?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password_here driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.pethospital.entity configuration: map-underscore-to-camel-case: true logging: level: com.example.pethospital.mapper: debugurl 里的三组参数是血泪教训。serverTimezoneAsia/Shanghai 不加MySQL 8 会报时区错误useSSLfalse 是避免本地开发时 SSL 握手警告allowPublicKeyRetrievaltrue 是解决 MySQL 8 的 Public Key Retrieval 报错。这三个参数如果原作者没写建议自己加上能省掉后面一半的数据库连接问题。map-underscore-to-camel-case 是 MyBatis 自动驼峰映射开关。数据库字段是 appointment_noJava 实体是 appointmentNo开了这个就不用写一堆 resultMap。如果关掉它查出来的 appointmentNo 全是 null预约单列表页看起来像 Bug实则是映射没生效。4.3 启动后端后先验证这五个接口再碰页面后端启动后不要直接开小程序。先用 POSTMAN 或 curl 把核心接口过一遍确认后端没问题再联调否则前后端同时出错时你根本不知道先修哪边。按依赖顺序验证接口方法入参期望返回/api/user/loginPOSTcodeopenid 和用户信息/api/doctor/listGET无医生列表/api/schedule/listGETdoctorId, date可约时段/api/appointment/createPOST预约参数预约单号/api/appointment/listGETuserId我的预约列表curl 验证预约提交接口curl -X POST http://localhost:8080/api/appointment/create \ -H Content-Type: application/json \ -d {doctorId:1,petId:1,scheduleId:3,serviceItemId:1,appointmentDate:2024-06-01,timeSlot:09:00-10:00}返回里如果能拿到预约单号说明整条链路已经通了。这里建议把日志级别调到 debugMyBatis 会把 SQL 打到控制台你能直观看到每步操作的 SQL 语句。如果 insert 语句的字段对不上实体类一般就是驼峰映射没开回 4.2 检查配置。4.4 用 test 目录里的代码做冒烟验证src/test 目录不是摆设。课设源码里如果带了 SpringBootTest 测试类我建议先跑一遍SpringBootTest Transactional public class AppointmentServiceTest { Autowired private AppointmentService appointmentService; Test public void testCreateAppointment() { AppointmentCreateDTO dto new AppointmentCreateDTO(); dto.setDoctorId(1); dto.setPetId(1); // 填好参数后调用 Result result appointmentService.createAppointment(dto); assertNotNull(result.getData()); } }Transactional 加在测试类上意味着测试结束后事务回滚不会污染数据库。跑测试用./mvnw test或者 IDE 里的 JUnit Runner 都行。测试的意义不在于证明代码没问题而在于你能通过测试类快速理解 Service 的入参是什么、返回结构长什么样这比翻半天代码快得多。5. 联调避坑指南五个高频问题的排查记录5.1 现象后端启动正常小程序请求全是 404后端控制台没有任何报错但小程序端所有请求都返回 404。我调试了一个下午最后发现是小程序端 BASE_URL 写的是http://localhost:8080/api而后端 Controller 的 RequestMapping 已经带了 /api结果实际请求路径变成了 /api/api/appointment。原因就是路径前缀叠了两层。解决方法是统一约定BASE_URL 只写到 IP 加端口所有接口路径以 Controller 上的注解为准。从那以后我每次联调第一件事就是看请求路径有没有叠前缀。5.2 现象数据库连接报 Public Key Retrieval is not allowed启动后端时报Public Key Retrieval is not allowed这是 MySQL 8 的加密连接机制引起的。MySQL 8 默认使用 caching_sha2_password 认证客户端首次连接需要从服务器获取公钥但 JDBC 默认不允许。解决方式是在 jdbc url 上加allowPublicKeyRetrievaltrue配合useSSLfalse一起用本地开发环境完全够用。如果是连公司的测试库建议用 ssl 连接并配置服务端证书不要盲目关 SSL。5.3 现象同样的代码换一台电脑就起不来同一份源码在自己电脑上跑得好好的拷给别人就启动失败。最常见的原因有三个JDK 版本不一致、Maven 版本不一致、MySQL 版本不一致。这份课设源码如果是基于 Spring Boot 2.x 写的换到 JDK 17 虽然能编过但运行时会遇到反射相关的兼容问题。解决方式是严格按项目.md 里写的环境要求装 JDK用 Maven Wrapper 跑构建数据库统一 MySQL 8。如果都没有问题检查本地仓库是不是缺依赖删掉~/.m2/repository里对应的目录重新拉一次。5.4 现象管理员端有数据小程序列表却为空管理员后台能看到医生排班小程序端预约列表却是空的。先确认不是网络问题大概率是 MyBatis 查询结果映射失败。数据库字段 appointment_no实体类属性 appointmentNo如果没开 map-underscore-to-camel-case查询结果里这个字段就是 null前端拿不到就显示空。解决方式是在 application.yml 打开驼峰映射或者在 XML Mapper 里写完整的 resultMap。血泪经验是看到列表为空先看控制台 SQL再比对数据库字段和实体类属性不要一上来就改前端。5.5 现象预约重复提交同一时段出现两条预订用户手抖点了两次提交预约表里出现同医生同时段的两条记录。原因有二前端没有做按钮防抖后端也没有做重复性校验。先在前端加一个提交中的状态锁这一层能挡住大部分误操作后端 Service 在 insert 前先 count 一次如果已存在未取消的预约就拒绝。更稳妥的做法是给 appointment 表加唯一索引字段是 doctor_id、appointment_date、time_slot这样即使代码漏了判断数据库也能拦住。ALTER TABLE appointment ADD UNIQUE KEY uk_doctor_slot (doctor_id, appointment_date, time_slot);注意唯一索引会带来副作用用户取消了预约想重新约同一个时段时如果历史记录没做状态过滤索引会一直拦着。所以加索引之前要确保业务里“已取消”状态不参与冲突判断通常做法是查重时加AND status IN (0, 1, 2)把 3 排除掉。这一步就是这个系统的业务边界也是答辩时能加分的点。6. 从课设到毕设给预约流程补上状态机与审计日志课设阶段状态字段只是几个 int够用但不优雅。如果你想拿这套系统当毕设最值得升级的点是预约状态流转。当前 status 的取值是分散散落的每次判断状态都要写裸数字status 1一旦流程变复杂比如新增“爽约”和“待支付”代码就会失控。先把状态封装成枚举再让状态流转集中在同一个类里管理。public enum AppointmentStatus { PENDING(0, 待确认), CONFIRMED(1, 已确认), COMPLETED(2, 已完成), CANCELLED(3, 已取消); private final int code; private final String desc; AppointmentStatus(int code, String desc) { this.code code; this.desc desc; } }状态机只允许配置的迁移路径例如 PENDING 只能到 CONFIRMED 或 CANCELLEDCONFIRMED 只能到 COMPLETED 或 CANCELLED。然后在 Service 里加一个 updateStatus 方法每次修改状态前先查当前状态再校验迁移是否合法。这样即使小程序端绕过按钮直接调接口后端也能拦住非法操作。审计日志是另一个高性价比升级。新建一张 appointment_log 表字段包括 id、appointment_id、old_status、new_status、operator_id、remark、create_time。每次预约状态变更后往这张表插一条日志答辩时老师问“预约被取消后怎么追溯”你直接演示这张表比空口解释强得多。实现也不复杂在 Service 的 updateStatus 方法里做一个 insert 操作事务和状态变更绑在一起。最后再优化接口返回体。课设项目通常每个接口返回结构不一样有的返回 true有的返回 data联调时前端要写一堆兼容逻辑。统一成 Result 对象code、message、data 三件套。Controller 里异常时直接Result.error(...)成功时Result.success(data)。前端收到 code 非 0 就走错误提示逻辑清晰也利于后续扩展分页和 Token 鉴权。从那以后我每次拿到一份课设源码都强制走完整套流程先盘文件结构再导 SQL改配置起服务用 curl 验证核心接口最后才会打开小程序。这套流程帮我过滤掉至少一半的联调问题。如果你正缺一套能讲清楚后端调用链路的课设源码这份宠物医院预约系统可以拿来当底子把状态机和审计日志加上去就是一份有亮点的毕设。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

基于规则与大模型混合的Issue智能分诊助手设计与实现

基于规则与大模型混合的Issue智能分诊助手设计与实现

1. 为什么需要给 Issue 做分诊 维护过开源项目的人都有一个共同感受:真正消耗精力的往往不是写代码,而是处理源源不断涌进来的 Issue。一个新 Issue 提交上来,里面可能混杂着 Bug 报告、功能请求、使用咨询、重复提问、甚至完全跑题的灌水内容…

📅 2026/10/22 19:50:15
LiteLLM投毒事件分析与应急防护:从排查到加固

LiteLLM投毒事件分析与应急防护:从排查到加固

1. 这起投毒事件到底是怎么回事先给还没搞清楚状况的朋友补个背景。LiteLLM 是现在做 AI 网关最常用的开源项目之一,很多团队拿它做统一入口,对接 OpenAI、Anthropic、各种国产模型以及自建的本地模型服务,转发请求、管 Token 配额、做预算控…

📅 2026/10/22 19:45:14
LangChain工具定义五种方式详解:从@tool到StructuredTool的选型与避坑指南

LangChain工具定义五种方式详解:从@tool到StructuredTool的选型与避坑指南

1. 五种工具定义方式,到底在解决什么问题LangChain 里让大模型调用外部能力,绕不开 Tool 这个概念。但很多人第一次接触时会被各种写法搞晕:有tool装饰器、有StructuredTool、有Tool类、有convert_to_openai_tool、还有直接传函数给 Agent 的…

📅 2026/10/22 19:45:14
MORE NEWS

更多资讯

📰

C++抽卡模拟器

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

📰

VideoChat-Flash-Qwen2_5-7B_InternVideo2-1B_rai_1.8.0_npu_16K部署FAQ:已知限制、MIT/TPN许可说明与从1.8.0升级的完整路线图

【免费下载链接】VideoChat-Flash-Qwen2_5-7B_InternVideo2-1B_rai_1.8.0_npu_16K 项目地址: https://ai.gitcode.com/hf_mirrors/amd/VideoChat-Flash-Qwen2_5-7B_InternVideo2-1B_rai_1.8.0_npu_16K 点击查看 免费下载 VideoChat-Flash-Qwen2_5-7B_InternVideo2-…

📰

Office 下载安装超详细保姆级教程(附安装包)【2026最新】

文章目录第一部分:了解Office 2021第二部分:Office 2021安装步骤详解第三部分:总结大家好!本文将分步骤介绍Office 2021的安装方法,这是一份详细的指南,旨在帮助大家顺利完成安装。Office是常用的办公软件&…

📰

c# web发布与IIS搭建合集教程(Win11):

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

📰

【2025】RobotStudio 2024安装教程保姆级一键安装教程(附安装包)

文章目录RobotStudio下载RobotStudio 2024详细安装步骤安装前准备与解压启动RobotStudio安装程序安装向导操作自定义安装位置首次运行与许可设置RobotStudio常见问题与解决方案本教程为您带来RobotStudio 2024的保姆级一键安装指南。RobotStudio是瑞士ABB公司推出的机器人编程软…

📰

本地高精度测温与远程多区采集:嵌入式HVAC温度监测方案解析

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

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬