1
0

feat: 使用liteflow替换自定义的任务执行

This commit is contained in:
2025-09-06 14:54:47 +08:00
parent 550692e281
commit 0ad9d8239c
22 changed files with 763 additions and 428 deletions

72
.idea/compiler.xml generated
View File

@@ -85,78 +85,6 @@
<entry name="$MAVEN_REPOSITORY$/org/easymock/easymock/5.6.0/easymock-5.6.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/objenesis/objenesis/3.4/objenesis-3.4.jar" />
<entry name="$MAVEN_REPOSITORY$/org/javassist/javassist/3.30.2-GA/javassist-3.30.2-GA.jar" />
<entry name="$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-configuration-processor/3.5.0/spring-boot-configuration-processor-3.5.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" />
<entry name="$MAVEN_REPOSITORY$/org/hibernate/orm/hibernate-jpamodelgen/6.6.8.Final/hibernate-jpamodelgen-6.6.8.Final.jar" />
<entry name="$MAVEN_REPOSITORY$/org/hibernate/orm/hibernate-core/6.6.8.Final/hibernate-core-6.6.8.Final.jar" />
<entry name="$MAVEN_REPOSITORY$/jakarta/persistence/jakarta.persistence-api/3.2.0/jakarta.persistence-api-3.2.0.jar" />
<entry name="$MAVEN_REPOSITORY$/jakarta/transaction/jakarta.transaction-api/2.0.1/jakarta.transaction-api-2.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jboss/logging/jboss-logging/3.5.0.Final/jboss-logging-3.5.0.Final.jar" />
<entry name="$MAVEN_REPOSITORY$/org/hibernate/common/hibernate-commons-annotations/7.0.3.Final/hibernate-commons-annotations-7.0.3.Final.jar" />
<entry name="$MAVEN_REPOSITORY$/io/smallrye/jandex/3.2.0/jandex-3.2.0.jar" />
<entry name="$MAVEN_REPOSITORY$/com/fasterxml/classmate/1.5.1/classmate-1.5.1.jar" />
<entry name="$MAVEN_REPOSITORY$/net/bytebuddy/byte-buddy/1.15.11/byte-buddy-1.15.11.jar" />
<entry name="$MAVEN_REPOSITORY$/jakarta/xml/bind/jakarta.xml.bind-api/4.0.0/jakarta.xml.bind-api-4.0.0.jar" />
<entry name="$MAVEN_REPOSITORY$/jakarta/activation/jakarta.activation-api/2.1.0/jakarta.activation-api-2.1.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/glassfish/jaxb/jaxb-runtime/4.0.2/jaxb-runtime-4.0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/glassfish/jaxb/jaxb-core/4.0.2/jaxb-core-4.0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/eclipse/angus/angus-activation/2.0.0/angus-activation-2.0.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/glassfish/jaxb/txw2/4.0.2/txw2-4.0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/com/sun/istack/istack-commons-runtime/4.1.1/istack-commons-runtime-4.1.1.jar" />
<entry name="$MAVEN_REPOSITORY$/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/antlr/antlr4-runtime/4.13.0/antlr4-runtime-4.13.0.jar" />
<entry name="$MAVEN_REPOSITORY$/jakarta/validation/jakarta.validation-api/3.0.2/jakarta.validation-api-3.0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar" />
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-apt/7.0/querydsl-apt-7.0-jpa.jar" />
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-codegen/7.0/querydsl-codegen-7.0.jar" />
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-core/7.0/querydsl-core-7.0.jar" />
<entry name="$MAVEN_REPOSITORY$/io/projectreactor/reactor-core/3.7.6/reactor-core-3.7.6.jar" />
<entry name="$MAVEN_REPOSITORY$/org/reactivestreams/reactive-streams/1.0.4/reactive-streams-1.0.4.jar" />
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-codegen-utils/7.0/querydsl-codegen-utils-7.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/eclipse/jdt/ecj/3.40.0/ecj-3.40.0.jar" />
<entry name="$MAVEN_REPOSITORY$/io/github/classgraph/classgraph/4.8.179/classgraph-4.8.179.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jetbrains/annotations/26.0.2/annotations-26.0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/dev/morphia/morphia/morphia-core/2.5.0/morphia-core-2.5.0.jar" />
<entry name="$MAVEN_REPOSITORY$/io/smallrye/config/smallrye-config/3.10.1/smallrye-config-3.10.1.jar" />
<entry name="$MAVEN_REPOSITORY$/io/smallrye/config/smallrye-config-core/3.10.1/smallrye-config-core-3.10.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/eclipse/microprofile/config/microprofile-config-api/3.1/microprofile-config-api-3.1.jar" />
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-annotation/2.8.0/smallrye-common-annotation-2.8.0.jar" />
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-expression/2.8.0/smallrye-common-expression-2.8.0.jar" />
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-function/2.8.0/smallrye-common-function-2.8.0.jar" />
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-constraint/2.8.0/smallrye-common-constraint-2.8.0.jar" />
<entry name="$MAVEN_REPOSITORY$/io/smallrye/common/smallrye-common-classloader/2.8.0/smallrye-common-classloader-2.8.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/ow2/asm/asm/9.8/asm-9.8.jar" />
<entry name="$MAVEN_REPOSITORY$/io/smallrye/config/smallrye-config-common/3.10.1/smallrye-config-common-3.10.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mongodb/mongodb-driver-sync/5.4.0/mongodb-driver-sync-5.4.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mongodb/bson/5.4.0/bson-5.4.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mongodb/mongodb-driver-core/5.4.0/mongodb-driver-core-5.4.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mongodb/bson-record-codec/5.4.0/bson-record-codec-5.4.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/mongodb/mongodb-driver-legacy/5.4.0/mongodb-driver-legacy-5.4.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar" />
<entry name="$MAVEN_REPOSITORY$/com/github/spotbugs/spotbugs-annotations/4.8.6/spotbugs-annotations-4.8.6.jar" />
<entry name="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/semver4j/semver4j/5.6.0/semver4j-5.6.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/jsoup/jsoup/1.18.3/jsoup-1.18.3.jar" />
<entry name="$MAVEN_REPOSITORY$/org/hibernate/orm/hibernate-envers/7.0.0.Beta1/hibernate-envers-7.0.0.Beta1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/hibernate/models/hibernate-models/0.8.6/hibernate-models-0.8.6.jar" />
<entry name="$MAVEN_REPOSITORY$/io/github/openfeign/querydsl/querydsl-core/7.0/querydsl-core-7.0-tests.jar" />
<entry name="$MAVEN_REPOSITORY$/org/joda/joda-money/2.0.2/joda-money-2.0.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter/5.13.1/junit-jupiter-5.13.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-api/5.13.1/junit-jupiter-api-5.13.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-commons/1.13.1/junit-platform-commons-1.13.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-params/5.13.1/junit-jupiter-params-5.13.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/junit/jupiter/junit-jupiter-engine/5.13.1/junit-jupiter-engine-5.13.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/junit/platform/junit-platform-engine/1.13.1/junit-platform-engine-1.13.1.jar" />
<entry name="$MAVEN_REPOSITORY$/org/assertj/assertj-core/3.27.3/assertj-core-3.27.3.jar" />
<entry name="$MAVEN_REPOSITORY$/org/junit/vintage/junit-vintage-engine/5.13.1/junit-vintage-engine-5.13.1.jar" />
<entry name="$MAVEN_REPOSITORY$/junit/junit/4.13.2/junit-4.13.2.jar" />
<entry name="$MAVEN_REPOSITORY$/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar" />
<entry name="$MAVEN_REPOSITORY$/org/easymock/easymock/5.6.0/easymock-5.6.0.jar" />
<entry name="$MAVEN_REPOSITORY$/org/objenesis/objenesis/3.4/objenesis-3.4.jar" />
<entry name="$MAVEN_REPOSITORY$/org/javassist/javassist/3.30.2-GA/javassist-3.30.2-GA.jar" />
</processorPath>
<module name="leopard-server" />
</profile>

View File

@@ -39,6 +39,15 @@
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>

View File

@@ -2,14 +2,15 @@ package com.lanyuanxiaoyao.leopard.server.controller;
import com.lanyuanxiaoyao.leopard.server.entity.Stock;
import com.lanyuanxiaoyao.leopard.server.entity.Task;
import com.lanyuanxiaoyao.leopard.server.entity.TaskTemplate;
import com.lanyuanxiaoyao.leopard.server.service.StockService;
import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Trigger;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -82,9 +83,11 @@ public class CommonOptionsController {
);
private final StockService stockService;
private final TaskTemplateService taskTemplateService;
public CommonOptionsController(StockService stockService) {
public CommonOptionsController(StockService stockService, TaskTemplateService taskTemplateService) {
this.stockService = stockService;
this.taskTemplateService = taskTemplateService;
}
@GetMapping("/options/{name}")
@@ -107,9 +110,10 @@ public class CommonOptionsController {
new Option("已退市", false)
)
);
case "task_template_type" -> GlobalResponse.responseSuccess(
Arrays.stream(TaskTemplate.Type.values())
.map(type -> new Option(type.getChineseName(), type.name()))
case "task_template_id" -> GlobalResponse.responseSuccess(
taskTemplateService.list()
.stream()
.map(template -> new Option(template.getName(), template.getId()))
.toList()
);
default -> GlobalResponse.responseSuccess(List.of());
@@ -138,10 +142,15 @@ public class CommonOptionsController {
.toList(),
field
));
case "task_template_type" -> GlobalResponse.responseSuccess(buildMapping(
Arrays.stream(TaskTemplate.Type.values())
.map(type -> new Mapping(type.name(), type.getChineseName()))
.toList(),
case "trigger_status" -> GlobalResponse.responseSuccess(buildMapping(
List.of(
new Mapping(Trigger.TriggerState.NONE.name(), ""),
new Mapping(Trigger.TriggerState.NORMAL.name(), "正常"),
new Mapping(Trigger.TriggerState.PAUSED.name(), "暂停"),
new Mapping(Trigger.TriggerState.COMPLETE.name(), "完成"),
new Mapping(Trigger.TriggerState.ERROR.name(), "错误"),
new Mapping(Trigger.TriggerState.BLOCKED.name(), "阻塞")
),
field
));
default -> GlobalResponse.responseSuccess(Map.of());

View File

@@ -0,0 +1,90 @@
package com.lanyuanxiaoyao.leopard.server.controller;
import com.lanyuanxiaoyao.leopard.server.service.QuartzService;
import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import java.time.LocalDateTime;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("task_schedule")
public class QuartzController {
private final QuartzService quartzService;
private final TaskTemplateService taskTemplateService;
public QuartzController(QuartzService quartzService, TaskTemplateService taskTemplateService) {
this.quartzService = quartzService;
this.taskTemplateService = taskTemplateService;
}
@PostMapping("save")
public GlobalResponse<Object> save(@RequestBody SaveItem item) throws SchedulerException {
quartzService.save(item.templateId(), item.cron());
return GlobalResponse.responseSuccess();
}
@GetMapping("list")
public GlobalResponse<List<ListItem>> list() throws SchedulerException {
var list = quartzService.list()
.stream()
.map(task -> {
var template = taskTemplateService.detail(task.templateId());
return new ListItem(
task.key(),
template.getName(),
template.getDescription(),
task.cron(),
task.status(),
task.previousFireTime(),
task.nextFireTime()
);
})
.toList();
return GlobalResponse.responseSuccess(list);
}
@GetMapping("pause/{key}")
public GlobalResponse<Object> pause(@PathVariable("key") String key) throws SchedulerException {
quartzService.pause(key);
return GlobalResponse.responseSuccess();
}
@GetMapping("resume/{key}")
public GlobalResponse<Object> resume(@PathVariable("key") String key) throws SchedulerException {
quartzService.resume(key);
return GlobalResponse.responseSuccess();
}
@GetMapping("remove/{key}")
public GlobalResponse<Object> remove(@PathVariable("key") String key) throws SchedulerException {
quartzService.remove(key);
return GlobalResponse.responseSuccess();
}
public record SaveItem(
Long templateId,
String cron
) {
}
public record ListItem(
String key,
String templateName,
String templateDescription,
String cron,
Trigger.TriggerState status,
LocalDateTime previousFireTime,
LocalDateTime nextFireTime
) {
}
}

View File

@@ -5,6 +5,7 @@ import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
@@ -36,7 +37,7 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
@PostMapping("execute")
public GlobalResponse<Object> execute(@RequestBody ExecuteRequest request) {
taskService.execute(request.templateId());
taskService.execute(request.templateId(), request.params());
return GlobalResponse.responseSuccess();
}
@@ -93,6 +94,6 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
) {
}
public record ExecuteRequest(Long templateId) {
public record ExecuteRequest(Long templateId, Map<String, Object> params) {
}
}

View File

@@ -1,7 +1,6 @@
package com.lanyuanxiaoyao.leopard.server.controller;
import com.lanyuanxiaoyao.leopard.server.entity.TaskTemplate;
import com.lanyuanxiaoyao.leopard.server.entity.templates.ClassTaskTemplate;
import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import java.util.function.Function;
@@ -20,26 +19,22 @@ public class TaskTemplateController extends SimpleControllerSupport<TaskTemplate
@Override
protected Function<Item, TaskTemplate> saveItemMapper() {
return item -> {
return switch (item.type) {
case CLASS -> {
var template = new ClassTaskTemplate();
var template = new TaskTemplate();
template.setId(item.id());
template.setName(item.name());
template.setDescription(item.description());
template.setClazz(item.clazz());
yield template;
}
};
template.setChain(item.chain());
return template;
};
}
private Item convert(TaskTemplate template) {
return switch (template.getType()) {
case CLASS -> {
ClassTaskTemplate classTaskTemplate = (ClassTaskTemplate) template;
yield new Item(classTaskTemplate.getId(), classTaskTemplate.getName(), classTaskTemplate.getDescription(), classTaskTemplate.getType(), classTaskTemplate.getClazz());
}
};
return new Item(
template.getId(),
template.getName(),
template.getDescription(),
template.getChain()
);
}
@Override
@@ -56,8 +51,7 @@ public class TaskTemplateController extends SimpleControllerSupport<TaskTemplate
Long id,
String name,
String description,
TaskTemplate.Type type,
String clazz
String chain
) {
}
}

View File

@@ -1,15 +1,11 @@
package com.lanyuanxiaoyao.leopard.server.entity;
import com.lanyuanxiaoyao.leopard.server.Constants;
import com.lanyuanxiaoyao.leopard.server.entity.base.SimpleEnum;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@@ -29,20 +25,11 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Table(name = Constants.DATABASE_PREFIX + "task_template")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class TaskTemplate extends SimpleEntity {
public class TaskTemplate extends SimpleEntity {
@Column(nullable = false)
private String name;
@Column(nullable = false, length = 500)
private String description;
public abstract Type getType();
@Getter
@AllArgsConstructor
public enum Type implements SimpleEnum {
CLASS("类任务");
private final String chineseName;
}
@Column(nullable = false)
private String chain;
}

View File

@@ -1,36 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.entity.templates;
import com.lanyuanxiaoyao.leopard.server.Constants;
import com.lanyuanxiaoyao.leopard.server.entity.TaskTemplate;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SoftDelete;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@Setter
@Getter
@ToString(callSuper = true)
@FieldNameConstants
@Entity
@SoftDelete
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Table(name = Constants.DATABASE_PREFIX + "task_template_class")
public class ClassTaskTemplate extends TaskTemplate {
@Column(nullable = false)
private String clazz;
@Override
public Type getType() {
return Type.CLASS;
}
}

View File

@@ -0,0 +1,106 @@
package com.lanyuanxiaoyao.leopard.server.service;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class QuartzService {
private final Scheduler scheduler;
public QuartzService(Scheduler scheduler) {
this.scheduler = scheduler;
}
public List<QuartzTask> list() throws SchedulerException {
var tasks = new ArrayList<QuartzTask>();
for (var key : scheduler.getJobKeys(GroupMatcher.anyGroup())) {
var detail = scheduler.getJobDetail(key);
var trigger = (CronTrigger) scheduler.getTriggersOfJob(key).get(0);
tasks.add(new QuartzTask(
detail.getKey().getName(),
detail.getJobDataMap().getLong("template_id"),
trigger.getCronExpression(),
scheduler.getTriggerState(trigger.getKey()),
ObjectUtil.isNull(trigger.getPreviousFireTime()) ? null : LocalDateTime.ofInstant(trigger.getPreviousFireTime().toInstant(), ZoneId.systemDefault()),
ObjectUtil.isNull(trigger.getNextFireTime()) ? null : LocalDateTime.ofInstant(trigger.getNextFireTime().toInstant(), ZoneId.systemDefault())
));
}
return tasks;
}
public void save(Long templateId, String cron) throws SchedulerException {
var detail = JobBuilder.newJob(TaskExecutionJob.class)
.withIdentity("task_execution_" + IdUtil.fastUUID())
.usingJobData("template_id", templateId)
.storeDurably()
.build();
var trigger = TriggerBuilder.newTrigger()
.forJob(detail)
.withIdentity("task_execution_" + IdUtil.fastUUID())
.withSchedule(
CronScheduleBuilder.cronSchedule(cron)
.withMisfireHandlingInstructionDoNothing()
)
.build();
scheduler.scheduleJob(detail, trigger);
}
public void pause(String jobKey) throws SchedulerException {
scheduler.pauseJob(JobKey.jobKey(jobKey));
}
public void resume(String jobKey) throws SchedulerException {
scheduler.resumeJob(JobKey.jobKey(jobKey));
}
public void remove(String jobKey) throws SchedulerException {
pause(jobKey);
scheduler.deleteJob(JobKey.jobKey(jobKey));
}
@Slf4j
public static class TaskExecutionJob extends QuartzJobBean {
private final TaskService taskService;
public TaskExecutionJob(TaskService taskService) {
this.taskService = taskService;
}
@Override
protected void executeInternal(JobExecutionContext context) {
var dataMap = context.getMergedJobDataMap();
var templateId = dataMap.getLong("template_id");
var parms = (Map<String, Object>) dataMap.getOrDefault("params", Map.of());
taskService.execute(templateId, parms);
}
}
public record QuartzTask(
String key,
Long templateId,
String cron,
Trigger.TriggerState status,
LocalDateTime previousFireTime,
LocalDateTime nextFireTime
) {
}
}

View File

@@ -1,15 +1,12 @@
package com.lanyuanxiaoyao.leopard.server.service;
import com.lanyuanxiaoyao.leopard.server.entity.Task;
import com.lanyuanxiaoyao.leopard.server.entity.templates.ClassTaskTemplate;
import com.lanyuanxiaoyao.leopard.server.repository.TaskRepository;
import com.lanyuanxiaoyao.leopard.server.service.task.TaskRunner;
import com.lanyuanxiaoyao.leopard.server.service.task.TaskMonitorNodes;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import com.yomahub.liteflow.core.FlowExecutor;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
/**
@@ -19,26 +16,18 @@ import org.springframework.stereotype.Service;
@Slf4j
@Service
public class TaskService extends SimpleServiceSupport<Task> {
private final ApplicationContext context;
private final TaskTemplateService taskTemplateService;
private final ExecutorService executors = Executors.newFixedThreadPool(50);
private final FlowExecutor flowExecutor;
public TaskService(TaskRepository repository, ApplicationContext context, TaskTemplateService taskTemplateService) {
public TaskService(TaskRepository repository, TaskTemplateService taskTemplateService, @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") FlowExecutor flowExecutor) {
super(repository);
this.context = context;
this.taskTemplateService = taskTemplateService;
this.flowExecutor = flowExecutor;
}
public void execute(Long templateId) {
public void execute(Long templateId, Map<String, Object> params) {
var template = taskTemplateService.detail(templateId);
switch (template.getType()) {
case CLASS -> {
ClassTaskTemplate classTaskTemplate = (ClassTaskTemplate) template;
var runner = context.getBean(classTaskTemplate.getClazz(), TaskRunner.class);
executors.submit(() -> {
runner.runTask(classTaskTemplate, Map.of());
});
}
}
var context = new TaskMonitorNodes.TaskMonitorContext(template);
flowExecutor.execute2Future(template.getChain(), params, context);
}
}

View File

@@ -0,0 +1,79 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.leopard.server.entity.Task;
import com.lanyuanxiaoyao.leopard.server.entity.TaskTemplate;
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.annotation.LiteflowFact;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import java.time.LocalDateTime;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@LiteflowComponent
public class TaskMonitorNodes {
private final TaskService taskService;
public TaskMonitorNodes(TaskService taskService) {
this.taskService = taskService;
}
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "task_start", nodeName = "任务开始", nodeType = NodeTypeEnum.COMMON)
public void taskStart(NodeComponent node) {
var context = node.getContextBean(TaskMonitorContext.class);
if (ObjectUtil.isNotNull(context)) {
var task = new Task();
task.setName(context.getTemplate().getName());
task.setDescription(context.getTemplate().getDescription());
task.setStatus(Task.Status.RUNNING);
task.setLaunchedTime(LocalDateTime.now());
var taskId = taskService.save(task);
context.setTaskId(taskId);
}
}
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "task_end", nodeName = "任务结束", nodeType = NodeTypeEnum.COMMON)
public void taskEnd(NodeComponent node, @LiteflowFact("taskId") Long taskId) {
if (ObjectUtil.isNotNull(taskId)) {
var task = taskService.detail(taskId);
task.setStatus(Task.Status.SUCCESS);
task.setFinishedTime(LocalDateTime.now());
var result = node.<String>getContextValue("taskResult");
if (StrUtil.isNotBlank(result)) {
task.setResult(result);
}
taskService.save(task);
}
}
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "task_error", nodeName = "任务错误", nodeType = NodeTypeEnum.COMMON)
public void taskError(NodeComponent node, @LiteflowFact("taskId") Long taskId) {
if (ObjectUtil.isNotNull(taskId)) {
var task = taskService.detail(taskId);
task.setStatus(Task.Status.FAILURE);
task.setFinishedTime(LocalDateTime.now());
var exception = node.getSlot().getException();
if (ObjectUtil.isNotNull(exception)) {
task.setError(exception.getMessage());
}
taskService.save(task);
}
}
@Data
public static final class TaskMonitorContext {
private TaskTemplate template;
private Long taskId;
private String taskResult;
public TaskMonitorContext(TaskTemplate template) {
this.template = template;
}
}
}

View File

@@ -1,41 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.leopard.server.entity.Task;
import com.lanyuanxiaoyao.leopard.server.entity.TaskTemplate;
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import java.time.LocalDateTime;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public abstract class TaskRunner {
private final TaskService taskService;
protected TaskRunner(TaskService taskService) {
this.taskService = taskService;
}
public void runTask(TaskTemplate template, Map<String, Object> params) {
var task = new Task();
task.setName(template.getName());
task.setDescription(template.getDescription());
task.setStatus(Task.Status.RUNNING);
task.setLaunchedTime(LocalDateTime.now());
task.setId(taskService.save(task));
try {
var result = run(params);
task.setStatus(Task.Status.SUCCESS);
if (StrUtil.isNotBlank(result)) {
task.setResult(result);
}
} catch (Exception e) {
task.setStatus(Task.Status.FAILURE);
task.setError(e.getMessage());
}
task.setFinishedTime(LocalDateTime.now());
taskService.save(task);
}
abstract String run(Map<String, Object> params);
}

View File

@@ -4,25 +4,25 @@ import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.leopard.server.entity.Stock;
import com.lanyuanxiaoyao.leopard.server.service.StockService;
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
import java.util.Map;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import jakarta.transaction.Transactional;
import java.util.stream.Collectors;
import org.springframework.stereotype.Component;
@Component("com.lanyuanxiaoyao.leopard.server.service.task.UpdateStockInformationTask")
public class UpdateStockInformationTask extends TaskRunner {
@LiteflowComponent("update_stock_information")
public class UpdateStockInformationNode extends NodeComponent {
private final StockService stockService;
private final TuShareService tuShareService;
public UpdateStockInformationTask(TaskService taskService, StockService stockService, TuShareService tuShareService) {
super(taskService);
public UpdateStockInformationNode(StockService stockService, TuShareService tuShareService) {
this.stockService = stockService;
this.tuShareService = tuShareService;
}
@Transactional(rollbackOn = Throwable.class)
@Override
public String run(Map<String, Object> params) {
public void process() throws Exception {
var stocks = stockService.list();
var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
tuShareService.stockList()
@@ -54,6 +54,5 @@ public class UpdateStockInformationTask extends TaskRunner {
}
});
stockService.save(stocks);
return null;
}
}

View File

@@ -15,5 +15,15 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
generate-ddl: true
quartz:
wait-for-jobs-to-complete-on-shutdown: true
startup-delay: 30s
job-store-type: jdbc
jdbc:
platform: mysql
fenix:
print-banner: false
liteflow:
print-banner: false
rule-source: flow.xml
check-node-exists: false

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flow PUBLIC "liteflow" "https://liteflow.cc/liteflow.dtd">
<flow>
<chain id="update_stock_information">
CATCH(THEN(task_start, update_stock_information, task_end)).DO(task_error)
</chain>
</flow>

View File

@@ -11,6 +11,8 @@ import TaskList from './pages/task/TaskList.tsx'
import TaskAdd from './pages/task/TaskAdd.tsx'
import TaskTemplateList from './pages/task/TaskTemplateList.tsx'
import TaskTemplateSave from './pages/task/TaskTemplateSave.tsx'
import TaskScheduleList from './pages/task/TaskScheduleList.tsx'
import TaskScheduleSave from './pages/task/TaskScheduleSave.tsx'
const routes: RouteObject[] = [
{
@@ -66,6 +68,19 @@ const routes: RouteObject[] = [
},
],
},
{
path: 'schedule',
children: [
{
path: 'list',
Component: TaskScheduleList,
},
{
path: 'save',
Component: TaskScheduleSave,
},
],
},
],
},
{

View File

@@ -2,8 +2,8 @@ import {
DeploymentUnitOutlined,
InfoCircleOutlined,
MoneyCollectOutlined,
UnorderedListOutlined
} from "@ant-design/icons";
UnorderedListOutlined,
} from '@ant-design/icons'
import {type AppItemProps, ProLayout} from '@ant-design/pro-components'
import {ConfigProvider} from 'antd'
import {dateFormat} from 'licia'
@@ -52,8 +52,12 @@ const menus = {
{
path: '/task/template/list',
name: '任务模板',
}
]
},
{
path: '/task/schedule/list',
name: '定时任务',
},
],
},
{
path: '/test',

View File

@@ -0,0 +1,125 @@
import React from 'react'
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, remoteMappings, time} from '../../util/amis.tsx'
import {useNavigate} from 'react-router'
function TaskScheduleList() {
const navigate = useNavigate()
return (
<div className="task-schedule-list">
{amisRender(
{
type: 'page',
title: '定时任务',
body: [
{
type: 'crud',
api: `get:${commonInfo.baseUrl}/task_schedule/list`,
...crudCommonOptions(),
...paginationTemplate(
15,
undefined,
[
{
type: 'action',
label: '',
icon: 'fa fa-plus',
tooltip: '添加定时任务',
tooltipPlacement: 'top',
onEvent: {
click: {
actions: [
{
actionType: 'custom',
// @ts-ignore
script: (context, action, event) => {
navigate('/task/schedule/save')
},
},
],
},
},
},
],
),
columns: [
{
name: 'templateName',
label: '模板名称',
width: 150,
},
{
name: 'templateDescription',
label: '模板描述',
},
{
name: 'cron',
label: '定时策略',
width: 150,
},
{
name: 'status',
label: '状态',
width: 100,
...remoteMappings('trigger_status', 'status'),
},
{
name: 'previousFireTime',
label: '上次启动时间',
width: 150,
align: 'center',
...time('previousFireTime'),
},
{
name: 'nextFireTime',
label: '下次启动时间',
width: 150,
align: 'center',
...time('nextFireTime'),
},
{
type: 'operation',
label: '操作',
width: 150,
buttons: [
{
visibleOn: 'status !== \'PAUSED\' ',
type: 'action',
label: '暂停',
level: 'link',
actionType: 'ajax',
api: `get:${commonInfo.baseUrl}/task_schedule/pause/\${key}`,
confirmText: '确认暂停定时任务<span class="text-lg font-bold mx-2">${templateName}</span>',
confirmTitle: '暂停',
},
{
visibleOn: 'status === \'PAUSED\' ',
type: 'action',
label: '恢复',
level: 'link',
actionType: 'ajax',
api: `get:${commonInfo.baseUrl}/task_schedule/resume/\${key}`,
confirmText: '确认恢复定时任务<span class="text-lg font-bold mx-2">${templateName}</span>',
confirmTitle: '恢复',
},
{
className: 'text-danger',
type: 'action',
label: '删除',
level: 'link',
actionType: 'ajax',
api: `get:${commonInfo.baseUrl}/task_schedule/remove/\${key}`,
confirmText: '确认删除定时任务<span class="text-lg font-bold mx-2">${templateName}</span>',
confirmTitle: '删除',
},
],
},
],
},
],
},
)}
</div>
)
}
export default React.memo(TaskScheduleList)

View File

@@ -0,0 +1,74 @@
import React from 'react'
import {amisRender, commonInfo, remoteOptions} from '../../util/amis.tsx'
import {useNavigate} from 'react-router'
function TaskScheduleSave() {
const navigate = useNavigate()
return (
<div className="task-template-save">
{amisRender(
{
type: 'page',
title: '任务模板添加',
body: [
{
debug: commonInfo.debug,
type: 'form',
api: `post:${commonInfo.baseUrl}/task_schedule/save`,
wrapWithPanel: false,
mode: 'horizontal',
labelAlign: 'left',
onEvent: {
submitSucc: {
actions: [
{
actionType: 'custom',
// @ts-ignore
script: (context, action, event) => {
navigate(-1)
},
},
],
},
},
body: [
{
name: 'templateId',
label: '名称',
required: true,
selectFirst: true,
...remoteOptions('select', 'task_template_id'),
},
{
type: 'input-text',
name: 'cron',
label: '定时策略',
required: true,
clearable: true,
},
{
type: 'button-toolbar',
buttons: [
{
type: 'action',
label: '提交',
actionType: 'submit',
level: 'primary',
},
{
type: 'action',
label: '重置',
actionType: 'reset',
},
],
},
],
},
],
},
)}
</div>
)
}
export default React.memo(TaskScheduleSave)

View File

@@ -1,5 +1,5 @@
import React from 'react'
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, remoteMappings} from '../../util/amis.tsx'
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate} from '../../util/amis.tsx'
import {useNavigate} from 'react-router'
function TaskTemplateList() {
@@ -60,12 +60,6 @@ function TaskTemplateList() {
name: 'description',
label: '描述',
},
{
name: 'type',
label: '类型',
width: 100,
...remoteMappings('task_template_type', 'type'),
},
{
type: 'operation',
label: '操作',

View File

@@ -1,5 +1,5 @@
import React from 'react'
import {amisRender, commonInfo, remoteOptions} from '../../util/amis.tsx'
import {amisRender, commonInfo} from '../../util/amis.tsx'
import {useNavigate, useParams} from 'react-router'
function TaskTemplateSave() {
@@ -31,8 +31,8 @@ function TaskTemplateSave() {
navigate(-1)
},
},
]
}
],
},
},
body: [
{
@@ -43,29 +43,21 @@ function TaskTemplateSave() {
type: 'input-text',
name: 'name',
label: '名称',
require: true,
required: true,
clearable: true,
},
{
type: 'textarea',
name: 'description',
label: '描述',
require: true,
required: true,
clearable: true,
},
{
name: 'type',
label: '任务类型',
require: true,
selectFirst: true,
...remoteOptions('select', 'task_template_type'),
},
{
visibleOn: 'type === \'CLASS\'',
type: 'input-text',
name: 'clazz',
label: '类路径',
require: true,
name: 'chain',
label: '流程编号',
required: true,
clearable: true,
},
{

View File

@@ -25,7 +25,7 @@
<eclipse-collections.version>13.0.0</eclipse-collections.version>
<hutool.version>5.8.39</hutool.version>
<liteflow.version>2.13.2</liteflow.version>
<liteflow.version>2.15.0</liteflow.version>
</properties>
<dependencies>