
简介这是一套基于JSP与MySQL开发的在线医疗预约挂号管理系统源码面向计算机专业学生、Java Web初学者及需要课程设计或毕业设计参考的开发者帮助解决医院挂号流程数字化、科室与医生排班管理等实际问题。压缩包共206个文件约1010KB其中72个jsp页面承担前后端交互与视图展示55个java文件实现预约、排班、验证码、邮件发送等业务逻辑另有72个png图片资源、2个properties配置及2个md说明文档结构完整便于二次开发。系统区分管理员、用户、医生三种角色管理员可查看预约折线图、管理医生患者与科室信息、审核排班申请用户可注册登录、接收验证码、浏览科室简介并完成预约医生可查看排班与患者队列、修改个人信息。已有53人学习下载适合作为Java Web综合实践参考能帮助读者理解MVC分层、数据库操作与多角色权限控制的实现思路。1. 从一份 JSP 挂号系统源码说起它到底能跑通什么如果你手头正好有一份基于 JSP MySQL 的在线医疗预约挂号管理系统源码第一反应大概率是这东西现在还有人用吗Spring Boot 都卷成这样了JSP 是不是已经进博物馆了先别急着下结论。我拆过不少 JavaWeb 课程设计和毕设项目JSP 这套技术栈在「能跑通、能讲清、能改」这三个维度上反而比很多花哨的框架更适合拿来当完整业务系统的练手底稿。这份源码覆盖了管理员、用户、医生三个角色的完整闭环——管理员管科室和医生排班用户注册登录后按科室预约医生查看自己的患者队列和排班申请。它不是玩具级的增删改查而是有验证码、有邮件找回密码、有排班申请审批流的真实业务骨架。你拿到的是一个.zip包解压后能看到Order.java、DBUtil.java、NumSourceDao.java、WorkDayDao.java、FindPwd.java、SendMail.java、ModifyPwd.java、QueryNumSource.java这些类文件外加1.html这样的静态页面。从命名就能看出分层意图DBUtil管连接Dao管数据访问SendMail和FindPwd管邮件找回密码链路QueryNumSource管号源查询。这套结构放在今天看不算优雅但胜在直白——每一层干什么文件名已经告诉你了。适合谁正在做 JavaWeb 课程设计、需要一份能讲清楚业务逻辑的毕设底稿、或者想通过一个完整项目把 JSP Servlet JDBC 串起来的人。不适合谁想直接拿去做生产级医疗系统的人——安全、并发、审计这些它都没覆盖。2. 环境搭建与数据库初始化把 MySQL 和 Tomcat 先伺候好2.1 JDK、Tomcat 与 MySQL 的版本选择这份源码用的是传统 JSP Servlet 架构没有 Maven 或 Gradle 构建文件所以你的环境得自己配。JDK 选 8 或 11 都行再高也不是不能跑但 JSP 编译时偶尔会碰到Unsupported class file major version这类玄学报错我一般直接锁 JDK 8省心。Tomcat 用 8.5 或 9.0别上 Tomcat 10——Tomcat 10 把javax.servlet换成了jakarta.servlet这份源码里的import javax.servlet.*全部会编译失败这是血泪经验不是理论推测。MySQL 用 5.7 或 8.0 都可以但 8.0 要注意驱动包版本和连接串参数。源码里DBUtil.java大概率写的是com.mysql.jdbc.Driver这是 5.x 的驱动类名8.0 要改成com.mysql.cj.jdbc.Driver并且连接串要加serverTimezoneAsia/Shanghai否则启动就报时区错误。下面是一个典型的DBUtil配置片段你可以对照自己的源码改// DBUtil.java 中数据库连接的核心参数 private static final String DRIVER com.mysql.cj.jdbc.Driver; // MySQL 8.0 用 cj 驱动 private static final String URL jdbc:mysql://localhost:3306/hospital_db ?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/Shanghai // 8.0 必须加时区否则报错 useSSLfalse; // 本地开发关掉 SSL避免警告 private static final String USER root; private static final String PASSWORD 你的密码;参数说明useUnicode和characterEncoding保证中文不乱码serverTimezone是 MySQL 8.0 的硬性要求useSSLfalse在本地开发时关掉可以避免控制台一堆 SSL 警告。如果你用的是 MySQL 5.7驱动类名改回com.mysql.jdbc.Driver时区参数可以不加。2.2 建库建表与初始数据导入源码包里通常会带一个.sql文件但有些课程设计项目会把建表语句散落在 Dao 类的注释里或者干脆没有。如果没有你得根据Order.java、NumSourceDao.java这些类里的 SQL 语句反推表结构。常见做法是先找DBUtil里连的库名然后全局搜索CREATE TABLE或insert into把涉及的表名和字段列出来。下面是一个根据这类系统常见结构整理的建表参考-- 科室表管理员维护用户预约时选择 CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, intro TEXT, status TINYINT DEFAULT 1 -- 1 启用 0 停用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 医生表关联科室医生登录后查看自己的排班 CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL, dept_id INT, title VARCHAR(30), account VARCHAR(30) UNIQUE, password VARCHAR(64), FOREIGN KEY (dept_id) REFERENCES department(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 排班表管理员审批医生的排班申请 CREATE TABLE work_day ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT, work_date DATE, num_source INT DEFAULT 0, -- 号源数量 status TINYINT DEFAULT 0, -- 0 待审批 1 已通过 2 已拒绝 FOREIGN KEY (doctor_id) REFERENCES doctor(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 预约订单表用户预约成功后写入 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT, doctor_id INT, work_day_id INT, order_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 -- 1 已预约 2 已取消 3 已完成 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建完表后至少往department和doctor里插几条测试数据否则你登录管理员后台看不到任何科室用户端也没法选科室预约。这一步别偷懒我见过太多人卡在「登录成功但页面空白」最后发现是表里没数据。2.3 项目导入 IDEA 与 Tomcat 配置用 IDEA 新建一个 Java Enterprise 项目或者直接 Open 源码目录。关键是把web目录标记为 Web 资源目录把src标记为 Sources Root。然后在 Run/Debug Configurations 里加一个 Tomcat LocalDeployment 里加 ArtifactApplication context 设成/hospital之类的短路径。启动后访问http://localhost:8080/hospital/1.html如果能看到首页说明静态资源通了再试登录接口如果报 500看控制台是不是数据库连接失败。提示IDEA 社区版没有内置 Tomcat 支持要么换 Ultimate要么手动用 Tomcat 的startup.bat部署。手动部署时把编译后的classes和web目录一起拷到webapps下改server.xml加 Context虽然土但能跑。3. 核心业务链路拆解从验证码到预约成功的完整代码路径3.1 用户注册与邮箱验证码链路用户注册这块源码里有SendMail.java和FindPwd.java说明它用了邮件发送验证码来做注册验证或密码找回。SendMail大概率封装了 JavaMail 的Transport.send你需要改的是发件人邮箱和授权码。常见做法是用 QQ 邮箱或 163 邮箱的 SMTP 服务在邮箱设置里开 SMTP 并生成授权码然后填到SendMail的静态常量里// SendMail.java 中邮件发送的核心配置 public class SendMail { private static final String HOST smtp.qq.com; // SMTP 服务器 private static final String PORT 587; // 端口QQ 邮箱用 587 private static final String FROM 你的邮箱qq.com; // 发件人 private static final String AUTH_CODE 你的授权码; // 不是登录密码 public static boolean send(String to, String code) { Properties props new Properties(); props.put(mail.smtp.host, HOST); props.put(mail.smtp.port, PORT); props.put(mail.smtp.auth, true); props.put(mail.smtp.starttls.enable, true); // 启用 TLS Session session Session.getInstance(props, new Authenticator() { protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(FROM, AUTH_CODE); } }); try { Message msg new MimeMessage(session); msg.setFrom(new InternetAddress(FROM)); msg.setRecipient(Message.RecipientType.TO, new InternetAddress(to)); msg.setSubject(注册验证码); msg.setText(您的验证码是 code 5 分钟内有效。); Transport.send(msg); return true; } catch (MessagingException e) { e.printStackTrace(); return false; } } }逻辑说明Session.getInstance拿到邮件会话Authenticator做 SMTP 认证MimeMessage组装邮件内容Transport.send真正发出。参数上最容易翻车的是端口和加密方式——QQ 邮箱用 587 端口配starttls.enabletrue163 邮箱用 465 端口配mail.smtp.ssl.enabletrue混用会报Could not connect to SMTP host。验证码的生成和校验一般在 Servlet 里做生成 6 位随机数存 session提交时比对过期时间自己加个System.currentTimeMillis()判断。3.2 号源查询与预约下单的 Dao 层实现QueryNumSource.java和NumSourceDao.java负责号源查询Order.java负责预约订单。典型流程是用户选科室 → 查该科室医生 → 查医生未来几天的排班和剩余号源 → 点预约 → 写orders表并扣减work_day的号源。下面是一个简化版的号源查询和扣减逻辑// NumSourceDao.java 中查询可用号源的方法 public ListWorkDay queryAvailable(int doctorId, String date) { ListWorkDay list new ArrayList(); String sql SELECT * FROM work_day WHERE doctor_id ? AND work_date ? AND status 1 AND num_source 0; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, doctorId); ps.setString(2, date); ResultSet rs ps.executeQuery(); while (rs.next()) { WorkDay wd new WorkDay(); wd.setId(rs.getInt(id)); wd.setNumSource(rs.getInt(num_source)); list.add(wd); } } catch (SQLException e) { e.printStackTrace(); } return list; } // Order.java 中预约下单并扣减号源 public boolean makeOrder(int userId, int workDayId) { String checkSql SELECT num_source FROM work_day WHERE id ? FOR UPDATE; String insertSql INSERT INTO orders (user_id, work_day_id) VALUES (?, ?); String updateSql UPDATE work_day SET num_source num_source - 1 WHERE id ?; Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 锁定号源行防止并发超卖 PreparedStatement check conn.prepareStatement(checkSql); check.setInt(1, workDayId); ResultSet rs check.executeQuery(); if (rs.next() rs.getInt(num_source) 0) { // 2. 插入订单 PreparedStatement insert conn.prepareStatement(insertSql); insert.setInt(1, userId); insert.setInt(2, workDayId); insert.executeUpdate(); // 3. 扣减号源 PreparedStatement update conn.prepareStatement(updateSql); update.setInt(1, workDayId); update.executeUpdate(); conn.commit(); return true; } conn.rollback(); return false; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { try { if (conn ! null) conn.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } } }逻辑说明FOR UPDATE在事务里锁住号源行防止两个用户同时读到num_source1然后都下单导致超卖。setAutoCommit(false)开启事务三步操作要么全成功要么全回滚。参数上注意work_day.status 1表示排班已审批通过num_source 0表示还有号。如果你把FOR UPDATE去掉在并发测试下大概率能看到号源变成负数——这是这类课程设计源码最常见的坑之一。3.3 管理员排班审批与医生队列查看管理员登录后能看到排班申请列表审批通过后work_day.status从 0 改成 1用户端才能查到号源。医生登录后看自己的患者队列本质是按doctor_id查orders表关联user表。这部分逻辑不复杂但要注意权限过滤——医生只能看自己的队列不能看别人的。常见做法是在 Servlet 里从 session 取doctorId然后传给 Dao 做条件查询。如果你发现医生登录后能看到全部患者检查一下 SQL 里有没有加WHERE doctor_id ?。4. 避坑与排查这套源码跑不起来时先看这五条4.1 启动报 ClassNotFoundException: com.mysql.jdbc.Driver现象Tomcat 启动或第一次访问数据库时报驱动类找不到。原因MySQL 8.0 的驱动 jar 包里类名是com.mysql.cj.jdbc.Driver源码里写的是旧版类名。解决把DBUtil里的DRIVER常量改成com.mysql.cj.jdbc.Driver同时确认WEB-INF/lib下放的是mysql-connector-java-8.0.x.jar而不是 5.x 的包。4.2 中文乱码注册用户名变问号现象用户注册后数据库里中文显示为???。原因连接串没加characterEncodingutf8或者数据库和表的字符集是latin1。解决连接串加useUnicodetruecharacterEncodingutf8建库时用CREATE DATABASE hospital_db DEFAULT CHARSET utf8mb4已有表用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4改。4.3 邮件发送超时或 535 认证失败现象点「获取验证码」后页面卡住控制台报Could not connect to SMTP host或535 Authentication failed。原因端口和加密方式不匹配或者用了登录密码而不是授权码。解决QQ 邮箱用 587 starttls.enabletrue163 邮箱用 465 mail.smtp.ssl.enabletrue授权码在邮箱设置里重新生成别填登录密码。4.4 预约后号源没减或者减成负数现象用户预约成功但num_source没变或者并发下变成 -1。原因扣减逻辑没放在事务里或者没加FOR UPDATE行锁。解决按 3.2 的代码把查询、插入、扣减包在一个事务里查询时加FOR UPDATE。如果用的是 MyISAM 引擎行锁不生效改成 InnoDB。4.5 Tomcat 10 下 JSP 编译报 javax.servlet 不存在现象换到 Tomcat 10 后所有 JSP 和 Servlet 编译失败提示package javax.servlet does not exist。原因Tomcat 10 把 Servlet API 的包名从javax.servlet迁到了jakarta.servlet。解决换回 Tomcat 8.5 或 9.0或者全局替换javax.servlet为jakarta.servlet并换用对应版本的 jar 包。对于这份源码我建议直接换 Tomcat 9改包名的工作量不值当。5. 进阶改造把号源查询加上分页和缓存再补一个并发压测5.1 号源列表分页从全量查询到 LIMIT 分页原始QueryNumSource大概率是一次性查出所有号源科室多了页面会卡。改造思路是加page和pageSize参数SQL 用LIMIT ?, ?。下面是一个分页查询的 Dao 方法// NumSourceDao.java 加分页查询 public ListWorkDay queryByPage(int doctorId, int page, int pageSize) { ListWorkDay list new ArrayList(); String sql SELECT * FROM work_day WHERE doctor_id ? AND status 1 ORDER BY work_date ASC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, doctorId); ps.setInt(2, (page - 1) * pageSize); // 起始偏移量 ps.setInt(3, pageSize); // 每页条数 ResultSet rs ps.executeQuery(); while (rs.next()) { WorkDay wd new WorkDay(); wd.setId(rs.getInt(id)); wd.setWorkDate(rs.getDate(work_date)); wd.setNumSource(rs.getInt(num_source)); list.add(wd); } } catch (SQLException e) { e.printStackTrace(); } return list; }参数说明page从 1 开始(page - 1) * pageSize算出偏移量pageSize建议 10 到 20。前端 JSP 里加「上一页 / 下一页」链接把page作为 URL 参数传回来。别忘了再写一个countByDoctor方法查总条数用来算总页数。5.2 用 ConcurrentHashMap 缓存科室列表科室列表变动频率极低每次预约都查库没必要。我一般会在 ServletContext 里放一个ConcurrentHashMapInteger, Department启动时加载一次管理员增删科室时更新缓存。这样用户端选科室时直接读内存响应快很多。注意缓存和数据库的一致性——管理员改完科室后要同步刷新缓存否则用户看到的是旧数据。5.3 并发压测用 JMeter 验证号源不超卖改造完事务和行锁后怎么验证真的不超卖用 JMeter 建一个线程组100 个线程同时请求预约接口目标号源设为 10。跑完后查orders表应该有 10 条记录work_day.num_source应该为 0不能是负数。如果出现负数说明事务隔离级别或锁没生效检查FOR UPDATE是否在事务内执行、表引擎是不是 InnoDB。这个压测我每次改完预约逻辑都会跑一遍从那以后我每次动号源相关的代码都强制走一遍「建表 → 插数据 → 并发下单 → 查余量」的流程不敢省。5.4 把 JSP 页面里的 Java 代码抽到 Servlet原始 JSP 里大概率混着% ... %脚本片段改起来痛苦。进阶做法是把业务逻辑抽到 Servlet 或 FilterJSP 只负责用 EL 和 JSTL 渲染。比如号源列表页Servlet 里request.setAttribute(workDays, list)JSP 里用c:forEach遍历。这样页面干净调试也方便。改造时注意web.xml里的 Servlet 映射别写错或者直接用WebServlet注解。希望帮到你。本文还有配套的精品资源点击获取