Appearance
City Ranking 城市里程榜(前端调用视角)
路由:
POST /api/v1/ranking(action 协议:{"action":"...", "payload":{...}}) action:cityRanking— 城市圈定用户的打卡里程排行 鉴权:需登录(Authorization: Bearer <access_token>) 错误码段:40422xxx 后端契约权威:api-docs/v1/ranking/index.md §3(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 缓存 —— 见必读 ④) |
⚠️ 必读六条
myRank = null是两种含义 —— ① 未绑定该城(想上榜先走城市选择页selectCity);② 排名在聚合上限(500 名)之外。前端建议:myRank === null且用户未绑定 → 展示「选择城市,参与排名」引导入口。- 空榜是正常冷启动 ——
list: []+total: 0表示该城暂无绑定用户打卡,不是错误。画空态(例如「还没有人上榜,来打第一个卡」),不要走错误分支。 nickname/avatarUrl可为null—— 用户未填资料时后端不编造。展示兜底:昵称null→ 「匿名」,头像null→ 默认头像。- 5 分钟缓存 —— 后端对
(城市, 周期)组合缓存 5 分钟,generatedAt是聚合时刻。5 分钟内重复进入页面数据不变是预期行为,不要做「下拉刷新无变化 = bug」的误判;需要强实时可等窗口过期后再拉。 - 接口不回城市名 —— 榜头「长沙 · 近 7 天」这类城市名由前端映射:用城市选择页
listCities结果或本地字典做adcode → name(后端只回cityAdcode)。 - 排序已稳定 ——
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/ranking §3
- 城市选择接口(拿 adcode / 绑定城市):api-docs/v1/city
- 错误码段总览:api-docs/v1/ranking/error-codes