perf: 支持批量修改通知和定时

pull/436/head
xiaojunnuo 2025-06-18 12:29:43 +08:00
parent 73fa937f5c
commit e11b3becfd
9 changed files with 340 additions and 74 deletions

View File

@ -52,7 +52,9 @@ export type Stage = Runnable & {
export type Trigger = { export type Trigger = {
id: string; id: string;
title: string; title: string;
cron: string; props: {
cron: string;
};
type: string; type: string;
}; };
@ -78,14 +80,13 @@ export type EmailOptions = {
receivers: string[]; receivers: string[];
}; };
export type NotificationWhen = "error" | "success" | "turnToSuccess" | "start"; export type NotificationWhen = "error" | "success" | "turnToSuccess" | "start";
export type NotificationType = "email" | "url"; export type NotificationType = "email" | "other";
export type Notification = { export type Notification = {
type: NotificationType; type: NotificationType;
when: NotificationWhen[]; when: NotificationWhen[];
options: EmailOptions; options?: EmailOptions;
notificationId: number; notificationId: number;
title: string; title: string;
subType: string;
}; };
export type Pipeline = Runnable & { export type Pipeline = Runnable & {

View File

@ -84,6 +84,23 @@ export async function BatchUpdateGroup(pipelineIds: number[], groupId: number):
}); });
} }
export async function BatchUpdateTrigger(pipelineIds: number[], trigger: any): Promise<void> {
return await request({
url: apiPrefix + "/batchUpdateTrigger",
method: "post",
data: { ids: pipelineIds, trigger },
});
}
export async function BatchUpdateNotificaiton(pipelineIds: number[], notification: any): Promise<void> {
return await request({
url: apiPrefix + "/batchUpdateNotification",
method: "post",
data: { ids: pipelineIds, notification },
});
}
export async function BatchDelete(pipelineIds: number[]): Promise<void> { export async function BatchDelete(pipelineIds: number[]): Promise<void> {
return await request({ return await request({
url: apiPrefix + "/batchDelete", url: apiPrefix + "/batchDelete",
@ -99,6 +116,8 @@ export async function BatchRerun(pipelineIds: number[]): Promise<void> {
}); });
} }
export async function GetFiles(pipelineId: number) { export async function GetFiles(pipelineId: number) {
return await request({ return await request({
url: historyApiPrefix + "/files", url: historyApiPrefix + "/files",

View File

@ -0,0 +1,96 @@
<template>
<fs-button icon="mdi:format-list-group" type="link" text="修改通知" @click="openFormDialog"></fs-button>
</template>
<script setup lang="ts">
import * as api from "../api";
import { useFormWrapper } from "@fast-crud/fast-crud";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import { ref } from "vue";
const props = defineProps<{
selectedRowKeys: any[];
}>();
const emit = defineEmits<{
change: any;
}>();
async function batchUpdateRequest(form: any) {
/**
* type: NotificationType;
* when: NotificationWhen[];
* options?: EmailOptions;
* notificationId: number;
* title: string;
*/
await api.BatchUpdateNotificaiton(props.selectedRowKeys, {
type: "other",
title: form.title || "通知",
when: form.when,
notificationId: form.notificationId,
});
emit("change");
}
const { openCrudFormDialog } = useFormWrapper();
async function openFormDialog() {
const crudOptions: any = {
columns: {
when: {
title: "触发时机",
form: {
value: ["error", "turnToSuccess"],
component: {
name: "a-select",
vModel: "value",
mode: "multiple",
options: [
{ value: "start", label: "开始时" },
{ value: "success", label: "成功时" },
{ value: "turnToSuccess", label: "失败转成功时" },
{ value: "error", label: "失败时" },
],
},
helper: `建议仅选择'失败时'和'失败转成功'两种即可`,
rules: [{ required: true, message: "此项必填" }],
},
},
notificationId: {
title: "通知配置",
form: {
component: {
name: NotificationSelector,
on: {
selectedChange({ form, $event }: any) {
form.title = $event?.name || "通知";
},
},
},
helper: "请选择通知方式",
rules: [{ required: true, message: "此项必填" }],
},
},
},
form: {
mode: "edit",
//@ts-ignore
async doSubmit({ form }) {
await batchUpdateRequest(form);
},
col: {
span: 22,
},
labelCol: {
style: {
width: "100px",
},
},
wrapper: {
title: "批量修改通知",
width: 600,
},
},
} as any;
await openCrudFormDialog({ crudOptions });
}
</script>

View File

@ -0,0 +1,62 @@
<template>
<fs-button icon="mdi:format-list-group" type="link" text="修改定时" @click="openFormDialog"></fs-button>
</template>
<script setup lang="ts">
import * as api from "../api";
import { useFormWrapper } from "@fast-crud/fast-crud";
const props = defineProps<{
selectedRowKeys: any[];
}>();
const emit = defineEmits<{
change: any;
}>();
async function batchUpdateRequest(form: any) {
await api.BatchUpdateTrigger(props.selectedRowKeys, {
title: "定时触发",
type: "timer",
props: form.props,
});
emit("change");
}
const { openCrudFormDialog } = useFormWrapper();
async function openFormDialog() {
const crudOptions: any = {
columns: {
"props.cron": {
title: "定时",
form: {
component: {
name: "cron-editor",
vModel: "modelValue",
},
rules: [{ required: true, message: "请选择定时Cron" }],
},
},
},
form: {
mode: "edit",
//@ts-ignore
async doSubmit({ form }) {
await batchUpdateRequest(form);
},
col: {
span: 22,
},
labelCol: {
style: {
width: "100px",
},
},
wrapper: {
title: "批量修改定时",
width: 600,
},
},
} as any;
await openCrudFormDialog({ crudOptions });
}
</script>

View File

@ -7,9 +7,11 @@
<div v-if="selectedRowKeys.length > 0" class="batch-actions"> <div v-if="selectedRowKeys.length > 0" class="batch-actions">
<div class="batch-actions-inner"> <div class="batch-actions-inner">
<span> 已选择 {{ selectedRowKeys.length }} </span> <span> 已选择 {{ selectedRowKeys.length }} </span>
<fs-button icon="ion:trash-outline" class="color-green" type="link" text="批量删除" @click="batchDelete"></fs-button> <fs-button icon="ion:trash-outline" class="color-red" type="link" text="批量删除" @click="batchDelete"></fs-button>
<change-group class="color-green" :selected-row-keys="selectedRowKeys" @change="groupChanged"></change-group>
<fs-button icon="icon-park-outline:replay-music" class="need-plus" type="link" text="强制重新运行" @click="batchRerun"></fs-button> <fs-button icon="icon-park-outline:replay-music" class="need-plus" type="link" text="强制重新运行" @click="batchRerun"></fs-button>
<change-group class="color-green" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-group>
<change-notification class="color-green" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-notification>
<change-trigger class="color-green" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-trigger>
</div> </div>
</div> </div>
<template #actionbar-right> </template> <template #actionbar-right> </template>
@ -26,8 +28,10 @@ import { dict, useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud"; import createCrudOptions from "./crud";
import PiCertdForm from "./certd-form/index.vue"; import PiCertdForm from "./certd-form/index.vue";
import ChangeGroup from "./components/change-group.vue"; import ChangeGroup from "./components/change-group.vue";
import ChangeTrigger from "./components/change-trigger.vue";
import { Modal, notification } from "ant-design-vue"; import { Modal, notification } from "ant-design-vue";
import * as api from "./api"; import * as api from "./api";
import ChangeNotification from "/@/views/certd/pipeline/components/change-notification.vue";
defineOptions({ defineOptions({
name: "PipelineManager", name: "PipelineManager",
@ -55,7 +59,7 @@ onActivated(async () => {
await crudExpose.doRefresh(); await crudExpose.doRefresh();
}); });
function groupChanged() { function batchFinished() {
crudExpose.doRefresh(); crudExpose.doRefresh();
selectedRowKeys.value = []; selectedRowKeys.value = [];
} }

View File

@ -24,17 +24,17 @@
disabled: !editMode, disabled: !editMode,
options: [ options: [
{ value: 'email', label: '邮件' }, { value: 'email', label: '邮件' },
{ value: 'other', label: '其他通知方式' } { value: 'other', label: '其他通知方式' },
] ],
}, },
rules: [{ required: true, message: '此项必填' }] rules: [{ required: true, message: '此项必填' }],
}" }"
/> />
<fs-form-item <fs-form-item
v-model="currentNotification.when" v-model="currentNotification.when"
:item="{ :item="{
title: '触发时机', title: '触发时机',
key: 'type', key: 'when',
value: ['error'], value: ['error'],
component: { component: {
name: 'a-select', name: 'a-select',
@ -45,11 +45,11 @@
{ value: 'start', label: '开始时' }, { value: 'start', label: '开始时' },
{ value: 'success', label: '成功时' }, { value: 'success', label: '成功时' },
{ value: 'turnToSuccess', label: '失败转成功时' }, { value: 'turnToSuccess', label: '失败转成功时' },
{ value: 'error', label: '失败时' } { value: 'error', label: '失败时' },
] ],
}, },
helper: `建议仅选择'失败时'和'失败转成功'两种即可`, helper: `建议仅选择'失败时'和'失败转成功'两种即可`,
rules: [{ required: true, message: '此项必填' }] rules: [{ required: true, message: '此项必填' }],
}" }"
/> />
<pi-notification-form-email v-if="currentNotification.type === 'email'" ref="optionsRef" v-model:options="currentNotification.options"></pi-notification-form-email> <pi-notification-form-email v-if="currentNotification.type === 'email'" ref="optionsRef" v-model:options="currentNotification.options"></pi-notification-form-email>
@ -59,14 +59,14 @@
v-model="currentNotification.notificationId" v-model="currentNotification.notificationId"
:item="{ :item="{
title: '通知配置', title: '通知配置',
key: 'type', key: 'notificationId',
component: { component: {
disabled: !editMode, disabled: !editMode,
name: NotificationSelector, name: NotificationSelector,
onSelectedChange onSelectedChange,
}, },
helper: '请选择通知方式', helper: '请选择通知方式',
rules: [{ required: true, message: '此项必填' }] rules: [{ required: true, message: '此项必填' }],
}" }"
/> />
</a-form> </a-form>
@ -96,8 +96,8 @@ export default {
props: { props: {
editMode: { editMode: {
type: Boolean, type: Boolean,
default: true default: true,
} },
}, },
emits: ["update"], emits: ["update"],
setup(props: any, context: any) { setup(props: any, context: any) {
@ -118,23 +118,23 @@ export default {
{ {
type: "string", type: "string",
required: true, required: true,
message: "请选择类型" message: "请选择类型",
} },
], ],
when: [ when: [
{ {
type: "string", type: "string",
required: true, required: true,
message: "请选择通知时机" message: "请选择通知时机",
} },
], ],
notificationId: [ notificationId: [
{ {
type: "number", type: "number",
required: true, required: true,
message: "请选择通知配置" message: "请选择通知配置",
} },
] ],
}); });
const notificationDrawerShow = () => { const notificationDrawerShow = () => {
@ -195,7 +195,7 @@ export default {
async onOk() { async onOk() {
callback.value("delete"); callback.value("delete");
notificationDrawerClose(); notificationDrawerClose();
} },
}); });
}; };
@ -222,21 +222,21 @@ export default {
notificationDelete, notificationDelete,
rules, rules,
blankFn, blankFn,
optionsRef optionsRef,
}; };
} }
return { return {
...useNotificationForm(), ...useNotificationForm(),
labelCol: { span: 6 }, labelCol: { span: 6 },
wrapperCol: { span: 16 } wrapperCol: { span: 16 },
}; };
}, },
computed: { computed: {
NotificationSelector() { NotificationSelector() {
return NotificationSelector; return NotificationSelector;
} },
} },
}; };
</script> </script>

View File

@ -1,12 +1,5 @@
<template> <template>
<a-drawer <a-drawer v-model:open="triggerDrawerVisible" placement="right" :closable="true" width="650px" class="pi-trigger-form" @after-open-change="triggerDrawerOnAfterVisibleChange">
v-model:open="triggerDrawerVisible"
placement="right"
:closable="true"
width="650px"
class="pi-trigger-form"
@after-open-change="triggerDrawerOnAfterVisibleChange"
>
<template #title> <template #title>
<div> <div>
编辑触发器 编辑触发器
@ -26,9 +19,9 @@
component: { component: {
name: 'a-input', name: 'a-input',
vModel: 'value', vModel: 'value',
disabled: !editMode disabled: !editMode,
}, },
rules: [{ required: true, message: '此项必填' }] rules: [{ required: true, message: '此项必填' }],
}" }"
/> />
@ -42,9 +35,9 @@
name: 'a-select', name: 'a-select',
vModel: 'value', vModel: 'value',
disabled: !editMode, disabled: !editMode,
options: [{ value: 'timer', label: '定时' }] options: [{ value: 'timer', label: '定时' }],
}, },
rules: [{ required: true, message: '此项必填' }] rules: [{ required: true, message: '此项必填' }],
}" }"
/> />
@ -56,10 +49,10 @@
component: { component: {
disabled: !editMode, disabled: !editMode,
name: 'cron-editor', name: 'cron-editor',
vModel: 'modelValue' vModel: 'modelValue',
}, },
helper: '点击上面的按钮,选择每天几点定时执行。\n建议设置为每天触发一次证书未到期之前任务会跳过不会重复执行', helper: '点击上面的按钮,选择每天几点定时执行。\n建议设置为每天触发一次证书未到期之前任务会跳过不会重复执行',
rules: [{ required: true, message: '此项必填' }] rules: [{ required: true, message: '此项必填' }],
}" }"
/> />
</a-form> </a-form>
@ -84,8 +77,8 @@ export default {
props: { props: {
editMode: { editMode: {
type: Boolean, type: Boolean,
default: true default: true,
} },
}, },
emits: ["update"], emits: ["update"],
setup(props, context) { setup(props, context) {
@ -105,9 +98,9 @@ export default {
{ {
type: "string", type: "string",
required: true, required: true,
message: "请输入名称" message: "请输入名称",
} },
] ],
}); });
const triggerDrawerShow = () => { const triggerDrawerShow = () => {
@ -117,7 +110,7 @@ export default {
triggerDrawerVisible.value = false; triggerDrawerVisible.value = false;
}; };
const triggerDrawerOnAfterVisibleChange = (val) => { const triggerDrawerOnAfterVisibleChange = val => {
console.log("triggerDrawerOnAfterVisibleChange", val); console.log("triggerDrawerOnAfterVisibleChange", val);
}; };
@ -128,7 +121,7 @@ export default {
triggerDrawerShow(); triggerDrawerShow();
}; };
const triggerAdd = (emit) => { const triggerAdd = emit => {
mode.value = "add"; mode.value = "add";
const trigger = { id: nanoid(), title: "定时触发", type: "timer", props: {} }; const trigger = { id: nanoid(), title: "定时触发", type: "timer", props: {} };
triggerOpen(trigger, emit); triggerOpen(trigger, emit);
@ -144,7 +137,7 @@ export default {
triggerOpen(trigger, emit); triggerOpen(trigger, emit);
}; };
const triggerSave = async (e) => { const triggerSave = async e => {
console.log("currentTriggerSave", currentTrigger.value); console.log("currentTriggerSave", currentTrigger.value);
try { try {
await triggerFormRef.value.validate(); await triggerFormRef.value.validate();
@ -164,7 +157,7 @@ export default {
async onOk() { async onOk() {
callback.value("delete"); callback.value("delete");
triggerDrawerClose(); triggerDrawerClose();
} },
}); });
}; };
@ -185,16 +178,16 @@ export default {
triggerSave, triggerSave,
triggerDelete, triggerDelete,
rules, rules,
blankFn blankFn,
}; };
} }
return { return {
...useTriggerForm(), ...useTriggerForm(),
labelCol: { span: 6 }, labelCol: { span: 6 },
wrapperCol: { span: 16 } wrapperCol: { span: 16 },
}; };
} },
}; };
</script> </script>

View File

@ -123,6 +123,19 @@ export class PipelineController extends CrudController<PipelineService> {
return this.ok({}); return this.ok({});
} }
@Post('/batchUpdateTrigger', { summary: Constants.per.authOnly })
async batchUpdateTrigger(@Body('ids') ids: number[], @Body('trigger') trigger: any) {
await this.service.batchUpdateTrigger(ids, trigger, this.getUserId());
return this.ok({});
}
@Post('/batchUpdateNotification', { summary: Constants.per.authOnly })
async batchUpdateNotification(@Body('ids') ids: number[], @Body('notification') notification: any) {
await this.service.batchUpdateNotifications(ids, notification, this.getUserId());
return this.ok({});
}
@Post('/batchRerun', { summary: Constants.per.authOnly }) @Post('/batchRerun', { summary: Constants.per.authOnly })
async batchRerun(@Body('ids') ids: number[]) { async batchRerun(@Body('ids') ids: number[]) {
await this.service.batchRerun(ids, this.getUserId()); await this.service.batchRerun(ids, this.getUserId());

View File

@ -17,7 +17,7 @@ import {
Executor, Executor,
IAccessService, IAccessService,
ICnameProxyService, ICnameProxyService,
INotificationService, INotificationService, Notification,
Pipeline, Pipeline,
ResultType, ResultType,
RunHistory, RunHistory,
@ -45,6 +45,7 @@ import {NotificationService} from "./notification-service.js";
import {UserSuiteEntity, UserSuiteService} from "@certd/commercial-core"; import {UserSuiteEntity, UserSuiteService} from "@certd/commercial-core";
import {CertInfoService} from "../../monitor/service/cert-info-service.js"; import {CertInfoService} from "../../monitor/service/cert-info-service.js";
import {TaskServiceBuilder} from "./task-service-getter.js"; import {TaskServiceBuilder} from "./task-service-getter.js";
import {nanoid} from "nanoid";
const runningTasks: Map<string | number, Executor> = new Map(); const runningTasks: Map<string | number, Executor> = new Map();
@ -149,8 +150,17 @@ export class PipelineService extends BaseService<PipelineEntity> {
const info = await this.info(pipelineId); const info = await this.info(pipelineId);
if (info && !info.disabled) { if (info && !info.disabled) {
const pipeline = JSON.parse(info.content); const pipeline = JSON.parse(info.content);
// 手动触发不要await this.registerTriggers(pipeline,false);
this.registerTriggers(pipeline); }
}
public async registerTrigger(info:PipelineEntity) {
if (info == null) {
return;
}
if (info && !info.disabled) {
const pipeline = JSON.parse(info.content);
this.registerTriggers(pipeline,false);
} }
} }
@ -174,10 +184,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
//修改 //修改
old = await this.info(bean.id); old = await this.info(bean.id);
} }
const pipeline = JSON.parse(bean.content || '{}');
RunnableCollection.initPipelineRunnableType(pipeline);
const isUpdate = bean.id > 0 && old != null; const isUpdate = bean.id > 0 && old != null;
const pipeline = JSON.parse(bean.content || '{}');
RunnableCollection.initPipelineRunnableType(pipeline);
let domains = []; let domains = [];
if (pipeline.stages) { if (pipeline.stages) {
RunnableCollection.each(pipeline.stages, (runnable: any) => { RunnableCollection.each(pipeline.stages, (runnable: any) => {
@ -199,22 +210,32 @@ export class PipelineService extends BaseService<PipelineEntity> {
//如果是添加先保存一下获取到id更新pipeline.id //如果是添加先保存一下获取到id更新pipeline.id
await this.addOrUpdate(bean); await this.addOrUpdate(bean);
} }
await this.clearTriggers(bean.id);
await this.doUpdatePipelineJson(bean, pipeline);
//保存域名信息到certInfo表
let fromType = 'pipeline';
if (bean.type === 'cert_upload') {
fromType = 'upload';
}
await this.certInfoService.updateDomains(pipeline.id, pipeline.userId || bean.userId, domains, fromType);
return bean;
}
/**
* Pipeline trigger
* @param bean
* @param pipeline
*/
async doUpdatePipelineJson(bean: PipelineEntity, pipeline:Pipeline) {
await this.clearTriggers(bean);
if (pipeline.title) { if (pipeline.title) {
bean.title = pipeline.title; bean.title = pipeline.title;
} }
pipeline.id = bean.id; pipeline.id = bean.id;
bean.content = JSON.stringify(pipeline); bean.content = JSON.stringify(pipeline);
await this.addOrUpdate(bean); await this.addOrUpdate(bean);
await this.registerTriggerById(bean.id); await this.registerTrigger(bean);
//保存域名信息到certInfo表
let fromType = 'pipeline';
if(bean.type === 'cert_upload') {
fromType = 'upload';
}
await this.certInfoService.updateDomains(pipeline.id, pipeline.userId || bean.userId, domains,fromType);
return bean;
} }
private async checkMaxPipelineCount(bean: PipelineEntity, pipeline: Pipeline, domains: string[]) { private async checkMaxPipelineCount(bean: PipelineEntity, pipeline: Pipeline, domains: string[]) {
@ -375,11 +396,16 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.certInfoService.deleteByPipelineId(id); await this.certInfoService.deleteByPipelineId(id);
} }
async clearTriggers(id: number) { async clearTriggers(id: number | PipelineEntity) {
if (id == null) { if (id == null) {
return; return;
} }
const pipeline = await this.info(id); let pipeline:PipelineEntity = null
if (typeof id === 'number') {
pipeline = await this.info(id);
}else{
pipeline = id
}
if (!pipeline) { if (!pipeline) {
return; return;
} }
@ -703,6 +729,58 @@ export class PipelineService extends BaseService<PipelineEntity> {
{ groupId } { groupId }
); );
} }
async batchUpdateTrigger(ids: number[], trigger: any, userId: any){
const list = await this.find({
where:{
id: In(ids),
userId
}
})
for (const item of list) {
const pipeline = JSON.parse(item.content);
pipeline.triggers = [{
id: nanoid(),
title: '定时触发',
...trigger
}]
await this.doUpdatePipelineJson(item,pipeline)
}
}
async batchUpdateNotifications(ids: number[], notification: Notification, userId: any){
const list = await this.find({
where:{
id: In(ids),
userId
}
})
for (const item of list) {
const pipeline = JSON.parse(item.content);
pipeline.notifications = [{
id: nanoid(),
title: '通知',
/**
* type: NotificationType;
* when: NotificationWhen[];
* options: EmailOptions;
* notificationId: number;
* title: string;
* subType: string;
*/
type: "other",
...notification
}]
await this.doUpdatePipelineJson(item,pipeline)
}
}
async batchRerun(ids: number[], userId: any) { async batchRerun(ids: number[], userId: any) {
if (!isPlus()){ if (!isPlus()){
throw new NeedVIPException("此功能需要升级专业版") throw new NeedVIPException("此功能需要升级专业版")