mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-01 23:10:26 +08:00
feat: message notify
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
|
||||
## 前言
|
||||
|
||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)。数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据操作、数据同步、数据迁移。redis(单机 哨兵 集群)。mongo 等集工单流程审批于一体的统一管理操作平台。**
|
||||
Web版 **统一管理操作平台**,集成了对Linux系统的全面操作支持(包括终端管理[终端回放、命令过滤]、文件管理、脚本执行、进程监控及计划任务设置),同时提供了多种数据库(如 MySQL、PostgreSQL、Oracle、SQL Server、达梦、高斯、SQLite 等)的数据操作、数据同步与数据迁移功能。此外,还支持 Redis(单机、哨兵、集群模式)以及 MongoDB 的操作管理,并结合工单流程审批功能,为企业提供一站式的运维与管理解决方案。
|
||||
|
||||
## 开发语言与主要框架
|
||||
|
||||
@@ -107,3 +107,10 @@ http://go.mayfly.run
|
||||
## 💌 支持作者
|
||||
|
||||
如果觉得项目不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/dromara/mayfly-go">Github</a> 或者 <a target="_blank" href="https://gitee.com/dromara/mayfly-go">Gitee</a> 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持。
|
||||
|
||||
> 喝杯咖啡 ☕️ 或者来杯奶茶 🧋,让作者更有精神,写出更棒的代码!这将是对作者最大的鼓励和支持!
|
||||
|
||||
<img class="no-margin" src="https://foruda.gitee.com/images/1744113367791412282/36a3c23b_1240250.png" alt="微信打赏" width="200" height="200">
|
||||
|
||||
> **特别感谢:**
|
||||
> 赞助金额达 199 元以上,加微信(wx-error),我将邀请您进入付费交流群,享受更快、更优先的技术支持!
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
## Preface
|
||||
|
||||
Browser-based management platform. **linux(Terminal [terminal playback, command filtering], file, script, process, cronjob), database (mysql, postgres, oracle, sqlserver, Dameng, gauss, sqlite) data operation, data synchronization, data migration, redis(standlone, sentinel, cluster), mongo and other unified management and operation platforms that integrate work order process approval.**
|
||||
Web-based **Unified Management and Operation Platform**, integrating comprehensive operation support for Linux systems (including terminal management [terminal playback, command filtering], file management, script execution, process monitoring, and cronjob settings). It also provides data operation, data synchronization, and data migration for multiple databases (such as MySQL, PostgreSQL, Oracle, SQL Server, Dameng, Gauss, SQLite, etc.). Additionally, it supports Redis operations (standalone, sentinel, and cluster modes) and MongoDB management, combined with work order process approval functionality to offer enterprises an all-in-one solution for operations and management.
|
||||
|
||||
## Development languages and major frameworks
|
||||
|
||||
|
||||
@@ -14,18 +14,18 @@ services:
|
||||
restart: always
|
||||
|
||||
server:
|
||||
image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:v1.8.5
|
||||
image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: mayfly-go-server
|
||||
ports:
|
||||
- "8888:8888"
|
||||
- "18888:18888"
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
WAIT_HOSTS: mysql:3306
|
||||
volumes:
|
||||
- ./server/config.yml.example:/mayfly/config.yml
|
||||
- ./server/config.yml:/mayfly/config.yml
|
||||
depends_on:
|
||||
- mysql
|
||||
restart: always
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-search": "^0.15.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
@@ -22,48 +22,46 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.9.5",
|
||||
"element-plus": "^2.9.7",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-sql-languages": "^0.12.2",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.1",
|
||||
"pinia": "^3.0.2",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.6",
|
||||
"splitpanes": "^3.1.8",
|
||||
"splitpanes": "^4.0.3",
|
||||
"sql-formatter": "^15.4.10",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.1",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-router": "^4.5.0",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/compiler-sfc": "^3.5.13",
|
||||
"code-inspector-plugin": "^0.4.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^9.31.0",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.85.1",
|
||||
"sass": "^1.86.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.1",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vue-eslint-parser": "^9.4.3"
|
||||
"vue-eslint-parser": "^10.1.3"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
@@ -15,7 +15,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.9.3',
|
||||
version: 'v1.9.4',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -23,7 +23,7 @@ export const Rules = {
|
||||
},
|
||||
|
||||
accountUsername: {
|
||||
pattern: /^[a-zA-Z0-9_]{5,16}$/g,
|
||||
pattern: /^[a-zA-Z0-9_.@:-]{5,16}$/,
|
||||
message: i18n.global.t('system.account.usernamePatternErrMsg'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
|
||||
7
frontend/src/components/df/design.vue
Normal file
7
frontend/src/components/df/design.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<FormDesign ref="makingForm" upload preview generate-code generate-json clearable> </FormDesign>
|
||||
<!-- <dev></dev> -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
// import { FormDesign } from 'mayfly-lc';
|
||||
</script>
|
||||
@@ -7,7 +7,7 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
enums: {
|
||||
type: Object, // 需要为EnumValue类型
|
||||
type: Object || Array, // 需要为EnumValue类型
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ export class TableColumn {
|
||||
/**
|
||||
* 插槽名,
|
||||
*/
|
||||
slotName: string = '';
|
||||
private slotName: string = '';
|
||||
|
||||
showOverflowTooltip: boolean = true;
|
||||
|
||||
@@ -73,12 +73,12 @@ export class TableColumn {
|
||||
/**
|
||||
* 是否显示该列
|
||||
*/
|
||||
show: boolean = true;
|
||||
private show: boolean = true;
|
||||
|
||||
/**
|
||||
* 是否展示美化按钮(主要用于美化json文本等)
|
||||
*/
|
||||
isBeautify: boolean = false;
|
||||
private isBeautify: boolean = false;
|
||||
|
||||
constructor(prop: string, label: string) {
|
||||
this.prop = prop;
|
||||
|
||||
@@ -108,5 +108,7 @@ const setIconSvgInsStyle = computed(() => {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
height: 100%; /* 确保高度与父元素一致 */
|
||||
line-height: 1; /* 确保行高与高度一致 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -73,8 +73,7 @@ import { TerminalExpose } from '@/components/terminal-rdp/index';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||
import { exitFullscreen, launchIntoFullscreen, unWatchFullscreenChange, watchFullscreenChange } from '@/components/terminal-rdp/guac/screen';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { debounce } from 'lodash';
|
||||
import { useDebounceFn, useEventListener } from '@vueuse/core';
|
||||
import { ClientState, TunnelState } from '@/components/terminal-rdp/guac/states';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
@@ -191,7 +190,7 @@ const installClipboard = () => {
|
||||
|
||||
const installResize = () => {
|
||||
// 在resize事件结束后300毫秒执行
|
||||
useEventListener('resize', debounce(resize, 300));
|
||||
useEventListener('resize', useDebounceFn(resize, 300));
|
||||
};
|
||||
|
||||
const installDisplay = () => {
|
||||
|
||||
@@ -17,9 +17,8 @@ import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { ref, nextTick, reactive, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import TerminalSearch from './TerminalSearch.vue';
|
||||
import { debounce } from 'lodash';
|
||||
import { TerminalStatus } from './common';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { useDebounceFn, useEventListener } from '@vueuse/core';
|
||||
import themes from './themes';
|
||||
import { TrzszFilter } from 'trzsz';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -129,7 +128,7 @@ async function initTerm() {
|
||||
term.loadAddon(fitAddon);
|
||||
fitTerminal();
|
||||
// 注册窗口大小监听器
|
||||
useEventListener('resize', debounce(fitTerminal, 400));
|
||||
useEventListener('resize', useDebounceFn(fitTerminal, 400));
|
||||
|
||||
initSocket();
|
||||
// 注册其他插件
|
||||
|
||||
@@ -17,6 +17,8 @@ export default {
|
||||
remark: 'Remark',
|
||||
status: 'Status',
|
||||
username: 'Username',
|
||||
mobile: 'Mobile',
|
||||
email: 'Email',
|
||||
role: 'Role',
|
||||
msg: 'Message',
|
||||
type: 'Type',
|
||||
@@ -281,6 +283,17 @@ export default {
|
||||
flowProcDefSave: 'Save Process Define',
|
||||
flowProcDefDelete: 'Delete Process Define',
|
||||
|
||||
msgManage: 'Message',
|
||||
channel: 'Message Channel',
|
||||
msgChannelBase: 'Base Permission',
|
||||
saveMsgChannel: 'Save Message Channel',
|
||||
delMsgChannel: 'Delete Message Channel',
|
||||
msgTmpl: 'Message Template',
|
||||
msgTmplBase: 'Base Permission',
|
||||
saveMsgTmpl: 'Save Message Template',
|
||||
delMsgTmpl: 'Delete Message Template',
|
||||
sendMsg: 'Send Message',
|
||||
|
||||
system: 'System',
|
||||
menuPermission: 'Menu & Permission',
|
||||
menuPermissionBase: 'Base Permission',
|
||||
|
||||
@@ -89,5 +89,6 @@ export default {
|
||||
taskName: 'Task Name',
|
||||
taskBeginTime: 'Begin Time',
|
||||
flowAudit: 'Approval Process',
|
||||
notify: 'Notification',
|
||||
},
|
||||
};
|
||||
|
||||
21
frontend/src/i18n/en/msg.ts
Normal file
21
frontend/src/i18n/en/msg.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
msg: {
|
||||
name: 'Name',
|
||||
email: 'Email',
|
||||
dingBot: 'DingTalk Bot',
|
||||
qywxBot: 'WeChat Work Bot',
|
||||
feishuBot: 'Feishu Bot',
|
||||
msgChannel: 'Message Channel',
|
||||
smtpAccount: 'SMTP Account',
|
||||
smtpPassword: 'SMTP Password',
|
||||
msgTmpl: 'Message Template',
|
||||
relateChannel: 'Related Channel',
|
||||
title: 'Title',
|
||||
tmpl: 'Template',
|
||||
send: 'Send',
|
||||
sendMsg: 'Send Message',
|
||||
selectTmplPlaceholder: 'Select message template, support fuzzy search by number',
|
||||
msgTmplTooltip:
|
||||
'Message template supports variable replacement, the variable format is {{.variable}}, the following are common built-in variables <br/>{{.receiver}}: used to @ specify the recipient',
|
||||
},
|
||||
};
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
lastLoginTime: 'Last Login Time',
|
||||
usernamePlacholder: '5-16 uppercase letters, numbers, -.: characters',
|
||||
random: 'Random',
|
||||
usernamePatternErrMsg: 'Only 5-16 uppercase letters, numbers, and -.: characters are allowed',
|
||||
usernamePatternErrMsg: 'Only 5-16 uppercase letters, numbers, and -@.: characters are allowed',
|
||||
accountSearchPlaceholder: 'Enter account fuzzy search and select',
|
||||
accountInfo: 'Account Information',
|
||||
allocateRoleTitle: 'Allocate the `{name}` role',
|
||||
|
||||
@@ -17,6 +17,8 @@ export default {
|
||||
remark: '备注',
|
||||
status: '状态',
|
||||
username: '用户名',
|
||||
mobile: '手机号',
|
||||
email: '邮箱',
|
||||
role: '角色',
|
||||
msg: '消息',
|
||||
type: '类型',
|
||||
@@ -291,6 +293,17 @@ export default {
|
||||
flowProcDefSave: '保存流程定义',
|
||||
flowProcDefDelete: '删除流程定义',
|
||||
|
||||
msgManage: '消息管理',
|
||||
channel: '消息渠道',
|
||||
msgChannelBase: '基础权限',
|
||||
saveMsgChannel: '保存消息渠道',
|
||||
delMsgChannel: '删除消息渠道',
|
||||
msgTmpl: '消息模板',
|
||||
msgTmplBase: '基础权限',
|
||||
saveMsgTmpl: '保存消息模板',
|
||||
delMsgTmpl: '删除消息模板',
|
||||
sendMsg: '发送消息',
|
||||
|
||||
system: '系统管理',
|
||||
menuPermission: '菜单权限',
|
||||
menuPermissionBase: '基本权限',
|
||||
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
startProcess: '发起流程',
|
||||
cancelProcessConfirm: '确认取消该流程?',
|
||||
bizType: '业务类型',
|
||||
bizKey: '业务Key',
|
||||
bizKey: '业务编号',
|
||||
initiator: '发起人',
|
||||
procdefName: '流程名',
|
||||
bizStatus: '业务状态',
|
||||
@@ -89,5 +89,6 @@ export default {
|
||||
taskName: '当前节点',
|
||||
taskBeginTime: '开始时间',
|
||||
flowAudit: '流程审批',
|
||||
notify: '通知',
|
||||
},
|
||||
};
|
||||
|
||||
20
frontend/src/i18n/zh-cn/msg.ts
Normal file
20
frontend/src/i18n/zh-cn/msg.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
msg: {
|
||||
name: '名称',
|
||||
email: '邮箱',
|
||||
dingBot: '钉钉机器人',
|
||||
qywxBot: '企微机器人',
|
||||
feishuBot: '飞书机器人',
|
||||
msgChannel: '消息渠道',
|
||||
smtpAccount: 'SMTP账号',
|
||||
smtpPassword: 'SMTP密码',
|
||||
msgTmpl: '消息模板',
|
||||
relateChannel: '关联渠道',
|
||||
title: '标题',
|
||||
tmpl: '模板',
|
||||
send: '发送',
|
||||
sendMsg: '发送消息',
|
||||
selectTmplPlaceholder: '选择消息模板,支持编号模糊搜索',
|
||||
msgTmplTooltip: '消息模板支持变量替换,变量格式为{{.变量名}},以下为通用内置变量 <br/>{{.receiver}}:用于@指定接收人',
|
||||
},
|
||||
};
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
success: '成功',
|
||||
menuCodeTips: `菜单类型则为访问路径(若菜单路径不以'/'开头则访问地址会自动拼接父菜单路径)、否则为资源唯一编码`,
|
||||
menuCodePlaceholder: `菜单不以'/'开头则自动拼接父菜单路径`,
|
||||
routerNameTips: '与vue的组件名一致才可使组件缓存生效,如ResourceLis',
|
||||
routerNameTips: '与vue的组件名一致才可使组件缓存生效,如ResourceList',
|
||||
componentPathTips: '访问的组件路径,如:`system/resource/ResourceList`,默认在`views`目录下',
|
||||
isCacheTips: '选择是则会被`keep-alive`缓存(重新进入页面不会刷新页面及重新请求数据),需要路由名与vue的组件名一致',
|
||||
isHideTips: '选择隐藏则路由将不会出现在菜单栏中,但仍然可以访问。禁用则不可访问与操作',
|
||||
@@ -47,7 +47,7 @@ export default {
|
||||
deleteAccountConfirm: '确定删除【{name}】的账号?',
|
||||
usernamePlacholder: '5-16位大小写字母、数字、_-.:',
|
||||
random: '随机',
|
||||
usernamePatternErrMsg: '只允许输入5-16位大小写字母、数字、_-.:',
|
||||
usernamePatternErrMsg: '只允许输入5-16位大小写字母、数字、_-@.:',
|
||||
accountSearchPlaceholder: '输入账号模糊搜索并选择',
|
||||
accountInfo: '账号信息',
|
||||
allocateRoleTitle: '分配 `{name}` 的角色',
|
||||
@@ -60,6 +60,9 @@ export default {
|
||||
userMenuTitle: '`{name}` 的菜单&权限',
|
||||
statusEnable: '启用',
|
||||
statusDisable: '禁用',
|
||||
qywxUserId: '企微UserId',
|
||||
dingUserId: '钉钉UserId',
|
||||
feishuUserId: '飞书UserId',
|
||||
},
|
||||
role: {
|
||||
permissionDetail: '权限详情',
|
||||
|
||||
@@ -12,7 +12,7 @@ export const URL_404: string = '/404';
|
||||
export const LAYOUT_ROUTE_NAME: string = 'layout';
|
||||
|
||||
// 路由白名单地址(本地存在的路由 staticRouter.ts 中)
|
||||
export const ROUTER_WHITE_LIST: string[] = [URL_404, URL_401, '/oauth2/callback'];
|
||||
export const ROUTER_WHITE_LIST: string[] = [URL_404, URL_401, '/oauth2/callback', '/form-design'];
|
||||
|
||||
// 静态路由
|
||||
export const staticRoutes: Array<RouteRecordRaw> = [
|
||||
@@ -65,6 +65,17 @@ export const staticRoutes: Array<RouteRecordRaw> = [
|
||||
titleRename: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/form-design',
|
||||
name: 'formDesign',
|
||||
component: () => import('@/components/df/design.vue'),
|
||||
meta: {
|
||||
// 将路径 'xxx?name=名字' 里的name字段值替换到title里
|
||||
title: 'terminal | {name}',
|
||||
// 是否根据query对标题名进行参数替换,即最终显示为‘终端_机器名’
|
||||
titleRename: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 错误页面路由
|
||||
|
||||
@@ -4,121 +4,95 @@
|
||||
.slide-right-leave-active,
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active {
|
||||
will-change: transform;
|
||||
transition: all 0.3s ease;
|
||||
will-change: transform;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
// slide-right
|
||||
.slide-right-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
.slide-right-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
// slide-left
|
||||
.slide-left-enter-from {
|
||||
@extend .slide-right-leave-to;
|
||||
@extend .slide-right-leave-to;
|
||||
}
|
||||
|
||||
.slide-left-leave-to {
|
||||
@extend .slide-right-enter-from;
|
||||
@extend .slide-right-enter-from;
|
||||
}
|
||||
|
||||
// opacitys
|
||||
.opacitys-enter-active,
|
||||
.opacitys-leave-active {
|
||||
will-change: transform;
|
||||
transition: all 0.3s ease;
|
||||
will-change: transform;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.opacitys-enter-from,
|
||||
.opacitys-leave-to {
|
||||
opacity: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Breadcrumb 面包屑过渡动画
|
||||
------------------------------- */
|
||||
.breadcrumb-enter-active,
|
||||
.breadcrumb-leave-active {
|
||||
transition: all 0.3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.breadcrumb-enter-from,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.breadcrumb-leave-active {
|
||||
position: absolute;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* logo 过渡动画
|
||||
------------------------------- */
|
||||
@keyframes logoAnimation {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
80% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 404、401 过渡动画
|
||||
------------------------------- */
|
||||
@keyframes error-num {
|
||||
0% {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes error-img {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
0% {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录页动画
|
||||
------------------------------- */
|
||||
@keyframes loginLeft {
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes loginTop {
|
||||
0% {
|
||||
top: -100%;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes loginRight {
|
||||
0% {
|
||||
right: -100%;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
right: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes loginBottom {
|
||||
0% {
|
||||
bottom: -100%;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
bottom: 100%;
|
||||
}
|
||||
@keyframes error-img {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,10 @@
|
||||
<el-input v-model.trim="form.remark" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="msgTmplId" :label="$t('flow.notify')">
|
||||
<MsgTmplSelect v-model="form.msgTmplId" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item ref="tagSelectRef" prop="codePaths" :label="$t('tag.relateTag')">
|
||||
<tag-tree-check height="300px" v-model="form.codePaths" :tag-type="[TagResourceTypePath.Db, TagResourceTypeEnum.Redis.value]" />
|
||||
</el-form-item>
|
||||
@@ -95,6 +99,7 @@ import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
|
||||
import { Rules } from '@/common/rule';
|
||||
import MsgTmplSelect from '../msg/components/MsgTmplSelect.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -129,6 +134,7 @@ const state = reactive({
|
||||
status: null,
|
||||
condition: '',
|
||||
remark: null,
|
||||
msgTmplId: null,
|
||||
// 流程的审批节点任务
|
||||
tasks: '',
|
||||
codePaths: [],
|
||||
@@ -140,9 +146,9 @@ const { form, tasks } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveFlowDefExec } = procdefApi.save.useApi(form);
|
||||
|
||||
watch(props, (newValue: any) => {
|
||||
watch(props, async (newValue: any) => {
|
||||
if (newValue.data) {
|
||||
state.form = { ...newValue.data };
|
||||
state.form = await procdefApi.detail.request({ id: newValue.data.id });
|
||||
state.form.codePaths = newValue.data.tags?.map((tag: any) => tag.codePath);
|
||||
const tasks = JSON.parse(state.form.tasks);
|
||||
tasks.forEach((t: any) => {
|
||||
|
||||
@@ -47,6 +47,7 @@ const { t } = useI18n();
|
||||
|
||||
const searchItems = [
|
||||
SearchItem.select('status', 'common.status').withEnum(ProcinstTaskStatus),
|
||||
SearchItem.input('bizKey', 'flow.bizKey'),
|
||||
SearchItem.select('bizType', 'flow.bizType').withEnum(FlowBizType),
|
||||
];
|
||||
const columns = [
|
||||
|
||||
@@ -2,6 +2,7 @@ import Api from '@/common/Api';
|
||||
|
||||
export const procdefApi = {
|
||||
list: Api.newGet('/flow/procdefs'),
|
||||
detail: Api.newGet('/flow/procdefs/detail/{id}'),
|
||||
getByResource: Api.newGet('/flow/procdefs/{resourceType}/{resourceCode}'),
|
||||
save: Api.newPost('/flow/procdefs'),
|
||||
del: Api.newDelete('/flow/procdefs/{id}'),
|
||||
|
||||
@@ -238,8 +238,6 @@ const oauth2Login = () => {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, transparent, var(--el-color-primary));
|
||||
animation: loginLeft 3s linear infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@@ -248,9 +246,6 @@ const oauth2Login = () => {
|
||||
right: 2px;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, transparent, var(--el-color-primary));
|
||||
animation: loginTop 3s linear infinite;
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,9 +256,6 @@ const oauth2Login = () => {
|
||||
right: -100%;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: linear-gradient(270deg, transparent, var(--el-color-primary));
|
||||
animation: loginRight 3s linear infinite;
|
||||
animation-delay: 1.4s;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@@ -272,9 +264,6 @@ const oauth2Login = () => {
|
||||
left: 0px;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background: linear-gradient(360deg, transparent, var(--el-color-primary));
|
||||
animation: loginBottom 3s linear infinite;
|
||||
animation-delay: 2.1s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
15
frontend/src/views/msg/api.ts
Normal file
15
frontend/src/views/msg/api.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Api from '@/common/Api';
|
||||
|
||||
export const channelApi = {
|
||||
list: Api.newGet('/msg/channels'),
|
||||
save: Api.newPost('/msg/channels'),
|
||||
del: Api.newDelete('/msg/channels'),
|
||||
};
|
||||
|
||||
export const tmplApi = {
|
||||
list: Api.newGet('/msg/tmpls'),
|
||||
relateChannels: Api.newGet('/msg/tmpls/{id}/channels'),
|
||||
save: Api.newPost('/msg/tmpls'),
|
||||
del: Api.newDelete('/msg/tmpls'),
|
||||
sendMsg: Api.newPost('/msg/tmpls/{code}/send'),
|
||||
};
|
||||
12
frontend/src/views/msg/channel/ChannelDing.vue
Executable file
12
frontend/src/views/msg/channel/ChannelDing.vue
Executable file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<el-form-item prop="extra.secret" :rules="[Rules.requiredInput('secret')]" :label="$t('Secret')">
|
||||
<el-input v-model.trim="extra.secret" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Rules } from '@/common/rule';
|
||||
|
||||
const extra = defineModel<any>('extra', { default: {} });
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
138
frontend/src/views/msg/channel/ChannelEdit.vue
Executable file
138
frontend/src/views/msg/channel/ChannelEdit.vue
Executable file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="formData" ref="formRef" :rules="rules" label-position="top" label-width="auto">
|
||||
<el-form-item prop="name" :label="$t('msg.name')">
|
||||
<el-input v-model.trim="formData.name" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="status" :label="$t('common.status')">
|
||||
<EnumSelect :enums="ChannelStatusEnum" v-model="formData.status" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="remark" :label="$t('common.remark')">
|
||||
<el-input v-model.trim="formData.remark" auto-complete="off" type="textarea" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="type" :label="$t('common.type')">
|
||||
<EnumSelect
|
||||
:enums="ChannelTypeEnum"
|
||||
v-model="formData.type"
|
||||
@change="
|
||||
() => {
|
||||
formData.extra = {};
|
||||
}
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="url" label="URL">
|
||||
<el-input v-model.trim="formData.url" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<component v-if="channelTypeComp" :is="channelTypeComp" v-model:extra="formData.extra" />
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watchEffect, useTemplateRef, shallowReactive, computed } from 'vue';
|
||||
import { channelApi } from '../api';
|
||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||
import { ChannelStatusEnum, ChannelTypeEnum } from '../enums';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import ChannelEmail from './ChannelEmail.vue';
|
||||
import ChannelDing from './ChannelDing.vue';
|
||||
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: [Boolean, Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const channels: any = shallowReactive({
|
||||
ChannelEmail,
|
||||
ChannelDing,
|
||||
});
|
||||
|
||||
const channelTypeComp = computed(() => {
|
||||
return channels[EnumValue.getEnumByValue(ChannelTypeEnum, state.form.type)?.extra?.component];
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['cancel', 'success']);
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const formRef: any = useTemplateRef('formRef');
|
||||
|
||||
const rules = {
|
||||
name: [Rules.requiredInput('msg.name')],
|
||||
type: [Rules.requiredSelect('common.type')],
|
||||
url: [Rules.requiredInput('URL')],
|
||||
};
|
||||
|
||||
const defaultForm = () => {
|
||||
return {
|
||||
id: null,
|
||||
name: null,
|
||||
type: null,
|
||||
url: '',
|
||||
status: ChannelStatusEnum.Enable.value,
|
||||
remark: '',
|
||||
extra: {},
|
||||
};
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
edit: false,
|
||||
form: defaultForm(),
|
||||
});
|
||||
|
||||
const { form: formData } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveFormExec } = channelApi.save.useApi(formData);
|
||||
|
||||
watchEffect(() => {
|
||||
const form: any = props.form;
|
||||
if (form) {
|
||||
state.form = { ...form };
|
||||
state.edit = true;
|
||||
} else {
|
||||
state.edit = false;
|
||||
state.form = defaultForm();
|
||||
}
|
||||
});
|
||||
|
||||
const btnOk = async () => {
|
||||
await useI18nFormValidate(formRef);
|
||||
await saveFormExec();
|
||||
useI18nSaveSuccessMsg();
|
||||
emit('success', state.form);
|
||||
//重置表单域
|
||||
formRef.value.resetFields();
|
||||
cancel();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
16
frontend/src/views/msg/channel/ChannelEmail.vue
Executable file
16
frontend/src/views/msg/channel/ChannelEmail.vue
Executable file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-form-item prop="extra.smtpAccount" :rules="[Rules.requiredInput('msg.smtpAccount')]" :label="$t('msg.smtpAccount')">
|
||||
<el-input v-model.trim="extra.smtpAccount" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="extra.smtpPassword" :rules="[Rules.requiredInput('msg.smtpPassword')]" :label="$t('msg.smtpPassword')">
|
||||
<el-input v-model.trim="extra.smtpPassword" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Rules } from '@/common/rule';
|
||||
|
||||
const extra = defineModel<any>('extra', { default: {} });
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
113
frontend/src/views/msg/channel/ChannelList.vue
Executable file
113
frontend/src/views/msg/channel/ChannelList.vue
Executable file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="channelApi.list"
|
||||
:search-items="searchItems"
|
||||
v-model:query-form="query"
|
||||
:show-selection="true"
|
||||
v-model:selection-data="selectionData"
|
||||
:columns="columns"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<el-button v-auth="perms.saveChannel" type="primary" icon="plus" @click="editChannel(false)">{{ $t('common.create') }}</el-button>
|
||||
<el-button v-auth="perms.delChannel" :disabled="state.selectionData.length < 1" @click="deleteChannel()" type="danger" icon="delete">
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button link v-auth="perms.saveChannel" @click="editChannel(data)" type="primary">{{ $t('common.edit') }}</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<ChannelEdit v-model:visible="editDialog.visible" :form="editDialog.data" :title="editDialog.title" @success="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
import { channelApi } from '../api';
|
||||
import { ChannelStatusEnum, ChannelTypeEnum } from '../enums';
|
||||
import ChannelEdit from './ChannelEdit.vue';
|
||||
|
||||
const perms = {
|
||||
saveChannel: 'msg:channel:save',
|
||||
delChannel: 'msg:channel:del',
|
||||
};
|
||||
|
||||
const searchItems = [SearchItem.input('name', 'msg.name')];
|
||||
const columns = [
|
||||
TableColumn.new('code', 'common.code'),
|
||||
TableColumn.new('name', 'msg.name'),
|
||||
TableColumn.new('status', 'common.status').typeTag(ChannelStatusEnum),
|
||||
TableColumn.new('type', 'common.type').typeTag(ChannelTypeEnum).setAddWidth(15),
|
||||
TableColumn.new('url', 'URL'),
|
||||
TableColumn.new('remark', 'common.remark'),
|
||||
TableColumn.new('creator', 'common.creator'),
|
||||
TableColumn.new('createTime', 'common.createTime').isTime(),
|
||||
];
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.saveChannel, perms.delChannel]);
|
||||
const actionColumn = TableColumn.new('action', 'common.operation').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter();
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
const state = reactive({
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectionData: [],
|
||||
/**
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
name: '',
|
||||
code: '',
|
||||
type: '',
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
editDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, editDialog } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
columns.push(actionColumn);
|
||||
}
|
||||
});
|
||||
|
||||
const search = async () => {
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const editChannel = (data: any) => {
|
||||
if (!data) {
|
||||
state.editDialog.title = useI18nCreateTitle('msg.msgChannel');
|
||||
state.editDialog.data = null;
|
||||
} else {
|
||||
state.editDialog.title = useI18nEditTitle('msg.msgChannel');
|
||||
state.editDialog.data = data;
|
||||
}
|
||||
state.editDialog.visible = true;
|
||||
};
|
||||
|
||||
const deleteChannel = async () => {
|
||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.code).join('、'));
|
||||
await channelApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
useI18nDeleteSuccessMsg();
|
||||
search();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
47
frontend/src/views/msg/components/MsgTmplSelect.vue
Normal file
47
frontend/src/views/msg/components/MsgTmplSelect.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<el-select
|
||||
remote
|
||||
:remote-method="getMsgTmpls"
|
||||
v-model="tmplId"
|
||||
filterable
|
||||
:placeholder="$t('msg.selectTmplPlaceholder')"
|
||||
v-bind="$attrs"
|
||||
:ref="(el: any) => props.focus && el?.focus()"
|
||||
>
|
||||
<el-option v-for="item in tmpls" :key="item.id" :label="item.name" :value="item.id">
|
||||
{{ item.code }}
|
||||
<el-divider direction="vertical" />
|
||||
{{ item.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { tmplApi } from '../api';
|
||||
|
||||
const props = defineProps({
|
||||
// 是否获取焦点
|
||||
focus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const tmplId = defineModel('modelValue');
|
||||
|
||||
const tmpls: any = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
// 如果初始化时有tmplId,则需要获取对应消息模板信息,用于回显
|
||||
tmplApi.list.request({ id: tmplId.value }).then((res) => {
|
||||
tmpls.value = res.list;
|
||||
});
|
||||
});
|
||||
|
||||
const getMsgTmpls = (code: any) => {
|
||||
tmplApi.list.request({ code }).then((res) => {
|
||||
tmpls.value = res.list;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
24
frontend/src/views/msg/enums.ts
Normal file
24
frontend/src/views/msg/enums.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { EnumValue } from '@/common/Enum';
|
||||
|
||||
export const ChannelStatusEnum = {
|
||||
Enable: EnumValue.of(1, 'common.enable').tagTypeSuccess(),
|
||||
Disable: EnumValue.of(-1, 'common.disable').tagTypeDanger(),
|
||||
};
|
||||
|
||||
export const TmplStatusEnum = {
|
||||
Enable: EnumValue.of(1, 'common.enable').tagTypeSuccess(),
|
||||
Disable: EnumValue.of(-1, 'common.disable').tagTypeDanger(),
|
||||
};
|
||||
|
||||
export const TmplTypeEnum = {
|
||||
Text: EnumValue.of(1, 'text'),
|
||||
Markdown: EnumValue.of(2, 'markdown'),
|
||||
Html: EnumValue.of(3, 'html'),
|
||||
};
|
||||
|
||||
export const ChannelTypeEnum = {
|
||||
Email: EnumValue.of('email', 'msg.email').setExtra({ component: 'ChannelEmail', msgTypes: [TmplTypeEnum.Text, TmplTypeEnum.Markdown, TmplTypeEnum.Html] }),
|
||||
DingBot: EnumValue.of('dingBot', 'msg.dingBot').setExtra({ component: 'ChannelDing', msgTypes: [TmplTypeEnum.Text, TmplTypeEnum.Markdown] }),
|
||||
QywxBot: EnumValue.of('qywxBot', 'msg.qywxBot').setExtra({ msgTypes: [TmplTypeEnum.Text.value, TmplTypeEnum.Markdown] }),
|
||||
FeishuBot: EnumValue.of('feishuBot', 'msg.feishuBot').setExtra({ component: 'ChannelDing', msgTypes: [TmplTypeEnum.Text] }),
|
||||
};
|
||||
152
frontend/src/views/msg/tmpl/TmplEdit.vue
Executable file
152
frontend/src/views/msg/tmpl/TmplEdit.vue
Executable file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="formData" ref="formRef" :rules="rules" label-position="top" label-width="auto">
|
||||
<el-form-item prop="name" :label="$t('msg.name')">
|
||||
<el-input v-model.trim="formData.name" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="status" :label="$t('common.status')">
|
||||
<EnumSelect :enums="ChannelStatusEnum" v-model="formData.status" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="remark" :label="$t('common.remark')">
|
||||
<el-input v-model.trim="formData.remark" auto-complete="off" type="textarea" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="channelIds" :label="$t('msg.msgChannel')">
|
||||
<el-select v-model="formData.channelIds" multiple clearable filterable>
|
||||
<el-option v-for="item in state.channels" :key="item.id" :label="item.name" :value="item.id">
|
||||
{{ $t(EnumValue.getLabelByValue(ChannelTypeEnum, item.type)) }}
|
||||
<el-divider direction="vertical" />
|
||||
{{ item.code }}
|
||||
<el-divider direction="vertical" />
|
||||
{{ item.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="msgType" :label="$t('common.type')">
|
||||
<EnumSelect :enums="TmplTypeEnum" v-model="formData.msgType" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="title" :label="$t('msg.title')">
|
||||
<el-input v-model.trim="formData.title" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<FormItemTooltip prop="tmpl" :label="$t('msg.tmpl')" :tooltip="$t('msg.msgTmplTooltip')">
|
||||
<MonacoEditor
|
||||
class="w100"
|
||||
height="200px"
|
||||
v-model="formData.tmpl"
|
||||
:language="EnumValue.getLabelByValue(TmplTypeEnum, formData.msgType)"
|
||||
></MonacoEditor>
|
||||
</FormItemTooltip>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watchEffect, useTemplateRef, toRefs } from 'vue';
|
||||
import { channelApi, tmplApi } from '../api';
|
||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||
import { ChannelStatusEnum, TmplStatusEnum, TmplTypeEnum, ChannelTypeEnum } from '../enums';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
|
||||
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: [Boolean, Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['cancel', 'success']);
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const formRef: any = useTemplateRef('formRef');
|
||||
|
||||
const rules = {
|
||||
name: [Rules.requiredInput('msg.name')],
|
||||
type: [Rules.requiredSelect('common.type')],
|
||||
tmpl: [Rules.requiredInput('msg.tmpl')],
|
||||
};
|
||||
|
||||
const defaultForm = () => {
|
||||
return {
|
||||
id: null,
|
||||
name: null,
|
||||
msgType: TmplTypeEnum.Text.value,
|
||||
title: '',
|
||||
tmpl: '',
|
||||
status: TmplStatusEnum.Enable.value,
|
||||
remark: '',
|
||||
channelIds: [],
|
||||
extra: {},
|
||||
};
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
edit: false,
|
||||
form: defaultForm(),
|
||||
channels: [] as any,
|
||||
});
|
||||
|
||||
const { form: formData } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveFormExec } = tmplApi.save.useApi(formData);
|
||||
|
||||
watchEffect(() => {
|
||||
if (visible.value) {
|
||||
channelApi.list.request({ pageNum: 1, pageSize: 200 }).then((res) => {
|
||||
state.channels = res?.list;
|
||||
});
|
||||
}
|
||||
|
||||
const form: any = props.form;
|
||||
if (form) {
|
||||
state.form = { ...form };
|
||||
tmplApi.relateChannels.request({ id: form.id }).then((res) => {
|
||||
state.form.channelIds = res.map((item: any) => item.id);
|
||||
});
|
||||
state.edit = true;
|
||||
} else {
|
||||
state.edit = false;
|
||||
state.form = defaultForm();
|
||||
}
|
||||
});
|
||||
|
||||
const btnOk = async () => {
|
||||
await useI18nFormValidate(formRef);
|
||||
await saveFormExec();
|
||||
useI18nSaveSuccessMsg();
|
||||
emit('success', state.form);
|
||||
//重置表单域
|
||||
formRef.value.resetFields();
|
||||
cancel();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
180
frontend/src/views/msg/tmpl/TmplList.vue
Executable file
180
frontend/src/views/msg/tmpl/TmplList.vue
Executable file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="tmplApi.list"
|
||||
:search-items="searchItems"
|
||||
v-model:query-form="query"
|
||||
:show-selection="true"
|
||||
v-model:selection-data="selectionData"
|
||||
:columns="columns"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<el-button v-auth="perms.saveTmpl" type="primary" icon="plus" @click="editTmpl(false)">{{ $t('common.create') }}</el-button>
|
||||
<el-button v-auth="perms.delTmpl" :disabled="state.selectionData.length < 1" @click="deleteTmpl()" type="danger" icon="delete">
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #relateChannel="{ data }">
|
||||
<el-popover placement="top-start" trigger="click" width="auto">
|
||||
<template #reference>
|
||||
<el-link @click="getRelateChannels(data.id)" icon="view" type="primary" :underline="false"></el-link>
|
||||
</template>
|
||||
<el-row v-for="item in state.relateChannels" :key="item.id">
|
||||
{{ $t(EnumValue.getLabelByValue(ChannelTypeEnum, item.type)) }}
|
||||
<el-divider direction="vertical" />
|
||||
{{ item.code }}
|
||||
<el-divider direction="vertical" />
|
||||
{{ item.name }}
|
||||
</el-row>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button link v-auth="perms.saveTmpl" @click="editTmpl(data)" type="primary">{{ $t('common.edit') }}</el-button>
|
||||
<el-button link v-auth="perms.sendMsg" @click="showSendMsgDialog(data)" type="warning">{{ $t('msg.send') }}</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<TmplEdit v-model:visible="editDialog.visible" :form="editDialog.data" :title="editDialog.title" @success="search" />
|
||||
|
||||
<el-dialog width="500px" :title="$t('msg.sendMsg')" v-model="sendMsgDialog.visible">
|
||||
<el-form label-width="auto">
|
||||
<el-form-item prop="params" :label="$t('params')">
|
||||
<el-input v-model.trim="sendMsgDialog.params" type="textarea" rows="5" placeholder="JSON" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<AccountSelectFormItem multiple v-model="sendMsgDialog.receiverIds" />
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="() => (sendMsgDialog.visible = false)">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="sendMsg()">{{ $t('msg.send') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||
import { tmplApi } from '../api';
|
||||
import { TmplStatusEnum, TmplTypeEnum, ChannelTypeEnum } from '../enums';
|
||||
import TmplEdit from './TmplEdit.vue';
|
||||
import EnumValue from '../../../common/Enum';
|
||||
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
|
||||
|
||||
const perms = {
|
||||
saveTmpl: 'msg:tmpl:save',
|
||||
delTmpl: 'msg:tmpl:del',
|
||||
sendMsg: 'msg:tmpl:send',
|
||||
};
|
||||
|
||||
const searchItems = [SearchItem.input('code', 'common.code')];
|
||||
const columns = [
|
||||
TableColumn.new('code', 'common.code'),
|
||||
TableColumn.new('name', 'msg.name'),
|
||||
TableColumn.new('status', 'common.status').typeTag(TmplStatusEnum),
|
||||
TableColumn.new('msgType', 'common.type').typeTag(TmplTypeEnum).setAddWidth(20),
|
||||
TableColumn.new('tmpl', 'msg.tmpl').canBeautify(),
|
||||
TableColumn.new('relateChannel', 'msg.relateChannel').isSlot().alignCenter(),
|
||||
TableColumn.new('remark', 'common.remark'),
|
||||
TableColumn.new('creator', 'common.creator'),
|
||||
TableColumn.new('createTime', 'common.createTime').isTime(),
|
||||
];
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.saveTmpl, perms.delTmpl]);
|
||||
const actionColumn = TableColumn.new('action', 'common.operation').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter();
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
const state = reactive({
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectionData: [],
|
||||
/**
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
name: '',
|
||||
code: '',
|
||||
type: '',
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
relateChannelsVisible: false,
|
||||
relateChannels: [] as any,
|
||||
editDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
sendMsgDialog: {
|
||||
tmpl: null,
|
||||
title: '',
|
||||
visible: false,
|
||||
params: '',
|
||||
receiverIds: [],
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, editDialog, sendMsgDialog } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
columns.push(actionColumn);
|
||||
}
|
||||
});
|
||||
|
||||
const search = async () => {
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const getRelateChannels = async (id: number) => {
|
||||
state.relateChannels = [];
|
||||
state.relateChannels = await tmplApi.relateChannels.request({ id });
|
||||
};
|
||||
|
||||
const editTmpl = (data: any) => {
|
||||
if (!data) {
|
||||
state.editDialog.title = useI18nCreateTitle('msg.msgTmpl');
|
||||
state.editDialog.data = null;
|
||||
} else {
|
||||
state.editDialog.title = useI18nEditTitle('msg.msgTmpl');
|
||||
state.editDialog.data = data;
|
||||
}
|
||||
state.editDialog.visible = true;
|
||||
};
|
||||
|
||||
const deleteTmpl = async () => {
|
||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.code).join('、'));
|
||||
await tmplApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
useI18nDeleteSuccessMsg();
|
||||
search();
|
||||
};
|
||||
|
||||
const showSendMsgDialog = (tmpl: any) => {
|
||||
state.sendMsgDialog.tmpl = tmpl;
|
||||
state.sendMsgDialog.params = '';
|
||||
state.sendMsgDialog.receiverIds = [];
|
||||
state.sendMsgDialog.visible = true;
|
||||
};
|
||||
|
||||
const sendMsg = async () => {
|
||||
const tmpl: any = state.sendMsgDialog.tmpl;
|
||||
await tmplApi.sendMsg.request({
|
||||
code: tmpl.code,
|
||||
params: state.sendMsgDialog.params,
|
||||
receiverIds: state.sendMsgDialog.receiverIds,
|
||||
});
|
||||
useI18nOperateSuccessMsg();
|
||||
state.sendMsgDialog.visible = false;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -21,7 +21,7 @@ const { width } = useWindowSize();
|
||||
|
||||
console.log(width);
|
||||
|
||||
const leftPaneSize = computed(() => (width.value >= 1600 ? 20 : 25));
|
||||
const leftPaneSize = computed(() => (width.value >= 1600 ? 20 : 24));
|
||||
|
||||
// 处理 resize 事件
|
||||
const handleResize = (event: any) => {
|
||||
|
||||
@@ -157,11 +157,11 @@ import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||
import { getDbDialect, getDbDialectMap } from '@/views/ops/db/dialect';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import _ from 'lodash';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import { deepClone } from '@/common/utils/object';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -274,7 +274,8 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
state.form = _.cloneDeep(props.data) as FormData;
|
||||
|
||||
state.form = deepClone(props.data) as FormData;
|
||||
let { srcDbId, targetDbId } = state.form;
|
||||
|
||||
// 初始化src数据源
|
||||
|
||||
@@ -464,7 +464,7 @@ export class DbInst {
|
||||
* @returns
|
||||
*/
|
||||
static isNumber(columnType: string) {
|
||||
return columnType && columnType.match(/(int|uint|double|float|number|decimal|byte|bit)/gi);
|
||||
return columnType && columnType.match(/(int|uint|double|float|number|numeric|decimal|byte|bit)/gi);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -102,9 +102,9 @@ import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { cmdConfApi } from '../api';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import TagCodePath from '../../component/TagCodePath.vue';
|
||||
import _ from 'lodash';
|
||||
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import { deepClone } from '@/common/utils/object';
|
||||
|
||||
const rules = {
|
||||
tags: [Rules.requiredInput('machine.relateMachine')],
|
||||
@@ -166,7 +166,7 @@ const openFormDialog = (data: any) => {
|
||||
if (!data) {
|
||||
state.form = { ...DefaultForm };
|
||||
} else {
|
||||
state.form = _.cloneDeep(data);
|
||||
state.form = deepClone(data);
|
||||
state.form.codePaths = data.tags?.map((tag: any) => tag.codePath);
|
||||
}
|
||||
state.dialogVisible = true;
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
|
||||
<template #right>
|
||||
<Splitpanes class="default-theme">
|
||||
<Pane size="40" max-size="45">
|
||||
<Pane size="35" max-size="50">
|
||||
<div class="key-list-vtree card pd5">
|
||||
<el-scrollbar>
|
||||
<el-row>
|
||||
<el-row :gutter="5">
|
||||
<el-col :span="2">
|
||||
<el-input v-model="state.keySeparator" :placeholder="$t('redis.delimiter')" size="small" class="ml5" />
|
||||
<el-input v-model="state.keySeparator" :placeholder="$t('redis.delimiter')" size="small" />
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-input
|
||||
@@ -59,12 +59,10 @@
|
||||
:placeholder="$t('redis.keyMatchTips')"
|
||||
clearable
|
||||
size="small"
|
||||
class="ml10"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button
|
||||
class="ml15"
|
||||
:disabled="!scanParam.id || !scanParam.db"
|
||||
@click="searchKey()"
|
||||
type="success"
|
||||
@@ -75,18 +73,11 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row class="mb5 mt5">
|
||||
<el-row :gutter="5" class="mb5 mt5">
|
||||
<el-col :span="19">
|
||||
<el-button
|
||||
class="ml5"
|
||||
:disabled="!scanParam.id || !scanParam.db"
|
||||
@click="scan(true)"
|
||||
type="success"
|
||||
icon="more"
|
||||
size="small"
|
||||
plain
|
||||
>{{ $t('redis.loadMore') }}</el-button
|
||||
>
|
||||
<el-button :disabled="!scanParam.id || !scanParam.db" @click="scan(true)" type="success" icon="more" size="small" plain>
|
||||
{{ $t('redis.loadMore') }}
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-auth="'redis:data:save'"
|
||||
@@ -96,8 +87,10 @@
|
||||
icon="plus"
|
||||
size="small"
|
||||
plain
|
||||
>{{ $t('redis.addKey') }}</el-button
|
||||
class="ml5"
|
||||
>
|
||||
{{ $t('redis.addKey') }}
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
:disabled="!scanParam.id || !scanParam.db"
|
||||
@@ -107,11 +100,13 @@
|
||||
v-auth="'redis:data:del'"
|
||||
size="small"
|
||||
icon="delete"
|
||||
>flush</el-button
|
||||
class="ml5"
|
||||
>
|
||||
flush
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<span style="display: inline-block" class="mt5">keys:{{ state.dbsize }}</span>
|
||||
<span class="mt5" style="display: inline-block">keys:{{ state.dbsize }}</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
@@ -148,7 +143,7 @@
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
<Pane min-size="40">
|
||||
<Pane>
|
||||
<div class="key-detail card pd5">
|
||||
<el-tabs @tab-remove="removeDataTab" v-model="state.activeName">
|
||||
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
|
||||
|
||||
@@ -187,17 +187,17 @@ const ttlConveter = (ttl: any) => {
|
||||
}
|
||||
|
||||
.key-header-item.key-name-input {
|
||||
width: calc(100% - 332px);
|
||||
min-width: 220px;
|
||||
width: calc(100% - 322px);
|
||||
min-width: 230px;
|
||||
max-width: 800px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.key-header-item.key-ttl-input {
|
||||
width: 200px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 190px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/*hide number input button*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="account-dialog">
|
||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :show-close="false" width="600px" :destroy-on-close="true">
|
||||
<el-dialog :title="title" v-model="visible" :before-close="cancel" :show-close="false" width="600px" :destroy-on-close="true">
|
||||
<el-form :model="form" ref="accountForm" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="name" :label="$t('system.account.name')">
|
||||
<el-input v-model.trim="form.name" auto-complete="off" clearable></el-input>
|
||||
@@ -16,6 +16,14 @@
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="mobile" :label="$t('common.mobile')">
|
||||
<el-input v-model.trim="form.mobile" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="email" :label="$t('common.email')">
|
||||
<el-input v-model.trim="form.email" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :required="!edit" prop="password" :label="$t('common.password')">
|
||||
<el-input type="password" v-model.trim="form.password" autocomplete="new-password" show-password>
|
||||
<template #append>
|
||||
@@ -31,29 +39,31 @@
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('system.account.qywxUserId')">
|
||||
<el-input v-model.trim="form.extra.qywxUserId" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('system.account.feishuUserId')">
|
||||
<el-input v-model.trim="form.extra.feishuUserId" clearable></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
</div>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, ref, watchEffect } from 'vue';
|
||||
import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { accountApi } from '../api';
|
||||
import { randomPassword } from '@/common/utils/string';
|
||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
account: {
|
||||
type: [Boolean, Object],
|
||||
},
|
||||
@@ -65,6 +75,8 @@ const props = defineProps({
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const accountForm: any = ref(null);
|
||||
|
||||
const rules = {
|
||||
@@ -73,43 +85,42 @@ const rules = {
|
||||
password: [Rules.requiredInput('common.password')],
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
edit: false,
|
||||
form: {
|
||||
const defaultForm = () => {
|
||||
return {
|
||||
id: null,
|
||||
name: null,
|
||||
username: null,
|
||||
mobile: null,
|
||||
email: null,
|
||||
password: '',
|
||||
repassword: null,
|
||||
},
|
||||
extra: {
|
||||
qywxUserId: '',
|
||||
feishuUserId: '',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
edit: false,
|
||||
form: defaultForm(),
|
||||
});
|
||||
|
||||
const { dialogVisible, edit, form } = toRefs(state);
|
||||
const { edit, form } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveAccountExec } = accountApi.save.useApi(form);
|
||||
|
||||
watch(props, (newValue: any) => {
|
||||
if (newValue.account) {
|
||||
state.form = { ...newValue.account };
|
||||
if (!state.form.extra) {
|
||||
state.form.extra = {} as any;
|
||||
}
|
||||
state.edit = true;
|
||||
} else {
|
||||
state.edit = false;
|
||||
state.form = {} as any;
|
||||
state.form = defaultForm();
|
||||
}
|
||||
state.dialogVisible = newValue.visible;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
const account: any = props.account;
|
||||
if (account) {
|
||||
state.form = { ...account };
|
||||
state.edit = true;
|
||||
} else {
|
||||
state.edit = false;
|
||||
state.form = {} as any;
|
||||
}
|
||||
state.dialogVisible = props.visible;
|
||||
});
|
||||
|
||||
const btnOk = async () => {
|
||||
@@ -119,11 +130,10 @@ const btnOk = async () => {
|
||||
emit('val-change', state.form);
|
||||
//重置表单域
|
||||
accountForm.value.resetFields();
|
||||
state.form = {} as any;
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -84,6 +84,8 @@ const searchItems = [SearchItem.input('username', 'common.username')];
|
||||
const columns = [
|
||||
TableColumn.new('name', 'system.account.name'),
|
||||
TableColumn.new('username', 'common.username'),
|
||||
TableColumn.new('mobile', 'common.mobile'),
|
||||
TableColumn.new('email', 'common.email'),
|
||||
TableColumn.new('status', 'common.status').typeTag(AccountStatusEnum),
|
||||
TableColumn.new('lastLoginTime', 'system.account.lastLoginTime').isTime(),
|
||||
TableColumn.new('creator', 'common.creator'),
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="assigner" :label="$t('system.role.assigner')"></el-table-column>
|
||||
<el-table-column prop="allocateTime" :label="$t('system.role.allocateTime')">
|
||||
<el-table-column prop="allocateTime" :label="$t('system.role.allocateTime')" min-width="150">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.allocateTime) }}
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@ module mayfly-go
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
gitee.com/chunanyong/dm v1.8.18
|
||||
gitee.com/chunanyong/dm v1.8.19
|
||||
gitee.com/liuzongyang/libpq v1.10.11
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||
github.com/emirpasic/gods v1.18.1
|
||||
@@ -14,8 +14,8 @@ require (
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.25.0
|
||||
github.com/go-sql-driver/mysql v1.9.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/go-sql-driver/mysql v1.9.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20241220152942-06eb5c6e8230
|
||||
@@ -23,18 +23,18 @@ require (
|
||||
github.com/microsoft/go-mssqldb v1.8.0
|
||||
github.com/mojocn/base64Captcha v1.3.8 // 验证码
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.7
|
||||
github.com/pkg/sftp v1.13.9
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/redis/go-redis/v9 v9.7.1
|
||||
github.com/redis/go-redis/v9 v9.7.3
|
||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||
github.com/sijms/go-ora/v2 v2.8.24
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/veops/go-ansiterm v0.0.5
|
||||
go.mongodb.org/mongo-driver v1.16.0 // mongo
|
||||
golang.org/x/crypto v0.36.0 // ssh
|
||||
golang.org/x/oauth2 v0.26.0
|
||||
golang.org/x/sync v0.12.0
|
||||
golang.org/x/crypto v0.37.0 // ssh
|
||||
golang.org/x/oauth2 v0.29.0
|
||||
golang.org/x/sync v0.13.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
// gorm
|
||||
@@ -93,8 +93,8 @@ require (
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
|
||||
golang.org/x/image v0.23.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
|
||||
@@ -16,12 +16,12 @@ var (
|
||||
initFuncs = make([]InitFunc, 0)
|
||||
)
|
||||
|
||||
// 添加初始化ioc函数,由各个默认自行添加(直接init方法中ioc.Register注册不会打印ioc相关日志)
|
||||
// 添加初始化ioc函数,由各个模块自行添加(直接init方法中ioc.Register注册不会打印ioc相关日志)
|
||||
func AddInitIocFunc(initIocFunc InitIocFunc) {
|
||||
initIocFuncs = append(initIocFuncs, initIocFunc)
|
||||
}
|
||||
|
||||
// 添加初始化函数,由各个默认自行添加
|
||||
// 添加初始化函数,由各个模块自行添加
|
||||
func AddInitFunc(initFunc InitFunc) {
|
||||
initFuncs = append(initFuncs, initFunc)
|
||||
}
|
||||
|
||||
@@ -30,4 +30,5 @@ const (
|
||||
ImsgNumDb = 60000
|
||||
ImsgNumRedis = 70000
|
||||
ImsgNumMongo = 80000
|
||||
ImsgNumMsg = 90000
|
||||
)
|
||||
|
||||
@@ -226,9 +226,10 @@ var (
|
||||
SQLValue: SQLValueNumeric,
|
||||
}
|
||||
|
||||
// 使用string进行转换,避免长度过长导致精度丢失等
|
||||
DTNumeric = &DataType{
|
||||
Name: "numeric",
|
||||
Valuer: ValuerFloat64,
|
||||
Valuer: ValuerString,
|
||||
SQLValue: SQLValueNumeric,
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ func (c *commonTypeConverter) Blob(col *dbi.Column) *dbi.DbDataType {
|
||||
return Blob
|
||||
}
|
||||
func (c *commonTypeConverter) Longblob(col *dbi.Column) *dbi.DbDataType {
|
||||
col.CharMaxLength = 0
|
||||
return Longblob
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package event
|
||||
|
||||
const (
|
||||
EventTopicDeleteMachine = "machine:delete" // 删除机器的事件主题名
|
||||
EventTopicResourceOp = "resource:op" // 资源操作主题
|
||||
EventTopicResourceOp = "resource:op" // 资源操作主题
|
||||
EventTopicBizMsgTmplSend = "biz:msgtmpl:send" // 发送业务关联的消息模板
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ type Procdef struct {
|
||||
Status entity.ProcdefStatus `json:"status" binding:"required"`
|
||||
Condition string `json:"condition"`
|
||||
Remark string `json:"remark"`
|
||||
MsgTmplId uint64 `json:"msgTmplId"`
|
||||
|
||||
CodePaths []string `json:"codePaths"`
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ import (
|
||||
"mayfly-go/internal/flow/application/dto"
|
||||
"mayfly-go/internal/flow/domain/entity"
|
||||
"mayfly-go/internal/flow/imsg"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
msgentity "mayfly-go/internal/msg/domain/entity"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/structx"
|
||||
"strings"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
@@ -20,12 +23,15 @@ import (
|
||||
type Procdef struct {
|
||||
procdefApp application.Procdef `inject:"T"`
|
||||
tagTreeRelateApp tagapp.TagTreeRelate `inject:"T"`
|
||||
msgTmplBizApp msgapp.MsgTmplBiz `inject:"T"`
|
||||
}
|
||||
|
||||
func (p *Procdef) ReqConfs() *req.Confs {
|
||||
reqs := [...]*req.Conf{
|
||||
req.NewGet("", p.GetProcdefPage),
|
||||
|
||||
req.NewGet("/detail/:id", p.GetProcdefDetail),
|
||||
|
||||
req.NewGet("/:resourceType/:resourceCode", p.GetProcdef),
|
||||
|
||||
req.NewPost("", p.Save).Log(req.NewLogSaveI(imsg.LogProcdefSave)).RequiredPermissionCode("flow:procdef:save"),
|
||||
@@ -49,6 +55,25 @@ func (p *Procdef) GetProcdefPage(rc *req.Ctx) {
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (p *Procdef) GetProcdefDetail(rc *req.Ctx) {
|
||||
def, err := p.procdefApp.GetById(cast.ToUint64(rc.PathParamInt("id")))
|
||||
biz.ErrIsNil(err)
|
||||
res := new(vo.Procdef)
|
||||
biz.ErrIsNil(structx.Copy(res, def))
|
||||
|
||||
p.tagTreeRelateApp.FillTagInfo(tagentity.TagRelateTypeFlowDef, res)
|
||||
|
||||
bizMsgTmpl := &msgentity.MsgTmplBiz{
|
||||
BizId: res.Id,
|
||||
BizType: application.FlowTaskNotifyBizKey,
|
||||
}
|
||||
if p.msgTmplBizApp.GetByCond(bizMsgTmpl) == nil {
|
||||
res.MsgTmplId = &bizMsgTmpl.TmplId
|
||||
}
|
||||
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (p *Procdef) GetProcdef(rc *req.Ctx) {
|
||||
resourceType := rc.PathParamInt("resourceType")
|
||||
resourceCode := rc.PathParam("resourceCode")
|
||||
@@ -61,6 +86,7 @@ func (a *Procdef) Save(rc *req.Ctx) {
|
||||
rc.ReqParam = form
|
||||
biz.ErrIsNil(a.procdefApp.SaveProcdef(rc.MetaCtx, &dto.SaveProcdef{
|
||||
Procdef: procdef,
|
||||
MsgTmplId: form.MsgTmplId,
|
||||
CodePaths: form.CodePaths,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
type Procdef struct {
|
||||
tagentity.RelateTags // 标签信息
|
||||
entity.Procdef
|
||||
|
||||
MsgTmplId *uint64 `json:"msgTmplId" gorm:"-"` // 消息模板ID
|
||||
}
|
||||
|
||||
func (p *Procdef) GetRelateId() uint64 {
|
||||
|
||||
5
server/internal/flow/application/const.go
Normal file
5
server/internal/flow/application/const.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package application
|
||||
|
||||
const (
|
||||
FlowTaskNotifyBizKey = "flow:task:notify" // 工单任务处理通知
|
||||
)
|
||||
@@ -4,6 +4,7 @@ import "mayfly-go/internal/flow/domain/entity"
|
||||
|
||||
type SaveProcdef struct {
|
||||
Procdef *entity.Procdef
|
||||
MsgTmplId uint64 // 消息模板id
|
||||
CodePaths []string
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"mayfly-go/internal/flow/domain/entity"
|
||||
"mayfly-go/internal/flow/domain/repository"
|
||||
"mayfly-go/internal/flow/imsg"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
msgdto "mayfly-go/internal/msg/application/dto"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
@@ -36,6 +38,7 @@ type procdefAppImpl struct {
|
||||
|
||||
procinstApp Procinst `inject:"T"`
|
||||
|
||||
msgTmplBizApp msgapp.MsgTmplBiz `inject:"T"`
|
||||
tagTreeApp tagapp.TagTree `inject:"T"`
|
||||
tagTreeRelateApp tagapp.TagTreeRelate `inject:"T"`
|
||||
}
|
||||
@@ -67,6 +70,14 @@ func (p *procdefAppImpl) SaveProcdef(ctx context.Context, defParam *dto.SaveProc
|
||||
return p.Tx(ctx, func(ctx context.Context) error {
|
||||
return p.Save(ctx, def)
|
||||
}, func(ctx context.Context) error {
|
||||
// 保存通知消息模板
|
||||
if err := p.msgTmplBizApp.SaveBizTmpl(ctx, msgdto.MsgTmplBizSave{
|
||||
TmplId: defParam.MsgTmplId,
|
||||
BizType: FlowTaskNotifyBizKey,
|
||||
BizId: def.Id,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.tagTreeRelateApp.RelateTag(ctx, tagentity.TagRelateTypeFlowDef, def.Id, defParam.CodePaths...)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,19 +3,24 @@ package application
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/event"
|
||||
"mayfly-go/internal/flow/application/dto"
|
||||
"mayfly-go/internal/flow/domain/entity"
|
||||
"mayfly-go/internal/flow/domain/repository"
|
||||
"mayfly-go/internal/flow/imsg"
|
||||
msgdto "mayfly-go/internal/msg/application/dto"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/contextx"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/global"
|
||||
"mayfly-go/pkg/i18n"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/anyx"
|
||||
"mayfly-go/pkg/utils/jsonx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
)
|
||||
|
||||
type Procinst interface {
|
||||
@@ -140,6 +145,7 @@ func (p *procinstAppImpl) CompleteTask(ctx context.Context, instTaskId uint64, r
|
||||
procinst.SetEnd()
|
||||
} else {
|
||||
procinst.TaskKey = task.TaskKey
|
||||
|
||||
}
|
||||
|
||||
return p.Tx(ctx, func(ctx context.Context) error {
|
||||
@@ -287,7 +293,26 @@ func (p *procinstAppImpl) createProcinstTask(ctx context.Context, procinst *enti
|
||||
TaskName: task.Name,
|
||||
Assignee: task.UserId,
|
||||
}
|
||||
return p.procinstTaskRepo.Insert(ctx, procinstTask)
|
||||
|
||||
if err := p.procinstTaskRepo.Insert(ctx, procinstTask); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 发送通知消息
|
||||
global.EventBus.Publish(ctx, event.EventTopicBizMsgTmplSend, msgdto.BizMsgTmplSend{
|
||||
BizType: FlowTaskNotifyBizKey,
|
||||
BizId: procinst.ProcdefId,
|
||||
Params: map[string]any{
|
||||
"creator": procinst.Creator,
|
||||
"procdefName": procinst.ProcdefName,
|
||||
"bizKey": procinst.BizKey,
|
||||
"taskName": task.Name,
|
||||
"procinstRemark": procinst.Remark,
|
||||
},
|
||||
ReceiverIds: []uint64{cast.ToUint64(task.UserId)},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取下一审批节点任务
|
||||
|
||||
@@ -15,6 +15,7 @@ type ProcinstTaskQuery struct {
|
||||
ProcinstId uint64 `json:"procinstId"` // 流程实例id
|
||||
ProcinstName string `json:"procinstName"` // 流程实例名称
|
||||
BizType string `json:"bizType" form:"bizType"`
|
||||
BizKey string `json:"bizKey" form:"bizKey"` // 业务key
|
||||
Assignee string `json:"assignee"` // 分配到该任务的用户
|
||||
Status ProcinstTaskStatus `json:"status" form:"status"` // 状态
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"mayfly-go/internal/flow/domain/entity"
|
||||
"mayfly-go/internal/flow/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/gormx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
@@ -31,6 +32,14 @@ func newProcinstTaskRepo() repository.ProcinstTask {
|
||||
}
|
||||
|
||||
func (p *procinstTaskImpl) GetPageList(condition *entity.ProcinstTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
qd := model.NewModelCond(condition)
|
||||
return p.PageByCondToAny(qd, pageParam, toEntity)
|
||||
qd := gormx.NewQueryWithTableName("t_flow_procinst_task t").
|
||||
Joins("JOIN t_flow_procinst tp ON t.procinst_id = tp.id ").
|
||||
WithCond(model.NewCond().Columns("t.*, tp.biz_key").
|
||||
Eq("tp.biz_key", condition.BizKey).
|
||||
Eq0("tp.is_deleted", model.ModelUndeleted).
|
||||
Eq("tp.biz_type", condition.BizType).
|
||||
Eq0("t.is_deleted", model.ModelUndeleted).
|
||||
Eq("t.status", condition.Status).
|
||||
OrderByDesc("t.id"))
|
||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,9 @@ func (m *MachineScript) RunMachineScript(rc *req.Ctx) {
|
||||
script := ms.Script
|
||||
// 如果有脚本参数,则用脚本参数替换脚本中的模板占位符参数
|
||||
if params := rc.Query("params"); params != "" {
|
||||
script, err = stringx.TemplateParse(ms.Script, jsonx.ToMap(params))
|
||||
p, err := jsonx.ToMap(params)
|
||||
biz.ErrIsNil(err)
|
||||
script, err = stringx.TemplateParse(ms.Script, p)
|
||||
biz.ErrIsNilAppendErr(err, "failed to parse the script template parameter: %s")
|
||||
}
|
||||
cli, err := m.machineApp.GetCliByAc(ac)
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/event"
|
||||
"mayfly-go/internal/machine/domain/entity"
|
||||
"mayfly-go/pkg/eventbus"
|
||||
"mayfly-go/pkg/global"
|
||||
"mayfly-go/pkg/ioc"
|
||||
"sync"
|
||||
)
|
||||
@@ -26,16 +21,6 @@ func Init() {
|
||||
GetMachineApp().TimerUpdateStats()
|
||||
|
||||
GetMachineTermOpApp().TimerDeleteTermOp()
|
||||
|
||||
global.EventBus.Subscribe(event.EventTopicDeleteMachine, "machineFile", func(ctx context.Context, event *eventbus.Event) error {
|
||||
me := event.Val.(*entity.Machine)
|
||||
return GetMachineFileApp().DeleteByCond(ctx, &entity.MachineFile{MachineId: me.Id})
|
||||
})
|
||||
|
||||
global.EventBus.Subscribe(event.EventTopicDeleteMachine, "machineScript", func(ctx context.Context, event *eventbus.Event) error {
|
||||
me := event.Val.(*entity.Machine)
|
||||
return GetMachineScriptApp().DeleteByCond(ctx, &entity.MachineScript{MachineId: me.Id})
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package application
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/event"
|
||||
"mayfly-go/internal/machine/application/dto"
|
||||
"mayfly-go/internal/machine/domain/entity"
|
||||
"mayfly-go/internal/machine/domain/repository"
|
||||
@@ -15,7 +14,6 @@ import (
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/global"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/scheduler"
|
||||
@@ -65,6 +63,9 @@ type machineAppImpl struct {
|
||||
|
||||
tagApp tagapp.TagTree `inject:"T"`
|
||||
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"T"`
|
||||
|
||||
machineScriptApp MachineScript `inject:"T"`
|
||||
machineFileApp MachineFile `inject:"T"`
|
||||
}
|
||||
|
||||
var _ (Machine) = (*machineAppImpl)(nil)
|
||||
@@ -198,12 +199,15 @@ func (m *machineAppImpl) Delete(ctx context.Context, id uint64) error {
|
||||
// 关闭连接
|
||||
mcm.DeleteCli(id)
|
||||
|
||||
// 发布机器删除事件
|
||||
global.EventBus.Publish(ctx, event.EventTopicDeleteMachine, machine)
|
||||
|
||||
resourceType := tagentity.TagTypeMachine
|
||||
return m.Tx(ctx,
|
||||
func(ctx context.Context) error {
|
||||
if err := m.machineFileApp.DeleteByCond(ctx, &entity.MachineFile{MachineId: id}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.machineScriptApp.DeleteByCond(ctx, &entity.MachineScript{MachineId: id}); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.DeleteById(ctx, id)
|
||||
}, func(ctx context.Context) error {
|
||||
return m.tagApp.SaveResourceTag(ctx, &tagdto.SaveResourceTag{
|
||||
|
||||
@@ -4,4 +4,6 @@ import "mayfly-go/pkg/ioc"
|
||||
|
||||
func InitIoc() {
|
||||
ioc.Register(new(Msg))
|
||||
ioc.Register(new(MsgChannel))
|
||||
ioc.Register(new(MsgTmpl))
|
||||
}
|
||||
|
||||
35
server/internal/msg/api/form/msg.go
Normal file
35
server/internal/msg/api/form/msg.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MsgChannel struct {
|
||||
model.ExtraData
|
||||
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
Url string `json:"url"`
|
||||
Remark string `json:"remark"`
|
||||
Status int8 `json:"status" binding:"required"`
|
||||
}
|
||||
|
||||
type MsgTmpl struct {
|
||||
model.ExtraData
|
||||
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Title string `json:"title"`
|
||||
Tmpl string `json:"tmpl" binding:"required"`
|
||||
MsgType msgx.MsgType `json:"msgType" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
Status int8 `json:"status" binding:"required"`
|
||||
ChannelIds []uint64 `json:"channelIds"`
|
||||
}
|
||||
|
||||
type SendMsg struct {
|
||||
Parmas string `json:"params"`
|
||||
ReceiverIds []uint64 `json:"receiverIds"`
|
||||
}
|
||||
54
server/internal/msg/api/msg_channel.go
Normal file
54
server/internal/msg/api/msg_channel.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/api/form"
|
||||
"mayfly-go/internal/msg/application"
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/imsg"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/req"
|
||||
"strings"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
)
|
||||
|
||||
type MsgChannel struct {
|
||||
msgChannelApp application.MsgChannel `inject:"T"`
|
||||
}
|
||||
|
||||
func (m *MsgChannel) ReqConfs() *req.Confs {
|
||||
basePermCode := "msg:channel:base"
|
||||
|
||||
reqs := [...]*req.Conf{
|
||||
req.NewGet("", m.GetMsgChannels).RequiredPermissionCode(basePermCode),
|
||||
req.NewPost("", m.SaveMsgChannels).Log(req.NewLogSaveI(imsg.LogMsgChannelSave)).RequiredPermissionCode("msg:channel:save"),
|
||||
req.NewDelete("", m.DelMsgChannels).Log(req.NewLogSaveI(imsg.LogMsgChannelDelete)).RequiredPermissionCode("msg:channel:del"),
|
||||
}
|
||||
|
||||
return req.NewConfs("/msg/channels", reqs[:]...)
|
||||
}
|
||||
|
||||
func (m *MsgChannel) GetMsgChannels(rc *req.Ctx) {
|
||||
condition := &entity.MsgChannel{}
|
||||
res, err := m.msgChannelApp.GetPageList(condition, rc.GetPageParam(), new([]entity.MsgChannel))
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (m *MsgChannel) SaveMsgChannels(rc *req.Ctx) {
|
||||
form := &form.MsgChannel{}
|
||||
rc.ReqParam = form
|
||||
channel := req.BindJsonAndCopyTo(rc, form, new(entity.MsgChannel))
|
||||
err := m.msgChannelApp.SaveChannel(rc.MetaCtx, channel)
|
||||
biz.ErrIsNil(err)
|
||||
}
|
||||
|
||||
func (m *MsgChannel) DelMsgChannels(rc *req.Ctx) {
|
||||
idsStr := rc.Query("id")
|
||||
rc.ReqParam = idsStr
|
||||
ids := strings.Split(idsStr, ",")
|
||||
|
||||
for _, v := range ids {
|
||||
biz.ErrIsNil(m.msgChannelApp.DeleteChannel(rc.MetaCtx, cast.ToUint64(v)))
|
||||
}
|
||||
}
|
||||
86
server/internal/msg/api/msg_tmpl.go
Normal file
86
server/internal/msg/api/msg_tmpl.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/api/form"
|
||||
"mayfly-go/internal/msg/application"
|
||||
"mayfly-go/internal/msg/application/dto"
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/imsg"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/jsonx"
|
||||
"strings"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
)
|
||||
|
||||
type MsgTmpl struct {
|
||||
msgTmplApp application.MsgTmpl `inject:"T"`
|
||||
}
|
||||
|
||||
func (m *MsgTmpl) ReqConfs() *req.Confs {
|
||||
basePermCode := "msg:tmpl:base"
|
||||
|
||||
reqs := [...]*req.Conf{
|
||||
req.NewGet("", m.GetMsgTmpls).RequiredPermissionCode(basePermCode),
|
||||
req.NewGet(":id/channels", m.GetMsgTmplChannels).RequiredPermissionCode(basePermCode),
|
||||
req.NewPost("", m.SaveMsgTmpl).Log(req.NewLogSaveI(imsg.LogMsgTmplSave)).RequiredPermissionCode("msg:tmpl:save"),
|
||||
req.NewDelete("", m.DelMsgTmpls).Log(req.NewLogSaveI(imsg.LogMsgTmplDelete)).RequiredPermissionCode("msg:tmpl:del"),
|
||||
req.NewPost(":code/send", m.SendMsg).Log(req.NewLogSaveI(imsg.LogMsgTmplSave)).RequiredPermissionCode("msg:tmpl:send"),
|
||||
}
|
||||
|
||||
return req.NewConfs("/msg/tmpls", reqs[:]...)
|
||||
}
|
||||
|
||||
func (m *MsgTmpl) GetMsgTmpls(rc *req.Ctx) {
|
||||
condition := &entity.MsgTmpl{
|
||||
Code: rc.Query("code"),
|
||||
}
|
||||
condition.Id = cast.ToUint64(rc.QueryInt("id"))
|
||||
res, err := m.msgTmplApp.GetPageList(condition, rc.GetPageParam(), new([]entity.MsgTmpl))
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (m *MsgTmpl) GetMsgTmplChannels(rc *req.Ctx) {
|
||||
channels, err := m.msgTmplApp.GetTmplChannels(rc.MetaCtx, cast.ToUint64(rc.PathParamInt("id")))
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = collx.ArrayMap(channels, func(val *entity.MsgChannel) collx.M {
|
||||
return collx.M{
|
||||
"id": val.Id,
|
||||
"name": val.Name,
|
||||
"type": val.Type,
|
||||
"code": val.Code,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m *MsgTmpl) SaveMsgTmpl(rc *req.Ctx) {
|
||||
form := &form.MsgTmpl{}
|
||||
rc.ReqParam = form
|
||||
channel := req.BindJsonAndCopyTo(rc, form, new(dto.MsgTmplSave))
|
||||
biz.ErrIsNil(m.msgTmplApp.SaveTmpl(rc.MetaCtx, channel))
|
||||
}
|
||||
|
||||
func (m *MsgTmpl) DelMsgTmpls(rc *req.Ctx) {
|
||||
idsStr := rc.Query("id")
|
||||
rc.ReqParam = idsStr
|
||||
ids := strings.Split(idsStr, ",")
|
||||
|
||||
for _, v := range ids {
|
||||
biz.ErrIsNil(m.msgTmplApp.DeleteTmpl(rc.MetaCtx, cast.ToUint64(v)))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MsgTmpl) SendMsg(rc *req.Ctx) {
|
||||
code := rc.PathParam("code")
|
||||
form := &form.SendMsg{}
|
||||
req.BindJsonAndValid(rc, form)
|
||||
|
||||
rc.ReqParam = form
|
||||
|
||||
params, err := jsonx.ToMap(form.Parmas)
|
||||
biz.ErrIsNil(err)
|
||||
biz.ErrIsNil(m.msgTmplApp.Send(rc.MetaCtx, code, params, form.ReceiverIds...))
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
_ "mayfly-go/internal/msg/msgx/sender" // 注册消息发送器
|
||||
"mayfly-go/pkg/ioc"
|
||||
)
|
||||
|
||||
func InitIoc() {
|
||||
ioc.Register(new(msgAppImpl), ioc.WithComponentName("MsgApp"))
|
||||
ioc.Register(new(msgChannelAppImpl), ioc.WithComponentName("MsgChannelApp"))
|
||||
ioc.Register(new(msgTmplAppImpl), ioc.WithComponentName("MsgTmplApp"))
|
||||
ioc.Register(new(msgTmplBizAppImpl), ioc.WithComponentName("MsgTmplBizApp"))
|
||||
}
|
||||
|
||||
func GetMsgApp() Msg {
|
||||
|
||||
36
server/internal/msg/application/dto/msg.go
Normal file
36
server/internal/msg/application/dto/msg.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MsgTmplSave struct {
|
||||
model.ExtraData
|
||||
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Remark string `json:"remark"`
|
||||
Status entity.MsgTmplStatus `json:"status" `
|
||||
Title string `json:"title"`
|
||||
Tmpl string `json:"type"`
|
||||
MsgType msgx.MsgType `json:"msgType"`
|
||||
|
||||
ChannelIds []uint64 `json:"channelIds"`
|
||||
}
|
||||
|
||||
// MsgTmplBizSave 消息模板关联业务信息
|
||||
type MsgTmplBizSave struct {
|
||||
TmplId uint64 // 消息模板id
|
||||
BizId uint64 // 业务id
|
||||
BizType string
|
||||
}
|
||||
|
||||
// BizMsgTmplSend 业务消息模板发送消息
|
||||
type BizMsgTmplSend struct {
|
||||
BizId uint64 // 业务id
|
||||
BizType string
|
||||
Params map[string]any // 模板占位符参数
|
||||
ReceiverIds []uint64 // 接收人id
|
||||
}
|
||||
52
server/internal/msg/application/msg_channel.go
Normal file
52
server/internal/msg/application/msg_channel.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
)
|
||||
|
||||
type MsgChannel interface {
|
||||
base.App[*entity.MsgChannel]
|
||||
|
||||
GetPageList(condition *entity.MsgChannel, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
|
||||
SaveChannel(ctx context.Context, msgChannel *entity.MsgChannel) error
|
||||
|
||||
DeleteChannel(ctx context.Context, id uint64) error
|
||||
}
|
||||
|
||||
type msgChannelAppImpl struct {
|
||||
base.AppImpl[*entity.MsgChannel, repository.MsgChannel]
|
||||
|
||||
msgTempApp MsgTmpl `inject:"T"`
|
||||
}
|
||||
|
||||
var _ (MsgChannel) = (*msgChannelAppImpl)(nil)
|
||||
|
||||
func (m *msgChannelAppImpl) GetPageList(condition *entity.MsgChannel, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return m.Repo.GetPageList(condition, pageParam, toEntity)
|
||||
}
|
||||
|
||||
func (m *msgChannelAppImpl) SaveChannel(ctx context.Context, msgChannel *entity.MsgChannel) error {
|
||||
if msgChannel.Id == 0 {
|
||||
msgChannel.Code = stringx.Rand(8)
|
||||
}
|
||||
return m.Save(ctx, msgChannel)
|
||||
}
|
||||
|
||||
func (m *msgChannelAppImpl) DeleteChannel(ctx context.Context, id uint64) error {
|
||||
return m.Tx(ctx, func(ctx context.Context) error {
|
||||
if err := m.DeleteById(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
// 删除渠道关联的模板
|
||||
if err := m.msgTempApp.DeleteTmplChannel(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
208
server/internal/msg/application/msg_tmpl.go
Normal file
208
server/internal/msg/application/msg_tmpl.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/msg/application/dto"
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/domain/repository"
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
sysentity "mayfly-go/internal/sys/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"mayfly-go/pkg/utils/structx"
|
||||
)
|
||||
|
||||
type MsgTmpl interface {
|
||||
base.App[*entity.MsgTmpl]
|
||||
|
||||
GetPageList(condition *entity.MsgTmpl, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
|
||||
SaveTmpl(ctx context.Context, msgTmpl *dto.MsgTmplSave) error
|
||||
|
||||
DeleteTmpl(ctx context.Context, id uint64) error
|
||||
|
||||
GetTmplChannels(ctx context.Context, tmplId uint64) ([]*entity.MsgChannel, error)
|
||||
|
||||
// Send 发送消息
|
||||
Send(ctx context.Context, tmplCode string, params map[string]any, receiverId ...uint64) error
|
||||
|
||||
// DeleteTmplChannel 删除指定渠道关联的模板
|
||||
DeleteTmplChannel(ctx context.Context, channelId uint64) error
|
||||
}
|
||||
|
||||
type msgTmplAppImpl struct {
|
||||
base.AppImpl[*entity.MsgTmpl, repository.MsgTmpl]
|
||||
|
||||
msgTmplChannelRepo repository.MsgTmplChannel `inject:"T"`
|
||||
|
||||
msgChannelApp MsgChannel `inject:"T"`
|
||||
msgTmplBizApp MsgTmplBiz `inject:"T"`
|
||||
accountApp sysapp.Account `inject:"T"`
|
||||
}
|
||||
|
||||
var _ (MsgTmpl) = (*msgTmplAppImpl)(nil)
|
||||
|
||||
func (m *msgTmplAppImpl) GetPageList(condition *entity.MsgTmpl, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return m.Repo.GetPageList(condition, pageParam, toEntity)
|
||||
}
|
||||
|
||||
func (m *msgTmplAppImpl) SaveTmpl(ctx context.Context, msgTmpl *dto.MsgTmplSave) error {
|
||||
return m.Tx(ctx, func(ctx context.Context) error {
|
||||
mt := &entity.MsgTmpl{}
|
||||
structx.Copy(mt, msgTmpl)
|
||||
isCreate := mt.Id == 0
|
||||
if isCreate {
|
||||
mt.Code = stringx.Rand(8)
|
||||
}
|
||||
|
||||
if err := m.Save(ctx, mt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldTemplChannelIds := []uint64{}
|
||||
if !isCreate {
|
||||
oldTemplChannels, err := m.msgTmplChannelRepo.SelectByCond(&entity.MsgTmplChannel{TmplId: mt.Id}, "channel_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldTemplChannelIds = collx.ArrayMap(oldTemplChannels, func(c *entity.MsgTmplChannel) uint64 {
|
||||
return c.ChannelId
|
||||
})
|
||||
}
|
||||
|
||||
add, del, _ := collx.ArrayCompare(msgTmpl.ChannelIds, oldTemplChannelIds)
|
||||
if len(add) > 0 {
|
||||
tmplChannels := collx.ArrayMap(msgTmpl.ChannelIds, func(channelId uint64) *entity.MsgTmplChannel {
|
||||
return &entity.MsgTmplChannel{
|
||||
ChannelId: channelId,
|
||||
TmplId: mt.Id,
|
||||
}
|
||||
})
|
||||
if err := m.msgTmplChannelRepo.BatchInsert(ctx, tmplChannels); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(del) > 0 {
|
||||
if err := m.msgTmplChannelRepo.DeleteByCond(ctx, model.NewCond().Eq("tmpl_id", mt.Id).In("channel_id", del)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m *msgTmplAppImpl) DeleteTmpl(ctx context.Context, id uint64) error {
|
||||
return m.Tx(ctx, func(ctx context.Context) error {
|
||||
if err := m.DeleteById(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.msgTmplBizApp.DeleteByTmplId(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.msgTmplChannelRepo.DeleteByCond(ctx, &entity.MsgTmplChannel{TmplId: id})
|
||||
})
|
||||
}
|
||||
|
||||
func (m *msgTmplAppImpl) GetTmplChannels(ctx context.Context, tmplId uint64) ([]*entity.MsgChannel, error) {
|
||||
tmplChannels, err := m.msgTmplChannelRepo.SelectByCond(&entity.MsgTmplChannel{TmplId: tmplId}, "channel_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tmplChannels) == 0 {
|
||||
return []*entity.MsgChannel{}, nil
|
||||
}
|
||||
|
||||
return m.msgChannelApp.GetByIds(collx.ArrayMap(tmplChannels, func(c *entity.MsgTmplChannel) uint64 {
|
||||
return c.ChannelId
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *msgTmplAppImpl) Send(ctx context.Context, tmplCode string, params map[string]any, receiverId ...uint64) error {
|
||||
tmpl := &entity.MsgTmpl{Code: tmplCode}
|
||||
err := m.GetByCond(tmpl)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("message template does not exist")
|
||||
}
|
||||
if tmpl.Status != entity.TmplStatusEnable {
|
||||
return errorx.NewBiz("message template is disabled")
|
||||
}
|
||||
|
||||
tmplChannels, err := m.msgTmplChannelRepo.SelectByCond(&entity.MsgTmplChannel{TmplId: tmpl.Id}, "channel_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tmplChannels) == 0 {
|
||||
return errorx.NewBiz("message template is not associated with any channel")
|
||||
}
|
||||
|
||||
channels, err := m.msgChannelApp.GetByIds(collx.ArrayMap(tmplChannels, func(c *entity.MsgTmplChannel) uint64 {
|
||||
return c.ChannelId
|
||||
}))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// content, err := stringx.TemplateParse(tmpl.Tmpl, params)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// toAll := len(receiverId) == 0
|
||||
accounts, err := m.accountApp.GetByIds(receiverId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := &msgx.Msg{
|
||||
Content: tmpl.Tmpl,
|
||||
Params: params,
|
||||
Title: tmpl.Title,
|
||||
Type: tmpl.MsgType,
|
||||
ExtraData: tmpl.ExtraData,
|
||||
}
|
||||
|
||||
if len(accounts) > 0 {
|
||||
msg.Receivers = collx.ArrayMap(accounts, func(account *sysentity.Account) msgx.Receiver {
|
||||
return msgx.Receiver{
|
||||
ExtraData: account.ExtraData,
|
||||
Email: account.Email,
|
||||
Mobile: account.Mobile,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, channel := range channels {
|
||||
if channel.Status != entity.ChannelStatusEnable {
|
||||
logx.Warnf("channel is disabled => %s", channel.Code)
|
||||
continue
|
||||
}
|
||||
|
||||
go func(channel *entity.MsgChannel) {
|
||||
if err := msgx.Send(&msgx.Channel{
|
||||
Type: channel.Type,
|
||||
Name: channel.Name,
|
||||
URL: channel.Url,
|
||||
ExtraData: channel.ExtraData,
|
||||
}, msg); err != nil {
|
||||
logx.Errorf("send msg error => channel=%s, msg=%s, err -> %v", channel.Code, msg.Content, err)
|
||||
}
|
||||
}(channel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *msgTmplAppImpl) DeleteTmplChannel(ctx context.Context, channelId uint64) error {
|
||||
return m.msgTmplChannelRepo.DeleteByCond(ctx, &entity.MsgTmplChannel{ChannelId: channelId})
|
||||
}
|
||||
102
server/internal/msg/application/msg_tmpl_biz.go
Normal file
102
server/internal/msg/application/msg_tmpl_biz.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/msg/application/dto"
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/errorx"
|
||||
)
|
||||
|
||||
type MsgTmplBiz interface {
|
||||
base.App[*entity.MsgTmplBiz]
|
||||
|
||||
// SaveBizTmpl 保存消息模板关联业务信息
|
||||
SaveBizTmpl(ctx context.Context, bizTmpl dto.MsgTmplBizSave) error
|
||||
|
||||
// DeleteByBiz 根据业务删除消息模板业务关联
|
||||
DeleteByBiz(ctx context.Context, bizType string, bizId uint64) error
|
||||
|
||||
// DeleteByTmplId 根据模板ID删除消息模板业务关联
|
||||
DeleteByTmplId(ctx context.Context, tmplId uint64) error
|
||||
|
||||
// Send 发送消息
|
||||
Send(ctx context.Context, sendParam dto.BizMsgTmplSend) error
|
||||
}
|
||||
|
||||
type msgTmplBizAppImpl struct {
|
||||
base.AppImpl[*entity.MsgTmplBiz, repository.MsgTmplBiz]
|
||||
|
||||
msgTmplApp MsgTmpl `inject:"T"`
|
||||
}
|
||||
|
||||
var _ (MsgTmplBiz) = (*msgTmplBizAppImpl)(nil)
|
||||
|
||||
func (m *msgTmplBizAppImpl) SaveBizTmpl(ctx context.Context, bizTmpl dto.MsgTmplBizSave) error {
|
||||
msgTmplId := bizTmpl.TmplId
|
||||
bizId := bizTmpl.BizId
|
||||
bizType := bizTmpl.BizType
|
||||
if bizId == 0 {
|
||||
return errorx.NewBiz("business ID cannot be empty")
|
||||
}
|
||||
if bizType == "" {
|
||||
return errorx.NewBiz("business type cannot be empty")
|
||||
}
|
||||
|
||||
msgTmplBiz := &entity.MsgTmplBiz{
|
||||
BizId: bizId,
|
||||
BizType: bizType,
|
||||
}
|
||||
// exist
|
||||
if err := m.GetByCond(msgTmplBiz); err == nil {
|
||||
// tmplId不变,直接返回即可
|
||||
if msgTmplBiz.TmplId == msgTmplId {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果模板ID为0,表示删除业务关联
|
||||
if msgTmplId == 0 {
|
||||
return m.DeleteByBiz(ctx, bizTmpl.BizType, bizTmpl.BizId)
|
||||
}
|
||||
|
||||
update := &entity.MsgTmplBiz{
|
||||
TmplId: msgTmplId,
|
||||
}
|
||||
update.Id = msgTmplBiz.Id
|
||||
return m.UpdateById(ctx, update)
|
||||
}
|
||||
|
||||
if msgTmplId == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
msgTmplBiz.TmplId = msgTmplId
|
||||
return m.Save(ctx, msgTmplBiz)
|
||||
}
|
||||
|
||||
func (m *msgTmplBizAppImpl) DeleteByBiz(ctx context.Context, bizType string, bizId uint64) error {
|
||||
return m.DeleteByCond(ctx, &entity.MsgTmplBiz{BizId: bizId, BizType: bizType})
|
||||
}
|
||||
|
||||
func (m *msgTmplBizAppImpl) DeleteByTmplId(ctx context.Context, tmplId uint64) error {
|
||||
return m.DeleteByCond(ctx, &entity.MsgTmplBiz{TmplId: tmplId})
|
||||
}
|
||||
|
||||
func (m *msgTmplBizAppImpl) Send(ctx context.Context, sendParam dto.BizMsgTmplSend) error {
|
||||
// 获取业务关联的消息模板
|
||||
msgTmplBiz := &entity.MsgTmplBiz{
|
||||
BizId: sendParam.BizId,
|
||||
BizType: sendParam.BizType,
|
||||
}
|
||||
if err := m.GetByCond(msgTmplBiz); err != nil {
|
||||
return errorx.NewBiz("message tmplate association business information does not exist")
|
||||
}
|
||||
|
||||
mstTmpl, err := m.msgTmplApp.GetById(msgTmplBiz.TmplId)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("message template does not exist")
|
||||
}
|
||||
|
||||
return m.msgTmplApp.Send(ctx, mstTmpl.Code, sendParam.Params, sendParam.ReceiverIds...)
|
||||
}
|
||||
29
server/internal/msg/domain/entity/msg_channel.go
Normal file
29
server/internal/msg/domain/entity/msg_channel.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MsgChannel struct {
|
||||
model.Model
|
||||
model.ExtraData
|
||||
|
||||
Name string `json:"name" gorm:"size:50;not null;"` // 渠道名称
|
||||
Code string `json:"code" gorm:"size:50;not null;"` // 渠道编码
|
||||
Type msgx.ChannelType `json:"type" gorm:"size:30;not null;"` // 渠道类型
|
||||
Url string `json:"url" gorm:"size:200;"` // 渠道url
|
||||
Status MsgChannelStatus `json:"status" gorm:"not null;"` // 状态
|
||||
Remark *string `json:"remark" gorm:"size:200;"` // 备注
|
||||
}
|
||||
|
||||
func (a *MsgChannel) TableName() string {
|
||||
return "t_msg_channel"
|
||||
}
|
||||
|
||||
type MsgChannelStatus int8
|
||||
|
||||
const (
|
||||
ChannelStatusEnable MsgChannelStatus = 1 // 启用状态
|
||||
ChannelStatusDisable MsgChannelStatus = -1 // 禁用状态
|
||||
)
|
||||
42
server/internal/msg/domain/entity/msg_tmpl.go
Normal file
42
server/internal/msg/domain/entity/msg_tmpl.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
// MsgTmpl 消息模板
|
||||
type MsgTmpl struct {
|
||||
model.Model
|
||||
model.ExtraData
|
||||
|
||||
Name string `json:"name" gorm:"size:50;not null;"` // 模板名称
|
||||
Code string `json:"code" gorm:"size:32;not null;"` // 模板编码
|
||||
Title string `json:"title" gorm:"size:100;"` // 标题
|
||||
Tmpl string `json:"tmpl" gorm:"size:2000;not null;"` // 消息模板
|
||||
MsgType msgx.MsgType `json:"msgType" gorm:"not null;"` // 消息类型
|
||||
Status MsgTmplStatus `json:"status" gorm:"not null;"` // 状态
|
||||
Remark *string `json:"remark" gorm:"size:200;"` // 备注
|
||||
}
|
||||
|
||||
func (a *MsgTmpl) TableName() string {
|
||||
return "t_msg_tmpl"
|
||||
}
|
||||
|
||||
type MsgTmplStatus int8
|
||||
|
||||
const (
|
||||
TmplStatusEnable MsgTmplStatus = 1 // 启用状态
|
||||
TmplStatusDisable MsgTmplStatus = -1 // 禁用状态
|
||||
)
|
||||
|
||||
// MsgTmplChannel 消息模板渠道关联
|
||||
type MsgTmplChannel struct {
|
||||
model.CreateModelNLD
|
||||
TmplId uint64 `json:"tmplId" gorm:"not null;"` // 模板id
|
||||
ChannelId uint64 `json:"channelId" gorm:"not null;"` // 渠道id
|
||||
}
|
||||
|
||||
func (a *MsgTmplChannel) TableName() string {
|
||||
return "t_msg_tmpl_channel"
|
||||
}
|
||||
18
server/internal/msg/domain/entity/msg_tmpl_biz.go
Normal file
18
server/internal/msg/domain/entity/msg_tmpl_biz.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
// MsgTmplBiz 消息模板关联业务信息
|
||||
type MsgTmplBiz struct {
|
||||
model.Model
|
||||
|
||||
TmplId uint64 `json:"tmplId" gorm:"not null;"` // 模板id
|
||||
BizId uint64 `json:"bizId" gorm:"not null;"` // 业务id
|
||||
BizType string `json:"bizType" gorm:"size:32;not null;"` // 业务类型
|
||||
}
|
||||
|
||||
func (a *MsgTmplBiz) TableName() string {
|
||||
return "t_msg_tmpl_biz"
|
||||
}
|
||||
13
server/internal/msg/domain/repository/msg_channel.go
Normal file
13
server/internal/msg/domain/repository/msg_channel.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MsgChannel interface {
|
||||
base.Repo[*entity.MsgChannel]
|
||||
|
||||
GetPageList(condition *entity.MsgChannel, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
}
|
||||
17
server/internal/msg/domain/repository/msg_tmpl.go
Normal file
17
server/internal/msg/domain/repository/msg_tmpl.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MsgTmpl interface {
|
||||
base.Repo[*entity.MsgTmpl]
|
||||
|
||||
GetPageList(condition *entity.MsgTmpl, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
}
|
||||
|
||||
type MsgTmplChannel interface {
|
||||
base.Repo[*entity.MsgTmplChannel]
|
||||
}
|
||||
10
server/internal/msg/domain/repository/msg_tmpl_biz.go
Normal file
10
server/internal/msg/domain/repository/msg_tmpl_biz.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
)
|
||||
|
||||
type MsgTmplBiz interface {
|
||||
base.Repo[*entity.MsgTmplBiz]
|
||||
}
|
||||
12
server/internal/msg/imsg/en.go
Normal file
12
server/internal/msg/imsg/en.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package imsg
|
||||
|
||||
import "mayfly-go/pkg/i18n"
|
||||
|
||||
var En = map[i18n.MsgId]string{
|
||||
LogMsgChannelSave: "Message channel- save",
|
||||
LogMsgChannelDelete: "Message channel- delete",
|
||||
|
||||
LogMsgTmplSave: "Message template- save",
|
||||
LogMsgTmplDelete: "Message template- delete",
|
||||
LogMsgTmplSend: "Message template- send",
|
||||
}
|
||||
20
server/internal/msg/imsg/imsg.go
Normal file
20
server/internal/msg/imsg/imsg.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package imsg
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/pkg/i18n"
|
||||
)
|
||||
|
||||
func init() {
|
||||
i18n.AppendLangMsg(i18n.Zh_CN, Zh_CN)
|
||||
i18n.AppendLangMsg(i18n.En, En)
|
||||
}
|
||||
|
||||
const (
|
||||
LogMsgChannelSave = iota + consts.ImsgNumMsg
|
||||
LogMsgChannelDelete
|
||||
|
||||
LogMsgTmplSave
|
||||
LogMsgTmplDelete
|
||||
LogMsgTmplSend
|
||||
)
|
||||
12
server/internal/msg/imsg/zh_cn.go
Normal file
12
server/internal/msg/imsg/zh_cn.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package imsg
|
||||
|
||||
import "mayfly-go/pkg/i18n"
|
||||
|
||||
var Zh_CN = map[i18n.MsgId]string{
|
||||
LogMsgChannelSave: "消息渠道-保存",
|
||||
LogMsgChannelDelete: "消息渠道-删除",
|
||||
|
||||
LogMsgTmplSave: "消息模板-保存",
|
||||
LogMsgTmplDelete: "消息模板-删除",
|
||||
LogMsgTmplSend: "消息模板-发送",
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type msgChannelRepoImpl struct {
|
||||
base.RepoImpl[*entity.MsgChannel]
|
||||
}
|
||||
|
||||
func newMsgChannelRepo() repository.MsgChannel {
|
||||
return &msgChannelRepoImpl{}
|
||||
}
|
||||
|
||||
func (m *msgChannelRepoImpl) GetPageList(condition *entity.MsgChannel, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
pd := model.NewCond().
|
||||
Eq("id", condition.Id).
|
||||
Like("code", condition.Code).
|
||||
OrderBy(orderBy...)
|
||||
return m.PageByCondToAny(pd, pageParam, toEntity)
|
||||
}
|
||||
32
server/internal/msg/infrastructure/persistence/msg_tmpl.go
Normal file
32
server/internal/msg/infrastructure/persistence/msg_tmpl.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type msgTmplRepoImpl struct {
|
||||
base.RepoImpl[*entity.MsgTmpl]
|
||||
}
|
||||
|
||||
func newMsgTmplRepo() repository.MsgTmpl {
|
||||
return &msgTmplRepoImpl{}
|
||||
}
|
||||
|
||||
func (m *msgTmplRepoImpl) GetPageList(condition *entity.MsgTmpl, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
pd := model.NewCond().
|
||||
Eq("id", condition.Id).
|
||||
Like("code", condition.Code).
|
||||
OrderBy(orderBy...)
|
||||
return m.PageByCondToAny(pd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
type msgTmplChannelRepoImpl struct {
|
||||
base.RepoImpl[*entity.MsgTmplChannel]
|
||||
}
|
||||
|
||||
func newMsgTmplChannelRepo() repository.MsgTmplChannel {
|
||||
return &msgTmplChannelRepoImpl{}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
)
|
||||
|
||||
type msgTmplBizRepoImpl struct {
|
||||
base.RepoImpl[*entity.MsgTmplBiz]
|
||||
}
|
||||
|
||||
func newMsgTmplBizRepo() repository.MsgTmplBiz {
|
||||
return &msgTmplBizRepoImpl{}
|
||||
}
|
||||
@@ -6,4 +6,8 @@ import (
|
||||
|
||||
func InitIoc() {
|
||||
ioc.Register(newMsgRepo(), ioc.WithComponentName("MsgRepo"))
|
||||
ioc.Register(newMsgChannelRepo(), ioc.WithComponentName("MsgChannelRepo"))
|
||||
ioc.Register(newMsgTmplRepo(), ioc.WithComponentName("MsgTmplRepo"))
|
||||
ioc.Register(newMsgTmplChannelRepo(), ioc.WithComponentName("MsgTmplChannelRepo"))
|
||||
ioc.Register(newMsgTmplBizRepo(), ioc.WithComponentName("MsgTmplBizRepo"))
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/initialize"
|
||||
"mayfly-go/internal/event"
|
||||
"mayfly-go/internal/msg/api"
|
||||
"mayfly-go/internal/msg/application"
|
||||
"mayfly-go/internal/msg/application/dto"
|
||||
"mayfly-go/internal/msg/infrastructure/persistence"
|
||||
"mayfly-go/pkg/eventbus"
|
||||
"mayfly-go/pkg/global"
|
||||
"mayfly-go/pkg/ioc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -13,4 +19,14 @@ func init() {
|
||||
application.InitIoc()
|
||||
api.InitIoc()
|
||||
})
|
||||
|
||||
initialize.AddInitFunc(Init)
|
||||
}
|
||||
|
||||
func Init() {
|
||||
msgTmplBizApp := ioc.Get[application.MsgTmplBiz]("MsgTmplBizApp")
|
||||
|
||||
global.EventBus.SubscribeAsync(event.EventTopicBizMsgTmplSend, "BizMsgTmplSend", func(ctx context.Context, event *eventbus.Event) error {
|
||||
return msgTmplBizApp.Send(ctx, event.Val.(dto.BizMsgTmplSend))
|
||||
}, false)
|
||||
}
|
||||
|
||||
84
server/internal/msg/msgx/msgx.go
Normal file
84
server/internal/msg/msgx/msgx.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package msgx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MsgType int8
|
||||
type ChannelType string
|
||||
|
||||
const (
|
||||
MsgTypeText MsgType = 1
|
||||
MsgTypeMarkdown MsgType = 2
|
||||
MsgTypeHtml MsgType = 3
|
||||
)
|
||||
|
||||
const (
|
||||
ChannelTypeEmail ChannelType = "email"
|
||||
ChannelTypeDingBot ChannelType = "dingBot"
|
||||
ChannelTypeQywxBot ChannelType = "qywxBot"
|
||||
ChannelTypeFeishuBot ChannelType = "feishuBot"
|
||||
)
|
||||
|
||||
const (
|
||||
ReceiverKey = "receiver"
|
||||
)
|
||||
|
||||
// Send 发送消息
|
||||
func Send(channel *Channel, msg *Msg) error {
|
||||
sender, err := GetMsgSender(channel.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sender.Send(channel, msg)
|
||||
}
|
||||
|
||||
type Receiver struct {
|
||||
model.ExtraData
|
||||
|
||||
Mobile string
|
||||
Email string
|
||||
}
|
||||
|
||||
type Msg struct {
|
||||
model.ExtraData
|
||||
|
||||
Title string // 消息title
|
||||
Type MsgType // 消息类型
|
||||
Content string // 消息内容
|
||||
Params map[string]any // 消息参数(替换消息中的占位符)
|
||||
|
||||
Receivers []Receiver // 消息接收人
|
||||
}
|
||||
|
||||
// Channel 消息发送渠道信息
|
||||
type Channel struct {
|
||||
model.ExtraData
|
||||
|
||||
Type ChannelType // 渠道类型
|
||||
Name string
|
||||
URL string
|
||||
}
|
||||
|
||||
// MsgSender 定义消息发送接口
|
||||
type MsgSender interface {
|
||||
// Send 发送消息
|
||||
Send(channel *Channel, msg *Msg) error
|
||||
}
|
||||
|
||||
var messageSenders = make(map[ChannelType]MsgSender)
|
||||
|
||||
// RegisterMsgSender 注册消息发送器
|
||||
func RegisterMsgSender(channel ChannelType, sender MsgSender) {
|
||||
messageSenders[channel] = sender
|
||||
}
|
||||
|
||||
// GetMsgSender 获取消息发送器
|
||||
func GetMsgSender(channel ChannelType) (MsgSender, error) {
|
||||
sender, ok := messageSenders[channel]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported message channel %s", channel)
|
||||
}
|
||||
return sender, nil
|
||||
}
|
||||
107
server/internal/msg/msgx/sender/ding_bot.go
Normal file
107
server/internal/msg/msgx/sender/ding_bot.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/httpx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"strings"
|
||||
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type dingBotMsgReq struct {
|
||||
MsgType string `json:"msgtype"`
|
||||
Text struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"text"`
|
||||
Markdown struct {
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
} `json:"markdown"`
|
||||
At struct {
|
||||
// AtUserIds []string `json:"atUserIds"`
|
||||
AtMobiles []string `json:"atMobiles"`
|
||||
IsAtAll bool `json:"isAtAll"`
|
||||
} `json:"at"`
|
||||
}
|
||||
|
||||
type dingBotMsgResp struct {
|
||||
Code int `json:"errcode"`
|
||||
Message string `json:"errmsg"`
|
||||
}
|
||||
|
||||
// DingBotSender 钉钉机器人消息发送
|
||||
type DingBotSender struct{}
|
||||
|
||||
func (d DingBotSender) Send(channel *msgx.Channel, msg *msgx.Msg) error {
|
||||
// https://open.dingtalk.com/document/robots/custom-robot-access#title-72m-8ag-pqw
|
||||
msgReq := dingBotMsgReq{}
|
||||
|
||||
params := msg.Params
|
||||
receiver := collx.ArrayMapFilter(msg.Receivers, func(a msgx.Receiver) (string, bool) {
|
||||
return a.Mobile, a.Mobile != ""
|
||||
})
|
||||
|
||||
if len(receiver) > 0 {
|
||||
msgReq.At.AtMobiles = receiver
|
||||
// 替换文本中的receiver,使用@mobile用于@指定用户
|
||||
params[msgx.ReceiverKey] = strings.Join(collx.ArrayMap(receiver, func(a string) string { return "@" + a }), "")
|
||||
} else {
|
||||
msgReq.At.IsAtAll = true
|
||||
params[msgx.ReceiverKey] = ""
|
||||
}
|
||||
|
||||
content, err := stringx.TemplateResolve(msg.Content, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if msg.Type == msgx.MsgTypeMarkdown {
|
||||
msgReq.MsgType = "markdown"
|
||||
msgReq.Markdown.Title = msg.Title
|
||||
msgReq.Markdown.Text = content
|
||||
} else {
|
||||
msgReq.MsgType = "text"
|
||||
msgReq.Text.Content = content
|
||||
}
|
||||
|
||||
timestamp := time.Now().UnixMilli()
|
||||
sign, err := d.sign(channel.GetExtraString("secret"), timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var res dingBotMsgResp
|
||||
err = httpx.NewReq(fmt.Sprintf("%s×tamp=%d&sign=%s", channel.URL, timestamp, sign)).
|
||||
PostObj(msgReq).
|
||||
BodyTo(&res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return errors.New(res.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DingBotSender) sign(secret string, timestamp int64) (string, error) {
|
||||
// https://open.dingtalk.com/document/robots/customize-robot-security-settings
|
||||
// timestamp + key -> sha256 -> URL encode
|
||||
stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
_, err := h.Write([]byte(stringToSign))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
signature = url.QueryEscape(signature)
|
||||
return signature, nil
|
||||
}
|
||||
100
server/internal/msg/msgx/sender/email.go
Normal file
100
server/internal/msg/msgx/sender/email.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
)
|
||||
|
||||
type EmailSender struct{}
|
||||
|
||||
func (e EmailSender) Send(channel *msgx.Channel, msg *msgx.Msg) error {
|
||||
return e.SendEmail(channel, msg)
|
||||
}
|
||||
|
||||
func (e EmailSender) SendEmail(channel *msgx.Channel, msg *msgx.Msg) error {
|
||||
subject := msg.Title
|
||||
content, err := stringx.TemplateResolve(msg.Content, msg.Params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
to := collx.ArrayMapFilter(msg.Receivers, func(a msgx.Receiver) (string, bool) {
|
||||
return a.Email, a.Email != ""
|
||||
})
|
||||
|
||||
if len(to) == 0 {
|
||||
return errors.New("no receiver")
|
||||
}
|
||||
|
||||
systemName := "mayfly-go"
|
||||
|
||||
serverAndPort := strings.Split(channel.URL, ":")
|
||||
smtpServer := serverAndPort[0]
|
||||
smtpPort := 465
|
||||
if len(serverAndPort) == 2 {
|
||||
smtpPort = cast.ToInt(serverAndPort[1])
|
||||
}
|
||||
|
||||
smtpAccount := channel.GetExtraString("smtpAccount")
|
||||
smtpPassword := channel.GetExtraString("smtpPassword")
|
||||
|
||||
encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject)))
|
||||
mail := []byte(fmt.Sprintf("To: %s\r\n"+
|
||||
"From: %s<%s>\r\n"+
|
||||
"Subject: %s\r\n"+
|
||||
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
||||
strings.Join(to, ";"), systemName, smtpAccount, encodedSubject, content))
|
||||
auth := smtp.PlainAuth("", smtpAccount, smtpPassword, smtpServer)
|
||||
addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort)
|
||||
|
||||
if smtpPort == 465 {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: smtpServer,
|
||||
}
|
||||
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", smtpServer, smtpPort), tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := smtp.NewClient(conn, smtpServer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = client.Mail(smtpAccount); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, receiver := range to {
|
||||
if err = client.Rcpt(receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(mail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err = smtp.SendMail(addr, auth, smtpAccount, to, mail)
|
||||
}
|
||||
return err
|
||||
}
|
||||
100
server/internal/msg/msgx/sender/feishu_bot.go
Normal file
100
server/internal/msg/msgx/sender/feishu_bot.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/httpx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"strings"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
)
|
||||
|
||||
type feishuBotMsgReq struct {
|
||||
MsgType string `json:"msg_type"`
|
||||
Content struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
type feishuBotMsgResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
}
|
||||
|
||||
// FeishuBotSender 发送飞书机器人消息
|
||||
type FeishuBotSender struct{}
|
||||
|
||||
func (f FeishuBotSender) Send(channel *msgx.Channel, msg *msgx.Msg) error {
|
||||
// https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
|
||||
msgReq := feishuBotMsgReq{
|
||||
MsgType: "text",
|
||||
}
|
||||
|
||||
params := msg.Params
|
||||
receiver := `<at user_id="all"></at>`
|
||||
// 使用receiver参数替换消息内容中可能存在的接收人信息
|
||||
if len(msg.Receivers) > 0 {
|
||||
if to := collx.ArrayMapFilter(msg.Receivers, func(a msgx.Receiver) (string, bool) {
|
||||
if uid := a.GetExtraString("feishuUserId"); uid != "" {
|
||||
// 使用<at user_id="userId"></at>
|
||||
return fmt.Sprintf(`<at user_id="%s"></at>`, uid), true
|
||||
}
|
||||
return "", false
|
||||
}); len(to) > 0 {
|
||||
receiver = strings.Join(to, "")
|
||||
}
|
||||
}
|
||||
params[msgx.ReceiverKey] = receiver
|
||||
content, err := stringx.TemplateResolve(msg.Content, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msgReq.Content.Text = content
|
||||
|
||||
if secret := channel.GetExtraString("secret"); secret != "" {
|
||||
timestamp := time.Now().Unix()
|
||||
if sign, err := f.sign(secret, timestamp); err != nil {
|
||||
return err
|
||||
} else {
|
||||
msgReq.Sign = sign
|
||||
}
|
||||
msgReq.Timestamp = cast.ToString(timestamp)
|
||||
}
|
||||
|
||||
var res feishuBotMsgResp
|
||||
err = httpx.NewReq(channel.URL).
|
||||
PostObj(msgReq).
|
||||
BodyTo(&res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return errors.New(res.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e FeishuBotSender) sign(secret string, timestamp int64) (string, error) {
|
||||
//timestamp + key 做sha256, 再进行base64 encode
|
||||
stringToSign := fmt.Sprintf("%v", timestamp) + "\n" + secret
|
||||
var data []byte
|
||||
h := hmac.New(sha256.New, []byte(stringToSign))
|
||||
_, err := h.Write(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
return signature, nil
|
||||
}
|
||||
82
server/internal/msg/msgx/sender/qywx_bot.go
Normal file
82
server/internal/msg/msgx/sender/qywx_bot.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/httpx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type qywxBotMsgReq struct {
|
||||
MsgType string `json:"msgtype"`
|
||||
Text struct {
|
||||
Content string `json:"content"`
|
||||
MentionedMobileList []string `json:"mentioned_mobile_list"`
|
||||
} `json:"text"`
|
||||
Markdown struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"markdown"`
|
||||
}
|
||||
|
||||
type qywxBotMsgResp struct {
|
||||
Code int `json:"errcode"`
|
||||
Message string `json:"errmsg"`
|
||||
}
|
||||
|
||||
// QywxBotSender 企业微信机器人消息发送
|
||||
type QywxBotSender struct{}
|
||||
|
||||
func (e QywxBotSender) Send(channel *msgx.Channel, msg *msgx.Msg) error {
|
||||
// https://developer.work.weixin.qq.com/document/path/91770
|
||||
msgReq := qywxBotMsgReq{}
|
||||
|
||||
params := msg.Params
|
||||
receiver := ""
|
||||
// 使用receiver参数替换消息内容中可能存在的接收人信息
|
||||
if len(msg.Receivers) > 0 {
|
||||
if to := collx.ArrayMapFilter(msg.Receivers, func(a msgx.Receiver) (string, bool) {
|
||||
if uid := a.GetExtraString("qywxUserId"); uid != "" {
|
||||
// 使用<@userId>用于@指定用户
|
||||
return fmt.Sprintf("<@%s>", uid), true
|
||||
}
|
||||
return "", false
|
||||
}); len(to) > 0 {
|
||||
receiver = strings.Join(to, "")
|
||||
}
|
||||
}
|
||||
params[msgx.ReceiverKey] = receiver
|
||||
content, err := stringx.TemplateResolve(msg.Content, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if msg.Type == msgx.MsgTypeMarkdown {
|
||||
msgReq.MsgType = "markdown"
|
||||
msgReq.Markdown.Content = content
|
||||
// msgReq.Markdown.MentionedMobileList = receivers // markdown不支持@人,需要使用<@userId>
|
||||
} else {
|
||||
msgReq.MsgType = "text"
|
||||
msgReq.Text.Content = content
|
||||
|
||||
// receivers := msg.Receivers
|
||||
// if len(msg.Receivers) == 0 {
|
||||
// receivers = []string{"@all"}
|
||||
// }
|
||||
|
||||
// msgReq.Text.MentionedMobileList = receivers
|
||||
}
|
||||
|
||||
var res qywxBotMsgResp
|
||||
err = httpx.NewReq(channel.URL).PostObj(msgReq).BodyTo(&res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return errors.New(res.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
10
server/internal/msg/msgx/sender/sender.go
Normal file
10
server/internal/msg/msgx/sender/sender.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package sender
|
||||
|
||||
import "mayfly-go/internal/msg/msgx"
|
||||
|
||||
func init() {
|
||||
msgx.RegisterMsgSender(msgx.ChannelTypeEmail, EmailSender{})
|
||||
msgx.RegisterMsgSender(msgx.ChannelTypeDingBot, DingBotSender{})
|
||||
msgx.RegisterMsgSender(msgx.ChannelTypeQywxBot, QywxBotSender{})
|
||||
msgx.RegisterMsgSender(msgx.ChannelTypeFeishuBot, FeishuBotSender{})
|
||||
}
|
||||
@@ -1,15 +1,25 @@
|
||||
package form
|
||||
|
||||
import "mayfly-go/pkg/model"
|
||||
|
||||
type AccountCreateForm struct {
|
||||
model.ExtraData
|
||||
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name" binding:"required,max=16" msg:"required=name cannot be blank,max=The maximum length of a name cannot exceed 16 characters"`
|
||||
Username string `json:"username" binding:"pattern=account_username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type AccountUpdateForm struct {
|
||||
model.ExtraData
|
||||
|
||||
Name string `json:"name" binding:"max=16"` // 姓名
|
||||
Username string `json:"username" binding:"omitempty,pattern=account_username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Password *string `json:"password"`
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,12 @@ import (
|
||||
|
||||
type AccountManageVO struct {
|
||||
model.Model
|
||||
model.ExtraData
|
||||
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email"`
|
||||
Status entity.AccountStatus `json:"status"`
|
||||
LastLoginTime *time.Time `json:"lastLoginTime"`
|
||||
OtpSecret string `json:"otpSecret"`
|
||||
@@ -19,6 +23,8 @@ type SimpleAccountVO struct {
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email"`
|
||||
|
||||
Roles []*AccountRoleVO `json:"roles" gorm:"-"`
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ func (m *syslogAppImpl) AppendLog(logId uint64, appendLog *AppendLogReq) {
|
||||
syslog.Resp = fmt.Sprintf("%s\n%s", syslog.Resp, appendLogMsg)
|
||||
syslog.Type = appendLog.Type
|
||||
if len(appendLog.Extra) > 0 {
|
||||
existExtra := jsonx.ToMap(syslog.Extra)
|
||||
existExtra, _ := jsonx.ToMap(syslog.Extra)
|
||||
syslog.Extra = jsonx.ToStr(collx.MapMerge(existExtra, appendLog.Extra))
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func (m *syslogAppImpl) SetExtra(logId uint64, key string, val any) {
|
||||
syslog = sl
|
||||
}
|
||||
|
||||
extraMap := jsonx.ToMap(syslog.Extra)
|
||||
extraMap, _ := jsonx.ToMap(syslog.Extra)
|
||||
if extraMap == nil {
|
||||
extraMap = make(map[string]any)
|
||||
}
|
||||
|
||||
@@ -10,9 +10,12 @@ import (
|
||||
|
||||
type Account struct {
|
||||
model.Model
|
||||
model.ExtraData
|
||||
|
||||
Name string `json:"name" gorm:"size:30;not null;"`
|
||||
Username string `json:"username" gorm:"size:30;not null;"`
|
||||
Mobile string `json:"mobile" gorm:"size:20;"`
|
||||
Email string `json:"email" gorm:"size:100;"`
|
||||
Password string `json:"-" gorm:"size:64;not null;"`
|
||||
Status AccountStatus `json:"status" gorm:"not null;"`
|
||||
LastLoginTime *time.Time `json:"lastLoginTime"`
|
||||
|
||||
@@ -22,10 +22,8 @@ func init() {
|
||||
}
|
||||
|
||||
func Init() {
|
||||
|
||||
global.EventBus.SubscribeAsync(event.EventTopicResourceOp, "ResourceOpLogApp", func(ctx context.Context, event *eventbus.Event) error {
|
||||
codePath := event.Val.(string)
|
||||
return application.GetResourceOpLogApp().AddResourceOpLog(ctx, codePath)
|
||||
}, false)
|
||||
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func run(db *gorm.DB, fs ...func() []*gormigrate.Migration) error {
|
||||
IDColumnName: "id",
|
||||
IDColumnSize: 300,
|
||||
UseTransaction: true,
|
||||
ValidateUnknownMigrations: true,
|
||||
ValidateUnknownMigrations: false,
|
||||
}, ms)
|
||||
if err := m.Migrate(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -2,7 +2,11 @@ package migrations
|
||||
|
||||
import (
|
||||
machineentity "mayfly-go/internal/machine/domain/entity"
|
||||
msgentity "mayfly-go/internal/msg/domain/entity"
|
||||
sysentity "mayfly-go/internal/sys/domain/entity"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"time"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
@@ -11,6 +15,7 @@ import (
|
||||
func V1_9() []*gormigrate.Migration {
|
||||
var migrations []*gormigrate.Migration
|
||||
migrations = append(migrations, V1_9_3()...)
|
||||
migrations = append(migrations, V1_9_4()...)
|
||||
return migrations
|
||||
}
|
||||
|
||||
@@ -40,3 +45,167 @@ func V1_9_3() []*gormigrate.Migration {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func V1_9_4() []*gormigrate.Migration {
|
||||
return []*gormigrate.Migration{
|
||||
{
|
||||
ID: "20250213-v1.9.4-addMsg",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
tx.AutoMigrate(&sysentity.Account{})
|
||||
tx.AutoMigrate(&msgentity.MsgTmpl{}, &msgentity.MsgTmplChannel{}, &msgentity.MsgChannel{}, &msgentity.MsgTmplBiz{})
|
||||
|
||||
la := &model.LoginAccount{Id: 1, Username: "admin"}
|
||||
// 创建审批默认消息模板
|
||||
processMsgTmplCode := "7u2MRCaB"
|
||||
if err := tx.Where("code = ?", processMsgTmplCode).First(&msgentity.MsgTmpl{}).Error; err != nil {
|
||||
tmplRemark := "工单审批通知模板"
|
||||
msgTmpl := &msgentity.MsgTmpl{
|
||||
Code: processMsgTmplCode,
|
||||
Name: "工单审批通知",
|
||||
Tmpl: `{{.receiver}}
|
||||
您有新的工单需要审批
|
||||
发起人:{{.creator}}
|
||||
工单标题:{{.procdefName}}
|
||||
备注:{{.procinstRemark}}
|
||||
业务编号:{{.bizKey}}`,
|
||||
Title: "工单审批",
|
||||
MsgType: 1,
|
||||
Status: 1,
|
||||
Remark: &tmplRemark,
|
||||
}
|
||||
msgTmpl.FillBaseInfo(model.IdGenTypeNone, la)
|
||||
if err := tx.Create(msgTmpl).Error; err != nil {
|
||||
logx.ErrorTrace("create msg tmpl error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
resources := []*sysentity.Resource{
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742816076}}}},
|
||||
Pid: 0,
|
||||
UiPath: "ckg5ICnd/",
|
||||
Name: "menu.msgManage",
|
||||
Code: "/msg",
|
||||
Type: 1,
|
||||
Meta: `{"icon":"Message","isKeepAlive":true,"routeName":"msg"}`,
|
||||
Weight: 60000000,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742816279}}}},
|
||||
Pid: 1742816076,
|
||||
UiPath: "ckg5ICnd/eKQ8qAlH/",
|
||||
Name: "menu.channel",
|
||||
Code: "channels",
|
||||
Type: 1,
|
||||
Meta: `{"component":"msg/channel/ChannelList","icon":"Message","isKeepAlive":true,"routeName":"ChannelList"}`,
|
||||
Weight: 1742816279,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742876893}}}},
|
||||
Pid: 1742816279,
|
||||
UiPath: "ckg5ICnd/eKQ8qAlH/p2Xi8asv/",
|
||||
Name: "menu.msgChannelBase",
|
||||
Code: "msg:channel:base",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742823660,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742823661}}}},
|
||||
Pid: 1742816279,
|
||||
UiPath: "ckg5ICnd/eKQ8qAlH/Iu82rFKW/",
|
||||
Name: "menu.saveMsgChannel",
|
||||
Code: "msg:channel:save",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742823661,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742826138}}}},
|
||||
Pid: 1742816279,
|
||||
UiPath: "ckg5ICnd/eKQ8qAlH/Y4kRzNJp/",
|
||||
Name: "menu.delMsgChannel",
|
||||
Code: "msg:channel:del",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742826138,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742876469}}}},
|
||||
Pid: 1742816076,
|
||||
UiPath: "ckg5ICnd/XiJf38uW/",
|
||||
Name: "menu.msgTmpl",
|
||||
Code: "tmpls",
|
||||
Type: 1,
|
||||
Meta: `{"component":"msg/tmpl/TmplList","icon":"Message","isKeepAlive":true,"routeName":"TmplList"}`,
|
||||
Weight: 1742876469,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742876795}}}},
|
||||
Pid: 1742876469,
|
||||
UiPath: "ckg5ICnd/XiJf38uW/ExV9tz2l/",
|
||||
Name: "menu.saveMsgTmpl",
|
||||
Code: "msg:tmpl:save",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742876795,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742876813}}}},
|
||||
Pid: 1742876469,
|
||||
UiPath: "ckg5ICnd/XiJf38uW/2y7drhga/",
|
||||
Name: "menu.delMsgTmpl",
|
||||
Code: "msg:tmpl:del",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742876813,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742876922}}}},
|
||||
Pid: 1742876469,
|
||||
UiPath: "ckg5ICnd/XiJf38uW/VRX9YtM3/",
|
||||
Name: "menu.msgTmplBase",
|
||||
Code: "msg:tmpl:base",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742876794,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742912893}}}},
|
||||
Pid: 1742876469,
|
||||
UiPath: "ckg5ICnd/XiJf38uW/42PkAmLB/",
|
||||
Name: "menu.sendMsg",
|
||||
Code: "msg:tmpl:send",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742912893,
|
||||
},
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, r := range resources {
|
||||
if err := tx.Where("ui_path = ?", r.UiPath).First(&sysentity.Resource{}).Error; err == nil {
|
||||
continue
|
||||
}
|
||||
r.Status = 1
|
||||
r.CreateTime = &now
|
||||
r.UpdateTime = &now
|
||||
r.Creator = la.Username
|
||||
r.Modifier = la.Username
|
||||
r.CreatorId = la.Id
|
||||
r.ModifierId = la.Id
|
||||
if err := tx.Create(r).Error; err != nil {
|
||||
logx.ErrorTrace("create msg resource menu error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import "fmt"
|
||||
|
||||
const (
|
||||
AppName = "mayfly-go"
|
||||
Version = "v1.9.3"
|
||||
Version = "v1.9.4"
|
||||
)
|
||||
|
||||
func GetAppInfo() string {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package httpclient
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// 默认超时
|
||||
const DefTimeout = 60
|
||||
|
||||
type RequestWrapper struct {
|
||||
type Req struct {
|
||||
client http.Client
|
||||
url string
|
||||
method string
|
||||
@@ -37,16 +37,16 @@ type MultipartFile struct {
|
||||
}
|
||||
|
||||
// 创建一个请求
|
||||
func NewRequest(url string) *RequestWrapper {
|
||||
return &RequestWrapper{url: url, client: http.Client{}}
|
||||
func NewReq(url string) *Req {
|
||||
return &Req{url: url, client: http.Client{}}
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) Url(url string) *RequestWrapper {
|
||||
func (r *Req) Url(url string) *Req {
|
||||
r.url = url
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) Header(name, value string) *RequestWrapper {
|
||||
func (r *Req) Header(name, value string) *Req {
|
||||
if r.header == nil {
|
||||
r.header = make(map[string]string)
|
||||
}
|
||||
@@ -54,12 +54,12 @@ func (r *RequestWrapper) Header(name, value string) *RequestWrapper {
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) Timeout(timeout int) *RequestWrapper {
|
||||
func (r *Req) Timeout(timeout int) *Req {
|
||||
r.timeout = timeout
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) GetByQuery(queryMap collx.M) *ResponseWrapper {
|
||||
func (r *Req) GetByQuery(queryMap collx.M) *Resp {
|
||||
var params string
|
||||
for k, v := range queryMap {
|
||||
if params != "" {
|
||||
@@ -71,13 +71,13 @@ func (r *RequestWrapper) GetByQuery(queryMap collx.M) *ResponseWrapper {
|
||||
return r.Get()
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) Get() *ResponseWrapper {
|
||||
func (r *Req) Get() *Resp {
|
||||
r.method = "GET"
|
||||
r.body = nil
|
||||
return sendRequest(r)
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) PostJson(body string) *ResponseWrapper {
|
||||
func (r *Req) PostJson(body string) *Resp {
|
||||
buf := bytes.NewBufferString(body)
|
||||
r.method = "POST"
|
||||
r.body = buf
|
||||
@@ -88,15 +88,15 @@ func (r *RequestWrapper) PostJson(body string) *ResponseWrapper {
|
||||
return sendRequest(r)
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) PostObj(body any) *ResponseWrapper {
|
||||
func (r *Req) PostObj(body any) *Resp {
|
||||
marshal, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return &ResponseWrapper{err: errors.New("解析json obj错误")}
|
||||
return &Resp{err: errors.New("解析json obj错误")}
|
||||
}
|
||||
return r.PostJson(string(marshal))
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) PostForm(params string) *ResponseWrapper {
|
||||
func (r *Req) PostForm(params string) *Resp {
|
||||
buf := bytes.NewBufferString(params)
|
||||
r.method = "POST"
|
||||
r.body = buf
|
||||
@@ -107,7 +107,7 @@ func (r *RequestWrapper) PostForm(params string) *ResponseWrapper {
|
||||
return sendRequest(r)
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) PostMulipart(files []MultipartFile, reqParams collx.M) *ResponseWrapper {
|
||||
func (r *Req) PostMulipart(files []MultipartFile, reqParams collx.M) *Resp {
|
||||
buf := &bytes.Buffer{}
|
||||
// 文件写入 buf
|
||||
writer := multipart.NewWriter(buf)
|
||||
@@ -117,7 +117,7 @@ func (r *RequestWrapper) PostMulipart(files []MultipartFile, reqParams collx.M)
|
||||
if uploadFile.FilePath != "" {
|
||||
file, err := os.Open(uploadFile.FilePath)
|
||||
if err != nil {
|
||||
return &ResponseWrapper{err: err}
|
||||
return &Resp{err: err}
|
||||
}
|
||||
defer file.Close()
|
||||
reader = file
|
||||
@@ -127,18 +127,18 @@ func (r *RequestWrapper) PostMulipart(files []MultipartFile, reqParams collx.M)
|
||||
|
||||
part, err := writer.CreateFormFile(uploadFile.FieldName, uploadFile.FileName)
|
||||
if err != nil {
|
||||
return &ResponseWrapper{err: err}
|
||||
return &Resp{err: err}
|
||||
}
|
||||
io.Copy(part, reader)
|
||||
}
|
||||
// 如果有其他参数,则写入body
|
||||
for k, v := range reqParams {
|
||||
if err := writer.WriteField(k, cast.ToString(v)); err != nil {
|
||||
return &ResponseWrapper{err: err}
|
||||
return &Resp{err: err}
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return &ResponseWrapper{err: err}
|
||||
return &Resp{err: err}
|
||||
}
|
||||
|
||||
r.method = "POST"
|
||||
@@ -150,8 +150,8 @@ func (r *RequestWrapper) PostMulipart(files []MultipartFile, reqParams collx.M)
|
||||
return sendRequest(r)
|
||||
}
|
||||
|
||||
func sendRequest(rw *RequestWrapper) *ResponseWrapper {
|
||||
respWrapper := &ResponseWrapper{}
|
||||
func sendRequest(rw *Req) *Resp {
|
||||
respWrapper := &Resp{}
|
||||
timeout := rw.timeout
|
||||
if timeout > 0 {
|
||||
rw.client.Timeout = time.Duration(timeout) * time.Second
|
||||
@@ -166,37 +166,37 @@ func sendRequest(rw *RequestWrapper) *ResponseWrapper {
|
||||
}
|
||||
setRequestHeader(req, rw.header)
|
||||
resp, err := rw.client.Do(req)
|
||||
return &ResponseWrapper{resp: resp, err: err}
|
||||
return &Resp{resp: resp, err: err}
|
||||
}
|
||||
|
||||
func setRequestHeader(req *http.Request, header map[string]string) {
|
||||
req.Header.Set("User-Agent", "golang/mayfly")
|
||||
req.Header.Set("User-Agent", "golang/mayfly-go")
|
||||
for k, v := range header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
type ResponseWrapper struct {
|
||||
type Resp struct {
|
||||
resp *http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
// 将响应体通过json解析转为指定结构体
|
||||
func (r *ResponseWrapper) BodyToObj(objPtr any) error {
|
||||
// BodyTo 将响应体通过json解析转为指定结构体
|
||||
func (r *Resp) BodyTo(ptr any) error {
|
||||
bodyBytes, err := r.BodyBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(bodyBytes, &objPtr)
|
||||
err = json.Unmarshal(bodyBytes, &ptr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析响应体-json解析失败-%s", err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 将响应体转为strings
|
||||
func (r *ResponseWrapper) BodyToString() (string, error) {
|
||||
// BodyToString 将响应体转为strings
|
||||
func (r *Resp) BodyToString() (string, error) {
|
||||
bodyBytes, err := r.BodyBytes()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -204,21 +204,20 @@ func (r *ResponseWrapper) BodyToString() (string, error) {
|
||||
return string(bodyBytes), nil
|
||||
}
|
||||
|
||||
// 将响应体通过json解析转为map
|
||||
func (r *ResponseWrapper) BodyToMap() (map[string]any, error) {
|
||||
// BodyToMap 将响应体通过json解析转为map
|
||||
func (r *Resp) BodyToMap() (map[string]any, error) {
|
||||
var res map[string]any
|
||||
return res, r.BodyToObj(&res)
|
||||
return res, r.BodyTo(&res)
|
||||
}
|
||||
|
||||
// 获取响应体的字节数组
|
||||
func (r *ResponseWrapper) BodyBytes() ([]byte, error) {
|
||||
resp, err := r.GetHttpResp()
|
||||
// BodyBytes 获取响应体的字节数组
|
||||
func (r *Resp) BodyBytes() ([]byte, error) {
|
||||
bodyReader, err := r.BodyReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
defer resp.Body.Close()
|
||||
defer bodyReader.Close()
|
||||
body, err := io.ReadAll(bodyReader)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应体数据失败-%s", err.Error())
|
||||
@@ -226,8 +225,18 @@ func (r *ResponseWrapper) BodyBytes() ([]byte, error) {
|
||||
return body, err
|
||||
}
|
||||
|
||||
// 获取http响应结果结构体
|
||||
func (r *ResponseWrapper) GetHttpResp() (*http.Response, error) {
|
||||
// BodyReader 获取响应体的reader
|
||||
func (r *Resp) BodyReader() (io.ReadCloser, error) {
|
||||
resp, err := r.GetHttpResp()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// GetHttpResp 获取http响应结果结构体
|
||||
func (r *Resp) GetHttpResp() (*http.Response, error) {
|
||||
if r.err != nil {
|
||||
return nil, fmt.Errorf("请求失败-%s", r.err.Error())
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user