33 Commits

Author SHA1 Message Date
meilin.huang
d711a36749 feat: v1.7.3 2024-02-08 09:53:48 +08:00
meilin.huang
9dbf104ef1 refactor: 机器操作界面调整 2024-02-07 21:14:29 +08:00
zongyangleo
20eb06fb28 !101 feat: 新增机器操作菜单
* feat: 新增机器操作菜单
2024-02-07 06:37:59 +00:00
meilin.huang
9c20bdef39 Merge branch 'dev' of https://gitee.com/objs/mayfly-go into dev 2024-02-06 15:33:31 +08:00
zongyangleo
3fdd98a390 !99 feat: DBMS新增kingbaseES、vastbase,还有一些优化
* refactor: 重构机器列表展示
* fix:修复编辑表问题
* refactor: 优化下拉实例显示
* feat: DBMS新增kingbaseES(已测试postgres、oracle兼容模式) 、vastbase
2024-02-06 07:32:03 +00:00
meilin.huang
d4f456c0cf Merge branch 'dev' of https://gitee.com/objs/mayfly-go into dev 2024-02-06 15:17:39 +08:00
kanzihuang
f2b6e15cf4 !100 定时清理数据库备份数据
* feat: 优化数据库 BINLOG 同步机制
* feat: 删除数据库实例前需删除关联的数据库备份与恢复任务
* refactor: 重构数据库备份与恢复模块
* feat: 定时清理数据库备份历史和本地 Binlog 文件
* feat: 压缩数据库备份文件
2024-02-06 07:16:56 +00:00
meilin.huang
6be0ea6aed fix: dbms数据行编辑 2024-02-01 12:05:41 +08:00
meilin.huang
eee08be2cc feat: 数据库支持编辑行数据 2024-01-31 20:41:41 +08:00
meilin.huang
252fc553f2 feat: v1.7.2 2024-01-31 12:53:27 +08:00
meilin.huang
ac2ceed3f9 refactor: code review 2024-01-30 21:56:49 +08:00
kanzihuang
3f828cc5b0 !96 删除数据库备份和恢复历史
* feat: 删除数据库备份历史
* refactor dbScheduler
* feat: 从数据库备份历史中恢复数据库
* feat: 删除数据库恢复历史记录
* refactor dbScheuler
2024-01-30 13:12:43 +00:00
zongyangleo
fc1b9ef35d !97 一些优化
* refactor: 重构表格分页组件,适配大数据量分页
* fix:定时任务修复
* feat: gaussdb单独提出来
2024-01-30 13:09:26 +00:00
meilin.huang
d0b71a1c40 refactor: dialect使用方式调整 2024-01-29 16:02:28 +08:00
meilin.huang
a743a6a05a Merge branch 'master' into dev 2024-01-29 12:21:22 +08:00
zongyangleo
0e6b9713ce !93 feat: DBMS支持mssql和一些功能优化
* feat: 表格+表格元数据缓存
* feat:跳板机支持多段跳
* fix: 所有数据库区分字段主键和自增
* feat: DBMS支持mssql
* refactor: 去除无用的getter方法
2024-01-29 04:20:23 +00:00
meilin.huang
b9afbc764d refactor: 去除无用的getter方法 2024-01-29 11:34:48 +08:00
meilin.huang
923e183a67 refactor: code review 2024-01-26 17:17:26 +08:00
meilin.huang
7e9a381641 refactor: 数据库meta使用注册方式,方便可插拔 2024-01-24 17:01:17 +08:00
zongyangleo
bed95254d0 !91 fix: oracle数据同步 bug
* fix: oracle数据同步 bug
2024-01-24 08:29:16 +00:00
meilin.huang
e4d13f3377 refactor: 引入日志切割库、indexApi拆分等 2024-01-23 19:30:28 +08:00
Coder慌
d530365ef9 !90 fix: 依赖注入支持私有变量
Merge pull request !90 from kanzihuang/feat-db-bak
2024-01-23 09:02:37 +00:00
wanli
070d4ea104 fix: 依赖注入支持私有变量 2024-01-23 16:29:41 +08:00
zongyangleo
3fc86f0fae !88 feat: dbms表支持右键菜单:删除表、编辑表、新建表、复制表
* feat: 支持复制表
* feat: dbms表支持右键菜单:删除表、编辑表、新建表
2024-01-23 04:08:02 +00:00
kanzihuang
3b77ab2727 !89 feat: 给数据库备份和恢复配置操作权限
* feat: 给数据库备份和恢复配置操作权限
* refactor: 数据库备份与恢复采用最新依赖注入机制
2024-01-23 04:06:08 +00:00
meilin.huang
76cb991282 fix: 数据同步更新时间展示等问题 2024-01-23 09:27:05 +08:00
meilin.huang
9efd20f1b9 refactor: ioc与系统初始化处理方式调整 2024-01-22 11:35:28 +08:00
kanzihuang
de5b9e46d3 !87 fix: 修复数据库备份与恢复问题
* feat: 修复数据库备份与恢复问题
* feat: 启用 BINLOG 支持全量备份和增量备份,未启用 BINLOG 仅支持全量备份
* feat: 数据库恢复后自动备份,避免数据丢失
2024-01-22 03:12:16 +00:00
meilin.huang
f27d3d200f feat: 新增简易版ioc 2024-01-21 22:52:20 +08:00
meilin.huang
f4a64b96a9 feat: v1.7.1新增支持sqlite&oracle分页限制等问题修复 2024-01-19 21:33:37 +08:00
zongyangleo
9a59749763 !86 dbms支持sqlite和一些bug修复
* fix: 达梦数据库连接修复,以支持带特殊字符的密码和schema
* fix: oracle bug修复
* feat: dbms支持sqlite
* fix: dbms 修改字段名bug
2024-01-19 08:59:35 +00:00
kanzihuang
b017b902f8 !85 fix: 修复 BINLOG同步任务加载问题
* Merge branch 'dev' of gitee.com:dromara/mayfly-go into feat-db-bak
* fix: 修复 BINLOG 同步任务加载问题
2024-01-19 00:40:44 +00:00
meilin.huang
7c53353c60 fix: sqlite数据问题时间类型问题修复等 2024-01-18 17:18:17 +08:00
272 changed files with 8313 additions and 2679 deletions

View File

@@ -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

View File

@@ -22,7 +22,7 @@
### 介绍 ### 介绍
web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle 达梦 高斯、redis(单机 哨兵 集群)、mongo 统一管理操作平台** web 版 **linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库mysql postgres oracle 达梦 高斯 sqlite、redis(单机 哨兵 集群)、mongo 统一管理操作平台**
### 开发语言与主要框架 ### 开发语言与主要框架

View File

@@ -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.

View File

@@ -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

View File

@@ -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
} }
] ]
} }

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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">

View File

@@ -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, {

View File

@@ -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();
});
}); });
}; };

View File

@@ -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'),
};

View File

@@ -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) {

View File

@@ -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';

View File

@@ -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 右击菜单按钮选项

View File

@@ -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();
} }
}; };

View 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>

View File

@@ -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>

View File

@@ -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);
}; };

View File

@@ -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(() => {

View File

@@ -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 });

View File

@@ -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;
} }

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);
};
/** /**
* 获取当前操作的数据库信息 * 获取当前操作的数据库信息
*/ */

View File

@@ -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) {

View File

@@ -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'),

View File

@@ -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>

View File

@@ -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, 否则格式化编辑器所有内容

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)));
}); });
} }
} }

View File

@@ -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,

View File

@@ -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);

View File

@@ -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[] = [];

View 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;
}
}

View File

@@ -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());
})();

View 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;
}
}

View File

@@ -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;
} }

View 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}'`;
}
}

View File

@@ -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`

View File

@@ -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[] = [];

View File

@@ -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}'`;
} }
} }

View 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}'`;
}
}

View 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;
}
}

View File

@@ -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,

View File

@@ -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);

View 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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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';

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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
}

View File

@@ -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()
} }
} }

View File

@@ -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:""`
} }
/** 用户账号密码登录 **/ /** 用户账号密码登录 **/

View File

@@ -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]

View File

@@ -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) {

View File

@@ -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"))
} }

View File

@@ -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})
} }

View File

@@ -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"

View File

@@ -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"))
} }

View 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)
}

View File

@@ -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")

View File

@@ -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,
}
}

View File

@@ -0,0 +1,10 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/common/router"
)
func init() {
initialize.AddInitRouterFunc(router.Init)
}

View File

@@ -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)
}
}

View File

@@ -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)
} }

View 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,
}
}

View File

@@ -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错误")

View File

@@ -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")
}

View File

@@ -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) {

View File

@@ -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)
} }
} }

View File

@@ -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
} }

View File

@@ -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]

View File

@@ -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) {

View File

@@ -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"` // 是否复制数据
}

View File

@@ -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 {

View File

@@ -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"`

View File

@@ -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:"-"` // 最近一次恢复结果
} }

View File

@@ -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))
} }

View File

@@ -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")
} }

View File

@@ -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]

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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...)
} }

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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
} }

View File

@@ -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