Files
BYW-VoiceHub/server/api/admin/wechat/diagnose.get.ts
T
Henry_Du 09e7f53a1a feat(微信订阅): 新增一键体检接口 /api/admin/wechat/diagnose
线上排查「订阅消息没发出去」以前只能靠猜,因为没有查询额度的接口,四条跳过路径
也不写日志。这个 GET 接口(仅超管,浏览器直接打开即可)一次返回所有事实:

- access_token 能不能拿到、长度多少
- WECHAT_MINI_SUBSCRIBE_TEMPLATES / APPID / SECRET 是否注入、模板开关是否启用
- 当前账号有没有绑微信 openid(没有就永远发不出去)、哪些 userId 绑了微信
- 每个模板的 ID、字段映射,以及 buildTemplateData 算出的、真发出去会用的 data
- 加 ?send=1 时顺手给当前用户真发一条(两个模板各一次),把微信的 errcode
  原样返回:0=通、43101=没额度、47003=字段名不对、40003=openid 不对、40001=token 失效

用法:登录后台后打开 /api/admin/wechat/diagnose (或 ?send=1)
2026-10-11 15:37:39 +08:00

122 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
buildTemplateData,
getMiniAccessToken,
getSubscribeTemplates,
getWechatOpenid,
isWechatSubscribeEnabled,
sendWechatSubscribeToUser
} from '~~/server/services/wechatSubscribeService'
import { db } from '~/drizzle/db'
import { userIdentities } from '~/drizzle/schema'
import { eq } from 'drizzle-orm'
/**
* 微信订阅消息的**一键体检**(仅超级管理员,用 GET,浏览器直接打开就能看)。
*
* 为什么要有它:线上「通知没发出去」以前只能靠猜——服务端日志里四条跳过路径
* 一句不写。现在把判断要用的事实全部一次返回:
* 1. access_token 能不能拿到(appid / secret 是否有效)
* 2. 配置了哪些模板、ID 是什么、环境变量有没有覆盖
* 3. 每个用户有没有绑微信 openid、通知总开关开没开
* 4. 「用户是不是授权过这张模板」——微信没有查额度的接口,唯一办法是调
* getwxacode 的同类接口 /api/cgi-bin/message/subscribe/… 没法查,所以这里
* 退而求其次:用 `?send=1` 真发一条给当前用户,把微信的 errcode 原样返回
* (0=通、43101=没额度、47003=字段名不对、40003=openid 不对、40001=token 失效)。
*
* 用法(在已登录后台的浏览器里直接打开):
* /api/admin/wechat/diagnose → 只体检,不发消息
* /api/admin/wechat/diagnose?send=1 → 顺手给当前登录用户发一条(消耗 1 条额度)
*/
export default defineEventHandler(async (event) => {
const user = event.context.user
if (!user || user.role !== 'SUPER_ADMIN') {
throw createError({ statusCode: 403, message: '只有超级管理员可以使用微信订阅体检' })
}
const query = getQuery(event)
const doSend = query.send === '1' || query.send === 'true'
const selfId = Number(user.id)
const templates = getSubscribeTemplates()
const kinds = Object.keys(templates)
const token = await getMiniAccessToken()
const selfOpenid = await getWechatOpenid(selfId)
// 绑了微信的账号(有 openid 才可能收到订阅消息)
const bound = await db
.select({ userId: userIdentities.userId })
.from(userIdentities)
.where(eq(userIdentities.provider, 'wechat-mp'))
const boundUserIds = Array.from(new Set(bound.map((r) => r.userId)))
// 每个模板算一遍「真发出去会是什么值」,方便直接比对公众平台后台的字段名
const previews = kinds.map((kind) => {
const conf = templates[kind]
const vars: Record<string, unknown> = {
songTitle: '测试歌曲《晴天》',
playDate: '2026-02-14',
playTime: '2026-02-14 12:30',
sequence: '1',
status: '已排期',
reason: '测试驳回原因',
message: '这是一条测试通知'
}
return {
kind,
templateId: conf?.id || null,
fields: conf?.fields || null,
// 真正发出去就是这一份 data(buildTemplateData 内部走 clampField 截断/补齐)
data: conf
? buildTemplateData(conf, Object.assign({ songTitle: '宝一外校园点歌系统' }, vars))
: null
}
})
const result: Record<string, unknown> = {
now: new Date().toISOString(),
env: {
// 环境变量里只要不是合法 JSON,服务端会忽略它并回退到内置模板
WECHAT_MINI_SUBSCRIBE_TEMPLATES: process.env.WECHAT_MINI_SUBSCRIBE_TEMPLATES || null,
appidSet: !!process.env.WECHAT_MINI_APPID,
secretSet: !!process.env.WECHAT_MINI_SECRET,
enabled: isWechatSubscribeEnabled(),
nodeEnv: process.env.NODE_ENV || null
},
tokenOk: !!token,
tokenLen: token ? token.length : 0,
me: {
userId: selfId,
username: user.username,
// openid 只露前 6 位,避免日志/截图泄露
openid: selfOpenid ? selfOpenid.slice(0, 6) + '****' : null,
canReceive: !!selfOpenid
},
boundWechatUserIds: boundUserIds,
templates: previews
}
if (doSend && selfOpenid) {
const attempts: Array<{ kind: string; result: unknown }> = []
for (const kind of kinds) {
attempts.push({
kind,
result: await sendWechatSubscribeToUser(selfId, kind, {
songTitle: '测试歌曲《晴天》',
playDate: '2026-02-14',
playTime: '2026-02-14 12:30',
sequence: '1',
status: '已排期',
reason: '测试驳回原因',
message: '这是一条测试通知'
})
})
}
result.sendAttempts = attempts
} else if (doSend && !selfOpenid) {
result.sendSkipped = 'no-wechat-openid:当前登录账号没有绑定微信,发不出去'
}
return result
})