最近在开发一个基于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缓存归档策略历史数据定期归档这套药品管理系统方案在实际项目中经过验证能够有效管理特殊药品的流转权限确保每笔交易都有据可查。特别是在处理受控药品时严格的权限控制和完整的审计日志为合规性提供了有力保障。