用药管理功能设计文档.md 31 KB

用药管理功能设计文档

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)。
  • 公共字段(versioncreate_usercreate_timeupdate_userupdate_time)由 BaseEntityCustomMetaObjectHandler 自动填充。
  • 根据需要添加索引以提高查询效率。
  • 数据迁移建议使用 Flyway/Liquibase(脚本命名遵循 V{version}__{description}.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)

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 请求对象

// 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 响应对象

// 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接口设计

// 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 接口定义

// 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 实现类

// 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层设计

// 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.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());
        }
    }
}

7. 工具类设计

7.1 JSON工具类 (JsonUtils.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 枚举中可能需要添加以下错误码:

// 在 ErrorCode.java 中添加
// NOTE: 项目代码中已存在错误码 `MEDICATION_NOT_FOUND(7000, "用药记录不存在")`。
// 当前实现:`PatientMedicationServiceImpl` 在找不到记录时抛出 `ErrorCode.DATA_NOT_FOUND`(4000)。建议:把 `DATA_NOT_FOUND` 替换为更明确的 `MEDICATION_NOT_FOUND`,便于客户端定位错误类型与处理。

9. 接口调用示例

9.1 添加用药记录

POST /patient-medication/create
Content-Type: application/json

{
  "patientUserId": 123456,
  "medicineName": "阿司匹林",
  "dosage": "100mg",
  "frequency": "每日1次,每次1粒",
  "times": ["08:00"],
  "note": "饭后服用"
}

9.2 更新用药记录

PUT /patient-medication/1
Content-Type: application/json

{
  "id": 1,
  "medicineName": "阿司匹林肠溶片",
  "dosage": "100mg",
  "frequency": "每日1次,每次1粒",
  "times": ["08:00"],
  "note": "饭后服用"
}

9.3 查询用药记录

POST /patient-medication/list
Content-Type: application/json

{
  "pageNum": 1,
  "pageSize": 10,
  "patientUserId": 123456
}

9.4 删除用药记录

DELETE /patient-medication/1

9.5 获取用药记录详情

GET /patient-medication/1

9.6 根据患者ID获取用药记录列表

GET /patient-medication/patient/123456

10. 权限设计

  1. 患者

    • 目标:仅允许患者查看/管理本人的用药记录。
    • 当前实现:接口需要登录(由 AuthInterceptor 校验),但 PatientMedicationService 并未强制检查 currentUserId 是否与 patientUserId 一致——因此前端/调用方需在调用时传入正确的患者ID。
    • 建议:服务层或控制层显式校验 SecurityUtils.getCurrentUserId() 是否等于 patientUserId,以防止越权写入或修改。
  2. 医生

    • 目标:医生可查看其绑定患者的用药记录(只读或写权限视业务而定)。
    • 当前实现:控制器与服务中未检查医生角色或绑定关系,理论上任何登录用户传入患者 ID 都可进行增删改查。
    • 建议:在医生操作的接口中(或在服务层)增加绑定校验 UserBindingService.checkUserBinding,并在必要时校验 PermissionGroup.DOCTOR
  3. 系统管理员

    • 具有全局权限,可以查询和管理所有用药记录(基于 PermissionGroup.SYS_ADMIN 校验)。
    • 建议:管理员接口或逻辑分支在查询大规模数据时需引入审计与分页限制,避免误操作带来数据风险。

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 中定义(项目中已存在 MEDICATION_NOT_FOUND(7000, "用药记录不存在"))。
  4. 日志与敏感信息

    • 在日志中避免记录完整 Token 或敏感字段;若需记录,脱敏或仅记录前 8 位。
    • 记录关键审计事件:创建、更新、删除等操作的用户 ID 与时间戳。
  5. SQL 注入与 ORM 使用

    • 使用 MyBatis-Plus 提供的预编译与条件构造器,禁止使用字符串拼接构建 SQL。
  6. 会话安全与 Token 管理

    • Token 存储与过期策略遵循全局规范(存 t_user_token,有效期 72 小时,低于阈值自动延长)。
  7. 部署/传输

    • 建议生产环境使用 HTTPS,证书放置在 classpath:cert/ 并在启动时加载。
  8. 最小权限原则

    • 服务端对每个接口、数据读取与修改操作都进行基于角色的授权校验,不信任前端传入的角色或用户 ID。
  9. 审计与监控

    • 对认证失败、权限拒绝、异常错误等关键安全事件进行集中日志与告警(便于安全分析)。