mirror of
http://git.pig4cloud.com/pig/pigx.git
synced 2026-11-14 22:29: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
+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