---
url: /docs/guide/uniapp/api/run-submit-debug.md
---
# 运动上报联调排障记录 —— submitRun 400（2026-10-10）

> 面向：前端开发 · 用途：定位「结束运动提交失败」的根因与修复指引
> 结论一句话：**载荷契约全对；断点在本地运动数据「冻结」——所有指标（点/距离/时长）停在第一次开跑后 6 秒的快照，28 分钟不再更新，被后端 ≥2 点门槛正确拒收。**
> 相关契约：[api/run-share](./run-share) · [方案设计 v4](/docs/sport-loop-design.html)

## 一、现象

真机多次「开始运动 → 跑 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":"系统异常"}
```

**判读（三点）**：

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 个点** |
| distance | 0 | **仍是 0** |
| duration | 5 | **仍是 5** |

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

### 前端请求层修复建议（否则永远看不到真实报错）

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

## 五、前端排查清单（按优先级）

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

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