医院预约挂号系统Java后端毕业设计:从业务建模到并发防超卖
简介:面向毕业设计场景的医院预约挂号系统,是一套基于Java与JSP技术栈的完整项目,提供源码、数据库脚本和文档说明,适合计算机、人工智能、自动化、通信等专业学生用于课程设计、期末大作业或毕业设计参考。压缩包共189个文件,大小1.81MB,核心包括55个Java业务逻辑类、39个JSP页面及配套CSS/JS前端资源,以及3份SQL数据库脚本和说明文档,代码结构清晰,便于运行调试与二次改动。项目界面基于Bootstrap风格,涵盖登录、管理员管理等常见模块,经作者调试测试且毕设答辩评分达98分,具备较高的完成度。目前已有126人学习浏览,能够帮助初学者理解预约挂号流程、JavaWeb分层开发与数据库表设计;基础扎实的读者还可在此基础上扩展科室管理、排班、预约记录统计等功能。
1. 医院预约挂号系统,到底在做什么
医院预约挂号系统,几乎是 Java 后端毕业设计里被选次数最多的题目前三。原因很直接:它不是一个“玩具项目”,而是真实医疗信息化里每天都在跑的业务流——排班、放号、锁号、退号、黑名单,每一个环节都有明确的业务规则。相比图书管理系统那种一张表 CRUD,预约挂号系统的数据关系更复杂,涉及角色更多,答辩时能讲的东西也更扎实。
这篇笔记面向已经能写 Java 增删改查、但还没独立完成过一个完整系统的开发者。我会从业务建模开始,讲到数据库表结构、后端核心接口、以及最容易被面试官追问的几个细节,比如超时未支付释放号源、医生排班状态机、并发挂号时防止超卖。整篇笔记的目标是让你拿着标题里的源码加数据库,能真正跑起来,也能从头讲清楚每一步的设计原因,而不是只会在答辩时打开 IDEA 演示。
2. 需求拆解与数据库设计:先把表和状态定义清楚
2.1 用户故事与角色边界,决定了系统里有哪些模块
任何系统的第一件事不是写代码,而是把使用者列出来。医院预约挂号系统里至少有四类角色:患者、医生、管理员(通常是科室排班人员)、以及可能存在的超级管理员。每类角色能做什么,就是系统的功能边界。
患者侧的核心诉求是:查科室、查医生排班、选时间段挂号、支付挂号费(或者选择到院支付)、查看挂号记录、取消挂号。医生侧的核心诉求是:看自己今天的排班、看已挂号患者列表、标记患者是否已就诊、关闭或开放某个时间段的号源(比如临时停诊)。管理员侧的核心诉求是:维护科室信息、维护医生信息、给医生安排排班、查看指定日期的全院号源情况。
这三条线合起来,就是一个典型的多角色 RBAC(基于角色的访问控制)系统。数据库里建议用 user 表加 role 字段,还是单独拆 user_role 关联表?我的建议是:毕业设计直接拆表,虽然多写一点关联查询,但在答辩时你能讲清楚“为什么用多对多而不是直接写死一个角色字段”,这本身就是加分项。更重要的是,拆表的代价很低,一张关系表不过十行 SQL,却能避免后期加需求时改动 user 表的麻烦。
2.2 核心表结构:用户、医生、排班、挂号单、科室
从数据模型上看,最小可用的表集合是六张。科室表 department、用户表 user、医生表 doctor(也可以并入 user,但独立更好)、排班表 schedule、挂号单表 registration、以及 token 相关的登录凭证表(一般用 Redis 存,如果不想引入 Redis,则建一张简单的 u_token 表)。
先看用户表和医生表的设计。用户表建议直接用手机号做登录账号,字段有 id、phone(唯一索引)、password(BCrypt 加密后的密文)、real_name、id_card、role、create_time。为什么不用用户名?因为手机号天然唯一,而且后续做短信验证码登录时不需要再改表结构。医生表不要重复存用户表的字段,只存 doctor_id(关联 user.id)、department_id、title(职称,比如主治/副主任)、introduction,这样以后医生也是用户,能用同一套认证逻辑。
排班表是整张逻辑里最核心的。字段需要包含 id、doctor_id、department_id、work_date、period_type(比如上午/下午/晚班)、start_time、end_time、total_slots、available_slots、status。这里的 total_slots 是排班时定死的号源总量,available_slots 是剩余量,挂号成功时 available_slots 减一。如果这两个字段没有区分,直接用 count(*) 去统计挂号记录,用不了多久就会出现脏数据——因为取消挂号时你会面临“到底要不要恢复号源”的判断,单独维护一个剩余量字段能省很多事。
挂号单表 registration 的字段要多一点:id、patient_id(关联 user.id)、doctor_id、schedule_id、visit_date、visit_time、fee、status(0待支付 1已支付 2已就诊 3已取消 4已退号)、create_time、cancel_time、diagnosis_code(医生填写的诊断编码,可选)。这里关键的是 status 字段要单独拎出来讲清楚状态流转,因为它直接对应业务逻辑里的核心流程。
2.3 挂号状态机与支付超时:两张表之间的事务边界
挂号单的状态流转是整个系统里最容易答不清的问题。我在做的时候会把状态机画成这样的线:待支付(0)可以走到已取消(3)和已支付(1);已支付(1)可以走到已就诊(2)和已退号(4);已就诊(2)是终态,不能再动;已退号(4)是终态,但它的数据要保留,不能物理删除。
有个细节很多人会漏:当用户发起挂号、选择“到院支付”时,系统应该给他一个默认状态“已挂号待支付”吗?如果这样做,号源已经在创建挂号单那一刻被扣掉了,但用户可能根本不会来。常见的做法是,到院支付也走“待支付”状态,但系统启动一个定时任务,超过约定时间(比如 30 分钟)未支付,就把该挂号单置为已取消,同时把对应排班的 available_slots 加一。这一步必须用事务包住:更新挂号单状态 + 更新排班剩余号源,两步要么都成功,要么都失败。
每次我在答辩时都会演示这个逻辑,因为它的价值在于你讲出了“并发和一致性问题”,哪怕实现得简单,也比“能跑就行”高一个层次。如果懒得引入消息队列,用一个 Spring 的 @Scheduled 定时任务扫库就行,每 30 秒扫一次,把超时单批量处理,注意把“当前时间 - create_time > 30分钟”作为条件写死在 SQL 里。
3. 后端实现与核心代码:从登录鉴权到挂号接口
3.1 项目结构与技术栈选型:SSM 比 Spring Boot 更适合毕业设计吗
如果你手里的参考源码用的是 Spring Boot,那么直接继续用。如果是 SSM(Spring + SpringMVC + MyBatis)的老项目,我的意见是不要强行迁移到 Spring Boot。毕业设计的评分点在“业务完整度”和“技术深度”,框架新旧只是锦上添花。Spring Boot 的优势是零配置启动,但 SSM 能让你在答辩时把请求处理链路讲得更细,比如 DispatcherServlet 是怎么路由到 Controller 的,这反而是加分点。
但有一点值得注意:依赖版本不要用校园网里传了五六年的旧包,比如 Spring 4.x 加 JDK 8 的组合在 2024 年已经很难跑起来。建议最少选用 Spring Boot 2.7.x 搭配 JDK 1.8 或 11,数据库用 MySQL 5.7+ 或 MySQL 8.0(注意驱动名字和时区参数的变化)。数据库连接池用 Druid,自带监控页面,答辩时打开 Druid 的 SQL 监控,能直观展示每条接口消耗了多少毫秒,这是个看起来很加分的小技巧。
项目结构上,我习惯按功能模块分包,而不是按技术分层分包。也就是说,不要建 controller / service / mapper 三个大包把一切都塞进去,而是按 auth、user、doctor、schedule、registration 分包,每个包里再放自己的 controller、service、mapper。理由很简单:按功能分包,增量加需求时只需要新增一个包,不需要改既有包的依赖关系;同时面试官看代码时按业务入口找,好读。
3.2 登录鉴权:用 Token 还是 Session,建议直接上 Token
很多毕业设计还在用 Session 做登录态。这能跑,但我会建议你改成 Token 方案,原因有两个:一是你以后找工作时写简历,Token、JWT 这些词都是高频面试考点,现在用一遍能加深理解;二是前后端分离是当前的主流开发模式,Session 天然不适合纯 API 场景。
考虑到这是毕业设计而非生产项目,可以不需要引入 Spring Security,那套东西学习成本高,容易把时间花在配置上而不是业务上。个性化做法是定义一个拦截器,在进入 Controller 之前校验请求头里的 Authorization 字段:
@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private UserTokenMapper userTokenMapper; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口和静态资源 if (request.getRequestURI().contains("/auth/")) { return true; } String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { response.setStatus(401); return false; } // 在数据库中查询 token 是否存在;生产环境建议用 Redis,此处为了简单直接查库 UserToken userToken = userTokenMapper.selectByToken(token); if (userToken == null || userToken.getExpireTime().before(new Date())) { response.setStatus(401); return false; } // 把当前用户信息放入请求上下文,供后续 Controller 直接取用 request.setAttribute("currentUserId", userToken.getUserId()); return true; } }这段代码的逻辑不复杂:除了登录注册接口,其余接口都先查一遍 token 表,校验是否过期。然后把查出来的用户 ID 塞进 request attribute,后续的业务方法从那里取当前用户,不用每次拿 token 再去反查用户。这里有个隐含的设计决策——token 是查数据库而不是用 JWT 自校验。查库的好处是退号、改密码时能立刻让旧 token 失效,坏处是多一次数据库 IO。在并发量不高的系统里这是完全没问题的,而且如果你答辩时主动说出这个取舍,面试官反而会觉得你考虑过方案选型。
登录接口本身的逻辑要两点:一是密码校验必须用 BCrypt 而不是 MD5,MD5 加盐也不行,因为 BCrypt 的慢哈希天然对抗暴力破解;二是登录成功后要生成一个随机 token(用 UUID 就好),存入 user_token 表,并设置过期时间。返回值里带上用户基本信息和 token,前端把 token 存在 localStorage 里,每次请求带上它。
3.3 排班模块:校验与生成策略
排班是预约挂号的源头。如果排班数据本身有冲突(比如一个医生同一天上午被排了两张表),后续所有挂号业务都会错乱。我的做法是提供一个排班生成接口,接收 doctorId、workDate、periodType 三个参数,插入前先查重——同医生同日期同时段不能存在两条排班记录。
-- 查重 SQL:如果返回 count > 0,说明已存在排班,禁止再次插入 SELECT COUNT(*) FROM schedule WHERE doctor_id = #{doctorId} AND work_date = #{workDate} AND period_type = #{periodType}注意 MySQL 的日期处理。work_date 建议直接用 DATE 类型,不要用 DATETIME+VARCHAR 存字符串。如果数据库表已经建成 VARCHAR,也要用 str_to_date 转一次再比较,否则索引会失效。批次生成排班是另一个需求——管理员经常需要把下个月一整月的工作日都生成排班。这种批量场景不要一条条循环插入,用 MyBatis 的 batch 模式或者拼一条 insert into values (),(),() 就能快几十倍。
关于可挂号时间段的划分,建议做成数据字典而不是硬编码。比如 period_type 的值域可以定义为 1=上午 2=下午 3=晚班,并且读取一张 t_config 表,让管理员可以自由改每个时间段允许挂号的开始和结束时间。硬编码“上午 8 点到 11 点放号”这类规则,后期想调整就得改代码重新部署,在答辩演示时也非常不灵活。
3.4 挂号接口与并发防超卖:唯一约束 + 乐观锁
挂号接口是整个系统里最重要的方法,也是面试官最有可能深挖的地方。它做的事情总共四步:校验排班存在且状态是已放号;校验当前时间在医院配置的放号时间段内;校验该患者没有重复挂号该时段;扣减 available_slots 并创建挂号单。
如果是在单体应用里,扣减号源最稳的方式是用带条件的 UPDATE 语句作为乐观锁:
int updated = scheduleMapper.decreaseAvailableSlots(scheduleId); // 这条 SQL 的核心是 available_slots > 0 这个条件,小于等于 0 时更新影响行数为 0UPDATE schedule SET available_slots = available_slots - 1 WHERE id = #{scheduleId} AND available_slots > 0这里最关键的洞察是:UPDATE 语句本身在 InnoDB 里是行级锁,两个请求同时执行这条 SQL 时,后一个会等前一个提交才能执行,就起到了串行化的效果。如果返回更新行数为 0,说明号已经挂完或者排班状态异常,直接返回“号源不足”即可。这个方法不需要显式加 SELECT FOR UPDATE,也不需要分布式锁,在单体应用里是最简且正确的实现。
但还有一步不要漏——防止同一个患者重复挂号同一个医生的同一个时段。光靠 Java 代码查一遍不够安全,应该在 registration 表上建联合唯一索引:
ALTER TABLE registration ADD UNIQUE INDEX uk_patient_schedule ( patient_id, schedule_id );这个唯一索引是最后一道防线。即使出现了两个请求同时通过业务校验,其中一个插入时依然会撞索引报 DuplicateKeyException,然后在代码里把异常捕获并转成友好的错误提示。有了这条索引,防重复挂号和防超卖这两件事都才算真正落地了。
4. 常见问题排查:跑不起来和答辩翻车的避坑清单
4.1 数据库连接报错:MySQL 8.0 的时区与驱动问题
现象:启动 Spring Boot 项目后,首次访问数据库接口直接报 CommunicationsException 或者连接超时。原因:很多现成源码用的还是 com.mysql.jdbc.Driver 这个过时驱动,而 MySQL 8.0 要求使用 com.mysql.cj.jdbc.Driver,同时在连接串里必须指定 serverTimezone。解决:把 JDBC 连接串改成下面这种格式,存入 application.yml 即可。
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/hospital_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver注意 useSSL=false 很重要。如果本地 MySQL 没配置 SSL 证书,默认开启 SSL 握手会耗掉额外时间,首次打开页面时会有明显的卡顿感。另外如果你的 MySQL 是 5.7,用 com.mysql.cj.jdbc.Driver 也能兼容,不用改回去。
4.2 前端页面能打开但接口 404:静态资源和拦截器放行冲突
现象:登录页和注册页显示正常,但点击登录后浏览器控制台出现 401 或者 404。原因:拦截器把所有请求拦截后直接返回 401,前端拿到的是失败响应,同时你配置的前端静态页面可能放在 resources/static 目录下,也被拦截器挡了。解决:在拦截器里放行静态资源路径和前端入口页面。
// 注册拦截器时,放行静态资源的路径 registry.addInterceptor(authInterceptor) .addPathPatterns("/**") .excludePathPatterns("/", "/index.html", "/static/**", "/auth/**", "/*.js", "/*.css", "/favicon.ico");这个现象的教训在于:做前后端未分离项目时,不要贪图方便把所有路径都交给拦截器,一定要在注册处排出不需要登录即可访问的资源。如果在浏览器 Network 里看到 404,大概率不是后端接口写错,而是请求路径被拦截器吞了,或者前端资源本身就没编译进 target 目录。
4.3 号源超卖:为什么明明判断了还是有两个人挂到同一个号
现象:并发测试时,用两个线程同时给同一个患者挂同一个医生的同一个时段,竟然都返回成功,数据库里也出现两条记录。原因:你在 Service 层写过类似这样的代码——先 SELECT 查询 schedule.available_slots 是否大于 0,然后 UPDATE 扣减。但这两个操作之间不是原子的,并发场景下两个请求都读到剩余 1,两个都能通过校验,两个都执行 UPDATE,最终把 -1 的脏数据写进表里。解决:不要用“先查后更”的模式,把判断挪进 UPDATE 的条件里,上一节 3.4 的写法就是正确解法。这是并发编程里的经典竞态条件,答辩时能主动讲出来,效果会非常加分。
4.4 管理员改排班后,已挂号患者的数据对不上
现象:某个医生临时停诊,管理员直接删除或修改了排班记录,结果已挂号患者仍然能在“我的预约”里看到这个医生,到了医院才发现人不在。原因:删除排班记录时没有同时处理关联挂号单。解决:不要在界面提供物理删除排班的功能,提供“停诊”操作,本质是把排班表 status 改为已停诊,同时把该排班下所有已挂号患者的挂号单状态置为已取消,并提醒患者重新预约。这个逻辑用事务包住,两步操作必须同时生效。
5. 答辩前的系统自测与三个加分优化点
5.1 用 Postman 跑一遍核心接口链路的冒烟测试
在打开 PPT 之前,先给自己 20 分钟做一轮手动冒烟测试。我会按照下面的链路走一遍:用测试账号登录拿 token;用 token 查询科室列表;查某科室下医生的排班;对某个可用排班发起挂号;再次发起同样挂号确认被拦截;取消挂号;确认号源数量恢复。这八步走完,系统的主干逻辑就没有问题了。接口自测时把响应时间和返回状态码记下来,答辩时万一演示环境出问题,还能用记录顶上。
挂号接口除了正常路径,一定要手动测一遍“号源为 0 时挂号返回什么”。很多项目前端能正常挂号,但号源满的时候直接抛出 500 异常,页面上能看到堆栈信息,这是非常掉分的。正确的做法是全局异常处理器捕获业务异常,返回统一结构体,比如 code=20001、message=号源不足,前端弹窗显示这个提示即可。
5.2 体检项目里的实用功能:给挂号单加一个可选的问诊备注
三个加分优化点里,第一个是做“问诊备注”功能。挂号时允许患者填写最多 200 字的症状描述或既往病史,在医生端的已挂号列表里展示,这样挂号单就不只是排队的凭证,还能帮助医生提前了解患者情况。开发成本极低,只加一个 remark 字段,找一个 textarea 输入框,但讲业务场景时非常落地,让人感觉不是照搬模板。
第二个优化点是“退号冷静期”。真实系统里有大量患者刚挂完号就发现挂错科室了,马上退号。做一个简单的规则:挂号成功后 5 分钟内退号不收手续费,超过 5 分钟收取 10% 手续费。这个规则在代码里只是一个时间差判断,但它展示了你对真实业务场景有过思考,属于调优思维而非纯功能堆叠。
第三个优化点是把运营数据可视化。在管理员端加一个简单的统计页,显示今天各科室挂号量 Top 5、取消率、平均退号时间。不要用 ECharts 画太复杂的图表,一张柱状图加一个表格就足够。但背后要写一个聚合查询的 SQL,涉及 GROUP BY 和 JOIN,面试官不问也得讲,问了就是实打实的工作量体现。
5.3 避免踩最后一个坑:把用户密码明文放进数据库
每次都觉得这是常识了,但每年答辩时还是能看到明文密码的数据库。哪怕项目源码是现成的,也要把用户注册逻辑里的密码做一次 BCrypt 加密再入库。不要觉得数据库只有自己看就没关系,答辩时老师看到表结构里 password 字段值是一串密文,对口述的“安全性设计”会顺利接受;如果现场打开数据库看到明文,前面讲的一切安全性设计都会瞬间打折扣。
最后分享一个个人的习惯:答辩前三天,把数据库删掉,用项目里的 SQL 初始化脚本从头建一次库,再用页面走一遍主流程。这个动作能暴露出所有写死在代码里的隐藏假设——比如某个字段的长度、某个外键的不存在。和用现成环境顺顺利利跑完一次演示相比,这种断水断粮式的演练更能让人踏实。希望帮到你。
本文还有配套的精品资源,点击获取