1. 在 utils/json.ts 新增 sanitizeWellFormedUnicode、deepSanitizeUnicode 和 safeJsonStringify,自动清理空字节 (\0) 并将畸变孤立 UTF-16 代理字符 (Lone Surrogates) 安全转换为规范字符,杜绝 PostgreSQL jsonb 解析错误; 2. 拼多多订单映射器与通用进单服务 (order-service) 全面接入 Unicode 深度清洗与安全序列化,确保买家昵称/留言中的各类特殊 Emoji 和生僻字符平滑落库; 3. 数据库仓储层 (order-repo / order-item-repo) 针对 raw_payload_json、spec_json、item_snapshot_json 等 jsonb 字段增加兜底安全序列化保护; 4. 补充 utils/json.test.ts 与 pdd-order-mapper.test.ts 单元测试,覆盖畸变字符与空字节场景。
130 lines
3.6 KiB
TypeScript
130 lines
3.6 KiB
TypeScript
import type { JsonObject } from '../types/json.js'
|
|
import { createHttpError } from './http.js'
|
|
|
|
const LARGE_INTEGER_PATTERN = /(:\s*)(-?\d{16,})(\s*[,}\]])/g
|
|
|
|
type ParseJsonObjectOptions = {
|
|
preserveLargeIntegers?: boolean
|
|
throwOnError?: boolean
|
|
}
|
|
|
|
export function parseJsonObject(
|
|
rawText: unknown,
|
|
{ preserveLargeIntegers = false, throwOnError = false }: ParseJsonObjectOptions = {},
|
|
): JsonObject {
|
|
const normalized = String(rawText || '').trim()
|
|
|
|
if (!normalized) {
|
|
return {}
|
|
}
|
|
|
|
try {
|
|
const prepared = preserveLargeIntegers ? wrapLargeIntegerLiterals(normalized) : normalized
|
|
const parsed = JSON.parse(prepared)
|
|
return isPlainObject(parsed) ? parsed : {}
|
|
} catch (error) {
|
|
if (!throwOnError) {
|
|
return {}
|
|
}
|
|
|
|
throw createHttpError('json 参数不是合法 JSON', {
|
|
statusCode: 400,
|
|
errorCode: 'invalid_json_payload',
|
|
cause: error,
|
|
})
|
|
}
|
|
}
|
|
|
|
export function wrapLargeIntegerLiterals(rawText: unknown): string {
|
|
return String(rawText || '').replace(LARGE_INTEGER_PATTERN, (_match, prefix, digits, suffix) => {
|
|
if (!shouldPreserveIntegerToken(digits)) {
|
|
return `${prefix}${digits}${suffix}`
|
|
}
|
|
|
|
return `${prefix}"${digits}"${suffix}`
|
|
})
|
|
}
|
|
|
|
function shouldPreserveIntegerToken(rawDigits: unknown): boolean {
|
|
const digits = String(rawDigits || '').trim()
|
|
const unsigned = digits.startsWith('-') ? digits.slice(1) : digits
|
|
|
|
if (!/^\d+$/.test(unsigned)) {
|
|
return false
|
|
}
|
|
|
|
if (unsigned.length > 16) {
|
|
return true
|
|
}
|
|
|
|
if (unsigned.length < 16) {
|
|
return false
|
|
}
|
|
|
|
return unsigned > '9007199254740991'
|
|
}
|
|
|
|
export function sanitizeWellFormedUnicode(value: unknown): string {
|
|
if (value === null || value === undefined) return ''
|
|
const str = String(value)
|
|
if (!str) return ''
|
|
// 1. Remove null bytes (\u0000) which PostgreSQL text/jsonb columns strictly forbid
|
|
const withoutNull = str.replace(/\0/g, '')
|
|
// 2. Convert lone surrogates into replacement character U+FFFD
|
|
const toWellFormedFn = (withoutNull as { toWellFormed?: () => string }).toWellFormed
|
|
return typeof toWellFormedFn === 'function'
|
|
? toWellFormedFn.call(withoutNull)
|
|
: withoutNull.replace(
|
|
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,
|
|
'\uFFFD',
|
|
)
|
|
}
|
|
|
|
export function deepSanitizeUnicode<T>(value: T): T {
|
|
if (value === null || value === undefined) {
|
|
return value
|
|
}
|
|
if (typeof value === 'string') {
|
|
return sanitizeWellFormedUnicode(value) as unknown as T
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => deepSanitizeUnicode(item)) as unknown as T
|
|
}
|
|
if (isPlainObject(value)) {
|
|
const res: Record<string, unknown> = {}
|
|
for (const [k, v] of Object.entries(value)) {
|
|
const cleanKey = sanitizeWellFormedUnicode(k)
|
|
res[cleanKey] = deepSanitizeUnicode(v)
|
|
}
|
|
return res as unknown as T
|
|
}
|
|
return value
|
|
}
|
|
|
|
export function safeJsonStringify(value: unknown, fallback = '{}'): string {
|
|
if (value === undefined || value === null) {
|
|
return fallback
|
|
}
|
|
if (typeof value === 'string') {
|
|
const trimmed = value.trim()
|
|
if (!trimmed) return fallback
|
|
try {
|
|
const parsed = JSON.parse(trimmed)
|
|
const sanitized = deepSanitizeUnicode(parsed)
|
|
return JSON.stringify(sanitized)
|
|
} catch {
|
|
return JSON.stringify(sanitizeWellFormedUnicode(value))
|
|
}
|
|
}
|
|
try {
|
|
const sanitized = deepSanitizeUnicode(value)
|
|
return JSON.stringify(sanitized)
|
|
} catch {
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
function isPlainObject(value: unknown): value is JsonObject {
|
|
return Object.prototype.toString.call(value) === '[object Object]'
|
|
}
|