---
url: /docs/api-docs/v1/city/error-codes.md
---
# city —— 错误码矩阵（40431xxx）

> 完整 action 字段表见 [city 概览](/api-docs/v1/city/)。
> 错误码权威注册：`docs/ai-contexts/error-code-registry.md`。
> HTTP 状态映射：`src/app/city/domain/exceptions.py::cz_city_error_to_http_status`。

## 5 个错误码全表

| code | 名称 | HTTP | 触发场景 | 前端处置 |
|---|---|---|---|---|
| `40431001` | `PARAM_INVALID` | **400** | 双模入参并存 / 都缺 / adcode 格式非 6 位 / level 越界（`{1,2,3}` 之外）/ unknown action | toast「参数错误」 |
| `40431002` | `CITY_NOT_FOUND` | **404** | adcode 不在 seed（数据漂移） | toast「城市不存在」+ 排查 seed |
| **`40431003`** | **`USER_CITY_NOT_BOUND`** | **404** | **用户尚未绑定城市（新用户 / 清 cookie / 重装）** | \*\*跳引导页 / 选城市页，**不当错误** |
| `40431004` | `UNAUTHORIZED` | **401** | 未登录 / token 无效 | 走 OAuth 登录页 |
| `40431005` | `GPS_RESOLVE_FAILED` | **400** | GPS 转码失败（**v1 必触发** —— `SeedNearestCityLocator` 恒返 None） | **前端 v1 不做「自动定位」按钮** |

## 关键设计点

### 40431003 ≠ 错误

新用户进入 App 第一时间 `getCurrentCity` **必然**拿到 `40431003`，这是**业务常态**，不是异常。前端必须按 code 判断 → 跳引导页，**不弹错误 toast**（不弹任何错误提示，体验上等同于「首次进入引导」）。

实现层：use case 内 raise `CityError(USER_CITY_NOT_BOUND, ...)` → handler 转 HTTP 404 → 前端按 status === 404 且 code === 40431003 走引导分支。

### 40431005 = v1 已知缺口

* **原因**：seed `adcode_seed.json` 不含 geo 坐标 → `SeedNearestCityLocator.cz_locate()` 永远返 `None`
* **影响**：v1 `selectCity {gps: ...}` 路径**永远**返 40431005
* **前端约束**：选城市页**不要**做「自动定位」按钮（用户点了 = 必失败体验差）
* **v2.1 修复**：接逆地理服务（高德 district API + 生产 AMAP\_KEY）→ 替换 `SeedNearestCityLocator` 实现即可，**端口契约不变**

### HTTP 状态码设计

| 类别 | HTTP | 错误码 |
|---|---|---|
| 入参校验失败 | 400 | `40431001` |
| GPS 转换失败 | 400 | `40431005` |
| 未登录 | 401 | `40431004` |
| 城市不存在 | 404 | `40431002` |
| 用户未绑（常态） | 404 | `40431003` |

`40431003` 给 404 而非 400 —— 体现「这个资源状态不存在（用户→城市关联）」，与 `40431002`（城市字典不存在）形成语义分层。

## 错误码段治理

* **段位**：`40431xxx`
* **本段已用**：5/1000 个（`001~005`）
* **预留**：`006~999`（同段内顺序分配，禁止跳号）
* **守卫**：`scripts/check_error_code_segments.py`（CI 拦截未登记段）
* **注册表**：`docs/ai-contexts/error-code-registry.md`

## v1 实测（来自 test\_city\_actions.py）

```text
40431001 → 双模入参并存 (selectCity {adcode,gps}) / adcode 非 6 位 / level=4
40431002 → adcode="999999"（不在 seed）
40431003 → 新用户 getCurrentCity（unbound binding）
40431004 → 无 Bearer token
40431005 → gps {lng,lat}（v1 必触发）
```

后端 5 例全绿，回归 `tests/integration/test_city_actions.py`（20 例）。
