---
url: /docs/guide/uniapp/api/city-ranking.md
---
# City Ranking 城市里程榜（前端调用视角）

> 路由：`POST /api/v1/ranking`（action 协议：`{"action":"...", "payload":{...}}`）
> action：**`cityRanking`** — 城市圈定用户的打卡里程排行
> 鉴权：需登录（`Authorization: Bearer <access_token>`）
> 错误码段：**40422xxx**
> 后端契约权威：[api-docs/v1/ranking/index.md §3](/api-docs/v1/ranking/#3-cityranking)（2026-09-30 上线 v2.0.120 + v2.0.122 修 SQL）

## 场景（榜单页）

用户在「城市榜页」查看**所在城市的跑步里程排行**：

* **默认城市长沙** —— 不传 `cityAdcode` 即看长沙榜（无需先做城市选择）
* 可切城市：从城市选择页拿到的 `adcode` 传入 `cityAdcode`
* **两档周期：近 30 天（`month30`）/ 全部（`all`）**（2026-09-30 修正 —— 原当日/每周档已下线）

## 调用链（与 city 域联动）

```
进入城市榜页
  ├─（可选）getCurrentCity → 拿到用户已绑定城市 adcode
  │     ├─ 已绑定 → cityRanking {period, cityAdcode: 绑定城市}
  │     └─ 未绑定 → cityRanking {period}          # 默认长沙榜
  └─ 用户切周期 / 切城市 / 下拉刷新 → 重调
```

绑定的用户才会**上榜**（进榜单需要先 `selectCity`）—— 未绑定用户能看榜，但自己的 `myRank` 恒为 `null`。

## 请求

```json
{ "action": "cityRanking", "payload": { ... } }
```

| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
| `period` | string | ⬜ | `"month30"` | `"month30"`（近 30 天）/ `"all"`（全部历史，不限下界） |
| `cityAdcode` | string | ⬜ | `"430100"` | 6 位行政区划码；**缺省 = 长沙** |
| `limit` | int | ⬜ | `50` | 1~100（榜单返回条数） |

## 响应字段

| 字段 | 类型 | 说明 |
|---|---|---|
| `period` | string | 入参回显 |
| `cityAdcode` | string | 实际生效的城市码（缺省时后端回 `"430100"`，前端可据此确认） |
| `list` | array | 榜单（最长 `limit` 条） |
| `list[].rank` | int | 名次（1 起） |
| `list[].userId` | string | 用户 ID |
| `list[].nickname` | string | null | 昵称 —— **可能为 null**（见必读 ③） |
| `list[].avatarUrl` | string | null | 头像 URL —— 可能为 null |
| `list[].distance` | float | 周期内累计里程 **km**（保留 2 位小数） |
| `list[].checkins` | int | 周期内打卡次数 |
| `myRank` | int | null | 当前用户在该城榜单的排名；**未绑定该城 → null**（见必读 ①） |
| `total` | int | 上榜总人数 |
| `generatedAt` | string | 聚合时刻 ISO 8601（服务端 5min 缓存 —— 见必读 ④） |

## ⚠️ 必读六条

1. **`myRank = null` 是两种含义** —— ① 未绑定该城（想上榜先走城市选择页 `selectCity`）；② 排名在聚合上限（500 名）之外。前端建议：`myRank === null` 且用户未绑定 → 展示「选择城市，参与排名」引导入口。
2. **空榜是正常冷启动** —— `list: []` + `total: 0` 表示该城暂无绑定用户打卡，**不是错误**。画空态（例如「还没有人上榜，来打第一个卡」），不要走错误分支。
3. **`nickname` / `avatarUrl` 可为 `null`** —— 用户未填资料时后端不编造。展示兜底：昵称 `null` → 「匿名」，头像 `null` → 默认头像。
4. **5 分钟缓存** —— 后端对 `(城市, 周期)` 组合缓存 5 分钟，`generatedAt` 是聚合时刻。5 分钟内重复进入页面数据不变是**预期行为**，不要做「下拉刷新无变化 = bug」的误判；需要强实时可等窗口过期后再拉。
5. **接口不回城市名** —— 榜头「长沙 · 近 7 天」这类城市名由前端映射：用城市选择页 `listCities` 结果或本地字典做 `adcode → name`（后端只回 `cityAdcode`）。
6. **排序已稳定** —— `distance` 降序、同分按 `userId` 升序。前端**不要二次排序**，否则与 `myRank` 序号可能不一致。

## 错误码

| code | HTTP | 场景 | 前端处理 |
|---|---|---|---|
| `40422001` | 400 | `period` 越界 / `cityAdcode` 非 6 位数字 | 检查入参（周期只允许 month30/all） |
| `40422002` | 401 | 未登录 / token 无效 | 跳登录 |
| `40422004` | 404 | 服务端未装配该 action（罕见） | 提示稍后重试 |

> 业务失败恒为 **HTTP 200 + code 非 "0"**（V2 信封约定），前端按 `code` 分支而非 HTTP 状态。

## 调用示例

### 请求：默认长沙 · 近 30 天榜（全缺省）

```json
{ "action": "cityRanking", "payload": {} }
```

### 请求：长沙 · 全部历史

```json
{ "action": "cityRanking", "payload": { "period": "all" } }
```

### 请求：切城市 · 近 30 天

```json
{ "action": "cityRanking", "payload": { "period": "month30", "cityAdcode": "440100", "limit": 20 } }
```

### 成功响应

```json
{
  "code": "0",
  "msg": "ok",
  "data": {
    "period": "month30",
    "cityAdcode": "430100",
    "list": [
      {"rank": 1, "userId": "u000074", "nickname": "荣耀用户", "avatarUrl": null,
       "distance": 42.3, "checkins": 7},
      {"rank": 2, "userId": "u000100", "nickname": "跑者甲", "avatarUrl": "https://cdn.example/a.png",
       "distance": 36.2, "checkins": 7}
    ],
    "myRank": 1,
    "total": 2,
    "generatedAt": "2026-09-30T12:34:56+00:00"
  }
}
```

### 未绑定用户的响应（可看榜 · 不参与排名）

```json
{
  "code": "0", "msg": "ok",
  "data": {
    "period": "month30", "cityAdcode": "430100",
    "list": [ {"rank": 1, "userId": "u000100", "nickname": "跑者甲", "avatarUrl": null,
               "distance": 6.2, "checkins": 1} ],
    "myRank": null,
    "total": 1,
    "generatedAt": "2026-09-30T12:34:56+00:00"
  }
}
```

### 错误响应

```json
{ "code": "40422001", "msg": "period 越界: year（month30|all）", "data": null }
```

## 页面 × API 矩阵

| 页面 / 交互 | 调用 | 时机 |
|---|---|---|
| 城市榜页 · 进入 | `cityRanking` | onShow / onLoad |
| 城市榜页 · 切周期（30 天 / 全部） | `cityRanking` | Tab 切换 |
| 城市榜页 · 切城市 | `cityRanking`（新 cityAdcode） | 城市选择页返回后 |
| 城市榜页 · 下拉刷新 | `cityRanking` | 手动刷新（注意 5min 缓存） |
| 城市选择页 | city 域 `listCities` / `selectCity` | 见 [api-docs/v1/city](/api-docs/v1/city/) |

## 相关

* 后端完整契约（含设计决策表）：[api-docs/v1/ranking §3](/api-docs/v1/ranking/#3-cityranking)
* 城市选择接口（拿 adcode / 绑定城市）：[api-docs/v1/city](/api-docs/v1/city/)
* 错误码段总览：[api-docs/v1/ranking/error-codes](/api-docs/v1/ranking/error-codes)
