Merge branch 'dev' into test
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package com.zt.plat.module.databus.controller.admin.gateway;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiAccessLogConvert;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogPageReqVO;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogRespVO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
|
||||
import com.zt.plat.module.databus.service.gateway.ApiAccessLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.zt.plat.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* Databus API 访问日志控制器。
|
||||
*/
|
||||
@Tag(name = "管理后台 - Databus API 访问日志")
|
||||
@RestController
|
||||
@RequestMapping("/databus/gateway/access-log")
|
||||
@Validated
|
||||
public class ApiAccessLogController {
|
||||
|
||||
@Resource
|
||||
private ApiAccessLogService apiAccessLogService;
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获取访问日志详情")
|
||||
@Parameter(name = "id", description = "日志编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('databus:gateway:access-log:query')")
|
||||
public CommonResult<ApiAccessLogRespVO> get(@RequestParam("id") Long id) {
|
||||
ApiAccessLogDO logDO = apiAccessLogService.get(id);
|
||||
return success(ApiAccessLogConvert.INSTANCE.convert(logDO));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "分页查询访问日志")
|
||||
@PreAuthorize("@ss.hasPermission('databus:gateway:access-log:query')")
|
||||
public CommonResult<PageResult<ApiAccessLogRespVO>> page(@Valid ApiAccessLogPageReqVO pageReqVO) {
|
||||
PageResult<ApiAccessLogDO> pageResult = apiAccessLogService.getPage(pageReqVO);
|
||||
return success(ApiAccessLogConvert.INSTANCE.convertPage(pageResult));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.zt.plat.module.databus.controller.admin.gateway;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiVersionConvert;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionCompareRespVO;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionDetailRespVO;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionPageReqVO;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionRespVO;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionRollbackReqVO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO;
|
||||
import com.zt.plat.module.databus.service.gateway.ApiVersionService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static com.zt.plat.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* API 版本历史控制器。
|
||||
*/
|
||||
@Tag(name = "管理后台 - API 版本历史")
|
||||
@RestController
|
||||
@RequestMapping("/databus/gateway/version")
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class ApiVersionController {
|
||||
|
||||
@Resource
|
||||
private ApiVersionService apiVersionService;
|
||||
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获取 API 版本详情")
|
||||
@Parameter(name = "id", description = "版本编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('databus:gateway:version:query')")
|
||||
public CommonResult<ApiVersionDetailRespVO> getVersion(@RequestParam("id") Long id) {
|
||||
ApiVersionDO versionDO = apiVersionService.getVersion(id);
|
||||
ApiVersionDetailRespVO respVO = ApiVersionConvert.INSTANCE.convertDetail(versionDO);
|
||||
|
||||
// 反序列化快照数据
|
||||
if (versionDO.getSnapshotData() != null) {
|
||||
try {
|
||||
ApiDefinitionSaveReqVO snapshot = objectMapper.readValue(versionDO.getSnapshotData(), ApiDefinitionSaveReqVO.class);
|
||||
respVO.setSnapshotData(snapshot);
|
||||
} catch (JsonProcessingException ex) {
|
||||
log.error("反序列化版本快照失败, versionId={}", id, ex);
|
||||
}
|
||||
}
|
||||
|
||||
return success(respVO);
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "分页查询 API 版本列表")
|
||||
@PreAuthorize("@ss.hasPermission('databus:gateway:version:query')")
|
||||
public CommonResult<PageResult<ApiVersionRespVO>> getVersionPage(@Valid ApiVersionPageReqVO pageReqVO) {
|
||||
PageResult<ApiVersionDO> pageResult = apiVersionService.getVersionPage(pageReqVO);
|
||||
return success(ApiVersionConvert.INSTANCE.convertPage(pageResult));
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "查询指定 API 的全部版本")
|
||||
@PreAuthorize("@ss.hasPermission('databus:gateway:version:query')")
|
||||
public CommonResult<java.util.List<ApiVersionRespVO>> getVersionList(@RequestParam("apiId") Long apiId) {
|
||||
return success(ApiVersionConvert.INSTANCE.convertList(apiVersionService.getVersionListByApiId(apiId)));
|
||||
}
|
||||
|
||||
@PutMapping("/rollback")
|
||||
@Operation(summary = "回滚到指定版本")
|
||||
@PreAuthorize("@ss.hasPermission('databus:gateway:version:rollback')")
|
||||
public CommonResult<Boolean> rollbackToVersion(@Valid @RequestBody ApiVersionRollbackReqVO reqVO) {
|
||||
apiVersionService.rollbackToVersion(reqVO.getId(), reqVO.getRemark());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/compare")
|
||||
@Operation(summary = "对比两个版本差异")
|
||||
@PreAuthorize("@ss.hasPermission('databus:gateway:version:query')")
|
||||
public CommonResult<ApiVersionCompareRespVO> compareVersions(
|
||||
@RequestParam("sourceId") Long sourceId,
|
||||
@RequestParam("targetId") Long targetId) {
|
||||
return success(apiVersionService.compareVersions(sourceId, targetId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.zt.plat.module.databus.controller.admin.gateway.convert;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogRespVO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ApiAccessLogConvert {
|
||||
|
||||
ApiAccessLogConvert INSTANCE = Mappers.getMapper(ApiAccessLogConvert.class);
|
||||
|
||||
ApiAccessLogRespVO convert(ApiAccessLogDO bean);
|
||||
|
||||
List<ApiAccessLogRespVO> convertList(List<ApiAccessLogDO> list);
|
||||
|
||||
default PageResult<ApiAccessLogRespVO> convertPage(PageResult<ApiAccessLogDO> page) {
|
||||
if (page == null) {
|
||||
return PageResult.empty();
|
||||
}
|
||||
PageResult<ApiAccessLogRespVO> result = new PageResult<>();
|
||||
result.setList(convertList(page.getList()));
|
||||
result.setTotal(page.getTotal());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,10 @@ 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.controller.admin.gateway.vo.definition.*;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiTransformDO;
|
||||
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
|
||||
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiFlowPublication;
|
||||
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiStepDefinition;
|
||||
@@ -101,4 +99,29 @@ public interface ApiDefinitionConvert {
|
||||
return publication == null ? null : BeanUtils.toBean(publication, ApiDefinitionPublicationRespVO.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换步骤列表(DO -> SaveReqVO)
|
||||
*/
|
||||
default List<ApiDefinitionStepSaveReqVO> convertStepList(List<ApiStepDO> steps) {
|
||||
if (CollUtil.isEmpty(steps)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return steps.stream()
|
||||
.sorted(Comparator.comparing(step -> step.getStepOrder() == null ? Integer.MAX_VALUE : step.getStepOrder()))
|
||||
.map(step -> BeanUtils.toBean(step, ApiDefinitionStepSaveReqVO.class))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换变换列表(DO -> SaveReqVO)
|
||||
*/
|
||||
default List<ApiDefinitionTransformSaveReqVO> convertTransformList(List<ApiTransformDO> transforms) {
|
||||
if (CollUtil.isEmpty(transforms)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return transforms.stream()
|
||||
.map(transform -> BeanUtils.toBean(transform, ApiDefinitionTransformSaveReqVO.class))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.zt.plat.module.databus.controller.admin.gateway.convert;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionDetailRespVO;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionRespVO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* API 版本历史 Convert。
|
||||
*/
|
||||
@Mapper
|
||||
public interface ApiVersionConvert {
|
||||
|
||||
ApiVersionConvert INSTANCE = Mappers.getMapper(ApiVersionConvert.class);
|
||||
|
||||
ApiVersionRespVO convert(ApiVersionDO bean);
|
||||
|
||||
PageResult<ApiVersionRespVO> convertPage(PageResult<ApiVersionDO> page);
|
||||
|
||||
List<ApiVersionRespVO> convertList(List<ApiVersionDO> list);
|
||||
|
||||
@Mapping(target = "snapshotData", ignore = true)
|
||||
ApiVersionDetailRespVO convertDetail(ApiVersionDO bean);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
/**
|
||||
* Databus API 访问日志分页查询 VO。
|
||||
*/
|
||||
@Schema(description = "管理后台 - Databus API 访问日志分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class ApiAccessLogPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "追踪 ID", example = "c8a3d52f-42c8-4b5d-9e26-8c2cc89f6bb5")
|
||||
private String traceId;
|
||||
|
||||
@Schema(description = "API 编码", example = "user.query")
|
||||
private String apiCode;
|
||||
|
||||
@Schema(description = "API 版本", example = "v1")
|
||||
private String apiVersion;
|
||||
|
||||
@Schema(description = "HTTP 方法", example = "POST")
|
||||
private String requestMethod;
|
||||
|
||||
@Schema(description = "响应 HTTP 状态", example = "200")
|
||||
private Integer responseStatus;
|
||||
|
||||
@Schema(description = "访问状态", example = "0")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "客户端 IP", example = "192.168.0.10")
|
||||
private String clientIp;
|
||||
|
||||
@Schema(description = "租户编号", example = "1")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "请求路径", example = "/gateway/api/user/query")
|
||||
private String requestPath;
|
||||
|
||||
@Schema(description = "请求时间区间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] requestTime;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Databus API 访问日志 Response VO。
|
||||
*/
|
||||
@Schema(description = "管理后台 - Databus API 访问日志 Response VO")
|
||||
@Data
|
||||
public class ApiAccessLogRespVO {
|
||||
|
||||
@Schema(description = "日志编号", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "追踪 ID", example = "c8a3d52f-42c8-4b5d-9e26-8c2cc89f6bb5")
|
||||
private String traceId;
|
||||
|
||||
@Schema(description = "API 编码", example = "user.query")
|
||||
private String apiCode;
|
||||
|
||||
@Schema(description = "API 版本", example = "v1")
|
||||
private String apiVersion;
|
||||
|
||||
@Schema(description = "HTTP 方法", example = "POST")
|
||||
private String requestMethod;
|
||||
|
||||
@Schema(description = "请求路径", example = "/gateway/api/user/query")
|
||||
private String requestPath;
|
||||
|
||||
@Schema(description = "查询参数(JSON)")
|
||||
private String requestQuery;
|
||||
|
||||
@Schema(description = "请求头(JSON)")
|
||||
private String requestHeaders;
|
||||
|
||||
@Schema(description = "请求体(JSON)")
|
||||
private String requestBody;
|
||||
|
||||
@Schema(description = "响应 HTTP 状态", example = "200")
|
||||
private Integer responseStatus;
|
||||
|
||||
@Schema(description = "响应提示", example = "OK")
|
||||
private String responseMessage;
|
||||
|
||||
@Schema(description = "响应体(JSON)")
|
||||
private String responseBody;
|
||||
|
||||
@Schema(description = "访问状态", example = "0")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "错误码", example = "DAT-001")
|
||||
private String errorCode;
|
||||
|
||||
@Schema(description = "错误信息", example = "API 调用失败")
|
||||
private String errorMessage;
|
||||
|
||||
@Schema(description = "异常堆栈")
|
||||
private String exceptionStack;
|
||||
|
||||
@Schema(description = "客户端 IP", example = "192.168.0.10")
|
||||
private String clientIp;
|
||||
|
||||
@Schema(description = "User-Agent")
|
||||
private String userAgent;
|
||||
|
||||
@Schema(description = "租户编号", example = "1")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "请求耗时(毫秒)", example = "123")
|
||||
private Long duration;
|
||||
|
||||
@Schema(description = "请求时间")
|
||||
private LocalDateTime requestTime;
|
||||
|
||||
@Schema(description = "响应时间")
|
||||
private LocalDateTime responseTime;
|
||||
|
||||
@Schema(description = "执行步骤(JSON)")
|
||||
private String stepResults;
|
||||
|
||||
@Schema(description = "额外调试信息(JSON)")
|
||||
private String extra;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.zt.plat.module.databus.controller.admin.gateway.vo.version;
|
||||
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* API 版本对比 Response VO。
|
||||
*/
|
||||
@Schema(description = "管理后台 - API 版本对比 Response VO")
|
||||
@Data
|
||||
public class ApiVersionCompareRespVO {
|
||||
|
||||
@Schema(description = "源版本 ID", example = "1001")
|
||||
private Long sourceVersionId;
|
||||
|
||||
@Schema(description = "源版本号", example = "2")
|
||||
private Integer sourceVersionNumber;
|
||||
|
||||
@Schema(description = "源版本描述")
|
||||
private String sourceDescription;
|
||||
|
||||
@Schema(description = "源版本操作人")
|
||||
private String sourceOperator;
|
||||
|
||||
@Schema(description = "源版本创建时间")
|
||||
private LocalDateTime sourceCreateTime;
|
||||
|
||||
@Schema(description = "目标版本 ID", example = "1002")
|
||||
private Long targetVersionId;
|
||||
|
||||
@Schema(description = "目标版本号", example = "3")
|
||||
private Integer targetVersionNumber;
|
||||
|
||||
@Schema(description = "目标版本描述")
|
||||
private String targetDescription;
|
||||
|
||||
@Schema(description = "目标版本操作人")
|
||||
private String targetOperator;
|
||||
|
||||
@Schema(description = "目标版本创建时间")
|
||||
private LocalDateTime targetCreateTime;
|
||||
|
||||
@Schema(description = "源版本快照")
|
||||
private ApiDefinitionSaveReqVO sourceSnapshot;
|
||||
|
||||
@Schema(description = "目标版本快照")
|
||||
private ApiDefinitionSaveReqVO targetSnapshot;
|
||||
|
||||
@Schema(description = "两者是否完全一致")
|
||||
private Boolean same;
|
||||
|
||||
@Schema(description = "字段差异列表")
|
||||
private List<FieldDiff> differences;
|
||||
|
||||
@Data
|
||||
public static class FieldDiff {
|
||||
|
||||
@Schema(description = "差异字段路径", example = "/steps[0]/targetEndpoint")
|
||||
private String path;
|
||||
|
||||
@Schema(description = "源版本值")
|
||||
private String sourceValue;
|
||||
|
||||
@Schema(description = "目标版本值")
|
||||
private String targetValue;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.zt.plat.module.databus.controller.admin.gateway.vo.version;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* API 版本历史创建 Request VO。
|
||||
*/
|
||||
@Schema(description = "管理后台 - API 版本历史创建 Request VO")
|
||||
@Data
|
||||
public class ApiVersionCreateReqVO {
|
||||
|
||||
@Schema(description = "API 定义 ID", required = true, example = "1024")
|
||||
@NotNull(message = "API 定义 ID 不能为空")
|
||||
private Long apiId;
|
||||
|
||||
@Schema(description = "版本号", required = true, example = "v1.0.0")
|
||||
@NotBlank(message = "版本号不能为空")
|
||||
private String versionNumber;
|
||||
|
||||
@Schema(description = "版本描述", example = "初始版本")
|
||||
private String description;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.zt.plat.module.databus.controller.admin.gateway.vo.version;
|
||||
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 版本详情 Response VO。
|
||||
* 包含完整的 API 定义快照数据。
|
||||
*/
|
||||
@Schema(description = "管理后台 - API 版本详情 Response VO")
|
||||
@Data
|
||||
public class ApiVersionDetailRespVO {
|
||||
|
||||
@Schema(description = "主键", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "API 定义 ID", example = "1001")
|
||||
private Long apiId;
|
||||
|
||||
@Schema(description = "版本号", example = "1")
|
||||
private Integer versionNumber;
|
||||
|
||||
@Schema(description = "版本描述", example = "初始版本")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "是否为当前版本", example = "true")
|
||||
private Boolean isCurrent;
|
||||
|
||||
@Schema(description = "操作人", example = "admin")
|
||||
private String operator;
|
||||
|
||||
@Schema(description = "API 定义快照数据")
|
||||
private ApiDefinitionSaveReqVO snapshotData;
|
||||
|
||||
@Schema(description = "创建者", example = "admin")
|
||||
private String creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "租户编号", example = "1")
|
||||
private Long tenantId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.zt.plat.module.databus.controller.admin.gateway.vo.version;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static com.zt.plat.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
/**
|
||||
* API 版本历史分页查询 VO。
|
||||
*/
|
||||
@Schema(description = "管理后台 - API 版本历史分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class ApiVersionPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "API 定义 ID", required = true, example = "1024")
|
||||
private Long apiId;
|
||||
|
||||
@Schema(description = "版本号", example = "1")
|
||||
private Integer versionNumber;
|
||||
|
||||
@Schema(description = "创建时间区间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.zt.plat.module.databus.controller.admin.gateway.vo.version;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 版本历史 Response VO。
|
||||
*/
|
||||
@Schema(description = "管理后台 - API 版本历史 Response VO")
|
||||
@Data
|
||||
public class ApiVersionRespVO {
|
||||
|
||||
@Schema(description = "主键", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "API 定义 ID", example = "1001")
|
||||
private Long apiId;
|
||||
|
||||
@Schema(description = "版本号", example = "1")
|
||||
private Integer versionNumber;
|
||||
|
||||
@Schema(description = "版本描述", example = "初始版本")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "是否为当前版本", example = "true")
|
||||
private Boolean isCurrent;
|
||||
|
||||
@Schema(description = "操作人", example = "admin")
|
||||
private String operator;
|
||||
|
||||
@Schema(description = "创建者", example = "admin")
|
||||
private String creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "租户编号", example = "1")
|
||||
private Long tenantId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.zt.plat.module.databus.controller.admin.gateway.vo.version;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* API 版本回滚 Request VO。
|
||||
*/
|
||||
@Schema(description = "管理后台 - API 版本回滚 Request VO")
|
||||
@Data
|
||||
public class ApiVersionRollbackReqVO {
|
||||
|
||||
@Schema(description = "待回滚的版本 ID", required = true, example = "1024")
|
||||
@NotNull(message = "版本 ID 不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "回滚备注", example = "回滚到版本 v5")
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.zt.plat.module.databus.dal.dataobject.gateway;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Databus API 访问日志数据对象。
|
||||
*
|
||||
* <p>用于记录 API 编排网关的请求与响应详情,便于审计与问题排查。</p>
|
||||
*/
|
||||
@TableName("databus_api_access_log")
|
||||
@KeySequence("databus_api_access_log_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ApiAccessLogDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 请求追踪标识,对应 {@link com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext#getRequestId()}
|
||||
*/
|
||||
private String traceId;
|
||||
|
||||
/**
|
||||
* API 编码
|
||||
*/
|
||||
private String apiCode;
|
||||
|
||||
/**
|
||||
* API 版本
|
||||
*/
|
||||
private String apiVersion;
|
||||
|
||||
/**
|
||||
* HTTP 方法
|
||||
*/
|
||||
private String requestMethod;
|
||||
|
||||
/**
|
||||
* 请求路径
|
||||
*/
|
||||
private String requestPath;
|
||||
|
||||
/**
|
||||
* 查询参数(JSON 字符串)
|
||||
*/
|
||||
private String requestQuery;
|
||||
|
||||
/**
|
||||
* 请求头信息(JSON 字符串)
|
||||
*/
|
||||
private String requestHeaders;
|
||||
|
||||
/**
|
||||
* 请求体(JSON 字符串)
|
||||
*/
|
||||
private String requestBody;
|
||||
|
||||
/**
|
||||
* 响应 HTTP 状态码
|
||||
*/
|
||||
private Integer responseStatus;
|
||||
|
||||
/**
|
||||
* 响应提示信息
|
||||
*/
|
||||
private String responseMessage;
|
||||
|
||||
/**
|
||||
* 响应体(JSON 字符串)
|
||||
*/
|
||||
private String responseBody;
|
||||
|
||||
/**
|
||||
* 访问状态:0-成功 1-客户端错误 2-服务端错误 3-未知
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 业务错误码
|
||||
*/
|
||||
private String errorCode;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 异常堆栈
|
||||
*/
|
||||
private String exceptionStack;
|
||||
|
||||
/**
|
||||
* 客户端 IP
|
||||
*/
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* User-Agent
|
||||
*/
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 请求耗时(毫秒)
|
||||
*/
|
||||
private Long duration;
|
||||
|
||||
/**
|
||||
* 请求时间
|
||||
*/
|
||||
private LocalDateTime requestTime;
|
||||
|
||||
/**
|
||||
* 响应时间
|
||||
*/
|
||||
private LocalDateTime responseTime;
|
||||
|
||||
/**
|
||||
* 执行步骤结果(JSON 字符串)
|
||||
*/
|
||||
private String stepResults;
|
||||
|
||||
/**
|
||||
* 额外调试信息(JSON 字符串)
|
||||
*/
|
||||
private String extra;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.zt.plat.module.databus.dal.dataobject.gateway;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* API 版本历史数据对象
|
||||
*
|
||||
* <p>每次修改 API 配置时自动创建新版本记录,支持完整的版本历史追溯和回滚。</p>
|
||||
* <p>版本号自动递增,不可删除,保留完整的历史记录链。</p>
|
||||
*/
|
||||
@TableName("databus_api_version")
|
||||
@KeySequence("databus_api_version_seq")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ApiVersionDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* API 定义 ID
|
||||
*/
|
||||
private Long apiId;
|
||||
|
||||
/**
|
||||
* 版本号(自动递增,从 1 开始)
|
||||
*/
|
||||
private Integer versionNumber;
|
||||
|
||||
/**
|
||||
* API 完整定义快照(JSON 格式,包含 definition、steps、transforms 等)
|
||||
*/
|
||||
private String snapshotData;
|
||||
|
||||
/**
|
||||
* 版本描述/变更说明
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 是否为当前版本(最新使用的版本)
|
||||
*/
|
||||
private Boolean isCurrent;
|
||||
|
||||
/**
|
||||
* 操作人
|
||||
*/
|
||||
private String operator;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.zt.plat.module.databus.dal.mapper.gateway;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionPageReqVO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* API 版本历史 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ApiVersionMapper extends BaseMapperX<ApiVersionDO> {
|
||||
|
||||
/**
|
||||
* 分页查询版本历史
|
||||
*/
|
||||
default PageResult<ApiVersionDO> selectPage(ApiVersionPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<ApiVersionDO>()
|
||||
.eqIfPresent(ApiVersionDO::getApiId, reqVO.getApiId())
|
||||
.eqIfPresent(ApiVersionDO::getVersionNumber, reqVO.getVersionNumber())
|
||||
.betweenIfPresent(ApiVersionDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(ApiVersionDO::getVersionNumber));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定 API 的所有版本历史
|
||||
*/
|
||||
default List<ApiVersionDO> selectListByApiId(Long apiId) {
|
||||
return selectList(new LambdaQueryWrapperX<ApiVersionDO>()
|
||||
.eq(ApiVersionDO::getApiId, apiId)
|
||||
.orderByDesc(ApiVersionDO::getVersionNumber));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定 API 的当前版本
|
||||
*/
|
||||
default ApiVersionDO selectCurrentByApiId(Long apiId) {
|
||||
return selectOne(new LambdaQueryWrapperX<ApiVersionDO>()
|
||||
.eq(ApiVersionDO::getApiId, apiId)
|
||||
.eq(ApiVersionDO::getIsCurrent, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定 API 的最大版本号
|
||||
*/
|
||||
default Integer selectMaxVersionNumber(Long apiId) {
|
||||
ApiVersionDO maxVersion = selectOne(new LambdaQueryWrapperX<ApiVersionDO>()
|
||||
.eq(ApiVersionDO::getApiId, apiId)
|
||||
.orderByDesc(ApiVersionDO::getVersionNumber)
|
||||
.last("LIMIT 1"));
|
||||
return maxVersion != null ? maxVersion.getVersionNumber() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将所有版本标记为非当前版本
|
||||
*/
|
||||
default void markAllAsNotCurrent(Long apiId) {
|
||||
UpdateWrapper<ApiVersionDO> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("api_id", apiId)
|
||||
.set("is_current", false);
|
||||
update(null, updateWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定版本
|
||||
*/
|
||||
default ApiVersionDO selectByApiIdAndVersionNumber(Long apiId, Integer versionNumber) {
|
||||
return selectOne(new LambdaQueryWrapperX<ApiVersionDO>()
|
||||
.eq(ApiVersionDO::getApiId, apiId)
|
||||
.eq(ApiVersionDO::getVersionNumber, versionNumber));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.zt.plat.module.databus.dal.mysql.gateway;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogPageReqVO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ApiAccessLogMapper extends BaseMapperX<ApiAccessLogDO> {
|
||||
|
||||
default PageResult<ApiAccessLogDO> selectPage(ApiAccessLogPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<ApiAccessLogDO> query = new LambdaQueryWrapperX<ApiAccessLogDO>()
|
||||
.likeIfPresent(ApiAccessLogDO::getTraceId, reqVO.getTraceId())
|
||||
.eqIfPresent(ApiAccessLogDO::getApiCode, reqVO.getApiCode())
|
||||
.eqIfPresent(ApiAccessLogDO::getApiVersion, reqVO.getApiVersion())
|
||||
.eqIfPresent(ApiAccessLogDO::getRequestMethod, reqVO.getRequestMethod())
|
||||
.eqIfPresent(ApiAccessLogDO::getResponseStatus, reqVO.getResponseStatus())
|
||||
.eqIfPresent(ApiAccessLogDO::getStatus, reqVO.getStatus())
|
||||
.likeIfPresent(ApiAccessLogDO::getClientIp, reqVO.getClientIp())
|
||||
.eqIfPresent(ApiAccessLogDO::getTenantId, reqVO.getTenantId())
|
||||
.likeIfPresent(ApiAccessLogDO::getRequestPath, reqVO.getRequestPath());
|
||||
if (ArrayUtil.isNotEmpty(reqVO.getRequestTime()) && reqVO.getRequestTime().length == 2) {
|
||||
query.between(ApiAccessLogDO::getRequestTime, reqVO.getRequestTime()[0], reqVO.getRequestTime()[1]);
|
||||
}
|
||||
return selectPage(reqVO, query.orderByDesc(ApiAccessLogDO::getRequestTime)
|
||||
.orderByDesc(ApiAccessLogDO::getId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package com.zt.plat.module.databus.framework.integration.gateway.core;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
|
||||
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
|
||||
import com.zt.plat.module.databus.service.gateway.ApiAccessLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 将 API 调用上下文持久化为访问日志。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ApiGatewayAccessLogger {
|
||||
|
||||
public static final String ATTR_LOG_ID = "ApiAccessLogId";
|
||||
public static final String ATTR_EXCEPTION_STACK = "ApiAccessLogExceptionStack";
|
||||
|
||||
private static final int MAX_TEXT_LENGTH = 4000;
|
||||
|
||||
private final ApiAccessLogService apiAccessLogService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 在分发前记录请求信息。
|
||||
*/
|
||||
public void onRequest(ApiInvocationContext context) {
|
||||
try {
|
||||
ApiAccessLogDO logDO = new ApiAccessLogDO();
|
||||
logDO.setTraceId(context.getRequestId());
|
||||
logDO.setApiCode(context.getApiCode());
|
||||
logDO.setApiVersion(context.getApiVersion());
|
||||
logDO.setRequestMethod(context.getHttpMethod());
|
||||
logDO.setRequestPath(context.getRequestPath());
|
||||
logDO.setRequestQuery(toJson(context.getRequestQueryParams()));
|
||||
logDO.setRequestHeaders(toJson(context.getRequestHeaders()));
|
||||
logDO.setRequestBody(toJson(context.getRequestBody()));
|
||||
logDO.setClientIp(firstNonBlank(context.getClientIp(),
|
||||
GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), "X-Forwarded-For")));
|
||||
logDO.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT));
|
||||
logDO.setStatus(3); // 默认未知
|
||||
logDO.setRequestTime(toLocalDateTime(context.getRequestTime()));
|
||||
logDO.setTenantId(parseTenantId(context.getTenantId()));
|
||||
Long logId = apiAccessLogService.create(logDO);
|
||||
context.getAttributes().put(ATTR_LOG_ID, logId);
|
||||
} catch (Exception ex) {
|
||||
log.warn("记录 API 访问日志开始阶段失败, traceId={}", context.getRequestId(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录异常堆栈,便于后续写入日志。
|
||||
*/
|
||||
public void onException(ApiInvocationContext context, Throwable throwable) {
|
||||
if (throwable == null) {
|
||||
return;
|
||||
}
|
||||
context.getAttributes().put(ATTR_EXCEPTION_STACK, buildStackTrace(throwable));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在分发完成后补全日志信息。
|
||||
*/
|
||||
public void onResponse(ApiInvocationContext context) {
|
||||
Long logId = getLogId(context);
|
||||
if (logId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ApiAccessLogDO update = new ApiAccessLogDO();
|
||||
update.setId(logId);
|
||||
update.setResponseStatus(context.getResponseStatus());
|
||||
update.setResponseMessage(context.getResponseMessage());
|
||||
update.setResponseBody(toJson(context.getResponseBody()));
|
||||
update.setStatus(resolveStatus(context.getResponseStatus()));
|
||||
update.setErrorCode(extractErrorCode(context.getResponseBody()));
|
||||
update.setErrorMessage(resolveErrorMessage(context));
|
||||
update.setExceptionStack((String) context.getAttributes().get(ATTR_EXCEPTION_STACK));
|
||||
update.setStepResults(toJson(context.getStepResults()));
|
||||
update.setExtra(toJson(buildExtra(context)));
|
||||
update.setResponseTime(LocalDateTime.now());
|
||||
update.setDuration(calculateDuration(context));
|
||||
apiAccessLogService.update(update);
|
||||
} catch (Exception ex) {
|
||||
log.warn("记录 API 访问日志结束阶段失败, traceId={}, logId={}", context.getRequestId(), logId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Long getLogId(ApiInvocationContext context) {
|
||||
Object value = context.getAttributes().get(ATTR_LOG_ID);
|
||||
if (value instanceof Long) {
|
||||
return (Long) value;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long calculateDuration(ApiInvocationContext context) {
|
||||
Instant start = context.getRequestTime();
|
||||
if (start == null) {
|
||||
return null;
|
||||
}
|
||||
return Duration.between(start, Instant.now()).toMillis();
|
||||
}
|
||||
|
||||
private Integer resolveStatus(Integer httpStatus) {
|
||||
if (httpStatus == null) {
|
||||
return 3;
|
||||
}
|
||||
if (httpStatus >= 200 && httpStatus < 400) {
|
||||
return 0;
|
||||
}
|
||||
if (httpStatus >= 400 && httpStatus < 500) {
|
||||
return 1;
|
||||
}
|
||||
if (httpStatus >= 500) {
|
||||
return 2;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
|
||||
private String resolveErrorMessage(ApiInvocationContext context) {
|
||||
if (StringUtils.hasText(context.getResponseMessage())) {
|
||||
return truncate(context.getResponseMessage());
|
||||
}
|
||||
Object responseBody = context.getResponseBody();
|
||||
if (responseBody instanceof Map<?, ?> map) {
|
||||
Object message = firstNonNull(map.get("errorMessage"), map.get("message"));
|
||||
if (message != null) {
|
||||
return truncate(String.valueOf(message));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractErrorCode(Object responseBody) {
|
||||
if (responseBody instanceof Map<?, ?> map) {
|
||||
Object errorCode = firstNonNull(map.get("errorCode"), map.get("code"));
|
||||
return errorCode == null ? null : truncate(String.valueOf(errorCode));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildExtra(ApiInvocationContext context) {
|
||||
Map<String, Object> extra = new HashMap<>();
|
||||
if (!CollectionUtils.isEmpty(context.getVariables())) {
|
||||
extra.put("variables", context.getVariables());
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(context.getAttributes())) {
|
||||
Map<String, Object> attributes = new HashMap<>(context.getAttributes());
|
||||
attributes.remove(ATTR_LOG_ID);
|
||||
attributes.remove(ATTR_EXCEPTION_STACK);
|
||||
if (!attributes.isEmpty()) {
|
||||
extra.put("attributes", attributes);
|
||||
}
|
||||
}
|
||||
if (CollectionUtils.isEmpty(extra)) {
|
||||
return null;
|
||||
}
|
||||
return extra;
|
||||
}
|
||||
|
||||
private String toJson(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof String str) {
|
||||
return truncate(str);
|
||||
}
|
||||
try {
|
||||
return truncate(objectMapper.writeValueAsString(value));
|
||||
} catch (JsonProcessingException ex) {
|
||||
return truncate(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
|
||||
private String truncate(String text) {
|
||||
if (!StringUtils.hasText(text)) {
|
||||
return text;
|
||||
}
|
||||
if (text.length() <= MAX_TEXT_LENGTH) {
|
||||
return text;
|
||||
}
|
||||
return text.substring(0, MAX_TEXT_LENGTH);
|
||||
}
|
||||
|
||||
private Long parseTenantId(String tenantId) {
|
||||
if (!StringUtils.hasText(tenantId)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(tenantId.trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDateTime toLocalDateTime(Instant instant) {
|
||||
if (instant == null) {
|
||||
return null;
|
||||
}
|
||||
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
private String buildStackTrace(Throwable throwable) {
|
||||
try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
|
||||
throwable.printStackTrace(pw);
|
||||
return truncate(sw.toString());
|
||||
} catch (Exception ex) {
|
||||
return throwable.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private String firstNonBlank(String... values) {
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (StringUtils.hasText(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object firstNonNull(Object... values) {
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
for (Object value : values) {
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -38,12 +38,15 @@ public class ApiGatewayExecutionService {
|
||||
private static final String HEADER_REQUEST_URI = org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri";
|
||||
private static final String HEADER_REQUEST_PARAMS = org.springframework.integration.http.HttpHeaders.PREFIX + "requestParams";
|
||||
private static final String HEADER_QUERY_STRING = org.springframework.integration.http.HttpHeaders.PREFIX + "queryString";
|
||||
private static final String HEADER_REMOTE_ADDRESS = org.springframework.integration.http.HttpHeaders.PREFIX + "remoteAddress";
|
||||
private static final String LOCAL_DEBUG_REMOTE_ADDRESS = "127.0.0.1";
|
||||
|
||||
private final ApiGatewayRequestMapper requestMapper;
|
||||
private final ApiFlowDispatcher apiFlowDispatcher;
|
||||
private final ApiGatewayErrorProcessor errorProcessor;
|
||||
private final ApiGatewayProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ApiGatewayAccessLogger accessLogger;
|
||||
|
||||
/**
|
||||
* Maps a raw HTTP message (as provided by Spring Integration) into a context message.
|
||||
@@ -62,26 +65,34 @@ public class ApiGatewayExecutionService {
|
||||
*/
|
||||
public ApiInvocationContext dispatch(Message<ApiInvocationContext> message) {
|
||||
ApiInvocationContext context = message.getPayload();
|
||||
accessLogger.onRequest(context);
|
||||
ApiInvocationContext responseContext;
|
||||
try {
|
||||
return apiFlowDispatcher.dispatch(context.getApiCode(), context.getApiVersion(), context);
|
||||
responseContext = apiFlowDispatcher.dispatch(context.getApiCode(), context.getApiVersion(), context);
|
||||
} catch (ServiceException ex) {
|
||||
errorProcessor.applyServiceException(context, ex);
|
||||
accessLogger.onException(context, ex);
|
||||
log.warn("[API-PORTAL] 分发 apiCode={} version={} 时出现 ServiceException: {}", context.getApiCode(), context.getApiVersion(), ex.getMessage());
|
||||
return context;
|
||||
responseContext = context;
|
||||
} catch (Exception ex) {
|
||||
ServiceException nestedServiceException = errorProcessor.resolveServiceException(ex);
|
||||
if (nestedServiceException != null) {
|
||||
errorProcessor.applyServiceException(context, nestedServiceException);
|
||||
accessLogger.onException(context, nestedServiceException);
|
||||
log.warn("[API-PORTAL] 分发 apiCode={} version={} 时出现 ServiceException(包装异常): {}", context.getApiCode(), context.getApiVersion(), nestedServiceException.getMessage());
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("[API-PORTAL] 包装异常堆栈", ex);
|
||||
}
|
||||
} else {
|
||||
errorProcessor.applyUnexpectedException(context, ex);
|
||||
accessLogger.onException(context, ex);
|
||||
log.error("[API-PORTAL] 分发 apiCode={} version={} 时出现未预期异常", context.getApiCode(), context.getApiVersion(), ex);
|
||||
}
|
||||
return context;
|
||||
responseContext = context;
|
||||
} finally {
|
||||
accessLogger.onResponse(context);
|
||||
}
|
||||
return responseContext;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +126,7 @@ public class ApiGatewayExecutionService {
|
||||
"version", reqVO.getVersion()
|
||||
);
|
||||
builder.setHeader(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables);
|
||||
builder.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD, HttpMethod.POST.name());
|
||||
builder.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD, HttpMethod.POST.name());
|
||||
|
||||
String basePath = normalizeBasePath(properties.getBasePath());
|
||||
String rawQuery = buildQueryString(reqVO.getQueryParams());
|
||||
@@ -125,10 +136,11 @@ public class ApiGatewayExecutionService {
|
||||
}
|
||||
builder.setHeader(HEADER_REQUEST_URI, requestUri);
|
||||
builder.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_URL, requestUri);
|
||||
builder.setHeader(HEADER_REMOTE_ADDRESS, LOCAL_DEBUG_REMOTE_ADDRESS);
|
||||
|
||||
Map<String, Object> requestHeaders = new LinkedHashMap<>();
|
||||
if (reqVO.getHeaders() != null) {
|
||||
reqVO.getHeaders().forEach(requestHeaders::put);
|
||||
requestHeaders.putAll(reqVO.getHeaders());
|
||||
}
|
||||
normalizeJwtHeaders(requestHeaders, reqVO.getQueryParams());
|
||||
requestHeaders.putIfAbsent(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
@@ -33,6 +33,7 @@ public class ApiGatewayRequestMapper {
|
||||
|
||||
private static final String HEADER_REQUEST_HEADERS = org.springframework.integration.http.HttpHeaders.PREFIX + "requestHeaders";
|
||||
private static final String HEADER_REQUEST_URI = org.springframework.integration.http.HttpHeaders.PREFIX + "requestUri";
|
||||
private static final String HEADER_REMOTE_ADDRESS = org.springframework.integration.http.HttpHeaders.PREFIX + "remoteAddress";
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public ApiInvocationContext map(Object payload, Map<String, Object> headers) {
|
||||
@@ -87,6 +88,8 @@ public class ApiGatewayRequestMapper {
|
||||
context.getRequestHeaders().putIfAbsent(key, normalized);
|
||||
}
|
||||
});
|
||||
context.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT));
|
||||
context.setClientIp(resolveClientIp(headers, context.getRequestHeaders()));
|
||||
populateQueryParams(headers, context, originalRequestUri);
|
||||
if (properties.isEnableTenantHeader()) {
|
||||
Object tenantHeaderValue = context.getRequestHeaders().get(properties.getTenantHeader());
|
||||
@@ -315,4 +318,26 @@ public class ApiGatewayRequestMapper {
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private String resolveClientIp(Map<String, Object> headers, Map<String, Object> requestHeaders) {
|
||||
String forwarded = GatewayHeaderUtils.findFirstHeaderValue(requestHeaders, "X-Forwarded-For");
|
||||
if (StringUtils.hasText(forwarded)) {
|
||||
int idx = forwarded.indexOf(',');
|
||||
return idx >= 0 ? forwarded.substring(0, idx).trim() : forwarded;
|
||||
}
|
||||
String realIp = GatewayHeaderUtils.findFirstHeaderValue(requestHeaders, "X-Real-IP");
|
||||
if (StringUtils.hasText(realIp)) {
|
||||
return realIp;
|
||||
}
|
||||
Object remote = headers.get(HEADER_REMOTE_ADDRESS);
|
||||
if (remote != null) {
|
||||
String candidate = remote.toString();
|
||||
int slash = candidate.indexOf('/') >= 0 ? candidate.indexOf('/') : candidate.indexOf(':');
|
||||
if (slash > 0) {
|
||||
candidate = candidate.substring(0, slash);
|
||||
}
|
||||
return candidate.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ public class ApiInvocationContext {
|
||||
|
||||
private String tenantId;
|
||||
|
||||
private String clientIp;
|
||||
|
||||
private String userAgent;
|
||||
|
||||
private String httpMethod;
|
||||
|
||||
private String requestPath;
|
||||
@@ -66,6 +70,8 @@ public class ApiInvocationContext {
|
||||
copy.apiCode = this.apiCode;
|
||||
copy.apiVersion = this.apiVersion;
|
||||
copy.tenantId = this.tenantId;
|
||||
copy.clientIp = this.clientIp;
|
||||
copy.userAgent = this.userAgent;
|
||||
copy.httpMethod = this.httpMethod;
|
||||
copy.requestPath = this.requestPath;
|
||||
copy.requestBody = this.requestBody;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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.accesslog.ApiAccessLogPageReqVO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
|
||||
|
||||
/**
|
||||
* Databus API 访问日志 Service。
|
||||
*/
|
||||
public interface ApiAccessLogService {
|
||||
|
||||
/**
|
||||
* 新增访问日志。
|
||||
*
|
||||
* @param logDO 日志信息
|
||||
* @return 日志编号
|
||||
*/
|
||||
Long create(ApiAccessLogDO logDO);
|
||||
|
||||
/**
|
||||
* 更新访问日志(仅更新非空字段)。
|
||||
*
|
||||
* @param logDO 日志信息
|
||||
*/
|
||||
void update(ApiAccessLogDO logDO);
|
||||
|
||||
/**
|
||||
* 根据编号获取访问日志。
|
||||
*
|
||||
* @param id 日志编号
|
||||
* @return 日志
|
||||
*/
|
||||
ApiAccessLogDO get(Long id);
|
||||
|
||||
/**
|
||||
* 分页查询访问日志。
|
||||
*
|
||||
* @param pageReqVO 查询条件
|
||||
* @return 日志分页
|
||||
*/
|
||||
PageResult<ApiAccessLogDO> getPage(ApiAccessLogPageReqVO pageReqVO);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
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.version.ApiVersionCompareRespVO;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionPageReqVO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* API 版本历史 Service 接口(备忘录模式)。
|
||||
*/
|
||||
public interface ApiVersionService {
|
||||
|
||||
/**
|
||||
* 捕获当前 API 配置快照并生成版本记录。
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @param description 版本描述(可为空)
|
||||
* @param operator 操作人
|
||||
* @return 版本 ID
|
||||
*/
|
||||
Long autoCreateVersion(Long apiId, String description, String operator);
|
||||
|
||||
/**
|
||||
* 查询版本详情。
|
||||
*
|
||||
* @param id 版本 ID
|
||||
* @return 版本信息
|
||||
*/
|
||||
ApiVersionDO getVersion(Long id);
|
||||
|
||||
/**
|
||||
* 查询指定 API 的版本历史(分页)。
|
||||
*
|
||||
* @param pageReqVO 分页参数
|
||||
* @return 版本列表
|
||||
*/
|
||||
PageResult<ApiVersionDO> getVersionPage(ApiVersionPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 查询指定 API 的全部版本历史(倒序)。
|
||||
*
|
||||
* @param apiId API ID
|
||||
* @return 版本列表
|
||||
*/
|
||||
List<ApiVersionDO> getVersionListByApiId(Long apiId);
|
||||
|
||||
/**
|
||||
* 回滚至指定版本。
|
||||
*
|
||||
* @param id 版本 ID
|
||||
* @param remark 回滚说明(可为空)
|
||||
*/
|
||||
void rollbackToVersion(Long id, String remark);
|
||||
|
||||
/**
|
||||
* 对比两个版本的差异。
|
||||
*
|
||||
* @param sourceVersionId 源版本 ID
|
||||
* @param targetVersionId 目标版本 ID
|
||||
* @return 差异结果
|
||||
*/
|
||||
ApiVersionCompareRespVO compareVersions(Long sourceVersionId, Long targetVersionId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.zt.plat.module.databus.service.gateway;
|
||||
|
||||
/**
|
||||
* Thread-local context to control whether API definition updates should capture version snapshots.
|
||||
*/
|
||||
public final class ApiVersionSnapshotContextHolder {
|
||||
|
||||
private static final ThreadLocal<Boolean> SKIP_SNAPSHOT = new ThreadLocal<>();
|
||||
|
||||
private ApiVersionSnapshotContextHolder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark that the current thread should skip automatic version snapshot creation once.
|
||||
*/
|
||||
public static void markSkipOnce() {
|
||||
SKIP_SNAPSHOT.set(Boolean.TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the current thread requested to skip snapshot creation.
|
||||
*/
|
||||
public static boolean shouldSkip() {
|
||||
return Boolean.TRUE.equals(SKIP_SNAPSHOT.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the skip flag from the current thread.
|
||||
*/
|
||||
public static void clear() {
|
||||
SKIP_SNAPSHOT.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.zt.plat.module.databus.service.gateway.impl;
|
||||
|
||||
import com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.accesslog.ApiAccessLogPageReqVO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
|
||||
import com.zt.plat.module.databus.dal.mysql.gateway.ApiAccessLogMapper;
|
||||
import com.zt.plat.module.databus.service.gateway.ApiAccessLogService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
* Databus API 访问日志 Service 实现。
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class ApiAccessLogServiceImpl implements ApiAccessLogService {
|
||||
|
||||
@Resource
|
||||
private ApiAccessLogMapper apiAccessLogMapper;
|
||||
|
||||
@Override
|
||||
public Long create(ApiAccessLogDO logDO) {
|
||||
apiAccessLogMapper.insert(logDO);
|
||||
return logDO.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(ApiAccessLogDO logDO) {
|
||||
int rows = apiAccessLogMapper.updateById(logDO);
|
||||
if (rows == 0 && log.isDebugEnabled()) {
|
||||
log.debug("访问日志不存在,无法更新。id={}", logDO.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiAccessLogDO get(Long id) {
|
||||
return apiAccessLogMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<ApiAccessLogDO> getPage(ApiAccessLogPageReqVO pageReqVO) {
|
||||
return apiAccessLogMapper.selectPage(pageReqVO);
|
||||
}
|
||||
}
|
||||
@@ -9,31 +9,27 @@ 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.security.core.util.SecurityFrameworkUtils;
|
||||
import com.zt.plat.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.zt.plat.framework.tenant.core.db.TenantBaseDO;
|
||||
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.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.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.dal.dataobject.gateway.*;
|
||||
import com.zt.plat.module.databus.dal.mysql.gateway.*;
|
||||
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 com.zt.plat.module.databus.service.gateway.ApiVersionService;
|
||||
import com.zt.plat.module.databus.service.gateway.ApiVersionSnapshotContextHolder;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -61,6 +57,7 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService {
|
||||
private final ApiFlowPublishMapper apiFlowPublishMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final ObjectProvider<ApiVersionService> apiVersionServiceProvider;
|
||||
|
||||
private LoadingCache<String, Optional<ApiDefinitionAggregate>> definitionCache;
|
||||
|
||||
@@ -133,6 +130,10 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService {
|
||||
|
||||
persistApiLevelTransforms(apiId, reqVO.getApiLevelTransforms());
|
||||
persistSteps(apiId, reqVO.getSteps());
|
||||
|
||||
String operator = SecurityFrameworkUtils.getLoginUserNickname();
|
||||
String description = String.format("创建 API (%s)", reqVO.getVersion());
|
||||
apiVersionServiceProvider.getObject().autoCreateVersion(apiId, description, operator);
|
||||
return apiId;
|
||||
}
|
||||
|
||||
@@ -141,19 +142,35 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService {
|
||||
public void update(ApiDefinitionSaveReqVO reqVO) {
|
||||
ApiDefinitionDO existing = ensureExists(reqVO.getId());
|
||||
|
||||
validateDuplication(reqVO, existing.getId());
|
||||
validateStructure(reqVO);
|
||||
validatePolicies(reqVO);
|
||||
boolean skipSnapshot = ApiVersionSnapshotContextHolder.shouldSkip();
|
||||
|
||||
ApiDefinitionDO updateObj = buildDefinitionDO(reqVO, existing);
|
||||
apiDefinitionMapper.updateById(updateObj);
|
||||
try {
|
||||
validateDuplication(reqVO, existing.getId());
|
||||
validateStructure(reqVO);
|
||||
validatePolicies(reqVO);
|
||||
|
||||
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());
|
||||
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());
|
||||
} finally {
|
||||
if (skipSnapshot) {
|
||||
ApiVersionSnapshotContextHolder.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (skipSnapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
String operator = SecurityFrameworkUtils.getLoginUserNickname();
|
||||
String description = String.format("更新 API (%s)", reqVO.getVersion());
|
||||
apiVersionServiceProvider.getObject().autoCreateVersion(existing.getId(), description, operator);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
package com.zt.plat.module.databus.service.gateway.impl;
|
||||
|
||||
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 com.zt.plat.framework.common.pojo.PageResult;
|
||||
import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.convert.ApiDefinitionConvert;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.definition.ApiDefinitionSaveReqVO;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionCompareRespVO;
|
||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.version.ApiVersionPageReqVO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiDefinitionDO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiStepDO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiTransformDO;
|
||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiVersionDO;
|
||||
import com.zt.plat.module.databus.dal.mapper.gateway.ApiVersionMapper;
|
||||
import com.zt.plat.module.databus.dal.mysql.gateway.ApiDefinitionMapper;
|
||||
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.service.gateway.ApiDefinitionService;
|
||||
import com.zt.plat.module.databus.service.gateway.ApiVersionService;
|
||||
import com.zt.plat.module.databus.service.gateway.ApiVersionSnapshotContextHolder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* API 版本历史 Service 实现类(备忘录模式)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ApiVersionServiceImpl implements ApiVersionService {
|
||||
|
||||
private final ApiVersionMapper apiVersionMapper;
|
||||
private final ApiDefinitionMapper apiDefinitionMapper;
|
||||
private final ApiStepMapper apiStepMapper;
|
||||
private final ApiTransformMapper apiTransformMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ApiDefinitionService apiDefinitionService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long autoCreateVersion(Long apiId, String description, String operator) {
|
||||
ApiDefinitionDO definition = ensureApiExists(apiId);
|
||||
|
||||
ApiDefinitionSaveReqVO snapshot = buildApiSnapshot(definition);
|
||||
String snapshotJson = serializeSnapshot(apiId, snapshot);
|
||||
|
||||
ApiVersionDO currentVersion = apiVersionMapper.selectCurrentByApiId(apiId);
|
||||
if (currentVersion != null && snapshotEquals(currentVersion.getSnapshotData(), snapshotJson)) {
|
||||
log.debug("[API-VERSION] Skip creating snapshot, apiId={} current version remains {}", apiId, currentVersion.getVersionNumber());
|
||||
return currentVersion.getId();
|
||||
}
|
||||
|
||||
Integer maxVersionNumber = apiVersionMapper.selectMaxVersionNumber(apiId);
|
||||
int nextVersionNumber = (maxVersionNumber == null ? 0 : maxVersionNumber) + 1;
|
||||
|
||||
apiVersionMapper.markAllAsNotCurrent(apiId);
|
||||
|
||||
ApiVersionDO version = new ApiVersionDO();
|
||||
version.setApiId(apiId);
|
||||
version.setVersionNumber(nextVersionNumber);
|
||||
version.setDescription(StringUtils.hasText(description) ? description : defaultDescription(nextVersionNumber));
|
||||
version.setIsCurrent(Boolean.TRUE);
|
||||
version.setOperator(StringUtils.hasText(operator) ? operator : "system");
|
||||
version.setSnapshotData(snapshotJson);
|
||||
apiVersionMapper.insert(version);
|
||||
|
||||
log.info("[API-VERSION] Created snapshot apiId={} version=v{} versionId={}", apiId, nextVersionNumber, version.getId());
|
||||
return version.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiVersionDO getVersion(Long id) {
|
||||
ApiVersionDO version = apiVersionMapper.selectById(id);
|
||||
if (version == null) {
|
||||
throw ServiceExceptionUtil.exception(API_VERSION_NOT_FOUND);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<ApiVersionDO> getVersionPage(ApiVersionPageReqVO pageReqVO) {
|
||||
return apiVersionMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ApiVersionDO> getVersionListByApiId(Long apiId) {
|
||||
ensureApiExists(apiId);
|
||||
return apiVersionMapper.selectListByApiId(apiId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void rollbackToVersion(Long id, String remark) {
|
||||
ApiVersionDO targetVersion = getVersion(id);
|
||||
ApiDefinitionSaveReqVO snapshot = deserializeSnapshot(targetVersion);
|
||||
|
||||
ensureApiExists(targetVersion.getApiId());
|
||||
|
||||
ApiVersionSnapshotContextHolder.markSkipOnce();
|
||||
try {
|
||||
apiDefinitionService.update(snapshot);
|
||||
} finally {
|
||||
ApiVersionSnapshotContextHolder.clear();
|
||||
}
|
||||
|
||||
String operator = SecurityFrameworkUtils.getLoginUserNickname();
|
||||
String description = StringUtils.hasText(remark)
|
||||
? remark
|
||||
: String.format("回滚到 v%d", targetVersion.getVersionNumber());
|
||||
autoCreateVersion(targetVersion.getApiId(), description, operator);
|
||||
log.info("[API-VERSION] Rolled back apiId={} to version v{} and created new snapshot", targetVersion.getApiId(), targetVersion.getVersionNumber());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiVersionCompareRespVO compareVersions(Long sourceVersionId, Long targetVersionId) {
|
||||
ApiVersionDO source = getVersion(sourceVersionId);
|
||||
ApiVersionDO target = getVersion(targetVersionId);
|
||||
|
||||
if (!Objects.equals(source.getApiId(), target.getApiId())) {
|
||||
throw ServiceExceptionUtil.exception(API_VERSION_API_MISMATCH);
|
||||
}
|
||||
|
||||
ApiDefinitionSaveReqVO sourceSnapshot = deserializeSnapshot(source);
|
||||
ApiDefinitionSaveReqVO targetSnapshot = deserializeSnapshot(target);
|
||||
|
||||
JsonNode sourceNode = readTree(source.getSnapshotData());
|
||||
JsonNode targetNode = readTree(target.getSnapshotData());
|
||||
|
||||
List<ApiVersionCompareRespVO.FieldDiff> differences = new ArrayList<>();
|
||||
collectDifferences(sourceNode, targetNode, "", differences);
|
||||
|
||||
ApiVersionCompareRespVO respVO = new ApiVersionCompareRespVO();
|
||||
respVO.setSourceVersionId(source.getId());
|
||||
respVO.setSourceVersionNumber(source.getVersionNumber());
|
||||
respVO.setSourceDescription(source.getDescription());
|
||||
respVO.setSourceOperator(source.getOperator());
|
||||
respVO.setSourceCreateTime(source.getCreateTime());
|
||||
respVO.setTargetVersionId(target.getId());
|
||||
respVO.setTargetVersionNumber(target.getVersionNumber());
|
||||
respVO.setTargetDescription(target.getDescription());
|
||||
respVO.setTargetOperator(target.getOperator());
|
||||
respVO.setTargetCreateTime(target.getCreateTime());
|
||||
respVO.setSourceSnapshot(sourceSnapshot);
|
||||
respVO.setTargetSnapshot(targetSnapshot);
|
||||
respVO.setSame(differences.isEmpty());
|
||||
respVO.setDifferences(differences);
|
||||
return respVO;
|
||||
}
|
||||
|
||||
private ApiDefinitionDO ensureApiExists(Long apiId) {
|
||||
ApiDefinitionDO definition = apiDefinitionMapper.selectById(apiId);
|
||||
if (definition == null) {
|
||||
throw ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND);
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
|
||||
private ApiDefinitionSaveReqVO buildApiSnapshot(ApiDefinitionDO definition) {
|
||||
Long apiId = definition.getId();
|
||||
ApiDefinitionSaveReqVO snapshot = new ApiDefinitionSaveReqVO();
|
||||
snapshot.setId(definition.getId());
|
||||
snapshot.setApiCode(definition.getApiCode());
|
||||
snapshot.setVersion(definition.getVersion());
|
||||
snapshot.setHttpMethod(definition.getHttpMethod());
|
||||
snapshot.setStatus(definition.getStatus());
|
||||
snapshot.setDescription(definition.getDescription());
|
||||
snapshot.setRateLimitId(definition.getRateLimitId());
|
||||
snapshot.setResponseTemplate(definition.getResponseTemplate());
|
||||
|
||||
List<ApiStepDO> steps = apiStepMapper.selectByApiId(apiId);
|
||||
if (steps != null && !steps.isEmpty()) {
|
||||
snapshot.setSteps(ApiDefinitionConvert.INSTANCE.convertStepList(steps));
|
||||
}
|
||||
|
||||
List<ApiTransformDO> apiTransforms = apiTransformMapper.selectApiLevelTransforms(apiId);
|
||||
if (apiTransforms != null && !apiTransforms.isEmpty()) {
|
||||
snapshot.setApiLevelTransforms(ApiDefinitionConvert.INSTANCE.convertTransformList(apiTransforms));
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private String serializeSnapshot(Long apiId, ApiDefinitionSaveReqVO snapshot) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(snapshot);
|
||||
} catch (JsonProcessingException ex) {
|
||||
log.error("[API-VERSION] Failed to serialize snapshot, apiId={}", apiId, ex);
|
||||
throw ServiceExceptionUtil.exception(API_VERSION_SNAPSHOT_SERIALIZE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
private ApiDefinitionSaveReqVO deserializeSnapshot(ApiVersionDO version) {
|
||||
try {
|
||||
return objectMapper.readValue(version.getSnapshotData(), ApiDefinitionSaveReqVO.class);
|
||||
} catch (JsonProcessingException ex) {
|
||||
log.error("[API-VERSION] Failed to deserialize snapshot, versionId={}", version.getId(), ex);
|
||||
throw ServiceExceptionUtil.exception(API_VERSION_SNAPSHOT_DESERIALIZE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean snapshotEquals(String left, String right) {
|
||||
if (Objects.equals(left, right)) {
|
||||
return true;
|
||||
}
|
||||
if (left == null || right == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
JsonNode leftNode = objectMapper.readTree(left);
|
||||
JsonNode rightNode = objectMapper.readTree(right);
|
||||
return leftNode.equals(rightNode);
|
||||
} catch (JsonProcessingException ex) {
|
||||
log.warn("[API-VERSION] Snapshot comparison failed, treat as different", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode readTree(String content) {
|
||||
try {
|
||||
return objectMapper.readTree(content);
|
||||
} catch (JsonProcessingException ex) {
|
||||
log.error("[API-VERSION] Failed to parse snapshot content", ex);
|
||||
throw ServiceExceptionUtil.exception(API_VERSION_SNAPSHOT_DESERIALIZE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectDifferences(JsonNode source, JsonNode target, String path, List<ApiVersionCompareRespVO.FieldDiff> differences) {
|
||||
if (Objects.equals(source, target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (source == null || source.isNull()) {
|
||||
addDifference(path, null, target, differences);
|
||||
return;
|
||||
}
|
||||
if (target == null || target.isNull()) {
|
||||
addDifference(path, source, null, differences);
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.isValueNode() && target.isValueNode()) {
|
||||
if (!Objects.equals(source, target)) {
|
||||
addDifference(path, source, target, differences);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.isObject() && target.isObject()) {
|
||||
Set<String> fieldNames = new LinkedHashSet<>();
|
||||
source.fieldNames().forEachRemaining(fieldNames::add);
|
||||
target.fieldNames().forEachRemaining(fieldNames::add);
|
||||
for (String name : fieldNames) {
|
||||
String childPath = path + "/" + name;
|
||||
collectDifferences(source.get(name), target.get(name), childPath, differences);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.isArray() && target.isArray()) {
|
||||
int max = Math.max(source.size(), target.size());
|
||||
for (int i = 0; i < max; i++) {
|
||||
String childPath = path + "[" + i + "]";
|
||||
JsonNode leftNode = i < source.size() ? source.get(i) : null;
|
||||
JsonNode rightNode = i < target.size() ? target.get(i) : null;
|
||||
collectDifferences(leftNode, rightNode, childPath, differences);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
addDifference(path, source, target, differences);
|
||||
}
|
||||
|
||||
private void addDifference(String path, JsonNode source, JsonNode target, List<ApiVersionCompareRespVO.FieldDiff> differences) {
|
||||
ApiVersionCompareRespVO.FieldDiff diff = new ApiVersionCompareRespVO.FieldDiff();
|
||||
diff.setPath(StringUtils.hasText(path) ? path : "/");
|
||||
diff.setSourceValue(source == null || source.isNull() ? "null" : source.toString());
|
||||
diff.setTargetValue(target == null || target.isNull() ? "null" : target.toString());
|
||||
differences.add(diff);
|
||||
}
|
||||
|
||||
private String defaultDescription(int versionNumber) {
|
||||
return String.format("自动版本 v%d", versionNumber);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -55,5 +55,11 @@ public interface GatewayServiceErrorCodeConstants {
|
||||
ErrorCode API_STEP_MAPPING_CONFIG_INVALID = new ErrorCode(1_010_000_046, "步骤映射配置 JSON 非法");
|
||||
ErrorCode API_CREDENTIAL_ANONYMOUS_USER_REQUIRED = new ErrorCode(1_010_000_047, "启用匿名访问时必须指定固定用户");
|
||||
ErrorCode API_CREDENTIAL_ANONYMOUS_USER_INVALID = new ErrorCode(1_010_000_048, "匿名访问固定用户不存在或已被禁用");
|
||||
ErrorCode API_VERSION_NOT_FOUND = new ErrorCode(1_010_000_049, "API 版本不存在");
|
||||
ErrorCode API_VERSION_DUPLICATE = new ErrorCode(1_010_000_050, "API 版本号已存在");
|
||||
ErrorCode API_VERSION_SNAPSHOT_SERIALIZE_FAILED = new ErrorCode(1_010_000_051, "API 版本快照序列化失败");
|
||||
ErrorCode API_VERSION_SNAPSHOT_DESERIALIZE_FAILED = new ErrorCode(1_010_000_052, "API 版本快照反序列化失败");
|
||||
ErrorCode API_VERSION_ACTIVE_CANNOT_DELETE = new ErrorCode(1_010_000_053, "当前激活版本不允许删除");
|
||||
ErrorCode API_VERSION_API_MISMATCH = new ErrorCode(1_010_000_054, "两个版本不属于同一 API");
|
||||
|
||||
}
|
||||
|
||||
@@ -28,14 +28,15 @@ import java.util.UUID;
|
||||
public final class DatabusApiInvocationExample {
|
||||
|
||||
public static final String TIMESTAMP = Long.toString(System.currentTimeMillis());
|
||||
private static final String APP_ID = "ztmy";
|
||||
private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ=";
|
||||
// private static final String APP_ID = "ztmy";
|
||||
// private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ=";
|
||||
// private static final String APP_ID = "test";
|
||||
// private static final String APP_SECRET = "RSYtKXrXPLMy3oeh0cOro6QCioRUgqfnKCkDkNq78sI=";
|
||||
// private static final String APP_ID = "testAnnoy";
|
||||
// private static final String APP_SECRET = "jyGCymUjCFL2i3a4Tm3qBIkUrUl4ZgKPYvOU/47ZWcM=";
|
||||
private static final String APP_ID = "testAnnoy";
|
||||
private static final String APP_SECRET = "jyGCymUjCFL2i3a4Tm3qBIkUrUl4ZgKPYvOU/47ZWcM=";
|
||||
private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES;
|
||||
private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/lgstOpenApi/v1";
|
||||
// private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/lgstOpenApi/v1";
|
||||
private static final String TARGET_API = "http://127.0.0.1:48080/admin-api/databus/api/portal/test/1";
|
||||
// private static final String TARGET_API = "http://127.0.0.1:48080/admin-api/databus/api/portal/lgstOpenApi/v1";
|
||||
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(5))
|
||||
|
||||
Reference in New Issue
Block a user