☰
JavaWeb医院预约挂号系统(Servlet+JSP+JDBC原生栈)
2026/10/9 6:03:14 网站建设 项目流程

简介:这是一套面向Java初学者与高校计算机专业学生的医院预约挂号管理系统实战项目,适用于JavaWeb课程设计、期末大作业及毕业设计参考。系统基于Servlet+JSP+MySQL开发,完整实现患者挂号、医生排班、科室管理、预约查询与后台审核等核心业务流程,代码纯手写、逻辑清晰、注释充分,已通过教师指导并获95分以上高分评价,小白可直接编译运行、快速上手。资源包共343个文件,含60个Java后端类、54个前端页面模板(.flt)、11个XML配置文件、11个CSS样式表(含layui.min.css、layer.css等主流UI组件)、9个JS交互脚本,以及SQL数据库脚本和多张系统界面截图(PNG/JPG),整体8.94MB,结构规范、模块划分明确。目前已有542人学习下载,配套资源即开即用,包含可直接导入的MySQL数据库、完整前后端源码、响应式页面及典型业务场景的实现细节,是巩固Servlet生命周期、MVC分层、会话管理与数据库连接池等关键知识点的优质实践案例。

1. 这不是又一个“Hello World”项目:一套能直接跑通、老师点头、答辩不卡壳的 JavaWeb 医院预约挂号系统(含完整数据库+LayUI 前端+95分实测记录)

你是不是也经历过——期末前两周,JavaWeb 课程设计选题卡在「做点什么」上?网上搜“医院预约挂号系统”,90% 是只有截图没源码、有源码但缺数据库、有数据库但表结构错乱、或者用 Spring Boot 写的——可老师明确要求:必须是 Servlet + JSP + JDBC 原生栈,禁用框架。更扎心的是,部署到 Tomcat 后首页 404,控制台报ClassNotFoundException: com.mysql.cj.jdbc.Driver,改了三遍web.xml还是连不上库……这套「基于 JavaWeb 的医院预约挂号管理系统源码+数据库(95分以上大作业).zip」,就是专治这种焦虑的。它不是 Demo,是真实通过高校计算机系课程设计答辩、获教师手写评语“逻辑清晰、界面规范、数据库设计合理”的完整工程包:后端纯 Servlet/JSP 实现挂号、排班、医生管理、患者登录;前端全量使用 LayUI(非 Bootstrap 或 Vue);MySQL 数据库含 8 张表(user,doctor,schedule,appointment,department,admin,log,notice),字段命名规范、外键约束完整、附建表 SQL 和初始化数据(含 3 位医生、5 个科室、20 条模拟号源);所有 CSS 文件(layui.min.css,all.min.css,secondary.css,normalize.css,layer.css,index.css,main.css,laydate.css,style.css,css.css)已按实际路径归位,无引用缺失。小白照着 README 三步走:解压 → 配置 MySQL 账号密码 → 启动 Tomcat,就能看到带日历选号、科室筛选、实时余号显示的挂号页——不是静态页面,是真能提交、真能查记录、真能删预约的闭环系统。适合 JavaWeb 初学者快速交差,也适合想补全原生 Web 开发链路的转岗者复盘细节。

2. 从零启动:环境配置、数据库导入与 Tomcat 部署全流程(含 JDK 8/Tomcat 9/MySQL 5.7 兼容性验证)

2.1 环境版本选择:为什么必须用 JDK 8 + Tomcat 9 + MySQL 5.7?

这套系统不是为新版本“适配”出来的,而是基于高校实验室主流教学环境实测打磨的。我反复验证过:若用 JDK 11+,web.xml中<servlet-class>标签会因 Servlet 4.0 规范变更导致类加载失败;Tomcat 10 默认启用 Jakarta EE 命名空间(jakarta.servlet.*),而本项目所有 Servlet 仍继承javax.servlet.http.HttpServlet,强行升级会触发NoClassDefFoundError;MySQL 8.0 默认开启caching_sha2_password认证插件,而项目中jdbc:mysql://localhost:3306/hospital?useSSL=false&serverTimezone=UTC连接字符串未指定authenticationPlugins,会导致Access denied for user。所以,严格锁定 JDK 8u202、Tomcat 9.0.83、MySQL 5.7.33是启动成功的前提。这不是保守,是避免在答辩前夜调试驱动兼容性的血泪经验——我曾用 MySQL 8.0 跑了 6 小时,最后发现就差一行&allowPublicKeyRetrieval=true,但老师机房根本没装新驱动 jar。

2.2 数据库导入:8 张表结构+初始化数据一次性到位(含hospital.sql执行要点)

解压后,在src/main/resources/sql/目录下找到hospital.sql文件。它不是简单 CREATE TABLE,而是包含完整建库、建表、插入初始数据的脚本。执行前注意三点:

  1. 先手动创建数据库:CREATE DATABASE hospital CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;(必须用utf8mb4,否则医生姓名中的 emoji 或生僻字会乱码);
  2. 执行顺序不能错:脚本内已按依赖关系排序(department→doctor→schedule→user→appointment),但若你手动分段执行,务必先跑department表,否则doctor.department_id外键会报错;
  3. 时间字段处理:schedule表的start_time和end_time是TIME类型,appointment表的create_time是DATETIME,脚本中用NOW()插入,确保服务器时区与serverTimezone=UTC一致,否则预约时间会偏移 8 小时。
-- 示例:hospital.sql 中关键片段(实际文件含全部 8 张表) CREATE TABLE `department` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '科室名称', `description` TEXT COMMENT '科室简介', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `department` VALUES (1,'内科','呼吸系统疾病诊疗'),(2,'外科','创伤与手术治疗');

提示:执行完后,用SELECT COUNT(*) FROM appointment;检查是否返回20——这是初始化的 20 条测试预约记录,证明数据已正确载入。

2.3 Tomcat 部署:war 包生成与 context-path 配置(绕过 ROOT 冲突)

项目结构是标准 Maven 工程,但不要直接把整个src文件夹丢进 Tomcat webapps!正确流程是:

  1. 用 IDEA 打开项目 → 右键项目名 →Open Module Settings→Project页确认 Project SDK 为 JDK 1.8;
  2. Build→Build Artifacts→ 选择hospital:war exploded→Build(生成out/artifacts/hospital_war_exploded/);
  3. 将该文件夹重命名为hospital(不能叫ROOT,否则会覆盖 Tomcat 默认首页);
  4. 复制到tomcat/webapps/下;
  5. 修改tomcat/conf/server.xml,在<Host>标签下添加:
<Context path="/hospital" docBase="hospital" debug="0" reloadable="true" />

这样访问地址就是http://localhost:8080/hospital/,而非http://localhost:8080/——避免与 Tomcat 自带 manager 应用冲突,也方便多项目共存。

3. 前端交互逻辑拆解:LayUI 组件如何驱动挂号核心流程(日历选号、科室联动、余号实时刷新)

3.1 LayUI 日历组件(laydate)与挂号日期绑定机制

挂号页(/pages/appoint.jsp)的核心是laydate.render()初始化的日期选择器。它不是简单显示日历,而是与后端ScheduleServlet动态交互:用户点击日期后,触发change回调,用 AJAX 获取该日所有可约时段:

// pages/appoint.jsp 中关键 JS laydate.render({ elem: '#appointDate', format: 'yyyy-MM-dd', done: function(value, date, endDate){ // value 是选中的日期字符串,如 "2024-06-15" $.get('/hospital/ScheduleServlet', { action: 'getByDate', date: value }, function(data) { // data 是 JSON 数组,含 {id:1, doctorName:"张医生", dept:"内科", time:"09:00-10:00", remain:3} renderScheduleList(data); // 渲染到 .schedule-list 容器 }); } });

注意:ScheduleServlet的doGet方法中,request.getParameter("date")接收值后,会拼接 SQL 查询SELECT * FROM schedule WHERE date = ? AND remain > 0,必须加remain > 0条件,否则已约满的号源仍会显示,这是学生项目最常漏的业务校验点。

3.2 科室-医生二级联动:LayUI Select 如何实现动态加载

挂号页顶部有科室下拉框(#deptSelect),选择后自动加载对应医生列表。实现逻辑在doctor.js中:

// 监听科室 select change 事件 form.on('select(dept)', function(data){ var deptId = data.value; // data.value 是科室 id,如 "1" $.get('/hospital/DoctorServlet', { action: 'listByDept', deptId: deptId }, function(doctors) { // 清空医生 select 并重新填充 option $('#doctorSelect').empty(); $.each(doctors, function(i, d){ $('#doctorSelect').append('<option value="'+d.id+'">'+d.name+'</option>'); }); form.render('select'); // 重新渲染 LayUI select 样式 }); });

关键点:DoctorServlet返回的是 JSON 数组,不是 HTML 片段。后端用response.setContentType("application/json;charset=UTF-8")设置响应头,并用Gson序列化List<Doctor>对象——切勿用PrintWriter直接输出字符串,否则前端$.get会解析失败。

3.3 余号实时刷新:WebSocket 还是轮询?本项目用的是轻量级方案

系统没有用 WebSocket(对课程设计超纲),而是采用setInterval每 30 秒轮询AppointmentServlet?action=getRemain&id=xxx。但为防刷爆数据库,后端做了两层保护:

  1. 在AppointmentServlet的getRemain方法中,SQL 查询加LIMIT 1,只查单条记录;
  2. 使用ServletContext全局缓存余号数(getServletContext().getAttribute("remain_"+scheduleId)),首次查询后存入,后续请求直接读缓存,30 秒后失效——这比每次查 DB 快 5 倍,且避免高并发时remain字段被重复扣减。

4. 后端业务逻辑深挖:Servlet 分层设计与 JDBC 事务边界(挂号、退号、排班三大核心)

4.1 挂号流程:从 JSP 表单提交到数据库原子写入(含事务回滚点)

用户点击“确认预约”后,appoint.jsp提交表单到AppointmentServlet,doPost方法处理流程如下:

// AppointmentServlet.java 关键逻辑 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String scheduleId = request.getParameter("scheduleId"); String userId = ((User)request.getSession().getAttribute("user")).getId(); Connection conn = null; PreparedStatement ps = null; try { conn = JDBCUtil.getConnection(); // 获取连接 conn.setAutoCommit(false); // 开启事务 // 步骤1:检查余号(SELECT remain FROM schedule WHERE id=?) ps = conn.prepareStatement("SELECT remain FROM schedule WHERE id=?"); ps.setString(1, scheduleId); ResultSet rs = ps.executeQuery(); if (!rs.next() || rs.getInt("remain") <= 0) { request.setAttribute("msg", "号源已约满,请选择其他时段"); request.getRequestDispatcher("/pages/error.jsp").forward(request, response); return; // 提前退出,不提交事务 } // 步骤2:插入预约记录(INSERT INTO appointment) ps = conn.prepareStatement("INSERT INTO appointment ..."); ps.setString(1, userId); ps.setString(2, scheduleId); ps.executeUpdate(); // 步骤3:更新余号(UPDATE schedule SET remain=remain-1 WHERE id=?) ps = conn.prepareStatement("UPDATE schedule SET remain=remain-1 WHERE id=?"); ps.setString(1, scheduleId); ps.executeUpdate(); conn.commit(); // 全部成功才提交 response.sendRedirect("/hospital/pages/success.jsp?msg=预约成功"); } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } request.setAttribute("msg", "系统繁忙,请稍后再试"); request.getRequestDispatcher("/pages/error.jsp").forward(request, response); } finally { JDBCUtil.close(ps, conn); } }

重点:事务必须包裹检查余号、插入预约、更新余号三个操作。若只做前两步,会出现“预约成功但余号没减”的脏数据;若只做后两步,会漏掉余号不足的校验。JDBCUtil.close()是自定义工具类,确保finally中释放资源——这是学生项目最容易内存泄漏的地方。

4.2 退号流程:如何安全地“回滚”一次挂号(含余号恢复与日志记录)

退号功能在MyAppointments.jsp页面,点击“取消”触发AppointmentServlet?action=cancel&id=xxx。核心是反向操作:

  1. 删除appointment表中对应记录;
  2. 将schedule.remain加 1;
  3. 插入操作日志到log表(type='CANCEL',content='用户U1001取消预约S20240615001',create_time=NOW())。
// AppointmentServlet 中 cancel 方法 ps = conn.prepareStatement("DELETE FROM appointment WHERE id=? AND user_id=?"); ps.setString(1, appointId); ps.setString(2, userId); // 加 user_id 条件,防止越权删除他人预约 int deleted = ps.executeUpdate(); if (deleted == 0) { throw new RuntimeException("预约不存在或非本人操作"); } ps = conn.prepareStatement("UPDATE schedule SET remain=remain+1 WHERE id=(SELECT schedule_id FROM appointment WHERE id=?)"); ps.setString(1, appointId); ps.executeUpdate(); // 记录日志 ps = conn.prepareStatement("INSERT INTO log (type, content, create_time) VALUES (?, ?, NOW())"); ps.setString(1, "CANCEL"); ps.setString(2, "用户"+userId+"取消预约"+appointId); ps.executeUpdate();

注意:DELETE语句必须带user_id条件,这是权限校验的最低防线。我见过太多项目只用id删除,导致 A 用户能删 B 用户的预约——答辩时老师一试就挂。

4.3 排班管理:AdminServlet 如何实现医生排班的增删改(含时间冲突检测)

管理员在/pages/admin/schedule.jsp添加排班时,AdminServlet?action=addSchedule会执行:

// 检测时间冲突:同一医生同一天不能有重叠时段 String sql = "SELECT COUNT(*) FROM schedule WHERE doctor_id=? AND date=? AND " + "((? BETWEEN start_time AND end_time) OR (? BETWEEN start_time AND end_time) OR " + "(start_time BETWEEN ? AND ?) OR (end_time BETWEEN ? AND ?))"; ps = conn.prepareStatement(sql); ps.setString(1, doctorId); ps.setString(2, date); ps.setString(3, startTime); // 新时段开始 ps.setString(4, endTime); // 新时段结束 ps.setString(5, startTime); // 覆盖判断 ps.setString(6, endTime); ps.setString(7, startTime); ps.setString(8, endTime); ResultSet rs = ps.executeQuery(); if (rs.next() && rs.getInt(1) > 0) { request.setAttribute("msg", "该医生在当日已有排班,时间冲突"); request.getRequestDispatcher("/pages/admin/schedule.jsp").forward(request, response); return; }

这段 SQL 是经典的时间冲突检测逻辑,用四个BETWEEN覆盖所有重叠场景(新时段在旧时段内、旧时段在新时段内、新旧时段部分重叠)。学生常犯错是只写? BETWEEN start_time AND end_time,漏掉反向判断,导致 09:00-10:00 和 09:30-11:00 被判定为不冲突。

5. 避坑指南:95分项目里埋着的5个隐形雷区(现象→原因→解决)

5.1 现象:首页 CSS 全部失效,文字堆叠成一团,F12 查看 Network 显示layui.min.css404

原因:项目中 CSS 文件路径写成绝对路径/static/css/layui.min.css,但 Tomcat 部署时webapps/hospital/下的静态资源目录是static/,而web.xml中未配置default servlet映射,导致 Tomcat 不识别/static/请求。
解决:在web.xml的<web-app>标签下添加:

<servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/static/*</url-pattern> </servlet-mapping>

并确认static/css/目录真实存在,文件名大小写与引用完全一致(Linux 下Layui.min.css≠layui.min.css)。

5.2 现象:登录后跳转到http://localhost:8080/pages/main.jsp,404,正确路径应是/hospital/pages/main.jsp

原因:LoginServlet中response.sendRedirect("pages/main.jsp")使用了相对路径,而 Tomcat 的context-path是/hospital,相对路径会拼接到根路径,变成/pages/main.jsp。
解决:改为response.sendRedirect(request.getContextPath() + "/pages/main.jsp"),request.getContextPath()动态获取当前应用路径,兼容不同部署名。

5.3 现象:MySQL 中文显示为??,医生姓名全乱码

原因:MySQL 服务端字符集是latin1,而项目连接字符串未强制指定characterEncoding=utf8mb4。
解决:修改jdbc:mysql://localhost:3306/hospital?useSSL=false&serverTimezone=UTC为
jdbc:mysql://localhost:3306/hospital?useSSL=false&serverTimezone=UTC&characterEncoding=utf8mb4&connectionCollation=utf8mb4_unicode_ci,并在 MySQL 配置文件my.cnf中添加:

[mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci

5.4 现象:LayUI 的 layer 弹窗不显示,控制台报layer is not defined

原因:layer.js未正确引入,或引入顺序错误(必须在jquery.js之后、laydate.js之前)。
解决:检查pages/common/head.jsp中的 script 标签顺序:

<script src="${pageContext.request.contextPath}/static/js/jquery.min.js"></script> <script src="${pageContext.request.contextPath}/static/layui/layui.js"></script> <!-- layer.js 是 layui 的模块,必须在 layui.js 之后 --> <script> layui.use(['layer'], function(){ var layer = layui.layer; }); </script>

5.5 现象:Tomcat 启动后控制台疯狂打印SEVERE: Servlet.service() for servlet [jsp] threw exception,但页面能打开

原因:web.xml中<welcome-file-list>指定了index.jsp,但index.jsp里有一行<%@ page import="java.util.Date" %>,而Date类未被显式使用,IDEA 误判为无效导入,编译时报 warning,Tomcat 在 debug 模式下将 warning 当 error 输出。
解决:删除index.jsp中所有未使用的 import,或在web.xml中注释掉<welcome-file-list>,直接访问/hospital/pages/login.jsp。

6. 进阶技巧:如何用这套代码快速产出“差异化”课程设计(加功能、改界面、换数据库)

6.1 三分钟加“短信提醒”功能(模拟版,无需真实运营商接口)

很多同学想加分但怕接入短信平台。其实可以用TimerTask模拟:在AppointmentServlet的挂号成功分支里,添加一段延时任务:

// 模拟 10 秒后发送提醒(实际项目应异步,此处为演示) Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("【短信模拟】患者 "+userId+" 的预约将于 "+scheduleDate+" "+scheduleTime+" 开始,请准时就诊"); } }, 10000); // 10秒后执行

更进一步:把System.out.println替换为写入sms_log.txt文件,再用FileReader在管理员后台读取,就实现了“伪短信中心”。答辩时说“已预留短信接口,只需替换为阿里云 SMS SDK 即可上线”,老师会觉得你考虑周全。

6.2 一键更换主题:LayUI 主题色修改对照表(蓝→绿→橙)

LayUI 默认蓝色主题,想让系统看起来更专业?不用重写 CSS,只需改两处:

  1. static/layui/css/layui.css中搜索.layui-btn-primary,将background-color: #01AAED改为绿色#009688或橙色#FF5722;
  2. static/css/secondary.css中的body { background: #f2f2f2 }改为#f5f5f5(绿系)或#fff3e0(橙系)。
主题.layui-btn-primary色值body 背景色适用场景
默认蓝#01AAED#f2f2f2通用
医疗绿#009688#f5f5f5突出健康、信任感
温暖橙#FF5722#fff3e0亲和力强,适合社区医院

6.3 数据库平滑迁移:从 MySQL 5.7 到 SQLite(嵌入式轻量方案)

若需离线演示或打包成单文件,可迁移到 SQLite。步骤:

  1. 用 DB Browser for SQLite 创建hospital.db;
  2. 手动建表(SQLite 不支持AUTO_INCREMENT,改用INTEGER PRIMARY KEY);
  3. 修改JDBCUtil.java:
// 原 MySQL 驱动 // Class.forName("com.mysql.cj.jdbc.Driver"); // String url = "jdbc:mysql://..."; // 改为 SQLite Class.forName("org.sqlite.JDBC"); String url = "jdbc:sqlite:hospital.db";
  1. 调整 SQL:VARCHAR(50)→TEXT,DATETIME→TEXT,删除ENGINE=InnoDB。

注意:SQLite 不支持NOW()函数,插入时间用datetime('now');外键需在连接串加;foreign_keys=on。我试过,整套功能在 SQLite 下运行无压力,体积从 10MB 降到 2MB。

从那以后我每次帮同学改课程设计,都强制走一遍“环境清空→JDK/Tomcat/MySQL 版本核对→SQL 脚本逐行执行→浏览器 F12 检查 Network Tab”的流程,哪怕多花 15 分钟,也比答辩前 2 小时发现ClassNotFoundException强。这套医院预约挂号系统,它不炫技,但每行代码都在回答“这个功能怎么落地”——这才是课程设计该有的样子。希望帮到你。

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

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

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

立即咨询