1
0

feat: 增加任务模板的增删改查

This commit is contained in:
2025-09-05 19:58:12 +08:00
parent 1fc8d3160c
commit 028a578896
15 changed files with 527 additions and 99 deletions

1
.idea/modules.xml generated
View File

@@ -3,7 +3,6 @@
<component name="ProjectModuleManager"> <component name="ProjectModuleManager">
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/leopard-core/leopard-core.iml" filepath="$PROJECT_DIR$/leopard-core/leopard-core.iml" /> <module fileurl="file://$PROJECT_DIR$/leopard-core/leopard-core.iml" filepath="$PROJECT_DIR$/leopard-core/leopard-core.iml" />
<module fileurl="file://$PROJECT_DIR$/leopard-server/leopard-server.iml" filepath="$PROJECT_DIR$/leopard-server/leopard-server.iml" />
</modules> </modules>
</component> </component>
</project> </project>

View File

@@ -2,6 +2,7 @@ package com.lanyuanxiaoyao.leopard.server.controller;
import com.lanyuanxiaoyao.leopard.server.entity.Stock; import com.lanyuanxiaoyao.leopard.server.entity.Stock;
import com.lanyuanxiaoyao.leopard.server.entity.Task; 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.StockService;
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse; import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
import java.util.Arrays; import java.util.Arrays;
@@ -106,6 +107,11 @@ public class CommonOptionsController {
new Option("已退市", false) new Option("已退市", false)
) )
); );
case "task_template_type" -> GlobalResponse.responseSuccess(
Arrays.stream(TaskTemplate.Type.values())
.map(type -> new Option(type.getChineseName(), type.name()))
.toList()
);
default -> GlobalResponse.responseSuccess(List.of()); default -> GlobalResponse.responseSuccess(List.of());
}; };
} }
@@ -132,6 +138,12 @@ public class CommonOptionsController {
.toList(), .toList(),
field field
)); ));
case "task_template_type" -> GlobalResponse.responseSuccess(buildMapping(
Arrays.stream(TaskTemplate.Type.values())
.map(type -> new Mapping(type.name(), type.getChineseName()))
.toList(),
field
));
default -> GlobalResponse.responseSuccess(Map.of()); default -> GlobalResponse.responseSuccess(Map.of());
}; };
} }

View File

@@ -0,0 +1,62 @@
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;
import lombok.extern.slf4j.Slf4j;
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> {
public TaskTemplateController(TaskTemplateService service) {
super(service);
}
@Override
protected Function<Item, TaskTemplate> saveItemMapper() {
return item -> {
return switch (item.type) {
case CLASS -> {
var template = new ClassTaskTemplate();
template.setName(item.name());
template.setDescription(item.description());
template.setClazz(item.clazz());
yield 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());
}
};
}
@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,
TaskTemplate.Type type,
String clazz
) {
}
}

View File

@@ -0,0 +1,48 @@
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;
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")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract 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;
}
}

View File

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

View File

@@ -1,12 +1,9 @@
package com.lanyuanxiaoyao.leopard.server.service; package com.lanyuanxiaoyao.leopard.server.service;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.leopard.server.entity.Stock; import com.lanyuanxiaoyao.leopard.server.entity.Stock;
import com.lanyuanxiaoyao.leopard.server.repository.StockRepository; import com.lanyuanxiaoyao.leopard.server.repository.StockRepository;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport; import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -18,58 +15,13 @@ import org.springframework.stereotype.Service;
@Service @Service
public class StockService extends SimpleServiceSupport<Stock> { public class StockService extends SimpleServiceSupport<Stock> {
private final StockRepository stockRepository; private final StockRepository stockRepository;
private final TuShareService tuShareService;
private final TaskService taskService;
public StockService(StockRepository repository, TuShareService tuShareService, TaskService taskService) { public StockService(StockRepository repository) {
super(repository); super(repository);
this.stockRepository = repository; this.stockRepository = repository;
this.tuShareService = tuShareService;
this.taskService = taskService;
} }
public List<String> findDistinctIndustries() { public List<String> findDistinctIndustries() {
return stockRepository.findDistinctIndustries(); return stockRepository.findDistinctIndustries();
} }
public void refresh() {
taskService.startTask(
"股票列表刷新",
"股票列表刷新",
() -> {
var stocks = list();
var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
tuShareService.stockList()
.data()
.items()
.forEach(item -> {
var code = item.get(0);
var name = item.get(1);
var fullname = item.get(2);
var market = EnumUtil.fromString(Stock.Market.class, item.get(3));
var industry = item.get(4);
var listed = StrUtil.equals("L", item.get(5));
if (stocksMap.containsKey(code)) {
var stock = stocksMap.get(code);
stock.setName(name);
stock.setFullname(fullname);
stock.setMarket(market);
stock.setIndustry(industry);
stock.setListed(listed);
} else {
var stock = new Stock();
stock.setCode(code);
stock.setName(name);
stock.setFullname(fullname);
stock.setMarket(market);
stock.setIndustry(industry);
stock.setListed(listed);
stocks.add(stock);
}
});
repository.saveOrUpdateAllByNotNullProperties(stocks);
return null;
}
);
}
} }

View File

@@ -1,14 +1,10 @@
package com.lanyuanxiaoyao.leopard.server.service; package com.lanyuanxiaoyao.leopard.server.service;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.leopard.server.entity.Task; import com.lanyuanxiaoyao.leopard.server.entity.Task;
import com.lanyuanxiaoyao.leopard.server.repository.TaskRepository; import com.lanyuanxiaoyao.leopard.server.repository.TaskRepository;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport; import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import java.time.LocalDateTime;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
/** /**
* @author lanyuanxiaoyao * @author lanyuanxiaoyao
@@ -17,36 +13,7 @@ import org.springframework.transaction.support.TransactionTemplate;
@Slf4j @Slf4j
@Service @Service
public class TaskService extends SimpleServiceSupport<Task> { public class TaskService extends SimpleServiceSupport<Task> {
private final TransactionTemplate transactionTemplate; public TaskService(TaskRepository repository) {
public TaskService(TaskRepository repository, TransactionTemplate transactionTemplate) {
super(repository); super(repository);
this.transactionTemplate = transactionTemplate;
}
public void startTask(String name, String description, Supplier<String> executor) {
var task = new Task();
task.setName(name);
task.setDescription(description);
task.setStatus(Task.Status.RUNNING);
task.setLaunchedTime(LocalDateTime.now());
repository.save(task);
transactionTemplate.execute(status -> {
try {
var result = executor.get();
task.setStatus(Task.Status.SUCCESS);
if (StrUtil.isNotBlank(result)) {
task.setResult(result);
}
return true;
} catch (Exception e) {
task.setStatus(Task.Status.FAILURE);
task.setError(e.getMessage());
status.setRollbackOnly();
return false;
}
});
task.setFinishedTime(LocalDateTime.now());
repository.save(task);
} }
} }

View File

@@ -0,0 +1,13 @@
package com.lanyuanxiaoyao.leopard.server.service;
import com.lanyuanxiaoyao.leopard.server.entity.TaskTemplate;
import com.lanyuanxiaoyao.leopard.server.repository.TaskTemplateRepository;
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
import org.springframework.stereotype.Service;
@Service
public class TaskTemplateService extends SimpleServiceSupport<TaskTemplate> {
public TaskTemplateService(TaskTemplateRepository repository) {
super(repository);
}
}

View File

@@ -0,0 +1,41 @@
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

@@ -0,0 +1,59 @@
package com.lanyuanxiaoyao.leopard.server.service.task;
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 java.util.stream.Collectors;
import org.springframework.stereotype.Component;
@Component
public class UpdateStockInformationTask extends TaskRunner {
private final StockService stockService;
private final TuShareService tuShareService;
public UpdateStockInformationTask(TaskService taskService, StockService stockService, TuShareService tuShareService) {
super(taskService);
this.stockService = stockService;
this.tuShareService = tuShareService;
}
@Override
public String run(Map<String, Object> params) {
var stocks = stockService.list();
var stocksMap = stocks.stream().collect(Collectors.toMap(Stock::getCode, stock -> stock));
tuShareService.stockList()
.data()
.items()
.forEach(item -> {
var code = item.get(0);
var name = item.get(1);
var fullname = item.get(2);
var market = EnumUtil.fromString(Stock.Market.class, item.get(3));
var industry = item.get(4);
var listed = StrUtil.equals("L", item.get(5));
if (stocksMap.containsKey(code)) {
var stock = stocksMap.get(code);
stock.setName(name);
stock.setFullname(fullname);
stock.setMarket(market);
stock.setIndustry(industry);
stock.setListed(listed);
} else {
var stock = new Stock();
stock.setCode(code);
stock.setName(name);
stock.setFullname(fullname);
stock.setMarket(market);
stock.setIndustry(industry);
stock.setListed(listed);
stocks.add(stock);
}
});
stockService.save(stocks);
return null;
}
}

View File

@@ -2,13 +2,15 @@ import {createRoot} from 'react-dom/client'
import {createHashRouter, Navigate, type RouteObject, RouterProvider} from 'react-router' import {createHashRouter, Navigate, type RouteObject, RouterProvider} from 'react-router'
import './index.scss' import './index.scss'
import './components/amis/Registry.ts' import './components/amis/Registry.ts'
import Overview from "./pages/Overview.tsx"; import Overview from './pages/Overview.tsx'
import Root from "./pages/Root.tsx"; import Root from './pages/Root.tsx'
import Test from "./pages/Test.tsx"; import Test from './pages/Test.tsx'
import StockList from "./pages/stock/StockList.tsx"; import StockList from './pages/stock/StockList.tsx'
import StockDetail from './pages/stock/StockDetail.tsx'; import StockDetail from './pages/stock/StockDetail.tsx'
import TaskList from "./pages/task/TaskList.tsx"; import TaskList from './pages/task/TaskList.tsx'
import TaskAdd from './pages/task/TaskAdd.tsx'; import TaskAdd from './pages/task/TaskAdd.tsx'
import TaskTemplateList from './pages/task/TaskTemplateList.tsx'
import TaskTemplateSave from './pages/task/TaskTemplateSave.tsx'
const routes: RouteObject[] = [ const routes: RouteObject[] = [
{ {
@@ -37,16 +39,12 @@ const routes: RouteObject[] = [
{ {
path: 'detail/:id', path: 'detail/:id',
Component: StockDetail, Component: StockDetail,
} },
] ],
}, },
{ {
path: 'task', path: 'task',
children: [ children: [
{
index: true,
element: <Navigate to="/task/list" replace/>,
},
{ {
path: 'list', path: 'list',
Component: TaskList, Component: TaskList,
@@ -55,12 +53,25 @@ const routes: RouteObject[] = [
path: 'add', path: 'add',
Component: TaskAdd, Component: TaskAdd,
}, },
] {
path: 'template',
children: [
{
path: 'list',
Component: TaskTemplateList,
},
{
path: 'save/:id',
Component: TaskTemplateSave,
},
],
},
],
}, },
{ {
path: 'test', path: 'test',
Component: Test, Component: Test,
} },
], ],
}, },
] ]

View File

@@ -44,6 +44,16 @@ const menus = {
path: '/task', path: '/task',
name: '任务', name: '任务',
icon: <UnorderedListOutlined/>, icon: <UnorderedListOutlined/>,
routes: [
{
path: '/task/list',
name: '任务列表',
},
{
path: '/task/template/list',
name: '任务模板',
}
]
}, },
{ {
path: '/test', path: '/test',

View File

@@ -0,0 +1,113 @@
import React from 'react'
import {amisRender, commonInfo, crudCommonOptions, paginationTemplate, remoteMappings} from '../../util/amis.tsx'
import {useNavigate} from 'react-router'
function TaskTemplateList() {
const navigate = useNavigate()
return (
<div className="task-template-list">
{amisRender(
{
type: 'page',
title: '任务模板',
body: [
{
type: 'crud',
api: {
method: 'post',
url: `${commonInfo.baseUrl}/task_template/list`,
data: {
page: {
index: '${page}',
size: '${perPage}',
},
},
},
...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')
},
},
],
},
},
},
],
),
columns: [
{
name: 'name',
label: '名称',
width: 150,
},
{
name: 'description',
label: '描述',
},
{
name: 'type',
label: '类型',
width: 100,
...remoteMappings('task_template_type', 'type'),
},
{
type: 'operation',
label: '操作',
width: 100,
buttons: [
{
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',
type: 'action',
label: '删除',
level: 'link',
actionType: 'ajax',
api: `get:${commonInfo.baseUrl}/task_template/remove/\${id}`,
confirmText: '确认删除模板[${name}]',
confirmTitle: "删除",
}
],
},
],
},
],
},
)}
</div>
)
}
export default React.memo(TaskTemplateList)

View File

@@ -0,0 +1,96 @@
import React from 'react'
import {amisRender, commonInfo, remoteOptions} 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: '名称',
require: true,
clearable: true,
},
{
type: 'textarea',
name: 'description',
label: '描述',
require: 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,
clearable: true,
},
{
type: 'button-toolbar',
buttons: [
{
type: 'action',
label: '提交',
actionType: 'submit',
level: 'primary',
},
{
type: 'action',
label: '重置',
actionType: 'reset',
},
],
},
],
},
],
},
)}
</div>
)
}
export default React.memo(TaskTemplateSave)