← API | 列表 | 同伴App_错误码参考
提示信息
# 同伴 App — 错误码参考

> 版本:V2.1  
> 后端技术栈:PHP 8.4 · Slim 4 · PostgreSQL 18  
> 前端技术栈:Flutter 3.41.x · Riverpod 3.x · go_router 17.x  
> 适用范围:前后端开发、接口联调、客服排查、AI 辅助开发上下文

---

## 目录

1. [响应格式约定](#一响应格式约定)
2. [错误码总表](#二错误码总表)
   - [1xxx — 通用 / 系统层](#1xxx--通用--系统层)
   - [2xxx — 认证 / 支付模块](#2xxx--认证--支付模块)
   - [3xxx — 信号模块](#3xxx--信号模块)
   - [4xxx — 用户社交模块](#4xxx--用户社交模块)
   - [5xxx — 上传 / 文件模块](#5xxx--上传--文件模块)
   - [6xxx — 通知 / 推送模块](#6xxx--通知--推送模块)
3. [extraData 字段说明](#三extradata-字段说明)
4. [账号登录冲突与特殊通知](#四账号登录冲突与特殊通知)
5. [后端抛错规范](#五后端抛错规范)
6. [HTTP 状态码与 code 映射关系](#六http-状态码与-code-映射关系)
7. [第三方 SDK 错误码(OpenIM)](#七第三方-sdk-错误码openim)
8. [错误码分配规范(新增时参照)](#八错误码分配规范新增时参照)

---

## 一、响应格式约定

所有接口统一返回以下结构,HTTP 状态码**固定为 200**(支付回调除外):

```json
{
  "code":        0,
  "msg":         "success",
  "data":        {},
  "server_time": 1710000000
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | int | `0` = 成功,非 0 = 失败,见下表 |
| `msg` | string | 错误描述,**可直接展示给用户**(客户端禁止覆盖 msg 展示自定义文案,除非规范特别说明) |
| `data` | any | 成功时的业务数据;失败时可能携带辅助数据(extraData),见第三章 |
| `server_time` | int | 服务器 Unix 时间戳(秒),前端可用于时钟校准 |

**约定:**
- HTTP 状态码固定 200,**不依赖 HTTP 状态码判断业务成功与否**
- 客户端通过 `code` 字段判断请求结果
- `msg` 由后端统一定义,前端 Toast 直接展示,禁止各层自行覆盖(特殊情况在各条目中说明)
- 支付回调(微信/支付宝/苹果)HTTP 状态码遵循第三方要求,不在此约定范围内

---

## 二、错误码总表

### 1xxx — 通用 / 系统层

> 拦截器层统一处理,业务层一般不需要感知。

| 错误码 | 含义 | 触发场景 | 前端处理 | 后端触发位置 |
|--------|------|----------|----------|-------------|
| **0**  | **成功** | 请求正常处理完毕 | 消费 `data` | `BaseService::success()` |
| **1001-1009** | **认证与会话** | | | |
| `1001` | 未登录 / 凭证过期 | Authorization Header 缺失,或 Token 已过期/被撤销 | 弹窗并 `forceLogout()` 👉 [详见UI规范](/docs/document/html/07-errors.html#code-1001) | `AuthMiddleware` |
| `1002` | Access Token 已过期 | JWT exp 校验失败 | 自动刷新 Token | `AuthMiddleware` |
| **1010-1019** | **安全与封禁** | | | |
| `1010` | 您的账号因违规已被封禁 | 触发社区公约封禁 | 跳封禁页 👉 [详见UI规范](/docs/document/html/07-errors.html#code-1010) | `BanCheckMiddleware` |
| `1011` | 您的账号涉嫌诈骗行为已被封禁 | 特殊诈骗风险封禁 | 诈骗警示页 👉 [详见UI规范](/docs/document/html/07-errors.html#code-1011) | `BanCheckMiddleware` |
| `1012` | 当前设备已被封禁 | 设备 ID 处于黑名单 | 显示设备封禁弹窗 👉 [详见UI规范](/docs/document/html/07-errors.html#code-1012) | `SignMiddleware` |
| **1020-1029** | **请求与协议** | | | |
| `1021` | 参数校验失败 | 字段缺失或格式错误 | Toast `msg` | `BaseService::validate()` |
| `1022` | 签名校验失败 | `X-Sign` 不匹配 (API 安全鉴权) | 上报日志 | `SignMiddleware` |
| `1023` | 无权限操作 | API 访问权限不足 | Toast "无权操作" | Service 层 |
| `1024` | 内容不存在 | 查询记录不存在 (Not Found) | Toast "内容不存在" | Service 层 |
| **1030-1039** | **系统运营** | | | |
| `1031` | 请求频率过高 | 触发限流 / 反爬虫 | Toast `msg` | `RateLimitMiddleware` |
| `1032` | 版本发现更新 | `/app/start` 检测到版本更新要求 | 展示更新弹窗 👉 [详见UI规范](/docs/document/html/07-errors.html#code-1032) | `AppService::start()` |
| `1033` | 隐私政策更新 | 用户未同意最新版本的隐私政策 | 展示授权同意框 👉 [详见UI规范](/docs/document/html/07-errors.html#code-1033) | `UserActivityMiddleware` |
| `1038` | 停服维护 | 系统正在进行升级维护,暂不可用 | 全屏维护页面 👉 [详见UI规范](/docs/document/html/07-errors.html#code-1038) | 全局拦截 |
| `1039` | 服务器故障 | 未捕获异常 / 服务端宕机 | 故障全屏页/兜底 👉 [详见UI规范](/docs/document/html/07-errors.html#code-1039) | `Handler.php` |
| **1040-1049** | **外部服务** | | | |
| `1041` | 聊天服务暂时不可用 | OpenIM 服务报错 | Toast `msg` | `IM` 类 |
| `1042` | 存储服务异常 | OSS 签名或上传失败 | Toast `msg` | `OssHandler` |

---

### 2xxx — 认证 / 支付模块

> 业务层 catch 后直接 Toast `msg`,除非条目有特殊说明。

#### 2001–2099 认证子模块

| 错误码 | 含义 | 触发场景 | 前端处理 | 后端触发位置 |
|--------|------|----------|----------|-------------|
| `2001` | 验证码错误或已过期 | 短信验证码不匹配 / 超过 5 分钟 | Toast `msg`,聚焦验证码输入框 | `AuthService::loginPhone()` |
| `2002` | 验证码发送频繁 | 同一手机号 60s 内重复请求 | Toast `msg`,展示剩余冷却秒数(`data.retry_after` 秒)| `AuthService::sendSms()` |
| `2003` | 微信授权失败 | 微信登录 SDK 回调失败或 code 无效 | Toast `msg`,引导重试 | `AuthService::loginWechat()` |
| `2004` | Apple 授权失败 | Apple 登录认证失败 | Toast `msg`,引导重试 | `AuthService::loginApple()` |
| `2005` | 实名认证未通过 | 审核未通过 返回失败 | Toast `msg`,引导重新提交 | `AuthService::verifyFace()` |
| `2006` | 必须实名认证后操作 | 访问需要实名的接口但未认证 | 弹出实名浮层 👉 [详见UI规范](/docs/document/html/07-errors.html#code-2006) | Service 层校验 |

#### 2101–2199 支付子模块

| 错误码 | 含义 | 触发场景 | 前端处理 | 后端触发位置 |
|--------|------|----------|----------|-------------|
| `2101` | 订单不存在 | `order_no` 非法、不属于当前用户、已被物理删除 | 返回订单列表 | `PayService` 查询订单 |
| `2102` | 订单状态异常 | 已支付订单再次触发支付、已取消订单操作 | 刷新订单状态后重试 | `PayService` 状态检查 |
| `2103` | IAP 凭证验证失败 | Apple `receipt` 无效、已使用、与 Bundle ID 不匹配 | Toast `msg`,保存 receipt 并引导联系客服 | `PayService::verifyApple()` |
| `2104` | 商品不存在或已下架 | `product_id` 非法、商品 `deleted_at` 非空 | 刷新商品列表 | `PayService` / `VipService` |
| `2105` | 订单创建失败(微信/支付宝签名失败) | 调用微信/支付宝预支付接口失败,通常是服务端密钥配置问题 | Toast "创建支付失败,请稍后重试",上报日志 | `PayService::createOrder()` |
| `2106` | 重复购买(已拥有该 VIP 套餐) | 用户 `vip_expire_at` 未过期时再次购买同档 VIP | Toast `msg`,提示当前到期时间 `data.expire_at` | `VipService::purchase()` |

---

### 3xxx — 信号模块

> 信号是核心业务,前端需精细化处理每个错误码。

| 错误码 | 含义 | 触发场景 | 前端处理 | 后端触发位置 |
|--------|------|----------|----------|-------------|
| `3001` | 信号数量已达上限 | 用户已有 5 条存活信号(`config('app.signal_max') = 5`)| Toast "信号已满,请先删除一条再发布" | `SignalService::publish()` |
| `3002` | 该分类已有信号 | 同一 `category` 已存在未删除信号 | Toast "该分类已有信号,如需更换请先删除" | `SignalService::publish()` |
| `3003` | 信号内容违规 | 内容审核不通过(关键词过滤 / 人工审核)| Toast `msg`,聚焦内容输入框 | `SignalService::publish()` / 审核回调 |
| `3004` | 不能回复自己的信号 | `from_uid == to_uid` | 前端 UI 层屏蔽入口,此为后端兜底校验 | `SignalService::reply()` |
| `3005` | 已回复过或已处理 | `signal_receive.status >= 1`(已接受/已拒绝/已过期)| 刷新列表,更新该条目状态 | `SignalService::reply()` / `collect()` |
| `3006` | 暂时没有新的信号 | 信号池无可推送内容(已全部收取 / 无匹配用户 / 池为空)| Toast `msg`提示"暂时没有新信号" | `SignalService::pull()` |
| `3007` | 今日收取已达上限(非 VIP)| `signal_limit:{uid}:{date}` 计数达到免费次数 | Toast `msg`(当前版本暂去了除看广告解锁弹窗等逻辑) | `SignalService::pull()` |
| `3008` | 今日收取已达绝对上限 | 达到最终封顶次数 | 弹出次日重置提示框 👉 [详见UI规范](/docs/document/html/07-errors.html#code-3008) | `SignalService::pull()` |
| `3009` | 信号已过期或已被删除 | 操作一条 `deleted_at IS NOT NULL` 或已过期的信号 | Toast "该信号已失效" → 刷新列表 | `SignalService` 多处查询 |
| `3010` | 无权操作该信号 | 修改/删除不属于自己的信号 | Toast "无权操作" | `SignalService::update()` / `delete()` |
| `3011` | 回复内容违规 | 回复文本 / 附件未通过内容审核 | Toast `msg`,聚焦输入框 | `SignalService::reply()` |
| `3012` | 广告解锁失败(未完整观看)| 后端验证广告观看凭证无效,或用户未完整看完 | Toast "广告观看未完成,请重新尝试" | `SignalService::adUnlock()` |

---

### 4xxx — 用户社交模块

> 部分错误前端应在 UI 层屏蔽,后端校验为兜底保障。社交类错误展示场景均采用**无按钮空页面**(Empty Page State),由导航栏返回箭头处理退出,不提供额外操作入口。

| 错误码 | 含义 | 触发场景 | 前端处理 | 后端触发位置 |
|--------|------|----------|----------|-------------|
| `4001` | 不能关注自己 | `uid == target_uid` | 前端 UI 层屏蔽,后端兜底 | `UserService::followAdd()` |
| `4002` | 已关注 | 重复关注请求 | 乐观更新时需回滚;刷新关注状态 | `UserService::followAdd()` |
| `4003` | 不能拉黑自己 | `uid == target_uid` | 前端 UI 层屏蔽,后端兜底 | `UserService::blockAdd()` |
| `4004` | 已被对方拉黑 | 对被拉黑用户执行关注、发送信号等操作时触发(主页访问时返回 4005 隐藏原因)| Toast "操作失败",**禁止暴露被拉黑原因** 👉 [详见UI规范](/docs/document/html/07-errors.html#code-4004) | `UserService` 多处校验 |
| `4005` | 用户不存在 / 已注销 / 被拉黑隐藏 | `target_uid` 不存在、已注销,或**对方已拉黑当前用户(隐私保护,统一伪装为不存在)** | 显示空页面(无操作按钮),Toast 可显示 "用户不存在" 👉 [详见UI规范](/docs/document/html/07-errors.html#code-4005) | `UserService::profileDetail()` |
| `4006` | 已举报过该用户 | 同一 `uid` 对同一 `target_uid` 重复举报 | Toast "您已举报过该用户,我们将尽快处理" | `UserService::reportSubmit()` |
| `4007` | 不能访问已拉黑用户的主页 | 当前用户主动拉黑了目标用户,再次尝试访问其主页 | Toast "您已拉黑该用户" | `UserService::profileDetail()` |
| `4008` | 关注数已达上限 | 用户关注数超过平台上限(如 3000)| Toast `msg` | `UserService::followAdd()` |
| `4009` | 该账号涉嫌违规(已封禁) | 访问已被系统封禁用户的主页 | 显示封禁空页面(无操作按钮) 👉 [详见UI规范](/docs/document/html/07-errors.html#code-4009) | `UserService::profileDetail()` |

---

### 5xxx — 上传 / 文件模块

| 错误码 | 含义 | 触发场景 | 前端处理 | 后端触发位置 |
|--------|------|----------|----------|-------------|
| `5002` | 文件类型不允许 | 上传的 MIME 类型不在白名单(仅限 image/jpeg、image/png、audio/m4a 等)| Toast "不支持该文件类型" | `OssService` 验证 |
| `5003` | 文件大小超限 | 图片超过 10MB,语音超过 5MB | Toast "文件过大,请压缩后重试" | `OssService` 验证 |


---

### 6xxx — 通知 / 推送模块

| 错误码 | 含义 | 触发场景 | 前端处理 | 后端触发位置 |
|--------|------|----------|----------|-------------|
| `6001` | 推送 Token 注册失败 | `device_token` 格式非法,或阿里云推送 API 调用失败 | 静默处理,不影响主流程;后台重试 | `NotifyService::pushRegister()` |
| `6002` | 通知不存在 | `notify_id` 非法或不属于当前用户 | 刷新通知列表 | `NotifyService::read()` |

---

## 三、extraData 字段说明

部分错误码会在 `data` 字段携带辅助数据,前端必须使用这些数据驱动后续交互。

---

### `1010` (普通封禁) & `1011` (涉嫌诈骗)

这两类错误响应在 `data` 中都会返回相同的 `ban_info` 结构。

```json
{
  "code": 1011,
  "msg":  "您的账号涉嫌诈骗行为已被封禁",
  "data": {
    "ban_info": {
      "forbidden_type": "涉嫌诈骗", 
      "end_at":         "2025-12-31T23:59:59Z", 
      "is_permanent":   false 
    }
  },
  "server_time": 1710000000
}
```

**字段解释:**
- `forbidden_type`:具体被封禁的原因文本(来自后端的 `ForbiddenType::label()`,例如 `"涉嫌诈骗"`, `"涉嫌色情低俗"` 等)。
- `is_permanent`:布尔值,是否永久封禁。如果为 `true`,前端在展示解封时间处应直接显示“永久封禁”。
- `end_at`:具体解封时间。

**前端呈现:**  
前端页面判断该响应结构并通过 `ApiInterceptor` 重定向到 `ban` 页,不同类别的处理细则及样式演示如下:
- 👉 [`1010` 账号被封禁(普通违规)交互指南](/docs/document/html/07-errors.html#code-1010)
- 👉 [`1011` 账号被封禁(涉嫌诈骗)专属高危红灯样式](/docs/document/html/07-errors.html#code-1011)

---


## 四、账号登录冲突与特殊通知

### 4.1 账号登录冲突(被踢下线)

当账号在另一台设备登录时,后端会通过 OpenIM 发送一条 **ContentType 为 110** 的自定义消息到当前设备。

**客户端对接要求:**
- APP 开发团队需要全局监听 IM 消息。
- 当解析到 ContentType 为 `110` 且 `type: "account_conflict"` 时,在界面上弹出提示框。
- 👉 [异地登录切断通知交互视图参考](/docs/document/html/07-errors.html#code-openim)
- 引导用户确认后,清除本地 Token 并跳转回登录页面。

**消息结构示例:**
```json
{
  "type":         "account_conflict",
  "device_model": "iPhone 15 Pro",
  "text":         "您的账号刚刚在另一台设备(\"iPhone 15 Pro\")登录。如非本人操作,请立即修改密码。"
}
```

---

### 4.2 隐私政策动态更新拦截 (1033)

**版本控制:**
系统持续提供动态的隐私政策升级拦截能力:
- 在后端的 `.env` 中维护一个全局配置:`PRIVACY_POLICY_VERSION=1`。
- 当法律合规要求或隐私政策发生重大变更、需要强制重新授权时,运维只需将环境变量递增改为 `2`。
- `UserActivityMiddleware` 会在所有登录用户请求接口时比对。全系用户表里 `accepted_privacy_version` 低于 `2` 的用户,均会在下一次请求时被无情拦截并返回 `1033`。

**客户端对接要求:**
- 出现 `1033` 时,立即终止并保存当前待发送的 Request。
- 弹出强制确认弹窗 👉 [界面参考](/docs/document/html/07-errors.html#code-1033)。
- 用户点击“同意并继续”后,调用同意接口(`POST /api/v1/user/agree-privacy`)。
- 接口通过后,无感重试并放行刚才被拦截的 HTTP 请求即可。

---

## 五、后端抛错规范

### 5.1 统一使用 AppException

```php
// ✅ 正确:统一使用 AppException + ErrorCode 枚举
throw new AppException('今日收取已达上限', ErrorCode::SIGNAL_DAILY_LIMIT->value, ['ad_config' => ...]);
throw new AppException('信号数量已达上限', ErrorCode::SIGNAL_LIMIT_REACHED->value);
throw new AppException('验证码错误或已过期', ErrorCode::SMS_CODE_INVALID->value);

// ✅ 参数校验:通过 validate() 块,自动转为 code=1021
$this->validate(function () use ($params) {
    Assert::keyExists($params, 'category', 'category 不能为空');
    Assert::range((int)$params['category'], 1, 5, 'category 超出范围 1-5');
});

// ❌ 禁止:直接 return fail() 处理业务异常
// fail() 仅用于极少数需要提前 return 而职不是抛出的场景
```

### 5.2 各中间件抛错方式

```php
// AuthMiddleware — 无 Token / Token 无效
throw new AppException('未登录或 Token 无效', ErrorCode::UNAUTHORIZED->value);

// AuthMiddleware — Token 过期(JWT exp 校验失败)
throw new AppException('Token 已过期', ErrorCode::ACCESS_TOKEN_EXPIRED->value);

// BanCheckMiddleware — 普通封禁
throw new AppException('您的账号因违规已被封禁', ErrorCode::ACCOUNT_BANNED->value, [
    'ban_info' => [
        'forbidden_type' => $ban->forbidden_type,
        'reason'         => $ban->reason,
        'end_at'         => $ban->end_at?->toIso8601String(),
        'is_permanent'   => $ban->is_permanent,
    ],
]);

// BanCheckMiddleware — 涉嫌诈骗封禁(reason_type = 'fraud')
throw new AppException('您的账号涉嫌诈骗行为已被封禁', ErrorCode::ACCOUNT_BANNED_FRAUD->value, [
    'ban_info' => [
        'forbidden_type' => $ban->forbidden_type,
        'reason'         => '涉嫌诈骗',
        'end_at'         => null,
        'is_permanent'   => true,
    ],
]);

// SignMiddleware — 签名失败
throw new AppException('签名校验失败', ErrorCode::SIGN_FAILED->value);

// RateLimitMiddleware — 限流
throw new AppException('请求过于频繁,请稍后再试', ErrorCode::RATE_LIMIT->value, [
    'retry_after' => $ttl, // Redis TTL 剩余秒数
]);
```

### 5.3 Handler.php 响应映射

| 异常类型 | HTTP 状态码 | `code` 字段 | `msg` 字段 |
|----------|-------------|-------------|------------|
| `AppException` | 400 | `appCode`(业务自定义)| `$exception->getMessage()` |
| JWT 验证失败 | 401 | `1001` | `'未登录或 Token 无效'` |
| 签名验证失败 | 403 | `1022` | `'签名校验失败'` |
| 未捕获异常(500) | 500 | `1039` | 生产环境固定 `'服务器内部错误'`,`APP_DEBUG=true` 时展示真实 message |

> **注意**:HTTP 状态码仅用于基础的中间件层区分(401/403/500),客户端判断业务结果**必须以 `code` 字段为准**,不依赖 HTTP 状态码。

### 5.4 日志写入规范(错误码相关)

| 场景 | 级别 | 强制字段 |
|------|------|----------|
| 业务异常(AppException) | `warning` | `uid, path, app_code, message` |
| 未捕获异常(500/1039)| `error` | `request_id, path, message, trace` |
| 签名失败(1022)| `warning` | `uid, path, x_app_key, x_timestamp` |
| 设备风控触发 | `warning` | `uid, device_id, risk_type` |
| 封禁拦截(1010 / 1011)| `info` | `uid, code, forbidden_type, path` |

---

## 六、HTTP 状态码与 code 映射关系

| HTTP 状态码 | 含义 | 对应 `code` | 说明 |
|------------|------|-------------|------|
| `200` | 正常(含业务失败)| 所有 code | **所有接口统一返回 200**,通过 `code` 字段区分 |
| `400` | 业务异常 | AppException 的 appCode | 中间件或 Service 层主动抛 AppException |
| `401` | 未认证 | `1001` | AuthMiddleware JWT 解析失败 |
| `403` | 签名/权限 | `1022` | SignMiddleware 签名校验失败 |
| `404` | 路由不存在 | — | Slim 框架路由匹配失败,不属于业务错误码范畴 |
| `429` | 限流 | `1031` | RateLimitMiddleware(可选,也可返回 400)|
| `500` | 服务器错误 | `1039` | 未捕获异常兜底 |

> 支付回调(`/pay/callback/wechat`、`/pay/callback/alipay`、`/pay/apple/verify`)的 HTTP 状态码遵循各支付平台要求,不在本规范范围。

---

## 七、第三方 SDK 错误码(OpenIM)

以下为 OpenIM SDK 自身错误码,**与本系统业务错误码体系无关**,由 SDK 回调直接处理,不经过后端 API 响应。

| 错误码 | 含义 | 前端处理 | 处理位置 |
|--------|------|----------|----------|
| `10004` | IM Token 过期 | 调用 `/auth/token/refresh` 刷新 `imToken` → 重新调用 `login()` | `im_adapter.dart` 内 |
| `10005` | IM Token 无效 | 同上 | `im_adapter.dart` 内 |

```dart
// im_adapter.dart 内处理(不走 Dio 拦截器)
OpenIM.iMManager.setOnUserTokenExpiredListener(() async {
  try {
    final newImToken = await AuthRepository.refreshImToken();
    await OpenIM.iMManager.login(
      userID: AppStorage.getString(StorageKeys.userId)!,
      token:  newImToken,
    );
  } catch (e) {
    talker.error('IM Token refresh failed', e);
    // 刷新失败说明业务 Token 也已失效,触发全局登出
    AuthRepository.forceLogout();
  }
});
```

> OpenIM 错误码不在 AppApiError 模型中定义,不在 Dio 拦截器中处理。

---

## 八、错误码分配规范(新增时参照)

**原则:错误码一旦分配不可更改,只可新增,禁止修改已有含义。**

| 范围 | 模块 | 说明 |
|------|------|------|
| `0` | — | 成功,固定不变 |
| `1001–1099` | 通用 / 系统层 | 认证、签名、封禁、系统错误等基础能力;`1010` 普通封禁,`1011` 涉嫌诈骗封禁 |
| `2001–2099` | 认证子模块 | 登录、注册、验证码、第三方授权 |
| `2101–2199` | 支付子模块 | 订单、凭证、商品、VIP |
| `3001–3099` | 信号模块 | 发布、收取、回复、广告解锁 |
| `4001–4099` | 用户社交模块 | 关注、拉黑、举报、访客 |
| `5001–5099` | 上传 / 文件模块 | OSS Token、文件校验 |
| `6001–6099` | 通知 / 推送模块 | 推送注册、通知状态 |
| `7001–7099` | *(预留)* 聊天 / IM 业务层 | OpenIM 回调处理后的业务错误 |
| `8001–8099` | *(预留)* 活动 / 运营 | 限时活动、优惠券等 |
| `-1` | 客户端内部 | 网络层错误(断网/超时),由前端 Dio 拦截器生成,**非后端返回** |

**新增错误码流程:**
1. 确认所属模块范围,在对应区间选取最小可用编号
2. 同步更新本文档(错误码总表 + extraData 说明)
3. 同步更新前端 `AppApiError` 模型的便捷 getter
4. 告知前端开发者需要处理的新 code 及 extraData 结构

---

*文档版本:V2.1 · 同步于《前端开发规范 v5》和《后端开发规范 V4.3》· Flutter 3.41.x / PHP 8.4 / Riverpod 3.x*