
简介面向开发者与高校学生的SSM框架网上医院预约挂号系统完整源码适用于毕业设计、课程实训及实际项目二次开发能够解决医院在线预约挂号、分时段就诊、科室信息管理、用户登录与后台管理等关键业务流程的技术实现问题。资源包共1191个文件约24.6MB核心组成部分包括256个HTML页面、223个CSS样式表与206个JavaScript脚本构建前端展示与交互64个JSP视图及35个Java源文件实现SSM分层架构中的控制层、业务层与持久层逻辑42个JAR包用于管理项目依赖另含SQL数据库脚本、PNG/GIF图片素材以及Eclipse工程配置文件整体目录结构清晰便于导入运行。目前已有431人学习下载。通过这套源码开发者可以掌握SSM框架整合流程、预约挂号核心业务表设计方法、前端页面动态渲染与交互技巧并在实际环境中部署验证。资源附带EasyUI、Bootstrap等前端框架样式适合需要快速搭建同类医院预约系统的学习者借鉴。1. SSM框架拼出一个网上医院挂号系统难点根本不在增删改查门诊大厅排长队挂号的场景这几年正被线上预约逐步替代。所谓“基于SSM框架的网上医院预约挂号系统设计源码”本质上是一个典型的JavaWeb三层架构项目Spring管对象、SpringMVC收请求、MyBatis做持久化前端用JSP加一点Bootstrap把科室、医生、排班、号源、预约记录这些业务串起来。它最常见的出处是课程设计、毕业设计以及一批想在简历上放一个完整项目的新人——但真正把它跑通并讲清楚的人并不多。这系统最反直觉的一点是几乎所有上手的人都会先去写用户表、写登录而真正的复杂度全在“排班”和“号源扣减”上。一个医生一天排两个时段每个时段放30个号剩余号怎么在并发下不减成负数才是这个SSM项目的技术分水岭。下面的内容按一套可复现的顺序展开先讲清楚数据模型怎么设计再落到SSM各层的代码写法然后专门处理预约并发和事务最后给出部署与验证时的常用手段。适合两类人一类是准备交设计、要讲源码的在校生另一类是工作几年想快速回顾SSM整合细节的工程师。2. 先定表结构再写代码网上预约挂号系统的数据模型与SSM工程骨架2.1 预约挂号的核心表把“号源”单独建模很多人在设计挂号系统时会顺手把“号”塞进预约记录表里觉得生成一个预约行就等于占了一个号。这个做法在并发稍微一高就会出问题两个请求同时读到剩余号是1各自都认为能预约结果产生两条记录号源成了负数。我一般会把号源建模成一张独立的排班表每一条记录代表“某医生在某天的某个时段放了几个号、还剩几个”。这样预约动作就变成了“对排班表执行一次带条件的原子扣减”而不是先查再插。下面这套建表SQL是这类系统里最常用的一组骨架覆盖用户、科室、医生、排班、预约五张表CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(50), phone VARCHAR(20), id_card VARCHAR(18), role TINYINT NOT NULL DEFAULT 0 COMMENT 0-患者 1-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, intro VARCHAR(500) ); CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, department_id INT NOT NULL, name VARCHAR(50) NOT NULL, title VARCHAR(50) COMMENT 职称主任医师/副主任医师, specialty VARCHAR(200) COMMENT 擅长领域, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_department (department_id) ); CREATE TABLE schedule ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, work_date DATE NOT NULL, period TINYINT NOT NULL COMMENT 1-上午 2-下午, total_num INT NOT NULL DEFAULT 30 COMMENT 总号源数, remain_num INT NOT NULL DEFAULT 30 COMMENT 剩余号数, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_doctor_period (doctor_id, work_date, period) ); CREATE TABLE appointment ( id INT PRIMARY KEY AUTO_INCREMENT, schedule_id INT NOT NULL, user_id INT NOT NULL, appoint_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-已预约 1-已取消, UNIQUE KEY uk_schedule_user (schedule_id, user_id) );关键点在于schedule表的唯一键uk_doctor_period和appointment表的唯一键uk_schedule_user。前者保证同一个医生同一天同一个时段只有一条排班记录后者保证同一个用户对同一个排班只能预约一次。这两个约束是后面防重复预约的数据库底线。2.2 SSM工程目录一个能讲清楚“源码”结构的标准分层拿到一套所谓的设计源码第一件事不是跑起来而是先看包结构。SSM项目几乎都遵循同一个分层习惯controller 收参数、service 写业务、mapper 管SQL。下面是我认为比较干净的一版结构新手照着搭不会走偏src/main/java com.hospital.appointment controller UserController.java DepartmentController.java DoctorController.java ScheduleController.java AppointmentController.java AdminController.java service AppointmentService.java ScheduleService.java impl AppointmentServiceImpl.java ScheduleServiceImpl.java dao UserMapper.java DepartmentMapper.java DoctorMapper.java ScheduleMapper.java AppointmentMapper.java entity User.java Department.java Doctor.java Schedule.java Appointment.java common Result.java PageBean.java src/main/resources mybatis mapper UserMapper.xml ScheduleMapper.xml AppointmentMapper.xml jdbc.properties mybatis-config.xml spring-context.xml spring-mvc.xml src/main/webapp WEB-INF web.xml jsp login.jsp index.jsp schedule.jsp appointment.jsp说一个小细节common包里放Result和PageBean这类通用类是很多模板源码的惯例但在课程设计里经常被忽略。如果不想在 controller 里每个方法都返回ModelAndView再手动塞分页参数就值得抽出这两个类。它们不是必须的但会让代码能少写三分之一。2.3 MyBatis Mapper层#{}和${}选错就是一次SQL注入事故SSM框架里数据访问层的写法比较固定接口定义方法XML写SQL。排班查询是最典型的列表页场景按科室过滤、按日期排序还要带着医生姓名一起查出来。select idfindScheduleList resultTypecom.hospital.appointment.entity.ScheduleVO SELECT s.id AS id, s.work_date AS workDate, s.period AS period, s.total_num AS totalNum, s.remain_num AS remainNum, d.name AS doctorName, d.title AS doctorTitle, dp.name AS departmentName FROM schedule s LEFT JOIN doctor d ON s.doctor_id d.id LEFT JOIN department dp ON d.department_id dp.id where if testdepartmentId ! null AND d.department_id #{departmentId} /if if testworkDate ! null AND s.work_date #{workDate} /if /where ORDER BY s.work_date ASC, s.period ASC /select注意这里的参数绑定全部用了#{}。#{}在预编译阶段会被替换成?占位符由 JDBC 的 PreparedStatement 传值这是防SQL注入的标准做法。而${}是字符串拼接只有当你要动态传入表名、排序列名这种数据库标识符时才考虑。网上能搜到一些“源码”为了省事在 order by 后面直接用${sort}一旦sort是从请求参数里透传的你的系统就裸奔了。顺带提一句 MyBatis 源码里 H2 的实现方式#{}对应的ParameterMapping在解析 XML 时就会确定而不是运行时才判断。所以如果看到有人把${}写进 where 条件的值里可以直接断定这套源码没过安全审查。3. 从查询排班到提交预约SSM框架下核心业务代码的落地写法3.1 Controller 只做参数接收和结果转发在 SSM 框架里SpringMVC 的 Controller 层应当是最薄的一层。网上预约挂号的提交接口常见设计是把请求参数封装成一个 DTOController 里不写任何业务判断直接交给 Service。Controller RequestMapping(/appointment) public class AppointmentController { Autowired private AppointmentService appointmentService; PostMapping(/submit) ResponseBody public Result submit(RequestBody AppointmentDTO dto) { return appointmentService.book(dto); } }这段代码覆盖了三个知识点。PostMapping限制请求方法避免 GET 请求也能改数据RequestBody接收 JSON 参数SpringMVC 会通过 Jackson 自动反序列化成 DTO 对象ResponseBody让返回值直接写入 HTTP 响应体配合前端 ajax 使用。这里不写Transactional是有意的事务的边界应该开在 Service 层而不是 Controller 层。如果 Controller 加了事务异常处理时返回的 JSON 也在事务上下文里容易把本该提交的结果回滚掉。3.2 Service 是预约业务的核心四步检查完成预约动作预约提交是整个系统里业务规则最密集的地方通常要按顺序做四件事查排班是否存在、查当前用户是否已预约过该排班、扣减号源、插入预约记录。Service public class AppointmentServiceImpl implements AppointmentService { Autowired private ScheduleMapper scheduleMapper; Autowired private AppointmentMapper appointmentMapper; Override Transactional(rollbackFor Exception.class) public Result book(AppointmentDTO dto) { // 1. 校验排班存在且未过期 Schedule schedule scheduleMapper.findById(dto.getScheduleId()); if (schedule null) { return Result.fail(排班不存在); } // 2. 校验当前用户是否已预约过防止重复提交 int count appointmentMapper.countByScheduleAndUser( dto.getScheduleId(), dto.getUserId()); if (count 0) { return Result.fail(您已预约该时段请勿重复提交); } // 3. 乐观锁扣减号源只有剩余号大于0才会更新成功 int rows scheduleMapper.decreaseRemain(dto.getScheduleId()); if (rows 0) { return Result.fail(号源已满请选择其他时段); } // 4. 生成预约记录 Appointment appointment new Appointment(); appointment.setScheduleId(dto.getScheduleId()); appointment.setUserId(dto.getUserId()); appointment.setStatus(0); appointmentMapper.insert(appointment); return Result.success(预约成功, appointment); } }第3步对应的 SQL 是这套业务里最值得讲的一句UPDATE schedule SET remain_num remain_num - 1 WHERE id #{scheduleId} AND remain_num 0这段 SQL 的效果是无论多少请求同时到达数据库行锁会串行化它们的更新remain_num 0条件保证减号永远不会低于 0。rows 0时说明号源已经被抢完直接返回失败。这就是乐观锁思路在代码里的落地形态——不显式加锁靠更新条件和影响行数来判断冲突。3.3 事务生效的前提同一个事务里执行扣减和插入上面的book方法加了Transactional(rollbackFor Exception.class)目的是保证“扣号源”和“插入预约记录”要么一起成功要么一起回滚。这里有一个刚入门的开发者经常踩的坑如果第 3 步成功了第 4 步插入预约记录时因为唯一键冲突抛了异常事务不回滚的话号就会被白扣用户却查不到自己的预约记录。rollbackFor Exception.class也是必须写清楚的Spring 默认只对 RuntimeException 回滚像Exception这种受检异常抛出时事务会被提交而不是回滚。虽然这里的Result.fail没有抛异常但 mapper 的 SQL 错误属于DataAccessException是 RuntimeException能正常触发回滚——可一旦你在未来加入文件写入、外部接口调用这类受检异常默认配置就不够用了。提前加上rollbackFor能少踩一个坑。顺带说一个 Spring 框架里的高频问题当book方法被同类里的另一个方法直接调用时事务不会生效。原因在于Transactional基于 AOP 代理同类直接调用走的是this引用而不是代理对象。4. 并发预约与数据库细节防超卖、唯一索引和连接池参数4.1 三层防线乐观锁、唯一索引、事务边界网上预约挂号系统在并发压力下最容易出的问题就是“超卖”100 个人同时抢 30 个号最后 35 个人收到成功提示。实际生产中防超卖通常不靠单个手段而是组合三层防线。第一层是业务代码里的remain_num 0条件解决“扣成负数”的问题第二层是uk_schedule_user唯一索引解决“同一用户同一排班重复预约”的问题第三层是Transactional事务边界解决“扣号成功但插入记录失败”的数据不一致问题。这三层里唯一索引往往被新手忽略。它会抛出 DuplicateKeyException 而不是返回错误码所以代码里必须做捕获处理否则用户会看到 500 页面。 ### 4.2 连接池参数别让数据库连接成为并发瓶颈 SSM 框架项目里最常用的连接池是 Druid 或 C3P0。在课程设计阶段并发不高默认参数够用但如果你想在简历上写“预研过并发场景”连接池参数就必须能讲出理由。下面是 Druid 在 spring-context.xml 里常见的配置方式 xml bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver} / property nameurl value${jdbc.url} / property nameusername value${jdbc.username} / property namepassword value${jdbc.password} / property nameinitialSize value5 / property nameminIdle value5 / property namemaxActive value20 / property namemaxWait value60000 / property namevalidationQuery valueSELECT 1 / /bean参数建议值说明initialSize5启动时建立的初始连接数太小会出现启动后第一批请求等待建连minIdle5空闲连接数低于该值时Druid 会主动补充防止突发流量打满 MySQL 连接maxActive20最大活跃连接数不要盲目调大MySQL 默认连接数只有 151maxWait60000获取连接的超时时间设置为 60 秒能避免线程无限等待validationQuerySELECT 1检测连接是否有效空闲连接被 MySQL 踢掉后能自动重连这里特别要提醒maxWait的语义当连接池耗尽时请求最多等待 60 秒超时直接抛GetConnectionTimeoutException。如果看到 Tomcat 日志里大量这个异常第一反应不是加连接池大小而是看 SQL 是不是有慢查询占着连接不释放。常见的祸首是查询排班列表时没有给work_date加索引导致全表扫描把连接拖死。4.3 排班查询的索引设计一个被忽视的优化点预约系统的核心查询是“按科室 日期查医生排班”。上面建的schedule表只有排班唯一键按科室过滤时实际上是通过 doctor 表关联回 schedule没有走索引。建议给schedule表再补一个索引ALTER TABLE schedule ADD INDEX idx_work_date (work_date);理由是用户的前端操作路径几乎都是先选日期、再选科室work_date是查询条件里区分度最高的字段。索引不是建得越多越好像appointment表的status这种只有 0/1 两个取值的字段建索引基本没用。能把work_date和uk_doctor_period的联合场景用好这套系统的查询性能就够课程设计和一般小型门诊使用了。5. 从源码到可运行Tomcat 部署步骤与三类高频排错拿到设计源码以后最常见的卡点不是代码看不懂而是本地跑不起来。SSM 项目推荐 JDK 1.8 Tomcat 8.5 Maven 3.6 的组合这是兼容性最稳的一组版本。部署流程基本固定导入 IDEA、配置 Maven、改jdbc.properties、启动 Tomcat。# 在项目根目录执行跳过测试打包 mvn clean package -DskipTests # 把打好的 war 包部署到 Tomcat cp target/hospital-appointment.war $TOMCAT_HOME/webapps/ # 启动 Tomcat $TOMCAT_HOME/bin/startup.sh # 查看启动日志确认没有异常 tail -f $TOMCAT_HOME/logs/catalina.out启动成功后按这条路径做冒烟测试先打开登录页注册一个用户再进入科室列表选择科室选择未来某一天的排班确认剩余号数是初始值然后提交预约并立即刷新排班列表看数字减 1。这套操作能同时验证 MyBatis 的增删改查、SpringMVC 的路由跳转、以及事务是否正常工作。我个人见过的三类高频启动失败问题第一类是 JDK 版本不匹配。Tomcat 9 配合 JDK 11 启动老项目时JSP 编译经常报Unable to compile class for JSP解决办法是换回 JDK 8 而不是去改代码。第二类是 MyBatis 的 mapper.xml 没有被 Maven 打进 classpath报Invalid bound statement (not found)原因是src/main/resources没有把 XML 当资源导出在 pom.xml 里补上 resources 配置即可。第三类是数据库连接失败先确认 MySQL 8 的驱动已经换成com.mysql.cj.jdbc.Driver并在jdbc.url里追加serverTimezoneAsia/ShanghaiuseSSLfalsecharacterEncodingutf8。做完这三步这套基于 SSM 的网上医院预约挂号系统就不只是一个能在编辑器里看懂的设计而是一个可以稳定跑起来、能面对完整预约流程检验的可用工程。本文还有配套的精品资源点击获取