SSM医院预约挂号系统源码解析:三层架构与MySQL预约闭环 简介医院预约挂号系统是一套基于Java与SSM框架开发的毕业设计项目源码面向计算机相关专业毕业生、课程设计学生以及需要快速搭建Web管理系统的开发者。系统按管理员、医生、用户三种角色划分权限覆盖门诊信息管理、预约挂号、取消预约、改约通知、留言板等功能模块配套MySQL数据库脚本、说明文档与LW论文从后端逻辑到前端页面形成完整闭环。压缩包共1287个文件约26.78MB以Java源码、JSP页面、JavaScript脚本、CSS样式及SQL脚本为主另有大量图片资源支撑界面展示目录按Maven工程结构组织便于导入IDE直接运行调试。已有62人学习浏览适合需要参考完整业务流、学习SSM框架整合或直接用于毕业设计答辩的读者。1. 医院预约挂号系统的源码价值先看清它是“能跑”还是“能答辩”拿到一份“java毕业设计之医院预约挂号系统源码ssm后端mysql数据库前端说明文档LW).zip”第一反应不该是解压就启动而是先判断这一包东西能不能支撑起你要的结果。如果是为了毕业答辩结果分两层系统能跑通或者论文能自圆其说。二者缺一这份源码对你就只是压缩包里的死文件。这个项目的本质是一个典型的 SSM 三层架构管理系统Spring 管对象、SpringMVC 管请求路由、MyBatis 管数据库访问配合 MySQL 存储业务数据前端负责页面交互和接口调用。适合两类人一类是拿它做毕设底子、需要快速补功能改界面的在校生另一类是要在短时间内交付预约类小项目的开发者。它解决的核心问题很朴素——把“科室、医生、排班、预约、用户”这几张表之间的关系理清楚并把预约这个高频动作做成稳定、可验收的闭环。2. SSM 后端怎么搭项目结构与预约核心接口的落地写法2.1 为什么这类源码还在用 SSM而不是 Spring Boot热词里能看到 springboot4、mybatisplus 这类新东西但医院预约挂号这种毕业设计题目的源码主流仍然落在 SSM。原因不是 Spring Boot 不好而是这套组合对“毕业设计”这个场景太合适了。SSM 的分层极其规整Controller 只做参数接收和视图转发Service 写业务规则Mapper 只管 SQL。评阅老师展开项目结构时每一层都能对上软件工程的经典理论工作量一眼可见。另外一个实际原因是部署环境很多学校机房还是 JDK 8 配 Tomcat 8SSM 打成 WAR 包丢进 webapps 就能跑不需要额外配置内嵌容器。你在简历上写“熟悉 SSM 框架”至少不会翻车面试官顺着问 MyBatis 的 #{} 和 ${} 区别、Spring 的事务传播机制这些都是有标准答案的 java 基础题比聊微服务容易被追问到底层源码轻松得多。2.2 项目结构怎么分三层职责的先看清楚再动手解压源码后先别急着点运行。第一件事是看目录结构是否完整。一个能跑起来的 SSM 项目必须同时存在四个东西源码目录src/main/java、配置文件目录src/main/resources、前端页面目录src/main/webapp、数据库脚本通常是 sql 文件或 db 目录下的 .sql。我见过不少从网上下载的压缩包代码和页面都有唯独少了建表脚本这种项目到手就得自己反推表结构时间成本很高。常见的分层结构长这样HospitalAppointment/ ├── pom.xml ├── src/main/java │ └── com/hospital/appointment │ ├── controller │ │ ├── DoctorController.java │ │ ├── AppointmentController.java │ │ └── UserController.java │ ├── service │ │ ├── AppointmentService.java │ │ └── impl/AppointmentServiceImpl.java │ ├── mapper │ │ └── AppointmentMapper.java │ ├── pojo │ │ ├── Doctor.java │ │ ├── Schedule.java │ │ └── Appointment.java │ └── common │ └── Result.java ├── src/main/resources │ ├── jdbc.properties │ ├── spring-mybatis.xml │ ├── spring-mvc.xml │ └── mapper/AppointmentMapper.xml ├── src/main/webapp │ ├── WEB-INF/jsp │ └── static └── sql └── hospital.sql注意看 pojo 包和 mapper 包是否一一对应。很多二手源码会在这里埋雷pojo 里的字段名和数据库列名不一致或者 Mapper.xml 里的 resultMap 没写导致查询结果全是 null。我的习惯是拿到源码先全局搜索resultType和resultMap确认每个查询方法的结果映射都声明清楚。另外jdbc.properties要检查数据库连接配置里面默认的用户名密码往往是 root/123456不改成你自己的根本连不上库。这个文件也是启动报错的重灾区后面部署章节再细说。2.3 预约挂号的三个核心接口排班查询、提交预约、取消预约整套系统的业务都围绕“预约”这个动作展开后端接口再花哨核心也就这三个。先说排班查询。它的本质是一个多条件动态 SQL患者选科室、选日期、看医生还有没有剩余号源。对应到 Mapper.xml 的写法select idfindAvailableSchedules resultTypecom.hospital.appointment.pojo.Schedule SELECT s.id, s.doctor_id, d.name AS doctor_name, d.title AS doctor_title, s.schedule_date, s.period, s.total_slots, s.booked_slots, (s.total_slots - s.booked_slots) AS available_slots FROM schedule s INNER JOIN doctor d ON s.doctor_id d.id where if testdeptId ! null AND d.dept_id #{deptId} /if if testscheduleDate ! null AND s.schedule_date #{scheduleDate} /if if testdoctorId ! null AND s.doctor_id #{doctorId} /if /where ORDER BY s.schedule_date ASC, s.period ASC /select这段 SQL 的逻辑核心是total_slots - booked_slots算出剩余号源前端页面直接拿这个差值显示“余号”状态不必在 Java 代码里再计算。where标签是 MyBatis 动态 SQL 的经典用法会自动处理“第一个条件前要不要加 AND”的问题。参数#{deptId}走的是预编译占位符能防 SQL 注入这也是 java 面试里经常会问到的点#{}会被解析成 JDBC 的?而${}是字符串拼接查表名动态排序不得不用${}查普通参数一律用#{}。提交预约的接口就没这么简单了它的难点在于事务。一个完整的预约动作要同时做两件事往 appointment 表插一条预约记录把 schedule 表的 booked_slots 加一。两件事要么都成功要么都失败。看源码里这个 Service 方法是否挂了事务注解是判断这份源码质量的一个重要信号Service public class AppointmentServiceImpl implements AppointmentService { Autowired private AppointmentMapper appointmentMapper; Autowired private ScheduleMapper scheduleMapper; Override Transactional(rollbackFor Exception.class) public Result createAppointment(Appointment appointment) { // 1. 校验号源是否还有余量避免用户提交时号源已被抢光 Schedule schedule scheduleMapper.selectById(appointment.getScheduleId()); if (schedule null) { return Result.error(排班不存在); } if (schedule.getBookedSlots() schedule.getTotalSlots()) { return Result.error(该时段号源已约满); } // 2. 插入预约记录状态默认为 0待就诊 appointment.setStatus(0); appointment.setCreateTime(new Date()); int insertRows appointmentMapper.insert(appointment); if (insertRows 0) { // 3. 更新排班已约人数 scheduleMapper.increaseBookedSlots(appointment.getScheduleId()); return Result.success(预约成功); } return Result.error(预约失败请稍后重试); } }方法上的Transactional(rollbackFor Exception.class)是关键。这里有一个很多人踩过的坑Spring 事务默认只在 RuntimeException 上回滚checked exception 不会触发回滚。rollbackFor Exception.class的含义是“任何异常都回滚”写得更稳。另外注意第 1 步的校验和第 3 步的更新并不是原子的两个请求同时进来时会出现“都通过了余量校验、都插入成功、booked_slots 只加了 1”的超卖问题。正式的方案要给 schedule 表的更新语句加WHERE booked_slots total_slots条件或者使用悲观锁SELECT ... FOR UPDATE。毕业设计源码里通常不会处理得这么深但你在答辩时能主动讲出这个并发风险并给出优化方案反而是一个加分亮点。取消预约的逻辑跟提交相反先查 appointment 的状态只有“待就诊”状态才能取消然后把状态改成 2已取消同时把 booked_slots 减一。这里要注意一个业务细节已就诊的号源不能退过期的号源不能退否则会破坏排班统计。Service 里用状态判断挡一层比在前端页面里 disable 按钮可靠得多。3. MySQL 数据库设计建表、状态流转与并发约号的三个坑3.1 核心表的最小集合五张表撑起整个预约业务看这套源码的数据库设计先别被脚本里几十张表吓住核心业务其实只有五张表用户表user、科室表department、医生表doctor、排班表schedule、预约表appointment。剩下的表大多是扩展功能或者论文凑字数的。建表脚本里值得重点研究的是 schedule 和 appointment 这两张表因为所有并发和状态问题都集中在它们身上CREATE TABLE schedule ( id INT NOT NULL AUTO_INCREMENT, doctor_id INT NOT NULL COMMENT 医生ID, schedule_date DATE NOT NULL COMMENT 出诊日期, period TINYINT NOT NULL COMMENT 1上午 2下午 3晚班, total_slots INT NOT NULL DEFAULT 20 COMMENT 总号源数, booked_slots INT NOT NULL DEFAULT 0 COMMENT 已约号数, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_doctor_date (doctor_id, schedule_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医生排班表; CREATE TABLE appointment ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL COMMENT 预约人ID, schedule_id INT NOT NULL COMMENT 排班ID, doctor_id INT NOT NULL COMMENT 医生ID, appoint_date DATE NOT NULL COMMENT 就诊日期, period TINYINT NOT NULL COMMENT 1上午 2下午 3晚班, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待就诊 1已完成 2已取消 3已过期, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user (user_id), KEY idx_schedule (schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约记录表;两张表的设计有几个细节值得学习。第一schedule 表用doctor_id schedule_date联合索引因为前端页面上最常见查询就是“某个医生某天的排班”走索引能避免全表扫描第二appointment 表把appoint_date和period冗余存了一份这看似违反数据库范式但实际是合理的——预约记录需要保留“当时约的是哪一天哪个时段”的快照不能等排班表数据变了再去关联查第三period用 TINYINT 而不是 VARCHAR后面扩展夜诊直接加一个数字即可不用改表结构。3.2 预约状态流转从待就诊到过期的四态切换预约状态是整个系统的业务中枢。源码里通常用整型常量表示状态而不会用字符串因为整型在 MySQL 里存储更省空间比较速度也更快。四个状态的流转路径是固定的新预约进来是“待就诊”患者按时到院后由后台改成“已完成”患者在就诊前手动取消变成“已取消”过了就诊日期系统仍未标记的自动变成“已过期”。流转的核心 SQL 是一个条件更新UPDATE appointment SET status 2 WHERE id #{appointmentId} AND status 0 AND appoint_date NOW();这条 SQL 的巧妙之处在AND status 0。它不只是过滤条件更是一道并发防线。如果两个请求同时调用取消接口第一个请求执行更新后 status 变成 2第二个请求的 WHERE 条件就匹配不上了影响行数为 0业务层通过判断返回行数就能知道“取消失败”。这就是乐观锁思想在业务状态上的应用——用一个状态字段当版本号不需要额外的 version 列。对应到 Service 层的处理逻辑int rows appointmentMapper.cancelAppointment(appointmentId); if (rows 1) { scheduleMapper.decreaseBookedSlots(scheduleId); return Result.success(取消成功); } return Result.error(取消失败该预约已不可取消);注意decreaseBookedSlots放在 update 成功之后这是顺序敏感的操作先改业务主单据再改辅助统计字段。如果反着写可能出现“号源已经释放但预约状态没改成功”的数据不一致。订单类系统基本都是这个套路先动主记录再动冗余统计。3.3 SQL 里最容易翻车的地方时间比较、字符集与索引失效医院预约系统对时间的处理比一般管理系统敏感得多。第一个坑是日期比较的字段类型。排班表里的出诊日期用 DATE 类型预约表里的就诊日期也用 DATE 类型但创建时间用 DATETIME。有些源码为了省事全部用 VARCHAR 存日期导致查询WHERE schedule_date 2025-06-01时走不了索引日期排序也会变成字符串排序。拿到源码后用下面这条 SQL 快速检查一下时间字段的类型SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA hospital_db AND COLUMN_NAME IN (schedule_date, appoint_date, create_time);第二个坑是字符集。建表语句里没写DEFAULT CHARSETutf8mb4的话会继承 MySQL 实例默认的 latin1 或 utf8存中文会出现“”的乱码。MyBatis 连 MySQL 时还要注意连接串有没有指定字符集jdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8。少了useUnicodetrue即使数据库是 utf8mb4应用层传进来的中文照样可能乱码。这是老生常谈的 mysql 问题但每届做毕设的人都会踩一遍。第三个坑是索引失效。很多源码在排班查询时会写WHERE DATE_FORMAT(schedule_date, %Y-%m-%d) #{date}这会导致索引失效因为函数包裹了索引列。正确写法是直接WHERE schedule_date #{date}日期格式转换是前端展示层的事不该出现在 SQL 的 WHERE 条件里。看到源码有这种写法顺手改掉也算是一个论文里能写的“系统优化点”。4. 前端联调登录会话、接口对接和页面改造的三件套4.1 前后端不分离的旧套路与改造新思路SSM 项目的年代决定了它的前端路子主要分两派。一派是 JSP 模板页面直接用${}表达式取后端塞进 ModelAndView 的数据这种做法的好处是跟 SSM 契合度最高源码里跳转和传参都是原生的部署到 Tomcat 就能跑几乎不用额外配置。另一派是前后端分离后端只出 JSON 接口前端用 HTML AJAX 调用页面里用 Vue 或者纯 JavaScript 渲染数据。热词里“前端sdk”“前端组件库”都是这一派的关键词。毕业设计源码给你的是哪一派决定你改造成本的高低。我见过不少源码号称“前后端分离”实际上只是把 JSP 换成了 HTML数据绑定还得靠 jQuery 手动拼字符串。这种半吊子方案的坑在于接口返回的 JSON 结构不稳定、错误提示不统一、前端无法处理后端异常。而纯正的后端分离项目中后端通常会封装一个统一的返回体public class Result { private Integer code; // 200 成功500 失败 private String message; // 给前端展示的提示信息 private Object data; // 真正的业务数据 public static Result success(Object data) { Result r new Result(); r.code 200; r.message 操作成功; r.data data; return r; } public static Result error(String msg) { Result r new Result(); r.code 500; r.message msg; r.data null; return r; } }所有 Controller 方法统一返回Result而不是返回裸数据这是前后端联调的第一条规矩。前端拿到响应先判断code 200再渲染数据如果有错误直接弹 message不需要每个接口单独写异常处理。如果你的源码里 Controller 返回的是散装 Map 或者直接返回实体类建议花半小时统一改成这个结构后面接任何前端页面都会省事很多。4.2 登录态怎么保持Session 拦截器与前端请求携带凭证医院预约系统里存在明显的用户角色差异普通患者要登录才能挂号和查看预约记录管理员登录后进入后台管理医生排班和号源。SSM 项目里最常见的做法是 Session 存用户信息配合拦截器做权限控制。拦截器是整个登录模块的守门员public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 不在白名单里的请求检查登录状态 Object user request.getSession().getAttribute(loginUser); if (user null) { response.setContentType(application/json;charsetutf-8); response.getWriter().write({\code\:401,\message\:\请先登录\}); return false; } return true; } }这段代码在 SpringMVC 配置文件里的注册方式也值得留意。要放行登录接口、首页和静态资源否则会出现“页面资源加载不出来”的诡异问题mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/register/ mvc:exclude-mapping path/static/**/ bean classcom.hospital.appointment.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors前端在 AJAX 请求里不需要手动携带 Session ID因为浏览器会自动在 Cookie 里带上JSESSIONID后端容器通过这个 ID 找到对应的 Session。这里有一个坑如果前端页面和后端接口不在同一个端口上比如前端 8080、后端 8081浏览器会拦截跨端口请求导致 Session 失效而且接口调不通。解决办法就是统一端口部署——还是把 HTML 放进 webapp 目录里让 Tomcat 一起托管最省心这也是毕业设计小组件最常见的打法。4.3 接口对接的三个老毛病日期格式、中文乱码、错误提示联调阶段翻车率最高的三个问题都属于“看着小、卡半天”的类型。日期格式排第一Java 端的java.util.Date序列化成 JSON 后默认是一串毫秒数1728000000000前端拿到手根本没法直接渲染成“2025-06-01”。SpringMVC 里配置一个 JSON 日期格式化器Bean public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() { ObjectMapper mapper new ObjectMapper(); mapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); mapper.setTimeZone(TimeZone.getTimeZone(GMT8)); return new MappingJackson2HttpMessageConverter(mapper); }前端再配合dayjs或原生new Date(value)转换一次日期显示就稳了。中文乱码排第二这个多半不是数据库问题而是 HTTP 层面的编码没对齐。POST 请求提交中文时前端fetch要设置Content-Type: application/json;charsetUTF-8后端web.xml里配置全局 filterfilter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter错误提示排第三。很多源码的前端是“接口报错了就弹System Error”用户根本不知道是没号了还是没登录。统一返回Result.error(该时段号源已约满)这种带业务语义的提示是用户能感知到的体验提升。具体做法前端封装一个request函数所有接口走这个函数code ! 200时统一弹res.message。等整个预约流程的报错都变成人话系统才算真正到了“能演示”的状态。5. 部署在本地跑通Tomcat MySQL 的最小操作与避坑排查5.1 从压缩包到浏览器出页面四步部署跑通源码拿到这套源码最快验证它能不能用的路径是装 MySQL 5.7 或 8.0装 JDK 8装 Maven 3.6 以上装 Tomcat 8.5。本质上是一个标准的 java web 项目部署流程。具体步骤按顺序操作每步做完都验证一下再继续# 第一步建库并导入源码附带的 SQL 脚本 mysql -u root -p mysql CREATE DATABASE hospital_db DEFAULT CHARACTER SET utf8mb4; mysql USE hospital_db; mysql SOURCE /path/to/sql/hospital.sql; # 第二步改数据库连接配置src/main/resources/jdbc.properties jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的密码 # 第三步Maven 打包跳过测试 mvn clean package -DskipTests # 第四步把打好的 WAR 包复制到 Tomcat webapps 目录启动 cp target/hospital.war /path/to/tomcat/webapps/ /opt/tomcat/bin/startup.sh前两步是绝大多数环境问题的根源。CREATE DATABASE时必须显式指定utf8mb4因为 MySQL 默认字符集可能是latin1否则导入中文数据直接乱码。jdbc.url里有两个常见参数要注意serverTimezoneAsia/Shanghai是 MySQL 8.0 必须的不设的话驱动会报时区错误useSSLfalse可以省去连接时的 SSL 握手警告。第四步启动后看到 Tomcat 日志输出Deploying web application archive且没有Exception再用浏览器访问http://localhost:8080/hospital看首页是否出来。5.2 避坑排查跑不起来时先看这五处问题一Tomcat 启动成功但页面 404。现象是webapps目录下能看到解压后的项目文件夹访问/hospital却打不开。原因通常是项目默认 Context Path 不是你猜的那个名字。办到 Tomcatconf/server.xml的 Host 节点加Context path/ docBasehospital reloadabletrue/然后访问根路径。问题二页面出来了但验证码不显示。现象是登录页的图片区域是个裂图。原因多为验证码生成类依赖的 jar 没有打进 WAR 包或者jcaptcha配置的字体在 Linux 上找不到。解决确认pom.xml里验证码依赖没有scopeprovided/scope并把字体配置改成系统自带的SansSerif。问题三MySQL 8.0 连不上。现象是启动时报Unable to load authentication plugin caching_sha2_password。原因是 MySQL 8.0 默认认证插件和 MySQL 5.7 的驱动不兼容。解决确认pom.xml用的驱动是mysql-connector-java8.0 以上版本或者在 MySQL 里把这个用户改回mysql_native_password。前者更省事。问题四排班列表加载慢。现象是数据库只有几百条排班数据页面却要转圈好几秒。原因多半是科室表、医生表、排班表三张表在 Java 代码里循环查库而不是 JOIN 查询。解决先看 MyBatis 的日志输出确认是不是 N1 查询再把查询逻辑合并到一条 JOIN 语句里。问题五部署后管理员后台登录报 500。现象是普通用户功能正常进入管理页面就数据库异常。原因一般是 admin 表结构里没有建初始化数据或密码字段用了不同的加密方式。解决检查 SQL 脚本里有没有INSERT INTO user初始数据确认登录逻辑里加密方式一致。6. 让源码经得起答辩功能验证清单与一个值得做的扩展思路6.1 答辩前必须能演示的八个场景打入源码能跑只是第一步答辩现场的演示才是生死线。我建议按这八个场景过一遍每个都走完整链路再做记录患者注册并登录查询指定科室的医生排班列表选择一个有余号的时段提交预约在“我的预约”中看到刚创建的预约记录并取消它取消后排班余号数量加回来管理员登录后台新增一个排班患者端能看到新增排班的号源后台查看预约统计报表。这八个场景覆盖了系统的全部核心链路任何一步断掉都会影响评分。6.2 一个即插即用的扩展超时未就诊自动过期如果想让系统比别人多一点亮点我推荐做一个“预约过期自动处理”的定时任务。这个功能完成不了多少代码量但属于明确的业务价值号源被约了但患者没来应该释放号源让其他人约。实现方式用 Spring 的Scheduled注解Component public class AppointmentExpireTask { Autowired private AppointmentMapper appointmentMapper; Scheduled(cron 0 0 1 * * ?) public void expireAppointments() { // 前一天所有待就诊的预约全部置为已过期 appointmentMapper.expireByDate(new Date()); } }配合一条 SQL 批量更新把就诊日期早于今天的待就诊预约统一改为已过期。这个任务放在毕业设计里能体现出你对真实业务的理解系统不只是记录数据还要能自动处理异常状态。做完这一点整个预约闭环才算真正闭环。这套源码对我来说最大的价值不是功能多花哨而是把医院挂号场景里最核心的“预约”动作从头到尾跑通了。你可以不喜欢它的页面样式也可以嫌它没用上最新框架但它作为一套可以参考的三层架构范例用来理解 SSM 各层怎么协作、MySQL 状态流转怎么设计确实给了我不少可以迁移到其它管理系统的经验。遇到卡住的地方先看日志再查 SQL最后翻配置文件九成的问题都能自己解决。希望帮到你。本文还有配套的精品资源点击获取