fix(微信订阅): 用微信真实关键词修正歌曲排期模板(thing1/phrase2/time3/character_string4),并修掉字段认领顺序导致的校准失效
This commit is contained in:
@@ -350,6 +350,7 @@ export async function resolveTemplate(
|
||||
reason: `拿不到微信模板列表(${error || '未知原因'}),沿用配置里的字段名`,
|
||||
unresolvedVariables: [],
|
||||
invalidConfiguredCodes: [],
|
||||
unclaimedCodes: [],
|
||||
remoteError: error,
|
||||
remoteTitle: null,
|
||||
remoteContent: null,
|
||||
@@ -818,6 +819,9 @@ export async function sendWechatSubscribe(
|
||||
: '本次字段名是【沿用配置】的:'
|
||||
detail =
|
||||
`微信回 47003:模板字段名或字段值不合规。${hint}` +
|
||||
(resolution?.unclaimedCodes?.length
|
||||
? `还有关键词没被任何字段填上(${resolution.unclaimedCodes.join('、')}),微信会报「value is empty」。`
|
||||
: '') +
|
||||
'用 /api/admin/wechat/diagnose 可以看到微信侧的真实关键词名。' +
|
||||
(resolution?.remoteKeywords?.length
|
||||
? ` 微信侧关键词:${resolution.remoteKeywords
|
||||
@@ -973,6 +977,8 @@ export type TemplateStatus = {
|
||||
fieldReason: string
|
||||
/** 配置里有、但微信模板里根本没有的关键词名(有值就一定会 47003) */
|
||||
invalidConfiguredCodes: string[]
|
||||
/** 微信模板里有、但没有任何字段去填的关键词(微信会回 `data.xxx.value is empty`) */
|
||||
unclaimedCodes: string[]
|
||||
/** 微信侧真实的关键词表(拿不到时为 null) */
|
||||
remoteKeywords: Array<{ code: string; type: string; label: string }> | null
|
||||
/** 微信侧的模板原文(能看到真实的关键词与顺序) */
|
||||
@@ -1109,6 +1115,7 @@ export async function inspectWechatSubscribe(
|
||||
fieldSource: resolution?.source === 'wechat-api' ? 'wechat-api' : 'config',
|
||||
fieldReason: resolution?.reason || '没有模板 ID,无法校准',
|
||||
invalidConfiguredCodes: resolution?.invalidConfiguredCodes || [],
|
||||
unclaimedCodes: resolution?.unclaimedCodes || [],
|
||||
remoteKeywords: resolution?.remoteKeywords?.length
|
||||
? resolution.remoteKeywords.map((k) => ({
|
||||
code: k.code,
|
||||
@@ -1166,6 +1173,14 @@ export async function inspectWechatSubscribe(
|
||||
)
|
||||
}
|
||||
templateStatuses.forEach((t) => {
|
||||
// 「少填一个关键词」是 47003 `data.xxx.value is empty` 的直接原因,比字段名错更常见,先说
|
||||
if (t.unclaimedCodes.length) {
|
||||
actions.push(
|
||||
`模板「${t.title}」里有关键词没被任何字段填上:${t.unclaimedCodes.join('、')} —— ` +
|
||||
`微信会回 47003「data.${t.unclaimedCodes[0]}.value is empty」。` +
|
||||
`对照 remoteContent 把这几个关键词补进 fields 映射(少填一个就整条发不出去)`
|
||||
)
|
||||
}
|
||||
// 字段名是 47003 的头号嫌疑,体检里把它放在前面说清楚
|
||||
if (t.remoteError) {
|
||||
actions.push(
|
||||
|
||||
@@ -153,6 +153,11 @@ export type ResolveTemplateFieldsResult = {
|
||||
unresolvedVariables: string[]
|
||||
/** 配置里有、但微信模板里根本不存在的关键词名(这些字段微信会直接 47003) */
|
||||
invalidConfiguredCodes: string[]
|
||||
/**
|
||||
* 微信模板里有、但**没有任何变量去填**的关键词(形如 `thing1`)。
|
||||
* 微信对缺失的关键词会回 `47003 data.thing1.value is empty` —— 这种「少填」比「填错」更常见。
|
||||
*/
|
||||
unclaimedCodes: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,6 +185,8 @@ export function resolveFieldsFromKeywords(
|
||||
reason,
|
||||
unresolvedVariables: [] as string[],
|
||||
invalidConfiguredCodes,
|
||||
// 配置里没覆盖到的关键词 = 微信会报 value is empty 的那些
|
||||
unclaimedCodes: keywords.map((k) => k.code).filter((code) => !(code in config)),
|
||||
...extra
|
||||
})
|
||||
|
||||
@@ -201,37 +208,44 @@ export function resolveFieldsFromKeywords(
|
||||
const resolved: Record<string, string> = {}
|
||||
const unresolved: string[] = []
|
||||
|
||||
// 一轮:类型 + 标签双重命中,优先认领把握最大的
|
||||
/*
|
||||
* 评分规则:首选类型 +2、次选递减;中文标签命中再 +3(目的就是压过纯类型匹配)。
|
||||
*
|
||||
* ⚠️ **必须全局按分数从高到低认领**,不能「按变量顺序逐个挑」—— 早先就是这么写的,
|
||||
* 结果 playDate(首选 date,但模板里没有 date 字段)先把 character_string4 抢走了,
|
||||
* 因为它的次选类型里也有 character_string;轮到 sequence 时已经没有可用关键词,
|
||||
* 整张模板退回旧配置,发出去就是 47003。按分数排序后 sequence↔character_string4(分高)先成对。
|
||||
*/
|
||||
const pairs: Array<{ variable: string; code: string; score: number }> = []
|
||||
for (const variable of variables) {
|
||||
const allowed = VARIABLE_TYPES[variable]
|
||||
let best: { code: string; score: number } | null = null
|
||||
for (const kw of keywords) {
|
||||
if (used.has(kw.code)) continue
|
||||
const typeRank = allowed.indexOf(kw.type)
|
||||
if (typeRank < 0) continue
|
||||
// 首选类型 +2,备选类型递减;中文标签命中 +3(目的就是压过纯类型匹配)
|
||||
const score = (allowed.length - typeRank) * 2 + (labelHits(kw.label, variable) ? 3 : 0)
|
||||
if (!best || score > best.score) best = { code: kw.code, score }
|
||||
pairs.push({ variable, code: kw.code, score })
|
||||
}
|
||||
if (!best) {
|
||||
unresolved.push(variable)
|
||||
continue
|
||||
}
|
||||
used.add(best.code)
|
||||
resolved[best.code] = variable
|
||||
}
|
||||
// 同分时优先「标签命中」的(score 里已经含了,这里只是让顺序稳定)
|
||||
pairs.sort((a, b) => b.score - a.score)
|
||||
|
||||
for (const pair of pairs) {
|
||||
if (resolved[pair.code] !== undefined) continue
|
||||
if (Object.values(resolved).includes(pair.variable)) continue
|
||||
if (used.has(pair.code)) continue
|
||||
used.add(pair.code)
|
||||
resolved[pair.code] = pair.variable
|
||||
}
|
||||
|
||||
if (unresolved.length) {
|
||||
return configResult(
|
||||
`有变量在微信模板里找不到对应的关键词(${unresolved.join('、')}),沿用配置`,
|
||||
{ unresolvedVariables: unresolved }
|
||||
)
|
||||
for (const variable of variables) {
|
||||
if (!Object.values(resolved).includes(variable)) unresolved.push(variable)
|
||||
}
|
||||
const unclaimed = keywords.map((k) => k.code).filter((code) => !(code in resolved))
|
||||
|
||||
/*
|
||||
* 安全检查:同一类型出现多个关键词时,必须靠中文标签区分开。
|
||||
* 例如驳回模板有两个 thing,如果不看标签只按类型分配,songTitle 和 reason 会互换 ——
|
||||
* 能把消息发出去,但内容张冠李戴,比发不出去更糟。
|
||||
* 安全检查(先做,因为它是"不可采纳"的硬条件):同一类型出现多个关键词时,
|
||||
* 必须靠中文标签区分开。例如驳回模板有两个 thing,如果不看标签只按类型分配,
|
||||
* songTitle 和 reason 会互换 —— 能把消息发出去,但内容张冠李戴,比发不出去更糟。
|
||||
*/
|
||||
const byType = new Map<KeywordType, TemplateKeyword[]>()
|
||||
for (const kw of keywords) {
|
||||
@@ -260,6 +274,23 @@ export function resolveFieldsFromKeywords(
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 到这里已经通过了同类型区分的安全检查,剩下的唯一问题是「能不能把模板填满」:
|
||||
* - 模板里每个关键词都有变量认领(unclaimed 为空)→ 采用微信侧映射。
|
||||
* 此时即使有变量在模板里找不到位置(比如配置里写了模板没有的 date),也**直接忽略**它 ——
|
||||
* 发一个微信不认识的字段没有任何好处,只会让整条消息 47003。
|
||||
* - 还有关键词没人填 → 配置可能本来就指望它(例如配置里写了正确的 phrase5),
|
||||
* 这时候不该自作聪明覆盖配置,沿用配置更安全。
|
||||
*/
|
||||
if (unclaimed.length) {
|
||||
return configResult(
|
||||
`微信模板里还有关键词没被任何字段填上(${unclaimed.join('、')}),` +
|
||||
(unresolved.length ? `同时有变量在模板里找不到位置(${unresolved.join('、')}):` : '') +
|
||||
'为避免把模板改坏,沿用配置',
|
||||
{ unresolvedVariables: unresolved }
|
||||
)
|
||||
}
|
||||
|
||||
// 配置里的关键词名只要有一个在微信模板里不存在,就说明配置已经过期,改用校准结果
|
||||
const changed =
|
||||
configuredCodes.some((code) => !(code in resolved)) ||
|
||||
@@ -271,9 +302,11 @@ export function resolveFieldsFromKeywords(
|
||||
reason: changed
|
||||
? `按微信真实模板校准了字段名:${Object.entries(resolved)
|
||||
.map(([code, variable]) => `${code}=${variable}`)
|
||||
.join(',')}`
|
||||
.join(',')}` +
|
||||
(unresolved.length ? `(模板里没有对应关键词的变量已忽略:${unresolved.join('、')})` : '')
|
||||
: '配置与微信真实模板一致',
|
||||
unresolvedVariables: [],
|
||||
invalidConfiguredCodes
|
||||
unresolvedVariables: unresolved,
|
||||
invalidConfiguredCodes,
|
||||
unclaimedCodes: unclaimed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
*
|
||||
* | key | 用途 | 模板字段 → 变量 |
|
||||
* |----------------|----------------------------|----------------------------------------------------------------------------|
|
||||
* | SONG_SELECTED | 歌曲被排进排期/发布 | thing2→songTitle, date4→playDate, time1→playTime, character_string3→sequence, phrase5→status |
|
||||
* | SONG_SELECTED | 歌曲被排进排期/发布 | thing1→songTitle, phrase2→status, time3→playTime, character_string4→sequence |
|
||||
* | SONG_REJECTED | 投稿被驳回 | thing6→songTitle, thing4→reason |
|
||||
*
|
||||
* 换模板/改字段**不用改代码**:环境变量 `WECHAT_MINI_SUBSCRIBE_TEMPLATES` 是 JSON,按
|
||||
@@ -55,12 +55,23 @@ export const BUILTIN_TEMPLATES: Record<string, TemplateConf> = {
|
||||
SONG_SELECTED: {
|
||||
id: 'oLTAefO92xYYlGRYmcsm5AfUUlUI5IKFfXhIVNrItSk',
|
||||
title: '歌曲排期通知',
|
||||
/*
|
||||
* 这张模板的**真实**关键词表(从微信 /wxaapi/newtmpl/gettemplate 读出来的原文):
|
||||
* 歌曲名称:{{thing1.DATA}} → songTitle
|
||||
* 排期状态:{{phrase2.DATA}} → status
|
||||
* 播放时间:{{time3.DATA}} → playTime('YYYY-MM-DD HH:mm',日期就含在里面)
|
||||
* 排期序号:{{character_string4.DATA}} → sequence
|
||||
*
|
||||
* ⚠️ **没有日期字段**。早先这里是「按常见命名猜的」thing2/date4/time1/character_string3/phrase5,
|
||||
* 五个编号全错、还多出一个模板里根本不存在的 date4 —— 微信的表现是
|
||||
* `47003 argument invalid! data.thing1.value is empty`(我们没给 thing1,
|
||||
* 而它才是模板真正要的「歌曲名称」)。换模板时请对着后台的模板详情逐个核,不要猜编号。
|
||||
*/
|
||||
fields: {
|
||||
thing2: 'songTitle',
|
||||
date4: 'playDate',
|
||||
time1: 'playTime',
|
||||
character_string3: 'sequence',
|
||||
phrase5: 'status'
|
||||
thing1: 'songTitle',
|
||||
phrase2: 'status',
|
||||
time3: 'playTime',
|
||||
character_string4: 'sequence'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user