想做计算机毕设的朋友,应该对“网上预约挂号系统”这个题目不陌生。它是Java Web方向非常经典的选题,业务场景清晰,技术栈主流,既不像“图书管理系统”那样烂大街到答辩没亮点,又比“电商秒杀”这类高并发项目更适合本科生驾驭。这几年我带过的学生里,选这个题目的不少,真正做完、做顺、答辩不慌的,基本都抓住了几个关键点:业务闭环要完整、表结构设计要合理、核心预约逻辑要讲清楚数据一致性。
这篇文章我就以Java + SpringBoot + Web为技术底座,完整拆解一个医院网上预约挂号平台怎么做。从选题逻辑、系统架构、数据库设计,到核心预约业务的代码实现,再到开发中一定会踩的坑,一次性讲透。无论是正在选题犹豫不决,还是已经开工写到一半卡住,这篇都能给你一份可以直接“抄作业”的参考方案。
1. 需求分析与系统整体设计思路
1.1 为什么说这个选题是“性价比之王”
毕设选题有个潜规则:题目要让人一听就知道你做了什么,但又要有足够的复杂度和区分度。
“图书管理”“学生信息管理”这类纯CRUD题目,技术上没有难点,答辩老师随便问两句“你怎么处理并发”“你的权限怎么控制”就答不上来,分数自然不高。反过来,如果选“秒杀系统”“分布式电商”这种题目,技术深度是够了,但工作量巨大,尤其是想自己一行行写出来,本科生很难在一个学期内完成。
网上预约挂号系统正好卡在中间。它的核心业务是“预约”,这意味着你必须考虑几个真实业务问题:
- 同一个医生同一个时间段,不能挂出两张号。
- 同一个患者,不能重复挂同一个医生的同一个时段。
- 号源是有限的,必须防止超挂。
- 挂号后有取消、有退号,号源要能释放。
这些问题往深处说,就是“数据一致性”和“并发控制”,正好是SpringBoot后端开发里最有含金量的知识点。你说清楚“怎么用数据库唯一索引 + 事务控制 + 乐观锁来防止重复预约”,答辩老师基本就满意了。
1.2 系统角色与业务闭环
预约挂号系统不是一个简单的“个人中心”,它必须覆盖三种角色的完整业务流程。
患者端:注册登录、浏览科室和医生、查看排班、选择时间段预约、支付(或免支付)、查看预约记录、取消预约。
医生端:查看自己的排班、查看被预约的患者列表、标记接诊/完成就诊。
管理员端:科室管理(增删改查)、医生管理、排班管理(给医生设置出诊时间和号源数量)、统计报表(某科室某天的预约量)、系统公告管理。
一句话概括业务闭环:管理员维护基础数据 → 医生有排班 → 患者看到排班并预约 → 医生接诊 → 数据沉淀为统计报表。
这里我特别提醒一点:如果时间紧张,医生端可以砍掉一部分,但排班和预约这个核心闭环不能砍。因为“有排班才能预约”是整个系统的存在前提,如果只是做个“患者随便选个医生提交预约”,那和普通的表单提交没区别,毫无业务价值。
1.3 技术选型:为什么是SpringBoot
现在Java生态里做Web项目,SpringBoot基本是事实标准。原因很简单:
- 内置Tomcat,不用单独部署服务器,打包即运行。
- 自动配置大幅减少XML配置,适合快速开发。
- 生态成熟,Spring Data JPA、MyBatis-Plus、Spring Security都有大量现成方案。
- 面试和工作中使用广泛,做毕设的同时等于提前熟悉了企业级开发框架。
前端方面有两种路线:一种是传统方案,用Thymeleaf模板引擎渲染服务端页面;另一种是前后端分离,Vue + SpringBoot,后端只提供JSON接口。
我不建议你在毕设里强行上前后端分离。原因很现实:工作量直接翻倍,你得同时写后端接口和前端页面,还要处理跨域、Token鉴权等问题。如果前端功底一般,很容易在联调阶段卡死。用Thymeleaf做服务端渲染,一套代码搞定页面和接口,专注把核心业务做好,反而更容易出彩。
我的习惯是:如果项目要求了“前后端分离”,那我就会明确区分;如果没有要求,默认用服务端渲染方案,把主力精力放在预约流程、数据一致性和后台管理上。
2. 数据库设计:预约系统的地基
2.1 核心表结构与关键字段
数据库设计直接决定业务逻辑能不能写顺。我见过太多人把预约记录表设计得过于简单,只有一个“患者ID + 医生ID + 预约时间”,结果后面想扩展排班、想校验冲突,全都无从下手。
一个合格的预约挂号系统,至少需要下面这几张表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| user | 用户表(患者、医生、管理员共用的角色表) | id, username, password, real_name, role, phone, id_card |
| department | 科室表 | id, dept_name, description, sort_order |
| doctor | 医生信息表 | id, user_id(关联用户表), dept_id, title(职称), intro, avatar |
| schedule | 医生排班表 | id, doctor_id, dept_id, work_date, time_slot(上午/下午), total_count, remain_count, status |
| appointment | 预约记录表 | id, patient_id, doctor_id, schedule_id, appoint_date, time_slot, status, create_time |
| medical_record | 就诊记录表(可选,如果医生端要做接诊) | id, appointment_id, diagnosis, prescription, create_time |
这里的核心设计点是:排班表会存一个remain_count(剩余号源数),预约的动作本质上是“先查排班,再扣减号源,最后生成预约记录”——这是一个典型的库存扣减模型。
2.2 防重复预约的表结构保证
防止同一患者重复预约同一时段,最简单有效的手段是在表结构层面加唯一约束。
在appointment表中,针对(patient_id, schedule_id)建立唯一索引。这样即使代码逻辑有漏洞,数据库层面也会兜底拒绝重复插入。
ALTER TABLE appointment ADD UNIQUE KEY uk_patient_schedule (patient_id, schedule_id);这个设计可以说是整个系统的“安全锁”。我在实际开发时,哪怕业务逻辑里已经写了重复校验,也一定会加这层约束,因为并发请求下,代码里的查重可能失效,但数据库唯一索引是绝对可靠的。
2.3 号源扣减与超卖问题
号源扣减是预约系统最核心的坑。“超卖”指的是100个号源卖出去了101张。SpringBoot默认情况下,多个用户同时发起预约请求,如果采用“先查剩余号数,判断是否大于0,再减1”的逻辑,在高并发下一定会出问题。
解决这个问题有三种常见方案:
- 数据库乐观锁:在排班表的remain_count字段上使用版本号控制,UPDATE时检查version。
- 条件更新:用一条SQL完成“判断剩余号数大于0并且扣减”的操作。
- 分布式锁:用Redis锁控制同一排班的预约请求串行化。
毕设场景下,最推荐的是条件更新,因为实现简单、SQL一条搞定、无额外依赖:
@Modifying @Query("UPDATE Schedule s SET s.remainCount = s.remainCount - 1 WHERE s.id = :scheduleId AND s.remainCount > 0") int deductStock(@Param("scheduleId") Long scheduleId);这个update方法返回int,如果返回值是1,说明扣减成功;返回0,说明号源已经为0,预约失败。配合事务控制,同一时刻即使来了100个请求,数据库行锁会让他们排队执行,只有前100个能成功扣减。
这一招,一定写进你的毕业设计说明书,也是答辩时最能讲清楚的亮点。
3. 核心业务实现:从搭建到预约闭环
3.1 SpringBoot项目搭建与目录结构
无论用IDEA还是Eclipse,创建SpringBoot项目都建议走Spring Initializr,直接在IDEA里选Spring Initializr然后勾选依赖。需要注意的依赖有这么几个:
- Spring Web:提供Web能力
- Spring Data JPA 或 MyBatis-Plus:操作数据库
- MySQL Driver:连接MySQL
- Thymeleaf:服务端模板引擎
- Lombok:简化实体类代码
- Validation:后端参数校验
项目搭建完成后,目录结构按照SpringBoot常规规范来分:
com.example.hospital ├── controller // 控制层,接收请求 ├── service // 业务层,核心逻辑 │ └── impl ├── repository // 数据访问层(JPA或MyBatis-Plus的Mapper) ├── entity // 数据库实体类 ├── dto // 数据传输对象(接收前端参数) ├── vo // 视图对象(返回前端数据) ├── config // 配置类(拦截器、跨域等) ├── common // 通用类(结果封装、异常处理、常量) └── utils // 工具类这个结构不是随便分的,它的核心思想是分层解耦。Controller只负责接收参数和返回结果,Service只写业务逻辑,Repository只做数据库操作。答辩时老师问你“为什么这么分层”,你要能说出“降低耦合度、便于维护和测试”这种标准答案。
3.2 用户登录与角色权限控制
预约挂号系统至少有两种角色:患者和管理员;加上医生的话就是三种。SpringBoot里做权限控制有两种常用方式:Spring Security(安全框架)和HandlerInterceptor(拦截器)。
如果时间紧,用拦截器就够了。我通常的做法是:
- 登录成功后,把用户信息存入Session。
- 自定义一个LoginInterceptor,拦截所有请求。
- 在拦截器里检查Session是否已有登录用户,如果没有,重定向到登录页。
- 对于管理员专属的URL(比如/admin/**),校验role字段是否为admin。
核心代码:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = (User) request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect("/login"); return false; } // 对管理员接口做强校验 String uri = request.getRequestURI(); if (uri.startsWith("/admin") && !"admin".equals(user.getRole())) { response.setStatus(403); return false; } return true; } }这个方案的好处是:不引入额外依赖,代码量少,逻辑清晰,而且完全满足毕设需求。答辩时还可以顺手提一句“生产环境中一般会用Spring Security或Sa-Token,但毕设场景拦截器更直观、更容易讲清楚”。
3.3 预约挂号的完整业务逻辑
这是整个系统的核心,思路必须无比清晰。我按步骤拆解:
第一步,患者进入医生详情页,选择一个排班时段,发起预约请求。请求携带两个参数:scheduleId(排班ID)和当前登录用户的userId。
第二步,校验阶段。这一步要检查三件事:
- 排班是否存在且状态为“可预约”。
- 当前时间是否超过该排班的截止预约时间(比如当天上午的号,10点后不能再预约)。
- 当前时间是否晚于排班的就诊日期。
第三步,查重。查询appointment表,确认该患者没有预约过这个排班。
第四步,扣减号源。执行上面提到的条件更新SQL,判断返回结果是否为1。
第五步,生成预约记录,状态设为“已预约”。
第六步,提交事务。任何一步失败,整体回滚。
这六步必须在同一个事务里执行。我习惯在Service方法上直接加@Transactional注解:
@Transactional(rollbackFor = Exception.class) public Appointment bookAppointment(Long scheduleId, Long patientId) { // 1. 查找排班 Schedule schedule = scheduleRepository.findById(scheduleId) .orElseThrow(() -> new BizException("排班不存在")); // 2. 校验时间 if (schedule.getStatus() != ScheduleStatus.AVAILABLE) { throw new BizException("该排班已不可预约"); } // 3. 查重 if (appointmentRepository.existsByPatientIdAndScheduleId(patientId, scheduleId)) { throw new BizException("您已预约过该时段,请勿重复预约"); } // 4. 扣减号源 int result = scheduleRepository.deductStock(scheduleId); if (result == 0) { throw new BizException("号源已满,预约失败"); } // 5. 生成预约记录 Appointment appointment = new Appointment(); appointment.setPatientId(patientId); appointment.setScheduleId(scheduleId); appointment.setStatus(AppointmentStatus.BOOKED); return appointmentRepository.save(appointment); }这里有一个非常容易被忽略的点:事务注解的rollbackFor属性一定要写。Spring默认只对RuntimeException回滚,如果业务代码里抛的是自定义的Exception,不加rollbackFor,事务就不会回滚,会出现号源扣了但预约记录没生成的情况。这是新手最容易踩的坑之一。
3.4 取消预约与号源释放
有预约自然有取消。取消预约的逻辑相对简单,但也要注意两点:
- 只有状态为“已预约”的记录才能取消。
- 取消后要释放号源,也就是把排班表的remain_count加回1。
同样要放在一个事务里,否则会出现“预约状态改了但号源没释放”的问题。很多人在这一步会漏掉,导致系统运行一段时间后,号源越来越少,最后变成“永远满号”——这个问题答辩时被问到就很尴尬。
4. 页面展示与管理端功能要点
4.1 患者端的页面流转
服务端渲染方案下,页面用Thymeleaf写。页面不多,我建议按这个节奏来:
- 首页:展示科室列表和重点医生推荐,导航栏有登录/注册入口。
- 医生列表页:支持按科室筛选、按职称排序,展示医生头像、简介、剩余号源情况。
- 医生详情页:展示医生介绍、本周排班表(每个时段显示剩余号数)。
- 预约确认页:展示预约信息,确认后提交。
- 个人中心:我的预约列表,可以对“未就诊”的预约发起取消。
页面上需要特别注意的是排班表的展示逻辑。医生一周有7天,每天可能有上午、下午两个时段,每个时段是一个排班记录。展示的时候需要先按日期分组,再按时段显示剩余号数。剩余为0的时段要置灰并提示“已约满”,这样患者体验才合理。
4.2 管理端的核心功能清单
管理端的核心职能是“维护系统基础数据和查看运营数据”。如果做得太粗糙,会让整个项目显得不够完整。
我建议最低限度包含这些功能:
- 科室管理:列表、新增、编辑、删除。删除前要校验该科室下是否有医生,有的话提示不能删除。
- 医生管理:列表(关联科室、职称)、新增医生(同时创建登录账号)、编辑、停用。
- 排班管理:选择医生、选择日期、设置时段和号源总数,生成排班。
- 预约查询:按日期、医生、状态筛选预约列表,处理“爽约”或“完成”操作。
- 数据统计:按科室统计近7天预约量,按医生统计接诊量,用柱状图展示。
这里有一个注意点:批量生成排班功能。如果排班只能一条一条手工添加,管理员创建一个月的排班会累死。好的做法是:管理员选好医生和一个日期范围,系统自动为该时间段内的每一天生成上下午排班。这个功能实现不难,但很能体现你做项目的思考深度。
4.3 Bootstrap或Tailwind,选一个就行
服务端渲染的页面,不推荐写原生的CSS来布局,效率太低。直接用Bootstrap 5或Tailwind CSS的CDN引入,组件现成、响应式适配也好。
如果想让页面有“医疗系统”的感觉,可以在配色上用蓝色或蓝绿色系,避免大红大紫。整体布局参考常见的“顶部导航 + 主体内容”结构,管理端用“左侧侧边栏 + 右侧内容区”的后台框架布局。页面样式这块不用追求花哨,干净、整齐、信息层次清晰才是关键。
5. 常见问题、排坑经验与答辩要点
5.1 开发期必踩的几个坑
这些坑在我带学生做项目的过程中反复出现,提前排掉能省很多时间。
时间格式问题。排班日期和时段,前端提交的是字符串,后端直接收会报错或者格式不对。在实体类的日期字段上加上@DateTimeFormat(pattern = "yyyy-MM-dd"),同时前端表单里固定用type="date"或"datetime-local"的输入框。另外,用LocalDate而不是java.util.Date来存日期,可以避免大量时区和格式化烦恼。
Lombok和实体类toString的坑。如果用Lombok的@Data注解生成toString,实体类里的关联对象(比如医生类里有关联的科室对象)会在toString时产生循环调用,导致栈溢出。解决办法一是用@ToString.Exclude排除关联字段,二是建议手动控制需要输出的字段。
IDEA里改代码但不生效。检查是否开了热部署。如果没开,每次改了后端代码都建议手动重启一下。如果用Thymeleaf模板,改了页面但没生效,检查application.yml里的缓存配置:
spring: thymeleaf: cache: false跨域问题。如果你最后还是选择了前后端分离,一定要在SpringBoot里配置CorsFilter或者用@CrossOrigin注解。否则前端页面调接口会报CORS错误,在浏览器里看起来像是后端崩溃了,其实是跨域被拦。
5.2 数据一致性验证的方法
项目做完后,建议做一个简单的并发测试,验证防超挂是否真的有效。方法也不复杂:
- 把某个排班的号源总数设为1。
- 用两个浏览器窗口,分别登录两个不同的患者账号。
- 同时点击预约这个排班的按钮。
- 如果系统正确,一个成功,另一个提示“号源已满”。
如果你能力强一点,用JMeter开10个线程同时请求预约接口,观察最终预约成功的数量有没有超过号源总数。这个测试结果截图放进论文里,是很有说服力的。
5.3 答辩时要主动展示的亮点
答辩时间通常只有5到10分钟,老师没有时间去细看你的全部代码。你要在有限时间内,主动把最有含金量的几个点讲出来:
- 表结构设计:为什么要给预约记录加唯一索引?为了数据库层面的兜底。
- 号源扣减:怎么防止超卖?条件更新SQL,一条语句完成判断和扣减。
- 事务控制:预约和取消预约为什么必须加事务?避免脏数据和号源泄漏。
- 角色权限:拦截器怎么控制不同角色的访问路径。
讲这些的时候,不要背概念,要用“我在实现中发现了一个什么问题,然后怎么解决的”这种叙事方式。比如:“我一开始用先查后减的写法,结果模拟并发的时候出现了超卖,后来改成一条SQL条件更新,再测试就稳定了。”这种表达比背理论高出一个档次。
5.4 在线演示前的心态准备
毕设答辩时大概率会用你自己电脑在线演示。演示前务必准备好:
- 确认MySQL已经启动,数据库导入脚本执行过。
- 确认项目是启动状态,不是写好了没跑。
- 准备一个演示账号(管理员),一个患者账号。
- 演示路径要提前走一遍:登录 → 查看科室 → 查看医生 → 查看排班 → 预约 → 个人中心确认 → 取消 → 管理端查看数据变化。
如果现场出现意外,比如数据库连不上或者页面报错,不要慌。直接说“这个环境之前是可以的,我重启一下服务”。临场解决问题的能力,本身就是答辩评分的一部分。为了万无一失,可以提前在本地把打包好的jar文件也跑一遍,熟悉一下从jar包启动的流程。用java -jar命令启动项目后,用浏览器访问,这套流程跑通了,演示就基本没大问题。
写在最后的经验
我在实际开发和指导学生的过程中,最大的感受是:毕设项目的分数高低,不全取决于代码量大小,更多取决于你有没有把核心业务讲清楚、关键问题有没有解决方案。预约挂号系统这个题目,天生自带“防重复预约”“号源防超卖”“事务一致性”这些高质量话题,你只要把一个点做扎实了,就已经超越了大量只会CRUD的同学。
如果你现在正好在做这个题目,我的建议是:先把排班表和预约记录表这两张表吃透,把预约和取消预约这两段业务代码写顺,然后再去补页面和管理功能。骨架立住了,剩下的都是锦上添花。开发过程中遇到具体报错,多用控制台日志和断点调试定位问题,别急着改代码,先搞清楚为什么错。这种排查过程,其实就是答辩时你能讲出“故事”的素材。