真实联调发现:抖店大量 id 是 19 位数字,超过 Number.MAX_SAFE_INTEGER,
入站推送我们用 express.text() 收原文保住了精度,但**出网响应是 JSON.parse 出来的**,
19 位数字在解析瞬间就被压成相邻 double:
平台真值 product_id = 3847255376114352567
我们存到的 = 3847255376114352600
后果:拿这个 product_id 去调 product.detail 报「商品不存在」(实测),
任何按商品ID做的映射/查询都会错。
修复:抖音出网响应改用共享的 parseJsonObject(..., { preserveLargeIntegers: true })。
utils/json.ts 里其实早有 wrapLargeIntegerLiterals(≥16 位/超 MAX_SAFE_INTEGER 才包裹,
只处理值位置的整数字面量,不误伤字符串内数字与短数字),但全仓没有调用方 —— 现在接上。
实测:同一笔订单 product_id 已精确为 …567,product.detail 返回 code=10000 并拿到商品图。
补了 json 解析的大整数单测(含「字符串里的数字、短数字不受影响」的反例)。
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import {
|
||
parseJsonObject,
|
||
sanitizeWellFormedUnicode,
|
||
deepSanitizeUnicode,
|
||
safeJsonStringify,
|
||
wrapLargeIntegerLiterals,
|
||
} from './json.js'
|
||
import type { JsonObject } from '../types/json.js'
|
||
|
||
test('sanitizeWellFormedUnicode handles null bytes and lone surrogates', () => {
|
||
// Null byte
|
||
assert.equal(sanitizeWellFormedUnicode('hello\0world'), 'helloworld')
|
||
|
||
// Lone high surrogate \ud835
|
||
const loneSurrogate = 'nickname\ud835test'
|
||
const cleaned = sanitizeWellFormedUnicode(loneSurrogate)
|
||
assert.equal(cleaned, 'nickname\uFFFDtest')
|
||
|
||
// Valid emoji surrogate pair \uD83D\uDE00 (😀) should remain intact
|
||
const validEmoji = 'Hello 😀'
|
||
assert.equal(sanitizeWellFormedUnicode(validEmoji), 'Hello 😀')
|
||
})
|
||
|
||
test('deepSanitizeUnicode recursively cleans objects and arrays', () => {
|
||
const input = {
|
||
user: 'buyer\ud83c',
|
||
nested: {
|
||
comment: 'good\0deal',
|
||
tags: ['cool\ud835', 'ok'],
|
||
},
|
||
}
|
||
|
||
const output = deepSanitizeUnicode(input)
|
||
assert.deepEqual(output, {
|
||
user: 'buyer\uFFFD',
|
||
nested: {
|
||
comment: 'gooddeal',
|
||
tags: ['cool\uFFFD', 'ok'],
|
||
},
|
||
})
|
||
})
|
||
|
||
test('safeJsonStringify produces valid JSON safe for postgres jsonb', () => {
|
||
const input = {
|
||
orderSn: '260919-123456',
|
||
nickname: '\ud835\ud83c*buyer',
|
||
nullByte: 'abc\0def',
|
||
}
|
||
|
||
const json = safeJsonStringify(input)
|
||
assert.ok(typeof json === 'string')
|
||
const parsed = JSON.parse(json)
|
||
assert.equal(parsed.nickname, '\uFFFD\uFFFD*buyer')
|
||
assert.equal(parsed.nullByte, 'abcdef')
|
||
})
|
||
|
||
test('preserveLargeIntegers:19 位 id 不被压精度(真实踩坑)', () => {
|
||
// 真实案例:抖店 product_id 真值 …567,直接 JSON.parse 会变成 …600
|
||
const raw =
|
||
'{"code":"10000","data":{"sku_order_list":[{"code":"幸运币2个","product_id":3847255376114352567,"sku_id":3383719439799589,"pay_amount":100}]}}'
|
||
|
||
assert.equal(JSON.parse(raw).data.sku_order_list[0].product_id, 3847255376114352600)
|
||
|
||
const safe = parseJsonObject(raw, { preserveLargeIntegers: true })
|
||
const row = (safe.data as JsonObject).sku_order_list as JsonObject[]
|
||
assert.equal(row[0]?.product_id, '3847255376114352567')
|
||
// 16 位但未超 MAX_SAFE_INTEGER 的数字保持不变(不误伤)
|
||
assert.equal(row[0]?.sku_id, 3383719439799589)
|
||
// 小整数保持数字类型
|
||
assert.equal(row[0]?.pay_amount, 100)
|
||
})
|
||
|
||
test('preserveLargeIntegers:不动字符串里的数字,也不动短数字', () => {
|
||
const raw =
|
||
'{"order_id":"6930318258716179555","note":"单号6930318258716179555","pay_time":1791521705}'
|
||
const safe = parseJsonObject(raw, { preserveLargeIntegers: true })
|
||
|
||
assert.equal(safe.order_id, '6930318258716179555')
|
||
assert.equal(safe.note, '单号6930318258716179555')
|
||
assert.equal(safe.pay_time, 1791521705)
|
||
})
|
||
|
||
test('wrapLargeIntegerLiterals 只包裹超大整数字面量', () => {
|
||
assert.equal(wrapLargeIntegerLiterals('{"a":123}'), '{"a":123}')
|
||
assert.equal(wrapLargeIntegerLiterals('{"a":3847255376114352567}'), '{"a":"3847255376114352567"}')
|
||
assert.equal(
|
||
wrapLargeIntegerLiterals('{"a":"3847255376114352567"}'),
|
||
'{"a":"3847255376114352567"}',
|
||
)
|
||
})
|