楼主把"凶手"归到emoji头上,这个归因其实可以再掰扯一下。严格说,炸掉接口的并不是那只笑脸,而是后端那处"拿字节长度判断"的逻辑本身就有问题——只要输入里混进任意一个多字节字符,中文也好、emoji也好,字节截断都可能在字符中间切一刀,产出非法字节序列。嗯emoji只是恰好把总长度顶过了截断阈值,让切口落到了多字节序列当中。换句话说,它是导火索,不是炸药。
再说"emoji大多是代理对"这个说法,稍微不够严谨。严格来说UTF-16里的代理对只覆盖基本多文种平面之外的码点,比如U+1F600那一批绘文字;可早期一批符号类emoji像U+263A、U+2665、U+27A1都还在BMP里,根本不走代理对。更麻烦的是ZWJ序列和组合附加符:国旗实际是两个区域指示符码点(U+1F1E8、U+1F1F3)拼出来的,家庭组合类靠零宽连接符串起好几个码点,肤色修饰符也是叠在基础码点上的附加符。
顺着这点我想补一层:改用码点计数固然比按码元数或字节数靠谱,但它依然不是"用户眼里的字符数"。同一个é,可以是U+00E9一个码点,也可以是e加组合重音两个码点——显示出来一模一样,码点计数却差了一倍。所以"按码点截断"能保你不切坏字节、不拆散代理对,却仍可能把国旗或一组组合字素从中间腰斩。真要做到显示层截断不出怪相,得用字素簇去切,JS里可以上Intl.Segmenter,服务端一般靠ICU这类库。
往深里看,这回事故的根子其实是各层对"一个字符算多长"各说各话。浏览器里JS的length按UTF-16码元数,Java的length()也是码元数,Python3的len()按码点,线上跑的字节又是UTF-8。同一个昵称,前端数出来8,后端数出来9,落库又变成20字节——emoji不过是让这几套账对不上的时候炸得最狠。所以除了把截断逻辑改成码点,更紧要的是在接口边界把"长度按什么单位算"定死,下游别再用另一套单位重新量一遍。
你们后端那块后来是只在入库前改了,还是连同返回给前端的字段也一起统一了?这个我有点好奇。