1.规范增量 SQL 文件命名
2.新增数据总线模块(未完成) 3.新增规则模块(未完成) 4.新增组织编码与外部系统组织编码映射关系表 5.补全 e 办单点登录回调逻辑
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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<>();
|
||||
|
||||
}
|
||||
@@ -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<>();
|
||||
|
||||
}
|
||||
@@ -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<>();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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, "不支持的步骤类型:{}");
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
spring:
|
||||
profiles:
|
||||
active: unit-test
|
||||
config:
|
||||
import: ""
|
||||
@@ -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";
|
||||
@@ -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
|
||||
);
|
||||
Reference in New Issue
Block a user