From 95d905e76fbda6587577ec487e810420530e420f Mon Sep 17 00:00:00 2001 From: chenbowen Date: Mon, 1 Dec 2025 17:46:42 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E9=99=90=E5=88=B6=E8=AE=B0=E5=BD=95=20api?= =?UTF-8?q?=20=E6=97=A5=E5=BF=97=E7=9A=84=E5=AD=97=E6=AE=B5=E9=95=BF?= =?UTF-8?q?=E5=BA=A6=202.=20=E5=AE=8C=E6=95=B4=E8=AE=B0=E5=BD=95=E6=89=80?= =?UTF-8?q?=E6=9C=89=E7=9A=84=20databus=20api=20=E7=9A=84=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E6=97=A5=E5=BF=97=203.=20=E6=96=B0=E5=A2=9E=20iwork?= =?UTF-8?q?=20=E5=90=8C=E6=AD=A5=E5=8F=AF=E4=BB=A5=E6=8C=89=20id=20?= =?UTF-8?q?=E7=BB=B4=E5=BA=A6=E8=BF=9B=E8=A1=8C=204.=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=89=AB=E6=8F=8F=20BusinessBaseDO=20?= =?UTF-8?q?=E7=9A=84=20=E5=85=AC=E5=8F=B8=E9=83=A8=E9=97=A8=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=9D=83=E9=99=90=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pom.xml | 5 + .../BusinessDataPermissionConfiguration.java | 49 +++-- .../BusinessDataPermissionEntityScanner.java | 159 +++++++++++++++ .../core/annotation/CompanyColumn.java | 23 +++ .../mybatis/core/annotation/DeptColumn.java | 23 +++ .../core/dataobject/BusinessBaseDO.java | 4 + .../gateway/core/ApiGatewayAccessLogger.java | 185 +++++++++++++----- .../gateway/core/ApiGatewayRequestMapper.java | 17 ++ .../security/GatewaySecurityFilter.java | 18 +- .../dal/dataobject/logger/ApiErrorLogDO.java | 65 ++++++ .../logger/ApiErrorLogServiceImpl.java | 32 ++- .../logger/ApiErrorLogServiceImplTest.java | 64 +++++- .../iwork/vo/IWorkFullSyncReqVO.java | 3 + .../iwork/impl/IWorkSyncServiceImpl.java | 21 ++ .../src/main/resources/application.yaml | 4 +- 15 files changed, 606 insertions(+), 66 deletions(-) create mode 100644 zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/framework/BusinessDataPermissionEntityScanner.java create mode 100644 zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/annotation/CompanyColumn.java create mode 100644 zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/annotation/DeptColumn.java diff --git a/zt-framework/zt-spring-boot-starter-biz-business/pom.xml b/zt-framework/zt-spring-boot-starter-biz-business/pom.xml index 86e2487d..a410267a 100644 --- a/zt-framework/zt-spring-boot-starter-biz-business/pom.xml +++ b/zt-framework/zt-spring-boot-starter-biz-business/pom.xml @@ -36,6 +36,11 @@ zt-spring-boot-starter-biz-tenant ${revision} + + com.zt.plat + zt-spring-boot-starter-mybatis + ${revision} + com.zt.plat diff --git a/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/framework/BusinessDataPermissionConfiguration.java b/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/framework/BusinessDataPermissionConfiguration.java index 614cf30c..174049ad 100644 --- a/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/framework/BusinessDataPermissionConfiguration.java +++ b/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/framework/BusinessDataPermissionConfiguration.java @@ -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 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()); + } + }); } } diff --git a/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/framework/BusinessDataPermissionEntityScanner.java b/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/framework/BusinessDataPermissionEntityScanner.java new file mode 100644 index 00000000..93c306f5 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-business/src/main/java/com/zt/plat/framework/business/framework/BusinessDataPermissionEntityScanner.java @@ -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 basePackages; + private final ClassLoader classLoader; + + private volatile List cachedEntities; + + public BusinessDataPermissionEntityScanner(Collection basePackages, ClassLoader classLoader) { + Set 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 getEntityMetadata() { + List result = cachedEntities; + if (result == null) { + synchronized (this) { + result = cachedEntities; + if (result == null) { + result = Collections.unmodifiableList(scanEntities()); + cachedEntities = result; + } + } + } + return result; + } + + private List scanEntities() { + Map 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 entityClass = (Class) 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 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 + '\'' + + '}'; + } + + } +} diff --git a/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/annotation/CompanyColumn.java b/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/annotation/CompanyColumn.java new file mode 100644 index 00000000..386036db --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/annotation/CompanyColumn.java @@ -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"; +} diff --git a/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/annotation/DeptColumn.java b/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/annotation/DeptColumn.java new file mode 100644 index 00000000..d8300aab --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/annotation/DeptColumn.java @@ -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"; +} diff --git a/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/dataobject/BusinessBaseDO.java b/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/dataobject/BusinessBaseDO.java index 56252a0e..c983a463 100644 --- a/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/dataobject/BusinessBaseDO.java +++ b/zt-framework/zt-spring-boot-starter-mybatis/src/main/java/com/zt/plat/framework/mybatis/core/dataobject/BusinessBaseDO.java @@ -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 { /** 公司编号 */ diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java index 8528c16d..bce32b79 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayAccessLogger.java @@ -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 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 collectHeaders(HttpServletRequest request) { + if (request == null) { + return Collections.emptyMap(); } - for (String value : values) { - if (StringUtils.hasText(value)) { - return value; + Map headers = new LinkedHashMap<>(); + Enumeration names = request.getHeaderNames(); + while (names != null && names.hasMoreElements()) { + String name = names.nextElement(); + Enumeration 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; - } -} +} \ No newline at end of file diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java index fadfe510..5d813d52 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/core/ApiGatewayRequestMapper.java @@ -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; diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java index 81e19c6a..b2bff142 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilter.java @@ -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_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(), "网关安全校验失败"); + } } } diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/dataobject/logger/ApiErrorLogDO.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/dataobject/logger/ApiErrorLogDO.java index 5be883e9..a0dd659d 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/dataobject/logger/ApiErrorLogDO.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/dataobject/logger/ApiErrorLogDO.java @@ -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; + /** * 编号 */ diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/logger/ApiErrorLogServiceImpl.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/logger/ApiErrorLogServiceImpl.java index 2c9d2567..7ca2cbff 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/logger/ApiErrorLogServiceImpl.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/logger/ApiErrorLogServiceImpl.java @@ -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 getApiErrorLogPage(ApiErrorLogPageReqVO pageReqVO) { return apiErrorLogMapper.selectPage(pageReqVO); diff --git a/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/service/logger/ApiErrorLogServiceImplTest.java b/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/service/logger/ApiErrorLogServiceImplTest.java index 94c82ed2..09d3e227 100644 --- a/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/service/logger/ApiErrorLogServiceImplTest.java +++ b/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/service/logger/ApiErrorLogServiceImplTest.java @@ -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)); + } + } diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java index 98e9f1dd..09fe2d3e 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFullSyncReqVO.java @@ -33,6 +33,9 @@ public class IWorkFullSyncReqVO { @Schema(description = "同步范围列表,默认同步全部。可选:subcompany、department、jobTitle、user") private List scopes; + @Schema(description = "指定同步记录的 iWork ID。传入后仅同步对应记录", example = "12345") + private String id; + @Schema(description = "是否包含已失效(canceled=1)的记录", example = "false") private Boolean includeCanceled = Boolean.FALSE; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java index 53c926e6..8954ea8f 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkSyncServiceImpl.java @@ -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 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 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 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 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 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()); diff --git a/zt-module-system/zt-module-system-server/src/main/resources/application.yaml b/zt-module-system/zt-module-system-server/src/main/resources/application.yaml index f27d9baa..c027e52b 100644 --- a/zt-module-system/zt-module-system-server/src/main/resources/application.yaml +++ b/zt-module-system/zt-module-system-server/src/main/resources/application.yaml @@ -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: