患者提醒数据管理功能设计文档.md 30 KB

患者提醒数据管理功能设计文档

1. 功能概述

患者提醒数据管理功能旨在为慢性病患者提供个性化的健康提醒服务,包括血压、血糖、心率等健康数据测量提醒以及用药提醒。每个患者只有一条提醒设置记录,包含所有提醒开关和时间点设置。系统将根据设置的时间推送提醒通知。

2. 数据库设计

2.1 患者提醒设置表 (t_patient_reminder)

根据项目数据库表设计规范(参见 docs/OLD/DevRule/07-数据库规范.md),创建患者提醒设置表:

字段名 类型 描述
id BIGINT(20) 主键ID,使用雪花算法(MyBatis-Plus ASSIGN_ID
patient_user_id BIGINT(20) 患者用户ID,每个患者只有一条记录
is_notification_enabled TINYINT(1) 是否启用消息通知总开关 (0-禁用, 1-启用)。在 Java 实体中以 Byte 类型保存,服务层对其值作 Boolean 转换(1=true)。
is_subscription_available TINYINT(1) 一次性订阅开关,服务器每发送消息后需重新申请用户授权 (0-需要重新授权, 1-已授权可发送)。在实体中为 Byte,对外 VO 封装为 Boolean
is_blood_pressure_enabled TINYINT(1) 测量血压提醒开关 (0-禁用, 1-启用)。实体字段为 Byte,响应层转为 Boolean
blood_pressure_times TEXT 测量血压的时间点(JSON格式存储,参考用药管理功能设计文档)
is_blood_sugar_enabled TINYINT(1) 测量血糖提醒开关 (0-禁用, 1-启用)。实体字段为 Byte,响应层转为 Boolean
blood_sugar_times TEXT 测量血糖的时间点(JSON格式存储)
is_heart_rate_enabled TINYINT(1) 测量心率提醒开关 (0-禁用, 1-启用)。实体字段为 Byte,响应层转为 Boolean
heart_rate_times TEXT 测量心率的时间点(JSON格式存储)
is_medication_enabled TINYINT(1) 用药提醒开关 (0-禁用, 1-启用)。实体字段为 Byte,响应层转为 Boolean
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_reminder` (
  `id` bigint(20) NOT NULL COMMENT '主键ID',
  `patient_user_id` bigint(20) NOT NULL COMMENT '患者用户ID',
  `is_notification_enabled` tinyint(1) DEFAULT '1' COMMENT '是否启用消息通知总开关 (0-禁用, 1-启用)',
  `is_subscription_available` tinyint(1) DEFAULT '1' COMMENT '一次性订阅开关 (0-需要重新授权, 1-已授权可发送)',
  `is_blood_pressure_enabled` tinyint(1) DEFAULT '1' COMMENT '测量血压提醒开关 (0-禁用, 1-启用)',
  `blood_pressure_times` text COMMENT '测量血压的时间点(JSON格式存储)',
  `is_blood_sugar_enabled` tinyint(1) DEFAULT '0' COMMENT '测量血糖提醒开关 (0-禁用, 1-启用)',
  `blood_sugar_times` text COMMENT '测量血糖的时间点(JSON格式存储)',
  `is_heart_rate_enabled` tinyint(1) DEFAULT '1' COMMENT '测量心率提醒开关 (0-禁用, 1-启用)',
  `heart_rate_times` text COMMENT '测量心率的时间点(JSON格式存储)',
  `is_medication_enabled` tinyint(1) DEFAULT '1' COMMENT '用药提醒开关 (0-禁用, 1-启用)',
  `version` int(11) DEFAULT '0' COMMENT '版本号(乐观锁)',
  `create_user` bigint(20) DEFAULT NULL COMMENT '创建者ID',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_user` bigint(20) DEFAULT NULL COMMENT '更新者ID',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_patient_user_id` (`patient_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='患者提醒设置表';

3. 枚举类型设计

本功能设计中不使用枚举类型,所有提醒类型直接作为字段存储在患者提醒设置表中。注意:实体层字段在数据库中以 TINYINT(1) 保存(Java 实体使用 Byte),但对外 VO 使用 Boolean,并在 Service 层做相应的转换。

    return code == null ? null : ReminderType.fromCode(code);
}

@Override
public ReminderType getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    String code = rs.getString(columnIndex);
    return code == null ? null : ReminderType.fromCode(code);
}

@Override
public ReminderType getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    String code = cs.getString(columnIndex);
    return code == null ? null : ReminderType.fromCode(code);
}

}


## 4. 实体类设计

### 4.1 PO实体类

#### 4.1.1 患者提醒设置实体类 (PatientReminder.java)

java package work.baiyun.chronicdiseaseapp.model.po;

import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode;

@Schema(description = "患者提醒设置表") @TableName("t_patient_reminder") @Data @EqualsAndHashCode(callSuper = false) public class PatientReminder extends BaseEntity {

@Schema(description = "患者用户ID")
@TableField("patient_user_id")
private Long patientUserId;

@Schema(description = "是否启用消息通知总开关 (0-禁用, 1-启用)")
@TableField("is_notification_enabled")
// 注意:数据库中为 TINYINT(1),实体类 `PatientReminder` 使用 `Byte`,响应 VO 仍是 Boolean
private Boolean notificationEnabled;

@Schema(description = "一次性订阅开关 (0-需要重新授权, 1-已授权可发送)")
@TableField("is_subscription_available")
// 注意:数据库中为 TINYINT(1),实体类 `PatientReminder` 使用 `Byte`,响应 VO 仍是 Boolean
private Boolean subscriptionAvailable;

@Schema(description = "测量血压提醒开关 (0-禁用, 1-启用)")
@TableField("is_blood_pressure_enabled")
// 注意:数据库中为 TINYINT(1),实体类 `PatientReminder` 使用 `Byte`,响应 VO 仍是 Boolean
private Boolean bloodPressureEnabled;

@Schema(description = "测量血压的时间点(JSON格式存储)")
@TableField("blood_pressure_times")
private String bloodPressureTimes;

@Schema(description = "测量血糖提醒开关 (0-禁用, 1-启用)")
@TableField("is_blood_sugar_enabled")
// 注意:数据库中为 TINYINT(1),实体类 `PatientReminder` 使用 `Byte`,响应 VO 仍是 Boolean
private Boolean bloodSugarEnabled;

@Schema(description = "测量血糖的时间点(JSON格式存储)")
@TableField("blood_sugar_times")
private String bloodSugarTimes;

@Schema(description = "测量心率提醒开关 (0-禁用, 1-启用)")
@TableField("is_heart_rate_enabled")
// 注意:数据库中为 TINYINT(1),实体类 `PatientReminder` 使用 `Byte`,响应 VO 仍是 Boolean
private Boolean heartRateEnabled;

@Schema(description = "测量心率的时间点(JSON格式存储)")
@TableField("heart_rate_times")
private String heartRateTimes;

@Schema(description = "用药提醒开关 (0-禁用, 1-启用)")
@TableField("is_medication_enabled")
// 注意:数据库中为 TINYINT(1),实体类 `PatientReminder` 使用 `Byte`,响应 VO 仍是 Boolean
private Boolean medicationEnabled;

}


### 4.2 VO对象

#### 4.2.1 请求对象

java // PatientReminderRequest.java package work.baiyun.chronicdiseaseapp.model.vo;

import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.util.List; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

@Schema(description = "患者提醒设置请求") @Data public class PatientReminderRequest {

@Schema(description = "是否启用消息通知总开关")
private Boolean notificationEnabled;

@Schema(description = "一次性订阅开关")
private Boolean subscriptionAvailable;

@Schema(description = "测量血压提醒开关")
private Boolean bloodPressureEnabled;

@Schema(description = "测量血压的时间点列表")
private List<String> bloodPressureTimes;

@Schema(description = "测量血糖提醒开关")
private Boolean bloodSugarEnabled;

@Schema(description = "测量血糖的时间点列表")
private List<String> bloodSugarTimes;

@Schema(description = "测量心率提醒开关")
private Boolean heartRateEnabled;

@Schema(description = "测量心率的时间点列表")
private List<String> heartRateTimes;

@Schema(description = "用药提醒开关")
private Boolean medicationEnabled;

}

// PatientReminderQueryRequest.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 PatientReminderQueryRequest extends BaseQueryRequest {

@Schema(description = "患者用户ID")
private Long patientUserId;

@Schema(description = "是否启用消息通知总开关")
private Boolean notificationEnabled;

@Schema(description = "测量血压提醒开关")
private Boolean bloodPressureEnabled;

@Schema(description = "测量血糖提醒开关")
private Boolean bloodSugarEnabled;

@Schema(description = "测量心率提醒开关")
private Boolean heartRateEnabled;

@Schema(description = "用药提醒开关")
private Boolean medicationEnabled;

}


#### 4.2.2 响应对象

java // PatientReminderResponse.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 PatientReminderResponse {

@Schema(description = "记录ID")
private String id;

@Schema(description = "患者用户ID")
private String patientUserId;

@Schema(description = "是否启用消息通知总开关")
private Boolean notificationEnabled;

@Schema(description = "一次性订阅开关")
private Boolean subscriptionAvailable;

@Schema(description = "测量血压提醒开关")
private Boolean bloodPressureEnabled;

@Schema(description = "测量血压的时间点列表")
private List<String> bloodPressureTimes;

@Schema(description = "测量血糖提醒开关")
private Boolean bloodSugarEnabled;

@Schema(description = "测量血糖的时间点列表")
private List<String> bloodSugarTimes;

@Schema(description = "测量心率提醒开关")
private Boolean heartRateEnabled;

@Schema(description = "测量心率的时间点列表")
private List<String> heartRateTimes;

@Schema(description = "用药提醒开关")
private Boolean medicationEnabled;

@Schema(description = "创建时间")
private java.time.LocalDateTime createTime;

}

// PatientReminderOverviewResponse.java package work.baiyun.chronicdiseaseapp.model.vo;

import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data;

@Schema(description = "患者提醒概览响应") @Data public class PatientReminderOverviewResponse {

@Schema(description = "患者提醒设置")
private PatientReminderResponse reminder;

}


## 5. Mapper接口设计

java // PatientReminderMapper.java package work.baiyun.chronicdiseaseapp.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper; import work.baiyun.chronicdiseaseapp.model.po.PatientReminder; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param;

@Mapper public interface PatientReminderMapper extends BaseMapper {

PatientReminder selectByPatientUserId(@Param("patientUserId") Long patientUserId);

}


## 6. Service层设计

### 6.1 接口定义

java // PatientReminderService.java package work.baiyun.chronicdiseaseapp.service;

import work.baiyun.chronicdiseaseapp.model.vo.PatientReminderOverviewResponse; import work.baiyun.chronicdiseaseapp.model.vo.PatientReminderRequest; // Update operations are handled by savePatientReminder, no separate UpdatePatientReminderRequest implementation is required.

public interface PatientReminderService {

/**
 * 获取患者提醒概览
 */
PatientReminderOverviewResponse getReminderOverview();

/**
 * 保存患者提醒设置(创建或更新)
 */
void savePatientReminder(PatientReminderRequest request);

/**
 * 删除患者提醒设置
 */
void deletePatientReminder(Long id);

}


### 6.2 实现类

java // PatientReminderServiceImpl.java package work.baiyun.chronicdiseaseapp.service.impl;

import com.fasterxml.jackson.core.type.TypeReference; import work.baiyun.chronicdiseaseapp.util.JsonUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import work.baiyun.chronicdiseaseapp.mapper.PatientReminderMapper; import work.baiyun.chronicdiseaseapp.model.po.PatientReminder; import work.baiyun.chronicdiseaseapp.model.vo.PatientReminderOverviewResponse; import work.baiyun.chronicdiseaseapp.model.vo.PatientReminderResponse; import work.baiyun.chronicdiseaseapp.model.vo.PatientReminderRequest; import work.baiyun.chronicdiseaseapp.service.PatientReminderService; import work.baiyun.chronicdiseaseapp.util.SecurityUtils; import java.util.List;

@Service public class PatientReminderServiceImpl implements PatientReminderService {

@Autowired
private PatientReminderMapper patientReminderMapper;

@Override
public PatientReminderOverviewResponse getReminderOverview() {
    Long userId = SecurityUtils.getCurrentUserId();
    // 注意:getReminderOverview 是只读方法,不会修改提醒设置;保存逻辑和订阅清理在 savePatientReminder 中完成。

    PatientReminder patientReminder = patientReminderMapper.selectOne(
        new QueryWrapper<PatientReminder>().eq("patient_user_id", userId)
    );

    PatientReminderOverviewResponse response = new PatientReminderOverviewResponse();

    if (patientReminder != null) {
        PatientReminderResponse reminderResponse = new PatientReminderResponse();
        BeanUtils.copyProperties(patientReminder, reminderResponse);
        reminderResponse.setId(patientReminder.getId().toString());
        reminderResponse.setPatientUserId(patientReminder.getPatientUserId().toString());
        // 通过 byte->Boolean 映射:1 -> true, others -> false
        reminderResponse.setNotificationEnabled(patientReminder.getNotificationEnabled() != null && patientReminder.getNotificationEnabled() == 1);
        reminderResponse.setSubscriptionAvailable(patientReminder.getSubscriptionAvailable() != null && patientReminder.getSubscriptionAvailable() == 1);
        reminderResponse.setBloodPressureEnabled(patientReminder.getBloodPressureEnabled() != null && patientReminder.getBloodPressureEnabled() == 1);
        reminderResponse.setBloodSugarEnabled(patientReminder.getBloodSugarEnabled() != null && patientReminder.getBloodSugarEnabled() == 1);
        reminderResponse.setHeartRateEnabled(patientReminder.getHeartRateEnabled() != null && patientReminder.getHeartRateEnabled() == 1);
        reminderResponse.setMedicationEnabled(patientReminder.getMedicationEnabled() != null && patientReminder.getMedicationEnabled() == 1);
        // 时间点字段使用 JsonUtils 解析为 List<String>
        reminderResponse.setBloodPressureTimes(JsonUtils.fromJson(patientReminder.getBloodPressureTimes(), new TypeReference<List<String>>() {}));
        reminderResponse.setBloodSugarTimes(JsonUtils.fromJson(patientReminder.getBloodSugarTimes(), new TypeReference<List<String>>() {}));
        reminderResponse.setHeartRateTimes(JsonUtils.fromJson(patientReminder.getHeartRateTimes(), new TypeReference<List<String>>() {}));
        response.setReminder(reminderResponse);
    }

    return response;
}

注:若患者尚未设置提醒,getReminderOverview 将返回成功响应,但 data.remindernull。调用方应对这种情况做兼容处理(例如在前端显示 "尚未设置提醒" 的提示)。

@Override
@Transactional(rollbackFor = Exception.class)
public void savePatientReminder(PatientReminderRequest request) {
    Long userId = SecurityUtils.getCurrentUserId();

    // 检查是否已存在记录
    PatientReminder existing = patientReminderMapper.selectByPatientUserId(userId);

    if (existing == null) {
        // 创建患者提醒设置
        PatientReminder patientReminder = new PatientReminder();
        patientReminder.setPatientUserId(userId);
        // 设置默认值与类型转换(Boolean -> Byte),默认值与代码中一致
        patientReminder.setNotificationEnabled(request.getNotificationEnabled() != null ? (request.getNotificationEnabled() ? (byte)1 : (byte)0) : (byte)1);
        patientReminder.setSubscriptionAvailable(request.getSubscriptionAvailable() != null ? (request.getSubscriptionAvailable() ? (byte)1 : (byte)0) : (byte)1);
        patientReminder.setBloodPressureEnabled(request.getBloodPressureEnabled() != null ? (request.getBloodPressureEnabled() ? (byte)1 : (byte)0) : (byte)1);
        patientReminder.setBloodPressureTimes(JsonUtils.toJson(request.getBloodPressureTimes()));
        patientReminder.setBloodSugarEnabled(request.getBloodSugarEnabled() != null ? (request.getBloodSugarEnabled() ? (byte)1 : (byte)0) : (byte)0);
        patientReminder.setBloodSugarTimes(JsonUtils.toJson(request.getBloodSugarTimes()));
        patientReminder.setHeartRateEnabled(request.getHeartRateEnabled() != null ? (request.getHeartRateEnabled() ? (byte)1 : (byte)0) : (byte)1);
        patientReminder.setHeartRateTimes(JsonUtils.toJson(request.getHeartRateTimes()));
        patientReminder.setMedicationEnabled(request.getMedicationEnabled() != null ? (request.getMedicationEnabled() ? (byte)1 : (byte)0) : (byte)1);
        patientReminderMapper.insert(patientReminder);
    } else {
        // 更新提醒设置
        if (request.getNotificationEnabled() != null) {
            existing.setNotificationEnabled(request.getNotificationEnabled() ? (byte)1 : (byte)0);
        }

        if (request.getSubscriptionAvailable() != null) {
            existing.setSubscriptionAvailable(request.getSubscriptionAvailable() ? (byte)1 : (byte)0);
        }

        if (request.getBloodPressureEnabled() != null) {
            existing.setBloodPressureEnabled(request.getBloodPressureEnabled() ? (byte)1 : (byte)0);
        }

        if (request.getBloodPressureTimes() != null) {
            existing.setBloodPressureTimes(JsonUtils.toJson(request.getBloodPressureTimes()));
        }

        if (request.getBloodSugarEnabled() != null) {
            existing.setBloodSugarEnabled(request.getBloodSugarEnabled() ? (byte)1 : (byte)0);
        }

        if (request.getBloodSugarTimes() != null) {
            existing.setBloodSugarTimes(JsonUtils.toJson(request.getBloodSugarTimes()));
        }

        if (request.getHeartRateEnabled() != null) {
            existing.setHeartRateEnabled(request.getHeartRateEnabled() ? (byte)1 : (byte)0);
        }

        if (request.getHeartRateTimes() != null) {
            existing.setHeartRateTimes(JsonUtils.toJson(request.getHeartRateTimes()));
        }

        if (request.getMedicationEnabled() != null) {
            existing.setMedicationEnabled(request.getMedicationEnabled() ? (byte)1 : (byte)0);
        }

        patientReminderMapper.updateById(existing);
    }
}

@Override
@Transactional(rollbackFor = Exception.class)
public void deletePatientReminder(Long id) {
    Long userId = SecurityUtils.getCurrentUserId();

    // 检查权限
    PatientReminder patientReminder = patientReminderMapper.selectById(id);
    if (patientReminder == null) {
        throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
            work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_NOT_FOUND.getCode(),
            "患者提醒设置不存在");
    }

    if (!patientReminder.getPatientUserId().equals(userId)) {
        throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
            work.baiyun.chronicdiseaseapp.enums.ErrorCode.DATA_ACCESS_DENIED.getCode(),
            "无权操作该患者提醒设置");
    }

    // 删除提醒设置
    patientReminderMapper.deleteById(id);
}

}


## 7. Controller层设计

java // PatientReminderController.java package work.baiyun.chronicdiseaseapp.controller;

import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import work.baiyun.chronicdiseaseapp.common.R; import work.baiyun.chronicdiseaseapp.model.vo.PatientReminderOverviewResponse; import work.baiyun.chronicdiseaseapp.model.vo.PatientReminderRequest; import work.baiyun.chronicdiseaseapp.service.PatientReminderService; import work.baiyun.chronicdiseaseapp.enums.ErrorCode; import org.slf4j.Logger; import org.slf4j.LoggerFactory;

@RestController @RequestMapping("/patient-reminder") @Tag(name = "患者提醒管理", description = "患者健康提醒与用药提醒相关接口") public class PatientReminderController {

private static final Logger logger = LoggerFactory.getLogger(PatientReminderController.class);

@Autowired
private PatientReminderService patientReminderService;

@Operation(summary = "获取提醒概览", description = "获取患者的所有提醒设置概览")
@ApiResponses(value = {
    @ApiResponse(responseCode = "200", description = "成功获取提醒概览",
        content = @Content(mediaType = "application/json",
            schema = @Schema(implementation = PatientReminderOverviewResponse.class))),
    @ApiResponse(responseCode = "500", description = "服务器内部错误",
        content = @Content(mediaType = "application/json",
            schema = @Schema(implementation = Void.class)))
})
@GetMapping(path = "/overview", produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> getOverview() {
    try {
        PatientReminderOverviewResponse response = patientReminderService.getReminderOverview();
        return R.success(200, "ok", response);
    } catch (Exception e) {
        logger.error("get patient reminder overview 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)))
})
@PostMapping(path = "/save", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> savePatientReminder(@RequestBody PatientReminderRequest req) {
    try {
        patientReminderService.savePatientReminder(req);
        return R.success(200, "患者提醒设置保存成功");
    } catch (Exception e) {
        logger.error("save patient reminder 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<?> deletePatientReminder(@PathVariable Long id) {
    try {
        patientReminderService.deletePatientReminder(id);
        return R.success(200, "患者提醒设置删除成功");
    } catch (Exception e) {
        logger.error("delete patient reminder failed", e);
        return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
    }
}

}


## 8. 错误码补充

当前实现使用 `ErrorCode.DATA_NOT_FOUND`(找不到记录)和 `ErrorCode.DATA_ACCESS_DENIED`(无权访问)来返回对应错误;如果你希望更精细的错误编码(便于前端识别或自动化监控),可以添加如下错误码:

java // 在 ErrorCode.java 中添加(可选) PATIENT_REMINDER_NOT_FOUND(7000, "患者提醒记录不存在"), PATIENT_REMINDER_ACCESS_DENIED(7001, "无权访问患者提醒记录");


## 9. 接口调用示例

### 9.1 获取提醒概览

GET /patient-reminder/overview


响应示例:

json { "code": 200, "message": "ok", "data": {

"reminder": {
  "id": "1988502746686595073",
  "patientUserId": "1988172854356631553",
  "notificationEnabled": true,
  "subscriptionAvailable": true,
  "bloodPressureEnabled": true,
  "bloodPressureTimes": ["07:00", "18:00"],
  "bloodSugarEnabled": false,
  "bloodSugarTimes": ["12:00"],
  "heartRateEnabled": true,
  "heartRateTimes": ["18:00"],
  "medicationEnabled": true,
  "createTime": "2025-11-20T10:00:00"
}

}, "timestamp": 1763570208009, "requestId": "xxx", "traceId": "xxx" }


### 9.2 保存患者提醒设置

POST /patient-reminder/save { "notificationEnabled": true, "subscriptionAvailable": true, "bloodPressureEnabled": true, "bloodPressureTimes": ["07:00", "18:00"], "bloodSugarEnabled": false, "bloodSugarTimes": ["12:00"], "heartRateEnabled": true, "heartRateTimes": ["18:00"], "medicationEnabled": true }

注意:当 notificationEnabled 为 false 时,服务端会把 subscriptionAvailable 自动设置为 false;如果调用方同时传入 subscriptionAvailable=true,保存后仍会被重置为 false(以保证一致性)。


### 9.3 删除患者提醒设置

DELETE /patient-reminder/1988502746686595073 ```

10. 权限设计

  1. 患者

    • 可以查看自己的提醒设置
    • 可以创建、更新、删除自己的提醒设置
  2. 医生

    • 不能直接操作患者的提醒设置
    • 可以在患者健康档案中查看提醒设置情况
  3. 系统管理员

    • 可以查看所有患者的提醒设置
    • 可以进行系统级的提醒配置管理

11. 安全考虑

本项目安全实现应遵循 docs/OLD/DevRule/08-安全规范.md 中的已有约定,以下为与提醒管理功能相关的要点(需在实现时落地):

  1. 认证与授权

    • 使用 Token(优先 Authorization: Bearer <token>,兼容 X-Token / token)并通过 AuthInterceptor 校验,将 currentUserId/currentUserRole 注入请求上下文。
    • 仅允许患者访问自己的提醒设置。
  2. 输入校验

    • 使用 Bean Validation 注解对请求参数进行校验。
    • 对时间格式进行严格校验,确保符合 HH:mm 格式。
    • 对JSON格式的时间点列表进行有效性校验。
  3. 异常与统一错误响应

    • 使用全局异常处理器统一映射异常到 R.fail(code, message),并返回合适的 HTTP 状态码。
  4. 日志与敏感信息

    • 在日志中避免记录完整 Token 或敏感字段。
  5. SQL 注入与 ORM 使用

    • 使用 MyBatis-Plus 提供的预编译与条件构造器,禁止使用字符串拼接构建 SQL。
  6. 最小权限原则

    • 服务端对每个接口、数据读取与修改操作都进行基于角色的授权校验。
  7. 审计与监控

    • 对关键操作进行日志记录,便于审计和问题排查。