Merge branch 'dev-klw' into test

* dev-klw:
  清理与ztcloud中重复的代码,改为 jar 包方式引用 ztcloud

# Conflicts:
#	zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java
#	zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/sms/SmsCallbackController.java
#	zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/mq/message/sms/SmsSendMessage.java
This commit is contained in:
ranke
2026-02-03 16:18:37 +08:00
3394 changed files with 158 additions and 260136 deletions

View File

@@ -1,195 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zt-module-databus</artifactId>
<groupId>com.zt.plat</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>zt-module-databus-server</artifactId>
<name>${project.artifactId}</name>
<description>
Databus 模块。
</description>
<dependencies>
<!-- Spring Cloud 基础 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-env</artifactId>
</dependency>
<!-- 依赖服务 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-module-system-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-module-infra-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-module-databus-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- DataBus Server Starter -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-databus-server</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-biz-data-permission</artifactId>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-biz-tenant</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-redis</artifactId>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-rpc</artifactId>
</dependency>
<!-- Registry 注册中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Config 配置中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Job 定时任务相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-job</artifactId>
</dependency>
<!-- 消息队列相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-mq</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-test</artifactId>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-excel</artifactId>
</dependency>
<!-- 监控相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-monitor</artifactId>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-biz-business</artifactId>
<version>${revision}</version>
</dependency>
<!-- Spring Integration & Flow Orchestration -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-http</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-scripting</artifactId>
</dependency>
<!-- Reactive HTTP client for internal REST orchestration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Expression evaluation & caching utilities -->
<dependency>
<groupId>com.ibm.jsonata4java</groupId>
<artifactId>JSONata4Java</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.5.2.Final</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!-- Testing support -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.12.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-databus-client</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,45 +0,0 @@
package com.zt.plat.module.databus.api;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.util.monitor.TracerUtils;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.framework.common.util.servlet.ServletUtils;
import com.zt.plat.module.databus.api.dto.ApiAccessLogCreateReq;
import com.zt.plat.module.databus.api.provider.DatabusAccessLogProviderApi;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
import com.zt.plat.module.databus.service.gateway.ApiAccessLogService;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.Map;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
/**
*
* 2026/1/21 10:05
*/
@RestController
public class DatabusAccessLogProviderApiImpl implements DatabusAccessLogProviderApi {
@Resource
private ApiAccessLogService apiAccessLogService;
@Override
@PermitAll
public CommonResult<Boolean> add(Map<String, String> headers, ApiAccessLogCreateReq req) {
ApiAccessLogDO logDO = new ApiAccessLogDO();
BeanUtils.copyProperties(req, logDO);
logDO.setTraceId(TracerUtils.getTraceId());
logDO.setClientIp(ServletUtils.getClientIP());
logDO.setCreateTime(LocalDateTime.now());
logDO.setUpdateTime(LocalDateTime.now());
logDO.setCreator("1");
logDO.setUpdater("1");
apiAccessLogService.create(logDO);
return success(Boolean.TRUE);
}
}

View File

@@ -1,108 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiAccessLogConvert;
import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
import com.zt.plat.module.databus.service.gateway.ApiAccessLogService;
import com.zt.plat.module.databus.service.gateway.ApiDefinitionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
/**
* Databus API 访问日志控制器。
*/
@Tag(name = "管理后台 - Databus API 访问日志")
@RestController
@RequestMapping("/databus/gateway/access-log")
@Validated
public class ApiAccessLogController {
@Resource
private ApiAccessLogService apiAccessLogService;
@Resource
private ApiDefinitionService apiDefinitionService;
@GetMapping("/get")
@Operation(summary = "获取访问日志详情")
@Parameter(name = "id", description = "日志编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('databus:gateway:access-log:query')")
public CommonResult<ApiAccessLogRespVO> get(@RequestParam("id") Long id) {
ApiAccessLogDO logDO = apiAccessLogService.get(id);
ApiAccessLogRespVO respVO = ApiAccessLogConvert.INSTANCE.convert(logDO);
enrichDefinitionInfo(respVO);
return success(respVO);
}
@GetMapping("/page")
@Operation(summary = "分页查询访问日志")
@PreAuthorize("@ss.hasPermission('databus:gateway:access-log:query')")
public CommonResult<PageResult<ApiAccessLogRespVO>> page(@Valid ApiAccessLogPageReqVO pageReqVO) {
PageResult<ApiAccessLogDO> pageResult = apiAccessLogService.getPage(pageReqVO);
PageResult<ApiAccessLogRespVO> result = ApiAccessLogConvert.INSTANCE.convertPage(pageResult);
enrichDefinitionInfo(result.getList());
return success(result);
}
private void enrichDefinitionInfo(List<ApiAccessLogRespVO> list) {
// 对分页结果批量补充 API 描述,使用本地缓存减少重复查询
if (CollectionUtils.isEmpty(list)) {
return;
}
Map<String, String> cache = new HashMap<>(list.size());
list.forEach(item -> {
if (item == null) {
return;
}
String cacheKey = buildCacheKey(item.getApiCode(), item.getApiVersion());
if (!cache.containsKey(cacheKey)) {
cache.put(cacheKey, resolveApiDescription(item.getApiCode(), item.getApiVersion()));
}
item.setApiDescription(cache.get(cacheKey));
});
}
private void enrichDefinitionInfo(ApiAccessLogRespVO item) {
// 单条数据同样需要补全描述信息
if (item == null) {
return;
}
item.setApiDescription(resolveApiDescription(item.getApiCode(), item.getApiVersion()));
}
private String resolveApiDescription(String apiCode, String apiVersion) {
if (!StringUtils.hasText(apiCode)) {
return null;
}
String normalizedVersion = StringUtils.hasText(apiVersion) ? apiVersion.trim() : apiVersion;
// 通过网关定义服务补全 API 描述,提升页面可读性
return apiDefinitionService.findByCodeAndVersionIncludingInactive(apiCode, normalizedVersion)
.map(aggregate -> aggregate.getDefinition() != null ? aggregate.getDefinition().getDescription() : null)
.filter(StringUtils::hasText)
.orElse(null);
}
private String buildCacheKey(String apiCode, String apiVersion) {
// 组合唯一键,避免重复查询相同的 API 描述
return (apiCode == null ? "" : apiCode) + "#" + (apiVersion == null ? "" : apiVersion);
}
}

View File

@@ -1,96 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiClientCredentialConvert;
import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialSaveReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialSimpleRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO;
import com.zt.plat.module.databus.service.gateway.ApiAnonymousUserService;
import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
import static org.springframework.util.CollectionUtils.isEmpty;
@Tag(name = "管理后台 - API 客户端凭证")
@RestController
@RequestMapping("/databus/gateway/credential")
@RequiredArgsConstructor
@Validated
public class ApiClientCredentialController {
private final ApiClientCredentialService credentialService;
private final ApiAnonymousUserService anonymousUserService;
@GetMapping("/page")
@Operation(summary = "分页查询客户端凭证")
public CommonResult<PageResult<ApiClientCredentialRespVO>> page(ApiClientCredentialPageReqVO reqVO) {
PageResult<ApiClientCredentialDO> page = credentialService.getPage(reqVO);
PageResult<ApiClientCredentialRespVO> respPage = ApiClientCredentialConvert.INSTANCE.convertPage(page);
populateAnonymousInfo(respPage.getList());
return success(respPage);
}
@GetMapping("/get")
@Operation(summary = "查询凭证详情")
public CommonResult<ApiClientCredentialRespVO> get(@RequestParam("id") Long id) {
ApiClientCredentialDO credential = credentialService.get(id);
ApiClientCredentialRespVO respVO = ApiClientCredentialConvert.INSTANCE.convert(credential);
populateAnonymousInfo(List.of(respVO));
return success(respVO);
}
@PostMapping("/create")
@Operation(summary = "新增客户端凭证")
public CommonResult<Long> create(@Valid @RequestBody ApiClientCredentialSaveReqVO reqVO) {
return success(credentialService.create(reqVO));
}
@PutMapping("/update")
@Operation(summary = "更新客户端凭证")
public CommonResult<Boolean> update(@Valid @RequestBody ApiClientCredentialSaveReqVO reqVO) {
credentialService.update(reqVO);
return success(Boolean.TRUE);
}
@DeleteMapping("/delete")
@Operation(summary = "删除客户端凭证")
public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
credentialService.delete(id);
return success(Boolean.TRUE);
}
@GetMapping("/list-simple")
@Operation(summary = "获取启用的凭证列表(精简)")
public CommonResult<List<ApiClientCredentialSimpleRespVO>> listSimple() {
List<ApiClientCredentialDO> list = credentialService.listEnabled();
return success(ApiClientCredentialConvert.INSTANCE.convertSimpleList(list));
}
private void populateAnonymousInfo(List<ApiClientCredentialRespVO> list) {
if (isEmpty(list)) {
return;
}
list.stream()
.filter(item -> Boolean.TRUE.equals(item.getAllowAnonymous()) && item.getAnonymousUserId() != null)
.forEach(item -> anonymousUserService.find(item.getAnonymousUserId())
.ifPresent(details -> item.setAnonymousUserNickname(details.getNickname())));
}
}

View File

@@ -1,79 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiDefinitionConvert;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionDetailRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSummaryRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
import com.zt.plat.module.databus.framework.integration.gateway.core.IntegrationFlowManager;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.service.gateway.ApiDefinitionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_DEFINITION_NOT_FOUND;
@Tag(name = "管理后台 - API 定义管理")
@RestController
@RequestMapping("/databus/gateway/definition")
@RequiredArgsConstructor
@Validated
public class ApiDefinitionController {
private final ApiDefinitionService apiDefinitionService;
private final IntegrationFlowManager integrationFlowManager;
@GetMapping("/page")
@Operation(summary = "分页查询 API 定义")
public CommonResult<PageResult<ApiDefinitionSummaryRespVO>> getDefinitionPage(@Valid ApiDefinitionPageReqVO reqVO) {
PageResult<ApiDefinitionDO> pageResult = apiDefinitionService.getPage(reqVO);
return success(ApiDefinitionConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/{id}")
@Operation(summary = "获取 API 定义详情")
public CommonResult<ApiDefinitionDetailRespVO> getDefinition(@PathVariable("id") Long id) {
ApiDefinitionAggregate aggregate = apiDefinitionService.findById(id)
.orElseThrow(() -> ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND));
return success(ApiDefinitionConvert.INSTANCE.convert(aggregate));
}
@PostMapping
@Operation(summary = "创建 API 定义")
public CommonResult<Long> createDefinition(@Valid @RequestBody ApiDefinitionSaveReqVO reqVO) {
Long id = apiDefinitionService.create(reqVO);
integrationFlowManager.refresh(reqVO.getApiCode(), reqVO.getVersion());
return success(id);
}
@PutMapping
@Operation(summary = "更新 API 定义")
public CommonResult<Boolean> updateDefinition(@Valid @RequestBody ApiDefinitionSaveReqVO reqVO) {
ApiDefinitionAggregate before = apiDefinitionService.findById(reqVO.getId())
.orElseThrow(() -> ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND));
apiDefinitionService.update(reqVO);
integrationFlowManager.refresh(before.getDefinition().getApiCode(), before.getDefinition().getVersion());
integrationFlowManager.refresh(reqVO.getApiCode(), reqVO.getVersion());
return success(Boolean.TRUE);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除 API 定义")
public CommonResult<Boolean> deleteDefinition(@PathVariable("id") Long id) {
ApiDefinitionAggregate aggregate = apiDefinitionService.findById(id)
.orElseThrow(() -> ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND));
apiDefinitionService.delete(id);
integrationFlowManager.refresh(aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
return success(Boolean.TRUE);
}
}

View File

@@ -1,56 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiDefinitionConvert;
import com.zt.plat.module.databus.controller.admin.gateway.vo.ApiGatewayInvokeReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionDetailRespVO;
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayExecutionService;
import com.zt.plat.module.databus.framework.integration.gateway.core.IntegrationFlowManager;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse;
import com.zt.plat.module.databus.service.gateway.ApiDefinitionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - API 门户")
@RestController
@RequestMapping("/databus/gateway")
@RequiredArgsConstructor
public class ApiGatewayController {
private final ApiGatewayExecutionService executionService;
private final ApiDefinitionService apiDefinitionService;
private final IntegrationFlowManager integrationFlowManager;
@PostMapping(value = "/invoke", consumes = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "测试调用 API 编排")
public ResponseEntity<ApiGatewayResponse> invoke(@RequestBody ApiGatewayInvokeReqVO reqVO) {
return executionService.invokeForDebug(reqVO);
}
@GetMapping("/definitions")
@Operation(summary = "获取当前已发布 API 配置")
public CommonResult<List<ApiDefinitionDetailRespVO>> listDefinitions() {
List<ApiDefinitionDetailRespVO> definitions = apiDefinitionService.loadActiveDefinitions().stream()
.map(ApiDefinitionConvert.INSTANCE::convert)
.collect(Collectors.toList());
return success(definitions);
}
@PostMapping("/cache/refresh")
@Operation(summary = "刷新 API 缓存")
public CommonResult<Boolean> refreshCache() {
apiDefinitionService.refreshAllCache();
integrationFlowManager.refreshAll();
return success(Boolean.TRUE);
}
}

View File

@@ -1,84 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiPolicyRateLimitConvert;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySaveReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySimpleRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
import com.zt.plat.module.databus.service.gateway.ApiPolicyRateLimitService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_POLICY_NOT_FOUND;
@Tag(name = "管理后台 - 网关限流策略")
@RestController
@RequestMapping("/databus/gateway/policy/rate-limit")
@RequiredArgsConstructor
@Validated
public class ApiPolicyRateLimitController {
private final ApiPolicyRateLimitService rateLimitService;
@GetMapping("/page")
@Operation(summary = "分页查询限流策略")
public CommonResult<PageResult<ApiPolicyRespVO>> getRateLimitPolicyPage(@Valid ApiPolicyPageReqVO reqVO) {
PageResult<ApiPolicyRateLimitDO> pageResult = rateLimitService.getPage(reqVO);
return success(ApiPolicyRateLimitConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/{id}")
@Operation(summary = "查询限流策略详情")
public CommonResult<ApiPolicyRespVO> getRateLimitPolicy(@PathVariable("id") Long id) {
ApiPolicyRateLimitDO policy = rateLimitService.get(id)
.orElseThrow(() -> ServiceExceptionUtil.exception(API_POLICY_NOT_FOUND));
return success(ApiPolicyRateLimitConvert.INSTANCE.convert(policy));
}
@GetMapping("/simple-list")
@Operation(summary = "获取限流策略精简列表")
public CommonResult<List<ApiPolicySimpleRespVO>> getRateLimitPolicySimpleList() {
List<ApiPolicyRateLimitDO> list = rateLimitService.getSimpleList();
return success(ApiPolicyRateLimitConvert.INSTANCE.convertSimpleList(list));
}
@PostMapping
@Operation(summary = "创建限流策略")
public CommonResult<Long> createRateLimitPolicy(@Valid @RequestBody ApiPolicySaveReqVO reqVO) {
Long id = rateLimitService.create(reqVO);
return success(id);
}
@PutMapping
@Operation(summary = "更新限流策略")
public CommonResult<Boolean> updateRateLimitPolicy(@Valid @RequestBody ApiPolicySaveReqVO reqVO) {
rateLimitService.update(reqVO);
return success(Boolean.TRUE);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除限流策略")
public CommonResult<Boolean> deleteRateLimitPolicy(@PathVariable("id") Long id) {
rateLimitService.delete(id);
return success(Boolean.TRUE);
}
}

View File

@@ -1,97 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiVersionConvert;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionCompareRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionDetailRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionRollbackReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO;
import com.zt.plat.module.databus.service.gateway.ApiVersionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
/**
* API 版本历史控制器。
*/
@Tag(name = "管理后台 - API 版本历史")
@RestController
@RequestMapping("/databus/gateway/version")
@Validated
@Slf4j
public class ApiVersionController {
@Resource
private ApiVersionService apiVersionService;
@Resource
private ObjectMapper objectMapper;
@GetMapping("/get")
@Operation(summary = "获取 API 版本详情")
@Parameter(name = "id", description = "版本编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('databus:gateway:version:query')")
public CommonResult<ApiVersionDetailRespVO> getVersion(@RequestParam("id") Long id) {
ApiVersionDO versionDO = apiVersionService.getVersion(id);
ApiVersionDetailRespVO respVO = ApiVersionConvert.INSTANCE.convertDetail(versionDO);
// 反序列化快照数据
if (versionDO.getSnapshotData() != null) {
try {
ApiDefinitionSaveReqVO snapshot = objectMapper.readValue(versionDO.getSnapshotData(), ApiDefinitionSaveReqVO.class);
respVO.setSnapshotData(snapshot);
} catch (JsonProcessingException ex) {
log.error("反序列化版本快照失败, versionId={}", id, ex);
}
}
return success(respVO);
}
@GetMapping("/page")
@Operation(summary = "分页查询 API 版本列表")
@PreAuthorize("@ss.hasPermission('databus:gateway:version:query')")
public CommonResult<PageResult<ApiVersionRespVO>> getVersionPage(@Valid ApiVersionPageReqVO pageReqVO) {
PageResult<ApiVersionDO> pageResult = apiVersionService.getVersionPage(pageReqVO);
return success(ApiVersionConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/list")
@Operation(summary = "查询指定 API 的全部版本")
@PreAuthorize("@ss.hasPermission('databus:gateway:version:query')")
public CommonResult<java.util.List<ApiVersionRespVO>> getVersionList(@RequestParam("apiId") Long apiId) {
return success(ApiVersionConvert.INSTANCE.convertList(apiVersionService.getVersionListByApiId(apiId)));
}
@PutMapping("/rollback")
@Operation(summary = "回滚到指定版本")
@PreAuthorize("@ss.hasPermission('databus:gateway:version:rollback')")
public CommonResult<Boolean> rollbackToVersion(@Valid @RequestBody ApiVersionRollbackReqVO reqVO) {
apiVersionService.rollbackToVersion(reqVO.getId(), reqVO.getRemark());
return success(true);
}
@GetMapping("/compare")
@Operation(summary = "对比两个版本差异")
@PreAuthorize("@ss.hasPermission('databus:gateway:version:query')")
public CommonResult<ApiVersionCompareRespVO> compareVersions(
@RequestParam("sourceId") Long sourceId,
@RequestParam("targetId") Long targetId) {
return success(apiVersionService.compareVersions(sourceId, targetId));
}
}

View File

@@ -1,55 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.convert;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import org.springframework.http.HttpStatus;
import java.util.List;
@Mapper
public interface ApiAccessLogConvert {
ApiAccessLogConvert INSTANCE = Mappers.getMapper(ApiAccessLogConvert.class);
@Mapping(target = "statusDesc", expression = "java(statusDesc(bean.getStatus()))")
@Mapping(target = "responseStatusText", expression = "java(resolveHttpStatusText(bean.getResponseStatus()))")
ApiAccessLogRespVO convert(ApiAccessLogDO bean);
List<ApiAccessLogRespVO> convertList(List<ApiAccessLogDO> list);
default PageResult<ApiAccessLogRespVO> convertPage(PageResult<ApiAccessLogDO> page) {
if (page == null) {
return PageResult.empty();
}
PageResult<ApiAccessLogRespVO> result = new PageResult<>();
result.setList(convertList(page.getList()));
result.setTotal(page.getTotal());
return result;
}
default String statusDesc(Integer status) {
// 将数字状态码转换为中文描述,方便前端直接展示
if (status == null) {
return "未知";
}
return switch (status) {
case 0 -> "成功";
case 1 -> "客户端错误";
case 2 -> "服务端错误";
default -> "未知";
};
}
default String resolveHttpStatusText(Integer status) {
// 统一使用 Spring 的 HttpStatus 解析出标准文案
if (status == null) {
return null;
}
HttpStatus resolved = HttpStatus.resolve(status);
return resolved != null ? resolved.getReasonPhrase() : null;
}
}

View File

@@ -1,41 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.convert;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialSimpleRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
import java.util.stream.Collectors;
@Mapper
public interface ApiClientCredentialConvert {
ApiClientCredentialConvert INSTANCE = Mappers.getMapper(ApiClientCredentialConvert.class);
ApiClientCredentialRespVO convert(ApiClientCredentialDO bean);
List<ApiClientCredentialRespVO> convertList(List<ApiClientCredentialDO> list);
default PageResult<ApiClientCredentialRespVO> convertPage(PageResult<ApiClientCredentialDO> page) {
if (page == null) {
return PageResult.empty();
}
PageResult<ApiClientCredentialRespVO> result = new PageResult<>();
result.setList(convertList(page.getList()));
result.setTotal(page.getTotal());
return result;
}
default List<ApiClientCredentialSimpleRespVO> convertSimpleList(List<ApiClientCredentialDO> list) {
return list == null ? List.of() : list.stream().map(item -> {
ApiClientCredentialSimpleRespVO vo = new ApiClientCredentialSimpleRespVO();
vo.setId(item.getId());
vo.setAppId(item.getAppId());
vo.setAppName(item.getAppName());
return vo;
}).collect(Collectors.toList());
}
}

View File

@@ -1,143 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.convert;
import cn.hutool.core.collection.CollUtil;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.*;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiTransformDO;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiCredentialBinding;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiFlowPublication;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiTransformDefinition;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Mapper
public interface ApiDefinitionConvert {
ApiDefinitionConvert INSTANCE = Mappers.getMapper(ApiDefinitionConvert.class);
ApiDefinitionSummaryRespVO convert(ApiDefinitionDO bean);
List<ApiDefinitionSummaryRespVO> convertList(List<ApiDefinitionDO> list);
default PageResult<ApiDefinitionSummaryRespVO> convertPage(PageResult<ApiDefinitionDO> page) {
if (page == null) {
return PageResult.empty();
}
PageResult<ApiDefinitionSummaryRespVO> result = new PageResult<>();
List<ApiDefinitionSummaryRespVO> list = convertList(page.getList());
result.setList(list == null ? new ArrayList<>() : list);
result.setTotal(page.getTotal());
return result;
}
default ApiDefinitionDetailRespVO convert(ApiDefinitionAggregate aggregate) {
if (aggregate == null) {
return null;
}
ApiDefinitionDetailRespVO detail = BeanUtils.toBean(aggregate.getDefinition(), ApiDefinitionDetailRespVO.class);
detail.setApiLevelTransforms(convertTransforms(aggregate.getDefinition().getId(), aggregate.getApiLevelTransforms().values()));
detail.setSteps(convertSteps(aggregate.getSteps()));
detail.setPublication(convert(aggregate.getPublication()));
detail.setCredentialBindings(convertCredentialBindings(aggregate.getCredentialBindings()));
detail.setCredentialIds(detail.getCredentialBindings().stream()
.map(ApiCredentialBindingRespVO::getCredentialId)
.filter(Objects::nonNull)
.collect(Collectors.toList()));
return detail;
}
default List<ApiDefinitionStepRespVO> convertSteps(List<ApiStepDefinition> steps) {
if (CollUtil.isEmpty(steps)) {
return new ArrayList<>();
}
return steps.stream()
.sorted(Comparator.comparing(step -> step.getStep().getStepOrder() == null ? Integer.MAX_VALUE : step.getStep().getStepOrder()))
.map(step -> {
ApiDefinitionStepRespVO resp = BeanUtils.toBean(step.getStep(), ApiDefinitionStepRespVO.class);
resp.setTransforms(convertStepTransforms(step.getStep().getApiId(), step.getStep().getId(), step.getTransforms()));
return resp;
})
.collect(Collectors.toList());
}
default List<ApiDefinitionTransformRespVO> convertTransforms(Long apiId, Collection<ApiTransformDefinition> transforms) {
if (CollUtil.isEmpty(transforms)) {
return new ArrayList<>();
}
return transforms.stream()
.sorted(Comparator.comparing(ApiTransformDefinition::getPhase, Comparator.nullsLast(String::compareTo)))
.map(transform -> {
ApiDefinitionTransformRespVO resp = BeanUtils.toBean(transform, ApiDefinitionTransformRespVO.class);
resp.setApiId(apiId);
resp.setStepId(null);
return resp;
})
.collect(Collectors.toList());
}
default List<ApiDefinitionTransformRespVO> convertStepTransforms(Long apiId, Long stepId, List<ApiTransformDefinition> transforms) {
if (CollUtil.isEmpty(transforms)) {
return new ArrayList<>();
}
return transforms.stream()
.sorted(Comparator.comparing(ApiTransformDefinition::getPhase, Comparator.nullsLast(String::compareTo)))
.map(transform -> {
ApiDefinitionTransformRespVO resp = BeanUtils.toBean(transform, ApiDefinitionTransformRespVO.class);
resp.setApiId(apiId);
resp.setStepId(stepId);
return resp;
})
.collect(Collectors.toList());
}
default ApiDefinitionPublicationRespVO convert(ApiFlowPublication publication) {
return publication == null ? null : BeanUtils.toBean(publication, ApiDefinitionPublicationRespVO.class);
}
default List<ApiCredentialBindingRespVO> convertCredentialBindings(List<ApiCredentialBinding> bindings) {
if (CollUtil.isEmpty(bindings)) {
return new ArrayList<>();
}
return bindings.stream()
.map(binding -> BeanUtils.toBean(binding, ApiCredentialBindingRespVO.class))
.collect(Collectors.toList());
}
/**
* 转换步骤列表DO -> SaveReqVO
*/
default List<ApiDefinitionStepSaveReqVO> convertStepList(List<ApiStepDO> steps) {
if (CollUtil.isEmpty(steps)) {
return new ArrayList<>();
}
return steps.stream()
.sorted(Comparator.comparing(step -> step.getStepOrder() == null ? Integer.MAX_VALUE : step.getStepOrder()))
.map(step -> BeanUtils.toBean(step, ApiDefinitionStepSaveReqVO.class))
.collect(Collectors.toList());
}
/**
* 转换变换列表DO -> SaveReqVO
*/
default List<ApiDefinitionTransformSaveReqVO> convertTransformList(List<ApiTransformDO> transforms) {
if (CollUtil.isEmpty(transforms)) {
return new ArrayList<>();
}
return transforms.stream()
.map(transform -> BeanUtils.toBean(transform, ApiDefinitionTransformSaveReqVO.class))
.collect(Collectors.toList());
}
}

View File

@@ -1,25 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.convert;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySimpleRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface ApiPolicyRateLimitConvert {
ApiPolicyRateLimitConvert INSTANCE = Mappers.getMapper(ApiPolicyRateLimitConvert.class);
ApiPolicyRespVO convert(ApiPolicyRateLimitDO bean);
List<ApiPolicyRespVO> convertList(List<ApiPolicyRateLimitDO> list);
PageResult<ApiPolicyRespVO> convertPage(PageResult<ApiPolicyRateLimitDO> page);
List<ApiPolicySimpleRespVO> convertSimpleList(List<ApiPolicyRateLimitDO> list);
}

View File

@@ -1,30 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.convert;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionDetailRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* API 版本历史 Convert。
*/
@Mapper
public interface ApiVersionConvert {
ApiVersionConvert INSTANCE = Mappers.getMapper(ApiVersionConvert.class);
ApiVersionRespVO convert(ApiVersionDO bean);
PageResult<ApiVersionRespVO> convertPage(PageResult<ApiVersionDO> page);
List<ApiVersionRespVO> convertList(List<ApiVersionDO> list);
@Mapping(target = "snapshotData", ignore = true)
ApiVersionDetailRespVO convertDetail(ApiVersionDO bean);
}

View File

@@ -1,27 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class ApiGatewayInvokeReqVO {
@Schema(description = "API 编码", requiredMode = Schema.RequiredMode.REQUIRED)
private String apiCode;
@Schema(description = "API 版本", requiredMode = Schema.RequiredMode.REQUIRED)
private String version;
@Schema(description = "请求头,可选")
private Map<String, String> headers = new HashMap<>();
@Schema(description = "请求参数,可选")
private Map<String, Object> queryParams = new HashMap<>();
@Schema(description = "请求体")
private Object payload;
}

View File

@@ -1,59 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog;
import com.zt.plat.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
/**
* Databus API 访问日志分页查询 VO。
*/
@Schema(description = "管理后台 - Databus API 访问日志分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class ApiAccessLogPageReqVO extends PageParam {
@Schema(description = "追踪 ID", example = "c8a3d52f-42c8-4b5d-9e26-8c2cc89f6bb5")
private String traceId;
@Schema(description = "API 编码", example = "user.query")
private String apiCode;
@Schema(description = "API 版本", example = "v1")
private String apiVersion;
@Schema(description = "HTTP 方法", example = "POST")
private String requestMethod;
@Schema(description = "响应 HTTP 状态", example = "200")
private Integer responseStatus;
@Schema(description = "访问状态", example = "0")
private Integer status;
@Schema(description = "客户端 IP", example = "192.168.0.10")
private String clientIp;
@Schema(description = "租户编号", example = "1")
private Long tenantId;
@Schema(description = "请求路径", example = "/gateway/api/user/query")
private String requestPath;
@Schema(description = "应用标识", example = "app-portal-01")
private String credentialAppId;
@Schema(description = "凭证主键", example = "10086")
private Long credentialId;
@Schema(description = "请求时间区间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] requestTime;
}

View File

@@ -1,104 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Databus API 访问日志 Response VO。
*/
@Schema(description = "管理后台 - Databus API 访问日志 Response VO")
@Data
public class ApiAccessLogRespVO {
@Schema(description = "日志编号", example = "1024")
private Long id;
@Schema(description = "追踪 ID", example = "c8a3d52f-42c8-4b5d-9e26-8c2cc89f6bb5")
private String traceId;
@Schema(description = "API 编码", example = "user.query")
private String apiCode;
@Schema(description = "API 描述", example = "用户查询服务")
private String apiDescription;
@Schema(description = "API 版本", example = "v1")
private String apiVersion;
@Schema(description = "HTTP 方法", example = "POST")
private String requestMethod;
@Schema(description = "请求路径", example = "/gateway/api/user/query")
private String requestPath;
@Schema(description = "应用标识", example = "app-portal-01")
private String credentialAppId;
@Schema(description = "凭证主键", example = "10086")
private Long credentialId;
@Schema(description = "查询参数(JSON)")
private String requestQuery;
@Schema(description = "请求头(JSON)")
private String requestHeaders;
@Schema(description = "请求体(JSON)")
private String requestBody;
@Schema(description = "响应 HTTP 状态", example = "200")
private Integer responseStatus;
@Schema(description = "响应 HTTP 状态说明", example = "OK")
private String responseStatusText;
@Schema(description = "响应提示", example = "OK")
private String responseMessage;
@Schema(description = "响应体(JSON)")
private String responseBody;
@Schema(description = "访问状态", example = "0")
private Integer status;
@Schema(description = "访问状态展示文案", example = "成功")
private String statusDesc;
@Schema(description = "错误码", example = "DAT-001")
private String errorCode;
@Schema(description = "错误信息", example = "API 调用失败")
private String errorMessage;
@Schema(description = "异常堆栈")
private String exceptionStack;
@Schema(description = "客户端 IP", example = "192.168.0.10")
private String clientIp;
@Schema(description = "User-Agent")
private String userAgent;
@Schema(description = "租户编号", example = "1")
private Long tenantId;
@Schema(description = "请求耗时(毫秒)", example = "123")
private Long duration;
@Schema(description = "请求时间")
private LocalDateTime requestTime;
@Schema(description = "响应时间")
private LocalDateTime responseTime;
@Schema(description = "执行步骤(JSON)")
private String stepResults;
@Schema(description = "额外调试信息(JSON)")
private String extra;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}

View File

@@ -1,19 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.credential;
import com.zt.plat.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - API 客户端凭证分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiClientCredentialPageReqVO extends PageParam {
@Schema(description = "关键字,匹配 appId 或名称", example = "databus-app")
private String keyword;
@Schema(description = "是否启用")
private Boolean enabled;
}

View File

@@ -1,54 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.credential;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - API 客户端凭证 Response VO")
@Data
public class ApiClientCredentialRespVO {
@Schema(description = "记录编号", example = "1024")
private Long id;
@Schema(description = "应用标识", example = "databus-app")
private String appId;
@Schema(description = "应用名称", example = "数据总线默认应用")
private String appName;
@Schema(description = "加密密钥", example = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")
private String encryptionKey;
@Schema(description = "加密算法", example = "AES")
private String encryptionType;
@Schema(description = "签名算法", example = "MD5")
private String signatureType;
@Schema(description = "是否启用", example = "true")
private Boolean enabled;
@Schema(description = "备注", example = "默认应用凭证")
private String remark;
@Schema(description = "允许匿名访问", example = "false")
private Boolean allowAnonymous;
@Schema(description = "匿名访问固定用户 ID", example = "1024")
private Long anonymousUserId;
@Schema(description = "匿名访问固定用户昵称", example = "张三")
private String anonymousUserNickname;
@Schema(description = "是否启用加密", example = "true")
private Boolean enableEncryption;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "最后更新时间")
private LocalDateTime updateTime;
}

View File

@@ -1,52 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.credential;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - API 客户端凭证保存 Request VO")
@Data
public class ApiClientCredentialSaveReqVO {
@Schema(description = "记录编号,仅更新时必填", example = "1024")
private Long id;
@Schema(description = "应用标识", example = "databus-app")
@NotBlank(message = "应用标识不能为空")
private String appId;
@Schema(description = "应用名称", example = "数据总线默认应用")
private String appName;
@Schema(description = "加密密钥", example = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")
@NotBlank(message = "加密密钥不能为空")
private String encryptionKey;
@Schema(description = "加密算法", example = "AES")
@NotBlank(message = "加密算法不能为空")
private String encryptionType;
@Schema(description = "签名算法", example = "MD5")
@NotBlank(message = "签名算法不能为空")
private String signatureType;
@Schema(description = "是否启用", example = "true")
@NotNull(message = "启用状态不能为空")
private Boolean enabled;
@Schema(description = "备注", example = "默认应用凭证")
private String remark;
@Schema(description = "允许匿名访问", example = "false")
@NotNull(message = "匿名访问标识不能为空")
private Boolean allowAnonymous;
@Schema(description = "匿名访问固定用户 ID", example = "1024")
private Long anonymousUserId;
@Schema(description = "是否启用加密", example = "true")
@NotNull(message = "启用加密标识不能为空")
private Boolean enableEncryption;
}

View File

@@ -1,19 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.credential;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - API 客户端凭证精简 Response VO")
@Data
public class ApiClientCredentialSimpleRespVO {
@Schema(description = "记录编号", example = "1024")
private Long id;
@Schema(description = "应用标识", example = "databus-app")
private String appId;
@Schema(description = "应用名称", example = "数据总线默认应用")
private String appName;
}

View File

@@ -1,17 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class ApiCredentialBindingRespVO {
@Schema(description = "凭证主键", example = "10086")
private Long credentialId;
@Schema(description = "应用标识", example = "app-portal-01")
private String appId;
@Schema(description = "应用名称")
private String appName;
}

View File

@@ -1,68 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "管理后台 - API 定义详情 Response VO")
public class ApiDefinitionDetailRespVO {
@Schema(description = "主键", example = "1024")
private Long id;
@Schema(description = "租户标识", example = "1")
private String tenantId;
@Schema(description = "API 编码", example = "order.create")
private String apiCode;
@Schema(description = "API 版本", example = "v1")
private String version;
@Schema(description = "HTTP 方法", example = "POST")
private String httpMethod;
@Schema(description = "状态", example = "1")
private Integer status;
@Schema(description = "描述")
private String description;
@Schema(description = "限流策略编号")
private Long rateLimitId;
@Schema(description = "响应模板(JSON)")
private String responseTemplate;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "创建人")
private String creator;
@Schema(description = "更新人")
private String updater;
@Schema(description = "API 级别变换列表")
private List<ApiDefinitionTransformRespVO> apiLevelTransforms = new ArrayList<>();
@Schema(description = "授权凭证 ID 列表")
private List<Long> credentialIds = new ArrayList<>();
@Schema(description = "授权凭证详情列表")
private List<ApiCredentialBindingRespVO> credentialBindings = new ArrayList<>();
@Schema(description = "步骤列表")
private List<ApiDefinitionStepRespVO> steps = new ArrayList<>();
@Schema(description = "发布信息")
private ApiDefinitionPublicationRespVO publication;
}

View File

@@ -1,22 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import com.zt.plat.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - API 定义分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiDefinitionPageReqVO extends PageParam {
@Schema(description = "关键字,匹配编码或描述", example = "order")
private String keyword;
@Schema(description = "API 状态", example = "1")
private Integer status;
@Schema(description = "HTTP 方法", example = "POST")
private String httpMethod;
}

View File

@@ -1,28 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "管理后台 - API 发布信息 Response VO")
public class ApiDefinitionPublicationRespVO {
@Schema(description = "发布记录主键", example = "4001")
private Long id;
@Schema(description = "发布标签", example = "release-20231001")
private String releaseTag;
@Schema(description = "快照内容(JSON)")
private String snapshot;
@Schema(description = "状态", example = "RELEASED")
private String status;
@Schema(description = "是否当前生效")
private Boolean active;
@Schema(description = "描述")
private String description;
}

View File

@@ -1,57 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "管理后台 - API 定义保存 Request VO")
public class ApiDefinitionSaveReqVO {
@Schema(description = "主键", example = "1001")
private Long id;
@Schema(description = "API 编码", example = "order.create")
@NotBlank(message = "API 编码不能为空")
private String apiCode;
@Schema(description = "API 版本", example = "v1")
@NotBlank(message = "API 版本不能为空")
private String version;
@Schema(description = "HTTP 方法", example = "POST")
@NotBlank(message = "HTTP 方法不能为空")
private String httpMethod;
@Schema(description = "API 状态", example = "1")
@NotNull(message = "API 状态不能为空")
private Integer status;
@Schema(description = "描述")
private String description;
@Schema(description = "限流策略编号")
private Long rateLimitId;
@Schema(description = "响应模板(JSON)")
private String responseTemplate;
@Schema(description = "API 级别变换列表")
@Valid
private List<ApiDefinitionTransformSaveReqVO> apiLevelTransforms = new ArrayList<>();
@Schema(description = "授权的客户端凭证 ID 列表")
private List<Long> credentialIds = new ArrayList<>();
@Schema(description = "步骤列表")
@NotEmpty(message = "编排步骤不能为空")
@Valid
private List<ApiDefinitionStepSaveReqVO> steps = new ArrayList<>();
}

View File

@@ -1,52 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "管理后台 - API 编排步骤详情 Response VO")
public class ApiDefinitionStepRespVO {
@Schema(description = "步骤主键", example = "21001")
private Long id;
@Schema(description = "所属 API 主键", example = "1024")
private Long apiId;
@Schema(description = "步骤序号", example = "1")
private Integer stepOrder;
@Schema(description = "并行分组")
private String parallelGroup;
@Schema(description = "步骤类型", example = "HTTP")
private String type;
@Schema(description = "目标端点")
private String targetEndpoint;
@Schema(description = "请求映射表达式(JSON)")
private String requestMappingExpr;
@Schema(description = "响应映射表达式(JSON)")
private String responseMappingExpr;
@Schema(description = "超时时间(毫秒)")
private Long timeout;
@Schema(description = "降级策略(JSON)")
private String fallbackStrategy;
@Schema(description = "条件表达式")
private String conditionExpr;
@Schema(description = "是否出错终止")
private Boolean stopOnError;
@Schema(description = "步骤级变换列表")
private List<ApiDefinitionTransformRespVO> transforms = new ArrayList<>();
}

View File

@@ -1,55 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Schema(description = "管理后台 - API 编排步骤保存 Request VO")
public class ApiDefinitionStepSaveReqVO {
@Schema(description = "步骤主键", example = "21001")
private Long id;
@Schema(description = "步骤序号", example = "1")
@NotNull(message = "步骤序号不能为空")
private Integer stepOrder;
@Schema(description = "并行分组")
private String parallelGroup;
@Schema(description = "步骤类型", example = "HTTP")
@NotBlank(message = "步骤类型不能为空")
private String type;
@Schema(description = "目标端点", example = "https://api.demo.com/order")
private String targetEndpoint;
@Schema(description = "请求映射表达式(JSON)")
private String requestMappingExpr;
@Schema(description = "响应映射表达式(JSON)")
private String responseMappingExpr;
@Schema(description = "超时时间(毫秒),缺省 20000(20s)", example = "20000")
private Long timeout;
@Schema(description = "降级策略(JSON)")
private String fallbackStrategy;
@Schema(description = "条件表达式")
private String conditionExpr;
@Schema(description = "是否出错终止")
private Boolean stopOnError;
@Schema(description = "步骤级变换列表")
@Valid
private List<ApiDefinitionTransformSaveReqVO> transforms = new ArrayList<>();
}

View File

@@ -1,42 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "管理后台 - API 定义分页列表 Response VO")
public class ApiDefinitionSummaryRespVO {
@Schema(description = "主键", example = "1024")
private Long id;
@Schema(description = "API 编码", example = "order.create")
private String apiCode;
@Schema(description = "API 版本", example = "v1")
private String version;
@Schema(description = "HTTP 方法", example = "POST")
private String httpMethod;
@Schema(description = "状态", example = "1")
private Integer status;
@Schema(description = "描述")
private String description;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "创建人")
private String creator;
@Schema(description = "更新人")
private String updater;
}

View File

@@ -1,31 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "管理后台 - API 变换详情 Response VO")
public class ApiDefinitionTransformRespVO {
@Schema(description = "变换主键", example = "31001")
private Long id;
@Schema(description = "所属 API 主键", example = "1024")
private Long apiId;
@Schema(description = "所属步骤主键", example = "21001")
private Long stepId;
@Schema(description = "阶段", example = "REQUEST")
private String phase;
@Schema(description = "表达式类型", example = "SPEL")
private String expressionType;
@Schema(description = "表达式内容", example = "#{payload}")
private String expression;
@Schema(description = "描述")
private String description;
}

View File

@@ -1,29 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.definition;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "管理后台 - API 变换保存 Request VO")
public class ApiDefinitionTransformSaveReqVO {
@Schema(description = "变换主键", example = "31001")
private Long id;
@Schema(description = "阶段", example = "REQUEST")
@NotBlank(message = "变换阶段不能为空")
private String phase;
@Schema(description = "表达式类型", example = "SPEL")
@NotBlank(message = "表达式类型不能为空")
private String expressionType;
@Schema(description = "表达式内容", example = "#{payload}")
@NotBlank(message = "表达式内容不能为空")
private String expression;
@Schema(description = "描述")
private String description;
}

View File

@@ -1,27 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.policy;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* Base VO for policy definitions shared by request/response objects.
*/
@Data
public class ApiPolicyBaseVO {
@Schema(description = "策略名称", example = "JWT")
@NotBlank(message = "策略名称不能为空")
private String name;
@Schema(description = "策略类型", example = "JWT")
@NotBlank(message = "策略类型不能为空")
private String type;
@Schema(description = "策略配置(JSON)", example = "{\"issuer\":\"iam\"}")
private String config;
@Schema(description = "策略描述", example = "JWT 认证策略")
private String description;
}

View File

@@ -1,22 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.policy;
import com.zt.plat.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Policy search conditions with pagination.
*/
@Schema(description = "管理后台 - 策略分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiPolicyPageReqVO extends PageParam {
@Schema(description = "关键字(名称/描述)", example = "JWT")
private String keyword;
@Schema(description = "策略类型", example = "JWT")
private String type;
}

View File

@@ -1,32 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.policy;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* Policy detail response VO.
*/
@Schema(description = "管理后台 - 策略详情 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiPolicyRespVO extends ApiPolicyBaseVO {
@Schema(description = "策略编号", example = "1024")
private Long id;
@Schema(description = "创建人", example = "admin")
private String creator;
@Schema(description = "修改人", example = "admin")
private String updater;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "最后更新时间")
private LocalDateTime updateTime;
}

View File

@@ -1,18 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.policy;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Policy create/update request VO.
*/
@Schema(description = "管理后台 - 策略保存 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiPolicySaveReqVO extends ApiPolicyBaseVO {
@Schema(description = "策略编号", example = "1024")
private Long id;
}

View File

@@ -1,25 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.policy;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Policy simple response VO used by dropdowns.
*/
@Schema(description = "管理后台 - 策略精简 Response VO")
@Data
public class ApiPolicySimpleRespVO {
@Schema(description = "策略编号", example = "1024")
private Long id;
@Schema(description = "策略名称", example = "JWT")
private String name;
@Schema(description = "策略类型", example = "JWT")
private String type;
@Schema(description = "策略描述")
private String description;
}

View File

@@ -1,72 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.version;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* API 版本对比 Response VO。
*/
@Schema(description = "管理后台 - API 版本对比 Response VO")
@Data
public class ApiVersionCompareRespVO {
@Schema(description = "源版本 ID", example = "1001")
private Long sourceVersionId;
@Schema(description = "源版本号", example = "2")
private Integer sourceVersionNumber;
@Schema(description = "源版本描述")
private String sourceDescription;
@Schema(description = "源版本操作人")
private String sourceOperator;
@Schema(description = "源版本创建时间")
private LocalDateTime sourceCreateTime;
@Schema(description = "目标版本 ID", example = "1002")
private Long targetVersionId;
@Schema(description = "目标版本号", example = "3")
private Integer targetVersionNumber;
@Schema(description = "目标版本描述")
private String targetDescription;
@Schema(description = "目标版本操作人")
private String targetOperator;
@Schema(description = "目标版本创建时间")
private LocalDateTime targetCreateTime;
@Schema(description = "源版本快照")
private ApiDefinitionSaveReqVO sourceSnapshot;
@Schema(description = "目标版本快照")
private ApiDefinitionSaveReqVO targetSnapshot;
@Schema(description = "两者是否完全一致")
private Boolean same;
@Schema(description = "字段差异列表")
private List<FieldDiff> differences;
@Data
public static class FieldDiff {
@Schema(description = "差异字段路径", example = "/steps[0]/targetEndpoint")
private String path;
@Schema(description = "源版本值")
private String sourceValue;
@Schema(description = "目标版本值")
private String targetValue;
}
}

View File

@@ -1,26 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.version;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* API 版本历史创建 Request VO。
*/
@Schema(description = "管理后台 - API 版本历史创建 Request VO")
@Data
public class ApiVersionCreateReqVO {
@Schema(description = "API 定义 ID", required = true, example = "1024")
@NotNull(message = "API 定义 ID 不能为空")
private Long apiId;
@Schema(description = "版本号", required = true, example = "v1.0.0")
@NotBlank(message = "版本号不能为空")
private String versionNumber;
@Schema(description = "版本描述", example = "初始版本")
private String description;
}

View File

@@ -1,47 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.version;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* API 版本详情 Response VO。
* 包含完整的 API 定义快照数据。
*/
@Schema(description = "管理后台 - API 版本详情 Response VO")
@Data
public class ApiVersionDetailRespVO {
@Schema(description = "主键", example = "1024")
private Long id;
@Schema(description = "API 定义 ID", example = "1001")
private Long apiId;
@Schema(description = "版本号", example = "1")
private Integer versionNumber;
@Schema(description = "版本描述", example = "初始版本")
private String description;
@Schema(description = "是否为当前版本", example = "true")
private Boolean isCurrent;
@Schema(description = "操作人", example = "admin")
private String operator;
@Schema(description = "API 定义快照数据")
private ApiDefinitionSaveReqVO snapshotData;
@Schema(description = "创建者", example = "admin")
private String creator;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "租户编号", example = "1")
private Long tenantId;
}

View File

@@ -1,33 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.version;
import com.zt.plat.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
/**
* API 版本历史分页查询 VO。
*/
@Schema(description = "管理后台 - API 版本历史分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class ApiVersionPageReqVO extends PageParam {
@Schema(description = "API 定义 ID", required = true, example = "1024")
private Long apiId;
@Schema(description = "版本号", example = "1")
private Integer versionNumber;
@Schema(description = "创建时间区间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@@ -1,42 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.version;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* API 版本历史 Response VO。
*/
@Schema(description = "管理后台 - API 版本历史 Response VO")
@Data
public class ApiVersionRespVO {
@Schema(description = "主键", example = "1024")
private Long id;
@Schema(description = "API 定义 ID", example = "1001")
private Long apiId;
@Schema(description = "版本号", example = "1")
private Integer versionNumber;
@Schema(description = "版本描述", example = "初始版本")
private String description;
@Schema(description = "是否为当前版本", example = "true")
private Boolean isCurrent;
@Schema(description = "操作人", example = "admin")
private String operator;
@Schema(description = "创建者", example = "admin")
private String creator;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "租户编号", example = "1")
private Long tenantId;
}

View File

@@ -1,21 +0,0 @@
package com.zt.plat.module.databus.controller.admin.gateway.vo.version;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* API 版本回滚 Request VO。
*/
@Schema(description = "管理后台 - API 版本回滚 Request VO")
@Data
public class ApiVersionRollbackReqVO {
@Schema(description = "待回滚的版本 ID", required = true, example = "1024")
@NotNull(message = "版本 ID 不能为空")
private Long id;
@Schema(description = "回滚备注", example = "回滚到版本 v5")
private String remark;
}

View File

@@ -1,150 +0,0 @@
package com.zt.plat.module.databus.dal.dataobject.gateway;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* Databus API 访问日志数据对象。
*
* <p>用于记录 API 编排网关的请求与响应详情,便于审计与问题排查。</p>
*/
@TableName("databus_api_access_log")
@KeySequence("databus_api_access_log_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiAccessLogDO extends TenantBaseDO {
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 请求追踪标识,对应 {@link com.zt.plat.framework.common.util.monitor.TracerUtils#getTraceId()}
*/
private String traceId;
/**
* API 编码
*/
private String apiCode;
/**
* API 版本
*/
private String apiVersion;
/**
* HTTP 方法
*/
private String requestMethod;
/**
* 请求路径
*/
private String requestPath;
/**
* 调用使用的应用标识
*/
private String credentialAppId;
/**
* 调用使用的凭证主键
*/
private Long credentialId;
/**
* 查询参数JSON 字符串)
*/
private String requestQuery;
/**
* 请求头信息JSON 字符串)
*/
private String requestHeaders;
/**
* 请求体JSON 字符串)
*/
private String requestBody;
/**
* 响应 HTTP 状态码
*/
private Integer responseStatus;
/**
* 响应提示信息
*/
private String responseMessage;
/**
* 响应体JSON 字符串)
*/
private String responseBody;
/**
* 访问状态0-成功 1-客户端错误 2-服务端错误 3-未知
*/
private Integer status;
/**
* 业务错误码
*/
private String errorCode;
/**
* 错误信息
*/
private String errorMessage;
/**
* 异常堆栈
*/
private String exceptionStack;
/**
* 客户端 IP
*/
private String clientIp;
/**
* User-Agent
*/
private String userAgent;
/**
* 请求耗时(毫秒)
*/
private Long duration;
/**
* 请求时间
*/
private LocalDateTime requestTime;
/**
* 响应时间
*/
private LocalDateTime responseTime;
/**
* 执行步骤结果JSON 字符串)
*/
private String stepResults;
/**
* 额外调试信息JSON 字符串)
*/
private String extra;
}

View File

@@ -1,43 +0,0 @@
package com.zt.plat.module.databus.dal.dataobject.gateway;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* API 客户端凭证,用于维护 appId 与加解密配置的关联关系。
*/
@Data
@TableName("databus_api_client_credential")
@KeySequence("databus_api_client_credential_seq")
@EqualsAndHashCode(callSuper = true)
public class ApiClientCredentialDO extends BaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String appId;
private String appName;
private String encryptionKey;
private String encryptionType;
private String signatureType;
private Boolean enabled;
private String remark;
private Boolean allowAnonymous;
private Long anonymousUserId;
private Boolean enableEncryption;
}

View File

@@ -1,37 +0,0 @@
package com.zt.plat.module.databus.dal.dataobject.gateway;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* API 与客户端凭证的授权关联。
*/
@Data
@TableName("databus_api_definition_credential")
@KeySequence("databus_api_definition_credential_seq")
@EqualsAndHashCode(callSuper = true)
public class ApiDefinitionCredentialDO extends BaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* API 主键
*/
private Long apiId;
/**
* 客户端凭证主键
*/
private Long credentialId;
/**
* 绑定时的应用标识冗余,便于快速校验
*/
private String appId;
}

View File

@@ -1,44 +0,0 @@
package com.zt.plat.module.databus.dal.dataobject.gateway;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* API definition data object describing external API metadata and policies.
*/
@TableName("databus_api_definition")
@KeySequence("databus_api_definition_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiDefinitionDO extends TenantBaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String apiCode;
private String httpMethod;
private String version;
/**
* API status, see {@code ApiPublishStatusEnum}.
*/
private Integer status;
private String description;
private Long rateLimitId;
private String responseTemplate;
private LocalDateTime updatedAt;
}

View File

@@ -1,35 +0,0 @@
package com.zt.plat.module.databus.dal.dataobject.gateway;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Publication record for API flow snapshots and gray releases.
*/
@TableName("databus_api_flow_publish")
@KeySequence("databus_api_flow_publish_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiFlowPublishDO extends TenantBaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long apiId;
private String releaseTag;
private String snapshot;
private String status;
private Boolean active;
private String description;
}

View File

@@ -1,31 +0,0 @@
package com.zt.plat.module.databus.dal.dataobject.gateway;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Rate limit policy definition stored in database.
*/
@TableName("databus_policy_rate_limit")
@KeySequence("databus_policy_rate_limit_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiPolicyRateLimitDO extends TenantBaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private String type;
private String config;
private String description;
}

View File

@@ -1,47 +0,0 @@
package com.zt.plat.module.databus.dal.dataobject.gateway;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* API orchestration step definition.
*/
@TableName("databus_api_step")
@KeySequence("databus_api_step_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiStepDO extends TenantBaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long apiId;
private Integer stepOrder;
private String parallelGroup;
private String type;
private String targetEndpoint;
private String requestMappingExpr;
private String responseMappingExpr;
private Long transformId;
private Long timeout;
private String fallbackStrategy;
private String conditionExpr;
private Boolean stopOnError;
}

View File

@@ -1,35 +0,0 @@
package com.zt.plat.module.databus.dal.dataobject.gateway;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* API request/response transformation expressions.
*/
@TableName("databus_api_transform")
@KeySequence("databus_api_transform_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiTransformDO extends TenantBaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long apiId;
private Long stepId;
private String phase;
private String expressionType;
private String expression;
private String description;
}

View File

@@ -1,59 +0,0 @@
package com.zt.plat.module.databus.dal.dataobject.gateway;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* API 版本历史数据对象
*
* <p>每次修改 API 配置时自动创建新版本记录,支持完整的版本历史追溯和回滚。</p>
* <p>版本号自动递增,不可删除,保留完整的历史记录链。</p>
*/
@TableName("databus_api_version")
@KeySequence("databus_api_version_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiVersionDO extends TenantBaseDO {
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* API 定义 ID
*/
private Long apiId;
/**
* 版本号(自动递增,从 1 开始)
*/
private Integer versionNumber;
/**
* API 完整定义快照JSON 格式,包含 definition、steps、transforms 等)
*/
private String snapshotData;
/**
* 版本描述/变更说明
*/
private String description;
/**
* 是否为当前版本(最新使用的版本)
*/
private Boolean isCurrent;
/**
* 操作人
*/
private String operator;
}

View File

@@ -1,78 +0,0 @@
package com.zt.plat.module.databus.dal.mapper.gateway;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionPageReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* API 版本历史 Mapper
*/
@Mapper
public interface ApiVersionMapper extends BaseMapperX<ApiVersionDO> {
/**
* 分页查询版本历史
*/
default PageResult<ApiVersionDO> selectPage(ApiVersionPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<ApiVersionDO>()
.eqIfPresent(ApiVersionDO::getApiId, reqVO.getApiId())
.eqIfPresent(ApiVersionDO::getVersionNumber, reqVO.getVersionNumber())
.betweenIfPresent(ApiVersionDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(ApiVersionDO::getVersionNumber));
}
/**
* 查询指定 API 的所有版本历史
*/
default List<ApiVersionDO> selectListByApiId(Long apiId) {
return selectList(new LambdaQueryWrapperX<ApiVersionDO>()
.eq(ApiVersionDO::getApiId, apiId)
.orderByDesc(ApiVersionDO::getVersionNumber));
}
/**
* 查询指定 API 的当前版本
*/
default ApiVersionDO selectCurrentByApiId(Long apiId) {
return selectOne(new LambdaQueryWrapperX<ApiVersionDO>()
.eq(ApiVersionDO::getApiId, apiId)
.eq(ApiVersionDO::getIsCurrent, true));
}
/**
* 查询指定 API 的最大版本号
*/
default Integer selectMaxVersionNumber(Long apiId) {
ApiVersionDO maxVersion = selectOne(new LambdaQueryWrapperX<ApiVersionDO>()
.eq(ApiVersionDO::getApiId, apiId)
.orderByDesc(ApiVersionDO::getVersionNumber)
.last("LIMIT 1"));
return maxVersion != null ? maxVersion.getVersionNumber() : 0;
}
/**
* 将所有版本标记为非当前版本
*/
default void markAllAsNotCurrent(Long apiId) {
UpdateWrapper<ApiVersionDO> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("api_id", apiId)
.set("is_current", false);
update(null, updateWrapper);
}
/**
* 查询指定版本
*/
default ApiVersionDO selectByApiIdAndVersionNumber(Long apiId, Integer versionNumber) {
return selectOne(new LambdaQueryWrapperX<ApiVersionDO>()
.eq(ApiVersionDO::getApiId, apiId)
.eq(ApiVersionDO::getVersionNumber, versionNumber));
}
}

View File

@@ -1,33 +0,0 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import cn.hutool.core.util.ArrayUtil;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogPageReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ApiAccessLogMapper extends BaseMapperX<ApiAccessLogDO> {
default PageResult<ApiAccessLogDO> selectPage(ApiAccessLogPageReqVO reqVO) {
LambdaQueryWrapperX<ApiAccessLogDO> query = new LambdaQueryWrapperX<ApiAccessLogDO>()
.likeIfPresent(ApiAccessLogDO::getTraceId, reqVO.getTraceId())
.eqIfPresent(ApiAccessLogDO::getApiCode, reqVO.getApiCode())
.eqIfPresent(ApiAccessLogDO::getApiVersion, reqVO.getApiVersion())
.eqIfPresent(ApiAccessLogDO::getRequestMethod, reqVO.getRequestMethod())
.eqIfPresent(ApiAccessLogDO::getResponseStatus, reqVO.getResponseStatus())
.eqIfPresent(ApiAccessLogDO::getStatus, reqVO.getStatus())
.likeIfPresent(ApiAccessLogDO::getClientIp, reqVO.getClientIp())
.eqIfPresent(ApiAccessLogDO::getCredentialAppId, reqVO.getCredentialAppId())
.eqIfPresent(ApiAccessLogDO::getCredentialId, reqVO.getCredentialId())
.eqIfPresent(ApiAccessLogDO::getTenantId, reqVO.getTenantId())
.likeIfPresent(ApiAccessLogDO::getRequestPath, reqVO.getRequestPath());
if (ArrayUtil.isNotEmpty(reqVO.getRequestTime()) && reqVO.getRequestTime().length == 2) {
query.between(ApiAccessLogDO::getRequestTime, reqVO.getRequestTime()[0], reqVO.getRequestTime()[1]);
}
return selectPage(reqVO, query.orderByDesc(ApiAccessLogDO::getRequestTime)
.orderByDesc(ApiAccessLogDO::getId));
}
}

View File

@@ -1,47 +0,0 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.databus.controller.admin.gateway.vo.credential.ApiClientCredentialPageReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.Optional;
@Mapper
public interface ApiClientCredentialMapper extends BaseMapperX<ApiClientCredentialDO> {
default Optional<ApiClientCredentialDO> selectByAppId(String appId) {
if (StrUtil.isBlank(appId)) {
return Optional.empty();
}
LambdaQueryWrapperX<ApiClientCredentialDO> query = new LambdaQueryWrapperX<>();
query.eq(ApiClientCredentialDO::getAppId, appId)
.eq(ApiClientCredentialDO::getDeleted, false);
return Optional.ofNullable(selectOne(query));
}
default PageResult<ApiClientCredentialDO> selectPage(ApiClientCredentialPageReqVO reqVO) {
LambdaQueryWrapperX<ApiClientCredentialDO> query = new LambdaQueryWrapperX<>();
if (StrUtil.isNotBlank(reqVO.getKeyword())) {
String keyword = reqVO.getKeyword();
query.and(wrapper -> wrapper.like(ApiClientCredentialDO::getAppId, keyword)
.or().like(ApiClientCredentialDO::getAppName, keyword));
}
query.eqIfPresent(ApiClientCredentialDO::getEnabled, reqVO.getEnabled())
.eq(ApiClientCredentialDO::getDeleted, false)
.orderByDesc(ApiClientCredentialDO::getUpdateTime)
.orderByDesc(ApiClientCredentialDO::getId);
return selectPage(reqVO, query);
}
default List<ApiClientCredentialDO> selectEnabledList() {
return selectList(new LambdaQueryWrapperX<ApiClientCredentialDO>()
.eq(ApiClientCredentialDO::getEnabled, true)
.eq(ApiClientCredentialDO::getDeleted, false)
.orderByAsc(ApiClientCredentialDO::getAppId));
}
}

View File

@@ -1,32 +0,0 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionCredentialDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiDefinitionCredentialMapper extends BaseMapperX<ApiDefinitionCredentialDO> {
default List<ApiDefinitionCredentialDO> selectByApiId(Long apiId) {
return selectList(new LambdaQueryWrapperX<ApiDefinitionCredentialDO>()
.eq(ApiDefinitionCredentialDO::getApiId, apiId));
}
default void deleteByApiId(Long apiId) {
delete(new LambdaQueryWrapperX<ApiDefinitionCredentialDO>()
.eq(ApiDefinitionCredentialDO::getApiId, apiId));
}
/**
* 按 API 逻辑删除已有绑定,保留操作记录。
*/
default void logicDeleteByApiId(Long apiId) {
ApiDefinitionCredentialDO entity = new ApiDefinitionCredentialDO();
entity.setDeleted(Boolean.TRUE);
update(entity, new LambdaQueryWrapperX<ApiDefinitionCredentialDO>()
.eq(ApiDefinitionCredentialDO::getApiId, apiId));
}
}

View File

@@ -1,52 +0,0 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionPageReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.Optional;
@Mapper
public interface ApiDefinitionMapper extends BaseMapperX<ApiDefinitionDO> {
default Optional<ApiDefinitionDO> selectByCodeAndVersion(String apiCode, String version) {
return Optional.ofNullable(selectOne(ApiDefinitionDO::getApiCode, apiCode,
ApiDefinitionDO::getVersion, version,
ApiDefinitionDO::getDeleted, false));
}
default List<ApiDefinitionDO> selectActiveDefinitions(List<Integer> statusList) {
return selectList(new LambdaQueryWrapperX<ApiDefinitionDO>()
.inIfPresent(ApiDefinitionDO::getStatus, statusList)
.eq(ApiDefinitionDO::getDeleted, false));
}
default PageResult<ApiDefinitionDO> selectPage(ApiDefinitionPageReqVO reqVO) {
LambdaQueryWrapperX<ApiDefinitionDO> query = new LambdaQueryWrapperX<>();
if (StrUtil.isNotBlank(reqVO.getKeyword())) {
String keyword = reqVO.getKeyword();
query.and(wrapper -> wrapper.like(ApiDefinitionDO::getApiCode, keyword)
.or().like(ApiDefinitionDO::getDescription, keyword));
}
query.eqIfPresent(ApiDefinitionDO::getStatus, reqVO.getStatus())
.eqIfPresent(ApiDefinitionDO::getHttpMethod, reqVO.getHttpMethod())
.orderByDesc(ApiDefinitionDO::getUpdateTime)
.orderByDesc(ApiDefinitionDO::getId);
return selectPage(reqVO, query);
}
default Long selectCountByRateLimitPolicyId(Long policyId) {
if (policyId == null) {
return 0L;
}
return selectCount(new LambdaQueryWrapperX<ApiDefinitionDO>()
.eq(ApiDefinitionDO::getRateLimitId, policyId)
.eq(ApiDefinitionDO::getDeleted, false));
}
}

View File

@@ -1,19 +0,0 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiFlowPublishDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Optional;
@Mapper
public interface ApiFlowPublishMapper extends BaseMapperX<ApiFlowPublishDO> {
default Optional<ApiFlowPublishDO> selectActiveByApiId(Long apiId) {
return Optional.ofNullable(selectOne(new LambdaQueryWrapperX<ApiFlowPublishDO>()
.eq(ApiFlowPublishDO::getApiId, apiId)
.eq(ApiFlowPublishDO::getActive, true)));
}
}

View File

@@ -1,36 +0,0 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyPageReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiPolicyRateLimitMapper extends BaseMapperX<ApiPolicyRateLimitDO> {
default PageResult<ApiPolicyRateLimitDO> selectPage(ApiPolicyPageReqVO reqVO) {
LambdaQueryWrapperX<ApiPolicyRateLimitDO> query = new LambdaQueryWrapperX<>();
if (StrUtil.isNotBlank(reqVO.getKeyword())) {
String keyword = reqVO.getKeyword();
query.and(wrapper -> wrapper.like(ApiPolicyRateLimitDO::getName, keyword)
.or().like(ApiPolicyRateLimitDO::getDescription, keyword));
}
query.eqIfPresent(ApiPolicyRateLimitDO::getType, reqVO.getType())
.eq(ApiPolicyRateLimitDO::getDeleted, false)
.orderByDesc(ApiPolicyRateLimitDO::getUpdateTime)
.orderByDesc(ApiPolicyRateLimitDO::getId);
return selectPage(reqVO, query);
}
default List<ApiPolicyRateLimitDO> selectSimpleList() {
return selectList(new LambdaQueryWrapperX<ApiPolicyRateLimitDO>()
.eq(ApiPolicyRateLimitDO::getDeleted, false)
.orderByDesc(ApiPolicyRateLimitDO::getUpdateTime)
.orderByDesc(ApiPolicyRateLimitDO::getId));
}
}

View File

@@ -1,25 +0,0 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiStepMapper extends BaseMapperX<ApiStepDO> {
default List<ApiStepDO> selectByApiId(Long apiId) {
return selectList(new LambdaQueryWrapperX<ApiStepDO>()
.eq(ApiStepDO::getApiId, apiId)
.orderByAsc(ApiStepDO::getParallelGroup)
.orderByAsc(ApiStepDO::getStepOrder));
}
default void deleteByApiId(Long apiId) {
delete(new LambdaQueryWrapperX<ApiStepDO>()
.eq(ApiStepDO::getApiId, apiId));
}
}

View File

@@ -1,39 +0,0 @@
package com.zt.plat.module.databus.dal.mysql.gateway;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiTransformDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiTransformMapper extends BaseMapperX<ApiTransformDO> {
default List<ApiTransformDO> selectByApiId(Long apiId) {
return selectList(new LambdaQueryWrapperX<ApiTransformDO>()
.eq(ApiTransformDO::getApiId, apiId));
}
default List<ApiTransformDO> selectByStepId(Long stepId) {
return selectList(new LambdaQueryWrapperX<ApiTransformDO>()
.eq(ApiTransformDO::getStepId, stepId));
}
default List<ApiTransformDO> selectApiLevelTransforms(Long apiId) {
return selectList(new LambdaQueryWrapperX<ApiTransformDO>()
.eq(ApiTransformDO::getApiId, apiId)
.isNull(ApiTransformDO::getStepId));
}
default void deleteByApiId(Long apiId) {
delete(new LambdaQueryWrapperX<ApiTransformDO>()
.eq(ApiTransformDO::getApiId, apiId));
}
default void deleteByStepId(Long stepId) {
delete(new LambdaQueryWrapperX<ApiTransformDO>()
.eq(ApiTransformDO::getStepId, stepId));
}
}

View File

@@ -1,46 +0,0 @@
package com.zt.plat.module.databus.enums.gateway;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* External API publish status enumeration.
*/
@AllArgsConstructor
@Getter
public enum ApiStatusEnum {
DRAFT(0, "草稿"),
ONLINE(1, "已上线"),
OFFLINE(2, "已下线"),
DEPRECATED(3, "已废弃");
private final int status;
private final String label;
public static boolean isOnline(Integer status) {
return status != null && status == ONLINE.status;
}
public static boolean isDeprecated(Integer status) {
return status != null && status == DEPRECATED.status;
}
public static ApiStatusEnum fromStatus(Integer status) {
if (status == null) {
return null;
}
for (ApiStatusEnum value : values()) {
if (value.status == status) {
return value;
}
}
return null;
}
public static String labelOf(Integer status) {
ApiStatusEnum value = fromStatus(status);
return value != null ? value.label : "未知";
}
}

View File

@@ -1,19 +0,0 @@
package com.zt.plat.module.databus.enums.gateway;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Step types supported by the unified API portal.
*/
@AllArgsConstructor
@Getter
public enum ApiStepTypeEnum {
START,
HTTP,
RPC,
SCRIPT,
END;
}

View File

@@ -1,32 +0,0 @@
package com.zt.plat.module.databus.enums.gateway;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Locale;
/**
* Supported expression languages for request/response mapping.
*/
@AllArgsConstructor
@Getter
public enum ExpressionTypeEnum {
JSON("json");
private final String code;
public static ExpressionTypeEnum fromValue(String value) {
if (value == null) {
return null;
}
String normalized = value.trim().toLowerCase(Locale.ROOT);
for (ExpressionTypeEnum type : values()) {
if (type.code.equals(normalized) || type.name().equals(normalized.toUpperCase(Locale.ROOT))) {
return type;
}
}
return null;
}
}

View File

@@ -1,28 +0,0 @@
package com.zt.plat.module.databus.enums.gateway;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Transformation phase enumeration.
*/
@AllArgsConstructor
@Getter
public enum TransformPhaseEnum {
REQUEST_PRE,
REQUEST_POST,
RESPONSE_PRE,
RESPONSE_POST,
ERROR;
public static TransformPhaseEnum fromCode(String code) {
for (TransformPhaseEnum phase : values()) {
if (phase.name().equalsIgnoreCase(code)) {
return phase;
}
}
return null;
}
}

View File

@@ -1,84 +0,0 @@
package com.zt.plat.module.databus.framework.integration.config;
import com.zt.plat.framework.common.util.security.CryptoSignatureUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Configuration properties for the unified API portal.
*/
@Data
@ConfigurationProperties(prefix = "databus.api-portal")
public class ApiGatewayProperties {
public static String APP_ID_HEADER = "ZT-App-Id";
public static String TIMESTAMP_HEADER = "ZT-Timestamp";
public static String NONCE_HEADER = "ZT-Nonce";
public static String SIGNATURE_HEADER = "ZT-Signature";
public static final String DEFAULT_BASE_PATH = "/admin-api/databus/api/portal";
public static final String LEGACY_BASE_PATH = "/databus/api/portal";
private String basePath = DEFAULT_BASE_PATH;
public void setBasePath(String basePath) {
if (!StringUtils.hasText(basePath)) {
this.basePath = DEFAULT_BASE_PATH;
return;
}
String normalized = basePath.startsWith("/") ? basePath : "/" + basePath;
if (LEGACY_BASE_PATH.equals(normalized)) {
this.basePath = DEFAULT_BASE_PATH;
return;
}
this.basePath = normalized;
}
public List<String> getAllBasePaths() {
Set<String> candidates = new LinkedHashSet<>();
candidates.add(basePath);
candidates.add(DEFAULT_BASE_PATH);
candidates.add(LEGACY_BASE_PATH);
return new ArrayList<>(candidates);
}
private List<String> allowedIps = new ArrayList<>();
private List<String> deniedIps = new ArrayList<>();
private Security security = new Security();
private boolean enableTenantHeader = false;
private String tenantHeader = "ZT-Tenant-Id";
private boolean enableAudit = true;
private boolean enableRateLimit = true;
@Data
public static class Security {
private boolean enabled = true;
private String signatureType = CryptoSignatureUtils.SIGNATURE_TYPE_MD5;
private String encryptionType = CryptoSignatureUtils.ENCRYPT_TYPE_AES;
private long allowedClockSkewSeconds = 300;
private long nonceTtlSeconds = 600;
private String nonceRedisKeyPrefix = "databus:gateway:nonce:";
private boolean requireBodyEncryption = true;
private boolean encryptResponse = true;
}
}

View File

@@ -1,25 +0,0 @@
package com.zt.plat.module.databus.framework.integration.config;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionEvaluatorRegistry;
import com.zt.plat.module.databus.framework.integration.gateway.expression.JsonataExpressionEvaluator;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
/**
* Registers expression evaluators with the registry.
*/
@Configuration
@RequiredArgsConstructor
public class ExpressionConfiguration {
private final ExpressionEvaluatorRegistry registry;
private final JsonataExpressionEvaluator jsonataExpressionEvaluator;
@PostConstruct
public void registerEvaluators() {
registry.register(ExpressionTypeEnum.JSON, jsonataExpressionEvaluator);
}
}

View File

@@ -1,68 +0,0 @@
package com.zt.plat.module.databus.framework.integration.config;
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayExecutionService;
import com.zt.plat.module.databus.framework.integration.gateway.core.ErrorHandlingStrategy;
import com.zt.plat.module.databus.framework.integration.gateway.security.GatewaySecurityFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpMethod;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.http.dsl.Http;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* Configures the unified API portal inbound gateway and supporting beans.
*/
@Configuration
@EnableConfigurationProperties(ApiGatewayProperties.class)
@RequiredArgsConstructor
public class GatewayIntegrationConfiguration {
private final ApiGatewayProperties properties;
private final ErrorHandlingStrategy errorHandlingStrategy;
@Bean(name = "apiPortalTaskExecutor")
public ThreadPoolTaskExecutor apiPortalTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(32);
executor.setQueueCapacity(256);
executor.setThreadNamePrefix("api-portal-");
executor.initialize();
return executor;
}
@Bean
public MessagingTemplate apiPortalMessagingTemplate() {
return new MessagingTemplate();
}
@Bean
public FilterRegistrationBean<GatewaySecurityFilter> gatewaySecurityFilterRegistration(GatewaySecurityFilter filter) {
FilterRegistrationBean<GatewaySecurityFilter> registration = new FilterRegistrationBean<>(filter);
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 10);
return registration;
}
@Bean
public IntegrationFlow apiGatewayInboundFlow(ApiGatewayExecutionService executionService) {
String pattern = properties.getBasePath() + "/{apiCode}/{version}";
return IntegrationFlow.from(Http.inboundGateway(pattern)
.requestMapping(spec -> spec
.methods(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.PATCH))
.errorChannel(errorHandlingStrategy.getErrorChannel())
.requestPayloadType(String.class)
.mappedRequestHeaders("*")
.mappedResponseHeaders("*"))
.handle(executionService, "mapRequest", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()))
.handle(executionService, "dispatch", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()))
.handle(executionService, "buildResponseEntity", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()))
.get();
}
}

View File

@@ -1,61 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import java.time.Duration;
@Configuration
public class GatewayWebClientConfiguration {
private final int maxInMemorySize;
private final long maxIdleTimeMillis;
private final long evictInBackgroundMillis;
private final boolean connectionPoolEnabled;
private final ReactorClientHttpConnector httpConnector;
private static final Logger log = LoggerFactory.getLogger(GatewayWebClientConfiguration.class);
public GatewayWebClientConfiguration(
@Value("${databus.gateway.web-client.max-in-memory-size:20971520}") int maxInMemorySize,
@Value("${databus.gateway.web-client.max-idle-time:45000}") long maxIdleTimeMillis,
@Value("${databus.gateway.web-client.evict-in-background-interval:20000}") long evictInBackgroundMillis,
@Value("${databus.gateway.web-client.connection-pool-enabled:true}") boolean connectionPoolEnabled) {
this.maxInMemorySize = maxInMemorySize;
this.maxIdleTimeMillis = maxIdleTimeMillis;
this.evictInBackgroundMillis = evictInBackgroundMillis;
this.connectionPoolEnabled = connectionPoolEnabled;
this.httpConnector = buildConnector();
}
@Bean
public WebClientCustomizer gatewayWebClientCustomizer() {
// 统一设置 WebClient 连接器与内存限制,避免各处重复配置
return builder -> builder
.clientConnector(httpConnector)
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(maxInMemorySize));
}
private ReactorClientHttpConnector buildConnector() {
if (connectionPoolEnabled) {
// 启用连接池,基于配置设置空闲回收参数
ConnectionProvider provider = ConnectionProvider.builder("databus-gateway")
.maxIdleTime(Duration.ofMillis(maxIdleTimeMillis))
.evictInBackground(Duration.ofMillis(evictInBackgroundMillis))
.build();
log.info("Databus gateway WebClient 已启用连接池 (maxIdleTime={}ms, evictInterval={}ms)",
maxIdleTimeMillis, evictInBackgroundMillis);
return new ReactorClientHttpConnector(HttpClient.create(provider).compress(true));
}
// 关闭连接池,每次请求都会重新建立 TCP 连接
log.info("Databus gateway WebClient 已禁用连接池,所有请求将使用全新连接");
return new ReactorClientHttpConnector(HttpClient.create().compress(true));
}
}

View File

@@ -1,222 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import com.zt.plat.module.databus.enums.gateway.TransformPhaseEnum;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionExecutor;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpec;
import com.zt.plat.module.databus.framework.integration.gateway.expression.ExpressionSpecParser;
import com.zt.plat.module.databus.framework.integration.gateway.expression.GatewayExpressionHelper;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.step.StepHandlerFactory;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.aop.Advice;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.TaskExecutor;
import org.springframework.integration.core.GenericHandler;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlowBuilder;
import org.springframework.integration.dsl.MessageChannels;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.*;
/**
* 按 API 定义装配动态集成流程。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiFlowAssembler {
private final StepHandlerFactory stepHandlerFactory;
private final PolicyAdvisorFactory policyAdvisorFactory;
private final ErrorHandlingStrategy errorHandlingStrategy;
private final MonitoringInterceptor monitoringInterceptor;
private final ExpressionExecutor expressionExecutor;
@Qualifier("apiPortalTaskExecutor")
private final TaskExecutor apiPortalTaskExecutor;
public ApiFlowRegistration assemble(ApiDefinitionAggregate aggregate) {
String inputChannelName = channelName(aggregate);
String flowId = flowId(aggregate);
IntegrationFlowBuilder builder = IntegrationFlow.from(MessageChannels.direct(inputChannelName)
.datatype(ApiInvocationContext.class)
.interceptor(monitoringInterceptor))
.log(message -> String.format("[API-PORTAL] 进入流程 %s", flowId))
.handle(ApiInvocationContext.class,
applyTransforms(aggregate, TransformPhaseEnum.REQUEST_PRE),
endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()));
List<FlowSegment> segments = segments(aggregate.getSteps());
for (FlowSegment segment : segments) {
if (segment instanceof SequentialSegment sequentialSegment) {
builder = applySequential(builder, aggregate, sequentialSegment.getStep());
} else if (segment instanceof ParallelSegment parallelSegment) {
builder = applyParallel(builder, aggregate, parallelSegment);
}
}
builder = builder
.handle(ApiInvocationContext.class,
applyTransforms(aggregate, TransformPhaseEnum.RESPONSE_PRE),
endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()))
.handle(ApiInvocationContext.class,
(payload, headers) -> payload,
endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()));
return ApiFlowRegistration.builder()
.flowId(flowId)
.inputChannelName(inputChannelName)
.flow(builder.get())
.build();
}
private GenericHandler<ApiInvocationContext> applyTransforms(ApiDefinitionAggregate aggregate, TransformPhaseEnum phase) {
boolean mutateRequest = phase == TransformPhaseEnum.REQUEST_PRE || phase == TransformPhaseEnum.REQUEST_POST;
boolean mutateResponse = phase == TransformPhaseEnum.RESPONSE_PRE
|| phase == TransformPhaseEnum.RESPONSE_POST
|| phase == TransformPhaseEnum.ERROR;
return (payload, headers) -> {
var transformDefinition = aggregate.getApiLevelTransforms().get(phase.name());
if (transformDefinition != null && StringUtils.hasText(transformDefinition.getExpression())) {
String rawExpression = transformDefinition.getExpressionType() + "::" + transformDefinition.getExpression();
ExpressionSpec spec = ExpressionSpecParser.parse(rawExpression, ExpressionTypeEnum.JSON);
try {
Object result = expressionExecutor.evaluate(spec, payload, payload.getRequestBody(), headers);
GatewayExpressionHelper.applyContextMutations(payload, result, mutateRequest, mutateResponse);
} catch (Exception ex) {
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_TRANSFORM_EVALUATION_FAILED, ex.getMessage());
}
}
return payload;
};
}
private IntegrationFlowBuilder applySequential(IntegrationFlowBuilder builder, ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
GenericHandler<ApiInvocationContext> handler = stepHandlerFactory.build(aggregate, stepDefinition);
return builder.handle(ApiInvocationContext.class, handler, endpoint -> {
endpoint.advice(errorHandlingStrategy.errorForwardingAdvice());
Advice[] advices = policyAdvisorFactory.buildAdvices(aggregate, stepDefinition);
if (advices.length > 0) {
endpoint.advice(advices);
}
});
}
private IntegrationFlowBuilder applyParallel(IntegrationFlowBuilder builder, ApiDefinitionAggregate aggregate, ParallelSegment segment) {
return builder.handle(ApiInvocationContext.class,
(payload, headers) -> executeParallel(payload, headers, aggregate, segment),
endpoint -> {
endpoint.advice(errorHandlingStrategy.errorForwardingAdvice());
Advice[] advices = policyAdvisorFactory.buildParallelAdvices(aggregate, segment);
if (advices.length > 0) {
endpoint.advice(advices);
}
});
}
private ApiInvocationContext executeParallel(ApiInvocationContext context, MessageHeaders headers,
ApiDefinitionAggregate aggregate, ParallelSegment segment) {
List<CompletableFuture<ApiInvocationContext>> futures = new ArrayList<>();
for (ApiStepDefinition step : segment.getSteps()) {
GenericHandler<ApiInvocationContext> handler = stepHandlerFactory.build(aggregate, step);
ApiInvocationContext childContext = context.copy();
futures.add(CompletableFuture.supplyAsync(() -> {
handler.handle(childContext, headers);
return childContext;
}, apiPortalTaskExecutor));
}
for (CompletableFuture<ApiInvocationContext> future : futures) {
try {
ApiInvocationContext child = future.get();
context.merge(child);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw ServiceExceptionUtil.exception(API_PARALLEL_INTERRUPTED);
} catch (ExecutionException ex) {
Throwable cause = ex.getCause();
if (cause instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_PARALLEL_FAILED, cause == null ? ex.getMessage() : cause.getMessage());
}
}
return context;
}
private List<FlowSegment> segments(List<ApiStepDefinition> steps) {
return steps.stream()
.sorted(Comparator.comparingInt(step -> step.getStep().getStepOrder() == null ? Integer.MAX_VALUE : step.getStep().getStepOrder()))
.collect(ArrayList::new, this::consumeStep, this::combineSegments);
}
private void consumeStep(List<FlowSegment> segments, ApiStepDefinition step) {
String parallelGroup = step.getStep().getParallelGroup();
if (!StringUtils.hasText(parallelGroup)) {
segments.add(new SequentialSegment(step));
return;
}
FlowSegment last = segments.isEmpty() ? null : segments.get(segments.size() - 1);
if (last instanceof ParallelSegment parallelSegment && parallelGroup.equals(parallelSegment.getGroup())) {
parallelSegment.getSteps().add(step);
} else {
ParallelSegment newSegment = new ParallelSegment(parallelGroup, new ArrayList<>());
newSegment.getSteps().add(step);
segments.add(newSegment);
}
}
private void combineSegments(List<FlowSegment> target, List<FlowSegment> source) {
target.addAll(source);
}
private String channelName(ApiDefinitionAggregate aggregate) {
return "api.portal.flow." + aggregate.getDefinition().getApiCode().toLowerCase() + "." + aggregate.getDefinition().getVersion();
}
private String flowId(ApiDefinitionAggregate aggregate) {
return "apiPortalFlow:" + aggregate.getDefinition().getApiCode() + ":" + aggregate.getDefinition().getVersion();
}
private interface FlowSegment {
}
@Getter
private static final class SequentialSegment implements FlowSegment {
private final ApiStepDefinition step;
private SequentialSegment(ApiStepDefinition step) {
this.step = step;
}
}
@Getter
private static final class ParallelSegment implements FlowSegment {
private final String group;
private final List<ApiStepDefinition> steps;
private ParallelSegment(String group, List<ApiStepDefinition> steps) {
this.group = group;
this.steps = steps;
}
}
}

View File

@@ -1,67 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.RequiredArgsConstructor;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_FLOW_NOT_FOUND;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_FLOW_NO_REPLY;
/**
* api 分发.
* @author chenbowen
*/
@Component
@RequiredArgsConstructor
public class ApiFlowDispatcher {
private final IntegrationFlowManager integrationFlowManager;
private final MessagingTemplate messagingTemplate;
public ApiInvocationContext dispatch(String apiCode, String version, ApiInvocationContext context) {
MessageChannel channel = requireInputChannel(apiCode, version);
Message<ApiInvocationContext> message = MessageBuilder.withPayload(context)
.setHeader("apiCode", apiCode)
.setHeader("version", version)
.build();
Message<?> reply = messagingTemplate.sendAndReceive(channel, message);
if (reply == null) {
throw ServiceExceptionUtil.exception(API_FLOW_NO_REPLY, apiCode, version);
}
return (ApiInvocationContext) reply.getPayload();
}
public ApiInvocationContext dispatchWithAggregate(ApiDefinitionAggregate aggregate, ApiInvocationContext context) {
IntegrationFlowManager.DebugFlowHandle handle = integrationFlowManager.obtainDebugHandle(aggregate);
MessageChannel channel = handle.channel();
Message<ApiInvocationContext> message = MessageBuilder.withPayload(context)
.setHeader("apiCode", aggregate.getDefinition().getApiCode())
.setHeader("version", aggregate.getDefinition().getVersion())
.build();
try {
Message<?> reply = messagingTemplate.sendAndReceive(channel, message);
if (reply == null) {
throw ServiceExceptionUtil.exception(API_FLOW_NO_REPLY, aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
}
return (ApiInvocationContext) reply.getPayload();
} finally {
integrationFlowManager.releaseDebugHandle(handle);
}
}
private MessageChannel requireInputChannel(String apiCode, String version) {
// 未命中时,进行一次兜底补偿查询
return integrationFlowManager.locateInputChannel(apiCode, version)
.or(() -> {
integrationFlowManager.refresh(apiCode, version);
return integrationFlowManager.locateInputChannel(apiCode, version);
})
.orElseThrow(() -> ServiceExceptionUtil.exception(API_FLOW_NOT_FOUND, apiCode, version));
}
}

View File

@@ -1,21 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import lombok.Builder;
import lombok.Value;
import org.springframework.integration.dsl.IntegrationFlow;
/**
* Metadata returned by the assembler for flow registration.
* @author chenbowen
*/
@Value
@Builder
public class ApiFlowRegistration {
String flowId;
String inputChannelName;
IntegrationFlow flow;
}

View File

@@ -1,385 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.framework.common.util.monitor.TracerUtils;
import com.zt.plat.framework.common.util.servlet.ServletUtils;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.security.CachedBodyHttpServletRequest;
import com.zt.plat.module.databus.service.gateway.ApiAccessLogService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
/**
* 将 API 调用上下文持久化为访问日志。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiGatewayAccessLogger {
public static final String ATTR_LOG_ID = "ApiAccessLogId";
public static final String ATTR_EXCEPTION_STACK = "ApiAccessLogExceptionStack";
public static final String HEADER_ACCESS_LOG_ID = "X-Databus-AccessLog-Id";
private static final String ATTR_REQUEST_START = "ApiAccessLogRequestStart";
private static final int MAX_TEXT_LENGTH = 4000;
private final ApiAccessLogService apiAccessLogService;
private final ObjectMapper objectMapper;
/**
* 在分发前记录请求信息。
*/
public void onRequest(ApiInvocationContext context) {
try {
ApiAccessLogDO logDO = buildRequestSnapshot(context);
Long existingLogId = getLogId(context);
if (existingLogId != null) {
logDO.setId(existingLogId);
apiAccessLogService.update(logDO);
} else {
Long logId = apiAccessLogService.create(logDO);
context.getAttributes().put(ATTR_LOG_ID, logId);
}
} catch (Exception ex) {
log.warn("记录 API 访问日志开始阶段失败, traceId={}", TracerUtils.getTraceId(), ex);
}
}
/**
* 记录异常堆栈,便于后续写入日志。
*/
public void onException(ApiInvocationContext context, Throwable throwable) {
if (throwable == null) {
return;
}
context.getAttributes().put(ATTR_EXCEPTION_STACK, buildStackTrace(throwable));
}
/**
* 在分发完成后补全日志信息。
*/
public void onResponse(ApiInvocationContext context) {
Long logId = getLogId(context);
if (logId == null) {
return;
}
try {
ApiAccessLogDO update = new ApiAccessLogDO();
update.setId(logId);
Integer responseStatus = resolveHttpStatus(context);
context.setResponseStatus(responseStatus);
update.setResponseStatus(responseStatus);
String responseMessage = resolveResponseMessage(context, responseStatus);
update.setResponseMessage(responseMessage);
if (!StringUtils.hasText(context.getResponseMessage()) && StringUtils.hasText(responseMessage)) {
context.setResponseMessage(responseMessage);
}
update.setResponseBody(toJson(context.getResponseBody()));
update.setStatus(resolveStatus(responseStatus));
update.setErrorCode(extractErrorCode(context.getResponseBody(), responseStatus));
update.setErrorMessage(resolveErrorMessage(context, responseStatus));
update.setExceptionStack((String) context.getAttributes().get(ATTR_EXCEPTION_STACK));
update.setStepResults(toJson(context.getStepResults()));
update.setExtra(toJson(buildExtra(context)));
update.setResponseTime(LocalDateTime.now());
update.setDuration(calculateDuration(context));
apiAccessLogService.update(update);
} catch (Exception ex) {
log.warn("记录 API 访问日志结束阶段失败, traceId={}, logId={}", TracerUtils.getTraceId(), logId, ex);
}
}
/**
* 安全过滤阶段的第一时间记录请求元数据,保证被快速拒绝的请求也能查询。
*/
public Long logEntrance(HttpServletRequest request) {
if (request == null) {
return null;
}
Object existing = request.getAttribute(ATTR_LOG_ID);
if (existing instanceof Long logId) {
return logId;
}
try {
ApiAccessLogDO logDO = new ApiAccessLogDO();
logDO.setTraceId(TracerUtils.getTraceId());
logDO.setRequestMethod(request.getMethod());
logDO.setRequestPath(request.getRequestURI());
logDO.setRequestQuery(truncate(request.getQueryString()));
logDO.setRequestHeaders(toJson(collectHeaders(request)));
logDO.setClientIp(ServletUtils.getClientIP(request));
logDO.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT));
logDO.setStatus(3);
logDO.setRequestTime(LocalDateTime.now());
logDO.setCreator("");
logDO.setUpdater("");
Long logId = apiAccessLogService.create(logDO);
request.setAttribute(ATTR_LOG_ID, logId);
request.setAttribute(ATTR_REQUEST_START, Instant.now());
return logId;
} catch (Exception ex) {
log.warn("记录入口 API 访问日志失败", ex);
return null;
}
}
/**
* 编排前即结束的请求在此补写状态码、耗时等关键信息。
*/
public void finalizeEarly(HttpServletRequest request, int status, String message) {
if (request == null) {
return;
}
Object existing = request.getAttribute(ATTR_LOG_ID);
if (!(existing instanceof Long logId)) {
return;
}
try {
ApiAccessLogDO update = new ApiAccessLogDO();
update.setId(logId);
update.setResponseStatus(status);
update.setResponseMessage(truncate(message));
update.setStatus(resolveStatus(status));
update.setResponseTime(LocalDateTime.now());
update.setDuration(calculateDuration(request));
update.setUpdater("1");
apiAccessLogService.update(update);
} catch (Exception ex) {
log.warn("更新入口 API 访问日志失败, logId={}", logId, ex);
}
}
/**
* 将入口阶段生成的 logId 通过请求头继续传递,供后续流程关联合并。
*/
public static void propagateLogIdHeader(CachedBodyHttpServletRequest requestWrapper, Long logId) {
if (requestWrapper == null || logId == null) {
return;
}
requestWrapper.setHeader(HEADER_ACCESS_LOG_ID, String.valueOf(logId));
}
private Long getLogId(ApiInvocationContext context) {
Object value = context.getAttributes().get(ATTR_LOG_ID);
return value instanceof Long ? (Long) value : null;
}
/**
* 根据编排上下文构建请求侧快照,用于访问日志首段信息。
*/
private ApiAccessLogDO buildRequestSnapshot(ApiInvocationContext context) {
ApiAccessLogDO logDO = new ApiAccessLogDO();
logDO.setTraceId(TracerUtils.getTraceId());
logDO.setApiCode(context.getApiCode());
logDO.setApiVersion(context.getApiVersion());
logDO.setRequestMethod(context.getHttpMethod());
logDO.setRequestPath(context.getRequestPath());
logDO.setRequestQuery(toJson(context.getRequestQueryParams()));
logDO.setRequestHeaders(toJson(context.getRequestHeaders()));
logDO.setRequestBody(toJson(context.getRequestBody()));
logDO.setClientIp(context.getClientIp());
logDO.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT));
logDO.setCredentialAppId(context.getCredentialAppId());
logDO.setCredentialId(context.getCredentialId());
logDO.setStatus(3);
logDO.setRequestTime(toLocalDateTime(context.getRequestTime()));
logDO.setTenantId(parseTenantId(context.getTenantId()));
return logDO;
}
private Long calculateDuration(ApiInvocationContext context) {
Instant start = context.getRequestTime();
if (start == null) {
return null;
}
return Duration.between(start, Instant.now()).toMillis();
}
private Long calculateDuration(HttpServletRequest request) {
Object startAttr = request.getAttribute(ATTR_REQUEST_START);
if (startAttr instanceof Instant start) {
return Duration.between(start, Instant.now()).toMillis();
}
return null;
}
private Integer resolveStatus(Integer httpStatus) {
if (httpStatus == null) {
return 3;
}
if (httpStatus >= 200 && httpStatus < 400) {
return 0;
}
if (httpStatus >= 400 && httpStatus < 500) {
return 1;
}
if (httpStatus >= 500) {
return 2;
}
return 3;
}
private String resolveErrorMessage(ApiInvocationContext context, Integer responseStatus) {
if (!isErrorStatus(responseStatus)) {
return null;
}
if (StringUtils.hasText(context.getResponseMessage())) {
return truncate(context.getResponseMessage());
}
Object responseBody = context.getResponseBody();
if (responseBody instanceof Map<?, ?> map) {
Object message = firstNonNull(map.get("errorMessage"), map.get("message"));
if (message != null) {
return truncate(String.valueOf(message));
}
}
return null;
}
private String extractErrorCode(Object responseBody, Integer responseStatus) {
if (!isErrorStatus(responseStatus)) {
return null;
}
if (responseBody instanceof Map<?, ?> map) {
Object errorCode = firstNonNull(map.get("errorCode"), map.get("code"));
return errorCode == null ? null : truncate(String.valueOf(errorCode));
}
return null;
}
private Integer resolveHttpStatus(ApiInvocationContext context) {
return context.getResponseStatus();
}
private String resolveResponseMessage(ApiInvocationContext context, Integer responseStatus) {
if (responseStatus == null) {
return null;
}
if (StringUtils.hasText(context.getResponseMessage())) {
return truncate(context.getResponseMessage());
}
HttpStatus resolved = HttpStatus.resolve(responseStatus);
return resolved != null ? resolved.getReasonPhrase() : null;
}
private boolean isErrorStatus(Integer responseStatus) {
return responseStatus != null && responseStatus >= 400;
}
private Map<String, Object> buildExtra(ApiInvocationContext context) {
Map<String, Object> extra = new HashMap<>();
if (!CollectionUtils.isEmpty(context.getVariables())) {
extra.put("variables", context.getVariables());
}
if (!CollectionUtils.isEmpty(context.getAttributes())) {
Map<String, Object> attributes = new HashMap<>(context.getAttributes());
attributes.remove(ATTR_LOG_ID);
attributes.remove(ATTR_EXCEPTION_STACK);
if (!attributes.isEmpty()) {
extra.put("attributes", attributes);
}
}
if (CollectionUtils.isEmpty(extra)) {
return null;
}
return extra;
}
@SafeVarargs
private <T> T firstNonNull(T... candidates) {
if (candidates == null) {
return null;
}
for (T candidate : candidates) {
if (candidate != null) {
return candidate;
}
}
return null;
}
private String toJson(Object value) {
if (value == null) {
return null;
}
if (value instanceof String str) {
return truncate(str);
}
try {
return truncate(objectMapper.writeValueAsString(value));
} catch (JsonProcessingException ex) {
return truncate(String.valueOf(value));
}
}
private String truncate(String text) {
if (!StringUtils.hasText(text)) {
return text;
}
if (text.length() <= MAX_TEXT_LENGTH) {
return text;
}
return text.substring(0, MAX_TEXT_LENGTH);
}
private Long parseTenantId(String tenantId) {
if (!StringUtils.hasText(tenantId)) {
return null;
}
try {
return Long.parseLong(tenantId.trim());
} catch (NumberFormatException ex) {
return null;
}
}
private LocalDateTime toLocalDateTime(Instant instant) {
if (instant == null) {
return null;
}
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
}
private String buildStackTrace(Throwable throwable) {
try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
throwable.printStackTrace(pw);
return truncate(sw.toString());
} catch (Exception ex) {
return throwable.getMessage();
}
}
private Map<String, String> collectHeaders(HttpServletRequest request) {
if (request == null) {
return Collections.emptyMap();
}
Map<String, String> headers = new LinkedHashMap<>();
Enumeration<String> names = request.getHeaderNames();
while (names != null && names.hasMoreElements()) {
String name = names.nextElement();
Enumeration<String> values = request.getHeaders(name);
if (values == null || !values.hasMoreElements()) {
continue;
}
headers.put(name, values.nextElement());
}
return headers;
}
}

View File

@@ -1,101 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_AUTH_UNAUTHORIZED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_CREDENTIAL_UNAUTHORIZED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_RATE_LIMIT_EXCEEDED;
/**
* Shared error processor that maps exceptions into {@link ApiInvocationContext} responses.
*/
@Component
public class ApiGatewayErrorProcessor {
/**
* Applies the given {@link Throwable} to the context, unwrapping {@link ServiceException}
* instances when present and falling back to a generic error payload otherwise.
*/
public void apply(ApiInvocationContext context, Throwable throwable) {
ServiceException serviceException = resolveServiceException(throwable);
if (serviceException != null) {
applyServiceException(context, serviceException);
} else {
applyUnexpectedException(context, throwable);
}
}
public void applyServiceException(ApiInvocationContext context, ServiceException ex) {
String message = StringUtils.hasText(ex.getMessage()) ? ex.getMessage() : "API invocation failed";
context.setResponseStatus(resolveHttpStatus(ex, context));
context.setResponseMessage(message);
if (context.getResponseBody() == null) {
Map<String, Object> body = new HashMap<>();
if (ex.getCode() != null) {
body.put("errorCode", ex.getCode());
}
body.put("errorMessage", message);
context.setResponseBody(body);
}
}
public void applyUnexpectedException(ApiInvocationContext context, Throwable throwable) {
String message = determineUnexpectedMessage(throwable);
context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
context.setResponseMessage(message);
if (context.getResponseBody() == null) {
Map<String, Object> body = new HashMap<>();
body.put("errorMessage", message);
body.put("exception", throwable.getClass().getSimpleName());
context.setResponseBody(body);
}
}
public ServiceException resolveServiceException(Throwable throwable) {
Throwable current = throwable;
while (current != null) {
if (current instanceof ServiceException serviceException) {
return serviceException;
}
current = current.getCause();
}
return null;
}
private int resolveHttpStatus(ServiceException ex, ApiInvocationContext context) {
if (context.getResponseStatus() != null) {
return context.getResponseStatus();
}
Integer code = ex.getCode();
if (code != null) {
if (API_AUTH_UNAUTHORIZED.getCode().equals(code)) {
return HttpStatus.UNAUTHORIZED.value();
}
if (API_RATE_LIMIT_EXCEEDED.getCode().equals(code)) {
return HttpStatus.TOO_MANY_REQUESTS.value();
}
if (API_CREDENTIAL_UNAUTHORIZED.getCode().equals(code)) {
return HttpStatus.FORBIDDEN.value();
}
}
return HttpStatus.INTERNAL_SERVER_ERROR.value();
}
private String determineUnexpectedMessage(Throwable throwable) {
Throwable current = throwable;
while (current != null) {
if (StringUtils.hasText(current.getMessage())) {
return current.getMessage();
}
current = current.getCause();
}
return "API invocation encountered an unexpected error";
}
}

View File

@@ -1,404 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.framework.common.util.monitor.TracerUtils;
import com.zt.plat.module.databus.controller.admin.gateway.vo.ApiGatewayInvokeReqVO;
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiCredentialBinding;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.security.GatewayJwtResolver;
import com.zt.plat.module.databus.framework.integration.gateway.security.GatewaySecurityFilter;
import com.zt.plat.module.databus.service.gateway.ApiDefinitionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.UriComponentsBuilder;
import java.lang.reflect.Array;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_CREDENTIAL_UNAUTHORIZED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_DEFINITION_NOT_FOUND;
/**
* 统一处理 API 门户的请求映射、分发与响应构建。
* 管理端调试与外部 HTTP 请求共享同一套逻辑,安全校验由 {@link GatewaySecurityFilter} 执行。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiGatewayExecutionService {
private static final String HEADER_REQUEST_HEADERS = org.springframework.integration.http.HttpHeaders.PREFIX + "requestHeaders";
private static final String HEADER_REQUEST_URI = org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri";
private static final String HEADER_REQUEST_PARAMS = org.springframework.integration.http.HttpHeaders.PREFIX + "requestParams";
private static final String HEADER_QUERY_STRING = org.springframework.integration.http.HttpHeaders.PREFIX + "queryString";
private static final String HEADER_REMOTE_ADDRESS = org.springframework.integration.http.HttpHeaders.PREFIX + "remoteAddress";
private static final String LOCAL_DEBUG_REMOTE_ADDRESS = "127.0.0.1";
private static final String ATTR_DEBUG_INVOKE = "gatewayDebugInvoke";
private static final String ATTR_API_AGGREGATE = "apiDefinitionAggregate";
private final ApiGatewayRequestMapper requestMapper;
private final ApiFlowDispatcher apiFlowDispatcher;
private final ApiGatewayErrorProcessor errorProcessor;
private final ApiGatewayProperties properties;
private final ObjectMapper objectMapper;
private final ApiGatewayAccessLogger accessLogger;
private final ApiDefinitionService apiDefinitionService;
/**
* 将 Spring Integration 提供的原始消息映射为网关上下文消息。
*/
public Message<ApiInvocationContext> mapRequest(Message<?> message) {
ApiInvocationContext context = requestMapper.map(message.getPayload(), message.getHeaders());
return MessageBuilder.withPayload(context)
.copyHeaders(message.getHeaders())
.setHeaderIfAbsent("apiCode", context.getApiCode())
.setHeaderIfAbsent("version", context.getApiVersion())
.build();
}
/**
* 分发 API 调用,并在异常场景套用统一错误处理。
*/
public ApiInvocationContext dispatch(Message<ApiInvocationContext> message) {
ApiInvocationContext context = message.getPayload();
accessLogger.onRequest(context);
ApiInvocationContext responseContext;
ApiDefinitionAggregate aggregate = null;
try {
enforceCredentialAuthorization(context);
if (Boolean.TRUE.equals(context.getAttributes().get(ATTR_DEBUG_INVOKE))) {
aggregate = resolveDebugAggregate(context);
} else {
// 对于非调试调用,也需要获取 aggregate 以便后续应用响应模板
aggregate = apiDefinitionService.findByCodeAndVersion(context.getApiCode(), context.getApiVersion())
.orElse(null);
}
// 将 aggregate 存储到上下文中,供响应构建时使用
if (aggregate != null) {
context.getAttributes().put(ATTR_API_AGGREGATE, aggregate);
}
if (aggregate != null && Boolean.TRUE.equals(context.getAttributes().get(ATTR_DEBUG_INVOKE))) {
responseContext = apiFlowDispatcher.dispatchWithAggregate(aggregate, context);
} else {
responseContext = apiFlowDispatcher.dispatch(context.getApiCode(), context.getApiVersion(), context);
}
responseContext.setResponseStatus(resolveStatus(responseContext));
} catch (ServiceException ex) {
errorProcessor.applyServiceException(context, ex);
accessLogger.onException(context, ex);
log.warn("[API-PORTAL] 分发 apiCode={} version={} 时出现 ServiceException: {}", context.getApiCode(), context.getApiVersion(), ex.getMessage());
responseContext = context;
} catch (Exception ex) {
ServiceException nestedServiceException = errorProcessor.resolveServiceException(ex);
if (nestedServiceException != null) {
errorProcessor.applyServiceException(context, nestedServiceException);
accessLogger.onException(context, nestedServiceException);
log.warn("[API-PORTAL] 分发 apiCode={} version={} 时出现 ServiceException(包装异常): {}", context.getApiCode(), context.getApiVersion(), nestedServiceException.getMessage());
if (log.isDebugEnabled()) {
log.debug("[API-PORTAL] 包装异常堆栈", ex);
}
} else {
errorProcessor.applyUnexpectedException(context, ex);
accessLogger.onException(context, ex);
log.error("[API-PORTAL] 分发 apiCode={} version={} 时出现未预期异常", context.getApiCode(), context.getApiVersion(), ex);
}
responseContext = context;
} finally {
accessLogger.onResponse(context);
}
return responseContext;
}
/**
* Builds a HTTP response entity for the external gateway flow.
*/
public ResponseEntity<ApiGatewayResponse> buildResponseEntity(ApiInvocationContext context) {
int status = resolveStatus(context);
ApiGatewayResponse envelope = buildResponseEnvelope(context, status);
return ResponseEntity.status(status).body(envelope);
}
/**
* Executes a debug invocation by reusing the same mapping/dispatch pipeline as the public gateway.
*/
public ResponseEntity<ApiGatewayResponse> invokeForDebug(ApiGatewayInvokeReqVO reqVO) {
Message<?> rawMessage = buildDebugMessage(reqVO);
Message<ApiInvocationContext> mappedMessage = mapRequest(rawMessage);
ApiInvocationContext context = mappedMessage.getPayload();
// 将调试透传的查询参数、请求头重新合并到上下文,避免映射阶段丢失
mergeDebugMetadata(context, reqVO);
context.getAttributes().put(ATTR_DEBUG_INVOKE, Boolean.TRUE);
ApiInvocationContext responseContext = dispatch(mappedMessage);
return buildResponseEntity(responseContext);
}
private ApiDefinitionAggregate resolveDebugAggregate(ApiInvocationContext context) {
Optional<ApiDefinitionAggregate> activeDefinition = apiDefinitionService.findByCodeAndVersion(context.getApiCode(), context.getApiVersion());
if (activeDefinition.isPresent()) {
return activeDefinition.get();
}
return apiDefinitionService.findByCodeAndVersionIncludingInactive(context.getApiCode(), context.getApiVersion())
.orElseThrow(() -> ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND));
}
private Message<?> buildDebugMessage(ApiGatewayInvokeReqVO reqVO) {
Object payload = preparePayload(reqVO.getPayload());
MessageBuilder<Object> builder = MessageBuilder.withPayload(payload);
Map<String, Object> uriVariables = Map.of(
"apiCode", reqVO.getApiCode(),
"version", reqVO.getVersion()
);
builder.setHeader(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables);
builder.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD, HttpMethod.POST.name());
String basePath = properties.getBasePath();
String rawQuery = buildQueryString(reqVO.getQueryParams());
String requestUri = basePath + "/" + reqVO.getApiCode() + "/" + reqVO.getVersion();
if (StringUtils.hasText(rawQuery)) {
requestUri = requestUri + "?" + rawQuery;
}
builder.setHeader(HEADER_REQUEST_URI, requestUri);
builder.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_URL, requestUri);
builder.setHeader(HEADER_REMOTE_ADDRESS, LOCAL_DEBUG_REMOTE_ADDRESS);
Map<String, Object> requestHeaders = new LinkedHashMap<>();
if (reqVO.getHeaders() != null) {
requestHeaders.putAll(reqVO.getHeaders());
}
normalizeJwtHeaders(requestHeaders, reqVO.getQueryParams());
requestHeaders.putIfAbsent(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
builder.setHeader(HEADER_REQUEST_HEADERS, requestHeaders);
requestHeaders.forEach((key, value) -> {
if (value != null) {
builder.setHeaderIfAbsent(key, value);
}
});
if (reqVO.getQueryParams() != null && !reqVO.getQueryParams().isEmpty()) {
Map<String, Object> paramsCopy = new LinkedHashMap<>(reqVO.getQueryParams());
builder.setHeader(HEADER_REQUEST_PARAMS, paramsCopy);
if (StringUtils.hasText(rawQuery)) {
builder.setHeader(HEADER_QUERY_STRING, rawQuery);
}
}
if (properties.isEnableTenantHeader()) {
String tenantHeader = properties.getTenantHeader();
Object tenantValue = requestHeaders.get(tenantHeader);
if (tenantValue != null) {
builder.setHeaderIfAbsent(tenantHeader, tenantValue);
}
}
return builder.build();
}
private Object preparePayload(Object payload) {
if (payload == null) {
return "";
}
if (payload instanceof String) {
return payload;
}
try {
return objectMapper.writeValueAsString(payload);
} catch (JsonProcessingException ex) {
log.debug("[API-PORTAL] 将调试请求体序列化为 JSON 失败,使用 toString()", ex);
return payload.toString();
}
}
private void mergeDebugMetadata(ApiInvocationContext context, ApiGatewayInvokeReqVO reqVO) {
if (reqVO.getHeaders() != null && !reqVO.getHeaders().isEmpty()) {
reqVO.getHeaders().forEach((key, value) -> context.getRequestHeaders().putIfAbsent(key, value));
}
if (reqVO.getQueryParams() != null && !reqVO.getQueryParams().isEmpty()) {
reqVO.getQueryParams().forEach((key, value) -> context.getRequestQueryParams().putIfAbsent(key, value));
}
if (!StringUtils.hasText(context.getHttpMethod())) {
context.setHttpMethod(HttpMethod.POST.name());
}
if (!StringUtils.hasText(context.getRequestPath())) {
String path = properties.getBasePath() + "/" + reqVO.getApiCode() + "/" + reqVO.getVersion();
context.setRequestPath(path);
}
}
private int resolveStatus(ApiInvocationContext context) {
return context.getResponseStatus() != null ? context.getResponseStatus() : HttpStatus.OK.value();
}
private ApiGatewayResponse buildResponseEnvelope(ApiInvocationContext context, int status) {
String message = StringUtils.hasText(context.getResponseMessage())
? context.getResponseMessage()
: HttpStatus.valueOf(status).getReasonPhrase();
// 尝试从上下文中获取 API 定义聚合对象
ApiDefinitionAggregate aggregate = (ApiDefinitionAggregate) context.getAttributes().get(ATTR_API_AGGREGATE);
String responseTemplate = aggregate != null ? aggregate.getDefinition().getResponseTemplate() : null;
// 如果配置了响应模板,则应用模板;否则使用默认格式(向后兼容)
if (StringUtils.hasText(responseTemplate)) {
return buildResponseWithTemplate(responseTemplate, context, status, message);
}
// 默认响应格式(向后兼容)
return ApiGatewayResponse.builder()
.code(status)
.message(message)
.response(context.getResponseBody())
.traceId(TracerUtils.getTraceId())
.build();
}
/**
* 根据响应模板构建响应对象
* 模板格式示例:{"code": 0, "message": "success", "data": {...}}
* 模板中的 data 字段会被实际响应数据替换,其他字段保持用户配置的原始值
*/
private ApiGatewayResponse buildResponseWithTemplate(String templateJson, ApiInvocationContext context, int status, String message) {
try {
// 解析模板 JSON
Map<String, Object> template = objectMapper.readValue(templateJson, Map.class);
// 构建最终响应数据,保留模板中的所有字段
Map<String, Object> responseData = new LinkedHashMap<>(template);
// 只替换 data 字段为实际的响应体数据
// 其他字段(如 code、message保持用户配置的原始值
if (responseData.containsKey("data")) {
responseData.put("data", context.getResponseBody());
}
// 返回使用模板结构的响应
// 注意ApiGatewayResponse 的 code 和 message 是 HTTP 层面的状态
// responseData 中的 code 和 message 是业务层面的状态
return ApiGatewayResponse.builder()
.code(status)
.message(message)
.response(responseData)
.traceId(TracerUtils.getTraceId())
.build();
} catch (JsonProcessingException ex) {
log.warn("[API-PORTAL] 解析响应模板失败,使用默认格式: {}", ex.getMessage());
// 解析失败时回退到默认格式
return ApiGatewayResponse.builder()
.code(status)
.message(message)
.response(context.getResponseBody())
.traceId(TracerUtils.getTraceId())
.build();
}
}
/**
* 调用前校验凭证白名单,非调试调用需匹配绑定的 appId。
*/
private void enforceCredentialAuthorization(ApiInvocationContext context) {
if (Boolean.TRUE.equals(context.getAttributes().get(ATTR_DEBUG_INVOKE))) {
return;
}
ApiDefinitionAggregate aggregate = apiDefinitionService.findByCodeAndVersion(context.getApiCode(), context.getApiVersion())
.orElseThrow(() -> ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND));
if (CollectionUtils.isEmpty(aggregate.getCredentialBindings())) {
return;
}
String appId = context.getCredentialAppId();
if (!StringUtils.hasText(appId)) {
throw ServiceExceptionUtil.exception(API_CREDENTIAL_UNAUTHORIZED);
}
boolean matched = aggregate.getCredentialBindings().stream()
.map(ApiCredentialBinding::getAppId)
.filter(StringUtils::hasText)
.anyMatch(boundAppId -> appId.trim().equalsIgnoreCase(boundAppId));
if (!matched) {
throw ServiceExceptionUtil.exception(API_CREDENTIAL_UNAUTHORIZED);
}
}
private String buildQueryString(Map<String, Object> queryParams) {
if (queryParams == null || queryParams.isEmpty()) {
return null;
}
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
queryParams.forEach((key, value) -> appendQueryParam(builder, key, value));
URI uri = builder.build(true).toUri();
return uri.getQuery();
}
private void appendQueryParam(UriComponentsBuilder builder, String key, Object value) {
if (!StringUtils.hasText(key)) {
return;
}
if (value == null) {
builder.queryParam(key, (Object) null);
return;
}
if (value instanceof Iterable<?> iterable) {
for (Object element : iterable) {
appendQueryParam(builder, key, element);
}
return;
}
if (value.getClass().isArray()) {
int length = Array.getLength(value);
for (int i = 0; i < length; i++) {
appendQueryParam(builder, key, Array.get(value, i));
}
return;
}
builder.queryParam(key, value);
}
private void normalizeJwtHeaders(Map<String, Object> headers, Map<String, Object> queryParams) {
String token = GatewayJwtResolver.resolveJwtToken(headers, queryParams, objectMapper);
if (!StringUtils.hasText(token)) {
return;
}
ensureHeaderValue(headers, GatewayJwtResolver.HEADER_ZT_AUTH_TOKEN, token);
ensureHeaderValue(headers, HttpHeaders.AUTHORIZATION, "Bearer " + token);
}
private void ensureHeaderValue(Map<String, Object> headers, String headerName, String value) {
if (!StringUtils.hasText(headerName) || value == null) {
return;
}
String existingKey = findHeaderKey(headers, headerName);
if (existingKey != null) {
headers.put(existingKey, value);
} else {
headers.put(headerName, value);
}
}
private String findHeaderKey(Map<String, Object> headers, String headerName) {
if (headers == null || !StringUtils.hasText(headerName)) {
return null;
}
for (String key : headers.keySet()) {
if (headerName.equalsIgnoreCase(key)) {
return key;
}
}
return null;
}
}

View File

@@ -1,372 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayAccessLogger;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Maps inbound HTTP request metadata into {@link ApiInvocationContext} instances.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ApiGatewayRequestMapper {
private final ObjectMapper objectMapper;
private final ApiGatewayProperties properties;
private static final String HEADER_REQUEST_HEADERS = org.springframework.integration.http.HttpHeaders.PREFIX + "requestHeaders";
private static final String HEADER_REQUEST_URI = org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri";
private static final String HEADER_REMOTE_ADDRESS = org.springframework.integration.http.HttpHeaders.PREFIX + "remoteAddress";
private static final String HEADER_CREDENTIAL_ID = "X-Databus-Credential-Id";
@SuppressWarnings("unchecked")
public ApiInvocationContext map(Object payload, Map<String, Object> headers) {
ApiInvocationContext context = ApiInvocationContext.create();
Map<String, Object> uriVariables = (Map<String, Object>) headers.get(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (uriVariables != null) {
Object apiCode = uriVariables.get("apiCode");
Object version = uriVariables.get("version");
if (apiCode != null) {
context.setApiCode(String.valueOf(apiCode));
}
if (version != null) {
context.setApiVersion(String.valueOf(version));
}
}
Object methodHeader = headers.get(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD);
if (methodHeader != null) {
context.setHttpMethod(String.valueOf(methodHeader));
}
Object requestPath = headers.get(HEADER_REQUEST_URI);
if (requestPath == null) {
requestPath = headers.get(org.springframework.integration.http.HttpHeaders.REQUEST_URL);
}
String originalRequestUri = requestPath != null ? String.valueOf(requestPath) : null;
if (originalRequestUri != null) {
context.setRequestPath(originalRequestUri);
}
if (!StringUtils.hasText(context.getApiCode())) {
Object apiCodeHeader = headers.get("apiCode");
if (apiCodeHeader != null) {
context.setApiCode(String.valueOf(apiCodeHeader));
}
}
if (!StringUtils.hasText(context.getApiVersion())) {
Object versionHeader = headers.get("version");
if (versionHeader != null) {
context.setApiVersion(String.valueOf(versionHeader));
}
}
if (!StringUtils.hasText(context.getApiCode()) || !StringUtils.hasText(context.getApiVersion())) {
inferFromRequestPath(context);
}
Map<String, Object> requestHeaders = (Map<String, Object>) headers.get(HEADER_REQUEST_HEADERS);
if (requestHeaders != null) {
context.getRequestHeaders().putAll(requestHeaders);
}
headers.forEach((key, value) -> {
if (isInternalHeader(key) || value == null) {
return;
}
context.getRequestHeaders().putIfAbsent(key, String.valueOf(value));
});
context.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT));
context.setClientIp(resolveClientIp(headers, context.getRequestHeaders()));
String appId = GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), ApiGatewayProperties.APP_ID_HEADER);
if (StringUtils.hasText(appId)) {
context.setCredentialAppId(appId.trim());
}
String credentialIdHeader = GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HEADER_CREDENTIAL_ID);
if (StringUtils.hasText(credentialIdHeader)) {
try {
context.setCredentialId(Long.valueOf(credentialIdHeader.trim()));
} catch (NumberFormatException ignored) {
context.setCredentialId(null);
}
}
captureAccessLogId(context);
populateQueryParams(headers, context, originalRequestUri);
if (properties.isEnableTenantHeader()) {
Object tenantHeaderValue = context.getRequestHeaders().get(properties.getTenantHeader());
if (tenantHeaderValue != null) {
context.setTenantId(String.valueOf(tenantHeaderValue));
}
}
if (payload instanceof String body) {
if (StringUtils.hasText(body) && isJsonContent(context)) {
try {
context.setRequestBody(objectMapper.readValue(body, Object.class));
} catch (IOException ex) {
log.warn("解析请求体为 JSON 失败", ex);
context.setRequestBody(body);
}
} else {
context.setRequestBody(body);
}
} else {
context.setRequestBody(payload);
}
return context;
}
private void captureAccessLogId(ApiInvocationContext context) {
String headerValue = GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), ApiGatewayAccessLogger.HEADER_ACCESS_LOG_ID);
if (!StringUtils.hasText(headerValue)) {
return;
}
try {
Long logId = Long.valueOf(headerValue);
context.getAttributes().put(ApiGatewayAccessLogger.ATTR_LOG_ID, logId);
} catch (NumberFormatException ex) {
// 忽略格式问题,仅在属性中保留原文以便排查
context.getAttributes().put(ApiGatewayAccessLogger.ATTR_LOG_ID, headerValue);
}
context.getRequestHeaders().remove(ApiGatewayAccessLogger.HEADER_ACCESS_LOG_ID);
}
private boolean isInternalHeader(String headerName) {
if (!StringUtils.hasText(headerName)) {
return true;
}
if (headerName.startsWith(org.springframework.integration.http.HttpHeaders.PREFIX)) {
return true;
}
if (org.springframework.messaging.MessageHeaders.ID.equals(headerName)
|| org.springframework.messaging.MessageHeaders.TIMESTAMP.equals(headerName)) {
return true;
}
return "correlationId".equals(headerName)
|| "sequenceNumber".equals(headerName)
|| "sequenceSize".equals(headerName)
|| "errorChannel".equals(headerName)
|| "replyChannel".equals(headerName)
|| "replyChannelName".equals(headerName)
|| "errorChannelName".equals(headerName);
}
private boolean isJsonContent(ApiInvocationContext context) {
String contentType = String.valueOf(context.getRequestHeaders().getOrDefault(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)).toLowerCase(Locale.ROOT);
return contentType.contains(MediaType.APPLICATION_JSON_VALUE);
}
private void populateQueryParams(Map<String, Object> headers, ApiInvocationContext context, String originalRequestUri) {
Map<?, ?> headerParams = findRequestParamsHeader(headers);
if (headerParams != null) {
mergeParameterMap(headerParams, context.getRequestQueryParams(), true);
}
String queryString = findQueryStringHeader(headers);
if (!StringUtils.hasText(queryString) && StringUtils.hasText(originalRequestUri)) {
int idx = originalRequestUri.indexOf('?');
if (idx >= 0 && idx + 1 < originalRequestUri.length()) {
queryString = originalRequestUri.substring(idx + 1);
}
}
if (StringUtils.hasText(queryString)) {
mergeQueryString(queryString, context.getRequestQueryParams());
}
}
private Map<?, ?> findRequestParamsHeader(Map<String, Object> headers) {
if (headers == null) {
return null;
}
for (Map.Entry<String, Object> entry : headers.entrySet()) {
String key = entry.getKey();
if (!StringUtils.hasText(key)) {
continue;
}
if (entry.getValue() instanceof Map<?, ?> map && key.toLowerCase(Locale.ROOT).endsWith("requestparams")) {
return map;
}
}
return null;
}
private String findQueryStringHeader(Map<String, Object> headers) {
if (headers == null) {
return null;
}
for (Map.Entry<String, Object> entry : headers.entrySet()) {
String key = entry.getKey();
if (!StringUtils.hasText(key)) {
continue;
}
if (key.toLowerCase(Locale.ROOT).endsWith("querystring") && entry.getValue() != null) {
return entry.getValue().toString();
}
}
return null;
}
private void mergeQueryString(String queryString, Map<String, Object> target) {
try {
MultiValueMap<String, String> params = UriComponentsBuilder.newInstance()
.query(queryString)
.build()
.getQueryParams();
params.forEach((key, values) -> {
if (!StringUtils.hasText(key) || values == null || values.isEmpty()) {
return;
}
if (target.containsKey(key)) {
return;
}
if (values.size() == 1) {
target.put(key, values.get(0));
} else {
target.put(key, List.copyOf(values));
}
});
} catch (IllegalArgumentException ex) {
log.debug("解析查询串 '{}' 失败", queryString, ex);
}
}
private void mergeParameterMap(Map<?, ?> source, Map<String, Object> target, boolean override) {
source.forEach((rawKey, rawValue) -> {
if (!(rawKey instanceof String key) || !StringUtils.hasText(key)) {
return;
}
List<String> values = normalizeParamValues(rawValue);
if (values.isEmpty()) {
return;
}
if (!override && target.containsKey(key)) {
return;
}
if (values.size() == 1) {
target.put(key, values.get(0));
} else {
target.put(key, values);
}
});
}
private List<String> normalizeParamValues(Object value) {
if (value == null) {
return List.of();
}
if (value instanceof CharSequence) {
String candidate = value.toString();
return candidate.isEmpty() ? List.of("") : List.of(candidate);
}
if (value instanceof Iterable<?>) {
List<String> result = new ArrayList<>();
for (Object element : (Iterable<?>) value) {
result.addAll(normalizeParamValues(element));
}
return result;
}
if (value.getClass().isArray()) {
int length = Array.getLength(value);
List<String> result = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
result.addAll(normalizeParamValues(Array.get(value, i)));
}
return result;
}
return List.of(value.toString());
}
private void inferFromRequestPath(ApiInvocationContext context) {
String requestPath = normalizeRequestPath(context.getRequestPath());
if (!StringUtils.hasText(requestPath)) {
return;
}
if (!requestPath.equals(context.getRequestPath())) {
context.setRequestPath(requestPath);
}
for (String basePath : properties.getAllBasePaths()) {
if (!requestPath.startsWith(basePath)) {
continue;
}
String remainder = requestPath.substring(basePath.length());
if (remainder.startsWith("/")) {
remainder = remainder.substring(1);
}
if (!StringUtils.hasText(remainder)) {
continue;
}
String[] segments = remainder.split("/");
if (segments.length < 2) {
continue;
}
if (!StringUtils.hasText(context.getApiCode())) {
context.setApiCode(segments[0]);
}
if (!StringUtils.hasText(context.getApiVersion())) {
context.setApiVersion(segments[1]);
}
if (StringUtils.hasText(context.getApiCode()) && StringUtils.hasText(context.getApiVersion())) {
return;
}
}
}
private String normalizeRequestPath(String requestPath) {
if (!StringUtils.hasText(requestPath)) {
return requestPath;
}
String candidate = requestPath;
int queryIndex = candidate.indexOf('?');
if (queryIndex >= 0) {
candidate = candidate.substring(0, queryIndex);
}
if (candidate.contains("://")) {
try {
java.net.URI uri = java.net.URI.create(candidate);
if (uri.getPath() != null) {
candidate = uri.getPath();
}
} catch (IllegalArgumentException ex) {
log.debug("将请求路径 '{}' 解析为 URI 失败", candidate, ex);
}
}
if (!candidate.startsWith("/")) {
candidate = "/" + candidate;
}
return candidate;
}
private String resolveClientIp(Map<String, Object> headers, Map<String, Object> requestHeaders) {
String forwarded = GatewayHeaderUtils.findFirstHeaderValue(requestHeaders, "X-Forwarded-For");
if (StringUtils.hasText(forwarded)) {
int idx = forwarded.indexOf(',');
return idx >= 0 ? forwarded.substring(0, idx).trim() : forwarded;
}
String realIp = GatewayHeaderUtils.findFirstHeaderValue(requestHeaders, "X-Real-IP");
if (StringUtils.hasText(realIp)) {
return realIp;
}
Object remote = headers.get(HEADER_REMOTE_ADDRESS);
if (remote != null) {
String candidate = remote.toString();
int slash = candidate.indexOf('/') >= 0 ? candidate.indexOf('/') : candidate.indexOf(':');
if (slash > 0) {
candidate = candidate.substring(0, slash);
}
return candidate.trim();
}
return null;
}
}

View File

@@ -1,74 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.dsl.MessageChannels;
import org.springframework.integration.handler.advice.AbstractHandleMessageAdvice;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.stereotype.Component;
/**
* 为 API 门户集中管理错误通道与处理器。
*/
@Slf4j
@Component
public class ErrorHandlingStrategy {
private final ApiGatewayErrorProcessor errorProcessor;
@Getter
private final MessageChannel errorChannel;
private final Advice errorForwardingAdvice;
public ErrorHandlingStrategy(ApiGatewayErrorProcessor errorProcessor) {
this.errorProcessor = errorProcessor;
DirectChannel channel = MessageChannels.direct("apiPortalErrorChannel").getObject();
this.errorChannel = channel;
channel.subscribe(this::handleErrorMessage);
this.errorForwardingAdvice = new ErrorForwardingAdvice();
}
public Advice errorForwardingAdvice() {
return errorForwardingAdvice;
}
private void handleErrorMessage(Message<?> message) {
if (message instanceof ErrorMessage errorMessage) {
handleError(errorMessage);
}
}
private void handleError(ErrorMessage errorMessage) {
Throwable throwable = errorMessage.getPayload();
Message<?> failedMessage = errorMessage.getOriginalMessage();
if (failedMessage != null && failedMessage.getPayload() instanceof ApiInvocationContext context) {
errorProcessor.apply(context, throwable);
}
log.error("[API-PORTAL] 集成流程发生错误", throwable);
}
private class ErrorForwardingAdvice extends AbstractHandleMessageAdvice {
@Override
protected Object doInvoke(MethodInvocation invocation, Message<?> message) throws Throwable {
try {
return invocation.proceed();
} catch (Throwable ex) {
ErrorMessage errorMessage = new ErrorMessage(ex, message);
try {
if (!errorChannel.send(errorMessage)) {
log.warn("[API-PORTAL] 无法将错误消息转发到通道 {}", errorChannel);
}
} catch (Exception sendEx) {
log.error("[API-PORTAL] 向错误通道投递消息时出错", sendEx);
}
throw ex;
}
}
}
}

View File

@@ -1,81 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import org.springframework.util.StringUtils;
import java.lang.reflect.Array;
import java.util.Map;
/**
* Utility helpers for working with request headers inside the gateway module.
*/
public final class GatewayHeaderUtils {
private GatewayHeaderUtils() {
}
public static void mergeNormalizedHeaders(Map<String, Object> source, Map<String, Object> target) {
if (source == null || target == null) {
return;
}
source.forEach((key, value) -> {
String normalized = normalizeHeaderValue(value);
if (normalized != null) {
target.put(key, normalized);
}
});
}
public static String findFirstHeaderValue(Map<String, Object> headers, String headerName) {
if (headers == null || !StringUtils.hasText(headerName)) {
return null;
}
for (Map.Entry<String, Object> entry : headers.entrySet()) {
if (headerName.equalsIgnoreCase(entry.getKey())) {
return normalizeHeaderValue(entry.getValue());
}
}
return null;
}
public static String stripBearerPrefix(String token) {
if (!StringUtils.hasText(token)) {
return token;
}
String candidate = token.trim();
if (candidate.regionMatches(true, 0, "Bearer ", 0, 7)) {
candidate = candidate.substring(7).trim();
}
return candidate;
}
static String normalizeHeaderValue(Object value) {
if (value == null) {
return null;
}
if (value instanceof CharSequence sequence) {
String candidate = sequence.toString().trim();
return StringUtils.hasText(candidate) ? candidate : null;
}
if (value instanceof Iterable<?> iterable) {
for (Object element : iterable) {
String candidate = normalizeHeaderValue(element);
if (candidate != null) {
return candidate;
}
}
return null;
}
if (value.getClass().isArray()) {
int length = Array.getLength(value);
for (int i = 0; i < length; i++) {
String candidate = normalizeHeaderValue(Array.get(value, i));
if (candidate != null) {
return candidate;
}
}
return null;
}
String candidate = value.toString().trim();
return StringUtils.hasText(candidate) ? candidate : null;
}
}

View File

@@ -1,144 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.service.gateway.ApiDefinitionService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.integration.dsl.context.IntegrationFlowContext;
import org.springframework.messaging.MessageChannel;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* 管理 API 集成流程的动态注册。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class IntegrationFlowManager {
private final IntegrationFlowContext integrationFlowContext;
private final ApiDefinitionService apiDefinitionService;
private final ApiFlowAssembler apiFlowAssembler;
private final Map<String, IntegrationFlowContext.IntegrationFlowRegistration> activeRegistrations = new ConcurrentHashMap<>();
@PostConstruct
public void bootstrap() {
refreshAll();
}
public void refreshAll() {
List<ApiDefinitionAggregate> aggregates = apiDefinitionService.loadActiveDefinitions();
Map<String, ApiDefinitionAggregate> desired = new ConcurrentHashMap<>();
for (ApiDefinitionAggregate aggregate : aggregates) {
desired.put(key(aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion()), aggregate);
}
// remove flows that are no longer active
activeRegistrations.keySet().stream()
.filter(existingKey -> !desired.containsKey(existingKey))
.forEach(this::deregisterByKey);
// register or refresh active flows
desired.values().forEach(this::registerFlow);
}
public void refresh(String apiCode, String version) {
apiDefinitionService.refresh(apiCode, version)
.ifPresentOrElse(this::registerFlow, () -> deregister(apiCode, version));
}
public Optional<MessageChannel> locateInputChannel(String apiCode, String version) {
String key = key(apiCode, version);
IntegrationFlowContext.IntegrationFlowRegistration registration = activeRegistrations.get(key);
if (registration == null) {
return Optional.empty();
}
return Optional.ofNullable(registration.getInputChannel());
}
public DebugFlowHandle obtainDebugHandle(ApiDefinitionAggregate aggregate) {
String key = key(aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
IntegrationFlowContext.IntegrationFlowRegistration existing = activeRegistrations.get(key);
if (existing != null) {
return new DebugFlowHandle(existing.getInputChannel(), existing.getId(), false);
}
ApiFlowRegistration registration = apiFlowAssembler.assemble(aggregate);
String debugId = registration.getFlowId() + "#debug#" + UUID.randomUUID();
IntegrationFlowContext.IntegrationFlowRegistration debugRegistration = integrationFlowContext.registration(registration.getFlow())
.id(debugId)
.register();
log.debug("[API-PORTAL] 临时注册调试流程 {} 对应 apiCode={} version={}", debugId, aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
return new DebugFlowHandle(debugRegistration.getInputChannel(), debugRegistration.getId(), true);
}
public void releaseDebugHandle(DebugFlowHandle handle) {
if (handle == null || !handle.temporary) {
return;
}
try {
integrationFlowContext.remove(handle.registrationId);
log.debug("[API-PORTAL] 已移除调试流程 {}", handle.registrationId);
} catch (Exception ex) {
log.warn("移除调试流程 {} 失败", handle.registrationId, ex);
}
}
private void registerFlow(ApiDefinitionAggregate aggregate) {
String key = key(aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
deregisterByKey(key);
ApiFlowRegistration apiFlowRegistration = apiFlowAssembler.assemble(aggregate);
IntegrationFlowContext.IntegrationFlowRegistration registration = integrationFlowContext.registration(apiFlowRegistration.getFlow())
.id(apiFlowRegistration.getFlowId())
.register();
activeRegistrations.put(key, registration);
log.info("[API-PORTAL] 已注册流程 {} 对应 apiCode={} version={}", apiFlowRegistration.getFlowId(), aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
}
private void deregister(String apiCode, String version) {
deregisterByKey(key(apiCode, version));
}
private void deregisterByKey(String key) {
IntegrationFlowContext.IntegrationFlowRegistration existing = activeRegistrations.remove(key);
if (existing != null) {
try {
integrationFlowContext.remove(existing.getId());
log.info("[API-PORTAL] 已注销流程 {} 对应 key {}", existing.getId(), key);
} catch (Exception ex) {
log.warn("移除集成流程 {} 失败", existing.getId(), ex);
}
}
}
private String key(String apiCode, String version) {
return (apiCode + ":" + version).toLowerCase();
}
public static final class DebugFlowHandle {
private final MessageChannel channel;
private final String registrationId;
private final boolean temporary;
private DebugFlowHandle(MessageChannel channel, String registrationId, boolean temporary) {
this.channel = channel;
this.registrationId = registrationId;
this.temporary = temporary;
}
public MessageChannel channel() {
return channel;
}
public boolean temporary() {
return temporary;
}
}
}

View File

@@ -1,54 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
/**
* 通道拦截器,用于捕获耗时指标并增强日志记录。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MonitoringInterceptor implements ChannelInterceptor {
private static final String HEADER_START_TIME = "ApiPortalStartTime";
private final MeterRegistry meterRegistry;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return MessageBuilder.fromMessage(message)
.setHeader(HEADER_START_TIME, Instant.now())
.build();
}
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
Instant start = message.getHeaders().get(HEADER_START_TIME, Instant.class);
if (start != null) {
Duration duration = Duration.between(start, Instant.now());
Object payload = message.getPayload();
if (payload instanceof ApiInvocationContext context) {
Timer.builder("api.portal.latency")
.tag("api", context.getApiCode())
.tag("version", context.getApiVersion())
.register(meterRegistry)
.record(duration);
}
}
if (ex != null) {
log.error("[API-PORTAL] 通道发送失败", ex);
}
}
}

View File

@@ -1,66 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.policy.RateLimitPolicyEvaluator;
import lombok.RequiredArgsConstructor;
import org.aopalliance.aop.Advice;
import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_EXECUTION_ERROR;
/**
* Builds advice chains for steps based on configured policies.
*/
@Component
@RequiredArgsConstructor
public class PolicyAdvisorFactory {
private final RateLimitPolicyEvaluator rateLimitPolicyEvaluator;
public Advice[] buildAdvices(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
List<Advice> advices = new ArrayList<>();
if (aggregate.getRateLimitPolicy() != null) {
advices.add(new RateLimitPolicyAdvice(aggregate));
}
return advices.toArray(Advice[]::new);
}
public Advice[] buildParallelAdvices(ApiDefinitionAggregate aggregate, Object segment) {
// For parallel segments we reuse the same rate-limit advice chain
return buildAdvices(aggregate, null);
}
private final class RateLimitPolicyAdvice extends AbstractRequestHandlerAdvice {
private final ApiDefinitionAggregate aggregate;
private RateLimitPolicyAdvice(ApiDefinitionAggregate aggregate) {
this.aggregate = aggregate;
}
@Override
protected Object doInvoke(ExecutionCallback callback, Object target, org.springframework.messaging.Message<?> message) {
if (aggregate.getRateLimitPolicy() != null) {
rateLimitPolicyEvaluator.evaluate(aggregate, (ApiInvocationContext) message.getPayload());
}
try {
return callback.execute();
} catch (Exception ex) {
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw ServiceExceptionUtil.exception(API_STEP_EXECUTION_ERROR, ex.getMessage());
}
}
}
}

View File

@@ -1,18 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.domain;
import lombok.Builder;
import lombok.Value;
/**
* API 授权绑定的凭证信息。
*/
@Value
@Builder
public class ApiCredentialBinding {
Long credentialId;
String appId;
String appName;
}

View File

@@ -1,62 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.domain;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
import lombok.Builder;
import lombok.Value;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Aggregate representing an API definition with its steps and policies.
*/
@Value
@Builder(toBuilder = true)
public class ApiDefinitionAggregate {
ApiDefinitionDO definition;
List<ApiStepDefinition> steps;
Map<String, ApiTransformDefinition> apiLevelTransforms;
ApiPolicyRateLimitDO rateLimitPolicy;
ApiFlowPublication publication;
List<ApiCredentialBinding> credentialBindings;
public List<ApiStepDefinition> getSteps() {
return steps == null ? Collections.emptyList() : steps;
}
public Map<String, ApiTransformDefinition> getApiLevelTransforms() {
return apiLevelTransforms == null ? Collections.emptyMap() : apiLevelTransforms;
}
public List<ApiCredentialBinding> getCredentialBindings() {
return credentialBindings == null ? Collections.emptyList() : credentialBindings;
}
/**
* 获取最后一个 HTTP 步骤的 IDorder 最大的 HTTP 步骤)
* 用于判断是否需要设置 responseBody
*/
public Long getLastHttpStepId() {
return getSteps().stream()
.filter(stepDef -> "HTTP".equalsIgnoreCase(stepDef.getStep().getType()))
.max((s1, s2) -> {
Integer order1 = s1.getStep().getStepOrder();
Integer order2 = s2.getStep().getStepOrder();
if (order1 == null && order2 == null) return 0;
if (order1 == null) return -1;
if (order2 == null) return 1;
return order1.compareTo(order2);
})
.map(stepDef -> stepDef.getStep().getId())
.orElse(null);
}
}

View File

@@ -1,25 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.domain;
import lombok.Builder;
import lombok.Value;
/**
* Publication metadata for an API flow.
*/
@Value
@Builder(toBuilder = true)
public class ApiFlowPublication {
Long id;
String releaseTag;
String snapshot;
String status;
boolean active;
String description;
}

View File

@@ -1,32 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.domain;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO;
import lombok.Builder;
import lombok.Value;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Domain representation of an orchestration step.
*/
@Value
@Builder(toBuilder = true)
public class ApiStepDefinition {
ApiStepDO step;
List<ApiTransformDefinition> transforms;
Map<String, Object> metadata;
public List<ApiTransformDefinition> getTransforms() {
return transforms == null ? Collections.emptyList() : transforms;
}
public Map<String, Object> getMetadata() {
return metadata == null ? Collections.emptyMap() : metadata;
}
}

View File

@@ -1,23 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.domain;
import lombok.Builder;
import lombok.Value;
/**
* Domain representation for transformation expression metadata.
*/
@Value
@Builder(toBuilder = true)
public class ApiTransformDefinition {
Long id;
String phase;
String expressionType;
String expression;
String description;
}

View File

@@ -1,24 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.Builder;
import lombok.Value;
import java.util.Map;
/**
* Context provided to expression engines when evaluating mappings.
*/
@Value
@Builder
public class ExpressionEvaluationContext {
ApiInvocationContext invocation;
Object payload;
Map<String, Object> variables;
Map<String, Object> headers;
}

View File

@@ -1,10 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
/**
* Expression evaluator contract.
*/
public interface ExpressionEvaluator {
Object evaluate(String expression, ExpressionEvaluationContext context) throws Exception;
}

View File

@@ -1,28 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
/**
* Registry maintaining expression evaluators per language.
*/
@Component
@RequiredArgsConstructor
public class ExpressionEvaluatorRegistry {
private final Map<ExpressionTypeEnum, ExpressionEvaluator> evaluators = new EnumMap<>(ExpressionTypeEnum.class);
public void register(ExpressionTypeEnum type, ExpressionEvaluator evaluator) {
evaluators.put(type, evaluator);
}
public Optional<ExpressionEvaluator> lookup(ExpressionTypeEnum type) {
return Optional.ofNullable(evaluators.get(type));
}
}

View File

@@ -1,51 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Optional;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_EXPRESSION_EVALUATION_FAILED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_EXPRESSION_NO_EVALUATOR;
/**
* Executes expressions using registered evaluators.
*/
@Component
@RequiredArgsConstructor
public class ExpressionExecutor {
private final ExpressionEvaluatorRegistry registry;
public Object evaluate(ExpressionSpec spec, ApiInvocationContext invocation, Object payload, Map<String, Object> headers) throws Exception {
if (spec == null || spec.getExpression() == null) {
return null;
}
ExpressionTypeEnum type = spec.getType();
return registry.lookup(type)
.orElseThrow(() -> ServiceExceptionUtil.exception(API_EXPRESSION_NO_EVALUATOR, type == null ? "" : type.name()))
.evaluate(spec.getExpression(), ExpressionEvaluationContext.builder()
.invocation(invocation)
.payload(payload)
.variables(invocation.getVariables())
.headers(headers)
.build());
}
public Optional<Object> evaluateOptional(ExpressionSpec spec, ApiInvocationContext invocation, Object payload, Map<String, Object> headers) {
try {
return Optional.ofNullable(evaluate(spec, invocation, payload, headers));
} catch (Exception ex) {
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_EXPRESSION_EVALUATION_FAILED, ex.getMessage());
}
}
}

View File

@@ -1,21 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import lombok.Builder;
import lombok.Value;
/**
* Parsed expression specification with language metadata.
*/
@Value
@Builder
public class ExpressionSpec {
ExpressionTypeEnum type;
String expression;
public static ExpressionSpec of(ExpressionTypeEnum type, String expression) {
return ExpressionSpec.builder().type(type).expression(expression).build();
}
}

View File

@@ -1,32 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.zt.plat.module.databus.enums.gateway.ExpressionTypeEnum;
import org.springframework.util.StringUtils;
/**
* Helper to parse unified expression definitions.
*/
public final class ExpressionSpecParser {
private ExpressionSpecParser() {
}
public static ExpressionSpec parse(String rawExpression, ExpressionTypeEnum defaultType) {
if (!StringUtils.hasText(rawExpression)) {
return null;
}
String trimmed = rawExpression.trim();
int separator = trimmed.indexOf("::");
if (separator < 0) {
return ExpressionSpec.of(defaultType, trimmed);
}
String type = trimmed.substring(0, separator);
String expression = trimmed.substring(separator + 2);
ExpressionTypeEnum expressionTypeEnum = ExpressionTypeEnum.fromValue(type);
if (expressionTypeEnum == null) {
expressionTypeEnum = defaultType;
}
return ExpressionSpec.of(expressionTypeEnum, expression);
}
}

View File

@@ -1,214 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import java.util.LinkedHashMap;
import java.util.Map;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_TRANSFORM_RESPONSE_STATUS_INVALID;
/**
* Utility helpers for mutating {@link ApiInvocationContext} based on expression evaluation results.
*/
public final class GatewayExpressionHelper {
private GatewayExpressionHelper() {
}
public static void applyContextMutations(ApiInvocationContext context, Object result,
boolean allowRequestMutations, boolean allowResponseMutations) {
if (result == null) {
return;
}
if (result instanceof Map<?, ?> map) {
applyCommonMutations(context, map);
if (allowRequestMutations) {
applyRequestMutations(context, map);
}
if (allowResponseMutations) {
applyResponseMutations(context, map);
}
return;
}
if (allowRequestMutations) {
context.setRequestBody(result);
} else if (allowResponseMutations) {
context.setResponseBody(result);
}
}
public static Map<String, Object> snapshotRequest(ApiInvocationContext context) {
Map<String, Object> snapshot = new LinkedHashMap<>();
if (context.getRequestBody() != null) {
snapshot.put("body", context.getRequestBody());
}
if (context.getRequestQueryParams() != null && !context.getRequestQueryParams().isEmpty()) {
snapshot.put("query", new LinkedHashMap<>(context.getRequestQueryParams()));
}
if (context.getRequestHeaders() != null && !context.getRequestHeaders().isEmpty()) {
snapshot.put("headers", new LinkedHashMap<>(context.getRequestHeaders()));
}
if (snapshot.isEmpty()) {
return null;
}
return snapshot;
}
public static Map<String, Object> snapshotResponse(ApiInvocationContext context) {
Map<String, Object> snapshot = new LinkedHashMap<>();
if (context.getResponseBody() != null) {
snapshot.put("body", context.getResponseBody());
}
if (context.getResponseStatus() != null) {
snapshot.put("status", context.getResponseStatus());
}
if (context.getResponseMessage() != null) {
snapshot.put("message", context.getResponseMessage());
}
if (snapshot.isEmpty()) {
return null;
}
return snapshot;
}
private static void applyCommonMutations(ApiInvocationContext context, Map<?, ?> map) {
Object variables = map.get("variables");
if (variables instanceof Map<?, ?> variableMap) {
context.getVariables().putAll(toObjectMap(variableMap));
}
Object attributes = map.get("attributes");
if (attributes instanceof Map<?, ?> attributeMap) {
context.getAttributes().putAll(toObjectMap(attributeMap));
}
Object tenantId = map.get("tenantId");
if (tenantId != null) {
context.setTenantId(String.valueOf(tenantId));
}
Object httpMethod = map.get("httpMethod");
if (httpMethod != null) {
context.setHttpMethod(String.valueOf(httpMethod));
}
Object requestPath = map.get("requestPath");
if (requestPath != null) {
context.setRequestPath(String.valueOf(requestPath));
}
}
private static void applyRequestMutations(ApiInvocationContext context, Map<?, ?> map) {
// 处理查询参数
Object query = firstNonNull(map.get("requestQuery"), map.get("requestQueryParams"), map.get("query"));
if (query instanceof Map<?, ?> queryMap) {
context.getRequestQueryParams().putAll(toObjectMap(queryMap));
}
// 处理请求头
Object headers = map.get("requestHeaders");
if (headers instanceof Map<?, ?> headerMap) {
context.getRequestHeaders().putAll(toStringMap(headerMap));
}
// 处理 body - 使用和 HTTP 步骤一致的逻辑
// 检查是否显式指定了 body 字段
boolean explicitBody = containsKeyIgnoreCase(map, "body", "payload", "requestBody");
if (explicitBody) {
// 如果显式指定了 body提取并与原有 body 合并
Object body = firstNonNull(map.get("requestBody"), map.get("body"), map.get("payload"));
Object existingBody = context.getRequestBody();
if (body instanceof Map && existingBody instanceof Map) {
// 两者都是 Map合并以保留未映射的字段
Map<String, Object> mergedBody = new LinkedHashMap<>(toObjectMap((Map<?, ?>) existingBody));
mergedBody.putAll(toObjectMap((Map<?, ?>) body));
context.setRequestBody(mergedBody);
} else if (body != null) {
// 直接替换
context.setRequestBody(body);
}
} else if (query == null) {
// 如果没有显式 body 字段,也没有 query 字段
// 说明整个 map 是新的 body与原有 body 合并
Object existingBody = context.getRequestBody();
if (existingBody instanceof Map) {
Map<String, Object> mergedBody = new LinkedHashMap<>(toObjectMap((Map<?, ?>) existingBody));
mergedBody.putAll(toObjectMap(map));
context.setRequestBody(mergedBody);
} else {
// 原有 body 不是 Map直接替换
context.setRequestBody(toObjectMap(map));
}
}
// 如果有 query 字段但没有显式 body 字段,保留原有 body不做任何操作
}
private static void applyResponseMutations(ApiInvocationContext context, Map<?, ?> map) {
Object body = firstNonNull(map.get("responseBody"), map.get("body"));
if (body != null) {
context.setResponseBody(body);
}
Object status = map.get("responseStatus");
if (status != null) {
context.setResponseStatus(asInteger(status));
}
Object message = map.get("responseMessage");
if (message != null) {
context.setResponseMessage(String.valueOf(message));
}
}
private static Map<String, Object> toObjectMap(Map<?, ?> origin) {
Map<String, Object> result = new LinkedHashMap<>();
origin.forEach((key, value) -> result.put(String.valueOf(key), value));
return result;
}
private static Map<String, String> toStringMap(Map<?, ?> origin) {
Map<String, String> result = new LinkedHashMap<>();
origin.forEach((key, value) -> {
if (value != null) {
result.put(String.valueOf(key), String.valueOf(value));
}
});
return result;
}
private static Integer asInteger(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.intValue();
}
String raw = String.valueOf(value);
try {
return Integer.parseInt(raw);
} catch (NumberFormatException ex) {
throw ServiceExceptionUtil.exception(API_TRANSFORM_RESPONSE_STATUS_INVALID, raw);
}
}
@SafeVarargs
private static <T> T firstNonNull(T... values) {
for (T value : values) {
if (value != null) {
return value;
}
}
return null;
}
private static boolean containsKeyIgnoreCase(Map<?, ?> source, String... keys) {
if (source == null || source.isEmpty()) {
return false;
}
for (String key : keys) {
for (Object entryKey : source.keySet()) {
if (key.equalsIgnoreCase(String.valueOf(entryKey))) {
return true;
}
}
}
return false;
}
}

View File

@@ -1,57 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
import com.api.jsonata4java.expressions.*;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_JSONATA_BIND_FAILED;
/**
* JSONata expression evaluator for JSON payload transformation.
* @author chenbowen
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JsonataExpressionEvaluator implements ExpressionEvaluator {
private final ObjectMapper objectMapper;
@Override
public Object evaluate(String expression, ExpressionEvaluationContext context) throws ParseException, EvaluateException, JsonProcessingException, IOException {
Expressions expressions = Expressions.parse(expression);
bindEnvironment(expressions.getEnvironment(), context);
JsonNode payloadNode = objectMapper.valueToTree(context.getPayload());
JsonNode resultNode = expressions.evaluate(payloadNode);
if (resultNode == null || resultNode.isNull()) {
return null;
}
return objectMapper.treeToValue(resultNode, Object.class);
}
private void bindEnvironment(FrameEnvironment environment, ExpressionEvaluationContext context) {
if (environment == null) {
return;
}
try {
environment.setVariable("vars", objectMapper.valueToTree(context.getVariables() == null ? java.util.Collections.emptyMap() : context.getVariables()));
environment.setVariable("headers", objectMapper.valueToTree(context.getHeaders() == null ? java.util.Collections.emptyMap() : context.getHeaders()));
environment.setVariable("ctx", objectMapper.valueToTree(context.getInvocation()));
// 直接绑定查询参数,方便访问
if (context.getInvocation() != null && context.getInvocation().getRequestQueryParams() != null) {
environment.setVariable("query", objectMapper.valueToTree(context.getInvocation().getRequestQueryParams()));
} else {
environment.setVariable("query", objectMapper.valueToTree(java.util.Collections.emptyMap()));
}
} catch (EvaluateRuntimeException e) {
throw ServiceExceptionUtil.exception(API_JSONATA_BIND_FAILED);
}
}
}

View File

@@ -1,18 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.init;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* 在集成流程启动前执行网关编排所需的幂等数据调整。
*/
@Slf4j
@Component("gatewayPolicyMigration")
public class GatewayPolicyMigration {
@PostConstruct
public void migrate() {
log.info("[API-PORTAL] 跳过网关策略迁移,继续使用标准头部令牌认证");
}
}

View File

@@ -1,24 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.model;
import lombok.Builder;
import lombok.Value;
/**
* Standardized response wrapper returned to external clients.
*/
@Value
@Builder
public class ApiGatewayResponse {
/**
* HTTP status code returned by the gateway. Always aligns with the response status line.
*/
int code;
String message;
Object response;
String traceId;
}

View File

@@ -1,123 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.model;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.Instant;
import java.util.*;
/**
* Runtime context for an API invocation flowing through the integration pipeline.
*/
@Getter
@Setter
@ToString
public class ApiInvocationContext {
private final String requestId;
private final Instant requestTime;
private String apiCode;
private String apiVersion;
private String tenantId;
private String clientIp;
private String userAgent;
private String credentialAppId;
private Long credentialId;
private String httpMethod;
private String requestPath;
private Map<String, Object> requestHeaders;
private Object requestBody;
private Map<String, Object> requestQueryParams;
private Map<String, Object> variables;
private Map<String, Object> attributes;
private List<ApiStepResult> stepResults;
private Object responseBody;
private Integer responseStatus;
private String responseMessage;
public ApiInvocationContext() {
this.requestId = UUID.randomUUID().toString();
this.requestTime = Instant.now();
this.variables = new HashMap<>();
this.attributes = new HashMap<>();
this.stepResults = new ArrayList<>();
this.requestHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
this.requestQueryParams = new HashMap<>();
}
public static ApiInvocationContext create() {
return new ApiInvocationContext();
}
public ApiInvocationContext copy() {
ApiInvocationContext copy = new ApiInvocationContext();
copy.apiCode = this.apiCode;
copy.apiVersion = this.apiVersion;
copy.tenantId = this.tenantId;
copy.clientIp = this.clientIp;
copy.userAgent = this.userAgent;
copy.credentialAppId = this.credentialAppId;
copy.credentialId = this.credentialId;
copy.httpMethod = this.httpMethod;
copy.requestPath = this.requestPath;
copy.requestBody = this.requestBody;
copy.requestQueryParams.putAll(this.requestQueryParams);
copy.responseBody = this.responseBody;
copy.responseStatus = this.responseStatus;
copy.responseMessage = this.responseMessage;
copy.getRequestHeaders().putAll(this.requestHeaders);
copy.getVariables().putAll(this.variables);
copy.getAttributes().putAll(this.attributes);
return copy;
}
public void addStepResult(ApiStepResult result) {
this.stepResults.add(result);
}
public ApiStepResult lastStepResult() {
if (stepResults.isEmpty()) {
return null;
}
return stepResults.get(stepResults.size() - 1);
}
public void merge(ApiInvocationContext other) {
if (other == null) {
return;
}
this.stepResults.addAll(other.getStepResults());
this.variables.putAll(other.getVariables());
this.attributes.putAll(other.getAttributes());
if (other.getResponseBody() != null) {
this.responseBody = other.getResponseBody();
}
if (other.getResponseStatus() != null) {
this.responseStatus = other.getResponseStatus();
}
if (other.getResponseMessage() != null) {
this.responseMessage = other.getResponseMessage();
}
}
}

View File

@@ -1,35 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.time.Duration;
/**
* Result of executing a single orchestration step.
*/
@Getter
@Setter
@ToString
@Builder(toBuilder = true)
public class ApiStepResult {
private Long stepId;
private String stepType;
private Object request;
private Object response;
private boolean success;
private Duration elapsed;
private String errorCode;
private String errorMessage;
}

View File

@@ -1,61 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.policy;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_RATE_LIMIT_EVALUATION_FAILED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_RATE_LIMIT_EXCEEDED;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Redis 支撑的基础限流评估器,支持固定窗口计数。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DefaultRateLimitPolicyEvaluator implements RateLimitPolicyEvaluator {
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {
};
private final ObjectMapper objectMapper;
private final StringRedisTemplate stringRedisTemplate;
@Override
public void evaluate(ApiDefinitionAggregate aggregate, ApiInvocationContext context) {
ApiPolicyRateLimitDO rateLimitDO = aggregate.getRateLimitPolicy();
if (rateLimitDO == null || !StringUtils.hasText(rateLimitDO.getConfig())) {
return;
}
try {
Map<String, Object> config = objectMapper.readValue(rateLimitDO.getConfig(), MAP_TYPE);
long limit = ((Number) config.getOrDefault("limit", 100)).longValue();
long windowSeconds = ((Number) config.getOrDefault("windowSeconds", 60)).longValue();
String key = String.format("databus:api:rl:%s:%s:%s", aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion(), context.getRequestHeaders().getOrDefault("X-Client-Id", "anonymous"));
Long counter = stringRedisTemplate.opsForValue().increment(key);
if (counter != null && counter == 1L) {
stringRedisTemplate.expire(key, Duration.ofSeconds(windowSeconds));
}
if (counter != null && counter > limit) {
throw ServiceExceptionUtil.exception(API_RATE_LIMIT_EXCEEDED);
}
} catch (JsonProcessingException | DataAccessException ex) {
log.error("API {} 的限流评估失败", aggregate.getDefinition().getApiCode(), ex);
throw ServiceExceptionUtil.exception(API_RATE_LIMIT_EVALUATION_FAILED);
}
}
}

View File

@@ -1,13 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.policy;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
/**
* Applies rate limiting decisions for the invocation.
*/
public interface RateLimitPolicyEvaluator {
void evaluate(ApiDefinitionAggregate aggregate, ApiInvocationContext context);
}

View File

@@ -1,176 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.security;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* 简单的报文缓存包装,便于后续处理链重复读取解密后的内容。
*/
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private final byte[] cachedBody;
private String characterEncoding;
private final Map<String, List<String>> additionalHeaders;
public CachedBodyHttpServletRequest(HttpServletRequest request, byte[] cachedBody) {
super(request);
this.cachedBody = cachedBody != null ? cachedBody : new byte[0];
this.characterEncoding = request.getCharacterEncoding();
this.additionalHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
}
@Override
public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException {
if (encoding != null) {
super.setCharacterEncoding(encoding);
}
this.characterEncoding = encoding;
}
@Override
public String getCharacterEncoding() {
return characterEncoding != null ? characterEncoding : StandardCharsets.UTF_8.name();
}
@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream delegate = new ByteArrayInputStream(cachedBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return delegate.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
// 保持空实现
}
@Override
public int read() {
return delegate.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
Charset charset = Charset.forName(getCharacterEncoding());
return new BufferedReader(new InputStreamReader(getInputStream(), charset));
}
@Override
public int getContentLength() {
return cachedBody.length;
}
@Override
public long getContentLengthLong() {
return cachedBody.length;
}
public byte[] getCachedBody() {
return cachedBody.clone();
}
public void setHeader(String name, String value) {
if (!StringUtils.hasText(name)) {
return;
}
if (!StringUtils.hasText(value)) {
removeHeader(name);
return;
}
additionalHeaders.put(name, new ArrayList<>(Collections.singletonList(value)));
}
public void setHeaderValues(String name, List<String> values) {
if (!StringUtils.hasText(name)) {
return;
}
if (CollectionUtils.isEmpty(values)) {
removeHeader(name);
return;
}
additionalHeaders.put(name, new ArrayList<>(values));
}
public void addHeader(String name, String value) {
if (!StringUtils.hasText(name) || !StringUtils.hasText(value)) {
return;
}
additionalHeaders.compute(name, (k, existing) -> {
List<String> list = existing == null ? new ArrayList<>() : new ArrayList<>(existing);
list.add(value);
return list;
});
}
public void removeHeader(String name) {
if (!StringUtils.hasText(name)) {
return;
}
additionalHeaders.remove(name);
}
@Override
public String getHeader(String name) {
if (StringUtils.hasText(name)) {
List<String> values = additionalHeaders.get(name);
if (!CollectionUtils.isEmpty(values)) {
return values.get(0);
}
}
return super.getHeader(name);
}
@Override
public Enumeration<String> getHeaders(String name) {
if (StringUtils.hasText(name)) {
List<String> custom = additionalHeaders.get(name);
// 如果自定义头已写入,则直接返回,避免与原始头部合并造成重复
if (!CollectionUtils.isEmpty(custom)) {
return Collections.enumeration(custom);
}
}
return super.getHeaders(name);
}
@Override
public Enumeration<String> getHeaderNames() {
// 使用忽略大小写的集合,避免父请求头名与自定义头名大小写不同而重复
Set<String> names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
Enumeration<String> parent = super.getHeaderNames();
while (parent.hasMoreElements()) {
names.add(parent.nextElement());
}
names.addAll(additionalHeaders.keySet());
return Collections.enumeration(names);
}
}

View File

@@ -1,167 +0,0 @@
package com.zt.plat.module.databus.framework.integration.gateway.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.module.databus.framework.integration.gateway.core.GatewayHeaderUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
/**
* Shared helpers for resolving JWT tokens from incoming payloads so debug flows
* and external HTTP requests stay consistent.
*/
public final class GatewayJwtResolver {
public static final String HEADER_ZT_AUTH_TOKEN = "ZT-Auth-Token";
private GatewayJwtResolver() {
}
public static String resolveJwtToken(HttpServletRequest request, ObjectMapper objectMapper) {
if (request == null) {
return null;
}
String token = extractTokenFromHeader(request.getHeaders(HEADER_ZT_AUTH_TOKEN), objectMapper);
if (!StringUtils.hasText(token)) {
token = extractTokenFromHeader(request.getHeaders(HttpHeaders.AUTHORIZATION), objectMapper);
}
if (!StringUtils.hasText(token)) {
token = normalizeTokenValue(request.getParameter("token"), objectMapper);
}
return token;
}
public static String resolveJwtToken(Map<String, Object> headers,
Map<String, Object> queryParams,
ObjectMapper objectMapper) {
String token = extractTokenFromMap(headers, HEADER_ZT_AUTH_TOKEN, objectMapper);
if (!StringUtils.hasText(token)) {
token = extractTokenFromMap(headers, HttpHeaders.AUTHORIZATION, objectMapper);
}
if (!StringUtils.hasText(token) && queryParams != null) {
token = normalizeTokenValue(queryParams.get("token"), objectMapper);
}
return token;
}
private static String extractTokenFromHeader(Enumeration<String> values, ObjectMapper objectMapper) {
if (values == null) {
return null;
}
while (values.hasMoreElements()) {
String candidate = normalizeTokenString(values.nextElement(), objectMapper);
if (StringUtils.hasText(candidate)) {
return candidate;
}
}
return null;
}
private static String extractTokenFromMap(Map<String, Object> headers, String headerName, ObjectMapper objectMapper) {
if (headers == null || !StringUtils.hasText(headerName)) {
return null;
}
for (Map.Entry<String, Object> entry : headers.entrySet()) {
if (!headerName.equalsIgnoreCase(entry.getKey())) {
continue;
}
String candidate = normalizeTokenValue(entry.getValue(), objectMapper);
if (StringUtils.hasText(candidate)) {
return candidate;
}
}
return null;
}
private static String normalizeTokenValue(Object value, ObjectMapper objectMapper) {
if (value == null) {
return null;
}
if (value instanceof String str) {
return normalizeTokenString(str, objectMapper);
}
if (value instanceof Map<?, ?> map) {
Object direct = map.get("token");
if (direct == null) {
direct = map.get("accessToken");
}
if (direct == null) {
direct = map.get("authToken");
}
if (direct == null) {
direct = map.get("jwt");
}
if (direct != null) {
String resolved = normalizeTokenValue(direct, objectMapper);
if (StringUtils.hasText(resolved)) {
return resolved;
}
}
for (Object entryValue : map.values()) {
String resolved = normalizeTokenValue(entryValue, objectMapper);
if (StringUtils.hasText(resolved)) {
return resolved;
}
}
return null;
}
if (value instanceof Iterable<?> iterable) {
Iterator<?> iterator = iterable.iterator();
while (iterator.hasNext()) {
String resolved = normalizeTokenValue(iterator.next(), objectMapper);
if (StringUtils.hasText(resolved)) {
return resolved;
}
}
return null;
}
if (value.getClass().isArray()) {
int length = Array.getLength(value);
for (int i = 0; i < length; i++) {
String resolved = normalizeTokenValue(Array.get(value, i), objectMapper);
if (StringUtils.hasText(resolved)) {
return resolved;
}
}
return null;
}
return normalizeTokenString(String.valueOf(value), objectMapper);
}
private static String normalizeTokenString(String rawValue, ObjectMapper objectMapper) {
if (!StringUtils.hasText(rawValue)) {
return null;
}
String candidate = rawValue.trim();
if (candidate.startsWith("\"") && candidate.endsWith("\"") && candidate.length() > 1) {
candidate = candidate.substring(1, candidate.length() - 1).trim();
}
if ((candidate.startsWith("{") && candidate.endsWith("}"))
|| (candidate.startsWith("[") && candidate.endsWith("]"))) {
candidate = parseStructuredToken(candidate, objectMapper);
}
if (!StringUtils.hasText(candidate)) {
return null;
}
return GatewayHeaderUtils.stripBearerPrefix(candidate);
}
private static String parseStructuredToken(String candidate, ObjectMapper objectMapper) {
if (objectMapper == null) {
return candidate;
}
try {
Object parsed = objectMapper.readValue(candidate, Object.class);
String resolved = normalizeTokenValue(parsed, objectMapper);
return StringUtils.hasText(resolved) ? resolved : null;
} catch (IOException ex) {
return candidate;
}
}
}

Some files were not shown because too many files have changed in this diff Show More