# 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 " ^ -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 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` 结构,通用包体为 `{ "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 | 否 | 会被追加到 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` | `["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 的状态标记是否准确。