boxmoe_header_banner_img

Hello! 欢迎来到悠悠畅享网!

文章导读

Spring Boot医患关系管理系统:灵活的数据模型与权限设计


avatar
站长 2025年8月16日 4

Spring Boot医患关系管理系统:灵活的数据模型与权限设计

本文深入探讨了在Spring Boot中构建医患关系管理系统的核心挑战,包括复杂的用户角色(医生与患者)、多对多关系以及基于角色的安全认证与授权。通过分析常见的两种数据模型方案,文章推荐了一种结合通用用户实体与特定角色实体的混合设计,并详细阐述了其实现细节,包括实体关系映射、代码示例及Spring Security集成策略,旨在提供一套灵活且可扩展的解决方案。

系统需求与挑战概述

构建一个医患关系管理系统,核心需求包括:

  1. 用户管理: 区分医生和患者两种用户类型。
  2. 关系管理: 医生可以关联多个患者,患者也可以关联多个医生(多对多关系)。
  3. 特定功能: 患者能够添加和管理自己的用药信息。
  4. 安全认证与授权: 实现用户登录注册,并根据用户角色(医生/患者)控制访问权限。

在设计数据模型时,如何有效地表示医生和患者,并处理其特有属性及关系,同时兼顾安全体系的集成,是关键的挑战。

常见数据模型方案探讨

在系统设计初期,通常会考虑以下两种主要的数据模型方案:

方案一:独立实体与多对多关联

这种方案将医生(Doctor)和患者(Patient)分别设计为独立的实体类,并通过 @ManyToMany 注解建立它们之间的关联。

优点: 实体职责明确,各司其职。 缺点:

  • 安全管理复杂: 对于登录和注册功能,需要针对 Doctor 和 Patient 分别实现用户认证逻辑,可能导致代码重复或逻辑分散。
  • 公共属性冗余: 医生和患者可能存在姓名、联系方式等公共属性,在两个独立实体中会造成数据冗余。

概念代码示例:

// Doctor.java (简化版) @Entity @Getter @Setter public class Doctor {     @Id @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;     private String name;     private String surname;      @ManyToMany     @JoinTable(name = "doctor_patient",                joinColumns = @JoinColumn(name = "doctor_id"),                inverseJoinColumns = @JoinColumn(name = "patient_id"))     private Set<Patient> patients = new HashSet<>(); }  // Patient.java (简化版) @Entity @Getter @Setter public class Patient {     @Id @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;     private String name;     private String surname;      @OneToMany(mappedBy = "patient", cascade = CascadeType.PERSIST)     private List<Medicine> medicineList = new ArrayList<>();      @ManyToMany(mappedBy = "patients")     private Set<Doctor> doctors = new HashSet<>(); }

方案二:单一用户表与角色区分

此方案将医生和患者抽象为统一的 User 实体,通过一个 roleType 字段来区分其身份。

优点:

  • 统一认证: 简化了用户登录注册流程,所有用户都通过 User 表进行认证。
  • 简化权限管理: 可以基于 roleType 字段直接进行权限控制。

缺点:

  • 数据稀疏: User 实体中会包含所有角色特有的字段(例如,medicineList 仅适用于患者),导致大量 null 值,造成数据稀疏和模型不清晰。
  • 业务逻辑耦合: 在服务层和控制器层,需要通过 roleType 进行大量条件判断,以处理不同角色的特定业务逻辑,增加了代码的复杂性和维护难度。

概念代码示例:

// User.java (简化版) @Entity @Table(name = "MYUSERS") @Getter @Setter public class User {     @Id @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;     private String name;     private String surname;      @Enumerated(EnumType.STRING)     @Column(nullable = false)     private RoleType roleType; // DOCTOR, PATIENT      // 仅针对患者:     @OneToMany(mappedBy = "patient", cascade = CascadeType.PERSIST)     private List<Medicine> medicineList = new ArrayList<>();      // 多对多关系(可能需要自关联或复杂逻辑)     // @ManyToMany with other Users (Doctors/Patients) }

推荐的数据模型设计:混合方案

综合以上两种方案的优缺点,推荐采用一种混合模型:将用户共性属性抽取到独立的 User 实体中,而将医生和患者的特有属性及关系分别定义在 Doctor 和 Patient 实体中。Doctor 和 Patient 实体通过 @OneToOne 关系与 User 实体关联。

核心思想

  • User 实体: 负责存储所有用户的通用信息,如ID、姓名、姓氏、登录凭证(密码、用户名等)以及一个标识用户类型的字段(可选,但推荐用于快速区分)。
  • Doctor 实体: 存储医生特有的信息,并与 User 实体建立一对一关系。它负责管理医生与患者之间的多对多关系。
  • Patient 实体: 存储患者特有的信息,同样与 User 实体建立一对一关系。它负责管理患者的用药信息以及与医生的多对多关系。
  • Medicine 实体: 存储药品信息,并与 Patient 实体建立多对多关系。

这种设计既实现了统一的用户认证,又保持了特定角色实体的清晰职责,避免了数据稀疏和业务逻辑的过度耦合。

详细实体类实现

1. User 实体: 基础用户信息,用于认证。

package com.example.model;  import lombok.Getter; import lombok.Setter; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor;  import javax.persistence.*;  @Entity @Table(name = "users") // 推荐使用更通用的表名 @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class User {      @Id     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;      @Column(unique = true, nullable = false)     private String username; // 用于登录的用户名或邮箱      @Column(nullable = false)     private String password; // 加密后的密码      private String name;     private String surname;      @Enumerated(EnumType.STRING)     @Column(nullable = false)     private UserType userType; // DOCTOR, PATIENT      // 可以添加其他通用字段,如电话、地址等 }

2. UserType 枚举: 定义用户类型。

package com.example.model;  public enum UserType {     DOCTOR,     PATIENT }

3. Doctor 实体: 医生特有信息及关系。

package com.example.model;  import lombok.Getter; import lombok.Setter; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor;  import javax.persistence.*; import java.util.HashSet; import java.util.Set;  @Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class Doctor {      @Id     private Long id; // 与User的ID共享      @OneToOne     @MapsId // 表示此实体的主键也是其关联User实体的主键     @JoinColumn(name = "id", nullable = false) // 外键列名为id,指向User的id     private User user;      // 医生特有属性,例如:专业领域、执业证书编号等     private String specialty;      @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) // 医生和患者的多对多关系     @JoinTable(         name = "doctor_patients", // 关联表名         joinColumns = @JoinColumn(name = "doctor_id"), // 本实体在关联表中的外键         inverseJoinColumns = @JoinColumn(name = "patient_id") // 对方实体在关联表中的外键     )     private Set<Patient> patients = new HashSet<>();      public void addPatient(Patient patient) {         this.patients.add(patient);         patient.getDoctors().add(this); // 维护双向关系     }      public void removePatient(Patient patient) {         this.patients.remove(patient);         patient.getDoctors().remove(this); // 维护双向关系     } }

4. Patient 实体: 患者特有信息及关系。

package com.example.model;  import lombok.Getter; import lombok.Setter; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor;  import javax.persistence.*; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set;  @Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class Patient {      @Id     private Long id; // 与User的ID共享      @OneToOne     @MapsId // 表示此实体的主键也是其关联User实体的主键     @JoinColumn(name = "id", nullable = false) // 外键列名为id,指向User的id     private User user;      // 患者特有属性,例如:病史、过敏信息等     private String medicalHistory;      @OneToMany(mappedBy = "patient", cascade = CascadeType.ALL, orphanRemoval = true) // 患者与药品清单的一对多关系     private List<Medicine> medicineList = new ArrayList<>();      @ManyToMany(mappedBy = "patients", fetch = FetchType.LAZY) // 患者与医生的多对多关系,由Doctor维护     private Set<Doctor> doctors = new HashSet<>();      public void addMedicine(Medicine medicine) {         this.medicineList.add(medicine);         medicine.setPatient(this); // 维护双向关系     }      public void removeMedicine(Medicine medicine) {         this.medicineList.remove(medicine);         medicine.setPatient(null); // 维护双向关系     } }

5. Medicine 实体: 药品信息。

package com.example.model;  import lombok.Getter; import lombok.Setter; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor;  import javax.persistence.*;  @Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class Medicine {      @Id     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;     private String name;     private String dosage; // 剂量     private String frequency; // 频率      @ManyToOne(fetch = FetchType.LAZY) // 药品与患者的多对一关系     @JoinColumn(name = "patient_id", nullable = false)     private Patient patient; }

注意:

  • @MapsId 注解使得 Doctor 和 Patient 的主键与其关联的 User 实体的主键相同,实现了共享主键的 @OneToOne 关系。
  • @JoinTable 用于定义 Doctor 和 Patient 之间的多对多关系。
  • cascade 类型需要根据业务需求谨慎选择,CascadeType.ALL 表示所有持久化操作(保存、更新、删除等)都会级联到关联实体。

安全与权限管理

在 Spring Boot 中,结合 Spring Security 可以轻松实现基于角色的认证与授权。

1. 用户认证

  • UserDetailsService 实现: 创建一个自定义的 UserDetailsService,用于从 User 实体中加载用户信息。

    package com.example.security;  import com.example.model.User; import com.example.repository.UserRepository; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service;  import java.util.Collections; import java.util.List;  @Service public class CustomUserDetailsService implements UserDetailsService {      private final UserRepository userRepository;      public CustomUserDetailsService(UserRepository userRepository) {         this.userRepository = userRepository;     }      @Override     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {         User user = userRepository.findByUsername(username)                 .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));          List<GrantedAuthority> authorities = Collections.singletonList(             new SimpleGrantedAuthority("ROLE_" + user.getUserType().name()) // 将UserType映射为Spring Security的角色         );          return new org.springframework.security.core.userdetails.User(                 user.getUsername(),                 user.getPassword(),                 authorities         );     } }
  • UserRepository 接口:

    package com.example.repository;  import com.example.model.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional;  public interface UserRepository extends JpaRepository<User, Long> {     Optional<User> findByUsername(String username); }

2. 权限授权

  • Spring Security 配置: 在 WebSecurityConfig 中配置 URL 访问权限。

    package com.example.security;  import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;  @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {      private final CustomUserDetailsService customUserDetailsService;      public WebSecurityConfig(CustomUserDetailsService customUserDetailsService) {         this.customUserDetailsService = customUserDetailsService;     }      @Override     protected void configure(AuthenticationManagerBuilder auth) throws Exception {         auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());     }      @Override     protected void configure(HttpSecurity http) throws Exception {         http             .csrf().disable() // 实际生产环境应启用CSRF防护             .authorizeRequests()                 .antMatchers("/register", "/login").permitAll() // 注册和登录接口允许所有人访问                 .antMatchers("/api/doctors/**").hasRole("DOCTOR") // 医生相关接口仅限DOCTOR角色访问                 .antMatchers("/api/patients/**").hasRole("PATIENT") // 患者相关接口仅限PATIENT角色访问                 .antMatchers("/api/medicines/**").hasRole("PATIENT") // 药品相关接口仅限PATIENT角色访问                 .anyRequest().authenticated() // 其他所有请求需要认证             .and()             .formLogin() // 或.httpBasic()             .and()             .logout();     }      @Bean     public PasswordEncoder passwordEncoder() {         return new BCryptPasswordEncoder();     } }
  • 控制器层授权: 在控制器或服务层方法上使用 @PreAuthorize 注解进行更细粒度的控制。

     package com.example.controller;  import com.example.model.User; import com.example.model.UserType; import com.example.repository.UserRepository; import com.example.service.DoctorService; import com.example.service.PatientService; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*;  @RestController @RequestMapping("/api") public class MyController {      private final UserRepository userRepository;     private final DoctorService doctorService;     private final PatientService patientService;      public MyController(UserRepository userRepository, DoctorService doctorService, PatientService patientService) {         this.userRepository = userRepository;         this.doctorService = doctorService;         this.patientService = patientService;     }      // 获取当前登录用户ID和类型     private User getCurrentUser() {         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();         String username = authentication.getName();



评论(已关闭)

评论已关闭