mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-04 00:10: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 .
|
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 install && \
|
||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ COPY --from=fe-builder /mayfly/dist /mayfly/static/static
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
|
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
|
||||||
go build -a \
|
go build -a -ldflags=-w \
|
||||||
-o mayfly-go main.go
|
-o mayfly-go main.go
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
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",
|
"countup.js": "^2.7.0",
|
||||||
"cropperjs": "^1.5.11",
|
"cropperjs": "^1.5.11",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"element-plus": "^2.5.1",
|
"element-plus": "^2.5.5",
|
||||||
"js-base64": "^3.7.5",
|
"js-base64": "^3.7.5",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"splitpanes": "^3.1.5",
|
"splitpanes": "^3.1.5",
|
||||||
"sql-formatter": "^15.0.2",
|
"sql-formatter": "^15.0.2",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"vue": "^3.4.14",
|
"vue": "^3.4.15",
|
||||||
"vue-router": "^4.2.5",
|
"vue-router": "^4.2.5",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
@@ -49,13 +49,14 @@
|
|||||||
"@typescript-eslint/parser": "^6.7.4",
|
"@typescript-eslint/parser": "^6.7.4",
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "^5.0.3",
|
||||||
"@vue/compiler-sfc": "^3.4.14",
|
"@vue/compiler-sfc": "^3.4.14",
|
||||||
|
"code-inspector-plugin": "^0.4.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.35.0",
|
"eslint": "^8.35.0",
|
||||||
"eslint-plugin-vue": "^9.19.2",
|
"eslint-plugin-vue": "^9.19.2",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.1.0",
|
||||||
"sass": "^1.69.0",
|
"sass": "^1.69.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"vite": "^5.0.11",
|
"vite": "^5.0.12",
|
||||||
"vue-eslint-parser": "^9.4.0"
|
"vue-eslint-parser": "^9.4.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -55,11 +55,11 @@
|
|||||||
"unicode_decimal": 58905
|
"unicode_decimal": 58905
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "11617944",
|
"icon_id": "25271976",
|
||||||
"name": "oracle",
|
"name": "oracle",
|
||||||
"font_class": "oracle",
|
"font_class": "oracle",
|
||||||
"unicode": "e6ea",
|
"unicode": "e507",
|
||||||
"unicode_decimal": 59114
|
"unicode_decimal": 58631
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "8105644",
|
"icon_id": "8105644",
|
||||||
@@ -67,6 +67,41 @@
|
|||||||
"font_class": "mariadb",
|
"font_class": "mariadb",
|
||||||
"unicode": "e513",
|
"unicode": "e513",
|
||||||
"unicode_decimal": 58643
|
"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`,
|
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||||
|
|
||||||
// 系统版本
|
// 系统版本
|
||||||
version: 'v1.7.0',
|
version: 'v1.7.3',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-input v-model="cron" placeholder="可点击左边按钮进行可视化配置">
|
<el-input v-model="cron" placeholder="可点击左边按钮配置">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<el-button @click="showCron = true" icon="Pointer"></el-button>
|
<el-button @click="showCron = true" icon="Pointer"></el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ const open = (optionProps: MonacoEditorDialogProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editorRef.value?.format();
|
|
||||||
editorRef.value?.focus();
|
editorRef.value?.focus();
|
||||||
|
editorRef.value?.format();
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
state.dialogVisible = true;
|
state.dialogVisible = true;
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ const emit = defineEmits(['update:queryForm', 'update:selectionData', 'pageChang
|
|||||||
|
|
||||||
export interface PageTableProps {
|
export interface PageTableProps {
|
||||||
size?: string;
|
size?: string;
|
||||||
pageApi: Api; // 请求表格数据的 api
|
pageApi?: Api; // 请求表格数据的 api
|
||||||
columns: TableColumn[]; // 列配置项 ==> 必传
|
columns: TableColumn[]; // 列配置项 ==> 必传
|
||||||
showSelection?: boolean;
|
showSelection?: boolean;
|
||||||
selectable?: (row: any) => boolean; // 是否可选
|
selectable?: (row: any) => boolean; // 是否可选
|
||||||
@@ -257,7 +257,7 @@ const changeSimpleFormItem = (searchItem: SearchItem) => {
|
|||||||
nowSearchItem.value = 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.pageable,
|
||||||
props.pageApi,
|
props.pageApi,
|
||||||
queryForm,
|
queryForm,
|
||||||
@@ -288,6 +288,13 @@ watch(isShowSearch, () => {
|
|||||||
calcuTableHeight();
|
calcuTableHeight();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(newValue: any) => {
|
||||||
|
tableData = newValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
calcuTableHeight();
|
calcuTableHeight();
|
||||||
useEventListener(window, 'resize', calcuTableHeight);
|
useEventListener(window, 'resize', calcuTableHeight);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import 'xterm/css/xterm.css';
|
import 'xterm/css/xterm.css';
|
||||||
import { Terminal } from 'xterm';
|
import { ITheme, Terminal } from 'xterm';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
import { SearchAddon } from 'xterm-addon-search';
|
import { SearchAddon } from 'xterm-addon-search';
|
||||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||||
@@ -92,12 +92,13 @@ function init() {
|
|||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
disableStdin: false,
|
disableStdin: false,
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
|
fastScrollModifier: 'ctrl',
|
||||||
theme: {
|
theme: {
|
||||||
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
||||||
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
||||||
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
||||||
// cursorAccent: "red", // 光标停止颜色
|
// cursorAccent: "red", // 光标停止颜色
|
||||||
} as any,
|
} as ITheme,
|
||||||
});
|
});
|
||||||
term.open(terminalRef.value);
|
term.open(terminalRef.value);
|
||||||
|
|
||||||
@@ -105,7 +106,7 @@ function init() {
|
|||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
state.addon.fit = fitAddon;
|
state.addon.fit = fitAddon;
|
||||||
term.loadAddon(fitAddon);
|
term.loadAddon(fitAddon);
|
||||||
fitTerminal();
|
resize();
|
||||||
|
|
||||||
// 注册搜索组件
|
// 注册搜索组件
|
||||||
const searchAddon = new SearchAddon();
|
const searchAddon = new SearchAddon();
|
||||||
@@ -146,7 +147,7 @@ const onConnected = () => {
|
|||||||
state.status = TerminalStatus.Connected;
|
state.status = TerminalStatus.Connected;
|
||||||
|
|
||||||
// 注册窗口大小监听器
|
// 注册窗口大小监听器
|
||||||
useEventListener('resize', debounce(fitTerminal, 400));
|
useEventListener('resize', debounce(resize, 400));
|
||||||
|
|
||||||
focus();
|
focus();
|
||||||
|
|
||||||
@@ -158,17 +159,11 @@ const onConnected = () => {
|
|||||||
|
|
||||||
// 自适应终端
|
// 自适应终端
|
||||||
const fitTerminal = () => {
|
const fitTerminal = () => {
|
||||||
const dimensions = state.addon.fit && state.addon.fit.proposeDimensions();
|
resize();
|
||||||
if (!dimensions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (dimensions?.cols && dimensions?.rows) {
|
|
||||||
term.resize(dimensions.cols, dimensions.rows);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const focus = () => {
|
const focus = () => {
|
||||||
setTimeout(() => term.focus(), 400);
|
setTimeout(() => term.focus(), 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
@@ -265,7 +260,13 @@ const getStatus = (): TerminalStatus => {
|
|||||||
return state.status;
|
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>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#terminal-body {
|
#terminal-body {
|
||||||
|
|||||||
@@ -259,6 +259,10 @@ defineExpose({
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-dialog {
|
||||||
|
padding: 1px 1px;
|
||||||
|
}
|
||||||
|
|
||||||
// 取消body最大高度,否则全屏有问题
|
// 取消body最大高度,否则全屏有问题
|
||||||
.el-dialog__body {
|
.el-dialog__body {
|
||||||
max-height: 100% !important;
|
max-height: 100% !important;
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-search-dialog">
|
<div class="layout-search-dialog">
|
||||||
<el-dialog v-model="state.isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false">
|
<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="菜单搜索"
|
<el-autocomplete
|
||||||
prefix-icon="el-icon-search" ref="layoutMenuAutocompleteRef" @select="onHandleSelect" @blur="onSearchBlur">
|
v-model="state.menuQuery"
|
||||||
|
:fetch-suggestions="menuSearch"
|
||||||
|
placeholder="菜单搜索"
|
||||||
|
prefix-icon="el-icon-search"
|
||||||
|
ref="layoutMenuAutocompleteRef"
|
||||||
|
@select="onHandleSelect"
|
||||||
|
@blur="onSearchBlur"
|
||||||
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon class="el-input__icon">
|
<el-icon class="el-input__icon">
|
||||||
<search />
|
<search />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<div>
|
<div><SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}</div>
|
||||||
<SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-autocomplete>
|
</el-autocomplete>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -23,7 +28,7 @@ import { reactive, ref, nextTick } from 'vue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useRoutesList } from '@/store/routesList';
|
import { useRoutesList } from '@/store/routesList';
|
||||||
|
|
||||||
const layoutMenuAutocompleteRef: any = ref(null);;
|
const layoutMenuAutocompleteRef: any = ref(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const state: any = reactive({
|
const state: any = reactive({
|
||||||
isShowSearch: false,
|
isShowSearch: false,
|
||||||
@@ -54,8 +59,7 @@ const menuSearch = (queryString: any, cb: any) => {
|
|||||||
const createFilter = (queryString: any) => {
|
const createFilter = (queryString: any) => {
|
||||||
return (restaurant: any) => {
|
return (restaurant: any) => {
|
||||||
return (
|
return (
|
||||||
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
|
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
|
||||||
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -97,7 +101,7 @@ const onSearchBlur = () => {
|
|||||||
closeSearch();
|
closeSearch();
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({openSearch})
|
defineExpose({ openSearch });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -615,6 +615,9 @@ const setLocalThemeConfigStyle = () => {
|
|||||||
};
|
};
|
||||||
// 一键复制配置
|
// 一键复制配置
|
||||||
const onCopyConfigClick = (target: any) => {
|
const onCopyConfigClick = (target: any) => {
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let copyThemeConfig = getLocal('themeConfig');
|
let copyThemeConfig = getLocal('themeConfig');
|
||||||
copyThemeConfig.isDrawer = false;
|
copyThemeConfig.isDrawer = false;
|
||||||
const clipboard = new ClipboardJS(target, {
|
const clipboard = new ClipboardJS(target, {
|
||||||
|
|||||||
@@ -73,12 +73,28 @@ const currentTime = computed(() => {
|
|||||||
|
|
||||||
// 初始化数字滚动
|
// 初始化数字滚动
|
||||||
const initNumCountUp = async () => {
|
const initNumCountUp = async () => {
|
||||||
const res: any = await indexApi.getIndexCount.request();
|
indexApi.machineDashbord.request().then((res: any) => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
new CountUp('mongoNum', res.mongoNum).start();
|
new CountUp('machineNum', res.machineNum).start();
|
||||||
new CountUp('machineNum', res.machineNum).start();
|
});
|
||||||
new CountUp('dbNum', res.dbNum).start();
|
});
|
||||||
new CountUp('redisNum', res.redisNum).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';
|
import Api from '@/common/Api';
|
||||||
|
|
||||||
export const indexApi = {
|
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"
|
@node-contextmenu="nodeContextmenu"
|
||||||
>
|
>
|
||||||
<template #default="{ node, data }">
|
<template #default="{ node, data }">
|
||||||
<span>
|
<span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
|
||||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||||
<tag-info :tag-path="data.label" />
|
<tag-info :tag-path="data.label" />
|
||||||
</span>
|
</span>
|
||||||
@@ -25,7 +25,13 @@
|
|||||||
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
||||||
|
|
||||||
<span class="ml3" :title="data.labelRemark">
|
<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>
|
</span>
|
||||||
|
|
||||||
<slot :node="node" :data="data" name="suffix"></slot>
|
<slot :node="node" :data="data" name="suffix"></slot>
|
||||||
@@ -135,15 +141,29 @@ const loadNode = async (node: any, resolve: any) => {
|
|||||||
|
|
||||||
const treeNodeClick = (data: any) => {
|
const treeNodeClick = (data: any) => {
|
||||||
emit('nodeClick', data);
|
emit('nodeClick', data);
|
||||||
if (data.type.nodeClickFunc) {
|
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
||||||
data.type.nodeClickFunc(data);
|
data.type.nodeClickFunc(data);
|
||||||
}
|
}
|
||||||
// 关闭可能存在的右击菜单
|
// 关闭可能存在的右击菜单
|
||||||
contextmenuRef.value.closeContextmenu();
|
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) => {
|
const nodeContextmenu = (event: any, data: any) => {
|
||||||
|
if (data.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 加载当前节点是否需要显示右击菜单
|
// 加载当前节点是否需要显示右击菜单
|
||||||
let items = data.type.contextMenuItems;
|
let items = data.type.contextMenuItems;
|
||||||
if (!items || items.length == 0) {
|
if (!items || items.length == 0) {
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
@change="changeNode"
|
@change="changeNode"
|
||||||
>
|
>
|
||||||
|
<template #prefix="{ node, data }">
|
||||||
|
<slot name="iconPrefix" :node="node" :data="data" />
|
||||||
|
</template>
|
||||||
<template #default="{ node, data }">
|
<template #default="{ node, data }">
|
||||||
<span>
|
<span>
|
||||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||||
@@ -33,7 +36,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { NodeType, TagTreeNode } from './tag';
|
||||||
import TagInfo from './TagInfo.vue';
|
import TagInfo from './TagInfo.vue';
|
||||||
import { tagApi } from '../tag/api';
|
import { tagApi } from '../tag/api';
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ export class TagTreeNode {
|
|||||||
*/
|
*/
|
||||||
isLeaf: boolean = false;
|
isLeaf: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否禁用状态
|
||||||
|
*/
|
||||||
|
disabled: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 额外需要传递的参数
|
* 额外需要传递的参数
|
||||||
*/
|
*/
|
||||||
@@ -53,6 +58,11 @@ export class TagTreeNode {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withDisabled(disabled: boolean) {
|
||||||
|
this.disabled = disabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
withParams(params: any) {
|
withParams(params: any) {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
return this;
|
return this;
|
||||||
@@ -91,8 +101,14 @@ export class NodeType {
|
|||||||
|
|
||||||
loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
|
loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点点击事件
|
||||||
|
*/
|
||||||
nodeClickFunc: (node: TagTreeNode) => void;
|
nodeClickFunc: (node: TagTreeNode) => void;
|
||||||
|
|
||||||
|
// 节点双击事件
|
||||||
|
nodeDblclickFunc: (node: TagTreeNode) => void;
|
||||||
|
|
||||||
constructor(value: number) {
|
constructor(value: number) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
@@ -117,6 +133,16 @@ export class NodeType {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 赋值节点双击事件回调函数
|
||||||
|
* @param func 节点双击事件回调函数
|
||||||
|
* @returns this
|
||||||
|
*/
|
||||||
|
withNodeDblclickFunc(func: (node: TagTreeNode) => void) {
|
||||||
|
this.nodeDblclickFunc = func;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 赋值右击菜单按钮选项
|
* 赋值右击菜单按钮选项
|
||||||
* @param contextMenuItems 右击菜单按钮选项
|
* @param contextMenuItems 右击菜单按钮选项
|
||||||
|
|||||||
@@ -23,13 +23,16 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="name" label="任务名称">
|
<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>
|
||||||
<el-form-item prop="startTime" label="开始时间">
|
<el-form-item prop="startTime" label="开始时间">
|
||||||
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="intervalDay" label="备份周期">
|
<el-form-item prop="intervalDay" label="备份周期(天)">
|
||||||
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="备份周期(单位:天)"></el-input>
|
<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-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
@@ -92,6 +95,14 @@ const rules = {
|
|||||||
trigger: ['change', 'blur'],
|
trigger: ['change', 'blur'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
maxSaveDays: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
pattern: /^[0-9]\d*$/,
|
||||||
|
message: '请输入非负整数',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const backupForm: any = ref(null);
|
const backupForm: any = ref(null);
|
||||||
@@ -101,10 +112,11 @@ const state = reactive({
|
|||||||
id: 0,
|
id: 0,
|
||||||
dbId: 0,
|
dbId: 0,
|
||||||
dbNames: '',
|
dbNames: '',
|
||||||
name: null as any,
|
name: '',
|
||||||
intervalDay: null,
|
intervalDay: 1,
|
||||||
startTime: null as any,
|
startTime: null as any,
|
||||||
repeated: null as any,
|
repeated: true,
|
||||||
|
maxSaveDays: 0,
|
||||||
},
|
},
|
||||||
btnLoading: false,
|
btnLoading: false,
|
||||||
dbNamesSelected: [] as any,
|
dbNamesSelected: [] as any,
|
||||||
@@ -137,12 +149,14 @@ const init = (data: any) => {
|
|||||||
state.form.name = data.name;
|
state.form.name = data.name;
|
||||||
state.form.intervalDay = data.intervalDay;
|
state.form.intervalDay = data.intervalDay;
|
||||||
state.form.startTime = data.startTime;
|
state.form.startTime = data.startTime;
|
||||||
|
state.form.maxSaveDays = data.maxSaveDays;
|
||||||
} else {
|
} else {
|
||||||
state.editOrCreate = false;
|
state.editOrCreate = false;
|
||||||
state.form.name = '';
|
state.form.name = '';
|
||||||
state.form.intervalDay = null;
|
state.form.intervalDay = 1;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
||||||
|
state.form.maxSaveDays = 0;
|
||||||
getDbNamesWithoutBackup();
|
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="plus" @click="createDbBackup()">添加</el-button>
|
||||||
<el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</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="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
|
||||||
|
<el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #action="{ data }">
|
<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="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="disableDbBackup(data)" type="primary" link>禁用</el-button>
|
||||||
<el-button v-if="data.enabled" @click="startDbBackup(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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</page-table>
|
</page-table>
|
||||||
@@ -49,7 +51,7 @@ import { dbApi } from './api';
|
|||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { SearchItem } from '@/components/SearchForm';
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
|
||||||
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
|
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
@@ -72,10 +74,10 @@ const columns = [
|
|||||||
TableColumn.new('name', '任务名称'),
|
TableColumn.new('name', '任务名称'),
|
||||||
TableColumn.new('startTime', '启动时间').isTime(),
|
TableColumn.new('startTime', '启动时间').isTime(),
|
||||||
TableColumn.new('intervalDay', '备份周期'),
|
TableColumn.new('intervalDay', '备份周期'),
|
||||||
TableColumn.new('enabled', '是否启用'),
|
TableColumn.new('enabledDesc', '是否启用'),
|
||||||
TableColumn.new('lastResult', '执行结果'),
|
TableColumn.new('lastResult', '执行结果'),
|
||||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
TableColumn.new('lastTime', '执行时间').isTime(),
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight(),
|
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
|
||||||
];
|
];
|
||||||
|
|
||||||
const emptyQuery = {
|
const emptyQuery = {
|
||||||
@@ -168,5 +170,25 @@ const startDbBackup = async (data: any) => {
|
|||||||
await search();
|
await search();
|
||||||
ElMessage.success('备份任务启动成功');
|
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>
|
</script>
|
||||||
<style lang="scss"></style>
|
<style lang="scss"></style>
|
||||||
|
|||||||
@@ -62,8 +62,21 @@
|
|||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
|
<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: '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: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
|
||||||
<el-dropdown-item :command="{ type: 'dbRestore', data }" v-if="supportAction('dbRestore', data.type)"> 恢复 </el-dropdown-item>
|
备份任务
|
||||||
|
</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>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
@@ -131,6 +144,16 @@
|
|||||||
<db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
|
<db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
|
||||||
</el-dialog>
|
</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
|
<el-dialog
|
||||||
width="80%"
|
width="80%"
|
||||||
:title="`${dbRestoreDialog.title} - 数据库恢复`"
|
:title="`${dbRestoreDialog.title} - 数据库恢复`"
|
||||||
@@ -185,6 +208,7 @@ import { getDbDialect } from './dialect/index';
|
|||||||
import { getTagPathSearchItem } from '../component/tag';
|
import { getTagPathSearchItem } from '../component/tag';
|
||||||
import { SearchItem } from '@/components/SearchForm';
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
import DbBackupList from './DbBackupList.vue';
|
import DbBackupList from './DbBackupList.vue';
|
||||||
|
import DbBackupHistoryList from './DbBackupHistoryList.vue';
|
||||||
import DbRestoreList from './DbRestoreList.vue';
|
import DbRestoreList from './DbRestoreList.vue';
|
||||||
|
|
||||||
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
|
||||||
@@ -193,6 +217,8 @@ const perms = {
|
|||||||
base: 'db',
|
base: 'db',
|
||||||
saveDb: 'db:save',
|
saveDb: 'db:save',
|
||||||
delDb: 'db:del',
|
delDb: 'db:del',
|
||||||
|
backupDb: 'db:backup',
|
||||||
|
restoreDb: 'db:restore',
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.slot('instanceId', '实例', 'instanceSelect')];
|
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 actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -253,6 +280,13 @@ const state = reactive({
|
|||||||
dbs: [],
|
dbs: [],
|
||||||
dbId: 0,
|
dbId: 0,
|
||||||
},
|
},
|
||||||
|
// 数据库备份历史弹框
|
||||||
|
dbBackupHistoryDialog: {
|
||||||
|
title: '',
|
||||||
|
visible: false,
|
||||||
|
dbs: [],
|
||||||
|
dbId: 0,
|
||||||
|
},
|
||||||
// 数据库恢复弹框
|
// 数据库恢复弹框
|
||||||
dbRestoreDialog: {
|
dbRestoreDialog: {
|
||||||
title: '',
|
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 () => {
|
onMounted(async () => {
|
||||||
if (Object.keys(actionBtns).length > 0) {
|
if (Object.keys(actionBtns).length > 0) {
|
||||||
@@ -345,11 +380,15 @@ const handleMoreActionCommand = (commond: any) => {
|
|||||||
onDumpDbs(data);
|
onDumpDbs(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'dbBackup': {
|
case 'backupDb': {
|
||||||
onShowDbBackupDialog(data);
|
onShowDbBackupDialog(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'dbRestore': {
|
case 'backupHistory': {
|
||||||
|
onShowDbBackupHistoryDialog(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'restoreDb': {
|
||||||
onShowDbRestoreDialog(data);
|
onShowDbRestoreDialog(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -402,6 +441,13 @@ const onShowDbBackupDialog = async (row: any) => {
|
|||||||
state.dbBackupDialog.visible = true;
|
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) => {
|
const onShowDbRestoreDialog = async (row: any) => {
|
||||||
state.dbRestoreDialog.title = `${row.name}`;
|
state.dbRestoreDialog.title = `${row.name}`;
|
||||||
state.dbRestoreDialog.dbId = row.id;
|
state.dbRestoreDialog.dbId = row.id;
|
||||||
@@ -455,7 +501,7 @@ const supportAction = (action: string, dbType: string): boolean => {
|
|||||||
switch (dbType) {
|
switch (dbType) {
|
||||||
case DbType.mysql:
|
case DbType.mysql:
|
||||||
case DbType.mariadb:
|
case DbType.mariadb:
|
||||||
actions = ['dumpDb', 'dbBackup', 'dbRestore'];
|
actions = ['dumpDb', 'backupDb', 'restoreDb'];
|
||||||
}
|
}
|
||||||
return actions.includes(action);
|
return actions.includes(action);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,7 +35,13 @@
|
|||||||
clearable
|
clearable
|
||||||
class="w100"
|
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-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="startTime" label="开始时间">
|
<el-form-item prop="startTime" label="开始时间">
|
||||||
@@ -56,7 +62,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, ref, watch } from 'vue';
|
import { onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { dbApi } from './api';
|
import { dbApi } from './api';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
@@ -83,20 +89,30 @@ const visible = defineModel<boolean>('visible', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const validatePointInTime = (rule: any, value: any, callback: any) => {
|
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()) {
|
if (value > new Date()) {
|
||||||
callback(new Error('恢复时间点晚于当前时间'));
|
callback(new Error('恢复时间点晚于当前时间'));
|
||||||
return;
|
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 = {
|
const rules = {
|
||||||
@@ -110,7 +126,6 @@ const rules = {
|
|||||||
pointInTime: [
|
pointInTime: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
// message: '请选择恢复时间点',
|
|
||||||
validator: validatePointInTime,
|
validator: validatePointInTime,
|
||||||
trigger: ['change', 'blur'],
|
trigger: ['change', 'blur'],
|
||||||
},
|
},
|
||||||
@@ -146,7 +161,7 @@ const state = reactive({
|
|||||||
id: 0,
|
id: 0,
|
||||||
dbId: 0,
|
dbId: 0,
|
||||||
dbName: null as any,
|
dbName: null as any,
|
||||||
intervalDay: 1,
|
intervalDay: 0,
|
||||||
startTime: null as any,
|
startTime: null as any,
|
||||||
repeated: null as any,
|
repeated: null as any,
|
||||||
dbBackupId: null as any,
|
dbBackupId: null as any,
|
||||||
@@ -218,7 +233,8 @@ const init = async (data: any) => {
|
|||||||
} else {
|
} else {
|
||||||
state.form.dbName = '';
|
state.form.dbName = '';
|
||||||
state.editOrCreate = false;
|
state.editOrCreate = false;
|
||||||
state.form.intervalDay = 1;
|
state.form.intervalDay = 0;
|
||||||
|
state.form.repeated = false;
|
||||||
state.form.pointInTime = new Date();
|
state.form.pointInTime = new Date();
|
||||||
state.form.startTime = new Date();
|
state.form.startTime = new Date();
|
||||||
state.histories = [];
|
state.histories = [];
|
||||||
@@ -237,6 +253,12 @@ const getDbNamesWithoutRestore = async () => {
|
|||||||
const btnOk = async () => {
|
const btnOk = async () => {
|
||||||
restoreForm.value.validate(async (valid: any) => {
|
restoreForm.value.validate(async (valid: any) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
|
await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
|
||||||
if (state.restoreMode == 'point-in-time') {
|
if (state.restoreMode == 'point-in-time') {
|
||||||
state.form.dbBackupId = 0;
|
state.form.dbBackupId = 0;
|
||||||
state.form.dbBackupHistoryId = 0;
|
state.form.dbBackupHistoryId = 0;
|
||||||
@@ -245,13 +267,14 @@ const btnOk = async () => {
|
|||||||
state.form.pointInTime = null;
|
state.form.pointInTime = null;
|
||||||
}
|
}
|
||||||
state.form.repeated = false;
|
state.form.repeated = false;
|
||||||
|
state.form.intervalDay = 0;
|
||||||
const reqForm = { ...state.form };
|
const reqForm = { ...state.form };
|
||||||
let api = dbApi.createDbRestore;
|
let api = dbApi.createDbRestore;
|
||||||
if (props.data) {
|
if (props.data) {
|
||||||
api = dbApi.saveDbRestore;
|
api = dbApi.saveDbRestore;
|
||||||
}
|
}
|
||||||
api.request(reqForm).then(() => {
|
api.request(reqForm).then(() => {
|
||||||
ElMessage.success('保存成功');
|
ElMessage.success('成功创建数据库恢复任务');
|
||||||
emit('val-change', state.form);
|
emit('val-change', state.form);
|
||||||
state.btnLoading = true;
|
state.btnLoading = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -21,12 +21,14 @@
|
|||||||
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
|
<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-play" @click="enableDbRestore(null)">启用</el-button>
|
||||||
<el-button type="primary" icon="video-pause" @click="disableDbRestore(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>
|
||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
|
<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="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="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
|
||||||
|
<el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</page-table>
|
</page-table>
|
||||||
|
|
||||||
@@ -49,7 +51,7 @@
|
|||||||
infoDialog.data.dbBackupHistoryName
|
infoDialog.data.dbBackupHistoryName
|
||||||
}}</el-descriptions-item>
|
}}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</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="执行时间">{{ dateFormat(infoDialog.data.lastTime) }}</el-descriptions-item>
|
||||||
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
|
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
@@ -63,7 +65,7 @@ import { dbApi } from './api';
|
|||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { SearchItem } from '@/components/SearchForm';
|
import { SearchItem } from '@/components/SearchForm';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { dateFormat } from '@/common/utils/date';
|
import { dateFormat } from '@/common/utils/date';
|
||||||
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
@@ -85,7 +87,7 @@ const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
|||||||
const columns = [
|
const columns = [
|
||||||
TableColumn.new('dbName', '数据库名称'),
|
TableColumn.new('dbName', '数据库名称'),
|
||||||
TableColumn.new('startTime', '启动时间').isTime(),
|
TableColumn.new('startTime', '启动时间').isTime(),
|
||||||
TableColumn.new('enabled', '是否启用'),
|
TableColumn.new('enabledDesc', '是否启用'),
|
||||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
TableColumn.new('lastTime', '执行时间').isTime(),
|
||||||
TableColumn.new('lastResult', '执行结果'),
|
TableColumn.new('lastResult', '执行结果'),
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
|
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
|
||||||
@@ -135,19 +137,39 @@ const createDbRestore = async () => {
|
|||||||
state.dbRestoreEditDialog.visible = true;
|
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) => {
|
const showDbRestore = async (data: any) => {
|
||||||
state.infoDialog.data = data;
|
state.infoDialog.data = data;
|
||||||
state.infoDialog.visible = true;
|
state.infoDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const enableDbRestore = async (data: any) => {
|
const enableDbRestore = async (data: any) => {
|
||||||
let restoreId: String;
|
let restoreId: string;
|
||||||
if (data) {
|
if (data) {
|
||||||
restoreId = data.id;
|
restoreId = data.id;
|
||||||
} else if (state.selectedData.length > 0) {
|
} else if (state.selectedData.length > 0) {
|
||||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('请选择需要启用的恢复任务');
|
ElMessage.error('请选择需要启用的数据库恢复任务');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||||
@@ -156,13 +178,13 @@ const enableDbRestore = async (data: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const disableDbRestore = async (data: any) => {
|
const disableDbRestore = async (data: any) => {
|
||||||
let restoreId: String;
|
let restoreId: string;
|
||||||
if (data) {
|
if (data) {
|
||||||
restoreId = data.id;
|
restoreId = data.id;
|
||||||
} else if (state.selectedData.length > 0) {
|
} else if (state.selectedData.length > 0) {
|
||||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('请选择需要禁用的恢复任务');
|
ElMessage.error('请选择需要禁用的数据库恢复任务');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { dbApi } from './api';
|
||||||
import { DbSqlExecTypeEnum } from './enums';
|
import { DbSqlExecTypeEnum } from './enums';
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
@@ -120,6 +120,12 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
|||||||
const primaryKey = getPrimaryKey(columns);
|
const primaryKey = getPrimaryKey(columns);
|
||||||
const oldValue = JSON.parse(sqlExecLog.oldValue);
|
const oldValue = JSON.parse(sqlExecLog.oldValue);
|
||||||
|
|
||||||
|
let schema = '';
|
||||||
|
let dbArr = sqlExecLog.db.split('/');
|
||||||
|
if (dbArr.length == 2) {
|
||||||
|
schema = dbArr[1] + '.';
|
||||||
|
}
|
||||||
|
|
||||||
const rollbackSqls = [];
|
const rollbackSqls = [];
|
||||||
if (sqlExecLog.type == DbSqlExecTypeEnum.Update.value) {
|
if (sqlExecLog.type == DbSqlExecTypeEnum.Update.value) {
|
||||||
for (let ov of oldValue) {
|
for (let ov of oldValue) {
|
||||||
@@ -130,7 +136,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
|||||||
}
|
}
|
||||||
setItems.push(`${key} = ${wrapValue(ov[key])}`);
|
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) {
|
} else if (sqlExecLog.type == DbSqlExecTypeEnum.Delete.value) {
|
||||||
const columnNames = columns.map((c: any) => c.columnName);
|
const columnNames = columns.map((c: any) => c.columnName);
|
||||||
@@ -139,7 +145,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
|
|||||||
for (let column of columnNames) {
|
for (let column of columnNames) {
|
||||||
values.push(wrapValue(ov[column]));
|
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 getPrimaryKey = (columns: any) => {
|
||||||
const col = columns.find((c: any) => c.columnKey == 'PRI');
|
const col = columns.find((c: any) => c.isPrimaryKey);
|
||||||
if (col) {
|
if (col) {
|
||||||
return col.columnName;
|
return col.columnName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,22 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="type" label="类型" required>
|
<el-form-item prop="type" label="类型" required>
|
||||||
<el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
|
<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">
|
<el-option
|
||||||
<SvgIcon :name="getDbDialect(dt.type).getInfo().icon" :size="18" />
|
v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
|
||||||
{{ dt.label }}
|
:key="key"
|
||||||
|
:value="dbTypeAndDialect[0]"
|
||||||
|
:label="dbTypeAndDialect[1].getInfo().name"
|
||||||
|
>
|
||||||
|
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
|
||||||
|
{{ dbTypeAndDialect[1].getInfo().name }}
|
||||||
</el-option>
|
</el-option>
|
||||||
|
|
||||||
|
<template #prefix>
|
||||||
|
<SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
|
||||||
|
</template>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</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-col :span="18">
|
||||||
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
|
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -24,13 +33,18 @@
|
|||||||
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
|
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-form-item>
|
</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-form-item v-if="form.type === DbType.oracle" prop="sid" label="SID">
|
||||||
<el-input v-model.trim="form.sid" placeholder="请输入服务id"></el-input>
|
<el-input v-model.trim="form.sid" placeholder="请输入服务id"></el-input>
|
||||||
</el-form-item>
|
</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-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
|
||||||
</el-form-item>
|
</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">
|
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
|
||||||
<template v-if="form.id && form.id != 0" #suffix>
|
<template v-if="form.id && form.id != 0" #suffix>
|
||||||
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
|
<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 { notBlank } from '@/common/assert';
|
||||||
import { RsaEncrypt } from '@/common/rsa';
|
import { RsaEncrypt } from '@/common/rsa';
|
||||||
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
|
||||||
import { DbType, getDbDialect } from './dialect';
|
import { DbType, getDbDialect, getDbDialectMap } from './dialect';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -148,35 +162,12 @@ const rules = {
|
|||||||
|
|
||||||
const dbForm: any = ref(null);
|
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({
|
const state = reactive({
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
tabActiveName: 'basic',
|
tabActiveName: 'basic',
|
||||||
form: {
|
form: {
|
||||||
id: null,
|
id: null,
|
||||||
type: null,
|
type: '',
|
||||||
name: null,
|
name: null,
|
||||||
host: '',
|
host: '',
|
||||||
port: null,
|
port: null,
|
||||||
@@ -187,17 +178,17 @@ const state = reactive({
|
|||||||
remark: '',
|
remark: '',
|
||||||
sshTunnelMachineId: null as any,
|
sshTunnelMachineId: null as any,
|
||||||
},
|
},
|
||||||
subimtForm: {},
|
submitForm: {},
|
||||||
// 原密码
|
// 原密码
|
||||||
pwd: '',
|
pwd: '',
|
||||||
// 原用户名
|
// 原用户名
|
||||||
oldUserName: null,
|
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: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(submitForm);
|
||||||
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(subimtForm);
|
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
|
||||||
|
|
||||||
watch(props, (newValue: any) => {
|
watch(props, (newValue: any) => {
|
||||||
state.dialogVisible = newValue.visible;
|
state.dialogVisible = newValue.visible;
|
||||||
@@ -209,7 +200,7 @@ watch(props, (newValue: any) => {
|
|||||||
state.form = { ...newValue.data };
|
state.form = { ...newValue.data };
|
||||||
state.oldUserName = state.form.username;
|
state.oldUserName = state.form.username;
|
||||||
} else {
|
} else {
|
||||||
state.form = { port: null } as any;
|
state.form = { port: null, type: DbType.mysql } as any;
|
||||||
state.oldUserName = null;
|
state.oldUserName = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -240,17 +231,19 @@ const testConn = async () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.subimtForm = await getReqForm();
|
state.submitForm = await getReqForm();
|
||||||
await testConnExec();
|
await testConnExec();
|
||||||
ElMessage.success('连接成功');
|
ElMessage.success('连接成功');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const btnOk = async () => {
|
const btnOk = async () => {
|
||||||
if (!state.form.id) {
|
if (state.form.type !== DbType.sqlite) {
|
||||||
notBlank(state.form.password, '新增操作,密码不可为空');
|
if (!state.form.id) {
|
||||||
} else if (state.form.username != state.oldUserName) {
|
notBlank(state.form.password, '新增操作,密码不可为空');
|
||||||
notBlank(state.form.password, '已修改用户名,请输入密码');
|
} else if (state.form.username != state.oldUserName) {
|
||||||
|
notBlank(state.form.password, '已修改用户名,请输入密码');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dbForm.value.validate(async (valid: boolean) => {
|
dbForm.value.validate(async (valid: boolean) => {
|
||||||
@@ -259,7 +252,7 @@ const btnOk = async () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.subimtForm = await getReqForm();
|
state.submitForm = await getReqForm();
|
||||||
await saveInstanceExec();
|
await saveInstanceExec();
|
||||||
ElMessage.success('保存成功');
|
ElMessage.success('保存成功');
|
||||||
emit('val-change', state.form);
|
emit('val-change', state.form);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #type="{ data }">
|
<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" />
|
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
<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.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>
|
</template>
|
||||||
</page-table>
|
</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 actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
|
||||||
@@ -150,14 +151,26 @@ const editInstance = async (data: any) => {
|
|||||||
state.instanceEditDialog.visible = true;
|
state.instanceEditDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteInstance = async () => {
|
const deleteInstance = async (data: any) => {
|
||||||
try {
|
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: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning',
|
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('删除成功');
|
ElMessage.success('删除成功');
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<el-descriptions-item label-align="right">
|
<el-descriptions-item label-align="right">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
<SvgIcon :name="getDbDialect(nowDbInst.type).getInfo().icon" :size="18" />
|
<SvgIcon :name="nowDbInst.getDialect().getInfo().icon" :size="18" />
|
||||||
实例
|
实例
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -151,12 +151,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</Pane>
|
</Pane>
|
||||||
</Splitpanes>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
|
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { formatByteSize } from '@/common/utils/format';
|
import { formatByteSize } from '@/common/utils/format';
|
||||||
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
|
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
|
||||||
import { NodeType, TagTreeNode } from '../component/tag';
|
import { NodeType, TagTreeNode } from '../component/tag';
|
||||||
@@ -165,12 +175,13 @@ import { dbApi } from './api';
|
|||||||
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { ContextmenuItem } from '@/components/contextmenu';
|
import { ContextmenuItem } from '@/components/contextmenu';
|
||||||
import { DbType, getDbDialect } from './dialect/index';
|
import { getDbDialect, schemaDbTypes } from './dialect/index';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
import { Pane, Splitpanes } from 'splitpanes';
|
import { Pane, Splitpanes } from 'splitpanes';
|
||||||
import { useEventListener } from '@vueuse/core';
|
import { useEventListener } from '@vueuse/core';
|
||||||
|
|
||||||
|
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
|
||||||
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
|
||||||
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
|
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
|
||||||
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
|
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
|
||||||
@@ -218,21 +229,25 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// tagpath 节点类型
|
const ContextmenuItemRefresh = new ContextmenuItem('refresh', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key));
|
||||||
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 [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
// tagpath 节点类型
|
||||||
await sleep(100);
|
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
|
||||||
return dbInfos?.map((x: any) => {
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
x.tagPath = parentNode.key;
|
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
|
||||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
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) => {
|
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)
|
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
||||||
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
|
.withContextMenuItems([ContextmenuItemRefresh])
|
||||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
|
params.parentKey = parentNode.key;
|
||||||
// pg类数据库会多一层schema
|
// pg类数据库会多一层schema
|
||||||
if (params.type == DbType.postgresql || params.type === DbType.dm || params.type === DbType.oracle) {
|
if (schemaDbTypes.includes(params.type)) {
|
||||||
const params = parentNode.params;
|
|
||||||
const { id, db } = params;
|
const { id, db } = params;
|
||||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||||
return schemaNames.map((sn: any) => {
|
return schemaNames.map((sn: any) => {
|
||||||
@@ -269,33 +284,37 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
|
|||||||
nParams.schema = sn;
|
nParams.schema = sn;
|
||||||
nParams.db = nParams.db + '/' + sn;
|
nParams.db = nParams.db + '/' + sn;
|
||||||
nParams.dbs = schemaNames;
|
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 [
|
return NodeTypeTables(params);
|
||||||
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),
|
|
||||||
];
|
|
||||||
})
|
})
|
||||||
.withNodeClickFunc(nodeClickChangeDb);
|
.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模式
|
// postgres schema模式
|
||||||
const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema)
|
const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema)
|
||||||
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
|
.withContextMenuItems([ContextmenuItemRefresh])
|
||||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
return [
|
params.parentKey = parentNode.key;
|
||||||
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
|
return NodeTypeTables(params);
|
||||||
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
|
|
||||||
];
|
|
||||||
})
|
})
|
||||||
.withNodeClickFunc(nodeClickChangeDb);
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
// 数据库表菜单节点
|
// 数据库表菜单节点
|
||||||
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||||
.withContextMenuItems([
|
.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) => {
|
new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
|
||||||
const params = data.params;
|
const params = data.params;
|
||||||
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key });
|
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) => {
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
let { id, db } = params;
|
let { id, db, type } = params;
|
||||||
// 获取当前库的所有表信息
|
// 获取当前库的所有表信息
|
||||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||||
state.reloadStatus = false;
|
state.reloadStatus = false;
|
||||||
let dbTableSize = 0;
|
let dbTableSize = 0;
|
||||||
const tablesNode = tables.map((x: any) => {
|
const tablesNode = tables.map((x: any) => {
|
||||||
dbTableSize += x.dataLength + x.indexLength;
|
const tableSize = x.dataLength + x.indexLength;
|
||||||
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable)
|
dbTableSize += tableSize;
|
||||||
|
const key = `${id}.${db}.${x.tableName}`;
|
||||||
|
return new TagTreeNode(key, x.tableName, NodeTypeTable)
|
||||||
.withIsLeaf(true)
|
.withIsLeaf(true)
|
||||||
.withParams({
|
.withParams({
|
||||||
id,
|
id,
|
||||||
db,
|
db,
|
||||||
|
type,
|
||||||
|
key: key,
|
||||||
|
parentKey: parentNode.key,
|
||||||
tableName: x.tableName,
|
tableName: x.tableName,
|
||||||
tableComment: x.tableComment,
|
tableComment: x.tableComment,
|
||||||
size: formatByteSize(x.dataLength + x.indexLength, 1),
|
size: tableSize == 0 ? '' : formatByteSize(tableSize, 1),
|
||||||
})
|
})
|
||||||
.withIcon(TableIcon)
|
.withIcon(TableIcon)
|
||||||
.withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
|
.withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
|
||||||
});
|
});
|
||||||
// 设置父节点参数的表大小
|
// 设置父节点参数的表大小
|
||||||
parentNode.params.dbTableSize = formatByteSize(dbTableSize);
|
parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
|
||||||
return tablesNode;
|
return tablesNode;
|
||||||
})
|
})
|
||||||
.withNodeClickFunc(nodeClickChangeDb);
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
@@ -340,22 +364,23 @@ const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
|
|||||||
return sqls.map((x: any) => {
|
return sqls.map((x: any) => {
|
||||||
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
|
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
|
||||||
.withIsLeaf(true)
|
.withIsLeaf(true)
|
||||||
.withParams({
|
.withParams({ id, db, dbs, sqlName: x.name })
|
||||||
id,
|
|
||||||
db,
|
|
||||||
dbs,
|
|
||||||
sqlName: x.name,
|
|
||||||
})
|
|
||||||
.withIcon(SqlIcon);
|
.withIcon(SqlIcon);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.withNodeClickFunc(nodeClickChangeDb);
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
// 表节点类型
|
// 表节点类型
|
||||||
const NodeTypeTable = new NodeType(SqlExecNodeType.Table).withNodeClickFunc((nodeData: TagTreeNode) => {
|
const NodeTypeTable = new NodeType(SqlExecNodeType.Table)
|
||||||
const params = nodeData.params;
|
.withContextMenuItems([
|
||||||
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
|
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模板节点类型
|
// sql模板节点类型
|
||||||
const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
|
const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
|
||||||
@@ -385,9 +410,19 @@ const state = reactive({
|
|||||||
loading: true,
|
loading: true,
|
||||||
version: '',
|
version: '',
|
||||||
},
|
},
|
||||||
|
tableCreateDialog: {
|
||||||
|
visible: false,
|
||||||
|
title: '',
|
||||||
|
activeName: '',
|
||||||
|
dbId: 0,
|
||||||
|
db: '',
|
||||||
|
dbType: '',
|
||||||
|
data: {},
|
||||||
|
parentKey: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { nowDbInst } = toRefs(state);
|
const { nowDbInst, tableCreateDialog } = toRefs(state);
|
||||||
|
|
||||||
const serverInfoReqParam = ref({
|
const serverInfoReqParam = ref({
|
||||||
instanceId: 0,
|
instanceId: 0,
|
||||||
@@ -408,7 +443,7 @@ onBeforeUnmount(() => {
|
|||||||
* 设置editor高度和数据表高度
|
* 设置editor高度和数据表高度
|
||||||
*/
|
*/
|
||||||
const setHeight = () => {
|
const setHeight = () => {
|
||||||
state.dataTabsTableHeight = window.innerHeight - 270 + 'px';
|
state.dataTabsTableHeight = window.innerHeight - 253 + 'px';
|
||||||
state.tablesOpHeight = window.innerHeight - 225 + 'px';
|
state.tablesOpHeight = window.innerHeight - 225 + 'px';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -603,6 +638,85 @@ const reloadNode = (nodeKey: string) => {
|
|||||||
tagTreeRef.value.reloadNode(nodeKey);
|
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
|
<db-select-tree
|
||||||
placeholder="请选择源数据库"
|
placeholder="请选择源数据库"
|
||||||
v-model:db-id="form.srcDbId"
|
v-model:db-id="form.srcDbId"
|
||||||
|
v-model:inst-name="form.srcInstName"
|
||||||
v-model:db-name="form.srcDbName"
|
v-model:db-name="form.srcDbName"
|
||||||
v-model:tag-path="form.srcTagPath"
|
v-model:tag-path="form.srcTagPath"
|
||||||
|
v-model:db-type="form.srcDbType"
|
||||||
@select-db="onSelectSrcDb"
|
@select-db="onSelectSrcDb"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -55,8 +57,10 @@
|
|||||||
<db-select-tree
|
<db-select-tree
|
||||||
placeholder="请选择目标数据库"
|
placeholder="请选择目标数据库"
|
||||||
v-model:db-id="form.targetDbId"
|
v-model:db-id="form.targetDbId"
|
||||||
|
v-model:inst-name="form.targetInstName"
|
||||||
v-model:db-name="form.targetDbName"
|
v-model:db-name="form.targetDbName"
|
||||||
v-model:tag-path="form.targetTagPath"
|
v-model:tag-path="form.targetTagPath"
|
||||||
|
v-model:db-type="form.targetDbType"
|
||||||
@select-db="onSelectTargetDb"
|
@select-db="onSelectTargetDb"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -181,7 +185,7 @@ import { ElMessage } from 'element-plus';
|
|||||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
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';
|
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -226,12 +230,16 @@ type FormData = {
|
|||||||
taskName?: string;
|
taskName?: string;
|
||||||
taskCron: string;
|
taskCron: string;
|
||||||
srcDbId?: number;
|
srcDbId?: number;
|
||||||
|
srcInstName?: string;
|
||||||
srcDbName?: string;
|
srcDbName?: string;
|
||||||
|
srcDbType?: string;
|
||||||
srcTagPath?: string;
|
srcTagPath?: string;
|
||||||
targetDbId?: number;
|
targetDbId?: number;
|
||||||
|
targetInstName?: string;
|
||||||
targetDbName?: string;
|
targetDbName?: string;
|
||||||
targetTagPath?: string;
|
targetTagPath?: string;
|
||||||
targetTableName?: string;
|
targetTableName?: string;
|
||||||
|
targetDbType?: string;
|
||||||
dataSql?: string;
|
dataSql?: string;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
updField?: string;
|
updField?: string;
|
||||||
@@ -245,7 +253,7 @@ const basicFormData = {
|
|||||||
targetDbId: -1,
|
targetDbId: -1,
|
||||||
dataSql: 'select * from',
|
dataSql: 'select * from',
|
||||||
pageSize: 1000,
|
pageSize: 1000,
|
||||||
updField: 'id',
|
updField: '',
|
||||||
updFieldVal: '0',
|
updFieldVal: '0',
|
||||||
fieldMap: [{ src: 'a', target: 'b' }],
|
fieldMap: [{ src: 'a', target: 'b' }],
|
||||||
status: 1,
|
status: 1,
|
||||||
@@ -302,6 +310,8 @@ watch(dialogVisible, async (newValue: boolean) => {
|
|||||||
// 初始化实例
|
// 初始化实例
|
||||||
db.databases = db.database?.split(' ').sort() || [];
|
db.databases = db.database?.split(' ').sort() || [];
|
||||||
state.srcDbInst = DbInst.getOrNewInst(db);
|
state.srcDbInst = DbInst.getOrNewInst(db);
|
||||||
|
state.form.srcDbType = state.srcDbInst.type;
|
||||||
|
state.form.srcInstName = db.instanceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化target数据源
|
// 初始化target数据源
|
||||||
@@ -312,6 +322,8 @@ watch(dialogVisible, async (newValue: boolean) => {
|
|||||||
// 初始化实例
|
// 初始化实例
|
||||||
db.databases = db.database?.split(' ').sort() || [];
|
db.databases = db.database?.split(' ').sort() || [];
|
||||||
state.targetDbInst = DbInst.getOrNewInst(db);
|
state.targetDbInst = DbInst.getOrNewInst(db);
|
||||||
|
state.form.targetDbType = state.targetDbInst.type;
|
||||||
|
state.form.targetInstName = db.instanceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetDbId && state.form.targetDbName) {
|
if (targetDbId && state.form.targetDbName) {
|
||||||
@@ -396,8 +408,8 @@ const handleGetSrcFields = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 判断sql是否是查询语句
|
// 判断sql是否是查询语句
|
||||||
if (!/^select/i.test(state.form.dataSql!)) {
|
if (!/^select/i.test(state.form.dataSql.trim()!)) {
|
||||||
let msg = 'sql语句错误,请输入查询语句';
|
let msg = 'sql语句错误,请输入select语句';
|
||||||
ElMessage.warning(msg);
|
ElMessage.warning(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -410,10 +422,16 @@ const handleGetSrcFields = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 执行sql
|
// 执行sql
|
||||||
|
// oracle的分页关键字不一样
|
||||||
|
let limit = ' limit 1';
|
||||||
|
if (state.form.srcDbType === DbType.oracle) {
|
||||||
|
limit = ' where rownum <= 1';
|
||||||
|
}
|
||||||
|
|
||||||
const res = await dbApi.sqlExec.request({
|
const res = await dbApi.sqlExec.request({
|
||||||
id: state.form.srcDbId,
|
id: state.form.srcDbId,
|
||||||
db: state.form.srcDbName,
|
db: state.form.srcDbName,
|
||||||
sql: state.form.dataSql.trim() + ' limit 1',
|
sql: `select * from (${state.form.dataSql}) t ${limit}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.columns) {
|
if (!res.columns) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const dbApi = {
|
|||||||
tableInfos: Api.newGet('/dbs/{id}/t-infos'),
|
tableInfos: Api.newGet('/dbs/{id}/t-infos'),
|
||||||
tableIndex: Api.newGet('/dbs/{id}/t-index'),
|
tableIndex: Api.newGet('/dbs/{id}/t-index'),
|
||||||
tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
|
tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
|
||||||
|
copyTable: Api.newPost('/dbs/{id}/copy-table'),
|
||||||
columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
|
columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
|
||||||
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
|
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
|
||||||
// 获取表即列提示
|
// 获取表即列提示
|
||||||
@@ -48,16 +49,20 @@ export const dbApi = {
|
|||||||
// 获取数据库备份列表
|
// 获取数据库备份列表
|
||||||
getDbBackups: Api.newGet('/dbs/{dbId}/backups'),
|
getDbBackups: Api.newGet('/dbs/{dbId}/backups'),
|
||||||
createDbBackup: Api.newPost('/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'),
|
getDbNamesWithoutBackup: Api.newGet('/dbs/{dbId}/db-names-without-backup'),
|
||||||
enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
|
enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
|
||||||
disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
|
disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
|
||||||
startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'),
|
startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'),
|
||||||
saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
|
saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
|
||||||
getDbBackupHistories: Api.newGet('/dbs/{dbId}/backup-histories'),
|
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'),
|
getDbRestores: Api.newGet('/dbs/{dbId}/restores'),
|
||||||
createDbRestore: Api.newPost('/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'),
|
getDbNamesWithoutRestore: Api.newGet('/dbs/{dbId}/db-names-without-restore'),
|
||||||
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
||||||
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
:resource-type="TagResourceTypeEnum.Db.value"
|
:resource-type="TagResourceTypeEnum.Db.value"
|
||||||
:tag-path-node-type="NodeTypeTagPath"
|
:tag-path-node-type="NodeTypeTagPath"
|
||||||
>
|
>
|
||||||
|
<template #iconPrefix>
|
||||||
|
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
|
||||||
|
</template>
|
||||||
<template #prefix="{ data }">
|
<template #prefix="{ data }">
|
||||||
<SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
|
<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" />
|
<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 { dbApi } from '@/views/ops/db/api';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import 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 TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
@@ -27,15 +30,21 @@ const props = defineProps({
|
|||||||
dbId: {
|
dbId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
},
|
},
|
||||||
|
instName: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
dbName: {
|
dbName: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
tagPath: {
|
tagPath: {
|
||||||
type: String,
|
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({
|
const selectNode = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return props.dbName ? `${props.tagPath} - ${props.dbId} - ${props.dbName}` : '';
|
return props.dbName ? `${props.tagPath} > ${props.instName} > ${props.dbName}` : '';
|
||||||
},
|
},
|
||||||
set: () => {
|
set: () => {
|
||||||
//
|
//
|
||||||
@@ -87,8 +96,8 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** mysql类型的数据库,没有schema层 */
|
/** mysql类型的数据库,没有schema层 */
|
||||||
const mysqlType = (type: string) => {
|
const noSchemaType = (type: string) => {
|
||||||
return type === DbType.mysql;
|
return noSchemaTypes.includes(type);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 数据库实例节点类型
|
// 数据库实例节点类型
|
||||||
@@ -96,7 +105,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
|||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
const dbs = params.database.split(' ')?.sort();
|
const dbs = params.database.split(' ')?.sort();
|
||||||
let fn: NodeType;
|
let fn: NodeType;
|
||||||
if (mysqlType(params.type)) {
|
if (noSchemaType(params.type)) {
|
||||||
fn = MysqlNodeTypes;
|
fn = MysqlNodeTypes;
|
||||||
} else {
|
} else {
|
||||||
fn = PgNodeTypes;
|
fn = PgNodeTypes;
|
||||||
@@ -114,7 +123,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
|
|||||||
db: x,
|
db: x,
|
||||||
})
|
})
|
||||||
.withIcon(DbIcon);
|
.withIcon(DbIcon);
|
||||||
if (mysqlType(params.type)) {
|
if (noSchemaType(params.type)) {
|
||||||
tagTreeNode.isLeaf = true;
|
tagTreeNode.isLeaf = true;
|
||||||
}
|
}
|
||||||
return tagTreeNode;
|
return tagTreeNode;
|
||||||
@@ -148,8 +157,10 @@ const changeNode = (nodeData: TagTreeNode) => {
|
|||||||
const params = nodeData.params;
|
const params = nodeData.params;
|
||||||
// postgres
|
// postgres
|
||||||
emits('update:dbName', params.db);
|
emits('update:dbName', params.db);
|
||||||
|
emits('update:instName', params.name);
|
||||||
emits('update:dbId', params.id);
|
emits('update:dbId', params.id);
|
||||||
emits('update:tagPath', params.tagPath);
|
emits('update:tagPath', params.tagPath);
|
||||||
|
emits('update:dbType', params.type);
|
||||||
emits('selectDb', params);
|
emits('selectDb', params);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -128,12 +128,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { getToken } from '@/common/utils/storage';
|
||||||
import { notBlank } from '@/common/assert';
|
import { notBlank } from '@/common/assert';
|
||||||
import { format as sqlFormatter } from 'sql-formatter';
|
import { format as sqlFormatter } from 'sql-formatter';
|
||||||
import config from '@/common/config';
|
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 * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
import { editor } from 'monaco-editor';
|
import { editor } from 'monaco-editor';
|
||||||
@@ -146,11 +146,9 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
|||||||
import { joinClientParams } from '@/common/request';
|
import { joinClientParams } from '@/common/request';
|
||||||
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
|
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
|
||||||
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
||||||
import { ElNotification } from 'element-plus';
|
|
||||||
import syssocket from '@/common/syssocket';
|
import syssocket from '@/common/syssocket';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { getDbDialect } from '../../dialect';
|
import { Pane, Splitpanes } from 'splitpanes';
|
||||||
import { Splitpanes, Pane } from 'splitpanes';
|
|
||||||
|
|
||||||
const emits = defineEmits(['saveSqlSuccess']);
|
const emits = defineEmits(['saveSqlSuccess']);
|
||||||
|
|
||||||
@@ -357,6 +355,7 @@ const onRunSql = async (newTab = false) => {
|
|||||||
const colAndData: any = data.value;
|
const colAndData: any = data.value;
|
||||||
if (!colAndData.res || colAndData.res.length === 0) {
|
if (!colAndData.res || colAndData.res.length === 0) {
|
||||||
ElMessage.warning('未查询到结果集');
|
ElMessage.warning('未查询到结果集');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 要实时响应,故需要用索引改变数据才生效
|
// 要实时响应,故需要用索引改变数据才生效
|
||||||
@@ -453,7 +452,7 @@ const formatSql = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDialect = getDbDialect(getNowDbInst().type).getInfo().formatSqlDialect;
|
const formatDialect = getNowDbInst().getDialect().getInfo().formatSqlDialect;
|
||||||
|
|
||||||
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||||
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-if="dataType == DataType.String"
|
v-if="dataType == DataType.String"
|
||||||
:ref="(el: any) => focus && el?.focus()"
|
:ref="(el: any) => focus && el?.focus()"
|
||||||
|
:disabled="disabled"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
:class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`"
|
:class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`"
|
||||||
input-style="text-align: center; height: 26px;"
|
|
||||||
size="small"
|
size="small"
|
||||||
v-model="itemValue"
|
v-model="itemValue"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
<el-input
|
<el-input
|
||||||
v-else-if="dataType == DataType.Number"
|
v-else-if="dataType == DataType.Number"
|
||||||
:ref="(el: any) => focus && el?.focus()"
|
:ref="(el: any) => focus && el?.focus()"
|
||||||
|
:disabled="disabled"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
class="w100 mb4"
|
class="w100 mb4"
|
||||||
input-style="text-align: center; height: 26px;"
|
|
||||||
size="small"
|
size="small"
|
||||||
v-model.number="itemValue"
|
v-model.number="itemValue"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-else-if="dataType == DataType.Date"
|
v-else-if="dataType == DataType.Date"
|
||||||
:ref="(el: any) => focus && el?.focus()"
|
:ref="(el: any) => focus && el?.focus()"
|
||||||
|
:disabled="disabled"
|
||||||
@change="emit('blur')"
|
@change="emit('blur')"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
class="edit-time-picker mb4"
|
class="edit-time-picker mb4"
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-else-if="dataType == DataType.DateTime"
|
v-else-if="dataType == DataType.DateTime"
|
||||||
:ref="(el: any) => focus && el?.focus()"
|
:ref="(el: any) => focus && el?.focus()"
|
||||||
|
:disabled="disabled"
|
||||||
@change="handleBlur"
|
@change="handleBlur"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
class="edit-time-picker mb4"
|
class="edit-time-picker mb4"
|
||||||
@@ -58,6 +60,7 @@
|
|||||||
<el-time-picker
|
<el-time-picker
|
||||||
v-else-if="dataType == DataType.Time"
|
v-else-if="dataType == DataType.Time"
|
||||||
:ref="(el: any) => focus && el?.focus()"
|
:ref="(el: any) => focus && el?.focus()"
|
||||||
|
:disabled="disabled"
|
||||||
@change="handleBlur"
|
@change="handleBlur"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
class="edit-time-picker mb4"
|
class="edit-time-picker mb4"
|
||||||
@@ -71,7 +74,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Ref, ref, computed } from 'vue';
|
import { computed, ref, Ref } from 'vue';
|
||||||
import { ElInput } from 'element-plus';
|
import { ElInput } from 'element-plus';
|
||||||
import { DataType } from '../../dialect/index';
|
import { DataType } from '../../dialect/index';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
@@ -83,11 +86,13 @@ export interface ColumnFormItemProps {
|
|||||||
focus?: boolean; // 是否获取焦点
|
focus?: boolean; // 是否获取焦点
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
columnName?: string;
|
columnName?: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
|
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
|
||||||
focus: false,
|
focus: false,
|
||||||
dataType: DataType.String,
|
dataType: DataType.String,
|
||||||
|
disabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'blur']);
|
const emit = defineEmits(['update:modelValue', 'blur']);
|
||||||
@@ -178,9 +183,6 @@ const getEditorLangByValue = (value: any) => {
|
|||||||
.el-input__prefix {
|
.el-input__prefix {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.el-input__inner {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-time-picker-popper {
|
.edit-time-picker-popper {
|
||||||
|
|||||||
@@ -46,14 +46,6 @@
|
|||||||
<b :title="column.remark" class="el-text" style="cursor: pointer">
|
<b :title="column.remark" class="el-text" style="cursor: pointer">
|
||||||
{{ column.title }}
|
{{ column.title }}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
<span>
|
|
||||||
<SvgIcon
|
|
||||||
color="var(--el-color-primary)"
|
|
||||||
v-if="column.title == nowSortColumn?.columnName"
|
|
||||||
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
|
|
||||||
></SvgIcon>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 字段备注信息 -->
|
<!-- 字段备注信息 -->
|
||||||
@@ -71,6 +63,13 @@
|
|||||||
{{ column.title }}
|
{{ column.title }}
|
||||||
</b>
|
</b>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,13 +137,25 @@
|
|||||||
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
|
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
|
||||||
</el-dialog>
|
</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" />
|
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
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 { copyToClipboard } from '@/common/utils/string';
|
||||||
import { DbInst } from '@/views/ops/db/db';
|
import { DbInst } from '@/views/ops/db/db';
|
||||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||||
@@ -154,6 +165,7 @@ import { dateStrFormat } from '@/common/utils/date';
|
|||||||
import { useIntervalFn, useStorage } from '@vueuse/core';
|
import { useIntervalFn, useStorage } from '@vueuse/core';
|
||||||
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
||||||
import ColumnFormItem from './ColumnFormItem.vue';
|
import ColumnFormItem from './ColumnFormItem.vue';
|
||||||
|
import DbTableDataForm from './DbTableDataForm.vue';
|
||||||
|
|
||||||
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
||||||
|
|
||||||
@@ -247,6 +259,13 @@ const cmDataDel = new ContextmenuItem('deleteData', '删除')
|
|||||||
return state.table == '';
|
return state.table == '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cmDataEdit = new ContextmenuItem('editData', '编辑行')
|
||||||
|
.withIcon('edit')
|
||||||
|
.withOnClick(() => onEditRowData())
|
||||||
|
.withHideFunc(() => {
|
||||||
|
return state.table == '';
|
||||||
|
});
|
||||||
|
|
||||||
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
||||||
.withIcon('tickets')
|
.withIcon('tickets')
|
||||||
.withOnClick(() => onGenerateInsertSql())
|
.withOnClick(() => onGenerateInsertSql())
|
||||||
@@ -333,7 +352,11 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
items: [] as ContextmenuItem[],
|
items: [] as ContextmenuItem[],
|
||||||
},
|
},
|
||||||
|
tableDataFormDialog: {
|
||||||
|
data: {},
|
||||||
|
title: '',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
genTxtDialog: {
|
genTxtDialog: {
|
||||||
title: 'SQL',
|
title: 'SQL',
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -444,7 +467,7 @@ const formatDataValues = (datas: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setTableData = (datas: any) => {
|
const setTableData = (datas: any) => {
|
||||||
tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
tableRef.value?.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
||||||
selectionRowsMap.clear();
|
selectionRowsMap.clear();
|
||||||
cellUpdateMap.clear();
|
cellUpdateMap.clear();
|
||||||
formatDataValues(datas);
|
formatDataValues(datas);
|
||||||
@@ -576,7 +599,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
|
|||||||
const { clientX, clientY } = event;
|
const { clientX, clientY } = event;
|
||||||
state.contextmenu.dropdown.x = clientX;
|
state.contextmenu.dropdown.x = clientX;
|
||||||
state.contextmenu.dropdown.y = clientY;
|
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 });
|
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 onGenerateInsertSql = async () => {
|
||||||
const selectionDatas = Array.from(selectionRowsMap.values());
|
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||||
state.genTxtDialog.txt = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
|
state.genTxtDialog.txt = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
|
||||||
@@ -714,36 +749,21 @@ const submitUpdateFields = async () => {
|
|||||||
|
|
||||||
const db = state.db;
|
const db = state.db;
|
||||||
let res = '';
|
let res = '';
|
||||||
const dbDialect = getDbDialect(dbInst.type);
|
|
||||||
|
|
||||||
for (let updateRow of cellUpdateMap.values()) {
|
for (let updateRow of cellUpdateMap.values()) {
|
||||||
let sql = `UPDATE ${dbInst.wrapName(state.table)} SET `;
|
const rowData = { ...updateRow.rowData };
|
||||||
const rowData = updateRow.rowData;
|
let updateColumnValue = {};
|
||||||
// 主键列信息
|
|
||||||
const primaryKey = await dbInst.loadTableColumn(db, state.table);
|
|
||||||
let primaryKeyType = primaryKey.columnType;
|
|
||||||
let primaryKeyName = primaryKey.columnName;
|
|
||||||
let primaryKeyValue = rowData[primaryKeyName];
|
|
||||||
|
|
||||||
for (let k of updateRow.columnsMap.keys()) {
|
for (let k of updateRow.columnsMap.keys()) {
|
||||||
const v = updateRow.columnsMap.get(k);
|
const v = updateRow.columnsMap.get(k);
|
||||||
if (!v) {
|
if (!v) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 更新字段列信息
|
updateColumnValue[k] = rowData[k];
|
||||||
const updateColumn = await dbInst.loadTableColumn(db, state.table, k);
|
// 将更新的字段对应的原始数据还原(主要应对可能更新修改了主键等)
|
||||||
|
rowData[k] = v.oldValue;
|
||||||
sql += ` ${dbInst.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, rowData[k], dbDialect)},`;
|
|
||||||
|
|
||||||
// 如果修改的字段是主键
|
|
||||||
if (k === primaryKeyName) {
|
|
||||||
primaryKeyValue = v.oldValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
|
||||||
sql = sql.substring(0, sql.length - 1);
|
|
||||||
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`;
|
|
||||||
res += sql;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dbInst.promptExeSql(
|
dbInst.promptExeSql(
|
||||||
@@ -868,9 +888,15 @@ defineExpose({
|
|||||||
color: var(--el-color-info-light-3);
|
color: var(--el-color-info-light-3);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -7px;
|
top: -5px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-right {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 0;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
height: 12px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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"
|
@data-delete="onRefresh"
|
||||||
></db-table-data>
|
></db-table-data>
|
||||||
|
|
||||||
<el-row type="flex" class="mt5" justify="center">
|
<el-row type="flex" class="mt5" :gutter="10" justify="space-between" style="user-select: none">
|
||||||
<el-pagination
|
<el-col :span="12">
|
||||||
small
|
<el-text
|
||||||
:total="count"
|
id="copyValue"
|
||||||
@size-change="handleSizeChange"
|
style="color: var(--el-color-info-light-3)"
|
||||||
@current-change="pageChange()"
|
class="is-truncated font12 mt5"
|
||||||
layout="prev, pager, next, total, sizes, jumper"
|
@click="copyToClipboard(sql)"
|
||||||
v-model:current-page="pageNum"
|
:title="sql"
|
||||||
v-model:page-size="pageSize"
|
>{{ sql }}</el-text
|
||||||
:page-sizes="pageSizes"
|
>
|
||||||
></el-pagination>
|
</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>
|
</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-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
|
||||||
<el-row>
|
<el-row>
|
||||||
@@ -203,31 +234,16 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="addDataDialog.visible" :title="addDataDialog.title" :destroy-on-close="true" width="600px">
|
<DbTableDataForm
|
||||||
<el-form ref="dataForm" :model="addDataDialog.data" :show-message="false" label-width="auto" size="small">
|
:db-inst="getNowDbInst()"
|
||||||
<el-form-item
|
:db-name="dbName"
|
||||||
v-for="column in columns"
|
:columns="columns"
|
||||||
:key="column.columnName"
|
:title="addDataDialog.title"
|
||||||
class="w100 mb5"
|
:table-name="tableName"
|
||||||
:prop="column.columnName"
|
v-model:visible="addDataDialog.visible"
|
||||||
:label="column.columnName"
|
v-model="addDataDialog.data"
|
||||||
:required="column.nullable != 'YES' && column.columnKey != 'PRI'"
|
@submit-success="onRefresh"
|
||||||
>
|
/>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -237,10 +253,11 @@ import { ElMessage } from 'element-plus';
|
|||||||
|
|
||||||
import { DbInst } from '@/views/ops/db/db';
|
import { DbInst } from '@/views/ops/db/db';
|
||||||
import DbTableData from './DbTableData.vue';
|
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 SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import ColumnFormItem from './ColumnFormItem.vue';
|
|
||||||
import { useEventListener, useStorage } from '@vueuse/core';
|
import { useEventListener, useStorage } from '@vueuse/core';
|
||||||
|
import { copyToClipboard } from '@/common/utils/string';
|
||||||
|
import DbTableDataForm from './DbTableDataForm.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
dbId: {
|
dbId: {
|
||||||
@@ -261,7 +278,6 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dataForm: any = ref(null);
|
|
||||||
const dbTableRef: Ref = ref(null);
|
const dbTableRef: Ref = ref(null);
|
||||||
const condInputRef: Ref = ref(null);
|
const condInputRef: Ref = ref(null);
|
||||||
const columnNameSearchInputRef: Ref = ref(null);
|
const columnNameSearchInputRef: Ref = ref(null);
|
||||||
@@ -289,7 +305,10 @@ const state = reactive({
|
|||||||
defaultPageSize * 40,
|
defaultPageSize * 40,
|
||||||
defaultPageSize * 80,
|
defaultPageSize * 80,
|
||||||
],
|
],
|
||||||
count: 0,
|
setPageNum: 0,
|
||||||
|
total: 0,
|
||||||
|
showTotal: false,
|
||||||
|
counting: false,
|
||||||
selectionDatas: [] as any,
|
selectionDatas: [] as any,
|
||||||
condPopVisible: false,
|
condPopVisible: false,
|
||||||
columnNameSearch: '',
|
columnNameSearch: '',
|
||||||
@@ -305,7 +324,6 @@ const state = reactive({
|
|||||||
addDataDialog: {
|
addDataDialog: {
|
||||||
data: {},
|
data: {},
|
||||||
title: '',
|
title: '',
|
||||||
placeholder: '',
|
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
tableHeight: '600px',
|
tableHeight: '600px',
|
||||||
@@ -313,7 +331,7 @@ const state = reactive({
|
|||||||
dbDialect: {} as DbDialect,
|
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(
|
watch(
|
||||||
() => props.tableHeight,
|
() => props.tableHeight,
|
||||||
@@ -331,7 +349,7 @@ onMounted(async () => {
|
|||||||
state.tableHeight = props.tableHeight;
|
state.tableHeight = props.tableHeight;
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
|
|
||||||
state.dbDialect = getDbDialect(getNowDbInst().type);
|
state.dbDialect = getNowDbInst().getDialect();
|
||||||
useEventListener('click', handlerWindowClick);
|
useEventListener('click', handlerWindowClick);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -346,18 +364,19 @@ const onRefresh = async () => {
|
|||||||
await selectData();
|
await selectData();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
watch(
|
||||||
* 数据tab修改页数
|
() => state.pageNum,
|
||||||
*/
|
async () => {
|
||||||
const pageChange = async () => {
|
await selectData();
|
||||||
await selectData();
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单表数据信息查询数据
|
* 单表数据信息查询数据
|
||||||
*/
|
*/
|
||||||
const selectData = async () => {
|
const selectData = async () => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
|
state.setPageNum = state.pageNum;
|
||||||
const dbInst = getNowDbInst();
|
const dbInst = getNowDbInst();
|
||||||
const db = props.dbName;
|
const db = props.dbName;
|
||||||
const table = props.tableName;
|
const table = props.tableName;
|
||||||
@@ -370,16 +389,10 @@ const selectData = async () => {
|
|||||||
state.columns = columns;
|
state.columns = columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
|
let sql = dbInst.getDefaultSelectSql(db, table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
||||||
state.count = countRes.res[0].count || countRes.res[0].COUNT || 0;
|
|
||||||
let sql = dbInst.getDefaultSelectSql(table, state.condition, state.orderBy, state.pageNum, state.pageSize);
|
|
||||||
state.sql = sql;
|
state.sql = sql;
|
||||||
if (state.count > 0) {
|
const colAndData: any = await dbInst.runSql(db, sql);
|
||||||
const colAndData: any = await dbInst.runSql(db, sql);
|
state.datas = colAndData.res;
|
||||||
state.datas = colAndData.res;
|
|
||||||
} else {
|
|
||||||
state.datas = [];
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
}
|
}
|
||||||
@@ -391,6 +404,33 @@ const handleSizeChange = async (size: any) => {
|
|||||||
await selectData();
|
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 = '';
|
let completeCond = '';
|
||||||
// 是否存在列建议
|
// 是否存在列建议
|
||||||
@@ -543,40 +583,10 @@ const onShowAddDataDialog = async () => {
|
|||||||
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
||||||
state.addDataDialog.visible = true;
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
<style lang="scss">
|
||||||
|
.op-page {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<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-form label-position="left" ref="formRef" :model="tableData" label-width="80px">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
:width="item.width"
|
:width="item.width"
|
||||||
>
|
>
|
||||||
<template #default="scope">
|
<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-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
|
||||||
<el-option
|
<el-option
|
||||||
@@ -42,35 +42,30 @@
|
|||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</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
|
<el-checkbox
|
||||||
v-else-if="item.prop === 'auto_increment'"
|
v-else-if="item.prop === 'auto_increment'"
|
||||||
size="small"
|
size="small"
|
||||||
v-model="scope.row.auto_increment"
|
v-model="scope.row.auto_increment"
|
||||||
:disabled="dbType === DbType.postgresql"
|
:disabled="disableEditIncr()"
|
||||||
>
|
/>
|
||||||
</el-checkbox>
|
|
||||||
|
|
||||||
<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
|
<el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteRow(scope.$index)">
|
||||||
v-else-if="item.prop === 'action'"
|
<template #reference>
|
||||||
type="danger"
|
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
|
||||||
plain
|
</template>
|
||||||
size="small"
|
</el-popconfirm>
|
||||||
:underline="false"
|
|
||||||
@click.prevent="deleteRow(scope.$index)"
|
|
||||||
>删除</el-link
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</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 v-if="item.prop === 'unique'" size="small" v-model="scope.row.unique" @change="indexChanges(scope.row)">
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
|
|
||||||
<el-select v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType">
|
<el-input 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 === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input>
|
<el-input v-if="item.prop === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input>
|
||||||
|
|
||||||
<el-link
|
<el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteIndex(scope.$index)">
|
||||||
v-if="item.prop === 'action'"
|
<template #reference>
|
||||||
type="danger"
|
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
|
||||||
plain
|
</template>
|
||||||
size="small"
|
</el-popconfirm>
|
||||||
:underline="false"
|
|
||||||
@click.prevent="deleteIndex(scope.$index)"
|
|
||||||
>删除</el-link
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -130,6 +119,7 @@
|
|||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<el-button @click="cancel()">取消</el-button>
|
||||||
<el-button :loading="btnloading" @click="submit()" type="primary">保存</el-button>
|
<el-button :loading="btnloading" @click="submit()" type="primary">保存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -166,7 +156,7 @@ const props = defineProps({
|
|||||||
//定义事件
|
//定义事件
|
||||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
||||||
|
|
||||||
const dbDialect = getDbDialect(props.dbType);
|
let dbDialect = getDbDialect(props.dbType);
|
||||||
|
|
||||||
type ColName = {
|
type ColName = {
|
||||||
prop: string;
|
prop: string;
|
||||||
@@ -180,29 +170,33 @@ const state = reactive({
|
|||||||
btnloading: false,
|
btnloading: false,
|
||||||
activeName: '1',
|
activeName: '1',
|
||||||
columnTypeList: dbDialect.getInfo().columnTypes,
|
columnTypeList: dbDialect.getInfo().columnTypes,
|
||||||
indexTypeList: ['BTREE', 'NORMAL'], // mysql索引类型详解 http://c.biancheng.net/view/7897.html
|
|
||||||
tableData: {
|
tableData: {
|
||||||
fields: {
|
fields: {
|
||||||
colNames: [
|
colNames: [
|
||||||
{
|
{
|
||||||
prop: 'name',
|
prop: 'name',
|
||||||
label: '字段名称',
|
label: '字段名称',
|
||||||
|
width: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'type',
|
prop: 'type',
|
||||||
label: '字段类型',
|
label: '字段类型',
|
||||||
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'length',
|
prop: 'length',
|
||||||
label: '长度',
|
label: '长度',
|
||||||
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'numScale',
|
prop: 'numScale',
|
||||||
label: '小数点',
|
label: '小数点',
|
||||||
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'value',
|
prop: 'value',
|
||||||
label: '默认值',
|
label: '默认值',
|
||||||
|
width: 120,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -231,6 +225,7 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
] as ColName[],
|
] as ColName[],
|
||||||
res: [] as RowDefinition[],
|
res: [] as RowDefinition[],
|
||||||
|
oldFields: [] as RowDefinition[],
|
||||||
},
|
},
|
||||||
indexs: {
|
indexs: {
|
||||||
colNames: [
|
colNames: [
|
||||||
@@ -261,19 +256,34 @@ const state = reactive({
|
|||||||
],
|
],
|
||||||
columns: [{ name: '', remark: '' }],
|
columns: [{ name: '', remark: '' }],
|
||||||
res: [] as IndexDefinition[],
|
res: [] as IndexDefinition[],
|
||||||
|
oldIndexs: [] as IndexDefinition[],
|
||||||
},
|
},
|
||||||
tableName: '',
|
tableName: '',
|
||||||
tableComment: '',
|
tableComment: '',
|
||||||
height: 450,
|
height: 450,
|
||||||
|
db: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { dialogVisible, btnloading, activeName, indexTypeList, tableData } = toRefs(state);
|
const { dialogVisible, btnloading, activeName, tableData } = toRefs(state);
|
||||||
|
|
||||||
watch(props, async (newValue) => {
|
watch(props, async (newValue) => {
|
||||||
state.dialogVisible = newValue.visible;
|
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 = () => {
|
const cancel = () => {
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
reset();
|
reset();
|
||||||
@@ -359,7 +369,10 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
|
|||||||
nowArr.forEach((a) => {
|
nowArr.forEach((a) => {
|
||||||
let k = a[key];
|
let k = a[key];
|
||||||
newMap[k] = a;
|
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);
|
data.add.push(a);
|
||||||
}
|
}
|
||||||
@@ -376,7 +389,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
|
|||||||
for (let f in a) {
|
for (let f in a) {
|
||||||
let oldV = a[f];
|
let oldV = a[f];
|
||||||
let newV = newData[f];
|
let newV = newData[f];
|
||||||
if (oldV.toString() !== newV.toString()) {
|
if (oldV?.toString() !== newV?.toString()) {
|
||||||
data.upd.push(newData);
|
data.upd.push(newData);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -390,22 +403,22 @@ const genSql = () => {
|
|||||||
let data = state.tableData;
|
let data = state.tableData;
|
||||||
// 创建表
|
// 创建表
|
||||||
if (!props.data?.edit) {
|
if (!props.data?.edit) {
|
||||||
if (state.activeName === '1') {
|
let createTable = dbDialect.getCreateTableSql(data);
|
||||||
return dbDialect.getCreateTableSql(data);
|
let createIndex = '';
|
||||||
} else if (state.activeName === '2' && data.indexs.res.length > 0) {
|
if (data.indexs.res.length > 0) {
|
||||||
return dbDialect.getCreateIndexSql(data);
|
createIndex = dbDialect.getCreateIndexSql(data);
|
||||||
}
|
}
|
||||||
|
return createTable + ';' + createIndex;
|
||||||
} else {
|
} else {
|
||||||
// 修改
|
// 修改列
|
||||||
if (state.activeName === '1') {
|
let changeColData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
|
||||||
// 修改列
|
let colSql = dbDialect.getModifyColumnSql(data, data.tableName, changeColData);
|
||||||
let changeData = filterChangedData(oldData.fields, state.tableData.fields.res, 'name');
|
// 修改索引
|
||||||
return dbDialect.getModifyColumnSql(data.tableName, changeData);
|
let changeIdxData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
|
||||||
} else if (state.activeName === '2') {
|
let idxSql = dbDialect.getModifyIndexSql(data, data.tableName, changeIdxData);
|
||||||
// 修改索引
|
// 修改表名
|
||||||
let changeData = filterChangedData(oldData.indexs, state.tableData.indexs.res, 'indexName');
|
|
||||||
return dbDialect.getModifyIndexSql(data.tableName, changeData);
|
return colSql + ';' + idxSql;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -414,28 +427,8 @@ const reset = () => {
|
|||||||
formRef.value.resetFields();
|
formRef.value.resetFields();
|
||||||
state.tableData.tableName = '';
|
state.tableData.tableName = '';
|
||||||
state.tableData.tableComment = '';
|
state.tableData.tableComment = '';
|
||||||
state.tableData.fields.res = [
|
state.tableData.fields.res = [];
|
||||||
{
|
state.tableData.indexs.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: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const indexChanges = (row: any) => {
|
const indexChanges = (row: any) => {
|
||||||
@@ -456,7 +449,21 @@ const indexChanges = (row: any) => {
|
|||||||
row.indexComment = `${tableData.value.tableName}表(${name.replaceAll('_', ',')})${commentSuffix}`;
|
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(
|
watch(
|
||||||
() => props.data,
|
() => props.data,
|
||||||
(newValue: any) => {
|
(newValue: any) => {
|
||||||
@@ -464,9 +471,10 @@ watch(
|
|||||||
// 回显表名表注释
|
// 回显表名表注释
|
||||||
state.tableData.tableName = row.tableName;
|
state.tableData.tableName = row.tableName;
|
||||||
state.tableData.tableComment = row.tableComment;
|
state.tableData.tableComment = row.tableComment;
|
||||||
|
state.tableData.db = props.db!;
|
||||||
// 回显列
|
// 回显列
|
||||||
if (columns && Array.isArray(columns) && columns.length > 0) {
|
if (columns && Array.isArray(columns) && columns.length > 0) {
|
||||||
oldData.fields = [];
|
state.tableData.fields.oldFields = [];
|
||||||
state.tableData.fields.res = [];
|
state.tableData.fields.res = [];
|
||||||
// 索引列下拉选
|
// 索引列下拉选
|
||||||
state.tableData.indexs.columns = [];
|
state.tableData.indexs.columns = [];
|
||||||
@@ -474,26 +482,33 @@ watch(
|
|||||||
let typeObj = a.columnType.replace(')', '').split('(');
|
let typeObj = a.columnType.replace(')', '').split('(');
|
||||||
let type = typeObj[0];
|
let type = typeObj[0];
|
||||||
let length = (typeObj.length > 1 && typeObj[1]) || '';
|
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 = {
|
let data = {
|
||||||
name: a.columnName,
|
name: a.columnName,
|
||||||
|
oldName: a.columnName,
|
||||||
type,
|
type,
|
||||||
value: a.columnDefault || '',
|
value: defaultValue,
|
||||||
length,
|
length,
|
||||||
numScale: a.numScale,
|
numScale: a.numScale,
|
||||||
notNull: a.nullable !== 'YES',
|
notNull: a.nullable !== 'YES',
|
||||||
pri: a.columnKey === 'PRI',
|
pri: a.isPrimaryKey,
|
||||||
auto_increment: a.columnKey === 'PRI' /*a.extra?.indexOf('auto_increment') > -1*/,
|
auto_increment: a.isIdentity /*a.extra?.indexOf('auto_increment') > -1*/,
|
||||||
remark: a.columnComment,
|
remark: a.columnComment,
|
||||||
};
|
};
|
||||||
state.tableData.fields.res.push(data);
|
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 });
|
state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 回显索引
|
// 回显索引
|
||||||
if (indexs && Array.isArray(indexs) && indexs.length > 0) {
|
if (indexs && Array.isArray(indexs) && indexs.length > 0) {
|
||||||
oldData.indexs = [];
|
state.tableData.indexs.oldIndexs = [];
|
||||||
state.tableData.indexs.res = [];
|
state.tableData.indexs.res = [];
|
||||||
// 索引过滤掉主键
|
// 索引过滤掉主键
|
||||||
indexs
|
indexs
|
||||||
@@ -502,12 +517,12 @@ watch(
|
|||||||
let data = {
|
let data = {
|
||||||
indexName: a.indexName,
|
indexName: a.indexName,
|
||||||
columnNames: a.columnName?.split(','),
|
columnNames: a.columnName?.split(','),
|
||||||
unique: a.nonUnique === 0 || false,
|
unique: a.isUnique || false,
|
||||||
indexType: a.indexType,
|
indexType: a.indexType,
|
||||||
indexComment: a.indexComment,
|
indexComment: a.indexComment,
|
||||||
};
|
};
|
||||||
state.tableData.indexs.res.push(data);
|
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">
|
<template #default="scope">
|
||||||
<el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link>
|
<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" @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 class="ml5" v-if="editDbTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning">编辑表</el-link>
|
||||||
>编辑表</el-link
|
|
||||||
>
|
|
||||||
<el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link>
|
<el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -127,7 +125,7 @@ import SqlExecBox from '../sqleditor/SqlExecBox';
|
|||||||
import config from '@/common/config';
|
import config from '@/common/config';
|
||||||
import { joinClientParams } from '@/common/request';
|
import { joinClientParams } from '@/common/request';
|
||||||
import { isTrue } from '@/common/assert';
|
import { isTrue } from '@/common/assert';
|
||||||
import { compatibleMysql, DbType } from '../../dialect/index';
|
import { compatibleMysql, DbType, editDbTypes } from '../../dialect/index';
|
||||||
|
|
||||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
||||||
|
|
||||||
@@ -181,7 +179,6 @@ const state = reactive({
|
|||||||
visible: false,
|
visible: false,
|
||||||
activeName: '1',
|
activeName: '1',
|
||||||
type: '',
|
type: '',
|
||||||
enableEditTypes: [DbType.mysql, DbType.mariadb, DbType.postgresql, DbType.dm, DbType.oracle], // 支持"编辑表"的数据库类型
|
|
||||||
data: {
|
data: {
|
||||||
// 修改表时,传递修改数据
|
// 修改表时,传递修改数据
|
||||||
edit: false,
|
edit: false,
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import { editor, languages, Position } from 'monaco-editor';
|
|||||||
|
|
||||||
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||||
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
|
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();
|
const dbInstCache: Map<number, DbInst> = new Map();
|
||||||
|
|
||||||
@@ -58,17 +62,23 @@ export class DbInst {
|
|||||||
if (!dbName) {
|
if (!dbName) {
|
||||||
throw new Error('dbName不能为空');
|
throw new Error('dbName不能为空');
|
||||||
}
|
}
|
||||||
let db = this.dbs.get(dbName);
|
let key = `${this.id}_${dbName}`;
|
||||||
|
let db = this.dbs.get(key);
|
||||||
if (db) {
|
if (db) {
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`);
|
console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`);
|
||||||
db = new Db();
|
db = new Db();
|
||||||
db.name = dbName;
|
db.name = dbName;
|
||||||
this.dbs.set(dbName, db);
|
this.dbs.set(key, db);
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取数据库实例方言
|
||||||
|
getDialect(): DbDialect {
|
||||||
|
return getDbDialect(this.type);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载数据库表信息
|
* 加载数据库表信息
|
||||||
* @param dbName 数据库名
|
* @param dbName 数据库名
|
||||||
@@ -77,17 +87,22 @@ export class DbInst {
|
|||||||
*/
|
*/
|
||||||
async loadTables(dbName: string, reload?: boolean) {
|
async loadTables(dbName: string, reload?: boolean) {
|
||||||
const db = this.getDb(dbName);
|
const db = this.getDb(dbName);
|
||||||
// 优先从 table map中获取
|
let key = this.dbTablesKey(dbName);
|
||||||
let tables = db.tables;
|
let tables = tableStorage.value.get(key);
|
||||||
|
// 优先从 table 缓存中获取
|
||||||
if (!reload && tables) {
|
if (!reload && tables) {
|
||||||
|
db.tables = tables;
|
||||||
return tables;
|
return tables;
|
||||||
}
|
}
|
||||||
// 重置列信息缓存与表提示信息
|
// 重置列信息缓存与表提示信息
|
||||||
db.columnsMap?.clear();
|
db.columnsMap?.clear();
|
||||||
db.tableHints = null;
|
|
||||||
console.log(`load tables -> dbName: ${dbName}`);
|
console.log(`load tables -> dbName: ${dbName}`);
|
||||||
tables = await dbApi.tableInfos.request({ id: this.id, db: dbName });
|
tables = await dbApi.tableInfos.request({ id: this.id, db: dbName });
|
||||||
|
tableStorage.value.set(key, tables);
|
||||||
db.tables = tables;
|
db.tables = tables;
|
||||||
|
|
||||||
|
// 异步加载表提示信息
|
||||||
|
this.loadDbHints(dbName, true).then(() => {});
|
||||||
return tables;
|
return tables;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,18 +184,30 @@ export class DbInst {
|
|||||||
return this.getDb(dbName).getColumn(table, columnName);
|
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);
|
const db = this.getDb(dbName);
|
||||||
if (db.tableHints) {
|
let key = this.dbTableHintsKey(dbName);
|
||||||
return db.tableHints;
|
let hints = hintsStorage.value.get(key);
|
||||||
|
if (!reload && hints) {
|
||||||
|
db.tableHints = hints;
|
||||||
|
return hints;
|
||||||
}
|
}
|
||||||
console.log(`load db-hits -> dbName: ${dbName}`);
|
console.log(`load db-hits -> dbName: ${dbName}`);
|
||||||
const hits = await dbApi.hintTables.request({ id: this.id, db: db.name });
|
hints = await dbApi.hintTables.request({ id: this.id, db: db.name });
|
||||||
db.tableHints = hits;
|
db.tableHints = hints;
|
||||||
return hits;
|
hintsStorage.value.set(key, hints);
|
||||||
|
return hints;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,8 +252,8 @@ export class DbInst {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 获取指定表的默认查询sql
|
// 获取指定表的默认查询sql
|
||||||
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
|
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
|
||||||
return getDbDialect(this.type).getDefaultSelectSql(table, condition, orderBy, pageNum, limit);
|
return getDbDialect(this.type).getDefaultSelectSql(db, table, condition, orderBy, pageNum, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -235,7 +262,7 @@ export class DbInst {
|
|||||||
* @param table 表名
|
* @param table 表名
|
||||||
* @param datas 要生成的数据
|
* @param datas 要生成的数据
|
||||||
*/
|
*/
|
||||||
async genInsertSql(dbName: string, table: string, datas: any[]) {
|
async genInsertSql(dbName: string, table: string, datas: any[], skipNull = false) {
|
||||||
if (!datas) {
|
if (!datas) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -247,6 +274,9 @@ export class DbInst {
|
|||||||
let values = [];
|
let values = [];
|
||||||
for (let column of columns) {
|
for (let column of columns) {
|
||||||
const colName = column.columnName;
|
const colName = column.columnName;
|
||||||
|
if (skipNull && data[colName] == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
colNames.push(this.wrapName(colName));
|
colNames.push(this.wrapName(colName));
|
||||||
values.push(DbInst.wrapValueByType(data[colName]));
|
values.push(DbInst.wrapValueByType(data[colName]));
|
||||||
}
|
}
|
||||||
@@ -255,6 +285,38 @@ export class DbInst {
|
|||||||
return sqls.join(';\n') + ';';
|
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语句
|
* 生成根据主键删除的sql语句
|
||||||
* @param table 表名
|
* @param table 表名
|
||||||
@@ -275,6 +337,7 @@ export class DbInst {
|
|||||||
sql,
|
sql,
|
||||||
dbId: this.id,
|
dbId: this.id,
|
||||||
db,
|
db,
|
||||||
|
dbType: this.getDialect().getInfo().formatSqlDialect,
|
||||||
runSuccessCallback: successFunc,
|
runSuccessCallback: successFunc,
|
||||||
cancelCallback: cancelFunc,
|
cancelCallback: cancelFunc,
|
||||||
});
|
});
|
||||||
@@ -287,7 +350,7 @@ export class DbInst {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
wrapName = (name: string) => {
|
wrapName = (name: string) => {
|
||||||
return getDbDialect(this.type).quoteIdentifier(name);
|
return this.getDialect().quoteIdentifier(name);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -363,7 +426,7 @@ export class DbInst {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (!dbDialect) {
|
if (!dbDialect) {
|
||||||
return `${value}`;
|
return `'${value}'`;
|
||||||
}
|
}
|
||||||
return dbDialect.wrapStrValue(columnType, value);
|
return dbDialect.wrapStrValue(columnType, value);
|
||||||
}
|
}
|
||||||
@@ -441,7 +504,7 @@ class Db {
|
|||||||
getColumn(table: string, columnName: string = '') {
|
getColumn(table: string, columnName: string = '') {
|
||||||
const cols = this.getColumns(table);
|
const cols = this.getColumns(table);
|
||||||
if (!columnName) {
|
if (!columnName) {
|
||||||
const col = cols.find((c: any) => c.columnKey == 'PRI');
|
const col = cols.find((c: any) => c.isPrimaryKey);
|
||||||
return col || cols[0];
|
return col || cols[0];
|
||||||
}
|
}
|
||||||
return cols.find((c: any) => c.columnName == columnName);
|
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' },
|
{ udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '100G-1' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 参考官方文档:https://eco.dameng.com/document/dm/zh-cn/pm/function.html
|
||||||
const replaceFunctions: EditorCompletionItem[] = [
|
const replaceFunctions: EditorCompletionItem[] = [
|
||||||
// 数值函数
|
// 数值函数
|
||||||
{ label: 'ABS', insertText: 'ABS(n)', description: '求数值 n 的绝对值' },
|
{ label: 'ABS', insertText: 'ABS(n)', description: '求数值 n 的绝对值' },
|
||||||
@@ -365,21 +366,22 @@ class DMDialect implements DbDialect {
|
|||||||
};
|
};
|
||||||
|
|
||||||
dmDialectInfo = {
|
dmDialectInfo = {
|
||||||
|
name: 'DM',
|
||||||
icon: 'iconfont icon-db-dm',
|
icon: 'iconfont icon-db-dm',
|
||||||
defaultPort: 5236,
|
defaultPort: 5236,
|
||||||
formatSqlDialect: 'postgresql',
|
formatSqlDialect: 'plsql',
|
||||||
columnTypes: DM_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
|
columnTypes: DM_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
|
||||||
editorCompletions,
|
editorCompletions,
|
||||||
};
|
};
|
||||||
return dmDialectInfo;
|
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)};`;
|
return `SELECT * FROM "${table}" ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(pageNum, limit)};`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageSql(pageNum: number, limit: number) {
|
getPageSql(pageNum: number, limit: number) {
|
||||||
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit};`;
|
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultRows(): RowDefinition[] {
|
getDefaultRows(): RowDefinition[] {
|
||||||
@@ -500,7 +502,9 @@ class DMDialect implements DbDialect {
|
|||||||
// 默认值
|
// 默认值
|
||||||
let defVal = this.getDefaultValueSql(cl);
|
let defVal = this.getDefaultValueSql(cl);
|
||||||
let incr = cl.auto_increment ? 'IDENTITY' : '';
|
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 {
|
getCreateTableSql(data: any): string {
|
||||||
@@ -546,35 +550,78 @@ class DMDialect implements DbDialect {
|
|||||||
return sql.join(';');
|
return sql.join(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||||
let sql: 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) {
|
if (changeData.add.length > 0) {
|
||||||
changeData.add.forEach((a) => {
|
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) {
|
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) {
|
if (changeData.upd.length > 0) {
|
||||||
changeData.upd.forEach((a) => {
|
changeData.upd.forEach((a) => {
|
||||||
sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`);
|
let cmtSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
|
||||||
if (a.remark) {
|
if (a.remark && a.oldName === a.name) {
|
||||||
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
|
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) {
|
if (changeData.del.length > 0) {
|
||||||
changeData.del.forEach((a) => {
|
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 dropIndexNames: string[] = [];
|
||||||
let addIndexs: any[] = [];
|
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 { DMDialect } from '@/views/ops/db/dialect/dm_dialect';
|
||||||
import { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect';
|
import { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect';
|
||||||
import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_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 {
|
export interface sqlColumnType {
|
||||||
udtName: string;
|
udtName: string;
|
||||||
@@ -14,6 +19,7 @@ export interface sqlColumnType {
|
|||||||
|
|
||||||
export interface RowDefinition {
|
export interface RowDefinition {
|
||||||
name: string;
|
name: string;
|
||||||
|
oldName?: string;
|
||||||
type: string;
|
type: string;
|
||||||
value: string;
|
value: string;
|
||||||
length: string;
|
length: string;
|
||||||
@@ -78,6 +84,11 @@ export const ColumnTypeSubscript = {
|
|||||||
|
|
||||||
// 数据库基础信息
|
// 数据库基础信息
|
||||||
export interface DialectInfo {
|
export interface DialectInfo {
|
||||||
|
/**
|
||||||
|
* 数据库类型label
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图标
|
* 图标
|
||||||
*/
|
*/
|
||||||
@@ -108,10 +119,23 @@ export const DbType = {
|
|||||||
mysql: 'mysql',
|
mysql: 'mysql',
|
||||||
mariadb: 'mariadb',
|
mariadb: 'mariadb',
|
||||||
postgresql: 'postgres',
|
postgresql: 'postgres',
|
||||||
|
gauss: 'gauss',
|
||||||
dm: 'dm', // 达梦
|
dm: 'dm', // 达梦
|
||||||
oracle: 'oracle',
|
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 => {
|
export const compatibleMysql = (dbType: string): boolean => {
|
||||||
switch (dbType) {
|
switch (dbType) {
|
||||||
case DbType.mysql:
|
case DbType.mysql:
|
||||||
@@ -130,13 +154,14 @@ export interface DbDialect {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取默认查询sql
|
* 获取默认查询sql
|
||||||
|
* @param db 数据库信息
|
||||||
* @param table 表名
|
* @param table 表名
|
||||||
* @param condition 条件
|
* @param condition 条件
|
||||||
* @param orderBy 排序
|
* @param orderBy 排序
|
||||||
* @param pageNum 页数
|
* @param pageNum 页数
|
||||||
* @param limit 条数
|
* @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;
|
getPageSql(pageNum: number, limit: number): string;
|
||||||
|
|
||||||
@@ -164,47 +189,53 @@ export interface DbDialect {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成编辑列sql
|
* 生成编辑列sql
|
||||||
|
* @param tableData 表数据,包含表名、列数据、索引数据
|
||||||
* @param tableName 表名
|
* @param tableName 表名
|
||||||
* @param changeData 改变信息
|
* @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
|
* 生成编辑索引sql
|
||||||
|
* @param tableData 表数据,包含表名、列数据、索引数据
|
||||||
* @param tableName 表名
|
* @param tableName 表名
|
||||||
* @param changeData 改变数据
|
* @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') */
|
/** 包装字符串数据, 如:oracle需要把date类型改为 to_date(str, 'yyyy-mm-dd hh24:mi:ss') */
|
||||||
wrapStrValue(columnType: string, value: string): string;
|
wrapStrValue(columnType: string, value: string): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mysqlDialect = new MysqlDialect();
|
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 => {
|
let dbType2DialectMap: Map<string, DbDialect> = new Map();
|
||||||
if (!dbType) {
|
|
||||||
return mysqlDialect;
|
export const registerDbDialect = (dbType: string, dd: DbDialect) => {
|
||||||
}
|
dbType2DialectMap.set(dbType, dd);
|
||||||
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('不支持的数据库');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
mariadbDialectInfo = {} as DialectInfo;
|
||||||
Object.assign(mariadbDialectInfo, super.getInfo());
|
Object.assign(mariadbDialectInfo, super.getInfo());
|
||||||
|
mariadbDialectInfo.name = 'MariaDB';
|
||||||
mariadbDialectInfo.icon = 'iconfont icon-mariadb';
|
mariadbDialectInfo.icon = 'iconfont icon-mariadb';
|
||||||
return mariadbDialectInfo;
|
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 };
|
export { MYSQL_TYPE_LIST, MysqlDialect };
|
||||||
|
|
||||||
|
// 参考官方文档:https://dev.mysql.com/doc/refman/8.0/en/data-types.html
|
||||||
const MYSQL_TYPE_LIST = [
|
const MYSQL_TYPE_LIST = [
|
||||||
'bigint',
|
'bigint',
|
||||||
'binary',
|
'binary',
|
||||||
@@ -31,6 +32,7 @@ const MYSQL_TYPE_LIST = [
|
|||||||
'varchar',
|
'varchar',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 参考官方文档:https://dev.mysql.com/doc/refman/8.3/en/functions.html
|
||||||
const replaceFunctions: EditorCompletionItem[] = [
|
const replaceFunctions: EditorCompletionItem[] = [
|
||||||
/** 字符串相关函数 */
|
/** 字符串相关函数 */
|
||||||
{ label: 'CONCAT', insertText: 'CONCAT(str1,str2,...)', description: '多字符串合并' },
|
{ label: 'CONCAT', insertText: 'CONCAT(str1,str2,...)', description: '多字符串合并' },
|
||||||
@@ -102,6 +104,7 @@ class MysqlDialect implements DbDialect {
|
|||||||
};
|
};
|
||||||
|
|
||||||
mysqlDialectInfo = {
|
mysqlDialectInfo = {
|
||||||
|
name: 'MySQL',
|
||||||
icon: 'iconfont icon-op-mysql',
|
icon: 'iconfont icon-op-mysql',
|
||||||
defaultPort: 3306,
|
defaultPort: 3306,
|
||||||
formatSqlDialect: 'mysql',
|
formatSqlDialect: 'mysql',
|
||||||
@@ -111,7 +114,7 @@ class MysqlDialect implements DbDialect {
|
|||||||
return mysqlDialectInfo;
|
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(
|
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
|
||||||
pageNum,
|
pageNum,
|
||||||
limit
|
limit
|
||||||
@@ -193,7 +196,7 @@ class MysqlDialect implements DbDialect {
|
|||||||
let defVal = val ? `DEFAULT ${val}` : '';
|
let defVal = val ? `DEFAULT ${val}` : '';
|
||||||
let length = cl.length ? `(${cl.length})` : '';
|
let length = cl.length ? `(${cl.length})` : '';
|
||||||
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
|
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' : ''
|
cl.auto_increment ? 'AUTO_INCREMENT' : ''
|
||||||
} ${defVal} ${onUpdate} comment '${cl.remark || ''}' `;
|
} ${defVal} ${onUpdate} comment '${cl.remark || ''}' `;
|
||||||
}
|
}
|
||||||
@@ -223,38 +226,34 @@ class MysqlDialect implements DbDialect {
|
|||||||
return sql.substring(0, sql.length - 1) + ';';
|
return sql.substring(0, sql.length - 1) + ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||||
let addSql = '',
|
let sql = `ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)}`;
|
||||||
updSql = '',
|
let arr = [] as string[];
|
||||||
delSql = '';
|
if (changeData.del.length > 0) {
|
||||||
if (changeData.add.length > 0) {
|
changeData.del.forEach((a) => {
|
||||||
addSql = `ALTER TABLE ${tableName}`;
|
arr.push(` DROP COLUMN ${this.quoteIdentifier(a.name)} `);
|
||||||
changeData.add.forEach((a) => {
|
});
|
||||||
addSql += ` ADD ${this.genColumnBasicSql(a)},`;
|
}
|
||||||
|
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) {
|
if (changeData.upd.length > 0) {
|
||||||
updSql = `ALTER TABLE ${tableName}`;
|
|
||||||
let arr = [] as string[];
|
|
||||||
changeData.upd.forEach((a) => {
|
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) {
|
return sql + arr.join(',') + ';';
|
||||||
changeData.del.forEach((a) => {
|
|
||||||
delSql += ` ALTER TABLE ${tableName} DROP COLUMN ${a.name}; `;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return addSql + updSql + delSql;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// 搜集修改和删除的索引,添加到drop index xx
|
||||||
// 收集新增和修改的索引,添加到ADD xx
|
// 收集新增和修改的索引,添加到ADD xx
|
||||||
// ALTER TABLE `test1`
|
// ALTER TABLE `test1`
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sq
|
|||||||
|
|
||||||
export { OracleDialect, ORACLE_TYPE_LIST };
|
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[] = [
|
const ORACLE_TYPE_LIST: sqlColumnType[] = [
|
||||||
// 字符数据类型
|
// 字符数据类型
|
||||||
{ udtName: 'CHAR', dataType: 'CHAR', desc: '定长字符串,自动在末尾用空格补全,非unicode', space: '', range: '1 - 2000' },
|
{ 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: '' },
|
{ udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 参考官方文档:https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions001.htm
|
||||||
const replaceFunctions: EditorCompletionItem[] = [
|
const replaceFunctions: EditorCompletionItem[] = [
|
||||||
// 字符函数
|
// 字符函数
|
||||||
{ label: 'ASCII', insertText: 'ASCII(x)', description: '返回字符X的ASCII码' },
|
{ 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' },
|
{ 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;
|
let oracleDialectInfo: DialectInfo;
|
||||||
class OracleDialect implements DbDialect {
|
class OracleDialect implements DbDialect {
|
||||||
@@ -103,6 +132,7 @@ class OracleDialect implements DbDialect {
|
|||||||
let { keywords, operators, builtinVariables } = sqlLanguage;
|
let { keywords, operators, builtinVariables } = sqlLanguage;
|
||||||
let functionNames = replaceFunctions.map((a) => a.label);
|
let functionNames = replaceFunctions.map((a) => a.label);
|
||||||
let excludeKeywords = new Set(functionNames.concat(operators));
|
let excludeKeywords = new Set(functionNames.concat(operators));
|
||||||
|
excludeKeywords.add('SELECT');
|
||||||
|
|
||||||
let editorCompletions: EditorCompletion = {
|
let editorCompletions: EditorCompletion = {
|
||||||
keywords: keywords
|
keywords: keywords
|
||||||
@@ -117,21 +147,14 @@ class OracleDialect implements DbDialect {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.concat(
|
.concat(addCustomKeywords),
|
||||||
// 加上自定义的关键字
|
|
||||||
addCustomKeywords.map(
|
|
||||||
(a): EditorCompletionItem => ({
|
|
||||||
label: a,
|
|
||||||
description: 'keyword',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
),
|
|
||||||
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
|
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
|
||||||
functions: replaceFunctions,
|
functions: replaceFunctions,
|
||||||
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
|
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
|
||||||
};
|
};
|
||||||
|
|
||||||
oracleDialectInfo = {
|
oracleDialectInfo = {
|
||||||
|
name: 'Oracle',
|
||||||
icon: 'iconfont icon-oracle',
|
icon: 'iconfont icon-oracle',
|
||||||
defaultPort: 1521,
|
defaultPort: 1521,
|
||||||
formatSqlDialect: 'plsql',
|
formatSqlDialect: 'plsql',
|
||||||
@@ -141,7 +164,7 @@ class OracleDialect implements DbDialect {
|
|||||||
return oracleDialectInfo;
|
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 `
|
return `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM (
|
FROM (
|
||||||
@@ -268,16 +291,22 @@ class OracleDialect implements DbDialect {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
genColumnBasicSql(cl: RowDefinition): string {
|
genColumnBasicSql(cl: RowDefinition, create: boolean): string {
|
||||||
let length = this.getTypeLengthSql(cl);
|
let length = this.getTypeLengthSql(cl);
|
||||||
// 默认值
|
// 默认值
|
||||||
let defVal = this.getDefaultValueSql(cl);
|
let defVal = this.getDefaultValueSql(cl);
|
||||||
let incr = cl.auto_increment ? 'generated by default as IDENTITY' : '';
|
let incr = cl.auto_increment && create ? '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 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 {
|
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 createSql = '';
|
||||||
let tableCommentSql = '';
|
let tableCommentSql = '';
|
||||||
let columCommentSql = '';
|
let columCommentSql = '';
|
||||||
@@ -285,17 +314,17 @@ class OracleDialect implements DbDialect {
|
|||||||
// 创建表结构
|
// 创建表结构
|
||||||
let fields: string[] = [];
|
let fields: string[] = [];
|
||||||
data.fields.res.forEach((item: any) => {
|
data.fields.res.forEach((item: any) => {
|
||||||
item.name && fields.push(this.genColumnBasicSql(item));
|
item.name && fields.push(this.genColumnBasicSql(item, true));
|
||||||
// 列注释
|
// 列注释
|
||||||
if (item.remark) {
|
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) {
|
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;
|
return createSql + tableCommentSql + columCommentSql;
|
||||||
@@ -304,43 +333,95 @@ class OracleDialect implements DbDialect {
|
|||||||
getCreateIndexSql(tableData: any): string {
|
getCreateIndexSql(tableData: any): string {
|
||||||
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
|
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
|
||||||
// COMMENT ON INDEX idx_column_name IS 'Your index comment here';
|
// 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[] = [];
|
let sql: string[] = [];
|
||||||
tableData.indexs.res.forEach((a: any) => {
|
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(';');
|
return sql.join(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||||
let sql: string[] = [];
|
let schemaArr = tableData.db.split('/');
|
||||||
if (changeData.add.length > 0) {
|
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||||
changeData.add.forEach((a) => {
|
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||||
sql.push(`ALTER TABLE "${tableName}" add COLUMN ${this.genColumnBasicSql(a)}`);
|
|
||||||
if (a.remark) {
|
let baseSql = `ALTER TABLE ${dbTable} `;
|
||||||
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
|
|
||||||
|
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) {
|
if (changeData.add.length > 0) {
|
||||||
changeData.upd.forEach((a) => {
|
changeData.add.forEach((a) => {
|
||||||
sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`);
|
modifyArr.push(` ADD (${this.genColumnBasicSql(a, false)})`);
|
||||||
if (a.remark) {
|
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) {
|
if (changeData.del.length > 0) {
|
||||||
changeData.del.forEach((a) => {
|
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 dropIndexNames: string[] = [];
|
||||||
let addIndexs: any[] = [];
|
let addIndexs: any[] = [];
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pgDialectInfo = {
|
pgDialectInfo = {
|
||||||
|
name: 'PostgreSQL',
|
||||||
icon: 'iconfont icon-op-postgres',
|
icon: 'iconfont icon-op-postgres',
|
||||||
defaultPort: 5432,
|
defaultPort: 5432,
|
||||||
formatSqlDialect: 'postgresql',
|
formatSqlDialect: 'postgresql',
|
||||||
@@ -132,7 +133,7 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
return pgDialectInfo;
|
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(
|
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
|
||||||
pageNum,
|
pageNum,
|
||||||
limit
|
limit
|
||||||
@@ -228,7 +229,7 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
let marks = false;
|
let marks = false;
|
||||||
if (this.matchType(cl.type, ['char', 'time', 'date', 'text'])) {
|
if (this.matchType(cl.type, ['char', 'time', 'date', 'text'])) {
|
||||||
// 默认值是now()的time或date不需要加引号
|
// 默认值是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;
|
marks = false;
|
||||||
} else {
|
} else {
|
||||||
marks = true;
|
marks = true;
|
||||||
@@ -260,7 +261,10 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
let length = this.getTypeLengthSql(cl);
|
let length = this.getTypeLengthSql(cl);
|
||||||
// 默认值
|
// 默认值
|
||||||
let defVal = this.getDefaultValueSql(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 {
|
getCreateTableSql(data: any): string {
|
||||||
@@ -299,52 +303,77 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
|
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
|
||||||
// COMMENT ON INDEX idx_column_name IS 'Your index comment here';
|
// 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[] = [];
|
let sql: string[] = [];
|
||||||
tableData.indexs.res.forEach((a: any) => {
|
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) {
|
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(';');
|
return sql.join(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
|
||||||
let sql: 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) {
|
if (changeData.add.length > 0) {
|
||||||
changeData.add.forEach((a) => {
|
changeData.add.forEach((a) => {
|
||||||
let typeLength = this.getTypeLengthSql(a);
|
modifySql += `alter table ${dbTable} add ${this.genColumnBasicSql(a)};`;
|
||||||
let defaultSql = this.getDefaultValueSql(a);
|
|
||||||
sql.push(`ALTER TABLE ${tableName} add ${a.name} ${a.type}${typeLength} ${defaultSql}`);
|
|
||||||
if (a.remark) {
|
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) {
|
if (changeData.upd.length > 0) {
|
||||||
changeData.upd.forEach((a) => {
|
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);
|
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);
|
let defaultSql = this.getDefaultValueSql(a);
|
||||||
if (defaultSql) {
|
if (defaultSql) {
|
||||||
sql.push(`alter table ${tableName} alter column ${a.name} set ${defaultSql}`);
|
modifySql += `alter table ${dbTable} alter column ${this.quoteIdentifier(name)} set ${defaultSql} ;`;
|
||||||
}
|
|
||||||
if (a.remark) {
|
|
||||||
sql.push(`comment on column "${tableName}"."${a.name}" is '${a.remark}'`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changeData.del.length > 0) {
|
if (changeData.del.length > 0) {
|
||||||
changeData.del.forEach((a) => {
|
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 dropIndexNames: string[] = [];
|
||||||
let addIndexs: any[] = [];
|
let addIndexs: any[] = [];
|
||||||
@@ -378,9 +407,11 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
|
|
||||||
if (addIndexs.length > 0) {
|
if (addIndexs.length > 0) {
|
||||||
addIndexs.forEach((a) => {
|
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) {
|
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
|
// 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}'`;
|
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();
|
tagSelectRef.validate();
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
:tag-path="form.tagPath"
|
||||||
:resource-code="form.code"
|
:resource-code="form.code"
|
||||||
:resource-type="TagResourceTypeEnum.Machine.value"
|
:resource-type="TagResourceTypeEnum.Machine.value"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@@ -153,6 +154,7 @@ const state = reactive({
|
|||||||
form: {
|
form: {
|
||||||
id: null,
|
id: null,
|
||||||
code: '',
|
code: '',
|
||||||
|
tagPath: '',
|
||||||
ip: null,
|
ip: null,
|
||||||
port: 22,
|
port: 22,
|
||||||
name: null,
|
name: null,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="machine-list">
|
||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
:page-api="machineApi.list"
|
:page-api="machineApi.list"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<span v-if="!data.stat">-</span>
|
<span v-if="!data.stat">-</span>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-row>
|
<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)"
|
<span :class="getStatsFontClass(data.stat.memAvailable, data.stat.memTotal)"
|
||||||
>{{ formatByteSize(data.stat.memAvailable, 1) }}/{{ formatByteSize(data.stat.memTotal, 1) }}
|
>{{ formatByteSize(data.stat.memAvailable, 1) }}/{{ formatByteSize(data.stat.memTotal, 1) }}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</el-text>
|
</el-text>
|
||||||
</el-row>
|
</el-row>
|
||||||
<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>
|
CPU(空闲): <span :class="getStatsFontClass(data.stat.cpuIdle, 100)">{{ data.stat.cpuIdle.toFixed(0) }}%</span>
|
||||||
</el-text>
|
</el-text>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<span v-if="!data.stat?.fsInfos">-</span>
|
<span v-if="!data.stat?.fsInfos">-</span>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-row v-for="(i, idx) in data.stat.fsInfos.slice(0, 2)" :key="i.mountPoint">
|
<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) }}
|
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
|
||||||
</el-text>
|
</el-text>
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-row v-for="i in data.stat.fsInfos.slice(2)" :key="i.mountPoint">
|
<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) }}
|
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
|
||||||
</el-text>
|
</el-text>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -231,8 +231,8 @@ const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Machine.value), Se
|
|||||||
const columns = [
|
const columns = [
|
||||||
TableColumn.new('name', '名称'),
|
TableColumn.new('name', '名称'),
|
||||||
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
|
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
|
||||||
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(50),
|
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(55),
|
||||||
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(20),
|
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(25),
|
||||||
TableColumn.new('username', '用户名'),
|
TableColumn.new('username', '用户名'),
|
||||||
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
||||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
||||||
@@ -464,10 +464,6 @@ const showRec = (row: any) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.el-dialog__body {
|
|
||||||
padding: 2px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dropdown-link-machine-list {
|
.el-dropdown-link-machine-list {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--el-color-primary);
|
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-input v-model="state.keySeparator" placeholder="分割符" size="small" class="ml5" />
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="18">
|
<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>
|
||||||
<el-col :span="4">
|
<el-col :span="4">
|
||||||
<el-button
|
<el-button
|
||||||
|
|||||||
@@ -107,10 +107,10 @@ defineExpose({ getContent });
|
|||||||
|
|
||||||
.format-viewer-container .el-textarea textarea {
|
.format-viewer-container .el-textarea textarea {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
height: calc(100vh - 546px + v-bind(height));
|
height: calc(100vh - 550px + v-bind(height));
|
||||||
}
|
}
|
||||||
|
|
||||||
.format-viewer-container .monaco-editor-content {
|
.format-viewer-container .monaco-editor-content {
|
||||||
height: calc(100vh - 560px + v-bind(height)) !important;
|
height: calc(100vh - 565px + v-bind(height)) !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
|
<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 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="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>
|
<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"
|
class="key-detail-filter-value"
|
||||||
v-model="state.filterValue"
|
v-model="state.filterValue"
|
||||||
@keyup.enter="hscan(true, true)"
|
@keyup.enter="hscan(true, true)"
|
||||||
placeholder="输入关键词回车搜索"
|
placeholder="关键词回车搜索"
|
||||||
clearable
|
clearable
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted, reactive, watch, toRefs } from 'vue';
|
import { ref, onMounted, reactive, toRefs } from 'vue';
|
||||||
import { redisApi } from './api';
|
import { redisApi } from './api';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { notBlank } from '@/common/assert';
|
import { notBlank } from '@/common/assert';
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ const search = async () => {
|
|||||||
|
|
||||||
const changeStatus = async (row: any) => {
|
const changeStatus = async (row: any) => {
|
||||||
let id = row.id;
|
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({
|
await accountApi.changeStatus.request({
|
||||||
id,
|
id,
|
||||||
status,
|
status,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import vue from '@vitejs/plugin-vue';
|
|||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import type { UserConfig } from 'vite';
|
import type { UserConfig } from 'vite';
|
||||||
import { loadEnv } from './src/common/utils/viteBuild';
|
import { loadEnv } from './src/common/utils/viteBuild';
|
||||||
|
import { CodeInspectorPlugin } from 'code-inspector-plugin';
|
||||||
|
|
||||||
const pathResolve = (dir: string): any => {
|
const pathResolve = (dir: string): any => {
|
||||||
return resolve(__dirname, '.', dir);
|
return resolve(__dirname, '.', dir);
|
||||||
@@ -14,7 +15,12 @@ const alias: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const viteConfig: UserConfig = {
|
const viteConfig: UserConfig = {
|
||||||
plugins: [vue()],
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
CodeInspectorPlugin({
|
||||||
|
bundler: 'vite',
|
||||||
|
}),
|
||||||
|
],
|
||||||
root: process.cwd(),
|
root: process.cwd(),
|
||||||
resolve: {
|
resolve: {
|
||||||
alias,
|
alias,
|
||||||
|
|||||||
@@ -37,22 +37,19 @@ sqlite:
|
|||||||
# password: 111049
|
# password: 111049
|
||||||
# db: 0
|
# db: 0
|
||||||
log:
|
log:
|
||||||
# 日志等级, debug, info, warn, error
|
# 日志等级, debug, info, warn, error
|
||||||
level: info
|
level: info
|
||||||
# 日志格式类型, text/json
|
# 日志格式类型, text/json
|
||||||
type: text
|
type: text
|
||||||
# 是否记录方法调用栈信息
|
# 是否记录方法调用栈信息
|
||||||
add-source: false
|
add-source: false
|
||||||
|
# 日志文件配置
|
||||||
# file:
|
# file:
|
||||||
# path: ./
|
# path: ./log
|
||||||
# name: mayfly-go.log
|
# name: mayfly-go.log
|
||||||
db:
|
# # 日志文件的最大大小(以兆字节为单位)。当日志文件大小达到该值时,将触发切割操作
|
||||||
backup-path: ./backup
|
# max-size: 500
|
||||||
mysqlutil-path:
|
# # 根据文件名中的时间戳,设置保留旧日志文件的最大天数
|
||||||
mysql: ./mysqlutil/bin/mysql
|
# max-age: 60
|
||||||
mysqldump: ./mysqlutil/bin/mysqldump
|
# # 是否使用 gzip 压缩方式压缩轮转后的日志文件
|
||||||
mysqlbinlog: ./mysqlutil/bin/mysqlbinlog
|
# compress: true
|
||||||
mariadbutil-path:
|
|
||||||
mysql: ./mariadbutil/bin/mariadb
|
|
||||||
mysqldump: ./mariadbutil/bin/mariadb-dump
|
|
||||||
mysqlbinlog: ./mariadbutil/bin/mariadb-binlog
|
|
||||||
|
|||||||
@@ -10,31 +10,34 @@ require (
|
|||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/glebarez/sqlite v1.10.0
|
github.com/glebarez/sqlite v1.10.0
|
||||||
github.com/go-gormigrate/gormigrate/v2 v2.1.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/locales v0.14.1
|
||||||
github.com/go-playground/universal-translator v0.18.1
|
github.com/go-playground/universal-translator v0.18.1
|
||||||
github.com/go-playground/validator/v10 v10.14.0
|
github.com/go-playground/validator/v10 v10.14.0
|
||||||
github.com/go-sql-driver/mysql v1.7.1
|
github.com/go-sql-driver/mysql v1.7.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
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/gorilla/websocket v1.5.1
|
||||||
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231018071450-ac8d9f0167e9
|
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/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/mojocn/base64Captcha v1.3.6 // 验证码
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pkg/sftp v1.13.6
|
github.com/pkg/sftp v1.13.6
|
||||||
github.com/pquerna/otp v1.4.0
|
github.com/pquerna/otp v1.4.0
|
||||||
github.com/redis/go-redis/v9 v9.4.0
|
github.com/redis/go-redis/v9 v9.4.0
|
||||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
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
|
github.com/stretchr/testify v1.8.4
|
||||||
go.mongodb.org/mongo-driver v1.13.1 // mongo
|
go.mongodb.org/mongo-driver v1.13.1 // mongo
|
||||||
golang.org/x/crypto v0.18.0 // ssh
|
golang.org/x/crypto v0.18.0 // ssh
|
||||||
golang.org/x/oauth2 v0.15.0
|
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
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
// gorm
|
// gorm
|
||||||
gorm.io/driver/mysql v1.5.2
|
gorm.io/driver/mysql v1.5.2
|
||||||
gorm.io/gorm v1.25.5
|
gorm.io/gorm v1.25.6
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -49,8 +52,10 @@ require (
|
|||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // 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/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/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/golang/glog v1.0.0 // indirect
|
github.com/golang/glog v1.0.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // 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/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||||
golang.org/x/arch v0.3.0 // 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/image v0.13.0 // indirect
|
||||||
golang.org/x/net v0.19.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/sys v0.16.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
|||||||
@@ -1,11 +1,45 @@
|
|||||||
package initialize
|
package initialize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
dbInit "mayfly-go/internal/db/init"
|
"mayfly-go/pkg/biz"
|
||||||
machineInit "mayfly-go/internal/machine/init"
|
"mayfly-go/pkg/ioc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitOther() {
|
// 初始化ioc函数
|
||||||
machineInit.Init()
|
type InitIocFunc func()
|
||||||
dbInit.Init()
|
|
||||||
|
// 初始化函数
|
||||||
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"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/config"
|
||||||
"mayfly-go/pkg/middleware"
|
"mayfly-go/pkg/middleware"
|
||||||
"mayfly-go/static"
|
"mayfly-go/static"
|
||||||
@@ -20,6 +11,18 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"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 {
|
func InitRouter() *gin.Engine {
|
||||||
// server配置
|
// server配置
|
||||||
serverConfig := config.Conf.Server
|
serverConfig := config.Conf.Server
|
||||||
@@ -43,20 +46,11 @@ func InitRouter() *gin.Engine {
|
|||||||
|
|
||||||
// 设置路由组
|
// 设置路由组
|
||||||
api := router.Group(serverConfig.ContextPath + "/api")
|
api := router.Group(serverConfig.ContextPath + "/api")
|
||||||
{
|
// 调用所有模块注册的初始化路由函数
|
||||||
common_router.Init(api)
|
for _, initRouterFunc := range initRouterFuncs {
|
||||||
|
initRouterFunc(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)
|
|
||||||
}
|
}
|
||||||
|
initRouterFuncs = nil
|
||||||
|
|
||||||
return router
|
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
|
package initialize
|
||||||
|
|
||||||
import (
|
// 系统进程退出终止函数
|
||||||
dbApp "mayfly-go/internal/db/application"
|
type TerminateFunc func()
|
||||||
|
|
||||||
|
var (
|
||||||
|
terminateFuncs = make([]TerminateFunc, 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 终止服务后的一些操作
|
// 添加系统退出终止时执行的函数,由各个默认自行添加
|
||||||
func Terminate() {
|
func AddTerminateFunc(terminateFunc TerminateFunc) {
|
||||||
closeDbTasks()
|
terminateFuncs = append(terminateFuncs, terminateFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeDbTasks() {
|
// 终止进程服务后的一些操作
|
||||||
restoreApp := dbApp.GetDbRestoreApp()
|
func Terminate() {
|
||||||
if restoreApp != nil {
|
for _, terminateFunc := range terminateFuncs {
|
||||||
restoreApp.Close()
|
terminateFunc()
|
||||||
}
|
|
||||||
binlogApp := dbApp.GetDbBinlogApp()
|
|
||||||
if binlogApp != nil {
|
|
||||||
binlogApp.Close()
|
|
||||||
}
|
|
||||||
backupApp := dbApp.GetDbBackupApp()
|
|
||||||
if backupApp != nil {
|
|
||||||
backupApp.Close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AccountLogin struct {
|
type AccountLogin struct {
|
||||||
AccountApp sysapp.Account
|
AccountApp sysapp.Account `inject:""`
|
||||||
MsgApp msgapp.Msg
|
MsgApp msgapp.Msg `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 用户账号密码登录 **/
|
/** 用户账号密码登录 **/
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type LdapLogin struct {
|
type LdapLogin struct {
|
||||||
AccountApp sysapp.Account
|
AccountApp sysapp.Account `inject:""`
|
||||||
MsgApp msgapp.Msg
|
MsgApp msgapp.Msg `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
// @router /auth/ldap/enabled [get]
|
// @router /auth/ldap/enabled [get]
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Oauth2Login struct {
|
type Oauth2Login struct {
|
||||||
Oauth2App application.Oauth2
|
Oauth2App application.Oauth2 `inject:""`
|
||||||
AccountApp sysapp.Account
|
AccountApp sysapp.Account `inject:""`
|
||||||
MsgApp msgapp.Msg
|
MsgApp msgapp.Msg `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Oauth2Login) OAuth2Login(rc *req.Ctx) {
|
func (a *Oauth2Login) OAuth2Login(rc *req.Ctx) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package application
|
package application
|
||||||
|
|
||||||
import "mayfly-go/internal/auth/infrastructure/persistence"
|
import (
|
||||||
|
"mayfly-go/internal/auth/infrastructure/persistence"
|
||||||
var (
|
"mayfly-go/pkg/ioc"
|
||||||
authApp = newAuthApp(persistence.GetOauthAccountRepo())
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAuthApp() Oauth2 {
|
func InitIoc() {
|
||||||
return authApp
|
persistence.Init()
|
||||||
|
|
||||||
|
ioc.Register(new(oauth2AppImpl), ioc.WithComponentName("Oauth2App"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,27 +14,21 @@ type Oauth2 interface {
|
|||||||
Unbind(accountId uint64)
|
Unbind(accountId uint64)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAuthApp(oauthAccountRepo repository.Oauth2Account) Oauth2 {
|
|
||||||
return &oauth2AppImpl{
|
|
||||||
oauthAccountRepo: oauthAccountRepo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type oauth2AppImpl struct {
|
type oauth2AppImpl struct {
|
||||||
oauthAccountRepo repository.Oauth2Account
|
Oauth2AccountRepo repository.Oauth2Account `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *oauth2AppImpl) GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error {
|
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 {
|
func (a *oauth2AppImpl) BindOAuthAccount(e *entity.Oauth2Account) error {
|
||||||
if e.Id == 0 {
|
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) {
|
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 (
|
import (
|
||||||
sysapp "mayfly-go/internal/sys/application"
|
sysapp "mayfly-go/internal/sys/application"
|
||||||
|
"mayfly-go/pkg/utils/conv"
|
||||||
"mayfly-go/pkg/utils/stringx"
|
"mayfly-go/pkg/utils/stringx"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,8 +27,8 @@ func GetAccountLoginSecurity() *AccountLoginSecurity {
|
|||||||
als := new(AccountLoginSecurity)
|
als := new(AccountLoginSecurity)
|
||||||
als.UseCaptcha = c.ConvBool(jm["useCaptcha"], true)
|
als.UseCaptcha = c.ConvBool(jm["useCaptcha"], true)
|
||||||
als.UseOtp = c.ConvBool(jm["useOtp"], false)
|
als.UseOtp = c.ConvBool(jm["useOtp"], false)
|
||||||
als.LoginFailCount = stringx.ConvInt(jm["loginFailCount"], 5)
|
als.LoginFailCount = conv.Str2Int(jm["loginFailCount"], 5)
|
||||||
als.LoginFailMin = stringx.ConvInt(jm["loginFailMin"], 10)
|
als.LoginFailMin = conv.Str2Int(jm["loginFailMin"], 10)
|
||||||
otpIssuer := jm["otpIssuer"]
|
otpIssuer := jm["otpIssuer"]
|
||||||
if otpIssuer == "" {
|
if otpIssuer == "" {
|
||||||
otpIssuer = "mayfly-go"
|
otpIssuer = "mayfly-go"
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package persistence
|
package persistence
|
||||||
|
|
||||||
import "mayfly-go/internal/auth/domain/repository"
|
import (
|
||||||
|
"mayfly-go/pkg/ioc"
|
||||||
var (
|
|
||||||
authAccountRepo = newAuthAccountRepo()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetOauthAccountRepo() repository.Oauth2Account {
|
func Init() {
|
||||||
return authAccountRepo
|
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 (
|
import (
|
||||||
"mayfly-go/internal/auth/api"
|
"mayfly-go/internal/auth/api"
|
||||||
"mayfly-go/internal/auth/application"
|
"mayfly-go/pkg/biz"
|
||||||
msgapp "mayfly-go/internal/msg/application"
|
"mayfly-go/pkg/ioc"
|
||||||
sysapp "mayfly-go/internal/sys/application"
|
|
||||||
"mayfly-go/pkg/req"
|
"mayfly-go/pkg/req"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(router *gin.RouterGroup) {
|
func Init(router *gin.RouterGroup) {
|
||||||
accountLogin := &api.AccountLogin{
|
accountLogin := new(api.AccountLogin)
|
||||||
AccountApp: sysapp.GetAccountApp(),
|
biz.ErrIsNil(ioc.Inject(accountLogin))
|
||||||
MsgApp: msgapp.GetMsgApp(),
|
|
||||||
}
|
|
||||||
|
|
||||||
ldapLogin := &api.LdapLogin{
|
ldapLogin := new(api.LdapLogin)
|
||||||
AccountApp: sysapp.GetAccountApp(),
|
biz.ErrIsNil(ioc.Inject(ldapLogin))
|
||||||
MsgApp: msgapp.GetMsgApp(),
|
|
||||||
}
|
|
||||||
|
|
||||||
oauth2Login := &api.Oauth2Login{
|
oauth2Login := new(api.Oauth2Login)
|
||||||
Oauth2App: application.GetAuthApp(),
|
biz.ErrIsNil(ioc.Inject(oauth2Login))
|
||||||
AccountApp: sysapp.GetAccountApp(),
|
|
||||||
MsgApp: msgapp.GetMsgApp(),
|
|
||||||
}
|
|
||||||
|
|
||||||
rg := router.Group("/auth")
|
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) {
|
func Init(router *gin.RouterGroup) {
|
||||||
InitCommonRouter(router)
|
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 {
|
type Db struct {
|
||||||
InstanceApp application.Instance
|
InstanceApp application.Instance `inject:"DbInstanceApp"`
|
||||||
DbApp application.Db
|
DbApp application.Db `inject:""`
|
||||||
DbSqlExecApp application.DbSqlExec
|
DbSqlExecApp application.DbSqlExec `inject:""`
|
||||||
MsgApp msgapp.Msg
|
MsgApp msgapp.Msg `inject:""`
|
||||||
TagApp tagapp.TagTree
|
TagApp tagapp.TagTree `inject:"TagTreeApp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// @router /api/dbs [get]
|
// @router /api/dbs [get]
|
||||||
@@ -78,8 +78,6 @@ func (d *Db) DeleteDb(rc *req.Ctx) {
|
|||||||
d.DbApp.Delete(ctx, dbId)
|
d.DbApp.Delete(ctx, dbId)
|
||||||
// 删除该库的sql执行记录
|
// 删除该库的sql执行记录
|
||||||
d.DbSqlExecApp.DeleteBy(ctx, &entity.DbSqlExec{DbId: dbId})
|
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")
|
writer.WriteString("BEGIN;\n")
|
||||||
}
|
}
|
||||||
insertSql := "INSERT INTO %s VALUES (%s);\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
|
var values []string
|
||||||
writer.TryFlush()
|
writer.TryFlush()
|
||||||
for _, column := range columns {
|
for _, column := range columns {
|
||||||
@@ -462,6 +460,20 @@ func (d *Db) GetSchemas(rc *req.Ctx) {
|
|||||||
rc.ResData = res
|
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 {
|
func getDbId(g *gin.Context) uint64 {
|
||||||
dbId, _ := strconv.Atoi(g.Param("dbId"))
|
dbId, _ := strconv.Atoi(g.Param("dbId"))
|
||||||
biz.IsTrue(dbId > 0, "dbId错误")
|
biz.IsTrue(dbId > 0, "dbId错误")
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ import (
|
|||||||
"mayfly-go/pkg/biz"
|
"mayfly-go/pkg/biz"
|
||||||
"mayfly-go/pkg/ginx"
|
"mayfly-go/pkg/ginx"
|
||||||
"mayfly-go/pkg/req"
|
"mayfly-go/pkg/req"
|
||||||
|
"mayfly-go/pkg/utils/timex"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DbBackup struct {
|
type DbBackup struct {
|
||||||
DbBackupApp *application.DbBackupApp
|
backupApp *application.DbBackupApp `inject:"DbBackupApp"`
|
||||||
DbApp application.Db
|
dbApp application.Db `inject:"DbApp"`
|
||||||
|
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: 鉴权,避免未经授权进行数据库备份和恢复
|
// todo: 鉴权,避免未经授权进行数据库备份和恢复
|
||||||
@@ -25,13 +28,13 @@ type DbBackup struct {
|
|||||||
func (d *DbBackup) GetPageList(rc *req.Ctx) {
|
func (d *DbBackup) GetPageList(rc *req.Ctx) {
|
||||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", 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")
|
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.DbInstanceId = db.InstanceId
|
||||||
queryCond.InDbNames = strings.Fields(db.Database)
|
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")
|
biz.ErrIsNilAppendErr(err, "获取数据库备份任务失败: %v")
|
||||||
rc.ResData = res
|
rc.ResData = res
|
||||||
}
|
}
|
||||||
@@ -48,23 +51,22 @@ func (d *DbBackup) Create(rc *req.Ctx) {
|
|||||||
|
|
||||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", 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")
|
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||||
|
|
||||||
jobs := make([]*entity.DbBackup, 0, len(dbNames))
|
jobs := make([]*entity.DbBackup, 0, len(dbNames))
|
||||||
for _, dbName := range dbNames {
|
for _, dbName := range dbNames {
|
||||||
job := &entity.DbBackup{
|
job := &entity.DbBackup{
|
||||||
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeBackup),
|
DbInstanceId: db.InstanceId,
|
||||||
Enabled: true,
|
DbName: dbName,
|
||||||
Repeated: backupForm.Repeated,
|
Enabled: true,
|
||||||
StartTime: backupForm.StartTime,
|
Repeated: backupForm.Repeated,
|
||||||
Interval: backupForm.Interval,
|
StartTime: backupForm.StartTime,
|
||||||
Name: backupForm.Name,
|
Interval: backupForm.Interval,
|
||||||
|
Name: backupForm.Name,
|
||||||
}
|
}
|
||||||
job.DbName = dbName
|
|
||||||
jobs = append(jobs, job)
|
jobs = append(jobs, job)
|
||||||
}
|
}
|
||||||
biz.ErrIsNilAppendErr(d.DbBackupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
|
biz.ErrIsNilAppendErr(d.backupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update 保存数据库备份任务
|
// Update 保存数据库备份任务
|
||||||
@@ -74,17 +76,18 @@ func (d *DbBackup) Update(rc *req.Ctx) {
|
|||||||
ginx.BindJsonAndValid(rc.GinCtx, backupForm)
|
ginx.BindJsonAndValid(rc.GinCtx, backupForm)
|
||||||
rc.ReqParam = backupForm
|
rc.ReqParam = backupForm
|
||||||
|
|
||||||
job := entity.NewDbJob(entity.DbJobTypeBackup).(*entity.DbBackup)
|
job := &entity.DbBackup{}
|
||||||
job.Id = backupForm.Id
|
job.Id = backupForm.Id
|
||||||
job.Name = backupForm.Name
|
job.Name = backupForm.Name
|
||||||
job.StartTime = backupForm.StartTime
|
job.StartTime = backupForm.StartTime
|
||||||
job.Interval = backupForm.Interval
|
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 {
|
func (d *DbBackup) walk(rc *req.Ctx, paramName string, fn func(ctx context.Context, id uint64) error) error {
|
||||||
idsStr := ginx.PathParam(rc.GinCtx, "backupId")
|
idsStr := ginx.PathParam(rc.GinCtx, paramName)
|
||||||
biz.NotEmpty(idsStr, "backupId 为空")
|
biz.NotEmpty(idsStr, paramName+" 为空")
|
||||||
rc.ReqParam = idsStr
|
rc.ReqParam = idsStr
|
||||||
ids := strings.Fields(idsStr)
|
ids := strings.Fields(idsStr)
|
||||||
for _, v := range ids {
|
for _, v := range ids {
|
||||||
@@ -104,28 +107,28 @@ func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint6
|
|||||||
// Delete 删除数据库备份任务
|
// Delete 删除数据库备份任务
|
||||||
// @router /api/dbs/:dbId/backups/:backupId [DELETE]
|
// @router /api/dbs/:dbId/backups/:backupId [DELETE]
|
||||||
func (d *DbBackup) Delete(rc *req.Ctx) {
|
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")
|
biz.ErrIsNilAppendErr(err, "删除数据库备份任务失败: %v")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable 启用数据库备份任务
|
// Enable 启用数据库备份任务
|
||||||
// @router /api/dbs/:dbId/backups/:backupId/enable [PUT]
|
// @router /api/dbs/:dbId/backups/:backupId/enable [PUT]
|
||||||
func (d *DbBackup) Enable(rc *req.Ctx) {
|
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")
|
biz.ErrIsNilAppendErr(err, "启用数据库备份任务失败: %v")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable 禁用数据库备份任务
|
// Disable 禁用数据库备份任务
|
||||||
// @router /api/dbs/:dbId/backups/:backupId/disable [PUT]
|
// @router /api/dbs/:dbId/backups/:backupId/disable [PUT]
|
||||||
func (d *DbBackup) Disable(rc *req.Ctx) {
|
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")
|
biz.ErrIsNilAppendErr(err, "禁用数据库备份任务失败: %v")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start 禁用数据库备份任务
|
// Start 禁用数据库备份任务
|
||||||
// @router /api/dbs/:dbId/backups/:backupId/start [PUT]
|
// @router /api/dbs/:dbId/backups/:backupId/start [PUT]
|
||||||
func (d *DbBackup) Start(rc *req.Ctx) {
|
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")
|
biz.ErrIsNilAppendErr(err, "运行数据库备份任务失败: %v")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,10 +136,10 @@ func (d *DbBackup) Start(rc *req.Ctx) {
|
|||||||
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
||||||
func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
||||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
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")
|
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||||
dbNames := strings.Fields(db.Database)
|
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")
|
biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
|
||||||
rc.ResData = dbNamesWithoutBackup
|
rc.ResData = dbNamesWithoutBackup
|
||||||
}
|
}
|
||||||
@@ -146,13 +149,74 @@ func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
|||||||
func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) {
|
func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) {
|
||||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", 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")
|
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||||
|
|
||||||
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
|
backupHistoryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
|
||||||
queryCond.DbInstanceId = db.InstanceId
|
backupHistoryCond.DbInstanceId = db.InstanceId
|
||||||
queryCond.InDbNames = strings.Fields(db.Database)
|
backupHistoryCond.InDbNames = strings.Fields(db.Database)
|
||||||
res, err := d.DbBackupApp.GetHistoryPageList(queryCond, page, new([]vo.DbBackupHistory))
|
backupHistories := make([]*vo.DbBackupHistory, 0, page.PageSize)
|
||||||
|
res, err := d.backupApp.GetHistoryPageList(backupHistoryCond, page, &backupHistories)
|
||||||
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
|
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
|
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
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"mayfly-go/internal/db/api/form"
|
"mayfly-go/internal/db/api/form"
|
||||||
"mayfly-go/internal/db/api/vo"
|
"mayfly-go/internal/db/api/vo"
|
||||||
@@ -15,11 +14,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DataSyncTask struct {
|
type DataSyncTask struct {
|
||||||
DataSyncTaskApp application.DataSyncTask
|
DataSyncTaskApp application.DataSyncTask `inject:"DbDataSyncTaskApp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataSyncTask) Tasks(rc *req.Ctx) {
|
func (d *DataSyncTask) Tasks(rc *req.Ctx) {
|
||||||
@@ -47,13 +45,6 @@ func (d *DataSyncTask) SaveTask(rc *req.Ctx) {
|
|||||||
task.DataSql = sql
|
task.DataSql = sql
|
||||||
form.DataSql = sql
|
form.DataSql = sql
|
||||||
|
|
||||||
key := task.TaskKey
|
|
||||||
// 判断key为空就生成随机key
|
|
||||||
if key == "" {
|
|
||||||
key = uuid.New().String()
|
|
||||||
task.TaskKey = key
|
|
||||||
}
|
|
||||||
|
|
||||||
rc.ReqParam = form
|
rc.ReqParam = form
|
||||||
biz.ErrIsNil(d.DataSyncTaskApp.Save(rc.MetaCtx, task))
|
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) {
|
func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
|
||||||
form := &form.DataSyncTaskStatusForm{}
|
form := &form.DataSyncTaskStatusForm{}
|
||||||
task := ginx.BindJsonAndCopyTo[*entity.DataSyncTask](rc.GinCtx, form, new(entity.DataSyncTask))
|
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 {
|
if task.Status == entity.DataSyncTaskStatusEnable {
|
||||||
task, err := d.DataSyncTaskApp.GetById(new(entity.DataSyncTask), task.Id)
|
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) {
|
func (d *DataSyncTask) Run(rc *req.Ctx) {
|
||||||
taskId := getTaskId(rc.GinCtx)
|
taskId := getTaskId(rc.GinCtx)
|
||||||
rc.ReqParam = taskId
|
rc.ReqParam = taskId
|
||||||
d.DataSyncTaskApp.RunCronJob(taskId)
|
_ = d.DataSyncTaskApp.RunCronJob(taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataSyncTask) Stop(rc *req.Ctx) {
|
func (d *DataSyncTask) Stop(rc *req.Ctx) {
|
||||||
@@ -99,7 +90,7 @@ func (d *DataSyncTask) Stop(rc *req.Ctx) {
|
|||||||
task := new(entity.DataSyncTask)
|
task := new(entity.DataSyncTask)
|
||||||
task.Id = taskId
|
task.Id = taskId
|
||||||
task.RunningState = entity.DataSyncTaskRunStateStop
|
task.RunningState = entity.DataSyncTaskRunStateStop
|
||||||
_ = d.DataSyncTaskApp.UpdateById(context.Background(), task)
|
_ = d.DataSyncTaskApp.UpdateById(rc.MetaCtx, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataSyncTask) GetTask(rc *req.Ctx) {
|
func (d *DataSyncTask) GetTask(rc *req.Ctx) {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Instance struct {
|
type Instance struct {
|
||||||
InstanceApp application.Instance
|
InstanceApp application.Instance `inject:"DbInstanceApp"`
|
||||||
DbApp application.Db
|
DbApp application.Db `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instances 获取数据库实例信息
|
// Instances 获取数据库实例信息
|
||||||
@@ -87,16 +87,10 @@ func (d *Instance) DeleteInstance(rc *req.Ctx) {
|
|||||||
|
|
||||||
for _, v := range ids {
|
for _, v := range ids {
|
||||||
value, err := strconv.Atoi(v)
|
value, err := strconv.Atoi(v)
|
||||||
biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s")
|
biz.ErrIsNilAppendErr(err, "删除数据库实例失败: %s")
|
||||||
instanceId := uint64(value)
|
instanceId := uint64(value)
|
||||||
if d.DbApp.Count(&entity.DbQuery{InstanceId: instanceId}) != 0 {
|
err = d.InstanceApp.Delete(rc.MetaCtx, instanceId)
|
||||||
instance, err := d.InstanceApp.GetById(new(entity.DbInstance), instanceId, "name")
|
biz.ErrIsNilAppendErr(err, "删除数据库实例失败: %s")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DbRestore struct {
|
type DbRestore struct {
|
||||||
DbRestoreApp *application.DbRestoreApp
|
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
|
||||||
DbApp application.Db
|
dbApp application.Db `inject:"DbApp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPageList 获取数据库恢复任务
|
// GetPageList 获取数据库恢复任务
|
||||||
@@ -23,14 +23,14 @@ type DbRestore struct {
|
|||||||
func (d *DbRestore) GetPageList(rc *req.Ctx) {
|
func (d *DbRestore) GetPageList(rc *req.Ctx) {
|
||||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", 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")
|
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||||
|
|
||||||
var restores []vo.DbRestore
|
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.DbInstanceId = db.InstanceId
|
||||||
queryCond.InDbNames = strings.Fields(db.Database)
|
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")
|
biz.ErrIsNilAppendErr(err, "获取数据库恢复任务失败: %v")
|
||||||
rc.ResData = res
|
rc.ResData = res
|
||||||
}
|
}
|
||||||
@@ -44,11 +44,12 @@ func (d *DbRestore) Create(rc *req.Ctx) {
|
|||||||
|
|
||||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
||||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", 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")
|
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||||
|
|
||||||
job := &entity.DbRestore{
|
job := &entity.DbRestore{
|
||||||
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeRestore),
|
DbInstanceId: db.InstanceId,
|
||||||
|
DbName: restoreForm.DbName,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Repeated: restoreForm.Repeated,
|
Repeated: restoreForm.Repeated,
|
||||||
StartTime: restoreForm.StartTime,
|
StartTime: restoreForm.StartTime,
|
||||||
@@ -58,8 +59,11 @@ func (d *DbRestore) Create(rc *req.Ctx) {
|
|||||||
DbBackupHistoryId: restoreForm.DbBackupHistoryId,
|
DbBackupHistoryId: restoreForm.DbBackupHistoryId,
|
||||||
DbBackupHistoryName: restoreForm.DbBackupHistoryName,
|
DbBackupHistoryName: restoreForm.DbBackupHistoryName,
|
||||||
}
|
}
|
||||||
job.DbName = restoreForm.DbName
|
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
|
||||||
biz.ErrIsNilAppendErr(d.DbRestoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
|
}
|
||||||
|
|
||||||
|
func (d *DbRestore) createWithBackupHistory(backupHistoryIds string) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update 保存数据库恢复任务
|
// Update 保存数据库恢复任务
|
||||||
@@ -73,7 +77,7 @@ func (d *DbRestore) Update(rc *req.Ctx) {
|
|||||||
job.Id = restoreForm.Id
|
job.Id = restoreForm.Id
|
||||||
job.StartTime = restoreForm.StartTime
|
job.StartTime = restoreForm.StartTime
|
||||||
job.Interval = restoreForm.Interval
|
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 {
|
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 删除数据库恢复任务
|
// Delete 删除数据库恢复任务
|
||||||
// @router /api/dbs/:dbId/restores/:restoreId [DELETE]
|
// @router /api/dbs/:dbId/restores/:restoreId [DELETE]
|
||||||
func (d *DbRestore) Delete(rc *req.Ctx) {
|
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")
|
biz.ErrIsNilAppendErr(err, "删除数据库恢复任务失败: %v")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable 启用数据库恢复任务
|
// Enable 启用数据库恢复任务
|
||||||
// @router /api/dbs/:dbId/restores/:restoreId/enable [PUT]
|
// @router /api/dbs/:dbId/restores/:restoreId/enable [PUT]
|
||||||
func (d *DbRestore) Enable(rc *req.Ctx) {
|
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")
|
biz.ErrIsNilAppendErr(err, "启用数据库恢复任务失败: %v")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable 禁用数据库恢复任务
|
// Disable 禁用数据库恢复任务
|
||||||
// @router /api/dbs/:dbId/restores/:restoreId/disable [PUT]
|
// @router /api/dbs/:dbId/restores/:restoreId/disable [PUT]
|
||||||
func (d *DbRestore) Disable(rc *req.Ctx) {
|
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")
|
biz.ErrIsNilAppendErr(err, "禁用数据库恢复任务失败: %v")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,21 +124,21 @@ func (d *DbRestore) Disable(rc *req.Ctx) {
|
|||||||
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
||||||
func (d *DbRestore) GetDbNamesWithoutRestore(rc *req.Ctx) {
|
func (d *DbRestore) GetDbNamesWithoutRestore(rc *req.Ctx) {
|
||||||
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
|
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")
|
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||||
dbNames := strings.Fields(db.Database)
|
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")
|
biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
|
||||||
rc.ResData = dbNamesWithoutRestore
|
rc.ResData = dbNamesWithoutRestore
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据库备份历史
|
// GetHistoryPageList 获取数据库备份历史
|
||||||
// @router /api/dbs/:dbId/restores/:restoreId/histories [GET]
|
// @router /api/dbs/:dbId/restores/:restoreId/histories [GET]
|
||||||
func (d *DbRestore) GetHistoryPageList(rc *req.Ctx) {
|
func (d *DbRestore) GetHistoryPageList(rc *req.Ctx) {
|
||||||
queryCond := &entity.DbRestoreHistoryQuery{
|
queryCond := &entity.DbRestoreHistoryQuery{
|
||||||
DbRestoreId: uint64(ginx.PathParamInt(rc.GinCtx, "restoreId")),
|
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")
|
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
|
||||||
rc.ResData = res
|
rc.ResData = res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DbSql struct {
|
type DbSql struct {
|
||||||
DbSqlApp application.DbSql
|
DbSqlApp application.DbSql `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
// @router /api/db/:dbId/sql [post]
|
// @router /api/db/:dbId/sql [post]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DbSqlExec struct {
|
type DbSqlExec struct {
|
||||||
DbSqlExecApp application.DbSqlExec
|
DbSqlExecApp application.DbSqlExec `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) {
|
func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) {
|
||||||
|
|||||||
@@ -23,3 +23,11 @@ type DbSqlExecForm struct {
|
|||||||
Sql string `binding:"required" json:"sql"` // 执行sql
|
Sql string `binding:"required" json:"sql"` // 执行sql
|
||||||
Remark string `json:"remark"` // 执行备注
|
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:"-"` // 间隔时间: 为零表示单次执行,为正表示反复执行
|
Interval time.Duration `json:"-"` // 间隔时间: 为零表示单次执行,为正表示反复执行
|
||||||
IntervalDay uint64 `json:"intervalDay"` // 间隔天数: 为零表示单次执行,为正表示反复执行
|
IntervalDay uint64 `json:"intervalDay"` // 间隔天数: 为零表示单次执行,为正表示反复执行
|
||||||
Repeated bool `json:"repeated"` // 是否重复执行
|
Repeated bool `json:"repeated"` // 是否重复执行
|
||||||
|
MaxSaveDays int `json:"maxSaveDays"` // 数据库备份历史保留天数,过期将自动删除
|
||||||
}
|
}
|
||||||
|
|
||||||
func (restore *DbBackupForm) UnmarshalJSON(data []byte) error {
|
func (restore *DbBackupForm) UnmarshalJSON(data []byte) error {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ type InstanceForm struct {
|
|||||||
Name string `binding:"required" json:"name"`
|
Name string `binding:"required" json:"name"`
|
||||||
Type string `binding:"required" json:"type"` // 类型,mysql oracle等
|
Type string `binding:"required" json:"type"` // 类型,mysql oracle等
|
||||||
Host string `binding:"required" json:"host"`
|
Host string `binding:"required" json:"host"`
|
||||||
Port int `binding:"required" json:"port"`
|
Port int `json:"port"`
|
||||||
Sid string `json:"sid"`
|
Sid string `json:"sid"`
|
||||||
Username string `binding:"required" json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Params string `json:"params"`
|
Params string `json:"params"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
|
|||||||
@@ -2,37 +2,51 @@ package vo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/pkg/utils/timex"
|
"mayfly-go/pkg/utils/timex"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DbBackup 数据库备份任务
|
// DbBackup 数据库备份任务
|
||||||
type DbBackup struct {
|
type DbBackup struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
DbName string `json:"dbName"` // 数据库名
|
DbName string `json:"dbName"` // 数据库名
|
||||||
CreateTime time.Time `json:"createTime"` // 创建时间
|
CreateTime time.Time `json:"createTime"` // 创建时间
|
||||||
StartTime time.Time `json:"startTime"` // 开始时间
|
StartTime time.Time `json:"startTime"` // 开始时间
|
||||||
Interval time.Duration `json:"-"` // 间隔时间
|
Interval time.Duration `json:"-"` // 间隔时间
|
||||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||||
Enabled bool `json:"enabled"` // 是否启用
|
MaxSaveDays int `json:"maxSaveDays"` // 数据库备份历史保留天数,过期将自动删除
|
||||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
Enabled bool `json:"enabled"` // 是否启用
|
||||||
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
|
||||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||||
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
LastStatus entity.DbJobStatus `json:"lastStatus"` // 最近一次执行状态
|
||||||
Name string `json:"name"` // 备份任务名称
|
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||||
|
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||||
|
Name string `json:"name"` // 备份任务名称
|
||||||
}
|
}
|
||||||
|
|
||||||
func (backup *DbBackup) MarshalJSON() ([]byte, error) {
|
func (backup *DbBackup) MarshalJSON() ([]byte, error) {
|
||||||
type dbBackup DbBackup
|
type dbBackup DbBackup
|
||||||
backup.IntervalDay = uint64(backup.Interval / time.Hour / 24)
|
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))
|
return json.Marshal((*dbBackup)(backup))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DbBackupHistory 数据库备份历史
|
// DbBackupHistory 数据库备份历史
|
||||||
type DbBackupHistory struct {
|
type DbBackupHistory struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
DbBackupId uint64 `json:"dbBackupId"`
|
DbBackupId uint64 `json:"dbBackupId"`
|
||||||
CreateTime time.Time `json:"createTime"`
|
CreateTime time.Time `json:"createTime"`
|
||||||
DbName string `json:"dbName"` // 数据库名称
|
DbName string `json:"dbName"` // 数据库名称
|
||||||
Name string `json:"name"` // 备份历史名称
|
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:"-"` // 间隔时间
|
Interval time.Duration `json:"-"` // 间隔时间
|
||||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||||
Enabled bool `json:"enabled"` // 是否启用
|
Enabled bool `json:"enabled"` // 是否启用
|
||||||
|
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
|
||||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||||
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
||||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||||
@@ -27,6 +28,13 @@ type DbRestore struct {
|
|||||||
func (restore *DbRestore) MarshalJSON() ([]byte, error) {
|
func (restore *DbRestore) MarshalJSON() ([]byte, error) {
|
||||||
type dbBackup DbRestore
|
type dbBackup DbRestore
|
||||||
restore.IntervalDay = uint64(restore.Interval / time.Hour / 24)
|
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))
|
return json.Marshal((*dbBackup)(restore))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,91 +2,53 @@ package application
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
|
||||||
"mayfly-go/internal/db/infrastructure/persistence"
|
"mayfly-go/internal/db/infrastructure/persistence"
|
||||||
tagapp "mayfly-go/internal/tag/application"
|
"mayfly-go/pkg/ioc"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func InitIoc() {
|
||||||
instanceApp Instance
|
persistence.Init()
|
||||||
dbApp Db
|
|
||||||
dbSqlExecApp DbSqlExec
|
ioc.Register(new(instanceAppImpl), ioc.WithComponentName("DbInstanceApp"))
|
||||||
dbSqlApp DbSql
|
ioc.Register(new(dbAppImpl), ioc.WithComponentName("DbApp"))
|
||||||
dbBackupApp *DbBackupApp
|
ioc.Register(new(dbSqlExecAppImpl), ioc.WithComponentName("DbSqlExecApp"))
|
||||||
dbRestoreApp *DbRestoreApp
|
ioc.Register(new(dbSqlAppImpl), ioc.WithComponentName("DbSqlApp"))
|
||||||
dbBinlogApp *DbBinlogApp
|
ioc.Register(new(dataSyncAppImpl), ioc.WithComponentName("DbDataSyncTaskApp"))
|
||||||
dataSyncApp DataSyncTask
|
|
||||||
)
|
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() {
|
func Init() {
|
||||||
sync.OnceFunc(func() {
|
sync.OnceFunc(func() {
|
||||||
repositories := &repository.Repositories{
|
if err := GetDbBackupApp().Init(); err != nil {
|
||||||
Instance: persistence.GetInstanceRepo(),
|
panic(fmt.Sprintf("初始化 DbBackupApp 失败: %v", err))
|
||||||
Backup: persistence.NewDbBackupRepo(),
|
|
||||||
BackupHistory: persistence.NewDbBackupHistoryRepo(),
|
|
||||||
Restore: persistence.NewDbRestoreRepo(),
|
|
||||||
RestoreHistory: persistence.NewDbRestoreHistoryRepo(),
|
|
||||||
Binlog: persistence.NewDbBinlogRepo(),
|
|
||||||
BinlogHistory: persistence.NewDbBinlogHistoryRepo(),
|
|
||||||
}
|
}
|
||||||
var err error
|
if err := GetDbRestoreApp().Init(); err != nil {
|
||||||
instanceRepo := persistence.GetInstanceRepo()
|
panic(fmt.Sprintf("初始化 DbRestoreApp 失败: %v", err))
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
dbBackupApp, err = newDbBackupApp(repositories, dbApp, scheduler)
|
if err := GetDbBinlogApp().Init(); err != nil {
|
||||||
if err != nil {
|
panic(fmt.Sprintf("初始化 DbBinlogApp 失败: %v", err))
|
||||||
panic(fmt.Sprintf("初始化 dbBackupApp 失败: %v", err))
|
|
||||||
}
|
}
|
||||||
dbRestoreApp, err = newDbRestoreApp(repositories, dbApp, scheduler)
|
GetDataSyncTaskApp().InitCronJob()
|
||||||
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()
|
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInstanceApp() Instance {
|
|
||||||
return instanceApp
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDbApp() Db {
|
|
||||||
return dbApp
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDbSqlApp() DbSql {
|
|
||||||
return dbSqlApp
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDbSqlExecApp() DbSqlExec {
|
|
||||||
return dbSqlExecApp
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDbBackupApp() *DbBackupApp {
|
func GetDbBackupApp() *DbBackupApp {
|
||||||
return dbBackupApp
|
return ioc.Get[*DbBackupApp]("DbBackupApp")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDbRestoreApp() *DbRestoreApp {
|
func GetDbRestoreApp() *DbRestoreApp {
|
||||||
return dbRestoreApp
|
return ioc.Get[*DbRestoreApp]("DbRestoreApp")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDbBinlogApp() *DbBinlogApp {
|
func GetDbBinlogApp() *DbBinlogApp {
|
||||||
return dbBinlogApp
|
return ioc.Get[*DbBinlogApp]("DbBinlogApp")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDataSyncTaskApp() DataSyncTask {
|
func GetDataSyncTaskApp() DataSyncTask {
|
||||||
return dataSyncApp
|
return ioc.Get[DataSyncTask]("DbDataSyncTaskApp")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,22 +40,17 @@ type Db interface {
|
|||||||
GetDbConnByInstanceId(instanceId uint64) (*dbi.DbConn, error)
|
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 {
|
type dbAppImpl struct {
|
||||||
base.AppImpl[*entity.Db, repository.Db]
|
base.AppImpl[*entity.Db, repository.Db]
|
||||||
|
|
||||||
dbSqlRepo repository.DbSql
|
dbSqlRepo repository.DbSql `inject:"DbSqlRepo"`
|
||||||
dbInstanceApp Instance
|
dbInstanceApp Instance `inject:"DbInstanceApp"`
|
||||||
tagApp tagapp.TagTree
|
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)
|
_, delDb, _ := collx.ArrayCompare(newDbs, oldDbs)
|
||||||
|
|
||||||
for _, v := range delDb {
|
// 先简单关闭可能存在的旧库连接(可能改了关联标签导致DbConn.Info.TagPath与修改后的标签不一致、导致操作权限校验出错)
|
||||||
|
for _, v := range oldDbs {
|
||||||
// 关闭数据库连接
|
// 关闭数据库连接
|
||||||
dbm.CloseDb(dbEntity.Id, v)
|
dbm.CloseDb(dbEntity.Id, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range delDb {
|
||||||
// 删除该库关联的所有sql记录
|
// 删除该库关联的所有sql记录
|
||||||
d.dbSqlRepo.DeleteByCond(ctx, &entity.DbSql{DbId: dbId, Db: v})
|
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
|
checkDb := dbName
|
||||||
// 兼容pgsql/dm db/schema模式
|
// 兼容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, "/")
|
ss := strings.Split(dbName, "/")
|
||||||
if len(ss) > 1 {
|
if len(ss) > 1 {
|
||||||
checkDb = ss[0]
|
checkDb = ss[0]
|
||||||
|
|||||||
@@ -3,69 +3,217 @@ package application
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"github.com/google/uuid"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"math"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
|
"mayfly-go/pkg/logx"
|
||||||
"mayfly-go/pkg/model"
|
"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) {
|
const maxBackupHistoryDays = 30
|
||||||
var jobs []*entity.DbBackup
|
|
||||||
if err := repositories.Backup.ListToDo(&jobs); err != nil {
|
var (
|
||||||
return nil, err
|
errRestoringBackupHistory = errors.New("正在从备份历史中恢复数据库")
|
||||||
}
|
)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type DbBackupApp struct {
|
type DbBackupApp struct {
|
||||||
backupRepo repository.DbBackup
|
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||||
instanceRepo repository.Instance
|
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||||
backupHistoryRepo repository.DbBackupHistory
|
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
||||||
dbApp Db
|
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||||
scheduler *dbScheduler
|
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() {
|
func (app *DbBackupApp) Close() {
|
||||||
app.scheduler.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 {
|
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 {
|
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 {
|
func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
|
||||||
// todo: 删除数据库备份历史文件
|
app.mutex.Lock()
|
||||||
return app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId)
|
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 {
|
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 {
|
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 {
|
func (app *DbBackupApp) StartNow(ctx context.Context, jobId uint64) error {
|
||||||
return app.scheduler.StartJobNow(ctx, entity.DbJobTypeBackup, jobId)
|
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 分页获取数据库备份任务
|
// 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...)
|
return app.backupRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +224,11 @@ func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []str
|
|||||||
|
|
||||||
// GetHistoryPageList 分页获取数据库备份历史
|
// GetHistoryPageList 分页获取数据库备份历史
|
||||||
func (app *DbBackupApp) GetHistoryPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
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) {
|
func NewIncUUID() (uuid.UUID, error) {
|
||||||
@@ -99,3 +251,35 @@ func NewIncUUID() (uuid.UUID, error) {
|
|||||||
|
|
||||||
return uid, nil
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"math"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/pkg/logx"
|
"mayfly-go/pkg/logx"
|
||||||
@@ -11,64 +12,132 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DbBinlogApp struct {
|
type DbBinlogApp struct {
|
||||||
binlogRepo repository.DbBinlog
|
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||||
binlogHistoryRepo repository.DbBinlogHistory
|
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
||||||
backupRepo repository.DbBackup
|
binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
|
||||||
backupHistoryRepo repository.DbBackupHistory
|
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||||
dbApp Db
|
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
||||||
context context.Context
|
instanceRepo repository.Instance `inject:"DbInstanceRepo"`
|
||||||
cancel context.CancelFunc
|
dbApp Db `inject:"DbApp"`
|
||||||
waitGroup sync.WaitGroup
|
|
||||||
scheduler *dbScheduler
|
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())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
svc := &DbBinlogApp{
|
svc := &DbBinlogApp{
|
||||||
binlogRepo: repositories.Binlog,
|
context: ctx,
|
||||||
binlogHistoryRepo: repositories.BinlogHistory,
|
cancel: cancel,
|
||||||
backupRepo: repositories.Backup,
|
|
||||||
backupHistoryRepo: repositories.BackupHistory,
|
|
||||||
dbApp: dbApp,
|
|
||||||
scheduler: scheduler,
|
|
||||||
context: ctx,
|
|
||||||
cancel: cancel,
|
|
||||||
}
|
}
|
||||||
svc.waitGroup.Add(1)
|
return svc
|
||||||
go svc.run()
|
}
|
||||||
return svc, nil
|
|
||||||
|
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() {
|
func (app *DbBinlogApp) run() {
|
||||||
defer app.waitGroup.Done()
|
defer app.waitGroup.Done()
|
||||||
|
|
||||||
// todo: 实现 binlog 并发下载
|
for app.context.Err() == nil {
|
||||||
timex.SleepWithContext(app.context, time.Minute)
|
if err := app.fetchBinlog(app.context); err != nil {
|
||||||
for !app.closed() {
|
|
||||||
jobs, err := app.loadJobs()
|
|
||||||
if err != nil {
|
|
||||||
logx.Errorf("DbBinlogApp: 加载 BINLOG 同步任务失败: %s", err.Error())
|
|
||||||
timex.SleepWithContext(app.context, time.Minute)
|
timex.SleepWithContext(app.context, time.Minute)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if app.closed() {
|
if err := app.pruneBinlog(app.context); err != nil {
|
||||||
break
|
timex.SleepWithContext(app.context, time.Minute)
|
||||||
}
|
continue
|
||||||
if err := app.scheduler.AddJob(app.context, false, entity.DbJobTypeBinlog, jobs); err != nil {
|
|
||||||
logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
|
|
||||||
}
|
}
|
||||||
timex.SleepWithContext(app.context, entity.BinlogDownloadInterval)
|
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
|
var instanceIds []uint64
|
||||||
if err := app.backupRepo.ListDbInstances(true, true, &instanceIds); err != nil {
|
if err := app.backupRepo.ListDbInstances(true, true, &instanceIds); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
jobs := make([]*entity.DbBinlog, 0, len(instanceIds))
|
jobs := make([]*entity.DbBinlog, 0, len(instanceIds))
|
||||||
for _, id := range instanceIds {
|
for _, id := range instanceIds {
|
||||||
if app.closed() {
|
if ctx.Err() != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
binlog := entity.NewDbBinlog(id)
|
binlog := entity.NewDbBinlog(id)
|
||||||
@@ -81,14 +150,15 @@ func (app *DbBinlogApp) loadJobs() ([]*entity.DbBinlog, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBinlogApp) Close() {
|
func (app *DbBinlogApp) Close() {
|
||||||
app.cancel()
|
cancel := app.cancel
|
||||||
|
if cancel == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.cancel = nil
|
||||||
|
cancel()
|
||||||
app.waitGroup.Wait()
|
app.waitGroup.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBinlogApp) closed() bool {
|
|
||||||
return app.context.Err() != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *DbBinlogApp) AddJobIfNotExists(ctx context.Context, job *entity.DbBinlog) error {
|
func (app *DbBinlogApp) AddJobIfNotExists(ctx context.Context, job *entity.DbBinlog) error {
|
||||||
if err := app.binlogRepo.AddJobIfNotExists(ctx, job); err != nil {
|
if err := app.binlogRepo.AddJobIfNotExists(ctx, job); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -98,11 +168,3 @@ func (app *DbBinlogApp) AddJobIfNotExists(ctx context.Context, job *entity.DbBin
|
|||||||
}
|
}
|
||||||
return nil
|
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/logx"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
"mayfly-go/pkg/scheduler"
|
"mayfly-go/pkg/scheduler"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DataSyncTask interface {
|
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)
|
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 {
|
type dataSyncAppImpl struct {
|
||||||
base.AppImpl[*entity.DataSyncTask, repository.DataSyncTask]
|
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) {
|
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 {
|
func (app *dataSyncAppImpl) Save(ctx context.Context, taskEntity *entity.DataSyncTask) error {
|
||||||
var err error
|
var err error
|
||||||
if taskEntity.Id == 0 {
|
if taskEntity.Id == 0 {
|
||||||
|
// 新建时生成key
|
||||||
|
taskEntity.TaskKey = uuid.New().String()
|
||||||
err = app.Insert(ctx, taskEntity)
|
err = app.Insert(ctx, taskEntity)
|
||||||
} else {
|
} else {
|
||||||
err = app.UpdateById(ctx, taskEntity)
|
err = app.UpdateById(ctx, taskEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
app.AddCronJob(taskEntity)
|
task, err := app.GetById(new(entity.DataSyncTask), taskEntity.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.AddCronJob(task)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +100,10 @@ func (app *dataSyncAppImpl) AddCronJob(taskEntity *entity.DataSyncTask) {
|
|||||||
|
|
||||||
// 根据状态添加新的任务
|
// 根据状态添加新的任务
|
||||||
if taskEntity.Status == entity.DataSyncTaskStatusEnable {
|
if taskEntity.Status == entity.DataSyncTaskStatusEnable {
|
||||||
|
taskId := taskEntity.Id
|
||||||
scheduler.AddFunByKey(key, taskEntity.TaskCron, func() {
|
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())
|
logx.Errorf("定时执行数据同步任务失败: %s", err.Error())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -126,7 +143,23 @@ func (app *dataSyncAppImpl) RunCronJob(id uint64) error {
|
|||||||
updSql := ""
|
updSql := ""
|
||||||
orderSql := ""
|
orderSql := ""
|
||||||
if task.UpdFieldVal != "0" && task.UpdFieldVal != "" && task.UpdField != "" {
|
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 "
|
orderSql = "order by " + task.UpdField + " asc "
|
||||||
}
|
}
|
||||||
// 组装查询sql
|
// 组装查询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 {
|
if err != nil {
|
||||||
return syncLog, errorx.NewBiz("连接源数据库失败: %s", err.Error())
|
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 {
|
if err != nil {
|
||||||
return syncLog, errorx.NewBiz("连接目标数据库失败: %s", err.Error())
|
return syncLog, errorx.NewBiz("连接目标数据库失败: %s", err.Error())
|
||||||
}
|
}
|
||||||
@@ -197,8 +230,8 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
|||||||
// 遍历columns 取task.UpdField的字段类型
|
// 遍历columns 取task.UpdField的字段类型
|
||||||
updFieldType = dbi.DataTypeString
|
updFieldType = dbi.DataTypeString
|
||||||
for _, column := range columns {
|
for _, column := range columns {
|
||||||
if column.Name == task.UpdField {
|
if strings.EqualFold(strings.ToLower(column.Name), strings.ToLower(task.UpdField)) {
|
||||||
updFieldType = srcDialect.GetDataType(column.Type)
|
updFieldType = srcDialect.GetDataConverter().GetDataType(column.Type)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,7 +240,7 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
|||||||
total++
|
total++
|
||||||
result = append(result, row)
|
result = append(result, row)
|
||||||
if total%batchSize == 0 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +262,7 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
|||||||
|
|
||||||
// 处理剩余的数据
|
// 处理剩余的数据
|
||||||
if len(result) > 0 {
|
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()
|
targetDbTx.Rollback()
|
||||||
return syncLog, err
|
return syncLog, err
|
||||||
}
|
}
|
||||||
@@ -249,10 +282,16 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
|
|||||||
return syncLog, nil
|
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 {
|
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 {
|
||||||
var data = make([]map[string]any, 0)
|
|
||||||
|
|
||||||
// 遍历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 {
|
for _, record := range srcRes {
|
||||||
var rowData = make(map[string]any)
|
var rowData = make(map[string]any)
|
||||||
// 遍历字段映射, target字段的值为src字段取值
|
// 遍历字段映射, target字段的值为src字段取值
|
||||||
@@ -265,18 +304,23 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
|||||||
|
|
||||||
data = append(data, rowData)
|
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])
|
task.UpdFieldVal = srcDialect.GetDataConverter().FormatData(updFieldVal, updFieldType)
|
||||||
updFieldVal = srcDialect.FormatStrData(updFieldVal, updFieldType)
|
|
||||||
task.UpdFieldVal = updFieldVal
|
|
||||||
|
|
||||||
// 获取目标库字段数组
|
// 获取目标库字段数组
|
||||||
targetWrapColumns := make([]string, 0)
|
targetWrapColumns := make([]string, 0)
|
||||||
// 获取源库字段数组
|
// 获取源库字段数组
|
||||||
srcColumns := make([]string, 0)
|
srcColumns := make([]string, 0)
|
||||||
|
srcFieldTypes := make(map[string]dbi.DataType)
|
||||||
for _, item := range fieldMap {
|
for _, item := range fieldMap {
|
||||||
targetField := item["target"]
|
targetField := item["target"]
|
||||||
srcField := item["target"]
|
srcField := item["target"]
|
||||||
|
srcFieldTypes[srcField] = srcDialect.GetDataConverter().GetDataType(srcColumnTypes[item["src"]])
|
||||||
targetWrapColumns = append(targetWrapColumns, targetDbConn.Info.Type.QuoteIdentifier(targetField))
|
targetWrapColumns = append(targetWrapColumns, targetDbConn.Info.Type.QuoteIdentifier(targetField))
|
||||||
srcColumns = append(srcColumns, srcField)
|
srcColumns = append(srcColumns, srcField)
|
||||||
}
|
}
|
||||||
@@ -286,7 +330,9 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
|||||||
for _, record := range data {
|
for _, record := range data {
|
||||||
rawValue := make([]any, 0)
|
rawValue := make([]any, 0)
|
||||||
for _, column := range srcColumns {
|
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)
|
values = append(values, rawValue)
|
||||||
}
|
}
|
||||||
@@ -328,7 +374,7 @@ func (app *dataSyncAppImpl) endRunning(taskEntity *entity.DataSyncTask, log *ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *dataSyncAppImpl) saveLog(log *entity.DataSyncLog) {
|
func (app *dataSyncAppImpl) saveLog(log *entity.DataSyncLog) {
|
||||||
app.dataSyncLogRepo.Save(context.Background(), log)
|
app.dbDataSyncLogRepo.Save(context.Background(), log)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *dataSyncAppImpl) InitCronJob() {
|
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) {
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
"mayfly-go/internal/db/dbm"
|
"mayfly-go/internal/db/dbm"
|
||||||
"mayfly-go/internal/db/dbm/dbi"
|
"mayfly-go/internal/db/dbm/dbi"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/pkg/base"
|
"mayfly-go/pkg/base"
|
||||||
|
"mayfly-go/pkg/biz"
|
||||||
"mayfly-go/pkg/errorx"
|
"mayfly-go/pkg/errorx"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
)
|
)
|
||||||
@@ -30,14 +33,17 @@ type Instance interface {
|
|||||||
GetDatabases(entity *entity.DbInstance) ([]string, error)
|
GetDatabases(entity *entity.DbInstance) ([]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newInstanceApp(instanceRepo repository.Instance) Instance {
|
|
||||||
app := new(instanceAppImpl)
|
|
||||||
app.Repo = instanceRepo
|
|
||||||
return app
|
|
||||||
}
|
|
||||||
|
|
||||||
type instanceAppImpl struct {
|
type instanceAppImpl struct {
|
||||||
base.AppImpl[*entity.DbInstance, repository.Instance]
|
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 分页获取数据库实例
|
// GetPageList 分页获取数据库实例
|
||||||
@@ -73,9 +79,11 @@ func (app *instanceAppImpl) Save(ctx context.Context, instanceEntity *entity.DbI
|
|||||||
|
|
||||||
err := app.GetBy(oldInstance)
|
err := app.GetBy(oldInstance)
|
||||||
if instanceEntity.Id == 0 {
|
if instanceEntity.Id == 0 {
|
||||||
if instanceEntity.Password == "" {
|
|
||||||
|
if instanceEntity.Type != string(dbi.DbTypeSqlite) && instanceEntity.Password == "" {
|
||||||
return errorx.NewBiz("密码不能为空")
|
return errorx.NewBiz("密码不能为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return errorx.NewBiz("该数据库实例已存在")
|
return errorx.NewBiz("该数据库实例已存在")
|
||||||
}
|
}
|
||||||
@@ -95,8 +103,50 @@ func (app *instanceAppImpl) Save(ctx context.Context, instanceEntity *entity.DbI
|
|||||||
return app.UpdateById(ctx, instanceEntity)
|
return app.UpdateById(ctx, instanceEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *instanceAppImpl) Delete(ctx context.Context, id uint64) error {
|
func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error {
|
||||||
return app.DeleteById(ctx, id)
|
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) {
|
func (app *instanceAppImpl) GetDatabases(ed *entity.DbInstance) ([]string, error) {
|
||||||
@@ -2,71 +2,130 @@ package application
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
|
"mayfly-go/pkg/logx"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDbRestoreApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbRestoreApp, error) {
|
type DbRestoreApp struct {
|
||||||
var jobs []*entity.DbRestore
|
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||||
if err := repositories.Restore.ListToDo(&jobs); err != nil {
|
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||||
return nil, err
|
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
|
||||||
}
|
mutex sync.Mutex
|
||||||
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 {
|
func (app *DbRestoreApp) Init() error {
|
||||||
restoreRepo repository.DbRestore
|
var jobs []*entity.DbRestore
|
||||||
instanceRepo repository.Instance
|
if err := app.restoreRepo.ListToDo(&jobs); err != nil {
|
||||||
backupHistoryRepo repository.DbBackupHistory
|
return err
|
||||||
restoreHistoryRepo repository.DbRestoreHistory
|
}
|
||||||
binlogHistoryRepo repository.DbBinlogHistory
|
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
|
||||||
dbApp Db
|
return err
|
||||||
scheduler *dbScheduler
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbRestoreApp) Close() {
|
func (app *DbRestoreApp) Close() {
|
||||||
app.scheduler.Close()
|
app.scheduler.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbRestoreApp) Create(ctx context.Context, job *entity.DbRestore) error {
|
func (app *DbRestoreApp) Create(ctx context.Context, jobs any) error {
|
||||||
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeRestore, job)
|
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 {
|
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 {
|
func (app *DbRestoreApp) Delete(ctx context.Context, jobId uint64) error {
|
||||||
// todo: 删除数据库恢复历史文件
|
app.mutex.Lock()
|
||||||
return app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId)
|
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 {
|
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 {
|
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 分页获取数据库恢复任务
|
// 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...)
|
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 获取未配置定时恢复的数据库名称
|
// GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
|
||||||
func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
|
func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
|
||||||
return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames)
|
return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames)
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
"gorm.io/gorm"
|
||||||
"mayfly-go/internal/db/dbm/dbi"
|
"mayfly-go/internal/db/dbm/dbi"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/pkg/logx"
|
|
||||||
"mayfly-go/pkg/runner"
|
"mayfly-go/pkg/runner"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -21,58 +23,35 @@ const (
|
|||||||
type dbScheduler struct {
|
type dbScheduler struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
runner *runner.Runner[entity.DbJob]
|
runner *runner.Runner[entity.DbJob]
|
||||||
dbApp Db
|
dbApp Db `inject:"DbApp"`
|
||||||
backupRepo repository.DbBackup
|
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||||
backupHistoryRepo repository.DbBackupHistory
|
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
||||||
restoreRepo repository.DbRestore
|
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||||
restoreHistoryRepo repository.DbRestoreHistory
|
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
|
||||||
binlogRepo repository.DbBinlog
|
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
||||||
binlogHistoryRepo repository.DbBinlogHistory
|
binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
|
||||||
binlogTimes map[uint64]time.Time
|
sfGroup singleflight.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDbScheduler(repositories *repository.Repositories) (*dbScheduler, error) {
|
func newDbScheduler() *dbScheduler {
|
||||||
scheduler := &dbScheduler{
|
scheduler := &dbScheduler{}
|
||||||
dbApp: dbApp,
|
|
||||||
backupRepo: repositories.Backup,
|
|
||||||
backupHistoryRepo: repositories.BackupHistory,
|
|
||||||
restoreRepo: repositories.Restore,
|
|
||||||
restoreHistoryRepo: repositories.RestoreHistory,
|
|
||||||
binlogRepo: repositories.Binlog,
|
|
||||||
binlogHistoryRepo: repositories.BinlogHistory,
|
|
||||||
}
|
|
||||||
scheduler.runner = runner.NewRunner[entity.DbJob](maxRunning, scheduler.runJob,
|
scheduler.runner = runner.NewRunner[entity.DbJob](maxRunning, scheduler.runJob,
|
||||||
runner.WithScheduleJob[entity.DbJob](scheduler.scheduleJob),
|
runner.WithScheduleJob[entity.DbJob](scheduler.scheduleJob),
|
||||||
runner.WithRunnableJob[entity.DbJob](scheduler.runnableJob),
|
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) {
|
func (s *dbScheduler) scheduleJob(job entity.DbJob) (time.Time, error) {
|
||||||
return job.Schedule()
|
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 {
|
func (s *dbScheduler) UpdateJob(ctx context.Context, job entity.DbJob) error {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
if err := s.repo(job.GetJobType()).UpdateById(ctx, job); err != nil {
|
_ = s.runner.Update(ctx, job)
|
||||||
return err
|
|
||||||
}
|
|
||||||
_ = s.runner.UpdateOrAdd(ctx, job)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,61 +59,39 @@ func (s *dbScheduler) Close() {
|
|||||||
s.runner.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()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
if saving {
|
|
||||||
if err := s.repo(jobType).AddJob(ctx, jobs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reflectValue := reflect.ValueOf(jobs)
|
reflectValue := reflect.ValueOf(jobs)
|
||||||
switch reflectValue.Kind() {
|
switch reflectValue.Kind() {
|
||||||
case reflect.Array, reflect.Slice:
|
case reflect.Array, reflect.Slice:
|
||||||
reflectLen := reflectValue.Len()
|
reflectLen := reflectValue.Len()
|
||||||
for i := 0; i < reflectLen; i++ {
|
for i := 0; i < reflectLen; i++ {
|
||||||
job := reflectValue.Index(i).Interface().(entity.DbJob)
|
job := reflectValue.Index(i).Interface().(entity.DbJob)
|
||||||
job.SetJobType(jobType)
|
|
||||||
_ = s.runner.Add(ctx, job)
|
_ = s.runner.Add(ctx, job)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
job := jobs.(entity.DbJob)
|
job := jobs.(entity.DbJob)
|
||||||
job.SetJobType(jobType)
|
|
||||||
_ = s.runner.Add(ctx, job)
|
_ = s.runner.Add(ctx, job)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
||||||
// todo: 删除数据库备份历史文件
|
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
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
|
return err
|
||||||
}
|
}
|
||||||
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
|
|
||||||
return nil
|
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()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
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)
|
_ = s.runner.Add(ctx, job)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -143,61 +100,37 @@ func (s *dbScheduler) DisableJob(ctx context.Context, jobType entity.DbJobType,
|
|||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
repo := s.repo(jobType)
|
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
|
||||||
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())
|
|
||||||
return nil
|
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()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
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)
|
_ = s.runner.StartNow(ctx, job)
|
||||||
return nil
|
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()
|
id, err := NewIncUUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
backup := job.(*entity.DbBackup)
|
|
||||||
history := &entity.DbBackupHistory{
|
history := &entity.DbBackupHistory{
|
||||||
Uuid: id.String(),
|
Uuid: id.String(),
|
||||||
DbBackupId: backup.Id,
|
DbBackupId: backup.Id,
|
||||||
DbInstanceId: backup.DbInstanceId,
|
DbInstanceId: backup.DbInstanceId,
|
||||||
DbName: backup.DbName,
|
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)
|
binlogInfo, err := dbProgram.Backup(ctx, history)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
name := backup.Name
|
name := backup.DbName
|
||||||
if len(name) == 0 {
|
if len(backup.Name) > 0 {
|
||||||
name = backup.DbName
|
name = fmt.Sprintf("%s-%s", backup.DbName, backup.Name)
|
||||||
}
|
}
|
||||||
history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime))
|
history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime))
|
||||||
history.CreateTime = now
|
history.CreateTime = now
|
||||||
@@ -211,43 +144,43 @@ func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error {
|
func (s *dbScheduler) singleFlightFetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, targetTime time.Time) error {
|
||||||
restore := job.(*entity.DbRestore)
|
key := strconv.FormatUint(instanceId, 10)
|
||||||
conn, err := s.dbApp.GetDbConnByInstanceId(restore.DbInstanceId)
|
for ctx.Err() == nil {
|
||||||
if err != nil {
|
c := s.sfGroup.DoChan(key, func() (interface{}, error) {
|
||||||
return err
|
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 {
|
if restore.PointInTime.Valid {
|
||||||
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
if err := s.fetchBinlog(ctx, dbProgram, restore.DbInstanceId, true, restore.PointInTime.Time); err != nil {
|
||||||
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 {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
|
if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,76 +195,74 @@ func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) {
|
func (s *dbScheduler) updateJob(ctx context.Context, job entity.DbJob) error {
|
||||||
job.SetLastStatus(entity.DbJobRunning, nil)
|
switch t := job.(type) {
|
||||||
if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
|
case *entity.DbBackup:
|
||||||
logx.Errorf("failed to update job status: %v", err)
|
return s.backupRepo.UpdateById(ctx, t)
|
||||||
return
|
case *entity.DbRestore:
|
||||||
}
|
return s.restoreRepo.UpdateById(ctx, t)
|
||||||
|
case *entity.DbBinlog:
|
||||||
var errRun error
|
return s.binlogRepo.UpdateById(ctx, t)
|
||||||
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)
|
|
||||||
default:
|
default:
|
||||||
errRun = errors.New(fmt.Sprintf("无效的数据库任务类型: %v", typ))
|
return fmt.Errorf("无效的数据库任务类型: %T", t)
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 maxCountByInstanceId = 4
|
||||||
const maxCountByDbName = 1
|
const maxCountByDbName = 1
|
||||||
var countByInstanceId, countByDbName int
|
var countByInstanceId, countByDbName int
|
||||||
jobBase := job.GetJobBase()
|
for item, ok := nextRunning(); ok; item, ok = nextRunning() {
|
||||||
for item, ok := next(); ok; item, ok = next() {
|
if job.GetInstanceId() == item.GetInstanceId() {
|
||||||
itemBase := item.GetJobBase()
|
|
||||||
if jobBase.DbInstanceId == itemBase.DbInstanceId {
|
|
||||||
countByInstanceId++
|
countByInstanceId++
|
||||||
if countByInstanceId >= maxCountByInstanceId {
|
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() {
|
if job.GetDbName() == item.GetDbName() {
|
||||||
countByDbName++
|
countByDbName++
|
||||||
if countByDbName >= maxCountByDbName {
|
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 {
|
func (s *dbScheduler) restorePointInTime(ctx context.Context, dbProgram dbi.DbProgram, job *entity.DbRestore) error {
|
||||||
return typ == entity.DbJobTypeRestore || typ == entity.DbJobTypeBinlog
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProgram, job *entity.DbRestore) error {
|
|
||||||
binlogHistory, err := s.binlogHistoryRepo.GetHistoryByTime(job.DbInstanceId, job.PointInTime.Time)
|
binlogHistory, err := s.binlogHistoryRepo.GetHistoryByTime(job.DbInstanceId, job.PointInTime.Time)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -340,7 +271,7 @@ func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProg
|
|||||||
Sequence: binlogHistory.Sequence,
|
Sequence: binlogHistory.Sequence,
|
||||||
Position: position,
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -360,31 +291,77 @@ func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProg
|
|||||||
TargetPosition: target.Position,
|
TargetPosition: target.Position,
|
||||||
TargetTime: job.PointInTime.Time,
|
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 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 {
|
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, backupHistory *entity.DbBackupHistory) (retErr error) {
|
||||||
backupHistory := &entity.DbBackupHistory{}
|
if _, err := s.backupHistoryRepo.UpdateRestoring(false, backupHistory.Id); err != nil {
|
||||||
if err := s.backupHistoryRepo.GetById(backupHistory, job.DbBackupHistoryId); err != nil {
|
|
||||||
return err
|
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)
|
return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob) error {
|
func (s *dbScheduler) fetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, downloadLatestBinlogFile bool, targetTime time.Time) error {
|
||||||
instanceId := backup.GetJobBase().DbInstanceId
|
if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
|
||||||
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
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)
|
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(instanceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if ok {
|
if downloadLatestBinlogFile && targetTime.Before(binlogHistory.LastEventTime) {
|
||||||
latestBinlogSequence = binlogHistory.Sequence
|
return nil
|
||||||
} else {
|
}
|
||||||
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(instanceId)
|
|
||||||
|
if !ok {
|
||||||
|
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistoryForBinlog(instanceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -393,14 +370,11 @@ func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob)
|
|||||||
}
|
}
|
||||||
earliestBackupSequence = backupHistory.BinlogSequence
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dbProgram := conn.GetDialect().GetDbProgram()
|
return s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, instanceId, binlogFiles)
|
||||||
binlogFiles, err := dbProgram.FetchBinlogs(ctx, false, earliestBackupSequence, latestBinlogSequence)
|
|
||||||
if err == nil {
|
|
||||||
err = s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, instanceId, binlogFiles)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ type dbSqlAppImpl struct {
|
|||||||
base.AppImpl[*entity.DbSql, repository.DbSql]
|
base.AppImpl[*entity.DbSql, repository.DbSql]
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDbSqlApp(dbSqlRepo repository.DbSql) DbSql {
|
// 注入DbSqlRepo
|
||||||
app := new(dbSqlAppImpl)
|
func (d *dbSqlAppImpl) InjectDbSqlRepo(repo repository.DbSql) {
|
||||||
app.Repo = dbSqlRepo
|
d.Repo = repo
|
||||||
return app
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user