最近在开发一个基于Spring Boot的医疗物资管理系统时遇到了一个很有意思的业务场景如何处理稀缺药品的分配与交易记录这让我想起了药品管理中的权限控制问题特别是像盘尼西林这类特殊药品的流转监控。本文将完整分享基于Spring Security的药品管理系统实战方案从权限设计到交易审计的全流程实现。1. 药品管理系统背景与需求分析1.1 业务场景说明在现代医疗物资管理中特殊药品如抗生素、麻醉药品等需要严格的权限控制和交易记录。系统需要确保只有授权人员才能进行药品的出入库操作并且每笔交易都要有完整的审计轨迹。1.2 技术选型考虑选择Spring Security作为权限框架的主要原因在于其成熟的认证授权机制和灵活的扩展性。结合Spring Boot可以快速搭建一套完整的药品管理系统满足以下核心需求多角色权限管理管理员、药剂师、医生等药品库存的精确控制交易记录的可追溯性操作日志的完整记录2. 环境准备与项目搭建2.1 开发环境要求JDK 1.8或更高版本Maven 3.6Spring Boot 2.7.xMySQL 8.0或H2数据库测试用IDE推荐IntelliJ IDEA或Eclipse2.2 项目初始化创建Spring Boot项目时选择以下依赖Spring WebSpring SecuritySpring Data JPAMySQL DriverThymeleaf可选用于前端界面!-- pom.xml 核心依赖 -- dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies2.3 数据库设计创建药品管理核心表结构-- 药品信息表 CREATE TABLE medicine ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL COMMENT 药品名称, batch_number VARCHAR(50) COMMENT 批号, quantity INT NOT NULL COMMENT 库存数量, unit_price DECIMAL(10,2) COMMENT 单价, is_controlled BOOLEAN DEFAULT FALSE COMMENT 是否受控药品, created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 用户角色表 CREATE TABLE user_role ( id BIGINT AUTO_INCREMENT PRIMARY KEY, role_name VARCHAR(50) NOT NULL UNIQUE COMMENT 角色名称, description VARCHAR(200) COMMENT 角色描述 ); -- 药品交易记录表 CREATE TABLE transaction_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, medicine_id BIGINT NOT NULL, user_id BIGINT NOT NULL, transaction_type ENUM(IN,OUT) NOT NULL COMMENT 交易类型, quantity INT NOT NULL COMMENT 交易数量, transaction_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(500) COMMENT 备注信息, FOREIGN KEY (medicine_id) REFERENCES medicine(id) );3. Spring Security核心配置3.1 安全配置类实现创建自定义的安全配置类定义权限规则和认证流程Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Autowired private UserDetailsService userDetailsService; Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(/admin/**).hasRole(ADMIN) .antMatchers(/medicine/controlled/**).hasAnyRole(ADMIN, PHARMACIST) .antMatchers(/medicine/**).authenticated() .antMatchers(/transaction/**).authenticated() .anyRequest().permitAll() .and() .formLogin() .loginPage(/login) .defaultSuccessUrl(/dashboard) .and() .logout() .logoutSuccessUrl(/login) .and() .csrf().disable(); } Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } }3.2 自定义用户详情服务实现基于数据库的用户认证服务Service public class CustomUserDetailsService implements UserDetailsService { Autowired private UserRepository userRepository; Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user userRepository.findByUsername(username) .orElseThrow(() - new UsernameNotFoundException(用户不存在: username)); return org.springframework.security.core.userdetails.User.builder() .username(user.getUsername()) .password(user.getPassword()) .roles(user.getRoles().stream() .map(Role::getName) .toArray(String[]::new)) .build(); } }4. 药品管理核心功能实现4.1 药品实体类设计使用JPA实现药品信息的持久化Entity Table(name medicine) public class Medicine { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false, length 100) private String name; private String batchNumber; Column(nullable false) private Integer quantity; private BigDecimal unitPrice; Column(name is_controlled) private Boolean controlled false; CreationTimestamp private Timestamp createdTime; // 省略getter/setter和构造方法 }4.2 受控药品交易服务实现带有权限检查的药品交易服务Service Transactional public class MedicineTransactionService { Autowired private MedicineRepository medicineRepository; Autowired private TransactionRecordRepository transactionRepository; PreAuthorize(hasAnyRole(ADMIN, PHARMACIST)) public TransactionRecord controlledMedicineOut(Long medicineId, Integer quantity, String remark, String currentUsername) { Medicine medicine medicineRepository.findById(medicineId) .orElseThrow(() - new RuntimeException(药品不存在)); if (!medicine.getControlled()) { throw new RuntimeException(非受控药品无需特殊审批); } if (medicine.getQuantity() quantity) { throw new RuntimeException(库存不足); } // 更新库存 medicine.setQuantity(medicine.getQuantity() - quantity); medicineRepository.save(medicine); // 记录交易 TransactionRecord record new TransactionRecord(); record.setMedicine(medicine); record.setQuantity(quantity); record.setTransactionType(TransactionType.OUT); record.setRemark(受控药品出库: remark); record.setTransactionTime(new Timestamp(System.currentTimeMillis())); return transactionRepository.save(record); } }4.3 交易记录查询接口提供带权限过滤的交易记录查询RestController RequestMapping(/api/transaction) public class TransactionController { Autowired private TransactionService transactionService; GetMapping(/history) PreAuthorize(isAuthenticated()) public ResponseEntityListTransactionRecord getTransactionHistory( RequestParam(required false) Long medicineId, RequestParam(required false) String startDate, RequestParam(required false) String endDate) { ListTransactionRecord records transactionService .getFilteredTransactions(medicineId, startDate, endDate); return ResponseEntity.ok(records); } }5. 权限控制进阶实现5.1 方法级权限控制使用Spring Security注解实现精细化的方法级权限控制Service public class AdvancedMedicineService { PreAuthorize(hasRole(ADMIN) or (#medicine.controlled false and hasRole(PHARMACIST))) public void updateMedicineInfo(Medicine medicine, String updatedBy) { // 只有管理员可以修改受控药品信息 // 药剂师只能修改非受控药品 medicineRepository.save(medicine); // 记录操作日志 auditService.logOperation(updatedBy, UPDATE_MEDICINE, 修改药品信息: medicine.getName()); } PostAuthorize(returnObject.controlled false or hasRole(ADMIN)) public Medicine getMedicineDetails(Long id) { // 非管理员只能查看非受控药品详情 return medicineRepository.findById(id).orElse(null); } }5.2 自定义权限注解创建业务相关的自定义权限注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) PreAuthorize(hasRole(ADMIN) or medicinePermissionChecker.canAccess(authentication, #medicineId)) public interface CanAccessMedicine { } // 权限检查器实现 Component(medicinePermissionChecker) public class MedicinePermissionChecker { public boolean canAccess(Authentication authentication, Long medicineId) { // 复杂的业务权限逻辑 return true; // 简化示例 } }6. 审计日志与监控6.1 操作审计实现记录所有关键操作的审计日志Aspect Component public class AuditAspect { Autowired private AuditLogRepository auditLogRepository; AfterReturning(pointcut annotation(auditable), returning result) public void logAuditEvent(JoinPoint joinPoint, Auditable auditable, Object result) { String operation auditable.operation(); String operator SecurityContextHolder.getContext() .getAuthentication().getName(); AuditLog log new AuditLog(); log.setOperation(operation); log.setOperator(operator); log.setOperationTime(new Timestamp(System.currentTimeMillis())); log.setDetails(buildDetails(joinPoint, result)); auditLogRepository.save(log); } }6.2 库存监控告警实现库存预警机制Service Scheduled(fixedRate 300000) // 5分钟检查一次 public class InventoryMonitor { Autowired private MedicineRepository medicineRepository; Autowired private NotificationService notificationService; public void checkLowInventory() { ListMedicine lowStockMedicines medicineRepository .findByQuantityLessThanEqual(10); // 库存低于10预警 lowStockMedicines.forEach(medicine - { notificationService.sendAlert( 药品库存预警: medicine.getName() 当前库存: medicine.getQuantity() ); }); } }7. 前端界面集成7.1 Thymeleaf安全集成在前端模板中集成权限控制!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org xmlns:sechttp://www.thymeleaf.org/extras/spring-security head title药品管理系统/title /head body div sec:authorizehasRole(ADMIN) a href/admin/dashboard管理员面板/a /div div sec:authorizehasAnyRole(ADMIN,PHARMACIST) a href/medicine/controlled受控药品管理/a /div div sec:authorizeisAuthenticated() span sec:authenticationname用户名/span a th:href{/logout}退出/a /div /body /html7.2 药品交易界面实现药品交易的前端交互// 药品交易前端逻辑 function handleMedicineTransaction(medicineId, quantity, transactionType) { const requestData { medicineId: medicineId, quantity: quantity, remark: $(#remark).val() }; $.ajax({ url: /api/transaction/ transactionType, type: POST, contentType: application/json, data: JSON.stringify(requestData), success: function(response) { showSuccess(交易成功); updateInventoryDisplay(); }, error: function(xhr) { showError(交易失败: xhr.responseJSON.message); } }); }8. 测试与验证8.1 单元测试编写编写权限相关的单元测试SpringBootTest AutoConfigureTestDatabase class MedicineServiceTest { Autowired private MedicineService medicineService; Autowired private TestEntityManager entityManager; Test WithMockUser(roles PHARMACIST) void testPharmacistCanAccessNonControlledMedicine() { Medicine medicine new Medicine(); medicine.setName(普通药品); medicine.setControlled(false); medicine.setQuantity(100); entityManager.persist(medicine); Medicine result medicineService.getMedicineDetails(medicine.getId()); assertNotNull(result); } Test WithMockUser(roles PHARMACIST) void testPharmacistCannotAccessControlledMedicine() { Medicine medicine new Medicine(); medicine.setName(盘尼西林); medicine.setControlled(true); medicine.setQuantity(10); entityManager.persist(medicine); assertThrows(AccessDeniedException.class, () - { medicineService.getMedicineDetails(medicine.getId()); }); } }8.2 集成测试配置配置完整的集成测试环境TestConfiguration public class TestSecurityConfig { Bean Primary public UserDetailsService userDetailsService() { UserDetails user User.withUsername(testuser) .password(password) .roles(PHARMACIST) .build(); return new InMemoryUserDetailsManager(user); } }9. 常见问题与解决方案9.1 权限配置不生效问题现象配置了权限规则但实际访问时没有限制。排查步骤检查SecurityConfig配置顺序验证注解是否启用EnableGlobalMethodSecurity检查角色前缀配置解决方案Configuration EnableGlobalMethodSecurity(prePostEnabled true) // 确保启用注解 public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { }9.2 事务记录不完整问题现象药品交易后库存更新了但交易记录缺失。可能原因事务管理配置不当异常处理导致回滚解决方案Service Transactional public class MedicineTransactionService { Transactional(rollbackFor Exception.class) public TransactionRecord processTransaction(TransactionRequest request) { try { // 库存更新 updateInventory(request); // 记录交易 return recordTransaction(request); } catch (Exception e) { // 记录失败日志 log.error(交易处理失败, e); throw e; } } }9.3 性能优化建议对于大数据量的交易记录查询建议添加数据库索引CREATE INDEX idx_transaction_medicine_time ON transaction_record(medicine_id, transaction_time);分页查询优化Query(SELECT t FROM TransactionRecord t WHERE t.medicine.id :medicineId ORDER BY t.transactionTime DESC) PageTransactionRecord findByMedicineId(Param(medicineId) Long medicineId, Pageable pageable);10. 生产环境部署建议10.1 安全加固措施密码策略强制使用强密码定期更换会话管理设置合理的会话超时时间HTTPS强制生产环境启用HTTPSAPI限流防止恶意请求10.2 监控与告警健康检查集成Spring Boot Actuator日志收集使用ELK栈进行日志分析性能监控集成Prometheus和Grafana业务监控关键业务指标监控如库存预警10.3 数据库优化读写分离主从复制配置连接池优化合理配置连接数缓存策略常用数据Redis缓存归档策略历史数据定期归档这套药品管理系统方案在实际项目中经过验证能够有效管理特殊药品的流转权限确保每笔交易都有据可查。特别是在处理受控药品时严格的权限控制和完整的审计日志为合规性提供了有力保障。
企业数字化 ERP 产品动态
相关推荐
Python办公自动化实战:Excel/Word/PDF高效处理 1. 为什么Python是办公自动化的首选? 作为一个每天要处理上百份Excel报表的财务分析师,三年前我第一次接触Python时,完全没想到这个工具会彻底改变我的工作方式。当时我正被月末结账折磨得焦头烂额——手动核对20多个部门的费用明细ÿ… · 2026/11/7 13:48:03
专业开发者必备:Tacent View图像查看器的终极指南 专业开发者必备:Tacent View图像查看器的终极指南 【免费下载链接】tacentview An image and texture viewer for tga, png, apng, exr, dds, pvr, ktx, ktx2, astc, pkm, qoi, gif, hdr, jpg, tif, ico, webp, and bmp files. Uses Dear ImGui, OpenGL, and Tacent… · 2026/11/7 21:43:13
【AI面试通关秘籍】:ChatGPT实战训练7大高频题型,3天提升应答专业度92% 更多请点击:
https://codechina.net
第一章:AI面试趋势洞察与ChatGPT备战价值定位 近年来,AI驱动的面试工具正快速渗透招聘全流程——从简历初筛、语音/视频行为分析,到基于大模型的实时技术问答与情境模拟。据2024年LinkedIn Ta… · 2026/11/8 17:55:22
萍乡电工证到哪里考取,外省证能转过来吗? 萍乡电工证到哪里考取,外省证能转过来吗? 之前考的证在外省能不能转过来?这几乎是每个打算在萍乡从事电工工作的人都会问的问题。别急,今天就来聊聊【萍乡电工证到哪里考取】这个问题,告诉你 在哪考 、怎么考、还能不能把外省证转过来。… · 2026/11/15 22:38:21
电工证多久能学别踩坑,工地太忙根本没时间复习考试 电工证多久能学别踩坑,工地太忙根本没时间复习考试 工地太忙根本没时间复习考试,这几乎是每个一线电工的真实写照。尤其是像洛阳涧西区这些中小型施工企业,项目紧、任务重,很多人一边干着活,一边还得抽空准备电工证考试,想想就头大。别以为电工证只是个… · 2026/11/15 22:38:21
消控证和电工证哪个难考试真题与备考技巧多少钱 消控证和电工证哪个难考试真题与备考技巧多少钱 实操考试心里没底怕挂科,特别是第一次报考特种作业证的朋友,常常会纠结: 消控证和电工证哪个难… · 2026/11/15 22:38:21
中山考电工证在哪考保姆级教程:工地太忙根本没时间复习考试?教你高效通关 中山考电工证在哪考保姆级教程:工地太忙根本没时间复习考试?教你高效通关 工地太忙根本没时间复习考试,这几乎是每个特种作业人员的共同痛点。尤其是电工作业,考试内容多、范围广,一不小心就容易挂科。今天我就从三门峡转岗过来的电工朋友角度,给你一套… · 2026/11/15 22:38:21
佛山电工证在哪里换证全国通用吗 佛山电工证在哪里换证全国通用吗 证书过期了不知道怎么复审延期,这种焦虑你是不是也经历过?尤其是像你这样在郑州金水区从事房建工程的电工,证书是工作的“通行证”,一旦过期,不仅影响项目进度,还可能面临停工、罚款甚至法律风险。现在问题来了:… · 2026/11/15 22:37:20
长春初级电工证怎么考?通过率揭秘全在这里 长春初级电工证怎么考?通过率揭秘全在这里 之前考的证在外省能不能转过来?这几乎是每个想从事电工工作的人都会问的问题。特别是那些在外地打拼多年、手里握着一张“外地电工证”的人,到了长春想继续干这一行,第一反应就是“我的证能用吗?”。今天我们就… · 2026/11/15 0:00:50
国内电工证报考条件与考试时间 低压电工证怎么考?学历不高也能报名,官方报名入口全在这 你是不是经常听说“电工证是入行门槛”,但又担心自己学历不够、经验不足,根本报不了名?别急,今天就带你从零了解【国内电工证】的报考流程、考试内容,还有怎么在 官方报名入口… · 2026/11/15 0:00:50
电工作业时如何断电?电子证书到底能不能全国通用? 电工作业时如何断电?电子证书到底能不能全国通用? 实操考试心里没底怕挂科,特别是电工作业时如何断电这一操作,不仅关系到你能不能拿到证书,更关系到以后能不能顺利上岗。很多人觉得这不过就是“断个电”,但其实这里面的门道可不少,稍有不慎就可能出错… · 2026/11/15 0:00:50
长春初级电工证怎么考?通过率揭秘全在这里 长春初级电工证怎么考?通过率揭秘全在这里 之前考的证在外省能不能转过来?这几乎是每个想从事电工工作的人都会问的问题。特别是那些在外地打拼多年、手里握着一张“外地电工证”的人,到了长春想继续干这一行,第一反应就是“我的证能用吗?”。今天我们就… · 2026/11/15 0:00:50
国内电工证报考条件与考试时间 低压电工证怎么考?学历不高也能报名,官方报名入口全在这 你是不是经常听说“电工证是入行门槛”,但又担心自己学历不够、经验不足,根本报不了名?别急,今天就带你从零了解【国内电工证】的报考流程、考试内容,还有怎么在 官方报名入口… · 2026/11/15 0:00:50
电工作业时如何断电?电子证书到底能不能全国通用? 电工作业时如何断电?电子证书到底能不能全国通用? 实操考试心里没底怕挂科,特别是电工作业时如何断电这一操作,不仅关系到你能不能拿到证书,更关系到以后能不能顺利上岗。很多人觉得这不过就是“断个电”,但其实这里面的门道可不少,稍有不慎就可能出错… · 2026/11/15 0:00:50
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 1:51:20
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 4:02:19
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/15 3:02:05