☰
SSM医院预约挂号系统源码拆解:号源状态机与并发控制实战
2026/10/1 12:42:47 网站建设 项目流程

简介:这份资源是面向计算机专业学生与Java初学者的一套医院预约挂号系统毕业设计源码,基于SSM框架与MySQL数据库开发,适合用作毕业设计、课程设计或SSM入门练手项目。压缩包共1386个文件,约27.49MB,涵盖135个Java源文件、169个JSP页面、358个JavaScript脚本及145个CSS样式文件,另有SQL建库脚本、XML配置、图片与字体等静态资源,前后端代码与数据库文件齐全,项目可正常运行。系统功能覆盖个人中心、用户管理、科室信息管理、医生管理、出诊信息管理、预约时间段管理、挂号预约管理、问题反馈与解答管理、系统管理等模块,并附带说明文档与LW论文资料,便于理解整体业务逻辑与实现思路。目前已有104人学习下载,适合需要完整赛题方案、可运行代码与文档参考的读者,帮助快速搭建环境、梳理模块结构并完成二次开发。

1. 医院预约挂号系统源码拆解:SSM 三件套怎么把挂号这件事跑通

医院预约挂号系统是计算机毕业设计里被选得最多、也最容易被做“水”的题目之一。它表面上是“增删改查”,实际上藏着一整套真实业务约束:号源不能被重复占用、同一患者同一时段不能挂两个号、退号后号源要能回滚、医生排班和挂号记录必须对得上。这套基于 SSM(Spring + SpringMVC + MyBatis)加 MySQL 的源码,解决的正是这些约束下的完整闭环,适合正在做 Java 毕业设计、想拿一套能跑通、能讲清楚、能改得动的项目打底的同学。它不追求高并发,但把“挂号—支付—退号—排班”这条主链路走通了,配合说明文档和论文(LW),基本能覆盖本科答辩对业务完整度和技术栈匹配度的要求。下面我按“先立住原理、再动手复现、最后讲坑”的顺序,把这套源码拆开讲。

2. 号源模型与 SSM 分层:为什么这套架构能撑住挂号业务

2.1 挂号系统的核心不是表多,是号源状态机

很多人拿到源码第一反应是数表:用户表、医生表、科室表、排班表、挂号记录表……表多是表象,真正决定系统能不能用的是号源状态机。一个号从“可预约”到“已锁定”到“已支付”到“已就诊”或“已取消”,每一步都有前置条件。SSM 这套源码通常把状态字段放在排班表(schedule)或号源明细表(source)里,用status加version两个字段控制流转。

常见做法是:排班表记录“某医生某天上午还剩几个号”,挂号记录表记录“谁在什么时候抢到了哪个号”。两者靠schedule_id关联。问题在于,如果只更新排班表的剩余数量,不做并发控制,两个请求同时读到“剩 1 个”,就会超卖。所以源码里一般会加乐观锁版本号,或者用update ... where remain > 0这种带条件的 SQL 兜底。

提示:判断一套挂号源码是否“能讲”,先看它有没有处理超卖。只做增删改查、不处理并发扣减的,答辩时一问就露馅。

2.2 SSM 三层怎么分工:Controller 收参、Service 管状态、Mapper 落库

SSM 的分层在挂号场景里分工很明确。Controller 只负责接收前端参数(医生 ID、排班 ID、患者 ID)并做基础校验;Service 层是状态机的执行者,负责判断“这个号还能不能挂”“这个人是不是已经挂过”“退号要不要回滚号源”;Mapper 层只做数据库读写,不掺业务逻辑。

这样分的好处是:状态判断集中在 Service,改规则只动一处。比如“同一患者同一天同一科室只能挂一个号”,这条规则写在 Service 的createAppointment方法里,Controller 和 Mapper 都不用改。源码里如果把这行判断写进了 Controller 或者 SQL,后期加规则就会到处补丁。

// Service 层挂号核心逻辑(示意) public Result createAppointment(Long scheduleId, Long patientId) { // 1. 查排班,判断号源是否充足 Schedule schedule = scheduleMapper.selectById(scheduleId); if (schedule == null || schedule.getRemain() <= 0) { return Result.fail("号源已满"); } // 2. 判断该患者是否已挂同一时段 int count = appointmentMapper.countByPatientAndSchedule(patientId, scheduleId); if (count > 0) { return Result.fail("您已预约该时段"); } // 3. 乐观锁扣减号源 int rows = scheduleMapper.reduceRemain(scheduleId, schedule.getVersion()); if (rows == 0) { return Result.fail("号源被抢,请重试"); } // 4. 写入挂号记录 Appointment appt = new Appointment(); appt.setScheduleId(scheduleId); appt.setPatientId(patientId); appt.setStatus(0); // 0=待支付 appointmentMapper.insert(appt); return Result.ok("预约成功"); }

这段代码的关键在第三步:reduceRemain带上了version条件,SQL 类似update schedule set remain = remain - 1, version = version + 1 where id = ? and version = ? and remain > 0。返回影响行数为 0 就说明被别人抢先改了,直接提示重试。参数version从查询时取出,保证“读—改—写”之间没有被插队。

2.3 数据库表设计:五张核心表撑起主链路

这套源码的表通常不止五张,但主链路靠这五张就能跑通。下面是我按常见实现整理的核心表结构,字段名可能和具体源码有出入,但逻辑一致。

表名作用关键字段
user患者/管理员账号id, username, password, role
doctor医生信息id, name, dept_id, title
schedule排班与号源id, doctor_id, work_date, time_slot, total, remain, version
appointment挂号记录id, schedule_id, patient_id, status, create_time
department科室id, name, location

schedule表的remain和version是并发控制的核心,appointment表的status驱动后续支付和退号。退号时不是删记录,而是把status改成已取消,同时schedule.remain + 1、version + 1。这样号源能回滚,记录也可追溯。

注意:如果源码里退号是直接delete挂号记录,说明它没考虑审计和号源回滚的一致性,答辩时容易被追问。

3. 本地跑通这套 SSM 挂号系统:环境、建库、启动三步走

3.1 环境准备:JDK、Maven、Tomcat、MySQL 的版本对齐

SSM 项目对版本比较敏感,尤其是 Spring 和 MyBatis 的兼容性。常见做法是 JDK 8、Maven 3.6、Tomcat 8.5、MySQL 5.7 或 8.0。MySQL 8.0 要注意驱动包用mysql-connector-java 8.x,连接 URL 加时区和 SSL 参数,否则启动就报错。

# 检查环境 java -version # 期望 1.8.x mvn -v # 期望 3.6.x mysql --version # 期望 5.7.x 或 8.0.x

如果 MySQL 是 8.0,URL 写成jdbc:mysql://localhost:3306/hospital?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false。serverTimezone不写会报时区错误,useSSL=false避免本地连接时的 SSL 警告。这两个参数是新手最容易漏的。

3.2 建库与导入:先跑通 SQL 再动代码

源码包里一般有sql目录或.sql文件。导入顺序是先建库、再建表、最后插初始数据。用 Navicat 或命令行都行,命令行更稳。

# 登录 MySQL mysql -u root -p # 建库 CREATE DATABASE hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 导入(在系统命令行执行,不是 MySQL 交互里) mysql -u root -p hospital < hospital.sql

导入后先查一下核心表有没有数据:select count(*) from doctor;、select count(*) from schedule;。如果排班表是空的,挂号页面就没号可挂,需要手动插几条测试数据。常见做法是插未来三天的排班,remain设成 10 左右,方便测试扣减。

-- 插入测试排班 INSERT INTO schedule (doctor_id, work_date, time_slot, total, remain, version) VALUES (1, '2025-06-01', '上午', 10, 10, 0), (1, '2025-06-01', '下午', 10, 10, 0), (2, '2025-06-02', '上午', 10, 10, 0);

version初始为 0,每次扣减加 1。time_slot用字符串存“上午/下午”是为了简单,正规做法是用时间段枚举或起止时间字段,但毕业设计里字符串够用。

3.3 改配置、起 Tomcat、验证主链路

数据库通了之后,改jdbc.properties或applicationContext.xml里的连接信息,然后mvn clean package打包,把 war 丢进 Tomcat 的webapps,启动。

mvn clean package -DskipTests cp target/hospital.war $TOMCAT_HOME/webapps/ $TOMCAT_HOME/bin/startup.sh

启动后访问http://localhost:8080/hospital/,用初始账号登录(常见是 admin/123456 或源码说明文档里写的)。验证顺序:先看科室列表能不能加载,再看医生排班能不能显示,最后走一遍挂号—查看记录—退号。退号后回到排班页,确认remain加回来了。这条链路走通,说明环境没问题,可以开始改功能了。

提示:如果页面 404,先看 war 包名和访问路径是否一致;如果 500,看 Tomcat 日志里是不是数据库连接失败或 MyBatis 映射文件没找到。

4. 挂号、退号、排班三个核心功能的实现细节与参数

4.1 挂号接口:参数校验与防重复提交

挂号接口通常接收scheduleId和patientId两个参数。前端传参后,Controller 先做非空校验,Service 再做业务校验。防重复提交有两层:一层是前端按钮置灰,一层是后端查重。前端不可靠,后端查重才是底线。

@RequestMapping("/appointment/create") @ResponseBody public Result create(@RequestParam Long scheduleId, @RequestParam Long patientId) { if (scheduleId == null || patientId == null) { return Result.fail("参数缺失"); } return appointmentService.createAppointment(scheduleId, patientId); }

参数说明:scheduleId对应排班表主键,patientId从登录会话里取更安全,源码里如果让前端传patientId,存在越权风险,答辩时可以主动提“我会改成从 session 取”。Result是统一返回体,包含code、msg、data三个字段,前端根据code判断成功失败。

4.2 退号逻辑:状态回滚与号源归还的顺序

退号不是简单改状态,要保证“挂号记录状态”和“排班号源”同时更新。常见做法是放在同一个 Service 方法里,用事务包住。

@Transactional public Result cancelAppointment(Long appointmentId) { Appointment appt = appointmentMapper.selectById(appointmentId); if (appt == null || appt.getStatus() != 0) { return Result.fail("记录不存在或不可取消"); } // 1. 改挂号记录状态 appt.setStatus(2); // 2=已取消 appointmentMapper.updateStatus(appt); // 2. 号源归还 scheduleMapper.increaseRemain(appt.getScheduleId()); return Result.ok("退号成功"); }

@Transactional保证两步要么都成功要么都回滚。increaseRemain的 SQL 是update schedule set remain = remain + 1, version = version + 1 where id = ?。注意这里也要加version,否则和挂号扣减并发时可能出问题。退号状态用 2 而不是删除,保留记录方便对账。

4.3 排班管理:时间槽与号源数量的配置方式

排班是管理员在后台配的,核心是“医生 + 日期 + 时段 + 号源数”。源码里一般是一个表单,提交后插入schedule表。时段常见用“上午/下午”两个值,也有用具体时间的。号源数total和remain初始相等,退号只动remain,不动total。

参数含义建议值
doctor_id医生主键从医生列表选
work_date出诊日期未来 7 天内
time_slot时段上午/下午
total总号源10~30
remain剩余号源初始等于 total
version乐观锁版本初始 0

排班一旦有人挂号,就不建议再改total,否则已挂号和总号源对不上。要加号就新增一条排班,或者只加remain并同步加total。这个细节在说明文档里不一定写,但改代码时要注意。

注意:如果源码里排班的remain是每次查询时动态算的(用 total 减已挂号数),那就不需要version,但性能差一些。两种做法都能用,关键是别混用。

5. 这套源码最容易翻车的五个地方:避坑与排查

5.1 现象:挂号成功但号源没减,或者减了没记录

原因通常是 Service 方法没加@Transactional,或者扣减号源和插入记录之间抛了异常但没回滚。也有可能是 MyBatis 的update返回影响行数没判断,扣减失败也继续插记录。

解决:给挂号方法加@Transactional,扣减后判断rows > 0再插记录,否则抛异常触发回滚。检查 Spring 配置里事务管理器是否生效,<tx:annotation-driven>有没有开。

5.2 现象:两个人同时挂最后一个号,都成功了

原因是没有并发控制,或者用了乐观锁但没判断影响行数。update ... where remain > 0如果没带version,两个请求可能都读到remain=1,都执行扣减,结果remain变成 -1。

解决:扣减 SQL 带上version条件,Service 里判断返回行数,为 0 就提示重试。或者用update ... set remain = remain - 1 where id = ? and remain > 0,靠数据库行锁兜底。两种选一种,别只靠先查后改。

5.3 现象:MySQL 8.0 启动报时区错误或 SSL 警告

原因是连接 URL 没加serverTimezone和useSSL。MySQL 8.0 默认时区是 UTC,Java 取到的时间会差 8 小时,排班日期可能对不上。

解决:URL 加serverTimezone=Asia/Shanghai&useSSL=false。如果还报驱动类找不到,检查pom.xml里mysql-connector-java版本是不是 8.x,驱动类名是com.mysql.cj.jdbc.Driver而不是老的com.mysql.jdbc.Driver。

5.4 现象:页面能打开但列表没数据,控制台报 MyBatis 映射错误

原因是 Mapper XML 没被扫描到,或者namespace和接口全限定名不一致。SSM 里 Mapper XML 通常放在resources/mapper下,需要在applicationContext.xml里配mapperLocations。

解决:检查mapperLocations的路径通配符,确认 XML 文件名和接口名对应,namespace写全限定名。改完重新mvn package,别只重启 Tomcat,XML 在 war 包里。

5.5 现象:退号后号源加回来了,但再挂同一个号提示“已预约”

原因是查重逻辑只按patientId + scheduleId查,没排除已取消的记录。退号后记录还在,状态是 2,查重时把已取消的也算进去了。

解决:查重 SQL 加and status != 2,只统计有效挂号。或者退号时把记录标记为已取消并单独存历史表,主表删除。前者改动小,推荐。

6. 从能跑到能答辩:改哪三处让这套源码更像你的作品

第一处改号源扣减的并发控制。源码里如果只用了先查后改,你把它改成带version的乐观锁,并在论文里写清楚“为什么需要乐观锁、和悲观锁的区别”。这一处改动小,但能体现你对并发有理解,答辩时是加分项。

第二处改退号后的号源回滚与查重排除。把查重 SQL 加上状态过滤,退号方法加事务,保证状态和号源一致。然后自己写一个测试:挂一个号、退掉、再挂同一个号,看能不能成功。这个测试过程写进论文的“测试章节”,比截图一堆页面有用。

第三处改登录态取 patientId。源码如果让前端传patientId,你改成从 session 或 token 里取,避免越权。改动涉及 Controller 和前端传参,工作量不大,但能讲“安全性考虑”。

// 从 session 取当前用户,替代前端传 patientId HttpSession session = request.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { return Result.fail("请先登录"); } Long patientId = user.getId();

这三处改完,这套源码就不再是“网上下的”,而是你能讲清楚每一处为什么这么写的作品。我自己的习惯是:拿到任何一套毕业设计源码,先跑通主链路,再找三个能体现技术判断的点改掉,最后把改动和测试写进文档。这样答辩时被问到“你做了什么”,有话可说。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询