51 Commits

Author SHA1 Message Date
meilin.huang
0695ad9a85 feat: 新增机器授权凭证管理与其他优化 2023-03-06 16:59:57 +08:00
meilin.huang
7c086bbec8 fix: 问题修复 2023-02-22 15:54:53 +08:00
meilin.huang
c75fe7135a refactor: 图标优化等 2023-02-20 18:41:45 +08:00
meilin.huang
edf29976dd feat: 资源操作相关标签树调整为el-tree 2023-02-18 23:02:14 +08:00
meilin.huang
7b5f963ec4 fix: 构建脚本调整 2023-02-15 21:32:51 +08:00
meilin.huang
f22badb861 feat: 优化 2023-02-15 21:28:01 +08:00
meilin.huang
8c501c90cd feat: 标签路径展示优化,新增提示 2023-02-14 18:24:39 +08:00
Coder慌
11eebdfcf0 !39 feat: 表和schema超出20条后启用模糊高亮过滤
Merge pull request !39 from zongyangleo/dev_20230213_1
2023-02-14 06:36:41 +00:00
刘宗洋
46e331783f fix: sql提示bug修复 2023-02-14 14:03:18 +08:00
刘宗洋
7e33e64659 Merge remote-tracking branch 'upstream/dev' into dev_20230213_1 2023-02-14 12:24:48 +08:00
刘宗洋
7e4152ad6d fix: sql提示bug修复 2023-02-14 12:23:38 +08:00
meilin.huang
d189ee7c22 feat: 数据库表格重构 2023-02-14 11:44:48 +08:00
刘宗洋
641c2abb24 feat: 表名+schema模糊高亮过滤 2023-02-14 10:33:57 +08:00
meilin.huang
3ab4ac891b 表和schema超出20条后启用过滤 2023-02-13 21:36:18 +08:00
meilin.huang
70b586e45a refactor: sqlexec组件重构优化、新增数据库相关系统参数配置、相关问题修复 2023-02-13 21:11:16 +08:00
Coder慌
77aa724003 !37 fix: ref修复
Merge pull request !37 from zongyangleo/dev_20230206
2023-02-07 09:30:37 +00:00
刘宗洋
db7b50d96a fix: ref修复 2023-02-07 17:22:52 +08:00
meilin.huang
00fee24a85 feat: 代码小调整 2023-02-07 16:54:44 +08:00
Coder慌
fa0cb73ec9 !36 feat: redis新增实例树、各数据操作按钮跳转
Merge pull request !36 from zongyangleo/dev_20230206
2023-02-07 03:33:23 +00:00
刘宗洋
6ee815b71d debugger 2023-02-07 11:06:54 +08:00
刘宗洋
311a048af5 Merge remote-tracking branch 'upstream/dev' into dev_20230206
# Conflicts:
#	mayfly_go_web/src/views/ops/db/SqlExec.vue
#	mayfly_go_web/src/views/ops/db/component/InstanceTree.vue
#	mayfly_go_web/src/views/ops/mongo/MongoInstanceTree.vue
2023-02-07 10:59:46 +08:00
刘宗洋
3da9ecfaa3 feat: 数据操作按钮跳转 2023-02-07 10:32:42 +08:00
刘宗洋
4c2c6f613e feat: redis新增实例树 2023-02-07 08:39:14 +08:00
meilin.huang
ba63736871 feat: 小功能优化 2023-02-06 17:14:16 +08:00
Coder慌
ed3dbafc35 !34 feat:mongo新增实例树
Merge pull request !34 from zongyangleo/dev_20230206
2023-02-06 08:54:31 +00:00
刘宗洋
fdeffbd495 Merge branch 'dev' of https://gitee.com/objs/mayfly-go into dev_20230206
yes.
2023-02-06 16:46:52 +08:00
刘宗洋
9870812e6b feat:mongo新增实例树 2023-02-06 16:46:40 +08:00
Coder慌
46f35e14c4 !33 feat: 数据管理添加实例树,细节修改
Merge pull request !33 from zongyangleo/dev_20230206
2023-02-06 06:53:38 +00:00
刘宗洋
e89cf96ff4 feat:实例树优化:加宽菜单宽度、表加载中loading状态、新建查询等按钮自成一行、加宽实例详情宽度、实例树高度默认撑满 2023-02-06 14:50:57 +08:00
Coder慌
5cd19bf38d !32 feat: 数据管理添加实例树
Merge pull request !32 from zongyangleo/dev_20230206
2023-02-06 03:13:25 +00:00
刘宗洋
a6f69f2b62 修改团队bug 2023-02-06 11:08:18 +08:00
刘宗洋
e34b9adada feat: 数据管理添加实例树 2023-02-06 10:35:30 +08:00
meilin.huang
9f43f731b5 fix: 问题修复 2023-01-19 19:45:12 +08:00
meilin.huang
594ca43505 refactor: 包名变更ctx -> req 2023-01-14 16:29:52 +08:00
meilin.huang
2a91cdb67a fix: pgsql主键字段获取修复 2023-01-09 20:05:35 +08:00
Coder慌
11148e720b !30 feat: 执行sql,无数据时也提示一下
Merge pull request !30 from zongyangleo/dev_20221228
2023-01-01 06:43:54 +00:00
刘宗洋
c5835d6d8c feat: 执行sql,无数据时也提示一下 2022-12-28 10:23:38 +08:00
meilin.huang
4a26bb3ba5 feat: 支持redis缓存权限等信息 2022-12-26 20:53:07 +08:00
meilin.huang
4fec38724d fix: pgsql隧道连接问题修复 2022-12-22 18:41:34 +08:00
meilin.huang
85349df8a1 code review 2022-12-17 22:24:21 +08:00
meilin.huang
ffe250f8a9 feat: 构建文档调整&移除静态文件 2022-12-16 23:14:00 +08:00
Coder慌
eeb310a1d2 !28 redis监控信息面板重构
Merge pull request !28 from zongyangleo/dev_20221216
2022-12-16 03:40:02 +00:00
刘宗洋
a42c606d20 redis监控信息面板重构 2022-12-16 11:35:16 +08:00
Coder慌
4e64d46cd2 !27 redis监控信息面板重构
Merge pull request !27 from zongyangleo/dev_20221216
2022-12-16 03:17:50 +00:00
刘宗洋
9886ee6828 redis监控信息面板重构 2022-12-16 11:13:13 +08:00
Coder慌
0e1bde09c3 !26 fix: 修复双击编辑事件报错
Merge pull request !26 from zongyangleo/dev_20221213
2022-12-13 06:15:42 +00:00
刘宗洋
dda600709b fix: 修复双击编辑事件报错 2022-12-13 11:08:42 +08:00
meilin.huang
15f38491b2 feat: 新增docker部署 2022-12-09 22:02:37 +08:00
may-fly
4b140732f7 Merge pull request #23 from 1ch0/master
Feat: add Dockerfile
2022-12-08 19:14:41 +08:00
1ch0
f2119b2c52 Feat: add docker-compose 2022-12-05 11:27:05 +08:00
1ch0
f15c45793b Feat: add Dockerfile 2022-12-03 08:25:22 +08:00
323 changed files with 7480 additions and 5895 deletions

View File

@@ -34,7 +34,7 @@ web版 **linux(终端[终端回放] 文件 脚本 进程)、数据库mysql po
### 系统相关资料
- 项目文档: https://objs.gitee.io/mayfly-go-docs
- 系统操作视频: https://space.bilibili.com/484091081/channel/collectiondetail?sid=392854
- 安装包下载https://gitee.com/objs/mayfly-go/releases
- 部署文档https://objs.gitee.io/mayfly-go-docs/download
### 系统核心功能截图

View File

@@ -44,7 +44,7 @@ function build() {
toFolder=$1
os=$2
arch=$3
copyStatic=$4
copyDocScript=$4
echo_yellow "-------------------${os}-${arch}打包构建开始-------------------"
@@ -68,17 +68,19 @@ function build() {
echo_green "移动二进制文件至'${toFolder}'"
mv ${server_folder}/${execFileName} ${toFolder}
if [ "${copy2Server}" == "1" ] ; then
echo_green "拷贝前端静态页面至'${toFolder}/static'"
mkdir -p ${toFolder}/static && cp -r ${web_folder}/dist/* ${toFolder}/static
fi
# if [ "${copy2Server}" == "1" ] ; then
# echo_green "拷贝前端静态页面至'${toFolder}/static'"
# mkdir -p ${toFolder}/static && cp -r ${web_folder}/dist/* ${toFolder}/static
# fi
echo_green "拷贝脚本等资源文件[config.yml、mayfly-go.sql、readme.txt、startup.sh、shutdown.sh]"
cp ${server_folder}/config.yml ${toFolder}
cp ${server_folder}/mayfly-go.sql ${toFolder}
cp ${server_folder}/readme.txt ${toFolder}
cp ${server_folder}/startup.sh ${toFolder}
cp ${server_folder}/shutdown.sh ${toFolder}
if [ "${copyDocScript}" == "1" ] ; then
echo_green "拷贝脚本等资源文件[config.yml、mayfly-go.sql、readme.txt、startup.sh、shutdown.sh]"
cp ${server_folder}/config.yml ${toFolder}
cp ${server_folder}/mayfly-go.sql ${toFolder}
cp ${server_folder}/readme.txt ${toFolder}
cp ${server_folder}/startup.sh ${toFolder}
cp ${server_folder}/shutdown.sh ${toFolder}
fi
echo_yellow ">>>>>>>>>>>>>>>>>>>${os}-${arch}打包构建完成<<<<<<<<<<<<<<<<<<<<\n"
}
@@ -99,45 +101,100 @@ function buildMac() {
build "$1/mayfly-go-mac" "darwin" "amd64" $2
}
function runBuild() {
# 构建结果的目的路径
read -p "请输入构建产物输出目录: " toPath
if [ ! -d ${toPath} ] ; then
echo_red "构建产物输出目录不存在!"
exit;
fi
# 进入目标路径,并赋值全路径
cd ${toPath}
toPath=`pwd`
function buildDocker() {
echo_yellow "-------------------构建docker镜像开始-------------------"
imageVersion=$1
cd ${server_folder}
imageName="mayflygo/mayfly-go:${imageVersion}"
docker build -t "${imageName}" .
echo_green "docker镜像构建完成->[${imageName}]"
echo_yellow "-------------------构建docker镜像结束-------------------"
}
read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static/static]: " runBuildWeb
read -p "请选择构建版本[0|其他->全部 1->linux-amd64 2->linux-arm64 3->windows 4->mac]: " buildType
if [ "${runBuildWeb}" == "1" ] || [ "${runBuildWeb}" == "2" ] ; then
buildWeb ${runBuildWeb}
function buildxDocker() {
echo_yellow "-------------------docker buildx构建镜像开始-------------------"
imageVersion=$1
cd ${server_folder}
imageName="mayflygo/mayfly-go:${imageVersion}"
docker buildx build --push --platform linux/amd64,linux/arm64 -t "${imageName}" .
echo_green "docker多版本镜像构建完成->[${imageName}]"
echo_yellow "-------------------docker buildx构建镜像结束-------------------"
}
function runBuild() {
read -p "请选择构建版本[0|其他->除docker镜像外其他 1->linux-amd64 2->linux-arm64 3->windows 4->mac 5->docker 6->docker buildx]: " buildType
toPath="."
imageVersion="latest"
copyDocScript="1"
if [[ "${buildType}" != "5" ]] && [[ "${buildType}" != "6" ]] ; then
# 构建结果的目的路径
read -p "请输入构建产物输出目录[默认当前路径]: " toPath
if [ ! -d ${toPath} ] ; then
echo_red "构建产物输出目录不存在!"
exit;
fi
if [ "${toPath}" == "" ] ; then
toPath="."
fi
read -p "是否拷贝文档&脚本[0->否 1->是][默认是]: " copyDocScript
if [ "${copyDocScript}" == "" ] ; then
copyDocScript="1"
fi
# 进入目标路径,并赋值全路径
cd ${toPath}
toPath=`pwd`
fi
if [[ "${buildType}" == "5" ]] || [[ "${buildType}" == "6" ]] ; then
read -p "请输入docker镜像版本号[默认latest]: " imageVersion
if [ "${imageVersion}" == "" ] ; then
imageVersion="latest"
fi
fi
# read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static/static]: " runBuildWeb
runBuildWeb="2"
# 编译web前端
buildWeb ${runBuildWeb}
case ${buildType} in
"1")
buildLinuxAmd64 ${toPath} ${runBuildWeb}
buildLinuxAmd64 ${toPath} ${copyDocScript}
;;
"2")
buildLinuxArm64 ${toPath} ${runBuildWeb}
buildLinuxArm64 ${toPath} ${copyDocScript}
;;
"3")
buildWindows ${toPath} ${runBuildWeb}
buildWindows ${toPath} ${copyDocScript}
;;
"4")
buildMac ${toPath} ${runBuildWeb}
buildMac ${toPath} ${copyDocScript}
;;
"5")
buildDocker ${imageVersion}
;;
"6")
buildxDocker ${imageVersion}
;;
*)
buildLinuxAmd64 ${toPath} ${runBuildWeb}
buildLinuxArm64 ${toPath} ${runBuildWeb}
buildWindows ${toPath} ${runBuildWeb}
buildMac ${toPath} ${runBuildWeb}
buildLinuxAmd64 ${toPath} ${copyDocScript}
buildLinuxArm64 ${toPath} ${copyDocScript}
buildWindows ${toPath} ${copyDocScript}
buildMac ${toPath} ${copyDocScript}
;;
esac
echo_green "删除['${server_folder}/static/static']下静态资源文件."
# 删除静态资源文件保留一个favicon.ico否则后端启动会报错
rm -rf ${server_folder}/static/static/assets
rm -rf ${server_folder}/static/static/config.js
rm -rf ${server_folder}/static/static/index.html
}
runBuild

View File

@@ -4,32 +4,34 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"build-preview": "npm run build && npm run preview",
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.0.10",
"asciinema-player": "^3.0.1",
"axios": "^1.2.0",
"@element-plus/icons-vue": "^2.1.0",
"asciinema-player": "^3.1.0",
"axios": "^1.3.4",
"countup.js": "^2.0.7",
"cropperjs": "^1.5.11",
"echarts": "^5.4.0",
"element-plus": "^2.2.26",
"jsencrypt": "^3.2.1",
"element-plus": "^2.2.33",
"jsencrypt": "^3.3.1",
"lodash": "^4.17.21",
"mitt": "^3.0.0",
"monaco-editor": "^0.34.1",
"monaco-sql-languages": "^0.9.5",
"monaco-editor": "^0.36.1",
"monaco-sql-languages": "^0.11.0",
"monaco-themes": "^0.4.2",
"nprogress": "^0.2.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.13.0",
"sql-formatter": "^9.2.0",
"vue": "^3.2.45",
"sql-formatter": "^12.1.2",
"vue": "^3.2.47",
"vue-clipboard3": "^1.0.1",
"vue-router": "^4.1.6",
"vuex": "^4.0.2",
"xterm": "^5.0.0",
"xterm-addon-fit": "^0.6.0"
"xterm": "^5.1.0",
"xterm-addon-fit": "^0.7.0"
},
"devDependencies": {
"@types/lodash": "^4.14.178",
@@ -41,13 +43,13 @@
"@vitejs/plugin-vue": "^2.3.3",
"@vue/compiler-sfc": "^3.0.11",
"dotenv": "^10.0.0",
"eslint": "^8.5.0",
"eslint": "^8.35.0",
"eslint-plugin-vue": "^8.2.0",
"prettier": "^2.3.0",
"sass": "^1.45.1",
"sass-loader": "^12.4.0",
"typescript": "^4.7.4",
"vite": "^2.9.13",
"sass": "^1.58.0",
"sass-loader": "^13.2.0",
"typescript": "^4.9.5",
"vite": "^4.1.4",
"vue-eslint-parser": "^8.0.1"
},
"browserslist": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -19,24 +19,6 @@ class Api {
this.method = method;
}
/**
* 设置rl
* @param {String} uri 请求url
*/
setUrl(url: string) {
this.url = url;
return this;
}
/**
* url的请求方法
* @param {String} method 请求方法
*/
setMethod(method: string) {
this.method = method;
return this;
}
/**
* 获取权限的完整url
*/

View File

@@ -1,9 +1,17 @@
function getBaseApiUrl() {
let path = window.location.pathname;
if (path == '/') {
return window.location.host;
}
return window.location.host + path;
}
const config = {
baseApiUrl: `${(window as any).globalConfig.BaseApiUrl}/api`,
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${location.host}`}/api`,
baseApiUrl: `${(window as any).globalConfig.BaseApiUrl || location.protocol + '//' + getBaseApiUrl()}/api`,
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.3.1'
version: 'v1.4.1'
}
export default config

View File

@@ -0,0 +1,39 @@
export function exportCsv(filename: string, columns: string[], datas: []) {
// 二维数组
const cvsData = [columns];
for (let data of datas) {
// 数据值组成的一维数组
let dataValueArr: any = [];
for (let column of columns) {
let val: any = data[column];
if (typeof val == 'string' && val) {
// csv格式如果有逗号整体用双引号括起来如果里面还有双引号就替换成两个双引号这样导出来的格式就不会有问题了
if (val.indexOf(',') != -1) {
// 如果还有双引号,先将双引号转义,避免两边加了双引号后转义错误
if (val.indexOf('"') != -1) {
val = val.replace(/\"/g, "\"\"");
}
// 再将逗号转义
val = `"${val}"`;
}
dataValueArr.push(val);
} else {
dataValueArr.push(val);
}
}
cvsData.push(dataValueArr);
}
const csvString = cvsData.map((e) => e.join(',')).join('\n');
// 导出
let link = document.createElement('a');
let exportContent = '\uFEFF';
let blob = new Blob([exportContent + csvString], {
type: 'text/plain;charset=utrf-8',
});
link.id = 'download-csv';
link.setAttribute('href', URL.createObjectURL(blob));
link.setAttribute('download', `${filename}.csv`);
document.body.appendChild(link);
link.click();
}

View File

@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
import loadingCss from '@/theme/loading.scss';
import loadingCss from "@/theme/loading.scss?inline"
// 定义方法
export const NextLoading = {

View File

@@ -18,6 +18,8 @@ import SvgIcon from '@/components/svgIcon/index.vue';
import '@/assets/font/font.css'
const app = createApp(App);
// 屏蔽警告信息
app.config.warnHandler = () => null;
/**
* 导出全局注册 element plus svg 图标

View File

@@ -6,14 +6,14 @@ export const imports = {
"Home": () => import('@/views/home/Home.vue'),
'Personal': () => import('@/views/personal/index.vue'),
// machine
"MachineList": () => import('@/views/ops/machine'),
"MachineList": () => import('@/views/ops/machine/MachineList.vue'),
"AuthCertList": () => import('@/views/ops/machine/authcert/AuthCertList.vue'),
// sys
"ResourceList": () => import('@/views/system/resource'),
"RoleList": () => import('@/views/system/role'),
"AccountList": () => import('@/views/system/account'),
"SyslogList": () => import('@/views/system/syslog/SyslogList.vue'),
"ConfigList": () => import('@/views/system/config/ConfigList.vue'),
// tag
"TagTreeList": () => import('@/views/ops/tag/TagTreeList.vue'),
"TeamList": () => import('@/views/ops/tag/TeamList.vue'),

View File

@@ -124,7 +124,7 @@ export const staticRoutes: Array<RouteRecordRaw> = [
name: 'login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登',
title: '登',
},
},
{

View File

@@ -5,9 +5,6 @@ import themeConfig from '@/store/modules/themeConfig.ts';
import routesList from '@/store/modules/routesList.ts';
import keepAliveNames from '@/store/modules/keepAliveNames.ts';
import userInfos from '@/store/modules/userInfos.ts';
import sqlExecInfo from '@/store/modules/mysqlDbOptInfo.ts';
import redisDbOptInfo from '@/store/modules/redisDbOptInfo.ts';
import mongoDbOptInfo from '@/store/modules/mongoDbOptInfo.ts';
export const key: InjectionKey<Store<RootStateTypes>> = Symbol();
@@ -17,9 +14,6 @@ export const store = createStore<RootStateTypes>({
routesList,
keepAliveNames,
userInfos,
sqlExecInfo,
redisDbOptInfo,
mongoDbOptInfo,
},
});

View File

@@ -72,14 +72,6 @@ export interface UserInfosState {
userInfos: object;
}
// 数据操作信息
export interface DbOptInfoState {
dbOptInfo: {
tagPath?: string,
dbId?: number,
db?: string,
}
}
// 后端返回原始路由(未处理时)
// export interface RequestOldRoutesState {
@@ -92,8 +84,5 @@ export interface RootStateTypes {
routesList: RoutesListState;
keepAliveNames: KeepAliveNamesState;
userInfos: UserInfosState;
sqlExecInfo: DbOptInfoState;
redisDbOptInfo: DbOptInfoState;
mongoDbOptInfo: DbOptInfoState;
// requestOldRoutes: RequestOldRoutesState;
}

View File

@@ -1,30 +0,0 @@
import { Module } from 'vuex';
// 此处加上 `.ts` 后缀报错,具体原因不详
import {DbOptInfoState, RootStateTypes} from '@/store/interface';
const mongoDbOptInfoModule: Module<DbOptInfoState, RootStateTypes> = {
namespaced: true,
state: {
dbOptInfo: {
tagPath: '',
dbId: 0,
db: '0',
},
},
mutations: {
// 设置用户信息
getMongoDbOptInfo(state: any, data: object) {
state.dbOptInfo = data;
},
},
actions: {
// 设置用户信息
async setMongoDbOptInfo({ commit }, data: object) {
if (data) {
commit('getMongoDbOptInfo', data);
}
},
},
};
export default mongoDbOptInfoModule;

View File

@@ -1,30 +0,0 @@
import { Module } from 'vuex';
// 此处加上 `.ts` 后缀报错,具体原因不详
import { DbOptInfoState, RootStateTypes } from '@/store/interface';
const sqlExecInfoModule: Module<DbOptInfoState, RootStateTypes> = {
namespaced: true,
state: {
dbOptInfo: {
tagPath: '',
dbId: 0,
db: '0',
}
},
mutations: {
// 设置用户信息
getSqlExecInfo(state: any, data: object) {
state.dbOptInfo = data;
},
},
actions: {
// 设置用户信息
async setSqlExecInfo({ commit }, data: object) {
if (data) {
commit('getSqlExecInfo', data);
}
},
},
};
export default sqlExecInfoModule;

View File

@@ -1,30 +0,0 @@
import { Module } from 'vuex';
// 此处加上 `.ts` 后缀报错,具体原因不详
import {DbOptInfoState, RootStateTypes} from '@/store/interface';
const redisDbOptInfoModule: Module<DbOptInfoState, RootStateTypes> = {
namespaced: true,
state: {
dbOptInfo: {
tagPath: '',
dbId: 0,
db: '0',
},
},
mutations: {
// 设置用户信息
getRedisDbOptInfo(state: any, data: object) {
state.dbOptInfo = data;
},
},
actions: {
// 设置用户信息
async setRedisDbOptInfo({ commit }, data: object) {
if (data) {
commit('getRedisDbOptInfo', data);
}
},
},
};
export default redisDbOptInfoModule;

View File

@@ -107,13 +107,13 @@ const themeConfigModule: Module<ThemeConfigState, RootStateTypes> = {
layout: 'classic',
// ssh终端字体颜色
terminalForeground: '#7e9192',
terminalForeground: '#C5C8C6',
// ssh终端背景色
terminalBackground: '#002833',
terminalBackground: '#121212',
// ssh终端cursor色
terminalCursor: '#268F81',
terminalFontSize: 15,
terminalFontWeight: 'normal',
terminalCursor: '#F0CC09',
terminalFontSize: 14,
terminalFontWeight: 'bold',
// 编辑器主题
editorTheme: 'vs',

View File

@@ -1,225 +1,261 @@
/* 初始化样式
------------------------------- */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
outline: none !important;
margin: 0;
padding: 0;
box-sizing: border-box;
outline: none !important;
}
html,
body,
#app {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
background-color: #f8f8f8;
font-size: 14px;
overflow: hidden;
position: relative;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
background-color: #f8f8f8;
font-size: 14px;
overflow: hidden;
position: relative;
}
/* 主布局样式
------------------------------- */
.layout-container {
width: 100%;
height: 100%;
.layout-aside {
background: var(--bg-menuBar);
box-shadow: 2px 0 6px rgb(0 21 41 / 1%);
height: inherit;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
overflow-x: hidden !important;
.el-scrollbar__view {
overflow: hidden;
}
}
.layout-header {
padding: 0 !important;
}
.layout-main {
padding: 0 !important;
overflow: hidden;
width: 100%;
background-color: #f8f8f8;
}
.el-scrollbar {
width: 100%;
}
.layout-view-bg-white {
background: white;
width: 100%;
height: 100%;
border-radius: 4px;
border: 1px solid #ebeef5;
}
.layout-el-aside-br-color {
border-right: 1px solid rgb(238, 238, 238);
}
.layout-aside-width-default {
width: 220px !important;
transition: width 0.3s ease;
}
.layout-aside-width64 {
width: 64px !important;
transition: width 0.3s ease;
}
.layout-aside-width1 {
width: 1px !important;
transition: width 0.3s ease;
}
.layout-scrollbar {
@extend .el-scrollbar;
padding: 10px;
}
.layout-mian-height-50 {
height: calc(100vh - 50px);
}
.layout-columns-warp {
flex: 1;
display: flex;
overflow: hidden;
}
.layout-hide {
display: none;
}
width: 100%;
height: 100%;
.layout-aside {
background: var(--bg-menuBar);
box-shadow: 2px 0 6px rgb(0 21 41 / 1%);
height: inherit;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
overflow-x: hidden !important;
.el-scrollbar__view {
overflow: hidden;
}
}
.layout-header {
padding: 0 !important;
}
.layout-main {
padding: 0 !important;
overflow: hidden;
width: 100%;
background-color: #f8f8f8;
}
.el-scrollbar {
width: 100%;
}
.layout-view-bg-white {
background: white;
width: 100%;
height: 100%;
border-radius: 4px;
border: 1px solid #ebeef5;
}
.layout-el-aside-br-color {
border-right: 1px solid rgb(238, 238, 238);
}
.layout-aside-width-default {
width: 220px !important;
transition: width 0.3s ease;
}
.layout-aside-width64 {
width: 64px !important;
transition: width 0.3s ease;
}
.layout-aside-width1 {
width: 1px !important;
transition: width 0.3s ease;
}
.layout-scrollbar {
@extend .el-scrollbar;
padding: 10px;
}
.layout-mian-height-50 {
height: calc(100vh - 50px);
}
.layout-columns-warp {
flex: 1;
display: flex;
overflow: hidden;
}
.layout-hide {
display: none;
}
}
/* element plus 全局样式
------------------------------- */
.layout-breadcrumb-seting {
.el-drawer__header {
padding: 0 15px !important;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid rgb(230, 230, 230);
}
.el-divider {
background-color: rgb(230, 230, 230);
}
.el-drawer__header {
padding: 0 15px !important;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid rgb(230, 230, 230);
}
.el-divider {
background-color: rgb(230, 230, 230);
}
}
/* nprogress 进度条跟随主题颜色
------------------------------- */
#nprogress {
.bar {
background: var(--color-primary) !important;
z-index: 9999999 !important;
}
.bar {
background: var(--color-primary) !important;
z-index: 9999999 !important;
}
}
/* flex 弹性布局
------------------------------- */
.flex {
display: flex;
display: flex;
}
.flex-auto {
flex: 1;
flex: 1;
}
.flex-center {
@extend .flex;
flex-direction: column;
width: 100%;
overflow: hidden;
@extend .flex;
flex-direction: column;
width: 100%;
overflow: hidden;
}
.flex-margin {
margin: auto;
margin: auto;
}
.flex-warp {
display: flex;
flex-wrap: wrap;
align-content: flex-start;
margin: 0 -5px;
.flex-warp-item {
padding: 5px;
.flex-warp-item-box {
width: 100%;
height: 100%;
}
}
display: flex;
flex-wrap: wrap;
align-content: flex-start;
margin: 0 -5px;
.flex-warp-item {
padding: 5px;
.flex-warp-item-box {
width: 100%;
height: 100%;
}
}
}
/* 宽高 100%
------------------------------- */
.w100 {
width: 100% !important;
width: 100% !important;
}
.h100 {
height: 100% !important;
height: 100% !important;
}
.vh100 {
height: 100vh !important;
height: 100vh !important;
}
.max100vh {
max-height: 100vh !important;
max-height: 100vh !important;
}
.min100vh {
min-height: 100vh !important;
min-height: 100vh !important;
}
/* 颜色值
------------------------------- */
.color-primary {
color: var(--color-primary);
color: var(--color-primary);
}
.color-success {
color: var(--color-success);
color: var(--color-success);
}
.color-warning {
color: var(--color-warning);
color: var(--color-warning);
}
.color-danger {
color: var(--color-danger);
color: var(--color-danger);
}
.color-info {
color: var(--color-info);
color: var(--color-info);
}
/* 字体大小全局样式
------------------------------- */
@for $i from 10 through 32 {
.font#{$i} {
font-size: #{$i}px !important;
}
.font#{$i} {
font-size: #{$i}px !important;
}
}
/* 外边距、内边距全局样式
------------------------------- */
@for $i from 1 through 35 {
.mt#{$i} {
margin-top: #{$i}px !important;
}
.mr#{$i} {
margin-right: #{$i}px !important;
}
.mb#{$i} {
margin-bottom: #{$i}px !important;
}
.ml#{$i} {
margin-left: #{$i}px !important;
}
.pt#{$i} {
padding-top: #{$i}px !important;
}
.pr#{$i} {
padding-right: #{$i}px !important;
}
.pb#{$i} {
padding-bottom: #{$i}px !important;
}
.pl#{$i} {
padding-left: #{$i}px !important;
}
.mt#{$i} {
margin-top: #{$i}px !important;
}
.mr#{$i} {
margin-right: #{$i}px !important;
}
.mb#{$i} {
margin-bottom: #{$i}px !important;
}
.ml#{$i} {
margin-left: #{$i}px !important;
}
.pt#{$i} {
padding-top: #{$i}px !important;
}
.pr#{$i} {
padding-right: #{$i}px !important;
}
.pb#{$i} {
padding-bottom: #{$i}px !important;
}
.pl#{$i} {
padding-left: #{$i}px !important;
}
}
@@ -249,15 +285,22 @@ body,
.el-menu .fa:not(.is-children) {
font-size: 14px;
}
.gray-mode{
.gray-mode {
filter: grayscale(100%);
}
.fade-enter-active, .fade-leave-active {
.fade-enter-active,
.fade-leave-active {
transition: opacity .2s ease-in-out;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
.fade-enter,
.fade-leave-to
/* .fade-leave-active below version 2.1.8 */
{
opacity: 0;
}
@@ -270,7 +313,7 @@ body,
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
}
.toolbar {
width: 100%;
@@ -293,4 +336,28 @@ body,
.el-table-z-index-inherit .el-table .el-table__cell {
z-index: inherit !important;
}
.f12 {
font-size: 12px
}
// 图标垂直居中
.icon-middle {
.el-icon {
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
vertical-align: middle;
}
}
.img-icon {
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
vertical-align: middle;
height: 16px;
width: 16px;
}

View File

@@ -24,10 +24,10 @@
</div> -->
</div>
</div>
<div class="login-copyright">
<!-- <div class="login-copyright">
<div class="mb5 login-copyright-company">mayfly</div>
<div class="login-copyright-msg">mayfly</div>
</div>
</div> -->
</div>
</template>

View File

@@ -0,0 +1,61 @@
<template>
<div style="width: 100%">
<el-select @focus="getSshTunnelMachines" @change="change" style="width: 100%" v-model="sshTunnelMachineId"
@clear="clear" placeholder="请选择SSH隧道机器" clearable>
<el-option v-for="item in sshTunnelMachineList" :key="item.id" :label="`${item.ip}:${item.port} [${item.name}]`"
:value="item.id">
</el-option>
</el-select>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted } from 'vue';
import { machineApi } from '../machine/api';
const props = defineProps({
modelValue: {
type: Number,
},
})
//定义事件
const emit = defineEmits(['update:modelValue'])
const state = reactive({
// 单选则为id多选为id数组
sshTunnelMachineId: null as any,
sshTunnelMachineList: [] as any,
});
const {
sshTunnelMachineId,
sshTunnelMachineList,
} = toRefs(state)
onMounted(async () => {
if (!props.modelValue || props.modelValue <= 0) {
state.sshTunnelMachineId = null;
} else {
state.sshTunnelMachineId = props.modelValue;
}
await getSshTunnelMachines();
});
const getSshTunnelMachines = async () => {
if (state.sshTunnelMachineList.length == 0) {
const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
state.sshTunnelMachineList = res.list;
}
};
const clear = () => {
state.sshTunnelMachineId = null;
change();
}
const change = () => {
emit('update:modelValue', state.sshTunnelMachineId);
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,65 @@
<template>
<div
style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer;vertical-align: middle;">
<el-popover @show="showTagInfo" placement="top-start" title="标签信息" :width="300" trigger="hover">
<template #reference>
<el-icon>
<InfoFilled />
</el-icon>
</template>
<span v-for="(v, i) in tags" :key="i">
<el-tooltip effect="customized" :content="v.remark" placement="top">
<span class="color-success">{{ v.name }}</span>
</el-tooltip>
<span v-if="i != state.tags.length - 1" class="color-primary"> / </span>
</span>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { reactive, toRefs, onMounted } from 'vue';
import { tagApi } from '../tag/api';
const props = defineProps({
tagPath: {
type: [String],
required: true,
},
})
const state = reactive({
tagPath: '',
tags: [] as any,
})
const {
tags,
} = toRefs(state)
onMounted(async () => {
state.tagPath = props.tagPath;
})
const showTagInfo = async () => {
if (state.tags && state.tags.length > 0) {
return;
}
const tagStrs = state.tagPath.split('/');
const tagPaths = [];
let nowTag = '';
for (let tagStr of tagStrs) {
if (nowTag) {
nowTag = `${nowTag}/${tagStr}`
} else {
nowTag = tagStr
}
tagPaths.push(nowTag)
}
state.tags = await tagApi.listByQuery.request({ tagPaths: tagPaths.join(',') })
}
</script>
<style lang="scss">
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-tree-select @check="changeTag" style="width: 100%" v-model="selectTags" :data="tags"
<el-tree-select @check="changeTag" style="width: 100%" v-model="selectTags" :data="tags" placeholder="请选择关联标签"
:render-after-expand="true" :default-expanded-keys="[selectTags]" show-checkbox check-strictly node-key="id"
:props="{
value: 'id',

View File

@@ -0,0 +1,122 @@
<template>
<div class="instances-box layout-aside">
<el-row type="flex" justify="space-between">
<el-col :span="24" class="el-scrollbar flex-auto" style="overflow: auto">
<el-input v-model="filterText" placeholder="输入关键字->搜索已展开节点信息" clearable size="small" class="mb5" />
<el-tree ref="treeRef" :style="{ maxHeight: state.height, height: state.height, overflow: 'auto' }"
:highlight-current="true" :indent="7" :load="loadNode" :props="treeProps" lazy node-key="key"
:expand-on-click-node="true" :filter-node-method="filterNode" @node-click="treeNodeClick" @node-expand="treeNodeClick">
<template #default="{ node, data }">
<span class="icon-middle ">
<span v-if="data.type == TagTreeNode.TagPath">
<tag-info :tag-path="data.label" />
</span>
<slot v-else :node="node" :data="data" name="prefix"></slot>
<span class="ml3">
<slot name="label" :data="data"> {{ data.label }}</slot>
</span>
</span>
</template>
</el-tree>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
import { TagTreeNode } from './tag';
import TagInfo from './TagInfo.vue';
const props = defineProps({
height: {
type: [Number, String],
default: 0
},
load: {
type: Function,
required: true,
}
})
const treeProps = {
label: 'name',
children: 'zones',
isLeaf: 'isLeaf',
}
const emit = defineEmits(['nodeClick'])
const treeRef: any = ref(null)
const state = reactive({
height: 600 as any,
filterText: '',
opend: {},
})
const { filterText } = toRefs(state)
onMounted(async () => {
if (!props.height) {
state.height = window.innerHeight - 145 + 'px';
} else {
state.height = props.height;
}
})
watch(filterText, (val) => {
treeRef.value?.filter(val)
})
const filterNode = (value: string, data: any) => {
if (!value) return true
return data.label.includes(value)
}
/**
* 加载树节点
* @param { Object } node
* @param { Object } resolve
*/
const loadNode = async (node: any, resolve: any) => {
if (typeof resolve !== 'function') {
return;
}
return resolve(await props.load(node));
};
const treeNodeClick = (data: any) => {
emit('nodeClick', data);
}
const reloadNode = (nodeKey: any) => {
let node = getNode(nodeKey);
node.loaded = false;
node.expand();
}
const getNode = (nodeKey: any) => {
let node = treeRef.value.getNode(nodeKey);
if (!node) {
throw new Error('未找到节点: ' + nodeKey);
}
return node;
}
defineExpose({
reloadNode,
})
</script>
<style lang="scss">
.instances-box {
overflow: 'auto';
.el-tree {
display: inline-block;
min-width: 100%;
}
}
</style>

View File

@@ -0,0 +1,38 @@
export class TagTreeNode {
/**
* 节点id
*/
key: any
/**
* 节点名称
*/
label: string
/**
* 树节点类型
*/
type: any
isLeaf: boolean = false;
params: any;
static TagPath = -1;
constructor(key: any, label: string, type?: any) {
this.key = key;
this.label = label;
this.type = type || TagTreeNode.TagPath;
}
withIsLeaf(isLeaf: boolean) {
this.isLeaf = isLeaf;
return this;
}
withParams(params: any) {
this.params = params;
return this;
}
}

View File

@@ -3,88 +3,85 @@
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false"
:destroy-on-close="true" width="38%">
<el-form :model="form" ref="dbForm" :rules="rules" label-width="95px">
<el-form-item prop="tagId" label="标签:" required>
<tag-select v-model:tag-id="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签:" required>
<tag-select v-model:tag-id="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="name" label="别名:" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="type" label="类型:" required>
<el-select style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
<el-option key="item.id" label="mysql" value="mysql"> </el-option>
<el-option key="item.id" label="postgres" value="postgres"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host:" required>
<el-col :span="18">
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip"
auto-complete="off"></el-input>
</el-col>
<el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5">
<el-input type="number" v-model.number="form.port" placeholder="请输入端口"></el-input>
</el-col>
</el-form-item>
<el-form-item prop="username" label="用户名:" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码,修改操作可不填"
autocomplete="new-password">
<template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click"
:content="pwd">
<template #reference>
<el-link @click="getDbPwd" :underline="false" type="primary" class="mr5">原密码
</el-link>
<el-form-item prop="name" label="别名:" required>
<el-input v-model.trim="form.name" placeholder="请输入数据库别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="type" label="类型:" required>
<el-select style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
<el-option key="item.id" label="mysql" value="mysql"> </el-option>
<el-option key="item.id" label="postgres" value="postgres"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host:" required>
<el-col :span="18">
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip"
auto-complete="off"></el-input>
</el-col>
<el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5">
<el-input type="number" v-model.number="form.port" placeholder="请输入端口"></el-input>
</el-col>
</el-form-item>
<el-form-item prop="username" label="用户名:" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码,修改操作可不填"
autocomplete="new-password">
<template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click"
:content="pwd">
<template #reference>
<el-link @click="getDbPwd" :underline="false" type="primary" class="mr5">原密码
</el-link>
</template>
</el-popover>
</template>
</el-popover>
</template>
</el-input>
</el-form-item>
<el-form-item prop="params" label="连接参数:">
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
<template #suffix>
<el-link target="_blank" href="https://github.com/go-sql-driver/mysql#parameters"
:underline="false" type="primary" class="mr5">参数参考</el-link>
</template>
</el-input>
</el-form-item>
<el-form-item prop="database" label="数据库名:" required>
<el-col :span="19">
<el-select @change="changeDatabase" v-model="databaseList" multiple clearable collapse-tags
collapse-tags-tooltip filterable allow-create placeholder="请确保数据库实例信息填写完整后获取库名"
style="width: 100%">
<el-option v-for="db in allDatabases" :key="db" :label="db" :value="db" />
</el-select>
</el-col>
<el-col style="text-align: center" :span="1">
<el-divider direction="vertical" border-style="dashed" />
</el-col>
<el-col :span="4">
<el-link @click="getAllDatabase" :underline="false" type="success">获取库名</el-link>
</el-col>
</el-form-item>
</el-input>
</el-form-item>
<el-form-item prop="database" label="数据库名:" required>
<el-col :span="19">
<el-select @change="changeDatabase" v-model="databaseList" multiple clearable collapse-tags
collapse-tags-tooltip filterable allow-create placeholder="请确保数据库实例信息填写完整后获取库名"
style="width: 100%">
<el-option v-for="db in allDatabases" :key="db" :label="db" :value="db" />
</el-select>
</el-col>
<el-col style="text-align: center" :span="1">
<el-divider direction="vertical" border-style="dashed" />
</el-col>
<el-col :span="4">
<el-link @click="getAllDatabase" :underline="false" type="success">获取库名</el-link>
</el-col>
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>
</el-tab-pane>
<el-form-item prop="enableSshTunnel" label="SSH隧道:">
<el-col :span="3">
<el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1"
:false-label="-1"></el-checkbox>
</el-col>
<el-col :span="5" v-if="form.enableSshTunnel == 1"> 机器: </el-col>
<el-col :span="16" v-if="form.enableSshTunnel == 1">
<el-select style="width: 100%" v-model="form.sshTunnelMachineId" placeholder="请选择SSH隧道机器">
<el-option v-for="item in sshTunnelMachineList" :key="item.id"
:label="`${item.ip}:${item.port} [${item.name}]`" :value="item.id">
</el-option>
</el-select>
</el-col>
</el-form-item>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="params" label="连接参数:">
<el-input v-model.trim="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2">
<template #suffix>
<el-link target="_blank" href="https://github.com/go-sql-driver/mysql#parameters"
:underline="false" type="primary" class="mr5">参数参考</el-link>
</template>
</el-input>
</el-form-item>
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
@@ -100,11 +97,11 @@
<script lang="ts" setup>
import { toRefs, reactive, watch, ref } from 'vue';
import { dbApi } from './api';
import { machineApi } from '../machine/api.ts';
import { ElMessage } from 'element-plus';
import { notBlank } from '@/common/assert';
import { RsaEncrypt } from '@/common/rsa';
import TagSelect from '../component/TagSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
const props = defineProps({
visible: {
@@ -170,9 +167,9 @@ const dbForm: any = ref(null);
const state = reactive({
dialogVisible: false,
tabActiveName: 'basic',
allDatabases: [] as any,
databaseList: [] as any,
sshTunnelMachineList: [] as any,
form: {
id: null,
tagId: null as any,
@@ -185,13 +182,8 @@ const state = reactive({
password: null,
params: null,
database: '',
project: null,
projectId: null,
envId: null,
env: null,
remark: '',
enableSshTunnel: null,
sshTunnelMachineId: null,
sshTunnelMachineId: null as any,
},
// 原密码
pwd: '',
@@ -200,9 +192,9 @@ const state = reactive({
const {
dialogVisible,
tabActiveName,
allDatabases,
databaseList,
sshTunnelMachineList,
form,
pwd,
btnLoading,
@@ -213,15 +205,15 @@ watch(props, (newValue: any) => {
if (!state.dialogVisible) {
return;
}
state.tabActiveName = 'basic';
if (newValue.db) {
state.form = { ...newValue.db };
// 将数据库名使用空格切割,获取所有数据库列表
state.databaseList = newValue.db.database.split(' ');
} else {
state.form = { port: 3306, enableSshTunnel: -1 } as any;
state.form = { port: 3306 } as any;
state.databaseList = [];
}
getSshTunnelMachines();
});
/**
@@ -231,13 +223,6 @@ const changeDatabase = () => {
state.form.database = state.databaseList.length == 0 ? '' : state.databaseList.join(' ');
};
const getSshTunnelMachines = async () => {
if (state.form.enableSshTunnel == 1 && state.sshTunnelMachineList.length == 0) {
const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
state.sshTunnelMachineList = res.list;
}
};
const getAllDatabase = async () => {
const reqForm = { ...state.form };
reqForm.password = await RsaEncrypt(reqForm.password);
@@ -257,6 +242,9 @@ const btnOk = async () => {
if (valid) {
const reqForm = { ...state.form };
reqForm.password = await RsaEncrypt(reqForm.password);
if (!state.form.sshTunnelMachineId) {
reqForm.sshTunnelMachineId = -1;
}
dbApi.saveDb.request(reqForm).then(() => {
ElMessage.success('保存成功');
emit('val-change', state.form);
@@ -287,6 +275,4 @@ const cancel = () => {
}, 500);
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@@ -20,7 +20,14 @@
</el-radio>
</template>
</el-table-column>
<el-table-column prop="tagPath" label="标签路径" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="tagPath" label="标签路径" min-width="150" show-overflow-tooltip>
<template #default="scope">
<tag-info :tag-path="scope.row.tagPath" />
<span class="ml5">
{{ scope.row.tagPath }}
</span>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip></el-table-column>
<el-table-column min-width="170" label="host:port" show-overflow-tooltip>
<template #default="scope">
@@ -45,10 +52,9 @@
</el-input>
<div class="el-tag--plain el-tag--success" v-for="db in filterDb.list" :key="db"
style="border:1px var(--color-success-light-3) solid; margin-top: 3px;border-radius: 5px; padding: 2px;position: relative">
<el-link type="success" plain size="small" :underline="false"
@click="showTableInfo(scope.row, db)">{{ db }}</el-link>
<el-link type="success" plain size="small" :underline="false">{{ db }}</el-link>
<el-link type="primary" plain size="small" :underline="false"
@click="openSqlExec(scope.row, db)" style="position: absolute; right: 4px">数据操作
@click="showTableInfo(scope.row, db)" style="position: absolute; right: 4px">操作
</el-link>
</div>
</el-popover>
@@ -181,6 +187,8 @@
size="small">DELETE</el-tag>
<el-tag v-if="scope.row.type == enums.DbSqlExecTypeEnum['INSERT'].value" color="#A8DEE0"
size="small">INSERT</el-tag>
<el-tag v-if="scope.row.type == enums.DbSqlExecTypeEnum['QUERY'].value" color="#A8DEE0"
size="small">QUERY</el-tag>
</template>
</el-table-column>
<el-table-column prop="sql" label="SQL" min-width="230" show-overflow-tooltip> </el-table-column>
@@ -256,7 +264,7 @@
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="3" label="数据库">{{ infoDialog.data.database }}</el-descriptions-item>
<el-descriptions-item :span="3" label="SSH隧道">{{ infoDialog.data.enableSshTunnel == 1 ? '是' : '否' }}
<el-descriptions-item :span="3" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }}
@@ -290,10 +298,9 @@ import config from '@/common/config';
import { getSession } from '@/common/utils/storage';
import { isTrue } from '@/common/assert';
import { Search as SearchIcon } from '@element-plus/icons-vue'
import router from '@/router';
import { store } from '@/store';
import { tagApi } from '../tag/api.ts';
import { dateFormat } from '@/common/utils/date';
import TagInfo from '../component/TagInfo.vue';
const permissions = {
saveDb: 'db:save',
@@ -315,7 +322,6 @@ const state = reactive({
*/
query: {
tagPath: null,
projectId: null,
pageNum: 1,
pageSize: 10,
},
@@ -695,20 +701,6 @@ const dropTable = async (row: any) => {
});
} catch (err) { }
};
const openSqlExec = (row: any, db: any) => {
// 判断db是否发生改变
let oldDb = store.state.sqlExecInfo.dbOptInfo.db;
if (db && oldDb !== db) {
const { tagPath, id } = row;
let params = {
tagPath,
dbId: id,
db
}
store.dispatch('sqlExecInfo/setSqlExecInfo', params);
}
router.push({ name: 'SqlExec' });
}
// 点击查看时初始化数据
const selectDb = (row: any) => {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,323 @@
<template>
<div>
<el-table @cell-dblclick="(row: any, column: any, cell: any, event: any) => cellClick(row, column, cell)"
@sort-change="(sort: any) => onTableSortChange(sort)" @selection-change="onDataSelectionChange"
:data="datas" size="small" :max-height="tableHeight" v-loading="loading" element-loading-text="查询中..."
:empty-text="emptyText" stripe border class="mt5">
<el-table-column v-if="datas.length > 0 && table" type="selection" width="35" />
<el-table-column min-width="100" :width="DbInst.flexColumnWidth(item, datas)" align="center"
v-for="item in columnNames" :key="item" :prop="item" :label="item" show-overflow-tooltip
:sortable="sortable">
<template #header v-if="showColumnTip">
<el-tooltip raw-content placement="top" effect="customized">
<template #content> {{ getColumnTip(item) }} </template>
{{ item }}
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script lang="ts" setup>
import { onMounted, watch, reactive, toRefs } from 'vue';
import { DbInst, UpdateFieldsMeta, FieldsMeta } from '../db';
const emits = defineEmits(['sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField'])
const props = defineProps({
dbId: {
type: Number,
required: true,
},
dbType: {
type: String,
default: ''
},
db: {
type: String,
required: true,
},
table: {
type: String,
default: '',
},
data: {
type: Array,
},
columnNames: {
type: Array,
},
sortable: {
type: [String, Boolean],
default: false,
},
loading: {
type: Boolean,
default: false,
},
emptyText: {
type: String,
default: '暂无数据',
},
showColumnTip: {
type: Boolean,
default: false,
},
height: {
type: String,
default: '600'
}
})
const state = reactive({
dbId: 0, // 当前选中操作的数据库实例
dbType: '',
db: '', // 数据库名
table: '', // 当前的表名
datas: [],
columnNames: [],
columns: [],
sortable: false,
loading: false,
selectionDatas: [] as any,
showColumnTip: false,
tableHeight: '600',
emptyText: '',
updatedFields: [] as UpdateFieldsMeta[],// 各个tab表被修改的字段信息
});
const {
tableHeight,
datas,
sortable,
loading,
columnNames,
showColumnTip,
} = toRefs(state);
watch(props, (newValue: any) => {
setState(newValue);
});
onMounted(async () => {
console.log('in DbTable mounted');
setState(props);
})
const setState = (props: any) => {
state.dbId = props.dbId;
state.dbType = props.dbType;
state.db = props.db;
state.table = props.table;
state.datas = props.data;
state.tableHeight = props.height;
state.sortable = props.sortable;
state.loading = props.loading;
state.columnNames = props.columnNames;
state.showColumnTip = props.showColumnTip;
state.emptyText = props.emptyText;
}
const getColumnTip = (columnName: string) => {
// 优先从 table map中获取
let columns = getNowDb().getColumns(state.table);
if (!columns) {
return '';
}
const column = columns.find((c: any) => c.columnName == columnName);
const comment = column.columnComment;
return `${column.columnType} ${comment ? ' | ' + comment : ''}`;
};
/**
* 表排序字段变更
*/
const onTableSortChange = async (sort: any) => {
if (!sort.prop) {
return;
}
cancelUpdateFields();
emits('sortChange', sort);
};
const onDataSelectionChange = (datas: []) => {
state.selectionDatas = datas;
emits('selectionChange', datas);
};
// 监听单元格点击事件
const cellClick = (row: any, column: any, cell: any) => {
const property = column.property;
// 如果当前操作的表名不存在 或者 当前列的property不存在(如多选框),则不允许修改当前单元格内容
if (!state.table || !property) {
return;
}
let div: HTMLElement = cell.children[0];
if (div && div.tagName === 'DIV') {
// 转为字符串比较,可能存在数字等
let text = (row[property] || row[property] == 0 ? row[property] : '') + '';
let input = document.createElement('input');
input.setAttribute('value', text);
// 将表格width也赋值于输入框避免输入框长度超过表格长度
input.setAttribute('style', 'height:23px;text-align:center;border:none;' + div.getAttribute('style'));
cell.replaceChildren(input);
input.focus();
input.addEventListener('blur', async () => {
row[property] = input.value;
cell.replaceChildren(div);
if (input.value !== text) {
let currentUpdatedFields = state.updatedFields
const dbInst = getNowDbInst();
// 主键
const primaryKey = await dbInst.loadTableColumn(state.db, state.table);
const primaryKeyValue = row[primaryKey.columnName];
// 更新字段列信息
const updateColumn = await dbInst.loadTableColumn(state.db, state.table, property);
const newField = {
div, row,
fieldName: column.rawColumnKey,
fieldType: updateColumn.columnType,
oldValue: text,
newValue: input.value
} as FieldsMeta;
// 被修改的字段
const primaryKeyFields = currentUpdatedFields.filter((meta) => meta.primaryKey === primaryKeyValue)
let hasKey = false;
if (primaryKeyFields.length <= 0) {
primaryKeyFields[0] = {
primaryKey: primaryKeyValue,
primaryKeyName: primaryKey.columnName,
primaryKeyType: primaryKey.columnType,
fields: [newField]
}
} else {
hasKey = true
let hasField = primaryKeyFields[0].fields.some(a => {
if (a.fieldName === newField.fieldName) {
a.newValue = newField.newValue
}
return a.fieldName === newField.fieldName
})
if (!hasField) {
primaryKeyFields[0].fields.push(newField)
}
}
let fields = primaryKeyFields[0].fields
const fieldsParam = fields.filter((a) => {
if (a.fieldName === column.rawColumnKey) {
a.newValue = input.value
}
return a.fieldName === column.rawColumnKey
})
const field = fieldsParam.length > 0 && fieldsParam[0] || {} as FieldsMeta
if (field.oldValue === input.value) { // 新值=旧值
// 删除数据
div.classList.remove('update_field_active')
let delIndex: number[] = [];
currentUpdatedFields.forEach((a, i) => {
if (a.primaryKey === primaryKeyValue) {
a.fields = a.fields && a.fields.length > 0 ? a.fields.filter(f => f.fieldName !== column.rawColumnKey) : [];
a.fields.length <= 0 && delIndex.push(i)
}
});
delIndex.forEach(i => delete currentUpdatedFields[i])
currentUpdatedFields = currentUpdatedFields.filter(a => a)
} else {
// 新增数据
div.classList.add('update_field_active')
if (hasKey) {
currentUpdatedFields.forEach((value, index, array) => {
if (value.primaryKey === primaryKeyValue) {
array[index].fields = fields
}
})
} else {
currentUpdatedFields.push({
primaryKey: primaryKeyValue,
primaryKeyName: primaryKey.columnName,
primaryKeyType: primaryKey.columnType,
fields
})
}
}
state.updatedFields = currentUpdatedFields;
changeUpdatedField();
}
});
}
};
const submitUpdateFields = () => {
let currentUpdatedFields = state.updatedFields;
if (currentUpdatedFields.length <= 0) {
return;
}
const db = state.db;
let res = '';
let divs: HTMLElement[] = [];
currentUpdatedFields.forEach(a => {
let sql = `UPDATE ${state.table} SET `;
let primaryKey = a.primaryKey;
let primaryKeyType = a.primaryKeyType;
let primaryKeyName = a.primaryKeyName;
a.fields.forEach(f => {
sql += ` ${f.fieldName} = ${DbInst.wrapColumnValue(f.fieldType, f.newValue)},`
divs.push(f.div)
})
sql = sql.substring(0, sql.length - 1)
sql += ` WHERE ${primaryKeyName} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKey)} ;`
res += sql;
})
DbInst.getInst(state.dbId).promptExeSql(db, res, () => { }, () => {
currentUpdatedFields = [];
divs.forEach(a => {
a.classList.remove('update_field_active');
})
state.updatedFields = [];
changeUpdatedField();
});
}
const cancelUpdateFields = () => {
state.updatedFields.forEach((a: any) => {
a.fields.forEach((b: any) => {
b.div.classList.remove('update_field_active')
b.row[b.fieldName] = b.oldValue
})
})
state.updatedFields = [];
changeUpdatedField();
}
const changeUpdatedField = () => {
emits('changeUpdatedField', state.updatedFields);
}
const getNowDb = () => {
return DbInst.getInst(state.dbId).getDb(state.db);
}
const getNowDbInst = () => {
return DbInst.getInst(state.dbId);
}
defineExpose({
submitUpdateFields,
cancelUpdateFields
})
</script>
<style lang="scss">
.update_field_active {
background-color: var(--el-color-success)
}
</style>

View File

@@ -0,0 +1,548 @@
<template>
<div>
<div>
<div class="toolbar">
<div class="fl">
<el-link @click="onRunSql()" :underline="false" class="ml15" icon="VideoPlay">
</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="format sql" placement="top">
<el-link @click="formatSql()" type="primary" :underline="false" icon="MagicStick">
</el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="commit" placement="top">
<el-link @click="onCommit()" type="success" :underline="false" icon="CircleCheck">
</el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-upload class="sql-file-exec" :before-upload="beforeUpload" :on-success="execSqlFileSuccess"
:headers="{ Authorization: token }" :action="getUploadSqlFileUrl()" :show-file-list="false"
name="file" multiple :limit="100">
<el-tooltip class="box-item" effect="dark" content="SQL脚本执行" placement="top">
<el-link type="success" :underline="false" icon="Document"></el-link>
</el-tooltip>
</el-upload>
</div>
<div style="float: right" class="fl">
<el-button @click="saveSql()" type="primary" icon="document-add" plain size="small">保存SQL
</el-button>
<el-button v-if="sqlName" @click="deleteSql()" type="danger" icon="delete" plain size="small">删除SQL
</el-button>
</div>
</div>
</div>
<div class="mt5 sqlEditor">
<div :id="'MonacoTextarea-' + ti.key" :style="{ height: editorHeight }">
</div>
</div>
<div class="mt5">
<el-row>
<el-link v-if="table" @click="onDeleteData()" class="ml5" type="danger" icon="delete"
:underline="false"></el-link>
<span v-if="execRes.data.length > 0">
<el-divider direction="vertical" border-style="dashed" />
<el-link type="success" :underline="false" @click="exportData"><span
style="font-size: 12px">导出</span></el-link>
</span>
<span v-if="hasUpdatedFileds">
<el-divider direction="vertical" border-style="dashed" />
<el-link type="success" :underline="false" @click="submitUpdateFields()"><span
style="font-size: 12px">提交</span></el-link>
</span>
<span v-if="hasUpdatedFileds">
<el-divider direction="vertical" border-style="dashed" />
<el-link type="warning" :underline="false" @click="cancelUpdateFields"><span
style="font-size: 12px">取消</span></el-link>
</span>
</el-row>
<db-table ref="dbTableRef" :db-id="state.ti.dbId" :db="state.ti.db"
:data="execRes.data" :table="state.table" :column-names="execRes.tableColumn" :loading="loading"
height="250" empty-text="tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改"
@selection-change="onDataSelectionChange" @change-updated-field="changeUpdatedField"></db-table>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, watch, onMounted, computed, reactive, toRefs, ref, Ref } from 'vue';
import { useStore } from '@/store/index.ts';
import { getSession } from '@/common/utils/storage';
import { isTrue, notBlank } from '@/common/assert';
import { format as sqlFormatter } from 'sql-formatter';
import config from '@/common/config';
import { ElMessage, ElMessageBox } from 'element-plus';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
import * as monaco from 'monaco-editor';
import { editor } from 'monaco-editor';
// 主题仓库 https://github.com/brijeshb42/monaco-themes
// 主题例子 https://editor.bitwiser.in/
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
import DbTable from '../DbTable.vue'
import { TabInfo } from '../../db';
import { exportCsv } from '@/common/utils/export';
import { dateStrFormat } from '@/common/utils/date';
import { dbApi } from '../../api';
const emits = defineEmits(['saveSqlSuccess', 'deleteSqlSuccess'])
const props = defineProps({
data: {
type: TabInfo,
required: true
},
// sql脚本名若有则去加载该sql内容
sqlName: {
type: String,
default: '',
},
editorHeight: {
type: String,
default: '600'
}
})
const store = useStore();
const token = getSession('token');
let monacoEditor = {} as editor.IStandaloneCodeEditor;
const dbTableRef = ref(null) as Ref;
const state = reactive({
token,
ti: {} as TabInfo,
dbs: [],
dbId: null, // 当前选中操作的数据库实例
table: '', // 当前单表操作sql的表信息
sqlName: '',
sql: '', // 当前编辑器的sql内容
loading: false, // 是否在加载数据
execRes: {
data: [],
tableColumn: []
},
selectionDatas: [] as any,
editorHeight: '500',
hasUpdatedFileds: false,
});
const {
editorHeight,
ti,
execRes,
table,
sqlName,
loading,
hasUpdatedFileds,
} = toRefs(state);
watch(() => props.editorHeight, (newValue: any) => {
state.editorHeight = newValue;
});
onMounted(async () => {
console.log('in query mounted');
state.ti = props.data;
state.editorHeight = props.editorHeight;
const params = state.ti.params;
state.dbs = params && params.dbs;
if (params && params.sqlName) {
state.sqlName = params.sqlName;
const res = await dbApi.getSql.request({ id: state.ti.dbId, type: 1, name: state.sqlName, db: state.ti.db });
state.sql = res.sql;
}
nextTick(() => {
setTimeout(() => initMonacoEditor(), 50)
})
await state.ti.getNowDbInst().loadDbHints(state.ti.db);
})
// 获取布局配置信息
const getThemeConfig: any = computed(() => {
return store.state.themeConfig.themeConfig;
});
self.MonacoEnvironment = {
getWorker() {
return new EditorWorker();
}
};
const initMonacoEditor = () => {
let monacoTextarea = document.getElementById('MonacoTextarea-' + state.ti.key) as HTMLElement
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
// 初始化一些主题
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
monacoEditor = monaco.editor.create(monacoTextarea, {
language: 'sql',
theme: getThemeConfig.value.editorTheme,
automaticLayout: true, //自适应宽高布局
folding: false,
roundedSelection: false, // 禁用选择文本背景的圆角
matchBrackets: 'near',
linkedEditing: true,
cursorBlinking: 'smooth',// 光标闪烁样式
mouseWheelZoom: true, // 在按住Ctrl键的同时使用鼠标滚轮时在编辑器中缩放字体
overviewRulerBorder: false, // 不要滚动条的边框
tabSize: 2, // tab 缩进长度
// fontFamily: 'JetBrainsMono', // 字体 暂时不要设置,否则光标容易错位
fontWeight: 'bold',
// letterSpacing: 1, 字符间距
// quickSuggestions:false, // 禁用代码提示
minimap: {
enabled: false, // 不要小地图
},
});
// 注册快捷键ctrl + R 运行选中的sql
monacoEditor.addAction({
// An unique identifier of the contributed action.
id: 'run-sql-action' + state.ti.key,
// A label of the action that will be presented to the user.
label: '执行SQL',
// A precondition for this action.
precondition: undefined,
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
keybindingContext: undefined,
keybindings: [
// chord
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, 0)
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5,
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: async function () {
try {
await onRunSql();
} catch (e: any) {
e.message && ElMessage.error(e.message)
}
}
});
// 注册快捷键ctrl + shift + f 格式化sql
monacoEditor.addAction({
// An unique identifier of the contributed action.
id: 'format-sql-action' + state.ti.key,
// A label of the action that will be presented to the user.
label: '格式化SQL',
// A precondition for this action.
precondition: undefined,
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
keybindingContext: undefined,
keybindings: [
// chord
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, 0)
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 2,
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: async function () {
try {
await formatSql();
} catch (e: any) {
e.message && ElMessage.error(e.message)
}
}
});
// 动态设置主题
// monaco.editor.setTheme('hc-black');
// 如果sql有值则默认赋值
if (state.sql) {
monacoEditor.getModel()?.setValue(state.sql);
}
};
/**
* 执行sql
*/
const onRunSql = async () => {
// 没有选中的文本,则为全部文本
let sql = getSql() as string;
notBlank(sql && sql.trim(), '请选中需要执行的sql');
// 去除字符串前的空格、换行等
sql = sql.replace(/(^\s*)/g, '');
let execRemark = '';
let canRun = true;
if (
sql.startsWith('update') ||
sql.startsWith('UPDATE') ||
sql.startsWith('INSERT') ||
sql.startsWith('insert') ||
sql.startsWith('DELETE') ||
sql.startsWith('delete')
) {
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/,
inputErrorMessage: '请输入执行该sql的备注信息',
});
execRemark = res.value;
if (!execRemark) {
canRun = false;
}
}
if (!canRun) {
return;
}
try {
state.loading = true;
const colAndData: any = await state.ti.getNowDbInst().runSql(state.ti.db, sql, execRemark);
if (!colAndData.res || colAndData.res.length === 0) {
ElMessage.warning('未查询到结果集')
}
state.execRes.data = colAndData.res;
state.execRes.tableColumn = colAndData.colNames;
cancelUpdateFields()
} catch (e: any) {
state.execRes.data = [];
state.execRes.tableColumn = [];
state.table = '';
return;
} finally {
state.loading = false;
}
// 即只有以该字符串开头的sql才可修改表数据内容
if (sql.startsWith('SELECT *') || sql.startsWith('select *') || sql.startsWith('SELECT\n *')) {
state.selectionDatas = [];
const tableName = sql.split(/from/i)[1];
if (tableName) {
const tn = tableName.trim().split(' ')[0];
state.table = tn;
state.table = tn;
} else {
state.table = '';
}
} else {
state.table = '';
}
};
/**
* 获取sql如果有鼠标选中则返回选中内容否则返回输入框内所有内容
*/
const getSql = () => {
let res = '' as string | undefined;
// 编辑器还没初始化
if (!monacoEditor?.getModel) {
return res;
}
// 选择选中的sql
let selection = monacoEditor.getSelection()
if (selection) {
res = monacoEditor.getModel()?.getValueInRange(selection)
}
// 整个编辑器的sql
if (!res) {
return monacoEditor.getModel()?.getValue()
}
return res
};
const saveSql = async () => {
const sql = monacoEditor.getModel()?.getValue();
notBlank(sql, 'sql内容不能为空');
let sqlName = state.sqlName;
const newSql = !sqlName;
if (newSql) {
try {
const input = await ElMessageBox.prompt('请输入SQL脚本名', 'SQL名', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern:
/\w+/,
inputErrorMessage: '请输入SQL脚本名',
});
sqlName = input.value;
state.sqlName = sqlName;
} catch (e) {
return;
}
}
await dbApi.saveSql.request({ id: state.ti.dbId, db: state.ti.db, sql: sql, type: 1, name: sqlName });
ElMessage.success('保存成功');
// 保存sql脚本成功事件
emits('saveSqlSuccess', state.ti.dbId, state.ti.db);
};
const deleteSql = async () => {
const sqlName = state.sqlName;
notBlank(sqlName, "该sql内容未保存");
const { dbId, db } = state.ti;
try {
await ElMessageBox.confirm(`确定删除【${sqlName}】该SQL内容?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbSql.request({ id: dbId, db: db, name: sqlName });
ElMessage.success('删除成功');
emits('deleteSqlSuccess', dbId, db);
} catch (err) { }
};
/**
* 格式化sql
*/
const formatSql = () => {
let selection = monacoEditor.getSelection()
if (!selection) {
return;
}
let sql = monacoEditor.getModel()?.getValueInRange(selection)
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
if (sql) {
replaceSelection(sqlFormatter(sql), selection)
return;
}
monacoEditor.getModel()?.setValue(sqlFormatter(monacoEditor.getValue()));
};
/**
* 提交事务,用于没有开启自动提交事务
*/
const onCommit = () => {
state.ti.getNowDbInst().runSql(state.ti.db, 'COMMIT;');
ElMessage.success('COMMIT success');
};
/**
* 替换选中的内容
*/
const replaceSelection = (str: string, selection: any) => {
const model = monacoEditor.getModel();
if (!model) {
return;
}
if (!selection) {
model.setValue(str);
return;
}
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection
const textBeforeSelection = model.getValueInRange({
startLineNumber: 1,
startColumn: 0,
endLineNumber: startLineNumber,
endColumn: startColumn,
})
const textAfterSelection = model.getValueInRange({
startLineNumber: endLineNumber,
startColumn: endColumn,
endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount()),
})
monacoEditor.setValue(textBeforeSelection + str + textAfterSelection)
monacoEditor.focus()
monacoEditor.setPosition({
lineNumber: startLineNumber,
column: 0,
})
}
/**
* 导出当前页数据
*/
const exportData = () => {
const dataList = state.execRes.data as any;
isTrue(dataList.length > 0, '没有数据可导出');
exportCsv(`数据查询导出-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, state.execRes.tableColumn, dataList)
};
const beforeUpload = (file: File) => {
ElMessage.success(`'${file.name}' 正在上传执行, 请关注结果通知`);
};
// 执行sql成功
const execSqlFileSuccess = (res: any) => {
if (res.code !== 200) {
ElMessage.error(res.msg);
}
};
// 获取sql文件上传执行url
const getUploadSqlFileUrl = () => {
return `${config.baseApiUrl}/dbs/${state.ti.dbId}/exec-sql-file?db=${state.ti.db}`;
};
const onDataSelectionChange = (datas: []) => {
state.selectionDatas = datas;
};
const changeUpdatedField = (updatedFields: []) => {
// 如果存在要更新字段,则显示提交和取消按钮
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
}
/**
* 执行删除数据事件
*/
const onDeleteData = async () => {
const deleteDatas = state.selectionDatas;
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
const { db } = state.ti;
const dbInst = state.ti.getNowDbInst()
const primaryKey = await dbInst.loadTableColumn(db, state.table);
const primaryKeyColumnName = primaryKey.columnName;
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
state.execRes.data = state.execRes.data.filter(
(d: any) => !(deleteDatas.findIndex((x: any) => x[primaryKeyColumnName] == d[primaryKeyColumnName]) != -1)
);
state.selectionDatas = [];
});
};
const submitUpdateFields = () => {
dbTableRef.value.submitUpdateFields();
}
const cancelUpdateFields = () => {
dbTableRef.value.cancelUpdateFields();
}
</script>
<style lang="scss">
.sql-file-exec {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
vertical-align: middle;
position: relative;
text-decoration: none;
}
.sqlEditor {
font-size: 8pt;
font-weight: 600;
border: 1px solid #ccc;
}
.update_field_active {
background-color: var(--el-color-success)
}
</style>

View File

@@ -0,0 +1,348 @@
<template>
<div>
<el-row>
<el-col :span="8">
<el-link @click="onRefresh()" icon="refresh" :underline="false" class="ml5">
</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link @click="addRow()" type="primary" icon="plus" :underline="false"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link @click="onDeleteData()" type="danger" icon="delete" :underline="false"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="commit" placement="top">
<el-link @click="onCommit()" type="success" icon="CircleCheck" :underline="false">
</el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="生成insert sql" placement="top">
<el-link @click="onGenerateInsertSql()" type="success" :underline="false">gi</el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip class="box-item" effect="dark" content="导出当前页的csv文件" placement="top">
<el-link type="success" :underline="false" @click="exportData"><span class="f12">导出</span></el-link>
</el-tooltip>
<el-divider direction="vertical" border-style="dashed" />
<el-tooltip v-if="hasUpdatedFileds" class="box-item" effect="dark" content="提交修改" placement="top">
<el-link @click="submitUpdateFields()" type="success" :underline="false" class="f12">提交</el-link>
</el-tooltip>
<el-divider v-if="hasUpdatedFileds" direction="vertical" border-style="dashed" />
<el-tooltip v-if="hasUpdatedFileds" class="box-item" effect="dark" content="取消修改" placement="top">
<el-link @click="cancelUpdateFields" type="warning" :underline="false" class="f12">取消</el-link>
</el-tooltip>
</el-col>
<el-col :span="16">
<el-input v-model="condition" placeholder="若需条件过滤,可选择列并点击对应的字段并输入需要过滤的内容点击查询按钮即可" clearable size="small"
style="width: 100%">
<template #prepend>
<el-popover trigger="click" :width="320" placement="right">
<template #reference>
<el-link type="success" :underline="false">选择列</el-link>
</template>
<el-table :data="columns" max-height="500" size="small" @row-click="
(...event: any) => {
onConditionRowClick(event);
}
" style="cursor: pointer">
<el-table-column property="columnName" label="列名" show-overflow-tooltip>
</el-table-column>
<el-table-column property="columnComment" label="备注" show-overflow-tooltip>
</el-table-column>
</el-table>
</el-popover>
</template>
<template #append>
<el-button @click="onSelectByCondition()" icon="search" size="small"></el-button>
</template>
</el-input>
</el-col>
</el-row>
<db-table ref="dbTableRef" :db-id="state.ti.dbId" :db="state.ti.db" :data="datas"
:table="state.table" :column-names="columnNames" :loading="loading" :height="tableHeight"
:show-column-tip="true" :sortable="'custom'" @sort-change="(sort: any) => onTableSortChange(sort)"
@selection-change="onDataSelectionChange" @change-updated-field="changeUpdatedField"></db-table>
<el-row type="flex" class="mt5" justify="center">
<el-pagination small :total="count" @current-change="pageChange()" layout="prev, pager, next, total, jumper"
v-model:current-page="pageNum" :page-size="DbInst.DefaultLimit"></el-pagination>
</el-row>
<div style=" font-size: 12px; padding: 0 10px; color: #606266"><span>{{ state.sql }}</span>
</div>
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
<el-row>
<el-col :span="5">
<el-select v-model="conditionDialog.condition">
<el-option label="=" value="="> </el-option>
<el-option label="LIKE" value="LIKE"> </el-option>
<el-option label=">" value=">"> </el-option>
<el-option label=">=" value=">="> </el-option>
<el-option label="<" value="<"> </el-option>
<el-option label="<=" value="<="> </el-option>
</el-select>
</el-col>
<el-col :span="19">
<el-input v-model="conditionDialog.value" :placeholder="conditionDialog.placeholder" />
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancelCondition">取消</el-button>
<el-button type="primary" @click="onConfirmCondition">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { onMounted, watch, reactive, toRefs, ref, Ref } from 'vue';
import { isTrue, notEmpty, notBlank } from '@/common/assert';
import { ElMessage } from 'element-plus';
import { DbInst, TabInfo } from '../../db';
import { exportCsv } from '@/common/utils/export';
import { dateStrFormat } from '@/common/utils/date';
import DbTable from '../DbTable.vue'
const emits = defineEmits(['genInsertSql'])
const props = defineProps({
data: {
type: TabInfo,
required: true
},
tableHeight: {
type: String,
default: '600'
}
})
const dbTableRef = ref(null) as Ref;
const state = reactive({
ti: {} as TabInfo,
table: '', // 当前的表名
datas: [],
sql: '', // 当前数据tab执行的sql
orderBy: '',
condition: '', // 当前条件框的条件
loading: false, // 是否在加载数据
columnNames: [],
columns: [],
pageNum: 1,
count: 0,
selectionDatas: [] as any,
conditionDialog: {
title: '',
placeholder: '',
columnRow: null,
dataTab: null,
visible: false,
condition: '=',
value: null
},
tableHeight: '600',
hasUpdatedFileds: false,
});
const {
datas,
condition,
loading,
columns,
columnNames,
pageNum,
count,
hasUpdatedFileds,
conditionDialog,
} = toRefs(state);
watch(() => props.tableHeight, (newValue: any) => {
state.tableHeight = newValue;
});
onMounted(async () => {
console.log('in table data mounted');
state.ti = props.data;
state.tableHeight = props.tableHeight;
state.table = state.ti.params.table;
notBlank(state.table, "TableData组件params.table信息不能为空")
const columns = await state.ti.getNowDbInst().loadColumns(state.ti.db, state.table);
state.columns = columns;
state.columnNames = columns.map((t: any) => t.columnName);
await onRefresh();
})
const onRefresh = async () => {
// 查询条件置空
state.condition = '';
state.pageNum = 1;
await selectData();
}
/**
* 数据tab修改页数
*/
const pageChange = async () => {
await selectData();
};
/**
* 单表数据信息查询数据
*/
const selectData = async () => {
state.loading = true;
const dbInst = state.ti.getNowDbInst();
const { db } = state.ti;
try {
const countRes = await dbInst.runSql(db, DbInst.getDefaultCountSql(state.table));
state.count = countRes.res[0].count;
let sql = dbInst.getDefaultSelectSql(state.table, state.condition, state.orderBy, state.pageNum);
state.sql = sql;
if (state.count > 0) {
const colAndData: any = await dbInst.runSql(db, sql);
state.datas = colAndData.res;
} else {
state.datas = [];
}
} finally {
state.loading = false;
}
}
/**
* 导出当前页数据
*/
const exportData = () => {
const dataList = state.datas as any;
isTrue(dataList.length > 0, '没有数据可导出');
exportCsv(`数据导出-${state.table}-${dateStrFormat('yyyyMMddHHmm', new Date().toString())}`, state.columnNames, dataList)
};
/**
* 条件查询,点击列信息后显示输入对应的值
*/
const onConditionRowClick = (event: any) => {
const row = event[0];
state.conditionDialog.title = `请输入 [${row.columnName}] 的值`;
state.conditionDialog.placeholder = `${row.columnType} ${row.columnComment}`;
state.conditionDialog.columnRow = row;
state.conditionDialog.visible = true;
};
// 确认条件
const onConfirmCondition = () => {
const conditionDialog = state.conditionDialog;
let condition = state.condition;
if (condition) {
condition += ` AND `;
}
const row = conditionDialog.columnRow as any;
condition += `${row.columnName} ${conditionDialog.condition} `;
state.condition = condition + DbInst.wrapColumnValue(row.columnType, conditionDialog.value);
onCancelCondition();
};
const onCancelCondition = () => {
state.conditionDialog.visible = false;
state.conditionDialog.title = ``;
state.conditionDialog.placeholder = ``;
state.conditionDialog.value = null;
state.conditionDialog.columnRow = null;
state.conditionDialog.dataTab = null;
};
/**
* 提交事务,用于没有开启自动提交事务
*/
const onCommit = () => {
state.ti.getNowDbInst().runSql(state.ti.db, 'COMMIT;');
ElMessage.success('COMMIT success');
};
const onSelectByCondition = async () => {
notEmpty(state.condition, '条件不能为空');
state.pageNum = 1;
await selectData();
}
/**
* 表排序字段变更
*/
const onTableSortChange = async (sort: any) => {
if (!sort.prop) {
return;
}
const sortType = sort.order == 'descending' ? 'DESC' : 'ASC';
state.orderBy = `ORDER BY ${sort.prop} ${sortType}`;
await onRefresh();
};
const onDataSelectionChange = (datas: []) => {
state.selectionDatas = datas;
};
const changeUpdatedField = (updatedFields: []) => {
// 如果存在要更新字段,则显示提交和取消按钮
state.hasUpdatedFileds = updatedFields && updatedFields.length > 0;
}
/**
* 执行删除数据事件
*/
const onDeleteData = async () => {
const deleteDatas = state.selectionDatas;
isTrue(deleteDatas && deleteDatas.length > 0, '请先选择要删除的数据');
const { db } = state.ti;
const dbInst = state.ti.getNowDbInst()
dbInst.promptExeSql(db, dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas), null, () => {
onRefresh();
});
};
const onGenerateInsertSql = async () => {
isTrue(state.selectionDatas && state.selectionDatas.length > 0, '请先选择数据');
emits('genInsertSql', state.ti.getNowDbInst().genInsertSql(state.ti.db, state.table, state.selectionDatas));
};
const submitUpdateFields = () => {
dbTableRef.value.submitUpdateFields();
}
const cancelUpdateFields = () => {
dbTableRef.value.cancelUpdateFields();
}
// 添加新数据行
const addRow = async () => {
const columns = state.ti.getNowDb().getColumns(state.table);
// key: 字段名value: 字段名提示
let obj: any = {};
columns.forEach((item: any) => {
obj[`${item.columnName}`] = `'${item.columnComment || ''} ${item.columnName}[${item.columnType}]${item.nullable == 'YES' ? '' : '[not null]'}'`;
});
let columnNames = Object.keys(obj).join(',');
let values = Object.values(obj).join(',');
let sql = `INSERT INTO ${state.table} (${columnNames}) VALUES (${values});`;
state.ti.getNowDbInst().promptExeSql(state.ti.db, sql, null, () => {
onRefresh();
});
};
</script>
<style lang="scss">
.update_field_active {
background-color: var(--el-color-success)
}
</style>

View File

@@ -0,0 +1,473 @@
/* eslint-disable no-unused-vars */
import { dbApi } from './api';
import SqlExecBox from './component/SqlExecBox';
const dbInstCache: Map<number, DbInst> = new Map();
export class DbInst {
/**
* 标签路径
*/
tagPath: string
/**
* 实例id
*/
id: number
/**
* 实例名
*/
name: string
/**
* 数据库类型, mysql postgres
*/
type: string
/**
* schema -> db
*/
dbs: Map<string, Db> = new Map()
/**
* 默认查询分页数量
*/
static DefaultLimit = 20;
/**
* 获取指定数据库实例,若不存在则新建并缓存
* @param dbName 数据库名
* @returns db实例
*/
getDb(dbName: string) {
if (!dbName) {
throw new Error('dbName不能为空')
}
let db = this.dbs.get(dbName)
if (db) {
return db;
}
console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`);
db = new Db();
db.name = dbName;
this.dbs.set(dbName, db);
return db;
}
/**
* 加载数据库表信息
* @param dbName 数据库名
* @returns 表信息
*/
async loadTables(dbName: string) {
const db = this.getDb(dbName);
// 优先从 table map中获取
let tables = db.tables;
if (tables) {
return tables;
}
console.log(`load tables -> dbName: ${dbName}`);
tables = await dbApi.tableMetadata.request({ id: this.id, db: dbName });
db.tables = tables;
return tables;
}
/**
* 获取表的所有列信息
* @param table 表名
*/
async loadColumns(dbName: string, table: string) {
const db = this.getDb(dbName);
// 优先从 table map中获取
let columns = db.getColumns(table);
if (columns) {
return columns;
}
console.log(`load columns -> dbName: ${dbName}, table: ${table}`);
columns = await dbApi.columnMetadata.request({
id: this.id,
db: dbName,
tableName: table,
});
db.columnsMap.set(table, columns);
return columns;
}
/**
* 获取指定表的指定信息
* @param table 表名
*/
async loadTableColumn(dbName: string, table: string, columnName?: string) {
// 确保该表的列信息都已加载
await this.loadColumns(dbName, table);
return this.getDb(dbName).getColumn(table, columnName);
}
/**
* 获取库信息提示
*/
async loadDbHints(dbName: string) {
const db = this.getDb(dbName);
if (db.tableHints) {
return db.tableHints;
}
console.log(`load db-hits -> dbName: ${dbName}`);
const hits = await dbApi.hintTables.request({ id: this.id, db: db.name, })
db.tableHints = hits;
return hits;
}
/**
* 执行sql
*
* @param sql sql
* @param remark 执行备注
*/
async runSql(dbName: string, sql: string, remark: string = '') {
return await dbApi.sqlExec.request({
id: this.id,
db: dbName,
sql: sql.trim(),
remark,
});
}
// 获取指定表的默认查询sql
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
const baseSql = `SELECT * FROM ${table} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''}`;
if (this.type == 'mysql') {
return `${baseSql} LIMIT ${(pageNum - 1) * limit}, ${limit};`;
}
if (this.type == 'postgres') {
return `${baseSql} OFFSET ${(pageNum - 1) * limit} LIMIT ${limit};`;
}
return baseSql;
}
/**
* 生成指定数据的insert语句
* @param dbName 数据库名
* @param table 表名
* @param datas 要生成的数据
*/
genInsertSql(dbName: string, table: string, datas: any[]): string {
if (!datas) {
return '';
}
const columns = this.getDb(dbName).getColumns(table);
const sqls = [];
for (let data of datas) {
let colNames = [];
let values = [];
for (let column of columns) {
const colName = column.columnName;
colNames.push(colName);
values.push(DbInst.wrapValueByType(data[colName]));
}
sqls.push(`INSERT INTO ${table} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
}
return sqls.join(';\n') + ';'
}
/**
* 生成根据主键删除的sql语句
* @param table 表名
* @param datas 要删除的记录
*/
genDeleteByPrimaryKeysSql(db: string, table: string, datas: any[]) {
const primaryKey = this.getDb(db).getColumn(table);
const primaryKeyColumnName = primaryKey.columnName;
const ids = datas.map((d: any) => `${DbInst.wrapColumnValue(primaryKey.columnType, d[primaryKeyColumnName])}`).join(',');
return `DELETE FROM ${table} WHERE ${primaryKeyColumnName} IN (${ids})`;
}
/*
* 弹框提示是否执行sql
*/
promptExeSql = (db: string, sql: string, cancelFunc: any = null, successFunc: any = null) => {
SqlExecBox({
sql, dbId: this.id, db,
runSuccessCallback: successFunc,
cancelCallback: cancelFunc,
});
};
/**
* 获取或新建dbInst如果缓存中不存在则新建否则直接返回
* @param inst 数据库实例,后端返回的列表接口中的信息
* @returns DbInst
*/
static getOrNewInst(inst: any) {
if (!inst) {
throw new Error('inst不能为空')
}
let dbInst = dbInstCache.get(inst.id);
if (dbInst) {
return dbInst;
}
console.info(`new dbInst: ${inst.id}, tagPath: ${inst.tagPath}`);
dbInst = new DbInst();
dbInst.tagPath = inst.tagPath;
dbInst.id = inst.id;
dbInst.name = inst.name;
dbInst.type = inst.type;
dbInstCache.set(dbInst.id, dbInst);
return dbInst;
}
/**
* 获取数据库实例id若不存在则新建一个并缓存
* @param dbId 数据库实例id
* @param dbType 第一次获取时为必传项,即第一次创建时
* @returns 数据库实例
*/
static getInst(dbId?: number): DbInst {
if (!dbId) {
throw new Error('dbId不能为空');
}
let dbInst = dbInstCache.get(dbId);
if (dbInst) {
return dbInst;
}
throw new Error('dbInst不存在! 请在合适调用点使用DbInst.newInst()新建该实例');
}
/**
* 清空所有实例缓存信息
*/
static clearAll() {
dbInstCache.clear();
}
/**
* 获取count sql
* @param table 表名
* @param condition 条件
* @returns count sql
*/
static getDefaultCountSql = (table: string, condition?: string) => {
return `SELECT COUNT(*) count FROM ${table} ${condition ? 'WHERE ' + condition : ''}`;
};
/**
* 根据返回值包装值,若值为字符串类型则添加''
* @param val 值
* @returns 包装后的值
*/
static wrapValueByType = (val: any) => {
if (val == null) {
return 'NULL';
}
if (typeof val == 'number') {
return val;
}
return `'${val}'`;
};
/**
* 根据字段类型包装字段值,如为字符串等则添加‘’,数字类型则直接返回即可
*/
static wrapColumnValue(columnType: string, value: any) {
if (columnType.match(/int|double|float|nubmer|decimal|byte|bit/gi)) {
return value;
}
return `'${value}'`;
};
/**
*
* @param str 字符串
* @param tableData 表数据
* @param flag 标志
* @returns 列宽度
*/
static flexColumnWidth = (str: any, tableData: any, flag = 'equal') => {
// str为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
// flag为可选值可不传该参数,传参时可选'max'或'equal',默认为'max'
// flag为'max'则设置列宽适配该列中最长的内容,flag为'equal'则设置列宽适配该列中第一行内容的长度。
str = str + '';
let columnContent = '';
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
return;
}
if (!str || !str.length || str.length === 0 || str === undefined) {
return;
}
if (flag === 'equal') {
// 获取该列中第一个不为空的数据(内容)
for (let i = 0; i < tableData.length; i++) {
// 转为字符串后比较
if ((tableData[i][str] + '').length > 0) {
columnContent = tableData[i][str] + '';
break;
}
}
} else {
// 获取该列中最长的数据(内容)
let index = 0;
for (let i = 0; i < tableData.length; i++) {
if (tableData[i][str] === null) {
return;
}
const now_temp = tableData[i][str] + '';
const max_temp = tableData[index][str] + '';
if (now_temp.length > max_temp.length) {
index = i;
}
}
columnContent = tableData[index][str] + '';
}
const contentWidth: number = DbInst.getContentWidth(columnContent);
// 获取列名称的长度 加上排序图标长度
const columnWidth: number = DbInst.getContentWidth(str) + 43;
const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
return flexWidth + 'px';
};
/**
* 获取内容所需要占用的宽度
*/
static getContentWidth = (content: any): number => {
// 以下分配的单位长度可根据实际需求进行调整
let flexWidth = 0;
for (const char of content) {
if (flexWidth > 500) {
break;
}
if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
// 如果是小写字母、数字字符分配8个单位宽度
flexWidth += 8.5;
continue;
}
if (char >= 'A' && char <= 'Z') {
flexWidth += 9;
continue;
}
if (char >= '\u4e00' && char <= '\u9fa5') {
// 如果是中文字符为字符分配16个单位宽度
flexWidth += 16;
} else {
// 其他种类字符为字符分配9个单位宽度
flexWidth += 8;
}
}
if (flexWidth > 500) {
// 设置最大宽度
flexWidth = 500;
}
return flexWidth;
};
}
/**
* 数据库实例信息
*/
class Db {
name: string // 库名
tables: [] // 数据库实例表信息
columnsMap: Map<string, any> = new Map // table -> columns
tableHints: any = null // 提示词
/**
* 获取指定表列信息前提需要dbInst.loadColumns
* @param table 表名
*/
getColumns(table: string) {
return this.columnsMap.get(table);
}
/**
* 获取指定表中的指定列名信息,若列名为空则默认返回主键
* @param table 表名
* @param columnName 列名
*/
getColumn(table: string, columnName: string = '') {
const cols = this.getColumns(table);
if (!columnName) {
const col = cols.find((c: any) => c.columnKey == 'PRI');
return col || cols[0];
}
return cols.find((c: any) => c.columnName == columnName);
}
}
export enum TabType {
/**
* 表数据
*/
TableData,
/**
* 查询框
*/
Query,
}
export class TabInfo {
/**
* tab唯一key。与label、name都一致
*/
key: string
/**
* 菜单树节点key
*/
treeNodeKey: string
/**
* 数据库实例id
*/
dbId: number
/**
* 库名
*/
db: string = ''
/**
* tab 类型
*/
type: TabType
/**
* tab需要的其他信息
*/
params: any
getNowDbInst() {
return DbInst.getInst(this.dbId);
}
getNowDb() {
return this.getNowDbInst().getDb(this.db);
}
}
/** 修改表字段所需数据 */
export type UpdateFieldsMeta = {
// 主键值
primaryKey: string
// 主键名
primaryKeyName: string
// 主键类型
primaryKeyType: string
// 新值
fields: FieldsMeta[]
}
export type FieldsMeta = {
// 字段所在div
div: HTMLElement
// 字段名
fieldName: string
// 字段所在的表格行数据
row: any
// 字段类型
fieldType: string
// 原值
oldValue: string
// 新值
newValue: string
}

View File

@@ -7,5 +7,6 @@ export default {
// 数据库sql执行类型
DbSqlExecTypeEnum: new Enum().add('UPDATE', 'UPDATE', 1)
.add('DELETE', 'DELETE', 2)
.add('INSERT', 'INSERT', 3),
.add('INSERT', 'INSERT', 3)
.add('QUERY', 'QUERY', 4),
}

View File

@@ -446,7 +446,6 @@ const showCreateFileDialog = (node: any) => {
const createFile = async () => {
const node = state.createFileDialog.node;
console.log(node.data);
const name = state.createFileDialog.name;
const type = state.createFileDialog.type;
const path = node.data.path + '/' + name;

View File

@@ -1,76 +1,73 @@
<template>
<div>
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true"
:before-close="cancel" width="38%">
:before-close="cancel" width="650px">
<el-form :model="form" ref="machineForm" :rules="rules" label-width="85px">
<el-form-item prop="tagId" label="标签:" required>
<tag-select v-model:tag-id="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="name" label="名称:" required>
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="ip" label="ip:" required>
<el-col :span="18">
<el-input :disabled="form.id" v-model.trim="form.ip" placeholder="主机ip" auto-complete="off">
</el-input>
</el-col>
<el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5">
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
</el-col>
</el-form-item>
<el-form-item prop="username" label="用户名:" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="authMethod" label="认证方式:" required>
<el-select style="width: 100%" v-model="form.authMethod" placeholder="请选择认证方式">
<el-option key="1" label="Password" :value="1"> </el-option>
<el-option key="2" label="PublicKey" :value="2"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="form.authMethod == 1" prop="password" label="密码:">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码,修改操作可不填"
autocomplete="new-password">
<template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click"
:content="pwd">
<template #reference>
<el-link @click="getPwd" :underline="false" type="primary" class="mr5">原密码</el-link>
</template>
</el-popover>
</template>
</el-input>
</el-form-item>
<el-form-item v-if="form.authMethod == 2" prop="password" label="秘钥:">
<el-input type="textarea" :rows="3" v-model="form.password" placeholder="请将私钥文件内容拷贝至此,修改操作可不填">
</el-input>
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-input type="textarea" v-model="form.remark"></el-input>
</el-form-item>
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签:" required :rules="{
required: true,
message: '请选择标签',
trigger: ['change', 'blur'],
}">
<tag-select v-model:tag-id="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="name" label="名称:" required>
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="ip" label="ip:" required>
<el-col :span="18">
<el-input :disabled="form.id" v-model.trim="form.ip" placeholder="主机ip" auto-complete="off">
</el-input>
</el-col>
<el-col style="text-align: center" :span="1">:</el-col>
<el-col :span="5">
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
</el-col>
</el-form-item>
<el-form-item prop="enableRecorder" label="终端回放:">
<el-checkbox v-model="form.enableRecorder" :true-label="1" :false-label="-1"></el-checkbox>
</el-form-item>
<el-form-item prop="username" label="用户名:">
<el-input v-model.trim="form.username" placeholder="请输授权用户名" autocomplete="new-password">
</el-input>
</el-form-item>
<el-form-item prop="enableSshTunnel" label="SSH隧道:">
<el-col :span="3">
<el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1"
:false-label="-1"></el-checkbox>
</el-col>
<el-col :span="2" v-if="form.enableSshTunnel == 1"> 机器: </el-col>
<el-col :span="19" v-if="form.enableSshTunnel == 1">
<el-select style="width: 100%" v-model="form.sshTunnelMachineId" placeholder="请选择SSH隧道机器">
<el-option v-for="item in sshTunnelMachineList" :key="item.id"
:label="`${item.ip}:${item.port} [${item.name}]`" :value="item.id">
</el-option>
</el-select>
</el-col>
</el-form-item>
<el-form-item label="认证方式:">
<el-select @change="changeAuthMethod" style="width: 100%" v-model="state.authType"
placeholder="请选认证方式">
<el-option key="1" label="密码" :value="1"> </el-option>
<el-option key="2" label="授权凭证" :value="2"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="state.authType == 1" prop="password" label="密码:">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码"
autocomplete="new-password">
</el-input>
</el-form-item>
<el-form-item v-if="state.authType == 2" prop="authCertId" label="授权凭证:" required>
<auth-cert-select ref="authCertSelectRef" v-model="form.authCertId" />
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-input type="textarea" v-model="form.remark"></el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="enableRecorder" label="终端回放:">
<el-checkbox v-model="form.enableRecorder" :true-label="1" :false-label="-1"></el-checkbox>
</el-form-item>
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<div>
<el-button @click="testConn" :loading="testConnBtnLoading" type="success">测试连接</el-button>
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="btnLoading" @click="btnOk"> </el-button>
</div>
@@ -83,17 +80,14 @@
import { toRefs, reactive, watch, ref } from 'vue';
import { machineApi } from './api';
import { ElMessage } from 'element-plus';
import { notBlank } from '@/common/assert';
import { RsaEncrypt } from '@/common/rsa';
import TagSelect from '../component/TagSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import AuthCertSelect from './authcert/AuthCertSelect.vue'
const props = defineProps({
visible: {
type: Boolean,
},
projects: {
type: Array,
},
machine: {
type: [Boolean, Object],
},
@@ -106,13 +100,6 @@ const props = defineProps({
const emit = defineEmits(['update:visible', 'cancel', 'val-change'])
const rules = {
tagId: [
{
required: true,
message: '请选择标签',
trigger: ['change', 'blur'],
},
],
name: [
{
required: true,
@@ -127,50 +114,62 @@ const rules = {
trigger: ['change', 'blur'],
},
],
username: [
authCertId: [
{
required: true,
message: '请输入用户名',
message: '请选择授权凭证',
trigger: ['change', 'blur'],
},
],
authMethod: [
username: [
{
required: true,
message: '请选择认证方式',
message: '请输入授权用户名',
trigger: ['change', 'blur'],
},
],
password: [
{
required: true,
message: '请输入授权密码',
trigger: ['change', 'blur'],
},
],
}
const machineForm: any = ref(null);
const authCertSelectRef: any = ref(null);
const state = reactive({
dialogVisible: false,
tabActiveName: 'basic',
sshTunnelMachineList: [] as any,
authCerts: [] as any,
authType: 1,
form: {
id: null,
tagId: null as any,
tagPath: '',
ip: null,
name: null,
authMethod: 1,
port: 22,
name: null,
authCertId: null as any,
username: '',
password: '',
tagId: null as any,
tagPath: null as any,
remark: '',
enableSshTunnel: null,
sshTunnelMachineId: null,
sshTunnelMachineId: null as any,
enableRecorder: -1,
},
pwd: '',
testConnBtnLoading: false,
btnLoading: false,
});
const {
dialogVisible,
sshTunnelMachineList,
tabActiveName,
form,
pwd,
testConnBtnLoading,
btnLoading,
} = toRefs(state)
@@ -179,53 +178,65 @@ watch(props, async (newValue: any) => {
if (!state.dialogVisible) {
return;
}
state.tabActiveName = 'basic';
if (newValue.machine) {
state.form = { ...newValue.machine };
// 如果凭证类型为公共的,则表示使用授权凭证认证
const authCertId = (state.form as any).authCertId
if (authCertId > 0) {
state.authType = 2;
} else {
state.authType = 1;
}
} else {
state.form = { port: 22, authMethod: 1 } as any;
state.form = { port: 22 } as any;
state.authType = 1;
}
getSshTunnelMachines();
});
const getSshTunnelMachines = async () => {
if (state.form.enableSshTunnel == 1 && state.sshTunnelMachineList.length == 0) {
const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
state.sshTunnelMachineList = res.list;
const changeAuthMethod = (val: any) => {
if (state.form.id) {
if (val == 2) {
state.form.authCertId = null;
} else {
state.form.password = '';
}
}
};
}
const getSshTunnelMachine = (machineId: any) => {
notBlank(machineId, '请选择或先创建一台隧道机器');
return state.sshTunnelMachineList.find((x: any) => x.id == machineId);
};
const getPwd = async () => {
state.pwd = await machineApi.getMachinePwd.request({ id: state.form.id });
};
const btnOk = async () => {
if (!state.form.id) {
notBlank(state.form.password, '新增操作,密码不可为空');
}
const testConn = async () => {
machineForm.value.validate(async (valid: boolean) => {
if (valid) {
const form: any = state.form;
if (form.enableSshTunnel == 1) {
const tunnelMachine: any = getSshTunnelMachine(form.sshTunnelMachineId);
if (tunnelMachine.ip == form.ip && tunnelMachine.port == form.port) {
ElMessage.error('隧道机器不能与本机器一致');
return;
}
const form = getReqForm();
if (!form) {
return;
}
const reqForm: any = { ...form };
if (reqForm.authMethod == 1) {
reqForm.password = await RsaEncrypt(state.form.password);
state.testConnBtnLoading = true;
try {
await machineApi.testConn.request(form);
ElMessage.success('连接成功');
} finally {
state.testConnBtnLoading = false;
}
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
}
const btnOk = async () => {
machineForm.value.validate(async (valid: boolean) => {
if (valid) {
const form = getReqForm();
if (!form) {
return;
}
state.btnLoading = true;
try {
await machineApi.saveMachine.request(reqForm);
await machineApi.saveMachine.request(form);
ElMessage.success('保存成功');
emit('val-change', state.form);
emit('val-change', form);
cancel();
} finally {
state.btnLoading = false;
@@ -237,11 +248,22 @@ const btnOk = async () => {
});
};
const getReqForm = () => {
const reqForm: any = { ...state.form };
debugger
// 如果为密码认证则置空授权凭证id
if (state.authType == 1) {
reqForm.authCertId = -1;
}
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
reqForm.sshTunnelMachineId = -1
}
return reqForm
}
const cancel = () => {
emit('update:visible', false);
emit('cancel');
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@@ -15,8 +15,8 @@
</el-select>
<el-input class="ml5" placeholder="请输入名称" style="width: 150px" v-model="params.name" @clear="search"
plain clearable></el-input>
<el-input class="ml5" placeholder="请输入ip" style="width: 150px" v-model="params.ip" @clear="search"
plain clearable></el-input>
<el-input class="ml5" placeholder="请输入ip" style="width: 150px" v-model="params.ip" @clear="search" plain
clearable></el-input>
<el-button class="ml5" @click="search" type="success" icon="search"></el-button>
</div>
</div>
@@ -29,16 +29,28 @@
</el-radio>
</template>
</el-table-column>
<el-table-column prop="tagPath" label="标签路径" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="tagPath" label="标签路径" min-width="150" show-overflow-tooltip>
<template #default="scope">
<tag-info :tag-path="scope.row.tagPath" />
<span class="ml5">
{{ scope.row.tagPath }}
</span>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip></el-table-column>
<el-table-column prop="ip" label="ip:port" min-width="150">
<template #default="scope">
<el-link :disabled="scope.row.status == -1" @click="showMachineStats(scope.row)" type="primary"
:underline="false">{{
`${scope.row.ip}:${scope.row.port}`
}}</el-link>
:underline="false">
{{ `${scope.row.ip}:${scope.row.port}` }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" min-width="100">
</el-table-column>
<el-table-column prop="status" label="状态" min-width="80">
<template #default="scope">
<el-switch v-auth:disabled="'machine:update'" :width="52" v-model="scope.row.status"
@@ -47,7 +59,7 @@
@change="changeStatus(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" min-width="90"></el-table-column>
<el-table-column prop="remark" label="备注" min-width="250" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" min-width="235" fixed="right">
@@ -59,13 +71,13 @@
</span>
<span v-auth="'machine:file'">
<el-link type="success" :disabled="scope.row.status == -1"
@click="showFileManage(scope.row)" plain size="small" :underline="false">文件</el-link>
<el-link type="success" :disabled="scope.row.status == -1" @click="showFileManage(scope.row)"
plain size="small" :underline="false">文件</el-link>
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-link :disabled="scope.row.status == -1" type="warning" @click="serviceManager(scope.row)"
plain size="small" :underline="false">脚本</el-link>
<el-link :disabled="scope.row.status == -1" type="warning" @click="serviceManager(scope.row)" plain
size="small" :underline="false">脚本</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-dropdown>
@@ -83,8 +95,8 @@
</el-dropdown-item>
<el-dropdown-item>
<el-link @click="showProcess(scope.row)" :disabled="scope.row.status == -1"
plain :underline="false" size="small">进程</el-link>
<el-link @click="showProcess(scope.row)" :disabled="scope.row.status == -1" plain
:underline="false" size="small">进程</el-link>
</el-dropdown-item>
<el-dropdown-item v-if="scope.row.enableRecorder == 1">
@@ -121,13 +133,13 @@
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
<el-descriptions-item :span="1" label="认证方式">{{ infoDialog.data.authMethod == 1 ? 'Password' :
'PublicKey'
}}</el-descriptions-item>
<el-descriptions-item :span="1" label="认证方式">
{{ infoDialog.data.authCertId > 1 ? '授权凭证' : '密码' }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.enableSshTunnel == 1 ? '是' : '否' }}
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }}
</el-descriptions-item>
@@ -176,6 +188,7 @@ import ProcessList from './ProcessList.vue';
import MachineStats from './MachineStats.vue';
import MachineRec from './MachineRec.vue';
import { dateFormat } from '@/common/utils/date';
import TagInfo from '../component/TagInfo.vue';
const router = useRouter();
const state = reactive({

View File

@@ -26,19 +26,27 @@
<el-col :span="5">
<el-input v-model="param.model" placeholder="内容中用{{.model}}替换"></el-input>
</el-col>
<el-divider :span="1" direction="vertical" border-style="dashed" />
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.name" placeholder="字段名"></el-input>
</el-col>
<el-divider :span="1" direction="vertical" border-style="dashed" />
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.placeholder" placeholder="字段说明"></el-input>
</el-col>
<el-divider :span="1" direction="vertical" border-style="dashed" />
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.options" placeholder="可选值 ,分割"></el-input>
</el-col>
<el-divider :span="1" direction="vertical" border-style="dashed" />
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="2">
<el-button @click="onDeleteParam(index)" size="small" type="danger">删除</el-button>
</el-col>
@@ -172,6 +180,4 @@ const cancel = () => {
state.params = [];
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@@ -1,5 +1,5 @@
<template>
<div :style="{ height: height }" id="xterm" class="xterm" />
<div :style="{ height: props.height }" id="xterm" class="xterm" />
</template>
<script lang="ts" setup>
@@ -9,40 +9,31 @@ import { FitAddon } from 'xterm-addon-fit';
import { getSession } from '@/common/utils/storage.ts';
import config from '@/common/config';
import { useStore } from '@/store/index.ts';
import { nextTick, toRefs, watch, computed, reactive, onMounted, onBeforeUnmount } from 'vue';
import { nextTick, computed, reactive, onMounted, onBeforeUnmount } from 'vue';
const props = defineProps({
machineId: { type: Number },
cmd: { type: String },
height: { type: String },
height: { type: [String, Number] },
})
const state = reactive({
machineId: 0,
cmd: '',
height: '',
term: null as any,
socket: null as any,
});
const {
height,
} = toRefs(state)
const resize = 1;
const data = 2;
const ping = 3;
watch(props, (newValue: any) => {
state.machineId = newValue.machineId;
state.cmd = newValue.cmd;
state.height = newValue.height;
});
onMounted(() => {
state.machineId = props.machineId as any;
state.height = props.height as any;
state.cmd = props.cmd as any;
nextTick(() => {
initXterm();
initSocket();
});
});
onBeforeUnmount(() => {
@@ -56,11 +47,6 @@ const getThemeConfig: any = computed(() => {
return store.state.themeConfig.themeConfig;
});
nextTick(() => {
initXterm();
initSocket();
});
function initXterm() {
const term: any = new Terminal({
fontSize: getThemeConfig.value.terminalFontSize || 15,
@@ -122,7 +108,7 @@ function initXterm() {
let pingInterval: any;
function initSocket() {
state.socket = new WebSocket(
`${config.baseWsUrl}/machines/${state.machineId}/terminal?token=${getSession('token')}&cols=${state.term.cols}&rows=${state.term.rows
`${config.baseWsUrl}/machines/${props.machineId}/terminal?token=${getSession('token')}&cols=${state.term.cols}&rows=${state.term.rows
}`
);
@@ -189,4 +175,11 @@ function closeAll() {
state.term = null;
}
}
</script>
</script>
<style lang="scss">
#xterm {
.xterm-viewport {
overflow-y: hidden
}
}
</style>

View File

@@ -12,7 +12,7 @@ import { useRoute } from 'vue-router';
const route = useRoute();
const state = reactive({
machineId: 0,
height: 700,
height: 0,
});
const {

View File

@@ -10,6 +10,7 @@ export const machineApi = {
// 终止进程
killProcess: Api.create("/machines/{id}/process", 'delete'),
closeCli: Api.create("/machines/{id}/close-cli", 'delete'),
testConn: Api.create("/machines/test-conn", 'post'),
// 保存按钮
saveMachine: Api.create("/machines", 'post'),
// 调整状态
@@ -35,4 +36,11 @@ export const machineApi = {
delConf: Api.create("/machines/{machineId}/files/{id}", 'delete'),
terminal: Api.create("/api/machines/{id}/terminal", 'get'),
recDirNames: Api.create("/machines/rec/names", 'get')
}
export const authCertApi = {
baseList : Api.create("/sys/authcerts/base", 'get'),
list: Api.create("/sys/authcerts", 'get'),
save: Api.create("/sys/authcerts", 'post'),
delete: Api.create("/sys/authcerts/{id}", 'delete'),
}

View File

@@ -0,0 +1,126 @@
<template>
<div>
<el-dialog :title="title" v-model="dvisible" :show-close="false" :before-close="cancel" width="500px"
:destroy-on-close="true">
<el-form ref="acForm" :rules="rules" :model="form" label-width="90px">
<el-form-item prop="name" label="名称:" required>
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item prop="authMethod" label="认证方式:" required>
<el-select style="width: 100%" v-model="form.authMethod" placeholder="请选择认证方式">
<el-option key="1" label="密码" :value="1"> </el-option>
<el-option key="2" label="密钥" :value="2"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="form.authMethod == 1" prop="password" label="密码:">
<el-input type="password" show-password clearable v-model.trim="form.password" placeholder="请输入密码"
autocomplete="new-password">
</el-input>
</el-form-item>
<el-form-item v-if="form.authMethod == 2" prop="password" label="秘钥:">
<el-input type="textarea" :rows="5" v-model="form.password" placeholder="请将私钥文件内容拷贝至此">
</el-input>
</el-form-item>
<el-form-item v-if="form.authMethod == 2" prop="passphrase" label="秘钥密码:">
<el-input type="password" v-model="form.passphrase">
</el-input>
</el-form-item>
<el-form-item label="备注:">
<el-input v-model="form.remark" type="textarea" :rows="2"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="btnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, watch } from 'vue';
import { authCertApi } from '../api';
const props = defineProps({
visible: {
type: Boolean,
},
data: {
type: [Boolean, Object],
},
title: {
type: String,
},
})
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change'])
const acForm: any = ref(null);
const rules = {
name: [
{
required: true,
message: '授权凭证名称不能为空',
trigger: ['change', 'blur'],
},
]
}
const state = reactive({
dvisible: false,
params: [] as any,
form: {
id: null,
name: '',
authMethod: 1,
password: '',
passphrase: '',
remark: '',
},
btnLoading: false,
});
const {
dvisible,
form,
btnLoading,
} = toRefs(state)
watch(props, (newValue: any) => {
state.dvisible = newValue.visible;
if (newValue.data) {
state.form = { ...newValue.data };
} else {
state.form = { authMethod: 1 } as any;
state.params = [];
}
});
const cancel = () => {
// 更新父组件visible prop对应的值为false
emit('update:visible', false);
// 若父组件有取消事件,则调用
emit('cancel');
};
const btnOk = async () => {
acForm.value.validate(async (valid: boolean) => {
if (valid) {
state.btnLoading = true;
try {
await authCertApi.save.request(state.form);
emit('val-change', state.form);
cancel();
} finally {
state.btnLoading = false;
}
}
});
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,159 @@
<template>
<div class="role-list">
<el-card>
<div>
<el-button type="primary" icon="plus" @click="edit(false)">添加</el-button>
<el-button :disabled="chooseId == null" @click="edit(chooseData)" type="primary" icon="edit">编辑
</el-button>
<el-button :disabled="chooseId == null" @click="deleteAc(chooseData)" type="danger" icon="delete">删除
</el-button>
<div style="float: right">
<el-select v-model="query.type" placeholder="请选择标签" @clear="search" filterable clearable>
<el-option label="" value="item"> </el-option>
</el-select>
<el-input class="ml5" placeholder="请输入凭证名称" style="width: 150px" v-model="query.name" @clear="search"
plain clearable></el-input>
<el-button class="ml5" @click="search" type="success" icon="search"></el-button>
</div>
</div>
<el-table :data="authcerts" @current-change="choose" ref="table" style="width: 100%">
<el-table-column label="选择" width="55px">
<template #default="scope">
<el-radio v-model="chooseId" :label="scope.row.id">
<i></i>
</el-radio>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" min-width="60px" show-overflow-tooltip></el-table-column>
<el-table-column prop="authMethod" label="认证方式" min-width="50px">
<template #default="scope">
<el-tag v-if="scope.row.authMethod == 1" type="success" size="small">密码</el-tag>
<el-tag v-if="scope.row.authMethod == 2" type="primary" size="small">密钥</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="100px" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="creator" label="创建人" min-width="60px"></el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="100px">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="modifier" label="修改者" min-width="60px" show-overflow-tooltip></el-table-column>
<el-table-column prop="updateTime" label="更新时间" min-width="100px">
<template #default="scope">
{{ dateFormat(scope.row.updateTime) }}
</template>
</el-table-column>
</el-table>
<el-row style="margin-top: 20px" type="flex" justify="end">
<el-pagination style="text-align: right" @current-change="handlePageChange" :total="total"
layout="prev, pager, next, total, jumper" v-model:current-page="query.pageNum"
:page-size="query.pageSize"></el-pagination>
</el-row>
</el-card>
<auth-cert-edit :title="editor.title" v-model:visible="editor.visible" :data="editor.authcert"
@val-change="editChange" />
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, onMounted } from 'vue';
import AuthCertEdit from './AuthCertEdit.vue';
import { authCertApi } from '../api';
import { ElMessage, ElMessageBox } from 'element-plus';
import { dateFormat } from '@/common/utils/date';
const state = reactive({
query: {
pageNum: 1,
pageSize: 10,
name: null,
type: null,
},
total: 0,
authcerts: [],
chooseId: null,
chooseData: null,
paramsDialog: {
visible: false,
config: null as any,
params: {},
paramsFormItem: [] as any,
},
editor: {
title: '授权凭证保存',
visible: false,
authcert: {},
},
});
const {
query,
total,
authcerts,
chooseId,
chooseData,
editor,
} = toRefs(state)
onMounted(() => {
search();
});
const search = async () => {
let res = await authCertApi.list.request(state.query);
state.authcerts = res.list;
state.total = res.total;
};
const handlePageChange = (curPage: number) => {
state.query.pageNum = curPage;
search();
};
const choose = (item: any) => {
if (!item) {
return;
}
state.chooseId = item.id;
state.chooseData = item;
};
const editChange = () => {
ElMessage.success('保存成功');
state.chooseId = null;
state.chooseData = null;
search();
};
const edit = (data: any) => {
if (data) {
state.editor.authcert = data;
} else {
state.editor.authcert = false;
}
state.editor.visible = true;
};
const deleteAc = async (data: any) => {
try {
await ElMessageBox.confirm(`确定删除该授权凭证?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await authCertApi.delete.request({ id: data.id });
ElMessage.success('删除成功');
state.chooseData = null;
state.chooseId = null;
search();
} catch (err) { }
}
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,63 @@
<template>
<div style="width: 100%">
<el-select @change="changeValue" v-model="id" filterable placeholder="请选择授权凭证,可前往[机器管理->授权凭证]添加"
style="width: 100%">
<el-option v-for="ac in acs" :key="ac.id" :value="ac.id" :label="ac.name">
<el-tag v-if="ac.authMethod == 1" type="success" size="small">密码</el-tag>
<el-tag v-if="ac.authMethod == 2" type="primary" size="small">密钥</el-tag>
<el-divider direction="vertical" border-style="dashed" />
{{ ac.name }}
<el-divider direction="vertical" border-style="dashed" />
{{ ac.remark }}
</el-option>
</el-select>
</div>
</template>
<script lang="ts" setup>
import { reactive, toRefs, onMounted } from 'vue';
import { authCertApi } from '../api';
//定义事件
const emit = defineEmits(['update:modelValue', 'change'])
const props = defineProps({
modelValue: {
type: [Number],
required: true,
},
})
const state = reactive({
acs: [] as any,
id: null as any,
})
const {
acs,
id,
} = toRefs(state)
onMounted(async () => {
await getAcs();
if (props.modelValue) {
state.id = props.modelValue;
}
})
const changeValue = (val: any) => {
emit('update:modelValue', val);
emit('change', val);
}
const getAcs = async () => {
const acs = await authCertApi.baseList.request({ pageSize: 100, type: 2 })
state.acs = acs.list;
}
</script>
<style lang="scss"></style>

View File

@@ -1,101 +1,110 @@
<template>
<div>
<div class="toolbar">
<el-row type="flex" justify="space-between">
<el-col :span="24">
<el-form class="search-form" label-position="right" :inline="true">
<el-form-item label="标签">
<el-select @change="changeTag" @focus="getTags" v-model="query.tagPath" placeholder="请选择标签"
filterable style="width: 250px">
<el-option v-for="item in tags" :key="item" :label="item" :value="item"> </el-option>
</el-select>
</el-form-item>
<el-form-item label="实例" label-width="40px">
<el-select v-model="mongoId" placeholder="请选择mongo" @change="changeMongo">
<el-option v-for="item in mongoList" :key="item.id" :label="item.name" :value="item.id">
<span style="float: left">{{ item.name }}</span>
<span style="float: right; color: #8492a6; margin-left: 6px; font-size: 13px">{{ `
[${item.uri}]`
}}</span>
</el-option>
</el-select>
</el-form-item>
<el-row>
<el-col :span="4">
<tag-tree @node-click="nodeClick" :load="loadNode">
<template #prefix="{ data }">
<span v-if="data.type == NodeType.Mongo">
<el-popover placement="right-start" title="mongo实例信息" trigger="hover" :width="210">
<template #reference>
<img src="@/assets/icon/mongo.png" class="img-icon" />
</template>
<template #default>
<el-form class="instances-pop-form" label-width="50px" :size="'small'">
<el-form class="instances-pop-form" label-width="55px" :size="'small'">
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
<el-form-item label="链接:">{{ data.params.uri }}</el-form-item>
</el-form>
</el-form>
</template>
</el-popover>
</span>
<el-form-item label="库" label-width="20px">
<el-select v-model="database" placeholder="请选择库" @change="changeDatabase" filterable>
<el-option v-for="item in databases" :key="item.Name" :label="item.Name"
:value="item.Name">
<span style="float: left">{{ item.Name }}</span>
<span style="float: right; color: #8492a6; margin-left: 4px; font-size: 13px">{{
` [${formatByteSize(item.SizeOnDisk)}]`
}}</span>
</el-option>
</el-select>
</el-form-item>
<el-icon v-if="data.type == NodeType.Dbs">
<Coin color="#67c23a" />
</el-icon>
<el-form-item label="集合" label-width="40px">
<el-select v-model="collection" placeholder="请选择集合" @change="changeCollection" filterable>
<el-option v-for="item in collections" :key="item" :label="item" :value="item">
</el-option>
</el-select>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
<el-icon v-if="data.type == NodeType.Coll || data.type == NodeType.CollMenu">
<Document class="color-primary" />
</el-icon>
</template>
<el-container id="data-exec" style="border: 1px solid #eee; margin-top: 1px">
<el-tabs @tab-remove="removeDataTab" @tab-click="onDataTabClick" style="width: 100%; margin-left: 5px"
v-model="activeName">
<el-tab-pane closable v-for="dt in dataTabs" :key="dt.name" :label="dt.name" :name="dt.name">
<el-row v-if="mongoId">
<el-link @click="findCommand(activeName)" icon="refresh" :underline="false" class="ml5">
</el-link>
<el-link @click="showInsertDocDialog" class="ml5" type="primary" icon="plus" :underline="false">
</el-link>
</el-row>
<el-row class="mt5 mb5">
<el-input ref="findParamInputRef" v-model="dt.findParamStr" placeholder="点击输入相应查询条件"
@focus="showFindDialog(dt.name)">
<template #prepend>查询参数</template>
</el-input>
</el-row>
<el-row>
<el-col :span="6" v-for="item in dt.datas" :key="item">
<el-card :body-style="{ padding: '0px', position: 'relative' }">
<el-input type="textarea" v-model="item.value" :rows="12" />
<div style="padding: 3px; float: right" class="mr5 mongo-doc-btns">
<template #label="{ data }">
<span v-if="data.type == NodeType.Dbs">
{{ data.params.dbName }}
<span style="color: #8492a6;font-size: 13px">
[{{ formatByteSize(data.params.size) }}]
</span>
</span>
<span v-else>{{ data.label }}</span>
</template>
</tag-tree>
</el-col>
<el-col :span="20">
<el-container id="mongo-tab" style="border: 1px solid #eee; margin-top: 1px">
<el-tabs @tab-remove="removeDataTab" style="width: 100%; margin-left: 5px"
v-model="state.activeName">
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label"
:name="dt.key">
<el-row>
<el-col :span="2">
<div>
<el-link @click="onJsonEditor(item)" :underline="false" type="success"
icon="MagicStick"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link @click="onSaveDoc(item.value)" :underline="false" type="warning"
icon="DocumentChecked"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-popconfirm @confirm="onDeleteDoc(item.value)" title="确定删除该文档?">
<template #reference>
<el-link :underline="false" type="danger" icon="DocumentDelete">
</el-link>
</template>
</el-popconfirm>
<el-link @click="findCommand(state.activeName)" icon="refresh"
:underline="false" class="">
</el-link>
<el-link @click="showInsertDocDialog" class="ml5" type="primary" icon="plus"
:underline="false">
</el-link>
</div>
</div>
</el-card>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</el-container>
</el-col>
<el-col :span="22">
<el-input ref="findParamInputRef" v-model="dt.findParamStr" placeholder="点击输入相应查询条件"
@focus="showFindDialog(dt.key)">
<template #prepend>查询参数</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="6" v-for="item in dt.datas" :key="item">
<el-card :body-style="{ padding: '0px', position: 'relative' }">
<el-input type="textarea" v-model="item.value" :rows="10" />
<div style="padding: 3px; float: right" class="mr5 mongo-doc-btns">
<div>
<el-link @click="onJsonEditor(item)" :underline="false" type="success"
icon="MagicStick"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link @click="onSaveDoc(item.value)" :underline="false"
type="warning" icon="DocumentChecked"></el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-popconfirm @confirm="onDeleteDoc(item.value)" title="确定删除该文档?">
<template #reference>
<el-link :underline="false" type="danger" icon="DocumentDelete">
</el-link>
</template>
</el-popconfirm>
</div>
</div>
</el-card>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</el-container>
</el-col>
</el-row>
<el-dialog width="600px" title="find参数" v-model="findDialog.visible">
<el-form label-width="70px">
<el-form-item label="filter">
<el-input v-model="findDialog.findParam.filter" type="textarea" :rows="6" clearable
auto-complete="off"></el-input>
<monaco-editor style="width: 100%;" height="150px" ref="monacoEditorRef"
v-model="findDialog.findParam.filter" language="json" />
</el-form-item>
<el-form-item label="sort">
<el-input v-model="findDialog.findParam.sort" type="textarea" :rows="3" clearable
@@ -116,7 +125,7 @@
</template>
</el-dialog>
<el-dialog width="60%" :title="`新增'${activeName}'集合文档`" v-model="insertDocDialog.visible"
<el-dialog width="60%" :title="`新增'${state.activeName}'集合文档`" v-model="insertDocDialog.visible"
:close-on-click-modal="false">
<monaco-editor v-model="insertDocDialog.doc" language="json" />
<template #footer>
@@ -127,9 +136,9 @@
</template>
</el-dialog>
<el-dialog width="60%" title="json编辑器" v-model="jsoneditorDialog.visible" @close="onCloseJsonEditDialog"
<el-dialog width="60%" title="json编辑器" v-model="jsonEditorDialog.visible" @close="onCloseJsonEditDialog"
:close-on-click-modal="false">
<monaco-editor v-model="jsoneditorDialog.doc" language="json" />
<monaco-editor v-model="jsonEditorDialog.doc" language="json" />
</el-dialog>
<div style="text-align: center; margin-top: 10px"></div>
@@ -138,29 +147,30 @@
<script lang="ts" setup>
import { mongoApi } from './api';
import { toRefs, ref, reactive, watch } from 'vue';
import { reactive, ref, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import { isTrue, notBlank, notNull } from '@/common/assert';
import { formatByteSize } from '@/common/utils/format';
import { tagApi } from '../tag/api.ts';
import { useStore } from '@/store/index.ts';
import { isTrue, notBlank } from '@/common/assert';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { formatByteSize } from '@/common/utils/format';
/**
* 树节点类型
*/
class NodeType {
static Mongo = 1
static Dbs = 2
static CollMenu = 3
static Coll = 4
}
const store = useStore();
const findParamInputRef: any = ref(null);
const state = reactive({
tags: [],
mongoList: [] as any,
query: {
tagPath: null,
},
mongoId: null, // 当前选择操作的mongo
database: '', // 当前选择操作的库
collection: '', //当前选中的collection
activeName: '', // 当前操作的tab
databases: [] as any,
collections: [] as any,
dataTabs: {} as any, // 数据tabs
findDialog: {
visible: false,
@@ -175,7 +185,7 @@ const state = reactive({
visible: false,
doc: '',
},
jsoneditorDialog: {
jsonEditorDialog: {
visible: false,
doc: '',
item: {} as any,
@@ -183,69 +193,116 @@ const state = reactive({
});
const {
tags,
mongoList,
query,
mongoId,
database,
collection,
activeName,
databases,
collections,
dataTabs,
findDialog,
insertDocDialog,
jsoneditorDialog,
jsonEditorDialog,
} = toRefs(state)
const searchMongo = async () => {
notNull(state.query.tagPath, '请先选择标签');
const res = await mongoApi.mongoList.request(state.query);
state.mongoList = res.list;
};
/**
* instmap; tagPaht -> mongo info[]
*/
const instMap: Map<string, any[]> = new Map();
const changeTag = (tagPath: string) => {
state.databases = [];
state.collections = [];
state.mongoId = null;
state.collection = '';
state.database = '';
state.dataTabs = {};
if (tagPath != null) {
searchMongo();
const getInsts = async () => {
const res = await mongoApi.mongoList.request({ pageNum: 1, pageSize: 1000, });
if (!res.total) return
for (const mongoInfo of res.list) {
const tagPath = mongoInfo.tagPath;
let mongoInsts = instMap.get(tagPath) || [];
mongoInsts.push(mongoInfo);
instMap.set(tagPath, mongoInsts);
}
}
/**
* 加载文件树节点
* @param {Object} node
* @param {Object} resolve
*/
const loadNode = async (node: any) => {
// 一级为tagPath
if (node.level === 0) {
await getInsts();
const tagPaths = instMap.keys();
const tagNodes = [];
for (let tagPath of tagPaths) {
tagNodes.push(new TagTreeNode(tagPath, tagPath));
}
return tagNodes;
}
const data = node.data;
const params = data.params;
const nodeType = data.type;
// 点击标签 -> 显示mongo信息列表
if (nodeType === TagTreeNode.TagPath) {
const mongoInfos = instMap.get(data.key)
return mongoInfos?.map((x: any) => {
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.Mongo).withParams(x);
});
}
// 点击mongo -> 加载mongo数据库列表
if (nodeType === NodeType.Mongo) {
return await getDatabases(params);
}
// 点击数据库列表 -> 加载数据库下拥有的菜单列表
if (nodeType === NodeType.Dbs) {
return [new TagTreeNode(`${params.id}.${params.dbName}.mongo-coll`, '集合', NodeType.CollMenu).withParams(params)];
}
// 点击数据库集合节点 -> 加载集合列表
if (nodeType === NodeType.CollMenu) {
return await getCollections(params.id, params.dbName)
}
return [];
};
const getTags = async () => {
state.tags = await tagApi.getAccountTags.request(null);
};
/**
* 获取实例的所有库信息
* @param inst 实例信息
*/
const getDatabases = async (inst: any) => {
const res = await mongoApi.databases.request({ id: inst.id });
return res.Databases.map((x: any) => {
const dbName = x.Name;
return new TagTreeNode(`${inst.id}.${dbName}`, dbName, NodeType.Dbs).withParams({
id: inst.id,
dbName,
size: x.SizeOnDisk,
})
})
}
const changeMongo = () => {
state.databases = [];
state.collections = [];
state.dataTabs = {};
getDatabases();
};
/**
* 获取集合列表信息
* @param inst
*/
const getCollections = async (id: any, database: string) => {
const colls = await mongoApi.collections.request({ id, database });
return colls.map((x: any) => {
return new TagTreeNode(`${id}.${database}.${x}`, x, NodeType.Coll).withIsLeaf(true).withParams({
id,
database,
collection: x,
});
});
}
const getDatabases = async () => {
const res = await mongoApi.databases.request({ id: state.mongoId });
state.databases = res.Databases;
};
const nodeClick = (data: any) => {
// 点击集合
if (data.type === NodeType.Coll) {
const { id, database, collection } = data.params;
changeCollection(id, database, collection);
}
}
const changeDatabase = () => {
state.collections = [];
state.collection = '';
state.dataTabs = {};
getCollections();
};
const getCollections = async () => {
state.collections = await mongoApi.collections.request({ id: state.mongoId, database: state.database });
};
const changeCollection = () => {
const collection = state.collection;
let dataTab = state.dataTabs[collection];
const changeCollection = (id: any, schema: string, collection: string) => {
const label = `${id}:\`${schema}\`.${collection}`;
let dataTab = state.dataTabs[label];
if (!dataTab) {
// 默认查询参数
const findParam = {
@@ -253,29 +310,33 @@ const changeCollection = () => {
sort: '{"_id": -1}',
skip: 0,
limit: 12,
},
dataTab = {
name: collection,
datas: [],
findParamStr: JSON.stringify(findParam),
findParam,
};
state.dataTabs[collection] = dataTab;
};
state.dataTabs[label] = {
key: label,
label: label,
name: label,
mongoId: id,
database: schema,
collection,
datas: [],
findParamStr: JSON.stringify(findParam),
findParam,
};
}
state.activeName = collection;
findCommand(collection);
state.activeName = label;
findCommand(label);
};
const showFindDialog = (collection: string) => {
const showFindDialog = (key: string) => {
// 获取当前tab的索引位置将其输入框失去焦点防止输入以及重复获取焦点
const dataTabNames = Object.keys(state.dataTabs);
for (let i = 0; i < dataTabNames.length; i++) {
if (collection == dataTabNames[i]) {
if (key == dataTabNames[i]) {
findParamInputRef.value[i].blur();
}
}
state.findDialog.findParam = state.dataTabs[collection].findParam;
state.findDialog.findParam = state.dataTabs[key].findParam;
state.findDialog.visible = true;
};
@@ -286,8 +347,8 @@ const confirmFindDialog = () => {
findCommand(state.activeName);
};
const findCommand = async (collection: string) => {
const dataTab = state.dataTabs[collection];
const findCommand = async (key: string) => {
const dataTab = getNowDataTab();
const findParma = dataTab.findParam;
let filter, sort;
try {
@@ -298,15 +359,15 @@ const findCommand = async (collection: string) => {
return;
}
const datas = await mongoApi.findCommand.request({
id: state.mongoId,
database: state.database,
collection,
id: dataTab.mongoId,
database: dataTab.database,
collection: dataTab.collection,
filter,
sort,
limit: findParma.limit || 12,
skip: findParma.skip || 0,
});
state.dataTabs[collection].datas = wrapDatas(datas);
state.dataTabs[key].datas = wrapDatas(datas);
};
/**
@@ -344,10 +405,11 @@ const onInsertDoc = async () => {
} catch (e) {
ElMessage.error('文档内容错误,无法解析为json对象');
}
const dataTab = getNowDataTab();
const res = await mongoApi.insertCommand.request({
id: state.mongoId,
database: state.database,
collection: state.activeName,
id: dataTab.mongoId,
database: dataTab.database,
collection: dataTab.collection,
doc: docObj,
});
isTrue(res.InsertedID, '新增失败');
@@ -357,13 +419,13 @@ const onInsertDoc = async () => {
};
const onJsonEditor = (item: any) => {
state.jsoneditorDialog.item = item;
state.jsoneditorDialog.doc = item.value;
state.jsoneditorDialog.visible = true;
state.jsonEditorDialog.item = item;
state.jsonEditorDialog.doc = item.value;
state.jsonEditorDialog.visible = true;
};
const onCloseJsonEditDialog = () => {
state.jsoneditorDialog.item.value = JSON.stringify(JSON.parse(state.jsoneditorDialog.doc), null, 4);
state.jsonEditorDialog.item.value = JSON.stringify(JSON.parse(state.jsonEditorDialog.doc), null, 4);
};
const onSaveDoc = async (doc: string) => {
@@ -371,10 +433,11 @@ const onSaveDoc = async (doc: string) => {
const id = docObj._id;
notBlank(id, '文档的_id属性不存在');
delete docObj['_id'];
const dataTab = getNowDataTab();
const res = await mongoApi.updateByIdCommand.request({
id: state.mongoId,
database: state.database,
collection: state.collection,
id: dataTab.mongoId,
database: dataTab.database,
collection: dataTab.collection,
docId: id,
update: { $set: docObj },
});
@@ -386,10 +449,11 @@ const onDeleteDoc = async (doc: string) => {
const docObj = parseDocJsonString(doc);
const id = docObj._id;
notBlank(id, '文档的_id属性不存在');
const dataTab = getNowDataTab();
const res = await mongoApi.deleteByIdCommand.request({
id: state.mongoId,
database: state.database,
collection: state.collection,
id: dataTab.mongoId,
database: dataTab.database,
collection: dataTab.collection,
docId: id,
});
isTrue(res.DeletedCount == 1, '删除失败');
@@ -409,15 +473,6 @@ const parseDocJsonString = (doc: string) => {
}
};
/**
* 数据tab点击
*/
const onDataTabClick = (tab: any) => {
const name = tab.props.name;
// 修改选择框绑定的表信息
state.collection = name;
};
const removeDataTab = (targetName: string) => {
const tabNames = Object.keys(state.dataTabs);
let activeName = state.activeName;
@@ -430,44 +485,16 @@ const removeDataTab = (targetName: string) => {
}
});
state.activeName = activeName;
// 如果移除最后一个数据tab则将选择框绑定的collection置空
if (activeName == targetName) {
state.collection = '';
} else {
state.collection = activeName;
}
delete state.dataTabs[targetName];
};
// 加载选中的tagPath
const setSelects = async (mongoDbOptInfo: any) => {
const { tagPath, dbId, db } = mongoDbOptInfo.dbOptInfo;
state.query.tagPath = tagPath
await searchMongo();
state.mongoId = dbId
await getDatabases();
state.database = db
await getCollections();
if (state.collection) {
state.collection = ''
state.dataTabs = {}
}
const getNowDataTab = () => {
return state.dataTabs[state.activeName]
}
// 判断如果有数据则加载下拉选项
let mongoDbOptInfo = store.state.mongoDbOptInfo
if (mongoDbOptInfo.dbOptInfo.tagPath) {
setSelects(mongoDbOptInfo)
}
// 监听选中操作的db变化并加载下拉选项
watch(store.state.mongoDbOptInfo, async (newValue) => {
await setSelects(newValue)
})
</script>
<style>
<style lang="scss">
.mongo-doc-btns {
position: absolute;
z-index: 2;
@@ -475,4 +502,14 @@ watch(store.state.mongoDbOptInfo, async (newValue) => {
top: 2px;
max-width: 120px;
}
#mongo-tab {
.el-tabs__header {
margin: 0 0 5px;
.el-tabs__item {
padding: 0 5px;
}
}
}
</style>

View File

@@ -1,34 +1,29 @@
<template>
<div>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false"
width="38%" :destroy-on-close="true">
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false" width="38%"
:destroy-on-close="true">
<el-form :model="form" ref="mongoForm" :rules="rules" label-width="85px">
<el-form-item prop="tagId" label="标签:" required>
<tag-select v-model:tag-id="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签:" required>
<tag-select v-model:tag-id="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入名称" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="uri" label="uri" required>
<el-input type="textarea" :rows="2" v-model.trim="form.uri"
placeholder="形如 mongodb://username:password@host1:port1" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入名称" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="uri" label="uri" required>
<el-input type="textarea" :rows="2" v-model.trim="form.uri"
placeholder="形如 mongodb://username:password@host1:port1" auto-complete="off"></el-input>
</el-form-item>
</el-tab-pane>
<el-form-item prop="enableSshTunnel" label="SSH隧道:">
<el-col :span="3">
<el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1"
:false-label="-1"></el-checkbox>
</el-col>
<el-col :span="2" v-if="form.enableSshTunnel == 1"> 机器: </el-col>
<el-col :span="19" v-if="form.enableSshTunnel == 1">
<el-select style="width: 100%" v-model="form.sshTunnelMachineId" placeholder="请选择SSH隧道机器">
<el-option v-for="item in sshTunnelMachineList" :key="item.id"
:label="`${item.ip}:${item.port} [${item.name}]`" :value="item.id">
</el-option>
</el-select>
</el-col>
</el-form-item>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
@@ -44,9 +39,9 @@
<script lang="ts" setup>
import { toRefs, reactive, watch, ref } from 'vue';
import { mongoApi } from './api';
import { machineApi } from '../machine/api.ts';
import { ElMessage } from 'element-plus';
import TagSelect from '../component/TagSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
const props = defineProps({
visible: {
@@ -90,13 +85,12 @@ const rules = {
const mongoForm: any = ref(null);
const state = reactive({
dialogVisible: false,
sshTunnelMachineList: [] as any,
tabActiveName: 'basic',
form: {
id: null,
name: null,
uri: null,
enableSshTunnel: -1,
sshTunnelMachineId: null,
sshTunnelMachineId: null as any,
tagId: null as any,
tagPath: null as any,
},
@@ -105,7 +99,7 @@ const state = reactive({
const {
dialogVisible,
sshTunnelMachineList,
tabActiveName,
form,
btnLoading,
} = toRefs(state)
@@ -115,25 +109,21 @@ watch(props, async (newValue: any) => {
if (!state.dialogVisible) {
return;
}
state.tabActiveName = 'basic';
if (newValue.mongo) {
state.form = { ...newValue.mongo };
} else {
state.form = { db: 0 } as any;
}
getSshTunnelMachines();
});
const getSshTunnelMachines = async () => {
if (state.form.enableSshTunnel == 1 && state.sshTunnelMachineList.length == 0) {
const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
state.sshTunnelMachineList = res.list;
}
};
const btnOk = async () => {
mongoForm.value.validate(async (valid: boolean) => {
if (valid) {
const reqForm = { ...state.form };
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
reqForm.sshTunnelMachineId = -1
}
// reqForm.uri = await RsaEncrypt(reqForm.uri);
mongoApi.saveMongo.request(reqForm).then(() => {
ElMessage.success('保存成功');
@@ -157,6 +147,4 @@ const cancel = () => {
emit('cancel');
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@@ -0,0 +1,178 @@
<template>
<tag-menu :instanceMenuMaxHeight="state.instanceMenuMaxHeight" :tags="instances.tags" ref="menuRef">
<template #submenu="props">
<el-sub-menu v-for="inst in instances.tree[props.tag.tagId]" :index="'mongo-instance-' + inst.id"
:key="'mongo-instance-' + inst.id" @click.stop="changeInstance(inst, () => { })">
<template #title>
<el-popover placement="right-start" title="mongo数据库实例信息" trigger="hover" :width="210">
<template #reference>
<span>&nbsp;&nbsp;<el-icon>
<MostlyCloudy color="#409eff" />
</el-icon>{{ inst.name }}</span>
</template>
<template #default>
<el-form class="instances-pop-form" label-width="55px" :size="'small'">
<el-form-item label="名称:">{{ inst.name }}</el-form-item>
<el-form-item label="链接:">{{ inst.uri }}</el-form-item>
</el-form>
</template>
</el-popover>
</template>
<!-- 第三级数据库 -->
<el-sub-menu v-for="db in instances.dbs[inst.id]" :index="inst.id + db.Name" :key="inst.id + db.Name"
:class="state.nowSchema === (inst.id + db.Name) && 'checked'"
@click.stop="changeSchema(inst, db.Name)">
<template #title>
&nbsp;&nbsp;&nbsp;&nbsp;<el-icon>
<Coin color="#67c23a" />
</el-icon>
<span class="checked-schema">
{{ db.Name }}
<span style="color: #8492a6;font-size: 13px">[{{
formatByteSize(db.SizeOnDisk)
}}]</span>
</span>
</template>
<!-- 第四级 01 -->
<el-sub-menu :index="inst.id + db.Name + '-table'">
<template #title>
<div style="width: 100%" @click="loadTableNames(inst, db.Name, () => { })">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<el-icon>
<Calendar color="#409eff" />
</el-icon>
<span>集合</span>
<el-icon v-show="state.loading[inst.id + db.Name]" class="is-loading">
<Loading />
</el-icon>
</div>
</template>
<el-menu-item :index="inst.id + db.Name + '-tableSearch'"
:key="inst.id + db.Name + '-tableSearch'">
<template #title>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<el-input size="small" placeholder="过滤" clearable
@change="filterTableName(inst.id, db.Name)"
@keyup="(e: any) => filterTableName(inst.id, db.Name, e)"
v-model="state.filterParam[inst.id + db.Name]" />
</template>
</el-menu-item>
<template v-for="tb in instances.tables[inst.id + db.Name]">
<el-menu-item :index="inst.id + db.Name + tb.tableName"
:key="inst.id + db.Name + tb.tableName" v-if="tb.show"
@click="loadTableData(inst, db.Name, tb.tableName)">
<template #title>
<div style="width: 100%">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<el-icon>
<Calendar color="#409eff" />
</el-icon>
<span :title="tb.tableComment || ''">{{ tb.tableName }}</span>
</div>
</template>
</el-menu-item>
</template>
</el-sub-menu>
</el-sub-menu>
</el-sub-menu>
</template>
</tag-menu>
</template>
<script lang="ts" setup>
import { onBeforeMount, reactive } from 'vue';
import { formatByteSize } from '@/common/utils/format';
import TagMenu from '../component/TagMenu.vue';
const props = defineProps({
instances: {
type: Object, required: true
},
})
const emits = defineEmits(['initLoadInstances', 'changeInstance', 'loadTableNames', 'loadTableData', 'changeSchema'])
onBeforeMount(async () => {
await initLoadInstances()
setHeight()
})
const setHeight = () => {
state.instanceMenuMaxHeight = window.innerHeight - 115 + 'px';
}
const state = reactive({
instanceMenuMaxHeight: '800px',
nowSchema: '',
filterParam: {},
loading: {},
})
/**
* 初始化加载实例数据
*/
const initLoadInstances = () => {
emits('initLoadInstances')
}
/**
* 改变选中的数据库实例
* @param inst 选中的实例对象
* @param fn 选中的实例对象后的回调事件
*/
const changeInstance = (inst: any, fn: Function) => {
emits('changeInstance', inst, fn)
}
/**
* 改变选中的数据库schema
* @param inst 选中的实例对象
* @param schema 选中的数据库schema
*/
const changeSchema = (inst: any, schema: string) => {
state.nowSchema = inst.id + schema
emits('changeSchema', inst, schema)
}
/**
* 加载schema下所有表
* @param inst 数据库实例
* @param fn 加载表名后的回调函数参数表名list
* @param schema database名
*/
const loadTableNames = async (inst: any, schema: string, fn: Function) => {
state.loading[inst.id + schema] = true
await emits('loadTableNames', inst, schema, (res: any) => {
state.loading[inst.id + schema] = false
fn && fn(res)
})
}
/**
* 加载选中表数据
* @param inst 数据库实例
* @param schema database名
* @param tableName 表名
*/
const loadTableData = (inst: any, schema: string, tableName: string) => {
emits('loadTableData', inst, schema, tableName)
}
const filterTableName = (instId: number, schema: string, event?: any) => {
if (event) {
state.filterParam[instId + schema] = event.target.value
}
let param = state.filterParam[instId + schema] as string
param = param?.replace('/', '\/')
const key = instId + schema;
props.instances.tables[key].forEach((a: any) => {
a.show = param ? eval('/' + param.split('').join('[_\w]*') + '[_\w]*/ig').test(a.tableName) : true
})
}
</script>
<style lang="scss">
.instances-pop-form {
.el-form-item {
margin-bottom: unset;
}
}
</style>

View File

@@ -20,7 +20,14 @@
</el-radio>
</template>
</el-table-column>
<el-table-column prop="tagPath" label="标签路径" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="tagPath" label="标签路径" min-width="150" show-overflow-tooltip>
<template #default="scope">
<tag-info :tag-path="scope.row.tagPath" />
<span class="ml5">
{{ scope.row.tagPath }}
</span>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width></el-table-column>
<el-table-column prop="uri" label="连接uri" min-width="150" show-overflow-tooltip>
<template #default="scope">
@@ -65,9 +72,6 @@
<el-divider direction="vertical" border-style="dashed" />
<el-link type="primary" @click="showCollections(scope.row.Name)" plain size="small"
:underline="false">集合</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link type="primary" @click="openDataOps(scope.row)" plain size="small" :underline="false">
数据操作</el-link>
</template>
</el-table-column>
</el-table>
@@ -195,9 +199,8 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import { tagApi } from '../tag/api.ts';
import MongoEdit from './MongoEdit.vue';
import { formatByteSize } from '@/common/utils/format';
import { store } from '@/store';
import router from '@/router';
import { dateFormat } from '@/common/utils/date';
import TagInfo from '../component/TagInfo.vue';
const state = reactive({
tags: [],
@@ -406,24 +409,6 @@ const valChange = () => {
search();
};
const openDataOps = (row: any) => {
state.dbOps.db = row.Name
debugger
let data = {
tagPath: state.currentData.tagPath,
dbId: state.dbOps.dbId,
db: state.dbOps.db,
}
state.databaseDialog.visible = false;
// 判断db是否发生改变
let oldDb = store.state.mongoDbOptInfo.dbOptInfo.db;
if (oldDb !== row.Name) {
store.dispatch('mongoDbOptInfo/setMongoDbOptInfo', data);
}
router.push({ name: 'MongoDataOp' });
}
</script>
<style>

View File

@@ -1,34 +1,41 @@
<template>
<div>
<el-card>
<div style="float: left">
<el-row>
<el-col :span="4">
<el-row type="flex" justify="space-between">
<el-col :span="24">
<el-form class="search-form" label-position="right" :inline="true">
<el-form-item label="标签">
<el-select @change="changeTag" @focus="getTags" v-model="query.tagPath"
placeholder="请选择标签" filterable style="width: 250px">
<el-option v-for="item in tags" :key="item" :label="item" :value="item">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="redis" label-width="40px">
<el-select v-model="scanParam.id" placeholder="请选择redis" @change="changeRedis"
@clear="clearRedis" clearable style="width: 250px">
<el-option v-for="item in redisList" :key="item.id"
:label="`${item.name ? item.name : ''} [${item.host}]`" :value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="库" label-width="20px">
<el-select v-model="scanParam.db" @change="changeDb" placeholder="库"
style="width: 85px">
<el-option v-for="db in dbList" :key="db" :label="db" :value="db"> </el-option>
</el-select>
</el-form-item>
</el-form>
<el-col :span="24" class="el-scrollbar flex-auto">
<tag-tree @node-click="nodeClick" :load="loadNode">
<template #prefix="{ data }">
<span v-if="data.type == NodeType.Redis">
<el-popover placement="right-start" title="redis实例信息" trigger="hover" :width="210">
<template #reference>
<img src="@/assets/icon/redis.png" class="img-icon" />
</template>
<template #default>
<el-form class="instances-pop-form" label-width="50px" :size="'small'">
<el-form-item label="名称:">{{ data.params.name }}</el-form-item>
<el-form-item label="模式:">{{ data.params.mode }}</el-form-item>
<el-form-item label="链接:">{{ data.params.host }}</el-form-item>
<el-form-item label="备注:">{{
data.params.remark
}}</el-form-item>
</el-form>
</template>
</el-popover>
</span>
<el-icon v-if="data.type == NodeType.Db">
<Coin color="#67c23a" />
</el-icon>
</template>
</tag-tree>
</el-col>
<el-col class="mt10">
</el-row>
</el-col>
<el-col :span="20" style="border-left: 1px solid var(--el-card-border-color);">
<div class="mt10 ml5">
<el-col>
<el-form class="search-form" label-position="right" :inline="true" label-width="60px">
<el-form-item label="key" label-width="40px">
<el-input placeholder="match 支持*模糊key" style="width: 250px" v-model="scanParam.match"
@@ -55,35 +62,36 @@
</el-popover>
</el-form-item>
<div style="float: right">
<span>keys: {{ dbsize }}</span>
<span>keys: {{ state.dbsize }}</span>
</div>
</el-form>
</el-col>
</el-row>
</div>
<el-table v-loading="loading" :data="keys" stripe :highlight-current-row="true" style="cursor: pointer">
<el-table-column show-overflow-tooltip prop="key" label="key"></el-table-column>
<el-table-column prop="type" label="type" width="80">
<template #default="scope">
<el-tag :color="getTypeColor(scope.row.type)" size="small">{{ scope.row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="ttl" label="ttl(过期时间)" width="140">
<template #default="scope">
{{ ttlConveter(scope.row.ttl) }}
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button @click="getValue(scope.row)" type="success" icon="search" plain size="small">查看
</el-button>
<el-button @click="del(scope.row.key)" type="danger" icon="delete" plain size="small">删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-table v-loading="state.loading" :data="state.keys" stripe :highlight-current-row="true"
style="cursor: pointer">
<el-table-column show-overflow-tooltip prop="key" label="key"></el-table-column>
<el-table-column prop="type" label="type" width="80">
<template #default="scope">
<el-tag :color="getTypeColor(scope.row.type)" size="small">{{ scope.row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="ttl" label="ttl(过期时间)" width="140">
<template #default="scope">
{{ ttlConveter(scope.row.ttl) }}
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button @click="getValue(scope.row)" type="success" icon="search" plain
size="small">查看
</el-button>
<el-button @click="del(scope.row.key)" type="danger" icon="delete" plain size="small">删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-col>
</el-row>
<div style="text-align: center; margin-top: 10px"></div>
@@ -107,19 +115,26 @@
<script lang="ts" setup>
import { redisApi } from './api';
import { toRefs, reactive, watch } from 'vue';
import { toRefs, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import HashValue from './HashValue.vue';
import StringValue from './StringValue.vue';
import SetValue from './SetValue.vue';
import ListValue from './ListValue.vue';
import { isTrue, notBlank, notNull } from '@/common/assert';
import { TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { useStore } from '@/store/index.ts';
import { tagApi } from '../tag/api.ts';
/**
* 树节点类型
*/
class NodeType {
static Redis = 1
static Db = 2
}
let store = useStore();
const state = reactive({
instanceMenuMaxHeight: '600',
loading: false,
tags: [],
redisList: [] as any,
@@ -162,60 +177,110 @@ const state = reactive({
});
const {
loading,
tags,
redisList,
dbList,
query,
scanParam,
dataEdit,
hashValueDialog,
stringValueDialog,
setValueDialog,
listValueDialog,
keys,
dbsize,
} = toRefs(state)
const searchRedis = async () => {
notBlank(state.query.tagPath, '请先选择标签');
const res = await redisApi.redisList.request(state.query);
state.redisList = res.list;
};
const changeTag = (tagPath: string) => {
clearRedis();
if (tagPath != null) {
searchRedis();
onMounted(async () => {
setHeight();
})
const setHeight = () => {
state.instanceMenuMaxHeight = window.innerHeight - 115 + 'px';
}
/**
* instmap; tagPaht -> redis info[]
*/
const instMap: Map<string, any[]> = new Map();
const getInsts = async () => {
const res = await redisApi.redisList.request({});
if (!res.total) return
for (const redisInfo of res.list) {
const tagPath = redisInfo.tagPath;
let redisInsts = instMap.get(tagPath) || [];
redisInsts.push(redisInfo);
instMap.set(tagPath, redisInsts);
}
}
/**
* 加载文件树节点
* @param {Object} node
* @param {Object} resolve
*/
const loadNode = async (node: any) => {
// 一级为tagPath
if (node.level === 0) {
await getInsts();
const tagPaths = instMap.keys();
const tagNodes = [];
for (let tagPath of tagPaths) {
tagNodes.push(new TagTreeNode(tagPath, tagPath));
}
return tagNodes;
}
const data = node.data;
// 点击tagPath -> 加载数据库信息列表
if (data.type === TagTreeNode.TagPath) {
const redisInfos = instMap.get(data.key)
return redisInfos?.map((x: any) => {
return new TagTreeNode(`${data.key}.${x.id}`, x.name, NodeType.Redis).withParams(x);
});
}
// 点击redis实例 -> 加载库列表
if (data.type === NodeType.Redis) {
return await getDbs(data.params);
}
return [];
};
const getTags = async () => {
state.tags = await tagApi.getAccountTags.request(null);
};
const nodeClick = (data: any) => {
// 点击库事件
if (data.type === NodeType.Db) {
resetScanParam();
state.scanParam.id = data.params.id;
state.scanParam.db = data.params.db;
scan();
}
}
const changeRedis = (id: number) => {
resetScanParam();
if (id != 0) {
const redis: any = state.redisList.find((x: any) => x.id == id);
if (redis) {
state.dbList = (state.redisList.find((x: any) => x.id == id) as any).db.split(',');
state.scanParam.mode = redis.mode;
/**
* 获取所有库信息
* @param redisInfo redis信息
*/
const getDbs = async (redisInfo: any) => {
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
return new TagTreeNode(x, x, NodeType.Db).withIsLeaf(true).withParams({
id: redisInfo.id,
db: x,
name: `db${x}`,
keys: 0,
})
})
const res = await redisApi.redisInfo.request({ id: redisInfo.id, host: redisInfo.host, section: "Keyspace" });
for (let db in res.Keyspace) {
for (let d of dbs) {
if (db == d.params.name) {
d.params.keys = res.Keyspace[db]?.split(',')[0]?.split('=')[1] || 0
}
}
}
// 默认选中配置的第一个库
state.scanParam.db = state.dbList[0];
state.keys = [];
state.dbsize = 0;
};
const changeDb = () => {
resetScanParam();
state.keys = [];
state.dbsize = 0;
searchKey();
};
// 替换label
dbs.forEach((e: any) => {
e.label = `${e.params.name} [${e.params.keys}]`
});
return dbs;
}
const scan = async () => {
isTrue(state.scanParam.id != null, '请先选择redis');
@@ -256,15 +321,6 @@ const searchKey = async () => {
await scan();
};
const clearRedis = () => {
state.redisList = [];
state.scanParam.id = null;
resetScanParam();
state.scanParam.db = '';
state.keys = [];
state.dbsize = 0;
};
const clear = () => {
resetScanParam();
if (state.scanParam.id) {
@@ -328,20 +384,18 @@ const del = (key: string) => {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
redisApi.delKey
.request({
key,
id: state.scanParam.id,
db: state.scanParam.db,
})
.then(() => {
ElMessage.success('删除成功!');
searchKey();
});
})
.catch(() => { });
}).then(() => {
redisApi.delKey
.request({
key,
id: state.scanParam.id,
db: state.scanParam.db,
})
.then(() => {
ElMessage.success('删除成功!');
searchKey();
});
}).catch(() => { });
};
const ttlConveter = (ttl: any) => {
@@ -391,31 +445,12 @@ const getTypeColor = (type: string) => {
return '#A8DEE0';
}
};
// 加载选中的db
const setSelects = async (redisDbOptInfo: any) => {
// 设置标签路径等
const { tagPath, dbId } = redisDbOptInfo.dbOptInfo;
state.query.tagPath = tagPath;
await searchRedis();
state.scanParam.id = dbId;
changeRedis(dbId);
changeDb();
};
// 判断如果有数据则加载下拉选项
let redisDbOptInfo = store.state.redisDbOptInfo;
if (redisDbOptInfo.dbOptInfo.tagPath) {
setSelects(redisDbOptInfo);
}
// 监听选中操作的db变化并加载下拉选项
watch(store.state.redisDbOptInfo, async (newValue) => {
await setSelects(newValue);
});
</script>
<style>
<style lang="scss">
.instances-pop-form {
.el-form-item {
margin-bottom: unset;
}
}
</style>

View File

@@ -1,152 +1,91 @@
<template>
<div>
<el-dialog :title="title" v-model="dialogVisible" :show-close="true" width="35%" @close="close()">
<el-collapse>
<el-collapse-item title="Server(Redis服务器的一般信息)" name="server">
<div class="row">
<span class="title">redis_version(版本):</span>
<span class="value">{{ info.Server.redis_version }}</span>
</div>
<div class="row">
<span class="title">tcp_port(端口):</span>
<span class="value">{{ info.Server.tcp_port }}</span>
</div>
<div class="row">
<span class="title">redis_mode(模式):</span>
<span class="value">{{ info.Server.redis_mode }}</span>
</div>
<div class="row">
<span class="title">os(宿主操作系统):</span>
<span class="value">{{ info.Server.os }}</span>
</div>
<div class="row">
<span class="title">uptime_in_days(运行天数):</span>
<span class="value">{{ info.Server.uptime_in_days }}</span>
</div>
<div class="row">
<span class="title">executable(可执行文件路径):</span>
<span class="value">{{ info.Server.executable }}</span>
</div>
<div class="row">
<span class="title">config_file(配置文件路径):</span>
<span class="value">{{ info.Server.config_file }}</span>
</div>
</el-collapse-item>
<el-dialog :title="title" v-model="dialogVisible" :show-close="true" width="1000px" @close="close()">
<el-collapse-item title="Clients(客户端连接)" name="client">
<div class="row">
<span class="title">connected_clients(已连接客户端数):</span>
<span class="value">{{ info.Clients.connected_clients }}</span>
</div>
<div class="row">
<span class="title">blocked_clients(正在等待阻塞命令客户端数):</span>
<span class="value">{{ info.Clients.blocked_clients }}</span>
</div>
</el-collapse-item>
<el-collapse-item title="Keyspace(key信息)" name="keyspace">
<div class="row" v-for="(value, key) in info.Keyspace" :key="key">
<span class="title">{{ key }}: </span>
<span class="value">{{ value }}</span>
</div>
</el-collapse-item>
<el-row :gutter="20">
<el-col :lg="16" :md="16">
<el-descriptions class="redis-info info-server" title="Redis服务器信息" :column="3" size="small" border>
<el-descriptions-item label="版本">{{ info.Server.redis_version }}</el-descriptions-item>
<el-descriptions-item label="端口">{{ info.Server.tcp_port }}</el-descriptions-item>
<el-descriptions-item label="PID">{{ info.Server.process_id }}</el-descriptions-item>
<el-descriptions-item label="模式">{{ info.Server.redis_mode }}</el-descriptions-item>
<el-descriptions-item label="操作系统">{{ info.Server.os }}</el-descriptions-item>
<el-descriptions-item label="运行天数">{{ info.Server.uptime_in_days }}</el-descriptions-item>
<el-descriptions-item label="可执行文件路径">{{ info.Server.executable }}</el-descriptions-item>
<el-descriptions-item label="配置文件路径">{{ info.Server.config_file }}</el-descriptions-item>
</el-descriptions>
</el-col>
<el-col :lg="8" :md="8" class="redis-info">
<div class="info-memory-chart" ref="memRef"></div>
</el-col>
</el-row>
<el-collapse-item title="Stats(统计)" name="state">
<div class="row">
<span class="title">total_commands_processed(总处理命令数):</span>
<span class="value">{{ info.Stats.total_commands_processed }}</span>
</div>
<div class="row">
<span class="title">instantaneous_ops_per_sec(当前qps):</span>
<span class="value">{{ info.Stats.instantaneous_ops_per_sec }}</span>
</div>
<div class="row">
<span class="title">total_net_input_bytes(网络入口流量字节数):</span>
<span class="value">{{ info.Stats.total_net_input_bytes }}</span>
</div>
<div class="row">
<span class="title">total_net_output_bytes(网络出口流量字节数):</span>
<span class="value">{{ info.Stats.total_net_output_bytes }}</span>
</div>
<div class="row">
<span class="title">expired_keys(过期key的总数量):</span>
<span class="value">{{ info.Stats.expired_keys }}</span>
</div>
<div class="row">
<span class="title">instantaneous_ops_per_sec(当前qps):</span>
<span class="value">{{ info.Stats.instantaneous_ops_per_sec }}</span>
</div>
</el-collapse-item>
<el-row :gutter="20">
<el-col :lg="12" :md="12">
<el-descriptions class="redis-info info-cluster" title="节点信息" :column="3" size="small" border>
<el-descriptions-item label="是否启用集群模式">{{ info.Cluster.cluster_enabled }}</el-descriptions-item>
<el-descriptions-item label="DB总数">{{ info.Cluster.databases }}</el-descriptions-item>
<el-descriptions-item label="节点总数">{{ info.Cluster.nodecount }}</el-descriptions-item>
</el-descriptions>
</el-col>
<el-collapse-item title="Persistence(持久化)" name="persistence">
<div class="row">
<span class="title">aof_enabled(是否启用aof):</span>
<span class="value">{{ info.Persistence.aof_enabled }}</span>
</div>
<div class="row">
<span class="title">loading(是否正在载入持久化文件):</span>
<span class="value">{{ info.Persistence.loading }}</span>
</div>
</el-collapse-item>
<el-col :lg="12" :md="12">
<el-descriptions class="redis-info info-client" title="客户端连接" :column="3" size="small" border>
<el-descriptions-item label="已连接客户端数">{{ info.Clients.connected_clients
}}</el-descriptions-item>
<el-descriptions-item label="正在等待阻塞命令客户端数">{{ info.Clients.blocked_clients
}}</el-descriptions-item>
</el-descriptions>
</el-col>
</el-row>
<el-collapse-item title="Cluster(集群)" name="cluster">
<div class="row">
<span class="title">cluster_enabled(是否启用集群模式):</span>
<span class="value">{{ info.Cluster.cluster_enabled }}</span>
</div>
</el-collapse-item>
<el-descriptions class="redis-info info-memory" title="CPU" :column="2" size="small" border>
<el-descriptions-item label="系统CPU">{{ info.CPU.used_cpu_sys }}</el-descriptions-item>
<el-descriptions-item label="用户CPU">{{ info.CPU.used_cpu_user }}</el-descriptions-item>
<el-descriptions-item label="后台系统CPU">{{ info.CPU.used_cpu_sys_children }}</el-descriptions-item>
<el-descriptions-item label="后台用户CPU">{{ info.CPU.used_cpu_user_children }}</el-descriptions-item>
</el-descriptions>
<el-collapse-item title="Memory(内存消耗相关信息)" name="memory">
<div class="row">
<span class="title">used_memory(分配内存总量):</span>
<span class="value">{{ info.Memory.used_memory_human }}</span>
</div>
<div class="row">
<span class="title">maxmemory(最大内存配置):</span>
<span class="value">{{ info.Memory.maxmemory }}</span>
</div>
<div class="row">
<span class="title">used_memory_rss(已分配的内存总量操作系统角度):</span>
<span class="value">{{ info.Memory.used_memory_rss_human }}</span>
</div>
<div class="row">
<span class="title">mem_fragmentation_ratio(used_memory_rss和used_memory 之间的比率):</span>
<span class="value">{{ info.Memory.mem_fragmentation_ratio }}</span>
</div>
<div class="row">
<span class="title">used_memory_peak(内存消耗峰值):</span>
<span class="value">{{ info.Memory.used_memory_peak_human }}</span>
</div>
<div class="row">
<span class="title">total_system_memory(主机总内存):</span>
<span class="value">{{ info.Memory.total_system_memory_human }}</span>
</div>
</el-collapse-item>
<el-row :gutter="20" class="redis-info">
<el-col :lg="24" :md="24">
<span style="font-size: 14px; font-weight: 700">键值统计</span>
<el-table :data="Keyspace" stripe max-height="250" style="width: 100%" border>
<el-table-column prop="db" label="数据库" min-width="100" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="keys" label="keys" min-width="70" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="expires" label="expires" min-width="70" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="avg_ttl" label="avg_ttl" min-width="70" show-overflow-tooltip>
</el-table-column>
</el-table>
</el-col>
</el-row>
<el-descriptions class="redis-info info-state" title="统计信息" :column="3" size="small" border>
<el-descriptions-item label="总处理命令数">{{ info.Stats.total_commands_processed }}</el-descriptions-item>
<el-descriptions-item label="当前qps">{{ info.Stats.instantaneous_ops_per_sec }}</el-descriptions-item>
<el-descriptions-item label="过期key的总数量">{{ info.Stats.expired_keys }}</el-descriptions-item>
<el-descriptions-item label="网络入口流量字节数">{{ info.Stats.total_net_input_bytes }}</el-descriptions-item>
<el-descriptions-item label="网络出口流量字节数">{{ info.Stats.total_net_output_bytes }}</el-descriptions-item>
</el-descriptions>
<el-descriptions class="redis-info info-persistence" title="持久化" :column="3" size="small" border>
<el-descriptions-item label="是否启用aof">{{ info.Persistence?.aof_enabled || false
}}</el-descriptions-item>
<el-descriptions-item label="是否正在载入持久化文件">{{ info.Persistence?.loading || false
}}</el-descriptions-item>
</el-descriptions>
<el-collapse-item title="CPU" name="cpu">
<div class="row">
<span class="title">used_cpu_sys(由Redis服务器消耗的系统CPU):</span>
<span class="value">{{ info.CPU.used_cpu_sys }}</span>
</div>
<div class="row">
<span class="title">used_cpu_user(由Redis服务器消耗的用户CPU):</span>
<span class="value">{{ info.CPU.used_cpu_user }}</span>
</div>
<div class="row">
<span class="title">used_cpu_sys_children(由后台进程消耗的系统CPU):</span>
<span class="value">{{ info.CPU.used_cpu_sys_children }}</span>
</div>
<div class="row">
<span class="title">used_cpu_user_children(由后台进程消耗的用户CPU):</span>
<span class="value">{{ info.CPU.used_cpu_user_children }}</span>
</div>
</el-collapse-item>
</el-collapse>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { reactive, watch, toRefs } from 'vue';
import { reactive, watch, toRefs, ref, nextTick } from 'vue';
import { formatByteSize } from '@/common/utils/format';
import useEcharts from '@/common/echarts/useEcharts';
import tdTheme from '@/common/echarts/theme.json';
const props = defineProps({
visible: {
@@ -164,10 +103,17 @@ const emit = defineEmits(['update:visible', 'close'])
const state = reactive({
dialogVisible: false,
memInfo: {} as any,
Keyspace: [] as any[],
});
let memChart: any = null;
let memRef = ref(null);
const {
dialogVisible,
Keyspace,
} = toRefs(state)
watch(
@@ -176,6 +122,90 @@ watch(
state.dialogVisible = val;
}
);
watch(
() => props.info,
(info: any) => {
state.memInfo = info['Memory'];
if (state.memInfo) {
initCharts();
}
if (info['Keyspace']) {
let arr = [];
for (let k in info['Keyspace']) {
let data = { db: k }
let d = info['Keyspace'][k].split(',')
for (let f of d) {
let v = f.split('=')
data[v[0]] = v[1]
}
arr.push(data)
}
state.Keyspace = arr
}
}
);
const initCharts = () => {
nextTick(() => {
initMemStats();
});
}
const initMemStats = () => {
let maxMem = state.memInfo.maxmemory === '0' ? state.memInfo.total_system_memory : state.memInfo.maxmemory
const data = [
{ name: '可用内存:', value: maxMem - state.memInfo.used_memory },
{
name: '已用内存:',
value: state.memInfo.used_memory,
},
];
const option = {
title: {
text: '内存',
x: 'left',
textStyle: { fontSize: 14 },
},
tooltip: {
trigger: 'item',
valueFormatter: formatByteSize,
},
legend: {
top: '15%',
orient: 'vertical',
left: 'left',
textStyle: { fontSize: 12 },
},
series: [
{
name: '内存',
type: 'pie',
radius: ['30%', '60%'], // 饼图内圈和外圈大小
center: ['40%', '50%'], // 饼图位置0: 左右1: 上下
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
fontSize: '15',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: data,
},
],
};
if (memChart) {
memChart.setOption(option, true);
return;
}
memChart = useEcharts(memRef.value, tdTheme, option);
}
const close = () => {
emit('update:visible', false);
@@ -183,7 +213,16 @@ const close = () => {
};
</script>
<style>
<style lang="scss">
.redis-info {
margin-top: 12px;
.info-memory-chart {
width: 360px;
height: 150px;
}
}
.row .title {
font-size: 12px;
color: #8492a6;
@@ -192,6 +231,6 @@ const close = () => {
.row .value {
font-size: 12px;
color: black;
color: var(--el-color-success);
}
</style>

View File

@@ -122,7 +122,6 @@ const cancel = () => {
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
state.key = newValue.key;
state.redisId = newValue.redisId;
state.db = newValue.db;
state.key = newValue.keyInfo;

View File

@@ -3,59 +3,57 @@
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :close-on-click-modal="false"
:destroy-on-close="true" width="38%">
<el-form :model="form" ref="redisForm" :rules="rules" label-width="85px">
<el-form-item prop="tagId" label="标签:" required>
<tag-select v-model:tag-id="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="name" label="名称:" required>
<el-input v-model.trim="form.name" placeholder="请输入redis名称" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="mode" label="mode:" required>
<el-select style="width: 100%" v-model="form.mode" placeholder="请选择模式">
<el-option label="standalone" value="standalone"> </el-option>
<el-option label="cluster" value="cluster"> </el-option>
<el-option label="sentinel" value="sentinel"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host:" required>
<el-input v-model.trim="form.host"
placeholder="请输入host:portsentinel模式为: mastername=sentinelhost:port若集群或哨兵需设多个节点可使用','分割"
auto-complete="off" type="textarea"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码, 修改操作可不填"
autocomplete="new-password"><template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click"
:content="pwd">
<template #reference>
<el-link @click="getPwd" :underline="false" type="primary" class="mr5">原密码</el-link>
</template>
</el-popover>
</template></el-input>
</el-form-item>
<el-form-item prop="db" label="库号:" required>
<el-select @change="changeDb" v-model="dbList" multiple allow-create filterable
placeholder="请选择可操作库号" style="width: 100%">
<el-option v-for="db in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]" :key="db"
:label="db" :value="db" />
</el-select>
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>
<el-form-item prop="enableSshTunnel" label="SSH隧道:">
<el-col :span="3">
<el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1"
:false-label="-1"></el-checkbox>
</el-col>
<el-col :span="2" v-if="form.enableSshTunnel == 1"> 机器: </el-col>
<el-col :span="19" v-if="form.enableSshTunnel == 1">
<el-select style="width: 100%" v-model="form.sshTunnelMachineId" placeholder="请选择SSH隧道机器">
<el-option v-for="item in sshTunnelMachineList as any" :key="item.id"
:label="`${item.ip}:${item.port} [${item.name}]`" :value="item.id">
</el-option>
</el-select>
</el-col>
</el-form-item>
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签:" required>
<tag-select v-model:tag-id="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" />
</el-form-item>
<el-form-item prop="name" label="名称:" required>
<el-input v-model.trim="form.name" placeholder="请输入redis名称" auto-complete="off"></el-input>
</el-form-item>
<el-form-item prop="mode" label="mode:" required>
<el-select style="width: 100%" v-model="form.mode" placeholder="请选择模式">
<el-option label="standalone" value="standalone"> </el-option>
<el-option label="cluster" value="cluster"> </el-option>
<el-option label="sentinel" value="sentinel"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host:" required>
<el-input v-model.trim="form.host"
placeholder="请输入host:portsentinel模式为: mastername=sentinelhost:port若集群或哨兵需设多个节点可使用','分割"
auto-complete="off" type="textarea"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码:">
<el-input type="password" show-password v-model.trim="form.password"
placeholder="请输入密码, 修改操作可不填" autocomplete="new-password"><template
v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click"
:content="pwd">
<template #reference>
<el-link @click="getPwd" :underline="false" type="primary"
class="mr5">原密码</el-link>
</template>
</el-popover>
</template></el-input>
</el-form-item>
<el-form-item prop="db" label="库号:" required>
<el-select @change="changeDb" v-model="dbList" multiple allow-create filterable
placeholder="请选择可操作库号" style="width: 100%">
<el-option v-for="db in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]" :key="db"
:label="db" :value="db" />
</el-select>
</el-form-item>
<el-form-item prop="remark" label="备注:">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="其他配置" name="other">
<el-form-item prop="sshTunnelMachineId" label="SSH隧道:">
<ssh-tunnel-select v-model="form.sshTunnelMachineId" />
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
@@ -71,10 +69,10 @@
<script lang="ts" setup>
import { toRefs, reactive, watch, ref } from 'vue';
import { redisApi } from './api';
import { machineApi } from '../machine/api.ts';
import { ElMessage } from 'element-plus';
import { RsaEncrypt } from '@/common/rsa';
import TagSelect from '../component/TagSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
const props = defineProps({
visible: {
@@ -91,20 +89,6 @@ const props = defineProps({
const emit = defineEmits(['update:visible', 'val-change', 'cancel'])
const rules = {
projectId: [
{
required: true,
message: '请选择项目',
trigger: ['change', 'blur'],
},
],
envId: [
{
required: true,
message: '请选择环境',
trigger: ['change', 'blur'],
},
],
host: [
{
required: true,
@@ -131,7 +115,7 @@ const rules = {
const redisForm: any = ref(null);
const state = reactive({
dialogVisible: false,
sshTunnelMachineList: [],
tabActiveName: 'basic',
form: {
id: null,
tagId: null as any,
@@ -141,13 +125,8 @@ const state = reactive({
host: '',
password: null,
db: '',
project: null,
projectId: null,
envId: null,
env: null,
remark: '',
enableSshTunnel: null,
sshTunnelMachineId: null,
sshTunnelMachineId: -1,
},
dbList: [0],
pwd: '',
@@ -157,7 +136,7 @@ const state = reactive({
const {
dialogVisible,
sshTunnelMachineList,
tabActiveName,
form,
dbList,
pwd,
@@ -169,14 +148,14 @@ watch(props, async (newValue: any) => {
if (!state.dialogVisible) {
return;
}
state.tabActiveName = 'basic';
if (newValue.redis) {
state.form = { ...newValue.redis };
convertDb(state.form.db);
} else {
state.form = { db: '0', enableSshTunnel: -1 } as any;
state.form = { db: '0' } as any;
state.dbList = [];
}
getSshTunnelMachines();
});
const convertDb = (db: string) => {
@@ -190,13 +169,6 @@ const changeDb = () => {
state.form.db = state.dbList.length == 0 ? '' : state.dbList.join(',');
};
const getSshTunnelMachines = async () => {
if (state.form.enableSshTunnel == 1 && state.sshTunnelMachineList.length == 0) {
const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
state.sshTunnelMachineList = res.list;
}
};
const getPwd = async () => {
state.pwd = await redisApi.getRedisPwd.request({ id: state.form.id });
};
@@ -209,6 +181,9 @@ const btnOk = async () => {
ElMessage.error('sentinel模式host需为: mastername=sentinelhost:sentinelport模式');
return;
}
if (!state.form.sshTunnelMachineId || state.form.sshTunnelMachineId <= 0) {
reqForm.sshTunnelMachineId = -1
}
reqForm.password = await RsaEncrypt(reqForm.password);
redisApi.saveRedis.request(reqForm).then(() => {
ElMessage.success('保存成功');
@@ -232,6 +207,4 @@ const cancel = () => {
emit('cancel');
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@@ -20,7 +20,14 @@
</el-radio>
</template>
</el-table-column>
<el-table-column prop="tagPath" label="标签路径" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column prop="tagPath" label="标签路径" min-width="150" show-overflow-tooltip>
<template #default="scope">
<tag-info :tag-path="scope.row.tagPath" />
<span class="ml5">
{{ scope.row.tagPath }}
</span>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" min-width="100"></el-table-column>
<el-table-column prop="host" label="host:port" min-width="150" show-overflow-tooltip> </el-table-column>
<el-table-column prop="mode" label="mode" min-width="100"></el-table-column>
@@ -35,9 +42,6 @@
@click="showInfoDialog(scope.row)" :underline="false">单机信息</el-link>
<el-link @click="onShowClusterInfo(scope.row)" v-if="scope.row.mode === 'cluster'"
type="primary" :underline="false">集群信息</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link @click="openDataOpt(scope.row)" type="success" :underline="false">数据操作</el-link>
</template>
</el-table-column>
</el-table>
@@ -133,7 +137,7 @@
<el-descriptions-item :span="3" label="库">{{ detailDialog.data.db }}</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ detailDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="3" label="SSH隧道">{{ detailDialog.data.enableSshTunnel == 1 ? '是' : '否' }}
<el-descriptions-item :span="3" label="SSH隧道">{{ detailDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(detailDialog.data.createTime) }}
@@ -159,8 +163,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import { tagApi } from '../tag/api.ts';
import RedisEdit from './RedisEdit.vue';
import { dateFormat } from '@/common/utils/date';
import { store } from '@/store';
import router from '@/router';
import TagInfo from '../component/TagInfo.vue';
const state = reactive({
tags: [],
@@ -258,7 +261,7 @@ const showInfoDialog = async (redis: any) => {
}
const res = await redisApi.redisInfo.request({ id: redis.id, host });
state.infoDialog.info = res;
state.infoDialog.title = `'${host}' info`;
state.infoDialog.title = `[${redis.name || host}] redis信息`;
state.infoDialog.visible = true;
};
@@ -296,21 +299,6 @@ const valChange = () => {
state.currentData = null;
search();
};
// 打开redis数据操作页
const openDataOpt = (row: any) => {
const { tagPath, id, db } = row;
// 判断db是否发生改变
let oldDbId = store.state.redisDbOptInfo.dbOptInfo.dbId;
if (oldDbId !== id) {
let params = {
tagPath,
dbId: id,
db
}
store.dispatch('redisDbOptInfo/setRedisDbOptInfo', params);
}
router.push({ name: 'DataOperation' });
}
</script>
<style>

View File

@@ -106,7 +106,6 @@ const cancel = () => {
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
state.key = newValue.key;
state.redisId = newValue.redisId;
state.db = newValue.db;
state.key = newValue.keyInfo;

View File

@@ -120,7 +120,6 @@ watch(
watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible;
state.key = newValue.key;
state.redisId = newValue.redisId;
state.db = newValue.db;
state.key = newValue.keyInfo;

View File

@@ -2,8 +2,6 @@
<div class="role-list">
<el-card>
<el-button v-auth="'team:save'" type="primary" icon="plus" @click="showSaveTeamDialog(false)">添加</el-button>
<el-button v-auth="'team:save'" :disabled="!chooseId" @click="showSaveTeamDialog(chooseData)" type="primary"
icon="edit">编辑</el-button>
<el-button v-auth="'team:del'" :disabled="!chooseId" @click="deleteTeam(chooseData)" type="danger"
icon="delete">删除</el-button>
@@ -33,6 +31,8 @@
<el-link @click.prevent="showMembers(scope.row)" :underline="false" type="primary">成员</el-link>
<el-divider direction="vertical" border-style="dashed" />
<el-link @click.prevent="showTags(scope.row)" :underline="false" type="success">标签</el-link>
<el-divider v-auth="'team:save'" direction="vertical" border-style="dashed" />
<el-link v-auth="'team:save'" @click.prevent="showSaveTeamDialog(scope.row)" :underline="false" type="warning">编辑</el-link>
</template>
</el-table-column>
</el-table>
@@ -63,7 +63,7 @@
<el-dialog width="500px" :title="showTagDialog.title" :before-close="closeTagDialog"
v-model="showTagDialog.visible">
<el-form label-width="70px">
<el-form-item prop="project" label="标签:">
<el-form-item prop="tag" label="标签:">
<el-tree-select ref="tagTreeRef" style="width: 100%" v-model="showTagDialog.tagTreeTeams"
:data="showTagDialog.tags" :default-expanded-keys="showTagDialog.tagTreeTeams" multiple
:render-after-expand="true" show-checkbox check-strictly node-key="id"

View File

@@ -2,6 +2,7 @@ import Api from '@/common/Api';
export const tagApi = {
getAccountTags: Api.create("/tag-trees/account-has", 'get'),
listByQuery: Api.create("/tag-trees/query", 'get'),
getTagTrees: Api.create("/tag-trees", 'get'),
saveTagTree: Api.create("/tag-trees", 'post'),
delTagTree: Api.create("/tag-trees/{id}", 'delete'),
@@ -11,8 +12,8 @@ export const tagApi = {
delTeam: Api.create("/teams/{id}", 'delete'),
getTeamMem: Api.create("/teams/{teamId}/members", 'get'),
saveTeamMem: Api.create("/teams/{teamId}/members", 'post'),
delTeamMem: Api.create("/teams/{teamId}/members/{accountId}", 'delete'),
saveTeamMem: Api.create("/teams/{teamId}/members", 'post'),
delTeamMem: Api.create("/teams/{teamId}/members/{accountId}", 'delete'),
getTeamTagIds: Api.create("/teams/{teamId}/tags", 'get'),
saveTeamTags: Api.create("/teams/{teamId}/tags", 'post'),

View File

@@ -79,9 +79,11 @@
</template>
</el-table-column>
</el-table>
<el-pagination @current-change="getMsgs" 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 type="flex" class="mt5" justify="center">
<el-pagination small @current-change="getMsgs" 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>
<!-- 营销推荐 -->

View File

@@ -40,4 +40,4 @@ export const configApi = {
export const logApi = {
list: Api.create("/syslogs", "get")
}
}

View File

@@ -18,19 +18,27 @@
<el-col :span="5">
<el-input v-model="param.model" placeholder="model"></el-input>
</el-col>
<el-divider :span="1" direction="vertical" border-style="dashed" />
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.name" placeholder="字段名"></el-input>
</el-col>
<el-divider :span="1" direction="vertical" border-style="dashed" />
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.placeholder" placeholder="字段说明"></el-input>
</el-col>
<el-divider :span="1" direction="vertical" border-style="dashed" />
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="4">
<el-input v-model="param.options" placeholder="可选值 ,分割"></el-input>
</el-col>
<el-divider :span="1" direction="vertical" border-style="dashed" />
<span :span="1">
<el-divider direction="vertical" border-style="dashed" />
</span>
<el-col :span="2">
<el-button @click="onDeleteParam(index)" size="small" type="danger">删除</el-button>
</el-col>
@@ -143,6 +151,4 @@ const btnOk = async () => {
});
};
</script>
<style lang="scss">
</style>
<style lang="scss"></style>

View File

@@ -13,16 +13,16 @@
</el-radio>
</template>
</el-table-column>
<el-table-column prop="name" label="配置项"></el-table-column>
<el-table-column prop="key" label="配置key"></el-table-column>
<el-table-column prop="value" label="配置值" min-width="100px" show-overflow-tooltip></el-table-column>
<el-table-column prop="name" label="配置项" min-width="100px" show-overflow-tooltip></el-table-column>
<el-table-column prop="key" label="配置key" min-width="100px"></el-table-column>
<el-table-column prop="value" label="配置值" show-overflow-tooltip></el-table-column>
<el-table-column prop="remark" label="备注" min-width="100px" show-overflow-tooltip></el-table-column>
<el-table-column prop="updateTime" label="更新时间" min-width="100px">
<template #default="scope">
{{ dateFormat(scope.row.createTime) }}
{{ dateFormat(scope.row.updateTime) }}
</template>
</el-table-column>
<el-table-column prop="modifier" label="修改者" show-overflow-tooltip></el-table-column>
<el-table-column prop="modifier" label="修改者" min-width="60px" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" min-width="50" fixed="right">
<template #default="scope">
<el-link :disabled="scope.row.status == -1" type="warning"
@@ -134,6 +134,8 @@ const showSetConfigDialog = (row: any) => {
if (row.value) {
state.paramsDialog.params = JSON.parse(row.value);
}
} else {
state.paramsDialog.params = row.value;
}
} else {
state.paramsDialog.params = row.value;

File diff suppressed because it is too large Load Diff

26
server/Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
FROM golang:1.19-alpine3.16 as builder
ENV GOPROXY https://goproxy.cn
WORKDIR /mayfly
COPY go.mod go.mod
COPY go.sum go.sum
RUN go mod download
# Copy the go source for building server
COPY . .
# Build
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
go build -a \
-o mayfly-go main.go
FROM alpine:3.16
RUN apk add --no-cache ca-certificates bash expat
WORKDIR /mayfly
COPY --from=builder /mayfly/config.yml /mayfly/config.yml
COPY --from=builder /mayfly/mayfly-go /usr/local/bin/mayfly-go
CMD ["mayfly-go"]

View File

@@ -21,12 +21,18 @@ mysql:
host: localhost:3306
username: root
password: 111049
db-name: mayfly-go
db-name: mayfly_go
config: charset=utf8&loc=Local&parseTime=true
max-idle-conns: 5
# 若同时部署多台机器则需要配置redis信息用于缓存权限码、验证码、公私钥等
# redis:
# host: localhost
# port: 6379
# passsord:
# db: 0
log:
# 日志等级, trace, debug, info, warn, error, fatal
level: info
# file:
# path: ./
# name: mayfly.log
# name: mayfly-go.log

View File

@@ -0,0 +1,33 @@
version: "3.9"
services:
mysql:
image: "mysql:5.7"
container_name: mayfly-go-mysql
environment:
MYSQL_ROOT_PASSWORD: 111049
MYSQL_DATABASE: mayfly-go
TZ: Asia/Shanghai
volumes:
- ./docs/docker-compose/mysql/data/mydir:/mydir
- ./docs/docker-compose/mysql/data/datadir:/var/lib/mysql
# 在宿主机编写 /apps/mysql/conf/my.cnf
- ./docs/docker-compose/mysql/my.cnf:/etc/my.cnf
# 数据库还原目录 可将需要还原的sql文件放在这里
- ./docs/docker-compose/mysql/init:/docker-entrypoint-initdb.d
restart: always
server:
image: mayfly-go:v1.3.1
build:
context: .
dockerfile: Dockerfile
container_name: mayfly-go-server
ports:
- "8888:8888"
environment:
TZ: Asia/Shanghai
WAIT_HOSTS: mysql:3306
depends_on:
- mysql
restart: always

View File

@@ -0,0 +1,808 @@
/*
Navicat Premium Data Transfer
Source Server Type : MySQL
Source Server Version : 50730
Source Schema : mayfly-go
Target Server Type : MySQL
Target Server Version : 50730
File Encoding : 65001
Date: 18/11/2021 14:33:55
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_db
-- ----------------------------
DROP TABLE IF EXISTS `t_db`;
CREATE TABLE `t_db` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '数据库实例名称',
`host` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`port` int(8) NOT NULL,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据库实例类型(mysql...)',
`database` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '数据库,空格分割多个数据库',
`params` varchar(125) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '其他连接参数',
`network` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`enable_ssh_tunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
`ssh_tunnel_machine_id` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
`remark` varchar(125) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注,描述等',
`tag_id` bigint(20) DEFAULT NULL COMMENT '标签id',
`tag_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '标签路径',
`create_time` datetime DEFAULT NULL,
`creator_id` bigint(20) DEFAULT NULL,
`creator` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`modifier_id` bigint(20) DEFAULT NULL,
`modifier` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_path` (`tag_path`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据库资源信息表';
-- ----------------------------
-- Records of t_db
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for t_db_sql
-- ----------------------------
DROP TABLE IF EXISTS `t_db_sql`;
CREATE TABLE `t_db_sql` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`db_id` bigint(20) NOT NULL COMMENT '数据库实例id',
`db` varchar(125) COLLATE utf8mb4_bin NOT NULL COMMENT '数据库',
`name` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'sql模板名',
`sql` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
`type` tinyint(8) NOT NULL,
`creator_id` bigint(20) NOT NULL,
`creator` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint(20) DEFAULT NULL,
`modifier` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据库sql信息';
-- ----------------------------
-- Records of t_db_sql
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for t_db_sql_exec
-- ----------------------------
DROP TABLE IF EXISTS `t_db_sql_exec`;
CREATE TABLE `t_db_sql_exec` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`db_id` bigint(20) NOT NULL COMMENT '数据库id',
`db` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据库',
`table` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '表名',
`type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'sql类型',
`sql` varchar(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '执行sql',
`old_value` varchar(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '操作前旧值',
`remark` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL,
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`creator_id` bigint(20) NOT NULL,
`update_time` datetime NOT NULL,
`modifier` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`modifier_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='数据库sql执行记录表';
-- ----------------------------
-- Records of t_db_sql_exec
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for t_machine
-- ----------------------------
DROP TABLE IF EXISTS `t_machine`;
CREATE TABLE `t_machine` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`port` int(12) NOT NULL,
`username` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`auth_method` tinyint(2) DEFAULT NULL COMMENT '1.密码登录2.publickey登录',
`password` varchar(3200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`enable_ssh_tunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
`ssh_tunnel_machine_id` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
`enable_recorder` tinyint(2) DEFAULT NULL COMMENT '是否启用终端回放记录',
`status` tinyint(2) NOT NULL COMMENT '状态: 1:启用; -1:禁用',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`tag_id` bigint(20) DEFAULT NULL COMMENT '标签id',
`tag_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '标签路径',
`need_monitor` tinyint(2) DEFAULT NULL,
`create_time` datetime NOT NULL,
`creator` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`creator_id` bigint(32) DEFAULT NULL,
`update_time` datetime NOT NULL,
`modifier` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`modifier_id` bigint(32) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_path` (`tag_path`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='机器信息';
-- ----------------------------
-- Records of t_machine
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for t_machine_file
-- ----------------------------
DROP TABLE IF EXISTS `t_machine_file`;
CREATE TABLE `t_machine_file` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '机器文件配置linux一切皆文件故也可以表示目录',
`machine_id` bigint(20) NOT NULL,
`name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`path` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`type` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '1目录2文件',
`creator_id` bigint(20) unsigned DEFAULT NULL,
`creator` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`modifier_id` bigint(20) unsigned DEFAULT NULL,
`modifier` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`create_time` datetime NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='机器文件';
-- ----------------------------
-- Records of t_machine_file
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for t_machine_monitor
-- ----------------------------
DROP TABLE IF EXISTS `t_machine_monitor`;
CREATE TABLE `t_machine_monitor` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`machine_id` bigint(20) unsigned NOT NULL COMMENT '机器id',
`cpu_rate` float(255,2) DEFAULT NULL,
`mem_rate` float(255,2) DEFAULT NULL,
`sys_load` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Records of t_machine_monitor
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for t_machine_script
-- ----------------------------
DROP TABLE IF EXISTS `t_machine_script`;
CREATE TABLE `t_machine_script` (
`id` bigint(64) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '脚本名',
`machine_id` bigint(64) NOT NULL COMMENT '机器id[0:公共]',
`script` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '脚本内容',
`params` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '脚本入参',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '脚本描述',
`type` tinyint(8) DEFAULT NULL COMMENT '脚本类型[1: 有结果2无结果3实时交互]',
`creator_id` bigint(20) DEFAULT NULL,
`creator` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`modifier_id` bigint(20) DEFAULT NULL,
`modifier` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='机器脚本';
-- ----------------------------
-- Records of t_machine_script
-- ----------------------------
BEGIN;
INSERT INTO `t_machine_script` VALUES (1, 'sys_info', 9999999, '# 获取系统cpu信息\nfunction get_cpu_info() {\n Physical_CPUs=$(grep \"physical id\" /proc/cpuinfo | sort | uniq | wc -l)\n Virt_CPUs=$(grep \"processor\" /proc/cpuinfo | wc -l)\n CPU_Kernels=$(grep \"cores\" /proc/cpuinfo | uniq | awk -F \': \' \'{print $2}\')\n CPU_Type=$(grep \"model name\" /proc/cpuinfo | awk -F \': \' \'{print $2}\' | sort | uniq)\n CPU_Arch=$(uname -m)\n echo -e \'\\n-------------------------- CPU信息 --------------------------\'\n cat <<EOF | column -t\n物理CPU个数: $Physical_CPUs\n逻辑CPU个数: $Virt_CPUs\n每CPU核心数: $CPU_Kernels\nCPU型号: $CPU_Type\nCPU架构: $CPU_Arch\nEOF\n}\n\n# 获取系统信息\nfunction get_systatus_info() {\n sys_os=$(uname -o)\n sys_release=$(cat /etc/redhat-release)\n sys_kernel=$(uname -r)\n sys_hostname=$(hostname)\n sys_selinux=$(getenforce)\n sys_lang=$(echo $LANG)\n sys_lastreboot=$(who -b | awk \'{print $3,$4}\')\n echo -e \'-------------------------- 系统信息 --------------------------\'\n cat <<EOF | column -t\n系统: ${sys_os}\n发行版本: ${sys_release}\n系统内核: ${sys_kernel}\n主机名: ${sys_hostname}\nselinux状态: ${sys_selinux}\n系统语言: ${sys_lang}\n系统最后重启时间: ${sys_lastreboot}\nEOF\n}\n\nget_systatus_info\n#echo -e \"\\n\"\nget_cpu_info', NULL, '获取系统信息', 1, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO `t_machine_script` VALUES (2, 'get_process_by_name', 9999999, '#! /bin/bash\n# Function: 根据输入的程序的名字过滤出所对应的PID并显示出详细信息如果有几个PID则全部显示\nNAME={{.processName}}\nN=`ps -aux | grep $NAME | grep -v grep | wc -l` ##统计进程总数\nif [ $N -le 0 ];then\n echo \"无该进程!\"\nfi\ni=1\nwhile [ $N -gt 0 ]\ndo\n echo \"进程PID: `ps -aux | grep $NAME | grep -v grep | awk \'NR==\'$i\'{print $0}\'| awk \'{print $2}\'`\"\n echo \"进程命令:`ps -aux | grep $NAME | grep -v grep | awk \'NR==\'$i\'{print $0}\'| awk \'{print $11}\'`\"\n echo \"进程所属用户: `ps -aux | grep $NAME | grep -v grep | awk \'NR==\'$i\'{print $0}\'| awk \'{print $1}\'`\"\n echo \"CPU占用率`ps -aux | grep $NAME | grep -v grep | awk \'NR==\'$i\'{print $0}\'| awk \'{print $3}\'`%\"\n echo \"内存占用率:`ps -aux | grep $NAME | grep -v grep | awk \'NR==\'$i\'{print $0}\'| awk \'{print $4}\'`%\"\n echo \"进程开始运行的时刻:`ps -aux | grep $NAME | grep -v grep | awk \'NR==\'$i\'{print $0}\'| awk \'{print $9}\'`\"\n echo \"进程运行的时间:` ps -aux | grep $NAME | grep -v grep | awk \'NR==\'$i\'{print $0}\'| awk \'{print $11}\'`\"\n echo \"进程状态:`ps -aux | grep $NAME | grep -v grep | awk \'NR==\'$i\'{print $0}\'| awk \'{print $8}\'`\"\n echo \"进程虚拟内存:`ps -aux | grep $NAME | grep -v grep | awk \'NR==\'$i\'{print $0}\'| awk \'{print $5}\'`\"\n echo \"进程共享内存:`ps -aux | grep $NAME | grep -v grep | awk \'NR==\'$i\'{print $0}\'| awk \'{print $6}\'`\"\n echo \"***************************************************************\"\n let N-- i++\ndone', '[{\"name\": \"进程名\",\"model\": \"processName\", \"placeholder\": \"请输入进程名\"}]', '获取进程运行状态', 1, NULL, NULL, 1, 'admin', NULL, '2021-07-12 15:33:41');
INSERT INTO `t_machine_script` VALUES (3, 'sys_run_info', 9999999, '#!/bin/bash\n# 获取要监控的本地服务器IP地址\nIP=`ifconfig | grep inet | grep -vE \'inet6|127.0.0.1\' | awk \'{print $2}\'`\necho \"IP地址\"$IP\n \n# 获取cpu总核数\ncpu_num=`grep -c \"model name\" /proc/cpuinfo`\necho \"cpu总核数\"$cpu_num\n \n# 1、获取CPU利用率\n################################################\n#us 用户空间占用CPU百分比\n#sy 内核空间占用CPU百分比\n#ni 用户进程空间内改变过优先级的进程占用CPU百分比\n#id 空闲CPU百分比\n#wa 等待输入输出的CPU时间百分比\n#hi 硬件中断\n#si 软件中断\n#################################################\n# 获取用户空间占用CPU百分比\ncpu_user=`top -b -n 1 | grep Cpu | awk \'{print $2}\' | cut -f 1 -d \"%\"`\necho \"用户空间占用CPU百分比\"$cpu_user\n \n# 获取内核空间占用CPU百分比\ncpu_system=`top -b -n 1 | grep Cpu | awk \'{print $4}\' | cut -f 1 -d \"%\"`\necho \"内核空间占用CPU百分比\"$cpu_system\n \n# 获取空闲CPU百分比\ncpu_idle=`top -b -n 1 | grep Cpu | awk \'{print $8}\' | cut -f 1 -d \"%\"`\necho \"空闲CPU百分比\"$cpu_idle\n \n# 获取等待输入输出占CPU百分比\ncpu_iowait=`top -b -n 1 | grep Cpu | awk \'{print $10}\' | cut -f 1 -d \"%\"`\necho \"等待输入输出占CPU百分比\"$cpu_iowait\n \n#2、获取CPU上下文切换和中断次数\n# 获取CPU中断次数\ncpu_interrupt=`vmstat -n 1 1 | sed -n 3p | awk \'{print $11}\'`\necho \"CPU中断次数\"$cpu_interrupt\n \n# 获取CPU上下文切换次数\ncpu_context_switch=`vmstat -n 1 1 | sed -n 3p | awk \'{print $12}\'`\necho \"CPU上下文切换次数\"$cpu_context_switch\n \n#3、获取CPU负载信息\n# 获取CPU15分钟前到现在的负载平均值\ncpu_load_15min=`uptime | awk \'{print $11}\' | cut -f 1 -d \',\'`\necho \"CPU 15分钟前到现在的负载平均值\"$cpu_load_15min\n \n# 获取CPU5分钟前到现在的负载平均值\ncpu_load_5min=`uptime | awk \'{print $10}\' | cut -f 1 -d \',\'`\necho \"CPU 5分钟前到现在的负载平均值\"$cpu_load_5min\n \n# 获取CPU1分钟前到现在的负载平均值\ncpu_load_1min=`uptime | awk \'{print $9}\' | cut -f 1 -d \',\'`\necho \"CPU 1分钟前到现在的负载平均值\"$cpu_load_1min\n \n# 获取任务队列(就绪状态等待的进程数)\ncpu_task_length=`vmstat -n 1 1 | sed -n 3p | awk \'{print $1}\'`\necho \"CPU任务队列长度\"$cpu_task_length\n \n#4、获取内存信息\n# 获取物理内存总量\nmem_total=`free -h | grep Mem | awk \'{print $2}\'`\necho \"物理内存总量:\"$mem_total\n \n# 获取操作系统已使用内存总量\nmem_sys_used=`free -h | grep Mem | awk \'{print $3}\'`\necho \"已使用内存总量(操作系统)\"$mem_sys_used\n \n# 获取操作系统未使用内存总量\nmem_sys_free=`free -h | grep Mem | awk \'{print $4}\'`\necho \"剩余内存总量(操作系统)\"$mem_sys_free\n \n# 获取应用程序已使用的内存总量\nmem_user_used=`free | sed -n 3p | awk \'{print $3}\'`\necho \"已使用内存总量(应用程序)\"$mem_user_used\n \n# 获取应用程序未使用内存总量\nmem_user_free=`free | sed -n 3p | awk \'{print $4}\'`\necho \"剩余内存总量(应用程序)\"$mem_user_free\n \n# 获取交换分区总大小\nmem_swap_total=`free | grep Swap | awk \'{print $2}\'`\necho \"交换分区总大小:\"$mem_swap_total\n \n# 获取已使用交换分区大小\nmem_swap_used=`free | grep Swap | awk \'{print $3}\'`\necho \"已使用交换分区大小:\"$mem_swap_used\n \n# 获取剩余交换分区大小\nmem_swap_free=`free | grep Swap | awk \'{print $4}\'`\necho \"剩余交换分区大小:\"$mem_swap_free', NULL, '获取cpu、内存等系统运行状态', 1, NULL, NULL, NULL, NULL, NULL, '2021-04-25 15:07:16');
INSERT INTO `t_machine_script` VALUES (4, 'top', 9999999, 'top', NULL, '实时获取系统运行状态', 3, NULL, NULL, 1, 'admin', NULL, '2021-05-24 15:58:20');
INSERT INTO `t_machine_script` VALUES (18, 'disk-mem', 9999999, 'df -h', '', '磁盘空间查看', 1, 1, 'admin', 1, 'admin', '2021-07-16 10:49:53', '2021-07-16 10:49:53');
COMMIT;
-- ----------------------------
-- Table structure for t_mongo
-- ----------------------------
DROP TABLE IF EXISTS `t_mongo`;
CREATE TABLE `t_mongo` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '名称',
`uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '连接uri',
`enable_ssh_tunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
`ssh_tunnel_machine_id` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
`tag_id` bigint(20) DEFAULT NULL COMMENT '标签id',
`tag_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '标签路径',
`create_time` datetime NOT NULL,
`creator_id` bigint(20) DEFAULT NULL,
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`modifier_id` bigint(20) DEFAULT NULL,
`modifier` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Records of t_mongo
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for t_redis
-- ----------------------------
DROP TABLE IF EXISTS `t_redis`;
CREATE TABLE `t_redis` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '名称',
`host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`db` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '库号: 多个库用,分割',
`mode` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
`enable_ssh_tunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
`ssh_tunnel_machine_id` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
`remark` varchar(125) COLLATE utf8mb4_bin DEFAULT NULL,
`tag_id` bigint(20) DEFAULT NULL COMMENT '标签id',
`tag_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '标签路径',
`creator` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`creator_id` bigint(32) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`modifier` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`modifier_id` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_tag_path` (`tag_path`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='redis信息';
-- ----------------------------
-- Records of t_redis
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for t_sys_account
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_account`;
CREATE TABLE `t_sys_account` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`status` tinyint(4) DEFAULT NULL,
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
`last_login_ip` varchar(24) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`create_time` datetime NOT NULL,
`creator_id` bigint(255) NOT NULL,
`creator` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint(255) NOT NULL,
`modifier` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='账号信息表';
-- ----------------------------
-- Records of t_sys_account
-- ----------------------------
BEGIN;
INSERT INTO `t_sys_account` VALUES (1, "管理员", 'admin', '$2a$10$w3Wky2U.tinvR7c/s0aKPuwZsIu6pM1/DMJalwBDMbE6niHIxVrrm', 1, '2022-10-26 20:03:48', '::1', '2020-01-01 19:00:00', 1, 'admin', '2020-01-01 19:00:00', 1, 'admin');
COMMIT;
-- ----------------------------
-- Table structure for t_sys_account_role
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_account_role`;
CREATE TABLE `t_sys_account_role` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Id',
`account_id` bigint(20) NOT NULL COMMENT '账号id',
`role_id` bigint(20) NOT NULL COMMENT '角色id',
`creator` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`creator_id` bigint(20) unsigned DEFAULT NULL,
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='账号角色关联表';
-- ----------------------------
-- Records of t_sys_account_role
-- ----------------------------
BEGIN;
INSERT INTO `t_sys_account_role` VALUES (25, 1, 1, 'admin', 1, '2021-05-28 16:21:45');
COMMIT;
-- ----------------------------
-- Table structure for t_sys_config
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_config`;
CREATE TABLE `t_sys_config` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '配置名',
`key` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '配置key',
`params` varchar(500) COLLATE utf8mb4_bin DEFAULT NULL,
`value` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '配置value',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL,
`creator_id` bigint(20) NOT NULL,
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint(20) NOT NULL,
`modifier` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ----------------------------
-- Records of t_sys_config
-- ----------------------------
BEGIN;
INSERT INTO `t_sys_config` VALUES (1, '是否启用登录验证码', 'UseLoginCaptcha', NULL, '1', '1: 启用、0: 不启用', '2022-08-25 22:27:17', 1, 'admin', '2022-08-26 10:26:56', 1, 'admin');
INSERT INTO `t_sys_config` VALUES (2, '是否启用水印', 'UseWartermark', NULL, '1', '1: 启用、0: 不启用', '2022-08-25 23:36:35', 1, 'admin', '2022-08-26 10:02:52', 1, 'admin');
COMMIT;
-- ----------------------------
-- Table structure for t_sys_log
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_log`;
CREATE TABLE `t_sys_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`type` tinyint(4) NOT NULL COMMENT '类型',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '描述',
`req_param` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '请求信息',
`resp` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '响应信息',
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '调用者',
`creator_id` bigint(20) NOT NULL COMMENT '调用者id',
`create_time` datetime NOT NULL COMMENT '操作时间',
PRIMARY KEY (`id`),
KEY `idx_creator_id` (`creator_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=58 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='系统操作日志';
-- ----------------------------
-- Records of t_sys_log
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for t_sys_msg
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_msg`;
CREATE TABLE `t_sys_msg` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`type` int(255) DEFAULT NULL,
`msg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`recipient_id` bigint(20) DEFAULT NULL COMMENT '接收人id-1为所有接收',
`creator_id` bigint(20) DEFAULT NULL,
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=91 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='系统消息表';
-- ----------------------------
-- Records of t_sys_msg
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for t_sys_resource
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_resource`;
CREATE TABLE `t_sys_resource` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`pid` int(11) NOT NULL COMMENT '父节点id',
`type` tinyint(255) NOT NULL COMMENT '1菜单路由2资源按钮等',
`status` int(255) NOT NULL COMMENT '状态1:可用,-1:禁用',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '名称',
`code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '菜单路由为path其他为唯一标识',
`weight` int(11) DEFAULT NULL COMMENT '权重顺序',
`meta` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '元数据',
`creator_id` bigint(20) NOT NULL,
`creator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`modifier_id` bigint(20) NOT NULL,
`modifier` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=103 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='资源表';
-- ----------------------------
-- Records of t_sys_resource
-- ----------------------------
BEGIN;
INSERT INTO `t_sys_resource` VALUES (1, 0, 1, 1, '首页', '/home', 1, '{\"component\":\"Home\",\"icon\":\"HomeFilled\",\"isAffix\":true,\"isKeepAlive\":true,\"routeName\":\"Home\"}', 1, 'admin', 1, 'admin', '2021-05-25 16:44:41', '2021-05-27 09:12:56');
INSERT INTO `t_sys_resource` VALUES (2, 0, 1, 1, '机器管理', '/machine', 4, '{\"icon\":\"Monitor\",\"isKeepAlive\":true,\"redirect\":\"machine/list\",\"routeName\":\"Machine\"}', 1, 'admin', 1, 'admin', '2021-05-25 16:48:16', '2022-10-06 14:58:49');
INSERT INTO `t_sys_resource` VALUES (3, 2, 1, 1, '机器列表', 'machines', 2, '{\"component\":\"MachineList\",\"icon\":\"Menu\",\"isKeepAlive\":true,\"routeName\":\"MachineList\"}', 2, 'admin', 1, 'admin', '2021-05-25 16:50:04', '2021-06-30 16:20:08');
INSERT INTO `t_sys_resource` VALUES (4, 0, 1, 1, '系统管理', '/sys', 8, '{\"icon\":\"Setting\",\"isKeepAlive\":true,\"redirect\":\"/sys/resources\",\"routeName\":\"sys\"}', 1, 'admin', 1, 'admin', '2021-05-26 15:20:20', '2022-10-06 14:59:53');
INSERT INTO `t_sys_resource` VALUES (5, 4, 1, 1, '资源管理', 'resources', 3, '{\"component\":\"ResourceList\",\"icon\":\"Menu\",\"isKeepAlive\":true,\"routeName\":\"ResourceList\"}', 1, 'admin', 1, 'admin', '2021-05-26 15:23:07', '2021-06-08 11:27:55');
INSERT INTO `t_sys_resource` VALUES (11, 4, 1, 1, '角色管理', 'roles', 2, '{\"component\":\"RoleList\",\"icon\":\"Menu\",\"isKeepAlive\":true,\"routeName\":\"RoleList\"}', 1, 'admin', 1, 'admin', '2021-05-27 11:15:35', '2021-06-03 09:59:41');
INSERT INTO `t_sys_resource` VALUES (12, 3, 2, 1, '机器终端按钮', 'machine:terminal', 4, '', 1, 'admin', 1, 'admin', '2021-05-28 14:06:02', '2021-05-31 17:47:59');
INSERT INTO `t_sys_resource` VALUES (14, 4, 1, 1, '账号管理', 'accounts', 1, '{\"component\":\"AccountList\",\"icon\":\"Menu\",\"isKeepAlive\":true,\"routeName\":\"AccountList\"}', 1, 'admin', 1, 'admin', '2021-05-28 14:56:25', '2021-06-03 09:39:22');
INSERT INTO `t_sys_resource` VALUES (15, 3, 2, 1, '文件管理按钮', 'machine:file', 5, NULL, 1, 'admin', 1, 'admin', '2021-05-31 17:44:37', '2021-05-31 17:48:07');
INSERT INTO `t_sys_resource` VALUES (16, 3, 2, 1, '机器添加按钮', 'machine:add', 1, NULL, 1, 'admin', 1, 'admin', '2021-05-31 17:46:11', '2021-05-31 19:34:15');
INSERT INTO `t_sys_resource` VALUES (17, 3, 2, 1, '机器编辑按钮', 'machine:update', 2, NULL, 1, 'admin', 1, 'admin', '2021-05-31 17:46:23', '2021-05-31 19:34:18');
INSERT INTO `t_sys_resource` VALUES (18, 3, 2, 1, '机器删除按钮', 'machine:del', 3, NULL, 1, 'admin', 1, 'admin', '2021-05-31 17:46:36', '2021-05-31 19:34:17');
INSERT INTO `t_sys_resource` VALUES (19, 14, 2, 1, '角色分配按钮', 'account:saveRoles', 1, NULL, 1, 'admin', 1, 'admin', '2021-05-31 17:50:51', '2021-05-31 19:19:30');
INSERT INTO `t_sys_resource` VALUES (20, 11, 2, 1, '分配菜单&权限按钮', 'role:saveResources', 1, NULL, 1, 'admin', 1, 'admin', '2021-05-31 17:51:41', '2021-05-31 19:33:37');
INSERT INTO `t_sys_resource` VALUES (21, 14, 2, 1, '账号删除按钮', 'account:del', 2, 'null', 1, 'admin', 1, 'admin', '2021-05-31 18:02:01', '2021-06-10 17:12:17');
INSERT INTO `t_sys_resource` VALUES (22, 11, 2, 1, '角色删除按钮', 'role:del', 2, NULL, 1, 'admin', 1, 'admin', '2021-05-31 18:02:29', '2021-05-31 19:33:38');
INSERT INTO `t_sys_resource` VALUES (23, 11, 2, 1, '角色新增按钮', 'role:add', 3, NULL, 1, 'admin', 1, 'admin', '2021-05-31 18:02:44', '2021-05-31 19:33:39');
INSERT INTO `t_sys_resource` VALUES (24, 11, 2, 1, '角色编辑按钮', 'role:update', 4, NULL, 1, 'admin', 1, 'admin', '2021-05-31 18:02:57', '2021-05-31 19:33:40');
INSERT INTO `t_sys_resource` VALUES (25, 5, 2, 1, '资源新增按钮', 'resource:add', 1, NULL, 1, 'admin', 1, 'admin', '2021-05-31 18:03:33', '2021-05-31 19:31:47');
INSERT INTO `t_sys_resource` VALUES (26, 5, 2, 1, '资源删除按钮', 'resource:delete', 2, NULL, 1, 'admin', 1, 'admin', '2021-05-31 18:03:47', '2021-05-31 19:29:40');
INSERT INTO `t_sys_resource` VALUES (27, 5, 2, 1, '资源编辑按钮', 'resource:update', 3, NULL, 1, 'admin', 1, 'admin', '2021-05-31 18:04:03', '2021-05-31 19:29:40');
INSERT INTO `t_sys_resource` VALUES (28, 5, 2, 1, '资源禁用启用按钮', 'resource:changeStatus', 4, NULL, 1, 'admin', 1, 'admin', '2021-05-31 18:04:33', '2021-05-31 18:04:33');
INSERT INTO `t_sys_resource` VALUES (29, 14, 2, 1, '账号添加按钮', 'account:add', 3, NULL, 1, 'admin', 1, 'admin', '2021-05-31 19:23:42', '2021-05-31 19:23:42');
INSERT INTO `t_sys_resource` VALUES (30, 14, 2, 1, '账号编辑修改按钮', 'account:update', 4, NULL, 1, 'admin', 1, 'admin', '2021-05-31 19:23:58', '2021-05-31 19:23:58');
INSERT INTO `t_sys_resource` VALUES (31, 14, 2, 1, '账号管理基本权限', 'account', 0, 'null', 1, 'admin', 1, 'admin', '2021-05-31 21:25:06', '2021-06-22 11:20:34');
INSERT INTO `t_sys_resource` VALUES (32, 5, 2, 1, '资源管理基本权限', 'resource', 0, NULL, 1, 'admin', 1, 'admin', '2021-05-31 21:25:25', '2021-05-31 21:25:25');
INSERT INTO `t_sys_resource` VALUES (33, 11, 2, 1, '角色管理基本权限', 'role', 0, NULL, 1, 'admin', 1, 'admin', '2021-05-31 21:25:40', '2021-05-31 21:25:40');
INSERT INTO `t_sys_resource` VALUES (34, 14, 2, 1, '账号启用禁用按钮', 'account:changeStatus', 5, NULL, 1, 'admin', 1, 'admin', '2021-05-31 21:29:48', '2021-05-31 21:29:48');
INSERT INTO `t_sys_resource` VALUES (36, 0, 1, 1, 'DBMS', '/dbms', 5, '{\"icon\":\"Grid\",\"isKeepAlive\":true,\"routeName\":\"DBMS\"}', 1, 'admin', 1, 'admin', '2021-06-01 14:01:33', '2022-10-06 15:00:40');
INSERT INTO `t_sys_resource` VALUES (37, 3, 2, 1, '添加文件配置', 'machine:addFile', 6, 'null', 1, 'admin', 1, 'admin', '2021-06-01 19:54:23', '2021-06-01 19:54:23');
INSERT INTO `t_sys_resource` VALUES (38, 36, 1, 1, '数据操作', 'sql-exec', 1, '{\"component\":\"SqlExec\",\"icon\":\"Search\",\"isKeepAlive\":true,\"routeName\":\"SqlExec\"}', 1, 'admin', 1, 'admin', '2021-06-03 09:09:29', '2021-11-08 09:59:26');
INSERT INTO `t_sys_resource` VALUES (39, 0, 1, 1, '个人中心', '/personal', 2, '{\"component\":\"Personal\",\"icon\":\"UserFilled\",\"isHide\":true,\"isKeepAlive\":true,\"routeName\":\"Personal\"}', 1, 'admin', 1, 'admin', '2021-06-03 14:25:35', '2021-09-10 09:18:46');
INSERT INTO `t_sys_resource` VALUES (40, 3, 2, 1, '文件管理-新增按钮', 'machine:file:add', 7, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:06:26', '2021-06-08 11:12:28');
INSERT INTO `t_sys_resource` VALUES (41, 3, 2, 1, '文件管理-删除按钮', 'machine:file:del', 8, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:06:49', '2021-06-08 11:06:49');
INSERT INTO `t_sys_resource` VALUES (42, 3, 2, 1, '文件管理-写入or下载文件权限', 'machine:file:write', 9, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:07:27', '2021-06-08 11:07:27');
INSERT INTO `t_sys_resource` VALUES (43, 3, 2, 1, '文件管理-文件上传按钮', 'machine:file:upload', 10, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:07:42', '2021-06-08 11:07:42');
INSERT INTO `t_sys_resource` VALUES (44, 3, 2, 1, '文件管理-删除文件按钮', 'machine:file:rm', 11, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:08:12', '2021-06-08 11:08:12');
INSERT INTO `t_sys_resource` VALUES (45, 3, 2, 1, '脚本管理-保存脚本按钮', 'machine:script:save', 12, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:01', '2021-06-08 11:09:01');
INSERT INTO `t_sys_resource` VALUES (46, 3, 2, 1, '脚本管理-删除按钮', 'machine:script:del', 13, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:27', '2021-06-08 11:09:27');
INSERT INTO `t_sys_resource` VALUES (47, 3, 2, 1, '脚本管理-执行按钮', 'machine:script:run', 14, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:50', '2021-06-08 11:09:50');
INSERT INTO `t_sys_resource` VALUES (49, 36, 1, 1, '数据库管理', 'dbs', 2, '{\"component\":\"DbList\",\"icon\":\"Menu\",\"isKeepAlive\":true,\"routeName\":\"DbList\"}', 1, 'admin', 1, 'admin', '2021-07-07 15:13:55', '2021-07-07 15:13:55');
INSERT INTO `t_sys_resource` VALUES (54, 49, 2, 1, '数据库保存', 'db:save', 1, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:36', '2021-07-08 17:31:05');
INSERT INTO `t_sys_resource` VALUES (55, 49, 2, 1, '数据库删除', 'db:del', 2, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:48', '2021-07-08 17:30:48');
INSERT INTO `t_sys_resource` VALUES (57, 3, 2, 1, '基本权限', 'machine', 0, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:48:02', '2021-07-09 10:48:02');
INSERT INTO `t_sys_resource` VALUES (58, 49, 2, 1, '基本权限', 'db', 0, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:48:22', '2021-07-09 10:48:22');
INSERT INTO `t_sys_resource` VALUES (59, 38, 2, 1, '基本权限', 'db:exec', 1, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:50:13', '2021-07-09 10:50:13');
INSERT INTO `t_sys_resource` VALUES (60, 0, 1, 1, 'Redis', '/redis', 6, '{\"icon\":\"Menu\",\"isKeepAlive\":true,\"routeName\":\"RDS\"}', 1, 'admin', 1, 'admin', '2021-07-19 20:15:41', '2022-10-06 15:01:29');
INSERT INTO `t_sys_resource` VALUES (61, 60, 1, 1, '数据操作', 'data-operation', 1, '{\"component\":\"DataOperation\",\"icon\":\"Search\",\"isKeepAlive\":true,\"routeName\":\"DataOperation\"}', 1, 'admin', 1, 'admin', '2021-07-19 20:17:29', '2021-07-20 10:45:28');
INSERT INTO `t_sys_resource` VALUES (62, 61, 2, 1, '基本权限', 'redis:data', 1, 'null', 1, 'admin', 1, 'admin', '2021-07-19 20:18:54', '2021-07-19 20:18:54');
INSERT INTO `t_sys_resource` VALUES (63, 60, 1, 1, 'redis管理', 'manage', 2, '{\"component\":\"RedisList\",\"icon\":\"Menu\",\"isKeepAlive\":true,\"routeName\":\"RedisList\"}', 1, 'admin', 1, 'admin', '2021-07-20 10:48:04', '2021-07-20 10:48:04');
INSERT INTO `t_sys_resource` VALUES (64, 63, 2, 1, '基本权限', 'redis:manage', 1, 'null', 1, 'admin', 1, 'admin', '2021-07-20 10:48:26', '2021-07-20 10:48:26');
INSERT INTO `t_sys_resource` VALUES (71, 61, 2, 1, '数据保存', 'redis:data:save', 6, 'null', 1, 'admin', 1, 'admin', '2021-08-17 11:20:37', '2021-08-17 11:20:37');
INSERT INTO `t_sys_resource` VALUES (72, 3, 2, 1, '终止进程', 'machine:killprocess', 6, 'null', 1, 'admin', 1, 'admin', '2021-08-17 11:20:37', '2021-08-17 11:20:37');
INSERT INTO `t_sys_resource` VALUES (79, 0, 1, 1, 'Mongo', '/mongo', 7, '{\"icon\":\"Document\",\"isKeepAlive\":true,\"routeName\":\"Mongo\"}', 1, 'admin', 1, 'admin', '2022-05-13 14:00:41', '2022-10-06 15:01:34');
INSERT INTO `t_sys_resource` VALUES (80, 79, 1, 1, '数据操作', 'mongo-data-operation', 1, '{\"component\":\"MongoDataOp\",\"icon\":\"Document\",\"isKeepAlive\":true,\"routeName\":\"MongoDataOp\"}', 1, 'admin', 1, 'admin', '2022-05-13 14:03:58', '2022-05-14 20:16:07');
INSERT INTO `t_sys_resource` VALUES (81, 80, 2, 1, '基本权限', 'mongo:base', 1, 'null', 1, 'admin', 1, 'admin', '2022-05-13 14:04:16', '2022-05-13 14:04:16');
INSERT INTO `t_sys_resource` VALUES (82, 79, 1, 1, 'Mongo管理', 'mongo-manage', 2, '{\"component\":\"MongoList\",\"icon\":\"Menu\",\"isKeepAlive\":true,\"routeName\":\"MongoList\"}', 1, 'admin', 1, 'admin', '2022-05-16 18:13:06', '2022-05-16 18:13:06');
INSERT INTO `t_sys_resource` VALUES (83, 82, 2, 1, '基本权限', 'mongo:manage:base', 1, 'null', 1, 'admin', 1, 'admin', '2022-05-16 18:13:25', '2022-05-16 18:13:25');
INSERT INTO `t_sys_resource` VALUES (84, 4, 1, 1, '操作日志', 'syslogs', 4, '{\"component\":\"SyslogList\",\"icon\":\"Tickets\",\"routeName\":\"SyslogList\"}', 1, 'admin', 1, 'admin', '2022-07-13 19:57:07', '2022-07-13 22:58:19');
INSERT INTO `t_sys_resource` VALUES (85, 84, 2, 1, '操作日志基本权限', 'syslog', 1, 'null', 1, 'admin', 1, 'admin', '2022-07-13 19:57:55', '2022-07-13 19:57:55');
INSERT INTO `t_sys_resource` VALUES (87, 4, 1, 1, '系统配置', 'configs', 5, '{\"component\":\"ConfigList\",\"icon\":\"Setting\",\"isKeepAlive\":true,\"routeName\":\"ConfigList\"}', 1, 'admin', 1, 'admin', '2022-08-25 22:18:55', '2022-08-25 22:19:18');
INSERT INTO `t_sys_resource` VALUES (88, 87, 2, 1, '基本权限', 'config:base', 1, 'null', 1, 'admin', 1, 'admin', '2022-08-25 22:19:35', '2022-08-25 22:19:35');
INSERT INTO `t_sys_resource` VALUES (93, 0, 1, 1, '标签管理', '/tag', 3, '{\"icon\":\"CollectionTag\",\"isKeepAlive\":true,\"routeName\":\"Tag\"}', 1, 'admin', 1, 'admin', '2022-10-24 15:18:40', '2022-10-24 15:24:29');
INSERT INTO `t_sys_resource` VALUES (94, 93, 1, 1, '标签树', 'tag-trees', 1, '{\"component\":\"TagTreeList\",\"icon\":\"CollectionTag\",\"isKeepAlive\":true,\"routeName\":\"TagTreeList\"}', 1, 'admin', 1, 'admin', '2022-10-24 15:19:40', '2022-10-24 15:28:07');
INSERT INTO `t_sys_resource` VALUES (95, 93, 1, 1, '团队管理', 'teams', 2, '{\"component\":\"TeamList\",\"icon\":\"UserFilled\",\"isKeepAlive\":true,\"routeName\":\"TeamList\"}', 1, 'admin', 1, 'admin', '2022-10-24 15:20:09', '2022-10-24 15:24:01');
INSERT INTO `t_sys_resource` VALUES (96, 94, 2, 1, '保存标签', 'tag:save', 1, 'null', 1, 'admin', 1, 'admin', '2022-10-24 15:20:40', '2022-10-26 13:58:36');
INSERT INTO `t_sys_resource` VALUES (97, 95, 2, 1, '保存团队', 'team:save', 1, 'null', 1, 'admin', 1, 'admin', '2022-10-24 15:20:57', '2022-10-26 13:58:56');
INSERT INTO `t_sys_resource` VALUES (98, 94, 2, 1, '删除标签', 'tag:del', 2, 'null', 1, 'admin', 1, 'admin', '2022-10-26 13:58:47', '2022-10-26 13:58:47');
INSERT INTO `t_sys_resource` VALUES (99, 95, 2, 1, '删除团队', 'team:del', 2, 'null', 1, 'admin', 1, 'admin', '2022-10-26 13:59:06', '2022-10-26 13:59:06');
INSERT INTO `t_sys_resource` VALUES (100, 95, 2, 1, '新增团队成员', 'team:member:save', 3, 'null', 1, 'admin', 1, 'admin', '2022-10-26 13:59:27', '2022-10-26 13:59:27');
INSERT INTO `t_sys_resource` VALUES (101, 95, 2, 1, '移除团队成员', 'team:member:del', 4, 'null', 1, 'admin', 1, 'admin', '2022-10-26 13:59:43', '2022-10-26 13:59:43');
INSERT INTO `t_sys_resource` VALUES (102, 95, 2, 1, '保存团队标签', 'team:tag:save', 5, 'null', 1, 'admin', 1, 'admin', '2022-10-26 13:59:57', '2022-10-26 13:59:57');
COMMIT;
-- ----------------------------
-- Table structure for t_sys_role
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_role`;
CREATE TABLE `t_sys_role` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '角色code',
`status` tinyint(255) DEFAULT NULL,
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`type` tinyint(2) NOT NULL COMMENT '类型1:公共角色2:特殊角色',
`create_time` datetime DEFAULT NULL,
`creator_id` bigint(20) DEFAULT NULL,
`creator` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`modifier_id` bigint(20) DEFAULT NULL,
`modifier` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='角色表';
-- ----------------------------
-- Records of t_sys_role
-- ----------------------------
BEGIN;
INSERT INTO `t_sys_role` VALUES (1, '超级管理员', 'SUPBER_ADMIN', 1, '权限超级大,拥有所有权限', 2, '2021-05-27 14:09:50', 1, 'admin', '2021-05-28 10:26:28', 1, 'admin');
INSERT INTO `t_sys_role` VALUES (6, '普通管理员', 'ADMIN', 1, '只拥有部分管理权限', 2, '2021-05-28 15:55:36', 1, 'admin', '2021-05-28 15:55:36', 1, 'admin');
INSERT INTO `t_sys_role` VALUES (7, '公共角色', 'COMMON', 1, '所有账号基础角色', 1, '2021-07-06 15:05:47', 1, 'admin', '2021-07-06 15:05:47', 1, 'admin');
INSERT INTO `t_sys_role` VALUES (8, '开发', 'DEV', 1, '研发人员', 0, '2021-07-09 10:46:10', 1, 'admin', '2021-07-09 10:46:10', 1, 'admin');
COMMIT;
-- ----------------------------
-- Table structure for t_sys_role_resource
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_role_resource`;
CREATE TABLE `t_sys_role_resource` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) NOT NULL,
`resource_id` bigint(20) NOT NULL,
`creator_id` bigint(20) unsigned DEFAULT NULL,
`creator` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=526 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='角色资源关联表';
-- ----------------------------
-- Records of t_sys_role_resource
-- ----------------------------
BEGIN;
INSERT INTO `t_sys_role_resource` VALUES (1, 1, 1, 1, 'admin', '2021-05-27 15:07:39');
INSERT INTO `t_sys_role_resource` VALUES (323, 1, 2, 1, 'admin', '2021-05-28 09:04:50');
INSERT INTO `t_sys_role_resource` VALUES (326, 1, 4, 1, 'admin', '2021-05-28 09:04:50');
INSERT INTO `t_sys_role_resource` VALUES (327, 1, 5, 1, 'admin', '2021-05-28 09:04:50');
INSERT INTO `t_sys_role_resource` VALUES (328, 1, 11, 1, 'admin', '2021-05-28 09:04:50');
INSERT INTO `t_sys_role_resource` VALUES (335, 1, 14, 1, 'admin', '2021-05-28 17:42:21');
INSERT INTO `t_sys_role_resource` VALUES (336, 1, 3, 1, 'admin', '2021-05-28 17:42:43');
INSERT INTO `t_sys_role_resource` VALUES (337, 1, 12, 1, 'admin', '2021-05-28 17:42:43');
INSERT INTO `t_sys_role_resource` VALUES (338, 6, 2, 1, 'admin', '2021-05-28 19:19:38');
INSERT INTO `t_sys_role_resource` VALUES (339, 6, 3, 1, 'admin', '2021-05-28 19:19:38');
INSERT INTO `t_sys_role_resource` VALUES (342, 6, 1, 1, 'admin', '2021-05-29 01:31:22');
INSERT INTO `t_sys_role_resource` VALUES (343, 5, 1, 1, 'admin', '2021-05-31 14:05:23');
INSERT INTO `t_sys_role_resource` VALUES (344, 5, 4, 1, 'admin', '2021-05-31 14:05:23');
INSERT INTO `t_sys_role_resource` VALUES (345, 5, 14, 1, 'admin', '2021-05-31 14:05:23');
INSERT INTO `t_sys_role_resource` VALUES (346, 5, 5, 1, 'admin', '2021-05-31 14:05:23');
INSERT INTO `t_sys_role_resource` VALUES (347, 5, 11, 1, 'admin', '2021-05-31 14:05:23');
INSERT INTO `t_sys_role_resource` VALUES (348, 5, 3, 1, 'admin', '2021-05-31 16:33:14');
INSERT INTO `t_sys_role_resource` VALUES (349, 5, 12, 1, 'admin', '2021-05-31 16:33:14');
INSERT INTO `t_sys_role_resource` VALUES (350, 5, 2, 1, 'admin', '2021-05-31 16:33:14');
INSERT INTO `t_sys_role_resource` VALUES (353, 1, 15, 1, 'admin', '2021-05-31 17:48:33');
INSERT INTO `t_sys_role_resource` VALUES (354, 1, 16, 1, 'admin', '2021-05-31 17:48:33');
INSERT INTO `t_sys_role_resource` VALUES (355, 1, 17, 1, 'admin', '2021-05-31 17:48:33');
INSERT INTO `t_sys_role_resource` VALUES (356, 1, 18, 1, 'admin', '2021-05-31 17:48:33');
INSERT INTO `t_sys_role_resource` VALUES (358, 1, 20, 1, 'admin', '2021-05-31 17:52:08');
INSERT INTO `t_sys_role_resource` VALUES (360, 1, 22, 1, 'admin', '2021-05-31 18:05:04');
INSERT INTO `t_sys_role_resource` VALUES (361, 1, 23, 1, 'admin', '2021-05-31 18:05:04');
INSERT INTO `t_sys_role_resource` VALUES (362, 1, 24, 1, 'admin', '2021-05-31 18:05:04');
INSERT INTO `t_sys_role_resource` VALUES (363, 1, 25, 1, 'admin', '2021-05-31 18:05:04');
INSERT INTO `t_sys_role_resource` VALUES (364, 1, 26, 1, 'admin', '2021-05-31 18:05:04');
INSERT INTO `t_sys_role_resource` VALUES (365, 1, 27, 1, 'admin', '2021-05-31 18:05:04');
INSERT INTO `t_sys_role_resource` VALUES (366, 1, 28, 1, 'admin', '2021-05-31 18:05:04');
INSERT INTO `t_sys_role_resource` VALUES (369, 1, 31, 1, 'admin', '2021-05-31 21:25:56');
INSERT INTO `t_sys_role_resource` VALUES (370, 1, 32, 1, 'admin', '2021-05-31 21:25:56');
INSERT INTO `t_sys_role_resource` VALUES (371, 1, 33, 1, 'admin', '2021-05-31 21:25:56');
INSERT INTO `t_sys_role_resource` VALUES (374, 1, 36, 1, 'admin', '2021-06-01 14:01:57');
INSERT INTO `t_sys_role_resource` VALUES (375, 1, 19, 1, 'admin', '2021-06-01 17:34:03');
INSERT INTO `t_sys_role_resource` VALUES (376, 1, 21, 1, 'admin', '2021-06-01 17:34:03');
INSERT INTO `t_sys_role_resource` VALUES (377, 1, 29, 1, 'admin', '2021-06-01 17:34:03');
INSERT INTO `t_sys_role_resource` VALUES (378, 1, 30, 1, 'admin', '2021-06-01 17:34:03');
INSERT INTO `t_sys_role_resource` VALUES (379, 1, 34, 1, 'admin', '2021-06-01 17:34:03');
INSERT INTO `t_sys_role_resource` VALUES (380, 1, 37, 1, 'admin', '2021-06-03 09:09:42');
INSERT INTO `t_sys_role_resource` VALUES (381, 1, 38, 1, 'admin', '2021-06-03 09:09:42');
INSERT INTO `t_sys_role_resource` VALUES (383, 1, 40, 1, 'admin', '2021-06-08 11:21:52');
INSERT INTO `t_sys_role_resource` VALUES (384, 1, 41, 1, 'admin', '2021-06-08 11:21:52');
INSERT INTO `t_sys_role_resource` VALUES (385, 1, 42, 1, 'admin', '2021-06-08 11:21:52');
INSERT INTO `t_sys_role_resource` VALUES (386, 1, 43, 1, 'admin', '2021-06-08 11:21:52');
INSERT INTO `t_sys_role_resource` VALUES (387, 1, 44, 1, 'admin', '2021-06-08 11:21:52');
INSERT INTO `t_sys_role_resource` VALUES (388, 1, 45, 1, 'admin', '2021-06-08 11:21:52');
INSERT INTO `t_sys_role_resource` VALUES (389, 1, 46, 1, 'admin', '2021-06-08 11:21:52');
INSERT INTO `t_sys_role_resource` VALUES (390, 1, 47, 1, 'admin', '2021-06-08 11:21:52');
INSERT INTO `t_sys_role_resource` VALUES (391, 6, 39, 1, 'admin', '2021-06-08 15:10:58');
INSERT INTO `t_sys_role_resource` VALUES (392, 6, 15, 1, 'admin', '2021-06-08 15:10:58');
INSERT INTO `t_sys_role_resource` VALUES (395, 6, 31, 1, 'admin', '2021-06-08 15:10:58');
INSERT INTO `t_sys_role_resource` VALUES (396, 6, 33, 1, 'admin', '2021-06-08 15:10:58');
INSERT INTO `t_sys_role_resource` VALUES (397, 6, 32, 1, 'admin', '2021-06-08 15:10:58');
INSERT INTO `t_sys_role_resource` VALUES (398, 6, 4, 1, 'admin', '2021-06-08 15:10:58');
INSERT INTO `t_sys_role_resource` VALUES (399, 6, 14, 1, 'admin', '2021-06-08 15:10:58');
INSERT INTO `t_sys_role_resource` VALUES (400, 6, 11, 1, 'admin', '2021-06-08 15:10:58');
INSERT INTO `t_sys_role_resource` VALUES (401, 6, 5, 1, 'admin', '2021-06-08 15:10:58');
INSERT INTO `t_sys_role_resource` VALUES (403, 7, 1, 1, 'admin', '2021-07-06 15:07:09');
INSERT INTO `t_sys_role_resource` VALUES (405, 1, 49, 1, 'admin', '2021-07-07 15:14:17');
INSERT INTO `t_sys_role_resource` VALUES (410, 1, 54, 1, 'admin', '2021-07-08 17:32:19');
INSERT INTO `t_sys_role_resource` VALUES (411, 1, 55, 1, 'admin', '2021-07-08 17:32:19');
INSERT INTO `t_sys_role_resource` VALUES (413, 1, 57, 1, 'admin', '2021-07-09 10:48:50');
INSERT INTO `t_sys_role_resource` VALUES (414, 1, 58, 1, 'admin', '2021-07-09 10:48:50');
INSERT INTO `t_sys_role_resource` VALUES (415, 8, 1, 1, 'admin', '2021-07-09 10:49:46');
INSERT INTO `t_sys_role_resource` VALUES (416, 8, 39, 1, 'admin', '2021-07-09 10:49:46');
INSERT INTO `t_sys_role_resource` VALUES (418, 8, 57, 1, 'admin', '2021-07-09 10:49:46');
INSERT INTO `t_sys_role_resource` VALUES (419, 8, 12, 1, 'admin', '2021-07-09 10:49:46');
INSERT INTO `t_sys_role_resource` VALUES (420, 8, 15, 1, 'admin', '2021-07-09 10:49:46');
INSERT INTO `t_sys_role_resource` VALUES (421, 8, 38, 1, 'admin', '2021-07-09 10:49:46');
INSERT INTO `t_sys_role_resource` VALUES (422, 8, 58, 1, 'admin', '2021-07-09 10:49:46');
INSERT INTO `t_sys_role_resource` VALUES (423, 8, 2, 1, 'admin', '2021-07-09 10:49:46');
INSERT INTO `t_sys_role_resource` VALUES (425, 8, 3, 1, 'admin', '2021-07-09 10:49:46');
INSERT INTO `t_sys_role_resource` VALUES (426, 8, 36, 1, 'admin', '2021-07-09 10:49:46');
INSERT INTO `t_sys_role_resource` VALUES (427, 8, 49, 1, 'admin', '2021-07-09 10:49:46');
INSERT INTO `t_sys_role_resource` VALUES (428, 1, 59, 1, 'admin', '2021-07-09 10:50:20');
INSERT INTO `t_sys_role_resource` VALUES (429, 8, 59, 1, 'admin', '2021-07-09 10:50:32');
INSERT INTO `t_sys_role_resource` VALUES (431, 6, 57, 1, 'admin', '2021-07-12 16:44:12');
INSERT INTO `t_sys_role_resource` VALUES (433, 1, 60, 1, 'admin', '2021-07-19 20:19:29');
INSERT INTO `t_sys_role_resource` VALUES (434, 1, 61, 1, 'admin', '2021-07-19 20:19:29');
INSERT INTO `t_sys_role_resource` VALUES (435, 1, 62, 1, 'admin', '2021-07-19 20:19:29');
INSERT INTO `t_sys_role_resource` VALUES (436, 1, 63, 1, 'admin', '2021-07-20 10:48:39');
INSERT INTO `t_sys_role_resource` VALUES (437, 1, 64, 1, 'admin', '2021-07-20 10:48:39');
INSERT INTO `t_sys_role_resource` VALUES (444, 7, 39, 1, 'admin', '2021-09-09 10:10:30');
INSERT INTO `t_sys_role_resource` VALUES (450, 6, 16, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (451, 6, 17, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (452, 6, 18, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (453, 6, 37, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (454, 6, 40, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (455, 6, 41, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (456, 6, 42, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (457, 6, 43, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (458, 6, 44, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (459, 6, 45, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (460, 6, 46, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (461, 6, 47, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (462, 6, 36, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (463, 6, 38, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (464, 6, 59, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (465, 6, 49, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (466, 6, 58, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (467, 6, 54, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (468, 6, 55, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (469, 6, 60, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (470, 6, 61, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (471, 6, 62, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (472, 6, 63, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (473, 6, 64, 1, 'admin', '2021-09-09 15:52:38');
INSERT INTO `t_sys_role_resource` VALUES (479, 6, 19, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (480, 6, 21, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (481, 6, 29, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (482, 6, 30, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (483, 6, 34, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (484, 6, 20, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (485, 6, 22, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (486, 6, 23, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (487, 6, 24, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (488, 6, 25, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (489, 6, 26, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (490, 6, 27, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (491, 6, 28, 1, 'admin', '2021-09-09 15:53:56');
INSERT INTO `t_sys_role_resource` VALUES (492, 8, 42, 1, 'admin', '2021-11-05 15:59:16');
INSERT INTO `t_sys_role_resource` VALUES (493, 8, 43, 1, 'admin', '2021-11-05 15:59:16');
INSERT INTO `t_sys_role_resource` VALUES (494, 8, 47, 1, 'admin', '2021-11-05 15:59:16');
INSERT INTO `t_sys_role_resource` VALUES (495, 8, 60, 1, 'admin', '2021-11-05 15:59:16');
INSERT INTO `t_sys_role_resource` VALUES (496, 8, 61, 1, 'admin', '2021-11-05 15:59:16');
INSERT INTO `t_sys_role_resource` VALUES (497, 8, 62, 1, 'admin', '2021-11-05 15:59:16');
INSERT INTO `t_sys_role_resource` VALUES (498, 8, 63, 1, 'admin', '2021-11-05 15:59:16');
INSERT INTO `t_sys_role_resource` VALUES (499, 8, 64, 1, 'admin', '2021-11-05 15:59:16');
INSERT INTO `t_sys_role_resource` VALUES (500, 1, 72, 1, 'admin', '2022-07-14 11:03:09');
INSERT INTO `t_sys_role_resource` VALUES (501, 1, 71, 1, 'admin', '2022-07-14 11:03:09');
INSERT INTO `t_sys_role_resource` VALUES (502, 1, 79, 1, 'admin', '2022-07-14 11:03:09');
INSERT INTO `t_sys_role_resource` VALUES (503, 1, 80, 1, 'admin', '2022-07-14 11:03:09');
INSERT INTO `t_sys_role_resource` VALUES (504, 1, 81, 1, 'admin', '2022-07-14 11:03:09');
INSERT INTO `t_sys_role_resource` VALUES (505, 1, 82, 1, 'admin', '2022-07-14 11:03:09');
INSERT INTO `t_sys_role_resource` VALUES (506, 1, 83, 1, 'admin', '2022-07-14 11:03:09');
INSERT INTO `t_sys_role_resource` VALUES (507, 1, 84, 1, 'admin', '2022-07-14 11:10:11');
INSERT INTO `t_sys_role_resource` VALUES (508, 1, 85, 1, 'admin', '2022-07-14 11:10:11');
INSERT INTO `t_sys_role_resource` VALUES (510, 1, 87, 1, 'admin', '2022-07-14 11:10:11');
INSERT INTO `t_sys_role_resource` VALUES (511, 1, 88, 1, 'admin', '2022-10-08 10:54:06');
INSERT INTO `t_sys_role_resource` VALUES (512, 8, 80, 1, 'admin', '2022-10-08 10:54:34');
INSERT INTO `t_sys_role_resource` VALUES (513, 8, 81, 1, 'admin', '2022-10-08 10:54:34');
INSERT INTO `t_sys_role_resource` VALUES (515, 8, 79, 1, 'admin', '2022-10-08 10:54:34');
INSERT INTO `t_sys_role_resource` VALUES (516, 1, 93, 1, 'admin', '2022-10-26 20:03:14');
INSERT INTO `t_sys_role_resource` VALUES (517, 1, 94, 1, 'admin', '2022-10-26 20:03:14');
INSERT INTO `t_sys_role_resource` VALUES (518, 1, 96, 1, 'admin', '2022-10-26 20:03:14');
INSERT INTO `t_sys_role_resource` VALUES (519, 1, 98, 1, 'admin', '2022-10-26 20:03:14');
INSERT INTO `t_sys_role_resource` VALUES (520, 1, 95, 1, 'admin', '2022-10-26 20:03:14');
INSERT INTO `t_sys_role_resource` VALUES (521, 1, 97, 1, 'admin', '2022-10-26 20:03:14');
INSERT INTO `t_sys_role_resource` VALUES (522, 1, 99, 1, 'admin', '2022-10-26 20:03:14');
INSERT INTO `t_sys_role_resource` VALUES (523, 1, 100, 1, 'admin', '2022-10-26 20:03:14');
INSERT INTO `t_sys_role_resource` VALUES (524, 1, 101, 1, 'admin', '2022-10-26 20:03:14');
INSERT INTO `t_sys_role_resource` VALUES (525, 1, 102, 1, 'admin', '2022-10-26 20:03:14');
COMMIT;
-- ----------------------------
-- Table structure for t_tag_tree
-- ----------------------------
DROP TABLE IF EXISTS `t_tag_tree`;
CREATE TABLE `t_tag_tree` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`pid` bigint(20) NOT NULL DEFAULT '0',
`code` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '标识符',
`code_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '标识符路径',
`name` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '名称',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`create_time` datetime NOT NULL,
`creator_id` bigint(20) NOT NULL,
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint(20) NOT NULL,
`modifier` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_code_path` (`code_path`(100)) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='标签树';
-- ----------------------------
-- Records of t_tag_tree
-- ----------------------------
BEGIN;
INSERT INTO `t_tag_tree` VALUES (33, 0, 'default', 'default', '默认', '默认标签', '2022-10-26 20:04:19', 1, 'admin', '2022-10-26 20:04:19', 1, 'admin');
COMMIT;
-- ----------------------------
-- Table structure for t_tag_tree_team
-- ----------------------------
DROP TABLE IF EXISTS `t_tag_tree_team`;
CREATE TABLE `t_tag_tree_team` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`tag_id` bigint(20) NOT NULL COMMENT '项目树id',
`tag_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`team_id` bigint(20) NOT NULL COMMENT '团队id',
`create_time` datetime NOT NULL,
`creator_id` bigint(20) NOT NULL,
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint(20) NOT NULL,
`modifier` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_tag_id` (`tag_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='标签树团队关联信息';
-- ----------------------------
-- Records of t_tag_tree_team
-- ----------------------------
BEGIN;
INSERT INTO `t_tag_tree_team` VALUES (31, 33, 'default', 3, '2022-10-26 20:04:45', 1, 'admin', '2022-10-26 20:04:45', 1, 'admin');
COMMIT;
-- ----------------------------
-- Table structure for t_team
-- ----------------------------
DROP TABLE IF EXISTS `t_team`;
CREATE TABLE `t_team` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '名称',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL,
`creator_id` bigint(20) NOT NULL,
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`modifier_id` bigint(20) DEFAULT NULL,
`modifier` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='团队信息';
-- ----------------------------
-- Records of t_team
-- ----------------------------
BEGIN;
INSERT INTO `t_team` VALUES (3, '默认团队', '默认团队', '2022-10-26 20:04:36', 1, 'admin', '2022-10-26 20:04:36', 1, 'admin');
COMMIT;
-- ----------------------------
-- Table structure for t_team_member
-- ----------------------------
DROP TABLE IF EXISTS `t_team_member`;
CREATE TABLE `t_team_member` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`team_id` bigint(20) NOT NULL COMMENT '项目团队id',
`account_id` bigint(20) NOT NULL COMMENT '成员id',
`username` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`create_time` datetime NOT NULL,
`creator_id` bigint(20) NOT NULL,
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`update_time` datetime NOT NULL,
`modifier_id` bigint(20) NOT NULL,
`modifier` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='团队成员表';
-- ----------------------------
-- Records of t_team_member
-- ----------------------------
BEGIN;
INSERT INTO `t_team_member` VALUES (7, 3, 1, 'admin', '2022-10-26 20:04:36', 1, 'admin', '2022-10-26 20:04:36', 1, 'admin');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,11 @@
[mysqld]
user=mysql
default-storage-engine=INNODB
character-set-server=utf8
character-set-client-handshake=FALSE
collation-server=utf8_unicode_ci
init_connect='SET NAMES utf8'
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8

View File

@@ -1,60 +1,64 @@
module mayfly-go
go 1.19
go 1.20
require (
github.com/gin-gonic/gin v1.8.1
github.com/gin-gonic/gin v1.9.0
github.com/go-redis/redis/v8 v8.11.5
github.com/go-sql-driver/mysql v1.6.0
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/go-sql-driver/mysql v1.7.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/gorilla/websocket v1.5.0
github.com/lib/pq v1.10.6
github.com/lib/pq v1.10.7
github.com/mojocn/base64Captcha v1.3.5 //
github.com/pkg/sftp v1.13.5
github.com/robfig/cron/v3 v3.0.1 //
github.com/sirupsen/logrus v1.9.0
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
go.mongodb.org/mongo-driver v1.11.0 // mongo
golang.org/x/crypto v0.3.0 // ssh
go.mongodb.org/mongo-driver v1.11.1 // mongo
golang.org/x/crypto v0.7.0 // ssh
gopkg.in/yaml.v3 v3.0.1
// gorm
gorm.io/driver/mysql v1.4.1
gorm.io/gorm v1.24.0
gorm.io/driver/mysql v1.4.5
gorm.io/gorm v1.24.6
)
require (
github.com/bytedance/sonic v1.8.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.1 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.1 // indirect
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.13.6 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
golang.org/x/net v0.2.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)

View File

@@ -74,7 +74,6 @@ func InitRouter() *gin.Engine {
sys_router.Init(api)
// project_router.Init(api)
tag_router.Init(api)
machine_router.Init(api)
db_router.Init(api)

View File

@@ -2,9 +2,9 @@ package initialize
import (
sysapp "mayfly-go/internal/sys/application"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/req"
)
func InitSaveLogFunc() ctx.SaveLogFunc {
func InitSaveLogFunc() req.SaveLogFunc {
return sysapp.GetSyslogApp().SaveFromReq
}

View File

@@ -2,14 +2,14 @@ package api
import (
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils"
)
type Common struct {
}
func (i *Common) RasPublicKey(rc *ctx.ReqCtx) {
func (i *Common) RasPublicKey(rc *req.Ctx) {
publicKeyStr, err := utils.GetRsaPublicKey()
biz.ErrIsNilAppendErr(err, "rsa生成公私钥失败")
rc.ResData = publicKeyStr

View File

@@ -10,7 +10,7 @@ import (
redisapp "mayfly-go/internal/redis/application"
redisentity "mayfly-go/internal/redis/domain/entity"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/req"
)
type Index struct {
@@ -21,7 +21,7 @@ type Index struct {
MongoApp mongoapp.Mongo
}
func (i *Index) Count(rc *ctx.ReqCtx) {
func (i *Index) Count(rc *req.Ctx) {
accountId := rc.LoginAccount.Id
tagIds := i.TagApp.ListTagIdByAccountId(accountId)

View File

@@ -2,7 +2,7 @@ package router
import (
"mayfly-go/internal/common/api"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
@@ -13,7 +13,7 @@ func InitCommonRouter(router *gin.RouterGroup) {
{
// 获取公钥
common.GET("public-key", func(g *gin.Context) {
ctx.NewReqCtxWithGin(g).
req.NewCtxWithGin(g).
WithNeedToken(false).
Handle(c.RasPublicKey)
})

View File

@@ -7,7 +7,7 @@ import (
mongoapp "mayfly-go/internal/mongo/application"
redisapp "mayfly-go/internal/redis/application"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
@@ -24,7 +24,7 @@ func InitIndexRouter(router *gin.RouterGroup) {
{
// 首页基本信息统计
index.GET("count", func(g *gin.Context) {
ctx.NewReqCtxWithGin(g).
req.NewCtxWithGin(g).
Handle(i.Count)
})
}

View File

@@ -10,9 +10,9 @@ import (
sysapp "mayfly-go/internal/sys/application"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils"
"mayfly-go/pkg/ws"
"strconv"
@@ -30,10 +30,10 @@ type Db struct {
TagApp tagapp.TagTree
}
const DEFAULT_ROW_SIZE = 1800
const DEFAULT_ROW_SIZE = 5000
// @router /api/dbs [get]
func (d *Db) Dbs(rc *ctx.ReqCtx) {
func (d *Db) Dbs(rc *req.Ctx) {
condition := new(entity.DbQuery)
condition.TagPathLike = rc.GinCtx.Query("tagPath")
@@ -47,7 +47,7 @@ func (d *Db) Dbs(rc *ctx.ReqCtx) {
rc.ResData = d.DbApp.GetPageList(condition, ginx.GetPageParam(rc.GinCtx), new([]vo.SelectDataDbVO))
}
func (d *Db) Save(rc *ctx.ReqCtx) {
func (d *Db) Save(rc *req.Ctx) {
form := &form.DbForm{}
ginx.BindJsonAndValid(rc.GinCtx, form)
@@ -68,7 +68,7 @@ func (d *Db) Save(rc *ctx.ReqCtx) {
}
// 获取数据库实例密码,由于数据库是加密存储,故提供该接口展示原文密码
func (d *Db) GetDbPwd(rc *ctx.ReqCtx) {
func (d *Db) GetDbPwd(rc *req.Ctx) {
dbId := GetDbId(rc.GinCtx)
dbEntity := d.DbApp.GetById(dbId, "Password")
dbEntity.PwdDecrypt()
@@ -76,7 +76,7 @@ func (d *Db) GetDbPwd(rc *ctx.ReqCtx) {
}
// 获取数据库实例的所有数据库名
func (d *Db) GetDatabaseNames(rc *ctx.ReqCtx) {
func (d *Db) GetDatabaseNames(rc *req.Ctx) {
form := &form.DbForm{}
ginx.BindJsonAndValid(rc.GinCtx, form)
@@ -95,30 +95,30 @@ func (d *Db) GetDatabaseNames(rc *ctx.ReqCtx) {
rc.ResData = d.DbApp.GetDatabases(db)
}
func (d *Db) DeleteDb(rc *ctx.ReqCtx) {
func (d *Db) DeleteDb(rc *req.Ctx) {
dbId := GetDbId(rc.GinCtx)
d.DbApp.Delete(dbId)
// 删除该库的sql执行记录
d.DbSqlExecApp.DeleteBy(&entity.DbSqlExec{DbId: dbId})
}
func (d *Db) TableInfos(rc *ctx.ReqCtx) {
func (d *Db) TableInfos(rc *req.Ctx) {
rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetMeta().GetTableInfos()
}
func (d *Db) TableIndex(rc *ctx.ReqCtx) {
func (d *Db) TableIndex(rc *req.Ctx) {
tn := rc.GinCtx.Query("tableName")
biz.NotEmpty(tn, "tableName不能为空")
rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetMeta().GetTableIndex(tn)
}
func (d *Db) GetCreateTableDdl(rc *ctx.ReqCtx) {
func (d *Db) GetCreateTableDdl(rc *req.Ctx) {
tn := rc.GinCtx.Query("tableName")
biz.NotEmpty(tn, "tableName不能为空")
rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetMeta().GetCreateTableDdl(tn)
}
func (d *Db) ExecSql(rc *ctx.ReqCtx) {
func (d *Db) ExecSql(rc *req.Ctx) {
g := rc.GinCtx
form := &form.DbSqlExecForm{}
ginx.BindJsonAndValid(g, form)
@@ -128,7 +128,7 @@ func (d *Db) ExecSql(rc *ctx.ReqCtx) {
dbInstance := d.DbApp.GetDbInstance(id, db)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.LoginAccount.Id, dbInstance.Info.TagPath), "%s")
rc.ReqParam = fmt.Sprintf("%s -> %s", dbInstance.Info.GetLogDesc(), form.Sql)
rc.ReqParam = fmt.Sprintf("%s\n-> %s", dbInstance.Info.GetLogDesc(), form.Sql)
biz.NotEmpty(form.Sql, "sql不能为空")
// 去除前后空格及换行符
@@ -153,7 +153,9 @@ func (d *Db) ExecSql(rc *ctx.ReqCtx) {
}
execReq.Sql = s
execRes, err := d.DbSqlExecApp.Exec(execReq)
biz.ErrIsNilAppendErr(err, "执行失败: %s")
if err != nil {
biz.ErrIsNilAppendErr(err, fmt.Sprintf("[%s] -> 执行失败: ", s)+"%s")
}
if execResAll == nil {
execResAll = execRes
@@ -169,7 +171,7 @@ func (d *Db) ExecSql(rc *ctx.ReqCtx) {
}
// 执行sql文件
func (d *Db) ExecSqlFile(rc *ctx.ReqCtx) {
func (d *Db) ExecSqlFile(rc *req.Ctx) {
g := rc.GinCtx
fileheader, err := g.FormFile("file")
biz.ErrIsNilAppendErr(err, "读取sql文件失败: %s")
@@ -178,24 +180,33 @@ func (d *Db) ExecSqlFile(rc *ctx.ReqCtx) {
filename := fileheader.Filename
dbId, db := GetIdAndDb(g)
rc.ReqParam = fmt.Sprintf("dbId: %d, db: %s, filename: %s", dbId, db, filename)
dbInstance := d.DbApp.GetDbInstance(dbId, db)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.LoginAccount.Id, dbInstance.Info.TagPath), "%s")
rc.ReqParam = fmt.Sprintf("%s -> filename: %s", dbInstance.Info.GetLogDesc(), filename)
logExecRecord := true
// 如果执行sql文件大于该值则不记录sql执行记录
if fileheader.Size > 50*1024 {
logExecRecord = false
}
go func() {
db := d.DbApp.GetDbInstance(dbId, db)
dbEntity := d.DbApp.GetById(dbId)
dbInfo := fmt.Sprintf("于%s的%s环境", dbEntity.Name, dbEntity.TagPath)
defer func() {
if err := recover(); err != nil {
switch t := err.(type) {
case *biz.BizError:
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrMsg("sql脚本执行失败", fmt.Sprintf("[%s]%s执行失败: [%s]", filename, dbInfo, t.Error())))
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrMsg("sql脚本执行失败", fmt.Sprintf("[%s]%s执行失败: [%s]", filename, dbInstance.Info.GetLogDesc(), t.Error())))
}
}
}()
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.LoginAccount.Id, db.Info.TagPath), "%s")
execReq := &application.DbSqlExecReq{
DbId: dbId,
Db: db,
Remark: fileheader.Filename,
DbInstance: dbInstance,
LoginAccount: rc.LoginAccount,
}
tokens := sqlparser.NewTokenizer(file)
for {
@@ -204,18 +215,25 @@ func (d *Db) ExecSqlFile(rc *ctx.ReqCtx) {
break
}
sql := sqlparser.String(stmt)
_, err = db.Exec(sql)
execReq.Sql = sql
// 需要记录执行记录
if logExecRecord {
_, err = d.DbSqlExecApp.Exec(execReq)
} else {
_, err = dbInstance.Exec(sql)
}
if err != nil {
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrMsg("sql脚本执行失败", fmt.Sprintf("[%s]%s执行失败: [%s]", filename, dbInfo, err.Error())))
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrMsg("sql脚本执行失败", fmt.Sprintf("[%s][%s] -> sql=[%s] 执行失败: [%s]", filename, dbInstance.Info.GetLogDesc(), sql, err.Error())))
return
}
}
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.SuccessMsg("sql脚本执行成功", fmt.Sprintf("[%s]%s执行完成", filename, dbInfo)))
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.SuccessMsg("sql脚本执行成功", fmt.Sprintf("[%s]执行完成 -> %s", filename, dbInstance.Info.GetLogDesc())))
}()
}
// 数据库dump
func (d *Db) DumpSql(rc *ctx.ReqCtx) {
func (d *Db) DumpSql(rc *req.Ctx) {
g := rc.GinCtx
dbId, db := GetIdAndDb(g)
dumpType := g.Query("type")
@@ -258,33 +276,13 @@ func (d *Db) DumpSql(rc *ctx.ReqCtx) {
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表记录: %s \n-- ----------------------------\n", table))
writer.WriteString("BEGIN;\n")
countSql := fmt.Sprintf("SELECT COUNT(*) count FROM %s", table)
_, countRes, _ := dbInstance.SelectData(countSql)
// 查询出所有列信息总数,手动分页获取所有数据
maCount := 0
// 查询出所有列信息总数,手动分页获取所有数据
if count64, is64 := countRes[0]["count"].(int64); is64 {
maCount = int(count64)
} else {
maCount = countRes[0]["count"].(int)
}
// 计算需要查询的页数
pageNum := maCount / DEFAULT_ROW_SIZE
if maCount%DEFAULT_ROW_SIZE > 0 {
pageNum++
}
var sqlTmp string
switch dbInstance.Info.Type {
case entity.DbTypeMysql:
sqlTmp = "SELECT * FROM %s LIMIT %d, %d"
case entity.DbTypePostgres:
sqlTmp = "SELECT * FROM %s OFFSET %d LIMIT %d"
}
for index := 0; index < pageNum; index++ {
sql := fmt.Sprintf(sqlTmp, table, index*DEFAULT_ROW_SIZE, DEFAULT_ROW_SIZE)
columns, result, _ := dbInstance.SelectData(sql)
pageNum := 1
for {
columns, result, _ := dbmeta.GetTableRecord(table, pageNum, DEFAULT_ROW_SIZE)
resultLen := len(result)
if resultLen == 0 {
break
}
insertSql := "INSERT INTO `%s` VALUES (%s);\n"
for _, res := range result {
var values []string
@@ -303,37 +301,38 @@ func (d *Db) DumpSql(rc *ctx.ReqCtx) {
}
writer.WriteString(fmt.Sprintf(insertSql, table, strings.Join(values, ", ")))
}
if resultLen < DEFAULT_ROW_SIZE {
break
}
pageNum++
}
writer.WriteString("COMMIT;\n")
}
rc.NoRes = true
rc.ReqParam = fmt.Sprintf("dbId: %d, db: %s, tables: %s, dumpType: %s", dbId, db, tablesStr, dumpType)
rc.ReqParam = fmt.Sprintf("%s, tables: %s, dumpType: %s", dbInstance.Info.GetLogDesc(), tablesStr, dumpType)
}
// @router /api/db/:dbId/t-metadata [get]
func (d *Db) TableMA(rc *ctx.ReqCtx) {
func (d *Db) TableMA(rc *req.Ctx) {
dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx))
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.LoginAccount.Id, dbi.Info.TagPath), "%s")
rc.ResData = dbi.GetMeta().GetTables()
}
// @router /api/db/:dbId/c-metadata [get]
func (d *Db) ColumnMA(rc *ctx.ReqCtx) {
func (d *Db) ColumnMA(rc *req.Ctx) {
g := rc.GinCtx
tn := g.Query("tableName")
biz.NotEmpty(tn, "tableName不能为空")
dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx))
// biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.LoginAccount.Id, dbInstance.TagPath), "%s")
rc.ResData = dbi.GetMeta().GetColumns(tn)
}
// @router /api/db/:dbId/hint-tables [get]
func (d *Db) HintTables(rc *ctx.ReqCtx) {
func (d *Db) HintTables(rc *req.Ctx) {
dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx))
// biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.LoginAccount.Id, dbInstance.TagPath), "%s")
dm := dbi.GetMeta()
// 获取所有表
@@ -372,7 +371,7 @@ func (d *Db) HintTables(rc *ctx.ReqCtx) {
}
// @router /api/db/:dbId/sql [post]
func (d *Db) SaveSql(rc *ctx.ReqCtx) {
func (d *Db) SaveSql(rc *req.Ctx) {
g := rc.GinCtx
account := rc.LoginAccount
dbSqlForm := &form.DbSqlSaveForm{}
@@ -400,7 +399,7 @@ func (d *Db) SaveSql(rc *ctx.ReqCtx) {
}
// 获取所有保存的sql names
func (d *Db) GetSqlNames(rc *ctx.ReqCtx) {
func (d *Db) GetSqlNames(rc *req.Ctx) {
id, db := GetIdAndDb(rc.GinCtx)
// 获取用于是否有该dbsql的保存记录有则更改否则新增
dbSql := &entity.DbSql{Type: 1, DbId: id, Db: db}
@@ -412,17 +411,18 @@ func (d *Db) GetSqlNames(rc *ctx.ReqCtx) {
}
// 删除保存的sql
func (d *Db) DeleteSql(rc *ctx.ReqCtx) {
func (d *Db) DeleteSql(rc *req.Ctx) {
dbSql := &entity.DbSql{Type: 1, DbId: GetDbId(rc.GinCtx)}
dbSql.CreatorId = rc.LoginAccount.Id
dbSql.Name = rc.GinCtx.Query("name")
dbSql.Db = rc.GinCtx.Query("db")
model.DeleteByCondition(dbSql)
}
// @router /api/db/:dbId/sql [get]
func (d *Db) GetSql(rc *ctx.ReqCtx) {
func (d *Db) GetSql(rc *req.Ctx) {
id, db := GetIdAndDb(rc.GinCtx)
// 根据创建者id 数据库id以及sql模板名称查询保存的sql信息
dbSql := &entity.DbSql{Type: 1, DbId: id, Db: db}

View File

@@ -3,15 +3,15 @@ package api
import (
"mayfly-go/internal/db/application"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
)
type DbSqlExec struct {
DbSqlExecApp application.DbSqlExec
}
func (d *DbSqlExec) DbSqlExecs(rc *ctx.ReqCtx) {
func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) {
g := rc.GinCtx
m := &entity.DbSqlExec{DbId: uint64(ginx.QueryInt(g, "dbId", 0)),
Db: g.Query("db"),

View File

@@ -1,25 +1,23 @@
package form
type DbForm struct {
Id uint64
Name string `binding:"required" json:"name"`
Type string `binding:"required" json:"type"` // 类型mysql oracle等
Host string `binding:"required" json:"host"`
Port int `binding:"required" json:"port"`
Username string `binding:"required" json:"username"`
Password string `json:"password"`
Params string `json:"params"`
Database string `json:"database"`
Remark string `json:"remark"`
TagId uint64 `json:"tagId"`
TagPath string `json:"tagPath"`
EnableSshTunnel int8 `json:"enableSshTunnel"`
SshTunnelMachineId uint64 `json:"sshTunnelMachineId"`
Id uint64
Name string `binding:"required" json:"name"`
Type string `binding:"required" json:"type"` // 类型mysql oracle等
Host string `binding:"required" json:"host"`
Port int `binding:"required" json:"port"`
Username string `binding:"required" json:"username"`
Password string `json:"password"`
Params string `json:"params"`
Database string `json:"database"`
Remark string `json:"remark"`
TagId uint64 `json:"tagId"`
TagPath string `json:"tagPath"`
SshTunnelMachineId int `json:"sshTunnelMachineId"`
}
type DbSqlSaveForm struct {
Name string
Name string `binding:"required"`
Sql string `binding:"required"`
Type int `binding:"required"`
Db string `binding:"required"`

View File

@@ -23,6 +23,5 @@ type SelectDataDbVO struct {
Modifier *string `json:"modifier"`
ModifierId *int64 `json:"modifierId"`
EnableSshTunnel *int8 `json:"enableSshTunnel"`
SshTunnelMachineId *uint64 `json:"sshTunnelMachineId"`
SshTunnelMachineId int `json:"sshTunnelMachineId"`
}

View File

@@ -1,6 +1,8 @@
package application
import "mayfly-go/internal/db/infrastructure/persistence"
import (
"mayfly-go/internal/db/infrastructure/persistence"
)
var (
dbApp Db = newDbApp(persistence.GetDbRepo(), persistence.GetDbSqlRepo())

View File

@@ -1,28 +1,22 @@
package application
import (
"context"
"database/sql"
"fmt"
"mayfly-go/internal/constant"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
machineapp "mayfly-go/internal/machine/application"
"mayfly-go/internal/machine/infrastructure/machine"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/cache"
"mayfly-go/pkg/global"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils"
"net"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/go-sql-driver/mysql"
"github.com/lib/pq"
)
type Db interface {
@@ -92,7 +86,7 @@ func (d *dbAppImpl) Save(dbEntity *entity.Db) {
}
// 查找是否存在该库
oldDb := &entity.Db{Host: dbEntity.Host, Port: dbEntity.Port, TagId: dbEntity.TagId}
oldDb := &entity.Db{Host: dbEntity.Host, Port: dbEntity.Port, Username: dbEntity.Username}
err := d.GetDbBy(oldDb)
if dbEntity.Id == 0 {
@@ -165,7 +159,7 @@ func (d *dbAppImpl) GetDatabases(ed *entity.Db) []string {
biz.ErrIsNilAppendErr(err, "数据库连接失败: %s")
defer dbConn.Close()
_, res, err := SelectDataByDb(dbConn, getDatabasesSql, true)
_, res, err := SelectDataByDb(dbConn, getDatabasesSql)
biz.ErrIsNilAppendErr(err, "获取数据库列表失败")
for _, re := range res {
databases = append(databases, re["dbname"].(string))
@@ -190,10 +184,10 @@ func (da *dbAppImpl) GetDbInstance(id uint64, db string) *DbInstance {
defer mutex.Unlock()
d := da.GetById(id)
// 密码解密
d.PwdDecrypt()
biz.NotNil(d, "数据库信息不存在")
biz.IsTrue(strings.Contains(d.Database, db), "未配置该库的操作权限")
// 密码解密
d.PwdDecrypt()
dbInfo := new(DbInfo)
utils.Copy(dbInfo, d)
@@ -234,8 +228,7 @@ type DbInfo struct {
Username string
TagPath string
Database string
EnableSshTunnel int8 // 是否启用ssh隧道
SshTunnelMachineId uint64
SshTunnelMachineId int
}
// 获取记录日志的描述
@@ -254,7 +247,7 @@ type DbInstance struct {
// 执行查询语句
// 依次返回 列名数组结果map错误
func (d *DbInstance) SelectData(execSql string) ([]string, []map[string]interface{}, error) {
return SelectDataByDb(d.db, execSql, false)
return SelectDataByDb(d.db, execSql)
}
// 将查询结果映射至struct可具体参考sqlx库
@@ -265,7 +258,7 @@ func (d *DbInstance) SelectData2Struct(execSql string, dest interface{}) error {
// 执行内部查询语句,不返回列名以及不限制行数
// 依次返回 结果map错误
func (d *DbInstance) innerSelect(execSql string) ([]map[string]interface{}, error) {
_, res, err := SelectDataByDb(d.db, execSql, true)
_, res, err := SelectDataByDb(d.db, execSql)
return res, err
}
@@ -303,9 +296,6 @@ func (d *DbInstance) Close() {
//------------------------------------------------------------------------------
// 单次最大查询数据集
const Max_Rows = 2000
// 客户端连接缓存,指定时间内没有访问则会被关闭, key为数据库实例id:数据库
var dbCache = cache.NewTimedCache(constant.DbConnExpireTime, 5*time.Second).
WithUpdateAccessTime(true).
@@ -315,7 +305,7 @@ var dbCache = cache.NewTimedCache(constant.DbConnExpireTime, 5*time.Second).
})
func init() {
machine.AddCheckSshTunnelMachineUseFunc(func(machineId uint64) bool {
machine.AddCheckSshTunnelMachineUseFunc(func(machineId int) bool {
// 遍历所有db连接实例若存在db实例使用该ssh隧道机器则返回true表示还在使用中...
items := dbCache.Items()
for _, v := range items {
@@ -347,22 +337,14 @@ func TestConnection(d *entity.Db) {
// 获取数据库连接
func GetDbConn(d *entity.Db, db string) (*sql.DB, error) {
// SSH Conect
if d.EnableSshTunnel == 1 && d.SshTunnelMachineId != 0 {
sshTunnelMachine := machineapp.GetMachineApp().GetSshTunnelMachine(d.SshTunnelMachineId)
if d.Type == entity.DbTypeMysql {
mysql.RegisterDialContext(d.Network, func(ctx context.Context, addr string) (net.Conn, error) {
return sshTunnelMachine.GetDialConn("tcp", addr)
})
} else if d.Type == entity.DbTypePostgres {
_, err := pq.DialOpen(&PqSqlDialer{sshTunnelMachine: sshTunnelMachine}, getDsn(d, db))
if err != nil {
panic(biz.NewBizErr(fmt.Sprintf("postgres隧道连接失败: %s", err.Error())))
}
}
var DB *sql.DB
var err error
if d.Type == entity.DbTypeMysql {
DB, err = getMysqlDB(d, db)
} else if d.Type == entity.DbTypePostgres {
DB, err = getPgsqlDB(d, db)
}
DB, err := sql.Open(d.Type, getDsn(d, db))
if err != nil {
return nil, err
}
@@ -375,29 +357,7 @@ func GetDbConn(d *entity.Db, db string) (*sql.DB, error) {
return DB, nil
}
// 获取dataSourceName
func getDsn(d *entity.Db, db string) string {
var dsn string
if d.Type == entity.DbTypeMysql {
// 更多参数参考https://github.com/go-sql-driver/mysql#dsn-data-source-name
dsn = fmt.Sprintf("%s:%s@%s(%s:%d)/%s?timeout=8s", d.Username, d.Password, d.Network, d.Host, d.Port, db)
if d.Params != "" {
dsn = fmt.Sprintf("%s&%s", dsn, d.Params)
}
return dsn
}
if d.Type == entity.DbTypePostgres {
dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", d.Host, d.Port, d.Username, d.Password, db)
if d.Params != "" {
dsn = fmt.Sprintf("%s %s", dsn, strings.Join(strings.Split(d.Params, "&"), " "))
}
return dsn
}
return ""
}
func SelectDataByDb(db *sql.DB, selectSql string, isInner bool) ([]string, []map[string]interface{}, error) {
func SelectDataByDb(db *sql.DB, selectSql string) ([]string, []map[string]interface{}, error) {
rows, err := db.Query(selectSql)
if err != nil {
return nil, nil, err
@@ -424,14 +384,7 @@ func SelectDataByDb(db *sql.DB, selectSql string, isInner bool) ([]string, []map
colNames := make([]string, 0)
// 是否第一次遍历,列名数组只需第一次遍历时加入
isFirst := true
rowNum := 0
for rows.Next() {
rowNum++
// 非内部sql则校验返回结果数量
if !isInner {
biz.IsTrue(rowNum <= Max_Rows, "结果集 > 2000, 请完善条件或分页信息")
}
// 不Scan也会导致等待该链接实际处于未工作的状态然后也会导致连接数迅速达到最大
err := rows.Scan(scans...)
if err != nil {
@@ -497,7 +450,7 @@ func valueConvert(data []byte, colType *sql.ColumnType) interface{} {
return intV
}
}
if strings.Contains(colScanType, "float") {
if strings.Contains(colScanType, "float") || strings.Contains(colDatabaseTypeName, "decimal") {
floatV, _ := strconv.ParseFloat(stringV, 64)
return floatV
}
@@ -525,20 +478,3 @@ func Select2StructByDb(db *sql.DB, selectSql string, dest interface{}) error {
func CloseDb(dbId uint64, db string) {
dbCache.Delete(GetDbCacheKey(dbId, db))
}
type PqSqlDialer struct {
sshTunnelMachine *machine.SshTunnelMachine
}
func (pd *PqSqlDialer) Dial(network, address string) (net.Conn, error) {
if sshConn, err := pd.sshTunnelMachine.GetDialConn("tcp", address); err == nil {
// 将ssh conn包装否则redis内部设置超时会报错,ssh conn不支持设置超时会返回错误: ssh: tcpChan: deadline not supported
return &utils.WrapSshConn{Conn: sshConn}, nil
} else {
return nil, err
}
}
func (pd *PqSqlDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
return pd.Dial(network, address)
}

View File

@@ -5,7 +5,11 @@ import (
"fmt"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
sysapp "mayfly-go/internal/sys/application"
sysentity "mayfly-go/internal/sys/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/model"
"strconv"
"strings"
"github.com/xwb1989/sqlparser"
@@ -73,32 +77,42 @@ func createSqlExecRecord(execSqlReq *DbSqlExecReq) *entity.DbSqlExec {
func (d *dbSqlExecAppImpl) Exec(execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error) {
sql := execSqlReq.Sql
stmt, err := sqlparser.Parse(sql)
if err != nil {
// 就算解析失败也执行sql让数据库来判断错误
//global.Log.Error("sql解析失败: ", err)
if strings.HasPrefix(strings.ToLower(execSqlReq.Sql), "select") ||
strings.HasPrefix(strings.ToLower(execSqlReq.Sql), "show") {
return doSelect(execSqlReq)
}
// 保存执行记录
d.dbSqlExecRepo.Insert(createSqlExecRecord(execSqlReq))
return doExec(execSqlReq.Sql, execSqlReq.DbInstance)
}
dbSqlExecRecord := createSqlExecRecord(execSqlReq)
var execRes *DbSqlExecRes
isSelect := false
stmt, err := sqlparser.Parse(sql)
if err != nil {
// 就算解析失败也执行sql让数据库来判断错误。如果是查询sql则简单判断是否有limit分页参数信息兼容pgsql
// global.Log.Warnf("sqlparse解析sql[%s]失败: %s", sql, err.Error())
lowerSql := strings.ToLower(execSqlReq.Sql)
isSelect := strings.HasPrefix(lowerSql, "select")
if isSelect {
biz.IsTrue(strings.Contains(lowerSql, "limit"), "请完善分页信息")
}
var execErr error
if isSelect || strings.HasPrefix(lowerSql, "show") {
execRes, execErr = doRead(execSqlReq)
} else {
execRes, execErr = doExec(execSqlReq.Sql, execSqlReq.DbInstance)
}
if execErr != nil {
return nil, execErr
}
d.saveSqlExecLog(isSelect, dbSqlExecRecord)
return execRes, nil
}
switch stmt := stmt.(type) {
case *sqlparser.Select:
isSelect = true
execRes, err = doSelect(execSqlReq)
execRes, err = doSelect(stmt, execSqlReq)
case *sqlparser.Show:
isSelect = true
execRes, err = doSelect(execSqlReq)
execRes, err = doRead(execSqlReq)
case *sqlparser.OtherRead:
isSelect = true
execRes, err = doSelect(execSqlReq)
execRes, err = doRead(execSqlReq)
case *sqlparser.Update:
execRes, err = doUpdate(stmt, execSqlReq, dbSqlExecRecord)
case *sqlparser.Delete:
@@ -111,12 +125,22 @@ func (d *dbSqlExecAppImpl) Exec(execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error)
if err != nil {
return nil, err
}
d.saveSqlExecLog(isSelect, dbSqlExecRecord)
return execRes, nil
}
if !isSelect {
// 保存执行记录
// 保存sql执行记录如果是查询类则根据系统配置判断是否保存
func (d *dbSqlExecAppImpl) saveSqlExecLog(isQuery bool, dbSqlExecRecord *entity.DbSqlExec) {
if !isQuery {
d.dbSqlExecRepo.Insert(dbSqlExecRecord)
return
}
if sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyDbSaveQuerySQL).BoolValue(false) {
dbSqlExecRecord.Table = "-"
dbSqlExecRecord.OldValue = "-"
dbSqlExecRecord.Type = entity.DbSqlExecTypeQuery
d.dbSqlExecRepo.Insert(dbSqlExecRecord)
}
return execRes, nil
}
func (d *dbSqlExecAppImpl) DeleteBy(condition *entity.DbSqlExec) {
@@ -127,7 +151,25 @@ func (d *dbSqlExecAppImpl) GetPageList(condition *entity.DbSqlExec, pageParam *m
return d.dbSqlExecRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
func doSelect(execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error) {
func doSelect(selectStmt *sqlparser.Select, execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error) {
selectExprsStr := sqlparser.String(selectStmt.SelectExprs)
if selectExprsStr == "*" || strings.Contains(selectExprsStr, ".*") ||
len(strings.Split(selectExprsStr, ",")) > 1 {
// 如果配置为0则不校验分页参数
maxCount := sysapp.GetConfigApp().GetConfig(sysentity.ConfigKeyDbQueryMaxCount).IntValue(200)
if maxCount != 0 {
limit := selectStmt.Limit
biz.NotNil(limit, "请完善分页信息后执行")
count, err := strconv.Atoi(sqlparser.String(limit.Rowcount))
biz.ErrIsNil(err, "分页参数有误")
biz.IsTrue(count <= maxCount, "查询结果集数需小于系统配置的%d条", maxCount)
}
}
return doRead(execSqlReq)
}
func doRead(execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error) {
dbInstance := execSqlReq.DbInstance
sql := execSqlReq.Sql
colNames, res, err := dbInstance.SelectData(sql)
@@ -220,5 +262,5 @@ func doExec(sql string, dbInstance *DbInstance) (*DbSqlExecRes, error) {
return &DbSqlExecRes{
ColNames: []string{"sql", "rowsAffected", "result"},
Res: res,
}, nil
}, err
}

View File

@@ -1,7 +1,7 @@
package application
// -----------------------------------元数据接口定义------------------------------------------
// 数据库元信息接口(表、列等元信息)
// 数据库元信息接口(表、列、获取表数据等元信息)
// 所有数据查出来直接用map接收注意不同数据库实现该接口返回的map中的key需要统一.
// 即: 使用别名统一即可。如table_name AS tableName
type DbMetadata interface {
@@ -25,4 +25,8 @@ type DbMetadata interface {
// 获取建表ddl
GetCreateTableDdl(tableName string) []map[string]interface{}
// 获取指定表的数据-分页查询
// @return columns: 列字段名result: 结果集error: 错误
GetTableRecord(tableName string, pageNum, pageSize int) ([]string, []map[string]interface{}, error)
}

View File

@@ -1,10 +1,33 @@
package application
import (
"context"
"database/sql"
"fmt"
"mayfly-go/internal/db/domain/entity"
machineapp "mayfly-go/internal/machine/application"
"mayfly-go/pkg/biz"
"net"
"github.com/go-sql-driver/mysql"
)
func getMysqlDB(d *entity.Db, db string) (*sql.DB, error) {
// SSH Conect
if d.SshTunnelMachineId > 0 {
sshTunnelMachine := machineapp.GetMachineApp().GetSshTunnelMachine(d.SshTunnelMachineId)
mysql.RegisterDialContext(d.Network, func(ctx context.Context, addr string) (net.Conn, error) {
return sshTunnelMachine.GetDialConn("tcp", addr)
})
}
// 设置dataSourceName -> 更多参数参考https://github.com/go-sql-driver/mysql#dsn-data-source-name
dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?timeout=8s", d.Username, d.Password, d.Network, d.Host, d.Port, db)
if d.Params != "" {
dsn = fmt.Sprintf("%s&%s", dsn, d.Params)
}
return sql.Open(d.Type, dsn)
}
// ---------------------------------- mysql元数据 -----------------------------------
const (
// mysql 表信息元数据
@@ -56,9 +79,7 @@ func (mm *MysqlMetadata) GetColumns(tableNames ...string) []map[string]interface
// 获取表主键字段名,不存在主键标识则默认第一个字段
func (mm *MysqlMetadata) GetPrimaryKey(tablename string) string {
columns := mm.GetColumns(tablename)
if len(columns) == 0 {
panic(biz.NewBizErr(fmt.Sprintf("[%s] 表不存在", tablename)))
}
biz.IsTrue(len(columns) > 0, "[%s] 表不存在", tablename)
for _, v := range columns {
if v["columnKey"].(string) == "PRI" {
return v["columnName"].(string)
@@ -103,3 +124,7 @@ func (mm *MysqlMetadata) GetCreateTableDdl(tableName string) []map[string]interf
res, _ := mm.di.innerSelect(fmt.Sprintf("show create table %s ", tableName))
return res
}
func (mm *MysqlMetadata) GetTableRecord(tableName string, pageNum, pageSize int) ([]string, []map[string]interface{}, error) {
return mm.di.SelectData(fmt.Sprintf("SELECT * FROM %s LIMIT %d, %d", tableName, (pageNum-1)*pageSize, pageSize))
}

View File

@@ -1,10 +1,60 @@
package application
import (
"database/sql"
"database/sql/driver"
"fmt"
"mayfly-go/internal/db/domain/entity"
machineapp "mayfly-go/internal/machine/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/utils"
"net"
"strings"
"time"
"github.com/lib/pq"
)
func getPgsqlDB(d *entity.Db, db string) (*sql.DB, error) {
driverName := d.Type
// SSH Conect
if d.SshTunnelMachineId > 0 {
// 如果使用了隧道,则使用`postgres:ssh:隧道机器id`注册名
driverName = fmt.Sprintf("postgres:ssh:%d", d.SshTunnelMachineId)
if !utils.ArrContains(sql.Drivers(), driverName) {
sql.Register(driverName, &PqSqlDialer{sshTunnelMachineId: d.SshTunnelMachineId})
}
sql.Drivers()
}
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", d.Host, d.Port, d.Username, d.Password, db)
if d.Params != "" {
dsn = fmt.Sprintf("%s %s", dsn, strings.Join(strings.Split(d.Params, "&"), " "))
}
return sql.Open(driverName, dsn)
}
// pgsql dialer
type PqSqlDialer struct {
sshTunnelMachineId int
}
func (d *PqSqlDialer) Open(name string) (driver.Conn, error) {
return pq.DialOpen(d, name)
}
func (pd *PqSqlDialer) Dial(network, address string) (net.Conn, error) {
if sshConn, err := machineapp.GetMachineApp().GetSshTunnelMachine(pd.sshTunnelMachineId).GetDialConn("tcp", address); err == nil {
return sshConn, nil
} else {
return nil, err
}
}
func (pd *PqSqlDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
return pd.Dial(network, address)
}
// ---------------------------------- pgsql元数据 -----------------------------------
const (
// postgres 表信息元数据
@@ -22,7 +72,7 @@ const (
A.attname AS "columnName",
tc.is_nullable AS "nullable",
concat_ws ( '', t.typname, SUBSTRING ( format_type ( a.atttypid, a.atttypmod ) FROM '\(.*\)' ) ) AS "columnType",
(CASE WHEN ( SELECT COUNT(*) FROM pg_constraint WHERE conrelid = a.attrelid AND conkey[1]= attnum AND contype = 'p' ) > 0 THEN 'PRI' ELSE '' END ) AS columnKey,
(CASE WHEN ( SELECT COUNT(*) FROM pg_constraint WHERE conrelid = a.attrelid AND conkey[1]= attnum AND contype = 'p' ) > 0 THEN 'PRI' ELSE '' END ) AS "columnKey",
d.description AS "columnComment"
FROM
pg_attribute a LEFT JOIN pg_description d ON d.objoid = a.attrelid
@@ -68,9 +118,7 @@ func (pm *PgsqlMetadata) GetColumns(tableNames ...string) []map[string]interface
func (pm *PgsqlMetadata) GetPrimaryKey(tablename string) string {
columns := pm.GetColumns(tablename)
if len(columns) == 0 {
panic(biz.NewBizErr(fmt.Sprintf("[%s] 表不存在", tablename)))
}
biz.IsTrue(len(columns) > 0, "[%s] 表不存在", tablename)
for _, v := range columns {
if v["columnKey"].(string) == "PRI" {
return v["columnName"].(string)
@@ -95,7 +143,11 @@ func (pm *PgsqlMetadata) GetTableIndex(tableName string) []map[string]interface{
}
// 获取建表ddl
func (mm *PgsqlMetadata) GetCreateTableDdl(tableName string) []map[string]interface{} {
func (pm *PgsqlMetadata) GetCreateTableDdl(tableName string) []map[string]interface{} {
biz.IsTrue(tableName == "", "暂不支持获取pgsql建表DDL")
return nil
}
func (pm *PgsqlMetadata) GetTableRecord(tableName string, pageNum, pageSize int) ([]string, []map[string]interface{}, error) {
return pm.di.SelectData(fmt.Sprintf("SELECT * FROM %s OFFSET %d LIMIT %d", tableName, (pageNum-1)*pageSize, pageSize))
}

View File

@@ -9,27 +9,25 @@ import (
type Db struct {
model.Model
Name string `orm:"column(name)" json:"name"`
Type string `orm:"column(type)" json:"type"` // 类型mysql oracle等
Host string `orm:"column(host)" json:"host"`
Port int `orm:"column(port)" json:"port"`
Network string `orm:"column(network)" json:"network"`
Username string `orm:"column(username)" json:"username"`
Password string `orm:"column(password)" json:"-"`
Database string `orm:"column(database)" json:"database"`
Params string `json:"params"`
Remark string `json:"remark"`
TagId uint64
TagPath string
EnableSshTunnel int8 `orm:"column(enable_ssh_tunnel)" json:"enableSshTunnel"` // 是否启用ssh隧道
SshTunnelMachineId uint64 `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
Name string `orm:"column(name)" json:"name"`
Type string `orm:"column(type)" json:"type"` // 类型mysql oracle等
Host string `orm:"column(host)" json:"host"`
Port int `orm:"column(port)" json:"port"`
Network string `orm:"column(network)" json:"network"`
Username string `orm:"column(username)" json:"username"`
Password string `orm:"column(password)" json:"-"`
Database string `orm:"column(database)" json:"database"`
Params string `json:"params"`
Remark string `json:"remark"`
TagId uint64
TagPath string
SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
}
// 获取数据库连接网络, 若没有使用ssh隧道则直接返回。否则返回拼接的网络需要注册至指定dial
func (d *Db) GetNetwork() string {
network := d.Network
if d.EnableSshTunnel == 0 || d.EnableSshTunnel == -1 {
if d.SshTunnelMachineId <= 0 {
if network == "" {
return "tcp"
} else {

View File

@@ -19,4 +19,5 @@ const (
DbSqlExecTypeUpdate int8 = 1 // 更新类型
DbSqlExecTypeDelete int8 = 2 // 删除类型
DbSqlExecTypeInsert int8 = 3 // 插入类型
DbSqlExecTypeQuery int8 = 4 // 查询类型如select、show等
)

View File

@@ -18,7 +18,6 @@ type DbQuery struct {
Remark string `json:"remark"`
TagId uint64
ProjectIds []uint64
TagIds []uint64
TagPathLike string
}

View File

@@ -5,7 +5,7 @@ import (
"mayfly-go/internal/db/application"
sysapp "mayfly-go/internal/sys/application"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
@@ -21,94 +21,94 @@ func InitDbRouter(router *gin.RouterGroup) {
}
// 获取所有数据库列表
db.GET("", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.Dbs)
req.NewCtxWithGin(c).Handle(d.Dbs)
})
saveDb := ctx.NewLogInfo("db-保存数据库信息").WithSave(true)
saveDb := req.NewLogInfo("db-保存数据库信息").WithSave(true)
db.POST("", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).
req.NewCtxWithGin(c).
WithLog(saveDb).
Handle(d.Save)
})
// 获取数据库实例的所有数据库名
db.POST("databases", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).
req.NewCtxWithGin(c).
Handle(d.GetDatabaseNames)
})
db.GET(":dbId/pwd", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.GetDbPwd)
req.NewCtxWithGin(c).Handle(d.GetDbPwd)
})
deleteDb := ctx.NewLogInfo("db-删除数据库信息").WithSave(true)
deleteDb := req.NewLogInfo("db-删除数据库信息").WithSave(true)
db.DELETE(":dbId", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).
req.NewCtxWithGin(c).
WithLog(deleteDb).
Handle(d.DeleteDb)
})
db.GET(":dbId/t-infos", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.TableInfos)
req.NewCtxWithGin(c).Handle(d.TableInfos)
})
db.GET(":dbId/t-index", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.TableIndex)
req.NewCtxWithGin(c).Handle(d.TableIndex)
})
db.GET(":dbId/t-create-ddl", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.GetCreateTableDdl)
req.NewCtxWithGin(c).Handle(d.GetCreateTableDdl)
})
execSqlLog := ctx.NewLogInfo("db-执行Sql")
execSqlLog := req.NewLogInfo("db-执行Sql")
db.POST(":dbId/exec-sql", func(g *gin.Context) {
rc := ctx.NewReqCtxWithGin(g).WithLog(execSqlLog)
rc := req.NewCtxWithGin(g).WithLog(execSqlLog)
rc.Handle(d.ExecSql)
})
execSqlFileLog := ctx.NewLogInfo("db-执行Sql文件").WithSave(true)
execSqlFileLog := req.NewLogInfo("db-执行Sql文件").WithSave(true)
db.POST(":dbId/exec-sql-file", func(g *gin.Context) {
ctx.NewReqCtxWithGin(g).
req.NewCtxWithGin(g).
WithLog(execSqlFileLog).
Handle(d.ExecSqlFile)
})
dumpLog := ctx.NewLogInfo("db-导出sql文件").WithSave(true)
dumpLog := req.NewLogInfo("db-导出sql文件").WithSave(true)
db.GET(":dbId/dump", func(g *gin.Context) {
ctx.NewReqCtxWithGin(g).
req.NewCtxWithGin(g).
WithLog(dumpLog).
Handle(d.DumpSql)
})
db.GET(":dbId/t-metadata", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.TableMA)
req.NewCtxWithGin(c).Handle(d.TableMA)
})
db.GET(":dbId/c-metadata", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.ColumnMA)
req.NewCtxWithGin(c).Handle(d.ColumnMA)
})
db.GET(":dbId/hint-tables", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.HintTables)
req.NewCtxWithGin(c).Handle(d.HintTables)
})
/** db sql相关接口 */
db.POST(":dbId/sql", func(c *gin.Context) {
rc := ctx.NewReqCtxWithGin(c)
rc := req.NewCtxWithGin(c)
rc.Handle(d.SaveSql)
})
db.GET(":dbId/sql", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.GetSql)
req.NewCtxWithGin(c).Handle(d.GetSql)
})
db.DELETE(":dbId/sql", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.DeleteSql)
req.NewCtxWithGin(c).Handle(d.DeleteSql)
})
db.GET(":dbId/sql-names", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.GetSqlNames)
req.NewCtxWithGin(c).Handle(d.GetSqlNames)
})
}
}

View File

@@ -3,7 +3,7 @@ package router
import (
"mayfly-go/internal/db/api"
"mayfly-go/internal/db/application"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
@@ -16,7 +16,7 @@ func InitDbSqlExecRouter(router *gin.RouterGroup) {
}
// 获取所有数据库sql执行记录列表
db.GET("", func(c *gin.Context) {
rc := ctx.NewReqCtxWithGin(c)
rc := req.NewCtxWithGin(c)
rc.Handle(d.DbSqlExecs)
})
}

View File

@@ -0,0 +1,62 @@
package api
import (
"mayfly-go/internal/machine/api/form"
"mayfly-go/internal/machine/api/vo"
"mayfly-go/internal/machine/application"
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils"
)
type AuthCert struct {
AuthCertApp application.AuthCert
}
func (ac *AuthCert) BaseAuthCerts(rc *req.Ctx) {
g := rc.GinCtx
condition := &entity.AuthCert{
Name: g.Query("name"),
AuthMethod: int8(ginx.QueryInt(g, "authMethod", 0)),
}
condition.Id = uint64(ginx.QueryInt(g, "id", 0))
rc.ResData = ac.AuthCertApp.GetPageList(condition, ginx.GetPageParam(g), new([]vo.AuthCertBaseVO))
}
func (ac *AuthCert) AuthCerts(rc *req.Ctx) {
g := rc.GinCtx
condition := &entity.AuthCert{
Name: g.Query("name"),
AuthMethod: int8(ginx.QueryInt(g, "authMethod", 0)),
}
condition.Id = uint64(ginx.QueryInt(g, "id", 0))
res := new([]*entity.AuthCert)
pageRes := ac.AuthCertApp.GetPageList(condition, ginx.GetPageParam(g), res)
for _, r := range *res {
r.PwdDecrypt()
}
rc.ResData = pageRes
}
func (c *AuthCert) SaveAuthCert(rc *req.Ctx) {
g := rc.GinCtx
acForm := &form.AuthCertForm{}
ginx.BindJsonAndValid(g, acForm)
ac := new(entity.AuthCert)
utils.Copy(ac, acForm)
// 脱敏记录日志
acForm.Passphrase = "***"
acForm.Password = "***"
rc.ReqParam = acForm
ac.SetBaseInfo(rc.LoginAccount)
c.AuthCertApp.Save(ac)
}
func (c *AuthCert) Delete(rc *req.Ctx) {
c.AuthCertApp.DeleteById(uint64(ginx.PathParamInt(rc.GinCtx, "id")))
}

View File

@@ -1,19 +1,21 @@
package form
type MachineForm struct {
Id uint64 `json:"id"`
Name string `json:"name" binding:"required"`
Ip string `json:"ip" binding:"required"` // IP地址
Username string `json:"username" binding:"required"` // 用户名
AuthMethod int8 `json:"authMethod" binding:"required"`
Password string `json:"password"`
Port int `json:"port" binding:"required"` // 端口号
Id uint64 `json:"id"`
Name string `json:"name" binding:"required"`
Ip string `json:"ip" binding:"required"` // IP地址
Port int `json:"port" binding:"required"` // 端口号
// 资产授权凭证信息列表
AuthCertId int `json:"authCertId"`
TagId uint64 `json:"tagId"`
TagPath string `json:"tagPath" binding:"required"`
Username string `json:"username"`
Password string `json:"password"`
Remark string `json:"remark"`
EnableSshTunnel int8 `json:"enableSshTunnel"` // 是否启用ssh隧道
SshTunnelMachineId uint64 `json:"sshTunnelMachineId"` // ssh隧道机器id
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
EnableRecorder int8 `json:"enableRecorder"` // 是否启用终端回放记录
TagId uint64 `json:"tagId"`
TagPath string `json:"tagPath"`
}
type MachineRunForm struct {
@@ -49,3 +51,21 @@ type MachineFileUpdateForm struct {
Id uint64 `binding:"required"`
Path string `binding:"required"`
}
// 授权凭证
type AuthCertForm struct {
Id uint64 `json:"id"`
Name string `json:"name" binding:"required"`
AuthMethod int8 `json:"authMethod" binding:"required"` // 1.密码 2.秘钥
Username string `json:"username"`
Password string `json:"password"` // 密码or私钥
Passphrase string `json:"passphrase"` // 私钥口令
Remark string `json:"remark"`
}
// 资产授权凭证信息
type AssetAuthCertForm struct {
AuthCertId uint64 `json:"authCertId"`
TagId uint64 `json:"tagId"`
TagPath string `json:"tagPath" binding:"required"`
}

View File

@@ -11,9 +11,9 @@ import (
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/config"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils"
"mayfly-go/pkg/ws"
"os"
@@ -31,7 +31,7 @@ type Machine struct {
TagApp tagapp.TagTree
}
func (m *Machine) Machines(rc *ctx.ReqCtx) {
func (m *Machine) Machines(rc *req.Ctx) {
condition := new(entity.MachineQuery)
condition.Ip = rc.GinCtx.Query("ip")
condition.Name = rc.GinCtx.Query("name")
@@ -53,17 +53,18 @@ func (m *Machine) Machines(rc *ctx.ReqCtx) {
list := res.List.(*[]*vo.MachineVO)
for _, mv := range *list {
mv.HasCli = machine.HasCli(*mv.Id)
mv.HasCli = machine.HasCli(mv.Id)
}
rc.ResData = res
}
func (m *Machine) MachineStats(rc *ctx.ReqCtx) {
func (m *Machine) MachineStats(rc *req.Ctx) {
stats := m.MachineApp.GetCli(GetMachineId(rc.GinCtx)).GetAllStats()
rc.ResData = stats
}
func (m *Machine) SaveMachine(rc *ctx.ReqCtx) {
// 保存机器信息
func (m *Machine) SaveMachine(rc *req.Ctx) {
g := rc.GinCtx
machineForm := new(form.MachineForm)
ginx.BindJsonAndValid(g, machineForm)
@@ -71,30 +72,25 @@ func (m *Machine) SaveMachine(rc *ctx.ReqCtx) {
me := new(entity.Machine)
utils.Copy(me, machineForm)
if me.AuthMethod == entity.MachineAuthMethodPassword {
// 密码解密,并使用解密后的赋值
originPwd, err := utils.DefaultRsaDecrypt(machineForm.Password, true)
biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
me.Password = originPwd
}
// 密码脱敏记录日志
machineForm.Password = "****"
machineForm.Password = "******"
rc.ReqParam = machineForm
me.SetBaseInfo(rc.LoginAccount)
m.MachineApp.Save(me)
}
// 获取机器实例密码,由于数据库是加密存储,故提供该接口展示原文密码
func (m *Machine) GetMachinePwd(rc *ctx.ReqCtx) {
mid := GetMachineId(rc.GinCtx)
me := m.MachineApp.GetById(mid, "Password")
me.PwdDecrypt()
rc.ResData = me.Password
func (m *Machine) TestConn(rc *req.Ctx) {
g := rc.GinCtx
machineForm := new(form.MachineForm)
ginx.BindJsonAndValid(g, machineForm)
me := new(entity.Machine)
utils.Copy(me, machineForm)
m.MachineApp.TestConn(me)
}
func (m *Machine) ChangeStatus(rc *ctx.ReqCtx) {
func (m *Machine) ChangeStatus(rc *req.Ctx) {
g := rc.GinCtx
id := uint64(ginx.PathParamInt(g, "machineId"))
status := int8(ginx.PathParamInt(g, "status"))
@@ -102,19 +98,19 @@ func (m *Machine) ChangeStatus(rc *ctx.ReqCtx) {
m.MachineApp.ChangeStatus(id, status)
}
func (m *Machine) DeleteMachine(rc *ctx.ReqCtx) {
func (m *Machine) DeleteMachine(rc *req.Ctx) {
id := uint64(ginx.PathParamInt(rc.GinCtx, "machineId"))
rc.ReqParam = id
m.MachineApp.Delete(id)
}
// 关闭机器客户端
func (m *Machine) CloseCli(rc *ctx.ReqCtx) {
func (m *Machine) CloseCli(rc *req.Ctx) {
machine.DeleteCli(GetMachineId(rc.GinCtx))
}
// 获取进程列表信息
func (m *Machine) GetProcess(rc *ctx.ReqCtx) {
func (m *Machine) GetProcess(rc *req.Ctx) {
g := rc.GinCtx
cmd := "ps -aux "
sortType := g.Query("sortType")
@@ -145,7 +141,7 @@ func (m *Machine) GetProcess(rc *ctx.ReqCtx) {
}
// 终止进程
func (m *Machine) KillProcess(rc *ctx.ReqCtx) {
func (m *Machine) KillProcess(rc *req.Ctx) {
pid := rc.GinCtx.Query("pid")
biz.NotEmpty(pid, "进程id不能为空")
@@ -169,8 +165,8 @@ func (m *Machine) WsSSH(g *gin.Context) {
biz.ErrIsNilAppendErr(err, "升级websocket失败: %s")
// 权限校验
rc := ctx.NewReqCtxWithGin(g).WithRequiredPermission(ctx.NewPermission("machine:terminal"))
if err = ctx.PermissionHandler(rc); err != nil {
rc := req.NewCtxWithGin(g).WithRequiredPermission(req.NewPermission("machine:terminal"))
if err = req.PermissionHandler(rc); err != nil {
panic(biz.NewBizErr("\033[1;31m您没有权限操作该机器终端,请重新登录后再试~\033[0m"))
}
@@ -197,16 +193,16 @@ func (m *Machine) WsSSH(g *gin.Context) {
biz.ErrIsNilAppendErr(err, "\033[1;31m连接失败: %s\033[0m")
// 记录系统操作日志
rc.WithLog(ctx.NewLogInfo("机器-终端操作").WithSave(true))
rc.WithLog(req.NewLogInfo("机器-终端操作").WithSave(true))
rc.ReqParam = cli.GetMachine().GetLogDesc()
ctx.LogHandler(rc)
req.LogHandler(rc)
mts.Start()
defer mts.Stop()
}
// 获取机器终端回放记录的相应文件夹名或文件内容
func (m *Machine) MachineRecDirNames(rc *ctx.ReqCtx) {
func (m *Machine) MachineRecDirNames(rc *req.Ctx) {
readPath := rc.GinCtx.Query("path")
biz.NotEmpty(readPath, "path不能为空")
path_ := path.Join(config.Conf.Server.GetMachineRecPath(), readPath)

View File

@@ -10,8 +10,8 @@ import (
"mayfly-go/internal/machine/domain/entity"
sysApplication "mayfly-go/internal/sys/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils"
"mayfly-go/pkg/ws"
"sort"
@@ -33,13 +33,13 @@ const (
max_read_size = 1 * 1024 * 1024
)
func (m *MachineFile) MachineFiles(rc *ctx.ReqCtx) {
func (m *MachineFile) MachineFiles(rc *req.Ctx) {
g := rc.GinCtx
condition := &entity.MachineFile{MachineId: GetMachineId(g)}
rc.ResData = m.MachineFileApp.GetPageList(condition, ginx.GetPageParam(g), new([]vo.MachineFileVO))
}
func (m *MachineFile) SaveMachineFiles(rc *ctx.ReqCtx) {
func (m *MachineFile) SaveMachineFiles(rc *req.Ctx) {
g := rc.GinCtx
fileForm := new(form.MachineFileForm)
ginx.BindJsonAndValid(g, fileForm)
@@ -52,7 +52,7 @@ func (m *MachineFile) SaveMachineFiles(rc *ctx.ReqCtx) {
m.MachineFileApp.Save(entity)
}
func (m *MachineFile) DeleteFile(rc *ctx.ReqCtx) {
func (m *MachineFile) DeleteFile(rc *req.Ctx) {
g := rc.GinCtx
fid := GetMachineFileId(g)
m.MachineFileApp.Delete(fid)
@@ -60,7 +60,7 @@ func (m *MachineFile) DeleteFile(rc *ctx.ReqCtx) {
/*** sftp相关操作 */
func (m *MachineFile) CreateFile(rc *ctx.ReqCtx) {
func (m *MachineFile) CreateFile(rc *req.Ctx) {
g := rc.GinCtx
fid := GetMachineFileId(g)
@@ -79,7 +79,7 @@ func (m *MachineFile) CreateFile(rc *ctx.ReqCtx) {
}
func (m *MachineFile) ReadFileContent(rc *ctx.ReqCtx) {
func (m *MachineFile) ReadFileContent(rc *req.Ctx) {
g := rc.GinCtx
fid := GetMachineFileId(g)
readPath := g.Query("path")
@@ -109,7 +109,7 @@ func (m *MachineFile) ReadFileContent(rc *ctx.ReqCtx) {
}
}
func (m *MachineFile) GetDirEntry(rc *ctx.ReqCtx) {
func (m *MachineFile) GetDirEntry(rc *req.Ctx) {
g := rc.GinCtx
fid := GetMachineFileId(g)
readPath := g.Query("path")
@@ -134,7 +134,7 @@ func (m *MachineFile) GetDirEntry(rc *ctx.ReqCtx) {
rc.ReqParam = fmt.Sprintf("path: %s", readPath)
}
func (m *MachineFile) WriteFileContent(rc *ctx.ReqCtx) {
func (m *MachineFile) WriteFileContent(rc *req.Ctx) {
g := rc.GinCtx
fid := GetMachineFileId(g)
@@ -148,7 +148,7 @@ func (m *MachineFile) WriteFileContent(rc *ctx.ReqCtx) {
rc.ReqParam = fmt.Sprintf("%s -> 修改文件内容: %s", mi.GetLogDesc(), path)
}
func (m *MachineFile) UploadFile(rc *ctx.ReqCtx) {
func (m *MachineFile) UploadFile(rc *req.Ctx) {
g := rc.GinCtx
fid := GetMachineFileId(g)
path := g.PostForm("path")
@@ -179,7 +179,7 @@ func (m *MachineFile) UploadFile(rc *ctx.ReqCtx) {
}()
}
func (m *MachineFile) RemoveFile(rc *ctx.ReqCtx) {
func (m *MachineFile) RemoveFile(rc *req.Ctx) {
g := rc.GinCtx
fid := GetMachineFileId(g)
path := g.Query("path")

View File

@@ -8,8 +8,8 @@ import (
"mayfly-go/internal/machine/domain/entity"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ctx"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils"
"strconv"
@@ -22,13 +22,13 @@ type MachineScript struct {
TagApp tagapp.TagTree
}
func (m *MachineScript) MachineScripts(rc *ctx.ReqCtx) {
func (m *MachineScript) MachineScripts(rc *req.Ctx) {
g := rc.GinCtx
condition := &entity.MachineScript{MachineId: GetMachineId(g)}
rc.ResData = m.MachineScriptApp.GetPageList(condition, ginx.GetPageParam(g), new([]vo.MachineScriptVO))
}
func (m *MachineScript) SaveMachineScript(rc *ctx.ReqCtx) {
func (m *MachineScript) SaveMachineScript(rc *req.Ctx) {
form := new(form.MachineScriptForm)
ginx.BindJsonAndValid(rc.GinCtx, form)
rc.ReqParam = form
@@ -41,7 +41,7 @@ func (m *MachineScript) SaveMachineScript(rc *ctx.ReqCtx) {
m.MachineScriptApp.Save(machineScript)
}
func (m *MachineScript) DeleteMachineScript(rc *ctx.ReqCtx) {
func (m *MachineScript) DeleteMachineScript(rc *req.Ctx) {
msa := m.MachineScriptApp
sid := GetMachineScriptId(rc.GinCtx)
ms := msa.GetById(sid)
@@ -50,7 +50,7 @@ func (m *MachineScript) DeleteMachineScript(rc *ctx.ReqCtx) {
msa.Delete(sid)
}
func (m *MachineScript) RunMachineScript(rc *ctx.ReqCtx) {
func (m *MachineScript) RunMachineScript(rc *req.Ctx) {
g := rc.GinCtx
scriptId := GetMachineScriptId(g)
@@ -70,9 +70,7 @@ func (m *MachineScript) RunMachineScript(rc *ctx.ReqCtx) {
res, err := cli.Run(script)
// 记录请求参数
rc.ReqParam = fmt.Sprintf("[machine: %s, scriptId: %d, name: %s]", cli.GetMachine().GetLogDesc(), scriptId, ms.Name)
if err != nil {
panic(biz.NewBizErr(fmt.Sprintf("执行命令失败:%s", err.Error())))
}
biz.ErrIsNilAppendErr(err, "执行命令失败:%s")
rc.ResData = res
}

View File

@@ -1,17 +1,25 @@
package vo
import "time"
import (
"time"
)
// 授权凭证基础信息
type AuthCertBaseVO struct {
Id int `json:"id"`
Name string `json:"name"`
AuthMethod int8 `json:"authMethod"`
}
type MachineVO struct {
Id *uint64 `json:"id"`
Name *string `json:"name"`
Username *string `json:"username"`
Ip *string `json:"ip"`
Port *int `json:"port"`
AuthMethod *int8 `json:"authMethod"`
Id uint64 `json:"id"`
Name string `json:"name"`
Ip string `json:"ip"`
Port int `json:"port"`
Username string `json:"username"`
AuthCertId int `json:"authCertId"`
Status *int8 `json:"status"`
EnableSshTunnel *int8 `json:"enableSshTunnel"` // 是否启用ssh隧道
SshTunnelMachineId *uint64 `json:"sshTunnelMachineId"` // ssh隧道机器id
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
CreateTime *time.Time `json:"createTime"`
Creator *string `json:"creator"`
CreatorId *int64 `json:"creatorId"`

View File

@@ -1,11 +1,20 @@
package application
import "mayfly-go/internal/machine/infrastructure/persistence"
import (
"mayfly-go/internal/machine/infrastructure/persistence"
)
var (
machineApp Machine = newMachineApp(persistence.GetMachineRepo())
machineFileApp MachineFile = newMachineFileApp(persistence.GetMachineFileRepo(), persistence.GetMachineRepo())
machineFileApp MachineFile = newMachineFileApp(persistence.GetMachineFileRepo(), persistence.GetMachineRepo())
machineScriptApp MachineScript = newMachineScriptApp(persistence.GetMachineScriptRepo(), persistence.GetMachineRepo())
authCertApp AuthCert = newAuthCertApp(persistence.GetAuthCertRepo())
machineApp Machine = newMachineApp(
persistence.GetMachineRepo(),
GetAuthCertApp(),
)
)
func GetMachineApp() Machine {
@@ -19,3 +28,7 @@ func GetMachineFileApp() MachineFile {
func GetMachineScriptApp() MachineScript {
return machineScriptApp
}
func GetAuthCertApp() AuthCert {
return authCertApp
}

View File

@@ -0,0 +1,64 @@
package application
import (
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/domain/repository"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/model"
)
type AuthCert interface {
GetPageList(condition *entity.AuthCert, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult
Save(ac *entity.AuthCert)
GetById(id uint64) *entity.AuthCert
GetByIds(ids ...uint64) []*entity.AuthCert
DeleteById(id uint64)
}
func newAuthCertApp(authCertRepo repository.AuthCert) AuthCert {
return &authCertAppImpl{
authCertRepo: authCertRepo,
}
}
type authCertAppImpl struct {
authCertRepo repository.AuthCert
}
func (a *authCertAppImpl) GetPageList(condition *entity.AuthCert, pageParam *model.PageParam, toEntity interface{}, orderBy ...string) *model.PageResult {
return a.authCertRepo.GetPageList(condition, pageParam, toEntity)
}
func (a *authCertAppImpl) Save(ac *entity.AuthCert) {
oldAc := &entity.AuthCert{Name: ac.Name}
err := a.authCertRepo.GetByCondition(oldAc, "Id", "Name")
ac.PwdEncrypt()
if ac.Id == 0 {
biz.IsTrue(err != nil, "该凭证名已存在")
a.authCertRepo.Insert(ac)
return
}
// 如果存在该库,则校验修改的库是否为该库
if err == nil {
biz.IsTrue(oldAc.Id == ac.Id, "该凭证名已存在")
}
a.authCertRepo.Update(ac)
}
func (a *authCertAppImpl) GetById(id uint64) *entity.AuthCert {
return a.authCertRepo.GetById(id)
}
func (a *authCertAppImpl) GetByIds(ids ...uint64) []*entity.AuthCert {
return a.authCertRepo.GetByIds(ids...)
}
func (a *authCertAppImpl) DeleteById(id uint64) {
a.authCertRepo.DeleteById(id)
}

View File

@@ -14,7 +14,10 @@ type Machine interface {
// 根据条件获取账号信息
GetMachine(condition *entity.Machine, cols ...string) error
Save(entity *entity.Machine)
Save(*entity.Machine)
// 测试机器连接
TestConn(me *entity.Machine)
// 调整机器状态
ChangeStatus(id uint64, status int8)
@@ -33,16 +36,19 @@ type Machine interface {
GetCli(id uint64) *machine.Cli
// 获取ssh隧道机器连接
GetSshTunnelMachine(id uint64) *machine.SshTunnelMachine
GetSshTunnelMachine(id int) *machine.SshTunnelMachine
}
func newMachineApp(machineRepo repository.Machine) Machine {
return &machineAppImpl{machineRepo: machineRepo}
func newMachineApp(machineRepo repository.Machine, authCertApp AuthCert) Machine {
return &machineAppImpl{
machineRepo: machineRepo,
authCertApp: authCertApp,
}
}
type machineAppImpl struct {
machineRepo repository.Machine
authCertApp AuthCert
}
// 分页获取机器信息列表
@@ -54,36 +60,35 @@ func (m *machineAppImpl) Count(condition *entity.MachineQuery) int64 {
return m.machineRepo.Count(condition)
}
// 根据条件获取机器信息
func (m *machineAppImpl) Save(me *entity.Machine) {
// ’修改机器信息且密码不为空‘ or ‘新增’需要测试是否可连接
if (me.Id != 0 && me.Password != "") || me.Id == 0 {
biz.ErrIsNilAppendErr(machine.TestConn(*me, func(u uint64) *entity.Machine {
me := m.GetById(u)
me.PwdDecrypt()
return me
}), "该机器无法连接: %s")
}
oldMachine := &entity.Machine{Ip: me.Ip, Port: me.Port, Username: me.Username}
err := m.GetMachine(oldMachine)
if me.Id != 0 {
// 如果存在该库,则校验修改的库是否为该库
if err == nil {
biz.IsTrue(oldMachine.Id == me.Id, "该机器信息已存在")
}
// 关闭连接
machine.DeleteCli(me.Id)
me.PwdEncrypt()
m.machineRepo.UpdateById(me)
} else {
me.PwdEncrypt()
if me.Id == 0 {
biz.IsTrue(err != nil, "该机器信息已存在")
// 新增机器,默认启用状态
me.Status = entity.MachineStatusEnable
me.PwdEncrypt()
m.machineRepo.Create(me)
return
}
// 如果存在该库,则校验修改的库是否为该库
if err == nil {
biz.IsTrue(oldMachine.Id == me.Id, "该机器信息已存在")
}
// 关闭连接
machine.DeleteCli(me.Id)
m.machineRepo.UpdateById(me)
}
func (m *machineAppImpl) TestConn(me *entity.Machine) {
me.Id = 0
// 测试连接
biz.ErrIsNilAppendErr(machine.TestConn(*m.toMachineInfo(me), func(u uint64) *machine.Info {
return m.toMachineInfoById(u)
}), "该机器无法连接: %s")
}
func (m *machineAppImpl) ChangeStatus(id uint64, status int8) {
@@ -128,24 +133,55 @@ func (m *machineAppImpl) GetById(id uint64, cols ...string) *entity.Machine {
return m.machineRepo.GetById(id, cols...)
}
func (m *machineAppImpl) GetCli(id uint64) *machine.Cli {
cli, err := machine.GetCli(id, func(machineId uint64) *entity.Machine {
machine := m.GetById(machineId)
machine.PwdDecrypt()
biz.IsTrue(machine.Status == entity.MachineStatusEnable, "该机器已被停用")
return machine
func (m *machineAppImpl) GetCli(machineId uint64) *machine.Cli {
cli, err := machine.GetCli(machineId, func(mid uint64) *machine.Info {
return m.toMachineInfoById(mid)
})
biz.ErrIsNilAppendErr(err, "获取客户端错误: %s")
return cli
}
func (m *machineAppImpl) GetSshTunnelMachine(id uint64) *machine.SshTunnelMachine {
sshTunnel, err := machine.GetSshTunnelMachine(id, func(machineId uint64) *entity.Machine {
machine := m.GetById(machineId)
machine.PwdDecrypt()
biz.IsTrue(machine.Status == entity.MachineStatusEnable, "该机器已被停用")
return machine
func (m *machineAppImpl) GetSshTunnelMachine(machineId int) *machine.SshTunnelMachine {
sshTunnel, err := machine.GetSshTunnelMachine(machineId, func(mid uint64) *machine.Info {
return m.toMachineInfoById(mid)
})
biz.ErrIsNilAppendErr(err, "获取ssh隧道连接失败: %s")
return sshTunnel
}
// 生成机器信息根据授权凭证id填充用户密码等
func (m *machineAppImpl) toMachineInfoById(machineId uint64) *machine.Info {
me := m.GetById(machineId)
biz.IsTrue(me.Status == entity.MachineStatusEnable, "该机器已被停用")
return m.toMachineInfo(me)
}
func (m *machineAppImpl) toMachineInfo(me *entity.Machine) *machine.Info {
mi := new(machine.Info)
mi.Id = me.Id
mi.Name = me.Name
mi.Ip = me.Ip
mi.Port = me.Port
mi.Username = me.Username
mi.TagId = me.TagId
mi.TagPath = me.TagPath
mi.EnableRecorder = me.EnableRecorder
mi.SshTunnelMachineId = me.SshTunnelMachineId
if me.UseAuthCert() {
ac := m.authCertApp.GetById(uint64(me.AuthCertId))
biz.NotNil(ac, "授权凭证信息已不存在,请重新关联")
mi.AuthMethod = ac.AuthMethod
ac.PwdDecrypt()
mi.Password = ac.Password
mi.Passphrase = ac.Passphrase
} else {
mi.AuthMethod = entity.AuthCertAuthMethodPassword
if me.Id != 0 {
me.PwdDecrypt()
}
mi.Password = me.Password
}
return mi
}

View File

@@ -6,6 +6,7 @@ import (
"io/fs"
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/domain/repository"
"mayfly-go/internal/machine/infrastructure/machine"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/model"
"os"
@@ -29,7 +30,7 @@ type MachineFile interface {
Delete(id uint64)
// 获取文件关联的机器信息,主要用于记录日志使用
GetMachine(fileId uint64) *entity.Machine
GetMachine(fileId uint64) *machine.Info
/** sftp 相关操作 **/
@@ -191,7 +192,7 @@ func (m *machineFileAppImpl) getSftpCli(machineId uint64) *sftp.Client {
return GetMachineApp().GetCli(machineId).GetSftpCli()
}
func (m *machineFileAppImpl) GetMachine(fileId uint64) *entity.Machine {
func (m *machineFileAppImpl) GetMachine(fileId uint64) *machine.Info {
return GetMachineApp().GetCli(m.GetById(fileId).MachineId).GetMachine()
}

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