# 组织架构 — 人员录入（Excel 批量导入）

> **平台自己解析 Excel，CLI 只负责把文件送上去、把解析结果取回来** —— CLI 不解析单元格，
> 也不想解析：字段映射、必填校验、主子表展开全在平台侧，CLI 自己解一遍只会跟平台不一致。

## 命令

| 命令 | 说明 |
|------|------|
| `e10-cli org employee template [--out <路径>]` | 导出人员导入模板（xlsx）。默认输出到当前目录的 `人员导入模板.xlsx` |
| `e10-cli org employee import <file>` | 从 Excel 导入人员。`--dry-run` 只预览不导入；`--mode add\|update`（默认 `add`）；`--find-by <字段>`（更新时按哪个字段认人，默认 `username`）；`--review <n>` 预览行数（默认 100） |
| `e10-cli org employee task <taskId>` | 查某个导入任务的结果（没等、或等超时了，用这个补看） |

导入默认**等结果**：打完 `已导入 N 条 / 失败 M 条` 才结束，**有失败行时退出码非 0**，脚本可以直接判。

| 选项 | 说明 |
|---|---|
| `--no-wait` | 提交后立刻返回。⚠ 不等就等于不校验 —— **「已提交」不代表导入成功**，要自己再用 `task` 查 |
| `--wait-timeout <ms>` | 等结果的超时（默认 60000）。超时不编成功，如实报「仍在处理中」并给 taskId |
| `--error-file <路径>` | 失败数据的保存路径。默认与源文件同目录、同名 + `.错误数据.xlsx` |
| `--no-error-file` | 不保存失败数据文件（原因仍会打出来） |

## 模板怎么填（三处最容易填错）

模板第 2 个工作表「模板注意事项说明」里也写了这几条，这里挑出**填错代价最大**的三处：

### 1. 「状态（必填）」列只认平台那几个词

`试用` / `试用延期` / `正式` / `临时` / `实习` / `离职` / `退休` / `解聘` / `无效`，就这九个 ——
填其它写法（比如 `1`、`在职`）**整行导入失败**。

⚠ 别拿**动作流里「人员状态」**的 `1`（在职）/ `2`（离职）来填这一列，那是另一套取值，两者不通。

### 2. 「密码」在第 35 列（AI 列）

模板一共 63 列，密码夹在中间那段：
`AH` 登录名 → **`AI` 密码** → `AJ` 解除账号 → `AK` 安全级别。
填到隔壁的「安全级别」（第 37 列）**不会报错**，只是那人的密码没设上 —— 属于静默填错。

### 3. 有上级/助理的人，拆两批导

「上级」「其他上级」「助理」填的是**别人的姓名**，如果那个人也在这张表里，
**同一批导入可能引用不到**。
做法：**第一批只导没有任何上级/助理的人**，第二批再导引用他们的那些 ——
不用改表结构，拆成两个文件依次导即可。

- 「上级」列**只填一个人**；要挂多个上级用「其他上级」列，多个之间用 `,` 分隔。
- 这几列按平台说明，必须是该上级/助理**「重复验证字段」**的值（编号、姓名选其一）。

### 别忘了删示例行

模板第 2~9 行是 `user001`~`user008` 的**演示数据**（平台自带的），**导入前整行删掉**，
否则这几行会被当成真人建进组织里。

## ⚠ 先 `--dry-run`，再真导

批量写组织数据是**外部可见且不好撤销**的操作。导入前先干跑一次：

```bash
e10-cli org employee import 人员.xlsx --dry-run
```

干跑会打出**平台解析出来的**工作表名、列数、行数和前几行内容 —— 拿它跟你的原表对一遍，
确认列没串、行数对得上（尤其是**模板自带的示例行要删掉**），再去掉 `--dry-run` 真导。

`--dry-run` 停在「预览」这一步，**这一步之前不会往组织里写任何东西**。

## 链路（七步，每步产出喂给下一步）

| | 端点 | 产出 |
|---|---|---|
| ⓪ | `POST /api/bs/hrm/emp/edit/importParam` | `bizId` + moduleName / functionName / handlerName / serviceName / dataType |
| ① | `POST /api/file/preUploadCheck`（multipart） | 预上传 id |
| ② | `POST /api/file/module/upload`（multipart） | `fileId` |
| ③ | `POST /api/ebatch/checkFilePassword` | — |
| ④ | `POST /api/ebatch/uploadFileId` | `taskId` |
| ⑤ | `POST /api/ebatch/getExcelData` | 预览行（**不落库**） |
| ⑥ | `POST /api/ebatch/importExcelData` | 真正执行 |
| ⑦ | `POST /api/ebatch/getByTaskId` | **导入结果**：条数 / 失败原因 / 错误文件 |

- **⓪ 把 ④ 要的元数据全给了** —— moduleName / functionName / handlerName / serviceName / dataType
  都不是 CLI 硬编码的，`bizId` 更是每次调用现生成。
- **①② 的 multipart 字段逐字相同**，② 只多 `preUploadId` 与 `uploadType=common`。
- ⑤⑥⑦ 的参数全在 **query string**，body 为空。

## 导入结果（⑥ 只是「提交了」，结论在 ⑦）

**⑥ 的响应体只有一个 `true`** —— 条数、失败原因、错误文件**全都不在里面**。只发 ⑥ 就报成功，
会让**整批失败也显示成「导入已提交」**，所以命令发完 ⑥ 会去轮询 ⑦。

```json
{"status": -1,
 "msg": "存在错误数据，下载错误文件查看",
 "callbackData": "{\"msg\":\"导入失败\",\"errorFileId\":\"...\",\"module\":\"hrm\",\"successCount\":0,\"errorCount\":\"1\"}"}
```

- **`callbackData` 是一个 JSON 字符串**，要二次解析 —— 条数和错误文件 id 都在里面。
- ⚠ **`successCount` 是数字、`errorCount` 是字符串**（`1` / `"0"`），同一段 JSON 里类型不一致，两边都得转一遍。
- ⚠ **`status: 0` 不是「成功」** —— 它是个**中途值**：同一个任务先读到 `status: 0` + `msg: 成功`、
  `callbackData` 还是空的，几秒后才落成 `-1` + 失败 1 条。**判完成的依据是 `callbackData` 能不能解出来，不是 `status`**；
  按 `status` 收工就会把失败读成成功。
- `status: -1` = 存在错误数据，可以直接当终态（命令也这么处理）。

### 失败原因在**单元格批注**里，不在任何一列

导出的失败数据文件里**只有出错的那几行，没有「原因」列** —— 用表格软件打开看值，看不出为什么失败。
原因挂在**出错单元格的批注（Excel comment）**上，形态是：

| 单元格 | 值 | 批注 |
|---|---|---|
| A2 | 人员A | **姓名 重复** |

所以 `import` / `task` 会把批注读出来直接打给你：

```
导入结果: 人员A.xlsx
  已导入     0 条
  失败       1 条
  平台说明   存在错误数据，下载错误文件查看
  失败原因
    第 2 行 · 姓名（人员A）: 姓名 重复
  失败数据   1323916251655815168 → 人员A.错误数据.xlsx
```

### 失败数据怎么下来的

```
GET /api/file/remotedownload/<fileId>/token/true?authModule=ebatch
  → 302 → 对象存储的签名地址（文件真身在这边，文件名也在这一步给）
```

下载按**两步**取：先带鉴权拿到 302，再**不带任何鉴权头**去对象存储取字节 ——
会话凭证不跟着跳转发到第三方域名上。

## 几个坑

- **取 id 的层级三步三样**：① 在 `data.preUploadId`（**有的环境**是 `data.uploadModuleDto.fileid`）、
  ② 在 `data.fileid`（**小写 d**）、④ 在 `data.data.taskId`（多一层）。照着别的接口的写法套会静默取到空。
- **① 的返回形状随环境变**：有的环境 `preUploadCheck` 只回 `data.preUploadId`
  （`data` 下就 `preUploadId` / `uploadType` / `permissionType` 三个键，**没有** `uploadModuleDto`）。
  两种写法语义相同，命令都认；一个都没有才报错。只认其中一种的话，那条环境整条导入链**第①步就断**。
- **⑤ 的参数是 `rerviewNum`** —— 平台把 review 拼错了，别"顺手改正"成 `reviewNum`。
- **`weaTable[0].total` 是 `-1`**，不能拿来当行数；行数看 `data` 数组长度。
- **`rerviewNum` 只是一次取多少行**，预览到的未必是全部 —— 行数刚好等于上限时命令会提示。
- **⚠⚠ 分部名写错不会报错 —— 平台会静默新建一个分部，把人放进去。**
  「分部（必填）」填一个机构里不存在的名字（例如把「泛微北京」写成「泛微北金」），导入**照样回成功**，
  然后机构里会多出一个同名分部（父级 = 操作者所在组织），人落在里面。**没有报错、没有警告**，
  `--dry-run` 的预览也看不出来 —— 预览只是把单元格原样回显，**不做任何校验**。
  所以：**分部名必须是机构里已有的名字**，导之前自己核一遍 —— 一次手误就是一条脏组织数据 + 一个错位的人。
- **`importType` 在 ④ 的报文里出现两次**：顶层是空串、真正生效的在 `uploadSet.customObj` 里。

## 环境差异

`emp/edit/importParam` 是判定「这个环境能不能导入人员」的探针 —— 返回里没有 `bizId` / `functionName`
就说明该环境不支持，命令会直接报错，不会带着半截参数往下走。
