Skip to content

运动上报联调排障记录 —— 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:11startRun(带 location)✅ 200,后端分配 run-3af1dec50c20
14:41:17(该次唯一采集到的定位点时刻)—
14:41:24 ~ 15:09:23submitRun ×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":"系统异常"}

判读(三点):

  1. {"code":"-1","msg":"系统异常"} 不是后端返回的 —— 这是前端请求层自己的兜底文案 (src/utils/request.ts 里 reject({ code: '-1', msg: '系统异常' }) 的 if (!body) 分支; reporter 侧若有自带分支同理)。它把后端真实错误吞掉了。
  2. 后端真实返回(服务端三重证据):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 完整)。
  3. 「待补传」队列机制工作正常(失败自动排队重试)——问题在于重试时载荷还是同一份冻结快照。

⭐ 冻结快照(本次新增判读) ​

同一份载荷(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 个点
distance0仍是 0
duration5仍是 5

→ 三个指标同源冻结 = 采集→存储→提交这一整条管道在首跑后断更(而不是只有 GPS 一项)。

前端请求层修复建议(否则永远看不到真实报错) ​

非 200 响应时把 statusCode + 原始响应体透出(console.log(res.statusCode, JSON.stringify(res.data))), toast 用 body?.msg——现在这个 -1/系统异常 兜底会把所有联调排障变成盲盒。

五、前端排查清单(按优先级) ​

  1. 先修请求层错误透出(见上节)——不然修完也看不到反馈。
  2. 采集管道为何冻结:首跑 6 秒后再无任何指标更新——检查定位回调/计时器写入的存储实例, 与提交时读取的是否为同一份(典型的「写 A 读 B」或存储实例被重建后写入悬空)。:点「开始运动」后,高德原生插件(qm-amap-native)的持续定位/watch 回调是否真正开启?(对照:开跑 6 秒时能拿到 1 个点 → 一次性定位是通的,连续性没有)
  3. 回调写入的数组 ≠ 提交读取的数组:检查定位回调里 append 的数组,与「结束运动」组装 points 时读取的数组,是不是同一个引用(常见坑:回调写入组件局部 ref,提交读的是页面级 state / Pinia store 的另一份)。
  4. 页面重渲染重置:锁屏/切后台再回来,轨迹数组是否被页面重建清空(6 秒后归档的 1 个点可能来自重新初始化)。
  5. duration / distance 计数器同源排查:两个计数器和轨迹数组大概率挂在同一条未启动的数据流上——修好 1 即可带出 2、3、4。
  6. 后台定位权限(若 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 轨迹普遍含偶发尖刺(城市峡谷/多径), 而我们的「野点剔除器」正是为滤尖刺设计——但原实现先五验后清洗,尖刺在清洗前就被 当作超速硬拒。修复:先清洗后五验(滤完仍超速 = 真伪造,照拒——防伪造不弱化)。

前端无需改动;此条对「测试点数据」同样适用——个例异常点会被清洗器滤除, 只有全程超物理限(伪造/开车)才会被拒收。