mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30:25 +08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98a4c92576 | ||
|
|
b1ee9b65ff | ||
|
|
99cc4c5e5e | ||
|
|
226bb8f089 | ||
|
|
37ed5134e8 | ||
|
|
0f54d4a472 | ||
|
|
64805360d6 | ||
|
|
7f69fe2ad9 | ||
|
|
f913510d3c | ||
|
|
f2d9e7786d | ||
|
|
e1afb1ed54 | ||
|
|
12f8cf0111 | ||
|
|
daa2ef5203 | ||
|
|
1e3e183930 | ||
|
|
366563a0fe | ||
|
|
577802e5ad |
22
README.md
22
README.md
@@ -1,7 +1,25 @@
|
||||
# 🌈mayfly-go
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/objs/mayfly-go" target="_blank">
|
||||
<img src="https://gitee.com/objs/mayfly-go/badge/star.svg?theme=white" alt="star"/>
|
||||
<img src="https://gitee.com/objs/mayfly-go/badge/fork.svg" alt="fork"/>
|
||||
</a>
|
||||
<a href="https://github.com/may-fly/mayfly-go" target="_blank">
|
||||
<img src="https://img.shields.io/github/stars/may-fly/mayfly-go.svg?style=social" alt="github star"/>
|
||||
<img src="https://img.shields.io/github/forks/may-fly/mayfly-go.svg?style=social" alt="github fork"/>
|
||||
</a>
|
||||
<a href="https://github.com/golang/go" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Golang-1.18%2B-yellow.svg" alt="golang"/>
|
||||
</a>
|
||||
<a href="https://cn.vuejs.org" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Vue-3.x-green.svg" alt="vue">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
### 介绍
|
||||
简单基于DDD(领域驱动设计)分层架构实现的web版 **linux、数据库(mysql postgres)、redis(单机 集群)、mongo统一管理操作平台**
|
||||
基于DDD分层实现的web版 **linux(终端 文件 脚本 进程)、数据库(mysql postgres)、redis(单机 哨兵 集群)、mongo统一管理操作平台**
|
||||
|
||||
|
||||
### 开发语言与主要框架
|
||||
@@ -10,7 +28,7 @@
|
||||
|
||||
|
||||
### 交流及问题反馈加 QQ 群
|
||||
<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?jump_from=webapi">119699946</a>
|
||||
<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=IdJSHW0jTMhmWFHBUS9a83wxtrxDDhFj&jump_from=webapi">119699946</a>
|
||||
|
||||
|
||||
### 系统相关资料
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript" src="./config.js"></script>
|
||||
<script type="application/javascript" src="./config.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<!-- <script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=wsijQt8sLXrCW71YesmispvYHitfG9gv&s=1"></script> -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.0.6",
|
||||
"@element-plus/icons-vue": "^2.0.9",
|
||||
"axios": "^0.27.2",
|
||||
"codemirror": "^5.65.5",
|
||||
"countup.js": "^2.0.7",
|
||||
"cropperjs": "^1.5.11",
|
||||
"echarts": "^5.3.3",
|
||||
"element-plus": "^2.2.10",
|
||||
"element-plus": "^2.2.14",
|
||||
"jsencrypt": "^3.2.1",
|
||||
"jsoneditor": "^9.9.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"screenfull": "^5.1.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.13.0",
|
||||
"sql-formatter": "^7.0.3",
|
||||
"sql-formatter": "^9.2.0",
|
||||
"vue": "^3.2.37",
|
||||
"vue-clipboard3": "^1.0.1",
|
||||
"vue-router": "^4.1.2",
|
||||
|
||||
@@ -28,7 +28,6 @@ export async function RsaEncrypt(value: any) {
|
||||
if (encryptor != null) {
|
||||
return encryptor.encrypt(value)
|
||||
}
|
||||
console.log(value)
|
||||
encryptor = new JSEncrypt()
|
||||
const publicKey = await getRsaPublicKey() as string;
|
||||
notBlank(publicKey, "获取公钥失败")
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface ThemeConfigState {
|
||||
terminalBackground: string;
|
||||
terminalCursor: string;
|
||||
terminalFontSize: number;
|
||||
terminalFontWeight: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ const themeConfigModule: Module<ThemeConfigState, RootStateTypes> = {
|
||||
// ssh终端cursor色
|
||||
terminalCursor: '#268F81',
|
||||
terminalFontSize: 15,
|
||||
terminalFontWeight: 'normal',
|
||||
|
||||
|
||||
/* 后端控制路由
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</el-input-number>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt15">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">字体粗细</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-select @change="setLocalThemeConfig" v-model="getThemeConfig.terminalFontWeight" size="small" style="width: 90px">
|
||||
@@ -48,7 +48,7 @@
|
||||
<el-option label="bold" value="bold"> </el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 全局主题 -->
|
||||
<el-divider content-position="left">全局主题</el-divider>
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
|
||||
<!-- <div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
|
||||
<el-icon title="菜单搜索">
|
||||
<search />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
|
||||
<el-icon title="布局设置">
|
||||
<setting />
|
||||
@@ -28,7 +28,7 @@
|
||||
<el-popover
|
||||
placement="bottom"
|
||||
trigger="click"
|
||||
v-model:visible="isShowUserNewsPopover"
|
||||
:visible="isShowUserNewsPopover"
|
||||
:width="300"
|
||||
popper-class="el-popover-pupop-user-news"
|
||||
>
|
||||
|
||||
@@ -131,6 +131,8 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 移除公钥, 方便后续重新获取
|
||||
sessionStorage.removeItem('RsaPublicKey')
|
||||
getCaptcha();
|
||||
});
|
||||
|
||||
|
||||
@@ -41,26 +41,39 @@
|
||||
v-model.trim="form.password"
|
||||
placeholder="请输入密码,修改操作可不填"
|
||||
autocomplete="new-password"
|
||||
></el-input>
|
||||
>
|
||||
<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-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="params" label="连接参数:">
|
||||
<el-input v-model="form.params" placeholder="其他连接参数,形如: key1=value1&key2=value2"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="database" label="数据库名:" required>
|
||||
<el-select
|
||||
@change="changeDatabase"
|
||||
@focus="getAllDatabase"
|
||||
v-model="databaseList"
|
||||
multiple
|
||||
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 :span="19">
|
||||
<el-select
|
||||
@change="changeDatabase"
|
||||
v-model="databaseList"
|
||||
multiple
|
||||
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="enableSshTunnel" label="SSH隧道:">
|
||||
@@ -142,6 +155,8 @@ export default defineComponent({
|
||||
enableSshTunnel: null,
|
||||
sshTunnelMachineId: null,
|
||||
},
|
||||
// 原密码
|
||||
pwd: '',
|
||||
btnLoading: false,
|
||||
rules: {
|
||||
projectId: [
|
||||
@@ -254,12 +269,14 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const getAllDatabase = async () => {
|
||||
if (state.allDatabases.length != 0) {
|
||||
return;
|
||||
}
|
||||
const reqForm = { ...state.form };
|
||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
||||
state.allDatabases = await dbApi.getAllDatabase.request(reqForm);
|
||||
ElMessage.success('获取成功, 请选择需要管理操作的数据库')
|
||||
};
|
||||
|
||||
const getDbPwd = async () => {
|
||||
state.pwd = await dbApi.getDbPwd.request({ id: state.form.id });
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
@@ -304,6 +321,7 @@ export default defineComponent({
|
||||
...toRefs(state),
|
||||
dbForm,
|
||||
getAllDatabase,
|
||||
getDbPwd,
|
||||
changeDatabase,
|
||||
getSshTunnelMachines,
|
||||
changeProject,
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<el-table-column prop="username" label="用户名" min-width="100"></el-table-column>
|
||||
|
||||
<el-table-column min-width="115" prop="creator" label="创建账号"></el-table-column>
|
||||
<el-table-column min-width="160" prop="createTime" label="创建时间">
|
||||
<el-table-column min-width="160" prop="createTime" label="创建时间" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ $filters.dateFormat(scope.row.createTime) }}
|
||||
</template>
|
||||
@@ -100,9 +100,17 @@
|
||||
|
||||
<el-button type="primary" size="small" @click="tableCreateDialog.visible = true">创建表</el-button>
|
||||
</el-row>
|
||||
<el-table v-loading="tableInfoDialog.loading" border stripe :data="tableInfoDialog.infos" size="small">
|
||||
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip></el-table-column>
|
||||
<el-table v-loading="tableInfoDialog.loading" border stripe :data="filterTableInfos" size="small">
|
||||
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip>
|
||||
<template #header>
|
||||
<el-input v-model="tableInfoDialog.tableNameSearch" size="small" placeholder="表名: 输入可过滤" clearable />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip>
|
||||
<template #header>
|
||||
<el-input v-model="tableInfoDialog.tableCommentSearch" size="small" placeholder="备注: 输入可过滤" clearable />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="tableRows"
|
||||
label="Rows"
|
||||
@@ -244,7 +252,7 @@
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
import { toRefs, reactive, onMounted, defineComponent } from 'vue';
|
||||
import { toRefs, reactive, computed, onMounted, defineComponent } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import DbEdit from './DbEdit.vue';
|
||||
@@ -317,6 +325,8 @@ export default defineComponent({
|
||||
loading: false,
|
||||
visible: false,
|
||||
infos: [],
|
||||
tableNameSearch: '',
|
||||
tableCommentSearch: '',
|
||||
},
|
||||
columnDialog: {
|
||||
visible: false,
|
||||
@@ -345,6 +355,26 @@ export default defineComponent({
|
||||
state.projects = await projectApi.accountProjects.request(null);
|
||||
});
|
||||
|
||||
const filterTableInfos = computed(() => {
|
||||
const infos = state.tableInfoDialog.infos;
|
||||
const tableNameSearch = state.tableInfoDialog.tableNameSearch;
|
||||
const tableCommentSearch = state.tableInfoDialog.tableCommentSearch;
|
||||
if (!tableNameSearch && !tableCommentSearch) {
|
||||
return infos;
|
||||
}
|
||||
return infos.filter((data: any) => {
|
||||
let tnMatch = true;
|
||||
let tcMatch = true;
|
||||
if (tableNameSearch) {
|
||||
tnMatch = data.tableName.toLowerCase().includes(tableNameSearch.toLowerCase());
|
||||
}
|
||||
if (tableCommentSearch) {
|
||||
tcMatch = data.tableComment.includes(tableCommentSearch);
|
||||
}
|
||||
return tnMatch && tcMatch;
|
||||
});
|
||||
});
|
||||
|
||||
const choose = (item: any) => {
|
||||
if (!item) {
|
||||
return;
|
||||
@@ -572,6 +602,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
filterTableInfos,
|
||||
enums,
|
||||
search,
|
||||
choose,
|
||||
|
||||
@@ -152,6 +152,10 @@
|
||||
<el-tooltip class="box-item" effect="dark" content="commit" placement="top">
|
||||
<el-link @click="onCommit" class="ml5" type="success" icon="check" :underline="false"></el-link>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="box-item" effect="dark" content="生成insert sql" placement="top">
|
||||
<el-link @click="onGenerateInsertSql" type="success" class="ml20" :underline="false">gi</el-link>
|
||||
</el-tooltip>
|
||||
</el-row>
|
||||
<el-row class="mt5">
|
||||
<el-input
|
||||
@@ -161,9 +165,14 @@
|
||||
size="small"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-popover trigger="click" :width="270" placement="right">
|
||||
<el-popover :visible="dt.selectColumnPopoverVisible" :width="320" placement="right">
|
||||
<template #reference>
|
||||
<el-link type="success" :underline="false">选择列</el-link>
|
||||
<el-link
|
||||
@click="dt.selectColumnPopoverVisible = !dt.selectColumnPopoverVisible"
|
||||
type="success"
|
||||
:underline="false"
|
||||
>选择列</el-link
|
||||
>
|
||||
</template>
|
||||
<el-table
|
||||
:data="getColumns4Map(dt.name)"
|
||||
@@ -174,6 +183,7 @@
|
||||
onConditionRowClick(event, dt);
|
||||
}
|
||||
"
|
||||
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>
|
||||
@@ -233,6 +243,34 @@
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-container>
|
||||
|
||||
<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>
|
||||
|
||||
<el-dialog @close="genSqlDialog.visible = false" v-model="genSqlDialog.visible" title="SQL" width="1000px">
|
||||
<el-input v-model="genSqlDialog.sql" type="textarea" rows="20" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -313,6 +351,20 @@ export default defineComponent({
|
||||
left: '',
|
||||
top: '',
|
||||
},
|
||||
selectColumnPopoverVisible: false,
|
||||
conditionDialog: {
|
||||
title: '',
|
||||
placeholder: '',
|
||||
columnRow: null,
|
||||
dataTab: null,
|
||||
visible: false,
|
||||
condition: '=',
|
||||
value: null,
|
||||
},
|
||||
genSqlDialog: {
|
||||
visible: false,
|
||||
sql: '',
|
||||
},
|
||||
cmOptions: {
|
||||
tabSize: 4,
|
||||
mode: 'text/x-sql',
|
||||
@@ -677,6 +729,7 @@ export default defineComponent({
|
||||
columnNames: [],
|
||||
pageNum: 1,
|
||||
count: 0,
|
||||
selectColumnPopoverVisible: false,
|
||||
};
|
||||
tab.columnNames = await getColumnNames(tableName);
|
||||
state.dataTabs[tableName] = tab;
|
||||
@@ -716,24 +769,36 @@ export default defineComponent({
|
||||
* 条件查询,点击列信息后显示输入对应的值
|
||||
*/
|
||||
const onConditionRowClick = (event: any, dataTab: any) => {
|
||||
dataTab.selectColumnPopoverVisible = false;
|
||||
const row = event[0];
|
||||
ElMessageBox.prompt(`请输入 [${row.columnName}] 的值`, '查询条件', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: `${row.columnType} ${row.columnComment}`,
|
||||
})
|
||||
.then(({ value }) => {
|
||||
if (!value) {
|
||||
value = '';
|
||||
}
|
||||
let condition = dataTab.condition;
|
||||
if (condition) {
|
||||
condition += ` AND `;
|
||||
}
|
||||
condition += `${row.columnName} = `;
|
||||
dataTab.condition = condition + wrapColumnValue(row, value);
|
||||
})
|
||||
.catch(() => {});
|
||||
state.conditionDialog.title = `请输入 [${row.columnName}] 的值`;
|
||||
state.conditionDialog.placeholder = `${row.columnType} ${row.columnComment}`;
|
||||
state.conditionDialog.columnRow = row;
|
||||
state.conditionDialog.dataTab = dataTab;
|
||||
state.conditionDialog.visible = true;
|
||||
};
|
||||
|
||||
// 确认条件
|
||||
const onConfirmCondition = () => {
|
||||
const conditionDialog = state.conditionDialog;
|
||||
const dataTab = state.conditionDialog.dataTab as any;
|
||||
let condition = dataTab.condition;
|
||||
if (condition) {
|
||||
condition += ` AND `;
|
||||
}
|
||||
const row = conditionDialog.columnRow as any;
|
||||
condition += `${row.columnName} ${conditionDialog.condition} `;
|
||||
dataTab.condition = condition + wrapColumnValue(row, 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 onRefresh = async (tableName: string) => {
|
||||
@@ -793,10 +858,10 @@ export default defineComponent({
|
||||
const getDefaultSelectSql = (tableName: string, where: string = '', orderBy: string = '', pageNum: number = 1) => {
|
||||
const baseSql = `SELECT * FROM ${tableName} ${where ? 'WHERE ' + where : ''} ${orderBy ? orderBy : ''}`;
|
||||
if (state.dbType == 'mysql') {
|
||||
return `${baseSql} LIMIT ${(pageNum - 1) * state.defalutLimit}, ${state.defalutLimit};`
|
||||
return `${baseSql} LIMIT ${(pageNum - 1) * state.defalutLimit}, ${state.defalutLimit};`;
|
||||
}
|
||||
if (state.dbType == 'postgres') {
|
||||
return `${baseSql} OFFSET ${(pageNum - 1) * state.defalutLimit} LIMIT ${state.defalutLimit};`
|
||||
return `${baseSql} OFFSET ${(pageNum - 1) * state.defalutLimit} LIMIT ${state.defalutLimit};`;
|
||||
}
|
||||
return baseSql;
|
||||
};
|
||||
@@ -963,6 +1028,38 @@ export default defineComponent({
|
||||
});
|
||||
};
|
||||
|
||||
const onGenerateInsertSql = async () => {
|
||||
const queryTab = isQueryTab();
|
||||
const datas = queryTab ? state.queryTab.selectionDatas : state.dataTabs[state.activeName].selectionDatas;
|
||||
isTrue(datas && datas.length > 0, '请先选择要生成insert语句的数据');
|
||||
const tableName = state.nowTableName;
|
||||
const columns: any = await getColumns(tableName);
|
||||
|
||||
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(wrapValueByType(data[colName]));
|
||||
}
|
||||
sqls.push(`INSERT INTO ${tableName} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
|
||||
}
|
||||
state.genSqlDialog.sql = sqls.join(';\n') + ';';
|
||||
state.genSqlDialog.visible = true;
|
||||
};
|
||||
|
||||
const wrapValueByType = (val: any) => {
|
||||
if (val == null) {
|
||||
return 'NULL';
|
||||
}
|
||||
if (typeof val == 'number') {
|
||||
return val;
|
||||
}
|
||||
return `'${val}'`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否为查询tab
|
||||
*/
|
||||
@@ -1121,6 +1218,8 @@ export default defineComponent({
|
||||
getColumnTip,
|
||||
getColumns4Map,
|
||||
onConditionRowClick,
|
||||
onConfirmCondition,
|
||||
onCancelCondition,
|
||||
changeSqlTemplate,
|
||||
deleteSql,
|
||||
saveSql,
|
||||
@@ -1137,6 +1236,7 @@ export default defineComponent({
|
||||
onDataSelectionChange,
|
||||
onDeleteData,
|
||||
onTableSortChange,
|
||||
onGenerateInsertSql,
|
||||
showExecBtns,
|
||||
closeExecBtns,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ export const dbApi = {
|
||||
dbs: Api.create("/dbs", 'get'),
|
||||
saveDb: Api.create("/dbs", 'post'),
|
||||
getAllDatabase: Api.create("/dbs/databases", 'post'),
|
||||
getDbPwd: Api.create("/dbs/{id}/pwd", 'get'),
|
||||
deleteDb: Api.create("/dbs/{id}", 'delete'),
|
||||
dumpDb: Api.create("/dbs/{id}/dump", 'post'),
|
||||
tableInfos: Api.create("/dbs/{id}/t-infos", 'get'),
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :title="tree.title" v-model="tree.visible" :close-on-click-modal="false" width="680px">
|
||||
<el-dialog :title="tree.title" v-model="tree.visible" :close-on-click-modal="false" width="50%">
|
||||
<el-progress
|
||||
v-if="uploadProgressShow"
|
||||
style="width: 90%; margin-left: 20px"
|
||||
|
||||
@@ -35,7 +35,15 @@
|
||||
v-model.trim="form.password"
|
||||
placeholder="请输入密码,修改操作可不填"
|
||||
autocomplete="new-password"
|
||||
></el-input>
|
||||
>
|
||||
<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>
|
||||
@@ -115,6 +123,7 @@ export default defineComponent({
|
||||
enableSshTunnel: null,
|
||||
sshTunnelMachineId: null,
|
||||
},
|
||||
pwd: '',
|
||||
btnLoading: false,
|
||||
rules: {
|
||||
projectId: [
|
||||
@@ -187,6 +196,10 @@ export default defineComponent({
|
||||
return state.sshTunnelMachineList.find((x: any) => x.id == machineId);
|
||||
};
|
||||
|
||||
const getPwd = async () => {
|
||||
state.pwd = await machineApi.getMachinePwd.request({ id: state.form.id });
|
||||
};
|
||||
|
||||
const changeProject = (projectId: number) => {
|
||||
for (let p of state.projects as any) {
|
||||
if (p.id == projectId) {
|
||||
@@ -238,6 +251,7 @@ export default defineComponent({
|
||||
...toRefs(state),
|
||||
machineForm,
|
||||
getSshTunnelMachines,
|
||||
getPwd,
|
||||
changeProject,
|
||||
btnOk,
|
||||
cancel,
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</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="140">
|
||||
<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}`
|
||||
@@ -68,11 +68,6 @@
|
||||
<el-table-column prop="username" label="用户名" min-width="90"></el-table-column>
|
||||
<el-table-column prop="projectName" label="项目" min-width="120"></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="250" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="ip" label="hasCli" width="70">
|
||||
<template #default="scope">
|
||||
{{ `${scope.row.hasCli ? '是' : '否'}` }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="165">
|
||||
<template #default="scope">
|
||||
{{ $filters.dateFormat(scope.row.createTime) }}
|
||||
@@ -255,6 +250,11 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const closeCli = async (row: any) => {
|
||||
await ElMessageBox.confirm(`确定关闭该机器客户端连接?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await machineApi.closeCli.request({ id: row.id });
|
||||
ElMessage.success('关闭成功');
|
||||
search();
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
:before-close="cancel"
|
||||
:show-close="true"
|
||||
:destroy-on-close="true"
|
||||
width="800px"
|
||||
width="900px"
|
||||
>
|
||||
<el-form :model="form" ref="mockDataForm" label-width="70px">
|
||||
<el-form :model="form" ref="scriptForm" label-width="70px" size="small">
|
||||
<el-form-item prop="method" label="名称">
|
||||
<el-input v-model.trim="form.name" placeholder="请输入名称"></el-input>
|
||||
</el-form-item>
|
||||
@@ -24,8 +24,23 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="params" label="参数">
|
||||
<el-input v-model="form.params" placeholder="参数数组json,若无可不填"></el-input>
|
||||
<el-row style="margin-left: 30px; margin-bottom: 5px">
|
||||
<el-button @click="onAddParam" size="small" type="success">新增占位符参数</el-button>
|
||||
</el-row>
|
||||
<el-form-item :key="param" v-for="(param, index) in params" prop="params" :label="`参数${index + 1}`">
|
||||
<el-row>
|
||||
<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" />
|
||||
<el-col :span="4"><el-input v-model="param.name" placeholder="字段名"></el-input></el-col>
|
||||
<el-divider :span="1" direction="vertical" border-style="dashed" />
|
||||
<el-col :span="4"><el-input v-model="param.placeholder" placeholder="字段说明"></el-input></el-col>
|
||||
<el-divider :span="1" direction="vertical" border-style="dashed" />
|
||||
<el-col :span="4">
|
||||
<el-input v-model="param.options" placeholder="可选值 ,分割"></el-input>
|
||||
</el-col>
|
||||
<el-divider :span="1" direction="vertical" border-style="dashed" />
|
||||
<el-col :span="2"><el-button @click="onDeleteParam(index)" size="small" type="danger">删除</el-button></el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="script" label="内容" id="content">
|
||||
@@ -35,13 +50,12 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()" :disabled="submitDisabled" size="small">关 闭</el-button>
|
||||
<el-button @click="cancel()" :disabled="submitDisabled">关 闭</el-button>
|
||||
<el-button
|
||||
v-auth="'machine:script:save'"
|
||||
type="primary"
|
||||
:loading="btnLoading"
|
||||
@click="btnOk"
|
||||
size="small"
|
||||
:disabled="submitDisabled"
|
||||
>保 存</el-button
|
||||
>
|
||||
@@ -84,41 +98,59 @@ export default defineComponent({
|
||||
},
|
||||
setup(props: any, { emit }) {
|
||||
const { isCommon, machineId } = toRefs(props);
|
||||
const mockDataForm: any = ref(null);
|
||||
const scriptForm: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
submitDisabled: false,
|
||||
params: [] as any,
|
||||
form: {
|
||||
id: null,
|
||||
name: '',
|
||||
machineId: 0,
|
||||
description: '',
|
||||
script: '',
|
||||
params: null,
|
||||
params: '',
|
||||
type: null,
|
||||
},
|
||||
btnLoading: false,
|
||||
});
|
||||
|
||||
watch(props, (newValue) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
if (!newValue.visible) {
|
||||
return;
|
||||
}
|
||||
if (newValue.data) {
|
||||
state.form = { ...newValue.data };
|
||||
if (state.form.params) {
|
||||
state.params = JSON.parse(state.form.params);
|
||||
}
|
||||
} else {
|
||||
state.form = {} as any;
|
||||
state.form.script = '';
|
||||
}
|
||||
state.dialogVisible = newValue.visible;
|
||||
});
|
||||
|
||||
const onAddParam = () => {
|
||||
state.params.push({ name: '', model: '', placeholder: '' });
|
||||
};
|
||||
|
||||
const onDeleteParam = (idx: number) => {
|
||||
state.params.splice(idx, 1);
|
||||
};
|
||||
|
||||
const btnOk = () => {
|
||||
state.form.machineId = isCommon.value ? 9999999 : (machineId.value as any);
|
||||
console.log('machineid:', machineId);
|
||||
mockDataForm.value.validate((valid: any) => {
|
||||
scriptForm.value.validate((valid: any) => {
|
||||
if (valid) {
|
||||
notEmpty(state.form.name, '名称不能为空');
|
||||
notEmpty(state.form.description, '描述不能为空');
|
||||
notEmpty(state.form.script, '内容不能为空');
|
||||
if (state.params) {
|
||||
state.form.params = JSON.stringify(state.params);
|
||||
}
|
||||
machineApi.saveScript.request(state.form).then(
|
||||
() => {
|
||||
ElMessage.success('保存成功');
|
||||
@@ -139,12 +171,15 @@ export default defineComponent({
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
emit('cancel');
|
||||
state.params = [];
|
||||
};
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
enums,
|
||||
mockDataForm,
|
||||
onAddParam,
|
||||
onDeleteParam,
|
||||
scriptForm,
|
||||
btnOk,
|
||||
cancel,
|
||||
};
|
||||
|
||||
@@ -76,7 +76,24 @@
|
||||
<el-dialog title="脚本参数" v-model="scriptParamsDialog.visible" width="400px">
|
||||
<el-form ref="paramsForm" :model="scriptParamsDialog.params" label-width="70px" size="small">
|
||||
<el-form-item v-for="item in scriptParamsDialog.paramsFormItem" :key="item.name" :prop="item.model" :label="item.name" required>
|
||||
<el-input v-model="scriptParamsDialog.params[item.model]" :placeholder="item.placeholder" autocomplete="off"></el-input>
|
||||
<el-input
|
||||
v-if="!item.options"
|
||||
v-model="scriptParamsDialog.params[item.model]"
|
||||
:placeholder="item.placeholder"
|
||||
autocomplete="off"
|
||||
clearable
|
||||
></el-input>
|
||||
<el-select
|
||||
v-else
|
||||
v-model="scriptParamsDialog.params[item.model]"
|
||||
:placeholder="item.placeholder"
|
||||
filterable
|
||||
autocomplete="off"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="option in item.options.split(',')" :key="option" :label="option" :value="option" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -88,7 +105,6 @@
|
||||
|
||||
<el-dialog title="执行结果" v-model="resultDialog.visible" width="50%">
|
||||
<div style="white-space: pre-line; padding: 10px; color: #000000">
|
||||
<!-- {{ resultDialog.result }} -->
|
||||
<el-input v-model="resultDialog.result" :rows="20" type="textarea" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -97,12 +113,12 @@
|
||||
v-if="terminalDialog.visible"
|
||||
title="终端"
|
||||
v-model="terminalDialog.visible"
|
||||
width="70%"
|
||||
width="80%"
|
||||
:close-on-click-modal="false"
|
||||
:modal="false"
|
||||
@close="closeTermnial"
|
||||
>
|
||||
<ssh-terminal ref="terminal" :cmd="terminalDialog.cmd" :machineId="terminalDialog.machineId" height="600px" />
|
||||
<ssh-terminal ref="terminal" :cmd="terminalDialog.cmd" :machineId="terminalDialog.machineId" height="560px" />
|
||||
</el-dialog>
|
||||
|
||||
<script-edit
|
||||
@@ -196,8 +212,10 @@ export default defineComponent({
|
||||
// 如果存在参数,则弹窗输入参数后执行
|
||||
if (script.params) {
|
||||
state.scriptParamsDialog.paramsFormItem = JSON.parse(script.params);
|
||||
state.scriptParamsDialog.visible = true;
|
||||
return;
|
||||
if (state.scriptParamsDialog.paramsFormItem && state.scriptParamsDialog.paramsFormItem.length > 0) {
|
||||
state.scriptParamsDialog.visible = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
run(script);
|
||||
@@ -268,8 +286,6 @@ export default defineComponent({
|
||||
const closeTermnial = () => {
|
||||
state.terminalDialog.visible = false;
|
||||
state.terminalDialog.machineId = 0;
|
||||
// const t: any = this.$refs['terminal']
|
||||
// t.closeAll()
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -295,8 +311,6 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const submitSuccess = () => {
|
||||
// this.delChoose()
|
||||
// this.search()
|
||||
getScripts();
|
||||
};
|
||||
|
||||
@@ -326,6 +340,7 @@ export default defineComponent({
|
||||
context.emit('update:machineId', null);
|
||||
context.emit('cancel');
|
||||
state.scriptTable = [];
|
||||
state.scriptParamsDialog.paramsFormItem = [];
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 { toRefs, watch, computed, reactive, defineComponent, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { nextTick, toRefs, watch, computed, reactive, defineComponent, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SshTerminal',
|
||||
@@ -27,22 +27,20 @@ export default defineComponent({
|
||||
socket: null as any,
|
||||
});
|
||||
|
||||
const resize = 1;
|
||||
const data = 2;
|
||||
const ping = 3;
|
||||
|
||||
watch(props, (newValue) => {
|
||||
state.machineId = newValue.machineId;
|
||||
state.cmd = newValue.cmd;
|
||||
state.height = newValue.height;
|
||||
if (state.machineId) {
|
||||
initSocket();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
state.machineId = props.machineId;
|
||||
state.height = props.height;
|
||||
state.cmd = props.cmd;
|
||||
if (state.machineId) {
|
||||
initSocket();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -56,13 +54,17 @@ export default defineComponent({
|
||||
return store.state.themeConfig.themeConfig;
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
initXterm();
|
||||
initSocket();
|
||||
});
|
||||
|
||||
function initXterm() {
|
||||
const term: any = new Terminal({
|
||||
fontSize: getThemeConfig.value.terminalFontSize || 15,
|
||||
// fontWeight: getThemeConfig.value.terminalFontWeight || 'normal',
|
||||
fontFamily: 'JetBrainsMono, Consolas, Menlo, Monaco',
|
||||
fontWeight: getThemeConfig.value.terminalFontWeight || 'normal',
|
||||
fontFamily: 'JetBrainsMono, monaco, Consolas, Lucida Console, monospace',
|
||||
cursorBlink: true,
|
||||
// cursorStyle: 'underline', //光标样式
|
||||
disableStdin: false,
|
||||
theme: {
|
||||
foreground: getThemeConfig.value.terminalForeground || '#7e9192', //字体
|
||||
@@ -82,6 +84,14 @@ export default defineComponent({
|
||||
try {
|
||||
// 窗口大小改变时,触发xterm的resize方法使自适应
|
||||
fitAddon.fit();
|
||||
if (state.term) {
|
||||
state.term.focus();
|
||||
send({
|
||||
type: resize,
|
||||
Cols: parseInt(state.term.cols),
|
||||
Rows: parseInt(state.term.rows),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
@@ -104,69 +114,52 @@ export default defineComponent({
|
||||
term.onData((key: any) => {
|
||||
sendCmd(key);
|
||||
});
|
||||
// 为解决窗体resize方法才会向后端发送列数和行数,所以页面加载时也要触发此方法
|
||||
send({
|
||||
type: 'resize',
|
||||
Cols: parseInt(term.cols),
|
||||
Rows: parseInt(term.rows),
|
||||
});
|
||||
// 如果有初始要执行的命令,则发送执行命令
|
||||
if (state.cmd) {
|
||||
sendCmd(state.cmd + ' \r');
|
||||
}
|
||||
}
|
||||
|
||||
let pingInterval: any;
|
||||
function initSocket() {
|
||||
state.socket = new WebSocket(`${config.baseWsUrl}/machines/${state.machineId}/terminal?token=${getSession('token')}`);
|
||||
state.socket = new WebSocket(
|
||||
`${config.baseWsUrl}/machines/${state.machineId}/terminal?token=${getSession('token')}&cols=${state.term.cols}&rows=${
|
||||
state.term.rows
|
||||
}`
|
||||
);
|
||||
|
||||
// 监听socket连接
|
||||
state.socket.onopen = open;
|
||||
state.socket.onopen = () => {
|
||||
// 如果有初始要执行的命令,则发送执行命令
|
||||
if (state.cmd) {
|
||||
sendCmd(state.cmd + ' \r');
|
||||
}
|
||||
// 开启心跳
|
||||
pingInterval = setInterval(() => {
|
||||
send({ type: ping, msg: 'ping' });
|
||||
}, 8000);
|
||||
};
|
||||
|
||||
// 监听socket错误信息
|
||||
state.socket.onerror = error;
|
||||
// 监听socket消息
|
||||
state.socket.onmessage = getMessage;
|
||||
state.socket.onerror = (e: any) => {
|
||||
console.log('连接错误', e);
|
||||
};
|
||||
|
||||
state.socket.onclose = () => {
|
||||
if (state.term) {
|
||||
state.term.writeln('\r\n\x1b[31m提示: 连接已关闭...');
|
||||
}
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval);
|
||||
}
|
||||
};
|
||||
|
||||
// 发送socket消息
|
||||
state.socket.onsend = send;
|
||||
|
||||
// 监听socket消息
|
||||
state.socket.onmessage = getMessage;
|
||||
}
|
||||
|
||||
function open() {
|
||||
console.log('socket连接成功');
|
||||
initXterm();
|
||||
//开启心跳
|
||||
// this.start();
|
||||
}
|
||||
|
||||
function error() {
|
||||
console.log('连接错误');
|
||||
//重连
|
||||
// reconnect();
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (state.socket) {
|
||||
state.socket.close();
|
||||
console.log('socket关闭');
|
||||
}
|
||||
|
||||
//重连
|
||||
// this.reconnect()
|
||||
}
|
||||
|
||||
function getMessage(msg: string) {
|
||||
// console.log(msg)
|
||||
state.term.write(msg['data']);
|
||||
//msg是返回的数据
|
||||
// msg = JSON.parse(msg.data);
|
||||
// this.socket.send("ping");//有事没事ping一下,看看ws还活着没
|
||||
// //switch用于处理返回的数据,根据返回数据的格式去判断
|
||||
// switch (msg["operation"]) {
|
||||
// case "stdout":
|
||||
// this.term.write(msg["data"]);//这里write也许不是固定的,失败后找后端看一下该怎么往term里面write
|
||||
// break;
|
||||
// default:
|
||||
// console.error("Unexpected message type:", msg);//但是错误是固定的。。。。
|
||||
// }
|
||||
//收到服务器信息,心跳重置
|
||||
// this.reset();
|
||||
function getMessage(msg: any) {
|
||||
// msg.data是真正后端返回的数据
|
||||
state.term.write(msg.data);
|
||||
}
|
||||
|
||||
function send(msg: any) {
|
||||
@@ -175,11 +168,18 @@ export default defineComponent({
|
||||
|
||||
function sendCmd(key: any) {
|
||||
send({
|
||||
type: 'cmd',
|
||||
type: data,
|
||||
msg: key,
|
||||
});
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (state.socket) {
|
||||
state.socket.close();
|
||||
console.log('socket关闭');
|
||||
}
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
close();
|
||||
if (state.term) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import Api from '@/common/Api';
|
||||
export const machineApi = {
|
||||
// 获取权限列表
|
||||
list: Api.create("/machines", 'get'),
|
||||
getMachinePwd: Api.create("/machines/{id}/pwd", 'get'),
|
||||
info: Api.create("/machines/{id}/sysinfo", 'get'),
|
||||
stats: Api.create("/machines/{id}/stats", 'get'),
|
||||
process: Api.create("/machines/{id}/process", 'get'),
|
||||
|
||||
@@ -61,7 +61,6 @@ import { mongoApi } from './api';
|
||||
import { projectApi } from '../project/api.ts';
|
||||
import { machineApi } from '../machine/api.ts';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { RsaEncrypt } from '@/common/rsa';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MongoEdit',
|
||||
@@ -181,7 +180,7 @@ export default defineComponent({
|
||||
mongoForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const reqForm = { ...state.form };
|
||||
reqForm.uri = await RsaEncrypt(reqForm.uri);
|
||||
// reqForm.uri = await RsaEncrypt(reqForm.uri);
|
||||
mongoApi.saveMongo.request(reqForm).then(() => {
|
||||
ElMessage.success('保存成功');
|
||||
emit('val-change', state.form);
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="projects" @current-change="choose" ref="table" style="width: 100%">
|
||||
<el-table-column label="选择" width="50px">
|
||||
<el-table-column label="选择" width="55px">
|
||||
<template #default="scope">
|
||||
<el-radio v-model="chooseId" :label="scope.row.id">
|
||||
<i></i>
|
||||
|
||||
@@ -1,313 +0,0 @@
|
||||
<template>
|
||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :show-close="false" width="750px" :destroy-on-close="true">
|
||||
<el-form label-width="85px">
|
||||
<el-form-item prop="key" label="key:">
|
||||
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="timed" label="过期时间:">
|
||||
<el-input v-model.number="key.timed" type="number"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="dataType" label="数据类型:">
|
||||
<el-select :disabled="operationType == 2" style="width: 100%" v-model="key.type" placeholder="请选择数据类型">
|
||||
<el-option key="string" label="string" value="string"> </el-option>
|
||||
<el-option key="hash" label="hash" value="hash"> </el-option>
|
||||
<el-option key="set" label="set" value="set"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="key.type == 'string'" prop="value" label="内容:">
|
||||
<div id="string-value-text" style="width: 100%">
|
||||
<el-input class="json-text" v-model="string.value" type="textarea" :autosize="{ minRows: 10, maxRows: 20 }"></el-input>
|
||||
<el-select class="text-type-select" @change="onChangeTextType" v-model="string.type">
|
||||
<el-option key="text" label="text" value="text"> </el-option>
|
||||
<el-option key="json" label="json" value="json"> </el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<span v-if="key.type == 'hash'">
|
||||
<el-button @click="onAddHashValue" icon="plus" size="small" plain class="mt10">添加</el-button>
|
||||
<el-table :data="hash.value" stripe style="width: 100%">
|
||||
<el-table-column prop="key" label="key" width>
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.key" clearable size="small"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="value" label="value" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.value"
|
||||
clearable
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 10 }"
|
||||
size="small"
|
||||
></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90">
|
||||
<template #default="scope">
|
||||
<el-button type="danger" @click="hash.value.splice(scope.$index, 1)" icon="delete" size="small" plain>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</span>
|
||||
|
||||
<span v-if="key.type == 'set'">
|
||||
<el-button @click="onAddSetValue" icon="plus" size="small" plain class="mt10">添加</el-button>
|
||||
<el-table :data="set.value" stripe style="width: 100%">
|
||||
<el-table-column prop="value" label="value" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.value"
|
||||
clearable
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 10 }"
|
||||
size="small"
|
||||
></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90">
|
||||
<template #default="scope">
|
||||
<el-button type="danger" @click="set.value.splice(scope.$index, 1)" icon="delete" size="small" plain>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</span>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, watch, toRefs } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { isTrue, notEmpty } from '@/common/assert';
|
||||
import { formatJsonString } from '@/common/utils/format';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DateEdit',
|
||||
components: {},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
// 操作类型,1:新增,2:修改
|
||||
operationType: {
|
||||
type: [Number],
|
||||
},
|
||||
stringValue: {
|
||||
type: [String],
|
||||
},
|
||||
setValue: {
|
||||
type: [Array, Object],
|
||||
},
|
||||
hashValue: {
|
||||
type: [Array, Object],
|
||||
},
|
||||
},
|
||||
emits: ['valChange', 'cancel', 'update:visible'],
|
||||
setup(props: any, { emit }) {
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
operationType: 1,
|
||||
redisId: '',
|
||||
key: {
|
||||
key: '',
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
},
|
||||
string: {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
hash: {
|
||||
value: [
|
||||
{
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
set: {
|
||||
value: [{ value: '' }],
|
||||
},
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
emit('cancel');
|
||||
setTimeout(() => {
|
||||
state.key = {
|
||||
key: '',
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
};
|
||||
state.string.value = '';
|
||||
state.string.type = 'text';
|
||||
state.hash.value = [
|
||||
{
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
];
|
||||
}, 500);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
state.dialogVisible = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.redisId,
|
||||
(val) => {
|
||||
state.redisId = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.operationType,
|
||||
(val) => {
|
||||
state.operationType = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.keyInfo,
|
||||
(val) => {
|
||||
if (val) {
|
||||
state.key = { ...val };
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true, // 深度监听的参数
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.stringValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
state.string.value = val;
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true, // 深度监听的参数
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.setValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
state.set.value = val;
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true, // 深度监听的参数
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.hashValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
state.hash.value = val;
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true, // 深度监听的参数
|
||||
}
|
||||
);
|
||||
|
||||
const saveValue = async () => {
|
||||
notEmpty(state.key.key, 'key不能为空');
|
||||
|
||||
if (state.key.type == 'string') {
|
||||
notEmpty(state.string.value, 'value不能为空');
|
||||
const sv = { value: formatJsonString(state.string.value, true), id: state.redisId };
|
||||
Object.assign(sv, state.key);
|
||||
await redisApi.saveStringValue.request(sv);
|
||||
}
|
||||
|
||||
if (state.key.type == 'hash') {
|
||||
isTrue(state.hash.value.length > 0, 'hash内容不能为空');
|
||||
const sv = { value: state.hash.value, id: state.redisId };
|
||||
Object.assign(sv, state.key);
|
||||
await redisApi.saveHashValue.request(sv);
|
||||
}
|
||||
|
||||
if (state.key.type == 'set') {
|
||||
isTrue(state.set.value.length > 0, 'set内容不能为空');
|
||||
const sv = { value: state.set.value.map((x) => x.value), id: state.redisId };
|
||||
Object.assign(sv, state.key);
|
||||
await redisApi.saveSetValue.request(sv);
|
||||
}
|
||||
|
||||
ElMessage.success('数据保存成功');
|
||||
cancel();
|
||||
emit('valChange');
|
||||
};
|
||||
|
||||
const onAddHashValue = () => {
|
||||
state.hash.value.push({ key: '', value: '' });
|
||||
};
|
||||
|
||||
const onAddSetValue = () => {
|
||||
state.set.value.push({ value: '' });
|
||||
};
|
||||
|
||||
// 更改文本类型
|
||||
const onChangeTextType = (val: string) => {
|
||||
if (val == 'json') {
|
||||
state.string.value = formatJsonString(state.string.value, false);
|
||||
return;
|
||||
}
|
||||
if (val == 'text') {
|
||||
state.string.value = formatJsonString(state.string.value, true);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
saveValue,
|
||||
cancel,
|
||||
onAddHashValue,
|
||||
onAddSetValue,
|
||||
onChangeTextType,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#string-value-text {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.text-type-select {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
max-width: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -23,7 +23,7 @@
|
||||
<el-form class="search-form" label-position="right" :inline="true" label-width="60px">
|
||||
<el-form-item label="key" label-width="40px">
|
||||
<el-input
|
||||
placeholder="支持*模糊key"
|
||||
placeholder="match 支持*模糊key"
|
||||
style="width: 240px"
|
||||
v-model="scanParam.match"
|
||||
@clear="clear()"
|
||||
@@ -36,7 +36,14 @@
|
||||
<el-form-item>
|
||||
<el-button @click="searchKey()" type="success" icon="search" plain></el-button>
|
||||
<el-button @click="scan()" icon="bottom" plain>scan</el-button>
|
||||
<el-button type="primary" icon="plus" @click="onAddData(false)" plain></el-button>
|
||||
<el-popover placement="right" :width="200" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="primary" icon="plus" plain></el-button>
|
||||
</template>
|
||||
<el-tag @click="onAddData('string')" :color="getTypeColor('string')" style="cursor: pointer">string</el-tag>
|
||||
<el-tag @click="onAddData('hash')" :color="getTypeColor('hash')" class="ml5" style="cursor: pointer">hash</el-tag>
|
||||
<el-tag @click="onAddData('set')" :color="getTypeColor('set')" class="ml5" style="cursor: pointer">set</el-tag>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
<div style="float: right">
|
||||
<span>keys: {{ dbsize }}</span>
|
||||
@@ -69,17 +76,32 @@
|
||||
|
||||
<div style="text-align: center; margin-top: 10px"></div>
|
||||
|
||||
<!-- <value-dialog v-model:visible="valueDialog.visible" :keyValue="valueDialog.value" /> -->
|
||||
<hash-value
|
||||
v-model:visible="hashValueDialog.visible"
|
||||
:operationType="dataEdit.operationType"
|
||||
:title="dataEdit.title"
|
||||
:keyInfo="dataEdit.keyInfo"
|
||||
:redisId="scanParam.id"
|
||||
@cancel="onCancelDataEdit"
|
||||
@valChange="searchKey"
|
||||
/>
|
||||
|
||||
<data-edit
|
||||
v-model:visible="dataEdit.visible"
|
||||
<string-value
|
||||
v-model:visible="stringValueDialog.visible"
|
||||
:operationType="dataEdit.operationType"
|
||||
:title="dataEdit.title"
|
||||
:keyInfo="dataEdit.keyInfo"
|
||||
:redisId="scanParam.id"
|
||||
@cancel="onCancelDataEdit"
|
||||
@valChange="searchKey"
|
||||
/>
|
||||
|
||||
<set-value
|
||||
v-model:visible="setValueDialog.visible"
|
||||
:title="dataEdit.title"
|
||||
:keyInfo="dataEdit.keyInfo"
|
||||
:redisId="scanParam.id"
|
||||
:operationType="dataEdit.operationType"
|
||||
:stringValue="dataEdit.stringValue"
|
||||
:setValue="dataEdit.setValue"
|
||||
:hashValue="dataEdit.hashValue"
|
||||
@valChange="searchKey"
|
||||
@cancel="onCancelDataEdit"
|
||||
/>
|
||||
@@ -91,13 +113,17 @@ import { redisApi } from './api';
|
||||
import { toRefs, reactive, defineComponent } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import ProjectEnvSelect from '../component/ProjectEnvSelect.vue';
|
||||
import DataEdit from './DataEdit.vue';
|
||||
import HashValue from './HashValue.vue';
|
||||
import StringValue from './StringValue.vue';
|
||||
import SetValue from './SetValue.vue';
|
||||
import { isTrue, notBlank, notNull } from '@/common/assert';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DataOperation',
|
||||
components: {
|
||||
DataEdit,
|
||||
StringValue,
|
||||
HashValue,
|
||||
SetValue,
|
||||
ProjectEnvSelect,
|
||||
},
|
||||
setup() {
|
||||
@@ -113,10 +139,6 @@ export default defineComponent({
|
||||
count: 10,
|
||||
cursor: {},
|
||||
},
|
||||
valueDialog: {
|
||||
visible: false,
|
||||
value: {},
|
||||
},
|
||||
dataEdit: {
|
||||
visible: false,
|
||||
title: '新增数据',
|
||||
@@ -126,9 +148,15 @@ export default defineComponent({
|
||||
timed: -1,
|
||||
key: '',
|
||||
},
|
||||
stringValue: '',
|
||||
hashValue: [{ key: '', value: '' }],
|
||||
setValue: [{ value: '' }],
|
||||
},
|
||||
hashValueDialog: {
|
||||
visible: false,
|
||||
},
|
||||
stringValueDialog: {
|
||||
visible: false,
|
||||
},
|
||||
setValueDialog: {
|
||||
visible: false,
|
||||
},
|
||||
keys: [],
|
||||
dbsize: 0,
|
||||
@@ -158,10 +186,15 @@ export default defineComponent({
|
||||
const scan = async () => {
|
||||
isTrue(state.scanParam.id != null, '请先选择redis');
|
||||
notBlank(state.scanParam.count, 'count不能为空');
|
||||
isTrue(state.scanParam.count < 20001, 'count不能超过20000');
|
||||
|
||||
const match = state.scanParam.match;
|
||||
if (!match || match == '*') {
|
||||
isTrue(state.scanParam.count <= 200, 'match为空或者*时, count不能超过200');
|
||||
} else {
|
||||
isTrue(state.scanParam.count <= 20000, 'count不能超过20000');
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
|
||||
try {
|
||||
const res = await redisApi.scan.request(state.scanParam);
|
||||
state.keys = res.keys;
|
||||
@@ -207,60 +240,43 @@ export default defineComponent({
|
||||
|
||||
const getValue = async (row: any) => {
|
||||
const type = row.type;
|
||||
const key = row.key;
|
||||
|
||||
let res: any;
|
||||
const reqParam = {
|
||||
key: row.key,
|
||||
id: state.scanParam.id,
|
||||
};
|
||||
switch (type) {
|
||||
case 'string':
|
||||
res = await redisApi.getStringValue.request(reqParam);
|
||||
break;
|
||||
case 'hash':
|
||||
res = await redisApi.getHashValue.request(reqParam);
|
||||
break;
|
||||
case 'set':
|
||||
res = await redisApi.getSetValue.request(reqParam);
|
||||
break;
|
||||
default:
|
||||
res = null;
|
||||
break;
|
||||
}
|
||||
notNull(res, '暂不支持该类型数据查看');
|
||||
|
||||
if (type == 'string') {
|
||||
state.dataEdit.stringValue = res;
|
||||
}
|
||||
if (type == 'set') {
|
||||
state.dataEdit.setValue = res.map((x: any) => {
|
||||
return {
|
||||
value: x,
|
||||
};
|
||||
});
|
||||
}
|
||||
if (type == 'hash') {
|
||||
const hash = [];
|
||||
//遍历key和value
|
||||
const keys = Object.keys(res);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const value = res[key];
|
||||
hash.push({
|
||||
key,
|
||||
value,
|
||||
});
|
||||
}
|
||||
state.dataEdit.hashValue = hash;
|
||||
}
|
||||
|
||||
state.dataEdit.keyInfo.type = type;
|
||||
state.dataEdit.keyInfo.timed = row.ttl;
|
||||
state.dataEdit.keyInfo.key = key;
|
||||
state.dataEdit.keyInfo.key = row.key;
|
||||
state.dataEdit.operationType = 2;
|
||||
state.dataEdit.title = '修改数据';
|
||||
state.dataEdit.visible = true;
|
||||
state.dataEdit.title = '查看数据';
|
||||
|
||||
if (type == 'hash') {
|
||||
state.hashValueDialog.visible = true;
|
||||
} else if (type == 'string') {
|
||||
state.stringValueDialog.visible = true;
|
||||
} else if (type == 'set') {
|
||||
state.setValueDialog.visible = true;
|
||||
} else {
|
||||
ElMessage.warning('暂不支持该类型');
|
||||
}
|
||||
};
|
||||
|
||||
const onAddData = (type: string) => {
|
||||
notNull(state.scanParam.id, '请先选择redis');
|
||||
state.dataEdit.operationType = 1;
|
||||
state.dataEdit.title = '新增数据';
|
||||
state.dataEdit.keyInfo.type = type;
|
||||
state.dataEdit.keyInfo.timed = -1;
|
||||
if (type == 'hash') {
|
||||
state.hashValueDialog.visible = true;
|
||||
} else if (type == 'string') {
|
||||
state.stringValueDialog.visible = true;
|
||||
} else if (type == 'set') {
|
||||
state.setValueDialog.visible = true;
|
||||
} else {
|
||||
ElMessage.warning('暂不支持该类型');
|
||||
}
|
||||
};
|
||||
|
||||
const onCancelDataEdit = () => {
|
||||
state.dataEdit.keyInfo = {} as any;
|
||||
};
|
||||
|
||||
const del = (key: string) => {
|
||||
@@ -331,20 +347,6 @@ export default defineComponent({
|
||||
}
|
||||
};
|
||||
|
||||
const onAddData = () => {
|
||||
notNull(state.scanParam.id, '请先选择redis');
|
||||
state.dataEdit.operationType = 1;
|
||||
state.dataEdit.title = '新增数据';
|
||||
state.dataEdit.visible = true;
|
||||
};
|
||||
|
||||
const onCancelDataEdit = () => {
|
||||
state.dataEdit.keyInfo = {} as any;
|
||||
state.dataEdit.stringValue = '';
|
||||
state.dataEdit.setValue = [];
|
||||
state.dataEdit.hashValue = [];
|
||||
};
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
changeProjectEnv,
|
||||
|
||||
254
mayfly_go_web/src/views/ops/redis/HashValue.vue
Normal file
254
mayfly_go_web/src/views/ops/redis/HashValue.vue
Normal file
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
|
||||
<el-form label-width="85px">
|
||||
<el-form-item prop="key" label="key:">
|
||||
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="timed" label="过期时间:">
|
||||
<el-input v-model.number="key.timed" type="number"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="dataType" label="数据类型:">
|
||||
<el-input v-model="key.type" disabled></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-row class="mt10">
|
||||
<el-form label-position="right" :inline="true">
|
||||
<el-form-item label="field" label-width="40px" v-if="operationType == 2">
|
||||
<el-input placeholder="支持*模糊field" style="width: 140px" v-model="scanParam.match" clearable size="small"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="count" v-if="operationType == 2">
|
||||
<el-input placeholder="count" style="width: 62px" v-model.number="scanParam.count" size="small"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="operationType == 2" @click="reHscan()" type="success" icon="search" plain size="small"></el-button>
|
||||
<el-button v-if="operationType == 2" @click="hscan()" icon="bottom" plain size="small">scan</el-button>
|
||||
<el-button @click="onAddHashValue" icon="plus" size="small" plain>添加</el-button>
|
||||
</el-form-item>
|
||||
<div v-if="operationType == 2" class="mt10" style="float: right">
|
||||
<span>fieldSize: {{ keySize }}</span>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<el-table :data="hashValues" stripe style="width: 100%">
|
||||
<el-table-column prop="field" label="field" width>
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.field" clearable size="small"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="value" label="value" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.value" clearable type="textarea" :autosize="{ minRows: 2, maxRows: 10 }" size="small"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-button v-if="operationType == 2" type="success" @click="hset(scope.row)" icon="check" size="small" plain></el-button>
|
||||
<el-button type="danger" @click="hdel(scope.row.field, scope.$index)" icon="delete" size="small" plain></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form>
|
||||
<template #footer v-if="operationType == 1">
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, watch, toRefs } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { isTrue, notEmpty } from '@/common/assert';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HashValue',
|
||||
components: {},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
// 操作类型,1:新增,2:修改
|
||||
operationType: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
},
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
hashValue: {
|
||||
type: [Array, Object],
|
||||
},
|
||||
},
|
||||
emits: ['valChange', 'cancel', 'update:visible'],
|
||||
setup(props: any, { emit }) {
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
operationType: 1,
|
||||
redisId: 0,
|
||||
key: {
|
||||
key: '',
|
||||
type: 'hash',
|
||||
timed: -1,
|
||||
},
|
||||
scanParam: {
|
||||
key: '',
|
||||
id: 0,
|
||||
cursor: 0,
|
||||
match: '',
|
||||
count: 10,
|
||||
},
|
||||
keySize: 0,
|
||||
hashValues: [
|
||||
{
|
||||
field: '',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
emit('cancel');
|
||||
setTimeout(() => {
|
||||
state.hashValues = [];
|
||||
state.key = {} as any;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
watch(props, async (newValue) => {
|
||||
const visible = newValue.visible;
|
||||
state.redisId = newValue.redisId;
|
||||
state.key = newValue.keyInfo;
|
||||
state.operationType = newValue.operationType;
|
||||
|
||||
if (visible && state.operationType == 2) {
|
||||
state.scanParam.id = props.redisId;
|
||||
state.scanParam.key = state.key.key;
|
||||
await reHscan();
|
||||
}
|
||||
|
||||
state.dialogVisible = visible;
|
||||
});
|
||||
|
||||
const reHscan = async () => {
|
||||
state.scanParam.id = state.redisId;
|
||||
state.scanParam.cursor = 0;
|
||||
hscan();
|
||||
};
|
||||
|
||||
const hscan = async () => {
|
||||
const match = state.scanParam.match;
|
||||
if (!match || match == '' || match == '*') {
|
||||
if (state.scanParam.count > 100) {
|
||||
ElMessage.error('match为空或者*时, count不能超过100');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (state.scanParam.count > 1000) {
|
||||
ElMessage.error('count不能超过1000');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const scanRes = await redisApi.hscan.request(state.scanParam);
|
||||
state.scanParam.cursor = scanRes.cursor;
|
||||
state.keySize = scanRes.keySize;
|
||||
|
||||
const keys = scanRes.keys;
|
||||
const hashValue = [];
|
||||
const fieldCount = keys.length / 2;
|
||||
let nextFieldIndex = 0;
|
||||
for (let i = 0; i < fieldCount; i++) {
|
||||
hashValue.push({ field: keys[nextFieldIndex++], value: keys[nextFieldIndex++] });
|
||||
}
|
||||
state.hashValues = hashValue;
|
||||
};
|
||||
|
||||
const hdel = async (field: any, index: any) => {
|
||||
// 如果是新增操作,则直接数组移除即可
|
||||
if (state.operationType == 1) {
|
||||
state.hashValues.splice(index, 1);
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除[${field}]?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await redisApi.hdel.request({
|
||||
id: state.redisId,
|
||||
key: state.key.key,
|
||||
field,
|
||||
});
|
||||
ElMessage.success('删除成功');
|
||||
reHscan();
|
||||
};
|
||||
|
||||
const hset = async (row: any) => {
|
||||
await redisApi.saveHashValue.request({
|
||||
id: state.redisId,
|
||||
key: state.key.key,
|
||||
timed: state.key.timed,
|
||||
value: [
|
||||
{
|
||||
field: row.field,
|
||||
value: row.value,
|
||||
},
|
||||
],
|
||||
});
|
||||
ElMessage.success('保存成功');
|
||||
};
|
||||
|
||||
const onAddHashValue = () => {
|
||||
state.hashValues.unshift({ field: '', value: '' });
|
||||
};
|
||||
|
||||
const saveValue = async () => {
|
||||
notEmpty(state.key.key, 'key不能为空');
|
||||
isTrue(state.hashValues.length > 0, 'hash内容不能为空');
|
||||
const sv = { value: state.hashValues, id: state.redisId };
|
||||
Object.assign(sv, state.key);
|
||||
await redisApi.saveHashValue.request(sv);
|
||||
ElMessage.success('保存成功');
|
||||
|
||||
cancel();
|
||||
emit('valChange');
|
||||
};
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
reHscan,
|
||||
hscan,
|
||||
cancel,
|
||||
hdel,
|
||||
hset,
|
||||
onAddHashValue,
|
||||
saveValue,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#string-value-text {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.text-type-select {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
max-width: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -17,12 +17,13 @@
|
||||
<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:port,集群模式用','分割"
|
||||
placeholder="请输入host:port;sentinel模式为: mastername=sentinelhost:port,若集群或哨兵需设多个节点可使用','分割"
|
||||
auto-complete="off"
|
||||
type="textarea"
|
||||
></el-input>
|
||||
@@ -34,7 +35,14 @@
|
||||
v-model.trim="form.password"
|
||||
placeholder="请输入密码, 修改操作可不填"
|
||||
autocomplete="new-password"
|
||||
></el-input>
|
||||
><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-input v-model.number="form.db" placeholder="请输入库号"></el-input>
|
||||
@@ -106,7 +114,7 @@ export default defineComponent({
|
||||
id: null,
|
||||
name: null,
|
||||
mode: 'standalone',
|
||||
host: null,
|
||||
host: '',
|
||||
password: null,
|
||||
project: null,
|
||||
projectId: null,
|
||||
@@ -116,6 +124,7 @@ export default defineComponent({
|
||||
enableSshTunnel: null,
|
||||
sshTunnelMachineId: null,
|
||||
},
|
||||
pwd: '',
|
||||
btnLoading: false,
|
||||
rules: {
|
||||
projectId: [
|
||||
@@ -183,6 +192,10 @@ export default defineComponent({
|
||||
state.envs = await projectApi.projectEnvs.request({ projectId });
|
||||
};
|
||||
|
||||
const getPwd = async () => {
|
||||
state.pwd = await redisApi.getRedisPwd.request({ id: state.form.id });
|
||||
};
|
||||
|
||||
const changeProject = (projectId: number) => {
|
||||
for (let p of state.projects as any) {
|
||||
if (p.id == projectId) {
|
||||
@@ -207,6 +220,10 @@ export default defineComponent({
|
||||
redisForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const reqForm = { ...state.form };
|
||||
if (reqForm.mode == 'sentinel' && reqForm.host.split('=').length != 2) {
|
||||
ElMessage.error('sentinel模式host需为: mastername=sentinelhost:sentinelport模式');
|
||||
return;
|
||||
}
|
||||
reqForm.password = await RsaEncrypt(reqForm.password);
|
||||
redisApi.saveRedis.request(reqForm).then(() => {
|
||||
ElMessage.success('保存成功');
|
||||
@@ -234,6 +251,7 @@ export default defineComponent({
|
||||
...toRefs(state),
|
||||
redisForm,
|
||||
getSshTunnelMachines,
|
||||
getPwd,
|
||||
changeProject,
|
||||
changeEnv,
|
||||
btnOk,
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<el-table-column prop="creator" label="创建人" min-width="100"></el-table-column>
|
||||
<el-table-column label="更多" min-width="130" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-link v-if="scope.row.mode == 'standalone'" type="primary" @click="info(scope.row)" :underline="false">单机信息</el-link>
|
||||
<el-link v-if="scope.row.mode == 'standalone' || scope.row.mode == 'sentinel'" type="primary" @click="info(scope.row)" :underline="false">单机信息</el-link>
|
||||
<el-link @click="onShowClusterInfo(scope.row)" v-if="scope.row.mode == 'cluster'" type="success" :underline="false"
|
||||
>集群信息</el-link
|
||||
>
|
||||
|
||||
157
mayfly_go_web/src/views/ops/redis/SetValue.vue
Normal file
157
mayfly_go_web/src/views/ops/redis/SetValue.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
|
||||
<el-form label-width="85px">
|
||||
<el-form-item prop="key" label="key:">
|
||||
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="timed" label="过期时间:">
|
||||
<el-input v-model.number="key.timed" type="number"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="dataType" label="数据类型:">
|
||||
<el-input v-model="key.type" disabled></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-button @click="onAddSetValue" icon="plus" size="small" plain class="mt10">添加</el-button>
|
||||
<el-table :data="value" stripe style="width: 100%">
|
||||
<el-table-column prop="value" label="value" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.value" clearable type="textarea" :autosize="{ minRows: 2, maxRows: 10 }" size="small"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90">
|
||||
<template #default="scope">
|
||||
<el-button type="danger" @click="set.value.splice(scope.$index, 1)" icon="delete" size="small" plain>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, watch, toRefs } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { isTrue, notEmpty } from '@/common/assert';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SetValue',
|
||||
components: {},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
// 操作类型,1:新增,2:修改
|
||||
operationType: {
|
||||
type: [Number],
|
||||
},
|
||||
setValue: {
|
||||
type: [Array, Object],
|
||||
},
|
||||
},
|
||||
emits: ['valChange', 'cancel', 'update:visible'],
|
||||
setup(props: any, { emit }) {
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
operationType: 1,
|
||||
redisId: '',
|
||||
key: {
|
||||
key: '',
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
},
|
||||
value: [{ value: '' }],
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
emit('cancel');
|
||||
setTimeout(() => {
|
||||
state.key = {
|
||||
key: '',
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
};
|
||||
state.value = [];
|
||||
}, 500);
|
||||
};
|
||||
|
||||
watch(props, async (newValue) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
state.key = newValue.key;
|
||||
state.redisId = newValue.redisId;
|
||||
state.key = newValue.keyInfo;
|
||||
state.operationType = newValue.operationType;
|
||||
// 如果是查看编辑操作,则获取值
|
||||
if (state.dialogVisible && state.operationType == 2) {
|
||||
getSetValue();
|
||||
}
|
||||
});
|
||||
|
||||
const getSetValue = async () => {
|
||||
const res = await redisApi.getSetValue.request({
|
||||
id: state.redisId,
|
||||
key: state.key.key,
|
||||
});
|
||||
state.value = res.map((x: any) => {
|
||||
return {
|
||||
value: x,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const saveValue = async () => {
|
||||
notEmpty(state.key.key, 'key不能为空');
|
||||
isTrue(state.value.length > 0, 'set内容不能为空');
|
||||
const sv = { value: state.value.map((x) => x.value), id: state.redisId };
|
||||
Object.assign(sv, state.key);
|
||||
await redisApi.saveSetValue.request(sv);
|
||||
|
||||
ElMessage.success('数据保存成功');
|
||||
cancel();
|
||||
emit('valChange');
|
||||
};
|
||||
|
||||
const onAddSetValue = () => {
|
||||
state.value.unshift({ value: '' });
|
||||
};
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
saveValue,
|
||||
cancel,
|
||||
onAddSetValue,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#string-value-text {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.text-type-select {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
max-width: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
169
mayfly_go_web/src/views/ops/redis/StringValue.vue
Normal file
169
mayfly_go_web/src/views/ops/redis/StringValue.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="800px" :destroy-on-close="true">
|
||||
<el-form label-width="85px">
|
||||
<el-form-item prop="key" label="key:">
|
||||
<el-input :disabled="operationType == 2" v-model="key.key"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="timed" label="过期时间:">
|
||||
<el-input v-model.number="key.timed" type="number"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="dataType" label="数据类型:">
|
||||
<el-input v-model="key.type" disabled></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<div id="string-value-text" style="width: 100%">
|
||||
<el-input class="json-text" v-model="string.value" type="textarea" :autosize="{ minRows: 10, maxRows: 20 }"></el-input>
|
||||
<el-select class="text-type-select" @change="onChangeTextType" v-model="string.type">
|
||||
<el-option key="text" label="text" value="text"> </el-option>
|
||||
<el-option key="json" label="json" value="json"> </el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button @click="saveValue" type="primary" v-auth="'redis:data:save'">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, watch, toRefs } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { notEmpty } from '@/common/assert';
|
||||
import { formatJsonString } from '@/common/utils/format';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'StringValue',
|
||||
components: {},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
redisId: {
|
||||
type: [Number],
|
||||
require: true,
|
||||
},
|
||||
keyInfo: {
|
||||
type: [Object],
|
||||
},
|
||||
// 操作类型,1:新增,2:修改
|
||||
operationType: {
|
||||
type: [Number],
|
||||
},
|
||||
},
|
||||
emits: ['valChange', 'cancel', 'update:visible'],
|
||||
setup(props: any, { emit }) {
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
operationType: 1,
|
||||
redisId: '',
|
||||
key: {
|
||||
key: '',
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
},
|
||||
string: {
|
||||
type: 'text',
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
emit('cancel');
|
||||
setTimeout(() => {
|
||||
state.key = {
|
||||
key: '',
|
||||
type: 'string',
|
||||
timed: -1,
|
||||
};
|
||||
state.string.value = '';
|
||||
state.string.type = 'text';
|
||||
}, 500);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
state.dialogVisible = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.redisId,
|
||||
(val) => {
|
||||
state.redisId = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(props, async (newValue) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
state.key = newValue.key;
|
||||
state.redisId = newValue.redisId;
|
||||
state.key = newValue.keyInfo;
|
||||
state.operationType = newValue.operationType
|
||||
// 如果是查看编辑操作,则获取值
|
||||
if (state.dialogVisible && state.operationType == 2) {
|
||||
getStringValue();
|
||||
}
|
||||
});
|
||||
|
||||
const getStringValue = async () => {
|
||||
state.string.value = await redisApi.getStringValue.request({
|
||||
id: state.redisId,
|
||||
key: state.key.key,
|
||||
});
|
||||
};
|
||||
|
||||
const saveValue = async () => {
|
||||
notEmpty(state.key.key, 'key不能为空');
|
||||
|
||||
notEmpty(state.string.value, 'value不能为空');
|
||||
const sv = { value: formatJsonString(state.string.value, true), id: state.redisId };
|
||||
Object.assign(sv, state.key);
|
||||
await redisApi.saveStringValue.request(sv);
|
||||
ElMessage.success('数据保存成功');
|
||||
cancel();
|
||||
emit('valChange');
|
||||
};
|
||||
|
||||
// 更改文本类型
|
||||
const onChangeTextType = (val: string) => {
|
||||
if (val == 'json') {
|
||||
state.string.value = formatJsonString(state.string.value, false);
|
||||
return;
|
||||
}
|
||||
if (val == 'text') {
|
||||
state.string.value = formatJsonString(state.string.value, true);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
saveValue,
|
||||
cancel,
|
||||
onChangeTextType,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#string-value-text {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.text-type-select {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
max-width: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@ import Api from '@/common/Api';
|
||||
|
||||
export const redisApi = {
|
||||
redisList : Api.create("/redis", 'get'),
|
||||
getRedisPwd: Api.create("/redis/{id}/pwd", 'get'),
|
||||
redisInfo: Api.create("/redis/{id}/info", 'get'),
|
||||
clusterInfo: Api.create("/redis/{id}/cluster-info", 'get'),
|
||||
saveRedis: Api.create("/redis", 'post'),
|
||||
@@ -11,6 +12,9 @@ export const redisApi = {
|
||||
getStringValue: Api.create("/redis/{id}/string-value", 'get'),
|
||||
saveStringValue: Api.create("/redis/{id}/string-value", 'post'),
|
||||
getHashValue: Api.create("/redis/{id}/hash-value", 'get'),
|
||||
hscan: Api.create("/redis/{id}/hscan", 'get'),
|
||||
hget: Api.create("/redis/{id}/hget", 'get'),
|
||||
hdel: Api.create("/redis/{id}/hdel", 'delete'),
|
||||
saveHashValue: Api.create("/redis/{id}/hash-value", 'post'),
|
||||
getSetValue: Api.create("/redis/{id}/set-value", 'get'),
|
||||
saveSetValue: Api.create("/redis/{id}/set-value", 'post'),
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.0.6.tgz"
|
||||
integrity sha512-lPpG8hYkjL/Z97DH5Ei6w6o22Z4YdNglWCNYOPcB33JCF2A4wye6HFgSI7hEt9zdLyxlSpiqtgf9XcYU+m5mew==
|
||||
|
||||
"@element-plus/icons-vue@^2.0.9":
|
||||
version "2.0.9"
|
||||
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.0.9.tgz#b7777c57534522e387303d194451d50ff549d49a"
|
||||
integrity sha512-okdrwiVeKBmW41Hkl0eMrXDjzJwhQMuKiBOu17rOszqM+LS/yBYpNQNV5Jvoh06Wc+89fMmb/uhzf8NZuDuUaQ==
|
||||
|
||||
"@eslint/eslintrc@^1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmmirror.com/@eslint/eslintrc/download/@eslint/eslintrc-1.0.5.tgz"
|
||||
@@ -126,10 +131,10 @@
|
||||
resolved "https://registry.npmmirror.com/@types/sortablejs/download/@types/sortablejs-1.10.7.tgz"
|
||||
integrity sha1-q5A5yFQp8FFpVextvAuyATlBexU=
|
||||
|
||||
"@types/web-bluetooth@^0.0.14":
|
||||
version "0.0.14"
|
||||
resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz"
|
||||
integrity sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A==
|
||||
"@types/web-bluetooth@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz#d60330046a6ed8a13b4a53df3813c44942ebdf72"
|
||||
integrity sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^4.23.0":
|
||||
version "4.33.0"
|
||||
@@ -364,25 +369,25 @@
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.37.tgz"
|
||||
integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==
|
||||
|
||||
"@vueuse/core@^8.7.5":
|
||||
version "8.7.5"
|
||||
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-8.7.5.tgz"
|
||||
integrity sha512-tqgzeZGoZcXzoit4kOGLWJibDMLp0vdm6ZO41SSUQhkhtrPhAg6dbIEPiahhUu6sZAmSYvVrZgEr5aKD51nrLA==
|
||||
"@vueuse/core@^9.1.0":
|
||||
version "9.1.0"
|
||||
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-9.1.0.tgz#f0fb13fd99768c0eb617169a2d2c1cbd5f5a52eb"
|
||||
integrity sha512-BIroqvXEqt826aE9r3K5cox1zobuPuAzdYJ36kouC2TVhlXvFKIILgFVWrpp9HZPwB3aLzasmG3K87q7TSyXZg==
|
||||
dependencies:
|
||||
"@types/web-bluetooth" "^0.0.14"
|
||||
"@vueuse/metadata" "8.7.5"
|
||||
"@vueuse/shared" "8.7.5"
|
||||
"@types/web-bluetooth" "^0.0.15"
|
||||
"@vueuse/metadata" "9.1.0"
|
||||
"@vueuse/shared" "9.1.0"
|
||||
vue-demi "*"
|
||||
|
||||
"@vueuse/metadata@8.7.5":
|
||||
version "8.7.5"
|
||||
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-8.7.5.tgz"
|
||||
integrity sha512-emJZKRQSaEnVqmlu39NpNp8iaW+bPC2kWykWoWOZMSlO/0QVEmO/rt8A5VhOEJTKLX3vwTevqbiRy9WJRwVOQg==
|
||||
"@vueuse/metadata@9.1.0":
|
||||
version "9.1.0"
|
||||
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.1.0.tgz#194d4bd47f7acb91e348c0f436e678ddf7ee235b"
|
||||
integrity sha512-8OEhlog1iaAGTD3LICZ8oBGQdYeMwByvXetOtAOZCJOzyCRSwqwdggTsmVZZ1rkgYIEqgUBk942AsAPwM21s6A==
|
||||
|
||||
"@vueuse/shared@8.7.5":
|
||||
version "8.7.5"
|
||||
resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-8.7.5.tgz"
|
||||
integrity sha512-THXPvMBFmg6Gf6AwRn/EdTh2mhqwjGsB2Yfp374LNQSQVKRHtnJ0I42bsZTn7nuEliBxqUrGQm/lN6qUHmhJLw==
|
||||
"@vueuse/shared@9.1.0":
|
||||
version "9.1.0"
|
||||
resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.1.0.tgz#d8459a45324f32fb05a2a56ed754637c3d0efaeb"
|
||||
integrity sha512-pB/3njQu4tfJJ78ajELNda0yMG6lKfpToQW7Soe09CprF1k3QuyoNi1tBNvo75wBDJWD+LOnr+c4B5HZ39jY/Q==
|
||||
dependencies:
|
||||
vue-demi "*"
|
||||
|
||||
@@ -421,7 +426,7 @@ ansi-regex@^5.0.1:
|
||||
resolved "https://registry.nlark.com/ansi-regex/download/ansi-regex-5.0.1.tgz"
|
||||
integrity sha1-CCyyyJyf6GWaMRpTvWpNxTAdswQ=
|
||||
|
||||
ansi-styles@^4.1.0:
|
||||
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.nlark.com/ansi-styles/download/ansi-styles-4.3.0.tgz"
|
||||
integrity sha1-7dgDYornHATIWuegkG7a00tkiTc=
|
||||
@@ -526,6 +531,15 @@ clipboard@^2.0.6:
|
||||
select "^1.1.2"
|
||||
tiny-emitter "^2.0.0"
|
||||
|
||||
cliui@^7.0.2:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
codemirror@^5.65.5:
|
||||
version "5.65.5"
|
||||
resolved "https://registry.npmmirror.com/codemirror/-/codemirror-5.65.5.tgz"
|
||||
@@ -596,6 +610,11 @@ deep-is@^0.1.3:
|
||||
resolved "https://registry.nlark.com/deep-is/download/deep-is-0.1.4.tgz"
|
||||
integrity sha1-pvLc5hL63S7x9Rm3NVHxfoUZmDE=
|
||||
|
||||
define-lazy-prop@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
|
||||
integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz"
|
||||
@@ -633,10 +652,10 @@ echarts@^5.3.3:
|
||||
tslib "2.3.0"
|
||||
zrender "5.3.2"
|
||||
|
||||
element-plus@^2.2.10:
|
||||
version "2.2.10"
|
||||
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.2.10.tgz#0b06a006b67b7ad3d5f071545a910782f9ba471b"
|
||||
integrity sha512-hJ+LlbRN3POu4Idl1LXB+SHSWdi+wwmdsoDXdQT2ynGuwzZsMYiusOooYXyEsPlrizeLibdnNGNDx4TIjXQvUg==
|
||||
element-plus@^2.2.14:
|
||||
version "2.2.14"
|
||||
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.2.14.tgz#161f2cbf2c12608a570af303f8191c7d7eae725b"
|
||||
integrity sha512-V5Pis0OHhePg1RgVogZrcefaVl8vjVn4Pn9Qsh/t2CbFgjg9kKOYFqf/tuP3ObSXGm3X89hpe0W+nLVAsaFnpw==
|
||||
dependencies:
|
||||
"@ctrl/tinycolor" "^3.4.1"
|
||||
"@element-plus/icons-vue" "^2.0.6"
|
||||
@@ -644,7 +663,7 @@ element-plus@^2.2.10:
|
||||
"@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7"
|
||||
"@types/lodash" "^4.14.182"
|
||||
"@types/lodash-es" "^4.17.6"
|
||||
"@vueuse/core" "^8.7.5"
|
||||
"@vueuse/core" "^9.1.0"
|
||||
async-validator "^4.2.5"
|
||||
dayjs "^1.11.3"
|
||||
escape-html "^1.0.3"
|
||||
@@ -652,7 +671,12 @@ element-plus@^2.2.10:
|
||||
lodash-es "^4.17.21"
|
||||
lodash-unified "^1.0.2"
|
||||
memoize-one "^6.0.0"
|
||||
normalize-wheel-es "^1.1.2"
|
||||
normalize-wheel-es "^1.2.0"
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||
|
||||
enquirer@^2.3.5:
|
||||
version "2.3.6"
|
||||
@@ -787,6 +811,11 @@ esbuild@^0.14.27:
|
||||
esbuild-windows-64 "0.14.38"
|
||||
esbuild-windows-arm64 "0.14.38"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
|
||||
|
||||
escape-html@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz"
|
||||
@@ -1029,6 +1058,11 @@ functional-red-black-tree@^1.0.1:
|
||||
resolved "https://registry.npm.taobao.org/functional-red-black-tree/download/functional-red-black-tree-1.0.1.tgz?cache=0&sync_timestamp=1577806294691&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffunctional-red-black-tree%2Fdownload%2Ffunctional-red-black-tree-1.0.1.tgz"
|
||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
||||
|
||||
get-caller-file@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmmirror.com/glob-parent/download/glob-parent-5.1.2.tgz"
|
||||
@@ -1148,11 +1182,21 @@ is-core-module@^2.8.1:
|
||||
dependencies:
|
||||
has "^1.0.3"
|
||||
|
||||
is-docker@^2.0.0, is-docker@^2.1.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.npmmirror.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
|
||||
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
|
||||
|
||||
is-extglob@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npm.taobao.org/is-extglob/download/is-extglob-2.1.1.tgz"
|
||||
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
|
||||
|
||||
is-fullwidth-code-point@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
||||
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||
|
||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmmirror.com/is-glob/download/is-glob-4.0.3.tgz"
|
||||
@@ -1165,6 +1209,13 @@ is-number@^7.0.0:
|
||||
resolved "https://registry.nlark.com/is-number/download/is-number-7.0.0.tgz"
|
||||
integrity sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=
|
||||
|
||||
is-wsl@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
|
||||
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
|
||||
dependencies:
|
||||
is-docker "^2.0.0"
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npm.taobao.org/isexe/download/isexe-2.0.0.tgz"
|
||||
@@ -1351,10 +1402,10 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
resolved "https://registry.npm.taobao.org/normalize-path/download/normalize-path-3.0.0.tgz"
|
||||
integrity sha1-Dc1p/yOhybEf0JeDFmRKA4ghamU=
|
||||
|
||||
normalize-wheel-es@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.1.2.tgz"
|
||||
integrity sha512-scX83plWJXYH1J4+BhAuIHadROzxX0UBF3+HuZNY2Ks8BciE7tSTQ+5JhTsvzjaO0/EJdm4JBGrfObKxFf3Png==
|
||||
normalize-wheel-es@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz#0fa2593d619f7245a541652619105ab076acf09e"
|
||||
integrity sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==
|
||||
|
||||
nprogress@^0.2.0:
|
||||
version "0.2.0"
|
||||
@@ -1368,6 +1419,15 @@ once@^1.3.0:
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
open@^8.4.0:
|
||||
version "8.4.0"
|
||||
resolved "https://registry.npmmirror.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8"
|
||||
integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==
|
||||
dependencies:
|
||||
define-lazy-prop "^2.0.0"
|
||||
is-docker "^2.1.1"
|
||||
is-wsl "^2.2.0"
|
||||
|
||||
optionator@^0.9.1:
|
||||
version "0.9.1"
|
||||
resolved "https://registry.npm.taobao.org/optionator/download/optionator-0.9.1.tgz"
|
||||
@@ -1477,6 +1537,11 @@ regexpp@^3.1.0, regexpp@^3.2.0:
|
||||
resolved "https://registry.nlark.com/regexpp/download/regexpp-3.2.0.tgz?cache=0&sync_timestamp=1623668872577&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fregexpp%2Fdownload%2Fregexpp-3.2.0.tgz"
|
||||
integrity sha1-BCWido2PI7rXDKS5BGH6LxIT4bI=
|
||||
|
||||
require-directory@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.nlark.com/resolve-from/download/resolve-from-4.0.0.tgz"
|
||||
@@ -1503,6 +1568,16 @@ rimraf@^3.0.2:
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rollup-plugin-visualizer@^5.8.0:
|
||||
version "5.8.0"
|
||||
resolved "https://registry.npmmirror.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.8.0.tgz#32f2fe23d4299e977c06c59c07255590354e3445"
|
||||
integrity sha512-pY6j/7qHz5I9rB7d/bQoA5gX+2FbV3MBG055wrsFxDn550bgl0FNViRj6wDHh85PMswv+JVdZjhnMBzz/hdAHA==
|
||||
dependencies:
|
||||
nanoid "^3.3.4"
|
||||
open "^8.4.0"
|
||||
source-map "^0.7.3"
|
||||
yargs "^17.5.1"
|
||||
|
||||
rollup@^2.59.0:
|
||||
version "2.61.1"
|
||||
resolved "https://registry.npmmirror.com/rollup/download/rollup-2.61.1.tgz"
|
||||
@@ -1534,10 +1609,10 @@ sass@^1.45.1:
|
||||
immutable "^4.0.0"
|
||||
source-map-js ">=0.6.2 <2.0.0"
|
||||
|
||||
screenfull@^5.1.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.npmmirror.com/screenfull/download/screenfull-5.2.0.tgz?cache=0&sync_timestamp=1635923453416&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fscreenfull%2Fdownload%2Fscreenfull-5.2.0.tgz"
|
||||
integrity sha1-ZTPVJNMGIfwSg7lpIUbz8TqT0bo=
|
||||
screenfull@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.npmmirror.com/screenfull/-/screenfull-6.0.2.tgz#3dbe4b8c4f8f49fb8e33caa8f69d0bca730ab238"
|
||||
integrity sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw==
|
||||
|
||||
select@^1.1.2:
|
||||
version "1.1.2"
|
||||
@@ -1588,19 +1663,33 @@ source-map@^0.6.1:
|
||||
resolved "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz"
|
||||
integrity sha1-dHIq8y6WFOnCh6jQu95IteLxomM=
|
||||
|
||||
source-map@^0.7.3:
|
||||
version "0.7.4"
|
||||
resolved "https://registry.npmmirror.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
|
||||
integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
|
||||
|
||||
sourcemap-codec@^1.4.4:
|
||||
version "1.4.8"
|
||||
resolved "https://registry.npm.taobao.org/sourcemap-codec/download/sourcemap-codec-1.4.8.tgz"
|
||||
integrity sha1-6oBL2UhXQC5pktBaOO8a41qatMQ=
|
||||
|
||||
sql-formatter@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.npmmirror.com/sql-formatter/-/sql-formatter-7.0.3.tgz"
|
||||
integrity sha512-E9zotLB0dy9ZZhs1sY4ZqzSzJGF2uC4Vzj0mEzXJC9rlE+Jjmz6t64qT2dzm/IPQosYvZknDbBOrWkygIJz67A==
|
||||
sql-formatter@^9.2.0:
|
||||
version "9.2.0"
|
||||
resolved "https://registry.npmmirror.com/sql-formatter/-/sql-formatter-9.2.0.tgz#18a398ae71436dc1936a45e6f230236b4347231b"
|
||||
integrity sha512-Dn4lEpUeAhfNDR2LnEs9Uaq92TSHjhcNrzhllPuMnp188P4sLU7UcdcB9UqIfMfcN62gWXJlJ3KocaAf/SOzXQ==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
strip-ansi@^6.0.1:
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmmirror.com/strip-ansi/download/strip-ansi-6.0.1.tgz"
|
||||
integrity sha1-nibGPTD1NEPpSJSVshBdN7Z6hdk=
|
||||
@@ -1768,6 +1857,15 @@ word-wrap@^1.2.3:
|
||||
resolved "https://registry.npm.taobao.org/word-wrap/download/word-wrap-1.2.3.tgz"
|
||||
integrity sha1-YQY29rH3A4kb00dxzLF/uTtHB5w=
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.nlark.com/wrappy/download/wrappy-1.0.2.tgz?cache=0&sync_timestamp=1619133505879&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fwrappy%2Fdownload%2Fwrappy-1.0.2.tgz"
|
||||
@@ -1783,11 +1881,34 @@ xterm@^4.19.0:
|
||||
resolved "https://registry.npmmirror.com/xterm/-/xterm-4.19.0.tgz"
|
||||
integrity sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.nlark.com/yallist/download/yallist-4.0.0.tgz"
|
||||
integrity sha1-m7knkNnA7/7GO+c1GeEaNQGaOnI=
|
||||
|
||||
yargs-parser@^21.0.0:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs@^17.5.1:
|
||||
version "17.5.1"
|
||||
resolved "https://registry.npmmirror.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e"
|
||||
integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==
|
||||
dependencies:
|
||||
cliui "^7.0.2"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.3"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.0.0"
|
||||
|
||||
zrender@5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.npmmirror.com/zrender/-/zrender-5.3.2.tgz"
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
app:
|
||||
name: mayfly-go
|
||||
version: 1.2.3
|
||||
|
||||
server:
|
||||
# debug release test
|
||||
model: release
|
||||
@@ -25,10 +21,13 @@ server:
|
||||
filepath: ./static/config.js
|
||||
|
||||
jwt:
|
||||
key: mykey
|
||||
# jwt key,不设置默认使用随机字符串
|
||||
key:
|
||||
# 过期时间单位分钟
|
||||
expire-time: 1440
|
||||
|
||||
# 资源密码aes加密key
|
||||
aes:
|
||||
key: 1111111111111111
|
||||
mysql:
|
||||
host: localhost:3306
|
||||
username: root
|
||||
|
||||
@@ -3,23 +3,23 @@ module mayfly-go
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // jwt
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
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/gorilla/websocket v1.5.0
|
||||
github.com/lib/pq v1.10.6
|
||||
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.8.1
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
|
||||
go.mongodb.org/mongo-driver v1.9.1 // mongo
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // ssh
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // ssh
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
// gorm
|
||||
gorm.io/driver/mysql v1.3.4
|
||||
gorm.io/gorm v1.23.5
|
||||
gorm.io/driver/mysql v1.3.5
|
||||
gorm.io/gorm v1.23.8
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -34,7 +34,7 @@ require (
|
||||
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.4 // 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/kr/fs v0.1.0 // indirect
|
||||
@@ -52,7 +52,7 @@ require (
|
||||
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
|
||||
35
server/internal/common/utils/pwd.go
Normal file
35
server/internal/common/utils/pwd.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/config"
|
||||
)
|
||||
|
||||
// 使用config.yml的aes.key进行密码加密
|
||||
func PwdAesEncrypt(password string) string {
|
||||
if password == "" {
|
||||
return ""
|
||||
}
|
||||
aes := config.Conf.Aes
|
||||
if aes == nil {
|
||||
return password
|
||||
}
|
||||
encryptPwd, err := aes.EncryptBase64([]byte(password))
|
||||
biz.ErrIsNilAppendErr(err, "密码加密失败: %s")
|
||||
return encryptPwd
|
||||
}
|
||||
|
||||
// 使用config.yml的aes.key进行密码解密
|
||||
func PwdAesDecrypt(encryptPwd string) string {
|
||||
if encryptPwd == "" {
|
||||
return ""
|
||||
}
|
||||
aes := config.Conf.Aes
|
||||
if aes == nil {
|
||||
return encryptPwd
|
||||
}
|
||||
decryptPwd, err := aes.DecryptBase64(encryptPwd)
|
||||
biz.ErrIsNilAppendErr(err, "密码解密失败: %s")
|
||||
// 解密后的密码
|
||||
return string(decryptPwd)
|
||||
}
|
||||
@@ -9,8 +9,8 @@ const (
|
||||
MongoConnExpireTime = 30 * time.Minute
|
||||
|
||||
/**** 开发测试使用 ****/
|
||||
// MachineConnExpireTime = 20 * time.Second
|
||||
// DbConnExpireTime = 20 * time.Second
|
||||
// RedisConnExpireTime = 20 * time.Second
|
||||
// MongoConnExpireTime = 20 * time.Second
|
||||
// MachineConnExpireTime = 4 * time.Minute
|
||||
// DbConnExpireTime = 2 * time.Minute
|
||||
// RedisConnExpireTime = 2 * time.Minute
|
||||
// MongoConnExpireTime = 2 * time.Minute
|
||||
)
|
||||
|
||||
@@ -61,6 +61,14 @@ func (d *Db) Save(rc *ctx.ReqCtx) {
|
||||
d.DbApp.Save(db)
|
||||
}
|
||||
|
||||
// 获取数据库实例密码,由于数据库是加密存储,故提供该接口展示原文密码
|
||||
func (d *Db) GetDbPwd(rc *ctx.ReqCtx) {
|
||||
dbId := GetDbId(rc.GinCtx)
|
||||
dbEntity := d.DbApp.GetById(dbId, "Password")
|
||||
dbEntity.PwdDecrypt()
|
||||
rc.ResData = dbEntity.Password
|
||||
}
|
||||
|
||||
// 获取数据库实例的所有数据库名
|
||||
func (d *Db) GetDatabaseNames(rc *ctx.ReqCtx) {
|
||||
form := &form.DbForm{}
|
||||
@@ -89,19 +97,19 @@ func (d *Db) DeleteDb(rc *ctx.ReqCtx) {
|
||||
}
|
||||
|
||||
func (d *Db) TableInfos(rc *ctx.ReqCtx) {
|
||||
rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetTableInfos()
|
||||
rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetMeta().GetTableInfos()
|
||||
}
|
||||
|
||||
func (d *Db) TableIndex(rc *ctx.ReqCtx) {
|
||||
tn := rc.GinCtx.Query("tableName")
|
||||
biz.NotEmpty(tn, "tableName不能为空")
|
||||
rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetTableIndex(tn)
|
||||
rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetMeta().GetTableIndex(tn)
|
||||
}
|
||||
|
||||
func (d *Db) GetCreateTableDdl(rc *ctx.ReqCtx) {
|
||||
tn := rc.GinCtx.Query("tableName")
|
||||
biz.NotEmpty(tn, "tableName不能为空")
|
||||
rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetCreateTableDdl(tn)
|
||||
rc.ResData = d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetMeta().GetCreateTableDdl(tn)
|
||||
}
|
||||
|
||||
func (d *Db) ExecSql(rc *ctx.ReqCtx) {
|
||||
@@ -229,11 +237,12 @@ func (d *Db) DumpSql(rc *ctx.ReqCtx) {
|
||||
writer.WriteString(fmt.Sprintf("\n-- 导出数据库: %s ", db))
|
||||
writer.WriteString("\n-- ----------------------------\n")
|
||||
|
||||
dbmeta := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)).GetMeta()
|
||||
for _, table := range tables {
|
||||
if needStruct {
|
||||
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表结构: %s \n-- ----------------------------\n", table))
|
||||
writer.WriteString(fmt.Sprintf("DROP TABLE IF EXISTS `%s`;\n", table))
|
||||
writer.WriteString(dbInstance.GetCreateTableDdl(table)[0]["Create Table"].(string) + ";\n")
|
||||
writer.WriteString(dbmeta.GetCreateTableDdl(table)[0]["Create Table"].(string) + ";\n")
|
||||
}
|
||||
|
||||
if !needData {
|
||||
@@ -295,7 +304,7 @@ func (d *Db) DumpSql(rc *ctx.ReqCtx) {
|
||||
func (d *Db) TableMA(rc *ctx.ReqCtx) {
|
||||
dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx))
|
||||
biz.ErrIsNilAppendErr(d.ProjectApp.CanAccess(rc.LoginAccount.Id, dbi.ProjectId), "%s")
|
||||
rc.ResData = dbi.GetTableMetedatas()
|
||||
rc.ResData = dbi.GetMeta().GetTables()
|
||||
}
|
||||
|
||||
// @router /api/db/:dbId/c-metadata [get]
|
||||
@@ -306,16 +315,17 @@ func (d *Db) ColumnMA(rc *ctx.ReqCtx) {
|
||||
|
||||
dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx))
|
||||
biz.ErrIsNilAppendErr(d.ProjectApp.CanAccess(rc.LoginAccount.Id, dbi.ProjectId), "%s")
|
||||
rc.ResData = dbi.GetColumnMetadatas(tn)
|
||||
rc.ResData = dbi.GetMeta().GetColumns(tn)
|
||||
}
|
||||
|
||||
// @router /api/db/:dbId/hint-tables [get]
|
||||
func (d *Db) HintTables(rc *ctx.ReqCtx) {
|
||||
dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx))
|
||||
biz.ErrIsNilAppendErr(d.ProjectApp.CanAccess(rc.LoginAccount.Id, dbi.ProjectId), "%s")
|
||||
// 获取所有表
|
||||
tables := dbi.GetTableMetedatas()
|
||||
|
||||
dm := dbi.GetMeta()
|
||||
// 获取所有表
|
||||
tables := dm.GetTables()
|
||||
tableNames := make([]string, 0)
|
||||
for _, v := range tables {
|
||||
tableNames = append(tableNames, v["tableName"].(string))
|
||||
@@ -330,7 +340,7 @@ func (d *Db) HintTables(rc *ctx.ReqCtx) {
|
||||
}
|
||||
|
||||
// 获取所有表下的所有列信息
|
||||
columnMds := dbi.GetColumnMetadatas(tableNames...)
|
||||
columnMds := dm.GetColumns(tableNames...)
|
||||
for _, v := range columnMds {
|
||||
tName := v["tableName"].(string)
|
||||
if res[tName] == nil {
|
||||
|
||||
@@ -72,6 +72,14 @@ func (m *Machine) SaveMachine(rc *ctx.ReqCtx) {
|
||||
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) ChangeStatus(rc *ctx.ReqCtx) {
|
||||
g := rc.GinCtx
|
||||
id := uint64(ginx.PathParamInt(g, "machineId"))
|
||||
@@ -152,21 +160,16 @@ func (m *Machine) WsSSH(g *gin.Context) {
|
||||
panic(biz.NewBizErr("\033[1;31m您没有权限操作该机器终端,请重新登录后再试~\033[0m"))
|
||||
}
|
||||
|
||||
cols := ginx.QueryInt(g, "cols", 80)
|
||||
rows := ginx.QueryInt(g, "rows", 40)
|
||||
|
||||
cli := m.MachineApp.GetCli(GetMachineId(g))
|
||||
biz.ErrIsNilAppendErr(m.ProjectApp.CanAccess(rc.LoginAccount.Id, cli.GetMachine().ProjectId), "%s")
|
||||
|
||||
sws, err := machine.NewLogicSshWsSession(cols, rows, cli, wsConn)
|
||||
biz.ErrIsNilAppendErr(err, "\033[1;31m连接失败:%s\033[0m")
|
||||
defer sws.Close()
|
||||
cols := ginx.QueryInt(g, "cols", 80)
|
||||
rows := ginx.QueryInt(g, "rows", 40)
|
||||
|
||||
quitChan := make(chan bool, 3)
|
||||
sws.Start(quitChan)
|
||||
go sws.Wait(quitChan)
|
||||
|
||||
<-quitChan
|
||||
mts, err := machine.NewTerminalSession(utils.RandString(16), wsConn, cli, rows, cols)
|
||||
biz.ErrIsNilAppendErr(err, "\033[1;31m连接失败: %s\033[0m")
|
||||
mts.Start()
|
||||
defer mts.Stop()
|
||||
}
|
||||
|
||||
func GetMachineId(g *gin.Context) uint64 {
|
||||
|
||||
@@ -38,10 +38,6 @@ func (m *Mongo) Save(rc *ctx.ReqCtx) {
|
||||
|
||||
mongo := new(entity.Mongo)
|
||||
utils.Copy(mongo, form)
|
||||
// 解密uri,并使用解密后的赋值
|
||||
originUri, err := utils.DefaultRsaDecrypt(form.Uri, true)
|
||||
biz.ErrIsNilAppendErr(err, "解密uri错误: %s")
|
||||
mongo.Uri = originUri
|
||||
|
||||
mongo.SetBaseInfo(rc.LoginAccount)
|
||||
m.MongoApp.Save(mongo)
|
||||
|
||||
@@ -52,6 +52,14 @@ func (r *Redis) Save(rc *ctx.ReqCtx) {
|
||||
r.RedisApp.Save(redis)
|
||||
}
|
||||
|
||||
// 获取redis实例密码,由于数据库是加密存储,故提供该接口展示原文密码
|
||||
func (r *Redis) GetRedisPwd(rc *ctx.ReqCtx) {
|
||||
rid := uint64(ginx.PathParamInt(rc.GinCtx, "id"))
|
||||
re := r.RedisApp.GetById(rid, "Password")
|
||||
re.PwdDecrypt()
|
||||
rc.ResData = re.Password
|
||||
}
|
||||
|
||||
func (r *Redis) DeleteRedis(rc *ctx.ReqCtx) {
|
||||
r.RedisApp.Delete(uint64(ginx.PathParamInt(rc.GinCtx, "id")))
|
||||
}
|
||||
@@ -185,7 +193,7 @@ func (r *Redis) Scan(rc *ctx.ReqCtx) {
|
||||
kis := make([]*vo.KeyInfo, 0)
|
||||
var cursorRes map[string]uint64 = make(map[string]uint64)
|
||||
|
||||
if ri.Mode == "" || ri.Mode == entity.RedisModeStandalone {
|
||||
if ri.Mode == "" || ri.Mode == entity.RedisModeStandalone || ri.Mode == entity.RedisModeSentinel {
|
||||
redisAddr := ri.Cli.Options().Addr
|
||||
keys, cursor := ri.Scan(form.Cursor[redisAddr], form.Match, form.Count)
|
||||
cursorRes[redisAddr] = cursor
|
||||
@@ -277,13 +285,6 @@ func (r *Redis) GetStringValue(rc *ctx.ReqCtx) {
|
||||
rc.ResData = str
|
||||
}
|
||||
|
||||
func (r *Redis) GetHashValue(rc *ctx.ReqCtx) {
|
||||
ri, key := r.checkKey(rc)
|
||||
res, err := ri.GetCmdable().HGetAll(context.TODO(), key).Result()
|
||||
biz.ErrIsNilAppendErr(err, "获取hash值失败: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (r *Redis) SetStringValue(rc *ctx.ReqCtx) {
|
||||
g := rc.GinCtx
|
||||
keyValue := new(form.StringValue)
|
||||
@@ -297,6 +298,45 @@ func (r *Redis) SetStringValue(rc *ctx.ReqCtx) {
|
||||
rc.ResData = str
|
||||
}
|
||||
|
||||
func (r *Redis) Hscan(rc *ctx.ReqCtx) {
|
||||
ri, key := r.checkKey(rc)
|
||||
g := rc.GinCtx
|
||||
count := ginx.QueryInt(g, "count", 10)
|
||||
match := g.Query("match")
|
||||
cursor := ginx.QueryInt(g, "cursor", 0)
|
||||
contextTodo := context.TODO()
|
||||
|
||||
cmdable := ri.GetCmdable()
|
||||
keys, nextCursor, err := cmdable.HScan(contextTodo, key, uint64(cursor), match, int64(count)).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hcan err: %s")
|
||||
keySize, err := cmdable.HLen(contextTodo, key).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hlen err: %s")
|
||||
|
||||
rc.ResData = map[string]interface{}{
|
||||
"keys": keys,
|
||||
"cursor": nextCursor,
|
||||
"keySize": keySize,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Redis) Hdel(rc *ctx.ReqCtx) {
|
||||
ri, key := r.checkKey(rc)
|
||||
field := rc.GinCtx.Query("field")
|
||||
|
||||
delRes, err := ri.GetCmdable().HDel(context.TODO(), key, field).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hdel err: %s")
|
||||
rc.ResData = delRes
|
||||
}
|
||||
|
||||
func (r *Redis) Hget(rc *ctx.ReqCtx) {
|
||||
ri, key := r.checkKey(rc)
|
||||
field := rc.GinCtx.Query("field")
|
||||
|
||||
res, err := ri.GetCmdable().HGet(context.TODO(), key, field).Result()
|
||||
biz.ErrIsNilAppendErr(err, "hget err: %s")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (r *Redis) SetHashValue(rc *ctx.ReqCtx) {
|
||||
g := rc.GinCtx
|
||||
hashValue := new(form.HashValue)
|
||||
@@ -307,13 +347,12 @@ func (r *Redis) SetHashValue(rc *ctx.ReqCtx) {
|
||||
|
||||
cmd := ri.GetCmdable()
|
||||
key := hashValue.Key
|
||||
// 简单处理->先删除,后新增
|
||||
cmd.Del(context.TODO(), key)
|
||||
contextTodo := context.TODO()
|
||||
for _, v := range hashValue.Value {
|
||||
res := cmd.HSet(context.TODO(), key, v["key"].(string), v["value"])
|
||||
res := cmd.HSet(contextTodo, key, v["field"].(string), v["value"])
|
||||
biz.ErrIsNilAppendErr(res.Err(), "保存hash值失败: %s")
|
||||
}
|
||||
if hashValue.Timed != -1 {
|
||||
if hashValue.Timed != 0 && hashValue.Timed != -1 {
|
||||
cmd.Expire(context.TODO(), key, time.Second*time.Duration(hashValue.Timed))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ func (d *dbAppImpl) Save(dbEntity *entity.Db) {
|
||||
if dbEntity.Id == 0 {
|
||||
biz.NotEmpty(dbEntity.Password, "密码不能为空")
|
||||
biz.IsTrue(err != nil, "该数据库实例已存在")
|
||||
dbEntity.PwdEncrypt()
|
||||
d.dbRepo.Insert(dbEntity)
|
||||
return
|
||||
}
|
||||
@@ -129,6 +130,7 @@ func (d *dbAppImpl) Save(dbEntity *entity.Db) {
|
||||
d.dbSqlRepo.DeleteBy(&entity.DbSql{DbId: dbId, Db: v.(string)})
|
||||
}
|
||||
|
||||
dbEntity.PwdEncrypt()
|
||||
d.dbRepo.Update(dbEntity)
|
||||
}
|
||||
|
||||
@@ -145,6 +147,7 @@ func (d *dbAppImpl) Delete(id uint64) {
|
||||
}
|
||||
|
||||
func (d *dbAppImpl) GetDatabases(ed *entity.Db) []string {
|
||||
ed.Network = ed.GetNetwork()
|
||||
databases := make([]string, 0)
|
||||
var dbConn *sql.DB
|
||||
var metaDb string
|
||||
@@ -180,10 +183,12 @@ func (da *dbAppImpl) GetDbInstance(id uint64, db string) *DbInstance {
|
||||
return load.(*DbInstance)
|
||||
}
|
||||
}
|
||||
biz.IsTrue(mutex.TryLock(), "有数据库实例在连接中...请稍后重试")
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
d := da.GetById(id)
|
||||
// 密码解密
|
||||
d.PwdDecrypt()
|
||||
biz.NotNil(d, "数据库信息不存在")
|
||||
biz.IsTrue(strings.Contains(d.Database, db), "未配置该库的操作权限")
|
||||
|
||||
@@ -214,6 +219,9 @@ func (da *dbAppImpl) GetDbInstance(id uint64, db string) *DbInstance {
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// 单次最大查询数据集
|
||||
const Max_Rows = 2000
|
||||
|
||||
// 客户端连接缓存,指定时间内没有访问则会被关闭, key为数据库实例id:数据库
|
||||
var dbCache = cache.NewTimedCache(constant.DbConnExpireTime, 5*time.Second).
|
||||
WithUpdateAccessTime(true).
|
||||
@@ -258,10 +266,9 @@ func GetDbConn(d *entity.Db, db string) (*sql.DB, error) {
|
||||
// SSH Conect
|
||||
if d.EnableSshTunnel == 1 && d.SshTunnelMachineId != 0 {
|
||||
sshTunnelMachine := MachineApp.GetSshTunnelMachine(d.SshTunnelMachineId)
|
||||
defer machine.CloseSshTunnelMachine(d.SshTunnelMachineId, 0)
|
||||
if d.Type == entity.DbTypeMysql {
|
||||
mysql.RegisterDialContext(d.Network, func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
return MachineApp.GetSshTunnelMachine(d.SshTunnelMachineId).GetDialConn("tcp", addr)
|
||||
return sshTunnelMachine.GetDialConn("tcp", addr)
|
||||
})
|
||||
} else if d.Type == entity.DbTypePostgres {
|
||||
_, err := pq.DialOpen(&PqSqlDialer{sshTunnelMachine: sshTunnelMachine}, getDsn(d, db))
|
||||
@@ -311,7 +318,11 @@ func SelectDataByDb(db *sql.DB, selectSql string) ([]string, []map[string]interf
|
||||
colNames := make([]string, 0)
|
||||
// 是否第一次遍历,列名数组只需第一次遍历时加入
|
||||
isFirst := true
|
||||
rowNum := 0
|
||||
for rows.Next() {
|
||||
rowNum++
|
||||
biz.IsTrue(rowNum <= Max_Rows, "结果集 > 2000, 请完善条件或分页信息")
|
||||
|
||||
// 不Scan也会导致等待,该链接实际处于未工作的状态,然后也会导致连接数迅速达到最大
|
||||
err := rows.Scan(scans...)
|
||||
if err != nil {
|
||||
@@ -325,6 +336,7 @@ func SelectDataByDb(db *sql.DB, selectSql string) ([]string, []map[string]interf
|
||||
colName := colType.Name()
|
||||
// 字段类型名
|
||||
colScanType := colType.ScanType().Name()
|
||||
// 如果是第一行,则将列名加入到列信息中,由于map是无序的,所有需要返回列名的有序数组
|
||||
if isFirst {
|
||||
colNames = append(colNames, colName)
|
||||
}
|
||||
@@ -419,6 +431,18 @@ func (d *DbInstance) Exec(sql string) (int64, error) {
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
// 获取数据库元信息实现接口
|
||||
func (di *DbInstance) GetMeta() DbMetadata {
|
||||
dbType := di.Type
|
||||
if dbType == entity.DbTypeMysql {
|
||||
return &MysqlMetadata{di: di}
|
||||
}
|
||||
if dbType == entity.DbTypePostgres {
|
||||
return &PgsqlMetadata{di: di}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 关闭连接
|
||||
func (d *DbInstance) Close() {
|
||||
if d.db != nil {
|
||||
@@ -455,8 +479,32 @@ func CloseDb(dbId uint64, db string) {
|
||||
dbCache.Delete(GetDbCacheKey(dbId, db))
|
||||
}
|
||||
|
||||
//-----------------------------------元数据-------------------------------------------
|
||||
// -----------------------------------元数据-------------------------------------------
|
||||
// 数据库元信息接口(表、列等元信息)
|
||||
type DbMetadata interface {
|
||||
// 获取表基础元信息, 如表名等
|
||||
GetTables() []map[string]interface{}
|
||||
|
||||
// 获取列元信息, 如列名等
|
||||
GetColumns(tableNames ...string) []map[string]interface{}
|
||||
|
||||
// 获取表主键字段名,默认第一个字段
|
||||
GetPrimaryKey(tablename string) string
|
||||
|
||||
// 获取表信息,比GetTables获取更详细的表信息
|
||||
GetTableInfos() []map[string]interface{}
|
||||
|
||||
// 获取表索引信息
|
||||
GetTableIndex(tableName string) []map[string]interface{}
|
||||
|
||||
// 获取建表ddl
|
||||
GetCreateTableDdl(tableName string) []map[string]interface{}
|
||||
}
|
||||
|
||||
// 默认每次查询列元信息数量
|
||||
const DEFAULT_COLUMN_SIZE = 2000
|
||||
|
||||
// ---------------------------------- mysql元数据 -----------------------------------
|
||||
const (
|
||||
// mysql 表信息元数据
|
||||
MYSQL_TABLE_MA = `SELECT table_name tableName, engine, table_comment tableComment,
|
||||
@@ -475,9 +523,6 @@ const (
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE table_schema = (SELECT database()) AND table_name = '%s' LIMIT 500`
|
||||
|
||||
// 默认每次查询列元信息数量
|
||||
DEFAULT_COLUMN_SIZE = 2000
|
||||
|
||||
// mysql 列信息元数据
|
||||
MYSQL_COLUMN_MA = `SELECT table_name tableName, column_name columnName, column_type columnType,
|
||||
column_comment columnComment, column_key columnKey, extra, is_nullable nullable from information_schema.columns
|
||||
@@ -488,6 +533,74 @@ const (
|
||||
WHERE table_name in (%s) AND table_schema = (SELECT database())`
|
||||
)
|
||||
|
||||
type MysqlMetadata struct {
|
||||
di *DbInstance
|
||||
}
|
||||
|
||||
// 获取表基础元信息, 如表名等
|
||||
func (mm *MysqlMetadata) GetTables() []map[string]interface{} {
|
||||
_, res, _ := mm.di.SelectData(MYSQL_TABLE_MA)
|
||||
return res
|
||||
}
|
||||
|
||||
// 获取列元信息, 如列名等
|
||||
func (mm *MysqlMetadata) GetColumns(tableNames ...string) []map[string]interface{} {
|
||||
var sql, tableName string
|
||||
for i := 0; i < len(tableNames); i++ {
|
||||
if i != 0 {
|
||||
tableName = tableName + ", "
|
||||
}
|
||||
tableName = tableName + "'" + tableNames[i] + "'"
|
||||
}
|
||||
|
||||
pageNum := 1
|
||||
// 如果大于一个表,则统计列数并分页获取
|
||||
if len(tableNames) > 1 {
|
||||
countSql := fmt.Sprintf(MYSQL_COLOUMN_MA_COUNT, tableName)
|
||||
_, countRes, _ := mm.di.SelectData(countSql)
|
||||
// 查询出所有列信息总数,手动分页获取所有数据
|
||||
maCount := int(countRes[0]["maNum"].(int64))
|
||||
// 计算需要查询的页数
|
||||
pageNum = maCount / DEFAULT_COLUMN_SIZE
|
||||
if maCount%DEFAULT_COLUMN_SIZE > 0 {
|
||||
pageNum++
|
||||
}
|
||||
}
|
||||
|
||||
res := make([]map[string]interface{}, 0)
|
||||
for index := 0; index < pageNum; index++ {
|
||||
sql = fmt.Sprintf(MYSQL_COLUMN_MA, tableName, index*DEFAULT_COLUMN_SIZE, DEFAULT_COLUMN_SIZE)
|
||||
_, result, err := mm.di.SelectData(sql)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库列信息失败: %s")
|
||||
res = append(res, result...)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 获取表主键字段名,默认第一个字段
|
||||
func (mm *MysqlMetadata) GetPrimaryKey(tablename string) string {
|
||||
return mm.GetColumns(tablename)[0]["columnName"].(string)
|
||||
}
|
||||
|
||||
// 获取表信息,比GetTableMetedatas获取更详细的表信息
|
||||
func (mm *MysqlMetadata) GetTableInfos() []map[string]interface{} {
|
||||
_, res, _ := mm.di.SelectData(MYSQL_TABLE_INFO)
|
||||
return res
|
||||
}
|
||||
|
||||
// 获取表索引信息
|
||||
func (mm *MysqlMetadata) GetTableIndex(tableName string) []map[string]interface{} {
|
||||
_, res, _ := mm.di.SelectData(MYSQL_INDEX_INFO)
|
||||
return res
|
||||
}
|
||||
|
||||
// 获取建表ddl
|
||||
func (mm *MysqlMetadata) GetCreateTableDdl(tableName string) []map[string]interface{} {
|
||||
_, res, _ := mm.di.SelectData(fmt.Sprintf("show create table %s ", tableName))
|
||||
return res
|
||||
}
|
||||
|
||||
// ---------------------------------- pgsql元数据 -----------------------------------
|
||||
const (
|
||||
// postgres 表信息元数据
|
||||
PGSQL_TABLE_MA = `SELECT obj_description(c.oid) AS "tableComment", c.relname AS "tableName" FROM pg_class c
|
||||
@@ -534,18 +647,18 @@ const (
|
||||
`
|
||||
)
|
||||
|
||||
func (d *DbInstance) GetTableMetedatas() []map[string]interface{} {
|
||||
var sql string
|
||||
if d.Type == entity.DbTypeMysql {
|
||||
sql = MYSQL_TABLE_MA
|
||||
} else if d.Type == "postgres" {
|
||||
sql = PGSQL_TABLE_MA
|
||||
}
|
||||
_, res, _ := d.SelectData(sql)
|
||||
type PgsqlMetadata struct {
|
||||
di *DbInstance
|
||||
}
|
||||
|
||||
// 获取表基础元信息, 如表名等
|
||||
func (pm *PgsqlMetadata) GetTables() []map[string]interface{} {
|
||||
_, res, _ := pm.di.SelectData(PGSQL_TABLE_MA)
|
||||
return res
|
||||
}
|
||||
|
||||
func (d *DbInstance) GetColumnMetadatas(tableNames ...string) []map[string]interface{} {
|
||||
// 获取列元信息, 如列名等
|
||||
func (pm *PgsqlMetadata) GetColumns(tableNames ...string) []map[string]interface{} {
|
||||
var sql, tableName string
|
||||
for i := 0; i < len(tableNames); i++ {
|
||||
if i != 0 {
|
||||
@@ -554,68 +667,48 @@ func (d *DbInstance) GetColumnMetadatas(tableNames ...string) []map[string]inter
|
||||
tableName = tableName + "'" + tableNames[i] + "'"
|
||||
}
|
||||
|
||||
var countSqlTmp string
|
||||
var sqlTmp string
|
||||
if d.Type == entity.DbTypeMysql {
|
||||
countSqlTmp = MYSQL_COLOUMN_MA_COUNT
|
||||
sqlTmp = MYSQL_COLUMN_MA
|
||||
} else if d.Type == entity.DbTypePostgres {
|
||||
countSqlTmp = PGSQL_COLUMN_MA_COUNT
|
||||
sqlTmp = PGSQL_COLUMN_MA
|
||||
}
|
||||
|
||||
countSql := fmt.Sprintf(countSqlTmp, tableName)
|
||||
_, countRes, _ := d.SelectData(countSql)
|
||||
// 查询出所有列信息总数,手动分页获取所有数据
|
||||
maCount := int(countRes[0]["maNum"].(int64))
|
||||
// 计算需要查询的页数
|
||||
pageNum := maCount / DEFAULT_COLUMN_SIZE
|
||||
if maCount%DEFAULT_COLUMN_SIZE > 0 {
|
||||
pageNum++
|
||||
pageNum := 1
|
||||
// 如果大于一个表,则统计列数并分页获取
|
||||
if len(tableNames) > 1 {
|
||||
countSql := fmt.Sprintf(PGSQL_COLUMN_MA_COUNT, tableName)
|
||||
_, countRes, _ := pm.di.SelectData(countSql)
|
||||
// 查询出所有列信息总数,手动分页获取所有数据
|
||||
maCount := int(countRes[0]["maNum"].(int64))
|
||||
// 计算需要查询的页数
|
||||
pageNum = maCount / DEFAULT_COLUMN_SIZE
|
||||
if maCount%DEFAULT_COLUMN_SIZE > 0 {
|
||||
pageNum++
|
||||
}
|
||||
}
|
||||
|
||||
res := make([]map[string]interface{}, 0)
|
||||
for index := 0; index < pageNum; index++ {
|
||||
sql = fmt.Sprintf(sqlTmp, tableName, index*DEFAULT_COLUMN_SIZE, DEFAULT_COLUMN_SIZE)
|
||||
_, result, err := d.SelectData(sql)
|
||||
sql = fmt.Sprintf(PGSQL_COLUMN_MA, tableName, index*DEFAULT_COLUMN_SIZE, DEFAULT_COLUMN_SIZE)
|
||||
_, result, err := pm.di.SelectData(sql)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库列信息失败: %s")
|
||||
res = append(res, result...)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 获取表的主键,目前默认第一列为主键
|
||||
func (d *DbInstance) GetPrimaryKey(tablename string) string {
|
||||
return d.GetColumnMetadatas(tablename)[0]["columnName"].(string)
|
||||
// 获取表主键字段名,默认第一个字段
|
||||
func (pm *PgsqlMetadata) GetPrimaryKey(tablename string) string {
|
||||
return pm.GetColumns(tablename)[0]["columnName"].(string)
|
||||
}
|
||||
|
||||
func (d *DbInstance) GetTableInfos() []map[string]interface{} {
|
||||
var sql string
|
||||
if d.Type == entity.DbTypeMysql {
|
||||
sql = MYSQL_TABLE_INFO
|
||||
} else if d.Type == entity.DbTypePostgres {
|
||||
sql = PGSQL_TABLE_INFO
|
||||
}
|
||||
_, res, _ := d.SelectData(sql)
|
||||
// 获取表信息,比GetTables获取更详细的表信息
|
||||
func (pm *PgsqlMetadata) GetTableInfos() []map[string]interface{} {
|
||||
_, res, _ := pm.di.SelectData(PGSQL_TABLE_INFO)
|
||||
return res
|
||||
}
|
||||
|
||||
func (d *DbInstance) GetTableIndex(tableName string) []map[string]interface{} {
|
||||
var sql string
|
||||
if d.Type == entity.DbTypeMysql {
|
||||
sql = fmt.Sprintf(MYSQL_INDEX_INFO, tableName)
|
||||
} else if d.Type == entity.DbTypePostgres {
|
||||
sql = fmt.Sprintf(PGSQL_INDEX_INFO, tableName)
|
||||
}
|
||||
_, res, _ := d.SelectData(sql)
|
||||
// 获取表索引信息
|
||||
func (pm *PgsqlMetadata) GetTableIndex(tableName string) []map[string]interface{} {
|
||||
_, res, _ := pm.di.SelectData(PGSQL_INDEX_INFO)
|
||||
return res
|
||||
}
|
||||
|
||||
func (d *DbInstance) GetCreateTableDdl(tableName string) []map[string]interface{} {
|
||||
var sql string
|
||||
if d.Type == entity.DbTypeMysql {
|
||||
sql = fmt.Sprintf("show create table %s ", tableName)
|
||||
}
|
||||
_, res, _ := d.SelectData(sql)
|
||||
return res
|
||||
// 获取建表ddl
|
||||
func (mm *PgsqlMetadata) GetCreateTableDdl(tableName string) []map[string]interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package application
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"mayfly-go/internal/devops/domain/entity"
|
||||
"mayfly-go/internal/devops/domain/repository"
|
||||
"mayfly-go/internal/devops/infrastructure/persistence"
|
||||
@@ -89,7 +90,7 @@ func doUpdate(update *sqlparser.Update, dbInstance *DbInstance, dbSqlExec *entit
|
||||
}
|
||||
|
||||
// 获取表主键列名,排除使用别名
|
||||
primaryKey := dbInstance.GetPrimaryKey(tableName)
|
||||
primaryKey := dbInstance.GetMeta().GetPrimaryKey(tableName)
|
||||
|
||||
updateColumnsAndPrimaryKey := strings.Join(updateColumns, ",") + "," + primaryKey
|
||||
// 查询要更新字段数据的旧值,以及主键值
|
||||
|
||||
@@ -69,11 +69,13 @@ func (m *machineAppImpl) Save(me *entity.Machine) {
|
||||
}
|
||||
// 关闭连接
|
||||
machine.DeleteCli(me.Id)
|
||||
me.PwdEncrypt()
|
||||
m.machineRepo.UpdateById(me)
|
||||
} else {
|
||||
biz.IsTrue(err != nil, "该机器信息已存在")
|
||||
// 新增机器,默认启用状态
|
||||
me.Status = entity.MachineStatusEnable
|
||||
me.PwdEncrypt()
|
||||
m.machineRepo.Create(me)
|
||||
}
|
||||
}
|
||||
@@ -123,6 +125,7 @@ func (m *machineAppImpl) GetById(id uint64, cols ...string) *entity.Machine {
|
||||
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
|
||||
})
|
||||
@@ -133,6 +136,7 @@ func (m *machineAppImpl) GetCli(id uint64) *machine.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
|
||||
})
|
||||
|
||||
@@ -80,6 +80,7 @@ func (r *redisAppImpl) Save(re *entity.Redis) {
|
||||
|
||||
if re.Id == 0 {
|
||||
biz.IsTrue(err != nil, "该库已存在")
|
||||
re.PwdEncrypt()
|
||||
r.redisRepo.Insert(re)
|
||||
} else {
|
||||
// 如果存在该库,则校验修改的库是否为该库
|
||||
@@ -88,6 +89,7 @@ func (r *redisAppImpl) Save(re *entity.Redis) {
|
||||
}
|
||||
// 先关闭数据库连接
|
||||
CloseRedis(re.Id)
|
||||
re.PwdEncrypt()
|
||||
r.redisRepo.Update(re)
|
||||
}
|
||||
}
|
||||
@@ -110,6 +112,7 @@ func (r *redisAppImpl) GetRedisInstance(id uint64) *RedisInstance {
|
||||
}
|
||||
// 缓存不存在,则回调获取redis信息
|
||||
re := r.GetById(id)
|
||||
re.PwdDecrypt()
|
||||
biz.NotNil(re, "redis信息不存在")
|
||||
|
||||
redisMode := re.Mode
|
||||
@@ -130,6 +133,14 @@ func (r *redisAppImpl) GetRedisInstance(id uint64) *RedisInstance {
|
||||
ri.Close()
|
||||
panic(biz.NewBizErr(fmt.Sprintf("redis集群连接失败: %s", e.Error())))
|
||||
}
|
||||
} else if redisMode == entity.RedisModeSentinel {
|
||||
ri = getRedisSentinelCient(re)
|
||||
// 测试连接
|
||||
_, e := ri.Cli.Ping(context.Background()).Result()
|
||||
if e != nil {
|
||||
ri.Close()
|
||||
panic(biz.NewBizErr(fmt.Sprintf("redis sentinel连接失败: %s", e.Error())))
|
||||
}
|
||||
}
|
||||
|
||||
global.Log.Infof("连接redis: %s", re.Host)
|
||||
@@ -174,6 +185,27 @@ func getRedisClusterClient(re *entity.Redis) *RedisInstance {
|
||||
return ri
|
||||
}
|
||||
|
||||
func getRedisSentinelCient(re *entity.Redis) *RedisInstance {
|
||||
ri := &RedisInstance{Id: re.Id, ProjectId: re.ProjectId, Mode: re.Mode}
|
||||
// sentinel模式host为 masterName=host:port,host:port
|
||||
masterNameAndHosts := strings.Split(re.Host, "=")
|
||||
sentinelOptions := &redis.FailoverOptions{
|
||||
MasterName: masterNameAndHosts[0],
|
||||
SentinelAddrs: strings.Split(masterNameAndHosts[1], ","),
|
||||
Password: re.Password, // no password set
|
||||
DB: re.Db, // use default DB
|
||||
DialTimeout: 8 * time.Second,
|
||||
ReadTimeout: -1, // Disable timeouts, because SSH does not support deadlines.
|
||||
WriteTimeout: -1,
|
||||
}
|
||||
if re.EnableSshTunnel == 1 {
|
||||
ri.sshTunnelMachineId = re.SshTunnelMachineId
|
||||
sentinelOptions.Dialer = getRedisDialer(re.SshTunnelMachineId)
|
||||
}
|
||||
ri.Cli = redis.NewFailoverClient(sentinelOptions)
|
||||
return ri
|
||||
}
|
||||
|
||||
func getRedisDialer(machineId uint64) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
sshTunnel := MachineApp.GetSshTunnelMachine(machineId)
|
||||
return func(_ context.Context, network, addr string) (net.Conn, error) {
|
||||
@@ -224,6 +256,10 @@ func TestRedisConnection(re *entity.Redis) {
|
||||
ccli := getRedisClusterClient(re)
|
||||
defer ccli.Close()
|
||||
cmd = ccli.ClusterCli
|
||||
} else if re.Mode == entity.RedisModeSentinel {
|
||||
rcli := getRedisSentinelCient(re)
|
||||
defer rcli.Close()
|
||||
cmd = rcli.Cli
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
@@ -244,10 +280,10 @@ type RedisInstance struct {
|
||||
// 获取命令执行接口的具体实现
|
||||
func (r *RedisInstance) GetCmdable() redis.Cmdable {
|
||||
redisMode := r.Mode
|
||||
if redisMode == "" || redisMode == entity.RedisModeStandalone {
|
||||
if redisMode == "" || redisMode == entity.RedisModeStandalone || r.Mode == entity.RedisModeSentinel {
|
||||
return r.Cli
|
||||
}
|
||||
if r.Mode == entity.RedisModeCluster {
|
||||
if redisMode == entity.RedisModeCluster {
|
||||
return r.ClusterCli
|
||||
}
|
||||
return nil
|
||||
@@ -260,7 +296,7 @@ func (r *RedisInstance) Scan(cursor uint64, match string, count int64) ([]string
|
||||
}
|
||||
|
||||
func (r *RedisInstance) Close() {
|
||||
if r.Mode == entity.RedisModeStandalone {
|
||||
if r.Mode == entity.RedisModeStandalone || r.Mode == entity.RedisModeSentinel {
|
||||
if err := r.Cli.Close(); err != nil {
|
||||
global.Log.Errorf("关闭redis单机实例[%d]连接失败: %s", r.Id, err.Error())
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/internal/common/utils"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
@@ -27,9 +28,9 @@ type Db struct {
|
||||
}
|
||||
|
||||
// 获取数据库连接网络, 若没有使用ssh隧道,则直接返回。否则返回拼接的网络需要注册至指定dial
|
||||
func (d Db) GetNetwork() string {
|
||||
func (d *Db) GetNetwork() string {
|
||||
network := d.Network
|
||||
if d.EnableSshTunnel == -1 {
|
||||
if d.EnableSshTunnel == 0 || d.EnableSshTunnel == -1 {
|
||||
if network == "" {
|
||||
return "tcp"
|
||||
} else {
|
||||
@@ -39,6 +40,16 @@ func (d Db) GetNetwork() string {
|
||||
return fmt.Sprintf("%s+ssh:%d", d.Type, d.SshTunnelMachineId)
|
||||
}
|
||||
|
||||
func (d *Db) PwdEncrypt() {
|
||||
// 密码替换为加密后的密码
|
||||
d.Password = utils.PwdAesEncrypt(d.Password)
|
||||
}
|
||||
|
||||
func (d *Db) PwdDecrypt() {
|
||||
// 密码替换为解密后的密码
|
||||
d.Password = utils.PwdAesDecrypt(d.Password)
|
||||
}
|
||||
|
||||
const (
|
||||
DbTypeMysql = "mysql"
|
||||
DbTypePostgres = "postgres"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/common/utils"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
@@ -26,3 +27,13 @@ const (
|
||||
MachineAuthMethodPassword int8 = 1 // 密码登录
|
||||
MachineAuthMethodPublicKey int8 = 2 // 公钥免密登录
|
||||
)
|
||||
|
||||
func (m *Machine) PwdEncrypt() {
|
||||
// 密码替换为加密后的密码
|
||||
m.Password = utils.PwdAesEncrypt(m.Password)
|
||||
}
|
||||
|
||||
func (m *Machine) PwdDecrypt() {
|
||||
// 密码替换为解密后的密码
|
||||
m.Password = utils.PwdAesDecrypt(m.Password)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/common/utils"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
@@ -23,4 +24,15 @@ type Redis struct {
|
||||
const (
|
||||
RedisModeStandalone = "standalone"
|
||||
RedisModeCluster = "cluster"
|
||||
RedisModeSentinel = "sentinel"
|
||||
)
|
||||
|
||||
func (r *Redis) PwdEncrypt() {
|
||||
// 密码替换为加密后的密码
|
||||
r.Password = utils.PwdAesEncrypt(r.Password)
|
||||
}
|
||||
|
||||
func (r *Redis) PwdDecrypt() {
|
||||
// 密码替换为解密后的密码
|
||||
r.Password = utils.PwdAesDecrypt(r.Password)
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package machine
|
||||
|
||||
const StatsShell = `
|
||||
cat /proc/uptime
|
||||
echo '-----'
|
||||
/bin/hostname -f
|
||||
echo '-----'
|
||||
cat /proc/loadavg
|
||||
echo '-----'
|
||||
cat /proc/meminfo
|
||||
echo '-----'
|
||||
df -B1
|
||||
echo '-----'
|
||||
/sbin/ip -o addr
|
||||
echo '-----'
|
||||
/bin/cat /proc/net/dev
|
||||
echo '-----'
|
||||
top -b -n 1 | grep Cpu
|
||||
`
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"io"
|
||||
"mayfly-go/internal/devops/domain/entity"
|
||||
"mayfly-go/pkg/global"
|
||||
"mayfly-go/pkg/scheduler"
|
||||
"mayfly-go/pkg/utils"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
@@ -31,30 +31,29 @@ type CheckSshTunnelMachineHasUseFunc func(uint64) bool
|
||||
|
||||
func startCheckUse() {
|
||||
global.Log.Info("开启定时检测ssh隧道机器是否还有被使用")
|
||||
heartbeat := time.Duration(10) * time.Minute
|
||||
tick := time.NewTicker(heartbeat)
|
||||
go func() {
|
||||
for range tick.C {
|
||||
func() {
|
||||
if !mutex.TryLock() {
|
||||
return
|
||||
}
|
||||
defer mutex.Unlock()
|
||||
// 遍历隧道机器,都未被使用将会被关闭
|
||||
for mid, sshTunnelMachine := range sshTunnelMachines {
|
||||
global.Log.Debugf("开始定时检查ssh隧道机器[%d]是否还有被使用...", mid)
|
||||
for _, checkUseFunc := range checkSshTunnelMachineHasUseFuncs {
|
||||
// 如果一个在使用则返回不关闭,不继续后续检查
|
||||
if checkUseFunc(mid) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// 都未被使用,则关闭
|
||||
sshTunnelMachine.Close()
|
||||
}
|
||||
}()
|
||||
// 每十分钟检查一次隧道机器是否还有被使用
|
||||
scheduler.AddFun("@every 10m", func() {
|
||||
if !mutex.TryLock() {
|
||||
return
|
||||
}
|
||||
}()
|
||||
defer mutex.Unlock()
|
||||
// 遍历隧道机器,都未被使用将会被关闭
|
||||
for mid, sshTunnelMachine := range sshTunnelMachines {
|
||||
global.Log.Debugf("开始定时检查ssh隧道机器[%d]是否还有被使用...", mid)
|
||||
hasUse := false
|
||||
for _, checkUseFunc := range checkSshTunnelMachineHasUseFuncs {
|
||||
// 如果一个在使用则返回不关闭,不继续后续检查
|
||||
if checkUseFunc(mid) {
|
||||
hasUse = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasUse {
|
||||
// 都未被使用,则关闭
|
||||
sshTunnelMachine.Close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加ssh隧道机器检测是否使用函数
|
||||
@@ -129,7 +128,10 @@ func (stm *SshTunnelMachine) Close() {
|
||||
|
||||
if stm.SshClient != nil {
|
||||
global.Log.Infof("ssh隧道机器[%d]未被使用, 关闭隧道...", stm.machineId)
|
||||
stm.SshClient.Close()
|
||||
err := stm.SshClient.Close()
|
||||
if err != nil {
|
||||
global.Log.Errorf("关闭ssh隧道机器[%d]发生错误: %s", stm.machineId, err.Error())
|
||||
}
|
||||
}
|
||||
delete(sshTunnelMachines, stm.machineId)
|
||||
}
|
||||
|
||||
@@ -53,6 +53,24 @@ type Stats struct {
|
||||
CPU CPUInfo // or []CPUInfo to get all the cpu-core's stats?
|
||||
}
|
||||
|
||||
const StatsShell = `
|
||||
cat /proc/uptime
|
||||
echo '-----'
|
||||
/bin/hostname -f
|
||||
echo '-----'
|
||||
cat /proc/loadavg
|
||||
echo '-----'
|
||||
cat /proc/meminfo
|
||||
echo '-----'
|
||||
df -B1
|
||||
echo '-----'
|
||||
/sbin/ip -o addr
|
||||
echo '-----'
|
||||
/bin/cat /proc/net/dev
|
||||
echo '-----'
|
||||
top -b -n 1 | grep Cpu
|
||||
`
|
||||
|
||||
func (c *Cli) GetAllStats() *Stats {
|
||||
res, _ := c.Run(StatsShell)
|
||||
infos := strings.Split(*res, "-----")
|
||||
|
||||
74
server/internal/devops/infrastructure/machine/terminal.go
Normal file
74
server/internal/devops/infrastructure/machine/terminal.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package machine
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Terminal struct {
|
||||
SshSession *ssh.Session
|
||||
StdinPipe io.WriteCloser
|
||||
StdoutReader *bufio.Reader
|
||||
}
|
||||
|
||||
// 新建机器ssh终端
|
||||
func NewTerminal(cli *Cli) (*Terminal, error) {
|
||||
sshSession, err := cli.GetSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stdoutPipe, err := sshSession.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdoutReader := bufio.NewReader(stdoutPipe)
|
||||
|
||||
stdinPipe, err := sshSession.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
terminal := Terminal{
|
||||
SshSession: sshSession,
|
||||
StdinPipe: stdinPipe,
|
||||
StdoutReader: stdoutReader,
|
||||
}
|
||||
|
||||
return &terminal, nil
|
||||
}
|
||||
|
||||
func (t *Terminal) Write(p []byte) (int, error) {
|
||||
return t.StdinPipe.Write(p)
|
||||
}
|
||||
|
||||
func (t *Terminal) ReadRune() (r rune, size int, err error) {
|
||||
return t.StdoutReader.ReadRune()
|
||||
}
|
||||
|
||||
func (t *Terminal) Close() error {
|
||||
if t.SshSession != nil {
|
||||
return t.SshSession.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Terminal) WindowChange(h int, w int) error {
|
||||
return t.SshSession.WindowChange(h, w)
|
||||
}
|
||||
|
||||
func (t *Terminal) RequestPty(term string, h, w int) error {
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ECHO: 1,
|
||||
ssh.TTY_OP_ISPEED: 14400,
|
||||
ssh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
|
||||
return t.SshSession.RequestPty(term, h, w, modes)
|
||||
}
|
||||
|
||||
func (t *Terminal) Shell() error {
|
||||
return t.SshSession.Shell()
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package machine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mayfly-go/pkg/global"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
Resize = 1
|
||||
Data = 2
|
||||
Ping = 3
|
||||
)
|
||||
|
||||
type TerminalSession struct {
|
||||
ID string
|
||||
wsConn *websocket.Conn
|
||||
terminal *Terminal
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
dataChan chan rune
|
||||
tick *time.Ticker
|
||||
}
|
||||
|
||||
func NewTerminalSession(sessionId string, ws *websocket.Conn, cli *Cli, rows, cols int) (*TerminalSession, error) {
|
||||
terminal, err := NewTerminal(cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = terminal.RequestPty("xterm-256color", rows, cols)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = terminal.Shell()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
tick := time.NewTicker(time.Millisecond * time.Duration(60))
|
||||
ts := &TerminalSession{
|
||||
ID: sessionId,
|
||||
wsConn: ws,
|
||||
terminal: terminal,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
dataChan: make(chan rune),
|
||||
tick: tick,
|
||||
}
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
func (r TerminalSession) Start() {
|
||||
go r.readFormTerminal()
|
||||
go r.writeToWebsocket()
|
||||
r.receiveWsMsg()
|
||||
}
|
||||
|
||||
func (r TerminalSession) Stop() {
|
||||
global.Log.Debug("close machine ssh terminal session")
|
||||
r.tick.Stop()
|
||||
r.cancel()
|
||||
if r.wsConn != nil {
|
||||
r.wsConn.Close()
|
||||
}
|
||||
if r.terminal != nil {
|
||||
if err := r.terminal.Close(); err != nil {
|
||||
global.Log.Errorf("关闭机器ssh终端失败: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ts TerminalSession) readFormTerminal() {
|
||||
for {
|
||||
select {
|
||||
case <-ts.ctx.Done():
|
||||
return
|
||||
default:
|
||||
rn, size, err := ts.terminal.ReadRune()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
global.Log.Error("机器ssh终端读取消息失败: ", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if size > 0 {
|
||||
ts.dataChan <- rn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ts TerminalSession) writeToWebsocket() {
|
||||
var buf []byte
|
||||
for {
|
||||
select {
|
||||
case <-ts.ctx.Done():
|
||||
return
|
||||
case <-ts.tick.C:
|
||||
if len(buf) > 0 {
|
||||
s := string(buf)
|
||||
if err := WriteMessage(ts.wsConn, s); err != nil {
|
||||
global.Log.Error("机器ssh终端发送消息至websocket失败: ", err)
|
||||
return
|
||||
}
|
||||
buf = []byte{}
|
||||
}
|
||||
case data := <-ts.dataChan:
|
||||
if data != utf8.RuneError {
|
||||
p := make([]byte, utf8.RuneLen(data))
|
||||
utf8.EncodeRune(p, data)
|
||||
buf = append(buf, p...)
|
||||
} else {
|
||||
buf = append(buf, []byte("@")...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type WsMsg struct {
|
||||
Type int `json:"type"`
|
||||
Msg string `json:"msg"`
|
||||
Cols int `json:"cols"`
|
||||
Rows int `json:"rows"`
|
||||
}
|
||||
|
||||
func (ts *TerminalSession) receiveWsMsg() {
|
||||
wsConn := ts.wsConn
|
||||
for {
|
||||
select {
|
||||
case <-ts.ctx.Done():
|
||||
return
|
||||
default:
|
||||
// read websocket msg
|
||||
_, wsData, err := wsConn.ReadMessage()
|
||||
if err != nil {
|
||||
global.Log.Debug("机器ssh终端读取websocket消息失败: ", err)
|
||||
return
|
||||
}
|
||||
// 解析消息
|
||||
msgObj := WsMsg{}
|
||||
if err := json.Unmarshal(wsData, &msgObj); err != nil {
|
||||
global.Log.Error("机器ssh终端消息解析失败: ", err)
|
||||
}
|
||||
switch msgObj.Type {
|
||||
case Resize:
|
||||
if msgObj.Cols > 0 && msgObj.Rows > 0 {
|
||||
if err := ts.terminal.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
|
||||
global.Log.Error("ssh pty change windows size failed")
|
||||
}
|
||||
}
|
||||
case Data:
|
||||
_, err := ts.terminal.Write([]byte(msgObj.Msg))
|
||||
if err != nil {
|
||||
global.Log.Debug("机器ssh终端写入消息失败: %s", err)
|
||||
}
|
||||
case Ping:
|
||||
_, err := ts.terminal.SshSession.SendRequest("ping", true, nil)
|
||||
if err != nil {
|
||||
WriteMessage(wsConn, "\r\n\033[1;31m提示: 终端连接已断开...\033[0m")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WriteMessage(ws *websocket.Conn, msg string) error {
|
||||
return ws.WriteMessage(websocket.TextMessage, []byte(msg))
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package machine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mayfly-go/pkg/global"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type safeBuffer struct {
|
||||
buffer bytes.Buffer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (w *safeBuffer) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.buffer.Write(p)
|
||||
}
|
||||
func (w *safeBuffer) Bytes() []byte {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.buffer.Bytes()
|
||||
}
|
||||
func (w *safeBuffer) Reset() {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.buffer.Reset()
|
||||
}
|
||||
|
||||
const (
|
||||
wsMsgCmd = "cmd"
|
||||
wsMsgResize = "resize"
|
||||
)
|
||||
|
||||
type WsMsg struct {
|
||||
Type string `json:"type"`
|
||||
Msg string `json:"msg"`
|
||||
Cols int `json:"cols"`
|
||||
Rows int `json:"rows"`
|
||||
}
|
||||
|
||||
type LogicSshWsSession struct {
|
||||
stdinPipe io.WriteCloser
|
||||
comboOutput *safeBuffer //ssh 终端混合输出
|
||||
inputFilterBuff *safeBuffer //用来过滤输入的命令和ssh_filter配置对比的
|
||||
session *ssh.Session
|
||||
wsConn *websocket.Conn
|
||||
}
|
||||
|
||||
func NewLogicSshWsSession(cols, rows int, cli *Cli, wsConn *websocket.Conn) (*LogicSshWsSession, error) {
|
||||
sshSession, err := cli.GetSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stdinP, err := sshSession.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comboWriter := new(safeBuffer)
|
||||
inputBuf := new(safeBuffer)
|
||||
//ssh.stdout and stderr will write output into comboWriter
|
||||
sshSession.Stdout = comboWriter
|
||||
sshSession.Stderr = comboWriter
|
||||
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ECHO: 1, // disable echo
|
||||
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
|
||||
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
||||
}
|
||||
// Request pseudo terminal
|
||||
if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Start remote shell
|
||||
if err := sshSession.Shell(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &LogicSshWsSession{
|
||||
stdinPipe: stdinP,
|
||||
comboOutput: comboWriter,
|
||||
inputFilterBuff: inputBuf,
|
||||
session: sshSession,
|
||||
wsConn: wsConn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
//Close 关闭
|
||||
func (sws *LogicSshWsSession) Close() {
|
||||
if sws.session != nil {
|
||||
sws.session.Close()
|
||||
}
|
||||
if sws.comboOutput != nil {
|
||||
sws.comboOutput = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (sws *LogicSshWsSession) Start(quitChan chan bool) {
|
||||
go sws.receiveWsMsg(quitChan)
|
||||
go sws.sendComboOutput(quitChan)
|
||||
}
|
||||
|
||||
//receiveWsMsg receive websocket msg do some handling then write into ssh.session.stdin
|
||||
func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) {
|
||||
wsConn := sws.wsConn
|
||||
//tells other go routine quit
|
||||
defer setQuit(exitCh)
|
||||
for {
|
||||
select {
|
||||
case <-exitCh:
|
||||
return
|
||||
default:
|
||||
//read websocket msg
|
||||
_, wsData, err := wsConn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
||||
return
|
||||
}
|
||||
global.Log.Error("reading webSocket message failed: ", err)
|
||||
return
|
||||
}
|
||||
//unmashal bytes into struct
|
||||
msgObj := WsMsg{}
|
||||
if err := json.Unmarshal(wsData, &msgObj); err != nil {
|
||||
global.Log.Error("unmarshal websocket message failed:", err)
|
||||
}
|
||||
switch msgObj.Type {
|
||||
case wsMsgResize:
|
||||
//handle xterm.js size change
|
||||
if msgObj.Cols > 0 && msgObj.Rows > 0 {
|
||||
if err := sws.session.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
|
||||
global.Log.Error("ssh pty change windows size failed")
|
||||
}
|
||||
}
|
||||
case wsMsgCmd:
|
||||
sws.sendWebsocketInputCommandToSshSessionStdinPipe([]byte(msgObj.Msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//sendWebsocketInputCommandToSshSessionStdinPipe
|
||||
func (sws *LogicSshWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) {
|
||||
if _, err := sws.stdinPipe.Write(cmdBytes); err != nil {
|
||||
global.Log.Error("ws cmd bytes write to ssh.stdin pipe failed")
|
||||
}
|
||||
}
|
||||
|
||||
func (sws *LogicSshWsSession) sendComboOutput(exitCh chan bool) {
|
||||
wsConn := sws.wsConn
|
||||
//todo 优化成一个方法
|
||||
//tells other go routine quit
|
||||
defer setQuit(exitCh)
|
||||
|
||||
//every 120ms write combine output bytes into websocket response
|
||||
tick := time.NewTicker(time.Millisecond * time.Duration(60))
|
||||
//for range time.Tick(120 * time.Millisecond){}
|
||||
defer tick.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
if sws.comboOutput == nil {
|
||||
return
|
||||
}
|
||||
bs := sws.comboOutput.Bytes()
|
||||
if len(bs) > 0 {
|
||||
err := wsConn.WriteMessage(websocket.TextMessage, bs)
|
||||
if err != nil {
|
||||
global.Log.Error("ssh sending combo output to webSocket failed")
|
||||
}
|
||||
sws.comboOutput.buffer.Reset()
|
||||
}
|
||||
|
||||
case <-exitCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sws *LogicSshWsSession) Wait(quitChan chan bool) {
|
||||
if err := sws.session.Wait(); err != nil {
|
||||
setQuit(quitChan)
|
||||
}
|
||||
}
|
||||
|
||||
func setQuit(ch chan bool) {
|
||||
ch <- true
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package scheduler
|
||||
|
||||
func init() {
|
||||
SaveMachineMonitor()
|
||||
}
|
||||
|
||||
func SaveMachineMonitor() {
|
||||
AddFun("@every 60s", func() {
|
||||
// for _, m := range models.GetNeedMonitorMachine() {
|
||||
// m := m
|
||||
// go func() {
|
||||
// cli, err := machine.GetCli(uint64(utils.GetInt4Map(m, "id")))
|
||||
// if err != nil {
|
||||
// mlog.Log.Error("获取客户端失败:", err.Error())
|
||||
// return
|
||||
// }
|
||||
// mm := cli.GetMonitorInfo()
|
||||
// if mm != nil {
|
||||
// err := model.Insert(mm)
|
||||
// if err != nil {
|
||||
// mlog.Log.Error("保存机器监控信息失败: ", err.Error())
|
||||
// }
|
||||
// }
|
||||
// }()
|
||||
// }
|
||||
})
|
||||
}
|
||||
@@ -20,8 +20,7 @@ func InitDbRouter(router *gin.RouterGroup) {
|
||||
}
|
||||
// 获取所有数据库列表
|
||||
db.GET("", func(c *gin.Context) {
|
||||
rc := ctx.NewReqCtxWithGin(c)
|
||||
rc.Handle(d.Dbs)
|
||||
ctx.NewReqCtxWithGin(c).Handle(d.Dbs)
|
||||
})
|
||||
|
||||
saveDb := ctx.NewLogInfo("保存数据库信息").WithSave(true)
|
||||
@@ -31,11 +30,16 @@ func InitDbRouter(router *gin.RouterGroup) {
|
||||
Handle(d.Save)
|
||||
})
|
||||
|
||||
// 获取数据库实例的所有数据库名
|
||||
db.POST("databases", func(c *gin.Context) {
|
||||
ctx.NewReqCtxWithGin(c).
|
||||
Handle(d.GetDatabaseNames)
|
||||
})
|
||||
|
||||
db.GET(":dbId/pwd", func(c *gin.Context) {
|
||||
ctx.NewReqCtxWithGin(c).Handle(d.GetDbPwd)
|
||||
})
|
||||
|
||||
deleteDb := ctx.NewLogInfo("删除数据库信息").WithSave(true)
|
||||
db.DELETE(":dbId", func(c *gin.Context) {
|
||||
ctx.NewReqCtxWithGin(c).
|
||||
|
||||
@@ -20,6 +20,10 @@ func InitMachineRouter(router *gin.RouterGroup) {
|
||||
ctx.NewReqCtxWithGin(c).Handle(m.Machines)
|
||||
})
|
||||
|
||||
machines.GET(":machineId/pwd", func(c *gin.Context) {
|
||||
ctx.NewReqCtxWithGin(c).Handle(m.GetMachinePwd)
|
||||
})
|
||||
|
||||
machines.GET(":machineId/stats", func(c *gin.Context) {
|
||||
ctx.NewReqCtxWithGin(c).Handle(m.MachineStats)
|
||||
})
|
||||
|
||||
@@ -26,6 +26,10 @@ func InitRedisRouter(router *gin.RouterGroup) {
|
||||
ctx.NewReqCtxWithGin(c).WithLog(save).Handle(rs.Save)
|
||||
})
|
||||
|
||||
redis.GET(":id/pwd", func(c *gin.Context) {
|
||||
ctx.NewReqCtxWithGin(c).Handle(rs.GetRedisPwd)
|
||||
})
|
||||
|
||||
delRedis := ctx.NewLogInfo("删除redis信息").WithSave(true)
|
||||
redis.DELETE(":id", func(c *gin.Context) {
|
||||
ctx.NewReqCtxWithGin(c).WithLog(delRedis).Handle(rs.DeleteRedis)
|
||||
@@ -60,9 +64,17 @@ func InitRedisRouter(router *gin.RouterGroup) {
|
||||
ctx.NewReqCtxWithGin(c).Handle(rs.SetStringValue)
|
||||
})
|
||||
|
||||
// 获取hash类型值
|
||||
redis.GET(":id/hash-value", func(c *gin.Context) {
|
||||
ctx.NewReqCtxWithGin(c).Handle(rs.GetHashValue)
|
||||
// hscan
|
||||
redis.GET(":id/hscan", func(c *gin.Context) {
|
||||
ctx.NewReqCtxWithGin(c).Handle(rs.Hscan)
|
||||
})
|
||||
|
||||
redis.GET(":id/hget", func(c *gin.Context) {
|
||||
ctx.NewReqCtxWithGin(c).Handle(rs.Hget)
|
||||
})
|
||||
|
||||
redis.DELETE(":id/hdel", func(c *gin.Context) {
|
||||
ctx.NewReqCtxWithGin(c).Handle(rs.Hdel)
|
||||
})
|
||||
|
||||
// 设置hash类型值
|
||||
|
||||
@@ -38,8 +38,10 @@ func (a *Account) Login(rc *ctx.ReqCtx) {
|
||||
originPwd, err := utils.DefaultRsaDecrypt(loginForm.Password, true)
|
||||
biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
|
||||
|
||||
account := &entity.Account{Username: loginForm.Username, Password: utils.Md5(originPwd)}
|
||||
biz.ErrIsNil(a.AccountApp.GetAccount(account, "Id", "Username", "Status", "LastLoginTime", "LastLoginIp"), "用户名或密码错误")
|
||||
account := &entity.Account{Username: loginForm.Username}
|
||||
err = a.AccountApp.GetAccount(account, "Id", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp")
|
||||
biz.ErrIsNil(err, "用户名或密码错误")
|
||||
biz.IsTrue(utils.CheckPwdHash(originPwd, account.Password), "用户名或密码错误")
|
||||
biz.IsTrue(account.IsEnable(), "该账号不可用")
|
||||
|
||||
// 校验密码强度是否符合
|
||||
@@ -86,8 +88,11 @@ func (a *Account) ChangePassword(rc *ctx.ReqCtx) {
|
||||
originOldPwd, err := utils.DefaultRsaDecrypt(form.OldPassword, true)
|
||||
biz.ErrIsNilAppendErr(err, "解密旧密码错误: %s")
|
||||
|
||||
account := &entity.Account{Username: form.Username, Password: utils.Md5(originOldPwd)}
|
||||
biz.ErrIsNil(a.AccountApp.GetAccount(account, "Id", "Username", "Status"), "旧密码不正确")
|
||||
account := &entity.Account{Username: form.Username}
|
||||
err = a.AccountApp.GetAccount(account, "Id", "Username", "Password", "Status")
|
||||
biz.ErrIsNil(err, "旧密码错误")
|
||||
biz.IsTrue(utils.CheckPwdHash(originOldPwd, account.Password), "旧密码错误")
|
||||
biz.IsTrue(account.IsEnable(), "该账号不可用")
|
||||
|
||||
originNewPwd, err := utils.DefaultRsaDecrypt(form.NewPassword, true)
|
||||
biz.ErrIsNilAppendErr(err, "解密新密码错误: %s")
|
||||
@@ -95,7 +100,7 @@ func (a *Account) ChangePassword(rc *ctx.ReqCtx) {
|
||||
|
||||
updateAccount := new(entity.Account)
|
||||
updateAccount.Id = account.Id
|
||||
updateAccount.Password = utils.Md5(originNewPwd)
|
||||
updateAccount.Password = utils.PwdHash(originNewPwd)
|
||||
a.AccountApp.Update(updateAccount)
|
||||
|
||||
// 赋值loginAccount 主要用于记录操作日志,因为操作日志保存请求上下文没有该信息不保存日志
|
||||
@@ -176,7 +181,7 @@ func (a *Account) UpdateAccount(rc *ctx.ReqCtx) {
|
||||
|
||||
if updateAccount.Password != "" {
|
||||
biz.IsTrue(CheckPasswordLever(updateAccount.Password), "密码强度必须8位以上且包含字⺟⼤⼩写+数字+特殊符号")
|
||||
updateAccount.Password = utils.Md5(updateAccount.Password)
|
||||
updateAccount.Password = utils.PwdHash(updateAccount.Password)
|
||||
}
|
||||
a.AccountApp.Update(updateAccount)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (a *accountAppImpl) GetPageList(condition *entity.Account, pageParam *model
|
||||
func (a *accountAppImpl) Create(account *entity.Account) {
|
||||
biz.IsTrue(a.GetAccount(&entity.Account{Username: account.Username}) != nil, "该账号用户名已存在")
|
||||
// 默认密码为账号用户名
|
||||
account.Password = utils.Md5(account.Username)
|
||||
account.Password = utils.PwdHash(account.Username)
|
||||
account.Status = entity.AccountEnableStatus
|
||||
a.accountRepo.Insert(account)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,5 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
starter.PrintBanner()
|
||||
starter.InitDb()
|
||||
starter.RunWebServer()
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ CREATE TABLE `t_db` (
|
||||
`database` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '数据库,空格分割多个数据库',
|
||||
`params` varchar(125) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '其他连接参数',
|
||||
`network` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`enableSshTunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
|
||||
`sshTunnelMachineId` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
|
||||
`enable_ssh_tunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
|
||||
`ssh_tunnel_machine_id` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
|
||||
`project_id` bigint(20) DEFAULT NULL,
|
||||
`project` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`env_id` bigint(20) DEFAULT NULL COMMENT '环境id',
|
||||
@@ -111,8 +111,8 @@ CREATE TABLE `t_machine` (
|
||||
`username` varchar(12) COLLATE utf8mb4_bin NOT NULL,
|
||||
`auth_method` tinyint(2) NULL DEFAULT NULL COMMENT '1.密码登录2.publickey登录',
|
||||
`password` varchar(3200) COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`enableSshTunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
|
||||
`sshTunnelMachineId` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
|
||||
`enable_ssh_tunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
|
||||
`ssh_tunnel_machine_id` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
|
||||
`status` tinyint(2) NOT NULL COMMENT '状态: 1:启用; -1:禁用',
|
||||
`remark` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`need_monitor` tinyint(2) DEFAULT NULL,
|
||||
@@ -260,11 +260,11 @@ DROP TABLE IF EXISTS `t_redis`;
|
||||
CREATE TABLE `t_redis` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`host` varchar(255) COLLATE utf8mb4_bin NOT NULL,
|
||||
`password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`password` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`db` int(32) DEFAULT NULL,
|
||||
`mode` varchar(32) DEFAULT NULL,
|
||||
`enableSshTunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
|
||||
`sshTunnelMachineId` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
|
||||
`enable_ssh_tunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
|
||||
`ssh_tunnel_machine_id` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
|
||||
`remark` varchar(125) DEFAULT NULL,
|
||||
`project_id` bigint(20) DEFAULT NULL,
|
||||
`project` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
@@ -286,7 +286,7 @@ CREATE TABLE `t_redis` (
|
||||
DROP TABLE IF EXISTS `t_sys_account`;
|
||||
CREATE TABLE `t_sys_account` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(12) COLLATE utf8mb4_bin NOT NULL,
|
||||
`username` varchar(30) COLLATE utf8mb4_bin NOT NULL,
|
||||
`password` varchar(64) COLLATE utf8mb4_bin NOT NULL,
|
||||
`status` tinyint(4) DEFAULT NULL,
|
||||
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
|
||||
@@ -304,7 +304,7 @@ CREATE TABLE `t_sys_account` (
|
||||
-- Records of t_sys_account
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `t_sys_account` VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, '2021-11-17 16:30:02', '12.0.216.228', '2020-01-01 19:00:00', 1, 'admin', '2020-01-01 19:00:00', 1, 'admin');
|
||||
INSERT INTO `t_sys_account` VALUES (1, 'admin', '$2a$10$w3Wky2U.tinvR7c/s0aKPuwZsIu6pM1/DMJalwBDMbE6niHIxVrrm', 1, '2021-11-17 16:30:02', '12.0.216.228', '2020-01-01 19:00:00', 1, 'admin', '2020-01-01 19:00:00', 1, 'admin');
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
@@ -670,8 +670,8 @@ CREATE TABLE `t_mongo` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(36) COLLATE utf8mb4_bin NOT NULL COMMENT '名称',
|
||||
`uri` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '连接uri',
|
||||
`enableSshTunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
|
||||
`sshTunnelMachineId` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
|
||||
`enable_ssh_tunnel` tinyint(2) DEFAULT NULL COMMENT '是否启用ssh隧道',
|
||||
`ssh_tunnel_machine_id` bigint(20) DEFAULT NULL COMMENT 'ssh隧道的机器id',
|
||||
`project_id` bigint(20) NOT NULL,
|
||||
`project` varchar(36) COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`env_id` bigint(20) DEFAULT NULL,
|
||||
|
||||
27
server/pkg/config/aes.go
Normal file
27
server/pkg/config/aes.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/pkg/utils"
|
||||
"mayfly-go/pkg/utils/assert"
|
||||
)
|
||||
|
||||
type Aes struct {
|
||||
Key string `yaml:"key"`
|
||||
}
|
||||
|
||||
// 编码并base64
|
||||
func (a *Aes) EncryptBase64(data []byte) (string, error) {
|
||||
return utils.AesEncryptBase64(data, []byte(a.Key))
|
||||
}
|
||||
|
||||
// base64解码后再aes解码
|
||||
func (a *Aes) DecryptBase64(data string) ([]byte, error) {
|
||||
return utils.AesDecryptBase64(data, []byte(a.Key))
|
||||
}
|
||||
|
||||
func (j *Aes) Valid() {
|
||||
aesKeyLen := len(j.Key)
|
||||
assert.IsTrue(aesKeyLen == 16 || aesKeyLen == 24 || aesKeyLen == 32,
|
||||
fmt.Sprintf("config.yml之 [aes.key] 长度需为16、24、32位长度, 当前为%d位", aesKeyLen))
|
||||
}
|
||||
@@ -2,11 +2,11 @@ package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
type App struct {
|
||||
Name string `yaml:"name"`
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
const (
|
||||
AppName = "mayfly-go"
|
||||
Version = "v1.2.6"
|
||||
)
|
||||
|
||||
func (a *App) GetAppInfo() string {
|
||||
return fmt.Sprintf("[%s:%s]", a.Name, a.Version)
|
||||
func GetAppInfo() string {
|
||||
return fmt.Sprintf("[%s:%s]", AppName, Version)
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
// 配置文件映射对象
|
||||
var Conf *Config
|
||||
|
||||
func init() {
|
||||
func Init() {
|
||||
configFilePath := flag.String("e", "./config.yml", "配置文件路径,默认为可执行文件目录")
|
||||
flag.Parse()
|
||||
// 获取启动参数中,配置文件的绝对路径
|
||||
path, _ := filepath.Abs(*configFilePath)
|
||||
startConfigParam = &CmdConfigParam{ConfigFilePath: path}
|
||||
startConfigParam := &CmdConfigParam{ConfigFilePath: path}
|
||||
// 读取配置文件信息
|
||||
yc := &Config{}
|
||||
if err := utils.LoadYml(startConfigParam.ConfigFilePath, yc); err != nil {
|
||||
@@ -32,14 +32,11 @@ type CmdConfigParam struct {
|
||||
ConfigFilePath string // -e 配置文件路径
|
||||
}
|
||||
|
||||
// 启动可执行文件时的参数
|
||||
var startConfigParam *CmdConfigParam
|
||||
|
||||
// yaml配置文件映射对象
|
||||
type Config struct {
|
||||
App *App `yaml:"app"`
|
||||
Server *Server `yaml:"server"`
|
||||
Jwt *Jwt `yaml:"jwt"`
|
||||
Aes *Aes `yaml:"aes"`
|
||||
Redis *Redis `yaml:"redis"`
|
||||
Mysql *Mysql `yaml:"mysql"`
|
||||
Log *Log `yaml:"log"`
|
||||
@@ -49,14 +46,7 @@ type Config struct {
|
||||
func (c *Config) Valid() {
|
||||
assert.IsTrue(c.Jwt != nil, "配置文件的[jwt]信息不能为空")
|
||||
c.Jwt.Valid()
|
||||
}
|
||||
|
||||
// 获取执行可执行文件时,指定的启动参数
|
||||
func getStartConfig() *CmdConfigParam {
|
||||
configFilePath := flag.String("e", "./config.yml", "配置文件路径,默认为可执行文件目录")
|
||||
flag.Parse()
|
||||
// 获取配置文件绝对路径
|
||||
path, _ := filepath.Abs(*configFilePath)
|
||||
sc := &CmdConfigParam{ConfigFilePath: path}
|
||||
return sc
|
||||
if c.Aes != nil {
|
||||
c.Aes.Valid()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,5 @@ type Jwt struct {
|
||||
}
|
||||
|
||||
func (j *Jwt) Valid() {
|
||||
assert.IsTrue(j.Key != "", "config.yml之 [jwt.key] 不能为空")
|
||||
assert.IsTrue(j.ExpireTime != 0, "config.yml之 [jwt.expire-time] 不能为空")
|
||||
}
|
||||
|
||||
@@ -5,15 +5,22 @@ import (
|
||||
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/config"
|
||||
"mayfly-go/pkg/global"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
var (
|
||||
JwtKey = config.Conf.Jwt.Key
|
||||
func InitTokenConfig() {
|
||||
JwtKey = config.Conf.Jwt.Key
|
||||
ExpTime = config.Conf.Jwt.ExpireTime
|
||||
}
|
||||
|
||||
var (
|
||||
JwtKey string
|
||||
ExpTime uint64
|
||||
)
|
||||
|
||||
// 创建用户token
|
||||
@@ -26,6 +33,11 @@ func CreateToken(userId uint64, username string) string {
|
||||
"exp": time.Now().Add(time.Minute * time.Duration(ExpTime)).Unix(),
|
||||
})
|
||||
|
||||
// 如果配置文件中的jwt key为空,则随机生成字符串
|
||||
if JwtKey == "" {
|
||||
JwtKey = utils.RandString(32)
|
||||
global.Log.Infof("config.yml未配置jwt.key, 随机生成key为: %s", JwtKey)
|
||||
}
|
||||
// 使用自定义字符串加密 and get the complete encoded token as a string
|
||||
tokenString, err := token.SignedString([]byte(JwtKey))
|
||||
biz.ErrIsNil(err, "token创建失败")
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
var Log = logrus.New()
|
||||
|
||||
func init() {
|
||||
func Init() {
|
||||
Log.SetFormatter(new(LogFormatter))
|
||||
Log.SetReportCaller(true)
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ import (
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Start()
|
||||
}
|
||||
|
||||
var cronService = cron.New()
|
||||
|
||||
func Start() {
|
||||
@@ -1,16 +1,17 @@
|
||||
package starter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/pkg/config"
|
||||
"mayfly-go/pkg/global"
|
||||
)
|
||||
|
||||
func PrintBanner() {
|
||||
global.Log.Print(`
|
||||
__ _
|
||||
_ __ ___ __ _ _ _ / _| |_ _ __ _ ___
|
||||
| '_ ' _ \ / _' | | | | |_| | | | |_____ / _' |/ _ \
|
||||
| | | | | | (_| | |_| | _| | |_| |_____| (_| | (_) |
|
||||
|_| |_| |_|\__,_|\__, |_| |_|\__, | \__, |\___/
|
||||
|___/ |___/ |___/
|
||||
`)
|
||||
func printBanner() {
|
||||
global.Log.Print(fmt.Sprintf(`
|
||||
__ _
|
||||
_ __ ___ __ _ _ _ / _| |_ _ __ _ ___
|
||||
| '_ ' _ \ / _' | | | | |_| | | | |_____ / _' |/ _ \
|
||||
| | | | | | (_| | |_| | _| | |_| |_____| (_| | (_) | version: %s
|
||||
|_| |_| |_|\__,_|\__, |_| |_|\__, | \__, |\___/
|
||||
|___/ |___/ |___/ `, config.Version))
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
func InitDb() {
|
||||
global.Db = GormMysql()
|
||||
func initDb() {
|
||||
global.Db = gormMysql()
|
||||
}
|
||||
|
||||
func GormMysql() *gorm.DB {
|
||||
func gormMysql() *gorm.DB {
|
||||
m := config.Conf.Mysql
|
||||
if m == nil || m.Dbname == "" {
|
||||
global.Log.Panic("未找到数据库配置信息")
|
||||
|
||||
23
server/pkg/starter/run.go
Normal file
23
server/pkg/starter/run.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package starter
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/config"
|
||||
"mayfly-go/pkg/ctx"
|
||||
"mayfly-go/pkg/logger"
|
||||
)
|
||||
|
||||
func RunWebServer() {
|
||||
// 初始化config.yml配置文件映射信息
|
||||
config.Init()
|
||||
// 初始化日志配置信息
|
||||
logger.Init()
|
||||
// 初始化jwt key与expire time等
|
||||
ctx.InitTokenConfig()
|
||||
|
||||
// 打印banner
|
||||
printBanner()
|
||||
// 初始化并赋值数据库全局变量
|
||||
initDb()
|
||||
// 运行web服务
|
||||
runWebServer()
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"mayfly-go/pkg/global"
|
||||
)
|
||||
|
||||
func RunWebServer() {
|
||||
func runWebServer() {
|
||||
// 权限处理器
|
||||
ctx.UseBeforeHandlerInterceptor(ctx.PermissionHandler)
|
||||
// 日志处理器
|
||||
@@ -21,11 +21,7 @@ func RunWebServer() {
|
||||
|
||||
server := config.Conf.Server
|
||||
port := server.GetPort()
|
||||
if app := config.Conf.App; app != nil {
|
||||
global.Log.Infof("%s- Listening and serving HTTP on %s", app.GetAppInfo(), port)
|
||||
} else {
|
||||
global.Log.Infof("Listening and serving HTTP on %s", port)
|
||||
}
|
||||
global.Log.Infof("Listening and serving HTTP on %s", port)
|
||||
|
||||
var err error
|
||||
if server.Tls != nil && server.Tls.Enable {
|
||||
|
||||
@@ -2,6 +2,8 @@ package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
@@ -10,6 +12,8 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// md5
|
||||
@@ -19,6 +23,17 @@ func Md5(str string) string {
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// bcrypt加密密码
|
||||
func PwdHash(password string) string {
|
||||
bytes, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
// 检查密码是否一致
|
||||
func CheckPwdHash(password, hash string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||||
}
|
||||
|
||||
// 系统统一RSA秘钥对
|
||||
var RsaPair []string
|
||||
|
||||
@@ -130,3 +145,84 @@ func GetRsaPrivateKey() (string, error) {
|
||||
RsaPair = append(RsaPair, publicKey)
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
//AesEncrypt 加密
|
||||
func AesEncrypt(data []byte, key []byte) ([]byte, error) {
|
||||
//创建加密实例
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//判断加密快的大小
|
||||
blockSize := block.BlockSize()
|
||||
//填充
|
||||
encryptBytes := pkcs7Padding(data, blockSize)
|
||||
//初始化加密数据接收切片
|
||||
crypted := make([]byte, len(encryptBytes))
|
||||
//使用cbc加密模式
|
||||
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
|
||||
//执行加密
|
||||
blockMode.CryptBlocks(crypted, encryptBytes)
|
||||
return crypted, nil
|
||||
}
|
||||
|
||||
//AesDecrypt 解密
|
||||
func AesDecrypt(data []byte, key []byte) ([]byte, error) {
|
||||
//创建实例
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//获取块的大小
|
||||
blockSize := block.BlockSize()
|
||||
//使用cbc
|
||||
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
|
||||
//初始化解密数据接收切片
|
||||
crypted := make([]byte, len(data))
|
||||
//执行解密
|
||||
blockMode.CryptBlocks(crypted, data)
|
||||
//去除填充
|
||||
crypted, err = pkcs7UnPadding(crypted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crypted, nil
|
||||
}
|
||||
|
||||
// aes加密 后 再base64
|
||||
func AesEncryptBase64(data []byte, key []byte) (string, error) {
|
||||
res, err := AesEncrypt(data, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(res), nil
|
||||
}
|
||||
|
||||
// base64解码后再 aes解码
|
||||
func AesDecryptBase64(data string, key []byte) ([]byte, error) {
|
||||
dataByte, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return AesDecrypt(dataByte, key)
|
||||
}
|
||||
|
||||
//pkcs7Padding 填充
|
||||
func pkcs7Padding(data []byte, blockSize int) []byte {
|
||||
//判断缺少几位长度。最少1,最多 blockSize
|
||||
padding := blockSize - len(data)%blockSize
|
||||
//补足位数。把切片[]byte{byte(padding)}复制padding个
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(data, padText...)
|
||||
}
|
||||
|
||||
//pkcs7UnPadding 填充的反向操作
|
||||
func pkcs7UnPadding(data []byte) ([]byte, error) {
|
||||
length := len(data)
|
||||
if length == 0 {
|
||||
return nil, errors.New("加密字符串错误!")
|
||||
}
|
||||
//获取填充的个数
|
||||
unPadding := int(data[length-1])
|
||||
return data[:(length - unPadding)], nil
|
||||
}
|
||||
|
||||
25
server/pkg/utils/rand.go
Normal file
25
server/pkg/utils/rand.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
const randChar = "0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
// 生成随机字符串
|
||||
func RandString(l int) string {
|
||||
strList := []byte(randChar)
|
||||
|
||||
result := []byte{}
|
||||
i := 0
|
||||
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
charLen := len(strList)
|
||||
for i < l {
|
||||
new := strList[r.Intn(charLen)]
|
||||
result = append(result, new)
|
||||
i = i + 1
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func checkConn() {
|
||||
|
||||
// 删除ws连接
|
||||
func Delete(userid uint64) {
|
||||
global.Log.Info("移除websocket连接:uid = ", userid)
|
||||
global.Log.Debug("移除websocket连接:uid = ", userid)
|
||||
conn := conns[userid]
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
相关配置文件:
|
||||
后端:
|
||||
config.yml: 服务端口,mysql等信息在此配置即可。
|
||||
config.yml: 服务端口,mysql,aeskey(16 24 32位),jwtkey等信息在此配置即可。
|
||||
建议务必将aes.key(资源密码加密如机器、数据库、redis等密码)与jwt.key(jwt秘钥)两信息使用随机字符串替换。
|
||||
|
||||
前端:
|
||||
static/config.js: 若前后端分开部署则将该文件中的api地址配成后端服务的真实地址即可,否则无需修改。
|
||||
|
||||
服务启动:./startup.sh
|
||||
服务启动&重启:./startup.sh
|
||||
服务关闭:./shutdown.sh
|
||||
|
||||
直接通过 host:ip即可访问项目
|
||||
初始账号 admin/123456
|
||||
初始账号 admin/admin123.
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
execfile=./mayfly-go
|
||||
|
||||
pid=`ps ax | grep -i 'mayfly-go' | grep -v grep | awk '{print $1}'`
|
||||
if [ ! -z "${pid}" ] ; then
|
||||
echo "The mayfly-go already running, shutdown and restart..."
|
||||
kill ${pid}
|
||||
fi
|
||||
|
||||
if [ ! -x "${execfile}" ]; then
|
||||
sudo chmod +x "${execfile}"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user