新增前端订阅模块,收敛模板 ID 到唯一来源

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuming
2026-08-16 09:25:12 +08:00
parent d8573359a2
commit 1142346e24
4 changed files with 77 additions and 2 deletions
+6 -1
View File
@@ -72,4 +72,9 @@ function person(action, data) {
return request({ url: '/api/person', data: { action, data } })
}
module.exports = { request, login, anniversary, person, BASE_URL }
// 订阅额度:grant 上报授权,get 查询余额与风险
function subscribe(action, data) {
return request({ url: '/api/subscribe', data: { action, data } })
}
module.exports = { request, login, anniversary, person, subscribe, BASE_URL }
+68
View File
@@ -0,0 +1,68 @@
/**
* 订阅消息统一入口
*
* ⚠️ 最重要的约束:wx.requestSubscribeMessage 和 wx.openSetting 必须由真实点击手势
* 直接触发(基础库 2.8.2+),且调用前不能有 await 等异步操作,否则手势上下文丢失、
* 调用必然失败(报 fail can only be invoked by user TAP gesture)。
* 所以本模块的 requestAndReport 必须在 bindtap 处理函数里同步调用,
* 用户状态要提前用 getStatus 查好缓存起来,不能现查现用。
*/
const sync = require('./sync')
const TEMPLATE_ID = '6J7Stt-lu7DKU6jblJ0nZGq_D81z5glnksf7qWfy5Yw'
/**
* 查询用户当前的订阅设置状态
* itemSettings 只包含用户勾选过「总是保持以上选择」的模板,据此可判断调用会不会弹窗。
* @returns {Promise<string>}
* silent — 已长期同意,调用不弹窗,可在任意点击上搭车静默补额度
* willPrompt — 没勾过,调用会弹窗,只在用户主动点击时才调
* rejected — 已长期拒绝或被封禁,再调也没用,需引导去设置页
* mainSwitchOff — 通知总开关关闭,需引导去设置页
* unknown — 查询失败,按 willPrompt 保守处理
*/
function getStatus() {
return new Promise((resolve) => {
wx.getSetting({
withSubscriptions: true,
success: (res) => {
const s = res.subscriptionsSetting || {}
if (s.mainSwitch === false) return resolve('mainSwitchOff')
const item = s.itemSettings && s.itemSettings[TEMPLATE_ID]
if (item === 'accept') return resolve('silent')
if (item === 'reject' || item === 'ban') return resolve('rejected')
resolve('willPrompt')
},
fail: () => resolve('unknown')
})
})
}
/**
* 请求订阅并把新增额度上报后端
* 必须在 tap handler 中同步调用(本函数第一条语句就是 wx.requestSubscribeMessage
* @returns {Promise<boolean>} 用户是否同意
*/
function requestAndReport() {
return new Promise((resolve) => {
wx.requestSubscribeMessage({
tmplIds: [TEMPLATE_ID],
success: (res) => {
const accepted = res[TEMPLATE_ID] === 'accept'
if (!accepted) return resolve(false)
// 走同步队列上报:失败会自动入队,下次启动 flush。
// 不能静默吞掉——用户已经同意、微信侧额度确实 +1 了,我们漏记会导致余额低估,
// 进而白白跳过本可以发出去的提醒。
sync.syncOrEnqueue({ kind: 'subscribe', action: 'grant', data: { count: 1 } })
.then(() => resolve(true))
},
fail: (err) => {
console.warn('[subscribe] 订阅失败', err)
resolve(false)
}
})
})
}
module.exports = { TEMPLATE_ID, getStatus, requestAndReport }
+1
View File
@@ -24,6 +24,7 @@ function enqueue(item) {
async function dispatch(item) {
if (item.kind === 'person') return api.person(item.action, item.data)
if (item.kind === 'anniversary') return api.anniversary(item.action, item.data)
if (item.kind === 'subscribe') return api.subscribe(item.action, item.data)
throw new Error('unknown sync kind: ' + item.kind)
}