feat(notify): 微信订阅消息推送(驳回通知已对接)
- 新增 server/services/wechatSubscribeService.ts:access_token 缓存 + 模板配置 + 字段裁剪 + subscribe/send · 内置默认模板 SONG_REJECTED(模板 ID erAK**…,审核内容=thing6←歌名,驳回原因=thing4←驳回原因) · 环境变量 WECHAT_MINI_SUBSCRIBE_TEMPLATES 可按键覆盖默认模板 · thing 字段按微信限制裁到 20 字(超长截 19 字 + 省略号),避免 47003 · 发送永不抛异常;43101(用户没授权/没额度)只记 info - 新增 server/api/user/wechat-subscribe-templates.get.ts:给小程序返回模板 ID(客户端要拿它去申请授权) - 新增 server/api/admin/wechat/test-subscribe.post.ts:超管自测接口(token/openid/字段/微信原文一次看清) - notificationService.ts:歌曲被选中/已播放/被驳回/系统通知 四处顺带发微信订阅消息 - .env.example + 部署流水线:WECHAT_MINI_SUBSCRIBE_TEMPLATES 可用仓库 Secret 下发
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
buildTemplateData,
|
||||
getMiniAccessToken,
|
||||
getSubscribeTemplates,
|
||||
getWechatOpenid,
|
||||
sendRawSubscribe
|
||||
} from '~~/server/services/wechatSubscribeService'
|
||||
|
||||
/**
|
||||
* 微信订阅消息的**自测接口**(仅超级管理员)—— 站长部署完用它「试一下」。
|
||||
*
|
||||
* 一次调用能看出四件事:
|
||||
* 1. access_token 能不能拿到(appid / secret 是否有效)
|
||||
* 2. 目标用户有没有绑微信(有没有 openid;纯密码用户必然是 null)
|
||||
* 3. 模板字段名 / 长度对不对(微信会回 47003 / 43101 之类的错误码)
|
||||
* 4. 微信返回的原文
|
||||
*
|
||||
* body(都可省):
|
||||
* { userId?: number, kind?: string, openid?: string, templateId?: string, data?: object }
|
||||
* · 默认给自己发、用配置里的第一个模板
|
||||
* · 想直接指定 openid / 模板 ID / data 也可以(用于排查)
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = event.context.user
|
||||
if (!user || user.role !== 'SUPER_ADMIN') {
|
||||
throw createError({ statusCode: 403, message: '只有超级管理员可以测试微信订阅消息' })
|
||||
}
|
||||
|
||||
const body = await readBody(event)
|
||||
const templates = getSubscribeTemplates()
|
||||
const kinds = Object.keys(templates)
|
||||
const kind = body?.kind || kinds[0] || ''
|
||||
const conf = kind ? templates[kind] : null
|
||||
const templateId = body?.templateId || conf?.id || ''
|
||||
const targetUserId = Number(body?.userId) || Number(user.id)
|
||||
|
||||
const token = await getMiniAccessToken()
|
||||
const openid = body?.openid || (await getWechatOpenid(targetUserId))
|
||||
|
||||
if (!templateId) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'no-template',
|
||||
hint: '服务器环境变量 WECHAT_MINI_SUBSCRIBE_TEMPLATES 没配置模板 ID(见 wechatSubscribeService.ts 头部注释)',
|
||||
configuredKinds: kinds,
|
||||
tokenOk: !!token,
|
||||
openid: openid ? openid.slice(0, 6) + '****' : null
|
||||
}
|
||||
}
|
||||
|
||||
if (!openid) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'no-wechat-openid',
|
||||
hint: '该用户没有绑定微信(userIdentities 里没有 provider=wechat-mp 的记录),微信发不到他那里',
|
||||
kind: kind,
|
||||
templateId: templateId,
|
||||
tokenOk: !!token,
|
||||
targetUserId: targetUserId
|
||||
}
|
||||
}
|
||||
|
||||
const data =
|
||||
body?.data && typeof body.data === 'object'
|
||||
? body.data
|
||||
: buildTemplateData(conf || { id: templateId }, {
|
||||
songTitle: '测试歌曲',
|
||||
message: '这是一条来自 VoiceHub 的测试通知'
|
||||
})
|
||||
|
||||
const result = await sendRawSubscribe(openid, templateId, data)
|
||||
|
||||
return {
|
||||
ok: result.ok,
|
||||
errcode: result.errcode,
|
||||
errmsg: result.errmsg,
|
||||
hint:
|
||||
result.errcode === 0
|
||||
? '发送成功,去微信里看「服务通知」'
|
||||
: result.errcode === 43101
|
||||
? '用户没有可用授权(一次性订阅:需要用户在小程序里点过「开启微信通知」才行)'
|
||||
: '看 errmsg:47003 = 模板字段名/长度不对;40003 = openid 不对;40001 = token 失效',
|
||||
configuredKinds: kinds,
|
||||
kind: kind,
|
||||
templateId: templateId,
|
||||
targetUserId: targetUserId,
|
||||
openid: openid.slice(0, 6) + '****',
|
||||
tokenOk: !!token,
|
||||
data: data,
|
||||
raw: result.raw
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getSubscribeTemplateIds, isWechatSubscribeEnabled } from '~~/server/services/wechatSubscribeService'
|
||||
|
||||
/**
|
||||
* 小程序申请「订阅消息」授权时需要的模板 ID。
|
||||
*
|
||||
* 模板 ID 不是秘密:它本来就要出现在客户端 `wx.requestSubscribeMessage({ tmplIds })` 里。
|
||||
* 没配置模板(环境变量 WECHAT_MINI_SUBSCRIBE_TEMPLATES 为空)时 enabled=false,
|
||||
* 小程序那边会静默隐藏「开启微信通知」的入口。
|
||||
*/
|
||||
export default defineEventHandler((event) => {
|
||||
const user = event.context.user
|
||||
if (!user) {
|
||||
throw createError({ statusCode: 401, message: '未授权访问' })
|
||||
}
|
||||
return {
|
||||
enabled: isWechatSubscribeEnabled(),
|
||||
tmplIds: getSubscribeTemplateIds()
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user