把「微信订阅消息」这套代码按验收标准推翻重写:用户能自己看懂为什么收不到,
任何一步失败都必须看得见。对外函数名不变,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 通过。
29 lines
1.1 KiB
SQL
29 lines
1.1 KiB
SQL
-- 微信订阅消息的授权记账表
|
|
--
|
|
-- 背景:微信没有「还剩几条订阅授权」的查询接口,而一次性订阅是发一条扣一条。
|
|
-- 这里按 (userId, templateId) 记账,让服务端和体检接口能说清「为什么收不到」。
|
|
-- 记账只是近似值,不参与是否发送的硬判定(可用 force 绕过)。
|
|
|
|
CREATE TABLE IF NOT EXISTS "WechatSubscribeGrant" (
|
|
"id" serial PRIMARY KEY,
|
|
"userId" integer NOT NULL,
|
|
"templateId" text NOT NULL,
|
|
"kind" text,
|
|
"grantedCount" integer DEFAULT 0 NOT NULL,
|
|
"sentCount" integer DEFAULT 0 NOT NULL,
|
|
"blockedCount" integer DEFAULT 0 NOT NULL,
|
|
"lastGrantedAt" timestamp,
|
|
"lastSentAt" timestamp,
|
|
"lastBlockedAt" timestamp,
|
|
"lastErrorCode" integer,
|
|
"lastErrorMessage" text,
|
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "WechatSubscribeGrant_userId_templateId_unq"
|
|
ON "WechatSubscribeGrant" ("userId", "templateId");
|
|
|
|
CREATE INDEX IF NOT EXISTS "WechatSubscribeGrant_userId_idx"
|
|
ON "WechatSubscribeGrant" ("userId");
|