JSP+SSM住院管理系统:模块设计、权限控制与部署实践

发布时间:2026/9/11 20:50:38
JSP+SSM住院管理系统:模块设计、权限控制与部署实践 简介一套基于JSPSSM框架的医院住院管理系统Java毕业设计资源面向需要完成毕业设计或课程设计的高校学生能帮助解决从环境搭建到业务功能实现的全流程问题。系统采用B/S架构使用MySQL数据库核心功能包含管理员对床位、药品、医嘱、住院消费及医护人员资料的管理医生处理病人和医嘱信息护士维护住院消费记录覆盖医院住院管理典型业务场景适合深入学习SSM框架整合与角色权限设计。压缩包共1026个文件约95.45MB以Java源码、JSP页面、CSS样式、JavaScript脚本为主辅以SQL数据库脚本、项目说明文档及演示视频其中大量图片和样式文件用于界面展示视频便于快速了解系统运行效果。已有282人学习下载内含源码、演示视频和说明文档可在Eclipse中直接导入运行帮助快速搭建项目骨架理解住院管理系统的数据流与业务逻辑为论文撰写与答辩演示提供有力支撑。1. 从病历到床位JSPSSM住院管理系统的模块边界与选型理由拿这套系统做毕业设计常有一个误区项目跑起来就算完事。实际上JSPSSM的医院住院管理系统核心不是页面有多花哨而是把医生、护士、管理员三种角色的业务边界拆清楚。管理员管医生护士资料、床位、药品、医嘱和住院消费报表医生管病人、开医嘱、看床位护士查医嘱、记费用。这些操作落到代码里就是典型的 SSM 三层结构Spring 管对象SpringMVC 接收请求MyBatis 负责 SQLJSP 只做展示。对正在做 Java 毕业设计或课程设计的人来说这套系统的价值在于能一次性看到登录权限、分页查询、统计报表和事务操作在同一个工程里怎么配合。理解这些模块边界比把项目点开更值得花时间。2. SpringMVC与MyBatis骨架把JSP页面和MySQL表串起来拿到源码后先别急着点运行。在 Eclipse 里导入 Maven 工程第一件事是确认目录结构否则配置文件和源码对不上时改起来会到处找文件。2.1 项目目录与依赖清单这套系统常见的源码结构如下hospital-system ├── src/main/java │ ├── com/hospital/controller │ ├── com/hospital/service │ ├── com/hospital/dao │ └── com/hospital/entity ├── src/main/resources │ ├── jdbc.properties │ ├── spring/applicationContext.xml │ └── spring/spring-mvc.xml ├── src/main/webapp │ ├── WEB-INF/jsp │ └── static └── pom.xmlcontroller 放 SpringMVC 的请求入口service 放业务逻辑dao 对应 MyBatis 的 Mapper 接口entity 是与表对应的实体类。resources 下的两个 spring 配置文件一个负责 Spring 容器一个负责 Web 层。WEB-INF/jsp是所有页面的最终落脚点。pom.xml 里的关键依赖如下dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.2.15.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.9/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.3.2/version /dependency /dependencies这里版本用的是 Spring 5.x 配合 MyBatis 3.5.xMySQL 8.x 驱动需要带上时区参数。JSTL 负责 JSP 里的c:forEach循环标签PageHelper 是后面做分页查询用的插件。如果是从课程设计里继承的旧工程容易看到 MyBatis 3.2 和 Spring 3.2 的组合那套配置在 Tomcat 8 以上会频繁报ClassNotFoundException建议直接按这个依赖版本升级。2.2 数据源与Spring容器配置接着看applicationContext.xml这里决定 Spring 如何连接 MySQL 并托管 MyBatis。context:component-scan base-packagecom.hospital.service/ context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classorg.springframework.jdbc.datasource.DriverManagerDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mybatis/*.xml/ property nametypeAliasesPackage valuecom.hospital.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.hospital.dao/ /beanclasspath:mybatis/*.xml告诉 MyBatis 去 resources 目录下加载所有 Mapper 映射文件。typeAliasesPackage可以把实体类简写为不带包名的别名返回结果时不用写com.hospital.entity.Bed全路径。最下面的 MapperScannerConfigurer 会自动扫描 dao 接口并生成代理这是 SSM 里最常见的配置方式。jdbc.properties 里需要注意 MySQL 8 的驱动和时区jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456如果数据库迁移到 MySQL 5.7驱动要改用com.mysql.jdbc.Driver否则连接池会报ClassNotFoundException。页面表单提交中文乱码时优先看这里有没有characterEncodingutf8。2.3 Controller→Service→Mapper请求链路以医生查看床位列表为例整个请求从浏览器开始经过 Controller 到 Service再到 MyBatis Mapper最后返回 JSP。Controller RequestMapping(/doctor) public class DoctorController { Resource private BedService bedService; RequestMapping(/list) public ModelAndView list(RequestParam(defaultValue 1) Integer pageNum) { PageHelper.startPage(pageNum, 10); ListBed bedList bedService.listAll(); PageInfoBed page new PageInfo(bedList); ModelAndView mv new ModelAndView(bed/list); mv.addObject(page, page); return mv; } }Service 层只做接口转调不直接拼 SQL。Service public class BedServiceImpl implements BedService { Resource private BedMapper bedMapper; Override public ListBed listAll() { return bedMapper.selectByExample(new BedExample()); } }BedMapper 是 MyBatis 自动生成的接口对应的 XML 文件放在 resources/mybatis 下select idselectByExample parameterTypecom.hospital.entity.BedExample resultTypecom.hospital.entity.Bed SELECT bed_id, bed_no, ward_no, bed_state, patient_id FROM bed where if testbedState ! null AND bed_state #{bedState} /if /where ORDER BY bed_no /select这段链路的关键点是 PageHelper.startPage 必须紧跟查询方法中间不能穿插其他 SQL。bed/list视图名对应WEB-INF/jsp/bed/list.jspSpringMVC 的视图解析器会补上前缀和后缀。where标签会自动处理第一个条件前的 AND避免 SQL 拼接错误。2.4 JSP页面的JSTL渲染与路径处理页面里不要写 Java 脚本片段尽量用 JSTL 和 EL 表达式这样页面改起来更直观。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table classtable table-bordered tr th床位号/th th病区/th th状态/th th病人编号/th /tr c:forEach items${page.list} varbed tr td${bed.bedNo}/td td${bed.wardNo}/td td${bed.bedState 1 ? 占用 : 空闲}/td td${bed.patientId}/td /tr /c:forEach /table c:if test${page.pages 1} a href${pageContext.request.contextPath}/doctor/list?pageNum${page.prePage}上一页/a ${page.pageNum}/${page.pages} a href${pageContext.request.contextPath}/doctor/list?pageNum${page.nextPage}下一页/a /c:ifpageContext.request.contextPath是动态前缀部署到 Tomcat 的任意应用名目录下都不用改链接。这里的三元表达式把床位的 0/1 转成页面上能看懂的文案。如果后续要支持“jsp个人信息展示页面”这类个人中心页面把请求路径换成/doctor/info再用同样的 EL 方式输出字段即可不需要额外引入前端框架。3. 医嘱、床位与住院消费核心表结构与事务处理代码源码中真正难的不是登录页而是医嘱、药品和住院消费这三个模块怎么串起来。医生开医嘱护士执行医嘱药品库存减少病人费用增加中间任何一步失败都会造成数据不一致。3.1 三张角色表与床位相关表设计系统里的管理员、医生、护士不是一律放进 user 表的而是各自独立成表兼顾了登录和后续资料维护。以下是在演示项目中可以直接使用的建表语句CREATE TABLE admin ( admin_id INT PRIMARY KEY AUTO_INCREMENT, admin_name VARCHAR(30) NOT NULL, password VARCHAR(64) NOT NULL, real_name VARCHAR(30) ); CREATE TABLE doctor ( doctor_id INT PRIMARY KEY AUTO_INCREMENT, doctor_name VARCHAR(30) NOT NULL, dept_id INT, position VARCHAR(20) ); CREATE TABLE nurse ( nurse_id INT PRIMARY KEY AUTO_INCREMENT, nurse_name VARCHAR(30) NOT NULL, dept_id INT, phone VARCHAR(20) );下面是床位和住院记录表表名作用关键字段bed维护床位状态bed_no、ward_no、bed_state、patient_idin_patient保存入出院时间patient_id、bed_id、in_date、out_dateadvice记录医嘱及执行状态doctor_id、patient_id、drug_id、statecost住院消费流水patient_id、item_name、amount、money代码里的bed.patient_id是一个冗余字段作用是让护士在首页直接看到“病人在几号床”省去每次 join in_patient。住院记录才保留真正的入出院时间。这种设计在毕业设计场景足够不用引入复杂的并发控制。3.2 医嘱、药品和费用在一个事务里处理医生开医嘱后护士执行时要同时完成三件事插入医嘱、扣减药品库存、写入消费记录。把它们放到同一个事务方法里是避免数据错乱的关键。Service public class AdviceService { Resource private AdviceMapper adviceMapper; Resource private DrugMapper drugMapper; Resource private CostMapper costMapper; Transactional(rollbackFor Exception.class) public void addAdviceAndDeduct(Integer doctorId, Integer patientId, Integer drugId, Integer amount, Double price) { Advice advice new Advice(); advice.setDoctorId(doctorId); advice.setPatientId(patientId); advice.setDrugId(drugId); advice.setState(0); // 0待执行1已执行 adviceMapper.insert(advice); Drug drug drugMapper.selectByPrimaryKey(drugId); if (drug.getStock() amount) { throw new RuntimeException(药品库存不足医嘱作废); } drugMapper.reduceStock(drugId, amount); Cost cost new Cost(); cost.setPatientId(patientId); cost.setItemName(drug.getDrugName()); cost.setAmount(amount); cost.setMoney(amount * price); costMapper.insert(cost); } }对应的药品库存扣减 SQL 要写成原子条件更新update idreduceStock UPDATE drug SET stock stock - #{amount} WHERE drug_id #{drugId} AND stock #{amount} /updateTransactional保证这三条 SQL 要么全部成功要么全部回滚。reduceStock中的AND stock #{amount}是原子判断在高并发下也能防止把库存扣成负数。方法里的 price 应从数据库的药品表取出不能用前端传值否则费用报表会被恶意篡改。3.3 防止护士重复执行医嘱另一种常见失误是护士连续点击两次“执行医嘱”导致同一份医嘱被扣两次药品。可以在执行时对医嘱状态做条件更新int updated adviceMapper.updateStateIfWaiting(adviceId); if (updated 0) { throw new RuntimeException(医嘱已执行请勿重复操作); }对应的 SQLupdate idupdateStateIfWaiting UPDATE advice SET state 1 WHERE advice_id #{adviceId} AND state 0 /update这条语句执行后如果返回影响行数为 0说明当前医嘱已经处于执行状态后续扣库存和记账逻辑不再执行。这个“状态字段 条件更新”的思路在订单、床位上同样适用。4. 三种角色权限与统计报表从登录拦截到分组查询管理员、医生、护士看到的内容不同最直接的做法是在登录时把角色名写入 session然后用 SpringMVC 拦截器统一拦截。4.1 登录拦截器按角色放行在实际源码里登录 Controller 会把 role 和 userId 同时放进 session。拦截器只需要判断 URL 前缀和角色是否匹配public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { Object role req.getSession().getAttribute(role); String uri req.getRequestURI(); if (role null) { resp.sendRedirect(req.getContextPath() /login.jsp); return false; } if (uri.contains(/admin/) !admin.equals(role)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } if (uri.contains(/doctor/) !doctor.equals(role)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } return true; } }在 spring-mvc.xml 里注册拦截范围mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/**/ mvc:exclude-mapping path/static/**/ bean classcom.hospital.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors/**会拦截所有请求所以必须放行/login/**和静态资源。static目录包含 bootstrap 系列 css 和 js 文件如果被拦截登录页会白板一张。如果后续引入更复杂的权限框架可以在 spring-mvc.xml 里调整 exclude-mapping但角色 URL 前缀的设计仍可沿用。4.2 管理员消费报表的分组SQL管理员查看住院病人消费金额统计报表本质是对 cost 表按病人和日期做 SUM 聚合。一种常见写法是SELECT patient_id, DATE_FORMAT(create_time, %Y-%m-%d) AS cost_date, SUM(money) AS total_money FROM cost GROUP BY patient_id, DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY cost_date DESC, total_money DESC;在 MyBatis 里报表查询结果可以直接用 Map 接收省去创建统计实体类select idselectCostReport resultTypemap SELECT p.patient_name, b.bed_no, SUM(c.money) AS total_money FROM cost c LEFT JOIN patient p ON c.patient_id p.patient_id LEFT JOIN bed b ON b.patient_id p.patient_id WHERE c.create_time BETWEEN #{startTime} AND #{endTime} GROUP BY p.patient_id, b.bed_no ORDER BY total_money DESC /selectresultTypemap适合管理员页面的表格展示不需要为每一行定义 JavaBean。startTime 和 endTime 由页面日期控件传入格式为yyyy-MM-dd HH:mm:ss。若报表需要保留两位小数可以在 JSP 中用fmt:formatNumber格式化。结合权限矩阵这套系统的访问控制大致如下页面前缀管理员医生护士/admin/**允许禁止禁止/doctor/**允许允许禁止/nurse/**允许禁止允许这种矩阵与拦截器里三个if一一对应。管理员虽然能访问所有控制器但页面上的菜单入口仍要按角色隐藏避免无权限操作被直链暴露。4.3 护士查询药品的分页与模糊匹配护士查询药品、病人的页面大多带搜索框和分页。下面的代码是常见的组合写法。public PageInfoDrug searchDrug(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); ListDrug drugs drugMapper.selectByKeyword(keyword); return new PageInfo(drugs); }对应的 Mapper XMLselect idselectByKeyword resultTypeDrug SELECT drug_id, drug_name, spec, stock, price FROM drug where if testkeyword ! null and keyword ! drug_name LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY drug_id /selectPageHelper.startPage 必须和查询方法在同一线程且中间不能有其他数据库操作。LIKE CONCAT(%, #{keyword}, %)比%#{keyword}%稳定后者会被 MyBatis 当成字符串的一部分导致拼出来的 SQL 不正确。搜索框的 name 设置为 keyword 后Controller 直接用RequestParam接收即可。5. 部署后常用的两个技巧JSP编译路径与Excel导出项目跑通后会遇到修改 JSP 不生效、导出报表没反应两个高频问题下面两个技巧能直接解决问题。5.1 JSP的class文件保存在哪里Tomcat 会把 JSP 翻译成 Java 再编译成 class位置默认在 Tomcat 的 work 目录下。路径大致是apache-tomcat-9.0.76/work/Catalina/localhost/工程名/org/apache/jsp/WEB_002dINF/jsp/当你发现修改 JSP 文件后浏览器没变化先看work/Catalina目录下有没有编译文件。如果 Eclipse 部署的是增量副本干净清掉 Tomcat 缓存再重新发布即可rm -rf apache-tomcat-9.0.76/work/Catalina这个操作等价于右键 Tomcat 的 Clean但命令行处理更直接。它不会删除你的源码只会让 JSP 在下次请求时重新编译。只要项目里没有把 JSP 放到奇怪位置都能借此解决页面缓存问题。5.2 用POI把住院消费明细导出到ExcelJSP 页面本身不适合直接输出 Excel 文件常见做法是用 Apache POI 在 Controller 里生成.xls再写回 response 输入流。先加依赖dependency groupIdorg.apache.poi/groupId artifactIdpoi/artifactId version5.2.3/version /dependency然后写一个导出接口RequestMapping(/export) public void exportCost(HttpServletResponse response) { ListCost list costMapper.selectAll(); try (Workbook wb new HSSFWorkbook()) { Sheet sheet wb.createSheet(住院消费); Row header sheet.createRow(0); header.createCell(0).setCellValue(病人编号); header.createCell(1).setCellValue(费用项目); header.createCell(2).setCellValue(金额); for (int i 0; i list.size(); i) { Row row sheet.createRow(i 1); row.createCell(0).setCellValue(list.get(i).getPatientId()); row.createCell(1).setCellValue(list.get(i).getItemName()); row.createCell(2).setCellValue(list.get(i).getMoney()); } response.setContentType(application/vnd.ms-excel;charsetutf-8); response.setHeader(Content-Disposition, attachment;filenamecost.xls); wb.write(response.getOutputStream()); } catch (IOException e) { throw new RuntimeException(e); } }HSSFWorkbook 是最基础的.xls格式文件体积小适合毕设演示。金额列在写入前应先用 cell.setCellType 转为数字类型否则 Excel 打开后会当作文本SUM 函数无法正确汇总。若导出文件名需要包含中文必须对文件名做 URL 编码否则不同浏览器容易乱码。本文还有配套的精品资源点击获取