news 2026/9/14 11:48:08

JavaWeb全栈实战:医院预约挂号系统从Servlet到数据库设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb全栈实战:医院预约挂号系统从Servlet到数据库设计

简介:基于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 + JSindex.jsp, register.html收集参数,展示排班与预约结果
控制层Java ServletLoginServlet, BookServlet参数校验,调用业务,控制跳转
业务层Java ServiceUserService, ScheduleService封装预约规则,检查号源冲突
数据层JDBC / MyBatisUserDao, 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_usert_admin。排班表中total_numremain_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、密码为cab: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); }); }

参数说明:doctorIddate由页面上的下拉框和日期输入框提供;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 提交预约的后端事务:保证号源与记录一致

点击预约按钮后,前端把userIdscheduleId通过 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/statsGETdate返回总人次、各科室预约量
科室列表/admin/departmentsGET用于维护科室
新增排班/admin/schedule/addPOSTdoctorId, 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。这三个点只要不踩,项目就能在干净环境里直接跑起来。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 11:44:22

Linux文件系统结构全解析:从目录树、挂载到inode底层原理

直接用一个真实场景开头&#xff1a;你折腾 Linux 半年了&#xff0c;ls、cd、cat这些命令早就滚瓜烂熟&#xff0c;但真遇到“U盘插上为什么没有盘符”、“df显示满了可就是找不到大文件”、“删了日志但空间没释放”这类问题时&#xff0c;还是会愣住。这些问题的根源全在一个…

作者头像 李华
网站建设 2026/9/14 11:41:58

c++——模版

目录 引子 模版是什么 模版格式 函数模版 函数模版的隐式实例化 函数模版的显式实例化 原理 一些使用的例子&#xff08;帮助理解&#xff09; 模版函数的匹配原则 类模板 简单使用 类模板中的成员函数函数放在类外进行定义 原理 非类型模版参数 typename指定类…

作者头像 李华