Browse Source

feat(health): 实现订阅消息状态检测和入口来源识别

- 在健康提醒页面集成订阅状态检查功能
- 添加 checkSubscriptionStatus 方法用于检测用户订阅状态
- 使用 uni.getSetting withSubscriptions 参数获取准确的订阅信息
- 根据主开关和模板状态更新本地存储和界面显示
- 新增页面入口来源检测逻辑,优化来源识别准确性
- 添加详细的日志输出以便调试和问题排查
- 更新 UI 交互反馈,确保用户操作与状态同步
mcbaiyun 1 month ago
parent
commit
d37b2224e6
3 changed files with 285 additions and 1 deletions
  1. 131 0
      docs/page-entry-source-detection.md
  2. 86 0
      docs/subscription-status-check.md
  3. 68 1
      src/pages/health/reminder.vue

+ 131 - 0
docs/page-entry-source-detection.md

@@ -0,0 +1,131 @@
+# 小程序页面入口来源检测解决方案
+
+## 问题背景
+
+在小程序开发中,经常需要识别用户从哪个入口进入当前页面,例如:
+- 从首页跳转进入
+- 从推送消息进入
+- 从分享链接进入
+
+在实现过程中,我们可能会遇到入口来源识别不准确的问题。
+
+## 问题原因
+
+### 小程序启动和页面跳转机制
+
+当用户通过推送消息等外部链接进入小程序时,流程通常是:
+1. 小程序首先启动到默认首页(如 `pages/index/index`)
+2. 然后通过内部路由跳转到目标页面(如 `pages/health/reminder`)
+3. 在这个跳转过程中,启动参数(launch options)会丢失
+
+### 参数传递机制区别
+
+1. **启动参数(launch options)**:
+   - 仅在小程序冷启动时可用
+   - 页面跳转过程中会丢失
+   - 适用于直接打开小程序的场景
+
+2. **页面参数(options)**:
+   - 在页面跳转时传递
+   - 始终与当前页面关联
+   - 即使经过多次跳转也不会丢失(如果正确传递)
+
+## 解决方案
+
+### 1. 检查当前页面参数
+
+优先检查当前页面的参数,这是最可靠的方式:
+
+```javascript
+const pages = getCurrentPages()
+if (pages && pages.length >= 1) {
+  const currentPage = pages[pages.length - 1]
+  const currentOptions = currentPage?.options || {}
+  
+  // 检查当前页面参数
+  if (currentOptions.from === 'subscribe') {
+    // 来自订阅消息
+  }
+}
+```
+
+### 2. 检查上一页信息
+
+检查页面栈中的上一页信息:
+
+```javascript
+if (pages && pages.length >= 2) {
+  const prevPage = pages[pages.length - 2]
+  const prevRoute = prevPage?.route || prevPage?.__route || prevPage?.$page?.route
+  
+  if (prevRoute && prevRoute.includes('pages/health/index')) {
+    // 来自健康首页
+  }
+}
+```
+
+### 3. 最后检查启动参数
+
+只有在没有上一页信息时才检查启动参数:
+
+```javascript
+const launchOptions = uni.getLaunchOptionsSync()
+if (launchOptions && launchOptions.query) {
+  if (launchOptions.query.from === 'subscribe') {
+    // 来自订阅消息
+  }
+}
+```
+
+## 完整实现示例
+
+```javascript
+function detectEntrySource() {
+  try {
+    const pages = getCurrentPages()
+    
+    // 1. 检查当前页面参数(最优先)
+    if (pages && pages.length >= 1) {
+      const currentPage = pages[pages.length - 1]
+      const currentOptions = currentPage?.options || {}
+      
+      if (currentOptions.from === 'subscribe') {
+        return 'subscribe'
+      }
+    }
+
+    // 2. 检查上一页信息
+    if (pages && pages.length >= 2) {
+      const prevPage = pages[pages.length - 2]
+      const prevRoute = prevPage?.route || prevPage?.__route || prevPage?.$page?.route
+      
+      if (prevRoute && prevRoute.includes('pages/health/index')) {
+        return 'healthIndex'
+      }
+    }
+
+    // 3. 最后检查启动参数
+    const launchOptions = uni.getLaunchOptionsSync()
+    if (launchOptions && launchOptions.query) {
+      if (launchOptions.query.from === 'subscribe') {
+        return 'subscribe'
+      }
+    }
+  } catch (e) {
+    console.error('Error detecting entry source:', e)
+  }
+  
+  return 'unknown'
+}
+```
+
+## 最佳实践建议
+
+1. **优先使用页面参数**:页面参数是最可靠的来源识别方式
+2. **合理设置跳转参数**:在页面跳转时确保携带必要的参数
+3. **增加日志调试**:在调试阶段增加详细的日志输出,便于排查问题
+4. **考虑兼容性**:不同平台(微信、支付宝等)可能有细微差别,需要做兼容处理
+
+## 总结
+
+正确识别小程序页面入口来源的关键是理解小程序的启动和跳转机制,优先检查当前页面参数,而不是仅仅依赖启动参数或上一页信息。通过合理的检测顺序和详细的日志输出,可以有效解决入口来源识别不准确的问题。

+ 86 - 0
docs/subscription-status-check.md

@@ -0,0 +1,86 @@
+# 微信小程序订阅消息状态检查解决方案
+
+## 问题背景
+
+在微信小程序开发中,订阅消息是一种重要的用户触达方式。然而,开发者经常面临一个问题:如何准确地检测用户是否订阅了特定的模板消息?当用户在设置中手动关闭订阅权限时,如何在前端及时反映这一状态变化?
+
+传统的做法是依赖本地存储来记录用户订阅状态,但这存在明显的缺陷:当用户在微信设置中手动关闭订阅权限时,应用无法感知这一变化,导致界面上显示的状态与实际状态不一致。
+
+## 解决方案
+
+### 核心API:uni.getSetting withSubscriptions参数
+
+微信小程序提供了检查订阅消息状态的官方API,关键在于使用 `withSubscriptions: true` 参数:
+
+```javascript
+uni.getSetting({
+  withSubscriptions: true, // 关键参数
+  success(res) {
+    console.log(res.subscriptionsSetting);
+    // 包含 mainSwitch 和 itemSettings 字段
+  }
+});
+```
+
+### subscriptionsSetting结构说明
+
+返回的 [subscriptionsSetting](file:///D:/慢病APP/uniapp-ts/node_modules/@dcloudio/types/uni-app.d.ts#L150-L150) 对象包含以下重要字段:
+
+1. **[mainSwitch](file:///D:/慢病APP/uniapp-ts/node_modules/@dcloudio/types/uni-app.d.ts#L151-L151)**: Boolean类型,表示订阅消息的总开关
+2. **[itemSettings](file:///D:/慢病APP/uniapp-ts/node_modules/@dcloudio/types/uni-app.d.ts#L152-L152)**: 对象类型,包含每个模板ID的具体状态
+   - 模板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
+          if (mainSwitch === false || templateStatus === 'reject' || templateStatus === 'ban') {
+            // 更新为未订阅状态
+            notificationsEnabled.value = false;
+            uni.setStorageSync('notificationsEnabled', false);
+          } else if (mainSwitch === true && templateStatus === 'accept') {
+            // 更新为已订阅状态
+            notificationsEnabled.value = true;
+            uni.setStorageSync('notificationsEnabled', true);
+          }
+        }
+      },
+      fail: (err) => {
+        console.error('Failed to get user settings:', err);
+      }
+    });
+  }
+};
+```
+
+## 注意事项
+
+1. **API兼容性**:确保目标平台支持 `withSubscriptions` 参数
+2. **状态同步**:定期检查状态以确保与用户设置同步
+3. **用户体验**:当检测到订阅状态变更时,应给予用户适当提示
+4. **错误处理**:妥善处理API调用失败的情况
+
+## 总结
+
+通过使用 `uni.getSetting` 配合 `withSubscriptions: true` 参数,我们可以准确地检测用户对订阅消息的设置,实现前端独立的状态管理,无需依赖后端服务。这种方法能够实时反映用户在微信设置中的操作,提升用户体验和应用的准确性。

+ 68 - 1
src/pages/health/reminder.vue

@@ -71,6 +71,9 @@ onMounted(() => {
     // ignore
   }
   
+  // 检查用户订阅状态
+  checkSubscriptionStatus()
+  
   // 尝试判断来源:优先检查页面栈的上一个页面;若无(直接打开),则检查 launch options 的 query
   try {
     const pages = (getCurrentPages as any)()
@@ -143,6 +146,63 @@ onMounted(() => {
   }
 })
 
+/**
+ * 检查用户订阅状态
+ */
+const checkSubscriptionStatus = () => {
+  // 使用 uni.getSetting 检查用户授权状态
+  if (typeof (uni as any).getSetting === 'function') {
+    (uni as any).getSetting({
+      withSubscriptions: true, // 启用订阅设置查询
+      success: (res: any) => {
+        console.log('User settings:', res)
+        
+        // 检查订阅消息设置
+        if (res.subscriptionsSetting) {
+          console.log('Subscriptions setting:', JSON.stringify(res.subscriptionsSetting, null, 2))
+          
+          // 检查主开关
+          const mainSwitch = res.subscriptionsSetting.mainSwitch
+          console.log('Subscription main switch:', mainSwitch)
+          
+          // 检查针对特定模板的设置
+          const itemSettings = res.subscriptionsSetting.itemSettings || {}
+          const templateStatus = itemSettings[TEMPLATE_ID]
+          console.log(`Template ${TEMPLATE_ID} status:`, templateStatus)
+          
+          // 如果主开关关闭或特定模板被拒绝,则更新本地状态
+          if (mainSwitch === false || templateStatus === 'reject' || templateStatus === 'ban') {
+            console.log('Subscription disabled by user settings, updating local state')
+            notificationsEnabled.value = false
+            try {
+              (uni as any).setStorageSync('notificationsEnabled', false)
+            } catch (e) {
+              console.error('Failed to update storage:', e)
+            }
+          }
+          // 如果模板被接受且主开关开启,更新本地状态为true
+          else if (mainSwitch === true && templateStatus === 'accept') {
+            console.log('Subscription enabled by user settings, updating local state')
+            notificationsEnabled.value = true
+            try {
+              (uni as any).setStorageSync('notificationsEnabled', true)
+            } catch (e) {
+              console.error('Failed to update storage:', e)
+            }
+          }
+        } else {
+          console.log('No subscriptionsSetting found in response')
+        }
+      },
+      fail: (err: any) => {
+        console.error('Failed to get user settings:', err)
+      }
+    })
+  } else {
+    console.log('uni.getSetting is not available in this environment')
+  }
+}
+
 /**
  * 顶部总开关变更
  * - 打开时:调用 uni.requestSubscribeMessage 请求用户同意订阅 TEMPLATE_ID
@@ -151,14 +211,18 @@ onMounted(() => {
  */
 const onNotificationChange = (e: any) => {
   const newVal = e?.detail?.value
+  console.log('Notification switch changed to:', newVal)
+  
   if (newVal) {
     // 请求订阅(仅在微信/小程序有效)
     ;(uni as any).requestSubscribeMessage({
       tmplIds: [TEMPLATE_ID],
       success(res: any) {
+        console.log('requestSubscribeMessage success result:', res)
         // res 可能形如 { "ACS7...": 'accept' }
         const result = res && res[TEMPLATE_ID]
         if (result === 'accept') {
+          console.log('User accepted the subscription')
           notificationsEnabled.value = true
           try {
             ;(uni as any).setStorageSync('notificationsEnabled', true)
@@ -167,6 +231,7 @@ const onNotificationChange = (e: any) => {
           }
           uni.showToast({ title: '订阅成功', icon: 'success' })
         } else {
+          console.log('User did not accept the subscription, result:', result)
           // 用户拒绝或关闭了弹窗
           notificationsEnabled.value = false
           try {
@@ -176,6 +241,7 @@ const onNotificationChange = (e: any) => {
         }
       },
       fail(err: any) {
+        console.log('requestSubscribeMessage failed:', err)
         notificationsEnabled.value = false
         try {
           ;(uni as any).setStorageSync('notificationsEnabled', false)
@@ -185,6 +251,7 @@ const onNotificationChange = (e: any) => {
     })
   } else {
     // 关闭订阅:不需要额外调用接口,只改变本地记录
+    console.log('Notification switch turned off')
     notificationsEnabled.value = false
     try {
       ;(uni as any).setStorageSync('notificationsEnabled', false)
@@ -271,4 +338,4 @@ const toggleReminder = (index: number) => {
   color: #8a8a8a;
   margin-top: 6rpx;
 }
-</style>
+</style>