首页 / 资讯中心 / 文章详情

Java+SpringBoot医疗预约挂号系统实战:号源模型与防超卖核心设计

Java+SpringBoot医疗预约挂号系统实战:号源模型与防超卖核心设计 ★ FEATURED ARTICLE
我这两年带过不少学生做毕业设计也面试过不少拿着类似项目来找工作的应届生。医疗预约挂号系统确实是个出镜率极高的题目同时也是个典型的“听着简单、做起来全是细节”的业务系统。它不像电商那样拼并发也不像管理系统那样拼CRUD核心难点在于那个“号源模型”——怎么把一个医生的出诊时间切成可预约的号怎么防止两个患者同时抢到同一个号怎么处理停诊、退号、爽约这些医疗特有的业务状态。这篇博文就围绕这套JavaSpringBoot的医疗预约挂号平台把项目从需求拆解、技术选型、数据库设计到核心模块的落地实现完整走一遍。不管是准备毕业设计答辩还是想用SpringBoot做个能写进简历的Web项目这篇都能给你一些能直接抄作业的东西。1. 项目定位与需求拆解先搞清楚要解决什么问题1.1 这个系统到底在解决什么很多同学拿到这个题目第一反应是“不就是管理员维护医生信息、用户能挂号嘛”结果做出来的东西文档里写的是预约挂号平台演示起来就是个增删改查后台。这是毕设最容易翻车的地方没有把业务问题拆透。真实场景里医院挂号难的本质问题有三个号源信息不透明、排班靠人工、爽约率高。患者不知道哪个医生哪天还有号必须跑到医院窗口问医生排班改了挂号处和患者可能都不知道挂了号不来的人多医生时间被白白浪费。所以这个系统要解决的是在这三件事上给出数字化方案第一让患者能在Web端实时查看科室、医生、排班和剩余号源在线完成预约第二让医院管理员可以维护科室医生信息、配置每周排班、设置号源数量、处理停诊第三让医生能查看自己的排班和预约患者记录并做简单的出诊确认或停诊操作。这三条主线对应了患者端、管理员端、医生端三种角色整个系统的功能边界就清晰了。1.2 用户角色与核心流程我一般建议学生把角色拆成三类权限划分直接融入功能设计患者注册登录、浏览科室与医生、查看排班、在线预约、我的预约管理取消预约、查看历史记录医生查看个人排班、查看某天的预约患者列表、标记出诊状态、维护个人简介与擅长领域管理员科室管理、医生账号管理、排班管理按周生成班次、号源数量配置、停诊/放号操作、系统数据统计核心流程是这条链路患者选择科室 → 查看该科室下的医生列表 → 进入医生详情查看7天排班 → 选择某个时间段点击预约 → 系统校验是否还有剩余号源 → 生成预约订单 → 患者可在个人中心查看和取消。这条链路里最容易出问题的环节就是“选择时间段”这一步的并发冲突后面我会专门讲。这里要提醒一点不要把功能做得太泛。比如有的同学想加在线支付、药品配送、病历电子化对毕业设计来说这完全是往自己身上加工作量。预约挂号的边界就是“号源管理 预约流程 基础档案”把这条线打通做扎实答辩时把并发预约和排班设计讲清楚就已经是一个完成度很高的项目了。1.3 为什么说它适合做毕业设计从技术训练角度这个项目的业务链路足够完整涉及SpringBoot的配置体系、MyBatis-Plus的持久层操作、关系型数据库的表设计与事务处理还牵涉前后端分离落地。从实操角度它有一个非常明确的业务重难点——号源扣减这需要你真正去理解数据库锁和事务隔离级别而不是背概念。从答辩角度评委老师大概率会问“两个用户同时抢最后一个号怎么办”而这个问题如果你真的做过、思考过会答得非常扎实。2. 技术选型与架构设计版本搭配是第一个大坑2.1 后端技术怎么选SpringBoot版本别跟风后端选型基本没什么悬念JDK SpringBoot MyBatis-Plus MySQL。但这里有个真实的坑我必须提前说。现在很多教程上来就让你用SpringBoot 3.x但SpringBoot 3.x对JDK版本要求是17以上而且它的starter命名和部分配置项和2.x差异很大。如果你本机装的是JDK 8很多学校实验室和老教程都是8直接建3.x项目会发现编译都过不去。我的建议是不是非要用新版就选SpringBoot 2.7.x JDK 8这套组合有海量的资料、问题排查记录MyBatis-Plus兼容性也最稳放服务器上内存占用还小。等你把业务跑通了想升级再升级不要在起步阶段跟版本较劲。依赖引入的时候注意几个核心点spring-boot-starter-web提供Web能力内嵌Tomcatmybatis-plus-boot-starter持久层框架注意要用适配当前SpringBoot版本的mysql-connector-j或mysql-connector-javaMySQL驱动lombok简化实体类代码记得在IDEA装好Lombok插件spring-boot-starter-validation参数校验这个很重要预约接口不能靠手写if去判断参数2.2 前端与打包方式Vue打包放进SpringBoot前端我推荐用Vue 2 Element UI不要上Vue 3 TypeScript Vite那套一是对毕设来说复杂度没必要二是Vue 2 Element UI在国内教程存量最大出了问题搜得到答案。很多学生纠结前后端要不要分离部署。现实情况是你答辩演示的机器可能没有Node环境也可能没条件跑两个服务。最省心的方案是前端开发时用Vite或Webpack的devServer做联调联调完成后执行npm run build把生成的dist目录下的所有文件复制到SpringBoot的src/main/resources/static目录里。这样SpringBoot启动后访问localhost:8080就能直接打开页面静态资源和后端接口在同一个端口下同源策略的问题也顺带解决了。这个方案有两点要注意。一是前端里所有请求路径不要用绝对路径统一用相对路径比如/api/user/login这样打包后才不依赖ip和端口配置。二是Vue路由如果用history模式刷新页面会出现404解决办法有两个后端写一个简单的转发Controller或者把路由改成hash模式。我建议直接用hash模式省事。2.3 项目分层别把代码全塞Controller项目分层直接决定了代码能不能看懂、能不能答辩时讲清楚也决定了面试官第一眼对你的印象。我建议按经典四层结构来com.example.hospital ├── controller // 接受请求参数校验返回结果 ├── service // 业务逻辑层事务边界在这里 ├── mapper // MyBatis-Plus的数据访问层 ├── entity // 数据库实体映射 ├── dto // 前端传参的封装对象 ├── vo // 返回给前端的数据视图对象 ├── config // 配置类跨域、MyBatis-Plus分页等 └── common // 统一返回值、异常处理、工具类为什么一定要分Controller、Service、Mapper因为预约扣减号源这种操作一定是跨了好几个表的业务动作校验排班状态、扣减剩余号数、生成订单记录、可能还要记录日志。这些必须放在Service层的方法里并且加上事务注解如果散写在Controller里事务没法控制出了问题难以维护答辩证这段代码也说不清楚。说白了分层不是炫技是给后续的维护和排查省时间。2.4 最佳实践登录鉴权 Session 而非 JWT前后端分离项目会有一个惯性思维觉得不用JWT就不是现代项目。但对这个系统来说我推荐用Session 拦截器的方式做登录态原因很实际项目规模小、同一时间在线用户数量级低、单机部署Session方案足够用还简单不存在分布式会话问题答辩时面试官如果问“为什么不选JWT”答“单体应用用Session更简单JWT适合无状态分布式场景这里没有这个需求”比硬上一个JWT然后解释半天它的缺点要好得多。前端登录成功后后端request.getSession().setAttribute(userId, userId)再写一个HandlerInterceptor在预检请求和没有登录态的请求上做放行与拦截判断。被拦截的请求前端配合响应拦截器统一跳到登录页整个用户体系就转起来了。3. 数据库设计医疗业务的核心难点全在这张ER图里3.1 核心表结构拆解我见过太多人数据库就建了三张表用户表、医生表、预约表。这远远不够。真正支撑起预约流程的至少需要这几张核心表表名核心字段作用sys_userid, username, password, real_name, phone, role, status统一用户表role区分患者/医生/管理员deptid, dept_name, dept_desc科室信息doctorid, user_id, dept_id, title, specialty, intro, avatar医生档案关联用户表和科室表scheduleid, doctor_id, dept_id, work_date, period_type, start_time, end_time, total_count, remain_count, status排班表决定某医生某天有哪些时段可约appointmentid, schedule_id, user_id, doctor_id, dept_id, appoint_date, appoint_time, status, create_time预约订单表记录一次预约appointment_logid, appointment_id, operate_type, remark, create_time操作日志方便排查问题自己对照一下如果两张表就想覆盖所有业务那一定是把排班信息硬塞进了预约表里或者没有拆分操作日志。这些字段不是随便定的比如schedule里total_count和remain_count是号源控制的关键period_type用来区分上午还是下午appointment里的status至少要有待就诊、已完成、已取消、已爽约这几种不然整个业务状态流转演示不完整。3.2 排班与号源设计两个最容易被想简单的点排班是医院系统的灵魂。我建议这样设计排班管理员为医生初始化一周的排班模板比如“周二上午限号30个、周四下午限号20个”系统为每一天生成对应的schedule记录。患者的挂号流程本质上是去选择一条schedule记录然后检查remain_count 0扣减它生成appointment记录。这里有三个设计细节你要想清楚时段切分不用做太细。把一天切成上午、下午两个时间段就够了硬要切成9:00、9:30这种粒度会让排班配置界面复杂到没法演示。号源数量的默认规则可以设定普通医生每天上午30个、下午20个专家号自动减半具体数值可配置不要在代码里写死。停诊逻辑停诊不是删除排班而是把schedule的status置为“已停诊”。同时要把已经预约的患者状态批量设置为“已取消”并记录日志。这个操作在后台管理里要做得显然答辩时这是个加分点。3.3 建表的几个硬性要求必须强调一下MyBatis-Plus要求实体类和表字段有映射关系所以表结构设计决定了你写实体类能有多省事。我的建议主键统一用id并配置自增所有表必须有create_time和update_time用MyBatis-Plus的自动填充逻辑删除字段deleted默认0别物理删除数据答辩时可以讲“数据可追溯”建议加上versionint字段乐观锁防超卖要用后面细说另外表字段命名不要用驼峰统一用下划线。MyBatis-Plus默认开启驼峰自动映射但你建表时规范一点后面写SQL心里舒服很多。4. 核心模块实现与踩坑记录串起整个业务流程的代码骨架4.1 预约下单核心逻辑事务、锁与防超卖这是整个系统含金量最高的地方也是答辩必问点。先看问题场景一个医生上午的号还剩最后1个患者A和患者B同时点了预约如果两个请求都先查remain_count 1发现大于0然后各自执行remain_count - 1那就会出现超卖——两个人都预约成功数据库里剩余号数变成-1。解决思路我从简单到复杂给你排三个方案方案一乐观锁推荐在schedule表加version字段更新号源采用UPDATE语句带上版本号作为条件通过影响行数判断是否更新成功Update(UPDATE schedule SET remain_count remain_count - 1, version version 1 WHERE id #{scheduleId} AND remain_count 0 AND version #{version}) int deductStock(Param(scheduleId) Long scheduleId, Param(version) Integer version);Service层的预约逻辑写法是Override Transactional(rollbackFor Exception.class) public Result bookAppointment(AppointmentDTO dto) { // 1. 查排班 Schedule schedule scheduleMapper.selectById(dto.getScheduleId()); if (schedule null || schedule.getStatus() ! 1) { return Result.error(排班不存在或已停诊); } if (schedule.getRemainCount() 0) { return Result.error(该时段已约满); } // 2. 尝试扣减号源 int rows scheduleMapper.deductStock(schedule.getId(), schedule.getVersion()); if (rows 0) { return Result.error(手慢了号源已被抢完); } // 3. 生成预约记录 Appointment appointment new Appointment(); appointment.setScheduleId(schedule.getId()); // ... 其他字段赋值 appointmentMapper.insert(appointment); return Result.success(预约成功); }deductStock返回0说明当前版本被别的请求改掉了或者号源已经为0此时事务回滚不会生成预约记录。这就是最基本的乐观锁方案结合WHERE remain_count 0双保险防超卖。方案二悲观锁直接SELECT ... FOR UPDATE把待更新的排班记录锁住再执行更新。这方案实现上更“无脑”稳妥但性能最差。对这个系统来说性能不是问题如果你想展示不同方案的取舍可以在答辩时提一句“单体应用且并发量有限也可以考虑悲观锁但过于频繁的加锁会影响数据库吞吐”。方案三Redis分布式锁如果面试官问“如果部署多个实例怎么办”答案就是分布式锁。但毕设项目做成单机Redis锁又引入额外中间件没必要。我建议在文档里提一句即可别真去集成。这里有个血泪教训扣号和生成订单必须放在同一个事务方法里。之前有学生把扣号写在Controller里、插订单写在Service里结果扣号成功但订单插入失败号没了但预约没生成数据直接对不上。事务边界一定划在对业务一致性要求高的那一层也就是Service层。4.2 登录鉴权与用户信息传递拦截器怎么配才不会出bug登录这块我推荐用Session方案但有一个细节经常被忽略前端用axios请求拦截器里必须放行两个路径/api/user/login和/api/user/register。此外前后端分离开发时会有CORS跨域但打包到一起后不存在跨域问题。如果你开发时是分开跑记得在Config里配置CorsFilter允许前端地址。拦截器获取当前用户再简单不过public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { HttpSession session request.getSession(); if (session.getAttribute(userId) null) { response.setStatus(401); return false; } return true; }前端统一处理401响应码并跳转登录页。这里我踩过坑忘记放行静态资源。打包后前端页面本身在static目录下如果拦截器写的是/*这种粗粒度路径会把/index.html和CSS、JS文件全拦住。配置拦截路径建议写成/api/**只拦接口静态资源零成本放行。除了登录接口用户查询自己的预约记录时也不应该把userId放在前端传参里应该从Session里取。常见问题就是用户A通过修改请求参数里的userId看到了用户B的预约记录这在答辩被面试官看到直接社死。统一从Session取userId才是正确的权限校验方式。4.3 高效开发MyBatis-Plus的用法与查询优化MyBatis-Plus用起来比手写XML舒服太多了。这个项目里高频操作基本就是通过LambdaQueryWrapper条件查询预约记录、分页查询医生排班、插入预约订单这些统统不需要写XML。举个例子查询某医生某天的预约列表LambdaQueryWrapperAppointment wrapper new LambdaQueryWrapper(); wrapper.eq(Appointment::getDoctorId, doctorId) .eq(Appointment::getAppointDate, date) .eq(Appointment::getStatus, 0); ListAppointment list appointmentMapper.selectList(wrapper);这种链式查询的可读性比写一长串XML好得多。如果你确实需要复杂的多表联查比如首页列表要显示医生姓名和科室名直接写一条Select注解在Mapper方法上就行别为了“规范”硬把所有SQL都塞到XML里。MyBatis-Plus还提供分页插件PaginationInnerInterceptor配置一下就能用。4.4 统一返回结果与全局异常处理代码质量的隐藏加分项新手项目最喜欢在Controller里到处返回Map或者直接返回实体类状态码时有时无前端判断逻辑写得痛不欲生。我建议定义ResultT统一返回结构包含code、message、data三个字段public class ResultT { private Integer code; private String message; private T data; }成功返回Result.success(data)失败返回Result.error(提示信息)。配合RestControllerAdvice做全局异常处理业务异常就通过自定义BusinessException抛出统一由全局异常处理器捕获并包装成Result返回。参数校验用ValidatedNotBlank等注解在Controller层直接挡掉非法参数。这套东西做得越早后面调试越省心而且答辩时能讲的东西又多了一个点。4.5 医生端与管理员端排班管理和状态流转别做复杂管理员端的界面是整个演示的重头戏因为讲需求的时候都是从管理员开始演示的。排班管理我做了两个入口一个是在医生列表里点“排班管理”弹出该医生未来7天的排班情况可以逐个停诊、放号、调整号源另一个是新增排班选择医生、日期、时段、总号数一键插入。界面用Element UI的表格和时间选择器就行不用做拖拽排班那种过度设计。医生端更简单登录后只能看到自己的排班和对应的预约患者列表。预约患者列表要加一个状态筛选待就诊、已完成、已取消每个待就诊患者可以一键标记为“已完成”这样患者端的“我的预约”里才能看到状态从“待就诊”变成“已完成”。完整的状态流转线比你做十个界面都更能体现业务思考。5. 常见问题排查与答辩准备那些热词背后的真实场景5.1 环境与部署常见坑SpringBoot 版本太高如果用了3.x但本机是JDK8直接换2.7.x。别犹豫时间性价比最重要。 端口被占用server.port配置生效了吗是不是有两个项目同时在跑 前端请求404先看请求路径是 /api/xxx 还是直接 /xxx后端Mapping是否匹配。 返回到前端的中文乱码检查MySQL连接串是否配置了 characterEncodingutf8。 刷新页面404Vue路由是不是history模式切换成hash模式。 vue打包放进SpringBoot把dist目录内容拷到resources/static不是拷整个dist文件夹。5.2 测试数据与演示脚本答辩能否顺利全靠提前排练我见过不少同学功能全做出来了一上台从注册用户开始现弄折腾五分钟页面还没跑通。演示前一定要准备一套固定的演示脚本和现成的测试数据。至少要有一个管理员账号、一个医生账号关联好科室和排班、一个患者账号已有几条预约记录、未来7天内有排班的医生数据。这样可以避免临时创建、等待验证码、排班主任没配好等意外。演示时的pass卡登录管理员 → 展示科室统计 → 展示医生排班配置 → 切换患者登录 → 找科室找医生 → 完成一次预约 → 个人中心看到预约记录 → 切医生端看到这位患者。这条主线走完系统90%的功能都展示到位了。5.3 答辩必问题与回答思路这里整理几个高频问题也是网上那些java面试题里常见变体问为什么选择SpringBoot而不是SSH或Servlet答SpringBoot简化了配置内嵌Tomcat自动装配适合快速构建独立运行的Web服务底层仍是Spring的IOC和AOP核心思想没有变。问预约操作如果两个人同时访问怎么保证不超卖答数据库乐观锁。更新时带上version条件配合remain_count 0的校验影响行数为0则说明已被其他请求抢先抛异常回滚事务。然后可以补充一句如果未来并发量上来可以引入Redis分布式锁或用消息队列削峰。问为什么医生和用户放在同一个表答统一认证体系用role字段区分角色方便登录逻辑一致处理。扩展时如果有不同字段需求再用单独的子表扩展。这比分开建表省掉很多重复的登录注册逻辑。问如果同一患者重复预约同一医生同一天你怎么处理答在appointment表上加唯一索引字段组合是user_id schedule_id这样数据库层面就挡住了重复预约。代码层面也可以用查询校验做前置判断但唯一索引是最终防线。这类问题没有标准答案但你的回答要让人感觉到你是真的做过、想过边界条件而不是照着博客背的。5.4 关于爽约、退号与停诊的简单处理建议毕设不需要把真实医院的规则完完整整做出来但至少要体现“业务状态流转”的思考。我的建议是做这几个状态动作患者取消预约appointment状态置为“已取消”同步把schedule的remain_count 1加回来。注意这里也要用乐观锁不过是个方向上的更新增加库存冲突风险更低。管理员停诊把schedule状态置为“停诊”同时把所有关联预约批量置为“已取消”并生成日志。手册里写“停诊后自动通知患者”如果没做短信可以写“预留扩展接口”。爽约判定系统不做定时任务自动判定而是在医生端“标记完成”时做一个判断如果预约时间已过且未就诊自动置为“爽约”。这些规则不需要写得多复杂但逻辑链条要在README文档里写清楚答辩时比背概念有用一百倍。5.5 项目文档与README怎么写很多毕设评分里文档占比甚至高于代码。README至少要有这些内容项目背景与需求分析就是第一章的内容、技术选型说明及理由、数据库ER图及表结构说明、核心接口文档每个接口的URL、参数、返回示例、核心实现思路说明包括防超卖方案、角色权限设计、部署运行说明如何初始化数据库、如何启动后端、如何构建前端、测试计划与演示脚本。写文档有个技巧核心模块的实现思路放在最前面因为老师看文档的时间往往不超过十分钟他能最快看到你项目最有含金量的地方。我个人在实际带项目过程中的体会是这个项目的难度曲线不是匀速的排班模型的建立和预约防超卖的实现占据了整个项目接近一半的时间也是最值得投入的部分。很多学生容易把时间耗在调页面样式、调Element UI组件上等到了核心业务逻辑反而没时间写了这是本末倒置。如果后面你还想往深了扩展可以考虑加一个简单的定时任务每天凌晨自动把超时的预约改成爽约状态或者引入一个轻量级的消息队列把预约成功后的通知异步处理再或者把权限体系换成Spring Security JWT往真实企业级架构再靠近一步。但这些都是后话先把这条主线跑通你已经比大多数做同样题目的人强了。
阅读完成 · 觉得有帮助?
咨询建站