☰
JSP+Servlet医院预约挂号系统源码:解压到跑通的完整指南
2026/10/7 15:49:38 网站建设 项目流程

简介:这是一份基于JSP+Servlet技术栈开发的医院预约挂号系统完整源码,适合毕业设计选题、Java Web课程实训与初学MVC模式的开发者参考。资源以项目工程形式组织,共1431个文件,涵盖JSP动态页面、Java业务处理类、Servlet控制器、CSS/JS前端资源、SQL数据库脚本以及项目配置文件等,压缩包整体约30.98MB,目录结构清晰,便于按模块定位和学习。系统讲解覆盖用户登录与权限控制、医生空闲时段查询、预约请求处理、数据库设计、并发控制、安全防护与日志记录等关键环节,学习热度较高,目前已有208人学习。通过研读该源码,读者可以理解前后端交互流程、MVC分层思想及预约挂号业务的实际落地方式,也适合在此基础上扩展功能或直接作为课程设计与毕业设计的项目蓝本。

1. JSP+Servlet 医院预约挂号系统源码:解压之后怎么跑起来

距离交毕设还有三周,你从同学U盘里拷来一份“基于Jsp+Servlet医院预约挂号系统源码.zip”,右键zip解压之后,里面是几十个JSP页面、一堆Servlet类和一个看着眼熟的DAO包,接下来就不知道该双击哪个文件了。如果是这个场景,这份资源对你还挺对路:它走的是经典 JSP+Servlet+JDBC+MySQL 组合,按 MVC 模式把患者注册登录、医生排班查询、在线预约挂号、后台维护拆成了完整 JavaWeb 工程,能直接导入 Eclipse/IDEA 跑起来演示。适合用它当毕业设计底子、做 Java 课程设计案例源码参考、或者对 Servlet 请求处理链路还不熟的新手。下面我把运行环境、预约核心流程、数据库并发坑和导入时最容易翻车的地方过一遍。

2. 项目结构与运行环境:MVC 骨架和 Tomcat/JDK/MySQL 版本怎么搭

2.1 先看懂三层:谁负责显示,谁负责逻辑

拿到这份源码第一步不是急着找某一段代码,而是先把工程放进能运行的环境里。老式 JavaWeb 项目最常见的组织方式就是三层:JSP 负责把数据渲染成页面,Servlet 负责接收请求、调用业务、控制跳转,DAO 配合 JavaBean 负责和数据库打交道。对应到预约系统里,注册页面提交表单到注册 Servlet,Servlet 调 UserDao 里的 insert 方法,成功后转发回登录页,这就是一个完整的 MVC 请求链路。

我一般建议先按这个思路把目录看一遍,而不是双击某个页面。因为直接打开 JSP 只能看到 HTML 静态部分,看不到数据从哪来;真正决定页面展示内容的,是 Servlet 里 request.setAttribute 塞进去的对象。明白了这条链,后面改功能才有方向。对答辩也友好,老师问“你的请求是怎么走的”,你能清晰说出一条线,比背代码强得多。

这套老工程适合做毕设还有一个现实原因:它没有 Spring 那套复杂配置,没有 Maven 依赖地狱,核心逻辑都在你能直接改的地方。对于那些只是想“把功能跑起来”的人,这种黑匣子反而少,所有代码你都能看得到。

2.2 解压、建库、配连接:让工程先“站起来”

解压这一步,Windows 下右键“全部解压缩”就行,终端党也可以用命令。我建议先建一个干净的目录,再把压缩包放进去解压,避免路径里有中文空格导致某些老工具识别出问题。

# 建目录并解压 mkdir -p ~/hospital && cd ~/hospital unzip 基于JspServlet医院预约挂号系统源码.zip -d project # 确认关键目录存在 ls project

解压后正常应该能看到 src 源码目录、WebContent 或 web 目录、数据库脚本和项目配置文件。如果只有一堆 .java 和 .jsp,没有工程描述文件,那就是导工程而不是开文件的问题,下面会处理。

环境版本上,学校机房和老教程里最常出现的是 JDK 1.8 + Tomcat 8.5 + MySQL 5.7 这套组合。我的建议:版本宁保守勿新潮,尤其原工程直接用 JDK 11 以上编译时,经常冒出 javax.servlet 找不到之类的兼容问题,纯属给自己上难度。

组件推荐版本说明
JDK1.8老 JavaWeb 工程最稳,答辩演示也不会出幺蛾子
Tomcat8.5支持 Servlet 3.1,和 JDK 1.8 组合最常见
MySQL5.7教程和驱动兼容性最好;MySQL 8 需要换驱动
IDEEclipse EE 或 IDEA Ultimate需要原生支持 Web 工程和 Tomcat
驱动包mysql-connector-java 5.1.49和 MySQL 5.7 搭配最省心

数据库连接信息一般集中在配置文件里,常见文件名是 db.properties、jdbc.properties 或者 DBUtil.java 顶部的常量。找到后改成你自己的账号密码:

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/hospital?useUnicode=true&characterEncoding=UTF-8&useSSL=false jdbc.username=root jdbc.password=你的密码

这里三个参数值得记一下:characterEncoding=UTF-8 解决中文乱码,useSSL=false 避免连不上时打印一堆握手日志,useUnicode=true 是老驱动常见搭配。如果你手头只有 MySQL 的 zip 免安装包,记得下载后先跑 mysqld --initialize-insecure 初始化 data 目录,再改 my.ini 里的端口和字符集,而不是直接双击 mysqld 就以为装好了。

接下来建库。数据库脚本一般在 sql 或 db 目录里,文件名叫 hospital.sql 之类的概率很大。用命令行导入最省事:

CREATE DATABASE IF NOT EXISTS hospital DEFAULT CHARACTER SET utf8mb4; USE hospital; SOURCE /你的路径/hospital.sql;

注意脚本里的表如果有外键关联,导入顺序不能乱;报错就看哪个表先建,实在不行把外键语句后置。导入成功后,第 2 章的核心骨架就算立住了。

2.3 一个请求怎么从 JSP 走到 Servlet 再到数据库

工程跑起来后,你需要确认项目里用的是 web.xml 映射还是注解式 Servlet,这决定了改路径时去哪里找。

<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.hospital.web.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/user/login</url-pattern> </servlet-mapping>
// 常见做法:直接用注解写在类上,和 web.xml 二选一,别同时配 @WebServlet("/user/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) { // 业务代码在这里 } }

页面表单里的 action 写的路径,必须和 url-pattern 对应上。比如 login.jsp 里写 action="user/login",那 Servlet 映射就必须是 /user/login,少一个斜杠都 404。这一条是新手最容易忽略的坑,后面第 5 章我会专门展开。

3. 核心业务实现:登录、排班查询与防重复预约

3.1 登录与角色权限:从 Session 到拦截器

预约系统至少要区分三种身份:患者、医生、管理员。最轻量做法是登录成功后把用户对象和角色塞进 Session,再写一个过滤器统一拦截受保护页面。

public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String path = request.getRequestURI(); // 放行登录页、登录接口和静态资源,避免循环跳转 if (path.endsWith("login.jsp") || path.contains("/user/login") || path.endsWith(".css") || path.endsWith(".js") || path.endsWith(".jpg") || path.endsWith(".png")) { chain.doFilter(req, resp); return; } // 没登录的一律踢回登录页 if (request.getSession().getAttribute("currentUser") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

过滤器在 web.xml 里配置到 /* 即可。这里的放行清单要写全,否则会出现登录页能打开但 CSS 全丢的尴尬情况。角色区分的常见做法是登录时把 role 字段也放进 Session,管理员入口页面单独判断 role 是否为 2,不是就提示无权限。这种方案虽然原始,但胜在直观,答辩时也容易讲清楚。

3.2 医生排班查询:按科室和日期组合查数据

患者预约前先看医生排班,这是个典型的多条件查询场景。前端页面通常是一个科室下拉框加一个日期选择框,提交后传给 Servlet,再由 Servlet 调 DAO 拼 SQL。

SELECT d.id, d.name, d.title, d.department_id, s.work_date, s.total_count, s.remain_count FROM doctor d JOIN schedule s ON d.id = s.doctor_id WHERE d.department_id = ? AND s.work_date = ? ORDER BY s.work_date, d.id;

Java 侧用 PreparedStatement 而不是字符串拼接,这个选择很重要,既是防 SQL 注入的第一道防线,也避免日期值拼进去时格式对不上。

String sql = "SELECT * FROM doctor WHERE department_id = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, Integer.parseInt(deptId)); // 参数从 String 转 int try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { // 封装成 Doctor 对象放进 List } } }

这里有个细节:页面传过来的科室 id 是 String,DAO 里必须先做类型转换,否则 SQL 参数类型不匹配会直接报错。如果日期是空字符串,还要在前面判空,不然 SQL 会变成 work_date='' 而不是查询全部。处理方式通常是在 Service 层判断:日期为空就只按科室查,日期不为空再追加时间条件。

3.3 预约挂号的核心:事务与防重复

预约是整个系统里最容易出问题的操作,因为一次成功的预约涉及两条写操作:插入预约记录、扣减号源余量。只要有一条失败另一条成功,数据就不一致。常见做法是把这两步放在同一个事务里,任何一个环节失败就整体回滚。

public boolean createAppointment(int patientId, int doctorId, String workDate) { String checkSql = "SELECT id FROM appointment WHERE patient_id=? AND doctor_id=? AND work_date=?"; String updateSql = "UPDATE schedule SET remain_count = remain_count - 1 " + "WHERE doctor_id=? AND work_date=? AND remain_count > 0"; String insertSql = "INSERT INTO appointment(patient_id, doctor_id, work_date, create_time, status) " + "VALUES (?,?,?,NOW(),1)"; try (Connection conn = dataSource.getConnection()) { conn.setAutoCommit(false); // 1. 先查同一患者同一天同一医生是否已经预约过 int duplicateId = 0; try (PreparedStatement ps = conn.prepareStatement(checkSql)) { ps.setInt(1, patientId); ps.setInt(2, doctorId); ps.setString(3, workDate); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { duplicateId = rs.getInt(1); } } } if (duplicateId > 0) { conn.rollback(); return false; } // 2. 条件更新号源余量,受影响行数为 0 说明没号了 try (PreparedStatement ps = conn.prepareStatement(updateSql)) { ps.setInt(1, doctorId); ps.setString(2, workDate); int rows = ps.executeUpdate(); if (rows == 0) { conn.rollback(); return false; } } // 3. 插入预约记录 try (PreparedStatement ps = conn.prepareStatement(insertSql)) { ps.setInt(1, patientId); ps.setInt(2, doctorId); ps.setString(3, workDate); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { try (Connection connDest = dataSource.getConnection()) { connDest.rollback(); } catch (SQLException ignored) { } return false; } }

这段代码里有三个关键点。条件更新里加上 remain_count > 0,相当于把号源判断和扣减合并成一条原子 SQL,避免“先查再减”造成的超卖。数据库层面再给 appoint ment 表加唯一约束,同一天同一患者同一医生只能有一条预约记录,这是兜底防线。最后事务千万别漏掉 setAutoCommit(false),否则两条 SQL 各自独立提交,扣号成功但插入失败,号就白丢了。

4. 数据库设计与并发控制:四张表、事务和唯一约束

4.1 核心表怎么建:科室、医生、患者、预约

这套系统通常围绕四张表展开:科室表、医生表、患者表、预约记录表。建表时把字符集统一成 utf8mb4,能避免一部分中文乱码的问题。

CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE, intro VARCHAR(255) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, department_id INT NOT NULL, name VARCHAR(30) NOT NULL, title VARCHAR(30), phone VARCHAR(20), CONSTRAINT fk_doctor_dept FOREIGN KEY (department_id) REFERENCES department(id) ) ENGINE=InnoDB; CREATE TABLE patient ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(30), id_card VARCHAR(18), phone VARCHAR(20) ) ENGINE=InnoDB; CREATE TABLE appointment ( id INT PRIMARY KEY AUTO_INCREMENT, patient_id INT NOT NULL, doctor_id INT NOT NULL, work_date DATE NOT NULL, status TINYINT DEFAULT 1, create_time DATETIME, UNIQUE KEY uk_patient_doctor_date (patient_id, doctor_id, work_date), CONSTRAINT fk_appt_patient FOREIGN KEY (patient_id) REFERENCES patient(id), CONSTRAINT fk_appt_doctor FOREIGN KEY (doctor_id) REFERENCES doctor(id) ) ENGINE=InnoDB;

如果导入时因为外键顺序报错,可以在脚本头部加 SET FOREIGN_KEY_CHECKS=0,导入完成后再设回 1。预约记录里的唯一索引 uk_patient_doctor_date 是防止重复挂号的最后一道防线,代码里的判断只能降低概率,不能绝对保证并发下不出问题。

4.2 时间字段统一:别用 String 存日期

我在不少毕设源码里见过 work_date 用 varchar 存储,当时图省事,JSP 页面显示确实没问题,但后面要做“查今天有哪些号”“按日期范围统计”就傻眼了,字符串比较日期会得到完全错误的顺序。这种骗过自己的写法属于典型的“先爽后坑”。正确做法是用 DATE 类型存日期,DATETIME 存创建时间,Java 侧对应 java.sql.Date 或 LocalDate。

如果你用的是 MySQL 的 zip 安装方式,my.ini 里最好把时区也定下来:

[mysqld] character-set-server=utf8mb4 default-time-zone=+08:00

连接串里也要带 serverTimezone,否则新版驱动会报时区错误。修改后重启 MySQL 服务,再把原来的 varchar 列改成 DATE,老数据如果格式不统一会报错,所以这种改动要放在造数据阶段之前做。

4.3 高并发下别只靠“先查再插”

医院的热门科室放号那几秒,请求量很容易集中爆发。很多初版代码的逻辑是:查号源余量、余量大于 0 就插入记录。这在单用户测试时完全正常,人一多就翻车:两个请求同时查到余量是 1,又同时去插入,最后可能出现一条预约记录配上了负号源。解决思路是把判断放到更新语句里,也就是第 3.3 节那套写法:UPDATE ... SET remain_count = remain_count - 1 WHERE remain_count > 0。

还有一种常用做法是行级锁:

START TRANSACTION; SELECT id, remain_count FROM schedule WHERE doctor_id = ? AND work_date = ? FOR UPDATE; -- 应用层判断 remain_count > 0 UPDATE schedule SET remain_count = remain_count - 1 WHERE id = ?; INSERT INTO appointment (patient_id, doctor_id, work_date, create_time, status) VALUES (?, ?, ?, NOW(), 1); COMMIT;

SELECT FOR UPDATE 会把这一行锁住,别的会话只能等它提交。逻辑上好理解,代价是并发稍微上去就可能出现锁等待,而且必须在 finally 里确保释放。对比起来,条件更新更适合这个场景,一条 SQL 就把判断和扣减做完了,代码量也更少。

5. 避坑与排查:导入运行源码时的高频问题

1. Eclipse 导入后项目名上全是红叉

现象:源码文件都在,但所有类都报错,HttpServlet 找不到。
原因:没有把 Tomcat 运行环境挂到项目的 Build Path 上,servlet-api.jar 不在编译路径里。
解决:右键项目 → Build Path → Add Library → Server Runtime → 选已配置的 Tomcat。IDEA 里类似问题是 Project Structure 里没有给模块添加 Web Facet,补上后重新部署。

2. JSP 和数据库同时中文乱码

现象:页面标题乱码、患者姓名存进去再读出来变成问号。
原因:三个环节的编码不一致。JSP 页面编码、Servlet 里 request 解析编码、数据库表的字符集,任何一环掉队都会出乱码。
解决:统一 UTF-8。JSP 文件头设置 pageEncoding,Servlet 的 doPost 开头加 request.setCharacterEncoding("UTF-8"),连接串保持 characterEncoding=UTF-8,数据库表用 utf8mb4。三处都对齐后,乱码基本消失。

3. 点登录一直 404,或用同一个路径有时通有时不通

现象:表单提交后浏览器地址栏路径是对的,但控制台报 404。
原因:web.xml 和注解 @WebServlet 同时配置了同一个路径,或表单 action 少写了开头斜杠。项目里如果既有 web.xml 又有注解,两套机制指向同一路径时行为不确定。
解决:统一用一种方式,用注解就把 web.xml 里对应 servlet 定义删掉;用 web.xml 就检查 url-pattern 是否和表单 action 完全一致,注意前导斜杠。

4. MySQL 连接失败:Communications link failure 或 Public Key Retrieval is not allowed

现象:Tomcat 启动后访问页面,控制台抛 SQLException,连接串看起来没问题。
原因:MySQL 8 的认证插件和旧版驱动不兼容,或者缺少 serverTimezone 参数。这类问题不是源码 bug,是版本搭配问题。
解决:MySQL 8 就换驱动,把 com.mysql.jdbc.Driver 改成 com.mysql.cj.jdbc.Driver,连接串加上 serverTimezone=Asia/Shanghai;想省事就回退到 MySQL 5.7 加老驱动,这在毕设环境下完全够用。

5. 静态资源和图片加载不出来,JSP 页面总感觉要刷新一次才会好

现象:页面能打开但 CSS 全丢,图片裂开,有时刷新一下才正常。
原因:资源路径写成了绝对路径,比如 /css/style.css,换项目名或部署路径后自然 404。JSP 里用相对路径又容易跟着当前 URL 改变基址,表现为“第一次不太对,刷新就好了”。
解决:统一用 EL 表达式拼上下文路径:

<%-- 原来写死 /hospital/css/style.css 一换项目名就失效 --%> <link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css"> <img src="${pageContext.request.contextPath}/images/doctor.jpg" alt="医生图片">

这五个坑是我陆续在几套老工程里真实碰到的,每一个都能让“代码没问题”变成“跑不起来”。遇到先别怀疑源码损坏,按上面顺序排查环境,绝大多数都能救回来。

6. 把源码改出自己的毕设:验证清单与三个加分项

拿到源码并跑通后,先用一条主线把功能走一遍,确认没有隐藏问题,再动手改代码。

验证项操作路径期望结果
注册打开注册页,填写手机号和身份证号能入库并跳转登录
登录用刚注册的账号登录进入患者首页,Session 有用户信息
排班查询选择科室和日期,点击查询只显示该科室当天的可预约医生
预约选择一个医生点击预约号源余量减 1,预约记录新增
重复预约再次预约同一医生同一天提示已预约或操作失败
后台维护管理员登录,维护医生排班能增删改排班信息

主流程没问题后,我建议做三个小改造,花不了多少时间但能明显加分。第一,密码不要明文存,患者表里的 password 字段改成 MD5 加盐后的密文,答辩时被问安全性至少能接上话。第二,手机号和身份证号做双重校验,前端用正则先过滤,后端 Servlet 里再校验一遍。第三,在预约成功和失败的分支里加一行日志,用 java.util.logging 就行,记录患者 id、医生 id 和操作时间,这会让数据流程有迹可循,也显得工程化。

从那以后我每次导入这类老 JavaWeb 工程,都会强制走一遍环境核对清单:JDK 版本、Tomcat 版本、连接串编码、数据库字符集、静态资源路径,五条查完再动手改代码。这套习惯帮我省下过不少无用功,希望帮到你。

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

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

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

立即咨询