feat: 2.6.0

1. 新增表达式校验接口
2. 修复前端任务项刷新参数错误
This commit is contained in:
byteblogs168 2024-01-17 23:49:46 +08:00
parent bd2e5b5633
commit ac0003ae1d
10 changed files with 345 additions and 51 deletions

View File

@ -1,5 +1,6 @@
package com.aizuda.easy.retry.server.web.controller;
import com.aizuda.easy.retry.server.common.dto.DecisionConfig;
import com.aizuda.easy.retry.server.web.annotation.LoginRequired;
import com.aizuda.easy.retry.server.web.annotation.RoleEnum;
import com.aizuda.easy.retry.server.web.model.base.PageResult;
@ -78,4 +79,10 @@ public class WorkflowController {
return workflowService.getWorkflowNameList(keywords, workflowId);
}
@PostMapping("/check-node-expression")
@LoginRequired(role = RoleEnum.ADMIN)
public void checkNodeExpression(@RequestBody DecisionConfig decisionConfig) {
workflowService.checkNodeExpression(decisionConfig);
}
}

View File

@ -1,5 +1,6 @@
package com.aizuda.easy.retry.server.web.service;
import com.aizuda.easy.retry.server.common.dto.DecisionConfig;
import com.aizuda.easy.retry.server.web.model.base.PageResult;
import com.aizuda.easy.retry.server.web.model.request.WorkflowQueryVO;
import com.aizuda.easy.retry.server.web.model.request.WorkflowRequestVO;
@ -31,4 +32,6 @@ public interface WorkflowService {
Boolean trigger(Long id);
List<WorkflowResponseVO> getWorkflowNameList(String keywords, Long workflowId);
void checkNodeExpression(DecisionConfig decisionConfig);
}

View File

@ -147,6 +147,8 @@ public class WorkflowHandler {
DecisionConfig decision = nodeInfo.getDecision();
Assert.notNull(decision, () -> new EasyRetryServerException("【{}】配置信息不能为空", nodeInfo.getNodeName()));
Assert.notBlank(decision.getNodeExpression(), ()-> new EasyRetryServerException("【{}】表达式不能为空", nodeInfo.getNodeName()));
Assert.notNull(decision.getDefaultDecision(), () -> new EasyRetryServerException("【{}】默认决策不能为空", nodeInfo.getNodeName()));
Assert.notNull(decision.getExpressionType(), () -> new EasyRetryServerException("【{}】表达式类型不能为空", nodeInfo.getNodeName()));
workflowNode.setNodeInfo(JsonUtil.toJsonString(decision));
}

View File

@ -5,10 +5,14 @@ import cn.hutool.core.util.HashUtil;
import cn.hutool.core.util.StrUtil;
import com.aizuda.easy.retry.common.core.constant.SystemConstants;
import com.aizuda.easy.retry.common.core.enums.StatusEnum;
import com.aizuda.easy.retry.common.core.expression.ExpressionEngine;
import com.aizuda.easy.retry.common.core.expression.ExpressionFactory;
import com.aizuda.easy.retry.common.core.util.JsonUtil;
import com.aizuda.easy.retry.server.common.WaitStrategy;
import com.aizuda.easy.retry.server.common.config.SystemProperties;
import com.aizuda.easy.retry.server.common.dto.DecisionConfig;
import com.aizuda.easy.retry.server.common.dto.JobTaskConfig;
import com.aizuda.easy.retry.server.common.enums.ExpressionTypeEnum;
import com.aizuda.easy.retry.server.common.enums.JobTaskExecutorSceneEnum;
import com.aizuda.easy.retry.server.common.exception.EasyRetryServerException;
import com.aizuda.easy.retry.server.common.strategy.WaitStrategies;
@ -19,6 +23,7 @@ import com.aizuda.easy.retry.server.job.task.dto.WorkflowPartitionTaskDTO;
import com.aizuda.easy.retry.server.job.task.dto.WorkflowTaskPrepareDTO;
import com.aizuda.easy.retry.server.job.task.support.WorkflowPrePareHandler;
import com.aizuda.easy.retry.server.job.task.support.WorkflowTaskConverter;
import com.aizuda.easy.retry.server.job.task.support.expression.ExpressionInvocationHandler;
import com.aizuda.easy.retry.server.web.model.base.PageResult;
import com.aizuda.easy.retry.server.web.model.request.SceneConfigRequestVO;
import com.aizuda.easy.retry.server.web.model.request.UserSessionVO;
@ -320,4 +325,13 @@ public class WorkflowServiceImpl implements WorkflowService {
return WorkflowConverter.INSTANCE.toWorkflowResponseVO(selectPage.getRecords());
}
@Override
public void checkNodeExpression(DecisionConfig decisionConfig) {
ExpressionEngine realExpressionEngine = ExpressionTypeEnum.valueOf(decisionConfig.getExpressionType());
Assert.notNull(realExpressionEngine, () -> new EasyRetryServerException("表达式引擎不存在"));
ExpressionInvocationHandler invocationHandler = new ExpressionInvocationHandler(realExpressionEngine);
ExpressionEngine expressionEngine = ExpressionFactory.getExpressionEngine(invocationHandler);
expressionEngine.eval(decisionConfig.getNodeExpression(), new HashMap<>());
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,16 +1,16 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Easy Retry</title>
<script type="module" crossorigin src="./assets/1l1Q8kYs.js"></script>
<link rel="stylesheet" crossorigin href="./assets/qOmWCRIC.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Easy Retry</title>
<script type="module" crossorigin src="./assets/O1TCl_bu.js"></script>
<link rel="stylesheet" crossorigin href="./assets/gKrsqr4E.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -0,0 +1,264 @@
<template>
<a-modal
:visible="visible"
width="100%"
wrap-class-name="full-modal"
:footer="null"
title="日志详情"
@cancel="onCancel">
<div class="log">
<div class="scroller">
<div class="gutters">
<div style="margin-top: 4px"></div>
<div v-for="(log, index) in logList" :key="index">
<div class="gutter-element">{{ index + 1 }}</div>
<div style="height: 25px"></div>
</div>
</div>
<div class="content">
<div class="line" v-for="log in logList" :key="log.time_stamp">
<div class="flex">
<div class="text" style="color: #2db7f5">{{ timestampToDate(log.time_stamp) }}</div>
<div class="text" :style="{ color: LevelEnum[log.level].color }">
{{ log.level.length === 4 ? log.level + '\t' : log.level }}
</div>
<div class="text" style="color: #00a3a3">[{{ log.thread }}]</div>
<div class="text" style="color: #a771bf; font-weight: 500">{{ log.location }}</div>
<div class="text">:</div>
</div>
<div class="text" style="font-size: 16px">{{ log.message }}</div>
</div>
<div style="text-align: center; width: 100vw">
<a-spin :indicator="indicator" :spinning="!finished"/>
</div>
</div>
</div>
</div>
</a-modal>
</template>
<script>
import request from '@/utils/request'
export default {
name: 'JobBatchLog',
components: {},
props: {
open: {
type: Boolean,
default: false
},
record: {
type: Object,
default: () => {}
},
value: {
type: Array,
default: () => []
}
},
watch: {
value: {
deep: true,
immediate: true,
handler (val) {
this.logList = val
}
},
open: {
deep: true,
immediate: true,
handler (val) {
this.visible = val
}
}
},
data () {
return {
visible: false,
finished: false,
logList: [],
interval: null,
startId: 0,
fromIndex: 0,
indicator: <a-icon type="loading" style="font-size: 24px; color: '#d9d9d9'" spin/>,
LevelEnum: {
DEBUG: {
name: 'DEBUG',
color: '#2647cc'
},
INFO: {
name: 'INFO',
color: '#5c962c'
},
WARN: {
name: 'WARN',
color: '#da9816'
},
ERROR: {
name: 'ERROR',
color: '#dc3f41'
}
}
}
},
mounted () {
this.getLogList()
this.interval = setInterval(() => {
this.getLogList()
}, 1000)
},
methods: {
onCancel () {
clearInterval(this.interval)
this.$emit('update:open', false)
},
getLogList () {
if (!this.finished) {
request(
{
url: '/job/log/list',
method: 'get',
params: {
taskBatchId: this.record.taskBatchId,
jobId: this.record.jobId,
taskId: this.record.id,
startId: this.startId,
fromIndex: this.fromIndex,
size: 50
}
}
)
.then((res) => {
this.finished = res.data.finished
this.startId = res.data.nextStartId
this.fromIndex = res.data.fromIndex
if (res.data.message) {
this.logList.push(...res.data.message)
}
})
.catch(() => {
this.finished = true
})
} else {
clearInterval(this.interval)
}
},
timestampToDate (timestamp) {
const date = new Date(Number.parseInt(timestamp.toString()))
const year = date.getFullYear()
const month =
(date.getMonth() + 1).toString().length === 1 ? '0' + (date.getMonth() + 1) : (date.getMonth() + 1).toString()
const day = date.getDate()
const hours = date.getHours()
const minutes = date.getMinutes().toString().length === 1 ? '0' + date.getMinutes() : date.getMinutes().toString()
const seconds = date.getSeconds().toString().length === 1 ? '0' + date.getSeconds() : date.getSeconds().toString()
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
}
}
</script>
<style scoped lang="less">
.log {
height: calc(100vh - 56px);
color: #abb2bf;
background-color: #282c34;
position: relative !important;
box-sizing: border-box;
display: flex !important;
flex-direction: column;
.scroller {
height: 100%;
overflow: auto;
display: flex !important;
align-items: flex-start !important;
font-family: monospace;
line-height: 1.4;
position: relative;
z-index: 0;
.gutters {
min-height: 108px;
position: sticky;
background-color: #1e1f22;
color: #7d8799;
border: none;
flex-shrink: 0;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
inset-inline-start: 0;
z-index: 200;
.gutter-element {
height: 25px;
font-size: 14px;
padding: 0 8px 0 5px;
min-width: 20px;
text-align: right;
white-space: nowrap;
box-sizing: border-box;
color: #7d8799;
display: flex;
align-items: center;
justify-content: flex-end;
}
}
.content {
tab-size: 4;
caret-color: transparent !important;
margin: 0;
flex-grow: 2;
flex-shrink: 0;
// display: block;
white-space: pre;
word-wrap: normal;
box-sizing: border-box;
min-height: 100%;
padding: 4px 8px;
outline: none;
color: #bcbec4;
.line {
height: 25px;
caret-color: transparent !important;
font-size: 16px;
// background-color: #6699ff0b;
display: contents;
padding: 0 2px 0 6px;
.flex{
display: flex;
align-items: center;
gap: 5px;
}
.text {
font-size: 16px;
height: 25px;
}
}
}
}
}
/deep/ .ant-modal {
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
}
/deep/ .ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh);
}
/deep/ .ant-modal-body {
flex: 1;
padding: 0;
}
</style>

View File

@ -80,6 +80,9 @@
rowKey="id"
@expand="getRows"
>
<span slot="log" slot-scope="text, record">
<a @click="getLogRows(record)">点击查看</a>
</span>
<span slot="serial" slot-scope="text, record">
{{ record.id }}
</span>
@ -91,20 +94,8 @@
<span slot="clientInfo" slot-scope="text">
{{ text !== '' ? text.split('@')[1] : '' }}
</span>
<a-table
slot="expandedRowRender"
slot-scope="record"
:columns="logColumns"
:data-source="record.logData"
:pagination="false"
rowKey="id"
>
<span slot="serial" slot-scope="text, record">
{{ record.id }}
</span>
</a-table>
</a-table>
<job-batch-log v-if="logOpen && record" :open.sync="logOpen" :record="record" />
</a-card>
</template>
@ -115,10 +106,12 @@ import { STable } from '@/components'
import { jobLogList, jobTaskList } from '@/api/jobApi'
import enums from '@/utils/jobEnum'
import moment from 'moment/moment'
import JobBatchLog from './JobBatchLog'
export default {
name: 'JobTaskList',
components: {
JobBatchLog,
AInput,
ATextarea,
STable
@ -129,6 +122,8 @@ export default {
visible: false,
// /
advanced: false,
logOpen: false,
record: {},
//
queryParam: {
startId: 0,
@ -139,6 +134,11 @@ export default {
taskStatus: enums.taskStatus,
//
columns: [
{
title: '日志',
scopedSlots: { customRender: 'log' },
width: '5%'
},
{
title: 'ID',
scopedSlots: { customRender: 'serial' },
@ -275,6 +275,10 @@ export default {
onSelectChange (selectedRowKeys, selectedRows) {
this.selectedRowKeys = selectedRowKeys
this.selectedRows = selectedRows
},
getLogRows (task) {
this.record = task
this.logOpen = true
}
}
}