diff --git a/doc/中铜技术文档/iWork集成说明.md b/doc/中铜技术文档/iWork集成说明.md new file mode 100644 index 0000000..d705391 --- /dev/null +++ b/doc/中铜技术文档/iWork集成说明.md @@ -0,0 +1,1191 @@ +# 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 的状态标记是否准确。 diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskDataExtendRespVO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskDataExtendRespVO.java index 0958e76..c20e850 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskDataExtendRespVO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskDataExtendRespVO.java @@ -18,6 +18,9 @@ public class BusinessAssayTaskDataExtendRespVO extends BusinessAssayTaskDataResp @Schema(description = "分析方法名称") private String configAssayMethodName; + @Schema(description = "检测方法配置名称及类别") + private String configAssayMethodNameAndCategory; + @Schema(description = "子样配置id") private Long configSubSampleId; diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskDataGroupRespVO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskDataGroupRespVO.java index 98186e0..dd06af3 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskDataGroupRespVO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskDataGroupRespVO.java @@ -5,9 +5,14 @@ import lombok.Data; @Data public class BusinessAssayTaskDataGroupRespVO { + /** 方法id **/ private Long configAssayMethodId; + /** 方法名称 **/ private String configAssayMethodName; + + /** 方法名称及类别 **/ + private String configAssayMethodNameAndCategory; private String assayType; diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskDataReqVO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskDataReqVO.java index e503688..6438367 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskDataReqVO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskDataReqVO.java @@ -107,6 +107,9 @@ public class BusinessAssayTaskDataReqVO { @Schema(description = "检测方法配置名称") private String configAssayMethodName; + + @Schema(description = "检测方法配置名称及类别") + private String configAssayMethodNameAndCategory; @Schema(description = "收样时间") @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskExtendRespVO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskExtendRespVO.java index 7ed693e..00dd818 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskExtendRespVO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessAssayTaskExtendRespVO.java @@ -4,6 +4,7 @@ import java.util.List; import com.zt.plat.module.qms.business.config.controller.vo.ConfigQCSampleMethodExtendRespVO; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; /** @@ -24,6 +25,9 @@ public class BusinessAssayTaskExtendRespVO extends BusinessAssayTaskRespVO { /** 分析方法名称 **/ private String configAssayMethodName; + @Schema(description = "检测方法配置名称及类别") + private String configAssayMethodNameAndCategory; + /** 分析方法对应的分析项目 **/ private String configAssayMethodProjectShowNames; diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessSubParentSampleAssessmentGroupReqVO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessSubParentSampleAssessmentGroupReqVO.java index 85cf363..1e92715 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessSubParentSampleAssessmentGroupReqVO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessSubParentSampleAssessmentGroupReqVO.java @@ -17,6 +17,9 @@ public class BusinessSubParentSampleAssessmentGroupReqVO { @Schema(description = "分析方法名称") private String configAssayMethodName; + + @Schema(description = "检测方法配置名称及类别") + private String configAssayMethodNameAndCategory; @Schema(description = "分析部门ID") private Long assayDepartmentId; diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessSubParentSampleAssessmentGroupRespVO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessSubParentSampleAssessmentGroupRespVO.java index f4f69cf..edccf6c 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessSubParentSampleAssessmentGroupRespVO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/BusinessSubParentSampleAssessmentGroupRespVO.java @@ -11,6 +11,9 @@ public class BusinessSubParentSampleAssessmentGroupRespVO { @Schema(description = "分析方法名称") private String configAssayMethodName; + + @Schema(description = "检测方法配置名称及类别") + private String configAssayMethodNameAndCategory; @Schema(description = "分析部门ID") private Long assayDepartmentId; diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/RecheckSubSampleParentMethodRespVO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/RecheckSubSampleParentMethodRespVO.java index afe6019..878b7d5 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/RecheckSubSampleParentMethodRespVO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/controller/vo/RecheckSubSampleParentMethodRespVO.java @@ -4,6 +4,7 @@ import java.util.List; import com.zt.plat.module.qms.business.config.controller.vo.*; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data @@ -20,6 +21,9 @@ public class RecheckSubSampleParentMethodRespVO { private Long configAssayMethodId; private String configAssayMethodName; + + @Schema(description = "检测方法配置名称及类别") + private String configAssayMethodNameAndCategory; private Long baseSampleId; diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/dal/mapper/BusinessAssayTaskDataMapper.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/dal/mapper/BusinessAssayTaskDataMapper.java index b01848b..e54e0fd 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/dal/mapper/BusinessAssayTaskDataMapper.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/dal/mapper/BusinessAssayTaskDataMapper.java @@ -2,7 +2,6 @@ package com.zt.plat.module.qms.business.bus.dal.mapper; import java.util.*; -import com.alibaba.druid.sql.ast.statement.SQLForeignKeyImpl.On; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.zt.plat.framework.common.pojo.PageResult; @@ -16,7 +15,6 @@ import com.zt.plat.module.qms.business.bus.controller.vo.BusinessAssayTaskDataRe import com.zt.plat.module.qms.business.bus.dal.dataobject.BusinessAssayTaskDataDO; import com.zt.plat.module.qms.business.bus.dal.dataobject.BusinessSubSampleAnalysisGroupDO; import com.zt.plat.module.qms.business.bus.dal.dataobject.BusinessSubSampleAssessmentDO; -import com.zt.plat.module.qms.business.bus.dal.dataobject.BusinessSubSampleAssessmentProjectDO; import com.zt.plat.module.qms.business.bus.dal.dataobject.BusinessSubSampleDO; import com.zt.plat.module.qms.business.config.dal.dataobject.ConfigAssayMethodDO; import com.zt.plat.module.qms.enums.QmsCommonConstant; @@ -26,7 +24,6 @@ import cn.hutool.core.util.ObjectUtil; import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; -import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -47,6 +44,7 @@ public interface BusinessAssayTaskDataMapper extends BaseMapperX businessSubParentSampleAssessmentList = businessSubParentSampleAssessmentExtendList.stream().filter(f -> f.getBusinessSubParentSampleId().equals(businessSubParentSampleDO.getId())).collect(Collectors.toList()); for (BusinessSubParentSampleAssessmentProjectExtendRespVO businessSubParentSampleAssessment : businessSubParentSampleAssessmentList) { Long configAssayMethodProjectId = businessSubParentSampleAssessment.getConfigAssayMethodProjectId(); - List configProjectFeildList = configProjectList.stream().filter(f -> f.getConfigAssayMethodProjectId().equals(configAssayMethodProjectId)).collect(Collectors.toList()); + List configProjectFeildList = configProjectList.stream().filter(f -> f.getConfigAssayMethodProjectId() != null && f.getConfigAssayMethodProjectId().equals(configAssayMethodProjectId)).collect(Collectors.toList()); for (ConfigProjectExtendRespVO configProjectFeild : configProjectFeildList) { //查询动态报表字段 ConfigReportFieldDO configReportField = configReportFieldDynamicList.stream().filter(f -> f.getConfigReportTypeId().equals(configSampleReport.getConfigReportTypeId()) && f.getField().equals(configProjectFeild.getSaveColumn())).findFirst().orElse(null); @@ -583,7 +583,9 @@ public class SampleResultReportingServiceImpl implements SampleResultReportingSe List currentBusinessSubParentSampleDOList = businessSubParentSampleMapper.selectByBusinessBaseSampleIds(currentBusinessBaseSampleIdList); List currentBusinessSubParentSampleIdList = currentBusinessSubParentSampleDOList.stream().map(m -> m.getId()).collect(Collectors.toList()); List currentBusinessSubParentSampleAssessmentDOList = businessSubParentSampleAssessmentMapper.selectByBusinessSubParentSampleIds(currentBusinessSubParentSampleIdList); - long count = currentBusinessSubParentSampleAssessmentDOList.stream().filter(f -> !reqVO.getBusinessSubParentSampleIds().contains(f.getBusinessSubParentSampleId()) && !f.getConfigAssayMethodId().equals(reqVO.getConfigAssayMethodId()) && f.getIsReported().equals(QmsCommonConstant.NO)).count(); + + //排除不在reqVO.getBusinessSubParentSampleIds()并且方法reqVO.getConfigAssayMethodId()的数据,其他数据校验是否已上报 + long count = currentBusinessSubParentSampleAssessmentDOList.stream().filter(f -> !(reqVO.getBusinessSubParentSampleIds().contains(f.getBusinessSubParentSampleId()) && f.getConfigAssayMethodId().equals(reqVO.getConfigAssayMethodId())) && f.getIsReported().equals(QmsCommonConstant.NO)).count(); if (count > 0) {//如果还存在未上报的数据,则继续 continue; } diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/service/SampleTaskAssignServiceImpl.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/service/SampleTaskAssignServiceImpl.java index b391d76..790aab8 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/service/SampleTaskAssignServiceImpl.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/bus/service/SampleTaskAssignServiceImpl.java @@ -214,6 +214,7 @@ public class SampleTaskAssignServiceImpl implements SampleTaskAssignService { jsonObject = new JSONObject(); jsonObject.put("configAssayMethodId", configSubSampleMethod.getConfigAssayMethodId()); jsonObject.put("configAssayMethodName", configSubSampleMethod.getConfigAssayMethodName()); + jsonObject.put("configAssayMethodNameAndCategory", configSubSampleMethod.getConfigAssayMethodNameAndCategory()); jsonObject.put("configAssayMethodCode", configSubSampleMethod.getConfigAssayMethodCode()); StringBuilder assayProjectBuilder = new StringBuilder(); diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodPageReqVO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodPageReqVO.java index 662d05e..7185cd0 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodPageReqVO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodPageReqVO.java @@ -24,6 +24,9 @@ public class ConfigAssayMethodPageReqVO extends PageParam { @Schema(description = "方法名称", example = "李四") private String name; + + @Schema(description = "方法名称及类别") + private String methodNameCategory; @Schema(description = "方法编号") private String methodCode; diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodReqVO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodReqVO.java index da30560..867a515 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodReqVO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodReqVO.java @@ -25,6 +25,9 @@ public class ConfigAssayMethodReqVO { @Schema(description = "方法名称", example = "李四") private String name; + @Schema(description = "方法名称及类别") + private String methodNameCategory; + @Schema(description = "方法编号") private String methodCode; diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodRespVO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodRespVO.java index 08bfc94..25188a9 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodRespVO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodRespVO.java @@ -30,6 +30,9 @@ public class ConfigAssayMethodRespVO { @ExcelProperty("方法名称") private String name; + @Schema(description = "方法名称及类别") + private String methodNameCategory; + @Schema(description = "方法编号") @ExcelProperty("方法编号") private String methodCode; diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodSaveReqVO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodSaveReqVO.java index 91fff24..b9dac4a 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodSaveReqVO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigAssayMethodSaveReqVO.java @@ -24,6 +24,9 @@ public class ConfigAssayMethodSaveReqVO { @Schema(description = "方法名称", example = "李四") private String name; + @Schema(description = "方法名称及类别") + private String methodNameCategory; + @Schema(description = "方法编号") private String methodCode; diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigSubSampleMethodExtendRespVO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigSubSampleMethodExtendRespVO.java index 9cce8d3..c6a76e0 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigSubSampleMethodExtendRespVO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/controller/vo/ConfigSubSampleMethodExtendRespVO.java @@ -10,6 +10,9 @@ public class ConfigSubSampleMethodExtendRespVO extends ConfigSubSampleMethodResp @Schema(description = "分析方法名称") private String configAssayMethodName; + @Schema(description = "检测方法配置名称及类别") + private String configAssayMethodNameAndCategory; + /** 分析方法编号 **/ @Schema(description = "分析方法编号") private String configAssayMethodCode; diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/dal/dataobject/ConfigAssayMethodDO.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/dal/dataobject/ConfigAssayMethodDO.java index 1d49e38..296c205 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/dal/dataobject/ConfigAssayMethodDO.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/dal/dataobject/ConfigAssayMethodDO.java @@ -57,6 +57,11 @@ public class ConfigAssayMethodDO extends BusinessBaseDO { @TableField("MTHD_CD") private String methodCode; /** + * 方法名称及类别 + */ + @TableField("MTHD_NAME_CTGR") + private String methodNameCategory; + /** * 描述 */ @TableField("DSP") diff --git a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/dal/mapper/ConfigAssayMethodMapper.java b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/dal/mapper/ConfigAssayMethodMapper.java index 2b0dbeb..85ce27a 100644 --- a/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/dal/mapper/ConfigAssayMethodMapper.java +++ b/zt-module-qms/zt-module-qms-server/src/main/java/com/zt/plat/module/qms/business/config/dal/mapper/ConfigAssayMethodMapper.java @@ -27,6 +27,7 @@ public interface ConfigAssayMethodMapper extends BaseMapperX AND tcam.NAME like '%' || #{reqVO.configAssayMethodName} || '%' + + + AND tcam.MTHD_NAME_CTGR like '%' || #{reqVO.configAssayMethodNameAndCategory} || '%' GROUP BY tbatd.CFG_ASY_MTHD_ID, - tcam.NAME + tcam.NAME, + tcam.MTHD_NAME_CTGR @@ -131,6 +138,7 @@ tdp.SHW_NAME AS dictionaryProjectShowName, tmasm.CFG_ASY_MTHD_ID AS configAssayMethodId, tcam.NAME AS configAssayMethodName, + tcam.MTHD_NAME_CTGR AS configAssayMethodNameAndCategory, tmas.BSE_SMP_ID AS baseSampleId, tbs.NAME AS baseSampleName, tmasm.IS_RCHK_DFT AS isRecheckDefault