Merge remote-tracking branch 'base-version/main' into dev

This commit is contained in:
chenbowen
2025-12-01 17:52:13 +08:00
15 changed files with 606 additions and 66 deletions

View File

@@ -36,6 +36,11 @@
<artifactId>zt-spring-boot-starter-biz-tenant</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-mybatis</artifactId>
<version>${revision}</version>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>com.zt.plat</groupId>

View File

@@ -2,27 +2,54 @@ package com.zt.plat.framework.business.framework;
import com.zt.plat.framework.datapermission.core.rule.company.CompanyDataPermissionRuleCustomizer;
import com.zt.plat.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* @author chenbowen
* 自动为继承 BusinessBaseDO 的实体注册公司/部门数据权限字段。
*/
@Configuration(proxyBeanMethods = false)
public class BusinessDataPermissionConfiguration {
@Bean
public CompanyDataPermissionRuleCustomizer sysCompanyDataPermissionRuleCustomizer() {
return rule -> {
// companyId
rule.addCompanyColumn("demo_contract", "company_id");
};
public BusinessDataPermissionEntityScanner businessDataPermissionEntityScanner(BeanFactory beanFactory, ApplicationContext applicationContext) {
Set<String> basePackages = new LinkedHashSet<>();
if (AutoConfigurationPackages.has(beanFactory)) {
basePackages.addAll(AutoConfigurationPackages.get(beanFactory));
}
if (basePackages.isEmpty()) {
basePackages.add("com.zt");
}
ClassLoader classLoader = applicationContext != null
? applicationContext.getClassLoader()
: Thread.currentThread().getContextClassLoader();
if (classLoader == null) {
classLoader = BusinessDataPermissionConfiguration.class.getClassLoader();
}
return new BusinessDataPermissionEntityScanner(basePackages, classLoader);
}
@Bean
public DeptDataPermissionRuleCustomizer businessDeptDataPermissionRuleCustomizer() {
return rule -> {
// dept
rule.addDeptColumn("demo_contract", "dept_id");
};
public CompanyDataPermissionRuleCustomizer autoCompanyDataPermissionRuleCustomizer(BusinessDataPermissionEntityScanner scanner) {
return rule -> scanner.getEntityMetadata().forEach(metadata -> {
if (metadata.hasCompanyColumn()) {
rule.addCompanyColumn(metadata.getTableName(), metadata.getCompanyColumn());
}
});
}
@Bean
public DeptDataPermissionRuleCustomizer autoDeptDataPermissionRuleCustomizer(BusinessDataPermissionEntityScanner scanner) {
return rule -> scanner.getEntityMetadata().forEach(metadata -> {
if (metadata.hasDeptColumn()) {
rule.addDeptColumn(metadata.getTableName(), metadata.getDeptColumn());
}
});
}
}

View File

@@ -0,0 +1,159 @@
package com.zt.plat.framework.business.framework;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.zt.plat.framework.mybatis.core.annotation.CompanyColumn;
import com.zt.plat.framework.mybatis.core.annotation.DeptColumn;
import com.zt.plat.framework.mybatis.core.dataobject.BusinessBaseDO;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* 扫描继承 {@link BusinessBaseDO} 的实体,自动提取公司/部门字段用于数据权限注册。
*
* @author chenbow
*/
@Slf4j
public class BusinessDataPermissionEntityScanner {
private final Set<String> basePackages;
private final ClassLoader classLoader;
private volatile List<EntityMetadata> cachedEntities;
public BusinessDataPermissionEntityScanner(Collection<String> basePackages, ClassLoader classLoader) {
Set<String> packages = new LinkedHashSet<>();
if (!CollectionUtils.isEmpty(basePackages)) {
packages.addAll(basePackages);
}
if (packages.isEmpty()) {
packages.add("com.zt");
}
this.basePackages = Collections.unmodifiableSet(packages);
this.classLoader = classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader();
}
public List<EntityMetadata> getEntityMetadata() {
List<EntityMetadata> result = cachedEntities;
if (result == null) {
synchronized (this) {
result = cachedEntities;
if (result == null) {
result = Collections.unmodifiableList(scanEntities());
cachedEntities = result;
}
}
}
return result;
}
private List<EntityMetadata> scanEntities() {
Map<String, EntityMetadata> metadataMap = new LinkedHashMap<>();
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AssignableTypeFilter(BusinessBaseDO.class));
for (String basePackage : basePackages) {
for (BeanDefinition beanDefinition : scanner.findCandidateComponents(basePackage)) {
String className = beanDefinition.getBeanClassName();
if (!StringUtils.hasText(className)) {
continue;
}
try {
Class<?> clazz = ClassUtils.forName(className, classLoader);
if (clazz == BusinessBaseDO.class || !BusinessBaseDO.class.isAssignableFrom(clazz)) {
continue;
}
if (Modifier.isAbstract(clazz.getModifiers())) {
continue;
}
@SuppressWarnings("unchecked")
Class<? extends BusinessBaseDO> entityClass = (Class<? extends BusinessBaseDO>) clazz;
EntityMetadata metadata = buildMetadata(entityClass);
if (metadata != null && StringUtils.hasText(metadata.getTableName())) {
metadataMap.putIfAbsent(metadata.getTableName(), metadata);
}
} catch (ClassNotFoundException ex) {
log.warn("[scanEntities][无法加载类 {}]", className, ex);
}
}
}
return new ArrayList<>(metadataMap.values());
}
private EntityMetadata buildMetadata(Class<? extends BusinessBaseDO> entityClass) {
String tableName = resolveTableName(entityClass);
if (!StringUtils.hasText(tableName)) {
log.debug("[buildMetadata][实体 {} 缺少表名配置,跳过自动注册]", entityClass.getName());
return null;
}
String companyColumn = resolveCompanyColumn(entityClass);
String deptColumn = resolveDeptColumn(entityClass);
if (!StringUtils.hasText(companyColumn) && !StringUtils.hasText(deptColumn)) {
log.debug("[buildMetadata][实体 {} 未配置公司/部门字段,跳过]", entityClass.getName());
return null;
}
return new EntityMetadata(tableName, companyColumn, deptColumn);
}
private String resolveTableName(Class<?> entityClass) {
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
if (tableInfo != null && StringUtils.hasText(tableInfo.getTableName())) {
return tableInfo.getTableName();
}
TableName tableName = AnnotatedElementUtils.findMergedAnnotation(entityClass, TableName.class);
if (StringUtils.hasText(tableName.value())) {
return tableName.value();
}
// 退化为根据类名猜测(驼峰转下划线)
String fallback = com.baomidou.mybatisplus.core.toolkit.StringUtils.camelToUnderline(entityClass.getSimpleName());
return StringUtils.hasText(fallback) ? fallback : null;
}
private String resolveCompanyColumn(Class<?> entityClass) {
CompanyColumn annotation = AnnotatedElementUtils.findMergedAnnotation(entityClass, CompanyColumn.class);
return annotation.value();
}
private String resolveDeptColumn(Class<?> entityClass) {
DeptColumn annotation = AnnotatedElementUtils.findMergedAnnotation(entityClass, DeptColumn.class);
return annotation.value();
}
@Getter
@RequiredArgsConstructor
public static class EntityMetadata {
private final String tableName;
private final String companyColumn;
private final String deptColumn;
public boolean hasCompanyColumn() {
return StringUtils.hasText(companyColumn);
}
public boolean hasDeptColumn() {
return StringUtils.hasText(deptColumn);
}
@Override
public String toString() {
return "EntityMetadata{" +
"table='" + tableName + '\'' +
", company='" + companyColumn + '\'' +
", dept='" + deptColumn + '\'' +
'}';
}
}
}

View File

@@ -0,0 +1,23 @@
package com.zt.plat.framework.mybatis.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记业务实体对应表中的公司字段名称,默认 company_id。
*
* @author chenbow
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CompanyColumn {
/**
* 表中公司字段名称
*/
String value() default "company_id";
}

View File

@@ -0,0 +1,23 @@
package com.zt.plat.framework.mybatis.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记业务实体对应表中的部门字段名称,默认 dept_id。
*
* @author chenbow
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DeptColumn {
/**
* 表中部门字段名称
*/
String value() default "dept_id";
}

View File

@@ -2,6 +2,8 @@ package com.zt.plat.framework.mybatis.core.dataobject;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.zt.plat.framework.mybatis.core.annotation.CompanyColumn;
import com.zt.plat.framework.mybatis.core.annotation.DeptColumn;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.ibatis.type.JdbcType;
@@ -13,6 +15,8 @@ import java.util.List;
*/
@Data
@EqualsAndHashCode(callSuper = true)
@CompanyColumn
@DeptColumn
public class BusinessBaseDO extends BaseDO {
/** 公司编号 */

View File

@@ -3,9 +3,12 @@ package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.framework.common.util.monitor.TracerUtils;
import com.zt.plat.framework.common.util.servlet.ServletUtils;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiAccessLogDO;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.security.CachedBodyHttpServletRequest;
import com.zt.plat.module.databus.service.gateway.ApiAccessLogService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
@@ -20,8 +23,7 @@ import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
/**
* 将 API 调用上下文持久化为访问日志。
@@ -33,6 +35,8 @@ public class ApiGatewayAccessLogger {
public static final String ATTR_LOG_ID = "ApiAccessLogId";
public static final String ATTR_EXCEPTION_STACK = "ApiAccessLogExceptionStack";
public static final String HEADER_ACCESS_LOG_ID = "X-Databus-AccessLog-Id";
private static final String ATTR_REQUEST_START = "ApiAccessLogRequestStart";
private static final int MAX_TEXT_LENGTH = 4000;
@@ -44,24 +48,15 @@ public class ApiGatewayAccessLogger {
*/
public void onRequest(ApiInvocationContext context) {
try {
String traceId = TracerUtils.getTraceId();
ApiAccessLogDO logDO = new ApiAccessLogDO();
logDO.setTraceId(traceId);
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);
ApiAccessLogDO logDO = buildRequestSnapshot(context);
Long existingLogId = getLogId(context);
if (existingLogId != null) {
logDO.setId(existingLogId);
apiAccessLogService.update(logDO);
} else {
Long logId = apiAccessLogService.create(logDO);
context.getAttributes().put(ATTR_LOG_ID, logId);
}
} catch (Exception ex) {
log.warn("记录 API 访问日志开始阶段失败, traceId={}", TracerUtils.getTraceId(), ex);
}
@@ -111,15 +106,97 @@ public class ApiGatewayAccessLogger {
}
}
/**
* 安全过滤阶段的第一时间记录请求元数据,保证被快速拒绝的请求也能查询。
*/
public Long logEntrance(HttpServletRequest request) {
if (request == null) {
return null;
}
Object existing = request.getAttribute(ATTR_LOG_ID);
if (existing instanceof Long logId) {
return logId;
}
try {
ApiAccessLogDO logDO = new ApiAccessLogDO();
logDO.setTraceId(TracerUtils.getTraceId());
logDO.setRequestMethod(request.getMethod());
logDO.setRequestPath(request.getRequestURI());
logDO.setRequestQuery(truncate(request.getQueryString()));
logDO.setRequestHeaders(toJson(collectHeaders(request)));
logDO.setClientIp(ServletUtils.getClientIP(request));
logDO.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT));
logDO.setStatus(3);
logDO.setRequestTime(LocalDateTime.now());
Long logId = apiAccessLogService.create(logDO);
request.setAttribute(ATTR_LOG_ID, logId);
request.setAttribute(ATTR_REQUEST_START, Instant.now());
return logId;
} catch (Exception ex) {
log.warn("记录入口 API 访问日志失败", ex);
return null;
}
}
/**
* 编排前即结束的请求在此补写状态码、耗时等关键信息。
*/
public void finalizeEarly(HttpServletRequest request, int status, String message) {
if (request == null) {
return;
}
Object existing = request.getAttribute(ATTR_LOG_ID);
if (!(existing instanceof Long logId)) {
return;
}
try {
ApiAccessLogDO update = new ApiAccessLogDO();
update.setId(logId);
update.setResponseStatus(status);
update.setResponseMessage(truncate(message));
update.setStatus(resolveStatus(status));
update.setResponseTime(LocalDateTime.now());
update.setDuration(calculateDuration(request));
apiAccessLogService.update(update);
} catch (Exception ex) {
log.warn("更新入口 API 访问日志失败, logId={}", logId, ex);
}
}
/**
* 将入口阶段生成的 logId 通过请求头继续传递,供后续流程关联合并。
*/
public static void propagateLogIdHeader(CachedBodyHttpServletRequest requestWrapper, Long logId) {
if (requestWrapper == null || logId == null) {
return;
}
requestWrapper.setHeader(HEADER_ACCESS_LOG_ID, String.valueOf(logId));
}
private Long getLogId(ApiInvocationContext context) {
Object value = context.getAttributes().get(ATTR_LOG_ID);
if (value instanceof Long) {
return (Long) value;
}
if (value instanceof Number number) {
return number.longValue();
}
return null;
return value instanceof Long ? (Long) value : null;
}
/**
* 根据编排上下文构建请求侧快照,用于访问日志首段信息。
*/
private ApiAccessLogDO buildRequestSnapshot(ApiInvocationContext context) {
ApiAccessLogDO logDO = new ApiAccessLogDO();
logDO.setTraceId(TracerUtils.getTraceId());
logDO.setApiCode(context.getApiCode());
logDO.setApiVersion(context.getApiVersion());
logDO.setRequestMethod(context.getHttpMethod());
logDO.setRequestPath(context.getRequestPath());
logDO.setRequestQuery(toJson(context.getRequestQueryParams()));
logDO.setRequestHeaders(toJson(context.getRequestHeaders()));
logDO.setRequestBody(toJson(context.getRequestBody()));
logDO.setClientIp(context.getClientIp());
logDO.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT));
logDO.setStatus(3);
logDO.setRequestTime(toLocalDateTime(context.getRequestTime()));
logDO.setTenantId(parseTenantId(context.getTenantId()));
return logDO;
}
private Long calculateDuration(ApiInvocationContext context) {
@@ -130,6 +207,14 @@ public class ApiGatewayAccessLogger {
return Duration.between(start, Instant.now()).toMillis();
}
private Long calculateDuration(HttpServletRequest request) {
Object startAttr = request.getAttribute(ATTR_REQUEST_START);
if (startAttr instanceof Instant start) {
return Duration.between(start, Instant.now()).toMillis();
}
return null;
}
private Integer resolveStatus(Integer httpStatus) {
if (httpStatus == null) {
return 3;
@@ -214,6 +299,19 @@ public class ApiGatewayAccessLogger {
return extra;
}
@SafeVarargs
private <T> T firstNonNull(T... candidates) {
if (candidates == null) {
return null;
}
for (T candidate : candidates) {
if (candidate != null) {
return candidate;
}
}
return null;
}
private String toJson(Object value) {
if (value == null) {
return null;
@@ -265,27 +363,20 @@ public class ApiGatewayAccessLogger {
}
}
private String firstNonBlank(String... values) {
if (values == null) {
return null;
private Map<String, String> collectHeaders(HttpServletRequest request) {
if (request == null) {
return Collections.emptyMap();
}
for (String value : values) {
if (StringUtils.hasText(value)) {
return value;
Map<String, String> headers = new LinkedHashMap<>();
Enumeration<String> names = request.getHeaderNames();
while (names != null && names.hasMoreElements()) {
String name = names.nextElement();
Enumeration<String> values = request.getHeaders(name);
if (values == null || !values.hasMoreElements()) {
continue;
}
headers.put(name, values.nextElement());
}
return null;
return headers;
}
private Object firstNonNull(Object... values) {
if (values == null) {
return null;
}
for (Object value : values) {
if (value != null) {
return value;
}
}
return null;
}
}
}

View File

@@ -3,6 +3,7 @@ package com.zt.plat.module.databus.framework.integration.gateway.core;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayAccessLogger;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
@@ -90,6 +91,7 @@ public class ApiGatewayRequestMapper {
});
context.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT));
context.setClientIp(resolveClientIp(headers, context.getRequestHeaders()));
captureAccessLogId(context);
populateQueryParams(headers, context, originalRequestUri);
if (properties.isEnableTenantHeader()) {
Object tenantHeaderValue = context.getRequestHeaders().get(properties.getTenantHeader());
@@ -114,6 +116,21 @@ public class ApiGatewayRequestMapper {
return context;
}
private void captureAccessLogId(ApiInvocationContext context) {
String headerValue = GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), ApiGatewayAccessLogger.HEADER_ACCESS_LOG_ID);
if (!StringUtils.hasText(headerValue)) {
return;
}
try {
Long logId = Long.valueOf(headerValue);
context.getAttributes().put(ApiGatewayAccessLogger.ATTR_LOG_ID, logId);
} catch (NumberFormatException ex) {
// 忽略格式问题,仅在属性中保留原文以便排查
context.getAttributes().put(ApiGatewayAccessLogger.ATTR_LOG_ID, headerValue);
}
context.getRequestHeaders().remove(ApiGatewayAccessLogger.HEADER_ACCESS_LOG_ID);
}
private boolean isInternalHeader(String headerName) {
if (!StringUtils.hasText(headerName)) {
return true;

View File

@@ -13,6 +13,7 @@ import com.zt.plat.framework.tenant.core.context.TenantContextHolder;
import com.zt.plat.framework.web.core.util.WebFrameworkUtils;
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO;
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayAccessLogger;
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse;
import com.zt.plat.module.databus.service.gateway.ApiAnonymousUserService;
import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService;
@@ -56,6 +57,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
private final ApiClientCredentialService credentialService;
private final ApiAnonymousUserService anonymousUserService;
private final ObjectMapper objectMapper;
private final ApiGatewayAccessLogger accessLogger;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};
@@ -72,18 +74,24 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
return;
}
Long accessLogId = accessLogger.logEntrance(request);
// 校验访问 IP 是否落在允许范围内
if (!isIpAllowed(request)) {
log.warn("[API-PORTAL] 拦截来自 IP {} 访问 {} 的请求", request.getRemoteAddr(), pathWithinApplication);
response.sendError(HttpStatus.FORBIDDEN.value(), "IP 禁止访问");
accessLogger.finalizeEarly(request, HttpStatus.FORBIDDEN.value(), "IP 禁止访问");
return;
}
ApiGatewayProperties.Security security = properties.getSecurity();
ApiClientCredentialDO credential = null;
if (!security.isEnabled()) {
filterChain.doFilter(request, response);
byte[] originalBody = StreamUtils.copyToByteArray(request.getInputStream());
CachedBodyHttpServletRequest passthroughRequest = new CachedBodyHttpServletRequest(request, originalBody);
ApiGatewayAccessLogger.propagateLogIdHeader(passthroughRequest, accessLogId);
filterChain.doFilter(passthroughRequest, response);
return;
}
boolean dispatchedToGateway = false;
try {
Long tenantId = resolveTenantId(request);
// 从请求头解析 appId 并加载客户端凭证,包含匿名访问配置
@@ -118,6 +126,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
// 使用可重复读取的请求包装,供后续过滤器继续消费
CachedBodyHttpServletRequest securedRequest = new CachedBodyHttpServletRequest(request, decryptedBody);
ApiGatewayAccessLogger.propagateLogIdHeader(securedRequest, accessLogId);
if (StringUtils.hasText(request.getCharacterEncoding())) {
securedRequest.setCharacterEncoding(request.getCharacterEncoding());
}
@@ -129,6 +138,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
try {
filterChain.doFilter(securedRequest, responseWrapper);
dispatchedToGateway = true;
encryptResponse(responseWrapper, credential, security);
} finally {
responseWrapper.copyBodyToResponse();
@@ -136,9 +146,15 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
} catch (SecurityValidationException ex) {
log.warn("[API-PORTAL] 安全校验失败: {}", ex.getMessage());
writeErrorResponse(response, security, credential, ex.status(), ex.getMessage());
if (!dispatchedToGateway) {
accessLogger.finalizeEarly(request, ex.status().value(), ex.getMessage());
}
} catch (Exception ex) {
log.error("[API-PORTAL] 处理安全校验时出现异常", ex);
writeErrorResponse(response, security, credential, HttpStatus.INTERNAL_SERVER_ERROR, "网关安全校验失败");
if (!dispatchedToGateway) {
accessLogger.finalizeEarly(request, HttpStatus.INTERNAL_SERVER_ERROR.value(), "网关安全校验失败");
}
}
}

View File

@@ -26,11 +26,76 @@ import java.time.LocalDateTime;
@KeySequence(value = "infra_api_error_log_seq")
public class ApiErrorLogDO extends BaseDO {
/**
* {@link #traceId} 的最大长度
*/
public static final Integer TRACE_ID_MAX_LENGTH = 64;
/**
* {@link #applicationName} 的最大长度
*/
public static final Integer APPLICATION_NAME_MAX_LENGTH = 50;
/**
* {@link #requestMethod} 的最大长度
*/
public static final Integer REQUEST_METHOD_MAX_LENGTH = 16;
/**
* {@link #requestUrl} 的最大长度
*/
public static final Integer REQUEST_URL_MAX_LENGTH = 255;
/**
* {@link #requestParams} 的最大长度
*/
public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000;
/**
* {@link #userIp} 的最大长度
*/
public static final Integer USER_IP_MAX_LENGTH = 50;
/**
* {@link #userAgent} 的最大长度
*/
public static final Integer USER_AGENT_MAX_LENGTH = 512;
/**
* {@link #exceptionName} 的最大长度
*/
public static final Integer EXCEPTION_NAME_MAX_LENGTH = 128;
/**
* {@link #exceptionClassName} 的最大长度
*/
public static final Integer EXCEPTION_CLASS_NAME_MAX_LENGTH = 512;
/**
* {@link #exceptionFileName} 的最大长度
*/
public static final Integer EXCEPTION_FILE_NAME_MAX_LENGTH = 512;
/**
* {@link #exceptionMethodName} 的最大长度
*/
public static final Integer EXCEPTION_METHOD_NAME_MAX_LENGTH = 512;
/**
* {@link #exceptionMessage} 的最大长度
*/
public static final Integer EXCEPTION_MESSAGE_MAX_LENGTH = 4000;
/**
* {@link #exceptionRootCauseMessage} 的最大长度
*/
public static final Integer EXCEPTION_ROOT_CAUSE_MESSAGE_MAX_LENGTH = 4000;
/**
* {@link #exceptionStackTrace} 的最大长度
*/
public static final Integer EXCEPTION_STACK_TRACE_MAX_LENGTH = 8000;
/**
* 编号
*/

View File

@@ -18,7 +18,6 @@ import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.zt.plat.module.infra.dal.dataobject.logger.ApiErrorLogDO.REQUEST_PARAMS_MAX_LENGTH;
import static com.zt.plat.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_NOT_FOUND;
import static com.zt.plat.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_PROCESSED;
@@ -39,7 +38,7 @@ public class ApiErrorLogServiceImpl implements ApiErrorLogService {
public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) {
ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class)
.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus());
apiErrorLog.setRequestParams(StrUtils.maxLength(apiErrorLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH));
truncateOverflowFields(apiErrorLog);
if (TenantContextHolder.getTenantId() != null) {
apiErrorLogMapper.insert(apiErrorLog);
} else {
@@ -48,6 +47,35 @@ public class ApiErrorLogServiceImpl implements ApiErrorLogService {
}
}
private void truncateOverflowFields(ApiErrorLogDO apiErrorLog) {
apiErrorLog.setTraceId(StrUtils.maxLength(apiErrorLog.getTraceId(), ApiErrorLogDO.TRACE_ID_MAX_LENGTH));
apiErrorLog.setApplicationName(StrUtils.maxLength(apiErrorLog.getApplicationName(),
ApiErrorLogDO.APPLICATION_NAME_MAX_LENGTH));
apiErrorLog.setRequestMethod(StrUtils.maxLength(apiErrorLog.getRequestMethod(),
ApiErrorLogDO.REQUEST_METHOD_MAX_LENGTH));
apiErrorLog.setRequestUrl(StrUtils.maxLength(apiErrorLog.getRequestUrl(),
ApiErrorLogDO.REQUEST_URL_MAX_LENGTH));
apiErrorLog.setRequestParams(StrUtils.maxLength(apiErrorLog.getRequestParams(),
ApiErrorLogDO.REQUEST_PARAMS_MAX_LENGTH));
apiErrorLog.setUserIp(StrUtils.maxLength(apiErrorLog.getUserIp(), ApiErrorLogDO.USER_IP_MAX_LENGTH));
apiErrorLog.setUserAgent(StrUtils.maxLength(apiErrorLog.getUserAgent(),
ApiErrorLogDO.USER_AGENT_MAX_LENGTH));
apiErrorLog.setExceptionName(StrUtils.maxLength(apiErrorLog.getExceptionName(),
ApiErrorLogDO.EXCEPTION_NAME_MAX_LENGTH));
apiErrorLog.setExceptionClassName(StrUtils.maxLength(apiErrorLog.getExceptionClassName(),
ApiErrorLogDO.EXCEPTION_CLASS_NAME_MAX_LENGTH));
apiErrorLog.setExceptionFileName(StrUtils.maxLength(apiErrorLog.getExceptionFileName(),
ApiErrorLogDO.EXCEPTION_FILE_NAME_MAX_LENGTH));
apiErrorLog.setExceptionMethodName(StrUtils.maxLength(apiErrorLog.getExceptionMethodName(),
ApiErrorLogDO.EXCEPTION_METHOD_NAME_MAX_LENGTH));
apiErrorLog.setExceptionMessage(StrUtils.maxLength(apiErrorLog.getExceptionMessage(),
ApiErrorLogDO.EXCEPTION_MESSAGE_MAX_LENGTH));
apiErrorLog.setExceptionRootCauseMessage(StrUtils.maxLength(apiErrorLog.getExceptionRootCauseMessage(),
ApiErrorLogDO.EXCEPTION_ROOT_CAUSE_MESSAGE_MAX_LENGTH));
apiErrorLog.setExceptionStackTrace(StrUtils.maxLength(apiErrorLog.getExceptionStackTrace(),
ApiErrorLogDO.EXCEPTION_STACK_TRACE_MAX_LENGTH));
}
@Override
public PageResult<ApiErrorLogDO> getApiErrorLogPage(ApiErrorLogPageReqVO pageReqVO) {
return apiErrorLogMapper.selectPage(pageReqVO);

View File

@@ -3,6 +3,7 @@ package com.zt.plat.module.infra.service.logger;
import com.zt.plat.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO;
import com.zt.plat.framework.common.enums.UserTypeEnum;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.util.string.StrUtils;
import com.zt.plat.framework.test.core.ut.BaseDbUnitTest;
import com.zt.plat.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO;
import com.zt.plat.module.infra.dal.dataobject.logger.ApiErrorLogDO;
@@ -13,6 +14,7 @@ import org.springframework.context.annotation.Import;
import jakarta.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import static cn.hutool.core.util.RandomUtil.randomEle;
@@ -79,15 +81,63 @@ public class ApiErrorLogServiceImplTest extends BaseDbUnitTest {
@Test
public void testCreateApiErrorLog() {
// 准备参数
ApiErrorLogCreateReqDTO createDTO = randomPojo(ApiErrorLogCreateReqDTO.class);
// 准备参数:手动设置多个超长字段,确保持久化前会被截断
ApiErrorLogCreateReqDTO createDTO = new ApiErrorLogCreateReqDTO();
createDTO.setTraceId(repeat('t', ApiErrorLogDO.TRACE_ID_MAX_LENGTH + 20));
createDTO.setUserId(10086L);
createDTO.setUserType(UserTypeEnum.ADMIN.getValue());
createDTO.setApplicationName(repeat('a', ApiErrorLogDO.APPLICATION_NAME_MAX_LENGTH + 5));
createDTO.setRequestMethod(repeat('b', ApiErrorLogDO.REQUEST_METHOD_MAX_LENGTH + 10));
createDTO.setRequestUrl(repeat('c', ApiErrorLogDO.REQUEST_URL_MAX_LENGTH + 20));
createDTO.setRequestParams(repeat('d', ApiErrorLogDO.REQUEST_PARAMS_MAX_LENGTH + 50));
createDTO.setUserIp(repeat('1', ApiErrorLogDO.USER_IP_MAX_LENGTH + 10));
createDTO.setUserAgent(repeat('e', ApiErrorLogDO.USER_AGENT_MAX_LENGTH + 100));
createDTO.setExceptionTime(LocalDateTime.of(2025, 1, 1, 10, 20, 30));
createDTO.setExceptionName(repeat('f', ApiErrorLogDO.EXCEPTION_NAME_MAX_LENGTH + 10));
createDTO.setExceptionClassName(repeat('g', ApiErrorLogDO.EXCEPTION_CLASS_NAME_MAX_LENGTH + 10));
createDTO.setExceptionFileName(repeat('h', ApiErrorLogDO.EXCEPTION_FILE_NAME_MAX_LENGTH + 10));
createDTO.setExceptionMethodName(repeat('i', ApiErrorLogDO.EXCEPTION_METHOD_NAME_MAX_LENGTH + 10));
createDTO.setExceptionLineNumber(256);
createDTO.setExceptionStackTrace(repeat('s', ApiErrorLogDO.EXCEPTION_STACK_TRACE_MAX_LENGTH + 100));
createDTO.setExceptionRootCauseMessage(repeat('r', ApiErrorLogDO.EXCEPTION_ROOT_CAUSE_MESSAGE_MAX_LENGTH + 80));
createDTO.setExceptionMessage(repeat('m', ApiErrorLogDO.EXCEPTION_MESSAGE_MAX_LENGTH + 60));
// 调用
apiErrorLogService.createApiErrorLog(createDTO);
// 断言
ApiErrorLogDO apiErrorLogDO = apiErrorLogMapper.selectOne(null);
assertPojoEquals(createDTO, apiErrorLogDO);
assertEquals(createDTO.getUserId(), apiErrorLogDO.getUserId());
assertEquals(createDTO.getUserType(), apiErrorLogDO.getUserType());
assertEquals(createDTO.getExceptionTime(), apiErrorLogDO.getExceptionTime());
assertEquals(createDTO.getExceptionLineNumber(), apiErrorLogDO.getExceptionLineNumber());
assertEquals(ApiErrorLogProcessStatusEnum.INIT.getStatus(), apiErrorLogDO.getProcessStatus());
assertTruncated(createDTO.getTraceId(), apiErrorLogDO.getTraceId(), ApiErrorLogDO.TRACE_ID_MAX_LENGTH);
assertTruncated(createDTO.getApplicationName(), apiErrorLogDO.getApplicationName(),
ApiErrorLogDO.APPLICATION_NAME_MAX_LENGTH);
assertTruncated(createDTO.getRequestMethod(), apiErrorLogDO.getRequestMethod(),
ApiErrorLogDO.REQUEST_METHOD_MAX_LENGTH);
assertTruncated(createDTO.getRequestUrl(), apiErrorLogDO.getRequestUrl(),
ApiErrorLogDO.REQUEST_URL_MAX_LENGTH);
assertTruncated(createDTO.getRequestParams(), apiErrorLogDO.getRequestParams(),
ApiErrorLogDO.REQUEST_PARAMS_MAX_LENGTH);
assertTruncated(createDTO.getUserIp(), apiErrorLogDO.getUserIp(), ApiErrorLogDO.USER_IP_MAX_LENGTH);
assertTruncated(createDTO.getUserAgent(), apiErrorLogDO.getUserAgent(),
ApiErrorLogDO.USER_AGENT_MAX_LENGTH);
assertTruncated(createDTO.getExceptionName(), apiErrorLogDO.getExceptionName(),
ApiErrorLogDO.EXCEPTION_NAME_MAX_LENGTH);
assertTruncated(createDTO.getExceptionClassName(), apiErrorLogDO.getExceptionClassName(),
ApiErrorLogDO.EXCEPTION_CLASS_NAME_MAX_LENGTH);
assertTruncated(createDTO.getExceptionFileName(), apiErrorLogDO.getExceptionFileName(),
ApiErrorLogDO.EXCEPTION_FILE_NAME_MAX_LENGTH);
assertTruncated(createDTO.getExceptionMethodName(), apiErrorLogDO.getExceptionMethodName(),
ApiErrorLogDO.EXCEPTION_METHOD_NAME_MAX_LENGTH);
assertTruncated(createDTO.getExceptionStackTrace(), apiErrorLogDO.getExceptionStackTrace(),
ApiErrorLogDO.EXCEPTION_STACK_TRACE_MAX_LENGTH);
assertTruncated(createDTO.getExceptionRootCauseMessage(), apiErrorLogDO.getExceptionRootCauseMessage(),
ApiErrorLogDO.EXCEPTION_ROOT_CAUSE_MESSAGE_MAX_LENGTH);
assertTruncated(createDTO.getExceptionMessage(), apiErrorLogDO.getExceptionMessage(),
ApiErrorLogDO.EXCEPTION_MESSAGE_MAX_LENGTH);
}
@Test
@@ -161,4 +211,12 @@ public class ApiErrorLogServiceImplTest extends BaseDbUnitTest {
assertPojoEquals(log02, logs.get(0), "createTime", "updateTime");
}
private static void assertTruncated(String source, String actual, int maxLength) {
assertEquals(StrUtils.maxLength(source, maxLength), actual);
}
private static String repeat(char ch, int length) {
return String.valueOf(ch).repeat(Math.max(length, 0));
}
}

View File

@@ -33,6 +33,9 @@ public class IWorkFullSyncReqVO {
@Schema(description = "同步范围列表默认同步全部。可选subcompany、department、jobTitle、user")
private List<String> scopes;
@Schema(description = "指定同步记录的 iWork ID。传入后仅同步对应记录", example = "12345")
private String id;
@Schema(description = "是否包含已失效canceled=1的记录", example = "false")
private Boolean includeCanceled = Boolean.FALSE;

View File

@@ -14,7 +14,9 @@ import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.zt.plat.module.system.service.integration.iwork.IWorkIntegrationErrorCodeConstants.IWORK_ORG_REMOTE_FAILED;
@@ -86,6 +88,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
IWorkSubcompanyQueryReqVO query = new IWorkSubcompanyQueryReqVO();
query.setCurpage(page);
query.setPagesize(pageSize);
applyQueryConditions(query, reqVO);
IWorkHrSubcompanyPageRespVO pageResp = orgRestService.listSubcompanies(query);
ensureIWorkSuccess("拉取分部", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrSubcompanyPageRespVO.Subcompany> dataList = CollUtil.emptyIfNull(pageResp.getDataList());
@@ -103,6 +106,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
IWorkDepartmentQueryReqVO query = new IWorkDepartmentQueryReqVO();
query.setCurpage(page);
query.setPagesize(pageSize);
applyQueryConditions(query, reqVO);
IWorkHrDepartmentPageRespVO pageResp = orgRestService.listDepartments(query);
ensureIWorkSuccess("拉取部门", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrDepartmentPageRespVO.Department> dataList = CollUtil.emptyIfNull(pageResp.getDataList());
@@ -120,6 +124,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
IWorkJobTitleQueryReqVO query = new IWorkJobTitleQueryReqVO();
query.setCurpage(page);
query.setPagesize(pageSize);
applyQueryConditions(query, reqVO);
IWorkHrJobTitlePageRespVO pageResp = orgRestService.listJobTitles(query);
ensureIWorkSuccess("拉取岗位", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrJobTitlePageRespVO.JobTitle> dataList = CollUtil.emptyIfNull(pageResp.getDataList());
@@ -137,6 +142,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
IWorkUserQueryReqVO query = new IWorkUserQueryReqVO();
query.setCurpage(page);
query.setPagesize(pageSize);
applyQueryConditions(query, reqVO);
IWorkHrUserPageRespVO pageResp = orgRestService.listUsers(query);
ensureIWorkSuccess("拉取人员", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrUserPageRespVO.User> dataList = CollUtil.emptyIfNull(pageResp.getDataList());
@@ -181,6 +187,21 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
stat.incrementFailed(result.getFailed());
}
private void applyQueryConditions(IWorkOrgBaseQueryReqVO query, IWorkFullSyncReqVO reqVO) {
if (query == null || reqVO == null) {
return;
}
if (StrUtil.isBlank(reqVO.getId())) {
return;
}
Map<String, Object> params = query.getParams();
if (params == null) {
params = new HashMap<>();
query.setParams(params);
}
params.put("id", reqVO.getId());
}
private IWorkSyncProcessor.SyncOptions buildFullSyncOptions(IWorkFullSyncReqVO reqVO) {
boolean includeCanceled = Boolean.TRUE.equals(reqVO.getIncludeCanceled());
boolean allowUpdate = Boolean.TRUE.equals(reqVO.getAllowUpdate());

View File

@@ -120,8 +120,8 @@ iwork:
ttl-seconds: 3600
refresh-ahead-seconds: 60
client:
connect-timeout: 5s
response-timeout: 30s
connect-timeout: 60s
response-timeout: 60s
org:
token-seed: 456465
paths: