diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/config/GatewayWebClientConfiguration.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/config/GatewayWebClientConfiguration.java index 8113e1a3..67bfe6ef 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/config/GatewayWebClientConfiguration.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/config/GatewayWebClientConfiguration.java @@ -38,7 +38,7 @@ public class GatewayWebClientConfiguration { private ReactorClientHttpConnector buildConnector() { ConnectionProvider.Builder providerBuilder = ConnectionProvider.builder("databus-gateway") - .maxIdleTime(Duration.ofMillis(maxIdleTimeMillis)); + .maxIdleTime(Duration.ofMillis(maxIdleTimeMillis)); if (evictInBackgroundMillis > 0) { providerBuilder.evictInBackground(Duration.ofMillis(evictInBackgroundMillis)); } diff --git a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandler.java b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandler.java index 07dfacc8..6be45216 100644 --- a/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandler.java +++ b/zt-module-databus/zt-module-databus-server/src/main/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandler.java @@ -47,6 +47,7 @@ public class HttpStepHandler implements ApiStepHandler { private final ExpressionExecutor expressionExecutor; private static final Duration RETRY_DELAY = Duration.ofMillis(200); + private static final int RETRY_ATTEMPTS = 3; private static final Set DEFAULT_FORWARDED_HEADERS = Set.of( "authorization", @@ -388,7 +389,7 @@ public class HttpStepHandler implements ApiStepHandler { } private Mono applyResilientRetry(Mono responseMono, ApiStepDefinition stepDefinition) { - return responseMono.retryWhen(Retry.fixedDelay(1, RETRY_DELAY) + return responseMono.retryWhen(Retry.fixedDelay(RETRY_ATTEMPTS, RETRY_DELAY) .filter(this::isRetryableException) .doBeforeRetry(signal -> { if (log.isWarnEnabled()) { diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandlerTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandlerTest.java index 02badc3d..6f156e95 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandlerTest.java +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandlerTest.java @@ -11,32 +11,57 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.messaging.MessageHeaders; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; import java.io.IOException; +import java.net.SocketException; +import java.time.Duration; import java.util.Collections; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; class HttpStepHandlerTest { private MockWebServer server; private ExpressionExecutor expressionExecutor; private HttpStepHandler handler; + private ConnectionProvider connectionProvider; @BeforeEach void setUp() throws IOException { server = new MockWebServer(); server.start(); expressionExecutor = Mockito.mock(ExpressionExecutor.class); - handler = new HttpStepHandler(WebClient.builder(), expressionExecutor); + connectionProvider = ConnectionProvider.builder("http-step-handler-test") + .maxConnections(1) + .maxIdleTime(Duration.ofMinutes(2)) + .pendingAcquireMaxCount(-1) + .build(); + HttpClient httpClient = HttpClient.create(connectionProvider); + WebClient.Builder builder = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)); + handler = new HttpStepHandler(builder, expressionExecutor); } @AfterEach void tearDown() throws IOException { + if (connectionProvider != null) { + connectionProvider.disposeLater().block(Duration.ofSeconds(5)); + } server.shutdown(); } @@ -102,4 +127,44 @@ class HttpStepHandlerTest { assertThat(request.getHeader("Content-Type")).contains("application/json"); assertThat(request.getBody().readUtf8()).contains("\"amount\":100"); } + + @Test + void shouldRecoverWhenReusingStaleConnectionAfterIdle() { + AtomicInteger attemptCounter = new AtomicInteger(); + ExchangeFunction exchangeFunction = request -> { + int attempt = attemptCounter.incrementAndGet(); + if (attempt <= 2) { + return Mono.error(new IOException("Simulated connection reset", new SocketException("Connection reset"))); + } + return Mono.just(ClientResponse.create(HttpStatus.OK) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .body("{\"ok\":true}") + .build()); + }; + + HttpStepHandler simulatedHandler = new HttpStepHandler(WebClient.builder().exchangeFunction(exchangeFunction), expressionExecutor); + + ApiStepDO stepDO = new ApiStepDO(); + stepDO.setId(3L); + stepDO.setType("HTTP"); + stepDO.setTargetEndpoint("POST http://idle-reset.test/stale-connection"); + + ApiStepDefinition stepDefinition = ApiStepDefinition.builder() + .step(stepDO) + .metadata(Collections.emptyMap()) + .transforms(Collections.emptyList()) + .build(); + + var stepHandler = simulatedHandler.build(null, stepDefinition); + + ApiInvocationContext context = ApiInvocationContext.create(); + context.setRequestBody(Collections.singletonMap("foo", "bar")); + + assertThatCode(() -> stepHandler.handle(context, new MessageHeaders(Collections.emptyMap()))) + .doesNotThrowAnyException(); + + assertThat(attemptCounter.get()).isEqualTo(3); + assertThat(context.getStepResults()).isNotEmpty(); + assertThat(context.getStepResults().get(0).isSuccess()).isTrue(); + } } diff --git a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java index e6a49d71..4e1aeda5 100644 --- a/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java +++ b/zt-module-system/zt-module-system-api/src/main/java/com/zt/plat/module/system/enums/ErrorCodeConstants.java @@ -57,7 +57,7 @@ public interface ErrorCodeConstants { ErrorCode USER_REGISTER_DISABLED = new ErrorCode(1_002_003_011, "注册功能已关闭"); // ========== 部门模块 1-002-004-000 ========== - ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门"); + ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "当前上级部门已存在同名子部门"); ErrorCode DEPT_PARENT_NOT_EXITS = new ErrorCode(1_002_004_001,"父级部门不存在"); ErrorCode DEPT_NOT_FOUND = new ErrorCode(1_002_004_002, "机构不存在或当前账号无权限修改"); ErrorCode DEPT_EXITS_CHILDREN = new ErrorCode(1_002_004_003, "存在子部门,无法删除"); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java index b3adfa26..9e160f68 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptServiceImpl.java @@ -71,9 +71,16 @@ public class DeptServiceImpl implements DeptService { // 校验部门名的唯一性 validateDeptNameUnique(null, createReqVO.getParentId(), createReqVO.getName()); // 生成并校验部门编码 - String generatedCode = generateDeptCode(createReqVO.getParentId()); - createReqVO.setCode(generatedCode); - validateDeptCodeUnique(null, generatedCode); + Long effectiveParentId = normalizeParentId(createReqVO.getParentId()); + boolean isTopLevel = Objects.equals(effectiveParentId, DeptDO.PARENT_ID_ROOT); + String resolvedCode; + if (isTopLevel) { + resolvedCode = resolveTopLevelCode(null, createReqVO.getCode()); + } else { + resolvedCode = generateDeptCode(effectiveParentId); + validateDeptCodeUnique(null, resolvedCode); + } + createReqVO.setCode(resolvedCode); // 插入部门 DeptDO dept = BeanUtils.toBean(createReqVO, DeptDO.class); @@ -104,10 +111,25 @@ public class DeptServiceImpl implements DeptService { Long oldParentId = normalizeParentId(originalDept.getParentId()); boolean parentChanged = !Objects.equals(newParentId, oldParentId); if (parentChanged) { - String newCode = generateDeptCode(updateReqVO.getParentId()); + String newCode; + if (Objects.equals(newParentId, DeptDO.PARENT_ID_ROOT)) { + newCode = resolveTopLevelCode(updateReqVO.getId(), updateReqVO.getCode()); + } else { + newCode = generateDeptCode(updateReqVO.getParentId()); + validateDeptCodeUnique(updateReqVO.getId(), newCode); + } updateReqVO.setCode(newCode); } else { - updateReqVO.setCode(originalDept.getCode()); + if (Objects.equals(newParentId, DeptDO.PARENT_ID_ROOT)) { + String requestedCode = updateReqVO.getCode(); + if (StrUtil.isNotBlank(requestedCode) && !StrUtil.equals(requestedCode.trim(), originalDept.getCode())) { + updateReqVO.setCode(resolveTopLevelCode(updateReqVO.getId(), requestedCode)); + } else { + updateReqVO.setCode(originalDept.getCode()); + } + } else { + updateReqVO.setCode(originalDept.getCode()); + } } // 更新部门 @@ -189,7 +211,11 @@ public class DeptServiceImpl implements DeptService { @VisibleForTesting void validateDeptNameUnique(Long id, Long parentId, String name) { - DeptDO dept = deptMapper.selectByParentIdAndName(parentId, name); + Long effectiveParentId = normalizeParentId(parentId); + if (Objects.equals(effectiveParentId, DeptDO.PARENT_ID_ROOT)) { + return; + } + DeptDO dept = deptMapper.selectByParentIdAndName(effectiveParentId, name); if (dept == null) { return; } @@ -312,6 +338,18 @@ public class DeptServiceImpl implements DeptService { deptMapper.updateById(update); } + private String resolveTopLevelCode(Long currentDeptId, String requestedCode) { + String candidate = requestedCode; + if (candidate != null) { + candidate = candidate.trim(); + } + if (StrUtil.isBlank(candidate)) { + candidate = generateDeptCode(DeptDO.PARENT_ID_ROOT); + } + validateDeptCodeUnique(currentDeptId, candidate); + return candidate; + } + @Override public DeptDO getDept(Long id) { return deptMapper.selectById(id); diff --git a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptServiceImplTest.java b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptServiceImplTest.java index b98bb736..8ce4a892 100644 --- a/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptServiceImplTest.java +++ b/zt-module-system/zt-module-system-server/src/test/java/com/zt/plat/module/system/service/dept/DeptServiceImplTest.java @@ -84,6 +84,45 @@ public class DeptServiceImplTest extends BaseDbUnitTest { assertEquals(parentDept.getCode() + "001", childDept.getCode()); } + @Test + public void testCreateDept_topLevelAutoCodeAndDuplicates() { + DeptSaveReqVO topLevelReq = new DeptSaveReqVO(); + topLevelReq.setParentId(DeptDO.PARENT_ID_ROOT); + topLevelReq.setName("总部"); + topLevelReq.setSort(1); + topLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); + topLevelReq.setDeptSource(1); + Long topLevelId = deptService.createDept(topLevelReq); + DeptDO firstTop = deptMapper.selectById(topLevelId); + assertEquals("ZT001", firstTop.getCode()); + + DeptSaveReqVO secondTopLevelReq = new DeptSaveReqVO(); + secondTopLevelReq.setParentId(DeptDO.PARENT_ID_ROOT); + secondTopLevelReq.setName("总部"); + secondTopLevelReq.setSort(2); + secondTopLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); + secondTopLevelReq.setDeptSource(1); + Long secondTopId = deptService.createDept(secondTopLevelReq); + DeptDO secondTop = deptMapper.selectById(secondTopId); + assertEquals("ZT002", secondTop.getCode()); + } + + @Test + public void testCreateDept_topLevelRespectCustomCode() { + String customCode = "ROOT-001"; + DeptSaveReqVO topLevelReq = new DeptSaveReqVO(); + topLevelReq.setParentId(DeptDO.PARENT_ID_ROOT); + topLevelReq.setName("集团"); + topLevelReq.setSort(1); + topLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); + topLevelReq.setDeptSource(1); + topLevelReq.setCode(customCode); + + Long deptId = deptService.createDept(topLevelReq); + DeptDO created = deptMapper.selectById(deptId); + assertEquals(customCode, created.getCode()); + } + @Test public void testUpdateDept() { // mock 数据 @@ -205,20 +244,68 @@ public class DeptServiceImplTest extends BaseDbUnitTest { @Test public void testValidateNameUnique_duplicate() { - // mock 数据 - DeptDO deptDO = randomPojo(DeptDO.class).setDeptSource(null); + // mock 上级部门 + DeptDO parentDept = randomPojo(DeptDO.class, o -> { + o.setParentId(DeptDO.PARENT_ID_ROOT); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }).setDeptSource(null); + deptMapper.insert(parentDept); + + // mock 同级重名部门 + String duplicateName = randomString(6); + DeptDO deptDO = randomPojo(DeptDO.class, o -> { + o.setParentId(parentDept.getId()); + o.setName(duplicateName); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }).setDeptSource(null); deptMapper.insert(deptDO); - // 准备参数 - Long id = randomLongId(); - Long parentId = deptDO.getParentId(); - String name = deptDO.getName(); - // 调用, 并断言异常 - assertServiceException(() -> deptService.validateDeptNameUnique(id, parentId, name), + assertServiceException(() -> deptService.validateDeptNameUnique(randomLongId(), parentDept.getId(), duplicateName), DEPT_NAME_DUPLICATE); } + @Test + public void testValidateDeptNameUnique_topLevelDuplicateAllowed() { + // mock 顶级部门 + String duplicateName = randomString(6); + DeptDO topLevelDept = randomPojo(DeptDO.class, o -> { + o.setParentId(DeptDO.PARENT_ID_ROOT); + o.setName(duplicateName); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }).setDeptSource(null); + deptMapper.insert(topLevelDept); + + // 调用, 验证不会抛出异常 + assertDoesNotThrow(() -> deptService.validateDeptNameUnique(null, DeptDO.PARENT_ID_ROOT, duplicateName)); + } + + @Test + public void testValidateDeptNameUnique_differentParentAllowed() { + // mock 不同上级部门 + DeptDO parentA = randomPojo(DeptDO.class, o -> { + o.setParentId(DeptDO.PARENT_ID_ROOT); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }).setDeptSource(null); + deptMapper.insert(parentA); + DeptDO parentB = randomPojo(DeptDO.class, o -> { + o.setParentId(DeptDO.PARENT_ID_ROOT); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }).setDeptSource(null); + deptMapper.insert(parentB); + + String duplicateName = randomString(6); + DeptDO childUnderA = randomPojo(DeptDO.class, o -> { + o.setParentId(parentA.getId()); + o.setName(duplicateName); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }).setDeptSource(null); + deptMapper.insert(childUnderA); + + // 调用, 验证不同父级可以重名 + assertDoesNotThrow(() -> deptService.validateDeptNameUnique(null, parentB.getId(), duplicateName)); + } + @Test public void testGetDept() { // mock 数据