Browse Source

feat(reminder): 优化订阅消息状态检查逻辑,支持主动请求订阅并同步状态

mcbaiyun 5 days ago
parent
commit
89762cfd14

+ 98 - 71
docs/微信订阅消息状态检测与同步方案.md

@@ -31,72 +31,108 @@ uni.getSetting({
    - 模板ID作为键名
    - 状态值包括:'accept'(接受)、'reject'(拒绝)、'ban'(封禁)
 
-### 实现步骤
-
-1. 在页面加载时调用检查函数
-2. 使用 `withSubscriptions: true` 参数调用 [getSetting](file:///D:/慢病APP/uniapp-ts/node_modules/@dcloudio/types/uni-app.d.ts#L4363-L4363)
-3. 解析 [subscriptionsSetting](file:///D:/慢病APP/uniapp-ts/node_modules/@dcloudio/types/uni-app.d.ts#L150-L150) 中的 [mainSwitch](file:///D:/慢病APP/uniapp-ts/node_modules/@dcloudio/types/uni-app.d.ts#L151-L151) 和 [itemSettings](file:///D:/慢病APP/uniapp-ts/node_modules/@dcloudio/types/uni-app.d.ts#L152-L152)
-4. 根据状态更新本地存储和界面显示
-
-### 代码示例
-
-```javascript
-const checkSubscriptionStatus = () => {
-  if (typeof uni.getSetting === 'function') {
-    uni.getSetting({
-      withSubscriptions: true,
-      success: (res) => {
-        if (res.subscriptionsSetting) {
-          // 检查主开关
-          const mainSwitch = res.subscriptionsSetting.mainSwitch;
-          
-          // 检查特定模板状态
-          const itemSettings = res.subscriptionsSetting.itemSettings || {};
-          const templateStatus = itemSettings[TEMPLATE_ID];
-          
-          // 根据状态更新本地存储和UI
-          // 将 templateStatus !== 'accept' 视为未授权(例如 'reject'、'ban' 或 undefined)
-          if (mainSwitch === false || templateStatus !== 'accept') {
-            // 更新为未订阅状态(将状态保存在 patientReminders.notificationsEnabled)
-            notificationsEnabled.value = false;
-            try {
-              const reminders = uni.getStorageSync('patientReminders') || {
-                bloodPressureReminder: { enabled: false, times: [] },
-                bloodSugarReminder: { enabled: false, times: [] },
-                heartRateReminder: { enabled: false, times: [] },
-                medicationReminder: { enabled: false, times: [] },
-                notificationsEnabled: false,
-                subscriptionAvailable: true
-              };
-              reminders.notificationsEnabled = false;
-              uni.setStorageSync('patientReminders', reminders);
-            } catch (e) {}
-          } else if (mainSwitch === true && templateStatus === 'accept') {
-            // 更新为已订阅状态(保存到 patientReminders)
-            notificationsEnabled.value = true;
-            try {
-              const reminders = uni.getStorageSync('patientReminders') || {
-                bloodPressureReminder: { enabled: false, times: [] },
-                bloodSugarReminder: { enabled: false, times: [] },
-                heartRateReminder: { enabled: false, times: [] },
-                medicationReminder: { enabled: false, times: [] },
-                notificationsEnabled: false,
-                subscriptionAvailable: true
-              };
-              reminders.notificationsEnabled = true;
-              uni.setStorageSync('patientReminders', reminders);
-            } catch (e) {}
-          }
-        }
-      },
-      fail: (err) => {
-        console.error('Failed to get user settings:', err);
+## 核心逻辑
+
+我们通过 `uni.getSetting({ withSubscriptions: true })` 获取微信本地状态,并与服务器返回的 `overview` 数据进行对比同步。
+
+### 状态定义
+
+- **mainSwitch**: 微信订阅消息总开关。
+- **templateStatus**: 特定模板(TEMPLATE_ID)的订阅状态('accept' 表示长期订阅)。
+- **notificationsEnabled**: 服务器记录的通知总开关。
+- **subscriptionAvailable**: 服务器记录的订阅可用性('NONE' | 'ONCE' | 'MULTI')。
+
+### 同步策略
+
+根据微信本地获取的状态,分为以下三种情况进行处理:
+
+#### 情况一:用户不允许推送消息 (mainSwitch: false)
+
+- **操作**:
+  - 检查服务器的 `notificationsEnabled`。如果是开启的(`true`),先将其设定为关闭(`false`),然后**积极尝试请求订阅**(调用 `requestSubscribeMessage`),因为这可能是用户误操作关闭的。
+  - 检查服务器的 `subscriptionAvailable`。如果不为 `"NONE"`,则修正为 `"NONE"`。
+- **目的**:确保服务器状态反映用户已关闭通知,同时尝试挽回订阅。
+
+#### 情况二:用户同意发通知,但没有长期订阅 (mainSwitch: true, templateStatus !== 'accept')
+
+- **操作**:
+  - 检查服务器的 `subscriptionAvailable`:
+    - 如果为 `"NONE"`:说明权限已消耗或未获得。**只有在服务器 `notificationsEnabled` 为 `true` 时**,才发起订阅请求。成功后根据微信侧 `getSetting` 的真实结果设定为 `"ONCE"` 或 `"MULTI"`。
+    - 如果为 `"ONCE"`:保持现状,无需重复请求。
+    - 如果为 `"MULTI"`:状态不一致(服务器认为是长期但微信本地不是),先将服务器设定为 `"NONE"`。**只有在服务器 `notificationsEnabled` 为 `true` 时**,才重新发起订阅请求。成功后根据微信侧 `getSetting` 的真实结果设定为 `"ONCE"` 或 `"MULTI"`。
+- **目的**:在用户允许通知的前提下,确保至少拥有一次发送权限,并修正不一致的长期订阅状态。同时遵循“若用户关闭通知则不主动打扰”的原则。
+
+#### 情况三:用户同意长期发送消息 (mainSwitch: true, templateStatus === 'accept')
+
+- **操作**:
+  - 确保服务器的 `subscriptionAvailable` 为 `"MULTI"`。如果不是,则进行更正。
+- **目的**:确保服务器充分利用用户的长期订阅授权。
+
+### 特别说明
+
+- **notificationsEnabled 的变更限制**:在自动同步逻辑中,只有在本地微信获取到 `mainSwitch: false` 且服务器为 `true` 时,才会将服务器状态改为 `false`。
+- **UI 状态回滚与引导**:在手动或自动触发订阅请求(`requestSubscription`)时:
+  - 若因主开关关闭导致请求失败(如 `errCode: 20004`),程序会自动将 `notificationsEnabled` 设为 `false` 并同步至服务器,同时弹出权限引导。
+  - 若用户在手动开启开关时拒绝了订阅弹窗,程序会回滚开关状态,并**强制弹出权限引导**,以确保用户了解如何恢复通知能力。
+  - **自动同步时的引导**:若服务器记录的 `notificationsEnabled` 为 `true`,但在自动检查过程中发现微信权限已关闭或订阅请求失败,程序也会弹出权限引导,帮助用户恢复预期的通知状态。
+  - 这确保了 UI 上的开关状态始终真实反映用户当前的推送接收能力,避免用户误以为通知已开启。
+
+## 代码实现参考 (src/pages/patient/health/reminder.vue)
+
+项目中实现了统一的 `checkSubscriptionStatus` 函数来执行上述逻辑:
+
+```typescript
+const checkSubscriptionStatus = async (passive = false) => {
+  const info = await fetchSubscriptionSettings()
+  const { mainSwitch, templateStatus } = info
+  
+  let needsSave = false
+  let shouldRequest = false
+  const wasEnabledBeforeCheck = notificationsEnabled.value
+
+  if (mainSwitch === false) {
+    if (notificationsEnabled.value === true) {
+      notificationsEnabled.value = false
+      needsSave = true
+      shouldRequest = true // 积极请求
+    }
+    if (subscriptionAvailable.value !== 'NONE') {
+      subscriptionAvailable.value = 'NONE'
+      needsSave = true
+    }
+  } else if (mainSwitch === true && templateStatus !== 'accept') {
+    if (subscriptionAvailable.value === 'NONE' && notificationsEnabled.value === true) {
+      shouldRequest = true
+    } else if (subscriptionAvailable.value === 'MULTI') {
+      subscriptionAvailable.value = 'NONE'
+      needsSave = true
+      if (notificationsEnabled.value === true) {
+        shouldRequest = true
       }
-    });
+    }
+  } else if (mainSwitch === true && templateStatus === 'accept') {
+    if (subscriptionAvailable.value !== 'MULTI') {
+      subscriptionAvailable.value = 'MULTI'
+      needsSave = true
+    }
+  }
+
+  if (needsSave) {
+    await saveReminders()
   }
-};
+  if (shouldRequest) {
+    await requestSubscription(false, wasEnabledBeforeCheck)
+  }
+}
 ```
 
+## 注意事项
+
+1. **触发时机**:该检查在页面 `onShow`(被动检查)和用户手动操作(主动检查)时触发。
+2. **用户交互**:`requestSubscribeMessage` 在某些平台上必须由用户点击触发。在 `onShow` 中自动触发可能会受到平台限制或弹出频率限制。
+3. **权限引导**:当用户彻底拒绝或关闭开关时,通过 `showPermissionGuide` 弹窗引导用户前往系统设置开启。
+
+
 ## 注意事项
 
 1. **API兼容性**:确保目标平台支持 `withSubscriptions` 参数
@@ -108,16 +144,7 @@ const checkSubscriptionStatus = () => {
 
 通过使用 `uni.getSetting` 配合 `withSubscriptions: true` 参数,我们可以准确地检测用户对订阅消息的设置,实现前端独立的状态管理,无需依赖后端服务。这种方法能够实时反映用户在微信设置中的操作,提升用户体验和应用的准确性。
 
-## 本项目中已做的同步变更
-
-在本仓库的 `src/pages/patient/health/reminder.vue` 中,我们把原来分散的订阅检查逻辑合并为一个统一的函数 `checkSubscriptionStatus`,并为其新增了一个 `passive` 参数,用来区分“被动检查”和“主动检查”的行为:
 
-- `checkSubscriptionStatus(passive = false)`
-  - passive = false(默认,主动检查):用于用户交互触发的场景(例如用户在设置页返回后、或用户明确请求刷新订阅状态时)。在此模式下:
-    - 如果 `mainSwitch === false` 或模板状态非 `accept`(例如 `reject`、`ban` 或未返回模板状态 `undefined`),将把 `notificationsEnabled` 设为 `false`、显示权限引导(`showPermissionGuide = true`,当主开关关闭时),并保存本地状态。
-    - 如果 `mainSwitch === true` 且模板状态为 `accept`,将把 `notificationsEnabled` 设为 `true`、关闭权限引导,并保存本地状态。
-  - passive = true(被动检查):用于生命周期或自动触发的场景(例如页面 `onShow` 时自动检测)。在此模式下:
-    - 仅在检测到 `mainSwitch === false` 或模板状态非 `accept`(例如 `reject`、`ban` 或未返回模板状态 `undefined`)时,才会把 `notificationsEnabled` 设为 `false` 并保存本地状态;不会在检测到 `accept` 时把本地开关置为 `true`,也不会显示权限引导(遵循“被动不打扰用户”的原则)。
 
 修改要点:
 

+ 5 - 2
src/api/patientReminder.ts

@@ -1,11 +1,14 @@
 import request from './request'
 
+// 订阅可用性类型(与后端保持一致)
+export type SubscriptionAvailability = 'NONE' | 'ONCE' | 'MULTI'
+
 // 患者提醒设置实体
 export interface PatientReminder {
   id: string // Snowflake 64位ID 使用字符串保存以避免 JS Number 精度丢失
   patientUserId: string // 患者用户ID
   notificationEnabled: boolean // 是否启用消息通知总开关
-  subscriptionAvailable: boolean // 一次性订阅开关
+  subscriptionAvailable: SubscriptionAvailability // 订阅可用性("NONE"|"ONCE"|"MULTI")
   bloodPressureEnabled: boolean // 测量血压提醒开关
   bloodPressureTimes: string[] // 测量血压的时间点列表
   bloodSugarEnabled: boolean // 测量血糖提醒开关
@@ -25,7 +28,7 @@ export interface PatientReminderOverviewResponse {
 // 保存患者提醒设置请求参数
 export interface SavePatientReminderRequest {
   notificationEnabled?: boolean // 是否启用消息通知总开关
-  subscriptionAvailable?: boolean // 一次性订阅开关
+  subscriptionAvailable?: SubscriptionAvailability // 订阅可用性("NONE"|"ONCE"|"MULTI")
   bloodPressureEnabled?: boolean // 测量血压提醒开关
   bloodPressureTimes?: string[] // 测量血压的时间点列表
   bloodSugarEnabled?: boolean // 测量血糖提醒开关

+ 193 - 169
src/pages/patient/health/reminder.vue

@@ -132,12 +132,21 @@
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted, onUnmounted, computed } from 'vue'
+import { ref, onMounted, computed } from 'vue'
 import { onShow, onHide } from '@dcloudio/uni-app'
 
 import CustomNav from '@/components/custom-nav.vue'
 import { getMedicationListByPatientId } from '@/api/patientMedication'
 import { getPatientReminderOverview, savePatientReminder } from '@/api/patientReminder'
+import type { SubscriptionAvailability } from '@/api/patientReminder'
+
+// fetchSubscriptionSettings 返回类型
+type FetchSubscriptionInfo = {
+  mainSwitch: boolean | null
+  templateStatus: string | null
+  raw?: any
+  error?: any
+}
 
 // 定义各个提醒项的状态
 const bloodPressureReminder = ref({
@@ -167,8 +176,8 @@ const TEMPLATE_NO = '7536'
 // 全局消息开关
 const notificationsEnabled = ref<boolean>(false)
 
-// 一次性订阅开关
-const subscriptionAvailable = ref<boolean>(true)
+// 订阅可用性('NONE' | 'ONCE' | 'MULTI')
+const subscriptionAvailable = ref<SubscriptionAvailability>('NONE')
 
 // 是否显示权限引导
 const showPermissionGuide = ref<boolean>(false)
@@ -419,30 +428,7 @@ onHide(() => {
   console.log('[reminder] 页面/应用进入后台(onHide)', { entrySource: entrySource.value })
 })
 
-// H5 平台兼容:document.visibilityState
-const handleVisibilityChange = () => {
-  try {
-    const state = (document as any).visibilityState
-    if (state === 'hidden') {
-      console.log('[reminder][visibilitychange] H5 页面变为 hidden(退到后台/切换窗口)')
-    } else if (state === 'visible') {
-      console.log('[reminder][visibilitychange] H5 页面变为 visible(回到前台)')
-    }
-  } catch (e) {
-    // 忽略在非 H5 环境下的错误
-  }
-}
-
 onMounted(() => {
-  // 注册 H5 可见性变更监听
-  try {
-    if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
-      document.addEventListener('visibilitychange', handleVisibilityChange)
-    }
-  } catch (e) {
-    // 忽略
-  }
-
   // 额外注册 uni 全局的 App 前后台事件(部分平台需要)
   try {
     if ((uni as any) && typeof (uni as any).onAppShow === 'function') {
@@ -460,16 +446,6 @@ onMounted(() => {
   }
 })
 
-onUnmounted(() => {
-  try {
-    if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
-      document.removeEventListener('visibilitychange', handleVisibilityChange)
-    }
-  } catch (e) {
-    // 忽略
-  }
-})
-
 /**
  * 加载提醒设置
  */
@@ -503,9 +479,9 @@ const loadReminders = async () => {
           times: medicationReminder.value.times // 保留已加载的用药时间点
         }
 
-        // 更新全局消息开关和一次性订阅开关
+        // 更新全局消息开关和订阅可用性(后端返回字符串 'NONE'|'ONCE'|'MULTI')
         notificationsEnabled.value = reminderData.notificationEnabled
-        subscriptionAvailable.value = reminderData.subscriptionAvailable
+        subscriptionAvailable.value = (reminderData.subscriptionAvailable as SubscriptionAvailability) || 'NONE'
 
         try {
           const remindersToSave = {
@@ -622,98 +598,187 @@ const loadMedicationTimes = async () => {
   }
 }
 
-const checkSubscriptionStatus = (passive = false) => {
-  // 使用 uni.getSetting 检查用户授权状态
-  if (typeof (uni as any).getSetting === 'function') {
-    (uni as any).getSetting({
-      withSubscriptions: true, // 启用订阅设置查询
-      success: (res: any) => {
-        console.log('[checkSubscriptionStatus] 用户设置:', res)
-        try {
-          // 额外输出可序列化的 JSON 版本,方便在日志中完整查看返回结构
-          console.log('[checkSubscriptionStatus] 用户设置(JSON):', JSON.stringify(res, null, 2))
-        } catch (err) {
-          console.error('[checkSubscriptionStatus] 无法序列化 getSetting 返回值:', err)
+/**
+ * 仅获取订阅设置(不做任何状态修改)
+ * 返回对象:{ mainSwitch: boolean|null, templateStatus: string|null, raw?: any, error?: any }
+ */
+const fetchSubscriptionSettings = async (): Promise<FetchSubscriptionInfo> => {
+  return new Promise((resolve: (v: FetchSubscriptionInfo) => void) => {
+    if (typeof (uni as any).getSetting === 'function') {
+      (uni as any).getSetting({
+        withSubscriptions: true,
+        success: (res: any) => {
+          try {
+            const subs = res.subscriptionsSetting
+            const mainSwitch = subs ? subs.mainSwitch : null
+            const itemSettings = subs ? subs.itemSettings || {} : {}
+            const templateStatus = itemSettings[TEMPLATE_ID] || null
+            resolve({ mainSwitch, templateStatus, raw: res })
+          } catch (e) {
+            resolve({ mainSwitch: null, templateStatus: null, raw: res })
+          }
+        },
+        fail: (err: any) => {
+          resolve({ mainSwitch: null, templateStatus: null, error: err })
         }
+      })
+    } else {
+      resolve({ mainSwitch: null, templateStatus: null })
+    }
+  })
+}
 
-        try {
-          // 检查订阅消息设置
-          const subs = res.subscriptionsSetting
-          if (subs) {
-            console.log('[checkSubscriptionStatus] 订阅设置详情:', JSON.stringify(subs, null, 2))
-
-            // 检查主开关
-            const mainSwitch = subs.mainSwitch
-            console.log(`[checkSubscriptionStatus] 订阅主开关状态: ${mainSwitch ? '已开启(用户允许接收通知)' : '已关闭(用户禁止接收所有通知)'}`)
-
-            // 检查针对特定模板的设置
-            const itemSettings = subs.itemSettings || {}
-            const templateStatus = itemSettings[TEMPLATE_ID]
-            console.log(`[checkSubscriptionStatus] 模板 ${TEMPLATE_ID} 状态:`, templateStatus)
-            // console.log输出subscriptionAvailable
-            console.log(`[checkSubscriptionStatus] 一次性订阅开关状态: ${subscriptionAvailable.value}`)
-
-            // 若主开关关闭或模板为 'reject'、'ban' ,则主动关闭本地通知开关
-            if (mainSwitch === false || templateStatus === 'reject' || templateStatus === 'ban') {
-              console.log('[checkSubscriptionStatus] 发现订阅被关闭或本模板被拒绝,更新本地开关并尝试同步到服务器')
-              notificationsEnabled.value = false
-              // 在主动模式下显示权限引导;被动模式(如 onShow)只更新本地并同步服务器,但不主动弹窗
-              if (!passive) {
-                showPermissionGuide.value = (mainSwitch === false)
-              }
-              try {
-                // 先更新本地存储
-                saveLocalPatientReminders()
-              } catch (e) {
-                console.error('[checkSubscriptionStatus] 更新本地存储失败:', e)
-              }
-
-              // 异步上传到服务器:不阻塞 UI,记录成功/失败日志
-              try {
-                saveReminders()
-                  .then(() => {
-                    console.log('[checkSubscriptionStatus] 已将订阅状态同步到服务器')
-                  })
-                  .catch((err: any) => {
-                    console.error('[checkSubscriptionStatus] 同步订阅状态到服务器失败:', err)
-                  })
-              } catch (e) {
-                console.error('[checkSubscriptionStatus] 调用 saveReminders 异常:', e)
-              }
-            }
-            // 主开关开启且模板被接受:仅在主动模式下将本地开关置为 true 并关闭引导
-            else if (!passive && mainSwitch === true && templateStatus === 'accept') {
-              console.log('[checkSubscriptionStatus] 用户开启了订阅设置,正在更新本地状态并同步到服务器')
-              notificationsEnabled.value = true
-              showPermissionGuide.value = false
-              try {
-                saveLocalPatientReminders()
-              } catch (e) {
-                console.error('[checkSubscriptionStatus] 更新本地存储失败:', e)
-              }
-              try {
-                saveReminders()
-                  .then(() => console.log('[checkSubscriptionStatus] 同步订阅开启状态到服务器成功'))
-                  .catch((err: any) => console.error('[checkSubscriptionStatus] 同步订阅开启状态到服务器失败:', err))
-              } catch (e) {
-                console.error('[checkSubscriptionStatus] 调用 saveReminders 异常:', e)
-              }
-            } else {
-              console.log('[checkSubscriptionStatus] 订阅设置未发现需要变更,本次检查为被动=' + passive)
-            }
+/**
+ * 积极请求订阅消息
+ * 逻辑:调用微信订阅接口,成功后根据 getSetting 结果更新为 ONCE 或 MULTI
+ * @param manual 是否为用户手动触发(手动触发时若拒绝则强制显示权限引导)
+ * @param wasEnabled 调用前服务器/本地记录的通知开关状态(用于在自动同步失败时决定是否显示引导)
+ */
+const requestSubscription = async (manual = false, wasEnabled = false) => {
+  return new Promise<void>((resolve) => {
+    console.log(`[requestSubscription] 正在发起订阅请求... (manual=${manual}, wasEnabled=${wasEnabled})`)
+    if (typeof (uni as any).requestSubscribeMessage !== 'function') {
+      console.warn('[requestSubscription] 当前环境不支持 requestSubscribeMessage')
+      resolve()
+      return
+    }
+
+    (uni as any).requestSubscribeMessage({
+      tmplIds: [TEMPLATE_ID],
+      success: async (res: any) => {
+        console.log('[requestSubscription] 订阅请求成功结果:', res)
+        const result = res && res[TEMPLATE_ID]
+        if (result === 'accept') {
+          // 订阅成功,进一步检查是长期还是单次
+          const info = await fetchSubscriptionSettings()
+          if (info.mainSwitch === true && info.templateStatus === 'accept') {
+            subscriptionAvailable.value = 'MULTI'
           } else {
-            console.log('[checkSubscriptionStatus] 响应中未找到订阅设置')
+            subscriptionAvailable.value = 'ONCE'
+          }
+          uni.showToast({ title: '订阅成功', icon: 'success' })
+          showPermissionGuide.value = false
+        } else {
+          // 用户拒绝或关闭
+          notificationsEnabled.value = false
+          subscriptionAvailable.value = 'NONE'
+          uni.showToast({ title: '订阅被拒绝', icon: 'none' })
+          // 如果是手动触发且被拒绝,或者主开关关闭,或者原本是开启状态但现在失败了,则显示引导
+          const info = await fetchSubscriptionSettings()
+          if (info.mainSwitch === false || manual || wasEnabled) {
+            showPermissionGuide.value = true
           }
-        } catch (e) {
-          console.error('[checkSubscriptionStatus] 处理返回值时出错:', e)
         }
+        saveLocalPatientReminders()
+        await saveReminders()
+        resolve()
       },
-      fail: (err: any) => {
-        console.error('[checkSubscriptionStatus] 获取用户设置失败:', err)
+      fail: async (err: any) => {
+        console.log('[requestSubscription] 订阅请求失败:', err)
+        // 订阅请求失败,确保 UI 开关状态回滚为关闭
+        notificationsEnabled.value = false
+        // 根据错误类型显示不同的提示信息
+        if (err.errCode === 20004) {
+          uni.showToast({ title: '推送权限已关闭', icon: 'none' })
+          showPermissionGuide.value = true
+        } else {
+          uni.showToast({ title: '订阅请求失败', icon: 'none' })
+          // 手动触发失败或原本开启但现在失败,也尝试显示引导
+          if (manual || wasEnabled) showPermissionGuide.value = true
+        }
+        saveLocalPatientReminders()
+        await saveReminders()
+        resolve()
       }
     })
-  } else {
-    console.log('当前环境不支持 uni.getSetting')
+  })
+}
+
+/**
+ * 检查订阅/消息权限状态并同步到服务器
+ * 逻辑要求:
+ * 1. 本地微信 mainSwitch: false
+ *    - 若服务器 notificationsEnabled 为 true,则设为 false 并积极请求订阅
+ *    - 若服务器 subscriptionAvailable 不为 NONE,则设为 NONE
+ * 2. 本地微信 mainSwitch: true 但无长期订阅 (templateStatus !== 'accept')
+ *    - 若服务器 subscriptionAvailable 为 NONE,请求订阅,成功后设为 ONCE
+ *    - 若服务器 subscriptionAvailable 为 MULTI,设为 NONE 并重新请求订阅
+ * 3. 本地微信 mainSwitch: true 且有长期订阅 (templateStatus === 'accept')
+ *    - 确保服务器 subscriptionAvailable 为 MULTI
+ */
+const checkSubscriptionStatus = async (passive = false) => {
+  try {
+    const info = await fetchSubscriptionSettings()
+    const mainSwitch = info.mainSwitch
+    const templateStatus = info.templateStatus
+
+    console.log(`[checkSubscriptionStatus] 微信状态: mainSwitch=${mainSwitch}, templateStatus=${templateStatus}`)
+    console.log(`[checkSubscriptionStatus] 服务器状态: notificationsEnabled=${notificationsEnabled.value}, subscriptionAvailable=${subscriptionAvailable.value}`)
+
+    let needsSave = false
+    let shouldRequest = false
+
+    // 记录检查前的开启状态,用于在自动同步失败时决定是否显示引导
+    const wasEnabledBeforeCheck = notificationsEnabled.value
+
+    // 情况一:用户不允许推送消息 (mainSwitch: false)
+    if (mainSwitch === false) {
+      // 检查服务器的 notificationsEnabled,如果是开启的,先设定为关闭
+      if (notificationsEnabled.value === true) {
+        console.log('[checkSubscriptionStatus] Case 1: mainSwitch 为 false 但服务器开启,正在关闭并请求订阅')
+        notificationsEnabled.value = false
+        needsSave = true
+        // 积极尝试请求订阅
+        shouldRequest = true
+      }
+      // 检查服务器的 subscriptionAvailable,如果不为 "NONE",需要设定为 NONE
+      if (subscriptionAvailable.value !== 'NONE') {
+        console.log('[checkSubscriptionStatus] Case 1: subscriptionAvailable 不为 NONE,修正为 NONE')
+        subscriptionAvailable.value = 'NONE'
+        needsSave = true
+      }
+    }
+    // 情况二:用户同意发通知,但没有长期订阅 (mainSwitch: true, templateStatus !== 'accept')
+    else if (mainSwitch === true && templateStatus !== 'accept') {
+      // 如果为 "NONE",说明被服务器标记为消耗了,需要请求订阅
+      // 修改点:只有在 notificationsEnabled 为 true 时才发起请求
+      if (subscriptionAvailable.value === 'NONE' && notificationsEnabled.value === true) {
+        console.log('[checkSubscriptionStatus] Case 2: subscriptionAvailable 为 NONE 且开关开启,发起订阅请求')
+        shouldRequest = true
+      }
+      // 如果服务器的 subscriptionAvailable 原本是 MULTI,那么就不对,先把服务器的设定为 NONE 再去请求
+      else if (subscriptionAvailable.value === 'MULTI') {
+        console.log('[checkSubscriptionStatus] Case 2: subscriptionAvailable 为 MULTI 但微信无长期订阅,修正为 NONE')
+        subscriptionAvailable.value = 'NONE'
+        needsSave = true
+        // 只有在开关开启时才请求
+        if (notificationsEnabled.value === true) {
+          shouldRequest = true
+        }
+      }
+      // 如果是 ONCE,则不做处理
+    }
+    // 情况三:用户同意长期发送消息 (mainSwitch: true, templateStatus === 'accept')
+    else if (mainSwitch === true && templateStatus === 'accept') {
+      // 保证远程服务器的 subscriptionAvailable 为 MULTI
+      if (subscriptionAvailable.value !== 'MULTI') {
+        console.log('[checkSubscriptionStatus] Case 3: 微信有长期订阅但服务器不是 MULTI,修正为 MULTI')
+        subscriptionAvailable.value = 'MULTI'
+        needsSave = true
+      }
+    }
+
+    if (needsSave) {
+      saveLocalPatientReminders()
+      await saveReminders()
+    }
+
+    // 如果需要请求订阅
+    if (shouldRequest) {
+      await requestSubscription(false, wasEnabledBeforeCheck)
+    }
+  } catch (e) {
+    console.error('[checkSubscriptionStatus] 检查订阅状态失败:', e)
   }
 }
 
@@ -759,54 +824,13 @@ const onNotificationChange = (e: any) => {
   console.log('通知开关更改为:', newVal)
 
   if (newVal) {
-  // 先将开关设置为开启状态
-  notificationsEnabled.value = true
-  try { saveLocalPatientReminders() } catch (e) { /* ignore */ }
-
-      // 请求订阅(仅在微信/小程序有效)
-      ; (uni as any).requestSubscribeMessage({
-        tmplIds: [TEMPLATE_ID],
-        success(res: any) {
-          console.log('订阅消息请求成功结果:', res)
-          // res 可能形如 { "ACS7...": 'accept' }
-          const result = res && res[TEMPLATE_ID]
-          if (result === 'accept') {
-            console.log('用户接受了订阅')
-            // 设置一次性订阅开关为可用
-            subscriptionAvailable.value = true
-            // 用户手动同意订阅并保存
-            saveReminders()
-            uni.showToast({ title: '订阅成功', icon: 'success' })
-            // 隐藏权限引导
-            showPermissionGuide.value = false
-          } else {
-            console.log('用户未接受订阅,结果:', result)
-            // 用户拒绝或关闭了弹窗
-            notificationsEnabled.value = false
-            // 用户主动拒绝订阅:更新并保存状态
-            saveReminders()
-            uni.showToast({ title: '订阅被拒绝', icon: 'none' })
-            // 显示权限引导
-            showPermissionGuide.value = true
-          }
-        },
-        fail(err: any) {
-          console.log('订阅消息请求失败:', err)
-          notificationsEnabled.value = false
-          // 请求失败视为未开启订阅(非被动检查),认为是用户操作失败,保存当前状态
-          saveReminders()
-          // 根据错误类型显示不同的提示信息
-          if (err.errCode === 20004) {
-            uni.showToast({ title: '推送权限已关闭', icon: 'none' })
-            // 显示权限引导
-            showPermissionGuide.value = true
-          } else {
-            uni.showToast({ title: '订阅请求失败', icon: 'none' })
-          }
-        }
-      })
+    // 先将开关设置为开启状态
+    notificationsEnabled.value = true
+    saveLocalPatientReminders()
+    // 调用统一的请求订阅函数(手动触发)
+    requestSubscription(true)
   } else {
-    // 关闭订阅:不需要额外调用接口,只改变本地记录
+    // 关闭订阅
     console.log('通知开关已关闭')
     notificationsEnabled.value = false
     // 用户手动关闭开关 -> 保存当前状态