From 029c6189a7e9ef7934bdc9715bc15c4bd82270a4 Mon Sep 17 00:00:00 2001 From: xcbyy <2578898740@qq.com> Date: Mon, 19 Oct 2026 09:01:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(mini):=20=E4=B8=8B=E5=8D=95/=E6=8F=90?= =?UTF-8?q?=E7=8E=B0/=E6=8A=A5=E5=90=8D=E5=89=8D=E8=A1=A5=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E6=B6=88=E6=81=AF=E6=8E=88=E6=9D=83=EF=BC=88=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E5=BE=AE=E4=BF=A1=E6=9C=8D=E5=8A=A1=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E5=88=B0=E8=BE=BE=E7=8E=87=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 common/subscribe.js:页面预加载模板ID + 点击动作中同步请求授权(避免 await 丢失手势上下文) - 下单支付前请求订单状态/核销码模板;提现前请求到账模板;组局报名确认后请求报名模板 - 自动过滤 tmpl_ 占位模板;授权失败不阻塞业务 - 新增回归护栏 subscribe-authorization.spec.ts(80 套件/615 用例全绿) --- pwuniapp/common/subscribe.js | 51 ++++++++++++++++++++ pwuniapp/pages/activity/detail.vue | 4 ++ pwuniapp/pages/order/create.vue | 4 ++ pwuniapp/pages/user/withdraw.vue | 4 ++ server/tests/subscribe-authorization.spec.ts | 40 +++++++++++++++ 5 files changed, 103 insertions(+) create mode 100644 pwuniapp/common/subscribe.js create mode 100644 server/tests/subscribe-authorization.spec.ts diff --git a/pwuniapp/common/subscribe.js b/pwuniapp/common/subscribe.js new file mode 100644 index 0000000..36e2a6f --- /dev/null +++ b/pwuniapp/common/subscribe.js @@ -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 (_) { + // 授权失败不影响主流程 + } +} diff --git a/pwuniapp/pages/activity/detail.vue b/pwuniapp/pages/activity/detail.vue index 61c4c0b..9920e75 100644 --- a/pwuniapp/pages/activity/detail.vue +++ b/pwuniapp/pages/activity/detail.vue @@ -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 { diff --git a/pwuniapp/pages/order/create.vue b/pwuniapp/pages/order/create.vue index 3b106f7..7ec3555 100644 --- a/pwuniapp/pages/order/create.vue +++ b/pwuniapp/pages/order/create.vue @@ -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; diff --git a/pwuniapp/pages/user/withdraw.vue b/pwuniapp/pages/user/withdraw.vue index 05f3347..8f8830a 100644 --- a/pwuniapp/pages/user/withdraw.vue +++ b/pwuniapp/pages/user/withdraw.vue @@ -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(() => { diff --git a/server/tests/subscribe-authorization.spec.ts b/server/tests/subscribe-authorization.spec.ts new file mode 100644 index 0000000..a3c5514 --- /dev/null +++ b/server/tests/subscribe-authorization.spec.ts @@ -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')"); + }); +});