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 = { 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 = { 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 })