mirror of
http://git.pig4cloud.com/pig/pigx.git
synced 2026-11-08 04:39:00 +08:00
feat: add new models and update application structure for job scheduling
This commit is contained in:
1 parent
124a2547dd
commit
5eff0d71df
54 files changed
+1215
-420
No files matched your search
@@ -29,8 +29,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@SpringBootApplication
|
||||
public class PigxBootApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(PigxBootApplication.class, args);
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(PigxBootApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,11 +12,10 @@ spring:
|
||||
password: root
|
||||
url: jdbc:mysql://pigx-mysql:3306/pigxx_boot?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
|
||||
|
||||
|
||||
# 文件上传配置
|
||||
file:
|
||||
type: local
|
||||
local:
|
||||
enable: true
|
||||
base-path: /Users/lengleng/Downloads/img
|
||||
|
||||
# 验证码配置
|
||||
@@ -36,4 +35,3 @@ jasypt:
|
||||
# swagger token url 配置
|
||||
swagger:
|
||||
token-url: ${swagger.gateway}/admin/oauth2/token
|
||||
|
||||
@@ -2,8 +2,6 @@ server:
|
||||
port: 9999
|
||||
servlet:
|
||||
context-path: /admin
|
||||
undertow:
|
||||
max-http-post-size: 100MB
|
||||
|
||||
spring:
|
||||
application:
|
||||
@@ -14,8 +12,8 @@ spring:
|
||||
# 上传文件大小限制
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: ${server.undertow.max-http-post-size}
|
||||
max-request-size: ${server.undertow.max-http-post-size}
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
cloud:
|
||||
discovery:
|
||||
enabled: false
|
||||
@@ -34,6 +32,8 @@ spring:
|
||||
request-context-attribute: request
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
jackson:
|
||||
use-jackson2-defaults: true
|
||||
profiles:
|
||||
active: dev
|
||||
|
||||
@@ -57,6 +57,7 @@ security:
|
||||
- /code/**
|
||||
|
||||
#--------------如下配置尽量不要变动-------------
|
||||
management.endpoints.access.default: none # 默认所有端点都不允许访问
|
||||
# mybatis-plus 配置
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:/mapper/*Mapper.xml
|
||||
@@ -73,8 +74,10 @@ mybatis-plus:
|
||||
call-setters-on-nulls: true
|
||||
shrink-whitespaces-in-sql: true
|
||||
# 查询以下前缀的表,不输出日志
|
||||
pigx:
|
||||
mybatis:
|
||||
skip-table:
|
||||
- QRTZ_ #quartz 定时任务的表
|
||||
- ACT_ #工作流相关的表
|
||||
pigx.mybatis.skip-table:
|
||||
- QRTZ_ #quartz 定时任务的表
|
||||
- ACT_ #工作流相关的表
|
||||
|
||||
# flowable 配置
|
||||
flowable:
|
||||
variable-json-mapper: jackson2
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
~ Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
~
|
||||
~ Redistribution and use in source and binary forms, with or without
|
||||
~ modification, are permitted provided that the following conditions are met:
|
||||
@@ -85,6 +85,19 @@
|
||||
<appender-ref ref="error"/>
|
||||
</logger>
|
||||
|
||||
<!-- magic-api 日志屏蔽 -->
|
||||
<logger name="org.ssssssss.magicapi" level="OFF">
|
||||
<appender-ref ref="error"/>
|
||||
</logger>
|
||||
|
||||
<!-- 关闭PDF识别 -->
|
||||
<logger name="org.apache.pdfbox.pdmodel.font" level="OFF">
|
||||
</logger>
|
||||
|
||||
<logger name="dev.ai4j" level="DEBUG">
|
||||
<appender-ref ref="debug"/>
|
||||
</logger>
|
||||
|
||||
<!-- Level: FATAL 0 ERROR 3 WARN 4 INFO 6 DEBUG 7 -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="console"/>
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -69,7 +69,7 @@ public class DynamicRouteAutoConfiguration {
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
public DynamicRouteHealthIndicator healthIndicator(RedisTemplate redisTemplate) {
|
||||
public DynamicRouteHealthIndicator dynamicRouteHealthIndicator(RedisTemplate redisTemplate) {
|
||||
|
||||
redisTemplate.setKeySerializer(new StringRedisSerializer());
|
||||
if (!redisTemplate.hasKey(CacheConstants.ROUTE_KEY)) {
|
||||
|
||||
+26
-26
@@ -1,12 +1,12 @@
|
||||
package com.pig4cloud.pigx.common.gateway.configuration;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
|
||||
import org.springframework.boot.webflux.error.ErrorWebExceptionHandler;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
@@ -29,33 +29,33 @@ import reactor.core.publisher.Mono;
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
|
||||
public class GlobalExceptionConfiguration implements ErrorWebExceptionHandler {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
@Override
|
||||
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
|
||||
if (response.isCommitted()) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
if (response.isCommitted()) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
|
||||
// header set
|
||||
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
if (ex instanceof ResponseStatusException) {
|
||||
response.setStatusCode(((ResponseStatusException) ex).getStatusCode());
|
||||
}
|
||||
// header set
|
||||
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
if (ex instanceof ResponseStatusException) {
|
||||
response.setStatusCode(((ResponseStatusException) ex).getStatusCode());
|
||||
}
|
||||
|
||||
return response.writeWith(Mono.fromSupplier(() -> {
|
||||
DataBufferFactory bufferFactory = response.bufferFactory();
|
||||
try {
|
||||
log.error("Error request :{} Error Spring Cloud Gateway : {}", exchange.getRequest().getPath(),
|
||||
ex.getMessage());
|
||||
return bufferFactory.wrap(objectMapper.writeValueAsBytes(R.failed(ex.getMessage())));
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("Error writing response", ex);
|
||||
return bufferFactory.wrap(new byte[0]);
|
||||
}
|
||||
}));
|
||||
}
|
||||
return response.writeWith(Mono.fromSupplier(() -> {
|
||||
DataBufferFactory bufferFactory = response.bufferFactory();
|
||||
try {
|
||||
log.error("Error request :{} Error Spring Cloud Gateway : {}", exchange.getRequest().getPath(),
|
||||
ex.getMessage());
|
||||
return bufferFactory.wrap(objectMapper.writeValueAsBytes(R.failed(ex.getMessage())));
|
||||
} catch (JacksonException e) {
|
||||
log.warn("Error writing response", ex);
|
||||
return bufferFactory.wrap(new byte[0]);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -65,7 +65,7 @@ public class VersionGrayLoadBalancer implements GrayLoadBalancer {
|
||||
// 过滤出不含VERSION实例
|
||||
List<ServiceInstance> versionInstanceList = instances.stream()
|
||||
.filter(instance -> !instance.getMetadata().containsKey(CommonConstants.VERSION))
|
||||
.toList();
|
||||
.toList();
|
||||
if (CollUtil.isEmpty(versionInstanceList)) {
|
||||
// 根据权重获取实例
|
||||
return randomByWeight(instances);
|
||||
@@ -77,7 +77,7 @@ public class VersionGrayLoadBalancer implements GrayLoadBalancer {
|
||||
List<ServiceInstance> availableList = instances.stream()
|
||||
.filter(instance -> reqVersion
|
||||
.equalsIgnoreCase(MapUtil.getStr(instance.getMetadata(), CommonConstants.VERSION)))
|
||||
.toList();
|
||||
.toList();
|
||||
if (CollUtil.isEmpty(availableList)) {
|
||||
// 根据权重获取实例
|
||||
return randomByWeight(instances);
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ package com.pig4cloud.pigx.common.gateway.support;
|
||||
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.health.contributor.AbstractHealthIndicator;
|
||||
import org.springframework.boot.health.contributor.Health;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
|
||||
/**
|
||||
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -27,7 +27,6 @@ import org.springframework.cloud.gateway.route.RouteDefinition;
|
||||
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
|
||||
import org.springframework.data.redis.core.HashOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -43,6 +42,7 @@ import java.util.List;
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@SuppressWarnings("removal")
|
||||
public class RedisRouteDefinitionWriter implements RouteDefinitionRepository {
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
@@ -83,7 +83,8 @@ public class RedisRouteDefinitionWriter implements RouteDefinitionRepository {
|
||||
return Flux.fromIterable(routeList);
|
||||
}
|
||||
|
||||
redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(RouteDefinitionVo.class));
|
||||
redisTemplate.setHashValueSerializer(
|
||||
new org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer<>(RouteDefinitionVo.class));
|
||||
HashOperations<String, String, RouteDefinitionVo> stringObjectObjectHashOperations = redisTemplate.opsForHash();
|
||||
List<RouteDefinitionVo> values = stringObjectObjectHashOperations.values(CacheConstants.ROUTE_KEY);
|
||||
log.info("redis 中路由定义条数: {}, {}", values.size(), values);
|
||||
|
||||
+2
-2
@@ -77,7 +77,7 @@ public class GrayRoundRobinLoadBalancer extends RoundRobinLoadBalancer {
|
||||
// 过滤出不含VERSION实例
|
||||
List<ServiceInstance> versionInstanceList = instances.stream()
|
||||
.filter(instance -> !instance.getMetadata().containsKey(CommonConstants.VERSION))
|
||||
.toList();
|
||||
.toList();
|
||||
if (CollUtil.isEmpty(versionInstanceList)) {
|
||||
// 根据权重获取实例
|
||||
return new DefaultResponse(randomByWeight(instances));
|
||||
@@ -92,7 +92,7 @@ public class GrayRoundRobinLoadBalancer extends RoundRobinLoadBalancer {
|
||||
Map<String, String> metadata = nacosInstance.getMetadata();
|
||||
String targetVersion = MapUtil.getStr(metadata, CommonConstants.VERSION);
|
||||
return reqVersion.equalsIgnoreCase(targetVersion);
|
||||
}).toList();
|
||||
}).toList();
|
||||
|
||||
// 存在 随机返回
|
||||
if (CollUtil.isNotEmpty(serviceInstanceList)) {
|
||||
|
||||
+34
-3
@@ -4,11 +4,17 @@ import com.pig4cloud.pigx.common.idempotent.aspect.IdempotentAspect;
|
||||
import com.pig4cloud.pigx.common.idempotent.expression.ExpressionResolver;
|
||||
import com.pig4cloud.pigx.common.idempotent.expression.KeyResolver;
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.config.Config;
|
||||
import org.redisson.config.SingleServerConfig;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration;
|
||||
import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
@@ -17,7 +23,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
* 幂等插件初始化
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@AutoConfigureAfter(RedisAutoConfiguration.class)
|
||||
@AutoConfigureAfter(DataRedisAutoConfiguration.class)
|
||||
@EnableConfigurationProperties(DataRedisProperties.class)
|
||||
public class IdempotentAutoConfiguration {
|
||||
|
||||
|
||||
@@ -40,7 +47,31 @@ public class IdempotentAutoConfiguration {
|
||||
* @return {@link IdempotentAspect }
|
||||
*/
|
||||
@Bean
|
||||
public IdempotentAspect idempotentAspect(Redisson redisson, KeyResolver keyResolver) {
|
||||
@ConditionalOnMissingBean(RedissonClient.class)
|
||||
public RedissonClient redissonClient(DataRedisProperties redisProperties) {
|
||||
Config config = new Config();
|
||||
SingleServerConfig singleServerConfig = config.useSingleServer()
|
||||
.setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort())
|
||||
.setDatabase(redisProperties.getDatabase());
|
||||
|
||||
if (StringUtils.hasText(redisProperties.getUsername())) {
|
||||
singleServerConfig.setUsername(redisProperties.getUsername());
|
||||
}
|
||||
if (StringUtils.hasText(redisProperties.getPassword())) {
|
||||
singleServerConfig.setPassword(redisProperties.getPassword());
|
||||
}
|
||||
if (redisProperties.getConnectTimeout() != null) {
|
||||
singleServerConfig.setConnectTimeout((int) redisProperties.getConnectTimeout().toMillis());
|
||||
}
|
||||
if (redisProperties.getTimeout() != null) {
|
||||
singleServerConfig.setTimeout((int) redisProperties.getTimeout().toMillis());
|
||||
}
|
||||
|
||||
return Redisson.create(config);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IdempotentAspect idempotentAspect(RedissonClient redisson, KeyResolver keyResolver) {
|
||||
return new IdempotentAspect(redisson, keyResolver);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RMapCache;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -60,7 +60,7 @@ public class IdempotentAspect {
|
||||
/** 是否自动删除 Key 标识 */
|
||||
private static final String DELKEY = "delKey";
|
||||
|
||||
private final Redisson redisson;
|
||||
private final RedissonClient redisson;
|
||||
|
||||
private final KeyResolver keyResolver;
|
||||
|
||||
|
||||
+1
-3
@@ -1,7 +1,7 @@
|
||||
package com.pig4cloud.pigx.common.log.init;
|
||||
|
||||
import org.springframework.boot.EnvironmentPostProcessor;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.env.EnvironmentPostProcessor;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
@@ -22,8 +22,6 @@ public class ApplicationLoggerInitializer implements EnvironmentPostProcessor, O
|
||||
System.setProperty("logging.file.name", String.format("%s/%s/debug.log", logBase, appName));
|
||||
// 避免 sentinel 1.8.4+ 心跳日志过大
|
||||
System.setProperty("csp.sentinel.log.level", "OFF");
|
||||
// 避免 sentinel 健康检查 server
|
||||
System.setProperty("management.health.sentinel.enabled", "false");
|
||||
// 避免各种依赖的地方组件造成 BeanPostProcessorChecker 警告
|
||||
System.setProperty("logging.level.org.springframework.context.support.PostProcessorRegistrationDelegate",
|
||||
"ERROR");
|
||||
|
||||
+19
-9
@@ -1,7 +1,9 @@
|
||||
package com.pig4cloud.pigx.common.log.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFilter;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import com.fasterxml.jackson.databind.ser.FilterProvider;
|
||||
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
|
||||
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
|
||||
@@ -10,6 +12,12 @@ import lombok.SneakyThrows;
|
||||
|
||||
/**
|
||||
* Jackson 脱敏工具类
|
||||
* <p>
|
||||
* 状态字段 {@code objectMapper} 通过 read-copy-write 模式更新,{@link #configureSensitiveFields}
|
||||
* 与 {@link #registerCustomModule} 都需要在并发调用下保证修改不丢失,因此设为 {@code synchronized}。
|
||||
* 读取路径({@link #readValue} 等)依赖 {@code volatile} 保证可见性,无需加锁。
|
||||
* <p>
|
||||
* 注:{@link ObjectMapper#copy()} 是 Jackson 2 才支持的能力,这里有意保留 Jackson 2 依赖。
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2024/07/18
|
||||
@@ -17,20 +25,22 @@ import lombok.SneakyThrows;
|
||||
public class JacksonSensitiveFieldUtil {
|
||||
|
||||
@Getter
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private static volatile ObjectMapper objectMapper = JsonMapper.builder()
|
||||
.addMixIn(Object.class, PropertyFilterMixIn.class)
|
||||
.build();
|
||||
|
||||
static {
|
||||
objectMapper.addMixIn(Object.class, PropertyFilterMixIn.class);
|
||||
}
|
||||
|
||||
public static void configureSensitiveFields(String[] ignorableFieldNames) {
|
||||
public static synchronized void configureSensitiveFields(String[] ignorableFieldNames) {
|
||||
FilterProvider filters = new SimpleFilterProvider().addFilter("filter properties by name",
|
||||
SimpleBeanPropertyFilter.serializeAllExcept(ignorableFieldNames));
|
||||
objectMapper.setFilterProvider(filters);
|
||||
ObjectMapper mapper = objectMapper.copy();
|
||||
mapper.setFilterProvider(filters);
|
||||
objectMapper = mapper;
|
||||
}
|
||||
|
||||
public static void registerCustomModule(com.fasterxml.jackson.databind.Module module) {
|
||||
objectMapper.registerModule(module);
|
||||
public static synchronized void registerCustomModule(Module module) {
|
||||
ObjectMapper mapper = objectMapper.copy();
|
||||
mapper.registerModule(module);
|
||||
objectMapper = mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
||||
org.springframework.boot.EnvironmentPostProcessor=\
|
||||
com.pig4cloud.pigx.common.log.init.ApplicationLoggerInitializer
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -77,8 +77,7 @@ public class SensitiveSerialize extends JsonSerializer<String> implements Contex
|
||||
sensitive = beanProperty.getContextAnnotation(Sensitive.class);
|
||||
}
|
||||
if (sensitive != null) {
|
||||
this.sensitive = sensitive;
|
||||
return this;
|
||||
return new SensitiveSerialize(sensitive);
|
||||
}
|
||||
}
|
||||
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ public class SensitiveWordsProperties {
|
||||
/**
|
||||
* The sensitive words to detect.
|
||||
*/
|
||||
private String mask = "no_mask";
|
||||
private String mask = "*";
|
||||
|
||||
/**
|
||||
* Whether the sensitive words detection is enabled.
|
||||
|
||||
+2
-1
@@ -19,6 +19,7 @@ import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支持自动降级注入 重写 {@link com.alibaba.cloud.sentinel.feign.SentinelFeign}
|
||||
*
|
||||
@@ -90,7 +91,7 @@ public final class PigxSentinelFeign {
|
||||
Object fallbackInstance = feignClientFactory.getInstance(name, fallbackType);
|
||||
if (fallbackInstance == null) {
|
||||
throw new IllegalStateException(String
|
||||
.format("No %s instance of type %s found for feign client %s", type, fallbackType, name));
|
||||
.format("No %s instance of type %s found for feign client %s", type, fallbackType, name));
|
||||
}
|
||||
|
||||
if (!targetType.isAssignableFrom(fallbackType)) {
|
||||
|
||||
+286
-93
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -17,130 +17,323 @@
|
||||
|
||||
package com.pig4cloud.pigx.common.sentinel.handle;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.csp.sentinel.Tracer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import feign.FeignException;
|
||||
import lombok.SneakyThrows;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.method.ParameterValidationResult;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
* <p>
|
||||
* 适配 Spring Boot 4.0+/Spring Framework 7.0+,统一拦截 Web、参数校验、Feign、IO 等异常。
|
||||
*
|
||||
* @author lengleng
|
||||
* @date 2020-06-29
|
||||
* @date 2026-05-18
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class GlobalBizExceptionHandler {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
/**
|
||||
* JSON 序列化工具,用于解析 Feign 透传的远端 R 响应体。
|
||||
*/
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 全局异常.
|
||||
*
|
||||
* @param e the e
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleGlobalException(Exception e) {
|
||||
/**
|
||||
* 全局兜底异常
|
||||
* @param e 未被更具体处理器捕获的异常
|
||||
* @return 统一失败响应,消息来自异常本地化描述
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleGlobalException(Exception e) {
|
||||
log.error("全局异常信息 ex={}", e.getMessage(), e);
|
||||
// 业务异常交由 sentinel 记录
|
||||
Tracer.trace(e);
|
||||
return R.failed(e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
log.error("全局异常信息 ex={}", e.getMessage(), e);
|
||||
/**
|
||||
* 客户端断开导致的异步响应不可用异常,无需返回任何内容
|
||||
* <p>
|
||||
* Spring Boot 3.2+ 在 SSE / 长连接场景客户端主动断开时抛出,需静默处理避免污染日志
|
||||
* @param e 异步请求不可用异常
|
||||
*/
|
||||
@ExceptionHandler(AsyncRequestNotUsableException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public void handleAsyncRequestNotUsableException(AsyncRequestNotUsableException e) {
|
||||
log.debug("异步请求已不可用 ex={}", e.getMessage());
|
||||
}
|
||||
|
||||
// 业务异常交由 sentinel 记录
|
||||
Tracer.trace(e);
|
||||
return R.failed(e.getLocalizedMessage());
|
||||
}
|
||||
/**
|
||||
* 数据库异常
|
||||
* @param exception 数据库调用异常
|
||||
* @return 统一失败响应,不向客户端暴露底层 SQL 细节
|
||||
*/
|
||||
@ExceptionHandler(SQLException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleSQLException(SQLException exception) {
|
||||
log.error("数据库调用异常 ex={}", exception.getMessage(), exception);
|
||||
return R.failed("数据库调用异常,请联系管理员处理");
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLException Exception
|
||||
*
|
||||
* @param exception 数据库调用异常
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({SQLException.class})
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleSQLException(SQLException exception) {
|
||||
log.error("数据库调用异常 ex={}", exception.getMessage(), exception);
|
||||
return R.failed("数据库调用异常,请联系管理员处理");
|
||||
}
|
||||
/**
|
||||
* Feign 远程调用异常,透传远端 R 响应体
|
||||
* @param e Feign 调用异常
|
||||
* @return 远端 R 响应体或本地统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(FeignException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleFeignException(FeignException e) {
|
||||
log.error("Feign 调用异常 ex={}", e.getMessage(), e);
|
||||
Tracer.trace(e);
|
||||
|
||||
@SneakyThrows
|
||||
@ExceptionHandler(FeignException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleGlobalException(FeignException e) {
|
||||
log.error("全局异常信息 ex={}", e.getMessage(), e);
|
||||
if (e.responseBody().isPresent()) {
|
||||
try {
|
||||
ByteBuffer responseBody = e.responseBody().get();
|
||||
byte[] body = new byte[responseBody.remaining()];
|
||||
responseBody.get(body);
|
||||
return objectMapper.readValue(body, R.class);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
log.warn("Feign 响应体反序列化失败 ex={}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
return R.failed(e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
// 业务异常交由 sentinel 记录
|
||||
Tracer.trace(e);
|
||||
/**
|
||||
* 拒绝授权异常
|
||||
* @param e Spring Security 访问拒绝异常
|
||||
* @return 权限不足的统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public R handleAccessDeniedException(AccessDeniedException e) {
|
||||
log.error("拒绝授权异常信息 ex={}", e.getMessage());
|
||||
return R.failed("权限不足,不允许访问");
|
||||
}
|
||||
|
||||
if (e.responseBody().isPresent()) {
|
||||
return objectMapper.readValue(e.responseBody().get().array(), R.class);
|
||||
}
|
||||
/**
|
||||
* 表单参数绑定校验异常(@ModelAttribute / 表单提交)
|
||||
* @param exception 表单参数绑定异常
|
||||
* @return 字段校验失败的统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleBindException(BindException exception) {
|
||||
log.warn("参数绑定异常 ex={}", exception.getMessage());
|
||||
return R.failed(buildFieldErrorMessage(exception.getBindingResult().getFieldErrors()));
|
||||
}
|
||||
|
||||
return R.failed(e.getLocalizedMessage());
|
||||
}
|
||||
/**
|
||||
* @RequestBody + @Valid 校验异常
|
||||
* @param exception 请求体参数校验异常
|
||||
* @return 字段校验失败的统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
|
||||
log.warn("请求体参数校验异常 ex={}", exception.getMessage());
|
||||
return R.failed(buildFieldErrorMessage(exception.getBindingResult().getFieldErrors()));
|
||||
}
|
||||
|
||||
/**
|
||||
* AccessDeniedException
|
||||
*
|
||||
* @param e the e
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public R handleAccessDeniedException(AccessDeniedException e) {
|
||||
log.error("拒绝授权异常信息 ex={}", e.getMessage());
|
||||
return R.failed("权限不足,不允许访问");
|
||||
}
|
||||
/**
|
||||
* Spring 7.0+ 方法级参数校验异常(@Validated 标注在 Controller 上的 @RequestParam / @PathVariable 校验)
|
||||
* @param exception 方法参数校验异常
|
||||
* @return 方法参数校验失败的统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(HandlerMethodValidationException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleHandlerMethodValidationException(HandlerMethodValidationException exception) {
|
||||
log.warn("方法参数校验异常 ex={}", exception.getMessage());
|
||||
String parameterMessage = exception.getParameterValidationResults()
|
||||
.stream()
|
||||
.map(ParameterValidationResult::getResolvableErrors)
|
||||
.flatMap(List::stream)
|
||||
.map(error -> error.getDefaultMessage() == null ? StrUtil.EMPTY : error.getDefaultMessage())
|
||||
.collect(Collectors.joining(StrUtil.COMMA));
|
||||
String crossParameterMessage = buildResolvableErrorMessage(exception.getCrossParameterValidationResults());
|
||||
String message = joinErrorMessages(parameterMessage, crossParameterMessage);
|
||||
return R.failed(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* validation Exception
|
||||
*
|
||||
* @param exception
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({BindException.class})
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleBodyValidException(BindException exception) {
|
||||
List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
|
||||
// 插入log 的逻辑
|
||||
return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
|
||||
}
|
||||
/**
|
||||
* @Validated 在 Service 层方法或单参数校验失败
|
||||
* @param exception 约束校验异常
|
||||
* @return 约束校验失败的统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleConstraintViolationException(ConstraintViolationException exception) {
|
||||
log.warn("约束校验异常 ex={}", exception.getMessage());
|
||||
String message = exception.getConstraintViolations()
|
||||
.stream()
|
||||
.map(ConstraintViolation::getMessage)
|
||||
.collect(Collectors.joining(StrUtil.COMMA));
|
||||
return R.failed(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 避免 404 重定向到 /error 导致NPE ,ignore-url 需要配置对应端点
|
||||
*
|
||||
* @return R
|
||||
*/
|
||||
@DeleteMapping("/error")
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public R noHandlerFoundException() {
|
||||
return R.failed(HttpStatus.NOT_FOUND.getReasonPhrase());
|
||||
}
|
||||
/**
|
||||
* 请求体解析异常(JSON 格式错误 / 类型不匹配)
|
||||
* @param exception 请求体解析异常
|
||||
* @return 请求体格式错误的统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleHttpMessageNotReadableException(HttpMessageNotReadableException exception) {
|
||||
log.warn("请求体解析异常 ex={}", exception.getMessage());
|
||||
return R.failed("请求体格式错误,无法解析");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保持和低版本请求路径不存在的行为一致
|
||||
* <p>
|
||||
* <a href="https://github.com/spring-projects/spring-boot/issues/38733">[Spring Boot
|
||||
* 3.2.0] 404 Not Found behavior #38733</a>
|
||||
*
|
||||
* @param exception
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({NoResourceFoundException.class})
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public R noResourceFoundException(NoResourceFoundException exception) {
|
||||
log.debug("请求路径 404 {}", exception.getMessage());
|
||||
return R.failed(exception.getMessage());
|
||||
}
|
||||
/**
|
||||
* 缺少必填请求参数
|
||||
* @param exception 缺少请求参数异常
|
||||
* @return 标明缺失参数名的统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleMissingServletRequestParameterException(MissingServletRequestParameterException exception) {
|
||||
log.warn("缺少必填参数 ex={}", exception.getMessage());
|
||||
return R.failed(StrUtil.format("缺少必填参数: {}", exception.getParameterName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求参数类型转换异常
|
||||
* @param exception 方法参数类型不匹配异常
|
||||
* @return 标明参数名的统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException exception) {
|
||||
log.warn("参数类型不匹配 ex={}", exception.getMessage());
|
||||
return R.failed(StrUtil.format("参数 {} 类型不正确", exception.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求方法不支持
|
||||
* @param exception 请求方法不支持异常
|
||||
* @return 请求方法不支持的统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
public R handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException exception) {
|
||||
log.warn("请求方法不支持 ex={}", exception.getMessage());
|
||||
return R.failed(exception.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Content-Type 不支持
|
||||
* @param exception 媒体类型不支持异常
|
||||
* @return 媒体类型不支持的统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
|
||||
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||
public R handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException exception) {
|
||||
log.warn("媒体类型不支持 ex={}", exception.getMessage());
|
||||
return R.failed(exception.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传超过大小限制
|
||||
* @param exception 文件大小超过限制异常
|
||||
* @return 文件大小超过限制的统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
@ResponseStatus(HttpStatus.CONTENT_TOO_LARGE)
|
||||
public R handleMaxUploadSizeExceededException(MaxUploadSizeExceededException exception) {
|
||||
log.warn("上传文件超过大小限制 ex={}", exception.getMessage());
|
||||
return R.failed("上传文件大小超过限制");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保持和低版本请求路径不存在的行为一致
|
||||
* <p>
|
||||
* <a href="https://github.com/spring-projects/spring-boot/issues/38733">[Spring Boot
|
||||
* 3.2.0] 404 Not Found behavior #38733</a>
|
||||
* @param exception 静态资源或请求路径不存在异常
|
||||
* @return 404 的统一失败响应
|
||||
*/
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public R handleNoResourceFoundException(NoResourceFoundException exception) {
|
||||
log.debug("请求路径 404 {}", exception.getMessage());
|
||||
return R.failed(exception.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造字段校验错误信息
|
||||
* @param fieldErrors 字段校验错误列表,空集合返回空字符串
|
||||
* @return 拼接后的字段校验错误信息
|
||||
*/
|
||||
private String buildFieldErrorMessage(List<FieldError> fieldErrors) {
|
||||
if (fieldErrors == null || fieldErrors.isEmpty()) {
|
||||
return StrUtil.EMPTY;
|
||||
}
|
||||
return fieldErrors.stream()
|
||||
.map(error -> StrUtil.format("{} {}", error.getField(), error.getDefaultMessage()))
|
||||
.collect(Collectors.joining(StrUtil.COMMA));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 Spring 参数校验错误信息
|
||||
* @param errors 参数校验错误,空集合返回空字符串
|
||||
* @return 拼接后的错误信息
|
||||
*/
|
||||
private String buildResolvableErrorMessage(List<? extends MessageSourceResolvable> errors) {
|
||||
if (errors == null || errors.isEmpty()) {
|
||||
return StrUtil.EMPTY;
|
||||
}
|
||||
return errors.stream()
|
||||
.map(error -> error.getDefaultMessage() == null ? StrUtil.EMPTY : error.getDefaultMessage())
|
||||
.collect(Collectors.joining(StrUtil.COMMA));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接非空错误信息片段
|
||||
* @param first 第一段错误信息,可为空
|
||||
* @param second 第二段错误信息,可为空
|
||||
* @return 拼接后的错误信息,全部为空时返回空字符串
|
||||
*/
|
||||
private String joinErrorMessages(String first, String second) {
|
||||
if (StrUtil.isBlank(first)) {
|
||||
return StrUtil.blankToDefault(second, StrUtil.EMPTY);
|
||||
}
|
||||
if (StrUtil.isBlank(second)) {
|
||||
return first;
|
||||
}
|
||||
return StrUtil.join(StrUtil.COMMA, first, second);
|
||||
}
|
||||
|
||||
}
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -26,8 +26,8 @@ import org.anyline.metadata.Index;
|
||||
import org.anyline.metadata.Table;
|
||||
import org.anyline.proxy.ServiceProxy;
|
||||
import org.anyline.service.AnylineService;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.context.WebServerInitializedEvent;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
|
||||
@@ -49,7 +49,7 @@ public class SequenceAutoConfiguration {
|
||||
/**
|
||||
* 启动后自动创建表
|
||||
*/
|
||||
@EventListener({WebServerInitializedEvent.class})
|
||||
@EventListener({ApplicationReadyEvent.class})
|
||||
public void onApplicationReady() throws Exception {
|
||||
// 判断是否需要创建表
|
||||
AnylineService service = ServiceProxy.service();
|
||||
|
||||
+3
-3
@@ -31,10 +31,10 @@ public final class SseEmitterHolder {
|
||||
|
||||
/**
|
||||
* 删除一个 sseEmitter
|
||||
* @param sseEmitter SseEmitter
|
||||
* @param sseEmitter SseEmitter
|
||||
*/
|
||||
public static void removeSseEmitter(SseEmitter sseEmitter) {
|
||||
USER_SSE_EMITTER_MAP.values().remove(sseEmitter);
|
||||
public static void removeSseEmitter(SseEmitter sseEmitter) {
|
||||
USER_SSE_EMITTER_MAP.values().remove(sseEmitter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+31
-30
@@ -21,7 +21,6 @@ import com.pig4cloud.pigx.common.xss.core.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -31,6 +30,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* jackson xss 配置
|
||||
*
|
||||
@@ -40,40 +40,41 @@ import java.util.List;
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(PigxXssProperties.class)
|
||||
@ConditionalOnProperty(prefix = PigxXssProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
@SuppressWarnings("removal")
|
||||
public class PigxXssAutoConfiguration implements WebMvcConfigurer {
|
||||
|
||||
private final PigxXssProperties xssProperties;
|
||||
private final PigxXssProperties xssProperties;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public XssCleaner xssCleaner(PigxXssProperties properties) {
|
||||
return new DefaultXssCleaner(properties);
|
||||
}
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public XssCleaner xssCleaner(PigxXssProperties properties) {
|
||||
return new DefaultXssCleaner(properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public FormXssClean formXssClean(PigxXssProperties properties, XssCleaner xssCleaner) {
|
||||
return new FormXssClean(properties, xssCleaner);
|
||||
}
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public FormXssClean formXssClean(PigxXssProperties properties, XssCleaner xssCleaner) {
|
||||
return new FormXssClean(properties, xssCleaner);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(PigxXssProperties properties,
|
||||
XssCleaner xssCleaner) {
|
||||
JacksonXssClean xssClean = new JacksonXssClean(properties, xssCleaner);
|
||||
return builder -> builder.deserializerByType(String.class, xssClean);
|
||||
}
|
||||
@Bean
|
||||
public org.springframework.boot.jackson2.autoconfigure.Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(
|
||||
PigxXssProperties properties, XssCleaner xssCleaner) {
|
||||
JacksonXssClean xssClean = new JacksonXssClean(properties, xssCleaner);
|
||||
return builder -> builder.deserializerByType(String.class, xssClean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
List<String> patterns = xssProperties.getPathPatterns();
|
||||
if (patterns.isEmpty()) {
|
||||
patterns.add("/**");
|
||||
}
|
||||
XssCleanInterceptor interceptor = new XssCleanInterceptor(xssProperties);
|
||||
registry.addInterceptor(interceptor)
|
||||
.addPathPatterns(patterns)
|
||||
.excludePathPatterns(xssProperties.getPathExcludePatterns())
|
||||
.order(Ordered.LOWEST_PRECEDENCE);
|
||||
}
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
List<String> patterns = xssProperties.getPathPatterns();
|
||||
if (patterns.isEmpty()) {
|
||||
patterns.add("/**");
|
||||
}
|
||||
XssCleanInterceptor interceptor = new XssCleanInterceptor(xssProperties);
|
||||
registry.addInterceptor(interceptor)
|
||||
.addPathPatterns(patterns)
|
||||
.excludePathPatterns(xssProperties.getPathExcludePatterns())
|
||||
.order(Ordered.LOWEST_PRECEDENCE);
|
||||
}
|
||||
|
||||
}
|
||||
+8
-4
@@ -18,10 +18,9 @@ package com.pig4cloud.pigx.common.xss.core;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import com.pig4cloud.pigx.common.xss.config.PigxXssProperties;
|
||||
import com.pig4cloud.pigx.common.xss.utils.XssUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -32,13 +31,18 @@ import java.io.IOException;
|
||||
* @author L.cm
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class JacksonXssClean extends JsonDeserializer<String> {
|
||||
public class JacksonXssClean extends StdDeserializer<String> {
|
||||
|
||||
private final PigxXssProperties properties;
|
||||
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
public JacksonXssClean(PigxXssProperties properties, XssCleaner xssCleaner) {
|
||||
super(String.class);
|
||||
this.properties = properties;
|
||||
this.xssCleaner = xssCleaner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
|
||||
// XSS filter
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
~ Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
~
|
||||
~ Redistribution and use in source and binary forms, with or without
|
||||
~ modification, are permitted provided that the following conditions are met:
|
||||
|
||||
@@ -25,7 +25,7 @@ public class PigxNacosApplication {
|
||||
/**
|
||||
* 独立模式系统属性名称
|
||||
*/
|
||||
private static String STANDALONE_MODE = "nacos.standalone";
|
||||
private static final String STANDALONE_MODE = "nacos.standalone";
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.setProperty(STANDALONE_MODE, "true");
|
||||
@@ -46,12 +46,14 @@ public class PigxNacosApplication {
|
||||
ConfigurableApplicationContext serverWebContext = new SpringApplicationBuilder(NacosServerWebApplication.class)
|
||||
.parent(coreContext)
|
||||
.run(args);
|
||||
System.out.println(serverWebContext);
|
||||
|
||||
// Start Console Context
|
||||
NacosStartUpManager.start(NacosStartUp.CONSOLE_START_UP_PHASE);
|
||||
ConfigurableApplicationContext consoleContext = new SpringApplicationBuilder(NacosConsole.class)
|
||||
.parent(coreContext)
|
||||
.run(args);
|
||||
System.out.println(consoleContext);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
# Nacos \u63A7\u5236\u53F0\u7AEF\u53E3
|
||||
nacos.console.port=18080
|
||||
# Nacos \u63A7\u5236\u53F0\u7AEF\u53E3\uFF0C\u8BF7\u6CE8\u610F\u8BBF\u95EE\u7684 IP:8080
|
||||
nacos.console.port=18080
|
||||
# Nacos \u670D\u52A1\u7AEF\u4E3B\u7AEF\u53E3
|
||||
nacos.server.main.port=8848
|
||||
# \u6570\u636E\u5E93\u8BBE\u7F6E
|
||||
@@ -8,10 +8,8 @@ db.num=1
|
||||
db.url.0=jdbc:mysql://${MYSQL_HOST:pigx-mysql}:${MYSQL_PORT:3306}/${MYSQL_DB:pigxx_config}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
|
||||
db.user=root
|
||||
db.password=root
|
||||
db.pool.config.connectionTimeout=30000
|
||||
db.pool.config.validationTimeout=10000
|
||||
# nacos \u4F1A\u81EA\u52A8\u88C5\u914D\u6570\u636E\u6E90\uFF0C\u6240\u4EE5\u6392\u9664 spring.datasource \u81EA\u52A8\u914D\u7F6E
|
||||
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
|
||||
spring.autoconfigure.exclude=org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration
|
||||
#*************** \u76D1\u63A7\u76F8\u5173\u914D\u7F6E ***************#
|
||||
# Prometheus \u76D1\u63A7\u914D\u7F6E
|
||||
#management.endpoints.web.exposure.include=prometheus
|
||||
@@ -41,7 +39,7 @@ server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D %{User-Agent}i %{Reque
|
||||
server.tomcat.basedir=file:.
|
||||
#*************** API \u9519\u8BEF\u5904\u7406 ***************#
|
||||
# \u9519\u8BEF\u4FE1\u606F\u663E\u793A\u8BBE\u7F6E
|
||||
server.error.include-message=ALWAYS
|
||||
spring.web.error.include-message=ALWAYS
|
||||
#*************** Nacos \u63A7\u5236\u53F0\u914D\u7F6E ***************#
|
||||
# Nacos \u63A7\u5236\u53F0\u4E0A\u4E0B\u6587\u8DEF\u5F84
|
||||
nacos.console.contextPath=
|
||||
@@ -53,7 +51,7 @@ nacos.security.ignore.urls=/,/error,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/
|
||||
# \u8BA4\u8BC1\u7CFB\u7EDF\u7C7B\u578B
|
||||
nacos.core.auth.system.type=nacos
|
||||
# \u5F00\u542F\u8BA4\u8BC1
|
||||
nacos.core.auth.enabled=false
|
||||
nacos.core.auth.enabled=true
|
||||
# \u542F\u7528 API \u8BBF\u95EE\u6743\u9650
|
||||
nacos.core.auth.admin.enabled=true
|
||||
# \u542F\u7528\u63A7\u5236\u53F0 API \u8BA4\u8BC1
|
||||
@@ -68,9 +66,6 @@ nacos.core.auth.plugin.nacos.token.cache.enable=false
|
||||
nacos.core.auth.plugin.nacos.token.expire.seconds=18000
|
||||
# \u8BA4\u8BC1\u5BC6\u94A5 (Base64 \u7F16\u7801)
|
||||
nacos.core.auth.plugin.nacos.token.secret.key=VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg=
|
||||
#*************** Istio \u670D\u52A1\u7F51\u683C\u914D\u7F6E ***************#
|
||||
# MCP \u670D\u52A1\u652F\u6301 - \u5DF2\u7981\u7528
|
||||
nacos.istio.mcp.server.enabled=false
|
||||
#*************** K8s \u76F8\u5173\u914D\u7F6E ***************#
|
||||
# K8s \u540C\u6B65\u652F\u6301 - \u5DF2\u7981\u7528
|
||||
nacos.k8s.sync.enabled=false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
~ Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
~
|
||||
~ Redistribution and use in source and binary forms, with or without
|
||||
~ modification, are permitted provided that the following conditions are met:
|
||||
|
||||
+2
-6
@@ -17,14 +17,11 @@
|
||||
|
||||
package com.pig4cloud.pigx.daemon.elastic.job;
|
||||
|
||||
import com.xxl.job.core.biz.model.ReturnT;
|
||||
import com.xxl.job.core.context.XxlJobHelper;
|
||||
import com.xxl.job.core.handler.annotation.XxlJob;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import static com.xxl.job.core.biz.model.ReturnT.SUCCESS;
|
||||
|
||||
/**
|
||||
* @author lengleng
|
||||
* @date 2019-09-18
|
||||
@@ -34,9 +31,8 @@ import static com.xxl.job.core.biz.model.ReturnT.SUCCESS;
|
||||
public class DemoJob {
|
||||
|
||||
@XxlJob("demoJobHandler")
|
||||
public ReturnT<String> demoJobHandler(String s) {
|
||||
public void demoJobHandler() {
|
||||
XxlJobHelper.log("This is a demo job." + XxlJobHelper.getShardIndex());
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
~ Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
~
|
||||
~ Redistribution and use in source and binary forms, with or without
|
||||
~ modification, are permitted provided that the following conditions are met:
|
||||
|
||||
+18
-18
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -42,22 +42,22 @@ public class PigxInitQuartzJob implements InitializingBean {
|
||||
|
||||
private final Scheduler scheduler;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
sysJobService.list().forEach(sysjob -> {
|
||||
if (PigxQuartzEnum.JOB_STATUS_RELEASE.getType().equals(sysjob.getJobStatus())) {
|
||||
taskUtil.removeJob(sysjob, scheduler);
|
||||
}
|
||||
else if (PigxQuartzEnum.JOB_STATUS_RUNNING.getType().equals(sysjob.getJobStatus())) {
|
||||
taskUtil.resumeJob(sysjob, scheduler);
|
||||
}
|
||||
else if (PigxQuartzEnum.JOB_STATUS_NOT_RUNNING.getType().equals(sysjob.getJobStatus())) {
|
||||
taskUtil.pauseJob(sysjob, scheduler);
|
||||
}
|
||||
else {
|
||||
taskUtil.removeJob(sysjob, scheduler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
sysJobService.list().forEach(sysjob -> {
|
||||
if (PigxQuartzEnum.JOB_STATUS_RELEASE.getType().equals(sysjob.getJobStatus())) {
|
||||
taskUtil.removeJob(sysjob, scheduler);
|
||||
}
|
||||
else if (PigxQuartzEnum.JOB_STATUS_RUNNING.getType().equals(sysjob.getJobStatus())) {
|
||||
taskUtil.resumeJob(sysjob, scheduler);
|
||||
}
|
||||
else if (PigxQuartzEnum.JOB_STATUS_NOT_RUNNING.getType().equals(sysjob.getJobStatus())) {
|
||||
taskUtil.pauseJob(sysjob, scheduler);
|
||||
}
|
||||
else {
|
||||
taskUtil.removeJob(sysjob, scheduler);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+50
-19
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -25,9 +25,9 @@ import org.quartz.Trigger;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.quartz.QuartzProperties;
|
||||
import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.quartz.autoconfigure.QuartzProperties;
|
||||
import org.springframework.boot.quartz.autoconfigure.SchedulerFactoryBeanCustomizer;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -40,13 +40,17 @@ import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* @author 郑健楠
|
||||
* Quartz 调度器配置。
|
||||
* <p>
|
||||
* 负责装配 {@link SchedulerFactoryBean}、加载 `quartz-config.yml` 中的 Quartz 参数,
|
||||
* 并启用本模块的异步任务监听与重复触发保护配置。
|
||||
* </p>
|
||||
*/
|
||||
@EnableAsync
|
||||
@Configuration
|
||||
@PropertySource(value = "classpath:quartz-config.yml", factory = YamlPropertySourceFactory.class)
|
||||
@ConditionalOnClass({ Scheduler.class, SchedulerFactoryBean.class })
|
||||
@EnableConfigurationProperties({ QuartzProperties.class })
|
||||
@EnableConfigurationProperties({QuartzProperties.class, QuartzProtectionProperties.class})
|
||||
public class PigxQuartzConfig {
|
||||
|
||||
private final QuartzProperties properties;
|
||||
@@ -61,6 +65,16 @@ public class PigxQuartzConfig {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
/**
|
||||
* 创建 Quartz 配置类。
|
||||
*
|
||||
* @param properties Quartz 原生属性配置
|
||||
* @param customizers 调度器自定义器集合
|
||||
* @param jobDetails 预注册 JobDetail 集合
|
||||
* @param calendars Quartz 日历配置
|
||||
* @param triggers 预注册 Trigger 集合
|
||||
* @param applicationContext Spring 应用上下文
|
||||
*/
|
||||
public PigxQuartzConfig(QuartzProperties properties,
|
||||
ObjectProvider<List<SchedulerFactoryBeanCustomizer>> customizers, ObjectProvider<JobDetail[]> jobDetails,
|
||||
ObjectProvider<Map<String, Calendar>> calendars, ObjectProvider<Trigger[]> triggers,
|
||||
@@ -70,9 +84,14 @@ public class PigxQuartzConfig {
|
||||
this.jobDetails = jobDetails.getIfAvailable();
|
||||
this.calendars = calendars.getIfAvailable();
|
||||
this.triggers = triggers.getIfAvailable();
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Quartz 调度器工厂。
|
||||
*
|
||||
* @return 已注入 Spring JobFactory 和 Quartz 属性的调度器工厂
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SchedulerFactoryBean quartzScheduler() {
|
||||
@@ -96,27 +115,39 @@ public class PigxQuartzConfig {
|
||||
}
|
||||
|
||||
this.customize(schedulerFactoryBean);
|
||||
return schedulerFactoryBean;
|
||||
}
|
||||
return schedulerFactoryBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Spring Boot 的 Quartz Map 属性转换为标准 {@link Properties}。
|
||||
*
|
||||
* @param source Quartz Map 形式的属性源
|
||||
* @return Quartz 可直接消费的标准属性对象
|
||||
*/
|
||||
private Properties asProperties(Map<String, String> source) {
|
||||
Properties properties = new Properties();
|
||||
properties.putAll(source);
|
||||
return properties;
|
||||
}
|
||||
properties.putAll(source);
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一执行所有调度器自定义器。
|
||||
*
|
||||
* @param schedulerFactoryBean 当前待定制的调度器工厂
|
||||
*/
|
||||
private void customize(SchedulerFactoryBean schedulerFactoryBean) {
|
||||
if (this.customizers != null) {
|
||||
for (SchedulerFactoryBeanCustomizer customizer : this.customizers) {
|
||||
customizer.customize(schedulerFactoryBean);
|
||||
}
|
||||
}
|
||||
customizer.customize(schedulerFactoryBean);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过SchedulerFactoryBean获取Scheduler的实例
|
||||
* @return
|
||||
/**
|
||||
* 通过 {@link SchedulerFactoryBean} 获取最终的 Quartz 调度器实例。
|
||||
*
|
||||
* @return Quartz 调度器实例
|
||||
*/
|
||||
@Bean
|
||||
public Scheduler scheduler() {
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
package com.pig4cloud.pigx.daemon.quartz.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer;
|
||||
import org.springframework.boot.quartz.autoconfigure.SchedulerFactoryBeanCustomizer;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
|
||||
|
||||
|
||||
+75
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -19,18 +19,26 @@ package com.pig4cloud.pigx.daemon.quartz.config;
|
||||
|
||||
import com.pig4cloud.pigx.daemon.quartz.constants.PigxQuartzEnum;
|
||||
import com.pig4cloud.pigx.daemon.quartz.entity.SysJob;
|
||||
import com.pig4cloud.pigx.daemon.quartz.entity.SysJobLog;
|
||||
import com.pig4cloud.pigx.daemon.quartz.event.SysJobLogEvent;
|
||||
import com.pig4cloud.pigx.daemon.quartz.support.QuartzExecutionMetadata;
|
||||
import com.pig4cloud.pigx.daemon.quartz.support.QuartzProtectionSupport;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.quartz.DisallowConcurrentExecution;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
/**
|
||||
* @author 郑健楠
|
||||
*
|
||||
* Quartz 任务工厂。
|
||||
* <p>
|
||||
* 动态任务工厂
|
||||
* 该工厂是 Quartz 真正进入业务逻辑前的第一道关口,
|
||||
* 负责构建执行元数据、拦截同一触发点的重复回调,并把合法执行请求继续分发到异步执行链路。
|
||||
* </p>
|
||||
*
|
||||
* @author 郑健楠
|
||||
*/
|
||||
@Slf4j
|
||||
@DisallowConcurrentExecution
|
||||
@@ -39,12 +47,74 @@ public class PigxQuartzFactory implements Job {
|
||||
@Autowired
|
||||
private PigxQuartzInvokeFactory pigxQuartzInvokeFactory;
|
||||
|
||||
@Autowired
|
||||
private QuartzProtectionSupport quartzProtectionSupport;
|
||||
|
||||
@Autowired
|
||||
private ApplicationEventPublisher publisher;
|
||||
|
||||
/**
|
||||
* 执行 Quartz 调度任务。
|
||||
*
|
||||
* @param jobExecutionContext Quartz 原始执行上下文,包含任务数据与触发器信息
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void execute(JobExecutionContext jobExecutionContext) {
|
||||
SysJob sysJob = (SysJob) jobExecutionContext.getMergedJobDataMap()
|
||||
.get(PigxQuartzEnum.SCHEDULE_JOB_KEY.getType());
|
||||
pigxQuartzInvokeFactory.init(sysJob, jobExecutionContext.getTrigger());
|
||||
QuartzExecutionMetadata executionMetadata = quartzProtectionSupport
|
||||
.buildExecutionMetadata(jobExecutionContext, jobExecutionContext.getTrigger());
|
||||
if (quartzProtectionSupport.isProtectionEnabled()) {
|
||||
QuartzProtectionSupport.ProtectionLock protectionLock = quartzProtectionSupport
|
||||
.tryAcquireFireDedupLock(sysJob, executionMetadata);
|
||||
if (!protectionLock.isAcquired()) {
|
||||
publishSkippedLog(sysJob, executionMetadata);
|
||||
return;
|
||||
}
|
||||
}
|
||||
pigxQuartzInvokeFactory.init(sysJob, jobExecutionContext.getTrigger(), executionMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录“同一触发点重复回调”场景下的跳过日志。
|
||||
*
|
||||
* @param sysJob 当前任务配置
|
||||
* @param executionMetadata 当前触发对应的执行元数据
|
||||
*/
|
||||
private void publishSkippedLog(SysJob sysJob, QuartzExecutionMetadata executionMetadata) {
|
||||
if (!quartzProtectionSupport.shouldLogSkipped()) {
|
||||
return;
|
||||
}
|
||||
publisher.publishEvent(new SysJobLogEvent(buildSkippedLog(sysJob, executionMetadata)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造重复触发场景下的任务日志。
|
||||
*
|
||||
* @param sysJob 当前任务配置
|
||||
* @param executionMetadata 当前触发对应的执行元数据
|
||||
* @return 可直接落库的跳过日志对象
|
||||
*/
|
||||
private SysJobLog buildSkippedLog(SysJob sysJob, QuartzExecutionMetadata executionMetadata) {
|
||||
return SysJobLog.builder()
|
||||
.jobId(sysJob.getJobId())
|
||||
.jobName(sysJob.getJobName())
|
||||
.jobGroup(sysJob.getJobGroup())
|
||||
.jobOrder(sysJob.getJobOrder())
|
||||
.jobType(sysJob.getJobType())
|
||||
.executePath(sysJob.getExecutePath())
|
||||
.className(sysJob.getClassName())
|
||||
.methodName(sysJob.getMethodName())
|
||||
.methodParamsValue(sysJob.getMethodParamsValue())
|
||||
.cronExpression(sysJob.getCronExpression())
|
||||
.tenantId(sysJob.getTenantId())
|
||||
.jobMessage(PigxQuartzEnum.JOB_DEDUP_STATUS_DUPLICATE_FIRE.getDescription())
|
||||
.jobLogStatus(PigxQuartzEnum.JOB_LOG_STATUS_SUCCESS.getType())
|
||||
.dedupStatus(PigxQuartzEnum.JOB_DEDUP_STATUS_DUPLICATE_FIRE.getType())
|
||||
.scheduledFireTime(executionMetadata.scheduledFireTime())
|
||||
.fireInstanceId(executionMetadata.fireInstanceId())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
+17
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -19,6 +19,7 @@ package com.pig4cloud.pigx.daemon.quartz.config;
|
||||
|
||||
import com.pig4cloud.pigx.daemon.quartz.entity.SysJob;
|
||||
import com.pig4cloud.pigx.daemon.quartz.event.SysJobEvent;
|
||||
import com.pig4cloud.pigx.daemon.quartz.support.QuartzExecutionMetadata;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -28,7 +29,11 @@ import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author 郑健楠
|
||||
* Quartz 调用事件发布器。
|
||||
* <p>
|
||||
* 负责把 Quartz 工厂线程中的任务执行请求转换为 Spring 事件,
|
||||
* 以便后续由异步监听器统一处理真实业务调用。
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@@ -38,9 +43,16 @@ public class PigxQuartzInvokeFactory {
|
||||
|
||||
private final ApplicationEventPublisher publisher;
|
||||
|
||||
@SneakyThrows
|
||||
void init(SysJob sysJob, Trigger trigger) {
|
||||
publisher.publishEvent(new SysJobEvent(sysJob, trigger));
|
||||
/**
|
||||
* 发布任务执行事件。
|
||||
*
|
||||
* @param sysJob 当前任务配置
|
||||
* @param trigger 当前 Quartz 触发器
|
||||
* @param executionMetadata 当前触发对应的执行元数据
|
||||
*/
|
||||
@SneakyThrows
|
||||
void init(SysJob sysJob, Trigger trigger, QuartzExecutionMetadata executionMetadata) {
|
||||
publisher.publishEvent(new SysJobEvent(sysJob, trigger, executionMetadata));
|
||||
}
|
||||
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.pig4cloud.pigx.daemon.quartz.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Quartz 重复触发保护配置。
|
||||
* <p>
|
||||
* 该配置统一控制 Quartz 任务的两层保护能力:
|
||||
* 第一层用于拦截同一调度时间点的重复触发,第二层用于限制任务执行期间的并发进入。
|
||||
* </p>
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "pigx.quartz.protection")
|
||||
public class QuartzProtectionProperties {
|
||||
|
||||
/**
|
||||
* 是否启用保护
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 同一触发点去重键有效期
|
||||
*/
|
||||
private long fireDedupTtlSeconds = 24 * 60 * 60L;
|
||||
|
||||
/**
|
||||
* 任务运行锁有效期
|
||||
*/
|
||||
private long runningLockTtlSeconds = 10 * 60L;
|
||||
|
||||
/**
|
||||
* 是否记录跳过日志
|
||||
*/
|
||||
private boolean logSkipped = true;
|
||||
|
||||
}
|
||||
+20
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -21,10 +21,13 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author 郑健楠
|
||||
*
|
||||
* Quartz 调度相关枚举常量。
|
||||
* <p>
|
||||
* 定时任务枚举
|
||||
* 统一维护任务状态、执行状态、错失策略以及重复触发防护状态,
|
||||
* 便于调度器、执行器和日志体系共用同一套语义。
|
||||
* </p>
|
||||
*
|
||||
* @author 郑健楠
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@@ -64,6 +67,19 @@ public enum PigxQuartzEnum {
|
||||
*/
|
||||
JOB_LOG_STATUS_FAIL("1", "执行失败"),
|
||||
|
||||
/**
|
||||
* 去重状态:0 正常执行
|
||||
*/
|
||||
JOB_DEDUP_STATUS_NORMAL("0", "正常执行"),
|
||||
/**
|
||||
* 去重状态:1 同一触发点重复,已跳过
|
||||
*/
|
||||
JOB_DEDUP_STATUS_DUPLICATE_FIRE("1", "同一触发点重复,已跳过"),
|
||||
/**
|
||||
* 去重状态:2 任务正在运行,已跳过
|
||||
*/
|
||||
JOB_DEDUP_STATUS_RUNNING_SKIP("2", "任务正在运行,已跳过"),
|
||||
|
||||
/**
|
||||
* JOB状态:1已发布
|
||||
*/
|
||||
|
||||
+21
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -27,9 +27,14 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 定时任务执行日志表
|
||||
* <p>
|
||||
* 该实体除保留原有任务日志字段外,还新增了 Quartz 防重排查字段,
|
||||
* 用于定位同一秒重复触发、运行中跳过等场景。
|
||||
* </p>
|
||||
*
|
||||
* @author frwcloud
|
||||
* @date 2019-01-27 13:40:20
|
||||
@@ -121,6 +126,21 @@ public class SysJobLog extends Model<SysJobLog> {
|
||||
*/
|
||||
private String exceptionInfo;
|
||||
|
||||
/**
|
||||
* 计划触发时间
|
||||
*/
|
||||
private Date scheduledFireTime;
|
||||
|
||||
/**
|
||||
* Quartz 触发实例ID,用于定位同一轮调度是否被重复回调
|
||||
*/
|
||||
private String fireInstanceId;
|
||||
|
||||
/**
|
||||
* 去重状态(0正常执行 1同一触发点重复 2任务运行中跳过)
|
||||
*/
|
||||
private String dedupStatus;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
|
||||
+9
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -18,12 +18,17 @@
|
||||
package com.pig4cloud.pigx.daemon.quartz.event;
|
||||
|
||||
import com.pig4cloud.pigx.daemon.quartz.entity.SysJob;
|
||||
import com.pig4cloud.pigx.daemon.quartz.support.QuartzExecutionMetadata;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.quartz.Trigger;
|
||||
|
||||
/**
|
||||
* @author frwcloud 定时任务多线程事件
|
||||
* 定时任务异步执行事件。
|
||||
* <p>
|
||||
* 该事件用于在 Quartz 工厂线程与异步任务监听线程之间传递任务对象、
|
||||
* 触发器对象以及本次调度的排查元数据。
|
||||
* </p>
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@@ -33,4 +38,6 @@ public class SysJobEvent {
|
||||
|
||||
private final Trigger trigger;
|
||||
|
||||
private final QuartzExecutionMetadata executionMetadata;
|
||||
|
||||
}
|
||||
+12
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -28,7 +28,11 @@ import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author frwcloud 异步监听定时任务事件
|
||||
* 异步监听定时任务执行事件。
|
||||
* <p>
|
||||
* 监听器收到事件后,会在异步线程中继续调用真实任务执行器,
|
||||
* 从而避免 Quartz 调度线程被业务执行时间阻塞。
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -37,13 +41,18 @@ public class SysJobListener {
|
||||
|
||||
private final TaskInvokUtil taskInvokUtil;
|
||||
|
||||
/**
|
||||
* 处理 Quartz 发布的任务执行事件。
|
||||
*
|
||||
* @param event 当前任务执行事件,包含任务配置、触发器和执行元数据
|
||||
*/
|
||||
@Async
|
||||
@Order
|
||||
@EventListener(SysJobEvent.class)
|
||||
public void comSysJob(SysJobEvent event) {
|
||||
SysJob sysJob = event.getSysJob();
|
||||
Trigger trigger = event.getTrigger();
|
||||
taskInvokUtil.invokMethod(sysJob, trigger);
|
||||
taskInvokUtil.invokMethod(sysJob, trigger, event.getExecutionMetadata());
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -44,7 +44,7 @@ public class SysJobLogListener {
|
||||
public void saveSysJobLog(SysJobLogEvent event) {
|
||||
SysJobLog sysJobLog = event.getSysJobLog();
|
||||
// 只保存发布状态的任务日志
|
||||
if (Objects.nonNull(sysJobLog.getJobId())){
|
||||
if (Objects.nonNull(sysJobLog.getJobId())) {
|
||||
sysJobLogService.save(sysJobLog);
|
||||
}
|
||||
log.info("执行定时任务日志");
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.pig4cloud.pigx.daemon.quartz.support;
|
||||
|
||||
import org.quartz.Trigger;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Quartz 执行上下文元数据。
|
||||
* <p>
|
||||
* 用于在 Quartz 工厂、异步监听器和真实任务执行器之间传递同一轮调度的关键上下文,
|
||||
* 避免重复从 {@code JobExecutionContext} 中取值,也便于记录排查日志。
|
||||
* </p>
|
||||
*/
|
||||
public record QuartzExecutionMetadata(Date scheduledFireTime, String fireInstanceId, String triggerType,
|
||||
String dedupSource) {
|
||||
|
||||
/**
|
||||
* 根据 Quartz 原生触发器构建执行元数据。
|
||||
*
|
||||
* @param scheduledFireTime Quartz 计划触发时间,为空时由调用方提前兜底
|
||||
* @param fireInstanceId Quartz 触发实例ID,可为空
|
||||
* @param trigger 当前触发器实例,可为空
|
||||
* @param dedupSource 去重来源标识,用于后续日志排查
|
||||
* @return 封装后的 Quartz 执行元数据
|
||||
*/
|
||||
public static QuartzExecutionMetadata of(Date scheduledFireTime, String fireInstanceId, Trigger trigger,
|
||||
String dedupSource) {
|
||||
String resolvedTriggerType = trigger == null ? "UNKNOWN" : trigger.getClass().getSimpleName();
|
||||
return new QuartzExecutionMetadata(scheduledFireTime, fireInstanceId, resolvedTriggerType, dedupSource);
|
||||
}
|
||||
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
package com.pig4cloud.pigx.daemon.quartz.support;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.pig4cloud.pigx.common.data.cache.RedisUtils;
|
||||
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
|
||||
import com.pig4cloud.pigx.daemon.quartz.config.QuartzProtectionProperties;
|
||||
import com.pig4cloud.pigx.daemon.quartz.entity.SysJob;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.Trigger;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Quartz 重复触发保护支撑组件。
|
||||
* <p>
|
||||
* 该组件负责封装 Quartz 防重逻辑所需的 Redis 锁、触发元数据生成和锁释放流程,
|
||||
* 让调度入口与任务执行入口都能共享同一套保护规则。
|
||||
* </p>
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class QuartzProtectionSupport {
|
||||
|
||||
private static final String FIRE_DEDUP_KEY_PREFIX = "quartz:fire:dedup:";
|
||||
|
||||
private static final String RUNNING_LOCK_KEY_PREFIX = "quartz:job:running:";
|
||||
|
||||
private final QuartzProtectionProperties properties;
|
||||
|
||||
/**
|
||||
* 判断是否启用 Quartz 防重保护。
|
||||
*
|
||||
* @return 启用返回 {@code true},否则返回 {@code false}
|
||||
*/
|
||||
public boolean isProtectionEnabled() {
|
||||
return properties.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否记录被跳过的执行日志。
|
||||
*
|
||||
* @return 需要记录返回 {@code true},否则返回 {@code false}
|
||||
*/
|
||||
public boolean shouldLogSkipped() {
|
||||
return properties.isLogSkipped();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Quartz 原始执行上下文构建统一元数据。
|
||||
*
|
||||
* @param jobExecutionContext Quartz 原始执行上下文,不允许为空
|
||||
* @param trigger 当前触发器,可为空
|
||||
* @return 统一封装后的执行元数据
|
||||
*/
|
||||
public QuartzExecutionMetadata buildExecutionMetadata(JobExecutionContext jobExecutionContext, Trigger trigger) {
|
||||
Date scheduledFireTime = jobExecutionContext.getScheduledFireTime();
|
||||
if (scheduledFireTime == null) {
|
||||
scheduledFireTime = jobExecutionContext.getFireTime();
|
||||
}
|
||||
if (scheduledFireTime == null) {
|
||||
scheduledFireTime = new Date();
|
||||
}
|
||||
return QuartzExecutionMetadata.of(scheduledFireTime, jobExecutionContext.getFireInstanceId(), trigger, "QUARTZ");
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取“同一触发点去重锁”。
|
||||
*
|
||||
* @param sysJob 当前任务配置,不允许为空
|
||||
* @param metadata 当前触发元数据,不允许为空
|
||||
* @return 去重锁句柄;当防护关闭时返回一个恒定视为已获取的空锁句柄
|
||||
*/
|
||||
public ProtectionLock tryAcquireFireDedupLock(SysJob sysJob, QuartzExecutionMetadata metadata) {
|
||||
if (!isProtectionEnabled()) {
|
||||
return ProtectionLock.disabled();
|
||||
}
|
||||
String token = IdUtil.fastSimpleUUID();
|
||||
String lockKey = FIRE_DEDUP_KEY_PREFIX + resolveTenantId(sysJob) + ":" + sysJob.getJobId() + ":"
|
||||
+ metadata.scheduledFireTime().getTime();
|
||||
boolean acquired = RedisUtils.getLock(lockKey, token, toExpireSeconds(properties.getFireDedupTtlSeconds()));
|
||||
return new ProtectionLock(lockKey, token, acquired, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取“任务运行锁”。
|
||||
*
|
||||
* @param sysJob 当前任务配置,不允许为空
|
||||
* @return 运行锁句柄;当防护关闭时返回一个恒定视为已获取的空锁句柄
|
||||
*/
|
||||
public ProtectionLock tryAcquireRunningLock(SysJob sysJob) {
|
||||
if (!isProtectionEnabled()) {
|
||||
return ProtectionLock.disabled();
|
||||
}
|
||||
String token = IdUtil.fastSimpleUUID();
|
||||
String lockKey = RUNNING_LOCK_KEY_PREFIX + resolveTenantId(sysJob) + ":" + sysJob.getJobId();
|
||||
boolean acquired = RedisUtils.getLock(lockKey, token, toExpireSeconds(properties.getRunningLockTtlSeconds()));
|
||||
return new ProtectionLock(lockKey, token, acquired, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放运行锁。
|
||||
*
|
||||
* @param protectionLock 当前持有的锁句柄,为空或不可释放时直接忽略
|
||||
*/
|
||||
public void releaseRunningLock(ProtectionLock protectionLock) {
|
||||
if (protectionLock == null || !protectionLock.releasable || !protectionLock.acquired) {
|
||||
return;
|
||||
}
|
||||
RedisUtils.releaseLock(protectionLock.lockKey, protectionLock.token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析任务所属租户。
|
||||
*
|
||||
* @param sysJob 当前任务配置,不允许为空
|
||||
* @return 任务显式租户ID,若未配置则回退到当前线程租户上下文
|
||||
*/
|
||||
private Long resolveTenantId(SysJob sysJob) {
|
||||
return Objects.nonNull(sysJob.getTenantId()) ? sysJob.getTenantId() : TenantContextHolder.getTenantId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将秒级过期时间转换为 Redis 锁所需的整型秒数。
|
||||
*
|
||||
* @param expireSeconds 配置中的过期秒数
|
||||
* @return 适配 Redis 工具类后的整型秒数,最小为 1 秒
|
||||
*/
|
||||
private int toExpireSeconds(long expireSeconds) {
|
||||
if (expireSeconds <= 0) {
|
||||
return 1;
|
||||
}
|
||||
return expireSeconds > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) expireSeconds;
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static final class ProtectionLock {
|
||||
|
||||
private final String lockKey;
|
||||
|
||||
private final String token;
|
||||
|
||||
private final boolean acquired;
|
||||
|
||||
private final boolean releasable;
|
||||
|
||||
/**
|
||||
* 创建一个锁句柄。
|
||||
*
|
||||
* @param lockKey Redis 锁键,防护关闭时允许为空
|
||||
* @param token 当前线程持有的锁值,防护关闭时允许为空
|
||||
* @param acquired 是否已成功获取锁
|
||||
* @param releasable 当前锁是否需要在结束时释放
|
||||
*/
|
||||
public ProtectionLock(String lockKey, String token, boolean acquired, boolean releasable) {
|
||||
this.lockKey = lockKey;
|
||||
this.token = token;
|
||||
this.acquired = acquired;
|
||||
this.releasable = releasable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个“防护关闭”场景下的空锁句柄。
|
||||
*
|
||||
* @return 视为已获取且无需释放的空锁句柄
|
||||
*/
|
||||
public static ProtectionLock disabled() {
|
||||
return new ProtectionLock(null, null, true, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+180
-66
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -24,6 +24,8 @@ import com.pig4cloud.pigx.daemon.quartz.entity.SysJob;
|
||||
import com.pig4cloud.pigx.daemon.quartz.entity.SysJobLog;
|
||||
import com.pig4cloud.pigx.daemon.quartz.event.SysJobLogEvent;
|
||||
import com.pig4cloud.pigx.daemon.quartz.service.SysJobService;
|
||||
import com.pig4cloud.pigx.daemon.quartz.support.QuartzExecutionMetadata;
|
||||
import com.pig4cloud.pigx.daemon.quartz.support.QuartzProtectionSupport;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -34,9 +36,13 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 定时任务反射工具类
|
||||
* 定时任务执行工具类。
|
||||
* <p>
|
||||
* 负责真正调用任务执行器、记录执行日志,并在调用前后挂载 Quartz 防重保护相关的运行锁与元数据。
|
||||
* </p>
|
||||
*
|
||||
* @author 郑健楠
|
||||
*/
|
||||
@@ -45,74 +51,182 @@ import java.util.Date;
|
||||
@RequiredArgsConstructor
|
||||
public class TaskInvokUtil {
|
||||
|
||||
private final ApplicationEventPublisher publisher;
|
||||
private final ApplicationEventPublisher publisher;
|
||||
|
||||
private final SysJobService sysJobService;
|
||||
private final SysJobService sysJobService;
|
||||
|
||||
@SneakyThrows
|
||||
public void invokMethod(SysJob sysJob, Trigger trigger) {
|
||||
private final QuartzProtectionSupport quartzProtectionSupport;
|
||||
|
||||
// 执行开始时间
|
||||
long startTime;
|
||||
// 执行结束时间
|
||||
long endTime;
|
||||
// 获取执行开始时间
|
||||
startTime = System.currentTimeMillis();
|
||||
// 更新定时任务表内的状态、执行时间、上次执行时间、下次执行时间等信息
|
||||
SysJob updateSysjob = new SysJob();
|
||||
updateSysjob.setJobId(sysJob.getJobId());
|
||||
// 日志
|
||||
SysJobLog sysJobLog = new SysJobLog();
|
||||
sysJobLog.setJobId(sysJob.getJobId());
|
||||
sysJobLog.setJobName(sysJob.getJobName());
|
||||
sysJobLog.setJobGroup(sysJob.getJobGroup());
|
||||
sysJobLog.setJobOrder(sysJob.getJobOrder());
|
||||
sysJobLog.setJobType(sysJob.getJobType());
|
||||
sysJobLog.setExecutePath(sysJob.getExecutePath());
|
||||
sysJobLog.setClassName(sysJob.getClassName());
|
||||
sysJobLog.setMethodName(sysJob.getMethodName());
|
||||
sysJobLog.setMethodParamsValue(sysJob.getMethodParamsValue());
|
||||
sysJobLog.setCronExpression(sysJob.getCronExpression());
|
||||
sysJobLog.setTenantId(sysJob.getTenantId());
|
||||
try {
|
||||
// 执行任务
|
||||
ITaskInvok iTaskInvok = TaskInvokFactory.getInvoker(sysJob.getJobType());
|
||||
// 确保租户上下文有值,使得当前线程中的多租户特性生效。
|
||||
TenantBroker.runAs(sysJob.getTenantId(), tenantId -> iTaskInvok.invokMethod(sysJob));
|
||||
/**
|
||||
* 使用兼容旧接口的方式执行定时任务。
|
||||
*
|
||||
* @param sysJob 当前任务配置
|
||||
* @param trigger 当前触发器
|
||||
*/
|
||||
@SneakyThrows
|
||||
public void invokMethod(SysJob sysJob, Trigger trigger) {
|
||||
invokMethod(sysJob, trigger,
|
||||
QuartzExecutionMetadata.of(new Date(), null, trigger, "QUARTZ"));
|
||||
}
|
||||
|
||||
// 记录成功状态
|
||||
sysJobLog.setJobMessage(PigxQuartzEnum.JOB_LOG_STATUS_SUCCESS.getDescription());
|
||||
sysJobLog.setJobLogStatus(PigxQuartzEnum.JOB_LOG_STATUS_SUCCESS.getType());
|
||||
// 任务表信息更新
|
||||
updateSysjob.setJobExecuteStatus(PigxQuartzEnum.JOB_LOG_STATUS_SUCCESS.getType());
|
||||
}
|
||||
catch (Throwable e) {
|
||||
log.error("定时任务执行失败,任务名称:{};任务组名:{},cron执行表达式:{},执行时间:{}", sysJob.getJobName(), sysJob.getJobGroup(),
|
||||
sysJob.getCronExpression(), new Date());
|
||||
// 记录失败状态
|
||||
sysJobLog.setJobMessage(PigxQuartzEnum.JOB_LOG_STATUS_FAIL.getDescription());
|
||||
sysJobLog.setJobLogStatus(PigxQuartzEnum.JOB_LOG_STATUS_FAIL.getType());
|
||||
sysJobLog.setExceptionInfo(StrUtil.sub(e.getMessage(), 0, 2000));
|
||||
// 任务表信息更新
|
||||
updateSysjob.setJobExecuteStatus(PigxQuartzEnum.JOB_LOG_STATUS_FAIL.getType());
|
||||
}
|
||||
finally {
|
||||
// 记录执行时间 立刻执行使用的是simpleTeigger
|
||||
if (trigger instanceof CronTrigger) {
|
||||
updateSysjob
|
||||
.setStartTime(trigger.getStartTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
|
||||
updateSysjob.setPreviousTime(
|
||||
trigger.getPreviousFireTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
|
||||
updateSysjob.setNextTime(
|
||||
trigger.getNextFireTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
|
||||
}
|
||||
// 记录执行时长
|
||||
endTime = System.currentTimeMillis();
|
||||
sysJobLog.setExecuteTime(String.valueOf(endTime - startTime));
|
||||
/**
|
||||
* 执行定时任务,并记录本次任务的日志与执行状态。
|
||||
*
|
||||
* @param sysJob 当前任务配置,不允许为空
|
||||
* @param trigger 当前触发器,可为空
|
||||
* @param executionMetadata 当前调度的执行元数据,不允许为空
|
||||
*/
|
||||
@SneakyThrows
|
||||
public void invokMethod(SysJob sysJob, Trigger trigger, QuartzExecutionMetadata executionMetadata) {
|
||||
Objects.requireNonNull(sysJob, "sysJob must not be null");
|
||||
Objects.requireNonNull(executionMetadata, "executionMetadata must not be null");
|
||||
|
||||
publisher.publishEvent(new SysJobLogEvent(sysJobLog));
|
||||
sysJobService.updateById(updateSysjob);
|
||||
}
|
||||
}
|
||||
// 执行开始时间
|
||||
long startTime;
|
||||
// 执行结束时间
|
||||
long endTime;
|
||||
// 获取执行开始时间
|
||||
startTime = System.currentTimeMillis();
|
||||
// 更新定时任务表内的状态、执行时间、上次执行时间、下次执行时间等信息
|
||||
SysJob updateSysjob = createUpdateJob(sysJob);
|
||||
SysJobLog sysJobLog = buildBaseJobLog(sysJob, executionMetadata);
|
||||
QuartzProtectionSupport.ProtectionLock runningLock = quartzProtectionSupport.tryAcquireRunningLock(sysJob);
|
||||
try {
|
||||
if (!runningLock.isAcquired()) {
|
||||
markRunningSkipped(sysJobLog);
|
||||
return;
|
||||
}
|
||||
// 执行任务
|
||||
ITaskInvok iTaskInvok = TaskInvokFactory.getInvoker(sysJob.getJobType());
|
||||
// 确保租户上下文有值,使得当前线程中的多租户特性生效。
|
||||
TenantBroker.runAs(sysJob.getTenantId(), tenantId -> iTaskInvok.invokMethod(sysJob));
|
||||
|
||||
// 记录成功状态
|
||||
markSuccess(sysJobLog, updateSysjob);
|
||||
} catch (Throwable e) {
|
||||
log.error("定时任务执行失败,任务名称:{};任务组名:{},cron执行表达式:{},执行时间:{}", sysJob.getJobName(), sysJob.getJobGroup(),
|
||||
sysJob.getCronExpression(), new Date());
|
||||
markFailure(sysJobLog, updateSysjob, e);
|
||||
} finally {
|
||||
fillTriggerTimes(updateSysjob, trigger);
|
||||
endTime = System.currentTimeMillis();
|
||||
sysJobLog.setExecuteTime(String.valueOf(endTime - startTime));
|
||||
publishLogAndUpdateJob(sysJobLog, updateSysjob);
|
||||
releaseRunningLockIfNecessary(runningLock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建任务状态更新对象。
|
||||
*
|
||||
* @param sysJob 当前任务配置
|
||||
* @return 仅包含主键的状态更新对象
|
||||
*/
|
||||
private SysJob createUpdateJob(SysJob sysJob) {
|
||||
SysJob updateSysjob = new SysJob();
|
||||
updateSysjob.setJobId(sysJob.getJobId());
|
||||
return updateSysjob;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造本次任务的基础日志对象。
|
||||
*
|
||||
* @param sysJob 当前任务配置
|
||||
* @param executionMetadata 当前调度的执行元数据
|
||||
* @return 已填充任务基本信息的日志对象
|
||||
*/
|
||||
private SysJobLog buildBaseJobLog(SysJob sysJob, QuartzExecutionMetadata executionMetadata) {
|
||||
SysJobLog sysJobLog = new SysJobLog();
|
||||
sysJobLog.setJobId(sysJob.getJobId());
|
||||
sysJobLog.setJobName(sysJob.getJobName());
|
||||
sysJobLog.setJobGroup(sysJob.getJobGroup());
|
||||
sysJobLog.setJobOrder(sysJob.getJobOrder());
|
||||
sysJobLog.setJobType(sysJob.getJobType());
|
||||
sysJobLog.setExecutePath(sysJob.getExecutePath());
|
||||
sysJobLog.setClassName(sysJob.getClassName());
|
||||
sysJobLog.setMethodName(sysJob.getMethodName());
|
||||
sysJobLog.setMethodParamsValue(sysJob.getMethodParamsValue());
|
||||
sysJobLog.setCronExpression(sysJob.getCronExpression());
|
||||
sysJobLog.setTenantId(sysJob.getTenantId());
|
||||
sysJobLog.setScheduledFireTime(executionMetadata.scheduledFireTime());
|
||||
sysJobLog.setFireInstanceId(executionMetadata.fireInstanceId());
|
||||
sysJobLog.setDedupStatus(PigxQuartzEnum.JOB_DEDUP_STATUS_NORMAL.getType());
|
||||
return sysJobLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记任务因运行锁未获取而跳过。
|
||||
*
|
||||
* @param sysJobLog 当前任务日志对象
|
||||
*/
|
||||
private void markRunningSkipped(SysJobLog sysJobLog) {
|
||||
sysJobLog.setJobMessage(PigxQuartzEnum.JOB_DEDUP_STATUS_RUNNING_SKIP.getDescription());
|
||||
sysJobLog.setJobLogStatus(PigxQuartzEnum.JOB_LOG_STATUS_SUCCESS.getType());
|
||||
sysJobLog.setDedupStatus(PigxQuartzEnum.JOB_DEDUP_STATUS_RUNNING_SKIP.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记任务成功执行。
|
||||
*
|
||||
* @param sysJobLog 当前任务日志对象
|
||||
* @param updateSysjob 当前任务状态更新对象
|
||||
*/
|
||||
private void markSuccess(SysJobLog sysJobLog, SysJob updateSysjob) {
|
||||
sysJobLog.setJobMessage(PigxQuartzEnum.JOB_LOG_STATUS_SUCCESS.getDescription());
|
||||
sysJobLog.setJobLogStatus(PigxQuartzEnum.JOB_LOG_STATUS_SUCCESS.getType());
|
||||
updateSysjob.setJobExecuteStatus(PigxQuartzEnum.JOB_LOG_STATUS_SUCCESS.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记任务执行失败。
|
||||
*
|
||||
* @param sysJobLog 当前任务日志对象
|
||||
* @param updateSysjob 当前任务状态更新对象
|
||||
* @param throwable 本次执行捕获到的异常
|
||||
*/
|
||||
private void markFailure(SysJobLog sysJobLog, SysJob updateSysjob, Throwable throwable) {
|
||||
sysJobLog.setJobMessage(PigxQuartzEnum.JOB_LOG_STATUS_FAIL.getDescription());
|
||||
sysJobLog.setJobLogStatus(PigxQuartzEnum.JOB_LOG_STATUS_FAIL.getType());
|
||||
sysJobLog.setExceptionInfo(StrUtil.sub(throwable.getMessage(), 0, 2000));
|
||||
updateSysjob.setJobExecuteStatus(PigxQuartzEnum.JOB_LOG_STATUS_FAIL.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Cron 触发器回填任务时间信息。
|
||||
*
|
||||
* @param updateSysjob 当前任务状态更新对象
|
||||
* @param trigger 当前触发器,可为空
|
||||
*/
|
||||
private void fillTriggerTimes(SysJob updateSysjob, Trigger trigger) {
|
||||
if (!(trigger instanceof CronTrigger)) {
|
||||
return;
|
||||
}
|
||||
updateSysjob.setStartTime(trigger.getStartTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
|
||||
updateSysjob.setPreviousTime(
|
||||
trigger.getPreviousFireTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
|
||||
updateSysjob.setNextTime(trigger.getNextFireTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布任务日志并更新任务执行状态。
|
||||
*
|
||||
* @param sysJobLog 当前任务日志对象
|
||||
* @param updateSysjob 当前任务状态更新对象
|
||||
*/
|
||||
private void publishLogAndUpdateJob(SysJobLog sysJobLog, SysJob updateSysjob) {
|
||||
publisher.publishEvent(new SysJobLogEvent(sysJobLog));
|
||||
sysJobService.updateById(updateSysjob);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按需释放运行锁。
|
||||
*
|
||||
* @param runningLock 当前任务持有的运行锁句柄
|
||||
*/
|
||||
private void releaseRunningLockIfNecessary(QuartzProtectionSupport.ProtectionLock runningLock) {
|
||||
if (runningLock.isAcquired()) {
|
||||
quartzProtectionSupport.releaseRunningLock(runningLock);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+2
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -20,7 +20,6 @@ package com.pig4cloud.pigx.bi;
|
||||
import com.pig4cloud.pigx.common.feign.annotation.EnablePigxFeignClients;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
|
||||
/**
|
||||
@@ -30,8 +29,7 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
*/
|
||||
@EnablePigxFeignClients
|
||||
@EnableDiscoveryClient
|
||||
@SpringBootApplication(scanBasePackages = { "org.jeecg.modules.jmreport", "com.pig4cloud.pigx.bi" },
|
||||
exclude = MongoAutoConfiguration.class)
|
||||
@SpringBootApplication(scanBasePackages = {"org.jeecg.modules.jmreport", "com.pig4cloud.pigx.bi"})
|
||||
public class PigxJimuApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary form, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the pig4cloud.com developer nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
* Author: lengleng (wangiegie@gmail.com)
|
||||
*/
|
||||
|
||||
package com.pig4cloud.pigx.bi.config;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* JimuReport 2.0.0 AI Assistant 与 Spring Framework 7 暂不兼容。
|
||||
* <p>
|
||||
* 其 AI Service 仍调用已移除的
|
||||
* {@code HttpComponentsClientHttpRequestFactory#setConnectTimeout(int)}。
|
||||
* 这里仅移除 AI Assistant 相关 Bean,保留 JimuReport 报表核心能力。
|
||||
*
|
||||
* @author lengleng
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class JimuAiAssistantCompatibilityConfiguration {
|
||||
|
||||
private static final String AI_PACKAGE_PREFIX = "org.jeecg.modules.jmreport.ai.";
|
||||
|
||||
@Bean
|
||||
public static BeanDefinitionRegistryPostProcessor jimuAiAssistantBeanDefinitionPostProcessor() {
|
||||
return new BeanDefinitionRegistryPostProcessor() {
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
|
||||
Arrays.stream(registry.getBeanDefinitionNames())
|
||||
.filter(beanName -> isJimuAiAssistantBean(registry, beanName))
|
||||
.forEach(registry::removeBeanDefinition);
|
||||
}
|
||||
|
||||
private boolean isJimuAiAssistantBean(BeanDefinitionRegistry registry, String beanName) {
|
||||
BeanDefinition beanDefinition = registry.getBeanDefinition(beanName);
|
||||
String beanClassName = beanDefinition.getBeanClassName();
|
||||
return beanName.startsWith("jimuAiAssistant")
|
||||
|| (beanClassName != null && beanClassName.startsWith(AI_PACKAGE_PREFIX));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
~ Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
~
|
||||
~ Redistribution and use in source and binary forms, with or without
|
||||
~ modification, are permitted provided that the following conditions are met:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
~ Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
~
|
||||
~ Redistribution and use in source and binary forms, with or without
|
||||
~ modification, are permitted provided that the following conditions are met:
|
||||
|
||||
+12
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, 云集汇通 All rights reserved.
|
||||
* Copyright (c) 2018-2025, 云集汇通 All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -40,7 +40,7 @@ public class WxMpInRedisConfigStorage extends WxMpDefaultConfigImpl {
|
||||
|
||||
private String cardapiTicketKey;
|
||||
|
||||
public WxMpInRedisConfigStorage() {
|
||||
public WxMpInRedisConfigStorage() {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,52 +57,52 @@ public class WxMpInRedisConfigStorage extends WxMpDefaultConfigImpl {
|
||||
|
||||
@Override
|
||||
public String getAccessToken() {
|
||||
return RedisUtils.get(this.accessTokenKey);
|
||||
return RedisUtils.get(this.accessTokenKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccessTokenExpired() {
|
||||
return RedisUtils.getExpire(accessTokenKey) < 2L;
|
||||
return RedisUtils.getExpire(accessTokenKey) < 2L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
|
||||
RedisUtils.set(accessTokenKey, accessToken, expiresInSeconds - 200);
|
||||
RedisUtils.set(accessTokenKey, accessToken, expiresInSeconds - 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expireAccessToken() {
|
||||
RedisUtils.expire(this.accessTokenKey, 0);
|
||||
RedisUtils.expire(this.accessTokenKey, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJsapiTicket() {
|
||||
return RedisUtils.get(this.jsapiTicketKey);
|
||||
return RedisUtils.get(this.jsapiTicketKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCardApiTicket() {
|
||||
return RedisUtils.get(cardapiTicketKey);
|
||||
return RedisUtils.get(cardapiTicketKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTicket(TicketType type) {
|
||||
return RedisUtils.get(getTicketRedisKey(type));
|
||||
return RedisUtils.get(getTicketRedisKey(type));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTicketExpired(TicketType type) {
|
||||
return RedisUtils.getExpire(this.getTicketRedisKey(type)) < 2;
|
||||
return RedisUtils.getExpire(this.getTicketRedisKey(type)) < 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateTicket(TicketType type, String jsapiTicket, int expiresInSeconds) {
|
||||
RedisUtils.set(getTicketRedisKey(type), jsapiTicket, expiresInSeconds - 200);
|
||||
RedisUtils.set(getTicketRedisKey(type), jsapiTicket, expiresInSeconds - 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expireTicket(TicketType type) {
|
||||
RedisUtils.expire(getTicketRedisKey(type), 0);
|
||||
RedisUtils.expire(getTicketRedisKey(type), 0);
|
||||
}
|
||||
|
||||
private String getTicketRedisKey(TicketType type) {
|
||||
|
||||
+7
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
* Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
@@ -81,8 +81,8 @@ public class WxAccountController {
|
||||
@HasPermission("mp_wxaccount_add")
|
||||
public R save(@RequestBody WxAccount wxAccount) {
|
||||
wxAccountService.save(wxAccount);
|
||||
RedisUtils.execute((RedisCallback<Long>) connection ->
|
||||
connection.publish(CacheConstants.MP_REDIS_RELOAD_TOPIC.getBytes(), "重新加载公众号配置".getBytes()));
|
||||
RedisUtils.execute((RedisCallback<Long>) connection ->
|
||||
connection.publish(CacheConstants.MP_REDIS_RELOAD_TOPIC.getBytes(), "重新加载公众号配置".getBytes()));
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@@ -96,8 +96,8 @@ public class WxAccountController {
|
||||
@HasPermission("mp_wxaccount_edit")
|
||||
public R updateById(@RequestBody WxAccount wxAccount) {
|
||||
wxAccountService.updateById(wxAccount);
|
||||
RedisUtils.execute((RedisCallback<Long>) connection ->
|
||||
connection.publish(CacheConstants.MP_REDIS_RELOAD_TOPIC.getBytes(), "重新加载公众号配置".getBytes()));
|
||||
RedisUtils.execute((RedisCallback<Long>) connection ->
|
||||
connection.publish(CacheConstants.MP_REDIS_RELOAD_TOPIC.getBytes(), "重新加载公众号配置".getBytes()));
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@@ -111,8 +111,8 @@ public class WxAccountController {
|
||||
@HasPermission("mp_wxaccount_del")
|
||||
public R removeById(@PathVariable Long id) {
|
||||
wxAccountService.removeById(id);
|
||||
RedisUtils.execute((RedisCallback<Long>) connection ->
|
||||
connection.publish(CacheConstants.MP_REDIS_RELOAD_TOPIC.getBytes(), "重新加载公众号配置".getBytes()));
|
||||
RedisUtils.execute((RedisCallback<Long>) connection ->
|
||||
connection.publish(CacheConstants.MP_REDIS_RELOAD_TOPIC.getBytes(), "重新加载公众号配置".getBytes()));
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
|
||||
-34
@@ -3,7 +3,6 @@ package com.pig4cloud.pigx.mp.controller;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.pig4cloud.pigx.common.api.encrypt.annotation.NoEncrypt;
|
||||
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
|
||||
import com.pig4cloud.pigx.common.core.util.R;
|
||||
import com.pig4cloud.pigx.common.data.tenant.TenantBroker;
|
||||
import com.pig4cloud.pigx.common.security.annotation.Inner;
|
||||
import com.pig4cloud.pigx.common.xss.core.XssCleanIgnore;
|
||||
@@ -11,15 +10,11 @@ import com.pig4cloud.pigx.mp.config.WxMpContextHolder;
|
||||
import com.pig4cloud.pigx.mp.config.WxMpInitConfigRunner;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.chanjar.weixin.common.bean.WxJsapiSignature;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
|
||||
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Binary Wang
|
||||
* @author lengleng
|
||||
@@ -92,35 +87,6 @@ public class WxPortalController {
|
||||
(id) -> handleMessage(appId, requestBody, signature, timestamp, nonce, openid, encType, msgSignature));
|
||||
}
|
||||
|
||||
@XssCleanIgnore
|
||||
@GetMapping("/jssdk-config")
|
||||
public R getJssdkConfig(@PathVariable("appId") String appId,
|
||||
@RequestParam(name = "url", required = false) String url){
|
||||
if (StrUtil.isAllBlank(url)) {
|
||||
throw new IllegalArgumentException("请求参数非法,请核实!");
|
||||
}
|
||||
|
||||
final WxMpService wxService = WxMpInitConfigRunner.getMpServices().get(appId);
|
||||
|
||||
if (wxService == null) {
|
||||
throw new IllegalArgumentException(String.format("未找到对应appid=[%d]的配置,请核实!", appId));
|
||||
}
|
||||
|
||||
try {
|
||||
WxJsapiSignature signature = wxService.createJsapiSignature(url);
|
||||
|
||||
return R.ok(Map.of(
|
||||
"appId", signature.getAppId(),
|
||||
"timestamp", signature.getTimestamp(),
|
||||
"nonceStr", signature.getNonceStr(),
|
||||
"signature", signature.getSignature()
|
||||
));
|
||||
} catch (WxErrorException e) {
|
||||
log.error("获取微信js api出现错误: {}", e.getLocalizedMessage());
|
||||
return R.failed("获取微信js api出现错误: " + e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信消息处理
|
||||
* @param appId 多公众号标志位
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
~ Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
~
|
||||
~ Redistribution and use in source and binary forms, with or without
|
||||
~ modification, are permitted provided that the following conditions are met:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<!--
|
||||
~
|
||||
~ Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
~ Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
~
|
||||
~ Redistribution and use in source and binary forms, with or without
|
||||
~ modification, are permitted provided that the following conditions are met:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<!--
|
||||
~
|
||||
~ Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
~ Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
~
|
||||
~ Redistribution and use in source and binary forms, with or without
|
||||
~ modification, are permitted provided that the following conditions are met:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<!--
|
||||
~
|
||||
~ Copyright (c) 2018-2026, lengleng All rights reserved.
|
||||
~ Copyright (c) 2018-2025, lengleng All rights reserved.
|
||||
~
|
||||
~ Redistribution and use in source and binary forms, with or without
|
||||
~ modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Reference in new issue
Block a user