踩坑实录

真踩过的坑,带现象、定位过程和结论

2026-10-17 · nginx

不存在的文件,为什么返回 200?

排查一个「图片 404 了但页面不报错」的问题。用 curl 一测,/images/ 返回的不是 404,而是 200,而且响应体是首页的 HTML。

原因在 SPA 的兜底配置:

location / {
    try_files $uri $uri/ /index.html;
}

请求一个不存在的路径时,前两个都不命中,最后落到 /index.html,状态码自然是 200。对单页应用来说这是正确的 —— 前端路由需要它才能刷新不 404。但它同时意味着:任何不存在的文件都返回 200。

这带来两个实际后果。

第一,判断「文件到底在不在」不能只看状态码。我在核对一个验证文件是否部署成功时,curl -o /dev/null -w '%{http_code}' 一直是 200,看起来一切正常,其实文件根本不存在。正确做法是比对内容:算一个 md5,和本地那份比。

第二,$uri/ 命中真实目录时,请求会交给 index 模块处理。如果那个目录里没有 index.html,结果是 403,而不是回退到 /index.html。这条更隐蔽 —— 一次性往 dist 下加了 7 个语言目录,每个里面都缺 index.html,上线后 /en/ /ja/ 等首页全部从 200 变成 403。

所以现在有一条硬规则:在 dist 下新建任何目录,必须同时放 index.html。构建脚本里也加了自检,遍历所有目录,缺一个就构建失败。

下次遇到「明明删了文件却还能访问」,先怀疑兜底规则,别怀疑缓存。

把响应头粘进去看真实的 Content-Type 与状态 →

2026-10-20 · encoding

二维码里的中文为什么扫出来是乱码

把二维码生成从「调第三方接口」换成本地库之后,英文内容一切正常,中文扫出来是乱码。

第一反应是编码问题,于是加了一句 encodeURIComponent —— 没用。又怀疑扫描器不支持,换两个 App 扫,一样乱。

问题出在库的默认行为。这类二维码库内部要把字符串转成字节数组,默认实现是「每个字符取低 8 位」的 latin1 方案。对 ASCII 完全正确,对一个汉字就只留了它 Unicode 码点的最低一字节 ——「中文测试」四个字变成了 4 个字节,而 UTF-8 下应该是 12 个字节。字节数都不对,扫出来当然是错的。

定位方法很简单:直接打印字节数组长度,和 UTF-8 期望值比。

const bytes = qrcode.stringToBytes('中文测试')
console.log(bytes.length)   // 4,应该是 12

修复只有一行,但必须知道去找它:

qrcode.stringToBytes = qrcode.stringToBytesFuncs['UTF-8']

这件事提醒我:换库之后「英文能跑」不等于「功能正确」。凡是涉及文本编码的库,必须用非 ASCII 样本验证一次,而且要验证的是内容对不对,不是有没有输出。

用中文内容生成一次,扫一下验证 →

2026-10-21 · auth

签名验证失败,其实是 token 过期了

接口开始报「token 无效」。第一反应是密钥配错了,于是把密钥从头核对一遍,前后端一致。又怀疑算法不对,改成 HS256 试、RS256 试,都不行。

折腾了半小时才想起来把 token 解开看一眼。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwibmFtZSI6ImRldiIsImV4cCI6MTcwMDAwMDAwMH0.xxx

中间那段 base64 解出来,exp 字段是 1700000000 —— 对应 2023 年。token 早就过期了。

为什么会被「签名失败」误导?因为很多库在校验时先验签名、再验 exp,而过期这个错误在某些封装里会被统一成「认证失败」返回。看到的结果和签名错误一模一样,于是就往密钥方向查。

现在的习惯:任何 token 相关的报错,第一件事是解码看 exp。这只要 5 秒,比核对密钥快得多。

顺带记住 exp 是秒级时间戳,不是毫秒。前端如果按毫秒处理,会得到 1970 年或几万年后的日期,那又是另一个方向的问题。

粘贴 token 直接看 exp 与剩余时间 →

2026-10-22 · debug

两个环境的返回「看起来一样」,其实差在一个字段

测试环境正常、线上不对。把两边的接口返回都复制出来对比,肉眼扫了三遍,没看出区别。字段名一样、层级一样、值看起来也一样。

问题在于肉眼比对天然不可靠:两个响应各 80 多行,缩进一致,唯一不同的是某个数组里第二条记录的 status 从 1 变成了 2 —— 而那个字段在下单流程里决定走不走风控。

后来用结构化对比工具跑了一遍,输出一行:

$.data.list[1].status: 1 → 2

三秒钟定位。

顺带发现另一个细节:直接对比两段 JSON 文本时,如果数组中间插入了一条记录,纯文本 diff 会把后面所有行都标成「变化」,看起来像 40 处差异,实际只有 1 处新增。结构化对比能按 key 匹配,插入一条只报一条。

现在的做法是:只要对比的是 JSON,就不要用文本 diff。文本 diff 适合配置文件和代码,JSON 有结构信息,不用白不用。

两侧粘贴 JSON,按路径列出差异 →

2026-10-22 · payment

微信支付签名总是失败,因为 body 多了一个空格

接微信支付 v3,签名一直返回「签名错误」。私钥格式确认过、商户号确认过、证书序列号确认过,时间戳也确认是秒级。

签名串是五行固定格式:

请求方法\n
请求路径\n
时间戳\n
随机串\n
请求体\n

注意最后那个换行也在里面,而且请求体必须是实际发出去的那串字节,不是「等价的 JSON」。

我的代码是先用对象序列化生成签名,再用 HTTP 客户端序列化一次发出去。两次序列化之间,客户端把 {"a":1} 加了个空格变成 {"a": 1}。语义完全一样,字节不同,签名就不匹配。

定位方式是打印签名串,逐字符看。肉眼一看就发现了那个空格。

修复不是改签名逻辑,而是先确定 body 字符串,签它,然后原样发送这个字符串,不要序列化两次。

另外一个容易忽略的点:路径要包含 query string,且必须是发出去的原始形式,不能重新排序参数。

粘私钥和 body,它会显示被签名的完整原文 →

2026-10-23 · validation

身份证批量导入失败,卡在最后一位

客户导了一份员工名单,系统报「身份证格式错误」,但那些号码在表格里看着完全正常 —— 18 位、数字结尾、没有空格。

写了个脚本抽了 20 条单独跑正则校验,全部通过。说明不是格式问题,是校验逻辑。

18 位身份证的最后一位是校验位,算法是前 17 位加权求和再对 11 取模:

权重 = [7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]
校验码表 = "10X98765432"
校验位 = 校验码表[加权和 % 11]

那批数据里有一部分最后一位写成了 X 的小写形式,还有一部分是 Excel 把纯数字结尾的号码当成浮点数存了,末尾悄悄变了。

所以校验身份证要做三件事,缺一不可:查格式(18 位、末位是数字或 X)、查校验位、查出生日期是否真实存在(2 月 30 日这种)。只做正则等于没校验 —— 随手编一个 18 位数大概率能过正则。

顺带提醒:脱敏时保留前 6 位(地区)和后 4 位就够了,中间 8 位(出生日期)才是敏感的那部分。

身份证 / 信用代码 / 卡号一次校验到位 →