mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-22 09:00:25 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3017460cc7 | ||
|
|
4836a770c4 | ||
|
|
e6c89fad1b | ||
|
|
dba19b1e66 | ||
|
|
4e30bdb7cc | ||
|
|
4ac57cd140 | ||
|
|
c4d52ce47a | ||
|
|
54d0688571 |
@@ -11,40 +11,40 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"@logicflow/core": "^2.1.1",
|
"@logicflow/core": "^2.1.3",
|
||||||
"@logicflow/extension": "^2.1.2",
|
"@logicflow/extension": "^2.1.5",
|
||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^14.0.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-search": "^0.15.0",
|
"@xterm/addon-search": "^0.15.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"asciinema-player": "^3.10.0",
|
"asciinema-player": "^3.12.1",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.19",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.11.2",
|
"element-plus": "^2.11.8",
|
||||||
"js-base64": "^3.7.7",
|
"js-base64": "^3.7.8",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.5.4",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.54.0",
|
||||||
"monaco-sql-languages": "^0.15.1",
|
"monaco-sql-languages": "^0.15.1",
|
||||||
"monaco-themes": "^0.4.6",
|
"monaco-themes": "^0.4.7",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.4",
|
||||||
"qrcode.vue": "^3.6.0",
|
"qrcode.vue": "^3.6.0",
|
||||||
"screenfull": "^6.0.2",
|
"screenfull": "^6.0.2",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
"sql-formatter": "^15.6.5",
|
"sql-formatter": "^15.6.10",
|
||||||
"trzsz": "^1.1.5",
|
"trzsz": "^1.1.5",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^13.0.0",
|
||||||
"vue": "^v3.6.0-alpha.2",
|
"vue": "^v3.6.0-alpha.3",
|
||||||
"vue-i18n": "^11.1.11",
|
"vue-i18n": "^11.1.12",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.6.3",
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.12",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^22.13.14",
|
"@types/node": "^22.13.14",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
@@ -52,16 +52,16 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
||||||
"@typescript-eslint/parser": "^8.35.0",
|
"@typescript-eslint/parser": "^8.35.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/compiler-sfc": "^3.5.18",
|
"@vue/compiler-sfc": "^3.5.22",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"code-inspector-plugin": "^1.0.4",
|
"code-inspector-plugin": "^1.0.4",
|
||||||
"eslint": "^9.29.0",
|
"eslint": "^9.29.0",
|
||||||
"eslint-plugin-vue": "^10.4.0",
|
"eslint-plugin-vue": "^10.5.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.1",
|
"prettier": "^3.6.1",
|
||||||
"sass": "^1.92.1",
|
"sass": "^1.94.0",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.17",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"vite": "npm:rolldown-vite@latest",
|
||||||
"vite-plugin-progress": "0.0.7",
|
"vite-plugin-progress": "0.0.7",
|
||||||
"vue-eslint-parser": "^10.2.0"
|
"vue-eslint-parser": "^10.2.0"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const config = {
|
|||||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||||
|
|
||||||
// 系统版本
|
// 系统版本
|
||||||
version: 'v1.10.3',
|
version: 'v1.10.4',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form-item v-bind="$attrs">
|
<el-form-item v-bind="$attrs">
|
||||||
<template #label>
|
<template #label>
|
||||||
|
<div class="flex items-center">
|
||||||
{{ props.label }}
|
{{ props.label }}
|
||||||
|
|
||||||
<el-tooltip :placement="props.placement">
|
<el-tooltip :placement="props.placement">
|
||||||
<template #content>
|
<template #content>
|
||||||
<span v-html="props.tooltip"></span>
|
<span v-html="props.tooltip"></span>
|
||||||
</template>
|
</template>
|
||||||
<SvgIcon name="QuestionFilled" />
|
<SvgIcon name="QuestionFilled" class="ml-1" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 遍历父组件传入的 solts 透传给子组件 -->
|
<!-- 遍历父组件传入的 solts 透传给子组件 -->
|
||||||
@@ -24,11 +26,11 @@ import { useSlots } from 'vue';
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
require: true,
|
required: true,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
type: String,
|
type: String,
|
||||||
require: true,
|
required: true,
|
||||||
},
|
},
|
||||||
placement: {
|
placement: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ const defaultOptions = {
|
|||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
lineNumbers: 'on',
|
lineNumbers: 'on',
|
||||||
lineNumbersMinChars: 3,
|
lineNumbersMinChars: 3,
|
||||||
|
fixedOverflowWidgets: true, // 使弹出层不被容器限制
|
||||||
} as editor.IStandaloneEditorConstructionOptions;
|
} as editor.IStandaloneEditorConstructionOptions;
|
||||||
|
|
||||||
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
|
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
es: {
|
es: {
|
||||||
keywordPlaceholder: 'host / name / code',
|
keywordPlaceholder: 'host / name / code',
|
||||||
|
protocol: 'Protocol',
|
||||||
port: 'Port',
|
port: 'Port',
|
||||||
size: 'size',
|
size: 'size',
|
||||||
docs: 'docs',
|
docs: 'docs',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ export default {
|
|||||||
personalCenter: 'Personal Center',
|
personalCenter: 'Personal Center',
|
||||||
myResource: 'Resource',
|
myResource: 'Resource',
|
||||||
|
|
||||||
tag: 'Tag',
|
tag: 'Resource',
|
||||||
tagTree: 'Tag Tree',
|
tagTree: 'Resource Tree',
|
||||||
tagSave: 'Save Tag',
|
tagSave: 'Save Tag',
|
||||||
tagDelete: 'Delete Tag',
|
tagDelete: 'Delete Tag',
|
||||||
authorization: 'Authorization',
|
authorization: 'Authorization',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default {
|
|||||||
tagTips1: '1. Used to group assets',
|
tagTips1: '1. Used to group assets',
|
||||||
tagTips2: '2. Can be allocated in team management for resource isolation',
|
tagTips2: '2. Can be allocated in team management for resource isolation',
|
||||||
tagTips3: '3. Team members who own a parent tag have access to resources that manipulate their own or child tag associations',
|
tagTips3: '3. Team members who own a parent tag have access to resources that manipulate their own or child tag associations',
|
||||||
|
tagTips4: '4. Right-click nodes to edit or add child tags',
|
||||||
machine: 'Machine',
|
machine: 'Machine',
|
||||||
db: 'Db',
|
db: 'Db',
|
||||||
code: 'Code',
|
code: 'Code',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
es: {
|
es: {
|
||||||
keywordPlaceholder: 'host / 名称 / 编号',
|
keywordPlaceholder: 'host / 名称 / 编号',
|
||||||
|
protocol: '协议',
|
||||||
port: '端口',
|
port: '端口',
|
||||||
size: '存储大小',
|
size: '存储大小',
|
||||||
docs: '文档数',
|
docs: '文档数',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ export default {
|
|||||||
personalCenter: '个人中心',
|
personalCenter: '个人中心',
|
||||||
myResource: '我的资源',
|
myResource: '我的资源',
|
||||||
|
|
||||||
tag: '标签',
|
tag: '资源',
|
||||||
tagTree: '标签树',
|
tagTree: '资源树',
|
||||||
tagSave: '保存标签',
|
tagSave: '保存标签',
|
||||||
tagDelete: '删除标签',
|
tagDelete: '删除标签',
|
||||||
authorization: '授权凭证',
|
authorization: '授权凭证',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default {
|
|||||||
tagTips1: '1. 用于将资产进行归类',
|
tagTips1: '1. 用于将资产进行归类',
|
||||||
tagTips2: '2. 可在团队管理中进行分配,用于资源隔离',
|
tagTips2: '2. 可在团队管理中进行分配,用于资源隔离',
|
||||||
tagTips3: '3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源',
|
tagTips3: '3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源',
|
||||||
|
tagTips4: '4. 右击节点可进行编辑或添加子标签操作',
|
||||||
machine: '机器',
|
machine: '机器',
|
||||||
db: '数据库',
|
db: '数据库',
|
||||||
es: 'ES',
|
es: 'ES',
|
||||||
|
|||||||
@@ -654,8 +654,9 @@ const onCopyConfigClick = (target: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checkClientWidth = () => {
|
const checkClientWidth = () => {
|
||||||
const oldLayout = getLocal('oldLayout');
|
let oldLayout = getLocal('oldLayout');
|
||||||
if (!oldLayout) {
|
if (!oldLayout) {
|
||||||
|
oldLayout = themeConfig.value.layout;
|
||||||
setLocal('oldLayout', themeConfig.value.layout);
|
setLocal('oldLayout', themeConfig.value.layout);
|
||||||
}
|
}
|
||||||
if (width.value < 1000) {
|
if (width.value < 1000) {
|
||||||
|
|||||||
@@ -138,9 +138,8 @@ onBeforeRouteUpdate((to) => {
|
|||||||
.horizontal-menu :deep(.el-sub-menu__title) {
|
.horizontal-menu :deep(.el-sub-menu__title) {
|
||||||
margin: 0 5px !important;
|
margin: 0 5px !important;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-width: 150px;
|
width: fit-content;
|
||||||
min-width: 120px; // 统一最小宽度
|
|
||||||
text-align: center; // 使文字居中对齐
|
text-align: center; // 使文字居中对齐
|
||||||
padding: 0 8px !important; // 统一内边距
|
padding: 0 16px !important; // 统一内边距
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
|
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
|
||||||
<el-form-item prop="id" label="DB" required>
|
<el-form-item prop="id" label="DB" required>
|
||||||
<TagTreeResourceSelect
|
<ResourceSelect
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
v-model="selectRedis"
|
v-model="selectRedis"
|
||||||
@change="changeRedis"
|
@change="changeRedis"
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
:tag-path-node-type="NodeTypeTagPath"
|
:tag-path-node-type="NodeTypeTagPath"
|
||||||
:placeholder="$t('flow.selectRedisPlaceholder')"
|
:placeholder="$t('flow.selectRedisPlaceholder')"
|
||||||
>
|
>
|
||||||
</TagTreeResourceSelect>
|
</ResourceSelect>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="cmd" label="CMD" required>
|
<el-form-item prop="cmd" label="CMD" required>
|
||||||
@@ -21,12 +21,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
import TagTreeResourceSelect from '@/views/ops/component/TagTreeResourceSelect.vue';
|
import ResourceSelect from '@/views/ops/resource/ResourceSelect.vue';
|
||||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||||
import { redisApi } from '@/views/ops/redis/api';
|
import { redisApi } from '@/views/ops/redis/api';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Rules } from '@/common/rule';
|
import { Rules } from '@/common/rule';
|
||||||
|
import { RedisIcon } from '@/views/ops/redis/resource';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
|
|||||||
await sleep(100);
|
await sleep(100);
|
||||||
return redisInfos.map((x: any) => {
|
return redisInfos.map((x: any) => {
|
||||||
x.tagPath = parentNode.key;
|
x.tagPath = parentNode.key;
|
||||||
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x);
|
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x).withIcon(RedisIcon);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,7 +62,9 @@ const NodeTypeRedis = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTr
|
|||||||
const redisInfo = parentNode.params;
|
const redisInfo = parentNode.params;
|
||||||
|
|
||||||
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
||||||
return new TagTreeNode(x, `db${x}`, 2 as any).withIsLeaf(true).withParams({
|
return new TagTreeNode(x, `db${x}`, 2 as any)
|
||||||
|
.withIsLeaf(true)
|
||||||
|
.withParams({
|
||||||
id: redisInfo.id,
|
id: redisInfo.id,
|
||||||
db: x,
|
db: x,
|
||||||
name: `db${x}`,
|
name: `db${x}`,
|
||||||
@@ -69,7 +72,8 @@ const NodeTypeRedis = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTr
|
|||||||
tagPath: redisInfo.tagPath,
|
tagPath: redisInfo.tagPath,
|
||||||
redisName: redisInfo.name,
|
redisName: redisInfo.name,
|
||||||
code: redisInfo.code,
|
code: redisInfo.code,
|
||||||
});
|
})
|
||||||
|
.withIcon({ name: 'Coin', color: '#67c23a' });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (redisInfo.mode == 'cluster') {
|
if (redisInfo.mode == 'cluster') {
|
||||||
|
|||||||
@@ -37,10 +37,9 @@
|
|||||||
:label="$t(TagResourceTypeEnum.Machine.label)"
|
:label="$t(TagResourceTypeEnum.Machine.label)"
|
||||||
:value="TagResourceTypeEnum.Machine.value"
|
:value="TagResourceTypeEnum.Machine.value"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<el-option
|
<el-option
|
||||||
:key="TagResourceTypeEnum.DbInstance.value"
|
:key="TagResourceTypeEnum.DbInstance.value"
|
||||||
:label="TagResourceTypeEnum.DbInstance.label"
|
:label="$t(TagResourceTypeEnum.DbInstance.label)"
|
||||||
:value="TagResourceTypeEnum.DbInstance.value"
|
:value="TagResourceTypeEnum.DbInstance.value"
|
||||||
/>
|
/>
|
||||||
<el-option
|
<el-option
|
||||||
|
|||||||
@@ -1,246 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-dialog :title="title" :model-value="visible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="38%">
|
|
||||||
<el-form :model="state.form" ref="backupForm" label-width="auto" :rules="rules">
|
|
||||||
<el-form-item prop="dbNames" label="数据库名称">
|
|
||||||
<el-select
|
|
||||||
v-model="state.dbNamesSelected"
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
collapse-tags
|
|
||||||
collapse-tags-tooltip
|
|
||||||
filterable
|
|
||||||
:disabled="state.editOrCreate"
|
|
||||||
:filter-method="filterDbNames"
|
|
||||||
placeholder="数据库名称"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<el-checkbox v-model="checkAllDbNames" :indeterminate="indeterminateDbNames" @change="handleCheckAll"> 全选 </el-checkbox>
|
|
||||||
</template>
|
|
||||||
<el-option v-for="db in state.dbNamesFiltered" :key="db" :label="db" :value="db" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item prop="name" label="任务名称">
|
|
||||||
<el-input v-model="state.form.name" type="text" placeholder="任务名称"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="startTime" label="开始时间">
|
|
||||||
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="intervalDay" label="备份周期(天)">
|
|
||||||
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="单位:天"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="maxSaveDays" label="备份历史保留天数">
|
|
||||||
<el-input v-model.number="state.form.maxSaveDays" type="number" placeholder="0: 永久保留"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="cancel()">取 消</el-button>
|
|
||||||
<el-button type="primary" :loading="state.btnLoading" @click="btnOk">确 定</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { reactive, ref, toRefs, watch } from 'vue';
|
|
||||||
import { dbApi } from './api';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import type { CheckboxValueType } from 'element-plus';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
data: {
|
|
||||||
type: [Boolean, Object],
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
dbId: {
|
|
||||||
type: [Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const visible = defineModel<boolean>('visible', {
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
//定义事件
|
|
||||||
const emit = defineEmits(['cancel', 'val-change']);
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
dbNames: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择需要备份的数据库',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
intervalDay: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
pattern: /^[1-9]\d*$/,
|
|
||||||
message: '请输入正整数',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
startTime: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择开始时间',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
maxSaveDays: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
pattern: /^[0-9]\d*$/,
|
|
||||||
message: '请输入非负整数',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const backupForm: any = ref(null);
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
form: {
|
|
||||||
id: 0,
|
|
||||||
dbId: 0,
|
|
||||||
dbNames: '',
|
|
||||||
name: '',
|
|
||||||
intervalDay: 1,
|
|
||||||
startTime: null as any,
|
|
||||||
repeated: true,
|
|
||||||
maxSaveDays: 0,
|
|
||||||
},
|
|
||||||
btnLoading: false,
|
|
||||||
dbNamesSelected: [] as any,
|
|
||||||
dbNamesWithoutBackup: [] as any,
|
|
||||||
dbNamesFiltered: [] as any,
|
|
||||||
filterString: '',
|
|
||||||
editOrCreate: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { dbNamesSelected, dbNamesWithoutBackup } = toRefs(state);
|
|
||||||
|
|
||||||
const checkAllDbNames = ref(false);
|
|
||||||
const indeterminateDbNames = ref(false);
|
|
||||||
|
|
||||||
watch(visible, (newValue: any) => {
|
|
||||||
if (newValue) {
|
|
||||||
init(props.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const init = (data: any) => {
|
|
||||||
state.dbNamesSelected = [];
|
|
||||||
state.form.dbId = props.dbId;
|
|
||||||
if (data) {
|
|
||||||
state.editOrCreate = true;
|
|
||||||
state.dbNamesWithoutBackup = [data.dbName];
|
|
||||||
state.dbNamesSelected = [data.dbName];
|
|
||||||
state.form.id = data.id;
|
|
||||||
state.form.dbNames = data.dbName;
|
|
||||||
state.form.name = data.name;
|
|
||||||
state.form.intervalDay = data.intervalDay;
|
|
||||||
state.form.startTime = data.startTime;
|
|
||||||
state.form.maxSaveDays = data.maxSaveDays;
|
|
||||||
} else {
|
|
||||||
state.editOrCreate = false;
|
|
||||||
state.form.name = '';
|
|
||||||
state.form.intervalDay = 1;
|
|
||||||
const now = new Date();
|
|
||||||
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
||||||
state.form.maxSaveDays = 0;
|
|
||||||
getDbNamesWithoutBackup();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDbNamesWithoutBackup = async () => {
|
|
||||||
if (props.dbId > 0) {
|
|
||||||
state.dbNamesWithoutBackup = await dbApi.getDbNamesWithoutBackup.request({ dbId: props.dbId });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const btnOk = async () => {
|
|
||||||
backupForm.value.validate(async (valid: boolean) => {
|
|
||||||
if (!valid) {
|
|
||||||
ElMessage.error('请正确填写信息');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.form.repeated = true;
|
|
||||||
const reqForm = { ...state.form };
|
|
||||||
let api = dbApi.createDbBackup;
|
|
||||||
if (props.data) {
|
|
||||||
api = dbApi.saveDbBackup;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
state.btnLoading = true;
|
|
||||||
await api.request(reqForm);
|
|
||||||
ElMessage.success('保存成功');
|
|
||||||
emit('val-change', state.form);
|
|
||||||
cancel();
|
|
||||||
} finally {
|
|
||||||
state.btnLoading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
visible.value = false;
|
|
||||||
emit('cancel');
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkDbSelect = (val: string[]) => {
|
|
||||||
const selected = val.filter((dbName: string) => {
|
|
||||||
return dbName.includes(state.filterString);
|
|
||||||
});
|
|
||||||
if (selected.length === 0) {
|
|
||||||
checkAllDbNames.value = false;
|
|
||||||
indeterminateDbNames.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (selected.length === state.dbNamesFiltered.length) {
|
|
||||||
checkAllDbNames.value = true;
|
|
||||||
indeterminateDbNames.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
indeterminateDbNames.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(dbNamesSelected, (val: string[]) => {
|
|
||||||
checkDbSelect(val);
|
|
||||||
state.form.dbNames = val.join(' ');
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(dbNamesWithoutBackup, (val: string[]) => {
|
|
||||||
state.dbNamesFiltered = val.map((dbName: string) => dbName);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleCheckAll = (val: CheckboxValueType) => {
|
|
||||||
const selected = state.dbNamesSelected.filter((dbName: string) => {
|
|
||||||
return !state.dbNamesFiltered.includes(dbName);
|
|
||||||
});
|
|
||||||
if (val) {
|
|
||||||
state.dbNamesSelected = selected.concat(state.dbNamesFiltered);
|
|
||||||
} else {
|
|
||||||
state.dbNamesSelected = selected;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterDbNames = (filterString: string) => {
|
|
||||||
state.dbNamesFiltered = state.dbNamesWithoutBackup.filter((dbName: string) => {
|
|
||||||
return dbName.includes(filterString);
|
|
||||||
});
|
|
||||||
state.filterString = filterString;
|
|
||||||
checkDbSelect(state.dbNamesSelected);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="db-backup-history">
|
|
||||||
<page-table
|
|
||||||
height="100%"
|
|
||||||
ref="pageTableRef"
|
|
||||||
:page-api="dbApi.getDbBackupHistories"
|
|
||||||
:show-selection="true"
|
|
||||||
v-model:selection-data="state.selectedData"
|
|
||||||
:searchItems="searchItems"
|
|
||||||
:before-query-fn="beforeQueryFn"
|
|
||||||
v-model:query-form="query"
|
|
||||||
:columns="columns"
|
|
||||||
>
|
|
||||||
<template #dbSelect>
|
|
||||||
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
|
||||||
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
|
||||||
</el-select>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #tableHeader>
|
|
||||||
<el-button type="primary" icon="back" @click="restoreDbBackupHistory(null)">立即恢复</el-button>
|
|
||||||
<el-button type="danger" icon="delete" @click="deleteDbBackupHistory(null)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #action="{ data }">
|
|
||||||
<div>
|
|
||||||
<el-button @click="restoreDbBackupHistory(data)" type="primary" link>立即恢复</el-button>
|
|
||||||
<el-button @click="deleteDbBackupHistory(data)" type="danger" link>删除</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</page-table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { toRefs, reactive, Ref, ref } from 'vue';
|
|
||||||
import { dbApi } from './api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
|
||||||
import { TableColumn } from '@/components/pagetable';
|
|
||||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
|
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
dbId: {
|
|
||||||
type: [Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
dbNames: {
|
|
||||||
type: [Array<String>],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
TableColumn.new('dbName', '数据库名称'),
|
|
||||||
TableColumn.new('name', '备份名称'),
|
|
||||||
TableColumn.new('createTime', '创建时间').isTime(),
|
|
||||||
TableColumn.new('lastResult', '恢复结果'),
|
|
||||||
TableColumn.new('lastTime', '恢复时间').isTime(),
|
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(160).fixedRight(),
|
|
||||||
];
|
|
||||||
|
|
||||||
const emptyQuery = {
|
|
||||||
dbId: 0,
|
|
||||||
dbName: '',
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
query: emptyQuery,
|
|
||||||
/**
|
|
||||||
* 选中的数据
|
|
||||||
*/
|
|
||||||
selectedData: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const { query } = toRefs(state);
|
|
||||||
|
|
||||||
const beforeQueryFn = (query: any) => {
|
|
||||||
query.dbId = props.dbId;
|
|
||||||
return query;
|
|
||||||
};
|
|
||||||
|
|
||||||
const search = async () => {
|
|
||||||
await pageTableRef.value.search();
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDbBackupHistory = async (data: any) => {
|
|
||||||
let backupHistoryId: string;
|
|
||||||
if (data) {
|
|
||||||
backupHistoryId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要删除的数据库备份历史');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ElMessageBox.confirm(`确定删除 “数据库备份历史” 吗?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
await dbApi.deleteDbBackupHistory.request({ dbId: props.dbId, backupHistoryId: backupHistoryId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('删除成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const restoreDbBackupHistory = async (data: any) => {
|
|
||||||
let backupHistoryId: string;
|
|
||||||
if (data) {
|
|
||||||
backupHistoryId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
const pluralDbNames: string[] = [];
|
|
||||||
const dbNames: Map<string, boolean> = new Map();
|
|
||||||
state.selectedData.forEach((item: any) => {
|
|
||||||
if (!dbNames.has(item.dbName)) {
|
|
||||||
dbNames.set(item.dbName, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!dbNames.get(item.dbName)) {
|
|
||||||
dbNames.set(item.dbName, true);
|
|
||||||
pluralDbNames.push(item.dbName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (pluralDbNames.length > 0) {
|
|
||||||
ElMessage.error('多次选择相同数据库:' + pluralDbNames.join(', '));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要恢复的数据库备份历史');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ElMessageBox.confirm(`确定从 “数据库备份历史” 中恢复数据库吗?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
|
|
||||||
await dbApi.restoreDbBackupHistory.request({
|
|
||||||
dbId: props.dbId,
|
|
||||||
backupHistoryId: backupHistoryId,
|
|
||||||
});
|
|
||||||
await search();
|
|
||||||
ElMessage.success('成功创建数据库恢复任务');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="db-backup">
|
|
||||||
<page-table
|
|
||||||
height="100%"
|
|
||||||
ref="pageTableRef"
|
|
||||||
:page-api="dbApi.getDbBackups"
|
|
||||||
:show-selection="true"
|
|
||||||
v-model:selection-data="state.selectedData"
|
|
||||||
:searchItems="searchItems"
|
|
||||||
:before-query-fn="beforeQueryFn"
|
|
||||||
v-model:query-form="query"
|
|
||||||
:columns="columns"
|
|
||||||
>
|
|
||||||
<template #dbSelect>
|
|
||||||
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
|
||||||
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
|
||||||
</el-select>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #tableHeader>
|
|
||||||
<el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button>
|
|
||||||
<el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</el-button>
|
|
||||||
<el-button type="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
|
|
||||||
<el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #action="{ data }">
|
|
||||||
<div>
|
|
||||||
<el-button @click="editDbBackup(data)" type="primary" link>编辑</el-button>
|
|
||||||
<el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
|
|
||||||
<el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
|
|
||||||
<el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
|
|
||||||
<el-button @click="deleteDbBackup(data)" type="danger" link>删除</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</page-table>
|
|
||||||
|
|
||||||
<db-backup-edit
|
|
||||||
@val-change="search"
|
|
||||||
:title="dbBackupEditDialog.title"
|
|
||||||
:dbId="dbId"
|
|
||||||
:data="dbBackupEditDialog.data"
|
|
||||||
v-model:visible="dbBackupEditDialog.visible"
|
|
||||||
></db-backup-edit>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { toRefs, reactive, defineAsyncComponent, Ref, ref } from 'vue';
|
|
||||||
import { dbApi } from './api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
|
||||||
import { TableColumn } from '@/components/pagetable';
|
|
||||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
|
|
||||||
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
|
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
dbId: {
|
|
||||||
type: [Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
dbNames: {
|
|
||||||
type: [Array<String>],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
TableColumn.new('dbName', '数据库名称'),
|
|
||||||
TableColumn.new('name', '任务名称'),
|
|
||||||
TableColumn.new('startTime', '启动时间').isTime(),
|
|
||||||
TableColumn.new('intervalDay', '备份周期'),
|
|
||||||
TableColumn.new('enabledDesc', '是否启用'),
|
|
||||||
TableColumn.new('lastResult', '执行结果'),
|
|
||||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
|
|
||||||
];
|
|
||||||
|
|
||||||
const emptyQuery = {
|
|
||||||
dbId: 0,
|
|
||||||
dbName: '',
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
repeated: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
query: emptyQuery,
|
|
||||||
dbBackupEditDialog: {
|
|
||||||
visible: false,
|
|
||||||
data: null as any,
|
|
||||||
title: '创建数据库备份任务',
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 选中的数据
|
|
||||||
*/
|
|
||||||
selectedData: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const { query, dbBackupEditDialog } = toRefs(state);
|
|
||||||
|
|
||||||
const beforeQueryFn = (query: any) => {
|
|
||||||
query.dbId = props.dbId;
|
|
||||||
return query;
|
|
||||||
};
|
|
||||||
|
|
||||||
const search = async () => {
|
|
||||||
await pageTableRef.value.search();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createDbBackup = async () => {
|
|
||||||
state.dbBackupEditDialog.data = null;
|
|
||||||
state.dbBackupEditDialog.title = '创建数据库备份任务';
|
|
||||||
state.dbBackupEditDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const editDbBackup = async (data: any) => {
|
|
||||||
state.dbBackupEditDialog.data = data;
|
|
||||||
state.dbBackupEditDialog.title = '修改数据库备份任务';
|
|
||||||
state.dbBackupEditDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const enableDbBackup = async (data: any) => {
|
|
||||||
let backupId: String;
|
|
||||||
if (data) {
|
|
||||||
backupId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要启用的备份任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dbApi.enableDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('启用成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableDbBackup = async (data: any) => {
|
|
||||||
let backupId: String;
|
|
||||||
if (data) {
|
|
||||||
backupId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要禁用的备份任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dbApi.disableDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('禁用成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const startDbBackup = async (data: any) => {
|
|
||||||
let backupId: String;
|
|
||||||
if (data) {
|
|
||||||
backupId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要启用的备份任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dbApi.startDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('备份任务启动成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDbBackup = async (data: any) => {
|
|
||||||
let backupId: string;
|
|
||||||
if (data) {
|
|
||||||
backupId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要删除的数据库备份任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ElMessageBox.confirm(`确定删除 “数据库备份任务” 吗?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
await dbApi.deleteDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('删除成功');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-dialog :title="title" :model-value="visible" :before-close="cancel" :close-on-click-modal="false" width="38%">
|
|
||||||
<el-form :model="state.form" ref="restoreForm" label-width="auto" :rules="rules">
|
|
||||||
<el-form-item label="恢复方式">
|
|
||||||
<el-radio-group :disabled="state.editOrCreate" v-model="state.restoreMode">
|
|
||||||
<el-radio label="point-in-time">指定时间点</el-radio>
|
|
||||||
<el-radio label="backup-history">指定备份</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="dbName" label="数据库名称">
|
|
||||||
<el-select
|
|
||||||
:disabled="state.editOrCreate"
|
|
||||||
@change="changeDatabase"
|
|
||||||
v-model="state.form.dbName"
|
|
||||||
placeholder="数据库名称"
|
|
||||||
filterable
|
|
||||||
clearable
|
|
||||||
class="!w-full"
|
|
||||||
>
|
|
||||||
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="state.restoreMode == 'point-in-time'" prop="pointInTime" label="恢复时间点">
|
|
||||||
<el-date-picker :disabled="state.editOrCreate" v-model="state.form.pointInTime" type="datetime" placeholder="恢复时间点" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="state.restoreMode == 'backup-history'" prop="dbBackupHistoryId" label="数据库备份">
|
|
||||||
<el-select
|
|
||||||
:disabled="state.editOrCreate"
|
|
||||||
@change="changeHistory"
|
|
||||||
v-model="state.history"
|
|
||||||
value-key="id"
|
|
||||||
placeholder="数据库备份"
|
|
||||||
filterable
|
|
||||||
clearable
|
|
||||||
class="!w-full"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in state.histories"
|
|
||||||
:key="item.id"
|
|
||||||
:label="item.name + (item.binlogFileName ? ' ' : ' 不') + '支持指定时间点恢复'"
|
|
||||||
:value="item"
|
|
||||||
>
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="startTime" label="开始时间">
|
|
||||||
<el-date-picker :disabled="state.editOrCreate" v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="cancel()">取 消</el-button>
|
|
||||||
<el-button type="primary" :loading="state.btnLoading" @click="btnOk">确 定</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted, reactive, ref, watch } from 'vue';
|
|
||||||
import { dbApi } from './api';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
data: {
|
|
||||||
type: [Boolean, Object],
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
dbId: {
|
|
||||||
type: [Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
dbNames: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//定义事件
|
|
||||||
const emit = defineEmits(['cancel', 'val-change']);
|
|
||||||
|
|
||||||
const visible = defineModel<boolean>('visible', {
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const validatePointInTime = (rule: any, value: any, callback: any) => {
|
|
||||||
if (value > new Date()) {
|
|
||||||
callback(new Error('恢复时间点晚于当前时间'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!state.histories || state.histories.length == 0) {
|
|
||||||
callback(new Error('数据库没有备份记录'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let last = null;
|
|
||||||
for (const history of state.histories) {
|
|
||||||
if (!history.binlogFileName || history.binlogFileName.length === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (new Date(history.createTime) < value) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
last = history;
|
|
||||||
}
|
|
||||||
if (!last) {
|
|
||||||
callback(new Error('现有数据库备份不支持指定时间恢复'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
callback(last.name + ' 之前的数据库备份不支持指定时间恢复');
|
|
||||||
};
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
dbName: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择需要恢复的数据库',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pointInTime: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
validator: validatePointInTime,
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
dbBackupHistoryId: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择数据库备份',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
intervalDay: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
pattern: /^[1-9]\d*$/,
|
|
||||||
message: '请输入正整数',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
startTime: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择开始时间',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const restoreForm: any = ref(null);
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
form: {
|
|
||||||
id: 0,
|
|
||||||
dbId: 0,
|
|
||||||
dbName: null as any,
|
|
||||||
intervalDay: 0,
|
|
||||||
startTime: null as any,
|
|
||||||
repeated: null as any,
|
|
||||||
dbBackupId: null as any,
|
|
||||||
dbBackupHistoryId: null as any,
|
|
||||||
dbBackupHistoryName: null as any,
|
|
||||||
pointInTime: null as any,
|
|
||||||
},
|
|
||||||
btnLoading: false,
|
|
||||||
dbNamesSelected: [] as any,
|
|
||||||
dbNamesWithoutRestore: [] as any,
|
|
||||||
editOrCreate: false,
|
|
||||||
histories: [] as any,
|
|
||||||
history: null as any,
|
|
||||||
restoreMode: null as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await init(props.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(visible, (newValue: any) => {
|
|
||||||
if (newValue) {
|
|
||||||
init(props.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 改变表单中的数据库字段,方便表单错误提示。如全部删光,可提示请添加数据库
|
|
||||||
*/
|
|
||||||
const changeDatabase = async () => {
|
|
||||||
await getBackupHistories(props.dbId, state.form.dbName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeHistory = async () => {
|
|
||||||
if (state.history) {
|
|
||||||
state.form.dbBackupId = state.history.dbBackupId;
|
|
||||||
state.form.dbBackupHistoryId = state.history.id;
|
|
||||||
state.form.dbBackupHistoryName = state.history.name;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = async (data: any) => {
|
|
||||||
state.dbNamesSelected = [];
|
|
||||||
state.form.dbId = props.dbId;
|
|
||||||
if (data) {
|
|
||||||
state.editOrCreate = true;
|
|
||||||
state.dbNamesWithoutRestore = [data.dbName];
|
|
||||||
state.dbNamesSelected = [data.dbName];
|
|
||||||
state.form.id = data.id;
|
|
||||||
state.form.dbName = data.dbName;
|
|
||||||
state.form.intervalDay = data.intervalDay;
|
|
||||||
state.form.pointInTime = data.pointInTime;
|
|
||||||
state.form.startTime = data.startTime;
|
|
||||||
state.form.dbBackupId = data.dbBackupId;
|
|
||||||
state.form.dbBackupHistoryId = data.dbBackupHistoryId;
|
|
||||||
state.form.dbBackupHistoryName = data.dbBackupHistoryName;
|
|
||||||
if (data.pointInTime) {
|
|
||||||
state.restoreMode = 'point-in-time';
|
|
||||||
} else {
|
|
||||||
state.restoreMode = 'backup-history';
|
|
||||||
}
|
|
||||||
state.history = {
|
|
||||||
dbBackupId: data.dbBackupId,
|
|
||||||
id: data.dbBackupHistoryId,
|
|
||||||
name: data.dbBackupHistoryName,
|
|
||||||
createTime: data.createTime,
|
|
||||||
};
|
|
||||||
await getBackupHistories(props.dbId, data.dbName);
|
|
||||||
} else {
|
|
||||||
state.form.dbName = '';
|
|
||||||
state.editOrCreate = false;
|
|
||||||
state.form.intervalDay = 0;
|
|
||||||
state.form.repeated = false;
|
|
||||||
state.form.pointInTime = new Date();
|
|
||||||
state.form.startTime = new Date();
|
|
||||||
state.histories = [];
|
|
||||||
state.history = null;
|
|
||||||
state.restoreMode = 'point-in-time';
|
|
||||||
await getDbNamesWithoutRestore();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDbNamesWithoutRestore = async () => {
|
|
||||||
if (props.dbId > 0) {
|
|
||||||
state.dbNamesWithoutRestore = await dbApi.getDbNamesWithoutRestore.request({ dbId: props.dbId });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const btnOk = async () => {
|
|
||||||
restoreForm.value.validate(async (valid: any) => {
|
|
||||||
if (valid) {
|
|
||||||
await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (state.restoreMode == 'point-in-time') {
|
|
||||||
state.form.dbBackupId = 0;
|
|
||||||
state.form.dbBackupHistoryId = 0;
|
|
||||||
state.form.dbBackupHistoryName = '';
|
|
||||||
} else {
|
|
||||||
state.form.pointInTime = null;
|
|
||||||
}
|
|
||||||
state.form.repeated = false;
|
|
||||||
state.form.intervalDay = 0;
|
|
||||||
const reqForm = { ...state.form };
|
|
||||||
let api = dbApi.createDbRestore;
|
|
||||||
if (props.data) {
|
|
||||||
api = dbApi.saveDbRestore;
|
|
||||||
}
|
|
||||||
api.request(reqForm).then(() => {
|
|
||||||
ElMessage.success('成功创建数据库恢复任务');
|
|
||||||
emit('val-change', state.form);
|
|
||||||
state.btnLoading = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
state.btnLoading = false;
|
|
||||||
}, 1000);
|
|
||||||
cancel();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请正确填写信息');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
visible.value = false;
|
|
||||||
emit('cancel');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBackupHistories = async (dbId: Number, dbName: String) => {
|
|
||||||
if (!dbId || !dbName) {
|
|
||||||
state.histories = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await dbApi.getDbBackupHistories.request({ dbId, dbName });
|
|
||||||
if (!data || !data.list) {
|
|
||||||
ElMessage.error('该数据库没有备份记录,无法创建数据库恢复任务');
|
|
||||||
state.histories = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.histories = data.list;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="db-restore">
|
|
||||||
<page-table
|
|
||||||
height="100%"
|
|
||||||
ref="pageTableRef"
|
|
||||||
:page-api="dbApi.getDbRestores"
|
|
||||||
:show-selection="true"
|
|
||||||
v-model:selection-data="state.selectedData"
|
|
||||||
:searchItems="searchItems"
|
|
||||||
:before-query-fn="beforeQueryFn"
|
|
||||||
v-model:query-form="query"
|
|
||||||
:columns="columns"
|
|
||||||
>
|
|
||||||
<template #dbSelect>
|
|
||||||
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
|
||||||
<el-option v-for="item in dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
|
||||||
</el-select>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #tableHeader>
|
|
||||||
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
|
|
||||||
<el-button type="primary" icon="video-play" @click="enableDbRestore(null)">启用</el-button>
|
|
||||||
<el-button type="primary" icon="video-pause" @click="disableDbRestore(null)">禁用</el-button>
|
|
||||||
<el-button type="danger" icon="delete" @click="deleteDbRestore(null)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #action="{ data }">
|
|
||||||
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
|
|
||||||
<el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
|
|
||||||
<el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
|
|
||||||
<el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
|
|
||||||
</template>
|
|
||||||
</page-table>
|
|
||||||
|
|
||||||
<db-restore-edit
|
|
||||||
@val-change="search"
|
|
||||||
:title="dbRestoreEditDialog.title"
|
|
||||||
:dbId="dbId"
|
|
||||||
:dbNames="dbNames"
|
|
||||||
:data="dbRestoreEditDialog.data"
|
|
||||||
v-model:visible="dbRestoreEditDialog.visible"
|
|
||||||
></db-restore-edit>
|
|
||||||
|
|
||||||
<el-dialog v-model="infoDialog.visible" title="数据库恢复">
|
|
||||||
<el-descriptions :column="1" border>
|
|
||||||
<el-descriptions-item :span="1" label="数据库名称">{{ infoDialog.data.dbName }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item v-if="infoDialog.data.pointInTime" :span="1" label="恢复时间点">{{
|
|
||||||
formatDate(infoDialog.data.pointInTime)
|
|
||||||
}}</el-descriptions-item>
|
|
||||||
<el-descriptions-item v-if="!infoDialog.data.pointInTime" :span="1" label="数据库备份">{{
|
|
||||||
infoDialog.data.dbBackupHistoryName
|
|
||||||
}}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" label="开始时间">{{ formatDate(infoDialog.data.startTime) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" label="执行时间">{{ formatDate(infoDialog.data.lastTime) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { toRefs, reactive, defineAsyncComponent, Ref, ref } from 'vue';
|
|
||||||
import { dbApi } from './api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
|
||||||
import { TableColumn } from '@/components/pagetable';
|
|
||||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
import { formatDate } from '@/common/utils/format';
|
|
||||||
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
dbId: {
|
|
||||||
type: [Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
dbNames: {
|
|
||||||
type: [Array<String>],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// const queryConfig = [TableQuery.slot('dbName', '数据库名称', 'dbSelect')];
|
|
||||||
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
TableColumn.new('dbName', '数据库名称'),
|
|
||||||
TableColumn.new('startTime', '启动时间').isTime(),
|
|
||||||
TableColumn.new('enabledDesc', '是否启用'),
|
|
||||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
|
||||||
TableColumn.new('lastResult', '执行结果'),
|
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
|
|
||||||
];
|
|
||||||
|
|
||||||
const emptyQuery = {
|
|
||||||
dbId: props.dbId,
|
|
||||||
dbName: '',
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
repeated: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
query: emptyQuery,
|
|
||||||
dbRestoreEditDialog: {
|
|
||||||
visible: false,
|
|
||||||
data: null as any,
|
|
||||||
title: '创建数据库恢复任务',
|
|
||||||
},
|
|
||||||
infoDialog: {
|
|
||||||
visible: false,
|
|
||||||
data: null as any,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 选中的数据
|
|
||||||
*/
|
|
||||||
selectedData: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const { query, dbRestoreEditDialog, infoDialog } = toRefs(state);
|
|
||||||
|
|
||||||
const beforeQueryFn = (query: any) => {
|
|
||||||
query.dbId = props.dbId;
|
|
||||||
return query;
|
|
||||||
};
|
|
||||||
|
|
||||||
const search = async () => {
|
|
||||||
await pageTableRef.value.search();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createDbRestore = async () => {
|
|
||||||
state.dbRestoreEditDialog.data = null;
|
|
||||||
state.dbRestoreEditDialog.title = '数据库恢复';
|
|
||||||
state.dbRestoreEditDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDbRestore = async (data: any) => {
|
|
||||||
let restoreId: string;
|
|
||||||
if (data) {
|
|
||||||
restoreId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要删除的数据库恢复任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ElMessageBox.confirm(`确定删除 “数据库恢复任务” 吗?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
await dbApi.deleteDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('删除成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const showDbRestore = async (data: any) => {
|
|
||||||
state.infoDialog.data = data;
|
|
||||||
state.infoDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const enableDbRestore = async (data: any) => {
|
|
||||||
let restoreId: string;
|
|
||||||
if (data) {
|
|
||||||
restoreId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要启用的数据库恢复任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('启用成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableDbRestore = async (data: any) => {
|
|
||||||
let restoreId: string;
|
|
||||||
if (data) {
|
|
||||||
restoreId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要禁用的数据库恢复任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('禁用成功');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -59,36 +59,13 @@ export const dbApi = {
|
|||||||
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
||||||
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
||||||
saveDbRestore: Api.newPut('/dbs/{dbId}/restores/{id}'),
|
saveDbRestore: Api.newPut('/dbs/{dbId}/restores/{id}'),
|
||||||
|
|
||||||
// 数据同步相关
|
|
||||||
datasyncTasks: Api.newGet('/datasync/tasks'),
|
|
||||||
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler(async (param: any) => await encryptField(param, 'dataSql')),
|
|
||||||
getDatasyncTask: Api.newGet('/datasync/tasks/{taskId}'),
|
|
||||||
deleteDatasyncTask: Api.newDelete('/datasync/tasks/{taskId}/del'),
|
|
||||||
updateDatasyncTaskStatus: Api.newPost('/datasync/tasks/{taskId}/status'),
|
|
||||||
runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
|
|
||||||
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
|
|
||||||
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
|
|
||||||
|
|
||||||
// 数据库迁移相关
|
|
||||||
dbTransferTasks: Api.newGet('/dbTransfer'),
|
|
||||||
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
|
|
||||||
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
|
|
||||||
updateDbTransferTaskStatus: Api.newPost('/dbTransfer/{taskId}/status'),
|
|
||||||
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
|
|
||||||
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
|
|
||||||
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
|
|
||||||
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
|
|
||||||
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
|
|
||||||
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
|
|
||||||
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dbSqlExecApi = {
|
export const dbSqlExecApi = {
|
||||||
// 根据业务key获取sql执行信息
|
// 根据业务key获取sql执行信息
|
||||||
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
|
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
|
||||||
};
|
};
|
||||||
const encryptField = async (param: any, field: string) => {
|
export const encryptField = async (param: any, field: string) => {
|
||||||
// sql编码处理
|
// sql编码处理
|
||||||
if (!param['_encrypted'] && param[field]) {
|
if (!param['_encrypted'] && param[field]) {
|
||||||
// 判断是开发环境就打印sql
|
// 判断是开发环境就打印sql
|
||||||
|
|||||||
@@ -1,31 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<TagTreeResourceSelect
|
<ResourceSelect v-bind="$attrs" v-model="selectNode" @change="changeNode" :resource-type="TagResourceTypePath.Db" :tag-path-node-type="NodeTypeDbInst">
|
||||||
v-bind="$attrs"
|
|
||||||
v-model="selectNode"
|
|
||||||
@change="changeNode"
|
|
||||||
:resource-type="TagResourceTypePath.Db"
|
|
||||||
:tag-path-node-type="NodeTypeTagPath"
|
|
||||||
>
|
|
||||||
<template #iconPrefix>
|
<template #iconPrefix>
|
||||||
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
|
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
|
||||||
</template>
|
</template>
|
||||||
<template #prefix="{ data }">
|
</ResourceSelect>
|
||||||
<SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
|
|
||||||
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
|
||||||
</template>
|
|
||||||
</TagTreeResourceSelect>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
|
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
|
||||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||||
import { dbApi } from '@/views/ops/db/api';
|
import { dbApi } from '@/views/ops/db/api';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import { getDbDialect, schemaDbTypes } from '@/views/ops/db/dialect';
|
||||||
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
|
import ResourceSelect from '@/views/ops/resource/ResourceSelect.vue';
|
||||||
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
|
import NodeDbInst from '@/views/ops/db/resource/NodeDbInst.vue';
|
||||||
import { computed } from 'vue';
|
import NodeDb from '@/views/ops/db/resource/NodeDb.vue';
|
||||||
import { DbInst } from '../db';
|
import { DbIcon, SchemaIcon } from '@/views/ops/db/resource';
|
||||||
|
import { DbInst } from '@/views/ops/db/db';
|
||||||
|
|
||||||
const dbId = defineModel<number>('dbId');
|
const dbId = defineModel<number>('dbId');
|
||||||
const instName = defineModel<string>('instName');
|
const instName = defineModel<string>('instName');
|
||||||
@@ -35,20 +27,6 @@ const dbType = defineModel<string>('dbType');
|
|||||||
|
|
||||||
const emits = defineEmits(['selectDb']);
|
const emits = defineEmits(['selectDb']);
|
||||||
|
|
||||||
/**
|
|
||||||
* 树节点类型
|
|
||||||
*/
|
|
||||||
class SqlExecNodeType {
|
|
||||||
static DbInst = 1;
|
|
||||||
static Db = 2;
|
|
||||||
static TableMenu = 3;
|
|
||||||
static SqlMenu = 4;
|
|
||||||
static Table = 5;
|
|
||||||
static Sql = 6;
|
|
||||||
static PgSchemaMenu = 7;
|
|
||||||
static PgSchema = 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectNode = computed({
|
const selectNode = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return dbName.value ? `${tagPath.value} > ${instName.value} > ${dbName.value}` : '';
|
return dbName.value ? `${tagPath.value} > ${instName.value} > ${dbName.value}` : '';
|
||||||
@@ -58,90 +36,94 @@ const selectNode = computed({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const DbIcon = {
|
const NodeTypeDbInst = new NodeType(TagResourceTypeEnum.DbInstance.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
name: 'Coin',
|
const tagPath = parentNode.key;
|
||||||
color: '#67c23a',
|
|
||||||
};
|
|
||||||
|
|
||||||
// pgsql schema icon
|
const dbInstancesRes = await dbApi.instances.request({ tagPath, pageSize: 100 });
|
||||||
const SchemaIcon = {
|
const dbInstances = dbInstancesRes.list;
|
||||||
name: 'List',
|
if (!dbInstances) {
|
||||||
color: '#67c23a',
|
|
||||||
};
|
|
||||||
|
|
||||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
|
||||||
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
|
|
||||||
const dbInfos = dbInfoRes.list;
|
|
||||||
if (!dbInfos) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
return dbInfos?.map((x: any) => {
|
return dbInstances?.map((x: any) => {
|
||||||
x.tagPath = parentNode.key;
|
x.tagPath = tagPath;
|
||||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbConf).withParams(x).withNodeComponent(NodeDbInst);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/** mysql类型的数据库,没有schema层 */
|
const NodeTypeDbConf = new NodeType(TagResourceTypeEnum.Db.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const noSchemaType = (type: string) => {
|
const params = parentNode.params;
|
||||||
return noSchemaTypes.includes(type);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 数据库实例节点类型
|
const tagPath = params.tagPath;
|
||||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
const authCerts = {} as any;
|
||||||
|
for (let authCert of params.authCerts) {
|
||||||
|
authCerts[authCert.name] = authCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbInfoRes = await dbApi.dbs.request({
|
||||||
|
tagPath: `${tagPath}${TagResourceTypeEnum.DbInstance.value}|${params.code}`,
|
||||||
|
});
|
||||||
|
const dbInfos = dbInfoRes.list;
|
||||||
|
if (!dbInfos) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbInfos?.map((x: any) => {
|
||||||
|
x.tagPath = tagPath;
|
||||||
|
x.username = authCerts[x.authCertName]?.username;
|
||||||
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbs).withParams(x).withIcon(DbIcon).withNodeComponent(NodeDb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据库列表名类型
|
||||||
|
const NodeTypeDbs = new NodeType(222).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
const dbs = (await DbInst.getDbNames(params))?.sort();
|
const dbs = (await DbInst.getDbNames(params))?.sort();
|
||||||
let fn: NodeType;
|
const hasSchema = schemaDbTypes.includes(params.type);
|
||||||
if (noSchemaType(params.type)) {
|
const nodeType = hasSchema ? NodeTypeDbSchema : NodeTypeNoSchemaDb;
|
||||||
fn = MysqlNodeTypes;
|
|
||||||
} else {
|
|
||||||
fn = PgNodeTypes;
|
|
||||||
}
|
|
||||||
return dbs.map((x: any) => {
|
return dbs.map((x: any) => {
|
||||||
let tagTreeNode = new TagTreeNode(`${parentNode.key}.${x}`, `${x}`, fn)
|
return TagTreeNode.new(parentNode, `${parentNode.key}.${x}`, x, nodeType)
|
||||||
.withParams({
|
.withParams({
|
||||||
tagPath: params.tagPath,
|
tagPath: params.tagPath,
|
||||||
id: params.id,
|
id: params.id,
|
||||||
code: params.code,
|
|
||||||
instanceId: params.instanceId,
|
|
||||||
name: params.name,
|
name: params.name,
|
||||||
type: params.type,
|
type: params.type,
|
||||||
host: `${params.host}:${params.port}`,
|
host: `${params.host}:${params.port}`,
|
||||||
dbs: dbs,
|
dbs: dbs,
|
||||||
db: x,
|
db: x,
|
||||||
|
code: params.code,
|
||||||
})
|
})
|
||||||
.withIcon(DbIcon);
|
.withIcon(DbIcon)
|
||||||
if (noSchemaType(params.type)) {
|
.withIsLeaf(!hasSchema);
|
||||||
tagTreeNode.isLeaf = true;
|
|
||||||
}
|
|
||||||
return tagTreeNode;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 数据库节点
|
// 数据库节点
|
||||||
const PgNodeTypes = new NodeType(SqlExecNodeType.Db).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
const NodeTypeDbSchema = new NodeType(2).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
// pg类数据库会多一层schema
|
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
|
params.parentKey = parentNode.key;
|
||||||
const { id, db } = params;
|
const { id, db } = params;
|
||||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||||
|
const dbs = schemaNames.map((x: any) => `${db}/${x}`);
|
||||||
return schemaNames.map((sn: any) => {
|
return schemaNames.map((sn: any) => {
|
||||||
// 将db变更为 db/schema;
|
// 将db变更为 db/schema;
|
||||||
const nParams = { ...params };
|
const nParams = { ...params };
|
||||||
nParams.schema = sn;
|
nParams.schema = sn;
|
||||||
nParams.db = nParams.db + '/' + sn;
|
nParams.db = nParams.db + '/' + sn;
|
||||||
nParams.dbs = schemaNames;
|
nParams.dbs = dbs;
|
||||||
let tagTreeNode = new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
|
return TagTreeNode.new(parentNode, `${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema)
|
||||||
tagTreeNode.isLeaf = true;
|
.withParams(nParams)
|
||||||
return tagTreeNode;
|
.withIcon(SchemaIcon)
|
||||||
|
.withIsLeaf(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const MysqlNodeTypes = new NodeType(SqlExecNodeType.Db);
|
|
||||||
|
|
||||||
// postgres schema模式
|
// postgres schema模式
|
||||||
const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema);
|
const NodeTypePostgresSchema = new NodeType(99);
|
||||||
|
const NodeTypeNoSchemaDb = new NodeType(99);
|
||||||
|
|
||||||
const changeNode = (nodeData: TagTreeNode) => {
|
const changeNode = (nodeData: TagTreeNode) => {
|
||||||
const params = nodeData.params;
|
const params = nodeData.params;
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-row>
|
<el-row>
|
||||||
<span v-if="dt.hasUpdatedFileds" class="mt-1">
|
<span v-if="dt.hasUpdatedFields" class="mt-1">
|
||||||
<span>
|
<span>
|
||||||
<el-link type="success" underline="never" @click="submitUpdateFields(dt)"
|
<el-link type="success" underline="never" @click="submitUpdateFields(dt)"
|
||||||
><span style="font-size: 12px">{{ $t('common.submit') }}</span></el-link
|
><span style="font-size: 12px">{{ $t('common.submit') }}</span></el-link
|
||||||
@@ -200,7 +200,7 @@ class ExecResTab {
|
|||||||
/**
|
/**
|
||||||
* 是否有更新字段
|
* 是否有更新字段
|
||||||
*/
|
*/
|
||||||
hasUpdatedFileds: boolean;
|
hasUpdatedFields: boolean;
|
||||||
|
|
||||||
errorMsg: string;
|
errorMsg: string;
|
||||||
|
|
||||||
@@ -783,7 +783,7 @@ const getUploadSqlFileUrl = () => {
|
|||||||
|
|
||||||
const changeUpdatedField = (updatedFields: any, dt: ExecResTab) => {
|
const changeUpdatedField = (updatedFields: any, dt: ExecResTab) => {
|
||||||
// 如果存在要更新字段,则显示提交和取消按钮
|
// 如果存在要更新字段,则显示提交和取消按钮
|
||||||
dt.hasUpdatedFileds = updatedFields && updatedFields.size > 0;
|
dt.hasUpdatedFields = updatedFields && updatedFields.size > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,39 +19,3 @@ export const DbSqlExecStatusEnum = {
|
|||||||
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
||||||
Fail: EnumValue.of(-2, 'common.fail').setTagType('danger'),
|
Fail: EnumValue.of(-2, 'common.fail').setTagType('danger'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DbDataSyncDuplicateStrategyEnum = {
|
|
||||||
None: EnumValue.of(-1, 'db.none'),
|
|
||||||
Ignore: EnumValue.of(1, 'db.ignore'),
|
|
||||||
Replace: EnumValue.of(2, 'db.replace'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DbDataSyncRecentStateEnum = {
|
|
||||||
Success: EnumValue.of(1, 'common.success').setTagType('success'),
|
|
||||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DbDataSyncLogStatusEnum = {
|
|
||||||
Success: EnumValue.of(1, 'common.success').setTagType('success'),
|
|
||||||
Running: EnumValue.of(2, 'db.running').setTagType('primary'),
|
|
||||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DbDataSyncRunningStateEnum = {
|
|
||||||
Running: EnumValue.of(1, 'db.running').setTagType('success'),
|
|
||||||
WaitRun: EnumValue.of(2, 'db.waitRun').setTagType('primary'),
|
|
||||||
Stop: EnumValue.of(3, 'db.stop').setTagType('danger'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DbTransferRunningStateEnum = {
|
|
||||||
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
|
||||||
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
|
|
||||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
|
||||||
Stop: EnumValue.of(-2, 'db.stop').setTagType('warning'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DbTransferFileStatusEnum = {
|
|
||||||
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
|
|
||||||
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
|
||||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -16,18 +16,18 @@ const NodeDbInst = defineAsyncComponent(() => import('./NodeDbInst.vue'));
|
|||||||
const NodeDb = defineAsyncComponent(() => import('./NodeDb.vue'));
|
const NodeDb = defineAsyncComponent(() => import('./NodeDb.vue'));
|
||||||
const NodeDbTable = defineAsyncComponent(() => import('./NodeDbTable.vue'));
|
const NodeDbTable = defineAsyncComponent(() => import('./NodeDbTable.vue'));
|
||||||
|
|
||||||
const DbIcon = {
|
export const DbIcon = {
|
||||||
name: ResourceTypeEnum.Db.extra.icon,
|
name: ResourceTypeEnum.Db.extra.icon,
|
||||||
color: ResourceTypeEnum.Db.extra.iconColor,
|
color: ResourceTypeEnum.Db.extra.iconColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
// pgsql schema icon
|
// pgsql schema icon
|
||||||
const SchemaIcon = {
|
export const SchemaIcon = {
|
||||||
name: 'List',
|
name: 'List',
|
||||||
color: '#67c23a',
|
color: '#67c23a',
|
||||||
};
|
};
|
||||||
|
|
||||||
const TableIcon = {
|
export const TableIcon = {
|
||||||
name: 'icon db/table',
|
name: 'icon db/table',
|
||||||
color: '#409eff',
|
color: '#409eff',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default {
|
export default {
|
||||||
SyncTaskList: () => import('@/views/ops/db/SyncTaskList.vue'),
|
SyncTaskList: () => import('@/views/ops/db/sync/SyncTaskList.vue'),
|
||||||
DbTransferList: () => import('@/views/ops/db/DbTransferList.vue'),
|
DbTransferList: () => import('@/views/ops/db/transfer/DbTransferList.vue'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
<DrawerHeader :header="title" :back="cancel" />
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
<el-form :model="form" ref="dbForm" :rules="rules" label-position="top" label-width="auto">
|
||||||
<el-tabs v-model="tabActiveName">
|
<el-tabs v-model="tabActiveName">
|
||||||
<el-tab-pane :label="$t('common.basic')" :name="basicTab">
|
<el-tab-pane :label="$t('common.basic')" :name="basicTab">
|
||||||
<el-row>
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
||||||
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-form-item prop="status" :label="$t('common.status')" label-width="60" required>
|
<el-form-item prop="status" :label="$t('common.status')" label-position="left" label-width="60" required>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="form.status"
|
v-model="form.status"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<monaco-editor height="200px" class="task-sql" language="sql" v-model="form.dataSql" />
|
<monaco-editor height="200px" class="task-sql" language="sql" v-model="form.dataSql" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-row>
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="targetTableName" :label="$t('db.targetDbTable')" required>
|
<el-form-item prop="targetTableName" :label="$t('db.targetDbTable')" required>
|
||||||
<el-select v-model="form.targetTableName" filterable>
|
<el-select v-model="form.targetTableName" filterable>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row>
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTooltip :label="$t('db.updateField')" prop="updField" :tooltip="$t('db.updateFieldTips')">
|
<FormItemTooltip :label="$t('db.updateField')" prop="updField" :tooltip="$t('db.updateFieldTips')">
|
||||||
<el-input v-model.trim="form.updField" :placeholder="$t('db.updateFiledPlaceholder')" auto-complete="off" />
|
<el-input v-model.trim="form.updField" :placeholder="$t('db.updateFiledPlaceholder')" auto-complete="off" />
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row>
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTooltip :label="$t('db.fieldValueSrc')" prop="updFieldSrc" :tooltip="$t('db.fieldValueSrcTips')">
|
<FormItemTooltip :label="$t('db.fieldValueSrc')" prop="updFieldSrc" :tooltip="$t('db.fieldValueSrcTips')">
|
||||||
<el-input v-model.trim="form.updFieldSrc" :placeholder="$t('db.fieldValueSrcPlaceholder')" auto-complete="off" />
|
<el-input v-model.trim="form.updFieldSrc" :placeholder="$t('db.fieldValueSrcPlaceholder')" auto-complete="off" />
|
||||||
@@ -105,17 +105,32 @@
|
|||||||
|
|
||||||
<el-tab-pane :label="$t('db.fieldMap')" :name="fieldTab" :disabled="!baseFieldCompleted">
|
<el-tab-pane :label="$t('db.fieldMap')" :name="fieldTab" :disabled="!baseFieldCompleted">
|
||||||
<el-form-item prop="fieldMap" :label="$t('db.fieldMap')" required>
|
<el-form-item prop="fieldMap" :label="$t('db.fieldMap')" required>
|
||||||
<el-table :data="form.fieldMap" :max-height="fieldMapTableHeight" size="small">
|
<el-table :data="form.fieldMap" :max-height="fieldMapTableHeight">
|
||||||
<el-table-column prop="src" :label="$t('db.srcField')" :width="200" />
|
<el-table-column prop="src" :label="$t('db.srcField')" :width="200"></el-table-column>
|
||||||
<el-table-column prop="target" :label="$t('db.targetField')">
|
<el-table-column prop="target" :label="$t('db.targetField')">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-select v-model="scope.row.target" allow-create filterable>
|
<el-select v-model="scope.row.target" allow-create filterable>
|
||||||
|
<template #label="{ label, value }">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<el-text tag="b">{{ value }}</el-text>
|
||||||
|
<el-text size="small">{{ label }}</el-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in state.targetColumnList"
|
v-for="item in state.targetColumnList"
|
||||||
:key="item.columnName"
|
:key="item.columnName"
|
||||||
:label="item.columnName + ` ${item.columnType}` + (item.columnComment && ' - ' + item.columnComment)"
|
:label="`${item.columnType}${item.columnComment && ' - ' + item.columnComment}`"
|
||||||
:value="item.columnName"
|
:value="item.columnName"
|
||||||
/>
|
>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
{{ item.columnName }}
|
||||||
|
|
||||||
|
<el-text size="small">
|
||||||
|
{{ item.columnType }}{{ item.columnComment && ' - ' + item.columnComment }}
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -194,7 +209,6 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, reactive, ref, toRefs, watch } from 'vue';
|
import { computed, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { dbApi } from './api';
|
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
@@ -203,11 +217,13 @@ import { compatibleDuplicateStrategy, DbType, getDbDialect } from '@/views/ops/d
|
|||||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||||
import { DbDataSyncDuplicateStrategyEnum } from './enums';
|
|
||||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
|
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
|
||||||
import { Rules } from '@/common/rule';
|
import { Rules } from '@/common/rule';
|
||||||
|
import { DbDataSyncDuplicateStrategyEnum } from '@/views/ops/db/sync/enums';
|
||||||
|
import { dbSyncApi } from '@/views/ops/db/sync/api';
|
||||||
|
import { dbApi } from '@/views/ops/db/api';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -291,7 +307,7 @@ const state = reactive({
|
|||||||
|
|
||||||
const { tabActiveName, form, submitForm, fieldMapTableHeight } = toRefs(state);
|
const { tabActiveName, form, submitForm, fieldMapTableHeight } = toRefs(state);
|
||||||
|
|
||||||
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDatasyncTask.useApi(submitForm);
|
const { isFetching: saveBtnLoading, execute: saveExec } = dbSyncApi.saveDatasyncTask.useApi(submitForm);
|
||||||
|
|
||||||
// 基础字段信息是否填写完整
|
// 基础字段信息是否填写完整
|
||||||
const baseFieldCompleted = computed(() => {
|
const baseFieldCompleted = computed(() => {
|
||||||
@@ -305,13 +321,13 @@ watch(dialogVisible, async (newValue: boolean) => {
|
|||||||
state.tabActiveName = 'basic';
|
state.tabActiveName = 'basic';
|
||||||
const propsData = props.data as any;
|
const propsData = props.data as any;
|
||||||
if (!propsData?.id) {
|
if (!propsData?.id) {
|
||||||
let d = {} as FormData;
|
let d = { taskCron: '' } as FormData;
|
||||||
Object.assign(d, basicFormData);
|
Object.assign(d, basicFormData);
|
||||||
state.form = d;
|
state.form = d;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = await dbApi.getDatasyncTask.request({ taskId: propsData?.id });
|
let data = await dbSyncApi.getDatasyncTask.request({ taskId: propsData?.id });
|
||||||
state.form = data;
|
state.form = data;
|
||||||
if (!state.form.duplicateStrategy) {
|
if (!state.form.duplicateStrategy) {
|
||||||
state.form.duplicateStrategy = -1;
|
state.form.duplicateStrategy = -1;
|
||||||
@@ -401,6 +417,7 @@ const refreshPreviewInsertSql = () => {
|
|||||||
const onSelectSrcDb = async (params: any) => {
|
const onSelectSrcDb = async (params: any) => {
|
||||||
// 初始化数据源
|
// 初始化数据源
|
||||||
params.databases = params.dbs; // 数据源里需要这个值
|
params.databases = params.dbs; // 数据源里需要这个值
|
||||||
|
console.log(params.dbs);
|
||||||
state.srcDbInst = await DbInst.getOrNewInst(params);
|
state.srcDbInst = await DbInst.getOrNewInst(params);
|
||||||
registerDbCompletionItemProvider(params.id, params.db, params.dbs, params.type);
|
registerDbCompletionItemProvider(params.id, params.db, params.dbs, params.type);
|
||||||
};
|
};
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
:page-api="dbApi.datasyncTasks"
|
:page-api="dbSyncApi.datasyncTasks"
|
||||||
:searchItems="searchItems"
|
:searchItems="searchItems"
|
||||||
v-model:query-form="query"
|
v-model:query-form="query"
|
||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
@@ -50,13 +50,13 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||||
import { dbApi } from './api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { hasPerms } from '@/components/auth/auth';
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||||
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from './enums';
|
|
||||||
import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
|
import { dbSyncApi } from '@/views/ops/db/sync/api';
|
||||||
|
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from '@/views/ops/db/sync/enums';
|
||||||
|
|
||||||
const DataSyncTaskEdit = defineAsyncComponent(() => import('./SyncTaskEdit.vue'));
|
const DataSyncTaskEdit = defineAsyncComponent(() => import('./SyncTaskEdit.vue'));
|
||||||
const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue'));
|
const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue'));
|
||||||
@@ -143,14 +143,14 @@ const edit = async (data: any) => {
|
|||||||
|
|
||||||
const run = async (id: any) => {
|
const run = async (id: any) => {
|
||||||
await useI18nConfirm('db.runConfirm');
|
await useI18nConfirm('db.runConfirm');
|
||||||
await dbApi.runDatasyncTask.request({ taskId: id });
|
await dbSyncApi.runDatasyncTask.request({ taskId: id });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
setTimeout(search, 1000);
|
setTimeout(search, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stop = async (id: any) => {
|
const stop = async (id: any) => {
|
||||||
await useI18nConfirm('db.stopConfirm');
|
await useI18nConfirm('db.stopConfirm');
|
||||||
await dbApi.stopDatasyncTask.request({ taskId: id });
|
await dbSyncApi.stopDatasyncTask.request({ taskId: id });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
search();
|
search();
|
||||||
};
|
};
|
||||||
@@ -163,7 +163,7 @@ const log = async (data: any) => {
|
|||||||
|
|
||||||
const updStatus = async (id: any, status: 1 | -1) => {
|
const updStatus = async (id: any, status: 1 | -1) => {
|
||||||
try {
|
try {
|
||||||
await dbApi.updateDatasyncTaskStatus.request({ taskId: id, status });
|
await dbSyncApi.updateDatasyncTaskStatus.request({ taskId: id, status });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -174,7 +174,7 @@ const updStatus = async (id: any, status: 1 | -1) => {
|
|||||||
const del = async () => {
|
const del = async () => {
|
||||||
try {
|
try {
|
||||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
|
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
|
||||||
await dbApi.deleteDatasyncTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
await dbSyncApi.deleteDatasyncTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||||
useI18nDeleteSuccessMsg();
|
useI18nDeleteSuccessMsg();
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<el-switch v-model="realTime" @change="watchPolling" inline-prompt :active-text="$t('db.realTime')" :inactive-text="$t('db.noRealTime')" />
|
<el-switch v-model="realTime" @change="watchPolling" inline-prompt :active-text="$t('db.realTime')" :inactive-text="$t('db.noRealTime')" />
|
||||||
<el-button @click="search" icon="Refresh" circle size="small" :loading="realTime" class="ml-2"></el-button>
|
<el-button @click="search" icon="Refresh" circle size="small" :loading="realTime" class="ml-2"></el-button>
|
||||||
</template>
|
</template>
|
||||||
<page-table ref="logTableRef" :page-api="dbApi.datasyncLogs" v-model:query-form="query" :tool-button="false" :columns="columns" size="small">
|
<page-table ref="logTableRef" :page-api="dbSyncApi.datasyncLogs" v-model:query-form="query" :tool-button="false" :columns="columns" size="small">
|
||||||
</page-table>
|
</page-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, Ref, ref, toRefs, watch } from 'vue';
|
import { reactive, Ref, ref, toRefs, watch } from 'vue';
|
||||||
import { dbApi } from '@/views/ops/db/api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { DbDataSyncLogStatusEnum } from './enums';
|
import { dbSyncApi } from '@/views/ops/db/sync/api';
|
||||||
|
import { DbDataSyncLogStatusEnum } from '@/views/ops/db/sync/enums';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
taskId: {
|
taskId: {
|
||||||
14
frontend/src/views/ops/db/sync/api.ts
Normal file
14
frontend/src/views/ops/db/sync/api.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Api from '@/common/Api';
|
||||||
|
import { encryptField } from '@/views/ops/db/api';
|
||||||
|
|
||||||
|
export const dbSyncApi = {
|
||||||
|
// 数据同步相关
|
||||||
|
datasyncTasks: Api.newGet('/datasync/tasks'),
|
||||||
|
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler(async (param: any) => await encryptField(param, 'dataSql')),
|
||||||
|
getDatasyncTask: Api.newGet('/datasync/tasks/{taskId}'),
|
||||||
|
deleteDatasyncTask: Api.newDelete('/datasync/tasks/{taskId}/del'),
|
||||||
|
updateDatasyncTaskStatus: Api.newPost('/datasync/tasks/{taskId}/status'),
|
||||||
|
runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
|
||||||
|
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
|
||||||
|
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
|
||||||
|
};
|
||||||
24
frontend/src/views/ops/db/sync/enums.ts
Normal file
24
frontend/src/views/ops/db/sync/enums.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { EnumValue } from '@/common/Enum';
|
||||||
|
|
||||||
|
export const DbDataSyncDuplicateStrategyEnum = {
|
||||||
|
None: EnumValue.of(-1, 'db.none'),
|
||||||
|
Ignore: EnumValue.of(1, 'db.ignore'),
|
||||||
|
Replace: EnumValue.of(2, 'db.replace'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DbDataSyncRecentStateEnum = {
|
||||||
|
Success: EnumValue.of(1, 'common.success').setTagType('success'),
|
||||||
|
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DbDataSyncLogStatusEnum = {
|
||||||
|
Success: EnumValue.of(1, 'common.success').setTagType('success'),
|
||||||
|
Running: EnumValue.of(2, 'db.running').setTagType('primary'),
|
||||||
|
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DbDataSyncRunningStateEnum = {
|
||||||
|
Running: EnumValue.of(1, 'db.running').setTagType('success'),
|
||||||
|
WaitRun: EnumValue.of(2, 'db.waitRun').setTagType('primary'),
|
||||||
|
Stop: EnumValue.of(3, 'db.stop').setTagType('danger'),
|
||||||
|
};
|
||||||
1
frontend/src/views/ops/db/sync/readme.txt
Normal file
1
frontend/src/views/ops/db/sync/readme.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Db sync (数据库迁移模块)
|
||||||
@@ -5,17 +5,16 @@
|
|||||||
<DrawerHeader :header="title" :back="cancel" />
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
<el-form :model="form" ref="dbForm" :rules="rules" label-position="top" label-width="auto">
|
||||||
<el-divider content-position="left">{{ $t('common.basic') }}</el-divider>
|
<el-divider content-position="left">{{ $t('common.basic') }}</el-divider>
|
||||||
|
|
||||||
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
||||||
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-row class="w-full!">
|
||||||
<el-row class="!w-full">
|
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="status" :label="$t('common.status')">
|
<el-form-item prop="status" :label="$t('common.status')" label-position="left">
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="form.status"
|
v-model="form.status"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
@@ -28,7 +27,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="cronAble" :label="$t('db.cronAble')" required>
|
<el-form-item prop="cronAble" :label="$t('db.cronAble')" required label-position="left">
|
||||||
<el-radio-group v-model="form.cronAble">
|
<el-radio-group v-model="form.cronAble">
|
||||||
<el-radio :label="$t('common.yes')" :value="1" />
|
<el-radio :label="$t('common.yes')" :value="1" />
|
||||||
<el-radio :label="$t('common.no')" :value="-1" />
|
<el-radio :label="$t('common.no')" :value="-1" />
|
||||||
@@ -36,13 +35,12 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item prop="cron" label="cron" :required="form.cronAble == 1">
|
<el-form-item prop="cron" label="cron" :required="form.cronAble == 1">
|
||||||
<CrontabInput v-model="form.cron" />
|
<CrontabInput v-model="form.cron" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="srcDbId" :label="$t('db.srcDb')" class="!w-full" required>
|
<el-form-item prop="srcDbId" :label="$t('db.srcDb')" class="w-full!" required>
|
||||||
<db-select-tree
|
<db-select-tree
|
||||||
v-model:db-id="form.srcDbId"
|
v-model:db-id="form.srcDbId"
|
||||||
v-model:inst-name="form.srcInstName"
|
v-model:inst-name="form.srcInstName"
|
||||||
@@ -61,7 +59,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item v-if="form.mode === 2">
|
<el-form-item v-if="form.mode === 2">
|
||||||
<el-row class="!w-full">
|
<el-row class="w-full!">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="targetFileDbType" :label="$t('db.dbFileType')" :required="form.mode === 2">
|
<el-form-item prop="targetFileDbType" :label="$t('db.dbFileType')" :required="form.mode === 2">
|
||||||
<el-select v-model="form.targetFileDbType" clearable filterable>
|
<el-select v-model="form.targetFileDbType" clearable filterable>
|
||||||
@@ -100,7 +98,7 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item v-if="form.mode == 1" prop="targetDbId" :label="$t('db.targetDb')" class="!w-full" :required="form.mode === 1">
|
<el-form-item v-if="form.mode == 1" prop="targetDbId" :label="$t('db.targetDb')" class="w-full!" :required="form.mode === 1">
|
||||||
<db-select-tree
|
<db-select-tree
|
||||||
v-model:db-id="form.targetDbId"
|
v-model:db-id="form.targetDbId"
|
||||||
v-model:inst-name="form.targetInstName"
|
v-model:inst-name="form.targetInstName"
|
||||||
@@ -123,11 +121,11 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-input v-model="state.filterSrcTableText" placeholder="filter table" size="small" />
|
<el-input v-model="state.filterSrcTableText" placeholder="filter table" size="small" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item class="!w-full">
|
<el-form-item class="w-full!">
|
||||||
<el-tree
|
<el-tree
|
||||||
ref="srcTreeRef"
|
ref="srcTreeRef"
|
||||||
class="!w-full"
|
class="w-full! overflow-y-auto"
|
||||||
style="max-height: 200px; overflow-y: auto"
|
style="max-height: 200px"
|
||||||
default-expand-all
|
default-expand-all
|
||||||
:expand-on-click-node="false"
|
:expand-on-click-node="false"
|
||||||
:data="state.srcTableTree"
|
:data="state.srcTableTree"
|
||||||
@@ -151,7 +149,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, reactive, ref, toRefs, watch } from 'vue';
|
import { nextTick, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { dbApi } from './api';
|
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||||
@@ -162,6 +160,8 @@ import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Rules } from '@/common/rule';
|
import { Rules } from '@/common/rule';
|
||||||
import { deepClone } from '@/common/utils/object';
|
import { deepClone } from '@/common/utils/object';
|
||||||
|
import { dbApi } from '@/views/ops/db/api';
|
||||||
|
import { dbTransferApi } from '@/views/ops/db/transfer/api';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ const state = reactive({
|
|||||||
|
|
||||||
const { form, submitForm } = toRefs(state);
|
const { form, submitForm } = toRefs(state);
|
||||||
|
|
||||||
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDbTransferTask.useApi(submitForm);
|
const { isFetching: saveBtnLoading, execute: saveExec } = dbTransferApi.saveDbTransferTask.useApi(submitForm);
|
||||||
|
|
||||||
watch(dialogVisible, async (newValue: boolean) => {
|
watch(dialogVisible, async (newValue: boolean) => {
|
||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
v-model:query-form="state.query"
|
v-model:query-form="state.query"
|
||||||
:page-api="dbApi.dbTransferFileList"
|
:page-api="dbTransferApi.dbTransferFileList"
|
||||||
:lazy="true"
|
:lazy="true"
|
||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
v-model:selection-data="state.selectionData"
|
v-model:selection-data="state.selectionData"
|
||||||
@@ -79,7 +79,6 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue';
|
import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue';
|
||||||
import { dbApi } from '@/views/ops/db/api';
|
|
||||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
@@ -89,10 +88,11 @@ import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
|||||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import { getClientId } from '@/common/utils/storage';
|
import { getClientId } from '@/common/utils/storage';
|
||||||
import FileInfo from '@/components/file/FileInfo.vue';
|
import FileInfo from '@/components/file/FileInfo.vue';
|
||||||
import { DbTransferFileStatusEnum } from './enums';
|
|
||||||
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Rules } from '@/common/rule';
|
import { Rules } from '@/common/rule';
|
||||||
|
import { dbTransferApi } from '@/views/ops/db/transfer/api';
|
||||||
|
import { DbTransferFileStatusEnum } from '@/views/ops/db/transfer/enums';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ const state = reactive({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
state.runDialog.runForm.clientId = getClientId();
|
state.runDialog.runForm.clientId = getClientId();
|
||||||
await dbApi.dbTransferFileRun.request(state.runDialog.runForm);
|
await dbTransferApi.dbTransferFileRun.request(state.runDialog.runForm);
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
state.runDialog.onCancel();
|
state.runDialog.onCancel();
|
||||||
await search();
|
await search();
|
||||||
@@ -196,7 +196,7 @@ const state = reactive({
|
|||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
pageTableRef.value?.search();
|
pageTableRef.value?.search();
|
||||||
// const { total, list } = await dbApi.dbTransferFileList.request(state.query);
|
// const { total, list } = await dbTransferApi.dbTransferFileList.request(state.query);
|
||||||
// state.tableData = list;
|
// state.tableData = list;
|
||||||
// pageTableRef.value.total = total;
|
// pageTableRef.value.total = total;
|
||||||
};
|
};
|
||||||
@@ -204,7 +204,7 @@ const search = async () => {
|
|||||||
const onDel = async function () {
|
const onDel = async function () {
|
||||||
try {
|
try {
|
||||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).join('、'));
|
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).join('、'));
|
||||||
await dbApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
|
await dbTransferApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||||
useI18nDeleteSuccessMsg();
|
useI18nDeleteSuccessMsg();
|
||||||
await search();
|
await search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
:page-api="dbApi.dbTransferTasks"
|
:page-api="dbTransferApi.dbTransferTasks"
|
||||||
:searchItems="searchItems"
|
:searchItems="searchItems"
|
||||||
v-model:query-form="query"
|
v-model:query-form="query"
|
||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
@@ -84,19 +84,19 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||||
import { dbApi } from './api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { hasPerms } from '@/components/auth/auth';
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||||
import { DbTransferRunningStateEnum } from './enums';
|
|
||||||
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
||||||
import DbTransferFile from './DbTransferFile.vue';
|
|
||||||
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { dbTransferApi } from '@/views/ops/db/transfer/api';
|
||||||
|
import { DbTransferRunningStateEnum } from '@/views/ops/db/transfer/enums';
|
||||||
|
|
||||||
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
|
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
|
||||||
|
const DbTransferFile = defineAsyncComponent(() => import('./DbTransferFile.vue'));
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ const edit = async (data: any) => {
|
|||||||
|
|
||||||
const stop = async (id: any) => {
|
const stop = async (id: any) => {
|
||||||
await useI18nConfirm('db.stopConfirm');
|
await useI18nConfirm('db.stopConfirm');
|
||||||
await dbApi.stopDbTransferTask.request({ taskId: id });
|
await dbTransferApi.stopDbTransferTask.request({ taskId: id });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
search();
|
search();
|
||||||
};
|
};
|
||||||
@@ -204,7 +204,7 @@ const onOpenLog = (data: any) => {
|
|||||||
const onReRun = async (data: any) => {
|
const onReRun = async (data: any) => {
|
||||||
await useI18nConfirm('db.runConfirm');
|
await useI18nConfirm('db.runConfirm');
|
||||||
try {
|
try {
|
||||||
let res = await dbApi.runDbTransferTask.request({ taskId: data.id });
|
let res = await dbTransferApi.runDbTransferTask.request({ taskId: data.id });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
// 拿到日志id之后,弹出日志弹窗
|
// 拿到日志id之后,弹出日志弹窗
|
||||||
onOpenLog({ logId: res, state: DbTransferRunningStateEnum.Running.value });
|
onOpenLog({ logId: res, state: DbTransferRunningStateEnum.Running.value });
|
||||||
@@ -225,7 +225,7 @@ const openFiles = async (data: any) => {
|
|||||||
};
|
};
|
||||||
const updStatus = async (id: any, status: 1 | -1) => {
|
const updStatus = async (id: any, status: 1 | -1) => {
|
||||||
try {
|
try {
|
||||||
await dbApi.updateDbTransferTaskStatus.request({ taskId: id, status });
|
await dbTransferApi.updateDbTransferTaskStatus.request({ taskId: id, status });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -236,7 +236,7 @@ const updStatus = async (id: any, status: 1 | -1) => {
|
|||||||
const del = async () => {
|
const del = async () => {
|
||||||
try {
|
try {
|
||||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
|
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
|
||||||
await dbApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
await dbTransferApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||||
useI18nDeleteSuccessMsg();
|
useI18nDeleteSuccessMsg();
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
16
frontend/src/views/ops/db/transfer/api.ts
Normal file
16
frontend/src/views/ops/db/transfer/api.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Api from '@/common/Api';
|
||||||
|
|
||||||
|
export const dbTransferApi = {
|
||||||
|
// 数据库迁移相关
|
||||||
|
dbTransferTasks: Api.newGet('/dbTransfer'),
|
||||||
|
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
|
||||||
|
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
|
||||||
|
updateDbTransferTaskStatus: Api.newPost('/dbTransfer/{taskId}/status'),
|
||||||
|
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
|
||||||
|
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
|
||||||
|
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
|
||||||
|
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
|
||||||
|
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
|
||||||
|
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
|
||||||
|
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
|
||||||
|
};
|
||||||
14
frontend/src/views/ops/db/transfer/enums.ts
Normal file
14
frontend/src/views/ops/db/transfer/enums.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { EnumValue } from '@/common/Enum';
|
||||||
|
|
||||||
|
export const DbTransferRunningStateEnum = {
|
||||||
|
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
||||||
|
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
|
||||||
|
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||||
|
Stop: EnumValue.of(-2, 'db.stop').setTagType('warning'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DbTransferFileStatusEnum = {
|
||||||
|
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
|
||||||
|
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
||||||
|
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||||
|
};
|
||||||
1
frontend/src/views/ops/db/transfer/readme.txt
Normal file
1
frontend/src/views/ops/db/transfer/readme.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Db transfer (数据库迁移模块)
|
||||||
@@ -33,13 +33,9 @@
|
|||||||
|
|
||||||
<el-descriptions-item :span="3" :label="$t('tag.relateTag')"><ResourceTags :tags="detailDialog.data.tags" /></el-descriptions-item>
|
<el-descriptions-item :span="3" :label="$t('tag.relateTag')"><ResourceTags :tags="detailDialog.data.tags" /></el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="3" label="Host">{{ detailDialog.data.host }}</el-descriptions-item>
|
<el-descriptions-item :span="3" :label="$t('docker.addr')">{{ detailDialog.data.addr }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item :span="3" label="DB">{{ detailDialog.data.db }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="3" :label="$t('common.remark')">{{ detailDialog.data.remark }}</el-descriptions-item>
|
<el-descriptions-item :span="3" :label="$t('common.remark')">{{ detailDialog.data.remark }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="3" :label="$t('machine.sshTunnel')">
|
|
||||||
{{ detailDialog.data.sshTunnelMachineId > 0 ? $t('common.yes') : $t('common.no') }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item :span="2" :label="$t('common.createTime')">{{ formatDate(detailDialog.data.createTime) }} </el-descriptions-item>
|
<el-descriptions-item :span="2" :label="$t('common.createTime')">{{ formatDate(detailDialog.data.createTime) }} </el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" :label="$t('common.creator')">{{ detailDialog.data.creator }}</el-descriptions-item>
|
<el-descriptions-item :span="1" :label="$t('common.creator')">{{ detailDialog.data.creator }}</el-descriptions-item>
|
||||||
|
|||||||
@@ -19,6 +19,13 @@
|
|||||||
<el-form-item prop="version" :label="t('common.version')">
|
<el-form-item prop="version" :label="t('common.version')">
|
||||||
<el-input v-model.trim="form.version" auto-complete="off" disabled></el-input>
|
<el-input v-model.trim="form.version" auto-complete="off" disabled></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<!-- 增加协议下拉框 http和https,默认http-->
|
||||||
|
<el-form-item prop="protocol" :label="t('es.protocol')">
|
||||||
|
<el-select v-model="form.protocol" placeholder="http">
|
||||||
|
<el-option label="http" value="http"></el-option>
|
||||||
|
<el-option label="https" value="https"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="host" label="Host" required>
|
<el-form-item prop="host" label="Host" required>
|
||||||
<el-col :span="18">
|
<el-col :span="18">
|
||||||
@@ -105,6 +112,7 @@ const DefaultForm = {
|
|||||||
id: null,
|
id: null,
|
||||||
code: '',
|
code: '',
|
||||||
name: null,
|
name: null,
|
||||||
|
protocol: 'http',
|
||||||
host: '',
|
host: '',
|
||||||
version: '',
|
version: '',
|
||||||
port: 9200,
|
port: 9200,
|
||||||
|
|||||||
@@ -569,9 +569,9 @@ const parseParams = () => {
|
|||||||
|
|
||||||
// minimum_should_match 需要结合should使用,默认为1,表示至少一个should条件满足
|
// minimum_should_match 需要结合should使用,默认为1,表示至少一个should条件满足
|
||||||
if (should.length > 0) {
|
if (should.length > 0) {
|
||||||
state.search['minimum_should_match'] = Math.max(1, state.minimum_should_match);
|
state.search.query.bool['minimum_should_match'] = Math.max(1, state.minimum_should_match);
|
||||||
} else {
|
} else {
|
||||||
delete state.search['minimum_should_match'];
|
delete state.search.query.bool['minimum_should_match'];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<el-row class="mb-2 ml-4">
|
<el-row class="mb-2 ml-4">
|
||||||
<el-breadcrumb separator-icon="ArrowRight">
|
<el-breadcrumb separator-icon="ArrowRight">
|
||||||
<el-breadcrumb-item v-for="path in filePathNav" :key="path">
|
<el-breadcrumb-item v-for="path in filePathNav" :key="path">
|
||||||
<el-link @click="setFiles(path.path)" style="font-weight: bold">{{ path.name }}</el-link>
|
<el-link @click="setFiles(path.path)" class="!cursor-pointer !font-bold">{{ path.name }}</el-link>
|
||||||
</el-breadcrumb-item>
|
</el-breadcrumb-item>
|
||||||
</el-breadcrumb>
|
</el-breadcrumb>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
</el-popover>
|
</el-popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div :ref="(el: any) => setTerminalWrapperRef(el, dt.key)" class="terminal-wrapper flex-1 h-[calc(100vh-155px)]">
|
<div class="terminal-wrapper flex-1 h-[calc(100vh-155px)]">
|
||||||
<TerminalBody
|
<TerminalBody
|
||||||
v-if="dt.params.protocol == MachineProtocolEnum.Ssh.value"
|
v-if="dt.params.protocol == MachineProtocolEnum.Ssh.value"
|
||||||
:mount-init="false"
|
:mount-init="false"
|
||||||
@@ -161,11 +161,6 @@ const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
|||||||
|
|
||||||
const emits = defineEmits(['init']);
|
const emits = defineEmits(['init']);
|
||||||
|
|
||||||
class MachineNodeType {
|
|
||||||
static Machine = 1;
|
|
||||||
static AuthCert = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -294,10 +289,11 @@ const openTerminal = (machine: any, ex?: boolean) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
state.tabs.set(key, tab);
|
state.tabs.set(key, tab);
|
||||||
state.activeTermName = key;
|
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
handleReconnect(tab);
|
handleReconnect(tab);
|
||||||
|
state.activeTermName = key;
|
||||||
|
setTimeout(() => fitTerminal(), 300);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -398,17 +394,6 @@ const setTerminalRef = (el: any, key: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const terminalWrapperRefs: any = {};
|
|
||||||
const setTerminalWrapperRef = (el: any, key: any) => {
|
|
||||||
if (key) {
|
|
||||||
terminalWrapperRefs[key] = el;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onResizeTagTree = () => {
|
|
||||||
fitTerminal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const fitTerminal = () => {
|
const fitTerminal = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
let info = state.tabs.get(state.activeTermName);
|
let info = state.tabs.get(state.activeTermName);
|
||||||
@@ -419,9 +404,7 @@ const fitTerminal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReconnect = (tab: any, force = false) => {
|
const handleReconnect = (tab: any, force = false) => {
|
||||||
let width = terminalWrapperRefs[tab.key]?.offsetWidth;
|
terminalRefs[tab.key]?.init();
|
||||||
let height = terminalWrapperRefs[tab.key]?.offsetHeight;
|
|
||||||
terminalRefs[tab.key]?.init(width, height, force);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
|||||||
@@ -111,8 +111,6 @@
|
|||||||
</el-splitter-panel>
|
</el-splitter-panel>
|
||||||
</el-splitter>
|
</el-splitter>
|
||||||
|
|
||||||
<div style="text-align: center; margin-top: 10px"></div>
|
|
||||||
|
|
||||||
<el-dialog :title="$t('redis.addKey')" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
|
<el-dialog :title="$t('redis.addKey')" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
|
||||||
<el-form ref="keyForm" label-width="auto" :rules="keyFormRules" :model="newKeyDialog.keyInfo">
|
<el-form ref="keyForm" label-width="auto" :rules="keyFormRules" :model="newKeyDialog.keyInfo">
|
||||||
<el-form-item prop="key" label="Key" required>
|
<el-form-item prop="key" label="Key" required>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
|||||||
import { redisApi } from '../api';
|
import { redisApi } from '../api';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
|
|
||||||
const RedisIcon = {
|
export const RedisIcon = {
|
||||||
name: ResourceTypeEnum.Redis.extra.icon,
|
name: ResourceTypeEnum.Redis.extra.icon,
|
||||||
color: ResourceTypeEnum.Redis.extra.iconColor,
|
color: ResourceTypeEnum.Redis.extra.iconColor,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
:id="props.node.key"
|
:id="props.node.key"
|
||||||
class="w-full node-container flex items-center cursor-pointer select-none"
|
class="w-full node-container flex items-center cursor-pointer select-none"
|
||||||
:class="props.data.type.nodeDblclickFunc ? 'select-none' : ''"
|
:class="props.data.type?.nodeDblclickFunc ? 'select-none' : ''"
|
||||||
@mouseenter="showActions = true"
|
@mouseenter="showActions = true"
|
||||||
@mouseleave="showActions = false"
|
@mouseleave="showActions = false"
|
||||||
>
|
>
|
||||||
@@ -55,7 +55,7 @@ import { ContextmenuItem } from '@/components/contextmenu';
|
|||||||
import { ResourceOpCtx, TagTreeNode } from '@/views/ops/component/tag';
|
import { ResourceOpCtx, TagTreeNode } from '@/views/ops/component/tag';
|
||||||
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
|
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
|
||||||
|
|
||||||
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey, undefined);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
node: {
|
node: {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
:filter-node-method="filterNode"
|
:filter-node-method="filterNode"
|
||||||
@node-click="treeNodeClick"
|
@node-click="treeNodeClick"
|
||||||
@node-expand="treeNodeClick"
|
@node-expand="treeNodeClick"
|
||||||
|
@node-contextmenu="onNodeContextmenu"
|
||||||
:default-expanded-keys="state.defaultExpandedKeys"
|
:default-expanded-keys="state.defaultExpandedKeys"
|
||||||
>
|
>
|
||||||
<template #default="{ node, data }">
|
<template #default="{ node, data }">
|
||||||
@@ -65,12 +66,15 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
</el-splitter-panel>
|
</el-splitter-panel>
|
||||||
</el-splitter>
|
</el-splitter>
|
||||||
|
|
||||||
|
<Contextmenu :dropdown="state.dropdown" :items="state.contextmenuItems" ref="contextmenuRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { markRaw, nextTick, provide, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
|
import { markRaw, nextTick, provide, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Contextmenu } from '@/components/contextmenu';
|
||||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
@@ -108,6 +112,7 @@ const { t } = useI18n();
|
|||||||
const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
|
const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
|
||||||
|
|
||||||
const treeRef: any = useTemplateRef('treeRef');
|
const treeRef: any = useTemplateRef('treeRef');
|
||||||
|
const contextmenuRef: any = useTemplateRef('contextmenuRef');
|
||||||
|
|
||||||
// 存储所有注册的资源组件引用,key -> 组件名称
|
// 存储所有注册的资源组件引用,key -> 组件名称
|
||||||
const resourceComponents = ref<Record<string, ResourceComponentConfig>>({});
|
const resourceComponents = ref<Record<string, ResourceComponentConfig>>({});
|
||||||
@@ -134,6 +139,11 @@ const setResourceComponentRefs = async (name: string, ref: any) => {
|
|||||||
const state = reactive({
|
const state = reactive({
|
||||||
defaultExpandedKeys: [] as string[],
|
defaultExpandedKeys: [] as string[],
|
||||||
filterText: '',
|
filterText: '',
|
||||||
|
contextmenuItems: [],
|
||||||
|
dropdown: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { filterText } = toRefs(state);
|
const { filterText } = toRefs(state);
|
||||||
@@ -216,6 +226,9 @@ const loadNode = async (node: any, resolve: (data: any) => void, reject: () => v
|
|||||||
let lastNodeClickTime = 0;
|
let lastNodeClickTime = 0;
|
||||||
|
|
||||||
const treeNodeClick = async (data: any, node: any) => {
|
const treeNodeClick = async (data: any, node: any) => {
|
||||||
|
// 关闭可能存在的右击菜单
|
||||||
|
contextmenuRef.value?.closeContextmenu();
|
||||||
|
|
||||||
const currentClickNodeTime = Date.now();
|
const currentClickNodeTime = Date.now();
|
||||||
// 双击节点
|
// 双击节点
|
||||||
if (currentClickNodeTime - lastNodeClickTime < 300) {
|
if (currentClickNodeTime - lastNodeClickTime < 300) {
|
||||||
@@ -248,6 +261,29 @@ const treeNodeDblclick = async (data: any, node: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 树节点右击事件
|
||||||
|
const onNodeContextmenu = (event: any, data: any) => {
|
||||||
|
if (data.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载当前节点是否需要显示右击菜单
|
||||||
|
let items = data.type.contextMenuItems;
|
||||||
|
if (!items || items.length == 0) {
|
||||||
|
if (props.loadContextmenuItems) {
|
||||||
|
items = props.loadContextmenuItems(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!items) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.contextmenuItems = items;
|
||||||
|
const { clientX, clientY } = event;
|
||||||
|
state.dropdown.x = clientX;
|
||||||
|
state.dropdown.y = clientY;
|
||||||
|
contextmenuRef.value.openContextmenu(data);
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化资源组件ref
|
// 初始化资源组件ref
|
||||||
const initResourceComp = (val: any) => {
|
const initResourceComp = (val: any) => {
|
||||||
if (!val.ref || resourceComponentRefs.value[val.name]) {
|
if (!val.ref || resourceComponentRefs.value[val.name]) {
|
||||||
|
|||||||
@@ -18,28 +18,18 @@
|
|||||||
<slot name="iconPrefix" :node="node" :data="data" />
|
<slot name="iconPrefix" :node="node" :data="data" />
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ node, data }">
|
<template #default="{ node, data }">
|
||||||
<span>
|
<component v-if="data.nodeComponent" :is="data.nodeComponent" :node="node" :data="data" />
|
||||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
<BaseTreeNode v-else :node="node" :data="data" />
|
||||||
<tag-info :tag-path="data.label" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
|
||||||
|
|
||||||
<span class="ml-0.5" :title="data.labelRemark">
|
|
||||||
<slot name="label" :data="data"> {{ data.label }}</slot>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<slot :node="node" :data="data" name="suffix"></slot>
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-tree-select>
|
</el-tree-select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, ref, toRefs, watch } from 'vue';
|
import { onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { NodeType, TagTreeNode } from './tag';
|
|
||||||
import TagInfo from './TagInfo.vue';
|
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||||
import { tagApi } from '../tag/api';
|
import { tagApi } from '@/views/ops/tag/api';
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
resourceType: {
|
resourceType: {
|
||||||
@@ -16,8 +16,11 @@
|
|||||||
<template #content>
|
<template #content>
|
||||||
{{ $t('tag.tagTips1') }}
|
{{ $t('tag.tagTips1') }}
|
||||||
<br />
|
<br />
|
||||||
{{ $t('tag.tagTips2') }} <br />
|
{{ $t('tag.tagTips2') }}
|
||||||
|
<br />
|
||||||
{{ $t('tag.tagTips3') }}
|
{{ $t('tag.tagTips3') }}
|
||||||
|
<br />
|
||||||
|
{{ $t('tag.tagTips4') }}
|
||||||
</template>
|
</template>
|
||||||
<SvgIcon class="ml-1" name="question-filled" />
|
<SvgIcon class="ml-1" name="question-filled" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -37,10 +40,6 @@
|
|||||||
@node-contextmenu="onNodeContextmenu"
|
@node-contextmenu="onNodeContextmenu"
|
||||||
@node-click="onTreeNodeClick"
|
@node-click="onTreeNodeClick"
|
||||||
:default-expanded-keys="defaultExpandedKeys"
|
:default-expanded-keys="defaultExpandedKeys"
|
||||||
draggable
|
|
||||||
:allow-drop="allowDrop"
|
|
||||||
:allow-drag="allowDrag"
|
|
||||||
@node-drop="onNodeDrop"
|
|
||||||
:expand-on-click-node="false"
|
:expand-on-click-node="false"
|
||||||
:filter-node-method="filterNode"
|
:filter-node-method="filterNode"
|
||||||
>
|
>
|
||||||
@@ -279,7 +278,7 @@ watch(filterTag, (val) => {
|
|||||||
watch(
|
watch(
|
||||||
() => state.currentTag,
|
() => state.currentTag,
|
||||||
(val: any) => {
|
(val: any) => {
|
||||||
if (val.type == TagResourceTypeEnum.Tag.value) {
|
if (val?.type == TagResourceTypeEnum.Tag.value) {
|
||||||
tagApi.countTagResource.request({ tagPath: val.codePath }).then((res: any) => {
|
tagApi.countTagResource.request({ tagPath: val.codePath }).then((res: any) => {
|
||||||
state.resourceCount = res;
|
state.resourceCount = res;
|
||||||
});
|
});
|
||||||
@@ -289,82 +288,6 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const allowDrop = (draggingNode: any, dropNode: any, type: any) => {
|
|
||||||
// 不允许同层级移动
|
|
||||||
if (type != 'inner') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dropNodeData = dropNode.data;
|
|
||||||
const draggingNodeData = draggingNode.data;
|
|
||||||
const dropTagType = dropNodeData.type;
|
|
||||||
const draggingTagType = draggingNodeData.type;
|
|
||||||
|
|
||||||
// 目标节点只允许为标签类型
|
|
||||||
if (dropTagType != TagResourceTypeEnum.Tag.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 目标节点下没有子节点
|
|
||||||
if (!dropNodeData.children) {
|
|
||||||
// 都为标签类型允许移动
|
|
||||||
if (dropTagType == draggingTagType && dropTagType == TagResourceTypeEnum.Tag.value) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 目标节点为标签,允许移动
|
|
||||||
if (dropTagType == TagResourceTypeEnum.Tag.value) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let child of dropNodeData.children) {
|
|
||||||
// 当前移动节点若在目标节点下有相同code,则不允许移动
|
|
||||||
if (draggingNodeData.code == child.code) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const childType = child.type;
|
|
||||||
// 移动节点非标签类型时(资源标签),并且子节点存在标签类型,则不允许移动,因为资源只允许放在叶子标签类型下
|
|
||||||
if (draggingTagType != TagResourceTypeEnum.Tag.value && childType == TagResourceTypeEnum.Tag.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移动节点为标签类型时(资源标签),并且子节点存在资源类型,则不允许移动
|
|
||||||
if (draggingTagType == TagResourceTypeEnum.Tag.value && childType != TagResourceTypeEnum.Tag.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const allowDrag = (node: any) => {
|
|
||||||
const tagType = node.data.type;
|
|
||||||
return (
|
|
||||||
tagType == TagResourceTypeEnum.Tag.value ||
|
|
||||||
tagType == TagResourceTypeEnum.DbInstance.value ||
|
|
||||||
tagType == TagResourceTypeEnum.Redis.value ||
|
|
||||||
tagType == TagResourceTypeEnum.Machine.value ||
|
|
||||||
tagType == TagResourceTypeEnum.Mongo.value
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onNodeDrop = async (draggingNode: any, dropNode: any) => {
|
|
||||||
const draggingData = draggingNode.data;
|
|
||||||
const dropData = dropNode.data;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await tagApi.movingTag.request({
|
|
||||||
fromPath: draggingData.codePath,
|
|
||||||
toPath: dropData.codePath,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTabChange = () => {
|
const onTabChange = () => {
|
||||||
setNowTabData();
|
setNowTabData();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,45 +3,45 @@ module mayfly-go
|
|||||||
go 1.25
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitee.com/chunanyong/dm v1.8.20
|
gitee.com/chunanyong/dm v1.8.21
|
||||||
gitee.com/liuzongyang/libpq v1.10.11
|
gitee.com/liuzongyang/libpq v1.10.11
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||||
github.com/docker/docker v28.3.3+incompatible
|
github.com/docker/docker v28.5.0+incompatible
|
||||||
github.com/docker/go-connections v0.6.0
|
github.com/docker/go-connections v0.6.0
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-gormigrate/gormigrate/v2 v2.1.4
|
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||||
github.com/go-ldap/ldap/v3 v3.4.11
|
github.com/go-ldap/ldap/v3 v3.4.12
|
||||||
github.com/go-playground/locales v0.14.1
|
github.com/go-playground/locales v0.14.1
|
||||||
github.com/go-playground/universal-translator v0.18.1
|
github.com/go-playground/universal-translator v0.18.1
|
||||||
github.com/go-playground/validator/v10 v10.27.0
|
github.com/go-playground/validator/v10 v10.28.0
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20250630080345-f9402614f6ba
|
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20250930013652-2d71241a3bb9
|
||||||
github.com/microsoft/go-mssqldb v1.9.2
|
github.com/microsoft/go-mssqldb v1.9.3
|
||||||
github.com/mojocn/base64Captcha v1.3.8 // 验证码
|
github.com/mojocn/base64Captcha v1.3.8 // 验证码
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
github.com/opencontainers/image-spec v1.1.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pkg/sftp v1.13.9
|
github.com/pkg/sftp v1.13.10
|
||||||
github.com/pquerna/otp v1.5.0
|
github.com/pquerna/otp v1.5.0
|
||||||
github.com/redis/go-redis/v9 v9.12.1
|
github.com/redis/go-redis/v9 v9.16.0
|
||||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||||
github.com/sijms/go-ora/v2 v2.9.0
|
github.com/sijms/go-ora/v2 v2.9.0
|
||||||
github.com/spf13/cast v1.9.2
|
github.com/spf13/cast v1.10.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
github.com/veops/go-ansiterm v0.0.5
|
github.com/veops/go-ansiterm v0.0.5
|
||||||
go.mongodb.org/mongo-driver/v2 v2.3.0 // mongo
|
go.mongodb.org/mongo-driver/v2 v2.3.0 // mongo
|
||||||
golang.org/x/crypto v0.41.0 // ssh
|
golang.org/x/crypto v0.44.0 // ssh
|
||||||
golang.org/x/oauth2 v0.30.0
|
golang.org/x/oauth2 v0.33.0
|
||||||
golang.org/x/sync v0.16.0
|
golang.org/x/sync v0.18.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
// gorm
|
// gorm
|
||||||
gorm.io/driver/mysql v1.6.0
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/gorm v1.30.3
|
gorm.io/gorm v1.31.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -49,10 +49,12 @@ require (
|
|||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/boombuler/barcode v1.1.0 // indirect
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.14.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
@@ -62,14 +64,14 @@ require (
|
|||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
@@ -82,7 +84,7 @@ require (
|
|||||||
github.com/kr/fs v0.1.0 // indirect
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
github.com/moby/term v0.5.2 // indirect
|
github.com/moby/term v0.5.2 // indirect
|
||||||
@@ -93,9 +95,10 @@ require (
|
|||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/tidwall/match v1.2.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
@@ -104,24 +107,26 @@ require (
|
|||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
golang.org/x/arch v0.19.0 // indirect
|
golang.org/x/arch v0.21.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
|
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
|
||||||
golang.org/x/image v0.29.0 // indirect
|
golang.org/x/image v0.31.0 // indirect
|
||||||
golang.org/x/net v0.42.0 // indirect
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/net v0.46.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
modernc.org/libc v1.66.4 // indirect
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
modernc.org/libc v1.66.10 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.38.1 // indirect
|
modernc.org/sqlite v1.39.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20250603155806-513f23925822
|
replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20250603155806-513f23925822
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ func (d *dbAppImpl) GetDbConnByInstanceId(ctx context.Context, instanceId uint64
|
|||||||
return nil, errorx.NewBiz("failed to get database list")
|
return nil, errorx.NewBiz("failed to get database list")
|
||||||
}
|
}
|
||||||
if len(dbs) == 0 {
|
if len(dbs) == 0 {
|
||||||
return nil, errorx.NewBiz("DB instance [%d] database is not configured, please configure it first", instanceId)
|
return nil, errorx.NewBizf("DB instance [%d] database is not configured, please configure it first", instanceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用该实例关联的已配置数据库中的第一个库进行连接并返回
|
// 使用该实例关联的已配置数据库中的第一个库进行连接并返回
|
||||||
@@ -308,7 +308,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
|
|||||||
}
|
}
|
||||||
if len(tbs) <= 0 {
|
if len(tbs) <= 0 {
|
||||||
log(fmt.Sprintf("failed to get table [%s] information: No table information was retrieved", tableName))
|
log(fmt.Sprintf("failed to get table [%s] information: No table information was retrieved", tableName))
|
||||||
return errorx.NewBiz("Failed to get table information: %s", tableName)
|
return errorx.NewBizf("Failed to get table information: %s", tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
tableInfo := tbs[0]
|
tableInfo := tbs[0]
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func (app *dataSyncAppImpl) Delete(ctx context.Context, id uint64) error {
|
|||||||
|
|
||||||
func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
|
func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
|
||||||
if app.IsRunning(id) {
|
if app.IsRunning(id) {
|
||||||
logx.Warn("[%d] the db sync task is running...", id)
|
logx.Warnf("[%d] the db sync task is running...", id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
|
|||||||
}
|
}
|
||||||
updateStateTask.Id = id
|
updateStateTask.Id = id
|
||||||
if err := app.UpdateById(ctx, updateStateTask); err != nil {
|
if err := app.UpdateById(ctx, updateStateTask); err != nil {
|
||||||
return errorx.NewBiz("failed to update task running state: %s", err.Error())
|
return errorx.NewBizf("failed to update task running state: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记该任务运行中
|
// 标记该任务运行中
|
||||||
@@ -136,7 +136,7 @@ func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
|
|||||||
logx.ErrorfContext(ctx, "data source connection unavailable: %s", err.Error())
|
logx.ErrorfContext(ctx, "data source connection unavailable: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
srcConn, err := app.dbApp.GetDbConn(ctx, uint64(task.SrcDbId), task.SrcDbName)
|
srcConn, err := app.dbApp.GetDbConn(context.Background(), uint64(task.SrcDbId), task.SrcDbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.ErrorfContext(ctx, "failed to connect to the source database: %s", err.Error())
|
logx.ErrorfContext(ctx, "failed to connect to the source database: %s", err.Error())
|
||||||
return
|
return
|
||||||
@@ -184,20 +184,20 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
|
|||||||
srcConn, err := app.dbApp.GetDbConn(ctx, uint64(task.SrcDbId), task.SrcDbName)
|
srcConn, err := app.dbApp.GetDbConn(ctx, uint64(task.SrcDbId), task.SrcDbName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to connect to the source database: %s", err.Error())
|
return errorx.NewBizf("failed to connect to the source database: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取目标数据库连接
|
// 获取目标数据库连接
|
||||||
targetConn, err := app.dbApp.GetDbConn(ctx, uint64(task.TargetDbId), task.TargetDbName)
|
targetConn, err := app.dbApp.GetDbConn(ctx, uint64(task.TargetDbId), task.TargetDbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to connect to the target database: %s", err.Error())
|
return errorx.NewBizf("failed to connect to the target database: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// task.FieldMap为json数组字符串 [{"src":"id","target":"id"}],转为map
|
// task.FieldMap为json数组字符串 [{"src":"id","target":"id"}],转为map
|
||||||
var fieldMap []map[string]string
|
var fieldMap []map[string]string
|
||||||
err = json.Unmarshal([]byte(task.FieldMap), &fieldMap)
|
err = json.Unmarshal([]byte(task.FieldMap), &fieldMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("there was an error parsing the field map json: %s", err.Error())
|
return errorx.NewBizf("there was an error parsing the field map json: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录本次同步数据总数
|
// 记录本次同步数据总数
|
||||||
@@ -213,7 +213,7 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
|
|||||||
|
|
||||||
targetTableColumns, err := targetConn.GetMetadata().GetColumns(task.TargetTableName)
|
targetTableColumns, err := targetConn.GetMetadata().GetColumns(task.TargetTableName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to get target table columns: %s", err.Error())
|
return errorx.NewBizf("failed to get target table columns: %s", err.Error())
|
||||||
}
|
}
|
||||||
targetColumnName2Column := collx.ArrayToMap(targetTableColumns, func(column dbi.Column) string {
|
targetColumnName2Column := collx.ArrayToMap(targetTableColumns, func(column dbi.Column) string {
|
||||||
return column.ColumnName
|
return column.ColumnName
|
||||||
@@ -300,7 +300,7 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
|||||||
// 开启本批次执行事务
|
// 开启本批次执行事务
|
||||||
targetDbTx, err := targetDbConn.Begin()
|
targetDbTx, err := targetDbConn.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to start the target database transaction: %s", err.Error())
|
return errorx.NewBizf("failed to start the target database transaction: %s", err.Error())
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@@ -320,7 +320,7 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
|||||||
// 如果是mssql,暂不手动提交事务,否则报错 mssql: The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
|
// 如果是mssql,暂不手动提交事务,否则报错 mssql: The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
|
||||||
if err := targetDbTx.Commit(); err != nil {
|
if err := targetDbTx.Commit(); err != nil {
|
||||||
if targetDbConn.Info.Type != dbi.ToDbType("mssql") {
|
if targetDbConn.Info.Type != dbi.ToDbType("mssql") {
|
||||||
return errorx.NewBiz("data synchronization - The target database transaction failed to commit: %s", err.Error())
|
return errorx.NewBizf("data synchronization - The target database transaction failed to commit: %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,9 +370,11 @@ func (app *dataSyncAppImpl) saveLog(log *entity.DataSyncLog) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *dataSyncAppImpl) InitCronJob() {
|
func (app *dataSyncAppImpl) InitCronJob() {
|
||||||
|
ctx := contextx.NewTraceId()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
logx.ErrorTrace("the data synchronization task failed to initialize", err)
|
logx.ErrorTraceContext(ctx, "the data synchronization task failed to initialize", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -380,10 +382,11 @@ func (app *dataSyncAppImpl) InitCronJob() {
|
|||||||
_ = app.UpdateByCond(context.TODO(), &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateReady}, &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateRunning})
|
_ = app.UpdateByCond(context.TODO(), &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateReady}, &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateRunning})
|
||||||
|
|
||||||
if err := app.CursorByCond(&entity.DataSyncTaskQuery{Status: entity.DataSyncTaskStatusEnable}, func(dst *entity.DataSyncTask) error {
|
if err := app.CursorByCond(&entity.DataSyncTaskQuery{Status: entity.DataSyncTaskStatusEnable}, func(dst *entity.DataSyncTask) error {
|
||||||
app.addCronJob(contextx.NewTraceId(), dst)
|
app.MarkStop(dst.Id)
|
||||||
|
app.addCronJob(ctx, dst)
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logx.ErrorTrace("the db data sync task failed to initialize: %v", err)
|
logx.ErrorTraceContext(ctx, "the db data sync task failed to initialize: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,13 +417,13 @@ func (app *dataSyncAppImpl) addCronJob(ctx context.Context, taskEntity *entity.D
|
|||||||
// 根据状态添加新的任务
|
// 根据状态添加新的任务
|
||||||
if taskEntity.Status == entity.DataSyncTaskStatusEnable {
|
if taskEntity.Status == entity.DataSyncTaskStatusEnable {
|
||||||
taskId := taskEntity.Id
|
taskId := taskEntity.Id
|
||||||
logx.Infof("start add the data sync task job: %s, cron[%s]", taskEntity.TaskName, taskEntity.TaskCron)
|
logx.InfofContext(ctx, "start add the data sync task job: %s, cron[%s]", taskEntity.TaskName, taskEntity.TaskCron)
|
||||||
if err := scheduler.AddFunByKey(key, taskEntity.TaskCron, func() {
|
if err := scheduler.AddFunByKey(key, taskEntity.TaskCron, func() {
|
||||||
if err := app.Run(context.Background(), taskId); err != nil {
|
if err := app.Run(context.Background(), taskId); err != nil {
|
||||||
logx.Errorf("the data sync task failed to execute at a scheduled time: %s", err.Error())
|
logx.ErrorfContext(ctx, "the data sync task failed to execute at a scheduled time: %s", err.Error())
|
||||||
}
|
}
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logx.ErrorTrace("add db data sync job failed", err)
|
logx.ErrorTraceContext(ctx, "add db data sync job failed", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ func (d *dbSqlExecAppImpl) FlowBizHandle(ctx context.Context, bizHandleParam *fl
|
|||||||
|
|
||||||
execSqlBizForm, err := jsonx.To[*FlowDbExecSqlBizForm](procinst.BizForm)
|
execSqlBizForm, err := jsonx.To[*FlowDbExecSqlBizForm](procinst.BizForm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.NewBiz("failed to parse the business form information: %s", err.Error())
|
return nil, errorx.NewBizf("failed to parse the business form information: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
dbConn, err := d.dbApp.GetDbConn(ctx, execSqlBizForm.DbId, execSqlBizForm.DbName)
|
dbConn, err := d.dbApp.GetDbConn(ctx, execSqlBizForm.DbId, execSqlBizForm.DbName)
|
||||||
@@ -471,7 +471,7 @@ func (d *dbSqlExecAppImpl) doUpdate(ctx context.Context, sqlExecParam *sqlExecPa
|
|||||||
nowRec++
|
nowRec++
|
||||||
res = append(res, row)
|
res = append(res, row)
|
||||||
if nowRec == maxRec {
|
if nowRec == maxRec {
|
||||||
return errorx.NewBiz("update SQL - the maximum number of updated queries is exceeded: %d", maxRec)
|
return errorx.NewBizf("update SQL - the maximum number of updated queries is exceeded: %d", maxRec)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -122,12 +122,12 @@ func (app *dbTransferAppImpl) InitCronJob() {
|
|||||||
|
|
||||||
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64) (uint64, error) {
|
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64) (uint64, error) {
|
||||||
if app.IsRunning(taskId) {
|
if app.IsRunning(taskId) {
|
||||||
return 0, errorx.NewBiz("the db transfer task [%d] is running, please do not repeat the operation", taskId)
|
return 0, errorx.NewBizf("the db transfer task [%d] is running, please do not repeat the operation", taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
task, err := app.GetById(taskId)
|
task, err := app.GetById(taskId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errorx.NewBiz("db transfer task [%d] not found", taskId)
|
return 0, errorx.NewBizf("db transfer task [%d] not found", taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
logId, _ := app.CreateLog(ctx, taskId)
|
logId, _ := app.CreateLog(ctx, taskId)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mayfly-go/pkg/utils/collx"
|
"mayfly-go/pkg/utils/collx"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -111,6 +112,12 @@ func ClearNumScale(column *Column) {
|
|||||||
column.CharMaxLength = 0
|
column.CharMaxLength = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ClearNumPrecision(column *Column) {
|
||||||
|
column.NumScale = 0
|
||||||
|
column.NumPrecision = 0
|
||||||
|
column.CharMaxLength = 0
|
||||||
|
}
|
||||||
|
|
||||||
// DataType 数据类型, 对应于go类型,如int int64等。可自定义其他类型
|
// DataType 数据类型, 对应于go类型,如int int64等。可自定义其他类型
|
||||||
type DataType struct {
|
type DataType struct {
|
||||||
Name string // 类型名
|
Name string // 类型名
|
||||||
@@ -173,7 +180,13 @@ func SQLValueString(val any) string {
|
|||||||
return fmt.Sprintf("%v", val)
|
return fmt.Sprintf("%v", val)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("'%s'", strings.ReplaceAll(strings.ReplaceAll(strVal, "'", "''"), `\`, `\\`))
|
// 使用 strconv.Quote 来处理所有特殊字符
|
||||||
|
quoted := strconv.Quote(strVal)
|
||||||
|
// 去掉 strconv.Quote 添加的外层引号,因为会在最后添加 SQL 的单引号
|
||||||
|
quoted = quoted[1 : len(quoted)-1]
|
||||||
|
// 处理 SQL 中的单引号
|
||||||
|
quoted = strings.ReplaceAll(quoted, "'", "''")
|
||||||
|
return fmt.Sprintf("'%s'", quoted)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -41,6 +41,15 @@ func (d *DbConn) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DbConn) Ping() error {
|
func (d *DbConn) Ping() error {
|
||||||
|
// 首先检查d是否为nil
|
||||||
|
if d == nil {
|
||||||
|
return fmt.Errorf("d is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后检查d.db是否为nil,这是避免空指针异常的关键
|
||||||
|
if d.db == nil {
|
||||||
|
return fmt.Errorf("db is nil")
|
||||||
|
}
|
||||||
return d.db.Ping()
|
return d.db.Ping()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,13 +70,13 @@ func (di *DbInfo) Conn(ctx context.Context, meta Meta) (*DbConn, error) {
|
|||||||
conn, err := meta.GetSqlDb(ctx, di)
|
conn, err := meta.GetSqlDb(ctx, di)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("db connection failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
|
logx.Errorf("db connection failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
|
||||||
return nil, errorx.NewBiz("db connection failed: %s", err.Error())
|
return nil, errorx.NewBizf("db connection failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = conn.Ping()
|
err = conn.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("db ping failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
|
logx.Errorf("db ping failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
|
||||||
return nil, errorx.NewBiz("db connection failed: %s", err.Error())
|
return nil, errorx.NewBizf("db connection failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
dbc := &DbConn{Id: GetDbConnId(di.Id, database), Info: di}
|
dbc := &DbConn{Id: GetDbConnId(di.Id, database), Info: di}
|
||||||
|
|||||||
@@ -24,17 +24,16 @@ type Meta struct {
|
|||||||
func (dm *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
|
func (dm *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
|
||||||
driverName := "dm"
|
driverName := "dm"
|
||||||
db := d.Database
|
db := d.Database
|
||||||
var dbParam string
|
dbParam := "?escapeProcess=true"
|
||||||
if db != "" {
|
if db != "" {
|
||||||
// dm database可以使用db/schema表示,方便连接指定schema, 若不存在schema则使用默认schema
|
// dm database可以使用db/schema表示,方便连接指定schema, 若不存在schema则使用默认schema
|
||||||
ss := strings.Split(db, "/")
|
ss := strings.Split(db, "/")
|
||||||
if len(ss) > 1 {
|
if len(ss) > 1 {
|
||||||
dbParam = fmt.Sprintf("%s?schema=\"%s\"&escapeProcess=true", ss[0], ss[len(ss)-1])
|
dbParam = fmt.Sprintf("%s&schema=\"%s\"", dbParam, ss[len(ss)-1])
|
||||||
} else {
|
|
||||||
dbParam = db + "?escapeProcess=true"
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
dbParam = "?escapeProcess=true"
|
if d.Params != "" {
|
||||||
|
dbParam += "&" + d.Params
|
||||||
}
|
}
|
||||||
|
|
||||||
err := d.IfUseSshTunnelChangeIpPort(ctx)
|
err := d.IfUseSshTunnelChangeIpPort(ctx)
|
||||||
@@ -42,7 +41,7 @@ func (dm *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dsn := fmt.Sprintf("dm://%s:%s@%s:%d/%s", d.Username, url.PathEscape(d.Password), d.Host, d.Port, dbParam)
|
dsn := fmt.Sprintf("dm://%s:%s@%s:%d%s", d.Username, url.PathEscape(d.Password), d.Host, d.Port, dbParam)
|
||||||
return sql.Open(driverName, dsn)
|
return sql.Open(driverName, dsn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ func (dd *DMMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
if v.IsPrimaryKey {
|
if v.IsPrimaryKey {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func (md *MssqlMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ var (
|
|||||||
UnsignedMediumint = dbi.NewDbDataType("unsigned mediumint", dbi.DTInt64).WithCT(dbi.CTUnsignedInt4).WithFixColumn(dbi.ClearNumScale)
|
UnsignedMediumint = dbi.NewDbDataType("unsigned mediumint", dbi.DTInt64).WithCT(dbi.CTUnsignedInt4).WithFixColumn(dbi.ClearNumScale)
|
||||||
|
|
||||||
Decimal = dbi.NewDbDataType("decimal", dbi.DTDecimal).WithCT(dbi.CTDecimal)
|
Decimal = dbi.NewDbDataType("decimal", dbi.DTDecimal).WithCT(dbi.CTDecimal)
|
||||||
Double = dbi.NewDbDataType("double", dbi.DTNumeric).WithCT(dbi.CTNumeric)
|
Double = dbi.NewDbDataType("double", dbi.DTNumeric).WithCT(dbi.CTNumeric).WithFixColumn(dbi.ClearNumPrecision)
|
||||||
Float = dbi.NewDbDataType("float", dbi.DTNumeric).WithCT(dbi.CTNumeric)
|
Float = dbi.NewDbDataType("float", dbi.DTNumeric).WithCT(dbi.CTNumeric)
|
||||||
|
|
||||||
Varchar = dbi.NewDbDataType("varchar", dbi.DTString).WithCT(dbi.CTVarchar)
|
Varchar = dbi.NewDbDataType("varchar", dbi.DTString).WithCT(dbi.CTVarchar)
|
||||||
@@ -40,9 +40,9 @@ var (
|
|||||||
Enum = dbi.NewDbDataType("enum", dbi.DTString).WithCT(dbi.CTEnum)
|
Enum = dbi.NewDbDataType("enum", dbi.DTString).WithCT(dbi.CTEnum)
|
||||||
Set = dbi.NewDbDataType("set", dbi.DTString).WithCT(dbi.CTVarchar)
|
Set = dbi.NewDbDataType("set", dbi.DTString).WithCT(dbi.CTVarchar)
|
||||||
|
|
||||||
Blob = dbi.NewDbDataType("blob", dbi.DTBytes).WithCT(dbi.CTBlob)
|
Blob = dbi.NewDbDataType("blob", dbi.DTBytes).WithCT(dbi.CTBlob).WithFixColumn(dbi.ClearNumScale)
|
||||||
Mediumblob = dbi.NewDbDataType("mediumblob", dbi.DTBytes).WithCT(dbi.CTMediumblob)
|
Mediumblob = dbi.NewDbDataType("mediumblob", dbi.DTBytes).WithCT(dbi.CTMediumblob).WithFixColumn(dbi.ClearNumScale)
|
||||||
Longblob = dbi.NewDbDataType("longblob", dbi.DTBytes).WithCT(dbi.CTLongblob)
|
Longblob = dbi.NewDbDataType("longblob", dbi.DTBytes).WithCT(dbi.CTLongblob).WithFixColumn(dbi.ClearNumScale)
|
||||||
Binary = dbi.NewDbDataType("binary", dbi.DTBytes).WithCT(dbi.CTBinary)
|
Binary = dbi.NewDbDataType("binary", dbi.DTBytes).WithCT(dbi.CTBinary)
|
||||||
Varbinary = dbi.NewDbDataType("varbinary", dbi.DTBytes).WithCT(dbi.CTVarbinary)
|
Varbinary = dbi.NewDbDataType("varbinary", dbi.DTBytes).WithCT(dbi.CTVarbinary)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ func (md *MysqlMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ func (od *OracleMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
if v.IsPrimaryKey {
|
if v.IsPrimaryKey {
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func (pd *PgsqlMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
if v.IsPrimaryKey {
|
if v.IsPrimaryKey {
|
||||||
|
|||||||
@@ -148,14 +148,14 @@ func (d *Container) ContainerCreate(rc *req.Ctx) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
panic(errorx.NewBiz("create container failed, err: %v", err))
|
panic(errorx.NewBizf("create container failed, err: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
logx.Infof("create container %s successful! now check if the container is started and delete the container information if it is not.", containerCreate.Name)
|
logx.Infof("create container %s successful! now check if the container is started and delete the container information if it is not.", containerCreate.Name)
|
||||||
|
|
||||||
if err := cli.DockerClient.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil {
|
if err := cli.DockerClient.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil {
|
||||||
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
panic(errorx.NewBiz("create successful but start failed, err: %v", err))
|
panic(errorx.NewBizf("create successful but start failed, err: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
type InstanceForm struct {
|
type InstanceForm struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
|
Protocol string `binding:"required" json:"protocol"`
|
||||||
Name string `binding:"required" json:"name"`
|
Name string `binding:"required" json:"name"`
|
||||||
Host string `binding:"required" json:"host"`
|
Host string `binding:"required" json:"host"`
|
||||||
Port int `binding:"required" json:"port"`
|
Port int `binding:"required" json:"port"`
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type InstanceListVO struct {
|
|||||||
Id *int64 `json:"id"`
|
Id *int64 `json:"id"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
|
Protocol *string `json:"protocol"`
|
||||||
Host *string `json:"host"`
|
Host *string `json:"host"`
|
||||||
Port *int `json:"port"`
|
Port *int `json:"port"`
|
||||||
Version *string `json:"version"`
|
Version *string `json:"version"`
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type EsInstance struct {
|
|||||||
|
|
||||||
Code string `json:"code" gorm:"size:32;not null;"`
|
Code string `json:"code" gorm:"size:32;not null;"`
|
||||||
Name string `json:"name" gorm:"size:32;not null;"`
|
Name string `json:"name" gorm:"size:32;not null;"`
|
||||||
|
Protocol string `json:"protocol" gorm:"size:10;not null;"`
|
||||||
Host string `json:"host" gorm:"size:255;not null;"`
|
Host string `json:"host" gorm:"size:255;not null;"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Network string `json:"network" gorm:"size:20;"`
|
Network string `json:"network" gorm:"size:20;"`
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package esi
|
package esi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mayfly-go/internal/machine/mcm"
|
"mayfly-go/internal/machine/mcm"
|
||||||
"mayfly-go/pkg/logx"
|
"mayfly-go/pkg/logx"
|
||||||
@@ -27,6 +28,15 @@ func (d *EsConn) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *EsConn) Ping() error {
|
func (d *EsConn) Ping() error {
|
||||||
|
// 首先检查d是否为nil
|
||||||
|
if d == nil {
|
||||||
|
return fmt.Errorf("es connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后检查d.Info是否为nil,这是避免空指针异常的关键
|
||||||
|
if d.Info == nil {
|
||||||
|
return fmt.Errorf("es Info is nil")
|
||||||
|
}
|
||||||
_, err := d.Info.Ping()
|
_, err := d.Info.Ping()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -43,6 +53,16 @@ func (d *EsConn) StartProxy() error {
|
|||||||
d.proxy = httputil.NewSingleHostReverseProxy(targetURL)
|
d.proxy = httputil.NewSingleHostReverseProxy(targetURL)
|
||||||
// 设置 proxy buffer pool
|
// 设置 proxy buffer pool
|
||||||
d.proxy.BufferPool = NewBufferPool()
|
d.proxy.BufferPool = NewBufferPool()
|
||||||
|
|
||||||
|
// Configure TLS to skip certificate verification for non-compliant certificates
|
||||||
|
if targetURL.Scheme == "https" {
|
||||||
|
d.proxy.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type EsInfo struct {
|
|||||||
InstanceId uint64 // 实例id
|
InstanceId uint64 // 实例id
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
|
Protocol string // 协议,默认http
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
Network string
|
Network string
|
||||||
@@ -58,14 +59,14 @@ func (di *EsInfo) Conn(ctx context.Context) (*EsConn, map[string]any, error) {
|
|||||||
err := di.IfUseSshTunnelChangeIpPort(ctx)
|
err := di.IfUseSshTunnelChangeIpPort(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("es ssh failed: %s, err:%s", di.baseUrl, err.Error())
|
logx.Errorf("es ssh failed: %s, err:%s", di.baseUrl, err.Error())
|
||||||
return nil, nil, errorx.NewBiz("es ssh failed: %s", err.Error())
|
return nil, nil, errorx.NewBizf("es ssh failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试获取es版本信息,调用接口:get /
|
// 尝试获取es版本信息,调用接口:get /
|
||||||
res, err := di.Ping()
|
res, err := di.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("es ping failed: %s, err:%s", di.baseUrl, err.Error())
|
logx.Errorf("es ping failed: %s, err:%s", di.baseUrl, err.Error())
|
||||||
return nil, nil, errorx.NewBiz("es ping failed: %s", err.Error())
|
return nil, nil, errorx.NewBizf("es ping failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
esc := &EsConn{Id: di.InstanceId, Info: di}
|
esc := &EsConn{Id: di.InstanceId, Info: di}
|
||||||
@@ -90,7 +91,14 @@ func (di *EsInfo) Ping() (map[string]any, error) {
|
|||||||
|
|
||||||
// ExecApi 执行api
|
// ExecApi 执行api
|
||||||
func (di *EsInfo) ExecApi(method, path string, data any, timeoutSecond ...int) (map[string]any, error) {
|
func (di *EsInfo) ExecApi(method, path string, data any, timeoutSecond ...int) (map[string]any, error) {
|
||||||
request := httpx.NewReq(di.baseUrl + path)
|
var request *httpx.Req
|
||||||
|
// Use insecure TLS client for HTTPS connections to handle non-compliant certificates
|
||||||
|
if di.Protocol == "https" {
|
||||||
|
request = httpx.NewReqWithInsecureTLS(di.baseUrl + path)
|
||||||
|
} else {
|
||||||
|
request = httpx.NewReq(di.baseUrl + path)
|
||||||
|
}
|
||||||
|
|
||||||
if di.authorization != "" {
|
if di.authorization != "" {
|
||||||
request.Header("Authorization", di.authorization)
|
request.Header("Authorization", di.authorization)
|
||||||
}
|
}
|
||||||
@@ -111,12 +119,17 @@ func (di *EsInfo) ExecApi(method, path string, data any, timeoutSecond ...int) (
|
|||||||
return request.PutObj(data).BodyToMap()
|
return request.PutObj(data).BodyToMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errorx.NewBiz("不支持的请求方法: %s", method)
|
return nil, errorx.NewBizf("不支持的请求方法: %s", method)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果使用了ssh隧道,将其host port改变其本地映射host port
|
// 如果使用了ssh隧道,将其host port改变其本地映射host port
|
||||||
func (di *EsInfo) IfUseSshTunnelChangeIpPort(ctx context.Context) error {
|
func (di *EsInfo) IfUseSshTunnelChangeIpPort(ctx context.Context) error {
|
||||||
|
// 设置默认协议
|
||||||
|
if di.Protocol == "" {
|
||||||
|
di.Protocol = "http"
|
||||||
|
}
|
||||||
|
|
||||||
// 开启ssh隧道
|
// 开启ssh隧道
|
||||||
if di.SshTunnelMachineId > 0 {
|
if di.SshTunnelMachineId > 0 {
|
||||||
stm, err := GetSshTunnel(ctx, di.SshTunnelMachineId)
|
stm, err := GetSshTunnel(ctx, di.SshTunnelMachineId)
|
||||||
@@ -130,9 +143,9 @@ func (di *EsInfo) IfUseSshTunnelChangeIpPort(ctx context.Context) error {
|
|||||||
di.Host = exposedIp
|
di.Host = exposedIp
|
||||||
di.Port = exposedPort
|
di.Port = exposedPort
|
||||||
di.useSshTunnel = true
|
di.useSshTunnel = true
|
||||||
di.baseUrl = fmt.Sprintf("http://%s:%d", exposedIp, exposedPort)
|
di.baseUrl = fmt.Sprintf("%s://%s:%d", di.Protocol, exposedIp, exposedPort)
|
||||||
} else {
|
} else {
|
||||||
di.baseUrl = fmt.Sprintf("http://%s:%d", di.Host, di.Port)
|
di.baseUrl = fmt.Sprintf("%s://%s:%d", di.Protocol, di.Host, di.Port)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,10 +312,10 @@ func (m *machineAppImpl) ToMachineInfoById(machineId uint64) (*mcm.MachineInfo,
|
|||||||
func (m *machineAppImpl) getMachineAndAuthCert(machineId uint64) (*entity.Machine, *tagentity.ResourceAuthCert, error) {
|
func (m *machineAppImpl) getMachineAndAuthCert(machineId uint64) (*entity.Machine, *tagentity.ResourceAuthCert, error) {
|
||||||
me, err := m.GetById(machineId)
|
me, err := m.GetById(machineId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errorx.NewBiz("[%d] machine not found", machineId)
|
return nil, nil, errorx.NewBizf("[%d] machine not found", machineId)
|
||||||
}
|
}
|
||||||
if me.Status != entity.MachineStatusEnable && me.Protocol == 1 {
|
if me.Status != entity.MachineStatusEnable && me.Protocol == 1 {
|
||||||
return nil, nil, errorx.NewBiz("[%s] machine has been disable", me.Code)
|
return nil, nil, errorx.NewBizf("[%s] machine has been disable", me.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
authCert, err := m.resourceAuthCertApp.GetResourceAuthCert(tagentity.TagTypeMachine, me.Code)
|
authCert, err := m.resourceAuthCertApp.GetResourceAuthCert(tagentity.TagTypeMachine, me.Code)
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ func (m *machineFileAppImpl) GetDirSize(ctx context.Context, opParam *dto.Machin
|
|||||||
//du: cannot access ‘/proc/19087/fdinfo/3’: No such file or directory\n
|
//du: cannot access ‘/proc/19087/fdinfo/3’: No such file or directory\n
|
||||||
//18G /\n
|
//18G /\n
|
||||||
if res == "" {
|
if res == "" {
|
||||||
return "", errorx.NewBiz("failed to get directory size: %s", err.Error())
|
return "", errorx.NewBizf("failed to get directory size: %s", err.Error())
|
||||||
}
|
}
|
||||||
strs := strings.Split(res, "\n")
|
strs := strings.Split(res, "\n")
|
||||||
res = strs[len(strs)-2]
|
res = strs[len(strs)-2]
|
||||||
@@ -247,7 +247,7 @@ func (m *machineFileAppImpl) CreateFile(ctx context.Context, opParam *dto.Machin
|
|||||||
}
|
}
|
||||||
file, err := sftpCli.Create(path)
|
file, err := sftpCli.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.NewBiz("failed to create file: %s", err.Error())
|
return nil, errorx.NewBizf("failed to create file: %s", err.Error())
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
return mi, err
|
return mi, err
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func (m *machineTermOpAppImpl) TermConn(ctx context.Context, cli *mcm.Cli, wsCon
|
|||||||
|
|
||||||
fileKey, wc, saveFileFunc, err := m.fileApp.NewWriter(ctx, "", fmt.Sprintf("mto_%d_%s.cast", termOpRecord.MachineId, timex.TimeNo()))
|
fileKey, wc, saveFileFunc, err := m.fileApp.NewWriter(ctx, "", fmt.Sprintf("mto_%d_%s.cast", termOpRecord.MachineId, timex.TimeNo()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to create a terminal playback log file: %v", err)
|
return errorx.NewBizf("failed to create a terminal playback log file: %v", err)
|
||||||
}
|
}
|
||||||
defer saveFileFunc(&err)
|
defer saveFileFunc(&err)
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func (c *Cli) GetSftpCli() (*sftp.Client, error) {
|
|||||||
if sftpclient == nil {
|
if sftpclient == nil {
|
||||||
sc, serr := sftp.NewClient(c.sshClient)
|
sc, serr := sftp.NewClient(c.sshClient)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
return nil, errorx.NewBiz("failed to obtain the sftp client: %s", serr.Error())
|
return nil, errorx.NewBizf("failed to obtain the sftp client: %s", serr.Error())
|
||||||
}
|
}
|
||||||
sftpclient = sc
|
sftpclient = sc
|
||||||
c.sftpClient = sftpclient
|
c.sftpClient = sftpclient
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func (mi *MachineInfo) Conn(ctx context.Context) (*Cli, error) {
|
|||||||
// 如果使用了ssh隧道,则修改机器ip port为暴露的ip port
|
// 如果使用了ssh隧道,则修改机器ip port为暴露的ip port
|
||||||
err := mi.IfUseSshTunnelChangeIpPort(ctx, false)
|
err := mi.IfUseSshTunnelChangeIpPort(ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.NewBiz("ssh tunnel connection failed: %s", err.Error())
|
return nil, errorx.NewBizf("ssh tunnel connection failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := &Cli{Info: mi}
|
cli := &Cli{Info: mi}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package mgm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"mayfly-go/pkg/logx"
|
"mayfly-go/pkg/logx"
|
||||||
|
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
@@ -28,5 +29,14 @@ func (mc *MongoConn) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (mc *MongoConn) Ping() error {
|
func (mc *MongoConn) Ping() error {
|
||||||
|
// 首先检查mc是否为nil
|
||||||
|
if mc == nil {
|
||||||
|
return fmt.Errorf("mc connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后检查mc.Cli是否为nil,这是避免空指针异常的关键
|
||||||
|
if mc.Cli == nil {
|
||||||
|
return fmt.Errorf("mc client is nil")
|
||||||
|
}
|
||||||
return mc.Cli.Ping(context.Background(), nil)
|
return mc.Cli.Ping(context.Background(), nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import "fmt"
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
AppName = "mayfly-go"
|
AppName = "mayfly-go"
|
||||||
Version = "v1.10.3"
|
Version = "v1.10.4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAppInfo() string {
|
func GetAppInfo() string {
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ func (r *redisAppImpl) FlowBizHandle(ctx context.Context, bizHandleParam *flowap
|
|||||||
|
|
||||||
runCmdParam, err := jsonx.To[*FlowRedisRunCmdBizForm](procinst.BizForm)
|
runCmdParam, err := jsonx.To[*FlowRedisRunCmdBizForm](procinst.BizForm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.NewBiz("failed to parse the business form information: %s", err.Error())
|
return nil, errorx.NewBizf("failed to parse the business form information: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
redisConn, err := r.GetRedisConn(ctx, runCmdParam.Id, runCmdParam.Db)
|
redisConn, err := r.GetRedisConn(ctx, runCmdParam.Id, runCmdParam.Db)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package rdm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"mayfly-go/pkg/errorx"
|
"mayfly-go/pkg/errorx"
|
||||||
"mayfly-go/pkg/logx"
|
"mayfly-go/pkg/logx"
|
||||||
|
|
||||||
@@ -41,7 +42,19 @@ func (r *RedisConn) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RedisConn) Ping() error {
|
func (r *RedisConn) Ping() error {
|
||||||
_, err := r.Cli.Ping(context.Background()).Result()
|
// 首先检查r是否为nil
|
||||||
|
if r == nil {
|
||||||
|
return fmt.Errorf("redis connection is nil")
|
||||||
|
}
|
||||||
|
// 然后检查r.Cli是否为nil,这是避免空指针异常的关键
|
||||||
|
if r.Cli == nil {
|
||||||
|
return fmt.Errorf("redis client is nil")
|
||||||
|
}
|
||||||
|
cmd := r.Cli.Ping(context.Background())
|
||||||
|
if cmd == nil {
|
||||||
|
return fmt.Errorf("the ping cmd is nil")
|
||||||
|
}
|
||||||
|
_, err := cmd.Result()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ func (re *RedisInfo) connStandalone() (*RedisConn, error) {
|
|||||||
_, e := cli.Ping(context.Background()).Result()
|
_, e := cli.Ping(context.Background()).Result()
|
||||||
if e != nil {
|
if e != nil {
|
||||||
cli.Close()
|
cli.Close()
|
||||||
return nil, errorx.NewBiz("redis standalone connection failed: %s", e.Error())
|
return nil, errorx.NewBizf("redis standalone connection failed: %s", e.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
logx.Infof("redis standalone connection: %s/%d", re.Host, re.Db)
|
logx.Infof("redis standalone connection: %s/%d", re.Host, re.Db)
|
||||||
@@ -95,7 +95,7 @@ func (re *RedisInfo) connCluster() (*RedisConn, error) {
|
|||||||
_, e := cli.Ping(context.Background()).Result()
|
_, e := cli.Ping(context.Background()).Result()
|
||||||
if e != nil {
|
if e != nil {
|
||||||
cli.Close()
|
cli.Close()
|
||||||
return nil, errorx.NewBiz("redis cluster connection failed: %s", e.Error())
|
return nil, errorx.NewBizf("redis cluster connection failed: %s", e.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
logx.Infof("redis cluster connection: %s/%d", re.Host, re.Db)
|
logx.Infof("redis cluster connection: %s/%d", re.Host, re.Db)
|
||||||
@@ -128,7 +128,7 @@ func (re *RedisInfo) connSentinel() (*RedisConn, error) {
|
|||||||
_, e := cli.Ping(context.Background()).Result()
|
_, e := cli.Ping(context.Background()).Result()
|
||||||
if e != nil {
|
if e != nil {
|
||||||
cli.Close()
|
cli.Close()
|
||||||
return nil, errorx.NewBiz("redis sentinel connection failed: %s", e.Error())
|
return nil, errorx.NewBizf("redis sentinel connection failed: %s", e.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
logx.Infof("redis sentinel connection: %s/%d", re.Host, re.Db)
|
logx.Infof("redis sentinel connection: %s/%d", re.Host, re.Db)
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ func (a *Account) GetPermissions(rc *req.Ctx) {
|
|||||||
var resources vo.AccountResourceVOList
|
var resources vo.AccountResourceVOList
|
||||||
// 获取账号菜单资源
|
// 获取账号菜单资源
|
||||||
biz.ErrIsNil(a.resourceApp.GetAccountResources(account.Id, &resources))
|
biz.ErrIsNil(a.resourceApp.GetAccountResources(account.Id, &resources))
|
||||||
|
biz.IsTrue(len(resources) > 0, "no permission")
|
||||||
// 菜单树与权限code数组
|
// 菜单树与权限code数组
|
||||||
var menus vo.AccountResourceVOList
|
var menus vo.AccountResourceVOList
|
||||||
var permissions []string
|
var permissions []string
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func (r *resourceAuthCertAppImpl) RelateAuthCert(ctx context.Context, params *dt
|
|||||||
|
|
||||||
existNameAc := &entity.ResourceAuthCert{Name: addAcName}
|
existNameAc := &entity.ResourceAuthCert{Name: addAcName}
|
||||||
if r.GetByCond(existNameAc) == nil && existNameAc.ResourceCode != resourceCode {
|
if r.GetByCond(existNameAc) == nil && existNameAc.ResourceCode != resourceCode {
|
||||||
return errorx.NewBiz("The name of the authorization credential cannot be repeated: [%s]", addAcName)
|
return errorx.NewBizf("The name of the authorization credential cannot be repeated: [%s]", addAcName)
|
||||||
}
|
}
|
||||||
|
|
||||||
addAuthCerts = append(addAuthCerts, addAc)
|
addAuthCerts = append(addAuthCerts, addAc)
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ func (p *tagTreeAppImpl) RelateTagsByCodeAndType(ctx context.Context, param *dto
|
|||||||
|
|
||||||
if len(parentTagCodePaths) == 0 {
|
if len(parentTagCodePaths) == 0 {
|
||||||
// 不满足满足条件的标签
|
// 不满足满足条件的标签
|
||||||
return errorx.NewBiz("There is no tag that satisfies [type=%d, code=%s]", parentTagType, parentTagCode)
|
return errorx.NewBizf("There is no tag that satisfies [type=%d, code=%s]", parentTagType, parentTagCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tag := range param.Tags {
|
for _, tag := range param.Tags {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func V1_10() []*gormigrate.Migration {
|
|||||||
migrations = append(migrations, V1_10_1()...)
|
migrations = append(migrations, V1_10_1()...)
|
||||||
migrations = append(migrations, V1_10_2()...)
|
migrations = append(migrations, V1_10_2()...)
|
||||||
migrations = append(migrations, V1_10_3()...)
|
migrations = append(migrations, V1_10_3()...)
|
||||||
|
migrations = append(migrations, V1_10_4()...)
|
||||||
return migrations
|
return migrations
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,3 +327,28 @@ func V1_10_3() []*gormigrate.Migration {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func V1_10_4() []*gormigrate.Migration {
|
||||||
|
return []*gormigrate.Migration{
|
||||||
|
{
|
||||||
|
ID: "20251023-v1.10.4",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
// 给EsInstance表添加protocol列,默认值为http, 20251023,fudawei
|
||||||
|
if !tx.Migrator().HasColumn(&esentity.EsInstance{}, "protocol") {
|
||||||
|
// 先添加可为空的列
|
||||||
|
if err := tx.Exec("ALTER TABLE t_es_instance ADD COLUMN protocol VARCHAR(10) DEFAULT 'http'").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 更新所有现有记录为默认值http
|
||||||
|
if err := tx.Exec("UPDATE t_es_instance SET protocol = 'http' WHERE protocol IS NULL OR protocol = ''").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Rollback: func(tx *gorm.DB) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func ErrIsNil(err error, msgAndParams ...any) {
|
|||||||
panic(errorx.NewBiz(err.Error()))
|
panic(errorx.NewBiz(err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
panic(errorx.NewBiz(msgAndParams[0].(string), msgAndParams[1:]...))
|
panic(errorx.NewBizf(msgAndParams[0].(string), msgAndParams[1:]...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ func ErrIsNilI(ctx context.Context, err error, msgId i18n.MsgId, attrs ...any) {
|
|||||||
|
|
||||||
func ErrNotNil(err error, msg string, params ...any) {
|
func ErrNotNil(err error, msg string, params ...any) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
panic(errorx.NewBiz(msg, params...))
|
panic(errorx.NewBizf(msg, params...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ func ErrNotNil(err error, msg string, params ...any) {
|
|||||||
// biz.ErrIsNilAppendErr(err, "xxxx: %s")
|
// biz.ErrIsNilAppendErr(err, "xxxx: %s")
|
||||||
func ErrIsNilAppendErr(err error, msg string) {
|
func ErrIsNilAppendErr(err error, msg string) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errorx.NewBiz(msg, err.Error()))
|
panic(errorx.NewBizf(msg, err.Error()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,13 +63,13 @@ func ErrIsNilAppendErr(err error, msg string) {
|
|||||||
// biz.ErrIsNilAppendErr(err, "xxxx: %s")
|
// biz.ErrIsNilAppendErr(err, "xxxx: %s")
|
||||||
func ErrIsNilAppendErrI(ctx context.Context, err error, msgId i18n.MsgId) {
|
func ErrIsNilAppendErrI(ctx context.Context, err error, msgId i18n.MsgId) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errorx.NewBiz(i18n.TC(ctx, msgId), err.Error()))
|
panic(errorx.NewBizf(i18n.TC(ctx, msgId), err.Error()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsTrue(exp bool, msg string, params ...any) {
|
func IsTrue(exp bool, msg string, params ...any) {
|
||||||
if !exp {
|
if !exp {
|
||||||
panic(errorx.NewBiz(msg, params...))
|
panic(errorx.NewBizf(msg, params...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,19 +87,19 @@ func IsTrueBy(exp bool, err *errorx.BizError) {
|
|||||||
|
|
||||||
func NotEmpty(str string, msg string, params ...any) {
|
func NotEmpty(str string, msg string, params ...any) {
|
||||||
if str == "" {
|
if str == "" {
|
||||||
panic(errorx.NewBiz(msg, params...))
|
panic(errorx.NewBizf(msg, params...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NotNil(data any, msg string, params ...any) {
|
func NotNil(data any, msg string, params ...any) {
|
||||||
if reflect.ValueOf(data).IsNil() {
|
if reflect.ValueOf(data).IsNil() {
|
||||||
panic(errorx.NewBiz(msg, params...))
|
panic(errorx.NewBizf(msg, params...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NotBlank(data any, msg string, params ...any) {
|
func NotBlank(data any, msg string, params ...any) {
|
||||||
if anyx.IsBlank(data) {
|
if anyx.IsBlank(data) {
|
||||||
panic(errorx.NewBiz(msg, params...))
|
panic(errorx.NewBizf(msg, params...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,13 @@ func (e BizError) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewBiz 创建业务逻辑错误结构体,默认为业务逻辑错误
|
// NewBiz 创建业务逻辑错误结构体,默认为业务逻辑错误
|
||||||
func NewBiz(msg string, formatValues ...any) *BizError {
|
func NewBiz(msg string) *BizError {
|
||||||
return &BizError{code: BizErr.code, err: fmt.Sprintf(msg, formatValues...)}
|
return &BizError{code: BizErr.code, err: msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBizf 创建业务逻辑错误结构体,可设置格式化参数
|
||||||
|
func NewBizf(format string, formatValues ...any) *BizError {
|
||||||
|
return NewBiz(fmt.Sprintf(format, formatValues...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBizI 使用i18n的msgId创建业务逻辑错误结构体,默认为业务逻辑错误 (使用ctx中的国际化语言)
|
// NewBizI 使用i18n的msgId创建业务逻辑错误结构体,默认为业务逻辑错误 (使用ctx中的国际化语言)
|
||||||
@@ -47,7 +52,12 @@ func NewBizI(ctx context.Context, msgId i18n.MsgId, attrs ...any) *BizError {
|
|||||||
return &BizError{code: BizErr.code, err: i18n.TC(ctx, msgId, attrs...)}
|
return &BizError{code: BizErr.code, err: i18n.TC(ctx, msgId, attrs...)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建业务逻辑错误结构体,可设置指定错误code
|
// NewBizCode 创建业务逻辑错误结构体,可设置指定错误code
|
||||||
func NewBizCode(code int16, msg string, formats ...any) *BizError {
|
func NewBizCode(code int16, msg string) *BizError {
|
||||||
return &BizError{code: code, err: fmt.Sprintf(msg, formats...)}
|
return &BizError{code: code, err: msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBizCodef 创建业务逻辑错误结构体,可设置指定错误code,并且支持格式化参数
|
||||||
|
func NewBizCodef(code int16, format string, formats ...any) *BizError {
|
||||||
|
return NewBizCode(code, fmt.Sprintf(format, formats...))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package httpx
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -41,6 +42,16 @@ func NewReq(url string) *Req {
|
|||||||
return &Req{url: url, client: http.Client{}}
|
return &Req{url: url, client: http.Client{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建一个请求(不验证TLS证书)
|
||||||
|
func NewReqWithInsecureTLS(url string) *Req {
|
||||||
|
transport := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &Req{url: url, client: http.Client{Transport: transport}}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Req) Url(url string) *Req {
|
func (r *Req) Url(url string) *Req {
|
||||||
r.url = url
|
r.url = url
|
||||||
return r
|
return r
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ func Errorf(format string, args ...any) {
|
|||||||
Log(context.Background(), slog.LevelError, fmt.Sprintf(format, args...))
|
Log(context.Background(), slog.LevelError, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 错误记录,并将堆栈信息添加至msg里,默认记录10个堆栈信息
|
// ErrorTraceContext 错误记录,并将堆栈信息添加至msg里,默认记录10个堆栈信息
|
||||||
func ErrorTrace(msg string, err any) {
|
func ErrorTraceContext(ctx context.Context, msg string, err any) {
|
||||||
errMsg := ""
|
errMsg := ""
|
||||||
switch t := err.(type) {
|
switch t := err.(type) {
|
||||||
case error:
|
case error:
|
||||||
@@ -132,7 +132,12 @@ func ErrorTrace(msg string, err any) {
|
|||||||
default:
|
default:
|
||||||
errMsg = fmt.Sprintf("%v", t)
|
errMsg = fmt.Sprintf("%v", t)
|
||||||
}
|
}
|
||||||
Log(context.Background(), slog.LevelError, fmt.Sprintf(msg+"\n%s\n%s", errMsg, runtimex.StackStr(2, 20)))
|
Log(ctx, slog.LevelError, fmt.Sprintf(msg+"\n%s\n%s", errMsg, runtimex.StackStr(2, 20)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorTrace 错误记录,并将堆栈信息添加至msg里,默认记录10个堆栈信息
|
||||||
|
func ErrorTrace(msg string, err any) {
|
||||||
|
ErrorTraceContext(context.Background(), msg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrorWithFields(ctx context.Context, msg string, mapFields map[string]any) {
|
func ErrorWithFields(ctx context.Context, msg string, mapFields map[string]any) {
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ func Error(bizerr *errorx.BizError) *Result {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 返回服务器错误Result
|
// 返回服务器错误Result
|
||||||
func ServerError() *Result {
|
func ServerError(msg string) *Result {
|
||||||
return Error(errorx.ServerError)
|
serverErr := errorx.NewBizCode(errorx.ServerError.Code(), msg)
|
||||||
|
return Error(serverErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TokenError() *Result {
|
func TokenError() *Result {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package req
|
package req
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"mayfly-go/pkg/biz"
|
"mayfly-go/pkg/biz"
|
||||||
"mayfly-go/pkg/contextx"
|
"mayfly-go/pkg/contextx"
|
||||||
"mayfly-go/pkg/errorx"
|
"mayfly-go/pkg/errorx"
|
||||||
@@ -109,8 +111,8 @@ func (rc *Ctx) res() {
|
|||||||
case *errorx.BizError:
|
case *errorx.BizError:
|
||||||
rc.JSONRes(http.StatusOK, model.Error(t))
|
rc.JSONRes(http.StatusOK, model.Error(t))
|
||||||
default:
|
default:
|
||||||
logx.ErrorTrace("服务器错误", t)
|
logx.ErrorTrace("server error", t)
|
||||||
rc.JSONRes(http.StatusOK, model.ServerError())
|
rc.JSONRes(http.StatusOK, model.ServerError(fmt.Sprintf("server error [%d-%s]", errorx.ServerError.Code(), cmp.Or(contextx.GetTraceId(rc.MetaCtx), "none"))))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func Ip2Region(ip string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2、用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
|
// 2、用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
|
||||||
searcher, err := xdb.NewWithVectorIndex(ip2RegionDbPath, vectorIndex)
|
searcher, err := xdb.NewWithVectorIndex(xdb.IPv4, ip2RegionDbPath, vectorIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("failed to create searcher with vector index: %s\n", err)
|
logx.Errorf("failed to create searcher with vector index: %s\n", err)
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -9,29 +9,60 @@ import (
|
|||||||
// 心跳间隔
|
// 心跳间隔
|
||||||
const heartbeatInterval = 25 * time.Second
|
const heartbeatInterval = 25 * time.Second
|
||||||
|
|
||||||
// 单个用户的全部的连接, key->clientId, value->Client
|
// UserClient 用户全部的连接
|
||||||
type UserClients map[string]*Client
|
type UserClient struct {
|
||||||
|
clients map[string]*Client // key->clientId, value->Client
|
||||||
func (ucs UserClients) GetByCid(clientId string) *Client {
|
mutex sync.RWMutex
|
||||||
return ucs[clientId]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ucs UserClients) AddClient(client *Client) {
|
func NewUserClient() *UserClient {
|
||||||
ucs[client.ClientId] = client
|
return &UserClient{
|
||||||
|
clients: make(map[string]*Client),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ucs UserClients) DeleteByCid(clientId string) {
|
// AllClients 获取全部的连接
|
||||||
delete(ucs, clientId)
|
func (ucs *UserClient) AllClients() []*Client {
|
||||||
|
ucs.mutex.RLock()
|
||||||
|
defer ucs.mutex.RUnlock()
|
||||||
|
result := make([]*Client, 0, len(ucs.clients))
|
||||||
|
for _, client := range ucs.clients {
|
||||||
|
result = append(result, client)
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ucs UserClients) Count() int {
|
// GetByCid 获取指定客户端ID的客户端
|
||||||
return len(ucs)
|
func (ucs *UserClient) GetByCid(clientId string) *Client {
|
||||||
|
ucs.mutex.RLock()
|
||||||
|
defer ucs.mutex.RUnlock()
|
||||||
|
return ucs.clients[clientId]
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddClient 添加客户端
|
||||||
|
func (ucs *UserClient) AddClient(client *Client) {
|
||||||
|
ucs.mutex.Lock()
|
||||||
|
defer ucs.mutex.Unlock()
|
||||||
|
ucs.clients[client.ClientId] = client
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByCid 删除指定客户端ID的客户端
|
||||||
|
func (ucs *UserClient) DeleteByCid(clientId string) {
|
||||||
|
ucs.mutex.Lock()
|
||||||
|
defer ucs.mutex.Unlock()
|
||||||
|
delete(ucs.clients, clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count 返回客户端数量
|
||||||
|
func (ucs *UserClient) Count() int {
|
||||||
|
ucs.mutex.RLock()
|
||||||
|
defer ucs.mutex.RUnlock()
|
||||||
|
return len(ucs.clients)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接管理
|
// 连接管理
|
||||||
type ClientManager struct {
|
type ClientManager struct {
|
||||||
UserClientsMap map[UserId]UserClients // 全部的用户连接, key->userid, value->UserClients
|
UserClientMap sync.Map // 全部的用户连接, key->userid, value->*UserClient
|
||||||
RwLock sync.RWMutex // 读写锁
|
|
||||||
|
|
||||||
ConnectChan chan *Client // 连接处理
|
ConnectChan chan *Client // 连接处理
|
||||||
DisConnectChan chan *Client // 断开连接处理
|
DisConnectChan chan *Client // 断开连接处理
|
||||||
@@ -40,7 +71,7 @@ type ClientManager struct {
|
|||||||
|
|
||||||
func NewClientManager() (clientManager *ClientManager) {
|
func NewClientManager() (clientManager *ClientManager) {
|
||||||
return &ClientManager{
|
return &ClientManager{
|
||||||
UserClientsMap: make(map[UserId]UserClients),
|
UserClientMap: sync.Map{},
|
||||||
ConnectChan: make(chan *Client, 10),
|
ConnectChan: make(chan *Client, 10),
|
||||||
DisConnectChan: make(chan *Client, 10),
|
DisConnectChan: make(chan *Client, 10),
|
||||||
MsgChan: make(chan *Msg, 100),
|
MsgChan: make(chan *Msg, 100),
|
||||||
@@ -78,24 +109,30 @@ func (manager *ClientManager) CloseClient(client *Client) {
|
|||||||
|
|
||||||
// 根据用户id关闭客户端连接
|
// 根据用户id关闭客户端连接
|
||||||
func (manager *ClientManager) CloseByUid(userId UserId) {
|
func (manager *ClientManager) CloseByUid(userId UserId) {
|
||||||
for _, client := range manager.GetByUid(userId) {
|
userClient := manager.GetByUid(userId)
|
||||||
|
for _, client := range userClient.AllClients() {
|
||||||
manager.CloseClient(client)
|
manager.CloseClient(client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取所有的客户端
|
// 获取所有的客户端
|
||||||
func (manager *ClientManager) AllUserClient() map[UserId]UserClients {
|
func (manager *ClientManager) AllUserClient() map[UserId]*UserClient {
|
||||||
manager.RwLock.RLock()
|
result := make(map[UserId]*UserClient)
|
||||||
defer manager.RwLock.RUnlock()
|
manager.UserClientMap.Range(func(key, value any) bool {
|
||||||
|
userId := key.(UserId)
|
||||||
return manager.UserClientsMap
|
userClients := value.(*UserClient)
|
||||||
|
result[userId] = userClients
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通过userId获取用户所有客户端信息
|
// 通过userId获取用户所有客户端信息
|
||||||
func (manager *ClientManager) GetByUid(userId UserId) UserClients {
|
func (manager *ClientManager) GetByUid(userId UserId) *UserClient {
|
||||||
manager.RwLock.RLock()
|
if value, ok := manager.UserClientMap.Load(userId); ok {
|
||||||
defer manager.RwLock.RUnlock()
|
return value.(*UserClient)
|
||||||
return manager.UserClientsMap[userId]
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通过userId和clientId获取客户端信息
|
// 通过userId和clientId获取客户端信息
|
||||||
@@ -108,9 +145,12 @@ func (manager *ClientManager) GetByUidAndCid(uid UserId, clientId string) *Clien
|
|||||||
|
|
||||||
// 客户端数量
|
// 客户端数量
|
||||||
func (manager *ClientManager) Count() int {
|
func (manager *ClientManager) Count() int {
|
||||||
manager.RwLock.RLock()
|
count := 0
|
||||||
defer manager.RwLock.RUnlock()
|
manager.UserClientMap.Range(func(key, value any) bool {
|
||||||
return len(manager.UserClientsMap)
|
count++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送json数据给指定用户
|
// 发送json数据给指定用户
|
||||||
@@ -139,7 +179,8 @@ func (manager *ClientManager) WriteMessage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cid为空,则向该用户所有客户端发送该消息
|
// cid为空,则向该用户所有客户端发送该消息
|
||||||
for _, cli := range manager.GetByUid(uid) {
|
userClients := manager.GetByUid(uid)
|
||||||
|
for _, cli := range userClients.AllClients() {
|
||||||
if err := cli.WriteMsg(msg); err != nil {
|
if err := cli.WriteMsg(msg); err != nil {
|
||||||
logx.Warnf("ws send message failed - [uid=%d, cid=%s]: %s", uid, cli.ClientId, err.Error())
|
logx.Warnf("ws send message failed - [uid=%d, cid=%s]: %s", uid, cli.ClientId, err.Error())
|
||||||
}
|
}
|
||||||
@@ -156,21 +197,23 @@ func (manager *ClientManager) HeartbeatTimer() {
|
|||||||
for {
|
for {
|
||||||
<-ticker.C
|
<-ticker.C
|
||||||
//发送心跳
|
//发送心跳
|
||||||
for userId, clis := range manager.AllUserClient() {
|
manager.UserClientMap.Range(func(key, value any) bool {
|
||||||
for _, cli := range clis {
|
userId := key.(UserId)
|
||||||
|
userClient := value.(*UserClient)
|
||||||
|
for _, cli := range userClient.AllClients() {
|
||||||
if cli == nil || cli.WsConn == nil {
|
if cli == nil || cli.WsConn == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := cli.Ping(); err != nil {
|
if err := cli.Ping(); err != nil {
|
||||||
manager.CloseClient(cli)
|
manager.CloseClient(cli)
|
||||||
logx.Debugf("WS - failed to send heartbeat: uid=%v, cid=%s, usercount=%d", userId, cli.ClientId, Manager.Count())
|
logx.Debugf("WS - failed to send heartbeat: uid=%v, cid=%s, usercount=%d", userId, cli.ClientId, manager.Count())
|
||||||
} else {
|
} else {
|
||||||
logx.Debugf("WS - send heartbeat successfully: uid=%v, cid=%s", userId, cli.ClientId)
|
logx.Debugf("WS - send heartbeat successfully: uid=%v, cid=%s", userId, cli.ClientId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,31 +235,29 @@ func (manager *ClientManager) doDisconnect(client *Client) {
|
|||||||
client.WsConn = nil
|
client.WsConn = nil
|
||||||
}
|
}
|
||||||
manager.delUserClient4Map(client)
|
manager.delUserClient4Map(client)
|
||||||
logx.Debugf("WS client disconnected: uid=%d, cid=%s, usercount=%d", client.UserId, client.ClientId, Manager.Count())
|
logx.Debugf("WS client disconnected: uid=%d, cid=%s, usercount=%d", client.UserId, client.ClientId, manager.Count())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *ClientManager) addUserClient2Map(client *Client) {
|
func (manager *ClientManager) addUserClient2Map(client *Client) {
|
||||||
manager.RwLock.Lock()
|
// 先尝试加载现有的UserClients
|
||||||
defer manager.RwLock.Unlock()
|
if value, ok := manager.UserClientMap.Load(client.UserId); ok {
|
||||||
|
userClient := value.(*UserClient)
|
||||||
userClients := manager.UserClientsMap[client.UserId]
|
userClient.AddClient(client)
|
||||||
if userClients == nil {
|
} else {
|
||||||
userClients = make(UserClients)
|
// 创建新的UserClients
|
||||||
manager.UserClientsMap[client.UserId] = userClients
|
userClient := NewUserClient()
|
||||||
|
userClient.AddClient(client)
|
||||||
|
manager.UserClientMap.Store(client.UserId, userClient)
|
||||||
}
|
}
|
||||||
userClients.AddClient(client)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *ClientManager) delUserClient4Map(client *Client) {
|
func (manager *ClientManager) delUserClient4Map(client *Client) {
|
||||||
manager.RwLock.Lock()
|
if value, ok := manager.UserClientMap.Load(client.UserId); ok {
|
||||||
defer manager.RwLock.Unlock()
|
userClient := value.(*UserClient)
|
||||||
|
userClient.DeleteByCid(client.ClientId)
|
||||||
userClients := manager.UserClientsMap[client.UserId]
|
|
||||||
if userClients != nil {
|
|
||||||
userClients.DeleteByCid(client.ClientId)
|
|
||||||
// 如果用户所有客户端都关闭,则移除manager中的UserClientsMap值
|
// 如果用户所有客户端都关闭,则移除manager中的UserClientsMap值
|
||||||
if userClients.Count() == 0 {
|
if userClient.Count() == 0 {
|
||||||
delete(manager.UserClientsMap, client.UserId)
|
manager.UserClientMap.Delete(client.UserId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user