feat(sj_1.0.0): 重试任务-初稿

This commit is contained in:
dhb52 2024-04-21 12:45:12 +08:00
parent 82ec1b2296
commit 7e973cede9
15 changed files with 725 additions and 1 deletions

View File

@ -94,6 +94,20 @@ export const groupConfigYesOrNoRecord: Record<Api.GroupConfig.YesOrNoType, App.I
};
export const groupConfigYesOrNoOptions = transformRecordToNumberOption(groupConfigYesOrNoRecord, true);
export const retryTaskStatusTypeRecord: Record<Api.RetryTask.RetryStatusType, App.I18n.I18nKey> = {
0: 'page.retryTask.retryStatusType.retrying',
1: 'page.retryTask.retryStatusType.finished',
2: 'page.retryTask.retryStatusType.maxRetry',
3: 'page.retryTask.retryStatusType.paused'
};
export const retryTaskStatusTypeOptions = transformRecordToNumberOption(retryTaskStatusTypeRecord);
export const retryTaskTypeRecord: Record<Api.RetryTask.TaskType, App.I18n.I18nKey> = {
1: 'page.retryTask.taskTypeDict.retry',
2: 'page.retryTask.taskTypeDict.callback'
};
export const retryTaskTypeOptions = transformRecordToNumberOption(retryTaskTypeRecord);
export const backOffRecord: Record<Api.RetryScene.BackOff, App.I18n.I18nKey> = {
1: 'page.retryScene.backOffItem.delayLevel',
2: 'page.retryScene.backOffItem.fixed',

View File

@ -170,6 +170,7 @@ const local: App.I18n.Schema = {
notify_recipient: 'Notify recipient',
notify_scene: 'Notify scene',
retry: 'Retry task',
retry_task: 'Retry task',
retry_scene: 'Retry scene',
'manage_user-detail': 'User Detail',
manage_role: 'Role Manage',
@ -605,6 +606,44 @@ const local: App.I18n.Schema = {
weCom: 'WeCom',
lark: 'Lark'
},
retryTask: {
title: 'RetryTask List',
uniqueId: 'UniqueId',
groupName: 'Group name',
sceneName: 'Scene name',
idempotentId: 'Idempotent ID',
bizNo: 'Business Number',
executorName: 'Actuator name',
argsStr: 'Actuator arguments',
nextTriggerAt: 'Next trigger time',
retryCount: 'Number of retries',
retryStatus: 'Retry status',
taskType: 'Task type',
form: {
retryStatus: 'Please enter Retry status',
bizNo: 'Please enter Business Number',
uniqueId: 'Please enter UniqueId',
groupName: 'Please enter Group name',
argsStr: 'Please enter Execution method parameters',
sceneName: 'Please enter Scene name',
executorName: 'Please enter Actuator name',
taskType: 'Please enter Task type',
idempotentId: 'Please enter Idempotent ID'
},
retryStatusType: {
retrying: 'Retrying',
finished: 'Finished',
maxRetry: 'Maximum retry count reached',
paused: 'Paused'
},
taskTypeDict: {
retry: 'Retry data',
callback: 'Callback data'
},
generateIdempotentId: 'Generate by client',
addRetryTask: 'Add Retry task',
editRetryTask: 'Add Retry task'
},
retryScene: {
title: 'Scene List',
groupName: 'Group name',

View File

@ -171,6 +171,7 @@ const local: App.I18n.Schema = {
notify_recipient: '通知人',
notify_scene: '通知场景',
retry: '重试任务',
retry_task: '重试任务',
retry_scene: '重试场景',
'manage_user-detail': '用户详情',
manage_role: '角色管理',
@ -601,6 +602,44 @@ const local: App.I18n.Schema = {
weCom: '企业微信',
lark: '飞书'
},
retryTask: {
title: '重试任务列表',
uniqueId: 'UniqueId',
groupName: '组名称',
sceneName: '场景名称',
idempotentId: '幂等ID',
bizNo: '业务编号',
executorName: '执行器名称',
argsStr: '方法参数',
nextTriggerAt: '下次触发时间',
retryCount: '重试次数',
retryStatus: '重试状态',
taskType: '任务类型',
form: {
retryStatus: '请输入重试状态',
bizNo: '请输入业务编号',
uniqueId: '请输入UniqueId',
groupName: '请输入组名称',
argsStr: '请输入执行方法参数',
sceneName: '请输入场景名称',
executorName: '请输入执行器名称',
taskType: '请输入任务类型',
idempotentId: '请输入幂等id'
},
retryStatusType: {
retrying: '处理中',
finished: '完成',
maxRetry: '最大重试次数',
paused: '暂停'
},
taskTypeDict: {
retry: '重试数据',
callback: '回调数据'
},
generateIdempotentId: '通过客户端生成',
addRetryTask: '新增重试任务',
editRetryTask: '编辑重试任务'
},
retryScene: {
title: '场景列表',
groupName: '组名',

View File

@ -41,5 +41,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
notify_scene: () => import("@/views/notify/scene/index.vue"),
pods: () => import("@/views/pods/index.vue"),
retry_scene: () => import("@/views/retry/scene/index.vue"),
retry_task: () => import("@/views/retry/task/index.vue"),
"user-center": () => import("@/views/user-center/index.vue"),
};

View File

@ -398,6 +398,15 @@ export const generatedRoutes: GeneratedRoute[] = [
title: 'retry_scene',
i18nKey: 'route.retry_scene'
}
},
{
name: 'retry_task',
path: '/retry/task',
component: 'view.retry_task',
meta: {
title: 'retry_task',
i18nKey: 'route.retry_task'
}
}
]
},

View File

@ -182,6 +182,7 @@ const routeMap: RouteMap = {
"pods": "/pods",
"retry": "/retry",
"retry_scene": "/retry/scene",
"retry_task": "/retry/task",
"user-center": "/user-center"
};

View File

@ -6,4 +6,5 @@ export * from './namespace';
export * from './system-manage';
export * from './notify';
export * from './group-config';
export * from './retry-task';
export * from './retry';

View File

@ -0,0 +1,28 @@
import { request } from '../request';
/** get retryTask list */
export function fetchGetRetryTaskList(params?: Api.RetryTask.RetryTaskSearchParams) {
return request<Api.RetryTask.RetryTaskList>({
url: '/retry-task/list',
method: 'get',
params
});
}
/** add retryTask */
export function fetchAddRetryTask(data: Api.RetryTask.RetryTask) {
return request<boolean>({
url: '/retry-task',
method: 'post',
data
});
}
/** edit retryTask */
export function fetchEditRetryTask(data: Api.RetryTask.RetryTask) {
return request<boolean>({
url: '/retry-task',
method: 'put',
data
});
}

View File

@ -11,7 +11,7 @@ export function fetchGetRetryScenePageList(params?: Api.RetryScene.SceneSearchPa
/** get retry scene list */
export function fetchGetRetrySceneList(params?: Api.RetryScene.SceneSearchParams) {
return request<Api.RetryScene.SceneList>({
return request<Api.RetryScene.Scene[]>({
url: '/scene-config/list',
method: 'get',
params

50
src/typings/api.d.ts vendored
View File

@ -611,6 +611,56 @@ declare namespace Api {
type AlarmType = 1 | 2 | 3 | 4;
}
/**
* namespace RetryTask
*
* backend api module: "retryTask"
*/
namespace RetryTask {
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'page' | 'size'>;
type RetryStatusType = 0 | 1 | 2 | 3;
type TaskType = 1 | 2;
/** RetryTask */
type RetryTask = Common.CommonRecord<{
/** UniqueId */
uniqueId?: string;
/** 组名称 */
groupName: string;
/** 场景名称 */
sceneName: string;
/** 幂等id */
idempotentId: string;
/** 业务编号 */
bizNo: string;
/** 执行器名称 */
executorName: string;
/** 执行方法参数 */
argsStr: string;
/** 扩展字段 */
extAttrs?: string;
/** 下次触发时间 */
nextTriggerAt?: string;
/** 重试次数 */
retryCount?: number;
/** 重试状态 0、重试中 1、重试完成 2、最大次数 3、暂停 */
retryStatus: RetryStatusType;
/** 任务类型 1、重试数据 2、回调数据 */
taskType?: TaskType;
}>;
/** RetryTask search params */
type RetryTaskSearchParams = CommonType.RecordNullable<
Pick<Api.RetryTask.RetryTask, 'uniqueId' | 'groupName' | 'sceneName' | 'idempotentId' | 'bizNo' | 'retryStatus'> &
CommonSearchParams
>;
/** RetryTask list */
type RetryTaskList = Common.PaginatingQueryRecord<RetryTask>;
}
/**
* namespace Scene
*

38
src/typings/app.d.ts vendored
View File

@ -774,6 +774,44 @@ declare namespace App {
weCom: string;
lark: string;
};
retryTask: {
title: string;
uniqueId: string;
groupName: string;
sceneName: string;
idempotentId: string;
bizNo: string;
executorName: string;
argsStr: string;
nextTriggerAt: string;
retryCount: string;
retryStatus: string;
taskType: string;
form: {
retryStatus: string;
bizNo: string;
uniqueId: string;
groupName: string;
argsStr: string;
sceneName: string;
executorName: string;
taskType: string;
idempotentId: string;
};
retryStatusType: {
retrying: string;
finished: string;
maxRetry: string;
paused: string;
};
taskTypeDict: {
retry: string;
callback: string;
};
generateIdempotentId: string;
addRetryTask: string;
editRetryTask: string;
};
retryScene: {
title: string;
groupName: string;

View File

@ -56,6 +56,7 @@ declare module "@elegant-router/types" {
"pods": "/pods";
"retry": "/retry";
"retry_scene": "/retry/scene";
"retry_task": "/retry/task";
"user-center": "/user-center";
};
@ -150,6 +151,7 @@ declare module "@elegant-router/types" {
| "notify_scene"
| "pods"
| "retry_scene"
| "retry_task"
| "user-center"
>;

View File

@ -0,0 +1,207 @@
<script setup lang="tsx">
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { fetchGetRetryTaskList } from '@/service/api';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { retryTaskStatusTypeRecord, retryTaskTypeRecord } from '@/constants/business';
import RetryTaskOperateDrawer from './modules/retry-task-operate-drawer.vue';
import RetryTaskSearch from './modules/retry-task-search.vue';
const appStore = useAppStore();
const { columns, columnChecks, data, getData, loading, mobilePagination, searchParams, resetSearchParams } = useTable({
apiFn: fetchGetRetryTaskList,
apiParams: {
page: 1,
size: 10,
uniqueId: null,
groupName: null,
sceneName: null,
idempotentId: null,
bizNo: null,
retryStatus: null
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
key: 'uniqueId',
title: $t('page.retryTask.uniqueId'),
align: 'left',
minWidth: 120
},
{
key: 'groupName',
title: $t('page.retryTask.groupName'),
align: 'center',
minWidth: 120
},
{
key: 'sceneName',
title: $t('page.retryTask.sceneName'),
align: 'center',
minWidth: 120
},
{
key: 'idempotentId',
title: $t('page.retryTask.idempotentId'),
align: 'center',
minWidth: 120
},
{
key: 'bizNo',
title: $t('page.retryTask.bizNo'),
align: 'center',
minWidth: 120
},
{
key: 'nextTriggerAt',
title: $t('page.retryTask.nextTriggerAt'),
align: 'center',
minWidth: 120
},
{
key: 'retryCount',
title: $t('page.retryTask.retryCount'),
align: 'center',
minWidth: 120
},
{
key: 'retryStatus',
title: $t('page.retryTask.retryStatus'),
align: 'center',
minWidth: 120,
render: row => {
if (row.retryStatus === null) {
return null;
}
const tagMap: Record<Api.RetryTask.RetryStatusType, NaiveUI.ThemeColor> = {
0: 'info',
1: 'success',
2: 'error',
3: 'warning'
};
const label = $t(retryTaskStatusTypeRecord[row.retryStatus!]);
return <NTag type={tagMap[row.retryStatus!]}>{label}</NTag>;
}
},
{
key: 'taskType',
title: $t('page.retryTask.taskType'),
align: 'center',
minWidth: 120,
render: row => {
if (row.taskType === null) {
return null;
}
const tagMap: Record<Api.RetryTask.TaskType, NaiveUI.ThemeColor> = {
1: 'warning',
2: 'error'
};
const label = $t(retryTaskTypeRecord[row.taskType!]);
return <NTag type={tagMap[row.taskType!]}>{label}</NTag>;
}
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => (
<div class="flex-center gap-8px">
<NButton type="primary" ghost size="small" onClick={() => edit(row.id!)}>
{$t('common.edit')}
</NButton>
<NPopconfirm onPositiveClick={() => handleDelete(row.id!)}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" ghost size="small">
{$t('common.delete')}
</NButton>
)
}}
</NPopconfirm>
</div>
)
}
]
});
const {
drawerVisible,
operateType,
editingData,
handleAdd,
handleEdit,
checkedRowKeys,
onDeleted
// closeDrawer
} = useTableOperate(data, getData);
function handleDelete(id: string) {
// request
console.log(id);
onDeleted();
}
function edit(id: string) {
handleEdit(id);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<RetryTaskSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
<NCard
:title="$t('page.retryTask.title')"
:bordered="false"
size="small"
class="sm:flex-1-hidden card-wrapper"
header-class="view-card-header"
>
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
@add="handleAdd"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<RetryTaskOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getData"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,202 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import OperateDrawer from '@/components/common/operate-drawer.vue';
import { $t } from '@/locales';
import { fetchAddRetryTask, fetchGetAllGroupNameList, fetchGetRetrySceneList } from '@/service/api';
import { translateOptions, translateOptions2 } from '@/utils/common';
import { retryTaskStatusTypeOptions } from '@/constants/business';
defineOptions({
name: 'RetryTaskOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.RetryTask.RetryTask | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { defaultRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: $t('page.retryTask.addRetryTask'),
edit: $t('page.retryTask.editRetryTask')
};
return titles[props.operateType];
});
type Model = Pick<
Api.RetryTask.RetryTask,
'groupName' | 'sceneName' | 'idempotentId' | 'bizNo' | 'executorName' | 'argsStr' | 'retryStatus'
>;
const model: Model = reactive(createDefaultModel());
/** 组列表 */
const groupNameList = ref<string[]>([]);
/** 场景列表 */
const sceneNameList = ref<string[]>([]);
function createDefaultModel(): Model {
return {
groupName: '',
sceneName: '',
idempotentId: '',
bizNo: '',
executorName: '',
argsStr: '',
retryStatus: 0
};
}
type RuleKey = Extract<
keyof Model,
'groupName' | 'sceneName' | 'idempotentId' | 'bizNo' | 'executorName' | 'argsStr' | 'retryStatus'
>;
const rules: Record<RuleKey, App.Global.FormRule> = {
groupName: defaultRequiredRule,
sceneName: defaultRequiredRule,
idempotentId: defaultRequiredRule,
bizNo: defaultRequiredRule,
executorName: defaultRequiredRule,
argsStr: defaultRequiredRule,
retryStatus: defaultRequiredRule
};
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
return;
}
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData);
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
if (props.operateType === 'add') {
const { groupName, sceneName, idempotentId, bizNo, executorName, argsStr, retryStatus } = model;
fetchAddRetryTask({ groupName, sceneName, idempotentId, bizNo, executorName, argsStr, retryStatus });
window.$message?.success($t('common.addSuccess'));
}
// if (props.operateType === 'edit') {
// const { ... } = model;
// fetchEditRetryTask({ ... });
// window.$message?.success($t('common.updateSuccess'));
// }
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenEdit();
restoreValidation();
}
});
async function getGroupNameList() {
const res = await fetchGetAllGroupNameList();
groupNameList.value = res.data as string[];
}
async function handleGroupNameUpdate(groupName: string) {
if (groupName) {
const res = await fetchGetRetrySceneList({ groupName });
sceneNameList.value = res.data!.map((scene: Api.RetryScene.Scene) => scene.sceneName);
} else {
model.sceneName = '';
sceneNameList.value = [];
}
}
function setIdempotentId() {
model.idempotentId = 'test';
}
onMounted(() => {
nextTick(() => {
getGroupNameList();
});
});
</script>
<template>
<OperateDrawer v-model="visible" :title="title" @handle-submit="handleSubmit">
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem :label="$t('page.retryTask.groupName')" path="groupName">
<NSelect
v-model:value="model.groupName"
:placeholder="$t('page.retryTask.form.groupName')"
:options="translateOptions2(groupNameList)"
@update-value="handleGroupNameUpdate"
/>
</NFormItem>
<NFormItem :label="$t('page.retryTask.sceneName')" path="sceneName">
<NSelect
v-model:value="model.sceneName"
:placeholder="$t('page.retryTask.form.sceneName')"
:options="translateOptions2(sceneNameList)"
/>
</NFormItem>
<NFormItem :label="$t('page.retryTask.idempotentId')" path="idempotentId">
<NInputGroup>
<NInput v-model:value="model.idempotentId" :placeholder="$t('page.retryTask.form.idempotentId')" />
<NButton type="primary" ghost :disabled="props.operateType === 'edit'" @click="setIdempotentId">
{{ $t('page.retryTask.generateIdempotentId') }}
</NButton>
</NInputGroup>
</NFormItem>
<NFormItem :label="$t('page.retryTask.bizNo')" path="bizNo">
<NInput v-model:value="model.bizNo" :placeholder="$t('page.retryTask.form.bizNo')" />
</NFormItem>
<NFormItem :label="$t('page.retryTask.executorName')" path="executorName">
<NInput v-model:value="model.executorName" :placeholder="$t('page.retryTask.form.executorName')" />
</NFormItem>
<NFormItem :label="$t('page.retryTask.argsStr')" path="argsStr">
<NInput v-model:value="model.argsStr" type="textarea" :placeholder="$t('page.retryTask.form.argsStr')" />
</NFormItem>
<NFormItem :label="$t('page.retryTask.retryStatus')" path="retryStatus">
<NSelect
v-model:value="model.retryStatus"
:placeholder="$t('page.retryTask.form.retryStatus')"
:options="translateOptions(retryTaskStatusTypeOptions)"
/>
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.save') }}</NButton>
</NSpace>
</template>
</OperateDrawer>
</template>
<style scoped></style>

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { $t } from '@/locales';
import { translateOptions, translateOptions2 } from '@/utils/common';
import { retryTaskStatusTypeOptions } from '@/constants/business';
import { fetchGetAllGroupNameList, fetchGetRetrySceneList } from '@/service/api';
defineOptions({
name: 'RetryTaskSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const model = defineModel<Api.RetryTask.RetryTaskSearchParams>('model', { required: true });
/** 组列表 */
const groupNameList = ref<string[]>([]);
/** 场景列表 */
const sceneNameList = ref<string[]>([]);
function reset() {
emit('reset');
}
function search() {
emit('search');
}
async function getGroupNameList() {
const res = await fetchGetAllGroupNameList();
groupNameList.value = res.data as string[];
}
async function handleGroupNameUpdate(groupName: string) {
if (groupName) {
const res = await fetchGetRetrySceneList({ groupName });
sceneNameList.value = res.data!.map((scene: Api.RetryScene.Scene) => scene.sceneName);
} else {
model.value.sceneName = '';
sceneNameList.value = [];
}
}
onMounted(() => {
getGroupNameList();
});
</script>
<template>
<SearchForm :model="model" @search="search" @reset="reset">
<NFormItemGi span="24 s:12 m:6" :label="$t('page.retryTask.uniqueId')" path="uniqueId" class="py-1 pr-24px">
<NInput v-model:value="model.uniqueId" :placeholder="$t('page.retryTask.form.uniqueId')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.retryTask.groupName')" path="groupName" class="py-1 pr-24px">
<NSelect
v-model:value="model.groupName"
:placeholder="$t('page.retryTask.form.groupName')"
:options="translateOptions2(groupNameList)"
filterable
clearable
@update:value="handleGroupNameUpdate"
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.retryTask.sceneName')" path="sceneName" class="py-1 pr-24px">
<NSelect
v-model:value="model.sceneName"
:placeholder="$t('page.retryTask.form.sceneName')"
:options="translateOptions2(sceneNameList)"
clearable
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.retryTask.idempotentId')" path="idempotentId" class="py-1 pr-24px">
<NInput v-model:value="model.idempotentId" :placeholder="$t('page.retryTask.form.idempotentId')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.retryTask.bizNo')" path="bizNo" class="py-1 pr-24px">
<NInput v-model:value="model.bizNo" :placeholder="$t('page.retryTask.form.bizNo')" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" :label="$t('page.retryTask.retryStatus')" path="retryStatus" class="py-1 pr-24px">
<NSelect
v-model:value="model.retryStatus"
:placeholder="$t('page.retryTask.form.retryStatus')"
:options="translateOptions(retryTaskStatusTypeOptions)"
/>
</NFormItemGi>
</SearchForm>
</template>
<style scoped></style>