Files
order_site/apps/backend/src/utils/json.test.ts
T
yml2213 ecc8ffaee4 fix(douyin-mashida): 出网响应按大整数安全解析(product_id 被压精度)
真实联调发现:抖店大量 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 解析的大整数单测(含「字符串里的数字、短数字不受影响」的反例)。
2026-10-09 14:47:55 +08:00

93 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"}',
)
})