资讯中心

JavaWeb医院系统实战:Servlet+JSP+MySQL从部署到高并发压测

📅 2026/9/28 16:12:34
JavaWeb医院系统实战:Servlet+JSP+MySQL从部署到高并发压测
简介这是一套完整的基于JavaWeb技术开发的医院门诊病人管理系统源码面向计算机专业本科生毕设、课程设计及JavaWeb初学者聚焦医疗场景下的挂号、就诊、收费、取药等核心业务流程信息化实践。资源包共831个文件涵盖440个HTML前端页面、58个JS交互脚本、35个JSP动态页面、67个JAR依赖库、80个PNG图标与UI素材以及22个编译后的Class字节码和12个Java源文件完整呈现MVC分层结构压缩包大小为32.03MB结构清晰模块耦合度低便于理解前后端协作逻辑。已有67人学习下载适合通过真实业务系统掌握ServletJDBCMySQL开发全流程。读者可直接部署运行深入分析管理员、医生、收费员、药房四大角色模块的职责划分与接口调用关系并参考loginService、guahao_servlet、yisheng_servlet等关键类实现权限控制与业务流转逻辑。1. 为什么一个“基于JavaWeb的医院门诊病人管理系统”源码至今还在被上百所高校用作课程设计压轴题这不是一个被时代淘汰的古董项目——恰恰相反它在2024年高校Java课程设计、毕业实训、信创适配教学中仍高频出现。原因很实在它用最朴素的ServletJSPMySQL技术栈完整覆盖了真实医疗场景中「挂号→分诊→候诊→就诊→缴费→病历归档」六步闭环且所有模块都可拆解为独立实验单元比如只跑挂号模块练表单校验只跑医生排班模块练时间调度逻辑。更关键的是它不依赖Spring Boot自动装配、不绑定特定IDE插件、不强求Docker环境一台装好JDK8Tomcat9MySQL5.7的笔记本就能从零部署运行。我带过12届学生实训发现凡是能把这个系统本地跑通、改出两个业务变体比如加个医保结算子模块、把排队叫号改成微信推送基本就跨过了JavaWeb工程能力的临界点。如果你正卡在“学完Servlet却写不出完整业务系统”的阶段这个源码不是怀旧文物而是你代码肌肉记忆的负重片。2. 从零启动用最简环境跑通核心挂号与就诊流程这个系统不是靠“一键部署脚本”糊弄人的玩具。它要求你亲手配置每个环节而这种“笨功夫”恰恰是排查线上问题的底层能力来源。下面以主流开发组合JDK8u202 Tomcat9.0.83 MySQL5.7.42为例走通最核心的挂号→医生接诊→生成病历三步链路。2.1 数据库初始化避开字符集和时区两大隐形杀手很多初学者卡在“登录页面空白”或“中文存成问号”根源都在数据库初始化阶段。必须用以下命令创建库不能直接用Navicat图形界面点“新建数据库”CREATE DATABASE hospital_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建用户并授权避免root直连 CREATE USER hosp_userlocalhost IDENTIFIED BY Hosp2024; GRANT ALL PRIVILEGES ON hospital_db.* TO hosp_userlocalhost; FLUSH PRIVILEGES;注意utf8mb4是硬性要求MySQL5.7默认utf8实际只支持3字节UTF-8无法存储emoji和部分生僻汉字如“龘”“biáng”而医院系统病历中常含患者手写姓名扫描件转录文字COLLATE utf8mb4_unicode_ci比utf8mb4_general_ci更准确处理中文排序和模糊查询。导入SQL脚本前务必检查.sql文件头部是否有SET NAMES utf8mb4;若无手动在第一行插入。执行导入时用命令行而非图形工具mysql -uhosp_user -pHosp2024 hospital_db hospital_schema.sql2.2 Tomcat配置让JSP编译错误暴露在控制台而不是静默白屏默认Tomcat对JSP语法错误只返回HTTP 500不打印具体行号。需修改conf/web.xml中JSP Servlet配置servlet servlet-namejsp/servlet-name servlet-classorg.apache.jasper.servlet.JspServlet/servlet-class init-param param-namefork/param-name param-valuefalse/param-value /init-param init-param param-namexpoweredBy/param-name param-valuefalse/param-value /init-param !-- 关键开启详细错误输出 -- init-param param-namedisplaySourceFragment/param-name param-valuetrue/param-value /init-param /servlet重启Tomcat后当JSP里写错EL表达式如${patient.namee}多打一个e控制台会直接报org.apache.jasper.compiler.DefaultErrorHandler.jspError Line 42: Unable to find property namee in class com.hospital.entity.Patient这比在浏览器看500错误再翻日志快10倍。2.3 验证核心流程用curl绕过前端直击Servlet逻辑链先不碰HTML页面用curl验证后端是否真正联通# 1. 模拟挂号请求POST /register curl -X POST http://localhost:8080/hospital/RegisterServlet \ -H Content-Type: application/x-www-form-urlencoded \ -d patientName张三 \ -d idCard110101199003072315 \ -d phone13800138000 \ -d departmentId1 # 2. 查询刚挂号的患者GET /query?card... curl http://localhost:8080/hospital/QueryPatientServlet?card110101199003072315 # 3. 医生接诊并录入初步诊断POST /diagnose curl -X POST http://localhost:8080/hospital/DiagnoseServlet \ -H Content-Type: application/x-www-form-urlencoded \ -d regId1001 \ -d doctorId201 \ -d symptom头晕伴恶心2天每步成功返回JSON如{code:200,msg:挂号成功,data:{regId:1001}}即证明MVC三层Controller→Service→DAO数据流畅通。此时再打开http://localhost:8080/hospital/index.jsp页面才值得信任。3. 业务逻辑深挖三个必须重写的模块及其改造逻辑源码提供的是最小可行骨架但真实医院场景会立刻暴露其简化缺陷。以下三个模块在实际教学或二次开发中100%需要重构不是修bug而是补设计。3.1 排队叫号模块从“静态序号”到“动态优先级调度”原始代码用SELECT MAX(queue_no) FROM queue_table获取下一个号导致高峰期并发挂号时重复取号。更致命的是它把所有科室共用一个队列而现实中儿科、急诊、普通门诊的叫号规则完全不同儿科按预约时间现场号混合排序迟到15分钟自动过号急诊生命体征危重患者插队需对接监护仪API普通门诊严格按挂号时间顺序但支持“过号重排”患者扫码确认过号后进入二次排队池改造方案新建queue_rule表字段含dept_id,rule_type(1时间序,2危重插队,3预约优先),timeout_minutes在QueueService.java中实现策略模式public interface QueueStrategy { int generateNextNumber(QueueContext context); } // 具体实现类TimeOrderStrategy、CriticalPriorityStrategy、AppointmentFirstStrategy叫号线程不再轮询全表改为监听queue_status字段变更用MySQL的SELECT ... FOR UPDATE保证原子性3.2 病历模板引擎摆脱硬编码HTML支持医生自定义字段原始病历页是固定JSP新增“过敏史”“家族史”字段就得改JSPJavaBean数据库三处。应引入轻量级模板引擎// 使用Apache Velocity无需Springjar包仅300KB VelocityEngine ve new VelocityEngine(); ve.setProperty(RuntimeConstants.RESOURCE_LOADER, class); ve.setProperty(class.resource.loader.class, org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader); ve.init(); Template t ve.getTemplate(templates/outpatient_record.vm); Context context new VelocityContext(); context.put(patient, patient); context.put(diagnosis, diagnosis); context.put(customFields, getCustomFieldsByDept(deptId)); // 从dept_config表读取JSON配置 StringWriter writer new StringWriter(); t.merge(context, writer); return writer.toString(); // 返回渲染后的HTML片段对应模板outpatient_record.vm中可写#if($customFields.containsKey(allergy)) div classfield label过敏史/label input typetext nameallergy value$!{patient.allergy} /div #end3.3 缴费对账模块解决“支付成功但系统未记账”的资金悬空原始代码在PaymentServlet中先调用模拟支付接口再更新数据库若支付成功后DB写入失败钱就进了医院账户但系统无记录。必须实现最终一致性新增payment_log表字段log_id,order_no,status(0待支付,1支付中,2已成功,3已失败),create_time,update_time支付回调接口/payment/callback只做两件事更新payment_log.status2发送MQ消息可用ActiveMQ或RabbitMQ教学版可用java.util.concurrent.BlockingQueue模拟后台定时任务WebServlet(urlPatterns /reconcile, loadOnStartup 1)每5分钟扫描status1且update_time now()-10min的记录重新查支付平台接口确认状态血泪经验某次实训中学生用Thread.sleep(3000)模拟支付延迟结果Tomcat超时中断线程payment_log卡在status1后续对账任务因未捕获InterruptedException直接退出导致3笔“幽灵支付”。最终解决方案是所有异步操作必须包裹try-catch(InterruptedException)并重置中断状态Thread.currentThread().interrupt()。4. 避坑指南部署与调试中五个高频翻车点及根治方法这个项目看似简单但每个环节都有反直觉陷阱。以下是我在12届学生实训中统计出的TOP5翻车点按现象→原因→根治三步法说明4.1 现象登录成功后跳转到/index.jsp显示404但直接访问/index.jsp又能打开原因web.xml中welcome-file-list配置了index.jsp但servlet-mapping中url-pattern为/导致容器将/路径优先匹配到某个Servlet如LoginServlet而该Servlet未处理/路径的GET请求。根治在web.xml中明确声明欢迎页且确保无Servlet拦截/路径welcome-file-list welcome-filelogin.jsp/welcome-file !-- 不要用index.jsp避免冲突 -- /welcome-file-list !-- 所有Servlet的url-pattern不要写成/或/* -- servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern !-- 用具体路径 -- /servlet-mapping4.2 现象MySQL连接池报Cannot create PoolableConnectionFactory但命令行能连原因context.xml中url参数缺少serverTimezoneGMT%2B8MySQL5.7默认要求时区显式声明否则驱动抛SQLException。根治Resource namejdbc/hospital authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/hospital_db?useUnicodetrueamp;characterEncodingutf8mb4amp;serverTimezoneGMT%2B8 usernamehosp_user passwordHosp2024 /4.3 现象JSP中EL表达式${patient.name}不解析直接输出字符串原因web.xml的web-app根标签未声明Servlet 3.0规范Tomcat默认启用EL表达式开关关闭。根治检查web.xml第一行?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 !-- 必须是4.0或3.1 --4.4 现象上传身份证图片后数据库存的是乱码路径如C:\fakepath\id.jpg原因前端input typefile在Chrome/Firefox中出于安全限制JS无法获取真实绝对路径value属性只返回fakepath。原始代码用request.getParameter(photo)试图读取路径必然失败。根治必须用request.getPart(photo)Servlet 3.0Part photoPart request.getPart(photo); String fileName Paths.get(photoPart.getSubmittedFileName()).getFileName().toString(); InputStream is photoPart.getInputStream(); // 保存到服务器指定目录如 /upload/idcard/20240510/xxx.jpg String savePath /upload/idcard/ new SimpleDateFormat(yyyyMMdd).format(new Date()) / fileName; Files.copy(is, Paths.get(savePath), StandardCopyOption.REPLACE_EXISTING);4.5 现象医生排班页面显示“无排班数据”但数据库doctor_schedule表明明有记录原因DoctorScheduleDAO.java中SQL使用了LEFT JOIN但未处理NULL值当某医生某天无排班时schedule_date字段为NULL而Java实体类DoctorSchedule的date字段是java.util.Date类型无法接收NULL导致ResultSet映射失败抛SQLException。根治方案A推荐在SQL中用IFNULL(schedule_date, 1970-01-01) as schedule_dateJava端用String接收再解析方案BDAO层用rs.getTimestamp(schedule_date)Java实体类字段改为java.sql.Timestamp并增加判空逻辑if (rs.getTimestamp(schedule_date) ! null) { schedule.setDate(new Date(rs.getTimestamp(schedule_date).getTime())); }5. 进阶验证用JUnitMockito给核心Service层写可落地的单元测试很多学生认为“JavaWeb项目没法写单元测试”这是误解。Servlet容器只是运行环境业务逻辑应完全剥离。以下以RegisterService.java为例展示如何为挂号业务写真正有用的测试——不是测“能不能跑”而是测“逻辑是否防错”。5.1 测试准备隔离数据库聚焦业务规则挂号核心规则有三条同一身份证号1小时内不可重复挂号防黄牛每个科室每日号源上限50个需查department_quota表患者手机号必须符合11位数字格式我们用Mockito模拟DAO层只验证Service逻辑RunWith(MockitoJUnitRunner.class) public class RegisterServiceTest { Mock private RegisterDAO registerDAO; Mock private DepartmentDAO departmentDAO; InjectMocks private RegisterService registerService; Test public void testRegisterWithinOneHour_shouldReject() { // 给定10分钟前已有同身份证挂号记录 RegisterRecord existing new RegisterRecord(); existing.setIdCard(110101199003072315); existing.setRegTime(new Date(System.currentTimeMillis() - 10 * 60 * 1000)); // 10分钟前 // 模拟DAO返回该记录 when(registerDAO.findLatestByCard(110101199003072315)) .thenReturn(existing); // 当尝试再次挂号 RegisterRequest request new RegisterRequest(); request.setIdCard(110101199003072315); request.setPhone(13800138000); // 那么应拒绝并返回特定错误码 Result result registerService.register(request); assertEquals(400, result.getCode()); // 业务错误码 assertTrue(result.getMsg().contains(1小时内不可重复挂号)); } Test public void testRegisterExceedQuota_shouldReject() { // 给定该科室今日已挂号49人quota50 when(departmentDAO.getTodayRegCount(1)).thenReturn(49); when(departmentDAO.getQuota(1)).thenReturn(50); RegisterRequest request new RegisterRequest(); request.setDepartmentId(1); request.setIdCard(110101199003072316); Result result registerService.register(request); assertEquals(400, result.getCode()); assertTrue(result.getMsg().contains(号源已满)); } }关键技巧测试用例命名必须体现业务意图如testRegisterWithinOneHour_shouldReject而非技术动作如testRegister。这样当测试失败时团队成员一眼就知道哪个业务规则崩了。我坚持让学生用这种命名法三年下来他们提交的PR里业务缺陷率下降62%。5.2 真实压力验证用JMeter模拟百人并发挂号单元测试保逻辑集成测试保链路压力测试保容量。用JMeter验证系统瓶颈创建线程组100线程Ramp-Up Period10秒模拟10秒内涌入100人HTTP请求POST/hospital/RegisterServletBody Data填patientName患者${__Random(1,999)} idCard1101011990030723${__Random(00,99)} phone1380013${__Random(0000,9999)} departmentId${__Random(1,5)}添加“聚合报告”监听器重点关注90% Line90%请求响应时间 ≤ 800ms挂号业务合理阈值Error %错误率 0.5%需立即排查常见于数据库连接池耗尽Active Threads Over Time观察线程数是否稳定在100若持续攀升说明有线程阻塞如未关闭Connection典型瓶颈定位若Avg Response Time突增至3s但Active Threads稳定 → 数据库慢查询开slow_query_log抓SELECT COUNT(*) FROM register WHERE id_card? AND reg_time DATE_SUB(NOW(), INTERVAL 1 HOUR)若Active Threads持续增长至200 → 连接池配置不足maxActive20太小应设为505.3 安全加固三处必须添加的防护点教学项目常忽略安全但真实系统上线即面临风险。以下三点必须补风险点原始代码问题加固方案验证方式SQL注入String sql SELECT * FROM patient WHERE id_cardcard;改用PreparedStatementps.setString(1, card);用 OR 11作为idCard输入看是否查出全部患者XSS攻击JSP直接输出% patient.getName() %输出前HTML转义% org.apache.commons.text.StringEscapeUtils.escapeHtml4(patient.getName()) %输入scriptalert(1)/script看是否弹窗越权访问/admin/deletePatient?id1001无权限校验在Servlet开头加if (!ADMIN.equals(session.getAttribute(role))) { response.sendError(403); return; }用普通用户cookie访问admin接口应返回403我带的最后一届学生在答辩前用OWASP ZAP扫出17个高危漏洞其中12个来自这三类问题。他们花两天补完答辩时评委专门表扬了安全意识——这比功能炫酷更能体现工程素养。6. 我的三个硬核习惯让JavaWeb项目从“能跑”到“敢上生产”这个系统教给我的从来不是怎么写Servlet而是怎么建立对工程交付的敬畏。以下是我坚持十年的三个习惯没有玄学全是血换来的第一永远在web.xml里写明版本号和编码声明哪怕Tomcat默认支持我也要手敲?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0因为见过太多人因web.xml缺version导致EL失效又因缺encoding导致中文乱码最后花三天debug才发现是XML声明没写全。版本号不是装饰是契约。第二DAO层所有方法必须带throws SQLException绝不吞异常public ListPatient findByCard(String idCard) throws SQLException { String sql SELECT * FROM patient WHERE id_card ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, idCard); ResultSet rs ps.executeQuery(); // ... mapping logic } } // 不catch让Service层决定如何处理吞掉SQLException等于掩埋地雷。某次线上故障日志只显示“挂号失败”追查三天才发现是DAO层catch(Exception e){}吃掉了具体的MySQLTimeoutException真实原因是连接池配置错误。异常是系统的求救信号不是噪音。第三每次改完业务逻辑必跑三遍“脏数据测试”第一遍身份证号输成123长度不对第二遍手机号输成138abcd1234非数字第三遍科室ID输成999不存在的外键如果这三遍没触发预设的友好提示如“身份证号应为18位”说明校验逻辑有缺口。用户永远会用你没想到的方式输入而你的防御必须比他多想一步。这些习惯不会让你代码看起来更炫但会让每一次部署少一次凌晨三点的电话。希望帮到你。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案