SSM+MySQL医院预约挂号系统:架构、部署与并发控制实战解析
发布时间:2026/9/13 15:00:06 作者:尧图编辑部 阅读量:1,286

简介这套基于 SSM 与 MySQL 的医院预约挂号系统 Java 项目源码面向计算机专业毕业设计、课程设计场景也适合正在学习 SSM 整合开发的人员参考项目覆盖医院号源管理核心流程包含个人中心、用户管理、科室信息管理、医生管理、出诊信息管理、预约时间段管理、挂号预约管理、问题反馈与问题解答、系统管理等模块形成从科室配置、医生排班到用户挂号、反馈处理的后台运维闭环。资源包共 1386 个文件压缩后约 27.49MB。其中 135 个 Java 文件处理后端业务逻辑169 个 JSP 页面搭配 358 个 JS 脚本与 145 个 CSS 样式完成前端展示与交互2 个 SQL 文件提供数据库建表及初始数据此外还有 XML 配置、说明文档以及 Bootstrap、ElementUI 等前端组件和 png/jpg/gif 图片素材方便导入开发工具直接运行按模块阅读源码或二次开发。目前该资源已有 104 人学习浏览。下载后可直接获得可运行项目、数据库脚本和配套说明文档对理解 SSM 分层架构、理清预约挂号业务实体关系有很大帮助也能快速撑起毕业设计所需的功能演示和答辩材料。1. 医院预约挂号系统源码ssmmysql为什么一直是毕业设计里的常青树当你在压缩包里看到“医院预约挂号系统源码ssmmysql说明文档LW.zip”这个名字时第一反应往往不是“能做什么”而是“哪里要改才能答辩”。说实话这类题目的价值不在于页面多炫而在于它把 Spring、SpringMVC、MyBatis 这三层框架套进了一个有明确业务规则的场景用户注册登录、医生排班、预约挂号、取消挂号、后台管理。预约挂号的并发写入和状态变更比普通 CRUD 更能讲清楚事务、锁和 SqlSession 的生命周期。对 Java 后端岗位面试来说这个项目能直接对应“SSM 框架怎么协同”“MySQL 索引怎么设计”“怎么防止重复提交”这类高频问题。对 5 年以上的人来说看这套源码的意义不在 README而是看它的表结构、事务边界和缓存策略是否经得起推敲。下面按“架构—部署—代码链路—答辩技巧”的顺序把这套源码的用法和坑位一次说清。2. SSM 环境与数据库设计先看懂医院预约挂号系统的骨架要跑起来先看清楚坐标。这一章解决的是“这个项目为什么长这样”的问题。2.1 SSM 三层架构在预约业务里的分工在 SSM 体系里Spring 管对象的创建和依赖注入SpringMVC 管 HTTP 请求的分发和参数绑定MyBatis 管 SQL 与 Java 对象之间的映射。落到医院预约挂号系统上这个分工很直接SpringMVC 接收用户的/login、/register、/appointment/book等请求并把HttpServletRequest里的参数转换成 Controller 方法的入参。Spring 的 Service 层负责业务规则比如用户登录时校验密码预约时检查号源数量和是否重复挂号取消预约时改状态。MyBatis 只做两件事根据 Mapper 接口生成代理实现把接口方法调用转成 SQL 执行并把 ResultSet 映射成实体类。典型的项目的包结构大致如下以com.hospital.*为例包名对应角色典型内容com.hospital.controllerSpringMVC 控制层UserController、DoctorController、AppointmentControllercom.hospital.serviceSpring 业务层UserService、AppointmentService、ScheduleServicecom.hospital.dao/mapperMyBatis Mapper 接口UserMapper、ScheduleMapper、AppointmentMappercom.hospital.pojo/entity实体类User、Doctor、Schedule、Appointmentcom.hospital.util工具类MD5 加密、分页封装、统一结果返回分层翻译成代码就是Controller 不写 SQLMapper 不扛业务Service 不去绑定HttpSession。这套规则在刚接手时可能觉得绕但在你要做“医生排班导出 Excel”或“按科室统计预约量”这类需求时分层带来的可替换性就体现出来了。项目中关于数据源的配置通常放在jdbc.properties或db.properties下面是一份常见配置注意不同 MySQL 版本之间的差异。jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/hospital_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456serverTimezoneAsia/Shanghai只有在 MySQL 8.x 下才需要显式声明MySQL 5.7 通常不校验这个参数。如果你用的驱动是com.mysql.cj.jdbc.Driver对应的就是 MySQL 8 客户端驱动com.mysql.jdbc.Driver则是 5.x 时代的写法。判断源码属于哪个时代直接看这两个类名就能十拿九稳。提示不要一上来就跑mvn spring-boot:run。这个项目如果是 Servlet 模式通常会打成 war 包扔给 Tomcat如果是 Spring Boot 改造版才适合直接启动 main 方法。先看web.xml或pom.xml的 packaging 字段。2.2 MySQL 表结构挂号记录、医生排班怎么建模预约挂号的业务实体不复杂核心是医院科室、医生、排班、挂号这几张表。常见表结构设计为五张admin、user、department或dept、doctor、doctor_schedule、appointment。下面把关键字段列出来表名关键字段说明userid、username、password、real_name、phone患者用户密码建议存 MD5 或 BCrypt 摘要doctorid、name、dept_id、title、intro医生信息dept_id关联科室表doctor_scheduleid、doctor_id、work_date、time_slot、total_count、remain_count、version每天每半天的号源数量和剩余量appointmentid、user_id、schedule_id、status、create_time预约行为status1有效0取消departmentid、name、description科室appointment表只存“谁在哪天约了哪个排班”不冗余医生姓名和科室是为了避免医生改名或排期调整时出现数据不一致。但实际开发中很多源码会故意冗余doctor_name因为列表页查询时减少一个 join这就是“空间换时间”的取舍。排班表可能是整个系统里最容易出并发问题的地方。下面这段 SQL 是常见建表脚本的精简版CREATE TABLE doctor_schedule ( id int(11) NOT NULL AUTO_INCREMENT, doctor_id int(11) NOT NULL, dept_id int(11) NOT NULL, work_date date NOT NULL, time_slot varchar(20) NOT NULL COMMENT AM/PM, total_count int(11) DEFAULT 20, remain_count int(11) DEFAULT 20, version int(11) DEFAULT 0, PRIMARY KEY (id), KEY idx_doctor_date (doctor_id, work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里remain_count是剩余号数version是更新时用来做乐观锁的版本号。有些项目没有version而是直接用remain_count 0作为更新条件也能起作用两种方式在后面的并发部分会细聊。现实中的排班可能是每周固定重复因此排班日期不应该存在timestamp而是date并且time_slot如果只有“上午/下午”两个值更推荐用tinyint加字典解释而不是字符串。存储空间不是主要问题问题在于用字符串拼接时间槽时容易混入中英文标点导致后续统计脚本踩坑。2.3 从 ER 图到外键设计与索引选择按毕业设计的说明文档LW要求ER 图里通常会画清楚user与appointment、doctor_schedule与doctor之间的关系。但很多源码在创建表时并没有真正写FOREIGN KEY而是只保留逻辑关联字段。这是合理的外键会降低高频写入时的性能也会让删库、导库变得麻烦。索引的选择要按查询场景走。这个系统里最大的查询压力来自两类 SQL用户查当日某个科室的排班列表WHERE dept_id ? AND work_date BETWEEN ? AND ?后台管理员统计某医生的预约量WHERE doctor_id ? AND status 1对第一类复合索引(dept_id, work_date)会很有用对第二类(doctor_id, status)比较常用。上面建表脚本里的idx_doctor_date就是只为“医生日期”的查询设计的一个例子。做索引不要贪多索引多了插入和更新时维护成本会指数上升。提示在 MySQL 8 中可以用EXPLAIN验证查询是否走索引。例如EXPLAIN SELECT * FROM doctor_schedule WHERE doctor_id1 AND work_date2025-06-10;重点看key和rows字段。如果typeALL说明是全表扫描务必复查索引。3. 把源码跑起来从 JDK、Tomcat 到 MySQL 的配置清单第 2 章把架构和表关系理清了这一章直接落地把所有可能踩到的运行环境问题放到台面上。3.1 本机环境准备JDK8、Maven 3.6、Tomcat8、MySQL 5.7这个项目在 SSM 背景下最稳妥的组合是 JDK 1.8、Maven 3.6.x、Tomcat 8.5、MySQL 5.7。如果你电脑上已经装了 JDK 17 或 JDK 21直接跑老 SSM 项目会碰到一些兼容性问题一部分是因为 CGLIB 代理和高版本 JDK 的模块限制一部分是 Tomcat 版本过低。先做环境自检命令如下java -version mvn -version mysql --version如果mvn命令提示找不到要么没装 Maven要么没有配置M2_HOME和PATH。在 Windows 上常见的坑是Path环境变量里写了 Maven 路径但没把JAVA_HOME指到 JDK 目录导致mvn -version能出来但编译阶段报Unable to obtain javac。Tomcat 可以不用单独解压如果代码是基于 Servlet 的老式项目很多 IDE 在部署时可以直接嵌入 Tomcat。但如果你习惯手动把 war 包丢进 Tomcat 的webapps目录注意conf/server.xml里的URIEncoding最好显式设置为UTF-8否则中文参数传过来就是乱码。server.xml里连接器部分可以改成这样Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /connectionTimeout是建立连接后等待请求的最大毫秒数不是整个请求的超时时间调成 20000 是为了避免空闲连接占着线程。这个参数在某些源码包里被误写成 2000结果页面稍微慢一点就会出现 socket time out。环境组合兼容性常见问题JDK 8 Tomcat 8 MySQL 5.7最稳几乎不会遇到版本兼容问题JDK 11 Tomcat 9 MySQL 8.0基本可用需更换 MySQL 驱动并配置时区JDK 17 Tomcat 8.5不推荐反射访问java.lang模块会被拒绝CGLIB 代理可能报错MySQL 5.7 驱动 8.0需测试驱动升级后报Public Key Retrieval is not allowed需要在 URL 加allowPublicKeyRetrievaltrueMySQL 建议直接使用 5.7如果非要上 8.0需要把数据库驱动和serverTimezone一起改掉。另一种常见选择是用 MySQL 8 自带的大版本兼容包但对这种毕业设计源码选 5.7 能省下绝大多数无意义的时间成本。3.2 导入项目并修改数据库连接参数用 IDEA 打开源码时如果pom.xml在根目录直接选择 “Open” 让它识别为 Maven 项目。需要注意源码里可能同时存在多个pom.xml这种情况下通常是一个父工程加两个子模块选择父工程即可。导入后打开src/main/resources目录主要关注三个文件jdbc.properties、mybatis-config.xml、spring-mybatis.xml。有些项目把配置合并到了一个applicationContext.xml里顺序是spring-mvc.xml负责 Controller 扫描spring-mybatis.xml负责数据源和事务。第一次跑之前把数据库连接改成你自己的账号密码jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordroot然后把db.sql或hospital.sql导入 MySQLmysql -u root -p hospital_db db.sql如果导入过程中报Unknown database说明hospital_db还没创建先执行CREATE DATABASE hospital_db DEFAULT CHARACTER SET utf8mb4;再导。很多二次开发的人在导库时报错不是 SQL 文件有问题而是数据库字符集和 SQL 文本不一致导致中文乱码或语法报错。这里建议统一utf8mb4它兼容 emoji 和生僻字比utf8更保险。3.3 首次启动和验证动作配置完成后用 IDEA 的 Tomcat 插件启动如果用 war 包部署则先执行mvn clean package然后生成target/hospital.war复制到 Tomcat 的webapps下再运行bin/startup.shWindows 是startup.bat。启动日志如果出现Deployed application at context path / hospital说明项目没挂。接下来打开浏览器访问http://localhost:8080/hospital/login.jsp或项目约定的首页路径。建议按下面的顺序手动验证任何一个环节失败都不要急着改代码先看控制台或日志注册一个患者账号并且用 MD5 字符串对比一下数据库里存的是不是密文登录成功后进入预约页面看科室列表和医生排班是否显示点击某医生的“预约”成功后在后台或用户中心看到预约记录管理端登录看医生列表的增删改查是否正常。如果登录失败报 404 或 500最常见的原因有三个数据库连接串里的库名写错、Mapper XML 文件没被扫描到、静态资源路径和spring-mvc.xml里的mvc:resources配置对不上。一般排查时先看启动日志里有没有nested exception is ... SQLException有就是数据库问题如果日志里看到BeanCreationException或mapper相关报错就是 Spring 容器装配的问题。提示Tomcat 默认端口是 8080如果本机已经被占可以在 IDEA 的 Run Configuration 里修改 HTTP port也可以直接改server.xml。改端口后记得把浏览器地址一并对齐。4. 预约挂号的代码链路Controller、Service、Mapper 的参数细节把项目跑起来只是开始真正决定你答辩水平的是能否把一条预约链路讲清楚。4.1 从 Controller 到 Mapper 的调用链请求到达服务器后SpringMVC 的DispatcherServlet会根据RequestMapping找到对应 Controller 方法。下面是AppointmentController中典型的一段Controller RequestMapping(/appointment) public class AppointmentController { Resource private AppointmentService appointmentService; RequestMapping(/book) ResponseBody public Result book(RequestParam(scheduleId) Integer scheduleId, RequestParam(userId) Integer userId) { try { return appointmentService.bookAppointment(scheduleId, userId); } catch (Exception e) { return Result.error(e.getMessage()); } } }Resource优先按名称注入Autowired优先按类型注入这两种注解在很多项目里混用。在 SSM 项目里建议统一用一种避免两套装配规则叠加后出现诡异问题。ResponseBody会把返回对象序列化为 JSON所以 Response 类不能有循环引用字段否则 Jackson 会抛Infinite recursion。RequestParam(scheduleId)要求前端必须传一个名为scheduleId的参数如果传了空字符串会被转成 0如果没传会直接报MissingServletRequestParameterException。很多项目会加一个全局异常处理器把这类异常统一捕获否则前端会收到一堆不带业务语义的错误页。参数校验不应该只在 Controller 做一层。Controller 这一层更适合做基础校验比如scheduleId是否为空真正检查“这个排班是否可约”一定要落到 Service 层因为并发场景下Controller 里的判断只能防住普通用户防不住两个请求同时进入。4.2 挂号事务、扣减号源与并发控制挂号这个动作的业务逻辑是把“余号减一”和“插入预约记录”放在同一个事务里要么都成功要么都回滚。一个常见的 Service 实现是这样Transactional(rollbackFor Exception.class) public Result bookAppointment(Integer scheduleId, Integer userId) { DoctorSchedule schedule scheduleMapper.selectByPrimaryKey(scheduleId); if (schedule null || schedule.getRemainCount() 0) { return Result.error(号源不足); } int updated scheduleMapper.decreaseCount(scheduleId, schedule.getRemainCount() - 1, schedule.getVersion()); if (updated 0) { return Result.error(预约失败请重试); } Appointment appointment new Appointment(); appointment.setScheduleId(scheduleId); appointment.setUserId(userId); appointment.setStatus(1); appointmentMapper.insert(appointment); return Result.success(预约成功); }这段代码的精髓在decreaseCount的 SQL 上使用乐观锁避免两个线程同时读到remainCount1后都执行“减一”update iddecreaseCount UPDATE doctor_schedule SET remain_count #{remainCount}, version version 1 WHERE id #{scheduleId} AND version #{version} AND remain_count 0 /updateWHERE条件中带上version #{version}后如果两次请求同时读到同样的version第一次更新会把version改成新值第二次更新的影响行数就会变成 0。Java 侧通过判断updated 0直接返回失败避免超卖。Transactional(rollbackFor Exception.class)里的rollbackFor是必须写清楚的。Spring 声明式事务默认只在抛RuntimeException时回滚如果你在 Service 里把业务异常包装成受检异常并向上抛不加rollbackFor就可能导致预约记录写入后事务没有回滚余额却已经扣掉。排查这个问题时可以看方法体的异常类型和事务配置的匹配关系。4.2.1 悲观锁与乐观锁的选择如果使用SELECT ... FOR UPDATE它就是悲观锁在事务内把排班行锁住直到提交或回滚才释放。优点是实现逻辑简单缺点是并发高时锁等待时间变长而且如果忘记在事务内释放会让数据库连接池很快耗尽。上面这种“版本号影响行数”的写法属于乐观锁适合读多写少、冲突不多的预约场景。锁类型实现方式适用场景潜在问题悲观锁SELECT ... FOR UPDATE并发写入冲突极高锁等待导致连接池耗尽乐观锁version字段 影响行数判断读多写少冲突不多冲突时用户需要重试唯一索引防重(user_id, schedule_id)唯一键防重复预约重复点击时暴露原生报错不过乐观锁也有缺陷每次失败都需要用户重新点击预约。要解决这个问题可以在前端加一个按钮防抖或者在失败时自动重试一次但重试前必须重新查一次schedule的version否则第二次更新同样会失败。更复杂的项目会引入 Redis 预减库存但在毕业设计这个体量里MySQL 行锁加乐观锁已经完全够用。4.3 数据层 Mapper 的 SQL 写法与参数绑定MyBatis 中 Mapper 接口和 XML 文件通过命名空间绑定。接口方法的名字必须和 XML 中的id一一对应参数名前缀用的是#{}而不是${}。int decreaseCount(Param(scheduleId) Integer scheduleId, Param(remainCount) Integer remainCount, Param(version) Integer version);对应的 XML 里的parameterType通常省略MyBatis 会自动从方法签名推断。但需要注意的是当你用了Param注解后XML 里的参数必须带上前缀名否则会报Parameter scheduleId not found。这在老版本 MyBatis 3.4 之前的单参数场景下尤其明显单参数可以不写多参数就必须写。关于${}与#{}的选择所有外部输入都要走#{}预编译后数据库会把#{value}当成一个占位符而不是拼接 SQL。某些排序字段需要动态拼接ORDER BY这种场景确实避不开${}但一定要做白名单校验比如用一个 Map 把前端传的列名映射成固定值而不是把原始字符串直接拼进去。5. 源码自带的说明文档 LW 怎么用以及答辩前必须做的三个检查点这里的 LW 通常指的是毕业设计论文或综述文档它在整个压缩包里占的体积不小但很多人直到答辩前一周才打开。与其临时背概念不如把它当一份“需求对照清单”来用。5.1 对照说明文档检查表结构与代码的版本一致性很多压缩包里的说明文档是早期文稿表结构和代码里可能存在细微差异。先翻到 ER 图章节把图里的表名和开发环境里实际的SHOW TABLES输出逐行对照一遍。重点看两类字段第一类是create_time、update_time这类时间字段第二类是status和version这类逻辑字段。如果文档里的appointment表少了version字段而代码里 Mapper 使用了version那么运行时会直接抛出Unknown column错误这类问题要在答辩前修掉。5.2 安全与并发避免 SQL 注入和重复请求答辩演示时考官很可能瞥一眼你的 Mapper XML。把所有${}替换成#{}就是最直接的改进。给appointment表加上user_id和schedule_id的唯一索引能直接阻止同一个用户对同一个排班重复预约代码里只需要捕获DuplicateKeyException并转成友好提示即可。重复请求可以用一个前端开关解决点击预约后立即把按钮置灰直到接口返回再恢复。后端还可以做幂等性校验在表结构里给(user_id, schedule_id)加唯一索引这样重复插入会直接报Duplicate entry简单粗暴但有效。不过这个方法会让重复点击的用户看到数据库报错信息更温和的做法是用INSERT IGNORE或ON DUPLICATE KEY UPDATE把逻辑控制在 SQL 一层。5.3 答辩时如何讲清楚设计决策用三个问题自测你可以用下面三个问题来复盘这个项目如果两个患者同时预约同一个医生你的系统怎么保证不超过总号源为什么把doctor_schedule和appointment拆成两张表而不是直接给医生表加一个“已约人数”字段取消预约时除了把appointment.status改成 0为什么还要把remain_count加回 1在回答这三个问题时关键不是背出乐观锁、外键这些术语而是说清楚你的代码在什么样的并发假设下成立。如果你能拿出第 4 章那段decreaseCount的 SQL 和Transactional的事务配置作为证据考官会相信这套系统是你自己调试过的而不仅仅是把源码跑通后照着念。任何毕业设计源码的价值都不在于压缩包本身而在于你读懂它之后能够针对一个明确的业务问题做取舍。本文还有配套的精品资源点击获取