☰
Java+Springboot+Vue口腔牙科诊所预约管理系统源码实战:环境搭建、排班冲突校验与二次开发
2026/10/7 16:53:30 网站建设 项目流程

简介:这份源码面向计算机相关专业的毕业设计、课程设计学生,以及需要搭建口腔牙科诊所预约系统的开发者,采用Java、Springboot与Vue实现前后端分离架构,可解决诊所线上预约、资料管理与服务预约等信息化需求。压缩包共393个文件,约10.41MB,其中83个Java源文件承载后端业务逻辑,40个Vue组件与24个TypeScript脚本、22个JavaScript文件构建前端交互界面,另有126个JPEG与23个PNG图片用于视觉展示,9个XML配置、1个SQL脚本及yml、json等文件支撑系统配置与数据存储,目录划分清晰。已有486人学习下载。项目包含表结构文档、代码说明与readme使用指南,后端涵盖控制器、拦截器与业务实现类,前端组件化程度高,便于读者理解前后端分离的完整开发流程,也可在此基础上二次开发出更贴合实际诊所需求的预约管理软件。

1. 从一份牙科诊所排班表说起:这套 Java+Springboot+Vue 源码到底能跑出什么

上周帮朋友的口腔诊所看他们排班,前台还在用一张 Excel 表手动改时间,医生临时请假就得挨个打电话通知患者。这种场景下,一套前后端分离的预约管理系统就不是"练手项目"那么简单了,它直接对应着真实诊所里"号源冲突、医生排班、患者改约"这三件最磨人的事。这份基于 Java + Springboot + Vue 的口腔牙科诊所预约管理系统源码,走的是标准的前后端分离架构:后端 Springboot 提供 REST 接口,前端 Vue 做单页应用,中间靠 JSON 通信。它适合两类人——一类是想找一个业务闭环完整、能直接改造成毕设或课程设计的开发者,另一类是诊所里懂点技术、想自己搭一套内部预约工具的负责人。整套代码覆盖了患者端预约、医生排班、号源管理、后台维护这几条主线,不是那种只有登录注册的"半截子"项目。下面我按"能跑起来 → 能改得动 → 不踩坑"的顺序,把这份源码拆开讲。

2. 环境搭建与前后端分离骨架:从 JDK 到 npm run serve 的完整链路

拿到一份前后端分离源码,第一件翻车的事往往不是代码写错,而是环境版本对不上。Springboot 对 JDK 版本敏感,Vue 对 Node 版本敏感,这两个卡住,后面全白搭。这一章先把运行环境钉死,再把前后端两个工程的启动链路走通。

2.1 后端 Springboot 工程的依赖与启动配置

这份源码的后端是典型的 Springboot + MyBatis 结构,数据库用 MySQL。我一般会先确认三件事:JDK 版本、Maven 依赖能不能拉下来、数据库连接串改没改。Springboot 2.x 配 JDK 8 最稳,如果你本地装了 JDK 17,启动时容易在反射相关的地方报错,这是血泪经验。

先看pom.xml里几个关键依赖,确认版本区间:

<!-- pom.xml 关键依赖片段 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.6</version> <!-- 2.7.x 对 JDK8 友好,别盲目上 3.x --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>

这段依赖的逻辑很直白:spring-boot-starter-web负责把内嵌 Tomcat 和 SpringMVC 拉进来,mybatis-spring-boot-starter负责 ORM 映射,MySQL 驱动只在运行时需要。参数上唯一要盯的是 parent 的版本号——2.7.x 是 JDK8 生态里兼容性最好的一个区间,如果你看到源码里写的是 3.x,那基本要求 JDK17,别硬降。

数据库配置在application.yml里,改这几行:

spring: datasource: url: jdbc:mysql://localhost:3306/dental_clinic?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.clinic.entity server: port: 8080

serverTimezone=Asia/Shanghai这个参数别省,牙科预约系统里全是时间字段,时区不对会导致预约时间整体偏移 8 小时,患者看到的号和实际存的号对不上,这种玄学问题排查起来能耗一下午。mapper-locations指向 XML 映射文件的位置,如果启动报Invalid bound statement,八成是这里路径写错了。

启动命令就一句:

mvn spring-boot:run # 或者先打包再跑 mvn clean package -DskipTests java -jar target/clinic-0.0.1-SNAPSHOT.jar

看到控制台打印出 Tomcat started on port 8080 就算后端通了。这一步失败最常见的原因是数据库没建、表结构没导入,源码里一般会带一个sql目录,先把它执行了再启动。

2.2 前端 Vue 工程的依赖安装与接口代理

前端是 Vue CLI 或 Vite 搭的工程,先看package.json里的 scripts 和依赖版本。Vue 2 和 Vue 3 的写法差异很大,这份源码如果用的是 Vue 2 + Element UI,那 Node 版本别超过 16,Node 18 以上装依赖经常报ERR_OSSL_EVP_UNSUPPORTED,这是新手最容易卡住的地方。

# 进入前端目录 cd frontend # 安装依赖,Node 16 环境下最稳 npm install # 如果 Node 版本高报错,加这个环境变量临时绕过 export NODE_OPTIONS=--openssl-legacy-provider # 启动开发服务器 npm run serve

npm install卡住或报错,先换国内镜像源,再删掉node_modules和package-lock.json重来,别在残留依赖上反复折腾。启动成功后默认跑在 8081 或 8082 端口。

前后端分离的关键在接口代理。前端直接请求localhost:8080会跨域,所以要在vue.config.js里配 proxy:

// vue.config.js module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, pathRewrite: { '^/api': '' } // 去掉前缀再转发 } } } }

changeOrigin: true是为了让后端收到的 Host 头正确,pathRewrite决定前端/api/xxx转发到后端时是否保留/api前缀。这两个参数配错,表现就是前端页面能打开但所有数据都是空的,控制台一堆 404。配好之后,前端调/api/appointment/list,实际打到后端就是/appointment/list。

前后端都起来后,用浏览器打开前端地址,能登录、能看到预约列表,这条链路就算通了。接下来才是真正要动脑子的业务部分。

3. 预约与排班核心业务:号源生成、时间冲突校验和状态流转

环境通了不代表系统能用,牙科预约系统的核心难点全在业务逻辑上:号源怎么生成、两个患者抢同一个时段怎么拦、预约状态怎么流转。这一章把这几块拆开,讲清楚源码里对应的实现思路和你要改的地方。

3.1 号源生成与医生排班的数据模型

牙科诊所和普通门诊不一样,一个医生一天能看的号是有限的,而且不同项目(洗牙、补牙、种植)耗时不同。源码里一般用三张表撑起这块:医生表、排班表、号源表。

表名关键字段作用
doctorid, name, title, department医生基本信息
scheduleid, doctor_id, work_date, time_slot, max_num医生某天某时段的排班
appointmentid, patient_id, schedule_id, status, create_time患者预约记录

排班表里的time_slot通常存"上午/下午"或具体时间段,max_num是这个时段最大接诊数。号源不是提前把每个号都插一条记录,而是用"排班容量 - 已预约数"实时算出来的,这样医生临时加号或减号都好处理。

生成排班的逻辑一般放在后台管理端,核心是批量插入:

// ScheduleServiceImpl.java 批量生成排班 public void batchCreateSchedule(Long doctorId, LocalDate startDate, LocalDate endDate, int maxNum) { List<Schedule> list = new ArrayList<>(); for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) { // 跳过周末,牙科诊所一般周末也营业,这里按实际业务改 if (date.getDayOfWeek() == DayOfWeek.SUNDAY) continue; for (String slot : Arrays.asList("上午", "下午")) { Schedule s = new Schedule(); s.setDoctorId(doctorId); s.setWorkDate(date); s.setTimeSlot(slot); s.setMaxNum(maxNum); list.add(s); } } scheduleMapper.batchInsert(list); // 一条 SQL 批量插入,别循环单条插 }

这段逻辑的参数说明:startDate和endDate控制排班区间,maxNum是每个时段的最大号数。batchInsert一定要用批量插入,循环单条 insert 在生成一个月排班时会慢到让你怀疑人生。跳过周末那行按你实际诊所的营业时间改,很多口腔诊所周日是开的。

3.2 预约时间冲突校验与并发抢号处理

这是整个系统最容易出问题的地方。两个患者同时点"预约",如果不做校验,就会超卖。源码里通常有两层防护:应用层查重 + 数据库唯一约束。

应用层的校验逻辑:

// AppointmentServiceImpl.java 预约前的冲突校验 public Result createAppointment(Long patientId, Long scheduleId) { Schedule schedule = scheduleMapper.selectById(scheduleId); // 1. 查该排班已预约数量 int booked = appointmentMapper.countByScheduleId(scheduleId); if (booked >= schedule.getMaxNum()) { return Result.fail("该时段号源已满"); } // 2. 查该患者是否在同一时段已有预约,防止重复约 int dup = appointmentMapper.countByPatientAndSchedule(patientId, scheduleId); if (dup > 0) { return Result.fail("您已预约该时段,请勿重复提交"); } // 3. 插入预约记录 Appointment appt = new Appointment(); appt.setPatientId(patientId); appt.setScheduleId(scheduleId); appt.setStatus(0); // 0=待就诊 appt.setCreateTime(new Date()); appointmentMapper.insert(appt); return Result.success("预约成功"); }

逻辑上分三步:先判断号源是否已满,再判断患者是否重复预约,最后落库。但这段代码在并发下是有漏洞的——两个线程同时查到booked < maxNum,然后都插入,就超卖了。常见做法是在appointment表上加一个(schedule_id, patient_id)的唯一索引,同时在 service 方法上加@Transactional和行级锁,或者用 Redis 做分布式锁。如果只是单机部署,给查询排班那行加SELECT ... FOR UPDATE也能挡住大部分并发。

状态流转这块,预约记录一般有四个状态:待就诊、已就诊、已取消、爽约。改状态要走专门的接口,别让前端直接改数据库字段:

// 状态流转:只有待就诊状态才能取消 public Result cancelAppointment(Long appointmentId) { Appointment appt = appointmentMapper.selectById(appointmentId); if (appt.getStatus() != 0) { return Result.fail("当前状态不可取消"); } appt.setStatus(2); // 2=已取消 appointmentMapper.updateById(appt); return Result.success("已取消"); }

参数上要盯的是状态码的定义,源码里如果用的是魔法数字(0、1、2、3),建议你改成枚举,不然改到后面自己都记不清哪个数字对应哪个状态。取消后号源要不要释放?如果号源是实时算的,取消后countByScheduleId自然就少了,不用额外操作;如果是预扣减模式,就得手动加回去,这点要看源码用的是哪种方案。

4. 避坑与常见问题排查:从跨域到时间字段的五个真实翻车点

这份源码跑起来不难,难的是跑通之后各种"看起来对但就是不对"的问题。下面五条是我在类似项目里反复踩过的,按现象、原因、解决三段写清楚。

4.1 前端请求全部 404,但后端接口单独测是通的

现象:浏览器控制台一堆 404,但用 Postman 直接打后端接口能返回数据。原因:vue.config.js里的 proxy 没配,或者pathRewrite把该保留的前缀去掉了。解决:确认前端请求路径带/api,proxy 的 target 指向后端端口,pathRewrite按后端实际路由决定是否去掉前缀。改完必须重启npm run serve,热更新不会重载 proxy 配置。

4.2 预约时间存进去差 8 小时

现象:前端选的是下午 3 点,数据库里存的是早上 7 点。原因:JDBC 连接串没配serverTimezone,或者实体类用的是java.util.Date而数据库是datetime,中间转换丢了时区。解决:连接串加serverTimezone=Asia/Shanghai,实体类时间字段统一用LocalDateTime,MyBatis 映射时确认jdbcType是TIMESTAMP。

4.3 并发下同一个号被约了两次

现象:压测或手动快速点击时,一个号源出现两条预约记录。原因:应用层校验和插入之间有时间窗口,没有原子性。解决:给appointment表加(schedule_id, patient_id)唯一索引兜底,service 方法加@Transactional,高并发场景引入 Redis 锁或数据库悲观锁。唯一索引是最后一道防线,别省。

4.4 npm install 报 ERR_OSSL_EVP_UNSUPPORTED

现象:Node 18 以上装依赖或启动时报 OpenSSL 相关错误。原因:新版 Node 的 OpenSSL 3.0 和旧版 webpack 不兼容。解决:临时方案是export NODE_OPTIONS=--openssl-legacy-provider,根治方案是用 nvm 把 Node 降到 16。别去改 webpack 配置,改到最后依赖树全乱。

4.5 后台改了医生排班,前端号源没更新

现象:管理端删了一个排班,患者端还能看到并预约。原因:前端缓存了排班列表,或者号源计算依赖的字段没跟着变。解决:确认前端是否用了 keep-alive 缓存页面,改完排班后强制刷新;号源如果是实时算的,检查countByScheduleId是否把已取消的预约也算进去了,已取消的应该排除。

5. 二次开发与验证:把通用预约系统改造成真正的口腔诊所工具

源码能跑、坑也避了,接下来是让它真正贴合口腔诊所的业务。通用预约系统最大的问题是把"看牙"当成"看感冒"处理,忽略了牙科特有的复诊、疗程、项目时长这几个维度。这一章讲怎么改,以及改完怎么验证没改坏。

5.1 增加诊疗项目与疗程关联

牙科的核心业务是"项目 + 疗程"。补牙可能一次搞定,种植牙要分好几次复诊。建议在原有表结构上加两张表:treatment_item(诊疗项目,含预计时长、价格)和treatment_plan(疗程计划,关联患者和项目,记录第几次复诊)。

-- 新增诊疗项目表 CREATE TABLE treatment_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, -- 项目名,如"种植牙一期" duration INT NOT NULL, -- 预计耗时(分钟) price DECIMAL(10,2), -- 参考价格 need_revisit TINYINT DEFAULT 0 -- 是否需要复诊 ); -- 疗程计划表,把预约和疗程关联起来 CREATE TABLE treatment_plan ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL, item_id BIGINT NOT NULL, current_stage INT DEFAULT 1, -- 当前第几阶段 total_stage INT DEFAULT 1, -- 总阶段数 next_appointment_id BIGINT -- 下次预约记录ID );

加完之后,预约接口要能接收itemId,根据项目时长去匹配排班时段。比如种植牙一期要 90 分钟,就不能塞进 30 分钟的普通号里。这一步改完,系统才从"通用预约"变成"牙科预约"。

5.2 验证改造是否成功的三个检查点

改完别急着交付,走一遍这三个检查点:

检查点验证方法通过标准
号源容量把某时段 maxNum 设为 2,用两个账号同时约第二个提示号满,数据库只有两条记录
时间一致性前端选 15:00,查数据库 create_time 和 work_date时间不偏移,时区正确
状态流转预约→取消→再约同一时段取消后号源释放,能重新约上

这三个点过了,基本功能就稳了。进阶一点,可以加一个定时任务,每天凌晨把前一天"待就诊但没来"的记录自动标记为爽约,释放号源给当天现场挂号的患者。这个逻辑用 Springboot 的@Scheduled就能实现,cron 表达式写0 0 1 * * ?,凌晨 1 点跑。

从那以后我每次拿到一份前后端分离的预约类源码,都强制先走一遍"环境版本 → 数据库时区 → 并发校验"这三步,再谈业务改造。顺序反了,后面全是返工。希望这份拆解能帮你少走点弯路,把这份源码真正用起来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询