feat(微信订阅): 自测接口支持 dryRun(不消耗额度)与 sendToAll(区分没额度/字段错)
This commit is contained in:
@@ -5,6 +5,9 @@ import {
|
||||
getWechatOpenid,
|
||||
sendRawSubscribe
|
||||
} from '~~/server/services/wechatSubscribeService'
|
||||
import { db } from '~/drizzle/db'
|
||||
import { userIdentities } from '~/drizzle/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
/**
|
||||
* 微信订阅消息的**自测接口**(仅超级管理员)—— 站长部署完用它「试一下」。
|
||||
@@ -17,11 +20,15 @@ import {
|
||||
*
|
||||
* body(都可省):
|
||||
* { userId?: number, kind?: string, openid?: string, templateId?: string,
|
||||
* vars?: object, data?: object }
|
||||
* vars?: object, data?: object, dryRun?: boolean, sendToAll?: boolean }
|
||||
* · 默认给自己发、用配置里的第一个模板
|
||||
* · vars 覆盖**变量**(songTitle / playDate / playTime / sequence / status / message…),
|
||||
* 仍走 buildTemplateData → clampField,所以看到的是真发出去的值
|
||||
* · data 直接当微信的 data 用(最后的兜底排查手段)
|
||||
* · **dryRun: true —— 只算出 data、一个字都不发,不消耗任何订阅额度**(排查字段时先用它)
|
||||
* · **sendToAll: true —— 给所有绑了微信的用户各试一次**(一次性订阅的额度是
|
||||
* 每用户一条,逐个人发能立刻区分「额度用光了」和「字段名不对」:
|
||||
* 43101 for all = 没额度;47003 = 字段问题;只要有一个 0 = 字段其实是对的)
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = event.context.user
|
||||
@@ -36,6 +43,8 @@ export default defineEventHandler(async (event) => {
|
||||
const conf = kind ? templates[kind] : null
|
||||
const templateId = body?.templateId || conf?.id || ''
|
||||
const targetUserId = Number(body?.userId) || Number(user.id)
|
||||
const dryRun = body?.dryRun === true
|
||||
const sendToAll = body?.sendToAll === true
|
||||
|
||||
const token = await getMiniAccessToken()
|
||||
const openid = body?.openid || (await getWechatOpenid(targetUserId))
|
||||
@@ -51,18 +60,6 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!openid) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'no-wechat-openid',
|
||||
hint: '该用户没有绑定微信(userIdentities 里没有 provider=wechat-mp 的记录),微信发不到他那里',
|
||||
kind: kind,
|
||||
templateId: templateId,
|
||||
tokenOk: !!token,
|
||||
targetUserId: targetUserId
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 各通知类型的**真实例子**:拿这些值走一遍 buildTemplateData → clampField,
|
||||
* 站长看到的 data 就是线上真会发出去的样子(比 '测试歌曲' 更能验字段名/长度)。
|
||||
@@ -83,9 +80,9 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
const data =
|
||||
body?.data && typeof body.data === 'object'
|
||||
? body.data
|
||||
const buildData = (override?: any) =>
|
||||
override && typeof override === 'object'
|
||||
? override
|
||||
: buildTemplateData(
|
||||
conf || { id: templateId },
|
||||
Object.assign(
|
||||
@@ -98,6 +95,103 @@ export default defineEventHandler(async (event) => {
|
||||
)
|
||||
)
|
||||
|
||||
/*
|
||||
* 我们代码里假定的字段映射(公众平台模板的关键词名)—— 微信模板是允许自己起关键词名的,
|
||||
* 站长选的模板如果不是我们猜的这几个名字,就会 47003(而且报错点名的字段不一定是真凶)。
|
||||
*/
|
||||
const assumedFields = conf?.fields ? Object.keys(conf.fields) : []
|
||||
const previewData = buildData(body?.data)
|
||||
const fieldMeta = assumedFields.map((name) => {
|
||||
const v = previewData[name]?.value ?? ''
|
||||
return {
|
||||
field: name,
|
||||
type: String(name).replace(/[0-9]+$/, ''),
|
||||
value: v === '' ? '' : v,
|
||||
empty: v === ''
|
||||
}
|
||||
})
|
||||
|
||||
if (dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
dryRun: true,
|
||||
hint: '⚠️ 这次**没有发送**,不消耗订阅额度。看上表:空值(empty)的字段微信会直接 47003;字段名也要和公众平台模板里的关键词名完全一致',
|
||||
configuredKinds: kinds,
|
||||
kind: kind,
|
||||
templateId: templateId,
|
||||
targetUserId: targetUserId,
|
||||
tokenOk: !!token,
|
||||
openid: openid ? openid.slice(0, 6) + '****' : null,
|
||||
assumedFields: assumedFields,
|
||||
fields: fieldMeta,
|
||||
quotaNote:
|
||||
'微信**不提供**「还剩几条授权」的查询接口:服务端只有真发一条、收到 43101 才知道没额度了。所以界面上那个「增加一条额度」只是本地计数,不代表微信侧的真实剩余量',
|
||||
data: previewData
|
||||
}
|
||||
}
|
||||
|
||||
if (sendToAll) {
|
||||
const rows: Array<{ userId: number; providerUserId: string }> = await db
|
||||
.select({ userId: userIdentities.userId, providerUserId: userIdentities.providerUserId })
|
||||
.from(userIdentities)
|
||||
.where(eq(userIdentities.provider, 'wechat-mp'))
|
||||
const uniq = Array.from(new Map(rows.map((r) => [r.userId, r])).values()).slice(0, 30)
|
||||
if (!uniq.length) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'no-wechat-openid',
|
||||
hint: '一个绑了微信的用户都没有(userIdentities 里没有 provider=wechat-mp),微信发不到任何人',
|
||||
kind: kind,
|
||||
templateId: templateId,
|
||||
tokenOk: !!token
|
||||
}
|
||||
}
|
||||
const results: any[] = []
|
||||
for (const r of uniq) {
|
||||
const res = await sendRawSubscribe(r.providerUserId, templateId, previewData)
|
||||
results.push({
|
||||
userId: r.userId,
|
||||
openid: String(r.providerUserId).slice(0, 6) + '****',
|
||||
ok: res.ok,
|
||||
errcode: res.errcode,
|
||||
errmsg: res.errmsg
|
||||
})
|
||||
if (res.ok) break // 有一个人成功就够证明字段名是对的,别再挨个烧额度
|
||||
}
|
||||
const anyOk = results.some((r) => r.ok)
|
||||
const codes = Array.from(new Set(results.map((r) => r.errcode)))
|
||||
return {
|
||||
ok: anyOk,
|
||||
hint: anyOk
|
||||
? '至少有一个用户发送成功 —— 字段名是对的,收不到的人只是「没有可用授权」(一次性订阅,每点一次授权只够发一条)'
|
||||
: codes.includes(47003)
|
||||
? '47003 = 字段名/长度不对:去公众平台模板里核对关键词名,或先用 dryRun 看 fields'
|
||||
: codes.every((c) => c === 43101)
|
||||
? '全部 43101 = 所有人当前都没有可用授权(用户要在小程序里点一次「开启微信通知」,且只够发一条)'
|
||||
: '看每条的 errmsg',
|
||||
configuredKinds: kinds,
|
||||
kind: kind,
|
||||
templateId: templateId,
|
||||
attempted: uniq.length,
|
||||
assumedFields: assumedFields,
|
||||
fields: fieldMeta,
|
||||
results: results
|
||||
}
|
||||
}
|
||||
|
||||
if (!openid) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'no-wechat-openid',
|
||||
hint: '该用户没有绑定微信(userIdentities 里没有 provider=wechat-mp 的记录),微信发不到他那里',
|
||||
kind: kind,
|
||||
templateId: templateId,
|
||||
tokenOk: !!token,
|
||||
targetUserId: targetUserId
|
||||
}
|
||||
}
|
||||
|
||||
const data = previewData
|
||||
const result = await sendRawSubscribe(openid, templateId, data)
|
||||
|
||||
return {
|
||||
@@ -108,7 +202,7 @@ export default defineEventHandler(async (event) => {
|
||||
result.errcode === 0
|
||||
? '发送成功,去微信里看「服务通知」'
|
||||
: result.errcode === 43101
|
||||
? '用户没有可用授权(一次性订阅:需要用户在小程序里点过「开启微信通知」才行)'
|
||||
? '用户没有可用授权(一次性订阅:需要用户在小程序里点过「开启微信通知」才行,而且每点一次只够发一条)'
|
||||
: '看 errmsg:47003 = 模板字段名/长度不对;40003 = openid 不对;40001 = token 失效',
|
||||
configuredKinds: kinds,
|
||||
kind: kind,
|
||||
@@ -116,6 +210,10 @@ export default defineEventHandler(async (event) => {
|
||||
targetUserId: targetUserId,
|
||||
openid: openid.slice(0, 6) + '****',
|
||||
tokenOk: !!token,
|
||||
assumedFields: assumedFields,
|
||||
fields: fieldMeta,
|
||||
quotaNote:
|
||||
'微信**不提供**「还剩几条授权」的查询接口:服务端只有真发一条、收到 43101 才知道没额度了。一次授权只够发一条,发完要用户再点一次「开启」',
|
||||
data: data,
|
||||
raw: result.raw
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user