diff --git a/docs/iWork用印流程集成开发文档.md b/docs/iWork用印流程集成开发文档.md new file mode 100644 index 00000000..c435a76c --- /dev/null +++ b/docs/iWork用印流程集成开发文档.md @@ -0,0 +1,514 @@ +# iWork 用印流程集成开发文档 + +## 1. 概述 + +本文档描述了 ZT Cloud 平台与 iWork 系统的用印流程集成方案,包括流程发起、回调处理、消息通知及重试机制。 + +### 1.1 功能特性 + +- **流程发起**:支持用印专用流程和通用流程两种创建方式 +- **回调处理**:接收 iWork 回调,自动通知业务模块 +- **消息队列**:基于 RocketMQ 的异步消息通知机制 +- **自动重试**:失败回调自动重试,支持配置重试次数和间隔 +- **日志追踪**:完整记录流程创建和回调处理全生命周期 + +### 1.2 整体架构 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 业务系统 │────▶│ System 模块 │────▶│ iWork 系统 │ +│ (调用方) │ │ (集成层) │ │ (OA 流程) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + ▲ ▲ │ + │ │ │ + │ └───────────────────────┘ + │ iWork 流程完成后回调 + │ ┌─────────────────┐ + │ │ RocketMQ │ + │ │ (消息队列) │ + └───────────────┴─────────────────┘ +``` + +### 1.3 完整流程时序图 + +``` +┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ +│ 业务系统 │ │ System │ │ iWork │ │RocketMQ│ │业务消费者│ +└───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ + │ │ │ │ │ + │ 1.发起用印流程 │ │ │ │ + │──────────────▶│ │ │ │ + │ │ 2.创建流程 │ │ │ + │ │──────────────▶│ │ │ + │ │ 返回requestId│ │ │ + │ │◀──────────────│ │ │ + │ 返回结果 │ │ │ │ + │◀──────────────│ │ │ │ + │ │ │ │ │ + │ │ │ 3.OA流程审批 │ │ + │ │ │ (异步进行) │ │ + │ │ │ │ │ + │ │ 4.流程完成回调 │ │ │ + │ │◀──────────────│ │ │ + │ │ │ │ │ + │ │ 5.发送MQ消息 │ │ │ + │ │──────────────────────────────▶│ │ + │ │ │ │ 6.投递消息 │ + │ │ │ │──────────────▶│ + │ │ │ │ │ + │ │ │ │ 7.返回处理结果 │ + │ │ │ │◀──────────────│ + │ │ 8.接收结果 │ │ │ + │ │◀──────────────────────────────│ │ + │ │ │ │ │ + │ │ 9.更新日志状态 │ │ │ + │ │ (成功/重试) │ │ │ + └───────────────┴───────────────┴───────────────┴───────────────┘ +``` + +**流程说明**: + +1. **发起流程**:业务系统调用 System 模块的流程创建接口 +2. **创建流程**:System 模块调用 iWork API 创建 OA 流程,获取 `requestId` +3. **OA 审批**:流程在 iWork 系统中流转(审批、签章等),此过程异步进行 +4. **iWork 回调**:流程完成后,iWork 系统主动回调 System 模块的回调接口 +5. **MQ 通知**:System 模块将回调数据通过 RocketMQ 发送给业务消费者 +6. **业务处理**:业务消费者接收消息并处理(如保存签章文件、更新业务状态) +7. **返回结果**:业务消费者处理完成后,发送处理结果消息 +8. **接收结果**:System 模块接收处理结果 +9. **状态更新**:根据结果更新日志状态,失败则触发重试机制 + +## 2. 数据库设计 + +### 2.1 流程日志表 (system_iwork_workflow_log) + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| id | BIGINT | 主键 | +| request_id | VARCHAR(128) | iWork 请求编号(唯一) | +| workflow_id | BIGINT | 流程模板 ID | +| business_code | VARCHAR(128) | 业务编码 | +| biz_callback_key | VARCHAR(255) | 业务回调标识(MQ tag) | +| raw_request | VARCHAR(2000) | 创建请求原文 | +| status | VARCHAR(32) | 流程状态 | +| callback_status | INTEGER | 回调处理状态 | +| retry_count | INTEGER | 已重试次数 | +| max_retry | INTEGER | 最大重试次数 | +| last_error_message | VARCHAR(512) | 最后错误信息 | +| raw_callback | VARCHAR(2000) | 回调原文 | +| last_callback_time | TIMESTAMP | 最近回调时间 | +| tenant_id | BIGINT | 租户编号 | + +### 2.2 回调状态枚举 (callback_status) + +| 值 | 状态 | 说明 | +|----|------|------| +| 0 | CREATE_PENDING | 创建中 | +| 1 | CREATE_SUCCESS | 创建成功 | +| 2 | CREATE_FAILED | 创建失败 | +| 3 | CALLBACK_PENDING | 回调待处理 | +| 4 | CALLBACK_SUCCESS | 回调处理成功 | +| 5 | CALLBACK_FAILED | 回调处理失败 | +| 6 | CALLBACK_RETRYING | 回调重试中 | +| 7 | CALLBACK_RETRY_FAILED | 回调重试失败 | + +### 2.3 状态流转图 + +``` + ┌─────────────────────────────────────────────────────┐ + │ 流程创建阶段 │ + │ ┌──────────┐ 成功 ┌──────────┐ │ + │ │ PENDING │ ─────────▶ │ SUCCESS │ │ + │ │ (0) │ │ (1) │ │ + │ └──────────┘ └──────────┘ │ + │ │ │ + │ │ 失败 │ + │ ▼ │ + │ ┌──────────┐ │ + │ │ FAILED │ │ + │ │ (2) │ │ + │ └──────────┘ │ + └─────────────────────────────────────────────────────┘ + │ + │ iWork 回调 + ▼ + ┌─────────────────────────────────────────────────────┐ + │ 回调处理阶段 │ + │ ┌──────────┐ 成功 ┌──────────┐ │ + │ │ CALLBACK │ ─────────▶ │ CALLBACK │ │ + │ │ PENDING │ │ SUCCESS │ │ + │ │ (3) │ │ (4) │ │ + │ └──────────┘ └──────────┘ │ + │ │ │ + │ │ 失败 │ + │ ▼ │ + │ ┌──────────┐ 重试中 ┌──────────┐ │ + │ │ CALLBACK │ ◀───────▶ │ CALLBACK │ │ + │ │ FAILED │ │ RETRYING │ │ + │ │ (5) │ │ (6) │ │ + │ └──────────┘ └──────────┘ │ + │ │ │ + │ │ 重试次数耗尽 │ + │ ▼ │ + │ ┌──────────┐ │ + │ │ RETRY │ │ + │ │ FAILED(7)│ │ + │ └──────────┘ │ + └─────────────────────────────────────────────────────┘ +``` + +## 3. API 接口说明 + +### 3.1 用印流程创建 + +**接口地址**:`POST /admin-api/system/integration/iwork/workflow/create` + +**请求参数**: + +```json +{ + "operatorUserId": "1001", + "jbr": "1001", + "yybm": "2001", + "fb": "3001", + "sqsj": "2025-01-30", + "yyqx": "内部使用", + "yyfkUrl": "https://example.com/attachment.pdf", + "yysy": "合同盖章", + "xyywjUrl": "https://example.com/contract.pdf", + "yysx": "公章", + "ywxtdjbh": "DJ-2025-0001", + "bizCallbackKey": "seal-callback" +} +``` + +| 参数 | 必填 | 说明 | +|------|------|------| +| operatorUserId | 是 | 操作人 iWork 用户 ID | +| jbr | 是 | 用印申请人 | +| yybm | 是 | 用印部门 ID | +| fb | 是 | 用印单位(分部 ID) | +| sqsj | 是 | 申请时间 (yyyy-MM-dd) | +| yyqx | 是 | 用印去向 | +| xyywjUrl | 是 | 用印材料附件 URL | +| yysx | 是 | 用印事项 | +| ywxtdjbh | 是 | 业务系统单据编号 | +| bizCallbackKey | 否 | 业务回调标识 | +| yyfkUrl | 否 | 用印依据附件 URL | +| yysy | 否 | 用印事由 | + +### 3.2 通用流程创建 + +**接口地址**:`POST /admin-api/system/integration/iwork/workflow/create-generic` + +**请求参数**: + +```json +{ + "operatorUserId": "1001", + "workflowId": 54, + "payload": { + "requestName": "用印-DJ-2025-0001", + "mainData": [ + {"fieldName": "jbr", "fieldValue": "1001"}, + {"fieldName": "yybm", "fieldValue": "2001"} + ] + }, + "ywxtdjbh": "DJ-2025-0001", + "bizCallbackKey": "seal-callback" +} +``` + +| 参数 | 必填 | 说明 | +|------|------|------| +| operatorUserId | 是 | 操作人 iWork 用户 ID | +| workflowId | 是 | 流程模板 ID | +| payload | 是 | 透传给 iWork 的业务参数 | +| ywxtdjbh | 否 | 业务编码 | +| bizCallbackKey | 否 | 业务回调标识 | + +### 3.3 iWork 回调接口 + +**接口地址**:`POST /admin-api/system/integration/iwork/callback/file` + +**说明**:此接口供 iWork 系统回调,无需认证(@PermitAll, @TenantIgnore) + +**iWork 侧配置**:需要在 iWork 系统中配置回调地址,当流程完成时自动调用此接口。 + +**请求参数**: + +```json +{ + "requestId": "3603649", + "businessCode": "DJ-2025-0001", + "fileUrl": "https://iwork.example.com/signed-file.pdf", + "fileName": "已签章合同.pdf", + "status": "COMPLETED" +} +``` + +| 参数 | 必填 | 说明 | +|------|------|------| +| requestId | 是 | iWork 请求编号(与创建流程时返回的一致) | +| businessCode | 是 | 业务编码(与创建流程时传入的 ywxtdjbh 一致) | +| fileUrl | 是 | 签章后文件 URL | +| fileName | 否 | 文件名称 | +| status | 否 | 业务状态 | + +**回调处理逻辑**: + +1. 根据 `requestId` 查询流程创建日志,获取 `bizCallbackKey` +2. 更新日志状态为 `CALLBACK_PENDING` +3. 发送 MQ 消息通知业务模块(仅当 `bizCallbackKey` 存在时) +4. 返回处理结果 + +## 4. MQ 消息机制 + +### 4.1 消息流程图 + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ iWork 回调 │───▶│ System 模块 │───▶│ RocketMQ │───▶│ 业务消费者 │ +│ │ │ (Producer) │ │ │ │ │ +└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ + │ + ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ 更新日志状态 │◀───│ System 模块 │◀───│ RocketMQ │◀───│ 返回处理结果 │ +│ │ │ (Listener) │ │ │ │ │ +└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ +``` + +### 4.2 Topic 定义 + +| Topic | 说明 | +|-------|------| +| SYSTEM_IWORK_BIZ_CALLBACK | 回调通知消息(System → 业务模块) | +| SYSTEM_IWORK_BIZ_CALLBACK_RESULT | 处理结果消息(业务模块 → System) | + +### 4.3 回调通知消息 (IWorkBizCallbackMessage) + +```java +{ + "requestId": "3603649", + "bizCallbackKey": "seal-callback", + "payload": { /* 回调原始数据 */ }, + "attempt": 0, + "maxAttempts": 3 +} +``` + +**Tag 规则**:消息 tag = `bizCallbackKey`,业务模块按 tag 订阅 + +### 4.4 处理结果消息 (IWorkBizCallbackResultMessage) + +```java +{ + "requestId": "3603649", + "bizCallbackKey": "seal-callback", + "success": true, + "errorMessage": null, + "attempt": 0, + "maxAttempts": 3, + "payload": { /* 原始数据,用于重试 */ } +} +``` + +## 5. 业务模块接入指南 + +### 5.1 添加依赖 + +```xml + + com.zt.plat + zt-module-system-api + +``` + +### 5.2 实现消费者 + +```java +@Slf4j +@Component +@RequiredArgsConstructor +@RocketMQMessageListener( + topic = IWorkBizCallbackMessage.TOPIC, + consumerGroup = IWorkBizCallbackMessage.TOPIC + "_YOUR_BIZ_KEY", + selectorExpression = "your-biz-callback-key" // 与 bizCallbackKey 一致 +) +public class YourBizCallbackConsumer implements RocketMQListener { + + private final RocketMQTemplate rocketMQTemplate; + + @Override + public void onMessage(IWorkBizCallbackMessage message) { + log.info("收到 iWork 回调: requestId={}", message.getRequestId()); + + IWorkBizCallbackResultMessage result; + try { + // 处理业务逻辑 + processCallback(message); + + result = IWorkBizCallbackResultMessage.builder() + .requestId(message.getRequestId()) + .bizCallbackKey(message.getBizCallbackKey()) + .success(true) + .attempt(message.getAttempt()) + .maxAttempts(message.getMaxAttempts()) + .payload(message.getPayload()) + .build(); + } catch (Exception e) { + log.error("处理回调失败", e); + result = IWorkBizCallbackResultMessage.builder() + .requestId(message.getRequestId()) + .bizCallbackKey(message.getBizCallbackKey()) + .success(false) + .errorMessage(e.getMessage()) + .attempt(message.getAttempt()) + .maxAttempts(message.getMaxAttempts()) + .payload(message.getPayload()) + .build(); + } + + // 发送处理结果 + rocketMQTemplate.syncSend(IWorkBizCallbackResultMessage.TOPIC, result); + } + + private void processCallback(IWorkBizCallbackMessage message) { + // 业务处理逻辑 + // 1. 解析 payload 获取回调数据 + // 2. 更新业务状态 + // 3. 保存签章文件等 + } +} +``` + +### 5.3 关键配置项 + +| 配置项 | 说明 | +|--------|------| +| consumerGroup | 消费者组,建议格式:`TOPIC + "_" + bizCallbackKey` | +| selectorExpression | Tag 过滤,必须与发起流程时的 `bizCallbackKey` 一致 | + +### 5.4 注意事项 + +1. **bizCallbackKey 唯一性**:每个业务场景使用独立的 bizCallbackKey +2. **幂等处理**:消费者需实现幂等,同一 requestId 可能重复投递 +3. **必须返回结果**:处理完成后必须发送 `IWorkBizCallbackResultMessage` +4. **错误信息**:失败时填写 errorMessage,便于问题排查 + +## 6. 重试机制 + +### 6.1 重试流程 + +``` +业务处理失败 → 返回 success=false → System Listener 接收 + ↓ + 检查 attempt < maxAttempts? + ↓ ↓ + 是 否 + ↓ ↓ + 延迟后重新投递 标记最终失败 +``` + +### 6.2 配置参数 + +```yaml +iwork: + callback: + retry: + max-attempts: 3 # 最大重试次数 + delay-seconds: 5 # 重试间隔(秒) +``` + +### 6.3 手工重试 + +**接口地址**:`POST /admin-api/system/integration/iwork/log/retry` + +```json +{ + "requestId": "3603649" +} +``` + +## 7. 日志查询 + +### 7.1 分页查询接口 + +**接口地址**:`POST /admin-api/system/integration/iwork/log/page` + +**请求参数**: + +```json +{ + "requestId": "3603649", + "businessCode": "DJ-2025-0001", + "bizCallbackKey": "seal-callback", + "status": 4, + "pageNo": 1, + "pageSize": 10 +} +``` + +## 8. 本地开发调试 + +### 8.1 隔离测试环境 + +为避免与测试环境消息冲突,本地开发时需修改: + +1. **Listener 消费者组**:添加本地标识后缀 +```java +consumerGroup = IWorkBizCallbackResultMessage.TOPIC + "_CONSUMER_local" +``` + +2. **Listener Tag 过滤**:使用本地专用 tag +```java +selectorExpression = "local_test" +``` + +3. **业务消费者**:同样使用本地专用 bizCallbackKey +```java +selectorExpression = "your-biz-key_local" +``` + +4. **数据库记录**:将 `biz_callback_key` 设为本地专用值 + +### 8.2 调试建议 + +- 使用独立的 `bizCallbackKey` 避免消息串扰 +- 检查 RocketMQ 控制台确认消息投递情况 +- 关注日志中的 `requestId` 进行链路追踪 + +## 9. 常见问题 + +### Q1: 业务消费者收不到消息? + +检查项: +- `selectorExpression` 是否与 `bizCallbackKey` 一致 +- 消费者组名是否正确 +- RocketMQ 连接是否正常 + +### Q2: 收到重复消息? + +可能原因: +- 多个环境的 Listener 都在消费同一 topic +- 解决:使用独立的消费者组和 tag 过滤 + +### Q3: 重试不生效? + +检查项: +- 是否正确返回了 `IWorkBizCallbackResultMessage` +- `success` 字段是否为 `false` +- 配置的 `max-attempts` 是否大于当前 `attempt` + +## 10. 相关代码位置 + +| 组件 | 路径 | +|------|------| +| Controller | `zt-module-system-server/.../controller/admin/integration/iwork/IWorkIntegrationController.java` | +| Service | `zt-module-system-server/.../service/integration/iwork/impl/IWorkIntegrationServiceImpl.java` | +| 日志 Service | `zt-module-system-server/.../service/integration/iwork/impl/IWorkWorkflowLogServiceImpl.java` | +| MQ Producer | `zt-module-system-server/.../mq/iwork/IWorkBizCallbackProducer.java` | +| MQ Listener | `zt-module-system-server/.../mq/iwork/IWorkBizCallbackListener.java` | +| 消息定义 | `zt-module-system-api/.../mq/iwork/IWorkBizCallbackMessage.java` | +| 配置类 | `zt-module-system-server/.../framework/integration/iwork/config/IWorkProperties.java` | 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 6c95cc7c..063610e6 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 @@ -14,6 +14,7 @@ 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.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import java.util.List; @@ -45,6 +46,7 @@ public class ZtDataPermissionAutoConfiguration { } @Bean + @ConditionalOnMissingBean public MenuDataPermissionHandler menuDataPermissionHandler(MybatisPlusInterceptor interceptor) { // 创建菜单数据权限处理器 MenuDataPermissionHandler handler = new MenuDataPermissionHandler(); 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 index 172fba3d..7f3ad205 100644 --- 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 @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionIntercepto 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.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -20,6 +21,7 @@ public class MenuDataPermissionConfiguration { @Bean @ConditionalOnBean(MybatisPlusInterceptor.class) + @ConditionalOnMissingBean public MenuDataPermissionHandler menuDataPermissionHandler(MybatisPlusInterceptor interceptor) { // 创建菜单数据权限处理器 MenuDataPermissionHandler handler = new MenuDataPermissionHandler(); 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 60951994..274b9249 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 @@ -96,7 +96,6 @@ public class DeptDataPermissionRule implements DataPermissionRule { /** * 基于用户的表字段配置 * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 - * key:表名 * value:字段名 */ private final Map userColumns = new HashMap<>(); @@ -262,7 +261,11 @@ public class DeptDataPermissionRule implements DataPermissionRule { if (Boolean.FALSE.equals(self)) { return null; } - String columnName = userColumns.get(tableName); + String userColumnsKey = tableName; + if (StrUtil.isNotBlank(workCode)) { + userColumnsKey = userColumnsKey + "_work_code"; + } + String columnName = userColumns.get(userColumnsKey); if (StrUtil.isEmpty(columnName)) { return null; } diff --git a/zt-module-bpm/zt-module-bpm-server/src/main/java/org/flowable/common/engine/impl/db/DbSqlSessionFactory.java b/zt-module-bpm/zt-module-bpm-server/src/main/java/org/flowable/common/engine/impl/db/DbSqlSessionFactory.java index ed2f0c94..cbea9553 100644 --- a/zt-module-bpm/zt-module-bpm-server/src/main/java/org/flowable/common/engine/impl/db/DbSqlSessionFactory.java +++ b/zt-module-bpm/zt-module-bpm-server/src/main/java/org/flowable/common/engine/impl/db/DbSqlSessionFactory.java @@ -77,7 +77,12 @@ public class DbSqlSessionFactory implements SessionFactory { // 当前系统适配 dm,如果存在 schema 为空的情况,从 connection 获取 try { if (getDatabaseSchema() == null || getDatabaseSchema().length() == 0){ - setDatabaseSchema(dbSqlSession.getSqlSession().getConnection().getSchema()); + String schemaFromUrl = extractSchemaFromJdbcUrl(dbSqlSession.getSqlSession().getConnection()); + if (schemaFromUrl != null && schemaFromUrl.length() > 0) { + setDatabaseSchema(schemaFromUrl); + } else { + setDatabaseSchema(dbSqlSession.getSqlSession().getConnection().getSchema()); + } } dbSqlSession.getSqlSession().getConnection().getSchema(); } catch (SQLException e) { @@ -351,4 +356,39 @@ public class DbSqlSessionFactory implements SessionFactory { public void setUsePrefixId(boolean usePrefixId) { this.usePrefixId = usePrefixId; } + + private String extractSchemaFromJdbcUrl(java.sql.Connection connection) { + if (connection == null) { + return null; + } + try { + String url = connection.getMetaData().getURL(); + if (url == null || url.isEmpty()) { + return null; + } + int queryIndex = url.indexOf('?'); + if (queryIndex < 0 || queryIndex == url.length() - 1) { + return null; + } + String query = url.substring(queryIndex + 1); + String[] parts = query.split("[&;]"); + for (String part : parts) { + int eqIndex = part.indexOf('='); + if (eqIndex <= 0 || eqIndex == part.length() - 1) { + continue; + } + String key = part.substring(0, eqIndex).trim().toLowerCase(Locale.ROOT); + if ("schema".equals(key) || "currentschema".equals(key) || "current_schema".equals(key)) { + String value = part.substring(eqIndex + 1).trim(); + if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.substring(1, value.length() - 1); + } + return value; + } + } + } catch (SQLException ignored) { + return null; + } + return null; + } } 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 6b68ebe6..f0057d1c 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 @@ -31,6 +31,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import static com.zt.plat.framework.common.pojo.CommonResult.error; import static com.zt.plat.framework.common.pojo.CommonResult.success; import static com.zt.plat.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static com.zt.plat.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment; @@ -52,13 +53,13 @@ public class FileController { private FileService fileService; @GetMapping("/get") - @Operation(summary = "获取文件预览地址", description = "根据 fileId 返回文件预览 url(kkfile),支持加密文件预览,需要传递验证码 code,加密文件预览地址默认5分钟内有效,可在配置文件中添加zt.file.preview-expire-seconds配置") + @Operation(summary = "获取文件预览地址", description = "根据 fileId 返回文件预览 url(kkfile),支持加密文件预览,加密文件预览地址默认5分钟内有效,可在配置文件中添加zt.file.preview-expire-seconds配置有效时间") 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(), "文件不存在"); + return error(HttpStatus.NOT_FOUND.value(), "文件不存在"); } // 统计下载次数 @@ -68,26 +69,24 @@ public class FileController { FileRespVO fileRespVO = BeanUtils.toBean(fileDO, FileRespVO.class); // 加密文件:塞入“临时解密预览 URL” - if (Boolean.TRUE.equals(fileRespVO.getIsEncrypted())) { // FileDO 通过 aesIv 判断加密 + if (Boolean.TRUE.equals(fileRespVO.getIsEncrypted()) // FileDO 通过 aesIv 判断加密 + && cn.hutool.core.util.StrUtil.isNotBlank(code)) { // 预览文件会调用两次该接口,只有code不为空时候才塞url - if (cn.hutool.core.util.StrUtil.isBlank(code)) { + /*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(), "验证码错误"); + return error(HttpStatus.BAD_REQUEST.value(), "验证码错误"); } - String token = fileService.generatePreviewToken(fileId, userId); - String baseUrl = buildPublicBaseUrl(request); // 见下方函数 - String fullfilename = java.net.URLEncoder .encode(fileDO.getName(), java.nio.charset.StandardCharsets.UTF_8) .replace("+", "%20"); - String decryptUrl = baseUrl + "/admin-api/infra/file/preview-decrypt" + "?fileId=" + fileId + "&token=" + token @@ -215,14 +214,14 @@ public class FileController { try { sendTypeEnum = VerifyCodeSendType.valueOf(sendType.trim().toUpperCase()); } catch (IllegalArgumentException ex) { - return CommonResult.error(HttpStatus.BAD_REQUEST.value(), + return error(HttpStatus.BAD_REQUEST.value(), "sendType 参数不合法,可选:SMS / E_OFFICE"); } } FileDO activeFileById = fileService.getActiveFileById(fileId); if (activeFileById == null) { - return CommonResult.error(HttpStatus.NOT_FOUND.value(), "文件不存在"); + return error(HttpStatus.NOT_FOUND.value(), "文件不存在"); } FileRespVO fileRespVO = BeanUtils.toBean(activeFileById, FileRespVO.class); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/datapermission/config/DataPermissionConfiguration.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/datapermission/config/DataPermissionConfiguration.java index def9db9b..78725a5e 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/datapermission/config/DataPermissionConfiguration.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/datapermission/config/DataPermissionConfiguration.java @@ -24,6 +24,7 @@ public class DataPermissionConfiguration { rule.addDeptColumn(DeptDO.class, "id"); // user rule.addUserColumn(AdminUserDO.class, "id"); + rule.addUserColumn("system_users_work_code", "workcode"); }; } diff --git a/zt-server/src/main/resources/application-dev.yaml b/zt-server/src/main/resources/application-dev.yaml index 2a3d7761..c62b86e8 100644 --- a/zt-server/src/main/resources/application-dev.yaml +++ b/zt-server/src/main/resources/application-dev.yaml @@ -47,14 +47,14 @@ spring: primary: master datasource: master: - url: jdbc:dm://172.16.46.247:1050?schema=RUOYI-VUE-PRO + url: jdbc:dm://172.17.11.98:20870?schema=JYGK_TEST username: SYSDBA - password: pgbsci6ddJ6Sqj@e + password: P@ssword25 slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 lazy: true # 开启懒加载,保证启动速度 - url: jdbc:dm://172.16.46.247:1050?schema=RUOYI-VUE-PRO + url: jdbc:dm://172.17.11.98:20870?schema=JYGK_TEST username: SYSDBA - password: pgbsci6ddJ6Sqj@e + password: P@ssword25 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: