简介:这是一套面向Java Web初学者与课程设计者的在线医疗预约挂号系统实战源码,基于JSP+Servlet+MySQL技术栈构建,完整覆盖医院挂号业务核心流程。资源共206个文件,包含72个JSP页面(实现前后端交互与界面展示)、72张PNG图标与界面素材、55个Java类(含DBUtil数据库工具、Order订单处理、SendMail邮件验证、FindPwd密码找回等关键逻辑),以及HTML首页、properties配置文件和README说明文档,压缩包仅1010KB,轻量易部署。已有53人学习下载,适合用于Java Web课程实训、毕业设计参考或小型医疗信息化原型开发。读者可直接运行系统,深入理解MVC分层结构、用户角色权限控制(管理员/医生/患者三端功能分离)、验证码与邮件服务集成、折线图数据可视化及排班预约业务逻辑实现。
1. 为什么还在用 JSP + MySQL 做在线医疗预约挂号系统?——不是怀旧,是落地成本、团队栈和医院信息科的真实约束
你搜“JSP在线医疗预约挂号管理系统源码”,点开一堆压缩包,第一反应可能是:这都2024年了,怎么还有人用 JSP?Spring Boot 不香吗?Vue + Spring Cloud 不更现代?但现实是:全国二级及以下公立医院、社区卫生服务中心、民营专科诊所里,仍有大量在用或正在维护的 Web 系统,底层就是 JSP + Servlet + MySQL。它们不跑在云原生平台,不对接微服务网关,甚至没上 HTTPS;但它们每天处理着真实的挂号、分诊、医生排班、号源锁定、退号回滚——而且不能宕机、不能丢数据、不能超时。这类系统不是技术秀场,而是业务黑匣子:前端页面要兼容 IE8(别笑,很多基层医院终端仍强制使用),后端要能塞进 2G 内存的老服务器,数据库得在 Windows Server 2008 R2 上跑稳三年不重启。本篇讲的,就是如何把一个典型的JSP在线医疗预约挂号管理系统源码.zip拿到手后,从解压、部署、调通,到真正能支撑门诊日均 300+ 预约的最小可行闭环。它不教你怎么写优雅的 MVC,而是告诉你:当 Tomcat 报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver、当挂号页面提交后跳转空白、当 MySQL 的datetime字段存进去变成0000-00-00 00:00:00——你该看哪几行日志、改哪三个配置、删哪两行注释。适合 Java 初级开发、医院信息科驻场工程师、外包项目交付人员——尤其是刚接手老系统维护、被要求“三天内让挂号功能跑起来”的人。
2. 解压即启动:JSP 医疗挂号系统本地运行的最小闭环
2.1 源码结构拆解:认出WebContent、src、WEB-INF这三个关键目录
拿到JSP在线医疗预约挂号管理系统源码.zip后,先别急着导入 IDE。用任意解压工具打开,观察顶层目录结构。典型结构如下:
JSP_Online_Medical_System/ ├── WebContent/ ← JSP 页面、CSS、JS、图片存放处(Tomcat 直接映射为 Web 根目录) │ ├── index.jsp │ ├── login.jsp │ ├── register.jsp │ ├── doctor_list.jsp │ └── WEB-INF/ ← web.xml、lib、classes 全在这里(安全敏感资源,不对外暴露) │ ├── web.xml ← 核心配置:servlet 映射、filter、context-param │ ├── lib/ ← 所有 jar 包:mysql-connector-java-5.1.47.jar、jstl-1.2.jar、servlet-api.jar 等 │ └── classes/ ← 编译后的 .class 文件(若 src 已编译)或空目录(需手动编译) ├── src/ ← Java 源码:Servlet、DAO、Bean、Util 类(包结构如 com.medical.dao.UserDao.java) ├── database/ ← SQL 脚本:通常含 create_table.sql、insert_init_data.sql └── README.txt ← 很可能没有,或只有一行“请用 Eclipse 导入”提示:
WebContent是 Eclipse/MyEclipse 时代的标准 Web Root,对应 IntelliJ 的webapp目录。如果看到src/main/webapp,说明作者后期强行迁过 Maven 结构,但实际未清理干净——这种混合结构最容易翻车。
2.2 环境三件套:JDK 8 + Tomcat 8.5 + MySQL 5.7 —— 版本锁死是刚需
这个系统几乎必然绑定特定版本组合。试过 JDK 11?Tomcat 10?MySQL 8.0?90% 概率直接报错。原因很实在:
com.mysql.jdbc.Driver在 MySQL 8.0+ 已废弃,新驱动类名是com.mysql.cj.jdbc.Driver,而老 JSP 代码里硬编码了旧类;- Tomcat 10 默认使用 Jakarta EE 9 命名空间(
jakarta.servlet.*),但老代码 import 的全是javax.servlet.*; - JDK 9+ 移除了
javax.xml.bind,而某些老 DAO 层 XML 解析逻辑会崩。
我一般会这样配环境(Windows / Linux 均适用):
- JDK:下载
jdk-8u202-windows-x64.exe(Windows)或jdk-8u202-linux-x64.tar.gz(Linux),安装后设置JAVA_HOME并验证java -version输出1.8.0_202; - Tomcat:选
apache-tomcat-8.5.90.zip(8.5.x 是最后一个支持javax.servlet且稳定支持 JSP 2.3 的大版本),解压后修改conf/server.xml中port="8080"为port="8081"(避免与现有服务冲突); - MySQL:用
mysql-5.7.44-winx64.zip(Windows)或mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz(Linux),关键操作:初始化时加参数--defaults-file=my.ini(Windows)或--defaults-file=/etc/my.cnf(Linux),并在配置文件中强制指定default-storage-engine=INNODB和sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION—— 否则插入NULL到NOT NULL datetime字段会静默失败。
2.3 数据库初始化:执行 SQL 脚本前必须做的三件事
database/目录下通常有medical_system.sql或create_table.sql。别直接双击运行!按顺序做:
新建数据库并指定字符集:
CREATE DATABASE medical_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;注意:
utf8mb4是必须的。老系统常写utf8,但 MySQL 的utf8实际只支持 3 字节 UTF-8(不支持 emoji 和部分生僻汉字),挂号姓名含“䶮”“堃”等字会乱码或截断。检查 SQL 脚本里的 ENGINE 和 CHARSET:
打开create_table.sql,搜索ENGINE=MyISAM—— 全部替换成ENGINE=InnoDB;搜索DEFAULT CHARSET=utf8—— 全部替换成DEFAULT CHARSET=utf8mb4。MyISAM 不支持事务,而挂号涉及“扣号 + 写日志 + 更新医生余号”原子操作,不用 InnoDB 必丢数据。执行脚本并验证表结构:
mysql -u root -p medical_db < database/create_table.sql登录后执行:
USE medical_db; SHOW TABLES; -- 应看到 user, doctor, schedule, appointment, department 等 DESCRIBE appointment; -- 检查字段:patient_id INT, doctor_id INT, appoint_time DATETIME, status TINYINT(1) DEFAULT 1
2.4 Tomcat 部署:把 WebContent 当成 WebRoot,绕过 IDE 导入陷阱
很多新手卡在“Eclipse 导入失败”或“IntelliJ 提示 no artifact configured”。其实最稳路径是纯手工部署:
将
WebContent/目录整体复制,粘贴到 Tomcat 安装目录下的webapps/文件夹中,重命名为medical(即webapps/medical/);将
WebContent/WEB-INF/lib/下所有.jar文件,确认已包含:mysql-connector-java-5.1.47.jar(不是 8.x)jstl-1.2.jar(JSP 标签库,缺了<c:if>会报错)standard-1.1.2.jar(配合 jstl)servlet-api.jar(Tomcat 自带,但老项目常自带一份,务必删掉,否则类冲突)
修改
WebContent/WEB-INF/web.xml中数据库连接配置(通常在<context-param>或<servlet>初始化参数里):<context-param> <param-name>dbUrl</param-name> <param-value>jdbc:mysql://localhost:3306/medical_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8</param-value> </context-param> <context-param> <param-name>dbUser</param-name> <param-value>root</param-value> </context-param> <context-param> <param-name>dbPassword</param-name> <param-value>your_real_password</param-value> </context-param>注意:
&在 XML 中要写成&;serverTimezone=GMT%2B8是解决The server time zone value '...' is unrecognized的后悔药,比改 MySQL 时区更稳妥。启动 Tomcat:
# Windows cd apache-tomcat-8.5.90\bin startup.bat # Linux cd apache-tomcat-8.5.90/bin ./startup.sh访问
http://localhost:8081/medical/—— 如果看到首页(通常是index.jsp),说明部署成功。
3. 登录就 404?挂号点不动?—— JSP 医疗系统三大高频翻车现场与血泪修复法
3.1 现象:访问http://localhost:8081/medical/login.jsp显示 404,但文件明明存在
原因:login.jsp里用了<jsp:include page="/common/header.jsp"/>,而/common/header.jsp路径错误或文件缺失;更常见的是web.xml中 servlet 映射未覆盖.jsp后缀(Tomcat 8.5 默认支持,但某些定制版 Tomcat 会关掉)。
解决:
- 查
login.jsp第一行是否含<%@ page contentType="text/html;charset=UTF-8" %>,缺了会导致 JSP 引擎不解析; - 检查
web.xml是否有如下默认 servlet 声明(Tomcat 8.5 通常自带,但老项目可能删了):<servlet-mapping> <servlet-name>jsp</servlet-name> <url-pattern>*.jsp</url-pattern> </servlet-mapping> - 若用 include,确保被包含文件路径正确:
<jsp:include page="common/header.jsp"/>(相对路径,不加/)比<jsp:include page="/common/header.jsp"/>(绝对路径,从 WebRoot 开始)更安全。
3.2 现象:输入账号密码点击登录,页面刷新但 URL 变成http://localhost:8081/medical/login.jsp,无跳转、无报错、后台无日志
原因:login.jsp表单提交目标是action="LoginServlet",但web.xml中<servlet>名称或<servlet-mapping>的<url-pattern>与实际不符;或LoginServlet.java编译后 class 文件未生成到WEB-INF/classes/对应包路径下。
解决:
- 查
login.jsp表单:<form action="LoginServlet" method="post"> <!-- 注意:这里 action 是相对路径 --> - 查
web.xml中 servlet 映射:<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.medical.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/LoginServlet</url-pattern> <!-- 必须与 form action 一致 --> </servlet-mapping> - 确认
src/com/medical/servlet/LoginServlet.java已编译,且WEB-INF/classes/com/medical/servlet/LoginServlet.class文件存在。若不存在,用命令行编译:cd src javac -encoding UTF-8 -cp "../WebContent/WEB-INF/lib/*;C:/apache-tomcat-8.5.90/lib/servlet-api.jar" com/medical/servlet/LoginServlet.java mkdir -p ../WebContent/WEB-INF/classes/com/medical/servlet/ move com\medical\servlet\LoginServlet.class ../WebContent/WEB-INF/classes/com/medical/servlet/
3.3 现象:挂号成功后,appointment表里appoint_time字段存为0000-00-00 00:00:00,且后续查询全为空
原因:JSP 页面传参是字符串(如request.getParameter("appoint_time")返回"2024-05-20 08:30"),而 DAO 层用PreparedStatement.setDate()传java.util.Date,但未做SimpleDateFormat解析;或 MySQL 表字段定义为DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',而严格模式下拒绝该默认值。
解决:
- 在
AppointmentServlet.java中,解析时间字符串:String timeStr = request.getParameter("appoint_time"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); java.util.Date utilDate = sdf.parse(timeStr); // 注意:捕获 ParseException java.sql.Timestamp sqlTime = new java.sql.Timestamp(utilDate.getTime()); // 传给 PreparedStatement ps.setTimestamp(4, sqlTime); // 第4个 ? 占位符 - 检查 MySQL 表结构:
ALTER TABLE appointment MODIFY appoint_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; - 若用
java.sql.Date(只含日期),务必改用java.sql.Timestamp(含时分秒),否则秒级精度丢失导致同一时段号源冲突。
4. 让挂号不丢号:MySQL 事务 + JSP 同步锁的硬核落地细节
4.1 挂号核心逻辑必须包裹在事务里——否则并发时号源超卖
老系统常把“查余号 → 扣号 → 写预约记录”拆成三个独立 SQL,在高并发下必然超卖。比如两个用户同时抢最后一个号,查余号都返回 1,都通过,结果写入两条记录。
正确做法(在 DAO 层):
public boolean createAppointment(Appointment appt) { Connection conn = null; PreparedStatement ps = null; try { conn = DBUtil.getConnection(); // 从自定义连接池获取 conn.setAutoCommit(false); // 关键:开启事务 // 1. 查询并锁定该时段医生号源(for update 防止其他事务读到旧值) String checkSql = "SELECT remain_num FROM schedule WHERE id = ? FOR UPDATE"; ps = conn.prepareStatement(checkSql); ps.setInt(1, appt.getScheduleId()); ResultSet rs = ps.executeQuery(); if (rs.next() && rs.getInt("remain_num") > 0) { // 2. 扣减余号 String updateSql = "UPDATE schedule SET remain_num = remain_num - 1 WHERE id = ?"; ps = conn.prepareStatement(updateSql); ps.setInt(1, appt.getScheduleId()); ps.executeUpdate(); // 3. 插入预约记录 String insertSql = "INSERT INTO appointment(...) VALUES(...)"; ps = conn.prepareStatement(insertSql); // 设置参数... ps.executeUpdate(); conn.commit(); // 提交事务 return true; } else { conn.rollback(); // 余号不足,回滚 return false; } } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) {} e.printStackTrace(); return false; } finally { DBUtil.close(ps, conn); } }注意:
FOR UPDATE只在 InnoDB 引擎 + 事务中生效;若schedule.id无索引,会锁整张表,性能雪崩。务必确认schedule.id是主键或有索引。
4.2 JSP 页面防重复提交:按钮置灰 + 时间戳 Token 双保险
用户手抖连点两次“确认挂号”,后端若没防重,会生成两条预约。光靠前端 JS 置灰不够(F5 刷新可绕过)。
方案:
- 页面生成唯一 Token:
<% String token = java.util.UUID.randomUUID().toString().replace("-", ""); session.setAttribute("submit_token", token); %> <input type="hidden" name="token" value="<%=token%>" /> - Servlet 校验 Token:
String clientToken = request.getParameter("token"); String sessionToken = (String) session.getAttribute("submit_token"); if (clientToken == null || !clientToken.equals(sessionToken)) { request.setAttribute("error", "重复提交,请勿刷新页面"); request.getRequestDispatcher("appoint_fail.jsp").forward(request, response); return; } session.removeAttribute("submit_token"); // 用完即焚 - 按钮 JS 置灰:
<button type="submit" onclick="this.disabled=true;this.value='提交中...';">确认挂号</button>
4.3 MySQL 连接池配置:别用 DriverManager,用 DBCP 或 HikariCP(但老系统只能用 DBCP)
DBUtil.java里若还用DriverManager.getConnection(),每次请求都新建连接,100 并发直接拖垮 MySQL。必须用连接池。
WEB-INF/classes/db.properties示例(DBCP):
driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/medical_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8 username=root password=your_pass initialSize=5 maxActive=20 maxWait=60000 minIdle=2 timeBetweenEvictionRunsMillis=60000 minEvictableIdleTimeMillis=300000 validationQuery=SELECT 1 testWhileIdle=true testOnBorrow=false testOnReturn=falseDBUtil.java加载逻辑:
static { try { Properties props = new Properties(); props.load(DBUtil.class.getClassLoader().getResourceAsStream("db.properties")); dataSource = new BasicDataSource(); dataSource.setDriverClassName(props.getProperty("driverClassName")); dataSource.setUrl(props.getProperty("url")); dataSource.setUsername(props.getProperty("username")); dataSource.setPassword(props.getProperty("password")); dataSource.setInitialSize(Integer.parseInt(props.getProperty("initialSize"))); dataSource.setMaxActive(Integer.parseInt(props.getProperty("maxActive"))); // ... 其他配置 } catch (Exception e) { e.printStackTrace(); } }5. 从能跑,到能扛:挂号系统上线前必做的五项压力验证与调优
5.1 模拟真实挂号流:用 Apache Bench 测 50 并发下的号源一致性
别信“页面能打开就 OK”。用ab工具模拟真实用户行为:
# 模拟 50 个用户,各发 10 次挂号请求(POST 到 AppointmentServlet) ab -n 500 -c 50 -p post_data.txt -T "application/x-www-form-urlencoded" "http://localhost:8081/medical/AppointmentServlet"post_data.txt内容(模拟不同患者、同一时段):
patient_id=1001&schedule_id=201&appoint_time=2024-05-20+08%3A30 patient_id=1002&schedule_id=201&appoint_time=2024-05-20+08%3A30 ...验证点:
schedule.remain_num最终值 = 初始值 - 成功预约数(应严格相等);appointment表记录数 = 成功预约数;- 查
appointment表中schedule_id=201的记录,appoint_time全为2024-05-20 08:30:00(无时间漂移); - Tomcat
logs/catalina.out无Deadlock found或Lock wait timeout报错。
5.2 MySQL 慢查询狙击:给挂号高频字段加联合索引
挂号页常按“科室 + 医生 + 日期”筛选号源,若schedule表只有doctor_id单列索引,查询会全表扫描。
建索引命令:
-- 覆盖查询:WHERE department_id = ? AND doctor_id = ? AND date = ? ALTER TABLE schedule ADD INDEX idx_dept_doc_date (department_id, doctor_id, date); -- 覆盖更新:UPDATE schedule SET remain_num = ? WHERE id = ? ALTER TABLE schedule ADD INDEX idx_id (id);用EXPLAIN验证:
EXPLAIN SELECT * FROM schedule WHERE department_id=5 AND doctor_id=12 AND date='2024-05-20'; -- type 应为 'ref',key 应为 'idx_dept_doc_date',rows 应 << 表总行数5.3 JSP 编译优化:关闭热加载,预编译 JSP 提升首屏速度
Tomcat 默认在首次访问 JSP 时动态编译(.java→.class),首屏慢且 CPU 飙高。生产环境必须预编译。
方法一(推荐):用 Tomcat 自带jspc工具
cd apache-tomcat-8.5.90/bin ./jspc.sh -uriroot /path/to/your/webapps/medical -d /path/to/your/webapps/medical/WEB-INF/classes生成的.class文件会放在WEB-INF/classes/org/apache/jsp/下,Tomcat 启动时直接加载。
方法二:修改conf/web.xml,禁用 JSP 热加载
<servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> <init-param> <param-name>development</param-name> <param-value>false</param-value> <!-- 关键:设为 false --> </init-param> <init-param> <param-name>fork</param-name> <param-value>false</param-value> </init-param> </servlet>5.4 日志分级:把挂号关键操作记到独立文件,方便审计追溯
System.out.println()在生产环境等于自杀。必须用 Log4j 或 JUL(Java Util Logging)。
WEB-INF/classes/logging.properties示例(JUL):
# 全局日志级别 .level = INFO # 挂号专用 Handler com.medical.servlet.AppointmentServlet.level = ALL com.medical.servlet.AppointmentServlet.handlers = java.util.logging.FileHandler java.util.logging.FileHandler.pattern = logs/appoint_%u.log java.util.logging.FileHandler.limit = 10000000 java.util.logging.FileHandler.count = 5 java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatterServlet 中记录:
private static final Logger logger = Logger.getLogger(AppointmentServlet.class.getName()); // ... logger.info("挂号成功 | patient_id=" + patientId + " | schedule_id=" + scheduleId + " | time=" + appointTime);5.5 安全加固:过滤 XSS 与 SQL 注入,哪怕只是基层医院系统
别觉得“内网系统不用防”。挂号姓名、手机号、病情描述栏都是注入入口。
通用过滤器(web.xml注册):
<filter> <filter-name>XssFilter</filter-name> <filter-class>com.medical.filter.XssFilter</filter-class> </filter> <filter-mapping> <filter-name>XssFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>XssFilter.java核心逻辑(简单有效):
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request = (HttpServletRequest) req; XssHttpServletRequestWrapper wrapper = new XssHttpServletRequestWrapper(request); chain.doFilter(wrapper, resp); } // XssHttpServletRequestWrapper 重写 getParameter 等方法,对值做 HTML 转义: public String cleanXss(String value) { if (value == null || value.isEmpty()) return value; return value.replaceAll("<", "<").replaceAll(">", ">").replaceAll("\\(", "(").replaceAll("\\)", ")"); }我在三家社区医院部署时,都坚持加上这一层。不是怕黑客,是怕医生手误粘贴带
<script>的病历模板进来,导致整个挂号页弹窗崩溃。这种玄学问题,查三天日志都找不到源头。
希望帮到你。
本文还有配套的精品资源,点击获取