一个 ID 溢出引发的线上资损

你给某支付平台做「交易流水导出」功能。

需求很直接:把数据库里 bigint(20) 的订单 ID 渲染到表格里。

你顺手写了这么一行:

js 复制代码
// ❌ 线上事故代码
const row = `<tr><td>${order.id}</td><td>${order.amount}</td></tr>`;

结果上线第二天,财务发现:

"有笔 1.8e+17 的订单,点进去详情金额对不上!"

排查发现:

  • 数据库 ID 是 18012345678901234567
  • 但 JS 里 Number(order.id) 变成了 18012345678901234000 ------ 尾部 567 直接丢了
  • 因为它超过了 Number.MAX_SAFE_INTEGER(9007199254740991),JS 的 64 位浮点数精度崩了。

这可不是显示问题,而是 ID 错位导致查串了订单,差点引发资损。


解决方案:三层防御,把大数关进"安全笼"

1. 表面用法:用 BigInt 代替 Number

js 复制代码
// ✅ 正确处理大数
const bigId = BigInt("18012345678901234567"); // 🔍 字符串转 BigInt
console.log(bigId.toString()); // "18012345678901234567"

// 用于计算
const nextId = bigId + 1n; // 🔍 必须加后缀 n

关键点:

  • 必须用字符串初始化BigInt(18012345678901234567) 会先被转成 Number 再转 BigInt,已经丢精度了;
  • 运算时操作数必须都是 BigInt,不能和 Number 混算;
  • 比较可以用 ===,但 == 会自动转换,有坑。

2. 底层机制:为什么 JS 数字会"失精"?

类型 存储方式 范围 精度
Number IEEE 754 双精度浮点 ±1.79e+308 53 位有效数字
BigInt 任意长度整数 无上限 完全精确

原理图(文字版):

flowchart LR A["Number: [1位符号][11位指数][52位尾数]"] --> B["实际精度 2^53 - 1 = 9007199254740991"] B --> C["超过这个值,尾数不够用,低位被舍入"]

所以 9007199254740992 === 9007199254740993 在 JS 里居然是 true

3. 设计哲学:从"传输"到"渲染"全链路防溢出

(1)接口层:后端传字符串,前端不碰大数

json 复制代码
{
  "order_id": "18012345678901234567",  // 🔍 ID 用字符串
  "amount": 123456789,                 // 数值小,可用 Number
  "user_id": "18012345678901234568"
}

(2)状态层:用 BigInt 做计算,但不存进 Redux

js 复制代码
// calc.js
export function addId(idStr, offset) {
  const id = BigInt(idStr);
  return (id + BigInt(offset)).toString(); // 🔍 计算完转回字符串
}

(3)渲染层:永远用字符串插值

js 复制代码
// ✅ 安全渲染
const row = `<tr data-id="${order.id}">  // 🔍 直接用字符串,不转 Number
  <td>${order.id}</td>
</tr>`;

应用扩展:可复用的配置片段

1. Axios 自动转换大数字段

js 复制代码
// axios.interceptor.js
axios.defaults.transformResponse = [
  (data, headers) => {
    if (headers['content-type']?.includes('json')) {
      return JSON.parse(data, (key, value) => {
        // 🔍 指定字段转 BigInt
        if (['order_id', 'user_id'].includes(key) && /^\d{16,}$/.test(value)) {
          return value; // 🔍 保持字符串,由业务层决定是否转 BigInt
        }
        return value;
      });
    }
    return data;
  }
];

2. 环境适配说明

场景 注意点
IE 浏览器 BigInt 不支持,需降级用 string + bignumber.js
TypeScript 类型定义用 bigintstring,别用 number
JSON 序列化 BigInt 不能直接 JSON.stringify(),需自定义 toJSON

举一反三:3 个变体场景

  1. 金融计算(高精度小数)
    BigInt 模拟定点数:123.45 存为 12345n(单位:分),运算后再除 100
  2. 数据库主键生成(Snowflake ID)
    前端生成 ID 时用 BigInt 拼接时间戳、机器码、序列号,避免重复;
  3. 区块链地址校验
    以太坊地址是 256 位整数,用 BigInt 做范围校验和签名计算。

小结

别让 Number 碰超过 16 位的数字。

传用字符串,算用 BigInt,渲染不转 Number,三招封死精度陷阱。

相关推荐
伍哥的传说32 分钟前
CSS+JavaScript 禁用浏览器复制功能的几种方法
前端·javascript·css·vue.js·vue·css3·禁用浏览器复制
lichenyang45338 分钟前
Axios封装以及添加拦截器
前端·javascript·react.js·typescript
Trust yourself2431 小时前
想把一个easyui的表格<th>改成下拉怎么做
前端·深度学习·easyui
三口吃掉你1 小时前
Web服务器(Tomcat、项目部署)
服务器·前端·tomcat
Trust yourself2431 小时前
在easyui中如何设置自带的弹窗,有输入框
前端·javascript·easyui
烛阴1 小时前
Tile Pattern
前端·webgl
前端工作日常2 小时前
前端基建的幸存者偏差
前端·vue.js·前端框架
Electrolux2 小时前
你敢信,不会点算法没准你赛尔号都玩不明白
前端·后端·算法
a cool fish(无名)2 小时前
rust-参考与借用
java·前端·rust
Feather_743 小时前
从Taro的Dialog.open出发,学习远程控制组件之【事件驱动】
javascript·学习·taro