1. 新增 iwork 同步用户组织信息接口
2. 修复错误设置版本信息在 zt-dependencies 的 bug
This commit is contained in:
340
docs/iWork集成说明.md
Normal file
340
docs/iWork集成说明.md
Normal file
@@ -0,0 +1,340 @@
|
||||
# iWork 统一集成使用说明
|
||||
|
||||
本文档介绍如何在 System 模块中使用项目已实现的统一 iWork 流程发起能力(controller + service + properties)。内容包含:配置项、调用方式(内部 Java 调用 & 外部 HTTP 调用)、请求/响应示例、错误处理、缓存与 Token 生命周期、典型问题与排查步骤。
|
||||
|
||||
---
|
||||
|
||||
## 概览
|
||||
|
||||
项目在 `system` 模块下实现了一套对外统一的 iWork 集成能力:
|
||||
|
||||
- 提供管理端接口(REST),路径前缀:`/system/integration/iwork`。
|
||||
- 提供 Service 层 `IWorkIntegrationService`,供其它模块以 Spring Bean 注入方式直接调用。
|
||||
- 使用 `IWorkProperties` 绑定 `application.yml` 中 `iwork` 的配置项。
|
||||
- Token / 会话采用本地 Caffeine 缓存缓存(按 appId + operatorUserId 缓存 session),并在到期前按配置提前刷新。
|
||||
- 使用统一配置的 appId、公钥以及默认流程编号,无需再维护多套凭证。
|
||||
|
||||
---
|
||||
|
||||
## 配置(YAML)
|
||||
|
||||
在 `application.yml`(或 profile)中,添加或修改如下项(示例摘自 `zt-server/src/main/resources/application.yaml`):
|
||||
|
||||
```yaml
|
||||
iwork:
|
||||
base-url: https://iwork.example.com
|
||||
app-id: my-iwork-app # 固定使用的 iWork 应用编号
|
||||
client-public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A... # 与 iWork 约定的客户端公钥(Base64)
|
||||
user-id: system # 默认操作用户(当调用未指定 operatorUserId 时使用)
|
||||
org:
|
||||
token-seed: 5936562a-d47c-4a29-9b74-b310e6c971b7
|
||||
paths:
|
||||
subcompany-page: /api/hrm/resful/getHrmsubcompanyWithPage
|
||||
department-page: /api/hrm/resful/getHrmdepartmentWithPage
|
||||
job-title-page: /api/hrm/resful/getJobtitleInfoWithPage
|
||||
user-page: /api/hrm/resful/getHrmUserInfoWithPage
|
||||
sync-subcompany: /api/hrm/resful/synSubcompany
|
||||
sync-department: /api/hrm/resful/synDepartment
|
||||
sync-job-title: /api/hrm/resful/synJobtitle
|
||||
sync-user: /api/hrm/resful/synHrmresource
|
||||
workflow-id: 54 # 当调用方未传 workflowId 时使用的默认流程编号
|
||||
paths:
|
||||
register: /api/ec/dev/auth/regist
|
||||
apply-token: /api/ec/dev/auth/applytoken
|
||||
user-info: /api/workflow/paService/getUserInfo
|
||||
create-workflow: /api/workflow/paService/doCreateRequest
|
||||
void-workflow: /api/workflow/paService/doCancelRequest
|
||||
token:
|
||||
ttl-seconds: 3600 # token 有效期(秒)
|
||||
refresh-ahead-seconds: 60 # 在到期前多少秒认为需要刷新
|
||||
client:
|
||||
connect-timeout: 5s
|
||||
response-timeout: 30s
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `base-url` 为 iWork 网关的基础地址,不能留空。
|
||||
- `app-id` 与 `client-public-key` 共同构成注册/申请 token 所需的凭据信息,由配置统一提供,不再支持多套切换。
|
||||
- `workflow-id` 提供全局默认流程编号,单次调用也可通过 `workflowId` 覆盖。
|
||||
- 请求头键名固定为 `app-id`、`client-public-key`、`secret`、`token`、`time`、`user-id`,无需在配置中重复声明。
|
||||
- `org.*` 配置负责 iWork 人力组织 REST 代理:`token-seed` 为与 iWork 约定的标识,系统会自动将其与毫秒时间戳拼接并计算 MD5 生成 `key`,无需额外传递 token。
|
||||
|
||||
---
|
||||
|
||||
## 典型调用路径(Controller)
|
||||
|
||||
Controller 暴露的 REST 接口:
|
||||
|
||||
- POST /system/integration/iwork/user/resolve
|
||||
- 说明:根据外部识别信息查找 iWork 的用户编号(userId)。
|
||||
- 请求:见下方 `Resolve User` 示例。
|
||||
|
||||
- POST /system/integration/iwork/workflow/create
|
||||
- 说明:在 iWork 中发起流程。
|
||||
- 请求:见下方 `Create Workflow` 示例。
|
||||
|
||||
- POST /system/integration/iwork/workflow/void
|
||||
- 说明:作废/干预流程。
|
||||
- 请求:见下方 `Void Workflow` 示例。
|
||||
|
||||
这些接口的响应均使用项目的 `CommonResult` 封装,实际返回的业务对象在 `data` 字段。
|
||||
|
||||
---
|
||||
|
||||
### 人力组织 REST 接口(key + ts)
|
||||
|
||||
为对接 PDF 所述的人力组织 RESTFUL 接口,Controller 额外暴露了以下代理端点,用于通过 `key + ts` 生成的 token 与 iWork 交互:
|
||||
|
||||
- POST `/system/integration/iwork/hr/subcompany/page` —— 请求体传入 `params`(Map),对应 `getHrmsubcompanyWithPage`。
|
||||
- POST `/system/integration/iwork/hr/department/page` —— 对应 `getHrmdepartmentWithPage`。
|
||||
- POST `/system/integration/iwork/hr/job-title/page` —— 对应 `getJobtitleInfoWithPage`。
|
||||
- POST `/system/integration/iwork/hr/user/page` —— 对应 `getHrmUserInfoWithPage`。
|
||||
- POST `/system/integration/iwork/hr/subcompany/sync` —— 请求体传入 `data`(List<Map>),对应 `synSubcompany`。
|
||||
- POST `/system/integration/iwork/hr/department/sync` —— 对应 `synDepartment`。
|
||||
- POST `/system/integration/iwork/hr/job-title/sync` —— 对应 `synJobtitle`。
|
||||
- POST `/system/integration/iwork/hr/user/sync` —— 对应 `synHrmresource`。
|
||||
|
||||
所有请求均自动封装为 `application/x-www-form-urlencoded`,并把 `token` 字段设置为 `{"key":"<md5>","ts":"<timestamp>"}`,无需调用方重复计算。
|
||||
|
||||
---
|
||||
|
||||
## 请求 VO 说明(重要字段)
|
||||
|
||||
- IWorkBaseReqVO(公用字段)
|
||||
- `appId` (String):为兼容历史接口保留,系统始终使用配置项 `iwork.app-id`。
|
||||
- `operatorUserId` (String):在 iWork 内部代表操作人的用户编号(可为空,框架会使用 `properties.userId`)。
|
||||
- `forceRefreshToken` (Boolean):是否强制刷新 token(例如遇到 token 错误时强制刷新)。
|
||||
|
||||
- IWorkUserInfoReqVO(用于解析用户)
|
||||
- `identifierKey` (String):外部标识 key(必须,例如 "loginid")。
|
||||
- `identifierValue` (String):外部标识值(必须,例如用户名)。
|
||||
- `payload` (Map):额外的请求载荷,会与 identifier 合并后发送到 iWork。
|
||||
- `queryParams` (Map):如果需要通过查询参数传递额外信息,可使用此字段。
|
||||
|
||||
- IWorkUserInfoRespVO(解析用户响应)
|
||||
- `userId` (String):从 iWork 响应中解析出的用户编号(如果能解析到)。
|
||||
- `payload` / `rawBody`:原始返回信息。
|
||||
- `success` / `message`:调用成功标志与提示信息。
|
||||
|
||||
- IWorkWorkflowCreateReqVO(发起流程)
|
||||
- `requestName` (String):流程标题。
|
||||
- `workflowId` (Long):流程模板 ID(可选,缺省时使用配置的默认值)。
|
||||
- `mainFields` (`List<IWorkFormFieldVO>`):主表字段集合。
|
||||
- `detailTables` (`List<IWorkDetailTableVO>`):明细表集合(可选)。
|
||||
- `otherParams` / `formExtras`:额外参数,`formExtras` 会以 form-data 方式追加。
|
||||
|
||||
- IWorkWorkflowVoidReqVO(作废)
|
||||
- `requestId` (String):流程请求编号(必填)。
|
||||
- `reason`、`extraParams`、`formExtras` 等用于传递作废原因或额外字段。
|
||||
|
||||
- IWorkFormFieldVO(表单字段)
|
||||
- `fieldName` (String):字段名(必填),与 iWork 表单字段 key 对应。
|
||||
- `fieldValue` (String):字段值(必填)。
|
||||
|
||||
- IWorkDetailRecordVO(明细记录)
|
||||
- `recordOrder` (Integer):可选记录序号(从 0 开始),用于 iWork 明细排序。
|
||||
- `fields` (List<IWorkFormFieldVO>):该明细行下的字段集合(必填)。
|
||||
|
||||
- IWorkDetailTableVO(明细表)
|
||||
- `tableDBName` (String):iWork 明细表表名(必填,如 `formtable_main_26_dt1`)。
|
||||
- `records` (List<IWorkDetailRecordVO>):明细记录集合(必填)。
|
||||
|
||||
---
|
||||
|
||||
## Java(内部)调用示例
|
||||
|
||||
项目同时提供 `IWorkIntegrationService` Bean,可直接注入并调用:
|
||||
|
||||
```java
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkDetailRecordVO;
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkDetailTableVO;
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFormFieldVO;
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkOperationRespVO;
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkWorkflowCreateReqVO;
|
||||
import com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class MyService {
|
||||
private final IWorkIntegrationService iworkService;
|
||||
|
||||
public void startFlow() {
|
||||
IWorkWorkflowCreateReqVO req = new IWorkWorkflowCreateReqVO();
|
||||
// 使用 application.yml 中配置的 app-id,无需额外指定
|
||||
req.setRequestName("测试-创建流程");
|
||||
// 若需要覆盖配置的默认流程,可显式设置 workflowId
|
||||
// req.setWorkflowId(54L);
|
||||
|
||||
// 主表字段
|
||||
IWorkFormFieldVO nameField = new IWorkFormFieldVO();
|
||||
nameField.setFieldName("name");
|
||||
nameField.setFieldValue("张三");
|
||||
|
||||
IWorkFormFieldVO amountField = new IWorkFormFieldVO();
|
||||
amountField.setFieldName("amount");
|
||||
amountField.setFieldValue("1000");
|
||||
req.setMainFields(List.of(nameField, amountField));
|
||||
|
||||
// 明细表(可选)
|
||||
IWorkFormFieldVO detailField = new IWorkFormFieldVO();
|
||||
detailField.setFieldName("itemName");
|
||||
detailField.setFieldValue("办公用品");
|
||||
|
||||
IWorkDetailRecordVO record = new IWorkDetailRecordVO();
|
||||
record.setRecordOrder(0);
|
||||
record.setFields(List.of(detailField));
|
||||
|
||||
IWorkDetailTableVO detailTable = new IWorkDetailTableVO();
|
||||
detailTable.setTableDBName("formtable_main_26_dt1");
|
||||
detailTable.setRecords(List.of(record));
|
||||
req.setDetailTables(List.of(detailTable));
|
||||
|
||||
IWorkOperationRespVO resp = iworkService.createWorkflow(req);
|
||||
if (resp.isSuccess()) {
|
||||
// 处理成功,例如记录 requestId
|
||||
} else {
|
||||
// 日志或重试
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 若需使用特定凭证,可设置 `req.setAppId("my-iwork-app")`。
|
||||
- 若需覆盖默认流程模板,可调用 `req.setWorkflowId(123L)` 指定。
|
||||
- 若希望以特定 iWork 操作人发起,可设置 `req.setOperatorUserId("1001")`。
|
||||
|
||||
---
|
||||
|
||||
## HTTP(外部)调用示例(cURL)
|
||||
|
||||
1. Resolve user
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"appId":"my-iwork-app",
|
||||
"identifierKey":"loginid",
|
||||
"identifierValue":"zhangsan"
|
||||
}' \
|
||||
https://your-zt-server/admin-api/system/integration/iwork/user/resolve
|
||||
```
|
||||
|
||||
成功返回示例(CommonResult 包装):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"userId": "1001",
|
||||
"success": true,
|
||||
"payload": { ... },
|
||||
"rawBody": "{...}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Create workflow
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/json" -d '{
|
||||
"requestName":"测试REST创建流程",
|
||||
"workflowId":54,
|
||||
"mainFields":[{"fieldName":"name","fieldValue":"张三"}],
|
||||
"appId":"my-iwork-app"
|
||||
}' https://your-zt-server/admin-api/system/integration/iwork/workflow/create
|
||||
```
|
||||
|
||||
1. Void workflow
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/json" -d '{
|
||||
"requestId":"REQ-001",
|
||||
"reason":"作废原因",
|
||||
"appId":"my-iwork-app"
|
||||
}' https://your-zt-server/admin-api/system/integration/iwork/workflow/void
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心逻辑与细节
|
||||
|
||||
1. 基础参数解析
|
||||
|
||||
系统始终使用 `application.yml` 中配置的 `app-id` 与 `client-public-key` 与 iWork 通信。
|
||||
请求体中的 `appId` 字段仅为兼容历史调用而保留,框架内部不会使用该值做切换。
|
||||
|
||||
1. Workflow 模板解析
|
||||
|
||||
调用时优先使用请求体中的 `workflowId`。
|
||||
若未显式传入,则回退到全局 `iwork.workflow-id`,若仍为空则抛出 `IWORK_WORKFLOW_ID_MISSING`。
|
||||
|
||||
1. 注册 + RSA + Token
|
||||
|
||||
- 在首次或 token 过期时,会按以下步骤获取 session:
|
||||
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`。
|
||||
- 当 token 接近到期(`refresh-ahead-seconds`)时会在下一次请求触发刷新。
|
||||
|
||||
1. 请求构造
|
||||
|
||||
- JSON 请求使用 `application/json`,表单请求(如创建流程/作废)使用 `application/x-www-form-urlencoded`。
|
||||
- 认证 Header:由 `IWorkProperties.Headers` 中的常量控制,固定键名为 `app-id`、`client-public-key`、`secret`、`token`、`time`、`user-id`。
|
||||
|
||||
1. 响应解析
|
||||
|
||||
- 实现里对响应成功的判定比较宽松:检查 `code`、`status`、`success`、`errno` 等字段(支持布尔、字符串 ‘0’/‘1’/‘success’)以判断是否成功,并解析常见的 message 字段 `msg|message|errmsg`。
|
||||
|
||||
---
|
||||
|
||||
## 常见错误与排查
|
||||
|
||||
- baseUrl 未配置(IWORK_BASE_URL_MISSING)
|
||||
- 处理:确保 `iwork.base-url` 配置正确。
|
||||
|
||||
- 配置缺失(IWORK_CONFIGURATION_INVALID)
|
||||
- 场景:`app-id`、`client-public-key`、`user-id` 等关键字段没有配置或只包含空白字符。
|
||||
- 处理:在 `application.yml` 或配置中心中补充对应字段,确保它们与 iWork 侧一致。
|
||||
|
||||
- 流程编号缺失(IWORK_WORKFLOW_ID_MISSING)
|
||||
- 场景:请求体、凭证与全局配置均未提供流程模板编号。
|
||||
- 处理:在请求中指定 `workflowId`,或在配置中设置 `workflow-id` / 凭证级 `default-workflow-id`。
|
||||
|
||||
- RSA 加密/注册/申请 token 失败(IWORK_REGISTER_FAILED / IWORK_APPLY_TOKEN_FAILED / IWORK_REMOTE_REQUEST_FAILED)
|
||||
- 处理:通过日志查看 iWork 返回的 HTTP 状态码与 body,确认请求头/路径/参数是否匹配 iWork 网关要求。
|
||||
|
||||
- 用户解析失败
|
||||
- 确认 `identifierKey`/`identifierValue` 是否正确填写并与 iWork 的查询接口契合;可启用 `forceRefreshToken` 触发 session 刷新以排除 token 过期造成的问题。
|
||||
|
||||
---
|
||||
|
||||
## 进阶主题
|
||||
|
||||
- 并发与缓存
|
||||
- `sessionCache` 最大条目数为 256;若高并发/多凭证/多操作人场景,可能需调整容量。
|
||||
|
||||
- 超时与 HTTP 客户端
|
||||
- `IWorkProperties.client.response-timeout` 可用于设定响应超时;连接超时通常由 Reactor Netty 全局配置控制。
|
||||
|
||||
- 单元测试
|
||||
- 项目中已有 MockWebServer 测试样例(`IWorkIntegrationServiceImplTest`),可参考测试用例模拟 iWork 的注册、申请 token、用户查询、创建/作废流程的交互。
|
||||
|
||||
---
|
||||
|
||||
## 小结与建议
|
||||
|
||||
- 在配置中补齐 `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)失败。
|
||||
|
||||
文档已生成并保存到:`docs/iWork集成说明.md`。
|
||||
0
docs/主数据同步指南.md
Normal file
0
docs/主数据同步指南.md
Normal file
124
docs/分页汇总功能使用说明.md
Normal file
124
docs/分页汇总功能使用说明.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 分页汇总功能使用说明
|
||||
|
||||
本文档介绍如何在平台项目中启用分页接口的汇总行(SUM)统计能力。该能力基于 `PageResult` 返回体与 `@PageSum` 标注,在分页查询时自动计算并返回指定字段的合计值。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 需要在分页列表底部展示金额、数量等合计值。
|
||||
- 希望后端自动补充汇总信息,避免前端手动累加。
|
||||
- 已使用 `BaseMapperX` 及其 `selectPage` 等分页便捷方法。
|
||||
|
||||
## 功能概览
|
||||
|
||||
| 组件 | 位置 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| `@PageSum` | `com.zt.plat.framework.common.annotation.PageSum` | 标注需要参与 SUM 聚合的实体字段 |
|
||||
| `PageResult.summary` | `com.zt.plat.framework.common.pojo.PageResult` | 承载字段 -> `BigDecimal` 的汇总结果 |
|
||||
| `PageSumSupport` | `com.zt.plat.framework.mybatis.core.sum.PageSumSupport` | 负责扫描注解、克隆查询条件并执行 SUM 查询 |
|
||||
| `BaseMapperX.selectPage` | `com.zt.plat.framework.mybatis.core.mapper.BaseMapperX` | 在分页与非分页查询后自动附加汇总信息 |
|
||||
|
||||
## 接入步骤
|
||||
|
||||
### 1. 在实体上标注 `@PageSum`
|
||||
|
||||
```java
|
||||
@TableName("order_summary")
|
||||
public class OrderSummaryDO {
|
||||
|
||||
private Long id;
|
||||
|
||||
@PageSum
|
||||
private BigDecimal amount;
|
||||
|
||||
@PageSum(column = "tax_amount")
|
||||
private BigDecimal tax;
|
||||
|
||||
@PageSum(column = "discount")
|
||||
private BigDecimal discountSummary;
|
||||
|
||||
// 其它字段 ...
|
||||
}
|
||||
```
|
||||
|
||||
- 不传 `column` 时,默认使用 MyBatis-Plus 实体字段映射的数据库列。
|
||||
- 如需跨表或函数(例如 `sum(price * quantity)`),可在 `column` 中直接写 SQL 片段。
|
||||
- 必须是数值类型(`Number`、`BigDecimal`、原生数值)。非数值字段会被忽略并打印警告日志。
|
||||
- 对于并不存在于表中的“汇总专用字段”,仅需在 `@PageSum` 中声明 `exist = false`,框架会自动注入等效的 `@TableField(exist = false)`,无需再次编写 `@TableField` 注解。
|
||||
|
||||
### 2. 使用 `BaseMapperX` 的分页能力
|
||||
|
||||
```java
|
||||
PageResult<OrderSummaryRespVO> page = orderSummaryMapper.selectPage(pageParam, wrapper);
|
||||
```
|
||||
|
||||
- 仅 `BaseMapperX.selectPage`(含排序参数版本)支持自动附加汇总结果。
|
||||
- 对于 `PageParam.PAGE_SIZE_NONE`(不分页)场景同样有效。
|
||||
- `selectJoinPage` 暂未附加汇总信息,如需支持请二次封装。
|
||||
|
||||
> ⚠️ 目前的汇总增强依赖 MyBatis-Plus 默认分页(单表/简单条件)实现聚合。若需在复杂联表或高度自定义 SQL 中进行统计,请单独编写汇总接口,或在自定义逻辑中手工调用 `PageSumSupport.tryAttachSummary(...)`,避免影响现有查询语句。
|
||||
|
||||
### 3. 暴露响应结果
|
||||
|
||||
`PageResult` 现在包含两个与数量相关的属性:
|
||||
|
||||
- `total`:分页总数量,仍通过 `total` 字段返回(向后兼容 `totalCount` 反序列化)。
|
||||
- `summary`:Map 结构,键为实体字段名,值为 `BigDecimal` 类型的合计值。
|
||||
|
||||
示例响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"list": [
|
||||
{ "id": 1, "amount": 20.00, "tax": 1.20 },
|
||||
{ "id": 2, "amount": 30.00, "tax": 1.80 }
|
||||
],
|
||||
"total": 2,
|
||||
"summary": {
|
||||
"amount": 50.00,
|
||||
"tax": 3.00
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
前端即可直接读取 `data.summary.amount` 展示汇总行,无需手工聚合。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 汇总结果为空
|
||||
|
||||
- 检查实体字段是否正确标注 `@PageSum`,且类型为数值。
|
||||
- 确认 Mapper 的泛型实体与查询结果实体一致,`PageSumSupport` 会基于 Mapper 泛型解析实体类型。
|
||||
- 若查询条件覆盖了 `select` 列表(如显式调用 `select(...)`),请确保 SUM 语句仍能执行;`PageSumSupport` 会克隆 Wrapper 并重新设置 `select` 列表,手写 SQL 需保证兼容。
|
||||
|
||||
### 自定义 SQL & 复杂场景
|
||||
|
||||
- 对于需要复杂汇总(如 CASE WHEN),可在 `column` 属性中写 SQL 表达式:
|
||||
|
||||
```java
|
||||
@PageSum(column = "SUM(CASE WHEN status = 'PAID' THEN amount ELSE 0 END)")
|
||||
private BigDecimal paidAmount;
|
||||
```
|
||||
|
||||
- 当前实现 **仅会扫描 Mapper 泛型实体类** 上的 `@PageSum` 标注。若分页接口最终返回 VO,请先在实体上完成标注,再使用 `PageResult.convert(...)` 或其它方式将数据转换为 VO;转换后 `summary` 内容会被完整保留。
|
||||
|
||||
### 非 BaseMapperX 查询
|
||||
|
||||
- 目前自动聚合只对 `BaseMapperX.selectPage` 及分页列表查询有效。
|
||||
- 若使用 XML 自定义 SQL,可在逻辑中手动调用 `PageSumSupport.tryAttachSummary(mapper, wrapper, pageResult)`。
|
||||
|
||||
## 调试与测试
|
||||
|
||||
- 单元测试示例:`com.zt.plat.framework.mybatis.core.sum.PageSumSupportTest`。
|
||||
- 运行 `mvn -pl zt-framework/zt-spring-boot-starter-mybatis -am test` 可验证功能和回归。
|
||||
- 日志中会输出数字解析或字段配置异常的告警信息,便于定位问题。
|
||||
|
||||
## 变更兼容性
|
||||
|
||||
- `PageResult` 仍通过 `list`/`total` 提供原有分页数据,向后兼容旧接口。
|
||||
- 新增 `summary` 字段,前端可按需展示。
|
||||
- `totalCount` Setter / Getter 仍保留(`@JsonIgnore`),可兼容旧代码逻辑。
|
||||
|
||||
如需进一步扩展(例如 AVG、MAX 等聚合),可按现有结构在 `PageSumSupport` 基础上新增注解与聚合逻辑。
|
||||
194
docs/外部单点登录.md
Normal file
194
docs/外部单点登录.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# 外部单点登录(External SSO)接入说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
- 支持外部系统携带一次性 token 跳转本系统,实现免密单点登录。
|
||||
- 去除了历史实现中的 payload 解密、nonce 强校验、自动建号与邮箱匹配等逻辑,所有账号均需在本地预先存在并保持映射关系。
|
||||
- 前后端新增 `/system/sso/verify` 校验能力,返回标准的 `AuthLoginRespVO` 令牌信息,并记录审计与登录日志。
|
||||
- 通过 `ExternalSsoClient` 抽象外部接口调用,可按需自定义实现或复用默认 HTTP 客户端封装。
|
||||
|
||||
## 关键组件
|
||||
|
||||
### 后端
|
||||
|
||||
- `ExternalSsoServiceImpl`:单点登录主流程,实现参数校验、外部用户查询、本地账号匹配、令牌签发与日志记录。
|
||||
- `ExternalSsoStrategy`:定义按来源系统拆分的策略接口,不同系统可实现自定义的拉取与匹配逻辑。
|
||||
- `DefaultExternalSsoStrategy`:默认策略实现,复用配置化的 HTTP 客户端与匹配顺序,可按优先级被自定义策略覆盖。
|
||||
- `ExternalSsoClient`:获取外部用户信息的接口抽象。
|
||||
- `DefaultExternalSsoClient`:基于 `RestTemplate` 的默认实现,支持 Header/Query/Body 占位符渲染、重试、响应字段映射、代理配置。
|
||||
- `ExternalSsoClientConfiguration`:通过 `@Configuration` 在缺省情况下注册 `DefaultExternalSsoClient` Bean,允许业务自定义覆盖。
|
||||
- `ExternalSsoProperties`:`external-sso.*` 配置项,包含开关、外部接口、账号映射、跨域等子配置;示例配置已同步到 `zt-module-system/zt-module-system-server/src/main/resources/application.yaml` 与 `zt-server/src/main/resources/application.yaml`。
|
||||
- `ExternalSsoVerifyReqVO`:`POST /system/sso/verify` 请求载荷。
|
||||
- `ExternalSsoUserInfo`:外部用户标准化模型,包含基础字段与自定义属性集合。
|
||||
|
||||
### 前端
|
||||
|
||||
- `src/router/modules/remaining.ts`:新增隐藏路由 `/externalsso`,用于回调页面。
|
||||
- `src/views/Login/ExternalSsoCallback.vue`:处理 URL 参数、调用校验接口、落地令牌、跳转目标页面,异常时提示并引导返回登录页。
|
||||
- `src/api/login/index.ts`:新增 `externalSsoVerify` 方法,请求 `/system/sso/verify` 并返回 `TokenType`。
|
||||
|
||||
## 调用流程
|
||||
|
||||
1. 外部系统完成本地认证后,构造 URL:`{本系统域名}/#/externalsso?x-token={外部token}&target={回跳地址}` 并跳转,可选附带 `sourceSystem` 指定来源系统。
|
||||
2. Vue 页面 `ExternalSsoCallback` 解析查询参数,优先读取 `x-token`(兼容历史的 `token` 参数)并校验是否存在;缺失时终止流程并提示错误。
|
||||
3. 前端调用 `POST /system/sso/verify`,请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "外部系统颁发的 token",
|
||||
"targetUri": "/#/dashboard", // 可选
|
||||
"sourceSystem": "partner-a" // 可选
|
||||
}
|
||||
```
|
||||
|
||||
4. `ExternalSsoServiceImpl#verifyToken` 执行以下步骤:
|
||||
|
||||
- 校验功能开关与 token 有效性。
|
||||
- 基于 `sourceSystem` 选择匹配的 `ExternalSsoStrategy`,若无匹配直接返回来源系统不支持。
|
||||
- 通过策略触发 `ExternalSsoClient` 拉取外部用户信息(默认调用配置的 HTTP 接口)。
|
||||
- 按策略内定义的匹配顺序(默认外部 ID → 用户名 → 手机号)查找本地账号,未命中直接返回 "未找到匹配的本地用户"。
|
||||
- 校验账号状态是否启用,签发 OAuth2 访问令牌并记录登录日志(类型 `LOGIN_EXTERNAL_SSO`)。
|
||||
- 生成操作审计日志,记录外部响应摘要、映射账号、目标地址、来源系统等信息。
|
||||
|
||||
5. 前端拿到 `AuthLoginRespVO`,通过 `authUtil.setToken`/`setTenantId` 持久化,随后跳转到整理后的 `targetUri`(默认 `/`)。
|
||||
6. 当流程出现异常(例如 token 缺失、外部接口失败、账号不存在)时,后端返回对应错误码,前端弹出提示并清除本地缓存。
|
||||
|
||||
下图为时序示意:
|
||||
|
||||
```text
|
||||
外部系统 -> 浏览器 -> Vue /externalsso -> POST /system/sso/verify -> ExternalSsoService -> ExternalSsoClient -> 外部用户接口
|
||||
\-> OAuth2TokenService -> 登录日志/审计日志
|
||||
```
|
||||
|
||||
## 前端细节
|
||||
|
||||
- `ExternalSsoCallback.vue` 对 `target`/`targetUri`/`redirect` 参数做统一解码与归一化,支持携带绝对地址或 hash 路由,防止开放跳转漏洞。
|
||||
- 解析 URL 中的 `sourceSystem`(兼容 `source`、`systemCode`)并透传给后端,便于在多来源系统场景下选择策略。
|
||||
- 在成功回调后调用 `router.replace`,保证不会产生历史记录;失败时引导用户回到 `/login` 并附带原目标地址。
|
||||
- 通过 `buildErrorMessage` 兼容后端返回的 `msg`、`Error` 对象或字符串,统一展示错误提示。
|
||||
|
||||
## 后端流程拆解
|
||||
|
||||
- **开关与参数校验**:
|
||||
- 关闭开关或缺少 token 时抛出 `EXTERNAL_SSO_DISABLED` / `EXTERNAL_SSO_TOKEN_MISSING`。
|
||||
- **外部用户获取**:
|
||||
- 默认客户端会在 `external-sso.remote` 中加载请求配置,支持 GET/POST 等多种场景。
|
||||
- 占位符:`${externalUserId}`(初始值为 token)、`${shareToken}`/`${token}`(共享服务访问 token)、`${xToken}`(原始回调 token)、`${targetUri}`、`${sourceSystem}`。
|
||||
- 会自动通过 `ShareServiceUtils` 获取共享服务访问 token,并写入 `ShareServiceProperties#tokenHeaderName` 对应的请求头。
|
||||
- 请求体会以 JSON 形式发送 `{ "x-token": "回调参数中的 token" }`,满足上游接口 `S_BF_CS_01` 的要求。
|
||||
- `validateResponse` 可按 `codeField` 与 `successCode` 校验业务状态,失败时抛出带详细信息的 `ExternalSsoClientException`。
|
||||
- **本地账号匹配**:
|
||||
- 使用 `mapping.order` 控制字段优先级;`custom.entries` 保存 "外部ID → 本地用户ID" 静态映射。
|
||||
- 找不到用户或账号禁用时分别抛出 `EXTERNAL_SSO_USER_NOT_FOUND`、`EXTERNAL_SSO_USER_DISABLED`,均会同步写入登录日志。
|
||||
- **令牌签发与日志**:
|
||||
- 通过 `OAuth2TokenService#createAccessToken` 使用默认客户端 `CLIENT_ID_DEFAULT` 颁发本地访问令牌。
|
||||
- `recordAuditLog` 把原始响应的 SHA-256 摘要、外部属性、token 摘要等写入操作日志,便于排查。
|
||||
- `recordLoginLog` 记录登录行为并在成功时更新用户最后登录 IP。
|
||||
|
||||
## `ExternalSsoClient` 扩展
|
||||
|
||||
- 默认实现 `DefaultExternalSsoClient` 由 `ExternalSsoClientConfiguration` 自动注册,若需要接入其它协议,可在任意配置类中自定义:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public ExternalSsoClient customExternalSsoClient(...) {
|
||||
return new MyExternalSsoClient(...);
|
||||
}
|
||||
```
|
||||
|
||||
- 默认实现要点:
|
||||
- 按配置构造 `RestTemplate`,支持连接/读取超时、HTTP 代理、重试次数。
|
||||
- 解析 JSON 响应,将指定字段映射到 `ExternalSsoUserInfo`,并保留原始 data 节点到 `attributes`。
|
||||
- 在解析失败、状态码异常时抛出带原始响应的 `ExternalSsoClientException`。
|
||||
|
||||
## 配置项参考
|
||||
|
||||
```yaml
|
||||
external-sso:
|
||||
enabled: true
|
||||
system-code: example-partner
|
||||
token:
|
||||
secret: "shared-secret"
|
||||
algorithm: AES
|
||||
allowed-clock-skew-seconds: 60
|
||||
max-age-seconds: 300
|
||||
require-nonce: false
|
||||
replay-protection-enabled: false
|
||||
remote:
|
||||
base-url: http://10.1.7.110
|
||||
user-info-path: /api/sso/user
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer ${token}"
|
||||
query-params: {}
|
||||
body:
|
||||
userId: "${externalUserId}"
|
||||
code-field: code
|
||||
success-code: "0"
|
||||
message-field: message
|
||||
data-field: data
|
||||
user-id-field: data.userId
|
||||
username-field: data.username
|
||||
nickname-field: data.nickname
|
||||
email-field: data.email
|
||||
mobile-field: data.mobile
|
||||
tenant-id-field: data.tenantId
|
||||
connect-timeout-millis: 3000
|
||||
read-timeout-millis: 5000
|
||||
retry-count: 1
|
||||
proxy:
|
||||
enabled: false
|
||||
mapping:
|
||||
order:
|
||||
- EXTERNAL_ID
|
||||
- USERNAME
|
||||
- MOBILE
|
||||
ignore-case: true
|
||||
update-profile-on-login: false
|
||||
custom:
|
||||
entries:
|
||||
partnerUser001: 10001
|
||||
cors:
|
||||
allowed-origins:
|
||||
- https://partner.example.com
|
||||
allowed-methods: ["OPTIONS", "POST"]
|
||||
allowed-headers: ["Authorization", "Content-Type"]
|
||||
allow-credentials: true
|
||||
max-age: 1800
|
||||
```
|
||||
|
||||
| 配置路径 | 说明 |
|
||||
| --- | --- |
|
||||
| `enabled` | 总开关,关闭后接口直接返回 `EXTERNAL_SSO_DISABLED` |
|
||||
| `system-code` | 默认来源系统标识,可作为 `sourceSystem` 的缺省值及日志标签 |
|
||||
| `token.*` | 若仍需解密/校验外部 token,可在自定义 `ExternalSsoClient` 内按需使用;默认实现仅透传 |
|
||||
| `remote.*` | 外部接口 HTTP 调用参数、字段映射与超时控制,模板占位符支持 `externalUserId`、`shareToken`(`token`)、`xToken`、`targetUri`、`sourceSystem` |
|
||||
| `mapping.order` | 本地账号匹配优先级,支持 `EXTERNAL_ID`、`USERNAME`、`MOBILE` |
|
||||
| `mapping.custom.entries` | 外部用户标识到本地用户 ID 的静态映射表 |
|
||||
| `cors.*` | 用于开放 `/system/sso/verify` 的跨域访问白名单 |
|
||||
|
||||
## 错误码与日志
|
||||
|
||||
- 错误码:
|
||||
- `1_002_000_050`:功能未开启。
|
||||
- `1_002_000_051`:token 缺失。
|
||||
- `1_002_000_055`:外部接口异常,具体原因写入占位符。
|
||||
- `1_002_000_056`:未匹配到本地用户。
|
||||
- `1_002_000_057`:本地用户已禁用。
|
||||
- `1_002_000_058`:来源系统不支持,需配置匹配的策略实现。
|
||||
- 登录日志:使用 `LoginLogTypeEnum.LOGIN_EXTERNAL_SSO` 记录成功/失败。
|
||||
- 操作日志:类型 `EXTERNAL_SSO/VERIFY`,包含外部用户 ID、映射账号、目标地址、来源系统、响应摘要等元数据。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 所有账号须提前维护映射,系统不会自动创建或按邮箱兜底匹配用户。
|
||||
- `targetUri` 会在前端归一化,避免开放跳转风险;无合法目标时默认跳转首页。
|
||||
- 确保外部接口返回的 JSON 字段与配置保持一致,必要时可通过 `remote.data-field` 指向具体节点。
|
||||
- 若外部接口速度较慢或易失败,可提高 `retry-count`、超时时间或自定义客户端实现。
|
||||
- 如需记录更多审计信息,可在 `ExternalSsoUserInfo#addAttribute` 注入自定义字段,审计日志会自动保留。
|
||||
|
||||
## 扩展与测试建议
|
||||
|
||||
- 通过提供新的 `ExternalSsoStrategy` 或 `ExternalSsoClient` Bean,可扩展不同来源系统的对接方式。
|
||||
- 建议为主要错误场景编写集成测试:token 缺失、映射缺失、来源系统不支持、外部接口超时、账号被禁用等。
|
||||
- 外部系统回调前可先调用 `/system/sso/verify` 联调接口验证配置是否正确,再接入正式流程。
|
||||
205
docs/数据总线模块大致功能与调用介绍.md
Normal file
205
docs/数据总线模块大致功能与调用介绍.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# Databus 模块 API 功能与第三方调用说明
|
||||
|
||||
> 适用范围:`zt-module-databus`(Server 侧)+ `zt-module-databus-api`(接口定义)。本文基于 2025-11-20 主干分支代码。
|
||||
|
||||
## 1. 模块定位与整体能力
|
||||
|
||||
- **目标**:对外暴露统一的数据/业务编排网关,允许后台在可视化界面中配置 API、步骤、变换与限流策略,并即时发布到运行态。
|
||||
- **核心特性**:
|
||||
1. API 全生命周期管理(定义、版本、回滚、发布缓存刷新)。
|
||||
2. 编排引擎基于 Spring Integration 动态装配,支持 Start/HTTP/RPC/Script/End 步骤及 JSON 变换链路。
|
||||
3. 多重安全防护:IP 白/黑名单、应用凭证、时间戳 + 随机串、报文加解密、签名、防重放、租户隔离、匿名固定用户等。
|
||||
4. QoS 能力:可插拔限流策略(Redis 固定窗口计数)、审计日志、追踪 ID & Step 级结果入库。
|
||||
5. Debug 支持:管理端 `POST /databus/gateway/invoke` 可注入任意参数模拟真实调用。
|
||||
|
||||
## 2. 运行时架构概览
|
||||
|
||||
| 组件 | 位置 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| `GatewaySecurityFilter` | `framework.integration.gateway.security` | 过滤并校验所有落在 `databus.api-portal.base-path` 之下的 HTTP 请求,完成 IP 校验、报文解密、签名、防重放、匿名用户注入、响应加密。 |
|
||||
| `ApiGatewayExecutionService` | `framework.integration.gateway.core` | 将 HTTP 请求映射为 `ApiInvocationContext`,调度 Integration Flow,构造统一响应。 |
|
||||
| `IntegrationFlowManager` | `framework.integration.gateway.core` | 按 `apiCode + version` 动态注册 Spring Integration Flow,支持热刷新与调试临时 Flow。 |
|
||||
| `ApiFlowDispatcher` | 同上 | 依据 apiCode/version 找到输入通道,发送请求并等待 `ApiInvocationContext` 回传。 |
|
||||
| `PolicyAdvisorFactory` + `DefaultRateLimitPolicyEvaluator` | `framework.integration.gateway.core/policy` | 在 Flow 上织入限流等策略,当前默认实现支持 Redis 固定窗口。 |
|
||||
| `ApiGatewayAccessLogger` | `framework.integration.gateway.core` | 生成访问日志 `databus_api_access_log`,记录 Trace、请求/响应、耗时、步骤结果等。 |
|
||||
| 管控 REST 控制器 | `controller.admin.gateway.*` | 管理 API 定义、版本、凭证、策略、访问日志等。 |
|
||||
|
||||
|
||||
## 4. 管控端 REST 接口速查
|
||||
|
||||
| 模块 | 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| API 定义 | GET | `/databus/gateway/definition/page` | 分页查询(支持 code/描述筛选)。 |
|
||||
| | GET | `/databus/gateway/definition/{id}` | 详情(含步骤、变换、限流绑定)。 |
|
||||
| | POST | `/databus/gateway/definition` | 新建定义,必填步骤(至少 Start+End)。 |
|
||||
| | PUT | `/databus/gateway/definition` | 更新并自动刷新对应 Flow。 |
|
||||
| | DELETE | `/databus/gateway/definition/{id}` | 删除并注销 Flow。 |
|
||||
| API 网关 | POST | `/databus/gateway/invoke` | 管理端调试调用。 |
|
||||
| | GET | `/databus/gateway/definitions` | 拉取当前已上线定义(供灰度/网关缓存)。 |
|
||||
| | POST | `/databus/gateway/cache/refresh` | 强制刷新所有 Flow 缓存。 |
|
||||
| API 版本 | GET | `/databus/gateway/version/get?id=` | 查询版本详情(自动还原 snapshotData)。 |
|
||||
| | GET | `/databus/gateway/version/page` | 分页。 |
|
||||
| | GET | `/databus/gateway/version/list?apiId=` | 列出某 API 的全部版本。 |
|
||||
| | PUT | `/databus/gateway/version/rollback` | 根据 `id + remark` 回滚。 |
|
||||
| | GET | `/databus/gateway/version/compare` | 差异对比(sourceId/targetId)。 |
|
||||
| 客户端凭证 | GET | `/databus/gateway/credential/page` | 分页。 |
|
||||
| | GET | `/databus/gateway/credential/get?id=` | 详情(含匿名配置)。 |
|
||||
| | POST | `/databus/gateway/credential/create` | 新增凭证。 |
|
||||
| | PUT | `/databus/gateway/credential/update` | 更新。 |
|
||||
| | DELETE | `/databus/gateway/credential/delete?id=` | 删除。 |
|
||||
| | GET | `/databus/gateway/credential/list-simple` | 下拉使用。 |
|
||||
| 限流策略 | GET | `/databus/gateway/policy/rate-limit/page` | 分页检索。 |
|
||||
| | GET | `/databus/gateway/policy/rate-limit/{id}` | 详情。 |
|
||||
| | GET | `/databus/gateway/policy/rate-limit/simple-list` | 精简列表。 |
|
||||
| | POST/PUT/DELETE | `/databus/gateway/policy/rate-limit` | 新增/更新/删除。 |
|
||||
| 访问日志 | GET | `/databus/gateway/access-log/page` | 分页(需 `databus:gateway:access-log:query` 权限)。 |
|
||||
| | GET | `/databus/gateway/access-log/get?id=` | 单条详情(自动补充 API 描述)。 |
|
||||
|
||||
> 所有接口默认返回 `CommonResult` 包装,字段 `code/message/data`。必要时参考对应 VO(位置 `controller.admin.gateway.vo`)。
|
||||
|
||||
## 5. API 生命周期管理要点
|
||||
|
||||
1. **状态机**:`ApiStatusEnum`(草稿/已上线/已下线/已废弃)。Integration Flow 只加载 `ONLINE` 状态定义。
|
||||
2. **版本快照**:每次保存时写入 `databus_api_version`,可通过 `snapshotData` 一键恢复(`rollback` 接口)。
|
||||
3. **变换校验**:保存时会校验同一级 `TransformPhaseEnum` 不可重复,并确保 Start/End 唯一且位于首尾。
|
||||
4. **缓存刷新**:
|
||||
- 单 API:创建/更新/删除后自动调用 `IntegrationFlowManager.refresh(apiCode, version)`。
|
||||
- 全量:管理员可调用 `/databus/gateway/cache/refresh` 做兜底。
|
||||
|
||||
## 6. 网关请求路径与响应格式
|
||||
|
||||
- **默认 Base Path**:`/admin-api/databus/api/portal`(可通过 `databus.api-portal.base-path` 覆盖;兼容旧版 `/databus/api/portal`)。
|
||||
- **最终路径**:`{basePath}/{apiCode}/{version}`,示例 `/admin-api/databus/api/portal/order.create/v1`。
|
||||
- **支持方法**:GET/POST/PUT/DELETE/PATCH,均被映射为 `ApiInvocationContext.httpMethod`。
|
||||
- **响应包装**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "OK",
|
||||
"response": { "bizField": "value" },
|
||||
"traceId": "c8a3d52f-..."
|
||||
}
|
||||
```
|
||||
|
||||
> `code` 与 HTTP 状态保持一致;`response` 为 API 变换后的业务体;所有错误也沿用该 Envelope(若启用响应加密则返回 Base64 字串)。
|
||||
|
||||
## 7. 配置项(`application.yml`)重点
|
||||
|
||||
```yaml
|
||||
databus:
|
||||
api-portal:
|
||||
base-path: /admin-api/databus/api/portal
|
||||
allowed-ips: [10.0.0.0/24] # 可为空表示全放行
|
||||
denied-ips: []
|
||||
enable-tenant-header: true
|
||||
tenant-header: ZT-Tenant-Id
|
||||
enable-audit: true
|
||||
enable-rate-limit: true
|
||||
security:
|
||||
enabled: true
|
||||
signature-type: MD5 # 或 SHA256
|
||||
encryption-type: AES # 或 DES
|
||||
allowed-clock-skew-seconds: 300
|
||||
nonce-ttl-seconds: 600
|
||||
require-body-encryption: true
|
||||
encrypt-response: true
|
||||
```
|
||||
|
||||
> `GatewaySecurityFilter` 会自动注册到最高优先级 +10,确保该路径的请求先经过安全校验。
|
||||
|
||||
## 8. 第三方调用流程详解
|
||||
|
||||
### 8.1 前置准备
|
||||
|
||||
1. **申请凭证**:在后台创建 `API 客户端凭证`,得到:
|
||||
- `appId`(对应 `ZT-App-Id` 头)
|
||||
- `encryptionKey`(用于 AES/DES 对称加密,服务器使用 `CryptoSignatureUtils.decrypt` 解密)
|
||||
- `encryptionType`、`signatureType`
|
||||
- `allowAnonymous` = true 时需选择一个固定系统用户(服务器将自动颁发内部 JWT)。
|
||||
2. **确定 API**:记录 `apiCode`、`version`、请求方法、入参/变换契约。
|
||||
3. **网络白名单**:将第三方出口 IP 加入 `allowed-ips`,否则直接返回 403。
|
||||
4. **Redis 要求**:需保证 Redis 可用(用于 nonce、防重放、限流计数)。
|
||||
|
||||
### 8.2 请求构建步骤
|
||||
|
||||
| 序号 | 操作 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 生成时间戳 | `timestamp = System.currentTimeMillis()`,与服务器时间差 ≤ 300s。 |
|
||||
| 2 | 生成随机串 | `nonce` 长度≥8,可使用 `UUID.randomUUID().toString().replace("-", "")`。 |
|
||||
| 3 | 准备明文 Body | 例如 `{"orderNo":"SO20251120001"}`,记为 `plainBody`。 |
|
||||
| 4 | 计算签名 | 将所有签名字段放入 Map(详见下节),调用 `CryptoSignatureUtils.verifySignature` 同样的规则:对 key 排序、跳过 `signature` 字段、使用 `&` 连接 `key=value`,再用 `MD5/SHA256` 计算;结果赋值给 `ZT-Signature`。*注意:签名使用明文 body。* |
|
||||
| 5 | 加密请求体 | 使用凭证的 `encryptionKey + encryptionType` 对 `plainBody` 进行对称加密,Base64 结果作为 HTTP Body;Content-Type 可设 `text/plain` 或 `application/json`。 |
|
||||
| 6 | 组装请求头 | `ZT-App-Id`, `ZT-Timestamp`, `ZT-Nonce`, `ZT-Signature`, `ZT-Tenant-Id`(可选), `X-Client-Id`(建议,与限流相关),如有自带 JWT 则设置 `Authorization`。 |
|
||||
| 7 | 发送请求 | URL = `https://{host}{basePath}/{apiCode}/{version}`,方法与 API 定义保持一致。 |
|
||||
|
||||
#### 签名字段示例
|
||||
|
||||
```
|
||||
appId=demo-app
|
||||
&body={"orderNo":"SO20251120001"}
|
||||
&nonce=0c5e2df9a1
|
||||
×tamp=1732070400000
|
||||
```
|
||||
|
||||
- Query 参数将被拼接为 `key=value`(多值以逗号连接),自动忽略 `signature` 字段。
|
||||
- Request Body 若非 JSON,则退化为字符串整体签名。
|
||||
|
||||
#### cURL 示例
|
||||
|
||||
```bash
|
||||
curl -X POST "https://gw.example.com/admin-api/databus/api/portal/order.create/v1" \
|
||||
-H "ZT-App-Id: demo-app" \
|
||||
-H "ZT-Timestamp: 1732070400000" \
|
||||
-H "ZT-Nonce: 0c5e2df9a1" \
|
||||
-H "ZT-Signature: 8e377..." \
|
||||
-H "X-Client-Id: mall" \
|
||||
-H "Content-Type: text/plain" \
|
||||
-d "Q2hhcnNldGV4dC1CYXNlNjQgZW5jcnlwdGVkIGJvZHk="
|
||||
```
|
||||
|
||||
> `-d` 的实际内容应当是 AES/ DES 加密后的 Base64 字符串。
|
||||
|
||||
### 8.3 响应处理
|
||||
|
||||
1. 读取 HTTP 状态与 `ApiGatewayResponse.code/message/traceId`。
|
||||
2. 若 `security.encrypt-response=true`,则响应体本身是加密串,需要使用同一 `encryptionKey/encryptionType` 解密得到 JSON,再解析 `response` 字段。
|
||||
3. `traceId` 可用于后台日志及 `访问日志` 页面关联排查。
|
||||
|
||||
### 8.4 错误与重试策略
|
||||
|
||||
| 场景 | 表现 | 处理建议 |
|
||||
| --- | --- | --- |
|
||||
| 时间戳/Nonce 不合法 | HTTP 401,`message` = `请求到达时间超出 300s`/`重复请求` | 校准服务器时间;`nonce` 不可重复(Redis TTL 默认 600s)。 |
|
||||
| 签名失败 | HTTP 401,`message` = `签名校验失败` | 检查签名字符串、字符编码、大小写。 |
|
||||
| 未配置密钥 | HTTP 500,`message` = `应用未配置加密密钥` | 在后台凭证中补齐密钥与算法,或取消强制加密。 |
|
||||
| 限流触发 | HTTP 429,`message` = `请求触发限流策略` | 调整 `X-Client-Id` 级并发或增大策略 `limit/windowSeconds`。 |
|
||||
| API 未发布 | HTTP 404,`message` = `API 定义未发布或已下线` | 确认 `status=ONLINE`,并刷新缓存。 |
|
||||
|
||||
## 9. 限流策略配置
|
||||
|
||||
- 存储在 `ApiPolicyRateLimitDO.config`,JSON 结构示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"limit": 1000,
|
||||
"windowSeconds": 60,
|
||||
"keyTemplate": "${apiCode}:${tenantId}:${header.X-Client-Id}" // 预留扩展
|
||||
}
|
||||
```
|
||||
|
||||
- 当前默认实现读取 `limit`(默认 100)与 `windowSeconds`(默认 60)。
|
||||
- Redis Key 格式:`databus:api:rl:{apiCode}:{version}:{X-Client-Id}`,当计数首次出现时自动设置过期。
|
||||
- 限流拦截后会抛出 `API_RATE_LIMIT_EXCEEDED`,在访问日志中标记 `status=1/2`。
|
||||
|
||||
## 10. 访问日志字段对照
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `traceId` | 来自 `TracerUtils`,可在日志与链路追踪中搜索。 |
|
||||
| `requestHeaders`, `requestBody`, `responseBody` | 默认截断至 4000 字符,JSON 序列化存储。 |
|
||||
| `status` | 0=成功,1=客户端错误,2=服务端错误,3=未知。 |
|
||||
| `stepResults` | 序列化的步骤执行列表(见 `ApiStepResult`),含 `request/response/elapsed/error`。 |
|
||||
| `extra` | 附加变量/属性,供排查自定义上下文。 |
|
||||
|
||||
> 可通过 `/databus/gateway/access-log/page` + `traceId` 或 `apiCode` 条件快速定位第三方问题。
|
||||
Reference in New Issue
Block a user