|
|
@@ -0,0 +1,801 @@
|
|
|
+# 用药管理功能设计文档
|
|
|
+
|
|
|
+## 1. 功能概述
|
|
|
+
|
|
|
+用药管理功能旨在为患者提供一个便捷的用药记录和管理平台。该功能允许患者添加、编辑、删除和查看自己的用药信息,包括药品名称、剂量、服用频率、服用时间等。医生也可以查看患者的用药记录,以便更好地了解患者的治疗情况。
|
|
|
+
|
|
|
+## 2. 数据库设计
|
|
|
+
|
|
|
+### 2.1 患者用药记录表 (t_patient_medication)
|
|
|
+
|
|
|
+根据项目数据库表设计规范(参见 `docs/OLD/DevRule/07-数据库规范.md`),创建患者用药记录表:
|
|
|
+
|
|
|
+| 字段名 | 类型 | 描述 |
|
|
|
+|--------|------|------|
|
|
|
+| id | BIGINT(20) | 主键ID,使用雪花算法(MyBatis-Plus `ASSIGN_ID`) |
|
|
|
+| patient_user_id | BIGINT(20) | 患者用户ID,外键关联用户表 |
|
|
|
+| medicine_name | VARCHAR(100) | 药品名称 |
|
|
|
+| dosage | VARCHAR(50) | 剂量规格(如:100mg、0.5g) |
|
|
|
+| frequency | VARCHAR(100) | 服用频率(如:每日1次、每日3次每次1粒) |
|
|
|
+| times | TEXT | 服用时间点列表(JSON格式存储) |
|
|
|
+| note | 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`)。
|
|
|
+
|
|
|
+示例建表(参考):
|
|
|
+```sql
|
|
|
+CREATE TABLE `t_patient_medication` (
|
|
|
+ `id` bigint(20) NOT NULL COMMENT '主键ID',
|
|
|
+ `patient_user_id` bigint(20) NOT NULL COMMENT '患者用户ID',
|
|
|
+ `medicine_name` varchar(100) NOT NULL COMMENT '药品名称',
|
|
|
+ `dosage` varchar(50) NOT NULL COMMENT '剂量规格',
|
|
|
+ `frequency` varchar(100) NOT NULL COMMENT '服用频率',
|
|
|
+ `times` text COMMENT '服用时间点列表(JSON格式存储)',
|
|
|
+ `note` 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`),
|
|
|
+ KEY `idx_patient_user_id` (`patient_user_id`),
|
|
|
+ KEY `idx_medicine_name` (`medicine_name`)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='患者用药记录表';
|
|
|
+```
|
|
|
+
|
|
|
+备注:对外响应的 `id` 建议以字符串形式返回以避免前端 JS 精度丢失(参见 `docs/OLD/DevRule/03-API设计规范.md` 的长整型 ID 传输策略)。
|
|
|
+
|
|
|
+## 3. 实体类设计
|
|
|
+
|
|
|
+### 3.1 PO实体类 (PatientMedication.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;
|
|
|
+
|
|
|
+import java.time.LocalDateTime;
|
|
|
+
|
|
|
+@Schema(description = "患者用药记录表")
|
|
|
+@TableName("t_patient_medication")
|
|
|
+@Data
|
|
|
+@EqualsAndHashCode(callSuper = false)
|
|
|
+public class PatientMedication extends BaseEntity {
|
|
|
+ @Schema(description = "患者用户ID")
|
|
|
+ @TableField("patient_user_id")
|
|
|
+ private Long patientUserId;
|
|
|
+
|
|
|
+ @Schema(description = "药品名称")
|
|
|
+ @TableField("medicine_name")
|
|
|
+ private String medicineName;
|
|
|
+
|
|
|
+ @Schema(description = "剂量规格")
|
|
|
+ @TableField("dosage")
|
|
|
+ private String dosage;
|
|
|
+
|
|
|
+ @Schema(description = "服用频率")
|
|
|
+ @TableField("frequency")
|
|
|
+ private String frequency;
|
|
|
+
|
|
|
+ @Schema(description = "服用时间点列表(JSON格式存储)")
|
|
|
+ @TableField("times")
|
|
|
+ private String times;
|
|
|
+
|
|
|
+ @Schema(description = "备注信息")
|
|
|
+ @TableField("note")
|
|
|
+ private String note;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 VO对象
|
|
|
+
|
|
|
+#### 3.2.1 请求对象
|
|
|
+
|
|
|
+```java
|
|
|
+// CreateMedicationRequest.java
|
|
|
+package work.baiyun.chronicdiseaseapp.model.vo;
|
|
|
+
|
|
|
+import io.swagger.v3.oas.annotations.media.Schema;
|
|
|
+import lombok.Data;
|
|
|
+
|
|
|
+import jakarta.validation.constraints.NotBlank;
|
|
|
+import jakarta.validation.constraints.NotNull;
|
|
|
+import jakarta.validation.constraints.Size;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@Schema(description = "创建用药记录请求")
|
|
|
+@Data
|
|
|
+public class CreateMedicationRequest {
|
|
|
+ @Schema(description = "患者用户ID", required = true)
|
|
|
+ @NotNull(message = "患者用户ID不能为空")
|
|
|
+ private Long patientUserId;
|
|
|
+
|
|
|
+ @Schema(description = "药品名称", required = true)
|
|
|
+ @NotBlank(message = "药品名称不能为空")
|
|
|
+ @Size(max = 100, message = "药品名称长度不能超过100个字符")
|
|
|
+ private String medicineName;
|
|
|
+
|
|
|
+ @Schema(description = "剂量规格", required = true)
|
|
|
+ @NotBlank(message = "剂量规格不能为空")
|
|
|
+ @Size(max = 50, message = "剂量规格长度不能超过50个字符")
|
|
|
+ private String dosage;
|
|
|
+
|
|
|
+ @Schema(description = "服用频率", required = true)
|
|
|
+ @NotBlank(message = "服用频率不能为空")
|
|
|
+ @Size(max = 100, message = "服用频率长度不能超过100个字符")
|
|
|
+ private String frequency;
|
|
|
+
|
|
|
+ @Schema(description = "服用时间点列表", required = true)
|
|
|
+ @NotNull(message = "服用时间点列表不能为空")
|
|
|
+ private List<String> times;
|
|
|
+
|
|
|
+ @Schema(description = "备注信息")
|
|
|
+ private String note;
|
|
|
+}
|
|
|
+
|
|
|
+// UpdateMedicationRequest.java
|
|
|
+package work.baiyun.chronicdiseaseapp.model.vo;
|
|
|
+
|
|
|
+import io.swagger.v3.oas.annotations.media.Schema;
|
|
|
+import lombok.Data;
|
|
|
+
|
|
|
+import jakarta.validation.constraints.NotBlank;
|
|
|
+import jakarta.validation.constraints.NotNull;
|
|
|
+import jakarta.validation.constraints.Size;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@Schema(description = "更新用药记录请求")
|
|
|
+@Data
|
|
|
+public class UpdateMedicationRequest {
|
|
|
+ @Schema(description = "用药记录ID", required = true)
|
|
|
+ @NotNull(message = "用药记录ID不能为空")
|
|
|
+ private Long id;
|
|
|
+
|
|
|
+ @Schema(description = "药品名称", required = true)
|
|
|
+ @NotBlank(message = "药品名称不能为空")
|
|
|
+ @Size(max = 100, message = "药品名称长度不能超过100个字符")
|
|
|
+ private String medicineName;
|
|
|
+
|
|
|
+ @Schema(description = "剂量规格", required = true)
|
|
|
+ @NotBlank(message = "剂量规格不能为空")
|
|
|
+ @Size(max = 50, message = "剂量规格长度不能超过50个字符")
|
|
|
+ private String dosage;
|
|
|
+
|
|
|
+ @Schema(description = "服用频率", required = true)
|
|
|
+ @NotBlank(message = "服用频率不能为空")
|
|
|
+ @Size(max = 100, message = "服用频率长度不能超过100个字符")
|
|
|
+ private String frequency;
|
|
|
+
|
|
|
+ @Schema(description = "服用时间点列表", required = true)
|
|
|
+ @NotNull(message = "服用时间点列表不能为空")
|
|
|
+ private List<String> times;
|
|
|
+
|
|
|
+ @Schema(description = "备注信息")
|
|
|
+ private String note;
|
|
|
+}
|
|
|
+
|
|
|
+// MedicationQueryRequest.java
|
|
|
+package work.baiyun.chronicdiseaseapp.model.vo;
|
|
|
+
|
|
|
+import io.swagger.v3.oas.annotations.media.Schema;
|
|
|
+import lombok.Data;
|
|
|
+import lombok.EqualsAndHashCode;
|
|
|
+
|
|
|
+@Schema(description = "用药记录查询请求")
|
|
|
+@Data
|
|
|
+@EqualsAndHashCode(callSuper = true)
|
|
|
+public class MedicationQueryRequest extends BaseQueryRequest {
|
|
|
+ @Schema(description = "患者用户ID")
|
|
|
+ private Long patientUserId;
|
|
|
+
|
|
|
+ @Schema(description = "药品名称关键字")
|
|
|
+ private String medicineNameKeyword;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.2.2 响应对象
|
|
|
+
|
|
|
+```java
|
|
|
+// MedicationResponse.java
|
|
|
+package work.baiyun.chronicdiseaseapp.model.vo;
|
|
|
+
|
|
|
+import io.swagger.v3.oas.annotations.media.Schema;
|
|
|
+import lombok.Data;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@Schema(description = "用药记录响应")
|
|
|
+@Data
|
|
|
+public class MedicationResponse {
|
|
|
+ @Schema(description = "用药记录ID")
|
|
|
+ private String id;
|
|
|
+
|
|
|
+ @Schema(description = "患者用户ID")
|
|
|
+ private Long patientUserId;
|
|
|
+
|
|
|
+ @Schema(description = "药品名称")
|
|
|
+ private String medicineName;
|
|
|
+
|
|
|
+ @Schema(description = "剂量规格")
|
|
|
+ private String dosage;
|
|
|
+
|
|
|
+ @Schema(description = "服用频率")
|
|
|
+ private String frequency;
|
|
|
+
|
|
|
+ @Schema(description = "服用时间点列表")
|
|
|
+ private List<String> times;
|
|
|
+
|
|
|
+ @Schema(description = "备注信息")
|
|
|
+ private String note;
|
|
|
+
|
|
|
+ @Schema(description = "创建时间")
|
|
|
+ private LocalDateTime createTime;
|
|
|
+}
|
|
|
+
|
|
|
+// MedicationPageResponse.java
|
|
|
+package work.baiyun.chronicdiseaseapp.model.vo;
|
|
|
+
|
|
|
+import io.swagger.v3.oas.annotations.media.Schema;
|
|
|
+import lombok.Data;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@Schema(description = "用药记录分页响应")
|
|
|
+@Data
|
|
|
+public class MedicationPageResponse {
|
|
|
+ @Schema(description = "数据列表")
|
|
|
+ private List<MedicationResponse> records;
|
|
|
+
|
|
|
+ @Schema(description = "总数")
|
|
|
+ private long total;
|
|
|
+
|
|
|
+ @Schema(description = "每页大小")
|
|
|
+ private long size;
|
|
|
+
|
|
|
+ @Schema(description = "当前页码")
|
|
|
+ private long current;
|
|
|
+
|
|
|
+ @Schema(description = "总页数")
|
|
|
+ private long pages;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 4. Mapper接口设计
|
|
|
+
|
|
|
+```java
|
|
|
+// PatientMedicationMapper.java
|
|
|
+package work.baiyun.chronicdiseaseapp.mapper;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.po.PatientMedication;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface PatientMedicationMapper extends BaseMapper<PatientMedication> {
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 5. Service层设计
|
|
|
+
|
|
|
+### 5.1 接口定义
|
|
|
+
|
|
|
+```java
|
|
|
+// PatientMedicationService.java
|
|
|
+package work.baiyun.chronicdiseaseapp.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.vo.MedicationQueryRequest;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.vo.MedicationResponse;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.vo.CreateMedicationRequest;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.vo.UpdateMedicationRequest;
|
|
|
+
|
|
|
+public interface PatientMedicationService {
|
|
|
+ /**
|
|
|
+ * 创建用药记录
|
|
|
+ */
|
|
|
+ void createMedication(CreateMedicationRequest request);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新用药记录
|
|
|
+ */
|
|
|
+ void updateMedication(UpdateMedicationRequest request);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 分页查询用药记录
|
|
|
+ */
|
|
|
+ Page<MedicationResponse> listMedications(MedicationQueryRequest request);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据ID删除用药记录
|
|
|
+ */
|
|
|
+ void deleteMedication(Long id);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据ID获取患者用药记录详情
|
|
|
+ */
|
|
|
+ MedicationResponse getMedicationById(Long id);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据患者ID获取患者用药记录列表
|
|
|
+ */
|
|
|
+ List<MedicationResponse> listMedicationsByPatientId(Long patientUserId);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 实现类
|
|
|
+
|
|
|
+```java
|
|
|
+// PatientMedicationServiceImpl.java
|
|
|
+package work.baiyun.chronicdiseaseapp.service.impl;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.fasterxml.jackson.core.type.TypeReference;
|
|
|
+import org.springframework.beans.BeanUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import work.baiyun.chronicdiseaseapp.mapper.PatientMedicationMapper;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.po.PatientMedication;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.vo.MedicationQueryRequest;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.vo.MedicationResponse;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.vo.CreateMedicationRequest;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.vo.UpdateMedicationRequest;
|
|
|
+import work.baiyun.chronicdiseaseapp.service.PatientMedicationService;
|
|
|
+import work.baiyun.chronicdiseaseapp.util.JsonUtils;
|
|
|
+import work.baiyun.chronicdiseaseapp.util.SecurityUtils;
|
|
|
+
|
|
|
+import java.util.List;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class PatientMedicationServiceImpl implements PatientMedicationService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private PatientMedicationMapper medicationMapper;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void createMedication(CreateMedicationRequest request) {
|
|
|
+ PatientMedication medication = new PatientMedication();
|
|
|
+ BeanUtils.copyProperties(request, medication);
|
|
|
+ medication.setTimes(JsonUtils.toJson(request.getTimes()));
|
|
|
+ medication.setPatientUserId(request.getPatientUserId());
|
|
|
+ medicationMapper.insert(medication);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void updateMedication(UpdateMedicationRequest request) {
|
|
|
+ PatientMedication medication = medicationMapper.selectById(request.getId());
|
|
|
+ if (medication == null) {
|
|
|
+ throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
|
|
|
+ work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_NOT_FOUND.getCode(),
|
|
|
+ "用药记录不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ BeanUtils.copyProperties(request, medication);
|
|
|
+ medication.setTimes(JsonUtils.toJson(request.getTimes()));
|
|
|
+ medicationMapper.updateById(medication);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Page<MedicationResponse> listMedications(MedicationQueryRequest request) {
|
|
|
+ Page<PatientMedication> page = new Page<>(request.getPageNum(), request.getPageSize());
|
|
|
+ LambdaQueryWrapper<PatientMedication> wrapper = new LambdaQueryWrapper<>();
|
|
|
+
|
|
|
+ // 根据患者ID筛选
|
|
|
+ wrapper.eq(request.getPatientUserId() != null, PatientMedication::getPatientUserId, request.getPatientUserId());
|
|
|
+
|
|
|
+ // 根据药品名称关键字搜索
|
|
|
+ if (request.getMedicineNameKeyword() != null && !request.getMedicineNameKeyword().trim().isEmpty()) {
|
|
|
+ wrapper.like(PatientMedication::getMedicineName, request.getMedicineNameKeyword().trim());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 时间范围筛选
|
|
|
+ wrapper.ge(request.getStartTime() != null, PatientMedication::getCreateTime, request.getStartTime())
|
|
|
+ .le(request.getEndTime() != null, PatientMedication::getCreateTime, request.getEndTime())
|
|
|
+ .orderByDesc(PatientMedication::getCreateTime);
|
|
|
+
|
|
|
+ Page<PatientMedication> result = medicationMapper.selectPage(page, wrapper);
|
|
|
+
|
|
|
+ List<MedicationResponse> responses = result.getRecords().stream().map(this::convertToResponse).collect(Collectors.toList());
|
|
|
+
|
|
|
+ Page<MedicationResponse> responsePage = new Page<>();
|
|
|
+ responsePage.setRecords(responses);
|
|
|
+ responsePage.setCurrent(result.getCurrent());
|
|
|
+ responsePage.setSize(result.getSize());
|
|
|
+ responsePage.setTotal(result.getTotal());
|
|
|
+ responsePage.setPages(result.getPages());
|
|
|
+ return responsePage;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void deleteMedication(Long id) {
|
|
|
+ PatientMedication medication = medicationMapper.selectById(id);
|
|
|
+ if (medication == null) {
|
|
|
+ throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
|
|
|
+ work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_NOT_FOUND.getCode(),
|
|
|
+ "用药记录不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ medicationMapper.deleteById(id);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public MedicationResponse getMedicationById(Long id) {
|
|
|
+ PatientMedication medication = medicationMapper.selectById(id);
|
|
|
+ if (medication == null) {
|
|
|
+ throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
|
|
|
+ work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_NOT_FOUND.getCode(),
|
|
|
+ "用药记录不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ return convertToResponse(medication);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<MedicationResponse> listMedicationsByPatientId(Long patientUserId) {
|
|
|
+ LambdaQueryWrapper<PatientMedication> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(PatientMedication::getPatientUserId, patientUserId)
|
|
|
+ .orderByDesc(PatientMedication::getCreateTime);
|
|
|
+
|
|
|
+ List<PatientMedication> medications = medicationMapper.selectList(wrapper);
|
|
|
+ return medications.stream().map(this::convertToResponse).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ private MedicationResponse convertToResponse(PatientMedication medication) {
|
|
|
+ MedicationResponse response = new MedicationResponse();
|
|
|
+ BeanUtils.copyProperties(medication, response);
|
|
|
+ response.setId(medication.getId().toString());
|
|
|
+ response.setTimes(JsonUtils.fromJson(medication.getTimes(), new TypeReference<List<String>>(){}));
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 6. Controller层设计
|
|
|
+
|
|
|
+```java
|
|
|
+// PatientMedicationController.java
|
|
|
+package work.baiyun.chronicdiseaseapp.controller;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+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.MedicationQueryRequest;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.vo.MedicationResponse;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.vo.CreateMedicationRequest;
|
|
|
+import work.baiyun.chronicdiseaseapp.model.vo.UpdateMedicationRequest;
|
|
|
+import work.baiyun.chronicdiseaseapp.service.MedicationService;
|
|
|
+import work.baiyun.chronicdiseaseapp.enums.ErrorCode;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@RestController
|
|
|
+@RequestMapping("/patient-medication")
|
|
|
+@Tag(name = "用药管理", description = "用药管理相关接口")
|
|
|
+public class PatientMedicationController {
|
|
|
+
|
|
|
+ private static final Logger logger = LoggerFactory.getLogger(MedicationController.class);
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private MedicationService medicationService;
|
|
|
+
|
|
|
+ @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 = "/create", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
|
|
+ public R<?> create(@RequestBody CreateMedicationRequest req) {
|
|
|
+ try {
|
|
|
+ medicationService.createMedication(req);
|
|
|
+ return R.success(200, "用药记录创建成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("create medication 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 = Void.class))),
|
|
|
+ @ApiResponse(responseCode = "500", description = "服务器内部错误",
|
|
|
+ content = @Content(mediaType = "application/json",
|
|
|
+ schema = @Schema(implementation = Void.class)))
|
|
|
+ })
|
|
|
+ @PutMapping(path = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
|
|
+ public R<?> update(@PathVariable Long id, @RequestBody UpdateMedicationRequest req) {
|
|
|
+ try {
|
|
|
+ // 将路径ID赋值到请求对象,保证一致性
|
|
|
+ req.setId(id);
|
|
|
+ medicationService.updateMedication(req);
|
|
|
+ return R.success(200, "用药记录更新成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("update medication 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 = work.baiyun.chronicdiseaseapp.model.vo.MedicationPageResponse.class))),
|
|
|
+ @ApiResponse(responseCode = "500", description = "服务器内部错误",
|
|
|
+ content = @Content(mediaType = "application/json",
|
|
|
+ schema = @Schema(implementation = Void.class)))
|
|
|
+ })
|
|
|
+ @PostMapping(path = "/list", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
|
|
+ public R<?> list(@RequestBody MedicationQueryRequest req) {
|
|
|
+ try {
|
|
|
+ Page<MedicationResponse> page = medicationService.listMedications(req);
|
|
|
+ work.baiyun.chronicdiseaseapp.model.vo.MedicationPageResponse vo = new work.baiyun.chronicdiseaseapp.model.vo.MedicationPageResponse();
|
|
|
+ vo.setRecords(page.getRecords());
|
|
|
+ vo.setTotal(page.getTotal());
|
|
|
+ vo.setSize(page.getSize());
|
|
|
+ vo.setCurrent(page.getCurrent());
|
|
|
+ vo.setPages(page.getPages());
|
|
|
+ return R.success(200, "ok", vo);
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("list medications failed", e);
|
|
|
+ return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Operation(summary = "删除用药记录", description = "根据ID删除用药记录")
|
|
|
+ @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)))
|
|
|
+ })
|
|
|
+ @DeleteMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
|
|
+ public R<?> delete(@PathVariable Long id) {
|
|
|
+ try {
|
|
|
+ medicationService.deleteMedication(id);
|
|
|
+ return R.success(200, "用药记录删除成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("delete medication failed", e);
|
|
|
+ return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Operation(summary = "获取用药记录详情", description = "根据ID获取用药记录详细信息")
|
|
|
+ @ApiResponses(value = {
|
|
|
+ @ApiResponse(responseCode = "200", description = "成功获取用药记录信息",
|
|
|
+ content = @Content(mediaType = "application/json",
|
|
|
+ schema = @Schema(implementation = work.baiyun.chronicdiseaseapp.model.vo.MedicationResponse.class))),
|
|
|
+ @ApiResponse(responseCode = "500", description = "服务器内部错误",
|
|
|
+ content = @Content(mediaType = "application/json",
|
|
|
+ schema = @Schema(implementation = Void.class)))
|
|
|
+ })
|
|
|
+ @GetMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
|
|
+ public R<?> get(@PathVariable Long id) {
|
|
|
+ try {
|
|
|
+ MedicationResponse medication = medicationService.getMedicationById(id);
|
|
|
+ return R.success(200, "ok", medication);
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("get medication failed", e);
|
|
|
+ return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Operation(summary = "根据患者ID获取用药记录列表", description = "根据患者ID获取用药记录列表")
|
|
|
+ @ApiResponses(value = {
|
|
|
+ @ApiResponse(responseCode = "200", description = "成功获取用药记录列表",
|
|
|
+ content = @Content(mediaType = "application/json",
|
|
|
+ schema = @Schema(implementation = work.baiyun.chronicdiseaseapp.model.vo.MedicationResponse.class))),
|
|
|
+ @ApiResponse(responseCode = "500", description = "服务器内部错误",
|
|
|
+ content = @Content(mediaType = "application/json",
|
|
|
+ schema = @Schema(implementation = Void.class)))
|
|
|
+ })
|
|
|
+ @GetMapping(path = "/patient/{patientUserId}", produces = MediaType.APPLICATION_JSON_VALUE)
|
|
|
+ public R<?> listByPatientId(@PathVariable Long patientUserId) {
|
|
|
+ try {
|
|
|
+ List<MedicationResponse> medications = medicationService.listMedicationsByPatientId(patientUserId);
|
|
|
+ return R.success(200, "ok", medications);
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("list medications by patient id failed", e);
|
|
|
+ return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 7. 工具类设计
|
|
|
+
|
|
|
+### 7.1 JSON工具类 (JsonUtils.java)
|
|
|
+
|
|
|
+```java
|
|
|
+// JsonUtils.java
|
|
|
+package work.baiyun.chronicdiseaseapp.util;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.core.JsonProcessingException;
|
|
|
+import com.fasterxml.jackson.core.type.TypeReference;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+public class JsonUtils {
|
|
|
+ private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);
|
|
|
+ private static final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ public static String toJson(Object obj) {
|
|
|
+ try {
|
|
|
+ return objectMapper.writeValueAsString(obj);
|
|
|
+ } catch (JsonProcessingException e) {
|
|
|
+ logger.error("Failed to serialize object to JSON", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static <T> T fromJson(String json, Class<T> clazz) {
|
|
|
+ try {
|
|
|
+ return objectMapper.readValue(json, clazz);
|
|
|
+ } catch (JsonProcessingException e) {
|
|
|
+ logger.error("Failed to deserialize JSON to object", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static <T> T fromJson(String json, TypeReference<T> typeReference) {
|
|
|
+ try {
|
|
|
+ return objectMapper.readValue(json, typeReference);
|
|
|
+ } catch (JsonProcessingException e) {
|
|
|
+ logger.error("Failed to deserialize JSON to object", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## 8. 错误码补充
|
|
|
+
|
|
|
+在 ErrorCode 枚举中可能需要添加以下错误码:
|
|
|
+
|
|
|
+```java
|
|
|
+// 在 ErrorCode.java 中添加
|
|
|
+MEDICATION_NOT_FOUND(8000, "用药记录不存在");
|
|
|
+```
|
|
|
+
|
|
|
+## 9. 接口调用示例
|
|
|
+
|
|
|
+### 9.1 添加用药记录
|
|
|
+```http
|
|
|
+POST /patient-medication/create
|
|
|
+Content-Type: application/json
|
|
|
+
|
|
|
+{
|
|
|
+ "patientUserId": 123456,
|
|
|
+ "medicineName": "阿司匹林",
|
|
|
+ "dosage": "100mg",
|
|
|
+ "frequency": "每日1次,每次1粒",
|
|
|
+ "times": ["08:00"],
|
|
|
+ "note": "饭后服用"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 9.2 更新用药记录
|
|
|
+```http
|
|
|
+PUT /patient-medication/1
|
|
|
+Content-Type: application/json
|
|
|
+
|
|
|
+{
|
|
|
+ "id": 1,
|
|
|
+ "medicineName": "阿司匹林肠溶片",
|
|
|
+ "dosage": "100mg",
|
|
|
+ "frequency": "每日1次,每次1粒",
|
|
|
+ "times": ["08:00"],
|
|
|
+ "note": "饭后服用"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 9.3 查询用药记录
|
|
|
+```http
|
|
|
+POST /patient-medication/list
|
|
|
+Content-Type: application/json
|
|
|
+
|
|
|
+{
|
|
|
+ "pageNum": 1,
|
|
|
+ "pageSize": 10,
|
|
|
+ "patientUserId": 123456
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 9.4 删除用药记录
|
|
|
+```http
|
|
|
+DELETE /patient-medication/1
|
|
|
+```
|
|
|
+
|
|
|
+### 9.5 获取用药记录详情
|
|
|
+```http
|
|
|
+GET /patient-medication/1
|
|
|
+```
|
|
|
+
|
|
|
+### 9.6 根据患者ID获取用药记录列表
|
|
|
+```http
|
|
|
+GET /patient-medication/patient/123456
|
|
|
+```
|
|
|
+
|
|
|
+## 10. 权限设计
|
|
|
+
|
|
|
+1. **患者**:
|
|
|
+ - 可以查看自己的用药记录
|
|
|
+ - 可以添加新的用药记录
|
|
|
+ - 可以编辑自己的用药记录
|
|
|
+ - 可以删除自己的用药记录
|
|
|
+
|
|
|
+2. **医生**:
|
|
|
+ - 可以查看患者的用药记录
|
|
|
+ - 可以添加、编辑或删除患者的用药记录
|
|
|
+
|
|
|
+3. **系统管理员**:
|
|
|
+ - 具有患者和医生的所有权限
|
|
|
+ - 可以管理所有用药记录
|
|
|
+
|
|
|
+## 11. 安全考虑
|
|
|
+
|
|
|
+本项目安全实现应遵循 `docs/OLD/DevRule/08-安全规范.md` 中的已有约定,以下为与用药管理功能相关的要点(需在实现时落地):
|
|
|
+
|
|
|
+1. **认证与授权**:
|
|
|
+ - 使用 Token(优先 `Authorization: Bearer <token>`,兼容 `X-Token` / `token`)并通过 `AuthInterceptor` 校验,将 `currentUserId`/`currentUserRole` 注入请求上下文。
|
|
|
+ - 患者只能访问自己的用药记录
|
|
|
+ - 医生可以查看、添加、编辑或删除患者的用药记录
|
|
|
+ - 系统管理员可以访问所有用药记录
|
|
|
+
|
|
|
+2. **输入校验**:
|
|
|
+ - 使用 Bean Validation 注解(`@NotBlank`、`@Size` 等)对请求参数进行校验。
|
|
|
+
|
|
|
+3. **异常与统一错误响应**:
|
|
|
+ - 使用全局异常处理器(`@RestControllerAdvice`/`CustomExceptionHandler`)统一映射 `CustomException`、校验异常、未知异常到 `R.fail(code, message)`,并返回合适的 HTTP 状态码(如 400/403/404/500)。
|
|
|
+ - 业务异常(如 `MEDICATION_NOT_FOUND`)应使用明确错误码并在 `ErrorCode` 中定义。
|
|
|
+
|
|
|
+4. **日志与敏感信息**:
|
|
|
+ - 在日志中避免记录完整 Token 或敏感字段;若需记录,脱敏或仅记录前 8 位。
|
|
|
+ - 记录关键审计事件:创建、更新、删除等操作的用户 ID 与时间戳。
|
|
|
+
|
|
|
+5. **SQL 注入与 ORM 使用**:
|
|
|
+ - 使用 MyBatis-Plus 提供的预编译与条件构造器,禁止使用字符串拼接构建 SQL。
|
|
|
+
|
|
|
+6. **会话安全与 Token 管理**:
|
|
|
+ - Token 存储与过期策略遵循全局规范(存 `t_user_token`,有效期 72 小时,低于阈值自动延长)。
|
|
|
+
|
|
|
+7. **部署/传输**:
|
|
|
+ - 建议生产环境使用 HTTPS,证书放置在 `classpath:cert/` 并在启动时加载。
|
|
|
+
|
|
|
+8. **最小权限原则**:
|
|
|
+ - 服务端对每个接口、数据读取与修改操作都进行基于角色的授权校验,不信任前端传入的角色或用户 ID。
|
|
|
+
|
|
|
+9. **审计与监控**:
|
|
|
+ - 对认证失败、权限拒绝、异常错误等关键安全事件进行集中日志与告警(便于安全分析)。
|