简介:基于Java、JavaScript、CSS、HTML打造的医院预约挂号系统项目包,集成源码、数据库、开发文档及项目解析,可为毕业设计、课程设计或Java Web实践提供完整参考。包内共190个文件,以55个Java后端类、39个JSP页面、17个CSS样式、5个JS脚本及3个SQL脚本为主,辅以XML配置、PNG/JPG图片和说明文档,压缩包仅1.81MB,层次鲜明便于研读。已有168人学习下载。项目围绕挂号预约管理,实现基础数据管理、预约管理、查询与统计分析三大子系统,涵盖医生、患者、科室、诊室、号源池、排班与预约时段等核心模块,并遵循先预约、开放全部号源等业务规则。开发文档与项目解析帮助快速掌握系统架构与实现思路,测试通过的源码可直接作为基线二次开发。
1. 从课程设计到简历项目:医院预约挂号系统到底考察什么
在课程设计和开源源码里,“基于java+javaScript+css+html开发的医院预约挂号系统”是被最多人下载的标题之一。Java 提供后端业务逻辑,JavaScript 负责页面交互,CSS 和 HTML 搭出界面,数据库存储用户、科室、医生与预约记录,再加上数据库文件和开发文档,整体就是一个标准的 JavaWeb 全栈练习。这个系统看起来只是增删改查,实际考察了 Servlet 生命周期、会话管理、SQL 表设计、事务一致性、前端事件绑定,以及面对答辩时如何讲清项目亮点。无论你是补齐课设代码,还是想用这个题目准备“java面试题”里的 web 部分,下面的路径都值得顺着走一遍:选定技术栈、建好数据库、实现预约核心链路,最后把开发文档和项目解析写成能让人一眼看懂的形态。
2. 技术选型与三层架构:把Java、JavaScript、CSS、HTML放到各自的位置
2.1 为什么用JSP/Servlet而不是Spring Boot?先说清取舍
医院预约挂号系统的源码包大多长这样:src/main/java下按 controller/service/dao 分包,webapp下放 JSP 和静态资源。之所以绕开 Spring Boot,不是因为它不好,而是课程设计与面试准备需要你把手写链路说清楚。Servlet 是请求入口,能看到 HTTP 如何变成 Java 对象;JSP 负责渲染动态页面;JDBC 或 MyBatis 负责数据库读写。这样可以在不引入过多配置的前提下,把 JavaWeb 的核心流程走一遍。从验收角度,纯 Servlet 也更容易在答辩时画三层架构图。常见分层如下:
| 层 | 技术 | 典型文件 | 职责 |
|---|---|---|---|
| 表现层 | JSP + HTML + CSS + JS | index.jsp, register.html | 收集参数,展示排班与预约结果 |
| 控制层 | Java Servlet | LoginServlet, BookServlet | 参数校验,调用业务,控制跳转 |
| 业务层 | Java Service | UserService, ScheduleService | 封装预约规则,检查号源冲突 |
| 数据层 | JDBC / MyBatis | UserDao, ScheduleDao | 增删改查,事务提交 |
2.1.1 Servlet 的线程模型与请求处理
Servlet 默认单实例多线程,多个请求会并发进入 doGet/doPost。预约挂号是典型的写多读少场景,必须避免把用户数据放到实例变量里,否则一个请求的数据会污染另一个请求。正确做法是使用 service 方法内的局部变量。下面是一个登录接口的最小实现:
@WebServlet("/login") public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userService.login(username, password); if (user == null) { resp.sendRedirect("login.jsp?error=1"); return; } req.getSession().setAttribute("loginUser", user); resp.sendRedirect("index.jsp"); } }这段代码中req.getParameter从表单拿到用户名和密码,userService.login去数据库比对,比对成功后把用户对象放进 session,随后重定向到首页。参数说明:表单中<input name="username">的名称必须与 Java 代码里的字符串一致;sendRedirect会丢失 request 域属性,所以登录失败我用 query string 传 error 码;实际开发中密码不应明文存储,后面第 4 章会补上加密处理。
2.1.2 JSP 与 HTML 的边界,CSS hover 算不算事件
JSP 会被容器编译成 Servlet,所以页面里可以直接写 Java,但过量的 Scriptlet 会让前端难以维护。我一般这样约定:JSP 只用 EL 和 JSTL 渲染后端数据,样式全部交给 CSS,点击和动态行为交给 JavaScript。比如首页的科室卡片,鼠标移入的浮起效果不需要写 JS,一个 hover 规则就够了:
/* hospital-card.css */ .hospital-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); }这段代码只有三行,却对应了很多人搜的“css 鼠标移入事件”和“三行模式的css文件”,实际上它只是 CSS 伪类,与 JavaScript 的 mouseenter 事件是两回事。用这种方式把表现分离,后续替换成 Vue 或 React 时,样式仍然可以复用。
2.2 前端三件套在预约页面里的具体分工
患者端首页要选择科室、选择医生、选择日期、确认预约。HTML 负责表单结构,CSS 负责卡片布局和响应式,JavaScript 负责联动请求。这里最常用的“javascript函数”是loadDoctors()、loadSchedules()这类异步加载函数。以“选择科室后加载医生”为例:
// appointment.js document.getElementById('deptSelect').addEventListener('change', async (event) => { const deptId = event.target.value; if (!deptId) return; const resp = await fetch('api/doctors?deptId=' + deptId); const doctors = await resp.json(); const doctorSelect = document.getElementById('doctorSelect'); doctorSelect.innerHTML = ''; doctors.forEach(doctor => { const option = document.createElement('option'); option.value = doctor.id; option.textContent = doctor.name + ' ' + doctor.title; doctorSelect.appendChild(option); }); });这段代码注册了 change 事件,用户切换科室后向api/doctors发起异步请求,得到的 JSON 数组再渲染成<option>。参数说明:deptId来自 select 的 value,后端要校验它是纯数字;fetch 请求需要 catch 网络异常,否则接口挂掉时页面没有提示。如果想更严谨,可以禁用当前无号源的医生选项,用一个 disabled 属性即可。
2.3 数据库连接:JDBC 还是 MyBatis,课程设计怎么选
虽然标题里没有提框架,但“数据库”是硬指标。原生 JDBC 能展示你对 Connection、PreparedStatement、ResultSet 的理解,代码量偏大;MyBatis 把 SQL 独立到 Mapper,适合有 Spring 经验的开发者。如果这是我的项目,我选“JDBC 连接池 + DAO”的方式。好处是:不依赖额外框架,答辩时可以从 URL、用户名密码、驱动加载一直讲到结果集封装;表关联不复杂,手写 SQL 完全可控;源码包里的数据库脚本可以直接导入 MySQL,不会因为框架版本差异跑不起来。
public boolean insertReservation(Reservation res) { String sql = "INSERT INTO reservation (user_id, schedule_id, record_time, status) VALUES (?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, res.getUserId()); ps.setInt(2, res.getScheduleId()); ps.setTimestamp(3, new Timestamp(res.getRecordTime().getTime())); ps.setInt(4, 1); return ps.executeUpdate() > 0; } catch (Exception e) { System.out.println("insert reservation failed: " + e.getMessage()); return false; } }DBUtil.getConnection()从 Druid 连接池拿连接,try-with-resources自动关闭 Statement 和 Connection。PreparedStatement 的四个参数分别对应预约用户、排班 ID、预约时间和状态,状态 1 表示已预约。这里要提醒一个细节:如果项目使用 MySQL 8,连接串必须带serverTimezone=Asia/Shanghai,否则setTimestamp写入的日期会差 8 小时。
3. 数据库设计:医院预约挂号系统的表结构与号源模型
3.1 核心表清单:从用户到预约记录
预约系统的表一般不少于 6 张。下面是一张可以直接作为开发文档基础的表清单:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| t_user | 患者用户 | id, username, password, real_name, phone, salt |
| department | 科室 | id, dept_name, dept_desc |
| doctor | 医生 | id, dept_id, doctor_name, title, profile |
| schedule | 医生排班 | id, doctor_id, work_date, start_time, end_time, total_num, remain_num |
| reservation | 预约记录 | id, user_id, schedule_id, record_time, status |
| t_admin | 管理员 | id, username, password |
表名尽量不要用user,因为它是 MySQL 的系统库表名之一,虽然可以加反引号,但为了省事,我通常在项目里写成t_user、t_admin。排班表中total_num和remain_num是一对,前者表示当前时段总号源,后者表示剩余号源,二者共同支撑预约扣减逻辑。
3.2 建表 SQL:带外键和唯一约束的完整脚本
下面的 SQL 适合 MySQL 8.0。先建用户表,再建科室和医生,最后是排班和预约。t_user表里的salt字段用于密码加盐,登录时会用到。
CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, real_name VARCHAR(30), phone VARCHAR(20), salt VARCHAR(16) ); CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL, dept_desc VARCHAR(255) ); CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, dept_id INT NOT NULL, doctor_name VARCHAR(50) NOT NULL, title VARCHAR(30), profile TEXT, FOREIGN KEY (dept_id) REFERENCES department(id) ); CREATE TABLE schedule ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, work_date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, total_num INT NOT NULL DEFAULT 20, remain_num INT NOT NULL DEFAULT 20, FOREIGN KEY (doctor_id) REFERENCES doctor(id), UNIQUE KEY uk_doctor_date (doctor_id, work_date, start_time) ); CREATE TABLE reservation ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, schedule_id INT NOT NULL, record_time DATETIME NOT NULL, status TINYINT DEFAULT 1 COMMENT '1已预约 0已取消 2已完成', FOREIGN KEY (user_id) REFERENCES t_user(id), FOREIGN KEY (schedule_id) REFERENCES schedule(id), UNIQUE KEY uk_user_schedule (user_id, schedule_id) );SQL 中uk_doctor_date是复合唯一索引,约束同一个医生在同一个工作日同一个开始时间只能有一条排班,后台重复添加会直接报错。uk_user_schedule则保证患者不能连续点击两次提交产生两条预约。注意FOREIGN KEY在 MySQL 5.7 以上默认是 InnoDB 引擎才支持,如果建表后外键没有生效,先检查表引擎。
3.3 号源扣减与并发控制:一条 UPDATE 处理超卖
预约系统最大的坑是并发超卖。两个浏览器同时点击预约,先 select 再 update 会觉得余号都大于 0,最终都落库。这里我会直接使用带条件更新的 SQL:
UPDATE schedule SET remain_num = remain_num - 1 WHERE id = ? AND remain_num > 0;执行后影响行数为 1,说明扣减成功;为 0 说明号源已满,直接返回“号源不足”,不需要再发起重试。如果要求预约记录和扣减同时成功,就把这条 Update 和 Insert reservation 放在同一个事务里。常见做法是在 Service 层开启事务,把 Connection 传给 DAO,确保两个操作要么都提交要么都回滚。这一小节对应的就是数据库课程设计里经常出现的“并发控制”考点,也解释了为什么项目解析里要写“使用原子 SQL 避免超卖”。
4. 核心功能实现:Java Servlet与JavaScript联手完成登录、排班与预约
4.1 用户注册与密码加密:从HTML表单到Servlet
注册页面通常包含用户名、密码、确认密码、手机号和真实姓名。页面开头要写<!DOCTYPE html>和<meta charset="utf-8">,否则中文用户名传到 Servlet 就会乱码。HTML 表单提交前先由 JavaScript 函数做基础校验,下面是最小实现:
<form action="register" method="post" onsubmit="return validate()"> <input type="text" name="username" id="username" placeholder="用户名"> <input type="password" name="password" id="password" placeholder="密码"> <input type="text" name="phone" id="phone" placeholder="手机号"> <button type="submit">注册</button> </form>function validate() { const username = document.getElementById('username').value; const phone = document.getElementById('phone').value; if (username.trim().length < 3) { alert('用户名至少3位'); return false; } if (!/^1[3-9]\d{9}$/.test(phone)) { alert('手机号格式不正确'); return false; } return true; }这里的正则/^1[3-9]\d{9}$/只负责前端格式检查,真正安全校验必须依赖 Java 端。我在RegisterServlet里会重复做一次非空校验,因为前端校验可以被绕过。
4.1.1 密码加盐处理
课程设计源码里经常看到直接存明文密码的版本,答辩时这是扣分点。我一般用加盐 MD5 或 SHA-256 做演示,生产环境再换 BCrypt。代码:
public String encrypt(String password, String salt) { String toEncrypt = salt + ":" + password; return DigestUtils.md5Hex(toEncrypt); }salt可以在注册时用UUID.randomUUID().toString().substring(0, 8)生成,然后和用户数据一起存到salt字段。登录时先根据用户名取出 salt,再用传入密码计算摘要,与数据库里的密文比较。用冒号连接 salt 和密码,是为了避免边界碰撞,例如 salt 为a、密码为bc会产生a:bc,与 salt 为ab、密码为c的ab:c不同。
4.2 医生排班与日期联动:JavaScript函数配合CSS反馈
排班页是预约系统的核心页面。患者先选科室,再选医生,然后选日期,系统展示当天该医生的可约时段和剩余号源。我用一个loadSchedules(doctorId, date)函数完成联动:
async function loadSchedules(doctorId, date) { if (!doctorId || !date) return; const resp = await fetch(`api/schedule?doctorId=${doctorId}&date=${date}`); const list = await resp.json(); const container = document.getElementById('scheduleList'); container.innerHTML = ''; list.forEach(item => { const btn = document.createElement('button'); btn.className = item.remainNum > 0 ? 'slot-btn' : 'slot-btn disabled'; btn.textContent = `${item.startTime} 剩余${item.remainNum}`; btn.disabled = item.remainNum <= 0; btn.addEventListener('click', () => { document.getElementById('scheduleId').value = item.id; }); container.appendChild(btn); }); }参数说明:doctorId和date由页面上的下拉框和日期输入框提供;remainNum的来源是后端查询排班表;按钮禁用状态直接由剩余号源决定,避免用户点开一个已经满号的时段。给可用按钮写一个悬停反馈,能明显提升交互观感:
.slot-btn:hover:not(:disabled) { background-color: #1890ff; color: #fff; transition: background-color 0.2s ease; }这段 CSS 用到了:hover和:not(:disabled),它会在按钮可点时改变背景色,让用户知道可以点击。很多人搜“css 从入门到精通——动画”时关心的是 transition 和 keyframes,这里用 transition 就够了,不需要动画效果。
4.3 提交预约的后端事务:保证号源与记录一致
点击预约按钮后,前端把userId、scheduleId通过 POST 提交到BookServlet。注意:userId不应从表单获取,而应该从 session 拿,否则任意用户都可以伪造别人的 ID。示例:
@WebServlet("/book") public class BookServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); Object loginUser = session.getAttribute("loginUser"); if (loginUser == null) { resp.sendRedirect("login.jsp?error=login"); return; } int userId = ((User) loginUser).getId(); int scheduleId = Integer.parseInt(req.getParameter("scheduleId")); boolean ok = reservationService.book(userId, scheduleId); if (ok) { resp.sendRedirect("book_success.jsp"); } else { resp.sendRedirect("book_fail.jsp?reason=no_remain"); } } }参数说明:scheduleId来自隐藏域或按钮 value,提交前要确认它对应的是可约时段。book方法内部执行两步操作:先执行带条件的UPDATE schedule SET remain_num=remain_num-1 WHERE id=? AND remain_num>0,影响行数为 1 后,再插入预约记录。这两步放在同一个事务里,保证不会出现“号源扣了但记录没生成”的脏数据。如果项目用 MyBatis,可以给book方法加@Transactional,用 Spring 的事务注解来管理。
4.4 后台管理:预约统计与状态标签
管理员端一般包括科室管理、医生管理、排班管理、预约记录查看。最简单的首页是展示今日预约总人次、各科室预约量、剩余号源。这里可以先看几个主要接口:
| 功能 | 请求地址 | 方法 | 主要参数 | 说明 |
|---|---|---|---|---|
| 查看今日预约 | /admin/stats | GET | date | 返回总人次、各科室预约量 |
| 科室列表 | /admin/departments | GET | 无 | 用于维护科室 |
| 新增排班 | /admin/schedule/add | POST | doctorId, workDate, startTime, endTime | 创建排班 |
用 JSP + JSTL 输出表格时,可以把状态码翻译成可读文字:
<c:choose> <c:when test="${r.status == 1}">已预约</c:when> <c:when test="${r.status == 0}">已取消</c:when> <c:otherwise>已完成</c:otherwise> </c:choose>这里r.status对应 reservation 表的 TINYINT 字段,JSTL 的c:choose在服务端渲染,比在 Java 里拼接写死的方案更容易维护。给不同状态配不同颜色,用 CSS 类名控制:
.status-1 { color: #1677ff; } .status-0 { color: #999; } .status-2 { color: #52c41a; }这种“枚举状态 + CSS 类”的模式是后台管理的常见做法。当预约记录达到几百条时,记得给reservation.schedule_id加索引,否则联表查询会越来越慢。数据库层面的优化,往往比代码层面的优化更值得先做。
5. 开发文档与项目解析:让答辩和面试官一眼看到亮点
5.1 项目解析文档的六段式结构
拿到源码后,不要急着改代码,先按下面顺序梳理文档。需求分析写角色和用例,数据库设计放 ER 图和建表脚本,技术架构画三层图,功能实现列出关键接口和核心代码,测试用例写输入输出和预期结果,部署说明写 JDK、Tomcat、MySQL 的版本与配置步骤。文档每段控制在半页以内,重点写“为什么这样做”,而不是贴大段代码。答辩时被问到的绝大多数问题,都能在这六段里找到出处。
5.2 用日志和断点验证预约链路
很多人的项目出问题后只会看页面报错,实际上要先看 Tomcat 控制台。在ReservationService.book方法里加一行日志:
System.out.println("book attempt: user=" + userId + " schedule=" + scheduleId + " time=" + System.currentTimeMillis());这行日志能确认请求是否到了业务层。如果日志出现但页面仍然失败,多半是事务回滚,可以把异常堆栈打印出来,重点看remain_num是否被更新。断点调试时,先看scheduleId是否为空字符串,再看Connection是否在同一线程,这两个是常见的隐身问题。
5.3 部署时最容易踩的三个环境坑
第一,JDK 版本与 Tomcat 不匹配,Tomcat 9 需要 JDK 8 以上,Tomcat 10 的包名从javax.servlet改为jakarta.servlet,旧代码直接放到 Tomcat 10 会编译不通过。第二,MySQL 8 使用com.mysql.cj.jdbc.Driver,连接串必须加时区,否则写入数据库的预约时间比实际少 8 小时:
jdbc:mysql://localhost:3306/hospital?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai第三,静态资源路径问题。JSP 里引用 CSS、JS 时,前面要加${pageContext.request.contextPath},例如:
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/hospital-card.css">不加上下文路径,部署到http://localhost:8080/hospital/时样式会 404。部署前先按这个顺序检查:web.xml或注解里的 Servlet URL 是否带斜杠、MySQL 驱动是 5.x 还是 8.x、JSP 页面是否用了 JSTL 但 lib 下没有 jstl.jar。这三个点只要不踩,项目就能在干净环境里直接跑起来。
本文还有配套的精品资源,点击获取