100 lines
4.0 KiB
TypeScript
100 lines
4.0 KiB
TypeScript
/**
|
||
* 微信**订阅消息**字段的取值规则(纯函数,不依赖 Nitro / 数据库,可直接用 node 跑验证)。
|
||
*
|
||
* 为什么单独放一个文件:微信的字段校验是**整条消息级别**的 —— 只要有一个字段超长、
|
||
* 格式不对或为空,整条消息发不出去,微信只回一个 `47003 argument invalid! data.xxx.value invalid`
|
||
* (而且报错里点名的字段不一定是真正有问题的那一个)。所以这些裁剪规则必须能单独测。
|
||
*
|
||
* 各类型限制(公众平台模板字段名后缀就是类型):
|
||
* thing 20 字
|
||
* phrase 5 个以内纯汉字(**只截断不补齐**:补齐会塞进非汉字字符,更容易 47003)
|
||
* time 必须是 'YYYY-MM-DD HH:mm'
|
||
* date 必须是 'YYYY-MM-DD'
|
||
* character_string 32 字(只能数字、字母)
|
||
* number/amount 32 字(只能数字)
|
||
*/
|
||
|
||
/** 各类型字段的长度上限 */
|
||
export const FIELD_MAX_LEN: Record<string, number> = {
|
||
thing: 20,
|
||
phrase: 5,
|
||
time: 24,
|
||
date: 10,
|
||
character_string: 32,
|
||
number: 32,
|
||
amount: 32,
|
||
digit: 32,
|
||
letter: 32,
|
||
symbol: 5
|
||
}
|
||
|
||
/** 字段名 → 类型(thing1/character_string3 → thing/character_string) */
|
||
export function fieldType(key: string): string {
|
||
return String(key || '').replace(/[0-9]+$/, '')
|
||
}
|
||
|
||
/**
|
||
* 按字段类型裁剪成微信能接受的值。
|
||
* ⚠️ 返回空字符串仍然会被微信拒,调用方要保证变量有值(见 wechatSubscribeService 的占位逻辑)。
|
||
*/
|
||
export function clampField(key: string, value: unknown): string {
|
||
const raw = value === null || value === undefined ? '' : String(value)
|
||
// 微信不认换行,先压成一行(站内通知的 message 是多行拼的)
|
||
const text = raw.replace(/\s+/g, ' ').trim()
|
||
const type = fieldType(key)
|
||
|
||
if (type === 'thing') {
|
||
// thing:20 字以内;超了截 19 字 + 省略号(省略号也算 1 个字,正好 20)
|
||
return Array.from(text).length > 20 ? Array.from(text).slice(0, 19).join('') + '…' : text
|
||
}
|
||
if (type === 'phrase') {
|
||
/*
|
||
* phrase = 5 个以内纯汉字。**只截断、不补齐**。
|
||
*
|
||
* 早先的版本为了绕开「社区反馈恰好 5 个汉字会报 47003」,把短词用全角空格补到 5 个字 ——
|
||
* 结果补进去的 U+3000 不是汉字,反而更容易被判「不是纯汉字」。现在按官方文档字面执行:
|
||
* 汉字数 ≤ 5 就原样发(「已排期」3 个字完全合规),超了才截。
|
||
*/
|
||
return Array.from(text).slice(0, 5).join('')
|
||
}
|
||
const max = FIELD_MAX_LEN[type] || 32
|
||
return text.slice(0, max)
|
||
}
|
||
|
||
/** 北京时间(UTC+8)的 'YYYY-MM-DD' */
|
||
export function beijingDateString(date: Date): string {
|
||
const t = new Date(date.getTime() + 8 * 60 * 60 * 1000)
|
||
const p = (n: number) => (n < 10 ? '0' + n : String(n))
|
||
return t.getUTCFullYear() + '-' + p(t.getUTCMonth() + 1) + '-' + p(t.getUTCDate())
|
||
}
|
||
|
||
/**
|
||
* 把播放日期 + 排期时段揉成微信 time 型字段要求的 'YYYY-MM-DD HH:mm'。
|
||
* 支持 '12:30'、'12:30-13:00'、'12:30~13:00'、'12:30'(全角冒号),取**开始时间**。
|
||
* 解析不出来时退化成当天 00:00 —— time 型字段不能为空,否则整条消息 47003。
|
||
*/
|
||
export function buildWechatTimeValue(date: Date, timeRange?: string | null): string {
|
||
const day = beijingDateString(date)
|
||
const m = String(timeRange || '').match(/(\d{1,2})\s*[::]\s*(\d{2})/)
|
||
if (!m) return day + ' 00:00'
|
||
const hh = String(Math.min(23, parseInt(m[1], 10))).padStart(2, '0')
|
||
return day + ' ' + hh + ':' + m[2]
|
||
}
|
||
|
||
/** 'YYYY-MM-DD HH:mm'(微信 time 型字段要求的格式,北京时间) */
|
||
export function beijingNow(): string {
|
||
const now = new Date(Date.now() + 8 * 60 * 60 * 1000)
|
||
const p = (n: number) => (n < 10 ? '0' + n : String(n))
|
||
return (
|
||
now.getUTCFullYear() +
|
||
'-' +
|
||
p(now.getUTCMonth() + 1) +
|
||
'-' +
|
||
p(now.getUTCDate()) +
|
||
' ' +
|
||
p(now.getUTCHours()) +
|
||
':' +
|
||
p(now.getUTCMinutes())
|
||
)
|
||
}
|