患者健康档案功能设计文档.md 19 KB

患者健康档案功能设计文档

1. 功能概述

患者健康档案功能旨在为患者提供一个便捷的健康信息录入和管理平台。该功能允许患者填写和更新个人健康档案,包括吸烟饮酒史、疾病诊断、过敏史和家族史等信息,为医生提供全面的患者健康背景。

2. 数据库设计

2.1 患者健康档案表 (t_patient_health_record)

根据项目数据库表设计规范(参见 docs/OLD/DevRule/07-数据库规范.md),创建患者健康档案表:

字段名 类型 描述
id BIGINT(20) 主键ID,使用雪花算法(MyBatis-Plus ASSIGN_ID
patient_user_id BIGINT(20) 患者用户ID,外键
smoking_history VARCHAR(50) 吸烟史 (no-否, occasional-偶尔, frequent-经常)
drinking_history VARCHAR(50) 饮酒史 (no-否, occasional-偶尔, frequent-经常)
diabetes TINYINT(1) 糖尿病 (0-否, 1-是)
hypertension TINYINT(1) 高血压 (0-否, 1-是)
dyslipidemia TINYINT(1) 血脂异常 (0-否, 1-是)
coronary_heart_disease TINYINT(1) 冠心病 (0-否, 1-是)
cerebral_infarction TINYINT(1) 脑梗塞 (0-否, 1-是)
allergy_history TEXT 过敏史
family_history TEXT 家族史
version INT(11) 版本号(乐观锁)
create_user BIGINT(20) 创建者ID
create_time DATETIME 创建时间,默认值CURRENT_TIMESTAMP
update_user BIGINT(20) 更新者ID
update_time DATETIME 更新时间,默认值CURRENT_TIMESTAMP,更新时自动设置为当前时间
remark VARCHAR(255) 备注

遵循的数据库规范要点:

  • 使用 BIGINT 主键并通过雪花算法生成(MyBatis-Plus ASSIGN_ID)。
  • 公共字段(versioncreate_usercreate_timeupdate_userupdate_time)由 BaseEntityCustomMetaObjectHandler 自动填充。
  • 根据需要添加外键约束(注意性能影响)或在应用层校验关联数据完整性。
  • 数据迁移建议使用 Flyway/Liquibase(脚本命名遵循 V{version}__{description}.sql)。

示例建表(参考):

CREATE TABLE `t_patient_health_record` (
  `id` bigint(20) NOT NULL COMMENT '主键ID',
  `patient_user_id` bigint(20) NOT NULL COMMENT '患者用户ID',
  `smoking_history` varchar(50) DEFAULT NULL COMMENT '吸烟史 (no-否, occasional-偶尔, frequent-经常)',
  `drinking_history` varchar(50) DEFAULT NULL COMMENT '饮酒史 (no-否, occasional-偶尔, frequent-经常)',
  `diabetes` tinyint(1) DEFAULT '0' COMMENT '糖尿病 (0-否, 1-是)',
  `hypertension` tinyint(1) DEFAULT '0' COMMENT '高血压 (0-否, 1-是)',
  `dyslipidemia` tinyint(1) DEFAULT '0' COMMENT '血脂异常 (0-否, 1-是)',
  `coronary_heart_disease` tinyint(1) DEFAULT '0' COMMENT '冠心病 (0-否, 1-是)',
  `cerebral_infarction` tinyint(1) DEFAULT '0' COMMENT '脑梗塞 (0-否, 1-是)',
  `allergy_history` text COMMENT '过敏史',
  `family_history` text COMMENT '家族史',
  `version` int(11) DEFAULT '0' COMMENT '版本号(乐观锁)',
  `create_user` bigint(20) DEFAULT NULL COMMENT '创建者ID',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_user` bigint(20) DEFAULT NULL COMMENT '更新者ID',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_patient_user_id` (`patient_user_id`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='患者健康档案表';

备注:对外响应的 id 以字符串形式返回以避免前端 JS 精度丢失(参见 docs/OLD/DevRule/03-API设计规范.md 的长整型 ID 传输策略)。此外,patientUserId 在响应中也使用字符串类型返回,避免前端精度丢失。

3. 实体类设计

3.1 PO实体类 (PatientHealthRecord.java)

package work.baiyun.chronicdiseaseapp.model.po;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Schema(description = "患者健康档案表")
@TableName("t_patient_health_record")
@Data
@EqualsAndHashCode(callSuper = false)
public class PatientHealthRecord extends BaseEntity {
    @Schema(description = "患者用户ID")
    @TableField("patient_user_id")
    private Long patientUserId;

    @Schema(description = "吸烟史")
    @TableField("smoking_history")
    private String smokingHistory;

    @Schema(description = "饮酒史")
    @TableField("drinking_history")
    private String drinkingHistory;

    @Schema(description = "糖尿病")
    @TableField("diabetes")
    private Boolean diabetes;

    @Schema(description = "高血压")
    @TableField("hypertension")
    private Boolean hypertension;

    @Schema(description = "血脂异常")
    @TableField("dyslipidemia")
    private Boolean dyslipidemia;

    @Schema(description = "冠心病")
    @TableField("coronary_heart_disease")
    private Boolean coronaryHeartDisease;

    @Schema(description = "脑梗塞")
    @TableField("cerebral_infarction")
    private Boolean cerebralInfarction;

    @Schema(description = "过敏史")
    @TableField("allergy_history")
    private String allergyHistory;

    @Schema(description = "家族史")
    @TableField("family_history")
    private String familyHistory;
}

3.2 VO对象

3.2.1 请求对象

// CreateOrUpdatePatientHealthRecordRequest.java
package work.baiyun.chronicdiseaseapp.model.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Schema(description = "创建或更新患者健康档案请求")
@Data
public class CreateOrUpdatePatientHealthRecordRequest {
    @Schema(description = "吸烟史")
    private String smokingHistory;

    @Schema(description = "饮酒史")
    private String drinkingHistory;

    @Schema(description = "糖尿病")
    private Boolean diabetes;

    @Schema(description = "高血压")
    private Boolean hypertension;

    @Schema(description = "血脂异常")
    private Boolean dyslipidemia;

    @Schema(description = "冠心病")
    private Boolean coronaryHeartDisease;

    @Schema(description = "脑梗塞")
    private Boolean cerebralInfarction;

    @Schema(description = "过敏史")
    private String allergyHistory;

    @Schema(description = "家族史")
    private String familyHistory;
}

3.2.2 响应对象

// PatientHealthRecordResponse.java
package work.baiyun.chronicdiseaseapp.model.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.time.LocalDateTime;

@Schema(description = "患者健康档案响应")
@Data
public class PatientHealthRecordResponse {
    @Schema(description = "记录ID")
    private String id;

    @Schema(description = "患者用户ID")
    private String patientUserId;

    @Schema(description = "吸烟史")
    private String smokingHistory;

    @Schema(description = "饮酒史")
    private String drinkingHistory;

    @Schema(description = "糖尿病")
    private Boolean diabetes;

    @Schema(description = "高血压")
    private Boolean hypertension;

    @Schema(description = "血脂异常")
    private Boolean dyslipidemia;

    @Schema(description = "冠心病")
    private Boolean coronaryHeartDisease;

    @Schema(description = "脑梗塞")
    private Boolean cerebralInfarction;

    @Schema(description = "过敏史")
    private String allergyHistory;

    @Schema(description = "家族史")
    private String familyHistory;

    @Schema(description = "创建时间")
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    private LocalDateTime updateTime;

    @Schema(description = "患者昵称")
    private String patientNickname;
}

4. Mapper接口设计

// PatientHealthRecordMapper.java
package work.baiyun.chronicdiseaseapp.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import work.baiyun.chronicdiseaseapp.model.po.PatientHealthRecord;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface PatientHealthRecordMapper extends BaseMapper<PatientHealthRecord> {
}

5. Service层设计

5.1 接口定义

// PatientHealthRecordService.java
package work.baiyun.chronicdiseaseapp.service;

import work.baiyun.chronicdiseaseapp.model.vo.PatientHealthRecordResponse;
import work.baiyun.chronicdiseaseapp.model.vo.CreateOrUpdatePatientHealthRecordRequest;

public interface PatientHealthRecordService {
    /**
     * 创建或更新患者健康档案
     */
    void createOrUpdateHealthRecord(CreateOrUpdatePatientHealthRecordRequest request);

    /**
     * 获取当前用户的健康档案
     */
    PatientHealthRecordResponse getCurrentUserHealthRecord();

    /**
     * 医生获取患者健康档案
     */
    PatientHealthRecordResponse getPatientHealthRecord(Long patientUserId);
}

注:`createOrUpdateHealthRecord` 使用 `patient_user_id` 做唯一查重(`uk_patient_user_id`),如果存在则更新,否则插入新记录。该方法不会显式返回新插入 ID;调用方可通过 `getCurrentUserHealthRecord` 获取最新记录。服务端没有对传入字段(如吸烟、饮酒史)做额外的语义校验,若需强校验应在 Service 层补充。

5.2 实现类

// PatientHealthRecordServiceImpl.java
package work.baiyun.chronicdiseaseapp.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import work.baiyun.chronicdiseaseapp.mapper.PatientHealthRecordMapper;
import work.baiyun.chronicdiseaseapp.mapper.UserInfoMapper;
import work.baiyun.chronicdiseaseapp.model.po.PatientHealthRecord;
import work.baiyun.chronicdiseaseapp.model.po.UserInfo;
import work.baiyun.chronicdiseaseapp.model.vo.PatientHealthRecordResponse;
import work.baiyun.chronicdiseaseapp.model.vo.CreateOrUpdatePatientHealthRecordRequest;
import work.baiyun.chronicdiseaseapp.service.PatientHealthRecordService;
import work.baiyun.chronicdiseaseapp.util.SecurityUtils;

@Service
public class PatientHealthRecordServiceImpl implements PatientHealthRecordService {

    @Autowired
    private PatientHealthRecordMapper patientHealthRecordMapper;

    @Autowired
    private UserInfoMapper userInfoMapper;

    @Override
    public void createOrUpdateHealthRecord(CreateOrUpdatePatientHealthRecordRequest request) {
        Long userId = SecurityUtils.getCurrentUserId();
        
        // 检查是否已有记录
        LambdaQueryWrapper<PatientHealthRecord> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(PatientHealthRecord::getPatientUserId, userId);
        PatientHealthRecord existingRecord = patientHealthRecordMapper.selectOne(wrapper);
        
        if (existingRecord != null) {
            // 更新现有记录
            BeanUtils.copyProperties(request, existingRecord);
            patientHealthRecordMapper.updateById(existingRecord);
        } else {
            // 创建新记录
            PatientHealthRecord newRecord = new PatientHealthRecord();
            BeanUtils.copyProperties(request, newRecord);
            newRecord.setPatientUserId(userId);
            patientHealthRecordMapper.insert(newRecord);
        }
    }

    @Override
    public PatientHealthRecordResponse getCurrentUserHealthRecord() {
        Long userId = SecurityUtils.getCurrentUserId();
        return getHealthRecordByPatientId(userId);
    }

    @Override
    public PatientHealthRecordResponse getPatientHealthRecord(Long patientUserId) {
        // 权限检查:只有医生和管理员可以查看其他患者的健康档案
        work.baiyun.chronicdiseaseapp.enums.PermissionGroup role = SecurityUtils.getCurrentUserRole();
        if (role != work.baiyun.chronicdiseaseapp.enums.PermissionGroup.DOCTOR && 
            role != work.baiyun.chronicdiseaseapp.enums.PermissionGroup.SYS_ADMIN) {
            throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
                work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_ACCESS_DENIED.getCode(),
                "无权查看其他患者的健康档案");
        }
        
        return getHealthRecordByPatientId(patientUserId);
    }

    private PatientHealthRecordResponse getHealthRecordByPatientId(Long patientUserId) {
        LambdaQueryWrapper<PatientHealthRecord> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(PatientHealthRecord::getPatientUserId, patientUserId);
        PatientHealthRecord record = patientHealthRecordMapper.selectOne(wrapper);
        
        if (record == null) {
            // 当前实现直接返回 null,调用方(controller)会原样返回成功响应中的 data 为 null。
            return null;
        }
        
        PatientHealthRecordResponse response = new PatientHealthRecordResponse();
        BeanUtils.copyProperties(record, response);
        response.setId(record.getId().toString());
        
        // 获取患者昵称
        UserInfo userInfo = userInfoMapper.selectById(patientUserId);
        if (userInfo != null) {
            response.setPatientNickname(userInfo.getNickname());
        }
        
        return response;
    }
}

6. Controller层设计

// PatientHealthRecordController.java
package work.baiyun.chronicdiseaseapp.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import work.baiyun.chronicdiseaseapp.common.R;
import work.baiyun.chronicdiseaseapp.model.vo.PatientHealthRecordResponse;
import work.baiyun.chronicdiseaseapp.model.vo.CreateOrUpdatePatientHealthRecordRequest;
import work.baiyun.chronicdiseaseapp.service.PatientHealthRecordService;
import work.baiyun.chronicdiseaseapp.enums.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
@RequestMapping("/patient-health-record")
@Tag(name = "患者健康档案", description = "患者健康档案管理相关接口")
public class PatientHealthRecordController {

    private static final Logger logger = LoggerFactory.getLogger(PatientHealthRecordController.class);

    @Autowired
    private PatientHealthRecordService patientHealthRecordService;

    @Operation(summary = "创建或更新健康档案", description = "患者创建或更新自己的健康档案")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "健康档案保存成功",
            content = @Content(mediaType = "application/json",
                schema = @Schema(implementation = Void.class))),
        @ApiResponse(responseCode = "500", description = "服务器内部错误",
            content = @Content(mediaType = "application/json",
                schema = @Schema(implementation = Void.class)))
    })
    @PostMapping(path = "/save", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public R<?> save(@RequestBody CreateOrUpdatePatientHealthRecordRequest req) {
        try {
            patientHealthRecordService.createOrUpdateHealthRecord(req);
            return R.success(200, "健康档案保存成功");
        } catch (Exception e) {
            logger.error("save patient health record failed", e);
            return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
        }
    }

注意:`save` 接口没有对当前用户角色做额外限制(会使用当前 loginUserId 作为 patientUserId),因此仅认证通过的用户能创建/更新其健康档案。

    @Operation(summary = "获取当前用户健康档案", description = "患者获取自己的健康档案")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "成功获取健康档案",
            content = @Content(mediaType = "application/json",
                schema = @Schema(implementation = PatientHealthRecordResponse.class))),
        @ApiResponse(responseCode = "500", description = "服务器内部错误",
            content = @Content(mediaType = "application/json",
                schema = @Schema(implementation = Void.class)))
    })
    @GetMapping(path = "/my-record", produces = MediaType.APPLICATION_JSON_VALUE)
    public R<?> getMyRecord() {
        try {
            PatientHealthRecordResponse response = patientHealthRecordService.getCurrentUserHealthRecord();
            return R.success(200, "ok", response);
        } catch (Exception e) {
            logger.error("get my health record failed", e);
            return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
        }
    }

    @Operation(summary = "医生获取患者健康档案", description = "医生获取指定患者的健康档案(需有权限)")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "成功获取健康档案",
            content = @Content(mediaType = "application/json",
                schema = @Schema(implementation = PatientHealthRecordResponse.class))),
        @ApiResponse(responseCode = "403", description = "无权限访问",
            content = @Content(mediaType = "application/json",
                schema = @Schema(implementation = Void.class)))
    })
    @GetMapping(path = "/patient/{patientUserId}", produces = MediaType.APPLICATION_JSON_VALUE)
    public R<?> getPatientRecord(@PathVariable Long patientUserId) {
        try {
            PatientHealthRecordResponse response = patientHealthRecordService.getPatientHealthRecord(patientUserId);
            return R.success(200, "ok", response);
        } catch (Exception e) {
            logger.error("get patient health record failed", e);
            return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
        }
    }
}

## 7. 权限设计

1. **患者**:
    - 可以创建或更新自己的健康档案(通过 `/patient-health-record/save`)。
    - 可以查看自己的健康档案(通过 `/patient-health-record/my-record`)。

2. **医生**:
    - 可以通过 `/patient-health-record/patient/{patientUserId}` 查看指定患者的健康档案。当前实现仅基于角色校验(DOCTOR 或 SYS_ADMIN),没有在此处进行绑定关系检查。

3. **系统管理员**:
    - 在当前实现中,`SYS_ADMIN` 与 `DOCTOR` 在 `getPatientHealthRecord` 中具有同等查询权限。若要使管理员拥有更广泛的权限(例如强制查看所有记录),应统一权限策略并在 Service 层明确限制。

注:当前实现的权限校验仅基于角色(`PermissionGroup.DOCTOR` 或 `PermissionGroup.SYS_ADMIN`),并不在此处主动检查医生是否与患者存在绑定关系(关系校验需由 `UserBindingService` 提供并在 Service 层显式调用)。

7. 错误码补充

在 ErrorCode 枚举中可能需要添加以下错误码:

// 在 ErrorCode.java 中添加
HEALTH_RECORD_ACCESS_DENIED(7000, "无权访问健康档案");