feat: add new models and update application structure for job scheduling

This commit is contained in:
PIGCLOUD committed 2026-05-01 23:59:12 +08:00
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
+12 -9
View File
@@ -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"/>
@@ -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)) {
@@ -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]);
}
}));
}
}
@@ -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);
@@ -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;
/**
@@ -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);
@@ -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)) {
@@ -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);
}
@@ -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,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");
@@ -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
@@ -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);
@@ -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.
@@ -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)) {
@@ -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);
}
}
@@ -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();
@@ -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);
}
/**
@@ -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);
}
}
@@ -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:
@@ -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:
@@ -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);
}
});
}
}
@@ -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() {
@@ -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;
@@ -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();
}
}
@@ -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));
}
}
@@ -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;
}
@@ -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已发布
*/
@@ -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;
/**
* 创建时间
*/
@@ -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;
}
@@ -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());
}
}
@@ -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("执行定时任务日志");
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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) {
@@ -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:
@@ -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) {
@@ -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();
}
@@ -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: