feat(mini): 下单/提现/报名前补订阅消息授权(提升微信服务通知到达率)

- 新增 common/subscribe.js:页面预加载模板ID + 点击动作中同步请求授权(避免 await 丢失手势上下文)

- 下单支付前请求订单状态/核销码模板;提现前请求到账模板;组局报名确认后请求报名模板

- 自动过滤 tmpl_ 占位模板;授权失败不阻塞业务

- 新增回归护栏 subscribe-authorization.spec.ts(80 套件/615 用例全绿)
这个提交包含在:
xcbyy
2026-10-19 09:01:36 +08:00
父节点 1ff4da290a
当前提交 029c6189a7
共有 5 个文件被更改,包括 103 次插入 和 0 次删除

查看文件

@@ -0,0 +1,51 @@
// 已读取AGENTS.md
// 微信订阅消息授权工具:预加载模板ID,在关键动作点击时同步请求授权。
// 说明:订阅消息为一次性订阅,需在用户点击(或确认弹窗)动作中调用 requestSubscribeFor。
import { getSubscribeTemplates } from './api.js';
const SCENE_KEYS = {
order: ['orderStatusTemplateId', 'verifyCodeTemplateId'],
withdraw: ['withdrawSuccessTemplateId'],
activity: ['activityJoinTemplateId']
};
let templates = null;
/** 页面加载时预加载模板配置(避免在点击时 await 丢失手势上下文) */
export async function preloadSubscribeTemplates() {
try {
templates = await getSubscribeTemplates();
} catch (_) {
templates = null;
}
return templates;
}
/** 过滤掉占位/空模板ID,只保留真实模板 */
function resolveTmplIds(scene) {
if (!templates || templates.enablePush === false) return [];
const keys = SCENE_KEYS[scene] || [];
return keys
.map((k) => templates[k])
.filter((x) => x && !String(x).startsWith('tmpl_'));
}
/**
* 在关键动作前请求订阅授权(同步调用,不阻塞业务)。
* scene: 'order' | 'withdraw' | 'activity'
*/
export function requestSubscribeFor(scene) {
try {
// #ifdef MP-WEIXIN
if (typeof uni.requestSubscribeMessage !== 'function') return;
const tmplIds = resolveTmplIds(scene);
if (tmplIds.length === 0) return;
uni.requestSubscribeMessage({
tmplIds,
complete: () => {}
});
// #endif
} catch (_) {
// 授权失败不影响主流程
}
}

查看文件

@@ -423,6 +423,7 @@ import {
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { ensureLoginOrRedirect } from '@/common/role.js';
import { preloadSubscribeTemplates, requestSubscribeFor } from '@/common/subscribe.js';
const currentUserId = computed(() => {
try {
@@ -465,6 +466,7 @@ onMounted(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
uni.$on('theme-change', onThemeChanged);
preloadSubscribeTemplates();
});
onShow(() => {
@@ -846,6 +848,8 @@ const handleJoin = () => {
confirmColor: '#6366F1',
success: async (res) => {
if (res.confirm) {
// 报名前请求一次性订阅授权(组局报名通知)
requestSubscribeFor('activity');
uni.showLoading({ title: '正在报名生成核销码...' });
let code = '8392';
try {

查看文件

@@ -240,6 +240,7 @@
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { createOrder, payOrder as payOrderApi, requestWechatPayment, getCompanionDetail, getUserProfile, getMarketingCoupons } from '@/common/api.js';
import { preloadSubscribeTemplates, requestSubscribeFor } from '@/common/subscribe.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { ensureLoginOrRedirect } from '@/common/role.js';
@@ -260,6 +261,7 @@ onMounted(() => {
applyThemeToNativeUI(currentThemeKey.value);
uni.$on('theme-change', onThemeChanged);
loadUserWallet();
preloadSubscribeTemplates();
});
onShow(() => {
@@ -463,6 +465,8 @@ const handlePay = async () => {
if (!ensureLoginOrRedirect('/pages/order/create')) {
return;
}
// 下单/支付前请求一次性订阅授权(订单状态 + 核销码通知)
requestSubscribeFor('order');
if (payMethod.value === 'BALANCE' && Number(userBalance.value) < finalTotal.value) {
selectBalancePay();
return;

查看文件

@@ -184,6 +184,7 @@ import { onShow } from '@dcloudio/uni-app';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { getNavBarInfo } from '@/common/nav.js';
import { preloadSubscribeTemplates, requestSubscribeFor } from '@/common/subscribe.js';
import {
getUserProfile,
withdrawCommission,
@@ -342,6 +343,8 @@ const handleSubmitWithdraw = async () => {
isSubmitting.value = true;
uni.showLoading({ title: '正在提交申请...' });
// 提现前请求一次性订阅授权(提现到账通知)
requestSubscribeFor('withdraw');
try {
const payload = {
@@ -389,6 +392,7 @@ onMounted(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
loadPageData();
preloadSubscribeTemplates();
});
onShow(() => {

查看文件

@@ -0,0 +1,40 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
/**
* 第六十四批回归闸门:关键动作前补订阅消息授权
*/
describe('第六十四批订阅授权补请求闸门', () => {
const rootDir = path.resolve(__dirname, '../../');
const read = (rel: string) => fs.readFileSync(path.join(rootDir, rel), 'utf-8');
const subscribeUtil = read('pwuniapp/common/subscribe.js');
const orderCreate = read('pwuniapp/pages/order/create.vue');
const withdraw = read('pwuniapp/pages/user/withdraw.vue');
const activityDetail = read('pwuniapp/pages/activity/detail.vue');
it('通用订阅工具:预加载 + 按场景请求 + 过滤占位模板', () => {
expect(subscribeUtil).toContain('export async function preloadSubscribeTemplates(');
expect(subscribeUtil).toContain('export function requestSubscribeFor(');
expect(subscribeUtil).toContain("order: ['orderStatusTemplateId', 'verifyCodeTemplateId']");
expect(subscribeUtil).toContain("withdraw: ['withdrawSuccessTemplateId']");
expect(subscribeUtil).toContain("activity: ['activityJoinTemplateId']");
expect(subscribeUtil).toContain("startsWith('tmpl_')");
});
it('下单页在支付前请求授权并预加载模板', () => {
expect(orderCreate).toContain('preloadSubscribeTemplates()');
expect(orderCreate).toContain("requestSubscribeFor('order')");
});
it('提现页在提交前请求授权并预加载模板', () => {
expect(withdraw).toContain('preloadSubscribeTemplates()');
expect(withdraw).toContain("requestSubscribeFor('withdraw')");
});
it('组局报名在确认后请求授权并预加载模板', () => {
expect(activityDetail).toContain('preloadSubscribeTemplates()');
expect(activityDetail).toContain("requestSubscribeFor('activity')");
});
});