diff --git a/.gitignore b/.gitignore index e55eb64b..3d91d080 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ package-lock.json # visual studio code .history *.log +logs/** functions/mock .temp/** diff --git a/deployment-databus.yaml b/deployment-databus.yaml index fc975572..62b5d9bc 100644 --- a/deployment-databus.yaml +++ b/deployment-databus.yaml @@ -1,7 +1,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-module-databus labels: app: zt-module-databus @@ -19,7 +19,7 @@ spec: labels: app: zt-module-databus spec: - dnsPolicy: None + dnsPolicy: ClusterFirst dnsConfig: nameservers: - "172.16.36.16" diff --git a/deployment-gateway.yaml b/deployment-gateway.yaml index 2d9669ec..007201ea 100644 --- a/deployment-gateway.yaml +++ b/deployment-gateway.yaml @@ -1,7 +1,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-gateway labels: app: zt-gateway @@ -61,7 +61,7 @@ spec: apiVersion: v1 kind: Service metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-gateway spec: type: NodePort diff --git a/deployment-infra.yaml b/deployment-infra.yaml index a38e4320..dcfba5de 100644 --- a/deployment-infra.yaml +++ b/deployment-infra.yaml @@ -1,7 +1,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-module-infra labels: app: zt-module-infra @@ -19,7 +19,7 @@ spec: labels: app: zt-module-infra spec: - dnsPolicy: None + dnsPolicy: ClusterFirst dnsConfig: nameservers: - "172.16.36.16" diff --git a/deployment-system.yaml b/deployment-system.yaml index f1b07d3b..2be3a5da 100644 --- a/deployment-system.yaml +++ b/deployment-system.yaml @@ -1,7 +1,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-module-system labels: app: zt-module-system @@ -28,7 +28,7 @@ spec: operator: In values: - node-3 - dnsPolicy: None + dnsPolicy: ClusterFirst dnsConfig: nameservers: - "172.16.36.16" diff --git a/deployment.yaml b/deployment.yaml index 4fc70456..a2d24372 100644 --- a/deployment.yaml +++ b/deployment.yaml @@ -2,7 +2,7 @@ #apiVersion: apps/v1 #kind: Deployment #metadata: -# namespace: ns-d6a0e78ebd674c279614498e4c57b133 +# namespace: ns-f16a3067ca7b434aad127d15eac82503 # name: zt-gateway # labels: # app: zt-gateway @@ -13,6 +13,8 @@ # app: zt-gateway # template: # metadata: +# annotations: +# proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}' # labels: # app: zt-gateway # spec: @@ -51,7 +53,7 @@ apiVersion: v1 kind: Service metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-gateway spec: type: NodePort @@ -67,7 +69,7 @@ spec: apiVersion: apps/v1 kind: Deployment metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-module-infra labels: app: zt-module-infra @@ -82,10 +84,12 @@ spec: app: zt-module-infra template: metadata: + annotations: + proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}' labels: app: zt-module-infra spec: - dnsPolicy: None + dnsPolicy: ClusterFirst dnsConfig: nameservers: - "172.16.36.16" @@ -128,7 +132,7 @@ spec: apiVersion: v1 kind: Service metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-module-infra spec: type: NodePort @@ -144,7 +148,7 @@ spec: apiVersion: apps/v1 kind: Deployment metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-module-system labels: app: zt-module-system @@ -161,6 +165,8 @@ spec: metadata: labels: app: zt-module-system + annotations: + proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}' spec: affinity: nodeAffinity: @@ -171,7 +177,7 @@ spec: operator: In values: - node-3 - dnsPolicy: None + dnsPolicy: ClusterFirst dnsConfig: nameservers: - "172.16.36.16" @@ -214,7 +220,7 @@ spec: apiVersion: v1 kind: Service metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-module-system spec: type: NodePort @@ -230,7 +236,7 @@ spec: #apiVersion: apps/v1 #kind: Deployment #metadata: -# namespace: ns-d6a0e78ebd674c279614498e4c57b133 +# namespace: ns-f16a3067ca7b434aad127d15eac82503 # name: zt-module-bpm # labels: # app: zt-module-bpm @@ -245,6 +251,8 @@ spec: # app: zt-module-bpm # template: # metadata: +# annotations: +# proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}' # labels: # app: zt-module-bpm # spec: @@ -286,7 +294,7 @@ spec: #apiVersion: v1 #kind: Service #metadata: -# namespace: ns-d6a0e78ebd674c279614498e4c57b133 +# namespace: ns-f16a3067ca7b434aad127d15eac82503 # name: zt-module-bpm #spec: # type: NodePort @@ -302,7 +310,7 @@ spec: #apiVersion: apps/v1 #kind: Deployment #metadata: -# namespace: ns-d6a0e78ebd674c279614498e4c57b133 +# namespace: ns-f16a3067ca7b434aad127d15eac82503 # name: zt-module-report # labels: # app: zt-module-report @@ -358,7 +366,7 @@ spec: #apiVersion: v1 #kind: Service #metadata: -# namespace: ns-d6a0e78ebd674c279614498e4c57b133 +# namespace: ns-f16a3067ca7b434aad127d15eac82503 # name: zt-module-report #spec: # type: NodePort @@ -374,7 +382,7 @@ spec: apiVersion: apps/v1 kind: Deployment metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-module-databus labels: app: zt-module-databus @@ -391,6 +399,8 @@ spec: metadata: labels: app: zt-module-databus + annotations: + proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}' spec: affinity: nodeAffinity: @@ -401,7 +411,7 @@ spec: operator: In values: - node-3 - dnsPolicy: None + dnsPolicy: ClusterFirst dnsConfig: nameservers: - "172.16.36.16" @@ -444,7 +454,7 @@ spec: apiVersion: v1 kind: Service metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-module-databus spec: type: NodePort @@ -462,7 +472,7 @@ spec: apiVersion: apps/v1 kind: Deployment metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-module-template labels: app: zt-module-template @@ -477,10 +487,12 @@ spec: app: zt-module-template template: metadata: + annotations: + proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}' labels: app: zt-module-template spec: - dnsPolicy: None + dnsPolicy: ClusterFirst dnsConfig: nameservers: - "172.16.36.16" @@ -523,7 +535,7 @@ spec: apiVersion: v1 kind: Service metadata: - namespace: ns-d6a0e78ebd674c279614498e4c57b133 + namespace: ns-f16a3067ca7b434aad127d15eac82503 name: zt-module-template spec: type: NodePort diff --git a/docs/databus-client使用说明.md b/docs/databus-client使用说明.md new file mode 100644 index 00000000..c6eb7b1e --- /dev/null +++ b/docs/databus-client使用说明.md @@ -0,0 +1,31 @@ +# Databus Client 使用说明 + +databus client 最主要用于调用基于http协议的第三方接口时需要记录调用日志到 databus 的情况, 通过databus client 调用第三方接口会将调用日志记录到databus的访问日志中 + +# 使用方法 +1. 添加依赖: +```xml + + com.zt.plat + zt-module-databus-client + 3.0.47-SNAPSHOT + +``` +2. 注入 DatabusClient +```java +@Resource +private DatabusClient databusClient; +``` +3. 方法说明 + * get(...) : 发送 get 请求 + * post(...): 发送 post 请求 + * put(...): 发送 put 请求 + * delete(...): 发送 delete 请求 + * doRequest(...): 发送自定义请求 +4. 方法参数说明(由于所有方法参数都是一样的,所以在此统一说明) + * String urlString: 请求的 http 接口地址(get/delete请求不需要带url参数) + * Map data: 请求的参数(post/put方法会转换为json提交, get/delete会拼接到url上) + * Map headers: 请求头信息 + * String appId: databus 的appid + * String authToken: databus 的访问令牌 + * Method method: doRequest 方法独有,如果要使用 get/post/put/delete 之外的方法,请使用doRequest方法并通过method参数指定 diff --git a/docs/iWork集成说明.md b/docs/iWork集成说明.md index 2cb03e59..86b606f8 100644 --- a/docs/iWork集成说明.md +++ b/docs/iWork集成说明.md @@ -1,6 +1,6 @@ # iWork 统一集成使用说明 -本文档介绍如何在 System 模块中使用项目已实现的统一 iWork 流程发起能力(controller + service + properties)。内容包含:配置项、调用方式(内部 Java 调用 & 外部 HTTP 调用)、请求/响应示例、错误处理、缓存与 Token 生命周期、典型问题与排查步骤。 +本文档介绍如何在 System 模块中使用项目已实现的统一 iWork 流程发起能力(controller + service + properties)。内容包含:配置项、调用方式(内部 Java 调用 & 外部 HTTP 调用)、请求/响应示例、错误处理、缓存与 Token 生命周期、业务回调分发与重试、流程日志查询,以及典型问题与排查步骤。 --- @@ -12,7 +12,10 @@ - 提供 Service 层 `IWorkIntegrationService`,供其它模块以 Spring Bean 注入方式直接调用。 - 使用 `IWorkProperties` 绑定 `application.yml` 中 `iwork` 的配置项。 - Token / 会话采用本地 Caffeine 缓存缓存(按 appId + operatorUserId 缓存 session),并在到期前按配置提前刷新。 -- 使用统一配置的 appId、公钥以及默认流程编号,无需再维护多套凭证。 +- 使用统一配置的 appId、公钥以及默认流程编号,无需再维护多套凭证,所有调用强制使用配置的 appId,不再接受请求覆盖。 +- 全链路以 requestId 作为唯一业务标识(发起返回、作废入参、日志查询、回调与重试均基于 requestId),workflowId 仅用于指定 iWork 模板。 +- 支持业务回调标识 bizCallbackKey(字符串,≤255),发起时提交,回调时按标识分发业务回调并带自动重试与手工重试入口。 +- 回调与日志记录会保存截断后的原始回调请求/响应文本(无需脱敏),无业务附件或处理异常时标记失败。 --- @@ -47,6 +50,10 @@ iwork: token: ttl-seconds: 3600 # token 有效期(秒) refresh-ahead-seconds: 60 # 在到期前多少秒认为需要刷新 + callback: + retry: + max-attempts: 3 # 业务回调自动重试次数(默认 3 次,可调整) + delay-seconds: 5 # 业务回调自动重试间隔秒数(默认 5 秒,可调整) client: connect-timeout: 5s response-timeout: 30s @@ -55,10 +62,11 @@ iwork: 说明: - `base-url` 为 iWork 网关的基础地址,不能留空。 -- `app-id` 与 `client-public-key` 共同构成注册/申请 token 所需的凭据信息,由配置统一提供,不再支持多套切换。 -- `workflow-id` 提供全局默认流程编号,单次调用也可通过 `workflowId` 覆盖。 +- `app-id` 与 `client-public-key` 共同构成注册/申请 token 所需的凭据信息,由配置统一提供,不再支持多套切换,系统强制使用配置的 appId,忽略请求中的 appId。 +- `workflow-id` 提供全局默认流程编号,仅用于向 iWork 指定流程模板;流程标识、查询、补偿、回调与重试一律使用 requestId,不再以 workflowId 标识业务。 - 请求头键名固定为 `app-id`、`client-public-key`、`secret`、`token`、`time`、`user-id`,无需在配置中重复声明。 - `org.*` 配置负责 iWork 人力组织 REST 代理:`token-seed` 为与 iWork 约定的标识,系统会自动将其与毫秒时间戳拼接并计算 MD5 生成 `key`,无需额外传递 token。 +- `callback.retry.*` 控制业务回调的自动重试次数与间隔,默认为 3 次、5 秒,可按需调整。 --- @@ -102,7 +110,7 @@ Controller 暴露的 REST 接口: ## 请求 VO 说明(重要字段) - IWorkBaseReqVO(公用字段) - - `appId` (String):为兼容历史接口保留,系统始终使用配置项 `iwork.app-id`。 + - `appId` (String):仅保留字段,系统强制使用配置项 `iwork.app-id`,忽略请求值。 - `operatorUserId` (String):在 iWork 内部代表操作人的用户编号(可为空,框架会使用 `properties.userId`)。 - `forceRefreshToken` (Boolean):是否强制刷新 token(例如遇到 token 错误时强制刷新)。 @@ -117,7 +125,7 @@ Controller 暴露的 REST 接口: - `success` / `message`:调用成功标志与提示信息。 - IWorkWorkflowCreateReqVO(统一用印流程发起) - - `workflowId` (String):流程模板 ID,必填,不再回退到配置。 + - `workflowId` (String):流程模板 ID,必填,通常由配置 `iwork.workflow-id` 提供;仅用于向 iWork 指定模板,不再作为业务标识。 - `jbr`:用印申请人(iWork 人员 ID,必填)。 - `yybm`:用印部门 ID(必填)。 - `fb`:用印单位/分部 ID(必填)。 @@ -128,10 +136,11 @@ Controller 暴露的 REST 接口: - `xyywjUrl`:用印材料附件 URL(必填)。 - `yysx`:用印事项(必填)。 - `ywxtdjbh`:业务系统单据编号(必填,同时用于生成流程标题“用印-{ywxtdjbh}”)。 + - `bizCallbackKey` (String):业务回调标识,≤255 字符,回调时按该标识分发到对应业务回调入口(可选但推荐)。 - 额外字段不再支持,Service 会根据以上字段自动补齐固定流程类型 (`lclx=2979600781334966993`) 与签署动作 (`qsdz=CORPORATE`)。 - IWorkWorkflowVoidReqVO(作废) - - `requestId` (String):流程请求编号(必填)。 + - `requestId` (String):流程请求编号(必填,唯一标识)。 - `reason`、`extraParams`、`formExtras` 等用于传递作废原因或额外字段。 - IWorkFormFieldVO(表单字段) @@ -188,8 +197,9 @@ public class MyService { 说明: -- 若需使用特定凭证,可设置 `req.setAppId("my-iwork-app")`。 -- 若需覆盖默认流程模板,可调用 `req.setWorkflowId(123L)` 指定。 +- 无需设置 appId,系统强制使用配置项 `iwork.app-id`。 +- `workflowId` 通常来自配置 `iwork.workflow-id`,也可在请求中指定模板编号,但流程标识一律以返回的 requestId 为准。 +- 可设置 `bizCallbackKey` 以便回调时分发到对应业务处理;建议在发起后立即记录响应中的 requestId 及 bizCallbackKey。 - 若希望以特定 iWork 操作人发起,可设置 `req.setOperatorUserId("1001")`。 --- @@ -202,7 +212,6 @@ public class MyService { curl -X POST \ -H "Content-Type: application/json" \ -d '{ - "appId":"my-iwork-app", "identifierKey":"loginid", "identifierValue":"zhangsan" }' \ @@ -238,19 +247,20 @@ curl -X POST -H "Content-Type: application/json" -d '{ "yysy":"与客户合同用印", "xyywjUrl":"https://files.example.com/contract.pdf", "yysx":"合同用印", - "ywxtdjbh":"DJ-2025-0001" + "ywxtdjbh":"DJ-2025-0001", + "bizCallbackKey":"seal-flow-callback" }' https://your-zt-server/admin-api/system/integration/iwork/workflow/create ``` > 说明:外部仍以 JSON 请求调用本服务,系统在向 iWork 转发时会自动将负载转换为 `application/x-www-form-urlencoded` 表单(含 `requestName`、`workflowId`、`mainData` 等字段)。 +> 响应的 `data.requestId` 为唯一业务标识,请在业务侧保存;appId 始终取配置;`xyywjFileName` 不存储。 1. Void workflow ```bash curl -X POST -H "Content-Type: application/json" -d '{ "requestId":"REQ-001", - "reason":"作废原因", - "appId":"my-iwork-app" + "reason":"作废原因" }' https://your-zt-server/admin-api/system/integration/iwork/workflow/void ``` @@ -259,34 +269,98 @@ curl -X POST -H "Content-Type: application/json" -d '{ ## 核心逻辑与细节 1. 基础参数解析 +appId:始终取配置 `iwork.app-id`,忽略请求值。 +主标识:全链路仅使用 `requestId`(发起返回、作废入参、日志查询、回调分发、重试都基于 requestId)。 +模板:`workflowId` 仅用于选择 iWork 模板,不作为业务标识。 +业务回调标识:`bizCallbackKey`(字符串,≤255),发起时提交,回调按该标识分发到具体业务回调入口。 +附件文件名:`xyywjFileName` 不存储也不参与处理;仅使用 `xyywjUrl`。 - 系统始终使用 `application.yml` 中配置的 `app-id` 与 `client-public-key` 与 iWork 通信。 - 请求体中的 `appId` 字段仅为兼容历史调用而保留,框架内部不会使用该值做切换。 - -1. Workflow 模板解析 - - 调用时优先使用请求体中的 `workflowId`。 - 若未显式传入,则回退到全局 `iwork.workflow-id`,若仍为空则抛出 `IWORK_WORKFLOW_ID_MISSING`。 - -1. 注册 + RSA + Token - - - 在首次或 token 过期时,会按以下步骤获取 session: + 调用时可在请求体中指定 `workflowId` 以选择模板;若未显式传入,则回退到全局 `iwork.workflow-id`,若仍为空则抛出 `IWORK_WORKFLOW_ID_MISSING`。 +POST `/system/integration/iwork/workflow/create`:发起用印流程,返回 `requestId`。 +POST `/system/integration/iwork/workflow/void`:作废流程,请求需携带 `requestId`。 +POST `/system/integration/iwork/callback/file`(上游回调):接收 iWork 回调,校验 `requestId`,若缺 `bizCallbackKey` 则记录失败但不中断附件保存;下载并落库附件,记录回调日志(截断原文),并在提供 `bizCallbackKey` 时通过 RocketMQ 分发业务回调(自动/手工重试)。 +POST `/system/integration/iwork/log/page`:分页查询用印回调日志(requestId / 业务单号 / bizCallbackKey / 状态 / 时间段)。 +POST `/system/integration/iwork/log/retry`:手工重试业务回调,需权限,入参 `requestId`(状态为失败/超重试时可用)。 1. 向 iWork 的 `register` 接口发起请求(Headers 包含 appId 与 clientPublicKey)。 2. 从注册响应中获取 `secret` 与 `spk`(服务端公钥),使用本地的 client 公钥做 RSA 加密(`spk` 用于加密),得到加密后的 secret 与 encryptedUserId。 - 3. 使用注册返回的密钥申请 token(apply-token),token 会被按 `ttl-seconds` 缓存。 - - - `IWorkIntegrationServiceImpl` 中维护一个 Caffeine `sessionCache`,缓存 key 为 `appId::operatorUserId`。 +发起:`workflowId`、`jbr`、`yybm`、`fb`、`sqsj`、`yyqx`、`xyywjUrl`、`yysx`、`ywxtdjbh` 必填;`bizCallbackKey` 可选但建议提供;appId 忽略。 +作废:`requestId` 必填;`reason` 可选。 +回调:若无法找到业务附件(或不存在),标记用印失败并记日志,不抛出未捕获异常;记录原始回调文本(截断)。 - 当 token 接近到期(`refresh-ahead-seconds`)时会在下一次请求触发刷新。 -1. 请求构造 - - - 用户解析、作废等场景继续以 `application/json` 调用 iWork。 - - 用印流程发起在转发至 iWork 时改为 `application/x-www-form-urlencoded` 表单,请求正文包含 `requestName`、`workflowId` 及字符串化后的 `mainData`,与 iWork 网关当前要求保持一致。 - - 认证 Header:由 `IWorkProperties.Headers` 中的常量控制,固定键名为 `app-id`、`client-public-key`、`secret`、`token`、`time`、`user-id`。 +1) 发起:校验必填 → 使用配置 appId 与模板 workflowId → 申请/缓存 token → 以 form-urlencoded 调 iWork → 返回 `requestId`,记录发起日志状态。 +2) 回调(/callback/file):校验 requestId + bizCallbackKey → 校验租户/必备字段 → 下载/保存附件(若缺业务附件则直接标记失败并记日志)→ 写入回调日志,保存原始回调请求/响应(截断)→ 通过 RocketMQ 发送业务回调消息(topic=`SYSTEM_IWORK_BIZ_CALLBACK`,tag=`bizCallbackKey`)。 +3) 业务处理 & 结果上报(跨模块/跨进程):业务模块订阅 `SYSTEM_IWORK_BIZ_CALLBACK` 对应 tag 处理后,将结果发布到 `SYSTEM_IWORK_BIZ_CALLBACK_RESULT`(tag 同 bizCallbackKey,携带 requestId / bizCallbackKey / success / errorMessage / payload / attempt / maxAttempts),system 模块消费结果并更新日志,失败且未超限时由 system 端按配置延迟重试再次投递回调消息。 +4) 重试:默认 3 次、间隔 5 秒,可配置(`iwork.callback.retry.*`),手工重试仍使用 `/log/retry`,由 system 模块重新投递 MQ 消息。 +5) 手工重试:需具备 iWork 模块权限;根据 requestId 将任务重新投递至回调分发器,不增加自动重试计数,可在日志详情中发起。 1. 响应解析 +`CREATE_PENDING` / `CREATE_SUCCESS` / `CREATE_FAILED` +`CALLBACK_PENDING` / `CALLBACK_SUCCESS` / `CALLBACK_FAILED` +`CALLBACK_RETRYING` / `CALLBACK_RETRY_FAILED` - - 实现里对响应成功的判定比较宽松:检查 `code`、`status`、`success`、`errno` 等字段(支持布尔、字符串 ‘0’/‘1’/‘success’)以判断是否成功,并解析常见的 message 字段 `msg|message|errmsg`。 +--- + +## 用印流程日志与业务回调 + +### 范围与标识 + +- appId:始终取配置 `iwork.app-id`,忽略请求值。 +- 主标识:全链路仅使用 `requestId`(发起返回、作废入参、日志查询、回调分发、重试都基于 requestId)。 +- 模板:`workflowId` 仅用于选择 iWork 模板,不作为业务标识。 +- 业务回调标识:`bizCallbackKey`(字符串,≤255),发起时提交,回调按该标识分发到具体业务回调入口。 +- 附件文件名:`xyywjFileName` 不存储也不参与处理;仅使用 `xyywjUrl`。 + +### 入口(REST) + +- POST `/system/integration/iwork/workflow/create`:发起用印流程,返回 `requestId`。 +- POST `/system/integration/iwork/workflow/void`:作废流程,请求需携带 `requestId`。 +- POST `/system/integration/iwork/callback/file`(上游回调):接收 iWork 回调,校验 `requestId`,若缺 `bizCallbackKey` 则记录失败但不中断附件保存;下载并落库附件,记录回调日志(截断原文),并在提供 `bizCallbackKey` 时通过 RocketMQ 分发业务回调(自动/手工重试)。 +- POST `/system/integration/iwork/log/page`:分页查询用印回调日志(requestId / 业务单号 / bizCallbackKey / 状态 / 时间段)。 +- POST `/system/integration/iwork/log/retry`:手工重试业务回调,需权限,入参 `requestId`(状态为失败/超重试时可用)。 + +### 必填/校验要点 + +- 发起:`workflowId`、`jbr`、`yybm`、`fb`、`sqsj`、`yyqx`、`xyywjUrl`、`yysx`、`ywxtdjbh` 必填;`bizCallbackKey` 可选但建议提供;appId 忽略。 +- 作废:`requestId` 必填;`reason` 可选。 +- 回调:若无法找到业务附件(或不存在),标记用印失败并记日志,不抛出未捕获异常;记录原始回调文本(截断)。 + +### 处理流程(摘要) + +1) 发起:校验必填 → 使用配置 appId 与模板 workflowId → 申请/缓存 token → 以 form-urlencoded 调 iWork → 返回 `requestId`,记录发起日志状态。 +2) 回调(/callback/file):校验 requestId + bizCallbackKey → 校验租户/必备字段 → 下载/保存附件(若缺业务附件则直接标记失败并记日志)→ 写入回调日志,保存原始回调请求/响应(截断)→ 通过 RocketMQ 发送业务回调消息(topic=`SYSTEM_IWORK_BIZ_CALLBACK`,tag=`bizCallbackKey`)。 +3) 业务处理 & 结果上报(跨模块/跨进程):业务模块订阅 `SYSTEM_IWORK_BIZ_CALLBACK` 对应 tag 处理后,将结果发布到 `SYSTEM_IWORK_BIZ_CALLBACK_RESULT`(tag 同 bizCallbackKey,携带 requestId / bizCallbackKey / success / errorMessage / payload / attempt / maxAttempts),system 模块消费结果并更新日志,失败且未超限时由 system 端按配置延迟重试再次投递回调消息。 +4) 重试:默认 3 次、间隔 5 秒,可配置(`iwork.callback.retry.*`),手工重试仍使用 `/log/retry`,由 system 模块重新投递 MQ 消息。 +5) 手工重试:需具备 iWork 模块权限;根据 requestId 将任务重新投递至回调分发器,不增加自动重试计数,可在日志详情中发起。 + +### 状态字面量(示例) + +- `CREATE_PENDING` / `CREATE_SUCCESS` / `CREATE_FAILED` +- `CALLBACK_PENDING` / `CALLBACK_SUCCESS` / `CALLBACK_FAILED` +- `CALLBACK_RETRYING` / `CALLBACK_RETRY_FAILED` + +> 实际实现可根据需要细化,唯一标识均为 requestId。 + +### 重试与配置 + +- 自动重试:默认 `maxAttempts=3`、`delaySeconds=5`,可配置(示例键:`iwork.callback.retry.max-attempts`、`iwork.callback.retry.delay-seconds`)。 +- 手工重试:权限校验(沿用 iWork 模块权限前缀);仅在失败/超重试状态下开放。 +- 幂等:按 requestId + bizCallbackKey 进行幂等检查,成功后不再重复分发,除非手工强制重试。 + +### 日志存储 + +- 表:`system_iwork_seal_callback_log`(示例字段) + - `requestId`(PK)、`businessCode`(ywxtdjbh)、`bizCallbackKey`、`status`、`retryCount`、`lastErrorMessage`、`fileUrl`、`fileId`、`businessFileId`、`requestBody`、`responseBody`、`rawCallback`(截断保存原文)、`lastCallbackTime`、`creator`、`createTime`、`updateTime`。 +- 字段要点: + - `rawCallback`:保存回调原始文本,截断存储(无需脱敏,不注明上限)。 + - 无业务附件或保存失败:写 `status=CREATE_FAILED` / `CALLBACK_FAILED` 并记录错误原因。 + +### 查询与展示 + +- 分页:沿用 `PageParam` 约束,`pageNo` 默认 1、`pageSize` 默认 10、上限 10000,仅分页浏览不支持导出。 +- 查询条件:`requestId`、`businessCode`(ywxtdjbh)、`bizCallbackKey`、`status`、时间范围(createTime / lastCallbackTime)。 +- 列表字段:requestId、业务单号、bizCallbackKey、状态、重试次数、最后错误、更新时间。 +- 详情:展示截断的回调原文(请求/响应)、错误原因、附件信息;提供“手工重试”按钮(需权限)。 --- @@ -326,8 +400,8 @@ curl -X POST -H "Content-Type: application/json" -d '{ ## 小结与建议 -- 在配置中补齐 `iwork.app-id`、`iwork.client-public-key`、`iwork.user-id`、`iwork.workflow-id` 等关键字段。 -- 优先在本地通过 `IWorkIntegrationService` Java API 调试,成功后再通过 Controller 的 REST 接口对外暴露。 -- 若遇到请求失败,查看应用日志(`[iWork]` 前缀的日志)与 iWork 网关返回 body,定位是注册、申请 token,还是业务接口(user-info/create/void)失败。 +- 在配置中补齐 `iwork.app-id`、`iwork.client-public-key`、`iwork.user-id`、`iwork.workflow-id` 等关键字段,并按需配置回调重试参数(默认 3 次,5 秒间隔)。 +- 优先在本地通过 `IWorkIntegrationService` Java API 调试,成功后再通过 Controller 的 REST 接口对外暴露;发起后请记录返回的 requestId(唯一标识)与 bizCallbackKey。 +- 若遇到请求失败,查看应用日志(`[iWork]` 前缀的日志)与 iWork 网关返回 body;若业务回调失败,可在日志页面查看截断原文并按权限发起手工重试。 文档已生成并保存到:`docs/iWork集成说明.md`。 diff --git a/docs/iwork 人力资源组织机构RESTFUL接口说明.pdf b/docs/iwork 人力资源组织机构RESTFUL接口说明.pdf new file mode 100644 index 00000000..b60c8581 Binary files /dev/null and b/docs/iwork 人力资源组织机构RESTFUL接口说明.pdf differ diff --git a/docs/数据总线用户使用指南.md b/docs/数据总线用户使用指南.md new file mode 100644 index 00000000..d8e22a66 --- /dev/null +++ b/docs/数据总线用户使用指南.md @@ -0,0 +1,202 @@ +# 数据总线(Databus)用户使用指南 + +> **适用范围**:`zt-module-databus-server`(管理与运行时)、`zt-module-databus-api`(对外协议),面向已部署 `ztcloud` 后端 + `zt-vue-element` 前端的环境。文档区分 **业务管理员** 与 **第三方开发者** 两类读者,帮助您从零到一完成配置、发布与调用。 + +## 1. 角色视角与总体流程 + +| 角色 | 核心目标 | 关键系统入口 | +| --- | --- | --- | +| 业务管理员 | 在后台可视化界面中定义/编排 API,配置凭证、限流、访问日志,并确保版本可控 | 后台路径:`系统管理 → 数据总线`(或直接访问 `/#/databus`)| +| 第三方开发者 | 按照后台下发的 `apiCode + version + appId`,满足安全约束后调用统一网关,解析响应并接入自身系统 | 对外入口:`{protocol}://{host}{basePath}/{apiCode}/{version}`(默认 Base Path `/admin-api/databus/api/portal`)| + +完整流程示意: +1. **管理员初始化**(执行 DM8 脚本、开通菜单与权限)。 +2. **管理员在“API 定义”中建模**(步骤、变换、策略、版本发布)。 +3. **管理员为业务方创建凭证**(`ZT-App-Id`、密钥、IP 白名单、匿名用户等)。 +4. **第三方拿到凭证后发起调用**(签名 → 加密 → 发送 → 解密响应)。 +5. **管理员通过限流/日志功能持续运维**,必要时回滚版本或刷新缓存。 + +## 2. 通用准备事项 + +### 2.1 服务与配置 + +- **运行中服务**:`zt-server` 网关、`zt-module-databus-*` 相关微服务、Redis(用于 nonce、防重放、限流计数)。 +- **关键配置**(摘自 `application.yml`): + - `databus.api-portal.base-path`:默认 `/admin-api/databus/api/portal`,如需兼容旧系统可保留 `/databus/api/portal` 别名。 + - `allowed-ips/denied-ips`:无白名单时留空即放行全部;建议第三方出口 IP 全量登记。 + - `security`:`signature-type(MD5/SHA256)`、`encryption-type(AES/DES)`、`nonce-ttl`、`allowed-clock-skew-seconds`、是否强制请求/响应加密。 + - `enable-tenant-header`:如开启多租,会自动从 `ZT-Tenant-Id`(可自定义)透传。 + +### 2.2 数据库与菜单初始化(DM8) + +| 场景 | 脚本(目录 `sql/dm/`) | 说明 | +| --- | --- | --- | +| 初始化菜单与按钮权限 | `统一外部网关菜单_20251010.sql` | 建立“数据总线”一级菜单及 API/凭证/策略/访问日志页面的所有操作权限,支持重复执行。 | +| 访问日志菜单补充 | `数据总线API访问日志菜单权限_20251028.sql` | 若已执行主菜单脚本仍找不到访问日志入口,可追加该脚本。 | +| API 版本历史表 | `数据总线API版本历史表结构_备忘录模式_20251030.sql` | DM8 兼容语法,包含索引与字段注释,确保支持版本快照/回滚。 | +| 访问日志表 | `数据总线API访问日志表结构_20251028.sql` | 记录追踪 ID、请求/响应、步骤结果等,供后台“访问日志”页面检索。 | +| 示例/演示数据 | `../mysql/databus_sample_data.sql`(MySQL) | 可作为建模参考,字段一致,迁移到 DM 时仅需替换数据类型即可。 | + +> **执行顺序建议**:先运行菜单脚本 → 赋予管理员角色对应按钮权限 → 执行表结构脚本 → 根据需要导入示例数据。 + +## 3. 业务管理员操作路线 + +### 3.1 角色与权限校验 + +1. 通过 DM8 脚本写入菜单/按钮后,将 `数据总线` 相关权限分配给目标角色(如 `system_admin`)。 +2. 确认角色拥有以下最少权限:`databus:gateway:*`、`databus:credential:*`、`databus:policy:*`、`databus:gateway:access-log:query`。 + +### 3.2 创建 API 定义 + +进入 **数据总线 → API 定义**,按以下步骤操作: + +1. 点击“新建”填写基础信息:`API 编码 (apiCode)`、`版本 (version)`、`描述`、`HTTP Method`、`URI Pattern`(运行期仅用于文档,可自由描述)。 +2. 选择 **认证策略** 与 **限流策略**:默认策略可复用,也可在对应菜单先新建。 +3. 选择 **状态 = 草稿**,避免未完成配置就被调度。 + +> **提示**:Start/End 步骤系统会自动校验唯一性,必须至少存在一条 Start 与 End 步骤。 + +### 3.3 配置编排步骤与变换 + +1. 在 API 详情页新增步骤: + - **类型**:HTTP / RPC / Script / Start / End。 + - **目标地址**:HTTP 需写 `METHOD http://host/path`,并按需填写 Header/Body 映射表达式。 + - **映射表达式**:支持 JSON 表达式,常用写法示例同 `databus_sample_data.sql` 中 `JSON::(...)`。 + - **重试/超时**:以 JSON 形式配置 `maxAttempts`、`delayMs`。 +2. 配置步骤级或 API 级 **Transform**: + - 场景示例:请求前补链路追踪 ID(`REQUEST_PRE`)、响应前组装统一结构(`RESPONSE_PRE`)。 + - 校验规则:同一 `phase` 仅允许一条记录。 + +### 3.4 版本与发布 + +1. **保存即可生成版本快照**:系统会在 `databus_api_version` 中写入 JSON 快照并标记 `is_current=1`。 +2. **调试**:在 API 列表点击“调试”,通过 `POST /databus/gateway/invoke` 模拟真实请求,可自带 Header/Body。 +3. **上线**:修改 `状态=ONLINE` 后保存,系统自动刷新运行时 Flow。若需全量刷新,可在列表右上点击“刷新缓存”。 +4. **回滚**:在“版本历史”侧栏选择目标版本 → 点击“回滚”,系统将历史快照恢复为现有配置并重新发布。 + +### 3.5 凭证与策略运营 + +1. **客户端凭证**: + - 进入“客户端凭证” → 新建 → 生成 `appId`、密钥、签名/加密算法。 + - 可绑定匿名用户(后台会在调用时自动模拟该用户登录)。 + - 支持 IP 白名单、描述及状态维护。 +2. **限流策略**: + - 在“限流策略”菜单中新增 `FIXED_WINDOW` 类型,配置 `limit`、`windowSeconds`、`keyTemplate`。 + - 常见做法:`${apiCode}:${header.X-Client-Id}`,以不同客户端维度做计数。 + +### 3.6 监控与排障 + +1. **访问日志**:菜单路径“访问日志”,支持按 `traceId / apiCode / 时间` 检索详情,字段对应 `databus_api_access_log`。 +2. **常见问题定位**: + - 401 → 校验凭证/签名/时间戳。 + - 429 → 限流策略触发,可在日志中观察 `status=1`/`errorCode=API_RATE_LIMIT_EXCEEDED`。 + - 5xx → 检查步骤 `stepResults` 中的 `error` 描述、Integration Flow 日志。 + +## 4. 第三方开发者操作路线 + +### 4.1 凭证申请 + +向业务管理员提供以下信息:调用系统名称、出口 IP 列表、是否需要匿名账号、预期调用 API 列表。管理员创建凭证后会得到: + +- `ZT-App-Id` +- `encryptionKey` + `encryptionType`(AES/DES) +- `signatureType`(MD5/SHA256) +- 可选:匿名内部用户、租户 ID + +### 4.2 网络与安全要求 + +- 请求必须来自允许的 IP,且使用 HTTPS(推荐)或可信的内网段。 +- 时间戳与服务器偏差 ≤ `allowed-clock-skew-seconds`(默认 300 秒)。 +- `nonce` 在 `nonce-ttl-seconds` 内不得重复(默认 600 秒,服务器使用 Redis 去重)。 +- 若开启 `require-body-encryption`,请求体必须是 **加密后再 Base64 编码** 的字符串。 + +### 4.3 构建请求的 7 个步骤 + +| 步骤 | 动作 | 关键点 | +| --- | --- | --- | +| 1 | 生成 `timestamp` | `long` 类型毫秒值;建议每次现取。 | +| 2 | 生成 `nonce` | ≥8 位随机字符串,推荐 `UUID` 去掉 `-`。 | +| 3 | 准备明文 Body | JSON 文本,记为 `plainBody`。GET 请求仍建议传空 JSON `{}`,以便签名。 | +| 4 | 计算签名 | 将 `appId`、`timestamp`、`nonce`、`plainBody`(或 Query)按 key 排序拼接 `key=value`,使用约定算法得出 `signature`。 | +| 5 | 加密 Body | 以凭证密钥对 `plainBody` 执行 AES/DES 加密,再 Base64(需与服务端配置一致)。 | +| 6 | 组装请求头 | 至少包含 `ZT-App-Id`、`ZT-Timestamp`、`ZT-Nonce`、`ZT-Signature`、`Content-Type`,可附带 `ZT-Tenant-Id`、`X-Client-Id`。 | +| 7 | 发送请求 | URL = `{basePath}/{apiCode}/{version}`,HTTP 方法与后台配置一致;响应若被加密需反向解密。 | + +#### 请求示例(伪代码) + +```text +POST https://gw.example.com/admin-api/databus/api/portal/order.create/v1 +Headers: + ZT-App-Id: demo-app + ZT-Timestamp: 1732070400000 + ZT-Nonce: 0c5e2df9a1 + ZT-Signature: 8e377... + X-Client-Id: mall +Body (Base64):Q2hhcnNldGV4dC1CYXNlNjQgZW5jcnlwdGVkIGJvZHk= +``` + +#### 响应处理 + +1. 读取 HTTP 状态与 `code/message/traceId` 字段。 +2. 若后台开启响应加密,需使用同一密钥解密得到真正的 `response` JSON。 +3. 保留 `traceId` 以便与管理员对齐日志。 + +### 4.4 常见故障排查 + +| 症状 | 常见原因 | 自助检查 | +| --- | --- | --- | +| 401 + `签名校验失败` | 拼接顺序错误 / 对象未序列化一致 | 确认按字典序拼接 `key=value`,body 使用明文。 | +| 401 + `请求到达时间超出` | 客户端时间漂移 | 同步 NTP,或在发送前刷新服务器时间。 | +| 401 + `重复请求` | `nonce` 重复或被重放 | 确保每次生成唯一随机串。 | +| 403 | IP/应用被禁用 | 让管理员检查凭证状态、白名单。 | +| 429 | 限流策略触发 | 调整并发或与管理员协商提升 `limit`。 | +| 5xx + `步骤执行失败` | 后端步骤调用异常 | 提供 `traceId`,管理员可在访问日志中查看 `stepResults` 详细报错。 | + +## 5. 策略、监控与日志 + +### 5.1 限流与审计 + +- 策略以 JSON 存储在 `databus_policy_rate_limit`,字段:`limit`(次数)、`windowSeconds`(窗口秒)及可选 `keyTemplate`。 +- 当前默认策略实现为 **Redis 固定窗口**:Key 格式 `databus:api:rl:{apiCode}:{version}:{X-Client-Id}`。 +- 触发限流时返回 `HTTP 429`,日志中 `status=1` 且 `errorCode=API_RATE_LIMIT_EXCEEDED`。 +- 如需关闭限流,管理员可在 `application.yml` 中设置 `databus.api-portal.enable-rate-limit=false` 或在 API 上解除策略绑定。 +- 审计日志由 `ApiGatewayAccessLogger` 写入 `databus_api_access_log`,可结合 ELK/BI 做分析。 + +### 5.2 访问日志使用 + +1. **建表**:执行 `sql/dm/数据总线API访问日志表结构_20251028.sql`(已含主键、索引、字段注释)。 +2. **后台入口**:`数据总线 → 访问日志`,需 `databus:gateway:access-log:query` 权限。 +3. **常见字段解释**: + - `REQUEST_*`:存储原始请求;如启用请求加密,会记录解密后的明文。 + - `STEP_RESULTS`:JSON 数组,展示每个步骤的 `elapsed`、`request/response`、`error`,用于定位链路问题。 + - `TRACE_ID`:可与链路追踪或日志平台联动。 +4. **TODO**:如目标环境尚未执行该表脚本,请尽快在 DM 数据库运行,以免访问日志页面无法查询。 + +## 6. 演示数据与调试建议 + +- 使用 `sql/mysql/databus_sample_data.sql` 可一次性生成: + - 3 个示例 API(聚合查询 / 用户画像 / 快速登录)。 + - 对应的步骤、变换、限流策略、发布记录。 +- 若目标环境为 DM,可参考该脚本结构,将 `AUTO_INCREMENT` 改为手动 ID、`datetime` 改 `timestamp`,即可导入。 +- 通过示例数据,管理员可演练以下操作: + 1. 打开 API 列表验证导入内容; + 2. 使用“调试”通过管理端发起调用,确认步骤链路; + 3. 让第三方按照真实流程调用,观察访问日志、限流表现。 + +## 7. 附录:常用 REST 接口速查 + +| 模块 | 方法 | 路径 | 用途 | +| --- | --- | --- | --- | +| API 定义 | GET | `/databus/gateway/definition/page` | 列表检索 | +| | POST | `/databus/gateway/definition` | 新建/更新 API(PUT 更新) | +| | DELETE | `/databus/gateway/definition/{id}` | 删除并注销 Flow | +| API 版本 | GET | `/databus/gateway/version/list?apiId=` | 历史版本 | +| | PUT | `/databus/gateway/version/rollback` | 回滚 | +| 客户端凭证 | POST | `/databus/gateway/credential/create` | 创建凭证 | +| 限流策略 | POST | `/databus/gateway/policy/rate-limit` | 创建/更新策略 | +| 调试/刷新 | POST | `/databus/gateway/invoke` | 管理端调试 | +| | POST | `/databus/gateway/cache/refresh` | 全量刷新 Flow | +| 日志 | GET | `/databus/gateway/access-log/page` | 查看访问日志 | + +--- +如仍有未覆盖的业务场景,可在 `docs/数据总线模块大致功能与调用介绍.md` 中查阅更底层的架构说明,再结合本文步骤完成落地。祝使用顺利! diff --git a/docs/菜单数据权限使用文档.md b/docs/菜单数据权限使用文档.md new file mode 100644 index 00000000..fe539ea9 --- /dev/null +++ b/docs/菜单数据权限使用文档.md @@ -0,0 +1,617 @@ +# 菜单数据权限使用文档 + +## 📖 目录 + +1. [功能介绍](#功能介绍) +2. [架构说明](#架构说明) +3. [开发指南](#开发指南) +4. [配置指南](#配置指南) +5. [注意事项](#注意事项) +6. [完整示例](#完整示例) +7. [常见问题](#常见问题) + +--- + +## 功能介绍 + +### 什么是菜单数据权限? + +菜单数据权限是一种**基于菜单的动态数据过滤机制**,允许管理员为不同的角色配置不同的数据查询规则,实现细粒度的数据权限控制。 + +### 核心特性 + +- ✅ **动态配置**:无需修改代码,通过页面配置即可实现数据过滤 +- ✅ **基于角色**:不同角色可以看到不同的数据 +- ✅ **灵活规则**:支持多种条件(等于、大于、IN、LIKE等) +- ✅ **变量支持**:支持动态变量(如当前用户ID、部门ID等) +- ✅ **SQL级别**:在SQL层面过滤,性能高效 + +### 使用场景 + +1. **部门数据隔离**:用户只能查看自己部门的数据 +2. **创建人过滤**:用户只能查看自己创建的数据 +3. **状态过滤**:某些角色只能查看特定状态的数据 +4. **自定义规则**:根据业务需求配置任意过滤条件 + +--- + +## 架构说明 + +### 模块结构 + +``` +zt-framework/ +└── zt-spring-boot-starter-biz-data-permission/ # 框架模块 + └── menudatapermission/ + ├── annotation/ # @PermissionData 注解 + ├── aop/ # AOP 切面 + ├── context/ # ThreadLocal 上下文 + ├── handler/ # MyBatis 拦截器 + ├── model/ # DTO 模型 + └── util/ # 工具类 + +zt-module-system/ +└── zt-module-system-server/ # 业务模块 + └── framework/permission/ + └── MenuDataRuleLoaderImpl.java # 规则加载器实现 +``` + +### 工作流程 + +``` +1. 用户访问页面(如:角色管理) + ↓ +2. Controller 方法上有 @PermissionData 注解 + ↓ +3. AOP 切面拦截,根据 pageComponent 查询菜单ID + ↓ +4. 加载该菜单下用户角色关联的数据规则 + ↓ +5. 将规则存入 ThreadLocal + ↓ +6. MyBatis 执行查询时,拦截器读取规则 + ↓ +7. 构建 SQL WHERE 条件并添加到查询中 + ↓ +8. 返回过滤后的数据 + ↓ +9. finally 块清理 ThreadLocal +``` + +--- + +## 前置条件 + +### ✅ 检查 Maven 依赖 + +在使用菜单数据权限功能前,**必须确保业务模块已引入框架依赖**。 + +#### 1. 检查依赖是否存在 + +打开业务模块的 `pom.xml` 文件(如 `zt-module-xxx-server/pom.xml`),检查是否包含以下依赖: + +```xml + + com.zt.plat + zt-spring-boot-starter-biz-data-permission + +``` + +#### 2. 如何检查? + +**方法1:查看 pom.xml** +```bash +# 在项目根目录执行 +grep -r "zt-spring-boot-starter-biz-data-permission" zt-module-xxx/zt-module-xxx-server/pom.xml +``` + +**方法2:在 IDE 中查看** +- IDEA:打开 `pom.xml`,搜索 `zt-spring-boot-starter-biz-data-permission` +- 或者查看 Maven 依赖树 + +#### 3. 如果没有依赖,如何添加? + +在业务模块的 `pom.xml` 中添加: + +```xml + + + + + + com.zt.plat + zt-spring-boot-starter-biz-data-permission + + +``` + +#### 4. 已包含该依赖的模块 + +以下模块已默认包含该依赖,可直接使用: +- ✅ `zt-module-system-server` +- ⚠️ 其他业务模块需自行检查 + +--- + +## 开发指南 + +### 步骤1:在 Controller 方法上添加注解 + +在需要数据过滤的查询方法上添加 `@PermissionData` 注解: + +```java +import com.zt.plat.framework.datapermission.core.menudatapermission.annotation.PermissionData; + +@RestController +@RequestMapping("/system/role") +public class RoleController { + + @GetMapping("/page") + @PermissionData(pageComponent = "system/role/index") // 指定页面组件路径 + public CommonResult> getRolePage(RolePageReqVO pageReqVO) { + PageResult pageResult = roleService.getRolePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, RoleRespVO.class)); + } +} +``` + +### 步骤2:确定 pageComponent 值 + +`pageComponent` 是前端页面组件的路径,用于关联菜单: + +- **格式**:`模块名/功能名/页面名` +- **示例**: + - `system/role/index` - 角色管理页面 + - `system/user/index` - 用户管理页面 + - `system/dept/index` - 部门管理页面 + +**如何查找 pageComponent?** + +1. 打开前端项目,找到对应的 Vue 文件路径 +2. 例如:`src/views/system/role/index.vue` +3. pageComponent 就是:`system/role/index` + +### 步骤3:在菜单表中配置 component 字段 + +确保数据库 `system_menu` 表中,该菜单的 `component` 字段值与 `pageComponent` 一致: + +```sql +-- 示例:角色管理菜单 +UPDATE system_menu +SET component = 'system/role/index' +WHERE id = 角色管理菜单ID; +``` + +### 注解参数说明 + +```java +@PermissionData( + pageComponent = "system/role/index", // 必填:页面组件路径 + enable = true // 可选:是否启用,默认 true +) +``` + +**何时设置 `enable = false`?** + +当某个方法不需要数据权限过滤时(如管理员查看所有数据): + +```java +@GetMapping("/all") +@PermissionData(pageComponent = "system/role/index", enable = false) +public CommonResult> getAllRoles() { + // 返回所有角色,不受数据权限限制 +} +``` + +--- + +## 配置指南 + +### 步骤1:配置菜单 + +1. 登录系统,进入 **系统管理 > 菜单管理** +2. 找到需要配置数据权限的菜单(如:角色管理) +3. 确认该菜单的 **组件路径** 字段与代码中的 `pageComponent` 一致 + +### 步骤2:配置数据规则 + +1. 在菜单列表中,点击对应菜单的 **"数据规则"** 按钮 +2. 点击 **"新增规则"** 按钮 +3. 填写规则信息: + +#### 规则字段说明 + +| 字段 | 说明 | 示例 | +|------|------|------| +| **规则名称** | 规则的描述性名称 | `只看自己创建的角色` | +| **规则字段** | 数据库表的字段名 | `creator`、`dept_id`、`status` | +| **规则条件** | 比较条件 | `=`、`IN`、`LIKE` 等 | +| **规则值** | 比较的值,支持变量 | `#{userId}`、`#{deptId}` | +| **状态** | 启用/禁用 | 启用 | +| **排序** | 规则执行顺序 | 0 | + +#### 支持的规则条件 + +| 条件 | 说明 | 示例 | +|------|------|------| +| `=` | 等于 | `creator = #{userId}` | +| `!=` | 不等于 | `status != 0` | +| `>` | 大于 | `create_time > '2024-01-01'` | +| `<` | 小于 | `sort < 100` | +| `>=` | 大于等于 | `age >= 18` | +| `<=` | 小于等于 | `price <= 1000` | +| `IN` | 包含 | `dept_id IN (#{deptIds})` | +| `NOT_IN` | 不包含 | `status NOT_IN (0,1)` | +| `LIKE` | 模糊匹配 | `name LIKE '张'` | +| `NOT_LIKE` | 不匹配 | `name NOT_LIKE '测试'` | +| `IS_NULL` | 为空 | `deleted_time IS_NULL` | +| `IS_NOT_NULL` | 不为空 | `phone IS_NOT_NULL` | +| `BETWEEN` | 区间 | `age BETWEEN 18,60` | +| `NOT_BETWEEN` | 不在区间 | `score NOT_BETWEEN 0,60` | +| `SQL_RULE` | 自定义SQL | `(dept_id = 1 OR creator = 2)` | + +#### 支持的变量 + +| 变量 | 说明 | 示例 | +|------|------|------| +| `#{userId}` | 当前用户ID | `creator = #{userId}` | +| `#{username}` | 当前用户名 | `create_by = #{username}` | +| `#{deptId}` | 当前部门ID | `dept_id = #{deptId}` | +| `#{companyId}` | 当前公司ID | `company_id = #{companyId}` | +| `#{tenantId}` | 当前租户ID | `tenant_id = #{tenantId}` | +| `#{deptIds}` | 用户所有部门ID | `dept_id IN (#{deptIds})` | +| `#{companyIds}` | 用户所有公司ID | `company_id IN (#{companyIds})` | +| `#{postIds}` | 用户所有岗位ID | `post_id IN (#{postIds})` | + +#### SQL_RULE 类型详解 + +`SQL_RULE` 是一种特殊的规则类型,允许你直接编写自定义 SQL 表达式,实现更复杂的数据过滤逻辑。 + +**什么时候使用 SQL_RULE?** + +- ✅ 需要 `OR` 逻辑连接多个条件 +- ✅ 需要组合多个字段的复杂判断 +- ✅ 需要嵌套条件或括号分组 +- ✅ 标准规则条件无法满足业务需求 + +**配置方法** + +| 字段 | 配置值 | +|------|--------| +| 规则条件 | 选择 `SQL_RULE` | +| 规则值 | 直接填写 SQL WHERE 条件表达式 | +| 规则字段 | 可以留空(不使用) | + +**配置示例** + +1. **OR 逻辑 - 查看自己创建的或自己部门的数据** + +``` +规则条件:SQL_RULE +规则值:(creator = #{userId} OR dept_id = #{deptId}) +``` + +生成的 SQL: +```sql +WHERE (creator = '123' OR dept_id = '456') +``` + +2. **多字段组合 - 特定部门的已启用数据** + +``` +规则条件:SQL_RULE +规则值:(dept_id IN (#{deptIds}) AND status = 1) +``` + +生成的 SQL: +```sql +WHERE (dept_id IN ('100', '101', '102') AND status = 1) +``` + +3. **复杂嵌套条件 - 管理员或本部门负责人** + +``` +规则条件:SQL_RULE +规则值:(role_type = 'admin' OR (dept_id = #{deptId} AND is_leader = 1)) +``` + +生成的 SQL: +```sql +WHERE (role_type = 'admin' OR (dept_id = '456' AND is_leader = 1)) +``` + +4. **时间范围 + 状态过滤** + +``` +规则条件:SQL_RULE +规则值:(create_time >= '2024-01-01' AND status IN (1, 2)) +``` + +生成的 SQL: +```sql +WHERE (create_time >= '2024-01-01' AND status IN (1, 2)) +``` + +**工作原理** + +当规则条件为 `SQL_RULE` 时: + +1. 系统会忽略"规则字段"和"规则条件" +2. 直接使用"规则值"中的 SQL 表达式 +3. 先替换表达式中的变量(如 `#{userId}`) +4. 将替换后的表达式直接添加到 SQL WHERE 子句中 + +代码实现(MenuDataPermissionRule.java:64-67): +```java +// 处理 SQL_RULE 类型(自定义 SQL) +if ("SQL_RULE".equals(ruleConditions)) { + return actualValue; // 直接返回替换变量后的 SQL 表达式 +} +``` + +**⚠️ 重要警告** + +1. **SQL 注入风险** + - SQL_RULE 直接拼接到 SQL 中,存在注入风险 + - ✅ **安全做法**:只使用预定义变量(`#{userId}` 等) + - ❌ **危险做法**:不要在规则值中拼接用户输入的内容 + +2. **字段名必须正确** + - SQL_RULE 中的字段名必须与数据库表字段完全一致 + - 错误的字段名会导致 SQL 查询报错 + - 建议先在数据库中测试 SQL 语句 + +3. **括号很重要** + - 建议始终用括号包裹整个表达式:`(condition1 OR condition2)` + - 避免与其他规则或系统条件产生优先级问题 + +4. **变量替换** + - 变量会被替换为带引号的字符串值 + - 例如:`#{userId}` → `'123'` + - 数据库会自动处理类型转换 + +**SQL_RULE vs 普通规则对比** + +| 特性 | 普通规则 | SQL_RULE | +|------|---------|----------| +| 配置难度 | 简单,选择即可 | 需要 SQL 知识 | +| 灵活性 | 有限,单一条件 | 非常灵活,任意表达式 | +| 安全性 | 高,系统控制 | 需要注意 SQL 注入 | +| OR 逻辑 | ❌ 不支持 | ✅ 支持 | +| 嵌套条件 | ❌ 不支持 | ✅ 支持 | +| 多规则组合 | AND 连接 | 单个规则内实现 | +| 错误提示 | 友好 | SQL 错误信息 | + +**最佳实践** + +1. **优先使用普通规则**:能用普通规则解决的,不要用 SQL_RULE +2. **测试后再上线**:在测试环境验证 SQL 语句正确性 +3. **添加注释**:在规则名称中说明 SQL_RULE 的用途 +4. **定期审查**:定期检查 SQL_RULE 规则,删除不再使用的 +5. **权限控制**:限制能配置 SQL_RULE 的管理员权限 + +### 步骤3:关联角色 + +1. 配置完规则后,进入 **系统管理 > 角色管理** +2. 编辑需要应用规则的角色 +3. 在 **"数据权限"** 标签页中,勾选对应的菜单数据规则 +4. 保存角色配置 + +### 步骤4:测试验证 + +1. 使用该角色的用户登录系统 +2. 访问配置了数据规则的页面 +3. 验证数据是否按规则过滤 + +--- + +## 注意事项 + +### ⚠️ 重要提醒 + +#### 1. 规则字段必须与数据库表字段一致 + +**错误示例**: +``` +规则字段:dept_id_xxx +数据库字段:dept_id +结果:SQL 查询报错! +``` + +**正确做法**: +- 在配置规则前,先确认数据库表结构 +- 字段名必须完全一致(包括大小写) +- 前端已添加警告提示,请仔细阅读 + +#### 2. 只在菜单/页面级别配置规则 + +- ✅ **菜单/页面**(type=2):需要配置数据规则 +- ❌ **目录**(type=1):不需要配置 +- ❌ **按钮**(type=3):不需要配置 + +前端已自动隐藏目录和按钮的"数据规则"按钮。 + +#### 3. 多个规则使用 AND 连接 + +如果为同一个菜单配置了多个规则,它们会用 `AND` 连接: + +```sql +-- 规则1:creator = #{userId} +-- 规则2:status = 1 +-- 最终SQL: +WHERE creator = '当前用户ID' AND status = 1 +``` + +#### 4. 变量不存在时的处理 + +如果配置了不存在的变量(如 `#{unknownVar}`),系统会: +- 记录警告日志 +- 将变量替换为空字符串 +- 可能导致查询结果为空 + +**建议**:使用前端下拉框选择变量,避免手动输入错误。 + +#### 5. 性能考虑 + +- 数据规则在 SQL 层面过滤,性能较好 +- 但过多的规则会增加 SQL 复杂度 +- 建议每个菜单不超过 5 条规则 + +#### 6. 禁用数据权限的场景 + +某些查询不应该受数据权限限制,需要添加 `@DataPermission(enable = false)`: + +```java +// 示例:查询所有根级部门(不受数据权限限制) +@Override +@DataPermission(enable = false) +public List getTopLevelDeptList() { + // ... +} +``` + +--- + +## 完整示例 + +### 场景:角色管理 - 只看自己创建的角色 + +#### 1. 后端代码 + +```java +@RestController +@RequestMapping("/system/role") +public class RoleController { + + @Resource + private RoleService roleService; + + @GetMapping("/page") + @Operation(summary = "获得角色分页") + @PreAuthorize("@ss.hasPermission('system:role:query')") + @PermissionData(pageComponent = "system/role/index") // 添加数据权限注解 + public CommonResult> getRolePage(RolePageReqVO pageReqVO) { + PageResult pageResult = roleService.getRolePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, RoleRespVO.class)); + } +} +``` + +#### 2. 菜单配置 + +确保 `system_menu` 表中角色管理菜单的配置: + +```sql +SELECT id, name, component +FROM system_menu +WHERE name = '角色管理'; + +-- 结果: +-- id: 101 +-- name: 角色管理 +-- component: system/role/index ✅ 与代码中的 pageComponent 一致 +``` + +#### 3. 数据规则配置 + +在页面上配置规则: + +| 字段 | 值 | +|------|------| +| 规则名称 | 只看自己创建的角色 | +| 规则字段 | `creator` | +| 规则条件 | `=` | +| 规则值 | `#{userId}` | +| 状态 | 启用 | +| 排序 | 0 | + +#### 4. 角色关联 + +1. 进入角色管理,编辑"普通用户"角色 +2. 在"数据权限"标签页,勾选"只看自己创建的角色"规则 +3. 保存 + +#### 5. 生成的 SQL + +当普通用户(ID=123)查询角色列表时,实际执行的 SQL: + +```sql +SELECT * FROM system_role +WHERE deleted = 0 + AND tenant_id = 1 + AND creator = '123' -- 自动添加的数据权限条件 +ORDER BY sort ASC; +``` + +--- + +## 常见问题 + +### Q1: 为什么配置了规则但不生效? + +**可能原因**: + +1. ✅ 检查 Controller 方法是否添加了 `@PermissionData` 注解 +2. ✅ 检查 `pageComponent` 是否与菜单的 `component` 字段一致 +3. ✅ 检查规则是否启用(状态=启用) +4. ✅ 检查角色是否关联了该规则 +5. ✅ 检查用户是否拥有该角色 + +### Q2: 如何查看实际执行的 SQL? + +在 `application.yaml` 中开启 SQL 日志: + +```yaml +# 方式1:MyBatis Plus SQL 日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +# 方式2:Logback 日志 +logging: + level: + com.zt.plat.module.system.dal.mysql: debug +``` + +### Q3: 多个规则如何组合? + +多个规则使用 `AND` 连接,如果需要 `OR` 逻辑,使用 `SQL_RULE` 类型: + +``` +规则条件:SQL_RULE +规则值:(dept_id = #{deptId} OR creator = #{userId}) +``` + +### Q4: 如何配置"查看本部门及下级部门"的规则? + +这需要在业务层实现,菜单数据权限只支持简单的字段过滤。建议使用原有的部门数据权限功能。 + +### Q5: 规则字段配置错误会怎样? + +会导致 SQL 查询报错,因为数据库找不到该字段。前端已添加警告提示,请仔细检查。 + +--- + +## 总结 + +菜单数据权限提供了一种灵活、高效的数据过滤机制: + +✅ **开发简单**:只需添加一个注解 +✅ **配置灵活**:通过页面配置,无需修改代码 +✅ **性能高效**:SQL 层面过滤,不影响性能 +✅ **易于维护**:规则集中管理,便于调整 + +**最佳实践**: +1. 优先使用预定义变量,避免手动输入 +2. 规则字段必须与数据库表字段一致 +3. 合理使用规则,避免过度复杂 +4. 定期review规则配置,删除无用规则 + +--- + +## 技术支持 + +如有问题,请联系: +- 开发团队:ZT +- 文档版本:v1.0 +- 更新日期:2026-01-27 diff --git a/docs/附件上传注解使用说明.md b/docs/附件上传注解使用说明.md new file mode 100644 index 00000000..31ceda6b --- /dev/null +++ b/docs/附件上传注解使用说明.md @@ -0,0 +1,152 @@ +# @FileUploadController 注解使用说明 + +本文档说明 `@FileUploadController` 的使用方式、调用流程、以及报文结构示例。适用范围为业务创建接口在请求体中携带附件列表的场景,并在业务创建成功后自动建立“业务-附件”的关联关系。 + +## 1. 适用前提 + +- **附件列表在业务创建请求体中传入**。 +- **业务创建返回结构统一为**:`{ code, data: { id, code } }`。 +- 不涉及二次请求与补充请求头流程(文档不包含相关内容)。 + +## 2. 注解字段说明 + +位置:`com.zt.plat.framework.business.annotation.FileUploadController` + +| 字段 | 说明 | 默认值 | 备注 | +|---|---|---|---| +| `filesKey` | 请求体中附件数组的路径 | `files` | 支持多层级路径,如 `data.files` | +| `fileNameKey` | 附件名称字段 | `name` | 解析附件名称使用 | +| `fileIdKey` | 附件ID字段 | `id` | 解析附件ID使用 | +| `source` | 业务来源标识 | `default` | 例如 `bpm` / `oa` / `hr` 等 | +| `primaryKey` | 响应中业务主键路径 | `data.id` | 支持多层级路径 | +| `codeKey` | 响应中业务编码路径 | `data.code` | 支持多层级路径 | + +## 3. 关联处理机制(简述) + +当 Controller 标注 `@FileUploadController` 后: + +1. 请求进入时会读取注解配置并写入 request attribute。 +2. 请求处理完成后,过滤器会读取: + - 请求体中的附件数组 + - 响应体中的业务主键与业务编码 +3. 当响应 `code == 0` 时,自动调用 `BusinessFileApi.batchCreateBusinessFile(...)` 建立附件关联。 + +> 注意:如果请求体中未包含附件数组,或响应中未返回业务主键,则不会产生关联。 + +## 4. 标准调用流程 + +### 步骤 1:业务 Controller 标注注解 + +```java +@FileUploadController(source = "template.contract") +public class DemoContractController extends AbstractFileUploadController implements BusinessControllerMarker { + static { + FileUploadController annotation = DemoContractController.class.getAnnotation(FileUploadController.class); + if (annotation != null) { + setFileUploadInfo(annotation); + } + } +} +``` + +### 步骤 2:可选获取 source 信息 + +请求: + +``` +GET /template/demo-contract/upload-info +``` + +响应: + +```json +{ + "code": 0, + "data": { "source": "template.contract" }, + "msg": "成功" +} +``` + +### 步骤 3:业务创建请求携带附件列表 + +请求体示例(默认字段): + +```json +{ + "name": "测试合同", + "amount": 2000, + "files": [ + { "id": 10125, "name": "合同附件.pdf" }, + { "id": 10126, "name": "补充材料.docx" } + ] +} +``` + +### 步骤 4:业务创建成功返回 + +响应体示例(统一结构): + +```json +{ + "code": 0, + "data": { + "id": 90001, + "code": "HT-2026-0001" + }, + "msg": "成功" +} +``` + +系统会自动读取: + +- `files` 中的 `id/name` +- `data.id` 和 `data.code` + +并建立业务附件关联。 + +## 5. 多层级字段示例(可选配置) + +如果业务请求体与响应体存在嵌套结构,可通过注解自定义路径: + +```java +@FileUploadController( + source = "template.contract", + filesKey = "data.files", + fileIdKey = "fileId", + fileNameKey = "fileName", + primaryKey = "data.businessId", + codeKey = "data.businessCode" +) +``` + +对应请求体示例: + +```json +{ + "data": { + "files": [ + { "fileId": 1001, "fileName": "合同.pdf" } + ] + } +} +``` + +对应响应体示例: + +```json +{ + "code": 0, + "data": { + "businessId": 90001, + "businessCode": "HT-2026-0001" + }, + "msg": "成功" +} +``` + +## 6. 常见注意事项 + +- 响应 `code != 0` 时不会执行附件关联。 +- `files` 必须是数组,否则会被忽略。 +- 若业务返回未包含 `primaryKey` 对应的字段,附件不会关联。 +- `source` 建议以业务模块唯一标识命名,便于后续查询与归档。 diff --git a/pom.xml b/pom.xml index 1642e808..594bcd5a 100644 --- a/pom.xml +++ b/pom.xml @@ -172,50 +172,6 @@ - - - - huaweicloud - huawei - https://mirrors.huaweicloud.com/repository/maven/ - - - aliyunmaven - aliyun - https://maven.aliyun.com/repository/public - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - false - - - - ZT - 中铜 ZStack 私服 - http://172.16.46.63:30708/repository/zt-cloud/ - - always - warn - - - true - always - - - - ZT diff --git a/sql/dm/20260126菜单数据规则表.sql b/sql/dm/20260126菜单数据规则表.sql new file mode 100644 index 00000000..7fb4e0b3 --- /dev/null +++ b/sql/dm/20260126菜单数据规则表.sql @@ -0,0 +1,73 @@ +-- ---------------------------- +-- Table structure for system_menu_data_rule +-- ---------------------------- +CREATE TABLE system_menu_data_rule ( + id bigint NOT NULL PRIMARY KEY, + menu_id bigint NOT NULL, + rule_name varchar(100) NOT NULL, + rule_column varchar(100) DEFAULT NULL NULL, + rule_conditions varchar(20) NOT NULL, + rule_value varchar(500) NOT NULL, + status smallint DEFAULT 1 NOT NULL, + sort int DEFAULT 0 NOT NULL, + remark varchar(500) DEFAULT NULL NULL, + creator varchar(64) DEFAULT '' NULL, + create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + updater varchar(64) DEFAULT '' NULL, + update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted bit DEFAULT '0' NOT NULL, + tenant_id bigint DEFAULT 0 NOT NULL +); + +-- CREATE INDEX idx_menu_data_rule_menu ON system_menu_data_rule (menu_id); +-- CREATE INDEX idx_menu_data_rule_tenant ON system_menu_data_rule (tenant_id); + +COMMENT ON COLUMN system_menu_data_rule.id IS '规则ID'; +COMMENT ON COLUMN system_menu_data_rule.menu_id IS '菜单ID'; +COMMENT ON COLUMN system_menu_data_rule.rule_name IS '规则名称'; +COMMENT ON COLUMN system_menu_data_rule.rule_column IS '规则字段(数据库列名)'; +COMMENT ON COLUMN system_menu_data_rule.rule_conditions IS '规则条件(=、>、<、IN、LIKE等)'; +COMMENT ON COLUMN system_menu_data_rule.rule_value IS '规则值(支持变量如#{userId}、#{deptId})'; +COMMENT ON COLUMN system_menu_data_rule.status IS '状态(0=禁用 1=启用)'; +COMMENT ON COLUMN system_menu_data_rule.sort IS '排序'; +COMMENT ON COLUMN system_menu_data_rule.remark IS '备注'; +COMMENT ON COLUMN system_menu_data_rule.creator IS '创建者'; +COMMENT ON COLUMN system_menu_data_rule.create_time IS '创建时间'; +COMMENT ON COLUMN system_menu_data_rule.updater IS '更新者'; +COMMENT ON COLUMN system_menu_data_rule.update_time IS '更新时间'; +COMMENT ON COLUMN system_menu_data_rule.deleted IS '是否删除'; +COMMENT ON COLUMN system_menu_data_rule.tenant_id IS '租户编号'; +COMMENT ON TABLE system_menu_data_rule IS '菜单数据规则表'; + +-- ---------------------------- +-- Table structure for system_role_menu_data_rule +-- ---------------------------- +CREATE TABLE system_role_menu_data_rule ( + id bigint NOT NULL PRIMARY KEY, + role_id bigint NOT NULL, + menu_id bigint NOT NULL, + data_rule_id bigint NOT NULL, + creator varchar(64) DEFAULT '' NULL, + create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + updater varchar(64) DEFAULT '' NULL, + update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted bit DEFAULT '0' NOT NULL, + tenant_id bigint DEFAULT 0 NOT NULL +); + +-- CREATE INDEX idx_rmdr_role ON system_role_menu_data_rule (role_id); +-- CREATE INDEX idx_rmdr_menu ON system_role_menu_data_rule (menu_id); +-- CREATE INDEX idx_rmdr_tenant ON system_role_menu_data_rule (tenant_id); +-- CREATE INDEX idx_rmdr_role_menu_rule ON system_role_menu_data_rule (role_id, menu_id, data_rule_id); + +COMMENT ON COLUMN system_role_menu_data_rule.id IS '自增主键'; +COMMENT ON COLUMN system_role_menu_data_rule.role_id IS '角色ID'; +COMMENT ON COLUMN system_role_menu_data_rule.menu_id IS '菜单ID'; +COMMENT ON COLUMN system_role_menu_data_rule.data_rule_id IS '数据规则ID'; +COMMENT ON COLUMN system_role_menu_data_rule.creator IS '创建者'; +COMMENT ON COLUMN system_role_menu_data_rule.create_time IS '创建时间'; +COMMENT ON COLUMN system_role_menu_data_rule.updater IS '更新者'; +COMMENT ON COLUMN system_role_menu_data_rule.update_time IS '更新时间'; +COMMENT ON COLUMN system_role_menu_data_rule.deleted IS '是否删除'; +COMMENT ON COLUMN system_role_menu_data_rule.tenant_id IS '租户编号'; +COMMENT ON TABLE system_role_menu_data_rule IS '角色菜单数据规则关联表'; diff --git a/sql/dm/ruoyi-vue-pro-dm8.sql b/sql/dm/ruoyi-vue-pro-dm8.sql index 1a0eba7e..f9a6db25 100644 --- a/sql/dm/ruoyi-vue-pro-dm8.sql +++ b/sql/dm/ruoyi-vue-pro-dm8.sql @@ -340,7 +340,8 @@ CREATE TABLE infra_file ( updater varchar(64) DEFAULT '' NULL, update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, deleted bit DEFAULT '0' NOT NULL, - DOWNLOAD_COUNT INT DEFAULT 0 NOT NULL + DOWNLOAD_COUNT INT DEFAULT 0 NOT NULL, + DOWNLOADABLE SMALLINT DEFAULT 1 NOT NULL ); COMMENT ON COLUMN infra_file.id IS '文件编号'; @@ -358,6 +359,7 @@ COMMENT ON COLUMN infra_file.updater IS '更新者'; COMMENT ON COLUMN infra_file.update_time IS '更新时间'; COMMENT ON COLUMN infra_file.deleted IS '是否删除'; COMMENT ON COLUMN INFRA_FILE.DOWNLOAD_COUNT IS '下载次数'; +COMMENT ON COLUMN INFRA_FILE.DOWNLOADABLE IS '是否可下载(1是,0否)'; COMMENT ON TABLE infra_file IS '文件表'; CREATE INDEX idx_infra_file_hash ON infra_file(hash); @@ -4313,3 +4315,74 @@ VALUES (5022, 2, '日期格式', 'DATE', 'system_sequence_detail_rule_type', 0, 'success', '', '日期格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0), (5023, 3, '数字格式', 'NUMBER', 'system_sequence_detail_rule_type', 0, 'info', '', '数字格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0), (5024, 4, '自定义格式', 'CUSTOM', 'system_sequence_detail_rule_type', 0, 'warning', '', '自定义格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0); + +/* +增加菜单规则(system_menu_data_rule)和规则角色关联表(system_role_menu_data_rule),同增量脚本:sql/dm/20260126菜单数据规则表.sql +*/ +-- ---------------------------- +-- Table structure for system_menu_data_rule +-- ---------------------------- +CREATE TABLE system_menu_data_rule ( + id bigint NOT NULL PRIMARY KEY, + menu_id bigint NOT NULL, + rule_name varchar(100) NOT NULL, + rule_column varchar(100) DEFAULT NULL NULL, + rule_conditions varchar(20) NOT NULL, + rule_value varchar(500) NOT NULL, + status smallint DEFAULT 1 NOT NULL, + sort int DEFAULT 0 NOT NULL, + remark varchar(500) DEFAULT NULL NULL, + creator varchar(64) DEFAULT '' NULL, + create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + updater varchar(64) DEFAULT '' NULL, + update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted bit DEFAULT '0' NOT NULL, + tenant_id bigint DEFAULT 0 NOT NULL +); + + +COMMENT ON COLUMN system_menu_data_rule.id IS '规则ID'; +COMMENT ON COLUMN system_menu_data_rule.menu_id IS '菜单ID'; +COMMENT ON COLUMN system_menu_data_rule.rule_name IS '规则名称'; +COMMENT ON COLUMN system_menu_data_rule.rule_column IS '规则字段(数据库列名)'; +COMMENT ON COLUMN system_menu_data_rule.rule_conditions IS '规则条件(=、>、<、IN、LIKE等)'; +COMMENT ON COLUMN system_menu_data_rule.rule_value IS '规则值(支持变量如#{userId}、#{deptId})'; +COMMENT ON COLUMN system_menu_data_rule.status IS '状态(0=禁用 1=启用)'; +COMMENT ON COLUMN system_menu_data_rule.sort IS '排序'; +COMMENT ON COLUMN system_menu_data_rule.remark IS '备注'; +COMMENT ON COLUMN system_menu_data_rule.creator IS '创建者'; +COMMENT ON COLUMN system_menu_data_rule.create_time IS '创建时间'; +COMMENT ON COLUMN system_menu_data_rule.updater IS '更新者'; +COMMENT ON COLUMN system_menu_data_rule.update_time IS '更新时间'; +COMMENT ON COLUMN system_menu_data_rule.deleted IS '是否删除'; +COMMENT ON COLUMN system_menu_data_rule.tenant_id IS '租户编号'; +COMMENT ON TABLE system_menu_data_rule IS '菜单数据规则表'; + +-- ---------------------------- +-- Table structure for system_role_menu_data_rule +-- ---------------------------- +CREATE TABLE system_role_menu_data_rule ( + id bigint NOT NULL PRIMARY KEY, + role_id bigint NOT NULL, + menu_id bigint NOT NULL, + data_rule_id bigint NOT NULL, + creator varchar(64) DEFAULT '' NULL, + create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + updater varchar(64) DEFAULT '' NULL, + update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted bit DEFAULT '0' NOT NULL, + tenant_id bigint DEFAULT 0 NOT NULL +); + + +COMMENT ON COLUMN system_role_menu_data_rule.id IS '自增主键'; +COMMENT ON COLUMN system_role_menu_data_rule.role_id IS '角色ID'; +COMMENT ON COLUMN system_role_menu_data_rule.menu_id IS '菜单ID'; +COMMENT ON COLUMN system_role_menu_data_rule.data_rule_id IS '数据规则ID'; +COMMENT ON COLUMN system_role_menu_data_rule.creator IS '创建者'; +COMMENT ON COLUMN system_role_menu_data_rule.create_time IS '创建时间'; +COMMENT ON COLUMN system_role_menu_data_rule.updater IS '更新者'; +COMMENT ON COLUMN system_role_menu_data_rule.update_time IS '更新时间'; +COMMENT ON COLUMN system_role_menu_data_rule.deleted IS '是否删除'; +COMMENT ON COLUMN system_role_menu_data_rule.tenant_id IS '租户编号'; +COMMENT ON TABLE system_role_menu_data_rule IS '角色菜单数据规则关联表'; diff --git a/sql/dm/system_iwork_seal_log_dm8.sql b/sql/dm/system_iwork_seal_log_dm8.sql new file mode 100644 index 00000000..5451ee8e --- /dev/null +++ b/sql/dm/system_iwork_seal_log_dm8.sql @@ -0,0 +1,43 @@ +-- iWork 用印回调日志(DM8) +-- 表:system_iwork_seal_log +-- 序列:system_iwork_seal_log_seq + +-- 清理旧对象(若存在) +DROP TABLE IF EXISTS system_iwork_seal_log; + +CREATE TABLE system_iwork_seal_log ( + id BIGINT NOT NULL, + request_id VARCHAR(128) NOT NULL, + business_code VARCHAR(128), + biz_callback_key VARCHAR(255), + status INTEGER, + retry_count INTEGER DEFAULT 0, + max_retry INTEGER, + last_error_message VARCHAR(512), + raw_callback VARCHAR(2000), + last_callback_time DATETIME, + creator VARCHAR(64), + create_time DATETIME DEFAULT SYSDATE, + updater VARCHAR(64), + update_time DATETIME DEFAULT SYSDATE, + deleted SMALLINT DEFAULT 0 NOT NULL, + PRIMARY KEY (id), + UNIQUE (request_id) +); + +COMMENT ON TABLE system_iwork_seal_log IS 'iWork 用印回调日志'; +COMMENT ON COLUMN system_iwork_seal_log.id IS '主键'; +COMMENT ON COLUMN system_iwork_seal_log.request_id IS 'iWork requestId 唯一标识'; +COMMENT ON COLUMN system_iwork_seal_log.business_code IS '业务单号'; +COMMENT ON COLUMN system_iwork_seal_log.biz_callback_key IS '业务回调标识'; +COMMENT ON COLUMN system_iwork_seal_log.status IS '状态枚举'; +COMMENT ON COLUMN system_iwork_seal_log.retry_count IS '已重试次数'; +COMMENT ON COLUMN system_iwork_seal_log.max_retry IS '最大重试次数快照'; +COMMENT ON COLUMN system_iwork_seal_log.last_error_message IS '最后错误信息'; +COMMENT ON COLUMN system_iwork_seal_log.raw_callback IS '回调原文截断'; +COMMENT ON COLUMN system_iwork_seal_log.last_callback_time IS '最近回调时间'; +COMMENT ON COLUMN system_iwork_seal_log.creator IS '创建者'; +COMMENT ON COLUMN system_iwork_seal_log.create_time IS '创建时间'; +COMMENT ON COLUMN system_iwork_seal_log.updater IS '更新者'; +COMMENT ON COLUMN system_iwork_seal_log.update_time IS '最后更新时间'; +COMMENT ON COLUMN system_iwork_seal_log.deleted IS '是否删除'; diff --git a/sql/dm/外部系统推送配置初始化_DM8_20260120.sql b/sql/dm/外部系统推送配置初始化_DM8_20260120.sql new file mode 100644 index 00000000..1b16eef8 --- /dev/null +++ b/sql/dm/外部系统推送配置初始化_DM8_20260120.sql @@ -0,0 +1,132 @@ +-- DM8 外部系统推送配置初始化脚本 +-- 用于配置不同公司/部门/业务类型下的外部系统推送开关 +-- 创建日期:2026-01-20 + +-- 重复执行时请先备份数据 +-- DROP TABLE IF EXISTS system_external_push_config; + +-- 创建表 +CREATE TABLE system_external_push_config ( + id BIGINT NOT NULL PRIMARY KEY, + company_id BIGINT DEFAULT NULL NULL, + dept_id BIGINT DEFAULT NULL NULL, + business_type VARCHAR(32) DEFAULT NULL NULL, + external_system VARCHAR(64) DEFAULT NULL NULL, + enable_push BIT DEFAULT '1' NOT NULL, + remark VARCHAR(512) DEFAULT NULL NULL, + tenant_id BIGINT DEFAULT 0 NOT NULL, + creator VARCHAR(64) DEFAULT '' NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + updater VARCHAR(64) DEFAULT '' NULL, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted TINYINT DEFAULT 0 NOT NULL +); + +-- 表和字段注释 +COMMENT ON TABLE system_external_push_config IS '外部系统推送配置'; +COMMENT ON COLUMN system_external_push_config.id IS '主键编号'; +COMMENT ON COLUMN system_external_push_config.company_id IS '公司编号(关联 system_dept 表的 is_company=1 的记录,为空表示不限制公司)'; +COMMENT ON COLUMN system_external_push_config.dept_id IS '部门编号(关联 system_dept 表的 is_company=0 的记录,为空表示公司级配置)'; +COMMENT ON COLUMN system_external_push_config.business_type IS '业务类型(PURCHASE/SALE/PRODUCTION,为空表示所有业务类型)'; +COMMENT ON COLUMN system_external_push_config.external_system IS '外部系统标识(ERP/IWORK/等,为空表示所有外部系统)'; +COMMENT ON COLUMN system_external_push_config.enable_push IS '是否启用推送(1启用 0停用)'; +COMMENT ON COLUMN system_external_push_config.remark IS '备注'; +COMMENT ON COLUMN system_external_push_config.tenant_id IS '租户编号'; +COMMENT ON COLUMN system_external_push_config.creator IS '创建者'; +COMMENT ON COLUMN system_external_push_config.create_time IS '创建时间'; +COMMENT ON COLUMN system_external_push_config.updater IS '更新者'; +COMMENT ON COLUMN system_external_push_config.update_time IS '更新时间'; +COMMENT ON COLUMN system_external_push_config.deleted IS '删除标记(0未删除 1已删除)'; + +/*-- 唯一索引:租户+公司+部门+业务类型+外部系统的组合唯一 +-- 注意:因为 dept_id 和 external_system 可以为 NULL,使用 COALESCE 处理 +CREATE UNIQUE INDEX uk_external_push_config_unique + ON system_external_push_config (tenant_id, company_id, COALESCE(dept_id, 0), business_type, COALESCE(external_system, '')); + +-- 辅助索引:按公司查询 +CREATE INDEX idx_external_push_config_company + ON system_external_push_config (tenant_id, company_id); + +-- 辅助索引:按业务类型查询 +CREATE INDEX idx_external_push_config_biz_type + ON system_external_push_config (tenant_id, business_type); + + + +-- 初始化菜单权限数据 +-- 主菜单 +INSERT INTO system_menu ( + id, name, permission, type, sort, parent_id, + path, icon, component, component_name, status, + visible, keep_alive, always_show, creator, create_time, + updater, update_time, deleted +) +SELECT + 20060, '外部系统推送配置', '', 2, 60, 1, + 'external-push-config', 'setting', 'system/push/config/index', 'SystemExternalPushConfig', 0, + '1', '1', '1', 'admin', SYSDATE, + 'admin', SYSDATE, 0 +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM system_menu WHERE id = 20060 +); + +-- 查询权限 +INSERT INTO system_menu ( + id, name, permission, type, sort, parent_id, + path, icon, component, status, visible, keep_alive, + creator, create_time, updater, update_time, deleted +) +SELECT + 2006001, '推送配置查询', 'system:external-push-config:query', 3, 1, 20060, + '', '', '', 0, '1', '1', + 'admin', SYSDATE, 'admin', SYSDATE, 0 +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM system_menu WHERE id = 2006001 +); + +-- 创建权限 +INSERT INTO system_menu ( + id, name, permission, type, sort, parent_id, + path, icon, component, status, visible, keep_alive, + creator, create_time, updater, update_time, deleted +) +SELECT + 2006002, '推送配置创建', 'system:external-push-config:create', 3, 2, 20060, + '', '', '', 0, '1', '1', + 'admin', SYSDATE, 'admin', SYSDATE, 0 +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM system_menu WHERE id = 2006002 +); + +-- 修改权限 +INSERT INTO system_menu ( + id, name, permission, type, sort, parent_id, + path, icon, component, status, visible, keep_alive, + creator, create_time, updater, update_time, deleted +) +SELECT + 2006003, '推送配置修改', 'system:external-push-config:update', 3, 3, 20060, + '', '', '', 0, '1', '1', + 'admin', SYSDATE, 'admin', SYSDATE, 0 +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM system_menu WHERE id = 2006003 +); + +-- 删除权限 +INSERT INTO system_menu ( + id, name, permission, type, sort, parent_id, + path, icon, component, status, visible, keep_alive, + creator, create_time, updater, update_time, deleted +) +SELECT + 2006004, '推送配置删除', 'system:external-push-config:delete', 3, 4, 20060, + '', '', '', 0, '1', '1', + 'admin', SYSDATE, 'admin', SYSDATE, 0 +FROM dual +WHERE NOT EXISTS ( + SELECT 1 FROM system_menu WHERE id = 2006004 +);*/ diff --git a/sql/dm/统一对外网关_20251010.sql b/sql/dm/统一对外网关_20251010.sql index b334f638..acacbebf 100644 --- a/sql/dm/统一对外网关_20251010.sql +++ b/sql/dm/统一对外网关_20251010.sql @@ -218,6 +218,7 @@ CREATE TABLE databus_api_client_credential ( create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, updater VARCHAR(64) DEFAULT '' NOT NULL, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + enable_encryption BIT DEFAULT '1' NOT NULL, deleted BIT DEFAULT '0' NOT NULL ); diff --git a/zt-dependencies/pom.xml b/zt-dependencies/pom.xml index ceb1881b..69fbe2fd 100644 --- a/zt-dependencies/pom.xml +++ b/zt-dependencies/pom.xml @@ -72,7 +72,7 @@ 1.18.1 1.18.36 1.6.3 - 5.8.35 + 5.8.43 6.0.0-M19 4.0.3 2.4.1 diff --git a/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/enums/VerifyCodeSendType.java b/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/enums/VerifyCodeSendType.java new file mode 100644 index 00000000..10e1ef6f --- /dev/null +++ b/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/enums/VerifyCodeSendType.java @@ -0,0 +1,9 @@ +package com.zt.plat.framework.common.enums; + +/** + * 验证码发送方式 + */ +public enum VerifyCodeSendType { + SMS, // 短信验证码 + E_OFFICE // e办消息推送 +} diff --git a/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/asyncTask/AsyncLatchUtils.java b/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/asyncTask/AsyncLatchUtils.java index 25d4f235..3b2c532a 100644 --- a/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/asyncTask/AsyncLatchUtils.java +++ b/zt-framework/zt-common/src/main/java/com/zt/plat/framework/common/util/asyncTask/AsyncLatchUtils.java @@ -1,5 +1,7 @@ package com.zt.plat.framework.common.util.asyncTask; +import com.alibaba.ttl.TransmittableThreadLocal; + import java.util.LinkedList; import java.util.List; import java.util.concurrent.*; @@ -9,7 +11,7 @@ import java.util.concurrent.*; * 多次提交,一次等待 */ public class AsyncLatchUtils { - private static final ThreadLocal> THREAD_LOCAL = ThreadLocal.withInitial(LinkedList::new); + private static final TransmittableThreadLocal> THREAD_LOCAL = TransmittableThreadLocal.withInitial(LinkedList::new); /** * 提交一个异步任务 diff --git a/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/core/util/BusinessDeptHandleUtil.java b/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/core/util/BusinessDeptHandleUtil.java index 0f18c4bb..12478437 100644 --- a/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/core/util/BusinessDeptHandleUtil.java +++ b/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/core/util/BusinessDeptHandleUtil.java @@ -59,13 +59,15 @@ public class BusinessDeptHandleUtil { } // 如果有 deptId,校验其是否属于该 companyId if (deptIdHeader != null) { - boolean valid = companyDeptSetByCompanyId.stream().anyMatch(info -> String.valueOf(info.getDeptId()).equals(deptIdHeader)); - if (!valid) { + Optional matched = companyDeptSetByCompanyId.stream() + .filter(info -> String.valueOf(info.getDeptId()).equals(deptIdHeader)) + .findFirst(); + if (matched.isEmpty()) { return null; - } else { - // 部门存在,放行 - return new HashSet<>(); } + // 部门存在,先设置登录信息再放行 + applyAutoSelection(currentLoginUser, request, matched.get()); + return Collections.emptySet(); } if (companyDeptSetByCompanyId.size() == 1) { CompanyDeptInfo singleCompanyDept = companyDeptSetByCompanyId.iterator().next(); @@ -183,10 +185,10 @@ public class BusinessDeptHandleUtil { if (loginUser != null) { loginUser.setVisitCompanyId(Long.valueOf(info.getCompanyId())); loginUser.setVisitCompanyName(info.getCompanyName()); - loginUser.setVisitCompanyCode(info.getCompanyName()); + loginUser.setVisitCompanyCode(info.getCompanyCode()); loginUser.setVisitDeptId(Long.valueOf(info.getDeptId())); loginUser.setVisitDeptName(info.getDeptName()); - loginUser.setVisitDeptCode(info.getDeptName()); + loginUser.setVisitDeptCode(info.getDeptCode()); } request.setAttribute(WebFrameworkUtils.HEADER_VISIT_COMPANY_ID, info.getCompanyId()); if (info.getCompanyName() != null) { diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/config/ZtDataPermissionAutoConfiguration.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/config/ZtDataPermissionAutoConfiguration.java index 1c7d9e91..6c95cc7c 100644 --- a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/config/ZtDataPermissionAutoConfiguration.java +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/config/ZtDataPermissionAutoConfiguration.java @@ -4,12 +4,15 @@ import com.zt.plat.framework.datapermission.core.aop.CompanyDataPermissionIgnore import com.zt.plat.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor; import com.zt.plat.framework.datapermission.core.aop.DeptDataPermissionIgnoreAspect; import com.zt.plat.framework.datapermission.core.db.DataPermissionRuleHandler; +import com.zt.plat.framework.datapermission.core.menudatapermission.aop.MenuDataPermissionAspect; +import com.zt.plat.framework.datapermission.core.menudatapermission.handler.MenuDataPermissionHandler; import com.zt.plat.framework.datapermission.core.rule.DataPermissionRule; import com.zt.plat.framework.datapermission.core.rule.DataPermissionRuleFactory; import com.zt.plat.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; import com.zt.plat.framework.mybatis.core.util.MyBatisUtils; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; @@ -21,6 +24,7 @@ import java.util.List; * @author ZT */ @AutoConfiguration +@MapperScan("com.zt.plat.framework.datapermission.core.menudatapermission.dal.mapper") public class ZtDataPermissionAutoConfiguration { @Bean @@ -40,6 +44,21 @@ public class ZtDataPermissionAutoConfiguration { return handler; } + @Bean + public MenuDataPermissionHandler menuDataPermissionHandler(MybatisPlusInterceptor interceptor) { + // 创建菜单数据权限处理器 + MenuDataPermissionHandler handler = new MenuDataPermissionHandler(); + DataPermissionInterceptor inner = new DataPermissionInterceptor(handler); + // 添加到 interceptor 中,放在部门数据权限之后 + MyBatisUtils.addInterceptor(interceptor, inner, 1); + return handler; + } + + @Bean + public MenuDataPermissionAspect menuDataPermissionAspect() { + return new MenuDataPermissionAspect(); + } + @Bean public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() { return new DataPermissionAnnotationAdvisor(); diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/annotation/PermissionData.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/annotation/PermissionData.java new file mode 100644 index 00000000..4eed8a18 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/annotation/PermissionData.java @@ -0,0 +1,29 @@ +package com.zt.plat.framework.datapermission.core.menudatapermission.annotation; + +import java.lang.annotation.*; + +/** + * 数据权限注解 + * 标注在Controller方法上,表示该方法需要应用菜单数据规则 + * + * 参考JeecgBoot的实现方式 + * + * @author ZT + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PermissionData { + + /** + * 页面组件路径 + * 用于匹配菜单表中的component字段 + * 例如:system/role 对应角色管理菜单 + */ + String pageComponent(); + + /** + * 是否启用 + */ + boolean enable() default true; +} diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/aop/MenuDataPermissionAspect.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/aop/MenuDataPermissionAspect.java new file mode 100644 index 00000000..acb87fb8 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/aop/MenuDataPermissionAspect.java @@ -0,0 +1,89 @@ +package com.zt.plat.framework.datapermission.core.menudatapermission.aop; + +import cn.hutool.core.collection.CollUtil; +import com.zt.plat.framework.datapermission.core.menudatapermission.annotation.PermissionData; +import com.zt.plat.framework.datapermission.core.menudatapermission.context.MenuDataRuleContextHolder; +import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO; +import com.zt.plat.framework.datapermission.core.menudatapermission.service.MenuDataRuleLoader; +import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.List; + +/** + * 菜单数据权限切面 + * 拦截 @PermissionData 注解的方法,加载并应用菜单数据规则 + * + * @author ZT + */ +@Aspect +@Component +@Slf4j +public class MenuDataPermissionAspect { + + @Resource + private MenuDataRuleLoader menuDataRuleLoader; + + @Around("@annotation(com.zt.plat.framework.datapermission.core.menudatapermission.annotation.PermissionData)") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + // 获取方法签名 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + PermissionData annotation = method.getAnnotation(PermissionData.class); + + // 如果未启用,直接执行 + if (!annotation.enable()) { + return joinPoint.proceed(); + } + + try { + // 获取当前用户ID + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (userId == null) { + log.debug("[MenuDataPermissionAspect][未登录,跳过菜单数据权限]"); + return joinPoint.proceed(); + } + + // 从注解获取pageComponent + String pageComponent = annotation.pageComponent(); + if (pageComponent == null || pageComponent.isEmpty()) { + log.warn("[MenuDataPermissionAspect][未指定pageComponent,跳过菜单数据权限]"); + return joinPoint.proceed(); + } + + // 根据pageComponent查询菜单ID + Long menuId = menuDataRuleLoader.getMenuIdByPageComponent(pageComponent); + if (menuId == null) { + log.warn("[MenuDataPermissionAspect][未找到匹配的菜单: {},跳过菜单数据权限]", pageComponent); + return joinPoint.proceed(); + } + + log.debug("[MenuDataPermissionAspect][pageComponent: {} 对应菜单ID: {}]", pageComponent, menuId); + + // 加载用户的菜单数据规则 + List rules = menuDataRuleLoader.getUserMenuDataRules(userId, menuId); + + if (CollUtil.isEmpty(rules)) { + log.debug("[MenuDataPermissionAspect][用户 {} 在菜单 {} 下无数据规则]", userId, menuId); + } else { + log.debug("[MenuDataPermissionAspect][用户 {} 在菜单 {} 下加载了 {} 条数据规则]", + userId, menuId, rules.size()); + // 将规则存入 ThreadLocal + MenuDataRuleContextHolder.setRules(rules); + } + + // 执行目标方法 + return joinPoint.proceed(); + } finally { + // 清理 ThreadLocal + MenuDataRuleContextHolder.clear(); + } + } +} diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/config/MenuDataPermissionConfiguration.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/config/MenuDataPermissionConfiguration.java new file mode 100644 index 00000000..172fba3d --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/config/MenuDataPermissionConfiguration.java @@ -0,0 +1,36 @@ +package com.zt.plat.framework.datapermission.core.menudatapermission.config; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.zt.plat.framework.datapermission.core.menudatapermission.handler.MenuDataPermissionHandler; +import com.zt.plat.framework.mybatis.core.util.MyBatisUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * 菜单数据权限配置类 + * + * @author ZT + */ +@Configuration +@ComponentScan("com.zt.plat.framework.datapermission.core.menudatapermission") +public class MenuDataPermissionConfiguration { + + @Bean + @ConditionalOnBean(MybatisPlusInterceptor.class) + public MenuDataPermissionHandler menuDataPermissionHandler(MybatisPlusInterceptor interceptor) { + // 创建菜单数据权限处理器 + MenuDataPermissionHandler handler = new MenuDataPermissionHandler(); + + // 创建 DataPermissionInterceptor 拦截器 + DataPermissionInterceptor inner = new DataPermissionInterceptor(handler); + + // 添加到 interceptor 中 + // 添加在索引1的位置,在部门数据权限之后,但在分页插件之前 + MyBatisUtils.addInterceptor(interceptor, inner, 1); + + return handler; + } +} diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/context/MenuDataRuleContextHolder.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/context/MenuDataRuleContextHolder.java new file mode 100644 index 00000000..4f2abaa8 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/context/MenuDataRuleContextHolder.java @@ -0,0 +1,41 @@ +package com.zt.plat.framework.datapermission.core.menudatapermission.context; + +import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO; + +import java.util.List; + +/** + * 菜单数据规则上下文持有者 + * 使用 ThreadLocal 存储当前请求的菜单数据规则 + * + * @author ZT + */ +public class MenuDataRuleContextHolder { + + private static final ThreadLocal> CONTEXT = new ThreadLocal<>(); + + /** + * 设置当前请求的菜单数据规则 + * + * @param rules 规则列表 + */ + public static void setRules(List rules) { + CONTEXT.set(rules); + } + + /** + * 获取当前请求的菜单数据规则 + * + * @return 规则列表 + */ + public static List getRules() { + return CONTEXT.get(); + } + + /** + * 清除当前请求的菜单数据规则 + */ + public static void clear() { + CONTEXT.remove(); + } +} diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/dal/mapper/MenuDataPermissionMapper.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/dal/mapper/MenuDataPermissionMapper.java new file mode 100644 index 00000000..0c46c8b7 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/dal/mapper/MenuDataPermissionMapper.java @@ -0,0 +1,51 @@ +package com.zt.plat.framework.datapermission.core.menudatapermission.dal.mapper; + +import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO; +import com.zt.plat.framework.tenant.core.aop.TenantIgnore; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 菜单数据权限 Mapper + * 用于查询菜单和菜单数据规则 + * + * @author ZT + */ +@Mapper +public interface MenuDataPermissionMapper { + + /** + * 根据页面组件路径获取菜单ID + * + * @param component 页面组件路径,如:system/role/index + * @return 菜单ID,如果未找到返回null + */ + @Select("SELECT id FROM system_menu WHERE component = #{component} AND deleted = 0 LIMIT 1") + Long selectMenuIdByComponent(@Param("component") String component); + + /** + * 获取用户在指定菜单下的有效数据规则 + * + * @param userId 用户ID + * @param menuId 菜单ID + * @return 数据规则列表 + */ + @TenantIgnore + @Select("") + List selectUserMenuDataRules(@Param("userId") Long userId, + @Param("menuId") Long menuId); +} diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/handler/MenuDataPermissionHandler.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/handler/MenuDataPermissionHandler.java new file mode 100644 index 00000000..c72a2b0e --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/handler/MenuDataPermissionHandler.java @@ -0,0 +1,41 @@ +package com.zt.plat.framework.datapermission.core.menudatapermission.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler; +import com.zt.plat.framework.datapermission.core.menudatapermission.util.MenuDataPermissionRule; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; + +/** + * 菜单数据权限处理器 + * 基于 MyBatis Plus 的数据权限插件,应用菜单数据规则到 SQL 查询 + * + * @author ZT + */ +@Slf4j +public class MenuDataPermissionHandler implements MultiDataPermissionHandler { + + @Override + public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) { + try { + // 从 ThreadLocal 获取菜单数据规则并构建 SQL 条件 + String sqlCondition = MenuDataPermissionRule.buildSqlCondition(); + + if (StrUtil.isBlank(sqlCondition)) { + return null; + } + + // 将 SQL 字符串解析为 Expression 对象 + Expression expression = CCJSqlParserUtil.parseCondExpression(sqlCondition); + log.debug("[MenuDataPermissionHandler][表: {}, 添加条件: {}]", table.getName(), sqlCondition); + return expression; + + } catch (JSQLParserException e) { + log.error("[MenuDataPermissionHandler][解析 SQL 条件失败]", e); + return null; + } + } +} diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/model/MenuDataRuleDTO.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/model/MenuDataRuleDTO.java new file mode 100644 index 00000000..2e6ef64b --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/model/MenuDataRuleDTO.java @@ -0,0 +1,33 @@ +package com.zt.plat.framework.datapermission.core.menudatapermission.model; + +import lombok.Data; + +/** + * 菜单数据规则 DTO + * 用于在框架层传递菜单数据规则信息,避免依赖业务模块的数据库实体 + * + * @author ZT + */ +@Data +public class MenuDataRuleDTO { + + /** + * 规则字段(数据库列名) + */ + private String ruleColumn; + + /** + * 规则条件(=、>、<、IN、LIKE等) + */ + private String ruleConditions; + + /** + * 规则值(支持变量如#{userId}、#{deptId}) + */ + private String ruleValue; + + /** + * 状态(0=禁用 1=启用) + */ + private Integer status; +} diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/MenuDataRuleLoader.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/MenuDataRuleLoader.java new file mode 100644 index 00000000..2d1efe69 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/MenuDataRuleLoader.java @@ -0,0 +1,31 @@ +package com.zt.plat.framework.datapermission.core.menudatapermission.service; + +import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO; + +import java.util.List; + +/** + * 菜单数据规则加载器接口 + * 负责加载菜单和菜单数据规则 + * + * @author ZT + */ +public interface MenuDataRuleLoader { + + /** + * 根据页面组件路径获取菜单ID + * + * @param pageComponent 页面组件路径,如:system/role/index + * @return 菜单ID,如果未找到返回null + */ + Long getMenuIdByPageComponent(String pageComponent); + + /** + * 获取用户在指定菜单下的数据规则 + * + * @param userId 用户ID + * @param menuId 菜单ID + * @return 数据规则列表 + */ + List getUserMenuDataRules(Long userId, Long menuId); +} diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/impl/MenuDataRuleLoaderImpl.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/impl/MenuDataRuleLoaderImpl.java new file mode 100644 index 00000000..57311c64 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/service/impl/MenuDataRuleLoaderImpl.java @@ -0,0 +1,45 @@ +package com.zt.plat.framework.datapermission.core.menudatapermission.service.impl; + +import com.zt.plat.framework.datapermission.core.menudatapermission.dal.mapper.MenuDataPermissionMapper; +import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO; +import com.zt.plat.framework.datapermission.core.menudatapermission.service.MenuDataRuleLoader; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 菜单数据规则加载器默认实现 + * 直接从数据库加载菜单数据规则 + * + * @author ZT + */ +@Component +@Slf4j +public class MenuDataRuleLoaderImpl implements MenuDataRuleLoader { + + @Resource + private MenuDataPermissionMapper menuDataPermissionMapper; + + @Override + public Long getMenuIdByPageComponent(String pageComponent) { + try { + return menuDataPermissionMapper.selectMenuIdByComponent(pageComponent); + } catch (Exception e) { + log.error("[MenuDataRuleLoaderImpl][根据pageComponent查询菜单ID失败: {}]", pageComponent, e); + return null; + } + } + + @Override + public List getUserMenuDataRules(Long userId, Long menuId) { + try { + return menuDataPermissionMapper.selectUserMenuDataRules(userId, menuId); + } catch (Exception e) { + log.error("[MenuDataRuleLoaderImpl][查询用户菜单数据规则失败: userId={}, menuId={}]", + userId, menuId, e); + return List.of(); + } + } +} diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/DataRuleVariableUtils.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/DataRuleVariableUtils.java new file mode 100644 index 00000000..b368db61 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/DataRuleVariableUtils.java @@ -0,0 +1,92 @@ +package com.zt.plat.framework.datapermission.core.menudatapermission.util; + +import cn.hutool.core.util.StrUtil; +import com.zt.plat.framework.security.core.LoginUser; +import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; +import lombok.extern.slf4j.Slf4j; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 数据规则变量替换工具类 + * + * @author ZT + */ +@Slf4j +public class DataRuleVariableUtils { + + private static final Pattern VARIABLE_PATTERN = Pattern.compile("#\\{([^}]+)}"); + + /** + * 替换规则值中的变量 + * + * @param ruleValue 规则值,如 "#{userId}" 或 "#{deptId}" + * @return 替换后的值 + */ + public static String replaceVariables(String ruleValue) { + if (StrUtil.isBlank(ruleValue)) { + return ruleValue; + } + + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser == null) { + return ruleValue; + } + + Matcher matcher = VARIABLE_PATTERN.matcher(ruleValue); + StringBuffer result = new StringBuffer(); + + while (matcher.find()) { + String variable = matcher.group(1); + String replacement = getVariableValue(variable, loginUser); + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + + return result.toString(); + } + + /** + * 获取变量对应的值 + * + * @param variable 变量名,如 "userId", "deptId" + * @param loginUser 当前登录用户 + * @return 变量值 + */ + private static String getVariableValue(String variable, LoginUser loginUser) { + if (loginUser == null) { + return ""; + } + + switch (variable) { + case "userId": + return loginUser.getId() != null ? loginUser.getId().toString() : ""; + case "username": + return loginUser.getInfo() != null ? + loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_USERNAME, "") : ""; + case "deptId": + return loginUser.getVisitDeptId() != null ? + loginUser.getVisitDeptId().toString() : ""; + case "companyId": + return loginUser.getVisitCompanyId() != null ? + loginUser.getVisitCompanyId().toString() : ""; + case "tenantId": + return loginUser.getTenantId() != null ? + loginUser.getTenantId().toString() : ""; + case "deptIds": + return loginUser.getInfo() != null ? + loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_DEPT_IDS, "") : ""; + case "companyIds": + return loginUser.getInfo() != null ? + loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_COMPANY_IDS, "") : ""; + case "postIds": + return loginUser.getInfo() != null ? + loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_POST_IDS, "") : ""; + default: + // 未知变量,记录警告并返回空字符串 + log.warn("[DataRuleVariableUtils][未知的变量: {},请检查数据规则配置]", variable); + return ""; + } + } +} diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/MenuDataPermissionRule.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/MenuDataPermissionRule.java new file mode 100644 index 00000000..ef62d916 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/menudatapermission/util/MenuDataPermissionRule.java @@ -0,0 +1,156 @@ +package com.zt.plat.framework.datapermission.core.menudatapermission.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.zt.plat.framework.datapermission.core.menudatapermission.context.MenuDataRuleContextHolder; +import com.zt.plat.framework.datapermission.core.menudatapermission.model.MenuDataRuleDTO; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 菜单数据权限规则 + * 用于构建 SQL WHERE 条件 + * + * @author ZT + */ +@Slf4j +public class MenuDataPermissionRule { + + /** + * 构建 SQL WHERE 条件 + * + * @return SQL 条件字符串,如 "dept_id = 1 AND status = 1" + */ + public static String buildSqlCondition() { + List rules = MenuDataRuleContextHolder.getRules(); + if (CollUtil.isEmpty(rules)) { + return null; + } + + List conditions = rules.stream() + .filter(rule -> rule.getStatus() == 1) // 只处理启用的规则 + .map(MenuDataPermissionRule::buildSingleCondition) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + + if (CollUtil.isEmpty(conditions)) { + return null; + } + + // 多个规则用 AND 连接 + return "(" + String.join(" AND ", conditions) + ")"; + } + + /** + * 构建单个规则的 SQL 条件 + * + * @param rule 规则 + * @return SQL 条件字符串 + */ + private static String buildSingleCondition(MenuDataRuleDTO rule) { + String ruleColumn = rule.getRuleColumn(); + String ruleConditions = rule.getRuleConditions(); + String ruleValue = rule.getRuleValue(); + + // 替换变量 + String actualValue = DataRuleVariableUtils.replaceVariables(ruleValue); + + if (StrUtil.isBlank(ruleColumn) || StrUtil.isBlank(ruleConditions)) { + return null; + } + + // 处理 SQL_RULE 类型(自定义 SQL) + if ("SQL_RULE".equals(ruleConditions)) { + return actualValue; + } + + // 处理 IS_NULL 和 IS_NOT_NULL + if ("IS_NULL".equals(ruleConditions)) { + return ruleColumn + " IS NULL"; + } + if ("IS_NOT_NULL".equals(ruleConditions)) { + return ruleColumn + " IS NOT NULL"; + } + + // 其他条件需要有值 + if (StrUtil.isBlank(actualValue)) { + return null; + } + + // 构建条件 + switch (ruleConditions) { + case "=": + return ruleColumn + " = " + formatValue(actualValue); + case "!=": + return ruleColumn + " != " + formatValue(actualValue); + case ">": + return ruleColumn + " > " + formatValue(actualValue); + case "<": + return ruleColumn + " < " + formatValue(actualValue); + case ">=": + return ruleColumn + " >= " + formatValue(actualValue); + case "<=": + return ruleColumn + " <= " + formatValue(actualValue); + case "IN": + return ruleColumn + " IN (" + formatInValues(actualValue) + ")"; + case "NOT_IN": + return ruleColumn + " NOT IN (" + formatInValues(actualValue) + ")"; + case "LIKE": + return ruleColumn + " LIKE '%" + escapeSql(actualValue) + "%'"; + case "NOT_LIKE": + return ruleColumn + " NOT LIKE '%" + escapeSql(actualValue) + "%'"; + case "BETWEEN": + return buildBetweenCondition(ruleColumn, actualValue); + case "NOT_BETWEEN": + return "NOT " + buildBetweenCondition(ruleColumn, actualValue); + default: + log.warn("[buildSingleCondition][未知的规则条件: {}]", ruleConditions); + return null; + } + } + + /** + * 格式化值(添加引号) + * 统一给所有值加引号,数据库会自动处理类型转换 + */ + private static String formatValue(String value) { + // 统一添加单引号,避免达梦数据库等对字符串类型字段的类型转换错误 + return "'" + escapeSql(value) + "'"; + } + + /** + * 格式化 IN 条件的值 + */ + private static String formatInValues(String value) { + String[] values = value.split(","); + return java.util.Arrays.stream(values) + .map(String::trim) + .filter(StrUtil::isNotBlank) + .map(MenuDataPermissionRule::formatValue) + .collect(Collectors.joining(", ")); + } + + /** + * 构建 BETWEEN 条件 + */ + private static String buildBetweenCondition(String column, String value) { + String[] values = value.split(","); + if (values.length != 2) { + log.warn("[buildBetweenCondition][BETWEEN 条件需要两个值,用逗号分隔: {}]", value); + return null; + } + return column + " BETWEEN " + formatValue(values[0].trim()) + " AND " + formatValue(values[1].trim()); + } + + /** + * SQL 转义,防止 SQL 注入 + */ + private static String escapeSql(String value) { + if (StrUtil.isBlank(value)) { + return value; + } + return value.replace("'", "''"); + } +} diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java index ea4a8a93..60951994 100644 --- a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java @@ -18,19 +18,24 @@ import com.zt.plat.framework.tenant.core.context.CompanyContextHolder; import com.zt.plat.framework.tenant.core.context.DeptContextHolder; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.NullValue; +import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.conditional.OrExpression; import net.sf.jsqlparser.expression.operators.relational.*; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.SelectItem; +import org.apache.commons.lang3.StringUtils; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -67,7 +72,16 @@ public class DeptDataPermissionRule implements DataPermissionRule { private static final String DEPT_COLUMN_NAME = "dept_id"; private static final String USER_COLUMN_NAME = "user_id"; - static final Expression EXPRESSION_NULL = new NullValue(); + static final Expression EXPRESSION_NULL; + + static { + try { + EXPRESSION_NULL = CCJSqlParserUtil.parseCondExpression("1 = 0"); + } catch (JSQLParserException e) { + throw new RuntimeException(e); + } + } + public static final String SYSTEM_USERS = "system_users"; private final PermissionCommonApi permissionApi; @@ -177,7 +191,9 @@ public class DeptDataPermissionRule implements DataPermissionRule { // 情况三,拼接 Dept 和 Company User 的条件,最后组合 Expression deptExpression = buildDeptExpression(tableName, tableAlias, effectiveDeptIds); // Expression deptExpression = buildDeptExpression(tableName, tableAlias, deptDataPermission.getDeptIds()); - Expression userExpression = buildUserExpression(tableName, tableAlias, effectiveSelf, loginUser.getId()); + // 使用工号替换 UserId + String userWorkCode = SecurityFrameworkUtils.getLoginUserWorkCode(); + Expression userExpression = buildUserExpression(tableName, tableAlias, effectiveSelf, loginUser.getId(), userWorkCode); if (deptExpression == null && userExpression == null) { // TODO ZT:获得不到条件的时候,暂时不抛出异常,而是不返回数据 log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]", @@ -241,7 +257,7 @@ public class DeptDataPermissionRule implements DataPermissionRule { new ParenthesedExpressionList(new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new)))); } - private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { + private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId, String workCode) { // 如果不查看自己,则无需作为条件 if (Boolean.FALSE.equals(self)) { return null; @@ -250,8 +266,13 @@ public class DeptDataPermissionRule implements DataPermissionRule { if (StrUtil.isEmpty(columnName)) { return null; } + // 拼接条件 - return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); + if (StrUtil.isNotBlank(workCode) && "system_users".equals(tableName)) { + return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, "workcode"), new StringValue(workCode)); + } else { + return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); + } } // ==================== 添加配置 ==================== diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index df6ed09f..85d1ca70 100644 --- a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -2,3 +2,4 @@ com.zt.plat.framework.datapermission.config.ZtDataPermissionAutoConfiguration com.zt.plat.framework.datapermission.config.ZtDeptDataPermissionAutoConfiguration com.zt.plat.framework.datapermission.config.ZtBusinessDataPermissionAutoConfiguration com.zt.plat.framework.datapermission.config.ZtDataPermissionRpcAutoConfiguration +com.zt.plat.framework.datapermission.core.menudatapermission.config.MenuDataPermissionConfiguration diff --git a/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java b/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java index f4688a21..56302390 100644 --- a/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java +++ b/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java @@ -11,6 +11,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.HandlerInterceptor; +import java.util.Map; + /** * @author chenbowen */ @@ -45,7 +47,6 @@ public class CompanyVisitContextInterceptor implements HandlerInterceptor { } Long deptId = WebFrameworkUtils.getDeptId(request); - // 部门信息同样遵循“请求头 -> 请求属性 -> 登录缓存”的回退顺序 if (deptId == null || deptId <= 0L) { Long attrDeptId = resolveLong(request.getAttribute(WebFrameworkUtils.HEADER_VISIT_DEPT_ID)); if (attrDeptId != null && attrDeptId > 0L) { diff --git a/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/handler/DefaultDBFieldHandler.java b/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/handler/DefaultDBFieldHandler.java index ec449926..b3306ac0 100644 --- a/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/handler/DefaultDBFieldHandler.java +++ b/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/handler/DefaultDBFieldHandler.java @@ -11,6 +11,7 @@ import com.zt.plat.framework.security.core.LoginUser; import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; import com.zt.plat.framework.web.core.util.WebFrameworkUtils; import jakarta.annotation.Resource; +import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.reflection.MetaObject; import org.springframework.context.annotation.Lazy; import org.springframework.util.ReflectionUtils; @@ -48,14 +49,16 @@ public class DefaultDBFieldHandler implements MetaObjectHandler { } Long userId = getUserId(); + String userWorkCode = SecurityFrameworkUtils.getLoginUserWorkCode(); + String savedUserWorkCodeOrUserId = StringUtils.isNotEmpty(userWorkCode) ? userWorkCode : userId == null ? null : userId.toString(); String userNickname = SecurityFrameworkUtils.getLoginUserNickname(); // 当前登录用户不为空,创建人为空,则当前登录用户为创建人 if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) { - baseDO.setCreator(userId.toString()); + baseDO.setCreator(savedUserWorkCodeOrUserId); } // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) { - baseDO.setUpdater(userId.toString()); + baseDO.setUpdater(savedUserWorkCodeOrUserId); } } if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BusinessBaseDO businessBaseDO) { diff --git a/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/query/LambdaQueryWrapperX.java b/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/query/LambdaQueryWrapperX.java index 329bcec9..8f1de9f4 100644 --- a/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/query/LambdaQueryWrapperX.java +++ b/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/query/LambdaQueryWrapperX.java @@ -53,6 +53,20 @@ public class LambdaQueryWrapperX extends LambdaQueryWrapper { return this; } + public LambdaQueryWrapperX eqIfNotBlank(SFunction column, String val) { + if (StringUtils.hasText(val)) { + return (LambdaQueryWrapperX) super.eq(column, val); + } + return this; + } + + public LambdaQueryWrapperX neIfNotBlank(SFunction column, String val) { + if (StringUtils.hasText(val)) { + return (LambdaQueryWrapperX) super.ne(column, val); + } + return this; + } + public LambdaQueryWrapperX gtIfPresent(SFunction column, Object val) { if (val != null) { return (LambdaQueryWrapperX) super.gt(column, val); diff --git a/zt-framework/zt-spring-boot-starter-security/src/main/java/com/zt/plat/framework/security/core/LoginUser.java b/zt-framework/zt-spring-boot-starter-security/src/main/java/com/zt/plat/framework/security/core/LoginUser.java index f9b739dd..cf026e49 100644 --- a/zt-framework/zt-spring-boot-starter-security/src/main/java/com/zt/plat/framework/security/core/LoginUser.java +++ b/zt-framework/zt-spring-boot-starter-security/src/main/java/com/zt/plat/framework/security/core/LoginUser.java @@ -31,6 +31,9 @@ public class LoginUser { // 用户关联的岗位信息 public static final String INFO_KEY_POST_IDS = "postIds"; + // 工号 + public static final String INFO_KEY_WORK_CODE = "workCode"; + /** * 用户编号 */ diff --git a/zt-framework/zt-spring-boot-starter-security/src/main/java/com/zt/plat/framework/security/core/util/SecurityFrameworkUtils.java b/zt-framework/zt-spring-boot-starter-security/src/main/java/com/zt/plat/framework/security/core/util/SecurityFrameworkUtils.java index 48a2bac1..e9efb450 100644 --- a/zt-framework/zt-spring-boot-starter-security/src/main/java/com/zt/plat/framework/security/core/util/SecurityFrameworkUtils.java +++ b/zt-framework/zt-spring-boot-starter-security/src/main/java/com/zt/plat/framework/security/core/util/SecurityFrameworkUtils.java @@ -15,6 +15,7 @@ import org.springframework.security.web.authentication.WebAuthenticationDetailsS import org.springframework.util.StringUtils; import java.util.Collections; +import java.util.Map; /** * 安全服务工具类 @@ -93,6 +94,19 @@ public class SecurityFrameworkUtils { return loginUser != null ? loginUser.getVisitCompanyId() : null; } + @Nullable + public static String getLoginUserWorkCode() { + LoginUser loginUser = getLoginUser(); + if (loginUser == null) { + return null; + } + Map info = loginUser.getInfo(); + if (info == null) { + return null; + } + return MapUtil.getStr(info, LoginUser.INFO_KEY_WORK_CODE); + } + /** * 获得当前用户的编号,从上下文中 * diff --git a/zt-module-databus/pom.xml b/zt-module-databus/pom.xml index 904627a5..c06c914b 100644 --- a/zt-module-databus/pom.xml +++ b/zt-module-databus/pom.xml @@ -11,6 +11,7 @@ zt-module-databus-api zt-module-databus-server zt-module-databus-server-app + zt-module-databus-client 4.0.0 diff --git a/zt-module-databus/zt-module-databus-api/src/main/java/com/zt/plat/module/databus/api/dto/ApiAccessLogCreateReq.java b/zt-module-databus/zt-module-databus-api/src/main/java/com/zt/plat/module/databus/api/dto/ApiAccessLogCreateReq.java new file mode 100644 index 00000000..fb3536bb --- /dev/null +++ b/zt-module-databus/zt-module-databus-api/src/main/java/com/zt/plat/module/databus/api/dto/ApiAccessLogCreateReq.java @@ -0,0 +1,152 @@ +package com.zt.plat.module.databus.api.dto; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.zt.plat.framework.common.util.json.databind.TimestampLocalDateTimeSerializer; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 新增Databus API 访问日志请求体。 + */ +@Data +@EqualsAndHashCode +public class ApiAccessLogCreateReq { + + /** + * 主键 + */ + @Schema(description = "主键") + private Long id; + + /** + * HTTP 方法 + */ + @Schema(description = "HTTP 方法") + @NotNull(message = "HTTP 方法不能为空") + private String requestMethod; + + /** + * 请求路径 + */ + @Schema(description = "请求路径") + @NotNull(message = "请求路径不能为空") + private String requestPath; + + /** + * 调用使用的应用标识 + */ + @Schema(description = "调用使用的应用标识") + @NotNull(message = "调用使用的应用标识不能为空") + private String credentialAppId; + + /** + * 多租户编号 + */ + @Schema(description = "多租户编号") + @NotNull(message = "多租户编号不能为空") + private Long tenantId; + + /** + * 查询参数(JSON 字符串) + */ + @Schema(description = "查询参数(JSON 字符串)") + private String requestQuery; + + /** + * 请求头信息(JSON 字符串) + */ + @Schema(description = "请求头信息(JSON 字符串)") + private String requestHeaders; + + /** + * 请求体(JSON 字符串) + */ + @Schema(description = "请求体(JSON 字符串)") + private String requestBody; + + /** + * 响应 HTTP 状态码 + */ + @Schema(description = "响应 HTTP 状态码") + private Integer responseStatus; + + /** + * 响应提示信息 + */ + @Schema(description = "响应提示信息") + private String responseMessage; + + /** + * 响应体(JSON 字符串) + */ + @Schema(description = "响应体(JSON 字符串)") + private String responseBody; + + /** + * 访问状态:0-成功 1-客户端错误 2-服务端错误 3-未知 + */ + @Schema(description = "访问状态:0-成功 1-客户端错误 2-服务端错误 3-未知") + @NotNull(message = "访问状态不能为空") + private Integer status; + + /** + * 业务错误码 + */ + @Schema(description = "业务错误码") + private String errorCode; + + /** + * 错误信息 + */ + @Schema(description = "错误信息") + private String errorMessage; + + /** + * 异常堆栈 + */ + @Schema(description = "异常堆栈") + private String exceptionStack; + + /** + * 客户端 IP + */ + @Schema(description = "客户端 IP") + private String clientIp; + + /** + * User-Agent + */ + @Schema(description = "User-Agent") + private String userAgent; + + /** + * 请求耗时(毫秒) + */ + @Schema(description = "请求耗时(毫秒)") + private Long duration; + + /** + * 请求时间 + */ + @Schema(description = "请求时间") + @JsonSerialize(using = TimestampLocalDateTimeSerializer.class) + private LocalDateTime requestTime; + + /** + * 响应时间 + */ + @Schema(description = "响应时间") + @JsonSerialize(using = TimestampLocalDateTimeSerializer.class) + private LocalDateTime responseTime; + + /** + * 额外调试信息(JSON 字符串) + */ + @Schema(description = "额外调试信息(JSON 字符串)") + private String extra; + +} diff --git a/zt-module-databus/zt-module-databus-api/src/main/java/com/zt/plat/module/databus/api/provider/DatabusAccessLogProviderApi.java b/zt-module-databus/zt-module-databus-api/src/main/java/com/zt/plat/module/databus/api/provider/DatabusAccessLogProviderApi.java new file mode 100644 index 00000000..a2f80c6e --- /dev/null +++ b/zt-module-databus/zt-module-databus-api/src/main/java/com/zt/plat/module/databus/api/provider/DatabusAccessLogProviderApi.java @@ -0,0 +1,28 @@ +package com.zt.plat.module.databus.api.provider; + +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.module.databus.api.dto.ApiAccessLogCreateReq; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; + +import java.util.Map; + +/** + * Databus API访问日志接口 + * 2026/1/20 16:26 + */ +@FeignClient(name = "${databus.provider.log.service:databus-server}") +@Tag(name = "RPC 服务 - Databus API访问日志接口") +public interface DatabusAccessLogProviderApi { + + String PREFIX = "/databus/api/portal/access-log"; + + @PostMapping(PREFIX + "/add") + @Operation(summary = "新增访问日志") + CommonResult add(@RequestHeader Map headers, @RequestBody ApiAccessLogCreateReq req); + +} diff --git a/zt-module-databus/zt-module-databus-client/pom.xml b/zt-module-databus/zt-module-databus-client/pom.xml new file mode 100644 index 00000000..4ee8c93f --- /dev/null +++ b/zt-module-databus/zt-module-databus-client/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + zt-module-databus + com.zt.plat + ${revision} + + zt-module-databus-client + jar + ${project.artifactId} + + Databus client, 提供调用第三方服务的能力并记录调用日志。 + + + + + + + com.zt.plat + zt-module-databus-api + ${revision} + + + + cn.hutool + hutool-all + + + + org.springframework.boot + spring-boot-starter-web + provided + + + + org.springframework.boot + spring-boot-starter-aop + provided + + + + org.apache.commons + commons-lang3 + + + + com.github.ben-manes.caffeine + caffeine + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + diff --git a/zt-module-databus/zt-module-databus-client/src/main/java/com/zt/plat/module/databus/client/DatabusClient.java b/zt-module-databus/zt-module-databus-client/src/main/java/com/zt/plat/module/databus/client/DatabusClient.java new file mode 100644 index 00000000..0954ef21 --- /dev/null +++ b/zt-module-databus/zt-module-databus-client/src/main/java/com/zt/plat/module/databus/client/DatabusClient.java @@ -0,0 +1,241 @@ +package com.zt.plat.module.databus.client; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.hutool.http.Method; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.module.databus.api.dto.ApiAccessLogCreateReq; +import com.zt.plat.module.databus.api.provider.DatabusAccessLogProviderApi; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * 数据总线提供的 http 客户端, 通过此客户端发起接口调用,会自动记录请求日志到数据总线 + * 2026/1/20 09:44 + */ +@Component +@Slf4j +public class DatabusClient { + + /** + * 多租户编号 + */ + @Value("${zt.plat.databus.client.tenantId:1}") + private Long tenantId; + + @Resource + private DatabusAccessLogProviderApi databusAccessLogProviderApi; + + private static final int MAX_TEXT_LENGTH = 4000; + + /** + * 发送 get 请求 + * @param urlString 仅接口地址,不带参数,参数由data提供 + * @param data 请求参数 + * @param headers 请求头 + * @return 响应结果 + */ + public String get(String urlString, Map data, Map headers, String appId, String authToken) { + return doRequest(urlString, data, headers, Method.GET, appId, authToken); + } + + /** + * 发送 post 请求 + * @param urlString 仅接口地址,不带参数,参数由data提供 + * @param data 请求数据 + * @param headers 请求头 + * @return 响应结果 + */ + public String post(String urlString, Map data, Map headers, String appId, String authToken) { + return doRequest(urlString, data, headers, Method.POST, appId, authToken); + } + + /** + * 发送 put 请求 + * @param urlString 仅接口地址,不带参数,参数由data提供 + * @param data 请求数据 + * @param headers 请求头 + * @return 响应结果 + */ + public String put(String urlString, Map data, Map headers, String appId, String authToken) { + return doRequest(urlString, data, headers, Method.PUT, appId, authToken); + } + + /** + * 发送 delete 请求 + * @param urlString 仅接口地址,不带参数,参数由data提供 + * @param data 请求数据 + * @param headers 请求头 + * @return 响应结果 + */ + public String delete(String urlString, Map data, Map headers, String appId, String authToken) { + return doRequest(urlString, data, headers, Method.DELETE, appId, authToken); + } + + + /** + * 发送请求到门户(token模式,不证书加密) + * @param urlString 仅接口地址,不带参数,参数由data提供 + * @param data 请求数据 + * @param headers 请求头 + * @param method 请求方式,默认为 GET + * @return 响应结果 + */ + public String doRequest(String urlString, Map data, Map headers, Method method, String appId, String authToken) { + if (method == null) { + method = Method.GET; + } + Assert.hasText(urlString, "接口地址不能为空"); + HttpRequest request; + ApiAccessLogCreateReq logReq = new ApiAccessLogCreateReq(); + if (Method.GET.equals(method) || Method.DELETE.equals(method)) { + logReq.setRequestQuery(JSONUtil.toJsonStr(data)); + } else { + logReq.setRequestBody(JSONUtil.toJsonStr(data)); + } + request = HttpUtil.createRequest(method, urlString).form(data); + if (headers != null && !headers.isEmpty()) { + for (Map.Entry entry : headers.entrySet()) { + request.header(entry.getKey(), entry.getValue(), true); + } + } + + logReq.setTenantId(tenantId); + + logReq.setRequestMethod(method.name()); + logReq.setRequestPath(urlString); + logReq.setCredentialAppId(appId); + logReq.setRequestHeaders(JSONUtil.toJsonStr(headers)); + logReq.setUserAgent(request.header("User-Agent")); + String result; + logReq.setRequestTime(LocalDateTime.now()); + long requestTime = System.currentTimeMillis(); + try (HttpResponse response = request.execute()) { + logReq.setDuration(System.currentTimeMillis() - requestTime); + logReq.setResponseTime(LocalDateTime.now()); + result = response.body(); + logReq.setResponseStatus(response.getStatus()); + logReq.setResponseBody(result); + logReq.setStatus(resolveStatus(response.getStatus())); + Map errorCodeAndMsg = extractErrorCodeAndMsg(result, response.getStatus()); + logReq.setErrorCode(errorCodeAndMsg.get("errorCode")); + logReq.setErrorMessage(errorCodeAndMsg.get("errorMessage")); + addAccessLog(logReq, appId, authToken); + } catch (Exception e) { + // 错误的日志服务端记录了,这里就不再记录了 +// logReq.setStatus(1); +// logReq.setExceptionStack(buildStackTrace( e)); +// addAccessLog(logReq, appId, authToken); + throw new RuntimeException(e); + } + return result; + } + + private void addAccessLog(ApiAccessLogCreateReq logReq, String appId, String authToken) { + String nonce = randomNonce(); + Map headers = new HashMap<>(); + headers.put("tenant-id", String.valueOf(tenantId)); + headers.put("ZT-App-Id", appId); + headers.put("ZT-Timestamp", Long.toString(System.currentTimeMillis())); + headers.put("ZT-Nonce", nonce); + headers.put("ZT-Auth-Token", authToken); + CommonResult response = databusAccessLogProviderApi.add(headers, logReq); + if (response.getCode() != 0) { + throw new RuntimeException("添加访问日志失败: " + response); + } + } + + private static String randomNonce() { + return UUID.randomUUID().toString().replace("-", ""); + } + + private String buildStackTrace(Throwable throwable) { + try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) { + throwable.printStackTrace(pw); + return truncate(sw.toString()); + } catch (Exception ex) { + return throwable.getMessage(); + } + } + + private Integer resolveStatus(Integer httpStatus) { + if (httpStatus == null) { + return 3; + } + if (httpStatus >= 200 && httpStatus < 400) { + return 0; + } + if (httpStatus >= 400 && httpStatus < 500) { + return 1; + } + if (httpStatus >= 500) { + return 2; + } + return 3; + } + + private Map extractErrorCodeAndMsg(String responseBody, Integer responseStatus) { + Map result = new HashMap<>(); + if (!isErrorStatus(responseStatus)) { + return result; + } + if (JSONUtil.isTypeJSONObject(responseBody)) { + JSONObject map = JSONUtil.parseObj(responseBody); + Object errorCode = firstNonNull(map.get("errorCode"), map.get("code")); + errorCode = errorCode == null ? null : truncate(String.valueOf(errorCode)); + if (errorCode != null) { + result.put("errorCode", errorCode.toString()); + } + Object message = firstNonNull(map.get("errorMessage"), map.get("message")); + if (message != null) { + message = truncate(String.valueOf(message)); + result.put("errorMessage", message.toString()); + } + + } + return result; + } + + + private boolean isErrorStatus(Integer responseStatus) { + return responseStatus != null && responseStatus >= 400; + } + + @SafeVarargs + private T firstNonNull(T... candidates) { + if (candidates == null) { + return null; + } + for (T candidate : candidates) { + if (candidate != null) { + return candidate; + } + } + return null; + } + + private String truncate(String text) { + if (!StringUtils.hasText(text)) { + return text; + } + if (text.length() <= MAX_TEXT_LENGTH) { + return text; + } + return text.substring(0, MAX_TEXT_LENGTH); + } + +} diff --git a/zt-module-databus/zt-module-databus-client/src/main/java/com/zt/plat/module/databus/client/RpcConfiguration.java b/zt-module-databus/zt-module-databus-client/src/main/java/com/zt/plat/module/databus/client/RpcConfiguration.java new file mode 100644 index 00000000..08c69d96 --- /dev/null +++ b/zt-module-databus/zt-module-databus-client/src/main/java/com/zt/plat/module/databus/client/RpcConfiguration.java @@ -0,0 +1,16 @@ +package com.zt.plat.module.databus.client; + +/** + * + * 2026/1/21 10:48 + */ + +import com.zt.plat.module.databus.api.provider.DatabusAccessLogProviderApi; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableFeignClients(clients = {DatabusAccessLogProviderApi.class}) +public class RpcConfiguration { + +} diff --git a/zt-module-databus/zt-module-databus-client/src/main/resources/META-INF/spring.factories b/zt-module-databus/zt-module-databus-client/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..5fe820ee --- /dev/null +++ b/zt-module-databus/zt-module-databus-client/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.zt.plat.module.databus.client.DatabusClient,\ +com.zt.plat.module.databus.client.RpcConfiguration \ No newline at end of file diff --git a/zt-module-databus/zt-module-databus-client/src/test/java/com/zt/plat/module/databus/DatabusClientTest.java b/zt-module-databus/zt-module-databus-client/src/test/java/com/zt/plat/module/databus/DatabusClientTest.java new file mode 100644 index 00000000..b26c74d8 --- /dev/null +++ b/zt-module-databus/zt-module-databus-client/src/test/java/com/zt/plat/module/databus/DatabusClientTest.java @@ -0,0 +1,24 @@ +package com.zt.plat.module.databus; + +import com.zt.plat.module.databus.client.DatabusClient; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * + * 2026/1/20 14:29 + */ +@SpringBootTest(classes = TestApplication.class) +public class DatabusClientTest { + + @Autowired + private DatabusClient databusClient; + + @Test + void test() { + String result = databusClient.get("https://www.baidu.com/", null, null, "jwyw2", "a5d7cf609c0b47038ea405c660726ee9"); + System.out.println(result); + } + +} diff --git a/zt-module-databus/zt-module-databus-client/src/test/java/com/zt/plat/module/databus/TestApplication.java b/zt-module-databus/zt-module-databus-client/src/test/java/com/zt/plat/module/databus/TestApplication.java new file mode 100644 index 00000000..fa8ca7d9 --- /dev/null +++ b/zt-module-databus/zt-module-databus-client/src/test/java/com/zt/plat/module/databus/TestApplication.java @@ -0,0 +1,26 @@ +package com.zt.plat.module.databus; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * + * 2026/1/20 14:26 + */ +@SpringBootTest +@SpringBootApplication(exclude = { + DataSourceAutoConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class, + HibernateJpaAutoConfiguration.class, + JdbcTemplateAutoConfiguration.class, +}) +public class TestApplication { + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } +} diff --git a/zt-module-databus/zt-module-databus-client/src/test/resources/application.yaml b/zt-module-databus/zt-module-databus-client/src/test/resources/application.yaml new file mode 100644 index 00000000..a47ddc71 --- /dev/null +++ b/zt-module-databus/zt-module-databus-client/src/test/resources/application.yaml @@ -0,0 +1,11 @@ +spring: + cloud: + nacos: + server-addr: 172.16.46.63:30848 # Nacos 服务器地址 + username: nacos # Nacos 账号 + password: P@ssword25 # Nacos 密码 + discovery: # 【配置中心】配置项 + namespace: klw # 命名空间。这里使用 maven Profile 资源过滤进行动态替换 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + metadata: + version: 1.0.0 # 服务实例的版本号,可用于灰度发布 \ No newline at end of file diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/api/DatabusAccessLogProviderApiImpl.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/api/DatabusAccessLogProviderApiImpl.java new file mode 100644 index 00000000..a700c240 --- /dev/null +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/api/DatabusAccessLogProviderApiImpl.java @@ -0,0 +1,45 @@ +package com.zt.plat.module.databus.api; + +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.framework.common.util.monitor.TracerUtils; +import com.zt.plat.framework.common.util.object.BeanUtils; +import com.zt.plat.framework.common.util.servlet.ServletUtils; +import com.zt.plat.module.databus.api.dto.ApiAccessLogCreateReq; +import com.zt.plat.module.databus.api.provider.DatabusAccessLogProviderApi; +import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO; +import com.zt.plat.module.databus.service.gateway.ApiAccessLogService; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.Map; + +import static com.zt.plat.framework.common.pojo.CommonResult.success; + +/** + * + * 2026/1/21 10:05 + */ +@RestController +public class DatabusAccessLogProviderApiImpl implements DatabusAccessLogProviderApi { + + @Resource + private ApiAccessLogService apiAccessLogService; + + @Override + @PermitAll + public CommonResult add(Map headers, ApiAccessLogCreateReq req) { + ApiAccessLogDO logDO = new ApiAccessLogDO(); + BeanUtils.copyProperties(req, logDO); + logDO.setTraceId(TracerUtils.getTraceId()); + logDO.setClientIp(ServletUtils.getClientIP()); + logDO.setCreateTime(LocalDateTime.now()); + logDO.setUpdateTime(LocalDateTime.now()); + logDO.setCreator("1"); + logDO.setUpdater("1"); + + apiAccessLogService.create(logDO); + return success(Boolean.TRUE); + } +} diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/config/GatewayWebClientConfiguration.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/config/GatewayWebClientConfiguration.java index 96ba5a07..24811a53 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/config/GatewayWebClientConfiguration.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/config/GatewayWebClientConfiguration.java @@ -27,7 +27,7 @@ public class GatewayWebClientConfiguration { @Value("${databus.gateway.web-client.max-in-memory-size:20971520}") int maxInMemorySize, @Value("${databus.gateway.web-client.max-idle-time:45000}") long maxIdleTimeMillis, @Value("${databus.gateway.web-client.evict-in-background-interval:20000}") long evictInBackgroundMillis, - @Value("${databus.gateway.web-client.connection-pool-enabled:false}") boolean connectionPoolEnabled) { + @Value("${databus.gateway.web-client.connection-pool-enabled:true}") boolean connectionPoolEnabled) { this.maxInMemorySize = maxInMemorySize; this.maxIdleTimeMillis = maxIdleTimeMillis; this.evictInBackgroundMillis = evictInBackgroundMillis; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java index 741db3f2..d56e56a6 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java @@ -159,6 +159,7 @@ public class ApiGatewayAccessLogger { update.setStatus(resolveStatus(status)); update.setResponseTime(LocalDateTime.now()); update.setDuration(calculateDuration(request)); + update.setUpdater("1"); apiAccessLogService.update(update); } catch (Exception ex) { log.warn("更新入口 API 访问日志失败, logId={}", logId, ex); diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java index 14a5ed11..e80c29ce 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayExecutionService.java @@ -50,6 +50,7 @@ public class ApiGatewayExecutionService { private static final String HEADER_REMOTE_ADDRESS = org.springframework.integration.http.HttpHeaders.PREFIX + "remoteAddress"; private static final String LOCAL_DEBUG_REMOTE_ADDRESS = "127.0.0.1"; private static final String ATTR_DEBUG_INVOKE = "gatewayDebugInvoke"; + private static final String ATTR_API_AGGREGATE = "apiDefinitionAggregate"; private final ApiGatewayRequestMapper requestMapper; private final ApiFlowDispatcher apiFlowDispatcher; @@ -78,17 +79,26 @@ public class ApiGatewayExecutionService { ApiInvocationContext context = message.getPayload(); accessLogger.onRequest(context); ApiInvocationContext responseContext; - ApiDefinitionAggregate debugAggregate = null; + ApiDefinitionAggregate aggregate = null; try { enforceCredentialAuthorization(context); if (Boolean.TRUE.equals(context.getAttributes().get(ATTR_DEBUG_INVOKE))) { - debugAggregate = resolveDebugAggregate(context); + aggregate = resolveDebugAggregate(context); + } else { + // 对于非调试调用,也需要获取 aggregate 以便后续应用响应模板 + aggregate = apiDefinitionService.findByCodeAndVersion(context.getApiCode(), context.getApiVersion()) + .orElse(null); } - if (debugAggregate != null) { - responseContext = apiFlowDispatcher.dispatchWithAggregate(debugAggregate, context); + // 将 aggregate 存储到上下文中,供响应构建时使用 + if (aggregate != null) { + context.getAttributes().put(ATTR_API_AGGREGATE, aggregate); + } + if (aggregate != null && Boolean.TRUE.equals(context.getAttributes().get(ATTR_DEBUG_INVOKE))) { + responseContext = apiFlowDispatcher.dispatchWithAggregate(aggregate, context); } else { responseContext = apiFlowDispatcher.dispatch(context.getApiCode(), context.getApiVersion(), context); } + responseContext.setResponseStatus(resolveStatus(responseContext)); } catch (ServiceException ex) { errorProcessor.applyServiceException(context, ex); accessLogger.onException(context, ex); @@ -239,12 +249,64 @@ public class ApiGatewayExecutionService { String message = StringUtils.hasText(context.getResponseMessage()) ? context.getResponseMessage() : HttpStatus.valueOf(status).getReasonPhrase(); - return ApiGatewayResponse.builder() - .code(status) - .message(message) - .response(context.getResponseBody()) - .traceId(TracerUtils.getTraceId()) - .build(); + + // 尝试从上下文中获取 API 定义聚合对象 + ApiDefinitionAggregate aggregate = (ApiDefinitionAggregate) context.getAttributes().get(ATTR_API_AGGREGATE); + String responseTemplate = aggregate != null ? aggregate.getDefinition().getResponseTemplate() : null; + + // 如果配置了响应模板,则应用模板;否则使用默认格式(向后兼容) + if (StringUtils.hasText(responseTemplate)) { + return buildResponseWithTemplate(responseTemplate, context, status, message); + } + + // 默认响应格式(向后兼容) + return ApiGatewayResponse.builder() + .code(status) + .message(message) + .response(context.getResponseBody()) + .traceId(TracerUtils.getTraceId()) + .build(); + } + + /** + * 根据响应模板构建响应对象 + * 模板格式示例:{"code": 0, "message": "success", "data": {...}} + * 模板中的 data 字段会被实际响应数据替换,其他字段保持用户配置的原始值 + */ + private ApiGatewayResponse buildResponseWithTemplate(String templateJson, ApiInvocationContext context, int status, String message) { + try { + // 解析模板 JSON + Map template = objectMapper.readValue(templateJson, Map.class); + + // 构建最终响应数据,保留模板中的所有字段 + Map responseData = new LinkedHashMap<>(template); + + // 只替换 data 字段为实际的响应体数据 + // 其他字段(如 code、message)保持用户配置的原始值 + if (responseData.containsKey("data")) { + responseData.put("data", context.getResponseBody()); + } + + // 返回使用模板结构的响应 + // 注意:ApiGatewayResponse 的 code 和 message 是 HTTP 层面的状态 + // responseData 中的 code 和 message 是业务层面的状态 + return ApiGatewayResponse.builder() + .code(status) + .message(message) + .response(responseData) + .traceId(TracerUtils.getTraceId()) + .build(); + + } catch (JsonProcessingException ex) { + log.warn("[API-PORTAL] 解析响应模板失败,使用默认格式: {}", ex.getMessage()); + // 解析失败时回退到默认格式 + return ApiGatewayResponse.builder() + .code(status) + .message(message) + .response(context.getResponseBody()) + .traceId(TracerUtils.getTraceId()) + .build(); + } } /** diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiDefinitionAggregate.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiDefinitionAggregate.java index 730bc36a..a11321d1 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiDefinitionAggregate.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/domain/ApiDefinitionAggregate.java @@ -40,4 +40,23 @@ public class ApiDefinitionAggregate { return credentialBindings == null ? Collections.emptyList() : credentialBindings; } + /** + * 获取最后一个 HTTP 步骤的 ID(order 最大的 HTTP 步骤) + * 用于判断是否需要设置 responseBody + */ + public Long getLastHttpStepId() { + return getSteps().stream() + .filter(stepDef -> "HTTP".equalsIgnoreCase(stepDef.getStep().getType())) + .max((s1, s2) -> { + Integer order1 = s1.getStep().getStepOrder(); + Integer order2 = s2.getStep().getStepOrder(); + if (order1 == null && order2 == null) return 0; + if (order1 == null) return -1; + if (order2 == null) return 1; + return order1.compareTo(order2); + }) + .map(stepDef -> stepDef.getStep().getId()) + .orElse(null); + } + } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/GatewayExpressionHelper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/GatewayExpressionHelper.java index 425357f5..f61d47c9 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/GatewayExpressionHelper.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/GatewayExpressionHelper.java @@ -96,18 +96,50 @@ public final class GatewayExpressionHelper { } private static void applyRequestMutations(ApiInvocationContext context, Map map) { - Object body = firstNonNull(map.get("requestBody"), map.get("body")); - if (body != null) { - context.setRequestBody(body); - } - Object headers = map.get("requestHeaders"); - if (headers instanceof Map headerMap) { - context.getRequestHeaders().putAll(toStringMap(headerMap)); - } + // 处理查询参数 Object query = firstNonNull(map.get("requestQuery"), map.get("requestQueryParams"), map.get("query")); if (query instanceof Map queryMap) { context.getRequestQueryParams().putAll(toObjectMap(queryMap)); } + + // 处理请求头 + Object headers = map.get("requestHeaders"); + if (headers instanceof Map headerMap) { + context.getRequestHeaders().putAll(toStringMap(headerMap)); + } + + // 处理 body - 使用和 HTTP 步骤一致的逻辑 + // 检查是否显式指定了 body 字段 + boolean explicitBody = containsKeyIgnoreCase(map, "body", "payload", "requestBody"); + + if (explicitBody) { + // 如果显式指定了 body,提取并与原有 body 合并 + Object body = firstNonNull(map.get("requestBody"), map.get("body"), map.get("payload")); + Object existingBody = context.getRequestBody(); + + if (body instanceof Map && existingBody instanceof Map) { + // 两者都是 Map,合并以保留未映射的字段 + Map mergedBody = new LinkedHashMap<>(toObjectMap((Map) existingBody)); + mergedBody.putAll(toObjectMap((Map) body)); + context.setRequestBody(mergedBody); + } else if (body != null) { + // 直接替换 + context.setRequestBody(body); + } + } else if (query == null) { + // 如果没有显式 body 字段,也没有 query 字段 + // 说明整个 map 是新的 body,与原有 body 合并 + Object existingBody = context.getRequestBody(); + if (existingBody instanceof Map) { + Map mergedBody = new LinkedHashMap<>(toObjectMap((Map) existingBody)); + mergedBody.putAll(toObjectMap(map)); + context.setRequestBody(mergedBody); + } else { + // 原有 body 不是 Map,直接替换 + context.setRequestBody(toObjectMap(map)); + } + } + // 如果有 query 字段但没有显式 body 字段,保留原有 body(不做任何操作) } private static void applyResponseMutations(ApiInvocationContext context, Map map) { @@ -165,4 +197,18 @@ public final class GatewayExpressionHelper { } return null; } + + private static boolean containsKeyIgnoreCase(Map source, String... keys) { + if (source == null || source.isEmpty()) { + return false; + } + for (String key : keys) { + for (Object entryKey : source.keySet()) { + if (key.equalsIgnoreCase(String.valueOf(entryKey))) { + return true; + } + } + } + return false; + } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/JsonataExpressionEvaluator.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/JsonataExpressionEvaluator.java index a4247866..1fccfa76 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/JsonataExpressionEvaluator.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/expression/JsonataExpressionEvaluator.java @@ -43,6 +43,13 @@ public class JsonataExpressionEvaluator implements ExpressionEvaluator { environment.setVariable("vars", objectMapper.valueToTree(context.getVariables() == null ? java.util.Collections.emptyMap() : context.getVariables())); environment.setVariable("headers", objectMapper.valueToTree(context.getHeaders() == null ? java.util.Collections.emptyMap() : context.getHeaders())); environment.setVariable("ctx", objectMapper.valueToTree(context.getInvocation())); + + // 直接绑定查询参数,方便访问 + if (context.getInvocation() != null && context.getInvocation().getRequestQueryParams() != null) { + environment.setVariable("query", objectMapper.valueToTree(context.getInvocation().getRequestQueryParams())); + } else { + environment.setVariable("query", objectMapper.valueToTree(java.util.Collections.emptyMap())); + } } catch (EvaluateRuntimeException e) { throw ServiceExceptionUtil.exception(API_JSONATA_BIND_FAILED); } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java index 7bd4e98f..d0839a7d 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java @@ -41,6 +41,7 @@ import java.time.Duration; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import static com.zt.plat.framework.common.util.security.CryptoSignatureUtils.SIGNATURE_FIELD; import static com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties.*; @@ -76,6 +77,9 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); return; } + + // 添加日志接口需要特殊处理 + boolean isNormalProcess = !pathMatcher.match("/databus/api/portal/access-log/**", pathWithinApplication); Long accessLogId = null; ApiGatewayProperties.Security security = properties.getSecurity(); ApiClientCredentialDO credential = null; @@ -94,8 +98,10 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { accessLogger.finalizeEarly(request, HttpStatus.FORBIDDEN.value(), "IP 禁止访问"); return; } - // IP 校验通过后再补录入口日志,避免无租户信息写库 - accessLogId = accessLogger.logEntrance(request); + // IP 校验通过后再补录入口日志,避免无租户信息写库, 非日志添加接口才记录日志 + if (isNormalProcess) { + accessLogId = accessLogger.logEntrance(request); + } if (!security.isEnabled()) { byte[] originalBody = StreamUtils.copyToByteArray(request.getInputStream()); CachedBodyHttpServletRequest passthroughRequest = new CachedBodyHttpServletRequest(request, originalBody); @@ -127,13 +133,17 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { if (nonce.length() < 8) { throw new SecurityValidationException(HttpStatus.BAD_REQUEST, "随机数长度不足"); } - String signature = requireHeader(request, SIGNATURE_HEADER, "缺少签名"); - // 尝试按凭证配置解密请求体,并构建签名载荷进行校验 - byte[] decryptedBody = decryptRequestBody(requestBody, credential, security); - verifySignature(request, decryptedBody, signature, credential, security, appId, timestampHeader); + if (isNormalProcess) { + // 非日志添加接口才处理 + String signature = requireHeader(request, SIGNATURE_HEADER, "缺少签名"); + // 尝试按凭证配置解密请求体,并构建签名载荷进行校验 + byte[] decryptedBody = decryptRequestBody(requestBody, credential, security); + verifySignature(request, decryptedBody, signature, credential, security, appId, timestampHeader); + requestBody = decryptedBody; + } ensureNonce(tenantId, appId, nonce, security); - requestBody = decryptedBody; + } // 使用可重复读取的请求包装,供后续过滤器继续消费 @@ -153,7 +163,9 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { try { filterChain.doFilter(securedRequest, responseWrapper); dispatchedToGateway = true; - encryptResponse(responseWrapper, credential, security); + if (isNormalProcess) { + encryptResponse(responseWrapper, credential, security); + } } finally { responseWrapper.copyBodyToResponse(); } @@ -162,7 +174,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { accessLogId = accessLogger.logEntrance(request); } log.warn("[API-PORTAL] 安全校验失败: {}", ex.getMessage()); - writeErrorResponse(response, security, credential, ex.status(), ex.getMessage()); + writeErrorResponse(response, security, credential, ex.status(), ex.getMessage(), isNormalProcess); if (!dispatchedToGateway) { accessLogger.finalizeEarly(request, ex.status().value(), ex.getMessage()); } @@ -171,7 +183,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { accessLogId = accessLogger.logEntrance(request); } log.error("[API-PORTAL] 处理安全校验时出现异常", ex); - writeErrorResponse(response, security, credential, HttpStatus.INTERNAL_SERVER_ERROR, "网关安全校验失败"); + writeErrorResponse(response, security, credential, HttpStatus.INTERNAL_SERVER_ERROR, "网关安全校验失败", isNormalProcess); if (!dispatchedToGateway) { accessLogger.finalizeEarly(request, HttpStatus.INTERNAL_SERVER_ERROR.value(), "网关安全校验失败"); } @@ -471,11 +483,14 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { } securedRequest.removeHeader(GatewayJwtResolver.HEADER_ZT_AUTH_TOKEN); securedRequest.removeHeader(HttpHeaders.AUTHORIZATION); - anonymousUserService.issueAccessToken(anonymousDetails) - .ifPresent(token -> { - securedRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token); - securedRequest.setHeader(GatewayJwtResolver.HEADER_ZT_AUTH_TOKEN, token); - }); + Optional tokenOptional = anonymousUserService.issueAccessToken(anonymousDetails); + if (tokenOptional.isEmpty()) { + throw new SecurityValidationException(HttpStatus.UNAUTHORIZED, "匿名访问获取token失败"); + } + String token = tokenOptional.get(); + securedRequest.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token); + securedRequest.setHeader(GatewayJwtResolver.HEADER_ZT_AUTH_TOKEN, token); + } private static final class SecurityValidationException extends RuntimeException { @@ -496,7 +511,8 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { ApiGatewayProperties.Security security, ApiClientCredentialDO credential, HttpStatus status, - String message) { + String message, + boolean isNormalProcess) { if (response.isCommitted()) { log.warn("[API-PORTAL] 响应已提交,无法写入安全校验错误: {}", message); return; @@ -511,7 +527,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { .response(null) .traceId(traceId) .build(); - if (shouldEncryptErrorResponse(security, credential)) { + if (shouldEncryptErrorResponse(security, credential) && isNormalProcess) { String encryptionKey = credential.getEncryptionKey(); String encryptionType = resolveEncryptionType(credential, security); try { diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandler.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandler.java index db205143..fdf7e28e 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandler.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandler.java @@ -68,6 +68,7 @@ public class HttpStepHandler implements ApiStepHandler { private HttpRequestPayload coerceRequestPayload(Object evaluated, Object fallbackBody, Map fallbackQuery) { Map querySnapshot = new LinkedHashMap<>(fallbackQuery); + if (evaluated == null) { return HttpRequestPayload.of(fallbackBody, querySnapshot); } @@ -80,23 +81,47 @@ public class HttpStepHandler implements ApiStepHandler { mergeQueryParams(querySnapshot, multiValueMap); return HttpRequestPayload.of(fallbackBody, querySnapshot); } - if (evaluated instanceof Map map) { - Object queryPart = extractCaseInsensitive(map, "query", "queryParams", "params"); + if (evaluated instanceof Map evaluatedMap) { + // 处理查询参数 + Object queryPart = extractCaseInsensitive(evaluatedMap, "query", "queryParams", "params"); if (queryPart != null) { mergeQueryParams(querySnapshot, queryPart); } - boolean explicitBody = containsKeyIgnoreCase(map, "body", "payload"); + + // 检查是否显式指定了body字段 + boolean explicitBody = containsKeyIgnoreCase(evaluatedMap, "body", "payload"); Object body = explicitBody - ? Optional.ofNullable(extractCaseInsensitive(map, "body", "payload")).orElse(fallbackBody) + ? Optional.ofNullable(extractCaseInsensitive(evaluatedMap, "body", "payload")).orElse(fallbackBody) : (queryPart != null ? fallbackBody : evaluated); - if (!explicitBody && queryPart == null) { + + if (explicitBody) { + // 如果显式指定了body,将原始body和映射body合并 + if (body instanceof Map && fallbackBody instanceof Map) { + Map mergedBody = new LinkedHashMap<>((Map) fallbackBody); + mergedBody.putAll((Map) body); + return HttpRequestPayload.of(mergedBody, querySnapshot); + } + return HttpRequestPayload.of(body, querySnapshot); + } else if (queryPart != null) { + // 如果有查询参数部分但没有显式body,保留原始body + return HttpRequestPayload.of(fallbackBody, querySnapshot); + } else { + // 如果没有查询参数也没有显式body,说明整个evaluatedMap是新的body + if (fallbackBody instanceof Map) { + // 将原始body和映射结果合并,保留原始字段 + Map mergedBody = new LinkedHashMap<>((Map) fallbackBody); + ((Map) evaluatedMap).forEach((key, value) -> { + mergedBody.put((String) key, value); + }); + return HttpRequestPayload.of(mergedBody, querySnapshot); + } return HttpRequestPayload.of(evaluated, querySnapshot); } - return HttpRequestPayload.of(body, querySnapshot); } return HttpRequestPayload.of(evaluated, querySnapshot); } + @Override public GenericHandler build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) { return (payload, headers) -> { @@ -134,7 +159,7 @@ public class HttpStepHandler implements ApiStepHandler { .success(true) .elapsed(Duration.between(start, Instant.now())) .build()); - applyResponseMapping(stepDefinition, payload, headers, response); + applyResponseMapping(aggregate, stepDefinition, payload, headers, response); } catch (Exception ex) { payload.addStepResult(ApiStepResult.builder() .stepId(stepDefinition.getStep().getId()) @@ -157,22 +182,66 @@ public class HttpStepHandler implements ApiStepHandler { ExpressionSpec spec = ExpressionSpecParser.parse(stepDefinition.getStep().getRequestMappingExpr(), ExpressionTypeEnum.JSON); Map baseQuery = new LinkedHashMap<>(context.getRequestQueryParams()); Object fallbackBody = context.getRequestBody(); + if (spec == null) { return HttpRequestPayload.of(fallbackBody, baseQuery); } - Object evaluated = expressionExecutor.evaluate(spec, context, fallbackBody, headers); - return coerceRequestPayload(evaluated, fallbackBody, baseQuery); + + // 将查询参数合并到 payload 中,使表达式可以通过 $.query.k 访问 + // 这样可以避免 JSONata 环境变量访问的限制 + Object enrichedPayload = enrichPayloadWithQuery(fallbackBody, baseQuery); + + Object evaluated = expressionExecutor.evaluate(spec, context, enrichedPayload, headers); + + // 如果映射结果是Map且原始body也是Map,合并两个Map以保留未映射的字段 + // 但如果映射结果包含显式的 body/query 结构,则不进行合并,直接传递 + HttpRequestPayload result; + if (evaluated instanceof Map && fallbackBody instanceof Map) { + Map evaluatedMap = (Map) evaluated; + + // 检查是否包含显式的 body 或 query 键(表示结构化映射) + boolean hasStructuredMapping = containsKeyIgnoreCase(evaluatedMap, "body", "payload") + || containsKeyIgnoreCase(evaluatedMap, "query", "queryParams", "params"); + + if (hasStructuredMapping) { + // 有结构化映射,直接传递给 coerceRequestPayload 处理 + result = coerceRequestPayload(evaluated, fallbackBody, baseQuery); + } else { + // 没有结构化映射,进行字段级合并以保留未映射的字段 + Map originalBodyMap = (Map) fallbackBody; + Map mergedBody = new LinkedHashMap<>(originalBodyMap); + mergedBody.putAll(evaluatedMap); + result = coerceRequestPayload(mergedBody, fallbackBody, baseQuery); + } + } else { + result = coerceRequestPayload(evaluated, fallbackBody, baseQuery); + } + + return result; } - private void applyResponseMapping(ApiStepDefinition stepDefinition, ApiInvocationContext context, Map headers, Object response) throws Exception { + + private void applyResponseMapping(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition, ApiInvocationContext context, Map headers, Object response) throws Exception { ExpressionSpec spec = ExpressionSpecParser.parse(stepDefinition.getStep().getResponseMappingExpr(), ExpressionTypeEnum.JSON); if (spec == null) { context.setResponseBody(response); return; } + Object mapped = expressionExecutor.evaluate(spec, context, response, headers); + + // 判断当前步骤是否是最后一个 HTTP 步骤 + Long lastHttpStepId = aggregate.getLastHttpStepId(); + boolean isLastHttpStep = lastHttpStepId != null && lastHttpStepId.equals(stepDefinition.getStep().getId()); + if (mapped instanceof Map map) { + // 将映射结果放入 variables map.forEach((key, value) -> context.getVariables().put(String.valueOf(key), value)); + + // 如果是最后一个 HTTP 步骤,也要设置 responseBody,供 END 步骤使用 + if (isLastHttpStep) { + context.setResponseBody(mapped); + } } else { context.setResponseBody(mapped); } @@ -330,7 +399,8 @@ public class HttpStepHandler implements ApiStepHandler { mergeQueryParams(queryParams, requestPayload.body()); } applyQueryParams(builder, queryParams); - return builder.build(true).toUri(); + URI uri = builder.build(true).toUri(); + return uri; } private Object extractCaseInsensitive(Map source, String... keys) { @@ -435,4 +505,26 @@ public class HttpStepHandler implements ApiStepHandler { } return false; } + + /** + * 将查询参数合并到 payload 中,使表达式可以通过 $.query.k 访问 + * 这样可以避免 JSONata 环境变量访问的限制 + */ + private Object enrichPayloadWithQuery(Object originalPayload, Map queryParams) { + if (queryParams == null || queryParams.isEmpty()) { + return originalPayload; + } + + Map enriched = new LinkedHashMap<>(); + + // 如果原始 payload 是 Map,复制所有字段 + if (originalPayload instanceof Map) { + enriched.putAll((Map) originalPayload); + } + + // 添加查询参数到 query 字段 + enriched.put("query", new LinkedHashMap<>(queryParams)); + + return enriched; + } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandler.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandler.java index db6d83e9..4fe73b23 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandler.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/StartStepHandler.java @@ -18,10 +18,19 @@ import org.springframework.stereotype.Component; import java.time.Duration; import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; + import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_START_EXECUTION_FAILED; /** * Handler for START orchestration nodes. + * + *

增强功能:START 节点的映射结果会存入 context.requestBody, + * 使得后续 HTTP 步骤可以自动继承这些公共参数,避免重复配置。 + * + *

注意:不将参数存入 context.variables,避免与后续步骤的响应结果冲突。 + * variables 应该保留给步骤执行结果使用。 */ @Component @RequiredArgsConstructor @@ -40,11 +49,22 @@ public class StartStepHandler implements ApiStepHandler { Instant start = Instant.now(); Object snapshotBefore = GatewayExpressionHelper.snapshotRequest(payload); try { - ExpressionSpec spec = ExpressionSpecParser.parse(stepDefinition.getStep().getRequestMappingExpr(), ExpressionTypeEnum.JSON); + ExpressionSpec spec = ExpressionSpecParser.parse( + stepDefinition.getStep().getRequestMappingExpr(), + ExpressionTypeEnum.JSON + ); if (spec != null) { - Object evaluated = expressionExecutor.evaluate(spec, payload, payload.getRequestBody(), headers); + // 将查询参数合并到 payload 中,使表达式可以通过 $.query.k 访问 + // 这样和 HTTP 步骤的映射逻辑保持一致 + Object enrichedPayload = enrichPayloadWithQuery(payload.getRequestBody(), payload.getRequestQueryParams()); + + Object evaluated = expressionExecutor.evaluate(spec, payload, enrichedPayload, headers); + + // 应用上下文变更(包括 requestBody、requestQuery 等) + // 支持字段级映射,将外部字段名转换为内部字段名 GatewayExpressionHelper.applyContextMutations(payload, evaluated, true, false); } + payload.addStepResult(ApiStepResult.builder() .stepId(stepDefinition.getStep().getId()) .stepType(stepDefinition.getStep().getType()) @@ -70,4 +90,26 @@ public class StartStepHandler implements ApiStepHandler { return payload; }; } + + /** + * 将查询参数合并到 payload 中,使表达式可以通过 $.query.k 访问 + * 这样可以避免 JSONata 环境变量访问的限制 + */ + private Object enrichPayloadWithQuery(Object originalPayload, Map queryParams) { + if (queryParams == null || queryParams.isEmpty()) { + return originalPayload; + } + + Map enriched = new LinkedHashMap<>(); + + // 如果原始 payload 是 Map,复制所有字段 + if (originalPayload instanceof Map) { + enriched.putAll((Map) originalPayload); + } + + // 添加查询参数到 query 字段 + enriched.put("query", new LinkedHashMap<>(queryParams)); + + return enriched; + } } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAnonymousUserService.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAnonymousUserService.java index 788b0498..ae04eabe 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAnonymousUserService.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/service/gateway/ApiAnonymousUserService.java @@ -40,6 +40,9 @@ public class ApiAnonymousUserService { private final AdminUserApi adminUserApi; private final OAuth2TokenCommonApi oauth2TokenApi; + private static final int RETRY_ATTEMPTS = 10; + private static final Duration RETRY_DELAY = Duration.ofSeconds(5); + private LoadingCache> cache; @PostConstruct @@ -105,18 +108,33 @@ public class ApiAnonymousUserService { if (details == null) { return Optional.empty(); } - try { - OAuth2AccessTokenCreateReqDTO req = buildAccessTokenRequest(details); - OAuth2AccessTokenRespDTO resp = oauth2TokenApi.createAccessToken(req).getCheckedData(); - if (resp == null || !StringUtils.hasText(resp.getAccessToken())) { - log.warn("[ANONYMOUS] 获取用户 {} 的访问令牌失败: 响应为空", details.getUserId()); - return Optional.empty(); + OAuth2AccessTokenCreateReqDTO req = buildAccessTokenRequest(details); + Exception lastException = null; + for (int attempt = 1; attempt <= RETRY_ATTEMPTS; attempt++) { + try { + OAuth2AccessTokenRespDTO resp = oauth2TokenApi.createAccessToken(req).getCheckedData(); + if (resp == null || !StringUtils.hasText(resp.getAccessToken())) { + log.warn("[ANONYMOUS] 获取用户 {} 的访问令牌失败: 响应为空", details.getUserId()); + return Optional.empty(); + } + return Optional.of(resp.getAccessToken()); + } catch (Exception ex) { + lastException = ex; + if (attempt < RETRY_ATTEMPTS) { + log.warn("[ANONYMOUS] 获取用户 {} 的访问令牌失败,开始第 {} 次重试,原因:{}", + details.getUserId(), attempt, ex.getMessage()); + try { + Thread.sleep(RETRY_DELAY.toMillis()); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + log.error("[ANONYMOUS] 获取用户 {} 的访问令牌重试被中断", details.getUserId()); + return Optional.empty(); + } + } } - return Optional.of(resp.getAccessToken()); - } catch (Exception ex) { - log.error("[ANONYMOUS] 获取用户 {} 的访问令牌时发生异常", details.getUserId(), ex); - return Optional.empty(); } + log.error("[ANONYMOUS] 获取用户 {} 的访问令牌时发生异常", details.getUserId(), lastException); + return Optional.empty(); } private OAuth2AccessTokenCreateReqDTO buildAccessTokenRequest(AnonymousUserDetails details) { diff --git a/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileCreateReqDTO.java b/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileCreateReqDTO.java index 4260da92..6bfce374 100644 --- a/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileCreateReqDTO.java +++ b/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileCreateReqDTO.java @@ -21,4 +21,6 @@ public class FileCreateReqDTO { @NotEmpty(message = "文件内容不能为空") private byte[] content; + @Schema(description = "是否可下载(true是,false否)", example = "true") + private Boolean downloadable; } diff --git a/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileRespDTO.java b/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileRespDTO.java index 58af4c9a..3b318d0c 100644 --- a/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileRespDTO.java +++ b/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/file/dto/FileRespDTO.java @@ -37,4 +37,7 @@ public class FileRespDTO { @Schema(description = "文件下载次数") private Integer downloadCount; + @Schema(description = "是否可下载(true是,false否)") + private Boolean downloadable; + } \ No newline at end of file diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/FileController.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/FileController.java index d4b971a9..3e812f1b 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/FileController.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/FileController.java @@ -3,6 +3,7 @@ package com.zt.plat.module.infra.controller.admin.file; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.URLUtil; +import com.zt.plat.framework.common.enums.VerifyCodeSendType; import com.zt.plat.framework.common.exception.ServiceException; import com.zt.plat.framework.common.pojo.CommonResult; import com.zt.plat.framework.common.pojo.PageResult; @@ -21,6 +22,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; @@ -43,18 +45,48 @@ import static com.zt.plat.module.infra.framework.file.core.utils.FileTypeUtils.w @Slf4j public class FileController { + @Value("${zt.file.preview-base-url:}") + private String previewBaseUrl; + @Resource private FileService fileService; @GetMapping("/get") @Operation(summary = "获取文件预览地址", description = "根据 fileId 返回文件预览 url(kkfile)") - public CommonResult getPreviewUrl(@RequestParam("fileId") Long fileId) { + public CommonResult getPreviewUrl(@RequestParam("fileId") Long fileId, + @RequestParam(value = "code", required = false) String code, + HttpServletRequest request) throws Exception { FileDO fileDO = fileService.getActiveFileById(fileId); if (fileDO == null) { return CommonResult.error(HttpStatus.NOT_FOUND.value(), "文件不存在"); } + + // 统计下载次数 + fileService.incDownloadCount(fileId); + // FileDO 转换为 FileRespVO FileRespVO fileRespVO = BeanUtils.toBean(fileDO, FileRespVO.class); + + // 加密文件:塞入“临时解密预览 URL” + if (Boolean.TRUE.equals(fileRespVO.getIsEncrypted())) { // FileDO 通过 aesIv 判断加密 + + if (cn.hutool.core.util.StrUtil.isBlank(code)) { + return CommonResult.error(HttpStatus.BAD_REQUEST.value(), "加密文件预览需要验证码 code"); + } + // 验证通过:发放给 kkfile 用的短期 token(kkfile 不带登录态) + Long userId = getLoginUserId(); + boolean flag = fileService.verifyCode(fileId, userId, code); + if(!flag){ + return CommonResult.customize(null, HttpStatus.INTERNAL_SERVER_ERROR.value(), "验证码错误"); + } + + String token = fileService.generatePreviewToken(fileId, userId); + + String baseUrl = buildPublicBaseUrl(request); // 见下方函数 + String decryptUrl = baseUrl + "/admin-api/infra/file/preview-decrypt?fileId=" + fileId + "&token=" + token; + fileRespVO.setUrl(decryptUrl); + } + return success(fileRespVO); } @@ -162,15 +194,32 @@ public class FileController { } @GetMapping("/generate-download-code") @Operation(summary = "获取下载验证码") - public CommonResult preDownloadEncrypt(@RequestParam("fileId") Long fileId) { + public CommonResult preDownloadEncrypt(@RequestParam("fileId") Long fileId, + @RequestParam(value = "sendType", required = false) String sendType // 可选:SMS / E_OFFICE + ) { Long userId = getLoginUserId(); + + // 解析 sendType(允许为空) + VerifyCodeSendType sendTypeEnum = null; + if (sendType != null && !sendType.trim().isEmpty()) { + try { + sendTypeEnum = VerifyCodeSendType.valueOf(sendType.trim().toUpperCase()); + } catch (IllegalArgumentException ex) { + return CommonResult.error(HttpStatus.BAD_REQUEST.value(), + "sendType 参数不合法,可选:SMS / E_OFFICE"); + } + } + FileDO activeFileById = fileService.getActiveFileById(fileId); if (activeFileById == null) { return CommonResult.error(HttpStatus.NOT_FOUND.value(), "文件不存在"); } + FileRespVO fileRespVO = BeanUtils.toBean(activeFileById, FileRespVO.class); try { - fileService.generateFileVerificationCode(fileId, userId); + String code = fileService.generateFileVerificationCode(fileId, userId); + if(sendTypeEnum != null) + fileService.sendVerifyCode(code, sendTypeEnum); // 发送验证码 return CommonResult.customize(fileRespVO, HttpStatus.OK.value(), "验证码已生成,请使用验证码下载文件"); } catch (ServiceException e) { return CommonResult.customize(fileRespVO, HttpStatus.OK.value(), e.getMessage()); @@ -187,4 +236,53 @@ public class FileController { } return CommonResult.customize(null, HttpStatus.OK.value(), "验证码校验通过"); } + + @GetMapping("/preview-decrypt") + @PermitAll + @TenantIgnore + @Operation(summary = "加密文件预览解密流(供 kkfile 拉取)") + public void previewDecrypt(@RequestParam("fileId") Long fileId, + @RequestParam("token") String token, + HttpServletResponse response) throws Exception { + + boolean ok = fileService.verifyPreviewToken(fileId, token); + if (!ok) { + response.setStatus(HttpStatus.FORBIDDEN.value()); + return; + } + + FileDO fileDO = fileService.getActiveFileById(fileId); + if (fileDO == null) { + response.setStatus(HttpStatus.NOT_FOUND.value()); + return; + } + + // byte[] content = fileService.getDecryptedBytes(fileId); + response.setHeader("Cache-Control", "no-store"); + response.setContentType(fileDO.getType()); + + String filename = java.net.URLEncoder.encode(fileDO.getName(), java.nio.charset.StandardCharsets.UTF_8); + response.setHeader("Content-Disposition", "inline; filename*=UTF-8''" + filename); + + // cn.hutool.core.io.IoUtil.write(response.getOutputStream(), true, content); + fileService.writeDecryptedToStream(fileId, response.getOutputStream()); + } + + private String buildPublicBaseUrl(HttpServletRequest request) { + if (previewBaseUrl != null && !previewBaseUrl.isBlank()) { + return previewBaseUrl.endsWith("/") + ? previewBaseUrl.substring(0, previewBaseUrl.length() - 1) + : previewBaseUrl; + } + // 兜底:从请求推断 + String scheme = request.getHeader("X-Forwarded-Proto"); + if (scheme == null) scheme = request.getScheme(); + + String host = request.getHeader("X-Forwarded-Host"); + if (host == null) host = request.getHeader("Host"); + if (host == null) host = request.getServerName() + ":" + request.getServerPort(); + + return scheme + "://" + host; + } + } diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/vo/file/FileRespVO.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/vo/file/FileRespVO.java index 6d4d12f4..77d3d443 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/vo/file/FileRespVO.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/file/vo/file/FileRespVO.java @@ -23,8 +23,8 @@ import java.util.Date; @Accessors(chain = true) public class FileRespVO { public String getUrl() { - // 加密附件不返回 url - if (Boolean.TRUE.equals(this.isEncrypted)) { + // 不可下载 或 加密附件不返回 url + if (Boolean.FALSE.equals(this.downloadable) || Boolean.TRUE.equals(this.isEncrypted)) { return null; } // 如果 url 已经是临时下载地址(如预签名 URL),直接返回 @@ -62,8 +62,8 @@ public class FileRespVO { private String previewUrl; public String getPreviewUrl() { - // 加密附件不返回 previewUrl - if (Boolean.TRUE.equals(this.isEncrypted)) { + // 不可下载不返回 previewUrl + if (Boolean.FALSE.equals(this.downloadable) ) { return null; } // 仅当 url 不为空时生成 @@ -75,7 +75,15 @@ public class FileRespVO { if (onlinePreview == null || onlinePreview.isEmpty()) { return null; } - String presignedUrl = this.getUrl(); + // 添加加密文件预览逻辑 + String presignedUrl = null; + if (Boolean.TRUE.equals(this.isEncrypted)) { + if (url != null && (url.startsWith("http://") || url.startsWith("https://"))) { + presignedUrl = url; + } + }else{ + presignedUrl = this.getUrl(); + } if (presignedUrl == null || presignedUrl.isEmpty()) { return null; } @@ -102,4 +110,6 @@ public class FileRespVO { @Schema(description = "下载次数") private Integer downloadCount; + @Schema(description = "是否可下载(true是,false否)") + private Boolean downloadable; } diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/dataobject/file/FileDO.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/dataobject/file/FileDO.java index c82c0e3c..d1be1f72 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/dataobject/file/FileDO.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/dataobject/file/FileDO.java @@ -70,6 +70,11 @@ public class FileDO extends BaseDO { */ private Integer downloadCount; + /** + * 是否可下载(true是,false否) + */ + private Boolean downloadable; + /** * 是否加密 *

diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/mysql/file/FileMapper.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/mysql/file/FileMapper.java index 9857cdc2..1cd9807c 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/mysql/file/FileMapper.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/mysql/file/FileMapper.java @@ -9,6 +9,8 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; +import java.util.List; + /** * 文件操作 Mapper * @@ -34,7 +36,16 @@ public interface FileMapper extends BaseMapperX { return selectFirstOne(FileDO::getHash, hash); } + /** + * 根据configId和path查找文件信息 + * @param configId + * @param path + * @return + */ + default List selectByConfigIdAndPath(Long configId, String path){ + return selectList(FileDO::getConfigId, configId, FileDO::getPath, path); + }; - @Update("UPDATE INFRA_FILE SET DOWNLOAD_COUNT = DOWNLOAD_COUNT + 1 WHERE CONFIG_ID = #{configId} AND PATH = #{path}") - int incDownloadCount(@Param("configId") Long configId, @Param("path") String path); + @Update("UPDATE INFRA_FILE SET DOWNLOAD_COUNT = DOWNLOAD_COUNT + 1 WHERE ID = #{fileId}") + int incDownloadCount(@Param("fileId") Long fileId); } diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/redis/RedisKeyConstants.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/redis/RedisKeyConstants.java index 8ffde8c1..dcaf93ff 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/redis/RedisKeyConstants.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/redis/RedisKeyConstants.java @@ -6,4 +6,7 @@ package com.zt.plat.module.infra.dal.redis; public class RedisKeyConstants { public static final String FILE_VERIFICATION_CODE = "infra:file:verification_code:%d:%d"; public static final String FILE_VERIFICATION_CODE_USER_SET = "infra:file:verification_code:user:%d"; + + // 加密文件预览token + public static final String FILE_PREVIEW_TOKEN = "infra:file:preview-token:%s"; } diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/framework/rpc/config/RpcConfiguration.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/framework/rpc/config/RpcConfiguration.java index 352e5313..b978f66b 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/framework/rpc/config/RpcConfiguration.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/framework/rpc/config/RpcConfiguration.java @@ -2,11 +2,12 @@ package com.zt.plat.module.infra.framework.rpc.config; import com.zt.plat.module.system.api.permission.PermissionApi; import com.zt.plat.module.system.api.permission.RoleApi; +import com.zt.plat.module.system.api.sms.SmsSendApi; import com.zt.plat.module.system.api.user.AdminUserApi; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Configuration; @Configuration(value = "infraRpcConfiguration", proxyBeanMethods = false) -@EnableFeignClients(clients = {PermissionApi.class, RoleApi.class, AdminUserApi.class}) +@EnableFeignClients(clients = {PermissionApi.class, RoleApi.class, AdminUserApi.class, SmsSendApi.class }) public class RpcConfiguration { } diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileService.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileService.java index 6624810c..52d959b1 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileService.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileService.java @@ -1,5 +1,6 @@ package com.zt.plat.module.infra.service.file; +import com.zt.plat.framework.common.enums.VerifyCodeSendType; import com.zt.plat.framework.common.pojo.PageResult; import com.zt.plat.module.infra.controller.admin.file.vo.file.FileCreateReqVO; import com.zt.plat.module.infra.controller.admin.file.vo.file.FilePageReqVO; @@ -10,6 +11,8 @@ import com.zt.plat.module.infra.dal.dataobject.file.FileDO; import jakarta.validation.constraints.NotEmpty; import lombok.SneakyThrows; +import java.io.OutputStream; + /** * 文件 Service 接口 * @@ -72,6 +75,14 @@ public interface FileService { */ String generateFileVerificationCode(Long fileId, Long userId); + + /** + * 发送验证码 + * @param code 验证码 + * @param verifyCodeSendType 发送类型 + */ + void sendVerifyCode(String code, VerifyCodeSendType verifyCodeSendType); + /** * 校验验证码并返回解密后的文件内容 */ @@ -119,4 +130,31 @@ public interface FileService { * @param path */ void incDownloadCount(Long configId, String path); + + /** + * 更新文件下载次数 + * @param fileId + */ + void incDownloadCount(Long fileId); + + /** + * 临时生成文件预览token + * @param fileId 文件ID + * @param userId 用户ID + * @return 临时token + */ + String generatePreviewToken(Long fileId, Long userId); + + /** + * 验证文件预览token + * @param fileId 文件ID + * @param token 用户ID + * @return 临时token + */ + boolean verifyPreviewToken(Long fileId, String token); + + /** + * 校验预览 token 后,将文件内容解密并写入输出流(用于预览) + */ + void writeDecryptedToStream(Long fileId, OutputStream outputStream) throws Exception; } diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileServiceImpl.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileServiceImpl.java index 14f42c77..d890a6c2 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileServiceImpl.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileServiceImpl.java @@ -2,12 +2,17 @@ package com.zt.plat.module.infra.service.file; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IoUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.google.common.annotations.VisibleForTesting; +import com.zt.plat.framework.common.enums.VerifyCodeSendType; import com.zt.plat.framework.common.pojo.PageResult; import com.zt.plat.framework.common.util.object.BeanUtils; +import com.zt.plat.framework.security.core.LoginUser; +import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; import com.zt.plat.module.infra.controller.admin.file.vo.file.FileCreateReqVO; import com.zt.plat.module.infra.controller.admin.file.vo.file.FilePageReqVO; import com.zt.plat.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; @@ -20,6 +25,9 @@ import com.zt.plat.module.infra.framework.file.core.client.FileClient; import com.zt.plat.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; import com.zt.plat.module.infra.framework.file.core.utils.FileTypeUtils; import com.zt.plat.module.infra.util.VerificationCodeUtil; +import com.zt.plat.module.system.api.permission.RoleApi; +import com.zt.plat.module.system.api.sms.SmsSendApi; +import com.zt.plat.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; import jakarta.annotation.Resource; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; @@ -30,8 +38,9 @@ import org.springframework.stereotype.Service; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import java.io.OutputStream; import java.security.SecureRandom; -import java.util.Base64; +import java.util.*; import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN; import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -50,6 +59,15 @@ public class FileServiceImpl implements FileService { private String aesKey; @Value("${zt.verify-code:}") private String fixedVerifyCode; + // 加密文件预览token过期时间 + @Value("${zt.file.preview-expire-seconds:300}") + private Integer previewExpireSeconds; + + @Resource + private RoleApi roleApi; + + @Resource + private SmsSendApi smsSendApi; @Resource private StringRedisTemplate stringRedisTemplate; @@ -62,6 +80,31 @@ public class FileServiceImpl implements FileService { return VerificationCodeUtil.generateCode(codeKey, userSetKey, stringRedisTemplate); } + @Override + public void sendVerifyCode(String code, VerifyCodeSendType verifyCodeSendType) { + if(verifyCodeSendType == null) return; + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + Assert.notNull(loginUser,"用户未登录或权限不足!"); + if(loginUser == null) return; + if (VerifyCodeSendType.SMS.equals(verifyCodeSendType)) { + Map templateParams = new HashMap<>(); + templateParams.put("code",code); + SmsSendSingleToUserReqDTO smsSendReqDTO = new SmsSendSingleToUserReqDTO(); + if(loginUser.getInfo().get(LoginUser.INFO_KEY_PHONE)!=null) + smsSendReqDTO.setMobile(loginUser.getInfo().get(LoginUser.INFO_KEY_PHONE)); + smsSendReqDTO.setUserId(loginUser.getId()); + smsSendReqDTO.setTemplateCode("test_02"); + smsSendReqDTO.setTemplateParams(templateParams); + smsSendApi.sendSingleSmsToAdmin(smsSendReqDTO); + return; + } + + if (VerifyCodeSendType.E_OFFICE.equals(verifyCodeSendType)) { + // TODO 预留实现接口 + return; + } + } + @Override public byte[] verifyCodeAndGetFile(Long fileId, Long userId, String code) throws Exception { // 开发模式下,验证码直接获取配置进行比对 @@ -336,7 +379,46 @@ public class FileServiceImpl implements FileService { @Override public void incDownloadCount(Long configId, String path) { - fileMapper.incDownloadCount(configId, path); + List fileList = fileMapper.selectByConfigIdAndPath(configId, path); + if(fileList != null && !fileList.isEmpty()) + incDownloadCount(fileList.get(0).getId()); } + @Override + public void incDownloadCount(Long fileId) { + fileMapper.incDownloadCount(fileId); + } + + @Override + public String generatePreviewToken(Long fileId, Long userId) { + // 你也可以加:validateFileExists(fileId) + String token = UUID.randomUUID().toString().replace("-", ""); + String key = String.format(RedisKeyConstants.FILE_PREVIEW_TOKEN, token); + stringRedisTemplate.opsForValue().set(key, String.valueOf(fileId), + java.time.Duration.ofSeconds(previewExpireSeconds)); + return token; + } + + @Override + public boolean verifyPreviewToken(Long fileId, String token) { + String key = String.format(RedisKeyConstants.FILE_PREVIEW_TOKEN, token); + String val = stringRedisTemplate.opsForValue().get(key); + if (val == null || !val.equals(String.valueOf(fileId))) { + return false; + } + // 可选:单次使用更安全 + // stringRedisTemplate.delete(key); + return true; + } + + @Override + public void writeDecryptedToStream(Long fileId, OutputStream os) throws Exception { + FileDO fileDO = getActiveFileById(fileId); + if (fileDO == null) { + throw exception(FILE_NOT_EXISTS); + } + + byte[] decrypted = getDecryptedBytes(fileId); + IoUtil.write(os, true, decrypted); + } } diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/dept/DeptApi.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/dept/DeptApi.java index abc53972..da378c52 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/dept/DeptApi.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/dept/DeptApi.java @@ -2,6 +2,7 @@ package com.zt.plat.module.system.api.dept; import com.zt.plat.framework.common.pojo.CommonResult; import com.zt.plat.framework.common.util.collection.CollectionUtils; +import com.zt.plat.framework.common.util.object.BeanUtils; import com.zt.plat.module.system.api.dept.dto.*; import com.zt.plat.module.system.enums.ApiConstants; import io.swagger.v3.oas.annotations.Operation; @@ -15,6 +16,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import static com.zt.plat.framework.common.pojo.CommonResult.success; + @FeignClient(name = ApiConstants.NAME) // TODO ZT:fallbackFactory = @Tag(name = "RPC 服务 - 部门") public interface DeptApi { @@ -86,6 +89,11 @@ public interface DeptApi { @Parameter(name = "userId", description = "用户编号", example = "1", required = true) CommonResult> getCompanyDeptInfoListByUserId(@RequestParam("userId") Long userId); + @GetMapping(PREFIX+"/up-find-company-node") + @Operation(summary = "获取公司节点信息", description = "通过部门编号,向上追溯部门信息,直到上级部门是公司,返回追溯到的部门信息列表") + @Parameter(name = "deptId", description = "部门编号", required = true, example = "1024") + CommonResult> upFindCompanyNode(@RequestParam("deptId") Long deptId); + // ========== 数据同步专用接口 ========== @PostMapping(PREFIX + "/sync") diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApi.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApi.java index 9181a071..84851de0 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApi.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApi.java @@ -46,18 +46,18 @@ public interface IWorkIntegrationApi { @PostMapping(PREFIX + "/hr/subcompany/page") @Operation(summary = "获取 iWork 分部列表") - CommonResult listSubcompanies(@RequestBody IWorkOrgPageReqDTO reqDTO); + CommonResult listSubcompanies(@RequestBody IWorkSubcompanyQueryReqDTO reqDTO); @PostMapping(PREFIX + "/hr/department/page") @Operation(summary = "获取 iWork 部门列表") - CommonResult listDepartments(@RequestBody IWorkOrgPageReqDTO reqDTO); + CommonResult listDepartments(@RequestBody IWorkDepartmentQueryReqDTO reqDTO); @PostMapping(PREFIX + "/hr/job-title/page") @Operation(summary = "获取 iWork 岗位列表") - CommonResult listJobTitles(@RequestBody IWorkOrgPageReqDTO reqDTO); + CommonResult listJobTitles(@RequestBody IWorkJobTitleQueryReqDTO reqDTO); @PostMapping(PREFIX + "/hr/user/page") @Operation(summary = "获取 iWork 人员列表") - CommonResult listUsers(@RequestBody IWorkOrgPageReqDTO reqDTO); + CommonResult listUsers(@RequestBody IWorkUserQueryReqDTO reqDTO); } diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkDepartmentQueryReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkDepartmentQueryReqDTO.java new file mode 100644 index 00000000..7072bb1e --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkDepartmentQueryReqDTO.java @@ -0,0 +1,48 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * iWork 部门查询参数。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IWorkDepartmentQueryReqDTO extends IWorkOrgBaseQueryReqDTO { + + @JsonProperty("departmentcode") + @Schema(description = "部门编号") + private String departmentCode; + + @JsonProperty("departmentname") + @Schema(description = "部门名称") + private String departmentName; + + @JsonProperty("subcompanyid1") + @Schema(description = "分部 ID") + private String subcompanyId1; + + @JsonProperty("created") + @Schema(description = "创建时间戳(>=)") + private String created; + + @JsonProperty("modified") + @Schema(description = "修改时间戳(>=)") + private String modified; + + @JsonProperty("canceled") + @Schema(description = "封存标志,默认查询非封存数据。1:封存") + private String canceled; + + @JsonProperty("custom_data") + @Schema(description = "自定义字段列表(逗号分隔)") + private String customData; + + @JsonProperty("id") + @Schema(description = "OA 部门 ID") + private String id; +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkJobTitleQueryReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkJobTitleQueryReqDTO.java new file mode 100644 index 00000000..553a614c --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkJobTitleQueryReqDTO.java @@ -0,0 +1,32 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * iWork 岗位查询参数。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IWorkJobTitleQueryReqDTO extends IWorkOrgBaseQueryReqDTO { + + @JsonProperty("jobtitlename") + @Schema(description = "岗位名称") + private String jobTitleName; + + @JsonProperty("created") + @Schema(description = "创建时间戳(>=)") + private String created; + + @JsonProperty("modified") + @Schema(description = "修改时间戳(>=)") + private String modified; + + @JsonProperty("id") + @Schema(description = "岗位 ID") + private String id; +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgBaseQueryReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgBaseQueryReqDTO.java new file mode 100644 index 00000000..aeddfdc7 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgBaseQueryReqDTO.java @@ -0,0 +1,17 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * iWork 人力组织查询基础参数。 + */ +@Data +public class IWorkOrgBaseQueryReqDTO { + + @Schema(description = "当前页码", example = "1") + private Integer curpage; + + @Schema(description = "每页条数", example = "20") + private Integer pagesize; +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgPageReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgPageReqDTO.java deleted file mode 100644 index 60fe8231..00000000 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkOrgPageReqDTO.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.zt.plat.module.system.api.iwork.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -/** - * iWork 人力组织分页查询通用请求 DTO - */ -@Data -public class IWorkOrgPageReqDTO { - - @Schema(description = "页码", example = "1", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer pageNo; - - @Schema(description = "每页大小", example = "20", requiredMode = Schema.RequiredMode.REQUIRED) - private Integer pageSize; - - @Schema(description = "关键字过滤") - private String keyword; - -} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkSubcompanyQueryReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkSubcompanyQueryReqDTO.java new file mode 100644 index 00000000..0ae343d1 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkSubcompanyQueryReqDTO.java @@ -0,0 +1,40 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * iWork 分部查询参数。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IWorkSubcompanyQueryReqDTO extends IWorkOrgBaseQueryReqDTO { + + @JsonProperty("subcompanycode") + @Schema(description = "分部编号") + private String subcompanyCode; + + @JsonProperty("subcompanyname") + @Schema(description = "分部名称") + private String subcompanyName; + + @JsonProperty("modified") + @Schema(description = "修改时间戳(>=)") + private String modified; + + @JsonProperty("canceled") + @Schema(description = "封存标志,默认查询非封存数据。1:封存") + private String canceled; + + @JsonProperty("custom_data") + @Schema(description = "自定义字段列表(逗号分隔)") + private String customData; + + @JsonProperty("id") + @Schema(description = "OA 分部 ID") + private String id; +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkUserQueryReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkUserQueryReqDTO.java new file mode 100644 index 00000000..d15b4366 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkUserQueryReqDTO.java @@ -0,0 +1,60 @@ +package com.zt.plat.module.system.api.iwork.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * iWork 人员查询参数。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IWorkUserQueryReqDTO extends IWorkOrgBaseQueryReqDTO { + + @JsonProperty("workcode") + @Schema(description = "人员编号") + private String workCode; + + @JsonProperty("subcompanyid1") + @Schema(description = "分部 ID") + private String subcompanyId1; + + @JsonProperty("departmentid") + @Schema(description = "部门 ID") + private String departmentId; + + @JsonProperty("jobtitleid") + @Schema(description = "岗位 ID") + private String jobTitleId; + + @JsonProperty("id") + @Schema(description = "人员 ID") + private String id; + + @JsonProperty("loginid") + @Schema(description = "登录名") + private String loginId; + + @JsonProperty("created") + @Schema(description = "创建时间戳(>=)") + private String created; + + @JsonProperty("modified") + @Schema(description = "修改时间戳(>=)") + private String modified; + + @JsonProperty("base_custom_data") + @Schema(description = "基本信息自定义字段列表(逗号分隔)") + private String baseCustomData; + + @JsonProperty("person_custom_data") + @Schema(description = "个人信息自定义字段列表(逗号分隔)") + private String personCustomData; + + @JsonProperty("work_custom_data") + @Schema(description = "工作信息自定义字段列表(逗号分隔)") + private String workCustomData; +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java index 013eea65..95a10c6d 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/iwork/dto/IWorkWorkflowCreateReqDTO.java @@ -36,9 +36,9 @@ public class IWorkWorkflowCreateReqDTO extends IWorkBaseReqDTO { @Schema(description = "用印材料附件 URL(必填)") private String xyywjUrl; - - @Schema(description = "用印材料附件文件名(必填)") - private String xyywjFileName; + + @Schema(description = "业务回调标识(回调分发使用,≤255 字符)") + private String bizCallbackKey; @Schema(description = "用印事项") private String yysx; diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/push/ExternalPushConfigApi.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/push/ExternalPushConfigApi.java new file mode 100644 index 00000000..5020a83f --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/push/ExternalPushConfigApi.java @@ -0,0 +1,39 @@ +package com.zt.plat.module.system.api.push; + +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.module.system.enums.ApiConstants; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * 外部系统推送配置 Feign API + * + * @author ZT Cloud + */ +@FeignClient(name = ApiConstants.NAME) +@Tag(name = "RPC 服务 - 外部系统推送配置") +public interface ExternalPushConfigApi { + + String PREFIX = ApiConstants.PREFIX + "/external-push-config"; + + /** + * 判断是否允许推送到外部系统 + * + * @param companyId 公司编号(可选,为 null 时表示不限制公司) + * @param deptId 部门编号(可选,为 null 时只按公司配置判断) + * @param businessType 业务类型(可选:PURCHASE/SALE/PRODUCTION,为 null 时表示所有业务类型) + * @param externalSystem 外部系统标识(可选:ERP/IWORK/等,为 null 时表示所有外部系统) + * @return 是否允许推送(true=允许,false=禁止,默认 true) + */ + @GetMapping(PREFIX + "/is-push-enabled") + @Operation(summary = "判断是否允许推送到外部系统") + CommonResult isPushEnabled( + @RequestParam(value = "companyId", required = false) @Parameter(description = "公司编号") Long companyId, + @RequestParam(value = "deptId", required = false) @Parameter(description = "部门编号") Long deptId, + @RequestParam(value = "businessType", required = false) @Parameter(description = "业务类型") String businessType, + @RequestParam(value = "externalSystem", required = false) @Parameter(description = "外部系统标识") String externalSystem); +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java index 41f4e40e..b9231f52 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java @@ -230,4 +230,11 @@ public interface ErrorCodeConstants { // ========== 门户网站 1-002-033-000 ========== ErrorCode PORTAL_NOT_EXISTS = new ErrorCode(1_002_033_000, "门户不存在"); + // ========== 外部系统推送配置 1_002_034_000 ========== + ErrorCode EXTERNAL_PUSH_CONFIG_NOT_EXISTS = new ErrorCode(1_002_034_001, "外部系统推送配置不存在"); + ErrorCode EXTERNAL_PUSH_CONFIG_EXISTS = new ErrorCode(1_002_034_002, "该配置已存在"); + ErrorCode EXTERNAL_PUSH_CONFIG_COMPANY_INVALID = new ErrorCode(1_002_034_003, "公司编号必须是公司节点(is_company=1)"); + ErrorCode EXTERNAL_PUSH_CONFIG_DEPT_INVALID = new ErrorCode(1_002_034_004, "部门编号必须是部门节点(is_company=0)"); + ErrorCode EXTERNAL_PUSH_CONFIG_BUSINESS_TYPE_INVALID = new ErrorCode(1_002_034_005, "业务类型无效,仅支持 PURCHASE/SALE/PRODUCTION"); + } diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleConditionEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleConditionEnum.java new file mode 100644 index 00000000..9064c4d6 --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleConditionEnum.java @@ -0,0 +1,60 @@ +package com.zt.plat.module.system.enums.permission; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 数据规则条件枚举 + * + * 用于菜单数据规则的条件类型 + * + * @author ZT + */ +@Getter +@AllArgsConstructor +public enum DataRuleConditionEnum { + + EQ("=", "等于"), + NE("!=", "不等于"), + GT(">", "大于"), + GE(">=", "大于等于"), + LT("<", "小于"), + LE("<=", "小于等于"), + IN("IN", "包含"), + NOT_IN("NOT_IN", "不包含"), + LIKE("LIKE", "模糊匹配"), + LEFT_LIKE("LEFT_LIKE", "左模糊"), + RIGHT_LIKE("RIGHT_LIKE", "右模糊"), + NOT_LIKE("NOT_LIKE", "不匹配"), + IS_NULL("IS_NULL", "为空"), + IS_NOT_NULL("IS_NOT_NULL", "不为空"), + SQL_RULE("SQL_RULE", "自定义SQL"); + + /** + * 条件符号 + */ + private final String condition; + + /** + * 条件描述 + */ + private final String description; + + /** + * 根据条件符号查找枚举 + * + * @param condition 条件符号 + * @return 枚举值 + */ + public static DataRuleConditionEnum findByCondition(String condition) { + if (condition == null) { + return null; + } + for (DataRuleConditionEnum value : values()) { + if (value.condition.equals(condition)) { + return value; + } + } + return null; + } +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleVariableEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleVariableEnum.java new file mode 100644 index 00000000..1c5881cd --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/permission/DataRuleVariableEnum.java @@ -0,0 +1,53 @@ +package com.zt.plat.module.system.enums.permission; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 数据规则变量枚举 + * + * 用于菜单数据规则的变量替换 + * + * @author ZT + */ +@Getter +@AllArgsConstructor +public enum DataRuleVariableEnum { + + USER_ID("#{userId}", "当前用户ID"), + USERNAME("#{username}", "当前用户名"), + DEPT_ID("#{deptId}", "当前用户部门ID"), + DEPT_IDS("#{deptIds}", "当前用户所有部门ID"), + ORG_CODE("#{orgCode}", "当前用户组织编码"), + TENANT_ID("#{tenantId}", "当前租户ID"), + CURRENT_DATE("#{currentDate}", "当前日期"), + CURRENT_TIME("#{currentTime}", "当前时间"); + + /** + * 变量名 + */ + private final String variable; + + /** + * 变量描述 + */ + private final String description; + + /** + * 根据变量名查找枚举 + * + * @param variable 变量名 + * @return 枚举值 + */ + public static DataRuleVariableEnum findByVariable(String variable) { + if (variable == null) { + return null; + } + for (DataRuleVariableEnum value : values()) { + if (value.variable.equals(variable)) { + return value; + } + } + return null; + } +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/push/BusinessTypeEnum.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/push/BusinessTypeEnum.java new file mode 100644 index 00000000..441a239a --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/push/BusinessTypeEnum.java @@ -0,0 +1,70 @@ +package com.zt.plat.module.system.enums.push; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 业务类型枚举 + * + * @author ZT Cloud + */ +@AllArgsConstructor +@Getter +public enum BusinessTypeEnum { + + PURCHASE(1, "PURCHASE", "采购"), + SALE(2, "SALE", "销售"), + PRODUCTION(3, "PRODUCTION", "生产"); + + /** + * 类型 + */ + private final Integer type; + + /** + * 编码 + */ + private final String code; + + /** + * 名称 + */ + private final String name; + + /** + * 根据编码获取枚举 + */ + public static BusinessTypeEnum valueOfCode(String code) { + if (code == null) { + return null; + } + for (BusinessTypeEnum value : BusinessTypeEnum.values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return null; + } + + /** + * 根据类型获取枚举 + */ + public static BusinessTypeEnum valueOfType(Integer type) { + if (type == null) { + return null; + } + for (BusinessTypeEnum value : BusinessTypeEnum.values()) { + if (value.getType().equals(type)) { + return value; + } + } + return null; + } + + /** + * 验证编码是否有效 + */ + public static boolean isValidCode(String code) { + return valueOfCode(code) != null; + } +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackMessage.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackMessage.java new file mode 100644 index 00000000..4eb80a2b --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackMessage.java @@ -0,0 +1,23 @@ +package com.zt.plat.module.system.mq.iwork; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class IWorkBizCallbackMessage { + + /** 统一回调主题 */ + public static final String TOPIC = "SYSTEM_IWORK_BIZ_CALLBACK"; + + /** requestId 唯一标识 */ + private String requestId; + /** 业务回调标识 */ + private String bizCallbackKey; + /** 回调负载对象(可为 Map) */ + private Object payload; + /** 当前尝试次数,从 0 开始 */ + private int attempt; + /** 最大尝试次数 */ + private int maxAttempts; +} diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackResultMessage.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackResultMessage.java new file mode 100644 index 00000000..2a34f72c --- /dev/null +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackResultMessage.java @@ -0,0 +1,31 @@ +package com.zt.plat.module.system.mq.iwork; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class IWorkBizCallbackResultMessage { + + /** 统一回调结果主题 */ + public static final String TOPIC = "SYSTEM_IWORK_BIZ_CALLBACK_RESULT"; + + /** requestId 唯一标识 */ + private String requestId; + /** 业务回调标识,对应发送方设置的 tag */ + private String bizCallbackKey; + /** 是否成功 */ + private boolean success; + /** 错误消息 */ + private String errorMessage; + /** 当前尝试次数(业务侧可回传) */ + private int attempt; + /** 最大尝试次数 */ + private int maxAttempts; + /** 回调负载(用于 system 端重试再投递) */ + private Object payload; +} diff --git a/zt-module-system/zt-module-system-server-app/src/main/resources/logback-spring.xml b/zt-module-system/zt-module-system-server-app/src/main/resources/logback-spring.xml index d68b13bb..b592120f 100644 --- a/zt-module-system/zt-module-system-server-app/src/main/resources/logback-spring.xml +++ b/zt-module-system/zt-module-system-server-app/src/main/resources/logback-spring.xml @@ -91,6 +91,10 @@ + + + + @@ -103,8 +107,4 @@ - - - - diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/dept/DeptApiImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/dept/DeptApiImpl.java index bed2d2b6..8b87fd52 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/dept/DeptApiImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/dept/DeptApiImpl.java @@ -4,6 +4,8 @@ import com.zt.plat.framework.common.enums.CommonStatusEnum; import com.zt.plat.framework.common.pojo.CommonResult; import com.zt.plat.framework.common.pojo.CompanyDeptInfo; import com.zt.plat.framework.common.util.object.BeanUtils; +import com.zt.plat.framework.datapermission.core.annotation.CompanyDataPermissionIgnore; +import com.zt.plat.framework.datapermission.core.annotation.DeptDataPermissionIgnore; import com.zt.plat.module.system.api.dept.dto.*; import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; @@ -78,6 +80,8 @@ public class DeptApiImpl implements DeptApi { } @Override + @CompanyDataPermissionIgnore + @DeptDataPermissionIgnore public CommonResult getDept(Long id) { DeptDO dept = deptService.getDept(id); return success(BeanUtils.toBean(dept, DeptRespDTO.class)); @@ -107,6 +111,12 @@ public class DeptApiImpl implements DeptApi { return success(BeanUtils.toBean(companyDeptInfos, CompanyDeptInfoRespDTO.class)); } + @Override + public CommonResult> upFindCompanyNode(Long deptId) { + List depts = deptService.upFindCompanyNode(deptId); + return success(BeanUtils.toBean(depts, DeptRespDTO.class)); + } + // ========== 数据同步专用接口 ========== @Override diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApiImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApiImpl.java index b12f91f9..eefc2e95 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApiImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/iwork/IWorkIntegrationApiImpl.java @@ -77,7 +77,7 @@ public class IWorkIntegrationApiImpl implements IWorkIntegrationApi { // ----------------- 人力组织分页接口 ----------------- @Override - public CommonResult listSubcompanies(IWorkOrgPageReqDTO reqDTO) { + public CommonResult listSubcompanies(IWorkSubcompanyQueryReqDTO reqDTO) { IWorkSubcompanyQueryReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkSubcompanyQueryReqVO.class); IWorkHrSubcompanyPageRespVO respVO = orgRestService.listSubcompanies(reqVO); IWorkHrSubcompanyPageRespDTO respDTO = BeanUtils.toBean(respVO, IWorkHrSubcompanyPageRespDTO.class); @@ -85,7 +85,7 @@ public class IWorkIntegrationApiImpl implements IWorkIntegrationApi { } @Override - public CommonResult listDepartments(IWorkOrgPageReqDTO reqDTO) { + public CommonResult listDepartments(IWorkDepartmentQueryReqDTO reqDTO) { IWorkDepartmentQueryReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkDepartmentQueryReqVO.class); IWorkHrDepartmentPageRespVO respVO = orgRestService.listDepartments(reqVO); IWorkHrDepartmentPageRespDTO respDTO = BeanUtils.toBean(respVO, IWorkHrDepartmentPageRespDTO.class); @@ -93,7 +93,7 @@ public class IWorkIntegrationApiImpl implements IWorkIntegrationApi { } @Override - public CommonResult listJobTitles(IWorkOrgPageReqDTO reqDTO) { + public CommonResult listJobTitles(IWorkJobTitleQueryReqDTO reqDTO) { IWorkJobTitleQueryReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkJobTitleQueryReqVO.class); IWorkHrJobTitlePageRespVO respVO = orgRestService.listJobTitles(reqVO); IWorkHrJobTitlePageRespDTO respDTO = BeanUtils.toBean(respVO, IWorkHrJobTitlePageRespDTO.class); @@ -101,7 +101,7 @@ public class IWorkIntegrationApiImpl implements IWorkIntegrationApi { } @Override - public CommonResult listUsers(IWorkOrgPageReqDTO reqDTO) { + public CommonResult listUsers(IWorkUserQueryReqDTO reqDTO) { IWorkUserQueryReqVO reqVO = BeanUtils.toBean(reqDTO, IWorkUserQueryReqVO.class); IWorkHrUserPageRespVO respVO = orgRestService.listUsers(reqVO); IWorkHrUserPageRespDTO respDTO = BeanUtils.toBean(respVO, IWorkHrUserPageRespDTO.class); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/push/ExternalPushConfigApiImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/push/ExternalPushConfigApiImpl.java new file mode 100644 index 00000000..7fac6614 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/api/push/ExternalPushConfigApiImpl.java @@ -0,0 +1,28 @@ +package com.zt.plat.module.system.api.push; + +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.module.system.service.push.ExternalPushConfigService; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import static com.zt.plat.framework.common.pojo.CommonResult.success; + +/** + * 外部系统推送配置 Feign API 实现类 + * + * @author ZT Cloud + */ +@RestController +@Validated +public class ExternalPushConfigApiImpl implements ExternalPushConfigApi { + + @Resource + private ExternalPushConfigService externalPushConfigService; + + @Override + public CommonResult isPushEnabled(Long companyId, Long deptId, String businessType, String externalSystem) { + Boolean result = externalPushConfigService.isPushEnabled(companyId, deptId, businessType, externalSystem); + return success(result); + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthLoginReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthLoginReqVO.java index 2e4e2813..61d840cf 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthLoginReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthLoginReqVO.java @@ -6,7 +6,6 @@ import com.zt.plat.module.system.enums.social.SocialTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -22,8 +21,8 @@ public class AuthLoginReqVO extends CaptchaVerificationReqVO { @Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ztyuanma") @NotEmpty(message = "登录账号不能为空") - @Length(min = 4, max = 16, message = "账号长度为 4-16 位") - @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母") + @Length(min = 1, max = 16, message = "账号长度为 1-16 位") +// @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母") private String username; @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthTestLoginReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthTestLoginReqVO.java index 5e9ab950..b43587ee 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthTestLoginReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/auth/vo/AuthTestLoginReqVO.java @@ -2,7 +2,6 @@ package com.zt.plat.module.system.controller.admin.auth.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -19,7 +18,7 @@ public class AuthTestLoginReqVO { @Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ztyuanma") @NotEmpty(message = "登录账号不能为空") @Length(min = 4, max = 16, message = "账号长度为 4-16 位") - @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母") +// @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母") private String username; @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java index 54ba9b37..c2da8184 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/dept/DeptController.java @@ -165,4 +165,11 @@ public class DeptController { return success(BeanUtils.toBean(companyDeptInfos, CompanyDeptInfoRespDTO.class)); } + @GetMapping("/up-find-company-node") + @Operation(summary = "获取公司节点信息", description = "通过部门编号,向上追溯部门信息,直到上级部门是公司,返回追溯到的部门信息列表") + @Parameter(name = "deptId", description = "部门编号", required = true, example = "1024") + public CommonResult> upFindCompanyNode(@RequestParam("deptId") Long deptId) { + List list = deptService.upFindCompanyNode(deptId); + return success(BeanUtils.toBean(list, DeptRespVO.class)); + } } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/IWorkIntegrationController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/IWorkIntegrationController.java index 88adb502..f3b6f08a 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/IWorkIntegrationController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/IWorkIntegrationController.java @@ -3,6 +3,7 @@ package com.zt.plat.module.system.controller.admin.integration.iwork; import com.zt.plat.framework.common.pojo.CommonResult; import com.zt.plat.framework.tenant.core.aop.TenantIgnore; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.*; +import com.zt.plat.module.system.service.integration.iwork.IWorkCallbackLogService; import com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationService; import com.zt.plat.module.system.service.integration.iwork.IWorkOrgRestService; import com.zt.plat.module.system.service.integration.iwork.IWorkSyncService; @@ -19,6 +20,8 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.access.prepost.PreAuthorize; + import static com.zt.plat.framework.common.pojo.CommonResult.success; /** * 提供统一 iWork 流程能力的管理端接口。 @@ -34,6 +37,7 @@ public class IWorkIntegrationController { private final IWorkIntegrationService integrationService; private final IWorkOrgRestService orgRestService; private final IWorkSyncService syncService; + private final IWorkCallbackLogService callbackLogService; @PostMapping("/auth/register") @Operation(summary = "注册 iWork 凭证,获取服务端公钥与 secret") @@ -87,6 +91,39 @@ public class IWorkIntegrationController { return success(integrationService.voidWorkflow(reqVO)); } + @PreAuthorize("@ss.hasPermission('system:iwork:log:query')") + @PostMapping("/log/page") + @Operation(summary = "iWork 回调日志分页查询") + public CommonResult> pageLogs(@Valid @RequestBody IWorkCallbackLogPageReqVO reqVO) { + com.zt.plat.framework.common.pojo.PageResult page = callbackLogService.page(reqVO); + java.util.List mapped = new java.util.ArrayList<>(); + for (com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO log : page.getList()) { + IWorkCallbackLogRespVO vo = new IWorkCallbackLogRespVO(); + vo.setId(log.getId()); + vo.setRequestId(log.getRequestId()); + vo.setBusinessCode(log.getBusinessCode()); + vo.setBizCallbackKey(log.getBizCallbackKey()); + vo.setStatus(log.getStatus()); + vo.setRetryCount(log.getRetryCount()); + vo.setMaxRetry(log.getMaxRetry()); + vo.setLastErrorMessage(log.getLastErrorMessage()); + vo.setRawCallback(log.getRawCallback()); + vo.setLastCallbackTime(log.getLastCallbackTime()); + vo.setCreateTime(log.getCreateTime()); + vo.setUpdateTime(log.getUpdateTime()); + mapped.add(vo); + } + return success(new com.zt.plat.framework.common.pojo.PageResult<>(mapped, page.getTotal(), page.getSummary())); + } + + @PreAuthorize("@ss.hasPermission('system:iwork:log:retry')") + @PostMapping("/log/retry") + @Operation(summary = "iWork 回调手工重试") + public CommonResult retry(@Valid @RequestBody IWorkWorkflowVoidReqVO reqVO) { + callbackLogService.resetAndDispatch(reqVO.getRequestId()); + return success(true); + } + // ----------------- 人力组织接口 ----------------- @PostMapping("/hr/subcompany/page") diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkCallbackLogPageReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkCallbackLogPageReqVO.java new file mode 100644 index 00000000..0eefd069 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkCallbackLogPageReqVO.java @@ -0,0 +1,39 @@ +package com.zt.plat.module.system.controller.admin.integration.iwork.vo; + +import com.zt.plat.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - iWork 用印回调日志分页查询") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IWorkCallbackLogPageReqVO extends PageParam { + + @Schema(description = "requestId") + private String requestId; + + @Schema(description = "业务单号") + private String businessCode; + + @Schema(description = "业务回调标识") + private String bizCallbackKey; + + @Schema(description = "状态") + private Integer status; + + @Schema(description = "创建时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "最后回调时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] lastCallbackTime; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkCallbackLogRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkCallbackLogRespVO.java new file mode 100644 index 00000000..577908f9 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkCallbackLogRespVO.java @@ -0,0 +1,24 @@ +package com.zt.plat.module.system.controller.admin.integration.iwork.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "iWork 用印回调日志响应 VO") +@Data +public class IWorkCallbackLogRespVO { + + private Long id; + private String requestId; + private String businessCode; + private String bizCallbackKey; + private Integer status; + private Integer retryCount; + private Integer maxRetry; + private String lastErrorMessage; + private String rawCallback; + private LocalDateTime lastCallbackTime; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkDepartmentQueryReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkDepartmentQueryReqVO.java index 059f0962..d0ae5b07 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkDepartmentQueryReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkDepartmentQueryReqVO.java @@ -1,5 +1,6 @@ package com.zt.plat.module.system.controller.admin.integration.iwork.vo; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,12 +14,35 @@ import lombok.ToString; @ToString(callSuper = true) public class IWorkDepartmentQueryReqVO extends IWorkOrgBaseQueryReqVO { - @Schema(description = "部门编码") + @JsonProperty("departmentcode") + @Schema(description = "部门编号") private String departmentCode; + @JsonProperty("departmentname") @Schema(description = "部门名称") private String departmentName; - @Schema(description = "所属分部ID") - private String subcompanyId; + @JsonProperty("subcompanyid1") + @Schema(description = "分部 ID") + private String subcompanyId1; + + @JsonProperty("created") + @Schema(description = "创建时间戳(>=)") + private String created; + + @JsonProperty("modified") + @Schema(description = "修改时间戳(>=)") + private String modified; + + @JsonProperty("canceled") + @Schema(description = "封存标志,默认查询非封存数据。1:封存") + private String canceled; + + @JsonProperty("custom_data") + @Schema(description = "自定义字段列表(逗号分隔)") + private String customData; + + @JsonProperty("id") + @Schema(description = "OA 部门 ID") + private String id; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java index 12a5d2b5..cb044d91 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java @@ -2,12 +2,21 @@ package com.zt.plat.module.system.controller.admin.integration.iwork.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.Data; @Schema(name = "IWorkFileCallbackReqVO", description = "iWork 文件回调请求 VO") @Data public class IWorkFileCallbackReqVO { + @Schema(description = "iWork requestId,唯一标识", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "requestId 不能为空") + private String requestId; + + @Schema(description = "业务回调标识 bizCallbackKey,≤255 字符", example = "seal-flow-callback") + @Size(max = 255, message = "bizCallbackKey 长度不能超过 255 字符") + private String bizCallbackKey; + @Schema(description = "文件下载 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://example.com/file.pdf") @NotBlank(message = "文件 URL 不能为空") private String fileUrl; @@ -19,6 +28,9 @@ public class IWorkFileCallbackReqVO { @Schema(description = "文件名称,可选", example = "合同附件.pdf") private String fileName; - @Schema(description = "OA 单点下载使用的 ssoToken,可选", example = "6102A7C13F09DD6B1AF06CDA0E479AC8...") - private String ssoToken; + @Schema(description = "业务回调状态/结果码,可选") + private String status; + + @Schema(description = "原始回调文本(可选,若不传则使用 payload 或请求体序列化)") + private String rawBody; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java index e3793181..74e12911 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java @@ -1,5 +1,6 @@ package com.zt.plat.module.system.controller.admin.integration.iwork.vo; +import com.fasterxml.jackson.annotation.JsonProperty; import com.zt.plat.module.system.enums.integration.IWorkSyncEntityTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; @@ -42,6 +43,70 @@ public class IWorkFullSyncReqVO { @Schema(description = "是否允许更新已存在的本地实体", example = "false") private Boolean allowUpdate = Boolean.FALSE; + @JsonProperty("departmentcode") + @Schema(description = "部门编号") + private String departmentCode; + + @JsonProperty("departmentname") + @Schema(description = "部门名称") + private String departmentName; + + @JsonProperty("subcompanycode") + @Schema(description = "分部编号") + private String subcompanyCode; + + @JsonProperty("subcompanyname") + @Schema(description = "分部名称") + private String subcompanyName; + + @JsonProperty("subcompanyid1") + @Schema(description = "分部 ID") + private String subcompanyId1; + + @JsonProperty("jobtitleid") + @Schema(description = "岗位 ID") + private String jobTitleId; + + @JsonProperty("jobtitlename") + @Schema(description = "岗位名称") + private String jobTitleName; + + @JsonProperty("workcode") + @Schema(description = "人员编号") + private String workCode; + + @JsonProperty("loginid") + @Schema(description = "登录名") + private String loginId; + + @JsonProperty("created") + @Schema(description = "创建时间戳(>=)") + private String created; + + @JsonProperty("modified") + @Schema(description = "修改时间戳(>=)") + private String modified; + + @JsonProperty("canceled") + @Schema(description = "封存标志,默认查询非封存数据。1:封存") + private String canceled; + + @JsonProperty("custom_data") + @Schema(description = "自定义字段列表(逗号分隔)") + private String customData; + + @JsonProperty("base_custom_data") + @Schema(description = "基本信息自定义字段列表(逗号分隔)") + private String baseCustomData; + + @JsonProperty("person_custom_data") + @Schema(description = "个人信息自定义字段列表(逗号分隔)") + private String personCustomData; + + @JsonProperty("work_custom_data") + @Schema(description = "工作信息自定义字段列表(逗号分隔)") + private String workCustomData; + public Set resolveScopes() { EnumSet defaults = EnumSet.allOf(IWorkSyncEntityTypeEnum.class); if (scopes == null || scopes.isEmpty()) { diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkJobTitleQueryReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkJobTitleQueryReqVO.java index 64760eed..191267f9 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkJobTitleQueryReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkJobTitleQueryReqVO.java @@ -1,5 +1,6 @@ package com.zt.plat.module.system.controller.admin.integration.iwork.vo; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,9 +14,19 @@ import lombok.ToString; @ToString(callSuper = true) public class IWorkJobTitleQueryReqVO extends IWorkOrgBaseQueryReqVO { - @Schema(description = "岗位编码") - private String jobTitleCode; - + @JsonProperty("jobtitlename") @Schema(description = "岗位名称") private String jobTitleName; + + @JsonProperty("created") + @Schema(description = "创建时间戳(>=)") + private String created; + + @JsonProperty("modified") + @Schema(description = "修改时间戳(>=)") + private String modified; + + @JsonProperty("id") + @Schema(description = "岗位 ID") + private String id; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaTokenReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaTokenReqVO.java index bfbd488f..37df96b0 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaTokenReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOaTokenReqVO.java @@ -14,6 +14,6 @@ public class IWorkOaTokenReqVO { @NotBlank(message = "loginId 不能为空") private String loginId; - @Schema(description = "应用 appid,未填则使用配置默认值", example = "a17ca6ca-88b0-463e-bffa-7995086bf225") + @Schema(description = "应用 appid,已固定使用配置值,无需传递", example = "") private String appId; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgBaseQueryReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgBaseQueryReqVO.java index 58be39c6..e39d5f02 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgBaseQueryReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkOrgBaseQueryReqVO.java @@ -3,7 +3,6 @@ package com.zt.plat.module.system.controller.admin.integration.iwork.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import java.util.Map; /** * iWork 组织查询基础参数。 @@ -16,7 +15,4 @@ public class IWorkOrgBaseQueryReqVO { @Schema(description = "每页条数", example = "10") private Integer pagesize; - - @Schema(description = "查询参数(扩展用),将被序列化为 params 传给 iWork") - private Map params; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSubcompanyQueryReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSubcompanyQueryReqVO.java index 781613e7..768e559f 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSubcompanyQueryReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkSubcompanyQueryReqVO.java @@ -1,5 +1,6 @@ package com.zt.plat.module.system.controller.admin.integration.iwork.vo; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,9 +14,27 @@ import lombok.ToString; @ToString(callSuper = true) public class IWorkSubcompanyQueryReqVO extends IWorkOrgBaseQueryReqVO { - @Schema(description = "分部编码") + @JsonProperty("subcompanycode") + @Schema(description = "分部编号") private String subcompanyCode; + @JsonProperty("subcompanyname") @Schema(description = "分部名称") private String subcompanyName; + + @JsonProperty("modified") + @Schema(description = "修改时间戳(>=)") + private String modified; + + @JsonProperty("canceled") + @Schema(description = "封存标志,默认查询非封存数据。1:封存") + private String canceled; + + @JsonProperty("custom_data") + @Schema(description = "自定义字段列表(逗号分隔)") + private String customData; + + @JsonProperty("id") + @Schema(description = "OA 分部 ID") + private String id; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkUserQueryReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkUserQueryReqVO.java index 3e4b73fd..5f1fb2f1 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkUserQueryReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkUserQueryReqVO.java @@ -1,5 +1,6 @@ package com.zt.plat.module.system.controller.admin.integration.iwork.vo; +import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,27 +14,47 @@ import lombok.ToString; @ToString(callSuper = true) public class IWorkUserQueryReqVO extends IWorkOrgBaseQueryReqVO { - @Schema(description = "人员工号") + @JsonProperty("workcode") + @Schema(description = "人员编号") private String workCode; - @Schema(description = "人员姓名") - private String lastName; + @JsonProperty("subcompanyid1") + @Schema(description = "分部 ID") + private String subcompanyId1; - @Schema(description = "所属部门ID") + @JsonProperty("departmentid") + @Schema(description = "部门 ID") private String departmentId; - @Schema(description = "所属分部ID") - private String subcompanyId; - - @Schema(description = "所属岗位ID") + @JsonProperty("jobtitleid") + @Schema(description = "岗位 ID") private String jobTitleId; - @Schema(description = "人员状态 (0:试用, 1:正式, 2:临时, 3:试用延期, 4:解聘, 5:离职, 6:退休, 7:无效)") - private String status; + @JsonProperty("id") + @Schema(description = "人员 ID") + private String id; - @Schema(description = "手机号") - private String mobile; + @JsonProperty("loginid") + @Schema(description = "登录名") + private String loginId; - @Schema(description = "邮箱") - private String email; + @JsonProperty("created") + @Schema(description = "创建时间戳(>=)") + private String created; + + @JsonProperty("modified") + @Schema(description = "修改时间戳(>=)") + private String modified; + + @JsonProperty("base_custom_data") + @Schema(description = "基本信息自定义字段列表(逗号分隔)") + private String baseCustomData; + + @JsonProperty("person_custom_data") + @Schema(description = "个人信息自定义字段列表(逗号分隔)") + private String personCustomData; + + @JsonProperty("work_custom_data") + @Schema(description = "工作信息自定义字段列表(逗号分隔)") + private String workCustomData; } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCallbackReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCallbackReqVO.java new file mode 100644 index 00000000..66cf7d87 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCallbackReqVO.java @@ -0,0 +1,26 @@ +package com.zt.plat.module.system.controller.admin.integration.iwork.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Schema(description = "iWork 流程回调请求") +@Data +public class IWorkWorkflowCallbackReqVO { + + @Schema(description = "iWork requestId,唯一标识", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "requestId 不能为空") + private String requestId; + + @Schema(description = "业务单号 (ywxtdjbh)") + private String businessCode; + + @Schema(description = "业务回调标识 bizCallbackKey") + private String bizCallbackKey; + + @Schema(description = "回调状态/结果码") + private String status; + + @Schema(description = "原始回调文本(可截断存储)") + private String rawBody; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCreateReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCreateReqVO.java index 1b2fcc54..2430fc04 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCreateReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkWorkflowCreateReqVO.java @@ -34,9 +34,9 @@ public class IWorkWorkflowCreateReqVO extends IWorkBaseReqVO { @Schema(description = "用印材料附件 URL(必填)") private String xyywjUrl; - - @Schema(description = "用印材料附件文件名(必填)") - private String xyywjFileName; + + @Schema(description = "业务回调标识(回调分发使用,≤255 字符)") + private String bizCallbackKey; @Schema(description = "用印事项") private String yysx; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/MenuDataRuleController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/MenuDataRuleController.java new file mode 100644 index 00000000..be27f6c4 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/MenuDataRuleController.java @@ -0,0 +1,77 @@ +package com.zt.plat.module.system.controller.admin.permission; + +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleRespVO; +import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleSaveReqVO; +import com.zt.plat.module.system.convert.permission.MenuDataRuleConvert; +import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO; +import com.zt.plat.module.system.service.permission.MenuDataRuleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static com.zt.plat.framework.common.pojo.CommonResult.success; + +/** + * 菜单数据规则 Controller + * + * @author ZT + */ +@Tag(name = "管理后台 - 菜单数据规则") +@RestController +@RequestMapping("/system/menu-data-rule") +@Validated +public class MenuDataRuleController { + + @Resource + private MenuDataRuleService menuDataRuleService; + + @PostMapping("/create") + @Operation(summary = "创建菜单数据规则") + @PreAuthorize("@ss.hasPermission('system:menu:update')") + public CommonResult createMenuDataRule(@Valid @RequestBody MenuDataRuleSaveReqVO createReqVO) { + return success(menuDataRuleService.createMenuDataRule(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新菜单数据规则") + @PreAuthorize("@ss.hasPermission('system:menu:update')") + public CommonResult updateMenuDataRule(@Valid @RequestBody MenuDataRuleSaveReqVO updateReqVO) { + menuDataRuleService.updateMenuDataRule(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除菜单数据规则") + @Parameter(name = "id", description = "规则ID", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:menu:update')") + public CommonResult deleteMenuDataRule(@RequestParam("id") Long id) { + menuDataRuleService.deleteMenuDataRule(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得菜单数据规则") + @Parameter(name = "id", description = "规则ID", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:menu:query')") + public CommonResult getMenuDataRule(@RequestParam("id") Long id) { + MenuDataRuleDO rule = menuDataRuleService.getMenuDataRule(id); + return success(MenuDataRuleConvert.INSTANCE.convert(rule)); + } + + @GetMapping("/list") + @Operation(summary = "获得菜单的所有数据规则") + @Parameter(name = "menuId", description = "菜单ID", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('system:menu:query')") + public CommonResult> getMenuDataRuleList(@RequestParam("menuId") Long menuId) { + List list = menuDataRuleService.getMenuDataRuleListByMenuId(menuId); + return success(MenuDataRuleConvert.INSTANCE.convertList(list)); + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java index d84bc342..cd1d2728 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/PermissionController.java @@ -66,6 +66,9 @@ public class PermissionController { PermissionAssignRoleMenuItemReqVO reqVO = new PermissionAssignRoleMenuItemReqVO(); reqVO.setId(menu.getMenuId()); reqVO.setShowMenu(menu.getShowMenu()); + // 获取该角色在该菜单下的数据规则ID列表 + Set dataRuleIds = permissionService.getRoleMenuDataRules(roleId, menu.getMenuId()); + reqVO.setDataRuleIds(dataRuleIds != null ? new ArrayList<>(dataRuleIds) : null); return reqVO; }).collect(Collectors.toSet()); return success(result); @@ -83,6 +86,10 @@ public class PermissionController { // 更新菜单的显示状态 permissionService.updateMenuDisplay(reqVO.getRoleId(), reqVO.getMenus()); + + // 保存菜单数据规则关联 + permissionService.assignRoleMenuDataRules(reqVO.getRoleId(), reqVO.getMenus()); + return success(true); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/RoleController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/RoleController.java index 74845b6c..86186e3b 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/RoleController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/RoleController.java @@ -11,6 +11,7 @@ import com.zt.plat.module.system.controller.admin.permission.vo.role.RolePageReq import com.zt.plat.module.system.controller.admin.permission.vo.role.RoleRespVO; import com.zt.plat.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; import com.zt.plat.module.system.dal.dataobject.permission.RoleDO; +import com.zt.plat.framework.datapermission.core.menudatapermission.annotation.PermissionData; import com.zt.plat.module.system.service.permission.RoleService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -78,6 +79,7 @@ public class RoleController { @GetMapping("/page") @Operation(summary = "获得角色分页") @PreAuthorize("@ss.hasPermission('system:role:query')") + @PermissionData(pageComponent = "system/role/index") public CommonResult> getRolePage(RolePageReqVO pageReqVO) { PageResult pageResult = roleService.getRolePage(pageReqVO); // 获取所有父级角色信息 diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleRespVO.java new file mode 100644 index 00000000..4b624dea --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleRespVO.java @@ -0,0 +1,46 @@ +package com.zt.plat.module.system.controller.admin.permission.vo.menudatarule; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 菜单数据规则 Response VO + * + * @author ZT + */ +@Schema(description = "管理后台 - 菜单数据规则 Response VO") +@Data +public class MenuDataRuleRespVO { + + @Schema(description = "规则ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "菜单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long menuId; + + @Schema(description = "规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "仅看本部门数据") + private String ruleName; + + @Schema(description = "规则字段", example = "dept_id") + private String ruleColumn; + + @Schema(description = "规则条件", requiredMode = Schema.RequiredMode.REQUIRED, example = "=") + private String ruleConditions; + + @Schema(description = "规则值", requiredMode = Schema.RequiredMode.REQUIRED, example = "#{deptId}") + private String ruleValue; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "排序", example = "1") + private Integer sort; + + @Schema(description = "备注", example = "限制只能查看本部门数据") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleSaveReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleSaveReqVO.java new file mode 100644 index 00000000..d3044877 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/menudatarule/MenuDataRuleSaveReqVO.java @@ -0,0 +1,53 @@ +package com.zt.plat.module.system.controller.admin.permission.vo.menudatarule; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 菜单数据规则创建/修改 Request VO + * + * @author ZT + */ +@Schema(description = "管理后台 - 菜单数据规则创建/修改 Request VO") +@Data +public class MenuDataRuleSaveReqVO { + + @Schema(description = "规则ID", example = "1024") + private Long id; + + @Schema(description = "菜单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "菜单ID不能为空") + private Long menuId; + + @Schema(description = "规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "仅看本部门数据") + @NotBlank(message = "规则名称不能为空") + @Size(max = 100, message = "规则名称长度不能超过 100 个字符") + private String ruleName; + + @Schema(description = "规则字段", example = "dept_id") + @Size(max = 100, message = "规则字段长度不能超过 100 个字符") + private String ruleColumn; + + @Schema(description = "规则条件", requiredMode = Schema.RequiredMode.REQUIRED, example = "=") + @NotBlank(message = "规则条件不能为空") + @Size(max = 20, message = "规则条件长度不能超过 20 个字符") + private String ruleConditions; + + @Schema(description = "规则值", requiredMode = Schema.RequiredMode.REQUIRED, example = "#{deptId}") + @NotBlank(message = "规则值不能为空") + @Size(max = 500, message = "规则值长度不能超过 500 个字符") + private String ruleValue; + + @Schema(description = "状态", example = "1") + private Integer status; + + @Schema(description = "排序", example = "1") + private Integer sort; + + @Schema(description = "备注", example = "限制只能查看本部门数据") + @Size(max = 500, message = "备注长度不能超过 500 个字符") + private String remark; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuItemReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuItemReqVO.java index 0d038266..576274b9 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuItemReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuItemReqVO.java @@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; +import java.util.List; + @Schema(description = "管理后台 - 赋予角色菜单--菜单列表 Request VO") @Data @@ -19,4 +21,7 @@ public class PermissionAssignRoleMenuItemReqVO { @Schema(description = "是否显示菜单按钮是否点击过(避免大量更新数据,只更新点击过的)") private Boolean showMenuChanged = false; + @Schema(description = "菜单数据规则ID列表", example = "[1, 2, 3]") + private List dataRuleIds; + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/portal/PortalController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/portal/PortalController.java index c956f82e..eb8625a7 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/portal/PortalController.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/portal/PortalController.java @@ -4,6 +4,9 @@ import com.zt.plat.framework.common.pojo.CommonResult; import com.zt.plat.framework.common.pojo.PageResult; import com.zt.plat.framework.common.util.object.BeanUtils; import com.zt.plat.framework.excel.core.util.ExcelUtils; +import com.zt.plat.framework.tenant.core.aop.TenantIgnore; +import com.zt.plat.module.infra.api.file.FileApi; +import com.zt.plat.module.infra.api.file.dto.FileRespDTO; import com.zt.plat.module.system.controller.admin.portal.vo.PortalPageReqVO; import com.zt.plat.module.system.controller.admin.portal.vo.PortalRespVO; import com.zt.plat.module.system.controller.admin.portal.vo.PortalSaveReqVO; @@ -13,6 +16,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; @@ -20,7 +24,12 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; + +import org.springframework.util.StringUtils; import static com.zt.plat.framework.common.pojo.CommonResult.success; import static com.zt.plat.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @@ -39,6 +48,9 @@ public class PortalController { @Resource private PortalService portalService; + @Resource + private FileApi fileApi; + @PostMapping("/create") @Operation(summary = "创建门户网站") @PreAuthorize("@ss.hasPermission('system:portal:create')") @@ -97,9 +109,60 @@ public class PortalController { */ @GetMapping("/list") @Operation(summary = "获取我的门户列表") + @PermitAll + @TenantIgnore public CommonResult> getMyPortalList() { - Long userId = getLoginUserId(); - List portals = portalService.getPortalListByUserId(userId); + Long userId = null; + try { + userId = getLoginUserId(); + } catch (Exception ignored) { + // 未登录时获取公开门户 + } + List portals = (userId == null) + ? portalService.getPublicPortalList() + : portalService.getPortalListByUserId(userId); return success(BeanUtils.toBean(portals, PortalRespVO.class)); } + + /** + * 匿名获取公开门户的图标文件信息 + * 仅允许访问门户中已配置的图标文件 + */ + @GetMapping("/public-icon-files") + @Operation(summary = "获取公开门户图标文件信息") + @PermitAll + @TenantIgnore + public CommonResult> getPublicPortalIconFiles(@RequestParam("fileIds") List fileIds) { + if (fileIds == null || fileIds.isEmpty()) { + return success(java.util.Collections.emptyList()); + } + + List portals = portalService.getPublicPortalList(); + Set allowedFileIds = new HashSet<>(); + for (PortalDO portal : portals) { + if (portal.getIconType() == null || portal.getIconType() != 2) { + continue; + } + if (!StringUtils.hasText(portal.getIconFileId())) { + continue; + } + try { + allowedFileIds.add(Long.parseLong(portal.getIconFileId())); + } catch (NumberFormatException ignored) { + // ignore invalid fileId + } + } + + List result = new ArrayList<>(); + for (Long fileId : fileIds) { + if (!allowedFileIds.contains(fileId)) { + continue; + } + CommonResult fileResult = fileApi.getFileInfo(fileId); + if (fileResult != null && fileResult.isSuccess() && fileResult.getData() != null) { + result.add(fileResult.getData()); + } + } + return success(result); + } } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/ExternalPushConfigController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/ExternalPushConfigController.java new file mode 100644 index 00000000..1e3b160b --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/ExternalPushConfigController.java @@ -0,0 +1,160 @@ +package com.zt.plat.module.system.controller.admin.push; + +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.framework.common.util.collection.CollectionUtils; +import com.zt.plat.framework.common.util.object.BeanUtils; +import com.zt.plat.module.system.controller.admin.push.vo.BusinessTypeRespVO; +import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigPageReqVO; +import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigRespVO; +import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigSaveReqVO; +import com.zt.plat.module.system.dal.dataobject.dept.DeptDO; +import com.zt.plat.module.system.dal.dataobject.push.ExternalPushConfigDO; +import com.zt.plat.module.system.enums.push.BusinessTypeEnum; +import com.zt.plat.module.system.service.dept.DeptService; +import com.zt.plat.module.system.service.push.ExternalPushConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.zt.plat.framework.common.pojo.CommonResult.success; + +/** + * 外部系统推送配置 Controller + * + * @author ZT Cloud + */ +@Tag(name = "管理后台 - 外部系统推送配置") +@RestController +@RequestMapping("/system/external-push-config") +@Validated +public class ExternalPushConfigController { + + @Resource + private ExternalPushConfigService externalPushConfigService; + @Resource + private DeptService deptService; + + @PostMapping("/create") + @Operation(summary = "创建推送配置") + @PreAuthorize("@ss.hasPermission('system:external-push-config:create')") + public CommonResult create(@Valid @RequestBody ExternalPushConfigSaveReqVO createReqVO) { + Long id = externalPushConfigService.createExternalPushConfig(createReqVO); + return success(id); + } + + @PutMapping("/update") + @Operation(summary = "修改推送配置") + @PreAuthorize("@ss.hasPermission('system:external-push-config:update')") + public CommonResult update(@Valid @RequestBody ExternalPushConfigSaveReqVO updateReqVO) { + externalPushConfigService.updateExternalPushConfig(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除推送配置") + @PreAuthorize("@ss.hasPermission('system:external-push-config:delete')") + public CommonResult delete(@RequestParam("id") Long id) { + externalPushConfigService.deleteExternalPushConfig(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取推送配置详情") + @PreAuthorize("@ss.hasPermission('system:external-push-config:query')") + public CommonResult get(@RequestParam("id") Long id) { + ExternalPushConfigDO entity = externalPushConfigService.getExternalPushConfig(id); + ExternalPushConfigRespVO respVO = BeanUtils.toBean(entity, ExternalPushConfigRespVO.class); + fillDeptInfo(List.of(respVO)); + return success(respVO); + } + + @GetMapping("/page") + @Operation(summary = "分页查询推送配置") + @PreAuthorize("@ss.hasPermission('system:external-push-config:query')") + public CommonResult> page(@Valid ExternalPushConfigPageReqVO reqVO) { + PageResult pageResult = externalPushConfigService.getExternalPushConfigPage(reqVO); + PageResult result = BeanUtils.toBean(pageResult, ExternalPushConfigRespVO.class); + fillDeptInfo(result.getList()); + return success(result); + } + + @GetMapping("/is-push-enabled") + @Operation(summary = "判断是否允许推送") + @Parameter(name = "companyId", description = "公司编号(为空表示不限制公司)") + @Parameter(name = "deptId", description = "部门编号") + @Parameter(name = "businessType", description = "业务类型(为空表示所有业务类型)") + @Parameter(name = "externalSystem", description = "外部系统标识(为空表示所有外部系统)") + @PreAuthorize("@ss.hasPermission('system:external-push-config:query')") + public CommonResult isPushEnabled( + @RequestParam(value = "companyId", required = false) Long companyId, + @RequestParam(value = "deptId", required = false) Long deptId, + @RequestParam(value = "businessType", required = false) String businessType, + @RequestParam(value = "externalSystem", required = false) String externalSystem) { + Boolean result = externalPushConfigService.isPushEnabled(companyId, deptId, businessType, externalSystem); + return success(result); + } + + @GetMapping("/business-types") + @Operation(summary = "获取业务类型列表") + public CommonResult> getBusinessTypes() { + List result = Arrays.stream(BusinessTypeEnum.values()) + .map(e -> new BusinessTypeRespVO(e.getType(), e.getCode(), e.getName())) + .collect(Collectors.toList()); + return success(result); + } + + /** + * 填充公司和部门名称 + */ + private void fillDeptInfo(List list) { + if (list == null || list.isEmpty()) { + return; + } + + // 收集所有公司ID和部门ID + Set deptIds = list.stream() + .flatMap(item -> { + Set ids = new java.util.HashSet<>(); + ids.add(item.getCompanyId()); + if (item.getDeptId() != null) { + ids.add(item.getDeptId()); + } + return ids.stream(); + }) + .collect(Collectors.toSet()); + + if (deptIds.isEmpty()) { + return; + } + + // 批量查询部门信息 + Map deptMap = CollectionUtils.convertMap( + deptService.getDeptList(deptIds), DeptDO::getId); + + // 填充名称 + list.forEach(item -> { + DeptDO company = deptMap.get(item.getCompanyId()); + if (company != null) { + item.setCompanyName(company.getName()); + } + if (item.getDeptId() != null) { + DeptDO dept = deptMap.get(item.getDeptId()); + if (dept != null) { + item.setDeptName(dept.getName()); + } + } + }); + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/BusinessTypeRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/BusinessTypeRespVO.java new file mode 100644 index 00000000..5dc83f66 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/BusinessTypeRespVO.java @@ -0,0 +1,22 @@ +package com.zt.plat.module.system.controller.admin.push.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 业务类型 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class BusinessTypeRespVO { + + @Schema(description = "类型", example = "1") + private Integer type; + + @Schema(description = "编码", example = "PURCHASE") + private String code; + + @Schema(description = "名称", example = "采购") + private String name; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigBaseVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigBaseVO.java new file mode 100644 index 00000000..fd385e70 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigBaseVO.java @@ -0,0 +1,33 @@ +package com.zt.plat.module.system.controller.admin.push.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Schema(description = "管理后台 - 外部系统推送配置基础信息") +@Data +public class ExternalPushConfigBaseVO { + + @Schema(description = "公司编号(为空表示不限制公司)", example = "1024") + private Long companyId; + + @Schema(description = "部门编号(为空表示公司级配置)", example = "2048") + private Long deptId; + + @Schema(description = "业务类型(为空表示所有业务类型)", example = "PURCHASE") + @Size(max = 32, message = "业务类型长度不能超过 32 个字符") + private String businessType; + + @Schema(description = "外部系统标识(为空表示所有外部系统)", example = "ERP") + @Size(max = 64, message = "外部系统标识长度不能超过 64 个字符") + private String externalSystem; + + @Schema(description = "是否启用推送", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "推送开关不能为空") + private Boolean enablePush; + + @Schema(description = "备注", example = "ERP 采购单推送配置") + @Size(max = 512, message = "备注长度不能超过 512 个字符") + private String remark; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigPageReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigPageReqVO.java new file mode 100644 index 00000000..2ac1eb4e --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigPageReqVO.java @@ -0,0 +1,27 @@ +package com.zt.plat.module.system.controller.admin.push.vo; + +import com.zt.plat.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 外部系统推送配置分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ExternalPushConfigPageReqVO extends PageParam { + + @Schema(description = "公司编号", example = "1024") + private Long companyId; + + @Schema(description = "部门编号", example = "2048") + private Long deptId; + + @Schema(description = "业务类型", example = "PURCHASE") + private String businessType; + + @Schema(description = "外部系统标识", example = "ERP") + private String externalSystem; + + @Schema(description = "是否启用推送", example = "true") + private Boolean enablePush; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigRespVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigRespVO.java new file mode 100644 index 00000000..7e2e5763 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigRespVO.java @@ -0,0 +1,28 @@ +package com.zt.plat.module.system.controller.admin.push.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 外部系统推送配置 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ExternalPushConfigRespVO extends ExternalPushConfigBaseVO { + + @Schema(description = "配置编号", example = "1024") + private Long id; + + @Schema(description = "公司名称", example = "浙江中天建设集团") + private String companyName; + + @Schema(description = "部门名称", example = "采购部") + private String deptName; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "最后更新时间") + private LocalDateTime updateTime; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigSaveReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigSaveReqVO.java new file mode 100644 index 00000000..3a51a5f8 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/push/vo/ExternalPushConfigSaveReqVO.java @@ -0,0 +1,14 @@ +package com.zt.plat.module.system.controller.admin.push.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 外部系统推送配置创建/修改 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ExternalPushConfigSaveReqVO extends ExternalPushConfigBaseVO { + + @Schema(description = "配置编号", example = "1024") + private Long id; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSaveReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSaveReqVO.java index 842ee71c..7d11c31f 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSaveReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/user/vo/user/UserSaveReqVO.java @@ -10,7 +10,10 @@ import com.zt.plat.module.system.framework.operatelog.core.DeptParseFunction; import com.zt.plat.module.system.framework.operatelog.core.PostParseFunction; import com.zt.plat.module.system.framework.operatelog.core.SexParseFunction; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.*; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.Data; import org.hibernate.validator.constraints.Length; @@ -28,7 +31,7 @@ public class UserSaveReqVO { @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "zt") @NotBlank(message = "用户账号不能为空") - @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "用户账号由 数字、字母 组成") +// @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "用户账号由 数字、字母 组成") @Size(min = 1, max = 30, message = "用户账号长度为 1-30 个字符") @DiffLogField(name = "用户账号") private String username; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/app/portal/AppPortalController.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/app/portal/AppPortalController.java deleted file mode 100644 index a1386fe7..00000000 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/app/portal/AppPortalController.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.zt.plat.module.system.controller.app.portal; - -import com.zt.plat.framework.common.pojo.CommonResult; -import com.zt.plat.framework.common.util.object.BeanUtils; -import com.zt.plat.module.system.controller.admin.portal.vo.PortalRespVO; -import com.zt.plat.module.system.dal.dataobject.portal.PortalDO; -import com.zt.plat.module.system.service.portal.PortalService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -import static com.zt.plat.framework.common.pojo.CommonResult.success; -import static com.zt.plat.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; - -/** - * 用户端 - 门户网站 Controller - * - * @author 中铜数字供应链平台 - */ -@Tag(name = "用户端 - 门户网站") -@RestController -@RequestMapping("/system/portal") -@Validated -public class AppPortalController { - - @Resource - private PortalService portalService; - - /** - * 获取当前用户可访问的门户列表 - * 此接口无需权限验证,因为已经通过登录验证, - * 返回的门户列表已经根据用户权限进行了过滤 - */ - @GetMapping("/list") - @Operation(summary = "获取我的门户列表") - public CommonResult> getMyPortalList() { - Long userId = getLoginUserId(); - List portals = portalService.getPortalListByUserId(userId); - return success(BeanUtils.toBean(portals, PortalRespVO.class)); - } - -} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/permission/MenuDataRuleConvert.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/permission/MenuDataRuleConvert.java new file mode 100644 index 00000000..1841074b --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/convert/permission/MenuDataRuleConvert.java @@ -0,0 +1,26 @@ +package com.zt.plat.module.system.convert.permission; + +import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleRespVO; +import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleSaveReqVO; +import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 菜单数据规则 Convert + * + * @author ZT + */ +@Mapper +public interface MenuDataRuleConvert { + + MenuDataRuleConvert INSTANCE = Mappers.getMapper(MenuDataRuleConvert.class); + + MenuDataRuleDO convert(MenuDataRuleSaveReqVO bean); + + MenuDataRuleRespVO convert(MenuDataRuleDO bean); + + List convertList(List list); +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/iwork/IWorkSealLogDO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/iwork/IWorkSealLogDO.java new file mode 100644 index 00000000..09e1d325 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/iwork/IWorkSealLogDO.java @@ -0,0 +1,72 @@ +package com.zt.plat.module.system.dal.dataobject.iwork; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.zt.plat.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * iWork 用印流程回调日志。 + */ +@TableName("system_iwork_seal_log") +@KeySequence("system_iwork_seal_log_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class IWorkSealLogDO extends BaseDO { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * iWork 返回的请求编号,唯一业务标识。 + */ + private String requestId; + + /** + * 业务单号(ywxtdjbh)。 + */ + private String businessCode; + + /** + * 业务回调标识。 + */ + private String bizCallbackKey; + + /** + * 状态枚举,参考 IWorkCallbackStatusEnum。 + */ + private Integer status; + + /** + * 已执行的自动/手工重试次数。 + */ + private Integer retryCount; + + /** + * 最大重试次数(快照)。 + */ + private Integer maxRetry; + + /** + * 最后一次错误信息。 + */ + private String lastErrorMessage; + + /** + * 回调原始负载(截断)。 + */ + private String rawCallback; + + /** + * 最近一次回调时间。 + */ + private LocalDateTime lastCallbackTime; + +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/MenuDataRuleDO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/MenuDataRuleDO.java new file mode 100644 index 00000000..f8843af7 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/MenuDataRuleDO.java @@ -0,0 +1,67 @@ +package com.zt.plat.module.system.dal.dataobject.permission; + +import com.zt.plat.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 菜单数据规则 DO + * + * @author ZT + */ +@TableName("system_menu_data_rule") +@KeySequence("system_menu_data_rule_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class MenuDataRuleDO extends TenantBaseDO { + + /** + * 规则ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 菜单ID + */ + private Long menuId; + + /** + * 规则名称 + */ + private String ruleName; + + /** + * 规则字段(数据库列名) + */ + private String ruleColumn; + + /** + * 规则条件(=、>、<、IN、LIKE等) + */ + private String ruleConditions; + + /** + * 规则值(支持变量如#{userId}、#{deptId}) + */ + private String ruleValue; + + /** + * 状态(0=禁用 1=启用) + */ + private Integer status; + + /** + * 排序 + */ + private Integer sort; + + /** + * 备注 + */ + private String remark; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/RoleMenuDataRuleDO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/RoleMenuDataRuleDO.java new file mode 100644 index 00000000..f5d9ae2f --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/permission/RoleMenuDataRuleDO.java @@ -0,0 +1,42 @@ +package com.zt.plat.module.system.dal.dataobject.permission; + +import com.zt.plat.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 角色菜单数据规则关联 DO + * + * @author ZT + */ +@TableName("system_role_menu_data_rule") +@KeySequence("system_role_menu_data_rule_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleMenuDataRuleDO extends TenantBaseDO { + + /** + * 自增主键 + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 角色ID + */ + private Long roleId; + + /** + * 菜单ID + */ + private Long menuId; + + /** + * 数据规则ID + */ + private Long dataRuleId; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/push/ExternalPushConfigDO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/push/ExternalPushConfigDO.java new file mode 100644 index 00000000..b42ae1b9 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/dataobject/push/ExternalPushConfigDO.java @@ -0,0 +1,76 @@ +package com.zt.plat.module.system.dal.dataobject.push; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.zt.plat.framework.tenant.core.db.TenantBaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 外部系统推送配置 DO + * + * 用于配置不同公司/部门/业务类型下的外部系统推送开关 + * + * @author ZT Cloud + */ +@TableName("system_external_push_config") +@KeySequence("system_external_push_config_seq") +@Data +@EqualsAndHashCode(callSuper = true) +public class ExternalPushConfigDO extends TenantBaseDO { + + /** + * 主键编号 + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 公司编号(可为空) + * + * 关联 system_dept 表,is_company = 1 + * 为空表示不限制公司 + */ + private Long companyId; + + /** + * 部门编号(可为空) + * + * 关联 system_dept 表,is_company = 0 + * 为空表示公司级配置 + */ + private Long deptId; + + /** + * 业务类型(可为空) + * + * 枚举值:PURCHASE, SALE, PRODUCTION + * 为空表示所有业务类型 + * 枚举 {@link com.zt.plat.module.system.enums.push.BusinessTypeEnum} + */ + private String businessType; + + /** + * 外部系统标识(可为空) + * + * 如:ERP, IWORK + * 为空表示所有外部系统 + * 枚举 {@link com.zt.plat.module.system.enums.dept.ExternalPlatformEnum} + */ + private String externalSystem; + + /** + * 是否启用推送 + * + * true:启用推送 + * false:停用推送 + */ + private Boolean enablePush; + + /** + * 备注 + */ + private String remark; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java index a8416485..00029e99 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/DeptMapper.java @@ -10,6 +10,8 @@ import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX; import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import com.zt.plat.module.system.dal.dataobject.dept.DeptDO; import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import java.util.Collection; import java.util.List; @@ -167,4 +169,9 @@ public interface DeptMapper extends BaseMapperX { ); } + @Select(""" + SELECT sd.* FROM SYSTEM_DEPT sd START WITH sd.id = #{deptId} + CONNECT BY PRIOR sd.parent_id = sd.id AND PRIOR sd.is_company <> 1 ; + """) + List upFindCompanyNode(@Param("deptId") Long deptId); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/PostMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/PostMapper.java index 4556ce9a..d1bd29d5 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/PostMapper.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/dept/PostMapper.java @@ -9,6 +9,7 @@ import org.apache.ibatis.annotations.Mapper; import java.util.Collection; import java.util.List; +import java.util.Collections; @Mapper public interface PostMapper extends BaseMapperX { @@ -35,4 +36,12 @@ public interface PostMapper extends BaseMapperX { return selectOne(PostDO::getCode, code); } + default List selectByCodes(Collection codes) { + if (codes == null || codes.isEmpty()) { + return Collections.emptyList(); + } + return selectList(new LambdaQueryWrapperX() + .in(PostDO::getCode, codes)); + } + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/iwork/IWorkSealLogMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/iwork/IWorkSealLogMapper.java new file mode 100644 index 00000000..55ac9ee6 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/iwork/IWorkSealLogMapper.java @@ -0,0 +1,27 @@ +package com.zt.plat.module.system.dal.mysql.iwork; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; +import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkCallbackLogPageReqVO; +import com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface IWorkSealLogMapper extends BaseMapperX { + + default IWorkSealLogDO selectByRequestId(String requestId) { + return selectOne(IWorkSealLogDO::getRequestId, requestId); + } + + default PageResult selectPage(IWorkCallbackLogPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(IWorkSealLogDO::getRequestId, reqVO.getRequestId()) + .eqIfPresent(IWorkSealLogDO::getBusinessCode, reqVO.getBusinessCode()) + .eqIfPresent(IWorkSealLogDO::getBizCallbackKey, reqVO.getBizCallbackKey()) + .eqIfPresent(IWorkSealLogDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(IWorkSealLogDO::getCreateTime, reqVO.getCreateTime()) + .betweenIfPresent(IWorkSealLogDO::getLastCallbackTime, reqVO.getLastCallbackTime()) + .orderByDesc(IWorkSealLogDO::getId)); + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuDataRuleMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuDataRuleMapper.java new file mode 100644 index 00000000..2f8aa889 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuDataRuleMapper.java @@ -0,0 +1,59 @@ +package com.zt.plat.module.system.dal.mysql.permission; + +import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; +import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.Collection; +import java.util.List; + +/** + * 菜单数据规则 Mapper + * + * @author ZT + */ +@Mapper +public interface MenuDataRuleMapper extends BaseMapperX { + + /** + * 根据菜单ID查询规则列表 + * + * @param menuId 菜单ID + * @return 规则列表 + */ + default List selectListByMenuId(Long menuId) { + return selectList(MenuDataRuleDO::getMenuId, menuId); + } + + /** + * 根据角色和菜单查询规则ID列表 + * + * @param roleIds 角色ID集合 + * @param menuId 菜单ID + * @return 规则ID列表 + */ + @Select("") + List selectRuleIdsByRoleAndMenu(@Param("roleIds") Collection roleIds, + @Param("menuId") Long menuId); + + /** + * 批量查询菜单的规则 + * + * @param menuIds 菜单ID集合 + * @return 规则列表 + */ + default List selectListByMenuIds(Collection menuIds) { + return selectList("menu_id", menuIds); + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuMapper.java index 60a4af8b..5f9b4a3d 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuMapper.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/MenuMapper.java @@ -33,4 +33,8 @@ public interface MenuMapper extends BaseMapperX { return selectOne(MenuDO::getComponentName, componentName); } + default MenuDO selectByComponent(String component) { + return selectOne(MenuDO::getComponent, component); + } + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/RoleMenuDataRuleMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/RoleMenuDataRuleMapper.java new file mode 100644 index 00000000..25d7e101 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/permission/RoleMenuDataRuleMapper.java @@ -0,0 +1,43 @@ +package com.zt.plat.module.system.dal.mysql.permission; + +import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; +import com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * 角色菜单数据规则关联 Mapper + * + * @author ZT + */ +@Mapper +public interface RoleMenuDataRuleMapper extends BaseMapperX { + + /** + * 根据角色ID和菜单ID查询规则关联 + * + * @param roleId 角色ID + * @param menuId 菜单ID + * @return 规则关联列表 + */ + default List selectListByRoleAndMenu(Long roleId, Long menuId) { + return selectList(new LambdaQueryWrapper() + .eq(RoleMenuDataRuleDO::getRoleId, roleId) + .eq(RoleMenuDataRuleDO::getMenuId, menuId)); + } + + /** + * 根据角色ID和菜单ID删除规则关联 + * + * @param roleId 角色ID + * @param menuId 菜单ID + */ + default void deleteByRoleAndMenu(Long roleId, Long menuId) { + delete(new LambdaQueryWrapper() + .eq(RoleMenuDataRuleDO::getRoleId, roleId) + .eq(RoleMenuDataRuleDO::getMenuId, menuId)); + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/push/ExternalPushConfigMapper.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/push/ExternalPushConfigMapper.java new file mode 100644 index 00000000..f05cdaf6 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/dal/mysql/push/ExternalPushConfigMapper.java @@ -0,0 +1,92 @@ +package com.zt.plat.module.system.dal.mysql.push; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; +import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigPageReqVO; +import com.zt.plat.module.system.dal.dataobject.push.ExternalPushConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 外部系统推送配置 Mapper + * + * @author ZT Cloud + */ +@Mapper +public interface ExternalPushConfigMapper extends BaseMapperX { + + default PageResult selectPage(ExternalPushConfigPageReqVO reqVO) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .eqIfPresent(ExternalPushConfigDO::getCompanyId, reqVO.getCompanyId()) + .eqIfPresent(ExternalPushConfigDO::getBusinessType, reqVO.getBusinessType()) + .eqIfPresent(ExternalPushConfigDO::getExternalSystem, reqVO.getExternalSystem()) + .eqIfPresent(ExternalPushConfigDO::getEnablePush, reqVO.getEnablePush()); + + // 如果传了 companyId 但没传 deptId,则只查公司级配置(dept_id IS NULL) + if (reqVO.getCompanyId() != null && reqVO.getDeptId() == null) { + wrapper.isNull(ExternalPushConfigDO::getDeptId); + } else if (reqVO.getDeptId() != null) { + // 如果传了 deptId,则查指定部门的配置 + wrapper.eq(ExternalPushConfigDO::getDeptId, reqVO.getDeptId()); + } + // 如果都没传,则查所有配置 + + wrapper.orderByDesc(ExternalPushConfigDO::getId); + return selectPage(reqVO, wrapper); + } + + /** + * 通用查询配置方法 + * + * @param companyId 公司ID(null 表示查询 company_id IS NULL 的记录) + * @param deptId 部门ID(null 表示查询 dept_id IS NULL 的记录) + * @param businessType 业务类型(null 表示查询 business_type IS NULL 的记录) + * @param externalSystem 外部系统(null 表示查询 external_system IS NULL 的记录) + * @return 配置对象 + */ + default ExternalPushConfigDO selectByConfig(Long companyId, Long deptId, String businessType, String externalSystem) { + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX<>(); + + if (companyId == null) { + wrapper.isNull(ExternalPushConfigDO::getCompanyId); + } else { + wrapper.eq(ExternalPushConfigDO::getCompanyId, companyId); + } + + if (deptId == null) { + wrapper.isNull(ExternalPushConfigDO::getDeptId); + } else { + wrapper.eq(ExternalPushConfigDO::getDeptId, deptId); + } + + if (businessType == null) { + wrapper.isNull(ExternalPushConfigDO::getBusinessType); + } else { + wrapper.eq(ExternalPushConfigDO::getBusinessType, businessType); + } + + if (externalSystem == null) { + wrapper.isNull(ExternalPushConfigDO::getExternalSystem); + } else { + wrapper.eq(ExternalPushConfigDO::getExternalSystem, externalSystem); + } + + return selectOne(wrapper); + } + + /** + * 查询公司下所有配置 + */ + default List selectListByCompanyId(Long companyId) { + return selectList(ExternalPushConfigDO::getCompanyId, companyId); + } + + /** + * 查询部门下所有配置 + */ + default List selectListByDeptId(Long deptId) { + return selectList(ExternalPushConfigDO::getDeptId, deptId); + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkProperties.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkProperties.java index dde38deb..c7a4e0ee 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkProperties.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/integration/iwork/config/IWorkProperties.java @@ -42,6 +42,7 @@ public class IWorkProperties { private final Client client = new Client(); private final OrgRest org = new OrgRest(); private final Workflow workflow = new Workflow(); + private final Callback callback = new Callback(); private final Oa oa = new Oa(); @Data @@ -127,6 +128,26 @@ public class IWorkProperties { private String sealWorkflowId; } + @Data + public static class Callback { + /** + * 业务回调重试配置。 + */ + private final Retry retry = new Retry(); + } + + @Data + public static class Retry { + /** + * 最大重试次数,默认 3。 + */ + private int maxAttempts = 3; + /** + * 重试延迟(秒),默认 5。 + */ + private int delaySeconds = 5; + } + @Data public static class Oa { /** diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/job/sync/SyncIWorkOrgChangeJob.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/job/sync/SyncIWorkOrgChangeJob.java new file mode 100644 index 00000000..0e2c23e2 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/job/sync/SyncIWorkOrgChangeJob.java @@ -0,0 +1,40 @@ +package com.zt.plat.module.system.job.sync; + +import com.xxl.job.core.handler.annotation.XxlJob; +import com.zt.plat.framework.tenant.core.job.TenantJob; +import com.zt.plat.module.system.service.sync.SyncIWorkOrgChangeService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 用于定时同步 iWork 当日变更的组织数据 + * 同步时间:每日23:00 + */ +@Component +@Slf4j +public class SyncIWorkOrgChangeJob { + + @Resource + private SyncIWorkOrgChangeService syncIWorkOrgChangeService; + + /** + * 执行定时任务 + * 配置执行频率:每日23:00时执行一次 + * cron表达式:0 0 23 * * ? + */ + @XxlJob("syncIWorkOrgChangeJob") + @TenantJob + public void execute() { + log.info("[syncIWorkOrgChangeJob][开始执行同步 iWork 当日变更组织任务]"); + try { + int processedCount = syncIWorkOrgChangeService.process(); + if (processedCount > 0) { + log.info("[syncIWorkOrgChangeJob][同步任务执行完成,处理了 {} 条记录]", processedCount); + } + } catch (Exception e) { + log.error("[syncIWorkOrgChangeJob][同步任务执行失败]", e); + throw e; + } + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/job/sync/SyncIWorkUserChangeJob.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/job/sync/SyncIWorkUserChangeJob.java new file mode 100644 index 00000000..602c8402 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/job/sync/SyncIWorkUserChangeJob.java @@ -0,0 +1,39 @@ +package com.zt.plat.module.system.job.sync; + +import com.xxl.job.core.handler.annotation.XxlJob; +import com.zt.plat.framework.tenant.core.job.TenantJob; +import com.zt.plat.module.system.service.sync.SyncIWorkUserChangeService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 用于定时同步iWork当日修改的用户数据 + * 同步时间:每日23:00 + */ +@Component +@Slf4j +public class SyncIWorkUserChangeJob { + + @Resource + private SyncIWorkUserChangeService syncIWorkUserChangeService; + + /** + * 执行批量重跑任务 + * 配置执行频率:每日23:00时执行一次 + * cron表达式:0 0 23 * * ? + */ + @XxlJob("syncIWorkUserChangeJob") + @TenantJob + public void execute() { + log.info("[syncLogBatchRerunJob][开始执行同步iWork当日变更用户任务]"); + try{ + int processedCount = syncIWorkUserChangeService.process(); + if (processedCount > 0) { + log.info("[syncLogBatchRerunJob][同步任务执行完成,处理了 {} 条记录]", processedCount); + } + } catch (Exception e) { + log.error("[syncLogBatchRerunJob][同步iWork当日变更用户任务执行异常]", e); + } + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackListener.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackListener.java new file mode 100644 index 00000000..c92bfe45 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackListener.java @@ -0,0 +1,66 @@ +package com.zt.plat.module.system.mq.iwork; + +import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties; +import com.zt.plat.module.system.service.integration.iwork.IWorkCallbackLogService; +import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackMessage; +import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackResultMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 监听业务模块回传的处理结果,并在失败时由 system 模块负责重试投递原始回调消息。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +@RocketMQMessageListener(topic = IWorkBizCallbackResultMessage.TOPIC, consumerGroup = IWorkBizCallbackResultMessage.TOPIC + "_CONSUMER") +public class IWorkBizCallbackListener implements RocketMQListener, InitializingBean { + + private final IWorkCallbackLogService logService; + private final IWorkProperties properties; + private final IWorkBizCallbackProducer producer; + private ScheduledExecutorService scheduler; + + @Override + public void afterPropertiesSet() { + scheduler = Executors.newScheduledThreadPool(1, r -> new Thread(r, "iwork-callback-retry")); + } + + @Override + public void onMessage(IWorkBizCallbackResultMessage message) { + String key = message.getBizCallbackKey(); + if (message.isSuccess()) { + logService.markSuccess(message.getRequestId()); + return; + } + + int attempt = message.getAttempt() + 1; + logService.incrementRetry(message.getRequestId()); + int maxAttempts = message.getMaxAttempts() > 0 ? message.getMaxAttempts() : properties.getCallback().getRetry().getMaxAttempts(); + + if (attempt < maxAttempts) { + logService.markFailure(message.getRequestId(), message.getErrorMessage(), true, maxAttempts); + + IWorkBizCallbackMessage next = IWorkBizCallbackMessage.builder() + .requestId(message.getRequestId()) + .bizCallbackKey(key) + .payload(message.getPayload()) + .attempt(attempt) + .maxAttempts(maxAttempts) + .build(); + + int delay = properties.getCallback().getRetry().getDelaySeconds(); + scheduler.schedule(() -> producer.send(next), delay, TimeUnit.SECONDS); + } else { + logService.markFailure(message.getRequestId(), message.getErrorMessage(), false, maxAttempts); + } + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackProducer.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackProducer.java new file mode 100644 index 00000000..1b526f23 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/iwork/IWorkBizCallbackProducer.java @@ -0,0 +1,18 @@ +package com.zt.plat.module.system.mq.iwork; + +import lombok.RequiredArgsConstructor; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class IWorkBizCallbackProducer { + + private final RocketMQTemplate rocketMQTemplate; + + public void send(IWorkBizCallbackMessage message) { + // 使用 tag=bizCallbackKey,方便业务侧按 key 订阅 + String destination = IWorkBizCallbackMessage.TOPIC + ":" + message.getBizCallbackKey(); + rocketMQTemplate.syncSend(destination, message); + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImpl.java index 5038fde4..d7e43355 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/auth/AdminAuthServiceImpl.java @@ -110,6 +110,9 @@ public class AdminAuthServiceImpl implements AdminAuthService { final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; // 校验账号是否存在 AdminUserDO user = userService.getUserByUsername(username); + if (user == null) { + user = userService.getUserByWorkcode(username); + } if (user == null) { createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java index 3996eef8..7f8258e1 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java @@ -185,4 +185,11 @@ public interface DeptService { // ========== 数据同步专用接口 ========== void syncDept(DeptSaveReqVO syncReqVO); + + /** + * 向上查找公司节点信息 + * @param deptId + * @return + */ + List upFindCompanyNode(Long deptId); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java index 0a5c607e..81fc35bf 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java @@ -16,13 +16,12 @@ import com.zt.plat.module.system.dal.dataobject.dept.DeptDO; import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO; import com.zt.plat.module.system.dal.mysql.dept.DeptMapper; import com.zt.plat.module.system.dal.mysql.userdept.UserDeptMapper; -import com.zt.plat.module.system.service.dept.DeptExternalCodeService; import com.zt.plat.module.system.dal.redis.RedisKeyConstants; import com.zt.plat.module.system.enums.dept.DeptSourceEnum; import com.zt.plat.module.system.service.permission.PermissionService; -import org.apache.seata.spring.annotation.GlobalTransactional; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.apache.seata.spring.annotation.GlobalTransactional; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @@ -762,6 +761,7 @@ public class DeptServiceImpl implements DeptService { return deptIds; } + @DataPermission(enable = false) private Long resolveNearestCompanyId(Long deptId, Map deptCache) { DeptDO current = loadDept(deptId, deptCache); while (current != null) { @@ -960,4 +960,9 @@ public class DeptServiceImpl implements DeptService { // 注意:不发布变更事件,避免循环同步 } + @Override + public List upFindCompanyNode(Long deptId) { + return deptMapper.upFindCompanyNode(deptId); + } + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkCallbackLogService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkCallbackLogService.java new file mode 100644 index 00000000..c87ba5de --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/IWorkCallbackLogService.java @@ -0,0 +1,21 @@ +package com.zt.plat.module.system.service.integration.iwork; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkCallbackLogPageReqVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCallbackReqVO; +import com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO; + +public interface IWorkCallbackLogService { + + IWorkSealLogDO upsertOnCallback(IWorkWorkflowCallbackReqVO reqVO, int maxRetry, String rawBody); + + void markSuccess(String requestId); + + void markFailure(String requestId, String error, boolean retrying, int maxRetry); + + void incrementRetry(String requestId); + + PageResult page(IWorkCallbackLogPageReqVO reqVO); + + void resetAndDispatch(String requestId); +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/callback/IWorkBizCallbackHandler.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/callback/IWorkBizCallbackHandler.java new file mode 100644 index 00000000..967d3140 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/callback/IWorkBizCallbackHandler.java @@ -0,0 +1,16 @@ +package com.zt.plat.module.system.service.integration.iwork.callback; + +public interface IWorkBizCallbackHandler { + + /** + * 返回 handler 能处理的 bizCallbackKey。 + */ + String getBizCallbackKey(); + + /** + * 处理业务回调。 + * @param requestId iWork requestId + * @param payload 回调负载 + */ + void handle(String requestId, Object payload) throws Exception; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/enums/IWorkCallbackStatusEnum.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/enums/IWorkCallbackStatusEnum.java new file mode 100644 index 00000000..c08a3e1c --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/enums/IWorkCallbackStatusEnum.java @@ -0,0 +1,20 @@ +package com.zt.plat.module.system.service.integration.iwork.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum IWorkCallbackStatusEnum { + + CREATE_PENDING(0), + CREATE_SUCCESS(1), + CREATE_FAILED(2), + CALLBACK_PENDING(3), + CALLBACK_SUCCESS(4), + CALLBACK_FAILED(5), + CALLBACK_RETRYING(6), + CALLBACK_RETRY_FAILED(7); + + private final int status; +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkCallbackLogServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkCallbackLogServiceImpl.java new file mode 100644 index 00000000..d46dbbe6 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkCallbackLogServiceImpl.java @@ -0,0 +1,130 @@ +package com.zt.plat.module.system.service.integration.iwork.impl; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.framework.common.util.json.JsonUtils; +import com.zt.plat.framework.common.util.object.ObjectUtils; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkCallbackLogPageReqVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCallbackReqVO; +import com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO; +import com.zt.plat.module.system.dal.mysql.iwork.IWorkSealLogMapper; +import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties; +import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackMessage; +import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackProducer; +import com.zt.plat.module.system.service.integration.iwork.IWorkCallbackLogService; +import com.zt.plat.module.system.service.integration.iwork.enums.IWorkCallbackStatusEnum; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class IWorkCallbackLogServiceImpl implements IWorkCallbackLogService { + + private static final int RAW_MAX = 2000; + + private final IWorkSealLogMapper logMapper; + private final IWorkBizCallbackProducer producer; + private final IWorkProperties properties; + + @Override + @Transactional(rollbackFor = Exception.class) + public IWorkSealLogDO upsertOnCallback(IWorkWorkflowCallbackReqVO reqVO, int maxRetry, String rawBody) { + IWorkSealLogDO existing = logMapper.selectByRequestId(reqVO.getRequestId()); + IWorkSealLogDO log = Optional.ofNullable(existing).orElseGet(IWorkSealLogDO::new); + log.setRequestId(reqVO.getRequestId()); + log.setBusinessCode(reqVO.getBusinessCode()); + log.setBizCallbackKey(reqVO.getBizCallbackKey()); + log.setStatus(IWorkCallbackStatusEnum.CALLBACK_PENDING.getStatus()); + log.setRetryCount(ObjectUtils.defaultIfNull(log.getRetryCount(), 0)); + log.setMaxRetry(maxRetry); + log.setRawCallback(truncate(rawBody)); + log.setLastCallbackTime(LocalDateTime.now()); + if (log.getId() == null) { + logMapper.insert(log); + } else { + logMapper.updateById(log); + } + return log; + } + + @Override + public void markSuccess(String requestId) { + IWorkSealLogDO log = new IWorkSealLogDO(); + log.setRequestId(requestId); + log.setStatus(IWorkCallbackStatusEnum.CALLBACK_SUCCESS.getStatus()); + log.setLastErrorMessage(null); + log.setLastCallbackTime(LocalDateTime.now()); + logMapper.update(log, new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(IWorkSealLogDO::getRequestId, requestId)); + } + + @Override + public void markFailure(String requestId, String error, boolean retrying, int maxRetry) { + IWorkSealLogDO log = new IWorkSealLogDO(); + log.setRequestId(requestId); + log.setStatus(retrying ? IWorkCallbackStatusEnum.CALLBACK_RETRYING.getStatus() : IWorkCallbackStatusEnum.CALLBACK_FAILED.getStatus()); + log.setLastErrorMessage(error); + log.setLastCallbackTime(LocalDateTime.now()); + log.setMaxRetry(maxRetry); + logMapper.update(log, new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(IWorkSealLogDO::getRequestId, requestId)); + } + + @Override + public void incrementRetry(String requestId) { + IWorkSealLogDO db = logMapper.selectByRequestId(requestId); + if (db == null) { + return; + } + IWorkSealLogDO log = new IWorkSealLogDO(); + log.setId(db.getId()); + log.setRetryCount(ObjectUtils.defaultIfNull(db.getRetryCount(), 0) + 1); + log.setLastCallbackTime(LocalDateTime.now()); + logMapper.updateById(log); + } + + @Override + public PageResult page(IWorkCallbackLogPageReqVO reqVO) { + return logMapper.selectPage(reqVO); + } + + @Override + public void resetAndDispatch(String requestId) { + IWorkSealLogDO db = logMapper.selectByRequestId(requestId); + if (db == null) { + return; + } + IWorkSealLogDO log = new IWorkSealLogDO(); + log.setId(db.getId()); + log.setRetryCount(0); + log.setStatus(IWorkCallbackStatusEnum.CALLBACK_RETRYING.getStatus()); + log.setLastCallbackTime(LocalDateTime.now()); + logMapper.updateById(log); + + int maxAttempts = properties.getCallback().getRetry().getMaxAttempts(); + Object payload; + try { + payload = JsonUtils.parseObject(db.getRawCallback(), Object.class); + } catch (Exception ex) { + payload = db.getRawCallback(); + } + producer.send(IWorkBizCallbackMessage.builder() + .requestId(db.getRequestId()) + .bizCallbackKey(db.getBizCallbackKey()) + .payload(payload) + .attempt(0) + .maxAttempts(maxAttempts) + .build()); + } + + private String truncate(String raw) { + if (!StringUtils.hasText(raw)) { + return raw; + } + return raw.length() > RAW_MAX ? raw.substring(0, RAW_MAX) : raw; + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java index 445af73b..d06b0adf 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java @@ -182,7 +182,7 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { } AtomicReference attachmentIdRef = new AtomicReference<>(); - TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile, reqVO.getSsoToken()))); + TenantUtils.execute(tenantId, () -> attachmentIdRef.set(saveCallbackAttachment(fileUrl, reqVO.getFileName(), referenceBusinessFile))); return attachmentIdRef.get(); } @@ -251,14 +251,14 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { return executeOaRequest(request); } - private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile, String ssoToken) { + private Long saveCallbackAttachment(String fileUrl, String overrideFileName, BusinessFileRespDTO referenceBusinessFile) { Long businessId = referenceBusinessFile.getBusinessId(); FileCreateReqDTO fileCreateReqDTO = new FileCreateReqDTO(); fileCreateReqDTO.setName(resolveFileName(overrideFileName, fileUrl)); fileCreateReqDTO.setDirectory(null); fileCreateReqDTO.setType(null); - fileCreateReqDTO.setContent(downloadFileBytes(fileUrl, ssoToken)); + fileCreateReqDTO.setContent(downloadFileBytes(fileUrl)); CommonResult fileResult = fileApi.createFileWithReturn(fileCreateReqDTO); if (fileResult == null || !fileResult.isSuccess() || fileResult.getData() == null) { @@ -297,10 +297,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { return businessFile; } - private byte[] downloadFileBytes(String fileUrl, String ssoToken) { - // 如果回调已提供 ssoToken,按需拼接后下载 OA 附件 - String finalUrl = appendSsoTokenIfNeeded(fileUrl, ssoToken); - + private byte[] downloadFileBytes(String fileUrl) { + String finalUrl = fileUrl; OkHttpClient client = okHttpClient(); Request request = new Request.Builder().url(finalUrl).get().build(); try (Response response = client.newCall(request).execute()) { @@ -317,22 +315,6 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { } } - private String appendSsoTokenIfNeeded(String fileUrl, String ssoToken) { - // 未提供 token 或 URL 为空,直接返回原链接 - if (!StringUtils.hasText(ssoToken) || !StringUtils.hasText(fileUrl)) { - return fileUrl; - } - // 已包含 ssoToken(不区分大小写)则不重复添加 - String lower = fileUrl.toLowerCase(); - if (lower.contains("ssotoken=")) { - return fileUrl; - } - // 简单拼接查询参数 - return fileUrl.contains("?") - ? fileUrl + "&ssoToken=" + ssoToken - : fileUrl + "?ssoToken=" + ssoToken; - } - private String resolveFileName(String overrideName, String fileUrl) { if (StringUtils.hasText(overrideName)) { return overrideName; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImpl.java index f31003a1..0d8973aa 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkOrgRestServiceImpl.java @@ -92,6 +92,18 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { if (StringUtils.hasText(reqVO.getSubcompanyName())) { params.put("subcompanyname", reqVO.getSubcompanyName()); } + if (StringUtils.hasText(reqVO.getModified())) { + params.put("modified", reqVO.getModified()); + } + if (StringUtils.hasText(reqVO.getCanceled())) { + params.put("canceled", reqVO.getCanceled()); + } + if (StringUtils.hasText(reqVO.getCustomData())) { + params.put("custom_data", reqVO.getCustomData()); + } + if (StringUtils.hasText(reqVO.getId())) { + params.put("id", reqVO.getId()); + } JsonNode node = invokeParamsEndpoint(path, params); return buildSubcompanyPageResp(node); } @@ -106,8 +118,23 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { if (StringUtils.hasText(reqVO.getDepartmentName())) { params.put("departmentname", reqVO.getDepartmentName()); } - if (StringUtils.hasText(reqVO.getSubcompanyId())) { - params.put("subcompanyid", reqVO.getSubcompanyId()); + if (StringUtils.hasText(reqVO.getSubcompanyId1())) { + params.put("subcompanyid1", reqVO.getSubcompanyId1()); + } + if (StringUtils.hasText(reqVO.getCreated())) { + params.put("created", reqVO.getCreated()); + } + if (StringUtils.hasText(reqVO.getModified())) { + params.put("modified", reqVO.getModified()); + } + if (StringUtils.hasText(reqVO.getCanceled())) { + params.put("canceled", reqVO.getCanceled()); + } + if (StringUtils.hasText(reqVO.getCustomData())) { + params.put("custom_data", reqVO.getCustomData()); + } + if (StringUtils.hasText(reqVO.getId())) { + params.put("id", reqVO.getId()); } JsonNode node = invokeParamsEndpoint(path, params); return buildDepartmentPageResp(node); @@ -117,12 +144,18 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { public IWorkHrJobTitlePageRespVO listJobTitles(IWorkJobTitleQueryReqVO reqVO) { String path = orgPaths().getJobTitlePage(); Map params = buildBaseParams(reqVO); - if (StringUtils.hasText(reqVO.getJobTitleCode())) { - params.put("jobtitlecode", reqVO.getJobTitleCode()); - } if (StringUtils.hasText(reqVO.getJobTitleName())) { params.put("jobtitlename", reqVO.getJobTitleName()); } + if (StringUtils.hasText(reqVO.getCreated())) { + params.put("created", reqVO.getCreated()); + } + if (StringUtils.hasText(reqVO.getModified())) { + params.put("modified", reqVO.getModified()); + } + if (StringUtils.hasText(reqVO.getId())) { + params.put("id", reqVO.getId()); + } JsonNode node = invokeParamsEndpoint(path, params); return buildJobTitlePageResp(node); } @@ -134,11 +167,8 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { if (StringUtils.hasText(reqVO.getWorkCode())) { params.put("workcode", reqVO.getWorkCode()); } - if (StringUtils.hasText(reqVO.getLastName())) { - params.put("lastname", reqVO.getLastName()); - } - if (StringUtils.hasText(reqVO.getSubcompanyId())) { - params.put("subcompanyid", reqVO.getSubcompanyId()); + if (StringUtils.hasText(reqVO.getSubcompanyId1())) { + params.put("subcompanyid1", reqVO.getSubcompanyId1()); } if (StringUtils.hasText(reqVO.getDepartmentId())) { params.put("departmentid", reqVO.getDepartmentId()); @@ -146,14 +176,26 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { if (StringUtils.hasText(reqVO.getJobTitleId())) { params.put("jobtitleid", reqVO.getJobTitleId()); } - if (StringUtils.hasText(reqVO.getStatus())) { - params.put("status", reqVO.getStatus()); + if (StringUtils.hasText(reqVO.getId())) { + params.put("id", reqVO.getId()); } - if (StringUtils.hasText(reqVO.getMobile())) { - params.put("mobile", reqVO.getMobile()); + if (StringUtils.hasText(reqVO.getLoginId())) { + params.put("loginid", reqVO.getLoginId()); } - if (StringUtils.hasText(reqVO.getEmail())) { - params.put("email", reqVO.getEmail()); + if (StringUtils.hasText(reqVO.getCreated())) { + params.put("created", reqVO.getCreated()); + } + if (StringUtils.hasText(reqVO.getModified())) { + params.put("modified", reqVO.getModified()); + } + if (StringUtils.hasText(reqVO.getBaseCustomData())) { + params.put("base_custom_data", reqVO.getBaseCustomData()); + } + if (StringUtils.hasText(reqVO.getPersonCustomData())) { + params.put("person_custom_data", reqVO.getPersonCustomData()); + } + if (StringUtils.hasText(reqVO.getWorkCustomData())) { + params.put("work_custom_data", reqVO.getWorkCustomData()); } JsonNode node = invokeParamsEndpoint(path, params); return buildUserPageResp(node); @@ -161,9 +203,6 @@ public class IWorkOrgRestServiceImpl implements IWorkOrgRestService { private Map buildBaseParams(IWorkOrgBaseQueryReqVO reqVO) { Map params = new HashMap<>(); - if (reqVO.getParams() != null) { - params.putAll(reqVO.getParams()); - } if (reqVO.getCurpage() != null) { params.put("curpage", reqVO.getCurpage()); } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java index 3c92a184..a32be23a 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncProcessorImpl.java @@ -13,8 +13,11 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUs import com.zt.plat.module.system.controller.admin.user.vo.user.UserSaveReqVO; import com.zt.plat.module.system.dal.dataobject.dept.DeptDO; import com.zt.plat.module.system.dal.dataobject.dept.PostDO; +import com.zt.plat.module.system.dal.dataobject.dept.UserPostDO; import com.zt.plat.module.system.dal.dataobject.user.AdminUserDO; +import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO; import com.zt.plat.module.system.dal.mysql.dept.PostMapper; +import com.zt.plat.module.system.dal.mysql.dept.UserPostMapper; import com.zt.plat.module.system.dal.mysql.user.AdminUserMapper; import com.zt.plat.module.system.enums.common.SexEnum; import com.zt.plat.module.system.enums.dept.DeptSourceEnum; @@ -24,6 +27,7 @@ import com.zt.plat.module.system.service.dept.DeptService; import com.zt.plat.module.system.service.dept.PostService; import com.zt.plat.module.system.service.integration.iwork.IWorkSyncProcessor; import com.zt.plat.module.system.service.user.AdminUserService; +import com.zt.plat.module.system.service.userdept.UserDeptService; import com.zt.plat.module.system.util.sync.SyncVerifyUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -33,6 +37,7 @@ import org.springframework.util.DigestUtils; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; @Slf4j @Service @@ -47,8 +52,10 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { private final DeptService deptService; private final PostService postService; private final PostMapper postMapper; + private final UserPostMapper userPostMapper; private final AdminUserService adminUserService; private final AdminUserMapper adminUserMapper; + private final UserDeptService userDeptService; private final Map postCache = new ConcurrentHashMap<>(); @@ -322,7 +329,41 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { if (records.isEmpty()) { return result; } + long batchStart = System.currentTimeMillis(); result.increasePulled(records.size()); + // 预取已有用户,避免逐条查询 + long preloadUsersStart = System.currentTimeMillis(); + Map existingUsers = new HashMap<>(); + List userIds = records.stream() + .map(IWorkHrUserPageRespVO.User::getId) + .filter(Objects::nonNull) + .map(Integer::longValue) + .distinct() + .toList(); + if (!userIds.isEmpty()) { + List users = adminUserMapper.selectBatchIds(userIds); + if (CollUtil.isNotEmpty(users)) { + users.forEach(user -> existingUsers.put(user.getId(), user)); + } + } + long preloadUsersMs = System.currentTimeMillis() - preloadUsersStart; + + // 预取岗位,避免逐条按编码查询 + long preloadPostsStart = System.currentTimeMillis(); + List postCodes = records.stream() + .map(IWorkHrUserPageRespVO.User::getJobtitleid) + .filter(Objects::nonNull) + .map(this::buildJobCode) + .distinct() + .toList(); + if (!postCodes.isEmpty()) { + List posts = postMapper.selectByCodes(postCodes); + if (CollUtil.isNotEmpty(posts)) { + posts.forEach(post -> postCache.put(buildPostCacheKey(post.getCode()), post)); + } + } + long preloadPostsMs = System.currentTimeMillis() - preloadPostsStart; + for (IWorkHrUserPageRespVO.User user : records) { if (user == null) { continue; @@ -344,7 +385,7 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { CommonStatusEnum status = inactive ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE; // 直接沿用 iWork 原始密码,避免重复格式化造成校验偏差 String externalPassword = trimToNull(user.getPassword()); - AdminUserDO existing = adminUserMapper.selectById(user.getId()); + AdminUserDO existing = user.getId() == null ? null : existingUsers.get(user.getId().longValue()); UserSyncOutcome outcome; if (existing == null) { if (!options.isCreateIfMissing()) { @@ -377,6 +418,9 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { result.withMessage("同步人员失败: " + ex.getMessage()); } } + long totalMs = System.currentTimeMillis() - batchStart; + log.info("[iWork] 人员批次同步完成 size={} preloadUsersMs={} preloadPostsMs={} totalMs={}", + records.size(), preloadUsersMs, preloadPostsMs, totalMs); return result; } //TODO @@ -494,12 +538,49 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { String externalPassword) { UserSaveReqVO req = buildUserSaveReq(source, username, deptId, postId, status); req.setId(existing.getId()); + Long iworkDeptId = resolveIWorkDeptId(deptId); + req.setDeptIds(null); + req.setPostIds(null); boolean disabledChanged = CommonStatusEnum.isDisable(status.getStatus()) && CommonStatusEnum.isEnable(existing.getStatus()); - adminUserService.updateUser(req); - syncPassword(existing, externalPassword); + boolean infoChanged = isUserInfoChanged(existing, req); + boolean passwordChanged = isPasswordChanged(existing, externalPassword); + boolean deptChanged = syncIWorkUserDeptRelations(existing.getId(), iworkDeptId); + boolean postChanged = syncIWorkUserPostRelations(existing.getId(), postId); + + if (!infoChanged && !passwordChanged && !deptChanged && !postChanged) { + return new UserSyncOutcome(SyncAction.SKIPPED, false, existing.getId()); + } + + if (infoChanged) { + adminUserService.updateUser(req); + } + if (passwordChanged) { + syncPassword(existing, externalPassword); + } return new UserSyncOutcome(SyncAction.UPDATED, disabledChanged, existing.getId()); } + private boolean isUserInfoChanged(AdminUserDO existing, UserSaveReqVO req) { + if (existing == null || req == null) { + return false; + } + return !Objects.equals(existing.getUsername(), req.getUsername()) + || !Objects.equals(existing.getWorkcode(), req.getWorkcode()) + || !Objects.equals(existing.getNickname(), req.getNickname()) + || !Objects.equals(existing.getRemark(), req.getRemark()) + || !Objects.equals(existing.getEmail(), req.getEmail()) + || !Objects.equals(existing.getMobile(), req.getMobile()) + || !Objects.equals(existing.getSex(), req.getSex()) + || !Objects.equals(existing.getStatus(), req.getStatus()); + } + + private boolean isPasswordChanged(AdminUserDO existing, String externalPassword) { + if (existing == null || StrUtil.isBlank(externalPassword)) { + return false; + } + return !StrUtil.equals(externalPassword, existing.getPassword()); + } + private DeptSaveReqVO buildSubcompanySaveReq(IWorkHrSubcompanyPageRespVO.Subcompany data, Long deptId, Long parentId, @@ -550,8 +631,9 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { req.setWorkcode(resolveWorkcode(source)); req.setNickname(limitLength(StrUtil.blankToDefault(source.getLastname(), username), 30)); req.setRemark(buildUserRemark(source)); - if (deptId != null) { - req.setDeptIds(singletonSet(deptId)); + Long iworkDeptId = resolveIWorkDeptId(deptId); + if (iworkDeptId != null) { + req.setDeptIds(singletonSet(iworkDeptId)); } if (postId != null) { req.setPostIds(singletonSet(postId)); @@ -566,6 +648,74 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { return req; } + private boolean syncIWorkUserDeptRelations(Long userId, Long iworkDeptId) { + if (userId == null) { + return false; + } + List relations = userDeptService.getValidUserDeptListByUserIds(Collections.singleton(userId)); + Set existingDeptIds = relations.stream() + .map(UserDeptDO::getDeptId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Set existingIworkDeptIds = new LinkedHashSet<>(); + if (CollUtil.isNotEmpty(existingDeptIds)) { + Map deptMap = deptService.getDeptMap(existingDeptIds); + existingIworkDeptIds = deptMap.values().stream() + .filter(this::isIWorkDept) + .map(DeptDO::getId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + Set desiredIworkDeptIds = iworkDeptId == null + ? Collections.emptySet() + : Collections.singleton(iworkDeptId); + if (existingIworkDeptIds.equals(desiredIworkDeptIds)) { + return false; + } + Collection toDelete = CollUtil.subtract(existingIworkDeptIds, desiredIworkDeptIds); + Collection toAdd = CollUtil.subtract(desiredIworkDeptIds, existingIworkDeptIds); + if (CollUtil.isNotEmpty(toDelete)) { + userDeptService.deleteUserDeptByUserIdAndDeptIds(userId, toDelete); + } + if (CollUtil.isNotEmpty(toAdd)) { + userDeptService.batchCreateUserDept(Collections.singletonList( + new UserDeptDO().setUserId(userId).setDeptId(iworkDeptId))); + } + return true; + } + + private boolean syncIWorkUserPostRelations(Long userId, Long postId) { + if (userId == null || postId == null) { + return false; + } + List relations = userPostMapper.selectListByUserId(userId); + Set existingPostIds = relations.stream() + .map(UserPostDO::getPostId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (existingPostIds.contains(postId)) { + return false; + } + userPostMapper.insertBatch(Collections.singletonList( + new UserPostDO().setUserId(userId).setPostId(postId))); + return true; + } + + private Long resolveIWorkDeptId(Long deptId) { + if (deptId == null) { + return null; + } + DeptDO dept = deptService.getDept(deptId); + return isIWorkDept(dept) ? deptId : null; + } + + private boolean isIWorkDept(DeptDO dept) { + if (dept == null) { + return false; + } + return Objects.equals(dept.getDeptSource(), DeptSourceEnum.IWORK.getSource()); + } + private void mergeDeptDefaults(DeptSaveReqVO target, DeptDO existing) { target.setCode(StrUtil.blankToDefault(target.getCode(), existing.getCode())); target.setShortName(StrUtil.blankToDefault(target.getShortName(), existing.getShortName())); @@ -729,7 +879,12 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor { if (StrUtil.isBlank(statusFlag)) { return false; } - return !"0".equals(statusFlag.trim()); + Integer status = parseInteger(statusFlag); + if (status == null) { + return false; + } + // iWork 状态:0试用、1正式、2临时、3试用延期、4解聘、5离职、6退休、7无效 + return status >= 4; } private Integer resolveSex(String sexFlag) { diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java index 95ac0cdc..dfb327a1 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java @@ -11,14 +11,13 @@ import com.zt.plat.module.system.service.integration.iwork.IWorkSyncProcessor; import com.zt.plat.module.system.service.integration.iwork.IWorkSyncService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.EnumSet; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Set; import static com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationErrorCodeConstants.IWORK_ORG_REMOTE_FAILED; @@ -217,11 +216,14 @@ public class IWorkSyncServiceImpl implements IWorkSyncService { int pagesLimit = reqVO.getMaxPages() == null ? Integer.MAX_VALUE : reqVO.getMaxPages(); int processedPages = 0; for (int page = startPage; processedPages < pagesLimit; page++) { + long pageStart = System.currentTimeMillis(); BatchExecution execution = executor.execute(page, pageSize); + long pageMs = System.currentTimeMillis() - pageStart; if (execution == null || execution.totalPulled == 0) { break; } processedPages++; + log.info("[iWork] 全量同步 {} 页={} pulled={} costMs={}", type, page, execution.totalPulled, pageMs); IWorkSyncBatchStatVO batchStat = new IWorkSyncBatchStatVO(); batchStat.setEntityType(type); batchStat.setPageNumber(page); @@ -258,15 +260,18 @@ public class IWorkSyncServiceImpl implements IWorkSyncService { if (query == null || reqVO == null) { return; } + copyQueryParameters(reqVO, query); // 设置查询条件 if (StrUtil.isBlank(reqVO.getId())) { return; } - Map params = query.getParams(); - if (params == null) { - params = new HashMap<>(); - query.setParams(params); + applyQueryId(query, reqVO.getId()); + } + + private void copyQueryParameters(IWorkFullSyncReqVO reqVO, IWorkOrgBaseQueryReqVO query) { + BeanUtils.copyProperties(reqVO, query); + if (query instanceof IWorkUserQueryReqVO userQuery) { + userQuery.setDepartmentId(reqVO.getDepartmentCode()); // 设置部门编号 } - params.put("id", reqVO.getId()); } /** @@ -385,12 +390,28 @@ public class IWorkSyncServiceImpl implements IWorkSyncService { if (StrUtil.isBlank(reqVO.getId())) { return; } - Map params = query.getParams(); - if (params == null) { - params = new HashMap<>(); - query.setParams(params); + applyQueryId(query, reqVO.getId()); + } + + private void applyQueryId(IWorkOrgBaseQueryReqVO query, String id) { + if (query == null || StrUtil.isBlank(id)) { + return; + } + if (query instanceof IWorkSubcompanyQueryReqVO subcompanyQuery) { + subcompanyQuery.setId(id); + return; + } + if (query instanceof IWorkDepartmentQueryReqVO departmentQuery) { + departmentQuery.setId(id); + return; + } + if (query instanceof IWorkJobTitleQueryReqVO jobTitleQuery) { + jobTitleQuery.setId(id); + return; + } + if (query instanceof IWorkUserQueryReqVO userQuery) { + userQuery.setId(id); } - params.put("id", reqVO.getId()); } @@ -403,11 +424,14 @@ public class IWorkSyncServiceImpl implements IWorkSyncService { int pagesLimit = reqVO.getMaxPages() == null ? Integer.MAX_VALUE : reqVO.getMaxPages(); int processedPages = 0; for (int page = startPage; processedPages < pagesLimit; page++) { + long pageStart = System.currentTimeMillis(); BatchExecution execution = executor.execute(page, pageSize); + long pageMs = System.currentTimeMillis() - pageStart; if (execution == null || execution.totalPulled == 0) { break; } processedPages++; + log.info("[iWork] 手动同步 {} 页={} pulled={} costMs={}", type, page, execution.totalPulled, pageMs); IWorkSyncBatchStatVO batchStat = new IWorkSyncBatchStatVO(); batchStat.setEntityType(type); batchStat.setPageNumber(page); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/OAuth2TokenServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/OAuth2TokenServiceImpl.java index eee1404e..33552e9f 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/OAuth2TokenServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/oauth2/OAuth2TokenServiceImpl.java @@ -203,6 +203,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { .put(LoginUser.INFO_KEY_TENANT_ID, user.getTenantId().toString()) .put(LoginUser.INFO_KEY_USERNAME, user.getUsername()) .put(LoginUser.INFO_KEY_PHONE, user.getMobile()) + .put(LoginUser.INFO_KEY_WORK_CODE, user.getWorkcode()) .put(LoginUser.INFO_KEY_POST_IDS, CollUtil.isEmpty(user.getPostIds()) ? "[]" : JsonUtils.toJsonString(user.getPostIds())) .build(); } else if (userType.equals(UserTypeEnum.MEMBER.getValue())) { diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuDataRuleService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuDataRuleService.java new file mode 100644 index 00000000..e49d10d8 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/MenuDataRuleService.java @@ -0,0 +1,72 @@ +package com.zt.plat.module.system.service.permission; + +import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleSaveReqVO; +import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO; + +import jakarta.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 菜单数据规则 Service 接口 + * + * @author ZT + */ +public interface MenuDataRuleService { + + /** + * 创建菜单数据规则 + * + * @param createReqVO 创建信息 + * @return 规则ID + */ + Long createMenuDataRule(@Valid MenuDataRuleSaveReqVO createReqVO); + + /** + * 更新菜单数据规则 + * + * @param updateReqVO 更新信息 + */ + void updateMenuDataRule(@Valid MenuDataRuleSaveReqVO updateReqVO); + + /** + * 删除菜单数据规则 + * + * @param id 规则ID + */ + void deleteMenuDataRule(Long id); + + /** + * 获取菜单数据规则 + * + * @param id 规则ID + * @return 规则信息 + */ + MenuDataRuleDO getMenuDataRule(Long id); + + /** + * 获取菜单的所有数据规则 + * + * @param menuId 菜单ID + * @return 规则列表 + */ + List getMenuDataRuleListByMenuId(Long menuId); + + /** + * 获取用户在指定菜单下的有效数据规则 + * + * @param userId 用户ID + * @param menuId 菜单ID + * @return 规则列表 + */ + List getUserMenuDataRules(Long userId, Long menuId); + + /** + * 批量获取菜单的数据规则(带缓存) + * + * @param menuIds 菜单ID列表 + * @return 菜单ID -> 规则列表的映射 + */ + Map> getMenuDataRuleMap(Collection menuIds); +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PageComponentMappingService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PageComponentMappingService.java new file mode 100644 index 00000000..f62f9bc1 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PageComponentMappingService.java @@ -0,0 +1,56 @@ +package com.zt.plat.module.system.service.permission; + +import cn.hutool.core.util.StrUtil; +import com.zt.plat.module.system.dal.dataobject.permission.MenuDO; +import com.zt.plat.module.system.dal.mysql.permission.MenuMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 页面组件映射服务 + * 根据pageComponent查询对应的菜单ID + * + * @author ZT + */ +@Service +@Slf4j +public class PageComponentMappingService { + + @Resource + private MenuMapper menuMapper; + + /** + * 根据页面组件路径获取菜单ID + * + * @param pageComponent 页面组件路径,如:system/role/index + * @return 菜单ID,如果未找到返回null + */ + public Long getMenuIdByPageComponent(String pageComponent) { + if (StrUtil.isBlank(pageComponent)) { + return null; + } + + log.debug("[getMenuIdByPageComponent][查询pageComponent: {}]", pageComponent); + + // 使用精确匹配查询菜单 + MenuDO menu = menuMapper.selectByComponent(pageComponent); + + if (menu != null) { + log.debug("[getMenuIdByPageComponent][找到匹配菜单: ID={}, Name={}, Component={}]", + menu.getId(), menu.getName(), menu.getComponent()); + // 兼容达梦数据库,ID可能是Integer类型,需要转换为Long + Object id = menu.getId(); + if (id instanceof Number) { + return ((Number) id).longValue(); + } + return (Long) id; + } + + log.warn("[getMenuIdByPageComponent][未找到匹配的菜单: {}]", pageComponent); + return null; + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionService.java index 7a23c24e..6d570ca7 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionService.java @@ -86,6 +86,23 @@ public interface PermissionService { */ Set getMenuRoleIdListByMenuIdFromCache(Long menuId); + /** + * 批量设置角色-菜单-规则关联 + * + * @param roleId 角色编号 + * @param menuDataRules 菜单和规则的映射关系 + */ + void assignRoleMenuDataRules(Long roleId, Collection menuDataRules); + + /** + * 获取角色在指定菜单下已选择的数据规则ID列表 + * + * @param roleId 角色编号 + * @param menuId 菜单编号 + * @return 数据规则ID列表 + */ + Set getRoleMenuDataRules(Long roleId, Long menuId); + // ========== 用户-角色的相关方法 ========== /** diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java index ce302e9e..96820ed6 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/PermissionServiceImpl.java @@ -76,6 +76,8 @@ public class PermissionServiceImpl implements PermissionService { private RoleMenuMapper roleMenuMapper; @Resource private UserRoleMapper userRoleMapper; + @Resource + private com.zt.plat.module.system.dal.mysql.permission.RoleMenuDataRuleMapper roleMenuDataRuleMapper; private RoleService roleService; @Resource @@ -221,6 +223,45 @@ public class PermissionServiceImpl implements PermissionService { } } + @Override + @Transactional(rollbackFor = Exception.class) + public void assignRoleMenuDataRules(Long roleId, Collection menuDataRules) { + if (CollUtil.isEmpty(menuDataRules)) { + return; + } + + // 遍历每个菜单,更新其数据规则关联 + for (PermissionAssignRoleMenuItemReqVO menuDataRule : menuDataRules) { + Long menuId = menuDataRule.getId(); + List dataRuleIds = menuDataRule.getDataRuleIds(); + + // 删除该角色在该菜单下的旧规则关联 + roleMenuDataRuleMapper.deleteByRoleAndMenu(roleId, menuId); + + // 如果有新规则,则插入 + if (CollUtil.isNotEmpty(dataRuleIds)) { + List entities = + dataRuleIds.stream().map(ruleId -> { + com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO entity = + new com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO(); + entity.setRoleId(roleId); + entity.setMenuId(menuId); + entity.setDataRuleId(ruleId); + return entity; + }).collect(Collectors.toList()); + roleMenuDataRuleMapper.insertBatch(entities); + } + } + } + + @Override + public Set getRoleMenuDataRules(Long roleId, Long menuId) { + List list = + roleMenuDataRuleMapper.selectListByRoleAndMenu(roleId, menuId); + return CollectionUtils.convertSet(list, + com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDataRuleDO::getDataRuleId); + } + @Override @Transactional(rollbackFor = Exception.class) @Caching(evict = { diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/impl/MenuDataRuleServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/impl/MenuDataRuleServiceImpl.java new file mode 100644 index 00000000..da8cbef8 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/permission/impl/MenuDataRuleServiceImpl.java @@ -0,0 +1,109 @@ +package com.zt.plat.module.system.service.permission.impl; + +import cn.hutool.core.collection.CollUtil; +import com.zt.plat.module.system.controller.admin.permission.vo.menudatarule.MenuDataRuleSaveReqVO; +import com.zt.plat.module.system.convert.permission.MenuDataRuleConvert; +import com.zt.plat.module.system.dal.dataobject.permission.MenuDataRuleDO; +import com.zt.plat.module.system.dal.mysql.permission.MenuDataRuleMapper; +import com.zt.plat.module.system.service.permission.MenuDataRuleService; +import com.zt.plat.module.system.service.permission.PermissionService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import jakarta.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.zt.plat.module.system.enums.ErrorCodeConstants.*; + +/** + * 菜单数据规则 Service 实现类 + * + * @author ZT + */ +@Service +@Validated +@Slf4j +public class MenuDataRuleServiceImpl implements MenuDataRuleService { + + @Resource + private MenuDataRuleMapper menuDataRuleMapper; + + @Resource + private PermissionService permissionService; + + @Override + @CacheEvict(value = "menuDataRule", key = "#createReqVO.menuId") + public Long createMenuDataRule(MenuDataRuleSaveReqVO createReqVO) { + MenuDataRuleDO rule = MenuDataRuleConvert.INSTANCE.convert(createReqVO); + menuDataRuleMapper.insert(rule); + return rule.getId(); + } + + @Override + @CacheEvict(value = "menuDataRule", key = "#updateReqVO.menuId") + public void updateMenuDataRule(MenuDataRuleSaveReqVO updateReqVO) { + validateMenuDataRuleExists(updateReqVO.getId()); + MenuDataRuleDO updateObj = MenuDataRuleConvert.INSTANCE.convert(updateReqVO); + menuDataRuleMapper.updateById(updateObj); + } + + @Override + public void deleteMenuDataRule(Long id) { + MenuDataRuleDO rule = validateMenuDataRuleExists(id); + menuDataRuleMapper.deleteById(id); + } + + @Override + public MenuDataRuleDO getMenuDataRule(Long id) { + return menuDataRuleMapper.selectById(id); + } + + @Override + @Cacheable(value = "menuDataRule", key = "#menuId") + public List getMenuDataRuleListByMenuId(Long menuId) { + return menuDataRuleMapper.selectListByMenuId(menuId); + } + + @Override + public List getUserMenuDataRules(Long userId, Long menuId) { + Set roleIds = permissionService.getUserRoleIdListByUserId(userId); + if (CollUtil.isEmpty(roleIds)) { + return Collections.emptyList(); + } + + List allRules = getMenuDataRuleListByMenuId(menuId); + if (CollUtil.isEmpty(allRules)) { + return Collections.emptyList(); + } + + List ruleIds = menuDataRuleMapper.selectRuleIdsByRoleAndMenu(roleIds, menuId); + + // 如果角色没有关联任何规则,返回空列表(不应用任何过滤) + if (CollUtil.isEmpty(ruleIds)) { + return Collections.emptyList(); + } + + return allRules.stream() + .filter(rule -> ruleIds.contains(rule.getId()) && rule.getStatus() == 1) + .collect(Collectors.toList()); + } + + @Override + public Map> getMenuDataRuleMap(Collection menuIds) { + List rules = menuDataRuleMapper.selectListByMenuIds(menuIds); + return rules.stream().collect(Collectors.groupingBy(MenuDataRuleDO::getMenuId)); + } + + private MenuDataRuleDO validateMenuDataRuleExists(Long id) { + MenuDataRuleDO rule = menuDataRuleMapper.selectById(id); + if (rule == null) { + throw exception(MENU_NOT_EXISTS); + } + return rule; + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalService.java index 192b1851..acc8724b 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalService.java @@ -60,4 +60,11 @@ public interface PortalService { */ List getPortalListByUserId(Long userId); + /** + * 获得公开门户列表(无需登录) + * + * @return 门户列表 + */ + List getPublicPortalList(); + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalServiceImpl.java index 1c30986a..902b5a21 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/portal/PortalServiceImpl.java @@ -126,6 +126,11 @@ public class PortalServiceImpl implements PortalService { return portalMapper.selectListByPermissions(permissions); } + @Override + public List getPublicPortalList() { + return portalMapper.selectListByPermissions(Collections.emptyList()); + } + @VisibleForTesting public PortalDO validatePortalExists(Long id) { if (id == null) { diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/push/ExternalPushConfigService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/push/ExternalPushConfigService.java new file mode 100644 index 00000000..904a474e --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/push/ExternalPushConfigService.java @@ -0,0 +1,53 @@ +package com.zt.plat.module.system.service.push; + +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigPageReqVO; +import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigSaveReqVO; +import com.zt.plat.module.system.dal.dataobject.push.ExternalPushConfigDO; +import jakarta.validation.Valid; + +/** + * 外部系统推送配置 Service 接口 + * + * @author ZT Cloud + */ +public interface ExternalPushConfigService { + + /** + * 创建推送配置 + */ + Long createExternalPushConfig(@Valid ExternalPushConfigSaveReqVO createReqVO); + + /** + * 修改推送配置 + */ + void updateExternalPushConfig(@Valid ExternalPushConfigSaveReqVO updateReqVO); + + /** + * 删除推送配置 + */ + void deleteExternalPushConfig(Long id); + + /** + * 获取推送配置详情 + */ + ExternalPushConfigDO getExternalPushConfig(Long id); + + /** + * 分页查询推送配置 + */ + PageResult getExternalPushConfigPage(ExternalPushConfigPageReqVO reqVO); + + /** + * 判断是否允许推送(核心业务逻辑) + * + * 优先级:部门配置 > 公司配置 > 默认允许 + * + * @param companyId 公司编号(必填) + * @param deptId 部门编号(可选) + * @param businessType 业务类型(必填) + * @param externalSystem 外部系统标识(必填) + * @return 是否允许推送(true=允许,false=禁止,默认 true) + */ + Boolean isPushEnabled(Long companyId, Long deptId, String businessType, String externalSystem); +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/push/ExternalPushConfigServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/push/ExternalPushConfigServiceImpl.java new file mode 100644 index 00000000..b36aa03c --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/push/ExternalPushConfigServiceImpl.java @@ -0,0 +1,280 @@ +package com.zt.plat.module.system.service.push; + +import cn.hutool.core.util.StrUtil; +import com.zt.plat.framework.common.pojo.PageResult; +import com.zt.plat.framework.common.util.object.BeanUtils; +import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigPageReqVO; +import com.zt.plat.module.system.controller.admin.push.vo.ExternalPushConfigSaveReqVO; +import com.zt.plat.module.system.dal.dataobject.dept.DeptDO; +import com.zt.plat.module.system.dal.dataobject.push.ExternalPushConfigDO; +import com.zt.plat.module.system.dal.mysql.dept.DeptMapper; +import com.zt.plat.module.system.dal.mysql.push.ExternalPushConfigMapper; +import com.zt.plat.module.system.enums.push.BusinessTypeEnum; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.zt.plat.module.system.enums.ErrorCodeConstants.*; + +/** + * 外部系统推送配置 Service 实现类 + * + * @author ZT Cloud + */ +@Service +@Validated +public class ExternalPushConfigServiceImpl implements ExternalPushConfigService { + + @Resource + private ExternalPushConfigMapper externalPushConfigMapper; + @Resource + private DeptMapper deptMapper; + + @Override + public Long createExternalPushConfig(ExternalPushConfigSaveReqVO createReqVO) { + // 参数规范化 + normalizeRequest(createReqVO); + + // 业务校验 + validateForCreateOrUpdate(null, createReqVO); + + // 创建配置 + ExternalPushConfigDO entity = BeanUtils.toBean(createReqVO, ExternalPushConfigDO.class); + if (entity.getEnablePush() == null) { + entity.setEnablePush(true); + } + externalPushConfigMapper.insert(entity); + return entity.getId(); + } + + @Override + public void updateExternalPushConfig(ExternalPushConfigSaveReqVO updateReqVO) { + // 参数规范化 + normalizeRequest(updateReqVO); + + // 校验存在 + validateExists(updateReqVO.getId()); + // 业务校验 + validateForCreateOrUpdate(updateReqVO.getId(), updateReqVO); + + // 更新配置 + ExternalPushConfigDO updateObj = BeanUtils.toBean(updateReqVO, ExternalPushConfigDO.class); + externalPushConfigMapper.updateById(updateObj); + } + + @Override + public void deleteExternalPushConfig(Long id) { + validateExists(id); + externalPushConfigMapper.deleteById(id); + } + + @Override + public ExternalPushConfigDO getExternalPushConfig(Long id) { + return externalPushConfigMapper.selectById(id); + } + + @Override + public PageResult getExternalPushConfigPage(ExternalPushConfigPageReqVO reqVO) { + return externalPushConfigMapper.selectPage(reqVO); + } + + @Override + public Boolean isPushEnabled(Long companyId, Long deptId, String businessType, String externalSystem) { + // 规范化参数 + String normalizedBusinessType = StrUtil.isNotBlank(businessType) ? businessType.trim().toUpperCase() : null; + String normalizedExternalSystem = StrUtil.isNotBlank(externalSystem) ? externalSystem.trim().toUpperCase() : null; + + // 优先级 1:部门级配置(如果 deptId 不为空),递归查找父部门 + if (deptId != null) { + ExternalPushConfigDO deptConfig = findDeptConfigRecursive( + companyId, deptId, normalizedBusinessType, normalizedExternalSystem); + if (deptConfig != null) { + return deptConfig.getEnablePush() != null ? deptConfig.getEnablePush() : true; + } + } + + // 优先级 2:公司级配置(dept_id 为 null) + if (companyId != null) { + ExternalPushConfigDO companyConfig = externalPushConfigMapper.selectByConfig( + companyId, null, normalizedBusinessType, normalizedExternalSystem); + if (companyConfig != null) { + return companyConfig.getEnablePush() != null ? companyConfig.getEnablePush() : true; + } + } + + // 优先级 3:全局配置(company_id 和 dept_id 都为空) + ExternalPushConfigDO globalConfig = externalPushConfigMapper.selectByConfig( + null, null, normalizedBusinessType, normalizedExternalSystem); + if (globalConfig != null) { + return globalConfig.getEnablePush() != null ? globalConfig.getEnablePush() : true; + } + + // 优先级 4:没有配置,默认允许推送 + return true; + } + + /** + * 递归查找部门配置(包括父部门) + * + * @param companyId 公司ID + * @param deptId 部门ID + * @param businessType 业务类型 + * @param externalSystem 外部系统 + * @return 配置对象,如果找不到返回 null + */ + private ExternalPushConfigDO findDeptConfigRecursive(Long companyId, Long deptId, + String businessType, String externalSystem) { + if (deptId == null) { + return null; + } + + // 查询当前部门的配置 + ExternalPushConfigDO config = externalPushConfigMapper.selectByConfig( + companyId, deptId, businessType, externalSystem); + if (config != null) { + return config; + } + + // 查询当前部门信息,获取父部门ID + DeptDO dept = deptMapper.selectById(deptId); + if (dept == null || dept.getParentId() == null || dept.getParentId() == 0L) { + // 没有父部门了,返回 null + return null; + } + + // 检查父部门是否是公司节点 + DeptDO parentDept = deptMapper.selectById(dept.getParentId()); + if (parentDept != null && Boolean.TRUE.equals(parentDept.getIsCompany())) { + // 父部门是公司,不再向上查找(公司级配置在外层处理) + return null; + } + + // 递归查找父部门的配置 + return findDeptConfigRecursive(companyId, dept.getParentId(), businessType, externalSystem); + } + + //==================== 私有方法 ==================== + + /** + * 校验配置是否存在 + */ + private ExternalPushConfigDO validateExists(Long id) { + if (id == null) { + throw exception(EXTERNAL_PUSH_CONFIG_NOT_EXISTS); + } + ExternalPushConfigDO entity = externalPushConfigMapper.selectById(id); + if (entity == null) { + throw exception(EXTERNAL_PUSH_CONFIG_NOT_EXISTS); + } + return entity; + } + + /** + * 业务校验 + */ + private void validateForCreateOrUpdate(Long id, ExternalPushConfigSaveReqVO reqVO) { + // 1. 如果指定公司,校验公司存在且是公司节点 + if (reqVO.getCompanyId() != null) { + DeptDO company = deptMapper.selectById(reqVO.getCompanyId()); + if (company == null) { + throw exception(DEPT_NOT_FOUND); + } + if (!Boolean.TRUE.equals(company.getIsCompany())) { + throw exception(EXTERNAL_PUSH_CONFIG_COMPANY_INVALID); + } + } + + // 2. 如果指定部门,校验部门存在且不是公司节点 + if (reqVO.getDeptId() != null) { + DeptDO dept = deptMapper.selectById(reqVO.getDeptId()); + if (dept == null) { + throw exception(DEPT_NOT_FOUND); + } + if (Boolean.TRUE.equals(dept.getIsCompany())) { + throw exception(EXTERNAL_PUSH_CONFIG_DEPT_INVALID); + } + } + + // 3. 如果指定业务类型,校验业务类型有效性 + if (StrUtil.isNotBlank(reqVO.getBusinessType())) { + if (!BusinessTypeEnum.isValidCode(reqVO.getBusinessType())) { + throw exception(EXTERNAL_PUSH_CONFIG_BUSINESS_TYPE_INVALID); + } + } + + // 4. 校验唯一性:同一租户+公司+部门+业务类型+外部系统的配置唯一 + ExternalPushConfigDO existing = externalPushConfigMapper.selectByConfig( + reqVO.getCompanyId(), reqVO.getDeptId(), + reqVO.getBusinessType(), reqVO.getExternalSystem()); + + if (existing != null && (id == null || !existing.getId().equals(id))) { + throw exception(EXTERNAL_PUSH_CONFIG_EXISTS); + } + } + + /** + * 参数规范化 + */ + private void normalizeRequest(ExternalPushConfigSaveReqVO reqVO) { + if (reqVO == null) { + return; + } + + // 如果 companyId 为空但 deptId 不为空,自动查找并填充顶级公司ID + if (reqVO.getCompanyId() == null && reqVO.getDeptId() != null) { + Long topCompanyId = findTopCompanyId(reqVO.getDeptId()); + if (topCompanyId != null) { + reqVO.setCompanyId(topCompanyId); + } + } + + if (StrUtil.isNotBlank(reqVO.getBusinessType())) { + reqVO.setBusinessType(reqVO.getBusinessType().trim().toUpperCase()); + } + if (StrUtil.isNotBlank(reqVO.getExternalSystem())) { + reqVO.setExternalSystem(reqVO.getExternalSystem().trim().toUpperCase()); + } + } + + /** + * 查找部门的顶级公司ID + * + * @param deptId 部门ID + * @return 顶级公司ID,如果不存在返回 null + */ + private Long findTopCompanyId(Long deptId) { + if (deptId == null) { + return null; + } + + DeptDO dept = deptMapper.selectById(deptId); + if (dept == null) { + return null; + } + + // 递归向上查找,直到找到公司节点或者parentId为空/0 + Long currentDeptId = deptId; + while (currentDeptId != null) { + DeptDO currentDept = deptMapper.selectById(currentDeptId); + if (currentDept == null) { + break; + } + + // 如果当前节点是公司,返回其ID + if (Boolean.TRUE.equals(currentDept.getIsCompany())) { + return currentDept.getId(); + } + + // 如果没有父节点或父节点为0,结束查找 + if (currentDept.getParentId() == null || currentDept.getParentId() == 0L) { + break; + } + + // 继续向上查找 + currentDeptId = currentDept.getParentId(); + } + + return null; + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkOrgChangeService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkOrgChangeService.java new file mode 100644 index 00000000..532a3b1d --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkOrgChangeService.java @@ -0,0 +1,13 @@ +package com.zt.plat.module.system.service.sync; + +/** + * 定时同步 iWork 组织变更服务 + */ +public interface SyncIWorkOrgChangeService { + + /** + * 执行同步 + * @return 拉取记录数量 + */ + int process(); +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkOrgChangeServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkOrgChangeServiceImpl.java new file mode 100644 index 00000000..3b81c13f --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkOrgChangeServiceImpl.java @@ -0,0 +1,41 @@ +package com.zt.plat.module.system.service.sync; + +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFullSyncReqVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFullSyncRespVO; +import com.zt.plat.module.system.service.integration.iwork.IWorkSyncService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +@Service +public class SyncIWorkOrgChangeServiceImpl implements SyncIWorkOrgChangeService { + + @Resource + private IWorkSyncService iWorkSyncService; + + @Override + public int process() { + IWorkFullSyncReqVO reqVO = new IWorkFullSyncReqVO(); + reqVO.setPageSize(10); + ZoneId zone = ZoneId.of("Asia/Shanghai"); + String startOfToday = LocalDate.now(zone) + .atStartOfDay(zone) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + reqVO.setModified(startOfToday); + IWorkFullSyncRespVO subcompanyResp = iWorkSyncService.fullSyncSubcompanies(reqVO); + IWorkFullSyncRespVO departmentResp = iWorkSyncService.fullSyncDepartments(reqVO); + return countPulled(subcompanyResp) + countPulled(departmentResp); + } + + private int countPulled(IWorkFullSyncRespVO respVO) { + if (respVO == null || respVO.getBatches() == null) { + return 0; + } + return respVO.getBatches().stream() + .mapToInt(batch -> batch.getPulled() == null ? 0 : batch.getPulled()) + .sum(); + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkUserChangeService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkUserChangeService.java new file mode 100644 index 00000000..dc3f3418 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkUserChangeService.java @@ -0,0 +1,14 @@ +package com.zt.plat.module.system.service.sync; + +/** + * 同步iWork当日修改的用户数据 + */ +public interface SyncIWorkUserChangeService { + + /** + * 同步入口 + * @return + */ + int process(); + +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkUserChangeServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkUserChangeServiceImpl.java new file mode 100644 index 00000000..7592823c --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sync/SyncIWorkUserChangeServiceImpl.java @@ -0,0 +1,37 @@ +package com.zt.plat.module.system.service.sync; + +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFullSyncReqVO; +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFullSyncRespVO; +import com.zt.plat.module.system.service.integration.iwork.IWorkSyncService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +@Service +public class SyncIWorkUserChangeServiceImpl implements SyncIWorkUserChangeService { + + @Resource + private IWorkSyncService iWorkSyncService; + + @Override + public int process() { + IWorkFullSyncReqVO reqVO = new IWorkFullSyncReqVO(); + reqVO.setPageSize(10); + // 设置修改日期的查询条件为当日0时起 + ZoneId zone = ZoneId.of("Asia/Shanghai"); + String startOfToday = LocalDate.now(zone) + .atStartOfDay(zone) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + reqVO.setModified(startOfToday); + IWorkFullSyncRespVO respVO = iWorkSyncService.fullSyncUsers(reqVO); + if(respVO!=null && respVO.getBatches()!=null) { + return respVO.getBatches().stream() + .mapToInt(b -> b.getPulled() == null ? 0 : b.getPulled()) + .sum(); + } + return 0; + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptService.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptService.java index 26c728b7..7a4e027c 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptService.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptService.java @@ -70,6 +70,13 @@ public interface UserDeptService { */ void deleteUserDeptByUserId(Long userId); + /** + * 根据用户ID与部门ID集合删除用户与部门关系 + * @param userId 用户ID + * @param deptIds 部门ID集合 + */ + void deleteUserDeptByUserIdAndDeptIds(Long userId, Collection deptIds); + /** * 批量创建用户与部门关系 * @param createReqVOList 创建信息列表 diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptServiceImpl.java index 8c71a020..bbc78645 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/userdept/UserDeptServiceImpl.java @@ -3,6 +3,7 @@ package com.zt.plat.module.system.service.userdept; import cn.hutool.core.collection.CollUtil; import com.zt.plat.framework.common.util.object.BeanUtils; import com.zt.plat.framework.security.core.LoginUser; +import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX; import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO; import com.zt.plat.module.system.dal.mysql.userdept.UserDeptMapper; import jakarta.annotation.Resource; @@ -128,10 +129,20 @@ public class UserDeptServiceImpl implements UserDeptService { @Override public void deleteUserDeptByUserId(Long userId) { if (userId == null) return; - userDeptMapper.delete(new com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX() + userDeptMapper.delete(new LambdaQueryWrapperX() .eq(UserDeptDO::getUserId, userId)); } + @Override + public void deleteUserDeptByUserIdAndDeptIds(Long userId, Collection deptIds) { + if (userId == null || CollUtil.isEmpty(deptIds)) { + return; + } + userDeptMapper.delete(new LambdaQueryWrapperX() + .eq(UserDeptDO::getUserId, userId) + .in(UserDeptDO::getDeptId, deptIds)); + } + @Override @Transactional(rollbackFor = Exception.class) public void batchCreateUserDept(List createReqVOList) { diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkCallbackLogServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkCallbackLogServiceImplTest.java new file mode 100644 index 00000000..f0287595 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkCallbackLogServiceImplTest.java @@ -0,0 +1,63 @@ +package com.zt.plat.module.system.service.integration.iwork.impl; + +import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCallbackReqVO; +import com.zt.plat.module.system.dal.dataobject.iwork.IWorkSealLogDO; +import com.zt.plat.module.system.dal.mysql.iwork.IWorkSealLogMapper; +import com.zt.plat.module.system.framework.integration.iwork.config.IWorkProperties; +import com.zt.plat.module.system.mq.iwork.IWorkBizCallbackProducer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class IWorkCallbackLogServiceImplTest { + + private IWorkSealLogMapper mapper; + private IWorkBizCallbackProducer producer; + private IWorkProperties properties; + private IWorkCallbackLogServiceImpl service; + + @BeforeEach + void setup() { + mapper = mock(IWorkSealLogMapper.class); + producer = mock(IWorkBizCallbackProducer.class); + properties = new IWorkProperties(); + service = new IWorkCallbackLogServiceImpl(mapper, producer, properties); + } + + @Test + void upsertOnCallback_shouldTruncateRaw() { + String longRaw = "x".repeat(2100); + IWorkWorkflowCallbackReqVO req = new IWorkWorkflowCallbackReqVO(); + req.setRequestId("REQ-1"); + req.setBizCallbackKey("key"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(IWorkSealLogDO.class); + when(mapper.selectByRequestId("REQ-1")).thenReturn(null); + + service.upsertOnCallback(req, 3, longRaw); + + verify(mapper).insert(captor.capture()); + IWorkSealLogDO saved = captor.getValue(); + assertThat(saved.getRawCallback()).hasSize(2000); + assertThat(saved.getMaxRetry()).isEqualTo(3); + } + + @Test + void incrementRetry_shouldIncreaseCount() { + IWorkSealLogDO existing = new IWorkSealLogDO(); + existing.setId(1L); + existing.setRequestId("REQ-2"); + existing.setRetryCount(1); + when(mapper.selectByRequestId("REQ-2")).thenReturn(existing); + + service.incrementRetry("REQ-2"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(IWorkSealLogDO.class); + verify(mapper).updateById(captor.capture()); + assertThat(captor.getValue().getRetryCount()).isEqualTo(2); + } +} diff --git a/zt-module-template/zt-module-template-api/src/main/java/com/zt/plat/module/template/enums/ErrorCodeConstants.java b/zt-module-template/zt-module-template-api/src/main/java/com/zt/plat/module/template/enums/ErrorCodeConstants.java index ec932af6..b95de638 100644 --- a/zt-module-template/zt-module-template-api/src/main/java/com/zt/plat/module/template/enums/ErrorCodeConstants.java +++ b/zt-module-template/zt-module-template-api/src/main/java/com/zt/plat/module/template/enums/ErrorCodeConstants.java @@ -13,6 +13,10 @@ public interface ErrorCodeConstants { // ========== 合同 补充编号 ========== ErrorCode DEMO_CONTRACT_NOT_EXISTS = new ErrorCode(2_100_000_000, "合同不存在"); + ErrorCode DEMO_CONTRACT_FILE_URL_NOT_EXISTS = new ErrorCode(2_100_000_001, "合同缺少文件地址,无法发起用印"); + ErrorCode DEMO_CONTRACT_IWORK_CREATE_FAILED = new ErrorCode(2_100_000_002, "iWork 用印发起失败:{}"); + ErrorCode DEMO_CONTRACT_IWORK_VOID_FAILED = new ErrorCode(2_100_000_003, "iWork 用印作废失败:{}"); + // ========== 虚拟化表格 1_100_000_001 ========== ErrorCode DEMO_VIRTUALIZED_TABLE_NOT_EXISTS = new ErrorCode(1_100_000_001, "虚拟化表格不存在"); diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/DemoContractController.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/DemoContractController.java index bd80ea2f..9044aa69 100644 --- a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/DemoContractController.java +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/DemoContractController.java @@ -13,15 +13,15 @@ import com.zt.plat.framework.common.util.object.BeanUtils; import com.zt.plat.framework.excel.core.util.ExcelUtils; import com.zt.plat.module.bpm.api.task.BpmProcessInstanceApi; import com.zt.plat.module.bpm.api.task.BpmTaskApi; -import com.zt.plat.module.bpm.api.task.dto.BpmApprovalDetailReqDTO; import com.zt.plat.module.infra.api.businessfile.BusinessFileApi; import com.zt.plat.module.infra.api.file.FileApi; -import com.zt.plat.module.infra.api.file.dto.FileRespDTO; import com.zt.plat.module.system.api.dept.DeptApi; -import com.zt.plat.module.template.controller.admin.contract.vo.DemoContractPageReqVO; -import com.zt.plat.module.template.controller.admin.contract.vo.DemoContractRespVO; -import com.zt.plat.module.template.controller.admin.contract.vo.DemoContractSaveReqVO; +import com.zt.plat.module.system.api.iwork.dto.IWorkOperationRespDTO; +import com.zt.plat.module.system.api.permission.PermissionApi; +import com.zt.plat.module.system.api.user.AdminUserApi; +import com.zt.plat.module.template.controller.admin.contract.vo.*; import com.zt.plat.module.template.dal.dataobject.contract.DemoContractDO; +import com.zt.plat.module.template.service.contract.DemoContractIWorkService; import com.zt.plat.module.template.service.contract.DemoContractService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -67,6 +67,14 @@ public class DemoContractController extends AbstractFileUploadController impleme private BpmProcessInstanceApi bpmInsApi; @Resource private FileApi fileApi; + @Resource + private AdminUserApi adminUserApi; + + @Resource + private PermissionApi permissionApi; + + @Resource + private DemoContractIWorkService demoContractIWorkService; @PostMapping("/create") @Operation(summary = "创建合同") @@ -75,6 +83,13 @@ public class DemoContractController extends AbstractFileUploadController impleme return success(demoContractService.createDemoContract(createReqVO)); } + @PostMapping("/iwork/create") + @Operation(summary = "发起合同 iWork 用印") + @PreAuthorize("@ss.hasPermission('template:demo-contract:create')") + public CommonResult createIWorkWorkflow(@Valid @RequestBody DemoContractIWorkCreateReqVO reqVO) { + return success(demoContractIWorkService.createWorkflow(reqVO)); + } + @PutMapping("/update") @Operation(summary = "更新合同") @PreAuthorize("@ss.hasPermission('template:demo-contract:update')") @@ -92,6 +107,13 @@ public class DemoContractController extends AbstractFileUploadController impleme return success(true); } + @PostMapping("/iwork/void") + @Operation(summary = "作废合同 iWork 用印") + @PreAuthorize("@ss.hasPermission('template:demo-contract:update')") + public CommonResult voidIWorkWorkflow(@Valid @RequestBody DemoContractIWorkVoidReqVO reqVO) { + return success(demoContractIWorkService.voidWorkflow(reqVO)); + } + @DeleteMapping("/delete-list") @Parameter(name = "ids", description = "编号", required = true) @Operation(summary = "批量删除合同") @@ -104,7 +126,6 @@ public class DemoContractController extends AbstractFileUploadController impleme @GetMapping("/get") @Operation(summary = "获得合同") @Parameter(name = "id", description = "编号", required = true, example = "1024") - @PreAuthorize("@ss.hasPermission('template:demo-contract:query')") public CommonResult getDemoContract(@RequestParam("id") Long id) { DemoContractDO demoContract = demoContractService.getDemoContract(id); return success(BeanUtils.toBean(demoContract, DemoContractRespVO.class)); @@ -112,11 +133,7 @@ public class DemoContractController extends AbstractFileUploadController impleme @GetMapping("/page") @Operation(summary = "获得合同分页") - @PreAuthorize("@ss.hasPermission('template:demo-contract:query')") public CommonResult> getDemoContractPage(@Valid DemoContractPageReqVO pageReqVO) { - CommonResult file = fileApi.getFile(1968928810422521857L); - - BpmApprovalDetailReqDTO reqDTO = new BpmApprovalDetailReqDTO(); Long id = IdWorker.getId(); System.out.println("Generated ID: " + id); PageResult pageResult = demoContractService.getDemoContractPage(pageReqVO); diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/vo/DemoContractIWorkCreateReqVO.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/vo/DemoContractIWorkCreateReqVO.java new file mode 100644 index 00000000..1cc3e3ed --- /dev/null +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/vo/DemoContractIWorkCreateReqVO.java @@ -0,0 +1,59 @@ +package com.zt.plat.module.template.controller.admin.contract.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 发起合同 iWork 用印请求 VO + */ +@Data +public class DemoContractIWorkCreateReqVO { + + @Schema(description = "合同 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10086") + @NotNull(message = "合同 ID 不能为空") + private Long contractId; + + @Schema(description = "用印申请人(iWork 人员 ID)", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "用印申请人不能为空") + private String jbr; + + @Schema(description = "用印部门 ID", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "用印部门不能为空") + private String yybm; + + @Schema(description = "用印单位(分部 ID)", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "用印单位不能为空") + private String fb; + + @Schema(description = "申请时间,格式 yyyy-MM-dd", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "申请时间不能为空") + private String sqsj; + + @Schema(description = "用印去向") + private String yyqx; + + @Schema(description = "用印依据附件 URL") + private String yyfkUrl; + + @Schema(description = "用印事由或内容摘要", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "用印事由不能为空") + private String yysy; + + @Schema(description = "用印事项", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "用印事项不能为空") + private String yysx; + + @Schema(description = "业务系统单据编号(用于派生流程标题)") + private String ywxtdjbh; + + @Schema(description = "配置的 iWork 凭证 appId;为空时使用默认凭证") + private String appId; + + @Schema(description = "iWork 操作人用户编号") + private String operatorUserId; + + @Schema(description = "是否强制刷新 token") + private Boolean forceRefreshToken; +} diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/vo/DemoContractIWorkVoidReqVO.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/vo/DemoContractIWorkVoidReqVO.java new file mode 100644 index 00000000..b6b36b3d --- /dev/null +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/controller/admin/contract/vo/DemoContractIWorkVoidReqVO.java @@ -0,0 +1,36 @@ +package com.zt.plat.module.template.controller.admin.contract.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.util.Map; + +/** + * 作废 / 干预 iWork 用印流程请求 VO + */ +@Data +public class DemoContractIWorkVoidReqVO { + + @Schema(description = "流程请求编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "流程请求编号不能为空") + private String requestId; + + @Schema(description = "作废原因") + private String reason; + + @Schema(description = "额外参数") + private Map extraParams; + + @Schema(description = "额外 Form 数据") + private Map formExtras; + + @Schema(description = "配置的 iWork 凭证 appId;为空时使用默认凭证") + private String appId; + + @Schema(description = "iWork 操作人用户编号") + private String operatorUserId; + + @Schema(description = "是否强制刷新 token") + private Boolean forceRefreshToken; +} diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/framework/rpc/config/RpcConfiguration.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/framework/rpc/config/RpcConfiguration.java index 4d1e73d1..8dee651c 100644 --- a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/framework/rpc/config/RpcConfiguration.java +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/framework/rpc/config/RpcConfiguration.java @@ -6,12 +6,17 @@ import com.zt.plat.module.infra.api.config.ConfigApi; import com.zt.plat.module.infra.api.file.FileApi; import com.zt.plat.module.infra.api.websocket.WebSocketSenderApi; import com.zt.plat.module.system.api.dept.DeptApi; +import com.zt.plat.module.system.api.iwork.IWorkIntegrationApi; +import com.zt.plat.module.system.api.permission.PermissionApi; import com.zt.plat.module.system.api.sequence.SequenceApi; import com.zt.plat.module.system.api.sms.SmsSendApi; +import com.zt.plat.module.system.api.user.AdminUserApi; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Configuration; @Configuration(value = "templateRpcConfiguration", proxyBeanMethods = false) -@EnableFeignClients(clients = {FileApi.class, WebSocketSenderApi.class, ConfigApi.class, DeptApi.class, SequenceApi.class, BpmTaskApi.class, BpmProcessInstanceApi.class, SmsSendApi.class}) +@EnableFeignClients(clients = {AdminUserApi.class, FileApi.class, WebSocketSenderApi.class, +ConfigApi.class, DeptApi.class, SequenceApi.class, BpmTaskApi.class, BpmProcessInstanceApi.class, +SmsSendApi.class, PermissionApi.class, IWorkIntegrationApi.class}) public class RpcConfiguration { } diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/service/contract/DemoContractIWorkService.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/service/contract/DemoContractIWorkService.java new file mode 100644 index 00000000..b7dc444f --- /dev/null +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/service/contract/DemoContractIWorkService.java @@ -0,0 +1,21 @@ +package com.zt.plat.module.template.service.contract; + +import com.zt.plat.module.system.api.iwork.dto.IWorkOperationRespDTO; +import com.zt.plat.module.template.controller.admin.contract.vo.DemoContractIWorkCreateReqVO; +import com.zt.plat.module.template.controller.admin.contract.vo.DemoContractIWorkVoidReqVO; + +/** + * 合同 iWork 集成 Service + */ +public interface DemoContractIWorkService { + + /** + * 发起 iWork 用印流程 + */ + IWorkOperationRespDTO createWorkflow(DemoContractIWorkCreateReqVO reqVO); + + /** + * 作废 / 干预 iWork 用印流程 + */ + IWorkOperationRespDTO voidWorkflow(DemoContractIWorkVoidReqVO reqVO); +} diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/service/contract/DemoContractIWorkServiceImpl.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/service/contract/DemoContractIWorkServiceImpl.java new file mode 100644 index 00000000..14764e47 --- /dev/null +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/service/contract/DemoContractIWorkServiceImpl.java @@ -0,0 +1,91 @@ +package com.zt.plat.module.template.service.contract; + +import cn.hutool.core.util.StrUtil; +import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; +import com.zt.plat.framework.common.pojo.CommonResult; +import com.zt.plat.framework.common.util.object.BeanUtils; +import com.zt.plat.module.system.api.iwork.IWorkIntegrationApi; +import com.zt.plat.module.system.api.iwork.dto.IWorkOperationRespDTO; +import com.zt.plat.module.system.api.iwork.dto.IWorkWorkflowCreateReqDTO; +import com.zt.plat.module.system.api.iwork.dto.IWorkWorkflowVoidReqDTO; +import com.zt.plat.module.template.controller.admin.contract.vo.DemoContractIWorkCreateReqVO; +import com.zt.plat.module.template.controller.admin.contract.vo.DemoContractIWorkVoidReqVO; +import com.zt.plat.module.template.dal.dataobject.contract.DemoContractDO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.zt.plat.module.template.enums.ErrorCodeConstants.*; + +/** + * 合同 iWork 集成 Service 实现 + */ +@Slf4j +@Service +@Validated +public class DemoContractIWorkServiceImpl implements DemoContractIWorkService { + + private static final String BIZ_CALLBACK_KEY = "demo-contract-seal"; + + @Resource + private DemoContractService demoContractService; + + @Resource + private IWorkIntegrationApi iWorkIntegrationApi; + + @Override + public IWorkOperationRespDTO createWorkflow(DemoContractIWorkCreateReqVO reqVO) { + DemoContractDO contract = demoContractService.getDemoContract(reqVO.getContractId()); + if (contract == null) { + throw exception(DEMO_CONTRACT_NOT_EXISTS); + } + if (StrUtil.isBlank(contract.getFileUrl())) { + throw exception(DEMO_CONTRACT_FILE_URL_NOT_EXISTS); + } + + IWorkWorkflowCreateReqDTO reqDTO = BeanUtils.toBean(reqVO, IWorkWorkflowCreateReqDTO.class); + reqDTO.setXyywjUrl(contract.getFileUrl()); + reqDTO.setBizCallbackKey(BIZ_CALLBACK_KEY); + if (StrUtil.isBlank(reqDTO.getYwxtdjbh())) { + reqDTO.setYwxtdjbh(contract.getCode()); + } + + CommonResult result = iWorkIntegrationApi.createWorkflow(reqDTO); + IWorkOperationRespDTO data = extractDataOrThrow(result, DEMO_CONTRACT_IWORK_CREATE_FAILED, "发起用印", reqVO.getContractId()); + Long requestId = data.getPayload() != null && data.getPayload().getData() != null + ? data.getPayload().getData().getRequestId() : null; + log.info("[bizKey={}] 发起 iWork 用印成功,contractId={}, requestId={}, message={}", + BIZ_CALLBACK_KEY, reqVO.getContractId(), requestId, data.getMessage()); + return data; + } + + @Override + public IWorkOperationRespDTO voidWorkflow(DemoContractIWorkVoidReqVO reqVO) { + IWorkWorkflowVoidReqDTO reqDTO = BeanUtils.toBean(reqVO, IWorkWorkflowVoidReqDTO.class); + CommonResult result = iWorkIntegrationApi.voidWorkflow(reqDTO); + IWorkOperationRespDTO data = extractDataOrThrow(result, DEMO_CONTRACT_IWORK_VOID_FAILED, "作废用印", null); + Long requestId = data.getPayload() != null && data.getPayload().getData() != null + ? data.getPayload().getData().getRequestId() : null; + log.info("[bizKey={}] 作废 iWork 用印成功,requestId={}, message={}", + BIZ_CALLBACK_KEY, requestId != null ? requestId : reqVO.getRequestId(), data.getMessage()); + return data; + } + + private IWorkOperationRespDTO extractDataOrThrow(CommonResult result, + com.zt.plat.framework.common.exception.ErrorCode errorCode, + String action, + Long contractId) { + if (result == null) { + log.warn("[bizKey={}] {}失败,contractId={},原因=响应为空", BIZ_CALLBACK_KEY, action, contractId); + throw ServiceExceptionUtil.exception(errorCode, "响应为空"); + } + if (result.isError() || result.getData() == null) { + log.warn("[bizKey={}] {}失败,contractId={},code={},msg={}", + BIZ_CALLBACK_KEY, action, contractId, result.getCode(), result.getMsg()); + throw ServiceExceptionUtil.exception(errorCode, result.getMsg()); + } + return result.getData(); + } +} diff --git a/zt-server/src/main/resources/logback-spring.xml b/zt-server/src/main/resources/logback-spring.xml index 0e551414..65b22b7a 100644 --- a/zt-server/src/main/resources/logback-spring.xml +++ b/zt-server/src/main/resources/logback-spring.xml @@ -5,6 +5,10 @@ + + + +       @@ -31,7 +35,7 @@ ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false} - ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB} + ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-50MB} ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0} @@ -56,17 +60,42 @@ + + + ${LOG_DIR}-error.log + + ERROR + ACCEPT + DENY + + + ${LOG_DIR}-error.%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + + + + - - + + + + + + + + - + + diff --git a/模块功能清单.md b/模块功能清单.md new file mode 100644 index 00000000..f76e18f7 --- /dev/null +++ b/模块功能清单.md @@ -0,0 +1,18 @@ +# 模块功能清单 + +## 模块功能一览 + +- zt-dependencies:统一管理整套微服务的依赖版本,确保各模块框架与组件版本一致、可兼容。 +- zt-framework:封装共用的 Spring Boot Starter 与基础工具(多租户、数据权限、监控、消息、MyBatis 扩展、WebSocket、安全、测试等),为业务模块提供统一的底座能力。 +- zt-gateway:基于 Spring Cloud Gateway 实现的服务网关,承担路由转发、统一鉴权、限流、灰度、日志等流量治理职责。 +- zt-server:汇聚管理端与用户端的统一入口服务,承载聚合接口与对外 API 发布。 +- 系统管理模块:提供用户、角色、部门、岗位、菜单、权限、租户、字典、地区、敏感词、短信邮件、站内信、OAuth2、SSO( epalt、e 办统一认证)、日志、外部同步(e 办数据同步、iWork 组织人员信息同步、中铝主数据同步、同平台跨系统采集与分发系统数据等)等一体化管理体验,支持企业组织架构与安全策略的全场景配置。 +- 基础设施模块:覆盖代码生成、配置中心、数据源、文件与存储通道、业务文件、标准命名库、接口文档、Redis、定时任务、监控日志、WebSocket、SkyWalking 等基础设施能力,提供可视化面板方便研发与运维协作。 +- 工作流程模块:基于 Flowable,提供 BPMN 与仿钉钉 SIMPLE 双设计器、流程建模、表单、监听器、表达式、任务办理、会签/或签/加签/驳回/抄送等流程能力,前端可视化配置与办理确保审批体验顺畅。 +- 数据总线模块:面向 API 网关编排的管理模块,具备接口定义与版本管理、凭证发放、策略编排、访问与运行日志、调用监控等能力,前端界面支持网关、凭证、策略、日志的一站式治理。 +- HTML 转 PDF 模块:提供 HTML 模板渲染为 PDF 的后台服务,支持批量转换与安全校验,适用于各种文档出具场景。 +- 规则引擎模块:提供业务规则建模、决策链编排、版本发布、测试仿真、运行监控等能力,并以可视化方式搭建与调试规则。 +- 报表与大屏模块:集成大屏与报表,支持设计、存储、预览、发布与数据源管理,实现数据可视化与报表快速构建。 +- OnlyOffice 集成:提供在线文档预览与协作入口,满足在线编辑、批注需求。 +- 公共页面、组件、状态与插件体系:包括登录/主页/个人中心等基础导航,图表表格表单等高复用组件,Pinia 状态管理以及 Element Plus、ECharts、form-create、国际化、UnoCSS、埋点等插件,支撑所有业务模块的产品化体验。 +- 信创化迁移改造:系统中所有不兼容国产化数据库的中间件,服务,以及模块都进行信创化改造,支持并适配达梦 8 数据库。