perf: 通知管理

pull/265/head
xiaojunnuo 2024-11-22 17:12:39 +08:00
parent 131ed13df1
commit d9a00eeaf7
30 changed files with 1031 additions and 30 deletions

View File

@ -48,4 +48,4 @@ admin/123456
## 五、备份恢复
将备份的`db.sqlite`覆盖到原来的位置即可
将备份的`db.sqlite`覆盖到原来的位置重启certd即可

View File

@ -81,4 +81,4 @@ services:
## 五、备份恢复
将备份的`db.sqlite`覆盖到原来的位置即可
将备份的`db.sqlite`覆盖到原来的位置重启certd即可

View File

@ -71,4 +71,4 @@ docker compose up -d
## 四、备份恢复
将备份的`db.sqlite`覆盖到原来的位置即可
将备份的`db.sqlite`覆盖到原来的位置重启certd即可

View File

@ -42,4 +42,4 @@ kill -9 $(lsof -t -i:7001)
## 四、备份恢复
将备份的`db.sqlite`覆盖到原来的位置即可
将备份的`db.sqlite`覆盖到原来的位置重启certd即可

View File

@ -27,4 +27,4 @@
## 三、备份恢复
将备份的`db.sqlite`覆盖到原来的位置即可
将备份的`db.sqlite`覆盖到原来的位置重启certd即可

View File

@ -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<T = any> = {
id?: number;
title?: string;
access: T;
};
export type AccessRequestHandleReq<T = any> = PluginRequestHandleReq<AccessRequestHandleReqInput<T>>;
export type AccessInputDefine = FormItemProps & {
title: string;

View File

@ -1,21 +0,0 @@
import { HttpClient, ILogger, utils } from "@certd/basic";
export type PluginRequestHandleReq<T = any> = {
typeName: string;
action: string;
input: T;
data: any;
};
export type AccessRequestHandleReqInput<T = any> = {
id?: number;
title?: string;
access: T;
};
export type AccessRequestHandleContext = {
http: HttpClient;
logger: ILogger;
utils: typeof utils;
};
export type AccessRequestHandleReq<T = any> = PluginRequestHandleReq<AccessRequestHandleReqInput<T>>;

View File

@ -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";

View File

@ -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";

View File

@ -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<T = any> = {
id?: number;
title?: string;
access: T;
};
export type NotificationRequestHandleReq<T = any> = PluginRequestHandleReq<NotificationRequestHandleReqInput<T>>;
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<void>;
}
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<void>;
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`);
}
}

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
export * from "./api.js";
export * from "./registry.js";
export * from "./decorator.js";

View File

@ -0,0 +1,4 @@
import { createRegistry } from "../registry/index.js";
// @ts-ignore
export const notificationRegistry = createRegistry("notification");

View File

@ -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<T = any> = {
typeName: string;
action: string;
input: T;
data: any;
};
export type UserInfo = {
role: "admin" | "user";
id: any;

View File

@ -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",

View File

@ -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",

View File

@ -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 }
});
}
};
}

View File

@ -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
};
}

View File

@ -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<UserPageRes> => {
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
}
}
};
}

View File

@ -0,0 +1,39 @@
<template>
<fs-page>
<template #header>
<div class="title">
通知管理
<span class="sub">管理通知配置</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts">
import { defineComponent, onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createApi } from "./api";
export default defineComponent({
name: "NotificationManager",
setup() {
const api = createApi();
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { api } });
//
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(() => {
crudExpose.doRefresh();
});
return {
crudBinding,
crudRef
};
}
});
</script>

View File

@ -0,0 +1,139 @@
<template>
<div class="notification-selector">
<span v-if="target.name" class="mr-5 cd-flex-inline">
<a-tag class="mr-5" color="green">{{ target.name }}</a-tag>
<fs-icon class="cd-icon-button" icon="ion:close-circle-outline" @click="clear"></fs-icon>
</span>
<span v-else class="mlr-5 text-gray">{{ placeholder }}</span>
<a-button class="ml-5" :size="size" @click="chooseForm.open"></a-button>
<a-form-item-rest v-if="chooseForm.show">
<a-modal v-model:open="chooseForm.show" title="选择通知" width="900px" @ok="chooseForm.ok">
<div style="height: 400px; position: relative">
<cert-notification-modal v-model="selectedId" :type="type" :from="from"></cert-notification-modal>
</div>
</a-modal>
</a-form-item-rest>
</div>
</template>
<script>
import { defineComponent, reactive, ref, watch, inject } from "vue";
import CertNotificationModal from "./modal/index.vue";
import { createApi } from "../api";
import { message } from "ant-design-vue";
export default defineComponent({
name: "NotificationSelector",
components: { CertNotificationModal },
props: {
modelValue: {
type: [Number, String],
default: null
},
type: {
type: String,
default: ""
},
placeholder: {
type: String,
default: "请选择"
},
size: {
type: String,
default: "middle"
}
},
emits: ["update:modelValue"],
setup(props, ctx) {
const api = createApi();
const target = ref({});
const selectedId = ref();
async function refreshTarget(value) {
selectedId.value = value;
if (value > 0) {
target.value = await api.GetSimpleInfo(value);
}
}
function clear() {
if (pipeline && pipeline.userId !== target.value.userId) {
message.error("对不起,您不能修改他人流水线的通知");
return;
}
selectedId.value = "";
target.value = null;
ctx.emit("update:modelValue", selectedId.value);
}
watch(
() => {
return props.modelValue;
},
async (value) => {
selectedId.value = null;
target.value = {};
if (value == null) {
return;
}
await refreshTarget(value);
},
{
immediate: true
}
);
const providerDefine = ref({});
async function refreshProviderDefine(type) {
providerDefine.value = await api.GetProviderDefine(type);
}
watch(
() => {
return props.type;
},
async (value) => {
await refreshProviderDefine(value);
},
{
immediate: true
}
);
//pipeline
const pipeline = inject("pipeline", null);
const chooseForm = reactive({
show: false,
open() {
chooseForm.show = true;
},
ok: () => {
chooseForm.show = false;
console.log("choose ok:", selectedId.value);
refreshTarget(selectedId.value);
if (pipeline && pipeline.userId !== target.value.userId) {
message.error("对不起,您不能修改他人流水线的授权");
return;
}
ctx.emit("change", selectedId.value);
ctx.emit("update:modelValue", selectedId.value);
}
});
return {
clear,
target,
selectedId,
providerDefine,
chooseForm
};
}
});
</script>
<style lang="less">
.notification-selector {
}
</style>

View File

@ -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<UserPageRes> => {
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
}
}
};
}

View File

@ -0,0 +1,58 @@
<template>
<fs-page class="page-cert-notification-modal">
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts">
import { defineComponent, onMounted, watch } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createApi } from "../../api";
export default defineComponent({
name: "CertNotificationModal",
props: {
type: {
type: String,
default: ""
},
modelValue: {}
},
emits: ["update:modelValue"],
setup(props, ctx) {
const api = createApi();
const context: any = { props, ctx, api };
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
// crud
function onTypeChanged(value: any) {
context.typeRef.value = value;
crudExpose.setSearchFormData({ form: { type: value }, mergeForm: true });
crudExpose.doRefresh();
}
watch(
() => {
return props.type;
},
(value) => {
console.log("access type changed:", value);
onTypeChanged(value);
}
);
//
onMounted(() => {
onTypeChanged(props.type);
});
return {
crudBinding,
crudRef
};
}
});
</script>
<style lang="less">
.page-cert-notification {
}
</style>

View File

@ -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));

View File

@ -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<NotificationService> {
@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);
}
}

View File

@ -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;
}

View File

@ -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<NotificationEntity> {
@InjectEntityModel(NotificationEntity)
repository: Repository<NotificationEntity>;
//@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);
}
}

View File

@ -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,
});
}
}

View File

@ -0,0 +1,2 @@
export * from './qywx/index.js';
export * from './email/index.js';

View File

@ -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,
},
},
});
}
}