From e11065a5968adf5c4025a4dd88c2f610ba77f267 Mon Sep 17 00:00:00 2001 From: chenbowen Date: Tue, 2 Dec 2025 17:45:58 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E5=90=AF=E5=8A=A8=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E8=B0=83=E5=BA=A6=EF=BC=8C=E5=AE=9A=E6=97=B6=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=20databus=20api=202.=20=E4=BF=AE=E5=A4=8D=20databus=20?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=203.=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=20iwork=20=E5=9B=9E=E8=B0=83=E4=B8=9A=E5=8A=A1=E7=BC=96?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../desensitize/core/DesensitizeTest.java | 2 +- .../security/GatewaySecurityFilter.java | 8 +- .../gateway/step/impl/HttpStepHandler.java | 9 +- .../gateway/ApiGatewayControllerTest.java | 16 +- .../sample/DatabusApiInvocationExample.java | 17 +- .../security/GatewaySecurityFilterTest.java | 49 ++- ...tepHandlerConnectionResetScenarioTest.java | 360 ------------------ .../gateway/ApiDefinitionServiceImplTest.java | 4 + .../api/businessfile/BusinessFileApi.java | 11 +- .../api/businessfile/BusinessFileApiImpl.java | 6 + .../businessfile/BusinessFileController.java | 9 + .../businessfile/BusinessFileMapper.java | 4 + .../businessfile/BusinessFileService.java | 8 + .../businessfile/BusinessFileServiceImpl.java | 13 + .../iwork/vo/IWorkFileCallbackReqVO.java | 6 +- .../impl/IWorkIntegrationServiceImpl.java | 33 +- .../template/TemplateServerApplication.java | 2 + .../databus/TemplateDatabusRequestLogDO.java | 126 ++++++ .../TemplateDatabusRequestLogMapper.java | 12 + .../job/databus/TemplateDatabusScheduler.java | 21 + .../databus/TemplateDatabusInvokeService.java | 304 +++++++++++++++ .../sql/template_databus_request_log.sql | 49 +++ 22 files changed, 662 insertions(+), 407 deletions(-) delete mode 100644 zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandlerConnectionResetScenarioTest.java create mode 100644 zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/dal/dataobject/databus/TemplateDatabusRequestLogDO.java create mode 100644 zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/dal/mysql/databus/TemplateDatabusRequestLogMapper.java create mode 100644 zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/job/databus/TemplateDatabusScheduler.java create mode 100644 zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/service/databus/TemplateDatabusInvokeService.java create mode 100644 zt-module-template/zt-module-template-server/src/main/resources/sql/template_databus_request_log.sql diff --git a/zt-framework/zt-spring-boot-starter-web/src/test/java/com/zt/plat/framework/desensitize/core/DesensitizeTest.java b/zt-framework/zt-spring-boot-starter-web/src/test/java/com/zt/plat/framework/desensitize/core/DesensitizeTest.java index 3b6dc26a..0176695d 100644 --- a/zt-framework/zt-spring-boot-starter-web/src/test/java/com/zt/plat/framework/desensitize/core/DesensitizeTest.java +++ b/zt-framework/zt-spring-boot-starter-web/src/test/java/com/zt/plat/framework/desensitize/core/DesensitizeTest.java @@ -48,7 +48,7 @@ public class DesensitizeTest { DesensitizeDemo d = JsonUtils.parseObject(JsonUtils.toJsonString(desensitizeDemo), DesensitizeDemo.class); // 断言 assertNotNull(d); - assertEquals("芋***", d.getNickname()); + assertEquals("Z***", d.getNickname()); assertEquals("998800********31", d.getBankCard()); assertEquals("粤A6***6", d.getCarLicense()); assertEquals("0108*****22", d.getFixedPhone()); 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 b2bff142..c7b76152 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 @@ -273,10 +273,10 @@ public class GatewaySecurityFilter extends OncePerRequestFilter { String signatureType = resolveSignatureType(credential, security); try { -// boolean valid = CryptoSignatureUtils.verifySignature(signaturePayload, signatureType); -// if (!valid) { -// throw new SecurityValidationException(HttpStatus.UNAUTHORIZED, "签名校验失败"); -// } + boolean valid = CryptoSignatureUtils.verifySignature(signaturePayload, signatureType); + if (!valid) { + throw new SecurityValidationException(HttpStatus.UNAUTHORIZED, "签名校验失败"); + } } catch (IllegalArgumentException ex) { throw new SecurityValidationException(HttpStatus.INTERNAL_SERVER_ERROR, "签名算法配置异常"); } 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 fc271c96..58a7c7fd 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 @@ -393,8 +393,13 @@ public class HttpStepHandler implements ApiStepHandler { } private boolean supportsRequestBody(HttpMethod method) { - // 所有请求都要传递请求体 - return true; + if (method == null) { + return true; + } + return !(HttpMethod.GET.equals(method) + || HttpMethod.HEAD.equals(method) + || HttpMethod.OPTIONS.equals(method) + || HttpMethod.TRACE.equals(method)); } private Mono applyResilientRetry(Mono responseMono, ApiStepDefinition stepDefinition) { diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayControllerTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayControllerTest.java index 6a3768f4..e18f72b1 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayControllerTest.java +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/controller/admin/gateway/ApiGatewayControllerTest.java @@ -2,8 +2,11 @@ package com.zt.plat.module.databus.controller.admin.gateway; import com.zt.plat.module.databus.controller.admin.gateway.vo.ApiGatewayInvokeReqVO; import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayExecutionService; +import com.zt.plat.module.databus.framework.integration.gateway.core.IntegrationFlowManager; import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse; 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.service.gateway.ApiAnonymousUserService; import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService; import com.zt.plat.module.databus.service.gateway.ApiDefinitionService; import org.junit.jupiter.api.Test; @@ -48,8 +51,17 @@ class ApiGatewayControllerTest { @MockBean private StringRedisTemplate stringRedisTemplate; - @MockBean - private ApiClientCredentialService apiClientCredentialService; + @MockBean + private IntegrationFlowManager integrationFlowManager; + + @MockBean + private ApiClientCredentialService apiClientCredentialService; + + @MockBean + private ApiAnonymousUserService apiAnonymousUserService; + + @MockBean + private ApiGatewayAccessLogger apiGatewayAccessLogger; @Test void invokeShouldReturnGatewayEnvelope() throws Exception { 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 6189626d..d964863a 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 @@ -28,16 +28,10 @@ import java.util.UUID; public final class DatabusApiInvocationExample { public static final String TIMESTAMP = Long.toString(System.currentTimeMillis()); -// private static final String APP_ID = "ztmy"; -// private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ="; -// private static final String APP_ID = "test"; -// private static final String APP_SECRET = "RSYtKXrXPLMy3oeh0cOro6QCioRUgqfnKCkDkNq78sI="; - private static final String APP_ID = "testAnnoy"; - private static final String APP_SECRET = "jyGCymUjCFL2i3a4Tm3qBIkUrUl4ZgKPYvOU/47ZWcM="; + private static final String APP_ID = "ztmy"; + private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ="; 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/lgstOpenApi/v1"; - private static final String TARGET_API = "http://127.0.0.1:48080/admin-api/databus/api/portal/test/1"; -// private static final String TARGET_API = "http://127.0.0.1:48080/admin-api/databus/api/portal/lgstOpenApi/v1"; + private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/callback/v1"; private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); @@ -88,8 +82,11 @@ public final class DatabusApiInvocationExample { Map queryParams = new LinkedHashMap<>(); long extraTimestamp = 1761556157185L; +// String bodyJson = String.format(""" +// {"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(""" - {"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); Map bodyParams = parseBodyJson(bodyJson); diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilterTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilterTest.java index 62e34af3..9add6954 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilterTest.java +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/security/GatewaySecurityFilterTest.java @@ -9,6 +9,7 @@ import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; 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.service.gateway.ApiAnonymousUserService; import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService; import jakarta.servlet.http.HttpServletRequest; @@ -25,7 +26,9 @@ import org.springframework.security.core.context.SecurityContextHolder; import java.time.Duration; import java.util.Collections; +import java.util.Map; import java.util.Optional; +import java.util.TreeMap; import java.util.UUID; import static com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties.*; @@ -47,11 +50,12 @@ class GatewaySecurityFilterTest { ApiGatewayProperties properties = createProperties(); properties.getSecurity().setEnabled(false); StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class); - ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); - ApiAnonymousUserService anonymousUserService = mock(ApiAnonymousUserService.class); - when(anonymousUserService.issueAccessToken(any())).thenReturn(Optional.empty()); + ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); + ApiAnonymousUserService anonymousUserService = mock(ApiAnonymousUserService.class); when(anonymousUserService.issueAccessToken(any())).thenReturn(Optional.empty()); - GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, new ObjectMapper()); + ApiGatewayAccessLogger accessLogger = mock(ApiGatewayAccessLogger.class); + when(accessLogger.logEntrance(any())).thenReturn(1L); + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, new ObjectMapper(), accessLogger); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin-api/databus/api/portal/demo/v1"); request.setRemoteAddr("127.0.0.1"); @@ -74,7 +78,9 @@ class GatewaySecurityFilterTest { ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); ApiAnonymousUserService anonymousUserService = mock(ApiAnonymousUserService.class); when(anonymousUserService.issueAccessToken(any())).thenReturn(Optional.empty()); - GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, new ObjectMapper()); + ApiGatewayAccessLogger accessLogger = mock(ApiGatewayAccessLogger.class); + when(accessLogger.logEntrance(any())).thenReturn(1L); + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, new ObjectMapper(), accessLogger); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin-api/databus/api/portal/demo/v1"); request.setRemoteAddr("10.0.0.1"); @@ -97,6 +103,8 @@ class GatewaySecurityFilterTest { ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); ApiAnonymousUserService anonymousUserService = mock(ApiAnonymousUserService.class); + ApiGatewayAccessLogger accessLogger = mock(ApiGatewayAccessLogger.class); + when(accessLogger.logEntrance(any())).thenReturn(1L); when(anonymousUserService.issueAccessToken(any())).thenReturn(Optional.empty()); ApiClientCredentialDO credential = new ApiClientCredentialDO(); credential.setAppId("demo-app"); @@ -108,13 +116,13 @@ class GatewaySecurityFilterTest { properties.getSecurity().setRequireBodyEncryption(false); properties.getSecurity().setEncryptResponse(false); - GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, new ObjectMapper()); + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, new ObjectMapper(), accessLogger); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin-api/databus/api/portal/demo/v1"); request.setRemoteAddr("127.0.0.1"); long timestamp = System.currentTimeMillis(); String nonce = UUID.randomUUID().toString().replaceAll("-", ""); - String signature = signatureForApp("demo-app"); + String signature = signatureForApp("demo-app", timestamp); request.addHeader(APP_ID_HEADER, "demo-app"); request.addHeader(TIMESTAMP_HEADER, String.valueOf(timestamp)); request.addHeader(NONCE_HEADER, nonce); @@ -142,13 +150,15 @@ class GatewaySecurityFilterTest { ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); ApiAnonymousUserService anonymousUserService = mock(ApiAnonymousUserService.class); + ApiGatewayAccessLogger accessLogger = mock(ApiGatewayAccessLogger.class); + when(accessLogger.logEntrance(any())).thenReturn(1L); ApiClientCredentialDO credential = new ApiClientCredentialDO(); credential.setAppId("demo-app"); credential.setEncryptionKey("demo-secret-key"); credential.setEncryptionType(CryptoSignatureUtils.ENCRYPT_TYPE_AES); when(credentialService.findActiveCredential("demo-app")).thenReturn(Optional.of(credential)); - GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, new ObjectMapper()); + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, new ObjectMapper(), accessLogger); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin-api/databus/api/portal/demo/v1"); request.setRemoteAddr("127.0.0.1"); @@ -183,6 +193,8 @@ class GatewaySecurityFilterTest { ApiClientCredentialService credentialService = mock(ApiClientCredentialService.class); ApiAnonymousUserService anonymousUserService = mock(ApiAnonymousUserService.class); + ApiGatewayAccessLogger accessLogger = mock(ApiGatewayAccessLogger.class); + when(accessLogger.logEntrance(any())).thenReturn(1L); ApiClientCredentialDO credential = new ApiClientCredentialDO(); credential.setAppId("demo-app"); credential.setSignatureType(null); @@ -200,7 +212,7 @@ class GatewaySecurityFilterTest { when(anonymousUserService.find(99L)).thenReturn(Optional.of(details)); when(anonymousUserService.issueAccessToken(details)).thenReturn(Optional.of("mock-token")); - GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, new ObjectMapper()); + GatewaySecurityFilter filter = new GatewaySecurityFilter(properties, redisTemplate, credentialService, anonymousUserService, new ObjectMapper(), accessLogger); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin-api/databus/api/portal/demo/v1"); request.setRemoteAddr("127.0.0.1"); @@ -209,7 +221,7 @@ class GatewaySecurityFilterTest { request.addHeader(APP_ID_HEADER, "demo-app"); request.addHeader(TIMESTAMP_HEADER, String.valueOf(timestamp)); request.addHeader(NONCE_HEADER, nonce); - request.addHeader(SIGNATURE_HEADER, signatureForApp("demo-app")); + request.addHeader(SIGNATURE_HEADER, signatureForApp("demo-app", timestamp)); MockHttpServletResponse response = new MockHttpServletResponse(); MockFilterChain chain = new MockFilterChain(); @@ -231,7 +243,20 @@ class GatewaySecurityFilterTest { return properties; } - private String signatureForApp(String appId) { - return SecureUtil.md5("appId=" + appId); + private String signatureForApp(String appId, long timestamp) { + Map payload = new TreeMap<>(); + payload.put(APP_ID_HEADER, appId); + payload.put(TIMESTAMP_HEADER, String.valueOf(timestamp)); + StringBuilder sb = new StringBuilder(); + payload.forEach((key, value) -> { + if (value == null) { + return; + } + if (sb.length() > 0) { + sb.append('&'); + } + sb.append(key).append('=').append(value); + }); + return SecureUtil.md5(sb.toString()); } } diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandlerConnectionResetScenarioTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandlerConnectionResetScenarioTest.java deleted file mode 100644 index 854c7122..00000000 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/framework/integration/gateway/step/impl/HttpStepHandlerConnectionResetScenarioTest.java +++ /dev/null @@ -1,360 +0,0 @@ -package com.zt.plat.module.databus.framework.integration.gateway.step.impl; - -import com.zt.plat.framework.common.exception.ServiceException; -import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; -import reactor.netty.http.client.HttpClient; -import reactor.netty.http.client.PrematureCloseException; -import reactor.netty.resources.ConnectionProvider; -import reactor.util.retry.Retry; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.*; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Demonstrates the stale-connection scenario using the legacy vs the deferred retry pipeline. - */ -class HttpStepHandlerConnectionResetScenarioTest { - - private static final Duration RETRY_DELAY = Duration.ofMillis(200); - private static final int RETRY_ATTEMPTS = 3; - private static final Duration BLOCK_TIMEOUT = Duration.ofSeconds(5); - private static final Duration RESET_WAIT = Duration.ofMillis(300); - - @Test - void legacyPipelineLosesSuccessfulRetry() throws Exception { - try (ResetOnceHttpServer server = new ResetOnceHttpServer()) { - WebClient webClient = createWebClient(); - URI uri = server.uri("/demo"); - - warmUp(server, webClient, uri); - server.awaitWarmupConnectionReset(RESET_WAIT); - - legacyInvoke(webClient, uri, Map.of("mode", "legacy")); - - server.awaitFreshResponses(1, Duration.ofSeconds(2)); - assertThat(server.getFreshResponseCount()).isEqualTo(1); - assertThat(server.getServedBodies()).contains("reset", "fresh"); - } - } - - @Test - void deferredPipelinePropagatesSuccessfulRetry() throws Exception { - try (ResetOnceHttpServer server = new ResetOnceHttpServer()) { - WebClient webClient = createWebClient(); - URI uri = server.uri("/demo"); - - warmUp(server, webClient, uri); - server.awaitWarmupConnectionReset(RESET_WAIT); - - Object result = deferredInvoke(webClient, uri, Map.of("mode", "defer")); - assertThat(result).isInstanceOf(Map.class); - Map resultMap = (Map) result; - assertThat(resultMap.get("stage")).isEqualTo("fresh"); - - server.awaitFreshResponses(1, Duration.ofSeconds(2)); - assertThat(server.getFreshResponseCount()).isEqualTo(1); - assertThat(server.getServedBodies()).contains("reset", "fresh"); - } - } - - private WebClient createWebClient() { - ConnectionProvider provider = ConnectionProvider.builder("http-step-handler-demo") - .maxConnections(1) - .pendingAcquireMaxCount(-1) - .maxIdleTime(Duration.ofSeconds(5)) - .build(); - HttpClient httpClient = HttpClient.create(provider).compress(true); - return WebClient.builder() - .clientConnector(new ReactorClientHttpConnector(httpClient)) - .build(); - } - - private void warmUp(ResetOnceHttpServer server, WebClient webClient, URI uri) { - webClient.post() - .uri(uri) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(Map.of("warm", true)) - .retrieve() - .bodyToMono(Object.class) - .block(BLOCK_TIMEOUT); - server.awaitWarmupResponse(Duration.ofSeconds(2)); - } - - private Object legacyInvoke(WebClient webClient, URI uri, Object body) { - WebClient.RequestHeadersSpec spec = webClient.post() - .uri(uri) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(body); - Mono responseMono = spec.retrieve() - .bodyToMono(Object.class) - // 模拟业务中首次订阅后缓存失败结果的场景 - .cache(); - return responseMono.retryWhen(Retry.fixedDelay(RETRY_ATTEMPTS, RETRY_DELAY) - .filter(this::isRetryableException) - .onRetryExhaustedThrow((specification, signal) -> signal.failure())) - .block(BLOCK_TIMEOUT); - } - - private Object deferredInvoke(WebClient webClient, URI uri, Object body) { - Mono responseMono = Mono.defer(() -> webClient.post() - .uri(uri) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(body) - .retrieve() - .bodyToMono(Object.class) - // 通过 defer,每次重试都会重新创建带缓存的响应 Mono - .cache()); - return responseMono.retryWhen(Retry.fixedDelay(RETRY_ATTEMPTS, RETRY_DELAY) - .filter(this::isRetryableException)) - .block(BLOCK_TIMEOUT); - } - - private boolean isRetryableException(Throwable throwable) { - if (throwable == null) { - return false; - } - Throwable cursor = throwable; - while (cursor != null) { - if (cursor instanceof ServiceException) { - return false; - } - if (cursor instanceof PrematureCloseException) { - return true; - } - if (cursor instanceof IOException) { - return true; - } - cursor = cursor.getCause(); - } - return false; - } - - private static final class ResetOnceHttpServer implements AutoCloseable { - - private static final Duration RESET_DELAY = Duration.ofMillis(250); - - private final ServerSocket serverSocket; - private final ExecutorService acceptExecutor; - private final ScheduledExecutorService scheduler; - private final AtomicInteger connectionCount = new AtomicInteger(); - private final AtomicInteger freshResponses = new AtomicInteger(); - private final CountDownLatch warmupResponseSent = new CountDownLatch(1); - private final CountDownLatch warmupReset = new CountDownLatch(1); - private final List servedBodies = new CopyOnWriteArrayList<>(); - private volatile boolean running = true; - private volatile Socket warmupSocket; - - ResetOnceHttpServer() throws IOException { - this.serverSocket = new ServerSocket(0, 50, InetAddress.getByName("127.0.0.1")); - this.serverSocket.setReuseAddress(true); - this.acceptExecutor = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r, "reset-once-http-accept"); - t.setDaemon(true); - return t; - }); - this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> { - Thread t = new Thread(r, "reset-once-http-scheduler"); - t.setDaemon(true); - return t; - }); - acceptExecutor.submit(this::acceptLoop); - } - - URI uri(String path) { - Objects.requireNonNull(path, "path"); - if (!path.startsWith("/")) { - path = "/" + path; - } - return URI.create("http://127.0.0.1:" + serverSocket.getLocalPort() + path); - } - - List getServedBodies() { - return new ArrayList<>(servedBodies); - } - - int getFreshResponseCount() { - return freshResponses.get(); - } - - void awaitWarmupResponse(Duration timeout) { - awaitLatch(warmupResponseSent, timeout); - } - - void awaitWarmupConnectionReset(Duration timeout) { - awaitLatch(warmupReset, timeout); - } - - void awaitFreshResponses(int expected, Duration timeout) { - long deadline = System.nanoTime() + timeout.toNanos(); - while (freshResponses.get() < expected && System.nanoTime() < deadline) { - try { - Thread.sleep(10); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - return; - } - } - } - - private void awaitLatch(CountDownLatch latch, Duration timeout) { - try { - if (!latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) { - throw new IllegalStateException("Timed out waiting for latch"); - } - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - throw new IllegalStateException(ex); - } - } - - private void acceptLoop() { - try { - while (running) { - Socket socket = serverSocket.accept(); - int index = connectionCount.incrementAndGet(); - handle(socket, index); - } - } catch (SocketException ex) { - if (running) { - throw new IllegalStateException("Unexpected server socket error", ex); - } - } catch (IOException ex) { - if (running) { - throw new IllegalStateException("I/O error in server", ex); - } - } - } - - private void handle(Socket socket, int index) { - try { - socket.setTcpNoDelay(true); - RequestMetadata metadata = readRequest(socket); - if (index == 1) { - warmupSocket = socket; - String body = "{\"stage\":\"warmup\",\"path\":\"" + metadata.path + "\"}"; - writeResponse(socket, body, true); - servedBodies.add("warmup"); - warmupResponseSent.countDown(); - scheduler.schedule(() -> forceReset(socket), RESET_DELAY.toMillis(), TimeUnit.MILLISECONDS); - } else if (index == 2) { - // 模拟客户端复用到仍在连接池中的旧连接,但服务端已在请求到达后立即复位。 - servedBodies.add("reset"); - scheduler.schedule(() -> closeWithReset(socket), 10, TimeUnit.MILLISECONDS); - } else { - String body = "{\"stage\":\"fresh\",\"attempt\":" + index + "}"; - writeResponse(socket, body, false); - servedBodies.add("fresh"); - freshResponses.incrementAndGet(); - socket.close(); - } - } catch (IOException ex) { - // ignore for the purpose of the test - } - } - - private void forceReset(Socket socket) { - try { - if (!socket.isClosed()) { - servedBodies.add("reset"); - closeWithReset(socket); - } - } finally { - warmupReset.countDown(); - } - } - - private void closeWithReset(Socket socket) { - try { - if (!socket.isClosed()) { - socket.setSoLinger(true, 0); - socket.close(); - } - } catch (IOException ignored) { - // ignore - } - } - - private RequestMetadata readRequest(Socket socket) throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII)); - String requestLine = reader.readLine(); - if (requestLine == null) { - return new RequestMetadata("unknown", 0); - } - String path = requestLine.split(" ", 3)[1]; - int contentLength = 0; - String line; - while ((line = reader.readLine()) != null && !line.isEmpty()) { - if (line.toLowerCase(Locale.ROOT).startsWith("content-length:")) { - contentLength = Integer.parseInt(line.substring(line.indexOf(':') + 1).trim()); - } - } - if (contentLength > 0) { - char[] buffer = new char[contentLength]; - int read = 0; - while (read < contentLength) { - int r = reader.read(buffer, read, contentLength - read); - if (r < 0) { - break; - } - read += r; - } - } - return new RequestMetadata(path, contentLength); - } - - private void writeResponse(Socket socket, String body, boolean keepAlive) throws IOException { - byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); - StringBuilder builder = new StringBuilder() - .append("HTTP/1.1 200 OK\r\n") - .append("Content-Type: application/json\r\n") - .append("Content-Length: ").append(bodyBytes.length).append("\r\n"); - if (keepAlive) { - builder.append("Connection: keep-alive\r\n"); - } else { - builder.append("Connection: close\r\n"); - } - builder.append("\r\n"); - OutputStream outputStream = socket.getOutputStream(); - outputStream.write(builder.toString().getBytes(StandardCharsets.US_ASCII)); - outputStream.write(bodyBytes); - outputStream.flush(); - } - - @Override - public void close() throws Exception { - running = false; - try { - serverSocket.close(); - } catch (IOException ignored) { - } - if (warmupSocket != null && !warmupSocket.isClosed()) { - try { - warmupSocket.close(); - } catch (IOException ignored) { - } - } - scheduler.shutdownNow(); - acceptExecutor.shutdownNow(); - } - - private record RequestMetadata(String path, int contentLength) { - } - } -} diff --git a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/service/gateway/ApiDefinitionServiceImplTest.java b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/service/gateway/ApiDefinitionServiceImplTest.java index 004baf8f..fc521e7c 100644 --- a/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/service/gateway/ApiDefinitionServiceImplTest.java +++ b/zt-module-databus/zt-module-databus-server/src/test/java/com/zt/plat/module/databus/service/gateway/ApiDefinitionServiceImplTest.java @@ -16,6 +16,7 @@ import com.zt.plat.module.databus.dal.mysql.gateway.ApiPolicyRateLimitMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiStepMapper; import com.zt.plat.module.databus.dal.mysql.gateway.ApiTransformMapper; import com.zt.plat.module.databus.enums.gateway.ApiStatusEnum; +import com.zt.plat.module.databus.service.gateway.ApiVersionService; import com.zt.plat.module.databus.service.gateway.impl.ApiDefinitionServiceImpl; import jakarta.annotation.Resource; import org.junit.jupiter.api.AfterEach; @@ -63,6 +64,9 @@ class ApiDefinitionServiceImplTest extends BaseDbUnitTest { @MockBean private StringRedisTemplate stringRedisTemplate; + @MockBean + private ApiVersionService apiVersionService; + @TestConfiguration static class JacksonTestConfiguration { diff --git a/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/businessfile/BusinessFileApi.java b/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/businessfile/BusinessFileApi.java index ded1090b..67919fe0 100644 --- a/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/businessfile/BusinessFileApi.java +++ b/zt-module-infra/zt-module-infra-api/src/main/java/com/zt/plat/module/infra/api/businessfile/BusinessFileApi.java @@ -6,7 +6,7 @@ import com.zt.plat.module.infra.api.businessfile.dto.BusinessFilePageReqDTO; import com.zt.plat.module.infra.api.businessfile.dto.BusinessFileRespDTO; import com.zt.plat.module.infra.api.businessfile.dto.BusinessFileSaveReqDTO; import com.zt.plat.module.infra.api.businessfile.dto.BusinessFileWithUrlRespDTO; -import com.zt.plat.module.infra.enums.ApiConstants; +import com.zt.plat.framework.common.enums.RpcConstants; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -19,11 +19,11 @@ import java.util.List; /** * @author chenbowen */ -@FeignClient(name = ApiConstants.NAME) +@FeignClient(name = RpcConstants.INFRA_NAME) @Tag(name = "RPC 服务 - 业务附件关联") public interface BusinessFileApi { - String PREFIX = ApiConstants.PREFIX + "/business-file"; + String PREFIX = RpcConstants.INFRA_PREFIX + "/business-file"; @PostMapping(PREFIX + "/create") @Operation(summary = "创建业务附件关联") @@ -52,6 +52,11 @@ public interface BusinessFileApi { @Parameter(name = "id", description = "编号", required = true) CommonResult getBusinessFile(@RequestParam("id") Long id); + @GetMapping(PREFIX + "/get-by-code") + @Operation(summary = "根据业务编码获得业务附件关联") + @Parameter(name = "businessCode", description = "业务编码", required = true) + CommonResult getBusinessFileByBusinessCode(@RequestParam("businessCode") String businessCode); + @PostMapping(PREFIX + "/page") @Operation(summary = "获得业务附件关联分页") CommonResult> getBusinessFilePage(@RequestBody BusinessFilePageReqDTO pageReqDTO); diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/api/businessfile/BusinessFileApiImpl.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/api/businessfile/BusinessFileApiImpl.java index 5731f200..3ebca19d 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/api/businessfile/BusinessFileApiImpl.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/api/businessfile/BusinessFileApiImpl.java @@ -67,6 +67,12 @@ public class BusinessFileApiImpl implements BusinessFileApi { return success(BeanUtils.toBean(businessFile, BusinessFileRespDTO.class)); } + @Override + public CommonResult getBusinessFileByBusinessCode(String businessCode) { + BusinessFileDO businessFile = businessFileService.getBusinessFileByBusinessCode(businessCode); + return success(BeanUtils.toBean(businessFile, BusinessFileRespDTO.class)); + } + @Override public CommonResult> getBusinessFilePage(BusinessFilePageReqDTO pageReqDTO) { PageResult pageResult = businessFileService.getBusinessFilePage(BeanUtils.toBean(pageReqDTO, BusinessFilePageReqVO.class)); diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/businessfile/BusinessFileController.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/businessfile/BusinessFileController.java index 8c4f8eaa..2d143fb8 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/businessfile/BusinessFileController.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/controller/admin/businessfile/BusinessFileController.java @@ -90,6 +90,15 @@ public class BusinessFileController { return success(BeanUtils.toBean(businessFile, BusinessFileRespVO.class)); } + @GetMapping("/get-by-code") + @Operation(summary = "根据业务编码获得业务附件关联") + @Parameter(name = "businessCode", description = "业务编码", required = true) + @PreAuthorize("@ss.hasPermission('infra:business-file:query')") + public CommonResult getBusinessFileByBusinessCode(@RequestParam("businessCode") String businessCode) { + BusinessFileDO businessFile = businessFileService.getBusinessFileByBusinessCode(businessCode); + return success(BeanUtils.toBean(businessFile, BusinessFileRespVO.class)); + } + @GetMapping("/page") @Operation(summary = "获得业务附件关联分页") @PreAuthorize("@ss.hasPermission('infra:business-file:query')") diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/mysql/businessfile/BusinessFileMapper.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/mysql/businessfile/BusinessFileMapper.java index 35b67b26..5e886088 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/mysql/businessfile/BusinessFileMapper.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/dal/mysql/businessfile/BusinessFileMapper.java @@ -29,4 +29,8 @@ public interface BusinessFileMapper extends BaseMapperX { .orderByDesc(BusinessFileDO::getId)); } + default BusinessFileDO selectByBusinessCode(String businessCode) { + return selectFirstOne(BusinessFileDO::getBusinessCode, businessCode); + } + } \ No newline at end of file diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/businessfile/BusinessFileService.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/businessfile/BusinessFileService.java index c9b79f35..a7298eef 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/businessfile/BusinessFileService.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/businessfile/BusinessFileService.java @@ -53,6 +53,14 @@ public interface BusinessFileService { */ BusinessFileDO getBusinessFile(Long id); + /** + * 根据业务编码获得业务附件关联 + * + * @param businessCode 业务编码 + * @return 业务附件关联 + */ + BusinessFileDO getBusinessFileByBusinessCode(String businessCode); + /** * 获得业务附件关联分页 * diff --git a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/businessfile/BusinessFileServiceImpl.java b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/businessfile/BusinessFileServiceImpl.java index eb482468..4a30d7fa 100644 --- a/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/businessfile/BusinessFileServiceImpl.java +++ b/zt-module-infra/zt-module-infra-server/src/main/java/com/zt/plat/module/infra/service/businessfile/BusinessFileServiceImpl.java @@ -18,6 +18,7 @@ import com.zt.plat.module.system.api.user.AdminUserApi; import com.zt.plat.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import org.springframework.validation.annotation.Validated; import java.util.*; @@ -99,6 +100,18 @@ public class BusinessFileServiceImpl implements BusinessFileService { return businessFileMapper.selectById(id); } + @Override + public BusinessFileDO getBusinessFileByBusinessCode(String businessCode) { + if (!StringUtils.hasText(businessCode)) { + throw exception(BUSINESS_FILE_NOT_EXISTS); + } + BusinessFileDO businessFile = businessFileMapper.selectByBusinessCode(businessCode.trim()); + if (businessFile == null) { + throw exception(BUSINESS_FILE_NOT_EXISTS); + } + return businessFile; + } + @Override public PageResult getBusinessFilePage(BusinessFilePageReqVO pageReqVO) { return businessFileMapper.selectPage(pageReqVO); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java index 9a7dc009..58519d84 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/controller/admin/integration/iwork/vo/IWorkFileCallbackReqVO.java @@ -12,9 +12,9 @@ public class IWorkFileCallbackReqVO { @NotBlank(message = "文件 URL 不能为空") private String fileUrl; - @Schema(description = "业务 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") - @NotBlank(message = "业务 ID 不能为空") - private String businessId; + @Schema(description = "业务编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "DJ-2025-0001") + @NotBlank(message = "业务编码不能为空") + private String businessCode; @Schema(description = "文件名称,可选", example = "合同附件.pdf") private String fileName; diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java index 60167296..d1d7108a 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/integration/iwork/impl/IWorkIntegrationServiceImpl.java @@ -11,6 +11,7 @@ import com.zt.plat.framework.common.exception.ServiceException; import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil; import com.zt.plat.framework.common.pojo.CommonResult; import com.zt.plat.module.infra.api.businessfile.BusinessFileApi; +import com.zt.plat.module.infra.api.businessfile.dto.BusinessFileRespDTO; import com.zt.plat.module.infra.api.businessfile.dto.BusinessFileSaveReqDTO; import com.zt.plat.module.infra.api.file.FileApi; import com.zt.plat.module.infra.api.file.dto.FileCreateReqDTO; @@ -161,20 +162,16 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "回调请求不能为空"); } String fileUrl = Optional.ofNullable(reqVO.getFileUrl()).map(String::trim).orElse(""); - String businessIdStr = Optional.ofNullable(reqVO.getBusinessId()).map(String::trim).orElse(""); + String businessCode = Optional.ofNullable(reqVO.getBusinessCode()).map(String::trim).orElse(""); if (!StringUtils.hasText(fileUrl)) { throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "文件 URL 不能为空"); } - if (!StringUtils.hasText(businessIdStr)) { - throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "业务 ID 不能为空"); + if (!StringUtils.hasText(businessCode)) { + throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "业务编码不能为空"); } - Long businessId; - try { - businessId = Long.valueOf(businessIdStr); - } catch (NumberFormatException ex) { - throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "业务 ID 必须是数字: " + businessIdStr); - } + BusinessFileRespDTO referenceBusinessFile = loadBusinessFileByBusinessCode(businessCode); + Long businessId = referenceBusinessFile.getBusinessId(); // 通过文件 API 创建文件 FileCreateReqDTO fileCreateReqDTO = new FileCreateReqDTO(); @@ -191,7 +188,8 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { Long fileId = fileResult.getData().getId(); BusinessFileSaveReqDTO businessReq = BusinessFileSaveReqDTO.builder() - .businessId(businessId) + .businessId(businessId) + .businessCode(referenceBusinessFile.getBusinessCode()) .fileId(fileId) .fileName(fileResult.getData().getName()) .source("iwork") @@ -204,6 +202,21 @@ public class IWorkIntegrationServiceImpl implements IWorkIntegrationService { return bizResult.getData(); } + private BusinessFileRespDTO loadBusinessFileByBusinessCode(String businessCode) { + CommonResult businessResult = businessFileApi.getBusinessFileByBusinessCode(businessCode); + if (businessResult == null || !businessResult.isSuccess() || businessResult.getData() == null) { + throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), + "根据业务编码获取业务附件关联失败: " + Optional.ofNullable(businessResult) + .map(CommonResult::getMsg) + .orElse("未知错误")); + } + BusinessFileRespDTO businessFile = businessResult.getData(); + if (businessFile.getBusinessId() == null) { + throw new ServiceException(IWORK_CONFIGURATION_INVALID.getCode(), "业务编码未绑定业务 ID: " + businessCode); + } + return businessFile; + } + private byte[] downloadFileBytes(String fileUrl) { OkHttpClient client = okHttpClient(); Request request = new Request.Builder().url(fileUrl).get().build(); diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/TemplateServerApplication.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/TemplateServerApplication.java index 435beee6..cbbe927a 100644 --- a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/TemplateServerApplication.java +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/TemplateServerApplication.java @@ -2,6 +2,7 @@ package com.zt.plat.module.template; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; /** * 项目的启动类 @@ -9,6 +10,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; * @author 周迪 */ @SpringBootApplication +@EnableScheduling public class TemplateServerApplication { public static void main(String[] args) { diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/dal/dataobject/databus/TemplateDatabusRequestLogDO.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/dal/dataobject/databus/TemplateDatabusRequestLogDO.java new file mode 100644 index 00000000..14427fe7 --- /dev/null +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/dal/dataobject/databus/TemplateDatabusRequestLogDO.java @@ -0,0 +1,126 @@ +package com.zt.plat.module.template.dal.dataobject.databus; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.zt.plat.framework.mybatis.core.dataobject.BaseDO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * Databus 请求与响应日志表。 + */ +@TableName("template_databus_request_log") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TemplateDatabusRequestLogDO extends BaseDO { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** + * 请求唯一标识。 + */ + @TableField("REQUEST_ID") + private String requestId; + + /** + * 实际请求 URL。 + */ + @TableField("TARGET_URL") + private String targetUrl; + + /** + * HTTP 方法。 + */ + @TableField("HTTP_METHOD") + private String httpMethod; + + /** + * Query 参数 JSON。 + */ + @TableField("QUERY_PARAMS") + private String queryParams; + + /** + * 请求头 JSON。 + */ + @TableField("REQUEST_HEADERS") + private String requestHeaders; + + /** + * 原始请求体。 + */ + @TableField("REQUEST_BODY") + private String requestBody; + + /** + * 加密后的请求体。 + */ + @TableField("ENCRYPTED_REQUEST_BODY") + private String encryptedRequestBody; + + /** + * 签名。 + */ + @TableField("SIGNATURE") + private String signature; + + /** + * 随机串。 + */ + @TableField("NONCE") + private String nonce; + + /** + * 时间戳。 + */ + @TableField("ZT_TIMESTAMP") + private String ztTimestamp; + + /** + * HTTP 返回状态码。 + */ + @TableField("RESPONSE_STATUS") + private Integer responseStatus; + + /** + * 加密响应体。 + */ + @TableField("ENCRYPTED_RESPONSE_BODY") + private String encryptedResponseBody; + + /** + * 解密后的响应体。 + */ + @TableField("RESPONSE_BODY") + private String responseBody; + + /** + * 是否调用成功。 + */ + @TableField("SUCCESS") + private Boolean success; + + /** + * 错误信息。 + */ + @TableField("ERROR_MESSAGE") + private String errorMessage; + + /** + * 调用耗时(毫秒)。 + */ + @TableField("DURATION_MS") + private Long durationMs; + +} diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/dal/mysql/databus/TemplateDatabusRequestLogMapper.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/dal/mysql/databus/TemplateDatabusRequestLogMapper.java new file mode 100644 index 00000000..d4182a24 --- /dev/null +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/dal/mysql/databus/TemplateDatabusRequestLogMapper.java @@ -0,0 +1,12 @@ +package com.zt.plat.module.template.dal.mysql.databus; + +import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX; +import com.zt.plat.module.template.dal.dataobject.databus.TemplateDatabusRequestLogDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * Databus 请求日志 Mapper。 + */ +@Mapper +public interface TemplateDatabusRequestLogMapper extends BaseMapperX { +} diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/job/databus/TemplateDatabusScheduler.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/job/databus/TemplateDatabusScheduler.java new file mode 100644 index 00000000..fb530505 --- /dev/null +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/job/databus/TemplateDatabusScheduler.java @@ -0,0 +1,21 @@ +package com.zt.plat.module.template.job.databus; + +import com.zt.plat.module.template.service.databus.TemplateDatabusInvokeService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 基于 Spring 的 Databus 调度任务。 + */ +@Component +@RequiredArgsConstructor +public class TemplateDatabusScheduler { + + private final TemplateDatabusInvokeService invokeService; + + @Scheduled(cron = "0 0/10 * * * ?") + public void execute() { + invokeService.invokeAndRecord(); + } +} diff --git a/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/service/databus/TemplateDatabusInvokeService.java b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/service/databus/TemplateDatabusInvokeService.java new file mode 100644 index 00000000..d0be0ddd --- /dev/null +++ b/zt-module-template/zt-module-template-server/src/main/java/com/zt/plat/module/template/service/databus/TemplateDatabusInvokeService.java @@ -0,0 +1,304 @@ +package com.zt.plat.module.template.service.databus; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; +import com.zt.plat.framework.tenant.core.aop.TenantIgnore; +import com.zt.plat.module.template.dal.dataobject.databus.TemplateDatabusRequestLogDO; +import com.zt.plat.module.template.dal.mysql.databus.TemplateDatabusRequestLogMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * 调用 Databus 接口并记录日志。 + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class TemplateDatabusInvokeService { + + private static final TypeReference> MAP_TYPE = new TypeReference<>() { + }; + private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + private static final String HEADER_ZT_APP_ID = "ZT-App-Id"; + private static final String HEADER_ZT_TIMESTAMP = "ZT-Timestamp"; + private static final String HEADER_ZT_NONCE = "ZT-Nonce"; + private static final String HEADER_ZT_SIGNATURE = "ZT-Signature"; + private static final String HEADER_ZT_AUTH_TOKEN = "ZT-Auth-Token"; + private static final String HEADER_CONTENT_TYPE = "Content-Type"; + private static final String TARGET_URL = "http://172.16.46.63:30081/admin-api/databus/api/portal/callback/v1"; + private static final String APP_ID = "ztmy"; + private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ="; + private static final String AUTH_TOKEN = "a5d7cf609c0b47038ea405c660726ee9"; + private static final String DEFAULT_HTTP_METHOD = "POST"; + private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES; + private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5); + private static final Duration READ_TIMEOUT = Duration.ofSeconds(10); + private static final Map BASE_QUERY_PARAMS = Map.of( + "businessCode", "11", + "fileId", "11" + ); + private static final Map EXTRA_HEADERS = Map.of(); + private static final String DEFAULT_REQUEST_BODY = """ + { + } + """; + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() + .connectTimeout(CONNECT_TIMEOUT) + .build(); + + private final TemplateDatabusRequestLogMapper requestLogMapper; + private final ObjectMapper objectMapper; + + @TenantIgnore + public void invokeAndRecord() { + TemplateDatabusRequestLogDO logDO = TemplateDatabusRequestLogDO.builder() + .requestId(generateRequestId()) + .httpMethod(DEFAULT_HTTP_METHOD) + .targetUrl(TARGET_URL) + .success(Boolean.FALSE) + .build(); + Instant start = Instant.now(); + try { + Map queryParams = buildQueryParams(); + Map bodyParams = buildBodyParams(logDO.getRequestId()); + String requestBody = toJson(bodyParams); + String serializedQuery = toJson(queryParams); + logDO.setRequestBody(requestBody); + logDO.setQueryParams(serializedQuery); + + String timestamp = Long.toString(System.currentTimeMillis()); + logDO.setZtTimestamp(timestamp); + String nonce = generateNonce(); + logDO.setNonce(nonce); + String signature = generateSignature(queryParams, bodyParams, timestamp); + logDO.setSignature(signature); + + String encryptedBody = encryptPayload(requestBody); + logDO.setEncryptedRequestBody(encryptedBody); + + URI uri = buildUri(TARGET_URL, queryParams); + logDO.setTargetUrl(uri.toString()); + Map headers = buildHeaders(nonce, timestamp, signature); + logDO.setRequestHeaders(toJson(headers)); + + HttpRequest request = buildHttpRequest(uri, headers, encryptedBody); + HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + logDO.setResponseStatus(response.statusCode()); + logDO.setEncryptedResponseBody(response.body()); + logDO.setResponseBody(tryDecrypt(response.body())); + boolean success = response.statusCode() >= 200 && response.statusCode() < 300; + logDO.setSuccess(success); + if (!success) { + log.warn("Databus API 返回非 2xx 状态码: {}", response.statusCode()); + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + logDO.setErrorMessage(truncate("Interrupted: " + ex.getMessage(), 1000)); + log.warn("Databus 调度被中断: {}", ex.getMessage()); + } catch (Exception ex) { + logDO.setErrorMessage(truncate(ex.getMessage(), 1000)); + log.error("Databus 调度执行异常", ex); + } finally { + logDO.setDurationMs(Duration.between(start, Instant.now()).toMillis()); + requestLogMapper.insert(logDO); + } + } + + private Map buildQueryParams() { + Map params = new LinkedHashMap<>(); + BASE_QUERY_PARAMS.forEach(params::put); + return params; + } + + private Map buildBodyParams(String requestId) { + Map body = new LinkedHashMap<>(parseTemplateBody()); + body.put("__requestId__", requestId); + body.put("datetime", DATETIME_FORMATTER.format(LocalDateTime.now())); + body.putIfAbsent("system", "TEMPLATE"); + return body; + } + + private Map parseTemplateBody() { + try { + return objectMapper.readValue(DEFAULT_REQUEST_BODY, MAP_TYPE); + } catch (JsonProcessingException ex) { + throw new IllegalStateException("内置 Databus 请求体 JSON 解析失败", ex); + } + } + + private Map buildHeaders(String nonce, String timestamp, String signature) { + Map headers = new LinkedHashMap<>(); + headers.put(HEADER_ZT_APP_ID, APP_ID); + headers.put(HEADER_ZT_TIMESTAMP, timestamp); + headers.put(HEADER_ZT_NONCE, nonce); + headers.put(HEADER_ZT_SIGNATURE, signature); + if (StringUtils.hasText(AUTH_TOKEN)) { + headers.put(HEADER_ZT_AUTH_TOKEN, AUTH_TOKEN); + } + headers.put(HEADER_CONTENT_TYPE, "application/json"); + EXTRA_HEADERS.forEach((key, value) -> { + if (StringUtils.hasText(key) && value != null) { + headers.put(key, value); + } + }); + return headers; + } + + private HttpRequest buildHttpRequest(URI uri, Map headers, String encryptedBody) { + HttpRequest.Builder builder = HttpRequest.newBuilder(uri) + .timeout(READ_TIMEOUT); + headers.forEach(builder::header); + HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofString(encryptedBody, StandardCharsets.UTF_8); + switch (DEFAULT_HTTP_METHOD) { + case "GET": + builder.GET(); + break; + case "PUT": + builder.PUT(publisher); + break; + case "PATCH": + builder.method("PATCH", publisher); + break; + case "DELETE": + builder.method("DELETE", publisher); + break; + default: + builder.POST(publisher); + } + return builder.build(); + } + + private String encryptPayload(String plaintext) { + try { + return CryptoSignatureUtils.encrypt(plaintext, APP_SECRET, ENCRYPTION_TYPE); + } catch (Exception ex) { + throw new IllegalStateException("请求体加密失败", ex); + } + } + + private String tryDecrypt(String cipherText) { + if (!StringUtils.hasText(cipherText)) { + return cipherText; + } + try { + return CryptoSignatureUtils.decrypt(cipherText, APP_SECRET, ENCRYPTION_TYPE); + } catch (Exception ex) { + return " " + ex.getMessage(); + } + } + + private URI buildUri(String baseUrl, Map queryParams) { + if (queryParams.isEmpty()) { + return URI.create(baseUrl); + } + StringBuilder builder = new StringBuilder(baseUrl); + builder.append(baseUrl.contains("?") ? '&' : '?'); + boolean first = true; + for (Map.Entry entry : queryParams.entrySet()) { + if (!first) { + builder.append('&'); + } + first = false; + builder.append(encode(entry.getKey())) + .append('=') + .append(encode(Objects.toString(entry.getValue(), ""))); + } + return URI.create(builder.toString()); + } + + private String generateSignature(Map queryParams, Map bodyParams, String timestamp) { + TreeMap sorted = new TreeMap<>(); + queryParams.forEach((key, value) -> sorted.put(key, normalizeValue(value))); + bodyParams.forEach((key, value) -> sorted.put(key, normalizeValue(value))); + sorted.put(HEADER_ZT_APP_ID, APP_ID); + sorted.put(HEADER_ZT_TIMESTAMP, timestamp); + StringBuilder canonical = new StringBuilder(); + sorted.forEach((key, value) -> { + if (value == null) { + return; + } + if (canonical.length() > 0) { + canonical.append('&'); + } + canonical.append(key).append('=').append(value); + }); + return md5Hex(canonical.toString()); + } + + private Object normalizeValue(Object value) { + if (value == null) { + return null; + } + if (value instanceof Map || value instanceof Iterable) { + try { + return objectMapper.writeValueAsString(value); + } catch (JsonProcessingException ex) { + return value.toString(); + } + } + return value; + } + + private String md5Hex(String source) { + try { + MessageDigest digest = MessageDigest.getInstance("MD5"); + byte[] bytes = digest.digest(source.getBytes(StandardCharsets.UTF_8)); + StringBuilder builder = new StringBuilder(bytes.length * 2); + for (byte aByte : bytes) { + String part = Integer.toHexString(aByte & 0xFF); + if (part.length() == 1) { + builder.append('0'); + } + builder.append(part); + } + return builder.toString(); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("MD5 算法不可用", ex); + } + } + + private String encode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + private String generateRequestId() { + return UUID.randomUUID().toString().replace("-", ""); + } + + private String generateNonce() { + return UUID.randomUUID().toString().replace("-", ""); + } + + private String toJson(Object value) { + try { + return objectMapper.writeValueAsString(value); + } catch (JsonProcessingException ex) { + return String.valueOf(value); + } + } + + private String truncate(String value, int maxLength) { + if (!StringUtils.hasText(value) || value.length() <= maxLength) { + return value; + } + return value.substring(0, maxLength); + } +} diff --git a/zt-module-template/zt-module-template-server/src/main/resources/sql/template_databus_request_log.sql b/zt-module-template/zt-module-template-server/src/main/resources/sql/template_databus_request_log.sql new file mode 100644 index 00000000..5112f7b2 --- /dev/null +++ b/zt-module-template/zt-module-template-server/src/main/resources/sql/template_databus_request_log.sql @@ -0,0 +1,49 @@ +CREATE TABLE template_databus_request_log ( + ID BIGINT NOT NULL, + REQUEST_ID VARCHAR(64) NOT NULL, + TARGET_URL VARCHAR(512) NOT NULL, + HTTP_METHOD VARCHAR(16) NOT NULL, + QUERY_PARAMS TEXT, + REQUEST_HEADERS TEXT, + REQUEST_BODY TEXT, + ENCRYPTED_REQUEST_BODY TEXT, + SIGNATURE VARCHAR(128), + NONCE VARCHAR(64), + ZT_TIMESTAMP VARCHAR(32), + RESPONSE_STATUS INT, + ENCRYPTED_RESPONSE_BODY TEXT, + RESPONSE_BODY TEXT, + SUCCESS BIT DEFAULT '0' NOT NULL, + ERROR_MESSAGE TEXT, + DURATION_MS BIGINT, + CREATE_TIME DATETIME(6) DEFAULT CURRENT_TIMESTAMP NOT NULL, + UPDATE_TIME DATETIME(6) DEFAULT CURRENT_TIMESTAMP NOT NULL, + CREATOR VARCHAR(64), + UPDATER VARCHAR(64), + DELETED BIT DEFAULT '0' NOT NULL, + PRIMARY KEY (ID) +); + +COMMENT ON TABLE template_databus_request_log IS 'Databus 请求调度日志表'; +COMMENT ON COLUMN template_databus_request_log.ID IS '主键'; +COMMENT ON COLUMN template_databus_request_log.REQUEST_ID IS '请求唯一标识'; +COMMENT ON COLUMN template_databus_request_log.TARGET_URL IS '目标地址(含 Query)'; +COMMENT ON COLUMN template_databus_request_log.HTTP_METHOD IS 'HTTP 方法'; +COMMENT ON COLUMN template_databus_request_log.QUERY_PARAMS IS 'Query 参数 JSON'; +COMMENT ON COLUMN template_databus_request_log.REQUEST_HEADERS IS '请求头 JSON'; +COMMENT ON COLUMN template_databus_request_log.REQUEST_BODY IS '原始请求体'; +COMMENT ON COLUMN template_databus_request_log.ENCRYPTED_REQUEST_BODY IS '加密请求体'; +COMMENT ON COLUMN template_databus_request_log.SIGNATURE IS '签名'; +COMMENT ON COLUMN template_databus_request_log.NONCE IS '随机串'; +COMMENT ON COLUMN template_databus_request_log.ZT_TIMESTAMP IS '时间戳'; +COMMENT ON COLUMN template_databus_request_log.RESPONSE_STATUS IS '响应状态码'; +COMMENT ON COLUMN template_databus_request_log.ENCRYPTED_RESPONSE_BODY IS '加密响应体'; +COMMENT ON COLUMN template_databus_request_log.RESPONSE_BODY IS '解密后的响应体'; +COMMENT ON COLUMN template_databus_request_log.SUCCESS IS '是否成功'; +COMMENT ON COLUMN template_databus_request_log.ERROR_MESSAGE IS '错误信息'; +COMMENT ON COLUMN template_databus_request_log.DURATION_MS IS '耗时(毫秒)'; +COMMENT ON COLUMN template_databus_request_log.CREATE_TIME IS '创建时间'; +COMMENT ON COLUMN template_databus_request_log.UPDATE_TIME IS '更新时间'; +COMMENT ON COLUMN template_databus_request_log.CREATOR IS '创建人'; +COMMENT ON COLUMN template_databus_request_log.UPDATER IS '更新人'; +COMMENT ON COLUMN template_databus_request_log.DELETED IS '逻辑删除标记';