Merge remote-tracking branch 'base-version/main' into dev
This commit is contained in:
15
sql/dm/字典导入菜单权限_DM8.sql
Normal file
15
sql/dm/字典导入菜单权限_DM8.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- DM8 字典导入按钮权限脚本
|
||||||
|
-- 幂等处理:清理旧的导入权限按钮,再重新写入
|
||||||
|
|
||||||
|
DELETE FROM system_role_menu WHERE menu_id = 103001;
|
||||||
|
DELETE FROM system_menu WHERE id = 103001;
|
||||||
|
|
||||||
|
INSERT INTO system_menu (
|
||||||
|
id, name, permission, type, sort, parent_id, path, icon, component, component_name,
|
||||||
|
status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted
|
||||||
|
) VALUES (
|
||||||
|
103001, '字典导入', 'system:dict:import', 3, 6, 105, '#', '#', '', NULL,
|
||||||
|
0, '1', '1', '1', 'admin', CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '0'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 如需同步给指定角色,请手工向 system_role_menu 插入对应关系记录
|
||||||
@@ -86,6 +86,8 @@
|
|||||||
<netty.version>4.1.116.Final</netty.version>
|
<netty.version>4.1.116.Final</netty.version>
|
||||||
<mqtt.version>1.2.5</mqtt.version>
|
<mqtt.version>1.2.5</mqtt.version>
|
||||||
<pf4j-spring.version>0.9.0</pf4j-spring.version>
|
<pf4j-spring.version>0.9.0</pf4j-spring.version>
|
||||||
|
<!-- 规则引擎 -->
|
||||||
|
<liteflow.version>2.15.1</liteflow.version>
|
||||||
<vertx.version>4.5.13</vertx.version>
|
<vertx.version>4.5.13</vertx.version>
|
||||||
<!-- 三方云服务相关 -->
|
<!-- 三方云服务相关 -->
|
||||||
<commons-io.version>2.17.0</commons-io.version>
|
<commons-io.version>2.17.0</commons-io.version>
|
||||||
@@ -661,6 +663,13 @@
|
|||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 规则引擎 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.yomahub</groupId>
|
||||||
|
<artifactId>liteflow-spring-boot-starter</artifactId>
|
||||||
|
<version>${liteflow.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- PF4J -->
|
<!-- PF4J -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.pf4j</groupId>
|
<groupId>org.pf4j</groupId>
|
||||||
|
|||||||
@@ -49,4 +49,11 @@ public class ExcelUtils {
|
|||||||
.doReadAllSync();
|
.doReadAllSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> List<T> read(MultipartFile file, Class<T> head, int sheetNo) throws IOException {
|
||||||
|
return EasyExcel.read(file.getInputStream(), head, null)
|
||||||
|
.autoCloseStream(false)
|
||||||
|
.sheet(sheetNo)
|
||||||
|
.doReadSync();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,6 +272,13 @@ spring:
|
|||||||
- Path=/admin-api/databus/**
|
- Path=/admin-api/databus/**
|
||||||
filters:
|
filters:
|
||||||
- RewritePath=/admin-api/databus/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
|
- RewritePath=/admin-api/databus/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
|
||||||
|
## rule-server 服务
|
||||||
|
- id: rule-admin-api # 路由的编号
|
||||||
|
uri: grayLb://rule-server
|
||||||
|
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||||
|
- Path=/admin-api/rule/**
|
||||||
|
filters:
|
||||||
|
- RewritePath=/admin-api/rule/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
|
||||||
x-forwarded:
|
x-forwarded:
|
||||||
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
|
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import lombok.Getter;
|
|||||||
@Getter
|
@Getter
|
||||||
public enum ApiStatusEnum {
|
public enum ApiStatusEnum {
|
||||||
|
|
||||||
DRAFT(0),
|
DRAFT(0, "草稿"),
|
||||||
ONLINE(1),
|
ONLINE(1, "已上线"),
|
||||||
OFFLINE(2),
|
OFFLINE(2, "已下线"),
|
||||||
DEPRECATED(3);
|
DEPRECATED(3, "已废弃");
|
||||||
|
|
||||||
private final int status;
|
private final int status;
|
||||||
|
private final String label;
|
||||||
|
|
||||||
public static boolean isOnline(Integer status) {
|
public static boolean isOnline(Integer status) {
|
||||||
return status != null && status == ONLINE.status;
|
return status != null && status == ONLINE.status;
|
||||||
@@ -25,4 +26,21 @@ public enum ApiStatusEnum {
|
|||||||
return status != null && status == DEPRECATED.status;
|
return status != null && status == DEPRECATED.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ApiStatusEnum fromStatus(Integer status) {
|
||||||
|
if (status == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (ApiStatusEnum value : values()) {
|
||||||
|
if (value.status == status) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String labelOf(Integer status) {
|
||||||
|
ApiStatusEnum value = fromStatus(status);
|
||||||
|
return value != null ? value.label : "未知";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,46 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||||
|
import reactor.netty.http.client.HttpClient;
|
||||||
|
import reactor.netty.resources.ConnectionProvider;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class GatewayWebClientConfiguration {
|
public class GatewayWebClientConfiguration {
|
||||||
|
|
||||||
private final int maxInMemorySize;
|
private final int maxInMemorySize;
|
||||||
|
private final long maxIdleTimeMillis;
|
||||||
|
private final long evictInBackgroundMillis;
|
||||||
|
private final ReactorClientHttpConnector httpConnector;
|
||||||
|
|
||||||
public GatewayWebClientConfiguration(
|
public GatewayWebClientConfiguration(
|
||||||
@Value("${databus.gateway.web-client.max-in-memory-size:20971520}") int maxInMemorySize) {
|
@Value("${databus.gateway.web-client.max-in-memory-size:20971520}") int maxInMemorySize,
|
||||||
|
@Value("${databus.gateway.web-client.max-idle-time:45000}") long maxIdleTimeMillis,
|
||||||
|
@Value("${databus.gateway.web-client.evict-in-background-interval:20000}") long evictInBackgroundMillis) {
|
||||||
this.maxInMemorySize = maxInMemorySize;
|
this.maxInMemorySize = maxInMemorySize;
|
||||||
|
this.maxIdleTimeMillis = maxIdleTimeMillis > 0 ? maxIdleTimeMillis : 45000L;
|
||||||
|
this.evictInBackgroundMillis = Math.max(evictInBackgroundMillis, 0L);
|
||||||
|
this.httpConnector = buildConnector();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public WebClientCustomizer gatewayWebClientCustomizer() {
|
public WebClientCustomizer gatewayWebClientCustomizer() {
|
||||||
return builder -> builder.codecs(configurer ->
|
return builder -> builder
|
||||||
configurer.defaultCodecs().maxInMemorySize(maxInMemorySize));
|
.clientConnector(httpConnector)
|
||||||
|
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(maxInMemorySize));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReactorClientHttpConnector buildConnector() {
|
||||||
|
ConnectionProvider.Builder providerBuilder = ConnectionProvider.builder("databus-gateway")
|
||||||
|
.maxIdleTime(Duration.ofMillis(maxIdleTimeMillis));
|
||||||
|
if (evictInBackgroundMillis > 0) {
|
||||||
|
providerBuilder.evictInBackground(Duration.ofMillis(evictInBackgroundMillis));
|
||||||
|
}
|
||||||
|
ConnectionProvider provider = providerBuilder.build();
|
||||||
|
HttpClient httpClient = HttpClient.create(provider).compress(true);
|
||||||
|
return new ReactorClientHttpConnector(httpClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.zt.plat.module.databus.framework.integration.gateway.core;
|
package com.zt.plat.module.databus.framework.integration.gateway.core;
|
||||||
|
|
||||||
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
|
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
|
||||||
|
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
|
||||||
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
|
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.integration.core.MessagingTemplate;
|
import org.springframework.integration.core.MessagingTemplate;
|
||||||
@@ -36,6 +37,24 @@ public class ApiFlowDispatcher {
|
|||||||
return (ApiInvocationContext) reply.getPayload();
|
return (ApiInvocationContext) reply.getPayload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ApiInvocationContext dispatchWithAggregate(ApiDefinitionAggregate aggregate, ApiInvocationContext context) {
|
||||||
|
IntegrationFlowManager.DebugFlowHandle handle = integrationFlowManager.obtainDebugHandle(aggregate);
|
||||||
|
MessageChannel channel = handle.channel();
|
||||||
|
Message<ApiInvocationContext> message = MessageBuilder.withPayload(context)
|
||||||
|
.setHeader("apiCode", aggregate.getDefinition().getApiCode())
|
||||||
|
.setHeader("version", aggregate.getDefinition().getVersion())
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
Message<?> reply = messagingTemplate.sendAndReceive(channel, message);
|
||||||
|
if (reply == null) {
|
||||||
|
throw ServiceExceptionUtil.exception(API_FLOW_NO_REPLY, aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
|
||||||
|
}
|
||||||
|
return (ApiInvocationContext) reply.getPayload();
|
||||||
|
} finally {
|
||||||
|
integrationFlowManager.releaseDebugHandle(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private MessageChannel requireInputChannel(String apiCode, String version) {
|
private MessageChannel requireInputChannel(String apiCode, String version) {
|
||||||
// 未命中时,进行一次兜底补偿查询
|
// 未命中时,进行一次兜底补偿查询
|
||||||
return integrationFlowManager.locateInputChannel(apiCode, version)
|
return integrationFlowManager.locateInputChannel(apiCode, version)
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ package com.zt.plat.module.databus.framework.integration.gateway.core;
|
|||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.zt.plat.framework.common.exception.ServiceException;
|
import com.zt.plat.framework.common.exception.ServiceException;
|
||||||
|
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
|
||||||
import com.zt.plat.module.databus.controller.admin.gateway.vo.ApiGatewayInvokeReqVO;
|
import com.zt.plat.module.databus.controller.admin.gateway.vo.ApiGatewayInvokeReqVO;
|
||||||
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
|
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
|
||||||
|
import com.zt.plat.module.databus.framework.integration.gateway.domain.ApiDefinitionAggregate;
|
||||||
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse;
|
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse;
|
||||||
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
|
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
|
||||||
import com.zt.plat.module.databus.framework.integration.gateway.security.GatewayJwtResolver;
|
import com.zt.plat.module.databus.framework.integration.gateway.security.GatewayJwtResolver;
|
||||||
import com.zt.plat.module.databus.framework.integration.gateway.security.GatewaySecurityFilter;
|
import com.zt.plat.module.databus.framework.integration.gateway.security.GatewaySecurityFilter;
|
||||||
|
import com.zt.plat.module.databus.service.gateway.ApiDefinitionService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
@@ -23,6 +26,9 @@ import java.lang.reflect.Array;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static com.zt.plat.module.databus.service.gateway.impl.GatewayServiceErrorCodeConstants.API_DEFINITION_NOT_FOUND;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Orchestrates API portal request mapping, dispatch and response building so that
|
* Orchestrates API portal request mapping, dispatch and response building so that
|
||||||
@@ -40,6 +46,7 @@ public class ApiGatewayExecutionService {
|
|||||||
private static final String HEADER_QUERY_STRING = org.springframework.integration.http.HttpHeaders.PREFIX + "queryString";
|
private static final String HEADER_QUERY_STRING = org.springframework.integration.http.HttpHeaders.PREFIX + "queryString";
|
||||||
private static final String HEADER_REMOTE_ADDRESS = org.springframework.integration.http.HttpHeaders.PREFIX + "remoteAddress";
|
private static final String HEADER_REMOTE_ADDRESS = org.springframework.integration.http.HttpHeaders.PREFIX + "remoteAddress";
|
||||||
private static final String LOCAL_DEBUG_REMOTE_ADDRESS = "127.0.0.1";
|
private static final String LOCAL_DEBUG_REMOTE_ADDRESS = "127.0.0.1";
|
||||||
|
private static final String ATTR_DEBUG_INVOKE = "gatewayDebugInvoke";
|
||||||
|
|
||||||
private final ApiGatewayRequestMapper requestMapper;
|
private final ApiGatewayRequestMapper requestMapper;
|
||||||
private final ApiFlowDispatcher apiFlowDispatcher;
|
private final ApiFlowDispatcher apiFlowDispatcher;
|
||||||
@@ -47,6 +54,7 @@ public class ApiGatewayExecutionService {
|
|||||||
private final ApiGatewayProperties properties;
|
private final ApiGatewayProperties properties;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final ApiGatewayAccessLogger accessLogger;
|
private final ApiGatewayAccessLogger accessLogger;
|
||||||
|
private final ApiDefinitionService apiDefinitionService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a raw HTTP message (as provided by Spring Integration) into a context message.
|
* Maps a raw HTTP message (as provided by Spring Integration) into a context message.
|
||||||
@@ -67,8 +75,16 @@ public class ApiGatewayExecutionService {
|
|||||||
ApiInvocationContext context = message.getPayload();
|
ApiInvocationContext context = message.getPayload();
|
||||||
accessLogger.onRequest(context);
|
accessLogger.onRequest(context);
|
||||||
ApiInvocationContext responseContext;
|
ApiInvocationContext responseContext;
|
||||||
|
ApiDefinitionAggregate debugAggregate = null;
|
||||||
try {
|
try {
|
||||||
responseContext = apiFlowDispatcher.dispatch(context.getApiCode(), context.getApiVersion(), context);
|
if (Boolean.TRUE.equals(context.getAttributes().get(ATTR_DEBUG_INVOKE))) {
|
||||||
|
debugAggregate = resolveDebugAggregate(context);
|
||||||
|
}
|
||||||
|
if (debugAggregate != null) {
|
||||||
|
responseContext = apiFlowDispatcher.dispatchWithAggregate(debugAggregate, context);
|
||||||
|
} else {
|
||||||
|
responseContext = apiFlowDispatcher.dispatch(context.getApiCode(), context.getApiVersion(), context);
|
||||||
|
}
|
||||||
} catch (ServiceException ex) {
|
} catch (ServiceException ex) {
|
||||||
errorProcessor.applyServiceException(context, ex);
|
errorProcessor.applyServiceException(context, ex);
|
||||||
accessLogger.onException(context, ex);
|
accessLogger.onException(context, ex);
|
||||||
@@ -113,10 +129,20 @@ public class ApiGatewayExecutionService {
|
|||||||
ApiInvocationContext context = mappedMessage.getPayload();
|
ApiInvocationContext context = mappedMessage.getPayload();
|
||||||
// Ensure query parameters & headers from debug payload are reflected after mapping.
|
// Ensure query parameters & headers from debug payload are reflected after mapping.
|
||||||
mergeDebugMetadata(context, reqVO);
|
mergeDebugMetadata(context, reqVO);
|
||||||
|
context.getAttributes().put(ATTR_DEBUG_INVOKE, Boolean.TRUE);
|
||||||
ApiInvocationContext responseContext = dispatch(mappedMessage);
|
ApiInvocationContext responseContext = dispatch(mappedMessage);
|
||||||
return buildResponseEntity(responseContext);
|
return buildResponseEntity(responseContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ApiDefinitionAggregate resolveDebugAggregate(ApiInvocationContext context) {
|
||||||
|
Optional<ApiDefinitionAggregate> activeDefinition = apiDefinitionService.findByCodeAndVersion(context.getApiCode(), context.getApiVersion());
|
||||||
|
if (activeDefinition.isPresent()) {
|
||||||
|
return activeDefinition.get();
|
||||||
|
}
|
||||||
|
return apiDefinitionService.findByCodeAndVersionIncludingInactive(context.getApiCode(), context.getApiVersion())
|
||||||
|
.orElseThrow(() -> ServiceExceptionUtil.exception(API_DEFINITION_NOT_FOUND));
|
||||||
|
}
|
||||||
|
|
||||||
private Message<?> buildDebugMessage(ApiGatewayInvokeReqVO reqVO) {
|
private Message<?> buildDebugMessage(ApiGatewayInvokeReqVO reqVO) {
|
||||||
Object payload = preparePayload(reqVO.getPayload());
|
Object payload = preparePayload(reqVO.getPayload());
|
||||||
MessageBuilder<Object> builder = MessageBuilder.withPayload(payload);
|
MessageBuilder<Object> builder = MessageBuilder.withPayload(payload);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.springframework.stereotype.Component;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +64,33 @@ public class IntegrationFlowManager {
|
|||||||
return Optional.ofNullable(registration.getInputChannel());
|
return Optional.ofNullable(registration.getInputChannel());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DebugFlowHandle obtainDebugHandle(ApiDefinitionAggregate aggregate) {
|
||||||
|
String key = key(aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
|
||||||
|
IntegrationFlowContext.IntegrationFlowRegistration existing = activeRegistrations.get(key);
|
||||||
|
if (existing != null) {
|
||||||
|
return new DebugFlowHandle(existing.getInputChannel(), existing.getId(), false);
|
||||||
|
}
|
||||||
|
ApiFlowRegistration registration = apiFlowAssembler.assemble(aggregate);
|
||||||
|
String debugId = registration.getFlowId() + "#debug#" + UUID.randomUUID();
|
||||||
|
IntegrationFlowContext.IntegrationFlowRegistration debugRegistration = integrationFlowContext.registration(registration.getFlow())
|
||||||
|
.id(debugId)
|
||||||
|
.register();
|
||||||
|
log.debug("[API-PORTAL] 临时注册调试流程 {} 对应 apiCode={} version={}", debugId, aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
|
||||||
|
return new DebugFlowHandle(debugRegistration.getInputChannel(), debugRegistration.getId(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void releaseDebugHandle(DebugFlowHandle handle) {
|
||||||
|
if (handle == null || !handle.temporary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
integrationFlowContext.remove(handle.registrationId);
|
||||||
|
log.debug("[API-PORTAL] 已移除调试流程 {}", handle.registrationId);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("移除调试流程 {} 失败", handle.registrationId, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void registerFlow(ApiDefinitionAggregate aggregate) {
|
private void registerFlow(ApiDefinitionAggregate aggregate) {
|
||||||
String key = key(aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
|
String key = key(aggregate.getDefinition().getApiCode(), aggregate.getDefinition().getVersion());
|
||||||
deregisterByKey(key);
|
deregisterByKey(key);
|
||||||
@@ -93,4 +121,24 @@ public class IntegrationFlowManager {
|
|||||||
private String key(String apiCode, String version) {
|
private String key(String apiCode, String version) {
|
||||||
return (apiCode + ":" + version).toLowerCase();
|
return (apiCode + ":" + version).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class DebugFlowHandle {
|
||||||
|
private final MessageChannel channel;
|
||||||
|
private final String registrationId;
|
||||||
|
private final boolean temporary;
|
||||||
|
|
||||||
|
private DebugFlowHandle(MessageChannel channel, String registrationId, boolean temporary) {
|
||||||
|
this.channel = channel;
|
||||||
|
this.registrationId = registrationId;
|
||||||
|
this.temporary = temporary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageChannel channel() {
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean temporary() {
|
||||||
|
return temporary;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ import org.springframework.web.reactive.function.BodyInserters;
|
|||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.netty.http.client.PrematureCloseException;
|
||||||
|
import reactor.util.retry.Retry;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -43,6 +46,8 @@ public class HttpStepHandler implements ApiStepHandler {
|
|||||||
private final WebClient.Builder webClientBuilder;
|
private final WebClient.Builder webClientBuilder;
|
||||||
private final ExpressionExecutor expressionExecutor;
|
private final ExpressionExecutor expressionExecutor;
|
||||||
|
|
||||||
|
private static final Duration RETRY_DELAY = Duration.ofMillis(200);
|
||||||
|
|
||||||
private static final Set<String> DEFAULT_FORWARDED_HEADERS = Set.of(
|
private static final Set<String> DEFAULT_FORWARDED_HEADERS = Set.of(
|
||||||
"authorization",
|
"authorization",
|
||||||
"zt-auth-token",
|
"zt-auth-token",
|
||||||
@@ -108,6 +113,7 @@ public class HttpStepHandler implements ApiStepHandler {
|
|||||||
WebClient client = webClientBuilder.build();
|
WebClient client = webClientBuilder.build();
|
||||||
WebClient.RequestHeadersSpec<?> requestSpec = buildRequest(client, callSpec, requestPayload, headerMap, supportsBody);
|
WebClient.RequestHeadersSpec<?> requestSpec = buildRequest(client, callSpec, requestPayload, headerMap, supportsBody);
|
||||||
Mono<Object> responseMono = requestSpec.retrieve().bodyToMono(Object.class);
|
Mono<Object> responseMono = requestSpec.retrieve().bodyToMono(Object.class);
|
||||||
|
responseMono = applyResilientRetry(responseMono, stepDefinition);
|
||||||
Object response = timeout == null ? responseMono.block() : responseMono.block(timeout);
|
Object response = timeout == null ? responseMono.block() : responseMono.block(timeout);
|
||||||
payload.addStepResult(ApiStepResult.builder()
|
payload.addStepResult(ApiStepResult.builder()
|
||||||
.stepId(stepDefinition.getStep().getId())
|
.stepId(stepDefinition.getStep().getId())
|
||||||
@@ -380,4 +386,37 @@ public class HttpStepHandler implements ApiStepHandler {
|
|||||||
// 所有请求都要传递请求体
|
// 所有请求都要传递请求体
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<Object> applyResilientRetry(Mono<Object> responseMono, ApiStepDefinition stepDefinition) {
|
||||||
|
return responseMono.retryWhen(Retry.fixedDelay(1, RETRY_DELAY)
|
||||||
|
.filter(this::isRetryableException)
|
||||||
|
.doBeforeRetry(signal -> {
|
||||||
|
if (log.isWarnEnabled()) {
|
||||||
|
log.warn("HTTP 步骤 stepId={} 第{}次重试,原因:{}",
|
||||||
|
stepDefinition.getStep().getId(),
|
||||||
|
signal.totalRetriesInARow(),
|
||||||
|
signal.failure() == null ? "未知" : signal.failure().getMessage());
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ public interface ApiDefinitionService {
|
|||||||
*/
|
*/
|
||||||
Optional<ApiDefinitionAggregate> findByCodeAndVersion(String apiCode, String version);
|
Optional<ApiDefinitionAggregate> findByCodeAndVersion(String apiCode, String version);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookup API definition regardless of publish status.
|
||||||
|
*/
|
||||||
|
Optional<ApiDefinitionAggregate> findByCodeAndVersionIncludingInactive(String apiCode, String version);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh a specific definition by evicting cache and reloading from DB.
|
* Refresh a specific definition by evicting cache and reloading from DB.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -92,6 +92,12 @@ public class ApiDefinitionServiceImpl implements ApiDefinitionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ApiDefinitionAggregate> findByCodeAndVersionIncludingInactive(String apiCode, String version) {
|
||||||
|
return apiDefinitionMapper.selectByCodeAndVersion(apiCode, version)
|
||||||
|
.map(this::buildAggregate);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<ApiDefinitionAggregate> refresh(String apiCode, String version) {
|
public Optional<ApiDefinitionAggregate> refresh(String apiCode, String version) {
|
||||||
String cacheKey = buildCacheKey(apiCode, version);
|
String cacheKey = buildCacheKey(apiCode, version);
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode DICT_TYPE_NAME_DUPLICATE = new ErrorCode(1_002_006_003, "已经存在该名字的字典类型");
|
ErrorCode DICT_TYPE_NAME_DUPLICATE = new ErrorCode(1_002_006_003, "已经存在该名字的字典类型");
|
||||||
ErrorCode DICT_TYPE_TYPE_DUPLICATE = new ErrorCode(1_002_006_004, "已经存在该类型的字典类型");
|
ErrorCode DICT_TYPE_TYPE_DUPLICATE = new ErrorCode(1_002_006_004, "已经存在该类型的字典类型");
|
||||||
ErrorCode DICT_TYPE_HAS_CHILDREN = new ErrorCode(1_002_006_005, "无法删除,该字典类型还有字典数据");
|
ErrorCode DICT_TYPE_HAS_CHILDREN = new ErrorCode(1_002_006_005, "无法删除,该字典类型还有字典数据");
|
||||||
|
ErrorCode DICT_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_002_006_100, "导入字典数据不能为空!");
|
||||||
|
|
||||||
// ========== 字典数据 1-002-007-000 ==========
|
// ========== 字典数据 1-002-007-000 ==========
|
||||||
ErrorCode DICT_DATA_NOT_EXISTS = new ErrorCode(1_002_007_001, "当前字典数据不存在");
|
ErrorCode DICT_DATA_NOT_EXISTS = new ErrorCode(1_002_007_001, "当前字典数据不存在");
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
package com.zt.plat.module.system.controller.admin.dict;
|
package com.zt.plat.module.system.controller.admin.dict;
|
||||||
|
|
||||||
|
import com.alibaba.excel.EasyExcel;
|
||||||
|
import com.alibaba.excel.ExcelWriter;
|
||||||
|
import com.alibaba.excel.write.metadata.WriteSheet;
|
||||||
import com.zt.plat.framework.apilog.core.annotation.ApiAccessLog;
|
import com.zt.plat.framework.apilog.core.annotation.ApiAccessLog;
|
||||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||||
import com.zt.plat.framework.common.pojo.PageParam;
|
import com.zt.plat.framework.common.pojo.PageParam;
|
||||||
import com.zt.plat.framework.common.pojo.PageResult;
|
import com.zt.plat.framework.common.pojo.PageResult;
|
||||||
|
import com.zt.plat.framework.common.util.http.HttpUtils;
|
||||||
import com.zt.plat.framework.common.util.object.BeanUtils;
|
import com.zt.plat.framework.common.util.object.BeanUtils;
|
||||||
|
import com.zt.plat.framework.excel.core.handler.SelectSheetWriteHandler;
|
||||||
import com.zt.plat.framework.excel.core.util.ExcelUtils;
|
import com.zt.plat.framework.excel.core.util.ExcelUtils;
|
||||||
|
import com.alibaba.excel.converters.longconverter.LongStringConverter;
|
||||||
|
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
|
||||||
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportExcelVO;
|
||||||
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportRespVO;
|
||||||
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypePageReqVO;
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypePageReqVO;
|
||||||
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeRespVO;
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeRespVO;
|
||||||
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO;
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO;
|
||||||
@@ -14,6 +23,7 @@ import com.zt.plat.module.system.dal.dataobject.dict.DictTypeDO;
|
|||||||
import com.zt.plat.module.system.service.dict.DictTypeService;
|
import com.zt.plat.module.system.service.dict.DictTypeService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@@ -21,8 +31,10 @@ import jakarta.validation.Valid;
|
|||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static com.zt.plat.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
|
import static com.zt.plat.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
|
||||||
@@ -99,4 +111,45 @@ public class DictTypeController {
|
|||||||
BeanUtils.toBean(list, DictTypeRespVO.class));
|
BeanUtils.toBean(list, DictTypeRespVO.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/get-import-template")
|
||||||
|
@Operation(summary = "获得字典导入模板")
|
||||||
|
public void importTemplate(HttpServletResponse response) throws IOException {
|
||||||
|
List<DictImportExcelVO> samples = Arrays.asList(
|
||||||
|
DictImportExcelVO.builder()
|
||||||
|
.dictTypeName("性别").dictType("system_user_sex").dictTypeRemark("系统内置示例")
|
||||||
|
.label("男").value("1").sort(1).colorType("primary").dataRemark("示例数据").build(),
|
||||||
|
DictImportExcelVO.builder()
|
||||||
|
.dictTypeName("证件类型").dictType("system_id_card_type").dictTypeRemark("自定义示例")
|
||||||
|
.label("身份证").value("ID").sort(1).dataRemark("示例数据").build()
|
||||||
|
);
|
||||||
|
|
||||||
|
try (ExcelWriter writer = EasyExcel.write(response.getOutputStream())
|
||||||
|
.autoCloseStream(false)
|
||||||
|
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
|
||||||
|
.registerConverter(new LongStringConverter())
|
||||||
|
.build()) {
|
||||||
|
WriteSheet sheet = EasyExcel.writerSheet(0, "字典导入")
|
||||||
|
.head(DictImportExcelVO.class)
|
||||||
|
.registerWriteHandler(new SelectSheetWriteHandler(DictImportExcelVO.class))
|
||||||
|
.build();
|
||||||
|
writer.write(samples, sheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.addHeader("Content-Disposition",
|
||||||
|
"attachment;filename=" + HttpUtils.encodeUtf8("字典导入模板.xls"));
|
||||||
|
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/import")
|
||||||
|
@Operation(summary = "导入字典")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = "file", description = "Excel 文件", required = true)
|
||||||
|
})
|
||||||
|
@PreAuthorize("@ss.hasPermission('system:dict:import')")
|
||||||
|
public CommonResult<DictImportRespVO> importDict(@RequestParam("file") MultipartFile file) throws IOException {
|
||||||
|
List<DictImportExcelVO> importList = ExcelUtils.read(file, DictImportExcelVO.class, 0);
|
||||||
|
DictImportRespVO respVO = dictTypeService.importDictList(importList);
|
||||||
|
return success(respVO);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.zt.plat.module.system.controller.admin.dict.vo.data;
|
||||||
|
|
||||||
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportExcelVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated 迁移到单工作表导入模型 {@link DictImportExcelVO}
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public final class DictDataImportExcelVO {
|
||||||
|
|
||||||
|
private DictDataImportExcelVO() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.zt.plat.module.system.controller.admin.dict.vo.type;
|
||||||
|
|
||||||
|
import com.alibaba.excel.annotation.ExcelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典导入 Excel VO(单行同时包含字典类型与字典数据)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Accessors(chain = false)
|
||||||
|
public class DictImportExcelVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典名称
|
||||||
|
*/
|
||||||
|
@ExcelProperty("字典名称")
|
||||||
|
private String dictTypeName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典类型
|
||||||
|
*/
|
||||||
|
@ExcelProperty("字典类型")
|
||||||
|
private String dictType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典类型备注
|
||||||
|
*/
|
||||||
|
@ExcelProperty("类型备注")
|
||||||
|
private String dictTypeRemark;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典标签
|
||||||
|
*/
|
||||||
|
@ExcelProperty("字典标签")
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典键值
|
||||||
|
*/
|
||||||
|
@ExcelProperty("字典键值")
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序
|
||||||
|
*/
|
||||||
|
@ExcelProperty("排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 颜色类型
|
||||||
|
*/
|
||||||
|
@ExcelProperty("颜色类型")
|
||||||
|
private String colorType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSS 样式
|
||||||
|
*/
|
||||||
|
@ExcelProperty("CSS 样式")
|
||||||
|
private String cssClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典数据备注
|
||||||
|
*/
|
||||||
|
@ExcelProperty("数据备注")
|
||||||
|
private String dataRemark;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.zt.plat.module.system.controller.admin.dict.vo.type;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典导入响应 VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@Schema(description = "管理后台 - 字典导入 Response VO")
|
||||||
|
public class DictImportRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "创建成功的字典类型名称列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private List<String> createDictTypeNames;
|
||||||
|
|
||||||
|
@Schema(description = "更新成功的字典类型名称列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private List<String> updateDictTypeNames;
|
||||||
|
|
||||||
|
@Schema(description = "导入失败的字典类型集合,key 为字典名称,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Map<String, String> failureDictTypeNames;
|
||||||
|
|
||||||
|
@Schema(description = "创建成功的字典数据标识列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private List<String> createDictDataKeys;
|
||||||
|
|
||||||
|
@Schema(description = "更新成功的字典数据标识列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private List<String> updateDictDataKeys;
|
||||||
|
|
||||||
|
@Schema(description = "导入失败的字典数据集合,key 为字典数据标识,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Map<String, String> failureDictDataKeys;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.zt.plat.module.system.controller.admin.dict.vo.type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated 保留空壳文件以兼容历史引用,新的导入请使用 {@link DictImportExcelVO}
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public final class DictTypeImportExcelVO {
|
||||||
|
|
||||||
|
private DictTypeImportExcelVO() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ package com.zt.plat.module.system.service.dict;
|
|||||||
import com.zt.plat.framework.common.pojo.PageResult;
|
import com.zt.plat.framework.common.pojo.PageResult;
|
||||||
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypePageReqVO;
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypePageReqVO;
|
||||||
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO;
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO;
|
||||||
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportExcelVO;
|
||||||
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportRespVO;
|
||||||
import com.zt.plat.module.system.dal.dataobject.dict.DictTypeDO;
|
import com.zt.plat.module.system.dal.dataobject.dict.DictTypeDO;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -67,4 +69,12 @@ public interface DictTypeService {
|
|||||||
*/
|
*/
|
||||||
List<DictTypeDO> getDictTypeList();
|
List<DictTypeDO> getDictTypeList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入字典类型与字典数据(单工作表)
|
||||||
|
*
|
||||||
|
* @param importList 导入行列表
|
||||||
|
* @return 导入结果
|
||||||
|
*/
|
||||||
|
DictImportRespVO importDictList(List<DictImportExcelVO> importList);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,36 @@
|
|||||||
package com.zt.plat.module.system.service.dict;
|
package com.zt.plat.module.system.service.dict;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.zt.plat.framework.common.enums.CommonStatusEnum;
|
||||||
import com.zt.plat.framework.common.pojo.PageResult;
|
import com.zt.plat.framework.common.pojo.PageResult;
|
||||||
|
import com.zt.plat.framework.common.util.collection.CollectionUtils;
|
||||||
import com.zt.plat.framework.common.util.date.LocalDateTimeUtils;
|
import com.zt.plat.framework.common.util.date.LocalDateTimeUtils;
|
||||||
import com.zt.plat.framework.common.util.object.BeanUtils;
|
import com.zt.plat.framework.common.util.object.BeanUtils;
|
||||||
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportExcelVO;
|
||||||
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictImportRespVO;
|
||||||
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypePageReqVO;
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypePageReqVO;
|
||||||
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO;
|
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO;
|
||||||
|
import com.zt.plat.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO;
|
||||||
|
import com.zt.plat.module.system.dal.dataobject.dict.DictDataDO;
|
||||||
import com.zt.plat.module.system.dal.dataobject.dict.DictTypeDO;
|
import com.zt.plat.module.system.dal.dataobject.dict.DictTypeDO;
|
||||||
import com.zt.plat.module.system.dal.mysql.dict.DictTypeMapper;
|
import com.zt.plat.module.system.dal.mysql.dict.DictTypeMapper;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
|
import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
|
||||||
@@ -24,6 +41,7 @@ import static com.zt.plat.module.system.enums.ErrorCodeConstants.*;
|
|||||||
* @author ZT
|
* @author ZT
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
|
@Slf4j
|
||||||
public class DictTypeServiceImpl implements DictTypeService {
|
public class DictTypeServiceImpl implements DictTypeService {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@@ -92,6 +110,128 @@ public class DictTypeServiceImpl implements DictTypeService {
|
|||||||
return dictTypeMapper.selectList();
|
return dictTypeMapper.selectList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public DictImportRespVO importDictList(List<DictImportExcelVO> importList) {
|
||||||
|
if (CollUtil.isEmpty(importList)) {
|
||||||
|
throw exception(DICT_IMPORT_LIST_IS_EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, DictTypeDO> typeByType = new HashMap<>();
|
||||||
|
Map<String, DictTypeDO> typeByName = new HashMap<>();
|
||||||
|
List<DictTypeDO> existingTypes = dictTypeMapper.selectList();
|
||||||
|
existingTypes.forEach(item -> {
|
||||||
|
typeByType.put(item.getType(), item);
|
||||||
|
typeByName.put(item.getName(), item);
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String> createTypeNames = new ArrayList<>();
|
||||||
|
Map<String, String> failureTypeNames = new LinkedHashMap<>();
|
||||||
|
List<String> createDataKeys = new ArrayList<>();
|
||||||
|
Map<String, String> failureDataKeys = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
Map<String, Map<String, DictDataDO>> dataCacheByType = new HashMap<>();
|
||||||
|
Set<String> seenRowKeys = new HashSet<>();
|
||||||
|
|
||||||
|
for (DictImportExcelVO row : importList) {
|
||||||
|
String dictTypeName = trimToNull(row.getDictTypeName());
|
||||||
|
String dictTypeCode = trimToNull(row.getDictType());
|
||||||
|
String label = trimToNull(row.getLabel());
|
||||||
|
String value = trimToNull(row.getValue());
|
||||||
|
String displayTypeKey = StrUtil.nullToDefault(dictTypeName, StrUtil.nullToDefault(dictTypeCode, "未知字典"));
|
||||||
|
String displayDataKey = String.format("%s-%s", displayTypeKey, StrUtil.nullToDefault(label, "未知标签"));
|
||||||
|
|
||||||
|
if (StrUtil.isEmpty(dictTypeName) || StrUtil.isEmpty(dictTypeCode)) {
|
||||||
|
failureTypeNames.put(displayTypeKey, "字典名称与字典类型均不能为空");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (StrUtil.isEmpty(label) || StrUtil.isEmpty(value)) {
|
||||||
|
failureDataKeys.put(displayDataKey, "字典标签和值不能为空");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String rowKey = dictTypeCode.toLowerCase(Locale.ROOT) + "::" + value.toLowerCase(Locale.ROOT);
|
||||||
|
if (!seenRowKeys.add(rowKey)) {
|
||||||
|
failureDataKeys.put(displayDataKey, "Excel 中存在重复的字典数据");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DictTypeDO dictType = typeByType.get(dictTypeCode);
|
||||||
|
if (dictType == null) {
|
||||||
|
dictType = typeByName.get(dictTypeName);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (dictType == null) {
|
||||||
|
DictTypeSaveReqVO typeReq = new DictTypeSaveReqVO();
|
||||||
|
typeReq.setName(dictTypeName);
|
||||||
|
typeReq.setType(dictTypeCode);
|
||||||
|
typeReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||||
|
typeReq.setRemark(trimToNull(row.getDictTypeRemark()));
|
||||||
|
Long id = createDictType(typeReq);
|
||||||
|
dictType = dictTypeMapper.selectById(id);
|
||||||
|
if (dictType == null) {
|
||||||
|
dictType = new DictTypeDO();
|
||||||
|
dictType.setId(id);
|
||||||
|
dictType.setName(typeReq.getName());
|
||||||
|
dictType.setType(typeReq.getType());
|
||||||
|
dictType.setStatus(typeReq.getStatus());
|
||||||
|
dictType.setRemark(typeReq.getRemark());
|
||||||
|
}
|
||||||
|
typeByType.put(dictType.getType(), dictType);
|
||||||
|
typeByName.put(dictType.getName(), dictType);
|
||||||
|
createTypeNames.add(dictType.getName());
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
String message = ex.getMessage();
|
||||||
|
failureTypeNames.put(displayTypeKey, StrUtil.blankToDefault(message, "导入失败"));
|
||||||
|
log.warn("Import dict type failed, key={}, message=", displayTypeKey, ex);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, DictDataDO> cache = dataCacheByType.computeIfAbsent(dictType.getType(),
|
||||||
|
key -> CollectionUtils.convertMap(dictDataService.getDictDataListByDictType(key), DictDataDO::getValue));
|
||||||
|
DictDataDO existsData = cache.get(value);
|
||||||
|
if (existsData != null) {
|
||||||
|
failureDataKeys.put(displayDataKey, "字典数据已存在,不允许重复导入");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DictDataSaveReqVO dataReq = new DictDataSaveReqVO();
|
||||||
|
dataReq.setDictType(dictType.getType());
|
||||||
|
dataReq.setLabel(label);
|
||||||
|
dataReq.setValue(value);
|
||||||
|
dataReq.setSort(ObjectUtil.defaultIfNull(row.getSort(), 0));
|
||||||
|
dataReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||||
|
dataReq.setColorType(trimToNull(row.getColorType()));
|
||||||
|
dataReq.setCssClass(trimToNull(row.getCssClass()));
|
||||||
|
dataReq.setRemark(trimToNull(row.getDataRemark()));
|
||||||
|
Long dataId = dictDataService.createDictData(dataReq);
|
||||||
|
DictDataDO created = dictDataService.getDictData(dataId);
|
||||||
|
if (created == null) {
|
||||||
|
created = new DictDataDO();
|
||||||
|
created.setId(dataId);
|
||||||
|
created.setDictType(dataReq.getDictType());
|
||||||
|
created.setLabel(dataReq.getLabel());
|
||||||
|
created.setValue(dataReq.getValue());
|
||||||
|
}
|
||||||
|
cache.put(created.getValue(), created);
|
||||||
|
createDataKeys.add(displayDataKey);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
String message = ex.getMessage();
|
||||||
|
failureDataKeys.put(displayDataKey, StrUtil.blankToDefault(message, "导入失败"));
|
||||||
|
log.warn("Import dict data failed, key={}, message=", displayDataKey, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DictImportRespVO.builder()
|
||||||
|
.createDictTypeNames(createTypeNames)
|
||||||
|
.updateDictTypeNames(List.of())
|
||||||
|
.failureDictTypeNames(failureTypeNames)
|
||||||
|
.createDictDataKeys(createDataKeys)
|
||||||
|
.updateDictDataKeys(List.of())
|
||||||
|
.failureDictDataKeys(failureDataKeys)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void validateDictTypeNameUnique(Long id, String name) {
|
void validateDictTypeNameUnique(Long id, String name) {
|
||||||
DictTypeDO dictType = dictTypeMapper.selectByName(name);
|
DictTypeDO dictType = dictTypeMapper.selectByName(name);
|
||||||
@@ -137,4 +277,12 @@ public class DictTypeServiceImpl implements DictTypeService {
|
|||||||
return dictType;
|
return dictType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String trimToNull(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String trim = StrUtil.trim(value);
|
||||||
|
return StrUtil.isEmpty(trim) ? null : trim;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user