简介:基于Java Web的医院预约挂号系统完整源码包,面向Java EE学习者与相关专业毕业生,尤其适合有Servlet/JSP基础、希望完成课程设计或毕业设计的开发者。系统覆盖患者在线预约、科室信息展示、挂号信息管理核心流程,运用Servlet处理请求、JSP动态生成页面,结合MVC设计模式与MySQL数据库交互,是理解Web服务端开发与数据库事务操作的典型工程案例。资源共577个文件,压缩包约14.75MB,其中包含55个Java源文件、78个JSP页面、116个已编译class文件,以及34个CSS、10个JS等前端资源,并附5个SQL脚本,便于导入数据库快速构建运行环境;war包和配置文件则有助于掌握部署与配置方法。所有源码均按功能模块组织,页面与逻辑代码分离,可对照学习前后端联动、表单校验及会话管理,同时还可从验证码、邮件通知等扩展功能中借鉴防恶意注册与服务优化的实现思路。目前已有2211人学习,适合需要完整参考项目、系统提升Java Web实战能力的开发者。
1. 基于JAVA WEB的医院预约挂号系统:一个毕设包背后的真实工程量
拿到这个基于JAVA WEB的医院预约挂号系统(源码+数据库).zip,大多数人第一反应是“又一个毕设项目”,但我拆开看过几十个类似包之后,可以负责任地讲:预约挂号看着简单,实际是 Java Web 里少有的同时踩中前端页面、会话管理、事务控制、并发锁号四个深水区的项目。它不像商城那样堆 CRUD,也不像管理系统那样只做表单,真正的价值集中在“号源不能超卖、医生排班不能冲突、患者预约状态必须可追溯”这几件事上。适合正在做毕设、准备 Java 面试项目经验、或者刚入职想练手 Web 工程化的人。本文直接按“架构拆解 → 数据库设计 → 核心流程实现 → 部署运行 → 避坑排查 → 进阶改造”往下走,每一步都给到能直接落地的命令和代码。
2. 先把架子立起来:三层架构与表结构设计
2.1 技术选型:为什么这个题目首选 JSP + Servlet + JDBC 而不是 Spring Boot
现在网上大量教程直接教你用 Spring Boot 写医院挂号系统,但从课题名称“基于 JAVA WEB”来看,传统 Java Web 三层架构——JSP + Servlet + JDBC——才是这个题目的本命。理由有三条:第一,大多数高校的 Java Web 课程还停留在 Servlet 阶段,答辩时评委问的也是“请求怎么走、Session 怎么存、事务怎么控制”,你用 Spring Boot 反而答不到考点上;第二,这种带数据库的源码包需要能在老版本 Tomcat 和 JDK 8 上直接跑,Servlet 方案兼容性最好;第三,预约挂号本身业务复杂度不高,用框架反而把“号源锁”这个核心逻辑包在事务注解里看不清。
我一般会保留项目的原始技术栈,只在表现层做轻量优化:前端用 JSP + JSTL,后端用 Servlet 3.0 注解方式省掉 web.xml 里的繁琐配置,JDBC 直接用 Druid 连接池加JdbcTemplate——注意这里不要引入 MyBatis,因为这会让 SQL 的可读性变差,而且很多源码包里的 SQL 是写在 Java 类里的,改成 XML 映射文件反而增加答辩风险。
2.2 六张核心表:从科室到号源的父子关系
预约挂号系统的数据库设计是面试官最爱追问的部分。我用过的表结构里,最稳定的是六张表,它们之间是严格的父子级联关系:
| 表名 | 关键字段 | 作用 |
|---|---|---|
department | dep_id, dep_name, dep_intro | 科室 |
doctor | doc_id, dep_id, doc_name, doc_title | 医生,外键关联科室 |
schedule | sch_id, doc_id, sch_date, am_pm, total_count, remain_count | 排班,定义某医生某天上午/下午有多少号 |
patient | patient_id, real_name, id_card, phone | 患者 |
appointment | app_id, sch_id, patient_id, status, create_time | 预约记录,状态区分待就诊/已就诊/已取消 |
user | user_id, username, password, role, patient_id | 登录账号,role 区分管理员、医生、患者 |
这个结构里最关键的字段是schedule.remain_count,它决定了系统能不能抗住并发。很多低质量源码包不设计这个字段,而是每次预约时去数appointment表里有多少条记录来判断是否满号,这种写法在单机测试没问题,但用户量稍大就会出现超卖——后面的章节我会重点讲这个坑。建表 SQL 按惯例放在/db/hospital.sql,用 Navicat 或命令行source导入即可。
2.3 初始化数据与字符集:导入数据库前必做的三个设置
数据库导入看起来简单,但 60% 的项目跑不起来是在这一步翻车的。第一步,建库时指定字符集,CREATE DATABASE hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;,千万别用 MySQL 默认的latin1,否则 JSP 页面写入的中文全变问号。第二步,检查 SQL 文件本身的编码,用 Notepad++ 或 VS Code 打开看右下角显示的是 UTF-8 还是 ANSI,如果是 ANSI 先用“转为 UTF-8 编码”另存一遍。第三步,导入顺序有讲究,先建库再USE hospital最后source hospital.sql,很多人直接在 Navicat 里运行整个文件,如果文件开头没有CREATE DATABASE语句就会跑到别的库里。
-- hospital.sql 文件头部通常会是这样一段安全设置,不要删掉 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- 建表顺序:先父表后子表,否则外键约束报错 DROP TABLE IF EXISTS appointment; DROP TABLE IF EXISTS schedule; DROP TABLE IF EXISTS doctor; DROP TABLE IF EXISTS department; -- ... 后续建表语句 SET FOREIGN_KEY_CHECKS = 1;这里FOREIGN_KEY_CHECKS = 0是导入阶段先把外键检查关掉,因为源文件里 DROP 表的顺序未必严格,先关闭可以避免“表存在但外键引用失败”的报错。导入完成后记得手动SET FOREIGN_KEY_CHECKS = 1再打开。
3. 挂号核心链路:排班、选号与号源锁定的实现
3.1 排班数据怎么生成:一个医生的号池从哪来
排班是预约系统的源头,管理者登录后台选择医生、日期和上午/下午时段,然后指定放号总数。这里有个很容易被忽略的现实需求:同一医生同一天不能既排上午又排下午。实现方式是在schedule表上建唯一索引,UNIQUE KEY uk_doc_date (doc_id, sch_date, am_pm),这个索引是数据库层面防止排班冲突的最后一道防线。后台 Servlet 接收参数后先查一遍有没有重叠排班,再执行插入。
// ScheduleServlet.java 核心片段,新增排班时检查冲突 String docId = req.getParameter("docId"); String schDate = req.getParameter("schDate"); String amPm = req.getParameter("amPm"); int total = Integer.parseInt(req.getParameter("totalCount")); // 关键检查:同一医生同一天同一时段只能有一条排班 String checkSql = "SELECT COUNT(*) FROM schedule WHERE doc_id = ? AND sch_date = ? AND am_pm = ?"; int count = jdbcTemplate.queryForObject(checkSql, new Object[]{docId, schDate, amPm}, Integer.class); if (count > 0) { // 这里要回一个 JSON 给前端,提示“排班冲突” resp.setContentType("application/json;charset=utf-8"); resp.getWriter().write("{\"code\":400,\"msg\":\"该医生此时间段已有排班\"}"); return; }这里的count > 0就是业务校验的第一道闸门,索引是第二道闸门。只做代码校验不做索引的话,两个管理员同时点提交就会各插一条——这是典型的“并发写冲突”,面试时把这两层防护讲出来,比单纯背框架要加分得多。
3.2 预约的核心事务:先锁号再下单
患者点击“预约”按钮时,请求链路是:前端提交schId和patientId,后端先查这个号源还剩多少,如果remain_count > 0就在同一事务里插入appointment记录并执行UPDATE schedule SET remain_count = remain_count - 1 WHERE sch_id = ?。整个过程必须在一个事务里完成,否则可能出现“预约记录写进去了,但号源数量没减”的脏数据。
// AppointmentServlet.java 预约核心事务,注意 synchronized 只对单实例有效 @Transactional public void createAppointment(String schId, String patientId) { // 第一步:悲观锁读取号源,锁住这一行直到事务结束 String lockSql = "SELECT remain_count, total_count FROM schedule WHERE sch_id = ? FOR UPDATE"; Map<String, Object> row = jdbcTemplate.queryForMap(lockSql, schId); int remain = ((Number) row.get("remain_count")).intValue(); int total = ((Number) row.get("total_count")).intValue(); // 第二步:判断是否还有号 if (remain <= 0) { throw new RuntimeException("号源已满"); } // 第三步:插入预约记录 String insertSql = "INSERT INTO appointment (sch_id, patient_id, status, create_time) VALUES (?, ?, '0', NOW())"; jdbcTemplate.update(insertSql, schId, patientId); // 第四步:扣减号源,这里带条件更新防止并发下负号 String updateSql = "UPDATE schedule SET remain_count = remain_count - 1 WHERE sch_id = ? AND remain_count > 0"; int rows = jdbcTemplate.update(updateSql, schId); if (rows == 0) { throw new RuntimeException("号源已被抢完"); } }这里的关键点是SELECT ... FOR UPDATE这条悲观锁语句——事务内先锁住这一行,其他会话的更新就必须等这个事务提交后才能执行。第四步的AND remain_count > 0是最后一道物理防线,万一前面的逻辑判断被绕过,数据库层面也不会让号减成负数。很多源码包里没有这两行,测试时用两个浏览器同时抢最后一个号就会出现两条预约记录共享一个号源的情况——这就是超卖。
3.3 取消预约与回补号源:最容易写漏的对称操作
取消预约的逻辑是预约的镜像操作:修改appointment表状态为“已取消”,同时UPDATE schedule SET remain_count = remain_count + 1 WHERE sch_id = ?。这个操作要求两个动作原子生效,不能出现“取消了但号没回收”或者“号回收了但预约还是待就诊”。代码结构和上面的createAppointment一样,先查后改,用事务包住。
有一个用户感知层面的细节:取消操作要限制在预约时间的 24 小时之前,否则医生排班已经锁定,号源回收了也约不出去。实现上是在查询时判断appointment.create_time与当前时间差,超过可取消窗口就返回“已过可取消时间”。这是医院业务里的真实规则,写进项目里起到的是业务完整度加分作用。
4. 把项目跑起来:JDK、Tomcat 与 MySQL 的版本组合与部署步骤
4.1 版本搭配:JDK 8 + Tomcat 8.5 + MySQL 5.7 是最稳的三角组合
这类源码包大多在 2020 年前后写成,依赖的生态是 JDK 8、Tomcat 8.5/9、MySQL 5.7。如果你直接上 JDK 17 + Tomcat 10 + MySQL 8.0,大概率会遇到三类报错:Tomcat 10 把javax.servlet包改名成jakarta.servlet,项目里的所有 import 全部失效;JDK 17 移除了部分反射访问权限,老版本的 Druid 连接池初始化时报InaccessibleObjectException;MySQL 8.0 的驱动类名和连接参数与 5.7 不同,jdbc:mysql://localhost:3306/hospital后面没有加useSSL=false&serverTimezone=Asia/Shanghai就直接连接失败。所以我一般会强烈建议:装这三个老版本,别用最新的。
这里给一个环境配置速查表,照着配不会错:
| 软件 | 推荐版本 | 下载文件名关键字 | 关键配置 |
|---|---|---|---|
| JDK | 1.8.0_202 | jdk-8u202-windows-x64 | JAVA_HOME 配置 |
| Tomcat | 8.5.100 | apache-tomcat-8.5.100 | 默认端口 8080 |
| MySQL | 5.7.44 | mysql-installer-community-5.7.44 | 编码选 utf8mb4 |
| IDEA | 2020.3+ | ideaIU-2020.3 | 内置 Tomcat 集成 |
MySQL 5.7 的安装有个小坑:如果你电脑上之前装过 MySQL 8.0,直接装 5.7 会提示版本冲突。最干净的方式是把原来的 MySQL 服务先net stop mysql再卸载,然后手动把C:\ProgramData\MySQL目录删掉,否则安装向导到最后一步起服务时一定失败。
4.2 IDEA 部署 web 项目的三步配置:Artifact、Tomcat、运行参数
拿到源码包后不要急着双击运行,先在 IDEA 里按以下顺序配置。第一步,打开项目后检查 Project Structure → Project 里的 SDK 是不是选到了 JDK 1.8,Language level 也改成 8。第二步,打开 Project Structure → Artifacts,点加号选 Web Application: Exploded——这里要确认 Output directory 指向out\artifacts\xxx_war_exploded,很多项目部署后 404 就是因为这个路径没配对。第三步,点右上角 Edit Configurations 加一个 Tomcat Server → Local,在 Deployment 页把刚才建好的 Artifact 加进去,Application context 填/hospital或其他项目名,这一步决定了你最终访问的 URL 路径。
# Tomcat 启动后默认目录结构,确认部署包位置是否正确 apache-tomcat-8.5.100/ ├── bin/ # startup.bat 和 shutdown.bat ├── conf/ # server.xml 端口配置,默认 8080 ├── lib/ # 注意:mysql-connector-java.jar 放这里还是项目 WEB-INF/lib 里 ├── logs/ # 运行日志,排错先看这里的 localhost 日志 ├── temp/ ├── webapps/ # IDEA 部署的 war 会展开在这里 └── work/ # JSP 编译后的 class 文件缓存在这这里有个血泪经验:数据库驱动 jar 究竟放哪。如果你用 IDEA 内置 Tomcat 启动,驱动 jar 必须放在项目的WEB-INF/lib目录里;如果你直接打 war 包塞进外部 Tomcat 的webapps目录,驱动 jar 在WEB-INF/lib也一样没问题。但如果你把 jar 单独丢在 Tomcat 的lib目录里,IDEA 内置 Tomcat 启动时未必能加载到——因为 IDEA 使用的是自己复制的 Tomcat 实例。解决方法是统一放到项目WEB-INF/lib下,不要动 Tomcat 的lib目录。
4.3 数据库连接配置:Druid 连接池的五个必调参数
源码包里一般有一个db.properties或jdbc.properties,里面写的是数据库连接信息。我用过的大部分项目用的都是 Druid 连接池,核心配置如下:
# db.properties 数据库连接配置 jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/hospital?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456 # 连接池参数,太小会排队,太大会浪费内存 jdbc.initialSize=5 jdbc.maxActive=20 jdbc.maxWait=6000driver这一个参数是版本重灾区。MySQL 5.7 配老驱动com.mysql.jdbc.Driver没问题,但如果你的库是 MySQL 8.0,驱动必须改成com.mysql.cj.jdbc.Driver,同时 URL 里要加serverTimezone=Asia/Shanghai,否则会报The server time zone value '�й���ʱ��' is unrecognized——这个报错里的乱码你看不懂没关系,直接改 URL 就能解决。maxActive=20在毕设和中小医院内部系统下完全够用,不要调到 200,连接池不是越大越好,大会拖慢数据库响应。
5. 避坑与排查:从 404 到中文乱码的 5 个高频现场
5.1 启动后访问 404,Tomcat 页面却正常,问题出在部署路径
现象:Tomcat 能启动,访问http://localhost:8080能看到 Tomcat 首页,但访问http://localhost:8080/hospital/login.jsp就是 404。原因:Artifact 没部署进去或部署后应用名不匹配。打开 IDEA 右侧的 Tomcat 配置,看 Deployment 栏,如果里面是空的,启动时项目压根没挂载。另一种常见原因是Application context填了/,访问路径就变成http://localhost:8080/login.jsp而不是/hospital/login.jsp。解决:把 Application context 设为/hospital,然后重新Build → Rebuild Project,再重启 Tomcat。
5.2 页面源码能看到中文,浏览器显示全是问号,问题出在 JSP 指令
现象:登录页输入中文用户名,提交后数据库里存的是??,页面显示也是问号。原因通常有两层:第一是 JSP 页面本身的<%@ page contentType="text/html;charset=UTF-8"%>这行被删了或写成了 ISO-8859-1,导致页面发送的请求体编码错误;第二是 Servlet 端读取请求参数时的编码设置,Tomcat 8 默认对 GET 请求按 UTF-8 解码,但对 POST 请求不一定。解决:在 JSP 页面头部确认 charset,然后在 Servlet 里总入口加一个编码过滤器。最稳妥的方式是写一个CharacterEncodingFilter,在doFilter里设置request.setCharacterEncoding("UTF-8"),前后端统一用 UTF-8 一把梭。
5.3 启动报ClassNotFoundException: com.mysql.jdbc.Driver,驱动 jar 没生效
现象:Tomcat 启动后访问任意页面,后台日志抛出java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因:驱动 jar 不在运行时的 classpath 中。解决思路是按 jar 的加载路径逐层排查:先看WEB-INF/lib目录下有没有mysql-connector-java-x.x.x.jar,没有就拷进去;有的话看 IDE 的 Artifacts 输出目录out\artifacts\xxx_war_exploded\WEB-INF\lib里有没有——有时候你 IDE 的 Libraries 里加了 jar 但 Artifact 没同步,必须手动在 Artifact 的 可用元素 里把 jar 挪到WEB-INF/lib下。
5.4 两个浏览器同时抢最后一个号,结果都预约成功,事务没生效
现象:打开两个浏览器(或浏览器无痕窗口),同时点预约,最后一个号竟然被两人都约上了。原因:预约代码没有放在事务里,或者方法没有走 Spring 代理。如果你用的是 Servlet + JDBC 原生方案,@Transactional注解是不生效的,必须手动conn.setAutoCommit(false),执行完两条 SQL 后conn.commit()。如果你在项目里引了 Spring,要检查applicationContext.xml里是否配置了tx:annotation-driven,以及 Service 类是否被 Spring 扫描到。这个坑在调试时不算最频繁,但在答辩时是最要命的,能一次讲清楚的人不多。
5.5 想看当前系统部署了哪些项目,在 Tomcat 管理界面确认
现象:不知道项目到底部署成功没有。原因:不熟悉 Tomcat 管理后台。解决:启动 Tomcat 后访问http://localhost:8080/manager/html,输入conf/tomcat-users.xml里配置的管理员账号密码,能看到所有已部署应用的列表及运行状态。如果你用的是本地起的 Tomcat,这里也能看到 IDEA 部署的hospital应用;如果列表里是红叉,点击后面的 Reload 或 Start 按钮重新加载。这个方法比我反复改代码重启要快得多,推荐先学会看状态再动手调。
6. 从能用到扛用:角色权限改造与一个验证习惯
源码包基本跑通后,想做得比别人扎实,优先改两个地方。第一个是角色权限:现在很多包里的登录逻辑只判断用户名密码对不对,没有做角色路由——患者登录进患者首页,医生登录进医生工作台,管理员登录进后台。改造方式是在user表查询时把role字段一起查出,登录成功后写入 Session,然后在 JSP 页面顶部用 JSTL 判断角色,<c:if test="${sessionScope.user.role == 'admin'}">来控制菜单显示。这个改动纯 JSP 层面就能做,不涉及业务表重构,半小时能完成,但答辩时能讲出“权限粒度”和“页面级访问控制”两个点,比单纯说“系统能预约”高一档。
第二个是号源锁的升级。我在 3.2 节用的悲观锁适合秒杀式抢号,但医院场景有大量“名额不紧张但要求不高频”的预约,此时可以用乐观锁替代——schedule表加一个version字段,每次扣减时UPDATE schedule SET remain_count = remain_count - 1, version = version + 1 WHERE sch_id = ? AND version = ?,更新行数为 0 就提示重新查询。乐观锁的优点是读多写少时不阻塞读操作,缺点是冲突时要重试。面试官追问“你怎么解决超卖”时,能把悲观锁和乐观锁的选用场景讲出来,比会写一个synchronized要有说服力得多。
说到验证习惯,我见过太多人把项目跑起来就以为完工了,其实连最基本的预约闭环都没验证。建议每次改动后按这个顺序过一遍:新建科室 → 添加医生 → 排班放号 → 患者注册 → 患者登录预约 → 号源扣减 → 取消预约 → 号源回补 → 医生登录查看预约列表 → 标记已就诊。这个链路十步走完,系统的核心功能才算真正闭环。我自己的习惯是这条链路每步截图存档,跑通后顺手把截图放进项目README.md,后面回看或者给老师演示都不用重新造数据。
另外说一个玄学:Tomcat 项目跑不起来时,最先看的不是代码,是日志。logs/localhost.2024-xx-xx.log是 Tomcat 自己的启动日志,logs/catalina.out是 Web 应用的控制台输出。我处理过的八成本地部署问题,看一眼最后 20 行日志就能定位,完全不需要在代码里到处打印。这个习惯从第一个 Web 项目养到现在,每次帮朋友调毕设,第一句都是“把 Tomcat 的 catalina.out 贴出来”,省掉很多来回试探的时间。
希望这篇文章能帮你把这个号源包从“能跑”带到“能讲清楚”,如果你卡在某个具体报错上,按第五章节的方法先做一轮排查,八成能找到出口。毕竟这类项目最大的门槛通常不是代码量,而是不知道去哪里看原因。
本文还有配套的精品资源,点击获取