---
url: /docs/api-docs/v1/sport.md
---
# sport API（运动打卡 + 跑群 + 跑步会话）

> 路由：`POST /api/v1/sport`（action 协议：`{"action": "...", "payload": {...}}`）
> 错误码段：40418xxx（全表见[错误码权威注册表](/ai-contexts/error-code-registry)）
> 鉴权：全部需登录
> 字段真相：`src/app/sport/application/usecase/`（Request dataclass / pydantic payload）+ `tests/integration/test_sport_actions.py`（可执行契约）
> 前端对接页：[run-share（跑步链）](/guide/uniapp/api/run-share) · [run-submit-debug（排障）](/guide/uniapp/api/run-submit-debug) · [方案设计 v4](/docs/sport-loop-design.html)

## Action 一览（19）

| action | 鉴权 | 说明 |
|---|---|---|
| `today` | 登录 | 今日运动大卡 |
| `checkin` | 登录 | 打卡（goal 达成检测钩子触发点） |
| `myStats` | 登录 | 我的统计 |
| `myGroups` | 登录 | 我的跑群 |
| `createGroup` | 登录 | 建群（上限约束） |
| `joinGroup` | 登录 | 入群 |
| `quitGroup` | 登录 | 退群（群主限制） |
| `groupRanking` | 登录 | 群排名 |
| `groupDetail` | 登录 | 群详情 |
| `groupMembers` | 登录 | 成员列表 |
| `announceGroup` | 登录 | 群公告 |
| `startRun` | 登录 | 开跑（**v3 登记簿** — 传 `activityType`+`location`，后端分配 sessionId） |
| `submitRun` | 登录 | 结束上报（**v3** — 一次全量交轨迹，清洗+归一入库） |
| `myActiveRun` | 登录 | 查活跃会话（App 被杀恢复现场） |
| `finishRun` | 登录 | 收尾（旧契约 — 手工补汇总；重复幂等） |
| `myRunRecords` | 登录 | 运动记录列表（会话 + 打卡两源合并） |
| `runSessionDetail` | 登录 | 单次运动详情（轨迹 + 配速派生） |
| `addWeightRecord` | 登录 | 体重记录写入 |
| `runShareImage` | 登录 | 轨迹分享图生成（→ COS URL）[→ 前端对接页](/guide/uniapp/api/run-share) |

> ⚠️ `reportRunCoordinate`（逐点上报）**已下线**（2026-10-09 任务制改造）——调用返回 unknown action（40418001）。

## 跑步会话 v3 契约（2026-10-10 运动数据闭环）

### startRun —— 开跑登记（Redis 登记簿，PG 零写入）

```jsonc
// 请求
{ "action": "startRun",
  "payload": { "activityType": "running",            // 必填 running|walking|cycling
               "location": { "province": "湖南省", "city": "长沙市", "area": "岳麓区",
                             "address": "…", "lng": "112.93", "lat": "28.17" },  // 可选
               "deviceId": "…" } }                    // 可选（多设备提示）
// 响应 data
{ "sessionId": "run-4a95716dcd8f",   // 后端分配（12 hex）；Redis key = sport:run:active:{env}:{uid}
  "startedAt": 1791609225395, "resumed": false, "resumedFromDevice": "…" }  // 后两个可选
```

* **单活跃幂等**：同用户 8h 内重复 go → 同一 sessionId + `resumed:true`（不重开）
* **8h TTL**：过期自动蒸发；过期后提交仍收（冷补路径 + `no_registration` 内部标记）
* 旧包宽容兼容：旧参数（前端自造 sessionId 等）静默忽略
* `provice`/`province` 双拼写兼容（前端历史 typo）

### submitRun —— 结束上报（一次全量；同步清洗 + 单事务归一）

```jsonc
// 请求
{ "action": "submitRun",
  "payload": { "sessionId": "run-4a95716dcd8f",
               "points": [{"lat": 28.2, "lng": 112.9, "ts": 1791609225395}, …],  // ≤20000，≥2
               "duration": 150000,        // 毫秒（含暂停累计）
               "distance": 2000.0,        // 可选 米（前端 SDK 实时里程——给了以它为主口径）
               "activityType": "running", // 可选（登记里有；离线补交必带）
               "calories": 120, "avgHr": 118, "steps": 2100,
               "startedAt": 1791609225395 } }  // 可选（离线补交必带）
// 响应 data
{ "summary": { "distanceKm": 2.0, … }, "cleanedDistanceKm": 1.93,   // 双口径（上报主/清洗披露）
  "routePointCount": 42, "newPointsInserted": 42, "flagged": "…" }  // flagged 仅异常时
```

* **五验硬拒**（→ 400 · 40418001）：点 ≥2 · 距离 ≥50m · 点 ≤20000 · 跨度 ≤24h · 段速 ≤物理上限（**对清洗后点集判定** — 先清洗后五验，个例 GPS 尖刺被野点剔除器滤除）
* **乱序容错**：时间戳乱序由服务端自动排序（2026-10-10 联调拍板 — 打包问题非物理错误）；原始顺序留档 raw\_event
* **同步单事务四写**：run\_session 定稿 + 清洗轨迹点(DP 抽稀) + raw\_event 留档 + **sport\_record 归一**（vendor='app\_run' → 出现在 device 域运动记录列表）
* 幂等：重复提交零重复（点级 + 记录级 upsert）

### myActiveRun —— 查活跃会话

```jsonc
{ "action": "myActiveRun", "payload": {} }
// data: { "sessionId": "…", "startedAt": 1791609225395, "activityType": "running" }  或 null
```

## 通用约定

* 信封：HTTP 200 + `{"code","msg","data"}`；`code="0"` 成功；`40418002` 未登录（HTTP 401）；`40418001` 参数错（HTTP 400）
* 写操作均落 audit/测试可断言（集成测试 = 可执行契约）
* payload 校验：pydantic + `extra="ignore"`（多余字段**静默忽略不报错** —— 多传即丢，注意）

## 变更记录

| 日期 | 内容 |
|---|---|
| 2026-09-16 | 首版（四域审查棒 3 — action 清单代码直出）|
| 2026-10-08 | 跑步会话 6 action（startRun/reportRunCoordinate/finishRun/myRunRecords/runSessionDetail/addWeightRecord）入档 + `runShareImage` 新增（轨迹分享图 — [前端对接页](/guide/uniapp/api/run-share)）|
| 2026-10-10 | **运动数据闭环 v3（v2.0.157–159）** — startRun 改登记簿制（Redis 8h TTL + 后端分配 ID + 单活跃幂等 + location 随登记落库）；submitRun 重写（清洗管线 + 同步单事务四写 + sport\_record 归一 vendor='app\_run' + 双口径距离 + 上限 20000）；新增 `myActiveRun`；`reportRunCoordinate` 正式标注下线；配套 device 域 `sportRecordDelete`/sourceLabel/bySource。联调排障实录：[run-submit-debug](/guide/uniapp/api/run-submit-debug) |
