From cacc478d71b8850ab891208f1e1dd82efe13eee8 Mon Sep 17 00:00:00 2001 From: xcbyy <2578898740@qq.com> Date: Mon, 19 Oct 2026 10:02:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(subscribe):=20=E8=AE=A2=E9=98=85=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=9C=AA=E4=B8=8B=E5=8F=91=E5=8E=9F=E5=9B=A0=E5=8F=AF?= =?UTF-8?q?=E8=A7=81=20+=20=E5=B0=8F=E7=A8=8B=E5=BA=8F=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E6=8E=88=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后台测试推送:未就绪返回 mode=SIMULATED 明确原因 + 配置自检(模板ID/openid/appId/appSecret/总开关),不再把模拟显示为成功 - 占位符 tmpl_xxx 视为未配置(测试推送与订阅模板接口均过滤),微信错误码中文化(43101/47003/40037/40001 等) - 小程序「平台消息与通知」点击时真实调用 wx.requestSubscribeMessage 并回显授权状态,标签不再恒为已开启 - 管理端拦截器错误提示改用 message;文档补订阅消息排查顺序;新增回归护栏(86 套件/631 用例全绿) --- .../src/api/interceptor.ts | 4 +- .../views/safety/platform-config/index.vue | 28 +++++- docs/部署验收与1分钱支付闭环.md | 15 ++- pwuniapp/common/subscribe.js | 39 ++++++++ pwuniapp/pages/user/index.vue | 26 +++++- server/src/index.ts | 91 ++++++++++++++++--- server/tests/subscribe-message.spec.ts | 10 +- .../subscription-message-diagnose.spec.ts | 45 +++++++++ 8 files changed, 229 insertions(+), 29 deletions(-) create mode 100644 server/tests/subscription-message-diagnose.spec.ts diff --git a/admin/arco-design-pro-vite/src/api/interceptor.ts b/admin/arco-design-pro-vite/src/api/interceptor.ts index 1d8214f..4861c44 100644 --- a/admin/arco-design-pro-vite/src/api/interceptor.ts +++ b/admin/arco-design-pro-vite/src/api/interceptor.ts @@ -42,7 +42,7 @@ axios.interceptors.response.use( // if the custom code is not 20000, it is judged as an error. if (res.code !== 20000) { Message.error({ - content: res.msg || 'Error', + content: res.message || res.msg || 'Error', duration: 5 * 1000, }); // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; @@ -63,7 +63,7 @@ axios.interceptors.response.use( }, }); } - return Promise.reject(new Error(res.msg || 'Error')); + return Promise.reject(new Error(res.message || res.msg || 'Error')); } return res; }, diff --git a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue index 5c6d9bf..130e815 100644 --- a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue +++ b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue @@ -697,8 +697,23 @@
- - 推送时间: {{ testResult.sendTime }} | 目标UID: {{ testResult.targetUserId }} | 状态: {{ testResult.status }} + +
推送时间: {{ testResult.sendTime }} | 目标UID: {{ testResult.targetUserId }} | 状态: {{ testResult.status || testResult.mode }}
+
+ 配置自检:模板ID {{ testResult.diagnostics.hasTemplateId ? '已配置' : '缺失' }}, + 目标用户 openid {{ testResult.diagnostics.hasOpenid ? '已具备' : '缺失' }}, + appId {{ testResult.diagnostics.hasAppId ? '已配置' : '缺失' }}, + appSecret {{ testResult.diagnostics.hasAppSecret ? '已配置' : '缺失' }}, + 总开关 {{ testResult.diagnostics.enablePush ? '已开启' : '未开启' }} +
+
微信返回:[{{ testResult.result.errcode }}] {{ testResult.result.errmsg }}
@@ -1460,8 +1475,15 @@ const sendTestMessage = async () => { const res: any = await axios.post('/api/admin/message/test-send', payload); const data = res?.data?.data || res?.data || res; testResult.value = data; - Message.success('测试服务通知推送成功'); + if (data?.mode === 'REAL') { + Message.success('微信订阅消息已真实下发'); + } else if (data?.mode === 'SIMULATED') { + Message.warning(`未真实下发:${data?.reason || '配置未就绪'}`); + } else { + Message.error(data?.reason || '推送失败'); + } } catch (err: any) { + // 真实下发失败时后端返回 code 40000,此处展示后端给出的中文原因 Message.error(err.response?.data?.message || err.message || '测试推送异常'); } finally { testSending.value = false; diff --git a/docs/部署验收与1分钱支付闭环.md b/docs/部署验收与1分钱支付闭环.md index 68dbf12..5ba4ba8 100644 --- a/docs/部署验收与1分钱支付闭环.md +++ b/docs/部署验收与1分钱支付闭环.md @@ -125,10 +125,17 @@ MYSQL_DATABASE=peiwan 接入说明: - **模板创建**:在微信公众平台「订阅消息」创建 4 个模板,把真实模板ID填入后台。各模板的字段(如 `phrase1`/`thing2`/`time3`/`character_string1`/`amount1`)**需与代码下发时的 data 键一致**(不一致会返回 47003 等错误,日志可见)。若你的模板字段名不同,把字段占位符发我,我按模板对齐 `data` 键。 - **触发点**:① 订单进入"履约中"→ 给买家下发订单状态 + 4位核销码通知;② 组局报名成功 → 给报名者下发通知;③ 提现"打款成功"(受理/回调确认到账)→ 给提现大神下达到账通知。 -- **就绪条件**:`enablePush=1` + 对应模板ID已填 + 小程序 `appId/appSecret` 已配置 + 目标用户有 `openid`。任一缺失则静默跳过(不阻塞业务)。 -- **后台测试推送**:管理端"测试推送"在就绪时**真实下发**;未就绪时返回模拟结果并说明原因(mode=REAL/SIMULATED)。目标用户填其 UID,且该用户须有 openid。 -- **小程序授权**:设置页开启通知时用**真实模板ID** 调 `wx.requestSubscribeMessage` 收集授权。订阅为**一次性**,需在关键动作前再次请求(后续可在下单/报名/提现前补请求)。 -- **启动自检**:不额外打印;如需可在后台测试推送处验证。 +- **就绪条件**:`enablePush=1` + 对应模板ID已填(**不能是 `tmpl_xxx` 占位符**)+ 小程序 `appId/appSecret` 已配置 + 目标用户有 `openid` + 用户已授权该模板订阅。任一缺失即不下发。 +- **后台测试推送**:管理端"测试推送"在就绪时**真实下发**(`mode=REAL`);未就绪返回 `mode=SIMULATED` 并给出**明确原因**与**配置自检**(模板ID/openid/appId/appSecret/总开关),不再显示"发送成功"。 +- **小程序授权**:「平台消息与通知」点击时会用**真实模板ID** 调 `wx.requestSubscribeMessage` 收集授权,并在页面标签显示实际状态。订阅为**一次性**(每条授权仅可下发 1 条),已在下单/报名/提现前再次请求。 + +### 收不到推送时的排查顺序(对照后台测试推送的"配置自检") +1. `模板ID 缺失` → 后台把该类型模板的**真实模板ID**填入并保存(占位符 `tmpl_xxx` 视为未配置)。 +2. `总开关 未开启` → 后台打开"开启微信通知"总开关。 +3. `目标用户 openid 缺失` → 该用户必须通过**微信登录**过小程序(REST 走 `/api/auth/wechat-login` 换 openid)。 +4. `appId/appSecret 缺失` → 在微信支付/小程序凭证配置里填写真实 `appId` 与 32 位 `appSecret`。 +5. 以上都 ✅ 但仍失败 → 看后台提示的**微信返回码**:`43101` 用户未授权(让用户在小程序内重新点「平台消息与通知」授权)、`47003` 模板字段名/长度与模板不一致、`40037` 模板ID无效。 +6. 订阅消息为**一次性**:用户授权一次只能收 1 条;高频场景需每次动作前请求授权(代码已在下单/报名/提现前请求)。 ## 八、内容安全(可选) diff --git a/pwuniapp/common/subscribe.js b/pwuniapp/common/subscribe.js index 36e2a6f..e09003e 100644 --- a/pwuniapp/common/subscribe.js +++ b/pwuniapp/common/subscribe.js @@ -49,3 +49,42 @@ export function requestSubscribeFor(scene) { // 授权失败不影响主流程 } } + +/** 全部已配置的真实模板ID(过滤空值与 tmpl_ 占位符) */ +function allTmplIds() { + if (!templates || templates.enablePush === false) return []; + return ['orderStatusTemplateId', 'activityJoinTemplateId', 'withdrawSuccessTemplateId', 'verifyCodeTemplateId'] + .map((k) => templates[k]) + .filter((x) => x && !String(x).startsWith('tmpl_')); +} + +/** + * 主动请求全部订阅授权(用于「平台消息与通知」入口)。 + * 注意:wx.requestSubscribeMessage 必须在用户点击手势中同步调用;本函数不 await 任何网络请求。 + * 返回每项授权结果,便于界面给出真实提示(订阅消息为一次性订阅,每条授权仅可下发 1 条)。 + */ +export function requestSubscribeAll() { + return new Promise((resolve) => { + try { + if (typeof uni.requestSubscribeMessage !== 'function') { + return resolve({ supported: false, tmplIds: [], accepted: [], rejected: [] }); + } + const tmplIds = allTmplIds(); + if (tmplIds.length === 0) { + return resolve({ supported: true, tmplIds: [], accepted: [], rejected: [], noTemplates: true }); + } + uni.requestSubscribeMessage({ + tmplIds, + success: (res) => { + const accepted = tmplIds.filter((id) => res && res[id] === 'accept'); + const rejected = tmplIds.filter((id) => !res || res[id] !== 'accept'); + resolve({ supported: true, tmplIds, accepted, rejected, raw: res }); + }, + fail: (err) => resolve({ supported: true, tmplIds, accepted: [], rejected: tmplIds, error: err }), + complete: () => {} + }); + } catch (e) { + resolve({ supported: true, tmplIds: [], accepted: [], rejected: [], error: e }); + } + }); +} diff --git a/pwuniapp/pages/user/index.vue b/pwuniapp/pages/user/index.vue index b2191b8..610257a 100644 --- a/pwuniapp/pages/user/index.vue +++ b/pwuniapp/pages/user/index.vue @@ -152,7 +152,7 @@ 平台消息与通知 - {{ noticeSubscribe ? '提醒开启' : '已静音' }} + {{ noticeSubscribe ? '已开启提醒' : '点击开启' }} @@ -352,6 +352,7 @@ import { getCurrentThemeKey, setTheme, THEME_KEYS, applyThemeToNativeUI } from ' import { getCurrentRole, setCurrentRole, ROLE_TYPES, toggleRole as toggleUserRole, isExpertCertified, getExpertCertStatus, setExpertCertStatus, CERT_STATUS } from '@/common/role.js'; import { getNavBarInfo } from '@/common/nav.js'; import { getUserProfile, rechargeBalance, withdrawCommission, getUserFinanceRecords, updateUserProfile, uploadImage, getRechargeTiers, getPlatformConfigs, applyExpertCert, getUserWithdrawals, getFavorites, removeFavorite, getDistributionTeam, getDistributionRecords, getMyReports, bindInviteCode } from '@/common/api.js'; +import { preloadSubscribeTemplates, requestSubscribeAll } from '@/common/subscribe.js'; const DEFAULT_GUEST_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200'; const initialUserId = typeof uni !== 'undefined' ? (uni.getStorageSync('userId') || null) : null; @@ -382,7 +383,7 @@ const openMyReportsModal = () => { // 系统与隐私设置 const privacyHidePhone = ref(uni.getStorageSync('privacy_hide_phone') === '1'); -const noticeSubscribe = ref(uni.getStorageSync('notice_subscribe') !== '0'); +const noticeSubscribe = ref(uni.getStorageSync('notice_subscribe') === '1'); const cacheSizeText = ref('1.25 MB'); // 分销推广管理 @@ -628,6 +629,8 @@ onMounted(() => { loadUserProfile(); loadPlatformConfigs(); loadMyReports(); + // 预加载订阅消息模板ID,确保点击授权时无需等待网络(保留点击手势) + preloadSubscribeTemplates(); }); onShow(() => { @@ -639,7 +642,7 @@ onShow(() => { loadUserProfile(); loadPlatformConfigs(); privacyHidePhone.value = uni.getStorageSync('privacy_hide_phone') === '1'; - noticeSubscribe.value = uni.getStorageSync('notice_subscribe') !== '0'; + noticeSubscribe.value = uni.getStorageSync('notice_subscribe') === '1'; if (isLoggedIn.value) { loadFavorites(); loadMyReports(); @@ -1163,6 +1166,9 @@ const contactService = () => { }; const openNotice = () => { + // 同步触发微信订阅授权(必须保留点击手势,不能先 await 网络);再展示平台须知。 + const subResult = typeof uni.requestSubscribeMessage === 'function' ? requestSubscribeAll() : null; + const notice = platformConfigs.value.safety_notice || {}; const title = notice.title || '平台官方公告与指引'; let content = '【安全履约须知】\n1. 为保障搭子与陪伴师人身与财产安全,线下约局须在合规公开商业街区进行。\n2. 现场碰面后请出示 4 位数字核销码进行签到确认。\n3. 平台提供 7x24 小时争议仲裁与资金托管保障服务。'; @@ -1177,6 +1183,20 @@ const openNotice = () => { confirmText: '我知道了', confirmColor: '#6366F1' }); + + if (subResult) { + subResult.then((r) => { + if (!r || !r.supported) return; + if (r.noTemplates || !r.tmplIds || r.tmplIds.length === 0) { + uni.showToast({ title: '后台未配置订阅消息模板', icon: 'none' }); + return; + } + const accepted = (r.accepted || []).length > 0; + noticeSubscribe.value = accepted; + uni.setStorageSync('notice_subscribe', accepted ? '1' : '0'); + uni.showToast({ title: accepted ? '已开启消息提醒' : '未获得提醒授权', icon: 'none' }); + }); + } }; const openFaq = () => { diff --git a/server/src/index.ts b/server/src/index.ts index 5dd7135..26cbb08 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -612,14 +612,19 @@ app.get('/api/message/subscribe-templates', async (_req: Request, res: Response) try { const record = await repo.getPlatformConfigByKey('message_template_rules'); const rules = (record && record.valueJson) || {}; + // 过滤占位符(tmpl_xxx),避免小程序拿无效模板ID去请求授权 + const clean = (v: any) => { + const s = String(v == null ? '' : v).trim(); + return /^tmpl_/i.test(s) ? '' : s; + }; res.json({ code: 20000, data: { enablePush: Number(rules.enablePush) === 1, - orderStatusTemplateId: String(rules.orderStatusTemplateId || ''), - activityJoinTemplateId: String(rules.activityJoinTemplateId || ''), - withdrawSuccessTemplateId: String(rules.withdrawSuccessTemplateId || ''), - verifyCodeTemplateId: String(rules.verifyCodeTemplateId || '') + orderStatusTemplateId: clean(rules.orderStatusTemplateId), + activityJoinTemplateId: clean(rules.activityJoinTemplateId), + withdrawSuccessTemplateId: clean(rules.withdrawSuccessTemplateId), + verifyCodeTemplateId: clean(rules.verifyCodeTemplateId) } }); } catch (err: any) { @@ -3754,7 +3759,24 @@ app.delete('/api/admin/posts/comments/:commentId', async (req: Request, res: Res } }); -// 管理端:微信订阅消息(服务通知)真实测试推送接口(模板ID/目标用户 openid/小程序凭证就绪时真实下发,否则模拟返回) +// 微信订阅消息错误码中文化,便于后台定位真实原因 +function describeWechatSubscribeError(r: any): string { + const code = Number(r && r.errcode) || 0; + const dict: Record = { + 40003: 'touser 不是有效 openid', + 40037: '模板ID无效(请在微信公众平台核对模板ID)', + 41030: 'page 路径不存在', + 43101: '用户未授权该模板订阅(用户需在小程序内授权;一次性订阅每条仅可下发 1 次)', + 45009: '接口调用频率超限(access_token 复用不足或调用过快)', + 40001: 'access_token 无效(appId 与 appSecret 不匹配)', + 40125: 'appSecret 无效', + 47003: '模板参数不合法(字段名或长度与模板不一致)', + }; + const extra = dict[code] ? `(${dict[code]})` : ''; + return `微信订阅消息下发失败: [${code}] ${(r && r.errmsg) || '未知错误'}${extra}`; +} + +// 管理端:微信订阅消息(服务通知)真实测试推送接口(模板ID/目标用户 openid/小程序凭证就绪时真实下发,否则返回明确原因) app.post('/api/admin/message/test-send', async (req: Request, res: Response) => { try { const { templateType, targetUserId, previewData, data, page } = req.body || {}; @@ -3773,14 +3795,34 @@ app.post('/api/admin/message/test-send', async (req: Request, res: Response) => withdrawSuccess: 'withdrawSuccessTemplateId', verifyCode: 'verifyCodeTemplateId', }; - const templateId = String(rules[idMap[tKey]] || '').trim(); + const rawTemplateId = String(rules[idMap[tKey]] || '').trim(); + // 过滤后台占位符(tmpl_xxx):避免用无效模板ID去真调微信 + const templateId = /^tmpl_/i.test(rawTemplateId) ? '' : rawTemplateId; + const enablePush = Number(rules.enablePush) === 1; const cfg = await resolveWechatPayRuntimeConfig(); const [uRows]: any = await pool.query('SELECT wechat_openid FROM users WHERE id = ? LIMIT 1', [Number(targetUserId) || 0]); const openid = uRows && uRows[0] && uRows[0].wechat_openid; + const diagnostics = { + enablePush, + hasTemplateId: Boolean(templateId), + hasOpenid: Boolean(openid), + hasAppId: Boolean(cfg.appId), + hasAppSecret: Boolean(cfg.appSecret), + }; + if (templateId && openid && cfg.appId && cfg.appSecret) { - const accessToken = await getWechatAccessToken(cfg.appId, cfg.appSecret); + let accessToken = ''; + try { + accessToken = await getWechatAccessToken(cfg.appId, cfg.appSecret); + } catch (e: any) { + return res.json({ + code: 40000, + data: { mode: 'REAL_FAILED', templateType: tKey, templateId, targetUserId, diagnostics, error: 'ACCESS_TOKEN_FAILED' }, + message: `获取微信 access_token 失败:${(e && e.message) || e}(请核对小程序 appId/appSecret)` + }); + } // 与所选模板字段对齐的示例数据(字段名必须与模板一致,否则 47003) const sampleData: Record> = { orderStatus: { character_string1: 'ORD20261017001', phrase2: '已开始服务', thing3: '同城陪伴服务', thing4: '核销码8392,请出示' }, @@ -3799,15 +3841,36 @@ app.post('/api/admin/message/test-send', async (req: Request, res: Response) => const ok = !result || !result.errcode || result.errcode === 0; return res.json({ code: ok ? 20000 : 40000, - data: { mode: 'REAL', templateType: tKey, templateId, targetUserId, result }, - message: ok ? '微信订阅消息已真实下发' : `微信订阅消息下发失败: [${result.errcode}] ${result.errmsg}` + data: { + mode: ok ? 'REAL' : 'REAL_FAILED', + templateType: tKey, + templateId, + targetUserId, + diagnostics, + result, + status: ok ? 'REAL' : 'REAL_FAILED', + wechatMsgId: `wx_msg_${Date.now()}`, + sendTime: new Date().toISOString(), + previewData: previewData || {} + }, + message: ok ? '微信订阅消息已真实下发' : describeWechatSubscribeError(result) }); } - // 未就绪:返回模拟结果并说明原因 - const reason = !templateId - ? '未配置该类型模板ID' - : (!openid ? '目标用户无 openid' : '小程序 appId/appSecret 未配置'); + // 未就绪:返回模拟结果并给出明确原因(后台据此提示,避免被误解为"已发送成功") + const typeLabels: Record = { + orderStatus: '订单履约状态变动', + activityJoin: '组局搭子报名与成局', + withdrawSuccess: '大神佣金提现打款到账', + verifyCode: '现场4位核销码签到提醒', + }; + const reason = !enablePush + ? '订阅消息总开关未开启' + : (!templateId + ? `未配置「${typeLabels[tKey] || tKey}」模板ID${rawTemplateId ? `(当前值为占位符 ${rawTemplateId})` : ''}` + : (!openid + ? `目标用户(ID=${targetUserId || 0})没有 wechat_openid(该用户需通过微信登录过小程序)` + : '小程序 appId/appSecret 未配置')); const result = { templateType: tKey, targetUserId: targetUserId || 1001, @@ -3816,7 +3879,7 @@ app.post('/api/admin/message/test-send', async (req: Request, res: Response) => wechatMsgId: `wx_msg_${Date.now()}`, previewData: previewData || { title: '订单履约提醒', detail: '您的同城生活陪伴订单已开始服务' } }; - res.json({ code: 20000, data: { mode: 'SIMULATED', reason, ...result }, message: `未真实下发(${reason}),已按模拟推送返回` }); + res.json({ code: 20000, data: { mode: 'SIMULATED', reason, diagnostics, ...result }, message: `未真实下发(${reason})` }); } catch (err: any) { res.status(400).json({ code: 40000, message: err.message }); } diff --git a/server/tests/subscribe-message.spec.ts b/server/tests/subscribe-message.spec.ts index f84a4a1..7480226 100644 --- a/server/tests/subscribe-message.spec.ts +++ b/server/tests/subscribe-message.spec.ts @@ -44,11 +44,15 @@ describe('第五十七批微信订阅消息闸门', () => { expect(indexCode).toContain('time3: toCnTimeOrNow(act?.startTime)'); }); - it('管理端测试推送在就绪时真实下发', () => { + it('管理端测试推送在就绪时真实下发,未就绪时明确标注模拟原因', () => { const start = indexCode.indexOf("app.post('/api/admin/message/test-send'"); - const block = indexCode.slice(start, start + 3400); - expect(block).toContain("mode: 'REAL'"); + const nextApp = indexCode.indexOf('\napp.', start + 10); + const block = indexCode.slice(start, nextApp > 0 ? nextApp : start + 8000); + expect(block).toContain("mode: ok ? 'REAL' : 'REAL_FAILED'"); expect(block).toContain("mode: 'SIMULATED'"); + // 未下发时必须返回明确原因与配置自检,避免被误解为"已发送成功" + expect(block).toContain('diagnostics'); + expect(block).toContain('reason'); }); it('小程序用真实模板ID 请求订阅授权', () => { diff --git a/server/tests/subscription-message-diagnose.spec.ts b/server/tests/subscription-message-diagnose.spec.ts new file mode 100644 index 0000000..1a2dd9f --- /dev/null +++ b/server/tests/subscription-message-diagnose.spec.ts @@ -0,0 +1,45 @@ +// 已读取AGENTS.md +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * 第七十批回归闸门:订阅消息「未下发原因可见」+ 小程序真实授权。 + * 背景:后台曾把 SIMULATED(未调微信)也显示为"发送成功",且小程序从未调用 requestSubscribeMessage。 + */ +describe('第七十批订阅消息原因可见与真实授权', () => { + const root = path.resolve(__dirname, '../../'); + const indexCode = fs.readFileSync(path.join(root, 'server/src/index.ts'), 'utf-8'); + const adminCode = fs.readFileSync( + path.join(root, 'admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue'), + 'utf-8' + ); + const subscribeCode = fs.readFileSync(path.join(root, 'pwuniapp/common/subscribe.js'), 'utf-8'); + const userCode = fs.readFileSync(path.join(root, 'pwuniapp/pages/user/index.vue'), 'utf-8'); + + it('后台占位符模板ID不会被当成已配置,并返回可诊断信息', () => { + expect(indexCode).toContain('/^tmpl_/i.test(rawTemplateId)'); + expect(indexCode).toContain('diagnostics'); + expect(indexCode).toContain('没有 wechat_openid'); + expect(indexCode).toContain('订阅消息总开关未开启'); + }); + + it('订阅模板接口下发给小程序前过滤占位符', () => { + expect(indexCode).toContain('const clean = (v: any)'); + expect(indexCode).toContain('orderStatusTemplateId: clean('); + expect(indexCode).toContain('verifyCodeTemplateId: clean('); + }); + + it('后台不再把 SIMULATED 显示为发送成功', () => { + expect(adminCode).not.toContain("Message.success('测试服务通知推送成功')"); + expect(adminCode).toContain("data?.mode === 'SIMULATED'"); + expect(adminCode).toContain('未真实下发'); + expect(adminCode).toContain('diagnostics.hasTemplateId'); + }); + + it('小程序真正调用订阅授权并过滤占位符', () => { + expect(subscribeCode).toContain('requestSubscribeAll'); + expect(subscribeCode).toContain("startsWith('tmpl_')"); + expect(userCode).toContain('requestSubscribeAll'); + expect(userCode).toContain('uni.requestSubscribeMessage'); + }); +});