mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 08:20:25 +08:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					f43851698e | ||
| 
						 | 
					73884bb693 | ||
| 
						 | 
					1b5bb1de8b | ||
| 
						 | 
					4814793546 | ||
| 
						 | 
					d85bbff270 | ||
| 
						 | 
					bb1522f4dc | ||
| 
						 | 
					a7632fbf58 | ||
| 
						 | 
					c4cb4234fd | 
@@ -22,7 +22,7 @@
 | 
			
		||||
 | 
			
		||||
### 介绍
 | 
			
		||||
 | 
			
		||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
 | 
			
		||||
web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据操作 数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台**
 | 
			
		||||
 | 
			
		||||
### 开发语言与主要框架
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ services:
 | 
			
		||||
    restart: always
 | 
			
		||||
 | 
			
		||||
  server:
 | 
			
		||||
    image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:v1.8.3
 | 
			
		||||
    image: ccr.ccs.tencentyun.com/mayfly/mayfly-go:v1.8.5
 | 
			
		||||
    build:
 | 
			
		||||
      context: .
 | 
			
		||||
      dockerfile: Dockerfile
 | 
			
		||||
 
 | 
			
		||||
@@ -10,20 +10,20 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@element-plus/icons-vue": "^2.3.1",
 | 
			
		||||
    "@vueuse/core": "^10.9.0",
 | 
			
		||||
    "@vueuse/core": "^10.10.0",
 | 
			
		||||
    "asciinema-player": "^3.7.1",
 | 
			
		||||
    "axios": "^1.6.2",
 | 
			
		||||
    "clipboard": "^2.0.11",
 | 
			
		||||
    "cropperjs": "^1.6.1",
 | 
			
		||||
    "dayjs": "^1.11.11",
 | 
			
		||||
    "echarts": "^5.5.0",
 | 
			
		||||
    "element-plus": "^2.7.2",
 | 
			
		||||
    "element-plus": "^2.7.4",
 | 
			
		||||
    "js-base64": "^3.7.7",
 | 
			
		||||
    "jsencrypt": "^3.3.2",
 | 
			
		||||
    "lodash": "^4.17.21",
 | 
			
		||||
    "mitt": "^3.0.1",
 | 
			
		||||
    "monaco-editor": "^0.48.0",
 | 
			
		||||
    "monaco-sql-languages": "^0.11.0",
 | 
			
		||||
    "monaco-editor": "^0.49.0",
 | 
			
		||||
    "monaco-sql-languages": "^0.12.0",
 | 
			
		||||
    "monaco-themes": "^0.4.4",
 | 
			
		||||
    "nprogress": "^0.2.0",
 | 
			
		||||
    "pinia": "^2.1.7",
 | 
			
		||||
@@ -55,9 +55,9 @@
 | 
			
		||||
    "eslint": "^8.35.0",
 | 
			
		||||
    "eslint-plugin-vue": "^9.25.0",
 | 
			
		||||
    "prettier": "^3.2.5",
 | 
			
		||||
    "sass": "^1.76.0",
 | 
			
		||||
    "sass": "^1.77.1",
 | 
			
		||||
    "typescript": "^5.4.5",
 | 
			
		||||
    "vite": "^5.2.11",
 | 
			
		||||
    "vite": "^5.2.12",
 | 
			
		||||
    "vue-eslint-parser": "^9.4.2"
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": [
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ const config = {
 | 
			
		||||
    baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
 | 
			
		||||
 | 
			
		||||
    // 系统版本
 | 
			
		||||
    version: 'v1.8.4',
 | 
			
		||||
    version: 'v1.8.7',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
 
 | 
			
		||||
@@ -97,43 +97,6 @@ export function getTextWidth(str: string) {
 | 
			
		||||
    return width;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 获取内容所需要占用的宽度
 | 
			
		||||
 */
 | 
			
		||||
export function getContentWidth(content: any): number {
 | 
			
		||||
    if (!content) {
 | 
			
		||||
        return 50;
 | 
			
		||||
    }
 | 
			
		||||
    // 以下分配的单位长度可根据实际需求进行调整
 | 
			
		||||
    let flexWidth = 0;
 | 
			
		||||
    for (const char of content) {
 | 
			
		||||
        if (flexWidth > 500) {
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) {
 | 
			
		||||
            // 小写字母、数字字符
 | 
			
		||||
            flexWidth += 9.3;
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        if (char >= 'A' && char <= 'Z') {
 | 
			
		||||
            flexWidth += 9;
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        if (char >= '\u4e00' && char <= '\u9fa5') {
 | 
			
		||||
            // 如果是中文字符,为字符分配16个单位宽度
 | 
			
		||||
            flexWidth += 20;
 | 
			
		||||
        } else {
 | 
			
		||||
            // 其他种类字符
 | 
			
		||||
            flexWidth += 8;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    // if (flexWidth > 450) {
 | 
			
		||||
    //     // 设置最大宽度
 | 
			
		||||
    //     flexWidth = 450;
 | 
			
		||||
    // }
 | 
			
		||||
    return flexWidth;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * @returns uuid
 | 
			
		||||
@@ -179,3 +142,38 @@ export async function copyToClipboard(txt: string, selector: string = '#copyValu
 | 
			
		||||
        clipboard.destroy();
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function fuzzyMatchField(keyword: string, fields: any[], ...valueExtractFuncs: Function[]) {
 | 
			
		||||
    keyword = keyword?.toLowerCase();
 | 
			
		||||
    return fields.filter((field) => {
 | 
			
		||||
        for (let valueExtractFunc of valueExtractFuncs) {
 | 
			
		||||
            const value = valueExtractFunc(field)?.toLowerCase();
 | 
			
		||||
            if (isPrefixSubsequence(keyword, value)) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 匹配是否为前缀子序列 targetTemplate=username prefix=uname -> true,prefix=uname2 -> false
 | 
			
		||||
 * @param prefix 字符串前缀(不连续也可以,但不改变字符的相对顺序)
 | 
			
		||||
 * @param targetTemplate 目标模板
 | 
			
		||||
 * @returns 是否匹配
 | 
			
		||||
 */
 | 
			
		||||
export function isPrefixSubsequence(prefix: string, targetTemplate: string) {
 | 
			
		||||
    let i = 0; // 指向prefix的索引
 | 
			
		||||
    let j = 0; // 指向targetTemplate的索引
 | 
			
		||||
 | 
			
		||||
    while (i < prefix.length && j < targetTemplate.length) {
 | 
			
		||||
        if (prefix[i] === targetTemplate[j]) {
 | 
			
		||||
            // 字符匹配,两个指针都向前移动
 | 
			
		||||
            i++;
 | 
			
		||||
        }
 | 
			
		||||
        j++; // 目标字符串指针始终向前移动
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 如果prefix的所有字符都被找到,返回true
 | 
			
		||||
    return i === prefix.length;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,7 @@ const state = reactive({
 | 
			
		||||
        search: null as any,
 | 
			
		||||
        weblinks: null as any,
 | 
			
		||||
    },
 | 
			
		||||
    status: TerminalStatus.NoConnected,
 | 
			
		||||
    status: -11,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
@@ -96,6 +96,7 @@ onBeforeUnmount(() => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function init() {
 | 
			
		||||
    state.status = TerminalStatus.NoConnected;
 | 
			
		||||
    if (term) {
 | 
			
		||||
        console.log('重新连接...');
 | 
			
		||||
        close();
 | 
			
		||||
@@ -105,7 +106,7 @@ function init() {
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initTerm() {
 | 
			
		||||
async function initTerm() {
 | 
			
		||||
    term = new Terminal({
 | 
			
		||||
        fontSize: themeConfig.value.terminalFontSize || 15,
 | 
			
		||||
        fontWeight: themeConfig.value.terminalFontWeight || 'normal',
 | 
			
		||||
@@ -155,6 +156,7 @@ function initSocket() {
 | 
			
		||||
        state.status = TerminalStatus.Connected;
 | 
			
		||||
 | 
			
		||||
        focus();
 | 
			
		||||
        fitTerminal();
 | 
			
		||||
 | 
			
		||||
        // 如果有初始要执行的命令,则发送执行命令
 | 
			
		||||
        if (props.cmd) {
 | 
			
		||||
@@ -209,7 +211,6 @@ function loadAddon() {
 | 
			
		||||
        // tell trzsz the terminal columns has been changed
 | 
			
		||||
        trzsz.setTerminalColumns(size.cols);
 | 
			
		||||
    });
 | 
			
		||||
    window.addEventListener('resize', () => state.addon.fit.fit());
 | 
			
		||||
    // enable drag files or directories to upload
 | 
			
		||||
    terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault());
 | 
			
		||||
    terminalRef.value.addEventListener('drop', (event: any) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,15 @@
 | 
			
		||||
import EnumValue from '@/common/Enum';
 | 
			
		||||
 | 
			
		||||
export enum TerminalStatus {
 | 
			
		||||
    Error = -1,
 | 
			
		||||
    NoConnected = 0,
 | 
			
		||||
    Connected = 1,
 | 
			
		||||
    Disconnected = 2,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TerminalStatusEnum = {
 | 
			
		||||
    Error: EnumValue.of(TerminalStatus.Error, '连接出错').setExtra({ iconColor: 'var(--el-color-error)' }),
 | 
			
		||||
    NoConnected: EnumValue.of(TerminalStatus.NoConnected, '未连接').setExtra({ iconColor: 'var(--el-color-primary)' }),
 | 
			
		||||
    Connected: EnumValue.of(TerminalStatus.Connected, '连接成功').setExtra({ iconColor: 'var(--el-color-success)' }),
 | 
			
		||||
    Disconnected: EnumValue.of(TerminalStatus.Disconnected, '连接失败').setExtra({ iconColor: 'var(--el-color-error)' }),
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,11 @@
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
                            <el-option :key="TagResourceTypeEnum.Db.value" :label="TagResourceTypeEnum.Db.label" :value="TagResourceTypeEnum.Db.value" />
 | 
			
		||||
                            <el-option
 | 
			
		||||
                                :key="TagResourceTypeEnum.Redis.value"
 | 
			
		||||
                                :label="TagResourceTypeEnum.Redis.label"
 | 
			
		||||
                                :value="TagResourceTypeEnum.Redis.value"
 | 
			
		||||
                            />
 | 
			
		||||
                        </el-select>
 | 
			
		||||
                    </el-form-item>
 | 
			
		||||
                    <el-form-item prop="resourceCode" label="资源编号" required>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
                :default-expanded-keys="props.defaultExpandedKeys"
 | 
			
		||||
            >
 | 
			
		||||
                <template #default="{ node, data }">
 | 
			
		||||
                    <span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
 | 
			
		||||
                    <span :id="node.key" @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
 | 
			
		||||
                        <span v-if="data.type.value == TagTreeNode.TagPath">
 | 
			
		||||
                            <tag-info :tag-path="data.label" />
 | 
			
		||||
                        </span>
 | 
			
		||||
@@ -48,11 +48,12 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
 | 
			
		||||
import { onMounted, reactive, ref, watch, toRefs, nextTick } from 'vue';
 | 
			
		||||
import { NodeType, TagTreeNode } from './tag';
 | 
			
		||||
import TagInfo from './TagInfo.vue';
 | 
			
		||||
import { Contextmenu } from '@/components/contextmenu';
 | 
			
		||||
import { tagApi } from '../tag/api';
 | 
			
		||||
import { isPrefixSubsequence } from '@/common/utils/string';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    resourceType: {
 | 
			
		||||
@@ -105,8 +106,7 @@ watch(filterText, (val) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const filterNode = (value: string, data: any) => {
 | 
			
		||||
    if (!value) return true;
 | 
			
		||||
    return data.label.includes(value);
 | 
			
		||||
    return !value || isPrefixSubsequence(value, data.label);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -126,7 +126,7 @@ const loadTags = async () => {
 | 
			
		||||
 * @param { Object } node
 | 
			
		||||
 * @param { Object } resolve
 | 
			
		||||
 */
 | 
			
		||||
const loadNode = async (node: any, resolve: any) => {
 | 
			
		||||
const loadNode = async (node: any, resolve: (data: any) => void, reject: () => void) => {
 | 
			
		||||
    if (typeof resolve !== 'function') {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -141,6 +141,8 @@ const loadNode = async (node: any, resolve: any) => {
 | 
			
		||||
        }
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
        console.error(e);
 | 
			
		||||
        // 调用 reject 以保持节点状态,并允许远程加载继续。
 | 
			
		||||
        return reject();
 | 
			
		||||
    }
 | 
			
		||||
    return resolve(nodes);
 | 
			
		||||
};
 | 
			
		||||
@@ -207,6 +209,17 @@ const getNode = (nodeKey: any) => {
 | 
			
		||||
 | 
			
		||||
const setCurrentKey = (nodeKey: any) => {
 | 
			
		||||
    treeRef.value.setCurrentKey(nodeKey);
 | 
			
		||||
 | 
			
		||||
    // 通过Id获取到对应的dom元素
 | 
			
		||||
    const node = document.getElementById(nodeKey);
 | 
			
		||||
    if (node) {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            nextTick(() => {
 | 
			
		||||
                // 通过scrollIntoView方法将对应的dom元素定位到可见区域 【block: 'center'】这个属性是在垂直方向居中显示
 | 
			
		||||
                node.scrollIntoView({ block: 'center' });
 | 
			
		||||
            });
 | 
			
		||||
        }, 100);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
                    v-bind="$attrs"
 | 
			
		||||
                    ref="tagTreeRef"
 | 
			
		||||
                    :data="state.tags"
 | 
			
		||||
                    :default-expanded-keys="checkedTags"
 | 
			
		||||
                    :default-expanded-keys="state.defaultExpandedKeys"
 | 
			
		||||
                    :default-checked-keys="checkedTags"
 | 
			
		||||
                    multiple
 | 
			
		||||
                    :render-after-expand="true"
 | 
			
		||||
@@ -50,6 +50,7 @@ import { ref, reactive, onMounted } from 'vue';
 | 
			
		||||
import { tagApi } from '../tag/api';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import EnumValue from '@/common/Enum';
 | 
			
		||||
import { isPrefixSubsequence } from '@/common/utils/string';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    height: {
 | 
			
		||||
@@ -74,10 +75,12 @@ const tagTreeRef: any = ref(null);
 | 
			
		||||
const filterTag = ref('');
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    defaultExpandedKeys: [] as any,
 | 
			
		||||
    tags: [],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    state.defaultExpandedKeys = checkedTags.value;
 | 
			
		||||
    search();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -100,10 +103,7 @@ const search = async () => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterNode = (value: string, data: any) => {
 | 
			
		||||
    if (!value) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
 | 
			
		||||
    return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onFilterValChanged = (val: string) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item prop="authCertName" label="授权凭证" required>
 | 
			
		||||
                    <el-select @change="changeAuthCert" v-model="form.authCertName" placeholder="请选择授权凭证" filterable>
 | 
			
		||||
                    <el-select v-model="form.authCertName" placeholder="请选择授权凭证" filterable>
 | 
			
		||||
                        <el-option v-for="item in state.authCerts" :key="item.id" :label="`${item.name}`" :value="item.name">
 | 
			
		||||
                            {{ item.name }}
 | 
			
		||||
 | 
			
		||||
@@ -39,8 +39,15 @@
 | 
			
		||||
                    </el-select>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item prop="getDatabaseMode" label="获库方式" required>
 | 
			
		||||
                    <el-select v-model="form.getDatabaseMode" @change="onChangeGetDatabaseMode" placeholder="请选择库名获取方式">
 | 
			
		||||
                        <el-option v-for="item in DbGetDbNamesMode" :key="item.value" :label="item.label" :value="item.value"> </el-option>
 | 
			
		||||
                    </el-select>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
 | 
			
		||||
                <el-form-item prop="database" label="数据库名">
 | 
			
		||||
                    <el-select
 | 
			
		||||
                        :disabled="form.getDatabaseMode == DbGetDbNamesMode.Auto.value || !form.authCertName"
 | 
			
		||||
                        v-model="dbNamesSelected"
 | 
			
		||||
                        multiple
 | 
			
		||||
                        clearable
 | 
			
		||||
@@ -49,8 +56,9 @@
 | 
			
		||||
                        filterable
 | 
			
		||||
                        :filter-method="filterDbNames"
 | 
			
		||||
                        allow-create
 | 
			
		||||
                        placeholder="请确保数据库实例信息填写完整后获取库名"
 | 
			
		||||
                        style="width: 100%"
 | 
			
		||||
                        placeholder="获库方式为‘指定库名’时,可选择"
 | 
			
		||||
                        @focus="getAllDatabase(form.authCertName)"
 | 
			
		||||
                        :loading="state.loadingDbNames"
 | 
			
		||||
                    >
 | 
			
		||||
                        <template #header>
 | 
			
		||||
                            <el-checkbox v-model="checkAllDbNames" :indeterminate="indeterminateDbNames" @change="handleCheckAll"> 全选 </el-checkbox>
 | 
			
		||||
@@ -75,7 +83,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRefs, reactive, watch, ref, watchEffect } from 'vue';
 | 
			
		||||
import { toRefs, reactive, watch, ref } from 'vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import { ElMessage } from 'element-plus';
 | 
			
		||||
import type { CheckboxValueType } from 'element-plus';
 | 
			
		||||
@@ -86,6 +94,7 @@ import EnumTag from '@/components/enumtag/EnumTag.vue';
 | 
			
		||||
import { AuthCertCiphertextTypeEnum } from '../tag/enums';
 | 
			
		||||
import { resourceAuthCertApi } from '../tag/api';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import { DbGetDbNamesMode } from './enums';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: {
 | 
			
		||||
@@ -147,10 +156,10 @@ const rules = {
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    database: [
 | 
			
		||||
    getDatabaseMode: [
 | 
			
		||||
        {
 | 
			
		||||
            required: true,
 | 
			
		||||
            message: '请添加数据库',
 | 
			
		||||
            message: '请选择库名获取方式',
 | 
			
		||||
            trigger: ['change', 'blur'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
@@ -172,38 +181,45 @@ const state = reactive({
 | 
			
		||||
    authCerts: [] as any,
 | 
			
		||||
    form: {
 | 
			
		||||
        id: null,
 | 
			
		||||
        // tagId: [],
 | 
			
		||||
        name: null,
 | 
			
		||||
        code: '',
 | 
			
		||||
        getDatabaseMode: DbGetDbNamesMode.Auto.value,
 | 
			
		||||
        database: '',
 | 
			
		||||
        remark: '',
 | 
			
		||||
        instanceId: null as any,
 | 
			
		||||
        authCertName: '',
 | 
			
		||||
    },
 | 
			
		||||
    instances: [] as any,
 | 
			
		||||
    loadingDbNames: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { dialogVisible, allDatabases, form, dbNamesSelected } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
    state.dialogVisible = props.visible;
 | 
			
		||||
    if (!state.dialogVisible) {
 | 
			
		||||
        return;
 | 
			
		||||
watch(
 | 
			
		||||
    () => props.visible,
 | 
			
		||||
    () => {
 | 
			
		||||
        state.dialogVisible = props.visible;
 | 
			
		||||
        if (!state.dialogVisible) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const db: any = props.db;
 | 
			
		||||
        if (db.code) {
 | 
			
		||||
            state.form = { ...db };
 | 
			
		||||
            if (db.getDatabaseMode == DbGetDbNamesMode.Assign.value) {
 | 
			
		||||
                // 将数据库名使用空格切割,获取所有数据库列表
 | 
			
		||||
                state.dbNamesSelected = db.database.split(' ');
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            state.form = { getDatabaseMode: DbGetDbNamesMode.Auto.value } as any;
 | 
			
		||||
            state.dbNamesSelected = [];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    const db: any = props.db;
 | 
			
		||||
    if (db.code) {
 | 
			
		||||
        state.form = { ...db };
 | 
			
		||||
        // state.form.tagId = newValue.db.tags.map((t: any) => t.tagId);
 | 
			
		||||
        // 将数据库名使用空格切割,获取所有数据库列表
 | 
			
		||||
        state.dbNamesSelected = db.database.split(' ');
 | 
			
		||||
    } else {
 | 
			
		||||
        state.form = {} as any;
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const onChangeGetDatabaseMode = (val: any) => {
 | 
			
		||||
    if (val == DbGetDbNamesMode.Auto.value) {
 | 
			
		||||
        state.dbNamesSelected = [];
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const changeAuthCert = (val: string) => {
 | 
			
		||||
    getAllDatabase(val);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getAuthCerts = async () => {
 | 
			
		||||
@@ -217,15 +233,20 @@ const getAuthCerts = async () => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getAllDatabase = async (authCertName: string) => {
 | 
			
		||||
    const req = { ...(props.instance as any) };
 | 
			
		||||
    req.authCert = state.authCerts?.find((x: any) => x.name == authCertName);
 | 
			
		||||
    let dbs = await dbApi.getAllDatabase.request(req);
 | 
			
		||||
    state.allDatabases = dbs;
 | 
			
		||||
    try {
 | 
			
		||||
        state.loadingDbNames = true;
 | 
			
		||||
        const req = { ...(props.instance as any) };
 | 
			
		||||
        req.authCert = state.authCerts?.find((x: any) => x.name == authCertName);
 | 
			
		||||
        let dbs = await dbApi.getAllDatabase.request(req);
 | 
			
		||||
        state.allDatabases = dbs;
 | 
			
		||||
 | 
			
		||||
    // 如果是oracle,且没查出数据库列表,则取实例sid
 | 
			
		||||
    let instance = state.instances.find((item: any) => item.id === state.form.instanceId);
 | 
			
		||||
    if (instance && instance.type === DbType.oracle && dbs.length === 0) {
 | 
			
		||||
        state.allDatabases = [instance.sid];
 | 
			
		||||
        // 如果是oracle,且没查出数据库列表,则取实例sid
 | 
			
		||||
        let instance = state.instances.find((item: any) => item.id === state.form.instanceId);
 | 
			
		||||
        if (instance && instance.type === DbType.oracle && dbs.length === 0) {
 | 
			
		||||
            state.allDatabases = [instance.sid];
 | 
			
		||||
        }
 | 
			
		||||
    } finally {
 | 
			
		||||
        state.loadingDbNames = false;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,93 +1,112 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="db-list">
 | 
			
		||||
        <page-table
 | 
			
		||||
            ref="pageTableRef"
 | 
			
		||||
            :page-api="dbApi.dbs"
 | 
			
		||||
            :before-query-fn="checkRouteTagPath"
 | 
			
		||||
            :search-items="searchItems"
 | 
			
		||||
            v-model:query-form="query"
 | 
			
		||||
            :columns="columns"
 | 
			
		||||
            lazy
 | 
			
		||||
        <el-drawer
 | 
			
		||||
            :title="title"
 | 
			
		||||
            v-model="dialogVisible"
 | 
			
		||||
            @open="search"
 | 
			
		||||
            :before-close="cancel"
 | 
			
		||||
            :destroy-on-close="true"
 | 
			
		||||
            :close-on-click-modal="true"
 | 
			
		||||
            size="60%"
 | 
			
		||||
        >
 | 
			
		||||
            <template #instanceSelect>
 | 
			
		||||
                <el-select remote :remote-method="getInstances" v-model="query.instanceId" placeholder="输入并选择实例" filterable clearable>
 | 
			
		||||
                    <el-option v-for="item in state.instances" :key="item.id" :label="`${item.name}`" :value="item.id">
 | 
			
		||||
                        {{ item.name }}
 | 
			
		||||
                        <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
 | 
			
		||||
                        {{ item.type }} / {{ item.host }}:{{ item.port }}
 | 
			
		||||
                        <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                        {{ item.username }}
 | 
			
		||||
                    </el-option>
 | 
			
		||||
                </el-select>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #type="{ data }">
 | 
			
		||||
                <el-tooltip :content="data.type" placement="top">
 | 
			
		||||
                    <SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
 | 
			
		||||
                </el-tooltip>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #host="{ data }">
 | 
			
		||||
                {{ `${data.host}:${data.port}` }}
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #database="{ data }">
 | 
			
		||||
                <el-popover placement="bottom" :width="200" trigger="click">
 | 
			
		||||
                    <template #reference>
 | 
			
		||||
                        <el-button @click="state.currentDbs = data.database" type="primary" link>查看库</el-button>
 | 
			
		||||
            <template #header>
 | 
			
		||||
                <DrawerHeader :header="title" :back="cancel">
 | 
			
		||||
                    <template #extra>
 | 
			
		||||
                        <div class="mr20">
 | 
			
		||||
                            <span>{{ $props.instance?.tags?.[0]?.codePath }}</span>
 | 
			
		||||
                            <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                            <SvgIcon :name="getDbDialect($props.instance?.type).getInfo()?.icon" :size="20" />
 | 
			
		||||
                            <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                            <span>{{ $props.instance?.host }}:{{ $props.instance?.port }}</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </template>
 | 
			
		||||
                    <el-table :data="filterDbs" size="small">
 | 
			
		||||
                        <el-table-column prop="dbName" label="数据库">
 | 
			
		||||
                            <template #header>
 | 
			
		||||
                                <el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
 | 
			
		||||
                            </template>
 | 
			
		||||
                        </el-table-column>
 | 
			
		||||
                    </el-table>
 | 
			
		||||
                </el-popover>
 | 
			
		||||
                </DrawerHeader>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #tagPath="{ data }">
 | 
			
		||||
                <ResourceTags :tags="data.tags" />
 | 
			
		||||
            </template>
 | 
			
		||||
            <page-table
 | 
			
		||||
                ref="pageTableRef"
 | 
			
		||||
                :page-api="dbApi.dbs"
 | 
			
		||||
                v-model:query-form="query"
 | 
			
		||||
                :columns="columns"
 | 
			
		||||
                lazy
 | 
			
		||||
                show-selection
 | 
			
		||||
                v-model:selection-data="state.selectionData"
 | 
			
		||||
            >
 | 
			
		||||
                <template #tableHeader>
 | 
			
		||||
                    <el-button v-auth="perms.saveDb" type="primary" circle icon="Plus" @click="editDb(null)"> </el-button>
 | 
			
		||||
                    <el-button v-auth="perms.delDb" :disabled="state.selectionData.length < 1" @click="deleteDb" type="danger" circle icon="delete"></el-button>
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
                <el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
 | 
			
		||||
                <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                <template #type="{ data }">
 | 
			
		||||
                    <el-tooltip :content="data.type" placement="top">
 | 
			
		||||
                        <SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
 | 
			
		||||
                    </el-tooltip>
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
                <el-dropdown @command="handleMoreActionCommand">
 | 
			
		||||
                    <span class="el-dropdown-link-more">
 | 
			
		||||
                        更多
 | 
			
		||||
                        <el-icon class="el-icon--right">
 | 
			
		||||
                            <arrow-down />
 | 
			
		||||
                        </el-icon>
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <template #dropdown>
 | 
			
		||||
                        <el-dropdown-menu>
 | 
			
		||||
                            <el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
 | 
			
		||||
                            <el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item>
 | 
			
		||||
                            <el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
 | 
			
		||||
                                备份任务
 | 
			
		||||
                            </el-dropdown-item>
 | 
			
		||||
                            <el-dropdown-item
 | 
			
		||||
                                :command="{ type: 'backupHistory', data }"
 | 
			
		||||
                                v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
 | 
			
		||||
                            >
 | 
			
		||||
                                备份历史
 | 
			
		||||
                            </el-dropdown-item>
 | 
			
		||||
                            <el-dropdown-item
 | 
			
		||||
                                :command="{ type: 'restoreDb', data }"
 | 
			
		||||
                                v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
 | 
			
		||||
                            >
 | 
			
		||||
                                恢复任务
 | 
			
		||||
                            </el-dropdown-item>
 | 
			
		||||
                        </el-dropdown-menu>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-dropdown>
 | 
			
		||||
            </template>
 | 
			
		||||
        </page-table>
 | 
			
		||||
                <template #database="{ data }">
 | 
			
		||||
                    <el-popover placement="bottom" :width="200" trigger="click">
 | 
			
		||||
                        <template #reference>
 | 
			
		||||
                            <el-button @click="getDbNames(data)" type="primary" link>查看库</el-button>
 | 
			
		||||
                        </template>
 | 
			
		||||
                        <el-table :data="filterDbs" v-loading="state.loadingDbNames" size="small">
 | 
			
		||||
                            <el-table-column prop="dbName" label="数据库">
 | 
			
		||||
                                <template #header>
 | 
			
		||||
                                    <el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
 | 
			
		||||
                                </template>
 | 
			
		||||
                            </el-table-column>
 | 
			
		||||
                        </el-table>
 | 
			
		||||
                    </el-popover>
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
        <el-dialog width="750px" :title="`${db} 数据库导出`" v-model="exportDialog.visible">
 | 
			
		||||
                <template #tagPath="{ data }">
 | 
			
		||||
                    <ResourceTags :tags="data.tags" />
 | 
			
		||||
                </template>
 | 
			
		||||
 | 
			
		||||
                <template #action="{ data }">
 | 
			
		||||
                    <el-button v-auth="perms.saveDb" @click="editDb(data)" type="primary" link>编辑</el-button>
 | 
			
		||||
 | 
			
		||||
                    <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
 | 
			
		||||
                    <el-button type="primary" @click="onShowSqlExec(data)" link>SQL记录</el-button>
 | 
			
		||||
 | 
			
		||||
                    <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
 | 
			
		||||
                    <el-dropdown @command="handleMoreActionCommand">
 | 
			
		||||
                        <span class="el-dropdown-link-more">
 | 
			
		||||
                            更多
 | 
			
		||||
                            <el-icon class="el-icon--right">
 | 
			
		||||
                                <arrow-down />
 | 
			
		||||
                            </el-icon>
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <template #dropdown>
 | 
			
		||||
                            <el-dropdown-menu>
 | 
			
		||||
                                <el-dropdown-item :command="{ type: 'dumpDb', data }"> 导出 </el-dropdown-item>
 | 
			
		||||
                                <el-dropdown-item
 | 
			
		||||
                                    :command="{ type: 'backupDb', data }"
 | 
			
		||||
                                    v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
 | 
			
		||||
                                >
 | 
			
		||||
                                    备份任务
 | 
			
		||||
                                </el-dropdown-item>
 | 
			
		||||
                                <el-dropdown-item
 | 
			
		||||
                                    :command="{ type: 'backupHistory', data }"
 | 
			
		||||
                                    v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
 | 
			
		||||
                                >
 | 
			
		||||
                                    备份历史
 | 
			
		||||
                                </el-dropdown-item>
 | 
			
		||||
                                <el-dropdown-item
 | 
			
		||||
                                    :command="{ type: 'restoreDb', data }"
 | 
			
		||||
                                    v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
 | 
			
		||||
                                >
 | 
			
		||||
                                    恢复任务
 | 
			
		||||
                                </el-dropdown-item>
 | 
			
		||||
                            </el-dropdown-menu>
 | 
			
		||||
                        </template>
 | 
			
		||||
                    </el-dropdown>
 | 
			
		||||
                </template>
 | 
			
		||||
            </page-table>
 | 
			
		||||
        </el-drawer>
 | 
			
		||||
 | 
			
		||||
        <el-dialog width="750px" :title="`${exportDialog.db} 数据库导出`" v-model="exportDialog.visible">
 | 
			
		||||
            <el-row justify="space-between">
 | 
			
		||||
                <el-col :span="9">
 | 
			
		||||
                    <el-form-item label="导出内容: ">
 | 
			
		||||
@@ -168,125 +187,98 @@
 | 
			
		||||
            <db-restore-list :dbId="dbRestoreDialog.dbId" :dbNames="dbRestoreDialog.dbs" />
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog">
 | 
			
		||||
            <el-descriptions title="详情" :column="3" border>
 | 
			
		||||
                <el-descriptions-item :span="2" label="名称">{{ infoDialog.data?.name }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="id">{{ infoDialog.data?.id }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="3" label="数据库实例名称">{{ infoDialog.instance?.name }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="主机">{{ infoDialog.instance?.host }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="端口">{{ infoDialog.instance?.port }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="授权凭证">{{ infoDialog.instance.authCertName }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="类型">
 | 
			
		||||
                    <SvgIcon :name="getDbDialect(infoDialog.instance?.type).getInfo().icon" :size="20" />{{ infoDialog.instance?.type }}
 | 
			
		||||
                </el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="3" label="备注">{{ infoDialog.data?.remark }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="创建时间">{{ formatDate(infoDialog.data?.createTime) }} </el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="创建者">{{ infoDialog.data?.creator }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="更新时间">{{ formatDate(infoDialog.data?.updateTime) }} </el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</el-descriptions-item>
 | 
			
		||||
            </el-descriptions>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
 | 
			
		||||
        <db-edit @val-change="search()" :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" v-model:db="dbEditDialog.data"></db-edit>
 | 
			
		||||
        <db-edit
 | 
			
		||||
            @confirm="confirmEditDb"
 | 
			
		||||
            @cancel="cancelEditDb"
 | 
			
		||||
            :title="dbEditDialog.title"
 | 
			
		||||
            v-model:visible="dbEditDialog.visible"
 | 
			
		||||
            :instance="props.instance"
 | 
			
		||||
            v-model:db="dbEditDialog.data"
 | 
			
		||||
        ></db-edit>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
 | 
			
		||||
import { computed, defineAsyncComponent, reactive, ref, Ref, toRefs } from 'vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import config from '@/common/config';
 | 
			
		||||
import { joinClientParams } from '@/common/request';
 | 
			
		||||
import { isTrue } from '@/common/assert';
 | 
			
		||||
import { formatDate } from '@/common/utils/format';
 | 
			
		||||
import PageTable from '@/components/pagetable/PageTable.vue';
 | 
			
		||||
import { TableColumn } from '@/components/pagetable';
 | 
			
		||||
import { hasPerms } from '@/components/auth/auth';
 | 
			
		||||
import DbSqlExecLog from './DbSqlExecLog.vue';
 | 
			
		||||
import { DbType } from './dialect';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { getDbDialect } from './dialect/index';
 | 
			
		||||
import { getTagPathSearchItem } from '../component/tag';
 | 
			
		||||
import { SearchItem } from '@/components/SearchForm';
 | 
			
		||||
import DbBackupList from './DbBackupList.vue';
 | 
			
		||||
import DbBackupHistoryList from './DbBackupHistoryList.vue';
 | 
			
		||||
import DbRestoreList from './DbRestoreList.vue';
 | 
			
		||||
import ResourceTags from '../component/ResourceTags.vue';
 | 
			
		||||
import { sleep } from '@/common/utils/loading';
 | 
			
		||||
import { DbGetDbNamesMode } from './enums';
 | 
			
		||||
import { DbInst } from './db';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
 | 
			
		||||
 | 
			
		||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
 | 
			
		||||
 | 
			
		||||
const searchItems = [
 | 
			
		||||
    getTagPathSearchItem(TagResourceTypeEnum.DbName.value),
 | 
			
		||||
    SearchItem.slot('instanceId', '实例', 'instanceSelect'),
 | 
			
		||||
    SearchItem.input('code', '编号'),
 | 
			
		||||
];
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    instance: {
 | 
			
		||||
        type: [Object],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    title: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const dialogVisible = defineModel<boolean>('visible');
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['cancel']);
 | 
			
		||||
 | 
			
		||||
const columns = ref([
 | 
			
		||||
    TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
 | 
			
		||||
    TableColumn.new('name', '名称'),
 | 
			
		||||
    TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
 | 
			
		||||
    TableColumn.new('instanceName', '实例名'),
 | 
			
		||||
    TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
 | 
			
		||||
    TableColumn.new('authCertName', '授权凭证'),
 | 
			
		||||
    TableColumn.new('getDatabaseMode', '获库方式').typeTag(DbGetDbNamesMode),
 | 
			
		||||
    TableColumn.new('database', '库').isSlot().setMinWidth(80),
 | 
			
		||||
    TableColumn.new('remark', '备注'),
 | 
			
		||||
    TableColumn.new('code', '编号'),
 | 
			
		||||
    TableColumn.new('action', '操作').isSlot().setMinWidth(210).fixedRight().alignCenter(),
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
const perms = {
 | 
			
		||||
    base: 'db',
 | 
			
		||||
    saveDb: 'db:save',
 | 
			
		||||
    delDb: 'db:del',
 | 
			
		||||
    backupDb: 'db:backup',
 | 
			
		||||
    restoreDb: 'db:restore',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 该用户拥有的的操作列按钮权限
 | 
			
		||||
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
 | 
			
		||||
const actionBtns = hasPerms(Object.values(perms));
 | 
			
		||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight().alignCenter();
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const pageTableRef: Ref<any> = ref(null);
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    row: {} as any,
 | 
			
		||||
    dbId: 0,
 | 
			
		||||
    db: '',
 | 
			
		||||
    currentDbs: '',
 | 
			
		||||
    loadingDbNames: false,
 | 
			
		||||
    currentDbNames: [],
 | 
			
		||||
    dbNameSearch: '',
 | 
			
		||||
    instances: [] as any,
 | 
			
		||||
    /**
 | 
			
		||||
     * 选中的数据
 | 
			
		||||
     */
 | 
			
		||||
    selectionData: [],
 | 
			
		||||
    selectionData: [] as any,
 | 
			
		||||
    /**
 | 
			
		||||
     * 查询条件
 | 
			
		||||
     */
 | 
			
		||||
    query: {
 | 
			
		||||
        tagPath: '',
 | 
			
		||||
        instanceId: null,
 | 
			
		||||
        instanceId: 0,
 | 
			
		||||
        pageNum: 1,
 | 
			
		||||
        pageSize: 0,
 | 
			
		||||
    },
 | 
			
		||||
    infoDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        data: null as any,
 | 
			
		||||
        instance: null as any,
 | 
			
		||||
        query: {
 | 
			
		||||
            instanceId: 0,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    // sql执行记录弹框
 | 
			
		||||
    sqlExecLogDialog: {
 | 
			
		||||
        title: '',
 | 
			
		||||
        visible: false,
 | 
			
		||||
        dbs: [],
 | 
			
		||||
        dbs: [] as any,
 | 
			
		||||
        dbId: 0,
 | 
			
		||||
    },
 | 
			
		||||
    // 数据库备份弹框
 | 
			
		||||
@@ -317,6 +309,7 @@ const state = reactive({
 | 
			
		||||
    exportDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        dbId: 0,
 | 
			
		||||
        db: '',
 | 
			
		||||
        type: 3,
 | 
			
		||||
        data: [] as any,
 | 
			
		||||
        value: [],
 | 
			
		||||
@@ -335,64 +328,77 @@ const state = reactive({
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { db, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state);
 | 
			
		||||
const { query, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
    if (Object.keys(actionBtns).length > 0) {
 | 
			
		||||
        columns.value.push(actionColumn);
 | 
			
		||||
const search = async () => {
 | 
			
		||||
    state.query.instanceId = props.instance?.id;
 | 
			
		||||
    pageTableRef.value.search();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getDbNames = async (db: any) => {
 | 
			
		||||
    try {
 | 
			
		||||
        state.loadingDbNames = true;
 | 
			
		||||
        state.currentDbNames = await DbInst.getDbNames(db);
 | 
			
		||||
    } finally {
 | 
			
		||||
        state.loadingDbNames = false;
 | 
			
		||||
    }
 | 
			
		||||
    search();
 | 
			
		||||
});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterDbs = computed(() => {
 | 
			
		||||
    const dbsStr = state.currentDbs;
 | 
			
		||||
    if (!dbsStr) {
 | 
			
		||||
    const dbNames = state.currentDbNames;
 | 
			
		||||
    if (!dbNames) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
    const dbs = dbsStr.split(' ').map((db: any) => {
 | 
			
		||||
        return { dbName: db };
 | 
			
		||||
    const dbNameObjs = dbNames.map((x) => {
 | 
			
		||||
        return {
 | 
			
		||||
            dbName: x,
 | 
			
		||||
        };
 | 
			
		||||
    });
 | 
			
		||||
    return dbs.filter((db: any) => {
 | 
			
		||||
    return dbNameObjs.filter((db: any) => {
 | 
			
		||||
        return db.dbName.includes(state.dbNameSearch);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const checkRouteTagPath = (query: any) => {
 | 
			
		||||
    if (route.query.tagPath) {
 | 
			
		||||
        query.tagPath = route.query.tagPath as string;
 | 
			
		||||
    }
 | 
			
		||||
    return query;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const search = async (tagPath: string = '') => {
 | 
			
		||||
    if (tagPath) {
 | 
			
		||||
        state.query.tagPath = tagPath;
 | 
			
		||||
    }
 | 
			
		||||
    pageTableRef.value.search();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const showInfo = async (info: any) => {
 | 
			
		||||
    state.infoDialog.data = info;
 | 
			
		||||
    state.infoDialog.query.instanceId = info.instanceId;
 | 
			
		||||
    const res = await dbApi.getInstance.request(state.infoDialog.query);
 | 
			
		||||
    state.infoDialog.instance = res;
 | 
			
		||||
    state.infoDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onBeforeCloseInfoDialog = () => {
 | 
			
		||||
    state.infoDialog.visible = false;
 | 
			
		||||
    state.infoDialog.data = null;
 | 
			
		||||
    state.infoDialog.instance = null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getInstances = async (instanceName = '') => {
 | 
			
		||||
    if (!instanceName) {
 | 
			
		||||
        state.instances = [];
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const data = await dbApi.instances.request({ name: instanceName });
 | 
			
		||||
const editDb = (data: any) => {
 | 
			
		||||
    if (data) {
 | 
			
		||||
        state.instances = data.list;
 | 
			
		||||
        state.dbEditDialog.data = { ...data };
 | 
			
		||||
    } else {
 | 
			
		||||
        state.dbEditDialog.data = {
 | 
			
		||||
            instanceId: props.instance.id,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    state.dbEditDialog.title = data ? '编辑数据库' : '新增数据库';
 | 
			
		||||
    state.dbEditDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const confirmEditDb = async (db: any) => {
 | 
			
		||||
    db.instanceId = props.instance.id;
 | 
			
		||||
    await dbApi.saveDb.request(db);
 | 
			
		||||
    ElMessage.success('保存成功');
 | 
			
		||||
    search();
 | 
			
		||||
    cancelEditDb();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancelEditDb = () => {
 | 
			
		||||
    state.dbEditDialog.visible = false;
 | 
			
		||||
    state.dbEditDialog.data = {};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteDb = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】库?`, '提示', {
 | 
			
		||||
            confirmButtonText: '确定',
 | 
			
		||||
            cancelButtonText: '取消',
 | 
			
		||||
            type: 'warning',
 | 
			
		||||
        });
 | 
			
		||||
        for (let db of state.selectionData) {
 | 
			
		||||
            await dbApi.deleteDb.request({ id: db.id });
 | 
			
		||||
        }
 | 
			
		||||
        ElMessage.success('删除成功');
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        //
 | 
			
		||||
    } finally {
 | 
			
		||||
        search();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -400,10 +406,6 @@ const handleMoreActionCommand = (commond: any) => {
 | 
			
		||||
    const data = commond.data;
 | 
			
		||||
    const type = commond.type;
 | 
			
		||||
    switch (type) {
 | 
			
		||||
        case 'detail': {
 | 
			
		||||
            showInfo(data);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        case 'dumpDb': {
 | 
			
		||||
            onDumpDbs(data);
 | 
			
		||||
            return;
 | 
			
		||||
@@ -426,7 +428,9 @@ const handleMoreActionCommand = (commond: any) => {
 | 
			
		||||
const onShowSqlExec = async (row: any) => {
 | 
			
		||||
    state.sqlExecLogDialog.title = `${row.name}`;
 | 
			
		||||
    state.sqlExecLogDialog.dbId = row.id;
 | 
			
		||||
    state.sqlExecLogDialog.dbs = row.database.split(' ');
 | 
			
		||||
    DbInst.getDbNames(row).then((res) => {
 | 
			
		||||
        state.sqlExecLogDialog.dbs = res;
 | 
			
		||||
    });
 | 
			
		||||
    state.sqlExecLogDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -439,26 +443,32 @@ const onBeforeCloseSqlExecDialog = () => {
 | 
			
		||||
const onShowDbBackupDialog = async (row: any) => {
 | 
			
		||||
    state.dbBackupDialog.title = `${row.name}`;
 | 
			
		||||
    state.dbBackupDialog.dbId = row.id;
 | 
			
		||||
    state.dbBackupDialog.dbs = row.database.split(' ');
 | 
			
		||||
    DbInst.getDbNames(row).then((res) => {
 | 
			
		||||
        state.sqlExecLogDialog.dbs = res;
 | 
			
		||||
    });
 | 
			
		||||
    state.dbBackupDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onShowDbBackupHistoryDialog = async (row: any) => {
 | 
			
		||||
    state.dbBackupHistoryDialog.title = `${row.name}`;
 | 
			
		||||
    state.dbBackupHistoryDialog.dbId = row.id;
 | 
			
		||||
    state.dbBackupHistoryDialog.dbs = row.database.split(' ');
 | 
			
		||||
    DbInst.getDbNames(row).then((res) => {
 | 
			
		||||
        state.sqlExecLogDialog.dbs = res;
 | 
			
		||||
    });
 | 
			
		||||
    state.dbBackupHistoryDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onShowDbRestoreDialog = async (row: any) => {
 | 
			
		||||
    state.dbRestoreDialog.title = `${row.name}`;
 | 
			
		||||
    state.dbRestoreDialog.dbId = row.id;
 | 
			
		||||
    state.dbRestoreDialog.dbs = row.database.split(' ');
 | 
			
		||||
    DbInst.getDbNames(row).then((res) => {
 | 
			
		||||
        state.sqlExecLogDialog.dbs = res;
 | 
			
		||||
    });
 | 
			
		||||
    state.dbRestoreDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onDumpDbs = async (row: any) => {
 | 
			
		||||
    const dbs = row.database.split(' ');
 | 
			
		||||
    const dbs = await DbInst.getDbNames(row);
 | 
			
		||||
    const data = [];
 | 
			
		||||
    for (let name of dbs) {
 | 
			
		||||
        data.push({
 | 
			
		||||
@@ -466,6 +476,7 @@ const onDumpDbs = async (row: any) => {
 | 
			
		||||
            label: name,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    state.exportDialog.db = row.name;
 | 
			
		||||
    state.exportDialog.value = [];
 | 
			
		||||
    state.exportDialog.data = data;
 | 
			
		||||
    state.exportDialog.dbId = row.id;
 | 
			
		||||
@@ -509,7 +520,10 @@ const supportAction = (action: string, dbType: string): boolean => {
 | 
			
		||||
    return actions.includes(action);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({ search });
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
    dialogVisible.value = false;
 | 
			
		||||
    emit('cancel');
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.db-list {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,170 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
 | 
			
		||||
            <template #header>
 | 
			
		||||
                <DrawerHeader :header="title" :back="cancel" />
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <el-table :data="state.dbs" stripe>
 | 
			
		||||
                <el-table-column prop="name" label="名称" show-overflow-tooltip min-width="100"> </el-table-column>
 | 
			
		||||
                <el-table-column prop="authCertName" label="授权凭证" min-width="120" show-overflow-tooltip> </el-table-column>
 | 
			
		||||
                <el-table-column prop="database" label="库" min-width="80">
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-popover placement="bottom" :width="200" trigger="click">
 | 
			
		||||
                            <template #reference>
 | 
			
		||||
                                <el-button @click="state.currentDbs = scope.row.database" type="primary" link>查看库</el-button>
 | 
			
		||||
                            </template>
 | 
			
		||||
                            <el-table :data="filterDbs" size="small">
 | 
			
		||||
                                <el-table-column prop="dbName" label="数据库">
 | 
			
		||||
                                    <template #header>
 | 
			
		||||
                                        <el-input v-model="state.dbNameSearch" size="small" placeholder="库名: 输入可过滤" clearable />
 | 
			
		||||
                                    </template>
 | 
			
		||||
                                </el-table-column>
 | 
			
		||||
                            </el-table>
 | 
			
		||||
                        </el-popover>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
 | 
			
		||||
                <el-table-column prop="remark" label="备注" show-overflow-tooltip min-width="120"> </el-table-column>
 | 
			
		||||
                <el-table-column prop="code" label="编号" show-overflow-tooltip min-width="120"> </el-table-column>
 | 
			
		||||
                <el-table-column min-wdith="120px">
 | 
			
		||||
                    <template #header>
 | 
			
		||||
                        操作
 | 
			
		||||
                        <el-button v-auth="perms.saveDb" type="primary" circle size="small" icon="Plus" @click="editDb(null)"> </el-button>
 | 
			
		||||
                    </template>
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-button v-auth="perms.saveDb" @click="editDb(scope.row)" type="primary" icon="edit" link></el-button>
 | 
			
		||||
                        <el-button class="ml1" v-auth="perms.delDb" type="danger" @click="deleteDb(scope.row)" icon="delete" link></el-button>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
            </el-table>
 | 
			
		||||
 | 
			
		||||
            <db-edit
 | 
			
		||||
                @confirm="confirmEditDb"
 | 
			
		||||
                @cancel="cancelEditDb"
 | 
			
		||||
                :title="dbEditDialog.title"
 | 
			
		||||
                v-model:visible="dbEditDialog.visible"
 | 
			
		||||
                :instance="props.instance"
 | 
			
		||||
                v-model:db="dbEditDialog.data"
 | 
			
		||||
            ></db-edit>
 | 
			
		||||
        </el-drawer>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, reactive, toRefs, watchEffect } from 'vue';
 | 
			
		||||
import { dbApi } from './api';
 | 
			
		||||
import { ElMessage, ElMessageBox } from 'element-plus';
 | 
			
		||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
 | 
			
		||||
import DbEdit from './DbEdit.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: {
 | 
			
		||||
        type: Boolean,
 | 
			
		||||
    },
 | 
			
		||||
    instance: {
 | 
			
		||||
        type: [Object],
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    title: {
 | 
			
		||||
        type: String,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const perms = {
 | 
			
		||||
    base: 'db',
 | 
			
		||||
    saveDb: 'db:save',
 | 
			
		||||
    delDb: 'db:del',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
//定义事件
 | 
			
		||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    dialogVisible: false,
 | 
			
		||||
    dbs: [] as any,
 | 
			
		||||
    currentDbs: '', // 当前数据库名,空格分割库名
 | 
			
		||||
    dbNameSearch: '',
 | 
			
		||||
    dbEditDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        data: null as any,
 | 
			
		||||
        title: '新增数据库',
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { dialogVisible, dbEditDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
    state.dialogVisible = props.visible;
 | 
			
		||||
    if (!state.dialogVisible) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDbs();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const filterDbs = computed(() => {
 | 
			
		||||
    const dbsStr = state.currentDbs;
 | 
			
		||||
    if (!dbsStr) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
    const dbs = dbsStr.split(' ').map((db: any) => {
 | 
			
		||||
        return { dbName: db };
 | 
			
		||||
    });
 | 
			
		||||
    return dbs.filter((db: any) => {
 | 
			
		||||
        return db.dbName.includes(state.dbNameSearch);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
    emit('update:visible', false);
 | 
			
		||||
    emit('cancel');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getDbs = () => {
 | 
			
		||||
    dbApi.dbs.request({ pageSize: 200, instanceId: props.instance.id }).then((res: any) => {
 | 
			
		||||
        state.dbs = res.list || [];
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const editDb = (data: any) => {
 | 
			
		||||
    if (data) {
 | 
			
		||||
        state.dbEditDialog.data = { ...data };
 | 
			
		||||
    } else {
 | 
			
		||||
        state.dbEditDialog.data = {
 | 
			
		||||
            instanceId: props.instance.id,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    state.dbEditDialog.title = data ? '编辑数据库' : '新增数据库';
 | 
			
		||||
    state.dbEditDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const deleteDb = async (db: any) => {
 | 
			
		||||
    try {
 | 
			
		||||
        await ElMessageBox.confirm(`确定删除【${db.name}】库?`, '提示', {
 | 
			
		||||
            confirmButtonText: '确定',
 | 
			
		||||
            cancelButtonText: '取消',
 | 
			
		||||
            type: 'warning',
 | 
			
		||||
        });
 | 
			
		||||
        await dbApi.deleteDb.request({ id: db.id });
 | 
			
		||||
        ElMessage.success('删除成功');
 | 
			
		||||
        getDbs();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        //
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const confirmEditDb = async (db: any) => {
 | 
			
		||||
    db.instanceId = props.instance.id;
 | 
			
		||||
    await dbApi.saveDb.request(db);
 | 
			
		||||
    ElMessage.success('保存成功');
 | 
			
		||||
    getDbs();
 | 
			
		||||
    cancelEditDb();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancelEditDb = () => {
 | 
			
		||||
    state.dbEditDialog.visible = false;
 | 
			
		||||
    state.dbEditDialog.data = {};
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss"></style>
 | 
			
		||||
@@ -35,7 +35,7 @@
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
                <el-button @click="showInfo(data)" link>详情</el-button>
 | 
			
		||||
                <el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
 | 
			
		||||
                <el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>库配置</el-button>
 | 
			
		||||
                <el-button v-if="actionBtns[perms.saveDb]" @click="editDb(data)" type="primary" link>库管理</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
        </page-table>
 | 
			
		||||
 | 
			
		||||
@@ -68,7 +68,7 @@
 | 
			
		||||
            v-model:data="instanceEditDialog.data"
 | 
			
		||||
        ></instance-edit>
 | 
			
		||||
 | 
			
		||||
        <instance-db-conf :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" />
 | 
			
		||||
        <DbList :title="dbEditDialog.title" v-model:visible="dbEditDialog.visible" :instance="dbEditDialog.instance" />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -89,7 +89,7 @@ import { getTagPathSearchItem } from '../component/tag';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
 | 
			
		||||
const InstanceEdit = defineAsyncComponent(() => import('./InstanceEdit.vue'));
 | 
			
		||||
const InstanceDbConf = defineAsyncComponent(() => import('./InstanceDbConf.vue'));
 | 
			
		||||
const DbList = defineAsyncComponent(() => import('./DbList.vue'));
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    lazy: {
 | 
			
		||||
@@ -215,7 +215,7 @@ const deleteInstance = async () => {
 | 
			
		||||
 | 
			
		||||
const editDb = (data: any) => {
 | 
			
		||||
    state.dbEditDialog.instance = data;
 | 
			
		||||
    state.dbEditDialog.title = `配置 "${data.name}" 数据库`;
 | 
			
		||||
    state.dbEditDialog.title = `管理 "${data.name}" 数据库`;
 | 
			
		||||
    state.dbEditDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -58,16 +58,61 @@
 | 
			
		||||
                    <el-row>
 | 
			
		||||
                        <el-col :span="24" v-if="state.db">
 | 
			
		||||
                            <el-descriptions :column="4" size="small" border>
 | 
			
		||||
                                <el-descriptions-item label-align="right" label="操作"
 | 
			
		||||
                                    ><el-button
 | 
			
		||||
                                <el-descriptions-item label-align="right" label="操作">
 | 
			
		||||
                                    <el-button
 | 
			
		||||
                                        :disabled="!state.db || !nowDbInst.id"
 | 
			
		||||
                                        type="primary"
 | 
			
		||||
                                        icon="Search"
 | 
			
		||||
                                        @click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases }, state.db)"
 | 
			
		||||
                                        size="small"
 | 
			
		||||
                                        >新建查询</el-button
 | 
			
		||||
                                    ></el-descriptions-item
 | 
			
		||||
                                >
 | 
			
		||||
                                        link
 | 
			
		||||
                                        @click="
 | 
			
		||||
                                            addQueryTab(
 | 
			
		||||
                                                { id: nowDbInst.id, dbs: nowDbInst.databases, nodeKey: getSqlMenuNodeKey(nowDbInst.id, state.db) },
 | 
			
		||||
                                                state.db
 | 
			
		||||
                                            )
 | 
			
		||||
                                        "
 | 
			
		||||
                                        title="新建查询"
 | 
			
		||||
                                    >
 | 
			
		||||
                                    </el-button>
 | 
			
		||||
 | 
			
		||||
                                    <template v-if="!dbConfig.locationTreeNode">
 | 
			
		||||
                                        <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                                        <el-button @click="locationNowTreeNode(null)" title="定位至左侧树的指定位置" icon="Location" link></el-button>
 | 
			
		||||
                                    </template>
 | 
			
		||||
 | 
			
		||||
                                    <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                                    <!-- 数据库展示配置 -->
 | 
			
		||||
                                    <el-popover
 | 
			
		||||
                                        popper-style="max-height: 550px; overflow: auto; max-width: 450px"
 | 
			
		||||
                                        placement="bottom"
 | 
			
		||||
                                        width="auto"
 | 
			
		||||
                                        title="数据库展示配置"
 | 
			
		||||
                                        trigger="click"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <el-row>
 | 
			
		||||
                                            <el-checkbox
 | 
			
		||||
                                                v-model="dbConfig.showColumnComment"
 | 
			
		||||
                                                label="显示字段备注"
 | 
			
		||||
                                                :true-value="1"
 | 
			
		||||
                                                :false-value="0"
 | 
			
		||||
                                                size="small"
 | 
			
		||||
                                            />
 | 
			
		||||
                                        </el-row>
 | 
			
		||||
 | 
			
		||||
                                        <el-row>
 | 
			
		||||
                                            <el-checkbox
 | 
			
		||||
                                                v-model="dbConfig.locationTreeNode"
 | 
			
		||||
                                                label="自动定位树节点"
 | 
			
		||||
                                                :true-value="1"
 | 
			
		||||
                                                :false-value="0"
 | 
			
		||||
                                                size="small"
 | 
			
		||||
                                            />
 | 
			
		||||
                                        </el-row>
 | 
			
		||||
 | 
			
		||||
                                        <template #reference>
 | 
			
		||||
                                            <el-link type="primary" icon="setting" :underline="false"></el-link>
 | 
			
		||||
                                        </template>
 | 
			
		||||
                                    </el-popover>
 | 
			
		||||
                                </el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                                <el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
@@ -103,7 +148,9 @@
 | 
			
		||||
                            <el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
 | 
			
		||||
                                <template #label>
 | 
			
		||||
                                    <el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
 | 
			
		||||
                                        <template #reference> {{ dt.label }} </template>
 | 
			
		||||
                                        <template #reference>
 | 
			
		||||
                                            <span class="font12">{{ dt.label }}</span>
 | 
			
		||||
                                        </template>
 | 
			
		||||
                                        <template #default>
 | 
			
		||||
                                            <el-descriptions :column="1" size="small">
 | 
			
		||||
                                                <el-descriptions-item label="tagPath">
 | 
			
		||||
@@ -130,6 +177,7 @@
 | 
			
		||||
                                    :db-name="dt.db"
 | 
			
		||||
                                    :table-name="dt.params.table"
 | 
			
		||||
                                    :table-height="state.dataTabsTableHeight"
 | 
			
		||||
                                    :ref="(el: any) => (dt.componentRef = el)"
 | 
			
		||||
                                ></db-table-data-op>
 | 
			
		||||
 | 
			
		||||
                                <db-sql-editor
 | 
			
		||||
@@ -138,6 +186,7 @@
 | 
			
		||||
                                    :db-name="dt.db"
 | 
			
		||||
                                    :sql-name="dt.params.sqlName"
 | 
			
		||||
                                    @save-sql-success="reloadSqls"
 | 
			
		||||
                                    :ref="(el: any) => (dt.componentRef = el)"
 | 
			
		||||
                                >
 | 
			
		||||
                                </db-sql-editor>
 | 
			
		||||
 | 
			
		||||
@@ -184,7 +233,7 @@ import { getDbDialect, schemaDbTypes } from './dialect/index';
 | 
			
		||||
import { sleep } from '@/common/utils/loading';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import { Pane, Splitpanes } from 'splitpanes';
 | 
			
		||||
import { useEventListener } from '@vueuse/core';
 | 
			
		||||
import { useEventListener, useStorage } from '@vueuse/core';
 | 
			
		||||
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
 | 
			
		||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
 | 
			
		||||
import { storeToRefs } from 'pinia';
 | 
			
		||||
@@ -272,7 +321,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
 | 
			
		||||
// 数据库实例节点类型
 | 
			
		||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
			
		||||
    const params = parentNode.params;
 | 
			
		||||
    const dbs = params.database.split(' ')?.sort();
 | 
			
		||||
    const dbs = (await DbInst.getDbNames(params))?.sort();
 | 
			
		||||
 | 
			
		||||
    const flowProcdef = await procdefApi.getByResource.request({ resourceType: TagResourceTypeEnum.DbName.value, resourceCode: params.code });
 | 
			
		||||
    return dbs.map((x: any) => {
 | 
			
		||||
@@ -456,6 +505,8 @@ const state = reactive({
 | 
			
		||||
 | 
			
		||||
const { nowDbInst, tableCreateDialog } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
const dbConfig = useStorage('dbConfig', { showColumnComment: false, locationTreeNode: false });
 | 
			
		||||
 | 
			
		||||
const serverInfoReqParam = ref({
 | 
			
		||||
    instanceId: 0,
 | 
			
		||||
});
 | 
			
		||||
@@ -530,7 +581,7 @@ const loadTableData = async (db: any, dbName: string, tableName: string) => {
 | 
			
		||||
    }
 | 
			
		||||
    changeDb(db, dbName);
 | 
			
		||||
 | 
			
		||||
    const key = `${db.id}:\`${dbName}\`.${tableName}`;
 | 
			
		||||
    const key = `tableData:${db.id}.${dbName}.${tableName}`;
 | 
			
		||||
    let tab = state.tabs.get(key);
 | 
			
		||||
    state.activeName = key;
 | 
			
		||||
    // 如果存在该表tab,则直接返回
 | 
			
		||||
@@ -565,7 +616,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
 | 
			
		||||
    // 存在sql模板名,则该模板名只允许一个tab
 | 
			
		||||
    if (sqlName) {
 | 
			
		||||
        label = `查询-${sqlName}`;
 | 
			
		||||
        key = `查询:${dbId}:${dbName}.${sqlName}`;
 | 
			
		||||
        key = `query:${dbId}.${dbName}.${sqlName}`;
 | 
			
		||||
    } else {
 | 
			
		||||
        let count = 1;
 | 
			
		||||
        state.tabs.forEach((v) => {
 | 
			
		||||
@@ -574,7 +625,7 @@ const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        label = `新查询-${count}`;
 | 
			
		||||
        key = `新查询${count}:${dbId}:${dbName}`;
 | 
			
		||||
        key = `query:${count}.${dbId}.${dbName}`;
 | 
			
		||||
    }
 | 
			
		||||
    state.activeName = key;
 | 
			
		||||
    let tab = state.tabs.get(key);
 | 
			
		||||
@@ -611,7 +662,7 @@ const addTablesOpTab = async (db: any) => {
 | 
			
		||||
    changeDb(db, dbName);
 | 
			
		||||
 | 
			
		||||
    const dbId = db.id;
 | 
			
		||||
    let key = `表操作:${dbId}:${dbName}.tablesOp`;
 | 
			
		||||
    let key = `tablesOp:${dbId}.${dbName}`;
 | 
			
		||||
    state.activeName = key;
 | 
			
		||||
 | 
			
		||||
    let tab = state.tabs.get(key);
 | 
			
		||||
@@ -642,15 +693,22 @@ const onRemoveTab = (targetName: string) => {
 | 
			
		||||
        if (tabName !== targetName) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        state.tabs.delete(targetName);
 | 
			
		||||
        if (activeName != targetName) {
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
 | 
			
		||||
        const nextTab = tabNames[i + 1] || tabNames[i - 1];
 | 
			
		||||
        if (nextTab) {
 | 
			
		||||
            activeName = nextTab;
 | 
			
		||||
        } else {
 | 
			
		||||
            activeName = '';
 | 
			
		||||
        }
 | 
			
		||||
        state.tabs.delete(targetName);
 | 
			
		||||
        state.activeName = activeName;
 | 
			
		||||
        onTabChange();
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -670,6 +728,21 @@ const onTabChange = () => {
 | 
			
		||||
        registerDbCompletionItemProvider(nowTab.dbId, nowTab.db, nowTab.params.dbs, nowDbInst.value.type);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 激活当前tab(需要调用DbTableData组件的active,否则表头与数据会出现错位,暂不知为啥,先这样处理)
 | 
			
		||||
    nowTab?.componentRef?.active();
 | 
			
		||||
 | 
			
		||||
    if (dbConfig.value.locationTreeNode) {
 | 
			
		||||
        locationNowTreeNode(nowTab);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 定位至当前树节点
 | 
			
		||||
 */
 | 
			
		||||
const locationNowTreeNode = (nowTab: any = null) => {
 | 
			
		||||
    if (!nowTab) {
 | 
			
		||||
        nowTab = state.tabs.get(state.activeName);
 | 
			
		||||
    }
 | 
			
		||||
    tagTreeRef.value.setCurrentKey(nowTab?.treeNodeKey);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -854,7 +927,7 @@ const getNowDbInfo = () => {
 | 
			
		||||
            margin: 0 0 5px;
 | 
			
		||||
 | 
			
		||||
            .el-tabs__item {
 | 
			
		||||
                padding: 0 10px;
 | 
			
		||||
                padding: 0 5px;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,10 @@ export const dbApi = {
 | 
			
		||||
            if (process.env.NODE_ENV === 'development') {
 | 
			
		||||
                console.log(param.sql);
 | 
			
		||||
            }
 | 
			
		||||
            param.sql = Base64.encode(param.sql);
 | 
			
		||||
            // 非base64编码sql,则进行base64编码(refreshToken时,会重复调用该方法,故简单判断下)
 | 
			
		||||
            if (!Base64.isValid(param.sql)) {
 | 
			
		||||
                param.sql = Base64.encode(param.sql);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return param;
 | 
			
		||||
    }),
 | 
			
		||||
@@ -40,6 +43,7 @@ export const dbApi = {
 | 
			
		||||
    instances: Api.newGet('/instances'),
 | 
			
		||||
    getInstance: Api.newGet('/instances/{instanceId}'),
 | 
			
		||||
    getAllDatabase: Api.newPost('/instances/databases'),
 | 
			
		||||
    getDbNamesByAc: Api.newGet('/instances/databases/{authCertName}'),
 | 
			
		||||
    getInstanceServerInfo: Api.newGet('/instances/{instanceId}/server-info'),
 | 
			
		||||
    testConn: Api.newPost('/instances/test-conn'),
 | 
			
		||||
    saveInstance: Api.newPost('/instances'),
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
 | 
			
		||||
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { DbInst } from '../db';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    dbId: {
 | 
			
		||||
@@ -101,9 +102,9 @@ const noSchemaType = (type: string) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 数据库实例节点类型
 | 
			
		||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
 | 
			
		||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
			
		||||
    const params = parentNode.params;
 | 
			
		||||
    const dbs = params.database.split(' ')?.sort();
 | 
			
		||||
    const dbs = (await DbInst.getDbNames(params))?.sort();
 | 
			
		||||
    let fn: NodeType;
 | 
			
		||||
    if (noSchemaType(params.type)) {
 | 
			
		||||
        fn = MysqlNodeTypes;
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@
 | 
			
		||||
 | 
			
		||||
            <Pane :size="100 - state.editorSize">
 | 
			
		||||
                <div class="mt5 sql-exec-res h100">
 | 
			
		||||
                    <el-tabs class="h100 w100" v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" v-model="state.activeTab">
 | 
			
		||||
                    <el-tabs class="h100 w100" v-if="state.execResTabs.length > 0" @tab-remove="onRemoveTab" @tab-change="active" v-model="state.activeTab">
 | 
			
		||||
                        <el-tab-pane class="h100" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
 | 
			
		||||
                            <template #label>
 | 
			
		||||
                                <el-popover :show-after="1000" placement="top-start" title="执行信息" trigger="hover" :width="300">
 | 
			
		||||
@@ -700,6 +700,19 @@ const initMonacoEditor = () => {
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const active = () => {
 | 
			
		||||
    const resTab = state.execResTabs[state.activeTab - 1];
 | 
			
		||||
    if (!resTab || !resTab.dbTableRef) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resTab.dbTableRef?.active();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
    active,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
 
 | 
			
		||||
@@ -152,6 +152,10 @@ const getEditorLangByValue = (value: any) => {
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
.string-input-container {
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    .el-input__wrapper {
 | 
			
		||||
        padding: 1px 3px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
.string-input-container-show-icon {
 | 
			
		||||
    .el-input__inner {
 | 
			
		||||
@@ -174,6 +178,10 @@ const getEditorLangByValue = (value: any) => {
 | 
			
		||||
    .el-input__prefix {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .el-input__wrapper {
 | 
			
		||||
        padding: 1px 3px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.edit-time-picker-popper {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@
 | 
			
		||||
                    fixed
 | 
			
		||||
                    class="table"
 | 
			
		||||
                    :row-event-handlers="rowEventHandlers"
 | 
			
		||||
                    @scroll="onTableScroll"
 | 
			
		||||
                >
 | 
			
		||||
                    <template #header="{ columns }">
 | 
			
		||||
                        <div v-for="(column, i) in columns" :key="i">
 | 
			
		||||
@@ -59,9 +60,7 @@
 | 
			
		||||
                                    </div>
 | 
			
		||||
 | 
			
		||||
                                    <div v-else class="header-column-title">
 | 
			
		||||
                                        <b class="el-text">
 | 
			
		||||
                                            {{ column.title }}
 | 
			
		||||
                                        </b>
 | 
			
		||||
                                        <b class="el-text"> {{ column.title }} </b>
 | 
			
		||||
                                    </div>
 | 
			
		||||
 | 
			
		||||
                                    <!-- 字段列右部分内容 -->
 | 
			
		||||
@@ -96,7 +95,7 @@
 | 
			
		||||
                                    />
 | 
			
		||||
                                </div>
 | 
			
		||||
 | 
			
		||||
                                <div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active' : ''">
 | 
			
		||||
                                <div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active ml2 mr2' : 'ml2 mr2'">
 | 
			
		||||
                                    <span v-if="rowData[column.dataKey!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
 | 
			
		||||
 | 
			
		||||
                                    <span v-else :title="rowData[column.dataKey!]" class="el-text el-text--small is-truncated">
 | 
			
		||||
@@ -121,7 +120,7 @@
 | 
			
		||||
 | 
			
		||||
                    <template #empty>
 | 
			
		||||
                        <div style="text-align: center">
 | 
			
		||||
                            <el-empty class="h100" :description="props.emptyText" :image-size="100" />
 | 
			
		||||
                            <el-empty :description="props.emptyText" :image-size="100" />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-v2>
 | 
			
		||||
@@ -486,7 +485,7 @@ const setTableColumns = (columns: any) => {
 | 
			
		||||
            dataKey: columnName,
 | 
			
		||||
            width: DbInst.flexColumnWidth(columnName, state.datas),
 | 
			
		||||
            title: columnName,
 | 
			
		||||
            align: 'center',
 | 
			
		||||
            align: x.dataType == DataType.Number ? 'right' : 'left',
 | 
			
		||||
            headerClass: 'table-column',
 | 
			
		||||
            class: 'table-column',
 | 
			
		||||
            sortable: true,
 | 
			
		||||
@@ -841,11 +840,23 @@ const triggerRefresh = () => {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const scrollLeftValue = ref(0);
 | 
			
		||||
const onTableScroll = (param: any) => {
 | 
			
		||||
    scrollLeftValue.value = param.scrollLeft;
 | 
			
		||||
};
 | 
			
		||||
/**
 | 
			
		||||
 * 激活表格,恢复滚动位置,否则会造成表头与数据单元格错位(暂不知为啥,先这样解决)
 | 
			
		||||
 */
 | 
			
		||||
const active = () => {
 | 
			
		||||
    setTimeout(() => tableRef.value.scrollToLeft(scrollLeftValue.value));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getNowDbInst = () => {
 | 
			
		||||
    return DbInst.getInst(state.dbId);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
    active,
 | 
			
		||||
    submitUpdateFields,
 | 
			
		||||
    cancelUpdateFields,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -259,7 +259,7 @@ import DbTableData from './DbTableData.vue';
 | 
			
		||||
import { DbDialect } from '@/views/ops/db/dialect';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import { useEventListener, useStorage } from '@vueuse/core';
 | 
			
		||||
import { copyToClipboard } from '@/common/utils/string';
 | 
			
		||||
import { copyToClipboard, fuzzyMatchField } from '@/common/utils/string';
 | 
			
		||||
import DbTableDataForm from './DbTableDataForm.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
@@ -476,10 +476,7 @@ const getColumnTips = (queryString: string, callback: any) => {
 | 
			
		||||
 | 
			
		||||
    let res = [];
 | 
			
		||||
    if (columnNameSearch) {
 | 
			
		||||
        columnNameSearch = columnNameSearch.toLowerCase();
 | 
			
		||||
        res = columns.filter((data: any) => {
 | 
			
		||||
            return data.columnName.toLowerCase().includes(columnNameSearch);
 | 
			
		||||
        });
 | 
			
		||||
        res = fuzzyMatchField(columnNameSearch, columns, (x: any) => x.columnName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    completeCond = condition.value;
 | 
			
		||||
@@ -534,10 +531,12 @@ const filterColumns = (searchKey: string) => {
 | 
			
		||||
    if (!searchKey) {
 | 
			
		||||
        return columns;
 | 
			
		||||
    }
 | 
			
		||||
    searchKey = searchKey.toLowerCase();
 | 
			
		||||
    return columns.filter((data: any) => {
 | 
			
		||||
        return data.columnName.toLowerCase().includes(searchKey) || data.columnComment.toLowerCase().includes(searchKey);
 | 
			
		||||
    });
 | 
			
		||||
    return fuzzyMatchField(
 | 
			
		||||
        searchKey,
 | 
			
		||||
        columns,
 | 
			
		||||
        (x: any) => x.columnName,
 | 
			
		||||
        (x: any) => x.columnComment
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -622,6 +621,10 @@ const onShowAddDataDialog = async () => {
 | 
			
		||||
    state.addDataDialog.title = `添加'${props.tableName}'表数据`;
 | 
			
		||||
    state.addDataDialog.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
    active: () => dbTableRef.value.active(),
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
        <el-row class="mb5">
 | 
			
		||||
            <el-popover v-model:visible="state.dumpInfo.visible" trigger="click" :width="470" placement="right">
 | 
			
		||||
                <template #reference>
 | 
			
		||||
                    <el-button class="ml5" type="success" size="small">导出</el-button>
 | 
			
		||||
                    <el-button :disabled="state.dumpInfo.tables?.length == 0" class="ml5" type="success" size="small">导出</el-button>
 | 
			
		||||
                </template>
 | 
			
		||||
                <el-form-item label="导出内容: ">
 | 
			
		||||
                    <el-radio-group v-model="dumpInfo.type">
 | 
			
		||||
@@ -131,6 +131,7 @@ import { compatibleMysql, editDbTypes, getDbDialect } from '../../dialect/index'
 | 
			
		||||
import { DbInst } from '../../db';
 | 
			
		||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
 | 
			
		||||
import { format as sqlFormatter } from 'sql-formatter';
 | 
			
		||||
import { fuzzyMatchField } from '@/common/utils/string';
 | 
			
		||||
 | 
			
		||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
 | 
			
		||||
 | 
			
		||||
@@ -219,17 +220,11 @@ const filterTableInfos = computed(() => {
 | 
			
		||||
    if (!tableNameSearch && !tableCommentSearch) {
 | 
			
		||||
        return tables;
 | 
			
		||||
    }
 | 
			
		||||
    return tables.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;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (tableNameSearch) {
 | 
			
		||||
        return fuzzyMatchField(tableNameSearch, tables, (table: any) => table.tableName);
 | 
			
		||||
    }
 | 
			
		||||
    return fuzzyMatchField(tableCommentSearch, tables, (table: any) => table.tableComment);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getTables = async () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import { editor, languages, Position } from 'monaco-editor';
 | 
			
		||||
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
 | 
			
		||||
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
 | 
			
		||||
import { type RemovableRef, useLocalStorage } from '@vueuse/core';
 | 
			
		||||
import { DbGetDbNamesMode } from './enums';
 | 
			
		||||
 | 
			
		||||
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
 | 
			
		||||
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
 | 
			
		||||
@@ -449,8 +450,8 @@ export class DbInst {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符
 | 
			
		||||
        const columnWidth: number = getTextWidth(prop + 'abc') + 23;
 | 
			
		||||
        // 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符、排序图标等
 | 
			
		||||
        const columnWidth: number = getTextWidth(prop + 'abc') + 10;
 | 
			
		||||
        // prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
 | 
			
		||||
        if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
 | 
			
		||||
            return columnWidth;
 | 
			
		||||
@@ -470,7 +471,7 @@ export class DbInst {
 | 
			
		||||
                maxWidthText = nowText;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        const contentWidth: number = getTextWidth(maxWidthText) + 15;
 | 
			
		||||
        const contentWidth: number = getTextWidth(maxWidthText) + 3;
 | 
			
		||||
        const flexWidth: number = contentWidth > columnWidth ? contentWidth : columnWidth;
 | 
			
		||||
        return flexWidth > 500 ? 500 : flexWidth;
 | 
			
		||||
    };
 | 
			
		||||
@@ -503,6 +504,19 @@ export class DbInst {
 | 
			
		||||
            col.columnType = col.dataType;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据数据库配置信息获取对应的库名列表
 | 
			
		||||
     * @param db db配置信息
 | 
			
		||||
     * @returns 库名列表
 | 
			
		||||
     */
 | 
			
		||||
    static async getDbNames(db: any) {
 | 
			
		||||
        if (db.getDatabaseMode == DbGetDbNamesMode.Assign.value) {
 | 
			
		||||
            return db.database.split(' ');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await dbApi.getDbNamesByAc.request({ authCertName: db.authCertName });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -587,6 +601,11 @@ export class TabInfo {
 | 
			
		||||
     */
 | 
			
		||||
    params: any;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 组件ref
 | 
			
		||||
     */
 | 
			
		||||
    componentRef: any;
 | 
			
		||||
 | 
			
		||||
    getNowDbInst() {
 | 
			
		||||
        return DbInst.getInst(this.dbId);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -71,9 +71,9 @@ export enum DataType {
 | 
			
		||||
/** 列数据类型角标 */
 | 
			
		||||
export const ColumnTypeSubscript = {
 | 
			
		||||
    /** 字符串 */
 | 
			
		||||
    string: 'abc',
 | 
			
		||||
    string: 'ab',
 | 
			
		||||
    /** 数字 */
 | 
			
		||||
    number: '123',
 | 
			
		||||
    number: '12',
 | 
			
		||||
    /** 日期 */
 | 
			
		||||
    date: 'icon-clock',
 | 
			
		||||
    /** 时间 */
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,10 @@
 | 
			
		||||
import { EnumValue } from '@/common/Enum';
 | 
			
		||||
 | 
			
		||||
export const DbGetDbNamesMode = {
 | 
			
		||||
    Auto: EnumValue.of(-1, '实时获取').setTagType('warning'),
 | 
			
		||||
    Assign: EnumValue.of(1, '指定库名').setTagType('primary'),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 数据库sql执行类型
 | 
			
		||||
export const DbSqlExecTypeEnum = {
 | 
			
		||||
    Update: EnumValue.of(1, 'UPDATE').setTagColor('#E4F5EB'),
 | 
			
		||||
 
 | 
			
		||||
@@ -34,20 +34,15 @@
 | 
			
		||||
 | 
			
		||||
            <Pane>
 | 
			
		||||
                <div class="machine-terminal-tabs card pd5">
 | 
			
		||||
                    <el-tabs
 | 
			
		||||
                        v-if="state.tabs.size > 0"
 | 
			
		||||
                        type="card"
 | 
			
		||||
                        @tab-remove="onRemoveTab"
 | 
			
		||||
                        @tab-change="onTabChange"
 | 
			
		||||
                        style="width: 100%"
 | 
			
		||||
                        v-model="state.activeTermName"
 | 
			
		||||
                        class="h100"
 | 
			
		||||
                    >
 | 
			
		||||
                    <el-tabs v-if="state.tabs.size > 0" type="card" @tab-remove="onRemoveTab" style="width: 100%" v-model="state.activeTermName" class="h100">
 | 
			
		||||
                        <el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
 | 
			
		||||
                            <template #label>
 | 
			
		||||
                                <el-popconfirm @confirm="handleReconnect(dt, true)" title="确认重新连接?">
 | 
			
		||||
                                    <template #reference>
 | 
			
		||||
                                        <el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'"
 | 
			
		||||
                                        <el-icon
 | 
			
		||||
                                            class="mr5"
 | 
			
		||||
                                            :color="EnumValue.getEnumByValue(TerminalStatusEnum, dt.status)?.extra?.iconColor"
 | 
			
		||||
                                            :title="dt.status == TerminalStatusEnum.Connected.value ? '' : '点击重连'"
 | 
			
		||||
                                            ><Connection />
 | 
			
		||||
                                        </el-icon>
 | 
			
		||||
                                    </template>
 | 
			
		||||
@@ -62,7 +57,7 @@
 | 
			
		||||
                                        <el-descriptions :column="1" size="small">
 | 
			
		||||
                                            <el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item>
 | 
			
		||||
                                            <el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
 | 
			
		||||
                                            <el-descriptions-item label="username"> {{ dt.params?.username }} </el-descriptions-item>
 | 
			
		||||
                                            <el-descriptions-item label="username"> {{ dt.params?.selectAuthCert.username }} </el-descriptions-item>
 | 
			
		||||
                                            <el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
 | 
			
		||||
                                        </el-descriptions>
 | 
			
		||||
                                    </template>
 | 
			
		||||
@@ -165,13 +160,14 @@ import TagTree from '../component/TagTree.vue';
 | 
			
		||||
import { Pane, Splitpanes } from 'splitpanes';
 | 
			
		||||
import { ContextmenuItem } from '@/components/contextmenu/index';
 | 
			
		||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
 | 
			
		||||
import { TerminalStatus } from '@/components/terminal/common';
 | 
			
		||||
import { TerminalStatus, TerminalStatusEnum } from '@/components/terminal/common';
 | 
			
		||||
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
 | 
			
		||||
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
 | 
			
		||||
import ResourceTags from '../component/ResourceTags.vue';
 | 
			
		||||
import { MachineProtocolEnum } from './enums';
 | 
			
		||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
 | 
			
		||||
import { storeToRefs } from 'pinia';
 | 
			
		||||
import EnumValue from '@/common/Enum';
 | 
			
		||||
 | 
			
		||||
// 组件
 | 
			
		||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
 | 
			
		||||
@@ -340,8 +336,13 @@ watch(
 | 
			
		||||
watch(
 | 
			
		||||
    () => state.activeTermName,
 | 
			
		||||
    (newValue, oldValue) => {
 | 
			
		||||
        fitTerminal();
 | 
			
		||||
 | 
			
		||||
        oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
 | 
			
		||||
        terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
 | 
			
		||||
 | 
			
		||||
        const nowTab = state.tabs.get(state.activeTermName);
 | 
			
		||||
        tagTreeRef.value.setCurrentKey(nowTab?.authCert);
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@@ -496,20 +497,27 @@ const onRemoveTab = (targetName: string) => {
 | 
			
		||||
        if (tabName !== targetName) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        state.tabs.delete(targetName);
 | 
			
		||||
        let info = state.tabs.get(targetName);
 | 
			
		||||
        if (info) {
 | 
			
		||||
            terminalRefs[info.key]?.close();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (activeTermName != targetName) {
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
 | 
			
		||||
        const nextTab = tabNames[i + 1] || tabNames[i - 1];
 | 
			
		||||
        if (nextTab) {
 | 
			
		||||
            activeTermName = nextTab;
 | 
			
		||||
        } else {
 | 
			
		||||
            activeTermName = '';
 | 
			
		||||
        }
 | 
			
		||||
        let info = state.tabs.get(targetName);
 | 
			
		||||
        if (info) {
 | 
			
		||||
            terminalRefs[info.key]?.close();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        state.tabs.delete(targetName);
 | 
			
		||||
        state.activeTermName = activeTermName;
 | 
			
		||||
        onTabChange();
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -535,21 +543,13 @@ const onResizeTagTree = () => {
 | 
			
		||||
    fitTerminal();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onTabChange = () => {
 | 
			
		||||
    fitTerminal();
 | 
			
		||||
 | 
			
		||||
    const nowTab = state.tabs.get(state.activeTermName);
 | 
			
		||||
    tagTreeRef.value.setCurrentKey(nowTab?.authCert);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const fitTerminal = () => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        let info = state.tabs.get(state.activeTermName);
 | 
			
		||||
        if (info) {
 | 
			
		||||
            terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal();
 | 
			
		||||
            terminalRefs[info.key]?.focus && terminalRefs[info.key]?.focus();
 | 
			
		||||
        }
 | 
			
		||||
    }, 100);
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleReconnect = (tab: any, force = false) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,8 @@ export const machineApi = {
 | 
			
		||||
    process: Api.newGet('/machines/{id}/process'),
 | 
			
		||||
    // 终止进程
 | 
			
		||||
    killProcess: Api.newDelete('/machines/{id}/process'),
 | 
			
		||||
    users: Api.newGet('/machines/{id}/users'),
 | 
			
		||||
    groups: Api.newGet('/machines/{id}/groups'),
 | 
			
		||||
    testConn: Api.newPost('/machines/test-conn'),
 | 
			
		||||
    // 保存按钮
 | 
			
		||||
    saveMachine: Api.newPost('/machines'),
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-popover placement="right" width="auto" title="机器详情" trigger="click">
 | 
			
		||||
            <template #reference>
 | 
			
		||||
                <el-link @click="getMachineDetail" type="primary">{{ props.code }}</el-link>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <el-descriptions v-loading="state.loading" :column="3" border>
 | 
			
		||||
                <el-descriptions-item :span="1" label="机器id">{{ state.machineDetail.id }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="编号">{{ state.machineDetail.code }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="名称">{{ state.machineDetail.name }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="state.machineDetail.tags" /></el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="IP">{{ state.machineDetail.ip }}</el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="端口">{{ state.machineDetail.port }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="3" label="备注">{{ state.machineDetail.remark }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="1.5" label="SSH隧道">{{ state.machineDetail.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1.5" label="终端回放">{{ state.machineDetail.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="创建时间">{{ formatDate(state.machineDetail.createTime) }} </el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="创建者">{{ state.machineDetail.creator }}</el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                <el-descriptions-item :span="2" label="更新时间">{{ formatDate(state.machineDetail.updateTime) }} </el-descriptions-item>
 | 
			
		||||
                <el-descriptions-item :span="1" label="修改者">{{ state.machineDetail.modifier }}</el-descriptions-item>
 | 
			
		||||
            </el-descriptions>
 | 
			
		||||
        </el-popover>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { reactive } from 'vue';
 | 
			
		||||
import { machineApi } from '../api';
 | 
			
		||||
import { formatDate } from '@/common/utils/format';
 | 
			
		||||
import ResourceTags from '../../component/ResourceTags.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    code: {
 | 
			
		||||
        type: [String],
 | 
			
		||||
        requierd: true,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    loading: false,
 | 
			
		||||
    machineDetail: {} as any,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getMachineDetail = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        state.machineDetail = {};
 | 
			
		||||
        state.loading = true;
 | 
			
		||||
        const res = await machineApi.list.request({
 | 
			
		||||
            code: props.code,
 | 
			
		||||
        });
 | 
			
		||||
        if (res.total == 0) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        state.machineDetail = res.list?.[0];
 | 
			
		||||
    } finally {
 | 
			
		||||
        state.loading = false;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style></style>
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
        <el-dialog
 | 
			
		||||
            :title="title"
 | 
			
		||||
            v-model="dialogVisible"
 | 
			
		||||
            @open="search()"
 | 
			
		||||
            :close-on-click-modal="false"
 | 
			
		||||
            :before-close="cancel"
 | 
			
		||||
            :show-close="true"
 | 
			
		||||
@@ -13,20 +14,13 @@
 | 
			
		||||
                ref="pageTableRef"
 | 
			
		||||
                :page-api="cronJobApi.execList"
 | 
			
		||||
                :lazy="true"
 | 
			
		||||
                :data-handler-fn="parseData"
 | 
			
		||||
                :search-items="searchItems"
 | 
			
		||||
                v-model:query-form="params"
 | 
			
		||||
                :data="state.data.list"
 | 
			
		||||
                :columns="columns"
 | 
			
		||||
            >
 | 
			
		||||
                <template #machineSelect>
 | 
			
		||||
                    <el-select v-model="params.machineId" filterable placeholder="选择机器查询" clearable>
 | 
			
		||||
                        <el-option v-for="ac in machineMap.values()" :key="ac.id" :value="ac.id" :label="ac.ip">
 | 
			
		||||
                            {{ ac.ip }}
 | 
			
		||||
                            <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
                            {{ ac.tagPath }}{{ ac.name }}
 | 
			
		||||
                        </el-option>
 | 
			
		||||
                    </el-select>
 | 
			
		||||
                <template #machineCode="{ data }">
 | 
			
		||||
                    <MachineDetail :code="data.machineCode" />
 | 
			
		||||
                </template>
 | 
			
		||||
            </page-table>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
@@ -34,12 +28,13 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { watch, ref, toRefs, reactive, Ref } from 'vue';
 | 
			
		||||
import { cronJobApi, machineApi } from '../api';
 | 
			
		||||
import { ref, toRefs, reactive, Ref } from 'vue';
 | 
			
		||||
import { cronJobApi } from '../api';
 | 
			
		||||
import PageTable from '@/components/pagetable/PageTable.vue';
 | 
			
		||||
import { TableColumn } from '@/components/pagetable';
 | 
			
		||||
import { CronJobExecStatusEnum } from '../enums';
 | 
			
		||||
import { SearchItem } from '@/components/SearchForm';
 | 
			
		||||
import MachineDetail from '../component/MachineDetail.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    visible: {
 | 
			
		||||
@@ -53,13 +48,10 @@ const props = defineProps({
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['update:visible', 'update:data', 'cancel']);
 | 
			
		||||
 | 
			
		||||
const searchItems = [SearchItem.slot('machineId', '机器', 'machineSelect'), SearchItem.select('status', '状态').withEnum(CronJobExecStatusEnum)];
 | 
			
		||||
const searchItems = [SearchItem.input('machineCode', '机器编号'), SearchItem.select('status', '状态').withEnum(CronJobExecStatusEnum)];
 | 
			
		||||
 | 
			
		||||
const columns = ref([
 | 
			
		||||
    TableColumn.new('machineIp', '机器IP').setMinWidth(120),
 | 
			
		||||
    TableColumn.new('machineName', '机器名称').setMinWidth(100),
 | 
			
		||||
    TableColumn.new('machineCode', '机器编号').isSlot(),
 | 
			
		||||
    TableColumn.new('status', '状态').typeTag(CronJobExecStatusEnum).setMinWidth(70),
 | 
			
		||||
    TableColumn.new('res', '执行结果').setMinWidth(250).canBeautify(),
 | 
			
		||||
    TableColumn.new('execTime', '执行时间').isTime().setMinWidth(150),
 | 
			
		||||
@@ -72,10 +64,10 @@ const state = reactive({
 | 
			
		||||
    tags: [] as any,
 | 
			
		||||
    params: {
 | 
			
		||||
        pageNum: 1,
 | 
			
		||||
        pageSize: 10,
 | 
			
		||||
        pageSize: 8,
 | 
			
		||||
        cronJobId: 0,
 | 
			
		||||
        status: null,
 | 
			
		||||
        machineId: null,
 | 
			
		||||
        machineCode: '',
 | 
			
		||||
    },
 | 
			
		||||
    // 列表数据
 | 
			
		||||
    data: {
 | 
			
		||||
@@ -85,64 +77,17 @@ const state = reactive({
 | 
			
		||||
    machines: [],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const machineMap: Map<number, any> = new Map();
 | 
			
		||||
const { params } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
const { dialogVisible, params } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
watch(props, async (newValue: any) => {
 | 
			
		||||
    state.dialogVisible = newValue.visible;
 | 
			
		||||
    if (!newValue.visible) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const machineIds = await cronJobApi.relateMachineIds.request({
 | 
			
		||||
        cronJobId: props.data?.id,
 | 
			
		||||
    });
 | 
			
		||||
    const res = await machineApi.list.request({
 | 
			
		||||
        ids: machineIds?.join(','),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    res.list?.forEach((x: any) => {
 | 
			
		||||
        machineMap.set(x.id, x);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    state.params.cronJobId = props.data?.id;
 | 
			
		||||
    search();
 | 
			
		||||
});
 | 
			
		||||
const dialogVisible = defineModel<boolean>('visible');
 | 
			
		||||
 | 
			
		||||
const search = async () => {
 | 
			
		||||
    state.params.cronJobId = props.data?.id;
 | 
			
		||||
    pageTableRef.value.search();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const parseData = async (res: any) => {
 | 
			
		||||
    const dataList = res.list;
 | 
			
		||||
    // 填充机器信息
 | 
			
		||||
    for (let x of dataList) {
 | 
			
		||||
        const machineId = x.machineId;
 | 
			
		||||
        let machine = machineMap.get(machineId);
 | 
			
		||||
        // 如果未找到,则可能被移除,则调接口查询机器信息
 | 
			
		||||
        if (!machine) {
 | 
			
		||||
            const machineRes = await machineApi.list.request({ ids: machineId });
 | 
			
		||||
            if (!machineRes.list) {
 | 
			
		||||
                machine = {
 | 
			
		||||
                    id: machineId,
 | 
			
		||||
                    ip: machineId,
 | 
			
		||||
                    name: '该机器已被删除',
 | 
			
		||||
                };
 | 
			
		||||
            } else {
 | 
			
		||||
                machine = machineRes.list[0];
 | 
			
		||||
            }
 | 
			
		||||
            machineMap.set(machineId, machine);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        x.machineIp = machine?.ip;
 | 
			
		||||
        x.machineName = machine?.name;
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
    emit('update:visible', false);
 | 
			
		||||
    dialogVisible.value = false;
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
        initData();
 | 
			
		||||
    }, 500);
 | 
			
		||||
@@ -152,7 +97,7 @@ const initData = () => {
 | 
			
		||||
    state.data.list = [];
 | 
			
		||||
    state.data.total = 0;
 | 
			
		||||
    state.params.pageNum = 1;
 | 
			
		||||
    state.params.machineId = null;
 | 
			
		||||
    state.params.machineCode = '';
 | 
			
		||||
    state.params.status = null;
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,12 +18,12 @@
 | 
			
		||||
                        </el-select>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
                <el-table-column prop="path" label="路径" min-width="150px" show-overflow-tooltip>
 | 
			
		||||
                <el-table-column prop="path" label="路径" min-width="180" show-overflow-tooltip>
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-input v-model="scope.row.path" :disabled="scope.row.id != null" clearable> </el-input>
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
                <el-table-column label="操作" min-wdith="120px">
 | 
			
		||||
                <el-table-column label="操作" min-width="130">
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <el-button v-if="scope.row.id == null" @click="addFiles(scope.row)" type="success" icon="success-filled" plain></el-button>
 | 
			
		||||
                        <el-button v-if="scope.row.id != null" @click="getConf(scope.row)" type="primary" icon="tickets" plain></el-button>
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
            >
 | 
			
		||||
                <el-table-column type="selection" width="30" />
 | 
			
		||||
 | 
			
		||||
                <el-table-column prop="name" label="名称">
 | 
			
		||||
                <el-table-column prop="name" label="名称" min-width="380">
 | 
			
		||||
                    <template #header>
 | 
			
		||||
                        <div class="machine-file-table-header">
 | 
			
		||||
                            <div>
 | 
			
		||||
@@ -171,7 +171,7 @@
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
 | 
			
		||||
                <el-table-column prop="size" label="大小" width="100" sortable>
 | 
			
		||||
                <el-table-column prop="size" label="大小" min-width="90" sortable>
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == '-'"> {{ formatByteSize(scope.row.size) }} </span>
 | 
			
		||||
                        <span style="color: #67c23a; font-weight: bold" v-if="scope.row.type == 'd' && scope.row.dirSize"> {{ scope.row.dirSize }} </span>
 | 
			
		||||
@@ -182,7 +182,11 @@
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
 | 
			
		||||
                <el-table-column prop="mode" label="属性" width="110"> </el-table-column>
 | 
			
		||||
                <el-table-column prop="modTime" label="修改时间" width="165" sortable> </el-table-column>
 | 
			
		||||
                <el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" prop="username" label="用户" min-width="55" show-overflow-tooltip>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
                <el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" prop="groupname" label="组" min-width="55" show-overflow-tooltip>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
                <el-table-column prop="modTime" label="修改时间" width="160" sortable> </el-table-column>
 | 
			
		||||
 | 
			
		||||
                <el-table-column width="100">
 | 
			
		||||
                    <template #header>
 | 
			
		||||
@@ -288,6 +292,8 @@ import MachineFileContent from './MachineFileContent.vue';
 | 
			
		||||
import { getToken } from '@/common/utils/storage';
 | 
			
		||||
import { convertToBytes, formatByteSize } from '@/common/utils/format';
 | 
			
		||||
import { getMachineConfig } from '@/common/sysconfig';
 | 
			
		||||
import { MachineProtocolEnum } from '../enums';
 | 
			
		||||
import { fuzzyMatchField } from '@/common/utils/string';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    machineId: { type: Number },
 | 
			
		||||
@@ -303,6 +309,9 @@ const folderUploadRef: any = ref();
 | 
			
		||||
 | 
			
		||||
const folderType = 'd';
 | 
			
		||||
 | 
			
		||||
const userMap = new Map<number, any>();
 | 
			
		||||
const groupMap = new Map<number, any>();
 | 
			
		||||
 | 
			
		||||
// 路径分隔符
 | 
			
		||||
const pathSep = '/';
 | 
			
		||||
 | 
			
		||||
@@ -343,13 +352,27 @@ const { basePath, nowPath, loading, fileNameFilter, progressNum, uploadProgressS
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
    state.basePath = props.path;
 | 
			
		||||
    const machineId = props.machineId;
 | 
			
		||||
 | 
			
		||||
    if (props.protocol == MachineProtocolEnum.Ssh.value) {
 | 
			
		||||
        machineApi.users.request({ id: machineId }).then((res: any) => {
 | 
			
		||||
            for (let user of res) {
 | 
			
		||||
                userMap.set(user.uid, user);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        machineApi.groups.request({ id: machineId }).then((res: any) => {
 | 
			
		||||
            for (let group of res) {
 | 
			
		||||
                groupMap.set(group.gid, group);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setFiles(props.path);
 | 
			
		||||
    state.machineConfig = await getMachineConfig();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const filterFiles = computed(() =>
 | 
			
		||||
    state.files.filter((data: any) => !state.fileNameFilter || data.name.toLowerCase().includes(state.fileNameFilter.toLowerCase()))
 | 
			
		||||
);
 | 
			
		||||
const filterFiles = computed(() => fuzzyMatchField(state.fileNameFilter, state.files, (file: any) => file.name));
 | 
			
		||||
 | 
			
		||||
const filePathNav = computed(() => {
 | 
			
		||||
    let basePath = state.basePath;
 | 
			
		||||
@@ -517,6 +540,11 @@ const lsFile = async (path: string) => {
 | 
			
		||||
        path,
 | 
			
		||||
    });
 | 
			
		||||
    for (const file of res) {
 | 
			
		||||
        if (props.protocol == MachineProtocolEnum.Ssh.value) {
 | 
			
		||||
            file.username = userMap.get(file.uid)?.uname || file.uid;
 | 
			
		||||
            file.groupname = groupMap.get(file.gid)?.gname || file.gid;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const type = file.type;
 | 
			
		||||
        if (type == folderType) {
 | 
			
		||||
            file.isFolder = true;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
            </el-table-column>
 | 
			
		||||
            <el-table-column prop="codePaths" label="关联机器" min-width="250px" show-overflow-tooltip>
 | 
			
		||||
                <template #default="scope">
 | 
			
		||||
                    <TagCodePath :path="scope.row.tags.map((tag: any) => tag.codePath)" />
 | 
			
		||||
                    <TagCodePath :path="scope.row.tags?.map((tag: any) => tag.codePath)" />
 | 
			
		||||
                </template>
 | 
			
		||||
            </el-table-column>
 | 
			
		||||
            <el-table-column prop="remark" label="备注" show-overflow-tooltip width="120px"> </el-table-column>
 | 
			
		||||
@@ -173,7 +173,7 @@ const openFormDialog = (data: any) => {
 | 
			
		||||
        state.form = { ...DefaultForm };
 | 
			
		||||
    } else {
 | 
			
		||||
        state.form = _.cloneDeep(data);
 | 
			
		||||
        state.form.codePaths = data.tags.map((tag: any) => tag.codePath);
 | 
			
		||||
        state.form.codePaths = data.tags?.map((tag: any) => tag.codePath);
 | 
			
		||||
    }
 | 
			
		||||
    state.dialogVisible = true;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,14 @@
 | 
			
		||||
                <el-button v-auth="'authcert:save'" type="primary" icon="plus" @click="edit(false)">添加</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #resourceCode="{ data }">
 | 
			
		||||
                <SvgIcon
 | 
			
		||||
                    :name="EnumValue.getEnumByValue(TagResourceTypeEnum, data.resourceType)?.extra.icon"
 | 
			
		||||
                    :color="EnumValue.getEnumByValue(TagResourceTypeEnum, data.resourceType)?.extra.iconColor"
 | 
			
		||||
                />
 | 
			
		||||
                {{ data.resourceCode }}
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #action="{ data }">
 | 
			
		||||
                <el-button v-auth="'authcert:save'" @click="edit(data)" type="primary" link>编辑</el-button>
 | 
			
		||||
 | 
			
		||||
@@ -41,6 +49,7 @@ import { SearchItem } from '@/components/SearchForm';
 | 
			
		||||
import { AuthCertCiphertextTypeEnum, AuthCertTypeEnum } from './enums';
 | 
			
		||||
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import ResourceAuthCertEdit from '../component/ResourceAuthCertEdit.vue';
 | 
			
		||||
import EnumValue from '@/common/Enum';
 | 
			
		||||
 | 
			
		||||
const pageTableRef: Ref<any> = ref(null);
 | 
			
		||||
const state = reactive({
 | 
			
		||||
@@ -50,6 +59,7 @@ const state = reactive({
 | 
			
		||||
        name: null,
 | 
			
		||||
    },
 | 
			
		||||
    searchItems: [
 | 
			
		||||
        SearchItem.input('resourceCode', '资源编号'),
 | 
			
		||||
        SearchItem.input('name', '凭证名称'),
 | 
			
		||||
        SearchItem.select('resourceType', '资源类型').withEnum(ResourceTypeEnum),
 | 
			
		||||
        SearchItem.select('type', '凭证类型').withEnum(AuthCertTypeEnum),
 | 
			
		||||
@@ -60,8 +70,7 @@ const state = reactive({
 | 
			
		||||
        TableColumn.new('type', '凭证类型').typeTag(AuthCertTypeEnum),
 | 
			
		||||
        TableColumn.new('username', '用户名'),
 | 
			
		||||
        TableColumn.new('ciphertextType', '密文类型').typeTag(AuthCertCiphertextTypeEnum),
 | 
			
		||||
        TableColumn.new('resourceType', '资源类型').typeTag(TagResourceTypeEnum),
 | 
			
		||||
        TableColumn.new('resourceCode', '资源编号'),
 | 
			
		||||
        TableColumn.new('resourceCode', '资源编号').isSlot().setAddWidth(30),
 | 
			
		||||
        TableColumn.new('remark', '备注'),
 | 
			
		||||
        TableColumn.new('creator', '创建人'),
 | 
			
		||||
        TableColumn.new('createTime', '创建时间').isTime(),
 | 
			
		||||
 
 | 
			
		||||
@@ -156,6 +156,7 @@ import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
 | 
			
		||||
import EnumValue from '@/common/Enum';
 | 
			
		||||
import TagCodePath from '../component/TagCodePath.vue';
 | 
			
		||||
import { isPrefixSubsequence } from '@/common/utils/string';
 | 
			
		||||
 | 
			
		||||
const MachineList = defineAsyncComponent(() => import('../machine/MachineList.vue'));
 | 
			
		||||
const InstanceList = defineAsyncComponent(() => import('../db/InstanceList.vue'));
 | 
			
		||||
@@ -371,8 +372,7 @@ const setNowTabData = () => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterNode = (value: string, data: Tree) => {
 | 
			
		||||
    if (!value) return true;
 | 
			
		||||
    return data.codePath.toLowerCase().includes(value) || data.name.includes(value);
 | 
			
		||||
    return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const search = async () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -100,31 +100,34 @@ const { dvisible, params, form } = toRefs(state);
 | 
			
		||||
 | 
			
		||||
const { isFetching: saveBtnLoading, execute: saveConfigExec } = configApi.save.useApi(form);
 | 
			
		||||
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
    state.dvisible = props.visible;
 | 
			
		||||
    if (!state.dvisible) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
watch(
 | 
			
		||||
    () => props.visible,
 | 
			
		||||
    () => {
 | 
			
		||||
        state.dvisible = props.visible;
 | 
			
		||||
        if (!state.dvisible) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    if (props.data) {
 | 
			
		||||
        state.form = { ...(props.data as any) };
 | 
			
		||||
        if (state.form.params) {
 | 
			
		||||
            state.params = JSON.parse(state.form.params);
 | 
			
		||||
        if (props.data) {
 | 
			
		||||
            state.form = { ...(props.data as any) };
 | 
			
		||||
            if (state.form.params) {
 | 
			
		||||
                state.params = JSON.parse(state.form.params);
 | 
			
		||||
            } else {
 | 
			
		||||
                state.params = [];
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            state.form = { permission: 'all' } as any;
 | 
			
		||||
            state.params = [];
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        state.form = { permission: 'all' } as any;
 | 
			
		||||
        state.params = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (state.form.permission != 'all') {
 | 
			
		||||
        const accounts = state.form.permission.split(',');
 | 
			
		||||
        state.permissionAccount = accounts.slice(0, accounts.length - 1);
 | 
			
		||||
    } else {
 | 
			
		||||
        state.permissionAccount = [];
 | 
			
		||||
        if (state.form.permission != 'all') {
 | 
			
		||||
            const accounts = state.form.permission.split(',');
 | 
			
		||||
            state.permissionAccount = accounts.slice(0, accounts.length - 1);
 | 
			
		||||
        } else {
 | 
			
		||||
            state.permissionAccount = [];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
    // 更新父组件visible prop对应的值为false
 | 
			
		||||
 
 | 
			
		||||
@@ -123,6 +123,7 @@ import { formatDate } from '@/common/utils/format';
 | 
			
		||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
 | 
			
		||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
 | 
			
		||||
import { Splitpanes, Pane } from 'splitpanes';
 | 
			
		||||
import { isPrefixSubsequence } from '@/common/utils/string';
 | 
			
		||||
 | 
			
		||||
const menuTypeValue = ResourceTypeEnum.Menu.value;
 | 
			
		||||
const permissionTypeValue = ResourceTypeEnum.Permission.value;
 | 
			
		||||
@@ -209,10 +210,7 @@ watch(filterResource, (val) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const filterNode = (value: string, data: any) => {
 | 
			
		||||
    if (!value) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    return data.name.includes(value);
 | 
			
		||||
    return !value || isPrefixSubsequence(value, data.name);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const search = async () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -28,12 +28,12 @@ require (
 | 
			
		||||
	github.com/pquerna/otp v1.4.0
 | 
			
		||||
	github.com/redis/go-redis/v9 v9.5.1
 | 
			
		||||
	github.com/robfig/cron/v3 v3.0.1 // 定时任务
 | 
			
		||||
	github.com/sijms/go-ora/v2 v2.8.17
 | 
			
		||||
	github.com/sijms/go-ora/v2 v2.8.19
 | 
			
		||||
	github.com/stretchr/testify v1.9.0
 | 
			
		||||
	github.com/veops/go-ansiterm v0.0.5
 | 
			
		||||
	go.mongodb.org/mongo-driver v1.15.0 // mongo
 | 
			
		||||
	golang.org/x/crypto v0.23.0 // ssh
 | 
			
		||||
	golang.org/x/oauth2 v0.20.0
 | 
			
		||||
	golang.org/x/crypto v0.24.0 // ssh
 | 
			
		||||
	golang.org/x/oauth2 v0.21.0
 | 
			
		||||
	golang.org/x/sync v0.7.0
 | 
			
		||||
	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1
 | 
			
		||||
@@ -94,8 +94,8 @@ require (
 | 
			
		||||
	golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect
 | 
			
		||||
	golang.org/x/image v0.13.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.25.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.20.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.15.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.21.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.16.0 // indirect
 | 
			
		||||
	google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect
 | 
			
		||||
	google.golang.org/grpc v1.52.3 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.34.1 // indirect
 | 
			
		||||
 
 | 
			
		||||
@@ -123,9 +123,8 @@ func (a *AccountLogin) RefreshToken(rc *req.Ctx) {
 | 
			
		||||
	biz.NotEmpty(refreshToken, "refresh_token不能为空")
 | 
			
		||||
 | 
			
		||||
	accountId, username, err := req.ParseToken(refreshToken)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(errorx.PermissionErr)
 | 
			
		||||
	}
 | 
			
		||||
	biz.IsTrueBy(err == nil, errorx.PermissionErr)
 | 
			
		||||
 | 
			
		||||
	token, refreshToken, err := req.CreateToken(accountId, username)
 | 
			
		||||
	biz.ErrIsNil(err)
 | 
			
		||||
	rc.ResData = collx.Kvs("token", token, "refresh_token", refreshToken)
 | 
			
		||||
 
 | 
			
		||||
@@ -57,10 +57,20 @@ func (d *Db) Dbs(rc *req.Ctx) {
 | 
			
		||||
	res, err := d.DbApp.GetPageList(queryCond, page, &dbvos)
 | 
			
		||||
	biz.ErrIsNil(err)
 | 
			
		||||
 | 
			
		||||
	// 填充标签信息
 | 
			
		||||
	d.TagApp.FillTagInfo(tagentity.TagTypeDbName, collx.ArrayMap(dbvos, func(dbvo *vo.DbListVO) tagentity.ITagResource {
 | 
			
		||||
		return dbvo
 | 
			
		||||
	})...)
 | 
			
		||||
	instances, _ := d.InstanceApp.GetByIds(collx.ArrayMap(dbvos, func(i *vo.DbListVO) uint64 {
 | 
			
		||||
		return i.InstanceId
 | 
			
		||||
	}))
 | 
			
		||||
	instancesMap := collx.ArrayToMap(instances, func(i *entity.DbInstance) uint64 {
 | 
			
		||||
		return i.Id
 | 
			
		||||
	})
 | 
			
		||||
	for _, dbvo := range dbvos {
 | 
			
		||||
		di := instancesMap[dbvo.InstanceId]
 | 
			
		||||
		if di != nil {
 | 
			
		||||
			dbvo.InstanceType = di.Type
 | 
			
		||||
			dbvo.Host = di.Host
 | 
			
		||||
			dbvo.Port = di.Port
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rc.ResData = res
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -114,6 +114,12 @@ func (d *Instance) GetDatabaseNames(rc *req.Ctx) {
 | 
			
		||||
	rc.ResData = res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Instance) GetDatabaseNamesByAc(rc *req.Ctx) {
 | 
			
		||||
	res, err := d.InstanceApp.GetDatabasesByAc(rc.PathParam("ac"))
 | 
			
		||||
	biz.ErrIsNil(err)
 | 
			
		||||
	rc.ResData = res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取数据库实例server信息
 | 
			
		||||
func (d *Instance) GetDbServer(rc *req.Ctx) {
 | 
			
		||||
	instanceId := getInstanceId(rc)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,16 @@
 | 
			
		||||
package form
 | 
			
		||||
 | 
			
		||||
import "mayfly-go/internal/db/domain/entity"
 | 
			
		||||
 | 
			
		||||
type DbForm struct {
 | 
			
		||||
	Id             uint64 `json:"id"`
 | 
			
		||||
	Code           string `binding:"required" json:"code"`
 | 
			
		||||
	Name           string `binding:"required" json:"name"`
 | 
			
		||||
	Database       string `json:"database"`
 | 
			
		||||
	Remark         string `json:"remark"`
 | 
			
		||||
	InstanceId     uint64 `binding:"required" json:"instanceId"`
 | 
			
		||||
	AuthCertName   string `json:"authCertName"`
 | 
			
		||||
	FlowProcdefKey string `json:"flowProcdefKey"`
 | 
			
		||||
	Id              uint64                   `json:"id"`
 | 
			
		||||
	Code            string                   `binding:"required" json:"code"`
 | 
			
		||||
	Name            string                   `binding:"required" json:"name"`
 | 
			
		||||
	GetDatabaseMode entity.DbGetDatabaseMode `json:"getDatabaseMode"` // 获取数据库方式
 | 
			
		||||
	Database        string                   `json:"database"`
 | 
			
		||||
	Remark          string                   `json:"remark"`
 | 
			
		||||
	InstanceId      uint64                   `binding:"required" json:"instanceId"`
 | 
			
		||||
	AuthCertName    string                   `json:"authCertName"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DbSqlSaveForm struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,23 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	tagentity "mayfly-go/internal/tag/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DbListVO struct {
 | 
			
		||||
	tagentity.ResourceTags
 | 
			
		||||
	Id              *int64                   `json:"id"`
 | 
			
		||||
	Code            string                   `json:"code"`
 | 
			
		||||
	Name            *string                  `json:"name"`
 | 
			
		||||
	GetDatabaseMode entity.DbGetDatabaseMode `json:"getDatabaseMode"` // 获取数据库方式
 | 
			
		||||
	Database        *string                  `json:"database"`
 | 
			
		||||
	Remark          *string                  `json:"remark"`
 | 
			
		||||
	InstanceId      uint64                   `json:"instanceId"`
 | 
			
		||||
	AuthCertName    string                   `json:"authCertName"`
 | 
			
		||||
 | 
			
		||||
	Id       *int64  `json:"id"`
 | 
			
		||||
	Code     string  `json:"code"`
 | 
			
		||||
	Name     *string `json:"name"`
 | 
			
		||||
	Database *string `json:"database"`
 | 
			
		||||
	Remark   *string `json:"remark"`
 | 
			
		||||
 | 
			
		||||
	InstanceId   *int64  `json:"instanceId"`
 | 
			
		||||
	AuthCertName string  `json:"authCertName"`
 | 
			
		||||
	InstanceName *string `json:"instanceName"`
 | 
			
		||||
	InstanceType *string `json:"type"`
 | 
			
		||||
	Host         string  `json:"host"`
 | 
			
		||||
	Port         int     `json:"port"`
 | 
			
		||||
	InstanceType string `json:"type" gorm:"-"`
 | 
			
		||||
	Host         string `json:"host" gorm:"-"`
 | 
			
		||||
	Port         int    `json:"port" gorm:"-"`
 | 
			
		||||
 | 
			
		||||
	CreateTime *time.Time `json:"createTime"`
 | 
			
		||||
	Creator    *string    `json:"creator"`
 | 
			
		||||
@@ -28,7 +26,3 @@ type DbListVO struct {
 | 
			
		||||
	Modifier   *string    `json:"modifier"`
 | 
			
		||||
	ModifierId *int64     `json:"modifierId"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d DbListVO) GetCode() string {
 | 
			
		||||
	return d.Code
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -194,7 +194,7 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbi.DbConn, error) {
 | 
			
		||||
		di.Id = db.Id
 | 
			
		||||
 | 
			
		||||
		checkDb := di.GetDatabase()
 | 
			
		||||
		if !strings.Contains(" "+db.Database+" ", " "+checkDb+" ") {
 | 
			
		||||
		if db.GetDatabaseMode == entity.DbGetDatabaseModeAssign && !strings.Contains(" "+db.Database+" ", " "+checkDb+" ") {
 | 
			
		||||
			return nil, errorx.NewBiz("未配置数据库【%s】的操作权限", dbName)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -297,7 +297,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
 | 
			
		||||
 | 
			
		||||
		// 生成insert sql,数据在索引前,加速insert
 | 
			
		||||
		if reqParam.DumpData {
 | 
			
		||||
			writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表记录: %s \n-- ----------------------------\n", tableName))
 | 
			
		||||
			writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表数据: %s \n-- ----------------------------\n", tableName))
 | 
			
		||||
 | 
			
		||||
			dumpHelper.BeforeInsert(writer, quoteTableName)
 | 
			
		||||
			// 获取列信息
 | 
			
		||||
 
 | 
			
		||||
@@ -410,7 +410,7 @@ func (app *dataSyncAppImpl) InitCronJob() {
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// 修改执行中状态为待执行
 | 
			
		||||
	_ = app.UpdateByCond(context.TODO(), &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateRunning}, &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateReady})
 | 
			
		||||
	_ = app.UpdateByCond(context.TODO(), &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateReady}, &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateRunning})
 | 
			
		||||
 | 
			
		||||
	// 把所有正常任务添加到定时任务中
 | 
			
		||||
	pageParam := &model.PageParam{
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ package application
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"mayfly-go/internal/common/consts"
 | 
			
		||||
	"mayfly-go/internal/db/application/dto"
 | 
			
		||||
	"mayfly-go/internal/db/dbm"
 | 
			
		||||
@@ -13,14 +12,11 @@ import (
 | 
			
		||||
	tagdto "mayfly-go/internal/tag/application/dto"
 | 
			
		||||
	tagentity "mayfly-go/internal/tag/domain/entity"
 | 
			
		||||
	"mayfly-go/pkg/base"
 | 
			
		||||
	"mayfly-go/pkg/biz"
 | 
			
		||||
	"mayfly-go/pkg/errorx"
 | 
			
		||||
	"mayfly-go/pkg/logx"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"mayfly-go/pkg/utils/collx"
 | 
			
		||||
	"mayfly-go/pkg/utils/structx"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Instance interface {
 | 
			
		||||
@@ -39,6 +35,9 @@ type Instance interface {
 | 
			
		||||
	// GetDatabases 获取数据库实例的所有数据库列表
 | 
			
		||||
	GetDatabases(entity *entity.DbInstance, authCert *tagentity.ResourceAuthCert) ([]string, error)
 | 
			
		||||
 | 
			
		||||
	// GetDatabasesByAc 根据授权凭证名获取所有数据库名称列表
 | 
			
		||||
	GetDatabasesByAc(acName string) ([]string, error)
 | 
			
		||||
 | 
			
		||||
	// ToDbInfo 根据实例与授权凭证返回对应的DbInfo
 | 
			
		||||
	ToDbInfo(instance *entity.DbInstance, authCertName string, database string) (*dbi.DbInfo, error)
 | 
			
		||||
}
 | 
			
		||||
@@ -53,6 +52,8 @@ type instanceAppImpl struct {
 | 
			
		||||
	restoreApp          *DbRestoreApp           `inject:"DbRestoreApp"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ (Instance) = (*instanceAppImpl)(nil)
 | 
			
		||||
 | 
			
		||||
// 注入DbInstanceRepo
 | 
			
		||||
func (app *instanceAppImpl) InjectDbInstanceRepo(repo repository.Instance) {
 | 
			
		||||
	app.Repo = repo
 | 
			
		||||
@@ -118,7 +119,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *dto.Sa
 | 
			
		||||
		return instanceEntity.Id, app.Tx(ctx, func(ctx context.Context) error {
 | 
			
		||||
			return app.Insert(ctx, instanceEntity)
 | 
			
		||||
		}, func(ctx context.Context) error {
 | 
			
		||||
			return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
 | 
			
		||||
			return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
 | 
			
		||||
				ResourceCode: instanceEntity.Code,
 | 
			
		||||
				ResourceType: tagentity.TagType(resourceType),
 | 
			
		||||
				AuthCerts:    authCerts,
 | 
			
		||||
@@ -147,7 +148,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *dto.Sa
 | 
			
		||||
	return oldInstance.Id, app.Tx(ctx, func(ctx context.Context) error {
 | 
			
		||||
		return app.UpdateById(ctx, instanceEntity)
 | 
			
		||||
	}, func(ctx context.Context) error {
 | 
			
		||||
		return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
 | 
			
		||||
		return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
 | 
			
		||||
			ResourceCode: oldInstance.Code,
 | 
			
		||||
			ResourceType: tagentity.TagType(resourceType),
 | 
			
		||||
			AuthCerts:    authCerts,
 | 
			
		||||
@@ -166,7 +167,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *dto.Sa
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error {
 | 
			
		||||
	instance, err := app.GetById(instanceId, "name")
 | 
			
		||||
	instance, err := app.GetById(instanceId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorx.NewBiz("获取数据库实例错误,数据库实例ID为: %d", instance.Id)
 | 
			
		||||
	}
 | 
			
		||||
@@ -175,26 +176,16 @@ func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error
 | 
			
		||||
		DbInstanceId: instanceId,
 | 
			
		||||
	}
 | 
			
		||||
	err = app.restoreApp.restoreRepo.GetByCond(restore)
 | 
			
		||||
	switch {
 | 
			
		||||
	case err == nil:
 | 
			
		||||
		biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name)
 | 
			
		||||
	case errors.Is(err, gorm.ErrRecordNotFound):
 | 
			
		||||
		break
 | 
			
		||||
	default:
 | 
			
		||||
		biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorx.NewBiz("不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	backup := &entity.DbBackup{
 | 
			
		||||
		DbInstanceId: instanceId,
 | 
			
		||||
	}
 | 
			
		||||
	err = app.backupApp.backupRepo.GetByCond(backup)
 | 
			
		||||
	switch {
 | 
			
		||||
	case err == nil:
 | 
			
		||||
		biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name)
 | 
			
		||||
	case errors.Is(err, gorm.ErrRecordNotFound):
 | 
			
		||||
		break
 | 
			
		||||
	default:
 | 
			
		||||
		biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorx.NewBiz("不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbs, _ := app.dbApp.ListByCond(&entity.Db{
 | 
			
		||||
@@ -205,7 +196,7 @@ func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error
 | 
			
		||||
		return app.DeleteById(ctx, instanceId)
 | 
			
		||||
	}, func(ctx context.Context) error {
 | 
			
		||||
		// 删除该实例关联的授权凭证信息
 | 
			
		||||
		return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
 | 
			
		||||
		return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
 | 
			
		||||
			ResourceCode: instance.Code,
 | 
			
		||||
			ResourceType: tagentity.TagType(consts.ResourceTypeDb),
 | 
			
		||||
		})
 | 
			
		||||
@@ -239,16 +230,22 @@ func (app *instanceAppImpl) GetDatabases(ed *entity.DbInstance, authCert *tagent
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ed.Network = ed.GetNetwork()
 | 
			
		||||
	dbi := app.toDbInfoByAc(ed, authCert, "")
 | 
			
		||||
	return app.getDatabases(ed, authCert)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	dbConn, err := dbm.Conn(dbi)
 | 
			
		||||
func (app *instanceAppImpl) GetDatabasesByAc(acName string) ([]string, error) {
 | 
			
		||||
	ac, err := app.resourceAuthCertApp.GetAuthCert(acName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
		return nil, errorx.NewBiz("该授权凭证不存在")
 | 
			
		||||
	}
 | 
			
		||||
	defer dbConn.Close()
 | 
			
		||||
 | 
			
		||||
	return dbConn.GetMetaData().GetDbNames()
 | 
			
		||||
	instance := &entity.DbInstance{Code: ac.ResourceCode}
 | 
			
		||||
	err = app.GetByCond(instance)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, errorx.NewBiz("不存在该授权凭证对应的数据库实例信息")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return app.getDatabases(instance, ac)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *instanceAppImpl) ToDbInfo(instance *entity.DbInstance, authCertName string, database string) (*dbi.DbInfo, error) {
 | 
			
		||||
@@ -260,6 +257,19 @@ func (app *instanceAppImpl) ToDbInfo(instance *entity.DbInstance, authCertName s
 | 
			
		||||
	return app.toDbInfoByAc(instance, ac, database), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *instanceAppImpl) getDatabases(instance *entity.DbInstance, ac *tagentity.ResourceAuthCert) ([]string, error) {
 | 
			
		||||
	instance.Network = instance.GetNetwork()
 | 
			
		||||
	dbi := app.toDbInfoByAc(instance, ac, "")
 | 
			
		||||
 | 
			
		||||
	dbConn, err := dbm.Conn(dbi)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer dbConn.Close()
 | 
			
		||||
 | 
			
		||||
	return dbConn.GetMetaData().GetDbNames()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *instanceAppImpl) toDbInfoByAc(instance *entity.DbInstance, ac *tagentity.ResourceAuthCert, database string) *dbi.DbInfo {
 | 
			
		||||
	di := new(dbi.DbInfo)
 | 
			
		||||
	di.InstanceId = instance.Id
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package application
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mayfly-go/internal/db/dbm/dbi"
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
@@ -87,16 +88,20 @@ func (app *dbTransferAppImpl) CreateLog(ctx context.Context, taskId uint64) (uin
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint64) {
 | 
			
		||||
	defer app.logApp.Flush(logId, true)
 | 
			
		||||
 | 
			
		||||
	task, err := app.GetById(taskId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logx.Errorf("创建DBMS-执行数据迁移日志失败:%v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if app.IsRunning(taskId) {
 | 
			
		||||
		logx.Warnf("[%d]该任务正在运行中...", taskId)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	start := time.Now()
 | 
			
		||||
 | 
			
		||||
	defer app.logApp.Flush(logId, true)
 | 
			
		||||
 | 
			
		||||
	// 修改状态与关联日志id
 | 
			
		||||
	task.LogId = logId
 | 
			
		||||
	task.RunningState = entity.DbTransferTaskRunStateRunning
 | 
			
		||||
@@ -322,6 +327,8 @@ func (app *dbTransferAppImpl) transfer2Target(taskId uint64, targetConn *dbi.DbC
 | 
			
		||||
		columnNames = append(columnNames, targetMeta.QuoteIdentifier(col.ColumnName))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dataHelper := targetMeta.GetDataHelper()
 | 
			
		||||
 | 
			
		||||
	// 从目标库数据中取出源库字段对应的值
 | 
			
		||||
	values := make([][]any, 0)
 | 
			
		||||
	for _, record := range result {
 | 
			
		||||
@@ -338,6 +345,14 @@ func (app *dbTransferAppImpl) transfer2Target(taskId uint64, targetConn *dbi.DbC
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if dataHelper.GetDataType(string(tc.DataType)) == dbi.DataTypeBlob {
 | 
			
		||||
				decodeBytes, err := hex.DecodeString(val.(string))
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					val = decodeBytes
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			rawValue = append(rawValue, val)
 | 
			
		||||
		}
 | 
			
		||||
		values = append(values, rawValue)
 | 
			
		||||
 
 | 
			
		||||
@@ -214,6 +214,9 @@ func valueConvert(data []byte, colType *sql.ColumnType) any {
 | 
			
		||||
	if strings.Contains(colDatabaseTypeName, "bit") {
 | 
			
		||||
		return data[0]
 | 
			
		||||
	}
 | 
			
		||||
	if colDatabaseTypeName == "blob" {
 | 
			
		||||
		return fmt.Sprintf("%x", data)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 这里把[]byte数据转成string
 | 
			
		||||
	stringV := string(data)
 | 
			
		||||
 
 | 
			
		||||
@@ -84,6 +84,13 @@ type Column struct {
 | 
			
		||||
 | 
			
		||||
// 拼接数据类型与长度等。如varchar(2000),decimal(20,2)
 | 
			
		||||
func (c *Column) GetColumnType() string {
 | 
			
		||||
	// 哪些mysql数据类型不需要添加字段长度
 | 
			
		||||
	if collx.ArrayAnyMatches([]string{"int", "blob", "float", "double", "date", "year", "json"}, string(c.DataType)) {
 | 
			
		||||
		return string(c.DataType)
 | 
			
		||||
	}
 | 
			
		||||
	if c.DataType == "timestamp" {
 | 
			
		||||
		return "timestamp(6)"
 | 
			
		||||
	}
 | 
			
		||||
	if c.CharMaxLength > 0 {
 | 
			
		||||
		return fmt.Sprintf("%s(%d)", c.DataType, c.CharMaxLength)
 | 
			
		||||
	}
 | 
			
		||||
@@ -124,6 +131,7 @@ const (
 | 
			
		||||
	CommonTypeVarbinary  ColumnDataType = "varbinary"
 | 
			
		||||
 | 
			
		||||
	CommonTypeInt      ColumnDataType = "int"
 | 
			
		||||
	CommonTypeBit      ColumnDataType = "bit"
 | 
			
		||||
	CommonTypeSmallint ColumnDataType = "smallint"
 | 
			
		||||
	CommonTypeTinyint  ColumnDataType = "tinyint"
 | 
			
		||||
	CommonTypeNumber   ColumnDataType = "number"
 | 
			
		||||
@@ -146,6 +154,7 @@ const (
 | 
			
		||||
	DataTypeDate     DataType = "date"
 | 
			
		||||
	DataTypeTime     DataType = "time"
 | 
			
		||||
	DataTypeDateTime DataType = "datetime"
 | 
			
		||||
	DataTypeBlob     DataType = "blob"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 列数据处理帮助方法
 | 
			
		||||
 
 | 
			
		||||
@@ -42,13 +42,13 @@ SELECT a.indexname                                                         AS "i
 | 
			
		||||
       indexdef                                                          AS "indexDef",
 | 
			
		||||
       c.attname                                                         AS "columnName",
 | 
			
		||||
       c.attnum                                                          AS "seqInIndex",
 | 
			
		||||
       case when a.indexname like '%_pkey' then 1 else 0 end             AS "isPrimaryKey"
 | 
			
		||||
       case when a.indexname like '%%_pkey' then 1 else 0 end             AS "isPrimaryKey"
 | 
			
		||||
FROM pg_indexes a
 | 
			
		||||
         join pg_class b on a.indexname = b.relname
 | 
			
		||||
         join pg_attribute c on b.oid = c.attrelid
 | 
			
		||||
WHERE a.schemaname = (select current_schema())
 | 
			
		||||
  AND a.tablename = '%s'
 | 
			
		||||
  AND a.indexname not like '%_pkey'
 | 
			
		||||
  AND a.indexname not like '%%_pkey'
 | 
			
		||||
---------------------------------------
 | 
			
		||||
--PGSQL_COLUMN_MA 表列信息
 | 
			
		||||
SELECT a.table_name                                                                            AS "tableName",
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,8 @@ var (
 | 
			
		||||
	// 时间类型
 | 
			
		||||
	timeRegexp = regexp.MustCompile(`(?i)time`)
 | 
			
		||||
 | 
			
		||||
	blobRegexp = regexp.MustCompile(`(?i)blob`)
 | 
			
		||||
 | 
			
		||||
	//  mysql数据类型 映射 公共数据类型
 | 
			
		||||
	commonColumnTypeMap = map[string]dbi.ColumnDataType{
 | 
			
		||||
		"bigint":     dbi.CommonTypeBigint,
 | 
			
		||||
@@ -37,6 +39,7 @@ var (
 | 
			
		||||
		"longtext":   dbi.CommonTypeLongtext,
 | 
			
		||||
		"mediumblob": dbi.CommonTypeBlob,
 | 
			
		||||
		"mediumtext": dbi.CommonTypeText,
 | 
			
		||||
		"bit":        dbi.CommonTypeBit,
 | 
			
		||||
		"set":        dbi.CommonTypeVarchar,
 | 
			
		||||
		"smallint":   dbi.CommonTypeSmallint,
 | 
			
		||||
		"text":       dbi.CommonTypeText,
 | 
			
		||||
@@ -60,6 +63,7 @@ var (
 | 
			
		||||
		dbi.CommonTypeMediumtext: "text",
 | 
			
		||||
		dbi.CommonTypeVarbinary:  "varbinary",
 | 
			
		||||
		dbi.CommonTypeInt:        "int",
 | 
			
		||||
		dbi.CommonTypeBit:        "bit",
 | 
			
		||||
		dbi.CommonTypeSmallint:   "smallint",
 | 
			
		||||
		dbi.CommonTypeTinyint:    "tinyint",
 | 
			
		||||
		dbi.CommonTypeNumber:     "decimal",
 | 
			
		||||
@@ -92,6 +96,10 @@ func (dc *DataHelper) GetDataType(dbColumnType string) dbi.DataType {
 | 
			
		||||
	if timeRegexp.MatchString(dbColumnType) {
 | 
			
		||||
		return dbi.DataTypeTime
 | 
			
		||||
	}
 | 
			
		||||
	// blob类型
 | 
			
		||||
	if blobRegexp.MatchString(dbColumnType) {
 | 
			
		||||
		return dbi.DataTypeBlob
 | 
			
		||||
	}
 | 
			
		||||
	return dbi.DataTypeString
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -157,6 +165,8 @@ func (dc *DataHelper) WrapValue(dbColumnValue any, dataType dbi.DataType) string
 | 
			
		||||
	case dbi.DataTypeDate, dbi.DataTypeDateTime, dbi.DataTypeTime:
 | 
			
		||||
		// mysql时间类型无需格式化
 | 
			
		||||
		return fmt.Sprintf("'%s'", dbColumnValue)
 | 
			
		||||
	case dbi.DataTypeBlob:
 | 
			
		||||
		return fmt.Sprintf("unhex('%s')", dbColumnValue)
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("'%s'", dbColumnValue)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,10 +7,18 @@ import (
 | 
			
		||||
type Db struct {
 | 
			
		||||
	model.Model
 | 
			
		||||
 | 
			
		||||
	Code         string `orm:"column(code)" json:"code"`
 | 
			
		||||
	Name         string `orm:"column(name)" json:"name"`
 | 
			
		||||
	Database     string `orm:"column(database)" json:"database"`
 | 
			
		||||
	Remark       string `json:"remark"`
 | 
			
		||||
	InstanceId   uint64
 | 
			
		||||
	AuthCertName string `json:"authCertName"`
 | 
			
		||||
	Code            string            `orm:"column(code)" json:"code"`
 | 
			
		||||
	Name            string            `orm:"column(name)" json:"name"`
 | 
			
		||||
	GetDatabaseMode DbGetDatabaseMode `json:"getDatabaseMode"` // 获取数据库方式
 | 
			
		||||
	Database        string            `orm:"column(database)" json:"database"`
 | 
			
		||||
	Remark          string            `json:"remark"`
 | 
			
		||||
	InstanceId      uint64
 | 
			
		||||
	AuthCertName    string `json:"authCertName"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DbGetDatabaseMode int8
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	DbGetDatabaseModeAuto   DbGetDatabaseMode = -1 // 自动获取(根据凭证获取对应所有库名)
 | 
			
		||||
	DbGetDatabaseModeAssign DbGetDatabaseMode = 1  // 指定库名
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ type InstanceQuery struct {
 | 
			
		||||
	Name    string `json:"name" form:"name"`
 | 
			
		||||
	Code    string `json:"code" form:"code"`
 | 
			
		||||
	Host    string `json:"host" form:"host"`
 | 
			
		||||
	TagPath string `json:"host" form:"tagPath"`
 | 
			
		||||
	TagPath string `json:"tagPath" form:"tagPath"`
 | 
			
		||||
	Codes   []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -27,16 +27,10 @@ type DbTransferLogQuery struct {
 | 
			
		||||
 | 
			
		||||
// 数据库查询实体,不与数据库表字段一一对应
 | 
			
		||||
type DbQuery struct {
 | 
			
		||||
	Id       uint64 `form:"id"`
 | 
			
		||||
	Code     string `json:"code" form:"code"`
 | 
			
		||||
	Name     string `orm:"column(name)" json:"name"`
 | 
			
		||||
	Database string `orm:"column(database)" json:"database"`
 | 
			
		||||
	Remark   string `json:"remark"`
 | 
			
		||||
 | 
			
		||||
	Codes   []string
 | 
			
		||||
	TagIds  []uint64 `orm:"column(tag_id)"`
 | 
			
		||||
	TagPath string   `form:"tagPath"`
 | 
			
		||||
 | 
			
		||||
	Id         uint64 `form:"id"`
 | 
			
		||||
	TagPath    string `form:"tagPath"`
 | 
			
		||||
	Code       string `json:"code" form:"code"`
 | 
			
		||||
	Codes      []string
 | 
			
		||||
	InstanceId uint64 `form:"instanceId"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ import (
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/db/domain/repository"
 | 
			
		||||
	"mayfly-go/pkg/base"
 | 
			
		||||
	"mayfly-go/pkg/gormx"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -18,16 +17,6 @@ func newDbRepo() repository.Db {
 | 
			
		||||
 | 
			
		||||
// 分页获取数据库信息列表
 | 
			
		||||
func (d *dbRepoImpl) GetDbList(condition *entity.DbQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
 | 
			
		||||
	qd := gormx.NewQueryWithTableName("t_db db").Joins("JOIN t_db_instance inst ON db.instance_id = inst.id").
 | 
			
		||||
		WithCond(model.NewCond().Columns("db.*, inst.name instance_name, inst.type instance_type, inst.host, inst.port ").
 | 
			
		||||
			Eq("db.instance_id", condition.InstanceId).
 | 
			
		||||
			Eq("db.id", condition.Id).
 | 
			
		||||
			Like("db.database", condition.Database).
 | 
			
		||||
			Eq("db.code", condition.Code).
 | 
			
		||||
			In("db.code", condition.Codes).
 | 
			
		||||
			Eq0("db."+model.DeletedColumn, model.ModelUndeleted).
 | 
			
		||||
			Eq0("inst."+model.DeletedColumn, model.ModelUndeleted),
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
	return gormx.PageQuery(qd, pageParam, toEntity)
 | 
			
		||||
	pd := model.NewCond().Eq("instance_id", condition.InstanceId).In("code", condition.Codes)
 | 
			
		||||
	return d.PageByCondToAny(pd, pageParam, toEntity)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,9 @@ func InitInstanceRouter(router *gin.RouterGroup) {
 | 
			
		||||
		// 获取数据库实例的所有数据库名
 | 
			
		||||
		req.NewPost("/databases", d.GetDatabaseNames),
 | 
			
		||||
 | 
			
		||||
		// 根据授权凭证名获取其所有库名
 | 
			
		||||
		req.NewGet("/databases/:ac", d.GetDatabaseNamesByAc),
 | 
			
		||||
 | 
			
		||||
		req.NewGet(":instanceId/server-info", d.GetDbServer),
 | 
			
		||||
 | 
			
		||||
		req.NewDelete(":instanceId", d.DeleteInstance).Log(req.NewLogSave("db-删除数据库实例")),
 | 
			
		||||
 
 | 
			
		||||
@@ -173,6 +173,22 @@ func (m *Machine) KillProcess(rc *req.Ctx) {
 | 
			
		||||
	biz.ErrIsNil(err, "终止进程失败: %s", res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Machine) GetUsers(rc *req.Ctx) {
 | 
			
		||||
	cli, err := m.MachineApp.GetCli(GetMachineId(rc))
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
 | 
			
		||||
	res, err := cli.GetUsers()
 | 
			
		||||
	biz.ErrIsNil(err)
 | 
			
		||||
	rc.ResData = res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Machine) GetGroups(rc *req.Ctx) {
 | 
			
		||||
	cli, err := m.MachineApp.GetCli(GetMachineId(rc))
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
 | 
			
		||||
	res, err := cli.GetGroups()
 | 
			
		||||
	biz.ErrIsNil(err)
 | 
			
		||||
	rc.ResData = res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Machine) WsSSH(g *gin.Context) {
 | 
			
		||||
	wsConn, err := ws.Upgrader.Upgrade(g.Writer, g.Request, nil)
 | 
			
		||||
	defer func() {
 | 
			
		||||
@@ -261,7 +277,7 @@ func (m *Machine) WsGuacamole(g *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = mi.IfUseSshTunnelChangeIpPort()
 | 
			
		||||
	err = mi.IfUseSshTunnelChangeIpPort(true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,13 +8,14 @@ import (
 | 
			
		||||
	"mayfly-go/internal/machine/domain/entity"
 | 
			
		||||
	tagapp "mayfly-go/internal/tag/application"
 | 
			
		||||
	tagentity "mayfly-go/internal/tag/domain/entity"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"mayfly-go/pkg/biz"
 | 
			
		||||
	"mayfly-go/pkg/req"
 | 
			
		||||
	"mayfly-go/pkg/scheduler"
 | 
			
		||||
	"mayfly-go/pkg/utils/collx"
 | 
			
		||||
 | 
			
		||||
	"github.com/may-fly/cast"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MachineCronJob struct {
 | 
			
		||||
@@ -58,9 +59,7 @@ func (m *MachineCronJob) Delete(rc *req.Ctx) {
 | 
			
		||||
	ids := strings.Split(idsStr, ",")
 | 
			
		||||
 | 
			
		||||
	for _, v := range ids {
 | 
			
		||||
		value, err := strconv.Atoi(v)
 | 
			
		||||
		biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s")
 | 
			
		||||
		m.MachineCronJobApp.Delete(rc.MetaCtx, uint64(value))
 | 
			
		||||
		m.MachineCronJobApp.Delete(rc.MetaCtx, cast.ToUint64(v))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,9 +28,11 @@ import (
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"github.com/may-fly/cast"
 | 
			
		||||
	"github.com/pkg/sftp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MachineFile struct {
 | 
			
		||||
	MachineApp     application.Machine     `inject:""`
 | 
			
		||||
	MachineFileApp application.MachineFile `inject:""`
 | 
			
		||||
	MsgApp         msgapp.Msg              `inject:""`
 | 
			
		||||
}
 | 
			
		||||
@@ -159,15 +161,21 @@ func (m *MachineFile) GetDirEntry(rc *req.Ctx) {
 | 
			
		||||
			path = readPath + name
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fisVO = append(fisVO, vo.MachineFileInfo{
 | 
			
		||||
		mfi := vo.MachineFileInfo{
 | 
			
		||||
			Name:    fi.Name(),
 | 
			
		||||
			Size:    fi.Size(),
 | 
			
		||||
			Path:    path,
 | 
			
		||||
			Type:    getFileType(fi.Mode()),
 | 
			
		||||
			Mode:    fi.Mode().String(),
 | 
			
		||||
			ModTime: timex.DefaultFormat(fi.ModTime()),
 | 
			
		||||
		})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if sftpFs, ok := fi.Sys().(*sftp.FileStat); ok {
 | 
			
		||||
			mfi.UID = sftpFs.UID
 | 
			
		||||
			mfi.GID = sftpFs.GID
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fisVO = append(fisVO, mfi)
 | 
			
		||||
	}
 | 
			
		||||
	sort.Sort(vo.MachineFileInfos(fisVO))
 | 
			
		||||
	rc.ResData = fisVO
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,9 @@ import (
 | 
			
		||||
	"mayfly-go/pkg/utils/collx"
 | 
			
		||||
	"mayfly-go/pkg/utils/jsonx"
 | 
			
		||||
	"mayfly-go/pkg/utils/stringx"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/may-fly/cast"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MachineScript struct {
 | 
			
		||||
@@ -42,9 +43,7 @@ func (m *MachineScript) DeleteMachineScript(rc *req.Ctx) {
 | 
			
		||||
	ids := strings.Split(idsStr, ",")
 | 
			
		||||
 | 
			
		||||
	for _, v := range ids {
 | 
			
		||||
		value, err := strconv.Atoi(v)
 | 
			
		||||
		biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s")
 | 
			
		||||
		m.MachineScriptApp.Delete(rc.MetaCtx, uint64(value))
 | 
			
		||||
		m.MachineScriptApp.Delete(rc.MetaCtx, cast.ToUint64(v))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -78,6 +78,9 @@ type MachineFileInfo struct {
 | 
			
		||||
	Type    string `json:"type"`
 | 
			
		||||
	Mode    string `json:"mode"`
 | 
			
		||||
	ModTime string `json:"modTime"`
 | 
			
		||||
 | 
			
		||||
	UID uint32 `json:"uid"`
 | 
			
		||||
	GID uint32 `json:"gid"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MachineFileInfos []MachineFileInfo
 | 
			
		||||
 
 | 
			
		||||
@@ -108,7 +108,7 @@ func (m *machineAppImpl) SaveMachine(ctx context.Context, param *dto.SaveMachine
 | 
			
		||||
		return m.Tx(ctx, func(ctx context.Context) error {
 | 
			
		||||
			return m.Insert(ctx, me)
 | 
			
		||||
		}, func(ctx context.Context) error {
 | 
			
		||||
			return m.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
 | 
			
		||||
			return m.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
 | 
			
		||||
				ResourceCode: me.Code,
 | 
			
		||||
				ResourceType: resourceType,
 | 
			
		||||
				AuthCerts:    authCerts,
 | 
			
		||||
@@ -138,7 +138,7 @@ func (m *machineAppImpl) SaveMachine(ctx context.Context, param *dto.SaveMachine
 | 
			
		||||
	return m.Tx(ctx, func(ctx context.Context) error {
 | 
			
		||||
		return m.UpdateById(ctx, me)
 | 
			
		||||
	}, func(ctx context.Context) error {
 | 
			
		||||
		return m.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
 | 
			
		||||
		return m.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
 | 
			
		||||
			ResourceCode: oldMachine.Code,
 | 
			
		||||
			ResourceType: resourceType,
 | 
			
		||||
			AuthCerts:    authCerts,
 | 
			
		||||
@@ -219,7 +219,7 @@ func (m *machineAppImpl) Delete(ctx context.Context, id uint64) error {
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
		}, func(ctx context.Context) error {
 | 
			
		||||
			return m.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
 | 
			
		||||
			return m.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
 | 
			
		||||
				ResourceCode: machine.Code,
 | 
			
		||||
				ResourceType: resourceType,
 | 
			
		||||
			})
 | 
			
		||||
@@ -334,6 +334,7 @@ func (m *machineAppImpl) getMachineAndAuthCert(machineId uint64) (*entity.Machin
 | 
			
		||||
func (m *machineAppImpl) toMi(me *entity.Machine, authCert *tagentity.ResourceAuthCert) (*mcm.MachineInfo, error) {
 | 
			
		||||
	mi := new(mcm.MachineInfo)
 | 
			
		||||
	mi.Id = me.Id
 | 
			
		||||
	mi.Code = me.Code
 | 
			
		||||
	mi.Name = me.Name
 | 
			
		||||
	mi.Ip = me.Ip
 | 
			
		||||
	mi.Port = me.Port
 | 
			
		||||
 
 | 
			
		||||
@@ -8,13 +8,11 @@ import (
 | 
			
		||||
	tagapp "mayfly-go/internal/tag/application"
 | 
			
		||||
	tagentity "mayfly-go/internal/tag/domain/entity"
 | 
			
		||||
	"mayfly-go/pkg/base"
 | 
			
		||||
	"mayfly-go/pkg/biz"
 | 
			
		||||
	"mayfly-go/pkg/errorx"
 | 
			
		||||
	"mayfly-go/pkg/logx"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"mayfly-go/pkg/rediscli"
 | 
			
		||||
	"mayfly-go/pkg/scheduler"
 | 
			
		||||
	"mayfly-go/pkg/utils/anyx"
 | 
			
		||||
	"mayfly-go/pkg/utils/collx"
 | 
			
		||||
	"mayfly-go/pkg/utils/stringx"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -178,43 +176,35 @@ func (m *machineCronJobAppImpl) addCronJob(mcj *entity.MachineCronJob) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *machineCronJobAppImpl) runCronJob0(mid uint64, cronJob *entity.MachineCronJob) {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := recover(); err != nil {
 | 
			
		||||
			res := anyx.ToString(err)
 | 
			
		||||
			m.machineCronJobExecRepo.Insert(context.TODO(), &entity.MachineCronJobExec{
 | 
			
		||||
				MachineId: mid,
 | 
			
		||||
				CronJobId: cronJob.Id,
 | 
			
		||||
				ExecTime:  time.Now(),
 | 
			
		||||
				Status:    entity.MachineCronJobExecStatusError,
 | 
			
		||||
				Res:       res,
 | 
			
		||||
			})
 | 
			
		||||
			logx.Errorf("机器:[%d]执行[%s]计划任务失败: %s", mid, cronJob.Name, res)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	execRes := &entity.MachineCronJobExec{
 | 
			
		||||
		CronJobId: cronJob.Id,
 | 
			
		||||
		ExecTime:  time.Now(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	machineCli, err := m.machineApp.GetCli(uint64(mid))
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
 | 
			
		||||
	res, err := machineCli.Run(cronJob.Script)
 | 
			
		||||
	res := ""
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if res == "" {
 | 
			
		||||
			res = err.Error()
 | 
			
		||||
		}
 | 
			
		||||
		logx.Errorf("机器:[%d]执行[%s]计划任务失败: %s", mid, cronJob.Name, res)
 | 
			
		||||
		machine, _ := m.machineApp.GetById(mid)
 | 
			
		||||
		execRes.MachineCode = machine.Code
 | 
			
		||||
	} else {
 | 
			
		||||
		logx.Debugf("机器:[%d]执行[%s]计划任务成功, 执行结果: %s", mid, cronJob.Name, res)
 | 
			
		||||
		execRes.MachineCode = machineCli.Info.Code
 | 
			
		||||
		res, err = machineCli.Run(cronJob.Script)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if res == "" {
 | 
			
		||||
				res = err.Error()
 | 
			
		||||
			}
 | 
			
		||||
			logx.Errorf("机器:[%d]执行[%s]计划任务失败: %s", mid, cronJob.Name, res)
 | 
			
		||||
		} else {
 | 
			
		||||
			logx.Debugf("机器:[%d]执行[%s]计划任务成功, 执行结果: %s", mid, cronJob.Name, res)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	execRes.Res = res
 | 
			
		||||
 | 
			
		||||
	if cronJob.SaveExecResType == entity.SaveExecResTypeNo ||
 | 
			
		||||
		(cronJob.SaveExecResType == entity.SaveExecResTypeOnError && err == nil) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	execRes := &entity.MachineCronJobExec{
 | 
			
		||||
		MachineId: mid,
 | 
			
		||||
		CronJobId: cronJob.Id,
 | 
			
		||||
		ExecTime:  time.Now(),
 | 
			
		||||
		Res:       res,
 | 
			
		||||
	}
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		execRes.Status = entity.MachineCronJobExecStatusSuccess
 | 
			
		||||
	} else {
 | 
			
		||||
 
 | 
			
		||||
@@ -222,8 +222,7 @@ func (m *machineFileAppImpl) MkDir(ctx context.Context, opParam *dto.MachineFile
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sftpCli.MkdirAll(path)
 | 
			
		||||
	return mi, err
 | 
			
		||||
	return mi, sftpCli.MkdirAll(path)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *machineFileAppImpl) CreateFile(ctx context.Context, opParam *dto.MachineFileOp) (*mcm.MachineInfo, error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -63,8 +63,8 @@ func (m *machineTermOpAppImpl) TermConn(ctx context.Context, cli *mcm.Cli, wsCon
 | 
			
		||||
		termOpRecord.MachineId = cli.Info.Id
 | 
			
		||||
		termOpRecord.Username = cli.Info.Username
 | 
			
		||||
 | 
			
		||||
		// 回放文件路径为: 基础配置路径/操作日期(202301)/day/hour/randstr.cast
 | 
			
		||||
		recRelPath := path.Join(now.Format("200601"), fmt.Sprintf("%d", now.Day()), fmt.Sprintf("%d", now.Hour()))
 | 
			
		||||
		// 回放文件路径为: 基础配置路径/机器编号/操作日期(202301)/day/hour/randstr.cast
 | 
			
		||||
		recRelPath := path.Join(cli.Info.Code, now.Format("200601"), fmt.Sprintf("%d", now.Day()), fmt.Sprintf("%d", now.Hour()))
 | 
			
		||||
		// 文件绝对路径
 | 
			
		||||
		recAbsPath := path.Join(config.GetMachine().TerminalRecPath, recRelPath)
 | 
			
		||||
		os.MkdirAll(recAbsPath, 0766)
 | 
			
		||||
 
 | 
			
		||||
@@ -23,11 +23,11 @@ type MachineCronJob struct {
 | 
			
		||||
type MachineCronJobExec struct {
 | 
			
		||||
	model.DeletedModel
 | 
			
		||||
 | 
			
		||||
	CronJobId uint64    `json:"cronJobId" form:"cronJobId"`
 | 
			
		||||
	MachineId uint64    `json:"machineId" form:"machineId"`
 | 
			
		||||
	Status    int       `json:"status" form:"status"` // 执行状态
 | 
			
		||||
	Res       string    `json:"res"`                  // 执行结果
 | 
			
		||||
	ExecTime  time.Time `json:"execTime"`
 | 
			
		||||
	CronJobId   uint64    `json:"cronJobId" form:"cronJobId"`
 | 
			
		||||
	MachineCode string    `json:"machineCode" form:"machineCode"`
 | 
			
		||||
	Status      int       `json:"status" form:"status"` // 执行状态
 | 
			
		||||
	Res         string    `json:"res"`                  // 执行结果
 | 
			
		||||
	ExecTime    time.Time `json:"execTime"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ package entity
 | 
			
		||||
import "time"
 | 
			
		||||
 | 
			
		||||
type MachineQuery struct {
 | 
			
		||||
	Ids      string `json:"ids" form:"ids"`
 | 
			
		||||
	Id       uint64 `json:"id" form:"id"`
 | 
			
		||||
	Code     string `json:"code" form:"code"`
 | 
			
		||||
	Name     string `json:"name" form:"name"`
 | 
			
		||||
	Status   int8   `json:"status" form:"status"`
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,6 @@ import (
 | 
			
		||||
	"mayfly-go/internal/machine/domain/repository"
 | 
			
		||||
	"mayfly-go/pkg/base"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"mayfly-go/pkg/utils/collx"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/may-fly/cast"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type machineRepoImpl struct {
 | 
			
		||||
@@ -22,18 +18,13 @@ func newMachineRepo() repository.Machine {
 | 
			
		||||
// 分页获取机器信息列表
 | 
			
		||||
func (m *machineRepoImpl) GetMachineList(condition *entity.MachineQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
 | 
			
		||||
	qd := model.NewCond().
 | 
			
		||||
		Eq("id", condition.Id).
 | 
			
		||||
		Eq("status", condition.Status).
 | 
			
		||||
		Like("ip", condition.Ip).
 | 
			
		||||
		Like("name", condition.Name).
 | 
			
		||||
		In("code", condition.Codes).
 | 
			
		||||
		Like("code", condition.Code).
 | 
			
		||||
		Eq("code", condition.Code).
 | 
			
		||||
		Eq("protocol", condition.Protocol)
 | 
			
		||||
 | 
			
		||||
	if condition.Ids != "" {
 | 
			
		||||
		qd.In("id", collx.ArrayMap[string, uint64](strings.Split(condition.Ids, ","), func(val string) uint64 {
 | 
			
		||||
			return cast.ToUint64(val)
 | 
			
		||||
		}))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return m.PageByCondToAny(qd, pageParam, toEntity)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,12 @@ import (
 | 
			
		||||
	"mayfly-go/pkg/logx"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/may-fly/cast"
 | 
			
		||||
	"github.com/pkg/sftp"
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 机器客户端
 | 
			
		||||
// Cli 机器客户端
 | 
			
		||||
type Cli struct {
 | 
			
		||||
	Info *MachineInfo // 机器信息
 | 
			
		||||
 | 
			
		||||
@@ -17,7 +18,7 @@ type Cli struct {
 | 
			
		||||
	sftpClient *sftp.Client // sftp客户端
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取sftp client
 | 
			
		||||
// GetSftpCli 获取sftp client
 | 
			
		||||
func (c *Cli) GetSftpCli() (*sftp.Client, error) {
 | 
			
		||||
	if c.sshClient == nil {
 | 
			
		||||
		return nil, errorx.NewBiz("请先进行机器客户端连接")
 | 
			
		||||
@@ -36,7 +37,7 @@ func (c *Cli) GetSftpCli() (*sftp.Client, error) {
 | 
			
		||||
	return sftpclient, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取session
 | 
			
		||||
// GetSession 获取session
 | 
			
		||||
func (c *Cli) GetSession() (*ssh.Session, error) {
 | 
			
		||||
	if c.sshClient == nil {
 | 
			
		||||
		return nil, errorx.NewBiz("请先进行机器客户端连接")
 | 
			
		||||
@@ -49,7 +50,7 @@ func (c *Cli) GetSession() (*ssh.Session, error) {
 | 
			
		||||
	return session, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行shell
 | 
			
		||||
// Run 执行shell
 | 
			
		||||
// @param shell shell脚本命令
 | 
			
		||||
// @return 返回执行成功或错误的消息
 | 
			
		||||
func (c *Cli) Run(shell string) (string, error) {
 | 
			
		||||
@@ -58,38 +59,15 @@ func (c *Cli) Run(shell string) (string, error) {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	defer session.Close()
 | 
			
		||||
	buf, err := session.CombinedOutput(shell)
 | 
			
		||||
	// 将可能存在的windows换行符替换为linux格式
 | 
			
		||||
	buf, err := session.CombinedOutput(strings.ReplaceAll(shell, "\r\n", "\n"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return string(buf), err
 | 
			
		||||
	}
 | 
			
		||||
	return string(buf), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取机器的所有状态信息
 | 
			
		||||
func (c *Cli) GetAllStats() *Stats {
 | 
			
		||||
	stats := new(Stats)
 | 
			
		||||
	res, err := c.Run(StatsShell)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logx.Errorf("执行机器[id=%d, name=%s]运行状态信息脚本失败: %s", c.Info.Id, c.Info.Name, err.Error())
 | 
			
		||||
		return stats
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	infos := strings.Split(res, "-----")
 | 
			
		||||
	if len(infos) < 8 {
 | 
			
		||||
		return stats
 | 
			
		||||
	}
 | 
			
		||||
	getUptime(infos[0], stats)
 | 
			
		||||
	getHostname(infos[1], stats)
 | 
			
		||||
	getLoad(infos[2], stats)
 | 
			
		||||
	getMemInfo(infos[3], stats)
 | 
			
		||||
	getFSInfo(infos[4], stats)
 | 
			
		||||
	getInterfaces(infos[5], stats)
 | 
			
		||||
	getInterfaceInfo(infos[6], stats)
 | 
			
		||||
	getCPU(infos[7], stats)
 | 
			
		||||
	return stats
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 关闭client并从缓存中移除,如果使用隧道则也关闭
 | 
			
		||||
// Close 关闭client并从缓存中移除,如果使用隧道则也关闭
 | 
			
		||||
func (c *Cli) Close() {
 | 
			
		||||
	m := c.Info
 | 
			
		||||
	logx.Debugf("close machine cli -> id=%d, name=%s, ip=%s", m.Id, m.Name, m.Ip)
 | 
			
		||||
@@ -114,3 +92,77 @@ func (c *Cli) Close() {
 | 
			
		||||
		CloseSshTunnelMachine(int(sshTunnelMachineId), m.GetTunnelId())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAllStats 获取机器的所有状态信息
 | 
			
		||||
func (c *Cli) GetAllStats() *Stats {
 | 
			
		||||
	stats := new(Stats)
 | 
			
		||||
	res, err := c.Run(StatsShell)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logx.Errorf("执行机器[id=%d, name=%s]运行状态信息脚本失败: %s", c.Info.Id, c.Info.Name, err.Error())
 | 
			
		||||
		return stats
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	infos := strings.Split(res, "-----")
 | 
			
		||||
	if len(infos) < 8 {
 | 
			
		||||
		return stats
 | 
			
		||||
	}
 | 
			
		||||
	getUptime(infos[0], stats)
 | 
			
		||||
	getHostname(infos[1], stats)
 | 
			
		||||
	getLoad(infos[2], stats)
 | 
			
		||||
	getMemInfo(infos[3], stats)
 | 
			
		||||
	getFSInfo(infos[4], stats)
 | 
			
		||||
	getInterfaces(infos[5], stats)
 | 
			
		||||
	getInterfaceInfo(infos[6], stats)
 | 
			
		||||
	getCPU(infos[7], stats)
 | 
			
		||||
	return stats
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUsers 读取/etc/passwd,获取系统所有用户信息
 | 
			
		||||
func (c *Cli) GetUsers() ([]*UserInfo, error) {
 | 
			
		||||
	res, err := c.Run("cat /etc/passwd")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	var users []*UserInfo
 | 
			
		||||
	userLines := strings.Split(res, "\n")
 | 
			
		||||
	for _, userLine := range userLines {
 | 
			
		||||
		if userLine == "" {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		fields := strings.Split(userLine, ":")
 | 
			
		||||
		user := &UserInfo{
 | 
			
		||||
			Username: fields[0],
 | 
			
		||||
			UID:      cast.ToUint32(fields[2]),
 | 
			
		||||
			GID:      cast.ToUint32(fields[3]),
 | 
			
		||||
			HomeDir:  fields[5],
 | 
			
		||||
			Shell:    fields[6],
 | 
			
		||||
		}
 | 
			
		||||
		users = append(users, user)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return users, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetGroups 读取/etc/group,获取系统所有组信息
 | 
			
		||||
func (c *Cli) GetGroups() ([]*GroupInfo, error) {
 | 
			
		||||
	res, err := c.Run("cat /etc/group")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var groups []*GroupInfo
 | 
			
		||||
	groupLines := strings.Split(res, "\n")
 | 
			
		||||
	for _, groupLine := range groupLines {
 | 
			
		||||
		if groupLine == "" {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		fields := strings.Split(groupLine, ":")
 | 
			
		||||
		group := &GroupInfo{
 | 
			
		||||
			Groupname: fields[0],
 | 
			
		||||
			GID:       cast.ToUint32(fields[2]),
 | 
			
		||||
		}
 | 
			
		||||
		groups = append(groups, group)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return groups, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import (
 | 
			
		||||
	tagentity "mayfly-go/internal/tag/domain/entity"
 | 
			
		||||
	"mayfly-go/pkg/errorx"
 | 
			
		||||
	"mayfly-go/pkg/logx"
 | 
			
		||||
	"mayfly-go/pkg/utils/netx"
 | 
			
		||||
	"net"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
@@ -16,6 +17,7 @@ type MachineInfo struct {
 | 
			
		||||
	Key      string `json:"key"` // 缓存key
 | 
			
		||||
	Id       uint64 `json:"id"`
 | 
			
		||||
	Name     string `json:"name"`
 | 
			
		||||
	Code     string `json:"code"`
 | 
			
		||||
	Protocol int    `json:"protocol"`
 | 
			
		||||
 | 
			
		||||
	Ip   string `json:"ip"` // IP地址
 | 
			
		||||
@@ -34,12 +36,12 @@ type MachineInfo struct {
 | 
			
		||||
	CodePath         []string     `json:"codePath"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MachineInfo) UseSshTunnel() bool {
 | 
			
		||||
	return m.SshTunnelMachine != nil
 | 
			
		||||
func (mi *MachineInfo) UseSshTunnel() bool {
 | 
			
		||||
	return mi.SshTunnelMachine != nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MachineInfo) GetTunnelId() string {
 | 
			
		||||
	return fmt.Sprintf("machine:%d", m.Id)
 | 
			
		||||
func (mi *MachineInfo) GetTunnelId() string {
 | 
			
		||||
	return fmt.Sprintf("machine:%d", mi.Id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 连接
 | 
			
		||||
@@ -47,7 +49,7 @@ func (mi *MachineInfo) Conn() (*Cli, error) {
 | 
			
		||||
	logx.Infof("[%s]机器连接:%s:%d", mi.Name, mi.Ip, mi.Port)
 | 
			
		||||
 | 
			
		||||
	// 如果使用了ssh隧道,则修改机器ip port为暴露的ip port
 | 
			
		||||
	err := mi.IfUseSshTunnelChangeIpPort()
 | 
			
		||||
	err := mi.IfUseSshTunnelChangeIpPort(false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, errorx.NewBiz("ssh隧道连接失败: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
@@ -65,33 +67,39 @@ func (mi *MachineInfo) Conn() (*Cli, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 如果使用了ssh隧道,则修改机器ip port为暴露的ip port
 | 
			
		||||
func (me *MachineInfo) IfUseSshTunnelChangeIpPort() error {
 | 
			
		||||
	if !me.UseSshTunnel() {
 | 
			
		||||
func (mi *MachineInfo) IfUseSshTunnelChangeIpPort(out bool) error {
 | 
			
		||||
	if !mi.UseSshTunnel() {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	originId := me.Id
 | 
			
		||||
	originId := mi.Id
 | 
			
		||||
	if originId == 0 {
 | 
			
		||||
		// 随机设置一个id,如果使用了隧道则用于临时保存隧道
 | 
			
		||||
		me.Id = uint64(time.Now().Nanosecond())
 | 
			
		||||
		mi.Id = uint64(time.Now().Nanosecond())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sshTunnelMachine, err := GetSshTunnelMachine(int(me.SshTunnelMachine.Id), func(u uint64) (*MachineInfo, error) {
 | 
			
		||||
		return me.SshTunnelMachine, nil
 | 
			
		||||
	sshTunnelMachine, err := GetSshTunnelMachine(int(mi.SshTunnelMachine.Id), func(u uint64) (*MachineInfo, error) {
 | 
			
		||||
		return mi.SshTunnelMachine, nil
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	exposeIp, exposePort, err := sshTunnelMachine.OpenSshTunnel(me.GetTunnelId(), me.Ip, me.Port)
 | 
			
		||||
	exposeIp, exposePort, err := sshTunnelMachine.OpenSshTunnel(mi.GetTunnelId(), mi.Ip, mi.Port)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 是否获取局域网的本地IP
 | 
			
		||||
	if out {
 | 
			
		||||
		exposeIp = netx.GetOutBoundIP()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 修改机器ip地址
 | 
			
		||||
	me.Ip = exposeIp
 | 
			
		||||
	me.Port = exposePort
 | 
			
		||||
	mi.Ip = exposeIp
 | 
			
		||||
	mi.Port = exposePort
 | 
			
		||||
	// 代理之后置空跳板机信息,防止重复跳
 | 
			
		||||
	me.TempSshMachineId = me.SshTunnelMachine.Id
 | 
			
		||||
	me.SshTunnelMachine = nil
 | 
			
		||||
	mi.TempSshMachineId = mi.SshTunnelMachine.Id
 | 
			
		||||
	mi.SshTunnelMachine = nil
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -89,7 +89,7 @@ func (stm *SshTunnelMachine) OpenSshTunnel(id string, ip string, port int) (expo
 | 
			
		||||
		return "", 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	localHost := "127.0.0.1"
 | 
			
		||||
	localHost := "0.0.0.0"
 | 
			
		||||
	localAddr := fmt.Sprintf("%s:%d", localHost, localPort)
 | 
			
		||||
	listener, err := net.Listen("tcp", localAddr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -264,6 +264,9 @@ func getInterfaceInfo(iInfo string, stats *Stats) (err error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getCPU(cpuInfo string, stats *Stats) (err error) {
 | 
			
		||||
	if !strings.Contains(cpuInfo, ":") {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// %Cpu(s):  6.1 us,  3.0 sy,  0.0 ni, 90.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
 | 
			
		||||
	value := strings.Split(cpuInfo, ":")[1]
 | 
			
		||||
	values := strings.Split(value, ",")
 | 
			
		||||
@@ -287,3 +290,16 @@ func getCPU(cpuInfo string, stats *Stats) (err error) {
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UserInfo struct {
 | 
			
		||||
	UID      uint32 `json:"uid"`
 | 
			
		||||
	Username string `json:"uname"`
 | 
			
		||||
	GID      uint32 `json:"gid"`
 | 
			
		||||
	HomeDir  string `json:"homeDir"` // 用户登录后的起始工作目录
 | 
			
		||||
	Shell    string `json:"shell"`   // 用户登录时使用的 shell 程序
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GroupInfo struct {
 | 
			
		||||
	GID       uint32 `json:"gid"`
 | 
			
		||||
	Groupname string `json:"gname"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,10 @@ func InitMachineRouter(router *gin.RouterGroup) {
 | 
			
		||||
 | 
			
		||||
			req.NewGet(":machineId/process", m.GetProcess),
 | 
			
		||||
 | 
			
		||||
			req.NewGet(":machineId/users", m.GetUsers),
 | 
			
		||||
 | 
			
		||||
			req.NewGet(":machineId/groups", m.GetGroups),
 | 
			
		||||
 | 
			
		||||
			req.NewDelete(":machineId/process", m.KillProcess).Log(req.NewLogSave("终止进程")).RequiredPermissionCode("machine:killprocess"),
 | 
			
		||||
 | 
			
		||||
			req.NewPost("", m.SaveMachine).Log(req.NewLogSave("保存机器信息")).RequiredPermission(saveMachineP),
 | 
			
		||||
 
 | 
			
		||||
@@ -128,7 +128,7 @@ func (r *redisAppImpl) SaveRedis(ctx context.Context, param *dto.SaveRedis) erro
 | 
			
		||||
				ParentTagCodePaths: tagCodePaths,
 | 
			
		||||
			})
 | 
			
		||||
		}, func(ctx context.Context) error {
 | 
			
		||||
			return r.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
 | 
			
		||||
			return r.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
 | 
			
		||||
				ResourceCode: re.Code,
 | 
			
		||||
				ResourceType: tagentity.TagTypeRedis,
 | 
			
		||||
				AuthCerts:    []*tagentity.ResourceAuthCert{param.AuthCert},
 | 
			
		||||
@@ -170,7 +170,7 @@ func (r *redisAppImpl) SaveRedis(ctx context.Context, param *dto.SaveRedis) erro
 | 
			
		||||
			ParentTagCodePaths: tagCodePaths,
 | 
			
		||||
		})
 | 
			
		||||
	}, func(ctx context.Context) error {
 | 
			
		||||
		return r.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
 | 
			
		||||
		return r.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
 | 
			
		||||
			ResourceCode: oldRedis.Code,
 | 
			
		||||
			ResourceType: tagentity.TagTypeRedis,
 | 
			
		||||
			AuthCerts:    []*tagentity.ResourceAuthCert{param.AuthCert},
 | 
			
		||||
@@ -200,7 +200,7 @@ func (r *redisAppImpl) Delete(ctx context.Context, id uint64) error {
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
	}, func(ctx context.Context) error {
 | 
			
		||||
		return r.resourceAuthCertApp.RelateAuthCert(ctx, &tagapp.RelateAuthCertParam{
 | 
			
		||||
		return r.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
 | 
			
		||||
			ResourceCode: re.Code,
 | 
			
		||||
			ResourceType: tagentity.TagTypeRedis,
 | 
			
		||||
		})
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ package vo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"mayfly-go/internal/tag/application/dto"
 | 
			
		||||
	"mayfly-go/pkg/utils/collx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TagTreeVOS []*dto.SimpleTagTree
 | 
			
		||||
@@ -18,14 +17,21 @@ func (m *TagTreeVOS) ToTrees(pid uint64) []*TagTreeItem {
 | 
			
		||||
		return ttis
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ttis = collx.ArrayMap(*m, func(tr *dto.SimpleTagTree) *TagTreeItem { return &TagTreeItem{SimpleTagTree: tr} })
 | 
			
		||||
	tagMap := collx.ArrayToMap(ttis, func(item *TagTreeItem) string {
 | 
			
		||||
		return item.CodePath
 | 
			
		||||
	})
 | 
			
		||||
	tagMap := make(map[string]*TagTreeItem)
 | 
			
		||||
	var roots []*TagTreeItem
 | 
			
		||||
	for _, tag := range *m {
 | 
			
		||||
		tti := &TagTreeItem{SimpleTagTree: tag}
 | 
			
		||||
		tagMap[tag.CodePath] = tti
 | 
			
		||||
		ttis = append(ttis, tti)
 | 
			
		||||
		if tti.IsRoot() {
 | 
			
		||||
			roots = append(roots, tti)
 | 
			
		||||
			tti.Root = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, node := range ttis {
 | 
			
		||||
		// 根节点
 | 
			
		||||
		if node.IsRoot() {
 | 
			
		||||
		if node.Root {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		parentCodePath := node.GetParentPath(0)
 | 
			
		||||
@@ -35,5 +41,5 @@ func (m *TagTreeVOS) ToTrees(pid uint64) []*TagTreeItem {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return collx.ArrayFilter(ttis, func(tti *TagTreeItem) bool { return tti.IsRoot() })
 | 
			
		||||
	return roots
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								server/internal/tag/application/dto/auth_cert.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								server/internal/tag/application/dto/auth_cert.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
package dto
 | 
			
		||||
 | 
			
		||||
import "mayfly-go/internal/tag/domain/entity"
 | 
			
		||||
 | 
			
		||||
type RelateAuthCert struct {
 | 
			
		||||
	ResourceCode string
 | 
			
		||||
 | 
			
		||||
	// 资源标签类型
 | 
			
		||||
	ResourceType entity.TagType
 | 
			
		||||
 | 
			
		||||
	// 空数组则为删除该资源绑定的授权凭证
 | 
			
		||||
	AuthCerts []*entity.ResourceAuthCert
 | 
			
		||||
}
 | 
			
		||||
@@ -43,6 +43,7 @@ type SimpleTagTree struct {
 | 
			
		||||
	CodePath string         `json:"codePath"` // 标识路径,tag1/tag2/tagType1|tagCode/tagType2|yyycode/,非普通标签类型段含有标签类型
 | 
			
		||||
	Name     string         `json:"name"`     // 名称
 | 
			
		||||
	Remark   string         `json:"remark"`
 | 
			
		||||
	Root     bool           `json:"-" gorm:"-"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pt *SimpleTagTree) IsRoot() bool {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,21 +12,11 @@ import (
 | 
			
		||||
	"mayfly-go/pkg/utils/collx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type RelateAuthCertParam struct {
 | 
			
		||||
	ResourceCode string
 | 
			
		||||
 | 
			
		||||
	// 资源标签类型
 | 
			
		||||
	ResourceType entity.TagType
 | 
			
		||||
 | 
			
		||||
	// 空数组则为删除该资源绑定的授权凭证
 | 
			
		||||
	AuthCerts []*entity.ResourceAuthCert
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ResourceAuthCert interface {
 | 
			
		||||
	base.App[*entity.ResourceAuthCert]
 | 
			
		||||
 | 
			
		||||
	// RelateAuthCert 关联资源授权凭证信息
 | 
			
		||||
	RelateAuthCert(ctx context.Context, param *RelateAuthCertParam) error
 | 
			
		||||
	RelateAuthCert(ctx context.Context, param *dto.RelateAuthCert) error
 | 
			
		||||
 | 
			
		||||
	// SaveAuthCert 保存授权凭证信息
 | 
			
		||||
	SaveAuthCert(ctx context.Context, rac *entity.ResourceAuthCert) error
 | 
			
		||||
@@ -36,7 +26,7 @@ type ResourceAuthCert interface {
 | 
			
		||||
	// GetAuthCert 根据授权凭证名称获取授权凭证
 | 
			
		||||
	GetAuthCert(authCertName string) (*entity.ResourceAuthCert, error)
 | 
			
		||||
 | 
			
		||||
	// GetResourceAuthCert 获取资源授权凭证,默认获取特权账号,若没有则返回第一个
 | 
			
		||||
	// GetResourceAuthCert 获取资源授权凭证,优先获取默认账号,若不存在默认账号则返回特权账号,都不存在则返回第一个
 | 
			
		||||
	GetResourceAuthCert(resourceType entity.TagType, resourceCode string) (*entity.ResourceAuthCert, error)
 | 
			
		||||
 | 
			
		||||
	// FillAuthCertByAcs 根据授权凭证列表填充资源的授权凭证信息
 | 
			
		||||
@@ -64,7 +54,7 @@ func (r *resourceAuthCertAppImpl) InjectResourceAuthCertRepo(resourceAuthCertRep
 | 
			
		||||
	r.Repo = resourceAuthCertRepo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *RelateAuthCertParam) error {
 | 
			
		||||
func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *dto.RelateAuthCert) error {
 | 
			
		||||
	resourceCode := params.ResourceCode
 | 
			
		||||
	resourceType := int8(params.ResourceType)
 | 
			
		||||
	resourceAuthCerts := params.AuthCerts
 | 
			
		||||
@@ -92,11 +82,6 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *Re
 | 
			
		||||
		resourceAuthCert.ResourceType = int8(resourceType)
 | 
			
		||||
		name2AuthCert[resourceAuthCert.Name] = resourceAuthCert
 | 
			
		||||
 | 
			
		||||
		existNameAc := &entity.ResourceAuthCert{Name: resourceAuthCert.Name}
 | 
			
		||||
		if resourceAuthCert.Id == 0 && r.GetByCond(existNameAc) == nil && existNameAc.ResourceCode != resourceCode {
 | 
			
		||||
			return errorx.NewBiz("授权凭证的名称不能重复[%s]", resourceAuthCert.Name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 公共授权凭证,则无需进行密文加密,密文即为公共授权凭证名
 | 
			
		||||
		if resourceAuthCert.CiphertextType == entity.AuthCertCiphertextTypePublic {
 | 
			
		||||
			continue
 | 
			
		||||
@@ -111,63 +96,66 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *Re
 | 
			
		||||
	oldAuthCert, _ := r.ListByCond(&entity.ResourceAuthCert{ResourceCode: resourceCode, ResourceType: resourceType})
 | 
			
		||||
 | 
			
		||||
	// 新增、删除以及不变的授权凭证名
 | 
			
		||||
	var adds, dels, unmodifys []string
 | 
			
		||||
	var addAcNames, delAcNames, unmodifyAcNames []string
 | 
			
		||||
	if len(oldAuthCert) == 0 {
 | 
			
		||||
		logx.DebugfContext(ctx, "RelateAuthCert[%d-%s]-不存在已有的授权凭证信息, 为新增资源授权凭证", resourceType, resourceCode)
 | 
			
		||||
		adds = collx.MapKeys(name2AuthCert)
 | 
			
		||||
		addAcNames = collx.MapKeys(name2AuthCert)
 | 
			
		||||
	} else {
 | 
			
		||||
		oldNames := collx.ArrayMap(oldAuthCert, func(ac *entity.ResourceAuthCert) string {
 | 
			
		||||
			return ac.Name
 | 
			
		||||
		})
 | 
			
		||||
		adds, dels, unmodifys = collx.ArrayCompare[string](collx.MapKeys(name2AuthCert), oldNames)
 | 
			
		||||
		addAcNames, delAcNames, unmodifyAcNames = collx.ArrayCompare[string](collx.MapKeys(name2AuthCert), oldNames)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	addAuthCerts := make([]*entity.ResourceAuthCert, 0)
 | 
			
		||||
	for _, add := range adds {
 | 
			
		||||
		addAc := name2AuthCert[add]
 | 
			
		||||
	for _, addAcName := range addAcNames {
 | 
			
		||||
		addAc := name2AuthCert[addAcName]
 | 
			
		||||
		addAc.Id = 0
 | 
			
		||||
 | 
			
		||||
		existNameAc := &entity.ResourceAuthCert{Name: addAcName}
 | 
			
		||||
		if r.GetByCond(existNameAc) == nil && existNameAc.ResourceCode != resourceCode {
 | 
			
		||||
			return errorx.NewBiz("授权凭证的名称不能重复[%s]", addAcName)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		addAuthCerts = append(addAuthCerts, addAc)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 处理新增的授权凭证
 | 
			
		||||
	if len(addAuthCerts) > 0 {
 | 
			
		||||
		logx.DebugfContext(ctx, "RelateAuthCert[%d-%s]-新增授权凭证-[%v]", resourceType, resourceCode, adds)
 | 
			
		||||
		logx.DebugfContext(ctx, "RelateAuthCert[%d-%s]-新增授权凭证-[%v]", resourceType, resourceCode, addAcNames)
 | 
			
		||||
		if err := r.BatchInsert(ctx, addAuthCerts); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, del := range dels {
 | 
			
		||||
		logx.DebugfContext(ctx, "RelateAuthCert[%d-%s]-删除授权凭证-[%v]", resourceType, resourceCode, del)
 | 
			
		||||
		if err := r.DeleteByCond(ctx, &entity.ResourceAuthCert{ResourceCode: resourceCode, ResourceType: resourceType, Name: del}); err != nil {
 | 
			
		||||
	for _, delAcName := range delAcNames {
 | 
			
		||||
		logx.DebugfContext(ctx, "RelateAuthCert[%d-%s]-删除授权凭证-[%v]", resourceType, resourceCode, delAcName)
 | 
			
		||||
		if err := r.DeleteByCond(ctx, &entity.ResourceAuthCert{ResourceCode: resourceCode, ResourceType: resourceType, Name: delAcName}); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(unmodifys) > 0 {
 | 
			
		||||
	if len(unmodifyAcNames) > 0 {
 | 
			
		||||
		// 旧凭证名 -> 旧凭证
 | 
			
		||||
		oldName2AuthCert := collx.ArrayToMap(oldAuthCert, func(ac *entity.ResourceAuthCert) string {
 | 
			
		||||
			return ac.Name
 | 
			
		||||
		})
 | 
			
		||||
		acTagType := GetResourceAuthCertTagType(params.ResourceType)
 | 
			
		||||
		for _, unmodify := range unmodifys {
 | 
			
		||||
			unmodifyAc := name2AuthCert[unmodify]
 | 
			
		||||
			if unmodifyAc.Id == 0 {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		for _, unmodifyAcName := range unmodifyAcNames {
 | 
			
		||||
			unmodifyAc := name2AuthCert[unmodifyAcName]
 | 
			
		||||
 | 
			
		||||
			oldAuthCert := oldName2AuthCert[unmodify]
 | 
			
		||||
			oldAuthCert := oldName2AuthCert[unmodifyAcName]
 | 
			
		||||
			if !unmodifyAc.HasChanged(oldAuthCert) {
 | 
			
		||||
				logx.DebugfContext(ctx, "RelateAuthCert[%d-%s]-授权凭证[%s]未发生字段变更", resourceType, resourceCode, unmodify)
 | 
			
		||||
				logx.DebugfContext(ctx, "RelateAuthCert[%d-%s]-授权凭证[%s]未发生字段变更", resourceType, resourceCode, unmodifyAcName)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 如果修改了用户名,且该凭证关联至标签,则需要更新对应的标签名(资源授权凭证类型的标签名为username)
 | 
			
		||||
			if oldAuthCert.Username != unmodifyAc.Username && acTagType != 0 {
 | 
			
		||||
				r.tagTreeApp.UpdateTagName(ctx, acTagType, unmodify, unmodifyAc.Username)
 | 
			
		||||
				r.tagTreeApp.UpdateTagName(ctx, acTagType, unmodifyAcName, unmodifyAc.Username)
 | 
			
		||||
			}
 | 
			
		||||
			logx.DebugfContext(ctx, "RelateAuthCert[%d-%s]-更新授权凭证-[%v]", resourceType, resourceCode, unmodify)
 | 
			
		||||
			if err := r.UpdateById(ctx, unmodifyAc); err != nil {
 | 
			
		||||
			logx.DebugfContext(ctx, "RelateAuthCert[%d-%s]-更新授权凭证-[%v]", resourceType, resourceCode, unmodifyAcName)
 | 
			
		||||
			if err := r.UpdateByCond(ctx, unmodifyAc, &entity.ResourceAuthCert{Name: unmodifyAcName, ResourceCode: resourceCode, ResourceType: resourceType}); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -236,6 +224,12 @@ func (r *resourceAuthCertAppImpl) GetResourceAuthCert(resourceType entity.TagTyp
 | 
			
		||||
		return nil, errorx.NewBiz("该资源不存在授权凭证账号")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, resourceAuthCert := range resourceAuthCerts {
 | 
			
		||||
		if resourceAuthCert.Type == entity.AuthCertTypePrivateDefault {
 | 
			
		||||
			return r.decryptAuthCert(resourceAuthCert)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, resourceAuthCert := range resourceAuthCerts {
 | 
			
		||||
		if resourceAuthCert.Type == entity.AuthCertTypePrivileged {
 | 
			
		||||
			return r.decryptAuthCert(resourceAuthCert)
 | 
			
		||||
 
 | 
			
		||||
@@ -312,6 +312,7 @@ func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *dto.DelRes
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	delTagType := param.ChildType
 | 
			
		||||
	var childrenTagIds []uint64
 | 
			
		||||
	for _, resourceTag := range resourceTags {
 | 
			
		||||
		// 获取所有关联的子标签
 | 
			
		||||
		childrenTag, _ := p.ListByCond(model.NewCond().RLike("code_path", resourceTag.CodePath).Eq("type", delTagType))
 | 
			
		||||
@@ -319,14 +320,16 @@ func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *dto.DelRes
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		childrenTagIds := collx.ArrayMap(childrenTag, func(item *entity.TagTree) uint64 {
 | 
			
		||||
		childrenTagIds = append(childrenTagIds, collx.ArrayMap(childrenTag, func(item *entity.TagTree) uint64 {
 | 
			
		||||
			return item.Id
 | 
			
		||||
		})
 | 
			
		||||
		// 删除code_path下的所有子标签
 | 
			
		||||
		return p.deleteByIds(ctx, childrenTagIds)
 | 
			
		||||
		})...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
	if len(childrenTagIds) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	// 删除code_path下的所有子标签
 | 
			
		||||
	return p.deleteByIds(ctx, collx.ArrayDeduplicate(childrenTagIds))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *tagTreeAppImpl) ListByQuery(condition *entity.TagTreeQuery, toEntity any) {
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,7 @@ func (m *ResourceAuthCert) HasChanged(rac *ResourceAuthCert) bool {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return m.Username != rac.Username ||
 | 
			
		||||
		m.Ciphertext != rac.Ciphertext ||
 | 
			
		||||
		(m.Ciphertext != "" && rac.Ciphertext != "" && m.Ciphertext != rac.Ciphertext) ||
 | 
			
		||||
		m.CiphertextType != rac.CiphertextType ||
 | 
			
		||||
		m.Remark != rac.Remark ||
 | 
			
		||||
		m.Type != rac.Type ||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import "fmt"
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	AppName = "mayfly-go"
 | 
			
		||||
	Version = "v1.8.4"
 | 
			
		||||
	Version = "v1.8.7"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetAppInfo() string {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ package netx
 | 
			
		||||
import (
 | 
			
		||||
	"mayfly-go/pkg/logx"
 | 
			
		||||
	"net"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
 | 
			
		||||
)
 | 
			
		||||
@@ -68,3 +69,13 @@ func Ip2Region(ip string) string {
 | 
			
		||||
	}
 | 
			
		||||
	return region
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetOutBoundIP() string {
 | 
			
		||||
	conn, err := net.Dial("udp", "8.8.8.8:53")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "0.0.0.0"
 | 
			
		||||
	}
 | 
			
		||||
	localAddr := conn.LocalAddr().(*net.UDPAddr)
 | 
			
		||||
	ip := strings.Split(localAddr.String(), ":")[0]
 | 
			
		||||
	return ip
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								server/pkg/utils/netx/netx_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								server/pkg/utils/netx/netx_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
package netx
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestIp(t *testing.T) {
 | 
			
		||||
	fmt.Println(GetOutBoundIP())
 | 
			
		||||
}
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@@ -36,6 +36,7 @@ CREATE TABLE `t_db` (
 | 
			
		||||
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
 | 
			
		||||
  `code` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
			
		||||
  `name` varchar(191) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
			
		||||
  `get_database_mode` tinyint NULL COMMENT '库名获取方式(-1.实时获取、1.指定库名)',
 | 
			
		||||
  `database` varchar(1000) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
			
		||||
  `remark` varchar(191) COLLATE utf8mb4_bin DEFAULT NULL,
 | 
			
		||||
  `instance_id` bigint unsigned NOT NULL,
 | 
			
		||||
@@ -464,9 +465,9 @@ DROP TABLE IF EXISTS `t_machine_cron_job_exec`;
 | 
			
		||||
CREATE TABLE `t_machine_cron_job_exec` (
 | 
			
		||||
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
 | 
			
		||||
  `cron_job_id` bigint DEFAULT NULL,
 | 
			
		||||
  `machine_id` bigint DEFAULT NULL,
 | 
			
		||||
  `machine_code` varchar(36) DEFAULT NULL,
 | 
			
		||||
  `status` tinyint DEFAULT NULL COMMENT '状态',
 | 
			
		||||
  `res` varchar(1000) DEFAULT NULL COMMENT '执行结果',
 | 
			
		||||
  `res` varchar(4000) DEFAULT NULL COMMENT '执行结果',
 | 
			
		||||
  `exec_time` datetime DEFAULT NULL COMMENT '执行时间',
 | 
			
		||||
  `is_deleted` tinyint NOT NULL DEFAULT 0,
 | 
			
		||||
  `delete_time` datetime DEFAULT NULL,
 | 
			
		||||
@@ -760,11 +761,10 @@ INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(45, 3, '12sSjal1/lskeiql1/Ljewisd3/', 2, 1, '脚本管理-保存脚本按钮', 'machine:script:save', 120000000, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:01', '2021-06-08 11:09:01', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(46, 3, '12sSjal1/lskeiql1/Ljeew43/', 2, 1, '脚本管理-删除按钮', 'machine:script:del', 130000000, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:27', '2021-06-08 11:09:27', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(47, 3, '12sSjal1/lskeiql1/ODewix43/', 2, 1, '脚本管理-执行按钮', 'machine:script:run', 140000000, 'null', 1, 'admin', 1, 'admin', '2021-06-08 11:09:50', '2021-06-08 11:09:50', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(49, 36, 'dbms23ax/xleaiec2/', 1, 1, '数据库管理', 'dbs', 20000000, '{"component":"ops/db/DbList","icon":"Coin","isKeepAlive":true,"routeName":"DbList"}', 1, 'admin', 1, 'admin', '2021-07-07 15:13:55', '2023-03-15 17:31:28', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(54, 49, 'dbms23ax/xleaiec2/leix3Axl/', 2, 1, '数据库保存', 'db:save', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:36', '2021-07-08 17:31:05', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(55, 49, 'dbms23ax/xleaiec2/ygjL3sxA/', 2, 1, '数据库删除', 'db:del', 20000000, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:48', '2021-07-08 17:30:48', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(54, 135, 'dbms23ax/X0f4BxT0/leix3Axl/', 2, 1, '数据库保存', 'db:save', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:36', '2021-07-08 17:31:05', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(55, 135, 'dbms23ax/X0f4BxT0/ygjL3sxA/', 2, 1, '数据库删除', 'db:del', 20000000, 'null', 1, 'admin', 1, 'admin', '2021-07-08 17:30:48', '2021-07-08 17:30:48', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(57, 3, '12sSjal1/lskeiql1/OJewex43/', 2, 1, '基本权限', 'machine', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:48:02', '2021-07-09 10:48:02', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(58, 49, 'dbms23ax/xleaiec2/AceXe321/', 2, 1, '基本权限', 'db', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:48:22', '2021-07-09 10:48:22', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(58, 135, 'dbms23ax/X0f4BxT0/AceXe321/', 2, 1, '数据库基本权限', 'db', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:48:22', '2021-07-09 10:48:22', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(59, 38, 'dbms23ax/exaeca2x/ealcia23/', 2, 1, '基本权限', 'db:exec', 10000000, 'null', 1, 'admin', 1, 'admin', '2021-07-09 10:50:13', '2021-07-09 10:50:13', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(60, 0, 'RedisXq4/', 1, 1, 'Redis', '/redis', 50000001, '{"icon":"iconfont icon-redis","isKeepAlive":true,"routeName":"RDS"}', 1, 'admin', 1, 'admin', '2021-07-19 20:15:41', '2023-03-15 16:44:59', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(61, 60, 'RedisXq4/Exitx4al/', 1, 1, '数据操作', 'data-operation', 10000000, '{"component":"ops/redis/DataOperation","icon":"iconfont icon-redis","isKeepAlive":true,"routeName":"DataOperation"}', 1, 'admin', 1, 'admin', '2021-07-19 20:17:29', '2023-03-15 16:37:50', 0, NULL);
 | 
			
		||||
@@ -813,8 +813,8 @@ INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(152, 150, 'Jra0n7De/zvAMo2vk/', 2, 1, '编辑', 'db:sync:save', 1703641320, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:42:00', '2023-12-27 09:42:12', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(151, 150, 'Jra0n7De/uAnHZxEV/', 2, 1, '基本权限', 'db:sync', 1703641202, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:40:02', '2023-12-27 09:40:02', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(150, 36, 'Jra0n7De/', 1, 1, '数据同步', 'sync', 1693040707, '{"component":"ops/db/SyncTaskList","icon":"Coin","isKeepAlive":true,"routeName":"SyncTaskList"}', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-22 09:51:34', '2023-12-27 10:16:57', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(160, 49, 'dbms23ax/xleaiec2/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(161, 49, 'dbms23ax/xleaiec2/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(160, 135, 'dbms23ax/X0f4BxT0/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(161, 135, 'dbms23ax/X0f4BxT0/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1709208354, 1708911264, '6egfEVYr/fw0Hhvye/b4cNf3iq/', 2, 1, '删除流程', 'flow:procdef:del', 1709208354, 'null', 1, 'admin', 1, 'admin', '2024-02-29 20:05:54', '2024-02-29 20:05:54', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1709208339, 1708911264, '6egfEVYr/fw0Hhvye/r9ZMTHqC/', 2, 1, '保存流程', 'flow:procdef:save', 1709208339, 'null', 1, 'admin', 1, 'admin', '2024-02-29 20:05:40', '2024-02-29 20:05:40', 0, NULL);
 | 
			
		||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1709103180, 1708910975, '6egfEVYr/oNCIbynR/', 1, 1, '我的流程', 'procinsts', 1708911263, '{"component":"flow/ProcinstList","icon":"Tickets","isKeepAlive":true,"routeName":"ProcinstList"}', 1, 'admin', 1, 'admin', '2024-02-28 14:53:00', '2024-02-29 20:36:07', 0, NULL);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								server/resources/script/sql/v1.8/v1.8.5.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								server/resources/script/sql/v1.8/v1.8.5.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
ALTER TABLE t_db ADD get_database_mode tinyint NULL COMMENT '库名获取方式(-1.实时获取、1.指定库名)';
 | 
			
		||||
UPDATE t_db SET get_database_mode = 1;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ALTER TABLE t_machine_cron_job_exec ADD machine_code varchar(36) NULL COMMENT '机器编号';
 | 
			
		||||
ALTER TABLE t_machine_cron_job_exec DROP COLUMN machine_id;
 | 
			
		||||
							
								
								
									
										7
									
								
								server/resources/script/sql/v1.8/v1.8.6.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								server/resources/script/sql/v1.8/v1.8.6.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/leix3Axl/', `type`=2, status=1, name='数据库保存', code='db:save', weight=1693041085, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2021-07-08 17:30:36', update_time='2024-05-17 21:50:01', is_deleted=0, delete_time=NULL WHERE id=54;
 | 
			
		||||
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/ygjL3sxA/', `type`=2, status=1, name='数据库删除', code='db:del', weight=1693041086, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2021-07-08 17:30:48', update_time='2024-05-17 21:50:04', is_deleted=0, delete_time=NULL WHERE id=55;
 | 
			
		||||
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/AceXe321/', `type`=2, status=1, name='数据库基本权限', code='db', weight=1693041085, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2021-07-09 10:48:22', update_time='2024-05-17 21:52:52', is_deleted=0, delete_time=NULL WHERE id=58;
 | 
			
		||||
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/3NUXQFIO/', `type`=2, status=1, name='数据库备份', code='db:backup', weight=1693041087, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2024-01-23 09:37:56', update_time='2024-05-17 21:50:07', is_deleted=0, delete_time=NULL WHERE id=160;
 | 
			
		||||
UPDATE t_sys_resource SET pid=135, ui_path='dbms23ax/X0f4BxT0/ghErkTdb/', `type`=2, status=1, name='数据库恢复', code='db:restore', weight=1693041088, meta='null', creator_id=1, creator='admin', modifier_id=1, modifier='admin', create_time='2024-01-23 09:38:29', update_time='2024-05-17 21:50:10', is_deleted=0, delete_time=NULL WHERE id=161;
 | 
			
		||||
 | 
			
		||||
DELETE FROM t_sys_resource WHERE id=49;
 | 
			
		||||
		Reference in New Issue
	
	Block a user