1. 新增 iwork 同步用户组织信息接口

2. 修复错误设置版本信息在 zt-dependencies 的 bug
This commit is contained in:
chenbowen
2025-11-20 18:27:01 +08:00
parent 52a0b561f9
commit 0b646295da
27 changed files with 2040 additions and 151 deletions

340
docs/iWork集成说明.md Normal file
View 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&lt;IWorkFormFieldVO&gt;):该明细行下的字段集合(必填)。
- IWorkDetailTableVO明细表
- `tableDBName` (String)iWork 明细表表名(必填,如 `formtable_main_26_dt1`)。
- `records` (List&lt;IWorkDetailRecordVO&gt;):明细记录集合(必填)。
---
## 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. 使用注册返回的密钥申请 tokenapply-tokentoken 会被按 `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`

View File

View 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
View 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` 联调接口验证配置是否正确,再接入正式流程。

View 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 BodyContent-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
&timestamp=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` 条件快速定位第三方问题。