Explorar o código

feat(patient-reminder): 实现患者提醒数据管理功能

- 新增患者提醒设置表(t_patient_reminder)及完整建表SQL
- 设计并实现患者提醒相关实体类(PatientReminder PO/VO)
- 实现患者提醒Mapper接口及自定义查询方法
- 开发患者提醒Service业务逻辑(增删改查)
- 创建患者提醒Controller RESTful接口
- 实现提醒设置的创建、更新、删除和查询功能
- 添加完整的API文档和调用示例
- 修复TINYINT(1)到Boolean的映射问题
- 实现JSON格式时间点存储与解析
- 添加权限控制和安全校验机制
mcbaiyun hai 1 mes
pai
achega
eac9745e6f

+ 32 - 0
docs/DB/t_patient_reminder.sql

@@ -0,0 +1,32 @@
+/*
+ * 患者提醒设置表
+ * 遵循项目数据库表设计规范:
+ * 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_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='患者提醒设置表';

+ 667 - 0
docs/New/患者提醒数据管理功能设计文档.md

@@ -0,0 +1,667 @@
+# 患者提醒数据管理功能设计文档
+
+## 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-启用) |
+| is_subscription_available | TINYINT(1) | 一次性订阅开关,服务器每发送消息后需重新申请用户授权 (0-需要重新授权, 1-已授权可发送) |
+| is_blood_pressure_enabled | TINYINT(1) | 测量血压提醒开关 (0-禁用, 1-启用) |
+| blood_pressure_times | TEXT | 测量血压的时间点(JSON格式存储,参考用药管理功能设计文档) |
+| is_blood_sugar_enabled | TINYINT(1) | 测量血糖提醒开关 (0-禁用, 1-启用) |
+| blood_sugar_times | TEXT | 测量血糖的时间点(JSON格式存储) |
+| is_heart_rate_enabled | TINYINT(1) | 测量心率提醒开关 (0-禁用, 1-启用) |
+| heart_rate_times | TEXT | 测量心率的时间点(JSON格式存储) |
+| is_medication_enabled | TINYINT(1) | 用药提醒开关 (0-禁用, 1-启用) |
+| 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_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. 枚举类型设计
+
+本功能设计中不使用枚举类型,所有提醒类型直接作为字段存储在患者提醒设置表中。
+        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")
+    private Boolean notificationEnabled;
+
+    @Schema(description = "一次性订阅开关 (0-需要重新授权, 1-已授权可发送)")
+    @TableField("is_subscription_available")
+    private Boolean subscriptionAvailable;
+
+    @Schema(description = "测量血压提醒开关 (0-禁用, 1-启用)")
+    @TableField("is_blood_pressure_enabled")
+    private Boolean bloodPressureEnabled;
+
+    @Schema(description = "测量血压的时间点(JSON格式存储)")
+    @TableField("blood_pressure_times")
+    private String bloodPressureTimes;
+
+    @Schema(description = "测量血糖提醒开关 (0-禁用, 1-启用)")
+    @TableField("is_blood_sugar_enabled")
+    private Boolean bloodSugarEnabled;
+
+    @Schema(description = "测量血糖的时间点(JSON格式存储)")
+    @TableField("blood_sugar_times")
+    private String bloodSugarTimes;
+
+    @Schema(description = "测量心率提醒开关 (0-禁用, 1-启用)")
+    @TableField("is_heart_rate_enabled")
+    private Boolean heartRateEnabled;
+
+    @Schema(description = "测量心率的时间点(JSON格式存储)")
+    @TableField("heart_rate_times")
+    private String heartRateTimes;
+
+    @Schema(description = "用药提醒开关 (0-禁用, 1-启用)")
+    @TableField("is_medication_enabled")
+    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;
+
+@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> {
+    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.CreatePatientReminderRequest;
+import work.baiyun.chronicdiseaseapp.model.vo.UpdatePatientReminderRequest;
+
+public interface PatientReminderService {
+    /**
+     * 获取患者提醒概览
+     */
+    PatientReminderOverviewResponse getReminderOverview();
+
+    /**
+     * 保存患者提醒设置(创建或更新)
+     */
+    void savePatientReminder(CreatePatientReminderRequest request);
+
+    /**
+     * 删除患者提醒设置
+     */
+    void deletePatientReminder(Long id);
+}
+```
+
+### 6.2 实现类
+
+```java
+// PatientReminderServiceImpl.java
+package work.baiyun.chronicdiseaseapp.service.impl;
+
+import com.alibaba.fastjson.JSON;
+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.CreatePatientReminderRequest;
+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();
+        
+        PatientReminder patientReminder = patientReminderMapper.selectByPatientUserId(userId);
+        
+        PatientReminderOverviewResponse response = new PatientReminderOverviewResponse();
+        
+        if (patientReminder != null) {
+            PatientReminderResponse reminderResponse = new PatientReminderResponse();
+            BeanUtils.copyProperties(patientReminder, reminderResponse);
+            reminderResponse.setId(patientReminder.getId().toString());
+            reminderResponse.setBloodPressureTimes(JSON.parseArray(patientReminder.getBloodPressureTimes(), String.class));
+            reminderResponse.setBloodSugarTimes(JSON.parseArray(patientReminder.getBloodSugarTimes(), String.class));
+            reminderResponse.setHeartRateTimes(JSON.parseArray(patientReminder.getHeartRateTimes(), String.class));
+            response.setReminder(reminderResponse);
+        }
+        
+        return response;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void savePatientReminder(CreatePatientReminderRequest request) {
+        Long userId = SecurityUtils.getCurrentUserId();
+        
+        // 检查是否已存在记录
+        PatientReminder existing = patientReminderMapper.selectByPatientUserId(userId);
+        
+        if (existing == null) {
+            // 创建患者提醒设置
+            PatientReminder patientReminder = new PatientReminder();
+            patientReminder.setPatientUserId(userId);
+            patientReminder.setNotificationEnabled(request.getNotificationEnabled() != null ? request.getNotificationEnabled() : true);
+            patientReminder.setSubscriptionAvailable(request.getSubscriptionAvailable() != null ? request.getSubscriptionAvailable() : true);
+            patientReminder.setBloodPressureEnabled(request.getBloodPressureEnabled() != null ? request.getBloodPressureEnabled() : true);
+            patientReminder.setBloodPressureTimes(JSON.toJSONString(request.getBloodPressureTimes()));
+            patientReminder.setBloodSugarEnabled(request.getBloodSugarEnabled() != null ? request.getBloodSugarEnabled() : false);
+            patientReminder.setBloodSugarTimes(JSON.toJSONString(request.getBloodSugarTimes()));
+            patientReminder.setHeartRateEnabled(request.getHeartRateEnabled() != null ? request.getHeartRateEnabled() : true);
+            patientReminder.setHeartRateTimes(JSON.toJSONString(request.getHeartRateTimes()));
+            patientReminder.setMedicationEnabled(request.getMedicationEnabled() != null ? request.getMedicationEnabled() : true);
+            patientReminderMapper.insert(patientReminder);
+        } else {
+            // 更新提醒设置
+            if (request.getNotificationEnabled() != null) {
+                existing.setNotificationEnabled(request.getNotificationEnabled());
+            }
+            
+            if (request.getSubscriptionAvailable() != null) {
+                existing.setSubscriptionAvailable(request.getSubscriptionAvailable());
+            }
+            
+            if (request.getBloodPressureEnabled() != null) {
+                existing.setBloodPressureEnabled(request.getBloodPressureEnabled());
+            }
+            
+            if (request.getBloodPressureTimes() != null) {
+                existing.setBloodPressureTimes(JSON.toJSONString(request.getBloodPressureTimes()));
+            }
+            
+            if (request.getBloodSugarEnabled() != null) {
+                existing.setBloodSugarEnabled(request.getBloodSugarEnabled());
+            }
+            
+            if (request.getBloodSugarTimes() != null) {
+                existing.setBloodSugarTimes(JSON.toJSONString(request.getBloodSugarTimes()));
+            }
+            
+            if (request.getHeartRateEnabled() != null) {
+                existing.setHeartRateEnabled(request.getHeartRateEnabled());
+            }
+            
+            if (request.getHeartRateTimes() != null) {
+                existing.setHeartRateTimes(JSON.toJSONString(request.getHeartRateTimes()));
+            }
+            
+            if (request.getMedicationEnabled() != null) {
+                existing.setMedicationEnabled(request.getMedicationEnabled());
+            }
+            
+            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.CreatePatientReminderRequest;
+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 CreatePatientReminderRequest 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 枚举中可能需要添加以下错误码:
+
+```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
+}
+```
+
+### 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. **审计与监控**:
+    - 对关键操作进行日志记录,便于审计和问题排查。

+ 188 - 0
docs/OLD/患者提醒数据管理功能修复经历.md

@@ -0,0 +1,188 @@
+# 患者提醒数据管理功能修复经历
+
+## 问题描述
+
+在患者提醒数据管理功能中,API响应中的Boolean字段(如`notificationEnabled`、`bloodPressureEnabled`等)始终返回`null`,尽管数据库中存储的是正确的TINYINT(1)值(1表示true,0表示false)。
+
+### 数据库记录示例
+```
+id: 1991527305027313666
+patient_user_id: 1988147181088956418
+is_notification_enabled: 1
+is_subscription_available: 1
+is_blood_pressure_enabled: 0
+is_blood_sugar_enabled: 0
+is_heart_rate_enabled: 0
+is_medication_enabled: 0
+```
+
+### API响应示例
+```json
+{
+  "code": 200,
+  "message": "ok",
+  "data": {
+    "reminder": {
+      "id": "1991527305027313666",
+      "patientUserId": "1988147181088956418",
+      "notificationEnabled": null,
+      "subscriptionAvailable": null,
+      "bloodPressureEnabled": null,
+      "bloodSugarEnabled": null,
+      "heartRateEnabled": null,
+      "medicationEnabled": null,
+      "bloodPressureTimes": [],
+      "bloodSugarTimes": [],
+      "heartRateTimes": [],
+      "medicationEnabled": null,
+      "createTime": "2025-11-20T23:21:25"
+    }
+  }
+}
+```
+
+## 错误思路
+
+### 1. 最初怀疑BeanUtils.copyProperties问题
+- **思路**:认为BeanUtils无法正确复制Boolean字段
+- **尝试**:手动设置Boolean字段赋值
+- **结果**:问题仍然存在,说明根本原因不在BeanUtils
+
+### 2. 怀疑MyBatis映射配置问题
+- **思路**:检查application.yml中的MyBatis配置
+- **发现**:配置正常,map-underscore-to-camel-case=true
+- **结果**:配置无问题
+
+### 3. 尝试修改SQL查询强制转换
+- **思路**:使用`CAST(field AS SIGNED)`或`field + 0`强制转换为数字
+- **尝试**:修改@Select注解中的SQL查询
+- **结果**:查询SQL改变了,但实际执行的仍然是`SELECT *`,说明可能有缓存或配置问题
+
+### 4. 怀疑字段名映射问题
+- **思路**:检查PO中的@TableField注解
+- **发现**:注解正确,字段名匹配
+- **结果**:映射配置无问题
+
+## 关键性解决思路
+
+### 核心问题识别
+通过调试日志发现,MyBatis查询确实返回了正确的数据:
+```
+==>  Preparing: SELECT * FROM t_patient_reminder WHERE patient_user_id = ?
+==> Parameters: 1988147181088956418(Long)
+<==    Columns: ..., 1, 1, 0, <<BLOB>>, 0, <<BLOB>>, 0, <<BLOB>>, 0, ...
+```
+
+但PO对象中的Boolean字段仍然为null。这说明MyBatis无法将TINYINT(1)正确映射到Java的Boolean类型。
+
+### 根本原因
+MySQL Connector/J 5.1.49版本对TINYINT(1)的处理方式与预期不符。在某些情况下,TINYINT(1)被映射为其他类型而不是Boolean。
+
+### 解决方案
+1. **改变PO字段类型**:将Boolean改为Byte类型
+2. **手动类型转换**:在Service层进行Byte到Boolean的转换
+3. **简化Mapper查询**:使用MyBatis Plus的标准查询方式
+
+## 修复过程
+
+### 第一阶段:识别问题
+1. 添加调试日志,确认数据库查询正常但PO字段为null
+2. 确认BeanUtils.copyProperties不是问题根源
+3. 确定是MyBatis映射问题
+
+### 第二阶段:尝试SQL转换
+1. 修改Mapper的@Select查询,使用CAST和+0操作
+2. 编译测试,发现实际执行的SQL未改变
+3. 怀疑有缓存或热重载问题
+
+### 第三阶段:改变字段类型
+1. 将PO中的Boolean字段改为Integer
+2. 修改Service中的转换逻辑
+3. 测试发现仍然无效
+
+### 第四阶段:使用Byte类型
+1. 将PO中的Integer字段改为Byte
+2. Byte类型更适合TINYINT(1)的映射
+3. 修改所有转换逻辑
+
+### 第五阶段:简化Mapper
+1. 移除自定义@Select注解
+2. 使用QueryWrapper进行查询
+3. 确保MyBatis Plus的自动映射生效
+
+## 最终修复代码
+
+### PO实体类修改
+```java
+// 原来
+private Boolean notificationEnabled;
+
+// 修改后
+private Byte notificationEnabled;
+```
+
+### Service转换逻辑
+```java
+// 查询时转换
+reminderResponse.setNotificationEnabled(
+    patientReminder.getNotificationEnabled() != null &&
+    patientReminder.getNotificationEnabled() == 1
+);
+
+// 保存时转换
+patientReminder.setNotificationEnabled(
+    request.getNotificationEnabled() ? (byte)1 : (byte)0
+);
+```
+
+### Mapper简化
+```java
+// 原来
+@Select("SELECT ... FROM t_patient_reminder WHERE patient_user_id = #{patientUserId}")
+PatientReminder selectByPatientUserId(@Param("patientUserId") Long patientUserId);
+
+// 修改后
+PatientReminder selectByPatientUserId(@Param("patientUserId") Long patientUserId);
+// 在Service中使用QueryWrapper查询
+```
+
+## 经验总结
+
+### 1. 调试技巧
+- **添加调试日志**:在关键位置打印对象字段值
+- **检查SQL执行**:确认实际执行的SQL和参数
+- **验证数据流**:从数据库到PO到VO的完整数据流
+
+### 2. MyBatis映射经验
+- **TINYINT(1)映射问题**:在某些JDBC驱动版本中可能无法正确映射到Boolean
+- **类型选择**:Byte类型比Integer更适合TINYINT映射
+- **避免自定义SQL**:优先使用MyBatis Plus的自动映射
+
+### 3. 问题排查思路
+- **从外到内**:先检查配置、注解,再检查类型映射
+- **分层验证**:数据库→Mapper→Service→Controller逐层验证
+- **最小化修改**:每次只修改一个变量,便于定位问题
+
+### 4. 架构设计建议
+- **类型一致性**:数据库、PO、VO的类型应保持一致或有明确转换
+- **异常处理**:对可能为null的值进行防御性编程
+- **文档同步**:代码修改后及时更新相关文档
+
+### 5. 工具使用经验
+- **QueryWrapper**:比自定义SQL更稳定可靠
+- **BeanUtils**:适用于简单对象复制,复杂转换需手动处理
+- **@TableField**:确保字段映射正确
+
+## 修复成果
+
+修复后API响应正确:
+```json
+{
+  "notificationEnabled": true,
+  "subscriptionAvailable": true,
+  "bloodPressureEnabled": false,
+  "bloodSugarEnabled": false,
+  "heartRateEnabled": false,
+  "medicationEnabled": false
+}
+```

+ 89 - 0
src/main/java/work/baiyun/chronicdiseaseapp/controller/PatientReminderController.java

@@ -0,0 +1,89 @@
+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());
+        }
+    }
+}

+ 13 - 0
src/main/java/work/baiyun/chronicdiseaseapp/mapper/PatientReminderMapper.java

@@ -0,0 +1,13 @@
+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;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface PatientReminderMapper extends BaseMapper<PatientReminder> {
+    @Select("SELECT * FROM t_patient_reminder WHERE patient_user_id = #{patientUserId}")
+    PatientReminder selectByPatientUserId(@Param("patientUserId") Long patientUserId);
+}

+ 53 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/po/PatientReminder.java

@@ -0,0 +1,53 @@
+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")
+    private Byte notificationEnabled;
+
+    @Schema(description = "一次性订阅开关 (0-需要重新授权, 1-已授权可发送)")
+    @TableField("is_subscription_available")
+    private Byte subscriptionAvailable;
+
+    @Schema(description = "测量血压提醒开关 (0-禁用, 1-启用)")
+    @TableField("is_blood_pressure_enabled")
+    private Byte bloodPressureEnabled;
+
+    @Schema(description = "测量血压的时间点(JSON格式存储)")
+    @TableField("blood_pressure_times")
+    private String bloodPressureTimes;
+
+    @Schema(description = "测量血糖提醒开关 (0-禁用, 1-启用)")
+    @TableField("is_blood_sugar_enabled")
+    private Byte bloodSugarEnabled;
+
+    @Schema(description = "测量血糖的时间点(JSON格式存储)")
+    @TableField("blood_sugar_times")
+    private String bloodSugarTimes;
+
+    @Schema(description = "测量心率提醒开关 (0-禁用, 1-启用)")
+    @TableField("is_heart_rate_enabled")
+    private Byte heartRateEnabled;
+
+    @Schema(description = "测量心率的时间点(JSON格式存储)")
+    @TableField("heart_rate_times")
+    private String heartRateTimes;
+
+    @Schema(description = "用药提醒开关 (0-禁用, 1-启用)")
+    @TableField("is_medication_enabled")
+    private Byte medicationEnabled;
+}

+ 11 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/PatientReminderOverviewResponse.java

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

+ 36 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/PatientReminderRequest.java

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

+ 45 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/PatientReminderResponse.java

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

+ 21 - 0
src/main/java/work/baiyun/chronicdiseaseapp/service/PatientReminderService.java

@@ -0,0 +1,21 @@
+package work.baiyun.chronicdiseaseapp.service;
+
+import work.baiyun.chronicdiseaseapp.model.vo.PatientReminderOverviewResponse;
+import work.baiyun.chronicdiseaseapp.model.vo.PatientReminderRequest;
+
+public interface PatientReminderService {
+    /**
+     * 获取患者提醒概览
+     */
+    PatientReminderOverviewResponse getReminderOverview();
+
+    /**
+     * 保存患者提醒设置(创建或更新)
+     */
+    void savePatientReminder(PatientReminderRequest request);
+
+    /**
+     * 删除患者提醒设置
+     */
+    void deletePatientReminder(Long id);
+}

+ 147 - 0
src/main/java/work/baiyun/chronicdiseaseapp/service/impl/PatientReminderServiceImpl.java

@@ -0,0 +1,147 @@
+package work.baiyun.chronicdiseaseapp.service.impl;
+
+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 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 work.baiyun.chronicdiseaseapp.util.JsonUtils;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import java.util.List;
+
+@Service
+public class PatientReminderServiceImpl implements PatientReminderService {
+
+    @Autowired
+    private PatientReminderMapper patientReminderMapper;
+
+    @Override
+    public PatientReminderOverviewResponse getReminderOverview() {
+        Long userId = SecurityUtils.getCurrentUserId();
+        
+        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());
+            // 手动设置Boolean字段,从Byte转换为Boolean
+            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);
+            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;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void savePatientReminder(PatientReminderRequest request) {
+        Long userId = SecurityUtils.getCurrentUserId();
+        
+        // 如果通知总开关被关闭,则强制关闭一次性订阅开关
+        if (request.getNotificationEnabled() != null && !request.getNotificationEnabled()) {
+            request.setSubscriptionAvailable(false);
+        }
+        
+        // 检查是否已存在记录
+        PatientReminder existing = patientReminderMapper.selectByPatientUserId(userId);
+        
+        if (existing == null) {
+            // 创建患者提醒设置
+            PatientReminder patientReminder = new PatientReminder();
+            patientReminder.setPatientUserId(userId);
+            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);
+    }
+}