# 用药管理功能设计文档 ## 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 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 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 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 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 { } ``` ## 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 listMedications(MedicationQueryRequest request); /** * 根据ID删除用药记录 */ void deleteMedication(Long id); /** * 根据ID获取患者用药记录详情 */ MedicationResponse getMedicationById(Long id); /** * 根据患者ID获取患者用药记录列表 */ List 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 listMedications(MedicationQueryRequest request) { Page page = new Page<>(request.getPageNum(), request.getPageSize()); LambdaQueryWrapper 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 result = medicationMapper.selectPage(page, wrapper); List responses = result.getRecords().stream().map(this::convertToResponse).collect(Collectors.toList()); Page 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 listMedicationsByPatientId(Long patientUserId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(PatientMedication::getPatientUserId, patientUserId) .orderByDesc(PatientMedication::getCreateTime); List 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>(){})); 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 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 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 fromJson(String json, Class clazz) { try { return objectMapper.readValue(json, clazz); } catch (JsonProcessingException e) { logger.error("Failed to deserialize JSON to object", e); return null; } } public static T fromJson(String json, TypeReference 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 `,兼容 `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. **审计与监控**: - 对认证失败、权限拒绝、异常错误等关键安全事件进行集中日志与告警(便于安全分析)。