Эх сурвалжийг харах

fix(health-reminder): 修复订阅消息开关状态同步问题

- 新增文档说明微信小程序订阅消息开关状态同步问题及解决方案
- 优化开关状态变更逻辑,先更新状态再执行异步操作
- 增加对错误码20004的特殊处理,提示用户推送权限已关闭
- 完善日志输出,统一使用中文便于调试追踪
- 修复存储操作异常处理,避免因存储失败影响功能逻辑
mcbaiyun 1 сар өмнө
parent
commit
cf38d3dc05

+ 81 - 0
docs/switch-state-sync-issue.md

@@ -0,0 +1,81 @@
+# 微信小程序订阅消息开关状态同步问题及解决方案
+
+## 问题描述
+
+在微信小程序中实现订阅消息功能时,我们遇到了一个UI状态与实际功能状态不同步的问题。具体表现为:
+
+当用户在微信设置中手动关闭了订阅消息的主开关后,在小程序页面中尝试开启消息通知开关时:
+1. 调用 `requestSubscribeMessage` 接口会失败(错误码20004)
+2. 在失败回调中虽然将状态变量设置为false,但页面上的开关仍然显示为开启状态
+3. 这导致UI状态与实际功能状态不一致,给用户造成困扰
+
+## 问题分析
+
+经过分析,问题的根本原因在于开关组件的状态更新机制:
+
+1. 当用户点击开关时,开关的视觉状态会立即改变(这是浏览器/小程序框架的默认行为)
+2. 然后才执行我们绑定的事件处理函数
+3. 在事件处理函数中,如果异步操作失败,我们虽然更新了状态变量,但开关的视觉状态不会自动回滚
+
+这种行为在用户手动关闭系统权限的情况下尤为明显,因为 `requestSubscribeMessage` 会立即返回错误,而不会弹出授权弹窗给用户选择的机会。
+
+## 解决方案
+
+### 核心思路
+
+在用户点击开启开关时,主动同步更新开关状态,然后再执行异步操作,根据异步操作的结果决定是否需要回滚状态。
+
+### 实现步骤
+
+1. 用户点击开启开关
+2. 立即将开关状态变量设置为true(确保UI与用户操作一致)
+3. 调用 `requestSubscribeMessage` 接口
+4. 根据接口返回结果决定最终状态:
+   - 成功:保持开启状态
+   - 失败:将开关状态变量重置为false(UI会自动更新)
+
+### 代码示例
+
+```javascript
+const onNotificationChange = (e) => {
+  const newVal = e?.detail?.value;
+  
+  if (newVal) {
+    // 关键步骤1:先更新状态以匹配用户操作
+    notificationsEnabled.value = true;
+    
+    // 调用订阅接口
+    uni.requestSubscribeMessage({
+      tmplIds: [TEMPLATE_ID],
+      success(res) {
+        // 处理成功情况
+        // 状态已经正确,无需更改
+      },
+      fail(err) {
+        // 关键步骤2:失败时重置状态
+        notificationsEnabled.value = false;
+        // 显示错误提示
+      }
+    });
+  } else {
+    // 关闭操作直接处理
+    notificationsEnabled.value = false;
+  }
+};
+```
+
+## 最佳实践
+
+1. **主动状态管理**:不要依赖异步操作的结果来更新UI状态,而应该在用户操作时立即更新状态
+
+2. **错误回滚**:当异步操作失败时,及时回滚UI状态,确保UI与实际功能状态一致
+
+3. **用户反馈**:提供清晰的错误提示,告知用户操作失败的原因
+
+4. **权限检查**:在执行关键操作前,先检查相关权限状态,避免不必要的操作
+
+## 总结
+
+通过主动管理开关状态并在异步操作失败时及时回滚,我们成功解决了UI状态与实际功能状态不同步的问题。这种解决方案不仅适用于订阅消息开关,也适用于其他类似的异步操作场景,如权限申请、网络请求等。
+
+关键在于理解用户界面交互的基本原理:用户操作应该立即得到视觉反馈,而异步操作的结果应该只用于确认或修正这一反馈,而不是作为反馈的唯一来源。

+ 43 - 36
src/pages/health/reminder.vue

@@ -68,7 +68,7 @@ onMounted(() => {
     const val = (uni as any).getStorageSync('notificationsEnabled')
     if (typeof val === 'boolean') notificationsEnabled.value = val
   } catch (e) {
-    // ignore
+    // 忽略错误
   }
   
   // 检查用户订阅状态
@@ -77,72 +77,72 @@ onMounted(() => {
   // 尝试判断来源:优先检查页面栈的上一个页面;若无(直接打开),则检查 launch options 的 query
   try {
     const pages = (getCurrentPages as any)()
-    console.log('Current pages stack:', pages)
+    console.log('当前页面栈:', pages)
     
     if (pages && pages.length >= 1) {
       const currentPage = pages[pages.length - 1]
       const currentRoute = currentPage?.route || currentPage?.__route || currentPage?.$page?.route
       const currentOptions = currentPage?.options || {}
       
-      console.log('Current page route:', currentRoute)
-      console.log('Current page options:', currentOptions)
+      console.log('当前页面路由:', currentRoute)
+      console.log('当前页面参数:', currentOptions)
       
       // 检查当前页面是否包含 from=subscribe 参数
       if (currentOptions.from === 'subscribe') {
         entrySource.value = 'subscribe'
-        console.log('Entry source identified as subscribe via current page options')
+        console.log('通过当前页面参数识别为订阅消息来源')
       }
       
       // 检查当前页面是否包含 templateId 相关参数
       if (currentOptions.templateId === TEMPLATE_ID || currentOptions.template_id === TEMPLATE_ID) {
         entrySource.value = 'subscribe'
-        console.log('Entry source identified as subscribe via templateId in current page options')
+        console.log('通过当前页面的模板ID参数识别为订阅消息来源')
       }
     }
 
     if (pages && pages.length >= 2) {
       const prev = pages[pages.length - 2]
       const prevRoute = prev?.route || prev?.__route || prev?.$page?.route
-      console.log('Previous page route:', prevRoute)
+      console.log('上一页路由:', prevRoute)
       if (prevRoute && String(prevRoute).includes('pages/health/index')) {
         entrySource.value = 'healthIndex'
-        console.log('Entry source identified as healthIndex')
+        console.log('识别来源为健康首页')
       }
     } else {
       // 如果没有上一页信息,检查小程序启动参数(例如用户通过订阅消息打开)
       if ((uni as any).getLaunchOptionsSync) {
         const launch = (uni as any).getLaunchOptionsSync()
-        console.log('Launch options:', launch)
+        console.log('启动参数:', launch)
         if (launch && launch.query) {
-          console.log('Query params:', launch.query)
+          console.log('查询参数:', launch.query)
           // 如果你在推送消息里把 from=subscribe 作为参数传入,这里会命中
           if (launch.query.from === 'subscribe') {
             entrySource.value = 'subscribe'
-            console.log('Entry source identified as subscribe via from param')
+            console.log('通过 from 参数识别为订阅消息来源')
           }
           // 有些平台会把 templateId 或其它字段放在 query 中,可据此判断
           if (launch.query.templateId === TEMPLATE_ID || launch.query.template_id === TEMPLATE_ID) {
             entrySource.value = 'subscribe'
-            console.log('Entry source identified as subscribe via templateId param')
+            console.log('通过模板ID参数识别为订阅消息来源')
           }
         }
       }
     }
   } catch (e) {
-    console.error('Error identifying entry source:', e)
+    console.error('识别入口来源时出错:', e)
   }
   
   // 显示来源提示(用于测试),延迟一点以保证 UI 已就绪
   try {
     setTimeout(() => {
-      console.log('Entry source:', entrySource.value, entrySourceText.value)
+      console.log('入口来源:', entrySource.value, entrySourceText.value)
     }, 250)
   } catch (e) {
-    // ignore
+    // 忽略错误
   }
   
   if (entrySource.value === 'unknown') {
-    console.log('Entry source defaulted to unknown')
+    console.log('入口来源默认为未知')
   }
 })
 
@@ -155,51 +155,51 @@ const checkSubscriptionStatus = () => {
     (uni as any).getSetting({
       withSubscriptions: true, // 启用订阅设置查询
       success: (res: any) => {
-        console.log('User settings:', res)
+        console.log('用户设置:', res)
         
         // 检查订阅消息设置
         if (res.subscriptionsSetting) {
-          console.log('Subscriptions setting:', JSON.stringify(res.subscriptionsSetting, null, 2))
+          console.log('订阅设置详情:', JSON.stringify(res.subscriptionsSetting, null, 2))
           
           // 检查主开关
           const mainSwitch = res.subscriptionsSetting.mainSwitch
-          console.log('Subscription main switch:', mainSwitch)
+          console.log('订阅主开关:', mainSwitch)
           
           // 检查针对特定模板的设置
           const itemSettings = res.subscriptionsSetting.itemSettings || {}
           const templateStatus = itemSettings[TEMPLATE_ID]
-          console.log(`Template ${TEMPLATE_ID} status:`, templateStatus)
+          console.log(`模板 ${TEMPLATE_ID} 状态:`, templateStatus)
           
           // 如果主开关关闭或特定模板被拒绝,则更新本地状态
           if (mainSwitch === false || templateStatus === 'reject' || templateStatus === 'ban') {
-            console.log('Subscription disabled by user settings, updating local state')
+            console.log('用户关闭了订阅设置,正在更新本地状态')
             notificationsEnabled.value = false
             try {
               (uni as any).setStorageSync('notificationsEnabled', false)
             } catch (e) {
-              console.error('Failed to update storage:', e)
+              console.error('更新存储失败:', e)
             }
           }
           // 如果模板被接受且主开关开启,更新本地状态为true
           else if (mainSwitch === true && templateStatus === 'accept') {
-            console.log('Subscription enabled by user settings, updating local state')
+            console.log('用户开启了订阅设置,正在更新本地状态')
             notificationsEnabled.value = true
             try {
               (uni as any).setStorageSync('notificationsEnabled', true)
             } catch (e) {
-              console.error('Failed to update storage:', e)
+              console.error('更新存储失败:', e)
             }
           }
         } else {
-          console.log('No subscriptionsSetting found in response')
+          console.log('响应中未找到订阅设置')
         }
       },
       fail: (err: any) => {
-        console.error('Failed to get user settings:', err)
+        console.error('获取用户设置失败:', err)
       }
     })
   } else {
-    console.log('uni.getSetting is not available in this environment')
+    console.log('当前环境不支持 uni.getSetting')
   }
 }
 
@@ -211,27 +211,29 @@ const checkSubscriptionStatus = () => {
  */
 const onNotificationChange = (e: any) => {
   const newVal = e?.detail?.value
-  console.log('Notification switch changed to:', newVal)
+  console.log('通知开关更改为:', newVal)
   
   if (newVal) {
+    // 先将开关设置为开启状态
+    notificationsEnabled.value = true
+    
     // 请求订阅(仅在微信/小程序有效)
     ;(uni as any).requestSubscribeMessage({
       tmplIds: [TEMPLATE_ID],
       success(res: any) {
-        console.log('requestSubscribeMessage success result:', res)
+        console.log('订阅消息请求成功结果:', res)
         // res 可能形如 { "ACS7...": 'accept' }
         const result = res && res[TEMPLATE_ID]
         if (result === 'accept') {
-          console.log('User accepted the subscription')
-          notificationsEnabled.value = true
+          console.log('用户接受了订阅')
           try {
             ;(uni as any).setStorageSync('notificationsEnabled', true)
           } catch (err) {
-            // ignore storage error
+            // 忽略存储错误
           }
           uni.showToast({ title: '订阅成功', icon: 'success' })
         } else {
-          console.log('User did not accept the subscription, result:', result)
+          console.log('用户未接受订阅,结果:', result)
           // 用户拒绝或关闭了弹窗
           notificationsEnabled.value = false
           try {
@@ -241,17 +243,22 @@ const onNotificationChange = (e: any) => {
         }
       },
       fail(err: any) {
-        console.log('requestSubscribeMessage failed:', err)
+        console.log('订阅消息请求失败:', err)
         notificationsEnabled.value = false
         try {
           ;(uni as any).setStorageSync('notificationsEnabled', false)
         } catch (e) {}
-        uni.showToast({ title: '订阅请求失败', icon: 'none' })
+        // 根据错误类型显示不同的提示信息
+        if (err.errCode === 20004) {
+          uni.showToast({ title: '推送权限已关闭', icon: 'none' })
+        } else {
+          uni.showToast({ title: '订阅请求失败', icon: 'none' })
+        }
       }
     })
   } else {
     // 关闭订阅:不需要额外调用接口,只改变本地记录
-    console.log('Notification switch turned off')
+    console.log('通知开关已关闭')
     notificationsEnabled.value = false
     try {
       ;(uni as any).setStorageSync('notificationsEnabled', false)