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');
+ });
+});