Appearance
city —— 选择城市(C 端 / 4 action)
统一入口:
POST /api/v1/citybody ={ "action": "<4种之一>", "payload": { ... } }鉴权:3 action 需登录(selectCity / getCurrentCity / getRecentCities),1 action 公开(listCities 选城市页用) 契约权威:src/app/city/interface/api/city_actions.py(DDD interface 层,路由必须在此 — 详踩坑见docs/internal-wiki/decisions/0013-city-domain.md)
4 个 Action 一览
| Action | 鉴权 | 触发场景 | 详细 |
|---|---|---|---|
listCities | 无 | 选城市页进入 / 搜索 | → §1 |
selectCity | Bearer | 用户点选 / 提交定位 | → §2 |
getCurrentCity | Bearer | 首页 / 我的页查当前城市 | → §3 |
getRecentCities | Bearer | 选城市页 onShow 拉最近 + 热门 | → §4 |
公共约定
统一信封(V2 全局)
- 成功:
{ "code": "0", "msg": "ok", "data": { ... } } - 业务失败:HTTP 200 +
{ "code": "40431xxx", "msg": "<原因>" } - 入参校验失败:HTTP 400 +
{ "msg": "<字段路径>: <原因>" } - 响应 data 全部 camelCase(V2 风格:
adcode/parentCode/isHot/updatedAt/boundAt)
city 对象结构
json
{
"adcode": "440100",
"name": "广州市",
"parentCode": "440000",
"level": 2,
"pinyin": "guangzhou",
"isHot": true
}| 字段 | 类型 | 说明 |
|---|---|---|
adcode | string | 6 位行政区划码(110000=北京 / 440100=广州 / 500000=重庆等) |
name | string | 中文名(含「市」) |
parentCode | string | null | 父级 adcode(直辖市 = 自身 adcode) |
level | int | 1 省 / 2 市(含直辖市场次归 2 — 见 D-6) |
pinyin | string | null | 拼音首字母(去「市」) |
isHot | bool | 10 热门城市标记(启动屏首推) |
HTTP 状态映射
| 层 | HTTP | 说明 |
|---|---|---|
| 参数校验失败 | 400 | payload 格式错(双模入参并存 / level 越界) |
| 业务失败(默认) | 400 | 40431001 / 40431005 |
| 未登录 | 401 | 40431004 |
| 城市不存在 | 404 | 40431002 |
| 用户未绑城市(新用户常态) | 404 | 40431003(前端按 code 跳引导页,不打错误 toast) |
错误码矩阵(40431xxx)
| code | 语义 | HTTP | 前端处置 |
|---|---|---|---|
40431001 | 参数错(含双模入参并存 / unknown action / adcode 非 6 位 / level 越界) | 400 | toast |
40431002 | adcode 不存在 | 404 | toast |
40431003 | 用户未绑城市(新用户常态) | 404 | 跳引导页 / 选城市页,不当错误 |
40431004 | 未登录 / token 无效 | 401 | 走登录 |
40431005 | GPS 转码失败(v1 GPS 不可用 — seed 无 geo → SeedNearestCityLocator 恒返 None) | 400 | 前端 v1 不做「自动定位」按钮,只做手动选择 |
双模入参契约(D-7 · selectCity)
selectCity 支持两种入参,二选一,双传 → 40431001:
| 模式 | payload | 适用 |
|---|---|---|
| 手动 | { "adcode": "440100" } | 用户点选 / 搜索结果 |
| GPS | { "gps": { "lng": 113.2644, "lat": 23.1291 } } | 自动定位(v1 不可用 — 必返 40431005) |
⚠️ GPS 路径隐私红线:后端一次性转码,坐标不持久化(不会落 PG,不会写日志)。
seed 数据规模
- 368 条:27 省 + 341 地级市
- 不含区县(D-6 v1 决策 —— 用户选城市粒度是「我在哪个市」)
- 数据源:
src/app/city/seeds/adcode_seed.json(scripts/gen_city_seed.py生成;modood/administrative-divisions-of-China 派生) - 10 热门城市(
isHot=true):北京/上海/广州/深圳/佛山/东莞/南京/无锡/杭州/成都/武汉/重庆
1. listCities
公开 —— 选城市页拉列表 / 搜索过滤。
请求 payload:
| 字段 | 类型 | 必填 | 默认 | 约束 | 说明 |
|---|---|---|---|---|---|
level | int | ⬜ | — | 1 | 2 | 3 | 1=省 / 2=市 / 3=区县(v1 seed 无 3) |
keyword | string | ⬜ | — | ≤32 字符 | 搜 name 或 pinyin("广州" / "guangzhou" 都命中) |
hot | bool | ⬜ | false | — | true 返 10 热门城市(不受 level 影响) |
page | int | ⬜ | 1 | ≥1 | 页码 |
size | int | ⬜ | 50 | ≥1 | 每页条数(v1 全部 368 条内存,单页能拉满) |
响应 data:
| 字段 | 类型 | 说明 |
|---|---|---|
cities | [City] | 当前页结果(结构见公共约定) |
total | int | 过滤后总数(非分页大小) |
错误码:40431001(level 越界)
示例:
json
// 请求
{ "action": "listCities", "payload": { "keyword": "广州", "page": 1, "size": 20 } }
// 响应
{
"code": "0", "msg": "ok", "data": {
"cities": [{"adcode":"440100","name":"广州市","parentCode":"440000","level":2,"pinyin":"guangzhou","isHot":true}],
"total": 1
}
}2. selectCity
登录 —— 绑定当前城市(D-7 双模入参)。
请求 payload(二选一,双传 40431001):
| 模式 | 字段 | 类型 | 必填 | 约束 |
|---|---|---|---|---|
| 手动 | adcode | string | ✅ | ^\d{6}$(6 位数字) |
| GPS | gps.lng | float | ✅ | |
gps.lat | float | ✅ |
响应 data:
| 字段 | 类型 | 说明 |
|---|---|---|
adcode | string | 6 位 adcode |
name | string | 城市中文名 |
source | string | "manual" / "auto_gps"(GPS 路径 → auto_gps,手动 → manual) |
updatedAt | string | ISO 8601(2026-09-29T11:06:48) |
错误码:
| code | 场景 |
|---|---|
40431001 | 双模入参并存 / 都缺 / adcode 格式非 6 位 |
40431002 | adcode 不在 seed(unauthorized) |
40431004 | 未登录(401) |
40431005 | GPS 转码失败(v1 不可用 —— 实际必走这条) |
示例:
json
// 手动请求
{ "action": "selectCity", "payload": { "adcode": "440100" } }
// 手动响应
{
"code": "0", "msg": "ok", "data": {
"adcode": "440100", "name": "广州市", "source": "manual", "updatedAt": "2026-09-29T11:06:48"
}
}json
// GPS 请求(v1 必返 40431005)
{ "action": "selectCity", "payload": { "gps": { "lng": 113.2644, "lat": 23.1291 } } }
// GPS 响应(v1)
{ "code": "40431005", "msg": "GPS 坐标无法解析到城市" }3. getCurrentCity
登录 —— 首页 / 我的页读当前绑定城市。
请求:{ "action": "getCurrentCity", "payload": {} }
响应 data:
| 字段 | 类型 | 说明 |
|---|---|---|
city | City | 当前绑定城市(结构见公共约定) |
boundAt | string | ISO 8601(首次绑定或最后一次 selectCity 时间) |
source | string | "manual" / "auto_gps" |
错误码:
| code | 场景 | 前端处置 |
|---|---|---|
40431003 | 用户尚未绑定城市(新用户 / 清 cookie 重装) | 跳引导页 / 选城市页,不当错误展示 |
40431002 | 绑定 adcode 在 seed 找不到(数据不一致 — 走 admin 排查) | toast |
40431004 | 未登录 | 走登录 |
示例:
json
// 已绑定用户响应
{
"code": "0", "msg": "ok", "data": {
"city": {"adcode":"440100","name":"广州市","parentCode":"440000","level":2,"pinyin":"guangzhou","isHot":true},
"boundAt": "2026-09-29T11:06:48",
"source": "manual"
}
}
// 新用户响应(**预期**返回)
{ "code": "40431003", "msg": "尚未绑定城市" }4. getRecentCities
登录 —— 选城市页 onShow 拉最近选择 + 热门垫底。
v1 简化:每用户绑定表只有 1 条 → 「最近」= 当前绑定单条 + seed 10 热门按此去重数垫底。
请求 payload:
| 字段 | 类型 | 必填 | 默认 | 约束 | 说明 |
|---|---|---|---|---|---|
limit | int | ⬜ | 5 | 1~50 | 返回上限 |
响应 data:
| 字段 | 类型 | 说明 |
|---|---|---|
cities | [City] | 已有绑定前置 + 热门按 adcode 去重垫底 |
错误码:40431004(401 未登录)
示例:
json
// 请求
{ "action": "getRecentCities", "payload": { "limit": 5 } }
// 响应(已绑广州市 + 热门垫底)
{
"code": "0", "msg": "ok", "data": {
"cities": [
{"adcode":"440100","name":"广州市","parentCode":"440000","level":2,"pinyin":"guangzhou","isHot":true},
{"adcode":"110100","name":"北京市","parentCode":"110000","level":2,"pinyin":"beijing","isHot":true},
{"adcode":"310100","name":"上海市","parentCode":"310000","level":2,"pinyin":"shanghai","isHot":true},
{"adcode":"440300","name":"深圳市","parentCode":"440000","level":2,"pinyin":"shenzhen","isHot":true},
{"adcode":"440600","name":"佛山市","parentCode":"440000","level":2,"pinyin":"foshan","isHot":true}
]
}
}页面 × API 矩阵
| uniapp 页面 / 组件 | 调用的 action | 调用时机 |
|---|---|---|
| 首页 | getCurrentCity | 启动时查当前城市(40431003 跳引导页) |
| 选城市页 | getRecentCities | onShow 拉最近 + 热门垫底 |
| 选城市页 · 搜索框 | listCities | 输入 keyword 时实时过滤 |
| 选城市页 · 点选 | selectCity {adcode} | 点列表项 → 返回 + 刷新 |
| 设置页 | getCurrentCity | 显示当前绑定 |
实现参考
后端实施规范见 🟢 Internal Wiki → ADR-0013 city 域。 前端对接指南(推荐页面流图)见 🟢 Internal Wiki → 2026-09-29 选择城市 + 生吃排名 前端对接指引。