☰
JSP+MySQL医疗挂号系统源码解析与部署实战
2026/10/6 12:45:45 网站建设 项目流程

简介:这是一套面向Java Web初学者与课程设计者的在线医疗预约挂号系统实战源码,基于JSP+Servlet+MySQL技术栈实现,覆盖前后端完整业务流程,适用于高校Web开发实训、毕业设计参考及小型医疗信息化原型构建。资源共206个文件,包含72个JSP页面(承担前端展示与交互)、55个Java类(含Order、DBUtil、SendMail、ModifyPwd等核心业务与工具类)、72张PNG界面素材,以及HTML、properties配置文件等,整体压缩包仅1010KB,结构紧凑、模块清晰,便于快速部署与二次开发。已有53人学习下载,资源提供完整的三角色权限体系:管理员可进行医生/患者/科室管理及排班监控;用户完成注册登录、科室浏览与在线预约;医生支持排班查看与队列管理,代码逻辑完整、注释规范,附带验证码发送、密码找回、数据库连接等实用功能模块,是理解MVC分层架构与医疗场景业务建模的优质入门范例。

1. 这不是又一个“Hello World”JSP demo:它是一套能跑通挂号全流程的MySQL医疗系统源码,适合想补全Java Web工程闭环能力的开发者

你手头那套Spring Boot+MyBatis的电商Demo,跑得再顺,也掩盖不了一个事实:它没碰过真实业务里的排班冲突校验、验证码时效控制、医生-科室-号源三级联动、预约状态机流转这些硬骨头。而这份jsp+mysql的在线医疗预约挂号系统,恰恰卡在Java Web技术栈最“土”却最真实的断层上——它不用Spring容器、不靠自动装配、所有DAO都手写SQL、事务靠Connection.setAutoCommit(false)硬控、验证码用JavaMail发、密码重置走邮箱Token验证链。这不是复古,是刻意裸奔:它把JSP的请求生命周期、Servlet的线程安全陷阱、MySQL事务隔离级别对并发挂号的影响,全摊开在眼皮底下。如果你正卡在“能写CRUD但不敢接医院类需求”“会配Spring Security但搞不定登录态跨页面传递”“看懂了MVC但画不出挂号流程的状态图”,这套源码就是你的手术刀——它不教你怎么写优雅代码,它逼你直面Java Web原始生态里每一个容易被框架屏蔽的坑。新手能照着部署跑起来,老手能从中抠出5个以上可复用的业务组件模板。


2. 从零部署:解压、建库、改配置三步落地,绕过90%新手卡点

这套系统不是开箱即用的Docker镜像,它要求你亲手触摸Tomcat的webapps目录、手动执行SQL建表、逐行核对JDBC连接参数。这种“低效”恰恰是理解Java Web底层的关键入口。下面步骤基于Windows 10 + Tomcat 9.0.83 + MySQL 5.7.44(与热搜词中高频出现的版本一致)实测通过,所有路径和参数均来自源码内实际文件。

2.1 解压与目录结构还原:别急着启动,先看清骨架

下载解压后,你会看到一个扁平目录,里面混着.java、.jsp、.html和.sql文件。这不是标准Maven项目结构,而是传统JSP Web应用的原始形态。你需要手动构建符合Servlet规范的目录树:

# 假设Tomcat安装在 C:\tomcat9 # 在 C:\tomcat9\webapps\ 下创建新目录 mkdir medical_system # 将源码中所有 .jsp 和 .html 文件复制到根目录 copy *.jsp *.html C:\tomcat9\webapps\medical_system\ # 创建 WEB-INF 目录(必须小写,大小写敏感!) mkdir C:\tomcat9\webapps\medical_system\WEB-INF # 将所有 .java 文件放入 WEB-INF\classes(需编译) mkdir C:\tomcat9\webapps\medical_system\WEB-INF\classes # 将 DBUtil.java 等工具类、DAO类、Servlet类全部复制进去 copy *.java C:\tomcat9\webapps\medical_system\WEB-INF\classes\ # 创建 lib 目录并放入 mysql-connector-java-5.1.47.jar(必须匹配MySQL 5.7) mkdir C:\tomcat9\webapps\medical_system\WEB-INF\lib # (此处需你自行下载对应版本jar包,放进去)

提示:源码中没有提供web.xml,这是故意为之——它依赖Tomcat 7+的注解式Servlet注册(如@WebServlet("/LoginServlet"))。但DBUtil.java里硬编码了数据库连接信息,这是第一个必须修改的点。

2.2 MySQL建库与初始化:执行SQL前必须做三件事

源码包里没有.sql文件?别慌。所有建表语句分散在DBUtil.java和各DAO类的createTable()方法中。这是典型的老派JSP做法:建表逻辑和业务逻辑耦合。我们把它抽出来统一执行:

-- 在MySQL命令行或Workbench中执行(注意:字符集必须为utf8mb4) CREATE DATABASE IF NOT EXISTS medical_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE medical_db; -- 科室表(关键:id为主键,name唯一) CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE, description TEXT ); -- 医生表(关键:dept_id外键,status字段控制是否可预约) CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL, dept_id INT NOT NULL, title VARCHAR(20), status TINYINT DEFAULT 1 COMMENT '1=可用,0=停诊', FOREIGN KEY (dept_id) REFERENCES department(id) ); -- 号源表(核心:date+doctor_id组合唯一,避免同一医生同天重复放号) CREATE TABLE num_source ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, date DATE NOT NULL, morning_num INT DEFAULT 0, afternoon_num INT DEFAULT 0, total_num INT DEFAULT 0, used_num INT DEFAULT 0, UNIQUE KEY uk_doctor_date (doctor_id, date), FOREIGN KEY (doctor_id) REFERENCES doctor(id) ); -- 预约表(核心:status状态机,含'待确认','已确认','已取消','已完成') CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id INT NOT NULL, doctor_id INT NOT NULL, num_source_id INT NOT NULL, appointment_date DATE NOT NULL, session_type ENUM('morning','afternoon') NOT NULL, status ENUM('pending','confirmed','cancelled','completed') DEFAULT 'pending', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (doctor_id) REFERENCES doctor(id), FOREIGN KEY (num_source_id) REFERENCES num_source(id) );

参数说明:

  • utf8mb4:必须!否则中文姓名、科室名存入会乱码(JSP页面默认UTF-8,MySQL若用utf8只支持3字节,不兼容emoji和部分生僻字);
  • UNIQUE KEY uk_doctor_date:这是挂号防重的核心约束,源码中NumSourceDao.java的insertNumSource()方法依赖此约束抛异常来拦截重复放号;
  • ENUM类型:比VARCHAR更安全,appointment.status的取值被严格限定,避免前端传入非法状态导致业务逻辑错乱。

2.3 JDBC连接配置:改三处,否则必连不上

DBUtil.java是整个系统的数据中枢,但它把数据库连接参数硬编码在静态变量里。你必须修改以下三处(路径:C:\tomcat9\webapps\medical_system\WEB-INF\classes\DBUtil.java):

// 修改前(示例) private static final String URL = "jdbc:mysql://localhost:3306/testdb"; private static final String USER = "root"; private static final String PASSWORD = "123456"; // 修改后(根据你的MySQL实际配置) private static final String URL = "jdbc:mysql://localhost:3306/medical_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8"; private static final String USER = "your_mysql_username"; // 不要用root,建议新建专用用户 private static final String PASSWORD = "your_strong_password"; // 密码强度必须达标

逻辑说明:

  • serverTimezone=GMT%2B8:解决MySQL 5.7+时区报错(常见错误:The server time zone value 'XXX' is unrecognized);
  • useUnicode=true&characterEncoding=utf8mb4:确保JSP页面提交的中文能正确存入数据库;
  • 新建MySQL用户而非用root:这是生产级安全底线,源码中SendMail.java调用JavaMail时若密码明文暴露,root权限等于交出服务器控制权。

2.4 编译Java类:用javac命令,别信IDE自动编译

Tomcat不会自动编译.java文件,必须手动编译成.class。打开CMD,定位到classes目录:

cd C:\tomcat9\webapps\medical_system\WEB-INF\classes # 编译所有Java文件(注意:顺序很重要!DBUtil必须先编译) javac -encoding UTF-8 DBUtil.java javac -encoding UTF-8 NumSourceDao.java javac -encoding UTF-8 WorkDayDao.java javac -encoding UTF-8 Order.java # ... 依次编译所有.java文件(按依赖顺序:工具类→DAO→Servlet→业务类)

为什么强调顺序?
Order.java(处理预约逻辑)依赖NumSourceDao.java,而NumSourceDao.java又依赖DBUtil.java。如果DBUtil.class不存在,编译会直接失败。源码中没有import语句的显式声明,全靠文件名隐式关联,这是JSP时代典型的紧耦合设计。


3. 角色功能拆解:管理员、医生、患者三端如何协同完成一次挂号?

这套系统的价值不在UI美观,而在业务状态流转的完整性。它用最朴素的JSP+Servlet实现了医疗场景下三个角色的职责边界与数据同步。我们以一次真实挂号为例,拆解背后的数据流与控制逻辑。

3.1 患者端:从注册到预约成功的四步状态机

患者操作路径:register.jsp→login.jsp→department_list.jsp→doctor_detail.jsp→make_appointment.jsp。每一步都对应明确的后端校验:

  1. 注册时邮箱唯一性校验:FindPwd.java中的checkEmailExists()方法执行SELECT COUNT(*) FROM patient WHERE email=?,防止同一邮箱注册多个账号;
  2. 登录后生成Session ID:LoginServlet.java将用户ID存入HttpSession,后续所有JSP页面通过session.getAttribute("userId")获取,未做JWT或Token加密,纯Session依赖;
  3. 选医生时号源实时查询:QueryNumSource.java的getAvailableNumsByDoctor()方法根据doctor_id和当前日期查num_source表,返回morning_num - used_num > 0的可预约时段;
  4. 提交预约时的双重锁:Order.java的createAppointment()方法包含两个关键动作:
    • 先UPDATE num_source SET used_num = used_num + 1 WHERE id = ? AND used_num < total_num(CAS更新,避免超挂);
    • 再INSERT INTO appointment (...) VALUES (...)(仅当UPDATE影响行数=1时才执行INSERT)。

关键参数:num_source.used_num是核心计数器,它不依赖数据库事务的原子性,而是靠UPDATE ... WHERE used_num < total_num的条件更新实现乐观锁。这是应对高并发挂号的低成本方案,比SELECT FOR UPDATE更轻量,但要求前端必须做二次校验(JSP中make_appointment.jsp会再次查used_num显示剩余号源)。

3.2 医生端:排班查看与队列管理的轻量级实现

医生登录后访问doctor_dashboard.jsp,其数据来源是WorkDayDao.java的getTodayAppointments()方法:

// 查询今日该医生所有状态为'pending'或'confirmed'的预约 String sql = "SELECT a.id, p.name, p.phone, a.session_type, a.appointment_date " + "FROM appointment a " + "JOIN patient p ON a.patient_id = p.id " + "WHERE a.doctor_id = ? AND a.appointment_date = CURDATE() " + "AND a.status IN ('pending','confirmed') " + "ORDER BY a.create_time";

玄学细节:CURDATE()函数返回MySQL服务器本地日期,未考虑时区转换。若Tomcat服务器和MySQL服务器时区不一致(如MySQL设为+08:00,Tomcat JVM设为GMT),会导致医生看不到当天预约。解决方案:在DBUtil.java的JDBC URL中强制指定serverTimezone=GMT%2B8,并确保Tomcat启动脚本中添加-Duser.timezone=GMT+08。

3.3 管理员端:折线图背后的SQL聚合逻辑

管理员首页的“预约折线图”并非ECharts渲染,而是JSP脚本直接拼接HTML<canvas>标签。数据来自AdminServlet.java调用的getAppointmentTrend()方法:

// 查询近7天每日预约数量(按status='confirmed'统计) String sql = "SELECT DATE(create_time) as day, COUNT(*) as count " + "FROM appointment " + "WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) " + "AND status = 'confirmed' " + "GROUP BY DATE(create_time) " + "ORDER BY day";

避坑点:DATE(create_time)会丢失时间精度,但满足日报表需求;DATE_SUB(CURDATE(), INTERVAL 7 DAY)是MySQL原生函数,比Java端计算日期更可靠(避免JVM时区干扰)。


4. 避坑指南:五个血泪经验,专治部署后“页面空白”“登录失败”“号源不减”

这套系统最大的价值,往往藏在它让你摔得最疼的几个坑里。以下是我在三台不同配置机器(Win10/Ubuntu/CentOS)上反复验证的典型问题,每一条都对应源码中的具体位置和修复方式。

4.1 现象:首页打开空白,浏览器F12看到404,但index.jsp明明存在

原因:Tomcat未识别medical_system为Web应用,根本原因是WEB-INF/web.xml缺失,而Tomcat 9默认要求web.xml存在(即使为空)。源码采用注解式Servlet,但缺少最低限度的部署描述符。
解决:在C:\tomcat9\webapps\medical_system\WEB-INF\下创建空的web.xml文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> </web-app>

4.2 现象:登录成功后跳转到main.jsp,但页面显示java.lang.NullPointerException

原因:main.jsp中<%= session.getAttribute("role") %>返回null,因为LoginServlet.java里设置session属性的key是"userRole",而JSP里读的是"role"。命名不一致导致空指针。
解决:统一key名。修改LoginServlet.java第45行:

// 修改前 session.setAttribute("userRole", user.getRole()); // 修改后 session.setAttribute("role", user.getRole());

4.3 现象:患者能预约,但num_source.used_num不增加,号源永远显示“剩余10”

原因:Order.java的createAppointment()方法中,UPDATE语句的WHERE条件写成了used_num < total_num,但初始used_num=0,total_num=10,条件成立;然而后续INSERT语句因patient_id字段在appointment表中为NOT NULL,而make_appointment.jsp提交时未传patient_id(只传了doctor_id和date),导致INSERT失败,事务回滚,UPDATE也被撤销。
解决:检查make_appointment.jsp表单,确保包含隐藏域:

<input type="hidden" name="patient_id" value="<%= session.getAttribute("userId") %>">

并在Order.java中接收该参数,加入INSERT语句。

4.4 现象:发送验证码邮件时抛javax.mail.AuthenticationFailedException

原因:SendMail.java中SMTP认证用户名写死了"your_email@gmail.com",密码是明文"your_app_password",但Gmail等服务商要求使用“应用专用密码”,且需开启“两步验证”。
解决:

  1. 登录Gmail → 账户设置 → 安全 → 开启两步验证 → 生成应用专用密码(16位);
  2. 修改SendMail.java:
props.put("mail.smtp.user", "your_real_gmail@gmail.com"); props.put("mail.smtp.password", "your_16_digit_app_password"); // 替换为真实密码

4.5 现象:MySQL插入中文显示为??,日志报Incorrect string value

原因:MySQL服务端、数据库、表、字段四级字符集未统一为utf8mb4,或JDBC URL未指定characterEncoding=utf8mb4。
解决:

  1. 检查MySQL配置文件my.ini(Windows)或my.cnf(Linux),确保有:
[client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci
  1. 重启MySQL服务;
  2. 执行SQL重建表:
ALTER DATABASE medical_db CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; ALTER TABLE patient CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 对所有表重复执行

5. 进阶技巧:把“挂号号源”模块抽成独立SDK,三步接入你的新项目

这套系统最值得复用的不是UI,而是NumSourceDao.java里封装的号源库存管理逻辑。它用纯SQL实现了“查-锁-减-校验”的完整链路,比Spring Boot里动辄引入Redis分布式锁更轻量,特别适合中小医疗SaaS的快速迭代。我把它提炼成可移植的Java SDK,只需三步就能接入任何Java Web项目。

5.1 提取核心类:剥离业务耦合,保留原子操作

从NumSourceDao.java中提取出两个静态方法,封装为NumSourceManager类(保存为NumSourceManager.java):

public class NumSourceManager { // 1. 查询某医生某日剩余号源(返回morning/afternoon剩余数) public static Map<String, Integer> getAvailableNums(int doctorId, String dateStr) throws SQLException { String sql = "SELECT morning_num - used_num as morning_left, " + " afternoon_num - used_num as afternoon_left " + "FROM num_source WHERE doctor_id = ? AND date = ?"; // (此处省略JDBC执行逻辑,复用DBUtil.getConnection()) return resultMap; // 返回{"morning_left":5,"afternoon_left":3} } // 2. 尝试锁定并减少号源(返回true=成功,false=库存不足) public static boolean tryLockAndReduce(int numSourceId, String sessionType) throws SQLException { String sql = "UPDATE num_source SET used_num = used_num + 1 " + "WHERE id = ? AND ((? = 'morning' AND morning_num > used_num) OR " + " (? = 'afternoon' AND afternoon_num > used_num))"; // 参数:numSourceId, sessionType, sessionType int rows = stmt.executeUpdate(sql, numSourceId, sessionType, sessionType); return rows == 1; } }

参数说明:

  • sessionType:必须为"morning"或"afternoon",用于区分时段库存;
  • tryLockAndReduce()的SQL用了OR条件,确保只更新对应时段的库存,避免morning_num被afternoon请求误减。

5.2 构建独立JAR包:用jar命令打包,不依赖Tomcat

在NumSourceManager.java所在目录执行:

# 编译 javac -cp ".;path/to/mysql-connector-java-5.1.47.jar" NumSourceManager.java # 打包(不包含MySQL驱动,由使用者自行引入) jar cvf numsource-sdk-1.0.jar NumSourceManager.class

5.3 在新项目中调用:Spring Boot示例(无需改一行XML)

假设你的新项目是Spring Boot 2.7,pom.xml已引入mysql-connector-java:

@Service public class AppointmentService { @Autowired private JdbcTemplate jdbcTemplate; // 或自定义DBUtil public boolean makeAppointment(int doctorId, String date, String sessionType) { try { // 步骤1:查剩余号源 Map<String, Integer> left = NumSourceManager.getAvailableNums(doctorId, date); int leftNum = "morning".equals(sessionType) ? left.get("morning_left") : left.get("afternoon_left"); if (leftNum <= 0) { throw new BusinessException("号源已满"); } // 步骤2:尝试锁定(这里需先查出num_source.id) Integer numSourceId = jdbcTemplate.queryForObject( "SELECT id FROM num_source WHERE doctor_id = ? AND date = ?", Integer.class, doctorId, date); // 步骤3:执行锁定减库存 boolean locked = NumSourceManager.tryLockAndReduce(numSourceId, sessionType); if (!locked) { throw new BusinessException("库存竞争,号源已被抢完"); } // 步骤4:创建预约记录(业务逻辑) jdbcTemplate.update("INSERT INTO appointment (...) VALUES (...)", ...); return true; } catch (SQLException e) { throw new RuntimeException("数据库操作异常", e); } } }

关键技巧:tryLockAndReduce()返回boolean而非抛异常,这是为了适配高并发场景下的“快速失败”。在Spring Boot中,你可以用@Retryable注解对makeAppointment()方法做重试(最多3次),每次重试前sleep 100ms,模拟乐观锁重试机制。

从那以后我每次接手新医疗项目,都会先把NumSourceManager放进common-utils模块,再写单元测试覆盖getAvailableNums()和tryLockAndReduce()的边界情况(如used_num=0、used_num=total_num、date格式错误)。它让我少写了80%的库存校验代码,也让我真正理解了“号源”这个业务概念在数据库层面的物理表达。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询