ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于SSM框架的医疗器械设备租赁报修借用管理系统设计与实现

基于SSM框架的医疗器械设备租赁报修借用管理系统设计与实现 做设备管理系统的同行应该都有同感医疗机构里设备管理真正让人头疼的不是台账清不清楚而是流程通不通畅。SSM260是一套基于SSM框架Spring SpringMVC MyBatis开发的医疗器械设备租赁报修借用管理系统立项初衷就是要把设备科的日常账目、租借审批、维修跟踪全部收进一套系统里。这套系统以MySQL为存储通过经典三层架构实现设备台账、租赁订单、借用登记、报修工单的闭环管理。如果你正在规划类似的设备管理系统或者在学校做SSM方向的实战项目下面这些设计思路和踩坑记录应该能直接用上。1. 需求拆解租赁、借用、报修不是三个孤岛1.1 设备科的真实痛点我在做SSM260之前先花了一周时间蹲在设备科看他们怎么干活。当时的场景大概是这样的全院的监护仪、注射泵、呼吸机、除颤仪加起来几百台但主要靠一个Excel台账维护谁借走了、借多久、什么时候还完全依赖口头登记。报修更麻烦设备故障后送修修到什么程度、花了多少钱、什么时候能返回科室继续用经常要打电话问工程师。这些场景汇总下来设备科最需要解决的其实就是三个问题设备在哪儿、设备能用吗、设备修得怎么样。租赁、借用、报修这三个词听起来是独立的业务模块但实际运营中它们是交叉的一台监护仪早上可能被A科室借用下午故障了要转成报修单修完之后又可能被B科室租走。如果系统把这三种单子完全分开设计设备状态根本对不上最后还是回到Excel死路。1.2 三条业务线的共性与差异所以我在设计SSM260的需求模型时没有把租赁、借用、报修当成三个需要分别搭建的独立系统而是把它们统一抽象成“围绕设备台账发生的状态流转事件”。它们的共性是都围绕同一张设备主表必须知道每台设备当前的真实状态。都包含申请、审批、执行、结束的流程节点需要记录每个节点的操作人、时间和结果。都依赖时间维度租赁有起止日期借用有预计归还时间报修有维修周期。都会改变设备在系统中的可用状态提交申请后设备不能继续被其他人申请。差异在于业务规则。租赁涉及费用计算有日租金、押金、超期违约金借用强调的是科室之间的快速周转审批层级相对简单但必须设置预计归还时间报修涉及工程师派工、维修记录、费用归集和修后验收。搞清楚这些共性和差异之后数据库模型和组织代码的结构就会清晰很多。1.3 用户角色与操作边界SSM260的用户角色划分得比较细因为设备科系统不像普通企业网站不同角色之间操作范围差异很大。实际项目中我定义了四类核心角色权限边界如下角色典型操作数据范围普通科室用户发起借用、租赁、报修申请查看本科室单据仅本科室设备科管理员设备台账维护、租借审批、报修派工、出入库登记全部设备与单据维修工程师接单、填写维修过程、提交完工或报废建议分配给自己的工单财务/分管领导查看租赁费用、维修费用、设备利用率报表全院汇总数据这个角色划分决定了后面的权限模块怎么做普通用户查询列表时必须在SQL层面带上学号管理员则可以跨科室查看。这里有个容易被忽略的点报修申请由科室用户发起但派工和维修记录必须由设备科管理员和工程师操作如果权限控制不到位会导致流程还没审批完设备状态就被人为改了。2. 技术选型复盘这个项目为什么仍选SSM框架2.1 选型背景与取舍很多人看到SSM会问为什么不用Spring Boot这个问题我在项目启动时也纠结过。当时客户环境的实际情况是内网部署JDK版本停留在1.7Tomcat版本也比较旧直接上Spring Boot 2.x会遇到兼容性问题。加上团队长期做SSM项目对Spring、SpringMVC、MyBatis整合的坑已经摸得很清楚SSM260又是典型的表单加列表类系统没有复杂的微服务需求SSM框架在稳定性和可控性上完全够用。另一个现实原因是项目预算和工期。医疗器械管理系统的部署环境通常很固定客户不希望系统随便升级依赖SSM这种传统架构在客户眼里反而没那么“重”改bug也方便。这里我不劝退Spring Boot新项目我一般也会优先Spring Boot但选型永远要结合部署环境、团队能力和项目性质来定不是越新越好。2.2 工程结构与请求流转SSM260的工程结构是经典的单应用分层简单直接方便排查问题ssm260-device/ ├── pom.xml ├── src/main/java/com/ssm260/device │ ├── controller # 控制层接收请求、参数校验 │ ├── service # 业务层事务控制核心流程 │ ├── dao # MyBatis Mapper接口 │ ├── pojo # 实体类、DTO、VO │ ├── interceptor # 登录拦截器、权限拦截器 │ └── util # 工具类 ├── src/main/resources │ ├── jdbc.properties │ ├── mybatis-config.xml │ ├── spring-context.xml │ └── spring-mvc.xml └── src/main/webapp ├── WEB-INF/views └── static请求流转路径是前端JSP页面发起HTTP请求SpringMVC的DispatcherServlet根据HandlerMapping找到对应的Controller方法Controller做参数封装和基础校验后调用Service层Service层持有事务和核心业务逻辑通过MyBatis的Mapper接口操作MySQL数据库。这套链路很成熟排查问题时按照“页面参数 - Controller - Service - SQL”的顺序一层层看就行。2.3 Maven依赖与整合配置Maven依赖里需要重点注意版本兼容。我当时用的是Spring 4.3.x、MyBatis 3.4.x、mybatis-spring 1.3.x这套组合在JDK 1.7下运行稳定。核心依赖片段如下dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version4.3.30.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.4.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version1.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.47/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.1.10/version /dependencySSM整合的关键是spring-context.xml里的SqlSessionFactoryBean、MapperScannerConfigurer和事务管理器三者缺一不可。我见过不少人在整合时把mapper.xml文件放错目录导致启动时找不到SQL映射实际排查起来就是看spring-context.xml里的mapperLocations路径和实际打包目录是否一致。前端这块我选的是JSP加Bootstrap加layui没用前后端分离原因很简单SSM这类传统项目要快速交付服务端渲染加表格组件能省掉大量联调时间按钮级权限在JSP里用自定义标签控制也更直接。3. 数据库与状态机设计所有业务围绕设备状态转3.1 核心表全景SSM260的核心表一共八张覆盖设备主数据、三类业务单据和系统权限表名用途device设备台账记录设备编码、名称、分类、状态、位置、价格device_category设备分类如监护仪、注射泵、呼吸机lease_order租赁单含起止时间、日租金、总费用、状态borrow_order借用单含预计归还时间、实际归还时间、状态repair_order报修单含故障描述、派工人、维修状态、费用repair_record维修过程记录一条报修单可对应多条维修记录sys_user用户表关联科室和角色operation_log操作日志记录关键流转动作每张业务单都冗余了device_id、device_name、申请科室、使用科室等字段。冗余的好处是列表页展示时不用每次都去join设备表报表查询也会快很多。缺点是更新设备名称后历史单据不会同步改实际上历史单据就该保持成历史快照这个冗余是刻意的。3.2 设备主表与状态字段设备主表是整套系统的心脏。state字段被我设计成单一整数状态而不是用多个布尔字段组合这样最不容易出现状态耦合的混乱局面。CREATE TABLE device ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_code VARCHAR(50) NOT NULL UNIQUE COMMENT 设备编码, device_name VARCHAR(100) NOT NULL COMMENT 设备名称, category_id BIGINT COMMENT 设备分类, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0空闲 1已租出 2已借出 3维修中 4停用 5报废, purchase_date DATE COMMENT 购置日期, price DECIMAL(12,2) COMMENT 购置价格, location VARCHAR(100) COMMENT 存放位置, create_time DATETIME COMMENT 创建时间 ) COMMENT 设备台账表;为什么要维护这个冗余的状态字段因为业务上所有“设备是否可用”的判断都必须基于它。一台设备一旦有人提交了租赁申请并审批通过就必须立刻把status从0改成1否则另一个人还能继续申请。但注意这里的status只是实时快照业务单据内部的审批状态比它精细得多。3.3 租借报修单据的状态链三个单据表各自维护了一张状态链这里我把状态枚举写在注释里方便前后端统一理解lease_order 0 待审批 - 1 审批通过待领取 - 2 使用中 - 3 已归还 - 4 已驳回 - 5 已取消 - 6 逾期未还 borrow_order 0 待审批 - 1 审批通过 - 2 已领用 - 3 已归还 - 4 已驳回 repair_order 0 待派工 - 1 维修中 - 2 待验收 - 3 已完成 - 4 无法修复已报废 - 5 已驳回设计逻辑很清晰业务单状态记录的是流程过程比如租赁单审批中、使用中、已归还设备状态记录的是当前能不能被申请只有“空闲”状态下的设备才能进入下次流转。从报修单“待验收”变成“已完成”时设备状态要根据验收结果改回0空闲或改成5报废。这个联动关系如果不做后续报表和实际库存会完全对不上。4. 核心业务流程实现与关键代码4.1 租赁申请与设备锁定租赁是三个流程里业务逻辑最重的一个因为它涉及费用和押金错一步就会引起经济纠纷。创建租赁单的核心代码大致如下这段逻辑足够回答“为什么设备状态必须在创建订单时就锁住”这个问题Override Transactional(rollbackFor Exception.class) public Long createLeaseOrder(LeaseOrderDTO dto) { // 1. 加悲观锁查询设备防止并发申请 Device device deviceMapper.selectByIdForUpdate(dto.getDeviceId()); if (device null) { throw new BizException(设备不存在); } if (device.getStatus() ! DeviceStatus.FREE) { throw new BizException(设备当前不可租赁); } // 2. 校验时间段是否有重叠 int count leaseOrderMapper.countOverlapping( dto.getDeviceId(), dto.getStartDate(), dto.getEndDate(), Arrays.asList(LeaseOrderStatus.APPROVED, LeaseOrderStatus.IN_USE)); if (count 0) { throw new BizException(该设备在选定时段已被占用); } // 3. 插入租赁单状态为待审批 LeaseOrder order new LeaseOrder(); order.setOrderNo(generateOrderNo(LEASE)); order.setDeviceId(dto.getDeviceId()); order.setDeviceName(device.getDeviceName()); order.setStartDate(dto.getStartDate()); order.setEndDate(dto.getEndDate()); order.setStatus(LeaseOrderStatus.PENDING); leaseOrderMapper.insert(order); // 4. 锁定设备状态为已租出保证后续不会被人重复申请 deviceMapper.updateStatus(dto.getDeviceId(), DeviceStatus.LEASED); return order.getId(); }这个方法使用了Transactional目的是让插入租赁单和更新设备状态在同一个事务里完成任何一步失败都回滚。先查设备是否存在且为空闲再查时间段重叠最后插入订单并锁定设备顺序不能乱。审批通过后设备状态不用再改因为创建订单时已经锁过了驳回或取消时再把设备状态改回空闲。4.2 借用流程与租赁流程的差异借用流程表面上和租赁很像但规则上做了几个关键区别。借用单没有费用字段但设置了预计归还时间设备科管理员在审批时如果认为归还时间不靠谱可以直接驳回。另外借用审批必须要求科室负责人签字环节设备科管理员看到审批记录完整才能通过。还有一个实际业务里很现实的需求紧急借用。有的科室半夜急需设备线上审批链路走不完系统必须支持设备科管理员先做“预出库”生成一条待补审批的借用单第二天再让申请人补齐手续。这个功能在租赁流程里我并不建议开因为租赁涉及费用先租后批容易产生扯皮但借用是内部科室互助灵活性可以更高。想看代码的朋友可以直接参考租赁方法核心逻辑一致只是把费用部分去掉再加一个“紧急借用”的标记字段。4.3 报修闭环与维修派工报修和租借的一个本质区别是租借是计划性流程报修是响应性流程它需要处理不可预知的故障时间和维修周期。报修单创建时必须记录故障描述、报修人、设备所在位置管理员在待派工列表里分配给工程师。设备状态同步变成了维修中即使它之前还挂着借用单也要先把借用单做强制归还处理再发起报修保证链路清晰。维修完成后的验收环节很容易被忽略所以我在系统里把验收单拎出来维修工程师只能提交完工申请真正把设备状态改成空闲的是设备科管理员。核心代码如下Override Transactional(rollbackFor Exception.class) public void completeRepair(Long repairId, String repairResult, BigDecimal cost) { RepairOrder order repairOrderMapper.selectByIdForUpdate(repairId); if (order null || order.getStatus() ! RepairOrderStatus.REPAIRING) { throw new BizException(报修单状态不正确); } order.setStatus(RepairOrderStatus.PENDING_ACCEPT); order.setRepairResult(repairResult); order.setCost(cost); repairOrderMapper.update(order); // 维修记录单独存保留设备故障历史 RepairRecord record new RepairRecord(); record.setRepairId(repairId); record.setRecordType(COMPLETE); record.setContent(repairResult); repairRecordMapper.insert(record); }验收通过后设备状态才恢复为空闲并可以被新的租借借用流程选中验收不通过则退回重新维修确认无法修复的走报废流程设备状态变成5。这个设计保证了业务单状态和设备状态永远在同一套业务流程中联动且每一次状态迁移都有操作日志记录。5. 并发、时间冲突与实施中踩过的坑5.1 时间段重叠判断的正确写法设备租赁最核心的校验就是时间段不能重叠。比如一台监护仪已经租给A科室租期到6月30日那7月1日开始的订单就不应该再被创建。但“边界相等”到底算不算冲突我在需求评审时和科室人员确认过好几次最终统一成左闭右开区间结束那一天设备已经归还可以继续租给别人。按照这个规则正确的重叠查询写成SELECT COUNT(*) FROM lease_order WHERE device_id #{deviceId} AND status IN (1, 2) AND start_date #{endDate} AND end_date #{startDate}这里的思路是新申请的占用区间是[startDate, endDate)只要已有订单的开始时间早于新订单的结束时间并且已有订单的结束时间晚于新订单的开始时间就说明两个区间有重叠。用和而不是和就是为了避免边界日期被误判。实际操作中很多BUG就出在边界判断上写完SQL之后一定要用一组边界用例去测。5.2 并发提交导致的重复租赁时间校验做了并发问题就紧随其后。假设设备科管理员同时收到A和B两个科室申请同一台设备的请求两个事务都执行了count查询都发现没有重叠然后都插入订单设备状态也都被改成已租出最终结果就是同一台设备同一时间段被租给了两个科室。解决这个问题的关键是查询设备时加锁。在创建租借单的第一步我用的是SELECT ... FOR UPDATE悲观锁锁住设备这一行后面的时间重叠校验和订单插入都在这个行锁的保护下串行执行。必须注意的是FOR UPDATE只有在事务内部才有效所以这个方法必须在Service层添加事务注解。如果是集群部署可以再叠加Redis分布式锁但SSM260是单机内网部署数据库行锁已经够用。5.3 MyBatis动态SQL中小于号的坑开发过程中让我记忆最深的一个坑是MyBatis的XML文件里直接写小于号。日期范围查询时我写了类似if teststartDate ! null AND start_date #{startDate}/if的SQL结果项目启动直接报错提示“元素类型必须由匹配的结束标记终止”。这是因为XML解析器把当成了标签开头不能直接出现在文本节点里。排查方法很简单看日志报错定位到具体mapper XML文件后把转义成lt;或者把整段SQL用![CDATA[ ]]包起来。我更喜欢用CDATA方式因为SQL一长串时转义符会降低可读性if testqueryEnd ! null AND start_date lt; #{queryEnd} /if这里一个小提醒CDATA块里的if标签不会生效只包SQL片段不要把MyBatis的动态标签也包进去我之前图省事直接把整个SQL包进去结果所有条件都失效了排查半天。5.4 事务配置了却失效另一个高发问题是事务失效。SSM260的租赁订单创建方法明明加了Transactional但测试时发现订单插入成功设备状态却还是空闲。排查过程我印象很深刻先是检查spring-context.xml的事务管理器发现配置正常再检查service类是否被扫描也正常最后发现问题出在同类内部方法调用上。我在一个Service方法里直接this调用另一个方法而Spring的Transactional是通过AOP代理实现的类内部this调用根本不会走代理注解自然不生效。解决办法是把设备状态更新拆到另一个Service类中或者通过AopContext.currentProxy()调用代理方法。还有一个相关坑是只扫描了class没有配置aop:aspectj-autoproxy proxy-target-classtrue/JDK动态代理和CGLIB代理混用容易出奇怪问题统一强制使用CGLIB并检查扫描路径会稳定很多。5.5 设备状态与单据状态不联动最后一个大坑是设备状态和业务单状态脱节。测试阶段发现审批通过了租赁单设备状态没有变成已租出导致另一个用户又申请这台设备。排查链路是前端提交审批请求 - Controller调用审批方法 - 审批方法执行了lease_order状态更新 - 但返回值一直正常说明问题不是异常回滚。最后定位到审批SQL里只update了lease_order.status没有任何一条语句去更新device.status。这个坑的根源就是我在第3章强调的状态联动设计没有彻底执行。后来我定了一个硬性规定所有涉及设备状态变化的操作必须经由Service服务方法完成禁止在Mapper层直接写零散的更新语句每个Service方法就是一个完整的状态机迁移入口。这个习惯帮我挡住了后面一大半数据不一致的隐患。6. 权限控制、操作日志与交付复盘6.1 基于角色的访问控制实现SSM260没有引入Shiro因为角色和功能点都不复杂自定义拦截器加一套菜单权限就够了。登录拦截器负责校验session没登录一律跳转到login页面。菜单权限按角色查询管理员看到设备台账、借用审批、维修派工等所有菜单科室用户只看到申请入口和本科室的单据列表。按钮级权限我在JSP里做了自定义标签页面上根据当前用户角色决定“审批”“派工”“取消”这些按钮是否渲染。数据权限是在SQL层面强制拼接的普通用户查询租赁列表时Service层从session里取出当前用户的部门ID添加到Mapper查询条件中管理员则允许传空条件查询全部。这里要特别注意数据权限过滤必须在服务端完成不能只靠前端隐藏按钮不然别人拼个URL就能看到全院的费用数据。6.2 操作日志与审计留痕医疗器械设备管理涉及责任追溯所以操作日志不是可选功能而是硬需求。我用Spring AOP做了一个自定义注解OpLog标记在需要记录的操作方法上切面里统一记录操作人、操作时间、模块、动作、目标单据ID和操作结果。日志表设计得比较简单operation_log id, user_id, user_name, module, action, target_id, detail, create_time举个例子设备科管理员驳回了一条租赁申请日志里会记录“张三 在2025-06-10 14:30 驳回了租赁单LEASE20250610001原因是设备维护中”。一开始我觉得AOP切面会有性能开销实测下来这种管理系统的操作频率完全不用担心重点是日志内容和业务逻辑解耦代码维护起来很舒服。6.3 报表指标与后续扩展方向系统上线后设备科最看重的是报表模块刚好可以拿来做后续扩展。目前我实现了设备利用率、维修费用统计、租赁收入统计三类核心指标。设备利用率的计算逻辑是统计某时间段内设备被租借或借用的实际天数除以总天数数据来源是租借单的开始和结束时间。这个指标能直观看出哪些设备配置过剩哪些设备常年超负荷运转。我在实际使用中发现真正好用的设备管理系统不能只做流程录入还要做主动提醒。SSM260现在留了三个扩展点借用到期前三天自动发通知给借用科室报修单超过七天未完成自动升级提醒管理员设备定期保养时间到了生成保养任务工单。这些功能都不复杂但能极大减少设备科的沟通成本。最后再分享一点个人经验SSM260这个项目做完之后我最大的体会是——设备管理系统的核心价值不在代码量而在状态机设计得是否清楚。只要设备状态、单据状态、操作日志这三者始终联动一致后台上那些CRUD逻辑再简单都不会出大乱子。反过来如果状态流转想不清楚用什么框架都是白搭。
返回列表