ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot + Vue 口腔诊所预约系统实战:前后端分离与号源并发控制

Spring Boot + Vue 口腔诊所预约系统实战:前后端分离与号源并发控制 简介这是一套面向高校学生与医疗信息化开发者的口腔牙科诊所预约管理系统源码采用Java、Springboot与Vue技术栈实现前后端分离架构可作为毕业设计、课程设计或诊所管理工具的参考方案。压缩包共393个文件约10.41MB其中83个Java源文件承载后端业务逻辑40个Vue组件与24个TypeScript脚本构建前端交互界面另有126张JPEG与23张PNG图片用于视觉展示辅以XML配置、SQL脚本及Markdown说明文档目录结构清晰便于按模块检索。资源内含表结构文档与代码说明覆盖在线预约、资料管理、服务预约等核心功能前后端分离设计让维护与二次开发更加灵活。目前已有486人学习下载适合希望深入理解Springboot与Vue在实际项目中应用、并快速搭建医疗管理系统的开发者参考借鉴。1. 口腔诊所预约系统为什么值得用 Spring Boot Vue 重做一遍很多牙科诊所还在用电话登记加 Excel 排班前台一忙就漏约、重约医生临时停诊也没法批量通知患者到店才发现时间对不上。这类场景的需求其实非常集中号源按医生和时段切分、预约状态要能回滚、患者档案要跟就诊记录挂钩。用 Java Spring Boot 做后端、Vue 做前端的前后端分离方案正好能把这几件事拆开后端管号源并发和状态机前端管日历选号和表单校验。它适合两类人一类是想拿一个真实业务练前后端分离项目实战的开发者一类是诊所里懂点技术、想自己搭一套内部系统的负责人。下面按「先定边界、再搭骨架、最后填业务」的顺序讲清楚怎么落地。2. 需求拆解与前后端分离的边界怎么划2.1 先锁定四类核心实体别一上来就画全表牙科预约系统的实体比通用预约系统多一层「牙位」概念但第一版不要把它做进主流程否则表结构会迅速膨胀。我一般先锁四个核心实体科室口腔内科、正畸、种植、医生绑定科室和可约时段、号源医生 日期 时段 剩余量、预约单患者 号源 状态。患者档案单独一张表用手机号做业务唯一键因为很多患者是替家人约的一个手机号可能对应多个就诊人。状态机是这类系统的命门。预约单至少要有PENDING待确认、CONFIRMED已确认、CANCELLED已取消、FINISHED已完成、NO_SHOW爽约五个状态。状态流转只能走固定路径比如PENDING → CONFIRMED → FINISHED取消只能从PENDING或CONFIRMED进入。把这条规则写进后端 Service 层前端只负责展示不要在前端做状态判断否则两个端会打架。2.2 前后端分离的接口契约先定再写代码前后端分离最容易翻车的地方不是技术是接口字段对不上。我的习惯是先写一份接口约定用表格固定下来前后端各自照着写联调时只对字段不对逻辑。接口方法路径关键参数返回号源列表GET/api/slotsdoctorId, date时段数组含 remaining创建预约POST/api/appointmentsslotId, patientId预约单号 状态取消预约PUT/api/appointments/{id}/cancel无新状态医生排班GET/api/doctors/{id}/schedulestartDate, endDate排班日历路径统一加/api前缀方便后面用 Nginx 或 Spring Boot 静态资源转发做同源部署。返回体统一包一层{ code, message, data }前端 axios 拦截器里统一处理code ! 200的情况业务代码里就不用每个请求都写 try-catch。2.3 号源并发扣减的两种做法与选型号源扣减是唯一有并发风险的点。常见做法有两种数据库乐观锁和 Redis 预扣。小诊所日预约量几十到几百直接用数据库乐观锁就够了别为了「高并发」提前上 Redis运维成本不划算。乐观锁的做法是在号源表加version字段扣减时带上版本号UPDATE slot SET remaining remaining - 1, version version 1 WHERE id #{slotId} AND remaining 0 AND version #{version};执行后判断影响行数为 0 就说明被别人抢先了返回「该时段已约满请换一个」。这个方案的好处是没有额外中间件事务边界清晰。缺点是并发高时失败率上升但诊所场景完全够用。如果后面真要做连锁机构多门店共享号源再考虑把扣减挪到 Redis用 Lua 脚本保证原子性。3. 后端骨架Spring Boot 分层与关键配置3.1 项目结构与依赖选择后端用 Spring Boot 3.x MyBatis-Plus MySQL 8这是目前最稳的组合。注意 Spring Boot 3 要求 JDK 17 起步如果你本地还是 JDK 8要么升 JDK要么把 Spring Boot 降到 2.7.x别硬混。pom.xml里核心依赖就这几个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency分层按controller → service → mapper → entity走DTO 和 entity 分开。很多人图省事直接用 entity 接请求参数结果前端多传一个字段就把数据库字段覆盖了这是血泪经验。预约创建接口的入参用AppointmentCreateDTO只暴露slotId和patientId其余字段后端自己填。3.2 预约创建的完整 Service 逻辑创建预约要在一个事务里做三件事扣号源、写预约单、写状态流水。状态流水表很多人会省掉但出问题时它是唯一的后悔药能查清楚谁在什么时候改了什么状态。Transactional(rollbackFor Exception.class) public AppointmentVO create(AppointmentCreateDTO dto) { // 1. 查号源带版本号 Slot slot slotMapper.selectById(dto.getSlotId()); if (slot null || slot.getRemaining() 0) { throw new BizException(该时段已约满); } // 2. 乐观锁扣减 int affected slotMapper.decrease(slot.getId(), slot.getVersion()); if (affected 0) { throw new BizException(手慢了请重新选择时段); } // 3. 写预约单 Appointment appt new Appointment(); appt.setSlotId(slot.getId()); appt.setPatientId(dto.getPatientId()); appt.setStatus(AppointmentStatus.PENDING.name()); appt.setCreateTime(LocalDateTime.now()); appointmentMapper.insert(appt); // 4. 写状态流水 statusLogMapper.insert(new StatusLog(appt.getId(), null, PENDING)); return toVO(appt); }Transactional的rollbackFor一定要写Exception.class默认只回滚运行时异常业务里抛的自定义异常如果是受检异常就不会回滚号源扣了但预约没写成这种 bug 很难查。decrease方法对应前面那条带 version 的 UPDATE 语句返回影响行数。3.3 跨域与静态资源转发的配置取舍开发阶段前端跑在 5173 端口后端跑 8080必然跨域。两种解法后端加 CORS 配置或者前端配代理。我倾向开发用前端代理、生产用同源部署。前端vite.config.js里配server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }生产环境把 Vue 打包产物放进 Spring Boot 的src/main/resources/static目录或者用 Nginx 托管静态文件、把/api反代到后端。前者简单适合单机部署后者灵活适合前后端分开扩容。注意 Vue 路由用 history 模式时Spring Boot 要配一个 fallback把所有非/api的 404 转发到index.html否则刷新页面会白屏。4. 前端骨架Vue 选号日历与状态联动4.1 选号日历的组件拆分前端核心就一个页面选医生 → 选日期 → 选时段 → 填患者信息 → 提交。用 Vue 3 组合式 API 写拆成三个组件DoctorPicker、SlotCalendar、AppointmentForm。父组件持有doctorId和selectedSlot两个响应式状态子组件通过 emit 往上抛事件不要用 provide/inject 传业务状态层级一深就乱。SlotCalendar接收一个时段数组每个时段对象形如{ id, startTime, endTime, remaining }。渲染时按remaining决定样式大于 0 可点等于 0 置灰并显示「已满」。这里有个细节时段要按开始时间排序后端返回的顺序不一定可靠前端拿到后先sort一遍。4.2 提交预约的防重复点击处理用户点提交按钮时网络慢的情况下会连点好几次后端虽然有乐观锁兜底但前端也该拦一道。用一个submitting标志位控制按钮禁用const submitting ref(false); async function submit() { if (submitting.value) return; submitting.value true; try { const res await createAppointment({ slotId: selectedSlot.value.id, patientId: patient.value.id }); if (res.code 200) { router.push(/appointment/${res.data.id}); } else { showToast(res.message); } } finally { submitting.value false; } }finally里重置标志位很关键否则请求失败后按钮永远点不了。另外提交成功后不要只弹个 toast 就完事要跳转到预约详情页让用户看到单号和状态减少「我到底约上没有」的焦虑。4.3 状态展示与轮询刷新预约详情页要展示当前状态并且允许在可取消的状态下取消。状态文案用映射表别把英文枚举直接显示给患者看const STATUS_TEXT { PENDING: 待确认, CONFIRMED: 已确认, CANCELLED: 已取消, FINISHED: 已完成, NO_SHOW: 已爽约 };如果诊所后台会改状态比如医生确认预约前端可以用轮询刷新30 秒拉一次详情接口。别上 WebSocket诊所场景没必要轮询实现简单、排查容易。轮询要在组件卸载时清掉定时器否则切页面后还在后台跑时间长了内存泄漏。5. 避坑与排查那些联调和上线才会暴露的问题5.1 时间字段前后端差 8 小时现象前端选的 9 点存到数据库变成 1 点。原因Java 的LocalDateTime序列化默认不带时区前端new Date()又按本地时区解析。解决在application.yml里配spring.jackson.time-zone: GMT8和date-format前端提交时统一用yyyy-MM-dd HH:mm:ss字符串不要传时间戳。这个坑几乎每个项目都会踩一次。5.2 号源扣了但预约没生成现象号源剩余量少了但预约列表里查不到。原因Transactional没生效常见于同类内部方法调用比如在 Service 的 A 方法里直接调this.create()代理没拦截到。解决把事务方法抽到单独的 Bean 里或者通过注入自身代理调用。排查时看日志里有没有两条独立的 SQL 提交记录。5.3 Vue 打包后刷新 404现象开发环境正常打包放进 Spring Boot 后访问/appointment/123刷新白屏。原因history 模式下浏览器直接请求这个路径后端没有对应映射。解决加一个WebMvcConfigurer把非/api开头的 404 请求转发到index.html。或者干脆用 hash 模式代价是 URL 里多个#。5.4 医生停诊后已约患者没通知现象医生临时停诊号源下架了但已经约上的患者不知道。原因下架号源时只改了号源状态没联动处理已有预约。解决下架号源时遍历该号源下的PENDING和CONFIRMED预约批量置为CANCELLED并记录原因同时触发通知短信或站内信。这个逻辑要放在同一个事务里别拆开。5.5 分页查询慢现象预约列表数据量到几万条后翻页越来越慢。原因LIMIT偏移量大时 MySQL 要扫描前面所有行。解决用游标分页前端传上一页最后一条的id后端用WHERE id #{lastId} ORDER BY id DESC LIMIT 20。诊所场景数据量不大但养成习惯没坏处。6. 上线前值得做的三件小事第一件是给号源加一个定时任务每天凌晨把过期未就诊的预约置为NO_SHOW把对应号源释放回池子。用 Spring 的Scheduled就够别引入 Quartz。第二件是加一个简单的操作日志记录谁在什么时候改了预约状态出纠纷时能查。第三件是压测一下创建预约接口用 JMeter 开 50 个线程打同一个号源确认乐观锁能正确拦住超卖返回的失败提示对用户友好。我自己做这类系统最大的教训是别在需求没锁死之前动手写代码。第一版我花了三天做牙位图结果诊所根本不用他们要的就是「选医生、选时间、填名字」。后来我把牙位砍掉两天就把主流程跑通了。先把最短路径跑通再按真实反馈加功能这个习惯帮我省了很多返工。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进