JavaScript 对大整数(超过 2^53 - 1)的精度丢失问题

遇到的问题:

后端返回的用户 ID 大概率是 Long 类型(64 位整数),而 JavaScript 的 Number 类型仅能精确表示 53 位整数,当 ID 超过 2^53 - 1(即 9007199254740991)时,超出部分会被截断或舍入,导致前端接收的 ID 与后端不一致(精度丢失)。

下面提供 4 种解决方案,按「推荐优先级」排序,覆盖不同场景:

一、方案 1:后端改造(最优,从根源解决,无需前端大量修改)

核心思路:后端将 Long 类型的 ID 转为 String 类型返回给前端,前端接收字符串格式的 ID,不会存在精度丢失问题(字符串可完整保留所有数字)。

具体实现(后端):
  1. 方式 1:修改实体类,序列化时将 Long 转为 String(推荐,全局 / 局部适配) 若使用 Jackson 序列化(Spring Boot 默认),可通过注解 @JsonFormat(shape = JsonFormat.Shape.STRING)@JsonProperty 实现局部字段序列化,或配置全局序列化规则。

    复制代码
    import com.fasterxml.jackson.databind.annotation.JsonSerialize;
    import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
    import lombok.Data;
    
    @Data
    public class User {
        // 局部配置:将 Long 类型 ID 序列化为 String 类型返回
        @JsonSerialize(using = ToStringSerializer.class)
        private Long id; // 后端存储仍为 Long,仅返回给前端时转为 String
        
        private String username;
        private String phone;
        private Integer status;
    }
  2. 方式 2:修改返回结果,手动将 Long 转为 String 若不想修改实体类,可在接口返回时,手动将 id 转为 String(如使用 Map 或 DTO 封装返回结果)。

    复制代码
    // 示例:手动封装返回结果,将 id 转为 String
    @GetMapping("/user/admin/{id}")
    public CommonResponseVO<Map<String, Object>> getUserDetail(@PathVariable Long id) {
        User user = userService.getUserDetail(id);
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("id", user.getId().toString()); // Long 转 String
        resultMap.put("username", user.getUsername());
        resultMap.put("phone", user.getPhone());
        resultMap.put("status", user.getStatus());
        resultMap.put("createTime", user.getCreateTime());
        return CommonResponseVO.success(resultMap);
    }
前端处理:

无需额外修改逻辑,直接接收字符串格式的 id 即可(后续传递接口(如禁用 / 启用)时,字符串格式的 id 会被正常解析,后端可自动转换为 Long 类型)。

二、方案 2:前端使用 BigInt 类型处理大整数(纯前端改造,无需后端配合)

核心思路:将前端接收的数字类型 ID 转为 BigInt 类型BigInt 是 JavaScript 专门用于表示大整数的类型,可精确表示任意长度的整数,无精度丢失问题。

具体实现(前端):
  1. 接收数据时,将 id 转为 BigInt 无论是表格数据还是详情数据,在赋值时将 id 转为 BigInt(注意:BigInt 不能与 Number 类型直接运算,需统一类型)。

    复制代码
    // 1. 分页查询用户时,处理 id 为 BigInt
    const handleQuery = async () => {
      try {
        loading.value = true;
        const params = { ...queryForm };
        if (!params.status) delete params.status;
        if (!params.keyword) delete params.keyword;
        
        const res = await pageQueryUsers(params);
        // 遍历用户列表,将 id 转为 BigInt
        userList.value = (res.data.records || []).map(user => ({
          ...user,
          id: BigInt(user.id) // 数字转 BigInt
        }));
        total.value = res.data.total || 0;
      } catch (error) {
        ElMessage.error(`用户查询失败:${error.message}`);
      } finally {
        loading.value = false;
      }
    };
    
    // 2. 获取用户详情时,处理 id 为 BigInt
    const handleDetail = async (userId) => {
      try {
        detailLoading.value = true;
        detailDialogVisible.value = true;
        const res = await getUserDetail(userId);
        // 将详情中的 id 转为 BigInt
        userDetail.value = {
          ...res.data,
          id: BigInt(res.data.id)
        };
      } catch (error) {
        ElMessage.error(`获取用户详情失败:${error.message}`);
      } finally {
        detailLoading.value = false;
      }
    };
  2. 传递接口参数时,将 BigInt 转为字符串(避免接口请求报错) BigInt 类型直接传递到接口中会被序列化为 "{ BigInt: "xxx" }",导致后端无法解析,因此传递时需转为字符串。

    复制代码
    // 禁用/启用用户时,将 BigInt 类型的 userId 转为字符串
    const handleStatus = async (userId, currentStatus) => {
      const actionText = currentStatus === 1 ? '禁用' : '启用';
      const targetStatus = currentStatus === 1 ? 0 : 1;
      
      try {
        await ElMessageBox.confirm(
          `确定要${actionText}该用户吗?`,
          '操作确认',
          {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: currentStatus === 1 ? 'warning' : 'info'
          }
        );
        
        // BigInt 转字符串传递给接口
        const res = await updateUserStatus(userId.toString(), targetStatus);
        if (res.result) {
          ElMessage.success(res.message || `${actionText}用户成功`);
          handleQuery();
        }
      } catch (error) {
        if (error !== 'cancel') {
          ElMessage.error(`${actionText}用户失败:${error.message}`);
        } else {
          ElMessage.info(`已取消${actionText}操作`);
        }
      }
    };
  3. 模板中展示 BigInt 类型(直接展示即可,Vue 会自动转为字符串)

    复制代码
    <!-- 表格中展示 BigInt 类型的 id,无需额外处理 -->
    <el-table-column prop="id" label="用户ID" width="180"></el-table-column>
    
    <!-- 详情弹窗中展示 BigInt 类型的 id -->
    <el-descriptions-item label="用户ID">{{ userDetail.id || '-' }}</el-descriptions-item>
注意事项:
  • BigInt 不支持 JSON.stringify() 直接序列化,若需存储或传递 BigInt 数据,需手动转为字符串;
  • BigInt 不能与 Number 类型直接比较或运算,如需运算,需先将 Number 转为 BigInt(如 BigInt(10) + userId)。

三、方案 3:前端配置 Axios 响应拦截器,自动处理大整数 ID(全局适配,一劳永逸)

核心思路:在 Axios 响应拦截器中,遍历返回数据,将所有符合「大整数」特征的字段(如 iduserId)自动转为 StringBigInt,无需在每个接口中单独处理。

具体实现(前端,基于 axios):
复制代码
// src/utils/request.js
import axios from 'axios';
import { useUserStore } from '@/store/modules/user';

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 5000
});

// 请求拦截器(保留原有逻辑)
request.interceptors.request.use(
  (config) => {
    const userStore = useUserStore();
    if (userStore.accessToken) {
      config.headers['Authorization'] = `Bearer ${userStore.accessToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// 新增:工具函数 - 遍历对象/数组,处理大整数 ID
const handleBigIntId = (data) => {
  if (typeof data !== 'object' || data === null) {
    return data;
  }
  
  // 数组处理
  if (Array.isArray(data)) {
    return data.map(item => handleBigIntId(item));
  }
  
  // 对象处理
  const result = { ...data };
  for (const key in result) {
    if (result.hasOwnProperty(key)) {
      const value = result[key];
      // 1. 匹配 id 相关字段(id、userId、merchantId 等)
      // 2. 匹配数字类型,且超过 2^53 - 1 的大整数
      if (
        (key.toLowerCase() === 'id' || key.endsWith('Id')) &&
        typeof value === 'number' &&
        value > Number.MAX_SAFE_INTEGER
      ) {
        // 方案A:转为 String(推荐,兼容性更好)
        result[key] = value.toString();
        // 方案B:转为 BigInt(需后续处理序列化问题)
        // result[key] = BigInt(value);
      } else if (typeof value === 'object' && value !== null) {
        // 递归处理嵌套对象
        result[key] = handleBigIntId(value);
      }
    }
  }
  return result;
};

// 响应拦截器(修改,添加大整数 ID 处理)
request.interceptors.response.use(
  (response) => {
    const res = response.data;
    // 核心:处理返回数据中的大整数 ID
    const handledData = handleBigIntId(res);
    if (handledData.result === true) {
      return handledData;
    } else {
      return Promise.reject(new Error(handledData.message || '接口请求失败'));
    }
  },
  (error) => Promise.reject(error)
);

export default request;
优势:
  • 全局生效,所有接口返回的 id 相关字段都会被自动处理,无需在页面中单独修改;
  • 兼容性好,转为 String 后,后续操作(传递接口、展示、存储)均无问题;
  • 可扩展,可根据业务需求添加更多需要处理的字段(如 orderIdgoodsId)。

四、方案 4:前后端约定 ID 长度限制(预防方案,适合新项目)

核心思路:后端限制 Long 类型 ID 的长度,确保其不超过 2^53 - 1 ,这样 JavaScript 的 Number 类型可精确表示,无需额外处理。

具体说明:
  • 2^53 - 1 对应的十进制数是 9007199254740991,约 9 万亿,对于大部分中小型项目,这个数值足够使用;
  • 后端可通过调整雪花算法(若使用)的机器位、时间位长度,限制 ID 不超过该数值;
  • 该方案仅适合新项目或数据量较小的项目,对于已有大 ID 的项目,无法回溯修改,仅能作为预防。

五、总结(推荐方案选择)

  1. 优先选择方案 1(后端改造):从根源解决问题,前端无需额外修改,兼容性最好,后续维护成本最低;
  2. 无法修改后端时,选择方案 3(Axios 全局拦截):一劳永逸,全局适配,无需在每个页面中单独处理大整数 ID;
  3. 仅个别接口需要处理时,选择方案 2(BigInt 类型):灵活可控,适合局部场景;
  4. 新项目推荐方案 4(ID 长度限制):提前预防,避免后续出现精度丢失问题。

按以上方案处理后,前端接收的用户 ID 会完整保留精度,不会出现「后端正确、前端丢失」的问题,后续接口传递也能保持一致性。

相关推荐
进击的丸子2 小时前
基于虹软Linux Pro SDK的多路RTSP流并发接入、解码与帧级处理实践
java·后端·github
小北方城市网2 小时前
微服务架构设计实战指南:从拆分到落地,构建高可用分布式系统
java·运维·数据库·分布式·python·微服务
开开心心_Every2 小时前
离线黑白照片上色工具:操作简单效果逼真
java·服务器·前端·学习·edge·c#·powerpoint
予枫的编程笔记2 小时前
【Java进阶】深入浅出 Java 锁机制:从“单身公寓”到“交通管制”的并发艺术
java·人工智能·
while(1){yan}2 小时前
SpringAOP
java·开发语言·spring boot·spring·aop
专注于大数据技术栈2 小时前
java学习--Collection
java·开发语言·学习
heartbeat..2 小时前
Spring 全局上下文实现指南:单机→异步→分布式
java·分布式·spring·context
浙江巨川-吉鹏2 小时前
【城市地表水位连续监测自动化系统】沃思智能
java·后端·struts·城市地表水位连续监测自动化系统·地表水位监测系统
zero.cyx2 小时前
javaweb(AI)-----后端
java·开发语言