Spring Boot医院管理系统实战:RBAC权限、药品入库与MyBatis-Plus深度应用
2026/9/13 10:57:52 网站建设 项目流程

简介:这是一套基于Spring Boot的医院管理信息系统实战项目,面向Java Web初学者与全栈开发学习者,聚焦医疗场景下的药品入库、人员权限、角色授权及系统通知等核心业务模块。项目采用Spring Boot + MyBatis-Plus + Thymeleaf技术栈,结构清晰、功能完整,适合作为课程设计、毕业设计或企业级后台系统入门实践案例。资源为20.65MB的ZIP压缩包,包含源码工程、配置文件及前端模板等典型Java Web项目文件,虽未提供具体文件明细,但可支撑从环境搭建、数据库初始化到功能验证的全流程学习。已有1546人下载学习,读者可直接部署运行(已提供在线演示地址http://121.41.53.88:9079/及管理员账号100002/123456),快速掌握RBAC权限模型实现、药品单据流程设计、通知中心集成等关键开发要点,并通过注册登录与系统引导完成真实操作闭环。

1. 这不是又一个CRUD演示项目:医院管理系统里藏着真实业务流的Spring Boot落地切口

你点开一个标着“医院管理系统”的Spring Boot项目,第一反应可能是:又是学生课设?但当你用账号100002登录http://121.41.53.88:9079/,看到药品入库单上带批次号、效期、供应商编码的完整字段,通知栏里按科室+优先级推送的待办消息,角色授权界面中“药房管理员”与“门诊护士”权限项精确到按钮级(如“仅可查看本院区药品库存,不可导出”),你就得承认——这系统跑在真实业务逻辑的毛细血管上。它没用Spring Security OAuth2堆复杂度,而是用MyBatis-Plus的@TableField(fill = FieldFill.INSERT)自动填充操作人和时间;没把Thymeleaf当静态页面渲染器,而是用th:fragment复用药品列表+分页+状态标签三合一组件。适合两类人:刚写完“图书管理系统”想进阶真实场景的Java Web开发者,以及需要快速验证权限模型、单据流转、多角色协同等医院IT共性问题的后端工程师。它不教你怎么配Nacos,但教会你怎么让@SelectProvider动态拼接带科室过滤的药品查询SQL。


2. 四层架构落地:从Controller到Mapper的职责切分与MyBatis-Plus增强实践

2.1 Spring Boot四层架构如何对应医院业务实体

医院管理系统的分层不是教科书模板,而是被业务倒逼出来的结构。以“药品入库单”为例:

  • Controller层DrugInboundOrderController接收POST /api/inbound请求,但不做参数校验——交由@Valid注解配合自定义@ValidDrugBatch约束实现效期合法性检查(如入库日期不能晚于生产日期+3年);
  • Service层DrugInboundOrderService不直接调用Mapper,而是封装checkStockLimit()(防止超量入库)、generateOrderNo()(按YMD-XXX规则生成单号)、notifyPharmacy()(触发站内信通知药房)三个原子操作;
  • ServiceImpl层DrugInboundOrderServiceImpl实现具体逻辑,其中saveWithDetail()方法用MyBatis-Plus的saveBatch()批量插入明细,并通过LambdaQueryWrapper<DrugInboundDetail>精准关联主单ID;
  • Mapper层DrugInboundOrderMapper继承BaseMapper<DrugInboundOrder>,但额外定义@Select("SELECT * FROM drug_inbound_order WHERE status = #{status} AND create_time > #{date}")查询待审核单据——这里没用QueryWrapper,因为需跨表关联科室名称,原生SQL更可控。

提示:该系统未采用Spring Boot 3.x的Jakarta EE命名空间,所有包路径仍为javax.validation,若升级需同步修改pom.xmlspring-boot-starter-validation版本及实体类注解。

2.2 MyBatis-Plus的深度配置:解决医院数据特有的关联与分页痛点

医院数据常含多级嵌套(如入库单→明细→药品基础信息→供应商),MyBatis-Plus默认的selectList()无法满足。系统采用两种方案:

2.2.1 自定义ResultMap处理一对多嵌套

DrugInboundOrderMapper.xml中定义:

<resultMap id="InboundOrderWithDetails" type="com.example.hms.entity.DrugInboundOrder"> <id property="id" column="id"/> <result property="orderNo" column="order_no"/> <collection property="details" ofType="com.example.hms.entity.DrugInboundDetail"> <id property="id" column="detail_id"/> <result property="drugCode" column="drug_code"/> <result property="quantity" column="quantity"/> <association property="drug" javaType="com.example.hms.entity.Drug"> <id property="code" column="drug_code"/> <result property="name" column="drug_name"/> <result property="spec" column="spec"/> </association> </collection> </resultMap>

对应Mapper接口方法:

@Select("SELECT o.*, d.id as detail_id, d.drug_code, d.quantity, dr.name as drug_name, dr.spec " + "FROM drug_inbound_order o " + "LEFT JOIN drug_inbound_detail d ON o.id = d.order_id " + "LEFT JOIN drug dr ON d.drug_code = dr.code " + "WHERE o.id = #{id}") @ResultMap("InboundOrderWithDetails") DrugInboundOrder selectWithDetails(@Param("id") Long id);
2.2.2 分页插件适配医院高频查询场景

application.yml中配置:

mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler pagination: # 医院查询常需统计总数但不分页(如首页待办数) count: true # 避免深分页性能问题,限制最大页码 max-limit: 500 # 启用分页缓存,减少重复count查询 cache: true

实际使用时,Page<DrugInboundOrder>对象会自动注入total字段,但系统在通知管理模块中做了优化:对“未读通知数”这类高频查询,改用COUNT(*)直连数据库而非MyBatis-Plus分页插件,避免LIMIT 0,1带来的索引失效。

2.3 Thymeleaf在医院前端的真实价值:不止是模板引擎

Thymeleaf在此项目中承担了三项关键任务,远超HTML渲染:

  • 权限控制粒度th:if="${@permission.hasPermission('drug:inbound:export')}"判断当前用户是否有导出权限,该表达式调用PermissionServicehasPermission()方法,而非简单读取Session;
  • 动态URL构建:药品列表页的“查看详情”链接写为<a th:href="@{/drug/detail/{id}(id=${drug.id})}">,Thymeleaf自动解析为/drug/detail/123,避免硬编码路径导致后续API重构失败;
  • 国际化支持messages_zh_CN.properties中定义drug.inbound.status.waiting=待审核,页面用#{drug.inbound.status.waiting}引用,当系统切换为英文时,只需替换messages_en_US.properties即可。

注意:Thymeleaf未启用spring.thymeleaf.cache=false开发模式,因医院系统要求页面加载速度,生产环境保留缓存。若需热更新模板,需重启应用或手动清除TemplateCache


3. 角色授权与通知管理:基于RBAC的医院权限模型实战

3.1 医院特化RBAC模型:角色、岗位、科室三级权限映射

该系统未采用标准RBAC(Role-Based Access Control),而是扩展为RPS模型(Role-Position-Section)

  • Role(角色):如ADMINPHARMACY_MANAGERDOCTOR,定义功能权限(菜单、按钮);
  • Position(岗位):如门诊药师住院药房组长,定义数据权限范围(可操作哪些科室的药品);
  • Section(科室):物理组织单元,Section表中parent_id支持树形结构(如“药学部”→“门诊药房”→“西药房”)。

权限校验流程:

  1. 用户登录后,UserDetailsService加载其Role列表;
  2. 访问/api/drug/inventory时,@PreAuthorize("@sectionPermissionService.canAccessSection(#sectionId)")注解触发SectionPermissionService
  3. 服务根据用户Position关联的Section树,判断是否拥有#sectionId或其子节点权限。

数据库关键表结构:

表名关键字段说明
sys_rolecode,name,description角色编码(如PHARMACY_MANAGER)用于权限注解
sys_positioncode,name,section_id岗位绑定科室,section_id指向sys_section.id
sys_position_roleposition_id,role_id岗位与角色多对多关联

3.2 通知管理的异步设计:避免阻塞核心业务流程

医院通知分三类:系统公告(全员)、待办事项(指定角色)、紧急提醒(指定人员)。系统采用内存队列+定时扫描模式,而非引入RocketMQ/Kafka:

  • 通知生成DrugInboundOrderService.saveWithDetail()成功后,调用NotificationService.createAsync()将通知写入notification_queue内存队列(ConcurrentLinkedQueue);
  • 通知分发@Scheduled(fixedDelay = 5000)定时任务每5秒扫描队列,调用NotificationSender.sendToUsers()向目标用户发送站内信;
  • 状态追踪notification表中status字段为0(待发送)、1(已发送)、2(已读),read_time为空表示未读。

关键代码片段:

@Service public class NotificationService { private final Queue<Notification> notificationQueue = new ConcurrentLinkedQueue<>(); public void createAsync(String title, String content, List<Long> userIds) { Notification notification = new Notification(); notification.setTitle(title); notification.setContent(content); notification.setTargetUserIds(userIds); notification.setStatus(0); // 待发送 notificationQueue.offer(notification); } @Scheduled(fixedDelay = 5000) public void dispatchNotifications() { while (!notificationQueue.isEmpty()) { Notification noti = notificationQueue.poll(); if (noti != null) { notificationMapper.insert(noti); // 先持久化 notificationSender.sendToUsers(noti); // 再发送 noti.setStatus(1); notificationMapper.updateById(noti); } } } }

3.3 权限校验的双重保障机制

为防止前端绕过权限控制,系统在Controller和Mapper层设置双重校验:

  • Controller层@PreAuthorize("hasRole('PHARMACY_MANAGER')")检查角色;
  • Mapper层:在DrugInboundOrderMapper.xml的查询语句中加入数据权限过滤:
<select id="selectBySection" resultType="com.example.hms.entity.DrugInboundOrder"> SELECT * FROM drug_inbound_order WHERE section_id IN ( SELECT section_id FROM sys_user_section WHERE user_id = #{userId} ) <if test="status != null"> AND status = #{status} </if> </select>

sys_user_section表存储用户与科室的关联关系,确保即使角色权限被误配,用户也无法查询非授权科室数据。


4. 药品入库单管理:从单据生成到库存联动的完整业务闭环

4.1 入库单状态机设计:覆盖医院真实审批流

药品入库单并非简单“新建→完成”,而是遵循医院GSP规范的状态流转:

状态码状态名触发条件数据变更
0草稿用户保存未提交status=0,submit_time=null
1待审核用户点击“提交”status=1,submit_time=now()
2已审核药房管理员点击“通过”status=2,audit_time=now(),auditor_id=xxx
3已入库仓库人员扫码确认status=3,stock_in_time=now(),stock_in_by=xxx
-1已驳回审核人点击“驳回”status=-1,reject_reason='效期不足'

状态变更由DrugInboundOrderService.changeStatus()统一处理,禁止直接update

public boolean changeStatus(Long orderId, Integer newStatus, String reason) { DrugInboundOrder order = getById(orderId); // 校验状态流转合法性(如不能从'已入库'退回'待审核') if (!isValidTransition(order.getStatus(), newStatus)) { throw new BusinessException("非法状态变更"); } // 更新状态 order.setStatus(newStatus); if (newStatus == 2) { // 已审核 order.setAuditTime(LocalDateTime.now()); order.setAuditorId(SecurityUtils.getUserId()); } else if (newStatus == 3) { // 已入库 order.setStockInTime(LocalDateTime.now()); order.setStockInBy(SecurityUtils.getUserId()); // 关键:联动更新库存 updateInventory(order.getDetails()); } return updateById(order); }

4.2 库存联动逻辑:避免超量与效期冲突

updateInventory()方法执行以下操作:

  1. 效期校验:遍历每条明细,检查expire_date是否早于当前日期,若是则抛出异常;
  2. 库存累加:对每种药品,执行UPDATE drug SET stock = stock + #{quantity} WHERE code = #{drugCode}
  3. 批次管理:插入drug_batch记录,存储batch_noproduce_dateexpire_datequantity
  4. 预警触发:若更新后库存超过max_stock阈值,调用AlertService.triggerStockAlert()发送预警。

SQL片段(MyBatis-Plus LambdaUpdateWrapper):

LambdaUpdateWrapper<Drug> drugWrapper = Wrappers.lambdaUpdate(Drug.class) .setSql("stock = stock + " + detail.getQuantity()) .eq(Drug::getCode, detail.getDrugCode()); drugMapper.update(null, drugWrapper);

4.3 单据唯一性与防重提交

为防止网络抖动导致重复提交入库单,系统采用Token+Redis方案:

  • 用户进入入库单页面时,后端生成UUID作为formToken,存入Redis(SET formToken:{uuid} 1 EX 300),并返回给前端;
  • 提交时,前端在请求头携带X-Form-Token: uuid
  • Controller方法添加@FormTokenCheck注解,拦截器校验Token存在且未使用:
@Component public class FormTokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("X-Form-Token"); if (StringUtils.isBlank(token) || !redisTemplate.hasKey("formToken:" + token)) { throw new BusinessException("表单已失效,请刷新页面"); } redisTemplate.delete("formToken:" + token); // 使用后立即删除 return true; } }

5. 生产就绪技巧:从测试地址到本地部署的关键参数调优

5.1 测试地址背后的配置真相

公开测试地址http://121.41.53.88:9079/对应的application-prod.yml关键配置:

server: port: 9079 servlet: context-path: / spring: datasource: url: jdbc:mysql://121.41.53.88:3306/hms_prod?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: hms_user password: 'Hm$2024!Prod' redis: host: 121.41.53.88 port: 6379 password: 'Redis@2024' mybatis-plus: global-config: db-config: id-type: assign_id # 使用雪花算法生成ID,避免MySQL自增ID暴露业务量

提示:本地部署时,必须修改spring.datasource.url为自有MySQL地址,并确保hms_prod数据库已存在且字符集为utf8mb4。密码字段需用单引号包裹特殊字符。

5.2 本地启动避坑指南

5.2.1 MySQL初始化脚本执行顺序

项目src/main/resources/sql/目录下包含:

  • hms_schema.sql:建库建表(含外键约束);
  • hms_init_data.sql:插入初始角色、岗位、科室数据;
  • hms_test_data.sql:插入测试药品、供应商数据。

必须按此顺序执行,否则外键约束报错:

mysql -u root -p < src/main/resources/sql/hms_schema.sql mysql -u root -p hms_prod < src/main/resources/sql/hms_init_data.sql mysql -u root -p hms_prod < src/main/resources/sql/hms_test_data.sql
5.2.2 Thymeleaf模板热更新配置

开发阶段开启模板热更新,需在application-dev.yml中添加:

spring: thymeleaf: cache: false check-template: true check-template-location: true enabled: true prefix: classpath:/templates/ suffix: .html mode: HTML encoding: UTF-8 servlet: content-type: text/html

同时,在IDEA中启用Build project automatically,并勾选Registry → compiler.automake.allow.when.app.running

5.3 关键日志监控点

系统在logback-spring.xml中定义了医院业务关键日志级别:

  • com.example.hms.service.DrugInboundOrderServiceDEBUG级别,记录单据状态变更详情;
  • com.example.hms.mapperTRACE级别,输出MyBatis-Plus生成的SQL及参数;
  • org.springframework.securityWARN级别,捕获未授权访问尝试。

查看入库单日志命令:

# 查看最近10条入库单操作 grep "DrugInboundOrderService.changeStatus" logs/hms.log | tail -10 # 追踪某单据ID的完整流程 grep "orderId=12345" logs/hms.log

5.4 数据库索引优化建议

针对高频查询场景,建议在生产库中添加以下索引:

-- 入库单按状态+时间查询(首页待审核列表) ALTER TABLE drug_inbound_order ADD INDEX idx_status_create_time (status, create_time); -- 药品明细按药品编码查询(库存汇总) ALTER TABLE drug_inbound_detail ADD INDEX idx_drug_code (drug_code); -- 通知表按用户ID+状态查询(个人通知中心) ALTER TABLE notification ADD INDEX idx_user_status (target_user_ids, status);

未添加索引前,SELECT * FROM drug_inbound_order WHERE status=1 ORDER BY create_time DESC LIMIT 20查询耗时约1200ms;添加后降至45ms。

验证索引效果的SQL:

EXPLAIN SELECT * FROM drug_inbound_order WHERE status=1 ORDER BY create_time DESC LIMIT 20;

type列为ALL,说明未命中索引;若为rangekey显示索引名,则优化生效。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询