# Excel 导入列映射 PRD

> **module**: robot-manager
> **doc_type**: PRD
> **status**: Implemented (2026-04-19)
> **owner**: FFOA Team
> **upstream_docs**: 07-api.md §5（Excel 导入导出）
> **last_updated**: 2026-04-19

**落地记录（2026-04-19 v1）**：
- 后端：新增 `preview` / `importFromExcel(mapping)`；controller 加 `/excel/preview` 端点；3 个新错误码入 RobotError
- 前端：`/import-export` 页改为上传即预览+展示映射表；i18n（zh/en）齐
- L1：新增 4 用例（preview / import with mapping / 必填缺失 / 重复列）

**扩展迭代（2026-04-19 v2）**：
- **列头中性化**：模板 / 映射页 UI 把 `Model Code` 简化为 `Model`（系统 code/name 都认）
- **status 显示名容错**：`In Stock` / `In Transit` / `In Repair` 等显示名也能识别
- **实体按 name 兜底**：Model / SKU / Supplier / Customer / Location 不再只按 code，name 也能命中
- **模糊自动匹配**：`suggestedMapping` 归一忽略大小写 / 空格 / 下划线 / 连字符
- **基础数据批量导入导出**：新增 `RobotEntityExcelService` + `RobotEntityExcelController`，五个实体各有 4 个端点（export/template/preview/import），支持 `skip`/`update`/`abort` 三种冲突策略
- **前端 EntityAdminPage**：加 Template / Export / Import 三个按钮，`excelResource` prop 开关；导入按模板列名自动匹配，无需映射页

**L1 总数**：109 → 122（+13 用例）

**未落地项**：
- 完整映射 UI 用在基础数据导入（当前快捷导入：要求 Excel 列名匹配模板）— 数据导入页保留完整映射，基础数据为简洁用快捷版
- 缺失实体自动创建（PRD 13 新增章节或开独立 PRD 讨论，需考虑相似候选/合并/权限）

---

## 1. 问题

当前 Excel 导入要求用户的表头列名**精确匹配**我们的 FieldDef `labelEn`（如 `FFSN` / `Model Code` / `Purchase Date`）。实际使用中：

- 用户从 SAP / WMS / 自己的 Excel 导出的数据，列名往往与我们不同（如 `Partner SN` vs `Supplier SN`，`SN` vs `FFSN`，中文列名等）
- 每次导入前必须手动修改 Excel 列名，体验差
- 不同来源的 Excel 差异大，别名表难以穷举

## 2. 目标

让用户**在 UI 上把 Excel 的列映射到我们系统的字段**，不强制改 Excel，一次映射当前导入生效。

**非目标**（本迭代不做）：
- 映射模板持久化（每次重映射，不存）
- 多 sheet 处理（仍只处理第一个 sheet）
- 数据清洗 / 类型转换配置（保持现有 service 层逻辑）

---

## 3. 用户流程

```
1. 用户进入 /robot-manager/import-export
2. 点击"选择文件"上传 .xlsx（不立刻导入）
3. 前端上传到后端 /excel/preview → 返回表头 + 有数据的列 + 每列 3 行样本
4. 页面展示"映射表"：
   - 左列：我们需要的字段（必填 + 选填）
   - 右列：Excel 列下拉（值域 = 有数据的 Excel 列 + "不导入"）
   - 自动预匹配：Excel 列名 ≡ FieldDef.labelEn（不区分大小写/空格）时默认选上，用户可改
5. 用户调整映射完成
6. 点击"开始导入"→ POST /excel/import 带 mapping
7. 后端按 mapping 读取数据 → 复用现有导入逻辑 → 返回 total/success/failed/errors
```

---

## 4. UI 设计（简图）

```
┌─────────────────────────────────────────────────────────────┐
│ Import Robots                                               │
├─────────────────────────────────────────────────────────────┤
│ [选择文件] robots-batch-01.xlsx  (12 cols, 45 rows)         │
│                                                             │
│ ┌─── 列映射 ──────────────────────────────────────────────┐ │
│ │ 我们的字段                  Excel 列                    │ │
│ │ ─────────────────────────  ──────────────────────────── │ │
│ │ FFSN *                     [SN ▼]                       │ │
│ │                            样本: FF-202604-01, ...      │ │
│ │ Model Code *               [Robot Model ▼]              │ │
│ │                            样本: Aegis 系列, Rover ...  │ │
│ │ SKU Code *                 [— 请选择 — ▼] ⚠ 必填        │ │
│ │ Supplier Code              [Partner ▼]                  │ │
│ │ Current Status             [Ordered ▼]                  │ │
│ │ Customer Code              [不导入 ▼]                   │ │
│ │ PO Number                  [PO Number ▼]  (自动匹配)    │ │
│ │ ...（按 FieldDef.sortOrder 展开全部 unit scope 字段） │ │
│ └─────────────────────────────────────────────────────────┘ │
│                                                             │
│           [取消]           [开始导入] (禁用直到必填全映射) │
└─────────────────────────────────────────────────────────────┘
```

**视觉规则**：
- 必填字段打 `*`，未映射时右侧下拉红框 + 按钮禁用
- 自动匹配的项显示淡灰色 "(自动匹配)" 提示，表明用户可改
- Excel 列若只有表头无数据 → **不出现在下拉里**（上传时过滤）
- 每个 Excel 列只能用一次（选过的在其它下拉中置灰，避免一列映到多字段）

---

## 5. API 设计

### 5.1 新增 `POST /api/v1/robot-manager/excel/preview`

**权限**：`robot-manager:import`
**Content-Type**：`multipart/form-data`
**Body**：`file` 字段（.xlsx 文件）

**响应**：

```json
{
  "totalRows": 45,
  "columns": [
    {
      "name": "SN",
      "hasData": true,
      "samples": ["FF-202604-00001", "FF-202604-00002", "FF-202604-00003"]
    },
    {
      "name": "Robot Model",
      "hasData": true,
      "samples": ["Aegis 系列", "Rover 系列", "Aegis 系列"]
    },
    {
      "name": "Notes",
      "hasData": false,
      "samples": []
    }
  ],
  "suggestedMapping": {
    "poNumber": "PO Number",
    "supplierSn": "Partner SN"
  }
}
```

**说明**：
- `hasData` = 该列至少有一行非空值；前端 `hasData=false` 的列不展示（也不返回 samples）
- `samples` 取最多 3 行非空值（去重、截断到 50 字符）
- `suggestedMapping` = 自动预匹配结果（labelEn 精确命中，不区分大小写/空白）。key 是我们字段 key，value 是 Excel 列名

**错误码**（复用现有）：
- `ROBOT_EXCEL_MISSING_SHEET` / `ROBOT_EXCEL_MISSING_ROWS`

### 5.2 修改 `POST /api/v1/robot-manager/excel/import`

**新增可选参数** `mapping`（`multipart/form-data` 里的字段之一，JSON 字符串）：

```json
{
  "FFSN": "SN",
  "Model Code": "Robot Model",
  "SKU Code": "SKU",
  "poNumber": "PO Number",
  "supplierSn": "Partner SN"
}
```

- **key**：我们的字段 identifier（固定列用 labelEn，动态字段用 FieldDef.key）
- **value**：Excel 列名

**mapping 未提供时**：退回到旧行为（按 labelEn 精确匹配表头），保证向后兼容。

**mapping 提供时**：
1. 后端按 mapping 读取每行数据（用 Excel 列名取值 → 赋到我们的字段）
2. 必填字段（FFSN / Model Code / SKU Code）若 mapping 里没有 → 返回 400 `ROBOT_IMPORT_REQUIRED_MAPPING_MISSING`
3. 其余逻辑复用现有 `importFromExcel`

---

## 6. 数据模型

**无 schema 变更**。映射仅存在于当次 HTTP 请求生命周期内，不持久化。

前端状态（运行时）：
```ts
type FieldMapping = Record<
  /* 我们字段 key */ string,
  /* Excel 列名，null 或 '__none__' 表示不导入 */ string | null
>;
```

---

## 7. 边界情况

| 情况 | 处理 |
|------|------|
| Excel 没有表头行 | 按现有 `EXCEL_MISSING_ROWS` 报错 |
| 全部列都没数据 | 返回 `columns: []`，前端展示"文件为空"，禁止继续 |
| 两列名相同 | preview 返回 `"Partner (2)"` 这样重命名避免歧义（Excel 本身允许重复列名）|
| 用户映射了同一 Excel 列到多个字段 | 前端阻止（下拉置灰）+ 后端校验返回 400 |
| mapping 里的 Excel 列名在文件中不存在 | 后端返回 400 指出是哪一条 mapping 无效 |
| 自动匹配推荐项与用户手选冲突 | 用户选择永远覆盖 suggested |
| 样本含多行空值 | samples 数组跳过空值，长度可能 < 3 |

---

## 8. 不影响 & 保持不变

- `/excel/template` 模板下载不变，仍是官方推荐路径（用户下载模板填写后不需要映射）
- `/excel/export` 导出不变
- 现有 `importFromExcel` service 方法保留旧签名，新加一个可选 `mapping` 参数
- 错误码单一事实源仍是 `RobotError` enum（可能新增 1 条 `IMPORT_REQUIRED_MAPPING_MISSING`）

---

## 9. 工作量拆分（估算 2-3 天）

### 后端（~1 天）
1. 新增 `RobotExcelService.preview(file)` 方法：解析 + 返回 columns/samples/suggested ≈ 3h
2. 新增 `POST /excel/preview` controller 端点 ≈ 1h
3. `importFromExcel` 支持 mapping 参数：适配数据读取路径 ≈ 2h
4. L1 集成测试：preview 各种边界 + import with mapping 至少 4 条用例 ≈ 2h

### 前端（~1 天）
5. `/import-export` 页改造：上传后先拉 preview，进映射界面 ≈ 3h
6. 映射表组件：左右两列 + 自动匹配高亮 + 一列只能选一次逻辑 ≈ 3h
7. 必填校验 + 提交按钮禁用 + 错误 toast ≈ 2h

### 文档与测试（~0.5 天）
8. 07-api.md 补 `/excel/preview` 规格 + `/excel/import` mapping 参数
9. 09-test-scenarios 加这几个新 L1 用例
10. E2E-008 升级加映射场景

---

## 10. 待确认

- [ ] 错误码 `ROBOT_IMPORT_REQUIRED_MAPPING_MISSING` 的中文/英文文案
- [ ] 是否在映射页保留"跳过映射，按原逻辑匹配"的快捷按钮（给老用户用模板直接导）

---

## 11. 后续可扩展（显式不在本期做）

- 映射模板持久化：按用户或组织存映射，下次一键复用
- 按样本数据智能推断（例如看到列值都是 `FF-YYYYMM-XXXXX` 就建议映到 FFSN）
- 批量导入日志页：把每次导入的 mapping + 结果存下来供追溯
