ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

口腔诊所预约管理系统:Java+Springboot+Vue前后端分离实战与避坑指南

口腔诊所预约管理系统:Java+Springboot+Vue前后端分离实战与避坑指南 简介这是一套面向高校学生与医疗信息化开发者的口腔牙科诊所预约管理系统源码采用Java、Springboot与Vue技术栈实现前后端分离架构可作为毕业设计、课程设计或诊所管理工具的实践参考。压缩包共393个文件约10.41MB其中83个Java源文件承载后端业务逻辑40个Vue组件与24个TypeScript脚本、22个JavaScript文件构建前端交互界面另有126张JPEG与23张PNG图片用于视觉展示辅以XML配置、SQL脚本、Markdown说明及字体资源目录划分清晰。项目围绕在线预约、资料管理与服务预约等核心场景展开后端基于Springboot完成接口与权限控制前端借助Vue组件化实现动态页面并配有代码说明与表结构文档便于理解整体设计思路与二次开发。目前已有486人学习下载适合希望掌握前后端分离实战、积累医疗管理系统开发经验的学习者参考借鉴。1. 口腔诊所排班这件事为什么用 JavaSpringbootVue 做前后端分离更省心很多牙科诊所的预约还停留在纸质登记本或者微信群接龙前台一忙就容易出现同一台牙椅被两个患者同时约上的尴尬。口腔诊所和综合医院不同它的核心资源不是床位而是牙椅和医生的时间段一颗种植牙可能要占掉两三个小时复诊又要卡在特定周期上排班逻辑比普通门诊复杂得多。基于 JavaSpringbootVue 的口腔牙科诊所预约管理系统本质上是把「医生—牙椅—时间段—患者」这四者的占用关系用数据库锁死再用前后端分离的方式让前台、医生、患者各看各的界面。这套方案适合中小型诊所自建也适合作为前后端分离项目实战的练手题材因为业务边界清晰、表结构不复杂但排班冲突、并发预约这些坑一个都不少。2. 拆解口腔预约的业务模型从牙椅占用到时间段冲突2.1 为什么口腔诊所不能照搬普通门诊的挂号模型普通门诊挂号是「医生上午/下午」这种粗粒度一个医生一上午看几十个号谁先谁后无所谓。口腔诊所不行一颗根管治疗要连续占用牙椅 60 到 90 分钟正畸复诊要精确到具体时间点而且同一个医生同一时间只能在一个牙椅旁操作。这意味着预约的最小单位不是「号」而是「牙椅 医生 起止时间」的三元组。设计表结构时如果只建一张 appointment 表存 patient_id 和 doctor_id迟早会遇到「同一个医生被约到两个牙椅」的问题。常见做法是引入 chair牙椅和 schedule排班时段两张表appointment 表通过 schedule_id 关联schedule 表里预先锁定 doctor_id 和 chair_id 的组合预约时只允许在空闲的 schedule 记录上创建 appointment。2.2 核心表结构与字段设计下面这套表结构是我在几个诊所项目里反复调整后比较稳的版本字段不多但每个都有存在的理由。-- 牙椅表诊所的物理资源 CREATE TABLE chair ( id BIGINT PRIMARY KEY AUTO_INCREMENT, chair_no VARCHAR(16) NOT NULL COMMENT 牙椅编号如 A01, room_name VARCHAR(32) COMMENT 诊室名称, status TINYINT DEFAULT 1 COMMENT 1可用 0停用 ); -- 医生排班表把医生和牙椅在某个时间段绑定 CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL, chair_id BIGINT NOT NULL, work_date DATE NOT NULL COMMENT 排班日期, start_time TIME NOT NULL, end_time TIME NOT NULL, status TINYINT DEFAULT 0 COMMENT 0空闲 1已约 2锁定, UNIQUE KEY uk_doctor_time (doctor_id, work_date, start_time), UNIQUE KEY uk_chair_time (chair_id, work_date, start_time) ); -- 预约记录表 CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, schedule_id BIGINT NOT NULL, patient_id BIGINT NOT NULL, treat_type VARCHAR(32) COMMENT 治疗类型洗牙/根管/种植, status TINYINT DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_schedule (schedule_id) );逻辑说明schedule 表上的两个唯一索引是关键uk_doctor_time 保证同一医生同一时间不会出现在两个牙椅uk_chair_time 保证同一牙椅同一时间不会被两个医生占用。appointment 表的 uk_schedule 唯一索引则从数据库层面杜绝了同一时段被重复预约。参数上start_time 和 end_time 用 TIME 类型而不是 DATETIME是因为排班是按天生成的日期维度放在 work_date 里这样查询某天某医生的空闲时段只需要一个 WHERE work_date ? AND doctor_id ? 就能命中索引。2.3 时间段粒度怎么定粒度太细比如 15 分钟一格会导致 schedule 表数据量膨胀一个医生一天 8 小时就是 32 条记录十个医生一个月接近一万条。粒度太粗比如 2 小时一格又没法适配洗牙这种短项目。我一般按治疗类型反推洗牙 30 分钟、补牙 45 分钟、根管 90 分钟、种植 180 分钟取最大公约数 30 分钟作为基础粒度生成排班时按 30 分钟一条批量插入预约时根据 treat_type 占用连续的 N 条 schedule。这样既不会数据爆炸又能灵活组合。3. Springboot 后端预约接口与并发冲突处理3.1 项目骨架与依赖选择后端用 Springboot 搭版本不要盲目追新。热搜里常有人问「springboot版本太高」怎么办我的血泪经验是如果团队里有人还在用 JDK 8就老老实实选 Springboot 2.7.x它对 MyBatis、Druid 这些老牌库的兼容性最稳。JDK 17 起步的团队可以上 3.x但要注意 3.x 把 javax 换成了 jakarta老代码迁移时包名要全局替换。依赖上核心就四个spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency参数说明mybatis-spring-boot-starter 的版本要和 Springboot 主版本匹配2.3.x 对应 Springboot 2.73.x 对应 Springboot 3.x。mysql-connector-j 是 MySQL 8 之后的官方驱动名老项目里看到的 mysql-connector-java 已经改名了别混用。3.2 预约接口的并发控制预约接口最怕的是两个人同时点「确认预约」如果只靠先查后插中间那几毫秒就会产生重复预约。常见做法有三种数据库唯一索引兜底、悲观锁 SELECT ... FOR UPDATE、乐观锁版本号。我一般用唯一索引加捕获异常的组合简单可靠。Transactional(rollbackFor Exception.class) public Result createAppointment(Long scheduleId, Long patientId, String treatType) { // 先查排班是否空闲 Schedule schedule scheduleMapper.selectById(scheduleId); if (schedule null || schedule.getStatus() ! 0) { return Result.fail(该时段已被预约); } // 更新排班状态利用数据库行锁 int updated scheduleMapper.updateStatus(scheduleId, 1); if (updated 0) { return Result.fail(手慢了该时段刚被约走); } // 插入预约记录uk_schedule 唯一索引兜底 try { Appointment appt new Appointment(); appt.setScheduleId(scheduleId); appt.setPatientId(patientId); appt.setTreatType(treatType); appointmentMapper.insert(appt); } catch (DuplicateKeyException e) { throw new BizException(重复预约); } return Result.ok(); }逻辑说明updateStatus 的 SQL 写成 UPDATE schedule SET status 1 WHERE id ? AND status 0靠 MySQL 的行锁保证只有一个事务能更新成功返回影响行数为 0 就说明被别人抢先了。参数上Transactional 的 rollbackFor 要显式写 Exception.class否则遇到 DuplicateKeyException 这种运行时异常虽然会回滚但遇到受检异常不会容易埋雷。3.3 排班查询接口与缓存前台打开预约页面时要实时看到哪些时段空闲这个查询频率很高但数据变化不快适合加一层缓存。我一般用 Spring Cache 加 Rediskey 按 doctor_id work_date 拼。Cacheable(value schedule, key #doctorId : #workDate) public ListScheduleVO listFreeSlots(Long doctorId, String workDate) { return scheduleMapper.selectFreeByDoctorAndDate(doctorId, workDate); }参数说明Cacheable 的 key 用 SpEL 表达式拼接注意 workDate 传进来是 String 类型如果传 Date 要加 #workDate.time 否则 toString 结果不稳定。缓存过期时间在配置文件里设 5 分钟因为排班变动不频繁5 分钟延迟可以接受。预约成功后要主动 evict 掉对应 key否则前台看到的还是旧数据。4. Vue 前端预约日历与动态路由的落地细节4.1 预约日历组件的选型与数据绑定前端用 Vue 3 加 Element Plus日历部分不建议自己从零写用 el-calendar 或者第三方日历组件都行。核心是把后端返回的 schedule 列表映射成日历上每一天的可用状态。// 获取某医生某月的排班按日期分组 async function loadMonthSchedule(doctorId, month) { const res await axios.get(/api/schedule/month, { params: { doctorId, month } }) // res.data 是 [{workDate: 2025-06-01, freeCount: 3}, ...] const map {} res.data.forEach(item { map[item.workDate] item.freeCount }) return map }逻辑说明后端返回的是按天聚合的空闲数量前端用对象做映射日历渲染时根据 freeCount 决定当天显示绿色有空还是灰色约满。参数上month 传 2025-06 这种格式后端用 DATE_FORMAT(work_date, %Y-%m) 匹配。注意时区问题如果服务器是 UTC 而诊所在东八区work_date 可能差一天建议数据库连接串里加 serverTimezoneAsia/Shanghai。4.2 动态路由与权限控制诊所系统里前台、医生、管理员看到的菜单不一样用 Vue 的动态路由按角色下发。登录后后端返回该角色的路由表前端用 router.addRoute 动态挂载。// 登录后根据角色加载路由 const routes await fetchUserRoutes() routes.forEach(route { router.addRoute(layout, { path: route.path, name: route.name, component: () import(/views/${route.component}.vue) }) })参数说明component 用动态 import 时路径要写死前缀Vite 对完全动态的路径解析不了常见做法是用 import.meta.glob 预扫描所有 views 下的文件再按 key 匹配。路由守卫里要判断目标路由是否已挂载否则刷新页面会白屏这是动态路由最常见的翻车点。4.3 前后端联调的跨域与打包开发阶段前端跑 5173 端口后端跑 8080跨域用 Vite 的 proxy 解决不要在后端加 CrossOrigin 到处撒。// vite.config.js export default { server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }上线时前端 npm run build 产出 dist 目录常见做法是把 dist 放进 Springboot 的 src/main/resources/static 下一起打包这样只有一个 jar 包部署简单。注意 Vue Router 要用 history 模式的话Springboot 里要加一个转发配置把所有非 /api 的 404 请求指回 index.html否则刷新子路由会 404。5. 避坑与排查口腔预约系统上线后最容易翻车的五件事5.1 现象同一时段出现两条预约记录原因只靠代码里的先查后插没有数据库唯一索引兜底并发时两个请求都查到空闲然后都插入成功。解决在 appointment 表的 schedule_id 上加 UNIQUE 索引代码里捕获 DuplicateKeyException 返回友好提示。这个后悔药一定要提前吃上线后再加索引要处理历史脏数据。5.2 现象排班查询接口越来越慢原因schedule 表按天生成几个月后数据量到几十万查询没走对索引。解决确认 uk_doctor_time 和 uk_chair_time 两个联合索引存在查询条件里 work_date 和 doctor_id 的顺序要和索引一致。如果还是慢考虑按月分表或者定期归档过期排班。5.3 现象前端日历显示的可用时段和后端不一致原因缓存没及时清除或者时区导致日期偏移。解决预约成功后主动删除对应 key 的缓存数据库连接串加 serverTimezone 参数前端传日期时统一用 YYYY-MM-DD 字符串不要传 Date 对象。5.4 现象动态路由刷新后白屏原因addRoute 是运行时添加的刷新页面后路由表还没加载完匹配不到目标路由。解决在路由守卫里判断如果目标路由不存在且用户已登录先拉取路由表再 next({ ...to, replace: true }) 重新导航。5.5 现象Springboot 打包后前端页面 404原因Vue Router 用了 history 模式Springboot 没有配置 fallback 转发。解决写一个 WebMvcConfigurer把 /api 之外的请求都转发到 index.html或者干脆用 hash 模式省事代价是 URL 里多个 #。6. 把预约系统做扎实的两个进阶技巧第一个技巧是给排班加「缓冲时间」。口腔治疗之间需要消毒牙椅、整理器械实际占用时间比治疗时间长。我一般会在 schedule 生成时在每个时段后面留 15 分钟缓冲比如 9:00-9:30 的治疗schedule 实际占 9:00-9:45。这样医生不会因为前一个患者拖堂而连环迟到。实现上就是在生成排班时把 end_time 往后延预约时按 treat_type 计算需要几个连续 slot把缓冲也算进去。第二个技巧是用定时任务做预约提醒和爽约处理。用 Spring 的 Scheduled 每天凌晨跑一次把前一天未确认的预约自动取消释放排班再给当天预约的患者发提醒。这里要注意定时任务在集群部署时会重复执行常见做法是加 Redis 分布式锁或者用数据库的乐观锁标记任务已执行。Scheduled(cron 0 0 2 * * ?) public void releaseExpiredAppointments() { // 把超过24小时未确认的预约置为取消排班状态回滚为空闲 int count appointmentMapper.cancelExpired(); scheduleMapper.releaseByExpiredAppointments(); log.info(释放过期预约 {} 条, count); }参数说明cron 表达式 0 0 2 * * ? 表示每天凌晨 2 点执行避开诊所营业时间。cancelExpired 的 SQL 里用 create_time DATE_SUB(NOW(), INTERVAL 24 HOUR) 筛选releaseByExpiredAppointments 用 UPDATE schedule s JOIN appointment a ON s.id a.schedule_id SET s.status 0 WHERE a.status 3 批量回滚。这两个操作要放在同一个事务里否则可能出现预约取消了但排班没释放的情况。我自己做这类系统最大的教训是别在业务逻辑里省数据库约束。唯一索引、外键、非空约束这些看起来「碍事」的东西恰恰是并发场景下最后的防线。代码可以改脏数据洗起来是真的头疼。希望帮到你。本文还有配套的精品资源点击获取
返回列表