Appearance
运动上报联调排障记录 —— submitRun 400(2026-10-10)
面向:前端开发 · 用途:定位「结束运动提交失败」的根因与修复指引 结论一句话:载荷契约全对;断点在本地运动数据「冻结」——所有指标(点/距离/时长)停在第一次开跑后 6 秒的快照,28 分钟不再更新,被后端 ≥2 点门槛正确拒收。 相关契约:api/run-share · 方案设计 v4
一、现象
真机多次「开始运动 → 跑 2~4 分钟 → 结束运动」,submitRun 一律返回:
HTTP 400 · code=40418001 · msg="points: List should have at least 2 items after validation, not 1"二、服务端取证(三路证据)
2.1 诊断日志(服务端载荷摘要,三次提交完全一致)
json
{"stage": "payload",
"reason": "points: List should have at least 2 items after validation, not 1",
"digest": {
"sessionId": "run-3af1dec50c20",
"duration": 5,
"distance": 0,
"pointsN": 1, ← ★ 全程只有 1 个定位点
"tsFirst": 1791614477617,
"tsLast": 1791614477617 ← ★ 首尾同一个点
}}2.2 时间线(API 记录仪)
| 时刻 | 动作 | 结果 |
|---|---|---|
| 14:41:11 | startRun(带 location) | ✅ 200,后端分配 run-3af1dec50c20 |
| 14:41:17 | (该次唯一采集到的定位点时刻) | — |
| 14:41:24 ~ 15:09:23 | submitRun ×9 次提交(跨 28 分钟,含 4 分钟真跑) | ❌ 400 ×9,每次载荷完全相同 |
2.3 会话登记(服务端 Redis)
json
{"sessionId": "run-3af1dec50c20", "startedAtMs": 1791614471778,
"location": {"province":"湖南省","city":"长沙市","area":"岳麓区", "lng":"112.92…","lat":"28.17…"}}判读:run-3af1dec50c20 正是后端 startRun 分配的会话 ID;startedAtMs 与 14:41:11 精确吻合 → 前端的「开跑登记」链路完全正确。惟一的采集点时刻(14:41:17)= 开跑后 6 秒——之后 28 分钟内的所有跑步(含 4 分钟那次)一个点都没增加。
三、载荷正确性核对表
| 项 | 判定 | 说明 |
|---|---|---|
startRun 带 activityType + location | ✅ | 逆地理地址完整上来(province 拼写规范,后端 provice/province 双兼容) |
使用后端分配的 sessionId | ✅ | run-3af1dec50c20 = 后端发放值(非本地自造) |
submitRun 字段名与结构 | ✅ | sessionId/points[{lat,lng,ts}]/duration/distance/startedAt/activityType 全部命中契约 |
points 数组被连续定位填充 | ❌ 断点 | 28 分钟恒定 1 个点 |
distance 计数器 | ❌ | 恒 0(同源问题) |
duration 计数器 | ❌ | 恒 5(同源问题;4 分钟跑也纹丝不动) |
四、前端 console 报错判读(截图证据 · 2026-10-10 15:09)
前端 console 显示:
15:09:02.996 [run] startRun… run-3af1dec50c20 ← 后端会话 ID 收到,✅
15:09:08.916 [run] 运动数据待补传, {"code":"-1","msg":"系统异常"}
15:09:23.023 [run] 运动数据待补传, {"code":"-1","msg":"系统异常"}判读(三点):
{"code":"-1","msg":"系统异常"}不是后端返回的 —— 这是前端请求层自己的兜底文案 (src/utils/request.ts里reject({ code: '-1', msg: '系统异常' })的if (!body)分支; reporter 侧若有自带分支同理)。它把后端真实错误吞掉了。- 后端真实返回(服务端三重证据):HTTP 400 + 完整信封体
{"code":"40418001","msg":"points: List should have at least 2 items after validation, not 1","data":null}—— 中间件从响应体提取到bizCode=40418001;nginx 4xx 体透传已用 curl 实测排除(content-type: application/json、content-length 完整)。 - 「待补传」队列机制工作正常(失败自动排队重试)——问题在于重试时载荷还是同一份冻结快照。
⭐ 冻结快照(本次新增判读)
同一份载荷(1 点 @14:41:17 + distance=0 + duration=5)在 14:41 → 15:09 的 9 次提交中逐字不变, 其中包含一次 4 分钟真跑与多次 6 秒测试。说明本地运动数据存储在首次开跑约 6 秒后即冻结:
| 指标 | 14:41 首跑 | 之后所有跑步(含 4 分钟) |
|---|---|---|
| points 数组 | 1 个点(14:41:17) | 仍是那 1 个点 |
| distance | 0 | 仍是 0 |
| duration | 5 | 仍是 5 |
→ 三个指标同源冻结 = 采集→存储→提交这一整条管道在首跑后断更(而不是只有 GPS 一项)。
前端请求层修复建议(否则永远看不到真实报错)
非 200 响应时把 statusCode + 原始响应体透出(console.log(res.statusCode, JSON.stringify(res.data))), toast 用 body?.msg——现在这个 -1/系统异常 兜底会把所有联调排障变成盲盒。
五、前端排查清单(按优先级)
- 先修请求层错误透出(见上节)——不然修完也看不到反馈。
- 采集管道为何冻结:首跑 6 秒后再无任何指标更新——检查定位回调/计时器写入的存储实例, 与提交时读取的是否为同一份(典型的「写 A 读 B」或存储实例被重建后写入悬空)。:点「开始运动」后,高德原生插件(
qm-amap-native)的持续定位/watch 回调是否真正开启?(对照:开跑 6 秒时能拿到 1 个点 → 一次性定位是通的,连续性没有) - 回调写入的数组 ≠ 提交读取的数组:检查定位回调里 append 的数组,与「结束运动」组装
points时读取的数组,是不是同一个引用(常见坑:回调写入组件局部 ref,提交读的是页面级 state / Pinia store 的另一份)。 - 页面重渲染重置:锁屏/切后台再回来,轨迹数组是否被页面重建清空(6 秒后归档的 1 个点可能来自重新初始化)。
- duration / distance 计数器同源排查:两个计数器和轨迹数组大概率挂在同一条未启动的数据流上——修好 1 即可带出 2、3、4。
- 后台定位权限(若 1 确认已调用但仍无点):Android 需前台服务/后台定位权限;iOS 需
UIBackgroundModes: location(锁屏采点必需)。
六、修复验收标准
修好后一次真机测试应满足:
- 诊断日志中
pointsN≥ 2(预期 = 跑步秒数 ± 误差),且tsFirst ≠ tsLast、tsAscending: true distance> 0(跑了 ≥50 米)submitRun返回code=0,响应含summary.distanceKm / cleanedDistanceKm / routePointCount- 「运动数据」页立即可见本次记录(来源徽标「App 记录」)
- ⚠️ 顺带验证 duration 单位 = 毫秒(如 2 分 30 秒应传
150000而非150;失败记录里duration:5疑似秒数——修好后我们会核对记录时长是否正常)
七、服务端侧状态(无需前端处理)
- 门槛(≥2 点、≥50 米等五验)为正确业务规则,不放宽
- 会话登记、单活跃幂等、失败不消耗登记 —— 均已实证工作正常
- 诊断日志(载荷摘要 + 拒绝原因)为联调期临时设施,问题闭环后摘除
附:本问题时间戳证据(供前端对照日志)
startRun 14:41:11 (IP 110.53.*) → run-3af1dec50c20
点采集 14:41:17(唯一一个)
submit 14:41:24 / 14:43:27 / 14:43:41 / 14:45:09 / 14:45:31 / 14:50:41
/ 15:08:47 / 15:09:08 / 15:09:23 → 全部 400 · 同一载荷八、最终定位与修复(2026-10-10 16:16 —— 本问题闭环)
真机 113 点真实轨迹(318 米 / 3'21")被拒的根因(服务端诊断日志实捕):
轨迹校验失败: 瞬时速度 22.0 m/s 超物理上限(running ≤ 10.0)这是后端的顺序缺陷,不是前端问题:真实 GPS 轨迹普遍含偶发尖刺(城市峡谷/多径), 而我们的「野点剔除器」正是为滤尖刺设计——但原实现先五验后清洗,尖刺在清洗前就被 当作超速硬拒。修复:先清洗后五验(滤完仍超速 = 真伪造,照拒——防伪造不弱化)。
前端无需改动;此条对「测试点数据」同样适用——个例异常点会被清洗器滤除, 只有全程超物理限(伪造/开车)才会被拒收。