# 机器人全生命周期管理 - 错误码

> **module**: robot-manager
> **doc_type**: ErrorCodes
> **status**: Active
> **owner**: FFOA Team
> **upstream_docs**: 07-api.md, 04-state-machine.md
> **last_verified**: 2026-04-18

---

## 约定

- 所有业务错误码以 `ROBOT_` 前缀 + 大写下划线分段命名
- 触发时抛 `BadRequestException / ConflictException` 并附 `code` 字段
- 全局 `HttpExceptionFilter` 把 `code` 放进响应 `error.code`
- 前端按 `error.code` 在 i18n `errorCodes` 字典查翻译；未命中则 fallback 到 `error.message`
- 单一事实源：[backend/src/modules/robot-manager/errors/robot-manager.errors.ts](../../../backend/src/modules/robot-manager/errors/robot-manager.errors.ts)

---

## 错误码清单（19 条）

### 状态机校验（6）

| Code | HTTP | 触发点 | 前端文案（zh） |
|------|------|--------|----------------|
| `ROBOT_VERSION_CONFLICT` | 409 | 乐观锁版本不一致 | 数据已被他人修改，请刷新 |
| `ROBOT_MUST_BIND_CUSTOMER_BEFORE_RESERVE` | 400 | IN_STOCK → RESERVED 无 customerId | 进入已预留状态前必须绑定客户 |
| `ROBOT_MUST_SET_SALES_PRICE_BEFORE_SOLD` | 400 | RESERVED → SOLD 无 salesPrice | 签订销售前必须设置销售价格 |
| `ROBOT_CUSTOMER_ID_REQUIRED_FOR_RETURN` | 400 | REPAIRED → DELIVERED 无 customerId | 归还客户时必须绑定客户 |
| `ROBOT_SALES_PRICE_REQUIRED_FOR_RETURN` | 400 | REPAIRED → DELIVERED 无 salesPrice | 归还客户时必须设置销售价格 |
| `ROBOT_REMARK_REQUIRED_FOR_CANCEL` | 400 | `→ CANCELLED` 无 remark | 取消或报废必须填写原因 |

### 不可变字段（2）

| Code | HTTP | 触发点 | 前端文案 |
|------|------|--------|----------|
| `ROBOT_PO_NUMBER_IMMUTABLE` | 400 | poNumber 二次修改 | 采购订单号已设置，不可修改 |
| `ROBOT_SALES_ORDER_ID_IMMUTABLE` | 400 | salesOrderId 二次修改 | 销售订单号已设置，不可修改 |

### 数据导入工具（v3 — 21 codes）

> v3 重做 import 工具的错误码（[14-import-export-tool-prd.md §10](14-import-export-tool-prd.md)）。
> 错误响应走项目标准 wrapper：`{ success: false, error: { code, message, details } }`，
> `details.errors: ValidationIssue[]`（`{ rowNo, field, code, params, severity }`）。
> **前端按 `t.robotManager.errorCodes[code]` 渲染 message，不依赖 server 返中文**。

| Code | HTTP | 触发点 | 前端文案模板（params 占位） |
|------|------|--------|----------|
| `IMPORT_FILE_INVALID` | 400 | Excel 文件格式不合法 / 损坏 | Excel 文件格式不合法或已损坏 |
| `IMPORT_FILE_TOO_LARGE` | 413 | 文件超 10MB | 文件超过 10MB 上限 |
| `IMPORT_FILE_EMPTY` | 400 | Excel 0 行数据 | Excel 文件无数据 |
| `IMPORT_SHEET_MISSING` | 400 | 缺 Sheet1 | Excel 缺少 Sheet1 |
| `IMPORT_COLUMN_MISSING` | 400 | 表头不含必填列 | 缺必填列：{field} |
| `IMPORT_ROW_LIMIT` | 400 | 超 1000 行 | 单批最多 1000 行（实际 {actual}，请拆分） |
| `IMPORT_TYPE_MISMATCH` | 400 | 类型转换失败 | 第 {row} 行 {field}：类型应为 {expected} |
| `IMPORT_REQUIRED_MISSING` | 400 | 必填漏 | 第 {row} 行 {field}：必填字段为空 |
| `IMPORT_ENUM_INVALID` | 400 | 枚举值错误 | 第 {row} 行 {field}：{value} 不在范围 |
| `IMPORT_FK_NOT_FOUND` | 400 | FK code 找不到 | 第 {row} 行 {field}：引用 {value} 在{业务术语}不存在 |
| `IMPORT_DUPLICATE_KEY` | 400 | unique 冲突 | 第 {row} 行 {field}：{value} 已存在 |
| `IMPORT_BUSINESS_RULE_VIOLATION` | 400 | 业务规则失败 | 第 {row} 行：{ruleDescription} |
| `IMPORT_BATCH_NOT_FOUND` | 404 | batch 不存在 / IDOR 探测 | 导入批次不存在（404 不抛 403，防探测） |
| `IMPORT_BATCH_HAS_ERRORS` | 400 | confirm 时 errorRows > 0 | 批次仍有 {n} 个错误行 |
| `IMPORT_BATCH_ALREADY_CONFIRMED` | 409 | 重复 confirm（CAS 冲突） | 批次已确认导入 |
| `IMPORT_BATCH_EXPIRED` | 410 | VALIDATED batch 超 N 天未 confirm | 批次已过期，请重新上传 |
| `IMPORT_BATCH_INTERRUPTED` | 500 | OnBootstrap 扫到 unfinished batch | 处理中断（进程崩溃），请重新上传 |
| `IMPORT_PERMISSION_DENIED` | 403 | 缺权限 | 缺少导入权限 |
| `IMPORT_FILE_TAMPERED` | 400 | fileHash 不匹配 | 文件与 preview 时不一致 |
| `IMPORT_REFS_CHANGED_RETRY` | 409 | confirm 阶段重校验失败（引用 drift） | 引用数据在等待期间被修改，请重新上传 |
| `IMPORT_CONCURRENT_BATCH` | 429 | per-user 同时 in-flight batch ≤ 1 | 您当前已有未完成的导入批次，请先处理或取消 |

### FieldDef 管理（7）

| Code | HTTP | 触发点 | 前端文案 |
|------|------|--------|----------|
| `ROBOT_FIELD_KEY_OR_LABEL_EN_REQUIRED` | 400 | POST 未提供 key/labelEn | 请填写 Key 或英文标签 |
| `ROBOT_FIELD_KEY_GENERATION_FAILED` | 400 | labelEn 无可用字母数字 | 无法从英文标签生成 Key，请改用含字母数字的文案或手动指定 |
| `ROBOT_FIELD_KEY_MUST_START_WITH_LETTER` | 400 | 生成的 key 以数字开头 | 生成的 Key 必须以字母开头 |
| `ROBOT_FIELD_GROUP_REQUIRED_FOR_UNIT_SCOPE` | 400 | scope=unit 无 group | scope=unit 时必须指定分组（group） |
| `ROBOT_FIELD_OPTIONS_REQUIRED_FOR_SELECT` | 400 | type=select 无 options | select 类型字段必须提供选项 |
| `ROBOT_FIELD_OPTION_REQUIRES_CODE_AND_LABEL` | 400 | options[].code 或 label 缺失 | 字典选项必须同时包含 code 和 label |
| `ROBOT_FIELD_OPTIONS_ONLY_FOR_SELECT` | 400 | type≠select 携带 options | 选项仅适用于 select 类型字段 |

### 附件（2）

| Code | HTTP | 触发点 | 前端文案 |
|------|------|--------|----------|
| `ROBOT_ATTACHMENT_NO_FILE` | 400 | 上传未提供 file | 请选择要上传的文件 |
| `ROBOT_ATTACHMENT_TOO_LARGE` | 400 | 文件超 50 MB | 文件超过大小上限（50MB） |

---

## 测试断言

- L1 集成测试对 18 条精确断言 `expect(res.body.error.code).toBe('ROBOT_XXX')`
- `ROBOT_ATTACHMENT_TOO_LARGE` 需要构造 50MB Buffer，成本高，路径与 NO_FILE 同构，不做精确断言但 i18n 已齐
- 覆盖详见 [09-test-scenarios.md §3 错误码矩阵](09-test-scenarios.md#三错误码矩阵robotError-枚举)

## i18n 同步

- [frontend/src/locales/robot-manager/zh.ts](../../../frontend/src/locales/robot-manager/zh.ts) `errorCodes`
- [frontend/src/locales/robot-manager/en.ts](../../../frontend/src/locales/robot-manager/en.ts) `errorCodes`
- 19 条 key 两边完全对称
