# iWork 集成 API 与使用指南

本文面向 **调用方后端开发者**，介绍如何在 System 模块中使用现有的 iWork 集成能力：流程发起/作废、附件回调、人力组织代理与全量同步。文档按“流程调用能力 / 组织同步能力”拆分，并包含 Mermaid 流程图、字段映射表及禁用策略说明，便于快速自助集成。

---

## 背景与适用范围

- 所有接口位于 `zt-module-system-server` 的 `IWorkIntegrationController` 下，统一前缀 `/system/integration/iwork`。
- Service 层提供 `IWorkIntegrationService`、`IWorkOrgRestService`、`IWorkSyncService` 三个 Bean，可在任意业务模块注入调用。
- 调用 iWork 之前会实时执行 register + apply-token，不保留本地缓存；组织同步则通过 key+ts 鉴权代理 iWork HR 接口并写入本地组织/用户表。

---

## 快速开始清单

- [ ] `application.yml` 或配置中心已填写 `iwork.*` 参数。
- [ ] 确认所在网络能访问 iWork register/apply-token/HR 接口。
- [ ] 明确调用方向：流程调用 or 组织同步，两种能力可独立使用。
- [ ] 若使用文件回调，已约定业务附件的 `businessCode` 与租户。
- [ ] 管理端 `admin-api/system/integration/iwork/**` 已放通权限。

---

## 配置清单

### YAML 示例

```yaml
iwork:
  base-url: https://iwork.example.com
  app-id: my-iwork-app
  client-public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
  user-id: system
  workflow:
    seal-workflow-id: 54
  paths:
    register: /api/ec/dev/auth/regist
    apply-token: /api/ec/dev/auth/applytoken
    user-info: /api/workflow/paService/getUserInfo
    create-workflow: /api/workflow/paService/doCreateRequest
    void-workflow: /api/workflow/paService/doCancelRequest
  token:
    ttl-seconds: 3600
  client:
    connect-timeout: 5s
    response-timeout: 30s
  org:
    token-seed: 5936562a-d47c-4a29-9b74-b310e6c971b7
    paths:
      subcompany-page: /api/hrm/resful/getHrmsubcompanyWithPage
      department-page: /api/hrm/resful/getHrmdepartmentWithPage
      job-title-page: /api/hrm/resful/getJobtitleInfoWithPage
      user-page: /api/hrm/resful/getHrmUserInfoWithPage
      sync-subcompany: /api/hrm/resful/synSubcompany
      sync-department: /api/hrm/resful/synDepartment
      sync-job-title: /api/hrm/resful/synJobtitle
      sync-user: /api/hrm/resful/synHrmresource
```

| 配置键 | 说明 | 必填 | 默认值 |
| --- | --- | --- | --- |
| `iwork.base-url` | iWork 网关地址，需与网络互通 | 是 | - |
| `iwork.app-id` | 平台分配的 AppId，用于申请 token | 是 | - |
| `iwork.client-public-key` | iWork 端提供的 RSA 公钥，系统用其加密凭证 | 是 | - |
| `iwork.workflow.seal-workflow-id` | 盖章流程模板 ID，其他流程可扩展 | 是 | - |
| `iwork.paths.*` | 控制器访问 iWork 的后端路由 | 是 | - |
| `iwork.token.ttl-seconds` | token 缓存时长，单位秒 | 否 | 3600 |
| `iwork.client.connect-timeout` | OkHttp 建连超时 | 否 | 5s |
| `iwork.org.token-seed` | HR 组织同步签名盐值 | 是 | - |

## 流程调用 API

### 端到端互动图

```mermaid
sequenceDiagram
    participant Client as 调用方服务
    participant Gateway as ZTCloud /system 接口
    participant IWork as iWork 平台
    Client->>Gateway: POST /auth/register
    Gateway->>IWork: /api/ec/dev/auth/regist
    IWork-->>Gateway: 返回 secret + spk
    Client->>Gateway: POST /auth/token
    Gateway->>IWork: /api/ec/dev/auth/applytoken
    IWork-->>Gateway: token
    Client->>Gateway: POST /workflow/create
    Gateway->>IWork: /api/workflow/paService/doCreateRequest
    IWork-->>Gateway: requestId
    IWork-->>Gateway: 文件回调
    Gateway-->>Client: 业务附件 ID
```

### HTTP 接口分组

| 功能 | Method & Path | 对应 iWork 接口 | 说明 |
| --- | --- | --- | --- |
| 注册凭证 | POST `/system/integration/iwork/auth/register` | `/api/ec/dev/auth/regist` | 生成 secret + spk，用于后续 token 申请 |
| 申请 token | POST `/system/integration/iwork/auth/token` | `/api/ec/dev/auth/applytoken` | 以 secret + operatorUserId 换取 token |
| 用户解析 | POST `/system/integration/iwork/user/resolve` | `/api/workflow/paService/getUserInfo` | 通过工号/手机号等标识查询 iWork 用户 ID |
| 发起流程 | POST `/system/integration/iwork/workflow/create` | `/api/workflow/paService/doCreateRequest` | 将本地业务单推送至 iWork 指定流程 |
| 作废流程 | POST `/system/integration/iwork/workflow/void` | `/api/workflow/paService/doCancelRequest` | 针对 requestId 触发终止/回收 |
| 文件回调 | POST `/system/integration/iwork/callback/file` | iWork 自定义回调 | 将 iWork 附件落地并与业务编码关联 |

### 关键参数与 VO

| 接口 | 请求体 | 核心字段 | 说明 |
| --- | --- | --- | --- |
| 注册凭证 | `IWorkAuthRegisterReqVO` | `forceRefreshRegistration` | true 时会重新向 iWork 注册，常用于公钥被重置的场景 |
| 申请 token | `IWorkAuthTokenReqVO` | `operatorUserId`、`forceRefreshToken` | `operatorUserId` 默认读取 `iwork.user-id`，必要时可传入办件人 ID |
| 用户解析 | `IWorkUserInfoReqVO` | `identifierKey`、`identifierValue` | 支持 `workCode`、`mobile`、`loginId` 等键值组合 |
| 创建流程 | `IWorkWorkflowCreateReqVO` | `workflowId`、`mainData`、`attachments` | `workflowId` 缺省则采用 `iwork.workflow.seal-workflow-id`，`mainData` 为流程主表字段 |
| 作废流程 | `IWorkWorkflowVoidReqVO` | `requestId`、`reason` | reason 最长 400 字，系统会自动截断超长字符 |
| 文件回调 | `IWorkFileCallbackReqVO` | `fileUrl`、`businessCode` | 将远程文件下载后与业务附件绑定，`businessCode` 对应已有 `BusinessFile` 记录 |

### 发起流程示例

```bash
curl -X POST https://{host}/system/integration/iwork/workflow/create ^
  -H "Content-Type: application/json" ^
  -H "Authorization: Bearer <your-token>" ^
  -d @payload.json
```

payload 示意：

```json
{
  "operatorUserId": "1001",
  "forceRefreshToken": false,
  "jbr": "1001",
  "yybm": "2001",
  "fb": "3001",
  "sqsj": "2025-01-01",
  "yyqx": "寄送客户",
  "yyfkUrl": "https://oss.example.com/依据附件.pdf",
  "yysy": "与 XX 公司签订框架合同",
  "xyywjUrl": "https://oss.example.com/材料附件.pdf",
  "xyywjFileName": "材料附件.pdf",
  "yysx": "合同用印",
  "ywxtdjbh": "DJ-2025-00018"
}
```

### Java 调用片段

```java
IWorkWorkflowCreateReqVO req = new IWorkWorkflowCreateReqVO();
req.setWorkflowId(54);
req.setOperatorUserId("1001");
req.setMainData(Map.of("title", "盖章申请", "amount", 128000));
req.setForceRefreshToken(false);

CommonResult<IWorkOperationRespVO> resp = restTemplate.postForObject(
        baseUrl + "/system/integration/iwork/workflow/create",
        req,
        new ParameterizedTypeReference<>() {});

if (resp == null || !resp.isSuccess()) {
    throw new IllegalStateException("iWork 调用失败" + Optional.ofNullable(resp).map(CommonResult::getMsg).orElse(""));
}
String requestId = resp.getData().getRequestId();
```

### 通用请求字段（IWorkBaseReqVO）

| 字段 | 类型 | 必填 | 说明 | 示例 |
| --- | --- | --- | --- | --- |
| `appId` | string | 否 | 覆盖默认的 iWork 应用编号；为空时取配置 | `iwork-app` |
| `operatorUserId` | string | 否 | 作为 iWork 操作人的用户编号；为空时取 `iwork.user-id` | `1001` |
| `forceRefreshToken` | boolean | 否 | true 时忽略本地缓存并强制重新申请 token | `false` |

> 所有接口均返回 `CommonResult<T>` 结构，通用包体为 `{ "code": 0, "data": T, "msg": "success", "traceId": "..." }`。下文的响应字段默认落在 `data` 节点内。

### 流程接口字段详解

#### POST `/system/integration/iwork/auth/register`

##### 请求字段（/auth/register）

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `forceRefreshRegistration` | boolean | 否 | true 时即使已有注册信息也会重新向 iWork 注册，以获取新的 secret + serverPublicKey |

##### 响应字段（/auth/register）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `appId` | string | 实际使用的 iWork 应用编号 |
| `clientPublicKey` | string(Base64) | 系统用于加密 secret 的客户端公钥 |
| `clientPrivateKey` | string(Base64) | 仅在动态生成公钥时回传，用于后续持久化 |
| `serverPublicKey` | string(Base64) | iWork 返回的服务端公钥，需用于加密 secret 与 userId |
| `secret` | string | iWork 发放的密钥，结合公钥加密后才能申请 token |

##### JSON 示例（/auth/register）

请求：

```http
POST /system/integration/iwork/auth/register HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer ***

{
  "forceRefreshRegistration": true
}
```

响应：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 0,
  "msg": "success",
  "data": {
    "appId": "iwork-app",
    "clientPublicKey": "MIIBIjANBgkq...",
    "clientPrivateKey": "MIIEvAIBADANBg...",
    "serverPublicKey": "MIIBCgKCAQEAv...",
    "secret": "d9e7f3e4c5"
  },
  "traceId": "efdb73a0e9f7"
}
```

失败示例（iWork 接口异常）：

```http
HTTP/1.1 502 Bad Gateway
Content-Type: application/json

{
  "code": 100500,
  "msg": "向 iWork 注册失败: upstream timeout",
  "data": null,
  "traceId": "c4019de7c3aa"
}
```

#### POST `/system/integration/iwork/auth/token`

##### 请求字段（/auth/token）

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `operatorUserId` | string | 否 | 不填则取配置的默认操作者 |
| `forceRefreshToken` | boolean | 否 | true 时忽略缓存直接调 iWork 申请新 token |
| `forceRefreshRegistration` | boolean | 否 | true 时在申请 token 前重新注册，以应对 serverPublicKey 失效 |

##### 响应字段（/auth/token）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `appId` | string | 本次 token 绑定的应用编号 |
| `operatorUserId` | string | 实际用于发起流程的 iWork 用户 |
| `token` | string | iWork 访问令牌 |
| `encryptedUserId` | string | 使用 serverPublicKey 加密后的 userId，调用 iWork 接口时需要写入 Header |
| `expiresAtEpochSecond` | long | 预计过期时间（Epoch 秒） |
| `serverPublicKey` | string | token 对应的 server 公钥 |

##### JSON 示例（/auth/token）

请求：

```http
POST /system/integration/iwork/auth/token HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer ***

{
  "operatorUserId": "1001",
  "forceRefreshToken": false,
  "forceRefreshRegistration": false
}
```

响应：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 0,
  "msg": "success",
  "data": {
    "appId": "iwork-app",
    "operatorUserId": "1001",
    "token": "389a6d30-2f7a-4a1a",
    "encryptedUserId": "Q0ZCU0RmVz...",
    "expiresAtEpochSecond": 1764825600,
    "serverPublicKey": "MIIBCgKCAQEAv..."
  },
  "traceId": "9012ab34cd56"
}
```

失败示例（secret 失效）：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 0,
  "msg": "success",
  "data": {
    "appId": "iwork-app",
    "operatorUserId": "1001",
    "token": null,
    "encryptedUserId": null,
    "expiresAtEpochSecond": null,
    "serverPublicKey": "MIIBCgKCAQEAv...",
    "success": false,
    "message": "iWork: secret expired, please re-register"
  },
  "traceId": "70e9570f218b"
}
```

#### POST `/system/integration/iwork/user/resolve`

##### 请求字段（/user/resolve）

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `identifierKey` | string | 是 | 写入 iWork 所需的字段名，如 `workcode`、`mobile`、`loginid` |
| `identifierValue` | string | 是 | 对应字段的取值 |
| `payload` | map | 否 | 附加在请求体的扩展参数，会与识别字段一起发送 |
| `queryParams` | map | 否 | 附加在 URL 上的查询参数 |

##### 响应字段（/user/resolve）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `payload` | map | iWork 原始返回 JSON |
| `success` | boolean | 是否判定为成功（根据 code/success 等字段自动推断） |
| `message` | string | 友好提示或错误信息 |
| `userId` | string | 从返回体解析出的 iWork 用户编号 |

##### JSON 示例（/user/resolve）

请求：

```http
POST /system/integration/iwork/user/resolve HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer ***

{
  "identifierKey": "workcode",
  "identifierValue": "A10086",
  "queryParams": {
    "tenant": "ztcloud"
  }
}
```

响应：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 0,
  "msg": "success",
  "data": {
    "success": true,
    "message": "OK",
    "userId": "1001",
    "payload": {
      "code": "SUCCESS",
      "data": {
        "userid": "1001",
        "lastname": "张三",
        "loginid": "zhangsan"
      }
    }
  },
  "traceId": "b8c1d5fe1039"
}
```

失败示例（未找到用户）：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 0,
  "msg": "success",
  "data": {
    "success": false,
    "message": "iWork 返回空数据",
    "userId": null,
    "payload": {
      "code": "SUCCESS",
      "data": []
    }
  },
  "traceId": "4ea7a1dd6281"
}
```

#### POST `/system/integration/iwork/workflow/create`

##### 请求字段（/workflow/create）

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `jbr` | string | 是 | 用印经办人（iWork 用户 ID） |
| `yybm` | string | 是 | 用印部门 ID |
| `fb` | string | 是 | 用印单位（分部 ID） |
| `sqsj` | string(yyyy-MM-dd) | 是 | 申请时间 |
| `yyqx` | string | 是 | 用印去向/流向 |
| `yyfkUrl` | string | 否 | 用印依据附件 URL |
| `yysy` | string | 否 | 用印事由 |
| `xyywjUrl` | string | 是 | 待用印材料附件 URL，系统会封装成 iWork 需要的数组结构 |
| `xyywjFileName` | string | 否 | 待用印材料附件名称，默认从 URL 截取 |
| `yysx` | string | 是 | 用印事项（流程标题关键字段） |
| `ywxtdjbh` | string | 是 | 业务系统单据编号，用于生成流程标题与追溯 |

##### 响应字段（/workflow/create）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `payload.code` | string | iWork 返回的业务状态，如 `SUCCESS` |
| `payload.data.requestId` | long | iWork 流程请求编号，后续作废/查询需要使用 |
| `payload.errMsg` | map | 错误附加信息（失败时） |
| `payload.reqFailMsg.keyParameters` | map | 失败时的关键参数回显 |
| `success` | boolean | SDK 根据 code/success 判断的结果 |
| `message` | string | 统一的提示文案 |

##### JSON 示例（/workflow/create）

请求：

```http
POST /system/integration/iwork/workflow/create HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer ***

{
  "operatorUserId": "1001",
  "forceRefreshToken": false,
  "jbr": "1001",
  "yybm": "2001",
  "fb": "3001",
  "sqsj": "2025-01-01",
  "yyqx": "寄送客户",
  "yyfkUrl": "https://oss.example.com/依据附件.pdf",
  "yysy": "与 XX 公司签订框架合同",
  "xyywjUrl": "https://oss.example.com/材料附件.pdf",
  "xyywjFileName": "材料附件.pdf",
  "yysx": "合同用印",
  "ywxtdjbh": "DJ-2025-00018"
}
```

响应：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 0,
  "msg": "success",
  "data": {
    "success": true,
    "message": "流程创建成功",
    "payload": {
      "code": "SUCCESS",
      "data": {
        "requestid": 9623451
      },
      "errMsg": {},
      "reqFailMsg": null
    }
  },
  "traceId": "0af7c8f2c7a1"
}
```

失败示例（字段缺失）：

```http
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "code": 100001,
  "msg": "参数校验失败",
  "data": {
    "success": false,
    "message": "缺少必填字段: yysx",
    "payload": {
      "code": "FAIL",
      "reqFailMsg": {
        "keyParameters": {
          "missingField": "yysx"
        },
        "msgInfo": {
          "message": "Please provide seal reason"
        }
      }
    }
  },
  "traceId": "f91c1c3772af"
}
```

#### POST `/system/integration/iwork/workflow/void`

##### 请求字段（/workflow/void）

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `requestId` | string | 是 | 需要作废的 iWork 流程编号 |
| `reason` | string | 否 | 作废原因，将映射到 iWork 的 `remark` 字段（超长会被截断） |
| `extraParams` | map | 否 | 直接透传给 iWork 的附加 JSON 字段 |
| `formExtras` | map<string,string> | 否 | 会被追加到 payload 中的额外 form 字段 |

##### 响应字段（/workflow/void）

与 `/workflow/create` 使用的 `IWorkOperationRespVO` 结构完全一致。

##### JSON 示例（/workflow/void）

请求：

```http
POST /system/integration/iwork/workflow/void HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer ***

{
  "requestId": "REQ-9623451",
  "reason": "业务单据撤回，需终止流程"
}
```

响应：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 0,
  "msg": "success",
  "data": {
    "success": true,
    "message": "流程已成功作废",
    "payload": {
      "code": "SUCCESS",
      "data": {
        "requestid": 9623451
      }
    }
  },
  "traceId": "5c7f0a9b1d23"
}
```

失败示例（requestId 不存在）：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 0,
  "msg": "success",
  "data": {
    "success": false,
    "message": "iWork 返回失败: request not found",
    "payload": {
      "code": "FAIL",
      "reqFailMsg": {
        "keyParameters": {
          "requestid": "REQ-999999"
        },
        "msgInfo": {
          "msg": "流程不存在或已处理"
        }
      }
    }
  },
  "traceId": "98d73ae0bf12"
}
```

#### POST `/system/integration/iwork/callback/file`

##### 请求字段（/callback/file）

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `fileUrl` | string | 是 | iWork 可访问的文件下载地址 |
| `businessCode` | string | 是 | 已存在的业务附件编码，将使用该编码定位租户与业务ID |
| `fileName` | string | 否 | 覆盖默认文件名，不填则从 URL 末尾截取 |

##### 响应字段（/callback/file）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `data` | long | 成功创建的业务附件关联 ID |

##### JSON 示例（/callback/file）

请求：

```http
POST /system/integration/iwork/callback/file HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "fileUrl": "https://oss.example.com/files/abc.pdf",
  "businessCode": "DJ-2025-00018",
  "fileName": "电子合同.pdf"
}
```

响应：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 0,
  "msg": "success",
  "data": 8823451990123,
  "traceId": "6f1b0c7d8e91"
}
```

失败示例（业务编码不存在）：

```http
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "code": 100210,
  "msg": "业务编码不存在，无法创建附件",
  "data": null,
  "traceId": "72e4fa109d55"
}
```

## 组织同步 API

### 数据流示意

```mermaid
flowchart LR
  IWork[(iWork HR)] -->|分页接口| RestProxy[/IWorkOrgRestService/]
  RestProxy -->|BatchResult| SyncService{{IWorkSyncService}}
  SyncService -->|Dept/Post/UserSaveReqVO| LocalDB[(ZTCloud 系统库)]
  SyncService -->|统计| 调用方
```

### 接口矩阵

| 功能 | Method & Path | 请求体 | 备注 |
| --- | --- | --- | --- |
| 分部分页 | POST `/system/integration/iwork/hr/subcompany/page` | `IWorkSubcompanyQueryReqVO` | 透传 `subcompanycode/subcompanyname` 条件 |
| 部门分页 | POST `/system/integration/iwork/hr/department/page` | `IWorkDepartmentQueryReqVO` | 支持按照 `subcompanyId`、名称过滤 |
| 岗位分页 | POST `/system/integration/iwork/hr/job-title/page` | `IWorkJobTitleQueryReqVO` | 可按岗位编码、名称筛选 |
| 人员分页 | POST `/system/integration/iwork/hr/user/page` | `IWorkUserQueryReqVO` | 支持工号、手机号、状态等组合查询 |
| 分部全量同步 | POST `/system/integration/iwork/hr/subcompanies/full-sync` | `IWorkFullSyncReqVO` | 默认 `scopes=subcompany`，按分页批量落库 |
| 部门全量同步 | POST `/system/integration/iwork/hr/departments/full-sync` | `IWorkFullSyncReqVO` | 自动处理父子依赖，多次遍历确保 parent ready |
| 岗位全量同步 | POST `/system/integration/iwork/hr/job-titles/full-sync` | `IWorkFullSyncReqVO` | 岗位编码固定为 `IWORK_JOB_${id}` |
| 人员全量同步 | POST `/system/integration/iwork/hr/users/full-sync` | `IWorkFullSyncReqVO` | 工号优先映射为用户名，自动建档岗位 |

### Full Sync 请求参数

| 字段 | 默认值 | 说明 |
| --- | --- | --- |
| `startPage` | 1 | 开始拉取的页码，对应 iWork `curpage` |
| `pageSize` | 100 | 单页数量（1-500） |
| `maxPages` | null | 限制最大页数，null 表示直到 iWork 返回空页 |
| `scopes` | 全量 | 枚举 `subcompany`、`department`、`jobTitle`、`user`，可多选 |
| `id` | null | 指定单个 iWork 实体 ID，主要用于补偿 |
| `includeCanceled` | false | `true` 时同步 iWork 失效记录并将其禁用 |
| `allowUpdate` | false | 是否允许更新本地已有但来源为 iWork 的记录 |

> 系统内部始终启用 `createIfMissing=true`，因此全量同步会自动建档缺失的分部/部门/岗位/人员。

### 字段映射（组织）

| iWork 字段 | 本地字段 | 说明 |
| --- | --- | --- |
| `subcompanyname` / `departmentname` | `DeptDO.name` | 自动截断到 30 字符，缺失时使用“未命名*” |
| `subcompanycode` / `departmentcode` | `DeptDO.code` | 空字符串会被置空，保持与 iWork 编码一致 |
| `supsubcomid` / `supdepid` | `DeptDO.parentId` | 优先使用部门父级，退化到分部或根节点 |
| `showorder` | `DeptDO.sort` | 为空则落地 999 |
| `canceled` | `DeptDO.status` | `1/true/yes` 视为禁用（CommonStatusEnum.DISABLE） |

### 字段映射（人员与岗位）

| iWork 字段 | 本地字段 | 处理规则 |
| --- | --- | --- |
| `jobtitleid` | `PostDO.code=IWORK_JOB_${id}` | 若本地不存在则自动建档，`jobtitlename` 作为名称 |
| `lastname` | `AdminUserDO.nickname` | 超过 30 字符会截断 |
| `workcode` → `loginid` | `AdminUserDO.username` | 首选工号，缺失时使用登录账号，均为空则跳过 |
| `departmentid` → `subcompanyid1` | `AdminUserDO.deptIds` | 优先部门，空则落地分部，仍为空则允许为空继续 |
| `jobtitleid` / `jobtitlename` | `AdminUserDO.postIds` | 通过岗位编码命中缓存，否则按名称动态创建 |
| `email` / `mobile` | `AdminUserDO.email/mobile` | 同步时跳过重复验证，兼容历史数据 |
| `password` | `AdminUserDO.password` | 直接使用 iWork 原始密码，避免哈希不一致 |
| `status` | `AdminUserDO.status` | `0` 视为启用，其余状态视为禁用 |
| `sex` | `AdminUserDO.sex` | 自动适配 `0/1/2` 与 `男/女/M/F` 等多种格式 |

### 组织同步禁用策略

| 场景 | 判定条件 | 本地处理 | 备注 |
| --- | --- | --- | --- |
| 分部/部门被禁用 | `canceled` = `1`/`true`/`yes` | 若 `includeCanceled=false`：跳过；否则创建/更新并设置为禁用 | 可通过 `includeCanceled` 控制是否同步 |
| 岗位被禁用 | `canceled` 标记为真 | 同步后 `PostDO.status=DISABLE`，同时记入统计 | |
| 人员离职 | `status` ≠ `0` | 创建/更新为 `DISABLE`，并统计在 `disabled` 数量 | `status` 字段通常为字符串 |
| 非 iWork 数据 | `deptSource`/`userSource` ≠ iWork | 永远跳过修改，保证手工数据安全 | 相关日志输出 `Skipped` 提示 |

### 鉴权与签名

组织接口统一由 `IWorkOrgRestService` 代理，鉴权方式为：

1. 读取 `iwork.org.token-seed`。
2. 运行时生成 `ts = System.currentTimeMillis()`。
3. 计算 `key = MD5(tokenSeed + ts)` 并转大写。
4. 将 `{ key, ts }` 作为 `token` 字段写入请求体，iWork 会基于相同算法校验。

请确保 ZTCloud 所在网络与 iWork 可直接互通，否则需在接口网关上放通上述 URL。

### 组织接口字段详解

#### 通用分页请求字段（`IWorkOrgBaseQueryReqVO`）

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `curpage` | integer | 否 | 当前页码，默认 1 |
| `pagesize` | integer | 否 | 每页条数，默认 20，最大 500 |
| `params` | map | 否 | 额外查询条件，直接透传给 iWork |

#### POST `/system/integration/iwork/hr/subcompany/page`

##### 请求字段（subcompany/page）

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `curpage` | integer | 否 | 继承自通用分页字段 |
| `pagesize` | integer | 否 | 同上 |
| `subcompanyCode` | string | 否 | 按分部编码模糊查询 |
| `subcompanyName` | string | 否 | 按分部名称模糊查询 |

##### 响应字段（`IWorkHrSubcompanyPageRespVO`）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `code` | string | iWork 返回码 |
| `message` | string | 提示信息 |
| `success` | boolean | 是否成功 |
| `totalSize` | integer | 总记录数 |
| `totalPage` | integer | 总页数 |
| `pageSize` | integer | 单页条数 |
| `pageNumber` | integer | 当前页码 |
| `dataList[]` | array | 分部列表 |

`dataList[]` 主要字段：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | integer | 分部 ID（也是本地部门 ID） |
| `subcompanycode` | string | 分部编码 |
| `subcompanyname` | string | 分部名称 |
| `companyid` / `companyname` | integer/string | 总部标识 |
| `supsubcomid` / `supsubcomname` | integer/string | 上级分部信息 |
| `showorder` | integer | 排序号 |
| `description` | string | 描述 |
| `canceled` | string | 是否失效（0/1/true/false） |
| `alllevel` | string | 层级路径 |
| `attributes` | map | iWork 额外字段的承载容器 |

##### JSON 示例（/hr/subcompany/page）

请求：

```http
POST /system/integration/iwork/hr/subcompany/page HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "curpage": 1,
  "pagesize": 50,
  "subcompanyCode": "HZ",
  "subcompanyName": "杭州"
}
```

响应：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": "SUCCESS",
  "message": "OK",
  "success": true,
  "totalSize": 2,
  "totalPage": 1,
  "pageSize": 50,
  "pageNumber": 1,
  "dataList": [
    {
      "id": 1001,
      "subcompanycode": "HZ01",
      "subcompanyname": "杭州总部",
      "supsubcomid": 0,
      "showorder": 1,
      "canceled": "0"
    },
    {
      "id": 1002,
      "subcompanycode": "HZ02",
      "subcompanyname": "杭州制造分部",
      "supsubcomid": 1001,
      "showorder": 2,
      "canceled": "0"
    }
  ]
}
```

失败示例（鉴权失败）：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": "401",
  "message": "key invalid",
  "success": false,
  "totalSize": 0,
  "totalPage": 0,
  "pageSize": 0,
  "pageNumber": 0,
  "dataList": []
}
```

#### POST `/system/integration/iwork/hr/department/page`

##### 请求字段（department/page）

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `curpage` | integer | 否 | 分页参数 |
| `pagesize` | integer | 否 | 分页参数 |
| `departmentCode` | string | 否 | 部门编码 |
| `departmentName` | string | 否 | 部门名称 |
| `subcompanyId` | string | 否 | 所属分部 ID |

##### 响应字段（`IWorkHrDepartmentPageRespVO`）

顶层字段：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `code` | string | iWork 返回码 |
| `message` | string | 提示信息 |
| `success` | boolean | 是否成功 |
| `data.totalSize` | integer | 总记录数 |
| `data.totalPage` | integer | 总页数 |
| `data.pageSize` | integer | 每页条数 |
| `data.page` | integer | 当前页码 |
| `data.dataList[]` | array | 部门列表 |

`data.dataList[]` 关键字段：部门 ID（`id`）、部门编码（`departmentcode`）、部门名称（`departmentname`）、所属分部（`subcompanyid1` / `subcompanyname`）、父部门（`supdepid`）、显示顺序（`showorder`）、失效标记（`canceled`）等，其余原样透传在 `attributes` 中。

##### JSON 示例（/hr/department/page）

请求：

```http
POST /system/integration/iwork/hr/department/page HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "curpage": 1,
  "pagesize": 100,
  "subcompanyId": "1001"
}
```

响应：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": "SUCCESS",
  "message": "OK",
  "success": true,
  "data": {
    "totalSize": 120,
    "totalPage": 2,
    "pageSize": 100,
    "page": 1,
    "dataList": [
      {
        "id": 2001,
        "departmentcode": "R-D",
        "departmentname": "研发一部",
        "subcompanyid1": 1001,
        "supdepid": 0,
        "showorder": 10,
        "canceled": "0"
      }
    ]
  }
}
```

#### POST `/system/integration/iwork/hr/job-title/page`

##### 请求字段（job-title/page）

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `curpage` | integer | 否 | 分页参数 |
| `pagesize` | integer | 否 | 分页参数 |
| `jobTitleCode` | string | 否 | 岗位编码 |
| `jobTitleName` | string | 否 | 岗位名称 |

##### 响应字段（`IWorkHrJobTitlePageRespVO`）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `code` | string | iWork 返回码 |
| `message` | string | 提示信息 |
| `success` | boolean | 是否成功 |
| `totalSize` / `totalPage` / `pageSize` / `pageNumber` | integer | 分页信息 |
| `dataList[]` | array | 岗位集合 |

`dataList[]` 字段包含 `id`、`jobtitlecode`、`jobtitlename`、`jobgroupid`、`jobgroupname`、`jobfunction`、`showorder`、`canceled` 等，可直接映射到本地岗位表。

#### POST `/system/integration/iwork/hr/user/page`

##### 请求字段（user/page）

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `curpage` / `pagesize` | integer | 否 | 分页参数 |
| `workCode` | string | 否 | 人员工号 |
| `lastName` | string | 否 | 姓名模糊匹配 |
| `departmentId` / `subcompanyId` | string | 否 | 组织过滤 |
| `jobTitleId` | string | 否 | 岗位过滤 |
| `status` | string | 否 | 人员状态（0:在职，其余为非在职） |
| `mobile` / `email` | string | 否 | 联系方式过滤 |

##### 响应字段（`IWorkHrUserPageRespVO`）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `code` / `message` / `success` | string/string/boolean | 通用响应字段 |
| `totalSize` / `totalPage` / `pageSize` / `pageNumber` | integer | 分页信息 |
| `dataList[]` | array | 人员数据 |

`dataList[]` 主要字段：`id`、`lastname`、`loginid`、`workcode`、`sex`、`departmentid`、`subcompanyid1`、`jobtitleid`、`mobile`、`email`、`status`、`password`、`hiredate`、`leavedate` 等，以及 `attributes` 中的扩展数据。同步服务会根据这些字段计算用户名、岗位与状态。

##### JSON 示例（/hr/user/page）

请求：

```http
POST /system/integration/iwork/hr/user/page HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "curpage": 1,
  "pagesize": 100,
  "departmentId": "2001",
  "status": "0"
}
```

响应：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": "SUCCESS",
  "message": "OK",
  "success": true,
  "totalSize": 320,
  "totalPage": 4,
  "pageSize": 100,
  "pageNumber": 1,
  "dataList": [
    {
      "id": 30001,
      "lastname": "李四",
      "loginid": "lisi",
      "workcode": "A10086",
      "departmentid": 2001,
      "subcompanyid1": 1001,
      "jobtitleid": 5001,
      "mobile": "13800001111",
      "email": "lisi@example.com",
      "status": "0",
      "password": "4a7d1ed414934d1c4b"
    }
  ]
}
```

#### Full Sync 请求参数（`IWorkFullSyncReqVO`）

| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `startPage` | integer | 1 | 起始页码，对应 iWork `curpage` |
| `pageSize` | integer | 100 | 单页记录数 (1-500) |
| `maxPages` | integer | null | 限制最大处理页数，null 表示遍历至空页 |
| `scopes` | `array<string>` | `["subcompany","department","jobTitle","user"]` | 指定同步范围 |
| `id` | string | null | 仅同步指定 iWork 主键，常用于补偿 |
| `includeCanceled` | boolean | false | true 时会同步并禁用 iWork 标记为失效的数据 |
| `allowUpdate` | boolean | false | true 时允许覆盖已有的 iWork 来源记录 |

> 系统内部始终启用 `createIfMissing=true`，因此全量同步会自动建档缺失的分部/部门/岗位/人员。

#### Full Sync 响应字段（`IWorkFullSyncRespVO`）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `processedPages` | integer | 实际处理的页数总和 |
| `pageSize` | integer | 每批次请求的页大小回显 |
| `subcompanyStat` / `departmentStat` / `jobTitleStat` / `userStat` | `IWorkSyncEntityStatVO` | 各实体的累计统计 |
| `batches[]` | `IWorkSyncBatchStatVO` | 每个分页批次的明细 |

`IWorkSyncEntityStatVO` 字段：`pulled`（拉取条数）、`created`（新建）、`skippedExisting`（跳过）、`disabled`（禁用）、`failed`（失败）。

`IWorkSyncBatchStatVO` 字段：`entityType`（subcompany/department/job_title/user）、`pageNumber`、`pulled`、`created`、`skippedExisting`、`disabled`、`failed`。借助这些字段可以直观看到失败批次并进行定向补偿。

##### JSON 示例（/hr/users/full-sync）

请求：

```http
POST /system/integration/iwork/hr/users/full-sync HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "startPage": 1,
  "pageSize": 200,
  "scopes": ["department", "user"],
  "includeCanceled": false,
  "allowUpdate": true
}
```

响应：

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 0,
  "msg": "success",
  "data": {
    "processedPages": 6,
    "pageSize": 200,
    "departmentStat": {
      "pulled": 600,
      "created": 120,
      "skippedExisting": 470,
      "disabled": 5,
      "failed": 5
    },
    "userStat": {
      "pulled": 1200,
      "created": 320,
      "skippedExisting": 780,
      "disabled": 60,
      "failed": 40
    },
    "batches": [
      {
        "entityType": "DEPARTMENT",
        "pageNumber": 1,
        "pulled": 200,
        "created": 40,
        "skippedExisting": 150,
        "disabled": 2,
        "failed": 8
      }
    ]
  },
  "traceId": "0cc5de912f40"
}
```

失败示例（iWork 远端异常）：

```http
HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
  "code": 200501,
  "msg": "调用 iWork 失败: 拉取人员接口超时",
  "data": {
    "processedPages": 3,
    "pageSize": 200,
    "userStat": {
      "pulled": 600,
      "created": 160,
      "failed": 20
    },
    "batches": [
      {
        "entityType": "USER",
        "pageNumber": 3,
        "pulled": 200,
        "failed": 20
      }
    ]
  },
  "traceId": "ab771c30d4ee"
}
```

## 故障排查与常见问题

### 1. Token 申请失败

- 现象：`IWORK_APPLY_TOKEN_FAILED`，日志提示 “返回缺少 token”。
- 处理：
  1. 确认 `iwork.base-url` 是否指向正式域名；
  2. 核对 appId、client 公钥是否与 iWork 后台一致；
  3. 如需重新注册，将 `/auth/register` 与 `/auth/token` 的 `forceRefreshRegistration=true`、`forceRefreshToken=true` 同时置为 true。

### 2. 组织分页 401 或“key 无效”

- 现象：iWork 返回 `code=401` 或 `success=false`，message 包含 `key invalid`。
- 处理：
  - 校验服务器时间是否与 iWork 相差 < 60s；
  - 确保 `token-seed` 未被误填为空；
  - 通过日志中输出的 cURL（`[iWork-Org] curl`）复现请求，核对 key/ts 是否被代理层篡改。

### 3. 人员同步缺少岗位

- 现象：`BatchResult` 中 `failed` 大量增加，日志提示 “岗位缺少标识”。
- 处理：
  - 确认先执行岗位全量同步；
  - 若 iWork 未维护岗位，可在本地预建岗位并记录在字典，再由 iWork 返回 `jobtitlename` 供系统创建。

### 4. 附件回调 404

- 现象：iWork 回调 `/callback/file` 返回 “业务编码不能为空”。
- 处理：
  - 需要提前在业务流程中调用 `BusinessFileApi` 建立 `businessCode`；
  - 回调只接受 POST JSON，并且必须包含 `fileUrl`、`businessCode`。

### 诊断 checklist

1. 查看 `logs/system/iwork*.log`，定位请求链路与响应体；
2. 通过 `IWorkFullSyncRespVO` 的 `batches` 字段复盘每页统计；
3. 若同步停在某一批次，可使用 `id` + `scopes` 方式做定向补偿；
4. 启用 `includeCanceled=true` 可帮助确认 iWork 的状态标记是否准确。
