1.规范增量 SQL 文件命名

2.新增数据总线模块(未完成)
3.新增规则模块(未完成)
4.新增组织编码与外部系统组织编码映射关系表
5.补全 e 办单点登录回调逻辑
This commit is contained in:
chenbowen
2025-10-15 08:59:57 +08:00
parent 97bd87de55
commit c0dc0823b6
246 changed files with 11118 additions and 2749 deletions

View File

@@ -126,6 +126,63 @@
<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>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</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>
</dependencies>
<build>

View File

@@ -1,29 +0,0 @@
package com.zt.plat.module.databus.controller.admin.databus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.zt.plat.framework.common.pojo.CommonResult;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
/**
* Databus 控制器
*
* @author ZT
*/
@Tag(name = "管理后台 - Databus")
@RestController
@RequestMapping("/admin/databus/databus")
public class DatabusController {
@GetMapping("/hello")
@Operation(summary = "Hello Databus")
public CommonResult<String> hello() {
return success("Hello, Databus!");
}
}

View File

@@ -0,0 +1,79 @@
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

@@ -0,0 +1,107 @@
package com.zt.plat.module.databus.controller.admin.gateway;
import com.zt.plat.framework.common.exception.ServiceException;
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.ApiFlowDispatcher;
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.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.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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 ApiFlowDispatcher apiFlowDispatcher;
private final ApiDefinitionService apiDefinitionService;
@PostMapping(value = "/invoke", consumes = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "测试调用 API 编排")
public CommonResult<ApiGatewayResponse> invoke(@RequestBody ApiGatewayInvokeReqVO reqVO) {
ApiInvocationContext context = ApiInvocationContext.create();
context.setApiCode(reqVO.getApiCode());
context.setApiVersion(reqVO.getVersion());
context.setRequestBody(reqVO.getPayload());
if (reqVO.getHeaders() != null) {
context.getRequestHeaders().putAll(reqVO.getHeaders());
}
if (reqVO.getQueryParams() != null) {
context.getRequestQueryParams().putAll(reqVO.getQueryParams());
}
ApiInvocationContext responseContext = context;
try {
responseContext = apiFlowDispatcher.dispatch(reqVO.getApiCode(), reqVO.getVersion(), context);
} catch (ServiceException ex) {
handleServiceException(responseContext, ex);
} catch (Exception ex) {
handleUnexpectedException(responseContext, ex);
}
int status = responseContext.getResponseStatus() != null ? responseContext.getResponseStatus() : HttpStatus.OK.value();
String message = StringUtils.hasText(responseContext.getResponseMessage())
? responseContext.getResponseMessage()
: HttpStatus.valueOf(status).getReasonPhrase();
ApiGatewayResponse envelope = ApiGatewayResponse.builder()
.code(status >= 200 && status < 400 ? "SUCCESS" : "ERROR")
.message(message)
.data(responseContext.getResponseBody())
.traceId(responseContext.getRequestId())
.build();
return success(envelope);
}
@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);
}
private void handleServiceException(ApiInvocationContext context, ServiceException ex) {
String message = StringUtils.hasText(ex.getMessage()) ? ex.getMessage() : "API 调用失败";
context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
context.setResponseMessage(message);
Map<String, Object> body = new HashMap<>();
if (ex.getCode() != null) {
body.put("errorCode", ex.getCode());
}
body.put("errorMessage", message);
context.setResponseBody(body);
}
private void handleUnexpectedException(ApiInvocationContext context, Exception ex) {
String message = StringUtils.hasText(ex.getMessage())
? ex.getMessage()
: ex.getCause() != null && StringUtils.hasText(ex.getCause().getMessage())
? ex.getCause().getMessage()
: "API invocation encountered an unexpected error";
context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
context.setResponseMessage(message);
Map<String, Object> body = new HashMap<>();
body.put("errorMessage", message);
body.put("exception", ex.getClass().getSimpleName());
context.setResponseBody(body);
}
}

View File

@@ -0,0 +1,84 @@
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.ApiPolicyAuthConvert;
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.ApiPolicyAuthDO;
import com.zt.plat.module.databus.service.gateway.ApiPolicyAuthService;
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/auth")
@RequiredArgsConstructor
@Validated
public class ApiPolicyAuthController {
private final ApiPolicyAuthService authService;
@GetMapping("/page")
@Operation(summary = "分页查询认证策略")
public CommonResult<PageResult<ApiPolicyRespVO>> getAuthPolicyPage(@Valid ApiPolicyPageReqVO reqVO) {
PageResult<ApiPolicyAuthDO> pageResult = authService.getPage(reqVO);
return success(ApiPolicyAuthConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/{id}")
@Operation(summary = "查询认证策略详情")
public CommonResult<ApiPolicyRespVO> getAuthPolicy(@PathVariable("id") Long id) {
ApiPolicyAuthDO policy = authService.get(id)
.orElseThrow(() -> ServiceExceptionUtil.exception(API_POLICY_NOT_FOUND));
return success(ApiPolicyAuthConvert.INSTANCE.convert(policy));
}
@GetMapping("/simple-list")
@Operation(summary = "获取认证策略精简列表")
public CommonResult<List<ApiPolicySimpleRespVO>> getAuthPolicySimpleList() {
List<ApiPolicyAuthDO> list = authService.getSimpleList();
return success(ApiPolicyAuthConvert.INSTANCE.convertSimpleList(list));
}
@PostMapping
@Operation(summary = "创建认证策略")
public CommonResult<Long> createAuthPolicy(@Valid @RequestBody ApiPolicySaveReqVO reqVO) {
Long id = authService.create(reqVO);
return success(id);
}
@PutMapping
@Operation(summary = "更新认证策略")
public CommonResult<Boolean> updateAuthPolicy(@Valid @RequestBody ApiPolicySaveReqVO reqVO) {
authService.update(reqVO);
return success(Boolean.TRUE);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除认证策略")
public CommonResult<Boolean> deleteAuthPolicy(@PathVariable("id") Long id) {
authService.delete(id);
return success(Boolean.TRUE);
}
}

View File

@@ -0,0 +1,84 @@
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

@@ -0,0 +1,104 @@
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.ApiDefinitionDetailRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionPublicationRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionStepRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSummaryRespVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionTransformRespVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
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.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()));
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);
}
}

View File

@@ -0,0 +1,25 @@
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.ApiPolicyAuthDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper
public interface ApiPolicyAuthConvert {
ApiPolicyAuthConvert INSTANCE = Mappers.getMapper(ApiPolicyAuthConvert.class);
ApiPolicyRespVO convert(ApiPolicyAuthDO bean);
List<ApiPolicyRespVO> convertList(List<ApiPolicyAuthDO> list);
PageResult<ApiPolicyRespVO> convertPage(PageResult<ApiPolicyAuthDO> page);
List<ApiPolicySimpleRespVO> convertSimpleList(List<ApiPolicyAuthDO> list);
}

View File

@@ -0,0 +1,25 @@
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

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,74 @@
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 = "URI 模板", example = "/external/order/create")
private String uriPattern;
@Schema(description = "状态", example = "1")
private Integer status;
@Schema(description = "是否灰度")
private Boolean greyReleased;
@Schema(description = "描述")
private String description;
@Schema(description = "认证策略编号")
private Long authPolicyId;
@Schema(description = "限流策略编号")
private Long rateLimitId;
@Schema(description = "响应模板(JSON)")
private String responseTemplate;
@Schema(description = "缓存策略(JSON)")
private String cacheStrategy;
@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 = "步骤列表")
private List<ApiDefinitionStepRespVO> steps = new ArrayList<>();
@Schema(description = "发布信息")
private ApiDefinitionPublicationRespVO publication;
}

View File

@@ -0,0 +1,25 @@
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 = "关键字,匹配编码/描述/URI", example = "order")
private String keyword;
@Schema(description = "API 状态", example = "1")
private Integer status;
@Schema(description = "HTTP 方法", example = "POST")
private String httpMethod;
@Schema(description = "是否灰度", example = "true")
private Boolean greyReleased;
}

View File

@@ -0,0 +1,28 @@
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

@@ -0,0 +1,67 @@
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 = "URI 模板", example = "/external/order/create")
@NotBlank(message = "URI 模板不能为空")
private String uriPattern;
@Schema(description = "API 状态", example = "1")
@NotNull(message = "API 状态不能为空")
private Integer status;
@Schema(description = "描述")
private String description;
@Schema(description = "认证策略编号")
private Long authPolicyId;
@Schema(description = "限流策略编号")
private Long rateLimitId;
@Schema(description = "响应模板(JSON)")
private String responseTemplate;
@Schema(description = "缓存策略(JSON)")
private String cacheStrategy;
@Schema(description = "是否开启灰度发布")
private Boolean greyReleased;
@Schema(description = "API 级别变换列表")
@Valid
private List<ApiDefinitionTransformSaveReqVO> apiLevelTransforms = new ArrayList<>();
@Schema(description = "步骤列表")
@NotEmpty(message = "编排步骤不能为空")
@Valid
private List<ApiDefinitionStepSaveReqVO> steps = new ArrayList<>();
}

View File

@@ -0,0 +1,55 @@
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 retryStrategy;
@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

@@ -0,0 +1,58 @@
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 = "超时时间(毫秒)", example = "5000")
private Long timeout;
@Schema(description = "重试策略(JSON)")
private String retryStrategy;
@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

@@ -0,0 +1,48 @@
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 = "URI 模板", example = "/external/order/create")
private String uriPattern;
@Schema(description = "状态", example = "1")
private Integer status;
@Schema(description = "是否灰度", example = "true")
private Boolean greyReleased;
@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

@@ -0,0 +1,31 @@
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

@@ -0,0 +1,29 @@
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

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,22 @@
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

@@ -0,0 +1,32 @@
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

@@ -0,0 +1,18 @@
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

@@ -0,0 +1,25 @@
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

@@ -0,0 +1,52 @@
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 uriPattern;
private String httpMethod;
private String version;
/**
* API status, see {@code ApiPublishStatusEnum}.
*/
private Integer status;
private String description;
private Long authPolicyId;
private Long rateLimitId;
private String responseTemplate;
private String cacheStrategy;
private LocalDateTime updatedAt;
private Boolean greyReleased;
}

View File

@@ -0,0 +1,35 @@
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

@@ -0,0 +1,31 @@
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;
/**
* Authentication policy definition.
*/
@TableName("databus_policy_auth")
@KeySequence("databus_policy_auth_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class ApiPolicyAuthDO extends TenantBaseDO {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private String type;
private String config;
private String description;
}

View File

@@ -0,0 +1,31 @@
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

@@ -0,0 +1,49 @@
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 retryStrategy;
private String fallbackStrategy;
private String conditionExpr;
private Boolean stopOnError;
}

View File

@@ -0,0 +1,35 @@
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

@@ -0,0 +1,63 @@
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)
.or().like(ApiDefinitionDO::getUriPattern, keyword));
}
query.eqIfPresent(ApiDefinitionDO::getStatus, reqVO.getStatus())
.eqIfPresent(ApiDefinitionDO::getHttpMethod, reqVO.getHttpMethod())
// .eqIfPresent(ApiDefinitionDO::getGreyReleased, reqVO.getGreyReleased())
.orderByDesc(ApiDefinitionDO::getUpdateTime)
.orderByDesc(ApiDefinitionDO::getId);
return selectPage(reqVO, query);
}
default Long selectCountByAuthPolicyId(Long policyId) {
if (policyId == null) {
return 0L;
}
return selectCount(new LambdaQueryWrapperX<ApiDefinitionDO>()
.eq(ApiDefinitionDO::getAuthPolicyId, policyId)
.eq(ApiDefinitionDO::getDeleted, false));
}
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

@@ -0,0 +1,19 @@
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

@@ -0,0 +1,36 @@
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.ApiPolicyAuthDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiPolicyAuthMapper extends BaseMapperX<ApiPolicyAuthDO> {
default PageResult<ApiPolicyAuthDO> selectPage(ApiPolicyPageReqVO reqVO) {
LambdaQueryWrapperX<ApiPolicyAuthDO> query = new LambdaQueryWrapperX<>();
if (StrUtil.isNotBlank(reqVO.getKeyword())) {
String keyword = reqVO.getKeyword();
query.and(wrapper -> wrapper.like(ApiPolicyAuthDO::getName, keyword)
.or().like(ApiPolicyAuthDO::getDescription, keyword));
}
query.eqIfPresent(ApiPolicyAuthDO::getType, reqVO.getType())
.eq(ApiPolicyAuthDO::getDeleted, false)
.orderByDesc(ApiPolicyAuthDO::getUpdateTime)
.orderByDesc(ApiPolicyAuthDO::getId);
return selectPage(reqVO, query);
}
default List<ApiPolicyAuthDO> selectSimpleList() {
return selectList(new LambdaQueryWrapperX<ApiPolicyAuthDO>()
.eq(ApiPolicyAuthDO::getDeleted, false)
.orderByDesc(ApiPolicyAuthDO::getUpdateTime)
.orderByDesc(ApiPolicyAuthDO::getId));
}
}

View File

@@ -0,0 +1,36 @@
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

@@ -0,0 +1,25 @@
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

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,28 @@
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;
public static boolean isOnline(Integer status) {
return status != null && status == ONLINE.status;
}
public static boolean isDeprecated(Integer status) {
return status != null && status == DEPRECATED.status;
}
}

View File

@@ -0,0 +1,18 @@
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 {
HTTP,
RPC,
SCRIPT,
FLOW;
}

View File

@@ -0,0 +1,32 @@
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

@@ -0,0 +1,28 @@
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

@@ -0,0 +1,36 @@
package com.zt.plat.module.databus.framework.integration.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
/**
* Configuration properties for the unified API portal.
*/
@Data
@ConfigurationProperties(prefix = "databus.api-portal")
public class ApiGatewayProperties {
private String basePath = "/api/portal";
private List<String> allowedIps = new ArrayList<>();
private List<String> deniedIps = new ArrayList<>();
private boolean enableSignature = false;
private String signatureHeader = "X-Signature";
private String signatureSecret;
private boolean enableTenantHeader = true;
private String tenantHeader = "X-Tenant-Id";
private boolean enableAudit = true;
private boolean enableRateLimit = true;
}

View File

@@ -0,0 +1,25 @@
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

@@ -0,0 +1,147 @@
package com.zt.plat.module.databus.framework.integration.config;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiFlowDispatcher;
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayRequestMapper;
import com.zt.plat.module.databus.framework.integration.gateway.core.ErrorHandlingStrategy;
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.GatewaySecurityFilter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
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.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.http.dsl.Http;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* Configures the unified API portal inbound gateway and supporting beans.
*/
@Slf4j
@Configuration
@EnableConfigurationProperties(ApiGatewayProperties.class)
@RequiredArgsConstructor
public class GatewayIntegrationConfiguration {
private final ApiGatewayProperties properties;
private final ApiGatewayRequestMapper requestMapper;
private final ObjectProvider<ApiFlowDispatcher> apiFlowDispatcherProvider;
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() {
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(this, "mapRequest", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()))
.handle(this, "dispatch", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()))
.handle(this, "buildResponse", endpoint -> endpoint.advice(errorHandlingStrategy.errorForwardingAdvice()))
.get();
}
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();
}
public ApiInvocationContext dispatch(Message<ApiInvocationContext> message) {
ApiInvocationContext context = message.getPayload();
try {
return apiFlowDispatcherProvider.getObject()
.dispatch(context.getApiCode(), context.getApiVersion(), context);
} catch (ServiceException ex) {
handleServiceException(context, ex);
log.warn("[API-PORTAL] ServiceException while dispatching apiCode={} version={}: {}", context.getApiCode(), context.getApiVersion(), ex.getMessage());
return context;
} catch (Exception ex) {
handleUnexpectedException(context, ex);
log.error("[API-PORTAL] Unexpected exception while dispatching apiCode={} version={}", context.getApiCode(), context.getApiVersion(), ex);
return context;
}
}
public ResponseEntity<ApiGatewayResponse> buildResponse(ApiInvocationContext context) {
int status = context.getResponseStatus() != null ? context.getResponseStatus() : HttpStatus.OK.value();
ApiGatewayResponse envelope = ApiGatewayResponse.builder()
.code(status >= 200 && status < 400 ? "SUCCESS" : "ERROR")
.message(StringUtils.hasText(context.getResponseMessage()) ? context.getResponseMessage() : HttpStatus.valueOf(status).getReasonPhrase())
.data(context.getResponseBody())
.traceId(context.getRequestId())
.build();
return ResponseEntity.status(status).body(envelope);
}
private void handleServiceException(ApiInvocationContext context, ServiceException ex) {
String message = StringUtils.hasText(ex.getMessage()) ? ex.getMessage() : "API invocation failed";
context.setResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
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);
}
}
private void handleUnexpectedException(ApiInvocationContext context, Exception ex) {
String message = StringUtils.hasText(ex.getMessage()) ? ex.getMessage() : "API invocation encountered an unexpected error";
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", ex.getClass().getSimpleName());
context.setResponseBody(body);
}
}
}

View File

@@ -0,0 +1,270 @@
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.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.step.StepHandlerFactory;
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.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_PARALLEL_FAILED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_PARALLEL_INTERRUPTED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_TRANSFORM_EVALUATION_FAILED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_TRANSFORM_RESPONSE_STATUS_INVALID;
/**
* Assembles dynamic integration flows per API definition.
*/
@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] entering flow %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) {
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);
applyTransformResult(payload, result);
} catch (Exception ex) {
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_TRANSFORM_EVALUATION_FAILED, ex.getMessage());
}
}
return payload;
};
}
private void applyTransformResult(ApiInvocationContext context, Object result) {
if (!(result instanceof Map<?, ?> map)) {
return;
}
Object headerUpdates = map.get("requestHeaders");
if (headerUpdates instanceof Map<?, ?> headerMap) {
headerMap.forEach((key, value) -> context.getRequestHeaders().put(String.valueOf(key), value));
}
Object variableUpdates = map.get("variables");
if (variableUpdates instanceof Map<?, ?> variables) {
variables.forEach((key, value) -> context.getVariables().put(String.valueOf(key), value));
}
Object attributeUpdates = map.get("attributes");
if (attributeUpdates instanceof Map<?, ?> attributes) {
attributes.forEach((key, value) -> context.getAttributes().put(String.valueOf(key), value));
}
if (map.containsKey("responseBody")) {
context.setResponseBody(map.get("responseBody"));
}
if (map.containsKey("responseStatus")) {
context.setResponseStatus(asInteger(map.get("responseStatus")));
}
if (map.containsKey("responseMessage")) {
Object message = map.get("responseMessage");
context.setResponseMessage(message == null ? null : String.valueOf(message));
}
}
private Integer asInteger(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.intValue();
}
try {
return Integer.parseInt(String.valueOf(value));
} catch (NumberFormatException ex) {
throw ServiceExceptionUtil.exception(API_TRANSFORM_RESPONSE_STATUS_INVALID, value);
}
}
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 {
}
private static final class SequentialSegment implements FlowSegment {
private final ApiStepDefinition step;
private SequentialSegment(ApiStepDefinition step) {
this.step = step;
}
public ApiStepDefinition getStep() {
return step;
}
}
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;
}
public String getGroup() {
return group;
}
public List<ApiStepDefinition> getSteps() {
return steps;
}
}
}

View File

@@ -0,0 +1,38 @@
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.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;
/**
* Dispatches API invocation contexts to the appropriate integration flow.
*/
@Component
@RequiredArgsConstructor
public class ApiFlowDispatcher {
private final IntegrationFlowManager integrationFlowManager;
private final MessagingTemplate messagingTemplate;
public ApiInvocationContext dispatch(String apiCode, String version, ApiInvocationContext context) {
MessageChannel channel = integrationFlowManager.locateInputChannel(apiCode, version)
.orElseThrow(() -> ServiceExceptionUtil.exception(API_FLOW_NOT_FOUND, 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();
}
}

View File

@@ -0,0 +1,20 @@
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.
*/
@Value
@Builder
public class ApiFlowRegistration {
String flowId;
String inputChannelName;
IntegrationFlow flow;
}

View File

@@ -0,0 +1,82 @@
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 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.StringUtils;
import org.springframework.web.servlet.HandlerMapping;
import java.io.IOException;
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";
@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) {
context.setApiCode(String.valueOf(uriVariables.get("apiCode")));
context.setApiVersion(String.valueOf(uriVariables.get("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);
}
if (requestPath != null) {
context.setRequestPath(String.valueOf(requestPath));
}
Map<String, Object> requestHeaders = (Map<String, Object>) headers.get(HEADER_REQUEST_HEADERS);
if (requestHeaders != null) {
requestHeaders.forEach((key, value) -> context.getRequestHeaders().put(key, String.valueOf(value)));
}
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("Failed to parse request body as JSON", ex);
context.setRequestBody(body);
}
} else {
context.setRequestBody(body);
}
} else {
context.setRequestBody(payload);
}
return context;
}
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);
}
}

View File

@@ -0,0 +1,73 @@
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;
/**
* Centralized error channel and handler for the API portal.
*/
@Slf4j
@Component
public class ErrorHandlingStrategy {
@Getter
private final MessageChannel errorChannel;
private final Advice errorForwardingAdvice;
public ErrorHandlingStrategy() {
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) {
context.setResponseStatus(500);
context.setResponseMessage(throwable.getMessage());
}
log.error("[API-PORTAL] Integration flow error", 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] Failed to forward error message to channel {}", errorChannel);
}
} catch (Exception sendEx) {
log.error("[API-PORTAL] Error while submitting message to error channel", sendEx);
}
throw ex;
}
}
}
}

View File

@@ -0,0 +1,96 @@
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.concurrent.ConcurrentHashMap;
/**
* Manages dynamic registration of API integration flows.
*/
@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());
}
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] registered flow {} for 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] deregistered flow {} for key {}", existing.getId(), key);
} catch (Exception ex) {
log.warn("Failed to remove integration flow {}", existing.getId(), ex);
}
}
}
private String key(String apiCode, String version) {
return (apiCode + ":" + version).toLowerCase();
}
}

View File

@@ -0,0 +1,54 @@
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;
/**
* Channel interceptor capturing timing metrics and enriched logging.
*/
@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] Channel send failed", ex);
}
}
}

View File

@@ -0,0 +1,166 @@
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.AuthPolicyEvaluator;
import com.zt.plat.module.databus.framework.integration.gateway.policy.RateLimitPolicyEvaluator;
import lombok.RequiredArgsConstructor;
import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice;
import org.springframework.integration.handler.advice.RequestHandlerRetryAdvice;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
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 AuthPolicyEvaluator authPolicyEvaluator;
private final RateLimitPolicyEvaluator rateLimitPolicyEvaluator;
public org.aopalliance.aop.Advice[] buildAdvices(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
List<org.aopalliance.aop.Advice> advices = new ArrayList<>();
advices.add(new AuthPolicyAdvice(aggregate));
advices.add(new RateLimitPolicyAdvice(aggregate));
advices.add(createRetryAdvice(stepDefinition));
return advices.stream().filter(advice -> advice != null).toArray(org.aopalliance.aop.Advice[]::new);
}
public org.aopalliance.aop.Advice[] buildParallelAdvices(ApiDefinitionAggregate aggregate, Object segment) {
// For parallel segments we reuse the same advice chain (auth + rateLimit once at entry)
return buildAdvices(aggregate, null);
}
private RequestHandlerRetryAdvice createRetryAdvice(ApiStepDefinition stepDefinition) {
if (stepDefinition == null) {
return null;
}
Object strategyConfig = stepDefinition.getMetadata().get("retryStrategy");
if (!(strategyConfig instanceof Map<?, ?> configMap)) {
return null;
}
RetryTemplate template = new RetryTemplate();
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
int maxAttempts = asInt(configMap.get("maxAttempts"), 3);
retryPolicy.setMaxAttempts(maxAttempts);
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
long initialInterval = asLong(configMap.get("initialInterval"), 200L);
double multiplier = asDouble(configMap.get("multiplier"), 2.0d);
long maxInterval = asLong(configMap.get("maxInterval"), 2000L);
backOffPolicy.setInitialInterval(initialInterval);
backOffPolicy.setMultiplier(multiplier);
backOffPolicy.setMaxInterval(maxInterval);
template.setBackOffPolicy(backOffPolicy);
template.setRetryPolicy(retryPolicy);
RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
advice.setRetryTemplate(template);
return advice;
}
private final class AuthPolicyAdvice extends AbstractRequestHandlerAdvice {
private final ApiDefinitionAggregate aggregate;
private AuthPolicyAdvice(ApiDefinitionAggregate aggregate) {
this.aggregate = aggregate;
}
@Override
protected Object doInvoke(ExecutionCallback callback, Object target, org.springframework.messaging.Message<?> message) {
if (aggregate.getAuthPolicy() != null) {
authPolicyEvaluator.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());
}
}
}
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());
}
}
}
private int asInt(Object value, int defaultValue) {
if (value instanceof Number number) {
return number.intValue();
}
if (value instanceof String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException ignored) {
// ignore and fall back to default
}
}
return defaultValue;
}
private long asLong(Object value, long defaultValue) {
if (value instanceof Number number) {
return number.longValue();
}
if (value instanceof String text) {
try {
return Long.parseLong(text);
} catch (NumberFormatException ignored) {
// ignore and fall back to default
}
}
return defaultValue;
}
private double asDouble(Object value, double defaultValue) {
if (value instanceof Number number) {
return number.doubleValue();
}
if (value instanceof String text) {
try {
return Double.parseDouble(text);
} catch (NumberFormatException ignored) {
// ignore and fall back to default
}
}
return defaultValue;
}
}

View File

@@ -0,0 +1,40 @@
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.ApiPolicyAuthDO;
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;
ApiPolicyAuthDO authPolicy;
ApiPolicyRateLimitDO rateLimitPolicy;
ApiFlowPublication publication;
public List<ApiStepDefinition> getSteps() {
return steps == null ? Collections.emptyList() : steps;
}
public Map<String, ApiTransformDefinition> getApiLevelTransforms() {
return apiLevelTransforms == null ? Collections.emptyMap() : apiLevelTransforms;
}
}

View File

@@ -0,0 +1,25 @@
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

@@ -0,0 +1,32 @@
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

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,24 @@
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

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,28 @@
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

@@ -0,0 +1,51 @@
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

@@ -0,0 +1,21 @@
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

@@ -0,0 +1,32 @@
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

@@ -0,0 +1,13 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
/**
* Legacy placeholder kept for binary compatibility. JSR-223 scripts are no longer supported.
*/
@Deprecated(forRemoval = true)
public class JsScriptExpressionEvaluator implements ExpressionEvaluator {
@Override
public Object evaluate(String expression, ExpressionEvaluationContext context) {
throw new UnsupportedOperationException("JSR-223 script expressions are no longer supported. Use JSON expressions instead.");
}
}

View File

@@ -0,0 +1,50 @@
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()));
} catch (EvaluateRuntimeException e) {
throw ServiceExceptionUtil.exception(API_JSONATA_BIND_FAILED);
}
}
}

View File

@@ -0,0 +1,13 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
/**
* Legacy placeholder kept for binary compatibility. MVEL evaluation is no longer supported.
*/
@Deprecated(forRemoval = true)
public class MvelExpressionEvaluator implements ExpressionEvaluator {
@Override
public Object evaluate(String expression, ExpressionEvaluationContext context) {
throw new UnsupportedOperationException("MVEL expressions are no longer supported. Use JSON expressions instead.");
}
}

View File

@@ -0,0 +1,13 @@
package com.zt.plat.module.databus.framework.integration.gateway.expression;
/**
* Legacy placeholder kept for binary compatibility. SpEL evaluation is no longer supported.
*/
@Deprecated(forRemoval = true)
public class SpelExpressionEvaluator implements ExpressionEvaluator {
@Override
public Object evaluate(String expression, ExpressionEvaluationContext context) {
throw new UnsupportedOperationException("SpEL expressions are no longer supported. Use JSON expressions instead.");
}
}

View File

@@ -0,0 +1,19 @@
package com.zt.plat.module.databus.framework.integration.gateway.init;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* Applies idempotent data adjustments required for gateway orchestration features
* before integration flows bootstrap.
*/
@Slf4j
@Component("gatewayPolicyMigration")
public class GatewayPolicyMigration {
@PostConstruct
public void migrate() {
log.info("[API-PORTAL] gateway policy migration skipped; standard header token auth in use");
}
}

View File

@@ -0,0 +1,21 @@
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 {
String code;
String message;
Object data;
String traceId;
}

View File

@@ -0,0 +1,115 @@
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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* 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 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 HashMap<>();
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.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

@@ -0,0 +1,35 @@
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

@@ -0,0 +1,13 @@
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;
/**
* Performs authentication / authorization policy evaluation for a request.
*/
public interface AuthPolicyEvaluator {
void evaluate(ApiDefinitionAggregate aggregate, ApiInvocationContext context);
}

View File

@@ -0,0 +1,56 @@
package com.zt.plat.module.databus.framework.integration.gateway.policy;
import com.zt.plat.framework.common.biz.system.oauth2.OAuth2TokenCommonApi;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO;
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.stereotype.Component;
import org.springframework.util.StringUtils;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_AUTH_UNAUTHORIZED;
/**
* Basic authentication evaluator delegating token validation to system module.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DefaultAuthPolicyEvaluator implements AuthPolicyEvaluator {
private static final String TOKEN_HEADER = "ZT-Auth-Token";
private final OAuth2TokenCommonApi oauth2TokenCommonApi;
@Override
public void evaluate(ApiDefinitionAggregate aggregate, ApiInvocationContext context) {
ApiPolicyAuthDO authPolicy = aggregate.getAuthPolicy();
if (authPolicy == null) {
return;
}
validateHeaderToken(context);
}
private void validateHeaderToken(ApiInvocationContext context) {
Object rawHeader = context.getRequestHeaders().get(TOKEN_HEADER);
String token = rawHeader == null ? null : String.valueOf(rawHeader).trim();
if (!StringUtils.hasText(token)) {
throw ServiceExceptionUtil.exception(API_AUTH_UNAUTHORIZED);
}
try {
oauth2TokenCommonApi.checkAccessToken(token).getCheckedData();
context.getAttributes().putIfAbsent("accessToken", token);
String bearerToken = token.startsWith("Bearer ") ? token : "Bearer " + token;
context.getRequestHeaders().putIfAbsent("Authorization", bearerToken);
} catch (ServiceException ex) {
log.warn("Access token validation failed: {}", ex.getMessage());
throw ServiceExceptionUtil.exception(API_AUTH_UNAUTHORIZED);
} catch (RuntimeException ex) {
log.error("Access token validation error", ex);
throw ServiceExceptionUtil.exception(API_AUTH_UNAUTHORIZED);
}
}
}

View File

@@ -0,0 +1,61 @@
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;
/**
* Simple Redis-backed rate limit evaluator supporting fixed window counters.
*/
@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("Rate limit evaluation failed for api {}", aggregate.getDefinition().getApiCode(), ex);
throw ServiceExceptionUtil.exception(API_RATE_LIMIT_EVALUATION_FAILED);
}
}
}

View File

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,75 @@
package com.zt.plat.module.databus.framework.integration.gateway.security;
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.HmacUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
/**
* Security filter performing IP allow/deny, signature validation, and tenant extraction for the unified portal.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class GatewaySecurityFilter extends OncePerRequestFilter {
private final ApiGatewayProperties properties;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String requestPath = request.getRequestURI();
if (!pathMatcher.match(properties.getBasePath() + "/**", requestPath)) {
filterChain.doFilter(request, response);
return;
}
if (!isIpAllowed(request)) {
response.sendError(HttpStatus.FORBIDDEN.value(), "IP not allowed");
return;
}
if (properties.isEnableSignature() && !validateSignature(request)) {
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Invalid signature");
return;
}
filterChain.doFilter(request, response);
}
private boolean isIpAllowed(HttpServletRequest request) {
String remoteIp = request.getRemoteAddr();
List<String> denied = properties.getDeniedIps();
if (!CollectionUtils.isEmpty(denied) && denied.contains(remoteIp)) {
return false;
}
List<String> allowed = properties.getAllowedIps();
return CollectionUtils.isEmpty(allowed) || allowed.contains(remoteIp);
}
private boolean validateSignature(HttpServletRequest request) {
String headerSignature = request.getHeader(properties.getSignatureHeader());
if (!StringUtils.hasText(headerSignature)) {
return false;
}
String secret = properties.getSignatureSecret();
if (!StringUtils.hasText(secret)) {
log.warn("Signature verification enabled but no secret configured");
return false;
}
String payload = request.getRequestURI() + "|" + (request.getQueryString() == null ? "" : request.getQueryString());
String computed = HmacUtils.hmacSha256Hex(secret, payload);
return headerSignature.equalsIgnoreCase(computed);
}
}

View File

@@ -0,0 +1,17 @@
package com.zt.plat.module.databus.framework.integration.gateway.step;
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 org.springframework.integration.core.GenericHandler;
/**
* Contract for building a Spring Integration handler for a specific step type.
*/
public interface ApiStepHandler {
boolean supports(String stepType);
GenericHandler<ApiInvocationContext> build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition);
}

View File

@@ -0,0 +1,34 @@
package com.zt.plat.module.databus.framework.integration.gateway.step;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.databus.enums.gateway.ApiStepTypeEnum;
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 lombok.RequiredArgsConstructor;
import org.springframework.integration.core.GenericHandler;
import org.springframework.stereotype.Component;
import java.util.List;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_UNSUPPORTED_TYPE;
/**
* Delegates step handler creation to registered implementations.
*/
@Component
@RequiredArgsConstructor
public class StepHandlerFactory {
private final List<ApiStepHandler> stepHandlers;
public GenericHandler<ApiInvocationContext> build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
ApiStepTypeEnum type = ApiStepTypeEnum.valueOf(stepDefinition.getStep().getType().toUpperCase());
return stepHandlers.stream()
.filter(handler -> handler.supports(type.name()))
.findFirst()
.orElseThrow(() -> ServiceExceptionUtil.exception(API_STEP_UNSUPPORTED_TYPE, type.name()))
.build(aggregate, stepDefinition);
}
}

View File

@@ -0,0 +1,395 @@
package com.zt.plat.module.databus.framework.integration.gateway.step.impl;
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.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.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiStepResult;
import com.zt.plat.module.databus.framework.integration.gateway.step.ApiStepHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.integration.core.GenericHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_HTTP_ENDPOINT_INVALID;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_HTTP_EXECUTION_FAILED;
/**
* Step handler that performs outbound HTTP calls.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class HttpStepHandler implements ApiStepHandler {
private final WebClient.Builder webClientBuilder;
private final ExpressionExecutor expressionExecutor;
private static final Set<String> DEFAULT_FORWARDED_HEADERS = Set.of(
"authorization",
"zt-auth-token",
"tenant-id",
"visit-tenant-id",
"visit-company-id",
"visit-company-name",
"visit-dept-id",
"visit-dept-name"
);
@Override
public boolean supports(String stepType) {
return "HTTP".equalsIgnoreCase(stepType);
}
private HttpRequestPayload coerceRequestPayload(Object evaluated, Object fallbackBody, Map<String, Object> fallbackQuery) {
Map<String, Object> querySnapshot = new LinkedHashMap<>(fallbackQuery);
if (evaluated == null) {
return HttpRequestPayload.of(fallbackBody, querySnapshot);
}
if (evaluated instanceof HttpRequestPayload payload) {
Map<String, Object> mergedQuery = new LinkedHashMap<>(fallbackQuery);
mergedQuery.putAll(payload.queryParams());
return HttpRequestPayload.of(payload.body(), mergedQuery);
}
if (evaluated instanceof MultiValueMap<?, ?> multiValueMap) {
mergeQueryParams(querySnapshot, multiValueMap);
return HttpRequestPayload.of(fallbackBody, querySnapshot);
}
if (evaluated instanceof Map<?, ?> map) {
Object queryPart = extractCaseInsensitive(map, "query", "queryParams", "params");
if (queryPart != null) {
mergeQueryParams(querySnapshot, queryPart);
}
boolean explicitBody = containsKeyIgnoreCase(map, "body", "payload");
Object body = explicitBody
? Optional.ofNullable(extractCaseInsensitive(map, "body", "payload")).orElse(fallbackBody)
: (queryPart != null ? fallbackBody : evaluated);
if (!explicitBody && queryPart == null) {
return HttpRequestPayload.of(evaluated, querySnapshot);
}
return HttpRequestPayload.of(body, querySnapshot);
}
return HttpRequestPayload.of(evaluated, querySnapshot);
}
@Override
public GenericHandler<ApiInvocationContext> build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
return (payload, headers) -> {
Instant start = Instant.now();
HttpRequestPayload requestPayload = null;
boolean supportsBody = false;
try {
HttpCallSpec callSpec = parseEndpoint(stepDefinition.getStep().getTargetEndpoint());
supportsBody = supportsRequestBody(callSpec.method);
requestPayload = mapRequest(stepDefinition, payload, headers);
if (!supportsBody && requestPayload != null && requestPayload.body() != null) {
requestPayload = HttpRequestPayload.of(null, requestPayload.queryParams());
}
Map<String, String> headerMap = resolveHeaders(stepDefinition, payload);
Duration timeout = resolveTimeout(stepDefinition);
WebClient client = webClientBuilder.build();
WebClient.RequestHeadersSpec<?> requestSpec = buildRequest(client, callSpec, requestPayload, headerMap, supportsBody);
Mono<Object> responseMono = requestSpec.retrieve().bodyToMono(Object.class);
Object response = timeout == null ? responseMono.block() : responseMono.block(timeout);
payload.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
.request(requestPayload == null ? null : requestPayload.snapshot(supportsBody))
.response(response)
.success(true)
.elapsed(Duration.between(start, Instant.now()))
.build());
applyResponseMapping(stepDefinition, payload, headers, response);
} catch (Exception ex) {
payload.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
.request(requestPayload == null ? null : requestPayload.snapshot(supportsBody))
.success(false)
.errorMessage(ex.getMessage())
.elapsed(Duration.between(start, Instant.now()))
.build());
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_STEP_HTTP_EXECUTION_FAILED, ex.getMessage());
}
return payload;
};
}
private HttpRequestPayload mapRequest(ApiStepDefinition stepDefinition, ApiInvocationContext context, Map<String, Object> headers) throws Exception {
ExpressionSpec spec = ExpressionSpecParser.parse(stepDefinition.getStep().getRequestMappingExpr(), ExpressionTypeEnum.JSON);
Map<String, Object> baseQuery = new LinkedHashMap<>(context.getRequestQueryParams());
Object fallbackBody = context.getRequestBody();
if (spec == null) {
return HttpRequestPayload.of(fallbackBody, baseQuery);
}
Object evaluated = expressionExecutor.evaluate(spec, context, fallbackBody, headers);
return coerceRequestPayload(evaluated, fallbackBody, baseQuery);
}
private void applyResponseMapping(ApiStepDefinition stepDefinition, ApiInvocationContext context, Map<String, Object> headers, Object response) throws Exception {
ExpressionSpec spec = ExpressionSpecParser.parse(stepDefinition.getStep().getResponseMappingExpr(), ExpressionTypeEnum.JSON);
if (spec == null) {
context.setResponseBody(response);
return;
}
Object mapped = expressionExecutor.evaluate(spec, context, response, headers);
if (mapped instanceof Map<?, ?> map) {
map.forEach((key, value) -> context.getVariables().put(String.valueOf(key), value));
} else {
context.setResponseBody(mapped);
}
}
private Map<String, String> resolveHeaders(ApiStepDefinition stepDefinition, ApiInvocationContext context) throws Exception {
Map<String, String> resolved = new LinkedHashMap<>();
context.getRequestHeaders().forEach((key, value) -> {
if (shouldForwardHeader(key) && value != null) {
resolved.put(key, String.valueOf(value));
}
});
Map<String, String> configured = extractConfiguredHeaders(stepDefinition, context);
resolved.putAll(configured);
return resolved;
}
private Map<String, String> extractConfiguredHeaders(ApiStepDefinition stepDefinition, ApiInvocationContext context) throws Exception {
Object headerConfig = stepDefinition.getMetadata().getOrDefault("headers", Collections.emptyMap());
if (headerConfig instanceof Map<?, ?> map) {
return toStringMap(map);
}
ExpressionSpec spec = ExpressionSpecParser.parse((String) stepDefinition.getMetadata().get("headerExpr"), ExpressionTypeEnum.JSON);
if (spec == null) {
return Collections.emptyMap();
}
Object evaluated = expressionExecutor.evaluate(spec, context, context.getRequestBody(), Collections.emptyMap());
if (evaluated instanceof Map<?, ?> map) {
return toStringMap(map);
}
return Collections.emptyMap();
}
private boolean shouldForwardHeader(String headerName) {
if (!StringUtils.hasText(headerName)) {
return false;
}
return DEFAULT_FORWARDED_HEADERS.contains(headerName.toLowerCase(Locale.ROOT));
}
private Map<String, String> toStringMap(Map<?, ?> map) {
if (map == null) {
return Collections.emptyMap();
}
Map<String, String> result = new java.util.LinkedHashMap<>();
map.forEach((key, value) -> {
if (value != null) {
result.put(String.valueOf(key), String.valueOf(value));
}
});
return result;
}
private Duration resolveTimeout(ApiStepDefinition stepDefinition) {
Long timeout = stepDefinition.getStep().getTimeout();
if (timeout == null || timeout <= 0) {
return Duration.ofSeconds(5);
}
return Duration.ofMillis(timeout);
}
private HttpCallSpec parseEndpoint(String targetEndpoint) {
if (!StringUtils.hasText(targetEndpoint)) {
throw ServiceExceptionUtil.exception(API_STEP_HTTP_ENDPOINT_INVALID);
}
String trimmed = targetEndpoint.trim();
String method = "POST";
String url = trimmed;
int spaceIndex = trimmed.indexOf(' ');
if (spaceIndex > 0) {
method = trimmed.substring(0, spaceIndex).toUpperCase();
url = trimmed.substring(spaceIndex + 1);
}
return new HttpCallSpec(HttpMethod.valueOf(method), url);
}
private record HttpCallSpec(HttpMethod method, String url) {
}
private void applyQueryParams(UriComponentsBuilder builder, Map<String, Object> queryParams) {
if (queryParams == null || queryParams.isEmpty()) {
return;
}
queryParams.forEach((key, value) -> addQueryParam(builder, key, value));
}
private void addQueryParam(UriComponentsBuilder builder, String key, Object value) {
if (!StringUtils.hasText(key)) {
return;
}
if (value == null) {
builder.queryParam(key);
return;
}
if (value instanceof MultiValueMap<?, ?> multiValueMap) {
multiValueMap.forEach((innerKey, values) -> addQueryParam(builder, String.valueOf(innerKey), values));
return;
}
if (value instanceof Iterable<?> iterable) {
iterable.forEach(item -> addQueryParam(builder, key, item));
return;
}
if (value.getClass().isArray()) {
int length = java.lang.reflect.Array.getLength(value);
for (int i = 0; i < length; i++) {
addQueryParam(builder, key, java.lang.reflect.Array.get(value, i));
}
return;
}
builder.queryParam(key, value);
}
private void mergeQueryParams(Map<String, Object> target, Object addition) {
if (addition == null || target == null) {
return;
}
if (addition instanceof MultiValueMap<?, ?> multiValueMap) {
multiValueMap.forEach((key, values) -> {
if (values == null) {
return;
}
if (values.size() == 1) {
target.put(String.valueOf(key), values.get(0));
return;
}
target.put(String.valueOf(key), new ArrayList<>(values));
});
return;
}
if (addition instanceof Map<?, ?> map) {
map.forEach((key, value) -> target.put(String.valueOf(key), value));
}
}
private WebClient.RequestHeadersSpec<?> buildRequest(WebClient client, HttpCallSpec callSpec, HttpRequestPayload requestPayload, Map<String, String> headerMap, boolean hasBody) {
URI uri = buildUri(callSpec, requestPayload, hasBody);
WebClient.RequestBodyUriSpec uriSpec = client.method(callSpec.method);
WebClient.RequestHeadersSpec<?> headersSpec = uriSpec.uri(uri)
.accept(MediaType.APPLICATION_JSON)
.headers(httpHeaders -> headerMap.forEach(httpHeaders::add));
if (hasBody) {
Object body = requestPayload.body() == null ? Collections.emptyMap() : requestPayload.body();
headersSpec = ((WebClient.RequestBodySpec) headersSpec)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(body));
}
return headersSpec;
}
private URI buildUri(HttpCallSpec callSpec, HttpRequestPayload requestPayload, boolean hasBody) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(callSpec.url);
Map<String, Object> queryParams = new LinkedHashMap<>(requestPayload.queryParams());
if (!hasBody) {
mergeQueryParams(queryParams, requestPayload.body());
}
applyQueryParams(builder, queryParams);
return builder.build(true).toUri();
}
private Object extractCaseInsensitive(Map<?, ?> source, String... keys) {
if (source == null || source.isEmpty()) {
return null;
}
for (String key : keys) {
for (Map.Entry<?, ?> entry : source.entrySet()) {
if (key.equalsIgnoreCase(String.valueOf(entry.getKey()))) {
return entry.getValue();
}
}
}
return null;
}
private 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;
}
private record HttpRequestPayload(Object body, Map<String, Object> queryParams) {
private HttpRequestPayload {
Map<String, Object> safeQuery = queryParams == null
? Collections.emptyMap()
: Collections.unmodifiableMap(new LinkedHashMap<>(queryParams));
queryParams = safeQuery;
}
static HttpRequestPayload of(Object body, Map<String, Object> queryParams) {
return new HttpRequestPayload(body, queryParams);
}
Object snapshot(boolean includeBody) {
boolean hasQuery = queryParams != null && !queryParams.isEmpty();
boolean hasBody = includeBody && body != null;
if (hasQuery && hasBody) {
Map<String, Object> composite = new LinkedHashMap<>();
composite.put("query", new LinkedHashMap<>(queryParams));
composite.put("body", body);
return composite;
}
if (hasQuery) {
return new LinkedHashMap<>(queryParams);
}
if (hasBody) {
return body;
}
return includeBody ? body : null;
}
}
private boolean supportsRequestBody(HttpMethod method) {
if (method == null) {
return true;
}
return !(HttpMethod.GET.equals(method)
|| HttpMethod.DELETE.equals(method)
|| HttpMethod.HEAD.equals(method)
|| HttpMethod.OPTIONS.equals(method)
|| HttpMethod.TRACE.equals(method));
}
}

View File

@@ -0,0 +1,154 @@
package com.zt.plat.module.databus.framework.integration.gateway.step.impl;
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.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.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiStepResult;
import com.zt.plat.module.databus.framework.integration.gateway.step.ApiStepHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.integration.core.GenericHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Map;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_RPC_ENDPOINT_INVALID;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_RPC_EXECUTION_FAILED;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_RPC_METHOD_NOT_FOUND;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_RPC_UNSUPPORTED_SIGNATURE;
/**
* Step handler performing intra-application RPC invocations via Spring beans.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RpcStepHandler implements ApiStepHandler {
private final ApplicationContext applicationContext;
private final ExpressionExecutor expressionExecutor;
@Override
public boolean supports(String stepType) {
return "RPC".equalsIgnoreCase(stepType);
}
@Override
public GenericHandler<ApiInvocationContext> build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
BeanMethod beanMethod = parseEndpoint(stepDefinition.getStep().getTargetEndpoint());
ExpressionSpec requestSpec = ExpressionSpecParser.parse(stepDefinition.getStep().getRequestMappingExpr(), ExpressionTypeEnum.JSON);
ExpressionSpec responseSpec = ExpressionSpecParser.parse(stepDefinition.getStep().getResponseMappingExpr(), ExpressionTypeEnum.JSON);
return (context, headers) -> {
Instant start = Instant.now();
try {
Object arguments = requestSpec == null ? context.getRequestBody() : expressionExecutor.evaluate(requestSpec, context, context.getRequestBody(), headers);
Object result = invoke(beanMethod, arguments, context);
context.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
.request(arguments)
.response(result)
.success(true)
.elapsed(Duration.between(start, Instant.now()))
.build());
if (result instanceof Map<?, ?> map) {
map.forEach((key, value) -> context.getVariables().put(String.valueOf(key), value));
} else if (result != null) {
context.setResponseBody(result);
}
} catch (Exception ex) {
context.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
.success(false)
.errorMessage(ex.getMessage())
.elapsed(Duration.between(start, Instant.now()))
.build());
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_STEP_RPC_EXECUTION_FAILED, ex.getMessage());
}
return context;
};
}
private Object invoke(BeanMethod beanMethod, Object argument, ApiInvocationContext context) throws Exception {
Object bean = applicationContext.getBean(beanMethod.beanName);
Method method = resolveMethod(bean, beanMethod.methodName, argument);
if (method == null) {
throw ServiceExceptionUtil.exception(API_STEP_RPC_METHOD_NOT_FOUND, beanMethod.beanName, beanMethod.methodName);
}
ReflectionUtils.makeAccessible(method);
if (method.getParameterCount() == 0) {
return method.invoke(bean);
}
if (method.getParameterCount() == 1) {
Class<?> parameterType = method.getParameterTypes()[0];
if (ApiInvocationContext.class.isAssignableFrom(parameterType)) {
return method.invoke(bean, context);
}
return method.invoke(bean, convertArgument(argument, parameterType));
}
if (method.getParameterCount() == 2) {
return method.invoke(bean, context, argument);
}
throw ServiceExceptionUtil.exception(API_STEP_RPC_UNSUPPORTED_SIGNATURE, beanMethod.methodName);
}
private Method resolveMethod(Object bean, String methodName, Object argument) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(bean.getClass());
return Arrays.stream(methods)
.filter(method -> method.getName().equals(methodName))
.filter(method -> method.getParameterCount() <= 2)
.findFirst()
.orElse(null);
}
private Object convertArgument(Object argument, Class<?> targetType) {
if (argument == null) {
return null;
}
if (targetType.isAssignableFrom(argument.getClass())) {
return argument;
}
if (targetType.equals(String.class)) {
return String.valueOf(argument);
}
if (Number.class.isAssignableFrom(targetType) && argument instanceof Number number) {
if (targetType.equals(Integer.class)) {
return number.intValue();
}
if (targetType.equals(Long.class)) {
return number.longValue();
}
if (targetType.equals(Double.class)) {
return number.doubleValue();
}
}
return argument;
}
private BeanMethod parseEndpoint(String endpoint) {
if (!endpoint.contains("#")) {
throw ServiceExceptionUtil.exception(API_STEP_RPC_ENDPOINT_INVALID);
}
String[] parts = endpoint.split("#", 2);
return new BeanMethod(parts[0], parts[1]);
}
private record BeanMethod(String beanName, String methodName) {
}
}

View File

@@ -0,0 +1,74 @@
package com.zt.plat.module.databus.framework.integration.gateway.step.impl;
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.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.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiStepResult;
import com.zt.plat.module.databus.framework.integration.gateway.step.ApiStepHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.integration.core.GenericHandler;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_STEP_SCRIPT_EXECUTION_FAILED;
/**
* Step handler executing JSON-based scripted expressions.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ScriptStepHandler implements ApiStepHandler {
private final ExpressionExecutor expressionExecutor;
@Override
public boolean supports(String stepType) {
return "SCRIPT".equalsIgnoreCase(stepType);
}
@Override
public GenericHandler<ApiInvocationContext> build(ApiDefinitionAggregate aggregate, ApiStepDefinition stepDefinition) {
ExpressionSpec spec = ExpressionSpecParser.parse(stepDefinition.getStep().getTargetEndpoint(), ExpressionTypeEnum.JSON);
return (context, headers) -> {
Instant start = Instant.now();
try {
Object result = expressionExecutor.evaluate(spec, context, context.getRequestBody(), headers);
context.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
.success(true)
.response(result)
.elapsed(Duration.between(start, Instant.now()))
.build());
if (result instanceof java.util.Map<?, ?> map) {
map.forEach((key, value) -> context.getVariables().put(String.valueOf(key), value));
} else if (result != null) {
context.setResponseBody(result);
}
} catch (Exception ex) {
context.addStepResult(ApiStepResult.builder()
.stepId(stepDefinition.getStep().getId())
.stepType(stepDefinition.getStep().getType())
.success(false)
.errorMessage(ex.getMessage())
.elapsed(Duration.between(start, Instant.now()))
.build());
if (ex instanceof ServiceException serviceException) {
throw serviceException;
}
throw ServiceExceptionUtil.exception(API_STEP_SCRIPT_EXECUTION_FAILED, ex.getMessage());
}
return context;
};
}
}

View File

@@ -0,0 +1,57 @@
package com.zt.plat.module.databus.service.gateway;
import com.zt.plat.framework.common.pojo.PageResult;
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.dal.dataobject.gateway.ApiDefinitionDO;
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
import java.util.List;
import java.util.Optional;
/**
* Service providing access to API definitions and their orchestration metadata.
*/
public interface ApiDefinitionService {
/**
* Load all active API definitions for bootstrap.
*/
List<ApiDefinitionAggregate> loadActiveDefinitions();
/**
* Lookup API definition by code and version.
*/
Optional<ApiDefinitionAggregate> findByCodeAndVersion(String apiCode, String version);
/**
* Refresh a specific definition by evicting cache and reloading from DB.
*/
Optional<ApiDefinitionAggregate> refresh(String apiCode, String version);
/**
* Lookup API definition aggregate by primary key.
*/
Optional<ApiDefinitionAggregate> findById(Long id);
/**
* Query API definitions with pagination.
*/
PageResult<ApiDefinitionDO> getPage(ApiDefinitionPageReqVO reqVO);
/**
* Create a new API definition with orchestration metadata.
*/
Long create(ApiDefinitionSaveReqVO reqVO);
/**
* Update an existing API definition with orchestration metadata.
*/
void update(ApiDefinitionSaveReqVO reqVO);
/**
* Delete API definition and related metadata.
*/
void delete(Long id);
}

View File

@@ -0,0 +1,46 @@
package com.zt.plat.module.databus.service.gateway;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySaveReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO;
import java.util.List;
import java.util.Optional;
/**
* Authentication policy operations.
*/
public interface ApiPolicyAuthService {
/**
* Paginate policies.
*/
PageResult<ApiPolicyAuthDO> getPage(ApiPolicyPageReqVO reqVO);
/**
* Fetch all active policies for dropdowns.
*/
List<ApiPolicyAuthDO> getSimpleList();
/**
* Find policy detail.
*/
Optional<ApiPolicyAuthDO> get(Long id);
/**
* Create policy definition.
*/
Long create(ApiPolicySaveReqVO reqVO);
/**
* Update policy definition.
*/
void update(ApiPolicySaveReqVO reqVO);
/**
* Delete policy definition.
*/
void delete(Long id);
}

View File

@@ -0,0 +1,46 @@
package com.zt.plat.module.databus.service.gateway;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySaveReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
import java.util.List;
import java.util.Optional;
/**
* Rate limit policy operations.
*/
public interface ApiPolicyRateLimitService {
/**
* Paginate policies.
*/
PageResult<ApiPolicyRateLimitDO> getPage(ApiPolicyPageReqVO reqVO);
/**
* Fetch all active policies for dropdowns.
*/
List<ApiPolicyRateLimitDO> getSimpleList();
/**
* Find policy detail.
*/
Optional<ApiPolicyRateLimitDO> get(Long id);
/**
* Create policy definition.
*/
Long create(ApiPolicySaveReqVO reqVO);
/**
* Update policy definition.
*/
void update(ApiPolicySaveReqVO reqVO);
/**
* Delete policy definition.
*/
void delete(Long id);
}

View File

@@ -0,0 +1,431 @@
package com.zt.plat.module.databus.service.gateway.impl;
import cn.hutool.core.collection.CollUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.framework.tenant.core.context.TenantContextHolder;
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.ApiDefinitionStepSaveReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionTransformSaveReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiFlowPublishDO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
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.dal.mysql.gateway.ApiDefinitionMapper;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiFlowPublishMapper;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyAuthMapper;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyRateLimitMapper;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiStepMapper;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiTransformMapper;
import com.zt.plat.module.databus.enums.gateway.ApiStatusEnum;
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 com.zt.plat.module.databus.service.gateway.ApiDefinitionService;
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class ApiDefinitionServiceImpl implements ApiDefinitionService {
private static final String REDIS_CACHE_PREFIX = "databus:api:def:";
private final ApiDefinitionMapper apiDefinitionMapper;
private final ApiStepMapper apiStepMapper;
private final ApiTransformMapper apiTransformMapper;
private final ApiPolicyAuthMapper apiPolicyAuthMapper;
private final ApiPolicyRateLimitMapper apiPolicyRateLimitMapper;
private final ApiFlowPublishMapper apiFlowPublishMapper;
private final ObjectMapper objectMapper;
private final StringRedisTemplate stringRedisTemplate;
private LoadingCache<String, Optional<ApiDefinitionAggregate>> definitionCache;
@PostConstruct
public void initCache() {
definitionCache = Caffeine.newBuilder()
.maximumSize(512)
.expireAfterWrite(Duration.ofMinutes(5))
.build(this::loadAggregateSync);
}
@Override
public List<ApiDefinitionAggregate> loadActiveDefinitions() {
List<ApiDefinitionDO> definitions = apiDefinitionMapper.selectActiveDefinitions(Collections.singletonList(ApiStatusEnum.ONLINE.getStatus()));
if (CollUtil.isEmpty(definitions)) {
return Collections.emptyList();
}
List<ApiDefinitionAggregate> aggregates = new ArrayList<>(definitions.size());
for (ApiDefinitionDO definition : definitions) {
aggregates.add(buildAggregate(definition));
}
return aggregates;
}
@Override
public Optional<ApiDefinitionAggregate> findByCodeAndVersion(String apiCode, String version) {
String cacheKey = buildCacheKey(apiCode, version);
try {
return definitionCache.get(cacheKey);
} catch (RuntimeException ex) {
throw ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND);
}
}
@Override
public Optional<ApiDefinitionAggregate> refresh(String apiCode, String version) {
String cacheKey = buildCacheKey(apiCode, version);
definitionCache.invalidate(cacheKey);
deleteRedis(cacheKey);
return findByCodeAndVersion(apiCode, version);
}
@Override
public Optional<ApiDefinitionAggregate> findById(Long id) {
return Optional.ofNullable(apiDefinitionMapper.selectById(id))
.map(this::buildAggregate);
}
@Override
public PageResult<ApiDefinitionDO> getPage(ApiDefinitionPageReqVO reqVO) {
return apiDefinitionMapper.selectPage(reqVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long create(ApiDefinitionSaveReqVO reqVO) {
validateDuplication(reqVO, null);
validateStructure(reqVO);
validatePolicies(reqVO);
ApiDefinitionDO definition = buildDefinitionDO(reqVO, null);
apiDefinitionMapper.insert(definition);
Long apiId = definition.getId();
persistApiLevelTransforms(apiId, reqVO.getApiLevelTransforms());
persistSteps(apiId, reqVO.getSteps());
return apiId;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(ApiDefinitionSaveReqVO reqVO) {
ApiDefinitionDO existing = ensureExists(reqVO.getId());
validateDuplication(reqVO, existing.getId());
validateStructure(reqVO);
validatePolicies(reqVO);
ApiDefinitionDO updateObj = buildDefinitionDO(reqVO, existing);
apiDefinitionMapper.updateById(updateObj);
invalidateCache(existing.getTenantId(), existing.getApiCode(), existing.getVersion());
apiTransformMapper.deleteByApiId(existing.getId());
apiStepMapper.deleteByApiId(existing.getId());
persistApiLevelTransforms(existing.getId(), reqVO.getApiLevelTransforms());
persistSteps(existing.getId(), reqVO.getSteps());
invalidateCache(updateObj.getTenantId(), updateObj.getApiCode(), updateObj.getVersion());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
ApiDefinitionDO existing = ensureExists(id);
invalidateCache(existing.getTenantId(), existing.getApiCode(), existing.getVersion());
apiTransformMapper.deleteByApiId(id);
apiStepMapper.deleteByApiId(id);
apiDefinitionMapper.deleteById(id);
}
private Optional<ApiDefinitionAggregate> loadAggregateSync(String cacheKey) {
Optional<ApiDefinitionAggregate> cached = loadFromRedis(cacheKey);
if (cached.isPresent()) {
return cached;
}
String[] parts = cacheKey.split(":");
String apiCode = parts[1];
String version = parts[2];
Optional<ApiDefinitionAggregate> aggregate = apiDefinitionMapper.selectByCodeAndVersion(apiCode, version)
.filter(definition -> ApiStatusEnum.isOnline(definition.getStatus()))
.map(this::buildAggregate);
aggregate.ifPresent(value -> persistToRedis(cacheKey, value));
return aggregate;
}
private Optional<ApiDefinitionAggregate> loadFromRedis(String cacheKey) {
try {
String json = stringRedisTemplate.opsForValue().get(REDIS_CACHE_PREFIX + cacheKey);
if (!StringUtils.hasText(json)) {
return Optional.empty();
}
ApiDefinitionAggregate aggregate = objectMapper.readValue(json, ApiDefinitionAggregate.class);
return Optional.of(aggregate);
} catch (JsonProcessingException | DataAccessException ex) {
log.warn("Failed to deserialize API definition aggregate from redis for key {}", cacheKey, ex);
return Optional.empty();
}
}
private void persistToRedis(String cacheKey, ApiDefinitionAggregate aggregate) {
try {
String json = objectMapper.writeValueAsString(aggregate);
stringRedisTemplate.opsForValue().set(REDIS_CACHE_PREFIX + cacheKey, json, 5, TimeUnit.MINUTES);
} catch (JsonProcessingException | DataAccessException ex) {
log.warn("Failed to persist API definition aggregate to redis for key {}", cacheKey, ex);
}
}
private void deleteRedis(String cacheKey) {
try {
stringRedisTemplate.delete(REDIS_CACHE_PREFIX + cacheKey);
} catch (DataAccessException ex) {
log.warn("Failed to delete API definition aggregate from redis for key {}", cacheKey, ex);
}
}
private String buildCacheKey(String apiCode, String version) {
Long tenantId = TenantContextHolder.getTenantId();
return buildCacheKeyForTenant(tenantId, apiCode, version);
}
private ApiDefinitionAggregate buildAggregate(ApiDefinitionDO definition) {
List<ApiStepDO> stepDOS = apiStepMapper.selectByApiId(definition.getId());
List<ApiStepDefinition> stepDefinitions = new ArrayList<>(stepDOS.size());
for (ApiStepDO stepDO : stepDOS) {
List<ApiTransformDefinition> transforms = convertTransforms(apiTransformMapper.selectByStepId(stepDO.getId()));
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("retryStrategy", parseJson(stepDO.getRetryStrategy()));
metadata.put("fallbackStrategy", parseJson(stepDO.getFallbackStrategy()));
metadata.put("timeout", stepDO.getTimeout());
metadata.put("stopOnError", stepDO.getStopOnError());
ApiStepDefinition stepDefinition = ApiStepDefinition.builder()
.step(stepDO)
.transforms(transforms)
.metadata(metadata)
.build();
stepDefinitions.add(stepDefinition);
}
Map<String, ApiTransformDefinition> apiTransforms = new LinkedHashMap<>();
for (ApiTransformDefinition transform : convertTransforms(apiTransformMapper.selectApiLevelTransforms(definition.getId()))) {
apiTransforms.put(transform.getPhase(), transform);
}
ApiPolicyAuthDO authPolicy = Optional.ofNullable(definition.getAuthPolicyId())
.map(apiPolicyAuthMapper::selectById)
.orElse(null);
ApiPolicyRateLimitDO rateLimitPolicy = Optional.ofNullable(definition.getRateLimitId())
.map(apiPolicyRateLimitMapper::selectById)
.orElse(null);
ApiFlowPublication publication = apiFlowPublishMapper.selectActiveByApiId(definition.getId())
.map(this::convertPublication)
.orElse(null);
return ApiDefinitionAggregate.builder()
.definition(definition)
.steps(stepDefinitions)
.apiLevelTransforms(apiTransforms)
.authPolicy(authPolicy)
.rateLimitPolicy(rateLimitPolicy)
.publication(publication)
.build();
}
private List<ApiTransformDefinition> convertTransforms(List<ApiTransformDO> transformDOS) {
if (CollUtil.isEmpty(transformDOS)) {
return Collections.emptyList();
}
List<ApiTransformDefinition> definitions = new ArrayList<>(transformDOS.size());
for (ApiTransformDO transformDO : transformDOS) {
definitions.add(ApiTransformDefinition.builder()
.id(transformDO.getId())
.phase(transformDO.getPhase())
.expression(transformDO.getExpression())
.expressionType(transformDO.getExpressionType())
.description(transformDO.getDescription())
.build());
}
return definitions;
}
private Map<String, Object> parseJson(String json) {
if (!StringUtils.hasText(json)) {
return Collections.emptyMap();
}
try {
return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {});
} catch (JsonProcessingException ex) {
log.warn("Failed to parse configuration JSON: {}", json, ex);
return Collections.emptyMap();
}
}
private ApiFlowPublication convertPublication(ApiFlowPublishDO publishDO) {
return ApiFlowPublication.builder()
.id(publishDO.getId())
.releaseTag(publishDO.getReleaseTag())
.snapshot(publishDO.getSnapshot())
.status(publishDO.getStatus())
.active(Boolean.TRUE.equals(publishDO.getActive()))
.description(publishDO.getDescription())
.build();
}
private ApiDefinitionDO buildDefinitionDO(ApiDefinitionSaveReqVO reqVO, ApiDefinitionDO existing) {
ApiDefinitionDO definition = BeanUtils.toBean(reqVO, ApiDefinitionDO.class);
if (existing == null) {
definition.setId(null);
definition.setTenantId(resolveTenantIdentifier());
definition.setDeleted(Boolean.FALSE);
} else {
definition.setId(existing.getId());
definition.setTenantId(existing.getTenantId());
definition.setDeleted(existing.getDeleted());
}
return definition;
}
private void persistApiLevelTransforms(Long apiId, List<ApiDefinitionTransformSaveReqVO> transforms) {
if (CollUtil.isEmpty(transforms)) {
return;
}
for (ApiDefinitionTransformSaveReqVO transformVO : transforms) {
ApiTransformDO transformDO = BeanUtils.toBean(transformVO, ApiTransformDO.class);
transformDO.setId(null);
transformDO.setApiId(apiId);
transformDO.setStepId(null);
applyTenantDefaults(transformDO);
apiTransformMapper.insert(transformDO);
}
}
private void persistSteps(Long apiId, List<ApiDefinitionStepSaveReqVO> steps) {
if (CollUtil.isEmpty(steps)) {
return;
}
List<ApiDefinitionStepSaveReqVO> ordered = new ArrayList<>(steps);
ordered.sort(Comparator.comparing(ApiDefinitionStepSaveReqVO::getStepOrder));
for (ApiDefinitionStepSaveReqVO stepVO : ordered) {
ApiStepDO stepDO = BeanUtils.toBean(stepVO, ApiStepDO.class);
stepDO.setId(null);
stepDO.setApiId(apiId);
applyTenantDefaults(stepDO);
apiStepMapper.insert(stepDO);
persistStepTransforms(apiId, stepDO.getId(), stepVO.getTransforms());
}
}
private void persistStepTransforms(Long apiId, Long stepId, List<ApiDefinitionTransformSaveReqVO> transforms) {
if (CollUtil.isEmpty(transforms)) {
return;
}
for (ApiDefinitionTransformSaveReqVO transformVO : transforms) {
ApiTransformDO transformDO = BeanUtils.toBean(transformVO, ApiTransformDO.class);
transformDO.setId(null);
transformDO.setApiId(apiId);
transformDO.setStepId(stepId);
applyTenantDefaults(transformDO);
apiTransformMapper.insert(transformDO);
}
}
private <T extends TenantBaseDO> void applyTenantDefaults(T entity) {
entity.setTenantId(resolveTenantIdentifier());
entity.setDeleted(Boolean.FALSE);
}
private ApiDefinitionDO ensureExists(Long id) {
if (id == null) {
throw ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND);
}
ApiDefinitionDO definition = apiDefinitionMapper.selectById(id);
if (definition == null || Boolean.TRUE.equals(definition.getDeleted())) {
throw ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND);
}
return definition;
}
private void validateDuplication(ApiDefinitionSaveReqVO reqVO, Long currentId) {
apiDefinitionMapper.selectByCodeAndVersion(reqVO.getApiCode(), reqVO.getVersion())
.filter(definition -> currentId == null || !Objects.equals(definition.getId(), currentId))
.ifPresent(definition -> { throw ServiceExceptionUtil.exception(API_DEFINITION_DUPLICATE); });
}
private void validateStructure(ApiDefinitionSaveReqVO reqVO) {
if (CollUtil.isEmpty(reqVO.getSteps())) {
throw ServiceExceptionUtil.exception(API_DEFINITION_STEP_EMPTY);
}
Set<Integer> orders = new HashSet<>();
for (ApiDefinitionStepSaveReqVO step : reqVO.getSteps()) {
Integer order = step.getStepOrder();
if (order == null || !orders.add(order)) {
throw ServiceExceptionUtil.exception(API_DEFINITION_STEP_ORDER_DUPLICATE);
}
validateTransformPhases(step.getTransforms());
}
validateTransformPhases(reqVO.getApiLevelTransforms());
}
private void validateTransformPhases(List<ApiDefinitionTransformSaveReqVO> transforms) {
if (CollUtil.isEmpty(transforms)) {
return;
}
Set<String> phases = new HashSet<>();
for (ApiDefinitionTransformSaveReqVO transform : transforms) {
String phase = transform.getPhase();
if (!StringUtils.hasText(phase) || !phases.add(phase.toUpperCase(Locale.ROOT))) {
throw ServiceExceptionUtil.exception(API_TRANSFORM_PHASE_DUPLICATE);
}
}
}
private void validatePolicies(ApiDefinitionSaveReqVO reqVO) {
if (reqVO.getAuthPolicyId() != null && apiPolicyAuthMapper.selectById(reqVO.getAuthPolicyId()) == null) {
throw ServiceExceptionUtil.exception(API_POLICY_NOT_FOUND);
}
if (reqVO.getRateLimitId() != null && apiPolicyRateLimitMapper.selectById(reqVO.getRateLimitId()) == null) {
throw ServiceExceptionUtil.exception(API_POLICY_NOT_FOUND);
}
}
private Long resolveTenantIdentifier() {
return TenantContextHolder.getTenantId();
}
private void invalidateCache(Long tenantId, String apiCode, String version) {
if (!StringUtils.hasText(apiCode) || !StringUtils.hasText(version)) {
return;
}
String cacheKey = buildCacheKeyForTenant(tenantId, apiCode, version);
definitionCache.invalidate(cacheKey);
deleteRedis(cacheKey);
}
private String buildCacheKeyForTenant(Long tenantId, String apiCode, String version) {
String tenantPart = tenantId == null ? "global" : tenantId.toString();
return tenantPart + ":" + apiCode.toLowerCase(Locale.ROOT) + ":" + version;
}
}

View File

@@ -0,0 +1,98 @@
package com.zt.plat.module.databus.service.gateway.impl;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.tenant.core.context.TenantContextHolder;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySaveReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiDefinitionMapper;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyAuthMapper;
import com.zt.plat.module.databus.service.gateway.ApiPolicyAuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import java.util.List;
import java.util.Optional;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_POLICY_IN_USE;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_POLICY_NOT_FOUND;
@Service
@RequiredArgsConstructor
public class ApiPolicyAuthServiceImpl implements ApiPolicyAuthService {
private final ApiPolicyAuthMapper authMapper;
private final ApiDefinitionMapper apiDefinitionMapper;
@Override
public PageResult<ApiPolicyAuthDO> getPage(ApiPolicyPageReqVO reqVO) {
return authMapper.selectPage(reqVO);
}
@Override
public List<ApiPolicyAuthDO> getSimpleList() {
return authMapper.selectSimpleList();
}
@Override
public Optional<ApiPolicyAuthDO> get(Long id) {
if (id == null) {
return Optional.empty();
}
return Optional.ofNullable(authMapper.selectById(id))
.filter(policy -> !Boolean.TRUE.equals(policy.getDeleted()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long create(ApiPolicySaveReqVO reqVO) {
ApiPolicyAuthDO policy = new ApiPolicyAuthDO();
apply(reqVO, policy);
policy.setId(null);
policy.setTenantId(TenantContextHolder.getTenantId());
policy.setDeleted(Boolean.FALSE);
authMapper.insert(policy);
return policy.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(ApiPolicySaveReqVO reqVO) {
ApiPolicyAuthDO existing = ensureExists(reqVO.getId());
apply(reqVO, existing);
authMapper.updateById(existing);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
ApiPolicyAuthDO existing = ensureExists(id);
Long referenceCount = apiDefinitionMapper.selectCountByAuthPolicyId(existing.getId());
if (referenceCount != null && referenceCount > 0) {
throw ServiceExceptionUtil.exception(API_POLICY_IN_USE);
}
authMapper.deleteById(existing.getId());
}
private ApiPolicyAuthDO ensureExists(Long id) {
Assert.notNull(id, "策略编号不能为空");
return get(id).orElseThrow(() -> ServiceExceptionUtil.exception(API_POLICY_NOT_FOUND));
}
private void apply(ApiPolicySaveReqVO reqVO, ApiPolicyAuthDO target) {
target.setName(StrUtil.trim(reqVO.getName()));
target.setType(StrUtil.trim(reqVO.getType()));
target.setConfig(normalizeNullable(reqVO.getConfig()));
target.setDescription(normalizeNullable(reqVO.getDescription()));
}
private String normalizeNullable(String value) {
String trimmed = StrUtil.trim(value);
return StrUtil.isEmpty(trimmed) ? null : trimmed;
}
}

View File

@@ -0,0 +1,98 @@
package com.zt.plat.module.databus.service.gateway.impl;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.tenant.core.context.TenantContextHolder;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicyPageReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.policy.ApiPolicySaveReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiDefinitionMapper;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyRateLimitMapper;
import com.zt.plat.module.databus.service.gateway.ApiPolicyRateLimitService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import java.util.List;
import java.util.Optional;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_POLICY_IN_USE;
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_POLICY_NOT_FOUND;
@Service
@RequiredArgsConstructor
public class ApiPolicyRateLimitServiceImpl implements ApiPolicyRateLimitService {
private final ApiPolicyRateLimitMapper rateLimitMapper;
private final ApiDefinitionMapper apiDefinitionMapper;
@Override
public PageResult<ApiPolicyRateLimitDO> getPage(ApiPolicyPageReqVO reqVO) {
return rateLimitMapper.selectPage(reqVO);
}
@Override
public List<ApiPolicyRateLimitDO> getSimpleList() {
return rateLimitMapper.selectSimpleList();
}
@Override
public Optional<ApiPolicyRateLimitDO> get(Long id) {
if (id == null) {
return Optional.empty();
}
return Optional.ofNullable(rateLimitMapper.selectById(id))
.filter(policy -> !Boolean.TRUE.equals(policy.getDeleted()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long create(ApiPolicySaveReqVO reqVO) {
ApiPolicyRateLimitDO policy = new ApiPolicyRateLimitDO();
apply(reqVO, policy);
policy.setId(null);
policy.setTenantId(TenantContextHolder.getTenantId());
policy.setDeleted(Boolean.FALSE);
rateLimitMapper.insert(policy);
return policy.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(ApiPolicySaveReqVO reqVO) {
ApiPolicyRateLimitDO existing = ensureExists(reqVO.getId());
apply(reqVO, existing);
rateLimitMapper.updateById(existing);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
ApiPolicyRateLimitDO existing = ensureExists(id);
Long referenceCount = apiDefinitionMapper.selectCountByRateLimitPolicyId(existing.getId());
if (referenceCount != null && referenceCount > 0) {
throw ServiceExceptionUtil.exception(API_POLICY_IN_USE);
}
rateLimitMapper.deleteById(existing.getId());
}
private ApiPolicyRateLimitDO ensureExists(Long id) {
Assert.notNull(id, "策略编号不能为空");
return get(id).orElseThrow(() -> ServiceExceptionUtil.exception(API_POLICY_NOT_FOUND));
}
private void apply(ApiPolicySaveReqVO reqVO, ApiPolicyRateLimitDO target) {
target.setName(StrUtil.trim(reqVO.getName()));
target.setType(StrUtil.trim(reqVO.getType()));
target.setConfig(normalizeNullable(reqVO.getConfig()));
target.setDescription(normalizeNullable(reqVO.getDescription()));
}
private String normalizeNullable(String value) {
String trimmed = StrUtil.trim(value);
return StrUtil.isEmpty(trimmed) ? null : trimmed;
}
}

View File

@@ -0,0 +1,39 @@
package com.zt.plat.module.databus.service.gateway.impl;
import com.zt.plat.framework.common.exception.ErrorCode;
/**
* Error code constants for unified API portal services.
*/
public interface GatewayServiceErrorCodeConstants {
ErrorCode API_DEFINITION_NOT_FOUND = new ErrorCode(1_010_000_001, "API 定义未发布或已下线");
ErrorCode API_DEFINITION_DUPLICATE = new ErrorCode(1_010_000_002, "API 编码与版本已存在");
ErrorCode API_DEFINITION_STEP_EMPTY = new ErrorCode(1_010_000_003, "至少需要配置一个编排步骤");
ErrorCode API_DEFINITION_STEP_ORDER_DUPLICATE = new ErrorCode(1_010_000_004, "步骤序号重复");
ErrorCode API_TRANSFORM_PHASE_DUPLICATE = new ErrorCode(1_010_000_005, "同一级别的变换阶段重复");
ErrorCode API_POLICY_NOT_FOUND = new ErrorCode(1_010_000_006, "绑定的策略不存在");
ErrorCode API_POLICY_IN_USE = new ErrorCode(1_010_000_028, "策略已被 API 定义引用,无法删除");
ErrorCode API_FLOW_NOT_FOUND = new ErrorCode(1_010_000_007, "未找到可用的 API 调度流程code={}, version={}");
ErrorCode API_FLOW_NO_REPLY = new ErrorCode(1_010_000_008, "集成流程未返回响应code={}, version={}");
ErrorCode API_AUTH_UNAUTHORIZED = new ErrorCode(1_010_000_009, "请求未通过认证");
ErrorCode API_RATE_LIMIT_EXCEEDED = new ErrorCode(1_010_000_010, "请求触发限流策略");
ErrorCode API_RATE_LIMIT_EVALUATION_FAILED = new ErrorCode(1_010_000_011, "限流策略执行失败");
ErrorCode API_STEP_HTTP_ENDPOINT_INVALID = new ErrorCode(1_010_000_012, "HTTP 步骤缺少目标地址");
ErrorCode API_STEP_HTTP_EXECUTION_FAILED = new ErrorCode(1_010_000_013, "HTTP 步骤执行失败:{}");
ErrorCode API_STEP_SCRIPT_EXECUTION_FAILED = new ErrorCode(1_010_000_014, "脚本步骤执行失败:{}");
ErrorCode API_STEP_RPC_ENDPOINT_INVALID = new ErrorCode(1_010_000_015, "RPC 步骤的目标端点格式必须为 beanName#method");
ErrorCode API_STEP_RPC_METHOD_NOT_FOUND = new ErrorCode(1_010_000_016, "RPC 步骤未找到目标方法:{}#{}");
ErrorCode API_STEP_RPC_UNSUPPORTED_SIGNATURE = new ErrorCode(1_010_000_017, "RPC 步骤暂不支持该方法签名:{}");
ErrorCode API_STEP_RPC_EXECUTION_FAILED = new ErrorCode(1_010_000_018, "RPC 步骤执行失败:{}");
ErrorCode API_TRANSFORM_EVALUATION_FAILED = new ErrorCode(1_010_000_019, "API 层变换执行失败:{}");
ErrorCode API_TRANSFORM_RESPONSE_STATUS_INVALID = new ErrorCode(1_010_000_020, "API 层变换返回的 responseStatus 不合法:{}");
ErrorCode API_PARALLEL_INTERRUPTED = new ErrorCode(1_010_000_021, "并行步骤执行被中断");
ErrorCode API_PARALLEL_FAILED = new ErrorCode(1_010_000_022, "并行步骤执行失败:{}");
ErrorCode API_EXPRESSION_NO_EVALUATOR = new ErrorCode(1_010_000_023, "未找到可用的表达式执行器:{}");
ErrorCode API_EXPRESSION_EVALUATION_FAILED = new ErrorCode(1_010_000_024, "表达式执行失败:{}");
ErrorCode API_JSONATA_BIND_FAILED = new ErrorCode(1_010_000_025, "表达式环境绑定失败");
ErrorCode API_STEP_EXECUTION_ERROR = new ErrorCode(1_010_000_026, "步骤执行出现异常");
ErrorCode API_STEP_UNSUPPORTED_TYPE = new ErrorCode(1_010_000_027, "不支持的步骤类型:{}");
}

View File

@@ -0,0 +1,105 @@
package com.zt.plat.module.databus.framework.integration.gateway.step.impl;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO;
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.model.ApiInvocationContext;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.messaging.MessageHeaders;
import org.springframework.web.reactive.function.client.WebClient;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
class HttpStepHandlerTest {
private MockWebServer server;
private ExpressionExecutor expressionExecutor;
private HttpStepHandler handler;
@BeforeEach
void setUp() throws IOException {
server = new MockWebServer();
server.start();
expressionExecutor = Mockito.mock(ExpressionExecutor.class);
handler = new HttpStepHandler(WebClient.builder(), expressionExecutor);
}
@AfterEach
void tearDown() throws IOException {
server.shutdown();
}
@Test
void shouldForwardQueryParamsFromContextForGet() throws Exception {
server.enqueue(new MockResponse().setBody("{\"ok\":true}").setHeader("Content-Type", "application/json"));
ApiInvocationContext context = ApiInvocationContext.create();
context.getRequestQueryParams().put("id", "123");
context.setRequestBody(Collections.singletonMap("ignored", "value"));
ApiStepDO stepDO = new ApiStepDO();
stepDO.setId(1L);
stepDO.setType("HTTP");
stepDO.setTargetEndpoint("GET " + server.url("/orders"));
ApiStepDefinition stepDefinition = ApiStepDefinition.builder()
.step(stepDO)
.metadata(Collections.emptyMap())
.transforms(Collections.emptyList())
.build();
handler.build(null, stepDefinition).handle(context, new MessageHeaders(Collections.emptyMap()));
RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS);
assertThat(request).isNotNull();
assertThat(request.getMethod()).isEqualTo("GET");
assertThat(request.getPath()).isEqualTo("/orders?id=123");
assertThat(request.getBody().size()).isZero();
}
@Test
void shouldMergeExpressionBodyAndQueryParamsForPost() throws Exception {
server.enqueue(new MockResponse().setBody("{\"ok\":true}").setHeader("Content-Type", "application/json"));
ApiInvocationContext context = ApiInvocationContext.create();
context.setRequestBody(Collections.singletonMap("ignored", "value"));
ApiStepDO stepDO = new ApiStepDO();
stepDO.setId(2L);
stepDO.setType("HTTP");
stepDO.setTargetEndpoint("POST " + server.url("/payments"));
stepDO.setRequestMappingExpr("JSON::{\"body\": {\"amount\": 100}, \"query\": {\"token\": \"abc\"}}");
Map<String, Object> expressionResult = Map.of(
"body", Map.of("amount", 100),
"query", Map.of("token", "abc")
);
Mockito.when(expressionExecutor.evaluate(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(expressionResult);
ApiStepDefinition stepDefinition = ApiStepDefinition.builder()
.step(stepDO)
.metadata(Collections.emptyMap())
.transforms(Collections.emptyList())
.build();
handler.build(null, stepDefinition).handle(context, new MessageHeaders(Collections.emptyMap()));
RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS);
assertThat(request).isNotNull();
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getPath()).isEqualTo("/payments?token=abc");
assertThat(request.getHeader("Content-Type")).contains("application/json");
assertThat(request.getBody().readUtf8()).contains("\"amount\":100");
}
}

View File

@@ -0,0 +1,276 @@
package com.zt.plat.module.databus.service.gateway;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.test.core.ut.BaseDbUnitTest;
import com.zt.plat.framework.tenant.core.context.TenantContextHolder;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionStepSaveReqVO;
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionTransformSaveReqVO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyAuthDO;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiPolicyRateLimitDO;
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.dal.mysql.gateway.ApiDefinitionMapper;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyAuthMapper;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyRateLimitMapper;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiStepMapper;
import com.zt.plat.module.databus.dal.mysql.gateway.ApiTransformMapper;
import com.zt.plat.module.databus.enums.gateway.ApiStatusEnum;
import com.zt.plat.module.databus.service.gateway.impl.ApiDefinitionServiceImpl;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.TestPropertySource;
import java.util.List;
import com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@Import({ApiDefinitionServiceImpl.class, ApiDefinitionServiceImplTest.JacksonTestConfiguration.class})
@TestPropertySource(properties = {
"spring.config.import=",
"config.server-addr=localhost:8848",
"config.group=DEFAULT_GROUP",
"config.namespace=public",
"config.username=nacos",
"config.password=nacos",
"spring.cloud.nacos.config.enabled=false",
"spring.cloud.nacos.discovery.enabled=false"
})
class ApiDefinitionServiceImplTest extends BaseDbUnitTest {
@Resource
private ApiDefinitionService apiDefinitionService;
@Resource
private ApiDefinitionMapper apiDefinitionMapper;
@Resource
private ApiStepMapper apiStepMapper;
@Resource
private ApiTransformMapper apiTransformMapper;
@Resource
private ApiPolicyAuthMapper apiPolicyAuthMapper;
@Resource
private ApiPolicyRateLimitMapper apiPolicyRateLimitMapper;
@MockBean
private StringRedisTemplate stringRedisTemplate;
@TestConfiguration
static class JacksonTestConfiguration {
@Bean
ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
@AfterEach
void tearDown() {
TenantContextHolder.clear();
}
@Test
void testCreate_success() {
TenantContextHolder.setTenantId(1L);
Long authId = insertAuthPolicy();
Long rateId = insertRateLimitPolicy();
ApiDefinitionSaveReqVO reqVO = buildSaveReq(null, authId, rateId);
Long definitionId = apiDefinitionService.create(reqVO);
ApiDefinitionDO definition = apiDefinitionMapper.selectById(definitionId);
assertNotNull(definition);
assertEquals(reqVO.getApiCode(), definition.getApiCode());
assertEquals(reqVO.getVersion(), definition.getVersion());
assertEquals(1L, definition.getTenantId());
assertEquals(reqVO.getStatus(), definition.getStatus());
assertEquals(reqVO.getDescription(), definition.getDescription());
List<ApiStepDO> steps = apiStepMapper.selectByApiId(definitionId);
assertEquals(1, steps.size());
ApiStepDO step = steps.get(0);
assertEquals(1, step.getStepOrder());
assertEquals("HTTP", step.getType());
List<ApiTransformDO> apiLevelTransforms = apiTransformMapper.selectApiLevelTransforms(definitionId);
assertEquals(1, apiLevelTransforms.size());
assertEquals("REQUEST_PRE", apiLevelTransforms.get(0).getPhase());
List<ApiTransformDO> stepTransforms = apiTransformMapper.selectByStepId(step.getId());
assertEquals(1, stepTransforms.size());
assertEquals("RESPONSE_PRE", stepTransforms.get(0).getPhase());
}
@Test
void testCreate_duplicate() {
TenantContextHolder.setTenantId(1L);
ApiDefinitionDO definition = new ApiDefinitionDO();
definition.setTenantId(1L);
definition.setDeleted(false);
definition.setApiCode("order.create");
definition.setVersion("v1");
definition.setHttpMethod("POST");
definition.setUriPattern("/order/create");
definition.setStatus(ApiStatusEnum.ONLINE.getStatus());
apiDefinitionMapper.insert(definition);
ApiDefinitionSaveReqVO reqVO = buildSaveReq(null, null, null);
ServiceException exception = assertThrows(ServiceException.class, () -> apiDefinitionService.create(reqVO));
assertEquals(GatewayServiceErrorCodeConstants.API_DEFINITION_DUPLICATE.getCode(), exception.getCode());
}
@Test
void testUpdate_replaceSteps() {
TenantContextHolder.setTenantId(1L);
Long authId = insertAuthPolicy();
Long rateId = insertRateLimitPolicy();
ApiDefinitionDO definition = new ApiDefinitionDO();
definition.setTenantId(1L);
definition.setDeleted(false);
definition.setApiCode("order.update");
definition.setVersion("v1");
definition.setHttpMethod("POST");
definition.setUriPattern("/order/update");
definition.setStatus(ApiStatusEnum.ONLINE.getStatus());
apiDefinitionMapper.insert(definition);
ApiStepDO oldStep = new ApiStepDO();
oldStep.setApiId(definition.getId());
oldStep.setStepOrder(1);
oldStep.setType("HTTP");
oldStep.setTenantId(1L);
oldStep.setDeleted(false);
apiStepMapper.insert(oldStep);
ApiTransformDO oldTransform = new ApiTransformDO();
oldTransform.setApiId(definition.getId());
oldTransform.setStepId(oldStep.getId());
oldTransform.setPhase("REQUEST_PRE");
oldTransform.setExpressionType("JSON");
oldTransform.setExpression("{}");
oldTransform.setTenantId(1L);
oldTransform.setDeleted(false);
apiTransformMapper.insert(oldTransform);
ApiDefinitionSaveReqVO reqVO = buildSaveReq(definition.getId(), authId, rateId);
reqVO.setApiCode("order.update");
reqVO.setVersion("v2");
reqVO.getSteps().get(0).setStepOrder(2);
apiDefinitionService.update(reqVO);
List<ApiStepDO> steps = apiStepMapper.selectByApiId(definition.getId());
assertEquals(1, steps.size());
assertEquals(2, steps.get(0).getStepOrder());
List<ApiTransformDO> transforms = apiTransformMapper.selectByApiId(definition.getId());
assertThat(transforms)
.extracting(ApiTransformDO::getPhase)
.containsExactlyInAnyOrder("REQUEST_PRE", "RESPONSE_PRE");
}
@Test
void testDelete_success() {
TenantContextHolder.setTenantId(1L);
ApiDefinitionDO definition = new ApiDefinitionDO();
definition.setTenantId(1L);
definition.setDeleted(false);
definition.setApiCode("order.delete");
definition.setVersion("v1");
definition.setHttpMethod("DELETE");
definition.setUriPattern("/order/delete");
definition.setStatus(ApiStatusEnum.ONLINE.getStatus());
apiDefinitionMapper.insert(definition);
ApiStepDO step = new ApiStepDO();
step.setApiId(definition.getId());
step.setStepOrder(1);
step.setType("HTTP");
step.setTenantId(1L);
step.setDeleted(false);
apiStepMapper.insert(step);
ApiTransformDO transform = new ApiTransformDO();
transform.setApiId(definition.getId());
transform.setStepId(step.getId());
transform.setPhase("REQUEST_PRE");
transform.setExpressionType("JSON");
transform.setExpression("{}");
transform.setTenantId(1L);
transform.setDeleted(false);
apiTransformMapper.insert(transform);
apiDefinitionService.delete(definition.getId());
ApiDefinitionDO deleted = apiDefinitionMapper.selectById(definition.getId());
assertThat(deleted).isNull();
assertThat(apiStepMapper.selectByApiId(definition.getId())).isEmpty();
assertThat(apiTransformMapper.selectByApiId(definition.getId())).isEmpty();
}
private ApiDefinitionSaveReqVO buildSaveReq(Long id, Long authId, Long rateId) {
ApiDefinitionSaveReqVO reqVO = new ApiDefinitionSaveReqVO();
reqVO.setId(id);
reqVO.setApiCode("order.create");
reqVO.setVersion("v1");
reqVO.setHttpMethod("POST");
reqVO.setUriPattern("/order/create");
reqVO.setStatus(ApiStatusEnum.ONLINE.getStatus());
reqVO.setDescription("create order");
reqVO.setAuthPolicyId(authId);
reqVO.setRateLimitId(rateId);
ApiDefinitionTransformSaveReqVO apiTransform = new ApiDefinitionTransformSaveReqVO();
apiTransform.setPhase("REQUEST_PRE");
apiTransform.setExpressionType("JSON");
apiTransform.setExpression("{}");
reqVO.getApiLevelTransforms().add(apiTransform);
ApiDefinitionStepSaveReqVO step = new ApiDefinitionStepSaveReqVO();
step.setStepOrder(1);
step.setType("HTTP");
step.setTargetEndpoint("https://api.example.com/order");
ApiDefinitionTransformSaveReqVO stepTransform = new ApiDefinitionTransformSaveReqVO();
stepTransform.setPhase("RESPONSE_PRE");
stepTransform.setExpressionType("JSON");
stepTransform.setExpression("{}");
step.getTransforms().add(stepTransform);
reqVO.getSteps().add(step);
return reqVO;
}
private Long insertAuthPolicy() {
ApiPolicyAuthDO policy = new ApiPolicyAuthDO();
policy.setName("auth");
policy.setType("BASIC");
policy.setConfig("{}");
policy.setTenantId(1L);
policy.setDeleted(false);
apiPolicyAuthMapper.insert(policy);
return policy.getId();
}
private Long insertRateLimitPolicy() {
ApiPolicyRateLimitDO policy = new ApiPolicyRateLimitDO();
policy.setName("rate");
policy.setType("GLOBAL");
policy.setConfig("{}");
policy.setTenantId(1L);
policy.setDeleted(false);
apiPolicyRateLimitMapper.insert(policy);
return policy.getId();
}
}

View File

@@ -0,0 +1,42 @@
spring:
config:
import: ""
main:
lazy-initialization: true
banner-mode: off
datasource:
name: databus-unit-test
url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value;
driver-class-name: org.h2.Driver
username: sa
password:
druid:
async-init: true
initial-size: 1
sql:
init:
schema-locations: classpath:/sql/create_tables.sql
data:
redis:
host: 127.0.0.1
port: 16379
database: 0
mybatis:
lazy-initialization: true
mybatis-plus:
global-config:
db-config:
id-type: AUTO
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
zt:
info:
base-package: com.zt.plat.module
env:
name: unit-test
config:
server-addr: localhost:8848
username: nacos
password: nacos
namespace: public
group: DEFAULT_GROUP

View File

@@ -0,0 +1,5 @@
spring:
profiles:
active: unit-test
config:
import: ""

View File

@@ -0,0 +1,7 @@
DELETE FROM "databus_api_transform";
DELETE FROM "databus_api_step";
DELETE FROM "databus_api_definition";
DELETE FROM "databus_policy_auth";
DELETE FROM "databus_policy_rate_limit";
DELETE FROM "databus_policy_audit";
DELETE FROM "databus_api_flow_publish";

View File

@@ -0,0 +1,118 @@
CREATE TABLE IF NOT EXISTS databus_api_definition (
id BIGINT PRIMARY KEY,
api_code VARCHAR(255) NOT NULL,
uri_pattern VARCHAR(512),
http_method VARCHAR(16),
version VARCHAR(64),
status INT,
description VARCHAR(1024),
auth_policy_id BIGINT,
rate_limit_id BIGINT,
audit_policy_id BIGINT,
response_template CLOB,
cache_strategy VARCHAR(255),
updated_at TIMESTAMP,
grey_released BOOLEAN,
tenant_id BIGINT,
create_time TIMESTAMP,
update_time TIMESTAMP,
creator VARCHAR(64),
updater VARCHAR(64),
deleted BOOLEAN
);
CREATE TABLE IF NOT EXISTS databus_api_step (
id BIGINT PRIMARY KEY,
api_id BIGINT NOT NULL,
step_order INT,
parallel_group VARCHAR(64),
type VARCHAR(64),
target_endpoint VARCHAR(512),
request_mapping_expr VARCHAR(1024),
response_mapping_expr VARCHAR(1024),
transform_id BIGINT,
timeout BIGINT,
retry_strategy VARCHAR(255),
fallback_strategy VARCHAR(255),
condition_expr VARCHAR(1024),
stop_on_error BOOLEAN,
tenant_id BIGINT,
create_time TIMESTAMP,
update_time TIMESTAMP,
creator VARCHAR(64),
updater VARCHAR(64),
deleted BOOLEAN
);
CREATE TABLE IF NOT EXISTS databus_api_transform (
id BIGINT PRIMARY KEY,
api_id BIGINT,
step_id BIGINT,
phase VARCHAR(64),
expression_type VARCHAR(64),
expression VARCHAR(1024),
description VARCHAR(1024),
tenant_id BIGINT,
create_time TIMESTAMP,
update_time TIMESTAMP,
creator VARCHAR(64),
updater VARCHAR(64),
deleted BOOLEAN
);
CREATE TABLE IF NOT EXISTS databus_policy_auth (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(64),
config CLOB,
description VARCHAR(512),
tenant_id BIGINT,
create_time TIMESTAMP,
update_time TIMESTAMP,
creator VARCHAR(64),
updater VARCHAR(64),
deleted BOOLEAN
);
CREATE TABLE IF NOT EXISTS databus_policy_rate_limit (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(64),
config CLOB,
description VARCHAR(512),
tenant_id BIGINT,
create_time TIMESTAMP,
update_time TIMESTAMP,
creator VARCHAR(64),
updater VARCHAR(64),
deleted BOOLEAN
);
CREATE TABLE IF NOT EXISTS databus_policy_audit (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(64),
config CLOB,
description VARCHAR(512),
tenant_id BIGINT,
create_time TIMESTAMP,
update_time TIMESTAMP,
creator VARCHAR(64),
updater VARCHAR(64),
deleted BOOLEAN
);
CREATE TABLE IF NOT EXISTS databus_api_flow_publish (
id BIGINT PRIMARY KEY,
api_id BIGINT,
version VARCHAR(64),
status INT,
publish_time TIMESTAMP,
rollback_time TIMESTAMP,
tenant_id BIGINT,
create_time TIMESTAMP,
update_time TIMESTAMP,
creator VARCHAR(64),
updater VARCHAR(64),
deleted BOOLEAN
);