fix(微信订阅): 字段名按微信真实模板校准,不再靠猜编号;phrase 只截断不补齐
This commit is contained in:
@@ -28,10 +28,10 @@ test('thing 型:20 字以内原样,超了截 19 字 + 省略号(正好 20
|
||||
assert.ok(long.startsWith(clamped.slice(0, 19)))
|
||||
})
|
||||
|
||||
test('phrase 型:5 个以内纯汉字——“已排期”会被补成全角 5 字,不是原样发出去', () => {
|
||||
const padded = clampField('phrase5', '已排期')
|
||||
assert.equal(len(padded), 5)
|
||||
assert.ok(padded.startsWith('已排期'))
|
||||
test('phrase 型:5 个以内纯汉字——短的原样发(不补全角空格),超了截到 5', () => {
|
||||
// 「已排期」3 个汉字本来就合规,绝不能补成 "已排期 "(U+3000 不是汉字,会 47003)
|
||||
assert.equal(clampField('phrase5', '已排期'), '已排期')
|
||||
assert.equal(clampField('phrase5', '已排期').includes(' '), false)
|
||||
|
||||
// 刚好 5 个汉字原样保留
|
||||
assert.equal(clampField('phrase5', '已排期播放'), '已排期播放')
|
||||
@@ -106,7 +106,7 @@ test('SONG_SELECTED 的内置字段映射:每个字段名后缀都要跟发出
|
||||
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(len(out.phrase5), 5) // “已排期” 3 字 → 补齐到 5 字,避开 47003
|
||||
assert.equal(out.phrase5, '已排期') // 3 个汉字本来就合规,不补全角空格
|
||||
// 不能有任何字段是空值:空值同样会被微信拒
|
||||
for (const key of Object.keys(out)) assert.ok(out[key].length > 0, key + ' 不能为空')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* `server/utils/wechatSubscribeRemote.ts` 的单测:把微信模板原文解析成关键词表,
|
||||
* 再按类型/中文标签把我们的变量名对应到微信**真实分配**的关键词编号。
|
||||
*
|
||||
* 背景:公众平台创建模板时,关键词编号(thing1 / date2 …)是微信自动打的,猜不出来。
|
||||
* 猜错的表现就是整条消息 47003,而报错点名的字段往往不是真凶 —— 所以解析和映射必须能单独测。
|
||||
*
|
||||
* 运行:node --test test/wechatSubscribeRemote.test.mjs
|
||||
*/
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
keywordTypeOf,
|
||||
parseTemplateContent,
|
||||
resolveFieldsFromKeywords
|
||||
} from '../server/utils/wechatSubscribeRemote.ts'
|
||||
|
||||
/** 微信「获取已有模板列表」返回里 content 的真实形状 */
|
||||
const SONG_SELECTED_CONTENT =
|
||||
'歌曲名称:{{thing1.DATA}}\n播放日期:{{date2.DATA}}\n播放时间:{{time3.DATA}}\n序号:{{character_string4.DATA}}\n状态:{{phrase5.DATA}}\n'
|
||||
|
||||
const SONG_REJECTED_CONTENT = '审核内容:{{thing6.DATA}}\n驳回原因:{{thing4.DATA}}\n'
|
||||
|
||||
test('keywordTypeOf:去掉尾部数字得到关键词类型,认不出的记为 unknown', () => {
|
||||
assert.equal(keywordTypeOf('thing1'), 'thing')
|
||||
assert.equal(keywordTypeOf('character_string3'), 'character_string')
|
||||
assert.equal(keywordTypeOf('phone_number2'), 'phone_number')
|
||||
assert.equal(keywordTypeOf('foo2'), 'unknown')
|
||||
assert.equal(keywordTypeOf(''), 'unknown')
|
||||
})
|
||||
|
||||
test('parseTemplateContent:按出现顺序解析出 code / type / 中文标签', () => {
|
||||
const kws = parseTemplateContent(SONG_SELECTED_CONTENT)
|
||||
assert.deepEqual(
|
||||
kws.map((k) => k.code),
|
||||
['thing1', 'date2', 'time3', 'character_string4', 'phrase5']
|
||||
)
|
||||
assert.deepEqual(
|
||||
kws.map((k) => k.type),
|
||||
['thing', 'date', 'time', 'character_string', 'phrase']
|
||||
)
|
||||
assert.deepEqual(
|
||||
kws.map((k) => k.label),
|
||||
['歌曲名称', '播放日期', '播放时间', '序号', '状态']
|
||||
)
|
||||
assert.deepEqual(
|
||||
kws.map((k) => k.index),
|
||||
[0, 1, 2, 3, 4]
|
||||
)
|
||||
})
|
||||
|
||||
test('parseTemplateContent:没有占位符就返回空表,不会抛', () => {
|
||||
assert.deepEqual(parseTemplateContent(''), [])
|
||||
assert.deepEqual(parseTemplateContent('随便一段没有占位符的文字'), [])
|
||||
assert.deepEqual(parseTemplateContent(undefined), [])
|
||||
})
|
||||
|
||||
test('resolveFieldsFromKeywords:配置的编号和微信一致时保持 config 以外的等价结果', () => {
|
||||
const configured = {
|
||||
thing1: 'songTitle',
|
||||
date2: 'playDate',
|
||||
time3: 'playTime',
|
||||
character_string4: 'sequence',
|
||||
phrase5: 'status'
|
||||
}
|
||||
const r = resolveFieldsFromKeywords(parseTemplateContent(SONG_SELECTED_CONTENT), configured)
|
||||
assert.equal(r.source, 'wechat-api')
|
||||
assert.deepEqual(r.fields, configured)
|
||||
assert.deepEqual(r.invalidConfiguredCodes, [])
|
||||
assert.match(r.reason, /一致/)
|
||||
})
|
||||
|
||||
test('resolveFieldsFromKeywords:配置编号猜错了也能按类型/标签纠正回真实编号', () => {
|
||||
// 我们原来猜的是 thing2/date4/time1/character_string3/phrase5,实际是 thing1/date2/time3/character_string4/phrase5
|
||||
const configured = {
|
||||
thing2: 'songTitle',
|
||||
date4: 'playDate',
|
||||
time1: 'playTime',
|
||||
character_string3: 'sequence',
|
||||
phrase5: 'status'
|
||||
}
|
||||
const r = resolveFieldsFromKeywords(parseTemplateContent(SONG_SELECTED_CONTENT), configured)
|
||||
assert.equal(r.source, 'wechat-api')
|
||||
assert.deepEqual(r.fields, {
|
||||
thing1: 'songTitle',
|
||||
date2: 'playDate',
|
||||
time3: 'playTime',
|
||||
character_string4: 'sequence',
|
||||
phrase5: 'status'
|
||||
})
|
||||
// 配置里 4 个编号在微信模板里根本不存在 —— 这就是 47003 的直接证据
|
||||
assert.deepEqual(r.invalidConfiguredCodes.sort(), [
|
||||
'character_string3',
|
||||
'date4',
|
||||
'thing2',
|
||||
'time1'
|
||||
])
|
||||
assert.match(r.reason, /校准/)
|
||||
})
|
||||
|
||||
test('resolveFieldsFromKeywords:两个同类型关键词必须靠中文标签区分(驳回模板)', () => {
|
||||
const configured = { thing6: 'songTitle', thing4: 'reason' }
|
||||
const r = resolveFieldsFromKeywords(parseTemplateContent(SONG_REJECTED_CONTENT), configured)
|
||||
assert.equal(r.source, 'wechat-api')
|
||||
assert.deepEqual(r.fields, { thing6: 'songTitle', thing4: 'reason' })
|
||||
assert.deepEqual(r.invalidConfiguredCodes, [])
|
||||
})
|
||||
|
||||
test('resolveFieldsFromKeywords:两个 thing 但标签认不出来时,不冒险互换,沿用配置', () => {
|
||||
const content = '{{thing1.DATA}}\n{{thing2.DATA}}\n'
|
||||
const configured = { thing6: 'songTitle', thing4: 'reason' }
|
||||
const r = resolveFieldsFromKeywords(parseTemplateContent(content), configured)
|
||||
assert.equal(r.source, 'config')
|
||||
assert.deepEqual(r.fields, configured)
|
||||
assert.match(r.reason, /标签不足以区分/)
|
||||
})
|
||||
|
||||
test('resolveFieldsFromKeywords:拿不到关键词表时一律沿用配置', () => {
|
||||
const configured = { thing2: 'songTitle' }
|
||||
const r = resolveFieldsFromKeywords([], configured)
|
||||
assert.equal(r.source, 'config')
|
||||
assert.deepEqual(r.fields, configured)
|
||||
})
|
||||
|
||||
test('resolveFieldsFromKeywords:变量类型未知时不做自动映射', () => {
|
||||
const r = resolveFieldsFromKeywords(parseTemplateContent(SONG_SELECTED_CONTENT), {
|
||||
thing1: 'whateverCustomVar'
|
||||
})
|
||||
assert.equal(r.source, 'config')
|
||||
assert.match(r.reason, /类型未知/)
|
||||
})
|
||||
@@ -176,7 +176,7 @@ test('buildTemplateData:按映射取值,并交给 clamp 处理(time 有兜
|
||||
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(Array.from(data.phrase5.value).length, 5)
|
||||
assert.equal(data.phrase5.value, '已排期')
|
||||
assert.deepEqual(findEmptyDataFields(data), [], '有完整变量时不该出现空字段')
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user