mirror of https://github.com/certd/certd
				
				
				
			feat: 邮件通知
							parent
							
								
									64afebecd4
								
							
						
					
					
						commit
						937e3fac19
					
				|  | @ -2,9 +2,9 @@ | ||||||
|   "name": "@certd/pipeline", |   "name": "@certd/pipeline", | ||||||
|   "private": false, |   "private": false, | ||||||
|   "version": "1.0.6", |   "version": "1.0.6", | ||||||
|   "main": "./dist/bundle.js", |   "main": "./src", | ||||||
|   "module": "./dist/pipeline.mjs", |   "module": "./src", | ||||||
|   "types": "./dist/d/index.d.ts", |   "types": "./src", | ||||||
|   "publishConfig": { |   "publishConfig": { | ||||||
|     "main": "./dist/bundle.js", |     "main": "./dist/bundle.js", | ||||||
|     "module": "./dist/bundle.mjs", |     "module": "./dist/bundle.mjs", | ||||||
|  | @ -19,6 +19,7 @@ | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "axios": "^1.4.0", |     "axios": "^1.4.0", | ||||||
|     "node-forge": "^1.3.1", |     "node-forge": "^1.3.1", | ||||||
|  |     "nodemailer": "^6.9.3", | ||||||
|     "qs": "^6.11.2" |     "qs": "^6.11.2" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { ConcurrencyStrategy, NotificationType, NotificationWhen, Pipeline, ResultType, Runnable, RunStrategy, Stage, Step, Task } from "../d.ts"; | import { ConcurrencyStrategy, NotificationWhen, Pipeline, ResultType, Runnable, RunStrategy, Stage, Step, Task } from "../d.ts"; | ||||||
| import _ from "lodash"; | import _ from "lodash"; | ||||||
| import { RunHistory, RunnableCollection } from "./run-history"; | import { RunHistory, RunnableCollection } from "./run-history"; | ||||||
| import { AbstractTaskPlugin, PluginDefine, pluginRegistry } from "../plugin"; | import { AbstractTaskPlugin, PluginDefine, pluginRegistry } from "../plugin"; | ||||||
|  | @ -216,13 +216,13 @@ export class Executor { | ||||||
|     let subject = ""; |     let subject = ""; | ||||||
|     let content = ""; |     let content = ""; | ||||||
|     if (when === "start") { |     if (when === "start") { | ||||||
|       subject = `【CertD】${this.pipeline.title} 开始执行,buildId:${this.runtime.id}`; |       subject = `【CertD】开始执行,${this.pipeline.title}, buildId:${this.runtime.id}`; | ||||||
|       content = `【CertD】${this.pipeline.title} 开始执行,buildId:${this.runtime.id}`; |       content = subject; | ||||||
|     } else if (when === "success") { |     } else if (when === "success") { | ||||||
|       subject = `【CertD】${this.pipeline.title} 执行成功,buildId:${this.runtime.id}`; |       subject = `【CertD】执行成功,${this.pipeline.title}, buildId:${this.runtime.id}`; | ||||||
|       content = `【CertD】${this.pipeline.title} 执行成功,buildId:${this.runtime.id}`; |       content = subject; | ||||||
|     } else if (when === "error") { |     } else if (when === "error") { | ||||||
|       subject = `【CertD】${this.pipeline.title} 执行失败,buildId:${this.runtime.id}`; |       subject = `【CertD】执行失败,${this.pipeline.title}, buildId:${this.runtime.id}`; | ||||||
|       content = `<pre>${error.message}</pre>`; |       content = `<pre>${error.message}</pre>`; | ||||||
|     } else { |     } else { | ||||||
|       return; |       return; | ||||||
|  | @ -234,6 +234,7 @@ export class Executor { | ||||||
|       } |       } | ||||||
|       if (notification.type === "email") { |       if (notification.type === "email") { | ||||||
|         this.options.emailService?.send({ |         this.options.emailService?.send({ | ||||||
|  |           userId: this.pipeline.userId, | ||||||
|           subject, |           subject, | ||||||
|           content, |           content, | ||||||
|           receivers: notification.options.receivers, |           receivers: notification.options.receivers, | ||||||
|  |  | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | export type EmailSend = { | ||||||
|  |   userId: number; | ||||||
|  |   subject: string; | ||||||
|  |   content: string; | ||||||
|  |   receivers: string[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export interface IEmailService { | ||||||
|  |   send(email: EmailSend): Promise<void>; | ||||||
|  | } | ||||||
|  | @ -1,9 +1 @@ | ||||||
| export type EmailSend = { | export * from "./email"; | ||||||
|   subject: string; |  | ||||||
|   content: string; |  | ||||||
|   receivers: string[]; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export interface IEmailService { |  | ||||||
|   send(email: EmailSend): Promise<void>; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
| VITE_APP_API=/api | VITE_APP_API=/api | ||||||
| #登录与权限关闭 | #登录与权限关闭 | ||||||
| VITE_APP_PM_ENABLED=true | VITE_APP_PM_ENABLED=true | ||||||
|  | VITE_APP_TITLE=Certd | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
|     <meta charset="UTF-8" /> |     <meta charset="UTF-8" /> | ||||||
|     <link rel="icon" href="/logo.svg" /> |     <link rel="icon" href="/logo.svg" /> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|     <title>antdv-fast-crud</title> |     <title>Certd-让你的证书永不过期</title> | ||||||
|     <link rel="stylesheet" type="text/css" href="/index.css" /> |     <link rel="stylesheet" type="text/css" href="/index.css" /> | ||||||
|   </head> |   </head> | ||||||
|   <body> |   <body> | ||||||
|  |  | ||||||
|  | @ -106,8 +106,8 @@ function createService() { | ||||||
|  * @description 创建请求方法 |  * @description 创建请求方法 | ||||||
|  * @param {Object} service axios 实例 |  * @param {Object} service axios 实例 | ||||||
|  */ |  */ | ||||||
| function createRequestFunction(service) { | function createRequestFunction(service: any) { | ||||||
|   return function (config) { |   return function (config: any) { | ||||||
|     const configDefault = { |     const configDefault = { | ||||||
|       headers: { |       headers: { | ||||||
|         "Content-Type": get(config, "headers.Content-Type", "application/json") |         "Content-Type": get(config, "headers.Content-Type", "application/json") | ||||||
|  |  | ||||||
|  | @ -48,7 +48,10 @@ export function responseError(data = {}, msg = "请求失败", code = 500) { | ||||||
|  * @description 记录和显示错误 |  * @description 记录和显示错误 | ||||||
|  * @param {Error} error 错误对象 |  * @param {Error} error 错误对象 | ||||||
|  */ |  */ | ||||||
| export function errorLog(error) { | export function errorLog(error: any) { | ||||||
|  |   if (error?.response?.data?.message) { | ||||||
|  |     error.message = error?.response?.data?.message; | ||||||
|  |   } | ||||||
|   // 打印到控制台
 |   // 打印到控制台
 | ||||||
|   console.error(error); |   console.error(error); | ||||||
|   // 显示提示
 |   // 显示提示
 | ||||||
|  | @ -59,8 +62,6 @@ export function errorLog(error) { | ||||||
|  * @description 创建一个错误 |  * @description 创建一个错误 | ||||||
|  * @param {String} msg 错误信息 |  * @param {String} msg 错误信息 | ||||||
|  */ |  */ | ||||||
| export function errorCreate(msg) { | export function errorCreate(msg: string) { | ||||||
|   const error = new Error(msg); |   throw new Error(msg); | ||||||
|   errorLog(error); |  | ||||||
|   throw error; |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -35,6 +35,28 @@ export const certdResources = [ | ||||||
|         meta: { |         meta: { | ||||||
|           icon: "ion:disc-outline" |           icon: "ion:disc-outline" | ||||||
|         } |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: "设置", | ||||||
|  |         name: "certdSettings", | ||||||
|  |         path: "/certd/settings", | ||||||
|  |         redirect: "/certd/settings/email", | ||||||
|  |         meta: { | ||||||
|  |           icon: "ion:settings-outline", | ||||||
|  |           auth: true | ||||||
|  |         }, | ||||||
|  |         children: [ | ||||||
|  |           { | ||||||
|  |             title: "邮箱设置", | ||||||
|  |             name: "email", | ||||||
|  |             path: "/certd/settings/email", | ||||||
|  |             component: "/certd/settings/email-setting.vue", | ||||||
|  |             meta: { | ||||||
|  |               icon: "ion:mail-outline", | ||||||
|  |               auth: true | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|       } |       } | ||||||
|     ] |     ] | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -1,15 +1,16 @@ | ||||||
|  | // @ts-ignore
 | ||||||
| import _ from "lodash"; | import _ from "lodash"; | ||||||
| export function getEnvValue(key) { | export function getEnvValue(key: string) { | ||||||
|   // @ts-ignore
 |   // @ts-ignore
 | ||||||
|   return import.meta.env["VITE_APP_" + key]; |   return import.meta.env["VITE_APP_" + key]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class EnvConfig { | export class EnvConfig { | ||||||
|   API; |   API: string; | ||||||
|   MODE; |   MODE: string; | ||||||
|   STORAGE; |   STORAGE: string; | ||||||
|   TITLE; |   TITLE: string; | ||||||
|   PM_ENABLED; |   PM_ENABLED: string; | ||||||
|   constructor() { |   constructor() { | ||||||
|     this.init(); |     this.init(); | ||||||
|   } |   } | ||||||
|  | @ -19,6 +20,7 @@ export class EnvConfig { | ||||||
|     _.forEach(import.meta.env, (value, key) => { |     _.forEach(import.meta.env, (value, key) => { | ||||||
|       if (key.startsWith("VITE_APP")) { |       if (key.startsWith("VITE_APP")) { | ||||||
|         key = key.replace("VITE_APP_", ""); |         key = key.replace("VITE_APP_", ""); | ||||||
|  |         //@ts-ignore
 | ||||||
|         this[key] = value; |         this[key] = value; | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|  | @ -26,7 +28,8 @@ export class EnvConfig { | ||||||
|     this.MODE = import.meta.env.MODE; |     this.MODE = import.meta.env.MODE; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   get(key, defaultValue) { |   get(key: string, defaultValue: string) { | ||||||
|  |     //@ts-ignore
 | ||||||
|     return this[key] ?? defaultValue; |     return this[key] ?? defaultValue; | ||||||
|   } |   } | ||||||
|   isDev() { |   isDev() { | ||||||
|  |  | ||||||
|  | @ -2,9 +2,9 @@ import { env } from "./util.env"; | ||||||
| export const site = { | export const site = { | ||||||
|   /** |   /** | ||||||
|    * @description 更新标题 |    * @description 更新标题 | ||||||
|    * @param {String} title 标题 |    * @param titleText | ||||||
|    */ |    */ | ||||||
|   title: function (titleText) { |   title: function (titleText: string) { | ||||||
|     const processTitle = env.TITLE || "FsAdmin"; |     const processTitle = env.TITLE || "FsAdmin"; | ||||||
|     window.document.title = `${processTitle}${titleText ? ` | ${titleText}` : ""}`; |     window.document.title = `${processTitle}${titleText ? ` | ${titleText}` : ""}`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import { RunHistory } from "/@/views/certd/pipeline/pipeline/type"; | ||||||
| 
 | 
 | ||||||
| const apiPrefix = "/pi/history"; | const apiPrefix = "/pi/history"; | ||||||
| 
 | 
 | ||||||
| export async function GetList(query) { | export async function GetList(query: any) { | ||||||
|   const list = await request({ |   const list = await request({ | ||||||
|     url: apiPrefix + "/list", |     url: apiPrefix + "/list", | ||||||
|     method: "post", |     method: "post", | ||||||
|  | @ -18,7 +18,7 @@ export async function GetList(query) { | ||||||
|   return list; |   return list; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function GetDetail(query): Promise<RunHistory> { | export async function GetDetail(query: any): Promise<RunHistory> { | ||||||
|   const detail = await request({ |   const detail = await request({ | ||||||
|     url: apiPrefix + "/detail", |     url: apiPrefix + "/detail", | ||||||
|     method: "post", |     method: "post", | ||||||
|  |  | ||||||
|  | @ -0,0 +1,200 @@ | ||||||
|  | <template> | ||||||
|  |   <a-drawer v-model:visible="notificationDrawerVisible" placement="right" :closable="true" width="600px" class="pi-notification-form" :after-visible-change="notificationDrawerOnAfterVisibleChange"> | ||||||
|  |     <template #title> | ||||||
|  |       编辑触发器 | ||||||
|  |       <a-button v-if="mode === 'edit'" @click="notificationDelete()"> | ||||||
|  |         <template #icon><DeleteOutlined /></template> | ||||||
|  |       </a-button> | ||||||
|  |     </template> | ||||||
|  |     <template v-if="currentNotification"> | ||||||
|  |       <pi-container> | ||||||
|  |         <a-form ref="notificationFormRef" class="notification-form" :model="currentNotification" :label-col="labelCol" :wrapper-col="wrapperCol"> | ||||||
|  |           <fs-form-item | ||||||
|  |             v-model="currentNotification.type" | ||||||
|  |             :item="{ | ||||||
|  |               title: '类型', | ||||||
|  |               key: 'type', | ||||||
|  |               value: 'email', | ||||||
|  |               component: { | ||||||
|  |                 name: 'a-select', | ||||||
|  |                 vModel: 'value', | ||||||
|  |                 disabled: !editMode, | ||||||
|  |                 options: [{ value: 'email', label: '邮件' }] | ||||||
|  |               }, | ||||||
|  |               rules: [{ required: true, message: '此项必填' }] | ||||||
|  |             }" | ||||||
|  |           /> | ||||||
|  |           <fs-form-item | ||||||
|  |             v-model="currentNotification.when" | ||||||
|  |             :item="{ | ||||||
|  |               title: '触发时机', | ||||||
|  |               key: 'type', | ||||||
|  |               value: ['error'], | ||||||
|  |               component: { | ||||||
|  |                 name: 'a-select', | ||||||
|  |                 vModel: 'value', | ||||||
|  |                 disabled: !editMode, | ||||||
|  |                 mode: 'multiple', | ||||||
|  |                 options: [ | ||||||
|  |                   { value: 'start', label: '开始时' }, | ||||||
|  |                   { value: 'success', label: '成功时' }, | ||||||
|  |                   { value: 'error', label: '错误时' } | ||||||
|  |                 ] | ||||||
|  |               }, | ||||||
|  |               rules: [{ required: true, message: '此项必填' }] | ||||||
|  |             }" | ||||||
|  |           /> | ||||||
|  |           <pi-notification-form-email ref="optionsRef" v-model:options="currentNotification.options"></pi-notification-form-email> | ||||||
|  |         </a-form> | ||||||
|  | 
 | ||||||
|  |         <template #footer> | ||||||
|  |           <a-form-item v-if="editMode" :wrapper-col="{ span: 14, offset: 4 }"> | ||||||
|  |             <a-button type="primary" @click="notificationSave"> 确定 </a-button> | ||||||
|  |           </a-form-item> | ||||||
|  |         </template> | ||||||
|  |       </pi-container> | ||||||
|  |     </template> | ||||||
|  |   </a-drawer> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { Modal } from "ant-design-vue"; | ||||||
|  | import { ref } from "vue"; | ||||||
|  | import _ from "lodash"; | ||||||
|  | import { nanoid } from "nanoid"; | ||||||
|  | import PiNotificationFormEmail from "./pi-notification-form-email.vue"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "PiNotificationForm", | ||||||
|  |   components: { PiNotificationFormEmail }, | ||||||
|  |   props: { | ||||||
|  |     editMode: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: true | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   emits: ["update"], | ||||||
|  |   setup(props: any, context: any) { | ||||||
|  |     /** | ||||||
|  |      *  notification drawer | ||||||
|  |      * @returns | ||||||
|  |      */ | ||||||
|  |     function useNotificationForm() { | ||||||
|  |       const mode = ref("add"); | ||||||
|  |       const callback = ref(); | ||||||
|  |       const currentNotification = ref({ type: undefined, when: [], options: {} }); | ||||||
|  |       const currentPlugin = ref({}); | ||||||
|  |       const notificationFormRef = ref(null); | ||||||
|  |       const notificationDrawerVisible = ref(false); | ||||||
|  |       const optionsRef = ref(); | ||||||
|  |       const rules = ref({ | ||||||
|  |         type: [ | ||||||
|  |           { | ||||||
|  |             type: "string", | ||||||
|  |             required: true, | ||||||
|  |             message: "请选择类型" | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         when: [ | ||||||
|  |           { | ||||||
|  |             type: "string", | ||||||
|  |             required: true, | ||||||
|  |             message: "请选择通知时机" | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       const notificationDrawerShow = () => { | ||||||
|  |         notificationDrawerVisible.value = true; | ||||||
|  |       }; | ||||||
|  |       const notificationDrawerClose = () => { | ||||||
|  |         notificationDrawerVisible.value = false; | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       const notificationDrawerOnAfterVisibleChange = (val: any) => { | ||||||
|  |         console.log("notificationDrawerOnAfterVisibleChange", val); | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       const notificationOpen = (notification: any, emit: any) => { | ||||||
|  |         callback.value = emit; | ||||||
|  |         currentNotification.value = _.cloneDeep(notification); | ||||||
|  |         console.log("currentNotificationOpen", currentNotification.value); | ||||||
|  |         notificationDrawerShow(); | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       const notificationAdd = (emit: any) => { | ||||||
|  |         mode.value = "add"; | ||||||
|  |         const notification = { id: nanoid(), type: "email", when: ["error"] }; | ||||||
|  |         notificationOpen(notification, emit); | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       const notificationEdit = (notification: any, emit: any) => { | ||||||
|  |         mode.value = "edit"; | ||||||
|  |         notificationOpen(notification, emit); | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       const notificationView = (notification: any, emit: any) => { | ||||||
|  |         mode.value = "view"; | ||||||
|  |         notificationOpen(notification, emit); | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       const notificationSave = async (e: any) => { | ||||||
|  |         currentNotification.value.options = await optionsRef.value.getValue(); | ||||||
|  |         console.log("currentNotificationSave", currentNotification.value); | ||||||
|  |         try { | ||||||
|  |           await notificationFormRef.value.validate(); | ||||||
|  |         } catch (e) { | ||||||
|  |           console.error("表单验证失败:", e); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         callback.value("save", currentNotification.value); | ||||||
|  |         notificationDrawerClose(); | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       const notificationDelete = () => { | ||||||
|  |         Modal.confirm({ | ||||||
|  |           title: "确认", | ||||||
|  |           content: `确定要删除此触发器吗?`, | ||||||
|  |           async onOk() { | ||||||
|  |             callback.value("delete"); | ||||||
|  |             notificationDrawerClose(); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       const blankFn = () => { | ||||||
|  |         return {}; | ||||||
|  |       }; | ||||||
|  |       return { | ||||||
|  |         notificationFormRef, | ||||||
|  |         mode, | ||||||
|  |         notificationAdd, | ||||||
|  |         notificationEdit, | ||||||
|  |         notificationView, | ||||||
|  |         notificationDrawerShow, | ||||||
|  |         notificationDrawerVisible, | ||||||
|  |         notificationDrawerOnAfterVisibleChange, | ||||||
|  |         currentNotification, | ||||||
|  |         currentPlugin, | ||||||
|  |         notificationSave, | ||||||
|  |         notificationDelete, | ||||||
|  |         rules, | ||||||
|  |         blankFn, | ||||||
|  |         optionsRef | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       ...useNotificationForm(), | ||||||
|  |       labelCol: { span: 6 }, | ||||||
|  |       wrapperCol: { span: 16 } | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="less"> | ||||||
|  | .pi-notification-form { | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -0,0 +1,57 @@ | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <fs-form-item | ||||||
|  |       v-model="optionsFormState.receivers" | ||||||
|  |       :item="{ | ||||||
|  |         title: '收件邮箱', | ||||||
|  |         key: 'type', | ||||||
|  |         component: { | ||||||
|  |           name: 'a-select', | ||||||
|  |           vModel: 'value', | ||||||
|  |           mode: 'tags' | ||||||
|  |         }, | ||||||
|  |         rules: [{ required: true, message: '此项必填' }] | ||||||
|  |       }" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { Ref, ref, watch } from "vue"; | ||||||
|  | 
 | ||||||
|  | const props = defineProps({ | ||||||
|  |   options: { | ||||||
|  |     type: Object as PropType<any>, | ||||||
|  |     default: () => {} | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const optionsFormState: Ref<any> = ref({}); | ||||||
|  | 
 | ||||||
|  | watch( | ||||||
|  |   () => { | ||||||
|  |     return props.options; | ||||||
|  |   }, | ||||||
|  |   () => { | ||||||
|  |     optionsFormState.value = { | ||||||
|  |       ...props.options | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     immediate: true | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const emit = defineEmits(["change"]); | ||||||
|  | function doEmit() { | ||||||
|  |   emit("change", { ...optionsFormState.value }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getValue() { | ||||||
|  |   return { ...optionsFormState.value }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | defineExpose({ | ||||||
|  |   doEmit, | ||||||
|  |   getValue | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | @ -2,7 +2,6 @@ | ||||||
|   <fs-page v-if="pipeline" class="page-pipeline-edit"> |   <fs-page v-if="pipeline" class="page-pipeline-edit"> | ||||||
|     <template #header> |     <template #header> | ||||||
|       <div class="title"> |       <div class="title"> | ||||||
|         <fs-button icon="ion:left" @click="goBack" /> |  | ||||||
|         <pi-editable v-model="pipeline.title" :hover-show="false" :disabled="!editMode"></pi-editable> |         <pi-editable v-model="pipeline.title" :hover-show="false" :disabled="!editMode"></pi-editable> | ||||||
|       </div> |       </div> | ||||||
|       <div class="more"> |       <div class="more"> | ||||||
|  | @ -63,7 +62,7 @@ | ||||||
|                 </div> |                 </div> | ||||||
|               </div> |               </div> | ||||||
| 
 | 
 | ||||||
|               <div v-for="(stage, index) of pipeline.stages" :key="stage.id" class="stage" :class="{ 'last-stage': !editMode && index === pipeline.stages.length - 1 }"> |               <div v-for="(stage, index) of pipeline.stages" :key="stage.id" class="stage" :class="{ 'last-stage': isLastStage(index) }"> | ||||||
|                 <div class="title"> |                 <div class="title"> | ||||||
|                   <pi-editable v-model="stage.title" :disabled="!editMode"></pi-editable> |                   <pi-editable v-model="stage.title" :disabled="!editMode"></pi-editable> | ||||||
|                 </div> |                 </div> | ||||||
|  | @ -118,6 +117,48 @@ | ||||||
|                       </a-button> |                       </a-button> | ||||||
|                     </div> |                     </div> | ||||||
|                   </div> |                   </div> | ||||||
|  |                   <div v-for="(item, ii) of pipeline.notifications" :key="ii" class="task-container"> | ||||||
|  |                     <div class="line"> | ||||||
|  |                       <div class="flow-line"></div> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="task"> | ||||||
|  |                       <a-button shape="round" @click="notificationEdit(item, ii as number)"> | ||||||
|  |                         <fs-icon icon="ion:notifications"></fs-icon> | ||||||
|  |                         【通知】 {{ item.type }} | ||||||
|  |                       </a-button> | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|  |                   <div class="task-container"> | ||||||
|  |                     <div class="line"> | ||||||
|  |                       <div class="flow-line"></div> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="task"> | ||||||
|  |                       <a-button shape="round" type="dashed" @click="notificationAdd()"> | ||||||
|  |                         <fs-icon icon="ion:add-circle-outline"></fs-icon> | ||||||
|  | 
 | ||||||
|  |                         添加通知 | ||||||
|  |                       </a-button> | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |               <div v-else class="stage last-stage"> | ||||||
|  |                 <div class="title"> | ||||||
|  |                   <pi-editable model-value="结束" :disabled="true" /> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="tasks"> | ||||||
|  |                   <div v-for="(item, index) of pipeline.notifications" :key="index" class="task-container" :class="{ 'first-task': index == 0 }"> | ||||||
|  |                     <div class="line"> | ||||||
|  |                       <div class="flow-line"></div> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="task"> | ||||||
|  |                       <a-button shape="round" @click="notificationEdit(item, index)"> | ||||||
|  |                         <fs-icon icon="ion:notifications"></fs-icon> | ||||||
|  | 
 | ||||||
|  |                         【通知】 {{ item.type }} | ||||||
|  |                       </a-button> | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|                 </div> |                 </div> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|  | @ -140,6 +181,7 @@ | ||||||
|     <pi-task-form ref="taskFormRef" :edit-mode="editMode"></pi-task-form> |     <pi-task-form ref="taskFormRef" :edit-mode="editMode"></pi-task-form> | ||||||
|     <pi-trigger-form ref="triggerFormRef" :edit-mode="editMode"></pi-trigger-form> |     <pi-trigger-form ref="triggerFormRef" :edit-mode="editMode"></pi-trigger-form> | ||||||
|     <pi-task-view ref="taskViewRef"></pi-task-view> |     <pi-task-view ref="taskViewRef"></pi-task-view> | ||||||
|  |     <PiNotificationForm ref="notificationFormRef" :edit-mode="editMode"></PiNotificationForm> | ||||||
|   </fs-page> |   </fs-page> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -148,9 +190,10 @@ import { defineComponent, ref, provide, Ref, watch } from "vue"; | ||||||
| import { useRouter } from "vue-router"; | import { useRouter } from "vue-router"; | ||||||
| import PiTaskForm from "./component/task-form/index.vue"; | import PiTaskForm from "./component/task-form/index.vue"; | ||||||
| import PiTriggerForm from "./component/trigger-form/index.vue"; | import PiTriggerForm from "./component/trigger-form/index.vue"; | ||||||
|  | import PiNotificationForm from "./component/notification-form/index.vue"; | ||||||
| import PiTaskView from "./component/task-view/index.vue"; | import PiTaskView from "./component/task-view/index.vue"; | ||||||
| import PiStatusShow from "./component/status-show.vue"; | import PiStatusShow from "./component/status-show.vue"; | ||||||
| import _ from "lodash-es"; | import _ from "lodash"; | ||||||
| import { message, Modal, notification } from "ant-design-vue"; | import { message, Modal, notification } from "ant-design-vue"; | ||||||
| import { pluginManager } from "/@/views/certd/pipeline/pipeline/plugin"; | import { pluginManager } from "/@/views/certd/pipeline/pipeline/plugin"; | ||||||
| import { nanoid } from "nanoid"; | import { nanoid } from "nanoid"; | ||||||
|  | @ -159,7 +202,7 @@ import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/hi | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|   name: "PipelineEdit", |   name: "PipelineEdit", | ||||||
|   // eslint-disable-next-line vue/no-unused-components |   // eslint-disable-next-line vue/no-unused-components | ||||||
|   components: { PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow }, |   components: { PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm }, | ||||||
|   props: { |   props: { | ||||||
|     pipelineId: { |     pipelineId: { | ||||||
|       type: [Number, String], |       type: [Number, String], | ||||||
|  | @ -269,7 +312,7 @@ export default defineComponent({ | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|         const detail: PipelineDetail = await props.options.getPipelineDetail({ pipelineId: value }); |         const detail: PipelineDetail = await props.options.getPipelineDetail({ pipelineId: value }); | ||||||
|         currentPipeline.value = _.merge({ title: "新管道流程", stages: [], triggers: [] }, detail.pipeline); |         currentPipeline.value = _.merge({ title: "新管道流程", stages: [], triggers: [], notifications: [] }, detail.pipeline); | ||||||
|         pipeline.value = currentPipeline.value; |         pipeline.value = currentPipeline.value; | ||||||
|         await loadHistoryList(true); |         await loadHistoryList(true); | ||||||
|       }, |       }, | ||||||
|  | @ -369,8 +412,13 @@ export default defineComponent({ | ||||||
|           pipeline.value.stages.splice(stageIndex, 0, stage); |           pipeline.value.stages.splice(stageIndex, 0, stage); | ||||||
|         }); |         }); | ||||||
|       }; |       }; | ||||||
|  | 
 | ||||||
|  |       function isLastStage(index: number) { | ||||||
|  |         return !props.editMode && index === pipeline.value.stages.length - 1 && pipeline.value.notifications?.length < 1; | ||||||
|  |       } | ||||||
|       return { |       return { | ||||||
|         stageAdd |         stageAdd, | ||||||
|  |         isLastStage | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -406,6 +454,41 @@ export default defineComponent({ | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     function useNotification() { | ||||||
|  |       const notificationFormRef = ref(); | ||||||
|  |       const notificationAdd = () => { | ||||||
|  |         notificationFormRef.value.notificationAdd((type: string, value: any) => { | ||||||
|  |           if (type === "save") { | ||||||
|  |             if (pipeline.value.notifications == null) { | ||||||
|  |               pipeline.value.notifications = []; | ||||||
|  |             } | ||||||
|  |             pipeline.value.notifications.push(value); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }; | ||||||
|  |       const notificationEdit = (notification: any, index: any) => { | ||||||
|  |         if (notificationFormRef.value == null) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         if (props.editMode) { | ||||||
|  |           notificationFormRef.value.notificationEdit(notification, (type: string, value: any) => { | ||||||
|  |             if (type === "delete") { | ||||||
|  |               pipeline.value.notifications.splice(index, 1); | ||||||
|  |             } else if (type === "save") { | ||||||
|  |               pipeline.value.notifications[index] = value; | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |         } else { | ||||||
|  |           notificationFormRef.value.notificationView(notification, (type: string, value: any) => {}); | ||||||
|  |         } | ||||||
|  |       }; | ||||||
|  |       return { | ||||||
|  |         notificationAdd, | ||||||
|  |         notificationEdit, | ||||||
|  |         notificationFormRef | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     function useActions() { |     function useActions() { | ||||||
|       const saveLoading = ref(); |       const saveLoading = ref(); | ||||||
|       const run = async () => { |       const run = async () => { | ||||||
|  | @ -484,6 +567,7 @@ export default defineComponent({ | ||||||
|         historyCancel |         historyCancel | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     const useTaskRet = useTask(); |     const useTaskRet = useTask(); | ||||||
|     const useStageRet = useStage(useTaskRet); |     const useStageRet = useStage(useTaskRet); | ||||||
| 
 | 
 | ||||||
|  | @ -495,7 +579,8 @@ export default defineComponent({ | ||||||
|       ...useStageRet, |       ...useStageRet, | ||||||
|       ...useTrigger(), |       ...useTrigger(), | ||||||
|       ...useActions(), |       ...useActions(), | ||||||
|       ...useHistory() |       ...useHistory(), | ||||||
|  |       ...useNotification() | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -8,9 +8,9 @@ export class PluginManager { | ||||||
|    * 初始化plugins |    * 初始化plugins | ||||||
|    * @param plugins |    * @param plugins | ||||||
|    */ |    */ | ||||||
|   init(plugins) { |   init(plugins: any) { | ||||||
|     const list = plugins; |     const list = plugins; | ||||||
|     const map = {}; |     const map: any = {}; | ||||||
|     for (const plugin of list) { |     for (const plugin of list) { | ||||||
|       map[plugin.key] = plugin; |       map[plugin.key] = plugin; | ||||||
|     } |     } | ||||||
|  | @ -21,7 +21,7 @@ export class PluginManager { | ||||||
|     return this.map[name]; |     return this.map[name]; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getPreStepOutputOptions({ pipeline, currentStageIndex, currentStepIndex, currentTask }) { |   getPreStepOutputOptions({ pipeline, currentStageIndex, currentStepIndex, currentTask }: any) { | ||||||
|     const steps = this.collectionPreStepOutputs({ |     const steps = this.collectionPreStepOutputs({ | ||||||
|       pipeline, |       pipeline, | ||||||
|       currentStageIndex, |       currentStageIndex, | ||||||
|  | @ -42,7 +42,7 @@ export class PluginManager { | ||||||
|     return options; |     return options; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   collectionPreStepOutputs({ pipeline, currentStageIndex, currentStepIndex, currentTask }) { |   collectionPreStepOutputs({ pipeline, currentStageIndex, currentStepIndex, currentTask }: any) { | ||||||
|     const steps: any[] = []; |     const steps: any[] = []; | ||||||
|     // 开始放step
 |     // 开始放step
 | ||||||
|     for (let i = 0; i < currentStageIndex; i++) { |     for (let i = 0; i < currentStageIndex; i++) { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,12 @@ | ||||||
|  | import { request } from "/@/api/service"; | ||||||
|  | const apiPrefix = "/basic/email"; | ||||||
|  | 
 | ||||||
|  | export async function TestSend(receiver: string) { | ||||||
|  |   await request({ | ||||||
|  |     url: apiPrefix + "/test", | ||||||
|  |     method: "post", | ||||||
|  |     data: { | ||||||
|  |       receiver | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,26 @@ | ||||||
|  | import { request } from "/@/api/service"; | ||||||
|  | const apiPrefix = "/sys/settings"; | ||||||
|  | 
 | ||||||
|  | export const SettingKeys = { | ||||||
|  |   Email: "email" | ||||||
|  | }; | ||||||
|  | export async function SettingsGet(key: string) { | ||||||
|  |   return await request({ | ||||||
|  |     url: apiPrefix + "/get", | ||||||
|  |     method: "post", | ||||||
|  |     params: { | ||||||
|  |       key | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function SettingsSave(key: string, setting: any) { | ||||||
|  |   await request({ | ||||||
|  |     url: apiPrefix + "/save", | ||||||
|  |     method: "post", | ||||||
|  |     data: { | ||||||
|  |       key, | ||||||
|  |       setting: JSON.stringify(setting) | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,128 @@ | ||||||
|  | <template> | ||||||
|  |   <fs-page class="page-setting-email"> | ||||||
|  |     <template #header> | ||||||
|  |       <div class="title">邮件设置</div> | ||||||
|  |     </template> | ||||||
|  |     <div class="email-form"> | ||||||
|  |       <a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed"> | ||||||
|  |         <a-form-item label="STMP域名" name="host" :rules="[{ required: true, message: '请输入smtp域名或ip' }]"> | ||||||
|  |           <a-input v-model:value="formState.host" /> | ||||||
|  |         </a-form-item> | ||||||
|  | 
 | ||||||
|  |         <a-form-item label="STMP端口" name="port" :rules="[{ required: true, message: '请输入smtp端口号' }]"> | ||||||
|  |           <a-input v-model:value="formState.port" /> | ||||||
|  |         </a-form-item> | ||||||
|  | 
 | ||||||
|  |         <a-form-item label="用户名" :name="['auth', 'user']" :rules="[{ required: true, message: '请输入用户名' }]"> | ||||||
|  |           <a-input v-model:value="formState.auth.user" /> | ||||||
|  |         </a-form-item> | ||||||
|  |         <a-form-item label="密码" :name="['auth', 'pass']" :rules="[{ required: true, message: '请输入密码' }]"> | ||||||
|  |           <a-input-password v-model:value="formState.auth.pass" /> | ||||||
|  |         </a-form-item> | ||||||
|  |         <a-form-item label="发件邮箱" name="sender" :rules="[{ required: true, message: '请输入发件邮箱' }]"> | ||||||
|  |           <a-input v-model:value="formState.sender" /> | ||||||
|  |         </a-form-item> | ||||||
|  |         <a-form-item label="是否ssl" name="secure"> | ||||||
|  |           <a-switch v-model:checked="formState.secure" /> | ||||||
|  |         </a-form-item> | ||||||
|  |         <a-form-item label="忽略证书校验" name="tls.rejectUnauthorized"> | ||||||
|  |           <a-switch v-model:checked="formState.tls.rejectUnauthorized" /> | ||||||
|  |         </a-form-item> | ||||||
|  |         <a-form-item :wrapper-col="{ offset: 8, span: 16 }"> | ||||||
|  |           <a-button type="primary" html-type="submit">保存</a-button> | ||||||
|  |         </a-form-item> | ||||||
|  |       </a-form> | ||||||
|  |       <div> | ||||||
|  |         <a-form :model="testFormState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onTestSend"> | ||||||
|  |           <a-form-item label="测试收件邮箱" name="receiver" :rules="[{ required: true, message: '请输入测试收件邮箱' }]"> | ||||||
|  |             <a-input v-model:value="testFormState.receiver" /> | ||||||
|  |           </a-form-item> | ||||||
|  |           <a-form-item :wrapper-col="{ offset: 8, span: 16 }"> | ||||||
|  |             <a-button type="primary" html-type="submit">测试</a-button> | ||||||
|  |           </a-form-item> | ||||||
|  |         </a-form> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </fs-page> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { reactive } from "vue"; | ||||||
|  | import * as api from "./api"; | ||||||
|  | import * as emailApi from "./api.email"; | ||||||
|  | 
 | ||||||
|  | import { SettingKeys } from "./api"; | ||||||
|  | import { notification } from "ant-design-vue"; | ||||||
|  | 
 | ||||||
|  | interface FormState { | ||||||
|  |   host: string; | ||||||
|  |   port: number; | ||||||
|  |   auth: { | ||||||
|  |     user: string; | ||||||
|  |     pass: string; | ||||||
|  |   }; | ||||||
|  |   secure: boolean; // use TLS | ||||||
|  |   tls: { | ||||||
|  |     // do not fail on invalid certs | ||||||
|  |     rejectUnauthorized?: boolean; | ||||||
|  |   }; | ||||||
|  |   sender: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const formState = reactive<Partial<FormState>>({ | ||||||
|  |   auth: { | ||||||
|  |     user: "", | ||||||
|  |     pass: "" | ||||||
|  |   }, | ||||||
|  |   tls: {} | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | async function load() { | ||||||
|  |   const data: any = await api.SettingsGet(SettingKeys.Email); | ||||||
|  |   const setting = JSON.parse(data.setting); | ||||||
|  |   Object.assign(formState, setting); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | load(); | ||||||
|  | 
 | ||||||
|  | const onFinish = async (form: any) => { | ||||||
|  |   console.log("Success:", form); | ||||||
|  |   await api.SettingsSave(SettingKeys.Email, form); | ||||||
|  |   notification.success({ | ||||||
|  |     message: "保存成功" | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const onFinishFailed = (errorInfo: any) => { | ||||||
|  |   // console.log("Failed:", errorInfo); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | interface TestFormState { | ||||||
|  |   receiver: string; | ||||||
|  |   loading: boolean; | ||||||
|  | } | ||||||
|  | const testFormState = reactive<TestFormState>({ | ||||||
|  |   receiver: "", | ||||||
|  |   loading: false | ||||||
|  | }); | ||||||
|  | async function onTestSend() { | ||||||
|  |   testFormState.loading = true; | ||||||
|  |   try { | ||||||
|  |     await emailApi.TestSend(testFormState.receiver); | ||||||
|  |     notification.success({ | ||||||
|  |       message: "发送成功" | ||||||
|  |     }); | ||||||
|  |   } finally { | ||||||
|  |     testFormState.loading = false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="less"> | ||||||
|  | .page-setting-email { | ||||||
|  |   .email-form { | ||||||
|  |     width: 500px; | ||||||
|  |     margin: 20px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | CREATE TABLE "sys_settings" ( | ||||||
|  |                               "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, | ||||||
|  |                               "user_id" integer NOT NULL, | ||||||
|  |                               "key" varchar(100) NOT NULL, | ||||||
|  |                               "title" varchar(100) NOT NULL, | ||||||
|  |                               "setting" varchar(1024), | ||||||
|  |                               "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), | ||||||
|  |                               "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP) | ||||||
|  | ); | ||||||
|  | @ -9,7 +9,7 @@ | ||||||
|     "online:preview": "NODE_ENV=preview node ./bootstrap.js", |     "online:preview": "NODE_ENV=preview node ./bootstrap.js", | ||||||
|     "dev": "cross-env NODE_ENV=local midway-bin dev --ts --watchFile='../../core/pipeline/src,../../plugins/'", |     "dev": "cross-env NODE_ENV=local midway-bin dev --ts --watchFile='../../core/pipeline/src,../../plugins/'", | ||||||
|     "dev:preview": "cross-env NODE_ENV=preview midway-bin dev --ts", |     "dev:preview": "cross-env NODE_ENV=preview midway-bin dev --ts", | ||||||
|     "dev:syncdb": "cross-env NODE_ENV=syncdb midway-bin dev --ts --watchFile='../../core/pipeline/src'", |     "db": "cross-env NODE_ENV=syncdb midway-bin dev --ts", | ||||||
|     "test": "midway-bin test --ts", |     "test": "midway-bin test --ts", | ||||||
|     "cov": "midway-bin cov --ts", |     "cov": "midway-bin cov --ts", | ||||||
|     "lint": "mwts check", |     "lint": "mwts check", | ||||||
|  | @ -54,6 +54,7 @@ | ||||||
|     "md5": "^2.3.0", |     "md5": "^2.3.0", | ||||||
|     "midway-flyway-js": "^3.0.0", |     "midway-flyway-js": "^3.0.0", | ||||||
|     "node-cron": "^3.0.2", |     "node-cron": "^3.0.2", | ||||||
|  |     "nodemailer": "^6.9.3", | ||||||
|     "reflect-metadata": "^0.1.13", |     "reflect-metadata": "^0.1.13", | ||||||
|     "sqlite3": "^5.1.4", |     "sqlite3": "^5.1.4", | ||||||
|     "svg-captcha": "^1.4.0", |     "svg-captcha": "^1.4.0", | ||||||
|  | @ -67,6 +68,7 @@ | ||||||
|     "@types/jest": "^26.0.24", |     "@types/jest": "^26.0.24", | ||||||
|     "@types/koa": "2.13.4", |     "@types/koa": "2.13.4", | ||||||
|     "@types/node": "^14.18.35", |     "@types/node": "^14.18.35", | ||||||
|  |     "@types/nodemailer": "^6.4.8", | ||||||
|     "cross-env": "^6.0.3", |     "cross-env": "^6.0.3", | ||||||
|     "jest": "^26.6.3", |     "jest": "^26.6.3", | ||||||
|     "mwts": "^1.3.0", |     "mwts": "^1.3.0", | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ export abstract class BaseController { | ||||||
|    * 成功返回 |    * 成功返回 | ||||||
|    * @param data 返回数据 |    * @param data 返回数据 | ||||||
|    */ |    */ | ||||||
|   ok(data) { |   ok(data: any) { | ||||||
|     const res = { |     const res = { | ||||||
|       ...Constants.res.success, |       ...Constants.res.success, | ||||||
|       data: undefined, |       data: undefined, | ||||||
|  | @ -22,12 +22,21 @@ export abstract class BaseController { | ||||||
|   } |   } | ||||||
|   /** |   /** | ||||||
|    * 失败返回 |    * 失败返回 | ||||||
|    * @param message |    * @param msg | ||||||
|  |    * @param code | ||||||
|    */ |    */ | ||||||
|   fail(msg, code) { |   fail(msg: string, code: any) { | ||||||
|     return { |     return { | ||||||
|       code: code ? code : Constants.res.error.code, |       code: code ? code : Constants.res.error.code, | ||||||
|       msg: msg ? msg : Constants.res.error.code, |       msg: msg ? msg : Constants.res.error.code, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   getUserId() { | ||||||
|  |     const userId = this.ctx.user?.id; | ||||||
|  |     if (userId == null) { | ||||||
|  |       throw new Error('Token已过期'); | ||||||
|  |     } | ||||||
|  |     return userId; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -187,14 +187,19 @@ export abstract class BaseService<T> { | ||||||
|     return await qb.getMany(); |     return await qb.getMany(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async checkUserId(id = 0, userId, userKey = 'userId') { |   async checkUserId( | ||||||
|  |     id: any = 0, | ||||||
|  |     userId, | ||||||
|  |     userKey = 'userId', | ||||||
|  |     queryIdKey = 'id' | ||||||
|  |   ) { | ||||||
|     // @ts-ignore
 |     // @ts-ignore
 | ||||||
|     const res = await this.getRepository().findOne({ |     const res = await this.getRepository().findOne({ | ||||||
|       // @ts-ignore
 |       // @ts-ignore
 | ||||||
|       select: { [userKey]: true }, |       select: { [userKey]: true }, | ||||||
|  |       // @ts-ignore
 | ||||||
|       where: { |       where: { | ||||||
|         // @ts-ignore
 |         [queryIdKey]: id, | ||||||
|         id, |  | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|     // @ts-ignore
 |     // @ts-ignore
 | ||||||
|  |  | ||||||
|  | @ -1,9 +1,5 @@ | ||||||
| import { Provide } from '@midwayjs/decorator'; | import { Provide } from '@midwayjs/decorator'; | ||||||
| import { | import { IWebMiddleware, IMidwayKoaContext, NextFunction } from '@midwayjs/koa'; | ||||||
|   IWebMiddleware, |  | ||||||
|   IMidwayKoaContext, |  | ||||||
|   NextFunction, |  | ||||||
| } from '@midwayjs/koa'; |  | ||||||
| import { logger } from '../utils/logger'; | import { logger } from '../utils/logger'; | ||||||
| import { Result } from '../basic/result'; | import { Result } from '../basic/result'; | ||||||
| 
 | 
 | ||||||
|  | @ -20,7 +16,10 @@ export class GlobalExceptionMiddleware implements IWebMiddleware { | ||||||
|       } catch (err) { |       } catch (err) { | ||||||
|         logger.error('请求异常:', url, Date.now() - startTime + 'ms', err); |         logger.error('请求异常:', url, Date.now() - startTime + 'ms', err); | ||||||
|         ctx.status = 200; |         ctx.status = 200; | ||||||
|         ctx.body = Result.error(err.code != null ? err.code : 1, err.message); |         if (err.code == null || typeof err.code !== 'number') { | ||||||
|  |           err.code = 1; | ||||||
|  |         } | ||||||
|  |         ctx.body = Result.error(err.code, err.message); | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -1,9 +1,10 @@ | ||||||
| import { Rule,RuleType } from '@midwayjs/validate'; | import { Rule, RuleType } from '@midwayjs/validate'; | ||||||
| import { ALL, Inject } from '@midwayjs/decorator'; | import { ALL, Inject } from '@midwayjs/decorator'; | ||||||
| import { Body } from '@midwayjs/decorator'; | import { Body } from '@midwayjs/decorator'; | ||||||
| import { Controller, Post, Provide } from '@midwayjs/decorator'; | import { Controller, Post, Provide } from '@midwayjs/decorator'; | ||||||
| import { BaseController } from '../../../basic/base-controller'; | import { BaseController } from '../../../basic/base-controller'; | ||||||
| import { CodeService } from '../service/code-service'; | import { CodeService } from '../service/code-service'; | ||||||
|  | import { EmailService } from '../service/email-service'; | ||||||
| export class SmsCodeReq { | export class SmsCodeReq { | ||||||
|   @Rule(RuleType.number().required()) |   @Rule(RuleType.number().required()) | ||||||
|   phoneCode: number; |   phoneCode: number; | ||||||
|  | @ -18,22 +19,17 @@ export class SmsCodeReq { | ||||||
|   imgCode: string; |   imgCode: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // const enumsMap = {};
 |  | ||||||
| // glob('src/modules/**/enums/*.ts', {}, (err, matches) => {
 |  | ||||||
| //   console.log('matched', matches);
 |  | ||||||
| //   for (const filePath of matches) {
 |  | ||||||
| //     const module = require('/' + filePath);
 |  | ||||||
| //     console.log('modules', module);
 |  | ||||||
| //   }
 |  | ||||||
| // });
 |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  */ |  */ | ||||||
| @Provide() | @Provide() | ||||||
| @Controller('/api/basic') | @Controller('/api/basic/code') | ||||||
| export class BasicController extends BaseController { | export class BasicController extends BaseController { | ||||||
|   @Inject() |   @Inject() | ||||||
|   codeService: CodeService; |   codeService: CodeService; | ||||||
|  | 
 | ||||||
|  |   @Inject() | ||||||
|  |   emailService: EmailService; | ||||||
|  | 
 | ||||||
|   @Post('/sendSmsCode') |   @Post('/sendSmsCode') | ||||||
|   public sendSmsCode( |   public sendSmsCode( | ||||||
|     @Body(ALL) |     @Body(ALL) | ||||||
|  | @ -53,4 +49,3 @@ export class BasicController extends BaseController { | ||||||
|     return this.ok(captcha.data); |     return this.ok(captcha.data); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 |  | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | import { Body, Controller, Inject, Post, Provide } from '@midwayjs/decorator'; | ||||||
|  | import { BaseController } from '../../../basic/base-controller'; | ||||||
|  | import { EmailService } from '../service/email-service'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  */ | ||||||
|  | @Provide() | ||||||
|  | @Controller('/api/basic/email') | ||||||
|  | export class EmailController extends BaseController { | ||||||
|  |   @Inject() | ||||||
|  |   emailService: EmailService; | ||||||
|  | 
 | ||||||
|  |   @Post('/test') | ||||||
|  |   public async test( | ||||||
|  |     @Body('receiver') | ||||||
|  |     receiver | ||||||
|  |   ) { | ||||||
|  |     const userId = super.getUserId(); | ||||||
|  |     await this.emailService.test(userId, receiver); | ||||||
|  |     return this.ok({}); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,60 @@ | ||||||
|  | import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/decorator'; | ||||||
|  | import type { EmailSend } from '@certd/pipeline'; | ||||||
|  | import { IEmailService } from '@certd/pipeline'; | ||||||
|  | import nodemailer from 'nodemailer'; | ||||||
|  | import { SettingsService } from '../../system/service/settings-service'; | ||||||
|  | import type SMTPConnection from 'nodemailer/lib/smtp-connection'; | ||||||
|  | 
 | ||||||
|  | export type EmailConfig = { | ||||||
|  |   host: string; | ||||||
|  |   port: number; | ||||||
|  |   auth: { | ||||||
|  |     user: string; | ||||||
|  |     pass: string; | ||||||
|  |   }; | ||||||
|  |   secure: boolean; // use TLS
 | ||||||
|  |   tls: { | ||||||
|  |     // do not fail on invalid certs
 | ||||||
|  |     rejectUnauthorized: boolean; | ||||||
|  |   }; | ||||||
|  |   sender: string; | ||||||
|  | } & SMTPConnection.Options; | ||||||
|  | @Provide() | ||||||
|  | @Scope(ScopeEnum.Singleton) | ||||||
|  | export class EmailService implements IEmailService { | ||||||
|  |   @Inject() | ||||||
|  |   settingsService: SettingsService; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    */ | ||||||
|  |   async send(email: EmailSend) { | ||||||
|  |     console.log('sendEmail', email); | ||||||
|  | 
 | ||||||
|  |     const emailConfigEntity = await this.settingsService.getByKey( | ||||||
|  |       'email', | ||||||
|  |       email.userId | ||||||
|  |     ); | ||||||
|  |     if (emailConfigEntity == null || !emailConfigEntity.setting) { | ||||||
|  |       throw new Error('email settings 未设置'); | ||||||
|  |     } | ||||||
|  |     const emailConfig = JSON.parse(emailConfigEntity.setting) as EmailConfig; | ||||||
|  |     const transporter = nodemailer.createTransport(emailConfig); | ||||||
|  |     const mailOptions = { | ||||||
|  |       from: emailConfig.sender, | ||||||
|  |       to: email.receivers.join(', '), // list of receivers
 | ||||||
|  |       subject: email.subject, | ||||||
|  |       text: email.content, | ||||||
|  |     }; | ||||||
|  |     await transporter.sendMail(mailOptions); | ||||||
|  |     console.log('sendEmail success', email); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async test(userId: number, receiver: string) { | ||||||
|  |     await this.send({ | ||||||
|  |       userId, | ||||||
|  |       receivers: [receiver], | ||||||
|  |       subject: '测试邮件,from certd', | ||||||
|  |       content: '测试邮件,from certd', | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/decorator"; | import { Autoload, Init, Inject, Scope, ScopeEnum } from '@midwayjs/decorator'; | ||||||
| import { PipelineService } from '../service/pipeline-service'; | import { PipelineService } from '../service/pipeline-service'; | ||||||
| import { logger } from '../../../utils/logger'; | import { logger } from '../../../utils/logger'; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ import { HistoryEntity } from '../entity/history'; | ||||||
| import { HistoryLogEntity } from '../entity/history-log'; | import { HistoryLogEntity } from '../entity/history-log'; | ||||||
| import { HistoryLogService } from './history-log-service'; | import { HistoryLogService } from './history-log-service'; | ||||||
| import { logger } from '../../../utils/logger'; | import { logger } from '../../../utils/logger'; | ||||||
|  | import { EmailService } from '../../basic/service/email-service'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * 证书申请 |  * 证书申请 | ||||||
|  | @ -23,7 +24,8 @@ import { logger } from '../../../utils/logger'; | ||||||
| export class PipelineService extends BaseService<PipelineEntity> { | export class PipelineService extends BaseService<PipelineEntity> { | ||||||
|   @InjectEntityModel(PipelineEntity) |   @InjectEntityModel(PipelineEntity) | ||||||
|   repository: Repository<PipelineEntity>; |   repository: Repository<PipelineEntity>; | ||||||
| 
 |   @Inject() | ||||||
|  |   emailService: EmailService; | ||||||
|   @Inject() |   @Inject() | ||||||
|   accessService: AccessService; |   accessService: AccessService; | ||||||
|   @Inject() |   @Inject() | ||||||
|  | @ -191,6 +193,7 @@ export class PipelineService extends BaseService<PipelineEntity> { | ||||||
|       onChanged, |       onChanged, | ||||||
|       accessService: this.accessService, |       accessService: this.accessService, | ||||||
|       storage: new DbStorage(userId, this.storageService), |       storage: new DbStorage(userId, this.storageService), | ||||||
|  |       emailService: this.emailService, | ||||||
|     }); |     }); | ||||||
|     try { |     try { | ||||||
|       await executor.init(); |       await executor.init(); | ||||||
|  |  | ||||||
|  | @ -1,6 +1,15 @@ | ||||||
| import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/decorator"; | import { | ||||||
| import { CrudController } from "../../../basic/crud-controller"; |   ALL, | ||||||
| import { SettingsService } from "../service/settings-service"; |   Body, | ||||||
|  |   Controller, | ||||||
|  |   Inject, | ||||||
|  |   Post, | ||||||
|  |   Provide, | ||||||
|  |   Query, | ||||||
|  | } from '@midwayjs/decorator'; | ||||||
|  | import { CrudController } from '../../../basic/crud-controller'; | ||||||
|  | import { SettingsService } from '../service/settings-service'; | ||||||
|  | import { SettingsEntity } from '../entity/settings'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  */ |  */ | ||||||
|  | @ -50,4 +59,18 @@ export class SettingsController extends CrudController<SettingsService> { | ||||||
|     return super.delete(id); |     return super.delete(id); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   @Post('/save') | ||||||
|  |   async save(@Body(ALL) bean: SettingsEntity) { | ||||||
|  |     await this.service.checkUserId(bean.key, this.ctx.user.id, 'userId', 'key'); | ||||||
|  |     bean.userId = this.ctx.user.id; | ||||||
|  |     await this.service.save(bean); | ||||||
|  |     return this.ok({}); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Post('/get') | ||||||
|  |   async get(@Query('key') key: string) { | ||||||
|  |     await this.service.checkUserId(key, this.ctx.user.id, 'userId', 'key'); | ||||||
|  |     const entity = await this.service.getByKey(key, this.ctx.user.id); | ||||||
|  |     return this.ok(entity); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -8,8 +8,10 @@ export class SettingsEntity { | ||||||
|   id: number; |   id: number; | ||||||
|   @Column({ name: 'user_id', comment: '用户id' }) |   @Column({ name: 'user_id', comment: '用户id' }) | ||||||
|   userId: number; |   userId: number; | ||||||
|  |   @Column({ comment: 'key', length: 100 }) | ||||||
|  |   key: string; | ||||||
|   @Column({ comment: '名称', length: 100 }) |   @Column({ comment: '名称', length: 100 }) | ||||||
|   name: string; |   title: string; | ||||||
|   @Column({ name: 'setting', comment: '设置', length: 1024, nullable: true }) |   @Column({ name: 'setting', comment: '设置', length: 1024, nullable: true }) | ||||||
|   setting: string; |   setting: string; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,17 +1,15 @@ | ||||||
| import { Provide, Scope, ScopeEnum } from "@midwayjs/decorator"; | import { Provide, Scope, ScopeEnum } from '@midwayjs/decorator'; | ||||||
| import { InjectEntityModel } from "@midwayjs/typeorm"; | import { InjectEntityModel } from '@midwayjs/typeorm'; | ||||||
| import { Repository } from "typeorm"; | import { Repository } from 'typeorm'; | ||||||
| import { BaseService } from "../../../basic/base-service"; | import { BaseService } from '../../../basic/base-service'; | ||||||
| import { SettingsEntity } from "../entity/settings"; | import { SettingsEntity } from '../entity/settings'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * 授权 |  * 授权 | ||||||
|  */ |  */ | ||||||
| @Provide() | @Provide() | ||||||
| @Scope(ScopeEnum.Singleton) | @Scope(ScopeEnum.Singleton) | ||||||
| export class SettingsService | export class SettingsService extends BaseService<SettingsEntity> { | ||||||
|   extends BaseService<SettingsEntity> |  | ||||||
| { |  | ||||||
|   @InjectEntityModel(SettingsEntity) |   @InjectEntityModel(SettingsEntity) | ||||||
|   repository: Repository<SettingsEntity>; |   repository: Repository<SettingsEntity>; | ||||||
| 
 | 
 | ||||||
|  | @ -19,8 +17,11 @@ export class SettingsService | ||||||
|     return this.repository; |     return this.repository; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async getById(id: any): Promise<any> { |   async getById(id: any): Promise<SettingsEntity | null> { | ||||||
|     const entity = await this.info(id); |     const entity = await this.info(id); | ||||||
|  |     if (!entity) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|     // const access = accessRegistry.get(entity.type);
 |     // const access = accessRegistry.get(entity.type);
 | ||||||
|     const setting = JSON.parse(entity.setting); |     const setting = JSON.parse(entity.setting); | ||||||
|     return { |     return { | ||||||
|  | @ -29,5 +30,38 @@ export class SettingsService | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async getByKey(key: string, userId: number): Promise<SettingsEntity | null> { | ||||||
|  |     if (!key || !userId) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     return await this.repository.findOne({ | ||||||
|  |       where: { | ||||||
|  |         key, | ||||||
|  |         userId, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|  |   async getSettingByKey(key: string, userId: number): Promise<any | null> { | ||||||
|  |     const entity = await this.getByKey(key, userId); | ||||||
|  |     if (!entity) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     return JSON.parse(entity.setting); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async save(bean: SettingsEntity) { | ||||||
|  |     const entity = await this.repository.findOne({ | ||||||
|  |       where: { | ||||||
|  |         key: bean.key, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |     if (entity) { | ||||||
|  |       entity.setting = bean.setting; | ||||||
|  |       await this.repository.save(entity); | ||||||
|  |     } else { | ||||||
|  |       bean.title = bean.key; | ||||||
|  |       await this.repository.save(bean); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 xiaojunnuo
						xiaojunnuo