把「微信订阅消息」这套代码按验收标准推翻重写:用户能自己看懂为什么收不到,
任何一步失败都必须看得见。对外函数名不变,notificationService / test-subscribe
等调用点无需改动。
新增 server/utils/wechatSubscribeTemplates.ts(纯函数,零依赖):
- 模板登记表唯一来源(内置 SONG_SELECTED / SONG_REJECTED),服务层不再自己写死
- parseSubscribeTemplates 如实报告环境变量覆盖情况:坏 JSON 只报告不抛、
只给 id 时沿用内置字段映射、缺 id 的项记进 unknownKeys 并忽略
- findEmptyFields / findEmptyDataFields 发出前自查空字段(微信空值整条 47003)
- explainWechatErrcode 把 43101/47003/40003/40037/40001/45009 翻成人话
重写 server/services/wechatSubscribeService.ts:
- sendWechatSubscribe 返回 SendOutcome,用 stage 明确「卡在哪一步」:
no-template / disabled-by-user / no-wechat-openid / no-access-token /
empty-field / no-grant / rejected-no-quota / rejected-bad-field / rejected-other
- 每一次返回都写一行日志,不再有静默跳过(这是「发不出去全靠猜」的根因)
- inspectWechatSubscribe 只读体检:不发消息、不消耗额度,返回逐项 verdict + actions
- grantWechatSubscribe 依靠新增的 WechatSubscribeGrant 表记账:微信没有查询
剩余授权的接口,只能自己数(用户点允许 +1、发送成功 -1)
- 记账表不可用时失败开放(fail-open):额度预检关闭、照常发送,避免通知整体哑掉
- access_token 显式 responseType: json 并做字符串兜底解析
新增接口:
- GET /api/user/wechat-subscribe-status 小程序显示「还能收到几条 / 为什么收不到」
- POST /api/user/wechat-subscribe-grant 小程序授权成功后上报 accept 的模板
- GET /api/admin/wechat/diagnose 重写:人话结论 + actions,?send=1 真发一次
其他:新增 WechatSubscribeGrant 表(schema + 手写迁移)、.env.example 说明 JSON 覆盖
写法、.gitattributes 补 *.sql eol=lf。
测试:新增 test/wechatSubscribeTemplates.test.mjs(13 个用例,可直接 import .ts,
不依赖 Nuxt 别名),npm test 里订阅相关 25/25 通过。
46 lines
1.8 KiB
TypeScript
46 lines
1.8 KiB
TypeScript
import { grantWechatSubscribe } from '~~/server/services/wechatSubscribeService'
|
||
|
||
/**
|
||
* 小程序在 `wx.requestSubscribeMessage` 成功之后来报账。
|
||
*
|
||
* 为什么要上报:微信**没有**「这个用户还剩几条授权」的查询接口,而一次性订阅是
|
||
* 发一条扣一条。服务端自己记账,才能在小程序里说清「还能收到几条」,也才能在
|
||
* 发送前就告诉用户「没额度了,去点一下开启」。
|
||
*
|
||
* 约定:
|
||
* - `templateIds` **只传微信真的返回 `accept` 的模板**(`reject` / 没弹出来的别传);
|
||
* - 上报失败不影响用户已经拿到的授权,只是我们的计数会不准;
|
||
* - 服务端会把不在配置里的模板 ID 过滤掉,并写一行日志说明。
|
||
*/
|
||
export default defineEventHandler(async (event) => {
|
||
const user = event.context.user
|
||
if (!user) {
|
||
throw createError({ statusCode: 401, message: '未授权访问' })
|
||
}
|
||
|
||
const body = await readBody(event).catch(() => null)
|
||
const raw = Array.isArray(body?.templateIds) ? body.templateIds : []
|
||
const templateIds = raw.filter(
|
||
(id: unknown): id is string => typeof id === 'string' && id.length > 0
|
||
)
|
||
|
||
if (!templateIds.length) {
|
||
throw createError({
|
||
statusCode: 400,
|
||
message: 'templateIds 不能为空(只传微信返回 accept 的模板 ID)'
|
||
})
|
||
}
|
||
|
||
const result = await grantWechatSubscribe(Number(user.id), templateIds, 'miniprogram')
|
||
return {
|
||
ok: result.ok,
|
||
// 只回模板键,不回 ID 清单,日志里也不打印完整 ID
|
||
grantedKinds: result.granted.map((g) => g.kind || 'unknown'),
|
||
granted: result.granted.length,
|
||
error: result.error || null,
|
||
hint: result.ok
|
||
? '已记账:每点一次「允许」记 1 条额度,服务端发一条扣一条'
|
||
: '记账失败,但不影响你已经拿到的微信授权'
|
||
}
|
||
})
|