refactor: 消息模块重构,infra包路径简写等

This commit is contained in:
meilin.huang
2025-07-27 21:02:48 +08:00
parent e96379b6c0
commit 6ad6c69660
149 changed files with 969 additions and 1098 deletions

View File

@@ -8,3 +8,7 @@ VITE_OPEN = false
VITE_PUBLIC_PATH = ''
VITE_EDITOR=idea
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash

View File

@@ -5,7 +5,3 @@ VITE_OPEN = true
# 本地环境接口地址
VITE_API_URL = '/api'
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash

View File

@@ -3,7 +3,3 @@ ENV = 'production'
# 线上环境接口地址
VITE_API_URL = '/api'
# 路由模式
# Optional: hash | history
VITE_ROUTER_MODE = hash

View File

@@ -24,7 +24,7 @@
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"element-plus": "^2.10.3",
"element-plus": "^2.10.4",
"js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"mitt": "^3.0.1",
@@ -39,8 +39,8 @@
"sql-formatter": "^15.6.5",
"trzsz": "^1.1.5",
"uuid": "^9.0.1",
"vue": "^3.5.17",
"vue-i18n": "^11.1.9",
"vue": "^v3.6.0-alpha.2",
"vue-i18n": "^11.1.11",
"vue-router": "^4.5.1",
"vuedraggable": "^4.1.0"
},
@@ -56,7 +56,6 @@
"@vue/compiler-sfc": "^3.5.17",
"autoprefixer": "^10.4.21",
"code-inspector-plugin": "^0.20.12",
"dotenv": "^16.5.0",
"eslint": "^9.29.0",
"eslint-plugin-vue": "^10.2.0",
"postcss": "^8.5.6",

View File

@@ -1,43 +0,0 @@
class SocketBuilder {
websocket: WebSocket;
constructor(url: string) {
if (typeof WebSocket === 'undefined') {
throw new Error('不支持websocket');
}
if (!url) {
throw new Error('websocket url不能为空');
}
this.websocket = new WebSocket(url);
}
static builder(url: string) {
return new SocketBuilder(url);
}
open(onopen: any) {
this.websocket.onopen = onopen;
return this;
}
error(onerror: any) {
this.websocket.onerror = onerror;
return this;
}
message(onmessage: any) {
this.websocket.onmessage = onmessage;
return this;
}
close(onclose: any) {
this.websocket.onclose = onclose;
return this;
}
build() {
return this.websocket;
}
}
export default SocketBuilder;

View File

@@ -42,3 +42,27 @@ export const TagResourceTypePath = {
Db: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}/${TagResourceTypeEnum.Db.value}`,
Es: `${TagResourceTypeEnum.EsInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
};
// 消息子类型
export const MsgSubtypeEnum = {
UserLogin: EnumValue.of('user.login', 'login.login').setExtra({
notifyType: 'primary',
}),
MachineFileUploadSuccess: EnumValue.of('machine.file.upload.success', 'machine.fileUploadSuccess').setExtra({
notifyType: 'success',
}),
MachineFileUploadFail: EnumValue.of('machine.file.upload.fail', 'machine.fileUploadFail').setExtra({
notifyType: 'error',
}),
DbDumpFail: EnumValue.of('db.dump.fail', 'db.dbDumpFail').setExtra({
notifyType: 'error',
}),
SqlScriptRunSuccess: EnumValue.of('db.sqlscript.run.success', 'db.sqlScriptRunSuccess').setExtra({
notifyType: 'success',
}),
SqlScriptRunFail: EnumValue.of('db.sqlscript.run.fail', 'db.sqlScriptRunFail').setExtra({
notifyType: 'error',
}),
};

View File

@@ -1,5 +1,8 @@
import CryptoJS from 'crypto-js';
import { getToken } from '@/common/utils/storage';
import openApi from './openApi';
import JSEncrypt from 'jsencrypt';
import { notBlank } from './assert';
/**
* AES 加密数据
@@ -36,3 +39,36 @@ export function AesDecrypt(word: string, key?: string): string {
return decrypted.toString(CryptoJS.enc.Base64);
}
var encryptor: any = null;
export async function getRsaPublicKey() {
let publicKey = sessionStorage.getItem('RsaPublicKey');
if (publicKey) {
return publicKey;
}
publicKey = (await openApi.getPublicKey()) as string;
sessionStorage.setItem('RsaPublicKey', publicKey);
return publicKey;
}
/**
* 公钥加密指定值
*
* @param value value
* @returns 加密后的值
*/
export async function RsaEncrypt(value: any) {
// 不存在则返回空值
if (!value) {
return '';
}
if (encryptor != null && sessionStorage.getItem('RsaPublicKey') != null) {
return encryptor.encrypt(value);
}
encryptor = new JSEncrypt();
const publicKey = (await getRsaPublicKey()) as string;
notBlank(publicKey, '获取公钥失败');
encryptor.setPublicKey(publicKey); //设置公钥
return encryptor.encrypt(value);
}

View File

@@ -204,6 +204,24 @@ function getApiUrl(url: string) {
return baseUrl + url + '?' + joinClientParams();
}
/**
* 创建 websocket
*/
export const createWebSocket = (url: string): Promise<WebSocket> => {
return new Promise<WebSocket>((resolve, reject) => {
const clientParam = (url.includes('?') ? '&' : '?') + joinClientParams();
const socket = new WebSocket(`${config.baseWsUrl}${url}${clientParam}`);
socket.onopen = () => {
resolve(socket);
};
socket.onerror = (e) => {
reject(e);
};
});
};
// 组装客户端参数,包括 token 和 clientId
export function joinClientParams(): string {
return `token=${getToken()}&clientId=${getClientId()}`;

View File

@@ -1,36 +0,0 @@
import openApi from './openApi';
import JSEncrypt from 'jsencrypt';
import { notBlank } from './assert';
var encryptor: any = null;
export async function getRsaPublicKey() {
let publicKey = sessionStorage.getItem('RsaPublicKey');
if (publicKey) {
return publicKey;
}
publicKey = (await openApi.getPublicKey()) as string;
sessionStorage.setItem('RsaPublicKey', publicKey);
return publicKey;
}
/**
* 公钥加密指定值
*
* @param value value
* @returns 加密后的值
*/
export async function RsaEncrypt(value: any) {
// 不存在则返回空值
if (!value) {
return '';
}
if (encryptor != null && sessionStorage.getItem('RsaPublicKey') != null) {
return encryptor.encrypt(value);
}
encryptor = new JSEncrypt();
const publicKey = (await getRsaPublicKey()) as string;
notBlank(publicKey, '获取公钥失败');
encryptor.setPublicKey(publicKey); //设置公钥
return encryptor.encrypt(value);
}

View File

@@ -4,15 +4,15 @@ import { h, reactive } from 'vue';
import { ElNotification } from 'element-plus';
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
export function initSysMsgs() {
registerDbSqlExecProgress();
export async function initSysMsgs() {
await registerDbSqlExecProgress();
}
const sqlExecNotifyMap: Map<string, any> = new Map();
function registerDbSqlExecProgress() {
syssocket.registerMsgHandler('execSqlFileProgress', function (message: any) {
const content = JSON.parse(message.msg);
async function registerDbSqlExecProgress() {
await syssocket.registerMsgHandler('sqlScriptRunProgress', function (message: any) {
const content = message.params;
const id = content.id;
let progress = sqlExecNotifyMap.get(id);
if (content.terminated) {
@@ -38,7 +38,7 @@ function registerDbSqlExecProgress() {
duration: 0,
title: message.title,
message: h(ProgressNotify, progress.props),
type: syssocket.getMsgType(message.type),
type: 'info',
showClose: false,
});
sqlExecNotifyMap.set(id, progress);

View File

@@ -1,34 +1,25 @@
import Config from './config';
import SocketBuilder from './SocketBuilder';
import { getToken } from '@/common/utils/storage';
import { joinClientParams } from './request';
import { createWebSocket } from './request';
import { ElNotification } from 'element-plus';
import { MsgSubtypeEnum } from './commonEnum';
import EnumValue from './Enum';
class SysSocket {
/**
* socket连接
*/
socket: any;
socket: WebSocket | null = null;
/**
* key -> 消息类别value -> 消息对应的处理器函数
*/
categoryHandlers: Map<string, any> = new Map();
/**
* 消息类型
*/
messageTypes: any = {
0: 'error',
1: 'success',
2: 'info',
};
/**
* 初始化全局系统消息websocket
*/
init() {
async init() {
// 存在则不需要重新建立连接
if (this.socket) {
return;
@@ -38,9 +29,9 @@ class SysSocket {
return null;
}
console.log('init system ws');
const sysMsgUrl = `${Config.baseWsUrl}/sysmsg?${joinClientParams()}`;
this.socket = SocketBuilder.builder(sysMsgUrl)
.message((event: { data: string }) => {
try {
this.socket = await createWebSocket('/sysmsg');
this.socket.onmessage = async (event: { data: string }) => {
let message;
try {
message = JSON.parse(event.data);
@@ -56,23 +47,32 @@ class SysSocket {
return;
}
// 默认通知处理
const type = this.getMsgType(message.type);
let msg = message.msg;
let duration = 0;
const msgSubtype = EnumValue.getEnumByValue(MsgSubtypeEnum, message.subtype);
if (!msgSubtype) {
console.log(`not found msg subtype: ${message.subtype}`);
return;
}
// 动态导入 i18n 或延迟获取 i18n 实例
let title = '';
try {
// 方式1: 动态导入
const { i18n } = await import('@/i18n');
title = i18n.global.t(msgSubtype?.label);
} catch (e) {
console.warn('i18n not ready, using default title');
}
ElNotification({
duration: duration,
title: message.title,
message: msg,
type: type,
duration: 0,
title,
message: message.msg,
type: msgSubtype?.extra.notifyType || 'info',
});
})
.open((event: any) => console.log(event))
.close(() => {
console.log('close sys socket');
this.socket = null;
})
.build();
};
} catch (e) {
console.error('open system ws error', e);
}
}
destory() {
@@ -87,8 +87,7 @@ class SysSocket {
* @param category 消息类别
* @param handlerFunc 消息处理函数
*/
registerMsgHandler(category: any, handlerFunc: any) {
this.init();
async registerMsgHandler(category: any, handlerFunc: any) {
if (this.categoryHandlers.has(category)) {
console.log(`${category}该类别消息处理器已存在...`);
return;
@@ -98,10 +97,6 @@ class SysSocket {
}
this.categoryHandlers.set(category, handlerFunc);
}
getMsgType(msgType: any) {
return this.messageTypes[msgType];
}
}
// 全局系统消息websocket;

View File

@@ -1,241 +0,0 @@
/**
* 2020.11.29 lyt 整理
* 工具类集合,适用于平时开发
*/
// 小数或整数(不可以负数)
export function verifyNumberIntegerAndFloat(val: string) {
// 匹配空格
let v = val.replace(/(^\s*)|(\s*$)/g, '');
// 只能是数字和小数点,不能是其他输入
v = v.replace(/[^\d.]/g, '');
// 以0开始只能输入一个
v = v.replace(/^0{2}$/g, '0');
// 保证第一位只能是数字,不能是点
v = v.replace(/^\./g, '');
// 小数只能出现1位
v = v.replace('.', '$#$').replace(/\./g, '').replace('$#$', '.');
// 小数点后面保留2位
v = v.replace(/^(\-)*(\d+)\.(\d\d).*$/, '$1$2.$3');
// 返回结果
return v;
}
// 正整数验证
export function verifiyNumberInteger(val: string) {
// 匹配空格
let v = val.replace(/(^\s*)|(\s*$)/g, '');
// 去掉 '.' , 防止贴贴的时候出现问题 如 0.1.12.12
v = v.replace(/[\.]*/g, '');
// 去掉以 0 开始后面的数, 防止贴贴的时候出现问题 如 00121323
v = v.replace(/(^0[\d]*)$/g, '0');
// 首位是0,只能出现一次
v = v.replace(/^0\d$/g, '0');
// 只匹配数字
v = v.replace(/[^\d]/g, '');
// 返回结果
return v;
}
// 去掉中文及空格
export function verifyCnAndSpace(val: string) {
// 匹配中文与空格
let v = val.replace(/[\u4e00-\u9fa5\s]+/g, '');
// 匹配空格
v = v.replace(/(^\s*)|(\s*$)/g, '');
// 返回结果
return v;
}
// 去掉英文及空格
export function verifyEnAndSpace(val: string) {
// 匹配英文与空格
let v = val.replace(/[a-zA-Z]+/g, '');
// 匹配空格
v = v.replace(/(^\s*)|(\s*$)/g, '');
// 返回结果
return v;
}
// 禁止输入空格
export function verifyAndSpace(val: string) {
// 匹配空格
let v = val.replace(/(^\s*)|(\s*$)/g, '');
// 返回结果
return v;
}
// 金额用 `,` 区分开
export function verifyNumberComma(val: string) {
// 调用小数或整数(不可以负数)方法
let v: any = verifyNumberIntegerAndFloat(val);
// 字符串转成数组
v = v.toString().split('.');
// \B 匹配非单词边界,两边都是单词字符或者两边都是非单词字符
v[0] = v[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// 数组转字符串
v = v.join('.');
// 返回结果
return v;
}
// 匹配文字变色(搜索时)
export function verifyTextColor(val: string, text = '', color = 'red') {
// 返回内容,添加颜色
let v = text.replace(new RegExp(val, 'gi'), `<span style='color: ${color}'>${val}</span>`);
// 返回结果
return v;
}
// 数字转中文大写
export function verifyNumberCnUppercase(val: any, unit = '仟佰拾亿仟佰拾万仟佰拾元角分', v = '') {
// 当前内容字符串添加 2个0为什么??
val += '00';
// 返回某个指定的字符串值在字符串中首次出现的位置,没有出现,则该方法返回 -1
let lookup = val.indexOf('.');
// substring不包含结束下标内容substr包含结束下标内容
if (lookup >= 0) val = val.substring(0, lookup) + val.substr(lookup + 1, 2);
// 根据内容 val 的长度,截取返回对应大写
unit = unit.substr(unit.length - val.length);
// 循环截取拼接大写
for (let i = 0; i < val.length; i++) {
v += '零壹贰叁肆伍陆柒捌玖'.substr(val.substr(i, 1), 1) + unit.substr(i, 1);
}
// 正则处理
v = v
.replace(/零角零分$/, '整')
.replace(/零[仟佰拾]/g, '零')
.replace(/零{2,}/g, '零')
.replace(/零([亿|万])/g, '$1')
.replace(/零+元/, '元')
.replace(/亿零{0,3}万/, '亿')
.replace(/^元/, '零元');
// 返回结果
return v;
}
// 手机号码
export function verifyPhone(val: string) {
// false: 手机号码不正确
if (!/^((12[0-9])|(13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\d{8}$/.test(val)) return false;
// true: 手机号码正确
else return true;
}
// 国内电话号码
export function verifyTelPhone(val: string) {
// false: 国内电话号码不正确
if (!/\d{3}-\d{8}|\d{4}-\d{7}/.test(val)) return false;
// true: 国内电话号码正确
else return true;
}
// 登录账号 (字母开头允许5-16字节允许字母数字下划线)
export function verifyAccount(val: string) {
// false: 登录账号不正确
if (!/^[a-zA-Z][a-zA-Z0-9_]{4,15}$/.test(val)) return false;
// true: 登录账号正确
else return true;
}
// 密码 (以字母开头长度在6~16之间只能包含字母、数字和下划线)
export function verifyPassword(val: string) {
// false: 密码不正确
if (!/^[a-zA-Z]\w{5,15}$/.test(val)) return false;
// true: 密码正确
else return true;
}
// 强密码 (字母+数字+特殊字符长度在6-16之间)
export function verifyPasswordPowerful(val: string) {
// false: 强密码不正确
if (!/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val))
return false;
// true: 强密码正确
else return true;
}
// 密码强度
export function verifyPasswordStrength(val: string) {
let v = '';
// 弱:纯数字,纯字母,纯特殊字符
if (/^(?:\d+|[a-zA-Z]+|[!@#$%^&\.*]+){6,16}$/.test(val)) v = '弱';
// 中:字母+数字,字母+特殊字符,数字+特殊字符
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '中';
// 强:字母+数字+特殊字符
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val)) v = '强';
// 返回结果
return v;
}
// IP地址
export function verifyIPAddress(val: string) {
// false: IP地址不正确
if (!/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/.test(val))
return false;
// true: IP地址正确
else return true;
}
// 邮箱
export function verifyEmail(val: string) {
// false: 邮箱不正确
if (
!/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
val
)
)
return false;
// true: 邮箱正确
else return true;
}
// 身份证
export function verifyIdCard(val: string) {
// false: 身份证不正确
if (!/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(val)) return false;
// true: 身份证正确
else return true;
}
// 姓名
export function verifyFullName(val: string) {
// false: 姓名不正确
if (!/^[\u4e00-\u9fa5]{1,6}(·[\u4e00-\u9fa5]{1,6}){0,2}$/.test(val)) return false;
// true: 姓名正确
else return true;
}
// 邮政编码
export function verifyPostalCode(val: string) {
// false: 邮政编码不正确
if (!/^[1-9][0-9]{5}$/.test(val)) return false;
// true: 邮政编码正确
else return true;
}
// url
export function verifyUrl(val: string) {
// false: url不正确
if (
!/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(
val
)
)
return false;
// true: url正确
else return true;
}
// 车牌号
export function verifyCarNum(val: string) {
// false: 车牌号不正确
if (
!/^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[DF])|([DF]([A-HJ-NP-Z0-9])[0-9]{4})))|([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领]))$/.test(
val
)
)
return false;
// true车牌号正确
else return true;
}

View File

@@ -1,13 +0,0 @@
const mode = import.meta.env.VITE_ROUTER_MODE;
/**
* @description 获取不同路由模式所对应的 url
* @returns {String}
*/
export function getNowUrl() {
const url = {
hash: location.hash.substring(1),
history: location.pathname + location.search,
};
return url[mode];
}

View File

@@ -1,27 +0,0 @@
// vite 打包相关
import dotenv from 'dotenv';
export interface ViteEnv {
VITE_PORT: number;
VITE_OPEN: boolean;
VITE_PUBLIC_PATH: string;
VITE_EDITOR: string;
}
export function loadEnv(): ViteEnv {
const env = process.env.NODE_ENV;
const ret: any = {};
const envList = [`.env.${env}.local`, `.env.${env}`, '.env.local', '.env', ,];
envList.forEach((e) => {
dotenv.config({ path: e });
});
for (const envName of Object.keys(process.env)) {
console.log(envName);
let realName = (process.env as any)[envName].replace(/\\n/g, '\n');
realName = realName === 'true' ? true : realName === 'false' ? false : realName;
if (envName === 'VITE_PORT') realName = Number(realName);
if (envName === 'VITE_OPEN') realName = Boolean(realName);
ret[envName] = realName;
process.env[envName] = realName;
}
return ret;
}

View File

@@ -21,7 +21,7 @@
<div class="flex">
<!-- 简易单个搜索项 -->
<div v-if="nowSearchItem" class="flex">
<el-dropdown v-if="searchItems?.length > 1">
<el-dropdown v-if="props.searchItems?.length > 1">
<SvgIcon :size="16" name="CaretBottom" class="!mr-1 !mt-1.5 simple-search-form-btn" />
<template #dropdown>
<el-dropdown-menu>
@@ -54,7 +54,7 @@
<!-- <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="execQuery()" /> -->
<el-button
v-if="showToolButton('search') && searchItems?.length > 1"
v-if="showToolButton('search') && props.searchItems?.length > 1"
:icon="isShowSearch ? 'ArrowDown' : 'ArrowUp'"
circle
@click="isShowSearch = !isShowSearch"

View File

@@ -22,6 +22,7 @@ import { useDebounceFn, useEventListener, useIntervalFn } from '@vueuse/core';
import themes from './themes';
import { TrzszFilter } from 'trzsz';
import { useI18n } from 'vue-i18n';
import { createWebSocket } from '@/common/request';
const { t } = useI18n();
@@ -124,7 +125,7 @@ const initTerm = async () => {
// 注册窗口大小监听器
useEventListener('resize', useDebounceFn(fitTerminal, 400));
initSocket();
await initSocket();
// 注册其他插件
loadAddon();
@@ -140,13 +141,19 @@ const initTerm = async () => {
});
};
const initSocket = () => {
const initSocket = async () => {
if (!props.socketUrl) {
return;
}
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
// 监听socket连接
socket.onopen = () => {
try {
socket = await createWebSocket(`${props.socketUrl}?rows=${term?.rows}&cols=${term?.cols}`);
} catch (e) {
term.writeln(`\r\n\x1b[31m${t('components.terminal.connErrMsg')}`);
state.status = TerminalStatus.Error;
console.log('连接错误', e);
return;
}
// 注册心跳
useIntervalFn(sendPing, 15000);
@@ -159,14 +166,6 @@ const initSocket = () => {
if (props.cmd) {
sendData(props.cmd + ' \r');
}
};
// 监听socket错误信息
socket.onerror = (e: Event) => {
term.writeln(`\r\n\x1b[31m${t('components.terminal.connErrMsg')}`);
state.status = TerminalStatus.Error;
console.log('连接错误', e);
};
socket.onclose = (e: CloseEvent) => {
console.log('terminal socket close...', e.reason);

View File

@@ -16,6 +16,7 @@ export default {
dbFilterPlaceholder: 'DB name: Input filterable',
sqlRecord: 'SQL records',
dump: 'Export',
dbDumpFail: 'DB export failed',
dumpContent: 'Export Content',
structure: 'Structure',
data: 'Data',
@@ -55,6 +56,8 @@ export default {
execSuccess: 'Successful execution',
execFail: 'Execution failure',
sqlScriptRun: 'Run SQL Script',
sqlScriptRunSuccess: 'SQL script executed successfully',
sqlScriptRunFail: 'SQL script execution failed',
saveSql: 'Save SQL',
execInfo: 'Execution info',
result: 'Result',

View File

@@ -138,5 +138,7 @@ export default {
fileTooLargeTips: 'The file is too large, please download and use it',
uploadSuccess: 'Upload successfully',
fileExceedsSysConf: 'The uploaded file exceeds the system configuration [{uploadMaxFileSize}]',
fileUploadSuccess: 'Machine file upload successful',
fileUploadFail: 'Machine file upload failed',
},
};

View File

@@ -16,6 +16,7 @@ export default {
dbFilterPlaceholder: '库名: 输入可过滤',
sqlRecord: 'SQL记录',
dump: '导出',
dbDumpFail: '数据库导出失败',
dumpContent: '导出内容',
structure: '结构',
data: '数据',
@@ -55,6 +56,8 @@ export default {
execSuccess: '执行成功',
execFail: '执行失败',
sqlScriptRun: 'SQL脚本执行',
sqlScriptRunSuccess: 'SQL脚本执行成功',
sqlScriptRunFail: 'SQL脚本执行失败',
saveSql: '保存SQL',
execInfo: '执行信息',
result: '结果',

View File

@@ -139,5 +139,7 @@ export default {
fileTooLargeTips: '文件太大, 请下载使用',
uploadSuccess: '上传成功',
fileExceedsSysConf: '上传的文件超过系统配置的【{uploadMaxFileSize}】',
fileUploadSuccess: '机器文件上传成功',
fileUploadFail: '机器文件上传失败',
},
};

View File

@@ -41,25 +41,27 @@
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
<SvgIcon name="search" :title="$t('layout.user.menuSearch')" />
</div>
<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
<SvgIcon name="setting" :title="$t('layout.user.layoutConf')" />
</div>
<div class="layout-navbars-breadcrumb-user-icon">
<el-popover placement="bottom" trigger="click" :visible="state.isShowUserNewsPopover" :width="300" popper-class="el-popover-pupop-user-news">
<el-popover @show="onShowMsgs" placement="bottom" trigger="click" :width="450">
<template #reference>
<el-badge :is-dot="false" @click="state.isShowUserNewsPopover = !state.isShowUserNewsPopover">
<div class="layout-navbars-breadcrumb-user-icon">
<el-badge :show-zero="false" :value="state.unreadMsgCount">
<SvgIcon name="bell" :title="$t('layout.user.news')" />
</el-badge>
</template>
<transition name="el-zoom-in-top">
<UserNews v-show="state.isShowUserNewsPopover" />
</transition>
</el-popover>
</div>
</template>
<UserNews ref="userNewsRef" @update:count="state.unreadMsgCount = $event" />
</el-popover>
<div class="layout-navbars-breadcrumb-user-icon mr-2" @click="onScreenfullClick">
<SvgIcon v-if="!state.isScreenfull" name="full-screen" :title="$t('layout.user.fullScreenOff')" />
<SvgIcon v-else name="crop" />
</div>
<el-dropdown trigger="click" :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
<span class="layout-navbars-breadcrumb-user-link cursor-pointer">
<img :src="userInfo.photo" class="layout-navbars-breadcrumb-user-link-photo mr-1" />
@@ -79,7 +81,7 @@
</template>
<script setup lang="ts" name="layoutBreadcrumbUser">
import { ref, computed, reactive, onMounted, watch } from 'vue';
import { ref, computed, reactive, onMounted, watch, useTemplateRef } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage } from 'element-plus';
import screenfull from 'screenfull';
@@ -100,10 +102,12 @@ import EnumValue from '@/common/Enum';
const router = useRouter();
const searchRef = ref();
const userNewsRef = useTemplateRef('userNewsRef');
const state = reactive({
isScreenfull: false,
isShowUserNewsPopover: false,
disabledSize: '',
unreadMsgCount: 0,
});
const { userInfo } = storeToRefs(useUserInfo());
const themeConfigStore = useThemeConfig();
@@ -126,8 +130,15 @@ onMounted(() => {
initComponentSize();
isDark.value = themeConfig.isDark;
}
// 获取未读消息数量
state.unreadMsgCount = 0;
});
const onShowMsgs = () => {
userNewsRef.value?.loadMsgs(true);
};
// 全屏点击时
const onScreenfullClick = () => {
if (!screenfull.isEnabled) {

View File

@@ -1,117 +1,132 @@
<template>
<div class="layout-navbars-breadcrumb-user-news">
<div class="head-box">
<div class="head-box-title">{{ $t('layout.user.newTitle') }}</div>
<div class="head-box-btn" v-if="newsList.length > 0" @click="onAllReadClick">{{ $t('layout.user.newBtn') }}</div>
<div class="flex flex-col w-full rounded-md shadow-sm">
<!-- Header -->
<div class="flex items-center justify-between border-b border-gray-200 px-4 py-2 text-sm text-gray-700">
<div class="font-medium">{{ $t('layout.user.newTitle') }}</div>
<div v-if="unreadCount > 0" class="color-primary cursor-pointer opacity-80 transition-opacity hover:opacity-100" @click="onRead()">
{{ $t('layout.user.newBtn') }}
</div>
<div class="content-box">
<template v-if="newsList.length > 0">
<div class="content-box-item" v-for="(v, k) in newsList" :key="k">
<div>{{ v.label }}</div>
<div class="content-box-msg">
{{ v.value }}
</div>
<div class="content-box-time">{{ v.time }}</div>
<!-- Content -->
<el-scrollbar height="350px" v-loading="loadingMsgs" class="px-4 py-2 text-sm">
<template v-if="msgs.length > 0">
<div
v-for="(v, k) in msgs"
:key="k"
class="pt-1 mt-0.5"
:style="{ backgroundColor: v.status == 1 ? 'var(--el-color-info-light-9)' : 'transparent' }"
@click="onRead(v)"
>
<div class="flex justify-between items-start">
<el-text size="small" tag="b" :type="EnumValue.getEnumByValue(MsgSubtypeEnum, v.subtype)?.extra?.notifyType">
{{ $t(EnumValue.getEnumByValue(MsgSubtypeEnum, v.subtype)?.label || '') }}
</el-text>
</div>
<div class="text-gray-500 mt-1 mb-1">{{ v.msg }}</div>
<div class="text-gray-500">{{ formatDate(v.createTime) }}</div>
<div class="mt-2 border-t border-gray-200"></div>
</div>
<el-button class="w-full mt-1" size="small" @click="loadMsgs()" v-if="!loadMoreDisable"> {{ $t('redis.loadMore') }} </el-button>
</template>
<el-empty :description="$t('layout.user.newDesc')" v-else></el-empty>
</div>
<div class="foot-box" @click="toMsgCenter" v-if="newsList.length > 0">{{ $t('layout.user.newGo') }}</div>
<el-empty v-if="msgs.length == 0 && !loadingMsgs" :image-size="100" :description="$t('layout.user.newDesc')" />
</el-scrollbar>
<!-- Footer -->
<!-- <div
v-if="msgs.length > 0"
class="color-primary flex h-9 items-center justify-center border-t border-gray-200 text-sm cursor-pointer opacity-80 transition-opacity hover:opacity-100"
@click="toMsgCenter"
>
{{ $t('layout.user.newGo') }}
</div> -->
</div>
</template>
<script lang="ts">
import { reactive, toRefs } from 'vue';
export default {
name: 'layoutBreadcrumbUserNews',
setup() {
const state = reactive({
newsList: [
{
label: '关于学习交流的通知',
value: 'QQ群号码 119699946',
time: '2021-09-08',
},
],
<script lang="ts" setup>
import { MsgSubtypeEnum } from '@/common/commonEnum';
import EnumValue from '@/common/Enum';
import { formatDate } from '@/common/utils/format';
import { personApi } from '@/views/personal/api';
import { useIntervalFn } from '@vueuse/core';
import { onMounted, ref, watchEffect } from 'vue';
const emit = defineEmits(['update:count']);
const msgQuery = ref<any>({
pageNum: 1,
pageSize: 10,
});
const loadMoreDisable = ref(true);
const loadingMsgs = ref(true);
const msgs = ref<Array<any>>([]);
const unreadCount = ref(0);
onMounted(() => {
useIntervalFn(
() => {
// 定时更新未读消息数
personApi.getUnreadMsgCount.request().then((res) => {
unreadCount.value = res;
});
// 全部已读点击
const onAllReadClick = () => {
state.newsList = [];
};
// 前往通知中心点击
const toMsgCenter = () => {};
return {
onAllReadClick,
toMsgCenter,
...toRefs(state),
};
},
10 * 1000,
{ immediate: true, immediateCallback: true }
);
});
watchEffect(() => {
emit('update:count', unreadCount.value);
});
const loadMsgs = async (research: boolean = false) => {
if (research) {
msgQuery.value.pageNum = 1;
msgs.value = [];
}
const msgList = await getMsgs();
msgs.value.push(...msgList.list);
msgQuery.value.pageNum += 1;
loadMoreDisable.value = msgList.total <= msgs.value.length;
};
const getMsgs = async () => {
try {
loadingMsgs.value = true;
return await personApi.getMsgs.request(msgQuery.value);
} catch (e) {
//
} finally {
loadingMsgs.value = false;
}
};
const onRead = async (msg: any = null) => {
if (msg && (msg.status == 1 || !msg.status)) {
return;
}
await personApi.readMsg.request({ id: msg?.id || 0 });
loadMsgs(true);
if (!msg) {
// 如果是全部已读,重置未读消息数
unreadCount.value = 0;
} else {
// 如果是单条已读,减少未读消息数
unreadCount.value = Math.max(unreadCount.value - 1, 0);
}
};
defineExpose({
loadMsgs,
});
const toMsgCenter = () => {};
</script>
<style scoped lang="scss">
.layout-navbars-breadcrumb-user-news {
.head-box {
display: flex;
border-bottom: 1px solid #ebeef5;
box-sizing: border-box;
color: #333333;
justify-content: space-between;
height: 35px;
align-items: center;
.head-box-btn {
color: var(--el-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
}
.content-box {
font-size: 13px;
.content-box-item {
padding-top: 12px;
&:last-of-type {
padding-bottom: 12px;
}
.content-box-msg {
color: #999999;
margin-top: 5px;
margin-bottom: 5px;
}
.content-box-time {
color: #999999;
}
}
}
.foot-box {
height: 35px;
color: var(--el-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid #ebeef5;
&:hover {
opacity: 1;
}
}
::v-deep(.el-empty__description p) {
font-size: 13px;
}
}
</style>
<style scoped lang="scss"></style>

View File

@@ -1,4 +1,3 @@
import { getNowUrl } from '@/common/utils/url';
import { defineStore } from 'pinia';
/**
@@ -12,14 +11,5 @@ export const useTagsViews = defineStore('tagsViews', {
setTagsViews(data: Array<TagsView>) {
this.tagsViews = data;
},
// 设置当前页面的tags view title
setNowTitle(title: string) {
this.tagsViews.forEach((item) => {
// console.log(getNowUrl(), item.path);
if (item.path == getNowUrl()) {
item.title = title;
}
});
},
},
});

View File

@@ -2,7 +2,7 @@
<div class="home-container personal">
<el-row :gutter="15">
<!-- 个人信息 -->
<el-col :xs="24" :sm="16">
<el-col :xs="24" :sm="24">
<el-card shadow="hover" :header="$t('home.personalInfo')">
<div class="personal-user">
<div class="personal-user-left">
@@ -52,23 +52,6 @@
</div>
</el-card>
</el-col>
<!-- 消息通知 -->
<el-col :xs="24" :sm="8" class="pl15 personal-info">
<el-card shadow="hover">
<template #header>
<span>{{ $t('home.msgNotify') }}</span>
<span @click="showMsgs" class="personal-info-more">{{ $t('common.more') }}</span>
</template>
<div class="personal-info-box">
<ul class="personal-info-ul">
<li v-for="(v, k) in state.msgs as any" :key="k" class="personal-info-li">
<a class="personal-info-li-title">{{ `[${$t(EnumValue.getLabelByValue(MsgTypeEnum, v.type))}] ${v.msg}` }}</a>
</li>
</ul>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="!mt-4 resource-info">
@@ -236,39 +219,11 @@
</el-card>
</el-col>
</el-row>
<el-dialog width="900px" :title="$t('common.msg')" v-model="msgDialog.visible">
<el-table border :data="msgDialog.msgs.list" size="small">
<el-table-column property="type" :label="$t('common.type')" width="60">
<template #default="scope">
{{ $t(EnumValue.getLabelByValue(MsgTypeEnum, scope.row.type)) }}
</template>
</el-table-column>
<el-table-column property="msg" :label="$t('common.msg')"></el-table-column>
<el-table-column property="createTime" :label="$t('common.time')" width="150">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
</el-table>
<el-row type="flex" class="mt-1" justify="center">
<el-pagination
small
@current-change="searchMsg"
style="text-align: center"
background
layout="prev, pager, next, total, jumper"
:total="msgDialog.msgs.total"
v-model:current-page="msgDialog.query.pageNum"
:page-size="msgDialog.query.pageSize"
/>
</el-row>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, toRefs } from 'vue';
import { computed, onMounted, reactive } from 'vue';
// import * as echarts from 'echarts';
import { formatAxis, formatDate } from '@/common/utils/format';
import { indexApi } from './api';
@@ -285,8 +240,6 @@ import { getAllTagInfoByCodePaths } from '../ops/component/tag';
import { ElMessage } from 'element-plus';
import { getFileUrl, getUploadFileUrl } from '@/common/request';
import { saveUser } from '@/common/utils/storage';
import EnumValue from '../../common/Enum';
import { MsgTypeEnum } from './enums';
const router = useRouter();
const { userInfo } = storeToRefs(useUserInfo());
@@ -296,17 +249,6 @@ const state = reactive({
roles: [],
},
msgs: [],
msgDialog: {
visible: false,
query: {
pageSize: 10,
pageNum: 1,
},
msgs: {
list: [],
total: null,
},
},
resourceOpTableHeight: 180,
defaultLogSize: 5,
machine: {
@@ -331,8 +273,6 @@ const state = reactive({
},
});
const { msgDialog } = toRefs(state);
const roleInfo = computed(() => {
if (state.accountInfo.roles.length == 0) {
return '';
@@ -349,30 +289,12 @@ const currentTime = computed(() => {
onMounted(() => {
initData();
getAccountInfo();
getMsgs().then((res) => {
state.msgs = res.list;
});
});
const showMsgs = async () => {
state.msgDialog.query.pageNum = 1;
searchMsg();
state.msgDialog.visible = true;
};
const searchMsg = async () => {
state.msgDialog.msgs = await getMsgs();
};
const getAccountInfo = async () => {
state.accountInfo = await personApi.accountInfo.request();
};
const getMsgs = async () => {
return await personApi.getMsgs.request(state.msgDialog.query);
};
const beforeAvatarUpload = (rawFile: any) => {
if (rawFile.size >= 512 * 1024) {
ElMessage.error('头像不能超过512KB!');

View File

@@ -1,6 +0,0 @@
import { EnumValue } from '@/common/Enum';
export const MsgTypeEnum = {
Login: EnumValue.of(1, 'home.msgTypeLogin'),
Notify: EnumValue.of(2, 'home.msgTypeNotify'),
};

View File

@@ -151,7 +151,7 @@ import { ElMessage } from 'element-plus';
import { initRouter } from '@/router/index';
import { getRefreshToken, saveRefreshToken, saveToken, saveUser } from '@/common/utils/storage';
import openApi from '@/common/openApi';
import { RsaEncrypt } from '@/common/rsa';
import { RsaEncrypt } from '@/common/crypto';
import { getAccountLoginSecurity, getLdapEnabled } from '@/common/sysconfig';
import { letterAvatar } from '@/common/utils/string';
import { useUserInfo } from '@/store/userInfo';

View File

@@ -1,5 +1,4 @@
import Api from '@/common/Api';
import config from '@/common/config';
import { joinClientParams } from '@/common/request';
export const machineApi = {
@@ -67,9 +66,9 @@ export const cmdConfApi = {
};
export function getMachineTerminalSocketUrl(authCertName: any) {
return `${config.baseWsUrl}/machines/terminal/${authCertName}?${joinClientParams()}`;
return `/machines/terminal/${authCertName}`;
}
export function getMachineRdpSocketUrl(authCertName: any) {
return `${config.baseWsUrl}/machines/rdp/${authCertName}`;
return `/machines/rdp/${authCertName}`;
}

View File

@@ -5,5 +5,7 @@ export const personApi = {
updateAccount: Api.newPut('/sys/accounts/self'),
authStatus: Api.newGet('/auth/oauth2/status'),
getMsgs: Api.newGet('/msgs/self'),
getUnreadMsgCount: Api.newGet('/msgs/self/unread/count'),
readMsg: Api.newGet('/msgs/self/read'),
unbindOauth2: Api.newGet('/auth/oauth2/unbind'),
};

View File

@@ -1,45 +1,40 @@
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import type { UserConfig } from 'vite';
import { loadEnv } from './src/common/utils/viteBuild';
import { CodeInspectorPlugin } from 'code-inspector-plugin';
import progress from 'vite-plugin-progress';
import tailwindcss from '@tailwindcss/vite';
import { ConfigEnv, defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }: ConfigEnv) => {
const env = loadEnv(mode, process.cwd(), '');
const isProd = process.env.NODE_ENV === 'production';
const pathResolve = (dir: string): any => {
return resolve(__dirname, '.', dir);
};
const { VITE_PORT, VITE_OPEN, VITE_PUBLIC_PATH, VITE_EDITOR } = loadEnv();
const isProd = process.env.NODE_ENV === 'production';
const alias: Record<string, string> = {
return {
base: isProd ? env.VITE_PUBLIC_PATH : './',
resolve: {
alias: {
'@': pathResolve('src/'),
};
const viteConfig: UserConfig = {
},
},
plugins: [
vue(),
tailwindcss(),
CodeInspectorPlugin({
bundler: 'vite',
editor: VITE_EDITOR as any,
editor: env.VITE_EDITOR as any,
}),
progress(),
],
root: process.cwd(),
resolve: {
alias,
},
base: isProd ? VITE_PUBLIC_PATH : './',
optimizeDeps: {
include: ['element-plus/es/locale/lang/zh-cn'],
},
server: {
host: '0.0.0.0',
port: VITE_PORT,
open: VITE_OPEN,
port: Number.parseInt(env.VITE_PORT) || 8889,
open: env.VITE_OPEN === 'true',
proxy: {
'/api': {
target: 'http://localhost:18888',
@@ -53,15 +48,15 @@ const viteConfig: UserConfig = {
chunkSizeWarningLimit: 1500,
rollupOptions: {
output: {
entryFileNames: `assets/[hash]-[name].js`,
chunkFileNames: `assets/[hash]-[name].js`,
assetFileNames: `assets/[hash]-[name].[ext]`,
entryFileNames: `assets/js/[hash]-[name].js`,
chunkFileNames: `assets/js/[hash]-[name].js`,
assetFileNames: `assets/[ext]/[hash]-[name].[ext]`,
hashCharacters: 'hex',
advancedChunks: {
groups: [
{ name: 'vue-vendor', test: /[\\/]node_modules[\\/](vue|@vue|vue-router|pinia)[\\/]/ },
{ name: 'echarts', test: /(echarts)/i },
{ name: 'monaco', test: /(monaco-editor)/i },
{ name: 'charts', test: /[\\/]node_modules[\\/](echarts)[\\/]/ },
{ name: 'monaco', test: /[\\/]node_modules[\\/]monaco-editor[\\/]/ },
],
},
},
@@ -89,5 +84,4 @@ const viteConfig: UserConfig = {
},
},
};
export default viteConfig;
});

View File

@@ -10,16 +10,16 @@ require (
github.com/gin-gonic/gin v1.10.1
github.com/glebarez/sqlite v1.11.0
github.com/go-gormigrate/gormigrate/v2 v2.1.4
github.com/go-ldap/ldap/v3 v3.4.8
github.com/go-ldap/ldap/v3 v3.4.11
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.26.0
github.com/go-playground/validator/v10 v10.27.0
github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/golang-jwt/jwt/v5 v5.2.3
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20250508043914-ed57fa5c5274
github.com/microsoft/go-mssqldb v1.9.1
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20250630080345-f9402614f6ba
github.com/microsoft/go-mssqldb v1.9.2
github.com/mojocn/base64Captcha v1.3.8 //
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.9
@@ -32,33 +32,32 @@ require (
github.com/tidwall/gjson v1.18.0
github.com/veops/go-ansiterm v0.0.5
go.mongodb.org/mongo-driver/v2 v2.2.2 // mongo
golang.org/x/crypto v0.39.0 // ssh
golang.org/x/crypto v0.40.0 // ssh
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.15.0
golang.org/x/sync v0.16.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
// gorm
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.30.0
gorm.io/gorm v1.30.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
@@ -66,15 +65,16 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
@@ -83,20 +83,20 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
golang.org/x/arch v0.14.0 // indirect
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
golang.org/x/arch v0.19.0 // indirect
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
golang.org/x/image v0.29.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
modernc.org/libc v1.66.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.38.1 // indirect
)

View File

@@ -47,7 +47,7 @@ func (a *AccountLogin) ReqConfs() *req.Confs {
// @router /auth/accounts/login [post]
func (a *AccountLogin) Login(rc *req.Ctx) {
loginForm := req.BindJsonAndValid[*form.LoginForm](rc)
loginForm := req.BindJson[*form.LoginForm](rc)
ctx := rc.MetaCtx
accountLoginSecurity := config.GetAccountLoginSecurity()
@@ -96,7 +96,7 @@ type OtpVerifyInfo struct {
// OTP双因素校验
func (a *AccountLogin) OtpVerify(rc *req.Ctx) {
otpVerify := req.BindJsonAndValid[*form.OtpVerfiy](rc)
otpVerify := req.BindJson[*form.OtpVerfiy](rc)
ctx := rc.MetaCtx
tokenKey := fmt.Sprintf("otp:token:%s", otpVerify.OtpToken)

View File

@@ -6,13 +6,13 @@ import (
"mayfly-go/internal/auth/config"
"mayfly-go/internal/auth/imsg"
"mayfly-go/internal/auth/pkg/otp"
msgapp "mayfly-go/internal/msg/application"
msgentity "mayfly-go/internal/msg/domain/entity"
msgdto "mayfly-go/internal/msg/application/dto"
"mayfly-go/internal/pkg/event"
sysapp "mayfly-go/internal/sys/application"
sysentity "mayfly-go/internal/sys/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/cache"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/global"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/netx"
@@ -114,14 +114,12 @@ func saveLogin(ctx context.Context, account *sysentity.Account, ip string) {
// 偷懒为了方便直接获取accountApp
biz.ErrIsNil(sysapp.GetAccountApp().Update(context.TODO(), updateAccount))
// 创建登录消息
loginMsg := &msgentity.Msg{
RecipientId: int64(account.Id),
Msg: i18n.TC(ctx, imsg.LoginMsg, "ip", ip, "time", timex.DefaultFormat(now)),
Type: 1,
}
loginMsg.CreateTime = &now
loginMsg.Creator = account.Username
loginMsg.CreatorId = account.Id
msgapp.GetMsgApp().Create(context.TODO(), loginMsg)
global.EventBus.Publish(ctx, event.EventTopicMsgTmplSend, &msgdto.MsgTmplSendEvent{
TmplChannel: msgdto.MsgTmplLogin,
Params: collx.M{
"ip": ip,
"time": timex.DefaultFormat(now),
},
ReceiverIds: []uint64{account.Id},
})
}

View File

@@ -47,7 +47,7 @@ func (a *LdapLogin) GetLdapEnabled(rc *req.Ctx) {
// @router /auth/ldap/login [post]
func (a *LdapLogin) Login(rc *req.Ctx) {
loginForm := req.BindJsonAndValid[*form.LoginForm](rc)
loginForm := req.BindJson[*form.LoginForm](rc)
ctx := rc.MetaCtx
accountLoginSecurity := config.GetAccountLoginSecurity()
// 判断是否有开启登录验证码校验
@@ -197,14 +197,14 @@ func dial(ldapConf *config.LdapLogin) (*ldap.Conn, error) {
InsecureSkipVerify: ldapConf.SkipTLSVerify,
}
if ldapConf.SecurityProtocol == "LDAPS" {
conn, err := ldap.DialTLS("tcp", addr, tlsConfig)
conn, err := ldap.DialURL("ldaps://"+addr, ldap.DialWithTLSConfig(tlsConfig))
if err != nil {
return nil, errors.Errorf("dial TLS: %v", err)
}
return conn, nil
}
conn, err := ldap.Dial("tcp", addr)
conn, err := ldap.DialURL("ldap://" + addr)
if err != nil {
return nil, errors.Errorf("dial: %v", err)
}

View File

@@ -15,7 +15,6 @@ var En = map[i18n.MsgId]string{
ErrOtpCheckRestrict: "Two-factor validation failed more than 5 times. Try again in 10 minutes",
ErrOtpCheckFail: "Two-factor authentication authorization code is incorrect",
ErrAccountNotAvailable: "Account is not available",
LoginMsg: "Log in to [{{.ip}}]-[{{.time}}]",
ErrUsernameOrPwdErr: "Wrong username or password",
ErrOauth2NoAutoRegister: "the system does not enable automatic registration, please ask the administrator to add the corresponding account first",
}

View File

@@ -23,7 +23,6 @@ const (
ErrOtpCheckRestrict
ErrOtpCheckFail
ErrAccountNotAvailable
LoginMsg
ErrUsernameOrPwdErr
ErrOauth2NoAutoRegister
)

View File

@@ -15,7 +15,6 @@ var Zh_CN = map[i18n.MsgId]string{
ErrOtpCheckRestrict: "双因素校验失败超过5次, 请10分钟后再试",
ErrOtpCheckFail: "双因素认证授权码不正确",
ErrAccountNotAvailable: "账号不可用",
LoginMsg: "于[{{.ip}}]-[{{.time}}]登录",
ErrUsernameOrPwdErr: "用户名或密码错误",
ErrOauth2NoAutoRegister: "系统未开启自动注册, 请先让管理员添加对应账号",
}

View File

@@ -4,7 +4,7 @@ import (
"mayfly-go/initialize"
"mayfly-go/internal/auth/api"
"mayfly-go/internal/auth/application"
"mayfly-go/internal/auth/infrastructure/persistence"
"mayfly-go/internal/auth/infra/persistence"
)
func init() {

View File

@@ -11,15 +11,13 @@ import (
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/imsg"
"mayfly-go/internal/event"
msgapp "mayfly-go/internal/msg/application"
msgdto "mayfly-go/internal/msg/application/dto"
"mayfly-go/internal/pkg/event"
"mayfly-go/internal/pkg/utils"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/global"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
@@ -36,7 +34,6 @@ type Db struct {
instanceApp application.Instance `inject:"T"`
dbApp application.Db `inject:"T"`
dbSqlExecApp application.DbSqlExec `inject:"T"`
msgApp msgapp.Msg `inject:"T"`
tagApp tagapp.TagTree `inject:"T"`
}
@@ -135,7 +132,7 @@ func (d *Db) DeleteDb(rc *req.Ctx) {
/** 数据库操作相关、执行sql等 ***/
func (d *Db) ExecSql(rc *req.Ctx) {
form := req.BindJsonAndValid[*form.DbSqlExecForm](rc)
form := req.BindJson[*form.DbSqlExecForm](rc)
ctx, cancel := context.WithTimeout(rc.MetaCtx, time.Duration(config.GetDbms().SqlExecTl)*time.Second)
defer cancel()
@@ -167,17 +164,6 @@ func (d *Db) ExecSql(rc *req.Ctx) {
rc.ResData = execRes
}
// progressCategory sql文件执行进度消息类型
const progressCategory = "execSqlFileProgress"
// progressMsg sql文件执行进度消息
type progressMsg struct {
Id string `json:"id"`
Title string `json:"title"`
ExecutedStatements int `json:"executedStatements"`
Terminated bool `json:"terminated"`
}
// 执行sql文件
func (d *Db) ExecSqlFile(rc *req.Ctx) {
multipart, err := rc.GetRequest().MultipartReader()
@@ -246,7 +232,11 @@ func (d *Db) DumpSql(rc *req.Ctx) {
if len(msg) > 0 {
msg = "DB dump error: " + msg
rc.GetWriter().Write([]byte(msg))
d.msgApp.CreateAndSend(la, msgdto.ErrSysMsg(i18n.T(imsg.DbDumpErr), msg))
global.EventBus.Publish(rc.MetaCtx, event.EventTopicMsgTmplSend, &msgdto.MsgTmplSendEvent{
TmplChannel: msgdto.MsgTmplDbDumpFail,
Params: collx.M{"error": msg},
ReceiverIds: []uint64{la.Id},
})
}
}()

View File

@@ -87,7 +87,7 @@ func (d *DataSyncTask) DeleteTask(rc *req.Ctx) {
}
func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
form := req.BindJsonAndValid[*form.DataSyncTaskStatusForm](rc)
form := req.BindJson[*form.DataSyncTaskStatusForm](rc)
rc.ReqParam = form
task, err := d.dataSyncTaskApp.GetById(form.Id)

View File

@@ -30,7 +30,7 @@ func (d *DbSql) ReqConfs() *req.Confs {
// @router /api/db/:dbId/sql [post]
func (d *DbSql) SaveSql(rc *req.Ctx) {
dbSqlForm := req.BindJsonAndValid[*form.DbSqlSaveForm](rc)
dbSqlForm := req.BindJson[*form.DbSqlSaveForm](rc)
rc.ReqParam = dbSqlForm
dbId := getDbId(rc)

View File

@@ -93,7 +93,7 @@ func (d *DbTransferTask) DeleteTask(rc *req.Ctx) {
}
func (d *DbTransferTask) ChangeStatus(rc *req.Ctx) {
form := req.BindJsonAndValid[*form.DbTransferTaskStatusForm](rc)
form := req.BindJson[*form.DbTransferTaskStatusForm](rc)
rc.ReqParam = form
task, err := d.dbTransferTaskApp.GetById(form.Id)
@@ -136,7 +136,7 @@ func (d *DbTransferTask) FileDel(rc *req.Ctx) {
}
func (d *DbTransferTask) FileRun(rc *req.Ctx) {
fm := req.BindJsonAndValid[*form.DbTransferFileRunForm](rc)
fm := req.BindJson[*form.DbTransferFileRunForm](rc)
rc.ReqParam = fm

View File

@@ -13,19 +13,19 @@ import (
"mayfly-go/internal/db/imsg"
flowapp "mayfly-go/internal/flow/application"
flowentity "mayfly-go/internal/flow/domain/entity"
msgapp "mayfly-go/internal/msg/application"
msgdto "mayfly-go/internal/msg/application/dto"
"mayfly-go/internal/pkg/event"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/global"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/jsonx"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/ws"
"strings"
"time"
)
type sqlExecParam struct {
@@ -71,7 +71,6 @@ type dbSqlExecAppImpl struct {
dbSqlExecRepo repository.DbSqlExec `inject:"T"`
flowProcdefApp flowapp.Procdef `inject:"T"`
msgApp msgapp.Msg `inject:"T"`
}
func createSqlExecRecord(ctx context.Context, execSqlReq *dto.DbSqlExecReq, sql string) *entity.DbSqlExec {
@@ -206,38 +205,55 @@ func (d *dbSqlExecAppImpl) ExecReader(ctx context.Context, execReader *dto.SqlRe
la := contextx.GetLoginAccount(ctx)
needSendMsg := la != nil && clientId != ""
startTime := time.Now()
executedStatements := 0
progressId := stringx.Rand(32)
msgEvent := &msgdto.MsgTmplSendEvent{
TmplChannel: msgdto.MsgTmplSqlScriptRunSuccess,
Params: collx.M{"filename": filename, "db": dbConn.Info.GetLogDesc()},
}
progressMsgEvent := &msgdto.MsgTmplSendEvent{
TmplChannel: msgdto.MsgTmplSqlScriptRunProgress,
Params: collx.M{
"id": progressId,
"title": filename,
"executedStatements": executedStatements,
"terminated": false,
"clientId": clientId,
},
}
if needSendMsg {
msgEvent.ReceiverIds = []uint64{la.Id}
progressMsgEvent.ReceiverIds = []uint64{la.Id}
}
defer func() {
if needSendMsg {
progressMsgEvent.Params["terminated"] = true
global.EventBus.Publish(ctx, event.EventTopicMsgTmplSend, progressMsgEvent)
}
if err := recover(); err != nil {
errInfo := anyx.ToString(err)
logx.Errorf("exec sql reader error: %s", errInfo)
if needSendMsg {
errInfo = stringx.Truncate(errInfo, 300, 10, "...")
d.msgApp.CreateAndSend(la, msgdto.ErrSysMsg(i18n.T(imsg.SqlScriptRunFail), fmt.Sprintf("[%s][%s] execution failure: [%s]", filename, dbConn.Info.GetLogDesc(), errInfo)).WithClientId(clientId))
msgEvent.TmplChannel = msgdto.MsgTmplSqlScriptRunFail
msgEvent.Params["error"] = errInfo
global.EventBus.Publish(ctx, event.EventTopicMsgTmplSend, msgEvent)
}
}
}()
executedStatements := 0
progressId := stringx.Rand(32)
if needSendMsg {
defer ws.SendJsonMsg(ws.UserId(la.Id), clientId, msgdto.InfoSysMsg(i18n.T(imsg.SqlScripRunProgress), &progressMsg{
Id: progressId,
Title: filename,
ExecutedStatements: executedStatements,
Terminated: true,
}).WithCategory(progressCategory))
}
tx, _ := dbConn.Begin()
err := sqlparser.SQLSplit(execReader.Reader, ';', func(sql string) error {
if executedStatements%50 == 0 {
if needSendMsg {
ws.SendJsonMsg(ws.UserId(la.Id), clientId, msgdto.InfoSysMsg(i18n.T(imsg.SqlScripRunProgress), &progressMsg{
Id: progressId,
Title: filename,
ExecutedStatements: executedStatements,
Terminated: false,
}).WithCategory(progressCategory))
progressMsgEvent.Params["executedStatements"] = executedStatements
global.EventBus.Publish(ctx, event.EventTopicMsgTmplSend, progressMsgEvent)
}
}
@@ -249,12 +265,18 @@ func (d *dbSqlExecAppImpl) ExecReader(ctx context.Context, execReader *dto.SqlRe
})
if err != nil {
_ = tx.Rollback()
if needSendMsg {
msgEvent.TmplChannel = msgdto.MsgTmplSqlScriptRunFail
msgEvent.Params["error"] = err.Error()
global.EventBus.Publish(ctx, event.EventTopicMsgTmplSend, msgEvent)
}
return err
}
_ = tx.Commit()
if needSendMsg {
d.msgApp.CreateAndSend(la, msgdto.SuccessSysMsg(i18n.T(imsg.SqlScriptRunSuccess), "execution success").WithClientId(clientId))
msgEvent.Params["cost"] = fmt.Sprintf("%dms", time.Since(startTime).Milliseconds())
global.EventBus.Publish(ctx, event.EventTopicMsgTmplSend, msgEvent)
}
return nil
}

View File

@@ -14,10 +14,7 @@ var En = map[i18n.MsgId]string{
LogDbRunSql: "DB - Run SQL",
LogDbDump: "DB - Export DB",
SqlScriptRunFail: "sql script failed to execute",
SqlScriptRunSuccess: "sql script executed successfully",
SqlScripRunProgress: "sql execution progress",
DbDumpErr: "Database export failed",
ErrDbNameExist: "The database name already exists in this instance",
ErrDbNotAccess: "The operation permissions of database [{{.dbName}}] are not configured",

View File

@@ -24,10 +24,7 @@ const (
LogDbRunSqlFile
LogDbDump
SqlScriptRunFail
SqlScriptRunSuccess
SqlScripRunProgress
DbDumpErr
ErrDbNameExist
ErrDbNotAccess

View File

@@ -14,10 +14,7 @@ var Zh_CN = map[i18n.MsgId]string{
LogDbRunSql: "DB-运行SQL",
LogDbDump: "DB-导出数据库",
SqlScriptRunFail: "sql脚本执行失败",
SqlScriptRunSuccess: "sql脚本执行成功",
SqlScripRunProgress: "sql执行进度",
DbDumpErr: "数据库导出失败",
ErrDbNameExist: "该实例下数据库名已存在",
ErrDbNotAccess: "未配置数据库【{{.dbName}}】的操作权限",

View File

@@ -4,7 +4,7 @@ import (
"mayfly-go/initialize"
"mayfly-go/internal/db/api"
"mayfly-go/internal/db/application"
"mayfly-go/internal/db/infrastructure/persistence"
"mayfly-go/internal/db/infra/persistence"
)
func init() {

View File

@@ -4,7 +4,7 @@ import (
"mayfly-go/initialize"
"mayfly-go/internal/es/api"
"mayfly-go/internal/es/application"
"mayfly-go/internal/es/infrastructure/persistence"
"mayfly-go/internal/es/infra/persistence"
)
func init() {

View File

@@ -1,15 +0,0 @@
package persistence
import (
"mayfly-go/internal/file/domain/entity"
"mayfly-go/internal/file/domain/repository"
"mayfly-go/pkg/base"
)
type fileRepoImpl struct {
base.RepoImpl[*entity.File]
}
func newFileRepo() repository.File {
return &fileRepoImpl{}
}

View File

@@ -1,9 +0,0 @@
package persistence
import (
"mayfly-go/pkg/ioc"
)
func InitIoc() {
ioc.Register(newFileRepo(), ioc.WithComponentName("FileRepo"))
}

View File

@@ -4,7 +4,7 @@ import (
"mayfly-go/initialize"
"mayfly-go/internal/file/api"
"mayfly-go/internal/file/application"
"mayfly-go/internal/file/infrastructure/persistence"
"mayfly-go/internal/file/infra/persistence"
)
func init() {

View File

@@ -97,7 +97,7 @@ func (a *Procdef) Save(rc *req.Ctx) {
}
func (a *Procdef) SaveFlowDef(rc *req.Ctx) {
form := req.BindJsonAndValid[*form.ProcdefFlow](rc)
form := req.BindJson[*form.ProcdefFlow](rc)
rc.ReqParam = form
biz.ErrIsNil(a.procdefApp.SaveFlowDef(rc.MetaCtx, &dto.SaveFlowDef{

View File

@@ -47,7 +47,7 @@ func (p *Procinst) GetProcinstPage(rc *req.Ctx) {
}
func (p *Procinst) ProcinstStart(rc *req.Ctx) {
startForm := req.BindJsonAndValid[*form.ProcinstStart](rc)
startForm := req.BindJson[*form.ProcinstStart](rc)
_, err := p.procinstApp.StartProc(rc.MetaCtx, startForm.ProcdefId, &dto.StarProc{
BizType: startForm.BizType,
BizForm: jsonx.ToStr(startForm.BizForm),

View File

@@ -74,7 +74,7 @@ func (p *ProcinstTask) GetTasks(rc *req.Ctx) {
}
func (p *ProcinstTask) PassTask(rc *req.Ctx) {
auditForm := req.BindJsonAndValid[*form.ProcinstTaskAudit](rc)
auditForm := req.BindJson[*form.ProcinstTaskAudit](rc)
rc.ReqParam = auditForm
la := rc.GetLoginAccount()
@@ -84,7 +84,7 @@ func (p *ProcinstTask) PassTask(rc *req.Ctx) {
}
func (p *ProcinstTask) RejectTask(rc *req.Ctx) {
auditForm := req.BindJsonAndValid[*form.ProcinstTaskAudit](rc)
auditForm := req.BindJson[*form.ProcinstTaskAudit](rc)
rc.ReqParam = auditForm
la := rc.GetLoginAccount()
@@ -94,7 +94,7 @@ func (p *ProcinstTask) RejectTask(rc *req.Ctx) {
}
func (p *ProcinstTask) BackTask(rc *req.Ctx) {
auditForm := req.BindJsonAndValid[*form.ProcinstTaskAudit](rc)
auditForm := req.BindJson[*form.ProcinstTaskAudit](rc)
rc.ReqParam = auditForm
biz.ErrIsNil(p.procinstTaskApp.BackTask(rc.MetaCtx, dto.UserTaskOp{TaskId: auditForm.Id, Remark: auditForm.Remark}))
}

View File

@@ -2,11 +2,11 @@ package application
import (
"context"
"mayfly-go/internal/event"
"mayfly-go/internal/flow/domain/entity"
"mayfly-go/internal/flow/imsg"
"mayfly-go/internal/flow/infrastructure/persistence"
"mayfly-go/internal/flow/infra/persistence"
msgdto "mayfly-go/internal/msg/application/dto"
"mayfly-go/internal/pkg/event"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/global"
"strings"

View File

@@ -4,7 +4,7 @@ import (
"mayfly-go/initialize"
"mayfly-go/internal/flow/api"
"mayfly-go/internal/flow/application"
"mayfly-go/internal/flow/infrastructure/persistence"
"mayfly-go/internal/flow/infra/persistence"
)
func init() {

View File

@@ -2,7 +2,6 @@ package api
import (
"fmt"
"mayfly-go/internal/event"
"mayfly-go/internal/machine/api/form"
"mayfly-go/internal/machine/api/vo"
"mayfly-go/internal/machine/application"
@@ -12,6 +11,7 @@ import (
"mayfly-go/internal/machine/imsg"
"mayfly-go/internal/machine/mcm"
"mayfly-go/internal/pkg/consts"
"mayfly-go/internal/pkg/event"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"

View File

@@ -12,15 +12,14 @@ import (
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/imsg"
"mayfly-go/internal/machine/mcm"
msgapp "mayfly-go/internal/msg/application"
msgdto "mayfly-go/internal/msg/application/dto"
"mayfly-go/internal/pkg/event"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/global"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/timex"
"mime/multipart"
@@ -36,7 +35,6 @@ import (
type MachineFile struct {
machineFileApp application.MachineFile `inject:"T"`
msgApp msgapp.Msg `inject:"T"`
}
func (mf *MachineFile) ReqConfs() *req.Confs {
@@ -106,7 +104,7 @@ func (m *MachineFile) DeleteFile(rc *req.Ctx) {
/*** sftp相关操作 */
func (m *MachineFile) CreateFile(rc *req.Ctx) {
opForm := req.BindJsonAndValid[*form.CreateFileForm](rc)
opForm := req.BindJson[*form.CreateFileForm](rc)
path := opForm.Path
attrs := collx.Kvs("path", path)
@@ -239,7 +237,7 @@ func (m *MachineFile) GetFileStat(rc *req.Ctx) {
}
func (m *MachineFile) WriteFileContent(rc *req.Ctx) {
opForm := req.BindJsonAndValid[*form.WriteFileContentForm](rc)
opForm := req.BindJson[*form.WriteFileContentForm](rc)
path := opForm.Path
mi, err := m.machineFileApp.WriteFileContent(rc.MetaCtx, opForm.MachineFileOp, []byte(opForm.Content))
@@ -264,14 +262,6 @@ func (m *MachineFile) UploadFile(rc *req.Ctx) {
file, _ := fileheader.Open()
defer file.Close()
la := rc.GetLoginAccount()
defer func() {
if anyx.ToString(recover()) != "" {
logx.Errorf("upload file error: %s", err)
m.msgApp.CreateAndSend(la, msgdto.ErrSysMsg(i18n.TC(ctx, imsg.ErrFileUploadFail), fmt.Sprintf("%s: \n<-e : %s", i18n.TC(ctx, imsg.ErrFileUploadFail), err)))
}
}()
opForm := &dto.MachineFileOp{
MachineId: machineId,
AuthCertName: authCertName,
@@ -281,9 +271,27 @@ func (m *MachineFile) UploadFile(rc *req.Ctx) {
mi, err := m.machineFileApp.UploadFile(ctx, opForm, fileheader.Filename, file)
rc.ReqParam = collx.Kvs("machine", mi, "path", fmt.Sprintf("%s/%s", path, fileheader.Filename))
// 发送文件上传结果消息
msgEvent := &msgdto.MsgTmplSendEvent{
TmplChannel: msgdto.MsgTmplMachineFileUploadSuccess,
Params: collx.M{
"filename": fileheader.Filename,
"path": path,
},
ReceiverIds: []uint64{rc.GetLoginAccount().Id},
}
if err != nil {
msgEvent.Params["error"] = err.Error()
msgEvent.TmplChannel = msgdto.MsgTmplMachineFileUploadFail
}
if mi != nil {
msgEvent.Params["machineName"] = mi.Name
msgEvent.Params["machineIp"] = mi.Ip
}
global.EventBus.Publish(ctx, event.EventTopicMsgTmplSend, msgEvent)
biz.ErrIsNilAppendErr(err, "upload file error: %s")
// 保存消息并发送文件上传成功通知
m.msgApp.CreateAndSend(la, msgdto.SuccessSysMsg(i18n.TC(ctx, imsg.MsgUploadFileSuccess), fmt.Sprintf("[%s] -> %s[%s:%s]", fileheader.Filename, mi.Name, mi.Ip, path)))
}
type FolderFile struct {
@@ -350,6 +358,17 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
}
}
msgEvent := &msgdto.MsgTmplSendEvent{
TmplChannel: msgdto.MsgTmplMachineFileUploadSuccess,
Params: collx.M{
"filename": folderName,
"path": basePath,
"machineName": mi.Name,
"machineIp": mi.Ip,
},
ReceiverIds: []uint64{rc.GetLoginAccount().Id},
}
// 分组处理
groupNum := 30
chunks := collx.ArraySplit(folderFiles, groupNum)
@@ -359,7 +378,6 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
wg.Add(len(chunks))
isSuccess := true
la := rc.GetLoginAccount()
for _, chunk := range chunks {
go func(files []FolderFile, wg *sync.WaitGroup) {
defer func() {
@@ -370,7 +388,11 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
logx.Errorf("upload file error: %s", err)
switch t := err.(type) {
case *errorx.BizError:
m.msgApp.CreateAndSend(la, msgdto.ErrSysMsg(i18n.TC(ctx, imsg.ErrFileUploadFail), fmt.Sprintf("%s: \n<-e errCode: %d, errMsg: %s", i18n.TC(ctx, imsg.ErrFileUploadFail), t.Code(), t.Error())))
{
msgEvent.TmplChannel = msgdto.MsgTmplMachineFileUploadFail
msgEvent.Params["error"] = t.Error()
global.EventBus.Publish(ctx, event.EventTopicMsgTmplSend, msgEvent)
}
}
}
}()
@@ -394,13 +416,12 @@ func (m *MachineFile) UploadFolder(rc *req.Ctx) {
// 等待所有协程执行完成
wg.Wait()
if isSuccess {
// 保存消息并发送文件上传成功通知
m.msgApp.CreateAndSend(la, msgdto.SuccessSysMsg(i18n.TC(ctx, imsg.MsgUploadFileSuccess), fmt.Sprintf("[%s] -> %s[%s:%s]", folderName, mi.Name, mi.Ip, basePath)))
global.EventBus.Publish(ctx, event.EventTopicMsgTmplSend, msgEvent)
}
}
func (m *MachineFile) RemoveFile(rc *req.Ctx) {
opForm := req.BindJsonAndValid[*form.RemoveFileForm](rc)
opForm := req.BindJson[*form.RemoveFileForm](rc)
mi, err := m.machineFileApp.RemoveFile(rc.MetaCtx, opForm.MachineFileOp, opForm.Paths...)
rc.ReqParam = collx.Kvs("machine", mi, "path", opForm)
@@ -408,21 +429,21 @@ func (m *MachineFile) RemoveFile(rc *req.Ctx) {
}
func (m *MachineFile) CopyFile(rc *req.Ctx) {
opForm := req.BindJsonAndValid[*form.CopyFileForm](rc)
opForm := req.BindJson[*form.CopyFileForm](rc)
mi, err := m.machineFileApp.Copy(rc.MetaCtx, opForm.MachineFileOp, opForm.ToPath, opForm.Paths...)
biz.ErrIsNilAppendErr(err, "file copy error: %s")
rc.ReqParam = collx.Kvs("machine", mi, "cp", opForm)
}
func (m *MachineFile) MvFile(rc *req.Ctx) {
opForm := req.BindJsonAndValid[*form.CopyFileForm](rc)
opForm := req.BindJson[*form.CopyFileForm](rc)
mi, err := m.machineFileApp.Mv(rc.MetaCtx, opForm.MachineFileOp, opForm.ToPath, opForm.Paths...)
rc.ReqParam = collx.Kvs("machine", mi, "mv", opForm)
biz.ErrIsNilAppendErr(err, "file move error: %s")
}
func (m *MachineFile) Rename(rc *req.Ctx) {
renameForm := req.BindJsonAndValid[*form.RenameForm](rc)
renameForm := req.BindJson[*form.RenameForm](rc)
mi, err := m.machineFileApp.Rename(rc.MetaCtx, renameForm.MachineFileOp, renameForm.Newname)
rc.ReqParam = collx.Kvs("machine", mi, "rename", renameForm)
biz.ErrIsNilAppendErr(err, "file rename error: %s")

View File

@@ -7,7 +7,7 @@ import (
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/domain/repository"
"mayfly-go/internal/machine/imsg"
"mayfly-go/internal/machine/infrastructure/cache"
"mayfly-go/internal/machine/infra/cache"
"mayfly-go/internal/machine/mcm"
tagapp "mayfly-go/internal/tag/application"
tagdto "mayfly-go/internal/tag/application/dto"

View File

@@ -4,7 +4,7 @@ import (
"mayfly-go/initialize"
"mayfly-go/internal/machine/api"
"mayfly-go/internal/machine/application"
"mayfly-go/internal/machine/infrastructure/persistence"
"mayfly-go/internal/machine/infra/persistence"
)
func init() {

View File

@@ -2,13 +2,13 @@ package api
import (
"context"
"mayfly-go/internal/event"
"mayfly-go/internal/mongo/api/form"
"mayfly-go/internal/mongo/api/vo"
"mayfly-go/internal/mongo/application"
"mayfly-go/internal/mongo/domain/entity"
"mayfly-go/internal/mongo/imsg"
"mayfly-go/internal/pkg/consts"
"mayfly-go/internal/pkg/event"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
@@ -146,7 +146,7 @@ func (m *Mongo) Collections(rc *req.Ctx) {
}
func (m *Mongo) RunCommand(rc *req.Ctx) {
commandForm := req.BindJsonAndValid[*form.MongoRunCommand](rc)
commandForm := req.BindJson[*form.MongoRunCommand](rc)
conn, err := m.mongoApp.GetMongoConn(rc.MetaCtx, m.GetMongoId(rc))
biz.ErrIsNil(err)
@@ -176,7 +176,7 @@ func (m *Mongo) RunCommand(rc *req.Ctx) {
}
func (m *Mongo) FindCommand(rc *req.Ctx) {
commandForm := req.BindJsonAndValid[*form.MongoFindCommand](rc)
commandForm := req.BindJson[*form.MongoFindCommand](rc)
conn, err := m.mongoApp.GetMongoConn(rc.MetaCtx, m.GetMongoId(rc))
biz.ErrIsNil(err)
@@ -211,7 +211,7 @@ func (m *Mongo) FindCommand(rc *req.Ctx) {
}
func (m *Mongo) UpdateByIdCommand(rc *req.Ctx) {
commandForm := req.BindJsonAndValid[*form.MongoUpdateByIdCommand](rc)
commandForm := req.BindJson[*form.MongoUpdateByIdCommand](rc)
conn, err := m.mongoApp.GetMongoConn(rc.MetaCtx, m.GetMongoId(rc))
biz.ErrIsNil(err)
@@ -235,7 +235,7 @@ func (m *Mongo) UpdateByIdCommand(rc *req.Ctx) {
}
func (m *Mongo) DeleteByIdCommand(rc *req.Ctx) {
commandForm := req.BindJsonAndValid[*form.MongoUpdateByIdCommand](rc)
commandForm := req.BindJson[*form.MongoUpdateByIdCommand](rc)
conn, err := m.mongoApp.GetMongoConn(rc.MetaCtx, m.GetMongoId(rc))
biz.ErrIsNil(err)
@@ -258,7 +258,7 @@ func (m *Mongo) DeleteByIdCommand(rc *req.Ctx) {
}
func (m *Mongo) InsertOneCommand(rc *req.Ctx) {
commandForm := req.BindJsonAndValid[*form.MongoInsertCommand](rc)
commandForm := req.BindJson[*form.MongoInsertCommand](rc)
conn, err := m.mongoApp.GetMongoConn(rc.MetaCtx, m.GetMongoId(rc))
biz.ErrIsNil(err)

View File

@@ -4,7 +4,7 @@ import (
"mayfly-go/initialize"
"mayfly-go/internal/mongo/api"
"mayfly-go/internal/mongo/application"
"mayfly-go/internal/mongo/infrastructure/persistence"
"mayfly-go/internal/mongo/infra/persistence"
)
func init() {

View File

@@ -14,12 +14,14 @@ type Msg struct {
func (m *Msg) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
req.NewGet("/self", m.GetMsgs),
req.NewGet("/self/unread/count", m.GetUnreadCount),
req.NewGet("/self/read", m.ReadMsg),
}
return req.NewConfs("/msgs", reqs[:]...)
}
// 获取账号接收的消息列表
// GetMsgs 获取账号接收的消息列表
func (m *Msg) GetMsgs(rc *req.Ctx) {
condition := &entity.Msg{
RecipientId: int64(rc.GetLoginAccount().Id),
@@ -28,3 +30,25 @@ func (m *Msg) GetMsgs(rc *req.Ctx) {
biz.ErrIsNil(err)
rc.ResData = res
}
// GetUnreadCount 获取账号接收的未读消息数量
func (m *Msg) GetUnreadCount(rc *req.Ctx) {
condition := &entity.Msg{
RecipientId: int64(rc.GetLoginAccount().Id),
Status: entity.MsgStatusUnRead,
}
rc.ResData = m.msgApp.CountByCond(condition)
}
// ReadMsg 将账号接收的未读消息标记为已读
func (m *Msg) ReadMsg(rc *req.Ctx) {
cond := &entity.Msg{
RecipientId: int64(rc.GetLoginAccount().Id),
Status: entity.MsgStatusUnRead,
}
cond.Id = uint64(rc.QueryInt("id"))
biz.ErrIsNil(m.msgApp.UpdateByCond(rc.MetaCtx, &entity.Msg{
Status: entity.MsgStatusRead,
}, cond))
}

View File

@@ -74,7 +74,7 @@ func (m *MsgTmpl) DelMsgTmpls(rc *req.Ctx) {
func (m *MsgTmpl) SendMsg(rc *req.Ctx) {
code := rc.PathParam("code")
form := req.BindJsonAndValid[*form.SendMsg](rc)
form := req.BindJson[*form.SendMsg](rc)
rc.ReqParam = form

View File

@@ -15,3 +15,7 @@ func InitIoc() {
func GetMsgApp() Msg {
return ioc.Get[Msg]("MsgApp")
}
func GetMsgTmplApp() MsgTmpl {
return ioc.Get[MsgTmpl]("MsgTmplApp")
}

View File

@@ -2,35 +2,93 @@ package dto
import (
"mayfly-go/internal/msg/domain/entity"
"mayfly-go/internal/msg/imsg"
"mayfly-go/internal/msg/msgx"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/collx"
)
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
type MsgTmplSendEvent struct {
TmplChannel *MsgTmplChannel
Params map[string]any // 模板占位符参数
ReceiverIds []uint64 // 接收人id
}
type MsgTmplChannel struct {
Tmpl *entity.MsgTmpl
Channels []*entity.MsgChannel
}
var (
MsgChannelSite = &entity.MsgChannel{
Type: msgx.ChannelTypeSiteMsg,
Status: entity.ChannelStatusEnable,
}
MsgChannelWs = &entity.MsgChannel{
Type: msgx.ChannelTypeWs,
Status: entity.ChannelStatusEnable,
}
)
var (
MsgTmplLogin = newMsgTmpl(entity.MsgTypeNotify,
entity.MsgSubtypeUserLogin,
entity.MsgStatusRead,
imsg.LoginMsg,
MsgChannelSite)
MsgTmplMachineFileUploadSuccess = newMsgTmpl(entity.MsgTypeNotify,
entity.MsgSubtypeMachineFileUploadSuccess,
entity.MsgStatusRead,
imsg.MachineFileUploadSuccessMsg,
MsgChannelSite, MsgChannelWs)
MsgTmplMachineFileUploadFail = newMsgTmpl(entity.MsgTypeNotify,
entity.MsgSubtypeMachineFileUploadFail,
entity.MsgStatusRead,
imsg.MachineFileUploadFailMsg,
MsgChannelSite, MsgChannelWs)
MsgTmplDbDumpFail = newMsgTmpl(entity.MsgTypeNotify,
entity.MsgSubtypeDbDumpFail,
entity.MsgStatusRead,
imsg.DbDumpFailMsg,
MsgChannelSite, MsgChannelWs)
MsgTmplSqlScriptRunFail = newMsgTmpl(entity.MsgTypeNotify,
entity.MsgSubtypeSqlScriptRunFail,
entity.MsgStatusRead,
imsg.SqlScriptRunFailMsg,
MsgChannelSite, MsgChannelWs)
MsgTmplSqlScriptRunSuccess = newMsgTmpl(entity.MsgTypeNotify,
entity.MsgSubtypeSqlScriptRunSuccess,
entity.MsgStatusRead,
imsg.SqlScriptRunSuccessMsg,
MsgChannelSite, MsgChannelWs)
MsgTmplSqlScriptRunProgress = &MsgTmplChannel{
Tmpl: &entity.MsgTmpl{
ExtraData: model.ExtraData{
Extra: collx.M{
"category": "sqlScriptRunProgress",
},
},
},
Channels: []*entity.MsgChannel{MsgChannelWs},
}
)
func newMsgTmpl(mtype entity.MsgType, subtype entity.MsgSubtype, status entity.MsgStatus, msgId i18n.MsgId, channels ...*entity.MsgChannel) *MsgTmplChannel {
msgTmpl := &entity.MsgTmpl{}
msgTmpl.SetExtraValue("msgId", msgId)
msgTmpl.SetExtraValue("subtype", subtype)
msgTmpl.SetExtraValue("type", mtype)
msgTmpl.SetExtraValue("status", status)
return &MsgTmplChannel{
Tmpl: msgTmpl,
Channels: channels,
}
}

View File

@@ -0,0 +1,43 @@
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
}
type MsgTmplSend struct {
Tmpl *entity.MsgTmpl
Channels []*entity.MsgChannel
Params map[string]any // 模板占位符参数
ReceiverIds []uint64 // 接收人id
}

View File

@@ -1,54 +0,0 @@
package dto
import "mayfly-go/pkg/utils/anyx"
// ************** 系统消息 **************
const SuccessSysMsgType = 1
const ErrorSysMsgType = 0
const InfoSysMsgType = 2
// websocket消息
type SysMsg struct {
Type int `json:"type"` // 消息类型
Category string `json:"category"` // 消息类别
Title string `json:"title"` // 消息标题
Msg string `json:"msg"` // 消息内容
ClientId string
}
func (sm *SysMsg) WithTitle(title string) *SysMsg {
sm.Title = title
return sm
}
func (sm *SysMsg) WithCategory(category string) *SysMsg {
sm.Category = category
return sm
}
func (sm *SysMsg) WithMsg(msg any) *SysMsg {
sm.Msg = anyx.ToString(msg)
return sm
}
func (sm *SysMsg) WithClientId(clientId string) *SysMsg {
sm.ClientId = clientId
return sm
}
// 普通消息
func InfoSysMsg(title string, msg any) *SysMsg {
return &SysMsg{Type: InfoSysMsgType, Title: title, Msg: anyx.ToString(msg)}
}
// 成功消息
func SuccessSysMsg(title string, msg any) *SysMsg {
return &SysMsg{Type: SuccessSysMsgType, Title: title, Msg: anyx.ToString(msg)}
}
// 错误消息
func ErrSysMsg(title string, msg any) *SysMsg {
return &SysMsg{Type: ErrorSysMsgType, Title: title, Msg: anyx.ToString(msg)}
}

View File

@@ -1,27 +1,30 @@
package application
import (
"cmp"
"context"
"mayfly-go/internal/msg/application/dto"
"mayfly-go/internal/msg/domain/entity"
"mayfly-go/internal/msg/domain/repository"
"mayfly-go/internal/msg/msgx"
"mayfly-go/pkg/base"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/model"
"mayfly-go/pkg/ws"
"time"
"mayfly-go/pkg/utils/stringx"
)
type Msg interface {
msgx.MsgSender
base.App[*entity.Msg]
GetPageList(condition *entity.Msg, pageParam model.PageParam, orderBy ...string) (*model.PageResult[*entity.Msg], error)
Create(ctx context.Context, msg *entity.Msg)
// 创建消息并通过ws发送
CreateAndSend(la *model.LoginAccount, msg *dto.SysMsg)
}
var _ (Msg) = (*msgAppImpl)(nil)
type msgAppImpl struct {
base.AppImpl[*entity.Msg, repository.Msg]
msgRepo repository.Msg `inject:"T"`
}
@@ -29,13 +32,29 @@ func (a *msgAppImpl) GetPageList(condition *entity.Msg, pageParam model.PagePara
return a.msgRepo.GetPageList(condition, pageParam)
}
func (a *msgAppImpl) Create(ctx context.Context, msg *entity.Msg) {
a.msgRepo.Insert(ctx, msg)
func (a *msgAppImpl) Send(ctx context.Context, channel *msgx.Channel, msg *msgx.Msg) error {
// 存在i18n msgIdcontent则使用msgId翻译
if msgId := msg.TmplExtra.GetInt("msgId"); msgId != 0 {
msg.Content = i18n.TC(ctx, i18n.MsgId(msgId))
}
content, err := stringx.TemplateParse(msg.Content, msg.Params)
if err != nil {
return err
}
func (a *msgAppImpl) CreateAndSend(la *model.LoginAccount, wmsg *dto.SysMsg) {
now := time.Now()
msg := &entity.Msg{Type: 2, Msg: wmsg.Msg, RecipientId: int64(la.Id), CreateTime: &now, CreatorId: la.Id, Creator: la.Username}
a.msgRepo.Insert(context.TODO(), msg)
ws.SendJsonMsg(ws.UserId(la.Id), wmsg.ClientId, wmsg)
for _, receiver := range msg.Receivers {
msgEntity := &entity.Msg{
Msg: content,
RecipientId: int64(receiver.Id),
Type: entity.MsgType(msg.TmplExtra.GetInt("type")),
Subtype: entity.MsgSubtype(msg.TmplExtra.GetStr("subtype")),
Status: cmp.Or(entity.MsgStatus(msg.TmplExtra.GetInt("status")), entity.MsgStatusRead),
}
msgEntity.Extra = msg.Params
if err := a.Save(ctx, msgEntity); err != nil {
return err
}
}
return nil
}

View File

@@ -31,6 +31,8 @@ type MsgTmpl interface {
// Send 发送消息
Send(ctx context.Context, tmplCode string, params map[string]any, receiverId ...uint64) error
SendMsg(ctx context.Context, mts *dto.MsgTmplSend) error
// DeleteTmplChannel 删除指定渠道关联的模板
DeleteTmplChannel(ctx context.Context, channelId uint64) error
}
@@ -153,47 +155,52 @@ func (m *msgTmplAppImpl) Send(ctx context.Context, tmplCode string, params map[s
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
return m.SendMsg(ctx, &dto.MsgTmplSend{
Tmpl: tmpl,
Channels: channels,
Params: params,
ReceiverIds: receiverId,
})
}
func (m *msgTmplAppImpl) SendMsg(ctx context.Context, mts *dto.MsgTmplSend) error {
tmpl := mts.Tmpl
msg := &msgx.Msg{
Content: tmpl.Tmpl,
Params: params,
Params: mts.Params,
Title: tmpl.Title,
Type: tmpl.MsgType,
ExtraData: tmpl.ExtraData,
TmplExtra: tmpl.Extra,
}
accounts, err := m.accountApp.GetByIds(mts.ReceiverIds)
if err != nil {
return err
}
if len(accounts) > 0 {
msg.Receivers = collx.ArrayMap(accounts, func(account *sysentity.Account) msgx.Receiver {
return msgx.Receiver{
ExtraData: account.ExtraData,
Id: account.Id,
Extra: account.Extra,
Email: account.Email,
Mobile: account.Mobile,
}
})
}
for _, channel := range channels {
for _, channel := range mts.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{
if err := msgx.Send(ctx, &msgx.Channel{
Type: channel.Type,
Name: channel.Name,
URL: channel.Url,
ExtraData: channel.ExtraData,
Extra: channel.Extra,
}, msg); err != nil {
logx.Errorf("send msg error => channel=%s, msg=%s, err -> %v", channel.Code, msg.Content, err)
}

View File

@@ -2,17 +2,15 @@ package entity
import (
"mayfly-go/pkg/model"
"time"
)
type Msg struct {
model.DeletedModel
model.ExtraData
model.CreateModel
CreateTime *time.Time `json:"createTime"`
CreatorId uint64 `json:"creatorId"`
Creator string `json:"creator" gorm:"size:50"`
Type int8 `json:"type"`
Type MsgType `json:"type"` // 消息类型
Subtype MsgSubtype `json:"subtype" gorm:"size:100"` // 消息子类型
Status MsgStatus `json:"status"`
Msg string `json:"msg" gorm:"size:2000"`
RecipientId int64 `json:"recipientId"` // 接收人id-1为所有接收
}
@@ -20,3 +18,33 @@ type Msg struct {
func (a *Msg) TableName() string {
return "t_sys_msg"
}
type MsgType int8
const (
MsgTypeNotify MsgType = 1 // 通知
MsgTypeTodo MsgType = 2 // 代办
)
type MsgSubtype string
const (
// sys
MsgSubtypeUserLogin MsgSubtype = "user.login"
// machine
MsgSubtypeMachineFileUploadSuccess MsgSubtype = "machine.file.upload.success"
MsgSubtypeMachineFileUploadFail MsgSubtype = "machine.file.upload.fail"
// db
MsgSubtypeDbDumpFail MsgSubtype = "db.dump.fail"
MsgSubtypeSqlScriptRunFail MsgSubtype = "db.sqlscript.run.fail"
MsgSubtypeSqlScriptRunSuccess MsgSubtype = "db.sqlscript.run.success"
)
type MsgStatus int8
const (
MsgStatusRead MsgStatus = 1 // 已读
MsgStatusUnRead MsgStatus = -1 // 未读
)

Some files were not shown because too many files have changed in this diff Show More