refactor(微信订阅): 重写订阅消息链路,每一步失败都可见
把「微信订阅消息」这套代码按验收标准推翻重写:用户能自己看懂为什么收不到,
任何一步失败都必须看得见。对外函数名不变,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 通过。
This commit is contained in:
+16
-10
@@ -123,15 +123,21 @@ WECHAT_MINI_APPID=
|
||||
WECHAT_MINI_SECRET=
|
||||
|
||||
# 微信「订阅消息」模板(可选:**留空也能用**,代码里内置了站长选好的模板)
|
||||
# 内置模板(见 server/services/wechatSubscribeService.ts 的 DEFAULT_TEMPLATES):
|
||||
# SONG_SELECTED「歌曲排期」—— 管理员把歌排进排期/发布排期时发
|
||||
# SONG_REJECTED「歌曲驳回」—— 投稿被驳回时发
|
||||
# 下面这条环境变量**只用来覆盖内置映射**(比如站长在公众平台选了别的字段名):
|
||||
# {"SONG_SELECTED":{"id":"模板ID","fields":{"thing2":"songTitle","time1":"playTime"}}}
|
||||
# 键是通知类型(SONG_SELECTED / SONG_PLAYED / SONG_REJECTED / SYSTEM_NOTICE),
|
||||
# fields 的键是模板里的字段名(thing2/date4/time1/character_string3/phrase5,选用模板时能看到),
|
||||
# 值是变量名(内置变量:songTitle / message / time / playDate / playTime / sequence / status)。
|
||||
# 同键只给 id 不给 fields 时,会沿用内置的字段映射(不会退化成 thing1/time2/thing3)。
|
||||
# 内置模板(唯一来源:server/utils/wechatSubscribeTemplates.ts 的 BUILTIN_TEMPLATES):
|
||||
# SONG_SELECTED「歌曲排期通知」—— 管理员把歌排进排期/发布排期时发
|
||||
# thing2→songTitle, date4→playDate, time1→playTime, character_string3→sequence, phrase5→status
|
||||
# SONG_REJECTED「审核驳回通知」—— 投稿被驳回时发
|
||||
# thing6→songTitle, thing4→reason
|
||||
# 下面这条环境变量**只用来覆盖内置映射**(比如站长在公众平台选了别的模板或字段名)。
|
||||
# ⚠️ 必须是合法 JSON;写了坏值(例如只填一个光秃秃的模板 ID)服务端会**忽略它**并在启动日志里
|
||||
# 打一行「不是合法 JSON,已忽略(仍用内置默认模板)」,属于可恢复状态,但请在体检接口里确认。
|
||||
# {"SONG_SELECTED":{"id":"模板ID"}} ← 只换 ID,沿用内置字段映射
|
||||
# {"SONG_REJECTED":{"id":"模板ID","fields":{"thing6":"songTitle"}}} ← 同时换字段映射
|
||||
# {"SONG_PLAYED":{"id":"模板ID","fields":{"thing1":"songTitle"}}} ← 也可以补一张内置表里没有的模板
|
||||
# fields 的键是模板里的字段名(thing2/date4/time1/character_string3/phrase5,公众平台选用模板时能看到),
|
||||
# 值是变量名(可用:songTitle / message / time / playDate / playTime / sequence / status / reason)。
|
||||
# ⚠️ 平台限制:一次性订阅=一次授权只能发一条;只有绑定了微信的账号才有 openid;
|
||||
# 模板 ID 变了老用户要**重新点一次「开启微信通知」**才有额度。
|
||||
# 模板 ID 变了老用户要**重新点一次「开启微信通知」**才有额度(旧授权按模板 ID 记账,直接作废)。
|
||||
# 发不出去时先看 GET /api/admin/wechat/diagnose(超管,只读不消耗额度):
|
||||
# 它会把「拿不拿到 token / 有没有绑微信 / 通知开关开没开 / 还剩几条授权」逐项说清。
|
||||
WECHAT_MINI_SUBSCRIBE_TEMPLATES=
|
||||
|
||||
Reference in New Issue
Block a user