返回博客

我填的经纬度,保存后变成了湖州

用户反馈:在某后台管理系统填入门店经纬度 30.550353,120.728027,保存后再打开,变成了 30.56369,120.007133。差距很大,不像精度截断,也不像格式转换。第一反应是前端 v-model 绑错了,或者读取路径有 mutator——但翻来翻去都没问题。

现象

  • 浏览器 DevTools → Network → 保存请求 payload:前端发出的就是正确的 30.550353,120.728027
  • 页面重新打开后 detail 回显 30.56369,120.007133
  • 数据库直接查,lat/lng 字段里存的就是 30.56369,120.007133
  • 把门店地址填成「测试 123」重新新建一条,lat/lng 还是 30.56369,120.007133

第三条是关键:DB 里就是错的,跟读取路径无关,问题在写入阶段。

根因

后端服务在 create 方法里做了一个「地址反查坐标」的数据增强逻辑:

// 反模式:反查 + 失败硬编码兜底
Map<String, String> json = getGeocoderLatitude(store.getAddress());
if (json != null) {
    store.setLat(json.get("lat"));   // 覆盖前端值
    store.setLng(json.get("lng"));
} else {
    store.setLat("30.56369");        // 地图 API 失败 → 硬编码某地坐标
    store.setLng("120.007133");
}
save(store);

三个错叠加:

  1. 从不检查前端是否已传值——反查无条件执行,前端入参被静默丢弃
  2. 反查作为不可绕过的副作用——与写入完全耦合
  3. 失败兜底用硬编码业务坐标——地图 API 超时、地址含「测试」、quota 用完,全都走 else,结果全表新建记录都共享同一个坐标
坑:fallback 分支写真实业务默认值是定时炸弹

任何外部反查(地理编码、手机号查会员、地址查门店 ID)一旦失败,fallback 里绝不能写一个真实的业务坐标 / 真实手机号 / 真实订单号作为默认值。

这个坏数据看起来"像是正常数据",不会触发任何告警,会在 DB 里安静地躺着,直到有人认真核对才发现全表污染。

排查步骤

Step 1 — 先查 DB,确认是写入问题还是读取问题

SELECT id, lat, lng, create_at, update_at, update_by
  FROM store WHERE id = '刚保存的记录 ID' LIMIT 1;

DB 里就是 30.56369 → 问题在写入阶段,读取路径不用看。

Step 2 — 看元数据,区分 insert 还是 update 分支

update_by IS NULLupdate_at = create_at → 该记录从未走过 update 分支,元凶在 insert 路径

Step 3 — 用 DB 里的实际值去全栈 grep

grep -rn "30.56369\|120.007133" /path/to/backend

命中 StoreServiceImpl.java:782,找到硬编码常量,根因证据链闭合。

正解

层 1(必做):尊重前端入参,仅在前端未传时才触发反查

// 前端已传坐标 → 跳过反查,直接用
if (StringUtils.isBlank(store.getLat()) || StringUtils.isBlank(store.getLng())) {
    Map<String, String> json = getGeocoderLatitude(store.getAddress());
    if (json != null) {
        store.setLat(json.get("lat"));
        store.setLng(json.get("lng"));
    }
    // 反查失败 → 保留 null,不写任何业务默认值
}
save(store);

层 2(兜底):失败时保留 null 或 throw,绝不写硬编码业务值

层 3(双后端特殊场景):若架构约定不改某一侧服务,可在另一侧写入完成后串行覆盖:前端拿到新 ID 后立即调「干净的」服务用原始 lat/lng 再写一次,注意是串行而非 Promise.all 并发,否则顺序无法保证。

正解:前端传了值,服务端就不应该反查覆盖

反查类数据增强应该是 opt-in:「前端没传 → 服务端补」,而不是「服务端无条件覆盖,外部 API 挂了再给个默认值」。update_by / update_at 元数据是免费的法医证据,业务表建表时务必加上。

一句话外卖

服务端在写入路径做反查增强时,先判断前端入参是否已有值;fallback 分支只允许保留 null 或抛异常,永远不要写一个"看起来正常"的硬编码业务默认值。