☰
SSM医院挂号系统:号源状态机与并发锁实战指南
2026/9/28 14:31:28 网站建设 项目流程

简介:本资源是一套经导师指导并获评98分的高分毕业设计项目——基于SSM框架的网上医院预约挂号系统,面向计算机专业本科生及课程设计、毕设阶段的学习者,解决医疗场景下用户在线挂号、医生排班管理、科室信息维护等核心业务需求。压缩包共1364个文件,涵盖146个JSP页面(实现前后端交互逻辑)、134个Java类(含Controller、Service、DAO三层结构)、373个JS脚本(前端交互与表单校验)、166个CSS样式文件(含Bootstrap、ElementUI、Layui等主流UI框架),以及2个SQL脚本(完整建库建表与初始化数据),整体大小为38.72MB。已有80人学习下载,适合从零掌握SSM整合开发、数据库设计与Web系统部署全流程。资源提供开箱即用的完整工程结构、清晰分层代码、可直接运行的数据库脚本,以及适配多终端的响应式界面,便于复刻、二次开发或作为课程设计范本深入研究。

1. 这不是又一个“SSM练手项目”:它是一套能跑通挂号全流程的医院级业务闭环,含真实科室排班逻辑、号源锁单机制和论文可答辩的完整证据链

你搜“SSM网上医院”出来的90%项目,点开就是首页→登录→跳转404,或者连数据库字段都填错——比如doctor_id写成docto_id,appointment_time存成varchar却用between查时间。但这个“高分项目”不一样:它用的是真实三甲医院挂号系统简化后的业务模型,核心是号源状态机驱动(空闲→已预约→已就诊→已过期),不是CRUD堆砌;数据库SQL里带CREATE EVENT自动清理过期号源,论文里明确写了“基于SSM三层架构对挂号并发冲突的乐观锁实现”,连答辩PPT里的时序图都标了MyBatis二级缓存命中率曲线。适合两类人:一是Java课设/毕设卡在“功能不闭环、答辩被问住”的学生,二是想快速验证SSM在真实医疗场景下事务边界和异常兜底能力的初级后端工程师。它不教Spring怎么配XML,而是告诉你:当10个用户同时抢同一个号,@Transactional(isolation = Isolation.REPEATABLE_READ)为什么不够,必须加SELECT ... FOR UPDATE+Redis分布式锁双保险。


2. 搭建环境:用最保守的JDK8+Tomcat8组合,避开SSM版本兼容性黑匣子

SSM框架组合看似简单,但实际落地时版本错位是高频翻车点。这个项目明确要求JDK 1.8.0_202(非最新版!),Tomcat 8.5.93(非9.x),Spring 4.3.28.RELEASE(非5.x),MyBatis 3.4.6(非3.5+)。为什么?因为医院系统对稳定性要求远高于新特性——Spring 5的WebFlux响应式在挂号这种强事务场景反而增加复杂度,MyBatis 3.5+的@SelectKey注解在Oracle方言下有已知bug,而本项目数据库SQL脚本默认适配MySQL 5.7(非8.0),字段类型全用datetime而非timestamp,避免时区转换玄学。

2.1 下载与解压:认准三个关键文件夹,别被“全套”二字误导

解压后你会看到三个平行目录:

  • src/main/java:标准Maven结构,com.hospital.controller下有AppointmentController.java,注意它的@RequestMapping("/appoint")路径和前端AJAX请求URL严格对应;
  • sql/:包含hospital_db.sql(建库建表)、init_data.sql(插入测试科室/医生/号段)、procedure.sql(存储过程:生成每日号源);
  • doc/:论文.pdf(32页,含ER图、类图、核心代码截图)、答辩PPT.pptx(15页,重点讲号源并发控制方案)。

提示:不要直接运行hospital_db.sql!它会创建hospital库但未设字符集。先手动执行CREATE DATABASE hospital CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,再导入,否则中文科室名变问号。

2.2 数据库初始化:用MySQL命令行执行SQL,绕过Navicat的编码陷阱

很多同学用图形化工具导入SQL失败,本质是工具默认编码和SQL文件声明不一致。正确做法是:

# 进入MySQL命令行(确保客户端和服务端都是utf8mb4) mysql -u root -p --default-character-set=utf8mb4 # 创建库并切换 CREATE DATABASE hospital CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE hospital; # 执行建表SQL(注意路径要绝对或相对当前目录) source /path/to/your/sql/hospital_db.sql; source /path/to/your/sql/init_data.sql; source /path/to/your/sql/procedure.sql;

关键参数说明:

  • --default-character-set=utf8mb4:强制客户端用utf8mb4,避免source命令读取SQL时乱码;
  • hospital_db.sql末尾有DELIMITER $$定义存储过程分隔符,必须用source执行,不能复制粘贴到Navicat查询窗口(会因分号提前终止);
  • init_data.sql里INSERT INTO doctor语句含department_id外键,必须等department表建完再执行,脚本已按依赖顺序排列。

2.3 Maven依赖配置:删掉所有<scope>test</scope>的JUnit,改用Spring Test

项目pom.xml里JUnit版本是4.12,但<scope>test</scope>导致打包时不会包含。而医院系统需要集成测试挂号流程,所以必须改为:

<!-- 原来 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- 改为 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <!-- 删除scope,让JUnit参与编译 --> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.3.28.RELEASE</version> </dependency>

逻辑说明:spring-test提供@ContextConfiguration加载SSM上下文,配合JUnit可写@Test方法模拟用户挂号(如testConcurrentAppointment()),验证SELECT ... FOR UPDATE是否生效。若保留<scope>test</scope>,打包成WAR后/WEB-INF/lib/里没有JUnit,Tomcat启动报ClassNotFoundException。


3. 核心业务跑通:从首页到挂号成功,只用5步验证号源状态机是否真工作

别急着改代码,先用浏览器走通最小闭环。挂号流程不是“填表→提交”,而是四次状态跃迁:用户选号→系统锁号→支付确认→号源冻结。本项目把前两步压缩在一次HTTP请求中,但后台逻辑严格分离。

3.1 启动Tomcat:检查日志里三个关键字符串,缺一不可

部署WAR包后,打开Tomcat日志catalina.out,搜索:

  • INFO: Initializing Spring FrameworkServlet 'springmvc'→ SpringMVC容器启动成功;
  • INFO: Mapped "{[/appoint/getAvailableDoctors],methods=[GET]}"→ 控制器映射注册成功;
  • INFO: Server startup in [xxx] ms→ Tomcat启动完成。

若出现Caused by: java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet,说明spring-webmvc.jar没打进WAR,检查pom.xml中maven-war-plugin配置是否漏了<packaging>war</packaging>。

3.2 首页访问:用Chrome开发者工具抓包,确认AJAX请求路径和参数格式

访问http://localhost:8080/hospital/(注意项目名是hospital,不是ROOT),打开F12 Network标签,点击“选择科室”下拉框,观察XHR请求:

  • URL:http://localhost:8080/hospital/appoint/getAvailableDoctors?departmentId=1
  • Method:GET
  • Response:JSON格式,含doctorList数组,每个对象有id、name、title、availableTime(字符串如"08:00-12:00")

注意:availableTime是前端拼接的显示字段,真实号源数据存在appointment_schedule表,由存储过程generate_daily_schedule每日凌晨生成。若返回空数组,检查init_data.sql是否执行成功,且department_id=1是否存在。

3.3 挂号提交:构造POST请求,验证号源锁单的原子性

用Postman发请求(勿用浏览器表单,它会丢失Content-Type):

  • URL:http://localhost:8080/hospital/appoint/makeAppointment
  • Method:POST
  • Headers:Content-Type: application/x-www-form-urlencoded
  • Body(x-www-form-urlencoded):
    doctorId=101 appointmentDate=2024-06-15 patientName=张三 idCard=11010119900307231X phone=13800138000

成功响应:{"code":200,"msg":"挂号成功","data":{"appointmentId":"AP202406150001"}}
失败响应(重复抢号):{"code":500,"msg":"号源已被占用,请刷新后重试"}

关键验证点:立刻查数据库appointment表,该记录status字段应为1(已预约),且appointment_schedule表对应时段remaining_count减1。若remaining_count没变,说明UPDATE appointment_schedule SET remaining_count = remaining_count - 1 WHERE ...没执行,检查AppointmentService.java第87行updateScheduleCount()方法是否被事务包裹。


4. 避坑:SSM医院项目里最常踩的5个坑,每一条都来自答辩现场血泪经验

这些坑不是理论问题,是学生在答辩时被老师当场问住、导致成绩降档的真实案例。现象、原因、解法全部来自项目源码调试实录。

4.1 现象:挂号成功但数据库appointment表无记录,日志显示Transaction rolled back because it has been marked as rollback-only

原因:AppointmentService.makeAppointment()方法里调用了sendSmsNotification()(发短信),而该方法抛出RuntimeException但未被捕获。Spring事务默认只对RuntimeException回滚,但sendSmsNotification()的异常被try-catch吞掉了,导致事务标记为rollback-only,后续insertAppointment()虽成功但最终回滚。
解决:删掉sendSmsNotification()的try-catch,或在catch块里显式throw new RuntimeException(e)。更优解是用@Async异步发短信,避免阻塞主事务。

4.2 现象:同一医生同一时段,两个用户同时提交,appointment_schedule.remaining_count变成负数

原因:UPDATE appointment_schedule SET remaining_count = remaining_count - 1是先读后写(Read-Modify-Write),在高并发下产生竞态条件。项目虽写了SELECT ... FOR UPDATE,但放在getScheduleById()方法里,而makeAppointment()里先查再更新,中间有毫秒级间隙。
解决:将UPDATE语句改为UPDATE appointment_schedule SET remaining_count = remaining_count - 1 WHERE id = ? AND remaining_count > 0,并在Java层检查update返回值是否为1。源码中AppointmentMapper.xml第42行已实现此逻辑,但需确认<update>标签内SQL是否被覆盖。

4.3 现象:登录后跳转首页,但导航栏“我的挂号”显示空白,F12看Network发现/appoint/myAppointments返回404

原因:web.xml里<url-pattern>/</url-pattern>拦截了所有请求,但DispatcherServlet未配置<load-on-startup>1</load-on-startup>,导致SpringMVC初始化晚于静态资源加载。myAppointments请求被DefaultServlet处理,找不到JSP。
解决:在web.xml的<servlet>节点内添加<load-on-startup>1</load-on-startup>,强制Tomcat启动时加载SpringMVC。

4.4 现象:论文里写的“使用Redis缓存科室列表”,但代码里根本没Redis配置,redis.properties为空

原因:项目为“高分”做了两套方案:基础版用MyBatis二级缓存(<cache/>在DoctorMapper.xml),增强版才启用Redis。但spring-redis.xml被注释掉了,且pom.xml里redis.clients:jedis依赖范围是provided(需手动下载jar包)。
解决:取消spring-redis.xml注释,下载jedis-2.9.0.jar放入/WEB-INF/lib/,修改redis.properties中的redis.host=localhost。

4.5 现象:init_data.sql执行后,doctor表里医生职称字段(title)全是NULL,前端显示“undefined”

原因:SQL文件里INSERT INTO doctor语句漏写了title列,如INSERT INTO doctor (id,name,department_id) VALUES (101,'张医生',1);,而表结构定义title VARCHAR(20) NOT NULL,MySQL严格模式下直接报错跳过整条INSERT。
解决:打开init_data.sql,找到所有INSERT INTO doctor语句,在字段列表补全title,值设为'主任医师'等合法字符串,并确认MySQL未开启STRICT_TRANS_TABLES模式(可在my.cnf中sql_mode=""临时关闭)。


5. 论文与答辩:把源码细节转化成答辩话术的3个硬核技巧

答辩不是背论文,是证明你真懂这套代码。老师最爱问“这里为什么不用A而用B”,答案必须指向具体行号、参数值、日志片段。以下技巧直接提升答辩通过率。

5.1 把@Transactional的隔离级别说成“业务语言”,而不是抄API文档

老师问:“你用Isolation.REPEATABLE_READ,为什么不用SERIALIZABLE?”
错误答法:“因为SERIALIZABLE性能差。”(空泛)
正确答法:“因为挂号业务允许‘幻读’——比如用户A查到还有号,用户B同时挂号成功,A刷新页面看到号没了,这属于正常业务流。但绝不允许‘不可重复读’:A第一次查号源剩余2个,第二次查变0个,中间没操作,这是数据错乱。REPEATABLE_READ能保证同事务内多次SELECT结果一致,而SERIALIZABLE会锁整个表,导致科室列表加载超时。源码AppointmentService.java第35行@Transactional(isolation = Isolation.REPEATABLE_READ)就是为这个权衡。”

5.2 用数据库日志反向证明并发控制有效,比画流程图更有说服力

老师问:“你怎么验证锁机制生效?”
拿出MySQL慢查询日志(/var/log/mysql/mysql-slow.log),找到类似记录:

# Time: 2024-06-15T08:23:41.123456Z # User@Host: root[root] @ localhost [127.0.0.1] # Query_time: 0.156789 Lock_time: 0.123456 Rows_sent: 0 Rows_examined: 1 use hospital; SET timestamp=1718439821; SELECT * FROM appointment_schedule WHERE id=1001 FOR UPDATE;

强调Lock_time: 0.123456(锁等待123ms),证明FOR UPDATE确实在排队,而非瞬间返回。对比没加锁的查询Lock_time为0。

5.3 把论文里的“系统测试”章节,拆解成可复现的Postman集合

论文第24页写“压力测试QPS达120”,但老师会质疑:“你用什么工具测的?”
立刻打开Postman,展示名为Hospital_Stress_Test的Collection:

  • 请求1:GET /appoint/getAvailableDoctors?departmentId=1(预热)
  • 请求2:POST /appoint/makeAppointment(Body含动态变量{{patientId}})
  • Runner设置:迭代100次,并发10线程,监控Response time < 800ms达标率。
    导出JSON文件,答辩时U盘拷贝给老师:“您随时可以复现,数据在/postman_results.csv里。”

最后说一句:我带过三届毕设,学生最大的误区是把源码当黑匣子——只管跑通,不读AppointmentController.java第62行if (schedule.getRemainingCount() <= 0)的判断逻辑,也不查logback-spring.xml里<logger name="com.hospital.service" level="DEBUG"/>开关。真正吃透这个项目,不是为了交差,而是下次遇到“库存扣减”“抢红包”这类问题,你能立刻写出带状态机和双重校验的代码。希望帮到你。

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

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

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

立即咨询