mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 08:20:25 +08:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					cf2bc6785c | ||
| 
						 | 
					98a4c92576 | ||
| 
						 | 
					b1ee9b65ff | ||
| 
						 | 
					99cc4c5e5e | ||
| 
						 | 
					226bb8f089 | ||
| 
						 | 
					37ed5134e8 | ||
| 
						 | 
					0f54d4a472 | ||
| 
						 | 
					64805360d6 | ||
| 
						 | 
					7f69fe2ad9 | ||
| 
						 | 
					f913510d3c | ||
| 
						 | 
					f2d9e7786d | ||
| 
						 | 
					e1afb1ed54 | ||
| 
						 | 
					12f8cf0111 | ||
| 
						 | 
					daa2ef5203 | ||
| 
						 | 
					1e3e183930 | ||
| 
						 | 
					366563a0fe | ||
| 
						 | 
					577802e5ad | ||
| 
						 | 
					76d6fc3ba5 | 
							
								
								
									
										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>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### 系统相关资料
 | 
			
		||||
 
 | 
			
		||||
@@ -30,9 +30,9 @@ function buildWeb() {
 | 
			
		||||
 | 
			
		||||
    echo_yellow "-------------------打包前端开始-------------------"
 | 
			
		||||
    yarn run build
 | 
			
		||||
    if [ "${copy2Server}" == "1" ] ; then
 | 
			
		||||
        echo_green '将打包后的静态文件拷贝至server/static'
 | 
			
		||||
        rm -rf ${server_folder}/static && mkdir -p ${server_folder}/static && cp -r ${web_folder}/dist/* ${server_folder}/static
 | 
			
		||||
    if [ "${copy2Server}" == "2" ] ; then
 | 
			
		||||
        echo_green '将打包后的静态文件拷贝至server/static/static'
 | 
			
		||||
        rm -rf ${server_folder}/static/static && mkdir -p ${server_folder}/static/static && cp -r ${web_folder}/dist/* ${server_folder}/static/static
 | 
			
		||||
    fi
 | 
			
		||||
    echo_yellow ">>>>>>>>>>>>>>>>>>>打包前端结束<<<<<<<<<<<<<<<<<<<<\n"
 | 
			
		||||
}
 | 
			
		||||
@@ -44,6 +44,7 @@ function build() {
 | 
			
		||||
    toFolder=$1
 | 
			
		||||
    os=$2
 | 
			
		||||
    arch=$3
 | 
			
		||||
    copyStatic=$4
 | 
			
		||||
 | 
			
		||||
    echo_yellow "-------------------${os}-${arch}打包构建开始-------------------"
 | 
			
		||||
 | 
			
		||||
@@ -67,8 +68,10 @@ function build() {
 | 
			
		||||
    echo_green "移动二进制文件至'${toFolder}'"
 | 
			
		||||
    mv ${server_folder}/${execFileName} ${toFolder}
 | 
			
		||||
 | 
			
		||||
    echo_green "拷贝前端静态页面至'${toFolder}/static'"
 | 
			
		||||
    mkdir -p ${toFolder}/static && cp -r ${web_folder}/dist/* ${toFolder}/static
 | 
			
		||||
    if [ "${copy2Server}" == "1" ] ; then
 | 
			
		||||
        echo_green "拷贝前端静态页面至'${toFolder}/static'"
 | 
			
		||||
        mkdir -p ${toFolder}/static && cp -r ${web_folder}/dist/* ${toFolder}/static
 | 
			
		||||
    fi
 | 
			
		||||
 | 
			
		||||
    echo_green "拷贝脚本等资源文件[config.yml、mayfly-go.sql、readme.txt、startup.sh、shutdown.sh]"
 | 
			
		||||
    cp ${server_folder}/config.yml ${toFolder}
 | 
			
		||||
@@ -81,15 +84,15 @@ function build() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildLinuxAmd64() {
 | 
			
		||||
    build "$1/mayfly-go-linux-amd64" "linux" "amd64"
 | 
			
		||||
    build "$1/mayfly-go-linux-amd64" "linux" "amd64" $2
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildLinuxArm64() {
 | 
			
		||||
    build "$1/mayfly-go-linux-arm64" "linux" "arm64"
 | 
			
		||||
    build "$1/mayfly-go-linux-arm64" "linux" "arm64" $2
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildWindows() {
 | 
			
		||||
    build "$1/mayfly-go-windows" "windows" "amd64"
 | 
			
		||||
    build "$1/mayfly-go-windows" "windows" "amd64" $2
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function runBuild() {
 | 
			
		||||
@@ -103,28 +106,23 @@ function runBuild() {
 | 
			
		||||
    cd ${toPath}
 | 
			
		||||
    toPath=`pwd`
 | 
			
		||||
 | 
			
		||||
    read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static]: " runBuildWeb
 | 
			
		||||
    read -p "是否构建前端[0|其他->否 1->是 2->构建并拷贝至server/static/static]: " runBuildWeb
 | 
			
		||||
    read -p "请选择构建版本[0|其他->全部 1->linux-amd64 2->linux-arm64 3->windows]: " buildType
 | 
			
		||||
    
 | 
			
		||||
    if [ "${runBuildWeb}" == "1" ];then
 | 
			
		||||
        buildWeb
 | 
			
		||||
    fi
 | 
			
		||||
    if [ "${runBuildWeb}" == "2" ];then
 | 
			
		||||
        buildWeb 1
 | 
			
		||||
    fi
 | 
			
		||||
    buildWeb ${runBuildWeb}
 | 
			
		||||
 | 
			
		||||
    if [ "${buildType}" == "1" ];then
 | 
			
		||||
        buildLinuxAmd64 ${toPath}
 | 
			
		||||
        buildLinuxAmd64 ${toPath} ${runBuildWeb}
 | 
			
		||||
        exit;
 | 
			
		||||
    fi
 | 
			
		||||
 | 
			
		||||
    if [ "${buildType}" == "2" ];then
 | 
			
		||||
        buildLinuxArm64 ${toPath}
 | 
			
		||||
        buildLinuxArm64 ${toPath} ${runBuildWeb}
 | 
			
		||||
        exit;
 | 
			
		||||
    fi
 | 
			
		||||
 | 
			
		||||
    if [ "${buildType}" == "3" ];then
 | 
			
		||||
        buildWindows ${toPath}
 | 
			
		||||
        buildWindows ${toPath} ${runBuildWeb}
 | 
			
		||||
        exit;
 | 
			
		||||
    fi
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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.9",
 | 
			
		||||
    "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',
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            /* 后端控制路由
 | 
			
		||||
 
 | 
			
		||||
@@ -947,12 +947,6 @@
 | 
			
		||||
.el-select-dropdown .el-scrollbar__wrap {
 | 
			
		||||
	overflow-x: scroll !important;
 | 
			
		||||
}
 | 
			
		||||
.el-select-dropdown__wrap {
 | 
			
		||||
	max-height: 274px !important; /*修复Select 选择器高度问题*/
 | 
			
		||||
}
 | 
			
		||||
.el-cascader-menu__wrap.el-scrollbar__wrap {
 | 
			
		||||
	height: 204px !important; /*修复Cascader 级联选择器高度问题*/
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Drawer 抽屉
 | 
			
		||||
------------------------------- */
 | 
			
		||||
 
 | 
			
		||||
@@ -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,34 +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-tag
 | 
			
		||||
                        v-for="db in databaseList"
 | 
			
		||||
                        :key="db"
 | 
			
		||||
                        class="ml5 mt5"
 | 
			
		||||
                        type="success"
 | 
			
		||||
                        effect="plain"
 | 
			
		||||
                        closable
 | 
			
		||||
                        :disable-transitions="false"
 | 
			
		||||
                        @close="handleClose(db)"
 | 
			
		||||
                    >
 | 
			
		||||
                        {{ db }}
 | 
			
		||||
                    </el-tag>
 | 
			
		||||
                    <el-input
 | 
			
		||||
                        v-if="inputDbVisible"
 | 
			
		||||
                        ref="InputDbRef"
 | 
			
		||||
                        v-model="inputDbValue"
 | 
			
		||||
                        style="width: 120px; margin-left: 5px; margin-top: 5px"
 | 
			
		||||
                        size="small"
 | 
			
		||||
                        @keyup.enter="handleInputDbConfirm"
 | 
			
		||||
                        @blur="handleInputDbConfirm"
 | 
			
		||||
                    />
 | 
			
		||||
                    <el-button v-else class="ml5 mt5" size="small" @click="showInputDb"> + 添加数据库 </el-button>
 | 
			
		||||
                    <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隧道:">
 | 
			
		||||
@@ -101,12 +106,11 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { toRefs, reactive, nextTick, watch, defineComponent, ref } from 'vue';
 | 
			
		||||
import { toRefs, reactive, watch, defineComponent, ref } from 'vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import { projectApi } from '../project/api.ts';
 | 
			
		||||
import { machineApi } from '../machine/api.ts';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import type { ElInput } from 'element-plus';
 | 
			
		||||
import { notBlank } from '@/common/assert';
 | 
			
		||||
import { RsaEncrypt } from '@/common/rsa';
 | 
			
		||||
 | 
			
		||||
@@ -128,16 +132,14 @@ export default defineComponent({
 | 
			
		||||
    },
 | 
			
		||||
    setup(props: any, { emit }) {
 | 
			
		||||
        const dbForm: any = ref(null);
 | 
			
		||||
        const InputDbRef = ref<InstanceType<typeof ElInput>>();
 | 
			
		||||
 | 
			
		||||
        const state = reactive({
 | 
			
		||||
            dialogVisible: false,
 | 
			
		||||
            projects: [],
 | 
			
		||||
            envs: [],
 | 
			
		||||
            allDatabases: [] as any,
 | 
			
		||||
            databaseList: [] as any,
 | 
			
		||||
            sshTunnelMachineList: [],
 | 
			
		||||
            inputDbVisible: false,
 | 
			
		||||
            inputDbValue: '',
 | 
			
		||||
            form: {
 | 
			
		||||
                id: null,
 | 
			
		||||
                name: null,
 | 
			
		||||
@@ -153,6 +155,8 @@ export default defineComponent({
 | 
			
		||||
                enableSshTunnel: null,
 | 
			
		||||
                sshTunnelMachineId: null,
 | 
			
		||||
            },
 | 
			
		||||
            // 原密码
 | 
			
		||||
            pwd: '',
 | 
			
		||||
            btnLoading: false,
 | 
			
		||||
            rules: {
 | 
			
		||||
                projectId: [
 | 
			
		||||
@@ -226,27 +230,6 @@ export default defineComponent({
 | 
			
		||||
            getSshTunnelMachines();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const handleClose = (db: string) => {
 | 
			
		||||
            state.databaseList.splice(state.databaseList.indexOf(db), 1);
 | 
			
		||||
            changeDatabase();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const showInputDb = () => {
 | 
			
		||||
            state.inputDbVisible = true;
 | 
			
		||||
            nextTick(() => {
 | 
			
		||||
                InputDbRef.value!.input!.focus();
 | 
			
		||||
            });
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const handleInputDbConfirm = () => {
 | 
			
		||||
            if (state.inputDbValue) {
 | 
			
		||||
                state.databaseList.push(state.inputDbValue);
 | 
			
		||||
                changeDatabase();
 | 
			
		||||
            }
 | 
			
		||||
            state.inputDbVisible = false;
 | 
			
		||||
            state.inputDbValue = '';
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * 改变表单中的数据库字段,方便表单错误提示。如全部删光,可提示请添加数据库
 | 
			
		||||
         */
 | 
			
		||||
@@ -285,6 +268,17 @@ export default defineComponent({
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const getAllDatabase = async () => {
 | 
			
		||||
            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 () => {
 | 
			
		||||
            if (!state.form.id) {
 | 
			
		||||
                notBlank(state.form.password, '新增操作,密码不可为空');
 | 
			
		||||
@@ -293,7 +287,6 @@ export default defineComponent({
 | 
			
		||||
                if (valid) {
 | 
			
		||||
                    const reqForm = { ...state.form };
 | 
			
		||||
                    reqForm.password = await RsaEncrypt(reqForm.password);
 | 
			
		||||
                    // reqForm.ssh_pass = await RsaEncrypt(reqForm.ssh_pass);
 | 
			
		||||
                    dbApi.saveDb.request(reqForm).then(() => {
 | 
			
		||||
                        ElMessage.success('保存成功');
 | 
			
		||||
                        emit('val-change', state.form);
 | 
			
		||||
@@ -312,9 +305,8 @@ export default defineComponent({
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const resetInputDb = () => {
 | 
			
		||||
            state.inputDbVisible = false;
 | 
			
		||||
            state.databaseList = [];
 | 
			
		||||
            state.inputDbValue = '';
 | 
			
		||||
            state.allDatabases = [];
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const cancel = () => {
 | 
			
		||||
@@ -328,10 +320,9 @@ export default defineComponent({
 | 
			
		||||
        return {
 | 
			
		||||
            ...toRefs(state),
 | 
			
		||||
            dbForm,
 | 
			
		||||
            InputDbRef,
 | 
			
		||||
            handleClose,
 | 
			
		||||
            showInputDb,
 | 
			
		||||
            handleInputDbConfirm,
 | 
			
		||||
            getAllDatabase,
 | 
			
		||||
            getDbPwd,
 | 
			
		||||
            changeDatabase,
 | 
			
		||||
            getSshTunnelMachines,
 | 
			
		||||
            changeProject,
 | 
			
		||||
            changeEnv,
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
@@ -342,7 +352,26 @@ export default defineComponent({
 | 
			
		||||
 | 
			
		||||
        onMounted(async () => {
 | 
			
		||||
            search();
 | 
			
		||||
            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) => {
 | 
			
		||||
@@ -369,7 +398,8 @@ export default defineComponent({
 | 
			
		||||
            search();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const editDb = (isAdd = false) => {
 | 
			
		||||
        const editDb = async (isAdd = false) => {
 | 
			
		||||
            state.projects = await projectApi.accountProjects.request(null);
 | 
			
		||||
            if (isAdd) {
 | 
			
		||||
                state.dbEditDialog.data = null;
 | 
			
		||||
                state.dbEditDialog.title = '新增数据库资源';
 | 
			
		||||
@@ -502,6 +532,8 @@ export default defineComponent({
 | 
			
		||||
                state.tableInfoDialog.infos = await dbApi.tableInfos.request({ id: row.id, db });
 | 
			
		||||
                state.dbId = row.id;
 | 
			
		||||
                state.db = db;
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                state.tableInfoDialog.visible = false;
 | 
			
		||||
            } finally {
 | 
			
		||||
                state.tableInfoDialog.loading = false;
 | 
			
		||||
            }
 | 
			
		||||
@@ -570,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,
 | 
			
		||||
        };
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@ 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"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true" :before-close="cancel" width="35%">
 | 
			
		||||
        <el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true" :before-close="cancel" width="38%">
 | 
			
		||||
            <el-form :model="form" ref="machineForm" :rules="rules" label-width="85px">
 | 
			
		||||
                <el-form-item prop="projectId" label="项目:" required>
 | 
			
		||||
                    <el-select style="width: 100%" v-model="form.projectId" placeholder="请选择项目" @change="changeProject" filterable>
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -43,6 +51,24 @@
 | 
			
		||||
                <el-form-item prop="remark" label="备注:">
 | 
			
		||||
                    <el-input type="textarea" v-model="form.remark"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item prop="enableSshTunnel" label="SSH隧道:">
 | 
			
		||||
                    <el-col :span="3">
 | 
			
		||||
                        <el-checkbox @change="getSshTunnelMachines" v-model="form.enableSshTunnel" :true-label="1" :false-label="-1"></el-checkbox>
 | 
			
		||||
                    </el-col>
 | 
			
		||||
                    <el-col :span="2" v-if="form.enableSshTunnel == 1"> 机器: </el-col>
 | 
			
		||||
                    <el-col :span="19" v-if="form.enableSshTunnel == 1">
 | 
			
		||||
                        <el-select style="width: 100%" v-model="form.sshTunnelMachineId" placeholder="请选择SSH隧道机器">
 | 
			
		||||
                            <el-option
 | 
			
		||||
                                v-for="item in sshTunnelMachineList"
 | 
			
		||||
                                :key="item.id"
 | 
			
		||||
                                :label="`${item.ip}:${item.port} [${item.name}]`"
 | 
			
		||||
                                :value="item.id"
 | 
			
		||||
                            >
 | 
			
		||||
                            </el-option>
 | 
			
		||||
                        </el-select>
 | 
			
		||||
                    </el-col>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
            </el-form>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
@@ -83,6 +109,7 @@ export default defineComponent({
 | 
			
		||||
        const state = reactive({
 | 
			
		||||
            dialogVisible: false,
 | 
			
		||||
            projects: [],
 | 
			
		||||
            sshTunnelMachineList: [],
 | 
			
		||||
            form: {
 | 
			
		||||
                id: null,
 | 
			
		||||
                projectId: null,
 | 
			
		||||
@@ -93,7 +120,10 @@ export default defineComponent({
 | 
			
		||||
                username: '',
 | 
			
		||||
                password: '',
 | 
			
		||||
                remark: '',
 | 
			
		||||
                enableSshTunnel: null,
 | 
			
		||||
                sshTunnelMachineId: null,
 | 
			
		||||
            },
 | 
			
		||||
            pwd: '',
 | 
			
		||||
            btnLoading: false,
 | 
			
		||||
            rules: {
 | 
			
		||||
                projectId: [
 | 
			
		||||
@@ -152,8 +182,24 @@ export default defineComponent({
 | 
			
		||||
            } else {
 | 
			
		||||
                state.form = { port: 22, authMethod: 1 } as any;
 | 
			
		||||
            }
 | 
			
		||||
            getSshTunnelMachines();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const getSshTunnelMachines = async () => {
 | 
			
		||||
            if (state.form.enableSshTunnel == 1 && state.sshTunnelMachineList.length == 0) {
 | 
			
		||||
                const res = await machineApi.list.request({ pageNum: 1, pageSize: 100 });
 | 
			
		||||
                state.sshTunnelMachineList = res.list;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const getSshTunnelMachine = (machineId: any) => {
 | 
			
		||||
            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) {
 | 
			
		||||
@@ -168,7 +214,15 @@ export default defineComponent({
 | 
			
		||||
            }
 | 
			
		||||
            machineForm.value.validate(async (valid: boolean) => {
 | 
			
		||||
                if (valid) {
 | 
			
		||||
                    const reqForm = { ...state.form };
 | 
			
		||||
                    const form: any = state.form;
 | 
			
		||||
                    if (form.enableSshTunnel == 1) {
 | 
			
		||||
                        const tunnelMachine: any = getSshTunnelMachine(form.sshTunnelMachineId);
 | 
			
		||||
                        if (tunnelMachine.ip == form.ip && tunnelMachine.port == form.port) {
 | 
			
		||||
                            ElMessage.error('隧道机器不能与本机器一致');
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    const reqForm: any = { ...form };
 | 
			
		||||
                    if (reqForm.authMethod == 1) {
 | 
			
		||||
                        reqForm.password = await RsaEncrypt(state.form.password);
 | 
			
		||||
                    }
 | 
			
		||||
@@ -196,6 +250,8 @@ export default defineComponent({
 | 
			
		||||
        return {
 | 
			
		||||
            ...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) }}
 | 
			
		||||
@@ -232,7 +227,6 @@ export default defineComponent({
 | 
			
		||||
 | 
			
		||||
        onMounted(async () => {
 | 
			
		||||
            search();
 | 
			
		||||
            state.projects = await projectApi.accountProjects.request(null);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const choose = (item: any) => {
 | 
			
		||||
@@ -255,12 +249,18 @@ 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();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const openFormDialog = (machine: any) => {
 | 
			
		||||
        const openFormDialog = async (machine: any) => {
 | 
			
		||||
            state.projects = await projectApi.accountProjects.request(null);
 | 
			
		||||
            let dialogTitle;
 | 
			
		||||
            if (machine) {
 | 
			
		||||
                state.machineEditDialog.data = state.currentData as any;
 | 
			
		||||
 
 | 
			
		||||
@@ -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'),
 | 
			
		||||
 
 | 
			
		||||
@@ -120,7 +120,7 @@
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <el-dialog width="800px" title="json编辑器" v-model="jsoneditorDialog.visible" @close="onCloseJsonEditDialog" :close-on-click-modal="false">
 | 
			
		||||
        <el-dialog width="70%" title="json编辑器" v-model="jsoneditorDialog.visible" @close="onCloseJsonEditDialog" :close-on-click-modal="false">
 | 
			
		||||
            <json-edit v-model="jsoneditorDialog.doc" />
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -250,7 +250,6 @@ export default defineComponent({
 | 
			
		||||
 | 
			
		||||
        onMounted(async () => {
 | 
			
		||||
            search();
 | 
			
		||||
            state.projects = await projectApi.accountProjects.request(null);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const handlePageChange = (curPage: number) => {
 | 
			
		||||
@@ -371,7 +370,8 @@ export default defineComponent({
 | 
			
		||||
            state.total = res.total;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const editMongo = (isAdd = false) => {
 | 
			
		||||
        const editMongo = async (isAdd = false) => {
 | 
			
		||||
            state.projects = await projectApi.accountProjects.request(null);
 | 
			
		||||
            if (isAdd) {
 | 
			
		||||
                state.mongoEditDialog.data = null;
 | 
			
		||||
                state.mongoEditDialog.title = '新增mongo';
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -152,7 +152,7 @@
 | 
			
		||||
                            :remote-method="getAccount"
 | 
			
		||||
                            v-model="showMemDialog.memForm.accountId"
 | 
			
		||||
                            filterable
 | 
			
		||||
                            placeholder="请选择"
 | 
			
		||||
                            placeholder="请输入账号模糊搜索并选择"
 | 
			
		||||
                        >
 | 
			
		||||
                            <el-option v-for="item in showMemDialog.accounts" :key="item.id" :label="item.username" :value="item.id"> </el-option>
 | 
			
		||||
                        </el-select>
 | 
			
		||||
 
 | 
			
		||||
@@ -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,13 @@
 | 
			
		||||
                <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
 | 
			
		||||
                        >
 | 
			
		||||
@@ -202,7 +208,6 @@ export default defineComponent({
 | 
			
		||||
 | 
			
		||||
        onMounted(async () => {
 | 
			
		||||
            search();
 | 
			
		||||
            state.projects = await projectApi.accountProjects.request(null);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const handlePageChange = (curPage: number) => {
 | 
			
		||||
@@ -258,7 +263,8 @@ export default defineComponent({
 | 
			
		||||
            state.total = res.total;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const editRedis = (isAdd = false) => {
 | 
			
		||||
        const editRedis = async (isAdd = false) => {
 | 
			
		||||
            state.projects = await projectApi.accountProjects.request(null);
 | 
			
		||||
            if (isAdd) {
 | 
			
		||||
                state.redisEditDialog.data = null;
 | 
			
		||||
                state.redisEditDialog.title = '新增redis';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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'),
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,7 +1,3 @@
 | 
			
		||||
app:
 | 
			
		||||
  name: mayfly-go
 | 
			
		||||
  version: 1.2.3
 | 
			
		||||
  
 | 
			
		||||
server:
 | 
			
		||||
  # debug release test
 | 
			
		||||
  model: release
 | 
			
		||||
@@ -11,24 +7,14 @@ server:
 | 
			
		||||
    enable: false
 | 
			
		||||
    key-file: ./default.key
 | 
			
		||||
    cert-file: ./default.pem
 | 
			
		||||
  # 静态资源
 | 
			
		||||
  static:
 | 
			
		||||
    - relative-path: /assets
 | 
			
		||||
      root: ./static/assets
 | 
			
		||||
  # 静态文件
 | 
			
		||||
  static-file:
 | 
			
		||||
    - relative-path: /
 | 
			
		||||
      filepath: ./static/index.html
 | 
			
		||||
    - relative-path: /favicon.ico
 | 
			
		||||
      filepath: ./static/favicon.ico
 | 
			
		||||
    - relative-path: /config.js
 | 
			
		||||
      filepath: ./static/config.js
 | 
			
		||||
 | 
			
		||||
jwt:
 | 
			
		||||
  key: mykey
 | 
			
		||||
  # jwt key,不设置默认使用随机字符串
 | 
			
		||||
  key: 
 | 
			
		||||
  # 过期时间单位分钟
 | 
			
		||||
  expire-time: 1440
 | 
			
		||||
 | 
			
		||||
# 资源密码aes加密key
 | 
			
		||||
aes:
 | 
			
		||||
  key: 1111111111111111
 | 
			
		||||
mysql:
 | 
			
		||||
  host: localhost:3306
 | 
			
		||||
  username: root
 | 
			
		||||
@@ -36,7 +22,6 @@ mysql:
 | 
			
		||||
  db-name: mayfly-go
 | 
			
		||||
  config: charset=utf8&loc=Local&parseTime=true
 | 
			
		||||
  max-idle-conns: 5
 | 
			
		||||
 | 
			
		||||
log:
 | 
			
		||||
   # 日志等级, trace, debug, info, warn, error, fatal
 | 
			
		||||
  level: info
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -2,16 +2,25 @@ package initialize
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	common_router "mayfly-go/internal/common/router"
 | 
			
		||||
	devops_router "mayfly-go/internal/devops/router"
 | 
			
		||||
	sys_router "mayfly-go/internal/sys/router"
 | 
			
		||||
	"mayfly-go/pkg/config"
 | 
			
		||||
	"mayfly-go/pkg/middleware"
 | 
			
		||||
	"mayfly-go/static"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func WrapStaticHandler(h http.Handler) gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		c.Writer.Header().Set("Cache-Control", `public, max-age=31536000`)
 | 
			
		||||
		h.ServeHTTP(c.Writer, c.Request)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func InitRouter() *gin.Engine {
 | 
			
		||||
	// server配置
 | 
			
		||||
	serverConfig := config.Conf.Server
 | 
			
		||||
@@ -25,12 +34,21 @@ func InitRouter() *gin.Engine {
 | 
			
		||||
		g.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": fmt.Sprintf("not found '%s:%s'", g.Request.Method, g.Request.URL.Path)})
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// 使用embed打包静态资源至二进制文件中
 | 
			
		||||
	fsys, _ := fs.Sub(static.Static, "static")
 | 
			
		||||
	fileServer := http.FileServer(http.FS(fsys))
 | 
			
		||||
	handler := WrapStaticHandler(fileServer)
 | 
			
		||||
	router.GET("/", handler)
 | 
			
		||||
	router.GET("/favicon.ico", handler)
 | 
			
		||||
	router.GET("/config.js", handler)
 | 
			
		||||
	// 所有/assets/**开头的都是静态资源文件
 | 
			
		||||
	router.GET("/assets/*file", handler)
 | 
			
		||||
 | 
			
		||||
	// 设置静态资源
 | 
			
		||||
	if staticConfs := serverConfig.Static; staticConfs != nil {
 | 
			
		||||
		for _, scs := range *staticConfs {
 | 
			
		||||
			router.Static(scs.RelativePath, scs.Root)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	// 设置静态文件
 | 
			
		||||
	if staticFileConfs := serverConfig.StaticFile; staticFileConfs != nil {
 | 
			
		||||
@@ -38,6 +56,7 @@ func InitRouter() *gin.Engine {
 | 
			
		||||
			router.StaticFile(sfs.RelativePath, sfs.Filepath)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 是否允许跨域
 | 
			
		||||
	if serverConfig.Cors {
 | 
			
		||||
		router.Use(middleware.Cors())
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								server/internal/constant/constant.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								server/internal/constant/constant.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
package constant
 | 
			
		||||
 | 
			
		||||
import "time"
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	MachineConnExpireTime = 60 * time.Minute
 | 
			
		||||
	DbConnExpireTime      = 45 * time.Minute
 | 
			
		||||
	RedisConnExpireTime   = 30 * time.Minute
 | 
			
		||||
	MongoConnExpireTime   = 30 * time.Minute
 | 
			
		||||
 | 
			
		||||
/****  开发测试使用   ****/
 | 
			
		||||
// MachineConnExpireTime = 4 * time.Minute
 | 
			
		||||
// DbConnExpireTime      = 2 * time.Minute
 | 
			
		||||
// RedisConnExpireTime   = 2 * time.Minute
 | 
			
		||||
// MongoConnExpireTime   = 2 * time.Minute
 | 
			
		||||
)
 | 
			
		||||
@@ -55,21 +55,40 @@ func (d *Db) Save(rc *ctx.ReqCtx) {
 | 
			
		||||
 | 
			
		||||
	// 密码脱敏记录日志
 | 
			
		||||
	form.Password = "****"
 | 
			
		||||
 | 
			
		||||
	// if form.Type == "mysql" && form.EnableSSH == 1 {
 | 
			
		||||
	// 	// originSSHPwd, err := utils.DefaultRsaDecrypt(form.SSHPass, true)
 | 
			
		||||
	// 	biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
 | 
			
		||||
	// 	// db.SSHPass = originSSHPwd
 | 
			
		||||
	// 	// 密码脱敏记录日志
 | 
			
		||||
	// 	form.SSHPass = "****"
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	rc.ReqParam = form
 | 
			
		||||
 | 
			
		||||
	db.SetBaseInfo(rc.LoginAccount)
 | 
			
		||||
	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{}
 | 
			
		||||
	ginx.BindJsonAndValid(rc.GinCtx, form)
 | 
			
		||||
 | 
			
		||||
	db := new(entity.Db)
 | 
			
		||||
	utils.Copy(db, form)
 | 
			
		||||
 | 
			
		||||
	// 密码解密,并使用解密后的赋值
 | 
			
		||||
	originPwd, err := utils.DefaultRsaDecrypt(form.Password, true)
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "解密密码错误: %s")
 | 
			
		||||
	db.Password = originPwd
 | 
			
		||||
 | 
			
		||||
	// 如果id不为空,并且密码为空则从数据库查询
 | 
			
		||||
	if form.Id != 0 && db.Password == "" {
 | 
			
		||||
		db = d.DbApp.GetById(form.Id)
 | 
			
		||||
	}
 | 
			
		||||
	rc.ResData = d.DbApp.GetDatabases(db)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Db) DeleteDb(rc *ctx.ReqCtx) {
 | 
			
		||||
	dbId := GetDbId(rc.GinCtx)
 | 
			
		||||
	d.DbApp.Delete(dbId)
 | 
			
		||||
@@ -78,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) {
 | 
			
		||||
@@ -218,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 {
 | 
			
		||||
@@ -284,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]
 | 
			
		||||
@@ -295,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))
 | 
			
		||||
@@ -319,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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ type DbForm struct {
 | 
			
		||||
	Username  string `binding:"required" json:"username"`
 | 
			
		||||
	Password  string `json:"password"`
 | 
			
		||||
	Params    string `json:"params"`
 | 
			
		||||
	Database  string `binding:"required" json:"database"`
 | 
			
		||||
	Database  string `json:"database"`
 | 
			
		||||
	ProjectId uint64 `binding:"required" json:"projectId"`
 | 
			
		||||
	Project   string `json:"project"`
 | 
			
		||||
	Env       string `json:"env"`
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,18 @@
 | 
			
		||||
package form
 | 
			
		||||
 | 
			
		||||
type MachineForm struct {
 | 
			
		||||
	Id          uint64 `json:"id"`
 | 
			
		||||
	ProjectId   uint64 `json:"projectId"`
 | 
			
		||||
	ProjectName string `json:"projectName"`
 | 
			
		||||
	Name        string `json:"name" binding:"required"`
 | 
			
		||||
	Ip          string `json:"ip" binding:"required"`       // IP地址
 | 
			
		||||
	Username    string `json:"username" binding:"required"` // 用户名
 | 
			
		||||
	AuthMethod  int8   `json:"authMethod" binding:"required"`
 | 
			
		||||
	Password    string `json:"password"`
 | 
			
		||||
	Port        int    `json:"port" binding:"required"` // 端口号
 | 
			
		||||
	Remark      string `json:"remark"`
 | 
			
		||||
	Id                 uint64 `json:"id"`
 | 
			
		||||
	ProjectId          uint64 `json:"projectId"`
 | 
			
		||||
	ProjectName        string `json:"projectName"`
 | 
			
		||||
	Name               string `json:"name" binding:"required"`
 | 
			
		||||
	Ip                 string `json:"ip" binding:"required"`       // IP地址
 | 
			
		||||
	Username           string `json:"username" binding:"required"` // 用户名
 | 
			
		||||
	AuthMethod         int8   `json:"authMethod" binding:"required"`
 | 
			
		||||
	Password           string `json:"password"`
 | 
			
		||||
	Port               int    `json:"port" binding:"required"` // 端口号
 | 
			
		||||
	Remark             string `json:"remark"`
 | 
			
		||||
	EnableSshTunnel    int8   `json:"enableSshTunnel"`    // 是否启用ssh隧道
 | 
			
		||||
	SshTunnelMachineId uint64 `json:"sshTunnelMachineId"` // ssh隧道机器id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MachineRunForm struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,8 @@ type Redis struct {
 | 
			
		||||
	ProjectId          *int64     `json:"projectId"`
 | 
			
		||||
	Project            *string    `json:"project"`
 | 
			
		||||
	Mode               *string    `json:"mode"`
 | 
			
		||||
	EnableSshTunnel    *int8      `orm:"column(enable_ssh_tunnel)" json:"enableSshTunnel"`        // 是否启用ssh隧道
 | 
			
		||||
	SshTunnelMachineId *uint64    `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
 | 
			
		||||
	EnableSshTunnel    *int8      `json:"enableSshTunnel"`    // 是否启用ssh隧道
 | 
			
		||||
	SshTunnelMachineId *uint64    `json:"sshTunnelMachineId"` // ssh隧道机器id
 | 
			
		||||
	Remark             *string    `json:"remark"`
 | 
			
		||||
	Env                *string    `json:"env"`
 | 
			
		||||
	EnvId              *int64     `json:"envId"`
 | 
			
		||||
 
 | 
			
		||||
@@ -15,23 +15,25 @@ type AccountVO struct {
 | 
			
		||||
 | 
			
		||||
type MachineVO struct {
 | 
			
		||||
	//models.BaseModel
 | 
			
		||||
	Id          *uint64    `json:"id"`
 | 
			
		||||
	ProjectId   uint64     `json:"projectId"`
 | 
			
		||||
	ProjectName string     `json:"projectName"`
 | 
			
		||||
	Name        *string    `json:"name"`
 | 
			
		||||
	Username    *string    `json:"username"`
 | 
			
		||||
	Ip          *string    `json:"ip"`
 | 
			
		||||
	Port        *int       `json:"port"`
 | 
			
		||||
	AuthMethod  *int8      `json:"authMethod"`
 | 
			
		||||
	Status      *int8      `json:"status"`
 | 
			
		||||
	CreateTime  *time.Time `json:"createTime"`
 | 
			
		||||
	Creator     *string    `json:"creator"`
 | 
			
		||||
	CreatorId   *int64     `json:"creatorId"`
 | 
			
		||||
	UpdateTime  *time.Time `json:"updateTime"`
 | 
			
		||||
	Modifier    *string    `json:"modifier"`
 | 
			
		||||
	ModifierId  *int64     `json:"modifierId"`
 | 
			
		||||
	HasCli      bool       `json:"hasCli" gorm:"-"`
 | 
			
		||||
	Remark      *string    `json:"remark"`
 | 
			
		||||
	Id                 *uint64    `json:"id"`
 | 
			
		||||
	ProjectId          uint64     `json:"projectId"`
 | 
			
		||||
	ProjectName        string     `json:"projectName"`
 | 
			
		||||
	Name               *string    `json:"name"`
 | 
			
		||||
	Username           *string    `json:"username"`
 | 
			
		||||
	Ip                 *string    `json:"ip"`
 | 
			
		||||
	Port               *int       `json:"port"`
 | 
			
		||||
	AuthMethod         *int8      `json:"authMethod"`
 | 
			
		||||
	Status             *int8      `json:"status"`
 | 
			
		||||
	EnableSshTunnel    *int8      `json:"enableSshTunnel"`    // 是否启用ssh隧道
 | 
			
		||||
	SshTunnelMachineId *uint64    `json:"sshTunnelMachineId"` // ssh隧道机器id
 | 
			
		||||
	CreateTime         *time.Time `json:"createTime"`
 | 
			
		||||
	Creator            *string    `json:"creator"`
 | 
			
		||||
	CreatorId          *int64     `json:"creatorId"`
 | 
			
		||||
	UpdateTime         *time.Time `json:"updateTime"`
 | 
			
		||||
	Modifier           *string    `json:"modifier"`
 | 
			
		||||
	ModifierId         *int64     `json:"modifierId"`
 | 
			
		||||
	HasCli             bool       `json:"hasCli" gorm:"-"`
 | 
			
		||||
	Remark             *string    `json:"remark"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MachineScriptVO struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import (
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mayfly-go/internal/constant"
 | 
			
		||||
	"mayfly-go/internal/devops/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/devops/domain/repository"
 | 
			
		||||
	"mayfly-go/internal/devops/infrastructure/machine"
 | 
			
		||||
@@ -23,7 +24,6 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/go-sql-driver/mysql"
 | 
			
		||||
	"github.com/lib/pq"
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Db interface {
 | 
			
		||||
@@ -47,6 +47,9 @@ type Db interface {
 | 
			
		||||
	// @param id 数据库实例id
 | 
			
		||||
	// @param db 数据库
 | 
			
		||||
	GetDbInstance(id uint64, db string) *DbInstance
 | 
			
		||||
 | 
			
		||||
	// 获取数据库实例的所有数据库列表
 | 
			
		||||
	GetDatabases(entity *entity.Db) []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type dbAppImpl struct {
 | 
			
		||||
@@ -94,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
 | 
			
		||||
	}
 | 
			
		||||
@@ -126,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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -141,11 +146,35 @@ func (d *dbAppImpl) Delete(id uint64) {
 | 
			
		||||
	d.dbSqlRepo.DeleteBy(&entity.DbSql{DbId: id})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbAppImpl) GetDatabases(ed *entity.Db) []string {
 | 
			
		||||
	ed.Network = ed.GetNetwork()
 | 
			
		||||
	databases := make([]string, 0)
 | 
			
		||||
	var dbConn *sql.DB
 | 
			
		||||
	var metaDb string
 | 
			
		||||
	var getDatabasesSql string
 | 
			
		||||
	if ed.Type == entity.DbTypeMysql {
 | 
			
		||||
		metaDb = "information_schema"
 | 
			
		||||
		getDatabasesSql = "SELECT SCHEMA_NAME AS dbname FROM SCHEMATA"
 | 
			
		||||
	} else {
 | 
			
		||||
		metaDb = "postgres"
 | 
			
		||||
		getDatabasesSql = "SELECT datname AS dbname FROM pg_database"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbConn, err := GetDbConn(ed, metaDb)
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "数据库连接失败: %s")
 | 
			
		||||
	defer dbConn.Close()
 | 
			
		||||
 | 
			
		||||
	_, res, err := SelectDataByDb(dbConn, getDatabasesSql)
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "获取数据库列表失败")
 | 
			
		||||
	for _, re := range res {
 | 
			
		||||
		databases = append(databases, re["dbname"].(string))
 | 
			
		||||
	}
 | 
			
		||||
	return databases
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var mutex sync.Mutex
 | 
			
		||||
 | 
			
		||||
func (da *dbAppImpl) GetDbInstance(id uint64, db string) *DbInstance {
 | 
			
		||||
	mutex.Lock()
 | 
			
		||||
	defer mutex.Unlock()
 | 
			
		||||
	// Id不为0,则为需要缓存
 | 
			
		||||
	needCache := id != 0
 | 
			
		||||
	if needCache {
 | 
			
		||||
@@ -154,43 +183,19 @@ func (da *dbAppImpl) GetDbInstance(id uint64, db string) *DbInstance {
 | 
			
		||||
			return load.(*DbInstance)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	mutex.Lock()
 | 
			
		||||
	defer mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	d := da.GetById(id)
 | 
			
		||||
	// 密码解密
 | 
			
		||||
	d.PwdDecrypt()
 | 
			
		||||
	biz.NotNil(d, "数据库信息不存在")
 | 
			
		||||
	biz.IsTrue(strings.Contains(d.Database, db), "未配置该库的操作权限")
 | 
			
		||||
 | 
			
		||||
	cacheKey := GetDbCacheKey(id, db)
 | 
			
		||||
	dbi := &DbInstance{Id: cacheKey, Type: d.Type, ProjectId: d.ProjectId}
 | 
			
		||||
	dbi := &DbInstance{Id: cacheKey, Type: d.Type, ProjectId: d.ProjectId, sshTunnelMachineId: d.SshTunnelMachineId}
 | 
			
		||||
 | 
			
		||||
	//SSH Conect
 | 
			
		||||
	if d.EnableSshTunnel == 1 && d.SshTunnelMachineId != 0 {
 | 
			
		||||
		me := MachineApp.GetById(d.SshTunnelMachineId)
 | 
			
		||||
		biz.NotNil(me, "隧道机器信息不存在")
 | 
			
		||||
		sshClient, err := machine.GetSshClient(me)
 | 
			
		||||
		biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
 | 
			
		||||
		dbi.sshTunnel = sshClient
 | 
			
		||||
 | 
			
		||||
		if d.Type == entity.DbTypeMysql {
 | 
			
		||||
			mysql.RegisterDialContext(d.Network, func(ctx context.Context, addr string) (net.Conn, error) {
 | 
			
		||||
				return sshClient.Dial("tcp", addr)
 | 
			
		||||
			})
 | 
			
		||||
		} else if d.Type == entity.DbTypePostgres {
 | 
			
		||||
			_, err := pq.DialOpen(&PqSqlDialer{sshTunnel: sshClient}, getDsn(d))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				dbi.Close()
 | 
			
		||||
				panic(biz.NewBizErr(fmt.Sprintf("postgres隧道连接失败: %s", err.Error())))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 将数据库替换为要访问的数据库,原本数据库为空格拼接的所有库
 | 
			
		||||
	d.Database = db
 | 
			
		||||
	DB, err := sql.Open(d.Type, getDsn(d))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		dbi.Close()
 | 
			
		||||
		panic(biz.NewBizErr(fmt.Sprintf("Open %s failed, err:%v\n", d.Type, err)))
 | 
			
		||||
	}
 | 
			
		||||
	err = DB.Ping()
 | 
			
		||||
	DB, err := GetDbConn(d, db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		dbi.Close()
 | 
			
		||||
		global.Log.Errorf("连接db失败: %s:%d/%s", d.Host, d.Port, db)
 | 
			
		||||
@@ -212,32 +217,32 @@ func (da *dbAppImpl) GetDbInstance(id uint64, db string) *DbInstance {
 | 
			
		||||
	return dbi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PqSqlDialer struct {
 | 
			
		||||
	sshTunnel *ssh.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pd *PqSqlDialer) Dial(network, address string) (net.Conn, error) {
 | 
			
		||||
	if sshConn, err := pd.sshTunnel.Dial(network, address); err == nil {
 | 
			
		||||
		// 将ssh conn包装,否则redis内部设置超时会报错,ssh conn不支持设置超时会返回错误: ssh: tcpChan: deadline not supported
 | 
			
		||||
		return &utils.WrapSshConn{Conn: sshConn}, nil
 | 
			
		||||
	} else {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
func (pd *PqSqlDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
 | 
			
		||||
	return pd.Dial(network, address)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
// 客户端连接缓存,30分钟内没有访问则会被关闭, key为数据库实例id:数据库
 | 
			
		||||
var dbCache = cache.NewTimedCache(30*time.Minute, 5*time.Second).
 | 
			
		||||
// 单次最大查询数据集
 | 
			
		||||
const Max_Rows = 2000
 | 
			
		||||
 | 
			
		||||
// 客户端连接缓存,指定时间内没有访问则会被关闭, key为数据库实例id:数据库
 | 
			
		||||
var dbCache = cache.NewTimedCache(constant.DbConnExpireTime, 5*time.Second).
 | 
			
		||||
	WithUpdateAccessTime(true).
 | 
			
		||||
	OnEvicted(func(key interface{}, value interface{}) {
 | 
			
		||||
		global.Log.Info(fmt.Sprintf("删除db连接缓存 id = %s", key))
 | 
			
		||||
		value.(*DbInstance).Close()
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	machine.AddCheckSshTunnelMachineUseFunc(func(machineId uint64) bool {
 | 
			
		||||
		// 遍历所有db连接实例,若存在redis实例使用该ssh隧道机器,则返回true,表示还在使用中...
 | 
			
		||||
		items := dbCache.Items()
 | 
			
		||||
		for _, v := range items {
 | 
			
		||||
			if v.Value.(*DbInstance).sshTunnelMachineId == machineId {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetDbCacheKey(dbId uint64, db string) string {
 | 
			
		||||
	return fmt.Sprintf("%d:%s", dbId, db)
 | 
			
		||||
}
 | 
			
		||||
@@ -250,55 +255,44 @@ func GetDbInstanceByCache(id string) *DbInstance {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestConnection(d *entity.Db) {
 | 
			
		||||
	//SSH Conect
 | 
			
		||||
	// 验证第一个库是否可以连接即可
 | 
			
		||||
	DB, err := GetDbConn(d, strings.Split(d.Database, " ")[0])
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "数据库连接失败: %s")
 | 
			
		||||
	defer DB.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取数据库连接
 | 
			
		||||
func GetDbConn(d *entity.Db, db string) (*sql.DB, error) {
 | 
			
		||||
	// SSH Conect
 | 
			
		||||
	if d.EnableSshTunnel == 1 && d.SshTunnelMachineId != 0 {
 | 
			
		||||
		me := MachineApp.GetById(d.SshTunnelMachineId)
 | 
			
		||||
		sshClient, err := machine.GetSshClient(me)
 | 
			
		||||
		biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
 | 
			
		||||
		defer sshClient.Close()
 | 
			
		||||
		sshTunnelMachine := MachineApp.GetSshTunnelMachine(d.SshTunnelMachineId)
 | 
			
		||||
		if d.Type == entity.DbTypeMysql {
 | 
			
		||||
			mysql.RegisterDialContext(d.Network, func(ctx context.Context, addr string) (net.Conn, error) {
 | 
			
		||||
				return sshClient.Dial("tcp", addr)
 | 
			
		||||
				return sshTunnelMachine.GetDialConn("tcp", addr)
 | 
			
		||||
			})
 | 
			
		||||
		} else if d.Type == entity.DbTypePostgres {
 | 
			
		||||
			_, err := pq.DialOpen(&PqSqlDialer{sshTunnel: sshClient}, getDsn(d))
 | 
			
		||||
			_, err := pq.DialOpen(&PqSqlDialer{sshTunnelMachine: sshTunnelMachine}, getDsn(d, db))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				panic(biz.NewBizErr(fmt.Sprintf("postgres隧道连接失败: %s", err.Error())))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 验证第一个库是否可以连接即可
 | 
			
		||||
	d.Database = strings.Split(d.Database, " ")[0]
 | 
			
		||||
	DB, err := sql.Open(d.Type, getDsn(d))
 | 
			
		||||
	biz.ErrIsNil(err, "Open %s failed, err:%v\n", d.Type, err)
 | 
			
		||||
	defer DB.Close()
 | 
			
		||||
	perr := DB.Ping()
 | 
			
		||||
	biz.ErrIsNilAppendErr(perr, "数据库连接失败: %s")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// db实例
 | 
			
		||||
type DbInstance struct {
 | 
			
		||||
	Id        string
 | 
			
		||||
	Type      string
 | 
			
		||||
	ProjectId uint64
 | 
			
		||||
	db        *sql.DB
 | 
			
		||||
	sshTunnel *ssh.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行查询语句
 | 
			
		||||
// 依次返回 列名数组,结果map,错误
 | 
			
		||||
func (d *DbInstance) SelectData(execSql string) ([]string, []map[string]interface{}, error) {
 | 
			
		||||
	execSql = strings.Trim(execSql, " ")
 | 
			
		||||
	isSelect := strings.HasPrefix(execSql, "SELECT") || strings.HasPrefix(execSql, "select")
 | 
			
		||||
	isShow := strings.HasPrefix(execSql, "show")
 | 
			
		||||
	isExplain := strings.HasPrefix(execSql, "explain")
 | 
			
		||||
 | 
			
		||||
	if !isSelect && !isShow && !isExplain {
 | 
			
		||||
		return nil, nil, errors.New("该sql非查询语句")
 | 
			
		||||
	DB, err := sql.Open(d.Type, getDsn(d, db))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	err = DB.Ping()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		DB.Close()
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rows, err := d.db.Query(execSql)
 | 
			
		||||
	return DB, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SelectDataByDb(db *sql.DB, selectSql string) ([]string, []map[string]interface{}, error) {
 | 
			
		||||
	rows, err := db.Query(selectSql)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -324,7 +318,11 @@ func (d *DbInstance) SelectData(execSql string) ([]string, []map[string]interfac
 | 
			
		||||
	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 {
 | 
			
		||||
@@ -338,6 +336,7 @@ func (d *DbInstance) SelectData(execSql string) ([]string, []map[string]interfac
 | 
			
		||||
			colName := colType.Name()
 | 
			
		||||
			// 字段类型名
 | 
			
		||||
			colScanType := colType.ScanType().Name()
 | 
			
		||||
			// 如果是第一行,则将列名加入到列信息中,由于map是无序的,所有需要返回列名的有序数组
 | 
			
		||||
			if isFirst {
 | 
			
		||||
				colNames = append(colNames, colName)
 | 
			
		||||
			}
 | 
			
		||||
@@ -383,6 +382,45 @@ func (d *DbInstance) SelectData(execSql string) ([]string, []map[string]interfac
 | 
			
		||||
	return colNames, result, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PqSqlDialer struct {
 | 
			
		||||
	sshTunnelMachine *machine.SshTunnelMachine
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pd *PqSqlDialer) Dial(network, address string) (net.Conn, error) {
 | 
			
		||||
	if sshConn, err := pd.sshTunnelMachine.GetDialConn("tcp", address); err == nil {
 | 
			
		||||
		// 将ssh conn包装,否则redis内部设置超时会报错,ssh conn不支持设置超时会返回错误: ssh: tcpChan: deadline not supported
 | 
			
		||||
		return &utils.WrapSshConn{Conn: sshConn}, nil
 | 
			
		||||
	} else {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
func (pd *PqSqlDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
 | 
			
		||||
	return pd.Dial(network, address)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// db实例
 | 
			
		||||
type DbInstance struct {
 | 
			
		||||
	Id                 string
 | 
			
		||||
	Type               string
 | 
			
		||||
	ProjectId          uint64
 | 
			
		||||
	db                 *sql.DB
 | 
			
		||||
	sshTunnelMachineId uint64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行查询语句
 | 
			
		||||
// 依次返回 列名数组,结果map,错误
 | 
			
		||||
func (d *DbInstance) SelectData(execSql string) ([]string, []map[string]interface{}, error) {
 | 
			
		||||
	execSql = strings.Trim(execSql, " ")
 | 
			
		||||
	isSelect := strings.HasPrefix(execSql, "SELECT") || strings.HasPrefix(execSql, "select")
 | 
			
		||||
	isShow := strings.HasPrefix(execSql, "show")
 | 
			
		||||
	isExplain := strings.HasPrefix(execSql, "explain")
 | 
			
		||||
 | 
			
		||||
	if !isSelect && !isShow && !isExplain {
 | 
			
		||||
		return nil, nil, errors.New("该sql非查询语句")
 | 
			
		||||
	}
 | 
			
		||||
	return SelectDataByDb(d.db, execSql)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行 update, insert, delete,建表等sql
 | 
			
		||||
// 返回影响条数和错误
 | 
			
		||||
func (d *DbInstance) Exec(sql string) (int64, error) {
 | 
			
		||||
@@ -393,25 +431,33 @@ 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 {
 | 
			
		||||
		if err := d.db.Close(); err != nil {
 | 
			
		||||
			global.Log.Errorf("关闭数据库实例[%s]连接失败: %s", d.Id, err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if d.sshTunnel != nil {
 | 
			
		||||
		if err := d.sshTunnel.Close(); err != nil {
 | 
			
		||||
			global.Log.Errorf("关闭数据库实例[%s]的ssh隧道失败: %s", d.Id, err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		d.db = nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取dataSourceName
 | 
			
		||||
func getDsn(d *entity.Db) string {
 | 
			
		||||
func getDsn(d *entity.Db, db string) string {
 | 
			
		||||
	var dsn string
 | 
			
		||||
	if d.Type == entity.DbTypeMysql {
 | 
			
		||||
		dsn = fmt.Sprintf("%s:%s@%s(%s:%d)/%s?timeout=8s", d.Username, d.Password, d.Network, d.Host, d.Port, d.Database)
 | 
			
		||||
		dsn = fmt.Sprintf("%s:%s@%s(%s:%d)/%s?timeout=8s", d.Username, d.Password, d.Network, d.Host, d.Port, db)
 | 
			
		||||
		if d.Params != "" {
 | 
			
		||||
			dsn = fmt.Sprintf("%s&%s", dsn, d.Params)
 | 
			
		||||
		}
 | 
			
		||||
@@ -419,7 +465,7 @@ func getDsn(d *entity.Db) string {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if d.Type == entity.DbTypePostgres {
 | 
			
		||||
		dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", d.Host, d.Port, d.Username, d.Password, d.Database)
 | 
			
		||||
		dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", d.Host, d.Port, d.Username, d.Password, db)
 | 
			
		||||
		if d.Params != "" {
 | 
			
		||||
			dsn = fmt.Sprintf("%s %s", dsn, strings.Join(strings.Split(d.Params, "&"), " "))
 | 
			
		||||
		}
 | 
			
		||||
@@ -433,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, 
 | 
			
		||||
@@ -453,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
 | 
			
		||||
@@ -466,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 
 | 
			
		||||
@@ -512,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 {
 | 
			
		||||
@@ -532,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
 | 
			
		||||
	// 查询要更新字段数据的旧值,以及主键值
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,9 @@ type Machine interface {
 | 
			
		||||
 | 
			
		||||
	// 获取机器连接
 | 
			
		||||
	GetCli(id uint64) *machine.Cli
 | 
			
		||||
 | 
			
		||||
	// 获取ssh隧道机器连接
 | 
			
		||||
	GetSshTunnelMachine(id uint64) *machine.SshTunnelMachine
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type machineAppImpl struct {
 | 
			
		||||
@@ -53,7 +56,7 @@ func (m *machineAppImpl) Count(condition *entity.Machine) int64 {
 | 
			
		||||
func (m *machineAppImpl) Save(me *entity.Machine) {
 | 
			
		||||
	// ’修改机器信息且密码不为空‘ or ‘新增’需要测试是否可连接
 | 
			
		||||
	if (me.Id != 0 && me.Password != "") || me.Id == 0 {
 | 
			
		||||
		biz.ErrIsNilAppendErr(machine.TestConn(me), "该机器无法连接: %s")
 | 
			
		||||
		biz.ErrIsNilAppendErr(machine.TestConn(*me, func(u uint64) *entity.Machine { return m.GetById(u) }), "该机器无法连接: %s")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	oldMachine := &entity.Machine{Ip: me.Ip, Port: me.Port, Username: me.Username}
 | 
			
		||||
@@ -66,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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -120,9 +125,21 @@ 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
 | 
			
		||||
	})
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "获取客户端错误: %s")
 | 
			
		||||
	return cli
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *machineAppImpl) GetSshTunnelMachine(id uint64) *machine.SshTunnelMachine {
 | 
			
		||||
	sshTunnel, err := machine.GetSshTunnelMachine(id, func(machineId uint64) *entity.Machine {
 | 
			
		||||
		machine := m.GetById(machineId)
 | 
			
		||||
		machine.PwdDecrypt()
 | 
			
		||||
		biz.IsTrue(machine.Status == entity.MachineStatusEnable, "该机器已被停用")
 | 
			
		||||
		return machine
 | 
			
		||||
	})
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "获取ssh隧道连接失败: %s")
 | 
			
		||||
	return sshTunnel
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package application
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"mayfly-go/internal/constant"
 | 
			
		||||
	"mayfly-go/internal/devops/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/devops/domain/repository"
 | 
			
		||||
	"mayfly-go/internal/devops/infrastructure/machine"
 | 
			
		||||
@@ -16,7 +17,6 @@ import (
 | 
			
		||||
 | 
			
		||||
	"go.mongodb.org/mongo-driver/mongo"
 | 
			
		||||
	"go.mongodb.org/mongo-driver/mongo/options"
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Mongo interface {
 | 
			
		||||
@@ -95,14 +95,27 @@ func (d *mongoAppImpl) GetMongoCli(id uint64) *mongo.Client {
 | 
			
		||||
 | 
			
		||||
// -----------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
//mongo客户端连接缓存,30分钟内没有访问则会被关闭
 | 
			
		||||
var mongoCliCache = cache.NewTimedCache(30*time.Minute, 5*time.Second).
 | 
			
		||||
//mongo客户端连接缓存,指定时间内没有访问则会被关闭
 | 
			
		||||
var mongoCliCache = cache.NewTimedCache(constant.MongoConnExpireTime, 5*time.Second).
 | 
			
		||||
	WithUpdateAccessTime(true).
 | 
			
		||||
	OnEvicted(func(key interface{}, value interface{}) {
 | 
			
		||||
		global.Log.Info("删除mongo连接缓存: id = ", key)
 | 
			
		||||
		value.(*MongoInstance).Close()
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	machine.AddCheckSshTunnelMachineUseFunc(func(machineId uint64) bool {
 | 
			
		||||
		// 遍历所有mongo连接实例,若存在redis实例使用该ssh隧道机器,则返回true,表示还在使用中...
 | 
			
		||||
		items := mongoCliCache.Items()
 | 
			
		||||
		for _, v := range items {
 | 
			
		||||
			if v.Value.(*MongoInstance).sshTunnelMachineId == machineId {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取mongo的连接实例
 | 
			
		||||
func GetMongoInstance(mongoId uint64, getMongoEntity func(uint64) *entity.Mongo) (*MongoInstance, error) {
 | 
			
		||||
	mi, err := mongoCliCache.ComputeIfAbsent(mongoId, func(_ interface{}) (interface{}, error) {
 | 
			
		||||
@@ -124,10 +137,10 @@ func DeleteMongoCache(mongoId uint64) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MongoInstance struct {
 | 
			
		||||
	Id        uint64
 | 
			
		||||
	ProjectId uint64
 | 
			
		||||
	Cli       *mongo.Client
 | 
			
		||||
	sshTunnel *ssh.Client
 | 
			
		||||
	Id                 uint64
 | 
			
		||||
	ProjectId          uint64
 | 
			
		||||
	Cli                *mongo.Client
 | 
			
		||||
	sshTunnelMachineId uint64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mi *MongoInstance) Close() {
 | 
			
		||||
@@ -135,11 +148,7 @@ func (mi *MongoInstance) Close() {
 | 
			
		||||
		if err := mi.Cli.Disconnect(context.Background()); err != nil {
 | 
			
		||||
			global.Log.Errorf("关闭mongo实例[%d]连接失败: %s", mi.Id, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if mi.sshTunnel != nil {
 | 
			
		||||
		if err := mi.sshTunnel.Close(); err != nil {
 | 
			
		||||
			global.Log.Errorf("关闭mongo实例[%d]的ssh隧道失败: %s", mi.Id, err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		mi.Cli = nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -154,19 +163,17 @@ func connect(me *entity.Mongo) (*MongoInstance, error) {
 | 
			
		||||
		SetMaxPoolSize(1)
 | 
			
		||||
	// 启用ssh隧道则连接隧道机器
 | 
			
		||||
	if me.EnableSshTunnel == 1 {
 | 
			
		||||
		machineEntity := MachineApp.GetById(4)
 | 
			
		||||
		sshClient, err := machine.GetSshClient(machineEntity)
 | 
			
		||||
		biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
 | 
			
		||||
		mongoInstance.sshTunnel = sshClient
 | 
			
		||||
 | 
			
		||||
		mongoOptions.SetDialer(&MongoSshDialer{sshTunnel: sshClient})
 | 
			
		||||
		mongoInstance.sshTunnelMachineId = me.SshTunnelMachineId
 | 
			
		||||
		mongoOptions.SetDialer(&MongoSshDialer{machineId: me.SshTunnelMachineId})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client, err := mongo.Connect(ctx, mongoOptions)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		mongoInstance.Close()
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err = client.Ping(context.TODO(), nil); err != nil {
 | 
			
		||||
		mongoInstance.Close()
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -176,11 +183,11 @@ func connect(me *entity.Mongo) (*MongoInstance, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MongoSshDialer struct {
 | 
			
		||||
	sshTunnel *ssh.Client
 | 
			
		||||
	machineId uint64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (sd *MongoSshDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
 | 
			
		||||
	if sshConn, err := sd.sshTunnel.Dial(network, address); err == nil {
 | 
			
		||||
	if sshConn, err := MachineApp.GetSshTunnelMachine(sd.machineId).GetDialConn(network, address); err == nil {
 | 
			
		||||
		// 将ssh conn包装,否则内部部设置超时会报错,ssh conn不支持设置超时会返回错误: ssh: tcpChan: deadline not supported
 | 
			
		||||
		return &utils.WrapSshConn{Conn: sshConn}, nil
 | 
			
		||||
	} else {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ package application
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mayfly-go/internal/constant"
 | 
			
		||||
	"mayfly-go/internal/devops/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/devops/domain/repository"
 | 
			
		||||
	"mayfly-go/internal/devops/infrastructure/machine"
 | 
			
		||||
@@ -17,7 +18,6 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-redis/redis/v8"
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Redis interface {
 | 
			
		||||
@@ -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)
 | 
			
		||||
@@ -151,9 +162,8 @@ func getRedisCient(re *entity.Redis) *RedisInstance {
 | 
			
		||||
		WriteTimeout: -1,
 | 
			
		||||
	}
 | 
			
		||||
	if re.EnableSshTunnel == 1 {
 | 
			
		||||
		sshClient, dialerFunc := getRedisDialer(re.SshTunnelMachineId)
 | 
			
		||||
		ri.sshTunnel = sshClient
 | 
			
		||||
		redisOptions.Dialer = dialerFunc
 | 
			
		||||
		ri.sshTunnelMachineId = re.SshTunnelMachineId
 | 
			
		||||
		redisOptions.Dialer = getRedisDialer(re.SshTunnelMachineId)
 | 
			
		||||
	}
 | 
			
		||||
	ri.Cli = redis.NewClient(redisOptions)
 | 
			
		||||
	return ri
 | 
			
		||||
@@ -168,21 +178,38 @@ func getRedisClusterClient(re *entity.Redis) *RedisInstance {
 | 
			
		||||
		DialTimeout: 8 * time.Second,
 | 
			
		||||
	}
 | 
			
		||||
	if re.EnableSshTunnel == 1 {
 | 
			
		||||
		sshClient, dialerFunc := getRedisDialer(re.SshTunnelMachineId)
 | 
			
		||||
		ri.sshTunnel = sshClient
 | 
			
		||||
		redisClusterOptions.Dialer = dialerFunc
 | 
			
		||||
		ri.sshTunnelMachineId = re.SshTunnelMachineId
 | 
			
		||||
		redisClusterOptions.Dialer = getRedisDialer(re.SshTunnelMachineId)
 | 
			
		||||
	}
 | 
			
		||||
	ri.ClusterCli = redis.NewClusterClient(redisClusterOptions)
 | 
			
		||||
	return ri
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getRedisDialer(machineId uint64) (*ssh.Client, func(ctx context.Context, network, addr string) (net.Conn, error)) {
 | 
			
		||||
	me := MachineApp.GetById(machineId)
 | 
			
		||||
	sshClient, err := machine.GetSshClient(me)
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	return sshClient, func(_ context.Context, network, addr string) (net.Conn, error) {
 | 
			
		||||
		if sshConn, err := sshClient.Dial(network, addr); err == nil {
 | 
			
		||||
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) {
 | 
			
		||||
		if sshConn, err := sshTunnel.GetDialConn(network, addr); err == nil {
 | 
			
		||||
			// 将ssh conn包装,否则redis内部设置超时会报错,ssh conn不支持设置超时会返回错误: ssh: tcpChan: deadline not supported
 | 
			
		||||
			return &utils.WrapSshConn{Conn: sshConn}, nil
 | 
			
		||||
		} else {
 | 
			
		||||
@@ -193,8 +220,8 @@ func getRedisDialer(machineId uint64) (*ssh.Client, func(ctx context.Context, ne
 | 
			
		||||
 | 
			
		||||
//------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
// redis客户端连接缓存,30分钟内没有访问则会被关闭
 | 
			
		||||
var redisCache = cache.NewTimedCache(30*time.Minute, 5*time.Second).
 | 
			
		||||
// redis客户端连接缓存,指定时间内没有访问则会被关闭
 | 
			
		||||
var redisCache = cache.NewTimedCache(constant.RedisConnExpireTime, 5*time.Second).
 | 
			
		||||
	WithUpdateAccessTime(true).
 | 
			
		||||
	OnEvicted(func(key interface{}, value interface{}) {
 | 
			
		||||
		global.Log.Info(fmt.Sprintf("删除redis连接缓存 id = %d", key))
 | 
			
		||||
@@ -206,6 +233,19 @@ func CloseRedis(id uint64) {
 | 
			
		||||
	redisCache.Delete(id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	machine.AddCheckSshTunnelMachineUseFunc(func(machineId uint64) bool {
 | 
			
		||||
		// 遍历所有redis连接实例,若存在redis实例使用该ssh隧道机器,则返回true,表示还在使用中...
 | 
			
		||||
		items := redisCache.Items()
 | 
			
		||||
		for _, v := range items {
 | 
			
		||||
			if v.Value.(*RedisInstance).sshTunnelMachineId == machineId {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRedisConnection(re *entity.Redis) {
 | 
			
		||||
	var cmd redis.Cmdable
 | 
			
		||||
	if re.Mode == "" || re.Mode == entity.RedisModeStandalone {
 | 
			
		||||
@@ -216,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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 测试连接
 | 
			
		||||
@@ -225,21 +269,21 @@ func TestRedisConnection(re *entity.Redis) {
 | 
			
		||||
 | 
			
		||||
// redis实例
 | 
			
		||||
type RedisInstance struct {
 | 
			
		||||
	Id         uint64
 | 
			
		||||
	ProjectId  uint64
 | 
			
		||||
	Mode       string
 | 
			
		||||
	Cli        *redis.Client
 | 
			
		||||
	ClusterCli *redis.ClusterClient
 | 
			
		||||
	sshTunnel  *ssh.Client
 | 
			
		||||
	Id                 uint64
 | 
			
		||||
	ProjectId          uint64
 | 
			
		||||
	Mode               string
 | 
			
		||||
	Cli                *redis.Client
 | 
			
		||||
	ClusterCli         *redis.ClusterClient
 | 
			
		||||
	sshTunnelMachineId uint64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取命令执行接口的具体实现
 | 
			
		||||
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
 | 
			
		||||
@@ -252,19 +296,16 @@ 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())
 | 
			
		||||
		}
 | 
			
		||||
		r.Cli = nil
 | 
			
		||||
	}
 | 
			
		||||
	if r.Mode == entity.RedisModeCluster {
 | 
			
		||||
		if err := r.ClusterCli.Close(); err != nil {
 | 
			
		||||
			global.Log.Errorf("关闭redis集群实例[%d]连接失败: %s", r.Id, err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if r.sshTunnel != nil {
 | 
			
		||||
		if err := r.sshTunnel.Close(); err != nil {
 | 
			
		||||
			global.Log.Errorf("关闭redis实例[%d]的ssh隧道失败: %s", r.Id, err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		r.ClusterCli = nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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,21 +1,24 @@
 | 
			
		||||
package entity
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"mayfly-go/internal/common/utils"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Machine struct {
 | 
			
		||||
	model.Model
 | 
			
		||||
	ProjectId   uint64 `json:"projectId"`
 | 
			
		||||
	ProjectName string `json:"projectName"`
 | 
			
		||||
	Name        string `json:"name"`
 | 
			
		||||
	Ip          string `json:"ip"`         // IP地址
 | 
			
		||||
	Username    string `json:"username"`   // 用户名
 | 
			
		||||
	AuthMethod  int8   `json:"authMethod"` // 授权认证方式
 | 
			
		||||
	Password    string `json:"-"`
 | 
			
		||||
	Port        int    `json:"port"`   // 端口号
 | 
			
		||||
	Status      int8   `json:"status"` // 状态 1:启用;2:停用
 | 
			
		||||
	Remark      string `json:"remark"` // 备注
 | 
			
		||||
	ProjectId          uint64 `json:"projectId"`
 | 
			
		||||
	ProjectName        string `json:"projectName"`
 | 
			
		||||
	Name               string `json:"name"`
 | 
			
		||||
	Ip                 string `json:"ip"`         // IP地址
 | 
			
		||||
	Username           string `json:"username"`   // 用户名
 | 
			
		||||
	AuthMethod         int8   `json:"authMethod"` // 授权认证方式
 | 
			
		||||
	Password           string `json:"-"`
 | 
			
		||||
	Port               int    `json:"port"`               // 端口号
 | 
			
		||||
	Status             int8   `json:"status"`             // 状态 1:启用;2:停用
 | 
			
		||||
	Remark             string `json:"remark"`             // 备注
 | 
			
		||||
	EnableSshTunnel    int8   `json:"enableSshTunnel"`    // 是否启用ssh隧道
 | 
			
		||||
	SshTunnelMachineId uint64 `json:"sshTunnelMachineId"` // ssh隧道机器id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -24,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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ package machine
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mayfly-go/internal/constant"
 | 
			
		||||
	"mayfly-go/internal/devops/domain/entity"
 | 
			
		||||
	"mayfly-go/pkg/biz"
 | 
			
		||||
	"mayfly-go/pkg/cache"
 | 
			
		||||
@@ -18,10 +19,12 @@ import (
 | 
			
		||||
// 客户端信息
 | 
			
		||||
type Cli struct {
 | 
			
		||||
	machine *entity.Machine
 | 
			
		||||
	// ssh客户端
 | 
			
		||||
	client *ssh.Client
 | 
			
		||||
 | 
			
		||||
	sftpClient *sftp.Client
 | 
			
		||||
	client     *ssh.Client  // ssh客户端
 | 
			
		||||
	sftpClient *sftp.Client // sftp客户端
 | 
			
		||||
 | 
			
		||||
	enableSshTunnel    int8
 | 
			
		||||
	sshTunnelMachineId uint64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//连接
 | 
			
		||||
@@ -39,7 +42,7 @@ func (c *Cli) connect() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 关闭client和并从缓存中移除
 | 
			
		||||
// 关闭client并从缓存中移除,如果使用隧道则也关闭
 | 
			
		||||
func (c *Cli) Close() {
 | 
			
		||||
	m := c.machine
 | 
			
		||||
	global.Log.Info(fmt.Sprintf("关闭机器客户端连接-> id: %d, name: %s, ip: %s", m.Id, m.Name, m.Ip))
 | 
			
		||||
@@ -51,6 +54,9 @@ func (c *Cli) Close() {
 | 
			
		||||
		c.sftpClient.Close()
 | 
			
		||||
		c.sftpClient = nil
 | 
			
		||||
	}
 | 
			
		||||
	if c.enableSshTunnel == 1 {
 | 
			
		||||
		CloseSshTunnelMachine(c.sshTunnelMachineId, c.machine.Id)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取sftp client
 | 
			
		||||
@@ -105,13 +111,26 @@ func (c *Cli) GetMachine() *entity.Machine {
 | 
			
		||||
	return c.machine
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 机器客户端连接缓存,45分钟内没有访问则会被关闭
 | 
			
		||||
var cliCache = cache.NewTimedCache(45*time.Minute, 5*time.Second).
 | 
			
		||||
// 机器客户端连接缓存,指定时间内没有访问则会被关闭
 | 
			
		||||
var cliCache = cache.NewTimedCache(constant.MachineConnExpireTime, 5*time.Second).
 | 
			
		||||
	WithUpdateAccessTime(true).
 | 
			
		||||
	OnEvicted(func(_, value interface{}) {
 | 
			
		||||
		value.(*Cli).Close()
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	AddCheckSshTunnelMachineUseFunc(func(machineId uint64) bool {
 | 
			
		||||
		// 遍历所有机器连接实例,若存在机器连接实例使用该ssh隧道机器,则返回true,表示还在使用中...
 | 
			
		||||
		items := cliCache.Items()
 | 
			
		||||
		for _, v := range items {
 | 
			
		||||
			if v.Value.(*Cli).sshTunnelMachineId == machineId {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 是否存在指定id的客户端连接
 | 
			
		||||
func HasCli(machineId uint64) bool {
 | 
			
		||||
	if _, ok := cliCache.Get(machineId); ok {
 | 
			
		||||
@@ -128,10 +147,18 @@ func DeleteCli(id uint64) {
 | 
			
		||||
// 从缓存中获取客户端信息,不存在则回调获取机器信息函数,并新建
 | 
			
		||||
func GetCli(machineId uint64, getMachine func(uint64) *entity.Machine) (*Cli, error) {
 | 
			
		||||
	cli, err := cliCache.ComputeIfAbsent(machineId, func(_ interface{}) (interface{}, error) {
 | 
			
		||||
		c, err := newClient(getMachine(machineId))
 | 
			
		||||
		me := getMachine(machineId)
 | 
			
		||||
		err := IfUseSshTunnelChangeIpPort(me, getMachine)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("ssh隧道连接失败: %s", err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		c, err := newClient(me)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			CloseSshTunnelMachine(me.SshTunnelMachineId, me.Id)
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		c.enableSshTunnel = me.EnableSshTunnel
 | 
			
		||||
		c.sshTunnelMachineId = me.SshTunnelMachineId
 | 
			
		||||
		return c, nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
@@ -141,9 +168,20 @@ func GetCli(machineId uint64, getMachine func(uint64) *entity.Machine) (*Cli, er
 | 
			
		||||
	return nil, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 测试连接
 | 
			
		||||
func TestConn(m *entity.Machine) error {
 | 
			
		||||
	sshClient, err := GetSshClient(m)
 | 
			
		||||
// 测试连接,使用传值的方式,而非引用。因为如果使用了ssh隧道,则ip和端口会变为本地映射地址与端口
 | 
			
		||||
func TestConn(me entity.Machine, getSshTunnelMachine func(uint64) *entity.Machine) error {
 | 
			
		||||
	originId := me.Id
 | 
			
		||||
	if originId == 0 {
 | 
			
		||||
		// 随机设置一个ip,如果使用了隧道则用于临时保存隧道
 | 
			
		||||
		me.Id = uint64(time.Now().Nanosecond())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := IfUseSshTunnelChangeIpPort(&me, getSshTunnelMachine)
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "ssh隧道连接失败: %s")
 | 
			
		||||
	if me.EnableSshTunnel == 1 {
 | 
			
		||||
		defer CloseSshTunnelMachine(me.SshTunnelMachineId, me.Id)
 | 
			
		||||
	}
 | 
			
		||||
	sshClient, err := GetSshClient(&me)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -151,6 +189,27 @@ func TestConn(m *entity.Machine) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 如果使用了ssh隧道,则修改机器ip port为暴露的ip port
 | 
			
		||||
func IfUseSshTunnelChangeIpPort(me *entity.Machine, getMachine func(uint64) *entity.Machine) error {
 | 
			
		||||
	if me.EnableSshTunnel != 1 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	sshTunnelMachine, err := GetSshTunnelMachine(me.SshTunnelMachineId, func(u uint64) *entity.Machine {
 | 
			
		||||
		return getMachine(u)
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	exposeIp, exposePort, err := sshTunnelMachine.OpenSshTunnel(me.Id, me.Ip, me.Port)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	// 修改机器ip地址
 | 
			
		||||
	me.Ip = exposeIp
 | 
			
		||||
	me.Port = exposePort
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetSshClient(m *entity.Machine) (*ssh.Client, error) {
 | 
			
		||||
	config := ssh.ClientConfig{
 | 
			
		||||
		User: m.Username,
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
`
 | 
			
		||||
							
								
								
									
										242
									
								
								server/internal/devops/infrastructure/machine/sshtunnel.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								server/internal/devops/infrastructure/machine/sshtunnel.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,242 @@
 | 
			
		||||
package machine
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"mayfly-go/internal/devops/domain/entity"
 | 
			
		||||
	"mayfly-go/pkg/global"
 | 
			
		||||
	"mayfly-go/pkg/scheduler"
 | 
			
		||||
	"mayfly-go/pkg/utils"
 | 
			
		||||
	"net"
 | 
			
		||||
	"os"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	sshTunnelMachines map[uint64]*SshTunnelMachine = make(map[uint64]*SshTunnelMachine)
 | 
			
		||||
 | 
			
		||||
	mutex sync.Mutex
 | 
			
		||||
 | 
			
		||||
	// 所有检测ssh隧道机器是否被使用的函数
 | 
			
		||||
	checkSshTunnelMachineHasUseFuncs []CheckSshTunnelMachineHasUseFunc
 | 
			
		||||
 | 
			
		||||
	// 是否开启检查ssh隧道机器是否被使用,只有使用到了隧道机器才启用
 | 
			
		||||
	startCheckSshTunnelHasUse bool = false
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 检查ssh隧道机器是否有被使用
 | 
			
		||||
type CheckSshTunnelMachineHasUseFunc func(uint64) bool
 | 
			
		||||
 | 
			
		||||
func startCheckUse() {
 | 
			
		||||
	global.Log.Info("开启定时检测ssh隧道机器是否还有被使用")
 | 
			
		||||
	// 每十分钟检查一次隧道机器是否还有被使用
 | 
			
		||||
	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隧道机器检测是否使用函数
 | 
			
		||||
func AddCheckSshTunnelMachineUseFunc(checkFunc CheckSshTunnelMachineHasUseFunc) {
 | 
			
		||||
	if checkSshTunnelMachineHasUseFuncs == nil {
 | 
			
		||||
		checkSshTunnelMachineHasUseFuncs = make([]CheckSshTunnelMachineHasUseFunc, 0)
 | 
			
		||||
	}
 | 
			
		||||
	checkSshTunnelMachineHasUseFuncs = append(checkSshTunnelMachineHasUseFuncs, checkFunc)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ssh隧道机器
 | 
			
		||||
type SshTunnelMachine struct {
 | 
			
		||||
	machineId uint64 // 隧道机器id
 | 
			
		||||
	SshClient *ssh.Client
 | 
			
		||||
	mutex     sync.Mutex
 | 
			
		||||
	tunnels   map[uint64]*Tunnel // 机器id -> 隧道
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (stm *SshTunnelMachine) OpenSshTunnel(id uint64, ip string, port int) (exposedIp string, exposedPort int, err error) {
 | 
			
		||||
	stm.mutex.Lock()
 | 
			
		||||
	defer stm.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	localPort, err := utils.GetAvailablePort()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hostname, err := os.Hostname()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", 0, err
 | 
			
		||||
	}
 | 
			
		||||
	// debug
 | 
			
		||||
	//hostname = "0.0.0.0"
 | 
			
		||||
 | 
			
		||||
	localAddr := fmt.Sprintf("%s:%d", hostname, localPort)
 | 
			
		||||
	listener, err := net.Listen("tcp", localAddr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tunnel := &Tunnel{
 | 
			
		||||
		id:         id,
 | 
			
		||||
		machineId:  stm.machineId,
 | 
			
		||||
		localHost:  hostname,
 | 
			
		||||
		localPort:  localPort,
 | 
			
		||||
		remoteHost: ip,
 | 
			
		||||
		remotePort: port,
 | 
			
		||||
		listener:   listener,
 | 
			
		||||
	}
 | 
			
		||||
	go tunnel.Open(stm.SshClient)
 | 
			
		||||
	stm.tunnels[tunnel.id] = tunnel
 | 
			
		||||
 | 
			
		||||
	return tunnel.localHost, tunnel.localPort, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (st *SshTunnelMachine) GetDialConn(network string, addr string) (net.Conn, error) {
 | 
			
		||||
	st.mutex.Lock()
 | 
			
		||||
	defer st.mutex.Unlock()
 | 
			
		||||
	return st.SshClient.Dial(network, addr)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (stm *SshTunnelMachine) Close() {
 | 
			
		||||
	stm.mutex.Lock()
 | 
			
		||||
	defer stm.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	for id, tunnel := range stm.tunnels {
 | 
			
		||||
		if tunnel != nil {
 | 
			
		||||
			tunnel.Close()
 | 
			
		||||
			delete(stm.tunnels, id)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if stm.SshClient != nil {
 | 
			
		||||
		global.Log.Infof("ssh隧道机器[%d]未被使用, 关闭隧道...", stm.machineId)
 | 
			
		||||
		err := stm.SshClient.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			global.Log.Errorf("关闭ssh隧道机器[%d]发生错误: %s", stm.machineId, err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	delete(sshTunnelMachines, stm.machineId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取ssh隧道机器,方便统一管理充当ssh隧道的机器,避免创建多个ssh client
 | 
			
		||||
func GetSshTunnelMachine(machineId uint64, getMachine func(uint64) *entity.Machine) (*SshTunnelMachine, error) {
 | 
			
		||||
	sshTunnelMachine := sshTunnelMachines[machineId]
 | 
			
		||||
	if sshTunnelMachine != nil {
 | 
			
		||||
		return sshTunnelMachine, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mutex.Lock()
 | 
			
		||||
	defer mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	me := getMachine(machineId)
 | 
			
		||||
	sshClient, err := GetSshClient(me)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	sshTunnelMachine = &SshTunnelMachine{SshClient: sshClient, machineId: machineId, tunnels: map[uint64]*Tunnel{}}
 | 
			
		||||
 | 
			
		||||
	global.Log.Infof("初次连接ssh隧道机器[%d][%s:%d]", machineId, me.Ip, me.Port)
 | 
			
		||||
	sshTunnelMachines[machineId] = sshTunnelMachine
 | 
			
		||||
 | 
			
		||||
	// 如果实用了隧道机器且还没开始定时检查是否还被实用,则执行定时任务检测隧道是否还被使用
 | 
			
		||||
	if !startCheckSshTunnelHasUse {
 | 
			
		||||
		startCheckUse()
 | 
			
		||||
		startCheckSshTunnelHasUse = true
 | 
			
		||||
	}
 | 
			
		||||
	return sshTunnelMachine, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 关闭ssh隧道机器的指定隧道
 | 
			
		||||
func CloseSshTunnelMachine(machineId uint64, tunnelId uint64) {
 | 
			
		||||
	sshTunnelMachine := sshTunnelMachines[machineId]
 | 
			
		||||
	if sshTunnelMachine == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sshTunnelMachine.mutex.Lock()
 | 
			
		||||
	defer sshTunnelMachine.mutex.Unlock()
 | 
			
		||||
	t := sshTunnelMachine.tunnels[tunnelId]
 | 
			
		||||
	if t != nil {
 | 
			
		||||
		t.Close()
 | 
			
		||||
		delete(sshTunnelMachine.tunnels, tunnelId)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Tunnel struct {
 | 
			
		||||
	id                uint64 // 唯一标识
 | 
			
		||||
	machineId         uint64 // 隧道机器id
 | 
			
		||||
	localHost         string // 本地监听地址
 | 
			
		||||
	localPort         int    // 本地端口
 | 
			
		||||
	remoteHost        string // 远程连接地址
 | 
			
		||||
	remotePort        int    // 远程端口
 | 
			
		||||
	listener          net.Listener
 | 
			
		||||
	localConnections  []net.Conn
 | 
			
		||||
	remoteConnections []net.Conn
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Tunnel) Open(sshClient *ssh.Client) {
 | 
			
		||||
	localAddr := fmt.Sprintf("%s:%d", r.localHost, r.localPort)
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		global.Log.Debugf("隧道 %v 等待客户端访问 %v", r.id, localAddr)
 | 
			
		||||
		localConn, err := r.listener.Accept()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			global.Log.Debugf("隧道 %v 接受连接失败 %v, 退出循环", r.id, err.Error())
 | 
			
		||||
			global.Log.Debug("-------------------------------------------------")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		r.localConnections = append(r.localConnections, localConn)
 | 
			
		||||
 | 
			
		||||
		global.Log.Debugf("隧道 %v 新增本地连接 %v", r.id, localConn.RemoteAddr().String())
 | 
			
		||||
		remoteAddr := fmt.Sprintf("%s:%d", r.remoteHost, r.remotePort)
 | 
			
		||||
		global.Log.Debugf("隧道 %v 连接远程地址 %v ...", r.id, remoteAddr)
 | 
			
		||||
		remoteConn, err := sshClient.Dial("tcp", remoteAddr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			global.Log.Debugf("隧道 %v 连接远程地址 %v, 退出循环", r.id, err.Error())
 | 
			
		||||
			global.Log.Debug("-------------------------------------------------")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		r.remoteConnections = append(r.remoteConnections, remoteConn)
 | 
			
		||||
 | 
			
		||||
		global.Log.Debugf("隧道 %v 连接远程主机成功", r.id)
 | 
			
		||||
		go copyConn(localConn, remoteConn)
 | 
			
		||||
		go copyConn(remoteConn, localConn)
 | 
			
		||||
		global.Log.Debugf("隧道 %v 开始转发数据 [%v]->[%v]", r.id, localAddr, remoteAddr)
 | 
			
		||||
		global.Log.Debug("~~~~~~~~~~~~~~~~~~~~分割线~~~~~~~~~~~~~~~~~~~~~~~~")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Tunnel) Close() {
 | 
			
		||||
	for i := range r.localConnections {
 | 
			
		||||
		_ = r.localConnections[i].Close()
 | 
			
		||||
	}
 | 
			
		||||
	r.localConnections = nil
 | 
			
		||||
	for i := range r.remoteConnections {
 | 
			
		||||
		_ = r.remoteConnections[i].Close()
 | 
			
		||||
	}
 | 
			
		||||
	r.remoteConnections = nil
 | 
			
		||||
	_ = r.listener.Close()
 | 
			
		||||
	global.Log.Debugf("隧道 %d 监听器关闭", r.id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func copyConn(writer, reader net.Conn) {
 | 
			
		||||
	_, _ = io.Copy(writer, reader)
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,7 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mayfly-go/internal/devops/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/devops/domain/repository"
 | 
			
		||||
	"mayfly-go/pkg/biz"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -51,9 +52,9 @@ func (m *machineRepo) GetById(id uint64, cols ...string) *entity.Machine {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *machineRepo) Create(entity *entity.Machine) {
 | 
			
		||||
	model.Insert(entity)
 | 
			
		||||
	biz.ErrIsNilAppendErr(model.Insert(entity), "创建机器信息失败: %s")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *machineRepo) UpdateById(entity *entity.Machine) {
 | 
			
		||||
	model.UpdateById(entity)
 | 
			
		||||
	biz.ErrIsNilAppendErr(model.UpdateById(entity), "更新机器信息失败: %s")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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,6 +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,6 +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,
 | 
			
		||||
  `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,
 | 
			
		||||
@@ -258,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,
 | 
			
		||||
@@ -284,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 '最后登录时间',
 | 
			
		||||
@@ -302,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;
 | 
			
		||||
 | 
			
		||||
-- ----------------------------
 | 
			
		||||
@@ -668,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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								server/pkg/utils/net.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								server/pkg/utils/net.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import "net"
 | 
			
		||||
 | 
			
		||||
// GetAvailablePort 获取可用端口
 | 
			
		||||
func GetAvailablePort() (int, error) {
 | 
			
		||||
	addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	l, err := net.ListenTCP("tcp", addr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defer func(l *net.TCPListener) {
 | 
			
		||||
		_ = l.Close()
 | 
			
		||||
	}(l)
 | 
			
		||||
	return l.Addr().(*net.TCPAddr).Port, 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)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func SSHConnect(user, password, host, key string, port int) (*ssh.Client, error) {
 | 
			
		||||
	var (
 | 
			
		||||
		auth         []ssh.AuthMethod
 | 
			
		||||
		addr         string
 | 
			
		||||
		clientConfig *ssh.ClientConfig
 | 
			
		||||
		client       *ssh.Client
 | 
			
		||||
		config       ssh.Config
 | 
			
		||||
		//session      *ssh.Session
 | 
			
		||||
		err error
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// get auth method
 | 
			
		||||
	auth = make([]ssh.AuthMethod, 0)
 | 
			
		||||
	if key == "" {
 | 
			
		||||
		auth = append(auth, ssh.Password(password))
 | 
			
		||||
	} else {
 | 
			
		||||
		pemBytes, err := ioutil.ReadFile(key)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var signer ssh.Signer
 | 
			
		||||
		if password == "" {
 | 
			
		||||
			signer, err = ssh.ParsePrivateKey(pemBytes)
 | 
			
		||||
		} else {
 | 
			
		||||
			signer, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(password))
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		auth = append(auth, ssh.PublicKeys(signer))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	clientConfig = &ssh.ClientConfig{
 | 
			
		||||
		User:    user,
 | 
			
		||||
		Auth:    auth,
 | 
			
		||||
		Timeout: 30 * time.Second,
 | 
			
		||||
		Config:  config,
 | 
			
		||||
		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	addr = fmt.Sprintf("%s:%d", host, port)
 | 
			
		||||
 | 
			
		||||
	client, err = ssh.Dial("tcp", addr, clientConfig)
 | 
			
		||||
	return client, err
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								server/static/static.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								server/static/static.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
package static
 | 
			
		||||
 | 
			
		||||
import "embed"
 | 
			
		||||
 | 
			
		||||
// 使用1.16特性编译阶段将静态资源文件打包进编译好的程序
 | 
			
		||||
var (
 | 
			
		||||
	//go:embed static/**
 | 
			
		||||
	Static embed.FS
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										1
									
								
								server/static/static/assets/401.1661345446364.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/static/static/assets/401.1661345446364.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
.error[data-v-6ec92039]{height:100%;background-color:#fff;display:flex}.error .error-flex[data-v-6ec92039]{margin:auto;display:flex;height:350px;width:900px}.error .error-flex .left[data-v-6ec92039]{flex:1;height:100%;align-items:center;display:flex}.error .error-flex .left .left-item .left-item-animation[data-v-6ec92039]{opacity:0;animation-name:error-num;animation-duration:.5s;animation-fill-mode:forwards}.error .error-flex .left .left-item .left-item-num[data-v-6ec92039]{color:#d6e0f6;font-size:55px}.error .error-flex .left .left-item .left-item-title[data-v-6ec92039]{font-size:20px;color:#333;margin:15px 0 5px;animation-delay:.1s}.error .error-flex .left .left-item .left-item-msg[data-v-6ec92039]{color:#c0bebe;font-size:12px;margin-bottom:30px;animation-delay:.2s}.error .error-flex .left .left-item .left-item-btn[data-v-6ec92039]{animation-delay:.2s}.error .error-flex .right[data-v-6ec92039]{flex:1;opacity:0;animation-name:error-img;animation-duration:2s;animation-fill-mode:forwards}.error .error-flex .right img[data-v-6ec92039]{width:100%;height:100%}
 | 
			
		||||
							
								
								
									
										1
									
								
								server/static/static/assets/401.1661345446364.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/static/static/assets/401.1661345446364.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
import{_ as s,u as n,b as l,e as c,h as e,g as d,w as f,Y as m,Q as u,R as _,d as p,B as h}from"./index.1661345446364.js";var x="assets/401.1661345446364.png";const v={name:"401",setup(){const t=n();return{onSetAuth:()=>{m(),t.push("/login")}}}},o=t=>(u("data-v-6ec92039"),t=t(),_(),t),g={class:"error"},y={class:"error-flex"},b={class:"left"},C={class:"left-item"},B=o(()=>e("div",{class:"left-item-animation left-item-num"},"401",-1)),w=o(()=>e("div",{class:"left-item-animation left-item-title"},"\u60A8\u672A\u88AB\u6388\u6743\u6216\u767B\u5F55\u8D85\u65F6\uFF0C\u6CA1\u6709\u64CD\u4F5C\u6743\u9650",-1)),A=o(()=>e("div",{class:"left-item-animation left-item-msg"},null,-1)),S={class:"left-item-animation left-item-btn"},F=h("\u91CD\u65B0\u767B\u5F55"),k=o(()=>e("div",{class:"right"},[e("img",{src:x})],-1));function I(t,r,z,a,D,N){const i=l("el-button");return p(),c("div",g,[e("div",y,[e("div",b,[e("div",C,[B,w,A,e("div",S,[d(i,{type:"primary",round:"",onClick:a.onSetAuth},{default:f(()=>[F]),_:1},8,["onClick"])])])]),k])])}var $=s(v,[["render",I],["__scopeId","data-v-6ec92039"]]);export{$ as default};
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								server/static/static/assets/401.1661345446364.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								server/static/static/assets/401.1661345446364.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 39 KiB  | 
							
								
								
									
										1
									
								
								server/static/static/assets/404.1661345446364.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/static/static/assets/404.1661345446364.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
.error[data-v-69e91ac8]{height:100%;background-color:#fff;display:flex}.error .error-flex[data-v-69e91ac8]{margin:auto;display:flex;height:350px;width:900px}.error .error-flex .left[data-v-69e91ac8]{flex:1;height:100%;align-items:center;display:flex}.error .error-flex .left .left-item .left-item-animation[data-v-69e91ac8]{opacity:0;animation-name:error-num;animation-duration:.5s;animation-fill-mode:forwards}.error .error-flex .left .left-item .left-item-num[data-v-69e91ac8]{color:#d6e0f6;font-size:55px}.error .error-flex .left .left-item .left-item-title[data-v-69e91ac8]{font-size:20px;color:#333;margin:15px 0 5px;animation-delay:.1s}.error .error-flex .left .left-item .left-item-msg[data-v-69e91ac8]{color:#c0bebe;font-size:12px;margin-bottom:30px;animation-delay:.2s}.error .error-flex .left .left-item .left-item-btn[data-v-69e91ac8]{animation-delay:.2s}.error .error-flex .right[data-v-69e91ac8]{flex:1;opacity:0;animation-name:error-img;animation-duration:2s;animation-fill-mode:forwards}.error .error-flex .right img[data-v-69e91ac8]{width:100%;height:100%}
 | 
			
		||||
							
								
								
									
										1
									
								
								server/static/static/assets/404.1661345446364.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/static/static/assets/404.1661345446364.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
import{_ as s,u as n,b as l,e as c,h as e,g as d,w as m,Q as f,R as u,d as _,B as p}from"./index.1661345446364.js";var h="assets/404.1661345446364.png";const x={name:"404",setup(){const t=n();return{onGoHome:()=>{t.push("/")}}}},o=t=>(f("data-v-69e91ac8"),t=t(),u(),t),v={class:"error"},g={class:"error-flex"},y={class:"left"},F={class:"left-item"},b=o(()=>e("div",{class:"left-item-animation left-item-num"},"404",-1)),C=o(()=>e("div",{class:"left-item-animation left-item-title"},"\u5730\u5740\u8F93\u5165\u6709\u8BEF\uFF0C\u8BF7\u91CD\u65B0\u8F93\u5165\u5730\u5740~",-1)),B=o(()=>e("div",{class:"left-item-animation left-item-msg"},"\u60A8\u53EF\u4EE5\u5148\u68C0\u67E5\u7F51\u5740\uFF0C\u7136\u540E\u91CD\u65B0\u8F93\u5165",-1)),E={class:"left-item-animation left-item-btn"},w=p("\u8FD4\u56DE\u9996\u9875"),k=o(()=>e("div",{class:"right"},[e("img",{src:h})],-1));function D(t,a,I,r,z,G){const i=l("el-button");return _(),c("div",v,[e("div",g,[e("div",y,[e("div",F,[b,C,B,e("div",E,[d(i,{type:"primary",round:"",onClick:r.onGoHome},{default:m(()=>[w]),_:1},8,["onClick"])])])]),k])])}var N=s(x,[["render",D],["__scopeId","data-v-69e91ac8"]]);export{N as default};
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								server/static/static/assets/404.1661345446364.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								server/static/static/assets/404.1661345446364.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 36 KiB  | 
							
								
								
									
										1
									
								
								server/static/static/assets/Api.1661345446364.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/static/static/assets/Api.1661345446364.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
import{p as r}from"./index.1661345446364.js";class s{constructor(t,e){this.url=t,this.method=e}setUrl(t){return this.url=t,this}setMethod(t){return this.method=t,this}getUrl(){return r.getApiUrl(this.url)}request(t=null,e=null){return r.send(this,t,e)}requestWithHeaders(t,e){return r.sendWithHeaders(this,t,e)}static create(t,e){return new s(t,e)}}export{s as A};
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
#string-value-text{flex-grow:1;display:flex;position:relative}#string-value-text .text-type-select{position:absolute;z-index:2;right:10px;top:10px;max-width:70px}
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user