最近在开发一个基于Spring Boot的医疗物资管理系统时,遇到了一个很有意思的业务场景:如何处理稀缺药品的分配与交易记录?这让我想起了药品管理中的权限控制问题,特别是像盘尼西林这类特殊药品的流转监控。本文将完整分享基于Spring Security的药品管理系统实战方案,从权限设计到交易审计的全流程实现。

1. 药品管理系统背景与需求分析

1.1 业务场景说明

在现代医疗物资管理中,特殊药品如抗生素、麻醉药品等需要严格的权限控制和交易记录。系统需要确保只有授权人员才能进行药品的出入库操作,并且每笔交易都要有完整的审计轨迹。

1.2 技术选型考虑

选择Spring Security作为权限框架的主要原因在于其成熟的认证授权机制和灵活的扩展性。结合Spring Boot可以快速搭建一套完整的药品管理系统,满足以下核心需求:

  • 多角色权限管理(管理员、药剂师、医生等)
  • 药品库存的精确控制
  • 交易记录的可追溯性
  • 操作日志的完整记录

2. 环境准备与项目搭建

2.1 开发环境要求

  • JDK 1.8或更高版本
  • Maven 3.6+
  • Spring Boot 2.7.x
  • MySQL 8.0或H2数据库(测试用)
  • IDE推荐IntelliJ IDEA或Eclipse

2.2 项目初始化

创建Spring Boot项目时选择以下依赖:

  • Spring Web
  • Spring Security
  • Spring Data JPA
  • MySQL Driver
  • Thymeleaf(可选,用于前端界面)
<!-- pom.xml 核心依赖 -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

2.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 ResponseEntity<List<TransactionRecord>> getTransactionHistory(
            @RequestParam(required = false) Long medicineId,
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate) {
        
        List<TransactionRecord> 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() {
        List<Medicine> lowStockMedicines = medicineRepository
            .findByQuantityLessThanEqual(10); // 库存低于10预警
        
        lowStockMedicines.forEach(medicine -> {
            notificationService.sendAlert(
                "药品库存预警: " + medicine.getName() + 
                " 当前库存: " + medicine.getQuantity()
            );
        });
    }
}

7. 前端界面集成

7.1 Thymeleaf安全集成

在前端模板中集成权限控制:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <title>药品管理系统</title>
</head>
<body>
    <div sec:authorize="hasRole('ADMIN')">
        <a href="/admin/dashboard">管理员面板</a>
    </div>
    
    <div sec:authorize="hasAnyRole('ADMIN','PHARMACIST')">
        <a href="/medicine/controlled">受控药品管理</a>
    </div>
    
    <div sec:authorize="isAuthenticated()">
        <span sec:authentication="name">用户名</span>
        <a th:href="@{/logout}">退出</a>
    </div>
</body>
</html>

7.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 权限配置不生效

问题现象 :配置了权限规则但实际访问时没有限制。

排查步骤

  1. 检查SecurityConfig配置顺序
  2. 验证注解是否启用(@EnableGlobalMethodSecurity)
  3. 检查角色前缀配置

解决方案

@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 性能优化建议

对于大数据量的交易记录查询,建议:

  1. 添加数据库索引:
CREATE INDEX idx_transaction_medicine_time ON transaction_record(medicine_id, transaction_time);
  1. 分页查询优化:
@Query("SELECT t FROM TransactionRecord t WHERE t.medicine.id = :medicineId " +
       "ORDER BY t.transactionTime DESC")
Page<TransactionRecord> findByMedicineId(@Param("medicineId") Long medicineId, Pageable pageable);

10. 生产环境部署建议

10.1 安全加固措施

  1. 密码策略 :强制使用强密码,定期更换
  2. 会话管理 :设置合理的会话超时时间
  3. HTTPS强制 :生产环境启用HTTPS
  4. API限流 :防止恶意请求

10.2 监控与告警

  1. 健康检查 :集成Spring Boot Actuator
  2. 日志收集 :使用ELK栈进行日志分析
  3. 性能监控 :集成Prometheus和Grafana
  4. 业务监控 :关键业务指标监控(如库存预警)

10.3 数据库优化

  1. 读写分离 :主从复制配置
  2. 连接池优化 :合理配置连接数
  3. 缓存策略 :常用数据Redis缓存
  4. 归档策略 :历史数据定期归档

这套药品管理系统方案在实际项目中经过验证,能够有效管理特殊药品的流转权限,确保每笔交易都有据可查。特别是在处理受控药品时,严格的权限控制和完整的审计日志为合规性提供了有力保障。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐