Sfoglia il codice sorgente

feat(subscription): 引入订阅可用性枚举,优化患者提醒设置逻辑

mcbaiyun 1 settimana fa
parent
commit
7bfb8eaf6e

+ 4 - 4
docs/DB/t_patient_reminder.txt

@@ -13,13 +13,13 @@ 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_subscription_available` tinyint(1) DEFAULT '0' COMMENT '(0-需要重新授权, 1-已授权可发送一次, 2-已授权可发送无限次)',
   `is_blood_pressure_enabled` tinyint(1) DEFAULT '1' COMMENT '测量血压提醒开关 (0-禁用, 1-启用)',
-  `blood_pressure_times` text COMMENT '测量血压的时间点(JSON格式存储)',
+  `blood_pressure_times` text COLLATE utf8mb4_unicode_ci COMMENT '测量血压的时间点(JSON格式存储)',
   `is_blood_sugar_enabled` tinyint(1) DEFAULT '0' COMMENT '测量血糖提醒开关 (0-禁用, 1-启用)',
-  `blood_sugar_times` text COMMENT '测量血糖的时间点(JSON格式存储)',
+  `blood_sugar_times` text COLLATE utf8mb4_unicode_ci COMMENT '测量血糖的时间点(JSON格式存储)',
   `is_heart_rate_enabled` tinyint(1) DEFAULT '1' COMMENT '测量心率提醒开关 (0-禁用, 1-启用)',
-  `heart_rate_times` text COMMENT '测量心率的时间点(JSON格式存储)',
+  `heart_rate_times` text COLLATE utf8mb4_unicode_ci 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',

+ 24 - 0
docs/Dev/modules/站内消息功能设计文档.md

@@ -36,6 +36,30 @@
 3. 消息类型管理:使用MessageType枚举定义消息类型,通过MessageTypeHandler实现MyBatis枚举与数据库VARCHAR字段的自动转换。
 4. 推送集成参考现有微信集成,使用订阅消息。
 
+#### 订阅消息发送前授权检查
+
+在发送订阅推送(`notify_subscribe=1`)前,服务端应读取 `t_patient_reminder` 中的提醒设置,并同时判断:
+
+- `is_notification_enabled` 为 1(消息总开关开启)
+- `is_subscription_available` 为 1(订阅授权可用)
+
+若任一条件不满足,应跳过调用订阅推送接口。服务端代码中示例实现:
+
+```java
+PatientReminder pr = patientReminderMapper.selectByPatientUserId(receiverId);
+if (pr != null) {
+  if (pr.getNotificationEnabled() == null || pr.getNotificationEnabled() != 1) {
+    canSend = false;
+  }
+  if (pr.getSubscriptionAvailable() == null || pr.getSubscriptionAvailable() != 1) {
+    canSend = false;
+  }
+}
+```
+
+- 家属推送同样适用该检查。
+- 注意:消息表的 `notify_subscribe` 字段表示发送意图,实际发送仍以 `t_patient_reminder` 的授权与总开关为准。
+
 ## 接口清单
 
 | 接口路径 | 方法 | 描述 | 权限要求 | 请求体/参数 | 响应 |

+ 32 - 0
docs/Dev/patch/字段is_subscription_available的拓展调整文档.md

@@ -0,0 +1,32 @@
+# 字段 `is_subscription_available` 的现状与新增值类型对应项目调整规划
+
+## 概述
+本文为对 `t_patient_reminder.is_subscription_available` 字段的现状梳理与将其从二值(0/1)扩展到三值(0/1/2)时的项目调整规划文档(数据库保持 `TINYINT`,Java 层使用枚举映射)。
+
+---
+
+## 一、现状快速梳理
+- 数据库字段:`t_patient_reminder.is_subscription_available`,当前定义为 `TINYINT(1) DEFAULT '0'`,注释:'一次性订阅开关 (0-需要重新授权, 1-已授权可发送一次)'。
+- 当前代码中主要使用点:
+    - `api-springboot/src/main/java/work/baiyun/chronicdiseaseapp/service/impl/PatientReminderServiceImpl.java`(保存时从请求将 Boolean->Byte,读取时 Byte->Boolean 给 VO)。
+    - `api-springboot/src/main/java/work/baiyun/chronicdiseaseapp/service/impl/MessageServiceImpl.java`(发送订阅推送前检查 `pr.getNotificationEnabled()==1 && pr.getSubscriptionAvailable()==1`,当前并未在发送后消费/置 0)。
+  - 文档:已有使用说明与设计文档位于 `api-springboot/docs`(见处理目录下的汇总与变更计划文档)。
+
+---
+
+## 二、变更目标
+将 `is_subscription_available` 扩展为三值语义:
+- 0:需要重新授权(禁止发送订阅消息)
+- 1:已授权,可发送一次(发送成功后需置为 0)
+- 2:已授权,可发送无限次(不改变值)
+
+要求:数据库字段保持 `TINYINT`,在 Java 层新增枚举以表达三状态;最小化对现有前端/后端的破坏,优先保证线上行为可控。
+
+---
+
+## 三、迁移进度
+
+- 2025-12-22:已完成数据库字段注释的改动,相关说明已同步到 `docs/DB/t_patient_reminder.txt`。
+ - 2025-12-22:已在 Java 层新增 `SubscriptionAvailability` 枚举及 MyBatis `SubscriptionAvailabilityTypeHandler`,相关文件:`src/main/java/work/baiyun/chronicdiseaseapp/enums/SubscriptionAvailability.java` 与 `src/main/java/work/baiyun/chronicdiseaseapp/handler/SubscriptionAvailabilityTypeHandler.java`。
+ - 2025-12-22:已将实体/VO/Request 的 `subscriptionAvailable` 切换为枚举类型:`PatientReminder`(PO)、`PatientReminderRequest`、`PatientReminderResponse` 已改为使用 `SubscriptionAvailability`;相应的保存/读取逻辑已在 `PatientReminderServiceImpl` 中调整。
+ - 2025-12-22:已在 `MessageServiceImpl` 中改用枚举语义判断订阅可用性,并在发送成功后对一次性授权(`ONCE`)进行消费(写回 `NONE`),以实现一次性订阅的语义。

+ 43 - 0
src/main/java/work/baiyun/chronicdiseaseapp/enums/SubscriptionAvailability.java

@@ -0,0 +1,43 @@
+package work.baiyun.chronicdiseaseapp.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+
+/**
+ * 订阅可用性枚举:0=NONE(需要重新授权),1=ONCE(已授权可发送一次),2=MULTI(已授权可发送无限次)
+ */
+public enum SubscriptionAvailability {
+    NONE(0, "需要重新授权"),
+    ONCE(1, "已授权,可发送一次"),
+    MULTI(2, "已授权,可发送无限次");
+
+    @EnumValue
+    private final int code;
+    private final String description;
+
+    SubscriptionAvailability(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    @Override
+    public String toString() {
+        return description;
+    }
+
+    public static SubscriptionAvailability fromCode(int code) {
+        for (SubscriptionAvailability s : SubscriptionAvailability.values()) {
+            if (s.code == code) return s;
+        }
+        throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
+                work.baiyun.chronicdiseaseapp.enums.ErrorCode.PARAMETER_ERROR.getCode(),
+                "Unknown SubscriptionAvailability code: " + code);
+    }
+}

+ 41 - 0
src/main/java/work/baiyun/chronicdiseaseapp/handler/SubscriptionAvailabilityTypeHandler.java

@@ -0,0 +1,41 @@
+package work.baiyun.chronicdiseaseapp.handler;
+
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.MappedTypes;
+import work.baiyun.chronicdiseaseapp.enums.SubscriptionAvailability;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+@MappedTypes(SubscriptionAvailability.class)
+public class SubscriptionAvailabilityTypeHandler extends BaseTypeHandler<SubscriptionAvailability> {
+
+    @Override
+    public void setNonNullParameter(PreparedStatement ps, int i, SubscriptionAvailability parameter, JdbcType jdbcType) throws SQLException {
+        ps.setInt(i, parameter.getCode());
+    }
+
+    @Override
+    public SubscriptionAvailability getNullableResult(ResultSet rs, String columnName) throws SQLException {
+        int code = rs.getInt(columnName);
+        if (rs.wasNull()) return null;
+        return SubscriptionAvailability.fromCode(code);
+    }
+
+    @Override
+    public SubscriptionAvailability getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
+        int code = rs.getInt(columnIndex);
+        if (rs.wasNull()) return null;
+        return SubscriptionAvailability.fromCode(code);
+    }
+
+    @Override
+    public SubscriptionAvailability getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
+        int code = cs.getInt(columnIndex);
+        if (cs.wasNull()) return null;
+        return SubscriptionAvailability.fromCode(code);
+    }
+}

+ 3 - 2
src/main/java/work/baiyun/chronicdiseaseapp/model/po/PatientReminder.java

@@ -3,6 +3,7 @@ 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 work.baiyun.chronicdiseaseapp.enums.SubscriptionAvailability;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
@@ -19,9 +20,9 @@ public class PatientReminder extends BaseEntity {
     @TableField("is_notification_enabled")
     private Byte notificationEnabled;
 
-    @Schema(description = "一次性订阅开关 (0-需要重新授权, 1-已授权可发送)")
+    @Schema(description = "订阅可用性 (0=需要重新授权,1=已授权可发送一次,2=已授权可发送无限次)")
     @TableField("is_subscription_available")
-    private Byte subscriptionAvailable;
+    private SubscriptionAvailability subscriptionAvailable;
 
     @Schema(description = "测量血压提醒开关 (0-禁用, 1-启用)")
     @TableField("is_blood_pressure_enabled")

+ 3 - 2
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/PatientReminderRequest.java

@@ -3,6 +3,7 @@ package work.baiyun.chronicdiseaseapp.model.vo;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import java.util.List;
+import work.baiyun.chronicdiseaseapp.enums.SubscriptionAvailability;
 
 @Schema(description = "患者提醒设置请求")
 @Data
@@ -10,8 +11,8 @@ public class PatientReminderRequest {
     @Schema(description = "是否启用消息通知总开关")
     private Boolean notificationEnabled;
 
-    @Schema(description = "一次性订阅开关")
-    private Boolean subscriptionAvailable;
+    @Schema(description = "订阅可用性 (0=需要重新授权,1=已授权可发送一次,2=已授权可发送无限次)")
+    private SubscriptionAvailability subscriptionAvailable;
 
     @Schema(description = "测量血压提醒开关")
     private Boolean bloodPressureEnabled;

+ 3 - 2
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/PatientReminderResponse.java

@@ -3,6 +3,7 @@ package work.baiyun.chronicdiseaseapp.model.vo;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import java.util.List;
+import work.baiyun.chronicdiseaseapp.enums.SubscriptionAvailability;
 
 @Schema(description = "患者提醒设置响应")
 @Data
@@ -16,8 +17,8 @@ public class PatientReminderResponse {
     @Schema(description = "是否启用消息通知总开关")
     private Boolean notificationEnabled;
 
-    @Schema(description = "一次性订阅开关")
-    private Boolean subscriptionAvailable;
+    @Schema(description = "订阅可用性 (0=需要重新授权,1=已授权可发送一次,2=已授权可发送无限次)")
+    private SubscriptionAvailability subscriptionAvailable;
 
     @Schema(description = "测量血压提醒开关")
     private Boolean bloodPressureEnabled;

+ 19 - 4
src/main/java/work/baiyun/chronicdiseaseapp/service/impl/MessageServiceImpl.java

@@ -24,6 +24,7 @@ import work.baiyun.chronicdiseaseapp.service.UserBindingService;
 import work.baiyun.chronicdiseaseapp.util.SecurityUtils;
 import work.baiyun.chronicdiseaseapp.mapper.PatientReminderMapper;
 import work.baiyun.chronicdiseaseapp.model.po.PatientReminder;
+import work.baiyun.chronicdiseaseapp.enums.SubscriptionAvailability;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -72,14 +73,15 @@ public class MessageServiceImpl implements MessageService {
             // 推送通知(先检查患者提醒设置)
             if (message.getNotifySubscribe() == 1) {
                 boolean canSend = true;
+                PatientReminder pr = null;
                 try {
-                    PatientReminder pr = patientReminderMapper.selectByPatientUserId(receiverId);
+                    pr = patientReminderMapper.selectByPatientUserId(receiverId);
                     if (pr != null) {
                         // 如果总开关被关闭,禁止推送;如果订阅授权不可用,也禁止
                         if (pr.getNotificationEnabled() == null || pr.getNotificationEnabled() != 1) {
                             canSend = false;
                         }
-                        if (pr.getSubscriptionAvailable() == null || pr.getSubscriptionAvailable() != 1) {
+                        if (pr.getSubscriptionAvailable() == null || pr.getSubscriptionAvailable() == SubscriptionAvailability.NONE) {
                             canSend = false;
                         }
                     }
@@ -94,6 +96,12 @@ public class MessageServiceImpl implements MessageService {
                     String nickname = user != null ? user.getNickname() : "未知用户";
                     if (pushResult.get("errcode").equals(0)) {
                         pushDetails.append("已成功推送给患者").append(nickname).append("\n");
+                        try {
+                            if (pr != null && pr.getSubscriptionAvailable() == SubscriptionAvailability.ONCE) {
+                                pr.setSubscriptionAvailable(SubscriptionAvailability.NONE);
+                                patientReminderMapper.updateById(pr);
+                            }
+                        } catch (Exception ignored) {}
                     } else {
                         pushDetails.append("未能推送给患者").append(nickname).append("(无权限)\n");
                     }
@@ -127,7 +135,7 @@ public class MessageServiceImpl implements MessageService {
                                 if (prFamily.getNotificationEnabled() == null || prFamily.getNotificationEnabled() != 1) {
                                     canSendFamily = false;
                                 }
-                                if (prFamily.getSubscriptionAvailable() == null || prFamily.getSubscriptionAvailable() != 1) {
+                                if (prFamily.getSubscriptionAvailable() == null || prFamily.getSubscriptionAvailable() == SubscriptionAvailability.NONE) {
                                     canSendFamily = false;
                                 }
                             }
@@ -142,6 +150,13 @@ public class MessageServiceImpl implements MessageService {
                             String familyNickname = familyUser != null ? familyUser.getNickname() : "未知用户";
                             if (pushResultFamily.get("errcode").equals(0)) {
                                 pushDetails.append("已成功推送给患者家属").append(familyNickname).append("\n");
+                                try {
+                                    PatientReminder prFamily = patientReminderMapper.selectByPatientUserId(familyId);
+                                    if (prFamily != null && prFamily.getSubscriptionAvailable() == SubscriptionAvailability.ONCE) {
+                                        prFamily.setSubscriptionAvailable(SubscriptionAvailability.NONE);
+                                        patientReminderMapper.updateById(prFamily);
+                                    }
+                                } catch (Exception ignored) {}
                             } else {
                                 pushDetails.append("未能推送给患者家属").append(familyNickname).append("(无权限)\n");
                             }
@@ -290,7 +305,7 @@ public class MessageServiceImpl implements MessageService {
                 if (pr.getNotificationEnabled() == null || pr.getNotificationEnabled() != 1) {
                     canSendAnomaly = false;
                 }
-                if (pr.getSubscriptionAvailable() == null || pr.getSubscriptionAvailable() != 1) {
+                if (pr.getSubscriptionAvailable() == null || pr.getSubscriptionAvailable() == SubscriptionAvailability.NONE) {
                     canSendAnomaly = false;
                 }
             }

+ 5 - 4
src/main/java/work/baiyun/chronicdiseaseapp/service/impl/PatientReminderServiceImpl.java

@@ -13,6 +13,7 @@ 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 work.baiyun.chronicdiseaseapp.enums.SubscriptionAvailability;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import java.util.List;
 
@@ -39,7 +40,7 @@ public class PatientReminderServiceImpl implements PatientReminderService {
             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.setSubscriptionAvailable(patientReminder.getSubscriptionAvailable());
             reminderResponse.setBloodPressureEnabled(patientReminder.getBloodPressureEnabled() != null && patientReminder.getBloodPressureEnabled() == 1);
             reminderResponse.setBloodSugarEnabled(patientReminder.getBloodSugarEnabled() != null && patientReminder.getBloodSugarEnabled() == 1);
             reminderResponse.setHeartRateEnabled(patientReminder.getHeartRateEnabled() != null && patientReminder.getHeartRateEnabled() == 1);
@@ -60,7 +61,7 @@ public class PatientReminderServiceImpl implements PatientReminderService {
         
         // 如果通知总开关被关闭,则强制关闭一次性订阅开关
         if (request.getNotificationEnabled() != null && !request.getNotificationEnabled()) {
-            request.setSubscriptionAvailable(false);
+            request.setSubscriptionAvailable(SubscriptionAvailability.NONE);
         }
         
         // 检查是否已存在记录
@@ -71,7 +72,7 @@ public class PatientReminderServiceImpl implements PatientReminderService {
             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.setSubscriptionAvailable(request.getSubscriptionAvailable() != null ? request.getSubscriptionAvailable() : SubscriptionAvailability.ONCE);
             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);
@@ -87,7 +88,7 @@ public class PatientReminderServiceImpl implements PatientReminderService {
             }
             
             if (request.getSubscriptionAvailable() != null) {
-                existing.setSubscriptionAvailable(request.getSubscriptionAvailable() ? (byte)1 : (byte)0);
+                existing.setSubscriptionAvailable(request.getSubscriptionAvailable());
             }
             
             if (request.getBloodPressureEnabled() != null) {