简介:这是一套基于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,并且连接串要加serverTimezone=Asia/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" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai" // 8.0 必须加时区,否则报错 + "&useSSL=false"; // 本地开发关掉 SSL,避免警告 private static final String USER = "root"; private static final String PASSWORD = "你的密码";参数说明:useUnicode和characterEncoding保证中文不乱码,serverTimezone是 MySQL 8.0 的硬性要求,useSSL=false在本地开发时关掉可以避免控制台一堆 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 停用 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 医生表:关联科室,医生登录后查看自己的排班 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 排班表:管理员审批医生的排班申请 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 预约订单表:用户预约成功后写入 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 已完成 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建完表后,至少往department和doctor里插几条测试数据,否则你登录管理员后台看不到任何科室,用户端也没法选科室预约。这一步别偷懒,我见过太多人卡在「登录成功但页面空白」,最后发现是表里没数据。
2.3 项目导入 IDEA 与 Tomcat 配置
用 IDEA 新建一个 Java Enterprise 项目,或者直接 Open 源码目录。关键是把web目录标记为 Web 资源目录,把src标记为 Sources Root。然后在 Run/Debug Configurations 里加一个 Tomcat Local,Deployment 里加 Artifact,Application 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.enable=true,163 邮箱用 465 端口配mail.smtp.ssl.enable=true,混用会报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 List<WorkDay> queryAvailable(int doctorId, String date) { List<WorkDay> 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_source=1然后都下单导致超卖。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 中文乱码:注册用户名变问号
现象:用户注册后数据库里中文显示为???。原因:连接串没加characterEncoding=utf8,或者数据库和表的字符集是latin1。解决:连接串加useUnicode=true&characterEncoding=utf8,建库时用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.enable=true,163 邮箱用 465 +mail.smtp.ssl.enable=true,授权码在邮箱设置里重新生成,别填登录密码。
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 List<WorkDay> queryByPage(int doctorId, int page, int pageSize) { List<WorkDay> 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 里放一个ConcurrentHashMap<Integer, Department>,启动时加载一次,管理员增删科室时更新缓存。这样用户端选科室时直接读内存,响应快很多。注意缓存和数据库的一致性——管理员改完科室后要同步刷新缓存,否则用户看到的是旧数据。
5.3 并发压测:用 JMeter 验证号源不超卖
改造完事务和行锁后,怎么验证真的不超卖?用 JMeter 建一个线程组,100 个线程同时请求预约接口,目标号源设为 10。跑完后查orders表应该有 10 条记录,work_day.num_source应该为 0,不能是负数。如果出现负数,说明事务隔离级别或锁没生效,检查FOR UPDATE是否在事务内执行、表引擎是不是 InnoDB。这个压测我每次改完预约逻辑都会跑一遍,从那以后我每次动号源相关的代码,都强制走一遍「建表 → 插数据 → 并发下单 → 查余量」的流程,不敢省。
5.4 把 JSP 页面里的 Java 代码抽到 Servlet
原始 JSP 里大概率混着<% ... %>脚本片段,改起来痛苦。进阶做法是把业务逻辑抽到 Servlet 或 Filter,JSP 只负责用 EL 和 JSTL 渲染。比如号源列表页,Servlet 里request.setAttribute("workDays", list),JSP 里用<c:forEach>遍历。这样页面干净,调试也方便。改造时注意web.xml里的 Servlet 映射别写错,或者直接用@WebServlet注解。
希望帮到你。
本文还有配套的精品资源,点击获取