Merge branch 'dev' into 'test'
重写手动针对用户以及组织的单条同步逻辑 See merge request jygk/dsc!29
This commit is contained in:
514
docs/iWork用印流程集成开发文档.md
Normal file
514
docs/iWork用印流程集成开发文档.md
Normal file
@@ -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
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.zt.plat</groupId>
|
||||||
|
<artifactId>zt-module-system-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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<IWorkBizCallbackMessage> {
|
||||||
|
|
||||||
|
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` |
|
||||||
@@ -14,6 +14,7 @@ import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
||||||
import org.mybatis.spring.annotation.MapperScan;
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -45,6 +46,7 @@ public class ZtDataPermissionAutoConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
public MenuDataPermissionHandler menuDataPermissionHandler(MybatisPlusInterceptor interceptor) {
|
public MenuDataPermissionHandler menuDataPermissionHandler(MybatisPlusInterceptor interceptor) {
|
||||||
// 创建菜单数据权限处理器
|
// 创建菜单数据权限处理器
|
||||||
MenuDataPermissionHandler handler = new MenuDataPermissionHandler();
|
MenuDataPermissionHandler handler = new MenuDataPermissionHandler();
|
||||||
|
|||||||
@@ -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.datapermission.core.menudatapermission.handler.MenuDataPermissionHandler;
|
||||||
import com.zt.plat.framework.mybatis.core.util.MyBatisUtils;
|
import com.zt.plat.framework.mybatis.core.util.MyBatisUtils;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.ComponentScan;
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -20,6 +21,7 @@ public class MenuDataPermissionConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnBean(MybatisPlusInterceptor.class)
|
@ConditionalOnBean(MybatisPlusInterceptor.class)
|
||||||
|
@ConditionalOnMissingBean
|
||||||
public MenuDataPermissionHandler menuDataPermissionHandler(MybatisPlusInterceptor interceptor) {
|
public MenuDataPermissionHandler menuDataPermissionHandler(MybatisPlusInterceptor interceptor) {
|
||||||
// 创建菜单数据权限处理器
|
// 创建菜单数据权限处理器
|
||||||
MenuDataPermissionHandler handler = new MenuDataPermissionHandler();
|
MenuDataPermissionHandler handler = new MenuDataPermissionHandler();
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ public class DeptDataPermissionRule implements DataPermissionRule {
|
|||||||
/**
|
/**
|
||||||
* 基于用户的表字段配置
|
* 基于用户的表字段配置
|
||||||
* 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
|
* 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
|
||||||
* key:表名
|
|
||||||
* value:字段名
|
* value:字段名
|
||||||
*/
|
*/
|
||||||
private final Map<String, String> userColumns = new HashMap<>();
|
private final Map<String, String> userColumns = new HashMap<>();
|
||||||
@@ -262,7 +261,11 @@ public class DeptDataPermissionRule implements DataPermissionRule {
|
|||||||
if (Boolean.FALSE.equals(self)) {
|
if (Boolean.FALSE.equals(self)) {
|
||||||
return null;
|
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)) {
|
if (StrUtil.isEmpty(columnName)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,12 @@ public class DbSqlSessionFactory implements SessionFactory {
|
|||||||
// 当前系统适配 dm,如果存在 schema 为空的情况,从 connection 获取
|
// 当前系统适配 dm,如果存在 schema 为空的情况,从 connection 获取
|
||||||
try {
|
try {
|
||||||
if (getDatabaseSchema() == null || getDatabaseSchema().length() == 0){
|
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();
|
dbSqlSession.getSqlSession().getConnection().getSchema();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@@ -351,4 +356,39 @@ public class DbSqlSessionFactory implements SessionFactory {
|
|||||||
public void setUsePrefixId(boolean usePrefixId) {
|
public void setUsePrefixId(boolean usePrefixId) {
|
||||||
this.usePrefixId = 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import java.io.IOException;
|
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.common.pojo.CommonResult.success;
|
||||||
import static com.zt.plat.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
import static com.zt.plat.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
||||||
import static com.zt.plat.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment;
|
import static com.zt.plat.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment;
|
||||||
@@ -52,13 +53,13 @@ public class FileController {
|
|||||||
private FileService fileService;
|
private FileService fileService;
|
||||||
|
|
||||||
@GetMapping("/get")
|
@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<FileRespVO> getPreviewUrl(@RequestParam("fileId") Long fileId,
|
public CommonResult<FileRespVO> getPreviewUrl(@RequestParam("fileId") Long fileId,
|
||||||
@RequestParam(value = "code", required = false) String code,
|
@RequestParam(value = "code", required = false) String code,
|
||||||
HttpServletRequest request) throws Exception {
|
HttpServletRequest request) throws Exception {
|
||||||
FileDO fileDO = fileService.getActiveFileById(fileId);
|
FileDO fileDO = fileService.getActiveFileById(fileId);
|
||||||
if (fileDO == null) {
|
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);
|
FileRespVO fileRespVO = BeanUtils.toBean(fileDO, FileRespVO.class);
|
||||||
|
|
||||||
// 加密文件:塞入“临时解密预览 URL”
|
// 加密文件:塞入“临时解密预览 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");
|
return CommonResult.error(HttpStatus.BAD_REQUEST.value(), "加密文件预览需要验证码 code");
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// 验证通过:发放给 kkfile 用的短期 token(kkfile 不带登录态)
|
// 验证通过:发放给 kkfile 用的短期 token(kkfile 不带登录态)
|
||||||
Long userId = getLoginUserId();
|
Long userId = getLoginUserId();
|
||||||
boolean flag = fileService.verifyCode(fileId, userId, code);
|
boolean flag = fileService.verifyCode(fileId, userId, code);
|
||||||
if(!flag){
|
if(!flag){
|
||||||
return CommonResult.customize(null, HttpStatus.INTERNAL_SERVER_ERROR.value(), "验证码错误");
|
return error(HttpStatus.BAD_REQUEST.value(), "验证码错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = fileService.generatePreviewToken(fileId, userId);
|
String token = fileService.generatePreviewToken(fileId, userId);
|
||||||
|
|
||||||
String baseUrl = buildPublicBaseUrl(request); // 见下方函数
|
String baseUrl = buildPublicBaseUrl(request); // 见下方函数
|
||||||
|
|
||||||
String fullfilename = java.net.URLEncoder
|
String fullfilename = java.net.URLEncoder
|
||||||
.encode(fileDO.getName(), java.nio.charset.StandardCharsets.UTF_8)
|
.encode(fileDO.getName(), java.nio.charset.StandardCharsets.UTF_8)
|
||||||
.replace("+", "%20");
|
.replace("+", "%20");
|
||||||
|
|
||||||
String decryptUrl = baseUrl + "/admin-api/infra/file/preview-decrypt"
|
String decryptUrl = baseUrl + "/admin-api/infra/file/preview-decrypt"
|
||||||
+ "?fileId=" + fileId
|
+ "?fileId=" + fileId
|
||||||
+ "&token=" + token
|
+ "&token=" + token
|
||||||
@@ -215,14 +214,14 @@ public class FileController {
|
|||||||
try {
|
try {
|
||||||
sendTypeEnum = VerifyCodeSendType.valueOf(sendType.trim().toUpperCase());
|
sendTypeEnum = VerifyCodeSendType.valueOf(sendType.trim().toUpperCase());
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
return CommonResult.error(HttpStatus.BAD_REQUEST.value(),
|
return error(HttpStatus.BAD_REQUEST.value(),
|
||||||
"sendType 参数不合法,可选:SMS / E_OFFICE");
|
"sendType 参数不合法,可选:SMS / E_OFFICE");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileDO activeFileById = fileService.getActiveFileById(fileId);
|
FileDO activeFileById = fileService.getActiveFileById(fileId);
|
||||||
if (activeFileById == null) {
|
if (activeFileById == null) {
|
||||||
return CommonResult.error(HttpStatus.NOT_FOUND.value(), "文件不存在");
|
return error(HttpStatus.NOT_FOUND.value(), "文件不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
FileRespVO fileRespVO = BeanUtils.toBean(activeFileById, FileRespVO.class);
|
FileRespVO fileRespVO = BeanUtils.toBean(activeFileById, FileRespVO.class);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class DataPermissionConfiguration {
|
|||||||
rule.addDeptColumn(DeptDO.class, "id");
|
rule.addDeptColumn(DeptDO.class, "id");
|
||||||
// user
|
// user
|
||||||
rule.addUserColumn(AdminUserDO.class, "id");
|
rule.addUserColumn(AdminUserDO.class, "id");
|
||||||
|
rule.addUserColumn("system_users_work_code", "workcode");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,14 +47,14 @@ spring:
|
|||||||
primary: master
|
primary: master
|
||||||
datasource:
|
datasource:
|
||||||
master:
|
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
|
username: SYSDBA
|
||||||
password: pgbsci6ddJ6Sqj@e
|
password: P@ssword25
|
||||||
slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
|
slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
|
||||||
lazy: true # 开启懒加载,保证启动速度
|
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
|
username: SYSDBA
|
||||||
password: pgbsci6ddJ6Sqj@e
|
password: P@ssword25
|
||||||
|
|
||||||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||||
data:
|
data:
|
||||||
|
|||||||
Reference in New Issue
Block a user