Merge remote-tracking branch 'base-version/main' into dev
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
/** 公司编号 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(), "网关安全校验失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user