mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30:25 +08:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d711a36749 | ||
|
|
9dbf104ef1 | ||
|
|
20eb06fb28 | ||
|
|
9c20bdef39 | ||
|
|
3fdd98a390 | ||
|
|
d4f456c0cf | ||
|
|
f2b6e15cf4 | ||
|
|
6be0ea6aed | ||
|
|
eee08be2cc | ||
|
|
252fc553f2 | ||
|
|
ac2ceed3f9 | ||
|
|
3f828cc5b0 | ||
|
|
fc1b9ef35d | ||
|
|
d0b71a1c40 | ||
|
|
a743a6a05a | ||
|
|
0e6b9713ce | ||
|
|
b9afbc764d | ||
|
|
923e183a67 | ||
|
|
7e9a381641 | ||
|
|
bed95254d0 | ||
|
|
e4d13f3377 | ||
|
|
d530365ef9 | ||
|
|
070d4ea104 | ||
|
|
3fc86f0fae | ||
|
|
3b77ab2727 | ||
|
|
76cb991282 | ||
|
|
9efd20f1b9 | ||
|
|
de5b9e46d3 | ||
|
|
f27d3d200f | ||
|
|
f4a64b96a9 | ||
|
|
9a59749763 | ||
|
|
b017b902f8 | ||
|
|
7c53353c60 |
@@ -5,7 +5,7 @@ WORKDIR /mayfly
|
||||
|
||||
COPY mayfly_go_web .
|
||||
|
||||
RUN yarn config set registry 'https://registry.npm.taobao.org' && \
|
||||
RUN yarn config set registry 'https://registry.npmmirror.com' && \
|
||||
yarn install && \
|
||||
yarn build
|
||||
|
||||
@@ -24,7 +24,7 @@ COPY --from=fe-builder /mayfly/dist /mayfly/static/static
|
||||
|
||||
# Build
|
||||
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
|
||||
go build -a \
|
||||
go build -a -ldflags=-w \
|
||||
-o mayfly-go main.go
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
### 介绍
|
||||
|
||||
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle 达梦 高斯)、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
|
||||
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle 达梦 高斯 sqlite)、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
|
||||
|
||||
### 开发语言与主要框架
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 lyt-Top
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -17,7 +17,7 @@
|
||||
"countup.js": "^2.7.0",
|
||||
"cropperjs": "^1.5.11",
|
||||
"echarts": "^5.4.3",
|
||||
"element-plus": "^2.5.1",
|
||||
"element-plus": "^2.5.5",
|
||||
"js-base64": "^3.7.5",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -33,7 +33,7 @@
|
||||
"splitpanes": "^3.1.5",
|
||||
"sql-formatter": "^15.0.2",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.4.14",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
@@ -49,13 +49,14 @@
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vue/compiler-sfc": "^3.4.14",
|
||||
"code-inspector-plugin": "^0.4.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"prettier": "^3.1.0",
|
||||
"sass": "^1.69.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^5.0.11",
|
||||
"vite": "^5.0.12",
|
||||
"vue-eslint-parser": "^9.4.0"
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -55,11 +55,11 @@
|
||||
"unicode_decimal": 58905
|
||||
},
|
||||
{
|
||||
"icon_id": "11617944",
|
||||
"icon_id": "25271976",
|
||||
"name": "oracle",
|
||||
"font_class": "oracle",
|
||||
"unicode": "e6ea",
|
||||
"unicode_decimal": 59114
|
||||
"unicode": "e507",
|
||||
"unicode_decimal": 58631
|
||||
},
|
||||
{
|
||||
"icon_id": "8105644",
|
||||
@@ -67,6 +67,41 @@
|
||||
"font_class": "mariadb",
|
||||
"unicode": "e513",
|
||||
"unicode_decimal": 58643
|
||||
},
|
||||
{
|
||||
"icon_id": "13601813",
|
||||
"name": "sqlite",
|
||||
"font_class": "sqlite",
|
||||
"unicode": "e546",
|
||||
"unicode_decimal": 58694
|
||||
},
|
||||
{
|
||||
"icon_id": "29340317",
|
||||
"name": "temp-mssql",
|
||||
"font_class": "MSSQLNATIVE",
|
||||
"unicode": "e600",
|
||||
"unicode_decimal": 58880
|
||||
},
|
||||
{
|
||||
"icon_id": "7699332",
|
||||
"name": "gaussdb",
|
||||
"font_class": "gauss",
|
||||
"unicode": "e683",
|
||||
"unicode_decimal": 59011
|
||||
},
|
||||
{
|
||||
"icon_id": "34836637",
|
||||
"name": "kingbase",
|
||||
"font_class": "kingbase",
|
||||
"unicode": "e882",
|
||||
"unicode_decimal": 59522
|
||||
},
|
||||
{
|
||||
"icon_id": "33047500",
|
||||
"name": "vastbase",
|
||||
"font_class": "vastbase",
|
||||
"unicode": "e62b",
|
||||
"unicode_decimal": 58923
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.7.0',
|
||||
version: 'v1.7.3',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-input v-model="cron" placeholder="可点击左边按钮进行可视化配置">
|
||||
<el-input v-model="cron" placeholder="可点击左边按钮配置">
|
||||
<template #prepend>
|
||||
<el-button @click="showCron = true" icon="Pointer"></el-button>
|
||||
</template>
|
||||
|
||||
@@ -119,8 +119,8 @@ const open = (optionProps: MonacoEditorDialogProps) => {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
editorRef.value?.format();
|
||||
editorRef.value?.focus();
|
||||
editorRef.value?.format();
|
||||
}, 300);
|
||||
|
||||
state.dialogVisible = true;
|
||||
|
||||
@@ -189,7 +189,7 @@ const emit = defineEmits(['update:queryForm', 'update:selectionData', 'pageChang
|
||||
|
||||
export interface PageTableProps {
|
||||
size?: string;
|
||||
pageApi: Api; // 请求表格数据的 api
|
||||
pageApi?: Api; // 请求表格数据的 api
|
||||
columns: TableColumn[]; // 列配置项 ==> 必传
|
||||
showSelection?: boolean;
|
||||
selectable?: (row: any) => boolean; // 是否可选
|
||||
@@ -257,7 +257,7 @@ const changeSimpleFormItem = (searchItem: SearchItem) => {
|
||||
nowSearchItem.value = searchItem;
|
||||
};
|
||||
|
||||
const { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
||||
let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
||||
props.pageable,
|
||||
props.pageApi,
|
||||
queryForm,
|
||||
@@ -288,6 +288,13 @@ watch(isShowSearch, () => {
|
||||
calcuTableHeight();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newValue: any) => {
|
||||
tableData = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
calcuTableHeight();
|
||||
useEventListener(window, 'resize', calcuTableHeight);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import 'xterm/css/xterm.css';
|
||||
import { Terminal } from 'xterm';
|
||||
import { ITheme, Terminal } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import { SearchAddon } from 'xterm-addon-search';
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
@@ -92,12 +92,13 @@ function init() {
|
||||
cursorBlink: true,
|
||||
disableStdin: false,
|
||||
allowProposedApi: true,
|
||||
fastScrollModifier: 'ctrl',
|
||||
theme: {
|
||||
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
||||
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
||||
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
||||
// cursorAccent: "red", // 光标停止颜色
|
||||
} as any,
|
||||
} as ITheme,
|
||||
});
|
||||
term.open(terminalRef.value);
|
||||
|
||||
@@ -105,7 +106,7 @@ function init() {
|
||||
const fitAddon = new FitAddon();
|
||||
state.addon.fit = fitAddon;
|
||||
term.loadAddon(fitAddon);
|
||||
fitTerminal();
|
||||
resize();
|
||||
|
||||
// 注册搜索组件
|
||||
const searchAddon = new SearchAddon();
|
||||
@@ -146,7 +147,7 @@ const onConnected = () => {
|
||||
state.status = TerminalStatus.Connected;
|
||||
|
||||
// 注册窗口大小监听器
|
||||
useEventListener('resize', debounce(fitTerminal, 400));
|
||||
useEventListener('resize', debounce(resize, 400));
|
||||
|
||||
focus();
|
||||
|
||||
@@ -158,17 +159,11 @@ const onConnected = () => {
|
||||
|
||||
// 自适应终端
|
||||
const fitTerminal = () => {
|
||||
const dimensions = state.addon.fit && state.addon.fit.proposeDimensions();
|
||||
if (!dimensions) {
|
||||
return;
|
||||
}
|
||||
if (dimensions?.cols && dimensions?.rows) {
|
||||
term.resize(dimensions.cols, dimensions.rows);
|
||||
}
|
||||
resize();
|
||||
};
|
||||
|
||||
const focus = () => {
|
||||
setTimeout(() => term.focus(), 400);
|
||||
setTimeout(() => term.focus(), 100);
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
@@ -265,7 +260,13 @@ const getStatus = (): TerminalStatus => {
|
||||
return state.status;
|
||||
};
|
||||
|
||||
defineExpose({ init, fitTerminal, focus, clear, close, getStatus });
|
||||
const resize = () => {
|
||||
nextTick(() => {
|
||||
state.addon.fit.fit();
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize, resize });
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#terminal-body {
|
||||
|
||||
@@ -259,6 +259,10 @@ defineExpose({
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
padding: 1px 1px;
|
||||
}
|
||||
|
||||
// 取消body最大高度,否则全屏有问题
|
||||
.el-dialog__body {
|
||||
max-height: 100% !important;
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
<template>
|
||||
<div class="layout-search-dialog">
|
||||
<el-dialog v-model="state.isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false">
|
||||
<el-autocomplete v-model="state.menuQuery" :fetch-suggestions="menuSearch" placeholder="菜单搜索"
|
||||
prefix-icon="el-icon-search" ref="layoutMenuAutocompleteRef" @select="onHandleSelect" @blur="onSearchBlur">
|
||||
<el-autocomplete
|
||||
v-model="state.menuQuery"
|
||||
:fetch-suggestions="menuSearch"
|
||||
placeholder="菜单搜索"
|
||||
prefix-icon="el-icon-search"
|
||||
ref="layoutMenuAutocompleteRef"
|
||||
@select="onHandleSelect"
|
||||
@blur="onSearchBlur"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<search />
|
||||
</el-icon>
|
||||
</template>
|
||||
<template #default="{ item }">
|
||||
<div>
|
||||
<SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}
|
||||
</div>
|
||||
<div><SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}</div>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-dialog>
|
||||
@@ -23,7 +28,7 @@ import { reactive, ref, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoutesList } from '@/store/routesList';
|
||||
|
||||
const layoutMenuAutocompleteRef: any = ref(null);;
|
||||
const layoutMenuAutocompleteRef: any = ref(null);
|
||||
const router = useRouter();
|
||||
const state: any = reactive({
|
||||
isShowSearch: false,
|
||||
@@ -54,8 +59,7 @@ const menuSearch = (queryString: any, cb: any) => {
|
||||
const createFilter = (queryString: any) => {
|
||||
return (restaurant: any) => {
|
||||
return (
|
||||
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
|
||||
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
|
||||
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -97,7 +101,7 @@ const onSearchBlur = () => {
|
||||
closeSearch();
|
||||
};
|
||||
|
||||
defineExpose({openSearch})
|
||||
defineExpose({ openSearch });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -615,6 +615,9 @@ const setLocalThemeConfigStyle = () => {
|
||||
};
|
||||
// 一键复制配置
|
||||
const onCopyConfigClick = (target: any) => {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
let copyThemeConfig = getLocal('themeConfig');
|
||||
copyThemeConfig.isDrawer = false;
|
||||
const clipboard = new ClipboardJS(target, {
|
||||
|
||||
@@ -73,12 +73,28 @@ const currentTime = computed(() => {
|
||||
|
||||
// 初始化数字滚动
|
||||
const initNumCountUp = async () => {
|
||||
const res: any = await indexApi.getIndexCount.request();
|
||||
nextTick(() => {
|
||||
new CountUp('mongoNum', res.mongoNum).start();
|
||||
new CountUp('machineNum', res.machineNum).start();
|
||||
new CountUp('dbNum', res.dbNum).start();
|
||||
new CountUp('redisNum', res.redisNum).start();
|
||||
indexApi.machineDashbord.request().then((res: any) => {
|
||||
nextTick(() => {
|
||||
new CountUp('machineNum', res.machineNum).start();
|
||||
});
|
||||
});
|
||||
|
||||
indexApi.dbDashbord.request().then((res: any) => {
|
||||
nextTick(() => {
|
||||
new CountUp('dbNum', res.dbNum).start();
|
||||
});
|
||||
});
|
||||
|
||||
indexApi.redisDashbord.request().then((res: any) => {
|
||||
nextTick(() => {
|
||||
new CountUp('redisNum', res.redisNum).start();
|
||||
});
|
||||
});
|
||||
|
||||
indexApi.mongoDashbord.request().then((res: any) => {
|
||||
nextTick(() => {
|
||||
new CountUp('mongoNum', res.mongoNum).start();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import Api from '@/common/Api';
|
||||
|
||||
export const indexApi = {
|
||||
getIndexCount: Api.newGet("/common/index/count"),
|
||||
}
|
||||
|
||||
machineDashbord: Api.newGet('/machines/dashbord'),
|
||||
dbDashbord: Api.newGet('/dbs/dashbord'),
|
||||
redisDashbord: Api.newGet('/redis/dashbord'),
|
||||
mongoDashbord: Api.newGet('/mongos/dashbord'),
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
@node-contextmenu="nodeContextmenu"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span>
|
||||
<span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
|
||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||
<tag-info :tag-path="data.label" />
|
||||
</span>
|
||||
@@ -25,7 +25,13 @@
|
||||
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
||||
|
||||
<span class="ml3" :title="data.labelRemark">
|
||||
<slot name="label" :data="data"> {{ data.label }}</slot>
|
||||
<slot name="label" :data="data" v-if="!data.disabled"> {{ data.label }}</slot>
|
||||
<!-- 禁用状态 -->
|
||||
<slot name="disabledLabel" :data="data" v-else>
|
||||
<el-link type="danger" disabled :underline="false">
|
||||
{{ `${data.label}` }}
|
||||
</el-link>
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<slot :node="node" :data="data" name="suffix"></slot>
|
||||
@@ -135,15 +141,29 @@ const loadNode = async (node: any, resolve: any) => {
|
||||
|
||||
const treeNodeClick = (data: any) => {
|
||||
emit('nodeClick', data);
|
||||
if (data.type.nodeClickFunc) {
|
||||
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
||||
data.type.nodeClickFunc(data);
|
||||
}
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
};
|
||||
|
||||
// 树节点双击事件
|
||||
const treeNodeDblclick = (data: any) => {
|
||||
// emit('nodeDblick', data);
|
||||
if (!data.disabled && data.type.nodeDblclickFunc) {
|
||||
data.type.nodeDblclickFunc(data);
|
||||
}
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
};
|
||||
|
||||
// 树节点右击事件
|
||||
const nodeContextmenu = (event: any, data: any) => {
|
||||
if (data.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载当前节点是否需要显示右击菜单
|
||||
let items = data.type.contextMenuItems;
|
||||
if (!items || items.length == 0) {
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
v-model="modelValue"
|
||||
@change="changeNode"
|
||||
>
|
||||
<template #prefix="{ node, data }">
|
||||
<slot name="iconPrefix" :node="node" :data="data" />
|
||||
</template>
|
||||
<template #default="{ node, data }">
|
||||
<span>
|
||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||
@@ -33,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
|
||||
import { onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { NodeType, TagTreeNode } from './tag';
|
||||
import TagInfo from './TagInfo.vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
|
||||
@@ -28,6 +28,11 @@ export class TagTreeNode {
|
||||
*/
|
||||
isLeaf: boolean = false;
|
||||
|
||||
/**
|
||||
* 是否禁用状态
|
||||
*/
|
||||
disabled: boolean = false;
|
||||
|
||||
/**
|
||||
* 额外需要传递的参数
|
||||
*/
|
||||
@@ -53,6 +58,11 @@ export class TagTreeNode {
|
||||
return this;
|
||||
}
|
||||
|
||||
withDisabled(disabled: boolean) {
|
||||
this.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
withParams(params: any) {
|
||||
this.params = params;
|
||||
return this;
|
||||
@@ -91,8 +101,14 @@ export class NodeType {
|
||||
|
||||
loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
|
||||
|
||||
/**
|
||||
* 节点点击事件
|
||||
*/
|
||||
nodeClickFunc: (node: TagTreeNode) => void;
|
||||
|
||||
// 节点双击事件
|
||||
nodeDblclickFunc: (node: TagTreeNode) => void;
|
||||
|
||||
constructor(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
@@ -117,6 +133,16 @@ export class NodeType {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值节点双击事件回调函数
|
||||
* @param func 节点双击事件回调函数
|
||||
* @returns this
|
||||
*/
|
||||
withNodeDblclickFunc(func: (node: TagTreeNode) => void) {
|
||||
this.nodeDblclickFunc = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 赋值右击菜单按钮选项
|
||||
* @param contextMenuItems 右击菜单按钮选项
|
||||
|
||||
@@ -23,13 +23,16 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="name" label="任务名称">
|
||||
<el-input v-model.number="state.form.name" type="text" placeholder="任务名称"></el-input>
|
||||
<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 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>
|
||||
|
||||
@@ -92,6 +95,14 @@ const rules = {
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
maxSaveDays: [
|
||||
{
|
||||
required: true,
|
||||
pattern: /^[0-9]\d*$/,
|
||||
message: '请输入非负整数',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const backupForm: any = ref(null);
|
||||
@@ -101,10 +112,11 @@ const state = reactive({
|
||||
id: 0,
|
||||
dbId: 0,
|
||||
dbNames: '',
|
||||
name: null as any,
|
||||
intervalDay: null,
|
||||
name: '',
|
||||
intervalDay: 1,
|
||||
startTime: null as any,
|
||||
repeated: null as any,
|
||||
repeated: true,
|
||||
maxSaveDays: 0,
|
||||
},
|
||||
btnLoading: false,
|
||||
dbNamesSelected: [] as any,
|
||||
@@ -137,12 +149,14 @@ const init = (data: any) => {
|
||||
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 = null;
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
155
mayfly_go_web/src/views/ops/db/DbBackupHistoryList.vue
Normal file
155
mayfly_go_web/src/views/ops/db/DbBackupHistoryList.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<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/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>
|
||||
@@ -21,6 +21,7 @@
|
||||
<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 }">
|
||||
@@ -29,6 +30,7 @@
|
||||
<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>
|
||||
@@ -49,7 +51,7 @@ import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
@@ -72,10 +74,10 @@ const columns = [
|
||||
TableColumn.new('name', '任务名称'),
|
||||
TableColumn.new('startTime', '启动时间').isTime(),
|
||||
TableColumn.new('intervalDay', '备份周期'),
|
||||
TableColumn.new('enabled', '是否启用'),
|
||||
TableColumn.new('enabledDesc', '是否启用'),
|
||||
TableColumn.new('lastResult', '执行结果'),
|
||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
|
||||
];
|
||||
|
||||
const emptyQuery = {
|
||||
@@ -168,5 +170,25 @@ const startDbBackup = async (data: any) => {
|
||||
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>
|
||||
|
||||
@@ -62,8 +62,21 @@
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'dumpDb', data }" v-if="supportAction('dumpDb', data.type)"> 导出 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'dbBackup', data }" v-if="supportAction('dbBackup', data.type)"> 备份 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'dbRestore', data }" v-if="supportAction('dbRestore', data.type)"> 恢复 </el-dropdown-item>
|
||||
<el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
|
||||
备份任务
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'backupHistory', data }"
|
||||
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
|
||||
>
|
||||
备份历史
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'restoreDb', data }"
|
||||
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
|
||||
>
|
||||
恢复任务
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -131,6 +144,16 @@
|
||||
<db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
width="80%"
|
||||
:title="`${dbBackupHistoryDialog.title} - 数据库备份历史`"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
v-model="dbBackupHistoryDialog.visible"
|
||||
>
|
||||
<db-backup-history-list :dbId="dbBackupHistoryDialog.dbId" :dbNames="dbBackupHistoryDialog.dbs" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
width="80%"
|
||||
:title="`${dbRestoreDialog.title} - 数据库恢复`"
|
||||
@@ -185,6 +208,7 @@ import { getDbDialect } from './dialect/index';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import DbBackupList from './DbBackupList.vue';
|
||||
import DbBackupHistoryList from './DbBackupHistoryList.vue';
|
||||
import DbRestoreList from './DbRestoreList.vue';
|
||||
|
||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||
@@ -193,6 +217,8 @@ const perms = {
|
||||
base: 'db',
|
||||
saveDb: 'db:save',
|
||||
delDb: 'db:del',
|
||||
backupDb: 'db:backup',
|
||||
restoreDb: 'db:restore',
|
||||
};
|
||||
|
||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.slot('instanceId', '实例', 'instanceSelect')];
|
||||
@@ -208,7 +234,8 @@ const columns = ref([
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
||||
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
|
||||
const actionBtns = hasPerms(Object.values(perms));
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
|
||||
|
||||
const route = useRoute();
|
||||
@@ -253,6 +280,13 @@ const state = reactive({
|
||||
dbs: [],
|
||||
dbId: 0,
|
||||
},
|
||||
// 数据库备份历史弹框
|
||||
dbBackupHistoryDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
dbs: [],
|
||||
dbId: 0,
|
||||
},
|
||||
// 数据库恢复弹框
|
||||
dbRestoreDialog: {
|
||||
title: '',
|
||||
@@ -285,7 +319,8 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbRestoreDialog } = toRefs(state);
|
||||
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } =
|
||||
toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
@@ -345,11 +380,15 @@ const handleMoreActionCommand = (commond: any) => {
|
||||
onDumpDbs(data);
|
||||
return;
|
||||
}
|
||||
case 'dbBackup': {
|
||||
case 'backupDb': {
|
||||
onShowDbBackupDialog(data);
|
||||
return;
|
||||
}
|
||||
case 'dbRestore': {
|
||||
case 'backupHistory': {
|
||||
onShowDbBackupHistoryDialog(data);
|
||||
return;
|
||||
}
|
||||
case 'restoreDb': {
|
||||
onShowDbRestoreDialog(data);
|
||||
return;
|
||||
}
|
||||
@@ -402,6 +441,13 @@ const onShowDbBackupDialog = async (row: any) => {
|
||||
state.dbBackupDialog.visible = true;
|
||||
};
|
||||
|
||||
const onShowDbBackupHistoryDialog = async (row: any) => {
|
||||
state.dbBackupHistoryDialog.title = `${row.name}`;
|
||||
state.dbBackupHistoryDialog.dbId = row.id;
|
||||
state.dbBackupHistoryDialog.dbs = row.database.split(' ');
|
||||
state.dbBackupHistoryDialog.visible = true;
|
||||
};
|
||||
|
||||
const onShowDbRestoreDialog = async (row: any) => {
|
||||
state.dbRestoreDialog.title = `${row.name}`;
|
||||
state.dbRestoreDialog.dbId = row.id;
|
||||
@@ -455,7 +501,7 @@ const supportAction = (action: string, dbType: string): boolean => {
|
||||
switch (dbType) {
|
||||
case DbType.mysql:
|
||||
case DbType.mariadb:
|
||||
actions = ['dumpDb', 'dbBackup', 'dbRestore'];
|
||||
actions = ['dumpDb', 'backupDb', 'restoreDb'];
|
||||
}
|
||||
return actions.includes(action);
|
||||
};
|
||||
|
||||
@@ -35,7 +35,13 @@
|
||||
clearable
|
||||
class="w100"
|
||||
>
|
||||
<el-option v-for="item in state.histories" :key="item.id" :label="item.name" :value="item"> </el-option>
|
||||
<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="开始时间">
|
||||
@@ -56,7 +62,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@@ -83,20 +89,30 @@ const visible = defineModel<boolean>('visible', {
|
||||
});
|
||||
|
||||
const validatePointInTime = (rule: any, value: any, callback: any) => {
|
||||
if (!state.histories || state.histories.length == 0) {
|
||||
callback(new Error('数据库没有备份记录'));
|
||||
return;
|
||||
}
|
||||
const history = state.histories[state.histories.length - 1];
|
||||
if (value < new Date(history.createTime)) {
|
||||
callback(new Error('在此之前数据库没有备份记录'));
|
||||
return;
|
||||
}
|
||||
if (value > new Date()) {
|
||||
callback(new Error('恢复时间点晚于当前时间'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
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 = {
|
||||
@@ -110,7 +126,6 @@ const rules = {
|
||||
pointInTime: [
|
||||
{
|
||||
required: true,
|
||||
// message: '请选择恢复时间点',
|
||||
validator: validatePointInTime,
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
@@ -146,7 +161,7 @@ const state = reactive({
|
||||
id: 0,
|
||||
dbId: 0,
|
||||
dbName: null as any,
|
||||
intervalDay: 1,
|
||||
intervalDay: 0,
|
||||
startTime: null as any,
|
||||
repeated: null as any,
|
||||
dbBackupId: null as any,
|
||||
@@ -218,7 +233,8 @@ const init = async (data: any) => {
|
||||
} else {
|
||||
state.form.dbName = '';
|
||||
state.editOrCreate = false;
|
||||
state.form.intervalDay = 1;
|
||||
state.form.intervalDay = 0;
|
||||
state.form.repeated = false;
|
||||
state.form.pointInTime = new Date();
|
||||
state.form.startTime = new Date();
|
||||
state.histories = [];
|
||||
@@ -237,6 +253,12 @@ const getDbNamesWithoutRestore = async () => {
|
||||
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;
|
||||
@@ -245,13 +267,14 @@ const btnOk = async () => {
|
||||
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('保存成功');
|
||||
ElMessage.success('成功创建数据库恢复任务');
|
||||
emit('val-change', state.form);
|
||||
state.btnLoading = true;
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -21,12 +21,14 @@
|
||||
<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>
|
||||
|
||||
@@ -49,7 +51,7 @@
|
||||
infoDialog.data.dbBackupHistoryName
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabled }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="执行时间">{{ dateFormat(infoDialog.data.lastTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
@@ -63,7 +65,7 @@ import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
@@ -85,7 +87,7 @@ const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
||||
const columns = [
|
||||
TableColumn.new('dbName', '数据库名称'),
|
||||
TableColumn.new('startTime', '启动时间').isTime(),
|
||||
TableColumn.new('enabled', '是否启用'),
|
||||
TableColumn.new('enabledDesc', '是否启用'),
|
||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
||||
TableColumn.new('lastResult', '执行结果'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
|
||||
@@ -135,19 +137,39 @@ const createDbRestore = async () => {
|
||||
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;
|
||||
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('请选择需要启用的恢复任务');
|
||||
ElMessage.error('请选择需要启用的数据库恢复任务');
|
||||
return;
|
||||
}
|
||||
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||
@@ -156,13 +178,13 @@ const enableDbRestore = async (data: any) => {
|
||||
};
|
||||
|
||||
const disableDbRestore = async (data: any) => {
|
||||
let restoreId: String;
|
||||
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('请选择需要禁用的恢复任务');
|
||||
ElMessage.error('请选择需要禁用的数据库恢复任务');
|
||||
return;
|
||||
}
|
||||
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, watch, reactive, onMounted, Ref, ref } from 'vue';
|
||||
import { onMounted, reactive, Ref, ref, toRefs, watch } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { DbSqlExecTypeEnum } from './enums';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
@@ -120,6 +120,12 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
||||
const primaryKey = getPrimaryKey(columns);
|
||||
const oldValue = JSON.parse(sqlExecLog.oldValue);
|
||||
|
||||
let schema = '';
|
||||
let dbArr = sqlExecLog.db.split('/');
|
||||
if (dbArr.length == 2) {
|
||||
schema = dbArr[1] + '.';
|
||||
}
|
||||
|
||||
const rollbackSqls = [];
|
||||
if (sqlExecLog.type == DbSqlExecTypeEnum.Update.value) {
|
||||
for (let ov of oldValue) {
|
||||
@@ -130,7 +136,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
||||
}
|
||||
setItems.push(`${key} = ${wrapValue(ov[key])}`);
|
||||
}
|
||||
rollbackSqls.push(`UPDATE ${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
|
||||
rollbackSqls.push(`UPDATE ${schema}${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
|
||||
}
|
||||
} else if (sqlExecLog.type == DbSqlExecTypeEnum.Delete.value) {
|
||||
const columnNames = columns.map((c: any) => c.columnName);
|
||||
@@ -139,7 +145,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
||||
for (let column of columnNames) {
|
||||
values.push(wrapValue(ov[column]));
|
||||
}
|
||||
rollbackSqls.push(`INSERT INTO ${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
|
||||
rollbackSqls.push(`INSERT INTO ${schema}${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +154,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
||||
};
|
||||
|
||||
const getPrimaryKey = (columns: any) => {
|
||||
const col = columns.find((c: any) => c.columnKey == 'PRI');
|
||||
const col = columns.find((c: any) => c.isPrimaryKey);
|
||||
if (col) {
|
||||
return col.columnName;
|
||||
}
|
||||
|
||||
@@ -9,13 +9,22 @@
|
||||
</el-form-item>
|
||||
<el-form-item prop="type" label="类型" required>
|
||||
<el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
|
||||
<el-option v-for="dt in dbTypes" :key="dt.type" :value="dt.type" :label="dt.label">
|
||||
<SvgIcon :name="getDbDialect(dt.type).getInfo().icon" :size="18" />
|
||||
{{ dt.label }}
|
||||
<el-option
|
||||
v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
|
||||
:key="key"
|
||||
:value="dbTypeAndDialect[0]"
|
||||
:label="dbTypeAndDialect[1].getInfo().name"
|
||||
>
|
||||
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
|
||||
{{ dbTypeAndDialect[1].getInfo().name }}
|
||||
</el-option>
|
||||
|
||||
<template #prefix>
|
||||
<SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="host" label="host" required>
|
||||
<el-form-item v-if="form.type !== DbType.sqlite" prop="host" label="host" required>
|
||||
<el-col :span="18">
|
||||
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
|
||||
</el-col>
|
||||
@@ -24,13 +33,18 @@
|
||||
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.type === DbType.sqlite" prop="host" label="sqlite地址">
|
||||
<el-input v-model.trim="form.host" placeholder="请输入sqlite文件在服务器的绝对地址"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.type === DbType.oracle" prop="sid" label="SID">
|
||||
<el-input v-model.trim="form.sid" placeholder="请输入服务id"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="username" label="用户名" required>
|
||||
<el-form-item v-if="form.type !== DbType.sqlite" prop="username" label="用户名" required>
|
||||
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" label="密码">
|
||||
<el-form-item v-if="form.type !== DbType.sqlite" prop="password" label="密码">
|
||||
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
|
||||
<template v-if="form.id && form.id != 0" #suffix>
|
||||
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
|
||||
@@ -90,7 +104,7 @@ import { ElMessage } from 'element-plus';
|
||||
import { notBlank } from '@/common/assert';
|
||||
import { RsaEncrypt } from '@/common/rsa';
|
||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||
import { DbType, getDbDialect } from './dialect';
|
||||
import { DbType, getDbDialect, getDbDialectMap } from './dialect';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -148,35 +162,12 @@ const rules = {
|
||||
|
||||
const dbForm: any = ref(null);
|
||||
|
||||
const dbTypes = [
|
||||
{
|
||||
type: 'mysql',
|
||||
label: 'mysql',
|
||||
},
|
||||
{
|
||||
type: 'mariadb',
|
||||
label: 'mariadb',
|
||||
},
|
||||
{
|
||||
type: 'postgres',
|
||||
label: 'postgres',
|
||||
},
|
||||
{
|
||||
type: 'dm',
|
||||
label: '达梦',
|
||||
},
|
||||
{
|
||||
type: 'oracle',
|
||||
label: 'oracle',
|
||||
},
|
||||
];
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
tabActiveName: 'basic',
|
||||
form: {
|
||||
id: null,
|
||||
type: null,
|
||||
type: '',
|
||||
name: null,
|
||||
host: '',
|
||||
port: null,
|
||||
@@ -187,17 +178,17 @@ const state = reactive({
|
||||
remark: '',
|
||||
sshTunnelMachineId: null as any,
|
||||
},
|
||||
subimtForm: {},
|
||||
submitForm: {},
|
||||
// 原密码
|
||||
pwd: '',
|
||||
// 原用户名
|
||||
oldUserName: null,
|
||||
});
|
||||
|
||||
const { dialogVisible, tabActiveName, form, subimtForm, pwd } = toRefs(state);
|
||||
const { dialogVisible, tabActiveName, form, submitForm, pwd } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(subimtForm);
|
||||
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(subimtForm);
|
||||
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(submitForm);
|
||||
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
|
||||
|
||||
watch(props, (newValue: any) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
@@ -209,7 +200,7 @@ watch(props, (newValue: any) => {
|
||||
state.form = { ...newValue.data };
|
||||
state.oldUserName = state.form.username;
|
||||
} else {
|
||||
state.form = { port: null } as any;
|
||||
state.form = { port: null, type: DbType.mysql } as any;
|
||||
state.oldUserName = null;
|
||||
}
|
||||
});
|
||||
@@ -240,17 +231,19 @@ const testConn = async () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.subimtForm = await getReqForm();
|
||||
state.submitForm = await getReqForm();
|
||||
await testConnExec();
|
||||
ElMessage.success('连接成功');
|
||||
});
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
if (!state.form.id) {
|
||||
notBlank(state.form.password, '新增操作,密码不可为空');
|
||||
} else if (state.form.username != state.oldUserName) {
|
||||
notBlank(state.form.password, '已修改用户名,请输入密码');
|
||||
if (state.form.type !== DbType.sqlite) {
|
||||
if (!state.form.id) {
|
||||
notBlank(state.form.password, '新增操作,密码不可为空');
|
||||
} else if (state.form.username != state.oldUserName) {
|
||||
notBlank(state.form.password, '已修改用户名,请输入密码');
|
||||
}
|
||||
}
|
||||
|
||||
dbForm.value.validate(async (valid: boolean) => {
|
||||
@@ -259,7 +252,7 @@ const btnOk = async () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.subimtForm = await getReqForm();
|
||||
state.submitForm = await getReqForm();
|
||||
await saveInstanceExec();
|
||||
ElMessage.success('保存成功');
|
||||
emit('val-change', state.form);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<template #type="{ data }">
|
||||
<el-tooltip :content="data.type" placement="top">
|
||||
<el-tooltip :content="getDbDialect(data.type).getInfo().name" placement="top">
|
||||
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
@@ -25,6 +25,7 @@
|
||||
<template #action="{ data }">
|
||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
||||
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
|
||||
<el-button v-if="actionBtns[perms.delInstance]" @click="deleteInstance(data)" type="primary" link>删除</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
@@ -91,7 +92,7 @@ const columns = ref([
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.saveInstance]);
|
||||
const actionBtns = hasPerms(Object.values(perms));
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
@@ -150,14 +151,26 @@ const editInstance = async (data: any) => {
|
||||
state.instanceEditDialog.visible = true;
|
||||
};
|
||||
|
||||
const deleteInstance = async () => {
|
||||
const deleteInstance = async (data: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除数据库实例【${state.selectionData.map((x: any) => x.name).join(', ')}】?`, '提示', {
|
||||
let instanceName: string;
|
||||
if (data) {
|
||||
instanceName = data.name;
|
||||
} else {
|
||||
instanceName = state.selectionData.map((x: any) => x.name).join(', ');
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除数据库实例【${instanceName}】?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteInstance.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
let instanceId: string;
|
||||
if (data) {
|
||||
instanceId = data.id;
|
||||
} else {
|
||||
instanceId = state.selectionData.map((x: any) => x.id).join(',');
|
||||
}
|
||||
await dbApi.deleteInstance.request({ id: instanceId });
|
||||
ElMessage.success('删除成功');
|
||||
search();
|
||||
} catch (err) {
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<el-descriptions-item label-align="right">
|
||||
<template #label>
|
||||
<div>
|
||||
<SvgIcon :name="getDbDialect(nowDbInst.type).getInfo().icon" :size="18" />
|
||||
<SvgIcon :name="nowDbInst.getDialect().getInfo().icon" :size="18" />
|
||||
实例
|
||||
</div>
|
||||
</template>
|
||||
@@ -151,12 +151,22 @@
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
<db-table-op
|
||||
:title="tableCreateDialog.title"
|
||||
:active-name="tableCreateDialog.activeName"
|
||||
:dbId="tableCreateDialog.dbId"
|
||||
:db="tableCreateDialog.db"
|
||||
:dbType="tableCreateDialog.dbType"
|
||||
:data="tableCreateDialog.data"
|
||||
v-model:visible="tableCreateDialog.visible"
|
||||
@submit-sql="onSubmitEditTableSql"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
|
||||
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
|
||||
import { NodeType, TagTreeNode } from '../component/tag';
|
||||
@@ -165,12 +175,13 @@ import { dbApi } from './api';
|
||||
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import { DbType, getDbDialect } from './dialect/index';
|
||||
import { getDbDialect, schemaDbTypes } from './dialect/index';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { Pane, Splitpanes } from 'splitpanes';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
|
||||
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
|
||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
||||
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
|
||||
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
|
||||
@@ -218,21 +229,25 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
||||
}
|
||||
};
|
||||
|
||||
// tagpath 节点类型
|
||||
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 [];
|
||||
}
|
||||
const ContextmenuItemRefresh = new ContextmenuItem('refresh', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key));
|
||||
|
||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||
await sleep(100);
|
||||
return dbInfos?.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
||||
});
|
||||
});
|
||||
// tagpath 节点类型
|
||||
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 [];
|
||||
}
|
||||
|
||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||
await sleep(100);
|
||||
return dbInfos?.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
||||
});
|
||||
})
|
||||
.withContextMenuItems([ContextmenuItemRefresh]);
|
||||
|
||||
// 数据库实例节点类型
|
||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
|
||||
@@ -255,12 +270,12 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
||||
|
||||
// 数据库节点
|
||||
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
||||
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
|
||||
.withContextMenuItems([ContextmenuItemRefresh])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
params.parentKey = parentNode.key;
|
||||
// pg类数据库会多一层schema
|
||||
if (params.type == DbType.postgresql || params.type === DbType.dm || params.type === DbType.oracle) {
|
||||
const params = parentNode.params;
|
||||
if (schemaDbTypes.includes(params.type)) {
|
||||
const { id, db } = params;
|
||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||
return schemaNames.map((sn: any) => {
|
||||
@@ -269,33 +284,37 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
||||
nParams.schema = sn;
|
||||
nParams.db = nParams.db + '/' + sn;
|
||||
nParams.dbs = schemaNames;
|
||||
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresScheam).withParams(nParams).withIcon(SchemaIcon);
|
||||
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
|
||||
});
|
||||
}
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
|
||||
];
|
||||
return NodeTypeTables(params);
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
const NodeTypeTables = (params: any) => {
|
||||
let tableKey = `${params.id}.${params.db}.table-menu`;
|
||||
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams({ ...params, key: tableKey }).withIcon(TableIcon),
|
||||
new TagTreeNode(sqlKey, 'SQL', NodeTypeSqlMenu).withParams({ ...params, key: sqlKey }).withIcon(SqlIcon),
|
||||
];
|
||||
};
|
||||
|
||||
// postgres schema模式
|
||||
const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema)
|
||||
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
|
||||
const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema)
|
||||
.withContextMenuItems([ContextmenuItemRefresh])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
return [
|
||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
|
||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
|
||||
];
|
||||
params.parentKey = parentNode.key;
|
||||
return NodeTypeTables(params);
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 数据库表菜单节点
|
||||
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key)),
|
||||
|
||||
ContextmenuItemRefresh,
|
||||
new ContextmenuItem('createTable', '创建表').withIcon('Plus').withOnClick((data: any) => onEditTable(data)),
|
||||
new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
|
||||
const params = data.params;
|
||||
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key });
|
||||
@@ -303,27 +322,32 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
let { id, db } = params;
|
||||
let { id, db, type } = params;
|
||||
// 获取当前库的所有表信息
|
||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||
state.reloadStatus = false;
|
||||
let dbTableSize = 0;
|
||||
const tablesNode = tables.map((x: any) => {
|
||||
dbTableSize += x.dataLength + x.indexLength;
|
||||
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable)
|
||||
const tableSize = x.dataLength + x.indexLength;
|
||||
dbTableSize += tableSize;
|
||||
const key = `${id}.${db}.${x.tableName}`;
|
||||
return new TagTreeNode(key, x.tableName, NodeTypeTable)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
id,
|
||||
db,
|
||||
type,
|
||||
key: key,
|
||||
parentKey: parentNode.key,
|
||||
tableName: x.tableName,
|
||||
tableComment: x.tableComment,
|
||||
size: formatByteSize(x.dataLength + x.indexLength, 1),
|
||||
size: tableSize == 0 ? '' : formatByteSize(tableSize, 1),
|
||||
})
|
||||
.withIcon(TableIcon)
|
||||
.withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
|
||||
});
|
||||
// 设置父节点参数的表大小
|
||||
parentNode.params.dbTableSize = formatByteSize(dbTableSize);
|
||||
parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
|
||||
return tablesNode;
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
@@ -340,22 +364,23 @@ const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
|
||||
return sqls.map((x: any) => {
|
||||
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
id,
|
||||
db,
|
||||
dbs,
|
||||
sqlName: x.name,
|
||||
})
|
||||
.withParams({ id, db, dbs, sqlName: x.name })
|
||||
.withIcon(SqlIcon);
|
||||
});
|
||||
})
|
||||
.withNodeClickFunc(nodeClickChangeDb);
|
||||
|
||||
// 表节点类型
|
||||
const NodeTypeTable = new NodeType(SqlExecNodeType.Table).withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
|
||||
});
|
||||
const NodeTypeTable = new NodeType(SqlExecNodeType.Table)
|
||||
.withContextMenuItems([
|
||||
new ContextmenuItem('copyTable', '复制表').withIcon('copyDocument').withOnClick((data: any) => onCopyTable(data)),
|
||||
new ContextmenuItem('editTable', '编辑表').withIcon('edit').withOnClick((data: any) => onEditTable(data)),
|
||||
new ContextmenuItem('delTable', '删除表').withIcon('Delete').withOnClick((data: any) => onDeleteTable(data)),
|
||||
])
|
||||
.withNodeClickFunc((nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
|
||||
});
|
||||
|
||||
// sql模板节点类型
|
||||
const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
|
||||
@@ -385,9 +410,19 @@ const state = reactive({
|
||||
loading: true,
|
||||
version: '',
|
||||
},
|
||||
tableCreateDialog: {
|
||||
visible: false,
|
||||
title: '',
|
||||
activeName: '',
|
||||
dbId: 0,
|
||||
db: '',
|
||||
dbType: '',
|
||||
data: {},
|
||||
parentKey: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { nowDbInst } = toRefs(state);
|
||||
const { nowDbInst, tableCreateDialog } = toRefs(state);
|
||||
|
||||
const serverInfoReqParam = ref({
|
||||
instanceId: 0,
|
||||
@@ -408,7 +443,7 @@ onBeforeUnmount(() => {
|
||||
* 设置editor高度和数据表高度
|
||||
*/
|
||||
const setHeight = () => {
|
||||
state.dataTabsTableHeight = window.innerHeight - 270 + 'px';
|
||||
state.dataTabsTableHeight = window.innerHeight - 253 + 'px';
|
||||
state.tablesOpHeight = window.innerHeight - 225 + 'px';
|
||||
};
|
||||
|
||||
@@ -603,6 +638,85 @@ const reloadNode = (nodeKey: string) => {
|
||||
tagTreeRef.value.reloadNode(nodeKey);
|
||||
};
|
||||
|
||||
const onEditTable = async (data: any) => {
|
||||
let { db, id, tableName, tableComment, type, parentKey, key } = data.params;
|
||||
// data.label就是表名
|
||||
if (tableName) {
|
||||
state.tableCreateDialog.title = '修改表';
|
||||
let indexs = await dbApi.tableIndex.request({ id, db, tableName });
|
||||
let columns = await dbApi.columnMetadata.request({ id, db, tableName });
|
||||
let row = { tableName, tableComment };
|
||||
state.tableCreateDialog.data = { edit: true, row, indexs, columns };
|
||||
state.tableCreateDialog.parentKey = parentKey;
|
||||
} else {
|
||||
state.tableCreateDialog.title = '创建表';
|
||||
state.tableCreateDialog.data = { edit: false, row: {} };
|
||||
state.tableCreateDialog.parentKey = key;
|
||||
}
|
||||
|
||||
state.tableCreateDialog.visible = true;
|
||||
state.tableCreateDialog.activeName = '1';
|
||||
state.tableCreateDialog.dbId = id;
|
||||
state.tableCreateDialog.db = db;
|
||||
state.tableCreateDialog.dbType = type;
|
||||
};
|
||||
|
||||
const onDeleteTable = async (data: any) => {
|
||||
let { db, id, tableName, parentKey } = data.params;
|
||||
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
// 执行sql
|
||||
dbApi.sqlExec.request({ id, db, sql: `drop table ${tableName}` }).then(() => {
|
||||
ElMessage.success('删除成功');
|
||||
setTimeout(() => {
|
||||
parentKey && reloadNode(parentKey);
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
const onCopyTable = async (data: any) => {
|
||||
let { db, id, tableName, parentKey } = data.params;
|
||||
|
||||
let checked = ref(false);
|
||||
|
||||
// 弹出确认框,并选择是否复制数据
|
||||
await ElMessageBox({
|
||||
title: `复制表【${tableName}】`,
|
||||
type: 'warning',
|
||||
// icon: markRaw(Delete),
|
||||
message: () =>
|
||||
h(ElCheckbox, {
|
||||
label: '是否复制数据?',
|
||||
modelValue: checked.value,
|
||||
'onUpdate:modelValue': (val: boolean | string | number) => {
|
||||
if (typeof val === 'boolean') {
|
||||
checked.value = val;
|
||||
}
|
||||
},
|
||||
}),
|
||||
callback: (action: string) => {
|
||||
if (action === 'confirm') {
|
||||
// 执行sql
|
||||
dbApi.copyTable.request({ id, db, tableName, copyData: checked.value }).then(() => {
|
||||
ElMessage.success('复制成功');
|
||||
setTimeout(() => {
|
||||
parentKey && reloadNode(parentKey);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmitEditTableSql = () => {
|
||||
state.tableCreateDialog.visible = false;
|
||||
state.tableCreateDialog.data = { edit: false, row: {} };
|
||||
reloadNode(state.tableCreateDialog.parentKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前操作的数据库信息
|
||||
*/
|
||||
|
||||
@@ -45,8 +45,10 @@
|
||||
<db-select-tree
|
||||
placeholder="请选择源数据库"
|
||||
v-model:db-id="form.srcDbId"
|
||||
v-model:inst-name="form.srcInstName"
|
||||
v-model:db-name="form.srcDbName"
|
||||
v-model:tag-path="form.srcTagPath"
|
||||
v-model:db-type="form.srcDbType"
|
||||
@select-db="onSelectSrcDb"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -55,8 +57,10 @@
|
||||
<db-select-tree
|
||||
placeholder="请选择目标数据库"
|
||||
v-model:db-id="form.targetDbId"
|
||||
v-model:inst-name="form.targetInstName"
|
||||
v-model:db-name="form.targetDbName"
|
||||
v-model:tag-path="form.targetTagPath"
|
||||
v-model:db-type="form.targetDbType"
|
||||
@select-db="onSelectTargetDb"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -181,7 +185,7 @@ import { ElMessage } from 'element-plus';
|
||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import { DbType, getDbDialect } from '@/views/ops/db/dialect';
|
||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -226,12 +230,16 @@ type FormData = {
|
||||
taskName?: string;
|
||||
taskCron: string;
|
||||
srcDbId?: number;
|
||||
srcInstName?: string;
|
||||
srcDbName?: string;
|
||||
srcDbType?: string;
|
||||
srcTagPath?: string;
|
||||
targetDbId?: number;
|
||||
targetInstName?: string;
|
||||
targetDbName?: string;
|
||||
targetTagPath?: string;
|
||||
targetTableName?: string;
|
||||
targetDbType?: string;
|
||||
dataSql?: string;
|
||||
pageSize?: number;
|
||||
updField?: string;
|
||||
@@ -245,7 +253,7 @@ const basicFormData = {
|
||||
targetDbId: -1,
|
||||
dataSql: 'select * from',
|
||||
pageSize: 1000,
|
||||
updField: 'id',
|
||||
updField: '',
|
||||
updFieldVal: '0',
|
||||
fieldMap: [{ src: 'a', target: 'b' }],
|
||||
status: 1,
|
||||
@@ -302,6 +310,8 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
// 初始化实例
|
||||
db.databases = db.database?.split(' ').sort() || [];
|
||||
state.srcDbInst = DbInst.getOrNewInst(db);
|
||||
state.form.srcDbType = state.srcDbInst.type;
|
||||
state.form.srcInstName = db.instanceName;
|
||||
}
|
||||
|
||||
// 初始化target数据源
|
||||
@@ -312,6 +322,8 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
// 初始化实例
|
||||
db.databases = db.database?.split(' ').sort() || [];
|
||||
state.targetDbInst = DbInst.getOrNewInst(db);
|
||||
state.form.targetDbType = state.targetDbInst.type;
|
||||
state.form.targetInstName = db.instanceName;
|
||||
}
|
||||
|
||||
if (targetDbId && state.form.targetDbName) {
|
||||
@@ -396,8 +408,8 @@ const handleGetSrcFields = async () => {
|
||||
}
|
||||
|
||||
// 判断sql是否是查询语句
|
||||
if (!/^select/i.test(state.form.dataSql!)) {
|
||||
let msg = 'sql语句错误,请输入查询语句';
|
||||
if (!/^select/i.test(state.form.dataSql.trim()!)) {
|
||||
let msg = 'sql语句错误,请输入select语句';
|
||||
ElMessage.warning(msg);
|
||||
return;
|
||||
}
|
||||
@@ -410,10 +422,16 @@ const handleGetSrcFields = async () => {
|
||||
}
|
||||
|
||||
// 执行sql
|
||||
// oracle的分页关键字不一样
|
||||
let limit = ' limit 1';
|
||||
if (state.form.srcDbType === DbType.oracle) {
|
||||
limit = ' where rownum <= 1';
|
||||
}
|
||||
|
||||
const res = await dbApi.sqlExec.request({
|
||||
id: state.form.srcDbId,
|
||||
db: state.form.srcDbName,
|
||||
sql: state.form.dataSql.trim() + ' limit 1',
|
||||
sql: `select * from (${state.form.dataSql}) t ${limit}`,
|
||||
});
|
||||
|
||||
if (!res.columns) {
|
||||
|
||||
@@ -11,6 +11,7 @@ export const dbApi = {
|
||||
tableInfos: Api.newGet('/dbs/{id}/t-infos'),
|
||||
tableIndex: Api.newGet('/dbs/{id}/t-index'),
|
||||
tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
|
||||
copyTable: Api.newPost('/dbs/{id}/copy-table'),
|
||||
columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
|
||||
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
|
||||
// 获取表即列提示
|
||||
@@ -48,16 +49,20 @@ export const dbApi = {
|
||||
// 获取数据库备份列表
|
||||
getDbBackups: Api.newGet('/dbs/{dbId}/backups'),
|
||||
createDbBackup: Api.newPost('/dbs/{dbId}/backups'),
|
||||
deleteDbBackup: Api.newDelete('/dbs/{dbId}/backups/{backupId}'),
|
||||
getDbNamesWithoutBackup: Api.newGet('/dbs/{dbId}/db-names-without-backup'),
|
||||
enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
|
||||
disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
|
||||
startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'),
|
||||
saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
|
||||
getDbBackupHistories: Api.newGet('/dbs/{dbId}/backup-histories'),
|
||||
restoreDbBackupHistory: Api.newPost('/dbs/{dbId}/backup-histories/{backupHistoryId}/restore'),
|
||||
deleteDbBackupHistory: Api.newDelete('/dbs/{dbId}/backup-histories/{backupHistoryId}'),
|
||||
|
||||
// 获取数据库备份列表
|
||||
// 获取数据库恢复列表
|
||||
getDbRestores: Api.newGet('/dbs/{dbId}/restores'),
|
||||
createDbRestore: Api.newPost('/dbs/{dbId}/restores'),
|
||||
deleteDbRestore: Api.newDelete('/dbs/{dbId}/restores/{restoreId}'),
|
||||
getDbNamesWithoutRestore: Api.newGet('/dbs/{dbId}/db-names-without-restore'),
|
||||
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
||||
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
:resource-type="TagResourceTypeEnum.Db.value"
|
||||
:tag-path-node-type="NodeTypeTagPath"
|
||||
>
|
||||
<template #iconPrefix>
|
||||
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
|
||||
</template>
|
||||
<template #prefix="{ data }">
|
||||
<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" />
|
||||
@@ -19,7 +22,7 @@ import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { DbType, getDbDialect } from '@/views/ops/db/dialect';
|
||||
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
|
||||
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
@@ -27,15 +30,21 @@ const props = defineProps({
|
||||
dbId: {
|
||||
type: Number,
|
||||
},
|
||||
instName: {
|
||||
type: String,
|
||||
},
|
||||
dbName: {
|
||||
type: String,
|
||||
},
|
||||
tagPath: {
|
||||
type: String,
|
||||
},
|
||||
dbType: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:dbId', 'selectDb']);
|
||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:instName', 'update:dbId', 'update:dbType', 'selectDb']);
|
||||
|
||||
/**
|
||||
* 树节点类型
|
||||
@@ -53,7 +62,7 @@ class SqlExecNodeType {
|
||||
|
||||
const selectNode = computed({
|
||||
get: () => {
|
||||
return props.dbName ? `${props.tagPath} - ${props.dbId} - ${props.dbName}` : '';
|
||||
return props.dbName ? `${props.tagPath} > ${props.instName} > ${props.dbName}` : '';
|
||||
},
|
||||
set: () => {
|
||||
//
|
||||
@@ -87,8 +96,8 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
|
||||
});
|
||||
|
||||
/** mysql类型的数据库,没有schema层 */
|
||||
const mysqlType = (type: string) => {
|
||||
return type === DbType.mysql;
|
||||
const noSchemaType = (type: string) => {
|
||||
return noSchemaTypes.includes(type);
|
||||
};
|
||||
|
||||
// 数据库实例节点类型
|
||||
@@ -96,7 +105,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
||||
const params = parentNode.params;
|
||||
const dbs = params.database.split(' ')?.sort();
|
||||
let fn: NodeType;
|
||||
if (mysqlType(params.type)) {
|
||||
if (noSchemaType(params.type)) {
|
||||
fn = MysqlNodeTypes;
|
||||
} else {
|
||||
fn = PgNodeTypes;
|
||||
@@ -114,7 +123,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
||||
db: x,
|
||||
})
|
||||
.withIcon(DbIcon);
|
||||
if (mysqlType(params.type)) {
|
||||
if (noSchemaType(params.type)) {
|
||||
tagTreeNode.isLeaf = true;
|
||||
}
|
||||
return tagTreeNode;
|
||||
@@ -148,8 +157,10 @@ const changeNode = (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
// postgres
|
||||
emits('update:dbName', params.db);
|
||||
emits('update:instName', params.name);
|
||||
emits('update:dbId', params.id);
|
||||
emits('update:tagPath', params.tagPath);
|
||||
emits('update:dbType', params.type);
|
||||
emits('selectDb', params);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -128,12 +128,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { h, nextTick, onMounted, reactive, toRefs, ref, unref } from 'vue';
|
||||
import { h, nextTick, onMounted, reactive, ref, toRefs, unref } from 'vue';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import { notBlank } from '@/common/assert';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import config from '@/common/config';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus';
|
||||
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { editor } from 'monaco-editor';
|
||||
@@ -146,11 +146,9 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
|
||||
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import syssocket from '@/common/syssocket';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { getDbDialect } from '../../dialect';
|
||||
import { Splitpanes, Pane } from 'splitpanes';
|
||||
import { Pane, Splitpanes } from 'splitpanes';
|
||||
|
||||
const emits = defineEmits(['saveSqlSuccess']);
|
||||
|
||||
@@ -357,6 +355,7 @@ const onRunSql = async (newTab = false) => {
|
||||
const colAndData: any = data.value;
|
||||
if (!colAndData.res || colAndData.res.length === 0) {
|
||||
ElMessage.warning('未查询到结果集');
|
||||
return;
|
||||
}
|
||||
|
||||
// 要实时响应,故需要用索引改变数据才生效
|
||||
@@ -453,7 +452,7 @@ const formatSql = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const formatDialect = getDbDialect(getNowDbInst().type).getInfo().formatSqlDialect;
|
||||
const formatDialect = getNowDbInst().getDialect().getInfo().formatSqlDialect;
|
||||
|
||||
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<el-input
|
||||
v-if="dataType == DataType.String"
|
||||
:ref="(el: any) => focus && el?.focus()"
|
||||
:disabled="disabled"
|
||||
@blur="handleBlur"
|
||||
:class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`"
|
||||
input-style="text-align: center; height: 26px;"
|
||||
size="small"
|
||||
v-model="itemValue"
|
||||
:placeholder="placeholder"
|
||||
@@ -16,9 +16,9 @@
|
||||
<el-input
|
||||
v-else-if="dataType == DataType.Number"
|
||||
:ref="(el: any) => focus && el?.focus()"
|
||||
:disabled="disabled"
|
||||
@blur="handleBlur"
|
||||
class="w100 mb4"
|
||||
input-style="text-align: center; height: 26px;"
|
||||
size="small"
|
||||
v-model.number="itemValue"
|
||||
:placeholder="placeholder"
|
||||
@@ -28,6 +28,7 @@
|
||||
<el-date-picker
|
||||
v-else-if="dataType == DataType.Date"
|
||||
:ref="(el: any) => focus && el?.focus()"
|
||||
:disabled="disabled"
|
||||
@change="emit('blur')"
|
||||
@blur="handleBlur"
|
||||
class="edit-time-picker mb4"
|
||||
@@ -43,6 +44,7 @@
|
||||
<el-date-picker
|
||||
v-else-if="dataType == DataType.DateTime"
|
||||
:ref="(el: any) => focus && el?.focus()"
|
||||
:disabled="disabled"
|
||||
@change="handleBlur"
|
||||
@blur="handleBlur"
|
||||
class="edit-time-picker mb4"
|
||||
@@ -58,6 +60,7 @@
|
||||
<el-time-picker
|
||||
v-else-if="dataType == DataType.Time"
|
||||
:ref="(el: any) => focus && el?.focus()"
|
||||
:disabled="disabled"
|
||||
@change="handleBlur"
|
||||
@blur="handleBlur"
|
||||
class="edit-time-picker mb4"
|
||||
@@ -71,7 +74,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Ref, ref, computed } from 'vue';
|
||||
import { computed, ref, Ref } from 'vue';
|
||||
import { ElInput } from 'element-plus';
|
||||
import { DataType } from '../../dialect/index';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
@@ -83,11 +86,13 @@ export interface ColumnFormItemProps {
|
||||
focus?: boolean; // 是否获取焦点
|
||||
placeholder?: string;
|
||||
columnName?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
|
||||
focus: false,
|
||||
dataType: DataType.String,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'blur']);
|
||||
@@ -178,9 +183,6 @@ const getEditorLangByValue = (value: any) => {
|
||||
.el-input__prefix {
|
||||
display: none;
|
||||
}
|
||||
.el-input__inner {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-time-picker-popper {
|
||||
|
||||
@@ -46,14 +46,6 @@
|
||||
<b :title="column.remark" class="el-text" style="cursor: pointer">
|
||||
{{ column.title }}
|
||||
</b>
|
||||
|
||||
<span>
|
||||
<SvgIcon
|
||||
color="var(--el-color-primary)"
|
||||
v-if="column.title == nowSortColumn?.columnName"
|
||||
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
|
||||
></SvgIcon>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 字段备注信息 -->
|
||||
@@ -71,6 +63,13 @@
|
||||
{{ column.title }}
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<!-- 字段列右部分内容 -->
|
||||
<div class="column-right">
|
||||
<span v-if="column.title == nowSortColumn?.columnName">
|
||||
<SvgIcon color="var(--el-color-primary)" :name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"></SvgIcon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,13 +137,25 @@
|
||||
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
|
||||
</el-dialog>
|
||||
|
||||
<DbTableDataForm
|
||||
v-if="state.tableDataFormDialog.visible"
|
||||
:db-inst="getNowDbInst()"
|
||||
:db-name="db"
|
||||
:columns="columns!"
|
||||
:title="state.tableDataFormDialog.title"
|
||||
:table-name="table"
|
||||
v-model:visible="state.tableDataFormDialog.visible"
|
||||
v-model="state.tableDataFormDialog.data"
|
||||
@submit-success="emits('changeUpdatedField')"
|
||||
/>
|
||||
|
||||
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { ElInput } from 'element-plus';
|
||||
import { ElInput, ElMessage } from 'element-plus';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import { DbInst } from '@/views/ops/db/db';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
@@ -154,6 +165,7 @@ import { dateStrFormat } from '@/common/utils/date';
|
||||
import { useIntervalFn, useStorage } from '@vueuse/core';
|
||||
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
||||
import ColumnFormItem from './ColumnFormItem.vue';
|
||||
import DbTableDataForm from './DbTableDataForm.vue';
|
||||
|
||||
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
||||
|
||||
@@ -247,6 +259,13 @@ const cmDataDel = new ContextmenuItem('deleteData', '删除')
|
||||
return state.table == '';
|
||||
});
|
||||
|
||||
const cmDataEdit = new ContextmenuItem('editData', '编辑行')
|
||||
.withIcon('edit')
|
||||
.withOnClick(() => onEditRowData())
|
||||
.withHideFunc(() => {
|
||||
return state.table == '';
|
||||
});
|
||||
|
||||
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
||||
.withIcon('tickets')
|
||||
.withOnClick(() => onGenerateInsertSql())
|
||||
@@ -333,7 +352,11 @@ const state = reactive({
|
||||
},
|
||||
items: [] as ContextmenuItem[],
|
||||
},
|
||||
|
||||
tableDataFormDialog: {
|
||||
data: {},
|
||||
title: '',
|
||||
visible: false,
|
||||
},
|
||||
genTxtDialog: {
|
||||
title: 'SQL',
|
||||
visible: false,
|
||||
@@ -444,7 +467,7 @@ const formatDataValues = (datas: any) => {
|
||||
};
|
||||
|
||||
const setTableData = (datas: any) => {
|
||||
tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
||||
tableRef.value?.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
||||
selectionRowsMap.clear();
|
||||
cellUpdateMap.clear();
|
||||
formatDataValues(datas);
|
||||
@@ -576,7 +599,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
|
||||
const { clientX, clientY } = event;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataEdit, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
||||
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
||||
};
|
||||
|
||||
@@ -601,6 +624,18 @@ const onDeleteData = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const onEditRowData = () => {
|
||||
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||
if (selectionDatas.length > 1) {
|
||||
ElMessage.warning('只能编辑一行数据');
|
||||
return;
|
||||
}
|
||||
const data = selectionDatas[0];
|
||||
state.tableDataFormDialog.data = data;
|
||||
state.tableDataFormDialog.title = `编辑表'${props.table}'数据`;
|
||||
state.tableDataFormDialog.visible = true;
|
||||
};
|
||||
|
||||
const onGenerateInsertSql = async () => {
|
||||
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||
state.genTxtDialog.txt = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
|
||||
@@ -714,36 +749,21 @@ const submitUpdateFields = async () => {
|
||||
|
||||
const db = state.db;
|
||||
let res = '';
|
||||
const dbDialect = getDbDialect(dbInst.type);
|
||||
|
||||
for (let updateRow of cellUpdateMap.values()) {
|
||||
let sql = `UPDATE ${dbInst.wrapName(state.table)} SET `;
|
||||
const rowData = updateRow.rowData;
|
||||
// 主键列信息
|
||||
const primaryKey = await dbInst.loadTableColumn(db, state.table);
|
||||
let primaryKeyType = primaryKey.columnType;
|
||||
let primaryKeyName = primaryKey.columnName;
|
||||
let primaryKeyValue = rowData[primaryKeyName];
|
||||
const rowData = { ...updateRow.rowData };
|
||||
let updateColumnValue = {};
|
||||
|
||||
for (let k of updateRow.columnsMap.keys()) {
|
||||
const v = updateRow.columnsMap.get(k);
|
||||
if (!v) {
|
||||
continue;
|
||||
}
|
||||
// 更新字段列信息
|
||||
const updateColumn = await dbInst.loadTableColumn(db, state.table, k);
|
||||
|
||||
sql += ` ${dbInst.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, rowData[k], dbDialect)},`;
|
||||
|
||||
// 如果修改的字段是主键
|
||||
if (k === primaryKeyName) {
|
||||
primaryKeyValue = v.oldValue;
|
||||
}
|
||||
updateColumnValue[k] = rowData[k];
|
||||
// 将更新的字段对应的原始数据还原(主要应对可能更新修改了主键等)
|
||||
rowData[k] = v.oldValue;
|
||||
}
|
||||
|
||||
sql = sql.substring(0, sql.length - 1);
|
||||
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`;
|
||||
res += sql;
|
||||
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
|
||||
}
|
||||
|
||||
dbInst.promptExeSql(
|
||||
@@ -868,9 +888,15 @@ defineExpose({
|
||||
color: var(--el-color-info-light-3);
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
top: -5px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.column-right {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 0;
|
||||
padding: 2px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="title" :destroy-on-close="true" width="600px">
|
||||
<el-form ref="dataForm" :model="modelValue" :show-message="false" label-width="auto" size="small">
|
||||
<el-form-item
|
||||
v-for="column in columns"
|
||||
:key="column.columnName"
|
||||
class="w100 mb5"
|
||||
:prop="column.columnName"
|
||||
:required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
|
||||
>
|
||||
<template #label>
|
||||
<span class="pointer" :title="`${column.columnType} | ${column.columnComment}`">
|
||||
{{ column.columnName }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<ColumnFormItem
|
||||
v-model="modelValue[`${column.columnName}`]"
|
||||
:data-type="dbInst.getDialect().getDataType(column.columnType)"
|
||||
:placeholder="`${column.columnType} ${column.columnComment}`"
|
||||
:column-name="column.columnName"
|
||||
:disabled="column.isIdentity"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirm">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import ColumnFormItem from './ColumnFormItem.vue';
|
||||
import { DbInst } from '../../db';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
export interface ColumnFormItemProps {
|
||||
dbInst: DbInst;
|
||||
dbName: string;
|
||||
tableName: string;
|
||||
columns: any[];
|
||||
title?: string; // dialog title
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
|
||||
title: '',
|
||||
});
|
||||
|
||||
const modelValue = defineModel<any>('modelValue');
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submitSuccess']);
|
||||
|
||||
const dataForm: any = ref(null);
|
||||
|
||||
let oldValue = null as any;
|
||||
|
||||
onMounted(() => {
|
||||
setOldValue();
|
||||
});
|
||||
|
||||
watch(visible, (newValue) => {
|
||||
if (newValue) {
|
||||
setOldValue();
|
||||
}
|
||||
});
|
||||
|
||||
const setOldValue = () => {
|
||||
// 空对象则为insert操作,否则为update
|
||||
if (Object.keys(modelValue.value).length > 0) {
|
||||
oldValue = Object.assign({}, modelValue.value);
|
||||
}
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
visible.value = false;
|
||||
modelValue.value = {};
|
||||
};
|
||||
|
||||
const confirm = async () => {
|
||||
dataForm.value.validate(async (valid: boolean) => {
|
||||
if (!valid) {
|
||||
ElMessage.error('请正确填写数据信息');
|
||||
return false;
|
||||
}
|
||||
|
||||
const dbInst = props.dbInst;
|
||||
const data = modelValue.value;
|
||||
const db = props.dbName;
|
||||
const tableName = props.tableName;
|
||||
|
||||
let sql = '';
|
||||
if (oldValue) {
|
||||
const updateColumnValue = {};
|
||||
Object.keys(oldValue).forEach((key) => {
|
||||
// 如果新旧值不相等,则为需要更新的字段
|
||||
if (oldValue[key] !== modelValue.value[key]) {
|
||||
updateColumnValue[key] = modelValue.value[key];
|
||||
}
|
||||
});
|
||||
sql = await dbInst.genUpdateSql(db, tableName, updateColumnValue, oldValue);
|
||||
} else {
|
||||
sql = await dbInst.genInsertSql(db, tableName, [data], true);
|
||||
}
|
||||
|
||||
dbInst.promptExeSql(db, sql, null, () => {
|
||||
closeDialog();
|
||||
emit('submitSuccess');
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -158,21 +158,52 @@
|
||||
@data-delete="onRefresh"
|
||||
></db-table-data>
|
||||
|
||||
<el-row type="flex" class="mt5" justify="center">
|
||||
<el-pagination
|
||||
small
|
||||
:total="count"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="pageChange()"
|
||||
layout="prev, pager, next, total, sizes, jumper"
|
||||
v-model:current-page="pageNum"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="pageSizes"
|
||||
></el-pagination>
|
||||
<el-row type="flex" class="mt5" :gutter="10" justify="space-between" style="user-select: none">
|
||||
<el-col :span="12">
|
||||
<el-text
|
||||
id="copyValue"
|
||||
style="color: var(--el-color-info-light-3)"
|
||||
class="is-truncated font12 mt5"
|
||||
@click="copyToClipboard(sql)"
|
||||
:title="sql"
|
||||
>{{ sql }}</el-text
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-row :gutter="10" justify="left">
|
||||
<el-link class="op-page" :underline="false" @click="pageNum = 1" :disabled="pageNum == 1" icon="DArrowLeft" title="首页" />
|
||||
<el-link class="op-page" :underline="false" @click="pageNum = --pageNum || 1" :disabled="pageNum == 1" icon="Back" title="上一页" />
|
||||
<div class="op-page">
|
||||
<el-input-number
|
||||
style="width: 50px"
|
||||
:controls="false"
|
||||
:min="1"
|
||||
v-model="state.setPageNum"
|
||||
size="small"
|
||||
@blur="handleSetPageNum"
|
||||
@keydown.enter="handleSetPageNum"
|
||||
/>
|
||||
</div>
|
||||
<el-link class="op-page" :underline="false" @click="++pageNum" :disabled="datas.length < pageSize" icon="Right" />
|
||||
<el-link class="op-page" :underline="false" @click="handleEndPage" :disabled="datas.length < pageSize" icon="DArrowRight" />
|
||||
<div style="width: 90px" class="op-page ml10">
|
||||
<el-select size="small" :default-first-option="true" v-model="pageSize" @change="handleSizeChange">
|
||||
<el-option
|
||||
style="font-size: 12px; height: 24px; line-height: 24px"
|
||||
v-for="(op, i) in pageSizes"
|
||||
:key="i"
|
||||
:label="op + '条/页'"
|
||||
:value="op"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<el-button @click="handleCount" :loading="state.counting" class="ml10" text bg size="small">
|
||||
{{ state.showTotal ? `${state.total} 条` : 'count' }}
|
||||
</el-button>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div style="font-size: 12px; padding: 0 10px; color: #606266">
|
||||
<span>{{ state.sql }}</span>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
|
||||
<el-row>
|
||||
@@ -203,31 +234,16 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="addDataDialog.visible" :title="addDataDialog.title" :destroy-on-close="true" width="600px">
|
||||
<el-form ref="dataForm" :model="addDataDialog.data" :show-message="false" label-width="auto" size="small">
|
||||
<el-form-item
|
||||
v-for="column in columns"
|
||||
:key="column.columnName"
|
||||
class="w100 mb5"
|
||||
:prop="column.columnName"
|
||||
:label="column.columnName"
|
||||
:required="column.nullable != 'YES' && column.columnKey != 'PRI'"
|
||||
>
|
||||
<ColumnFormItem
|
||||
v-model="addDataDialog.data[`${column.columnName}`]"
|
||||
:data-type="dbDialect.getDataType(column.columnType)"
|
||||
:placeholder="`${column.columnType} ${column.columnComment}`"
|
||||
:column-name="column.columnName"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeAddDataDialog">取消</el-button>
|
||||
<el-button type="primary" @click="addRow">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<DbTableDataForm
|
||||
:db-inst="getNowDbInst()"
|
||||
:db-name="dbName"
|
||||
:columns="columns"
|
||||
:title="addDataDialog.title"
|
||||
:table-name="tableName"
|
||||
v-model:visible="addDataDialog.visible"
|
||||
v-model="addDataDialog.data"
|
||||
@submit-success="onRefresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -237,10 +253,11 @@ import { ElMessage } from 'element-plus';
|
||||
|
||||
import { DbInst } from '@/views/ops/db/db';
|
||||
import DbTableData from './DbTableData.vue';
|
||||
import { DbDialect, getDbDialect } from '@/views/ops/db/dialect';
|
||||
import { DbDialect } from '@/views/ops/db/dialect';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import ColumnFormItem from './ColumnFormItem.vue';
|
||||
import { useEventListener, useStorage } from '@vueuse/core';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import DbTableDataForm from './DbTableDataForm.vue';
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
@@ -261,7 +278,6 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const dataForm: any = ref(null);
|
||||
const dbTableRef: Ref = ref(null);
|
||||
const condInputRef: Ref = ref(null);
|
||||
const columnNameSearchInputRef: Ref = ref(null);
|
||||
@@ -289,7 +305,10 @@ const state = reactive({
|
||||
defaultPageSize * 40,
|
||||
defaultPageSize * 80,
|
||||
],
|
||||
count: 0,
|
||||
setPageNum: 0,
|
||||
total: 0,
|
||||
showTotal: false,
|
||||
counting: false,
|
||||
selectionDatas: [] as any,
|
||||
condPopVisible: false,
|
||||
columnNameSearch: '',
|
||||
@@ -305,7 +324,6 @@ const state = reactive({
|
||||
addDataDialog: {
|
||||
data: {},
|
||||
title: '',
|
||||
placeholder: '',
|
||||
visible: false,
|
||||
},
|
||||
tableHeight: '600px',
|
||||
@@ -313,7 +331,7 @@ const state = reactive({
|
||||
dbDialect: {} as DbDialect,
|
||||
});
|
||||
|
||||
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, count, hasUpdatedFileds, conditionDialog, addDataDialog, dbDialect } = toRefs(state);
|
||||
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.tableHeight,
|
||||
@@ -331,7 +349,7 @@ onMounted(async () => {
|
||||
state.tableHeight = props.tableHeight;
|
||||
await onRefresh();
|
||||
|
||||
state.dbDialect = getDbDialect(getNowDbInst().type);
|
||||
state.dbDialect = getNowDbInst().getDialect();
|
||||
useEventListener('click', handlerWindowClick);
|
||||
});
|
||||
|
||||
@@ -346,18 +364,19 @@ const onRefresh = async () => {
|
||||
await selectData();
|
||||
};
|
||||
|
||||
/**
|
||||
* 数据tab修改页数
|
||||
*/
|
||||
const pageChange = async () => {
|
||||
await selectData();
|
||||
};
|
||||
watch(
|
||||
() => state.pageNum,
|
||||
async () => {
|
||||
await selectData();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 单表数据信息查询数据
|
||||
*/
|
||||
const selectData = async () => {
|
||||
state.loading = true;
|
||||
state.setPageNum = state.pageNum;
|
||||
const dbInst = getNowDbInst();
|
||||
const db = props.dbName;
|
||||
const table = props.tableName;
|
||||
@@ -370,16 +389,10 @@ const selectData = async () => {
|
||||
state.columns = columns;
|
||||
}
|
||||
|
||||
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
|
||||
state.count = countRes.res[0].count || countRes.res[0].COUNT || 0;
|
||||
let sql = dbInst.getDefaultSelectSql(table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
||||
let sql = dbInst.getDefaultSelectSql(db, table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
||||
state.sql = sql;
|
||||
if (state.count > 0) {
|
||||
const colAndData: any = await dbInst.runSql(db, sql);
|
||||
state.datas = colAndData.res;
|
||||
} else {
|
||||
state.datas = [];
|
||||
}
|
||||
const colAndData: any = await dbInst.runSql(db, sql);
|
||||
state.datas = colAndData.res;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
@@ -391,6 +404,33 @@ const handleSizeChange = async (size: any) => {
|
||||
await selectData();
|
||||
};
|
||||
|
||||
const handleEndPage = async () => {
|
||||
await handleCount();
|
||||
state.pageNum = Math.ceil(state.total / state.pageSize);
|
||||
await selectData();
|
||||
};
|
||||
|
||||
const handleSetPageNum = async () => {
|
||||
state.pageNum = state.setPageNum;
|
||||
await selectData();
|
||||
};
|
||||
const handleCount = async () => {
|
||||
state.counting = true;
|
||||
|
||||
try {
|
||||
const db = props.dbName;
|
||||
const table = props.tableName;
|
||||
const dbInst = getNowDbInst();
|
||||
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
|
||||
state.total = parseInt(countRes.res[0].count || countRes.res[0].COUNT || 0);
|
||||
state.showTotal = true;
|
||||
} catch (e) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
state.counting = false;
|
||||
};
|
||||
|
||||
// 完整的条件,每次选中后会重置条件框内容,故需要这个变量在获取建议时将文本框内容保存
|
||||
let completeCond = '';
|
||||
// 是否存在列建议
|
||||
@@ -543,40 +583,10 @@ const onShowAddDataDialog = async () => {
|
||||
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
||||
state.addDataDialog.visible = true;
|
||||
};
|
||||
|
||||
const closeAddDataDialog = () => {
|
||||
state.addDataDialog.visible = false;
|
||||
state.addDataDialog.data = {};
|
||||
};
|
||||
|
||||
// 添加新数据行
|
||||
const addRow = async () => {
|
||||
dataForm.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
const dbInst = getNowDbInst();
|
||||
const data = state.addDataDialog.data;
|
||||
// key: 字段名,value: 字段名提示
|
||||
let obj: any = {};
|
||||
for (let item of state.columns) {
|
||||
const value = data[item.columnName];
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
obj[`${dbInst.wrapName(item.columnName)}`] = DbInst.wrapValueByType(value);
|
||||
}
|
||||
let columnNames = Object.keys(obj).join(',');
|
||||
let values = Object.values(obj).join(',');
|
||||
let sql = `INSERT INTO ${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
|
||||
dbInst.promptExeSql(props.dbName, sql, null, () => {
|
||||
closeAddDataDialog();
|
||||
onRefresh();
|
||||
});
|
||||
} else {
|
||||
ElMessage.error('请正确填写数据信息');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss">
|
||||
.op-page {
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="90%" :close-on-press-escape="false" :close-on-click-modal="false">
|
||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="70%" :close-on-press-escape="false" :close-on-click-modal="false">
|
||||
<el-form label-position="left" ref="formRef" :model="tableData" label-width="80px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
@@ -26,7 +26,7 @@
|
||||
:width="item.width"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name"> </el-input>
|
||||
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name" />
|
||||
|
||||
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
|
||||
<el-option
|
||||
@@ -42,35 +42,30 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<el-input v-else-if="item.prop === 'value'" size="small" v-model="scope.row.value"> </el-input>
|
||||
<el-input v-else-if="item.prop === 'value'" size="small" v-model="scope.row.value" />
|
||||
|
||||
<el-input v-else-if="item.prop === 'length'" size="small" v-model="scope.row.length"> </el-input>
|
||||
<el-input v-else-if="item.prop === 'length'" type="number" size="small" v-model.number="scope.row.length" />
|
||||
|
||||
<el-input v-else-if="item.prop === 'numScale'" size="small" v-model="scope.row.numScale"> </el-input>
|
||||
<el-input v-else-if="item.prop === 'numScale'" type="number" size="small" v-model.number="scope.row.numScale" />
|
||||
|
||||
<el-checkbox v-else-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull"> </el-checkbox>
|
||||
<el-checkbox v-else-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull" />
|
||||
|
||||
<el-checkbox v-else-if="item.prop === 'pri'" size="small" v-model="scope.row.pri"> </el-checkbox>
|
||||
<el-checkbox v-else-if="item.prop === 'pri'" size="small" v-model="scope.row.pri" />
|
||||
|
||||
<el-checkbox
|
||||
v-else-if="item.prop === 'auto_increment'"
|
||||
size="small"
|
||||
v-model="scope.row.auto_increment"
|
||||
:disabled="dbType === DbType.postgresql"
|
||||
>
|
||||
</el-checkbox>
|
||||
:disabled="disableEditIncr()"
|
||||
/>
|
||||
|
||||
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark"> </el-input>
|
||||
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark" />
|
||||
|
||||
<el-link
|
||||
v-else-if="item.prop === 'action'"
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
:underline="false"
|
||||
@click.prevent="deleteRow(scope.$index)"
|
||||
>删除</el-link
|
||||
>
|
||||
<el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteRow(scope.$index)">
|
||||
<template #reference>
|
||||
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -104,21 +99,15 @@
|
||||
<el-checkbox v-if="item.prop === 'unique'" size="small" v-model="scope.row.unique" @change="indexChanges(scope.row)">
|
||||
</el-checkbox>
|
||||
|
||||
<el-select v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType">
|
||||
<el-option v-for="typeValue in indexTypeList" :key="typeValue" :value="typeValue">{{ typeValue }}</el-option>
|
||||
</el-select>
|
||||
<el-input v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType" />
|
||||
|
||||
<el-input v-if="item.prop === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input>
|
||||
|
||||
<el-link
|
||||
v-if="item.prop === 'action'"
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
:underline="false"
|
||||
@click.prevent="deleteIndex(scope.$index)"
|
||||
>删除</el-link
|
||||
>
|
||||
<el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteIndex(scope.$index)">
|
||||
<template #reference>
|
||||
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -130,6 +119,7 @@
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="cancel()">取消</el-button>
|
||||
<el-button :loading="btnloading" @click="submit()" type="primary">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -166,7 +156,7 @@ const props = defineProps({
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
||||
|
||||
const dbDialect = getDbDialect(props.dbType);
|
||||
let dbDialect = getDbDialect(props.dbType);
|
||||
|
||||
type ColName = {
|
||||
prop: string;
|
||||
@@ -180,29 +170,33 @@ const state = reactive({
|
||||
btnloading: false,
|
||||
activeName: '1',
|
||||
columnTypeList: dbDialect.getInfo().columnTypes,
|
||||
indexTypeList: ['BTREE', 'NORMAL'], // mysql索引类型详解 http://c.biancheng.net/view/7897.html
|
||||
tableData: {
|
||||
fields: {
|
||||
colNames: [
|
||||
{
|
||||
prop: 'name',
|
||||
label: '字段名称',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
prop: 'type',
|
||||
label: '字段类型',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
prop: 'length',
|
||||
label: '长度',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
prop: 'numScale',
|
||||
label: '小数点',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
prop: 'value',
|
||||
label: '默认值',
|
||||
width: 120,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -231,6 +225,7 @@ const state = reactive({
|
||||
},
|
||||
] as ColName[],
|
||||
res: [] as RowDefinition[],
|
||||
oldFields: [] as RowDefinition[],
|
||||
},
|
||||
indexs: {
|
||||
colNames: [
|
||||
@@ -261,19 +256,34 @@ const state = reactive({
|
||||
],
|
||||
columns: [{ name: '', remark: '' }],
|
||||
res: [] as IndexDefinition[],
|
||||
oldIndexs: [] as IndexDefinition[],
|
||||
},
|
||||
tableName: '',
|
||||
tableComment: '',
|
||||
height: 450,
|
||||
db: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { dialogVisible, btnloading, activeName, indexTypeList, tableData } = toRefs(state);
|
||||
const { dialogVisible, btnloading, activeName, tableData } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
dbDialect = getDbDialect(newValue.dbType);
|
||||
});
|
||||
|
||||
// 切换到索引tab时,刷新索引字段下拉选项
|
||||
watch(
|
||||
() => state.activeName,
|
||||
(newValue) => {
|
||||
if (newValue === '2') {
|
||||
state.tableData.indexs.columns = state.tableData.fields.res.map((a) => {
|
||||
return { name: a.name, remark: a.remark };
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
reset();
|
||||
@@ -359,7 +369,10 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
|
||||
nowArr.forEach((a) => {
|
||||
let k = a[key];
|
||||
newMap[k] = a;
|
||||
if (!oldMap.hasOwnProperty(k)) {
|
||||
// 取oldName,因为修改了name,但是oldName不会变
|
||||
let oldName = a['oldName'];
|
||||
oldName && (newMap[oldName] = a);
|
||||
if (!oldMap.hasOwnProperty(k) && (!oldName || (oldName && !oldMap.hasOwnProperty(oldName)))) {
|
||||
// 新增
|
||||
data.add.push(a);
|
||||
}
|
||||
@@ -376,7 +389,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
|
||||
for (let f in a) {
|
||||
let oldV = a[f];
|
||||
let newV = newData[f];
|
||||
if (oldV.toString() !== newV.toString()) {
|
||||
if (oldV?.toString() !== newV?.toString()) {
|
||||
data.upd.push(newData);
|
||||
break;
|
||||
}
|
||||
@@ -390,22 +403,22 @@ const genSql = () => {
|
||||
let data = state.tableData;
|
||||
// 创建表
|
||||
if (!props.data?.edit) {
|
||||
if (state.activeName === '1') {
|
||||
return dbDialect.getCreateTableSql(data);
|
||||
} else if (state.activeName === '2' && data.indexs.res.length > 0) {
|
||||
return dbDialect.getCreateIndexSql(data);
|
||||
let createTable = dbDialect.getCreateTableSql(data);
|
||||
let createIndex = '';
|
||||
if (data.indexs.res.length > 0) {
|
||||
createIndex = dbDialect.getCreateIndexSql(data);
|
||||
}
|
||||
return createTable + ';' + createIndex;
|
||||
} else {
|
||||
// 修改
|
||||
if (state.activeName === '1') {
|
||||
// 修改列
|
||||
let changeData = filterChangedData(oldData.fields, state.tableData.fields.res, 'name');
|
||||
return dbDialect.getModifyColumnSql(data.tableName, changeData);
|
||||
} else if (state.activeName === '2') {
|
||||
// 修改索引
|
||||
let changeData = filterChangedData(oldData.indexs, state.tableData.indexs.res, 'indexName');
|
||||
return dbDialect.getModifyIndexSql(data.tableName, changeData);
|
||||
}
|
||||
// 修改列
|
||||
let changeColData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
|
||||
let colSql = dbDialect.getModifyColumnSql(data, data.tableName, changeColData);
|
||||
// 修改索引
|
||||
let changeIdxData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
|
||||
let idxSql = dbDialect.getModifyIndexSql(data, data.tableName, changeIdxData);
|
||||
// 修改表名
|
||||
|
||||
return colSql + ';' + idxSql;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -414,28 +427,8 @@ const reset = () => {
|
||||
formRef.value.resetFields();
|
||||
state.tableData.tableName = '';
|
||||
state.tableData.tableComment = '';
|
||||
state.tableData.fields.res = [
|
||||
{
|
||||
name: '',
|
||||
type: '',
|
||||
value: '',
|
||||
length: '',
|
||||
numScale: '',
|
||||
notNull: false,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '',
|
||||
},
|
||||
];
|
||||
state.tableData.indexs.res = [
|
||||
{
|
||||
indexName: '',
|
||||
columnNames: [],
|
||||
unique: false,
|
||||
indexType: 'BTREE',
|
||||
indexComment: '',
|
||||
},
|
||||
];
|
||||
state.tableData.fields.res = [];
|
||||
state.tableData.indexs.res = [];
|
||||
};
|
||||
|
||||
const indexChanges = (row: any) => {
|
||||
@@ -456,7 +449,21 @@ const indexChanges = (row: any) => {
|
||||
row.indexComment = `${tableData.value.tableName}表(${name.replaceAll('_', ',')})${commentSuffix}`;
|
||||
};
|
||||
|
||||
const oldData = { indexs: [] as any[], fields: [] as RowDefinition[] };
|
||||
const disableEditIncr = () => {
|
||||
if (DbType.postgresql === props.dbType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果是mssql则不能修改自增
|
||||
if (props.data?.edit) {
|
||||
if (DbType.mssql === props.dbType) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newValue: any) => {
|
||||
@@ -464,9 +471,10 @@ watch(
|
||||
// 回显表名表注释
|
||||
state.tableData.tableName = row.tableName;
|
||||
state.tableData.tableComment = row.tableComment;
|
||||
state.tableData.db = props.db!;
|
||||
// 回显列
|
||||
if (columns && Array.isArray(columns) && columns.length > 0) {
|
||||
oldData.fields = [];
|
||||
state.tableData.fields.oldFields = [];
|
||||
state.tableData.fields.res = [];
|
||||
// 索引列下拉选
|
||||
state.tableData.indexs.columns = [];
|
||||
@@ -474,26 +482,33 @@ watch(
|
||||
let typeObj = a.columnType.replace(')', '').split('(');
|
||||
let type = typeObj[0];
|
||||
let length = (typeObj.length > 1 && typeObj[1]) || '';
|
||||
let defaultValue = '';
|
||||
if (a.columnDefault) {
|
||||
defaultValue = a.columnDefault.trim().replace(/^'|'$/g, '');
|
||||
// 解决高斯的默认值问题
|
||||
defaultValue = defaultValue.replace("'::character varying", '');
|
||||
}
|
||||
let data = {
|
||||
name: a.columnName,
|
||||
oldName: a.columnName,
|
||||
type,
|
||||
value: a.columnDefault || '',
|
||||
value: defaultValue,
|
||||
length,
|
||||
numScale: a.numScale,
|
||||
notNull: a.nullable !== 'YES',
|
||||
pri: a.columnKey === 'PRI',
|
||||
auto_increment: a.columnKey === 'PRI' /*a.extra?.indexOf('auto_increment') > -1*/,
|
||||
pri: a.isPrimaryKey,
|
||||
auto_increment: a.isIdentity /*a.extra?.indexOf('auto_increment') > -1*/,
|
||||
remark: a.columnComment,
|
||||
};
|
||||
state.tableData.fields.res.push(data);
|
||||
oldData.fields.push(JSON.parse(JSON.stringify(data)));
|
||||
state.tableData.fields.oldFields.push(JSON.parse(JSON.stringify(data)));
|
||||
// 索引字段下拉选项
|
||||
state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment });
|
||||
});
|
||||
}
|
||||
// 回显索引
|
||||
if (indexs && Array.isArray(indexs) && indexs.length > 0) {
|
||||
oldData.indexs = [];
|
||||
state.tableData.indexs.oldIndexs = [];
|
||||
state.tableData.indexs.res = [];
|
||||
// 索引过滤掉主键
|
||||
indexs
|
||||
@@ -502,12 +517,12 @@ watch(
|
||||
let data = {
|
||||
indexName: a.indexName,
|
||||
columnNames: a.columnName?.split(','),
|
||||
unique: a.nonUnique === 0 || false,
|
||||
unique: a.isUnique || false,
|
||||
indexType: a.indexType,
|
||||
indexComment: a.indexComment,
|
||||
};
|
||||
state.tableData.indexs.res.push(data);
|
||||
oldData.indexs.push(JSON.parse(JSON.stringify(data)));
|
||||
state.tableData.indexs.oldIndexs.push(JSON.parse(JSON.stringify(data)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,9 +68,7 @@
|
||||
<template #default="scope">
|
||||
<el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link>
|
||||
<el-link class="ml5" @click.prevent="showTableIndex(scope.row)" type="success">索引</el-link>
|
||||
<el-link class="ml5" v-if="tableCreateDialog.enableEditTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning"
|
||||
>编辑表</el-link
|
||||
>
|
||||
<el-link class="ml5" v-if="editDbTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning">编辑表</el-link>
|
||||
<el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -127,7 +125,7 @@ import SqlExecBox from '../sqleditor/SqlExecBox';
|
||||
import config from '@/common/config';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import { isTrue } from '@/common/assert';
|
||||
import { compatibleMysql, DbType } from '../../dialect/index';
|
||||
import { compatibleMysql, DbType, editDbTypes } from '../../dialect/index';
|
||||
|
||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
||||
|
||||
@@ -181,7 +179,6 @@ const state = reactive({
|
||||
visible: false,
|
||||
activeName: '1',
|
||||
type: '',
|
||||
enableEditTypes: [DbType.mysql, DbType.mariadb, DbType.postgresql, DbType.dm, DbType.oracle], // 支持"编辑表"的数据库类型
|
||||
data: {
|
||||
// 修改表时,传递修改数据
|
||||
edit: false,
|
||||
|
||||
@@ -7,6 +7,10 @@ import { editor, languages, Position } from 'monaco-editor';
|
||||
|
||||
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
|
||||
import { type RemovableRef, useLocalStorage } from '@vueuse/core';
|
||||
|
||||
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
|
||||
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
|
||||
|
||||
const dbInstCache: Map<number, DbInst> = new Map();
|
||||
|
||||
@@ -58,17 +62,23 @@ export class DbInst {
|
||||
if (!dbName) {
|
||||
throw new Error('dbName不能为空');
|
||||
}
|
||||
let db = this.dbs.get(dbName);
|
||||
let key = `${this.id}_${dbName}`;
|
||||
let db = this.dbs.get(key);
|
||||
if (db) {
|
||||
return db;
|
||||
}
|
||||
console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`);
|
||||
db = new Db();
|
||||
db.name = dbName;
|
||||
this.dbs.set(dbName, db);
|
||||
this.dbs.set(key, db);
|
||||
return db;
|
||||
}
|
||||
|
||||
// 获取数据库实例方言
|
||||
getDialect(): DbDialect {
|
||||
return getDbDialect(this.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载数据库表信息
|
||||
* @param dbName 数据库名
|
||||
@@ -77,17 +87,22 @@ export class DbInst {
|
||||
*/
|
||||
async loadTables(dbName: string, reload?: boolean) {
|
||||
const db = this.getDb(dbName);
|
||||
// 优先从 table map中获取
|
||||
let tables = db.tables;
|
||||
let key = this.dbTablesKey(dbName);
|
||||
let tables = tableStorage.value.get(key);
|
||||
// 优先从 table 缓存中获取
|
||||
if (!reload && tables) {
|
||||
db.tables = tables;
|
||||
return tables;
|
||||
}
|
||||
// 重置列信息缓存与表提示信息
|
||||
db.columnsMap?.clear();
|
||||
db.tableHints = null;
|
||||
console.log(`load tables -> dbName: ${dbName}`);
|
||||
tables = await dbApi.tableInfos.request({ id: this.id, db: dbName });
|
||||
tableStorage.value.set(key, tables);
|
||||
db.tables = tables;
|
||||
|
||||
// 异步加载表提示信息
|
||||
this.loadDbHints(dbName, true).then(() => {});
|
||||
return tables;
|
||||
}
|
||||
|
||||
@@ -169,18 +184,30 @@ export class DbInst {
|
||||
return this.getDb(dbName).getColumn(table, columnName);
|
||||
}
|
||||
|
||||
dbTableHintsKey(dbName: string) {
|
||||
return `db-table-hints_${this.id}_${dbName}`;
|
||||
}
|
||||
|
||||
dbTablesKey(dbName: string) {
|
||||
return `db-tables_${this.id}_${dbName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取库信息提示
|
||||
*/
|
||||
async loadDbHints(dbName: string) {
|
||||
async loadDbHints(dbName: string, reload?: boolean) {
|
||||
const db = this.getDb(dbName);
|
||||
if (db.tableHints) {
|
||||
return db.tableHints;
|
||||
let key = this.dbTableHintsKey(dbName);
|
||||
let hints = hintsStorage.value.get(key);
|
||||
if (!reload && hints) {
|
||||
db.tableHints = hints;
|
||||
return hints;
|
||||
}
|
||||
console.log(`load db-hits -> dbName: ${dbName}`);
|
||||
const hits = await dbApi.hintTables.request({ id: this.id, db: db.name });
|
||||
db.tableHints = hits;
|
||||
return hits;
|
||||
hints = await dbApi.hintTables.request({ id: this.id, db: db.name });
|
||||
db.tableHints = hints;
|
||||
hintsStorage.value.set(key, hints);
|
||||
return hints;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,8 +252,8 @@ export class DbInst {
|
||||
};
|
||||
|
||||
// 获取指定表的默认查询sql
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
|
||||
return getDbDialect(this.type).getDefaultSelectSql(table, condition, orderBy, pageNum, limit);
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
|
||||
return getDbDialect(this.type).getDefaultSelectSql(db, table, condition, orderBy, pageNum, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,7 +262,7 @@ export class DbInst {
|
||||
* @param table 表名
|
||||
* @param datas 要生成的数据
|
||||
*/
|
||||
async genInsertSql(dbName: string, table: string, datas: any[]) {
|
||||
async genInsertSql(dbName: string, table: string, datas: any[], skipNull = false) {
|
||||
if (!datas) {
|
||||
return '';
|
||||
}
|
||||
@@ -247,6 +274,9 @@ export class DbInst {
|
||||
let values = [];
|
||||
for (let column of columns) {
|
||||
const colName = column.columnName;
|
||||
if (skipNull && data[colName] == null) {
|
||||
continue;
|
||||
}
|
||||
colNames.push(this.wrapName(colName));
|
||||
values.push(DbInst.wrapValueByType(data[colName]));
|
||||
}
|
||||
@@ -255,6 +285,38 @@ export class DbInst {
|
||||
return sqls.join(';\n') + ';';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成根据主键更新语句
|
||||
* @param dbName 数据库名
|
||||
* @param table 表名
|
||||
* @param columnValue 要更新的列以及对应的值 field->columnName; value->columnValue
|
||||
* @param rowData 表的一行完整数据(需要获取主键信息)
|
||||
*/
|
||||
async genUpdateSql(dbName: string, table: string, columnValue: {}, rowData: {}) {
|
||||
let schema = '';
|
||||
let dbArr = dbName.split('/');
|
||||
if (dbArr.length == 2) {
|
||||
schema = this.wrapName(dbArr[1]) + '.';
|
||||
}
|
||||
|
||||
let sql = `UPDATE ${schema}${this.wrapName(table)} SET `;
|
||||
// 主键列信息
|
||||
const primaryKey = await this.loadTableColumn(dbName, table);
|
||||
let primaryKeyType = primaryKey.columnType;
|
||||
let primaryKeyName = primaryKey.columnName;
|
||||
let primaryKeyValue = rowData[primaryKeyName];
|
||||
const dialect = this.getDialect();
|
||||
for (let k of Object.keys(columnValue)) {
|
||||
const v = columnValue[k];
|
||||
// 更新字段列信息
|
||||
const updateColumn = await this.loadTableColumn(dbName, table, k);
|
||||
sql += ` ${this.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, v, dialect)},`;
|
||||
}
|
||||
|
||||
sql = sql.substring(0, sql.length - 1);
|
||||
return (sql += ` WHERE ${this.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成根据主键删除的sql语句
|
||||
* @param table 表名
|
||||
@@ -275,6 +337,7 @@ export class DbInst {
|
||||
sql,
|
||||
dbId: this.id,
|
||||
db,
|
||||
dbType: this.getDialect().getInfo().formatSqlDialect,
|
||||
runSuccessCallback: successFunc,
|
||||
cancelCallback: cancelFunc,
|
||||
});
|
||||
@@ -287,7 +350,7 @@ export class DbInst {
|
||||
* @returns
|
||||
*/
|
||||
wrapName = (name: string) => {
|
||||
return getDbDialect(this.type).quoteIdentifier(name);
|
||||
return this.getDialect().quoteIdentifier(name);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -363,7 +426,7 @@ export class DbInst {
|
||||
return value;
|
||||
}
|
||||
if (!dbDialect) {
|
||||
return `${value}`;
|
||||
return `'${value}'`;
|
||||
}
|
||||
return dbDialect.wrapStrValue(columnType, value);
|
||||
}
|
||||
@@ -441,7 +504,7 @@ class Db {
|
||||
getColumn(table: string, columnName: string = '') {
|
||||
const cols = this.getColumns(table);
|
||||
if (!columnName) {
|
||||
const col = cols.find((c: any) => c.columnKey == 'PRI');
|
||||
const col = cols.find((c: any) => c.isPrimaryKey);
|
||||
return col || cols[0];
|
||||
}
|
||||
return cols.find((c: any) => c.columnName == columnName);
|
||||
|
||||
@@ -54,6 +54,7 @@ const DM_TYPE_LIST: sqlColumnType[] = [
|
||||
{ udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '100G-1' },
|
||||
];
|
||||
|
||||
// 参考官方文档:https://eco.dameng.com/document/dm/zh-cn/pm/function.html
|
||||
const replaceFunctions: EditorCompletionItem[] = [
|
||||
// 数值函数
|
||||
{ label: 'ABS', insertText: 'ABS(n)', description: '求数值 n 的绝对值' },
|
||||
@@ -365,21 +366,22 @@ class DMDialect implements DbDialect {
|
||||
};
|
||||
|
||||
dmDialectInfo = {
|
||||
name: 'DM',
|
||||
icon: 'iconfont icon-db-dm',
|
||||
defaultPort: 5236,
|
||||
formatSqlDialect: 'postgresql',
|
||||
formatSqlDialect: 'plsql',
|
||||
columnTypes: DM_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
|
||||
editorCompletions,
|
||||
};
|
||||
return dmDialectInfo;
|
||||
}
|
||||
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `SELECT * FROM "${table}" ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(pageNum, limit)};`;
|
||||
}
|
||||
|
||||
getPageSql(pageNum: number, limit: number) {
|
||||
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit};`;
|
||||
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit}`;
|
||||
}
|
||||
|
||||
getDefaultRows(): RowDefinition[] {
|
||||
@@ -500,7 +502,9 @@ class DMDialect implements DbDialect {
|
||||
// 默认值
|
||||
let defVal = this.getDefaultValueSql(cl);
|
||||
let incr = cl.auto_increment ? 'IDENTITY' : '';
|
||||
return ` "${cl.name}" ${cl.type}${length} ${incr} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
|
||||
// 如果有原名以原名为准
|
||||
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
|
||||
return ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${incr} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
|
||||
}
|
||||
|
||||
getCreateTableSql(data: any): string {
|
||||
@@ -546,35 +550,78 @@ class DMDialect implements DbDialect {
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let sql: string[] = [];
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let schemaArr = tableData.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
|
||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||
|
||||
let modifySql = '';
|
||||
let dropSql = '';
|
||||
let renameSql = '';
|
||||
let commentSql = '';
|
||||
|
||||
// 主键字段
|
||||
let priArr = new Set();
|
||||
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
sql.push(`ALTER TABLE "${tableName}" add COLUMN ${this.genColumnBasicSql(a)}`);
|
||||
modifySql += `ALTER TABLE ${dbTable} add COLUMN ${this.genColumnBasicSql(a)};`;
|
||||
if (a.remark) {
|
||||
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
|
||||
commentSql += `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
|
||||
}
|
||||
if (a.pri) {
|
||||
priArr.add(`"${a.name}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`);
|
||||
if (a.remark) {
|
||||
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
|
||||
let cmtSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
|
||||
if (a.remark && a.oldName === a.name) {
|
||||
commentSql += cmtSql;
|
||||
}
|
||||
// 修改了字段名
|
||||
if (a.oldName !== a.name) {
|
||||
renameSql += `ALTER TABLE ${dbTable} RENAME COLUMN ${this.quoteIdentifier(a.oldName!)} TO ${this.quoteIdentifier(a.name)};`;
|
||||
if (a.remark) {
|
||||
commentSql += cmtSql;
|
||||
}
|
||||
}
|
||||
modifySql += `ALTER TABLE ${dbTable} MODIFY ${this.genColumnBasicSql(a)};`;
|
||||
if (a.pri) {
|
||||
priArr.add(`${this.quoteIdentifier(a.name)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
sql.push(`ALTER TABLE "${tableName}" DROP COLUMN ${a.name}`);
|
||||
dropSql += `ALTER TABLE ${dbTable} DROP COLUMN ${a.name};`;
|
||||
});
|
||||
}
|
||||
return sql.join(';');
|
||||
|
||||
// 编辑主键
|
||||
let dropPkSql = '';
|
||||
if (priArr.size > 0) {
|
||||
let resPri = tableData.fields.res.filter((a: RowDefinition) => a.pri);
|
||||
if (resPri) {
|
||||
priArr.add(`${this.quoteIdentifier(resPri.name)}`);
|
||||
}
|
||||
// 如果有编辑主键字段,则删除主键,再添加主键
|
||||
// 解析表字段中是否含有主键,有的话就删除主键
|
||||
if (tableData.fields.oldFields.find((a: RowDefinition) => a.pri)) {
|
||||
dropPkSql = `ALTER TABLE ${dbTable} DROP PRIMARY KEY;`;
|
||||
}
|
||||
}
|
||||
|
||||
let addPkSql = priArr.size > 0 ? `ALTER TABLE ${dbTable} ADD PRIMARY KEY (${Array.from(priArr).join(',')});` : '';
|
||||
|
||||
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
// 不能直接修改索引名或字段、需要先删后加
|
||||
let dropIndexNames: string[] = [];
|
||||
let addIndexs: any[] = [];
|
||||
|
||||
17
mayfly_go_web/src/views/ops/db/dialect/gauss_dialect.ts
Normal file
17
mayfly_go_web/src/views/ops/db/dialect/gauss_dialect.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
|
||||
import { DialectInfo } from '@/views/ops/db/dialect/index';
|
||||
|
||||
let gsDialectInfo: DialectInfo;
|
||||
export class GaussDialect extends PostgresqlDialect {
|
||||
getInfo(): DialectInfo {
|
||||
if (gsDialectInfo) {
|
||||
return gsDialectInfo;
|
||||
}
|
||||
|
||||
gsDialectInfo = {} as DialectInfo;
|
||||
Object.assign(gsDialectInfo, super.getInfo());
|
||||
gsDialectInfo.icon = 'iconfont icon-gauss';
|
||||
gsDialectInfo.name = 'GaussDB';
|
||||
return gsDialectInfo;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,11 @@ import { PostgresqlDialect } from './postgres_dialect';
|
||||
import { DMDialect } from '@/views/ops/db/dialect/dm_dialect';
|
||||
import { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect';
|
||||
import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_dialect';
|
||||
import { SqliteDialect } from '@/views/ops/db/dialect/sqlite_dialect';
|
||||
import { MssqlDialect } from '@/views/ops/db/dialect/mssql_dialect';
|
||||
import { GaussDialect } from '@/views/ops/db/dialect/gauss_dialect';
|
||||
import { KingbaseEsDialect } from '@/views/ops/db/dialect/kingbaseES_dialect';
|
||||
import { VastbaseDialect } from '@/views/ops/db/dialect/vastbase_dialect';
|
||||
|
||||
export interface sqlColumnType {
|
||||
udtName: string;
|
||||
@@ -14,6 +19,7 @@ export interface sqlColumnType {
|
||||
|
||||
export interface RowDefinition {
|
||||
name: string;
|
||||
oldName?: string;
|
||||
type: string;
|
||||
value: string;
|
||||
length: string;
|
||||
@@ -78,6 +84,11 @@ export const ColumnTypeSubscript = {
|
||||
|
||||
// 数据库基础信息
|
||||
export interface DialectInfo {
|
||||
/**
|
||||
* 数据库类型label
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
@@ -108,10 +119,23 @@ export const DbType = {
|
||||
mysql: 'mysql',
|
||||
mariadb: 'mariadb',
|
||||
postgresql: 'postgres',
|
||||
gauss: 'gauss',
|
||||
dm: 'dm', // 达梦
|
||||
oracle: 'oracle',
|
||||
sqlite: 'sqlite',
|
||||
mssql: 'mssql', // ms sqlserver
|
||||
kingbaseEs: 'kingbaseEs', // 人大金仓 pgsql模式 https://help.kingbase.com.cn/v8/index.html
|
||||
vastbase: 'vastbase', // https://docs.vastdata.com.cn/zh/docs/VastbaseG100Ver2.2.5/doc/%E5%BC%80%E5%8F%91%E8%80%85%E6%8C%87%E5%8D%97/SQL%E5%8F%82%E8%80%83/SQL%E5%8F%82%E8%80%83.html
|
||||
};
|
||||
|
||||
// mysql兼容的数据库
|
||||
export const noSchemaTypes = [DbType.mysql, DbType.mariadb, DbType.sqlite];
|
||||
|
||||
// 有schema层的数据库
|
||||
export const schemaDbTypes = [DbType.postgresql, DbType.gauss, DbType.dm, DbType.oracle, DbType.mssql, DbType.kingbaseEs, DbType.vastbase];
|
||||
|
||||
export const editDbTypes = [...noSchemaTypes, ...schemaDbTypes];
|
||||
|
||||
export const compatibleMysql = (dbType: string): boolean => {
|
||||
switch (dbType) {
|
||||
case DbType.mysql:
|
||||
@@ -130,13 +154,14 @@ export interface DbDialect {
|
||||
|
||||
/**
|
||||
* 获取默认查询sql
|
||||
* @param db 数据库信息
|
||||
* @param table 表名
|
||||
* @param condition 条件
|
||||
* @param orderBy 排序
|
||||
* @param pageNum 页数
|
||||
* @param limit 条数
|
||||
*/
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number): string;
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number): string;
|
||||
|
||||
getPageSql(pageNum: number, limit: number): string;
|
||||
|
||||
@@ -164,47 +189,53 @@ export interface DbDialect {
|
||||
|
||||
/**
|
||||
* 生成编辑列sql
|
||||
* @param tableData 表数据,包含表名、列数据、索引数据
|
||||
* @param tableName 表名
|
||||
* @param changeData 改变信息
|
||||
*/
|
||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string;
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string;
|
||||
|
||||
/**
|
||||
* 生成编辑索引sql
|
||||
* @param tableData 表数据,包含表名、列数据、索引数据
|
||||
* @param tableName 表名
|
||||
* @param changeData 改变数据
|
||||
*/
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
|
||||
|
||||
/** 通过数据库字段类型,返回基本数据类型 */
|
||||
getDataType: (columnType: string) => DataType;
|
||||
getDataType(columnType: string): DataType;
|
||||
|
||||
/** 包装字符串数据, 如:oracle需要把date类型改为 to_date(str, 'yyyy-mm-dd hh24:mi:ss') */
|
||||
wrapStrValue(columnType: string, value: string): string;
|
||||
}
|
||||
|
||||
let mysqlDialect = new MysqlDialect();
|
||||
let mariadbDialect = new MariadbDialect();
|
||||
let postgresDialect = new PostgresqlDialect();
|
||||
let dmDialect = new DMDialect();
|
||||
let oracleDialect = new OracleDialect();
|
||||
|
||||
export const getDbDialect = (dbType: string | undefined): DbDialect => {
|
||||
if (!dbType) {
|
||||
return mysqlDialect;
|
||||
}
|
||||
switch (dbType) {
|
||||
case DbType.mysql:
|
||||
return mysqlDialect;
|
||||
case DbType.mariadb:
|
||||
return mariadbDialect;
|
||||
case DbType.postgresql:
|
||||
return postgresDialect;
|
||||
case DbType.dm:
|
||||
return dmDialect;
|
||||
case DbType.oracle:
|
||||
return oracleDialect;
|
||||
default:
|
||||
throw new Error('不支持的数据库');
|
||||
}
|
||||
let dbType2DialectMap: Map<string, DbDialect> = new Map();
|
||||
|
||||
export const registerDbDialect = (dbType: string, dd: DbDialect) => {
|
||||
dbType2DialectMap.set(dbType, dd);
|
||||
};
|
||||
|
||||
export const getDbDialectMap = () => {
|
||||
return dbType2DialectMap;
|
||||
};
|
||||
|
||||
export const getDbDialect = (dbType?: string): DbDialect => {
|
||||
return dbType2DialectMap.get(dbType!) || mysqlDialect;
|
||||
};
|
||||
|
||||
(function () {
|
||||
console.log('init register db dialect');
|
||||
registerDbDialect(DbType.mysql, mysqlDialect);
|
||||
registerDbDialect(DbType.mariadb, new MariadbDialect());
|
||||
registerDbDialect(DbType.postgresql, new PostgresqlDialect());
|
||||
registerDbDialect(DbType.gauss, new GaussDialect());
|
||||
registerDbDialect(DbType.dm, new DMDialect());
|
||||
registerDbDialect(DbType.oracle, new OracleDialect());
|
||||
registerDbDialect(DbType.sqlite, new SqliteDialect());
|
||||
registerDbDialect(DbType.mssql, new MssqlDialect());
|
||||
registerDbDialect(DbType.kingbaseEs, new KingbaseEsDialect());
|
||||
registerDbDialect(DbType.vastbase, new VastbaseDialect());
|
||||
})();
|
||||
|
||||
18
mayfly_go_web/src/views/ops/db/dialect/kingbaseES_dialect.ts
Normal file
18
mayfly_go_web/src/views/ops/db/dialect/kingbaseES_dialect.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { DialectInfo } from './index';
|
||||
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
|
||||
|
||||
let kbpgDialectInfo: DialectInfo;
|
||||
|
||||
export class KingbaseEsDialect extends PostgresqlDialect {
|
||||
getInfo(): DialectInfo {
|
||||
if (kbpgDialectInfo) {
|
||||
return kbpgDialectInfo;
|
||||
}
|
||||
|
||||
kbpgDialectInfo = {} as DialectInfo;
|
||||
Object.assign(kbpgDialectInfo, super.getInfo());
|
||||
kbpgDialectInfo.name = 'KingbaseES';
|
||||
kbpgDialectInfo.icon = 'iconfont icon-kingbase';
|
||||
return kbpgDialectInfo;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ class MariadbDialect extends MysqlDialect implements DbDialect {
|
||||
|
||||
mariadbDialectInfo = {} as DialectInfo;
|
||||
Object.assign(mariadbDialectInfo, super.getInfo());
|
||||
mariadbDialectInfo.name = 'MariaDB';
|
||||
mariadbDialectInfo.icon = 'iconfont icon-mariadb';
|
||||
return mariadbDialectInfo;
|
||||
}
|
||||
|
||||
405
mayfly_go_web/src/views/ops/db/dialect/mssql_dialect.ts
Normal file
405
mayfly_go_web/src/views/ops/db/dialect/mssql_dialect.ts
Normal file
@@ -0,0 +1,405 @@
|
||||
import { DbInst } from '../db';
|
||||
import { commonCustomKeywords, DataType, DbDialect, DialectInfo, EditorCompletion, EditorCompletionItem, IndexDefinition, RowDefinition } from './index';
|
||||
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
|
||||
|
||||
export { MSSQL_TYPE_LIST, MssqlDialect };
|
||||
|
||||
// 参考官方文档:https://docs.microsoft.com/zh-cn/sql/t-sql/data-types/data-types-transact-sql?view=sql-server-ver15
|
||||
const MSSQL_TYPE_LIST = [
|
||||
//精确数字
|
||||
'bigint',
|
||||
'numeric',
|
||||
'bit',
|
||||
'smallint',
|
||||
'decimal',
|
||||
'smallmoney',
|
||||
'int',
|
||||
'tinyint',
|
||||
'money',
|
||||
// 近似数字
|
||||
'float',
|
||||
'real',
|
||||
// 日期和时间
|
||||
'date',
|
||||
'datetimeoffset',
|
||||
'datetime2',
|
||||
'smalldatetime',
|
||||
'datetime',
|
||||
'time',
|
||||
// 字符串
|
||||
'char',
|
||||
'varchar',
|
||||
'text',
|
||||
'nchar',
|
||||
'nvarchar',
|
||||
'ntext',
|
||||
'binary',
|
||||
'varbinary',
|
||||
|
||||
// 其他
|
||||
'cursor',
|
||||
'rowversion',
|
||||
'hierarchyid',
|
||||
'uniqueidentifier',
|
||||
'sql_variant',
|
||||
'xml',
|
||||
'table',
|
||||
// 空间几何类型 参照 https://learn.microsoft.com/zh-cn/sql/t-sql/spatial-geometry/spatial-types-geometry-transact-sql?view=sql-server-ver15
|
||||
'geometry',
|
||||
// 空间地理类型 参照 https://learn.microsoft.com/zh-cn/sql/t-sql/spatial-geography/spatial-types-geography?view=sql-server-ver15
|
||||
'geography',
|
||||
];
|
||||
// 函数参考官方文档 https://learn.microsoft.com/zh-cn/sql/t-sql/functions/functions?view=sql-server-ver15
|
||||
|
||||
let mssqlDialectInfo: DialectInfo;
|
||||
|
||||
const customKeywords: EditorCompletionItem[] = [
|
||||
{
|
||||
label: 'select top ',
|
||||
description: 'keyword',
|
||||
insertText: 'select top 100 * from',
|
||||
},
|
||||
{
|
||||
label: 'select page ',
|
||||
description: 'keyword',
|
||||
insertText: 'SELECT *, 0 AS _ORDER_F_ FROM table_name \n ORDER BY _ORDER_F_ \n OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY;',
|
||||
},
|
||||
];
|
||||
|
||||
const fixedLengthTypes = [
|
||||
'int',
|
||||
'bigint',
|
||||
'smallint',
|
||||
'tinyint',
|
||||
'float',
|
||||
'real',
|
||||
'datetime',
|
||||
'smalldatetime',
|
||||
'date',
|
||||
'time',
|
||||
'datetime2',
|
||||
'datetimeoffset',
|
||||
'bit',
|
||||
'uniqueidentifier',
|
||||
'geometry',
|
||||
'geography',
|
||||
];
|
||||
|
||||
class MssqlDialect implements DbDialect {
|
||||
getInfo(): DialectInfo {
|
||||
if (mssqlDialectInfo) {
|
||||
return mssqlDialectInfo;
|
||||
}
|
||||
|
||||
let { keywords, operators, builtinVariables, builtinFunctions } = sqlLanguage;
|
||||
let functions = builtinFunctions.map((a: string): EditorCompletionItem => ({ label: a, insertText: `${a}()`, description: 'func' }));
|
||||
|
||||
let excludeKeywords = new Set(operators);
|
||||
let editorCompletions: EditorCompletion = {
|
||||
keywords: keywords
|
||||
.filter((a: string) => !excludeKeywords.has(a)) // 移除已存在的operator、function
|
||||
.map((a: string): EditorCompletionItem => ({ label: a, description: 'keyword' }))
|
||||
.concat(customKeywords)
|
||||
.concat(commonCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' }))),
|
||||
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
|
||||
functions,
|
||||
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
|
||||
};
|
||||
|
||||
mssqlDialectInfo = {
|
||||
name: 'MSSQL',
|
||||
icon: 'iconfont icon-MSSQLNATIVE',
|
||||
defaultPort: 1433,
|
||||
formatSqlDialect: 'transactsql',
|
||||
columnTypes: MSSQL_TYPE_LIST.map((a) => ({ udtName: a, dataType: a, desc: '', space: '' })),
|
||||
editorCompletions,
|
||||
};
|
||||
return mssqlDialectInfo;
|
||||
}
|
||||
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
let schema = db.split('/')[1];
|
||||
return `SELECT *, 0 AS _MAY_ORDER_F_ FROM ${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${
|
||||
orderBy ? orderBy + ', _MAY_ORDER_F_' : 'order by _MAY_ORDER_F_'
|
||||
} ${this.getPageSql(pageNum, limit)};`.toUpperCase();
|
||||
}
|
||||
|
||||
getPageSql(pageNum: number, limit: number) {
|
||||
return ` offset ${(pageNum - 1) * limit} rows fetch next ${limit} rows only`.toUpperCase();
|
||||
}
|
||||
|
||||
getDefaultRows(): RowDefinition[] {
|
||||
return [
|
||||
{ name: 'id', type: 'bigint', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
|
||||
{ name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
|
||||
{
|
||||
name: 'creator',
|
||||
type: 'varchar',
|
||||
length: '100',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建人姓名',
|
||||
},
|
||||
{
|
||||
name: 'create_time',
|
||||
type: 'datetime2',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: 'CURRENT_TIMESTAMP',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建时间',
|
||||
},
|
||||
{ name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
|
||||
{
|
||||
name: 'updator',
|
||||
type: 'varchar',
|
||||
length: '100',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '修改人姓名',
|
||||
},
|
||||
{
|
||||
name: 'update_time',
|
||||
type: 'datetime2',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: 'CURRENT_TIMESTAMP',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '修改时间',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getDefaultIndex(): IndexDefinition {
|
||||
return {
|
||||
indexName: '',
|
||||
columnNames: [],
|
||||
unique: false,
|
||||
indexType: 'NONCLUSTERED',
|
||||
indexComment: '',
|
||||
};
|
||||
}
|
||||
|
||||
quoteIdentifier = (name: string) => {
|
||||
return `[${name}]`;
|
||||
};
|
||||
|
||||
genColumnBasicSql(cl: any): string {
|
||||
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
|
||||
let defVal = val ? `DEFAULT ${val}` : '';
|
||||
// mssql哪些字段允许有长度
|
||||
let length = '';
|
||||
if (!fixedLengthTypes.includes(cl.type)) {
|
||||
length = cl.length ? `(${cl.length})` : '';
|
||||
}
|
||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.auto_increment ? 'IDENTITY(1,1)' : ''} ${defVal} ${cl.notNull ? 'NOT NULL' : 'NULL'} `;
|
||||
}
|
||||
getCreateTableSql(data: any): string {
|
||||
let schema = data.db.split('/')[1];
|
||||
|
||||
// 创建表结构
|
||||
let pks = [] as string[];
|
||||
let fields: string[] = [];
|
||||
let fieldComments: string[] = [];
|
||||
data.fields.res.forEach((item: any) => {
|
||||
item.name && fields.push(this.genColumnBasicSql(item));
|
||||
item.remark &&
|
||||
fieldComments.push(
|
||||
`EXECUTE sp_addextendedproperty N'MS_Description', N'${item.remark}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'COLUMN', N'${item.name}'`
|
||||
);
|
||||
if (item.pri) {
|
||||
pks.push(`${this.quoteIdentifier(item.name)}`);
|
||||
}
|
||||
});
|
||||
|
||||
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
|
||||
|
||||
// 建表语句
|
||||
let createTable = `CREATE TABLE ${baseTable}
|
||||
( ${fields.join(',')}
|
||||
${pks.length > 0 ? `, PRIMARY KEY CLUSTERED (${pks.join(',')})` : ''}
|
||||
);`;
|
||||
|
||||
let createIndexSql = this.getCreateIndexSql(data);
|
||||
|
||||
// 表注释
|
||||
if (data.tableComment) {
|
||||
createTable += ` EXECUTE sp_addextendedproperty N'MS_Description', N'${data.tableComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}';`;
|
||||
}
|
||||
|
||||
return createTable + createIndexSql + fieldComments.join(';');
|
||||
}
|
||||
|
||||
getCreateIndexSql(data: any): string {
|
||||
// CREATE UNIQUE NONCLUSTERED INDEX [aaa]
|
||||
// ON [dbo].[无标题] (
|
||||
// [id],
|
||||
// [name]
|
||||
// )
|
||||
let schema = data.db.split('/')[1];
|
||||
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
|
||||
|
||||
let indexComment = [] as string[];
|
||||
|
||||
// 创建索引
|
||||
let sql: string[] = [];
|
||||
data.indexs.res.forEach((a: any) => {
|
||||
let columnNames = a.columnNames.map((b: string) => `${this.quoteIdentifier(b)}`);
|
||||
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} NONCLUSTERED INDEX ${this.quoteIdentifier(a.indexName)} on ${baseTable} (${columnNames.join(',')})`);
|
||||
if (a.indexComment) {
|
||||
indexComment.push(
|
||||
`EXECUTE sp_addextendedproperty N'MS_Description', N'${a.indexComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${data.tableName}', N'INDEX', N'${a.indexName}'`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return sql.join(';') + ';' + indexComment.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
// sql执行顺序
|
||||
// 1. 删除字段
|
||||
// 2. 添加字段
|
||||
// 3. 修改字段名字
|
||||
// 4. 修改字段类型
|
||||
// 5. 修改字段注释
|
||||
// 6. 添加字段注释
|
||||
|
||||
let schema = tableData.db.split('/')[1];
|
||||
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||
|
||||
let delSql = '';
|
||||
let addArr = [] as string[];
|
||||
let renameArr = [] as string[];
|
||||
let updArr = [] as string[];
|
||||
let changeCommentArr = [] as string[];
|
||||
let addCommentArr = [] as string[];
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
delSql = `ALTER TABLE ${baseTable} DROP ${changeData.del.map((a) => 'COLUMN ' + this.quoteIdentifier(a.name)).join(',')};`;
|
||||
}
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
addArr.push(` ALTER TABLE ${baseTable} ADD COLUMN ${this.genColumnBasicSql(a)}`);
|
||||
if (a.remark) {
|
||||
addCommentArr.push(
|
||||
`EXECUTE sp_addextendedproperty N'MS_Description', N'${a.remark}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'COLUMN', N'${a.name}'`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
if (a.oldName && a.name !== a.oldName) {
|
||||
renameArr.push(` EXEC sp_rename '${baseTable}.${this.quoteIdentifier(a.oldName)}', '${a.name}', 'COLUMN' `);
|
||||
} else {
|
||||
updArr.push(` ALTER TABLE ${baseTable} ALTER COLUMN ${this.genColumnBasicSql(a)} `);
|
||||
}
|
||||
if (a.remark) {
|
||||
changeCommentArr.push(`IF ((SELECT COUNT(*) FROM fn_listextendedproperty('MS_Description',
|
||||
'SCHEMA', N'${schema}',
|
||||
'TABLE', N'${tableName}',
|
||||
'COLUMN', N'${a.name}')) > 0)
|
||||
EXEC sp_updateextendedproperty
|
||||
'MS_Description', N'${a.remark}',
|
||||
'SCHEMA', N'${schema}',
|
||||
'TABLE', N'${tableName}',
|
||||
'COLUMN', N'${a.name}'
|
||||
ELSE
|
||||
EXEC sp_addextendedproperty
|
||||
'MS_Description', N'${a.remark}',
|
||||
'SCHEMA', N'${schema}',
|
||||
'TABLE', N'${tableName}',
|
||||
'COLUMN',N'${a.name}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
delSql +
|
||||
(addArr.length > 0 ? addArr.join(';') + ';' : '') +
|
||||
(renameArr.length > 0 ? renameArr.join(';') + ';' : '') +
|
||||
(updArr.length > 0 ? updArr.join(';') + ';' : '') +
|
||||
(changeCommentArr.length > 0 ? changeCommentArr.join(';') + ';' : '') +
|
||||
(addCommentArr.length > 0 ? addCommentArr.join(';') + ';' : '')
|
||||
);
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
let schema = tableData.db.split('/')[1];
|
||||
let baseTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||
|
||||
let dropArr = [] as string[];
|
||||
let addArr = [] as string[];
|
||||
let commentArr = [] as string[];
|
||||
|
||||
const pushDrop = (a: any) => {
|
||||
dropArr.push(` DROP INDEX ${this.quoteIdentifier(a.indexName)} ON ${baseTable} `);
|
||||
};
|
||||
const pushAdd = (a: any) => {
|
||||
addArr.push(
|
||||
` CREATE ${a.unique ? 'UNIQUE' : ''} NONCLUSTERED INDEX ${this.quoteIdentifier(a.indexName)} ON ${baseTable} (${a.columnNames.map((b: string) => this.quoteIdentifier(b)).join(',')}) `
|
||||
);
|
||||
if (a.indexComment) {
|
||||
commentArr.push(
|
||||
` EXEC sp_addextendedproperty N'MS_Description', N'${a.indexComment}', N'SCHEMA', N'${schema}', N'TABLE', N'${tableName}', N'INDEX', N'${a.indexName}' `
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
pushDrop(a);
|
||||
pushAdd(a);
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
pushDrop(a);
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => pushAdd(a));
|
||||
}
|
||||
let dropSql = dropArr.join(';');
|
||||
let addSql = addArr.join(';');
|
||||
let commentSql = commentArr.join(';');
|
||||
return dropSql + ';' + addSql + ';' + commentSql + ';';
|
||||
}
|
||||
|
||||
getDataType(columnType: string): DataType {
|
||||
if (DbInst.isNumber(columnType)) {
|
||||
return DataType.Number;
|
||||
}
|
||||
// 日期时间类型
|
||||
if (/datetime|timestamp/gi.test(columnType)) {
|
||||
return DataType.DateTime;
|
||||
}
|
||||
// 日期类型
|
||||
if (/date/gi.test(columnType)) {
|
||||
return DataType.Date;
|
||||
}
|
||||
// 时间类型
|
||||
if (/time/gi.test(columnType)) {
|
||||
return DataType.Time;
|
||||
}
|
||||
return DataType.String;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
wrapStrValue(columnType: string, value: string): string {
|
||||
return `'${value}'`;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { language as mysqlLanguage } from 'monaco-editor/esm/vs/basic-languages/
|
||||
|
||||
export { MYSQL_TYPE_LIST, MysqlDialect };
|
||||
|
||||
// 参考官方文档:https://dev.mysql.com/doc/refman/8.0/en/data-types.html
|
||||
const MYSQL_TYPE_LIST = [
|
||||
'bigint',
|
||||
'binary',
|
||||
@@ -31,6 +32,7 @@ const MYSQL_TYPE_LIST = [
|
||||
'varchar',
|
||||
];
|
||||
|
||||
// 参考官方文档:https://dev.mysql.com/doc/refman/8.3/en/functions.html
|
||||
const replaceFunctions: EditorCompletionItem[] = [
|
||||
/** 字符串相关函数 */
|
||||
{ label: 'CONCAT', insertText: 'CONCAT(str1,str2,...)', description: '多字符串合并' },
|
||||
@@ -102,6 +104,7 @@ class MysqlDialect implements DbDialect {
|
||||
};
|
||||
|
||||
mysqlDialectInfo = {
|
||||
name: 'MySQL',
|
||||
icon: 'iconfont icon-op-mysql',
|
||||
defaultPort: 3306,
|
||||
formatSqlDialect: 'mysql',
|
||||
@@ -111,7 +114,7 @@ class MysqlDialect implements DbDialect {
|
||||
return mysqlDialectInfo;
|
||||
}
|
||||
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
|
||||
pageNum,
|
||||
limit
|
||||
@@ -193,7 +196,7 @@ class MysqlDialect implements DbDialect {
|
||||
let defVal = val ? `DEFAULT ${val}` : '';
|
||||
let length = cl.length ? `(${cl.length})` : '';
|
||||
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
|
||||
return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
||||
cl.auto_increment ? 'AUTO_INCREMENT' : ''
|
||||
} ${defVal} ${onUpdate} comment '${cl.remark || ''}' `;
|
||||
}
|
||||
@@ -223,38 +226,34 @@ class MysqlDialect implements DbDialect {
|
||||
return sql.substring(0, sql.length - 1) + ';';
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let addSql = '',
|
||||
updSql = '',
|
||||
delSql = '';
|
||||
if (changeData.add.length > 0) {
|
||||
addSql = `ALTER TABLE ${tableName}`;
|
||||
changeData.add.forEach((a) => {
|
||||
addSql += ` ADD ${this.genColumnBasicSql(a)},`;
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let sql = `ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)}`;
|
||||
let arr = [] as string[];
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
arr.push(` DROP COLUMN ${this.quoteIdentifier(a.name)} `);
|
||||
});
|
||||
}
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
arr.push(` ADD COLUMN ${this.genColumnBasicSql(a)} `);
|
||||
});
|
||||
addSql = addSql.substring(0, addSql.length - 1);
|
||||
addSql += ';';
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
updSql = `ALTER TABLE ${tableName}`;
|
||||
let arr = [] as string[];
|
||||
changeData.upd.forEach((a) => {
|
||||
arr.push(` MODIFY ${this.genColumnBasicSql(a)}`);
|
||||
if (a.name === a.oldName) {
|
||||
arr.push(` MODIFY COLUMN ${this.genColumnBasicSql(a)} `);
|
||||
} else {
|
||||
arr.push(` CHANGE COLUMN ${this.quoteIdentifier(a.oldName!)} ${this.genColumnBasicSql(a)} `);
|
||||
}
|
||||
});
|
||||
updSql += arr.join(',');
|
||||
updSql += ';';
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
delSql += ` ALTER TABLE ${tableName} DROP COLUMN ${a.name}; `;
|
||||
});
|
||||
}
|
||||
return addSql + updSql + delSql;
|
||||
return sql + arr.join(',') + ';';
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
// 搜集修改和删除的索引,添加到drop index xx
|
||||
// 收集新增和修改的索引,添加到ADD xx
|
||||
// ALTER TABLE `test1`
|
||||
|
||||
@@ -14,7 +14,7 @@ import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sq
|
||||
|
||||
export { OracleDialect, ORACLE_TYPE_LIST };
|
||||
|
||||
// 参考文档:https://eco.dameng.com/document/dm/zh-cn/sql-dev/dmpl-sql-datatype.html#%E5%AD%97%E7%AC%A6%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B
|
||||
// 参考官方文档:https://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements001.htm
|
||||
const ORACLE_TYPE_LIST: sqlColumnType[] = [
|
||||
// 字符数据类型
|
||||
{ udtName: 'CHAR', dataType: 'CHAR', desc: '定长字符串,自动在末尾用空格补全,非unicode', space: '', range: '1 - 2000' },
|
||||
@@ -50,6 +50,7 @@ const ORACLE_TYPE_LIST: sqlColumnType[] = [
|
||||
{ udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '' },
|
||||
];
|
||||
|
||||
// 参考官方文档:https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions001.htm
|
||||
const replaceFunctions: EditorCompletionItem[] = [
|
||||
// 字符函数
|
||||
{ label: 'ASCII', insertText: 'ASCII(x)', description: '返回字符X的ASCII码' },
|
||||
@@ -91,7 +92,35 @@ const replaceFunctions: EditorCompletionItem[] = [
|
||||
{ label: 'NVL2', insertText: 'NVL2(x,value1,value2)', description: '如果x非空,返回value1,否则返回value2' },
|
||||
];
|
||||
|
||||
const addCustomKeywords = ['ROWNUM', 'DUAL'];
|
||||
const addCustomKeywords: EditorCompletionItem[] = [
|
||||
{
|
||||
label: 'ROWNUM',
|
||||
description: 'keyword',
|
||||
insertText: 'ROWNUM',
|
||||
},
|
||||
{
|
||||
label: 'DUAL',
|
||||
description: 'keyword',
|
||||
insertText: 'DUAL',
|
||||
},
|
||||
// 分页代码块
|
||||
{
|
||||
label: 'SELECT ROWNUM',
|
||||
description: 'code block',
|
||||
insertText: 'SELECT * from table_name where rownum <= 10',
|
||||
},
|
||||
{
|
||||
label: 'SELECT PAGE',
|
||||
description: 'code block',
|
||||
insertText: ` SELECT * FROM
|
||||
(
|
||||
SELECT t.*, ROWNUM AS rn
|
||||
FROM table_name t
|
||||
WHERE ROWNUM <= 25
|
||||
)
|
||||
WHERE rn > 0 \n`,
|
||||
},
|
||||
];
|
||||
|
||||
let oracleDialectInfo: DialectInfo;
|
||||
class OracleDialect implements DbDialect {
|
||||
@@ -103,6 +132,7 @@ class OracleDialect implements DbDialect {
|
||||
let { keywords, operators, builtinVariables } = sqlLanguage;
|
||||
let functionNames = replaceFunctions.map((a) => a.label);
|
||||
let excludeKeywords = new Set(functionNames.concat(operators));
|
||||
excludeKeywords.add('SELECT');
|
||||
|
||||
let editorCompletions: EditorCompletion = {
|
||||
keywords: keywords
|
||||
@@ -117,21 +147,14 @@ class OracleDialect implements DbDialect {
|
||||
})
|
||||
)
|
||||
)
|
||||
.concat(
|
||||
// 加上自定义的关键字
|
||||
addCustomKeywords.map(
|
||||
(a): EditorCompletionItem => ({
|
||||
label: a,
|
||||
description: 'keyword',
|
||||
})
|
||||
)
|
||||
),
|
||||
.concat(addCustomKeywords),
|
||||
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
|
||||
functions: replaceFunctions,
|
||||
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
|
||||
};
|
||||
|
||||
oracleDialectInfo = {
|
||||
name: 'Oracle',
|
||||
icon: 'iconfont icon-oracle',
|
||||
defaultPort: 1521,
|
||||
formatSqlDialect: 'plsql',
|
||||
@@ -141,7 +164,7 @@ class OracleDialect implements DbDialect {
|
||||
return oracleDialectInfo;
|
||||
}
|
||||
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `
|
||||
SELECT *
|
||||
FROM (
|
||||
@@ -268,16 +291,22 @@ class OracleDialect implements DbDialect {
|
||||
return '';
|
||||
}
|
||||
|
||||
genColumnBasicSql(cl: RowDefinition): string {
|
||||
genColumnBasicSql(cl: RowDefinition, create: boolean): string {
|
||||
let length = this.getTypeLengthSql(cl);
|
||||
// 默认值
|
||||
let defVal = this.getDefaultValueSql(cl);
|
||||
let incr = cl.auto_increment ? 'generated by default as IDENTITY' : '';
|
||||
let pri = cl.pri ? 'PRIMARY KEY' : '';
|
||||
return ` ${cl.name.toUpperCase()} ${cl.type}${length} ${incr} ${pri} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
|
||||
let incr = cl.auto_increment && create ? 'generated by default as IDENTITY' : '';
|
||||
// 如果有原名以原名为准
|
||||
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
|
||||
let baseSql = ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${incr}`;
|
||||
return incr ? baseSql : ` ${baseSql} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
|
||||
}
|
||||
|
||||
getCreateTableSql(data: any): string {
|
||||
let schemaArr = data.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
|
||||
|
||||
let createSql = '';
|
||||
let tableCommentSql = '';
|
||||
let columCommentSql = '';
|
||||
@@ -285,17 +314,17 @@ class OracleDialect implements DbDialect {
|
||||
// 创建表结构
|
||||
let fields: string[] = [];
|
||||
data.fields.res.forEach((item: any) => {
|
||||
item.name && fields.push(this.genColumnBasicSql(item));
|
||||
item.name && fields.push(this.genColumnBasicSql(item, true));
|
||||
// 列注释
|
||||
if (item.remark) {
|
||||
columCommentSql += ` comment on column ${data.tableName?.toUpperCase()}.${item.name?.toUpperCase()} is '${item.remark}'; `;
|
||||
columCommentSql += ` COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(item.name)} is '${item.remark}'; `;
|
||||
}
|
||||
});
|
||||
// 建表
|
||||
createSql = `CREATE TABLE ${data.tableName?.toUpperCase()} ( ${fields.join(',')} );`;
|
||||
createSql = `CREATE TABLE ${dbTable} ( ${fields.join(',')} );`;
|
||||
// 表注释
|
||||
if (data.tableComment) {
|
||||
tableCommentSql = ` comment on table ${data.tableName?.toUpperCase()} is '${data.tableComment}'; `;
|
||||
tableCommentSql = ` COMMENT ON TABLE ${dbTable} is '${data.tableComment}'; `;
|
||||
}
|
||||
|
||||
return createSql + tableCommentSql + columCommentSql;
|
||||
@@ -304,43 +333,95 @@ class OracleDialect implements DbDialect {
|
||||
getCreateIndexSql(tableData: any): string {
|
||||
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
|
||||
// COMMENT ON INDEX idx_column_name IS 'Your index comment here';
|
||||
// 创建索引
|
||||
|
||||
let schemaArr = tableData.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.tableName)}`;
|
||||
|
||||
let sql: string[] = [];
|
||||
tableData.indexs.res.forEach((a: any) => {
|
||||
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} ON "${tableData.tableName}" ("${a.columnNames.join('","')})"`);
|
||||
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} ON ${dbTable} ("${a.columnNames.join('","')})"`);
|
||||
});
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let sql: string[] = [];
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
sql.push(`ALTER TABLE "${tableName}" add COLUMN ${this.genColumnBasicSql(a)}`);
|
||||
if (a.remark) {
|
||||
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let schemaArr = tableData.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||
|
||||
let baseSql = `ALTER TABLE ${dbTable} `;
|
||||
|
||||
let modifyArr: string[] = [];
|
||||
let dropArr: string[] = [];
|
||||
// 重命名的sql要一条条执行
|
||||
let renameArr: string[] = [];
|
||||
let commentArr: string[] = [];
|
||||
|
||||
// 主键字段
|
||||
let priArr = new Set();
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
let commentSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}'`;
|
||||
if (a.remark && a.oldName === a.name) {
|
||||
commentArr.push(commentSql);
|
||||
}
|
||||
// 修改了字段名
|
||||
if (a.oldName !== a.name) {
|
||||
renameArr.push(baseSql + ` RENAME COLUMN ${this.quoteIdentifier(a.oldName!)} TO ${this.quoteIdentifier(a.name)} ;`);
|
||||
if (a.remark) {
|
||||
commentArr.push(commentSql);
|
||||
}
|
||||
}
|
||||
modifyArr.push(` MODIFY (${this.genColumnBasicSql(a, false)})`);
|
||||
if (a.pri) {
|
||||
priArr.add(`${this.quoteIdentifier(a.name)}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`);
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
modifyArr.push(` ADD (${this.genColumnBasicSql(a, false)})`);
|
||||
if (a.remark) {
|
||||
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
|
||||
commentArr.push(`COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}'`);
|
||||
}
|
||||
if (a.pri) {
|
||||
priArr.add(`"${a.name}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
sql.push(`ALTER TABLE "${tableName}" DROP COLUMN ${a.name}`);
|
||||
dropArr.push(`${this.quoteIdentifier(a.name)}`);
|
||||
});
|
||||
}
|
||||
return sql.join(';');
|
||||
|
||||
let dropPkSql = '';
|
||||
if (priArr.size > 0) {
|
||||
let resPri = tableData.fields.res.find((a: RowDefinition) => a.pri);
|
||||
if (resPri) {
|
||||
priArr.add(`"${resPri.name}"`);
|
||||
}
|
||||
// 如果有编辑主键字段,则删除主键,再添加主键
|
||||
// 解析表字段中是否含有主键,有的话就删除主键
|
||||
if (tableData.fields.oldFields.find((a: RowDefinition) => a.pri)) {
|
||||
dropPkSql = `ALTER TABLE ${dbTable} DROP PRIMARY KEY;`;
|
||||
}
|
||||
}
|
||||
|
||||
let modifySql = baseSql + modifyArr.join(' ') + ';';
|
||||
let dropSql = baseSql + ` DROP (${dropArr.join(',')}) ;`;
|
||||
let renameSql = renameArr.join('');
|
||||
let addPkSql = priArr.size > 0 ? `ALTER TABLE ${dbTable} ADD CONSTRAINT "PK_${tableName}" PRIMARY KEY (${Array.from(priArr).join(',')});` : '';
|
||||
let commentSql = commentArr.join(';');
|
||||
|
||||
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
// 不能直接修改索引名或字段、需要先删后加
|
||||
let dropIndexNames: string[] = [];
|
||||
let addIndexs: any[] = [];
|
||||
|
||||
@@ -123,6 +123,7 @@ class PostgresqlDialect implements DbDialect {
|
||||
};
|
||||
|
||||
pgDialectInfo = {
|
||||
name: 'PostgreSQL',
|
||||
icon: 'iconfont icon-op-postgres',
|
||||
defaultPort: 5432,
|
||||
formatSqlDialect: 'postgresql',
|
||||
@@ -132,7 +133,7 @@ class PostgresqlDialect implements DbDialect {
|
||||
return pgDialectInfo;
|
||||
}
|
||||
|
||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
|
||||
pageNum,
|
||||
limit
|
||||
@@ -228,7 +229,7 @@ class PostgresqlDialect implements DbDialect {
|
||||
let marks = false;
|
||||
if (this.matchType(cl.type, ['char', 'time', 'date', 'text'])) {
|
||||
// 默认值是now()的time或date不需要加引号
|
||||
if (cl.value.toLowerCase().replace(' ', '') === 'current_timestamp' && this.matchType(cl.type, ['time', 'date'])) {
|
||||
if (['pg_systimestamp()', 'current_timestamp'].includes(cl.value.toLowerCase()) && this.matchType(cl.type, ['time', 'date'])) {
|
||||
marks = false;
|
||||
} else {
|
||||
marks = true;
|
||||
@@ -260,7 +261,10 @@ class PostgresqlDialect implements DbDialect {
|
||||
let length = this.getTypeLengthSql(cl);
|
||||
// 默认值
|
||||
let defVal = this.getDefaultValueSql(cl);
|
||||
return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
|
||||
// 如果有原名以原名为准
|
||||
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
|
||||
|
||||
return ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
|
||||
}
|
||||
|
||||
getCreateTableSql(data: any): string {
|
||||
@@ -299,52 +303,77 @@ class PostgresqlDialect implements DbDialect {
|
||||
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
|
||||
// COMMENT ON INDEX idx_column_name IS 'Your index comment here';
|
||||
// 创建索引
|
||||
let schema = tableData.db.split('/')[1];
|
||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.tableName)}`;
|
||||
let sql: string[] = [];
|
||||
tableData.indexs.res.forEach((a: any) => {
|
||||
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} USING btree ("${a.columnNames.join('","')})"`);
|
||||
// 字段名用双引号包裹
|
||||
let colArr = a.columnNames.map((a: string) => `${this.quoteIdentifier(a)}`);
|
||||
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} on ${dbTable} (${colArr.join(',')})`);
|
||||
if (a.indexComment) {
|
||||
sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`);
|
||||
sql.push(`COMMENT ON INDEX ${schema}.${this.quoteIdentifier(a.indexName)} IS '${a.indexComment}'`);
|
||||
}
|
||||
});
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let sql: string[] = [];
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
let schemaArr = tableData.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||
|
||||
let dropPkSql = '';
|
||||
let modifySql = '';
|
||||
let dropSql = '';
|
||||
let renameSql = '';
|
||||
let addPkSql = '';
|
||||
let commentSql = '';
|
||||
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
let typeLength = this.getTypeLengthSql(a);
|
||||
let defaultSql = this.getDefaultValueSql(a);
|
||||
sql.push(`ALTER TABLE ${tableName} add ${a.name} ${a.type}${typeLength} ${defaultSql}`);
|
||||
modifySql += `alter table ${dbTable} add ${this.genColumnBasicSql(a)};`;
|
||||
if (a.remark) {
|
||||
sql.push(`comment on column "${tableName}"."${a.name}" is '${a.remark}'`);
|
||||
commentSql += `comment on column ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}';`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.upd.length > 0) {
|
||||
changeData.upd.forEach((a) => {
|
||||
let cmtSql = `comment on column ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}';`;
|
||||
if (a.remark && a.oldName === a.name) {
|
||||
commentSql += cmtSql;
|
||||
}
|
||||
// 修改了字段名
|
||||
if (a.oldName !== a.name) {
|
||||
renameSql += `alter table ${dbTable} rename column ${this.quoteIdentifier(a.oldName!)} to ${this.quoteIdentifier(a.name)};`;
|
||||
if (a.remark) {
|
||||
commentSql += cmtSql;
|
||||
}
|
||||
}
|
||||
let typeLength = this.getTypeLengthSql(a);
|
||||
sql.push(`ALTER TABLE ${tableName} alter column ${a.name} type ${a.type}${typeLength}`);
|
||||
// 如果有原名以原名为准
|
||||
let name = a.oldName && a.name !== a.oldName ? a.oldName : a.name;
|
||||
modifySql += `alter table ${dbTable} alter column ${this.quoteIdentifier(name)} type ${a.type}${typeLength} ;`;
|
||||
let defaultSql = this.getDefaultValueSql(a);
|
||||
if (defaultSql) {
|
||||
sql.push(`alter table ${tableName} alter column ${a.name} set ${defaultSql}`);
|
||||
}
|
||||
if (a.remark) {
|
||||
sql.push(`comment on column "${tableName}"."${a.name}" is '${a.remark}'`);
|
||||
modifySql += `alter table ${dbTable} alter column ${this.quoteIdentifier(name)} set ${defaultSql} ;`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
sql.push(`ALTER TABLE ${tableName} DROP COLUMN ${a.name}`);
|
||||
dropSql += `alter table ${dbTable} drop column ${a.name};`;
|
||||
});
|
||||
}
|
||||
return sql.join(';');
|
||||
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
let schema = tableData.db.split('/')[1];
|
||||
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||
|
||||
// 不能直接修改索引名或字段、需要先删后加
|
||||
let dropIndexNames: string[] = [];
|
||||
let addIndexs: any[] = [];
|
||||
@@ -378,9 +407,11 @@ class PostgresqlDialect implements DbDialect {
|
||||
|
||||
if (addIndexs.length > 0) {
|
||||
addIndexs.forEach((a) => {
|
||||
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')})`);
|
||||
// 字段名用双引号包裹
|
||||
let colArr = a.columnNames.map((a: string) => `${this.quoteIdentifier(a)}`);
|
||||
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} on ${dbTable} (${colArr.join(',')})`);
|
||||
if (a.indexComment) {
|
||||
sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`);
|
||||
sql.push(`COMMENT ON INDEX ${schema}.${this.quoteIdentifier(a.indexName)} IS '${a.indexComment}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -409,7 +440,7 @@ class PostgresqlDialect implements DbDialect {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
wrapStrValue(value: string, type: string): string {
|
||||
wrapStrValue(columnType: string, value: string): string {
|
||||
return `'${value}'`;
|
||||
}
|
||||
}
|
||||
|
||||
338
mayfly_go_web/src/views/ops/db/dialect/sqlite_dialect.ts
Normal file
338
mayfly_go_web/src/views/ops/db/dialect/sqlite_dialect.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import {
|
||||
commonCustomKeywords,
|
||||
DataType,
|
||||
DbDialect,
|
||||
DialectInfo,
|
||||
EditorCompletion,
|
||||
EditorCompletionItem,
|
||||
IndexDefinition,
|
||||
RowDefinition,
|
||||
sqlColumnType,
|
||||
} from './index';
|
||||
import { DbInst } from '@/views/ops/db/db';
|
||||
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
|
||||
|
||||
export { SqliteDialect };
|
||||
|
||||
// 参考官方文档:https://www.sqlite.org/datatype3.html
|
||||
const SQLITE_TYPE_LIST: sqlColumnType[] = [
|
||||
// INTEGER
|
||||
{ udtName: 'int', dataType: 'int', desc: '', space: '', range: '' },
|
||||
{ udtName: 'integer', dataType: 'integer', desc: '', space: '', range: '' },
|
||||
{ udtName: 'tinyint', dataType: 'tinyint', desc: '', space: '', range: '' },
|
||||
{ udtName: 'smallint', dataType: 'smallint', desc: '', space: '', range: '' },
|
||||
{ udtName: 'mediumint', dataType: 'mediumint', desc: '', space: '', range: '' },
|
||||
{ udtName: 'bigint', dataType: 'bigint', desc: '', space: '', range: '' },
|
||||
{ udtName: 'unsigned big int', dataType: 'unsigned big int', desc: '', space: '', range: '' },
|
||||
{ udtName: 'int2', dataType: 'int2', desc: '', space: '', range: '' },
|
||||
{ udtName: 'int8', dataType: 'int8', desc: '', space: '', range: '' },
|
||||
// TEXT
|
||||
{ udtName: 'character', dataType: 'character', desc: '', space: '', range: '' },
|
||||
{ udtName: 'varchar', dataType: 'varchar', desc: '', space: '', range: '' },
|
||||
{ udtName: 'varying character', dataType: 'varying character', desc: '', space: '', range: '' },
|
||||
{ udtName: 'nchar', dataType: 'nchar', desc: '', space: '', range: '' },
|
||||
{ udtName: 'native character', dataType: 'native character', desc: '', space: '', range: '' },
|
||||
{ udtName: 'nvarchar', dataType: 'nvarchar', desc: '', space: '', range: '' },
|
||||
{ udtName: 'text', dataType: 'text', desc: '', space: '', range: '' },
|
||||
{ udtName: 'clob', dataType: 'clob', desc: '', space: '', range: '' },
|
||||
// blob
|
||||
{ udtName: 'blob', dataType: 'blob', desc: '', space: '', range: '' },
|
||||
{ udtName: 'no datatype specified', dataType: 'no datatype specified', desc: '', space: '', range: '' },
|
||||
// REAL
|
||||
{ udtName: 'real', dataType: 'real', desc: '', space: '', range: '' },
|
||||
{ udtName: 'double', dataType: 'double', desc: '', space: '', range: '' },
|
||||
{ udtName: 'double precision', dataType: 'double precision', desc: '', space: '', range: '' },
|
||||
{ udtName: 'float', dataType: 'float', desc: '', space: '', range: '' },
|
||||
// NUMERIC
|
||||
{ udtName: 'numeric', dataType: 'numeric', desc: '', space: '', range: '' },
|
||||
{ udtName: 'decimal', dataType: 'decimal', desc: '', space: '', range: '' },
|
||||
{ udtName: 'boolean', dataType: 'boolean', desc: '', space: '', range: '' },
|
||||
{ udtName: 'date', dataType: 'date', desc: '', space: '', range: '' },
|
||||
{ udtName: 'datetime', dataType: 'datetime', desc: '', space: '', range: '' },
|
||||
];
|
||||
|
||||
const addCustomKeywords = ['PRAGMA', 'database_list', 'sqlite_master'];
|
||||
|
||||
// 参考官方文档:https://www.sqlite.org/lang_corefunc.html
|
||||
const functions: EditorCompletionItem[] = [
|
||||
// 字符函数
|
||||
{ label: 'abs', insertText: 'abs(X)', description: '返回给定数值的绝对值' },
|
||||
{ label: 'changes', insertText: 'changes()', description: '返回最近增删改影响的行数' },
|
||||
{ label: 'coalesce', insertText: 'coalesce(X,Y,...)', description: '返回第一个不为空的值' },
|
||||
{ label: 'hex', insertText: 'hex(X)', description: '返回给定字符的hex值' },
|
||||
{ label: 'ifnull', insertText: 'ifnull(X,Y)', description: '返回第一个不为空的值' },
|
||||
{ label: 'iif', insertText: 'iif(X,Y,Z)', description: '如果x为真则返回y,否则返回z' },
|
||||
{ label: 'instr', insertText: 'instr(X,Y)', description: '返回字符y在x的第n个位置' },
|
||||
{ label: 'length', insertText: 'length(X)', description: '返回给定字符的长度' },
|
||||
{ label: 'load_extension', insertText: 'load_extension(X[,Y])', description: '加载扩展块' },
|
||||
{ label: 'lower', insertText: 'lower(X)', description: '返回小写字符' },
|
||||
{ label: 'ltrim', insertText: 'ltrim(X[,Y])', description: '左trim' },
|
||||
{ label: 'nullif', insertText: 'nullif(X,Y)', description: '比较两值相等则返回null,否则返回第一个值' },
|
||||
{ label: 'printf', insertText: "printf('%s',...)", description: '字符串格式化拼接,如%s %d' },
|
||||
{ label: 'quote', insertText: 'quote(X)', description: '把字符串用引号包起来' },
|
||||
{ label: 'random', insertText: 'random()', description: '生成随机数' },
|
||||
{ label: 'randomblob', insertText: 'randomblob(N)', description: '生成一个包含N个随机字节的BLOB' },
|
||||
{ label: 'replace', insertText: 'replace(X,Y,Z)', description: '替换字符串' },
|
||||
{ label: 'round', insertText: 'round(X[,Y])', description: '将数值四舍五入到指定的小数位数' },
|
||||
{ label: 'rtrim', insertText: 'rtrim(X[,Y])', description: '右trim' },
|
||||
{ label: 'sign', insertText: 'sign(X)', description: '返回数字符号 1正 -1负 0零 null' },
|
||||
{ label: 'soundex', insertText: 'soundex(X)', description: '返回字符串X的soundex编码字符串' },
|
||||
{ label: 'sqlite_compileoption_get', insertText: 'sqlite_compileoption_get(N)', description: '获取指定编译选项的值' },
|
||||
{ label: 'sqlite_compileoption_used', insertText: 'sqlite_compileoption_used(X)', description: '检查SQLite编译时是否使用了指定的编译选项' },
|
||||
{ label: 'sqlite_source_id', insertText: 'sqlite_source_id()', description: '获取sqlite源代码标识符' },
|
||||
{ label: 'sqlite_version', insertText: 'sqlite_version()', description: '获取sqlite版本' },
|
||||
{ label: 'substr', insertText: 'substr(X,Y[,Z])', description: '截取字符串' },
|
||||
{ label: 'substring', insertText: 'substring(X,Y[,Z])', description: '截取字符串' },
|
||||
{ label: 'trim', insertText: 'trim(X[,Y])', description: '去除给定字符串前后的字符,默认空格' },
|
||||
{ label: 'typeof', insertText: 'typeof(X)', description: '返回X的基本类型:null,integer,real,text,blob' },
|
||||
{ label: 'unicode', insertText: 'unicode(X)', description: '返回与字符串X的第一个字符相对应的数字unicode代码点' },
|
||||
{ label: 'unlikely', insertText: 'unlikely(X)', description: '返回大写字符' },
|
||||
{ label: 'upper', insertText: 'upper(X)', description: '返回由0x00的N个字节组成的BLOB' },
|
||||
{ label: 'zeroblob', insertText: 'zeroblob(N)', description: '返回分组中的平均值' },
|
||||
{ label: 'avg', insertText: 'avg(X)', description: '返回总条数' },
|
||||
{ label: 'count', insertText: 'count(*)', description: '返回分组中用给定非空字符串连接的值' },
|
||||
{ label: 'group_concat', insertText: 'group_concat(X[,Y])', description: '返回分组中最大值' },
|
||||
{ label: 'max', insertText: 'max(X)', description: '返回分组中最小值' },
|
||||
{ label: 'min', insertText: 'min(X)', description: '返回分组中非空值的总和。' },
|
||||
{ label: 'sum', insertText: 'sum(X)', description: '返回分组中非空值的总和。' },
|
||||
{ label: 'total', insertText: 'total(X)', description: '返回YYYY-MM-DD格式的字符串' },
|
||||
{ label: 'date', insertText: 'date(time-value[, modifier, ...])', description: '返回HH:MM:SS格式的字符串' },
|
||||
{ label: 'time', insertText: 'time(time-value[, modifier, ...])', description: '将日期和时间字符串转换为特定的日期和时间格式' },
|
||||
{ label: 'datetime', insertText: 'datetime(time-value[, modifier, ...])', description: '计算日期和时间的儒略日数' },
|
||||
{ label: 'julianday', insertText: 'julianday(time-value[, modifier, ...])', description: '将日期和时间格式化为指定的字符串' },
|
||||
];
|
||||
|
||||
let sqliteDialectInfo: DialectInfo;
|
||||
class SqliteDialect implements DbDialect {
|
||||
getInfo(): DialectInfo {
|
||||
if (sqliteDialectInfo) {
|
||||
return sqliteDialectInfo;
|
||||
}
|
||||
|
||||
let { keywords, operators, builtinVariables } = sqlLanguage;
|
||||
|
||||
let editorCompletions: EditorCompletion = {
|
||||
keywords: keywords
|
||||
.filter((a: string) => addCustomKeywords.indexOf(a) === -1)
|
||||
.map((a: string): EditorCompletionItem => ({ label: a, description: 'keyword' }))
|
||||
.concat(commonCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' })))
|
||||
.concat(addCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' }))),
|
||||
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
|
||||
functions,
|
||||
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
|
||||
};
|
||||
|
||||
sqliteDialectInfo = {
|
||||
name: 'Sqlite',
|
||||
icon: 'iconfont icon-sqlite',
|
||||
defaultPort: 0,
|
||||
formatSqlDialect: 'sql',
|
||||
columnTypes: SQLITE_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
|
||||
editorCompletions,
|
||||
};
|
||||
return sqliteDialectInfo;
|
||||
}
|
||||
|
||||
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
|
||||
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
|
||||
pageNum,
|
||||
limit
|
||||
)};`;
|
||||
}
|
||||
|
||||
getPageSql(pageNum: number, limit: number) {
|
||||
return ` LIMIT ${(pageNum - 1) * limit}, ${limit}`;
|
||||
}
|
||||
|
||||
getDefaultRows(): RowDefinition[] {
|
||||
return [
|
||||
{ name: 'id', type: 'integer', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
|
||||
{ name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
|
||||
{
|
||||
name: 'creator',
|
||||
type: 'varchar',
|
||||
length: '100',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建人姓名',
|
||||
},
|
||||
{
|
||||
name: 'create_time',
|
||||
type: 'datetime',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: 'CURRENT_TIMESTAMP',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建时间',
|
||||
},
|
||||
{ name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
|
||||
{ name: 'updator', type: 'varchar', length: '100', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改姓名' },
|
||||
{
|
||||
name: 'update_time',
|
||||
type: 'datetime',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: 'CURRENT_TIMESTAMP',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '修改时间',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
getDefaultIndex(): IndexDefinition {
|
||||
return {
|
||||
indexName: '',
|
||||
columnNames: [],
|
||||
unique: false,
|
||||
indexType: 'BTREE',
|
||||
indexComment: '',
|
||||
};
|
||||
}
|
||||
|
||||
quoteIdentifier = (name: string) => {
|
||||
return `\"${name}\"`;
|
||||
};
|
||||
|
||||
genColumnBasicSql(cl: any): string {
|
||||
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
|
||||
let defVal = val ? `DEFAULT ${val}` : '';
|
||||
let length = cl.length ? `(${cl.length})` : '';
|
||||
let nullAble = cl.notNull ? 'NOT NULL' : 'NULL';
|
||||
if (cl.pri) {
|
||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} PRIMARY KEY ${cl.auto_increment ? 'AUTOINCREMENT' : ''} ${nullAble} `;
|
||||
}
|
||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${nullAble} ${defVal} `;
|
||||
}
|
||||
getCreateTableSql(data: any): string {
|
||||
// 创建表结构
|
||||
let fields: string[] = [];
|
||||
data.fields.res.forEach((item: any) => {
|
||||
item.name && fields.push(this.genColumnBasicSql(item));
|
||||
});
|
||||
|
||||
return `CREATE TABLE ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(data.tableName)}
|
||||
( ${fields.join(',')} )`;
|
||||
}
|
||||
|
||||
getCreateIndexSql(data: any): string {
|
||||
// 创建索引
|
||||
let sql = [] as string[];
|
||||
data.indexs.res.forEach((a: any) => {
|
||||
sql.push(
|
||||
`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(a.indexName)} ON "${data.tableName}" (${a.columnNames.join(',')})`
|
||||
);
|
||||
});
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||
// sqlite修改表结构需要先删除再创建
|
||||
|
||||
// 1.删除旧表索引 DROP INDEX "main"."aa";
|
||||
let sql = [] as string[];
|
||||
tableData.indexs.res.forEach((a: any) => {
|
||||
a.indexName && sql.push(`DROP INDEX ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(a.indexName)}`);
|
||||
});
|
||||
|
||||
// 2.重命名表,备份旧表 ALTER TABLE "main"."t_sys_resource" RENAME TO "_t_sys_resource_old_20240118162712"; new Date().getTime()
|
||||
let oldTableName = `_${tableName}_old_${new Date().getTime()}`;
|
||||
sql.push(`ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} RENAME TO ${this.quoteIdentifier(oldTableName)}`);
|
||||
|
||||
// 3.创建新表
|
||||
sql.push(this.getCreateTableSql(tableData));
|
||||
|
||||
// 4.复制数据 INSERT INTO "库名"."新表名" (${insertFields}) SELECT ${queryFields} FROM "库名"."旧表名";
|
||||
// 查询的字段数据类型和数量应与插入的字段一致
|
||||
// 判断哪些字段需要查询旧表,哪些字段需要插入新表
|
||||
// 解析changeData,统计需要查询旧表的字段,统计需要插入新表的字段
|
||||
let delFields = changeData.del.map((a) => a.name);
|
||||
let addFields = changeData.add.map((a) => a.name);
|
||||
|
||||
let queryFields = [] as string[];
|
||||
let insertFields = [] as string[];
|
||||
tableData.fields.res.forEach((a: any) => {
|
||||
// 新增、删除的字段不需要查询旧表,不需要插入新表
|
||||
if (addFields.includes(a.name) || delFields.includes(a.name)) {
|
||||
return;
|
||||
}
|
||||
// 修改的字段需要查询和插入,判断是否修改了字段名,如果修改了字段名,需要查询旧表原名,插入新表新名
|
||||
// 其余未删除、未修改的字段,需要查询旧表,插入新表
|
||||
queryFields.push(this.quoteIdentifier(a.name === a.oldName ? a.name : a.oldName));
|
||||
insertFields.push(this.quoteIdentifier(a.name));
|
||||
});
|
||||
// 生成sql
|
||||
sql.push(
|
||||
`INSERT INTO ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} (${insertFields.join(',')}) SELECT ${queryFields.join(
|
||||
','
|
||||
)} FROM ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(oldTableName)}`
|
||||
);
|
||||
|
||||
// 5.创建索引
|
||||
tableData.indexs.res.forEach((a: any) => {
|
||||
a.indexName &&
|
||||
sql.push(
|
||||
`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(a.indexName)} ON "${tableName}" (${a.columnNames.join(',')})`
|
||||
);
|
||||
});
|
||||
|
||||
return sql.join(';') + ';';
|
||||
}
|
||||
|
||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||
// sqlite创建索引需要先删除再创建
|
||||
// CREATE INDEX "main"."aa1" ON "t_sys_resource" ( "ui_path" );
|
||||
|
||||
let sql = [] as string[];
|
||||
|
||||
if (changeData.del.length > 0) {
|
||||
changeData.del.forEach((a) => {
|
||||
sql.push(`DROP INDEX ${this.quoteIdentifier(a.indexName)}`);
|
||||
});
|
||||
}
|
||||
|
||||
let indexData = [] as any[];
|
||||
if (changeData.add.length > 0) {
|
||||
indexData = indexData.concat(changeData.add);
|
||||
}
|
||||
if (changeData.upd.length > 0) {
|
||||
indexData = indexData.concat(changeData.upd);
|
||||
}
|
||||
|
||||
if (indexData.length > 0) {
|
||||
indexData.forEach((a) => {
|
||||
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} ON ${tableName} (${a.columnNames.join(',')})`);
|
||||
});
|
||||
}
|
||||
return sql.join(';');
|
||||
}
|
||||
|
||||
getDataType(columnType: string): DataType {
|
||||
if (DbInst.isNumber(columnType)) {
|
||||
return DataType.Number;
|
||||
}
|
||||
// 日期时间类型
|
||||
if (/datetime|timestamp/gi.test(columnType)) {
|
||||
return DataType.DateTime;
|
||||
}
|
||||
// 日期类型
|
||||
if (/date/gi.test(columnType)) {
|
||||
return DataType.Date;
|
||||
}
|
||||
// 时间类型
|
||||
if (/time/gi.test(columnType)) {
|
||||
return DataType.Time;
|
||||
}
|
||||
return DataType.String;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
wrapStrValue(columnType: string, value: string): string {
|
||||
return `'${value}'`;
|
||||
}
|
||||
}
|
||||
18
mayfly_go_web/src/views/ops/db/dialect/vastbase_dialect.ts
Normal file
18
mayfly_go_web/src/views/ops/db/dialect/vastbase_dialect.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { DialectInfo } from './index';
|
||||
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
|
||||
|
||||
let vastDialectInfo: DialectInfo;
|
||||
|
||||
export class VastbaseDialect extends PostgresqlDialect {
|
||||
getInfo(): DialectInfo {
|
||||
if (vastDialectInfo) {
|
||||
return vastDialectInfo;
|
||||
}
|
||||
|
||||
vastDialectInfo = {} as DialectInfo;
|
||||
Object.assign(vastDialectInfo, super.getInfo());
|
||||
vastDialectInfo.name = 'VastbaseG100';
|
||||
vastDialectInfo.icon = 'iconfont icon-vastbase';
|
||||
return vastDialectInfo;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
tagSelectRef.validate();
|
||||
}
|
||||
"
|
||||
:tag-path="form.tagPath"
|
||||
:resource-code="form.code"
|
||||
:resource-type="TagResourceTypeEnum.Machine.value"
|
||||
style="width: 100%"
|
||||
@@ -153,6 +154,7 @@ const state = reactive({
|
||||
form: {
|
||||
id: null,
|
||||
code: '',
|
||||
tagPath: '',
|
||||
ip: null,
|
||||
port: 22,
|
||||
name: null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="machine-list">
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="machineApi.list"
|
||||
@@ -25,7 +25,7 @@
|
||||
<span v-if="!data.stat">-</span>
|
||||
<div v-else>
|
||||
<el-row>
|
||||
<el-text size="small" style="font-size: 10px">
|
||||
<el-text size="small" class="font11">
|
||||
内存(可用/总):
|
||||
<span :class="getStatsFontClass(data.stat.memAvailable, data.stat.memTotal)"
|
||||
>{{ formatByteSize(data.stat.memAvailable, 1) }}/{{ formatByteSize(data.stat.memTotal, 1) }}
|
||||
@@ -33,7 +33,7 @@
|
||||
</el-text>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-text style="font-size: 10px" size="small">
|
||||
<el-text class="font11" size="small">
|
||||
CPU(空闲): <span :class="getStatsFontClass(data.stat.cpuIdle, 100)">{{ data.stat.cpuIdle.toFixed(0) }}%</span>
|
||||
</el-text>
|
||||
</el-row>
|
||||
@@ -44,7 +44,7 @@
|
||||
<span v-if="!data.stat?.fsInfos">-</span>
|
||||
<div v-else>
|
||||
<el-row v-for="(i, idx) in data.stat.fsInfos.slice(0, 2)" :key="i.mountPoint">
|
||||
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
||||
<el-text class="font11" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
||||
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
|
||||
</el-text>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</template>
|
||||
|
||||
<el-row v-for="i in data.stat.fsInfos.slice(2)" :key="i.mountPoint">
|
||||
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
||||
<el-text class="font11" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
||||
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
|
||||
</el-text>
|
||||
</el-row>
|
||||
@@ -231,8 +231,8 @@ const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Machine.value), Se
|
||||
const columns = [
|
||||
TableColumn.new('name', '名称'),
|
||||
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
|
||||
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(50),
|
||||
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(20),
|
||||
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(55),
|
||||
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(25),
|
||||
TableColumn.new('username', '用户名'),
|
||||
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
||||
@@ -464,10 +464,6 @@ const showRec = (row: any) => {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.el-dialog__body {
|
||||
padding: 2px 2px;
|
||||
}
|
||||
|
||||
.el-dropdown-link-machine-list {
|
||||
cursor: pointer;
|
||||
color: var(--el-color-primary);
|
||||
|
||||
402
mayfly_go_web/src/views/ops/machine/MachineOp.vue
Normal file
402
mayfly_go_web/src/views/ops/machine/MachineOp.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<template>
|
||||
<div class="flex-all-center">
|
||||
<!-- 文档: https://antoniandre.github.io/splitpanes/ -->
|
||||
<Splitpanes class="default-theme" @resized="onResizeTagTree">
|
||||
<Pane size="20" max-size="30">
|
||||
<tag-tree
|
||||
class="machine-terminal-tree"
|
||||
ref="tagTreeRef"
|
||||
:resource-type="TagResourceTypeEnum.Machine.value"
|
||||
:tag-path-node-type="NodeTypeTagPath"
|
||||
>
|
||||
<template #prefix="{ data }">
|
||||
<SvgIcon v-if="data.icon && data.params.status == 1" :name="data.icon.name" :color="data.icon.color" />
|
||||
<SvgIcon v-if="data.icon && data.params.status == -1" :name="data.icon.name" color="var(--el-color-danger)" />
|
||||
</template>
|
||||
|
||||
<template #suffix="{ data }">
|
||||
<span style="color: #c4c9c4; font-size: 9px" v-if="data.type.value == MachineNodeType.Machine">{{
|
||||
` ${data.params.username}@${data.params.ip}:${data.params.port}`
|
||||
}}</span>
|
||||
</template>
|
||||
</tag-tree>
|
||||
</Pane>
|
||||
|
||||
<Pane>
|
||||
<div class="machine-terminal-tabs card pd5">
|
||||
<el-tabs
|
||||
v-if="state.tabs.size > 0"
|
||||
type="card"
|
||||
@tab-remove="onRemoveTab"
|
||||
@tab-change="onTabChange"
|
||||
style="width: 100%"
|
||||
v-model="state.activeTermName"
|
||||
class="h100"
|
||||
>
|
||||
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<template #label>
|
||||
<el-popconfirm @confirm="handleReconnect(dt.key)" title="确认重新连接?">
|
||||
<template #reference>
|
||||
<el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'"
|
||||
><Connection />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||
<template #reference>
|
||||
<div>
|
||||
<span class="machine-terminal-tab-label">{{ dt.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions :column="1" size="small">
|
||||
<el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item>
|
||||
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
|
||||
<el-descriptions-item label="username"> {{ dt.params?.username }} </el-descriptions-item>
|
||||
<el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<div class="terminal-wrapper" :style="{ height: `calc(100vh - 155px)` }">
|
||||
<TerminalBody
|
||||
@status-change="terminalStatusChange(dt.key, $event)"
|
||||
:ref="(el) => setTerminalRef(el, dt.key)"
|
||||
:socket-url="dt.socketUrl"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="infoDialog.visible">
|
||||
<el-descriptions title="详情" :column="3" border>
|
||||
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="认证方式">
|
||||
{{ infoDialog.data.authCertId > 1 ? '授权凭证' : '密码' }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
|
||||
|
||||
<script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
|
||||
|
||||
<file-conf-list :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
|
||||
|
||||
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
|
||||
|
||||
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title" />
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, defineAsyncComponent } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { machineApi, getMachineTerminalSocketUrl } from './api';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { NodeType, TagTreeNode } from '../component/tag';
|
||||
import TagTree from '../component/TagTree.vue';
|
||||
import { Splitpanes, Pane } from 'splitpanes';
|
||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
||||
// 组件
|
||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
|
||||
const FileConfList = defineAsyncComponent(() => import('./file/FileConfList.vue'));
|
||||
const MachineStats = defineAsyncComponent(() => import('./MachineStats.vue'));
|
||||
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
|
||||
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
|
||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||
import { TerminalStatus } from '@/components/terminal/common';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const perms = {
|
||||
addMachine: 'machine:add',
|
||||
updateMachine: 'machine:update',
|
||||
delMachine: 'machine:del',
|
||||
terminal: 'machine:terminal',
|
||||
closeCli: 'machine:close-cli',
|
||||
};
|
||||
|
||||
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
|
||||
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
||||
|
||||
class MachineNodeType {
|
||||
static Machine = 1;
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
ip: null,
|
||||
name: null,
|
||||
tagPath: '',
|
||||
},
|
||||
infoDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
serviceDialog: {
|
||||
visible: false,
|
||||
machineId: 0,
|
||||
title: '',
|
||||
},
|
||||
processDialog: {
|
||||
visible: false,
|
||||
machineId: 0,
|
||||
},
|
||||
fileDialog: {
|
||||
visible: false,
|
||||
machineId: 0,
|
||||
title: '',
|
||||
},
|
||||
machineStatsDialog: {
|
||||
visible: false,
|
||||
stats: null,
|
||||
title: '',
|
||||
machineId: 0,
|
||||
},
|
||||
machineRecDialog: {
|
||||
visible: false,
|
||||
machineId: 0,
|
||||
title: '',
|
||||
},
|
||||
activeTermName: '',
|
||||
tabs: new Map<string, any>(),
|
||||
});
|
||||
|
||||
const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineRecDialog } = toRefs(state);
|
||||
|
||||
const tagTreeRef: any = ref(null);
|
||||
|
||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: any) => {
|
||||
// 加载标签树下的机器列表
|
||||
state.params.tagPath = node.key;
|
||||
state.params.pageNum = 1;
|
||||
state.params.pageSize = 1000;
|
||||
const res = await search();
|
||||
// 把list 根据name字段排序
|
||||
res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
return res.list.map((x: any) =>
|
||||
new TagTreeNode(x.id, x.name, NodeTypeMachine(x))
|
||||
.withParams(x)
|
||||
.withDisabled(x.status == -1)
|
||||
.withIcon({
|
||||
name: 'Monitor',
|
||||
color: '#409eff',
|
||||
})
|
||||
.withIsLeaf(true)
|
||||
);
|
||||
});
|
||||
|
||||
let openIds = {};
|
||||
|
||||
const NodeTypeMachine = (machine: any) => {
|
||||
let contextMenuItems = [];
|
||||
contextMenuItems.push(new ContextmenuItem('term', '打开终端').withIcon('Monitor').withOnClick(() => openTerminal(machine)));
|
||||
contextMenuItems.push(new ContextmenuItem('term-ex', '打开终端(新窗口)').withIcon('Monitor').withOnClick(() => openTerminal(machine, true)));
|
||||
contextMenuItems.push(new ContextmenuItem('detail', '详情').withIcon('More').withOnClick(() => showInfo(machine)));
|
||||
contextMenuItems.push(new ContextmenuItem('status', '状态').withIcon('Compass').withOnClick(() => showMachineStats(machine)));
|
||||
contextMenuItems.push(new ContextmenuItem('process', '进程').withIcon('DataLine').withOnClick(() => showProcess(machine)));
|
||||
|
||||
if (actionBtns[perms.updateMachine] && machine.enableRecorder == 1) {
|
||||
contextMenuItems.push(new ContextmenuItem('edit', '终端回放').withIcon('Compass').withOnClick(() => showRec(machine)));
|
||||
}
|
||||
|
||||
contextMenuItems.push(new ContextmenuItem('files', '文件管理').withIcon('FolderOpened').withOnClick(() => showFileManage(machine)));
|
||||
contextMenuItems.push(new ContextmenuItem('scripts', '脚本管理').withIcon('Files').withOnClick(() => serviceManager(machine)));
|
||||
return new NodeType(MachineNodeType.Machine).withContextMenuItems(contextMenuItems).withNodeDblclickFunc(() => {
|
||||
// for (let k of state.tabs.keys()) {
|
||||
// // 存在该机器相关的终端tab,则直接激活该tab
|
||||
// if (k.startsWith(`${machine.id}_${machine.username}_`)) {
|
||||
// state.activeTermName = k;
|
||||
// onTabChange();
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
openTerminal(machine);
|
||||
});
|
||||
};
|
||||
|
||||
const openTerminal = (machine: any, ex?: boolean) => {
|
||||
// 新窗口打开
|
||||
if (ex) {
|
||||
const { href } = router.resolve({
|
||||
path: `/machine/terminal`,
|
||||
query: {
|
||||
id: machine.id,
|
||||
name: machine.name,
|
||||
},
|
||||
});
|
||||
window.open(href, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
let { name, id, username } = machine;
|
||||
|
||||
// 同一个机器的终端打开多次,key后添加下划线和数字区分
|
||||
openIds[id] = openIds[id] ? ++openIds[id] : 1;
|
||||
let sameIndex = openIds[id];
|
||||
|
||||
let key = `${id}_${username}_${sameIndex}`;
|
||||
// 只保留name的10个字,超出部分只保留前后4个字符,中间用省略号代替
|
||||
let label = name.length > 10 ? name.slice(0, 4) + '...' + name.slice(-4) : name;
|
||||
|
||||
state.tabs.set(key, {
|
||||
key,
|
||||
label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
|
||||
params: machine,
|
||||
socketUrl: getMachineTerminalSocketUrl(id),
|
||||
});
|
||||
state.activeTermName = key;
|
||||
fitTerminal();
|
||||
};
|
||||
|
||||
const serviceManager = (row: any) => {
|
||||
state.serviceDialog.machineId = row.id;
|
||||
state.serviceDialog.visible = true;
|
||||
state.serviceDialog.title = `${row.name} => ${row.ip}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 显示机器状态统计信息
|
||||
*/
|
||||
const showMachineStats = async (machine: any) => {
|
||||
state.machineStatsDialog.machineId = machine.id;
|
||||
state.machineStatsDialog.title = `机器状态: ${machine.name} => ${machine.ip}`;
|
||||
state.machineStatsDialog.visible = true;
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
const res = await machineApi.list.request(state.params);
|
||||
return res;
|
||||
};
|
||||
|
||||
const showFileManage = (selectionData: any) => {
|
||||
state.fileDialog.visible = true;
|
||||
state.fileDialog.machineId = selectionData.id;
|
||||
state.fileDialog.title = `${selectionData.name} => ${selectionData.ip}`;
|
||||
};
|
||||
|
||||
const showInfo = (info: any) => {
|
||||
state.infoDialog.data = info;
|
||||
state.infoDialog.visible = true;
|
||||
};
|
||||
|
||||
const showProcess = (row: any) => {
|
||||
state.processDialog.machineId = row.id;
|
||||
state.processDialog.visible = true;
|
||||
};
|
||||
|
||||
const showRec = (row: any) => {
|
||||
state.machineRecDialog.title = `${row.name}[${row.ip}]-终端回放记录`;
|
||||
state.machineRecDialog.machineId = row.id;
|
||||
state.machineRecDialog.visible = true;
|
||||
};
|
||||
|
||||
const onRemoveTab = (targetName: string) => {
|
||||
let activeTermName = state.activeTermName;
|
||||
const tabNames = [...state.tabs.keys()];
|
||||
for (let i = 0; i < tabNames.length; i++) {
|
||||
const tabName = tabNames[i];
|
||||
if (tabName !== targetName) {
|
||||
continue;
|
||||
}
|
||||
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
||||
if (nextTab) {
|
||||
activeTermName = nextTab;
|
||||
} else {
|
||||
activeTermName = '';
|
||||
}
|
||||
|
||||
let info = state.tabs.get(targetName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.close();
|
||||
}
|
||||
|
||||
state.tabs.delete(targetName);
|
||||
state.activeTermName = activeTermName;
|
||||
onTabChange();
|
||||
}
|
||||
};
|
||||
|
||||
const terminalStatusChange = (key: string, status: TerminalStatus) => {
|
||||
state.tabs.get(key).status = status;
|
||||
};
|
||||
|
||||
const terminalRefs: any = {};
|
||||
const setTerminalRef = (el: any, key: any) => {
|
||||
if (key) {
|
||||
terminalRefs[key] = el;
|
||||
}
|
||||
};
|
||||
|
||||
const onResizeTagTree = () => {
|
||||
fitTerminal();
|
||||
};
|
||||
|
||||
const onTabChange = () => {
|
||||
fitTerminal();
|
||||
};
|
||||
|
||||
const fitTerminal = () => {
|
||||
setTimeout(() => {
|
||||
let info = state.tabs.get(state.activeTermName);
|
||||
if (info) {
|
||||
terminalRefs[info.key]?.resize();
|
||||
terminalRefs[info.key]?.focus();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleReconnect = (key: string) => {
|
||||
terminalRefs[key].init();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.machine-terminal-tabs {
|
||||
height: calc(100vh - 108px);
|
||||
--el-tabs-header-height: 30px;
|
||||
|
||||
.el-tabs {
|
||||
--el-tabs-header-height: 30px;
|
||||
}
|
||||
|
||||
.machine-terminal-tab-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
.el-tabs__header {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.el-tabs__item {
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -41,7 +41,15 @@
|
||||
<el-input v-model="state.keySeparator" placeholder="分割符" size="small" class="ml5" />
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-input @clear="clear" v-model="scanParam.match" placeholder="match 支持*模糊key" clearable size="small" class="ml10" />
|
||||
<el-input
|
||||
@clear="clear"
|
||||
v-model="scanParam.match"
|
||||
@keyup.enter.native="searchKey()"
|
||||
placeholder="match 支持*模糊key, 回车搜索"
|
||||
clearable
|
||||
size="small"
|
||||
class="ml10"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button
|
||||
|
||||
@@ -107,10 +107,10 @@ defineExpose({ getContent });
|
||||
|
||||
.format-viewer-container .el-textarea textarea {
|
||||
font-size: 14px;
|
||||
height: calc(100vh - 546px + v-bind(height));
|
||||
height: calc(100vh - 550px + v-bind(height));
|
||||
}
|
||||
|
||||
.format-viewer-container .monaco-editor-content {
|
||||
height: calc(100vh - 560px + v-bind(height)) !important;
|
||||
height: calc(100vh - 565px + v-bind(height)) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
|
||||
<el-table size="small" border :data="hashValues" height="450" min-height="300" stripe>
|
||||
<el-table size="small" border :data="hashValues" height="500" min-height="300" stripe>
|
||||
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> </el-table-column>
|
||||
<el-table-column resizable sortable prop="field" label="field" show-overflow-tooltip min-width="100"> </el-table-column>
|
||||
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200"> </el-table-column>
|
||||
@@ -11,7 +11,7 @@
|
||||
class="key-detail-filter-value"
|
||||
v-model="state.filterValue"
|
||||
@keyup.enter="hscan(true, true)"
|
||||
placeholder="输入关键词回车搜索"
|
||||
placeholder="关键词回车搜索"
|
||||
clearable
|
||||
size="small"
|
||||
/>
|
||||
@@ -51,7 +51,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, reactive, watch, toRefs } from 'vue';
|
||||
import { ref, onMounted, reactive, toRefs } from 'vue';
|
||||
import { redisApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { notBlank } from '@/common/assert';
|
||||
|
||||
@@ -142,7 +142,7 @@ const search = async () => {
|
||||
|
||||
const changeStatus = async (row: any) => {
|
||||
let id = row.id;
|
||||
let status = row.status == -1 ? 1 : -1;
|
||||
let status = row.status == AccountStatusEnum.Disable.value ? AccountStatusEnum.Enable.value : AccountStatusEnum.Disable.value;
|
||||
await accountApi.changeStatus.request({
|
||||
id,
|
||||
status,
|
||||
|
||||
@@ -2,6 +2,7 @@ import vue from '@vitejs/plugin-vue';
|
||||
import { resolve } from 'path';
|
||||
import type { UserConfig } from 'vite';
|
||||
import { loadEnv } from './src/common/utils/viteBuild';
|
||||
import { CodeInspectorPlugin } from 'code-inspector-plugin';
|
||||
|
||||
const pathResolve = (dir: string): any => {
|
||||
return resolve(__dirname, '.', dir);
|
||||
@@ -14,7 +15,12 @@ const alias: Record<string, string> = {
|
||||
};
|
||||
|
||||
const viteConfig: UserConfig = {
|
||||
plugins: [vue()],
|
||||
plugins: [
|
||||
vue(),
|
||||
CodeInspectorPlugin({
|
||||
bundler: 'vite',
|
||||
}),
|
||||
],
|
||||
root: process.cwd(),
|
||||
resolve: {
|
||||
alias,
|
||||
|
||||
@@ -37,22 +37,19 @@ sqlite:
|
||||
# password: 111049
|
||||
# db: 0
|
||||
log:
|
||||
# 日志等级, debug, info, warn, error
|
||||
# 日志等级, debug, info, warn, error
|
||||
level: info
|
||||
# 日志格式类型, text/json
|
||||
type: text
|
||||
# 是否记录方法调用栈信息
|
||||
add-source: false
|
||||
# 日志文件配置
|
||||
# file:
|
||||
# path: ./
|
||||
# path: ./log
|
||||
# name: mayfly-go.log
|
||||
db:
|
||||
backup-path: ./backup
|
||||
mysqlutil-path:
|
||||
mysql: ./mysqlutil/bin/mysql
|
||||
mysqldump: ./mysqlutil/bin/mysqldump
|
||||
mysqlbinlog: ./mysqlutil/bin/mysqlbinlog
|
||||
mariadbutil-path:
|
||||
mysql: ./mariadbutil/bin/mariadb
|
||||
mysqldump: ./mariadbutil/bin/mariadb-dump
|
||||
mysqlbinlog: ./mariadbutil/bin/mariadb-binlog
|
||||
# # 日志文件的最大大小(以兆字节为单位)。当日志文件大小达到该值时,将触发切割操作
|
||||
# max-size: 500
|
||||
# # 根据文件名中的时间戳,设置保留旧日志文件的最大天数
|
||||
# max-age: 60
|
||||
# # 是否使用 gzip 压缩方式压缩轮转后的日志文件
|
||||
# compress: true
|
||||
|
||||
@@ -10,31 +10,34 @@ require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/glebarez/sqlite v1.10.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.5
|
||||
github.com/go-ldap/ldap/v3 v3.4.6
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.14.0
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231018071450-ac8d9f0167e9
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230712084735-068dc2aee82d
|
||||
github.com/microsoft/go-mssqldb v1.6.0
|
||||
github.com/mojocn/base64Captcha v1.3.6 // 验证码
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/redis/go-redis/v9 v9.4.0
|
||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||
github.com/sijms/go-ora/v2 v2.8.5
|
||||
github.com/sijms/go-ora/v2 v2.8.7
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.mongodb.org/mongo-driver v1.13.1 // mongo
|
||||
golang.org/x/crypto v0.18.0 // ssh
|
||||
golang.org/x/oauth2 v0.15.0
|
||||
golang.org/x/sync v0.6.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
// gorm
|
||||
gorm.io/driver/mysql v1.5.2
|
||||
gorm.io/gorm v1.25.5
|
||||
gorm.io/gorm v1.25.6
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -49,8 +52,10 @@ require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
@@ -79,10 +84,9 @@ require (
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230519143937-03e91628a987
|
||||
golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect
|
||||
golang.org/x/image v0.13.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
|
||||
@@ -1,11 +1,45 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
dbInit "mayfly-go/internal/db/init"
|
||||
machineInit "mayfly-go/internal/machine/init"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ioc"
|
||||
)
|
||||
|
||||
func InitOther() {
|
||||
machineInit.Init()
|
||||
dbInit.Init()
|
||||
// 初始化ioc函数
|
||||
type InitIocFunc func()
|
||||
|
||||
// 初始化函数
|
||||
type InitFunc func()
|
||||
|
||||
var (
|
||||
initIocFuncs = make([]InitIocFunc, 0)
|
||||
initFuncs = make([]InitFunc, 0)
|
||||
)
|
||||
|
||||
// 添加初始化ioc函数,由各个默认自行添加
|
||||
func AddInitIocFunc(initIocFunc InitIocFunc) {
|
||||
initIocFuncs = append(initIocFuncs, initIocFunc)
|
||||
}
|
||||
|
||||
// 添加初始化函数,由各个默认自行添加
|
||||
func AddInitFunc(initFunc InitFunc) {
|
||||
initFuncs = append(initFuncs, initFunc)
|
||||
}
|
||||
|
||||
// 系统启动时,调用各个模块的初始化函数
|
||||
func InitOther() {
|
||||
// 调用各个默认ioc组件注册初始化,优先调用ioc初始化注册函数和注入函数(可能在后续的InitFunc中需要用到依赖实例)
|
||||
for _, initIocFunc := range initIocFuncs {
|
||||
initIocFunc()
|
||||
}
|
||||
initIocFuncs = nil
|
||||
|
||||
// 为所有注册的实例注入其依赖的其他组件实例
|
||||
biz.ErrIsNil(ioc.InjectComponents())
|
||||
|
||||
// 调用各个默认的初始化函数
|
||||
for _, initFunc := range initFuncs {
|
||||
go initFunc()
|
||||
}
|
||||
initFuncs = nil
|
||||
}
|
||||
|
||||
@@ -3,15 +3,6 @@ package initialize
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
auth_router "mayfly-go/internal/auth/router"
|
||||
common_router "mayfly-go/internal/common/router"
|
||||
db_router "mayfly-go/internal/db/router"
|
||||
machine_router "mayfly-go/internal/machine/router"
|
||||
mongo_router "mayfly-go/internal/mongo/router"
|
||||
msg_router "mayfly-go/internal/msg/router"
|
||||
redis_router "mayfly-go/internal/redis/router"
|
||||
sys_router "mayfly-go/internal/sys/router"
|
||||
tag_router "mayfly-go/internal/tag/router"
|
||||
"mayfly-go/pkg/config"
|
||||
"mayfly-go/pkg/middleware"
|
||||
"mayfly-go/static"
|
||||
@@ -20,6 +11,18 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 初始化路由函数
|
||||
type InitRouterFunc func(router *gin.RouterGroup)
|
||||
|
||||
var (
|
||||
initRouterFuncs = make([]InitRouterFunc, 0)
|
||||
)
|
||||
|
||||
// 添加初始化路由函数,由各个默认自行添加
|
||||
func AddInitRouterFunc(initRouterFunc InitRouterFunc) {
|
||||
initRouterFuncs = append(initRouterFuncs, initRouterFunc)
|
||||
}
|
||||
|
||||
func InitRouter() *gin.Engine {
|
||||
// server配置
|
||||
serverConfig := config.Conf.Server
|
||||
@@ -43,20 +46,11 @@ func InitRouter() *gin.Engine {
|
||||
|
||||
// 设置路由组
|
||||
api := router.Group(serverConfig.ContextPath + "/api")
|
||||
{
|
||||
common_router.Init(api)
|
||||
|
||||
auth_router.Init(api)
|
||||
|
||||
sys_router.Init(api)
|
||||
msg_router.Init(api)
|
||||
|
||||
tag_router.Init(api)
|
||||
machine_router.Init(api)
|
||||
db_router.Init(api)
|
||||
redis_router.Init(api)
|
||||
mongo_router.Init(api)
|
||||
// 调用所有模块注册的初始化路由函数
|
||||
for _, initRouterFunc := range initRouterFuncs {
|
||||
initRouterFunc(api)
|
||||
}
|
||||
initRouterFuncs = nil
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
"mayfly-go/pkg/req"
|
||||
)
|
||||
|
||||
func InitSaveLogFunc() req.SaveLogFunc {
|
||||
return sysapp.GetSyslogApp().SaveFromReq
|
||||
}
|
||||
@@ -1,25 +1,20 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
dbApp "mayfly-go/internal/db/application"
|
||||
// 系统进程退出终止函数
|
||||
type TerminateFunc func()
|
||||
|
||||
var (
|
||||
terminateFuncs = make([]TerminateFunc, 0)
|
||||
)
|
||||
|
||||
// 终止服务后的一些操作
|
||||
func Terminate() {
|
||||
closeDbTasks()
|
||||
// 添加系统退出终止时执行的函数,由各个默认自行添加
|
||||
func AddTerminateFunc(terminateFunc TerminateFunc) {
|
||||
terminateFuncs = append(terminateFuncs, terminateFunc)
|
||||
}
|
||||
|
||||
func closeDbTasks() {
|
||||
restoreApp := dbApp.GetDbRestoreApp()
|
||||
if restoreApp != nil {
|
||||
restoreApp.Close()
|
||||
}
|
||||
binlogApp := dbApp.GetDbBinlogApp()
|
||||
if binlogApp != nil {
|
||||
binlogApp.Close()
|
||||
}
|
||||
backupApp := dbApp.GetDbBackupApp()
|
||||
if backupApp != nil {
|
||||
backupApp.Close()
|
||||
// 终止进程服务后的一些操作
|
||||
func Terminate() {
|
||||
for _, terminateFunc := range terminateFuncs {
|
||||
terminateFunc()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ import (
|
||||
)
|
||||
|
||||
type AccountLogin struct {
|
||||
AccountApp sysapp.Account
|
||||
MsgApp msgapp.Msg
|
||||
AccountApp sysapp.Account `inject:""`
|
||||
MsgApp msgapp.Msg `inject:""`
|
||||
}
|
||||
|
||||
/** 用户账号密码登录 **/
|
||||
|
||||
@@ -28,8 +28,8 @@ import (
|
||||
)
|
||||
|
||||
type LdapLogin struct {
|
||||
AccountApp sysapp.Account
|
||||
MsgApp msgapp.Msg
|
||||
AccountApp sysapp.Account `inject:""`
|
||||
MsgApp msgapp.Msg `inject:""`
|
||||
}
|
||||
|
||||
// @router /auth/ldap/enabled [get]
|
||||
|
||||
@@ -28,9 +28,9 @@ import (
|
||||
)
|
||||
|
||||
type Oauth2Login struct {
|
||||
Oauth2App application.Oauth2
|
||||
AccountApp sysapp.Account
|
||||
MsgApp msgapp.Msg
|
||||
Oauth2App application.Oauth2 `inject:""`
|
||||
AccountApp sysapp.Account `inject:""`
|
||||
MsgApp msgapp.Msg `inject:""`
|
||||
}
|
||||
|
||||
func (a *Oauth2Login) OAuth2Login(rc *req.Ctx) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package application
|
||||
|
||||
import "mayfly-go/internal/auth/infrastructure/persistence"
|
||||
|
||||
var (
|
||||
authApp = newAuthApp(persistence.GetOauthAccountRepo())
|
||||
import (
|
||||
"mayfly-go/internal/auth/infrastructure/persistence"
|
||||
"mayfly-go/pkg/ioc"
|
||||
)
|
||||
|
||||
func GetAuthApp() Oauth2 {
|
||||
return authApp
|
||||
func InitIoc() {
|
||||
persistence.Init()
|
||||
|
||||
ioc.Register(new(oauth2AppImpl), ioc.WithComponentName("Oauth2App"))
|
||||
}
|
||||
|
||||
@@ -14,27 +14,21 @@ type Oauth2 interface {
|
||||
Unbind(accountId uint64)
|
||||
}
|
||||
|
||||
func newAuthApp(oauthAccountRepo repository.Oauth2Account) Oauth2 {
|
||||
return &oauth2AppImpl{
|
||||
oauthAccountRepo: oauthAccountRepo,
|
||||
}
|
||||
}
|
||||
|
||||
type oauth2AppImpl struct {
|
||||
oauthAccountRepo repository.Oauth2Account
|
||||
Oauth2AccountRepo repository.Oauth2Account `inject:""`
|
||||
}
|
||||
|
||||
func (a *oauth2AppImpl) GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error {
|
||||
return a.oauthAccountRepo.GetBy(condition, cols...)
|
||||
return a.Oauth2AccountRepo.GetBy(condition, cols...)
|
||||
}
|
||||
|
||||
func (a *oauth2AppImpl) BindOAuthAccount(e *entity.Oauth2Account) error {
|
||||
if e.Id == 0 {
|
||||
return a.oauthAccountRepo.Insert(context.Background(), e)
|
||||
return a.Oauth2AccountRepo.Insert(context.Background(), e)
|
||||
}
|
||||
return a.oauthAccountRepo.UpdateById(context.Background(), e)
|
||||
return a.Oauth2AccountRepo.UpdateById(context.Background(), e)
|
||||
}
|
||||
|
||||
func (a *oauth2AppImpl) Unbind(accountId uint64) {
|
||||
a.oauthAccountRepo.DeleteByCond(context.Background(), &entity.Oauth2Account{AccountId: accountId})
|
||||
a.Oauth2AccountRepo.DeleteByCond(context.Background(), &entity.Oauth2Account{AccountId: accountId})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
"mayfly-go/pkg/utils/conv"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
)
|
||||
|
||||
@@ -26,8 +27,8 @@ func GetAccountLoginSecurity() *AccountLoginSecurity {
|
||||
als := new(AccountLoginSecurity)
|
||||
als.UseCaptcha = c.ConvBool(jm["useCaptcha"], true)
|
||||
als.UseOtp = c.ConvBool(jm["useOtp"], false)
|
||||
als.LoginFailCount = stringx.ConvInt(jm["loginFailCount"], 5)
|
||||
als.LoginFailMin = stringx.ConvInt(jm["loginFailMin"], 10)
|
||||
als.LoginFailCount = conv.Str2Int(jm["loginFailCount"], 5)
|
||||
als.LoginFailMin = conv.Str2Int(jm["loginFailMin"], 10)
|
||||
otpIssuer := jm["otpIssuer"]
|
||||
if otpIssuer == "" {
|
||||
otpIssuer = "mayfly-go"
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package persistence
|
||||
|
||||
import "mayfly-go/internal/auth/domain/repository"
|
||||
|
||||
var (
|
||||
authAccountRepo = newAuthAccountRepo()
|
||||
import (
|
||||
"mayfly-go/pkg/ioc"
|
||||
)
|
||||
|
||||
func GetOauthAccountRepo() repository.Oauth2Account {
|
||||
return authAccountRepo
|
||||
func Init() {
|
||||
ioc.Register(newAuthAccountRepo(), ioc.WithComponentName("Oauth2AccountRepo"))
|
||||
}
|
||||
|
||||
12
server/internal/auth/init/init.go
Normal file
12
server/internal/auth/init/init.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"mayfly-go/initialize"
|
||||
"mayfly-go/internal/auth/application"
|
||||
"mayfly-go/internal/auth/router"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initialize.AddInitIocFunc(application.InitIoc)
|
||||
initialize.AddInitRouterFunc(router.Init)
|
||||
}
|
||||
@@ -2,30 +2,22 @@ package router
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/auth/api"
|
||||
"mayfly-go/internal/auth/application"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ioc"
|
||||
"mayfly-go/pkg/req"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Init(router *gin.RouterGroup) {
|
||||
accountLogin := &api.AccountLogin{
|
||||
AccountApp: sysapp.GetAccountApp(),
|
||||
MsgApp: msgapp.GetMsgApp(),
|
||||
}
|
||||
accountLogin := new(api.AccountLogin)
|
||||
biz.ErrIsNil(ioc.Inject(accountLogin))
|
||||
|
||||
ldapLogin := &api.LdapLogin{
|
||||
AccountApp: sysapp.GetAccountApp(),
|
||||
MsgApp: msgapp.GetMsgApp(),
|
||||
}
|
||||
ldapLogin := new(api.LdapLogin)
|
||||
biz.ErrIsNil(ioc.Inject(ldapLogin))
|
||||
|
||||
oauth2Login := &api.Oauth2Login{
|
||||
Oauth2App: application.GetAuthApp(),
|
||||
AccountApp: sysapp.GetAccountApp(),
|
||||
MsgApp: msgapp.GetMsgApp(),
|
||||
}
|
||||
oauth2Login := new(api.Oauth2Login)
|
||||
biz.ErrIsNil(ioc.Inject(oauth2Login))
|
||||
|
||||
rg := router.Group("/auth")
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/common/consts"
|
||||
dbapp "mayfly-go/internal/db/application"
|
||||
machineapp "mayfly-go/internal/machine/application"
|
||||
mongoapp "mayfly-go/internal/mongo/application"
|
||||
redisapp "mayfly-go/internal/redis/application"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
)
|
||||
|
||||
type Index struct {
|
||||
TagApp tagapp.TagTree
|
||||
MachineApp machineapp.Machine
|
||||
DbApp dbapp.Db
|
||||
RedisApp redisapp.Redis
|
||||
MongoApp mongoapp.Mongo
|
||||
}
|
||||
|
||||
func (i *Index) Count(rc *req.Ctx) {
|
||||
accountId := rc.GetLoginAccount().Id
|
||||
|
||||
mongoNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMongo, ""))
|
||||
machienNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMachine, ""))
|
||||
dbNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeDb, ""))
|
||||
redisNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeRedis, ""))
|
||||
|
||||
rc.ResData = collx.M{
|
||||
"mongoNum": mongoNum,
|
||||
"machineNum": machienNum,
|
||||
"dbNum": dbNum,
|
||||
"redisNum": redisNum,
|
||||
}
|
||||
}
|
||||
10
server/internal/common/init/init.go
Normal file
10
server/internal/common/init/init.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"mayfly-go/initialize"
|
||||
"mayfly-go/internal/common/router"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initialize.AddInitRouterFunc(router.Init)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/common/api"
|
||||
dbapp "mayfly-go/internal/db/application"
|
||||
machineapp "mayfly-go/internal/machine/application"
|
||||
mongoapp "mayfly-go/internal/mongo/application"
|
||||
redisapp "mayfly-go/internal/redis/application"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/req"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func InitIndexRouter(router *gin.RouterGroup) {
|
||||
index := router.Group("common/index")
|
||||
i := &api.Index{
|
||||
TagApp: tagapp.GetTagTreeApp(),
|
||||
MachineApp: machineapp.GetMachineApp(),
|
||||
DbApp: dbapp.GetDbApp(),
|
||||
RedisApp: redisapp.GetRedisApp(),
|
||||
MongoApp: mongoapp.GetMongoApp(),
|
||||
}
|
||||
{
|
||||
// 首页基本信息统计
|
||||
req.NewGet("count", i.Count).Group(index)
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,4 @@ import "github.com/gin-gonic/gin"
|
||||
|
||||
func Init(router *gin.RouterGroup) {
|
||||
InitCommonRouter(router)
|
||||
InitIndexRouter(router)
|
||||
}
|
||||
|
||||
23
server/internal/db/api/dashbord.go
Normal file
23
server/internal/db/api/dashbord.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/internal/db/application"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
)
|
||||
|
||||
type Dashbord struct {
|
||||
TagTreeApp tagapp.TagTree `inject:""`
|
||||
DbApp application.Db `inject:""`
|
||||
}
|
||||
|
||||
func (m *Dashbord) Dashbord(rc *req.Ctx) {
|
||||
accountId := rc.GetLoginAccount().Id
|
||||
dbNum := len(m.TagTreeApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeDb, ""))
|
||||
|
||||
rc.ResData = collx.M{
|
||||
"dbNum": dbNum,
|
||||
}
|
||||
}
|
||||
@@ -32,11 +32,11 @@ import (
|
||||
)
|
||||
|
||||
type Db struct {
|
||||
InstanceApp application.Instance
|
||||
DbApp application.Db
|
||||
DbSqlExecApp application.DbSqlExec
|
||||
MsgApp msgapp.Msg
|
||||
TagApp tagapp.TagTree
|
||||
InstanceApp application.Instance `inject:"DbInstanceApp"`
|
||||
DbApp application.Db `inject:""`
|
||||
DbSqlExecApp application.DbSqlExec `inject:""`
|
||||
MsgApp msgapp.Msg `inject:""`
|
||||
TagApp tagapp.TagTree `inject:"TagTreeApp"`
|
||||
}
|
||||
|
||||
// @router /api/dbs [get]
|
||||
@@ -78,8 +78,6 @@ func (d *Db) DeleteDb(rc *req.Ctx) {
|
||||
d.DbApp.Delete(ctx, dbId)
|
||||
// 删除该库的sql执行记录
|
||||
d.DbSqlExecApp.DeleteBy(ctx, &entity.DbSqlExec{DbId: dbId})
|
||||
|
||||
// todo delete restore task and histories
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +353,7 @@ func (d *Db) dumpDb(writer *gzipWriter, dbId uint64, dbName string, tables []str
|
||||
writer.WriteString("BEGIN;\n")
|
||||
}
|
||||
insertSql := "INSERT INTO %s VALUES (%s);\n"
|
||||
dbMeta.WalkTableRecord(table, func(record map[string]any, columns []*dbi.QueryColumn) error {
|
||||
dbConn.WalkTableRows(context.TODO(), table, func(record map[string]any, columns []*dbi.QueryColumn) error {
|
||||
var values []string
|
||||
writer.TryFlush()
|
||||
for _, column := range columns {
|
||||
@@ -462,6 +460,20 @@ func (d *Db) GetSchemas(rc *req.Ctx) {
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (d *Db) CopyTable(rc *req.Ctx) {
|
||||
form := &form.DbCopyTableForm{}
|
||||
copy := ginx.BindJsonAndCopyTo[*dbi.DbCopyTable](rc.GinCtx, form, new(dbi.DbCopyTable))
|
||||
|
||||
conn, err := d.DbApp.GetDbConn(form.Id, form.Db)
|
||||
biz.ErrIsNilAppendErr(err, "拷贝表失败: %s")
|
||||
|
||||
err = conn.GetDialect().CopyTable(copy)
|
||||
if err != nil {
|
||||
logx.Errorf("拷贝表失败: %s", err.Error())
|
||||
}
|
||||
biz.ErrIsNilAppendErr(err, "拷贝表失败: %s")
|
||||
}
|
||||
|
||||
func getDbId(g *gin.Context) uint64 {
|
||||
dbId, _ := strconv.Atoi(g.Param("dbId"))
|
||||
biz.IsTrue(dbId > 0, "dbId错误")
|
||||
|
||||
@@ -9,13 +9,16 @@ import (
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/ginx"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DbBackup struct {
|
||||
DbBackupApp *application.DbBackupApp
|
||||
DbApp application.Db
|
||||
backupApp *application.DbBackupApp `inject:"DbBackupApp"`
|
||||
dbApp application.Db `inject:"DbApp"`
|
||||
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
|
||||
}
|
||||
|
||||
// todo: 鉴权,避免未经授权进行数据库备份和恢复
|
||||
@@ -25,13 +28,13 @@ type DbBackup struct {
|
||||
func (d *DbBackup) GetPageList(rc *req.Ctx) {
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbJobQuery](rc.GinCtx, new(entity.DbJobQuery))
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupQuery](rc.GinCtx, new(entity.DbBackupQuery))
|
||||
queryCond.DbInstanceId = db.InstanceId
|
||||
queryCond.InDbNames = strings.Fields(db.Database)
|
||||
res, err := d.DbBackupApp.GetPageList(queryCond, page, new([]vo.DbBackup))
|
||||
res, err := d.backupApp.GetPageList(queryCond, page, new([]vo.DbBackup))
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份任务失败: %v")
|
||||
rc.ResData = res
|
||||
}
|
||||
@@ -48,23 +51,22 @@ func (d *DbBackup) Create(rc *req.Ctx) {
|
||||
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "instanceId")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instanceId")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
jobs := make([]*entity.DbBackup, 0, len(dbNames))
|
||||
for _, dbName := range dbNames {
|
||||
job := &entity.DbBackup{
|
||||
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeBackup),
|
||||
Enabled: true,
|
||||
Repeated: backupForm.Repeated,
|
||||
StartTime: backupForm.StartTime,
|
||||
Interval: backupForm.Interval,
|
||||
Name: backupForm.Name,
|
||||
DbInstanceId: db.InstanceId,
|
||||
DbName: dbName,
|
||||
Enabled: true,
|
||||
Repeated: backupForm.Repeated,
|
||||
StartTime: backupForm.StartTime,
|
||||
Interval: backupForm.Interval,
|
||||
Name: backupForm.Name,
|
||||
}
|
||||
job.DbName = dbName
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
biz.ErrIsNilAppendErr(d.DbBackupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
|
||||
biz.ErrIsNilAppendErr(d.backupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Update 保存数据库备份任务
|
||||
@@ -74,17 +76,18 @@ func (d *DbBackup) Update(rc *req.Ctx) {
|
||||
ginx.BindJsonAndValid(rc.GinCtx, backupForm)
|
||||
rc.ReqParam = backupForm
|
||||
|
||||
job := entity.NewDbJob(entity.DbJobTypeBackup).(*entity.DbBackup)
|
||||
job := &entity.DbBackup{}
|
||||
job.Id = backupForm.Id
|
||||
job.Name = backupForm.Name
|
||||
job.StartTime = backupForm.StartTime
|
||||
job.Interval = backupForm.Interval
|
||||
biz.ErrIsNilAppendErr(d.DbBackupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
|
||||
job.MaxSaveDays = backupForm.MaxSaveDays
|
||||
biz.ErrIsNilAppendErr(d.backupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint64) error) error {
|
||||
idsStr := ginx.PathParam(rc.GinCtx, "backupId")
|
||||
biz.NotEmpty(idsStr, "backupId 为空")
|
||||
func (d *DbBackup) walk(rc *req.Ctx, paramName string, fn func(ctx context.Context, id uint64) error) error {
|
||||
idsStr := ginx.PathParam(rc.GinCtx, paramName)
|
||||
biz.NotEmpty(idsStr, paramName+" 为空")
|
||||
rc.ReqParam = idsStr
|
||||
ids := strings.Fields(idsStr)
|
||||
for _, v := range ids {
|
||||
@@ -104,28 +107,28 @@ func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint6
|
||||
// Delete 删除数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId [DELETE]
|
||||
func (d *DbBackup) Delete(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbBackupApp.Delete)
|
||||
err := d.walk(rc, "backupId", d.backupApp.Delete)
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Enable 启用数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId/enable [PUT]
|
||||
func (d *DbBackup) Enable(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbBackupApp.Enable)
|
||||
err := d.walk(rc, "backupId", d.backupApp.Enable)
|
||||
biz.ErrIsNilAppendErr(err, "启用数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Disable 禁用数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId/disable [PUT]
|
||||
func (d *DbBackup) Disable(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbBackupApp.Disable)
|
||||
err := d.walk(rc, "backupId", d.backupApp.Disable)
|
||||
biz.ErrIsNilAppendErr(err, "禁用数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Start 禁用数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId/start [PUT]
|
||||
func (d *DbBackup) Start(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbBackupApp.Start)
|
||||
err := d.walk(rc, "backupId", d.backupApp.StartNow)
|
||||
biz.ErrIsNilAppendErr(err, "运行数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
@@ -133,10 +136,10 @@ func (d *DbBackup) Start(rc *req.Ctx) {
|
||||
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
||||
func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
dbNames := strings.Fields(db.Database)
|
||||
dbNamesWithoutBackup, err := d.DbBackupApp.GetDbNamesWithoutBackup(db.InstanceId, dbNames)
|
||||
dbNamesWithoutBackup, err := d.backupApp.GetDbNamesWithoutBackup(db.InstanceId, dbNames)
|
||||
biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
|
||||
rc.ResData = dbNamesWithoutBackup
|
||||
}
|
||||
@@ -146,13 +149,74 @@ func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
||||
func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) {
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
|
||||
queryCond.DbInstanceId = db.InstanceId
|
||||
queryCond.InDbNames = strings.Fields(db.Database)
|
||||
res, err := d.DbBackupApp.GetHistoryPageList(queryCond, page, new([]vo.DbBackupHistory))
|
||||
backupHistoryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
|
||||
backupHistoryCond.DbInstanceId = db.InstanceId
|
||||
backupHistoryCond.InDbNames = strings.Fields(db.Database)
|
||||
backupHistories := make([]*vo.DbBackupHistory, 0, page.PageSize)
|
||||
res, err := d.backupApp.GetHistoryPageList(backupHistoryCond, page, &backupHistories)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
|
||||
historyIds := make([]uint64, 0, len(backupHistories))
|
||||
for _, history := range backupHistories {
|
||||
historyIds = append(historyIds, history.Id)
|
||||
}
|
||||
restores := make([]*entity.DbRestore, 0, page.PageSize)
|
||||
if err := d.restoreApp.GetRestoresEnabled(&restores, historyIds...); err != nil {
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份恢复记录失败")
|
||||
}
|
||||
for _, history := range backupHistories {
|
||||
for _, restore := range restores {
|
||||
if restore.DbBackupHistoryId == history.Id {
|
||||
history.LastStatus = restore.LastStatus
|
||||
history.LastResult = restore.LastResult
|
||||
history.LastTime = restore.LastTime
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
// RestoreHistories 从数据库备份历史中恢复数据库
|
||||
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId/restore [POST]
|
||||
func (d *DbBackup) RestoreHistories(rc *req.Ctx) {
|
||||
pm := ginx.PathParam(rc.GinCtx, "backupHistoryId")
|
||||
biz.NotEmpty(pm, "backupHistoryId 为空")
|
||||
idsStr := strings.Fields(pm)
|
||||
ids := make([]uint64, 0, len(idsStr))
|
||||
for _, s := range idsStr {
|
||||
id, err := strconv.ParseUint(s, 10, 64)
|
||||
biz.ErrIsNilAppendErr(err, "从数据库备份历史恢复数据库失败: %v")
|
||||
ids = append(ids, id)
|
||||
}
|
||||
histories := make([]*entity.DbBackupHistory, 0, len(ids))
|
||||
err := d.backupApp.GetHistories(ids, &histories)
|
||||
biz.ErrIsNilAppendErr(err, "添加数据库恢复任务失败: %v")
|
||||
restores := make([]*entity.DbRestore, 0, len(histories))
|
||||
now := time.Now()
|
||||
for _, history := range histories {
|
||||
job := &entity.DbRestore{
|
||||
DbInstanceId: history.DbInstanceId,
|
||||
DbName: history.DbName,
|
||||
Enabled: true,
|
||||
Repeated: false,
|
||||
StartTime: now,
|
||||
Interval: 0,
|
||||
PointInTime: timex.NewNullTime(time.Time{}),
|
||||
DbBackupId: history.DbBackupId,
|
||||
DbBackupHistoryId: history.Id,
|
||||
DbBackupHistoryName: history.Name,
|
||||
}
|
||||
restores = append(restores, job)
|
||||
}
|
||||
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, restores), "添加数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
// DeleteHistories 删除数据库备份历史
|
||||
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId [DELETE]
|
||||
func (d *DbBackup) DeleteHistories(rc *req.Ctx) {
|
||||
err := d.walk(rc, "backupHistoryId", d.backupApp.DeleteHistory)
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库备份历史失败: %v")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"mayfly-go/internal/db/api/form"
|
||||
"mayfly-go/internal/db/api/vo"
|
||||
@@ -15,11 +14,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type DataSyncTask struct {
|
||||
DataSyncTaskApp application.DataSyncTask
|
||||
DataSyncTaskApp application.DataSyncTask `inject:"DbDataSyncTaskApp"`
|
||||
}
|
||||
|
||||
func (d *DataSyncTask) Tasks(rc *req.Ctx) {
|
||||
@@ -47,13 +45,6 @@ func (d *DataSyncTask) SaveTask(rc *req.Ctx) {
|
||||
task.DataSql = sql
|
||||
form.DataSql = sql
|
||||
|
||||
key := task.TaskKey
|
||||
// 判断key为空就生成随机key
|
||||
if key == "" {
|
||||
key = uuid.New().String()
|
||||
task.TaskKey = key
|
||||
}
|
||||
|
||||
rc.ReqParam = form
|
||||
biz.ErrIsNil(d.DataSyncTaskApp.Save(rc.MetaCtx, task))
|
||||
}
|
||||
@@ -73,7 +64,7 @@ func (d *DataSyncTask) DeleteTask(rc *req.Ctx) {
|
||||
func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
|
||||
form := &form.DataSyncTaskStatusForm{}
|
||||
task := ginx.BindJsonAndCopyTo[*entity.DataSyncTask](rc.GinCtx, form, new(entity.DataSyncTask))
|
||||
_ = d.DataSyncTaskApp.UpdateById(context.Background(), task)
|
||||
_ = d.DataSyncTaskApp.UpdateById(rc.MetaCtx, task)
|
||||
|
||||
if task.Status == entity.DataSyncTaskStatusEnable {
|
||||
task, err := d.DataSyncTaskApp.GetById(new(entity.DataSyncTask), task.Id)
|
||||
@@ -89,7 +80,7 @@ func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
|
||||
func (d *DataSyncTask) Run(rc *req.Ctx) {
|
||||
taskId := getTaskId(rc.GinCtx)
|
||||
rc.ReqParam = taskId
|
||||
d.DataSyncTaskApp.RunCronJob(taskId)
|
||||
_ = d.DataSyncTaskApp.RunCronJob(taskId)
|
||||
}
|
||||
|
||||
func (d *DataSyncTask) Stop(rc *req.Ctx) {
|
||||
@@ -99,7 +90,7 @@ func (d *DataSyncTask) Stop(rc *req.Ctx) {
|
||||
task := new(entity.DataSyncTask)
|
||||
task.Id = taskId
|
||||
task.RunningState = entity.DataSyncTaskRunStateStop
|
||||
_ = d.DataSyncTaskApp.UpdateById(context.Background(), task)
|
||||
_ = d.DataSyncTaskApp.UpdateById(rc.MetaCtx, task)
|
||||
}
|
||||
|
||||
func (d *DataSyncTask) GetTask(rc *req.Ctx) {
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
)
|
||||
|
||||
type Instance struct {
|
||||
InstanceApp application.Instance
|
||||
DbApp application.Db
|
||||
InstanceApp application.Instance `inject:"DbInstanceApp"`
|
||||
DbApp application.Db `inject:""`
|
||||
}
|
||||
|
||||
// Instances 获取数据库实例信息
|
||||
@@ -87,16 +87,10 @@ func (d *Instance) DeleteInstance(rc *req.Ctx) {
|
||||
|
||||
for _, v := range ids {
|
||||
value, err := strconv.Atoi(v)
|
||||
biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s")
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库实例失败: %s")
|
||||
instanceId := uint64(value)
|
||||
if d.DbApp.Count(&entity.DbQuery{InstanceId: instanceId}) != 0 {
|
||||
instance, err := d.InstanceApp.GetById(new(entity.DbInstance), instanceId, "name")
|
||||
biz.ErrIsNil(err, "获取数据库实例错误,数据库实例ID为: %d", instance.Id)
|
||||
biz.IsTrue(false, "不能删除数据库实例【%s】,请先删除其关联的数据库资源。", instance.Name)
|
||||
}
|
||||
// todo check if backup task has been disabled and backup histories have been deleted
|
||||
|
||||
d.InstanceApp.Delete(rc.MetaCtx, instanceId)
|
||||
err = d.InstanceApp.Delete(rc.MetaCtx, instanceId)
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库实例失败: %s")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
)
|
||||
|
||||
type DbRestore struct {
|
||||
DbRestoreApp *application.DbRestoreApp
|
||||
DbApp application.Db
|
||||
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
|
||||
dbApp application.Db `inject:"DbApp"`
|
||||
}
|
||||
|
||||
// GetPageList 获取数据库恢复任务
|
||||
@@ -23,14 +23,14 @@ type DbRestore struct {
|
||||
func (d *DbRestore) GetPageList(rc *req.Ctx) {
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
var restores []vo.DbRestore
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbJobQuery](rc.GinCtx, new(entity.DbJobQuery))
|
||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbRestoreQuery](rc.GinCtx, new(entity.DbRestoreQuery))
|
||||
queryCond.DbInstanceId = db.InstanceId
|
||||
queryCond.InDbNames = strings.Fields(db.Database)
|
||||
res, err := d.DbRestoreApp.GetPageList(queryCond, page, &restores)
|
||||
res, err := d.restoreApp.GetPageList(queryCond, page, &restores)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库恢复任务失败: %v")
|
||||
rc.ResData = res
|
||||
}
|
||||
@@ -44,11 +44,12 @@ func (d *DbRestore) Create(rc *req.Ctx) {
|
||||
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "instanceId")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instanceId")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
job := &entity.DbRestore{
|
||||
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeRestore),
|
||||
DbInstanceId: db.InstanceId,
|
||||
DbName: restoreForm.DbName,
|
||||
Enabled: true,
|
||||
Repeated: restoreForm.Repeated,
|
||||
StartTime: restoreForm.StartTime,
|
||||
@@ -58,8 +59,11 @@ func (d *DbRestore) Create(rc *req.Ctx) {
|
||||
DbBackupHistoryId: restoreForm.DbBackupHistoryId,
|
||||
DbBackupHistoryName: restoreForm.DbBackupHistoryName,
|
||||
}
|
||||
job.DbName = restoreForm.DbName
|
||||
biz.ErrIsNilAppendErr(d.DbRestoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
|
||||
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
func (d *DbRestore) createWithBackupHistory(backupHistoryIds string) {
|
||||
|
||||
}
|
||||
|
||||
// Update 保存数据库恢复任务
|
||||
@@ -73,7 +77,7 @@ func (d *DbRestore) Update(rc *req.Ctx) {
|
||||
job.Id = restoreForm.Id
|
||||
job.StartTime = restoreForm.StartTime
|
||||
job.Interval = restoreForm.Interval
|
||||
biz.ErrIsNilAppendErr(d.DbRestoreApp.Update(rc.MetaCtx, job), "保存数据库恢复任务失败: %v")
|
||||
biz.ErrIsNilAppendErr(d.restoreApp.Update(rc.MetaCtx, job), "保存数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
func (d *DbRestore) walk(rc *req.Ctx, fn func(ctx context.Context, restoreId uint64) error) error {
|
||||
@@ -98,21 +102,21 @@ func (d *DbRestore) walk(rc *req.Ctx, fn func(ctx context.Context, restoreId uin
|
||||
// Delete 删除数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores/:restoreId [DELETE]
|
||||
func (d *DbRestore) Delete(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbRestoreApp.Delete)
|
||||
err := d.walk(rc, d.restoreApp.Delete)
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
// Enable 启用数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores/:restoreId/enable [PUT]
|
||||
func (d *DbRestore) Enable(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbRestoreApp.Enable)
|
||||
err := d.walk(rc, d.restoreApp.Enable)
|
||||
biz.ErrIsNilAppendErr(err, "启用数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
// Disable 禁用数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores/:restoreId/disable [PUT]
|
||||
func (d *DbRestore) Disable(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.DbRestoreApp.Disable)
|
||||
err := d.walk(rc, d.restoreApp.Disable)
|
||||
biz.ErrIsNilAppendErr(err, "禁用数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
@@ -120,21 +124,21 @@ func (d *DbRestore) Disable(rc *req.Ctx) {
|
||||
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
||||
func (d *DbRestore) GetDbNamesWithoutRestore(rc *req.Ctx) {
|
||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||
db, err := d.DbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
|
||||
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
dbNames := strings.Fields(db.Database)
|
||||
dbNamesWithoutRestore, err := d.DbRestoreApp.GetDbNamesWithoutRestore(db.InstanceId, dbNames)
|
||||
dbNamesWithoutRestore, err := d.restoreApp.GetDbNamesWithoutRestore(db.InstanceId, dbNames)
|
||||
biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
|
||||
rc.ResData = dbNamesWithoutRestore
|
||||
}
|
||||
|
||||
// 获取数据库备份历史
|
||||
// GetHistoryPageList 获取数据库备份历史
|
||||
// @router /api/dbs/:dbId/restores/:restoreId/histories [GET]
|
||||
func (d *DbRestore) GetHistoryPageList(rc *req.Ctx) {
|
||||
queryCond := &entity.DbRestoreHistoryQuery{
|
||||
DbRestoreId: uint64(ginx.PathParamInt(rc.GinCtx, "restoreId")),
|
||||
}
|
||||
res, err := d.DbRestoreApp.GetHistoryPageList(queryCond, ginx.GetPageParam(rc.GinCtx), new([]vo.DbRestoreHistory))
|
||||
res, err := d.restoreApp.GetHistoryPageList(queryCond, ginx.GetPageParam(rc.GinCtx), new([]vo.DbRestoreHistory))
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type DbSql struct {
|
||||
DbSqlApp application.DbSql
|
||||
DbSqlApp application.DbSql `inject:""`
|
||||
}
|
||||
|
||||
// @router /api/db/:dbId/sql [post]
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
type DbSqlExec struct {
|
||||
DbSqlExecApp application.DbSqlExec
|
||||
DbSqlExecApp application.DbSqlExec `inject:""`
|
||||
}
|
||||
|
||||
func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) {
|
||||
|
||||
@@ -23,3 +23,11 @@ type DbSqlExecForm struct {
|
||||
Sql string `binding:"required" json:"sql"` // 执行sql
|
||||
Remark string `json:"remark"` // 执行备注
|
||||
}
|
||||
|
||||
// 数据库复制表
|
||||
type DbCopyTableForm struct {
|
||||
Id uint64 `binding:"required" json:"id"`
|
||||
Db string `binding:"required" json:"db" `
|
||||
TableName string `binding:"required" json:"tableName"`
|
||||
CopyData bool `json:"copyData"` // 是否复制数据
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type DbBackupForm struct {
|
||||
Interval time.Duration `json:"-"` // 间隔时间: 为零表示单次执行,为正表示反复执行
|
||||
IntervalDay uint64 `json:"intervalDay"` // 间隔天数: 为零表示单次执行,为正表示反复执行
|
||||
Repeated bool `json:"repeated"` // 是否重复执行
|
||||
MaxSaveDays int `json:"maxSaveDays"` // 数据库备份历史保留天数,过期将自动删除
|
||||
}
|
||||
|
||||
func (restore *DbBackupForm) UnmarshalJSON(data []byte) error {
|
||||
|
||||
@@ -5,9 +5,9 @@ type InstanceForm struct {
|
||||
Name string `binding:"required" json:"name"`
|
||||
Type string `binding:"required" json:"type"` // 类型,mysql oracle等
|
||||
Host string `binding:"required" json:"host"`
|
||||
Port int `binding:"required" json:"port"`
|
||||
Port int `json:"port"`
|
||||
Sid string `json:"sid"`
|
||||
Username string `binding:"required" json:"username"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Params string `json:"params"`
|
||||
Remark string `json:"remark"`
|
||||
|
||||
@@ -2,37 +2,51 @@ package vo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DbBackup 数据库备份任务
|
||||
type DbBackup struct {
|
||||
Id uint64 `json:"id"`
|
||||
DbName string `json:"dbName"` // 数据库名
|
||||
CreateTime time.Time `json:"createTime"` // 创建时间
|
||||
StartTime time.Time `json:"startTime"` // 开始时间
|
||||
Interval time.Duration `json:"-"` // 间隔时间
|
||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||
Name string `json:"name"` // 备份任务名称
|
||||
Id uint64 `json:"id"`
|
||||
DbName string `json:"dbName"` // 数据库名
|
||||
CreateTime time.Time `json:"createTime"` // 创建时间
|
||||
StartTime time.Time `json:"startTime"` // 开始时间
|
||||
Interval time.Duration `json:"-"` // 间隔时间
|
||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||
MaxSaveDays int `json:"maxSaveDays"` // 数据库备份历史保留天数,过期将自动删除
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
|
||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||
LastStatus entity.DbJobStatus `json:"lastStatus"` // 最近一次执行状态
|
||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||
Name string `json:"name"` // 备份任务名称
|
||||
}
|
||||
|
||||
func (backup *DbBackup) MarshalJSON() ([]byte, error) {
|
||||
type dbBackup DbBackup
|
||||
backup.IntervalDay = uint64(backup.Interval / time.Hour / 24)
|
||||
if len(backup.EnabledDesc) == 0 {
|
||||
if backup.Enabled {
|
||||
backup.EnabledDesc = "已启用"
|
||||
} else {
|
||||
backup.EnabledDesc = "已禁用"
|
||||
}
|
||||
}
|
||||
return json.Marshal((*dbBackup)(backup))
|
||||
}
|
||||
|
||||
// DbBackupHistory 数据库备份历史
|
||||
type DbBackupHistory struct {
|
||||
Id uint64 `json:"id"`
|
||||
DbBackupId uint64 `json:"dbBackupId"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
DbName string `json:"dbName"` // 数据库名称
|
||||
Name string `json:"name"` // 备份历史名称
|
||||
Id uint64 `json:"id"`
|
||||
DbBackupId uint64 `json:"dbBackupId"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
DbName string `json:"dbName"` // 数据库名称
|
||||
Name string `json:"name"` // 备份历史名称
|
||||
BinlogFileName string `json:"binlogFileName"`
|
||||
LastTime timex.NullTime `json:"lastTime" gorm:"-"` // 最近一次恢复时间
|
||||
LastStatus entity.DbJobStatus `json:"lastStatus" gorm:"-"` // 最近一次恢复状态
|
||||
LastResult string `json:"lastResult" gorm:"-"` // 最近一次恢复结果
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type DbRestore struct {
|
||||
Interval time.Duration `json:"-"` // 间隔时间
|
||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
|
||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||
@@ -27,6 +28,13 @@ type DbRestore struct {
|
||||
func (restore *DbRestore) MarshalJSON() ([]byte, error) {
|
||||
type dbBackup DbRestore
|
||||
restore.IntervalDay = uint64(restore.Interval / time.Hour / 24)
|
||||
if len(restore.EnabledDesc) == 0 {
|
||||
if restore.Enabled {
|
||||
restore.EnabledDesc = "已启用"
|
||||
} else {
|
||||
restore.EnabledDesc = "已禁用"
|
||||
}
|
||||
}
|
||||
return json.Marshal((*dbBackup)(restore))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,91 +2,53 @@ package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/internal/db/infrastructure/persistence"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/ioc"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
instanceApp Instance
|
||||
dbApp Db
|
||||
dbSqlExecApp DbSqlExec
|
||||
dbSqlApp DbSql
|
||||
dbBackupApp *DbBackupApp
|
||||
dbRestoreApp *DbRestoreApp
|
||||
dbBinlogApp *DbBinlogApp
|
||||
dataSyncApp DataSyncTask
|
||||
)
|
||||
func InitIoc() {
|
||||
persistence.Init()
|
||||
|
||||
ioc.Register(new(instanceAppImpl), ioc.WithComponentName("DbInstanceApp"))
|
||||
ioc.Register(new(dbAppImpl), ioc.WithComponentName("DbApp"))
|
||||
ioc.Register(new(dbSqlExecAppImpl), ioc.WithComponentName("DbSqlExecApp"))
|
||||
ioc.Register(new(dbSqlAppImpl), ioc.WithComponentName("DbSqlApp"))
|
||||
ioc.Register(new(dataSyncAppImpl), ioc.WithComponentName("DbDataSyncTaskApp"))
|
||||
|
||||
ioc.Register(newDbScheduler(), ioc.WithComponentName("DbScheduler"))
|
||||
ioc.Register(new(DbBackupApp), ioc.WithComponentName("DbBackupApp"))
|
||||
ioc.Register(new(DbRestoreApp), ioc.WithComponentName("DbRestoreApp"))
|
||||
ioc.Register(newDbBinlogApp(), ioc.WithComponentName("DbBinlogApp"))
|
||||
}
|
||||
|
||||
func Init() {
|
||||
sync.OnceFunc(func() {
|
||||
repositories := &repository.Repositories{
|
||||
Instance: persistence.GetInstanceRepo(),
|
||||
Backup: persistence.NewDbBackupRepo(),
|
||||
BackupHistory: persistence.NewDbBackupHistoryRepo(),
|
||||
Restore: persistence.NewDbRestoreRepo(),
|
||||
RestoreHistory: persistence.NewDbRestoreHistoryRepo(),
|
||||
Binlog: persistence.NewDbBinlogRepo(),
|
||||
BinlogHistory: persistence.NewDbBinlogHistoryRepo(),
|
||||
if err := GetDbBackupApp().Init(); err != nil {
|
||||
panic(fmt.Sprintf("初始化 DbBackupApp 失败: %v", err))
|
||||
}
|
||||
var err error
|
||||
instanceRepo := persistence.GetInstanceRepo()
|
||||
instanceApp = newInstanceApp(instanceRepo)
|
||||
dbApp = newDbApp(persistence.GetDbRepo(), persistence.GetDbSqlRepo(), instanceApp, tagapp.GetTagTreeApp())
|
||||
dbSqlExecApp = newDbSqlExecApp(persistence.GetDbSqlExecRepo())
|
||||
dbSqlApp = newDbSqlApp(persistence.GetDbSqlRepo())
|
||||
dataSyncApp = newDataSyncApp(persistence.GetDataSyncTaskRepo(), persistence.GetDataSyncLogRepo())
|
||||
|
||||
scheduler, err := newDbScheduler(repositories)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("初始化 dbScheduler 失败: %v", err))
|
||||
if err := GetDbRestoreApp().Init(); err != nil {
|
||||
panic(fmt.Sprintf("初始化 DbRestoreApp 失败: %v", err))
|
||||
}
|
||||
dbBackupApp, err = newDbBackupApp(repositories, dbApp, scheduler)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("初始化 dbBackupApp 失败: %v", err))
|
||||
if err := GetDbBinlogApp().Init(); err != nil {
|
||||
panic(fmt.Sprintf("初始化 DbBinlogApp 失败: %v", err))
|
||||
}
|
||||
dbRestoreApp, err = newDbRestoreApp(repositories, dbApp, scheduler)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("初始化 dbRestoreApp 失败: %v", err))
|
||||
}
|
||||
dbBinlogApp, err = newDbBinlogApp(repositories, dbApp, scheduler)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("初始化 dbBinlogApp 失败: %v", err))
|
||||
}
|
||||
|
||||
dataSyncApp.InitCronJob()
|
||||
GetDataSyncTaskApp().InitCronJob()
|
||||
})()
|
||||
}
|
||||
|
||||
func GetInstanceApp() Instance {
|
||||
return instanceApp
|
||||
}
|
||||
|
||||
func GetDbApp() Db {
|
||||
return dbApp
|
||||
}
|
||||
|
||||
func GetDbSqlApp() DbSql {
|
||||
return dbSqlApp
|
||||
}
|
||||
|
||||
func GetDbSqlExecApp() DbSqlExec {
|
||||
return dbSqlExecApp
|
||||
}
|
||||
|
||||
func GetDbBackupApp() *DbBackupApp {
|
||||
return dbBackupApp
|
||||
return ioc.Get[*DbBackupApp]("DbBackupApp")
|
||||
}
|
||||
|
||||
func GetDbRestoreApp() *DbRestoreApp {
|
||||
return dbRestoreApp
|
||||
return ioc.Get[*DbRestoreApp]("DbRestoreApp")
|
||||
}
|
||||
|
||||
func GetDbBinlogApp() *DbBinlogApp {
|
||||
return dbBinlogApp
|
||||
return ioc.Get[*DbBinlogApp]("DbBinlogApp")
|
||||
}
|
||||
|
||||
func GetDataSyncTaskApp() DataSyncTask {
|
||||
return dataSyncApp
|
||||
return ioc.Get[DataSyncTask]("DbDataSyncTaskApp")
|
||||
}
|
||||
|
||||
@@ -40,22 +40,17 @@ type Db interface {
|
||||
GetDbConnByInstanceId(instanceId uint64) (*dbi.DbConn, error)
|
||||
}
|
||||
|
||||
func newDbApp(dbRepo repository.Db, dbSqlRepo repository.DbSql, dbInstanceApp Instance, tagApp tagapp.TagTree) Db {
|
||||
app := &dbAppImpl{
|
||||
dbSqlRepo: dbSqlRepo,
|
||||
dbInstanceApp: dbInstanceApp,
|
||||
tagApp: tagApp,
|
||||
}
|
||||
app.Repo = dbRepo
|
||||
return app
|
||||
}
|
||||
|
||||
type dbAppImpl struct {
|
||||
base.AppImpl[*entity.Db, repository.Db]
|
||||
|
||||
dbSqlRepo repository.DbSql
|
||||
dbInstanceApp Instance
|
||||
tagApp tagapp.TagTree
|
||||
dbSqlRepo repository.DbSql `inject:"DbSqlRepo"`
|
||||
dbInstanceApp Instance `inject:"DbInstanceApp"`
|
||||
tagApp tagapp.TagTree `inject:"TagTreeApp"`
|
||||
}
|
||||
|
||||
// 注入DbRepo
|
||||
func (d *dbAppImpl) InjectDbRepo(repo repository.Db) {
|
||||
d.Repo = repo
|
||||
}
|
||||
|
||||
// 分页获取数据库信息列表
|
||||
@@ -103,9 +98,13 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db, tagIds ...u
|
||||
// 比较新旧数据库列表,需要将移除的数据库相关联的信息删除
|
||||
_, delDb, _ := collx.ArrayCompare(newDbs, oldDbs)
|
||||
|
||||
for _, v := range delDb {
|
||||
// 先简单关闭可能存在的旧库连接(可能改了关联标签导致DbConn.Info.TagPath与修改后的标签不一致、导致操作权限校验出错)
|
||||
for _, v := range oldDbs {
|
||||
// 关闭数据库连接
|
||||
dbm.CloseDb(dbEntity.Id, v)
|
||||
}
|
||||
|
||||
for _, v := range delDb {
|
||||
// 删除该库关联的所有sql记录
|
||||
d.dbSqlRepo.DeleteByCond(ctx, &entity.DbSql{DbId: dbId, Db: v})
|
||||
}
|
||||
@@ -155,7 +154,7 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbi.DbConn, error) {
|
||||
|
||||
checkDb := dbName
|
||||
// 兼容pgsql/dm db/schema模式
|
||||
if dbi.DbTypePostgres.Equal(instance.Type) || dbi.DbTypeDM.Equal(instance.Type) || dbi.DbTypeOracle.Equal(instance.Type) {
|
||||
if dbi.DbTypePostgres.Equal(instance.Type) || dbi.DbTypeGauss.Equal(instance.Type) || dbi.DbTypeDM.Equal(instance.Type) || dbi.DbTypeOracle.Equal(instance.Type) || dbi.DbTypeMssql.Equal(instance.Type) || dbi.DbTypeKingbaseEs.Equal(instance.Type) || dbi.DbTypeVastbase.Equal(instance.Type) {
|
||||
ss := strings.Split(dbName, "/")
|
||||
if len(ss) > 1 {
|
||||
checkDb = ss[0]
|
||||
|
||||
@@ -3,69 +3,217 @@ package application
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"github.com/google/uuid"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"math"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func newDbBackupApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbBackupApp, error) {
|
||||
var jobs []*entity.DbBackup
|
||||
if err := repositories.Backup.ListToDo(&jobs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := scheduler.AddJob(context.Background(), false, entity.DbJobTypeBackup, jobs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
app := &DbBackupApp{
|
||||
backupRepo: repositories.Backup,
|
||||
instanceRepo: repositories.Instance,
|
||||
backupHistoryRepo: repositories.BackupHistory,
|
||||
dbApp: dbApp,
|
||||
scheduler: scheduler,
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
const maxBackupHistoryDays = 30
|
||||
|
||||
var (
|
||||
errRestoringBackupHistory = errors.New("正在从备份历史中恢复数据库")
|
||||
)
|
||||
|
||||
type DbBackupApp struct {
|
||||
backupRepo repository.DbBackup
|
||||
instanceRepo repository.Instance
|
||||
backupHistoryRepo repository.DbBackupHistory
|
||||
dbApp Db
|
||||
scheduler *dbScheduler
|
||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
||||
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||
dbApp Db `inject:"DbApp"`
|
||||
mutex sync.Mutex
|
||||
closed chan struct{}
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Init() error {
|
||||
var jobs []*entity.DbBackup
|
||||
if err := app.backupRepo.ListToDo(&jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
app.ctx, app.cancel = context.WithCancel(context.Background())
|
||||
app.wg.Add(1)
|
||||
go func() {
|
||||
defer app.wg.Done()
|
||||
for app.ctx.Err() == nil {
|
||||
if err := app.prune(app.ctx); err != nil {
|
||||
logx.Errorf("清理数据库备份历史失败: %s", err.Error())
|
||||
timex.SleepWithContext(app.ctx, time.Minute*15)
|
||||
continue
|
||||
}
|
||||
timex.SleepWithContext(app.ctx, time.Hour*24)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) prune(ctx context.Context) error {
|
||||
var jobs []*entity.DbBackup
|
||||
if err := app.backupRepo.ListByCond(map[string]any{}, &jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
var histories []*entity.DbBackupHistory
|
||||
historyCond := map[string]any{
|
||||
"db_backup_id": job.Id,
|
||||
}
|
||||
if err := app.backupHistoryRepo.ListByCondOrder(historyCond, &histories, "id"); err != nil {
|
||||
return err
|
||||
}
|
||||
expiringTime := time.Now().Add(-math.MaxInt64)
|
||||
if job.MaxSaveDays > 0 {
|
||||
expiringTime = time.Now().Add(-time.Hour * 24 * time.Duration(job.MaxSaveDays+1))
|
||||
}
|
||||
for _, history := range histories {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
if history.CreateTime.After(expiringTime) {
|
||||
break
|
||||
}
|
||||
err := app.DeleteHistory(ctx, history.Id)
|
||||
if errors.Is(err, errRestoringBackupHistory) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Close() {
|
||||
app.scheduler.Close()
|
||||
if app.cancel != nil {
|
||||
app.cancel()
|
||||
app.cancel = nil
|
||||
}
|
||||
app.wg.Wait()
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Create(ctx context.Context, jobs []*entity.DbBackup) error {
|
||||
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeBackup, jobs)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.backupRepo.AddJob(ctx, jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
return app.scheduler.AddJob(ctx, jobs)
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Update(ctx context.Context, job *entity.DbBackup) error {
|
||||
return app.scheduler.UpdateJob(ctx, job)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.backupRepo.UpdateById(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = app.scheduler.UpdateJob(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
|
||||
// todo: 删除数据库备份历史文件
|
||||
return app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
history := &entity.DbBackupHistory{
|
||||
DbBackupId: jobId,
|
||||
}
|
||||
err := app.backupHistoryRepo.GetBy(history, "name")
|
||||
switch {
|
||||
default:
|
||||
return err
|
||||
case err == nil:
|
||||
return fmt.Errorf("请先删除关联的数据库备份历史【%s】", history.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
}
|
||||
if err := app.backupRepo.DeleteById(ctx, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Enable(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.EnableJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.backupRepo
|
||||
job := &entity.DbBackup{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
if job.IsExpired() {
|
||||
return errors.New("任务已过期")
|
||||
}
|
||||
_ = app.scheduler.EnableJob(ctx, job)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
|
||||
logx.Errorf("数据库备份任务已启用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Disable(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.backupRepo
|
||||
job := &entity.DbBackup{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
|
||||
logx.Errorf("数据库恢复任务已禁用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Start(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.StartJobNow(ctx, entity.DbJobTypeBackup, jobId)
|
||||
func (app *DbBackupApp) StartNow(ctx context.Context, jobId uint64) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
job := &entity.DbBackup{}
|
||||
if err := app.backupRepo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return errors.New("任务未启用")
|
||||
}
|
||||
_ = app.scheduler.StartJobNow(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库备份任务
|
||||
func (app *DbBackupApp) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
func (app *DbBackupApp) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.backupRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
@@ -76,7 +224,11 @@ func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []str
|
||||
|
||||
// GetHistoryPageList 分页获取数据库备份历史
|
||||
func (app *DbBackupApp) GetHistoryPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.backupHistoryRepo.GetHistories(condition, pageParam, toEntity, orderBy...)
|
||||
return app.backupHistoryRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) GetHistories(backupHistoryIds []uint64, toEntity any) error {
|
||||
return app.backupHistoryRepo.GetHistories(backupHistoryIds, toEntity)
|
||||
}
|
||||
|
||||
func NewIncUUID() (uuid.UUID, error) {
|
||||
@@ -99,3 +251,35 @@ func NewIncUUID() (uuid.UUID, error) {
|
||||
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) DeleteHistory(ctx context.Context, historyId uint64) (retErr error) {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if _, err := app.backupHistoryRepo.UpdateDeleting(false, historyId); err != nil {
|
||||
return err
|
||||
}
|
||||
ok, err := app.backupHistoryRepo.UpdateDeleting(true, historyId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return errRestoringBackupHistory
|
||||
}
|
||||
job := &entity.DbBackupHistory{}
|
||||
if err := app.backupHistoryRepo.GetById(job, historyId); err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := app.dbApp.GetDbConnByInstanceId(job.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram, err := conn.GetDialect().GetDbProgram()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dbProgram.RemoveBackupHistory(ctx, job.DbBackupId, job.Uuid); err != nil {
|
||||
return err
|
||||
}
|
||||
return app.backupHistoryRepo.DeleteById(ctx, historyId)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
@@ -11,64 +12,132 @@ import (
|
||||
)
|
||||
|
||||
type DbBinlogApp struct {
|
||||
binlogRepo repository.DbBinlog
|
||||
binlogHistoryRepo repository.DbBinlogHistory
|
||||
backupRepo repository.DbBackup
|
||||
backupHistoryRepo repository.DbBackupHistory
|
||||
dbApp Db
|
||||
context context.Context
|
||||
cancel context.CancelFunc
|
||||
waitGroup sync.WaitGroup
|
||||
scheduler *dbScheduler
|
||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
||||
binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
|
||||
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
||||
instanceRepo repository.Instance `inject:"DbInstanceRepo"`
|
||||
dbApp Db `inject:"DbApp"`
|
||||
|
||||
context context.Context
|
||||
cancel context.CancelFunc
|
||||
waitGroup sync.WaitGroup
|
||||
}
|
||||
|
||||
func newDbBinlogApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbBinlogApp, error) {
|
||||
func newDbBinlogApp() *DbBinlogApp {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
svc := &DbBinlogApp{
|
||||
binlogRepo: repositories.Binlog,
|
||||
binlogHistoryRepo: repositories.BinlogHistory,
|
||||
backupRepo: repositories.Backup,
|
||||
backupHistoryRepo: repositories.BackupHistory,
|
||||
dbApp: dbApp,
|
||||
scheduler: scheduler,
|
||||
context: ctx,
|
||||
cancel: cancel,
|
||||
context: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
svc.waitGroup.Add(1)
|
||||
go svc.run()
|
||||
return svc, nil
|
||||
return svc
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) Init() error {
|
||||
app.context, app.cancel = context.WithCancel(context.Background())
|
||||
app.waitGroup.Add(1)
|
||||
go app.run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) run() {
|
||||
defer app.waitGroup.Done()
|
||||
|
||||
// todo: 实现 binlog 并发下载
|
||||
timex.SleepWithContext(app.context, time.Minute)
|
||||
for !app.closed() {
|
||||
jobs, err := app.loadJobs()
|
||||
if err != nil {
|
||||
logx.Errorf("DbBinlogApp: 加载 BINLOG 同步任务失败: %s", err.Error())
|
||||
for app.context.Err() == nil {
|
||||
if err := app.fetchBinlog(app.context); err != nil {
|
||||
timex.SleepWithContext(app.context, time.Minute)
|
||||
continue
|
||||
}
|
||||
if app.closed() {
|
||||
break
|
||||
}
|
||||
if err := app.scheduler.AddJob(app.context, false, entity.DbJobTypeBinlog, jobs); err != nil {
|
||||
logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
|
||||
if err := app.pruneBinlog(app.context); err != nil {
|
||||
timex.SleepWithContext(app.context, time.Minute)
|
||||
continue
|
||||
}
|
||||
timex.SleepWithContext(app.context, entity.BinlogDownloadInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) loadJobs() ([]*entity.DbBinlog, error) {
|
||||
func (app *DbBinlogApp) fetchBinlog(ctx context.Context) error {
|
||||
jobs, err := app.loadJobs(ctx)
|
||||
if err != nil {
|
||||
logx.Errorf("DbBinlogApp: 加载 BINLOG 同步任务失败: %s", err.Error())
|
||||
timex.SleepWithContext(app.context, time.Minute)
|
||||
return err
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if err := app.scheduler.AddJob(app.context, jobs); err != nil {
|
||||
logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) pruneBinlog(ctx context.Context) error {
|
||||
var jobs []*entity.DbBinlog
|
||||
if err := app.binlogRepo.ListByCond(map[string]any{}, &jobs); err != nil {
|
||||
logx.Error("DbBinlogApp: 获取 BINLOG 同步任务失败: ", err.Error())
|
||||
return err
|
||||
}
|
||||
for _, instance := range jobs {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
var histories []*entity.DbBinlogHistory
|
||||
backupHistory, backupHistoryExists, err := app.backupHistoryRepo.GetEarliestHistoryForBinlog(instance.Id)
|
||||
if err != nil {
|
||||
logx.Errorf("DbBinlogApp: 获取数据库备份历史失败: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
var binlogSeq int64 = math.MaxInt64
|
||||
if backupHistoryExists {
|
||||
binlogSeq = backupHistory.BinlogSequence
|
||||
}
|
||||
if err := app.binlogHistoryRepo.GetHistoriesBeforeSequence(ctx, instance.Id, binlogSeq, &histories); err != nil {
|
||||
logx.Errorf("DbBinlogApp: 获取数据库 BINLOG 历史失败: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
conn, err := app.dbApp.GetDbConnByInstanceId(instance.Id)
|
||||
if err != nil {
|
||||
logx.Errorf("DbBinlogApp: 创建数据库连接失败: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
dbProgram, err := conn.GetDialect().GetDbProgram()
|
||||
if err != nil {
|
||||
logx.Errorf("DbBinlogApp: 获取数据库备份与恢复程序失败: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
for i, history := range histories {
|
||||
// todo: 在避免并发访问的前提下删除本地最新的 BINLOG 文件
|
||||
if !backupHistoryExists && i == len(histories)-1 {
|
||||
// 暂不删除本地最新的 BINLOG 文件
|
||||
break
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if err := dbProgram.PruneBinlog(history); err != nil {
|
||||
logx.Errorf("清理 BINLOG 文件失败: %v", err)
|
||||
continue
|
||||
}
|
||||
if err := app.binlogHistoryRepo.DeleteById(ctx, history.Id); err != nil {
|
||||
logx.Errorf("删除 BINLOG 历史失败: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) loadJobs(ctx context.Context) ([]*entity.DbBinlog, error) {
|
||||
var instanceIds []uint64
|
||||
if err := app.backupRepo.ListDbInstances(true, true, &instanceIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jobs := make([]*entity.DbBinlog, 0, len(instanceIds))
|
||||
for _, id := range instanceIds {
|
||||
if app.closed() {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
binlog := entity.NewDbBinlog(id)
|
||||
@@ -81,14 +150,15 @@ func (app *DbBinlogApp) loadJobs() ([]*entity.DbBinlog, error) {
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) Close() {
|
||||
app.cancel()
|
||||
cancel := app.cancel
|
||||
if cancel == nil {
|
||||
return
|
||||
}
|
||||
app.cancel = nil
|
||||
cancel()
|
||||
app.waitGroup.Wait()
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) closed() bool {
|
||||
return app.context.Err() != nil
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) AddJobIfNotExists(ctx context.Context, job *entity.DbBinlog) error {
|
||||
if err := app.binlogRepo.AddJobIfNotExists(ctx, job); err != nil {
|
||||
return err
|
||||
@@ -98,11 +168,3 @@ func (app *DbBinlogApp) AddJobIfNotExists(ctx context.Context, job *entity.DbBin
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) Delete(ctx context.Context, jobId uint64) error {
|
||||
// todo: 删除 Binlog 历史文件
|
||||
if err := app.binlogRepo.DeleteById(ctx, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,7 +14,12 @@ import (
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/scheduler"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type DataSyncTask interface {
|
||||
@@ -38,17 +43,20 @@ type DataSyncTask interface {
|
||||
GetTaskLogList(condition *entity.DataSyncLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
}
|
||||
|
||||
func newDataSyncApp(dataSyncRepo repository.DataSyncTask, dataSyncLogRepo repository.DataSyncLog) DataSyncTask {
|
||||
app := new(dataSyncAppImpl)
|
||||
app.Repo = dataSyncRepo
|
||||
app.dataSyncLogRepo = dataSyncLogRepo
|
||||
return app
|
||||
}
|
||||
|
||||
type dataSyncAppImpl struct {
|
||||
base.AppImpl[*entity.DataSyncTask, repository.DataSyncTask]
|
||||
|
||||
dataSyncLogRepo repository.DataSyncLog
|
||||
dbDataSyncLogRepo repository.DataSyncLog `inject:"DbDataSyncLogRepo"`
|
||||
|
||||
dbApp Db `inject:"DbApp"`
|
||||
}
|
||||
|
||||
var (
|
||||
dateTimeReg = regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`)
|
||||
)
|
||||
|
||||
func (app *dataSyncAppImpl) InjectDbDataSyncTaskRepo(repo repository.DataSyncTask) {
|
||||
app.Repo = repo
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) GetPageList(condition *entity.DataSyncTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
@@ -58,15 +66,22 @@ func (app *dataSyncAppImpl) GetPageList(condition *entity.DataSyncTaskQuery, pag
|
||||
func (app *dataSyncAppImpl) Save(ctx context.Context, taskEntity *entity.DataSyncTask) error {
|
||||
var err error
|
||||
if taskEntity.Id == 0 {
|
||||
// 新建时生成key
|
||||
taskEntity.TaskKey = uuid.New().String()
|
||||
err = app.Insert(ctx, taskEntity)
|
||||
} else {
|
||||
err = app.UpdateById(ctx, taskEntity)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app.AddCronJob(taskEntity)
|
||||
task, err := app.GetById(new(entity.DataSyncTask), taskEntity.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
app.AddCronJob(task)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -85,8 +100,10 @@ func (app *dataSyncAppImpl) AddCronJob(taskEntity *entity.DataSyncTask) {
|
||||
|
||||
// 根据状态添加新的任务
|
||||
if taskEntity.Status == entity.DataSyncTaskStatusEnable {
|
||||
taskId := taskEntity.Id
|
||||
scheduler.AddFunByKey(key, taskEntity.TaskCron, func() {
|
||||
if err := app.RunCronJob(taskEntity.Id); err != nil {
|
||||
logx.Infof("开始执行同步任务: %d", taskId)
|
||||
if err := app.RunCronJob(taskId); err != nil {
|
||||
logx.Errorf("定时执行数据同步任务失败: %s", err.Error())
|
||||
}
|
||||
})
|
||||
@@ -126,7 +143,23 @@ func (app *dataSyncAppImpl) RunCronJob(id uint64) error {
|
||||
updSql := ""
|
||||
orderSql := ""
|
||||
if task.UpdFieldVal != "0" && task.UpdFieldVal != "" && task.UpdField != "" {
|
||||
updSql = fmt.Sprintf("and %s > '%s'", task.UpdField, task.UpdFieldVal)
|
||||
srcConn, _ := app.dbApp.GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
|
||||
|
||||
task.UpdFieldVal = strings.Trim(task.UpdFieldVal, " ")
|
||||
// 把UpdFieldVal尝试转为int,如果可以转为int,则不添加引号,否则添加引号
|
||||
if _, err := strconv.Atoi(task.UpdFieldVal); err != nil {
|
||||
updSql = fmt.Sprintf("and %s > '%s'", task.UpdField, task.UpdFieldVal)
|
||||
} else {
|
||||
updSql = fmt.Sprintf("and %s > %s", task.UpdField, task.UpdFieldVal)
|
||||
}
|
||||
|
||||
// 如果是oracle且数据类型是时间类型,则需要加上to_date函数
|
||||
if srcConn.Info.Type == dbi.DbTypeOracle {
|
||||
// 用正则判断数据类型是时间
|
||||
if dateTimeReg.MatchString(task.UpdFieldVal) {
|
||||
updSql = fmt.Sprintf("and %s > to_date('%s','yyyy-mm-dd hh24:mi:ss')", task.UpdField, task.UpdFieldVal)
|
||||
}
|
||||
}
|
||||
orderSql = "order by " + task.UpdField + " asc "
|
||||
}
|
||||
// 组装查询sql
|
||||
@@ -153,13 +186,13 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
||||
}
|
||||
|
||||
// 获取源数据库连接
|
||||
srcConn, err := GetDbApp().GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
|
||||
srcConn, err := app.dbApp.GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
|
||||
if err != nil {
|
||||
return syncLog, errorx.NewBiz("连接源数据库失败: %s", err.Error())
|
||||
}
|
||||
|
||||
// 获取目标数据库连接
|
||||
targetConn, err := GetDbApp().GetDbConn(uint64(task.TargetDbId), task.TargetDbName)
|
||||
targetConn, err := app.dbApp.GetDbConn(uint64(task.TargetDbId), task.TargetDbName)
|
||||
if err != nil {
|
||||
return syncLog, errorx.NewBiz("连接目标数据库失败: %s", err.Error())
|
||||
}
|
||||
@@ -197,8 +230,8 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
||||
// 遍历columns 取task.UpdField的字段类型
|
||||
updFieldType = dbi.DataTypeString
|
||||
for _, column := range columns {
|
||||
if column.Name == task.UpdField {
|
||||
updFieldType = srcDialect.GetDataType(column.Type)
|
||||
if strings.EqualFold(strings.ToLower(column.Name), strings.ToLower(task.UpdField)) {
|
||||
updFieldType = srcDialect.GetDataConverter().GetDataType(column.Type)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -207,7 +240,7 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
||||
total++
|
||||
result = append(result, row)
|
||||
if total%batchSize == 0 {
|
||||
if err := app.srcData2TargetDb(result, fieldMap, updFieldType, task, srcDialect, targetConn, targetDbTx); err != nil {
|
||||
if err := app.srcData2TargetDb(result, fieldMap, columns, updFieldType, task, srcDialect, targetConn, targetDbTx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -229,7 +262,7 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
||||
|
||||
// 处理剩余的数据
|
||||
if len(result) > 0 {
|
||||
if err := app.srcData2TargetDb(result, fieldMap, updFieldType, task, srcDialect, targetConn, targetDbTx); err != nil {
|
||||
if err := app.srcData2TargetDb(result, fieldMap, queryColumns, updFieldType, task, srcDialect, targetConn, targetDbTx); err != nil {
|
||||
targetDbTx.Rollback()
|
||||
return syncLog, err
|
||||
}
|
||||
@@ -249,10 +282,16 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
||||
return syncLog, nil
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap []map[string]string, updFieldType dbi.DataType, task *entity.DataSyncTask, srcDialect dbi.Dialect, targetDbConn *dbi.DbConn, targetDbTx *sql.Tx) error {
|
||||
var data = make([]map[string]any, 0)
|
||||
func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap []map[string]string, columns []*dbi.QueryColumn, updFieldType dbi.DataType, task *entity.DataSyncTask, srcDialect dbi.Dialect, targetDbConn *dbi.DbConn, targetDbTx *sql.Tx) error {
|
||||
|
||||
// 遍历res,组装插入sql
|
||||
// 遍历src字段列表,取出字段对应的类型
|
||||
var srcColumnTypes = make(map[string]string)
|
||||
for _, column := range columns {
|
||||
srcColumnTypes[column.Name] = column.Type
|
||||
}
|
||||
|
||||
// 遍历res,组装数据
|
||||
var data = make([]map[string]any, 0)
|
||||
for _, record := range srcRes {
|
||||
var rowData = make(map[string]any)
|
||||
// 遍历字段映射, target字段的值为src字段取值
|
||||
@@ -265,18 +304,23 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
||||
|
||||
data = append(data, rowData)
|
||||
}
|
||||
// 解决字段大小写问题
|
||||
updFieldVal := srcRes[len(srcRes)-1][strings.ToUpper(task.UpdField)]
|
||||
if updFieldVal == "" || updFieldVal == nil {
|
||||
updFieldVal = srcRes[len(srcRes)-1][strings.ToLower(task.UpdField)]
|
||||
}
|
||||
|
||||
updFieldVal := fmt.Sprintf("%v", srcRes[len(srcRes)-1][task.UpdField])
|
||||
updFieldVal = srcDialect.FormatStrData(updFieldVal, updFieldType)
|
||||
task.UpdFieldVal = updFieldVal
|
||||
task.UpdFieldVal = srcDialect.GetDataConverter().FormatData(updFieldVal, updFieldType)
|
||||
|
||||
// 获取目标库字段数组
|
||||
targetWrapColumns := make([]string, 0)
|
||||
// 获取源库字段数组
|
||||
srcColumns := make([]string, 0)
|
||||
srcFieldTypes := make(map[string]dbi.DataType)
|
||||
for _, item := range fieldMap {
|
||||
targetField := item["target"]
|
||||
srcField := item["target"]
|
||||
srcFieldTypes[srcField] = srcDialect.GetDataConverter().GetDataType(srcColumnTypes[item["src"]])
|
||||
targetWrapColumns = append(targetWrapColumns, targetDbConn.Info.Type.QuoteIdentifier(targetField))
|
||||
srcColumns = append(srcColumns, srcField)
|
||||
}
|
||||
@@ -286,7 +330,9 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
||||
for _, record := range data {
|
||||
rawValue := make([]any, 0)
|
||||
for _, column := range srcColumns {
|
||||
rawValue = append(rawValue, record[column])
|
||||
// 某些情况,如oracle,需要转换时间类型的字符串为time类型
|
||||
res := srcDialect.GetDataConverter().ParseData(record[column], srcFieldTypes[column])
|
||||
rawValue = append(rawValue, res)
|
||||
}
|
||||
values = append(values, rawValue)
|
||||
}
|
||||
@@ -328,7 +374,7 @@ func (app *dataSyncAppImpl) endRunning(taskEntity *entity.DataSyncTask, log *ent
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) saveLog(log *entity.DataSyncLog) {
|
||||
app.dataSyncLogRepo.Save(context.Background(), log)
|
||||
app.dbDataSyncLogRepo.Save(context.Background(), log)
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) InitCronJob() {
|
||||
@@ -374,5 +420,5 @@ func (app *dataSyncAppImpl) InitCronJob() {
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) GetTaskLogList(condition *entity.DataSyncLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.dataSyncLogRepo.GetTaskLogList(condition, pageParam, toEntity, orderBy...)
|
||||
return app.dbDataSyncLogRepo.GetTaskLogList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"mayfly-go/internal/db/dbm"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
@@ -30,14 +33,17 @@ type Instance interface {
|
||||
GetDatabases(entity *entity.DbInstance) ([]string, error)
|
||||
}
|
||||
|
||||
func newInstanceApp(instanceRepo repository.Instance) Instance {
|
||||
app := new(instanceAppImpl)
|
||||
app.Repo = instanceRepo
|
||||
return app
|
||||
}
|
||||
|
||||
type instanceAppImpl struct {
|
||||
base.AppImpl[*entity.DbInstance, repository.Instance]
|
||||
|
||||
dbApp Db `inject:"DbApp"`
|
||||
backupApp *DbBackupApp `inject:"DbBackupApp"`
|
||||
restoreApp *DbRestoreApp `inject:"DbRestoreApp"`
|
||||
}
|
||||
|
||||
// 注入DbInstanceRepo
|
||||
func (app *instanceAppImpl) InjectDbInstanceRepo(repo repository.Instance) {
|
||||
app.Repo = repo
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库实例
|
||||
@@ -73,9 +79,11 @@ func (app *instanceAppImpl) Save(ctx context.Context, instanceEntity *entity.DbI
|
||||
|
||||
err := app.GetBy(oldInstance)
|
||||
if instanceEntity.Id == 0 {
|
||||
if instanceEntity.Password == "" {
|
||||
|
||||
if instanceEntity.Type != string(dbi.DbTypeSqlite) && instanceEntity.Password == "" {
|
||||
return errorx.NewBiz("密码不能为空")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return errorx.NewBiz("该数据库实例已存在")
|
||||
}
|
||||
@@ -95,8 +103,50 @@ func (app *instanceAppImpl) Save(ctx context.Context, instanceEntity *entity.DbI
|
||||
return app.UpdateById(ctx, instanceEntity)
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) Delete(ctx context.Context, id uint64) error {
|
||||
return app.DeleteById(ctx, id)
|
||||
func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error {
|
||||
instance, err := app.GetById(new(entity.DbInstance), instanceId, "name")
|
||||
biz.ErrIsNil(err, "获取数据库实例错误,数据库实例ID为: %d", instance.Id)
|
||||
|
||||
restore := &entity.DbRestore{
|
||||
DbInstanceId: instanceId,
|
||||
}
|
||||
err = app.restoreApp.restoreRepo.GetBy(restore)
|
||||
switch {
|
||||
case err == nil:
|
||||
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
break
|
||||
default:
|
||||
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||
}
|
||||
|
||||
backup := &entity.DbBackup{
|
||||
DbInstanceId: instanceId,
|
||||
}
|
||||
err = app.backupApp.backupRepo.GetBy(backup)
|
||||
switch {
|
||||
case err == nil:
|
||||
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
break
|
||||
default:
|
||||
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||
}
|
||||
|
||||
db := &entity.Db{
|
||||
InstanceId: instanceId,
|
||||
}
|
||||
err = app.dbApp.GetBy(db)
|
||||
switch {
|
||||
case err == nil:
|
||||
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库资源。", instance.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
break
|
||||
default:
|
||||
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||
}
|
||||
|
||||
return app.DeleteById(ctx, instanceId)
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) GetDatabases(ed *entity.DbInstance) ([]string, error) {
|
||||
@@ -2,71 +2,130 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func newDbRestoreApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbRestoreApp, error) {
|
||||
var jobs []*entity.DbRestore
|
||||
if err := repositories.Restore.ListToDo(&jobs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := scheduler.AddJob(context.Background(), false, entity.DbJobTypeRestore, jobs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
app := &DbRestoreApp{
|
||||
restoreRepo: repositories.Restore,
|
||||
instanceRepo: repositories.Instance,
|
||||
backupHistoryRepo: repositories.BackupHistory,
|
||||
restoreHistoryRepo: repositories.RestoreHistory,
|
||||
binlogHistoryRepo: repositories.BinlogHistory,
|
||||
dbApp: dbApp,
|
||||
scheduler: scheduler,
|
||||
}
|
||||
return app, nil
|
||||
type DbRestoreApp struct {
|
||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
type DbRestoreApp struct {
|
||||
restoreRepo repository.DbRestore
|
||||
instanceRepo repository.Instance
|
||||
backupHistoryRepo repository.DbBackupHistory
|
||||
restoreHistoryRepo repository.DbRestoreHistory
|
||||
binlogHistoryRepo repository.DbBinlogHistory
|
||||
dbApp Db
|
||||
scheduler *dbScheduler
|
||||
func (app *DbRestoreApp) Init() error {
|
||||
var jobs []*entity.DbRestore
|
||||
if err := app.restoreRepo.ListToDo(&jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Close() {
|
||||
app.scheduler.Close()
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Create(ctx context.Context, job *entity.DbRestore) error {
|
||||
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeRestore, job)
|
||||
func (app *DbRestoreApp) Create(ctx context.Context, jobs any) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.restoreRepo.AddJob(ctx, jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = app.scheduler.AddJob(ctx, jobs)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Update(ctx context.Context, job *entity.DbRestore) error {
|
||||
return app.scheduler.UpdateJob(ctx, job)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.restoreRepo.UpdateById(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = app.scheduler.UpdateJob(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Delete(ctx context.Context, jobId uint64) error {
|
||||
// todo: 删除数据库恢复历史文件
|
||||
return app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
history := &entity.DbRestoreHistory{
|
||||
DbRestoreId: jobId,
|
||||
}
|
||||
if err := app.restoreHistoryRepo.DeleteByCond(ctx, history); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.restoreRepo.DeleteById(ctx, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Enable(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.EnableJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.restoreRepo
|
||||
job := &entity.DbRestore{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
if job.IsExpired() {
|
||||
return errors.New("任务已过期")
|
||||
}
|
||||
_ = app.scheduler.EnableJob(ctx, job)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
|
||||
logx.Errorf("数据库恢复任务已启用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Disable(ctx context.Context, jobId uint64) error {
|
||||
return app.scheduler.DisableJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.restoreRepo
|
||||
job := &entity.DbRestore{}
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
|
||||
logx.Errorf("数据库恢复任务已禁用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库恢复任务
|
||||
func (app *DbRestoreApp) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
func (app *DbRestoreApp) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.restoreRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
// GetRestoresEnabled 获取数据库恢复任务
|
||||
func (app *DbRestoreApp) GetRestoresEnabled(toEntity any, backupHistoryId ...uint64) error {
|
||||
return app.restoreRepo.GetEnabledRestores(toEntity, backupHistoryId...)
|
||||
}
|
||||
|
||||
// GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
|
||||
func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
|
||||
return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames)
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/sync/singleflight"
|
||||
"gorm.io/gorm"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/runner"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -21,58 +23,35 @@ const (
|
||||
type dbScheduler struct {
|
||||
mutex sync.Mutex
|
||||
runner *runner.Runner[entity.DbJob]
|
||||
dbApp Db
|
||||
backupRepo repository.DbBackup
|
||||
backupHistoryRepo repository.DbBackupHistory
|
||||
restoreRepo repository.DbRestore
|
||||
restoreHistoryRepo repository.DbRestoreHistory
|
||||
binlogRepo repository.DbBinlog
|
||||
binlogHistoryRepo repository.DbBinlogHistory
|
||||
binlogTimes map[uint64]time.Time
|
||||
dbApp Db `inject:"DbApp"`
|
||||
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
||||
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
|
||||
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
||||
binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
|
||||
sfGroup singleflight.Group
|
||||
}
|
||||
|
||||
func newDbScheduler(repositories *repository.Repositories) (*dbScheduler, error) {
|
||||
scheduler := &dbScheduler{
|
||||
dbApp: dbApp,
|
||||
backupRepo: repositories.Backup,
|
||||
backupHistoryRepo: repositories.BackupHistory,
|
||||
restoreRepo: repositories.Restore,
|
||||
restoreHistoryRepo: repositories.RestoreHistory,
|
||||
binlogRepo: repositories.Binlog,
|
||||
binlogHistoryRepo: repositories.BinlogHistory,
|
||||
}
|
||||
func newDbScheduler() *dbScheduler {
|
||||
scheduler := &dbScheduler{}
|
||||
scheduler.runner = runner.NewRunner[entity.DbJob](maxRunning, scheduler.runJob,
|
||||
runner.WithScheduleJob[entity.DbJob](scheduler.scheduleJob),
|
||||
runner.WithRunnableJob[entity.DbJob](scheduler.runnableJob),
|
||||
runner.WithUpdateJob[entity.DbJob](scheduler.updateJob),
|
||||
)
|
||||
return scheduler, nil
|
||||
return scheduler
|
||||
}
|
||||
|
||||
func (s *dbScheduler) scheduleJob(job entity.DbJob) (time.Time, error) {
|
||||
return job.Schedule()
|
||||
}
|
||||
|
||||
func (s *dbScheduler) repo(typ entity.DbJobType) repository.DbJob {
|
||||
switch typ {
|
||||
case entity.DbJobTypeBackup:
|
||||
return s.backupRepo
|
||||
case entity.DbJobTypeRestore:
|
||||
return s.restoreRepo
|
||||
case entity.DbJobTypeBinlog:
|
||||
return s.binlogRepo
|
||||
default:
|
||||
panic(errors.New(fmt.Sprintf("无效的数据库任务类型: %v", typ)))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dbScheduler) UpdateJob(ctx context.Context, job entity.DbJob) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if err := s.repo(job.GetJobType()).UpdateById(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.runner.UpdateOrAdd(ctx, job)
|
||||
_ = s.runner.Update(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -80,61 +59,39 @@ func (s *dbScheduler) Close() {
|
||||
s.runner.Close()
|
||||
}
|
||||
|
||||
func (s *dbScheduler) AddJob(ctx context.Context, saving bool, jobType entity.DbJobType, jobs any) error {
|
||||
func (s *dbScheduler) AddJob(ctx context.Context, jobs any) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if saving {
|
||||
if err := s.repo(jobType).AddJob(ctx, jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
reflectValue := reflect.ValueOf(jobs)
|
||||
switch reflectValue.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
reflectLen := reflectValue.Len()
|
||||
for i := 0; i < reflectLen; i++ {
|
||||
job := reflectValue.Index(i).Interface().(entity.DbJob)
|
||||
job.SetJobType(jobType)
|
||||
_ = s.runner.Add(ctx, job)
|
||||
}
|
||||
default:
|
||||
job := jobs.(entity.DbJob)
|
||||
job.SetJobType(jobType)
|
||||
_ = s.runner.Add(ctx, job)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
||||
// todo: 删除数据库备份历史文件
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if err := s.repo(jobType).DeleteById(ctx, jobId); err != nil {
|
||||
if err := s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId)); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) EnableJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
||||
func (s *dbScheduler) EnableJob(ctx context.Context, job entity.DbJob) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
repo := s.repo(jobType)
|
||||
job := entity.NewDbJob(jobType)
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
job.SetEnabled(true)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.runner.Add(ctx, job)
|
||||
return nil
|
||||
}
|
||||
@@ -143,61 +100,37 @@ func (s *dbScheduler) DisableJob(ctx context.Context, jobType entity.DbJobType,
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
repo := s.repo(jobType)
|
||||
job := entity.NewDbJob(jobType)
|
||||
if err := repo.GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.runner.Remove(ctx, job.GetKey())
|
||||
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) StartJobNow(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
||||
func (s *dbScheduler) StartJobNow(ctx context.Context, job entity.DbJob) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
job := entity.NewDbJob(jobType)
|
||||
if err := s.repo(jobType).GetById(job, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return errors.New("任务未启用")
|
||||
}
|
||||
_ = s.runner.StartNow(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
|
||||
func (s *dbScheduler) backup(ctx context.Context, dbProgram dbi.DbProgram, backup *entity.DbBackup) error {
|
||||
id, err := NewIncUUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backup := job.(*entity.DbBackup)
|
||||
history := &entity.DbBackupHistory{
|
||||
Uuid: id.String(),
|
||||
DbBackupId: backup.Id,
|
||||
DbInstanceId: backup.DbInstanceId,
|
||||
DbName: backup.DbName,
|
||||
}
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(backup.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
binlogInfo, err := dbProgram.Backup(ctx, history)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
name := backup.Name
|
||||
if len(name) == 0 {
|
||||
name = backup.DbName
|
||||
name := backup.DbName
|
||||
if len(backup.Name) > 0 {
|
||||
name = fmt.Sprintf("%s-%s", backup.DbName, backup.Name)
|
||||
}
|
||||
history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime))
|
||||
history.CreateTime = now
|
||||
@@ -211,43 +144,43 @@ func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error {
|
||||
restore := job.(*entity.DbRestore)
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(restore.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
func (s *dbScheduler) singleFlightFetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, targetTime time.Time) error {
|
||||
key := strconv.FormatUint(instanceId, 10)
|
||||
for ctx.Err() == nil {
|
||||
c := s.sfGroup.DoChan(key, func() (interface{}, error) {
|
||||
if err := s.fetchBinlog(ctx, dbProgram, instanceId, true, targetTime); err != nil {
|
||||
return targetTime, err
|
||||
}
|
||||
return targetTime, nil
|
||||
})
|
||||
select {
|
||||
case res := <-c:
|
||||
if targetTime.Compare(res.Val.(time.Time)) <= 0 {
|
||||
return res.Err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restore(ctx context.Context, dbProgram dbi.DbProgram, restore *entity.DbRestore) error {
|
||||
if restore.PointInTime.Valid {
|
||||
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
||||
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(restore.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
latestBinlogSequence = binlogHistory.Sequence
|
||||
} else {
|
||||
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(restore.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
earliestBackupSequence = backupHistory.BinlogSequence
|
||||
}
|
||||
binlogFiles, err := dbProgram.FetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, restore.DbInstanceId, binlogFiles); err != nil {
|
||||
if err := s.fetchBinlog(ctx, dbProgram, restore.DbInstanceId, true, restore.PointInTime.Time); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := s.restoreBackupHistory(ctx, dbProgram, restore); err != nil {
|
||||
backupHistory := &entity.DbBackupHistory{}
|
||||
if err := s.backupHistoryRepo.GetById(backupHistory, restore.DbBackupHistoryId); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = errors.New("备份历史已删除")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -262,76 +195,74 @@ func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) {
|
||||
job.SetLastStatus(entity.DbJobRunning, nil)
|
||||
if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
|
||||
logx.Errorf("failed to update job status: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var errRun error
|
||||
switch typ := job.GetJobType(); typ {
|
||||
case entity.DbJobTypeBackup:
|
||||
errRun = s.backupMysql(ctx, job)
|
||||
case entity.DbJobTypeRestore:
|
||||
errRun = s.restoreMysql(ctx, job)
|
||||
case entity.DbJobTypeBinlog:
|
||||
errRun = s.fetchBinlogMysql(ctx, job)
|
||||
func (s *dbScheduler) updateJob(ctx context.Context, job entity.DbJob) error {
|
||||
switch t := job.(type) {
|
||||
case *entity.DbBackup:
|
||||
return s.backupRepo.UpdateById(ctx, t)
|
||||
case *entity.DbRestore:
|
||||
return s.restoreRepo.UpdateById(ctx, t)
|
||||
case *entity.DbBinlog:
|
||||
return s.binlogRepo.UpdateById(ctx, t)
|
||||
default:
|
||||
errRun = errors.New(fmt.Sprintf("无效的数据库任务类型: %v", typ))
|
||||
}
|
||||
status := entity.DbJobSuccess
|
||||
if errRun != nil {
|
||||
status = entity.DbJobFailed
|
||||
}
|
||||
job.SetLastStatus(status, errRun)
|
||||
if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
|
||||
logx.Errorf("failed to update job status: %v", err)
|
||||
return
|
||||
return fmt.Errorf("无效的数据库任务类型: %T", t)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dbScheduler) runnableJob(job entity.DbJob, next runner.NextJobFunc[entity.DbJob]) bool {
|
||||
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) error {
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(job.GetInstanceId())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram, err := conn.GetDialect().GetDbProgram()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t := job.(type) {
|
||||
case *entity.DbBackup:
|
||||
return s.backup(ctx, dbProgram, t)
|
||||
case *entity.DbRestore:
|
||||
return s.restore(ctx, dbProgram, t)
|
||||
case *entity.DbBinlog:
|
||||
return s.fetchBinlog(ctx, dbProgram, t.DbInstanceId, false, time.Now())
|
||||
default:
|
||||
return fmt.Errorf("无效的数据库任务类型: %T", t)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dbScheduler) runnableJob(job entity.DbJob, nextRunning runner.NextJobFunc[entity.DbJob]) (bool, error) {
|
||||
if job.IsExpired() {
|
||||
return false, runner.ErrJobExpired
|
||||
}
|
||||
const maxCountByInstanceId = 4
|
||||
const maxCountByDbName = 1
|
||||
var countByInstanceId, countByDbName int
|
||||
jobBase := job.GetJobBase()
|
||||
for item, ok := next(); ok; item, ok = next() {
|
||||
itemBase := item.GetJobBase()
|
||||
if jobBase.DbInstanceId == itemBase.DbInstanceId {
|
||||
for item, ok := nextRunning(); ok; item, ok = nextRunning() {
|
||||
if job.GetInstanceId() == item.GetInstanceId() {
|
||||
countByInstanceId++
|
||||
if countByInstanceId >= maxCountByInstanceId {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if relatedToBinlog(job.GetJobType()) {
|
||||
// todo: 恢复数据库前触发 BINLOG 同步,BINLOG 同步完成后才能恢复数据库
|
||||
if relatedToBinlog(item.GetJobType()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if job.GetDbName() == item.GetDbName() {
|
||||
countByDbName++
|
||||
if countByDbName >= maxCountByDbName {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if (job.GetJobType() == entity.DbJobTypeBinlog && item.GetJobType() == entity.DbJobTypeRestore) ||
|
||||
(job.GetJobType() == entity.DbJobTypeRestore && item.GetJobType() == entity.DbJobTypeBinlog) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func relatedToBinlog(typ entity.DbJobType) bool {
|
||||
return typ == entity.DbJobTypeRestore || typ == entity.DbJobTypeBinlog
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProgram, job *entity.DbRestore) error {
|
||||
func (s *dbScheduler) restorePointInTime(ctx context.Context, dbProgram dbi.DbProgram, job *entity.DbRestore) error {
|
||||
binlogHistory, err := s.binlogHistoryRepo.GetHistoryByTime(job.DbInstanceId, job.PointInTime.Time)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
position, err := program.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, job.PointInTime.Time)
|
||||
position, err := dbProgram.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, job.PointInTime.Time)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -340,7 +271,7 @@ func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProg
|
||||
Sequence: binlogHistory.Sequence,
|
||||
Position: position,
|
||||
}
|
||||
backupHistory, err := s.backupHistoryRepo.GetLatestHistory(job.DbInstanceId, job.DbName, target)
|
||||
backupHistory, err := s.backupHistoryRepo.GetLatestHistoryForBinlog(job.DbInstanceId, job.DbName, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -360,31 +291,77 @@ func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProg
|
||||
TargetPosition: target.Position,
|
||||
TargetTime: job.PointInTime.Time,
|
||||
}
|
||||
if err := program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid); err != nil {
|
||||
if err := dbProgram.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
return program.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo)
|
||||
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
|
||||
return err
|
||||
}
|
||||
// 由于 ReplayBinlog 未记录 BINLOG 事件,系统自动备份,避免数据丢失
|
||||
backup := &entity.DbBackup{
|
||||
DbInstanceId: backupHistory.DbInstanceId,
|
||||
DbName: backupHistory.DbName,
|
||||
Enabled: true,
|
||||
Repeated: false,
|
||||
StartTime: time.Now(),
|
||||
Interval: 0,
|
||||
Name: "系统备份",
|
||||
}
|
||||
backup.Id = backupHistory.DbBackupId
|
||||
if err := s.backup(ctx, dbProgram, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, job *entity.DbRestore) error {
|
||||
backupHistory := &entity.DbBackupHistory{}
|
||||
if err := s.backupHistoryRepo.GetById(backupHistory, job.DbBackupHistoryId); err != nil {
|
||||
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, backupHistory *entity.DbBackupHistory) (retErr error) {
|
||||
if _, err := s.backupHistoryRepo.UpdateRestoring(false, backupHistory.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
ok, err := s.backupHistoryRepo.UpdateRestoring(true, backupHistory.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_, err = s.backupHistoryRepo.UpdateRestoring(false, backupHistory.Id)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if retErr == nil {
|
||||
retErr = err
|
||||
return
|
||||
}
|
||||
retErr = fmt.Errorf("%w, %w", retErr, err)
|
||||
}()
|
||||
if !ok {
|
||||
return errors.New("关联的数据库备份历史已删除")
|
||||
}
|
||||
return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
|
||||
}
|
||||
|
||||
func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob) error {
|
||||
instanceId := backup.GetJobBase().DbInstanceId
|
||||
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
||||
func (s *dbScheduler) fetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, downloadLatestBinlogFile bool, targetTime time.Time) error {
|
||||
if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
|
||||
return err
|
||||
} else if !enabled {
|
||||
return errors.New("数据库未启用 BINLOG")
|
||||
}
|
||||
if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
|
||||
return err
|
||||
} else if !enabled {
|
||||
return errors.New("数据库未启用 BINLOG 行模式")
|
||||
}
|
||||
|
||||
earliestBackupSequence := int64(-1)
|
||||
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(instanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
latestBinlogSequence = binlogHistory.Sequence
|
||||
} else {
|
||||
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(instanceId)
|
||||
if downloadLatestBinlogFile && targetTime.Before(binlogHistory.LastEventTime) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !ok {
|
||||
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistoryForBinlog(instanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -393,14 +370,11 @@ func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob)
|
||||
}
|
||||
earliestBackupSequence = backupHistory.BinlogSequence
|
||||
}
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(instanceId)
|
||||
|
||||
// todo: 将循环从 dbProgram.FetchBinlogs 中提取出来,实现 BINLOG 同步成功后逐一保存 binlogHistory
|
||||
binlogFiles, err := dbProgram.FetchBinlogs(ctx, downloadLatestBinlogFile, earliestBackupSequence, binlogHistory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram := conn.GetDialect().GetDbProgram()
|
||||
binlogFiles, err := dbProgram.FetchBinlogs(ctx, false, earliestBackupSequence, latestBinlogSequence)
|
||||
if err == nil {
|
||||
err = s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, instanceId, binlogFiles)
|
||||
}
|
||||
return nil
|
||||
return s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, instanceId, binlogFiles)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ type dbSqlAppImpl struct {
|
||||
base.AppImpl[*entity.DbSql, repository.DbSql]
|
||||
}
|
||||
|
||||
func newDbSqlApp(dbSqlRepo repository.DbSql) DbSql {
|
||||
app := new(dbSqlAppImpl)
|
||||
app.Repo = dbSqlRepo
|
||||
return app
|
||||
// 注入DbSqlRepo
|
||||
func (d *dbSqlAppImpl) InjectDbSqlRepo(repo repository.DbSql) {
|
||||
d.Repo = repo
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user