Fix subscription renewal and partner notification delivery
This commit is contained in:
@@ -311,7 +311,7 @@ Page(withDataCache(withListPagination({
|
||||
var failReasons = [];
|
||||
|
||||
// ★ 在用户点击手势上下文中提前请求订阅消息(符合微信规范)
|
||||
requestSubscribeAfterAction('photo');
|
||||
await requestSubscribeAfterAction('photo');
|
||||
|
||||
try {
|
||||
// 上传前先预热 session,确保认证状态有效
|
||||
|
||||
@@ -171,12 +171,12 @@ Page(withDataCache({
|
||||
|
||||
submitCheck: async function() {
|
||||
var item = this.data.item;
|
||||
if (!item) return;
|
||||
|
||||
// ★ 在用户点击手势上下文中提前请求订阅消息(符合微信规范)
|
||||
requestSubscribeAfterAction('checkin');
|
||||
if (!item || this._submittingCheck) return;
|
||||
this._submittingCheck = true;
|
||||
|
||||
try {
|
||||
// 为本人接收TA的提醒补充授权。
|
||||
await requestSubscribeAfterAction('checkin');
|
||||
var photoPath = '';
|
||||
if (this.data.checkPhoto) {
|
||||
wx.showLoading({ title: '上传中...' });
|
||||
@@ -206,7 +206,8 @@ Page(withDataCache({
|
||||
wx.showToast({ title: toast, icon: 'success' });
|
||||
// 刷新详情数据
|
||||
this.loadDetail(item.id);
|
||||
} catch(e) { wx.showToast({ title: e.error || '打卡失败', icon: 'none' }); }
|
||||
} catch(e) { wx.hideLoading(); wx.showToast({ title: e.error || '打卡失败', icon: 'none' }); }
|
||||
finally { this._submittingCheck = false; }
|
||||
},
|
||||
|
||||
hideCheckModal: function() { this.setData({ showCheckModal: false }); },
|
||||
|
||||
@@ -886,7 +886,7 @@ Page(withDataCache({
|
||||
if (!inviteTargetId) { wx.showToast({ title: '请输入对方ID', icon: 'none' }); return; }
|
||||
if (!/^[0-9]{6}$/.test(inviteTargetId)) { wx.showToast({ title: 'ID为6位数字', icon: 'none' }); return; }
|
||||
// 邀请成功且对方接受时,需要通知邀请人,提前申请一次性订阅授权
|
||||
setTimeout(function() { requestSubscribe('notify').catch(function() {}); }, 1500);
|
||||
requestSubscribe('notify').catch(function() {});
|
||||
try {
|
||||
wx.showLoading({ title: '发送中...' });
|
||||
const res = await api.invitePartner({ target_user_id: inviteTargetId, message: inviteMessage });
|
||||
|
||||
@@ -353,14 +353,14 @@ Page({
|
||||
}
|
||||
|
||||
// ★ 在用户点击手势上下文中提前请求订阅消息(符合微信规范)
|
||||
// 订阅弹窗不阻塞后续异步操作,用户拒绝也不影响发布
|
||||
// 为本人接收TA的提醒补充授权,拒绝或保存失败也不影响发布
|
||||
var sceneMap = {
|
||||
diary: 'diary', album: 'photo', checkin: 'checkin',
|
||||
timeline: 'timeline', song: 'song'
|
||||
};
|
||||
var scene = sceneMap[type];
|
||||
if (scene) {
|
||||
requestSubscribeAfterAction(scene);
|
||||
await requestSubscribeAfterAction(scene);
|
||||
}
|
||||
|
||||
// 再执行异步操作(上传/API调用)
|
||||
|
||||
@@ -154,6 +154,24 @@ Page({
|
||||
this._subscribing = false;
|
||||
},
|
||||
|
||||
renewSubscribe: async function() {
|
||||
if (this._subscribing) return;
|
||||
this._subscribing = true;
|
||||
try {
|
||||
const results = await requestSubscribe('notify');
|
||||
if (results.notify) {
|
||||
this.setData({ subscribeChecked: true });
|
||||
wx.showToast({ title: '已订阅一次提醒', icon: 'success' });
|
||||
} else {
|
||||
wx.showToast({ title: '本次未授权提醒', icon: 'none' });
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '订阅失败,请重试', icon: 'none' });
|
||||
} finally {
|
||||
this._subscribing = false;
|
||||
}
|
||||
},
|
||||
|
||||
// ===== 背景图 =====
|
||||
showBgModal: async function() {
|
||||
this.setData({ showBgModal: true, bgPreview: '', bgFilePath: '' });
|
||||
|
||||
@@ -144,20 +144,23 @@
|
||||
<view class="subscribe-tip {{subscribeChecked ? 'on' : 'off'}}">
|
||||
<block wx:if="{{subscribeChecked}}">
|
||||
<text class="tip-icon">✅</text>
|
||||
<text class="tip-text">通知已开启,TA的操作会第一时间通知你</text>
|
||||
<text class="tip-text">已开启接收提醒,订阅次数用完后需要再次授权</text>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<text class="tip-icon">💡</text>
|
||||
<text class="tip-text">开启后即可收到TA的操作通知</text>
|
||||
<text class="tip-text">开启并授权后,可接收一次TA的操作提醒</text>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<button wx:if="{{subscribeChecked}}" class="btn btn-primary" bindtap="renewSubscribe">订阅一次 TA 的提醒</button>
|
||||
|
||||
<!-- 说明 -->
|
||||
<view class="subscribe-note">
|
||||
<text class="note-title">📌 温馨提示</text>
|
||||
<text class="note-item">• 通知通过微信「服务通知」推送</text>
|
||||
<text class="note-item">• 每次操作后可能会请求一次订阅授权</text>
|
||||
<text class="note-item">• 若收不到通知,可重新开启此开关</text>
|
||||
<text class="note-item">• 你授权的是自己接收TA提醒的次数,不能替TA授权</text>
|
||||
<text class="note-item">• 每次授权可接收一次提醒,用完后可点击上方按钮续订</text>
|
||||
<text class="note-item">• 勾选「总是保持以上选择」后,后续点击授权可免重复弹窗,仍需逐次申请</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
const test=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const path=require('node:path');
|
||||
const template='tOaqM_Z6pNSih2pVpaUmP86MgwnDT5ZlwOqd0lgmXP8';
|
||||
function setup(save){
|
||||
const storage={coupleId:10},calls=[];
|
||||
const wx={getStorageSync:key=>storage[key],setStorageSync:(key,v)=>storage[key]=v,requestSubscribeMessage:o=>calls.push(o)};
|
||||
const load=require('./load-module.cjs')({wx,console:{warn(){},error(){}}},{'./request':{api:{subscribeTemplate:save}}});
|
||||
return {helper:load(path.join(__dirname,'../utils/subscribe.js')),storage,calls};
|
||||
}
|
||||
test('authorization is immediate and concurrent calls share one prompt until persistence finishes',async()=>{
|
||||
let complete,saves=0;const e=setup(()=>{saves++;return new Promise(resolve=>complete=resolve)});
|
||||
const first=e.helper.requestSubscribe('notify');assert.equal(e.calls.length,1);
|
||||
assert.equal(e.helper.requestSubscribe('notify'),first);
|
||||
e.calls[0].success({[template]:'accept'});
|
||||
let finished=false;first.then(()=>finished=true);await Promise.resolve();
|
||||
assert.equal(finished,false);assert.equal(e.storage.subscribed_notify,undefined);
|
||||
complete({success:true});const result=await first;assert.equal(result.notify,true);assert.equal(saves,1);assert.equal(e.storage.subscribed_notify,true);
|
||||
});
|
||||
test('failed persistence never reports successful subscription and a later tap can retry',async()=>{
|
||||
let fail=true;const e=setup(async()=>{if(fail)throw Error('offline');return {success:true}});
|
||||
let pending=e.helper.requestSubscribe('notify');e.calls[0].success({[template]:'accept'});
|
||||
await assert.rejects(pending,/offline/);assert.equal(e.storage.subscribed_notify,undefined);
|
||||
fail=false;pending=e.helper.requestSubscribe('notify');assert.equal(e.calls.length,2);e.calls[1].success({[template]:'accept'});
|
||||
assert.equal((await pending).notify,true);
|
||||
});
|
||||
test('each subsequent user action can replenish authorization; denial does not prevent later attempts',async()=>{
|
||||
const e=setup(async()=>({success:true}));
|
||||
let pending=e.helper.requestSubscribeAfterAction('diary');assert.equal(e.calls.length,1);e.calls[0].success({[template]:'reject'});await pending;
|
||||
pending=e.helper.requestSubscribeAfterAction('photo');assert.equal(e.calls.length,2);e.calls[1].success({[template]:'accept'});await pending;
|
||||
pending=e.helper.requestSubscribeAfterAction('checkin');assert.equal(e.calls.length,3);e.calls[2].fail(Error('cancelled'));await pending;
|
||||
pending=e.helper.requestSubscribeAfterAction('checkin');assert.equal(e.calls.length,4);e.calls[3].success({[template]:'accept'});await pending;
|
||||
});
|
||||
test('explicitly disabled notifications and unbound users do not prompt during business actions',async()=>{
|
||||
const e=setup(async()=>({success:true}));e.storage.subscribed_notify=false;await e.helper.requestSubscribeAfterAction('diary');assert.equal(e.calls.length,0);
|
||||
delete e.storage.subscribed_notify;delete e.storage.coupleId;await e.helper.requestSubscribeAfterAction('diary');assert.equal(e.calls.length,0);
|
||||
});
|
||||
|
||||
test('manual renewal requests authorization again without disabling existing notifications',async()=>{
|
||||
let page, calls=0, cancelled=0, complete;
|
||||
const loader=require('./load-module.cjs')({Page:p=>page=p,wx:{showToast(){}}},{
|
||||
'../../utils/layout':{getLayout:()=>({})},
|
||||
'../../utils/request':{api:{unsubscribeTemplate:()=>cancelled++}},
|
||||
'../../utils/subscribe':{requestSubscribe:()=>{calls++;return new Promise(resolve=>complete=resolve)},getSubscribeStatus:async()=>({notify:true})}
|
||||
});
|
||||
loader(path.join(__dirname,'../pages/settings/settings.js'));
|
||||
const instance={data:{subscribeChecked:true},setData(v){Object.assign(this.data,v)}};
|
||||
let pending=page.renewSubscribe.call(instance);
|
||||
assert.equal(calls,1);await page.renewSubscribe.call(instance);assert.equal(calls,1);
|
||||
complete({notify:true});await pending;assert.equal(instance.data.subscribeChecked,true);assert.equal(cancelled,0);
|
||||
pending=page.renewSubscribe.call(instance);assert.equal(calls,2);complete({notify:false});await pending;
|
||||
assert.equal(instance.data.subscribeChecked,true);assert.equal(instance._subscribing,false);
|
||||
});
|
||||
+20
-54
@@ -2,7 +2,7 @@
|
||||
* 微信订阅消息工具
|
||||
* 统一管理订阅消息的请求和状态
|
||||
*
|
||||
* 订阅状态以服务端接口为准
|
||||
* 服务端记录的是授权登记,不代表微信侧仍有剩余次数。
|
||||
*/
|
||||
const { api } = require('./request');
|
||||
|
||||
@@ -11,8 +11,8 @@ const SUBSCRIBE_TEMPLATES = {
|
||||
notify: 'tOaqM_Z6pNSih2pVpaUmP86MgwnDT5ZlwOqd0lgmXP8',
|
||||
};
|
||||
|
||||
// 防重复标记:同一会话中已请求过订阅,不再重复弹窗
|
||||
let _requestedInSession = false;
|
||||
// 只合并正在进行的授权请求,后续用户点击仍可补充一次性授权。
|
||||
let _pendingSubscribe = null;
|
||||
|
||||
/**
|
||||
* 请求用户订阅指定模板
|
||||
@@ -20,7 +20,8 @@ let _requestedInSession = false;
|
||||
* @returns {Promise<object>} 订阅结果 { templateType: true/false }
|
||||
*/
|
||||
function requestSubscribe(templateTypes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (_pendingSubscribe) return _pendingSubscribe;
|
||||
const pending = new Promise((resolve, reject) => {
|
||||
if (!Array.isArray(templateTypes)) {
|
||||
templateTypes = [templateTypes];
|
||||
}
|
||||
@@ -48,9 +49,7 @@ function requestSubscribe(templateTypes) {
|
||||
});
|
||||
|
||||
// 将授权结果同步到后端
|
||||
syncSubscribeResults(results);
|
||||
|
||||
resolve(results);
|
||||
syncSubscribeResults(results).then(() => resolve(results), reject);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[subscribe] 请求订阅失败:', err);
|
||||
@@ -58,62 +57,29 @@ function requestSubscribe(templateTypes) {
|
||||
}
|
||||
});
|
||||
});
|
||||
_pendingSubscribe = pending.finally(() => { _pendingSubscribe = null; });
|
||||
return _pendingSubscribe;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将订阅结果同步到后端
|
||||
*/
|
||||
function syncSubscribeResults(results) {
|
||||
Object.keys(results).forEach((type) => {
|
||||
const accepted = results[type];
|
||||
const tmplId = SUBSCRIBE_TEMPLATES[type];
|
||||
if (!tmplId || tmplId === 'TEMPLATE_ID_') return;
|
||||
|
||||
if (accepted) {
|
||||
// 用户授权了,记录到后端,并同步本地标记
|
||||
api.subscribeTemplate({
|
||||
template_type: type,
|
||||
template_id: tmplId
|
||||
}).then(() => {
|
||||
wx.setStorageSync('subscribed_notify', true);
|
||||
}).catch((e) => {
|
||||
console.warn('[subscribe] 同步订阅状态到后端失败:', e);
|
||||
});
|
||||
}
|
||||
// 用户拒绝时不做任何操作
|
||||
});
|
||||
return Promise.all(Object.keys(results).filter(type => results[type]).map(type => {
|
||||
return api.subscribeTemplate({template_type:type, template_id:SUBSCRIBE_TEMPLATES[type]}).then(result => {
|
||||
if (!result || !result.success) throw new Error('订阅授权保存失败');
|
||||
wx.setStorageSync('subscribed_notify', true);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作后静默请求订阅(无应用层弹窗)
|
||||
*
|
||||
* 优化策略:
|
||||
* 1. 同一会话中只弹窗一次(_requestedInSession 标记)
|
||||
* 2. 用户明确关闭通知(subscribed_notify=false)时才跳过
|
||||
* 3. 如果用户没有伴侣,不弹窗
|
||||
*
|
||||
* @param {string} scene - 触发场景(diary/photo/checkin/login)
|
||||
*/
|
||||
// 必须由用户点击直接调用;补充的是当前用户接收TA提醒的次数。
|
||||
function requestSubscribeAfterAction(scene) {
|
||||
// 同一会话中已请求过,跳过
|
||||
if (_requestedInSession) return;
|
||||
|
||||
// 用户明确关闭了通知,不打扰
|
||||
if (wx.getStorageSync('subscribed_notify') === false) return;
|
||||
|
||||
// 检查是否有伴侣,没有伴侣不需要订阅
|
||||
const coupleId = wx.getStorageSync('coupleId');
|
||||
if (!coupleId) return;
|
||||
|
||||
// 标记已请求,防止重复弹窗
|
||||
_requestedInSession = true;
|
||||
|
||||
// 延迟一点请求,避免干扰用户当前操作。
|
||||
// 注意:微信订阅消息是一次性授权,每次操作后都应申请续订,
|
||||
// 用户勾选「总是保持以上选择」后这里会静默通过,不会反复弹窗。
|
||||
setTimeout(() => {
|
||||
requestSubscribe('notify').catch(() => {});
|
||||
}, 1500);
|
||||
if (wx.getStorageSync('subscribed_notify') === false || !wx.getStorageSync('coupleId')) return Promise.resolve({});
|
||||
return requestSubscribe('notify').catch(error => {
|
||||
console.warn('[subscribe] 授权或保存失败,下次点击可重试:', error);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user