Просмотр исходного кода

feat(enums): 添加健康档案访问拒绝错误码

- 在ErrorCode枚举中新增健康档案相关错误码
- 添加HEALTH_RECORD_ACCESS_DENIED错误码,编号为8000
- 错误信息描述为"无权访问健康档案"
- 将错误码归类到健康档案相关错误码区间8000-8999
mcbaiyun 1 месяц назад
Родитель
Сommit
5b2eafa589

+ 33 - 0
docs/DB/t_patient_health_record.sql

@@ -0,0 +1,33 @@
+/*
+ * 患者健康档案表
+ * 遵循项目数据库表设计规范:
+ * 1. 使用't_'前缀命名
+ * 2. 主键为BIGINT类型
+ * 3. 包含create_user、create_time、update_user、update_time、version、remark等标准审计字段
+ * 4. 字符集为utf8mb4,排序规则为utf8mb4_unicode_ci,引擎为InnoDB
+ * 5. create_time字段应设置DEFAULT CURRENT_TIMESTAMP
+ * 6. update_time字段应设置DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+ */
+
+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='患者健康档案表';

+ 468 - 0
docs/New/患者健康档案功能设计文档.md

@@ -0,0 +1,468 @@
+# 患者健康档案功能设计文档
+
+## 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`)。
+- 公共字段(`version`、`create_user`、`create_time`、`update_user`、`update_time`)由 `BaseEntity` 与 `CustomMetaObjectHandler` 自动填充。
+- 根据需要添加外键约束(注意性能影响)或在应用层校验关联数据完整性。
+- 数据迁移建议使用 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 传输策略)。
+
+## 3. 实体类设计
+
+### 3.1 PO实体类 (PatientHealthRecord.java)
+
+```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 请求对象
+
+```java
+// 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 响应对象
+
+```java
+// 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接口设计
+
+```java
+// 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 接口定义
+
+```java
+// 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);
+}
+```
+
+### 5.2 实现类
+
+```java
+// 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) {
+            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层设计
+
+```java
+// 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());
+        }
+    }
+
+    @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. 错误码补充
+
+在 ErrorCode 枚举中可能需要添加以下错误码:
+
+```java
+// 在 ErrorCode.java 中添加
+HEALTH_RECORD_ACCESS_DENIED(7000, "无权访问健康档案");
+```

+ 89 - 0
src/main/java/work/baiyun/chronicdiseaseapp/controller/PatientHealthRecordController.java

@@ -0,0 +1,89 @@
+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());
+        }
+    }
+
+    @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());
+        }
+    }
+}

+ 4 - 1
src/main/java/work/baiyun/chronicdiseaseapp/enums/ErrorCode.java

@@ -42,7 +42,10 @@ public enum ErrorCode {
     INVALID_DOCTOR(6003, "医生ID无效"),
     
     // 用药管理相关错误码 7000-7999
-    MEDICATION_NOT_FOUND(7000, "用药记录不存在");
+    MEDICATION_NOT_FOUND(7000, "用药记录不存在"),
+    
+    // 健康档案相关错误码 8000-8999
+    HEALTH_RECORD_ACCESS_DENIED(8000, "无权访问健康档案");
 
     private final int code;
     private final String message;

+ 9 - 0
src/main/java/work/baiyun/chronicdiseaseapp/mapper/PatientHealthRecordMapper.java

@@ -0,0 +1,9 @@
+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> {
+}

+ 53 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/po/PatientHealthRecord.java

@@ -0,0 +1,53 @@
+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;
+}

+ 35 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/CreateOrUpdatePatientHealthRecordRequest.java

@@ -0,0 +1,35 @@
+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;
+}

+ 52 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/PatientHealthRecordResponse.java

@@ -0,0 +1,52 @@
+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;
+}

+ 21 - 0
src/main/java/work/baiyun/chronicdiseaseapp/service/PatientHealthRecordService.java

@@ -0,0 +1,21 @@
+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);
+}

+ 88 - 0
src/main/java/work/baiyun/chronicdiseaseapp/service/impl/PatientHealthRecordServiceImpl.java

@@ -0,0 +1,88 @@
+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) {
+            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;
+    }
+}