SSM框架网上预约挂号系统:从数据模型到并发扣减的完整实现
2026/9/17 1:44:45 网站建设 项目流程

简介:面向开发者与高校学生的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_periodappointment表的唯一键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包里放ResultPageBean这类通用类是很多模板源码的惯例,但在课程设计里经常被忽略。如果不想在 controller 里每个方法都返回ModelAndView再手动塞分页参数,就值得抽出这两个类。它们不是必须的,但会让代码能少写三分之一。

2.3 MyBatis Mapper层:#{}${}选错就是一次SQL注入事故

SSM框架里数据访问层的写法比较固定,接口定义方法,XML写SQL。排班查询是最典型的列表页场景,按科室过滤、按日期排序,还要带着医生姓名一起查出来。

<select id="findScheduleList" resultType="com.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 test="departmentId != null"> AND d.department_id = #{departmentId} </if> <if test="workDate != 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 层应当是最薄的一层。网上预约挂号的提交接口,常见设计是把请求参数封装成一个 DTO,Controller 里不写任何业务判断,直接交给 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 id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <property name="initialSize" value="5" /> <property name="minIdle" value="5" /> <property name="maxActive" value="20" /> <property name="maxWait" value="60000" /> <property name="validationQuery" value="SELECT 1" /> </bean>
参数建议值说明
initialSize5启动时建立的初始连接数,太小会出现启动后第一批请求等待建连
minIdle5空闲连接数低于该值时,Druid 会主动补充,防止突发流量打满 MySQL 连接
maxActive20最大活跃连接数,不要盲目调大,MySQL 默认连接数只有 151
maxWait60000获取连接的超时时间,设置为 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_dateuk_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里追加serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8

做完这三步,这套基于 SSM 的网上医院预约挂号系统就不只是一个能在编辑器里看懂的设计,而是一个可以稳定跑起来、能面对完整预约流程检验的可用工程。

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

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

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

立即咨询