From d9a00eeaf72735ced67c59d7983d84e3c730064a Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Fri, 22 Nov 2024 17:12:39 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E9=80=9A=E7=9F=A5=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/guide/install/1panel/index.md | 2 +- docs/guide/install/baota/index.md | 2 +- docs/guide/install/docker/index.md | 2 +- docs/guide/install/source/index.md | 2 +- docs/guide/use/backup/index.md | 2 +- packages/core/pipeline/src/access/api.ts | 10 +- packages/core/pipeline/src/core/handler.ts | 21 --- packages/core/pipeline/src/core/index.ts | 1 - packages/core/pipeline/src/index.ts | 1 + .../core/pipeline/src/notification/api.ts | 82 ++++++++++ .../pipeline/src/notification/decorator.ts | 61 ++++++++ .../core/pipeline/src/notification/index.ts | 3 + .../pipeline/src/notification/registry.ts | 4 + packages/core/pipeline/src/plugin/api.ts | 10 +- .../src/router/source/modules/certd.ts | 11 ++ .../src/views/certd/cname/record/crud.tsx | 2 +- .../src/views/certd/notification/api.ts | 70 +++++++++ .../src/views/certd/notification/common.tsx | 147 ++++++++++++++++++ .../src/views/certd/notification/crud.tsx | 53 +++++++ .../src/views/certd/notification/index.vue | 39 +++++ .../notification-selector/index.vue | 139 +++++++++++++++++ .../notification-selector/modal/crud.tsx | 90 +++++++++++ .../notification-selector/modal/index.vue | 58 +++++++ .../db/migration/v10013__notification.sql | 2 + .../pipeline/notification-controller.ts | 91 +++++++++++ .../modules/pipeline/entity/notification.ts | 32 ++++ .../pipeline/service/notification-service.ts | 38 +++++ .../plugin-notification/email/index.ts | 30 ++++ .../src/plugins/plugin-notification/index.ts | 2 + .../plugins/plugin-notification/qywx/index.ts | 54 +++++++ 30 files changed, 1031 insertions(+), 30 deletions(-) delete mode 100644 packages/core/pipeline/src/core/handler.ts create mode 100644 packages/core/pipeline/src/notification/api.ts create mode 100644 packages/core/pipeline/src/notification/decorator.ts create mode 100644 packages/core/pipeline/src/notification/index.ts create mode 100644 packages/core/pipeline/src/notification/registry.ts create mode 100644 packages/ui/certd-client/src/views/certd/notification/api.ts create mode 100644 packages/ui/certd-client/src/views/certd/notification/common.tsx create mode 100644 packages/ui/certd-client/src/views/certd/notification/crud.tsx create mode 100644 packages/ui/certd-client/src/views/certd/notification/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/notification/notification-selector/index.vue create mode 100644 packages/ui/certd-client/src/views/certd/notification/notification-selector/modal/crud.tsx create mode 100644 packages/ui/certd-client/src/views/certd/notification/notification-selector/modal/index.vue create mode 100644 packages/ui/certd-server/db/migration/v10013__notification.sql create mode 100644 packages/ui/certd-server/src/controller/pipeline/notification-controller.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/entity/notification.ts create mode 100644 packages/ui/certd-server/src/modules/pipeline/service/notification-service.ts create mode 100644 packages/ui/certd-server/src/plugins/plugin-notification/email/index.ts create mode 100644 packages/ui/certd-server/src/plugins/plugin-notification/index.ts create mode 100644 packages/ui/certd-server/src/plugins/plugin-notification/qywx/index.ts diff --git a/docs/guide/install/1panel/index.md b/docs/guide/install/1panel/index.md index a42973c8..8472eb07 100644 --- a/docs/guide/install/1panel/index.md +++ b/docs/guide/install/1panel/index.md @@ -48,4 +48,4 @@ admin/123456 ## 五、备份恢复 -将备份的`db.sqlite`覆盖到原来的位置即可 +将备份的`db.sqlite`覆盖到原来的位置,重启certd即可 diff --git a/docs/guide/install/baota/index.md b/docs/guide/install/baota/index.md index b9b967e8..8f77a8a6 100644 --- a/docs/guide/install/baota/index.md +++ b/docs/guide/install/baota/index.md @@ -81,4 +81,4 @@ services: ## 五、备份恢复 -将备份的`db.sqlite`覆盖到原来的位置即可 +将备份的`db.sqlite`覆盖到原来的位置,重启certd即可 diff --git a/docs/guide/install/docker/index.md b/docs/guide/install/docker/index.md index f7ddb7ba..8c17707d 100644 --- a/docs/guide/install/docker/index.md +++ b/docs/guide/install/docker/index.md @@ -71,4 +71,4 @@ docker compose up -d ## 四、备份恢复 -将备份的`db.sqlite`覆盖到原来的位置即可 \ No newline at end of file +将备份的`db.sqlite`覆盖到原来的位置,重启certd即可 \ No newline at end of file diff --git a/docs/guide/install/source/index.md b/docs/guide/install/source/index.md index 7228e492..0114ee92 100644 --- a/docs/guide/install/source/index.md +++ b/docs/guide/install/source/index.md @@ -42,4 +42,4 @@ kill -9 $(lsof -t -i:7001) ## 四、备份恢复 -将备份的`db.sqlite`覆盖到原来的位置即可 +将备份的`db.sqlite`覆盖到原来的位置,重启certd即可 diff --git a/docs/guide/use/backup/index.md b/docs/guide/use/backup/index.md index 69ee475f..86ff93b4 100644 --- a/docs/guide/use/backup/index.md +++ b/docs/guide/use/backup/index.md @@ -27,4 +27,4 @@ ## 三、备份恢复 -将备份的`db.sqlite`覆盖到原来的位置即可 \ No newline at end of file +将备份的`db.sqlite`覆盖到原来的位置,重启certd即可 \ No newline at end of file diff --git a/packages/core/pipeline/src/access/api.ts b/packages/core/pipeline/src/access/api.ts index ed8b225c..72937697 100644 --- a/packages/core/pipeline/src/access/api.ts +++ b/packages/core/pipeline/src/access/api.ts @@ -2,7 +2,15 @@ import { Registrable } from "../registry/index.js"; import { FormItemProps } from "../dt/index.js"; import { HttpClient, ILogger, utils } from "@certd/basic"; import * as _ from "lodash-es"; -import { AccessRequestHandleReq } from "../core"; +import { PluginRequestHandleReq } from "../plugin/index.js"; + +export type AccessRequestHandleReqInput = { + id?: number; + title?: string; + access: T; +}; + +export type AccessRequestHandleReq = PluginRequestHandleReq>; export type AccessInputDefine = FormItemProps & { title: string; diff --git a/packages/core/pipeline/src/core/handler.ts b/packages/core/pipeline/src/core/handler.ts deleted file mode 100644 index 7d409f5f..00000000 --- a/packages/core/pipeline/src/core/handler.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { HttpClient, ILogger, utils } from "@certd/basic"; - -export type PluginRequestHandleReq = { - typeName: string; - action: string; - input: T; - data: any; -}; - -export type AccessRequestHandleReqInput = { - id?: number; - title?: string; - access: T; -}; -export type AccessRequestHandleContext = { - http: HttpClient; - logger: ILogger; - utils: typeof utils; -}; - -export type AccessRequestHandleReq = PluginRequestHandleReq>; diff --git a/packages/core/pipeline/src/core/index.ts b/packages/core/pipeline/src/core/index.ts index f273c8eb..59cdc83e 100644 --- a/packages/core/pipeline/src/core/index.ts +++ b/packages/core/pipeline/src/core/index.ts @@ -3,5 +3,4 @@ export * from "./run-history.js"; export * from "./context.js"; export * from "./storage.js"; export * from "./file-store.js"; -export * from "./handler.js"; export * from "./exceptions.js"; diff --git a/packages/core/pipeline/src/index.ts b/packages/core/pipeline/src/index.ts index 98b000dc..110b47e8 100644 --- a/packages/core/pipeline/src/index.ts +++ b/packages/core/pipeline/src/index.ts @@ -6,3 +6,4 @@ export * from "./plugin/index.js"; export * from "./context/index.js"; export * from "./decorator/index.js"; export * from "./service/index.js"; +export * from "./notification/index.js"; diff --git a/packages/core/pipeline/src/notification/api.ts b/packages/core/pipeline/src/notification/api.ts new file mode 100644 index 00000000..edef364a --- /dev/null +++ b/packages/core/pipeline/src/notification/api.ts @@ -0,0 +1,82 @@ +import { PluginRequestHandleReq } from "../plugin"; +import { Registrable } from "../registry/index.js"; +import { FormItemProps } from "../dt/index.js"; +import { HttpClient, ILogger, utils } from "@certd/basic"; +import * as _ from "lodash-es"; +import { IEmailService } from "../service"; + +export type NotificationBody = { + userId: number; + title: string; + content: string; + pipelineId: number; + historyId: number; + url: string; + extra?: any; + options?: any; +}; + +export type NotificationRequestHandleReqInput = { + id?: number; + title?: string; + access: T; +}; + +export type NotificationRequestHandleReq = PluginRequestHandleReq>; + +export type NotificationInputDefine = FormItemProps & { + title: string; + required?: boolean; + encrypt?: boolean; +}; +export type NotificationDefine = Registrable & { + input?: { + [key: string]: NotificationInputDefine; + }; +}; +export interface INotificationService { + send(body: NotificationBody): Promise; +} + +export interface INotification extends INotificationService { + ctx: NotificationContext; + [key: string]: any; +} + +export type NotificationContext = { + http: HttpClient; + logger: ILogger; + utils: typeof utils; + emailService: IEmailService; +}; + +export abstract class BaseNotification implements INotification { + ctx!: NotificationContext; + http!: HttpClient; + logger!: ILogger; + abstract send(body: NotificationBody): Promise; + setCtx(ctx: NotificationContext) { + this.ctx = ctx; + this.http = ctx.http; + this.logger = ctx.logger; + } + + async onRequest(req: NotificationRequestHandleReq) { + if (!req.action) { + throw new Error("action is required"); + } + + let methodName = req.action; + if (!req.action.startsWith("on")) { + methodName = `on${_.upperFirst(req.action)}`; + } + + // @ts-ignore + const method = this[methodName]; + if (method) { + // @ts-ignore + return await this[methodName](req.data); + } + throw new Error(`action ${req.action} not found`); + } +} diff --git a/packages/core/pipeline/src/notification/decorator.ts b/packages/core/pipeline/src/notification/decorator.ts new file mode 100644 index 00000000..42cbaf39 --- /dev/null +++ b/packages/core/pipeline/src/notification/decorator.ts @@ -0,0 +1,61 @@ +// src/decorator/memoryCache.decorator.ts +import { Decorator } from "../decorator/index.js"; +import * as _ from "lodash-es"; +import { notificationRegistry } from "./registry.js"; +import { http, logger, utils } from "@certd/basic"; +import { NotificationContext, NotificationDefine, NotificationInputDefine } from "./api.js"; + +// 提供一个唯一 key +export const NOTIFICATION_CLASS_KEY = "pipeline:notification"; +export const NOTIFICATION_INPUT_KEY = "pipeline:notification:input"; + +export function IsNotification(define: NotificationDefine): ClassDecorator { + return (target: any) => { + target = Decorator.target(target); + + const inputs: any = {}; + const properties = Decorator.getClassProperties(target); + for (const property in properties) { + const input = Reflect.getMetadata(NOTIFICATION_INPUT_KEY, target, property); + if (input) { + inputs[property] = input; + } + } + _.merge(define, { input: inputs }); + Reflect.defineMetadata(NOTIFICATION_CLASS_KEY, define, target); + target.define = define; + notificationRegistry.register(define.name, { + define, + target, + }); + }; +} + +export function NotificationInput(input?: NotificationInputDefine): PropertyDecorator { + return (target, propertyKey) => { + target = Decorator.target(target, propertyKey); + // const _type = Reflect.getMetadata("design:type", target, propertyKey); + Reflect.defineMetadata(NOTIFICATION_INPUT_KEY, input, target, propertyKey); + }; +} + +export function newNotification(type: string, input: any, ctx?: NotificationContext) { + const register = notificationRegistry.get(type); + if (register == null) { + throw new Error(`notification ${type} not found`); + } + // @ts-ignore + const access = new register.target(); + for (const key in input) { + access[key] = input[key]; + } + if (!ctx) { + ctx = { + http, + logger, + utils, + }; + } + access.ctx = ctx; + return access; +} diff --git a/packages/core/pipeline/src/notification/index.ts b/packages/core/pipeline/src/notification/index.ts new file mode 100644 index 00000000..9b9e3a48 --- /dev/null +++ b/packages/core/pipeline/src/notification/index.ts @@ -0,0 +1,3 @@ +export * from "./api.js"; +export * from "./registry.js"; +export * from "./decorator.js"; diff --git a/packages/core/pipeline/src/notification/registry.ts b/packages/core/pipeline/src/notification/registry.ts new file mode 100644 index 00000000..5d4a2f4e --- /dev/null +++ b/packages/core/pipeline/src/notification/registry.ts @@ -0,0 +1,4 @@ +import { createRegistry } from "../registry/index.js"; + +// @ts-ignore +export const notificationRegistry = createRegistry("notification"); diff --git a/packages/core/pipeline/src/plugin/api.ts b/packages/core/pipeline/src/plugin/api.ts index 60985e0c..da4379b0 100644 --- a/packages/core/pipeline/src/plugin/api.ts +++ b/packages/core/pipeline/src/plugin/api.ts @@ -3,12 +3,20 @@ import { FileItem, FormItemProps, Pipeline, Runnable, Step } from "../dt/index.j import { FileStore } from "../core/file-store.js"; import { IAccessService } from "../access/index.js"; import { ICnameProxyService, IEmailService } from "../service/index.js"; -import { CancelError, IContext, PluginRequestHandleReq, RunnableCollection } from "../core/index.js"; +import { CancelError, IContext, RunnableCollection } from "../core/index.js"; import { HttpRequestConfig, ILogger, logger, utils } from "@certd/basic"; import { HttpClient } from "@certd/basic"; import dayjs from "dayjs"; import { IPluginConfigService } from "../service/config"; import { upperFirst } from "lodash-es"; + +export type PluginRequestHandleReq = { + typeName: string; + action: string; + input: T; + data: any; +}; + export type UserInfo = { role: "admin" | "user"; id: any; diff --git a/packages/ui/certd-client/src/router/source/modules/certd.ts b/packages/ui/certd-client/src/router/source/modules/certd.ts index 87de82b1..a86cc063 100644 --- a/packages/ui/certd-client/src/router/source/modules/certd.ts +++ b/packages/ui/certd-client/src/router/source/modules/certd.ts @@ -49,6 +49,17 @@ export const certdResources = [ cache: true } }, + { + title: "通知设置", + name: "NotificationManager", + path: "/certd/notification", + component: "/certd/notification/index.vue", + meta: { + icon: "ion:disc-outline", + auth: true, + cache: true + } + }, { title: "CNAME记录管理", name: "CnameRecord", diff --git a/packages/ui/certd-client/src/views/certd/cname/record/crud.tsx b/packages/ui/certd-client/src/views/certd/cname/record/crud.tsx index 44cb1962..798355b3 100644 --- a/packages/ui/certd-client/src/views/certd/cname/record/crud.tsx +++ b/packages/ui/certd-client/src/views/certd/cname/record/crud.tsx @@ -115,7 +115,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat } }, cnameProviderId: { - title: "CNAME提供者", + title: "CNAME服务", type: "dict-select", dict: dict({ url: "/cname/provider/list", diff --git a/packages/ui/certd-client/src/views/certd/notification/api.ts b/packages/ui/certd-client/src/views/certd/notification/api.ts new file mode 100644 index 00000000..f38c7383 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/notification/api.ts @@ -0,0 +1,70 @@ +import { request } from "/src/api/service"; + +export function createApi() { + const apiPrefix = "/pi/notification"; + return { + async GetList(query: any) { + return await request({ + url: apiPrefix + "/page", + method: "post", + data: query + }); + }, + + async AddObj(obj: any) { + return await request({ + url: apiPrefix + "/add", + method: "post", + data: obj + }); + }, + + async UpdateObj(obj: any) { + return await request({ + url: apiPrefix + "/update", + method: "post", + data: obj + }); + }, + + async DelObj(id: number) { + return await request({ + url: apiPrefix + "/delete", + method: "post", + params: { id } + }); + }, + + async GetObj(id: number) { + return await request({ + url: apiPrefix + "/info", + method: "post", + params: { id } + }); + }, + + async GetSimpleInfo(id: number) { + return await request({ + url: apiPrefix + "/simpleInfo", + method: "post", + params: { id } + }); + }, + + async GetProviderDefine(type: string) { + return await request({ + url: apiPrefix + "/define", + method: "post", + params: { type } + }); + }, + + async GetProviderDefineByType(type: string) { + return await request({ + url: apiPrefix + "/defineByType", + method: "post", + params: { type } + }); + } + }; +} diff --git a/packages/ui/certd-client/src/views/certd/notification/common.tsx b/packages/ui/certd-client/src/views/certd/notification/common.tsx new file mode 100644 index 00000000..ff92ecd5 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/notification/common.tsx @@ -0,0 +1,147 @@ +import { ColumnCompositionProps, dict } from "@fast-crud/fast-crud"; +import { computed, provide, ref, toRef } from "vue"; +import { useReference } from "/@/use/use-refrence"; +import { forEach, get, merge, set } from "lodash-es"; + +export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) { + provide("notificationApi", api); + const notificationTypeDictRef = dict({ + url: "/pi/notification/getTypeDict" + }); + const defaultPluginConfig = { + component: { + name: "a-input", + vModel: "value" + } + }; + + function buildDefineFields(define: any, form: any, mode: string) { + const formWrapperRef = crudExpose.getFormWrapperRef(); + const columnsRef = toRef(formWrapperRef.formOptions, "columns"); + + for (const key in columnsRef.value) { + if (key.indexOf(".") >= 0) { + delete columnsRef.value[key]; + } + } + console.log('crudBinding.value[mode + "Form"].columns', columnsRef.value); + forEach(define.input, (value: any, mapKey: any) => { + const key = "body." + mapKey; + const field = { + ...value, + key + }; + const column = merge({ title: key }, defaultPluginConfig, field); + //eval + useReference(column); + + //设置默认值 + if (column.value != null && get(form, key) == null) { + set(form, key, column.value); + } + //字段配置赋值 + columnsRef.value[key] = column; + console.log("form", columnsRef.value); + }); + } + + const currentDefine = ref(); + + return { + id: { + title: "ID", + key: "id", + type: "number", + column: { + width: 100 + }, + form: { + show: false + } + }, + name: { + title: "通知名称", + search: { + show: true + }, + type: ["text"], + form: { + rules: [{ required: true, message: "请填写名称" }], + helper: "随便填,当多个相同类型的通知时,便于区分" + }, + column: { + width: 200 + } + }, + type: { + title: "类型", + type: "dict-select", + dict: notificationTypeDictRef, + search: { + show: false + }, + column: { + width: 200, + component: { + color: "auto" + } + }, + form: { + component: { + disabled: false, + showSearch: true, + filterOption: (input: string, option: any) => { + input = input?.toLowerCase(); + return option.value.toLowerCase().indexOf(input) >= 0 || option.label.toLowerCase().indexOf(input) >= 0; + } + }, + rules: [{ required: true, message: "请选择类型" }], + valueChange: { + immediate: true, + async handle({ value, mode, form, immediate }) { + if (value == null) { + return; + } + const define = await api.GetProviderDefine(value); + currentDefine.value = define; + console.log("define", define); + if (!immediate) { + form.body = {}; + } + buildDefineFields(define, form, mode); + } + }, + helper: computed(() => { + const define = currentDefine.value; + if (define == null) { + return ""; + } + return define.desc; + }) + }, + addForm: { + value: typeRef + } + } as ColumnCompositionProps, + setting: { + column: { show: false }, + form: { + show: false, + valueBuilder({ value, form }) { + form.body = {}; + if (!value) { + return; + } + const setting = JSON.parse(value); + for (const key in setting) { + form.body[key] = setting[key]; + } + }, + valueResolve({ form }) { + const setting = form.body; + form.setting = JSON.stringify(setting); + } + } + } as ColumnCompositionProps + }; +} diff --git a/packages/ui/certd-client/src/views/certd/notification/crud.tsx b/packages/ui/certd-client/src/views/certd/notification/crud.tsx new file mode 100644 index 00000000..f3ca01a1 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/notification/crud.tsx @@ -0,0 +1,53 @@ +// @ts-ignore +import { useI18n } from "vue-i18n"; +import { ref } from "vue"; +import { getCommonColumnDefine } from "./common"; +import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; + +export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { + const { t } = useI18n(); + const api = context.api; + const pageRequest = async (query: UserPageQuery): Promise => { + return await api.GetList(query); + }; + const editRequest = async (req: EditReq) => { + const { form, row } = req; + form.id = row.id; + const res = await api.UpdateObj(form); + return res; + }; + const delRequest = async (req: DelReq) => { + const { row } = req; + return await api.DelObj(row.id); + }; + + const addRequest = async (req: AddReq) => { + const { form } = req; + const res = await api.AddObj(form); + return res; + }; + + const typeRef = ref(); + const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api); + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + form: { + labelCol: { + span: 6 + } + }, + rowHandle: { + width: 200 + }, + columns: { + ...commonColumnsDefine + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/certd/notification/index.vue b/packages/ui/certd-client/src/views/certd/notification/index.vue new file mode 100644 index 00000000..a0b8e6ec --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/notification/index.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/ui/certd-client/src/views/certd/notification/notification-selector/index.vue b/packages/ui/certd-client/src/views/certd/notification/notification-selector/index.vue new file mode 100644 index 00000000..c6dc9bcf --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/notification/notification-selector/index.vue @@ -0,0 +1,139 @@ + + + + diff --git a/packages/ui/certd-client/src/views/certd/notification/notification-selector/modal/crud.tsx b/packages/ui/certd-client/src/views/certd/notification/notification-selector/modal/crud.tsx new file mode 100644 index 00000000..dc11f819 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/notification/notification-selector/modal/crud.tsx @@ -0,0 +1,90 @@ +// @ts-ignore +import { ref } from "vue"; +import { getCommonColumnDefine } from "../../common"; +import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; + +export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { + const { crudBinding } = crudExpose; + const { props, ctx, api } = context; + const lastResRef = ref(); + const pageRequest = async (query: UserPageQuery): Promise => { + return await context.api.GetList(query); + }; + const editRequest = async (req: EditReq) => { + const { form, row } = req; + form.id = row.id; + form.type = props.type; + const res = await context.api.UpdateObj(form); + lastResRef.value = res; + return res; + }; + const delRequest = async (req: DelReq) => { + const { row } = req; + return await context.api.DelObj(row.id); + }; + + const addRequest = async (req: AddReq) => { + const { form } = req; + form.type = props.type; + const res = await context.api.AddObj(form); + lastResRef.value = res; + return res; + }; + + const selectedRowKey = ref([props.modelValue]); + + const onSelectChange = (changed: any) => { + selectedRowKey.value = changed; + ctx.emit("update:modelValue", changed[0]); + }; + + const typeRef = ref(""); + context.typeRef = typeRef; + const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api); + commonColumnsDefine.type.form.component.disabled = true; + return { + typeRef, + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest + }, + toolbar: { + show: false + }, + search: { + show: false + }, + form: { + wrapper: { + width: "1050px" + } + }, + rowHandle: { + width: 200 + }, + table: { + scroll: { + x: 800 + }, + rowSelection: { + type: "radio", + selectedRowKeys: selectedRowKey, + onChange: onSelectChange + }, + customRow: (record: any) => { + return { + onClick: () => { + onSelectChange([record.id]); + } // 点击行 + }; + } + }, + columns: { + ...commonColumnsDefine + } + } + }; +} diff --git a/packages/ui/certd-client/src/views/certd/notification/notification-selector/modal/index.vue b/packages/ui/certd-client/src/views/certd/notification/notification-selector/modal/index.vue new file mode 100644 index 00000000..5af8aeb9 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/notification/notification-selector/modal/index.vue @@ -0,0 +1,58 @@ + + + + diff --git a/packages/ui/certd-server/db/migration/v10013__notification.sql b/packages/ui/certd-server/db/migration/v10013__notification.sql new file mode 100644 index 00000000..8822d005 --- /dev/null +++ b/packages/ui/certd-server/db/migration/v10013__notification.sql @@ -0,0 +1,2 @@ + +CREATE TABLE "pi_notification" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" integer NOT NULL, "name" varchar(100) NOT NULL, "type" varchar(100) NOT NULL, "setting" text, "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)); diff --git a/packages/ui/certd-server/src/controller/pipeline/notification-controller.ts b/packages/ui/certd-server/src/controller/pipeline/notification-controller.ts new file mode 100644 index 00000000..ca1d6827 --- /dev/null +++ b/packages/ui/certd-server/src/controller/pipeline/notification-controller.ts @@ -0,0 +1,91 @@ +import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core'; +import { Constants, CrudController } from '@certd/lib-server'; +import { NotificationService } from '../../modules/pipeline/service/notification-service.js'; +import { AuthService } from '../../modules/sys/authority/service/auth-service.js'; + +/** + * 通知 + */ +@Provide() +@Controller('/api/pi/notification') +export class NotificationController extends CrudController { + @Inject() + service: NotificationService; + @Inject() + authService: AuthService; + + getService(): NotificationService { + return this.service; + } + + @Post('/page', { summary: Constants.per.authOnly }) + async page(@Body(ALL) body) { + body.query = body.query ?? {}; + delete body.query.userId; + const buildQuery = qb => { + qb.andWhere('user_id = :userId', { userId: this.getUserId() }); + }; + const res = await this.service.page({ + query: body.query, + page: body.page, + sort: body.sort, + buildQuery, + }); + return this.ok(res); + } + + @Post('/list', { summary: Constants.per.authOnly }) + async list(@Body(ALL) body) { + body.userId = this.getUserId(); + return super.list(body); + } + + @Post('/add', { summary: Constants.per.authOnly }) + async add(@Body(ALL) bean) { + bean.userId = this.getUserId(); + return super.add(bean); + } + + @Post('/update', { summary: Constants.per.authOnly }) + async update(@Body(ALL) bean) { + await this.service.checkUserId(bean.id, this.getUserId()); + return super.update(bean); + } + @Post('/info', { summary: Constants.per.authOnly }) + async info(@Query('id') id: number) { + await this.service.checkUserId(id, this.getUserId()); + return super.info(id); + } + + @Post('/delete', { summary: Constants.per.authOnly }) + async delete(@Query('id') id: number) { + await this.service.checkUserId(id, this.getUserId()); + return super.delete(id); + } + + @Post('/define', { summary: Constants.per.authOnly }) + async define(@Query('type') type: string) { + const notification = this.service.getDefineByType(type); + return this.ok(notification); + } + + @Post('/getTypeDict', { summary: Constants.per.authOnly }) + async getTypeDict() { + const list = this.service.getDefineList(); + const dict = []; + for (const item of list) { + dict.push({ + value: item.name, + label: item.title, + }); + } + return this.ok(dict); + } + + @Post('/simpleInfo', { summary: Constants.per.authOnly }) + async simpleInfo(@Query('id') id: number) { + await this.authService.checkEntityUserId(this.ctx, this.service, id); + const res = await this.service.getSimpleInfo(id); + return this.ok(res); + } +} diff --git a/packages/ui/certd-server/src/modules/pipeline/entity/notification.ts b/packages/ui/certd-server/src/modules/pipeline/entity/notification.ts new file mode 100644 index 00000000..c9bc5f91 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/entity/notification.ts @@ -0,0 +1,32 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('pi_notification') +export class NotificationEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'user_id', comment: 'UserId' }) + userId: string; + + @Column({ name: 'type', comment: '通知类型' }) + type: string; + + @Column({ name: 'name', comment: '名称' }) + name: string; + + @Column({ name: 'setting', comment: '通知配置', length: 10240 }) + setting: string; + + @Column({ + name: 'create_time', + comment: '创建时间', + default: () => 'CURRENT_TIMESTAMP', + }) + createTime: Date; + @Column({ + name: 'update_time', + comment: '修改时间', + default: () => 'CURRENT_TIMESTAMP', + }) + updateTime: Date; +} diff --git a/packages/ui/certd-server/src/modules/pipeline/service/notification-service.ts b/packages/ui/certd-server/src/modules/pipeline/service/notification-service.ts new file mode 100644 index 00000000..8d294249 --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/service/notification-service.ts @@ -0,0 +1,38 @@ +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; +import { BaseService, ValidateException } from '@certd/lib-server'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { NotificationEntity } from '../entity/notification.js'; +import { notificationRegistry } from '@certd/pipeline'; + +@Provide() +@Scope(ScopeEnum.Singleton) +export class NotificationService extends BaseService { + @InjectEntityModel(NotificationEntity) + repository: Repository; + + //@ts-ignore + getRepository() { + return this.repository; + } + + async getSimpleInfo(id: number) { + const entity = await this.info(id); + if (entity == null) { + throw new ValidateException('该通知配置不存在,请确认是否已被删除'); + } + return { + id: entity.id, + name: entity.name, + userId: entity.userId, + }; + } + + getDefineList() { + return notificationRegistry.getDefineList(); + } + + getDefineByType(type: string) { + return notificationRegistry.getDefine(type); + } +} diff --git a/packages/ui/certd-server/src/plugins/plugin-notification/email/index.ts b/packages/ui/certd-server/src/plugins/plugin-notification/email/index.ts new file mode 100644 index 00000000..a01c1a4a --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-notification/email/index.ts @@ -0,0 +1,30 @@ +import { BaseNotification, IsNotification, NotificationBody, NotificationInput } from '@certd/pipeline'; + +@IsNotification({ + name: 'email', + title: '电子邮件', + desc: '电子邮件通知', +}) +export class EmailNotification extends BaseNotification { + @NotificationInput({ + title: '收件人邮箱', + component: { + name: 'a-select', + vModel: 'value', + mode: 'tags', + open: false, + }, + required: true, + helper: '可以填写多个,填写一个按回车键再填写下一个', + }) + receivers!: string[]; + + async send(body: NotificationBody) { + await this.ctx.emailService.send({ + userId: body.userId, + subject: body.title, + content: body.content, + receivers: this.receivers, + }); + } +} diff --git a/packages/ui/certd-server/src/plugins/plugin-notification/index.ts b/packages/ui/certd-server/src/plugins/plugin-notification/index.ts new file mode 100644 index 00000000..c4d75996 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-notification/index.ts @@ -0,0 +1,2 @@ +export * from './qywx/index.js'; +export * from './email/index.js'; diff --git a/packages/ui/certd-server/src/plugins/plugin-notification/qywx/index.ts b/packages/ui/certd-server/src/plugins/plugin-notification/qywx/index.ts new file mode 100644 index 00000000..1c8ef780 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-notification/qywx/index.ts @@ -0,0 +1,54 @@ +import { BaseNotification, IsNotification, NotificationBody, NotificationInput } from '@certd/pipeline'; + +@IsNotification({ + name: 'qywx', + title: '企业微信通知', + desc: '企业微信群聊机器人通知', +}) +export class QywxNotification extends BaseNotification { + @NotificationInput({ + title: 'webhook地址', + component: { + placeholder: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxx', + }, + required: true, + }) + webhook = ''; + + @NotificationInput({ + title: '提醒指定成员', + component: { + name: 'a-select', + vModel: 'value', + mode: 'tags', + open: false, + }, + required: false, + helper: '填写成员名字,@all 为提醒所有人', + }) + mentionedList!: string[]; + + async send(body: NotificationBody) { + console.log('send qywx'); + + /** + * + * "msgtype": "text", + * "text": { + * "content": "hello world" + * } + * } + */ + + await this.http.request({ + url: this.webhook, + data: { + msgtype: 'markdown', + text: { + content: `# ${body.title}\n\n${body.content}\n[查看详情](${body.url})`, + mentioned_list: this.mentionedList, + }, + }, + }); + } +}