当前位置: 首页 > web >正文

一次诡异的报错排查:为什么时间戳变成了 ١٧٥٦٦٣٢٧٨

在做服务端登录校验时,我们线上遇到了一个很奇怪的报错:

strconv.Atoi: parsing "١٧٥٦٦٣٢٧٨": invalid syntax

字符串看起来和数字个数对得上,但又像是乱码,Go 服务无法解析这段字符。这背后,其实是一个 国际化 (i18n) 的大坑


现象复盘

  • 服务端使用 Go,解析客户端传来的时间戳:

    v, err := strconv.Atoi(tsString)
    
  • 部分用户(主要在阿联酋)登录时,报错 invalid syntax

  • 打印出来的时间戳长这样:١٧٥٦٦٣٢٧٨٫١٧٢

看上去就是 175663278.172,为什么不行?


真相揭晓:Locale 搞的鬼

排查后发现:

  • 用户手机设置为 阿拉伯语 (Arabic) + 地区 = 阿联酋 (United Arab Emirates)

  • Android/Java 默认的 DecimalFormat 会跟随 Locale 决定数字符号;

  • ar_AE 下,数字会被格式化成 阿拉伯-印地数字

    • ١٧٥٦٦٣٢٧٨ = 175663278
    • ٫ = 小数点

所以客户端传过来的根本不是 ASCII 数字,而是另一套 Unicode 数字。Go 的 Atoi 当然解析失败。


为什么我们测试没复现?

我们在国内测试时,把系统语言切成阿拉伯语,却始终输出 123456...

原因是:

  • 只改 语言 不够,还要改 地区
  • 必须同时是 语言 = Arabic,地区 = 阿联酋 (ar_AE),并且启用「本地数字」选项,才会显示 ١٢٣٤٥٦...
  • MIUI 等国产 ROM 把“地区”设置藏得比较深(设置 → 更多设置 → 地区),所以一开始没找到。
  • 更坑的是,不同手机厂商 / Android 版本的 ICU/CLDR 数据不同,有的 ar_AE 默认就用阿拉伯数字,有的还是拉丁数字,所以有时根本复现不了。

解决方案

客户端改造(推荐)

  1. 强制使用 US Locale

    DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US);
    df.applyPattern("#.######");
    df.setGroupingUsed(false);
    String ts = df.format(timeMillis / 1000.0);
    
  2. 避免字符串传输

    • JSON 用 number 类型:

      { "ts": 175663278 }
      
    • 而不是字符串:

      { "ts": "١٧٥٦٦٣٢٧٨" }
      
  3. 如果只需要整数时间戳,直接:

    String ts = Long.toString(timeMillis / 1000);
    

服务端兜底

即使客户端改了,服务端也要健壮,能容错。加个数字规范化函数,把阿拉伯/波斯数字转成 ASCII:

func normalizeDigits(s string) string {out := make([]rune, 0, len(s))for _, r := range s {switch {case r >= '\u0660' && r <= '\u0669': // Arabic-Indic ٠..٩out = append(out, '0'+(r-'\u0660'))case r >= '\u06F0' && r <= '\u06F9': // Persian ۰..۹out = append(out, '0'+(r-'\u06F0'))default:out = append(out, r)}}return string(out)
}

这样再 strconv.Atoi(normalizeDigits(ts)) 就不会出错了。


总结经验

  1. 国际化的坑很多:不要依赖默认 Locale,显式指定才安全。
  2. 测试要全面:仅切语言不够,还要切地区;不同系统实现也可能有差异。
  3. 服务端要健壮:客户端可能各种情况,服务端要兜底。
  4. 最佳实践:跨端传递时间戳、ID 等数据,推荐直接用 数值,而不是字符串。
http://www.xdnf.cn/news/19664.html

相关文章:

  • 云端虚拟手机:云手机的原理是什么?
  • SRE 系列(五)| MTTK/MTTF/MTTV:故障应急机制的三板斧
  • 低空经济的中国式进化:无人机与实时视频链路的未来五年
  • 后端笔试题-多线程JUC相关
  • 用滑动窗口与线性回归将音频信号转换为“Token”序列:一种简单的音频特征编码方法
  • 全栈智算系列直播回顾 | 智算中心对网络的需求与应对策略(下)
  • Linux开发必备:yum/vim/gcc/make全攻略
  • 大模型微调显存内存节约方法
  • 【ComfyUI】图像描述词润色总结
  • 基于若依框架前端学习VUE和TS的核心内容
  • 函数、数组与 grep + 正则表达式的 Linux Shell 编程进阶指南
  • windows10专业版系统安装本地化mysql服务端
  • AI公共数据分析完整实战教程:从原始数据到商业洞察【网络研讨会完整回放】
  • golang -- viper
  • Go语言运维实用入门:高效构建运维工具
  • 洽洽的“成本龙卷风”与渠道断层
  • MVC问题记录
  • Python备份实战专栏第5/6篇:Docker + Nginx 生产环境一键部署方案
  • 【机器学习入门】4.4 聚类的应用——从西瓜分类到防控,看无监督学习如何落地
  • Mac上如何安装mysql
  • 阿里云代理商:轻量应用服务器介绍及搭建个人博客教程参考
  • 【赵渝强老师】阿里云大数据MaxCompute的体系架构
  • Git基础使用和PR贡献
  • 02-Media-1-acodec.py 使用G.711编码和解码音频的示例程序
  • 电子电气架构 --- 智能电动车EEA电子电气架构(上)
  • 时序数据库IoTDB:为何成为工业数据管理新宠?
  • (Mysql)MVCC、Redo Log 与 Undo Log
  • 《探索C++11:现代C++语法的性能革新(上篇)》
  • C++11 ——— lambda表达式
  • 前端必看:为什么同一段 CSS 在不同浏览器显示不一样?附解决方案和实战代码