diff --git a/pom.xml b/pom.xml index e10371b8..32bbd373 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ https://github.com/YunaiV/ruoyi-vue-pro - 3.0.45 + 3.0.46 17 ${java.version} @@ -271,7 +271,8 @@ chenbowen - local + + dev 172.16.46.63:30848 chenbowen diff --git a/sql/dm/数据总线API凭证绑定与访问日志补充_20251209.sql b/sql/dm/数据总线API凭证绑定与访问日志补充_20251209.sql index ab9dcc19..c9dc74f1 100644 --- a/sql/dm/数据总线API凭证绑定与访问日志补充_20251209.sql +++ b/sql/dm/数据总线API凭证绑定与访问日志补充_20251209.sql @@ -19,7 +19,8 @@ CREATE TABLE databus_api_definition_credential ( deleted BIT DEFAULT '0' NOT NULL ); -CREATE UNIQUE INDEX uk_databus_api_definition_credential ON databus_api_definition_credential (api_id, credential_id, deleted); +-- 去掉错误的唯一索引逻辑 +-- CREATE UNIQUE INDEX uk_databus_api_definition_credential ON databus_api_definition_credential (api_id, credential_id, deleted); CREATE INDEX idx_databus_api_definition_credential_api ON databus_api_definition_credential (api_id); CREATE INDEX idx_databus_api_definition_credential_cred ON databus_api_definition_credential (credential_id); diff --git a/zt-dependencies/pom.xml b/zt-dependencies/pom.xml index 8881c31e..d9a30c74 100644 --- a/zt-dependencies/pom.xml +++ b/zt-dependencies/pom.xml @@ -26,7 +26,7 @@ https://github.com/YunaiV/ruoyi-vue-pro - 3.0.45 + 3.0.46 1.6.0 3.4.5 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 index 93c306f5..2c195fed 100644 --- 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 @@ -28,6 +28,14 @@ import java.util.*; @Slf4j public class BusinessDataPermissionEntityScanner { + /** + * 临时排除的包前缀(物流模块 DO,不参与数据权限扫描) + */ + private static final Set EXCLUDED_PACKAGE_PREFIXES = Set.of( + "com.zt.plat.module.backendlogistics", + "com.zt.plat.module.erp", + "com.zt.plat.framework.mybatis.core.dataobject.BusinessBaseDO"); + private final Set basePackages; private final ClassLoader classLoader; @@ -70,6 +78,9 @@ public class BusinessDataPermissionEntityScanner { if (!StringUtils.hasText(className)) { continue; } + if (isExcludedPackage(className)) { + continue; + } try { Class clazz = ClassUtils.forName(className, classLoader); if (clazz == BusinessBaseDO.class || !BusinessBaseDO.class.isAssignableFrom(clazz)) { @@ -92,6 +103,15 @@ public class BusinessDataPermissionEntityScanner { return new ArrayList<>(metadataMap.values()); } + private boolean isExcludedPackage(String className) { + for (String prefix : EXCLUDED_PACKAGE_PREFIXES) { + if (className.startsWith(prefix)) { + return true; + } + } + return false; + } + private EntityMetadata buildMetadata(Class entityClass) { String tableName = resolveTableName(entityClass); if (!StringUtils.hasText(tableName)) { diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java index 7ff3fe02..e54a7f54 100644 --- a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/main/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java @@ -3,6 +3,7 @@ package com.zt.plat.framework.datapermission.core.rule.dept; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.zt.plat.framework.common.biz.system.permission.PermissionCommonApi; import com.zt.plat.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO; import com.zt.plat.framework.common.enums.UserTypeEnum; @@ -14,7 +15,7 @@ import com.zt.plat.framework.mybatis.core.util.MyBatisUtils; import com.zt.plat.framework.security.core.LoginUser; import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; import com.zt.plat.framework.tenant.core.context.CompanyContextHolder; -import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.zt.plat.framework.tenant.core.context.DeptContextHolder; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.sf.jsqlparser.expression.Alias; @@ -108,6 +109,11 @@ public class DeptDataPermissionRule implements DataPermissionRule { return null; } + // 显式忽略部门数据权限时直接放行 + if (DeptContextHolder.shouldIgnore()) { + return null; + } + // 获得数据权限 DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class); // 从上下文中拿不到,则调用逻辑进行获取 @@ -136,6 +142,20 @@ public class DeptDataPermissionRule implements DataPermissionRule { } } + // 若存在部门上下文,优先使用上下文中的单一部门,必要时校验公司一致性 + Long ctxDeptId = DeptContextHolder.getDeptId(); + if (ctxDeptId != null && ctxDeptId > 0L) { + Long currentCompanyId = CompanyContextHolder.getCompanyId(); + Long ctxCompanyId = DeptContextHolder.getCompanyId(); + Long compareCompanyId = ctxCompanyId != null ? ctxCompanyId : currentCompanyId; + if (currentCompanyId != null && currentCompanyId > 0L + && compareCompanyId != null && !currentCompanyId.equals(compareCompanyId)) { + log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptContextHolder company mismatch: currentCompanyId={}, ctxCompanyId={}, ctxDeptId={}, source=DeptContextHolder]", + JsonUtils.toJsonString(loginUser), tableName, tableAlias == null ? null : tableAlias.getName(), + currentCompanyId, compareCompanyId, ctxDeptId); + } + } + // 情况一,如果是 ALL 可查看全部,则无需拼接条件 if (deptDataPermission.getAll()) { return null; diff --git a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/test/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/test/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java index 2177695b..77f194ce 100644 --- a/zt-framework/zt-spring-boot-starter-biz-data-permission/src/test/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java +++ b/zt-framework/zt-spring-boot-starter-biz-data-permission/src/test/java/com/zt/plat/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java @@ -7,10 +7,13 @@ import com.zt.plat.framework.common.enums.UserTypeEnum; import com.zt.plat.framework.common.util.collection.SetUtils; import com.zt.plat.framework.security.core.LoginUser; import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; +import com.zt.plat.framework.tenant.core.context.CompanyContextHolder; +import com.zt.plat.framework.tenant.core.context.DeptContextHolder; import com.zt.plat.framework.test.core.ut.BaseMockitoUnitTest; import com.zt.plat.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Expression; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -27,6 +30,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; /** @@ -48,7 +52,13 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { // 清空 rule rule.getTableNames().clear(); ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); - ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); + ((Map) ReflectUtil.getFieldValue(rule, "userColumns")).clear(); + } + + @AfterEach + void tearDown() { + DeptContextHolder.clear(); + CompanyContextHolder.clear(); } @Test // 无 LoginUser @@ -236,4 +246,88 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { } } + @Test // 忽略部门数据权限,直接放行 + void testGetExpression_ignoreDeptContext() { + try (MockedStatic secMock = mockStatic(SecurityFrameworkUtils.class); + MockedStatic deptCtxMock = mockStatic(DeptContextHolder.class)) { + String tableName = "t_order"; + Alias alias = new Alias("o"); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + secMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + deptCtxMock.when(DeptContextHolder::shouldIgnore).thenReturn(true); + + Expression expression = rule.getExpression(tableName, alias); + + assertNull(expression); + verifyNoInteractions(permissionApi); + } + } + + @Test // 上下文部门存在且公司一致时,清空原集合并覆盖为单一 deptId + void testGetExpression_deptContextOverride_companyMatch() { + try (MockedStatic secMock = mockStatic(SecurityFrameworkUtils.class); + MockedStatic deptCtxMock = mockStatic(DeptContextHolder.class); + MockedStatic companyCtxMock = mockStatic(CompanyContextHolder.class)) { + + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + secMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)) + .setCompanyId(1L); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission)); + + deptCtxMock.when(DeptContextHolder::shouldIgnore).thenReturn(false); + deptCtxMock.when(DeptContextHolder::getDeptId).thenReturn(99L); + deptCtxMock.when(DeptContextHolder::getCompanyId).thenReturn(1L); + companyCtxMock.when(CompanyContextHolder::getCompanyId).thenReturn(1L); + companyCtxMock.when(CompanyContextHolder::isIgnore).thenReturn(false); + + rule.addDeptColumn(tableName, "dept_id"); + + Expression expression = rule.getExpression(tableName, tableAlias); + + assertEquals("u.dept_id IN (99)", expression.toString()); + assertEquals(CollUtil.newLinkedHashSet(99L), deptDataPermission.getDeptIds()); + assertEquals(1L, deptDataPermission.getCompanyId()); + } + } + + @Test // 上下文部门存在但公司不一致时,记录告警并保持原逻辑(不覆盖) + void testGetExpression_deptContextOverride_companyMismatch() { + try (MockedStatic secMock = mockStatic(SecurityFrameworkUtils.class); + MockedStatic deptCtxMock = mockStatic(DeptContextHolder.class); + MockedStatic companyCtxMock = mockStatic(CompanyContextHolder.class)) { + + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + secMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(CollUtil.newLinkedHashSet(10L)) + .setCompanyId(1L); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission)); + + deptCtxMock.when(DeptContextHolder::shouldIgnore).thenReturn(false); + deptCtxMock.when(DeptContextHolder::getDeptId).thenReturn(99L); + deptCtxMock.when(DeptContextHolder::getCompanyId).thenReturn(2L); + companyCtxMock.when(CompanyContextHolder::getCompanyId).thenReturn(1L); + companyCtxMock.when(CompanyContextHolder::isIgnore).thenReturn(false); + + rule.addDeptColumn(tableName, "dept_id"); + + Expression expression = rule.getExpression(tableName, tableAlias); + + assertEquals("u.dept_id IN (10)", expression.toString()); + assertEquals(CollUtil.newLinkedHashSet(10L), deptDataPermission.getDeptIds()); + assertEquals(1L, deptDataPermission.getCompanyId()); + } + } + } diff --git a/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/context/DeptContextHolder.java b/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/context/DeptContextHolder.java new file mode 100644 index 00000000..e463ae50 --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/context/DeptContextHolder.java @@ -0,0 +1,61 @@ +package com.zt.plat.framework.tenant.core.context; + +import com.alibaba.ttl.TransmittableThreadLocal; + +/** + * 部门上下文 Holder,使用 {@link TransmittableThreadLocal} 支持在线程池/异步场景下的上下文传递。 + * + * 包含当前部门编号、所属公司编号以及是否忽略部门数据权限的标识。 + */ +public class DeptContextHolder { + + /** 当前部门编号 */ + private static final ThreadLocal DEPT_ID = new TransmittableThreadLocal<>(); + /** 当前部门所属公司编号(用于一致性校验) */ + private static final ThreadLocal COMPANY_ID = new TransmittableThreadLocal<>(); + /** 是否忽略部门数据权限 */ + private static final ThreadLocal IGNORE = new TransmittableThreadLocal<>(); + + public static Long getDeptId() { + return DEPT_ID.get(); + } + + public static Long getCompanyId() { + return COMPANY_ID.get(); + } + + /** + * 设置部门与所属公司编号。 + */ + public static void setContext(Long deptId, Long companyId) { + DEPT_ID.set(deptId); + COMPANY_ID.set(companyId); + } + + public static void setDeptId(Long deptId) { + DEPT_ID.set(deptId); + } + + public static void setCompanyId(Long companyId) { + COMPANY_ID.set(companyId); + } + + public static boolean hasDeptId() { + Long deptId = DEPT_ID.get(); + return deptId != null && deptId > 0L; + } + + public static void setIgnore(Boolean ignore) { + IGNORE.set(ignore); + } + + public static boolean shouldIgnore() { + return Boolean.TRUE.equals(IGNORE.get()); + } + + public static void clear() { + DEPT_ID.remove(); + COMPANY_ID.remove(); + IGNORE.remove(); + } +} diff --git a/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java b/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java index 538f6d4f..f4688a21 100644 --- a/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java +++ b/zt-framework/zt-spring-boot-starter-biz-tenant/src/main/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptor.java @@ -3,6 +3,7 @@ package com.zt.plat.framework.tenant.core.web; import com.zt.plat.framework.security.core.LoginUser; import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; import com.zt.plat.framework.tenant.core.context.CompanyContextHolder; +import com.zt.plat.framework.tenant.core.context.DeptContextHolder; import com.zt.plat.framework.web.core.util.WebFrameworkUtils; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -66,11 +67,19 @@ public class CompanyVisitContextInterceptor implements HandlerInterceptor { if (companyId == null || companyId <= 0L) { CompanyContextHolder.setIgnore(true); + DeptContextHolder.clear(); return true; } CompanyContextHolder.setIgnore(false); CompanyContextHolder.setCompanyId(companyId); + // 默认不忽略部门数据权限;如果有有效部门则写入上下文 + DeptContextHolder.setIgnore(false); + if (deptId != null && deptId > 0L) { + DeptContextHolder.setContext(deptId, companyId); + } else { + DeptContextHolder.clear(); + } if (loginUser == null) { return true; } @@ -91,7 +100,9 @@ public class CompanyVisitContextInterceptor implements HandlerInterceptor { LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); if (loginUser != null) { loginUser.setVisitCompanyId(0L); + loginUser.setVisitDeptId(0L); } + DeptContextHolder.clear(); } private Long resolveLong(Object value) { diff --git a/zt-framework/zt-spring-boot-starter-biz-tenant/src/test/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptorTest.java b/zt-framework/zt-spring-boot-starter-biz-tenant/src/test/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptorTest.java new file mode 100644 index 00000000..6fbe99ff --- /dev/null +++ b/zt-framework/zt-spring-boot-starter-biz-tenant/src/test/java/com/zt/plat/framework/tenant/core/web/CompanyVisitContextInterceptorTest.java @@ -0,0 +1,88 @@ +package com.zt.plat.framework.tenant.core.web; + +import com.zt.plat.framework.security.core.LoginUser; +import com.zt.plat.framework.tenant.core.context.CompanyContextHolder; +import com.zt.plat.framework.tenant.core.context.DeptContextHolder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * CompanyVisitContextInterceptor 单测,覆盖公司/部门上下文写入及清理。 + */ +class CompanyVisitContextInterceptorTest { + + private final HandlerInterceptor interceptor = new CompanyVisitContextInterceptor(); + + @AfterEach + void tearDown() { + CompanyContextHolder.clear(); + DeptContextHolder.clear(); + SecurityContextHolder.clearContext(); + } + + @Test // 无公司 id:应 ignore,公司/部门上下文清空 + void testPreHandle_noCompanyId_ignore() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean result = interceptor.preHandle(request, response, new Object()); + + assertTrue(result); + assertTrue(CompanyContextHolder.isIgnore()); + assertNull(CompanyContextHolder.getCompanyId()); + assertNull(DeptContextHolder.getDeptId()); + } + + @Test // 有公司无部门:写入公司,部门清空 + void testPreHandle_companyOnly() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + LoginUser loginUser = new LoginUser(); + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null)); + request.addHeader("visit-company-id", "11"); + + boolean result = interceptor.preHandle(request, response, new Object()); + + assertTrue(result); + assertFalse(CompanyContextHolder.isIgnore()); + assertEquals(11L, CompanyContextHolder.getCompanyId()); + assertFalse(DeptContextHolder.shouldIgnore()); + assertNull(DeptContextHolder.getDeptId()); + assertEquals(11L, loginUser.getVisitCompanyId()); + assertNull(loginUser.getVisitDeptId()); + } + + @Test // 有公司+部门:写入公司、部门上下文,afterCompletion 清理 visitDeptId & holder + void testPreHandle_withCompanyAndDept_andAfterCompletionClear() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + LoginUser loginUser = new LoginUser(); + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null)); + request.addHeader("visit-company-id", "22"); + request.addHeader("visit-dept-id", "33"); + + boolean result = interceptor.preHandle(request, response, new Object()); + + assertTrue(result); + assertFalse(CompanyContextHolder.isIgnore()); + assertEquals(22L, CompanyContextHolder.getCompanyId()); + assertEquals(33L, DeptContextHolder.getDeptId()); + assertEquals(22L, DeptContextHolder.getCompanyId()); + assertEquals(22L, loginUser.getVisitCompanyId()); + assertEquals(33L, loginUser.getVisitDeptId()); + + // afterCompletion: 清理 visitCompanyId/visitDeptId 与 holder + interceptor.afterCompletion(request, response, new Object(), null); + assertEquals(0L, loginUser.getVisitCompanyId()); + assertEquals(0L, loginUser.getVisitDeptId()); + assertNull(DeptContextHolder.getDeptId()); + assertNull(DeptContextHolder.getCompanyId()); + } +} 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 48c2a627..f9e5754d 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 @@ -33,6 +33,7 @@ import org.springframework.web.util.ContentCachingResponseWrapper; import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; +import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -304,15 +305,28 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { .build() .getQueryParams(); params.forEach((key, values) -> { - if (!StringUtils.hasText(key) || "signature".equalsIgnoreCase(key)) { + String decodedKey = URLDecoder.decode(key, StandardCharsets.UTF_8); + if (!StringUtils.hasText(decodedKey) || "signature".equalsIgnoreCase(decodedKey)) { return; } if (CollectionUtils.isEmpty(values)) { - target.put(key, ""); - } else if (values.size() == 1) { - target.put(key, values.get(0)); + target.put(decodedKey, ""); + return; + } + // 对每一个 value 做 URL 解码,确保与客户端原文签名一致 + List decodedValues = values.stream() + .map(val -> URLDecoder.decode(val, StandardCharsets.UTF_8)) + .toList(); + boolean allNullLiteral = decodedValues.stream() + .allMatch(v -> "null".equals(v)); + if (allNullLiteral) { + // 过滤掉仅包含字符串 "null" 的参数 + return; + } + if (decodedValues.size() == 1) { + target.put(decodedKey, decodedValues.get(0)); } else { - target.put(key, String.join(",", values)); + target.put(decodedKey, String.join(",", decodedValues)); } }); } catch (IllegalArgumentException ex) { diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java index 5bd323d3..af84f587 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/sample/DatabusApiInvocationExample.java @@ -5,6 +5,10 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import java.io.IOException; import java.io.PrintStream; import java.net.URI; @@ -23,10 +27,6 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.TreeMap; import java.util.UUID; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; /** * 可直接运行的示例,演示如何使用 appId=test 与对应密钥调用本地 Databus API。 @@ -37,14 +37,14 @@ public final class DatabusApiInvocationExample { // private static final String APP_ID = "iwork"; // private static final String APP_SECRET = "lpGXiNe/GMLk0vsbYGLa8eYxXq8tGhTbuu3/D4MJzIk="; - private static final String APP_ID = "ztmy"; - private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ="; + private static final String APP_ID = "jwyw"; + private static final String APP_SECRET = "MhfCcqB59rDTnB5yGOVXWtp/5a0JXir7pSjPl5cVMJ8="; private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES; // private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/callback/v1"; // private static final String TARGET_API = "http://172.16.46.195:48080/admin-api/databus/api/portal/lgstOpenApi/v1"; // private static final String TARGET_API = "http://172.16.46.195:48080/admin-api/databus/api/portal/lgstOpenApi/v1"; - private static final String TARGET_API = "https://jygk.chncopper.com:30078/admin-api/databus/api/portal/lgstOpenApi/v1"; -// private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/callback/v1"; +// private static final String TARGET_API = "https://jygk.chncopper.com:30078/admin-api/databus/api/portal/lgstOpenApi/v1"; + private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/testcbw/456"; // private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/lgstOpenApi/v1"; // private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/testcbw/456"; // ⚠️ 仅用于联调:信任所有证书 + 关闭主机名校验,生产环境请改为受信 CA 或自定义 truststore。 @@ -102,10 +102,16 @@ public final class DatabusApiInvocationExample { public static void main(String[] args) throws Exception { OUT.println("=== GET 请求示例 ==="); -// executeGetExample(); + executeGetExample(); // OUT.println(); -// OUT.println("=== POST 请求示例 ==="); - executePostExample(); + OUT.println("=== POST 请求示例 ==="); + executePostExample(""" + {"operateFlag":"I","__interfaceType__":"R_MY_JY_03","data":{"endAddressName":"1","customerCompanyName":"中铜国贸","endAddressDetail":"测试地址","remark":" ","custSuppType":"1","shipperCompanyName":"中铜国贸","consigneeCorpCode":" ","consignerContactPhone":" 11","importFlag":"10","businessSupplierCode":" ","entrustMainCode":"WT3162251027027","endAddressCode":" ","specifyCarrierCorpCode":"10086689","materDetail":[{"detailStatus":"10","batchNo":"ZLTD2510ZTGM0017001","measureCodeMdm":"CU032110001","packType":" ","quantityPlanDetail":1,"deliveryOrderNo":"ZLTD2510ZTGM0017001","measureCode":"CU032110001","goodsSpecification":" ","measureUnitCode":"PAC","entrustDetailCode":"WT3162251027027001","brand":" ","soNumber":"68ecf0055502d565d22b378a"}],"operateFlag":1,"custSuppName":"上海锦生金属有限公司","startAddressCode":" ","planStartTime":1761556166000,"customerCompanyCode":0,"importMethod":"EXW","startAddressType":"10","shipperCompanyCode":"3162","deliverCondition":"20","businessSupplierName":" ","startAddressDetail":" 111","transType":"30","endAddressType":"20","planEndTime":1761556166000,"specifyCarrierCorpName":null,"custSuppFlag":"0101","businessType":"20","consigneeCorpName":" ","custSuppCode":"10086689","startAddressName":" 111","consignerContactName":" 11"},"datetime":"20251027170929","busiBillCode":"WT3162251027027","system":"BRMS","__requestId__":"f918841c-14fb-49eb-9640-c5d1b3d46bd1"} + """); + + executePostExample(""" + {"msgCode":"YWJYGK0003","data":"{\\"memberId\\":65352,\\"routes\\":[{\\"carrierCorpCode\\":\\"10193776\\",\\"carrierCorpName\\":\\"成都达海金属加工配送有限公司\\",\\"endAddressCode\\":\\"440000-440300\\",\\"endAddressDetail\\":\\"深圳港\\",\\"endAddressDetailDesc\\":\\"广东省深圳市盐田区深盐路\\",\\"endAddressLatitude\\":22.567426,\\"endAddressLongitude\\":114.283271,\\"endAddressName\\":\\"广东省-深圳市\\",\\"endAddressType\\":\\"port\\",\\"startAddressCode\\":\\"520000-0\\",\\"startAddressDetail\\":\\"安龙\\",\\"startAddressDetailDesc\\":\\"贵州省安龙县德卧镇坡告村\\",\\"startAddressLatitude\\":25.066532,\\"startAddressLongitude\\":105.244186,\\"startAddressName\\":\\"贵州省-null\\",\\"startAddressType\\":\\"railway-station\\",\\"taskEndTime\\":1766592000000,\\"taskStartTime\\":1766332800000,\\"transType\\":\\"10\\"},{\\"carrierCorpCode\\":\\"10193776\\",\\"carrierCorpName\\":\\"成都达海金属加工配送有限公司\\",\\"endAddressCode\\":\\"230000-230600\\",\\"endAddressDetail\\":\\"大庆东\\",\\"endAddressDetailDesc\\":\\"黑龙江省大庆市龙凤区凤一路28号\\",\\"endAddressLatitude\\":46.544097,\\"endAddressLongitude\\":125.118902,\\"endAddressName\\":\\"黑龙江省-大庆市\\",\\"endAddressType\\":\\"railway-station\\",\\"startAddressCode\\":\\"440000-440300\\",\\"startAddressDetail\\":\\"深圳港\\",\\"startAddressDetailDesc\\":\\"广东省深圳市盐田区深盐路\\",\\"startAddressLatitude\\":22.567426,\\"startAddressLongitude\\":114.283271,\\"startAddressName\\":\\"广东省-深圳市\\",\\"startAddressType\\":\\"port\\",\\"taskEndTime\\":1767110400000,\\"taskStartTime\\":1766592000000,\\"transType\\":\\"30\\"},{\\"carrierCorpCode\\":\\"10193776\\",\\"carrierCorpName\\":\\"成都达海金属加工配送有限公司\\",\\"endAddressCode\\":\\"520000-0\\",\\"endAddressDetail\\":\\"郑屯\\",\\"endAddressDetailDesc\\":\\"贵州省郑屯镇\\",\\"endAddressName\\":\\"贵州省-null\\",\\"endAddressType\\":\\"railway-station\\",\\"startAddressCode\\":\\"230000-230600\\",\\"startAddressDetail\\":\\"大庆东\\",\\"startAddressDetailDesc\\":\\"黑龙江省大庆市龙凤区凤一路28号\\",\\"startAddressLatitude\\":46.544097,\\"startAddressLongitude\\":125.118902,\\"startAddressName\\":\\"黑龙江省-大庆市\\",\\"startAddressType\\":\\"railway-station\\",\\"taskEndTime\\":1768320000000,\\"taskStartTime\\":1767110400000,\\"transType\\":\\"20\\"}],\\"taskLineNumber\\":\\"CT202512230001_001\\",\\"taskNumber\\":\\"CT202512230001\\"}"} + """); } private static void executeGetExample() throws Exception { @@ -113,9 +119,11 @@ public final class DatabusApiInvocationExample { queryParams.put("businessCode", "11"); queryParams.put("fileId", "11"); queryParams.put("null", null); + queryParams.put("empty", ""); + queryParams.put("taskTimeEnd", "2025-12-28 23:00:00"); String signature = generateSignature(queryParams, Map.of()); URI requestUri = buildUri(TARGET_API, queryParams); - String nonce = "171615676c7d4d96b9f55f3d90ad27e0"; + String nonce = randomNonce(); HttpRequest request = HttpRequest.newBuilder(requestUri) .timeout(Duration.ofSeconds(10)) @@ -131,16 +139,14 @@ public final class DatabusApiInvocationExample { printResponse(response); } - private static void executePostExample() throws Exception { + private static void executePostExample(String json) throws Exception { Map queryParams = new LinkedHashMap<>(); long extraTimestamp = 1761556157185L; -// String bodyJson = String.format(""" +// String bodyJson = String.json(""" // {"operateFlag":"I","__interfaceType__":"R_MY_JY_03","data":{"endAddressName":"1","customerCompanyName":"中铜国贸","endAddressDetail":"测试地址","remark":" ","custSuppType":"1","shipperCompanyName":"中铜国贸","consigneeCorpCode":" ","consignerContactPhone":" 11","importFlag":"10","businessSupplierCode":" ","entrustMainCode":"WT3162251027027","endAddressCode":" ","specifyCarrierCorpCode":"10086689","materDetail":[{"detailStatus":"10","batchNo":"ZLTD2510ZTGM0017001","measureCodeMdm":"CU032110001","packType":" ","quantityPlanDetail":1,"deliveryOrderNo":"ZLTD2510ZTGM0017001","measureCode":"CU032110001","goodsSpecification":" ","measureUnitCode":"PAC","entrustDetailCode":"WT3162251027027001","brand":" ","soNumber":"68ecf0055502d565d22b378a"}],"operateFlag":1,"custSuppName":"上海锦生金属有限公司","startAddressCode":" ","planStartTime":1761556166000,"customerCompanyCode":0,"importMethod":"EXW","startAddressType":"10","shipperCompanyCode":"3162","deliverCondition":"20","businessSupplierName":" ","startAddressDetail":" 111","transType":"30","endAddressType":"20","planEndTime":1761556166000,"specifyCarrierCorpName":null,"custSuppFlag":"0101","businessType":"20","consigneeCorpName":" ","custSuppCode":"10086689","startAddressName":" 111","consignerContactName":" 11"},"datetime":"20251027170929","busiBillCode":"WT3162251027027","system":"BRMS","__requestId__":"f918841c-14fb-49eb-9640-c5d1b3d46bd1"} // """, extraTimestamp); - String bodyJson = String.format(""" - {} - """, extraTimestamp); + String bodyJson = String.format(json, extraTimestamp); Map bodyParams = parseBodyJson(bodyJson); String signature = generateSignature(queryParams, bodyParams); diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileConfigServiceImpl.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileConfigServiceImpl.java index f0e41360..f625e2db 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileConfigServiceImpl.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/file/FileConfigServiceImpl.java @@ -2,6 +2,8 @@ package com.zt.plat.module.infra.service.file; import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.util.IdUtil; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; import com.zt.plat.framework.common.pojo.PageResult; import com.zt.plat.framework.common.util.json.JsonUtils; import com.zt.plat.framework.common.util.validation.ValidationUtils; @@ -14,8 +16,6 @@ import com.zt.plat.module.infra.framework.file.core.client.FileClient; import com.zt.plat.module.infra.framework.file.core.client.FileClientConfig; import com.zt.plat.module.infra.framework.file.core.client.FileClientFactory; import com.zt.plat.module.infra.framework.file.core.enums.FileStorageEnum; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; import jakarta.annotation.Resource; import jakarta.validation.Validator; import lombok.Getter; @@ -172,7 +172,7 @@ public class FileConfigServiceImpl implements FileConfigService { // 校验存在 validateFileConfigExists(id); // 上传文件 - byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + byte[] content = ResourceUtil.readBytes("file/bg1.png"); return getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg"); } diff --git a/zt-module-infra/zt-module-infra-server/src/main/resources/file/bg1.png b/zt-module-infra/zt-module-infra-server/src/main/resources/file/bg1.png new file mode 100644 index 00000000..fa136983 Binary files /dev/null and b/zt-module-infra/zt-module-infra-server/src/main/resources/file/bg1.png differ diff --git a/zt-module-infra/zt-module-infra-server/src/main/resources/file/erweima.jpg b/zt-module-infra/zt-module-infra-server/src/main/resources/file/erweima.jpg deleted file mode 100644 index 1447283c..00000000 Binary files a/zt-module-infra/zt-module-infra-server/src/main/resources/file/erweima.jpg and /dev/null differ diff --git a/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/ftp/FtpFileClientTest.java b/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/ftp/FtpFileClientTest.java index 3cf505d1..bb8f3f33 100644 --- a/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/ftp/FtpFileClientTest.java +++ b/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/ftp/FtpFileClientTest.java @@ -41,7 +41,7 @@ public class FtpFileClientTest { client.init(); // 上传文件 String path = IdUtil.fastSimpleUUID() + ".jpg"; - byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + byte[] content = ResourceUtil.readBytes("file/bg1.png"); String fullPath = client.upload(content, path, "image/jpeg"); System.out.println("访问地址:" + fullPath); if (false) { diff --git a/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/local/LocalFileClientTest.java b/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/local/LocalFileClientTest.java index 7d7f3f58..fe246f45 100644 --- a/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/local/LocalFileClientTest.java +++ b/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/local/LocalFileClientTest.java @@ -20,7 +20,7 @@ public class LocalFileClientTest { client.init(); // 上传文件 String path = IdUtil.fastSimpleUUID() + ".jpg"; - byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + byte[] content = ResourceUtil.readBytes("file/bg1.png"); String fullPath = client.upload(content, path, "image/jpeg"); System.out.println("访问地址:" + fullPath); client.delete(path); diff --git a/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/s3/S3FileClientTest.java b/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/s3/S3FileClientTest.java index 40f04f85..8687b731 100644 --- a/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/s3/S3FileClientTest.java +++ b/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/s3/S3FileClientTest.java @@ -101,7 +101,7 @@ public class S3FileClientTest { client.init(); // 上传文件 String path = IdUtil.fastSimpleUUID() + ".jpg"; - byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + byte[] content = ResourceUtil.readBytes("file/bg1.png"); String fullPath = client.upload(content, path, "image/jpeg"); System.out.println("访问地址:" + fullPath); // 读取文件 diff --git a/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/sftp/SftpFileClientTest.java b/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/sftp/SftpFileClientTest.java index 457912e7..0f73cc42 100644 --- a/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/sftp/SftpFileClientTest.java +++ b/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/framework/file/core/sftp/SftpFileClientTest.java @@ -34,7 +34,7 @@ public class SftpFileClientTest { client.init(); // 上传文件 String path = IdUtil.fastSimpleUUID() + ".jpg"; - byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + byte[] content = ResourceUtil.readBytes("file/bg1.png"); String fullPath = client.upload(content, path, "image/jpeg"); System.out.println("访问地址:" + fullPath); if (false) { diff --git a/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/service/file/FileServiceImplTest.java b/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/service/file/FileServiceImplTest.java index bbb02810..b72e7adb 100644 --- a/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/service/file/FileServiceImplTest.java +++ b/zt-module-infra/zt-module-infra-server/src/test/java/com/zt/plat/module/infra/service/file/FileServiceImplTest.java @@ -88,7 +88,7 @@ public class FileServiceImplTest extends BaseDbUnitTest { @Test public void testCreateFile_success_01() throws Exception { // 准备参数 - byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + byte[] content = ResourceUtil.readBytes("file/bg1.png"); String name = "单测文件名"; String directory = randomString(); String type = "image/jpeg"; @@ -122,7 +122,7 @@ public class FileServiceImplTest extends BaseDbUnitTest { @Test public void testCreateFile_success_02() throws Exception { // 准备参数 - byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + byte[] content = ResourceUtil.readBytes("file/bg1.png"); // mock Master 文件客户端 String type = "image/jpeg"; FileClient client = mock(FileClient.class); @@ -318,7 +318,7 @@ public class FileServiceImplTest extends BaseDbUnitTest { @Test public void testCreateFile_withSameHash() throws Exception { // 准备参数 - byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + byte[] content = ResourceUtil.readBytes("file/bg1.png"); String name = "单测文件名"; String directory = randomString(); String type = "image/jpeg";