1
0

refactor: 重构任务执行

This commit is contained in:
2025-09-24 22:34:16 +08:00
parent 8011a4f2cb
commit 01690bbcd6
35 changed files with 610 additions and 1137 deletions

View File

@@ -25,6 +25,15 @@
<artifactId>hutool-core</artifactId>
</dependency>
<dependency>
<groupId>io.github.ralfkonrad.quantlib_for_maven</groupId>
<artifactId>quantlib</artifactId>
</dependency>
<dependency>
<groupId>org.ta4j</groupId>
<artifactId>ta4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-ant</artifactId>

View File

@@ -61,7 +61,7 @@ public class Task extends SimpleEntity {
private Status status = Status.RUNNING;
@Column(nullable = false)
@Comment("任务进度")
private Integer step = 0;
private Double step = 0.0;
@Comment("任务开始时间")
private LocalDateTime launchedTime;
@Comment("任务结束时间")

View File

@@ -1,39 +0,0 @@
package com.lanyuanxiaoyao.leopard.core.entity;
import com.lanyuanxiaoyao.leopard.core.Constants;
import com.lanyuanxiaoyao.service.template.entity.SimpleEntity;
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.springframework.data.jpa.domain.support.AuditingEntityListener;
@Setter
@Getter
@ToString(callSuper = true)
@FieldNameConstants
@Entity
@DynamicUpdate
@DynamicInsert
@EntityListeners(AuditingEntityListener.class)
@Table(name = Constants.DATABASE_PREFIX + "task_template")
public class TaskTemplate extends SimpleEntity {
@Column(nullable = false)
private String name;
@Column(nullable = false, length = 500)
private String description;
@Column(nullable = false)
private String application;
@Column(nullable = false)
private String chain;
@Column(nullable = false)
private String expression;
@Column(nullable = false)
private String expressionEl;
}

View File

@@ -5,6 +5,7 @@ import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
/**
* @author lanyuanxiaoyao
@@ -16,7 +17,8 @@ public interface TaskRepository extends SimpleRepository<Task> {
@Query("update Task task set task.status = com.lanyuanxiaoyao.leopard.core.entity.Task.Status.FAILURE where task.status = com.lanyuanxiaoyao.leopard.core.entity.Task.Status.RUNNING")
void updateAllRunningTaskToFailure();
@Transactional(rollbackFor = Throwable.class)
@Modifying
@Query("update Task task set task.step = ?1 where task.id = ?2")
void updateStepById(Integer step, Long id);
@Query("update Task task set task.step = ?2 where task.id = ?1")
void updateStepById(Long id, Double step);
}

View File

@@ -1,9 +0,0 @@
package com.lanyuanxiaoyao.leopard.core.repository;
import com.lanyuanxiaoyao.leopard.core.entity.TaskTemplate;
import com.lanyuanxiaoyao.service.template.repository.SimpleRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface TaskTemplateRepository extends SimpleRepository<TaskTemplate> {
}

View File

@@ -0,0 +1,94 @@
package com.lanyuanxiaoyao.leopard.core.service;
import cn.hutool.core.util.ObjectUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.springframework.stereotype.Service;
/**
* 股票评估
*
* @author lanyuanxiaoyao
* @version 20250924
*/
@Slf4j
@Service
public class AssessmentService {
private final IndustryService industryService;
private final DailyRepository dailyRepository;
public AssessmentService(IndustryService industryService, DailyRepository dailyRepository) {
this.industryService = industryService;
this.dailyRepository = dailyRepository;
}
public Set<Result> assess(Set<Stock> stocks, int year) {
if (ObjectUtil.isNotEmpty(stocks)) {
var industries = stocks
.stream()
.map(Stock::getIndustry)
.collect(Collectors.toSet());
var topChange = industryService.topChange(year, industries, stocks);
var dailyMap = dailyRepository.findAll(
QDaily.daily.tradeDate.year().eq(year)
.and(QDaily.daily.stock.in(stocks))
)
.stream()
.collect(Collectors.groupingBy(Daily::getStock));
return stocks
.stream()
.filter(stock -> {
if (!dailyMap.containsKey(stock) || ObjectUtil.isEmpty(dailyMap.get(stock))) {
log.warn("Cannot find daily data in {} for {}", year, stock.getCode());
return false;
}
return true;
})
.map(stock -> {
var dailies = dailyMap.get(stock)
.stream()
.sorted(Comparator.comparing(Daily::getTradeDate))
.toList();
var change = getChange(dailies);
var std = getStd(dailies);
var industryTop = topChange.getOrDefault(new IndustryService.IndustryYearlyKey(stock.getIndustry(), year), 0.0);
return new Result(stock, change, std, industryTop);
})
.collect(Collectors.toSet());
}
return Set.of();
}
private double getChange(List<Daily> dailies) {
return (dailies.getLast().getHfqClose() - dailies.getFirst().getHfqClose()) / dailies.getFirst().getHfqClose();
}
private double getStd(List<Daily> dailies) {
var statistics = new DescriptiveStatistics();
dailies.forEach(daily -> statistics.addValue(daily.getHfqClose()));
return statistics.getStandardDeviation();
}
public record Result(Stock stock, double change, double std, double industryTop) {
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Result result = (Result) o;
return stock.equals(result.stock);
}
@Override
public int hashCode() {
return stock.hashCode();
}
}
}

View File

@@ -0,0 +1,93 @@
package com.lanyuanxiaoyao.leopard.core.service;
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 计算行业相关指标
*
* @author lanyuanxiaoyao
* @version 20250924
*/
@Slf4j
@Service
public class IndustryService {
private final StockRepository stockRepository;
private final DailyRepository dailyRepository;
public IndustryService(StockRepository stockRepository, DailyRepository dailyRepository) {
this.stockRepository = stockRepository;
this.dailyRepository = dailyRepository;
}
public Map<IndustryYearlyKey, Double> topChange(int year) {
return topChange(year, null, null);
}
public Map<IndustryYearlyKey, Double> topChange(int year, Set<String> includeIndustries) {
return topChange(year, includeIndustries, null);
}
public Map<IndustryYearlyKey, Double> topChange(int year, Set<String> includeIndustries, Set<Stock> includeStocks) {
return topChange(year, year, includeIndustries, includeStocks);
}
public Map<IndustryYearlyKey, Double> topChange(int startYear, int endYear) {
return topChange(startYear, endYear, null, null);
}
public Map<IndustryYearlyKey, Double> topChange(int startYear, int endYear, Set<String> includeIndustries) {
return topChange(startYear, endYear, includeIndustries, null);
}
public Map<IndustryYearlyKey, Double> topChange(int startYear, int endYear, Set<String> includeIndustries, Set<Stock> includeStocks) {
return stockRepository.findDistinctIndustries()
.parallelStream()
.filter(industry -> includeIndustries == null || includeIndustries.contains(industry))
.flatMap(industry -> {
var keys = new ArrayList<IndustryYearlyKey>();
for (int year = startYear; year <= endYear; year++) {
keys.add(new IndustryYearlyKey(industry, year));
}
return keys.stream();
})
.map(key -> {
log.info("计算行业 {} {} 年度涨跌幅", key.industry(), key.year());
var maxChange = dailyRepository
.findAll(
QDaily.daily.stock.industry.eq(key.industry())
.and(QDaily.daily.stock.in(includeStocks))
.and(QDaily.daily.tradeDate.year().eq(key.year())),
QDaily.daily.tradeDate.asc()
)
.stream()
.collect(Collectors.groupingBy(Daily::getStock))
.values()
.stream()
.mapToDouble(dailies -> {
var dailiesSorted = dailies
.stream()
.sorted(Comparator.comparing(Daily::getTradeDate))
.toList();
return (dailiesSorted.getLast().getHfqClose() - dailiesSorted.getFirst().getHfqClose()) / dailiesSorted.getFirst().getHfqClose();
})
.max()
.orElse(0.0);
return Map.entry(key, maxChange);
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
public record IndustryYearlyKey(String industry, int year) {
}
}

View File

@@ -1,4 +1,4 @@
package com.lanyuanxiaoyao.leopard.strategy;
package com.lanyuanxiaoyao.leopard.core.service.selector;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
@@ -6,44 +6,39 @@ import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
import com.lanyuanxiaoyao.leopard.core.entity.QStock;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import java.time.LocalDate;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.stereotype.Service;
/**
* 金字塔选股
*
* @author lanyuanxiaoyao
* @version 20250917
* @version 20250924
*/
@Slf4j
@LiteflowComponent("pyramid_stock_selector")
public class PyramidStockSelector extends NodeComponent {
@Service
public class PyramidStockSelector implements StockSelector<PyramidStockSelector.Request> {
private final StockRepository stockRepository;
public PyramidStockSelector(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
@Transactional(rollbackFor = Throwable.class)
@Override
public void process() {
var assessment = getContextBean(StockAssessmentNode.StockAssessment.class);
public Set<Candidate> select(Request request) {
// 选择至少有最近5年财报的股票
// 有点奇怪001400.SZ有近5年的财报但资料显示是2025年才上市的
var stocks = stockRepository.findAll(QStock.stock.listedDate.before(LocalDate.of(assessment.year(), 1, 1)));
log.info("Year: {} Stock: {}", assessment.year(), stocks.size());
var stocks = stockRepository.findAll(QStock.stock.listedDate.before(LocalDate.of(request.year(), 1, 1)));
log.info("Year: {} Stock: {}", request.year(), stocks.size());
var scores = stocks.stream().collect(Collectors.toMap(stock -> stock, code -> 0));
for (Stock stock : stocks) {
var recentIndicators = stock.getIndicators()
.stream()
.filter(indicator -> indicator.getYear() < assessment.year())
.filter(indicator -> indicator.getYear() < request.year())
.sorted((a, b) -> b.getYear() - a.getYear())
.limit(5)
.toList();
@@ -212,13 +207,17 @@ public class PyramidStockSelector extends NodeComponent {
}
scores.put(stock, scores.get(stock) + cashAscendingScore);
}
var first50 = scores.entrySet()
return scores.entrySet()
.stream()
.sorted((e1, e2) -> e2.getValue() - e1.getValue())
.limit(50)
.map(Map.Entry::getKey)
.limit(request.limit())
.map(entry -> new Candidate(entry.getKey(), entry.getValue()))
.collect(Collectors.toSet());
}
assessment.stocks().addAll(first50);
public record Request(int year, int limit) {
public Request(int year) {
this(year, 50);
}
}
}

View File

@@ -0,0 +1,21 @@
package com.lanyuanxiaoyao.leopard.core.service.selector;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import java.util.Map;
import java.util.Set;
/**
* 选股器
*
* @author lanyuanxiaoyao
* @version 20250924
*/
public interface StockSelector<T> {
Set<Candidate> select(T request);
record Candidate(Stock stock, double score, Map<String, Object> extra) {
public Candidate(Stock stock, double score) {
this(stock, score, Map.of());
}
}
}

View File

@@ -41,15 +41,6 @@
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-rule-sql</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>

View File

@@ -1,8 +1,5 @@
package com.lanyuanxiaoyao.leopard.server;
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
import com.yomahub.liteflow.core.FlowExecutor;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
@@ -22,15 +19,7 @@ public class LeopardServerApplication implements ApplicationRunner {
SpringApplication.run(LeopardServerApplication.class, args);
}
@Resource
private FlowExecutor executor;
@Resource
private TuShareService tuShareService;
@Override
public void run(ApplicationArguments args) {
// executor.execute2RespWithEL("THEN(update_daily)");
// executor.execute2RespWithEL("THEN(update_stock)");
// executor.execute2RespWithEL("THEN(check_daily)");
}
}

View File

@@ -3,7 +3,7 @@ package com.lanyuanxiaoyao.leopard.server.controller;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.entity.Task;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService;
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import java.util.Arrays;
import java.util.List;
@@ -87,11 +87,11 @@ public class CommonOptionsController {
);
private final StockRepository stockRepository;
private final TaskTemplateService taskTemplateService;
private final TaskService taskService;
public CommonOptionsController(StockRepository stockRepository, TaskTemplateService taskTemplateService) {
public CommonOptionsController(StockRepository stockRepository, TaskService taskService) {
this.stockRepository = stockRepository;
this.taskTemplateService = taskTemplateService;
this.taskService = taskService;
}
@GetMapping("/options/{name}")
@@ -109,9 +109,9 @@ public class CommonOptionsController {
.toList()
);
case "task_template_id" -> GlobalResponse.responseSuccess(
taskTemplateService.list()
taskService.getTemplates()
.stream()
.map(template -> new Option(template.getName(), template.getId()))
.map(template -> new Option(template.name(), template.id()))
.toList()
);
default -> GlobalResponse.responseSuccess(List.of());

View File

@@ -1,7 +1,7 @@
package com.lanyuanxiaoyao.leopard.server.controller;
import com.lanyuanxiaoyao.leopard.server.service.QuartzService;
import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService;
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import java.time.LocalDateTime;
import java.util.List;
@@ -20,11 +20,11 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("task_schedule")
public class QuartzController {
private final QuartzService quartzService;
private final TaskTemplateService taskTemplateService;
private final TaskService taskService;
public QuartzController(QuartzService quartzService, TaskTemplateService taskTemplateService) {
public QuartzController(QuartzService quartzService, TaskService taskService) {
this.quartzService = quartzService;
this.taskTemplateService = taskTemplateService;
this.taskService = taskService;
}
@PostMapping("save")
@@ -38,11 +38,11 @@ public class QuartzController {
var list = quartzService.list()
.stream()
.map(task -> {
var template = taskTemplateService.detail(task.templateId());
var template = taskService.getTemplate(task.templateId());
return new ListItem(
task.key(),
template.getName(),
template.getDescription(),
template.name(),
template.description(),
task.cron(),
task.status(),
task.previousFireTime(),
@@ -72,7 +72,7 @@ public class QuartzController {
}
public record SaveItem(
Long templateId,
String templateId,
String cron
) {
}

View File

@@ -12,6 +12,7 @@ import java.time.LocalDateTime;
import java.util.Map;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -45,6 +46,12 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
return GlobalResponse.responseSuccess();
}
@GetMapping("template/list")
public GlobalResponse<Map<String, Object>> templateList() {
var templates = taskService.getTemplates();
return GlobalResponse.responseCrudData(templates, templates.size());
}
@Override
protected Function<Void, Task> saveItemMapper() {
throw new UnsupportedOperationException();
@@ -108,7 +115,7 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
LocalDateTime finishedTime,
Long cost,
String costText,
Integer step
Double step
) {
}
@@ -123,11 +130,11 @@ public class TaskController extends SimpleControllerSupport<Task, Void, TaskCont
LocalDateTime finishedTime,
Long cost,
String costText,
Integer step
Double step
) {
}
public record ExecuteRequest(Long templateId, Map<String, Object> params) {
public record ExecuteRequest(String templateId, Map<String, Object> params) {
}
public record TaskCost(Long cost, String costText) {

View File

@@ -1,66 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.controller;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.leopard.core.entity.TaskTemplate;
import com.lanyuanxiaoyao.leopard.server.service.TaskTemplateService;
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("task_template")
public class TaskTemplateController extends SimpleControllerSupport<TaskTemplate, TaskTemplateController.Item, TaskTemplateController.Item, TaskTemplateController.Item> {
@Value("${spring.application.name}")
private String application;
public TaskTemplateController(TaskTemplateService service) {
super(service);
}
@Override
protected Function<Item, TaskTemplate> saveItemMapper() {
return item -> {
var template = new TaskTemplate();
template.setId(item.id());
template.setName(item.name());
template.setDescription(item.description());
template.setApplication(application);
template.setChain(IdUtil.simpleUUID());
template.setExpression(item.expression());
template.setExpressionEl(StrUtil.format("CATCH(THEN(task_start, ({}), task_end)).DO(task_error)", item.expression()));
return template;
};
}
private Item convert(TaskTemplate template) {
return new Item(
template.getId(),
template.getName(),
template.getDescription(),
template.getExpression()
);
}
@Override
protected Function<TaskTemplate, Item> listItemMapper() {
return this::convert;
}
@Override
protected Function<TaskTemplate, Item> detailItemMapper() {
return this::convert;
}
public record Item(
Long id,
String name,
String description,
String expression
) {
}
}

View File

@@ -2,8 +2,7 @@ package com.lanyuanxiaoyao.leopard.server.service;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.lanyuanxiaoyao.leopard.server.service.task.TaskMonitorNodes;
import com.yomahub.liteflow.core.FlowExecutor;
import cn.hutool.core.util.StrUtil;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
@@ -39,7 +38,7 @@ public class QuartzService {
var trigger = (CronTrigger) scheduler.getTriggersOfJob(key).getFirst();
tasks.add(new QuartzTask(
detail.getKey().getName(),
detail.getJobDataMap().getLong("template_id"),
detail.getJobDataMap().getString("template_id"),
trigger.getCronExpression(),
scheduler.getTriggerState(trigger.getKey()),
ObjectUtil.isNull(trigger.getPreviousFireTime()) ? null : LocalDateTime.ofInstant(trigger.getPreviousFireTime().toInstant(), ZoneId.systemDefault()),
@@ -49,7 +48,7 @@ public class QuartzService {
return tasks;
}
public void save(Long templateId, String cron) throws SchedulerException {
public void save(String templateId, String cron) throws SchedulerException {
var detail = JobBuilder.newJob(TaskExecutionJob.class)
.withIdentity("task_execution_" + IdUtil.fastUUID())
.usingJobData("template_id", templateId)
@@ -81,30 +80,27 @@ public class QuartzService {
@Slf4j
public static class TaskExecutionJob extends QuartzJobBean {
private final TaskTemplateService taskTemplateService;
private final FlowExecutor flowExecutor;
private final TaskService taskService;
public TaskExecutionJob(TaskTemplateService taskTemplateService, FlowExecutor flowExecutor) {
this.taskTemplateService = taskTemplateService;
this.flowExecutor = flowExecutor;
public TaskExecutionJob(TaskService taskService) {
this.taskService = taskService;
}
@SuppressWarnings("unchecked")
@Override
protected void executeInternal(JobExecutionContext context) {
var dataMap = context.getMergedJobDataMap();
var templateId = dataMap.getLong("template_id");
if (ObjectUtil.isNotNull(templateId)) {
var template = taskTemplateService.detail(templateId);
var templateId = dataMap.getString("template_id");
if (StrUtil.isNotBlank(templateId)) {
var params = (Map<String, Object>) dataMap.getOrDefault("params", Map.of());
var monitorContext = new TaskMonitorNodes.TaskMonitorContext(template);
flowExecutor.execute2Resp(template.getChain(), params, monitorContext);
taskService.execute(templateId, params, true);
}
}
}
public record QuartzTask(
String key,
Long templateId,
String templateId,
String cron,
Trigger.TriggerState status,
LocalDateTime previousFireTime,

View File

@@ -1,14 +1,26 @@
package com.lanyuanxiaoyao.leopard.server.service;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Task;
import com.lanyuanxiaoyao.leopard.core.repository.TaskRepository;
import com.lanyuanxiaoyao.leopard.server.service.task.TaskMonitorNodes;
import com.lanyuanxiaoyao.leopard.server.service.task.TaskRunner;
import com.lanyuanxiaoyao.leopard.server.service.task.UpdateDailyTask;
import com.lanyuanxiaoyao.leopard.server.service.task.UpdateFinanceIndicatorTask;
import com.lanyuanxiaoyao.leopard.server.service.task.UpdateStockTask;
import com.lanyuanxiaoyao.leopard.server.service.task.UpdateYearlyTask;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import com.yomahub.liteflow.core.FlowExecutor;
import jakarta.transaction.Transactional;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
@@ -19,15 +31,24 @@ import org.springframework.stereotype.Service;
@Slf4j
@Service
public class TaskService extends SimpleServiceSupport<Task> {
private final ExecutorService executor = Executors.newFixedThreadPool(50);
private final TaskRepository taskRepository;
private final TaskTemplateService taskTemplateService;
private final FlowExecutor flowExecutor;
private final ApplicationContext context;
public TaskService(TaskRepository repository, TaskTemplateService taskTemplateService, @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") FlowExecutor flowExecutor) {
@Getter
private final Set<TaskTemplate> templates = Stream.of(
new TaskTemplate("更新股票信息", "更新股票信息", UpdateStockTask.class),
new TaskTemplate("更新年线指标", "更新年线指标", UpdateYearlyTask.class),
new TaskTemplate("更新日线数据", "更新日线数据", UpdateDailyTask.class),
new TaskTemplate("更新财务指标", "更新财务指标", UpdateFinanceIndicatorTask.class)
).collect(Collectors.toSet());
private final Map<String, TaskTemplate> templateMap = templates.stream()
.collect(Collectors.toMap(TaskTemplate::id, template -> template));
public TaskService(TaskRepository repository, ApplicationContext context) {
super(repository);
this.taskRepository = repository;
this.taskTemplateService = taskTemplateService;
this.flowExecutor = flowExecutor;
this.context = context;
}
@Transactional(rollbackOn = Throwable.class)
@@ -37,14 +58,35 @@ public class TaskService extends SimpleServiceSupport<Task> {
taskRepository.updateAllRunningTaskToFailure();
}
public void execute(Long templateId, Map<String, Object> params) {
var template = taskTemplateService.detail(templateId);
var context = new TaskMonitorNodes.TaskMonitorContext(template);
flowExecutor.execute2Future(template.getChain(), params, context);
public TaskTemplate getTemplate(String templateId) {
return templateMap.get(templateId);
}
@Transactional(rollbackOn = Throwable.class)
public void updateStepById(Integer step, Long id) {
taskRepository.updateStepById(step, id);
public void execute(String templateId, Map<String, Object> params) {
execute(templateId, params, true);
}
public void execute(String templateId, Map<String, Object> params, boolean async) {
var template = templateMap.get(templateId);
if (ObjectUtil.isNull(template)) {
throw new RuntimeException("任务模板不存在");
}
var instance = context.getBean(template.runnerClass());
if (async) {
executor.submit(() -> instance.run(template, params));
} else {
instance.run(template, params);
}
}
public record TaskTemplate(
String id,
String name,
String description,
Class<? extends TaskRunner> runnerClass
) {
public TaskTemplate(String name, String description, Class<? extends TaskRunner> runnerClass) {
this(IdUtil.fastUUID(), name, description, runnerClass);
}
}
}

View File

@@ -1,49 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.service;
import com.lanyuanxiaoyao.leopard.core.entity.TaskTemplate;
import com.lanyuanxiaoyao.leopard.core.repository.TaskTemplateRepository;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import com.yomahub.liteflow.builder.el.LiteFlowChainELBuilder;
import com.yomahub.liteflow.meta.LiteflowMetaOperator;
import org.springframework.stereotype.Service;
@Service
public class TaskTemplateService extends SimpleServiceSupport<TaskTemplate> {
public TaskTemplateService(TaskTemplateRepository repository) {
super(repository);
}
private void validateExpression(String expression) {
var response = LiteFlowChainELBuilder.validateWithEx(expression);
if (!response.isSuccess()) {
throw new RuntimeException(response.getCause());
}
}
@Override
public Long save(TaskTemplate entity) {
validateExpression(entity.getExpression());
Long id = super.save(entity);
LiteflowMetaOperator.reloadAllChain();
return id;
}
@Override
public void save(Iterable<TaskTemplate> taskTemplates) {
taskTemplates.forEach(template -> validateExpression(template.getExpression()));
super.save(taskTemplates);
LiteflowMetaOperator.reloadAllChain();
}
@Override
public void remove(Iterable<Long> ids) {
super.remove(ids);
LiteflowMetaOperator.reloadAllChain();
}
@Override
public void remove(Long id) {
super.remove(id);
LiteflowMetaOperator.reloadAllChain();
}
}

View File

@@ -1,89 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@LiteflowComponent("check_daily")
public class CheckDaily extends TaskNodeComponent {
private final StockRepository stockRepository;
private final DailyRepository dailyRepository;
private final TuShareService tuShareService;
public CheckDaily(TaskService taskService, StockRepository stockRepository, DailyRepository dailyRepository, TuShareService tuShareService) {
super(taskService);
this.stockRepository = stockRepository;
this.dailyRepository = dailyRepository;
this.tuShareService = tuShareService;
}
@Override
public void process() {
var reports = new ArrayList<MissedTradeReport>();
var stocks = stockRepository.findAll();
var exchanges = stocks.stream().map(Stock::getMarket).distinct().toList();
for (Stock.Market exchange : exchanges) {
var nowDate = LocalDate.now();
var allTradeDates = tuShareService.tradeDateList(exchange.name())
.data()
.items()
.stream()
.map(item -> LocalDate.parse(item.getFirst(), TuShareService.TRADE_FORMAT))
.filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate))
.toList();
var total = stocks.size();
var progress = 0;
for (Stock stock : stocks) {
log.info("正在处理:{} {}", stock.getCode(), stock.getName());
if (exchange.equals(stock.getMarket())) {
var existsTradeDates = dailyRepository.findDistinctTradeDateByStockId(stock.getId());
var missedTradeDates = allTradeDates.stream()
.filter(date -> date.isAfter(stock.getListedDate()) || date.isEqual(stock.getListedDate()))
.filter(date -> !existsTradeDates.contains(date))
.filter(date -> {
ThreadUtil.safeSleep(100);
var response = tuShareService.dailyList(date, stock.getCode());
return !response.data().items().isEmpty();
})
.toList();
if (ObjectUtil.isNotEmpty(missedTradeDates)) {
reports.add(new MissedTradeReport(
stock.getCode(),
stock.getName(),
missedTradeDates
));
}
}
setStep(++progress * 100 / total);
}
}
if (ObjectUtil.isNotEmpty(reports)) {
var context = getContextBean(TaskMonitorNodes.TaskMonitorContext.class);
context.setTaskResult(
reports.stream()
.map(report -> StrUtil.format("{}{})缺少如下交易日数据:{}", report.name(), report.code(), report.missedTradeDates().stream().map(LocalDate::toString).collect(Collectors.joining(", "))))
.collect(Collectors.joining("\n"))
);
}
}
public record MissedTradeReport(
String code,
String name,
List<LocalDate> missedTradeDates
) {
}
}

View File

@@ -1,221 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.entity.StockCollection;
import com.lanyuanxiaoyao.leopard.core.repository.StockCollectionRepository;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import java.time.LocalDate;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
/**
* 金字塔选股
*
* @author lanyuanxiaoyao
* @version 20250917
*/
@Slf4j
@LiteflowComponent("pyramid_stock_selector")
public class PyramidStockSelector extends NodeComponent {
private final StockRepository stockRepository;
private final StockCollectionRepository stockCollectionRepository;
public PyramidStockSelector(StockRepository stockRepository, StockCollectionRepository stockCollectionRepository) {
this.stockRepository = stockRepository;
this.stockCollectionRepository = stockCollectionRepository;
}
@Override
public void process() {
// 选择至少有最近5年财报的股票
var stocks = stockRepository.findAllByIndicatorsSizeGreaterThanEqual(5);
var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
var scores = stocks.stream().map(Stock::getCode).collect(Collectors.toMap(code -> code, code -> 0));
for (Stock stock : stocks) {
var recentIndicators = stock.getIndicators()
.stream()
.sorted((a, b) -> b.getYear() - a.getYear())
.limit(5)
.toList();
var latestIndicator = recentIndicators.getFirst();
var roeScore = 0;
if (recentIndicators.stream().noneMatch(indicator -> indicator.getReturnOnEquity() == null || indicator.getReturnOnEquity() < 0)) {
var averageRoe = recentIndicators.stream()
.map(FinanceIndicator::getReturnOnEquity)
.map(item -> ObjectUtil.defaultIfNull(item, 0.0))
.mapToDouble(Double::doubleValue)
.average()
.orElse(0.0);
if (averageRoe >= 35) {
roeScore = 550;
} else if (averageRoe >= 30) {
roeScore = 500;
} else if (averageRoe >= 25) {
roeScore = 450;
} else if (averageRoe >= 20) {
roeScore = 400;
} else if (averageRoe >= 15) {
roeScore = 350;
} else if (averageRoe >= 10) {
roeScore = 300;
}
}
scores.put(stock.getCode(), scores.get(stock.getCode()) + roeScore);
var roaScore = 0;
if (recentIndicators.stream().noneMatch(indicator -> indicator.getReturnOnAssets() == null)) {
var averageRoa = recentIndicators.stream()
.map(FinanceIndicator::getReturnOnAssets)
.mapToDouble(Double::doubleValue)
.average()
.orElse(0.0);
if (averageRoa >= 15) {
roaScore = 100;
} else if (averageRoa >= 11) {
roaScore = 80;
} else if (averageRoa >= 7) {
roaScore = 50;
}
}
scores.put(stock.getCode(), scores.get(stock.getCode()) + roaScore);
var netProfitScore = 0;
if (recentIndicators.stream().noneMatch(indicator -> indicator.getNetProfit() == null)) {
var averageNetProfit = recentIndicators.stream()
.map(FinanceIndicator::getNetProfit)
.mapToDouble(Double::doubleValue)
.average()
.orElse(0.0);
if (averageNetProfit >= 10000.0 * 10000000) {
netProfitScore = 150;
} else if (averageNetProfit >= 1000.0 * 10000000) {
netProfitScore = 100;
}
}
scores.put(stock.getCode(), scores.get(stock.getCode()) + netProfitScore);
var cashScore = 0;
if (
ArrayUtil.isAllNotNull(latestIndicator.getTotalAssetsTurnover(), latestIndicator.getCashAndCashEquivalentsToTotalAssetsRatio())
&& (
latestIndicator.getTotalAssetsTurnover() > 0.8 && latestIndicator.getCashAndCashEquivalentsToTotalAssetsRatio() >= 0.1
|| latestIndicator.getTotalAssetsTurnover() <= 0.8 && latestIndicator.getCashAndCashEquivalentsToTotalAssetsRatio() >= 0.2
)
) {
cashScore = 50;
}
scores.put(stock.getCode(), scores.get(stock.getCode()) + cashScore);
if (ObjectUtil.isNotNull(latestIndicator.getDaysAccountsReceivableTurnover()) && latestIndicator.getDaysAccountsReceivableTurnover() <= 30) {
scores.put(stock.getCode(), scores.get(stock.getCode()) + 20);
}
if (ObjectUtil.isNotNull(latestIndicator.getDaysInventoryTurnover()) && latestIndicator.getDaysInventoryTurnover() <= 30) {
scores.put(stock.getCode(), scores.get(stock.getCode()) + 20);
}
if (ArrayUtil.isAllNotNull(latestIndicator.getDaysAccountsReceivableTurnover(), latestIndicator.getDaysInventoryTurnover())) {
if (latestIndicator.getDaysAccountsReceivableTurnover() + latestIndicator.getDaysInventoryTurnover() <= 40) {
scores.put(stock.getCode(), scores.get(stock.getCode()) + 20);
} else if (latestIndicator.getDaysAccountsReceivableTurnover() + latestIndicator.getDaysInventoryTurnover() <= 60) {
scores.put(stock.getCode(), scores.get(stock.getCode()) + 10);
}
}
if (recentIndicators.stream().noneMatch(indicator -> indicator.getOperatingGrossProfitMargin() == null)) {
var stat = new DescriptiveStatistics();
recentIndicators.stream()
.map(FinanceIndicator::getOperatingGrossProfitMargin)
.mapToDouble(Double::doubleValue)
.forEach(stat::addValue);
if (stat.getStandardDeviation() <= 0.3) {
scores.put(stock.getCode(), scores.get(stock.getCode()) + 50);
}
}
var operatingSafeMarginScore = 0;
if (ObjectUtil.isNotNull(latestIndicator.getOperatingSafetyMarginRatio())) {
if (latestIndicator.getOperatingSafetyMarginRatio() >= 70) {
operatingSafeMarginScore = 50;
} else if (latestIndicator.getOperatingSafetyMarginRatio() >= 50) {
operatingSafeMarginScore = 30;
} else if (latestIndicator.getOperatingSafetyMarginRatio() >= 30) {
operatingSafeMarginScore = 10;
}
}
scores.put(stock.getCode(), scores.get(stock.getCode()) + operatingSafeMarginScore);
var netProfitAscendingScore = 0;
if (recentIndicators.stream().noneMatch(indicator -> indicator.getNetProfit() == null)) {
if (recentIndicators.get(0).getNetProfit() > recentIndicators.get(1).getNetProfit()) {
netProfitAscendingScore += 30;
} else {
netProfitAscendingScore -= 30;
}
if (recentIndicators.get(1).getNetProfit() > recentIndicators.get(2).getNetProfit()) {
netProfitAscendingScore += 25;
} else {
netProfitAscendingScore -= 25;
}
if (recentIndicators.get(2).getNetProfit() > recentIndicators.get(3).getNetProfit()) {
netProfitAscendingScore += 20;
} else {
netProfitAscendingScore -= 20;
}
if (recentIndicators.get(3).getNetProfit() > recentIndicators.get(4).getNetProfit()) {
netProfitAscendingScore += 15;
} else {
netProfitAscendingScore -= 15;
}
}
scores.put(stock.getCode(), scores.get(stock.getCode()) + netProfitAscendingScore);
var cashAscendingScore = 0;
if (recentIndicators.stream().noneMatch(indicator -> indicator.getCashAndCashEquivalents() == null)) {
if (recentIndicators.get(0).getCashAndCashEquivalents() > recentIndicators.get(1).getCashAndCashEquivalents()) {
cashAscendingScore += 30;
} else {
cashAscendingScore -= 30;
}
if (recentIndicators.get(1).getCashAndCashEquivalents() > recentIndicators.get(2).getCashAndCashEquivalents()) {
cashAscendingScore += 25;
} else {
cashAscendingScore -= 25;
}
if (recentIndicators.get(2).getCashAndCashEquivalents() > recentIndicators.get(3).getCashAndCashEquivalents()) {
cashAscendingScore += 20;
} else {
cashAscendingScore -= 20;
}
if (recentIndicators.get(3).getCashAndCashEquivalents() > recentIndicators.get(4).getCashAndCashEquivalents()) {
cashAscendingScore += 15;
} else {
cashAscendingScore -= 15;
}
}
scores.put(stock.getCode(), scores.get(stock.getCode()) + cashAscendingScore);
}
var first50 = scores.entrySet()
.stream()
.sorted((e1, e2) -> e2.getValue() - e1.getValue())
.limit(50)
.map(entry -> stocksMap.get(entry.getKey()))
.collect(Collectors.toSet());
var collection = new StockCollection();
collection.setName(StrUtil.format("金字塔选股 ({})", LocalDate.now()));
collection.setDescription("");
collection.setStocks(first50);
stockCollectionRepository.save(collection);
}
}

View File

@@ -1,83 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Task;
import com.lanyuanxiaoyao.leopard.core.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) {
try {
var context = node.getContextBean(TaskMonitorContext.class);
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);
} catch (Exception exception) {
log.warn("Not in task", exception);
}
}
@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.setStep(100);
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(ExceptionUtil.stacktraceToString(exception));
}
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,24 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.exception.NoSuchContextBeanException;
public abstract class TaskNodeComponent extends NodeComponent {
private final TaskService taskService;
protected TaskNodeComponent(TaskService taskService) {
this.taskService = taskService;
}
protected void setStep(int step) {
if (step < 0 || step > 100) {
throw new IllegalArgumentException("step must be between 0 and 100");
}
try {
var context = getContextBean(TaskMonitorNodes.TaskMonitorContext.class);
taskService.updateStepById(step, context.getTaskId());
} catch (NoSuchContextBeanException ignored) {
}
}
}

View File

@@ -0,0 +1,63 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Task;
import com.lanyuanxiaoyao.leopard.core.repository.TaskRepository;
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import java.time.LocalDateTime;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
/**
* 任务运行
*
* @author lanyuanxiaoyao
* @version 20250924
*/
@Slf4j
public abstract class TaskRunner {
private final ApplicationContext context;
protected TaskRunner(ApplicationContext context) {
this.context = context;
}
public abstract String process(Map<String, Object> params, StepUpdater updater) throws Exception;
public void run(TaskService.TaskTemplate template, Map<String, Object> params) {
var taskRepository = context.getBean(TaskRepository.class);
var task = new Task();
task.setName(template.name());
task.setDescription(template.description());
task.setStatus(Task.Status.RUNNING);
task.setLaunchedTime(LocalDateTime.now());
taskRepository.saveAndFlush(task);
try {
var result = process(params, step -> taskRepository.updateStepById(task.getId(), step));
task.setStatus(Task.Status.SUCCESS);
task.setStep(1.0);
task.setFinishedTime(LocalDateTime.now());
if (StrUtil.isNotBlank(result)) {
task.setResult(result);
}
taskRepository.saveAndFlush(task);
} catch (Throwable throwable) {
log.error("任务执行失败", throwable);
task.setStatus(Task.Status.FAILURE);
task.setFinishedTime(LocalDateTime.now());
if (ObjectUtil.isNotNull(throwable)) {
task.setError(ExceptionUtil.stacktraceToString(throwable));
}
taskRepository.saveAndFlush(task);
}
}
public interface StepUpdater {
void update(double step);
}
}

View File

@@ -9,35 +9,41 @@ import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper;
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 更新日线数据
*
* @author lanyuanxiaoyao
* @version 20250924
*/
@Slf4j
@LiteflowComponent("update_daily")
public class UpdateDaily extends NodeComponent {
@Component
public class UpdateDailyTask extends TaskRunner {
private final StockRepository stockRepository;
private final DailyRepository dailyRepository;
private final TuShareService tuShareService;
private final TransactionTemplate transactionTemplate;
public UpdateDaily(StockRepository stockRepository, DailyRepository dailyRepository, TuShareService tuShareService, TransactionTemplate transactionTemplate) {
protected UpdateDailyTask(ApplicationContext context, StockRepository stockRepository, DailyRepository dailyRepository, TuShareService tuShareService) {
super(context);
this.stockRepository = stockRepository;
this.dailyRepository = dailyRepository;
this.tuShareService = tuShareService;
this.transactionTemplate = transactionTemplate;
}
@Transactional(rollbackFor = Throwable.class)
@Override
public void process() {
public String process(Map<String, Object> params, StepUpdater updater) throws Exception {
var tradeDates = new HashSet<LocalDate>();
for (String exchange : List.of("SSE", "SZSE", "BSE")) {
var response = tuShareService.tradeDateList(exchange);
@@ -53,7 +59,6 @@ public class UpdateDaily extends NodeComponent {
tradeDates.parallelStream()
.filter(date -> date.isBefore(nowDate) || date.isEqual(nowDate))
.filter(date -> !existsTradeDates.contains(date))
// .filter(date -> date.isAfter(LocalDate.of(2024, 12, 31)))
.forEach(tradeDate -> {
var factorResponse = tuShareService.factorList(tradeDate);
var factorMap = new HashMap<String, Double>();
@@ -62,8 +67,6 @@ public class UpdateDaily extends NodeComponent {
}
var response = tuShareService.dailyList(tradeDate);
transactionTemplate.execute(status -> {
try {
for (List<String> item : response.data().items()) {
var code = item.get(0);
if (stocksMap.containsKey(code)) {
@@ -85,13 +88,8 @@ public class UpdateDaily extends NodeComponent {
dailyRepository.save(daily);
}
}
return true;
} catch (Exception exception) {
log.error("Error", exception);
status.setRollbackOnly();
return false;
}
});
});
return null;
}
}

View File

@@ -2,38 +2,45 @@ package com.lanyuanxiaoyao.leopard.server.service.task;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.lanyuanxiaoyao.leopard.core.entity.FinanceIndicator;
import com.lanyuanxiaoyao.leopard.core.entity.QFinanceIndicator;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.repository.FinanceIndicatorRepository;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.server.helper.NumberHelper;
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* 更新财务指标数据
*
* @author lanyuanxiaoyao
* @version 20250924
*/
@Slf4j
@LiteflowComponent("update_finance")
public class UpdateFinanceIndicator extends TaskNodeComponent {
private final FinanceIndicatorRepository financeIndicatorRepository;
@Component
public class UpdateFinanceIndicatorTask extends TaskRunner {
private final StockRepository stockRepository;
private final FinanceIndicatorRepository financeIndicatorRepository;
private final TuShareService tuShareService;
protected UpdateFinanceIndicator(TaskService taskService, FinanceIndicatorRepository financeIndicatorRepository, StockRepository stockRepository, TuShareService tuShareService) {
super(taskService);
this.financeIndicatorRepository = financeIndicatorRepository;
protected UpdateFinanceIndicatorTask(ApplicationContext context, StockRepository stockRepository, FinanceIndicatorRepository financeIndicatorRepository, TuShareService tuShareService) {
super(context);
this.stockRepository = stockRepository;
this.financeIndicatorRepository = financeIndicatorRepository;
this.tuShareService = tuShareService;
}
@Override
public void process() throws Exception {
public String process(Map<String, Object> params, StepUpdater updater) throws JsonProcessingException {
var stocks = stockRepository.findAll();
var currentYear = LocalDate.now().getYear();
for (int year = 1990; year < currentYear; year++) {
@@ -209,7 +216,8 @@ public class UpdateFinanceIndicator extends TaskNodeComponent {
financeIndicatorRepository.save(indicator);
}
setStep((year - 1990) * 100 / (currentYear - 1990));
updater.update((year - 1990) * 1.0 / (currentYear - 1990));
}
return null;
}
}

View File

@@ -4,26 +4,34 @@ import cn.hutool.core.util.EnumUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.server.service.TuShareService;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import jakarta.transaction.Transactional;
import java.time.LocalDate;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@LiteflowComponent("update_stock")
public class UpdateStock extends NodeComponent {
/**
* 更新股票信息
*
* @author lanyuanxiaoyao
* @version 20250924
*/
@Component
public class UpdateStockTask extends TaskRunner {
private final StockRepository stockRepository;
private final TuShareService tuShareService;
public UpdateStock(StockRepository stockRepository, TuShareService tuShareService) {
public UpdateStockTask(ApplicationContext context, StockRepository stockRepository, TuShareService tuShareService) {
super(context);
this.stockRepository = stockRepository;
this.tuShareService = tuShareService;
}
@Transactional(rollbackOn = Throwable.class)
@Transactional(rollbackFor = Throwable.class)
@Override
public void process() {
public String process(Map<String, Object> params, StepUpdater updater) {
var existsStockMap = stockRepository.findAll().stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
var stocks = tuShareService.stockList()
.data()
@@ -51,5 +59,6 @@ public class UpdateStock extends NodeComponent {
var deleteCodes = existsCodes.stream().filter(code -> !currentCodes.contains(code)).toList();
stockRepository.deleteAllByCodeIn(deleteCodes);
stockRepository.saveAll(stocks);
return null;
}
}

View File

@@ -1,76 +0,0 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
import com.lanyuanxiaoyao.leopard.core.entity.QYearly;
import com.lanyuanxiaoyao.leopard.core.entity.Yearly;
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.core.repository.YearlyRepository;
import com.lanyuanxiaoyao.leopard.server.service.TaskService;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.support.TransactionTemplate;
@Slf4j
@LiteflowComponent("update_yearly")
public class UpdateYearly extends TaskNodeComponent {
private final StockRepository stockRepository;
private final DailyRepository dailyRepository;
private final YearlyRepository yearlyRepository;
private final TransactionTemplate transactionTemplate;
protected UpdateYearly(TaskService taskService, StockRepository stockRepository, DailyRepository dailyRepository, YearlyRepository yearlyRepository, TransactionTemplate transactionTemplate) {
super(taskService);
this.stockRepository = stockRepository;
this.dailyRepository = dailyRepository;
this.yearlyRepository = yearlyRepository;
this.transactionTemplate = transactionTemplate;
}
@Override
public void process() {
var startYear = dailyRepository.findMinTradeDate().getYear();
var endYear = dailyRepository.findMaxTradeDate().getYear();
var stocks = stockRepository.findAll();
for (int year = startYear, index = 0; year <= endYear; year++, index++) {
var currentYear = year;
transactionTemplate.execute(status -> {
try {
for (var stock : stocks) {
log.info("Processing {} {}", stock.getCode(), currentYear);
if (stock.getListedDate().getYear() > currentYear) {
continue;
}
var dailies = dailyRepository.findAll(
QDaily.daily.tradeDate.year().eq(currentYear)
.and(QDaily.daily.stock.eq(stock))
);
var yearly = yearlyRepository.findOne(
QYearly.yearly.stock.eq(stock)
.and(QYearly.yearly.year.eq(currentYear))
).orElseGet(Yearly::new);
yearly.setStock(stock);
yearly.setYear(currentYear);
yearly.setClose(dailies.getLast().getHfqClose());
yearly.setOpen(dailies.getFirst().getHfqOpen());
yearly.setHigh(dailies.stream().map(Daily::getHfqHigh).max(Double::compareTo).orElse(0.0));
yearly.setLow(dailies.stream().map(Daily::getHfqLow).min(Double::compareTo).orElse(0.0));
yearly.setVolume(dailies.stream().mapToDouble(Daily::getVolume).sum());
yearly.setTurnover(dailies.stream().mapToDouble(Daily::getTurnover).sum());
yearly.setPriceChangeAmount(yearly.getClose() - yearly.getOpen());
yearly.setPriceFluctuationRange((yearly.getClose() - yearly.getOpen()) / yearly.getOpen());
yearlyRepository.save(yearly);
}
return true;
} catch (Exception exception) {
log.error("Error for %s".formatted(currentYear), exception);
status.setRollbackOnly();
return false;
}
});
setStep((currentYear - startYear) * 100 / (endYear - startYear + 1));
}
}
}

View File

@@ -0,0 +1,72 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
import com.lanyuanxiaoyao.leopard.core.entity.QYearly;
import com.lanyuanxiaoyao.leopard.core.entity.Yearly;
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.lanyuanxiaoyao.leopard.core.repository.YearlyRepository;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 更新年线指标
*
* @author lanyuanxiaoyao
* @version 20250924
*/
@Slf4j
@Component
public class UpdateYearlyTask extends TaskRunner {
private final StockRepository stockRepository;
private final DailyRepository dailyRepository;
private final YearlyRepository yearlyRepository;
protected UpdateYearlyTask(ApplicationContext context, StockRepository stockRepository, DailyRepository dailyRepository, YearlyRepository yearlyRepository) {
super(context);
this.stockRepository = stockRepository;
this.dailyRepository = dailyRepository;
this.yearlyRepository = yearlyRepository;
}
@Transactional(rollbackFor = Throwable.class)
@Override
public String process(Map<String, Object> params, StepUpdater updater) {
var startYear = dailyRepository.findMinTradeDate().getYear();
var endYear = dailyRepository.findMaxTradeDate().getYear();
var stocks = stockRepository.findAll();
for (int year = startYear, index = 0; year <= endYear; year++, index++) {
for (var stock : stocks) {
log.info("Processing {} {}", stock.getCode(), year);
if (stock.getListedDate().getYear() > year) {
continue;
}
var dailies = dailyRepository.findAll(
QDaily.daily.tradeDate.year().eq(year)
.and(QDaily.daily.stock.eq(stock))
);
var yearly = yearlyRepository.findOne(
QYearly.yearly.stock.eq(stock)
.and(QYearly.yearly.year.eq(year))
).orElseGet(Yearly::new);
yearly.setStock(stock);
yearly.setYear(year);
yearly.setClose(dailies.getLast().getHfqClose());
yearly.setOpen(dailies.getFirst().getHfqOpen());
yearly.setHigh(dailies.stream().map(Daily::getHfqHigh).max(Double::compareTo).orElse(0.0));
yearly.setLow(dailies.stream().map(Daily::getHfqLow).min(Double::compareTo).orElse(0.0));
yearly.setVolume(dailies.stream().mapToDouble(Daily::getVolume).sum());
yearly.setTurnover(dailies.stream().mapToDouble(Daily::getTurnover).sum());
yearly.setPriceChangeAmount(yearly.getClose() - yearly.getOpen());
yearly.setPriceFluctuationRange((yearly.getClose() - yearly.getOpen()) / yearly.getOpen());
yearlyRepository.save(yearly);
}
updater.update((year - startYear) * 1.0 / (endYear - startYear + 1));
}
return null;
}
}

View File

@@ -31,13 +31,3 @@ spring:
driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
fenix:
print-banner: false
liteflow:
print-banner: false
check-node-exists: false
rule-source-ext-data-map:
applicationName: ${spring.application.name}
sqlLogEnabled: true
chainTableName: leopard_task_template
chainApplicationNameField: application
chainNameField: chain
elDataField: expression_el

View File

@@ -1,126 +0,0 @@
package com.lanyuanxiaoyao.leopard.strategy;
import cn.hutool.core.util.ObjectUtil;
import com.lanyuanxiaoyao.leopard.core.entity.Daily;
import com.lanyuanxiaoyao.leopard.core.entity.QDaily;
import com.lanyuanxiaoyao.leopard.core.entity.QStock;
import com.lanyuanxiaoyao.leopard.core.entity.Stock;
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.springframework.transaction.annotation.Transactional;
/**
* 股票评估
*
* @author lanyuanxiaoyao
* @version 20250919
*/
@Slf4j
@LiteflowComponent("assess_stock")
public class StockAssessmentNode extends NodeComponent {
private static final Map<String, Double> INDUSTRY_TOP = new HashMap<>();
private final StockRepository stockRepository;
private final DailyRepository dailyRepository;
public StockAssessmentNode(StockRepository stockRepository, DailyRepository dailyRepository) {
this.stockRepository = stockRepository;
this.dailyRepository = dailyRepository;
}
@Transactional(readOnly = true)
@Override
public void process() {
var assessment = getContextBean(StockAssessment.class);
if (ObjectUtil.isNotNull(assessment) && ObjectUtil.isNotEmpty(assessment.stocks())) {
var dailyMap = dailyRepository.findAll(
QDaily.daily.tradeDate.year().eq(assessment.year())
.and(QDaily.daily.stock.in(assessment.stocks()))
)
.stream()
.collect(Collectors.groupingBy(Daily::getStock));
for (Stock stock : assessment.stocks()) {
if (!dailyMap.containsKey(stock) || ObjectUtil.isEmpty(dailyMap.get(stock))) {
log.warn("Cannot find daily data in {} for {}", assessment.year(), stock.getCode());
continue;
}
var dailies = dailyMap.get(stock)
.stream()
.sorted(Comparator.comparing(Daily::getTradeDate))
.toList();
var change = getChange(dailies);
var std = getStd(dailies);
var industryTop = getTopOfIndustry(stock.getIndustry(), assessment.year());
assessment.results().add(new StockAssessment.Result(stock, change, std, industryTop));
}
}
}
private double getChange(List<Daily> dailies) {
return (dailies.getLast().getHfqClose() - dailies.getFirst().getHfqClose()) / dailies.getFirst().getHfqClose();
}
private double getStd(List<Daily> dailies) {
var statistics = new DescriptiveStatistics();
dailies.forEach(daily -> statistics.addValue(daily.getHfqClose()));
return statistics.getStandardDeviation();
}
private double getTopOfIndustry(String industry, int year) {
log.info("Calculate industry: {} for {}", industry, year);
if (INDUSTRY_TOP.containsKey(industry)) {
return INDUSTRY_TOP.get(industry);
}
var top = stockRepository.findAll(QStock.stock.industry.eq(industry))
.parallelStream()
.filter(stock -> stock.getListedDate().getYear() <= year)
.map(stock -> {
List<Daily> dailies = dailyRepository.findAll(
QDaily.daily.tradeDate.year().eq(year)
.and(QDaily.daily.stock.eq(stock))
);
if (ObjectUtil.isEmpty(dailies)) {
log.warn("Cannot find daily data in {} for {} {}", year, stock.getCode(), stock.getName());
}
return dailies;
})
.filter(ObjectUtil::isNotEmpty)
.map(this::getChange)
.mapToDouble(change -> change)
.max()
.orElse(0.0);
INDUSTRY_TOP.put(industry, top);
return top;
}
public record StockAssessment(int year, Set<Stock> stocks, Set<StockAssessment.Result> results) {
public StockAssessment(int year) {
this(year, new HashSet<>(), new HashSet<>());
}
public record Result(Stock stock, double change, double std, double industryTop) {
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Result result = (Result) o;
return stock.equals(result.stock);
}
@Override
public int hashCode() {
return stock.hashCode();
}
}
}
}

View File

@@ -1,25 +1,25 @@
package com.lanyuanxiaoyao.leopard.strategy;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lanyuanxiaoyao.leopard.core.repository.DailyRepository;
import com.lanyuanxiaoyao.leopard.core.repository.StockRepository;
import com.yomahub.liteflow.core.FlowExecutor;
import com.lanyuanxiaoyao.leopard.core.service.AssessmentService;
import com.lanyuanxiaoyao.leopard.core.service.selector.PyramidStockSelector;
import com.lanyuanxiaoyao.leopard.core.service.selector.StockSelector;
import jakarta.annotation.Resource;
import jakarta.transaction.Transactional;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@SpringBootApplication(scanBasePackages = "com.lanyuanxiaoyao.leopard")
@@ -27,11 +27,9 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
public class StrategyApplication {
private static final ObjectMapper mapper = new ObjectMapper();
@Resource
private StockRepository stockRepository;
private PyramidStockSelector pyramidStockSelector;
@Resource
private DailyRepository dailyRepository;
@Resource
private FlowExecutor flowExecutor;
private AssessmentService assessmentService;
public static void main(String[] args) {
SpringApplication.run(StrategyApplication.class, args);
@@ -268,9 +266,9 @@ public class StrategyApplication {
);
}
@Transactional(rollbackOn = Throwable.class)
@Transactional(readOnly = true)
@EventListener(ApplicationReadyEvent.class)
public void test() throws IOException {
public void test() {
/*var dailies = dailyRepository.findAll(
QDaily.daily.tradeDate.year().eq(2025),
Sort.by(Daily_.TRADE_DATE)
@@ -310,16 +308,20 @@ public class StrategyApplication {
var lines = new ArrayList<String>();
for (int year = 2024; year < 2025; year++) {
var assessment = new StockAssessmentNode.StockAssessment(year);
var response = flowExecutor.execute2RespWithEL("THEN(pyramid_stock_selector,assess_stock)", null, IdUtil.nanoId(), assessment);
assessment = response.getContextBean(StockAssessmentNode.StockAssessment.class);
int up = assessment.results()
.stream()
var candidates = pyramidStockSelector.select(new PyramidStockSelector.Request(year));
for (StockSelector.Candidate candidate : candidates) {
log.info("{} {}", candidate.stock().getName(), candidate.score());
}
var stocks = candidates.stream()
.map(StockSelector.Candidate::stock)
.collect(Collectors.toSet());
var results = assessmentService.assess(stocks, year);
int up = results.stream()
.filter(result -> result.change() > 0)
.mapToInt(result -> 1)
.sum();
lines.add(NumberUtil.roundStr(up * 100.0 / assessment.results().size(), 2));
assessment.results().forEach(result -> log.info("{} {} {} {} {}", result.stock().getCode(), result.stock().getName(), result.change(), result.std(), result.industryTop()));
lines.add(NumberUtil.roundStr(up * 100.0 / results.size(), 2));
results.forEach(result -> log.info("{} {} {} {} {}", result.stock().getCode(), result.stock().getName(), result.change(), result.std(), result.industryTop()));
}
for (int index = 0, year = 2010; index < lines.size(); index++, year++) {
log.info("胜率: {} {}", year, lines.get(index));

View File

@@ -9,7 +9,6 @@ import StockList from './pages/stock/StockList.tsx'
import StockDetail from './pages/stock/StockDetail.tsx'
import TaskList from './pages/task/TaskList.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'
import StockCollectionList from './pages/stock/StockCollectionList.tsx'
@@ -73,10 +72,6 @@ const routes: RouteObject[] = [
path: 'list',
Component: TaskTemplateList,
},
{
path: 'save/:id',
Component: TaskTemplateSave,
},
],
},
{

View File

@@ -1,5 +1,14 @@
import React from 'react'
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, remoteMappings, time} from '../../util/amis.tsx'
import {
amisRender,
commonInfo,
crudCommonOptions,
horizontalFormOptions,
paginationTemplate,
remoteMappings,
remoteOptions,
time,
} from '../../util/amis.tsx'
import {useNavigate} from 'react-router'
function TaskList() {
@@ -25,7 +34,41 @@ function TaskList() {
},
interval: 30000,
...crudCommonOptions(),
...paginationTemplate(15),
...paginationTemplate(
15,
undefined,
[
{
type: 'action',
label: '',
icon: 'fa fa-plus',
actionType: 'dialog',
dialog: {
title: '创建任务',
body: {
type: 'form',
api: {
method: 'post',
url: `${commonInfo.baseUrl}/task/execute`,
data: {
templateId: '${templateId|default:undefined}',
},
},
...horizontalFormOptions(),
body: [
{
name: 'templateId',
label: '名称',
required: true,
selectFirst: true,
...remoteOptions('select', 'task_template_id'),
},
],
},
},
},
],
),
columns: [
{
name: 'name',
@@ -44,10 +87,10 @@ function TaskList() {
...remoteMappings('task_status', 'status'),
},
{
name: 'step',
label: '进度',
type: 'progress',
width: 200,
value: '${ROUND(step * 100, 0)}',
},
{
label: '耗时',

View File

@@ -1,9 +1,7 @@
import React from 'react'
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate} from '../../util/amis.tsx'
import {useNavigate} from 'react-router'
function TaskTemplateList() {
const navigate = useNavigate()
return (
<div className="task-template-list">
{amisRender(
@@ -13,43 +11,10 @@ function TaskTemplateList() {
body: [
{
type: 'crud',
api: {
method: 'post',
url: `${commonInfo.baseUrl}/task_template/list`,
data: {
page: {
index: '${page}',
size: '${perPage}',
},
},
},
api: `get:${commonInfo.baseUrl}/task/template/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/template/save/-1')
},
},
],
},
},
},
],
),
...paginationTemplate(15),
loadOnce: true,
columns: [
{
name: 'name',
@@ -63,7 +28,7 @@ function TaskTemplateList() {
{
type: 'operation',
label: '操作',
width: 150,
width: 100,
buttons: [
{
type: 'action',
@@ -78,35 +43,7 @@ function TaskTemplateList() {
},
},
confirmText: '确认执行模板<span class="text-lg font-bold mx-2">${name}</span>',
confirmTitle: '删除',
},
{
type: 'action',
label: '详情',
level: 'link',
onEvent: {
click: {
actions: [
{
actionType: 'custom',
// @ts-ignore
script: (context, action, event) => {
navigate(`/task/template/save/${context.props.data['id']}`)
},
},
],
},
},
},
{
className: 'text-danger btn-deleted',
type: 'action',
label: '删除',
level: 'link',
actionType: 'ajax',
api: `get:${commonInfo.baseUrl}/task_template/remove/\${id}`,
confirmText: '确认删除模板<span class="text-lg font-bold mx-2">${name}</span>',
confirmTitle: '删除',
confirmTitle: '执行',
},
],
},

View File

@@ -1,105 +0,0 @@
import React from 'react'
import {amisRender, commonInfo} from '../../util/amis.tsx'
import {useNavigate, useParams} from 'react-router'
function TaskTemplateSave() {
const navigate = useNavigate()
const {id} = useParams()
return (
<div className="task-template-save">
{amisRender(
{
type: 'page',
title: '任务模板添加',
body: [
{
debug: commonInfo.debug,
type: 'form',
api: `post:${commonInfo.baseUrl}/task_template/save`,
initApi: `get:${commonInfo.baseUrl}/task_template/detail/${id}`,
initFetchOn: `${id} !== -1`,
wrapWithPanel: false,
mode: 'horizontal',
labelAlign: 'left',
onEvent: {
submitSucc: {
actions: [
{
actionType: 'custom',
// @ts-ignore
script: (context, action, event) => {
navigate(-1)
},
},
],
},
},
body: [
{
type: 'hidden',
name: 'id',
},
{
type: 'input-text',
name: 'name',
label: '名称',
required: true,
clearable: true,
},
{
type: 'textarea',
name: 'description',
label: '描述',
required: true,
clearable: true,
},
{
type: 'input-text',
name: 'expression',
label: 'EL表达式',
required: true,
clearable: true,
},
{
type: 'button-toolbar',
buttons: [
{
type: 'action',
label: '提交',
actionType: 'submit',
level: 'primary',
},
{
type: 'action',
label: '重置',
actionType: 'reset',
},
{
type: 'action',
label: '返回',
onEvent: {
click: {
actions: [
{
actionType: 'custom',
// @ts-ignore
script: (context, action, event) => {
navigate(-1)
},
},
],
},
},
},
],
},
],
},
],
},
)}
</div>
)
}
export default React.memo(TaskTemplateSave)