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 .
RUN yarn config set registry 'https://registry.npm.taobao.org' && \
RUN yarn config set registry 'https://registry.npmmirror.com' && \
yarn install && \
yarn build
@@ -24,7 +24,7 @@ COPY --from=fe-builder /mayfly/dist /mayfly/static/static
# Build
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux \
go build -a \
go build -a -ldflags=-w \
-o mayfly-go main.go
FROM debian:bookworm-slim

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",
"cropperjs": "^1.5.11",
"echarts": "^5.4.3",
"element-plus": "^2.5.1",
"element-plus": "^2.5.5",
"js-base64": "^3.7.5",
"jsencrypt": "^3.3.2",
"lodash": "^4.17.21",
@@ -33,7 +33,7 @@
"splitpanes": "^3.1.5",
"sql-formatter": "^15.0.2",
"uuid": "^9.0.1",
"vue": "^3.4.14",
"vue": "^3.4.15",
"vue-router": "^4.2.5",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
@@ -49,13 +49,14 @@
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/compiler-sfc": "^3.4.14",
"code-inspector-plugin": "^0.4.5",
"dotenv": "^16.3.1",
"eslint": "^8.35.0",
"eslint-plugin-vue": "^9.19.2",
"prettier": "^3.1.0",
"sass": "^1.69.0",
"typescript": "^5.3.2",
"vite": "^5.0.11",
"vite": "^5.0.12",
"vue-eslint-parser": "^9.4.0"
},
"browserslist": [

File diff suppressed because one or more lines are too long

View File

@@ -55,11 +55,11 @@
"unicode_decimal": 58905
},
{
"icon_id": "11617944",
"icon_id": "25271976",
"name": "oracle",
"font_class": "oracle",
"unicode": "e6ea",
"unicode_decimal": 59114
"unicode": "e507",
"unicode_decimal": 58631
},
{
"icon_id": "8105644",
@@ -67,6 +67,41 @@
"font_class": "mariadb",
"unicode": "e513",
"unicode_decimal": 58643
},
{
"icon_id": "13601813",
"name": "sqlite",
"font_class": "sqlite",
"unicode": "e546",
"unicode_decimal": 58694
},
{
"icon_id": "29340317",
"name": "temp-mssql",
"font_class": "MSSQLNATIVE",
"unicode": "e600",
"unicode_decimal": 58880
},
{
"icon_id": "7699332",
"name": "gaussdb",
"font_class": "gauss",
"unicode": "e683",
"unicode_decimal": 59011
},
{
"icon_id": "34836637",
"name": "kingbase",
"font_class": "kingbase",
"unicode": "e882",
"unicode_decimal": 59522
},
{
"icon_id": "33047500",
"name": "vastbase",
"font_class": "vastbase",
"unicode": "e62b",
"unicode_decimal": 58923
}
]
}

View File

@@ -15,7 +15,7 @@ const config = {
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.7.0',
version: 'v1.7.3',
};
export default config;

View File

@@ -1,5 +1,5 @@
<template>
<el-input v-model="cron" placeholder="可点击左边按钮进行可视化配置">
<el-input v-model="cron" placeholder="可点击左边按钮配置">
<template #prepend>
<el-button @click="showCron = true" icon="Pointer"></el-button>
</template>

View File

@@ -119,8 +119,8 @@ const open = (optionProps: MonacoEditorDialogProps) => {
}
setTimeout(() => {
editorRef.value?.format();
editorRef.value?.focus();
editorRef.value?.format();
}, 300);
state.dialogVisible = true;

View File

@@ -189,7 +189,7 @@ const emit = defineEmits(['update:queryForm', 'update:selectionData', 'pageChang
export interface PageTableProps {
size?: string;
pageApi: Api; // 请求表格数据的 api
pageApi?: Api; // 请求表格数据的 api
columns: TableColumn[]; // 列配置项 ==> 必传
showSelection?: boolean;
selectable?: (row: any) => boolean; // 是否可选
@@ -257,7 +257,7 @@ const changeSimpleFormItem = (searchItem: SearchItem) => {
nowSearchItem.value = searchItem;
};
const { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
props.pageable,
props.pageApi,
queryForm,
@@ -288,6 +288,13 @@ watch(isShowSearch, () => {
calcuTableHeight();
});
watch(
() => props.data,
(newValue: any) => {
tableData = newValue;
}
);
onMounted(async () => {
calcuTableHeight();
useEventListener(window, 'resize', calcuTableHeight);

View File

@@ -8,7 +8,7 @@
<script lang="ts" setup>
import 'xterm/css/xterm.css';
import { Terminal } from 'xterm';
import { ITheme, Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { SearchAddon } from 'xterm-addon-search';
import { WebLinksAddon } from 'xterm-addon-web-links';
@@ -92,12 +92,13 @@ function init() {
cursorBlink: true,
disableStdin: false,
allowProposedApi: true,
fastScrollModifier: 'ctrl',
theme: {
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
background: themeConfig.value.terminalBackground || '#002833', //背景色
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
// cursorAccent: "red", // 光标停止颜色
} as any,
} as ITheme,
});
term.open(terminalRef.value);
@@ -105,7 +106,7 @@ function init() {
const fitAddon = new FitAddon();
state.addon.fit = fitAddon;
term.loadAddon(fitAddon);
fitTerminal();
resize();
// 注册搜索组件
const searchAddon = new SearchAddon();
@@ -146,7 +147,7 @@ const onConnected = () => {
state.status = TerminalStatus.Connected;
// 注册窗口大小监听器
useEventListener('resize', debounce(fitTerminal, 400));
useEventListener('resize', debounce(resize, 400));
focus();
@@ -158,17 +159,11 @@ const onConnected = () => {
// 自适应终端
const fitTerminal = () => {
const dimensions = state.addon.fit && state.addon.fit.proposeDimensions();
if (!dimensions) {
return;
}
if (dimensions?.cols && dimensions?.rows) {
term.resize(dimensions.cols, dimensions.rows);
}
resize();
};
const focus = () => {
setTimeout(() => term.focus(), 400);
setTimeout(() => term.focus(), 100);
};
const clear = () => {
@@ -265,7 +260,13 @@ const getStatus = (): TerminalStatus => {
return state.status;
};
defineExpose({ init, fitTerminal, focus, clear, close, getStatus });
const resize = () => {
nextTick(() => {
state.addon.fit.fit();
});
};
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize, resize });
</script>
<style lang="scss">
#terminal-body {

View File

@@ -259,6 +259,10 @@ defineExpose({
padding: 10px;
}
.el-dialog {
padding: 1px 1px;
}
// 取消body最大高度否则全屏有问题
.el-dialog__body {
max-height: 100% !important;

View File

@@ -1,17 +1,22 @@
<template>
<div class="layout-search-dialog">
<el-dialog v-model="state.isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false">
<el-autocomplete v-model="state.menuQuery" :fetch-suggestions="menuSearch" placeholder="菜单搜索"
prefix-icon="el-icon-search" ref="layoutMenuAutocompleteRef" @select="onHandleSelect" @blur="onSearchBlur">
<el-autocomplete
v-model="state.menuQuery"
:fetch-suggestions="menuSearch"
placeholder="菜单搜索"
prefix-icon="el-icon-search"
ref="layoutMenuAutocompleteRef"
@select="onHandleSelect"
@blur="onSearchBlur"
>
<template #prefix>
<el-icon class="el-input__icon">
<search />
</el-icon>
</template>
<template #default="{ item }">
<div>
<SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}
</div>
<div><SvgIcon :name="item.meta.icon" class="mr5" />{{ item.meta.title }}</div>
</template>
</el-autocomplete>
</el-dialog>
@@ -23,7 +28,7 @@ import { reactive, ref, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { useRoutesList } from '@/store/routesList';
const layoutMenuAutocompleteRef: any = ref(null);;
const layoutMenuAutocompleteRef: any = ref(null);
const router = useRouter();
const state: any = reactive({
isShowSearch: false,
@@ -54,8 +59,7 @@ const menuSearch = (queryString: any, cb: any) => {
const createFilter = (queryString: any) => {
return (restaurant: any) => {
return (
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 || restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1
);
};
};
@@ -97,7 +101,7 @@ const onSearchBlur = () => {
closeSearch();
};
defineExpose({openSearch})
defineExpose({ openSearch });
</script>
<style scoped lang="scss">

View File

@@ -615,6 +615,9 @@ const setLocalThemeConfigStyle = () => {
};
// 一键复制配置
const onCopyConfigClick = (target: any) => {
if (!target) {
return;
}
let copyThemeConfig = getLocal('themeConfig');
copyThemeConfig.isDrawer = false;
const clipboard = new ClipboardJS(target, {

View File

@@ -73,12 +73,28 @@ const currentTime = computed(() => {
// 初始化数字滚动
const initNumCountUp = async () => {
const res: any = await indexApi.getIndexCount.request();
nextTick(() => {
new CountUp('mongoNum', res.mongoNum).start();
new CountUp('machineNum', res.machineNum).start();
new CountUp('dbNum', res.dbNum).start();
new CountUp('redisNum', res.redisNum).start();
indexApi.machineDashbord.request().then((res: any) => {
nextTick(() => {
new CountUp('machineNum', res.machineNum).start();
});
});
indexApi.dbDashbord.request().then((res: any) => {
nextTick(() => {
new CountUp('dbNum', res.dbNum).start();
});
});
indexApi.redisDashbord.request().then((res: any) => {
nextTick(() => {
new CountUp('redisNum', res.redisNum).start();
});
});
indexApi.mongoDashbord.request().then((res: any) => {
nextTick(() => {
new CountUp('mongoNum', res.mongoNum).start();
});
});
};

View File

@@ -1,6 +1,8 @@
import Api from '@/common/Api';
export const indexApi = {
getIndexCount: Api.newGet("/common/index/count"),
}
machineDashbord: Api.newGet('/machines/dashbord'),
dbDashbord: Api.newGet('/dbs/dashbord'),
redisDashbord: Api.newGet('/redis/dashbord'),
mongoDashbord: Api.newGet('/mongos/dashbord'),
};

View File

@@ -17,7 +17,7 @@
@node-contextmenu="nodeContextmenu"
>
<template #default="{ node, data }">
<span>
<span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
<span v-if="data.type.value == TagTreeNode.TagPath">
<tag-info :tag-path="data.label" />
</span>
@@ -25,7 +25,13 @@
<slot v-else :node="node" :data="data" name="prefix"></slot>
<span class="ml3" :title="data.labelRemark">
<slot name="label" :data="data"> {{ data.label }}</slot>
<slot name="label" :data="data" v-if="!data.disabled"> {{ data.label }}</slot>
<!-- 禁用状态 -->
<slot name="disabledLabel" :data="data" v-else>
<el-link type="danger" disabled :underline="false">
{{ `${data.label}` }}
</el-link>
</slot>
</span>
<slot :node="node" :data="data" name="suffix"></slot>
@@ -135,15 +141,29 @@ const loadNode = async (node: any, resolve: any) => {
const treeNodeClick = (data: any) => {
emit('nodeClick', data);
if (data.type.nodeClickFunc) {
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
data.type.nodeClickFunc(data);
}
// 关闭可能存在的右击菜单
contextmenuRef.value.closeContextmenu();
};
// 树节点双击事件
const treeNodeDblclick = (data: any) => {
// emit('nodeDblick', data);
if (!data.disabled && data.type.nodeDblclickFunc) {
data.type.nodeDblclickFunc(data);
}
// 关闭可能存在的右击菜单
contextmenuRef.value.closeContextmenu();
};
// 树节点右击事件
const nodeContextmenu = (event: any, data: any) => {
if (data.disabled) {
return;
}
// 加载当前节点是否需要显示右击菜单
let items = data.type.contextMenuItems;
if (!items || items.length == 0) {

View File

@@ -14,6 +14,9 @@
v-model="modelValue"
@change="changeNode"
>
<template #prefix="{ node, data }">
<slot name="iconPrefix" :node="node" :data="data" />
</template>
<template #default="{ node, data }">
<span>
<span v-if="data.type.value == TagTreeNode.TagPath">
@@ -33,7 +36,7 @@
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
import { onMounted, reactive, ref, toRefs, watch } from 'vue';
import { NodeType, TagTreeNode } from './tag';
import TagInfo from './TagInfo.vue';
import { tagApi } from '../tag/api';

View File

@@ -28,6 +28,11 @@ export class TagTreeNode {
*/
isLeaf: boolean = false;
/**
* 是否禁用状态
*/
disabled: boolean = false;
/**
* 额外需要传递的参数
*/
@@ -53,6 +58,11 @@ export class TagTreeNode {
return this;
}
withDisabled(disabled: boolean) {
this.disabled = disabled;
return this;
}
withParams(params: any) {
this.params = params;
return this;
@@ -91,8 +101,14 @@ export class NodeType {
loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
/**
* 节点点击事件
*/
nodeClickFunc: (node: TagTreeNode) => void;
// 节点双击事件
nodeDblclickFunc: (node: TagTreeNode) => void;
constructor(value: number) {
this.value = value;
}
@@ -117,6 +133,16 @@ export class NodeType {
return this;
}
/**
* 赋值节点双击事件回调函数
* @param func 节点双击事件回调函数
* @returns this
*/
withNodeDblclickFunc(func: (node: TagTreeNode) => void) {
this.nodeDblclickFunc = func;
return this;
}
/**
* 赋值右击菜单按钮选项
* @param contextMenuItems 右击菜单按钮选项

View File

@@ -23,13 +23,16 @@
</el-form-item>
<el-form-item prop="name" label="任务名称">
<el-input v-model.number="state.form.name" type="text" placeholder="任务名称"></el-input>
<el-input v-model="state.form.name" type="text" placeholder="任务名称"></el-input>
</el-form-item>
<el-form-item prop="startTime" label="开始时间">
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
</el-form-item>
<el-form-item prop="intervalDay" label="备份周期">
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="备份周期(单位:天"></el-input>
<el-form-item prop="intervalDay" label="备份周期(天)">
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="单位:天"></el-input>
</el-form-item>
<el-form-item prop="maxSaveDays" label="备份历史保留天数">
<el-input v-model.number="state.form.maxSaveDays" type="number" placeholder="0: 永久保留"></el-input>
</el-form-item>
</el-form>
@@ -92,6 +95,14 @@ const rules = {
trigger: ['change', 'blur'],
},
],
maxSaveDays: [
{
required: true,
pattern: /^[0-9]\d*$/,
message: '请输入非负整数',
trigger: ['change', 'blur'],
},
],
};
const backupForm: any = ref(null);
@@ -101,10 +112,11 @@ const state = reactive({
id: 0,
dbId: 0,
dbNames: '',
name: null as any,
intervalDay: null,
name: '',
intervalDay: 1,
startTime: null as any,
repeated: null as any,
repeated: true,
maxSaveDays: 0,
},
btnLoading: false,
dbNamesSelected: [] as any,
@@ -137,12 +149,14 @@ const init = (data: any) => {
state.form.name = data.name;
state.form.intervalDay = data.intervalDay;
state.form.startTime = data.startTime;
state.form.maxSaveDays = data.maxSaveDays;
} else {
state.editOrCreate = false;
state.form.name = '';
state.form.intervalDay = null;
state.form.intervalDay = 1;
const now = new Date();
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
state.form.maxSaveDays = 0;
getDbNamesWithoutBackup();
}
};

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="video-play" @click="enableDbBackup(null)">启用</el-button>
<el-button type="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
<el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
</template>
<template #action="{ data }">
@@ -29,6 +30,7 @@
<el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
<el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
<el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
<el-button @click="deleteDbBackup(data)" type="danger" link>删除</el-button>
</div>
</template>
</page-table>
@@ -49,7 +51,7 @@ import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
import { ElMessage } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
const pageTableRef: Ref<any> = ref(null);
@@ -72,10 +74,10 @@ const columns = [
TableColumn.new('name', '任务名称'),
TableColumn.new('startTime', '启动时间').isTime(),
TableColumn.new('intervalDay', '备份周期'),
TableColumn.new('enabled', '是否启用'),
TableColumn.new('enabledDesc', '是否启用'),
TableColumn.new('lastResult', '执行结果'),
TableColumn.new('lastTime', '执行时间').isTime(),
TableColumn.new('action', '操作').isSlot().setMinWidth(180).fixedRight(),
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
];
const emptyQuery = {
@@ -168,5 +170,25 @@ const startDbBackup = async (data: any) => {
await search();
ElMessage.success('备份任务启动成功');
};
const deleteDbBackup = async (data: any) => {
let backupId: string;
if (data) {
backupId = data.id;
} else if (state.selectedData.length > 0) {
backupId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要删除的数据库备份任务');
return;
}
await ElMessageBox.confirm(`确定删除 “数据库备份任务” 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbBackup.request({ dbId: props.dbId, backupId: backupId });
await search();
ElMessage.success('删除成功');
};
</script>
<style lang="scss"></style>

View File

@@ -62,8 +62,21 @@
<el-dropdown-menu>
<el-dropdown-item :command="{ type: 'detail', data }"> 详情 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'dumpDb', data }" v-if="supportAction('dumpDb', data.type)"> 导出 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'dbBackup', data }" v-if="supportAction('dbBackup', data.type)"> 备份 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'dbRestore', data }" v-if="supportAction('dbRestore', data.type)"> 恢复 </el-dropdown-item>
<el-dropdown-item :command="{ type: 'backupDb', data }" v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)">
备份任务
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'backupHistory', data }"
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
>
备份历史
</el-dropdown-item>
<el-dropdown-item
:command="{ type: 'restoreDb', data }"
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
>
恢复任务
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -131,6 +144,16 @@
<db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
</el-dialog>
<el-dialog
width="80%"
:title="`${dbBackupHistoryDialog.title} - 数据库备份历史`"
:close-on-click-modal="false"
:destroy-on-close="true"
v-model="dbBackupHistoryDialog.visible"
>
<db-backup-history-list :dbId="dbBackupHistoryDialog.dbId" :dbNames="dbBackupHistoryDialog.dbs" />
</el-dialog>
<el-dialog
width="80%"
:title="`${dbRestoreDialog.title} - 数据库恢复`"
@@ -185,6 +208,7 @@ import { getDbDialect } from './dialect/index';
import { getTagPathSearchItem } from '../component/tag';
import { SearchItem } from '@/components/SearchForm';
import DbBackupList from './DbBackupList.vue';
import DbBackupHistoryList from './DbBackupHistoryList.vue';
import DbRestoreList from './DbRestoreList.vue';
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
@@ -193,6 +217,8 @@ const perms = {
base: 'db',
saveDb: 'db:save',
delDb: 'db:del',
backupDb: 'db:backup',
restoreDb: 'db:restore',
};
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.slot('instanceId', '实例', 'instanceSelect')];
@@ -208,7 +234,8 @@ const columns = ref([
]);
// 该用户拥有的的操作列按钮权限
const actionBtns = hasPerms([perms.base, perms.saveDb]);
// const actionBtns = hasPerms([perms.base, perms.saveDb]);
const actionBtns = hasPerms(Object.values(perms));
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
const route = useRoute();
@@ -253,6 +280,13 @@ const state = reactive({
dbs: [],
dbId: 0,
},
// 数据库备份历史弹框
dbBackupHistoryDialog: {
title: '',
visible: false,
dbs: [],
dbId: 0,
},
// 数据库恢复弹框
dbRestoreDialog: {
title: '',
@@ -285,7 +319,8 @@ const state = reactive({
},
});
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbRestoreDialog } = toRefs(state);
const { db, selectionData, query, infoDialog, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } =
toRefs(state);
onMounted(async () => {
if (Object.keys(actionBtns).length > 0) {
@@ -345,11 +380,15 @@ const handleMoreActionCommand = (commond: any) => {
onDumpDbs(data);
return;
}
case 'dbBackup': {
case 'backupDb': {
onShowDbBackupDialog(data);
return;
}
case 'dbRestore': {
case 'backupHistory': {
onShowDbBackupHistoryDialog(data);
return;
}
case 'restoreDb': {
onShowDbRestoreDialog(data);
return;
}
@@ -402,6 +441,13 @@ const onShowDbBackupDialog = async (row: any) => {
state.dbBackupDialog.visible = true;
};
const onShowDbBackupHistoryDialog = async (row: any) => {
state.dbBackupHistoryDialog.title = `${row.name}`;
state.dbBackupHistoryDialog.dbId = row.id;
state.dbBackupHistoryDialog.dbs = row.database.split(' ');
state.dbBackupHistoryDialog.visible = true;
};
const onShowDbRestoreDialog = async (row: any) => {
state.dbRestoreDialog.title = `${row.name}`;
state.dbRestoreDialog.dbId = row.id;
@@ -455,7 +501,7 @@ const supportAction = (action: string, dbType: string): boolean => {
switch (dbType) {
case DbType.mysql:
case DbType.mariadb:
actions = ['dumpDb', 'dbBackup', 'dbRestore'];
actions = ['dumpDb', 'backupDb', 'restoreDb'];
}
return actions.includes(action);
};

View File

@@ -35,7 +35,13 @@
clearable
class="w100"
>
<el-option v-for="item in state.histories" :key="item.id" :label="item.name" :value="item"> </el-option>
<el-option
v-for="item in state.histories"
:key="item.id"
:label="item.name + (item.binlogFileName ? ' ' : ' 不') + '支持指定时间点恢复'"
:value="item"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item prop="startTime" label="开始时间">
@@ -56,7 +62,7 @@
<script lang="ts" setup>
import { onMounted, reactive, ref, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
const props = defineProps({
data: {
@@ -83,20 +89,30 @@ const visible = defineModel<boolean>('visible', {
});
const validatePointInTime = (rule: any, value: any, callback: any) => {
if (!state.histories || state.histories.length == 0) {
callback(new Error('数据库没有备份记录'));
return;
}
const history = state.histories[state.histories.length - 1];
if (value < new Date(history.createTime)) {
callback(new Error('在此之前数据库没有备份记录'));
return;
}
if (value > new Date()) {
callback(new Error('恢复时间点晚于当前时间'));
return;
}
callback();
if (!state.histories || state.histories.length == 0) {
callback(new Error('数据库没有备份记录'));
return;
}
let last = null;
for (const history of state.histories) {
if (!history.binlogFileName || history.binlogFileName.length === 0) {
break;
}
if (new Date(history.createTime) < value) {
callback();
return;
}
last = history;
}
if (!last) {
callback(new Error('现有数据库备份不支持指定时间恢复'));
return;
}
callback(last.name + ' 之前的数据库备份不支持指定时间恢复');
};
const rules = {
@@ -110,7 +126,6 @@ const rules = {
pointInTime: [
{
required: true,
// message: '请选择恢复时间点',
validator: validatePointInTime,
trigger: ['change', 'blur'],
},
@@ -146,7 +161,7 @@ const state = reactive({
id: 0,
dbId: 0,
dbName: null as any,
intervalDay: 1,
intervalDay: 0,
startTime: null as any,
repeated: null as any,
dbBackupId: null as any,
@@ -218,7 +233,8 @@ const init = async (data: any) => {
} else {
state.form.dbName = '';
state.editOrCreate = false;
state.form.intervalDay = 1;
state.form.intervalDay = 0;
state.form.repeated = false;
state.form.pointInTime = new Date();
state.form.startTime = new Date();
state.histories = [];
@@ -237,6 +253,12 @@ const getDbNamesWithoutRestore = async () => {
const btnOk = async () => {
restoreForm.value.validate(async (valid: any) => {
if (valid) {
await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
if (state.restoreMode == 'point-in-time') {
state.form.dbBackupId = 0;
state.form.dbBackupHistoryId = 0;
@@ -245,13 +267,14 @@ const btnOk = async () => {
state.form.pointInTime = null;
}
state.form.repeated = false;
state.form.intervalDay = 0;
const reqForm = { ...state.form };
let api = dbApi.createDbRestore;
if (props.data) {
api = dbApi.saveDbRestore;
}
api.request(reqForm).then(() => {
ElMessage.success('保存成功');
ElMessage.success('成功创建数据库恢复任务');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {

View File

@@ -21,12 +21,14 @@
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
<el-button type="primary" icon="video-play" @click="enableDbRestore(null)">启用</el-button>
<el-button type="primary" icon="video-pause" @click="disableDbRestore(null)">禁用</el-button>
<el-button type="danger" icon="delete" @click="deleteDbRestore(null)">删除</el-button>
</template>
<template #action="{ data }">
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
<el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
<el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
<el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
</template>
</page-table>
@@ -49,7 +51,7 @@
infoDialog.data.dbBackupHistoryName
}}</el-descriptions-item>
<el-descriptions-item :span="1" label="开始时间">{{ dateFormat(infoDialog.data.startTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabled }}</el-descriptions-item>
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行时间">{{ dateFormat(infoDialog.data.lastTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
</el-descriptions>
@@ -63,7 +65,7 @@ import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
import { ElMessage } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
import { dateFormat } from '@/common/utils/date';
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
const pageTableRef: Ref<any> = ref(null);
@@ -85,7 +87,7 @@ const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
const columns = [
TableColumn.new('dbName', '数据库名称'),
TableColumn.new('startTime', '启动时间').isTime(),
TableColumn.new('enabled', '是否启用'),
TableColumn.new('enabledDesc', '是否启用'),
TableColumn.new('lastTime', '执行时间').isTime(),
TableColumn.new('lastResult', '执行结果'),
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
@@ -135,19 +137,39 @@ const createDbRestore = async () => {
state.dbRestoreEditDialog.visible = true;
};
const deleteDbRestore = async (data: any) => {
let restoreId: string;
if (data) {
restoreId = data.id;
} else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要删除的数据库恢复任务');
return;
}
await ElMessageBox.confirm(`确定删除 “数据库恢复任务” 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
await search();
ElMessage.success('删除成功');
};
const showDbRestore = async (data: any) => {
state.infoDialog.data = data;
state.infoDialog.visible = true;
};
const enableDbRestore = async (data: any) => {
let restoreId: String;
let restoreId: string;
if (data) {
restoreId = data.id;
} else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要启用的恢复任务');
ElMessage.error('请选择需要启用的数据库恢复任务');
return;
}
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
@@ -156,13 +178,13 @@ const enableDbRestore = async (data: any) => {
};
const disableDbRestore = async (data: any) => {
let restoreId: String;
let restoreId: string;
if (data) {
restoreId = data.id;
} else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要禁用的恢复任务');
ElMessage.error('请选择需要禁用的数据库恢复任务');
return;
}
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });

View File

@@ -36,7 +36,7 @@
</template>
<script lang="ts" setup>
import { toRefs, watch, reactive, onMounted, Ref, ref } from 'vue';
import { onMounted, reactive, Ref, ref, toRefs, watch } from 'vue';
import { dbApi } from './api';
import { DbSqlExecTypeEnum } from './enums';
import PageTable from '@/components/pagetable/PageTable.vue';
@@ -120,6 +120,12 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
const primaryKey = getPrimaryKey(columns);
const oldValue = JSON.parse(sqlExecLog.oldValue);
let schema = '';
let dbArr = sqlExecLog.db.split('/');
if (dbArr.length == 2) {
schema = dbArr[1] + '.';
}
const rollbackSqls = [];
if (sqlExecLog.type == DbSqlExecTypeEnum.Update.value) {
for (let ov of oldValue) {
@@ -130,7 +136,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
}
setItems.push(`${key} = ${wrapValue(ov[key])}`);
}
rollbackSqls.push(`UPDATE ${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
rollbackSqls.push(`UPDATE ${schema}${sqlExecLog.table} SET ${setItems.join(', ')} WHERE ${primaryKey} = ${wrapValue(ov[primaryKey])};`);
}
} else if (sqlExecLog.type == DbSqlExecTypeEnum.Delete.value) {
const columnNames = columns.map((c: any) => c.columnName);
@@ -139,7 +145,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
for (let column of columnNames) {
values.push(wrapValue(ov[column]));
}
rollbackSqls.push(`INSERT INTO ${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
rollbackSqls.push(`INSERT INTO ${schema}${sqlExecLog.table} (${columnNames.join(', ')}) VALUES (${values.join(', ')});`);
}
}
@@ -148,7 +154,7 @@ const onShowRollbackSql = async (sqlExecLog: any) => {
};
const getPrimaryKey = (columns: any) => {
const col = columns.find((c: any) => c.columnKey == 'PRI');
const col = columns.find((c: any) => c.isPrimaryKey);
if (col) {
return col.columnName;
}

View File

@@ -9,13 +9,22 @@
</el-form-item>
<el-form-item prop="type" label="类型" required>
<el-select @change="changeDbType" style="width: 100%" v-model="form.type" placeholder="请选择数据库类型">
<el-option v-for="dt in dbTypes" :key="dt.type" :value="dt.type" :label="dt.label">
<SvgIcon :name="getDbDialect(dt.type).getInfo().icon" :size="18" />
{{ dt.label }}
<el-option
v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
:key="key"
:value="dbTypeAndDialect[0]"
:label="dbTypeAndDialect[1].getInfo().name"
>
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
{{ dbTypeAndDialect[1].getInfo().name }}
</el-option>
<template #prefix>
<SvgIcon :name="getDbDialect(form.type).getInfo().icon" :size="20" />
</template>
</el-select>
</el-form-item>
<el-form-item prop="host" label="host" required>
<el-form-item v-if="form.type !== DbType.sqlite" prop="host" label="host" required>
<el-col :span="18">
<el-input :disabled="form.id !== undefined" v-model.trim="form.host" placeholder="请输入主机ip" auto-complete="off"></el-input>
</el-col>
@@ -24,13 +33,18 @@
<el-input type="number" v-model.number="form.port" placeholder="端口"></el-input>
</el-col>
</el-form-item>
<el-form-item v-if="form.type === DbType.sqlite" prop="host" label="sqlite地址">
<el-input v-model.trim="form.host" placeholder="请输入sqlite文件在服务器的绝对地址"></el-input>
</el-form-item>
<el-form-item v-if="form.type === DbType.oracle" prop="sid" label="SID">
<el-input v-model.trim="form.sid" placeholder="请输入服务id"></el-input>
</el-form-item>
<el-form-item prop="username" label="用户名" required>
<el-form-item v-if="form.type !== DbType.sqlite" prop="username" label="用户名" required>
<el-input v-model.trim="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password" label="密码">
<el-form-item v-if="form.type !== DbType.sqlite" prop="password" label="密码">
<el-input type="password" show-password v-model.trim="form.password" placeholder="请输入密码" autocomplete="new-password">
<template v-if="form.id && form.id != 0" #suffix>
<el-popover @hide="pwd = ''" placement="right" title="原密码" :width="200" trigger="click" :content="pwd">
@@ -90,7 +104,7 @@ import { ElMessage } from 'element-plus';
import { notBlank } from '@/common/assert';
import { RsaEncrypt } from '@/common/rsa';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { DbType, getDbDialect } from './dialect';
import { DbType, getDbDialect, getDbDialectMap } from './dialect';
import SvgIcon from '@/components/svgIcon/index.vue';
const props = defineProps({
@@ -148,35 +162,12 @@ const rules = {
const dbForm: any = ref(null);
const dbTypes = [
{
type: 'mysql',
label: 'mysql',
},
{
type: 'mariadb',
label: 'mariadb',
},
{
type: 'postgres',
label: 'postgres',
},
{
type: 'dm',
label: '达梦',
},
{
type: 'oracle',
label: 'oracle',
},
];
const state = reactive({
dialogVisible: false,
tabActiveName: 'basic',
form: {
id: null,
type: null,
type: '',
name: null,
host: '',
port: null,
@@ -187,17 +178,17 @@ const state = reactive({
remark: '',
sshTunnelMachineId: null as any,
},
subimtForm: {},
submitForm: {},
// 原密码
pwd: '',
// 原用户名
oldUserName: null,
});
const { dialogVisible, tabActiveName, form, subimtForm, pwd } = toRefs(state);
const { dialogVisible, tabActiveName, form, submitForm, pwd } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(subimtForm);
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(subimtForm);
const { isFetching: saveBtnLoading, execute: saveInstanceExec } = dbApi.saveInstance.useApi(submitForm);
const { isFetching: testConnBtnLoading, execute: testConnExec } = dbApi.testConn.useApi(submitForm);
watch(props, (newValue: any) => {
state.dialogVisible = newValue.visible;
@@ -209,7 +200,7 @@ watch(props, (newValue: any) => {
state.form = { ...newValue.data };
state.oldUserName = state.form.username;
} else {
state.form = { port: null } as any;
state.form = { port: null, type: DbType.mysql } as any;
state.oldUserName = null;
}
});
@@ -240,17 +231,19 @@ const testConn = async () => {
return false;
}
state.subimtForm = await getReqForm();
state.submitForm = await getReqForm();
await testConnExec();
ElMessage.success('连接成功');
});
};
const btnOk = async () => {
if (!state.form.id) {
notBlank(state.form.password, '新增操作,密码不可为空');
} else if (state.form.username != state.oldUserName) {
notBlank(state.form.password, '已修改用户名,请输入密码');
if (state.form.type !== DbType.sqlite) {
if (!state.form.id) {
notBlank(state.form.password, '新增操作,密码不可为空');
} else if (state.form.username != state.oldUserName) {
notBlank(state.form.password, '已修改用户名,请输入密码');
}
}
dbForm.value.validate(async (valid: boolean) => {
@@ -259,7 +252,7 @@ const btnOk = async () => {
return false;
}
state.subimtForm = await getReqForm();
state.submitForm = await getReqForm();
await saveInstanceExec();
ElMessage.success('保存成功');
emit('val-change', state.form);

View File

@@ -17,7 +17,7 @@
</template>
<template #type="{ data }">
<el-tooltip :content="data.type" placement="top">
<el-tooltip :content="getDbDialect(data.type).getInfo().name" placement="top">
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
</el-tooltip>
</template>
@@ -25,6 +25,7 @@
<template #action="{ data }">
<el-button @click="showInfo(data)" link>详情</el-button>
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
<el-button v-if="actionBtns[perms.delInstance]" @click="deleteInstance(data)" type="primary" link>删除</el-button>
</template>
</page-table>
@@ -91,7 +92,7 @@ const columns = ref([
]);
// 该用户拥有的的操作列按钮权限
const actionBtns = hasPerms([perms.saveInstance]);
const actionBtns = hasPerms(Object.values(perms));
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
const pageTableRef: Ref<any> = ref(null);
@@ -150,14 +151,26 @@ const editInstance = async (data: any) => {
state.instanceEditDialog.visible = true;
};
const deleteInstance = async () => {
const deleteInstance = async (data: any) => {
try {
await ElMessageBox.confirm(`确定删除数据库实例【${state.selectionData.map((x: any) => x.name).join(', ')}】?`, '提示', {
let instanceName: string;
if (data) {
instanceName = data.name;
} else {
instanceName = state.selectionData.map((x: any) => x.name).join(', ');
}
await ElMessageBox.confirm(`确定删除数据库实例【${instanceName}】?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteInstance.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
let instanceId: string;
if (data) {
instanceId = data.id;
} else {
instanceId = state.selectionData.map((x: any) => x.id).join(',');
}
await dbApi.deleteInstance.request({ id: instanceId });
ElMessage.success('删除成功');
search();
} catch (err) {

View File

@@ -71,7 +71,7 @@
<el-descriptions-item label-align="right">
<template #label>
<div>
<SvgIcon :name="getDbDialect(nowDbInst.type).getInfo().icon" :size="18" />
<SvgIcon :name="nowDbInst.getDialect().getInfo().icon" :size="18" />
实例
</div>
</template>
@@ -151,12 +151,22 @@
</div>
</Pane>
</Splitpanes>
<db-table-op
:title="tableCreateDialog.title"
:active-name="tableCreateDialog.activeName"
:dbId="tableCreateDialog.dbId"
:db="tableCreateDialog.db"
:dbType="tableCreateDialog.dbType"
:data="tableCreateDialog.data"
v-model:visible="tableCreateDialog.visible"
@submit-sql="onSubmitEditTableSql"
/>
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { defineAsyncComponent, h, onBeforeUnmount, onMounted, reactive, ref, toRefs } from 'vue';
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
import { formatByteSize } from '@/common/utils/format';
import { DbInst, registerDbCompletionItemProvider, TabInfo, TabType } from './db';
import { NodeType, TagTreeNode } from '../component/tag';
@@ -165,12 +175,13 @@ import { dbApi } from './api';
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
import SvgIcon from '@/components/svgIcon/index.vue';
import { ContextmenuItem } from '@/components/contextmenu';
import { DbType, getDbDialect } from './dialect/index';
import { getDbDialect, schemaDbTypes } from './dialect/index';
import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Pane, Splitpanes } from 'splitpanes';
import { useEventListener } from '@vueuse/core';
const DbTableOp = defineAsyncComponent(() => import('./component/table/DbTableOp.vue'));
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
const DbTablesOp = defineAsyncComponent(() => import('./component/table/DbTablesOp.vue'));
@@ -218,21 +229,25 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
}
};
// tagpath 节点类型
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
const dbInfos = dbInfoRes.list;
if (!dbInfos) {
return [];
}
const ContextmenuItemRefresh = new ContextmenuItem('refresh', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key));
// 防止过快加载会出现一闪而过,对眼睛不好
await sleep(100);
return dbInfos?.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
});
});
// tagpath 节点类型
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath)
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
const dbInfos = dbInfoRes.list;
if (!dbInfos) {
return [];
}
// 防止过快加载会出现一闪而过,对眼睛不好
await sleep(100);
return dbInfos?.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
});
})
.withContextMenuItems([ContextmenuItemRefresh]);
// 数据库实例节点类型
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((parentNode: TagTreeNode) => {
@@ -255,12 +270,12 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
// 数据库节点
const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
.withContextMenuItems([ContextmenuItemRefresh])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
params.parentKey = parentNode.key;
// pg类数据库会多一层schema
if (params.type == DbType.postgresql || params.type === DbType.dm || params.type === DbType.oracle) {
const params = parentNode.params;
if (schemaDbTypes.includes(params.type)) {
const { id, db } = params;
const schemaNames = await dbApi.pgSchemas.request({ id, db });
return schemaNames.map((sn: any) => {
@@ -269,33 +284,37 @@ const NodeTypeDb = new NodeType(SqlExecNodeType.Db)
nParams.schema = sn;
nParams.db = nParams.db + '/' + sn;
nParams.dbs = schemaNames;
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresScheam).withParams(nParams).withIcon(SchemaIcon);
return new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
});
}
return [
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
];
return NodeTypeTables(params);
})
.withNodeClickFunc(nodeClickChangeDb);
const NodeTypeTables = (params: any) => {
let tableKey = `${params.id}.${params.db}.table-menu`;
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
return [
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams({ ...params, key: tableKey }).withIcon(TableIcon),
new TagTreeNode(sqlKey, 'SQL', NodeTypeSqlMenu).withParams({ ...params, key: sqlKey }).withIcon(SqlIcon),
];
};
// postgres schema模式
const NodeTypePostgresScheam = new NodeType(SqlExecNodeType.PgSchema)
.withContextMenuItems([new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key))])
const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema)
.withContextMenuItems([ContextmenuItemRefresh])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
return [
new TagTreeNode(`${params.id}.${params.db}.table-menu`, '表', NodeTypeTableMenu).withParams(params).withIcon(TableIcon),
new TagTreeNode(getSqlMenuNodeKey(params.id, params.db), 'SQL', NodeTypeSqlMenu).withParams(params).withIcon(SqlIcon),
];
params.parentKey = parentNode.key;
return NodeTypeTables(params);
})
.withNodeClickFunc(nodeClickChangeDb);
// 数据库表菜单节点
const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
.withContextMenuItems([
new ContextmenuItem('reloadTables', '刷新').withIcon('RefreshRight').withOnClick((data: any) => reloadNode(data.key)),
ContextmenuItemRefresh,
new ContextmenuItem('createTable', '创建表').withIcon('Plus').withOnClick((data: any) => onEditTable(data)),
new ContextmenuItem('tablesOp', '表操作').withIcon('Setting').withOnClick((data: any) => {
const params = data.params;
addTablesOpTab({ id: params.id, db: params.db, type: params.type, nodeKey: data.key });
@@ -303,27 +322,32 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
let { id, db } = params;
let { id, db, type } = params;
// 获取当前库的所有表信息
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
state.reloadStatus = false;
let dbTableSize = 0;
const tablesNode = tables.map((x: any) => {
dbTableSize += x.dataLength + x.indexLength;
return new TagTreeNode(`${id}.${db}.${x.tableName}`, x.tableName, NodeTypeTable)
const tableSize = x.dataLength + x.indexLength;
dbTableSize += tableSize;
const key = `${id}.${db}.${x.tableName}`;
return new TagTreeNode(key, x.tableName, NodeTypeTable)
.withIsLeaf(true)
.withParams({
id,
db,
type,
key: key,
parentKey: parentNode.key,
tableName: x.tableName,
tableComment: x.tableComment,
size: formatByteSize(x.dataLength + x.indexLength, 1),
size: tableSize == 0 ? '' : formatByteSize(tableSize, 1),
})
.withIcon(TableIcon)
.withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
});
// 设置父节点参数的表大小
parentNode.params.dbTableSize = formatByteSize(dbTableSize);
parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
return tablesNode;
})
.withNodeClickFunc(nodeClickChangeDb);
@@ -340,22 +364,23 @@ const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)
return sqls.map((x: any) => {
return new TagTreeNode(`${id}.${db}.${x.name}`, x.name, NodeTypeSql)
.withIsLeaf(true)
.withParams({
id,
db,
dbs,
sqlName: x.name,
})
.withParams({ id, db, dbs, sqlName: x.name })
.withIcon(SqlIcon);
});
})
.withNodeClickFunc(nodeClickChangeDb);
// 表节点类型
const NodeTypeTable = new NodeType(SqlExecNodeType.Table).withNodeClickFunc((nodeData: TagTreeNode) => {
const params = nodeData.params;
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
});
const NodeTypeTable = new NodeType(SqlExecNodeType.Table)
.withContextMenuItems([
new ContextmenuItem('copyTable', '复制表').withIcon('copyDocument').withOnClick((data: any) => onCopyTable(data)),
new ContextmenuItem('editTable', '编辑表').withIcon('edit').withOnClick((data: any) => onEditTable(data)),
new ContextmenuItem('delTable', '删除表').withIcon('Delete').withOnClick((data: any) => onDeleteTable(data)),
])
.withNodeClickFunc((nodeData: TagTreeNode) => {
const params = nodeData.params;
loadTableData({ id: params.id, nodeKey: nodeData.key }, params.db, params.tableName);
});
// sql模板节点类型
const NodeTypeSql = new NodeType(SqlExecNodeType.Sql)
@@ -385,9 +410,19 @@ const state = reactive({
loading: true,
version: '',
},
tableCreateDialog: {
visible: false,
title: '',
activeName: '',
dbId: 0,
db: '',
dbType: '',
data: {},
parentKey: '',
},
});
const { nowDbInst } = toRefs(state);
const { nowDbInst, tableCreateDialog } = toRefs(state);
const serverInfoReqParam = ref({
instanceId: 0,
@@ -408,7 +443,7 @@ onBeforeUnmount(() => {
* 设置editor高度和数据表高度
*/
const setHeight = () => {
state.dataTabsTableHeight = window.innerHeight - 270 + 'px';
state.dataTabsTableHeight = window.innerHeight - 253 + 'px';
state.tablesOpHeight = window.innerHeight - 225 + 'px';
};
@@ -603,6 +638,85 @@ const reloadNode = (nodeKey: string) => {
tagTreeRef.value.reloadNode(nodeKey);
};
const onEditTable = async (data: any) => {
let { db, id, tableName, tableComment, type, parentKey, key } = data.params;
// data.label就是表名
if (tableName) {
state.tableCreateDialog.title = '修改表';
let indexs = await dbApi.tableIndex.request({ id, db, tableName });
let columns = await dbApi.columnMetadata.request({ id, db, tableName });
let row = { tableName, tableComment };
state.tableCreateDialog.data = { edit: true, row, indexs, columns };
state.tableCreateDialog.parentKey = parentKey;
} else {
state.tableCreateDialog.title = '创建表';
state.tableCreateDialog.data = { edit: false, row: {} };
state.tableCreateDialog.parentKey = key;
}
state.tableCreateDialog.visible = true;
state.tableCreateDialog.activeName = '1';
state.tableCreateDialog.dbId = id;
state.tableCreateDialog.db = db;
state.tableCreateDialog.dbType = type;
};
const onDeleteTable = async (data: any) => {
let { db, id, tableName, parentKey } = data.params;
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
// 执行sql
dbApi.sqlExec.request({ id, db, sql: `drop table ${tableName}` }).then(() => {
ElMessage.success('删除成功');
setTimeout(() => {
parentKey && reloadNode(parentKey);
}, 1000);
});
};
const onCopyTable = async (data: any) => {
let { db, id, tableName, parentKey } = data.params;
let checked = ref(false);
// 弹出确认框,并选择是否复制数据
await ElMessageBox({
title: `复制表【${tableName}`,
type: 'warning',
// icon: markRaw(Delete),
message: () =>
h(ElCheckbox, {
label: '是否复制数据?',
modelValue: checked.value,
'onUpdate:modelValue': (val: boolean | string | number) => {
if (typeof val === 'boolean') {
checked.value = val;
}
},
}),
callback: (action: string) => {
if (action === 'confirm') {
// 执行sql
dbApi.copyTable.request({ id, db, tableName, copyData: checked.value }).then(() => {
ElMessage.success('复制成功');
setTimeout(() => {
parentKey && reloadNode(parentKey);
}, 1000);
});
}
},
});
};
const onSubmitEditTableSql = () => {
state.tableCreateDialog.visible = false;
state.tableCreateDialog.data = { edit: false, row: {} };
reloadNode(state.tableCreateDialog.parentKey);
};
/**
* 获取当前操作的数据库信息
*/

View File

@@ -45,8 +45,10 @@
<db-select-tree
placeholder="请选择源数据库"
v-model:db-id="form.srcDbId"
v-model:inst-name="form.srcInstName"
v-model:db-name="form.srcDbName"
v-model:tag-path="form.srcTagPath"
v-model:db-type="form.srcDbType"
@select-db="onSelectSrcDb"
/>
</el-form-item>
@@ -55,8 +57,10 @@
<db-select-tree
placeholder="请选择目标数据库"
v-model:db-id="form.targetDbId"
v-model:inst-name="form.targetInstName"
v-model:db-name="form.targetDbName"
v-model:tag-path="form.targetTagPath"
v-model:db-type="form.targetDbType"
@select-db="onSelectTargetDb"
/>
</el-form-item>
@@ -181,7 +185,7 @@ import { ElMessage } from 'element-plus';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
import { getDbDialect } from '@/views/ops/db/dialect';
import { DbType, getDbDialect } from '@/views/ops/db/dialect';
import CrontabInput from '@/components/crontab/CrontabInput.vue';
const props = defineProps({
@@ -226,12 +230,16 @@ type FormData = {
taskName?: string;
taskCron: string;
srcDbId?: number;
srcInstName?: string;
srcDbName?: string;
srcDbType?: string;
srcTagPath?: string;
targetDbId?: number;
targetInstName?: string;
targetDbName?: string;
targetTagPath?: string;
targetTableName?: string;
targetDbType?: string;
dataSql?: string;
pageSize?: number;
updField?: string;
@@ -245,7 +253,7 @@ const basicFormData = {
targetDbId: -1,
dataSql: 'select * from',
pageSize: 1000,
updField: 'id',
updField: '',
updFieldVal: '0',
fieldMap: [{ src: 'a', target: 'b' }],
status: 1,
@@ -302,6 +310,8 @@ watch(dialogVisible, async (newValue: boolean) => {
// 初始化实例
db.databases = db.database?.split(' ').sort() || [];
state.srcDbInst = DbInst.getOrNewInst(db);
state.form.srcDbType = state.srcDbInst.type;
state.form.srcInstName = db.instanceName;
}
// 初始化target数据源
@@ -312,6 +322,8 @@ watch(dialogVisible, async (newValue: boolean) => {
// 初始化实例
db.databases = db.database?.split(' ').sort() || [];
state.targetDbInst = DbInst.getOrNewInst(db);
state.form.targetDbType = state.targetDbInst.type;
state.form.targetInstName = db.instanceName;
}
if (targetDbId && state.form.targetDbName) {
@@ -396,8 +408,8 @@ const handleGetSrcFields = async () => {
}
// 判断sql是否是查询语句
if (!/^select/i.test(state.form.dataSql!)) {
let msg = 'sql语句错误请输入查询语句';
if (!/^select/i.test(state.form.dataSql.trim()!)) {
let msg = 'sql语句错误请输入select语句';
ElMessage.warning(msg);
return;
}
@@ -410,10 +422,16 @@ const handleGetSrcFields = async () => {
}
// 执行sql
// oracle的分页关键字不一样
let limit = ' limit 1';
if (state.form.srcDbType === DbType.oracle) {
limit = ' where rownum <= 1';
}
const res = await dbApi.sqlExec.request({
id: state.form.srcDbId,
db: state.form.srcDbName,
sql: state.form.dataSql.trim() + ' limit 1',
sql: `select * from (${state.form.dataSql}) t ${limit}`,
});
if (!res.columns) {

View File

@@ -11,6 +11,7 @@ export const dbApi = {
tableInfos: Api.newGet('/dbs/{id}/t-infos'),
tableIndex: Api.newGet('/dbs/{id}/t-index'),
tableDdl: Api.newGet('/dbs/{id}/t-create-ddl'),
copyTable: Api.newPost('/dbs/{id}/copy-table'),
columnMetadata: Api.newGet('/dbs/{id}/c-metadata'),
pgSchemas: Api.newGet('/dbs/{id}/pg/schemas'),
// 获取表即列提示
@@ -48,16 +49,20 @@ export const dbApi = {
// 获取数据库备份列表
getDbBackups: Api.newGet('/dbs/{dbId}/backups'),
createDbBackup: Api.newPost('/dbs/{dbId}/backups'),
deleteDbBackup: Api.newDelete('/dbs/{dbId}/backups/{backupId}'),
getDbNamesWithoutBackup: Api.newGet('/dbs/{dbId}/db-names-without-backup'),
enableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/enable'),
disableDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/disable'),
startDbBackup: Api.newPut('/dbs/{dbId}/backups/{backupId}/start'),
saveDbBackup: Api.newPut('/dbs/{dbId}/backups/{id}'),
getDbBackupHistories: Api.newGet('/dbs/{dbId}/backup-histories'),
restoreDbBackupHistory: Api.newPost('/dbs/{dbId}/backup-histories/{backupHistoryId}/restore'),
deleteDbBackupHistory: Api.newDelete('/dbs/{dbId}/backup-histories/{backupHistoryId}'),
// 获取数据库备份列表
// 获取数据库恢复列表
getDbRestores: Api.newGet('/dbs/{dbId}/restores'),
createDbRestore: Api.newPost('/dbs/{dbId}/restores'),
deleteDbRestore: Api.newDelete('/dbs/{dbId}/restores/{restoreId}'),
getDbNamesWithoutRestore: Api.newGet('/dbs/{dbId}/db-names-without-restore'),
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),

View File

@@ -6,6 +6,9 @@
:resource-type="TagResourceTypeEnum.Db.value"
:tag-path-node-type="NodeTypeTagPath"
>
<template #iconPrefix>
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
</template>
<template #prefix="{ data }">
<SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
@@ -19,7 +22,7 @@ import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
import { dbApi } from '@/views/ops/db/api';
import { sleep } from '@/common/utils/loading';
import SvgIcon from '@/components/svgIcon/index.vue';
import { DbType, getDbDialect } from '@/views/ops/db/dialect';
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
import { computed } from 'vue';
@@ -27,15 +30,21 @@ const props = defineProps({
dbId: {
type: Number,
},
instName: {
type: String,
},
dbName: {
type: String,
},
tagPath: {
type: String,
},
dbType: {
type: String,
},
});
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:dbId', 'selectDb']);
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:instName', 'update:dbId', 'update:dbType', 'selectDb']);
/**
* 树节点类型
@@ -53,7 +62,7 @@ class SqlExecNodeType {
const selectNode = computed({
get: () => {
return props.dbName ? `${props.tagPath} - ${props.dbId} - ${props.dbName}` : '';
return props.dbName ? `${props.tagPath} > ${props.instName} > ${props.dbName}` : '';
},
set: () => {
//
@@ -87,8 +96,8 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
});
/** mysql类型的数据库没有schema层 */
const mysqlType = (type: string) => {
return type === DbType.mysql;
const noSchemaType = (type: string) => {
return noSchemaTypes.includes(type);
};
// 数据库实例节点类型
@@ -96,7 +105,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
const params = parentNode.params;
const dbs = params.database.split(' ')?.sort();
let fn: NodeType;
if (mysqlType(params.type)) {
if (noSchemaType(params.type)) {
fn = MysqlNodeTypes;
} else {
fn = PgNodeTypes;
@@ -114,7 +123,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
db: x,
})
.withIcon(DbIcon);
if (mysqlType(params.type)) {
if (noSchemaType(params.type)) {
tagTreeNode.isLeaf = true;
}
return tagTreeNode;
@@ -148,8 +157,10 @@ const changeNode = (nodeData: TagTreeNode) => {
const params = nodeData.params;
// postgres
emits('update:dbName', params.db);
emits('update:instName', params.name);
emits('update:dbId', params.id);
emits('update:tagPath', params.tagPath);
emits('update:dbType', params.type);
emits('selectDb', params);
};
</script>

View File

@@ -128,12 +128,12 @@
</template>
<script lang="ts" setup>
import { h, nextTick, onMounted, reactive, toRefs, ref, unref } from 'vue';
import { h, nextTick, onMounted, reactive, ref, toRefs, unref } from 'vue';
import { getToken } from '@/common/utils/storage';
import { notBlank } from '@/common/assert';
import { format as sqlFormatter } from 'sql-formatter';
import config from '@/common/config';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { editor } from 'monaco-editor';
@@ -146,11 +146,9 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { joinClientParams } from '@/common/request';
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
import { ElNotification } from 'element-plus';
import syssocket from '@/common/syssocket';
import SvgIcon from '@/components/svgIcon/index.vue';
import { getDbDialect } from '../../dialect';
import { Splitpanes, Pane } from 'splitpanes';
import { Pane, Splitpanes } from 'splitpanes';
const emits = defineEmits(['saveSqlSuccess']);
@@ -357,6 +355,7 @@ const onRunSql = async (newTab = false) => {
const colAndData: any = data.value;
if (!colAndData.res || colAndData.res.length === 0) {
ElMessage.warning('未查询到结果集');
return;
}
// 要实时响应,故需要用索引改变数据才生效
@@ -453,7 +452,7 @@ const formatSql = () => {
return;
}
const formatDialect = getDbDialect(getNowDbInst().type).getInfo().formatSqlDialect;
const formatDialect = getNowDbInst().getDialect().getInfo().formatSqlDialect;
let sql = monacoEditor.getModel()?.getValueInRange(selection);
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容

View File

@@ -3,9 +3,9 @@
<el-input
v-if="dataType == DataType.String"
:ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@blur="handleBlur"
:class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`"
input-style="text-align: center; height: 26px;"
size="small"
v-model="itemValue"
:placeholder="placeholder"
@@ -16,9 +16,9 @@
<el-input
v-else-if="dataType == DataType.Number"
:ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@blur="handleBlur"
class="w100 mb4"
input-style="text-align: center; height: 26px;"
size="small"
v-model.number="itemValue"
:placeholder="placeholder"
@@ -28,6 +28,7 @@
<el-date-picker
v-else-if="dataType == DataType.Date"
:ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@change="emit('blur')"
@blur="handleBlur"
class="edit-time-picker mb4"
@@ -43,6 +44,7 @@
<el-date-picker
v-else-if="dataType == DataType.DateTime"
:ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@change="handleBlur"
@blur="handleBlur"
class="edit-time-picker mb4"
@@ -58,6 +60,7 @@
<el-time-picker
v-else-if="dataType == DataType.Time"
:ref="(el: any) => focus && el?.focus()"
:disabled="disabled"
@change="handleBlur"
@blur="handleBlur"
class="edit-time-picker mb4"
@@ -71,7 +74,7 @@
</template>
<script lang="ts" setup>
import { Ref, ref, computed } from 'vue';
import { computed, ref, Ref } from 'vue';
import { ElInput } from 'element-plus';
import { DataType } from '../../dialect/index';
import SvgIcon from '@/components/svgIcon/index.vue';
@@ -83,11 +86,13 @@ export interface ColumnFormItemProps {
focus?: boolean; // 是否获取焦点
placeholder?: string;
columnName?: string;
disabled?: boolean;
}
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
focus: false,
dataType: DataType.String,
disabled: false,
});
const emit = defineEmits(['update:modelValue', 'blur']);
@@ -178,9 +183,6 @@ const getEditorLangByValue = (value: any) => {
.el-input__prefix {
display: none;
}
.el-input__inner {
text-align: center;
}
}
.edit-time-picker-popper {

View File

@@ -46,14 +46,6 @@
<b :title="column.remark" class="el-text" style="cursor: pointer">
{{ column.title }}
</b>
<span>
<SvgIcon
color="var(--el-color-primary)"
v-if="column.title == nowSortColumn?.columnName"
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
></SvgIcon>
</span>
</div>
<!-- 字段备注信息 -->
@@ -71,6 +63,13 @@
{{ column.title }}
</b>
</div>
<!-- 字段列右部分内容 -->
<div class="column-right">
<span v-if="column.title == nowSortColumn?.columnName">
<SvgIcon color="var(--el-color-primary)" :name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"></SvgIcon>
</span>
</div>
</div>
</div>
</div>
@@ -138,13 +137,25 @@
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
</el-dialog>
<DbTableDataForm
v-if="state.tableDataFormDialog.visible"
:db-inst="getNowDbInst()"
:db-name="db"
:columns="columns!"
:title="state.tableDataFormDialog.title"
:table-name="table"
v-model:visible="state.tableDataFormDialog.visible"
v-model="state.tableDataFormDialog.data"
@submit-success="emits('changeUpdatedField')"
/>
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
</div>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { ElInput } from 'element-plus';
import { ElInput, ElMessage } from 'element-plus';
import { copyToClipboard } from '@/common/utils/string';
import { DbInst } from '@/views/ops/db/db';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
@@ -154,6 +165,7 @@ import { dateStrFormat } from '@/common/utils/date';
import { useIntervalFn, useStorage } from '@vueuse/core';
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
import ColumnFormItem from './ColumnFormItem.vue';
import DbTableDataForm from './DbTableDataForm.vue';
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
@@ -247,6 +259,13 @@ const cmDataDel = new ContextmenuItem('deleteData', '删除')
return state.table == '';
});
const cmDataEdit = new ContextmenuItem('editData', '编辑行')
.withIcon('edit')
.withOnClick(() => onEditRowData())
.withHideFunc(() => {
return state.table == '';
});
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
.withIcon('tickets')
.withOnClick(() => onGenerateInsertSql())
@@ -333,7 +352,11 @@ const state = reactive({
},
items: [] as ContextmenuItem[],
},
tableDataFormDialog: {
data: {},
title: '',
visible: false,
},
genTxtDialog: {
title: 'SQL',
visible: false,
@@ -444,7 +467,7 @@ const formatDataValues = (datas: any) => {
};
const setTableData = (datas: any) => {
tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
tableRef.value?.scrollTo({ scrollLeft: 0, scrollTop: 0 });
selectionRowsMap.clear();
cellUpdateMap.clear();
formatDataValues(datas);
@@ -576,7 +599,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataEdit, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
contextmenuRef.value.openContextmenu({ column, rowData: data });
};
@@ -601,6 +624,18 @@ const onDeleteData = async () => {
});
};
const onEditRowData = () => {
const selectionDatas = Array.from(selectionRowsMap.values());
if (selectionDatas.length > 1) {
ElMessage.warning('只能编辑一行数据');
return;
}
const data = selectionDatas[0];
state.tableDataFormDialog.data = data;
state.tableDataFormDialog.title = `编辑表'${props.table}'数据`;
state.tableDataFormDialog.visible = true;
};
const onGenerateInsertSql = async () => {
const selectionDatas = Array.from(selectionRowsMap.values());
state.genTxtDialog.txt = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
@@ -714,36 +749,21 @@ const submitUpdateFields = async () => {
const db = state.db;
let res = '';
const dbDialect = getDbDialect(dbInst.type);
for (let updateRow of cellUpdateMap.values()) {
let sql = `UPDATE ${dbInst.wrapName(state.table)} SET `;
const rowData = updateRow.rowData;
// 主键列信息
const primaryKey = await dbInst.loadTableColumn(db, state.table);
let primaryKeyType = primaryKey.columnType;
let primaryKeyName = primaryKey.columnName;
let primaryKeyValue = rowData[primaryKeyName];
const rowData = { ...updateRow.rowData };
let updateColumnValue = {};
for (let k of updateRow.columnsMap.keys()) {
const v = updateRow.columnsMap.get(k);
if (!v) {
continue;
}
// 更新字段列信息
const updateColumn = await dbInst.loadTableColumn(db, state.table, k);
sql += ` ${dbInst.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, rowData[k], dbDialect)},`;
// 如果修改的字段是主键
if (k === primaryKeyName) {
primaryKeyValue = v.oldValue;
}
updateColumnValue[k] = rowData[k];
// 将更新的字段对应的原始数据还原(主要应对可能更新修改了主键等)
rowData[k] = v.oldValue;
}
sql = sql.substring(0, sql.length - 1);
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`;
res += sql;
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
}
dbInst.promptExeSql(
@@ -868,9 +888,15 @@ defineExpose({
color: var(--el-color-info-light-3);
font-weight: bold;
position: absolute;
top: -7px;
top: -5px;
padding: 2px;
}
.column-right {
position: absolute;
top: 2px;
right: 0;
padding: 2px;
height: 12px;
}
}
</style>

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"
></db-table-data>
<el-row type="flex" class="mt5" justify="center">
<el-pagination
small
:total="count"
@size-change="handleSizeChange"
@current-change="pageChange()"
layout="prev, pager, next, total, sizes, jumper"
v-model:current-page="pageNum"
v-model:page-size="pageSize"
:page-sizes="pageSizes"
></el-pagination>
<el-row type="flex" class="mt5" :gutter="10" justify="space-between" style="user-select: none">
<el-col :span="12">
<el-text
id="copyValue"
style="color: var(--el-color-info-light-3)"
class="is-truncated font12 mt5"
@click="copyToClipboard(sql)"
:title="sql"
>{{ sql }}</el-text
>
</el-col>
<el-col :span="12">
<el-row :gutter="10" justify="left">
<el-link class="op-page" :underline="false" @click="pageNum = 1" :disabled="pageNum == 1" icon="DArrowLeft" title="首页" />
<el-link class="op-page" :underline="false" @click="pageNum = --pageNum || 1" :disabled="pageNum == 1" icon="Back" title="上一页" />
<div class="op-page">
<el-input-number
style="width: 50px"
:controls="false"
:min="1"
v-model="state.setPageNum"
size="small"
@blur="handleSetPageNum"
@keydown.enter="handleSetPageNum"
/>
</div>
<el-link class="op-page" :underline="false" @click="++pageNum" :disabled="datas.length < pageSize" icon="Right" />
<el-link class="op-page" :underline="false" @click="handleEndPage" :disabled="datas.length < pageSize" icon="DArrowRight" />
<div style="width: 90px" class="op-page ml10">
<el-select size="small" :default-first-option="true" v-model="pageSize" @change="handleSizeChange">
<el-option
style="font-size: 12px; height: 24px; line-height: 24px"
v-for="(op, i) in pageSizes"
:key="i"
:label="op + '条/页'"
:value="op"
/>
</el-select>
</div>
<el-button @click="handleCount" :loading="state.counting" class="ml10" text bg size="small">
{{ state.showTotal ? `${state.total} 条` : 'count' }}
</el-button>
</el-row>
</el-col>
</el-row>
<div style="font-size: 12px; padding: 0 10px; color: #606266">
<span>{{ state.sql }}</span>
</div>
<el-dialog v-model="conditionDialog.visible" :title="conditionDialog.title" width="420px">
<el-row>
@@ -203,31 +234,16 @@
</template>
</el-dialog>
<el-dialog v-model="addDataDialog.visible" :title="addDataDialog.title" :destroy-on-close="true" width="600px">
<el-form ref="dataForm" :model="addDataDialog.data" :show-message="false" label-width="auto" size="small">
<el-form-item
v-for="column in columns"
:key="column.columnName"
class="w100 mb5"
:prop="column.columnName"
:label="column.columnName"
:required="column.nullable != 'YES' && column.columnKey != 'PRI'"
>
<ColumnFormItem
v-model="addDataDialog.data[`${column.columnName}`]"
:data-type="dbDialect.getDataType(column.columnType)"
:placeholder="`${column.columnType} ${column.columnComment}`"
:column-name="column.columnName"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeAddDataDialog">取消</el-button>
<el-button type="primary" @click="addRow">确定</el-button>
</span>
</template>
</el-dialog>
<DbTableDataForm
:db-inst="getNowDbInst()"
:db-name="dbName"
:columns="columns"
:title="addDataDialog.title"
:table-name="tableName"
v-model:visible="addDataDialog.visible"
v-model="addDataDialog.data"
@submit-success="onRefresh"
/>
</div>
</template>
@@ -237,10 +253,11 @@ import { ElMessage } from 'element-plus';
import { DbInst } from '@/views/ops/db/db';
import DbTableData from './DbTableData.vue';
import { DbDialect, getDbDialect } from '@/views/ops/db/dialect';
import { DbDialect } from '@/views/ops/db/dialect';
import SvgIcon from '@/components/svgIcon/index.vue';
import ColumnFormItem from './ColumnFormItem.vue';
import { useEventListener, useStorage } from '@vueuse/core';
import { copyToClipboard } from '@/common/utils/string';
import DbTableDataForm from './DbTableDataForm.vue';
const props = defineProps({
dbId: {
@@ -261,7 +278,6 @@ const props = defineProps({
},
});
const dataForm: any = ref(null);
const dbTableRef: Ref = ref(null);
const condInputRef: Ref = ref(null);
const columnNameSearchInputRef: Ref = ref(null);
@@ -289,7 +305,10 @@ const state = reactive({
defaultPageSize * 40,
defaultPageSize * 80,
],
count: 0,
setPageNum: 0,
total: 0,
showTotal: false,
counting: false,
selectionDatas: [] as any,
condPopVisible: false,
columnNameSearch: '',
@@ -305,7 +324,6 @@ const state = reactive({
addDataDialog: {
data: {},
title: '',
placeholder: '',
visible: false,
},
tableHeight: '600px',
@@ -313,7 +331,7 @@ const state = reactive({
dbDialect: {} as DbDialect,
});
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, count, hasUpdatedFileds, conditionDialog, addDataDialog, dbDialect } = toRefs(state);
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } = toRefs(state);
watch(
() => props.tableHeight,
@@ -331,7 +349,7 @@ onMounted(async () => {
state.tableHeight = props.tableHeight;
await onRefresh();
state.dbDialect = getDbDialect(getNowDbInst().type);
state.dbDialect = getNowDbInst().getDialect();
useEventListener('click', handlerWindowClick);
});
@@ -346,18 +364,19 @@ const onRefresh = async () => {
await selectData();
};
/**
* 数据tab修改页数
*/
const pageChange = async () => {
await selectData();
};
watch(
() => state.pageNum,
async () => {
await selectData();
}
);
/**
* 单表数据信息查询数据
*/
const selectData = async () => {
state.loading = true;
state.setPageNum = state.pageNum;
const dbInst = getNowDbInst();
const db = props.dbName;
const table = props.tableName;
@@ -370,16 +389,10 @@ const selectData = async () => {
state.columns = columns;
}
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
state.count = countRes.res[0].count || countRes.res[0].COUNT || 0;
let sql = dbInst.getDefaultSelectSql(table, state.condition, state.orderBy, state.pageNum, state.pageSize);
let sql = dbInst.getDefaultSelectSql(db, table, state.condition, state.orderBy, state.pageNum, state.pageSize);
state.sql = sql;
if (state.count > 0) {
const colAndData: any = await dbInst.runSql(db, sql);
state.datas = colAndData.res;
} else {
state.datas = [];
}
const colAndData: any = await dbInst.runSql(db, sql);
state.datas = colAndData.res;
} finally {
state.loading = false;
}
@@ -391,6 +404,33 @@ const handleSizeChange = async (size: any) => {
await selectData();
};
const handleEndPage = async () => {
await handleCount();
state.pageNum = Math.ceil(state.total / state.pageSize);
await selectData();
};
const handleSetPageNum = async () => {
state.pageNum = state.setPageNum;
await selectData();
};
const handleCount = async () => {
state.counting = true;
try {
const db = props.dbName;
const table = props.tableName;
const dbInst = getNowDbInst();
const countRes = await dbInst.runSql(db, dbInst.getDefaultCountSql(table, state.condition));
state.total = parseInt(countRes.res[0].count || countRes.res[0].COUNT || 0);
state.showTotal = true;
} catch (e) {
/* empty */
}
state.counting = false;
};
// 完整的条件,每次选中后会重置条件框内容,故需要这个变量在获取建议时将文本框内容保存
let completeCond = '';
// 是否存在列建议
@@ -543,40 +583,10 @@ const onShowAddDataDialog = async () => {
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
state.addDataDialog.visible = true;
};
const closeAddDataDialog = () => {
state.addDataDialog.visible = false;
state.addDataDialog.data = {};
};
// 添加新数据行
const addRow = async () => {
dataForm.value.validate(async (valid: boolean) => {
if (valid) {
const dbInst = getNowDbInst();
const data = state.addDataDialog.data;
// key: 字段名value: 字段名提示
let obj: any = {};
for (let item of state.columns) {
const value = data[item.columnName];
if (!value) {
continue;
}
obj[`${dbInst.wrapName(item.columnName)}`] = DbInst.wrapValueByType(value);
}
let columnNames = Object.keys(obj).join(',');
let values = Object.values(obj).join(',');
let sql = `INSERT INTO ${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
dbInst.promptExeSql(props.dbName, sql, null, () => {
closeAddDataDialog();
onRefresh();
});
} else {
ElMessage.error('请正确填写数据信息');
return false;
}
});
};
</script>
<style lang="scss"></style>
<style lang="scss">
.op-page {
margin-left: 5px;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="90%" :close-on-press-escape="false" :close-on-click-modal="false">
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" width="70%" :close-on-press-escape="false" :close-on-click-modal="false">
<el-form label-position="left" ref="formRef" :model="tableData" label-width="80px">
<el-row>
<el-col :span="12">
@@ -26,7 +26,7 @@
:width="item.width"
>
<template #default="scope">
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name"> </el-input>
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name" />
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
<el-option
@@ -42,35 +42,30 @@
</el-option>
</el-select>
<el-input v-else-if="item.prop === 'value'" size="small" v-model="scope.row.value"> </el-input>
<el-input v-else-if="item.prop === 'value'" size="small" v-model="scope.row.value" />
<el-input v-else-if="item.prop === 'length'" size="small" v-model="scope.row.length"> </el-input>
<el-input v-else-if="item.prop === 'length'" type="number" size="small" v-model.number="scope.row.length" />
<el-input v-else-if="item.prop === 'numScale'" size="small" v-model="scope.row.numScale"> </el-input>
<el-input v-else-if="item.prop === 'numScale'" type="number" size="small" v-model.number="scope.row.numScale" />
<el-checkbox v-else-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull"> </el-checkbox>
<el-checkbox v-else-if="item.prop === 'notNull'" size="small" v-model="scope.row.notNull" />
<el-checkbox v-else-if="item.prop === 'pri'" size="small" v-model="scope.row.pri"> </el-checkbox>
<el-checkbox v-else-if="item.prop === 'pri'" size="small" v-model="scope.row.pri" />
<el-checkbox
v-else-if="item.prop === 'auto_increment'"
size="small"
v-model="scope.row.auto_increment"
:disabled="dbType === DbType.postgresql"
>
</el-checkbox>
:disabled="disableEditIncr()"
/>
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark"> </el-input>
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark" />
<el-link
v-else-if="item.prop === 'action'"
type="danger"
plain
size="small"
:underline="false"
@click.prevent="deleteRow(scope.$index)"
>删除</el-link
>
<el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteRow(scope.$index)">
<template #reference>
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
@@ -104,21 +99,15 @@
<el-checkbox v-if="item.prop === 'unique'" size="small" v-model="scope.row.unique" @change="indexChanges(scope.row)">
</el-checkbox>
<el-select v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType">
<el-option v-for="typeValue in indexTypeList" :key="typeValue" :value="typeValue">{{ typeValue }}</el-option>
</el-select>
<el-input v-if="item.prop === 'indexType'" disabled size="small" v-model="scope.row.indexType" />
<el-input v-if="item.prop === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input>
<el-link
v-if="item.prop === 'action'"
type="danger"
plain
size="small"
:underline="false"
@click.prevent="deleteIndex(scope.$index)"
>删除</el-link
>
<el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteIndex(scope.$index)">
<template #reference>
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
@@ -130,6 +119,7 @@
</el-tabs>
</el-form>
<template #footer>
<el-button @click="cancel()">取消</el-button>
<el-button :loading="btnloading" @click="submit()" type="primary">保存</el-button>
</template>
</el-dialog>
@@ -166,7 +156,7 @@ const props = defineProps({
//定义事件
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
const dbDialect = getDbDialect(props.dbType);
let dbDialect = getDbDialect(props.dbType);
type ColName = {
prop: string;
@@ -180,29 +170,33 @@ const state = reactive({
btnloading: false,
activeName: '1',
columnTypeList: dbDialect.getInfo().columnTypes,
indexTypeList: ['BTREE', 'NORMAL'], // mysql索引类型详解 http://c.biancheng.net/view/7897.html
tableData: {
fields: {
colNames: [
{
prop: 'name',
label: '字段名称',
width: 200,
},
{
prop: 'type',
label: '字段类型',
width: 120,
},
{
prop: 'length',
label: '长度',
width: 120,
},
{
prop: 'numScale',
label: '小数点',
width: 120,
},
{
prop: 'value',
label: '默认值',
width: 120,
},
{
@@ -231,6 +225,7 @@ const state = reactive({
},
] as ColName[],
res: [] as RowDefinition[],
oldFields: [] as RowDefinition[],
},
indexs: {
colNames: [
@@ -261,19 +256,34 @@ const state = reactive({
],
columns: [{ name: '', remark: '' }],
res: [] as IndexDefinition[],
oldIndexs: [] as IndexDefinition[],
},
tableName: '',
tableComment: '',
height: 450,
db: '',
},
});
const { dialogVisible, btnloading, activeName, indexTypeList, tableData } = toRefs(state);
const { dialogVisible, btnloading, activeName, tableData } = toRefs(state);
watch(props, async (newValue) => {
state.dialogVisible = newValue.visible;
dbDialect = getDbDialect(newValue.dbType);
});
// 切换到索引tab时刷新索引字段下拉选项
watch(
() => state.activeName,
(newValue) => {
if (newValue === '2') {
state.tableData.indexs.columns = state.tableData.fields.res.map((a) => {
return { name: a.name, remark: a.remark };
});
}
}
);
const cancel = () => {
emit('update:visible', false);
reset();
@@ -359,7 +369,10 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
nowArr.forEach((a) => {
let k = a[key];
newMap[k] = a;
if (!oldMap.hasOwnProperty(k)) {
// 取oldName因为修改了name但是oldName不会变
let oldName = a['oldName'];
oldName && (newMap[oldName] = a);
if (!oldMap.hasOwnProperty(k) && (!oldName || (oldName && !oldMap.hasOwnProperty(oldName)))) {
// 新增
data.add.push(a);
}
@@ -376,7 +389,7 @@ const filterChangedData = (oldArr: object[], nowArr: object[], key: string): { d
for (let f in a) {
let oldV = a[f];
let newV = newData[f];
if (oldV.toString() !== newV.toString()) {
if (oldV?.toString() !== newV?.toString()) {
data.upd.push(newData);
break;
}
@@ -390,22 +403,22 @@ const genSql = () => {
let data = state.tableData;
// 创建表
if (!props.data?.edit) {
if (state.activeName === '1') {
return dbDialect.getCreateTableSql(data);
} else if (state.activeName === '2' && data.indexs.res.length > 0) {
return dbDialect.getCreateIndexSql(data);
let createTable = dbDialect.getCreateTableSql(data);
let createIndex = '';
if (data.indexs.res.length > 0) {
createIndex = dbDialect.getCreateIndexSql(data);
}
return createTable + ';' + createIndex;
} else {
// 修改
if (state.activeName === '1') {
// 修改列
let changeData = filterChangedData(oldData.fields, state.tableData.fields.res, 'name');
return dbDialect.getModifyColumnSql(data.tableName, changeData);
} else if (state.activeName === '2') {
// 修改索引
let changeData = filterChangedData(oldData.indexs, state.tableData.indexs.res, 'indexName');
return dbDialect.getModifyIndexSql(data.tableName, changeData);
}
// 修改
let changeColData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
let colSql = dbDialect.getModifyColumnSql(data, data.tableName, changeColData);
// 修改索引
let changeIdxData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
let idxSql = dbDialect.getModifyIndexSql(data, data.tableName, changeIdxData);
// 修改表名
return colSql + ';' + idxSql;
}
};
@@ -414,28 +427,8 @@ const reset = () => {
formRef.value.resetFields();
state.tableData.tableName = '';
state.tableData.tableComment = '';
state.tableData.fields.res = [
{
name: '',
type: '',
value: '',
length: '',
numScale: '',
notNull: false,
pri: false,
auto_increment: false,
remark: '',
},
];
state.tableData.indexs.res = [
{
indexName: '',
columnNames: [],
unique: false,
indexType: 'BTREE',
indexComment: '',
},
];
state.tableData.fields.res = [];
state.tableData.indexs.res = [];
};
const indexChanges = (row: any) => {
@@ -456,7 +449,21 @@ const indexChanges = (row: any) => {
row.indexComment = `${tableData.value.tableName}表(${name.replaceAll('_', ',')})${commentSuffix}`;
};
const oldData = { indexs: [] as any[], fields: [] as RowDefinition[] };
const disableEditIncr = () => {
if (DbType.postgresql === props.dbType) {
return true;
}
// 如果是mssql则不能修改自增
if (props.data?.edit) {
if (DbType.mssql === props.dbType) {
return true;
}
}
return false;
};
watch(
() => props.data,
(newValue: any) => {
@@ -464,9 +471,10 @@ watch(
// 回显表名表注释
state.tableData.tableName = row.tableName;
state.tableData.tableComment = row.tableComment;
state.tableData.db = props.db!;
// 回显列
if (columns && Array.isArray(columns) && columns.length > 0) {
oldData.fields = [];
state.tableData.fields.oldFields = [];
state.tableData.fields.res = [];
// 索引列下拉选
state.tableData.indexs.columns = [];
@@ -474,26 +482,33 @@ watch(
let typeObj = a.columnType.replace(')', '').split('(');
let type = typeObj[0];
let length = (typeObj.length > 1 && typeObj[1]) || '';
let defaultValue = '';
if (a.columnDefault) {
defaultValue = a.columnDefault.trim().replace(/^'|'$/g, '');
// 解决高斯的默认值问题
defaultValue = defaultValue.replace("'::character varying", '');
}
let data = {
name: a.columnName,
oldName: a.columnName,
type,
value: a.columnDefault || '',
value: defaultValue,
length,
numScale: a.numScale,
notNull: a.nullable !== 'YES',
pri: a.columnKey === 'PRI',
auto_increment: a.columnKey === 'PRI' /*a.extra?.indexOf('auto_increment') > -1*/,
pri: a.isPrimaryKey,
auto_increment: a.isIdentity /*a.extra?.indexOf('auto_increment') > -1*/,
remark: a.columnComment,
};
state.tableData.fields.res.push(data);
oldData.fields.push(JSON.parse(JSON.stringify(data)));
state.tableData.fields.oldFields.push(JSON.parse(JSON.stringify(data)));
// 索引字段下拉选项
state.tableData.indexs.columns.push({ name: a.columnName, remark: a.columnComment });
});
}
// 回显索引
if (indexs && Array.isArray(indexs) && indexs.length > 0) {
oldData.indexs = [];
state.tableData.indexs.oldIndexs = [];
state.tableData.indexs.res = [];
// 索引过滤掉主键
indexs
@@ -502,12 +517,12 @@ watch(
let data = {
indexName: a.indexName,
columnNames: a.columnName?.split(','),
unique: a.nonUnique === 0 || false,
unique: a.isUnique || false,
indexType: a.indexType,
indexComment: a.indexComment,
};
state.tableData.indexs.res.push(data);
oldData.indexs.push(JSON.parse(JSON.stringify(data)));
state.tableData.indexs.oldIndexs.push(JSON.parse(JSON.stringify(data)));
});
}
}

View File

@@ -68,9 +68,7 @@
<template #default="scope">
<el-link @click.prevent="showColumns(scope.row)" type="primary">字段</el-link>
<el-link class="ml5" @click.prevent="showTableIndex(scope.row)" type="success">索引</el-link>
<el-link class="ml5" v-if="tableCreateDialog.enableEditTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning"
>编辑表</el-link
>
<el-link class="ml5" v-if="editDbTypes.indexOf(dbType) > -1" @click.prevent="openEditTable(scope.row)" type="warning">编辑表</el-link>
<el-link class="ml5" @click.prevent="showCreateDdl(scope.row)" type="info">DDL</el-link>
</template>
</el-table-column>
@@ -127,7 +125,7 @@ import SqlExecBox from '../sqleditor/SqlExecBox';
import config from '@/common/config';
import { joinClientParams } from '@/common/request';
import { isTrue } from '@/common/assert';
import { compatibleMysql, DbType } from '../../dialect/index';
import { compatibleMysql, DbType, editDbTypes } from '../../dialect/index';
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
@@ -181,7 +179,6 @@ const state = reactive({
visible: false,
activeName: '1',
type: '',
enableEditTypes: [DbType.mysql, DbType.mariadb, DbType.postgresql, DbType.dm, DbType.oracle], // 支持"编辑表"的数据库类型
data: {
// 修改表时,传递修改数据
edit: false,

View File

@@ -7,6 +7,10 @@ import { editor, languages, Position } from 'monaco-editor';
import { registerCompletionItemProvider } from '@/components/monaco/completionItemProvider';
import { DbDialect, EditorCompletionItem, getDbDialect } from './dialect';
import { type RemovableRef, useLocalStorage } from '@vueuse/core';
const hintsStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-table-hints', new Map());
const tableStorage: RemovableRef<Map<string, any>> = useLocalStorage('db-tables', new Map());
const dbInstCache: Map<number, DbInst> = new Map();
@@ -58,17 +62,23 @@ export class DbInst {
if (!dbName) {
throw new Error('dbName不能为空');
}
let db = this.dbs.get(dbName);
let key = `${this.id}_${dbName}`;
let db = this.dbs.get(key);
if (db) {
return db;
}
console.info(`new db -> dbId: ${this.id}, dbName: ${dbName}`);
db = new Db();
db.name = dbName;
this.dbs.set(dbName, db);
this.dbs.set(key, db);
return db;
}
// 获取数据库实例方言
getDialect(): DbDialect {
return getDbDialect(this.type);
}
/**
* 加载数据库表信息
* @param dbName 数据库名
@@ -77,17 +87,22 @@ export class DbInst {
*/
async loadTables(dbName: string, reload?: boolean) {
const db = this.getDb(dbName);
// 优先从 table map中获取
let tables = db.tables;
let key = this.dbTablesKey(dbName);
let tables = tableStorage.value.get(key);
// 优先从 table 缓存中获取
if (!reload && tables) {
db.tables = tables;
return tables;
}
// 重置列信息缓存与表提示信息
db.columnsMap?.clear();
db.tableHints = null;
console.log(`load tables -> dbName: ${dbName}`);
tables = await dbApi.tableInfos.request({ id: this.id, db: dbName });
tableStorage.value.set(key, tables);
db.tables = tables;
// 异步加载表提示信息
this.loadDbHints(dbName, true).then(() => {});
return tables;
}
@@ -169,18 +184,30 @@ export class DbInst {
return this.getDb(dbName).getColumn(table, columnName);
}
dbTableHintsKey(dbName: string) {
return `db-table-hints_${this.id}_${dbName}`;
}
dbTablesKey(dbName: string) {
return `db-tables_${this.id}_${dbName}`;
}
/**
* 获取库信息提示
*/
async loadDbHints(dbName: string) {
async loadDbHints(dbName: string, reload?: boolean) {
const db = this.getDb(dbName);
if (db.tableHints) {
return db.tableHints;
let key = this.dbTableHintsKey(dbName);
let hints = hintsStorage.value.get(key);
if (!reload && hints) {
db.tableHints = hints;
return hints;
}
console.log(`load db-hits -> dbName: ${dbName}`);
const hits = await dbApi.hintTables.request({ id: this.id, db: db.name });
db.tableHints = hits;
return hits;
hints = await dbApi.hintTables.request({ id: this.id, db: db.name });
db.tableHints = hints;
hintsStorage.value.set(key, hints);
return hints;
}
/**
@@ -225,8 +252,8 @@ export class DbInst {
};
// 获取指定表的默认查询sql
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
return getDbDialect(this.type).getDefaultSelectSql(table, condition, orderBy, pageNum, limit);
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number = DbInst.DefaultLimit) {
return getDbDialect(this.type).getDefaultSelectSql(db, table, condition, orderBy, pageNum, limit);
}
/**
@@ -235,7 +262,7 @@ export class DbInst {
* @param table 表名
* @param datas 要生成的数据
*/
async genInsertSql(dbName: string, table: string, datas: any[]) {
async genInsertSql(dbName: string, table: string, datas: any[], skipNull = false) {
if (!datas) {
return '';
}
@@ -247,6 +274,9 @@ export class DbInst {
let values = [];
for (let column of columns) {
const colName = column.columnName;
if (skipNull && data[colName] == null) {
continue;
}
colNames.push(this.wrapName(colName));
values.push(DbInst.wrapValueByType(data[colName]));
}
@@ -255,6 +285,38 @@ export class DbInst {
return sqls.join(';\n') + ';';
}
/**
* 生成根据主键更新语句
* @param dbName 数据库名
* @param table 表名
* @param columnValue 要更新的列以及对应的值 field->columnName; value->columnValue
* @param rowData 表的一行完整数据(需要获取主键信息)
*/
async genUpdateSql(dbName: string, table: string, columnValue: {}, rowData: {}) {
let schema = '';
let dbArr = dbName.split('/');
if (dbArr.length == 2) {
schema = this.wrapName(dbArr[1]) + '.';
}
let sql = `UPDATE ${schema}${this.wrapName(table)} SET `;
// 主键列信息
const primaryKey = await this.loadTableColumn(dbName, table);
let primaryKeyType = primaryKey.columnType;
let primaryKeyName = primaryKey.columnName;
let primaryKeyValue = rowData[primaryKeyName];
const dialect = this.getDialect();
for (let k of Object.keys(columnValue)) {
const v = columnValue[k];
// 更新字段列信息
const updateColumn = await this.loadTableColumn(dbName, table, k);
sql += ` ${this.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, v, dialect)},`;
}
sql = sql.substring(0, sql.length - 1);
return (sql += ` WHERE ${this.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`);
}
/**
* 生成根据主键删除的sql语句
* @param table 表名
@@ -275,6 +337,7 @@ export class DbInst {
sql,
dbId: this.id,
db,
dbType: this.getDialect().getInfo().formatSqlDialect,
runSuccessCallback: successFunc,
cancelCallback: cancelFunc,
});
@@ -287,7 +350,7 @@ export class DbInst {
* @returns
*/
wrapName = (name: string) => {
return getDbDialect(this.type).quoteIdentifier(name);
return this.getDialect().quoteIdentifier(name);
};
/**
@@ -363,7 +426,7 @@ export class DbInst {
return value;
}
if (!dbDialect) {
return `${value}`;
return `'${value}'`;
}
return dbDialect.wrapStrValue(columnType, value);
}
@@ -441,7 +504,7 @@ class Db {
getColumn(table: string, columnName: string = '') {
const cols = this.getColumns(table);
if (!columnName) {
const col = cols.find((c: any) => c.columnKey == 'PRI');
const col = cols.find((c: any) => c.isPrimaryKey);
return col || cols[0];
}
return cols.find((c: any) => c.columnName == columnName);

View File

@@ -54,6 +54,7 @@ const DM_TYPE_LIST: sqlColumnType[] = [
{ udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '100G-1' },
];
// 参考官方文档https://eco.dameng.com/document/dm/zh-cn/pm/function.html
const replaceFunctions: EditorCompletionItem[] = [
// 数值函数
{ label: 'ABS', insertText: 'ABS(n)', description: '求数值 n 的绝对值' },
@@ -365,21 +366,22 @@ class DMDialect implements DbDialect {
};
dmDialectInfo = {
name: 'DM',
icon: 'iconfont icon-db-dm',
defaultPort: 5236,
formatSqlDialect: 'postgresql',
formatSqlDialect: 'plsql',
columnTypes: DM_TYPE_LIST.sort((a, b) => a.udtName.localeCompare(b.udtName)),
editorCompletions,
};
return dmDialectInfo;
}
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
return `SELECT * FROM "${table}" ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(pageNum, limit)};`;
}
getPageSql(pageNum: number, limit: number) {
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit};`;
return ` OFFSET ${(pageNum - 1) * limit} LIMIT ${limit}`;
}
getDefaultRows(): RowDefinition[] {
@@ -500,7 +502,9 @@ class DMDialect implements DbDialect {
// 默认值
let defVal = this.getDefaultValueSql(cl);
let incr = cl.auto_increment ? 'IDENTITY' : '';
return ` "${cl.name}" ${cl.type}${length} ${incr} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
// 如果有原名以原名为准
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
return ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${incr} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
}
getCreateTableSql(data: any): string {
@@ -546,35 +550,78 @@ class DMDialect implements DbDialect {
return sql.join(';');
}
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let sql: string[] = [];
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let schemaArr = tableData.db.split('/');
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
let modifySql = '';
let dropSql = '';
let renameSql = '';
let commentSql = '';
// 主键字段
let priArr = new Set();
if (changeData.add.length > 0) {
changeData.add.forEach((a) => {
sql.push(`ALTER TABLE "${tableName}" add COLUMN ${this.genColumnBasicSql(a)}`);
modifySql += `ALTER TABLE ${dbTable} add COLUMN ${this.genColumnBasicSql(a)};`;
if (a.remark) {
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
commentSql += `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
}
if (a.pri) {
priArr.add(`"${a.name}"`);
}
});
}
if (changeData.upd.length > 0) {
changeData.upd.forEach((a) => {
sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`);
if (a.remark) {
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
let cmtSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}';`;
if (a.remark && a.oldName === a.name) {
commentSql += cmtSql;
}
// 修改了字段名
if (a.oldName !== a.name) {
renameSql += `ALTER TABLE ${dbTable} RENAME COLUMN ${this.quoteIdentifier(a.oldName!)} TO ${this.quoteIdentifier(a.name)};`;
if (a.remark) {
commentSql += cmtSql;
}
}
modifySql += `ALTER TABLE ${dbTable} MODIFY ${this.genColumnBasicSql(a)};`;
if (a.pri) {
priArr.add(`${this.quoteIdentifier(a.name)}`);
}
});
}
if (changeData.del.length > 0) {
changeData.del.forEach((a) => {
sql.push(`ALTER TABLE "${tableName}" DROP COLUMN ${a.name}`);
dropSql += `ALTER TABLE ${dbTable} DROP COLUMN ${a.name};`;
});
}
return sql.join(';');
// 编辑主键
let dropPkSql = '';
if (priArr.size > 0) {
let resPri = tableData.fields.res.filter((a: RowDefinition) => a.pri);
if (resPri) {
priArr.add(`${this.quoteIdentifier(resPri.name)}`);
}
// 如果有编辑主键字段,则删除主键,再添加主键
// 解析表字段中是否含有主键,有的话就删除主键
if (tableData.fields.oldFields.find((a: RowDefinition) => a.pri)) {
dropPkSql = `ALTER TABLE ${dbTable} DROP PRIMARY KEY;`;
}
}
let addPkSql = priArr.size > 0 ? `ALTER TABLE ${dbTable} ADD PRIMARY KEY (${Array.from(priArr).join(',')});` : '';
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
}
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
// 不能直接修改索引名或字段、需要先删后加
let dropIndexNames: string[] = [];
let addIndexs: any[] = [];

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 { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect';
import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_dialect';
import { SqliteDialect } from '@/views/ops/db/dialect/sqlite_dialect';
import { MssqlDialect } from '@/views/ops/db/dialect/mssql_dialect';
import { GaussDialect } from '@/views/ops/db/dialect/gauss_dialect';
import { KingbaseEsDialect } from '@/views/ops/db/dialect/kingbaseES_dialect';
import { VastbaseDialect } from '@/views/ops/db/dialect/vastbase_dialect';
export interface sqlColumnType {
udtName: string;
@@ -14,6 +19,7 @@ export interface sqlColumnType {
export interface RowDefinition {
name: string;
oldName?: string;
type: string;
value: string;
length: string;
@@ -78,6 +84,11 @@ export const ColumnTypeSubscript = {
// 数据库基础信息
export interface DialectInfo {
/**
* 数据库类型label
*/
name: string;
/**
* 图标
*/
@@ -108,10 +119,23 @@ export const DbType = {
mysql: 'mysql',
mariadb: 'mariadb',
postgresql: 'postgres',
gauss: 'gauss',
dm: 'dm', // 达梦
oracle: 'oracle',
sqlite: 'sqlite',
mssql: 'mssql', // ms sqlserver
kingbaseEs: 'kingbaseEs', // 人大金仓 pgsql模式 https://help.kingbase.com.cn/v8/index.html
vastbase: 'vastbase', // https://docs.vastdata.com.cn/zh/docs/VastbaseG100Ver2.2.5/doc/%E5%BC%80%E5%8F%91%E8%80%85%E6%8C%87%E5%8D%97/SQL%E5%8F%82%E8%80%83/SQL%E5%8F%82%E8%80%83.html
};
// mysql兼容的数据库
export const noSchemaTypes = [DbType.mysql, DbType.mariadb, DbType.sqlite];
// 有schema层的数据库
export const schemaDbTypes = [DbType.postgresql, DbType.gauss, DbType.dm, DbType.oracle, DbType.mssql, DbType.kingbaseEs, DbType.vastbase];
export const editDbTypes = [...noSchemaTypes, ...schemaDbTypes];
export const compatibleMysql = (dbType: string): boolean => {
switch (dbType) {
case DbType.mysql:
@@ -130,13 +154,14 @@ export interface DbDialect {
/**
* 获取默认查询sql
* @param db 数据库信息
* @param table 表名
* @param condition 条件
* @param orderBy 排序
* @param pageNum 页数
* @param limit 条数
*/
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number): string;
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number): string;
getPageSql(pageNum: number, limit: number): string;
@@ -164,47 +189,53 @@ export interface DbDialect {
/**
* 生成编辑列sql
* @param tableData 表数据,包含表名、列数据、索引数据
* @param tableName 表名
* @param changeData 改变信息
*/
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string;
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string;
/**
* 生成编辑索引sql
* @param tableData 表数据,包含表名、列数据、索引数据
* @param tableName 表名
* @param changeData 改变数据
*/
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string;
/** 通过数据库字段类型,返回基本数据类型 */
getDataType: (columnType: string) => DataType;
getDataType(columnType: string): DataType;
/** 包装字符串数据, 如oracle需要把date类型改为 to_date(str, 'yyyy-mm-dd hh24:mi:ss') */
wrapStrValue(columnType: string, value: string): string;
}
let mysqlDialect = new MysqlDialect();
let mariadbDialect = new MariadbDialect();
let postgresDialect = new PostgresqlDialect();
let dmDialect = new DMDialect();
let oracleDialect = new OracleDialect();
export const getDbDialect = (dbType: string | undefined): DbDialect => {
if (!dbType) {
return mysqlDialect;
}
switch (dbType) {
case DbType.mysql:
return mysqlDialect;
case DbType.mariadb:
return mariadbDialect;
case DbType.postgresql:
return postgresDialect;
case DbType.dm:
return dmDialect;
case DbType.oracle:
return oracleDialect;
default:
throw new Error('不支持的数据库');
}
let dbType2DialectMap: Map<string, DbDialect> = new Map();
export const registerDbDialect = (dbType: string, dd: DbDialect) => {
dbType2DialectMap.set(dbType, dd);
};
export const getDbDialectMap = () => {
return dbType2DialectMap;
};
export const getDbDialect = (dbType?: string): DbDialect => {
return dbType2DialectMap.get(dbType!) || mysqlDialect;
};
(function () {
console.log('init register db dialect');
registerDbDialect(DbType.mysql, mysqlDialect);
registerDbDialect(DbType.mariadb, new MariadbDialect());
registerDbDialect(DbType.postgresql, new PostgresqlDialect());
registerDbDialect(DbType.gauss, new GaussDialect());
registerDbDialect(DbType.dm, new DMDialect());
registerDbDialect(DbType.oracle, new OracleDialect());
registerDbDialect(DbType.sqlite, new SqliteDialect());
registerDbDialect(DbType.mssql, new MssqlDialect());
registerDbDialect(DbType.kingbaseEs, new KingbaseEsDialect());
registerDbDialect(DbType.vastbase, new VastbaseDialect());
})();

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;
Object.assign(mariadbDialectInfo, super.getInfo());
mariadbDialectInfo.name = 'MariaDB';
mariadbDialectInfo.icon = 'iconfont icon-mariadb';
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 };
// 参考官方文档https://dev.mysql.com/doc/refman/8.0/en/data-types.html
const MYSQL_TYPE_LIST = [
'bigint',
'binary',
@@ -31,6 +32,7 @@ const MYSQL_TYPE_LIST = [
'varchar',
];
// 参考官方文档https://dev.mysql.com/doc/refman/8.3/en/functions.html
const replaceFunctions: EditorCompletionItem[] = [
/** 字符串相关函数 */
{ label: 'CONCAT', insertText: 'CONCAT(str1,str2,...)', description: '多字符串合并' },
@@ -102,6 +104,7 @@ class MysqlDialect implements DbDialect {
};
mysqlDialectInfo = {
name: 'MySQL',
icon: 'iconfont icon-op-mysql',
defaultPort: 3306,
formatSqlDialect: 'mysql',
@@ -111,7 +114,7 @@ class MysqlDialect implements DbDialect {
return mysqlDialectInfo;
}
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
pageNum,
limit
@@ -193,7 +196,7 @@ class MysqlDialect implements DbDialect {
let defVal = val ? `DEFAULT ${val}` : '';
let length = cl.length ? `(${cl.length})` : '';
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
cl.auto_increment ? 'AUTO_INCREMENT' : ''
} ${defVal} ${onUpdate} comment '${cl.remark || ''}' `;
}
@@ -223,38 +226,34 @@ class MysqlDialect implements DbDialect {
return sql.substring(0, sql.length - 1) + ';';
}
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let addSql = '',
updSql = '',
delSql = '';
if (changeData.add.length > 0) {
addSql = `ALTER TABLE ${tableName}`;
changeData.add.forEach((a) => {
addSql += ` ADD ${this.genColumnBasicSql(a)},`;
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let sql = `ALTER TABLE ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)}`;
let arr = [] as string[];
if (changeData.del.length > 0) {
changeData.del.forEach((a) => {
arr.push(` DROP COLUMN ${this.quoteIdentifier(a.name)} `);
});
}
if (changeData.add.length > 0) {
changeData.add.forEach((a) => {
arr.push(` ADD COLUMN ${this.genColumnBasicSql(a)} `);
});
addSql = addSql.substring(0, addSql.length - 1);
addSql += ';';
}
if (changeData.upd.length > 0) {
updSql = `ALTER TABLE ${tableName}`;
let arr = [] as string[];
changeData.upd.forEach((a) => {
arr.push(` MODIFY ${this.genColumnBasicSql(a)}`);
if (a.name === a.oldName) {
arr.push(` MODIFY COLUMN ${this.genColumnBasicSql(a)} `);
} else {
arr.push(` CHANGE COLUMN ${this.quoteIdentifier(a.oldName!)} ${this.genColumnBasicSql(a)} `);
}
});
updSql += arr.join(',');
updSql += ';';
}
if (changeData.del.length > 0) {
changeData.del.forEach((a) => {
delSql += ` ALTER TABLE ${tableName} DROP COLUMN ${a.name}; `;
});
}
return addSql + updSql + delSql;
return sql + arr.join(',') + ';';
}
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
// 搜集修改和删除的索引添加到drop index xx
// 收集新增和修改的索引添加到ADD xx
// ALTER TABLE `test1`

View File

@@ -14,7 +14,7 @@ import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sq
export { OracleDialect, ORACLE_TYPE_LIST };
// 参考文档:https://eco.dameng.com/document/dm/zh-cn/sql-dev/dmpl-sql-datatype.html#%E5%AD%97%E7%AC%A6%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B
// 参考官方文档https://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements001.htm
const ORACLE_TYPE_LIST: sqlColumnType[] = [
// 字符数据类型
{ udtName: 'CHAR', dataType: 'CHAR', desc: '定长字符串,自动在末尾用空格补全,非unicode', space: '', range: '1 - 2000' },
@@ -50,6 +50,7 @@ const ORACLE_TYPE_LIST: sqlColumnType[] = [
{ udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '' },
];
// 参考官方文档https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions001.htm
const replaceFunctions: EditorCompletionItem[] = [
// 字符函数
{ label: 'ASCII', insertText: 'ASCII(x)', description: '返回字符X的ASCII码' },
@@ -91,7 +92,35 @@ const replaceFunctions: EditorCompletionItem[] = [
{ label: 'NVL2', insertText: 'NVL2(x,value1,value2)', description: '如果x非空返回value1否则返回value2' },
];
const addCustomKeywords = ['ROWNUM', 'DUAL'];
const addCustomKeywords: EditorCompletionItem[] = [
{
label: 'ROWNUM',
description: 'keyword',
insertText: 'ROWNUM',
},
{
label: 'DUAL',
description: 'keyword',
insertText: 'DUAL',
},
// 分页代码块
{
label: 'SELECT ROWNUM',
description: 'code block',
insertText: 'SELECT * from table_name where rownum <= 10',
},
{
label: 'SELECT PAGE',
description: 'code block',
insertText: ` SELECT * FROM
(
SELECT t.*, ROWNUM AS rn
FROM table_name t
WHERE ROWNUM <= 25
)
WHERE rn > 0 \n`,
},
];
let oracleDialectInfo: DialectInfo;
class OracleDialect implements DbDialect {
@@ -103,6 +132,7 @@ class OracleDialect implements DbDialect {
let { keywords, operators, builtinVariables } = sqlLanguage;
let functionNames = replaceFunctions.map((a) => a.label);
let excludeKeywords = new Set(functionNames.concat(operators));
excludeKeywords.add('SELECT');
let editorCompletions: EditorCompletion = {
keywords: keywords
@@ -117,21 +147,14 @@ class OracleDialect implements DbDialect {
})
)
)
.concat(
// 加上自定义的关键字
addCustomKeywords.map(
(a): EditorCompletionItem => ({
label: a,
description: 'keyword',
})
)
),
.concat(addCustomKeywords),
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
functions: replaceFunctions,
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
};
oracleDialectInfo = {
name: 'Oracle',
icon: 'iconfont icon-oracle',
defaultPort: 1521,
formatSqlDialect: 'plsql',
@@ -141,7 +164,7 @@ class OracleDialect implements DbDialect {
return oracleDialectInfo;
}
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
return `
SELECT *
FROM (
@@ -268,16 +291,22 @@ class OracleDialect implements DbDialect {
return '';
}
genColumnBasicSql(cl: RowDefinition): string {
genColumnBasicSql(cl: RowDefinition, create: boolean): string {
let length = this.getTypeLengthSql(cl);
// 默认值
let defVal = this.getDefaultValueSql(cl);
let incr = cl.auto_increment ? 'generated by default as IDENTITY' : '';
let pri = cl.pri ? 'PRIMARY KEY' : '';
return ` ${cl.name.toUpperCase()} ${cl.type}${length} ${incr} ${pri} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
let incr = cl.auto_increment && create ? 'generated by default as IDENTITY' : '';
// 如果有原名以原名为准
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
let baseSql = ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${incr}`;
return incr ? baseSql : ` ${baseSql} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
}
getCreateTableSql(data: any): string {
let schemaArr = data.db.split('/');
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(data.tableName)}`;
let createSql = '';
let tableCommentSql = '';
let columCommentSql = '';
@@ -285,17 +314,17 @@ class OracleDialect implements DbDialect {
// 创建表结构
let fields: string[] = [];
data.fields.res.forEach((item: any) => {
item.name && fields.push(this.genColumnBasicSql(item));
item.name && fields.push(this.genColumnBasicSql(item, true));
// 列注释
if (item.remark) {
columCommentSql += ` comment on column ${data.tableName?.toUpperCase()}.${item.name?.toUpperCase()} is '${item.remark}'; `;
columCommentSql += ` COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(item.name)} is '${item.remark}'; `;
}
});
// 建表
createSql = `CREATE TABLE ${data.tableName?.toUpperCase()} ( ${fields.join(',')} );`;
createSql = `CREATE TABLE ${dbTable} ( ${fields.join(',')} );`;
// 表注释
if (data.tableComment) {
tableCommentSql = ` comment on table ${data.tableName?.toUpperCase()} is '${data.tableComment}'; `;
tableCommentSql = ` COMMENT ON TABLE ${dbTable} is '${data.tableComment}'; `;
}
return createSql + tableCommentSql + columCommentSql;
@@ -304,43 +333,95 @@ class OracleDialect implements DbDialect {
getCreateIndexSql(tableData: any): string {
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
// COMMENT ON INDEX idx_column_name IS 'Your index comment here';
// 创建索引
let schemaArr = tableData.db.split('/');
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.tableName)}`;
let sql: string[] = [];
tableData.indexs.res.forEach((a: any) => {
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} ON "${tableData.tableName}" ("${a.columnNames.join('","')})"`);
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} ON ${dbTable} ("${a.columnNames.join('","')})"`);
});
return sql.join(';');
}
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let sql: string[] = [];
if (changeData.add.length > 0) {
changeData.add.forEach((a) => {
sql.push(`ALTER TABLE "${tableName}" add COLUMN ${this.genColumnBasicSql(a)}`);
if (a.remark) {
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let schemaArr = tableData.db.split('/');
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
let baseSql = `ALTER TABLE ${dbTable} `;
let modifyArr: string[] = [];
let dropArr: string[] = [];
// 重命名的sql要一条条执行
let renameArr: string[] = [];
let commentArr: string[] = [];
// 主键字段
let priArr = new Set();
if (changeData.upd.length > 0) {
changeData.upd.forEach((a) => {
let commentSql = `COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} IS '${a.remark}'`;
if (a.remark && a.oldName === a.name) {
commentArr.push(commentSql);
}
// 修改了字段名
if (a.oldName !== a.name) {
renameArr.push(baseSql + ` RENAME COLUMN ${this.quoteIdentifier(a.oldName!)} TO ${this.quoteIdentifier(a.name)} ;`);
if (a.remark) {
commentArr.push(commentSql);
}
}
modifyArr.push(` MODIFY (${this.genColumnBasicSql(a, false)})`);
if (a.pri) {
priArr.add(`${this.quoteIdentifier(a.name)}"`);
}
});
}
if (changeData.upd.length > 0) {
changeData.upd.forEach((a) => {
sql.push(`ALTER TABLE "${tableName}" MODIFY ${this.genColumnBasicSql(a)}`);
if (changeData.add.length > 0) {
changeData.add.forEach((a) => {
modifyArr.push(` ADD (${this.genColumnBasicSql(a, false)})`);
if (a.remark) {
sql.push(`comment on COLUMN "${tableName}"."${a.name}" is '${a.remark}'`);
commentArr.push(`COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}'`);
}
if (a.pri) {
priArr.add(`"${a.name}"`);
}
});
}
if (changeData.del.length > 0) {
changeData.del.forEach((a) => {
sql.push(`ALTER TABLE "${tableName}" DROP COLUMN ${a.name}`);
dropArr.push(`${this.quoteIdentifier(a.name)}`);
});
}
return sql.join(';');
let dropPkSql = '';
if (priArr.size > 0) {
let resPri = tableData.fields.res.find((a: RowDefinition) => a.pri);
if (resPri) {
priArr.add(`"${resPri.name}"`);
}
// 如果有编辑主键字段,则删除主键,再添加主键
// 解析表字段中是否含有主键,有的话就删除主键
if (tableData.fields.oldFields.find((a: RowDefinition) => a.pri)) {
dropPkSql = `ALTER TABLE ${dbTable} DROP PRIMARY KEY;`;
}
}
let modifySql = baseSql + modifyArr.join(' ') + ';';
let dropSql = baseSql + ` DROP (${dropArr.join(',')}) ;`;
let renameSql = renameArr.join('');
let addPkSql = priArr.size > 0 ? `ALTER TABLE ${dbTable} ADD CONSTRAINT "PK_${tableName}" PRIMARY KEY (${Array.from(priArr).join(',')});` : '';
let commentSql = commentArr.join(';');
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
}
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
// 不能直接修改索引名或字段、需要先删后加
let dropIndexNames: string[] = [];
let addIndexs: any[] = [];

View File

@@ -123,6 +123,7 @@ class PostgresqlDialect implements DbDialect {
};
pgDialectInfo = {
name: 'PostgreSQL',
icon: 'iconfont icon-op-postgres',
defaultPort: 5432,
formatSqlDialect: 'postgresql',
@@ -132,7 +133,7 @@ class PostgresqlDialect implements DbDialect {
return pgDialectInfo;
}
getDefaultSelectSql(table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
return `SELECT * FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
pageNum,
limit
@@ -228,7 +229,7 @@ class PostgresqlDialect implements DbDialect {
let marks = false;
if (this.matchType(cl.type, ['char', 'time', 'date', 'text'])) {
// 默认值是now()的time或date不需要加引号
if (cl.value.toLowerCase().replace(' ', '') === 'current_timestamp' && this.matchType(cl.type, ['time', 'date'])) {
if (['pg_systimestamp()', 'current_timestamp'].includes(cl.value.toLowerCase()) && this.matchType(cl.type, ['time', 'date'])) {
marks = false;
} else {
marks = true;
@@ -260,7 +261,10 @@ class PostgresqlDialect implements DbDialect {
let length = this.getTypeLengthSql(cl);
// 默认值
let defVal = this.getDefaultValueSql(cl);
return ` ${cl.name} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
// 如果有原名以原名为准
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
return ` ${this.quoteIdentifier(name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : ''} ${defVal} `;
}
getCreateTableSql(data: any): string {
@@ -299,52 +303,77 @@ class PostgresqlDialect implements DbDialect {
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
// COMMENT ON INDEX idx_column_name IS 'Your index comment here';
// 创建索引
let schema = tableData.db.split('/')[1];
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.tableName)}`;
let sql: string[] = [];
tableData.indexs.res.forEach((a: any) => {
sql.push(` CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName} USING btree ("${a.columnNames.join('","')})"`);
// 字段名用双引号包裹
let colArr = a.columnNames.map((a: string) => `${this.quoteIdentifier(a)}`);
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} on ${dbTable} (${colArr.join(',')})`);
if (a.indexComment) {
sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`);
sql.push(`COMMENT ON INDEX ${schema}.${this.quoteIdentifier(a.indexName)} IS '${a.indexComment}'`);
}
});
return sql.join(';');
}
getModifyColumnSql(tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let sql: string[] = [];
getModifyColumnSql(tableData: any, tableName: string, changeData: { del: RowDefinition[]; add: RowDefinition[]; upd: RowDefinition[] }): string {
let schemaArr = tableData.db.split('/');
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
let dropPkSql = '';
let modifySql = '';
let dropSql = '';
let renameSql = '';
let addPkSql = '';
let commentSql = '';
if (changeData.add.length > 0) {
changeData.add.forEach((a) => {
let typeLength = this.getTypeLengthSql(a);
let defaultSql = this.getDefaultValueSql(a);
sql.push(`ALTER TABLE ${tableName} add ${a.name} ${a.type}${typeLength} ${defaultSql}`);
modifySql += `alter table ${dbTable} add ${this.genColumnBasicSql(a)};`;
if (a.remark) {
sql.push(`comment on column "${tableName}"."${a.name}" is '${a.remark}'`);
commentSql += `comment on column ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}';`;
}
});
}
if (changeData.upd.length > 0) {
changeData.upd.forEach((a) => {
let cmtSql = `comment on column ${dbTable}.${this.quoteIdentifier(a.name)} is '${a.remark}';`;
if (a.remark && a.oldName === a.name) {
commentSql += cmtSql;
}
// 修改了字段名
if (a.oldName !== a.name) {
renameSql += `alter table ${dbTable} rename column ${this.quoteIdentifier(a.oldName!)} to ${this.quoteIdentifier(a.name)};`;
if (a.remark) {
commentSql += cmtSql;
}
}
let typeLength = this.getTypeLengthSql(a);
sql.push(`ALTER TABLE ${tableName} alter column ${a.name} type ${a.type}${typeLength}`);
// 如果有原名以原名为准
let name = a.oldName && a.name !== a.oldName ? a.oldName : a.name;
modifySql += `alter table ${dbTable} alter column ${this.quoteIdentifier(name)} type ${a.type}${typeLength} ;`;
let defaultSql = this.getDefaultValueSql(a);
if (defaultSql) {
sql.push(`alter table ${tableName} alter column ${a.name} set ${defaultSql}`);
}
if (a.remark) {
sql.push(`comment on column "${tableName}"."${a.name}" is '${a.remark}'`);
modifySql += `alter table ${dbTable} alter column ${this.quoteIdentifier(name)} set ${defaultSql} ;`;
}
});
}
if (changeData.del.length > 0) {
changeData.del.forEach((a) => {
sql.push(`ALTER TABLE ${tableName} DROP COLUMN ${a.name}`);
dropSql += `alter table ${dbTable} drop column ${a.name};`;
});
}
return sql.join(';');
return dropPkSql + modifySql + dropSql + renameSql + addPkSql + commentSql;
}
getModifyIndexSql(tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
let schema = tableData.db.split('/')[1];
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
// 不能直接修改索引名或字段、需要先删后加
let dropIndexNames: string[] = [];
let addIndexs: any[] = [];
@@ -378,9 +407,11 @@ class PostgresqlDialect implements DbDialect {
if (addIndexs.length > 0) {
addIndexs.forEach((a) => {
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')})`);
// 字段名用双引号包裹
let colArr = a.columnNames.map((a: string) => `${this.quoteIdentifier(a)}`);
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} on ${dbTable} (${colArr.join(',')})`);
if (a.indexComment) {
sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`);
sql.push(`COMMENT ON INDEX ${schema}.${this.quoteIdentifier(a.indexName)} IS '${a.indexComment}'`);
}
});
}
@@ -409,7 +440,7 @@ class PostgresqlDialect implements DbDialect {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
wrapStrValue(value: string, type: string): string {
wrapStrValue(columnType: string, value: string): string {
return `'${value}'`;
}
}

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();
}
"
:tag-path="form.tagPath"
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Machine.value"
style="width: 100%"
@@ -153,6 +154,7 @@ const state = reactive({
form: {
id: null,
code: '',
tagPath: '',
ip: null,
port: 22,
name: null,

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="machine-list">
<page-table
ref="pageTableRef"
:page-api="machineApi.list"
@@ -25,7 +25,7 @@
<span v-if="!data.stat">-</span>
<div v-else>
<el-row>
<el-text size="small" style="font-size: 10px">
<el-text size="small" class="font11">
内存(可用/):
<span :class="getStatsFontClass(data.stat.memAvailable, data.stat.memTotal)"
>{{ formatByteSize(data.stat.memAvailable, 1) }}/{{ formatByteSize(data.stat.memTotal, 1) }}
@@ -33,7 +33,7 @@
</el-text>
</el-row>
<el-row>
<el-text style="font-size: 10px" size="small">
<el-text class="font11" size="small">
CPU(空闲): <span :class="getStatsFontClass(data.stat.cpuIdle, 100)">{{ data.stat.cpuIdle.toFixed(0) }}%</span>
</el-text>
</el-row>
@@ -44,7 +44,7 @@
<span v-if="!data.stat?.fsInfos">-</span>
<div v-else>
<el-row v-for="(i, idx) in data.stat.fsInfos.slice(0, 2)" :key="i.mountPoint">
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
<el-text class="font11" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
</el-text>
@@ -55,7 +55,7 @@
</template>
<el-row v-for="i in data.stat.fsInfos.slice(2)" :key="i.mountPoint">
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
<el-text class="font11" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
</el-text>
</el-row>
@@ -231,8 +231,8 @@ const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Machine.value), Se
const columns = [
TableColumn.new('name', '名称'),
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(50),
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(20),
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(55),
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(25),
TableColumn.new('username', '用户名'),
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
@@ -464,10 +464,6 @@ const showRec = (row: any) => {
</script>
<style>
.el-dialog__body {
padding: 2px 2px;
}
.el-dropdown-link-machine-list {
cursor: pointer;
color: var(--el-color-primary);

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-col>
<el-col :span="18">
<el-input @clear="clear" v-model="scanParam.match" placeholder="match 支持*模糊key" clearable size="small" class="ml10" />
<el-input
@clear="clear"
v-model="scanParam.match"
@keyup.enter.native="searchKey()"
placeholder="match 支持*模糊key, 回车搜索"
clearable
size="small"
class="ml10"
/>
</el-col>
<el-col :span="4">
<el-button

View File

@@ -107,10 +107,10 @@ defineExpose({ getContent });
.format-viewer-container .el-textarea textarea {
font-size: 14px;
height: calc(100vh - 546px + v-bind(height));
height: calc(100vh - 550px + v-bind(height));
}
.format-viewer-container .monaco-editor-content {
height: calc(100vh - 560px + v-bind(height)) !important;
height: calc(100vh - 565px + v-bind(height)) !important;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<el-button @click="showEditDialog(null)" icon="plus" size="small" plain type="primary" class="mb10">添加新行</el-button>
<el-table size="small" border :data="hashValues" height="450" min-height="300" stripe>
<el-table size="small" border :data="hashValues" height="500" min-height="300" stripe>
<el-table-column type="index" :label="'ID (Total: ' + total + ')'" sortable width="100"> </el-table-column>
<el-table-column resizable sortable prop="field" label="field" show-overflow-tooltip min-width="100"> </el-table-column>
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200"> </el-table-column>
@@ -11,7 +11,7 @@
class="key-detail-filter-value"
v-model="state.filterValue"
@keyup.enter="hscan(true, true)"
placeholder="输入关键词回车搜索"
placeholder="关键词回车搜索"
clearable
size="small"
/>
@@ -51,7 +51,7 @@
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, reactive, watch, toRefs } from 'vue';
import { ref, onMounted, reactive, toRefs } from 'vue';
import { redisApi } from './api';
import { ElMessage } from 'element-plus';
import { notBlank } from '@/common/assert';

View File

@@ -142,7 +142,7 @@ const search = async () => {
const changeStatus = async (row: any) => {
let id = row.id;
let status = row.status == -1 ? 1 : -1;
let status = row.status == AccountStatusEnum.Disable.value ? AccountStatusEnum.Enable.value : AccountStatusEnum.Disable.value;
await accountApi.changeStatus.request({
id,
status,

View File

@@ -2,6 +2,7 @@ import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import type { UserConfig } from 'vite';
import { loadEnv } from './src/common/utils/viteBuild';
import { CodeInspectorPlugin } from 'code-inspector-plugin';
const pathResolve = (dir: string): any => {
return resolve(__dirname, '.', dir);
@@ -14,7 +15,12 @@ const alias: Record<string, string> = {
};
const viteConfig: UserConfig = {
plugins: [vue()],
plugins: [
vue(),
CodeInspectorPlugin({
bundler: 'vite',
}),
],
root: process.cwd(),
resolve: {
alias,

View File

@@ -37,22 +37,19 @@ sqlite:
# password: 111049
# db: 0
log:
# 日志等级, debug, info, warn, error
# 日志等级, debug, info, warn, error
level: info
# 日志格式类型, text/json
type: text
# 是否记录方法调用栈信息
add-source: false
# 日志文件配置
# file:
# path: ./
# path: ./log
# name: mayfly-go.log
db:
backup-path: ./backup
mysqlutil-path:
mysql: ./mysqlutil/bin/mysql
mysqldump: ./mysqlutil/bin/mysqldump
mysqlbinlog: ./mysqlutil/bin/mysqlbinlog
mariadbutil-path:
mysql: ./mariadbutil/bin/mariadb
mysqldump: ./mariadbutil/bin/mariadb-dump
mysqlbinlog: ./mariadbutil/bin/mariadb-binlog
# # 日志文件的最大大小(以兆字节为单位)。当日志文件大小达到该值时,将触发切割操作
# max-size: 500
# # 根据文件名中的时间戳,设置保留旧日志文件的最大天数
# max-age: 60
# # 是否使用 gzip 压缩方式压缩轮转后的日志文件
# compress: true

View File

@@ -10,31 +10,34 @@ require (
github.com/gin-gonic/gin v1.9.1
github.com/glebarez/sqlite v1.10.0
github.com/go-gormigrate/gormigrate/v2 v2.1.0
github.com/go-ldap/ldap/v3 v3.4.5
github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.14.0
github.com/go-sql-driver/mysql v1.7.1
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.3.0
github.com/google/uuid v1.3.1
github.com/gorilla/websocket v1.5.1
github.com/kanzihuang/vitess/go/vt/sqlparser v0.0.0-20231018071450-ac8d9f0167e9
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230712084735-068dc2aee82d
github.com/microsoft/go-mssqldb v1.6.0
github.com/mojocn/base64Captcha v1.3.6 //
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.6
github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/v9 v9.4.0
github.com/robfig/cron/v3 v3.0.1 //
github.com/sijms/go-ora/v2 v2.8.5
github.com/sijms/go-ora/v2 v2.8.7
github.com/stretchr/testify v1.8.4
go.mongodb.org/mongo-driver v1.13.1 // mongo
golang.org/x/crypto v0.18.0 // ssh
golang.org/x/oauth2 v0.15.0
golang.org/x/sync v0.6.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
// gorm
gorm.io/driver/mysql v1.5.2
gorm.io/gorm v1.25.5
gorm.io/gorm v1.25.6
)
require (
@@ -49,8 +52,10 @@ require (
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
@@ -79,10 +84,9 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230519143937-03e91628a987
golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.7 // indirect

View File

@@ -1,11 +1,45 @@
package initialize
import (
dbInit "mayfly-go/internal/db/init"
machineInit "mayfly-go/internal/machine/init"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ioc"
)
func InitOther() {
machineInit.Init()
dbInit.Init()
// 初始化ioc函数
type InitIocFunc func()
// 初始化函数
type InitFunc func()
var (
initIocFuncs = make([]InitIocFunc, 0)
initFuncs = make([]InitFunc, 0)
)
// 添加初始化ioc函数由各个默认自行添加
func AddInitIocFunc(initIocFunc InitIocFunc) {
initIocFuncs = append(initIocFuncs, initIocFunc)
}
// 添加初始化函数,由各个默认自行添加
func AddInitFunc(initFunc InitFunc) {
initFuncs = append(initFuncs, initFunc)
}
// 系统启动时,调用各个模块的初始化函数
func InitOther() {
// 调用各个默认ioc组件注册初始化优先调用ioc初始化注册函数和注入函数可能在后续的InitFunc中需要用到依赖实例
for _, initIocFunc := range initIocFuncs {
initIocFunc()
}
initIocFuncs = nil
// 为所有注册的实例注入其依赖的其他组件实例
biz.ErrIsNil(ioc.InjectComponents())
// 调用各个默认的初始化函数
for _, initFunc := range initFuncs {
go initFunc()
}
initFuncs = nil
}

View File

@@ -3,15 +3,6 @@ package initialize
import (
"fmt"
"io/fs"
auth_router "mayfly-go/internal/auth/router"
common_router "mayfly-go/internal/common/router"
db_router "mayfly-go/internal/db/router"
machine_router "mayfly-go/internal/machine/router"
mongo_router "mayfly-go/internal/mongo/router"
msg_router "mayfly-go/internal/msg/router"
redis_router "mayfly-go/internal/redis/router"
sys_router "mayfly-go/internal/sys/router"
tag_router "mayfly-go/internal/tag/router"
"mayfly-go/pkg/config"
"mayfly-go/pkg/middleware"
"mayfly-go/static"
@@ -20,6 +11,18 @@ import (
"github.com/gin-gonic/gin"
)
// 初始化路由函数
type InitRouterFunc func(router *gin.RouterGroup)
var (
initRouterFuncs = make([]InitRouterFunc, 0)
)
// 添加初始化路由函数,由各个默认自行添加
func AddInitRouterFunc(initRouterFunc InitRouterFunc) {
initRouterFuncs = append(initRouterFuncs, initRouterFunc)
}
func InitRouter() *gin.Engine {
// server配置
serverConfig := config.Conf.Server
@@ -43,20 +46,11 @@ func InitRouter() *gin.Engine {
// 设置路由组
api := router.Group(serverConfig.ContextPath + "/api")
{
common_router.Init(api)
auth_router.Init(api)
sys_router.Init(api)
msg_router.Init(api)
tag_router.Init(api)
machine_router.Init(api)
db_router.Init(api)
redis_router.Init(api)
mongo_router.Init(api)
// 调用所有模块注册的初始化路由函数
for _, initRouterFunc := range initRouterFuncs {
initRouterFunc(api)
}
initRouterFuncs = nil
return router
}

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
import (
dbApp "mayfly-go/internal/db/application"
// 系统进程退出终止函数
type TerminateFunc func()
var (
terminateFuncs = make([]TerminateFunc, 0)
)
// 终止服务后的一些操作
func Terminate() {
closeDbTasks()
// 添加系统退出终止时执行的函数,由各个默认自行添加
func AddTerminateFunc(terminateFunc TerminateFunc) {
terminateFuncs = append(terminateFuncs, terminateFunc)
}
func closeDbTasks() {
restoreApp := dbApp.GetDbRestoreApp()
if restoreApp != nil {
restoreApp.Close()
}
binlogApp := dbApp.GetDbBinlogApp()
if binlogApp != nil {
binlogApp.Close()
}
backupApp := dbApp.GetDbBackupApp()
if backupApp != nil {
backupApp.Close()
// 终止进程服务后的一些操作
func Terminate() {
for _, terminateFunc := range terminateFuncs {
terminateFunc()
}
}

View File

@@ -25,8 +25,8 @@ import (
)
type AccountLogin struct {
AccountApp sysapp.Account
MsgApp msgapp.Msg
AccountApp sysapp.Account `inject:""`
MsgApp msgapp.Msg `inject:""`
}
/** 用户账号密码登录 **/

View File

@@ -28,8 +28,8 @@ import (
)
type LdapLogin struct {
AccountApp sysapp.Account
MsgApp msgapp.Msg
AccountApp sysapp.Account `inject:""`
MsgApp msgapp.Msg `inject:""`
}
// @router /auth/ldap/enabled [get]

View File

@@ -28,9 +28,9 @@ import (
)
type Oauth2Login struct {
Oauth2App application.Oauth2
AccountApp sysapp.Account
MsgApp msgapp.Msg
Oauth2App application.Oauth2 `inject:""`
AccountApp sysapp.Account `inject:""`
MsgApp msgapp.Msg `inject:""`
}
func (a *Oauth2Login) OAuth2Login(rc *req.Ctx) {

View File

@@ -1,11 +1,12 @@
package application
import "mayfly-go/internal/auth/infrastructure/persistence"
var (
authApp = newAuthApp(persistence.GetOauthAccountRepo())
import (
"mayfly-go/internal/auth/infrastructure/persistence"
"mayfly-go/pkg/ioc"
)
func GetAuthApp() Oauth2 {
return authApp
func InitIoc() {
persistence.Init()
ioc.Register(new(oauth2AppImpl), ioc.WithComponentName("Oauth2App"))
}

View File

@@ -14,27 +14,21 @@ type Oauth2 interface {
Unbind(accountId uint64)
}
func newAuthApp(oauthAccountRepo repository.Oauth2Account) Oauth2 {
return &oauth2AppImpl{
oauthAccountRepo: oauthAccountRepo,
}
}
type oauth2AppImpl struct {
oauthAccountRepo repository.Oauth2Account
Oauth2AccountRepo repository.Oauth2Account `inject:""`
}
func (a *oauth2AppImpl) GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error {
return a.oauthAccountRepo.GetBy(condition, cols...)
return a.Oauth2AccountRepo.GetBy(condition, cols...)
}
func (a *oauth2AppImpl) BindOAuthAccount(e *entity.Oauth2Account) error {
if e.Id == 0 {
return a.oauthAccountRepo.Insert(context.Background(), e)
return a.Oauth2AccountRepo.Insert(context.Background(), e)
}
return a.oauthAccountRepo.UpdateById(context.Background(), e)
return a.Oauth2AccountRepo.UpdateById(context.Background(), e)
}
func (a *oauth2AppImpl) Unbind(accountId uint64) {
a.oauthAccountRepo.DeleteByCond(context.Background(), &entity.Oauth2Account{AccountId: accountId})
a.Oauth2AccountRepo.DeleteByCond(context.Background(), &entity.Oauth2Account{AccountId: accountId})
}

View File

@@ -2,6 +2,7 @@ package config
import (
sysapp "mayfly-go/internal/sys/application"
"mayfly-go/pkg/utils/conv"
"mayfly-go/pkg/utils/stringx"
)
@@ -26,8 +27,8 @@ func GetAccountLoginSecurity() *AccountLoginSecurity {
als := new(AccountLoginSecurity)
als.UseCaptcha = c.ConvBool(jm["useCaptcha"], true)
als.UseOtp = c.ConvBool(jm["useOtp"], false)
als.LoginFailCount = stringx.ConvInt(jm["loginFailCount"], 5)
als.LoginFailMin = stringx.ConvInt(jm["loginFailMin"], 10)
als.LoginFailCount = conv.Str2Int(jm["loginFailCount"], 5)
als.LoginFailMin = conv.Str2Int(jm["loginFailMin"], 10)
otpIssuer := jm["otpIssuer"]
if otpIssuer == "" {
otpIssuer = "mayfly-go"

View File

@@ -1,11 +1,9 @@
package persistence
import "mayfly-go/internal/auth/domain/repository"
var (
authAccountRepo = newAuthAccountRepo()
import (
"mayfly-go/pkg/ioc"
)
func GetOauthAccountRepo() repository.Oauth2Account {
return authAccountRepo
func Init() {
ioc.Register(newAuthAccountRepo(), ioc.WithComponentName("Oauth2AccountRepo"))
}

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 (
"mayfly-go/internal/auth/api"
"mayfly-go/internal/auth/application"
msgapp "mayfly-go/internal/msg/application"
sysapp "mayfly-go/internal/sys/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ioc"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
func Init(router *gin.RouterGroup) {
accountLogin := &api.AccountLogin{
AccountApp: sysapp.GetAccountApp(),
MsgApp: msgapp.GetMsgApp(),
}
accountLogin := new(api.AccountLogin)
biz.ErrIsNil(ioc.Inject(accountLogin))
ldapLogin := &api.LdapLogin{
AccountApp: sysapp.GetAccountApp(),
MsgApp: msgapp.GetMsgApp(),
}
ldapLogin := new(api.LdapLogin)
biz.ErrIsNil(ioc.Inject(ldapLogin))
oauth2Login := &api.Oauth2Login{
Oauth2App: application.GetAuthApp(),
AccountApp: sysapp.GetAccountApp(),
MsgApp: msgapp.GetMsgApp(),
}
oauth2Login := new(api.Oauth2Login)
biz.ErrIsNil(ioc.Inject(oauth2Login))
rg := router.Group("/auth")

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) {
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 {
InstanceApp application.Instance
DbApp application.Db
DbSqlExecApp application.DbSqlExec
MsgApp msgapp.Msg
TagApp tagapp.TagTree
InstanceApp application.Instance `inject:"DbInstanceApp"`
DbApp application.Db `inject:""`
DbSqlExecApp application.DbSqlExec `inject:""`
MsgApp msgapp.Msg `inject:""`
TagApp tagapp.TagTree `inject:"TagTreeApp"`
}
// @router /api/dbs [get]
@@ -78,8 +78,6 @@ func (d *Db) DeleteDb(rc *req.Ctx) {
d.DbApp.Delete(ctx, dbId)
// 删除该库的sql执行记录
d.DbSqlExecApp.DeleteBy(ctx, &entity.DbSqlExec{DbId: dbId})
// todo delete restore task and histories
}
}
@@ -355,7 +353,7 @@ func (d *Db) dumpDb(writer *gzipWriter, dbId uint64, dbName string, tables []str
writer.WriteString("BEGIN;\n")
}
insertSql := "INSERT INTO %s VALUES (%s);\n"
dbMeta.WalkTableRecord(table, func(record map[string]any, columns []*dbi.QueryColumn) error {
dbConn.WalkTableRows(context.TODO(), table, func(record map[string]any, columns []*dbi.QueryColumn) error {
var values []string
writer.TryFlush()
for _, column := range columns {
@@ -462,6 +460,20 @@ func (d *Db) GetSchemas(rc *req.Ctx) {
rc.ResData = res
}
func (d *Db) CopyTable(rc *req.Ctx) {
form := &form.DbCopyTableForm{}
copy := ginx.BindJsonAndCopyTo[*dbi.DbCopyTable](rc.GinCtx, form, new(dbi.DbCopyTable))
conn, err := d.DbApp.GetDbConn(form.Id, form.Db)
biz.ErrIsNilAppendErr(err, "拷贝表失败: %s")
err = conn.GetDialect().CopyTable(copy)
if err != nil {
logx.Errorf("拷贝表失败: %s", err.Error())
}
biz.ErrIsNilAppendErr(err, "拷贝表失败: %s")
}
func getDbId(g *gin.Context) uint64 {
dbId, _ := strconv.Atoi(g.Param("dbId"))
biz.IsTrue(dbId > 0, "dbId错误")

View File

@@ -9,13 +9,16 @@ import (
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/timex"
"strconv"
"strings"
"time"
)
type DbBackup struct {
DbBackupApp *application.DbBackupApp
DbApp application.Db
backupApp *application.DbBackupApp `inject:"DbBackupApp"`
dbApp application.Db `inject:"DbApp"`
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
}
// todo: 鉴权,避免未经授权进行数据库备份和恢复
@@ -25,13 +28,13 @@ type DbBackup struct {
func (d *DbBackup) GetPageList(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
db, err := d.DbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
queryCond, page := ginx.BindQueryAndPage[*entity.DbJobQuery](rc.GinCtx, new(entity.DbJobQuery))
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupQuery](rc.GinCtx, new(entity.DbBackupQuery))
queryCond.DbInstanceId = db.InstanceId
queryCond.InDbNames = strings.Fields(db.Database)
res, err := d.DbBackupApp.GetPageList(queryCond, page, new([]vo.DbBackup))
res, err := d.backupApp.GetPageList(queryCond, page, new([]vo.DbBackup))
biz.ErrIsNilAppendErr(err, "获取数据库备份任务失败: %v")
rc.ResData = res
}
@@ -48,23 +51,22 @@ func (d *DbBackup) Create(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
db, err := d.DbApp.GetById(new(entity.Db), dbId, "instanceId")
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instanceId")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
jobs := make([]*entity.DbBackup, 0, len(dbNames))
for _, dbName := range dbNames {
job := &entity.DbBackup{
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeBackup),
Enabled: true,
Repeated: backupForm.Repeated,
StartTime: backupForm.StartTime,
Interval: backupForm.Interval,
Name: backupForm.Name,
DbInstanceId: db.InstanceId,
DbName: dbName,
Enabled: true,
Repeated: backupForm.Repeated,
StartTime: backupForm.StartTime,
Interval: backupForm.Interval,
Name: backupForm.Name,
}
job.DbName = dbName
jobs = append(jobs, job)
}
biz.ErrIsNilAppendErr(d.DbBackupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
biz.ErrIsNilAppendErr(d.backupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
}
// Update 保存数据库备份任务
@@ -74,17 +76,18 @@ func (d *DbBackup) Update(rc *req.Ctx) {
ginx.BindJsonAndValid(rc.GinCtx, backupForm)
rc.ReqParam = backupForm
job := entity.NewDbJob(entity.DbJobTypeBackup).(*entity.DbBackup)
job := &entity.DbBackup{}
job.Id = backupForm.Id
job.Name = backupForm.Name
job.StartTime = backupForm.StartTime
job.Interval = backupForm.Interval
biz.ErrIsNilAppendErr(d.DbBackupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
job.MaxSaveDays = backupForm.MaxSaveDays
biz.ErrIsNilAppendErr(d.backupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
}
func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint64) error) error {
idsStr := ginx.PathParam(rc.GinCtx, "backupId")
biz.NotEmpty(idsStr, "backupId 为空")
func (d *DbBackup) walk(rc *req.Ctx, paramName string, fn func(ctx context.Context, id uint64) error) error {
idsStr := ginx.PathParam(rc.GinCtx, paramName)
biz.NotEmpty(idsStr, paramName+" 为空")
rc.ReqParam = idsStr
ids := strings.Fields(idsStr)
for _, v := range ids {
@@ -104,28 +107,28 @@ func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint6
// Delete 删除数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId [DELETE]
func (d *DbBackup) Delete(rc *req.Ctx) {
err := d.walk(rc, d.DbBackupApp.Delete)
err := d.walk(rc, "backupId", d.backupApp.Delete)
biz.ErrIsNilAppendErr(err, "删除数据库备份任务失败: %v")
}
// Enable 启用数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId/enable [PUT]
func (d *DbBackup) Enable(rc *req.Ctx) {
err := d.walk(rc, d.DbBackupApp.Enable)
err := d.walk(rc, "backupId", d.backupApp.Enable)
biz.ErrIsNilAppendErr(err, "启用数据库备份任务失败: %v")
}
// Disable 禁用数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId/disable [PUT]
func (d *DbBackup) Disable(rc *req.Ctx) {
err := d.walk(rc, d.DbBackupApp.Disable)
err := d.walk(rc, "backupId", d.backupApp.Disable)
biz.ErrIsNilAppendErr(err, "禁用数据库备份任务失败: %v")
}
// Start 禁用数据库备份任务
// @router /api/dbs/:dbId/backups/:backupId/start [PUT]
func (d *DbBackup) Start(rc *req.Ctx) {
err := d.walk(rc, d.DbBackupApp.Start)
err := d.walk(rc, "backupId", d.backupApp.StartNow)
biz.ErrIsNilAppendErr(err, "运行数据库备份任务失败: %v")
}
@@ -133,10 +136,10 @@ func (d *DbBackup) Start(rc *req.Ctx) {
// @router /api/dbs/:dbId/db-names-without-backup [GET]
func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
db, err := d.DbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
dbNames := strings.Fields(db.Database)
dbNamesWithoutBackup, err := d.DbBackupApp.GetDbNamesWithoutBackup(db.InstanceId, dbNames)
dbNamesWithoutBackup, err := d.backupApp.GetDbNamesWithoutBackup(db.InstanceId, dbNames)
biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
rc.ResData = dbNamesWithoutBackup
}
@@ -146,13 +149,74 @@ func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
db, err := d.DbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
queryCond.DbInstanceId = db.InstanceId
queryCond.InDbNames = strings.Fields(db.Database)
res, err := d.DbBackupApp.GetHistoryPageList(queryCond, page, new([]vo.DbBackupHistory))
backupHistoryCond, page := ginx.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc.GinCtx, new(entity.DbBackupHistoryQuery))
backupHistoryCond.DbInstanceId = db.InstanceId
backupHistoryCond.InDbNames = strings.Fields(db.Database)
backupHistories := make([]*vo.DbBackupHistory, 0, page.PageSize)
res, err := d.backupApp.GetHistoryPageList(backupHistoryCond, page, &backupHistories)
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
historyIds := make([]uint64, 0, len(backupHistories))
for _, history := range backupHistories {
historyIds = append(historyIds, history.Id)
}
restores := make([]*entity.DbRestore, 0, page.PageSize)
if err := d.restoreApp.GetRestoresEnabled(&restores, historyIds...); err != nil {
biz.ErrIsNilAppendErr(err, "获取数据库备份恢复记录失败")
}
for _, history := range backupHistories {
for _, restore := range restores {
if restore.DbBackupHistoryId == history.Id {
history.LastStatus = restore.LastStatus
history.LastResult = restore.LastResult
history.LastTime = restore.LastTime
break
}
}
}
rc.ResData = res
}
// RestoreHistories 从数据库备份历史中恢复数据库
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId/restore [POST]
func (d *DbBackup) RestoreHistories(rc *req.Ctx) {
pm := ginx.PathParam(rc.GinCtx, "backupHistoryId")
biz.NotEmpty(pm, "backupHistoryId 为空")
idsStr := strings.Fields(pm)
ids := make([]uint64, 0, len(idsStr))
for _, s := range idsStr {
id, err := strconv.ParseUint(s, 10, 64)
biz.ErrIsNilAppendErr(err, "从数据库备份历史恢复数据库失败: %v")
ids = append(ids, id)
}
histories := make([]*entity.DbBackupHistory, 0, len(ids))
err := d.backupApp.GetHistories(ids, &histories)
biz.ErrIsNilAppendErr(err, "添加数据库恢复任务失败: %v")
restores := make([]*entity.DbRestore, 0, len(histories))
now := time.Now()
for _, history := range histories {
job := &entity.DbRestore{
DbInstanceId: history.DbInstanceId,
DbName: history.DbName,
Enabled: true,
Repeated: false,
StartTime: now,
Interval: 0,
PointInTime: timex.NewNullTime(time.Time{}),
DbBackupId: history.DbBackupId,
DbBackupHistoryId: history.Id,
DbBackupHistoryName: history.Name,
}
restores = append(restores, job)
}
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, restores), "添加数据库恢复任务失败: %v")
}
// DeleteHistories 删除数据库备份历史
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId [DELETE]
func (d *DbBackup) DeleteHistories(rc *req.Ctx) {
err := d.walk(rc, "backupHistoryId", d.backupApp.DeleteHistory)
biz.ErrIsNilAppendErr(err, "删除数据库备份历史失败: %v")
}

View File

@@ -1,7 +1,6 @@
package api
import (
"context"
"encoding/base64"
"mayfly-go/internal/db/api/form"
"mayfly-go/internal/db/api/vo"
@@ -15,11 +14,10 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type DataSyncTask struct {
DataSyncTaskApp application.DataSyncTask
DataSyncTaskApp application.DataSyncTask `inject:"DbDataSyncTaskApp"`
}
func (d *DataSyncTask) Tasks(rc *req.Ctx) {
@@ -47,13 +45,6 @@ func (d *DataSyncTask) SaveTask(rc *req.Ctx) {
task.DataSql = sql
form.DataSql = sql
key := task.TaskKey
// 判断key为空就生成随机key
if key == "" {
key = uuid.New().String()
task.TaskKey = key
}
rc.ReqParam = form
biz.ErrIsNil(d.DataSyncTaskApp.Save(rc.MetaCtx, task))
}
@@ -73,7 +64,7 @@ func (d *DataSyncTask) DeleteTask(rc *req.Ctx) {
func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
form := &form.DataSyncTaskStatusForm{}
task := ginx.BindJsonAndCopyTo[*entity.DataSyncTask](rc.GinCtx, form, new(entity.DataSyncTask))
_ = d.DataSyncTaskApp.UpdateById(context.Background(), task)
_ = d.DataSyncTaskApp.UpdateById(rc.MetaCtx, task)
if task.Status == entity.DataSyncTaskStatusEnable {
task, err := d.DataSyncTaskApp.GetById(new(entity.DataSyncTask), task.Id)
@@ -89,7 +80,7 @@ func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
func (d *DataSyncTask) Run(rc *req.Ctx) {
taskId := getTaskId(rc.GinCtx)
rc.ReqParam = taskId
d.DataSyncTaskApp.RunCronJob(taskId)
_ = d.DataSyncTaskApp.RunCronJob(taskId)
}
func (d *DataSyncTask) Stop(rc *req.Ctx) {
@@ -99,7 +90,7 @@ func (d *DataSyncTask) Stop(rc *req.Ctx) {
task := new(entity.DataSyncTask)
task.Id = taskId
task.RunningState = entity.DataSyncTaskRunStateStop
_ = d.DataSyncTaskApp.UpdateById(context.Background(), task)
_ = d.DataSyncTaskApp.UpdateById(rc.MetaCtx, task)
}
func (d *DataSyncTask) GetTask(rc *req.Ctx) {

View File

@@ -16,8 +16,8 @@ import (
)
type Instance struct {
InstanceApp application.Instance
DbApp application.Db
InstanceApp application.Instance `inject:"DbInstanceApp"`
DbApp application.Db `inject:""`
}
// Instances 获取数据库实例信息
@@ -87,16 +87,10 @@ func (d *Instance) DeleteInstance(rc *req.Ctx) {
for _, v := range ids {
value, err := strconv.Atoi(v)
biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s")
biz.ErrIsNilAppendErr(err, "删除数据库实例失败: %s")
instanceId := uint64(value)
if d.DbApp.Count(&entity.DbQuery{InstanceId: instanceId}) != 0 {
instance, err := d.InstanceApp.GetById(new(entity.DbInstance), instanceId, "name")
biz.ErrIsNil(err, "获取数据库实例错误数据库实例ID为: %d", instance.Id)
biz.IsTrue(false, "不能删除数据库实例【%s】请先删除其关联的数据库资源。", instance.Name)
}
// todo check if backup task has been disabled and backup histories have been deleted
d.InstanceApp.Delete(rc.MetaCtx, instanceId)
err = d.InstanceApp.Delete(rc.MetaCtx, instanceId)
biz.ErrIsNilAppendErr(err, "删除数据库实例失败: %s")
}
}

View File

@@ -14,8 +14,8 @@ import (
)
type DbRestore struct {
DbRestoreApp *application.DbRestoreApp
DbApp application.Db
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
dbApp application.Db `inject:"DbApp"`
}
// GetPageList 获取数据库恢复任务
@@ -23,14 +23,14 @@ type DbRestore struct {
func (d *DbRestore) GetPageList(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
db, err := d.DbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
db, err := d.dbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
var restores []vo.DbRestore
queryCond, page := ginx.BindQueryAndPage[*entity.DbJobQuery](rc.GinCtx, new(entity.DbJobQuery))
queryCond, page := ginx.BindQueryAndPage[*entity.DbRestoreQuery](rc.GinCtx, new(entity.DbRestoreQuery))
queryCond.DbInstanceId = db.InstanceId
queryCond.InDbNames = strings.Fields(db.Database)
res, err := d.DbRestoreApp.GetPageList(queryCond, page, &restores)
res, err := d.restoreApp.GetPageList(queryCond, page, &restores)
biz.ErrIsNilAppendErr(err, "获取数据库恢复任务失败: %v")
rc.ResData = res
}
@@ -44,11 +44,12 @@ func (d *DbRestore) Create(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
db, err := d.DbApp.GetById(new(entity.Db), dbId, "instanceId")
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instanceId")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
job := &entity.DbRestore{
DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, entity.DbJobTypeRestore),
DbInstanceId: db.InstanceId,
DbName: restoreForm.DbName,
Enabled: true,
Repeated: restoreForm.Repeated,
StartTime: restoreForm.StartTime,
@@ -58,8 +59,11 @@ func (d *DbRestore) Create(rc *req.Ctx) {
DbBackupHistoryId: restoreForm.DbBackupHistoryId,
DbBackupHistoryName: restoreForm.DbBackupHistoryName,
}
job.DbName = restoreForm.DbName
biz.ErrIsNilAppendErr(d.DbRestoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
}
func (d *DbRestore) createWithBackupHistory(backupHistoryIds string) {
}
// Update 保存数据库恢复任务
@@ -73,7 +77,7 @@ func (d *DbRestore) Update(rc *req.Ctx) {
job.Id = restoreForm.Id
job.StartTime = restoreForm.StartTime
job.Interval = restoreForm.Interval
biz.ErrIsNilAppendErr(d.DbRestoreApp.Update(rc.MetaCtx, job), "保存数据库恢复任务失败: %v")
biz.ErrIsNilAppendErr(d.restoreApp.Update(rc.MetaCtx, job), "保存数据库恢复任务失败: %v")
}
func (d *DbRestore) walk(rc *req.Ctx, fn func(ctx context.Context, restoreId uint64) error) error {
@@ -98,21 +102,21 @@ func (d *DbRestore) walk(rc *req.Ctx, fn func(ctx context.Context, restoreId uin
// Delete 删除数据库恢复任务
// @router /api/dbs/:dbId/restores/:restoreId [DELETE]
func (d *DbRestore) Delete(rc *req.Ctx) {
err := d.walk(rc, d.DbRestoreApp.Delete)
err := d.walk(rc, d.restoreApp.Delete)
biz.ErrIsNilAppendErr(err, "删除数据库恢复任务失败: %v")
}
// Enable 启用数据库恢复任务
// @router /api/dbs/:dbId/restores/:restoreId/enable [PUT]
func (d *DbRestore) Enable(rc *req.Ctx) {
err := d.walk(rc, d.DbRestoreApp.Enable)
err := d.walk(rc, d.restoreApp.Enable)
biz.ErrIsNilAppendErr(err, "启用数据库恢复任务失败: %v")
}
// Disable 禁用数据库恢复任务
// @router /api/dbs/:dbId/restores/:restoreId/disable [PUT]
func (d *DbRestore) Disable(rc *req.Ctx) {
err := d.walk(rc, d.DbRestoreApp.Disable)
err := d.walk(rc, d.restoreApp.Disable)
biz.ErrIsNilAppendErr(err, "禁用数据库恢复任务失败: %v")
}
@@ -120,21 +124,21 @@ func (d *DbRestore) Disable(rc *req.Ctx) {
// @router /api/dbs/:dbId/db-names-without-backup [GET]
func (d *DbRestore) GetDbNamesWithoutRestore(rc *req.Ctx) {
dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
db, err := d.DbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
db, err := d.dbApp.GetById(new(entity.Db), dbId, "instance_id", "database")
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
dbNames := strings.Fields(db.Database)
dbNamesWithoutRestore, err := d.DbRestoreApp.GetDbNamesWithoutRestore(db.InstanceId, dbNames)
dbNamesWithoutRestore, err := d.restoreApp.GetDbNamesWithoutRestore(db.InstanceId, dbNames)
biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
rc.ResData = dbNamesWithoutRestore
}
// 获取数据库备份历史
// GetHistoryPageList 获取数据库备份历史
// @router /api/dbs/:dbId/restores/:restoreId/histories [GET]
func (d *DbRestore) GetHistoryPageList(rc *req.Ctx) {
queryCond := &entity.DbRestoreHistoryQuery{
DbRestoreId: uint64(ginx.PathParamInt(rc.GinCtx, "restoreId")),
}
res, err := d.DbRestoreApp.GetHistoryPageList(queryCond, ginx.GetPageParam(rc.GinCtx), new([]vo.DbRestoreHistory))
res, err := d.restoreApp.GetHistoryPageList(queryCond, ginx.GetPageParam(rc.GinCtx), new([]vo.DbRestoreHistory))
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
rc.ResData = res
}

View File

@@ -10,7 +10,7 @@ import (
)
type DbSql struct {
DbSqlApp application.DbSql
DbSqlApp application.DbSql `inject:""`
}
// @router /api/db/:dbId/sql [post]

View File

@@ -9,7 +9,7 @@ import (
)
type DbSqlExec struct {
DbSqlExecApp application.DbSqlExec
DbSqlExecApp application.DbSqlExec `inject:""`
}
func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) {

View File

@@ -23,3 +23,11 @@ type DbSqlExecForm struct {
Sql string `binding:"required" json:"sql"` // 执行sql
Remark string `json:"remark"` // 执行备注
}
// 数据库复制表
type DbCopyTableForm struct {
Id uint64 `binding:"required" json:"id"`
Db string `binding:"required" json:"db" `
TableName string `binding:"required" json:"tableName"`
CopyData bool `json:"copyData"` // 是否复制数据
}

View File

@@ -14,6 +14,7 @@ type DbBackupForm struct {
Interval time.Duration `json:"-"` // 间隔时间: 为零表示单次执行,为正表示反复执行
IntervalDay uint64 `json:"intervalDay"` // 间隔天数: 为零表示单次执行,为正表示反复执行
Repeated bool `json:"repeated"` // 是否重复执行
MaxSaveDays int `json:"maxSaveDays"` // 数据库备份历史保留天数,过期将自动删除
}
func (restore *DbBackupForm) UnmarshalJSON(data []byte) error {

View File

@@ -5,9 +5,9 @@ type InstanceForm struct {
Name string `binding:"required" json:"name"`
Type string `binding:"required" json:"type"` // 类型mysql oracle等
Host string `binding:"required" json:"host"`
Port int `binding:"required" json:"port"`
Port int `json:"port"`
Sid string `json:"sid"`
Username string `binding:"required" json:"username"`
Username string `json:"username"`
Password string `json:"password"`
Params string `json:"params"`
Remark string `json:"remark"`

View File

@@ -2,37 +2,51 @@ package vo
import (
"encoding/json"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/pkg/utils/timex"
"time"
)
// DbBackup 数据库备份任务
type DbBackup struct {
Id uint64 `json:"id"`
DbName string `json:"dbName"` // 数据库名
CreateTime time.Time `json:"createTime"` // 创建时间
StartTime time.Time `json:"startTime"` // 开始时间
Interval time.Duration `json:"-"` // 间隔时间
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
Enabled bool `json:"enabled"` // 是否启用
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
LastStatus string `json:"lastStatus"` // 最近一次执行状态
LastResult string `json:"lastResult"` // 最近一次执行结果
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
Name string `json:"name"` // 备份任务名称
Id uint64 `json:"id"`
DbName string `json:"dbName"` // 数据库名
CreateTime time.Time `json:"createTime"` // 创建时间
StartTime time.Time `json:"startTime"` // 开始时间
Interval time.Duration `json:"-"` // 间隔时间
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
MaxSaveDays int `json:"maxSaveDays"` // 数据库备份历史保留天数,过期将自动删除
Enabled bool `json:"enabled"` // 是否启用
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
LastStatus entity.DbJobStatus `json:"lastStatus"` // 最近一次执行状态
LastResult string `json:"lastResult"` // 最近一次执行结果
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
Name string `json:"name"` // 备份任务名称
}
func (backup *DbBackup) MarshalJSON() ([]byte, error) {
type dbBackup DbBackup
backup.IntervalDay = uint64(backup.Interval / time.Hour / 24)
if len(backup.EnabledDesc) == 0 {
if backup.Enabled {
backup.EnabledDesc = "已启用"
} else {
backup.EnabledDesc = "已禁用"
}
}
return json.Marshal((*dbBackup)(backup))
}
// DbBackupHistory 数据库备份历史
type DbBackupHistory struct {
Id uint64 `json:"id"`
DbBackupId uint64 `json:"dbBackupId"`
CreateTime time.Time `json:"createTime"`
DbName string `json:"dbName"` // 数据库名称
Name string `json:"name"` // 备份历史名称
Id uint64 `json:"id"`
DbBackupId uint64 `json:"dbBackupId"`
CreateTime time.Time `json:"createTime"`
DbName string `json:"dbName"` // 数据库名称
Name string `json:"name"` // 备份历史名称
BinlogFileName string `json:"binlogFileName"`
LastTime timex.NullTime `json:"lastTime" gorm:"-"` // 最近一次恢复时间
LastStatus entity.DbJobStatus `json:"lastStatus" gorm:"-"` // 最近一次恢复状态
LastResult string `json:"lastResult" gorm:"-"` // 最近一次恢复结果
}

View File

@@ -14,6 +14,7 @@ type DbRestore struct {
Interval time.Duration `json:"-"` // 间隔时间
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
Enabled bool `json:"enabled"` // 是否启用
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
LastStatus string `json:"lastStatus"` // 最近一次执行状态
LastResult string `json:"lastResult"` // 最近一次执行结果
@@ -27,6 +28,13 @@ type DbRestore struct {
func (restore *DbRestore) MarshalJSON() ([]byte, error) {
type dbBackup DbRestore
restore.IntervalDay = uint64(restore.Interval / time.Hour / 24)
if len(restore.EnabledDesc) == 0 {
if restore.Enabled {
restore.EnabledDesc = "已启用"
} else {
restore.EnabledDesc = "已禁用"
}
}
return json.Marshal((*dbBackup)(restore))
}

View File

@@ -2,91 +2,53 @@ package application
import (
"fmt"
"mayfly-go/internal/db/domain/repository"
"mayfly-go/internal/db/infrastructure/persistence"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/ioc"
"sync"
)
var (
instanceApp Instance
dbApp Db
dbSqlExecApp DbSqlExec
dbSqlApp DbSql
dbBackupApp *DbBackupApp
dbRestoreApp *DbRestoreApp
dbBinlogApp *DbBinlogApp
dataSyncApp DataSyncTask
)
func InitIoc() {
persistence.Init()
ioc.Register(new(instanceAppImpl), ioc.WithComponentName("DbInstanceApp"))
ioc.Register(new(dbAppImpl), ioc.WithComponentName("DbApp"))
ioc.Register(new(dbSqlExecAppImpl), ioc.WithComponentName("DbSqlExecApp"))
ioc.Register(new(dbSqlAppImpl), ioc.WithComponentName("DbSqlApp"))
ioc.Register(new(dataSyncAppImpl), ioc.WithComponentName("DbDataSyncTaskApp"))
ioc.Register(newDbScheduler(), ioc.WithComponentName("DbScheduler"))
ioc.Register(new(DbBackupApp), ioc.WithComponentName("DbBackupApp"))
ioc.Register(new(DbRestoreApp), ioc.WithComponentName("DbRestoreApp"))
ioc.Register(newDbBinlogApp(), ioc.WithComponentName("DbBinlogApp"))
}
func Init() {
sync.OnceFunc(func() {
repositories := &repository.Repositories{
Instance: persistence.GetInstanceRepo(),
Backup: persistence.NewDbBackupRepo(),
BackupHistory: persistence.NewDbBackupHistoryRepo(),
Restore: persistence.NewDbRestoreRepo(),
RestoreHistory: persistence.NewDbRestoreHistoryRepo(),
Binlog: persistence.NewDbBinlogRepo(),
BinlogHistory: persistence.NewDbBinlogHistoryRepo(),
if err := GetDbBackupApp().Init(); err != nil {
panic(fmt.Sprintf("初始化 DbBackupApp 失败: %v", err))
}
var err error
instanceRepo := persistence.GetInstanceRepo()
instanceApp = newInstanceApp(instanceRepo)
dbApp = newDbApp(persistence.GetDbRepo(), persistence.GetDbSqlRepo(), instanceApp, tagapp.GetTagTreeApp())
dbSqlExecApp = newDbSqlExecApp(persistence.GetDbSqlExecRepo())
dbSqlApp = newDbSqlApp(persistence.GetDbSqlRepo())
dataSyncApp = newDataSyncApp(persistence.GetDataSyncTaskRepo(), persistence.GetDataSyncLogRepo())
scheduler, err := newDbScheduler(repositories)
if err != nil {
panic(fmt.Sprintf("初始化 dbScheduler 失败: %v", err))
if err := GetDbRestoreApp().Init(); err != nil {
panic(fmt.Sprintf("初始化 DbRestoreApp 失败: %v", err))
}
dbBackupApp, err = newDbBackupApp(repositories, dbApp, scheduler)
if err != nil {
panic(fmt.Sprintf("初始化 dbBackupApp 失败: %v", err))
if err := GetDbBinlogApp().Init(); err != nil {
panic(fmt.Sprintf("初始化 DbBinlogApp 失败: %v", err))
}
dbRestoreApp, err = newDbRestoreApp(repositories, dbApp, scheduler)
if err != nil {
panic(fmt.Sprintf("初始化 dbRestoreApp 失败: %v", err))
}
dbBinlogApp, err = newDbBinlogApp(repositories, dbApp, scheduler)
if err != nil {
panic(fmt.Sprintf("初始化 dbBinlogApp 失败: %v", err))
}
dataSyncApp.InitCronJob()
GetDataSyncTaskApp().InitCronJob()
})()
}
func GetInstanceApp() Instance {
return instanceApp
}
func GetDbApp() Db {
return dbApp
}
func GetDbSqlApp() DbSql {
return dbSqlApp
}
func GetDbSqlExecApp() DbSqlExec {
return dbSqlExecApp
}
func GetDbBackupApp() *DbBackupApp {
return dbBackupApp
return ioc.Get[*DbBackupApp]("DbBackupApp")
}
func GetDbRestoreApp() *DbRestoreApp {
return dbRestoreApp
return ioc.Get[*DbRestoreApp]("DbRestoreApp")
}
func GetDbBinlogApp() *DbBinlogApp {
return dbBinlogApp
return ioc.Get[*DbBinlogApp]("DbBinlogApp")
}
func GetDataSyncTaskApp() DataSyncTask {
return dataSyncApp
return ioc.Get[DataSyncTask]("DbDataSyncTaskApp")
}

View File

@@ -40,22 +40,17 @@ type Db interface {
GetDbConnByInstanceId(instanceId uint64) (*dbi.DbConn, error)
}
func newDbApp(dbRepo repository.Db, dbSqlRepo repository.DbSql, dbInstanceApp Instance, tagApp tagapp.TagTree) Db {
app := &dbAppImpl{
dbSqlRepo: dbSqlRepo,
dbInstanceApp: dbInstanceApp,
tagApp: tagApp,
}
app.Repo = dbRepo
return app
}
type dbAppImpl struct {
base.AppImpl[*entity.Db, repository.Db]
dbSqlRepo repository.DbSql
dbInstanceApp Instance
tagApp tagapp.TagTree
dbSqlRepo repository.DbSql `inject:"DbSqlRepo"`
dbInstanceApp Instance `inject:"DbInstanceApp"`
tagApp tagapp.TagTree `inject:"TagTreeApp"`
}
// 注入DbRepo
func (d *dbAppImpl) InjectDbRepo(repo repository.Db) {
d.Repo = repo
}
// 分页获取数据库信息列表
@@ -103,9 +98,13 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db, tagIds ...u
// 比较新旧数据库列表,需要将移除的数据库相关联的信息删除
_, delDb, _ := collx.ArrayCompare(newDbs, oldDbs)
for _, v := range delDb {
// 先简单关闭可能存在的旧库连接可能改了关联标签导致DbConn.Info.TagPath与修改后的标签不一致、导致操作权限校验出错
for _, v := range oldDbs {
// 关闭数据库连接
dbm.CloseDb(dbEntity.Id, v)
}
for _, v := range delDb {
// 删除该库关联的所有sql记录
d.dbSqlRepo.DeleteByCond(ctx, &entity.DbSql{DbId: dbId, Db: v})
}
@@ -155,7 +154,7 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbi.DbConn, error) {
checkDb := dbName
// 兼容pgsql/dm db/schema模式
if dbi.DbTypePostgres.Equal(instance.Type) || dbi.DbTypeDM.Equal(instance.Type) || dbi.DbTypeOracle.Equal(instance.Type) {
if dbi.DbTypePostgres.Equal(instance.Type) || dbi.DbTypeGauss.Equal(instance.Type) || dbi.DbTypeDM.Equal(instance.Type) || dbi.DbTypeOracle.Equal(instance.Type) || dbi.DbTypeMssql.Equal(instance.Type) || dbi.DbTypeKingbaseEs.Equal(instance.Type) || dbi.DbTypeVastbase.Equal(instance.Type) {
ss := strings.Split(dbName, "/")
if len(ss) > 1 {
checkDb = ss[0]

View File

@@ -3,69 +3,217 @@ package application
import (
"context"
"encoding/binary"
"github.com/google/uuid"
"errors"
"fmt"
"gorm.io/gorm"
"math"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/timex"
"sync"
"time"
"github.com/google/uuid"
)
func newDbBackupApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbBackupApp, error) {
var jobs []*entity.DbBackup
if err := repositories.Backup.ListToDo(&jobs); err != nil {
return nil, err
}
if err := scheduler.AddJob(context.Background(), false, entity.DbJobTypeBackup, jobs); err != nil {
return nil, err
}
app := &DbBackupApp{
backupRepo: repositories.Backup,
instanceRepo: repositories.Instance,
backupHistoryRepo: repositories.BackupHistory,
dbApp: dbApp,
scheduler: scheduler,
}
return app, nil
}
const maxBackupHistoryDays = 30
var (
errRestoringBackupHistory = errors.New("正在从备份历史中恢复数据库")
)
type DbBackupApp struct {
backupRepo repository.DbBackup
instanceRepo repository.Instance
backupHistoryRepo repository.DbBackupHistory
dbApp Db
scheduler *dbScheduler
scheduler *dbScheduler `inject:"DbScheduler"`
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
dbApp Db `inject:"DbApp"`
mutex sync.Mutex
closed chan struct{}
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
func (app *DbBackupApp) Init() error {
var jobs []*entity.DbBackup
if err := app.backupRepo.ListToDo(&jobs); err != nil {
return err
}
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
return err
}
app.ctx, app.cancel = context.WithCancel(context.Background())
app.wg.Add(1)
go func() {
defer app.wg.Done()
for app.ctx.Err() == nil {
if err := app.prune(app.ctx); err != nil {
logx.Errorf("清理数据库备份历史失败: %s", err.Error())
timex.SleepWithContext(app.ctx, time.Minute*15)
continue
}
timex.SleepWithContext(app.ctx, time.Hour*24)
}
}()
return nil
}
func (app *DbBackupApp) prune(ctx context.Context) error {
var jobs []*entity.DbBackup
if err := app.backupRepo.ListByCond(map[string]any{}, &jobs); err != nil {
return err
}
for _, job := range jobs {
if ctx.Err() != nil {
return nil
}
var histories []*entity.DbBackupHistory
historyCond := map[string]any{
"db_backup_id": job.Id,
}
if err := app.backupHistoryRepo.ListByCondOrder(historyCond, &histories, "id"); err != nil {
return err
}
expiringTime := time.Now().Add(-math.MaxInt64)
if job.MaxSaveDays > 0 {
expiringTime = time.Now().Add(-time.Hour * 24 * time.Duration(job.MaxSaveDays+1))
}
for _, history := range histories {
if ctx.Err() != nil {
return nil
}
if history.CreateTime.After(expiringTime) {
break
}
err := app.DeleteHistory(ctx, history.Id)
if errors.Is(err, errRestoringBackupHistory) {
break
}
if err != nil {
return err
}
}
}
return nil
}
func (app *DbBackupApp) Close() {
app.scheduler.Close()
if app.cancel != nil {
app.cancel()
app.cancel = nil
}
app.wg.Wait()
}
func (app *DbBackupApp) Create(ctx context.Context, jobs []*entity.DbBackup) error {
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeBackup, jobs)
app.mutex.Lock()
defer app.mutex.Unlock()
if err := app.backupRepo.AddJob(ctx, jobs); err != nil {
return err
}
return app.scheduler.AddJob(ctx, jobs)
}
func (app *DbBackupApp) Update(ctx context.Context, job *entity.DbBackup) error {
return app.scheduler.UpdateJob(ctx, job)
app.mutex.Lock()
defer app.mutex.Unlock()
if err := app.backupRepo.UpdateById(ctx, job); err != nil {
return err
}
_ = app.scheduler.UpdateJob(ctx, job)
return nil
}
func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
// todo: 删除数据库备份历史文件
return app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId)
app.mutex.Lock()
defer app.mutex.Unlock()
if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId); err != nil {
return err
}
history := &entity.DbBackupHistory{
DbBackupId: jobId,
}
err := app.backupHistoryRepo.GetBy(history, "name")
switch {
default:
return err
case err == nil:
return fmt.Errorf("请先删除关联的数据库备份历史【%s】", history.Name)
case errors.Is(err, gorm.ErrRecordNotFound):
}
if err := app.backupRepo.DeleteById(ctx, jobId); err != nil {
return err
}
return nil
}
func (app *DbBackupApp) Enable(ctx context.Context, jobId uint64) error {
return app.scheduler.EnableJob(ctx, entity.DbJobTypeBackup, jobId)
app.mutex.Lock()
defer app.mutex.Unlock()
repo := app.backupRepo
job := &entity.DbBackup{}
if err := repo.GetById(job, jobId); err != nil {
return err
}
if job.IsEnabled() {
return nil
}
if job.IsExpired() {
return errors.New("任务已过期")
}
_ = app.scheduler.EnableJob(ctx, job)
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
logx.Errorf("数据库备份任务已启用( jobId: %d ),任务状态保存失败: %v", jobId, err)
return err
}
return nil
}
func (app *DbBackupApp) Disable(ctx context.Context, jobId uint64) error {
return app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
app.mutex.Lock()
defer app.mutex.Unlock()
repo := app.backupRepo
job := &entity.DbBackup{}
if err := repo.GetById(job, jobId); err != nil {
return err
}
if !job.IsEnabled() {
return nil
}
_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
logx.Errorf("数据库恢复任务已禁用( jobId: %d ),任务状态保存失败: %v", jobId, err)
return err
}
return nil
}
func (app *DbBackupApp) Start(ctx context.Context, jobId uint64) error {
return app.scheduler.StartJobNow(ctx, entity.DbJobTypeBackup, jobId)
func (app *DbBackupApp) StartNow(ctx context.Context, jobId uint64) error {
app.mutex.Lock()
defer app.mutex.Unlock()
job := &entity.DbBackup{}
if err := app.backupRepo.GetById(job, jobId); err != nil {
return err
}
if !job.IsEnabled() {
return errors.New("任务未启用")
}
_ = app.scheduler.StartJobNow(ctx, job)
return nil
}
// GetPageList 分页获取数据库备份任务
func (app *DbBackupApp) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
func (app *DbBackupApp) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return app.backupRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
@@ -76,7 +224,11 @@ func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []str
// GetHistoryPageList 分页获取数据库备份历史
func (app *DbBackupApp) GetHistoryPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return app.backupHistoryRepo.GetHistories(condition, pageParam, toEntity, orderBy...)
return app.backupHistoryRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
func (app *DbBackupApp) GetHistories(backupHistoryIds []uint64, toEntity any) error {
return app.backupHistoryRepo.GetHistories(backupHistoryIds, toEntity)
}
func NewIncUUID() (uuid.UUID, error) {
@@ -99,3 +251,35 @@ func NewIncUUID() (uuid.UUID, error) {
return uid, nil
}
func (app *DbBackupApp) DeleteHistory(ctx context.Context, historyId uint64) (retErr error) {
app.mutex.Lock()
defer app.mutex.Unlock()
if _, err := app.backupHistoryRepo.UpdateDeleting(false, historyId); err != nil {
return err
}
ok, err := app.backupHistoryRepo.UpdateDeleting(true, historyId)
if err != nil {
return err
}
if !ok {
return errRestoringBackupHistory
}
job := &entity.DbBackupHistory{}
if err := app.backupHistoryRepo.GetById(job, historyId); err != nil {
return err
}
conn, err := app.dbApp.GetDbConnByInstanceId(job.DbInstanceId)
if err != nil {
return err
}
dbProgram, err := conn.GetDialect().GetDbProgram()
if err != nil {
return err
}
if err := dbProgram.RemoveBackupHistory(ctx, job.DbBackupId, job.Uuid); err != nil {
return err
}
return app.backupHistoryRepo.DeleteById(ctx, historyId)
}

View File

@@ -2,6 +2,7 @@ package application
import (
"context"
"math"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx"
@@ -11,64 +12,132 @@ import (
)
type DbBinlogApp struct {
binlogRepo repository.DbBinlog
binlogHistoryRepo repository.DbBinlogHistory
backupRepo repository.DbBackup
backupHistoryRepo repository.DbBackupHistory
dbApp Db
context context.Context
cancel context.CancelFunc
waitGroup sync.WaitGroup
scheduler *dbScheduler
scheduler *dbScheduler `inject:"DbScheduler"`
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
instanceRepo repository.Instance `inject:"DbInstanceRepo"`
dbApp Db `inject:"DbApp"`
context context.Context
cancel context.CancelFunc
waitGroup sync.WaitGroup
}
func newDbBinlogApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbBinlogApp, error) {
func newDbBinlogApp() *DbBinlogApp {
ctx, cancel := context.WithCancel(context.Background())
svc := &DbBinlogApp{
binlogRepo: repositories.Binlog,
binlogHistoryRepo: repositories.BinlogHistory,
backupRepo: repositories.Backup,
backupHistoryRepo: repositories.BackupHistory,
dbApp: dbApp,
scheduler: scheduler,
context: ctx,
cancel: cancel,
context: ctx,
cancel: cancel,
}
svc.waitGroup.Add(1)
go svc.run()
return svc, nil
return svc
}
func (app *DbBinlogApp) Init() error {
app.context, app.cancel = context.WithCancel(context.Background())
app.waitGroup.Add(1)
go app.run()
return nil
}
func (app *DbBinlogApp) run() {
defer app.waitGroup.Done()
// todo: 实现 binlog 并发下载
timex.SleepWithContext(app.context, time.Minute)
for !app.closed() {
jobs, err := app.loadJobs()
if err != nil {
logx.Errorf("DbBinlogApp: 加载 BINLOG 同步任务失败: %s", err.Error())
for app.context.Err() == nil {
if err := app.fetchBinlog(app.context); err != nil {
timex.SleepWithContext(app.context, time.Minute)
continue
}
if app.closed() {
break
}
if err := app.scheduler.AddJob(app.context, false, entity.DbJobTypeBinlog, jobs); err != nil {
logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
if err := app.pruneBinlog(app.context); err != nil {
timex.SleepWithContext(app.context, time.Minute)
continue
}
timex.SleepWithContext(app.context, entity.BinlogDownloadInterval)
}
}
func (app *DbBinlogApp) loadJobs() ([]*entity.DbBinlog, error) {
func (app *DbBinlogApp) fetchBinlog(ctx context.Context) error {
jobs, err := app.loadJobs(ctx)
if err != nil {
logx.Errorf("DbBinlogApp: 加载 BINLOG 同步任务失败: %s", err.Error())
timex.SleepWithContext(app.context, time.Minute)
return err
}
if ctx.Err() != nil {
return ctx.Err()
}
if err := app.scheduler.AddJob(app.context, jobs); err != nil {
logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
return err
}
return nil
}
func (app *DbBinlogApp) pruneBinlog(ctx context.Context) error {
var jobs []*entity.DbBinlog
if err := app.binlogRepo.ListByCond(map[string]any{}, &jobs); err != nil {
logx.Error("DbBinlogApp: 获取 BINLOG 同步任务失败: ", err.Error())
return err
}
for _, instance := range jobs {
if ctx.Err() != nil {
return ctx.Err()
}
var histories []*entity.DbBinlogHistory
backupHistory, backupHistoryExists, err := app.backupHistoryRepo.GetEarliestHistoryForBinlog(instance.Id)
if err != nil {
logx.Errorf("DbBinlogApp: 获取数据库备份历史失败: %s", err.Error())
return err
}
var binlogSeq int64 = math.MaxInt64
if backupHistoryExists {
binlogSeq = backupHistory.BinlogSequence
}
if err := app.binlogHistoryRepo.GetHistoriesBeforeSequence(ctx, instance.Id, binlogSeq, &histories); err != nil {
logx.Errorf("DbBinlogApp: 获取数据库 BINLOG 历史失败: %s", err.Error())
return err
}
conn, err := app.dbApp.GetDbConnByInstanceId(instance.Id)
if err != nil {
logx.Errorf("DbBinlogApp: 创建数据库连接失败: %s", err.Error())
return err
}
dbProgram, err := conn.GetDialect().GetDbProgram()
if err != nil {
logx.Errorf("DbBinlogApp: 获取数据库备份与恢复程序失败: %s", err.Error())
return err
}
for i, history := range histories {
// todo: 在避免并发访问的前提下删除本地最新的 BINLOG 文件
if !backupHistoryExists && i == len(histories)-1 {
// 暂不删除本地最新的 BINLOG 文件
break
}
if ctx.Err() != nil {
return ctx.Err()
}
if err := dbProgram.PruneBinlog(history); err != nil {
logx.Errorf("清理 BINLOG 文件失败: %v", err)
continue
}
if err := app.binlogHistoryRepo.DeleteById(ctx, history.Id); err != nil {
logx.Errorf("删除 BINLOG 历史失败: %v", err)
continue
}
}
}
return nil
}
func (app *DbBinlogApp) loadJobs(ctx context.Context) ([]*entity.DbBinlog, error) {
var instanceIds []uint64
if err := app.backupRepo.ListDbInstances(true, true, &instanceIds); err != nil {
return nil, err
}
jobs := make([]*entity.DbBinlog, 0, len(instanceIds))
for _, id := range instanceIds {
if app.closed() {
if ctx.Err() != nil {
break
}
binlog := entity.NewDbBinlog(id)
@@ -81,14 +150,15 @@ func (app *DbBinlogApp) loadJobs() ([]*entity.DbBinlog, error) {
}
func (app *DbBinlogApp) Close() {
app.cancel()
cancel := app.cancel
if cancel == nil {
return
}
app.cancel = nil
cancel()
app.waitGroup.Wait()
}
func (app *DbBinlogApp) closed() bool {
return app.context.Err() != nil
}
func (app *DbBinlogApp) AddJobIfNotExists(ctx context.Context, job *entity.DbBinlog) error {
if err := app.binlogRepo.AddJobIfNotExists(ctx, job); err != nil {
return err
@@ -98,11 +168,3 @@ func (app *DbBinlogApp) AddJobIfNotExists(ctx context.Context, job *entity.DbBin
}
return nil
}
func (app *DbBinlogApp) Delete(ctx context.Context, jobId uint64) error {
// todo: 删除 Binlog 历史文件
if err := app.binlogRepo.DeleteById(ctx, jobId); err != nil {
return err
}
return nil
}

View File

@@ -14,7 +14,12 @@ import (
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/scheduler"
"regexp"
"strconv"
"strings"
"time"
"github.com/google/uuid"
)
type DataSyncTask interface {
@@ -38,17 +43,20 @@ type DataSyncTask interface {
GetTaskLogList(condition *entity.DataSyncLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
}
func newDataSyncApp(dataSyncRepo repository.DataSyncTask, dataSyncLogRepo repository.DataSyncLog) DataSyncTask {
app := new(dataSyncAppImpl)
app.Repo = dataSyncRepo
app.dataSyncLogRepo = dataSyncLogRepo
return app
}
type dataSyncAppImpl struct {
base.AppImpl[*entity.DataSyncTask, repository.DataSyncTask]
dataSyncLogRepo repository.DataSyncLog
dbDataSyncLogRepo repository.DataSyncLog `inject:"DbDataSyncLogRepo"`
dbApp Db `inject:"DbApp"`
}
var (
dateTimeReg = regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`)
)
func (app *dataSyncAppImpl) InjectDbDataSyncTaskRepo(repo repository.DataSyncTask) {
app.Repo = repo
}
func (app *dataSyncAppImpl) GetPageList(condition *entity.DataSyncTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
@@ -58,15 +66,22 @@ func (app *dataSyncAppImpl) GetPageList(condition *entity.DataSyncTaskQuery, pag
func (app *dataSyncAppImpl) Save(ctx context.Context, taskEntity *entity.DataSyncTask) error {
var err error
if taskEntity.Id == 0 {
// 新建时生成key
taskEntity.TaskKey = uuid.New().String()
err = app.Insert(ctx, taskEntity)
} else {
err = app.UpdateById(ctx, taskEntity)
}
if err != nil {
return err
}
app.AddCronJob(taskEntity)
task, err := app.GetById(new(entity.DataSyncTask), taskEntity.Id)
if err != nil {
return err
}
app.AddCronJob(task)
return nil
}
@@ -85,8 +100,10 @@ func (app *dataSyncAppImpl) AddCronJob(taskEntity *entity.DataSyncTask) {
// 根据状态添加新的任务
if taskEntity.Status == entity.DataSyncTaskStatusEnable {
taskId := taskEntity.Id
scheduler.AddFunByKey(key, taskEntity.TaskCron, func() {
if err := app.RunCronJob(taskEntity.Id); err != nil {
logx.Infof("开始执行同步任务: %d", taskId)
if err := app.RunCronJob(taskId); err != nil {
logx.Errorf("定时执行数据同步任务失败: %s", err.Error())
}
})
@@ -126,7 +143,23 @@ func (app *dataSyncAppImpl) RunCronJob(id uint64) error {
updSql := ""
orderSql := ""
if task.UpdFieldVal != "0" && task.UpdFieldVal != "" && task.UpdField != "" {
updSql = fmt.Sprintf("and %s > '%s'", task.UpdField, task.UpdFieldVal)
srcConn, _ := app.dbApp.GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
task.UpdFieldVal = strings.Trim(task.UpdFieldVal, " ")
// 把UpdFieldVal尝试转为int如果可以转为int则不添加引号否则添加引号
if _, err := strconv.Atoi(task.UpdFieldVal); err != nil {
updSql = fmt.Sprintf("and %s > '%s'", task.UpdField, task.UpdFieldVal)
} else {
updSql = fmt.Sprintf("and %s > %s", task.UpdField, task.UpdFieldVal)
}
// 如果是oracle且数据类型是时间类型则需要加上to_date函数
if srcConn.Info.Type == dbi.DbTypeOracle {
// 用正则判断数据类型是时间
if dateTimeReg.MatchString(task.UpdFieldVal) {
updSql = fmt.Sprintf("and %s > to_date('%s','yyyy-mm-dd hh24:mi:ss')", task.UpdField, task.UpdFieldVal)
}
}
orderSql = "order by " + task.UpdField + " asc "
}
// 组装查询sql
@@ -153,13 +186,13 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
}
// 获取源数据库连接
srcConn, err := GetDbApp().GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
srcConn, err := app.dbApp.GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
if err != nil {
return syncLog, errorx.NewBiz("连接源数据库失败: %s", err.Error())
}
// 获取目标数据库连接
targetConn, err := GetDbApp().GetDbConn(uint64(task.TargetDbId), task.TargetDbName)
targetConn, err := app.dbApp.GetDbConn(uint64(task.TargetDbId), task.TargetDbName)
if err != nil {
return syncLog, errorx.NewBiz("连接目标数据库失败: %s", err.Error())
}
@@ -197,8 +230,8 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
// 遍历columns 取task.UpdField的字段类型
updFieldType = dbi.DataTypeString
for _, column := range columns {
if column.Name == task.UpdField {
updFieldType = srcDialect.GetDataType(column.Type)
if strings.EqualFold(strings.ToLower(column.Name), strings.ToLower(task.UpdField)) {
updFieldType = srcDialect.GetDataConverter().GetDataType(column.Type)
break
}
}
@@ -207,7 +240,7 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
total++
result = append(result, row)
if total%batchSize == 0 {
if err := app.srcData2TargetDb(result, fieldMap, updFieldType, task, srcDialect, targetConn, targetDbTx); err != nil {
if err := app.srcData2TargetDb(result, fieldMap, columns, updFieldType, task, srcDialect, targetConn, targetDbTx); err != nil {
return err
}
@@ -229,7 +262,7 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
// 处理剩余的数据
if len(result) > 0 {
if err := app.srcData2TargetDb(result, fieldMap, updFieldType, task, srcDialect, targetConn, targetDbTx); err != nil {
if err := app.srcData2TargetDb(result, fieldMap, queryColumns, updFieldType, task, srcDialect, targetConn, targetDbTx); err != nil {
targetDbTx.Rollback()
return syncLog, err
}
@@ -249,10 +282,16 @@ func (app *dataSyncAppImpl) doDataSync(sql string, task *entity.DataSyncTask) (*
return syncLog, nil
}
func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap []map[string]string, updFieldType dbi.DataType, task *entity.DataSyncTask, srcDialect dbi.Dialect, targetDbConn *dbi.DbConn, targetDbTx *sql.Tx) error {
var data = make([]map[string]any, 0)
func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap []map[string]string, columns []*dbi.QueryColumn, updFieldType dbi.DataType, task *entity.DataSyncTask, srcDialect dbi.Dialect, targetDbConn *dbi.DbConn, targetDbTx *sql.Tx) error {
// 遍历res组装插入sql
// 遍历src字段列表取出字段对应的类型
var srcColumnTypes = make(map[string]string)
for _, column := range columns {
srcColumnTypes[column.Name] = column.Type
}
// 遍历res组装数据
var data = make([]map[string]any, 0)
for _, record := range srcRes {
var rowData = make(map[string]any)
// 遍历字段映射, target字段的值为src字段取值
@@ -265,18 +304,23 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
data = append(data, rowData)
}
// 解决字段大小写问题
updFieldVal := srcRes[len(srcRes)-1][strings.ToUpper(task.UpdField)]
if updFieldVal == "" || updFieldVal == nil {
updFieldVal = srcRes[len(srcRes)-1][strings.ToLower(task.UpdField)]
}
updFieldVal := fmt.Sprintf("%v", srcRes[len(srcRes)-1][task.UpdField])
updFieldVal = srcDialect.FormatStrData(updFieldVal, updFieldType)
task.UpdFieldVal = updFieldVal
task.UpdFieldVal = srcDialect.GetDataConverter().FormatData(updFieldVal, updFieldType)
// 获取目标库字段数组
targetWrapColumns := make([]string, 0)
// 获取源库字段数组
srcColumns := make([]string, 0)
srcFieldTypes := make(map[string]dbi.DataType)
for _, item := range fieldMap {
targetField := item["target"]
srcField := item["target"]
srcFieldTypes[srcField] = srcDialect.GetDataConverter().GetDataType(srcColumnTypes[item["src"]])
targetWrapColumns = append(targetWrapColumns, targetDbConn.Info.Type.QuoteIdentifier(targetField))
srcColumns = append(srcColumns, srcField)
}
@@ -286,7 +330,9 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
for _, record := range data {
rawValue := make([]any, 0)
for _, column := range srcColumns {
rawValue = append(rawValue, record[column])
// 某些情况如oracle需要转换时间类型的字符串为time类型
res := srcDialect.GetDataConverter().ParseData(record[column], srcFieldTypes[column])
rawValue = append(rawValue, res)
}
values = append(values, rawValue)
}
@@ -328,7 +374,7 @@ func (app *dataSyncAppImpl) endRunning(taskEntity *entity.DataSyncTask, log *ent
}
func (app *dataSyncAppImpl) saveLog(log *entity.DataSyncLog) {
app.dataSyncLogRepo.Save(context.Background(), log)
app.dbDataSyncLogRepo.Save(context.Background(), log)
}
func (app *dataSyncAppImpl) InitCronJob() {
@@ -374,5 +420,5 @@ func (app *dataSyncAppImpl) InitCronJob() {
}
func (app *dataSyncAppImpl) GetTaskLogList(condition *entity.DataSyncLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return app.dataSyncLogRepo.GetTaskLogList(condition, pageParam, toEntity, orderBy...)
return app.dbDataSyncLogRepo.GetTaskLogList(condition, pageParam, toEntity, orderBy...)
}

View File

@@ -2,11 +2,14 @@ package application
import (
"context"
"errors"
"gorm.io/gorm"
"mayfly-go/internal/db/dbm"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
)
@@ -30,14 +33,17 @@ type Instance interface {
GetDatabases(entity *entity.DbInstance) ([]string, error)
}
func newInstanceApp(instanceRepo repository.Instance) Instance {
app := new(instanceAppImpl)
app.Repo = instanceRepo
return app
}
type instanceAppImpl struct {
base.AppImpl[*entity.DbInstance, repository.Instance]
dbApp Db `inject:"DbApp"`
backupApp *DbBackupApp `inject:"DbBackupApp"`
restoreApp *DbRestoreApp `inject:"DbRestoreApp"`
}
// 注入DbInstanceRepo
func (app *instanceAppImpl) InjectDbInstanceRepo(repo repository.Instance) {
app.Repo = repo
}
// GetPageList 分页获取数据库实例
@@ -73,9 +79,11 @@ func (app *instanceAppImpl) Save(ctx context.Context, instanceEntity *entity.DbI
err := app.GetBy(oldInstance)
if instanceEntity.Id == 0 {
if instanceEntity.Password == "" {
if instanceEntity.Type != string(dbi.DbTypeSqlite) && instanceEntity.Password == "" {
return errorx.NewBiz("密码不能为空")
}
if err == nil {
return errorx.NewBiz("该数据库实例已存在")
}
@@ -95,8 +103,50 @@ func (app *instanceAppImpl) Save(ctx context.Context, instanceEntity *entity.DbI
return app.UpdateById(ctx, instanceEntity)
}
func (app *instanceAppImpl) Delete(ctx context.Context, id uint64) error {
return app.DeleteById(ctx, id)
func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error {
instance, err := app.GetById(new(entity.DbInstance), instanceId, "name")
biz.ErrIsNil(err, "获取数据库实例错误数据库实例ID为: %d", instance.Id)
restore := &entity.DbRestore{
DbInstanceId: instanceId,
}
err = app.restoreApp.restoreRepo.GetBy(restore)
switch {
case err == nil:
biz.ErrNotNil(err, "不能删除数据库实例【%s】请先删除关联的数据库恢复任务。", instance.Name)
case errors.Is(err, gorm.ErrRecordNotFound):
break
default:
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
}
backup := &entity.DbBackup{
DbInstanceId: instanceId,
}
err = app.backupApp.backupRepo.GetBy(backup)
switch {
case err == nil:
biz.ErrNotNil(err, "不能删除数据库实例【%s】请先删除关联的数据库备份任务。", instance.Name)
case errors.Is(err, gorm.ErrRecordNotFound):
break
default:
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
}
db := &entity.Db{
InstanceId: instanceId,
}
err = app.dbApp.GetBy(db)
switch {
case err == nil:
biz.ErrNotNil(err, "不能删除数据库实例【%s】请先删除关联的数据库资源。", instance.Name)
case errors.Is(err, gorm.ErrRecordNotFound):
break
default:
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
}
return app.DeleteById(ctx, instanceId)
}
func (app *instanceAppImpl) GetDatabases(ed *entity.DbInstance) ([]string, error) {

View File

@@ -2,71 +2,130 @@ package application
import (
"context"
"errors"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"sync"
)
func newDbRestoreApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbRestoreApp, error) {
var jobs []*entity.DbRestore
if err := repositories.Restore.ListToDo(&jobs); err != nil {
return nil, err
}
if err := scheduler.AddJob(context.Background(), false, entity.DbJobTypeRestore, jobs); err != nil {
return nil, err
}
app := &DbRestoreApp{
restoreRepo: repositories.Restore,
instanceRepo: repositories.Instance,
backupHistoryRepo: repositories.BackupHistory,
restoreHistoryRepo: repositories.RestoreHistory,
binlogHistoryRepo: repositories.BinlogHistory,
dbApp: dbApp,
scheduler: scheduler,
}
return app, nil
type DbRestoreApp struct {
scheduler *dbScheduler `inject:"DbScheduler"`
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
mutex sync.Mutex
}
type DbRestoreApp struct {
restoreRepo repository.DbRestore
instanceRepo repository.Instance
backupHistoryRepo repository.DbBackupHistory
restoreHistoryRepo repository.DbRestoreHistory
binlogHistoryRepo repository.DbBinlogHistory
dbApp Db
scheduler *dbScheduler
func (app *DbRestoreApp) Init() error {
var jobs []*entity.DbRestore
if err := app.restoreRepo.ListToDo(&jobs); err != nil {
return err
}
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
return err
}
return nil
}
func (app *DbRestoreApp) Close() {
app.scheduler.Close()
}
func (app *DbRestoreApp) Create(ctx context.Context, job *entity.DbRestore) error {
return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeRestore, job)
func (app *DbRestoreApp) Create(ctx context.Context, jobs any) error {
app.mutex.Lock()
defer app.mutex.Unlock()
if err := app.restoreRepo.AddJob(ctx, jobs); err != nil {
return err
}
_ = app.scheduler.AddJob(ctx, jobs)
return nil
}
func (app *DbRestoreApp) Update(ctx context.Context, job *entity.DbRestore) error {
return app.scheduler.UpdateJob(ctx, job)
app.mutex.Lock()
defer app.mutex.Unlock()
if err := app.restoreRepo.UpdateById(ctx, job); err != nil {
return err
}
_ = app.scheduler.UpdateJob(ctx, job)
return nil
}
func (app *DbRestoreApp) Delete(ctx context.Context, jobId uint64) error {
// todo: 删除数据库恢复历史文件
return app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId)
app.mutex.Lock()
defer app.mutex.Unlock()
if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId); err != nil {
return err
}
history := &entity.DbRestoreHistory{
DbRestoreId: jobId,
}
if err := app.restoreHistoryRepo.DeleteByCond(ctx, history); err != nil {
return err
}
if err := app.restoreRepo.DeleteById(ctx, jobId); err != nil {
return err
}
return nil
}
func (app *DbRestoreApp) Enable(ctx context.Context, jobId uint64) error {
return app.scheduler.EnableJob(ctx, entity.DbJobTypeRestore, jobId)
app.mutex.Lock()
defer app.mutex.Unlock()
repo := app.restoreRepo
job := &entity.DbRestore{}
if err := repo.GetById(job, jobId); err != nil {
return err
}
if job.IsEnabled() {
return nil
}
if job.IsExpired() {
return errors.New("任务已过期")
}
_ = app.scheduler.EnableJob(ctx, job)
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
logx.Errorf("数据库恢复任务已启用( jobId: %d ),任务状态保存失败: %v", jobId, err)
return err
}
return nil
}
func (app *DbRestoreApp) Disable(ctx context.Context, jobId uint64) error {
return app.scheduler.DisableJob(ctx, entity.DbJobTypeRestore, jobId)
app.mutex.Lock()
defer app.mutex.Unlock()
repo := app.restoreRepo
job := &entity.DbRestore{}
if err := repo.GetById(job, jobId); err != nil {
return err
}
if !job.IsEnabled() {
return nil
}
_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeRestore, jobId)
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
logx.Errorf("数据库恢复任务已禁用( jobId: %d ),任务状态保存失败: %v", jobId, err)
return err
}
return nil
}
// GetPageList 分页获取数据库恢复任务
func (app *DbRestoreApp) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
func (app *DbRestoreApp) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return app.restoreRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
// GetRestoresEnabled 获取数据库恢复任务
func (app *DbRestoreApp) GetRestoresEnabled(toEntity any, backupHistoryId ...uint64) error {
return app.restoreRepo.GetEnabledRestores(toEntity, backupHistoryId...)
}
// GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames)

View File

@@ -4,12 +4,14 @@ import (
"context"
"errors"
"fmt"
"golang.org/x/sync/singleflight"
"gorm.io/gorm"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/runner"
"reflect"
"strconv"
"sync"
"time"
)
@@ -21,58 +23,35 @@ const (
type dbScheduler struct {
mutex sync.Mutex
runner *runner.Runner[entity.DbJob]
dbApp Db
backupRepo repository.DbBackup
backupHistoryRepo repository.DbBackupHistory
restoreRepo repository.DbRestore
restoreHistoryRepo repository.DbRestoreHistory
binlogRepo repository.DbBinlog
binlogHistoryRepo repository.DbBinlogHistory
binlogTimes map[uint64]time.Time
dbApp Db `inject:"DbApp"`
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
sfGroup singleflight.Group
}
func newDbScheduler(repositories *repository.Repositories) (*dbScheduler, error) {
scheduler := &dbScheduler{
dbApp: dbApp,
backupRepo: repositories.Backup,
backupHistoryRepo: repositories.BackupHistory,
restoreRepo: repositories.Restore,
restoreHistoryRepo: repositories.RestoreHistory,
binlogRepo: repositories.Binlog,
binlogHistoryRepo: repositories.BinlogHistory,
}
func newDbScheduler() *dbScheduler {
scheduler := &dbScheduler{}
scheduler.runner = runner.NewRunner[entity.DbJob](maxRunning, scheduler.runJob,
runner.WithScheduleJob[entity.DbJob](scheduler.scheduleJob),
runner.WithRunnableJob[entity.DbJob](scheduler.runnableJob),
runner.WithUpdateJob[entity.DbJob](scheduler.updateJob),
)
return scheduler, nil
return scheduler
}
func (s *dbScheduler) scheduleJob(job entity.DbJob) (time.Time, error) {
return job.Schedule()
}
func (s *dbScheduler) repo(typ entity.DbJobType) repository.DbJob {
switch typ {
case entity.DbJobTypeBackup:
return s.backupRepo
case entity.DbJobTypeRestore:
return s.restoreRepo
case entity.DbJobTypeBinlog:
return s.binlogRepo
default:
panic(errors.New(fmt.Sprintf("无效的数据库任务类型: %v", typ)))
}
}
func (s *dbScheduler) UpdateJob(ctx context.Context, job entity.DbJob) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if err := s.repo(job.GetJobType()).UpdateById(ctx, job); err != nil {
return err
}
_ = s.runner.UpdateOrAdd(ctx, job)
_ = s.runner.Update(ctx, job)
return nil
}
@@ -80,61 +59,39 @@ func (s *dbScheduler) Close() {
s.runner.Close()
}
func (s *dbScheduler) AddJob(ctx context.Context, saving bool, jobType entity.DbJobType, jobs any) error {
func (s *dbScheduler) AddJob(ctx context.Context, jobs any) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if saving {
if err := s.repo(jobType).AddJob(ctx, jobs); err != nil {
return err
}
}
reflectValue := reflect.ValueOf(jobs)
switch reflectValue.Kind() {
case reflect.Array, reflect.Slice:
reflectLen := reflectValue.Len()
for i := 0; i < reflectLen; i++ {
job := reflectValue.Index(i).Interface().(entity.DbJob)
job.SetJobType(jobType)
_ = s.runner.Add(ctx, job)
}
default:
job := jobs.(entity.DbJob)
job.SetJobType(jobType)
_ = s.runner.Add(ctx, job)
}
return nil
}
func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
// todo: 删除数据库备份历史文件
s.mutex.Lock()
defer s.mutex.Unlock()
if err := s.repo(jobType).DeleteById(ctx, jobId); err != nil {
if err := s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId)); err != nil {
return err
}
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
return nil
}
func (s *dbScheduler) EnableJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
func (s *dbScheduler) EnableJob(ctx context.Context, job entity.DbJob) error {
s.mutex.Lock()
defer s.mutex.Unlock()
repo := s.repo(jobType)
job := entity.NewDbJob(jobType)
if err := repo.GetById(job, jobId); err != nil {
return err
}
if job.IsEnabled() {
return nil
}
job.SetEnabled(true)
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
return err
}
_ = s.runner.Add(ctx, job)
return nil
}
@@ -143,61 +100,37 @@ func (s *dbScheduler) DisableJob(ctx context.Context, jobType entity.DbJobType,
s.mutex.Lock()
defer s.mutex.Unlock()
repo := s.repo(jobType)
job := entity.NewDbJob(jobType)
if err := repo.GetById(job, jobId); err != nil {
return err
}
if !job.IsEnabled() {
return nil
}
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
return err
}
_ = s.runner.Remove(ctx, job.GetKey())
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
return nil
}
func (s *dbScheduler) StartJobNow(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
func (s *dbScheduler) StartJobNow(ctx context.Context, job entity.DbJob) error {
s.mutex.Lock()
defer s.mutex.Unlock()
job := entity.NewDbJob(jobType)
if err := s.repo(jobType).GetById(job, jobId); err != nil {
return err
}
if !job.IsEnabled() {
return errors.New("任务未启用")
}
_ = s.runner.StartNow(ctx, job)
return nil
}
func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
func (s *dbScheduler) backup(ctx context.Context, dbProgram dbi.DbProgram, backup *entity.DbBackup) error {
id, err := NewIncUUID()
if err != nil {
return err
}
backup := job.(*entity.DbBackup)
history := &entity.DbBackupHistory{
Uuid: id.String(),
DbBackupId: backup.Id,
DbInstanceId: backup.DbInstanceId,
DbName: backup.DbName,
}
conn, err := s.dbApp.GetDbConnByInstanceId(backup.DbInstanceId)
if err != nil {
return err
}
dbProgram := conn.GetDialect().GetDbProgram()
binlogInfo, err := dbProgram.Backup(ctx, history)
if err != nil {
return err
}
now := time.Now()
name := backup.Name
if len(name) == 0 {
name = backup.DbName
name := backup.DbName
if len(backup.Name) > 0 {
name = fmt.Sprintf("%s-%s", backup.DbName, backup.Name)
}
history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime))
history.CreateTime = now
@@ -211,43 +144,43 @@ func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
return nil
}
func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error {
restore := job.(*entity.DbRestore)
conn, err := s.dbApp.GetDbConnByInstanceId(restore.DbInstanceId)
if err != nil {
return err
func (s *dbScheduler) singleFlightFetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, targetTime time.Time) error {
key := strconv.FormatUint(instanceId, 10)
for ctx.Err() == nil {
c := s.sfGroup.DoChan(key, func() (interface{}, error) {
if err := s.fetchBinlog(ctx, dbProgram, instanceId, true, targetTime); err != nil {
return targetTime, err
}
return targetTime, nil
})
select {
case res := <-c:
if targetTime.Compare(res.Val.(time.Time)) <= 0 {
return res.Err
}
case <-ctx.Done():
}
}
dbProgram := conn.GetDialect().GetDbProgram()
return ctx.Err()
}
func (s *dbScheduler) restore(ctx context.Context, dbProgram dbi.DbProgram, restore *entity.DbRestore) error {
if restore.PointInTime.Valid {
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(restore.DbInstanceId)
if err != nil {
return err
}
if ok {
latestBinlogSequence = binlogHistory.Sequence
} else {
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(restore.DbInstanceId)
if err != nil {
return err
}
if !ok {
return nil
}
earliestBackupSequence = backupHistory.BinlogSequence
}
binlogFiles, err := dbProgram.FetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence)
if err != nil {
return err
}
if err := s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, restore.DbInstanceId, binlogFiles); err != nil {
if err := s.fetchBinlog(ctx, dbProgram, restore.DbInstanceId, true, restore.PointInTime.Time); err != nil {
return err
}
if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
return err
}
} else {
if err := s.restoreBackupHistory(ctx, dbProgram, restore); err != nil {
backupHistory := &entity.DbBackupHistory{}
if err := s.backupHistoryRepo.GetById(backupHistory, restore.DbBackupHistoryId); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
err = errors.New("备份历史已删除")
}
return err
}
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
return err
}
}
@@ -262,76 +195,74 @@ func (s *dbScheduler) restoreMysql(ctx context.Context, job entity.DbJob) error
return nil
}
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) {
job.SetLastStatus(entity.DbJobRunning, nil)
if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
logx.Errorf("failed to update job status: %v", err)
return
}
var errRun error
switch typ := job.GetJobType(); typ {
case entity.DbJobTypeBackup:
errRun = s.backupMysql(ctx, job)
case entity.DbJobTypeRestore:
errRun = s.restoreMysql(ctx, job)
case entity.DbJobTypeBinlog:
errRun = s.fetchBinlogMysql(ctx, job)
func (s *dbScheduler) updateJob(ctx context.Context, job entity.DbJob) error {
switch t := job.(type) {
case *entity.DbBackup:
return s.backupRepo.UpdateById(ctx, t)
case *entity.DbRestore:
return s.restoreRepo.UpdateById(ctx, t)
case *entity.DbBinlog:
return s.binlogRepo.UpdateById(ctx, t)
default:
errRun = errors.New(fmt.Sprintf("无效的数据库任务类型: %v", typ))
}
status := entity.DbJobSuccess
if errRun != nil {
status = entity.DbJobFailed
}
job.SetLastStatus(status, errRun)
if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
logx.Errorf("failed to update job status: %v", err)
return
return fmt.Errorf("无效的数据库任务类型: %T", t)
}
}
func (s *dbScheduler) runnableJob(job entity.DbJob, next runner.NextJobFunc[entity.DbJob]) bool {
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) error {
conn, err := s.dbApp.GetDbConnByInstanceId(job.GetInstanceId())
if err != nil {
return err
}
dbProgram, err := conn.GetDialect().GetDbProgram()
if err != nil {
return err
}
switch t := job.(type) {
case *entity.DbBackup:
return s.backup(ctx, dbProgram, t)
case *entity.DbRestore:
return s.restore(ctx, dbProgram, t)
case *entity.DbBinlog:
return s.fetchBinlog(ctx, dbProgram, t.DbInstanceId, false, time.Now())
default:
return fmt.Errorf("无效的数据库任务类型: %T", t)
}
}
func (s *dbScheduler) runnableJob(job entity.DbJob, nextRunning runner.NextJobFunc[entity.DbJob]) (bool, error) {
if job.IsExpired() {
return false, runner.ErrJobExpired
}
const maxCountByInstanceId = 4
const maxCountByDbName = 1
var countByInstanceId, countByDbName int
jobBase := job.GetJobBase()
for item, ok := next(); ok; item, ok = next() {
itemBase := item.GetJobBase()
if jobBase.DbInstanceId == itemBase.DbInstanceId {
for item, ok := nextRunning(); ok; item, ok = nextRunning() {
if job.GetInstanceId() == item.GetInstanceId() {
countByInstanceId++
if countByInstanceId >= maxCountByInstanceId {
return false
return false, nil
}
if relatedToBinlog(job.GetJobType()) {
// todo: 恢复数据库前触发 BINLOG 同步BINLOG 同步完成后才能恢复数据库
if relatedToBinlog(item.GetJobType()) {
return false
}
}
if job.GetDbName() == item.GetDbName() {
countByDbName++
if countByDbName >= maxCountByDbName {
return false
return false, nil
}
}
if (job.GetJobType() == entity.DbJobTypeBinlog && item.GetJobType() == entity.DbJobTypeRestore) ||
(job.GetJobType() == entity.DbJobTypeRestore && item.GetJobType() == entity.DbJobTypeBinlog) {
return false, nil
}
}
}
return true
return true, nil
}
func relatedToBinlog(typ entity.DbJobType) bool {
return typ == entity.DbJobTypeRestore || typ == entity.DbJobTypeBinlog
}
func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProgram, job *entity.DbRestore) error {
func (s *dbScheduler) restorePointInTime(ctx context.Context, dbProgram dbi.DbProgram, job *entity.DbRestore) error {
binlogHistory, err := s.binlogHistoryRepo.GetHistoryByTime(job.DbInstanceId, job.PointInTime.Time)
if err != nil {
return err
}
position, err := program.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, job.PointInTime.Time)
position, err := dbProgram.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, job.PointInTime.Time)
if err != nil {
return err
}
@@ -340,7 +271,7 @@ func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProg
Sequence: binlogHistory.Sequence,
Position: position,
}
backupHistory, err := s.backupHistoryRepo.GetLatestHistory(job.DbInstanceId, job.DbName, target)
backupHistory, err := s.backupHistoryRepo.GetLatestHistoryForBinlog(job.DbInstanceId, job.DbName, target)
if err != nil {
return err
}
@@ -360,31 +291,77 @@ func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbi.DbProg
TargetPosition: target.Position,
TargetTime: job.PointInTime.Time,
}
if err := program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid); err != nil {
if err := dbProgram.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo); err != nil {
return err
}
return program.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo)
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
return err
}
// 由于 ReplayBinlog 未记录 BINLOG 事件,系统自动备份,避免数据丢失
backup := &entity.DbBackup{
DbInstanceId: backupHistory.DbInstanceId,
DbName: backupHistory.DbName,
Enabled: true,
Repeated: false,
StartTime: time.Now(),
Interval: 0,
Name: "系统备份",
}
backup.Id = backupHistory.DbBackupId
if err := s.backup(ctx, dbProgram, backup); err != nil {
return err
}
return nil
}
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, job *entity.DbRestore) error {
backupHistory := &entity.DbBackupHistory{}
if err := s.backupHistoryRepo.GetById(backupHistory, job.DbBackupHistoryId); err != nil {
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, backupHistory *entity.DbBackupHistory) (retErr error) {
if _, err := s.backupHistoryRepo.UpdateRestoring(false, backupHistory.Id); err != nil {
return err
}
ok, err := s.backupHistoryRepo.UpdateRestoring(true, backupHistory.Id)
if err != nil {
return err
}
defer func() {
_, err = s.backupHistoryRepo.UpdateRestoring(false, backupHistory.Id)
if err == nil {
return
}
if retErr == nil {
retErr = err
return
}
retErr = fmt.Errorf("%w, %w", retErr, err)
}()
if !ok {
return errors.New("关联的数据库备份历史已删除")
}
return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
}
func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob) error {
instanceId := backup.GetJobBase().DbInstanceId
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
func (s *dbScheduler) fetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, downloadLatestBinlogFile bool, targetTime time.Time) error {
if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
return err
} else if !enabled {
return errors.New("数据库未启用 BINLOG")
}
if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
return err
} else if !enabled {
return errors.New("数据库未启用 BINLOG 行模式")
}
earliestBackupSequence := int64(-1)
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(instanceId)
if err != nil {
return err
}
if ok {
latestBinlogSequence = binlogHistory.Sequence
} else {
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(instanceId)
if downloadLatestBinlogFile && targetTime.Before(binlogHistory.LastEventTime) {
return nil
}
if !ok {
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistoryForBinlog(instanceId)
if err != nil {
return err
}
@@ -393,14 +370,11 @@ func (s *dbScheduler) fetchBinlogMysql(ctx context.Context, backup entity.DbJob)
}
earliestBackupSequence = backupHistory.BinlogSequence
}
conn, err := s.dbApp.GetDbConnByInstanceId(instanceId)
// todo: 将循环从 dbProgram.FetchBinlogs 中提取出来,实现 BINLOG 同步成功后逐一保存 binlogHistory
binlogFiles, err := dbProgram.FetchBinlogs(ctx, downloadLatestBinlogFile, earliestBackupSequence, binlogHistory)
if err != nil {
return err
}
dbProgram := conn.GetDialect().GetDbProgram()
binlogFiles, err := dbProgram.FetchBinlogs(ctx, false, earliestBackupSequence, latestBinlogSequence)
if err == nil {
err = s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, instanceId, binlogFiles)
}
return nil
return s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, instanceId, binlogFiles)
}

View File

@@ -14,8 +14,7 @@ type dbSqlAppImpl struct {
base.AppImpl[*entity.DbSql, repository.DbSql]
}
func newDbSqlApp(dbSqlRepo repository.DbSql) DbSql {
app := new(dbSqlAppImpl)
app.Repo = dbSqlRepo
return app
// 注入DbSqlRepo
func (d *dbSqlAppImpl) InjectDbSqlRepo(repo repository.DbSql) {
d.Repo = repo
}

Some files were not shown because too many files have changed in this diff Show More