SSH框架医院物资管理系统课程设计详解
简介基于SSH框架Struts2、Spring、Hibernate开发的医院物资管理系统课程设计项目面向Java Web学习者、高校在校生以及需要完成类似毕业设计或课设的开发者。系统覆盖物资采购、入库、领用、库存查询、盘点、报损报溢、调拨等核心业务前后端页面与交互逻辑完整能直观看到登录、主页等模块运行效果代码结构体现MVC分层、Spring依赖注入与事务管理、Hibernate对象关系映射等关键实践。压缩包共12个文件包含项目源码rar、SQL数据库脚本、操作演示avi、课程设计报告doc/docx、界面截图jpg/png以及阅读说明txt整体大小44.81MB文件类型覆盖源代码、数据库、视频演示和设计文档四类用途。其中报告对需求分析、总体设计、数据库表结构、类图及功能模块划分均有叙述视频则能辅助快速搭建运行环境并理解操作流程。已有36人学习浏览适合用来理解SSH整合思路、掌握医院物资管理的业务流程或直接在此基础上进行功能扩展和二次开发。1. jsp552 医院物资管理系统(ssh)课程设计在解决什么问题解压课程设计压缩包jsp552 只是命名序号技术栈在内部JSP 做视图SSHStruts2 Spring Hibernate做控制、业务和持久化。这里的 ssh 是三个 Java 框架的缩写不是连服务器用的 Secure Shell 工具。系统要解决的场景很具体科室领物资、库房登台账、月底对账。Excel 管会出现同名物资多条记录、出库后库存对不上、领用人查不到历史流水。落到项目里就是物资维护、入库单、出库单、库存查询、供应商管理、用户登录等模块。适合做课程设计要找思路的学生也适合要接手 SSH 老项目的工程师。下文按框架分工、建表与代码、页面查询、部署排错的顺序展开环境按 JDK 1.8 Tomcat 8.5 MySQL 5.7 常见组合来讲。2. SSH 三框架的分工从 struts.xml 到 applicationContext.xml 的配置线课程设计里最常见的翻车点不是功能没写出来而是三个框架的配置互相咬合不上Action 找不到、Service 注入失败、Session 提前关闭。先搞清楚每个框架管到哪一层再对照配置文件排查定位就快很多。下面按请求从浏览器进入 Struts2 的顺序把三份核心配置串起来讲。2.1 Struts2 在物资管理里处理什么表单到 Action 的流转Struts2 在 SSH 里负责 MVC 的 C。用户在入库页点提交浏览器发一个 /material/save.action 请求StrutsPrepareAndExecuteFilter 拦截后按 struts.xml 映射找到 Action 类和要执行的方法把表单字段自动填进 Action 对应的 setter方法返回 success 或 input 字符串框架再按 result 配置转发到 JSP。课程设计规模下一个 Action 管一个模块就够MaterialAction 管物资列表和新增StockAction 管入库出库的入口。struts !-- 统一请求编码post 提交不乱码的第一道保险 -- constant namestruts.i18n.encoding valueUTF-8 / package namemms namespace/material extendsstruts-default action namelist classmaterialAction methodlist result namesuccess/WEB-INF/views/material/list.jsp/result /action !-- 保存成功后重定向到列表防止 F5 重复提交 -- action namesave classmaterialAction methodsave result namesuccess typeredirectActionlist/result result nameinput/WEB-INF/views/material/edit.jsp/result /action /package /strutsnamespace 决定访问前缀/material/list.action 才能命中上面第一个 action。class 写的是 Spring 容器里的 bean 名 materialAction不是类的全路径这是 struts2-spring-plugin 的约定如果项目没集成 Spring 插件而是直接 new 的 Action这里才写全类名。method 指定执行方法save 返回 input 表示表单校验未通过回到编辑页保留用户输入。2.2 Spring 容器如何把 Action、Service、DAO 串成一个整体Spring 负责对象的创建和依赖注入。Action 要调 ServiceService 要调 DAODAO 要拿到 SessionFactory 才能操作数据库。课程设计普遍用注解扫描加少量 XMLService 和 DAO 打 Service、Repository 注解数据源、SessionFactory 这类没法用注解描述的对象写进 applicationContext.xml。!-- 扫描 com.hospital.mms 下所有带注解的类 -- context:component-scan base-packagecom.hospital.mms / bean iddataSource classorg.apache.commons.dbcp.BasicDataSource property namedriverClassName valuecom.mysql.jdbc.Driver / !-- 注意 amp; 是 的 XML 转义少一层会解析失败 -- property nameurl valuejdbc:mysql://localhost:3306/hospital_mms?useUnicodetrueamp;characterEncodingutf8 / property nameusername valueroot / property namepassword value123456 / /bean bean idsessionFactory classorg.springframework.orm.hibernate5.LocalSessionFactoryBean property namedataSource refdataSource / !-- 自动扫描 Entity 的实体类省掉 hbm.xml 映射文件 -- property namepackagesToScan valuecom.hospital.mms.entity / property namehibernateProperties props prop keyhibernate.dialectorg.hibernate.dialect.MySQL5Dialect/prop prop keyhibernate.show_sqltrue/prop prop keyhibernate.hbm2ddl.autoupdate/prop /props /property /beanpackagesToScan 只扫实体包show_sql 打开后控制台会打印 Hibernate 生成的 SQL排错时最有用hbm2ddl.auto 的 update 会根据实体自动建表加列课程设计图省事可以开生产环境必须改成 validate。url 里的 useUnicode 和 characterEncoding 是中文入库不乱码的关键参数在 XML 中 必须写成 这条写错启动时会报连接串解析异常。层次代表类配置位置职责控制层MaterialActionstruts.xml接收参数、调 Service、决定返回哪个 JSP业务层StockInServiceImplService 注解事务边界、库存校验等业务规则持久层MaterialDaoImplRepository 注解用 HQL 或 Criteria 读写数据库视图层list.jsp/WEB-INF/views用 EL 和 JSTL 展示数据、提交表单2.3 Hibernate 的实体映射与事务边界设在哪一层实体类和表一一对应注解映射比配置 hbm.xml 省事。Material 实体的写法是标准样式Entity 声明持久化类Table 指定表名Id 加 GeneratedValue(strategy GenerationType.IDENTITY) 对应 MySQL 自增主键。字段上的 Column 只有列名和字段名不一致时才需要写materialCode 映射到 material_code 就必须写。Entity Table(name material) public class Material { Id GeneratedValue(strategy GenerationType.IDENTITY) private Integer id; Column(name material_code, length 20) private String materialCode; Column(name material_name, length 50) private String materialName; Column(name spec, length 50) private String spec; Column(name unit, length 10) private String unit; Column(name stock) private Integer stock; // getter/setter 用 IDE 生成此处省略 }事务边界要放在 Service 层Spring 用 HibernateTransactionManager 统一管理Transactional 可加在实现类上也可加在具体方法上。两个常见错误一是把 Transactional 加到 DAO 上Service 调两个 DAO 方法时各自提交中途失败会出现库存改了、流水没写的脏数据二是整份配置里没注册 transactionManager运行时报 Could not obtain transaction-synchronized Session。DAO 里取 Session 统一用 sessionFactory.getCurrentSession()它跟随事务、事务结束自动关闭不要用 openSession() 自己关漏关会一直占着连接池。3. 医院物资入库出库的建表 SQL 与 Action-Service-DAO 链路物资管理系统的核心不在页面多少而在库存怎么变动。设计上守住一条原则任何入库或出库流水都必须和库存变动发生在同一个事务里。顺着这个原则建表、写 DAO、写 Service顺序不容易乱。3.1 物资、入库单、出库单三张核心表的字段设计三张表就够material 记录物资基础信息和当前库存stock_in 和 stock_out 各记一条变动流水。课程设计不推荐做成订单主子表入库单一行物资一张单拆成 header 和 line 两层纯属增加 JOIN评审也不会因为多两张表加分。建表 SQL 如下字符字段统一 utf8。CREATE TABLE material ( id INT AUTO_INCREMENT PRIMARY KEY, material_code VARCHAR(20) NOT NULL COMMENT 物资编码, material_name VARCHAR(50) NOT NULL COMMENT 物资名称, spec VARCHAR(50) COMMENT 规格型号, unit VARCHAR(10) COMMENT 计量单位, stock INT NOT NULL DEFAULT 0 COMMENT 当前库存, supplier_id INT COMMENT 默认供应商ID ) ENGINEInnoDB DEFAULT CHARSETutf8; CREATE TABLE stock_in ( id INT AUTO_INCREMENT PRIMARY KEY, material_id INT NOT NULL COMMENT 物资ID, quantity INT NOT NULL COMMENT 入库数量, in_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 入库时间, operator VARCHAR(20) COMMENT 经办人, remark VARCHAR(100) COMMENT 备注, KEY idx_material (material_id) ) ENGINEInnoDB DEFAULT CHARSETutf8; CREATE TABLE stock_out ( id INT AUTO_INCREMENT PRIMARY KEY, material_id INT NOT NULL COMMENT 物资ID, quantity INT NOT NULL COMMENT 出库数量, out_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 出库时间, department VARCHAR(30) COMMENT 领用科室, operator VARCHAR(20) COMMENT 经办人, remark VARCHAR(100) COMMENT 备注, KEY idx_material (material_id) ) ENGINEInnoDB DEFAULT CHARSETutf8;stock 字段用 INT医疗耗材按最小单位计数避免出现 0.5 件这种小数。库存冗余在 material 表里而不是靠 SUM 流水算出来是因为每次查询都扫流水表数据一多 SQL 越写越重课程设计里直接维护一个冗余字段最实用。department 和 operator 是后期按科室、按经办人统计的主要筛选条件长度按实际业务留够。字段类型选择理由material.stockINT按最小单位计数不做小数盘点stock_in.in_timeDATETIME默认 CURRENT_TIMESTAMP代码不用手动赋值stock_out.departmentVARCHAR(30)科室名长度够用普通 KEY 支持筛选3.2 入库实现在一个事务里写流水并增加库存入库的业务规则一句话先校验物资存在再把库存加上去最后插入一条流水。三个动作必须在同一个事务里否则要么库存和流水对不上要么出现只有流水没有库存的脏数据。Service(stockInService) Transactional public class StockInServiceImpl implements StockInService { Resource private MaterialDao materialDao; Resource private StockInDao stockInDao; Override public boolean inbound(StockIn in) { Material material materialDao.findById(in.getMaterialId()); if (material null) { return false; // 物资不存在直接返回失败 } material.setStock(material.getStock() in.getQuantity()); stockInDao.save(in); // 插入入库流水 return true; } }material 是 Hibernate 的持久态对象事务提交时 Hibernate 会把当前状态和快照对比自动生成 update 语句所以不调 materialDao.update(material) 也能写库课程设计里为了意图清楚显式调一次 update 也常见两种都对。顺序上先改库存再插流水或反过来不影响最终一致性但按先校验、再改动、后记流水读起来最清晰。Repository(stockInDao) public class StockInDaoImpl implements StockInDao { Resource private SessionFactory sessionFactory; private Session getSession() { // 必须配了事务管理器才能拿到当前 Session return sessionFactory.getCurrentSession(); } Override public void save(StockIn in) { getSession().save(in); } }提示控制台看到 No Hibernate Session bound to thread 时先确认 Service 方法上有 Transactional再检查 applicationContext.xml 里是否注册了 HibernateTransactionManager两个条件缺一个都会报这个错。3.3 出库扣减库存的写法与并发下的库存校验出库比入库多一个条件库存够不够。最稳的写法不是先查出来在 Java 里比大小再 update而是把 m.stock :qty 直接写进 update 语句让数据库在更新那一刻判断。这样即使两个请求同时出库行锁也只会放行其中一个。Override public boolean outbound(StockOut out) { Session session sessionFactory.getCurrentSession(); String hql update Material m set m.stock m.stock - :qty where m.id :id and m.stock :qty; int updated session.createQuery(hql) .setParameter(qty, out.getQuantity()) .setParameter(id, out.getMaterialId()) .executeUpdate(); if (updated 0) { return false; // 库存不足或物资不存在事务回滚 } session.save(out); // 插入出库流水 return true; }executeUpdate 返回受影响行数0 行说明物资不存在或库存不够返回 false 让事务回滚不会出现负数库存1 行说明扣减成功继续写流水。Action 层拿到 false 之后往 request 或 session 里存一条“库存不足”提示再由 result 配置回到列表页。不要在 Service 里为这个场景抛业务异常课程设计的页面提示用标志位最简单答辩时能说清 return false 和抛异常的取舍反而是加分项。4. JSP 列表页、分页查询与登录拦截的可抄写法框架通了剩下的功夫全在视图和数据展示。把最耗时间的三个点写清楚JSP 放哪里、分页怎么做、登录怎么拦按这个顺序抄能少调一晚上。4.1 视图放哪、EL 和 JSTL 怎么用附带个人信息展示视图文件建议统一放在 /WEB-INF/views 下。放在这里的 JSP 不能通过 URL 直接访问必须先经过 Action 的 result 转发等于一层免费的访问控制。页面里只用 EL 表达式和 JSTL 标签取数不要写 % % Java 脚本段循环、判空混在 HTML 里答辩时被问维护性会很尴尬。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle物资列表/title/head body p当前用户${sessionScope.loginUser.realName}${sessionScope.loginUser.username}/p table border1 cellpadding6 tr th物资编码/thth名称/thth规格/thth单位/thth库存/thth操作/th /tr c:forEach items${pageBean.list} varm tr td${m.materialCode}/td td${m.materialName}/td td${m.spec}/td td${m.unit}/td td${m.stock}/td td a href${pageContext.request.contextPath}/material/in_add.action?materialId${m.id}入库/a a href${pageContext.request.contextPath}/material/out_add.action?materialId${m.id}出库/a /td /tr /c:forEach /table /body /html第一行 EL 从 session 里取 loginUser 对象展示姓名和账号这就是课程设计里最常见的个人信息展示页面写法登录时往 session 放对象JSP 用 sessionScope 取不需要额外配置。c:forEach 的 items 是集合属性var 是循环变量名${m.stock} 自动调用 getStock()。链接里拼 pageContext.request.contextPath 是为了换上下文路径部署时不 404用写死的相对路径war 换个项目名访问就会断。in_add 和 out_add 两个 action 在 struts.xml 里分别配 method 指向 inAddPage 和 outAddPage页面把 materialId 用隐藏域带回表单提交。4.2 分页查询的 PageBean 与 HQL 写法物资表几百条数据时不分页也能用但课程设计评审表里经常有“分页查询”这一项躲不掉。PageBean 自己写一个通用类字段固定这几样。字段类型说明pageNumint当前页码从 1 开始pageSizeint每页条数默认 10totallong总记录数totalPagesint总页数(total pageSize - 1) / pageSizelistListT当前页数据Override public PageBeanMaterial findPage(int pageNum, int pageSize, String keyword) { PageBeanMaterial pb new PageBean(); pb.setPageNum(pageNum); pb.setPageSize(pageSize); Session session sessionFactory.getCurrentSession(); // 先统计总数用于计算总页数和页码条 String countHql select count(*) from Material where materialName like :kw; Long total session.createQuery(countHql, Long.class) .setParameter(kw, % keyword %) .getSingleResult(); pb.setTotal(total); pb.setTotalPages((int) ((total pageSize - 1) / pageSize)); // 再取当前页数据setFirstResult 是偏移量setMaxResults 是条数 String listHql from Material where materialName like :kw order by id desc; pb.setList(session.createQuery(listHql, Material.class) .setParameter(kw, % keyword %) .setFirstResult((pageNum - 1) * pageSize) .setMaxResults(pageSize) .list()); return pb; }count 和列表各打一次 SQLtotalPages 用向上取整公式避免最后一页算成 0。setFirstResult 传 (pageNum - 1) * pageSize第 1 页偏移 0第 2 页偏移 10正好对应 MySQL 的 LIMIT 10, 10。页码越界时列表会是空集合Service 里把 pageNum 钳位到 1 和 totalPages 之间即可。keyword 为空时 % 和 % 也匹配全部记录不用单独写分支。4.3 登录会话与 Struts2 拦截器做权限控制登录态用 session 存判断用拦截器。Struts2 拦截器在 Action 方法执行前插入逻辑session 里没有 loginUser 就返回 login 结果串由全局结果跳到登录页。这里有个很隐蔽的坑登录 action 本身也会被拦截必须放在不带这个拦截器的 package 里否则永远进不了登录流程。public class LoginInterceptor extends AbstractInterceptor { Override public String intercept(ActionInvocation invocation) throws Exception { MapString, Object session invocation.getInvocationContext().getSession(); if (session.get(loginUser) null) { return login; // 未登录跳到登录页 } return invocation.invoke(); // 已登录放行执行 Action } }!-- 登录包不挂登录拦截器否则自己都登不进去 -- package namelogin namespace/ extendsstruts-default action namedoLogin classloginAction methodlogin result namesuccess typeredirectAction/material/list/result result nameinput/login.jsp/result /action /package !-- 带拦截器的基包业务包 extends 它即可复用拦截器栈 -- package namesecure namespace/ extendsstruts-default interceptors interceptor nameloginCheck classcom.hospital.mms.interceptor.LoginInterceptor / interceptor-stack namemmsStack interceptor-ref namedefaultStack / interceptor-ref nameloginCheck / /interceptor-stack /interceptors default-interceptor-ref namemmsStack / global-results result namelogin/login.jsp/result /global-results /packagelogin package 只放 doLogin 一个 action走 struts-default 原始拦截器secure package 里定义了拦截器栈和全局结果。把第 2 章 material 所在 package 的 extends 从 struts-default 改成 secure/material 下的 action 就全部套上了登录检查这是 Struts2 里继承 package 复用拦截器栈的标准做法。login.jsp 要放在 webapp 根目录而不是 /WEB-INF/views因为它是被拦截器直接转发的登录页放受保护目录里会出现已登录也打不开的情况。5. Tomcat 部署后的验证步骤与三个高频坑5.1 打包部署与核心流程自测清单Maven 工程在根目录执行 mvn clean packageEclipse 工程右键 Export 选 WAR file把产物拷到 Tomcat 的 webapps 目录后启动访问 http://localhost:8080/项目名/。自测走一条固定链路登录 → 新增“一次性注射器” → 入库 10 件 → 列表库存显示 10 → 出库 3 件 → 库存变 7 → 到数据库查 stock_in、stock_out 各有一条记录数量和时间对得上。这条链路全通系统核心功能就算验收通过。启动失败时不要盯着页面看先翻 Tomcat 的 logs/catalina.out 最后 30 行端口被占、jar 没打进去、数据库连不上三类问题在日志里都有明确关键字。5.2 中文乱码与 JSP 编译缓存位置中文乱码按三个位置排查顺序不要乱JSP 头部的 pageEncoding 和 contentType 都写 UTF-8MySQL 连接串带 useUnicodetruecharacterEncodingutf8Tomcat 的 server.xml 里 Connector 加 URIEncodingUTF-8。前两个解决 post 提交和入库最后一个解决 GET 链接带中文参数的情况。另一个课程设计里高频的问题是“jsp 编译 class 文件保存在哪里”答案是 Tomcat 的 work/Catalina/localhost/项目名/org/apache/jsp 目录里面能看到每个 JSP 编译出的 .java 和 .class。改完 JSP 不生效时把 work 目录下对应项目整个删掉再重启 Tomcat比反复刷新页面靠谱得多。5.3 高频异常对照表与处理方式报错或现象常见原因处理方式Could not obtain transaction-synchronized SessionDAO 在事务外被调用检查 Service 方法是否有 TransactionalLazyInitializationException: could not initialize proxyJSP 里访问懒加载关联Session 已关闭HQL 里加 fetch或配置 OpenSessionInView 过滤器404 或 The requested resource is not availablestruts.xml 的 namespace 和链接路径对不上按 namespace action name 拼完整 URL 核对页面能打开但中文全是问号三处编码漏了其中一处按 5.2 的顺序逐项检查最后一招把 Tomcat 的 work 和 webapps 下已解压的项目目录全部清掉重新部署一次SSH 老项目里一半的“改了没反应”都是旧 class 缓存引起的与其怀疑代码不如先清缓存重启再看日志。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →