fix(微信订阅): 用微信真实关键词修正歌曲排期模板(thing1/phrase2/time3/character_string4),并修掉字段认领顺序导致的校准失效
This commit is contained in:
+6
-2
@@ -125,7 +125,9 @@ WECHAT_MINI_SECRET=
|
||||
# 微信「订阅消息」模板(可选:**留空也能用**,代码里内置了站长选好的模板)
|
||||
# 内置模板(唯一来源:server/utils/wechatSubscribeTemplates.ts 的 BUILTIN_TEMPLATES):
|
||||
# SONG_SELECTED「歌曲排期通知」—— 管理员把歌排进排期/发布排期时发
|
||||
# thing2→songTitle, date4→playDate, time1→playTime, character_string3→sequence, phrase5→status
|
||||
# thing1→songTitle, phrase2→status, time3→playTime, character_string4→sequence
|
||||
# (微信侧真实关键词,模板原文:歌曲名称/排期状态/播放时间/排期序号;**没有日期字段**,
|
||||
# 日期含在 playTime 的 'YYYY-MM-DD HH:mm' 里)
|
||||
# SONG_REJECTED「审核驳回通知」—— 投稿被驳回时发
|
||||
# thing6→songTitle, thing4→reason
|
||||
# 下面这条环境变量**只用来覆盖内置映射**(比如站长在公众平台选了别的模板或字段名)。
|
||||
@@ -134,7 +136,9 @@ WECHAT_MINI_SECRET=
|
||||
# {"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,公众平台选用模板时能看到),
|
||||
# fields 的键是模板里的字段名(thing1/phrase2/time3/character_string4,公众平台选用模板时能看到;
|
||||
# **编号是微信自动分配的,不能靠命名规律猜** —— 猜错一位就是整条 47003。核不准就别写,
|
||||
# 服务端会自动调 /wxaapi/newtmpl/gettemplate 读模板原文来校准),
|
||||
# 值是变量名(可用:songTitle / message / time / playDate / playTime / sequence / status / reason)。
|
||||
# ⚠️ 平台限制:一次性订阅=一次授权只能发一条;只有绑定了微信的账号才有 openid;
|
||||
# 模板 ID 变了老用户要**重新点一次「开启微信通知」**才有额度(旧授权按模板 ID 记账,直接作废)。
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,17 +84,23 @@ test('空白与换行会被压成一行(微信不接受换行)', () => {
|
||||
})
|
||||
|
||||
test('SONG_SELECTED 的内置字段映射:每个字段名后缀都要跟发出去的值类型匹配', () => {
|
||||
// 字段映射的唯一来源是 server/utils/wechatSubscribeTemplates.ts(那里有更完整的测试)
|
||||
/*
|
||||
* 字段映射的唯一来源是 server/utils/wechatSubscribeTemplates.ts(那里有更完整的测试)。
|
||||
* 这里的重点不是"是不是这几个编号",而是**每个键的后缀(类型)和值匹配**:
|
||||
* thing1 / phrase2 / time3 / character_string4
|
||||
* 真实模板内容(微信 /wxaapi/newtmpl/gettemplate 读出来):
|
||||
* 歌曲名称:{{thing1.DATA}} 排期状态:{{phrase2.DATA}}
|
||||
* 播放时间:{{time3.DATA}} 排期序号:{{character_string4.DATA}}
|
||||
* ⚠️ 这张模板**没有日期字段**,日期含在 time3 的 'YYYY-MM-DD HH:mm' 里。
|
||||
*/
|
||||
const fields = {
|
||||
thing2: 'songTitle',
|
||||
date4: 'playDate',
|
||||
time1: 'playTime',
|
||||
character_string3: 'sequence',
|
||||
phrase5: 'status'
|
||||
thing1: 'songTitle',
|
||||
phrase2: 'status',
|
||||
time3: 'playTime',
|
||||
character_string4: 'sequence'
|
||||
}
|
||||
const vars = {
|
||||
songTitle: '晴天',
|
||||
playDate: '2026-02-14',
|
||||
playTime: '2026-02-14 12:30',
|
||||
sequence: '1',
|
||||
status: '已排期'
|
||||
@@ -102,11 +108,10 @@ test('SONG_SELECTED 的内置字段映射:每个字段名后缀都要跟发出
|
||||
const out = {}
|
||||
for (const key of Object.keys(fields)) out[key] = clampField(key, vars[fields[key]])
|
||||
|
||||
assert.equal(out.thing2, '晴天')
|
||||
assert.equal(out.date4, '2026-02-14')
|
||||
assert.match(out.time1, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)
|
||||
assert.equal(out.character_string3, '1')
|
||||
assert.equal(out.phrase5, '已排期') // 3 个汉字本来就合规,不补全角空格
|
||||
assert.equal(out.thing1, '晴天')
|
||||
assert.match(out.time3, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/)
|
||||
assert.equal(out.character_string4, '1')
|
||||
assert.equal(out.phrase2, '已排期') // 3 个汉字本来就合规,不补全角空格
|
||||
// 不能有任何字段是空值:空值同样会被微信拒
|
||||
for (const key of Object.keys(out)) assert.ok(out[key].length > 0, key + ' 不能为空')
|
||||
})
|
||||
@@ -160,9 +165,12 @@ test('内置模板与线上代码一致(直接读源码,防止映射被改
|
||||
|
||||
const selected = block('SONG_SELECTED')
|
||||
assert.match(selected, /oLTAefO92xYYlGRYmcsm5AfUUlUI5IKFfXhIVNrItSk/)
|
||||
for (const f of ['thing2', 'date4', 'time1', 'character_string3', 'phrase5']) {
|
||||
// 这四个编号是微信侧真实分配的(见上方模板原文),改了就必须同步改这里
|
||||
for (const f of ['thing1', 'phrase2', 'time3', 'character_string4']) {
|
||||
assert.match(selected, new RegExp(f + ":\\s*'"), f + ' 的映射丢了')
|
||||
}
|
||||
// 这张模板没有日期字段,配置里不该出现 date 型键
|
||||
assert.doesNotMatch(selected, /date\d+:/, 'SONG_SELECTED 的模板里没有日期关键词')
|
||||
})
|
||||
|
||||
test('beijingNow 是合法的 time 型值,且与北京时间一致', () => {
|
||||
|
||||
@@ -123,6 +123,66 @@ test('resolveFieldsFromKeywords:拿不到关键词表时一律沿用配置', (
|
||||
assert.deepEqual(r.fields, configured)
|
||||
})
|
||||
|
||||
/**
|
||||
* 回归:线上真实踩过的坑。
|
||||
*
|
||||
* 旧配置是「按常见命名猜的」thing2/date4/time1/character_string3/phrase5,五个编号全错,
|
||||
* 而且模板里**根本没有日期关键词**。当时按变量顺序贪心认领,playDate(首选 date,模板里
|
||||
* 没有 date)先把 character_string4 抢走,轮到 sequence 就没位置了,整张模板退回旧配置,
|
||||
* 发出去就是 `47003 data.thing1.value is empty`。
|
||||
*/
|
||||
const REAL_SONG_SELECTED_CONTENT =
|
||||
'歌曲名称:{{thing1.DATA}}\n排期状态:{{phrase2.DATA}}\n播放时间:{{time3.DATA}}\n排期序号:{{character_string4.DATA}}\n'
|
||||
|
||||
test('回归:真实排期模板 + 猜错的旧配置 → 校准成正确的四个编号,并丢掉模板里没有的 playDate', () => {
|
||||
const wrongConfigured = {
|
||||
thing2: 'songTitle',
|
||||
date4: 'playDate',
|
||||
time1: 'playTime',
|
||||
character_string3: 'sequence',
|
||||
phrase5: 'status'
|
||||
}
|
||||
const r = resolveFieldsFromKeywords(
|
||||
parseTemplateContent(REAL_SONG_SELECTED_CONTENT),
|
||||
wrongConfigured
|
||||
)
|
||||
assert.equal(r.source, 'wechat-api')
|
||||
assert.deepEqual(r.fields, {
|
||||
thing1: 'songTitle',
|
||||
phrase2: 'status',
|
||||
time3: 'playTime',
|
||||
character_string4: 'sequence'
|
||||
})
|
||||
// 模板里没有日期关键词 → playDate 被忽略(发了微信也不认)
|
||||
assert.deepEqual(r.unresolvedVariables, ['playDate'])
|
||||
assert.deepEqual(r.unclaimedCodes, [], '四个关键词必须全部填上,否则微信会报 value is empty')
|
||||
assert.match(r.reason, /已忽略/)
|
||||
})
|
||||
|
||||
test('回归:真实排期模板 + 已修正的内置配置 → 一致,不做任何改动', () => {
|
||||
const good = {
|
||||
thing1: 'songTitle',
|
||||
phrase2: 'status',
|
||||
time3: 'playTime',
|
||||
character_string4: 'sequence'
|
||||
}
|
||||
const r = resolveFieldsFromKeywords(parseTemplateContent(REAL_SONG_SELECTED_CONTENT), good)
|
||||
assert.equal(r.source, 'wechat-api')
|
||||
assert.deepEqual(r.fields, good)
|
||||
assert.deepEqual(r.invalidConfiguredCodes, [])
|
||||
assert.deepEqual(r.unclaimedCodes, [])
|
||||
assert.equal(r.reason, '配置与微信真实模板一致')
|
||||
})
|
||||
|
||||
test('unclaimedCodes:模板里有、配置里没有的关键词要被点名(这是 value is empty 的来源)', () => {
|
||||
const r = resolveFieldsFromKeywords(parseTemplateContent(REAL_SONG_SELECTED_CONTENT), {
|
||||
thing1: 'songTitle'
|
||||
})
|
||||
assert.deepEqual(r.unclaimedCodes.sort(), ['character_string4', 'phrase2', 'time3'])
|
||||
// 填不满模板时不动配置
|
||||
assert.equal(r.source, 'config')
|
||||
})
|
||||
|
||||
test('resolveFieldsFromKeywords:变量类型未知时不做自动映射', () => {
|
||||
const r = resolveFieldsFromKeywords(parseTemplateContent(SONG_SELECTED_CONTENT), {
|
||||
thing1: 'whateverCustomVar'
|
||||
|
||||
@@ -30,12 +30,18 @@ test('内置模板:两个模板的 ID 与字段映射和公众平台一致', (
|
||||
|
||||
const selected = BUILTIN_TEMPLATES.SONG_SELECTED
|
||||
assert.equal(selected.id, 'oLTAefO92xYYlGRYmcsm5AfUUlUI5IKFfXhIVNrItSk')
|
||||
/*
|
||||
* 这四个键是微信侧真实分配的编号(模板原文:
|
||||
* 歌曲名称:{{thing1.DATA}} 排期状态:{{phrase2.DATA}}
|
||||
* 播放时间:{{time3.DATA}} 排期序号:{{character_string4.DATA}}
|
||||
* )。这张模板没有日期关键词 —— 曾经在这里写过一个不存在的 date4,
|
||||
* 微信就回 `47003 data.thing1.value is empty`。
|
||||
*/
|
||||
assert.deepEqual(selected.fields, {
|
||||
thing2: 'songTitle',
|
||||
date4: 'playDate',
|
||||
time1: 'playTime',
|
||||
character_string3: 'sequence',
|
||||
phrase5: 'status'
|
||||
thing1: 'songTitle',
|
||||
phrase2: 'status',
|
||||
time3: 'playTime',
|
||||
character_string4: 'sequence'
|
||||
})
|
||||
|
||||
// 每个字段名都要带类型后缀(thing2/date4/…),否则 clampField 认不出类型、不会裁剪
|
||||
@@ -139,17 +145,16 @@ test('findEmptyFields:变量没给或全是空白 → 点名到字段和变量
|
||||
findEmptyFields(conf, {})
|
||||
.map((f) => f.field)
|
||||
.sort(),
|
||||
['character_string3', 'date4', 'phrase5', 'thing2', 'time1']
|
||||
['character_string4', 'phrase2', 'thing1', 'time3']
|
||||
)
|
||||
|
||||
const partial = findEmptyFields(conf, {
|
||||
songTitle: '晴天',
|
||||
playDate: ' ',
|
||||
playTime: '2026-02-14 12:30',
|
||||
playTime: ' ',
|
||||
sequence: '1',
|
||||
status: '已排期'
|
||||
})
|
||||
assert.deepEqual(partial, [{ field: 'date4', variable: 'playDate' }])
|
||||
assert.deepEqual(partial, [{ field: 'time3', variable: 'playTime' }])
|
||||
})
|
||||
|
||||
test('findEmptyDataFields:拼完 data 再兜一层空值检查', () => {
|
||||
@@ -173,10 +178,10 @@ test('buildTemplateData:按映射取值,并交给 clamp 处理(time 有兜
|
||||
},
|
||||
clampField
|
||||
)
|
||||
assert.deepEqual(Object.keys(data), ['thing2', 'date4', 'time1', 'character_string3', 'phrase5'])
|
||||
assert.equal(data.thing2.value, '晴天')
|
||||
assert.equal(data.character_string3.value, '3')
|
||||
assert.equal(data.phrase5.value, '已排期')
|
||||
assert.deepEqual(Object.keys(data), ['thing1', 'phrase2', 'time3', 'character_string4'])
|
||||
assert.equal(data.thing1.value, '晴天')
|
||||
assert.equal(data.character_string4.value, '3')
|
||||
assert.equal(data.phrase2.value, '已排期')
|
||||
assert.deepEqual(findEmptyDataFields(data), [], '有完整变量时不该出现空字段')
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user