feat(subscribe): 新增拉取小程序真实模板列表诊断(定位40037)

- 新增 GET /api/admin/message/templates:调用 wxaapi/newtmpl/gettemplate 返回当前 appId(脱敏) 下真实模板列表,并标出后台已填但不在列表中的ID

- 测试推送 diagnostics 增加 appIdMasked,便于核对 appId

- 后台新增「拉取小程序模板列表(核对ID)」按钮与不匹配高亮提示

- 文档补充 40037 排查步骤;回归 86 套件/632 用例全绿
这个提交包含在:
xcbyy
2026-10-19 10:26:10 +08:00
父节点 cacc478d71
当前提交 584e526bca
共有 4 个文件被更改,包括 152 次插入 和 5 次删除

查看文件

@@ -3759,6 +3759,33 @@ app.delete('/api/admin/posts/comments/:commentId', async (req: Request, res: Res
}
});
// 微信 GET 接口(返回 JSON)
function wechatHttpGetJson(url: string): Promise<any> {
return new Promise((resolve, reject) => {
https
.get(url, (resp) => {
let data = '';
resp.on('data', (chunk) => {
data += chunk;
});
resp.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(e);
}
});
})
.on('error', reject);
});
}
/** 脱敏展示 appId(用于自检,避免完整明文) */
function maskTail(s: string): string {
const v = String(s || '');
return v ? `${v.slice(0, 6)}****${v.slice(-4)}` : '';
}
// 微信订阅消息错误码中文化,便于后台定位真实原因
function describeWechatSubscribeError(r: any): string {
const code = Number(r && r.errcode) || 0;
@@ -3810,6 +3837,7 @@ app.post('/api/admin/message/test-send', async (req: Request, res: Response) =>
hasOpenid: Boolean(openid),
hasAppId: Boolean(cfg.appId),
hasAppSecret: Boolean(cfg.appSecret),
appIdMasked: maskTail(cfg.appId),
};
if (templateId && openid && cfg.appId && cfg.appSecret) {
@@ -3885,6 +3913,48 @@ app.post('/api/admin/message/test-send', async (req: Request, res: Response) =>
}
});
// 管理端:拉取当前小程序已存在的「订阅消息」模板列表
// 用于核对后台填写的模板ID是否属于当前 appId(40037 invalid template_id 的根因排查)
app.get('/api/admin/message/templates', async (_req: Request, res: Response) => {
try {
const cfg = await resolveWechatPayRuntimeConfig();
if (!cfg.appId || !cfg.appSecret) {
return res.json({ code: 40000, message: '小程序 appId/appSecret 未配置' });
}
const accessToken = await getWechatAccessToken(cfg.appId, cfg.appSecret);
const data = await wechatHttpGetJson(
`https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate?access_token=${encodeURIComponent(accessToken)}`
);
const list = data && Array.isArray(data.data) ? data.data : [];
const record = await repo.getPlatformConfigByKey('message_template_rules');
const rules = (record && record.valueJson) || {};
res.json({
code: 20000,
data: {
appIdMasked: maskTail(cfg.appId),
errcode: Number(data && data.errcode) || 0,
errmsg: (data && data.errmsg) || 'ok',
count: list.length,
templates: list.map((t: any) => ({
templateId: t.priTmplId,
title: t.title,
type: t.type,
content: t.content,
example: t.example,
})),
configured: {
orderStatus: String(rules.orderStatusTemplateId || ''),
activityJoin: String(rules.activityJoinTemplateId || ''),
withdrawSuccess: String(rules.withdrawSuccessTemplateId || ''),
verifyCode: String(rules.verifyCodeTemplateId || ''),
},
},
});
} catch (err: any) {
res.status(400).json({ code: 40000, message: err.message });
}
});
app.post('/api/posts', async (req: Request, res: Response) => {
try {
const { content, userId } = req.body || {};