浏览代码

feat(patient-medication): 实现患者用药管理功能

- 新增患者用药记录表 t_patient_medication 及其建表SQL文件
- 设计并实现用药管理完整的前后端交互接口
- 实现用药记录的增删改查及分页查询功能
- 添加用药记录实体类及VO对象(请求/响应)
- 实现用药记录相关Controller、Service、Mapper层代码
- 添加JSON工具类用于处理服用时间点列表的序列化与反序列化
- 补充用药记录不存在等相关错误码定义
- 编写详细的用药管理功能设计文档,包括接口调用示例与权限设计
- 遵循项目数据库规范、API设计规范与安全规范
mcbaiyun 1 月之前
父节点
当前提交
0bed43869e

+ 29 - 0
docs/DB/t_patient_medication.sql

@@ -0,0 +1,29 @@
+/*
+ * 患者用药记录表
+ * 遵循项目数据库表设计规范:
+ * 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_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='患者用药记录表';

+ 801 - 0
docs/New/用药管理功能设计文档.md

@@ -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. **审计与监控**:
+    - 对认证失败、权限拒绝、异常错误等关键安全事件进行集中日志与告警(便于安全分析)。

+ 162 - 0
src/main/java/work/baiyun/chronicdiseaseapp/controller/PatientMedicationController.java

@@ -0,0 +1,162 @@
+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.PatientMedicationService;
+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(PatientMedicationController.class);
+
+    @Autowired
+    private PatientMedicationService 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());
+        }
+    }
+}

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

@@ -39,7 +39,10 @@ public enum ErrorCode {
     FOLLOW_UP_NOT_FOUND(6000, "复诊记录不存在"),
     FOLLOW_UP_ACCESS_DENIED(6001, "无权访问复诊记录"),
     FOLLOW_UP_STATUS_INVALID(6002, "复诊状态无效"),
-    INVALID_DOCTOR(6003, "医生ID无效");
+    INVALID_DOCTOR(6003, "医生ID无效"),
+    
+    // 用药管理相关错误码 7000-7999
+    MEDICATION_NOT_FOUND(7000, "用药记录不存在");
 
     private final int code;
     private final String message;

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

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

+ 37 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/po/PatientMedication.java

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

+ 39 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/CreateMedicationRequest.java

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

+ 24 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/MedicationPageResponse.java

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

+ 16 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/MedicationQueryRequest.java

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

+ 34 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/MedicationResponse.java

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

+ 39 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/UpdateMedicationRequest.java

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

+ 41 - 0
src/main/java/work/baiyun/chronicdiseaseapp/service/PatientMedicationService.java

@@ -0,0 +1,41 @@
+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;
+
+import java.util.List;
+
+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);
+}

+ 125 - 0
src/main/java/work/baiyun/chronicdiseaseapp/service/impl/PatientMedicationServiceImpl.java

@@ -0,0 +1,125 @@
+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 work.baiyun.chronicdiseaseapp.enums.ErrorCode;
+import work.baiyun.chronicdiseaseapp.exception.CustomException;
+
+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 CustomException(
+                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 CustomException(
+                ErrorCode.DATA_NOT_FOUND.getCode(),
+                "用药记录不存在");
+        }
+
+        medicationMapper.deleteById(id);
+    }
+
+    @Override
+    public MedicationResponse getMedicationById(Long id) {
+        PatientMedication medication = medicationMapper.selectById(id);
+        if (medication == null) {
+            throw new CustomException(
+                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;
+    }
+}

+ 39 - 0
src/main/java/work/baiyun/chronicdiseaseapp/util/JsonUtils.java

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