mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-14 05:10:24 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d711a36749 | ||
|
|
9dbf104ef1 | ||
|
|
20eb06fb28 | ||
|
|
9c20bdef39 | ||
|
|
3fdd98a390 | ||
|
|
d4f456c0cf | ||
|
|
f2b6e15cf4 | ||
|
|
6be0ea6aed | ||
|
|
eee08be2cc |
@@ -17,7 +17,7 @@
|
|||||||
"countup.js": "^2.7.0",
|
"countup.js": "^2.7.0",
|
||||||
"cropperjs": "^1.5.11",
|
"cropperjs": "^1.5.11",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"element-plus": "^2.5.3",
|
"element-plus": "^2.5.5",
|
||||||
"js-base64": "^3.7.5",
|
"js-base64": "^3.7.5",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -88,6 +88,20 @@
|
|||||||
"font_class": "gauss",
|
"font_class": "gauss",
|
||||||
"unicode": "e683",
|
"unicode": "e683",
|
||||||
"unicode_decimal": 59011
|
"unicode_decimal": 59011
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "34836637",
|
||||||
|
"name": "kingbase",
|
||||||
|
"font_class": "kingbase",
|
||||||
|
"unicode": "e882",
|
||||||
|
"unicode_decimal": 59522
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "33047500",
|
||||||
|
"name": "vastbase",
|
||||||
|
"font_class": "vastbase",
|
||||||
|
"unicode": "e62b",
|
||||||
|
"unicode_decimal": 58923
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const config = {
|
|||||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||||
|
|
||||||
// 系统版本
|
// 系统版本
|
||||||
version: 'v1.7.2',
|
version: 'v1.7.3',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ const open = (optionProps: MonacoEditorDialogProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editorRef.value?.format();
|
|
||||||
editorRef.value?.focus();
|
editorRef.value?.focus();
|
||||||
|
editorRef.value?.format();
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
state.dialogVisible = true;
|
state.dialogVisible = true;
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ const emit = defineEmits(['update:queryForm', 'update:selectionData', 'pageChang
|
|||||||
|
|
||||||
export interface PageTableProps {
|
export interface PageTableProps {
|
||||||
size?: string;
|
size?: string;
|
||||||
pageApi: Api; // 请求表格数据的 api
|
pageApi?: Api; // 请求表格数据的 api
|
||||||
columns: TableColumn[]; // 列配置项 ==> 必传
|
columns: TableColumn[]; // 列配置项 ==> 必传
|
||||||
showSelection?: boolean;
|
showSelection?: boolean;
|
||||||
selectable?: (row: any) => boolean; // 是否可选
|
selectable?: (row: any) => boolean; // 是否可选
|
||||||
@@ -257,7 +257,7 @@ const changeSimpleFormItem = (searchItem: SearchItem) => {
|
|||||||
nowSearchItem.value = searchItem;
|
nowSearchItem.value = searchItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
||||||
props.pageable,
|
props.pageable,
|
||||||
props.pageApi,
|
props.pageApi,
|
||||||
queryForm,
|
queryForm,
|
||||||
@@ -288,6 +288,13 @@ watch(isShowSearch, () => {
|
|||||||
calcuTableHeight();
|
calcuTableHeight();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(newValue: any) => {
|
||||||
|
tableData = newValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
calcuTableHeight();
|
calcuTableHeight();
|
||||||
useEventListener(window, 'resize', calcuTableHeight);
|
useEventListener(window, 'resize', calcuTableHeight);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import 'xterm/css/xterm.css';
|
import 'xterm/css/xterm.css';
|
||||||
import { Terminal } from 'xterm';
|
import { ITheme, Terminal } from 'xterm';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
import { SearchAddon } from 'xterm-addon-search';
|
import { SearchAddon } from 'xterm-addon-search';
|
||||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||||
@@ -92,12 +92,13 @@ function init() {
|
|||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
disableStdin: false,
|
disableStdin: false,
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
|
fastScrollModifier: 'ctrl',
|
||||||
theme: {
|
theme: {
|
||||||
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
||||||
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
||||||
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
||||||
// cursorAccent: "red", // 光标停止颜色
|
// cursorAccent: "red", // 光标停止颜色
|
||||||
} as any,
|
} as ITheme,
|
||||||
});
|
});
|
||||||
term.open(terminalRef.value);
|
term.open(terminalRef.value);
|
||||||
|
|
||||||
@@ -105,7 +106,7 @@ function init() {
|
|||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
state.addon.fit = fitAddon;
|
state.addon.fit = fitAddon;
|
||||||
term.loadAddon(fitAddon);
|
term.loadAddon(fitAddon);
|
||||||
fitTerminal();
|
resize();
|
||||||
|
|
||||||
// 注册搜索组件
|
// 注册搜索组件
|
||||||
const searchAddon = new SearchAddon();
|
const searchAddon = new SearchAddon();
|
||||||
@@ -146,7 +147,7 @@ const onConnected = () => {
|
|||||||
state.status = TerminalStatus.Connected;
|
state.status = TerminalStatus.Connected;
|
||||||
|
|
||||||
// 注册窗口大小监听器
|
// 注册窗口大小监听器
|
||||||
useEventListener('resize', debounce(fitTerminal, 400));
|
useEventListener('resize', debounce(resize, 400));
|
||||||
|
|
||||||
focus();
|
focus();
|
||||||
|
|
||||||
@@ -158,17 +159,11 @@ const onConnected = () => {
|
|||||||
|
|
||||||
// 自适应终端
|
// 自适应终端
|
||||||
const fitTerminal = () => {
|
const fitTerminal = () => {
|
||||||
const dimensions = state.addon.fit && state.addon.fit.proposeDimensions();
|
resize();
|
||||||
if (!dimensions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (dimensions?.cols && dimensions?.rows) {
|
|
||||||
term.resize(dimensions.cols, dimensions.rows);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const focus = () => {
|
const focus = () => {
|
||||||
setTimeout(() => term.focus(), 400);
|
setTimeout(() => term.focus(), 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
@@ -265,7 +260,13 @@ const getStatus = (): TerminalStatus => {
|
|||||||
return state.status;
|
return state.status;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({ init, fitTerminal, focus, clear, close, getStatus });
|
const resize = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
state.addon.fit.fit();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize, resize });
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#terminal-body {
|
#terminal-body {
|
||||||
|
|||||||
@@ -259,6 +259,10 @@ defineExpose({
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-dialog {
|
||||||
|
padding: 1px 1px;
|
||||||
|
}
|
||||||
|
|
||||||
// 取消body最大高度,否则全屏有问题
|
// 取消body最大高度,否则全屏有问题
|
||||||
.el-dialog__body {
|
.el-dialog__body {
|
||||||
max-height: 100% !important;
|
max-height: 100% !important;
|
||||||
|
|||||||
@@ -615,6 +615,9 @@ const setLocalThemeConfigStyle = () => {
|
|||||||
};
|
};
|
||||||
// 一键复制配置
|
// 一键复制配置
|
||||||
const onCopyConfigClick = (target: any) => {
|
const onCopyConfigClick = (target: any) => {
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let copyThemeConfig = getLocal('themeConfig');
|
let copyThemeConfig = getLocal('themeConfig');
|
||||||
copyThemeConfig.isDrawer = false;
|
copyThemeConfig.isDrawer = false;
|
||||||
const clipboard = new ClipboardJS(target, {
|
const clipboard = new ClipboardJS(target, {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
@node-contextmenu="nodeContextmenu"
|
@node-contextmenu="nodeContextmenu"
|
||||||
>
|
>
|
||||||
<template #default="{ node, data }">
|
<template #default="{ node, data }">
|
||||||
<span>
|
<span @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''">
|
||||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||||
<tag-info :tag-path="data.label" />
|
<tag-info :tag-path="data.label" />
|
||||||
</span>
|
</span>
|
||||||
@@ -25,7 +25,13 @@
|
|||||||
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
||||||
|
|
||||||
<span class="ml3" :title="data.labelRemark">
|
<span class="ml3" :title="data.labelRemark">
|
||||||
<slot name="label" :data="data"> {{ data.label }}</slot>
|
<slot name="label" :data="data" v-if="!data.disabled"> {{ data.label }}</slot>
|
||||||
|
<!-- 禁用状态 -->
|
||||||
|
<slot name="disabledLabel" :data="data" v-else>
|
||||||
|
<el-link type="danger" disabled :underline="false">
|
||||||
|
{{ `${data.label}` }}
|
||||||
|
</el-link>
|
||||||
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<slot :node="node" :data="data" name="suffix"></slot>
|
<slot :node="node" :data="data" name="suffix"></slot>
|
||||||
@@ -135,15 +141,29 @@ const loadNode = async (node: any, resolve: any) => {
|
|||||||
|
|
||||||
const treeNodeClick = (data: any) => {
|
const treeNodeClick = (data: any) => {
|
||||||
emit('nodeClick', data);
|
emit('nodeClick', data);
|
||||||
if (data.type.nodeClickFunc) {
|
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
||||||
data.type.nodeClickFunc(data);
|
data.type.nodeClickFunc(data);
|
||||||
}
|
}
|
||||||
// 关闭可能存在的右击菜单
|
// 关闭可能存在的右击菜单
|
||||||
contextmenuRef.value.closeContextmenu();
|
contextmenuRef.value.closeContextmenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 树节点双击事件
|
||||||
|
const treeNodeDblclick = (data: any) => {
|
||||||
|
// emit('nodeDblick', data);
|
||||||
|
if (!data.disabled && data.type.nodeDblclickFunc) {
|
||||||
|
data.type.nodeDblclickFunc(data);
|
||||||
|
}
|
||||||
|
// 关闭可能存在的右击菜单
|
||||||
|
contextmenuRef.value.closeContextmenu();
|
||||||
|
};
|
||||||
|
|
||||||
// 树节点右击事件
|
// 树节点右击事件
|
||||||
const nodeContextmenu = (event: any, data: any) => {
|
const nodeContextmenu = (event: any, data: any) => {
|
||||||
|
if (data.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 加载当前节点是否需要显示右击菜单
|
// 加载当前节点是否需要显示右击菜单
|
||||||
let items = data.type.contextMenuItems;
|
let items = data.type.contextMenuItems;
|
||||||
if (!items || items.length == 0) {
|
if (!items || items.length == 0) {
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
@change="changeNode"
|
@change="changeNode"
|
||||||
>
|
>
|
||||||
|
<template #prefix="{ node, data }">
|
||||||
|
<slot name="iconPrefix" :node="node" :data="data" />
|
||||||
|
</template>
|
||||||
<template #default="{ node, data }">
|
<template #default="{ node, data }">
|
||||||
<span>
|
<span>
|
||||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||||
@@ -33,7 +36,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, ref, watch, toRefs } from 'vue';
|
import { onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { NodeType, TagTreeNode } from './tag';
|
import { NodeType, TagTreeNode } from './tag';
|
||||||
import TagInfo from './TagInfo.vue';
|
import TagInfo from './TagInfo.vue';
|
||||||
import { tagApi } from '../tag/api';
|
import { tagApi } from '../tag/api';
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ export class TagTreeNode {
|
|||||||
*/
|
*/
|
||||||
isLeaf: boolean = false;
|
isLeaf: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否禁用状态
|
||||||
|
*/
|
||||||
|
disabled: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 额外需要传递的参数
|
* 额外需要传递的参数
|
||||||
*/
|
*/
|
||||||
@@ -53,6 +58,11 @@ export class TagTreeNode {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withDisabled(disabled: boolean) {
|
||||||
|
this.disabled = disabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
withParams(params: any) {
|
withParams(params: any) {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
return this;
|
return this;
|
||||||
@@ -91,8 +101,14 @@ export class NodeType {
|
|||||||
|
|
||||||
loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
|
loadNodesFunc: (parentNode: TagTreeNode) => Promise<TagTreeNode[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点点击事件
|
||||||
|
*/
|
||||||
nodeClickFunc: (node: TagTreeNode) => void;
|
nodeClickFunc: (node: TagTreeNode) => void;
|
||||||
|
|
||||||
|
// 节点双击事件
|
||||||
|
nodeDblclickFunc: (node: TagTreeNode) => void;
|
||||||
|
|
||||||
constructor(value: number) {
|
constructor(value: number) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
@@ -117,6 +133,16 @@ export class NodeType {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 赋值节点双击事件回调函数
|
||||||
|
* @param func 节点双击事件回调函数
|
||||||
|
* @returns this
|
||||||
|
*/
|
||||||
|
withNodeDblclickFunc(func: (node: TagTreeNode) => void) {
|
||||||
|
this.nodeDblclickFunc = func;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 赋值右击菜单按钮选项
|
* 赋值右击菜单按钮选项
|
||||||
* @param contextMenuItems 右击菜单按钮选项
|
* @param contextMenuItems 右击菜单按钮选项
|
||||||
|
|||||||
@@ -28,8 +28,11 @@
|
|||||||
<el-form-item prop="startTime" label="开始时间">
|
<el-form-item prop="startTime" label="开始时间">
|
||||||
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="intervalDay" label="备份周期">
|
<el-form-item prop="intervalDay" label="备份周期(天)">
|
||||||
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="备份周期(单位:天)"></el-input>
|
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="单位:天"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="maxSaveDays" label="备份历史保留天数">
|
||||||
|
<el-input v-model.number="state.form.maxSaveDays" type="number" placeholder="0: 永久保留"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
@@ -92,6 +95,14 @@ const rules = {
|
|||||||
trigger: ['change', 'blur'],
|
trigger: ['change', 'blur'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
maxSaveDays: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
pattern: /^[0-9]\d*$/,
|
||||||
|
message: '请输入非负整数',
|
||||||
|
trigger: ['change', 'blur'],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const backupForm: any = ref(null);
|
const backupForm: any = ref(null);
|
||||||
@@ -102,9 +113,10 @@ const state = reactive({
|
|||||||
dbId: 0,
|
dbId: 0,
|
||||||
dbNames: '',
|
dbNames: '',
|
||||||
name: '',
|
name: '',
|
||||||
intervalDay: null,
|
intervalDay: 1,
|
||||||
startTime: null as any,
|
startTime: null as any,
|
||||||
repeated: null as any,
|
repeated: true,
|
||||||
|
maxSaveDays: 0,
|
||||||
},
|
},
|
||||||
btnLoading: false,
|
btnLoading: false,
|
||||||
dbNamesSelected: [] as any,
|
dbNamesSelected: [] as any,
|
||||||
@@ -137,12 +149,14 @@ const init = (data: any) => {
|
|||||||
state.form.name = data.name;
|
state.form.name = data.name;
|
||||||
state.form.intervalDay = data.intervalDay;
|
state.form.intervalDay = data.intervalDay;
|
||||||
state.form.startTime = data.startTime;
|
state.form.startTime = data.startTime;
|
||||||
|
state.form.maxSaveDays = data.maxSaveDays;
|
||||||
} else {
|
} else {
|
||||||
state.editOrCreate = false;
|
state.editOrCreate = false;
|
||||||
state.form.name = '';
|
state.form.name = '';
|
||||||
state.form.intervalDay = null;
|
state.form.intervalDay = 1;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
||||||
|
state.form.maxSaveDays = 0;
|
||||||
getDbNamesWithoutBackup();
|
getDbNamesWithoutBackup();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #type="{ data }">
|
<template #type="{ data }">
|
||||||
<el-tooltip :content="data.type" placement="top">
|
<el-tooltip :content="getDbDialect(data.type).getInfo().name" placement="top">
|
||||||
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
<SvgIcon :name="getDbDialect(data.type).getInfo().icon" :size="20" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-button @click="showInfo(data)" link>详情</el-button>
|
<el-button @click="showInfo(data)" link>详情</el-button>
|
||||||
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
|
<el-button v-if="actionBtns[perms.saveInstance]" @click="editInstance(data)" type="primary" link>编辑</el-button>
|
||||||
|
<el-button v-if="actionBtns[perms.delInstance]" @click="deleteInstance(data)" type="primary" link>删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</page-table>
|
</page-table>
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ const columns = ref([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// 该用户拥有的的操作列按钮权限
|
// 该用户拥有的的操作列按钮权限
|
||||||
const actionBtns = hasPerms([perms.saveInstance]);
|
const actionBtns = hasPerms(Object.values(perms));
|
||||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
|
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(110).fixedRight().alignCenter();
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
const pageTableRef: Ref<any> = ref(null);
|
||||||
|
|
||||||
@@ -150,14 +151,26 @@ const editInstance = async (data: any) => {
|
|||||||
state.instanceEditDialog.visible = true;
|
state.instanceEditDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteInstance = async () => {
|
const deleteInstance = async (data: any) => {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(`确定删除数据库实例【${state.selectionData.map((x: any) => x.name).join(', ')}】?`, '提示', {
|
let instanceName: string;
|
||||||
|
if (data) {
|
||||||
|
instanceName = data.name;
|
||||||
|
} else {
|
||||||
|
instanceName = state.selectionData.map((x: any) => x.name).join(', ');
|
||||||
|
}
|
||||||
|
await ElMessageBox.confirm(`确定删除数据库实例【${instanceName}】?`, '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
});
|
});
|
||||||
await dbApi.deleteInstance.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
let instanceId: string;
|
||||||
|
if (data) {
|
||||||
|
instanceId = data.id;
|
||||||
|
} else {
|
||||||
|
instanceId = state.selectionData.map((x: any) => x.id).join(',');
|
||||||
|
}
|
||||||
|
await dbApi.deleteInstance.request({ id: instanceId });
|
||||||
ElMessage.success('删除成功');
|
ElMessage.success('删除成功');
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<el-descriptions-item label-align="right">
|
<el-descriptions-item label-align="right">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
<SvgIcon :name="getDbDialect(nowDbInst.type).getInfo().icon" :size="18" />
|
<SvgIcon :name="nowDbInst.getDialect().getInfo().icon" :size="18" />
|
||||||
实例
|
实例
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
<db-select-tree
|
<db-select-tree
|
||||||
placeholder="请选择源数据库"
|
placeholder="请选择源数据库"
|
||||||
v-model:db-id="form.srcDbId"
|
v-model:db-id="form.srcDbId"
|
||||||
|
v-model:inst-name="form.srcInstName"
|
||||||
v-model:db-name="form.srcDbName"
|
v-model:db-name="form.srcDbName"
|
||||||
v-model:tag-path="form.srcTagPath"
|
v-model:tag-path="form.srcTagPath"
|
||||||
v-model:db-type="form.srcDbType"
|
v-model:db-type="form.srcDbType"
|
||||||
@@ -56,8 +57,10 @@
|
|||||||
<db-select-tree
|
<db-select-tree
|
||||||
placeholder="请选择目标数据库"
|
placeholder="请选择目标数据库"
|
||||||
v-model:db-id="form.targetDbId"
|
v-model:db-id="form.targetDbId"
|
||||||
|
v-model:inst-name="form.targetInstName"
|
||||||
v-model:db-name="form.targetDbName"
|
v-model:db-name="form.targetDbName"
|
||||||
v-model:tag-path="form.targetTagPath"
|
v-model:tag-path="form.targetTagPath"
|
||||||
|
v-model:db-type="form.targetDbType"
|
||||||
@select-db="onSelectTargetDb"
|
@select-db="onSelectTargetDb"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -182,7 +185,7 @@ import { ElMessage } from 'element-plus';
|
|||||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
||||||
import {DbType, getDbDialect} from '@/views/ops/db/dialect'
|
import { DbType, getDbDialect } from '@/views/ops/db/dialect';
|
||||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -227,13 +230,16 @@ type FormData = {
|
|||||||
taskName?: string;
|
taskName?: string;
|
||||||
taskCron: string;
|
taskCron: string;
|
||||||
srcDbId?: number;
|
srcDbId?: number;
|
||||||
|
srcInstName?: string;
|
||||||
srcDbName?: string;
|
srcDbName?: string;
|
||||||
srcDbType?: string;
|
srcDbType?: string;
|
||||||
srcTagPath?: string;
|
srcTagPath?: string;
|
||||||
targetDbId?: number;
|
targetDbId?: number;
|
||||||
|
targetInstName?: string;
|
||||||
targetDbName?: string;
|
targetDbName?: string;
|
||||||
targetTagPath?: string;
|
targetTagPath?: string;
|
||||||
targetTableName?: string;
|
targetTableName?: string;
|
||||||
|
targetDbType?: string;
|
||||||
dataSql?: string;
|
dataSql?: string;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
updField?: string;
|
updField?: string;
|
||||||
@@ -304,7 +310,8 @@ watch(dialogVisible, async (newValue: boolean) => {
|
|||||||
// 初始化实例
|
// 初始化实例
|
||||||
db.databases = db.database?.split(' ').sort() || [];
|
db.databases = db.database?.split(' ').sort() || [];
|
||||||
state.srcDbInst = DbInst.getOrNewInst(db);
|
state.srcDbInst = DbInst.getOrNewInst(db);
|
||||||
state.form.srcDbType = state.srcDbInst.type
|
state.form.srcDbType = state.srcDbInst.type;
|
||||||
|
state.form.srcInstName = db.instanceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化target数据源
|
// 初始化target数据源
|
||||||
@@ -315,6 +322,8 @@ watch(dialogVisible, async (newValue: boolean) => {
|
|||||||
// 初始化实例
|
// 初始化实例
|
||||||
db.databases = db.database?.split(' ').sort() || [];
|
db.databases = db.database?.split(' ').sort() || [];
|
||||||
state.targetDbInst = DbInst.getOrNewInst(db);
|
state.targetDbInst = DbInst.getOrNewInst(db);
|
||||||
|
state.form.targetDbType = state.targetDbInst.type;
|
||||||
|
state.form.targetInstName = db.instanceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetDbId && state.form.targetDbName) {
|
if (targetDbId && state.form.targetDbName) {
|
||||||
@@ -414,15 +423,15 @@ const handleGetSrcFields = async () => {
|
|||||||
|
|
||||||
// 执行sql
|
// 执行sql
|
||||||
// oracle的分页关键字不一样
|
// oracle的分页关键字不一样
|
||||||
let limit = ' limit 1'
|
let limit = ' limit 1';
|
||||||
if(state.form.srcDbType === DbType.oracle){
|
if (state.form.srcDbType === DbType.oracle) {
|
||||||
limit = ' where rownum <= 1'
|
limit = ' where rownum <= 1';
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await dbApi.sqlExec.request({
|
const res = await dbApi.sqlExec.request({
|
||||||
id: state.form.srcDbId,
|
id: state.form.srcDbId,
|
||||||
db: state.form.srcDbName,
|
db: state.form.srcDbName,
|
||||||
sql: `select * from (${state.form.dataSql}) t ${limit}`
|
sql: `select * from (${state.form.dataSql}) t ${limit}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.columns) {
|
if (!res.columns) {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
:resource-type="TagResourceTypeEnum.Db.value"
|
:resource-type="TagResourceTypeEnum.Db.value"
|
||||||
:tag-path-node-type="NodeTypeTagPath"
|
:tag-path-node-type="NodeTypeTagPath"
|
||||||
>
|
>
|
||||||
|
<template #iconPrefix>
|
||||||
|
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
|
||||||
|
</template>
|
||||||
<template #prefix="{ data }">
|
<template #prefix="{ data }">
|
||||||
<SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
|
<SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
|
||||||
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
||||||
@@ -27,6 +30,9 @@ const props = defineProps({
|
|||||||
dbId: {
|
dbId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
},
|
},
|
||||||
|
instName: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
dbName: {
|
dbName: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
@@ -38,7 +44,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:dbId', 'update:dbType', 'selectDb']);
|
const emits = defineEmits(['update:dbName', 'update:tagPath', 'update:instName', 'update:dbId', 'update:dbType', 'selectDb']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 树节点类型
|
* 树节点类型
|
||||||
@@ -56,7 +62,7 @@ class SqlExecNodeType {
|
|||||||
|
|
||||||
const selectNode = computed({
|
const selectNode = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return props.dbName ? `${props.tagPath} - ${props.dbId} - ${props.dbName}` : '';
|
return props.dbName ? `${props.tagPath} > ${props.instName} > ${props.dbName}` : '';
|
||||||
},
|
},
|
||||||
set: () => {
|
set: () => {
|
||||||
//
|
//
|
||||||
@@ -151,6 +157,7 @@ const changeNode = (nodeData: TagTreeNode) => {
|
|||||||
const params = nodeData.params;
|
const params = nodeData.params;
|
||||||
// postgres
|
// postgres
|
||||||
emits('update:dbName', params.db);
|
emits('update:dbName', params.db);
|
||||||
|
emits('update:instName', params.name);
|
||||||
emits('update:dbId', params.id);
|
emits('update:dbId', params.id);
|
||||||
emits('update:tagPath', params.tagPath);
|
emits('update:tagPath', params.tagPath);
|
||||||
emits('update:dbType', params.type);
|
emits('update:dbType', params.type);
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ import { buildProgressProps } from '@/components/progress-notify/progress-notify
|
|||||||
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
|
||||||
import syssocket from '@/common/syssocket';
|
import syssocket from '@/common/syssocket';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { getDbDialect } from '../../dialect';
|
|
||||||
import { Pane, Splitpanes } from 'splitpanes';
|
import { Pane, Splitpanes } from 'splitpanes';
|
||||||
|
|
||||||
const emits = defineEmits(['saveSqlSuccess']);
|
const emits = defineEmits(['saveSqlSuccess']);
|
||||||
@@ -453,7 +452,7 @@ const formatSql = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDialect = getDbDialect(getNowDbInst().type).getInfo().formatSqlDialect;
|
const formatDialect = getNowDbInst().getDialect().getInfo().formatSqlDialect;
|
||||||
|
|
||||||
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
let sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||||
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
// 有选中sql则格式化并替换选中sql, 否则格式化编辑器所有内容
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
:class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`"
|
:class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`"
|
||||||
input-style="text-align: center; height: 26px;"
|
|
||||||
size="small"
|
size="small"
|
||||||
v-model="itemValue"
|
v-model="itemValue"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@@ -20,7 +19,6 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
class="w100 mb4"
|
class="w100 mb4"
|
||||||
input-style="text-align: center; height: 26px;"
|
|
||||||
size="small"
|
size="small"
|
||||||
v-model.number="itemValue"
|
v-model.number="itemValue"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@@ -185,9 +183,6 @@ const getEditorLangByValue = (value: any) => {
|
|||||||
.el-input__prefix {
|
.el-input__prefix {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.el-input__inner {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-time-picker-popper {
|
.edit-time-picker-popper {
|
||||||
|
|||||||
@@ -137,13 +137,25 @@
|
|||||||
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
|
<el-input v-model="state.genTxtDialog.txt" type="textarea" rows="20" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<DbTableDataForm
|
||||||
|
v-if="state.tableDataFormDialog.visible"
|
||||||
|
:db-inst="getNowDbInst()"
|
||||||
|
:db-name="db"
|
||||||
|
:columns="columns!"
|
||||||
|
:title="state.tableDataFormDialog.title"
|
||||||
|
:table-name="table"
|
||||||
|
v-model:visible="state.tableDataFormDialog.visible"
|
||||||
|
v-model="state.tableDataFormDialog.data"
|
||||||
|
@submit-success="emits('changeUpdatedField')"
|
||||||
|
/>
|
||||||
|
|
||||||
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { ElInput } from 'element-plus';
|
import { ElInput, ElMessage } from 'element-plus';
|
||||||
import { copyToClipboard } from '@/common/utils/string';
|
import { copyToClipboard } from '@/common/utils/string';
|
||||||
import { DbInst } from '@/views/ops/db/db';
|
import { DbInst } from '@/views/ops/db/db';
|
||||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||||
@@ -153,6 +165,7 @@ import { dateStrFormat } from '@/common/utils/date';
|
|||||||
import { useIntervalFn, useStorage } from '@vueuse/core';
|
import { useIntervalFn, useStorage } from '@vueuse/core';
|
||||||
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
||||||
import ColumnFormItem from './ColumnFormItem.vue';
|
import ColumnFormItem from './ColumnFormItem.vue';
|
||||||
|
import DbTableDataForm from './DbTableDataForm.vue';
|
||||||
|
|
||||||
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
const emits = defineEmits(['dataDelete', 'sortChange', 'deleteData', 'selectionChange', 'changeUpdatedField']);
|
||||||
|
|
||||||
@@ -246,6 +259,13 @@ const cmDataDel = new ContextmenuItem('deleteData', '删除')
|
|||||||
return state.table == '';
|
return state.table == '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cmDataEdit = new ContextmenuItem('editData', '编辑行')
|
||||||
|
.withIcon('edit')
|
||||||
|
.withOnClick(() => onEditRowData())
|
||||||
|
.withHideFunc(() => {
|
||||||
|
return state.table == '';
|
||||||
|
});
|
||||||
|
|
||||||
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
||||||
.withIcon('tickets')
|
.withIcon('tickets')
|
||||||
.withOnClick(() => onGenerateInsertSql())
|
.withOnClick(() => onGenerateInsertSql())
|
||||||
@@ -332,7 +352,11 @@ const state = reactive({
|
|||||||
},
|
},
|
||||||
items: [] as ContextmenuItem[],
|
items: [] as ContextmenuItem[],
|
||||||
},
|
},
|
||||||
|
tableDataFormDialog: {
|
||||||
|
data: {},
|
||||||
|
title: '',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
genTxtDialog: {
|
genTxtDialog: {
|
||||||
title: 'SQL',
|
title: 'SQL',
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -443,7 +467,7 @@ const formatDataValues = (datas: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setTableData = (datas: any) => {
|
const setTableData = (datas: any) => {
|
||||||
tableRef.value.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
tableRef.value?.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
||||||
selectionRowsMap.clear();
|
selectionRowsMap.clear();
|
||||||
cellUpdateMap.clear();
|
cellUpdateMap.clear();
|
||||||
formatDataValues(datas);
|
formatDataValues(datas);
|
||||||
@@ -575,7 +599,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
|
|||||||
const { clientX, clientY } = event;
|
const { clientX, clientY } = event;
|
||||||
state.contextmenu.dropdown.x = clientX;
|
state.contextmenu.dropdown.x = clientX;
|
||||||
state.contextmenu.dropdown.y = clientY;
|
state.contextmenu.dropdown.y = clientY;
|
||||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmDataEdit, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
||||||
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -600,6 +624,18 @@ const onDeleteData = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onEditRowData = () => {
|
||||||
|
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||||
|
if (selectionDatas.length > 1) {
|
||||||
|
ElMessage.warning('只能编辑一行数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = selectionDatas[0];
|
||||||
|
state.tableDataFormDialog.data = data;
|
||||||
|
state.tableDataFormDialog.title = `编辑表'${props.table}'数据`;
|
||||||
|
state.tableDataFormDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
const onGenerateInsertSql = async () => {
|
const onGenerateInsertSql = async () => {
|
||||||
const selectionDatas = Array.from(selectionRowsMap.values());
|
const selectionDatas = Array.from(selectionRowsMap.values());
|
||||||
state.genTxtDialog.txt = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
|
state.genTxtDialog.txt = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
|
||||||
@@ -713,40 +749,21 @@ const submitUpdateFields = async () => {
|
|||||||
|
|
||||||
const db = state.db;
|
const db = state.db;
|
||||||
let res = '';
|
let res = '';
|
||||||
const dbDialect = getDbDialect(dbInst.type);
|
|
||||||
let schema = '';
|
|
||||||
let dbArr = db.split('/');
|
|
||||||
if (dbArr.length == 2) {
|
|
||||||
schema = dbInst.wrapName(dbArr[1]) + '.';
|
|
||||||
}
|
|
||||||
for (let updateRow of cellUpdateMap.values()) {
|
for (let updateRow of cellUpdateMap.values()) {
|
||||||
let sql = `UPDATE ${schema}${dbInst.wrapName(state.table)} SET `;
|
const rowData = { ...updateRow.rowData };
|
||||||
const rowData = updateRow.rowData;
|
let updateColumnValue = {};
|
||||||
// 主键列信息
|
|
||||||
const primaryKey = await dbInst.loadTableColumn(db, state.table);
|
|
||||||
let primaryKeyType = primaryKey.columnType;
|
|
||||||
let primaryKeyName = primaryKey.columnName;
|
|
||||||
let primaryKeyValue = rowData[primaryKeyName];
|
|
||||||
|
|
||||||
for (let k of updateRow.columnsMap.keys()) {
|
for (let k of updateRow.columnsMap.keys()) {
|
||||||
const v = updateRow.columnsMap.get(k);
|
const v = updateRow.columnsMap.get(k);
|
||||||
if (!v) {
|
if (!v) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 更新字段列信息
|
updateColumnValue[k] = rowData[k];
|
||||||
const updateColumn = await dbInst.loadTableColumn(db, state.table, k);
|
// 将更新的字段对应的原始数据还原(主要应对可能更新修改了主键等)
|
||||||
|
rowData[k] = v.oldValue;
|
||||||
sql += ` ${dbInst.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, rowData[k], dbDialect)},`;
|
|
||||||
|
|
||||||
// 如果修改的字段是主键
|
|
||||||
if (k === primaryKeyName) {
|
|
||||||
primaryKeyValue = v.oldValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
|
||||||
sql = sql.substring(0, sql.length - 1);
|
|
||||||
sql += ` WHERE ${dbInst.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`;
|
|
||||||
res += sql;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dbInst.promptExeSql(
|
dbInst.promptExeSql(
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -234,32 +234,16 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="addDataDialog.visible" :title="addDataDialog.title" :destroy-on-close="true" width="600px">
|
<DbTableDataForm
|
||||||
<el-form ref="dataForm" :model="addDataDialog.data" :show-message="false" label-width="auto" size="small">
|
:db-inst="getNowDbInst()"
|
||||||
<el-form-item
|
:db-name="dbName"
|
||||||
v-for="column in columns"
|
:columns="columns"
|
||||||
:key="column.columnName"
|
:title="addDataDialog.title"
|
||||||
class="w100 mb5"
|
:table-name="tableName"
|
||||||
:prop="column.columnName"
|
v-model:visible="addDataDialog.visible"
|
||||||
:label="column.columnName"
|
v-model="addDataDialog.data"
|
||||||
:required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
|
@submit-success="onRefresh"
|
||||||
>
|
/>
|
||||||
<ColumnFormItem
|
|
||||||
v-model="addDataDialog.data[`${column.columnName}`]"
|
|
||||||
:data-type="dbDialect.getDataType(column.columnType)"
|
|
||||||
:placeholder="`${column.columnType} ${column.columnComment}`"
|
|
||||||
:column-name="column.columnName"
|
|
||||||
:disabled="column.isIdentity"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="closeAddDataDialog">取消</el-button>
|
|
||||||
<el-button type="primary" @click="addRow">确定</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -269,11 +253,11 @@ import { ElMessage } from 'element-plus';
|
|||||||
|
|
||||||
import { DbInst } from '@/views/ops/db/db';
|
import { DbInst } from '@/views/ops/db/db';
|
||||||
import DbTableData from './DbTableData.vue';
|
import DbTableData from './DbTableData.vue';
|
||||||
import { DbDialect, getDbDialect } from '@/views/ops/db/dialect';
|
import { DbDialect } from '@/views/ops/db/dialect';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import ColumnFormItem from './ColumnFormItem.vue';
|
|
||||||
import { useEventListener, useStorage } from '@vueuse/core';
|
import { useEventListener, useStorage } from '@vueuse/core';
|
||||||
import { copyToClipboard } from '@/common/utils/string';
|
import { copyToClipboard } from '@/common/utils/string';
|
||||||
|
import DbTableDataForm from './DbTableDataForm.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
dbId: {
|
dbId: {
|
||||||
@@ -294,7 +278,6 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dataForm: any = ref(null);
|
|
||||||
const dbTableRef: Ref = ref(null);
|
const dbTableRef: Ref = ref(null);
|
||||||
const condInputRef: Ref = ref(null);
|
const condInputRef: Ref = ref(null);
|
||||||
const columnNameSearchInputRef: Ref = ref(null);
|
const columnNameSearchInputRef: Ref = ref(null);
|
||||||
@@ -341,7 +324,6 @@ const state = reactive({
|
|||||||
addDataDialog: {
|
addDataDialog: {
|
||||||
data: {},
|
data: {},
|
||||||
title: '',
|
title: '',
|
||||||
placeholder: '',
|
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
tableHeight: '600px',
|
tableHeight: '600px',
|
||||||
@@ -349,7 +331,7 @@ const state = reactive({
|
|||||||
dbDialect: {} as DbDialect,
|
dbDialect: {} as DbDialect,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog, dbDialect } = toRefs(state);
|
const { datas, condition, loading, columns, pageNum, pageSize, pageSizes, sql, hasUpdatedFileds, conditionDialog, addDataDialog } = toRefs(state);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.tableHeight,
|
() => props.tableHeight,
|
||||||
@@ -367,7 +349,7 @@ onMounted(async () => {
|
|||||||
state.tableHeight = props.tableHeight;
|
state.tableHeight = props.tableHeight;
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
|
|
||||||
state.dbDialect = getDbDialect(getNowDbInst().type);
|
state.dbDialect = getNowDbInst().getDialect();
|
||||||
useEventListener('click', handlerWindowClick);
|
useEventListener('click', handlerWindowClick);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -601,46 +583,6 @@ const onShowAddDataDialog = async () => {
|
|||||||
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
state.addDataDialog.title = `添加'${props.tableName}'表数据`;
|
||||||
state.addDataDialog.visible = true;
|
state.addDataDialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeAddDataDialog = () => {
|
|
||||||
state.addDataDialog.visible = false;
|
|
||||||
state.addDataDialog.data = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加新数据行
|
|
||||||
const addRow = async () => {
|
|
||||||
dataForm.value.validate(async (valid: boolean) => {
|
|
||||||
if (valid) {
|
|
||||||
const dbInst = getNowDbInst();
|
|
||||||
const data = state.addDataDialog.data;
|
|
||||||
// key: 字段名,value: 字段名提示
|
|
||||||
let obj: any = {};
|
|
||||||
for (let item of state.columns) {
|
|
||||||
const value = data[item.columnName];
|
|
||||||
if (!value) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
obj[`${dbInst.wrapName(item.columnName)}`] = DbInst.wrapValueByType(value);
|
|
||||||
}
|
|
||||||
let columnNames = Object.keys(obj).join(',');
|
|
||||||
let values = Object.values(obj).join(',');
|
|
||||||
// 获取schema
|
|
||||||
let schema = '';
|
|
||||||
let arr = props.dbName?.split('/');
|
|
||||||
if (arr && arr.length > 1) {
|
|
||||||
schema = dbInst.wrapName(arr[1]) + '.';
|
|
||||||
}
|
|
||||||
let sql = `INSERT INTO ${schema}${dbInst.wrapName(props.tableName)} (${columnNames}) VALUES (${values});`;
|
|
||||||
dbInst.promptExeSql(props.dbName, sql, null, () => {
|
|
||||||
closeAddDataDialog();
|
|
||||||
onRefresh();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请正确填写数据信息');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -272,6 +272,18 @@ watch(props, async (newValue) => {
|
|||||||
dbDialect = getDbDialect(newValue.dbType);
|
dbDialect = getDbDialect(newValue.dbType);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 切换到索引tab时,刷新索引字段下拉选项
|
||||||
|
watch(
|
||||||
|
() => state.activeName,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue === '2') {
|
||||||
|
state.tableData.indexs.columns = state.tableData.fields.res.map((a) => {
|
||||||
|
return { name: a.name, remark: a.remark };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
reset();
|
reset();
|
||||||
@@ -391,22 +403,22 @@ const genSql = () => {
|
|||||||
let data = state.tableData;
|
let data = state.tableData;
|
||||||
// 创建表
|
// 创建表
|
||||||
if (!props.data?.edit) {
|
if (!props.data?.edit) {
|
||||||
if (state.activeName === '1') {
|
let createTable = dbDialect.getCreateTableSql(data);
|
||||||
return dbDialect.getCreateTableSql(data);
|
let createIndex = '';
|
||||||
} else if (state.activeName === '2' && data.indexs.res.length > 0) {
|
if (data.indexs.res.length > 0) {
|
||||||
return dbDialect.getCreateIndexSql(data);
|
createIndex = dbDialect.getCreateIndexSql(data);
|
||||||
}
|
}
|
||||||
|
return createTable + ';' + createIndex;
|
||||||
} else {
|
} else {
|
||||||
// 修改
|
// 修改列
|
||||||
if (state.activeName === '1') {
|
let changeColData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
|
||||||
// 修改列
|
let colSql = dbDialect.getModifyColumnSql(data, data.tableName, changeColData);
|
||||||
let changeData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
|
// 修改索引
|
||||||
return dbDialect.getModifyColumnSql(data, data.tableName, changeData);
|
let changeIdxData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
|
||||||
} else if (state.activeName === '2') {
|
let idxSql = dbDialect.getModifyIndexSql(data, data.tableName, changeIdxData);
|
||||||
// 修改索引
|
// 修改表名
|
||||||
let changeData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
|
|
||||||
return dbDialect.getModifyIndexSql(data, data.tableName, changeData);
|
return colSql + ';' + idxSql;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ export class DbInst {
|
|||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取数据库实例方言
|
||||||
|
getDialect(): DbDialect {
|
||||||
|
return getDbDialect(this.type);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载数据库表信息
|
* 加载数据库表信息
|
||||||
* @param dbName 数据库名
|
* @param dbName 数据库名
|
||||||
@@ -257,7 +262,7 @@ export class DbInst {
|
|||||||
* @param table 表名
|
* @param table 表名
|
||||||
* @param datas 要生成的数据
|
* @param datas 要生成的数据
|
||||||
*/
|
*/
|
||||||
async genInsertSql(dbName: string, table: string, datas: any[]) {
|
async genInsertSql(dbName: string, table: string, datas: any[], skipNull = false) {
|
||||||
if (!datas) {
|
if (!datas) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -269,6 +274,9 @@ export class DbInst {
|
|||||||
let values = [];
|
let values = [];
|
||||||
for (let column of columns) {
|
for (let column of columns) {
|
||||||
const colName = column.columnName;
|
const colName = column.columnName;
|
||||||
|
if (skipNull && data[colName] == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
colNames.push(this.wrapName(colName));
|
colNames.push(this.wrapName(colName));
|
||||||
values.push(DbInst.wrapValueByType(data[colName]));
|
values.push(DbInst.wrapValueByType(data[colName]));
|
||||||
}
|
}
|
||||||
@@ -277,6 +285,38 @@ export class DbInst {
|
|||||||
return sqls.join(';\n') + ';';
|
return sqls.join(';\n') + ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成根据主键更新语句
|
||||||
|
* @param dbName 数据库名
|
||||||
|
* @param table 表名
|
||||||
|
* @param columnValue 要更新的列以及对应的值 field->columnName; value->columnValue
|
||||||
|
* @param rowData 表的一行完整数据(需要获取主键信息)
|
||||||
|
*/
|
||||||
|
async genUpdateSql(dbName: string, table: string, columnValue: {}, rowData: {}) {
|
||||||
|
let schema = '';
|
||||||
|
let dbArr = dbName.split('/');
|
||||||
|
if (dbArr.length == 2) {
|
||||||
|
schema = this.wrapName(dbArr[1]) + '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
let sql = `UPDATE ${schema}${this.wrapName(table)} SET `;
|
||||||
|
// 主键列信息
|
||||||
|
const primaryKey = await this.loadTableColumn(dbName, table);
|
||||||
|
let primaryKeyType = primaryKey.columnType;
|
||||||
|
let primaryKeyName = primaryKey.columnName;
|
||||||
|
let primaryKeyValue = rowData[primaryKeyName];
|
||||||
|
const dialect = this.getDialect();
|
||||||
|
for (let k of Object.keys(columnValue)) {
|
||||||
|
const v = columnValue[k];
|
||||||
|
// 更新字段列信息
|
||||||
|
const updateColumn = await this.loadTableColumn(dbName, table, k);
|
||||||
|
sql += ` ${this.wrapName(k)} = ${DbInst.wrapColumnValue(updateColumn.columnType, v, dialect)},`;
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = sql.substring(0, sql.length - 1);
|
||||||
|
return (sql += ` WHERE ${this.wrapName(primaryKeyName)} = ${DbInst.wrapColumnValue(primaryKeyType, primaryKeyValue)} ;`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成根据主键删除的sql语句
|
* 生成根据主键删除的sql语句
|
||||||
* @param table 表名
|
* @param table 表名
|
||||||
@@ -297,7 +337,7 @@ export class DbInst {
|
|||||||
sql,
|
sql,
|
||||||
dbId: this.id,
|
dbId: this.id,
|
||||||
db,
|
db,
|
||||||
dbType: getDbDialect(this.type).getInfo().formatSqlDialect,
|
dbType: this.getDialect().getInfo().formatSqlDialect,
|
||||||
runSuccessCallback: successFunc,
|
runSuccessCallback: successFunc,
|
||||||
cancelCallback: cancelFunc,
|
cancelCallback: cancelFunc,
|
||||||
});
|
});
|
||||||
@@ -310,7 +350,7 @@ export class DbInst {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
wrapName = (name: string) => {
|
wrapName = (name: string) => {
|
||||||
return getDbDialect(this.type).quoteIdentifier(name);
|
return this.getDialect().quoteIdentifier(name);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_dialect';
|
|||||||
import { SqliteDialect } from '@/views/ops/db/dialect/sqlite_dialect';
|
import { SqliteDialect } from '@/views/ops/db/dialect/sqlite_dialect';
|
||||||
import { MssqlDialect } from '@/views/ops/db/dialect/mssql_dialect';
|
import { MssqlDialect } from '@/views/ops/db/dialect/mssql_dialect';
|
||||||
import { GaussDialect } from '@/views/ops/db/dialect/gauss_dialect';
|
import { GaussDialect } from '@/views/ops/db/dialect/gauss_dialect';
|
||||||
|
import { KingbaseEsDialect } from '@/views/ops/db/dialect/kingbaseES_dialect';
|
||||||
|
import { VastbaseDialect } from '@/views/ops/db/dialect/vastbase_dialect';
|
||||||
|
|
||||||
export interface sqlColumnType {
|
export interface sqlColumnType {
|
||||||
udtName: string;
|
udtName: string;
|
||||||
@@ -122,13 +124,15 @@ export const DbType = {
|
|||||||
oracle: 'oracle',
|
oracle: 'oracle',
|
||||||
sqlite: 'sqlite',
|
sqlite: 'sqlite',
|
||||||
mssql: 'mssql', // ms sqlserver
|
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兼容的数据库
|
// mysql兼容的数据库
|
||||||
export const noSchemaTypes = [DbType.mysql, DbType.mariadb, DbType.sqlite];
|
export const noSchemaTypes = [DbType.mysql, DbType.mariadb, DbType.sqlite];
|
||||||
|
|
||||||
// 有schema层的数据库
|
// 有schema层的数据库
|
||||||
export const schemaDbTypes = [DbType.postgresql, DbType.gauss, DbType.dm, DbType.oracle, DbType.mssql];
|
export const schemaDbTypes = [DbType.postgresql, DbType.gauss, DbType.dm, DbType.oracle, DbType.mssql, DbType.kingbaseEs, DbType.vastbase];
|
||||||
|
|
||||||
export const editDbTypes = [...noSchemaTypes, ...schemaDbTypes];
|
export const editDbTypes = [...noSchemaTypes, ...schemaDbTypes];
|
||||||
|
|
||||||
@@ -218,8 +222,8 @@ export const getDbDialectMap = () => {
|
|||||||
return dbType2DialectMap;
|
return dbType2DialectMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDbDialect = (dbType: string): DbDialect => {
|
export const getDbDialect = (dbType?: string): DbDialect => {
|
||||||
return dbType2DialectMap.get(dbType) || mysqlDialect;
|
return dbType2DialectMap.get(dbType!) || mysqlDialect;
|
||||||
};
|
};
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
@@ -232,4 +236,6 @@ export const getDbDialect = (dbType: string): DbDialect => {
|
|||||||
registerDbDialect(DbType.oracle, new OracleDialect());
|
registerDbDialect(DbType.oracle, new OracleDialect());
|
||||||
registerDbDialect(DbType.sqlite, new SqliteDialect());
|
registerDbDialect(DbType.sqlite, new SqliteDialect());
|
||||||
registerDbDialect(DbType.mssql, new MssqlDialect());
|
registerDbDialect(DbType.mssql, new MssqlDialect());
|
||||||
|
registerDbDialect(DbType.kingbaseEs, new KingbaseEsDialect());
|
||||||
|
registerDbDialect(DbType.vastbase, new VastbaseDialect());
|
||||||
})();
|
})();
|
||||||
|
|||||||
18
mayfly_go_web/src/views/ops/db/dialect/kingbaseES_dialect.ts
Normal file
18
mayfly_go_web/src/views/ops/db/dialect/kingbaseES_dialect.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { DialectInfo } from './index';
|
||||||
|
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
|
||||||
|
|
||||||
|
let kbpgDialectInfo: DialectInfo;
|
||||||
|
|
||||||
|
export class KingbaseEsDialect extends PostgresqlDialect {
|
||||||
|
getInfo(): DialectInfo {
|
||||||
|
if (kbpgDialectInfo) {
|
||||||
|
return kbpgDialectInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbpgDialectInfo = {} as DialectInfo;
|
||||||
|
Object.assign(kbpgDialectInfo, super.getInfo());
|
||||||
|
kbpgDialectInfo.name = 'KingbaseES';
|
||||||
|
kbpgDialectInfo.icon = 'iconfont icon-kingbase';
|
||||||
|
return kbpgDialectInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -303,11 +303,15 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
|
// CREATE UNIQUE INDEX idx_column_name ON your_table (column1, column2);
|
||||||
// COMMENT ON INDEX idx_column_name IS 'Your index comment here';
|
// COMMENT ON INDEX idx_column_name IS 'Your index comment here';
|
||||||
// 创建索引
|
// 创建索引
|
||||||
|
let schema = tableData.db.split('/')[1];
|
||||||
|
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableData.tableName)}`;
|
||||||
let sql: string[] = [];
|
let sql: string[] = [];
|
||||||
tableData.indexs.res.forEach((a: any) => {
|
tableData.indexs.res.forEach((a: any) => {
|
||||||
sql.push(` create ${a.unique ? 'UNIQUE' : ''} index ${a.indexName} ("${a.columnNames.join('","')})"`);
|
// 字段名用双引号包裹
|
||||||
|
let colArr = a.columnNames.map((a: string) => `${this.quoteIdentifier(a)}`);
|
||||||
|
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} on ${dbTable} (${colArr.join(',')})`);
|
||||||
if (a.indexComment) {
|
if (a.indexComment) {
|
||||||
sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`);
|
sql.push(`COMMENT ON INDEX ${schema}.${this.quoteIdentifier(a.indexName)} IS '${a.indexComment}'`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return sql.join(';');
|
return sql.join(';');
|
||||||
@@ -367,6 +371,9 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
getModifyIndexSql(tableData: any, tableName: string, changeData: { del: any[]; add: any[]; upd: any[] }): string {
|
||||||
|
let schema = tableData.db.split('/')[1];
|
||||||
|
let dbTable = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(tableName)}`;
|
||||||
|
|
||||||
// 不能直接修改索引名或字段、需要先删后加
|
// 不能直接修改索引名或字段、需要先删后加
|
||||||
let dropIndexNames: string[] = [];
|
let dropIndexNames: string[] = [];
|
||||||
let addIndexs: any[] = [];
|
let addIndexs: any[] = [];
|
||||||
@@ -400,9 +407,11 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
|
|
||||||
if (addIndexs.length > 0) {
|
if (addIndexs.length > 0) {
|
||||||
addIndexs.forEach((a) => {
|
addIndexs.forEach((a) => {
|
||||||
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${a.indexName}(${a.columnNames.join(',')})`);
|
// 字段名用双引号包裹
|
||||||
|
let colArr = a.columnNames.map((a: string) => `${this.quoteIdentifier(a)}`);
|
||||||
|
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} on ${dbTable} (${colArr.join(',')})`);
|
||||||
if (a.indexComment) {
|
if (a.indexComment) {
|
||||||
sql.push(`COMMENT ON INDEX ${a.indexName} IS '${a.indexComment}'`);
|
sql.push(`COMMENT ON INDEX ${schema}.${this.quoteIdentifier(a.indexName)} IS '${a.indexComment}'`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -431,7 +440,7 @@ class PostgresqlDialect implements DbDialect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||||
wrapStrValue(value: string, type: string): string {
|
wrapStrValue(columnType: string, value: string): string {
|
||||||
return `'${value}'`;
|
return `'${value}'`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
mayfly_go_web/src/views/ops/db/dialect/vastbase_dialect.ts
Normal file
18
mayfly_go_web/src/views/ops/db/dialect/vastbase_dialect.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { DialectInfo } from './index';
|
||||||
|
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
|
||||||
|
|
||||||
|
let vastDialectInfo: DialectInfo;
|
||||||
|
|
||||||
|
export class VastbaseDialect extends PostgresqlDialect {
|
||||||
|
getInfo(): DialectInfo {
|
||||||
|
if (vastDialectInfo) {
|
||||||
|
return vastDialectInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
vastDialectInfo = {} as DialectInfo;
|
||||||
|
Object.assign(vastDialectInfo, super.getInfo());
|
||||||
|
vastDialectInfo.name = 'VastbaseG100';
|
||||||
|
vastDialectInfo.icon = 'iconfont icon-vastbase';
|
||||||
|
return vastDialectInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
tagSelectRef.validate();
|
tagSelectRef.validate();
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
:tag-path="form.tagPath"
|
||||||
:resource-code="form.code"
|
:resource-code="form.code"
|
||||||
:resource-type="TagResourceTypeEnum.Machine.value"
|
:resource-type="TagResourceTypeEnum.Machine.value"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@@ -153,6 +154,7 @@ const state = reactive({
|
|||||||
form: {
|
form: {
|
||||||
id: null,
|
id: null,
|
||||||
code: '',
|
code: '',
|
||||||
|
tagPath: '',
|
||||||
ip: null,
|
ip: null,
|
||||||
port: 22,
|
port: 22,
|
||||||
name: null,
|
name: null,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="machine-list">
|
||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
:page-api="machineApi.list"
|
:page-api="machineApi.list"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<span v-if="!data.stat">-</span>
|
<span v-if="!data.stat">-</span>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-text size="small" style="font-size: 10px">
|
<el-text size="small" class="font11">
|
||||||
内存(可用/总):
|
内存(可用/总):
|
||||||
<span :class="getStatsFontClass(data.stat.memAvailable, data.stat.memTotal)"
|
<span :class="getStatsFontClass(data.stat.memAvailable, data.stat.memTotal)"
|
||||||
>{{ formatByteSize(data.stat.memAvailable, 1) }}/{{ formatByteSize(data.stat.memTotal, 1) }}
|
>{{ formatByteSize(data.stat.memAvailable, 1) }}/{{ formatByteSize(data.stat.memTotal, 1) }}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</el-text>
|
</el-text>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-text style="font-size: 10px" size="small">
|
<el-text class="font11" size="small">
|
||||||
CPU(空闲): <span :class="getStatsFontClass(data.stat.cpuIdle, 100)">{{ data.stat.cpuIdle.toFixed(0) }}%</span>
|
CPU(空闲): <span :class="getStatsFontClass(data.stat.cpuIdle, 100)">{{ data.stat.cpuIdle.toFixed(0) }}%</span>
|
||||||
</el-text>
|
</el-text>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<span v-if="!data.stat?.fsInfos">-</span>
|
<span v-if="!data.stat?.fsInfos">-</span>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-row v-for="(i, idx) in data.stat.fsInfos.slice(0, 2)" :key="i.mountPoint">
|
<el-row v-for="(i, idx) in data.stat.fsInfos.slice(0, 2)" :key="i.mountPoint">
|
||||||
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
<el-text class="font11" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
||||||
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
|
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
|
||||||
</el-text>
|
</el-text>
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-row v-for="i in data.stat.fsInfos.slice(2)" :key="i.mountPoint">
|
<el-row v-for="i in data.stat.fsInfos.slice(2)" :key="i.mountPoint">
|
||||||
<el-text style="font-size: 10px" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
<el-text class="font11" size="small" :class="getStatsFontClass(i.free, i.used + i.free)">
|
||||||
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
|
{{ i.mountPoint }} => {{ formatByteSize(i.free, 0) }}/{{ formatByteSize(i.used + i.free, 0) }}
|
||||||
</el-text>
|
</el-text>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -231,8 +231,8 @@ const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Machine.value), Se
|
|||||||
const columns = [
|
const columns = [
|
||||||
TableColumn.new('name', '名称'),
|
TableColumn.new('name', '名称'),
|
||||||
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
|
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
|
||||||
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(50),
|
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(55),
|
||||||
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(20),
|
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(25),
|
||||||
TableColumn.new('username', '用户名'),
|
TableColumn.new('username', '用户名'),
|
||||||
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
|
||||||
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
|
||||||
@@ -464,10 +464,6 @@ const showRec = (row: any) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.el-dialog__body {
|
|
||||||
padding: 2px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dropdown-link-machine-list {
|
.el-dropdown-link-machine-list {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
|
|||||||
402
mayfly_go_web/src/views/ops/machine/MachineOp.vue
Normal file
402
mayfly_go_web/src/views/ops/machine/MachineOp.vue
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-all-center">
|
||||||
|
<!-- 文档: https://antoniandre.github.io/splitpanes/ -->
|
||||||
|
<Splitpanes class="default-theme" @resized="onResizeTagTree">
|
||||||
|
<Pane size="20" max-size="30">
|
||||||
|
<tag-tree
|
||||||
|
class="machine-terminal-tree"
|
||||||
|
ref="tagTreeRef"
|
||||||
|
:resource-type="TagResourceTypeEnum.Machine.value"
|
||||||
|
:tag-path-node-type="NodeTypeTagPath"
|
||||||
|
>
|
||||||
|
<template #prefix="{ data }">
|
||||||
|
<SvgIcon v-if="data.icon && data.params.status == 1" :name="data.icon.name" :color="data.icon.color" />
|
||||||
|
<SvgIcon v-if="data.icon && data.params.status == -1" :name="data.icon.name" color="var(--el-color-danger)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #suffix="{ data }">
|
||||||
|
<span style="color: #c4c9c4; font-size: 9px" v-if="data.type.value == MachineNodeType.Machine">{{
|
||||||
|
` ${data.params.username}@${data.params.ip}:${data.params.port}`
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
</tag-tree>
|
||||||
|
</Pane>
|
||||||
|
|
||||||
|
<Pane>
|
||||||
|
<div class="machine-terminal-tabs card pd5">
|
||||||
|
<el-tabs
|
||||||
|
v-if="state.tabs.size > 0"
|
||||||
|
type="card"
|
||||||
|
@tab-remove="onRemoveTab"
|
||||||
|
@tab-change="onTabChange"
|
||||||
|
style="width: 100%"
|
||||||
|
v-model="state.activeTermName"
|
||||||
|
class="h100"
|
||||||
|
>
|
||||||
|
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||||
|
<template #label>
|
||||||
|
<el-popconfirm @confirm="handleReconnect(dt.key)" title="确认重新连接?">
|
||||||
|
<template #reference>
|
||||||
|
<el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'"
|
||||||
|
><Connection />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||||
|
<template #reference>
|
||||||
|
<div>
|
||||||
|
<span class="machine-terminal-tab-label">{{ dt.label }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<el-descriptions :column="1" size="small">
|
||||||
|
<el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item label="username"> {{ dt.params?.username }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="terminal-wrapper" :style="{ height: `calc(100vh - 155px)` }">
|
||||||
|
<TerminalBody
|
||||||
|
@status-change="terminalStatusChange(dt.key, $event)"
|
||||||
|
:ref="(el) => setTerminalRef(el, dt.key)"
|
||||||
|
:socket-url="dt.socketUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<el-dialog v-model="infoDialog.visible">
|
||||||
|
<el-descriptions title="详情" :column="3" border>
|
||||||
|
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="认证方式">
|
||||||
|
{{ infoDialog.data.authCertId > 1 ? '授权凭证' : '密码' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
|
||||||
|
|
||||||
|
<script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
|
||||||
|
|
||||||
|
<file-conf-list :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
|
||||||
|
|
||||||
|
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
|
||||||
|
|
||||||
|
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title" />
|
||||||
|
</div>
|
||||||
|
</Pane>
|
||||||
|
</Splitpanes>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, toRefs, reactive, defineAsyncComponent } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { machineApi, getMachineTerminalSocketUrl } from './api';
|
||||||
|
import { dateFormat } from '@/common/utils/date';
|
||||||
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import { NodeType, TagTreeNode } from '../component/tag';
|
||||||
|
import TagTree from '../component/TagTree.vue';
|
||||||
|
import { Splitpanes, Pane } from 'splitpanes';
|
||||||
|
import { ContextmenuItem } from '@/components/contextmenu/index';
|
||||||
|
// 组件
|
||||||
|
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
|
||||||
|
const FileConfList = defineAsyncComponent(() => import('./file/FileConfList.vue'));
|
||||||
|
const MachineStats = defineAsyncComponent(() => import('./MachineStats.vue'));
|
||||||
|
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
|
||||||
|
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
|
||||||
|
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||||
|
import { TerminalStatus } from '@/components/terminal/common';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const perms = {
|
||||||
|
addMachine: 'machine:add',
|
||||||
|
updateMachine: 'machine:update',
|
||||||
|
delMachine: 'machine:del',
|
||||||
|
terminal: 'machine:terminal',
|
||||||
|
closeCli: 'machine:close-cli',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
|
||||||
|
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
||||||
|
|
||||||
|
class MachineNodeType {
|
||||||
|
static Machine = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 0,
|
||||||
|
ip: null,
|
||||||
|
name: null,
|
||||||
|
tagPath: '',
|
||||||
|
},
|
||||||
|
infoDialog: {
|
||||||
|
visible: false,
|
||||||
|
data: null as any,
|
||||||
|
},
|
||||||
|
serviceDialog: {
|
||||||
|
visible: false,
|
||||||
|
machineId: 0,
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
processDialog: {
|
||||||
|
visible: false,
|
||||||
|
machineId: 0,
|
||||||
|
},
|
||||||
|
fileDialog: {
|
||||||
|
visible: false,
|
||||||
|
machineId: 0,
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
machineStatsDialog: {
|
||||||
|
visible: false,
|
||||||
|
stats: null,
|
||||||
|
title: '',
|
||||||
|
machineId: 0,
|
||||||
|
},
|
||||||
|
machineRecDialog: {
|
||||||
|
visible: false,
|
||||||
|
machineId: 0,
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
activeTermName: '',
|
||||||
|
tabs: new Map<string, any>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineRecDialog } = toRefs(state);
|
||||||
|
|
||||||
|
const tagTreeRef: any = ref(null);
|
||||||
|
|
||||||
|
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: any) => {
|
||||||
|
// 加载标签树下的机器列表
|
||||||
|
state.params.tagPath = node.key;
|
||||||
|
state.params.pageNum = 1;
|
||||||
|
state.params.pageSize = 1000;
|
||||||
|
const res = await search();
|
||||||
|
// 把list 根据name字段排序
|
||||||
|
res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||||
|
return res.list.map((x: any) =>
|
||||||
|
new TagTreeNode(x.id, x.name, NodeTypeMachine(x))
|
||||||
|
.withParams(x)
|
||||||
|
.withDisabled(x.status == -1)
|
||||||
|
.withIcon({
|
||||||
|
name: 'Monitor',
|
||||||
|
color: '#409eff',
|
||||||
|
})
|
||||||
|
.withIsLeaf(true)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let openIds = {};
|
||||||
|
|
||||||
|
const NodeTypeMachine = (machine: any) => {
|
||||||
|
let contextMenuItems = [];
|
||||||
|
contextMenuItems.push(new ContextmenuItem('term', '打开终端').withIcon('Monitor').withOnClick(() => openTerminal(machine)));
|
||||||
|
contextMenuItems.push(new ContextmenuItem('term-ex', '打开终端(新窗口)').withIcon('Monitor').withOnClick(() => openTerminal(machine, true)));
|
||||||
|
contextMenuItems.push(new ContextmenuItem('detail', '详情').withIcon('More').withOnClick(() => showInfo(machine)));
|
||||||
|
contextMenuItems.push(new ContextmenuItem('status', '状态').withIcon('Compass').withOnClick(() => showMachineStats(machine)));
|
||||||
|
contextMenuItems.push(new ContextmenuItem('process', '进程').withIcon('DataLine').withOnClick(() => showProcess(machine)));
|
||||||
|
|
||||||
|
if (actionBtns[perms.updateMachine] && machine.enableRecorder == 1) {
|
||||||
|
contextMenuItems.push(new ContextmenuItem('edit', '终端回放').withIcon('Compass').withOnClick(() => showRec(machine)));
|
||||||
|
}
|
||||||
|
|
||||||
|
contextMenuItems.push(new ContextmenuItem('files', '文件管理').withIcon('FolderOpened').withOnClick(() => showFileManage(machine)));
|
||||||
|
contextMenuItems.push(new ContextmenuItem('scripts', '脚本管理').withIcon('Files').withOnClick(() => serviceManager(machine)));
|
||||||
|
return new NodeType(MachineNodeType.Machine).withContextMenuItems(contextMenuItems).withNodeDblclickFunc(() => {
|
||||||
|
// for (let k of state.tabs.keys()) {
|
||||||
|
// // 存在该机器相关的终端tab,则直接激活该tab
|
||||||
|
// if (k.startsWith(`${machine.id}_${machine.username}_`)) {
|
||||||
|
// state.activeTermName = k;
|
||||||
|
// onTabChange();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
openTerminal(machine);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openTerminal = (machine: any, ex?: boolean) => {
|
||||||
|
// 新窗口打开
|
||||||
|
if (ex) {
|
||||||
|
const { href } = router.resolve({
|
||||||
|
path: `/machine/terminal`,
|
||||||
|
query: {
|
||||||
|
id: machine.id,
|
||||||
|
name: machine.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.open(href, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { name, id, username } = machine;
|
||||||
|
|
||||||
|
// 同一个机器的终端打开多次,key后添加下划线和数字区分
|
||||||
|
openIds[id] = openIds[id] ? ++openIds[id] : 1;
|
||||||
|
let sameIndex = openIds[id];
|
||||||
|
|
||||||
|
let key = `${id}_${username}_${sameIndex}`;
|
||||||
|
// 只保留name的10个字,超出部分只保留前后4个字符,中间用省略号代替
|
||||||
|
let label = name.length > 10 ? name.slice(0, 4) + '...' + name.slice(-4) : name;
|
||||||
|
|
||||||
|
state.tabs.set(key, {
|
||||||
|
key,
|
||||||
|
label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
|
||||||
|
params: machine,
|
||||||
|
socketUrl: getMachineTerminalSocketUrl(id),
|
||||||
|
});
|
||||||
|
state.activeTermName = key;
|
||||||
|
fitTerminal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const serviceManager = (row: any) => {
|
||||||
|
state.serviceDialog.machineId = row.id;
|
||||||
|
state.serviceDialog.visible = true;
|
||||||
|
state.serviceDialog.title = `${row.name} => ${row.ip}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示机器状态统计信息
|
||||||
|
*/
|
||||||
|
const showMachineStats = async (machine: any) => {
|
||||||
|
state.machineStatsDialog.machineId = machine.id;
|
||||||
|
state.machineStatsDialog.title = `机器状态: ${machine.name} => ${machine.ip}`;
|
||||||
|
state.machineStatsDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
const res = await machineApi.list.request(state.params);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showFileManage = (selectionData: any) => {
|
||||||
|
state.fileDialog.visible = true;
|
||||||
|
state.fileDialog.machineId = selectionData.id;
|
||||||
|
state.fileDialog.title = `${selectionData.name} => ${selectionData.ip}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showInfo = (info: any) => {
|
||||||
|
state.infoDialog.data = info;
|
||||||
|
state.infoDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showProcess = (row: any) => {
|
||||||
|
state.processDialog.machineId = row.id;
|
||||||
|
state.processDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showRec = (row: any) => {
|
||||||
|
state.machineRecDialog.title = `${row.name}[${row.ip}]-终端回放记录`;
|
||||||
|
state.machineRecDialog.machineId = row.id;
|
||||||
|
state.machineRecDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveTab = (targetName: string) => {
|
||||||
|
let activeTermName = state.activeTermName;
|
||||||
|
const tabNames = [...state.tabs.keys()];
|
||||||
|
for (let i = 0; i < tabNames.length; i++) {
|
||||||
|
const tabName = tabNames[i];
|
||||||
|
if (tabName !== targetName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
||||||
|
if (nextTab) {
|
||||||
|
activeTermName = nextTab;
|
||||||
|
} else {
|
||||||
|
activeTermName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let info = state.tabs.get(targetName);
|
||||||
|
if (info) {
|
||||||
|
terminalRefs[info.key]?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tabs.delete(targetName);
|
||||||
|
state.activeTermName = activeTermName;
|
||||||
|
onTabChange();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const terminalStatusChange = (key: string, status: TerminalStatus) => {
|
||||||
|
state.tabs.get(key).status = status;
|
||||||
|
};
|
||||||
|
|
||||||
|
const terminalRefs: any = {};
|
||||||
|
const setTerminalRef = (el: any, key: any) => {
|
||||||
|
if (key) {
|
||||||
|
terminalRefs[key] = el;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResizeTagTree = () => {
|
||||||
|
fitTerminal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTabChange = () => {
|
||||||
|
fitTerminal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fitTerminal = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
let info = state.tabs.get(state.activeTermName);
|
||||||
|
if (info) {
|
||||||
|
terminalRefs[info.key]?.resize();
|
||||||
|
terminalRefs[info.key]?.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReconnect = (key: string) => {
|
||||||
|
terminalRefs[key].init();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.machine-terminal-tabs {
|
||||||
|
height: calc(100vh - 108px);
|
||||||
|
--el-tabs-header-height: 30px;
|
||||||
|
|
||||||
|
.el-tabs {
|
||||||
|
--el-tabs-header-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-terminal-tab-label {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.el-tabs__header {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.el-tabs__item {
|
||||||
|
padding: 0 8px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -38,7 +38,6 @@ require (
|
|||||||
// gorm
|
// gorm
|
||||||
gorm.io/driver/mysql v1.5.2
|
gorm.io/driver/mysql v1.5.2
|
||||||
gorm.io/gorm v1.25.6
|
gorm.io/gorm v1.25.6
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
@@ -78,8 +78,6 @@ func (d *Db) DeleteDb(rc *req.Ctx) {
|
|||||||
d.DbApp.Delete(ctx, dbId)
|
d.DbApp.Delete(ctx, dbId)
|
||||||
// 删除该库的sql执行记录
|
// 删除该库的sql执行记录
|
||||||
d.DbSqlExecApp.DeleteBy(ctx, &entity.DbSqlExec{DbId: dbId})
|
d.DbSqlExecApp.DeleteBy(ctx, &entity.DbSqlExec{DbId: dbId})
|
||||||
|
|
||||||
// todo delete restore task and histories
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ func (d *DbBackup) Update(rc *req.Ctx) {
|
|||||||
job.Name = backupForm.Name
|
job.Name = backupForm.Name
|
||||||
job.StartTime = backupForm.StartTime
|
job.StartTime = backupForm.StartTime
|
||||||
job.Interval = backupForm.Interval
|
job.Interval = backupForm.Interval
|
||||||
|
job.MaxSaveDays = backupForm.MaxSaveDays
|
||||||
biz.ErrIsNilAppendErr(d.backupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
|
biz.ErrIsNilAppendErr(d.backupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +179,7 @@ func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) {
|
|||||||
rc.ResData = res
|
rc.ResData = res
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreHistories 删除数据库备份历史
|
// RestoreHistories 从数据库备份历史中恢复数据库
|
||||||
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId/restore [POST]
|
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId/restore [POST]
|
||||||
func (d *DbBackup) RestoreHistories(rc *req.Ctx) {
|
func (d *DbBackup) RestoreHistories(rc *req.Ctx) {
|
||||||
pm := ginx.PathParam(rc.GinCtx, "backupHistoryId")
|
pm := ginx.PathParam(rc.GinCtx, "backupHistoryId")
|
||||||
|
|||||||
@@ -87,16 +87,10 @@ func (d *Instance) DeleteInstance(rc *req.Ctx) {
|
|||||||
|
|
||||||
for _, v := range ids {
|
for _, v := range ids {
|
||||||
value, err := strconv.Atoi(v)
|
value, err := strconv.Atoi(v)
|
||||||
biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s")
|
biz.ErrIsNilAppendErr(err, "删除数据库实例失败: %s")
|
||||||
instanceId := uint64(value)
|
instanceId := uint64(value)
|
||||||
if d.DbApp.Count(&entity.DbQuery{InstanceId: instanceId}) != 0 {
|
err = d.InstanceApp.Delete(rc.MetaCtx, instanceId)
|
||||||
instance, err := d.InstanceApp.GetById(new(entity.DbInstance), instanceId, "name")
|
biz.ErrIsNilAppendErr(err, "删除数据库实例失败: %s")
|
||||||
biz.ErrIsNil(err, "获取数据库实例错误,数据库实例ID为: %d", instance.Id)
|
|
||||||
biz.IsTrue(false, "不能删除数据库实例【%s】,请先删除其关联的数据库资源。", instance.Name)
|
|
||||||
}
|
|
||||||
// todo check if backup task has been disabled and backup histories have been deleted
|
|
||||||
|
|
||||||
d.InstanceApp.Delete(rc.MetaCtx, instanceId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ type DbBackupForm struct {
|
|||||||
Interval time.Duration `json:"-"` // 间隔时间: 为零表示单次执行,为正表示反复执行
|
Interval time.Duration `json:"-"` // 间隔时间: 为零表示单次执行,为正表示反复执行
|
||||||
IntervalDay uint64 `json:"intervalDay"` // 间隔天数: 为零表示单次执行,为正表示反复执行
|
IntervalDay uint64 `json:"intervalDay"` // 间隔天数: 为零表示单次执行,为正表示反复执行
|
||||||
Repeated bool `json:"repeated"` // 是否重复执行
|
Repeated bool `json:"repeated"` // 是否重复执行
|
||||||
|
MaxSaveDays int `json:"maxSaveDays"` // 数据库备份历史保留天数,过期将自动删除
|
||||||
}
|
}
|
||||||
|
|
||||||
func (restore *DbBackupForm) UnmarshalJSON(data []byte) error {
|
func (restore *DbBackupForm) UnmarshalJSON(data []byte) error {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type DbBackup struct {
|
|||||||
StartTime time.Time `json:"startTime"` // 开始时间
|
StartTime time.Time `json:"startTime"` // 开始时间
|
||||||
Interval time.Duration `json:"-"` // 间隔时间
|
Interval time.Duration `json:"-"` // 间隔时间
|
||||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||||
|
MaxSaveDays int `json:"maxSaveDays"` // 数据库备份历史保留天数,过期将自动删除
|
||||||
Enabled bool `json:"enabled"` // 是否启用
|
Enabled bool `json:"enabled"` // 是否启用
|
||||||
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
|
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
|
||||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||||
@@ -29,9 +30,9 @@ func (backup *DbBackup) MarshalJSON() ([]byte, error) {
|
|||||||
backup.IntervalDay = uint64(backup.Interval / time.Hour / 24)
|
backup.IntervalDay = uint64(backup.Interval / time.Hour / 24)
|
||||||
if len(backup.EnabledDesc) == 0 {
|
if len(backup.EnabledDesc) == 0 {
|
||||||
if backup.Enabled {
|
if backup.Enabled {
|
||||||
backup.EnabledDesc = "任务已启用"
|
backup.EnabledDesc = "已启用"
|
||||||
} else {
|
} else {
|
||||||
backup.EnabledDesc = "任务已禁用"
|
backup.EnabledDesc = "已禁用"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return json.Marshal((*dbBackup)(backup))
|
return json.Marshal((*dbBackup)(backup))
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ func (restore *DbRestore) MarshalJSON() ([]byte, error) {
|
|||||||
restore.IntervalDay = uint64(restore.Interval / time.Hour / 24)
|
restore.IntervalDay = uint64(restore.Interval / time.Hour / 24)
|
||||||
if len(restore.EnabledDesc) == 0 {
|
if len(restore.EnabledDesc) == 0 {
|
||||||
if restore.Enabled {
|
if restore.Enabled {
|
||||||
restore.EnabledDesc = "任务已启用"
|
restore.EnabledDesc = "已启用"
|
||||||
} else {
|
} else {
|
||||||
restore.EnabledDesc = "任务已禁用"
|
restore.EnabledDesc = "已禁用"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return json.Marshal((*dbBackup)(restore))
|
return json.Marshal((*dbBackup)(restore))
|
||||||
|
|||||||
@@ -25,10 +25,13 @@ func InitIoc() {
|
|||||||
func Init() {
|
func Init() {
|
||||||
sync.OnceFunc(func() {
|
sync.OnceFunc(func() {
|
||||||
if err := GetDbBackupApp().Init(); err != nil {
|
if err := GetDbBackupApp().Init(); err != nil {
|
||||||
panic(fmt.Sprintf("初始化 dbBackupApp 失败: %v", err))
|
panic(fmt.Sprintf("初始化 DbBackupApp 失败: %v", err))
|
||||||
}
|
}
|
||||||
if err := GetDbRestoreApp().Init(); err != nil {
|
if err := GetDbRestoreApp().Init(); err != nil {
|
||||||
panic(fmt.Sprintf("初始化 dbRestoreApp 失败: %v", err))
|
panic(fmt.Sprintf("初始化 DbRestoreApp 失败: %v", err))
|
||||||
|
}
|
||||||
|
if err := GetDbBinlogApp().Init(); err != nil {
|
||||||
|
panic(fmt.Sprintf("初始化 DbBinlogApp 失败: %v", err))
|
||||||
}
|
}
|
||||||
GetDataSyncTaskApp().InitCronJob()
|
GetDataSyncTaskApp().InitCronJob()
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbi.DbConn, error) {
|
|||||||
|
|
||||||
checkDb := dbName
|
checkDb := dbName
|
||||||
// 兼容pgsql/dm db/schema模式
|
// 兼容pgsql/dm db/schema模式
|
||||||
if dbi.DbTypePostgres.Equal(instance.Type) || dbi.DbTypeGauss.Equal(instance.Type) || dbi.DbTypeDM.Equal(instance.Type) || dbi.DbTypeOracle.Equal(instance.Type) || dbi.DbTypeMssql.Equal(instance.Type) {
|
if dbi.DbTypePostgres.Equal(instance.Type) || dbi.DbTypeGauss.Equal(instance.Type) || dbi.DbTypeDM.Equal(instance.Type) || dbi.DbTypeOracle.Equal(instance.Type) || dbi.DbTypeMssql.Equal(instance.Type) || dbi.DbTypeKingbaseEs.Equal(instance.Type) || dbi.DbTypeVastbase.Equal(instance.Type) {
|
||||||
ss := strings.Split(dbName, "/")
|
ss := strings.Split(dbName, "/")
|
||||||
if len(ss) > 1 {
|
if len(ss) > 1 {
|
||||||
checkDb = ss[0]
|
checkDb = ss[0]
|
||||||
|
|||||||
@@ -6,15 +6,24 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"math"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/pkg/logx"
|
"mayfly-go/pkg/logx"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
|
"mayfly-go/pkg/utils/timex"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxBackupHistoryDays = 30
|
||||||
|
|
||||||
|
var (
|
||||||
|
errRestoringBackupHistory = errors.New("正在从备份历史中恢复数据库")
|
||||||
|
)
|
||||||
|
|
||||||
type DbBackupApp struct {
|
type DbBackupApp struct {
|
||||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||||
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||||
@@ -22,6 +31,10 @@ type DbBackupApp struct {
|
|||||||
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||||
dbApp Db `inject:"DbApp"`
|
dbApp Db `inject:"DbApp"`
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
|
closed chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBackupApp) Init() error {
|
func (app *DbBackupApp) Init() error {
|
||||||
@@ -32,11 +45,68 @@ func (app *DbBackupApp) Init() error {
|
|||||||
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
|
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBackupApp) Close() {
|
func (app *DbBackupApp) Close() {
|
||||||
app.scheduler.Close()
|
app.scheduler.Close()
|
||||||
|
if app.cancel != nil {
|
||||||
|
app.cancel()
|
||||||
|
app.cancel = nil
|
||||||
|
}
|
||||||
|
app.wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBackupApp) Create(ctx context.Context, jobs []*entity.DbBackup) error {
|
func (app *DbBackupApp) Create(ctx context.Context, jobs []*entity.DbBackup) error {
|
||||||
@@ -61,7 +131,6 @@ func (app *DbBackupApp) Update(ctx context.Context, job *entity.DbBackup) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
|
func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
|
||||||
// todo: 删除数据库备份历史文件
|
|
||||||
app.mutex.Lock()
|
app.mutex.Lock()
|
||||||
defer app.mutex.Unlock()
|
defer app.mutex.Unlock()
|
||||||
|
|
||||||
@@ -76,7 +145,7 @@ func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
|
|||||||
default:
|
default:
|
||||||
return err
|
return err
|
||||||
case err == nil:
|
case err == nil:
|
||||||
return fmt.Errorf("数据库备份存在历史记录【%s】,无法删除该任务", history.Name)
|
return fmt.Errorf("请先删除关联的数据库备份历史【%s】", history.Name)
|
||||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
}
|
}
|
||||||
if err := app.backupRepo.DeleteById(ctx, jobId); err != nil {
|
if err := app.backupRepo.DeleteById(ctx, jobId); err != nil {
|
||||||
@@ -184,27 +253,18 @@ func NewIncUUID() (uuid.UUID, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBackupApp) DeleteHistory(ctx context.Context, historyId uint64) (retErr error) {
|
func (app *DbBackupApp) DeleteHistory(ctx context.Context, historyId uint64) (retErr error) {
|
||||||
// todo: 删除数据库备份历史文件
|
|
||||||
app.mutex.Lock()
|
app.mutex.Lock()
|
||||||
defer app.mutex.Unlock()
|
defer app.mutex.Unlock()
|
||||||
|
|
||||||
|
if _, err := app.backupHistoryRepo.UpdateDeleting(false, historyId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
ok, err := app.backupHistoryRepo.UpdateDeleting(true, historyId)
|
ok, err := app.backupHistoryRepo.UpdateDeleting(true, historyId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
_, err = app.backupHistoryRepo.UpdateDeleting(false, historyId)
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if retErr == nil {
|
|
||||||
retErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
retErr = fmt.Errorf("%w, %w", retErr, err)
|
|
||||||
}()
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("正在从备份历史中恢复数据库")
|
return errRestoringBackupHistory
|
||||||
}
|
}
|
||||||
job := &entity.DbBackupHistory{}
|
job := &entity.DbBackupHistory{}
|
||||||
if err := app.backupHistoryRepo.GetById(job, historyId); err != nil {
|
if err := app.backupHistoryRepo.GetById(job, historyId); err != nil {
|
||||||
@@ -214,7 +274,10 @@ func (app *DbBackupApp) DeleteHistory(ctx context.Context, historyId uint64) (re
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dbProgram := conn.GetDialect().GetDbProgram()
|
dbProgram, err := conn.GetDialect().GetDbProgram()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := dbProgram.RemoveBackupHistory(ctx, job.DbBackupId, job.Uuid); err != nil {
|
if err := dbProgram.RemoveBackupHistory(ctx, job.DbBackupId, job.Uuid); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package application
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"math"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/pkg/logx"
|
"mayfly-go/pkg/logx"
|
||||||
@@ -11,9 +12,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DbBinlogApp struct {
|
type DbBinlogApp struct {
|
||||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||||
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
||||||
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
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
|
context context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -26,41 +31,113 @@ func newDbBinlogApp() *DbBinlogApp {
|
|||||||
context: ctx,
|
context: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
}
|
}
|
||||||
svc.waitGroup.Add(1)
|
|
||||||
go svc.run()
|
|
||||||
return svc
|
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() {
|
func (app *DbBinlogApp) run() {
|
||||||
defer app.waitGroup.Done()
|
defer app.waitGroup.Done()
|
||||||
|
|
||||||
// todo: 实现 binlog 并发下载
|
for app.context.Err() == nil {
|
||||||
timex.SleepWithContext(app.context, time.Minute)
|
if err := app.fetchBinlog(app.context); err != nil {
|
||||||
for !app.closed() {
|
|
||||||
jobs, err := app.loadJobs()
|
|
||||||
if err != nil {
|
|
||||||
logx.Errorf("DbBinlogApp: 加载 BINLOG 同步任务失败: %s", err.Error())
|
|
||||||
timex.SleepWithContext(app.context, time.Minute)
|
timex.SleepWithContext(app.context, time.Minute)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if app.closed() {
|
if err := app.pruneBinlog(app.context); err != nil {
|
||||||
break
|
timex.SleepWithContext(app.context, time.Minute)
|
||||||
}
|
continue
|
||||||
if err := app.scheduler.AddJob(app.context, jobs); err != nil {
|
|
||||||
logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
|
|
||||||
}
|
}
|
||||||
timex.SleepWithContext(app.context, entity.BinlogDownloadInterval)
|
timex.SleepWithContext(app.context, entity.BinlogDownloadInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBinlogApp) loadJobs() ([]*entity.DbBinlog, error) {
|
func (app *DbBinlogApp) fetchBinlog(ctx context.Context) error {
|
||||||
|
jobs, err := app.loadJobs(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logx.Errorf("DbBinlogApp: 加载 BINLOG 同步任务失败: %s", err.Error())
|
||||||
|
timex.SleepWithContext(app.context, time.Minute)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if err := app.scheduler.AddJob(app.context, jobs); err != nil {
|
||||||
|
logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBinlogApp) pruneBinlog(ctx context.Context) error {
|
||||||
|
var jobs []*entity.DbBinlog
|
||||||
|
if err := app.binlogRepo.ListByCond(map[string]any{}, &jobs); err != nil {
|
||||||
|
logx.Error("DbBinlogApp: 获取 BINLOG 同步任务失败: ", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, instance := range jobs {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
var histories []*entity.DbBinlogHistory
|
||||||
|
backupHistory, backupHistoryExists, err := app.backupHistoryRepo.GetEarliestHistoryForBinlog(instance.Id)
|
||||||
|
if err != nil {
|
||||||
|
logx.Errorf("DbBinlogApp: 获取数据库备份历史失败: %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var binlogSeq int64 = math.MaxInt64
|
||||||
|
if backupHistoryExists {
|
||||||
|
binlogSeq = backupHistory.BinlogSequence
|
||||||
|
}
|
||||||
|
if err := app.binlogHistoryRepo.GetHistoriesBeforeSequence(ctx, instance.Id, binlogSeq, &histories); err != nil {
|
||||||
|
logx.Errorf("DbBinlogApp: 获取数据库 BINLOG 历史失败: %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn, err := app.dbApp.GetDbConnByInstanceId(instance.Id)
|
||||||
|
if err != nil {
|
||||||
|
logx.Errorf("DbBinlogApp: 创建数据库连接失败: %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dbProgram, err := conn.GetDialect().GetDbProgram()
|
||||||
|
if err != nil {
|
||||||
|
logx.Errorf("DbBinlogApp: 获取数据库备份与恢复程序失败: %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, history := range histories {
|
||||||
|
// todo: 在避免并发访问的前提下删除本地最新的 BINLOG 文件
|
||||||
|
if !backupHistoryExists && i == len(histories)-1 {
|
||||||
|
// 暂不删除本地最新的 BINLOG 文件
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if err := dbProgram.PruneBinlog(history); err != nil {
|
||||||
|
logx.Errorf("清理 BINLOG 文件失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := app.binlogHistoryRepo.DeleteById(ctx, history.Id); err != nil {
|
||||||
|
logx.Errorf("删除 BINLOG 历史失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *DbBinlogApp) loadJobs(ctx context.Context) ([]*entity.DbBinlog, error) {
|
||||||
var instanceIds []uint64
|
var instanceIds []uint64
|
||||||
if err := app.backupRepo.ListDbInstances(true, true, &instanceIds); err != nil {
|
if err := app.backupRepo.ListDbInstances(true, true, &instanceIds); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
jobs := make([]*entity.DbBinlog, 0, len(instanceIds))
|
jobs := make([]*entity.DbBinlog, 0, len(instanceIds))
|
||||||
for _, id := range instanceIds {
|
for _, id := range instanceIds {
|
||||||
if app.closed() {
|
if ctx.Err() != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
binlog := entity.NewDbBinlog(id)
|
binlog := entity.NewDbBinlog(id)
|
||||||
@@ -73,14 +150,15 @@ func (app *DbBinlogApp) loadJobs() ([]*entity.DbBinlog, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBinlogApp) Close() {
|
func (app *DbBinlogApp) Close() {
|
||||||
app.cancel()
|
cancel := app.cancel
|
||||||
|
if cancel == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.cancel = nil
|
||||||
|
cancel()
|
||||||
app.waitGroup.Wait()
|
app.waitGroup.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBinlogApp) closed() bool {
|
|
||||||
return app.context.Err() != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *DbBinlogApp) AddJobIfNotExists(ctx context.Context, job *entity.DbBinlog) error {
|
func (app *DbBinlogApp) AddJobIfNotExists(ctx context.Context, job *entity.DbBinlog) error {
|
||||||
if err := app.binlogRepo.AddJobIfNotExists(ctx, job); err != nil {
|
if err := app.binlogRepo.AddJobIfNotExists(ctx, job); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -90,11 +168,3 @@ func (app *DbBinlogApp) AddJobIfNotExists(ctx context.Context, job *entity.DbBin
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbBinlogApp) Delete(ctx context.Context, jobId uint64) error {
|
|
||||||
// todo: 删除 Binlog 历史文件
|
|
||||||
if err := app.binlogRepo.DeleteById(ctx, jobId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ package application
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
"mayfly-go/internal/db/dbm"
|
"mayfly-go/internal/db/dbm"
|
||||||
"mayfly-go/internal/db/dbm/dbi"
|
"mayfly-go/internal/db/dbm/dbi"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/pkg/base"
|
"mayfly-go/pkg/base"
|
||||||
|
"mayfly-go/pkg/biz"
|
||||||
"mayfly-go/pkg/errorx"
|
"mayfly-go/pkg/errorx"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
)
|
)
|
||||||
@@ -32,6 +35,10 @@ type Instance interface {
|
|||||||
|
|
||||||
type instanceAppImpl struct {
|
type instanceAppImpl struct {
|
||||||
base.AppImpl[*entity.DbInstance, repository.Instance]
|
base.AppImpl[*entity.DbInstance, repository.Instance]
|
||||||
|
|
||||||
|
dbApp Db `inject:"DbApp"`
|
||||||
|
backupApp *DbBackupApp `inject:"DbBackupApp"`
|
||||||
|
restoreApp *DbRestoreApp `inject:"DbRestoreApp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注入DbInstanceRepo
|
// 注入DbInstanceRepo
|
||||||
@@ -96,8 +103,50 @@ func (app *instanceAppImpl) Save(ctx context.Context, instanceEntity *entity.DbI
|
|||||||
return app.UpdateById(ctx, instanceEntity)
|
return app.UpdateById(ctx, instanceEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *instanceAppImpl) Delete(ctx context.Context, id uint64) error {
|
func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error {
|
||||||
return app.DeleteById(ctx, id)
|
instance, err := app.GetById(new(entity.DbInstance), instanceId, "name")
|
||||||
|
biz.ErrIsNil(err, "获取数据库实例错误,数据库实例ID为: %d", instance.Id)
|
||||||
|
|
||||||
|
restore := &entity.DbRestore{
|
||||||
|
DbInstanceId: instanceId,
|
||||||
|
}
|
||||||
|
err = app.restoreApp.restoreRepo.GetBy(restore)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name)
|
||||||
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
backup := &entity.DbBackup{
|
||||||
|
DbInstanceId: instanceId,
|
||||||
|
}
|
||||||
|
err = app.backupApp.backupRepo.GetBy(backup)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name)
|
||||||
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db := &entity.Db{
|
||||||
|
InstanceId: instanceId,
|
||||||
|
}
|
||||||
|
err = app.dbApp.GetBy(db)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库资源。", instance.Name)
|
||||||
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
biz.ErrIsNil(err, "删除数据库实例失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.DeleteById(ctx, instanceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *instanceAppImpl) GetDatabases(ed *entity.DbInstance) ([]string, error) {
|
func (app *instanceAppImpl) GetDatabases(ed *entity.DbInstance) ([]string, error) {
|
||||||
@@ -55,7 +55,6 @@ func (app *DbRestoreApp) Update(ctx context.Context, job *entity.DbRestore) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *DbRestoreApp) Delete(ctx context.Context, jobId uint64) error {
|
func (app *DbRestoreApp) Delete(ctx context.Context, jobId uint64) error {
|
||||||
// todo: 删除数据库恢复历史文件
|
|
||||||
app.mutex.Lock()
|
app.mutex.Lock()
|
||||||
defer app.mutex.Unlock()
|
defer app.mutex.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"mayfly-go/internal/db/dbm/dbi"
|
"mayfly-go/internal/db/dbm/dbi"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/pkg/runner"
|
"mayfly-go/pkg/runner"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -28,6 +30,7 @@ type dbScheduler struct {
|
|||||||
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
|
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
|
||||||
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
||||||
binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
|
binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
|
||||||
|
sfGroup singleflight.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDbScheduler() *dbScheduler {
|
func newDbScheduler() *dbScheduler {
|
||||||
@@ -76,7 +79,6 @@ func (s *dbScheduler) AddJob(ctx context.Context, jobs any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
||||||
// todo: 删除数据库备份历史文件
|
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
@@ -110,12 +112,11 @@ func (s *dbScheduler) StartJobNow(ctx context.Context, job entity.DbJob) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbScheduler) backup(ctx context.Context, dbProgram dbi.DbProgram, job entity.DbJob) error {
|
func (s *dbScheduler) backup(ctx context.Context, dbProgram dbi.DbProgram, backup *entity.DbBackup) error {
|
||||||
id, err := NewIncUUID()
|
id, err := NewIncUUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
backup := job.(*entity.DbBackup)
|
|
||||||
history := &entity.DbBackupHistory{
|
history := &entity.DbBackupHistory{
|
||||||
Uuid: id.String(),
|
Uuid: id.String(),
|
||||||
DbBackupId: backup.Id,
|
DbBackupId: backup.Id,
|
||||||
@@ -143,45 +144,29 @@ func (s *dbScheduler) backup(ctx context.Context, dbProgram dbi.DbProgram, job e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbScheduler) restore(ctx context.Context, dbProgram dbi.DbProgram, job entity.DbJob) error {
|
func (s *dbScheduler) singleFlightFetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, targetTime time.Time) error {
|
||||||
restore := job.(*entity.DbRestore)
|
key := strconv.FormatUint(instanceId, 10)
|
||||||
|
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():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbScheduler) restore(ctx context.Context, dbProgram dbi.DbProgram, restore *entity.DbRestore) error {
|
||||||
if restore.PointInTime.Valid {
|
if restore.PointInTime.Valid {
|
||||||
//if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
|
if err := s.fetchBinlog(ctx, dbProgram, restore.DbInstanceId, true, restore.PointInTime.Time); 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 行模式")
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//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 {
|
|
||||||
// return err
|
|
||||||
//}
|
|
||||||
if err := s.fetchBinlog(ctx, dbProgram, job.GetInstanceId(), true); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
|
if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
|
||||||
@@ -210,102 +195,68 @@ func (s *dbScheduler) restore(ctx context.Context, dbProgram dbi.DbProgram, job
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//func (s *dbScheduler) updateLastStatus(ctx context.Context, job entity.DbJob) error {
|
|
||||||
// switch typ := job.GetJobType(); typ {
|
|
||||||
// case entity.DbJobTypeBackup:
|
|
||||||
// return s.backupRepo.UpdateLastStatus(ctx, job)
|
|
||||||
// case entity.DbJobTypeRestore:
|
|
||||||
// return s.restoreRepo.UpdateLastStatus(ctx, job)
|
|
||||||
// case entity.DbJobTypeBinlog:
|
|
||||||
// return s.binlogRepo.UpdateLastStatus(ctx, job)
|
|
||||||
// default:
|
|
||||||
// panic(fmt.Errorf("无效的数据库任务类型: %v", typ))
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
func (s *dbScheduler) updateJob(ctx context.Context, job entity.DbJob) error {
|
func (s *dbScheduler) updateJob(ctx context.Context, job entity.DbJob) error {
|
||||||
switch typ := job.GetJobType(); typ {
|
switch t := job.(type) {
|
||||||
case entity.DbJobTypeBackup:
|
case *entity.DbBackup:
|
||||||
return s.backupRepo.UpdateById(ctx, job)
|
return s.backupRepo.UpdateById(ctx, t)
|
||||||
case entity.DbJobTypeRestore:
|
case *entity.DbRestore:
|
||||||
return s.restoreRepo.UpdateById(ctx, job)
|
return s.restoreRepo.UpdateById(ctx, t)
|
||||||
case entity.DbJobTypeBinlog:
|
case *entity.DbBinlog:
|
||||||
return s.binlogRepo.UpdateById(ctx, job)
|
return s.binlogRepo.UpdateById(ctx, t)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("无效的数据库任务类型: %v", typ)
|
return fmt.Errorf("无效的数据库任务类型: %T", t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) error {
|
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) error {
|
||||||
//job.SetLastStatus(entity.DbJobRunning, nil)
|
|
||||||
//if err := s.updateLastStatus(ctx, job); err != nil {
|
|
||||||
// logx.Errorf("failed to update job status: %v", err)
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
|
|
||||||
//var errRun error
|
|
||||||
conn, err := s.dbApp.GetDbConnByInstanceId(job.GetInstanceId())
|
conn, err := s.dbApp.GetDbConnByInstanceId(job.GetInstanceId())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dbProgram := conn.GetDialect().GetDbProgram()
|
dbProgram, err := conn.GetDialect().GetDbProgram()
|
||||||
switch typ := job.GetJobType(); typ {
|
if err != nil {
|
||||||
case entity.DbJobTypeBackup:
|
return err
|
||||||
return s.backup(ctx, dbProgram, job)
|
}
|
||||||
case entity.DbJobTypeRestore:
|
switch t := job.(type) {
|
||||||
return s.restore(ctx, dbProgram, job)
|
case *entity.DbBackup:
|
||||||
case entity.DbJobTypeBinlog:
|
return s.backup(ctx, dbProgram, t)
|
||||||
return s.fetchBinlog(ctx, dbProgram, job.GetInstanceId(), false)
|
case *entity.DbRestore:
|
||||||
default:
|
return s.restore(ctx, dbProgram, t)
|
||||||
return fmt.Errorf("无效的数据库任务类型: %v", typ)
|
case *entity.DbBinlog:
|
||||||
|
return s.fetchBinlog(ctx, dbProgram, t.DbInstanceId, false, time.Now())
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("无效的数据库任务类型: %T", t)
|
||||||
}
|
}
|
||||||
//status := entity.DbJobSuccess
|
|
||||||
//if errRun != nil {
|
|
||||||
// status = entity.DbJobFailed
|
|
||||||
//}
|
|
||||||
//job.SetLastStatus(status, errRun)
|
|
||||||
//if err := s.updateLastStatus(ctx, job); err != nil {
|
|
||||||
// logx.Errorf("failed to update job status: %v", err)
|
|
||||||
// return
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbScheduler) runnableJob(job entity.DbJob, next runner.NextJobFunc[entity.DbJob]) (bool, error) {
|
func (s *dbScheduler) runnableJob(job entity.DbJob, nextRunning runner.NextJobFunc[entity.DbJob]) (bool, error) {
|
||||||
if job.IsExpired() {
|
if job.IsExpired() {
|
||||||
return false, runner.ErrJobExpired
|
return false, runner.ErrJobExpired
|
||||||
}
|
}
|
||||||
const maxCountByInstanceId = 4
|
const maxCountByInstanceId = 4
|
||||||
const maxCountByDbName = 1
|
const maxCountByDbName = 1
|
||||||
var countByInstanceId, countByDbName int
|
var countByInstanceId, countByDbName int
|
||||||
for item, ok := next(); ok; item, ok = next() {
|
for item, ok := nextRunning(); ok; item, ok = nextRunning() {
|
||||||
if job.GetInstanceId() == item.GetInstanceId() {
|
if job.GetInstanceId() == item.GetInstanceId() {
|
||||||
countByInstanceId++
|
countByInstanceId++
|
||||||
if countByInstanceId >= maxCountByInstanceId {
|
if countByInstanceId >= maxCountByInstanceId {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if relatedToBinlog(job.GetJobType()) {
|
|
||||||
// todo: 恢复数据库前触发 BINLOG 同步,BINLOG 同步完成后才能恢复数据库
|
|
||||||
if relatedToBinlog(item.GetJobType()) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if job.GetDbName() == item.GetDbName() {
|
if job.GetDbName() == item.GetDbName() {
|
||||||
countByDbName++
|
countByDbName++
|
||||||
if countByDbName >= maxCountByDbName {
|
if countByDbName >= maxCountByDbName {
|
||||||
return false, nil
|
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, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func relatedToBinlog(typ entity.DbJobType) bool {
|
|
||||||
return typ == entity.DbJobTypeRestore || typ == entity.DbJobTypeBinlog
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *dbScheduler) restorePointInTime(ctx context.Context, dbProgram 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)
|
binlogHistory, err := s.binlogHistoryRepo.GetHistoryByTime(job.DbInstanceId, job.PointInTime.Time)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -320,7 +271,7 @@ func (s *dbScheduler) restorePointInTime(ctx context.Context, dbProgram dbi.DbPr
|
|||||||
Sequence: binlogHistory.Sequence,
|
Sequence: binlogHistory.Sequence,
|
||||||
Position: position,
|
Position: position,
|
||||||
}
|
}
|
||||||
backupHistory, err := s.backupHistoryRepo.GetLatestHistory(job.DbInstanceId, job.DbName, target)
|
backupHistory, err := s.backupHistoryRepo.GetLatestHistoryForBinlog(job.DbInstanceId, job.DbName, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -364,6 +315,9 @@ func (s *dbScheduler) restorePointInTime(ctx context.Context, dbProgram dbi.DbPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, backupHistory *entity.DbBackupHistory) (retErr error) {
|
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)
|
ok, err := s.backupHistoryRepo.UpdateRestoring(true, backupHistory.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -385,7 +339,7 @@ func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbPr
|
|||||||
return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
|
return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *dbScheduler) fetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, downloadLatestBinlogFile bool) error {
|
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 {
|
if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !enabled {
|
} else if !enabled {
|
||||||
@@ -397,15 +351,17 @@ func (s *dbScheduler) fetchBinlog(ctx context.Context, dbProgram dbi.DbProgram,
|
|||||||
return errors.New("数据库未启用 BINLOG 行模式")
|
return errors.New("数据库未启用 BINLOG 行模式")
|
||||||
}
|
}
|
||||||
|
|
||||||
latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
|
earliestBackupSequence := int64(-1)
|
||||||
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(instanceId)
|
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(instanceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if ok {
|
if downloadLatestBinlogFile && targetTime.Before(binlogHistory.LastEventTime) {
|
||||||
latestBinlogSequence = binlogHistory.Sequence
|
return nil
|
||||||
} else {
|
}
|
||||||
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistory(instanceId)
|
|
||||||
|
if !ok {
|
||||||
|
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistoryForBinlog(instanceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -414,7 +370,9 @@ func (s *dbScheduler) fetchBinlog(ctx context.Context, dbProgram dbi.DbProgram,
|
|||||||
}
|
}
|
||||||
earliestBackupSequence = backupHistory.BinlogSequence
|
earliestBackupSequence = backupHistory.BinlogSequence
|
||||||
}
|
}
|
||||||
binlogFiles, err := dbProgram.FetchBinlogs(ctx, downloadLatestBinlogFile, earliestBackupSequence, latestBinlogSequence)
|
|
||||||
|
// todo: 将循环从 dbProgram.FetchBinlogs 中提取出来,实现 BINLOG 同步成功后逐一保存 binlogHistory
|
||||||
|
binlogFiles, err := dbProgram.FetchBinlogs(ctx, downloadLatestBinlogFile, earliestBackupSequence, binlogHistory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type DbProgram interface {
|
|||||||
|
|
||||||
Backup(ctx context.Context, backupHistory *entity.DbBackupHistory) (*entity.BinlogInfo, error)
|
Backup(ctx context.Context, backupHistory *entity.DbBackupHistory) (*entity.BinlogInfo, error)
|
||||||
|
|
||||||
FetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool, earliestBackupSequence, latestBinlogSequence int64) ([]*entity.BinlogFile, error)
|
FetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool, earliestBackupSequence int64, latestBinlogHistory *entity.DbBinlogHistory) ([]*entity.BinlogFile, error)
|
||||||
|
|
||||||
ReplayBinlog(ctx context.Context, originalDatabase, targetDatabase string, restoreInfo *RestoreInfo) error
|
ReplayBinlog(ctx context.Context, originalDatabase, targetDatabase string, restoreInfo *RestoreInfo) error
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@ type DbProgram interface {
|
|||||||
RemoveBackupHistory(ctx context.Context, dbBackupId uint64, dbBackupHistoryUuid string) error
|
RemoveBackupHistory(ctx context.Context, dbBackupId uint64, dbBackupHistoryUuid string) error
|
||||||
|
|
||||||
GetBinlogEventPositionAtOrAfterTime(ctx context.Context, binlogName string, targetTime time.Time) (position int64, parseErr error)
|
GetBinlogEventPositionAtOrAfterTime(ctx context.Context, binlogName string, targetTime time.Time) (position int64, parseErr error)
|
||||||
|
|
||||||
|
PruneBinlog(history *entity.DbBinlogHistory) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type RestoreInfo struct {
|
type RestoreInfo struct {
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ import (
|
|||||||
type DbType string
|
type DbType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DbTypeMysql DbType = "mysql"
|
DbTypeMysql DbType = "mysql"
|
||||||
DbTypeMariadb DbType = "mariadb"
|
DbTypeMariadb DbType = "mariadb"
|
||||||
DbTypePostgres DbType = "postgres"
|
DbTypePostgres DbType = "postgres"
|
||||||
DbTypeGauss DbType = "gauss"
|
DbTypeGauss DbType = "gauss"
|
||||||
DbTypeDM DbType = "dm"
|
DbTypeDM DbType = "dm"
|
||||||
DbTypeOracle DbType = "oracle"
|
DbTypeOracle DbType = "oracle"
|
||||||
DbTypeSqlite DbType = "sqlite"
|
DbTypeSqlite DbType = "sqlite"
|
||||||
DbTypeMssql DbType = "mssql"
|
DbTypeMssql DbType = "mssql"
|
||||||
|
DbTypeKingbaseEs DbType = "kingbaseEs"
|
||||||
|
DbTypeVastbase DbType = "vastbase"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ToDbType(dbType string) DbType {
|
func ToDbType(dbType string) DbType {
|
||||||
@@ -44,7 +46,7 @@ func (dbType DbType) QuoteIdentifier(name string) string {
|
|||||||
switch dbType {
|
switch dbType {
|
||||||
case DbTypeMysql, DbTypeMariadb:
|
case DbTypeMysql, DbTypeMariadb:
|
||||||
return quoteIdentifier(name, "`")
|
return quoteIdentifier(name, "`")
|
||||||
case DbTypePostgres, DbTypeGauss:
|
case DbTypePostgres, DbTypeGauss, DbTypeKingbaseEs, DbTypeVastbase:
|
||||||
return quoteIdentifier(name, `"`)
|
return quoteIdentifier(name, `"`)
|
||||||
case DbTypeMssql:
|
case DbTypeMssql:
|
||||||
return fmt.Sprintf("[%s]", name)
|
return fmt.Sprintf("[%s]", name)
|
||||||
@@ -57,7 +59,7 @@ func (dbType DbType) RemoveQuote(name string) string {
|
|||||||
switch dbType {
|
switch dbType {
|
||||||
case DbTypeMysql, DbTypeMariadb:
|
case DbTypeMysql, DbTypeMariadb:
|
||||||
return removeQuote(name, "`")
|
return removeQuote(name, "`")
|
||||||
case DbTypePostgres, DbTypeGauss:
|
case DbTypePostgres, DbTypeGauss, DbTypeKingbaseEs, DbTypeVastbase:
|
||||||
return removeQuote(name, `"`)
|
return removeQuote(name, `"`)
|
||||||
default:
|
default:
|
||||||
return removeQuote(name, `"`)
|
return removeQuote(name, `"`)
|
||||||
@@ -70,7 +72,7 @@ func (dbType DbType) QuoteLiteral(literal string) string {
|
|||||||
literal = strings.ReplaceAll(literal, `\`, `\\`)
|
literal = strings.ReplaceAll(literal, `\`, `\\`)
|
||||||
literal = strings.ReplaceAll(literal, `'`, `''`)
|
literal = strings.ReplaceAll(literal, `'`, `''`)
|
||||||
return "'" + literal + "'"
|
return "'" + literal + "'"
|
||||||
case DbTypePostgres, DbTypeGauss:
|
case DbTypePostgres, DbTypeGauss, DbTypeKingbaseEs, DbTypeVastbase:
|
||||||
return pq.QuoteLiteral(literal)
|
return pq.QuoteLiteral(literal)
|
||||||
default:
|
default:
|
||||||
return pq.QuoteLiteral(literal)
|
return pq.QuoteLiteral(literal)
|
||||||
@@ -85,6 +87,10 @@ func (dbType DbType) MetaDbName() string {
|
|||||||
return "postgres"
|
return "postgres"
|
||||||
case DbTypeDM:
|
case DbTypeDM:
|
||||||
return ""
|
return ""
|
||||||
|
case DbTypeKingbaseEs:
|
||||||
|
return "security"
|
||||||
|
case DbTypeVastbase:
|
||||||
|
return "vastbase"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -94,7 +100,7 @@ func (dbType DbType) Dialect() sqlparser.Dialect {
|
|||||||
switch dbType {
|
switch dbType {
|
||||||
case DbTypeMysql, DbTypeMariadb:
|
case DbTypeMysql, DbTypeMariadb:
|
||||||
return sqlparser.MysqlDialect{}
|
return sqlparser.MysqlDialect{}
|
||||||
case DbTypePostgres, DbTypeGauss:
|
case DbTypePostgres, DbTypeGauss, DbTypeKingbaseEs, DbTypeVastbase:
|
||||||
return sqlparser.PostgresDialect{}
|
return sqlparser.PostgresDialect{}
|
||||||
default:
|
default:
|
||||||
return sqlparser.PostgresDialect{}
|
return sqlparser.PostgresDialect{}
|
||||||
@@ -122,7 +128,7 @@ func (dbType DbType) StmtSetForeignKeyChecks(check bool) string {
|
|||||||
} else {
|
} else {
|
||||||
return "SET FOREIGN_KEY_CHECKS = 0;\n"
|
return "SET FOREIGN_KEY_CHECKS = 0;\n"
|
||||||
}
|
}
|
||||||
case DbTypePostgres, DbTypeGauss:
|
case DbTypePostgres, DbTypeGauss, DbTypeKingbaseEs, DbTypeVastbase:
|
||||||
// not currently supported postgres
|
// not currently supported postgres
|
||||||
return ""
|
return ""
|
||||||
default:
|
default:
|
||||||
@@ -134,10 +140,19 @@ func (dbType DbType) StmtUseDatabase(dbName string) string {
|
|||||||
switch dbType {
|
switch dbType {
|
||||||
case DbTypeMysql, DbTypeMariadb:
|
case DbTypeMysql, DbTypeMariadb:
|
||||||
return fmt.Sprintf("USE %s;\n", dbType.QuoteIdentifier(dbName))
|
return fmt.Sprintf("USE %s;\n", dbType.QuoteIdentifier(dbName))
|
||||||
case DbTypePostgres, DbTypeGauss:
|
case DbTypePostgres, DbTypeGauss, DbTypeKingbaseEs, DbTypeVastbase:
|
||||||
// not currently supported postgres
|
// not currently supported postgres
|
||||||
return ""
|
return ""
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dbType DbType) SupportingBackup() bool {
|
||||||
|
switch dbType {
|
||||||
|
case DbTypeMysql, DbTypeMariadb:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ type Dialect interface {
|
|||||||
GetSchemas() ([]string, error)
|
GetSchemas() ([]string, error)
|
||||||
|
|
||||||
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||||
GetDbProgram() DbProgram
|
GetDbProgram() (DbProgram, error)
|
||||||
|
|
||||||
// 批量保存数据
|
// 批量保存数据
|
||||||
BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error)
|
BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
--MSSQL_DBS 数据库名信息
|
--MSSQL_DBS 数据库名信息
|
||||||
SELECT name AS dbname
|
SELECT name AS dbname
|
||||||
FROM sys.databases
|
FROM sys.databases
|
||||||
WHERE owner_sid = SUSER_SID()
|
|
||||||
and name not in ('master', 'tempdb', 'model', 'msdb')
|
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
--MSSQL_TABLE_DETAIL 查询表名和表注释
|
--MSSQL_TABLE_DETAIL 查询表名和表注释
|
||||||
SELECT t.name AS tableName,
|
SELECT t.name AS tableName,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
--ORACLE_DB_SCHEMAS 库schemas
|
--ORACLE_DB_SCHEMAS 库schemas
|
||||||
select distinct owner as SCHEMA_NAME
|
select USERNAME
|
||||||
from all_objects
|
from sys.all_users
|
||||||
order by owner
|
order by USERNAME
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
--ORACLE_TABLE_INFO 表详细信息
|
--ORACLE_TABLE_INFO 表详细信息
|
||||||
select a.TABLE_NAME,
|
select a.TABLE_NAME,
|
||||||
@@ -10,9 +10,9 @@ select a.TABLE_NAME,
|
|||||||
d.BYTES as DATA_LENGTH,
|
d.BYTES as DATA_LENGTH,
|
||||||
0 as INDEX_LENGTH,
|
0 as INDEX_LENGTH,
|
||||||
a.NUM_ROWS as TABLE_ROWS
|
a.NUM_ROWS as TABLE_ROWS
|
||||||
from all_tables a
|
from ALL_TABLES a
|
||||||
left join ALL_TAB_COMMENTS b on b.TABLE_NAME = a.TABLE_NAME AND b.OWNER = a.OWNER
|
left join ALL_TAB_COMMENTS b on b.TABLE_NAME = a.TABLE_NAME AND b.OWNER = a.OWNER
|
||||||
left join all_objects c on c.OBJECT_TYPE = 'TABLE' AND c.OWNER = a.OWNER AND c.OBJECT_NAME = a.TABLE_NAME
|
left join ALL_OBJECTS c on c.OBJECT_TYPE = 'TABLE' AND c.OWNER = a.OWNER AND c.OBJECT_NAME = a.TABLE_NAME
|
||||||
left join dba_segments d on d.SEGMENT_TYPE = 'TABLE' AND d.OWNER = a.OWNER AND d.SEGMENT_NAME = a.TABLE_NAME
|
left join dba_segments d on d.SEGMENT_TYPE = 'TABLE' AND d.OWNER = a.OWNER AND d.SEGMENT_NAME = a.TABLE_NAME
|
||||||
where a.owner = (SELECT sys_context('USERENV', 'CURRENT_SCHEMA') FROM dual)
|
where a.owner = (SELECT sys_context('USERENV', 'CURRENT_SCHEMA') FROM dual)
|
||||||
ORDER BY a.TABLE_NAME
|
ORDER BY a.TABLE_NAME
|
||||||
@@ -55,12 +55,12 @@ SELECT a.TABLE_NAME as TABLE_NAME,
|
|||||||
a.DATA_SCALE as NUM_SCALE,
|
a.DATA_SCALE as NUM_SCALE,
|
||||||
CASE WHEN d.pri IS NOT NULL THEN 1 ELSE 0 END as IS_PRIMARY_KEY,
|
CASE WHEN d.pri IS NOT NULL THEN 1 ELSE 0 END as IS_PRIMARY_KEY,
|
||||||
CASE WHEN a.IDENTITY_COLUMN = 'YES' THEN 1 ELSE 0 END as IS_IDENTITY
|
CASE WHEN a.IDENTITY_COLUMN = 'YES' THEN 1 ELSE 0 END as IS_IDENTITY
|
||||||
FROM all_tab_columns a
|
FROM ALL_TAB_COLUMNS a
|
||||||
LEFT JOIN all_col_comments b
|
LEFT JOIN ALL_COL_COMMENTS b
|
||||||
on a.OWNER = b.OWNER AND a.TABLE_NAME = b.TABLE_NAME AND a.COLUMN_NAME = b.COLUMN_NAME
|
on a.OWNER = b.OWNER AND a.TABLE_NAME = b.TABLE_NAME AND a.COLUMN_NAME = b.COLUMN_NAME
|
||||||
LEFT JOIN (select ac.TABLE_NAME, ac.OWNER, cc.COLUMN_NAME, 1 as pri
|
LEFT JOIN (select ac.TABLE_NAME, ac.OWNER, cc.COLUMN_NAME, 1 as pri
|
||||||
from all_constraints ac
|
from ALL_CONSTRAINTS ac
|
||||||
join all_cons_columns cc on cc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME AND cc.OWNER = ac.OWNER
|
join ALL_CONS_COLUMNS cc on cc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME AND cc.OWNER = ac.OWNER
|
||||||
where cc.CONSTRAINT_NAME IS NOT NULL
|
where cc.CONSTRAINT_NAME IS NOT NULL
|
||||||
AND ac.CONSTRAINT_TYPE = 'P') d
|
AND ac.CONSTRAINT_TYPE = 'P') d
|
||||||
on d.OWNER = a.OWNER AND d.TABLE_NAME = a.TABLE_NAME AND d.COLUMN_NAME = a.COLUMN_NAME
|
on d.OWNER = a.OWNER AND d.TABLE_NAME = a.TABLE_NAME AND d.COLUMN_NAME = a.COLUMN_NAME
|
||||||
|
|||||||
@@ -248,8 +248,8 @@ func (dd *DMDialect) GetSchemas() ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||||
func (dd *DMDialect) GetDbProgram() dbi.DbProgram {
|
func (dd *DMDialect) GetDbProgram() (dbi.DbProgram, error) {
|
||||||
panic("implement me")
|
return nil, fmt.Errorf("该数据库类型不支持数据库备份与恢复: %v", dd.dc.Info.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -285,8 +285,8 @@ func (md *MssqlDialect) GetSchemas() ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||||
func (md *MssqlDialect) GetDbProgram() dbi.DbProgram {
|
func (md *MssqlDialect) GetDbProgram() (dbi.DbProgram, error) {
|
||||||
panic("implement me")
|
return nil, fmt.Errorf("该数据库类型不支持数据库备份与恢复: %v", md.dc.Info.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MssqlDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
func (md *MssqlDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
||||||
|
|||||||
@@ -38,8 +38,17 @@ func (md *Meta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
|||||||
query.Add("database", d.Database)
|
query.Add("database", d.Database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
params := query.Encode()
|
||||||
|
if d.Params != "" {
|
||||||
|
if !strings.HasPrefix(d.Params, "&") {
|
||||||
|
params = params + "&" + d.Params
|
||||||
|
} else {
|
||||||
|
params = params + d.Params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const driverName = "mssql"
|
const driverName = "mssql"
|
||||||
dsn := fmt.Sprintf("sqlserver://%s:%s@%s:%d?%s", url.PathEscape(d.Username), url.PathEscape(d.Password), d.Host, d.Port, query.Encode())
|
dsn := fmt.Sprintf("sqlserver://%s:%s@%s:%d?%s", url.PathEscape(d.Username), url.PathEscape(d.Password), d.Host, d.Port, params)
|
||||||
return sql.Open(driverName, dsn)
|
return sql.Open(driverName, dsn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,8 +169,8 @@ func (md *MysqlDialect) GetSchemas() ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||||
func (md *MysqlDialect) GetDbProgram() dbi.DbProgram {
|
func (md *MysqlDialect) GetDbProgram() (dbi.DbProgram, error) {
|
||||||
return NewDbProgramMysql(md.dc)
|
return NewDbProgramMysql(md.dc), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MysqlDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
func (md *MysqlDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package mysql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -130,22 +131,46 @@ func (svc *DbProgramMysql) Backup(ctx context.Context, backupHistory *entity.DbB
|
|||||||
if binlogEnabled && rowFormatEnabled {
|
if binlogEnabled && rowFormatEnabled {
|
||||||
binlogInfo, err = readBinlogInfoFromBackup(reader)
|
binlogInfo, err = readBinlogInfoFromBackup(reader)
|
||||||
}
|
}
|
||||||
_ = reader.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = reader.Close()
|
||||||
return nil, errors.Wrapf(err, "从备份文件中读取 binlog 信息失败")
|
return nil, errors.Wrapf(err, "从备份文件中读取 binlog 信息失败")
|
||||||
}
|
}
|
||||||
fileName := filepath.Join(dir, fmt.Sprintf("%s.sql", backupHistory.Uuid))
|
|
||||||
if err := os.Rename(tmpFile, fileName); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "备份文件改名失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
||||||
|
_ = reader.Close()
|
||||||
|
return nil, errors.Wrapf(err, "跳转到备份文件开始处失败")
|
||||||
|
}
|
||||||
|
gzipTmpFile := tmpFile + ".gz"
|
||||||
|
writer, err := os.Create(gzipTmpFile)
|
||||||
|
if err != nil {
|
||||||
|
_ = reader.Close()
|
||||||
|
return nil, errors.Wrapf(err, "创建备份压缩文件失败")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = os.Remove(gzipTmpFile)
|
||||||
|
}()
|
||||||
|
gzipWriter := gzip.NewWriter(writer)
|
||||||
|
gzipWriter.Name = backupHistory.Uuid + ".sql"
|
||||||
|
_, err = io.Copy(gzipWriter, reader)
|
||||||
|
_ = gzipWriter.Close()
|
||||||
|
_ = writer.Close()
|
||||||
|
_ = reader.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "压缩备份文件失败")
|
||||||
|
}
|
||||||
|
destPath := filepath.Join(dir, backupHistory.Uuid+".sql")
|
||||||
|
if err := os.Rename(gzipTmpFile, destPath+".gz"); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "备份文件更名失败")
|
||||||
|
}
|
||||||
return binlogInfo, nil
|
return binlogInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *DbProgramMysql) RemoveBackupHistory(_ context.Context, dbBackupId uint64, dbBackupHistoryUuid string) error {
|
func (svc *DbProgramMysql) RemoveBackupHistory(_ context.Context, dbBackupId uint64, dbBackupHistoryUuid string) error {
|
||||||
fileName := filepath.Join(svc.getDbBackupDir(svc.dbInfo().InstanceId, dbBackupId),
|
fileName := filepath.Join(svc.getDbBackupDir(svc.dbInfo().InstanceId, dbBackupId),
|
||||||
fmt.Sprintf("%v.sql", dbBackupHistoryUuid))
|
fmt.Sprintf("%v.sql", dbBackupHistoryUuid))
|
||||||
return os.Remove(fileName)
|
_ = os.Remove(fileName)
|
||||||
|
_ = os.Remove(fileName + ".gz")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *DbProgramMysql) RestoreBackupHistory(ctx context.Context, dbName string, dbBackupId uint64, dbBackupHistoryUuid string) error {
|
func (svc *DbProgramMysql) RestoreBackupHistory(ctx context.Context, dbName string, dbBackupId uint64, dbBackupHistoryUuid string) error {
|
||||||
@@ -158,18 +183,33 @@ func (svc *DbProgramMysql) RestoreBackupHistory(ctx context.Context, dbName stri
|
|||||||
"--password=" + dbInfo.Password,
|
"--password=" + dbInfo.Password,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compressed := false
|
||||||
fileName := filepath.Join(svc.getDbBackupDir(svc.dbInfo().InstanceId, dbBackupId),
|
fileName := filepath.Join(svc.getDbBackupDir(svc.dbInfo().InstanceId, dbBackupId),
|
||||||
fmt.Sprintf("%v.sql", dbBackupHistoryUuid))
|
fmt.Sprintf("%v.sql", dbBackupHistoryUuid))
|
||||||
|
_, err := os.Stat(fileName)
|
||||||
|
if err != nil {
|
||||||
|
compressed = true
|
||||||
|
fileName += ".gz"
|
||||||
|
}
|
||||||
file, err := os.Open(fileName)
|
file, err := os.Open(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "打开备份文件失败")
|
return errors.Wrap(err, "打开备份文件失败")
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() { _ = file.Close() }()
|
||||||
_ = file.Close()
|
|
||||||
}()
|
var reader io.ReadCloser
|
||||||
|
if compressed {
|
||||||
|
reader, err = gzip.NewReader(file)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "解压缩备份文件失败")
|
||||||
|
}
|
||||||
|
defer func() { _ = reader.Close() }()
|
||||||
|
} else {
|
||||||
|
reader = file
|
||||||
|
}
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, svc.getMysqlBin().MysqlPath, args...)
|
cmd := exec.CommandContext(ctx, svc.getMysqlBin().MysqlPath, args...)
|
||||||
cmd.Stdin = file
|
cmd.Stdin = reader
|
||||||
logx.Debug("恢复数据库: ", cmd.String())
|
logx.Debug("恢复数据库: ", cmd.String())
|
||||||
if err := runCmd(cmd); err != nil {
|
if err := runCmd(cmd); err != nil {
|
||||||
logx.Errorf("运行 mysql 程序失败: %v", err)
|
logx.Errorf("运行 mysql 程序失败: %v", err)
|
||||||
@@ -205,13 +245,17 @@ func (svc *DbProgramMysql) downloadBinlogFilesOnServer(ctx context.Context, binl
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse the first binlog eventTs of a local binlog file.
|
// Parse the first binlog eventTs of a local binlog file.
|
||||||
func (svc *DbProgramMysql) parseLocalBinlogLastEventTime(ctx context.Context, filePath string) (eventTime time.Time, parseErr error) {
|
func (svc *DbProgramMysql) parseLocalBinlogLastEventTime(ctx context.Context, filePath string, lastEventTime time.Time) (eventTime time.Time, parseErr error) {
|
||||||
// todo: implement me
|
return svc.parseLocalBinlogEventTime(ctx, filePath, false, lastEventTime)
|
||||||
return time.Now(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the first binlog eventTs of a local binlog file.
|
// Parse the first binlog eventTs of a local binlog file.
|
||||||
func (svc *DbProgramMysql) parseLocalBinlogFirstEventTime(ctx context.Context, filePath string) (eventTime time.Time, parseErr error) {
|
func (svc *DbProgramMysql) parseLocalBinlogFirstEventTime(ctx context.Context, filePath string) (eventTime time.Time, parseErr error) {
|
||||||
|
return svc.parseLocalBinlogEventTime(ctx, filePath, true, time.Time{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the first binlog eventTs of a local binlog file.
|
||||||
|
func (svc *DbProgramMysql) parseLocalBinlogEventTime(ctx context.Context, filePath string, firstOrLast bool, startTime time.Time) (eventTime time.Time, parseErr error) {
|
||||||
args := []string{
|
args := []string{
|
||||||
// Local binlog file path.
|
// Local binlog file path.
|
||||||
filePath,
|
filePath,
|
||||||
@@ -220,6 +264,9 @@ func (svc *DbProgramMysql) parseLocalBinlogFirstEventTime(ctx context.Context, f
|
|||||||
// Tell mysqlbinlog to suppress the BINLOG statements for row events, which reduces the unneeded output.
|
// Tell mysqlbinlog to suppress the BINLOG statements for row events, which reduces the unneeded output.
|
||||||
"--base64-output=DECODE-ROWS",
|
"--base64-output=DECODE-ROWS",
|
||||||
}
|
}
|
||||||
|
if !startTime.IsZero() {
|
||||||
|
args = append(args, "--start-datetime", startTime.Local().Format(time.DateTime))
|
||||||
|
}
|
||||||
cmd := exec.CommandContext(ctx, svc.getMysqlBin().MysqlbinlogPath, args...)
|
cmd := exec.CommandContext(ctx, svc.getMysqlBin().MysqlbinlogPath, args...)
|
||||||
var stderr strings.Builder
|
var stderr strings.Builder
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
@@ -237,22 +284,30 @@ func (svc *DbProgramMysql) parseLocalBinlogFirstEventTime(ctx context.Context, f
|
|||||||
parseErr = errors.Wrap(parseErr, stderr.String())
|
parseErr = errors.Wrap(parseErr, stderr.String())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
lastEventTime := time.Time{}
|
||||||
for s := bufio.NewScanner(pr); s.Scan(); {
|
for s := bufio.NewScanner(pr); s.Scan(); {
|
||||||
line := s.Text()
|
line := s.Text()
|
||||||
eventTimeParsed, found, err := parseBinlogEventTimeInLine(line)
|
eventTimeParsed, found, err := parseBinlogEventTimeInLine(line)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, errors.Wrap(err, "解析 binlog 文件失败")
|
return time.Time{}, errors.Wrap(err, "解析 binlog 文件失败")
|
||||||
}
|
}
|
||||||
if found {
|
if !found {
|
||||||
return eventTimeParsed, nil
|
continue
|
||||||
}
|
}
|
||||||
|
if !firstOrLast {
|
||||||
|
lastEventTime = eventTimeParsed
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return eventTimeParsed, nil
|
||||||
}
|
}
|
||||||
return time.Time{}, errors.New("解析 binlog 文件失败")
|
if lastEventTime.IsZero() {
|
||||||
|
return time.Time{}, errors.New("解析 binlog 文件失败")
|
||||||
|
}
|
||||||
|
return lastEventTime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchBinlogs downloads binlog files from startingFileName on server to `binlogDir`.
|
// FetchBinlogs downloads binlog files from startingFileName on server to `binlogDir`.
|
||||||
func (svc *DbProgramMysql) FetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool, earliestBackupSequence, latestBinlogSequence int64) ([]*entity.BinlogFile, error) {
|
func (svc *DbProgramMysql) FetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool, earliestBackupSequence int64, latestBinlogHistory *entity.DbBinlogHistory) ([]*entity.BinlogFile, error) {
|
||||||
// Read binlog files list on server.
|
// Read binlog files list on server.
|
||||||
binlogFilesOnServerSorted, err := svc.GetSortedBinlogFilesOnServer(ctx)
|
binlogFilesOnServerSorted, err := svc.GetSortedBinlogFilesOnServer(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -264,8 +319,11 @@ func (svc *DbProgramMysql) FetchBinlogs(ctx context.Context, downloadLatestBinlo
|
|||||||
}
|
}
|
||||||
indexHistory := -1
|
indexHistory := -1
|
||||||
for i, file := range binlogFilesOnServerSorted {
|
for i, file := range binlogFilesOnServerSorted {
|
||||||
if latestBinlogSequence == file.Sequence {
|
if latestBinlogHistory.Sequence == file.Sequence {
|
||||||
indexHistory = i + 1
|
indexHistory = i + 1
|
||||||
|
file.FirstEventTime = latestBinlogHistory.FirstEventTime
|
||||||
|
file.LastEventTime = latestBinlogHistory.LastEventTime
|
||||||
|
file.LocalSize = latestBinlogHistory.FileSize
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if earliestBackupSequence == file.Sequence {
|
if earliestBackupSequence == file.Sequence {
|
||||||
@@ -274,10 +332,15 @@ func (svc *DbProgramMysql) FetchBinlogs(ctx context.Context, downloadLatestBinlo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if indexHistory < 0 {
|
if indexHistory < 0 {
|
||||||
return nil, errors.New(fmt.Sprintf("在数据库服务器上未找到 binlog 文件: %d, %d", earliestBackupSequence, latestBinlogSequence))
|
// todo: 数据库服务器上的 binlog 序列已被删除, 导致 binlog 同步失败,如何处理?
|
||||||
|
return nil, errors.New(fmt.Sprintf("数据库服务器上的 binlog 序列已被删除: %d, %d", earliestBackupSequence, latestBinlogHistory.Sequence))
|
||||||
}
|
}
|
||||||
if indexHistory > len(binlogFilesOnServerSorted)-1 {
|
if indexHistory >= len(binlogFilesOnServerSorted)-1 {
|
||||||
indexHistory = len(binlogFilesOnServerSorted) - 1
|
indexHistory = len(binlogFilesOnServerSorted) - 1
|
||||||
|
if binlogFilesOnServerSorted[indexHistory].LocalSize == binlogFilesOnServerSorted[indexHistory].RemoteSize {
|
||||||
|
// 没有新的事件,不需要重新下载
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
binlogFilesOnServerSorted = binlogFilesOnServerSorted[indexHistory:]
|
binlogFilesOnServerSorted = binlogFilesOnServerSorted[indexHistory:]
|
||||||
|
|
||||||
@@ -331,13 +394,14 @@ func (svc *DbProgramMysql) downloadBinlogFile(ctx context.Context, binlogFileToD
|
|||||||
logx.Error("未找到 binlog 文件", logx.String("path", binlogFilePathTemp), logx.String("error", err.Error()))
|
logx.Error("未找到 binlog 文件", logx.String("path", binlogFilePathTemp), logx.String("error", err.Error()))
|
||||||
return errors.Wrapf(err, "未找到 binlog 文件: %q", binlogFilePathTemp)
|
return errors.Wrapf(err, "未找到 binlog 文件: %q", binlogFilePathTemp)
|
||||||
}
|
}
|
||||||
if !isLast && binlogFileTempInfo.Size() != binlogFileToDownload.Size {
|
|
||||||
|
if (isLast && binlogFileTempInfo.Size() < binlogFileToDownload.RemoteSize) || (!isLast && binlogFileTempInfo.Size() != binlogFileToDownload.RemoteSize) {
|
||||||
logx.Error("Downloaded archived binlog file size is not equal to size queried on the MySQL server earlier.",
|
logx.Error("Downloaded archived binlog file size is not equal to size queried on the MySQL server earlier.",
|
||||||
logx.String("binlog", binlogFileToDownload.Name),
|
logx.String("binlog", binlogFileToDownload.Name),
|
||||||
logx.Int64("sizeInfo", binlogFileToDownload.Size),
|
logx.Int64("sizeInfo", binlogFileToDownload.RemoteSize),
|
||||||
logx.Int64("downloadedSize", binlogFileTempInfo.Size()),
|
logx.Int64("downloadedSize", binlogFileTempInfo.Size()),
|
||||||
)
|
)
|
||||||
return errors.Errorf("下载的 binlog 文件 %q 与服务上的文件大小不一致 %d != %d", binlogFilePathTemp, binlogFileTempInfo.Size(), binlogFileToDownload.Size)
|
return errors.Errorf("下载的 binlog 文件 %q 与服务上的文件大小不一致 %d != %d", binlogFilePathTemp, binlogFileTempInfo.Size(), binlogFileToDownload.RemoteSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
binlogFilePath := svc.GetBinlogFilePath(binlogFileToDownload.Name)
|
binlogFilePath := svc.GetBinlogFilePath(binlogFileToDownload.Name)
|
||||||
@@ -348,7 +412,7 @@ func (svc *DbProgramMysql) downloadBinlogFile(ctx context.Context, binlogFileToD
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
lastEventTime, err := svc.parseLocalBinlogLastEventTime(ctx, binlogFilePath)
|
lastEventTime, err := svc.parseLocalBinlogLastEventTime(ctx, binlogFilePath, binlogFileToDownload.LastEventTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -394,9 +458,9 @@ func (svc *DbProgramMysql) GetSortedBinlogFilesOnServer(_ context.Context) ([]*e
|
|||||||
return nil, errors.Wrapf(err, "SQL 语句 %q 执行结果解析失败", query)
|
return nil, errors.Wrapf(err, "SQL 语句 %q 执行结果解析失败", query)
|
||||||
}
|
}
|
||||||
binlogFile := &entity.BinlogFile{
|
binlogFile := &entity.BinlogFile{
|
||||||
Name: name,
|
Name: name,
|
||||||
Size: int64(size),
|
RemoteSize: int64(size),
|
||||||
Sequence: seq,
|
Sequence: seq,
|
||||||
}
|
}
|
||||||
binlogFiles = append(binlogFiles, binlogFile)
|
binlogFiles = append(binlogFiles, binlogFile)
|
||||||
}
|
}
|
||||||
@@ -781,3 +845,9 @@ func (svc *DbProgramMysql) getDbBackupDir(instanceId, backupId uint64) string {
|
|||||||
fmt.Sprintf("instance-%d", instanceId),
|
fmt.Sprintf("instance-%d", instanceId),
|
||||||
fmt.Sprintf("backup-%d", backupId))
|
fmt.Sprintf("backup-%d", backupId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *DbProgramMysql) PruneBinlog(history *entity.DbBinlogHistory) error {
|
||||||
|
binlogFilePath := filepath.Join(svc.getBinlogDir(history.DbInstanceId), history.FileName)
|
||||||
|
_ = os.Remove(binlogFilePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ func (s *DbInstanceSuite) SetupSuite() {
|
|||||||
Username: "test",
|
Username: "test",
|
||||||
Password: "test",
|
Password: "test",
|
||||||
}
|
}
|
||||||
dbConn, err := dbInfo.Conn(GetMeta())
|
dbConn, err := dbInfo.Conn(dbi.GetMeta(dbi.DbTypeMysql))
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
s.dbConn = dbConn
|
s.dbConn = dbConn
|
||||||
s.repositories = &repository.Repositories{
|
s.repositories = &repository.Repositories{
|
||||||
Instance: persistence.GetInstanceRepo(),
|
Instance: persistence.NewInstanceRepo(),
|
||||||
Backup: persistence.NewDbBackupRepo(),
|
Backup: persistence.NewDbBackupRepo(),
|
||||||
BackupHistory: persistence.NewDbBackupHistoryRepo(),
|
BackupHistory: persistence.NewDbBackupHistoryRepo(),
|
||||||
Restore: persistence.NewDbRestoreRepo(),
|
Restore: persistence.NewDbRestoreRepo(),
|
||||||
@@ -111,7 +111,7 @@ func (s *DbInstanceSuite) testBackup(backupHistory *entity.DbBackupHistory) {
|
|||||||
binlogInfo, err := s.instanceSvc.Backup(context.Background(), backupHistory)
|
binlogInfo, err := s.instanceSvc.Backup(context.Background(), backupHistory)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
fileName := filepath.Join(s.instanceSvc.getDbBackupDir(s.dbConn.Info.InstanceId, backupHistory.Id), dbNameBackupTest+".sql")
|
fileName := filepath.Join(s.instanceSvc.getDbBackupDir(s.dbConn.Info.InstanceId, backupHistory.Id), dbNameBackupTest+".sql.gz")
|
||||||
_, err = os.Stat(fileName)
|
_, err = os.Stat(fileName)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
|
|||||||
@@ -242,14 +242,14 @@ func (od *OracleDialect) GetSchemas() ([]string, error) {
|
|||||||
}
|
}
|
||||||
schemaNames := make([]string, 0)
|
schemaNames := make([]string, 0)
|
||||||
for _, re := range res {
|
for _, re := range res {
|
||||||
schemaNames = append(schemaNames, anyx.ConvString(re["SCHEMA_NAME"]))
|
schemaNames = append(schemaNames, anyx.ConvString(re["USERNAME"]))
|
||||||
}
|
}
|
||||||
return schemaNames, nil
|
return schemaNames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||||
func (od *OracleDialect) GetDbProgram() dbi.DbProgram {
|
func (od *OracleDialect) GetDbProgram() (dbi.DbProgram, error) {
|
||||||
panic("implement me")
|
return nil, fmt.Errorf("该数据库类型不支持数据库备份与恢复: %v", od.dc.Info.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (od *OracleDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
func (od *OracleDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type PgsqlDialect struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (md *PgsqlDialect) GetDbServer() (*dbi.DbServer, error) {
|
func (md *PgsqlDialect) GetDbServer() (*dbi.DbServer, error) {
|
||||||
_, res, err := md.dc.Query("SHOW server_version")
|
_, res, err := md.dc.Query("SELECT version() as server_version")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -188,8 +188,8 @@ func (md *PgsqlDialect) GetSchemas() ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||||
func (md *PgsqlDialect) GetDbProgram() dbi.DbProgram {
|
func (md *PgsqlDialect) GetDbProgram() (dbi.DbProgram, error) {
|
||||||
panic("implement me")
|
return nil, fmt.Errorf("该数据库类型不支持数据库备份与恢复: %v", md.dc.Info.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *PgsqlDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
func (md *PgsqlDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
dbi.Register(dbi.DbTypePostgres, new(PostgresMeta))
|
meta := new(PostgresMeta)
|
||||||
|
dbi.Register(dbi.DbTypePostgres, meta)
|
||||||
|
dbi.Register(dbi.DbTypeKingbaseEs, meta)
|
||||||
|
dbi.Register(dbi.DbTypeVastbase, meta)
|
||||||
|
|
||||||
gauss := new(PostgresMeta)
|
gauss := new(PostgresMeta)
|
||||||
gauss.Param = "dbtype=gauss"
|
gauss.Param = "dbtype=gauss"
|
||||||
@@ -40,16 +43,17 @@ func (md *PostgresMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
|||||||
|
|
||||||
db := d.Database
|
db := d.Database
|
||||||
var dbParam string
|
var dbParam string
|
||||||
exsitSchema := false
|
existSchema := false
|
||||||
if db != "" {
|
if db == "" {
|
||||||
// postgres database可以使用db/schema表示,方便连接指定schema, 若不存在schema则使用默认schema
|
db = d.Type.MetaDbName()
|
||||||
ss := strings.Split(db, "/")
|
}
|
||||||
if len(ss) > 1 {
|
// postgres database可以使用db/schema表示,方便连接指定schema, 若不存在schema则使用默认schema
|
||||||
exsitSchema = true
|
ss := strings.Split(db, "/")
|
||||||
dbParam = fmt.Sprintf("dbname=%s search_path=%s", ss[0], ss[len(ss)-1])
|
if len(ss) > 1 {
|
||||||
} else {
|
existSchema = true
|
||||||
dbParam = "dbname=" + db
|
dbParam = fmt.Sprintf("dbname=%s search_path=%s", ss[0], ss[len(ss)-1])
|
||||||
}
|
} else {
|
||||||
|
dbParam = "dbname=" + db
|
||||||
}
|
}
|
||||||
|
|
||||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s %s sslmode=disable connect_timeout=8", d.Host, d.Port, d.Username, d.Password, dbParam)
|
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s %s sslmode=disable connect_timeout=8", d.Host, d.Port, d.Username, d.Password, dbParam)
|
||||||
@@ -62,7 +66,7 @@ func (md *PostgresMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
|||||||
if strings.HasPrefix(param, "dbname=") {
|
if strings.HasPrefix(param, "dbname=") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if exsitSchema && strings.HasPrefix(param, "search_path") {
|
if existSchema && strings.HasPrefix(param, "search_path") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -180,8 +180,8 @@ func (sd *SqliteDialect) GetSchemas() ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||||
func (sd *SqliteDialect) GetDbProgram() dbi.DbProgram {
|
func (sd *SqliteDialect) GetDbProgram() (dbi.DbProgram, error) {
|
||||||
panic("implement me")
|
return nil, fmt.Errorf("该数据库类型不支持数据库备份与恢复: %v", sd.dc.Info.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sd *SqliteDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
func (sd *SqliteDialect) BatchInsert(tx *sql.Tx, tableName string, columns []string, values [][]any) (int64, error) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type DbBackup struct {
|
|||||||
EnabledDesc string // 启用状态描述
|
EnabledDesc string // 启用状态描述
|
||||||
StartTime time.Time // 开始时间
|
StartTime time.Time // 开始时间
|
||||||
Interval time.Duration // 间隔时间
|
Interval time.Duration // 间隔时间
|
||||||
|
MaxSaveDays int // 数据库备份历史保留天数,过期将自动删除
|
||||||
Repeated bool // 是否重复执行
|
Repeated bool // 是否重复执行
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,10 +82,6 @@ func (b *DbBackup) GetInterval() time.Duration {
|
|||||||
return b.Interval
|
return b.Interval
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *DbBackup) SetLastStatus(status DbJobStatus, err error) {
|
|
||||||
b.setLastStatus(b.GetJobType(), status, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *DbBackup) GetKey() DbJobKey {
|
func (b *DbBackup) GetKey() DbJobKey {
|
||||||
return b.getKey(b.GetJobType())
|
return b.getKey(b.GetJobType())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ const (
|
|||||||
|
|
||||||
// BinlogFile is the metadata of the MySQL binlog file.
|
// BinlogFile is the metadata of the MySQL binlog file.
|
||||||
type BinlogFile struct {
|
type BinlogFile struct {
|
||||||
Name string
|
Name string
|
||||||
Size int64
|
RemoteSize int64
|
||||||
|
LocalSize int64
|
||||||
|
|
||||||
// Sequence is parsed from Name and is for the sorting purpose.
|
// Sequence is parsed from Name and is for the sorting purpose.
|
||||||
Sequence int64
|
Sequence int64
|
||||||
FirstEventTime time.Time
|
FirstEventTime time.Time
|
||||||
LastEventTime time.Time
|
LastEventTime time.Time
|
||||||
Downloaded bool
|
|
||||||
|
Downloaded bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ DbJob = (*DbBinlog)(nil)
|
var _ DbJob = (*DbBinlog)(nil)
|
||||||
@@ -76,10 +78,6 @@ func (b *DbBinlog) GetJobType() DbJobType {
|
|||||||
return DbJobTypeBinlog
|
return DbJobTypeBinlog
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *DbBinlog) SetLastStatus(status DbJobStatus, err error) {
|
|
||||||
b.setLastStatus(b.GetJobType(), status, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *DbBinlog) GetKey() DbJobKey {
|
func (b *DbBinlog) GetKey() DbJobKey {
|
||||||
return b.getKey(b.GetJobType())
|
return b.getKey(b.GetJobType())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type DbBinlogHistory struct {
|
|||||||
FileSize int64
|
FileSize int64
|
||||||
Sequence int64
|
Sequence int64
|
||||||
FirstEventTime time.Time
|
FirstEventTime time.Time
|
||||||
|
LastEventTime time.Time
|
||||||
DbInstanceId uint64 `json:"dbInstanceId"`
|
DbInstanceId uint64 `json:"dbInstanceId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ var _ runner.Job = (DbJob)(nil)
|
|||||||
|
|
||||||
type DbJobBase interface {
|
type DbJobBase interface {
|
||||||
model.ModelI
|
model.ModelI
|
||||||
GetLastStatus() DbJobStatus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DbJob interface {
|
type DbJob interface {
|
||||||
@@ -62,7 +61,6 @@ type DbJob interface {
|
|||||||
SetEnabled(enabled bool, desc string)
|
SetEnabled(enabled bool, desc string)
|
||||||
Update(job runner.Job)
|
Update(job runner.Job)
|
||||||
GetInterval() time.Duration
|
GetInterval() time.Duration
|
||||||
SetLastStatus(status DbJobStatus, err error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ DbJobBase = (*DbJobBaseImpl)(nil)
|
var _ DbJobBase = (*DbJobBaseImpl)(nil)
|
||||||
@@ -84,10 +82,6 @@ func (d *DbJobBaseImpl) getJobType() DbJobType {
|
|||||||
return job.GetJobType()
|
return job.GetJobType()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DbJobBaseImpl) GetLastStatus() DbJobStatus {
|
|
||||||
return d.LastStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DbJobBaseImpl) setLastStatus(jobType DbJobType, status DbJobStatus, err error) {
|
func (d *DbJobBaseImpl) setLastStatus(jobType DbJobType, status DbJobStatus, err error) {
|
||||||
var statusName, jobName string
|
var statusName, jobName string
|
||||||
switch status {
|
switch status {
|
||||||
|
|||||||
@@ -79,10 +79,6 @@ func (r *DbRestore) GetJobType() DbJobType {
|
|||||||
return DbJobTypeRestore
|
return DbJobTypeRestore
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *DbRestore) SetLastStatus(status DbJobStatus, err error) {
|
|
||||||
r.setLastStatus(r.GetJobType(), status, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *DbRestore) GetKey() DbJobKey {
|
func (r *DbRestore) GetKey() DbJobKey {
|
||||||
return r.getKey(r.GetJobType())
|
return r.getKey(r.GetJobType())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DbBackup interface {
|
type DbBackup interface {
|
||||||
DbJob
|
DbJob[*entity.DbBackup]
|
||||||
|
|
||||||
ListToDo(jobs any) error
|
ListToDo(jobs any) error
|
||||||
ListDbInstances(enabled bool, repeated bool, instanceIds *[]uint64) error
|
ListDbInstances(enabled bool, repeated bool, instanceIds *[]uint64) error
|
||||||
@@ -14,4 +14,6 @@ type DbBackup interface {
|
|||||||
|
|
||||||
// GetPageList 分页获取数据库任务列表
|
// GetPageList 分页获取数据库任务列表
|
||||||
GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||||
|
|
||||||
|
ListByCond(cond any, listModels any, cols ...string) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ type DbBackupHistory interface {
|
|||||||
// GetPageList 分页获取数据备份历史
|
// GetPageList 分页获取数据备份历史
|
||||||
GetPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
GetPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||||
|
|
||||||
GetLatestHistory(instanceId uint64, dbName string, bi *entity.BinlogInfo) (*entity.DbBackupHistory, error)
|
GetLatestHistoryForBinlog(instanceId uint64, dbName string, bi *entity.BinlogInfo) (*entity.DbBackupHistory, error)
|
||||||
|
|
||||||
GetEarliestHistory(instanceId uint64) (*entity.DbBackupHistory, bool, error)
|
GetEarliestHistoryForBinlog(instanceId uint64) (*entity.DbBackupHistory, bool, error)
|
||||||
|
|
||||||
GetHistories(backupHistoryIds []uint64, toEntity any) error
|
GetHistories(backupHistoryIds []uint64, toEntity any) error
|
||||||
|
|
||||||
UpdateDeleting(deleting bool, backupHistoryId ...uint64) (bool, error)
|
UpdateDeleting(deleting bool, backupHistoryId ...uint64) (bool, error)
|
||||||
UpdateRestoring(restoring bool, backupHistoryId ...uint64) (bool, error)
|
UpdateRestoring(restoring bool, backupHistoryId ...uint64) (bool, error)
|
||||||
|
ZeroBinlogInfo(backupHistoryId uint64) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DbBinlog interface {
|
type DbBinlog interface {
|
||||||
DbJob
|
DbJob[*entity.DbBinlog]
|
||||||
|
|
||||||
AddJobIfNotExists(ctx context.Context, job *entity.DbBinlog) error
|
AddJobIfNotExists(ctx context.Context, job *entity.DbBinlog) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,4 +19,6 @@ type DbBinlogHistory interface {
|
|||||||
InsertWithBinlogFiles(ctx context.Context, instanceId uint64, binlogFiles []*entity.BinlogFile) error
|
InsertWithBinlogFiles(ctx context.Context, instanceId uint64, binlogFiles []*entity.BinlogFile) error
|
||||||
|
|
||||||
Upsert(ctx context.Context, history *entity.DbBinlogHistory) error
|
Upsert(ctx context.Context, history *entity.DbBinlogHistory) error
|
||||||
|
|
||||||
|
GetHistoriesBeforeSequence(ctx context.Context, instanceId uint64, binlogSeq int64, histories *[]*entity.DbBinlogHistory) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,18 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
|
"mayfly-go/pkg/base"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DbJobBase interface {
|
type DbJobBase[T entity.DbJob] interface {
|
||||||
// GetById 根据实体id查询
|
base.Repo[T]
|
||||||
GetById(e entity.DbJob, id uint64, cols ...string) error
|
|
||||||
|
|
||||||
// UpdateById 根据实体id更新实体信息
|
|
||||||
UpdateById(ctx context.Context, e entity.DbJob, columns ...string) error
|
|
||||||
|
|
||||||
// DeleteById 根据实体主键删除实体
|
|
||||||
DeleteById(ctx context.Context, id uint64) error
|
|
||||||
|
|
||||||
// UpdateLastStatus 更新任务执行状态
|
// UpdateLastStatus 更新任务执行状态
|
||||||
UpdateLastStatus(ctx context.Context, job entity.DbJob) error
|
UpdateLastStatus(ctx context.Context, job entity.DbJob) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type DbJob interface {
|
type DbJob[T entity.DbJob] interface {
|
||||||
DbJobBase
|
DbJobBase[T]
|
||||||
|
|
||||||
// AddJob 添加数据库任务
|
// AddJob 添加数据库任务
|
||||||
AddJob(ctx context.Context, jobs any) error
|
AddJob(ctx context.Context, jobs any) error
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DbRestore interface {
|
type DbRestore interface {
|
||||||
DbJob
|
DbJob[*entity.DbRestore]
|
||||||
|
|
||||||
ListToDo(jobs any) error
|
ListToDo(jobs any) error
|
||||||
GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error)
|
GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error)
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ func (d *dbBackupRepoImpl) ListToDo(jobs any) error {
|
|||||||
|
|
||||||
// GetPageList 分页获取数据库备份任务列表
|
// GetPageList 分页获取数据库备份任务列表
|
||||||
func (d *dbBackupRepoImpl) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
|
func (d *dbBackupRepoImpl) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
|
||||||
d.GetModel()
|
|
||||||
qd := gormx.NewQuery(d.GetModel()).
|
qd := gormx.NewQuery(d.GetModel()).
|
||||||
Eq("id", condition.Id).
|
Eq("id", condition.Id).
|
||||||
Eq0("db_instance_id", condition.DbInstanceId).
|
Eq0("db_instance_id", condition.DbInstanceId).
|
||||||
@@ -83,12 +82,16 @@ func (d *dbBackupRepoImpl) UpdateEnabled(_ context.Context, jobId uint64, enable
|
|||||||
cond := map[string]any{
|
cond := map[string]any{
|
||||||
"id": jobId,
|
"id": jobId,
|
||||||
}
|
}
|
||||||
desc := "任务已禁用"
|
desc := "已禁用"
|
||||||
if enabled {
|
if enabled {
|
||||||
desc = "任务已启用"
|
desc = "已启用"
|
||||||
}
|
}
|
||||||
return d.Updates(cond, map[string]any{
|
return d.Updates(cond, map[string]any{
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
"enabled_desc": desc,
|
"enabled_desc": desc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *dbBackupRepoImpl) ListByCond(cond any, listModels any, cols ...string) error {
|
||||||
|
return d.dbJobBaseImpl.ListByCond(cond, listModels, cols...)
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,12 +34,13 @@ func (repo *dbBackupHistoryRepoImpl) GetPageList(condition *entity.DbBackupHisto
|
|||||||
func (repo *dbBackupHistoryRepoImpl) GetHistories(backupHistoryIds []uint64, toEntity any) error {
|
func (repo *dbBackupHistoryRepoImpl) GetHistories(backupHistoryIds []uint64, toEntity any) error {
|
||||||
return global.Db.Model(repo.GetModel()).
|
return global.Db.Model(repo.GetModel()).
|
||||||
Where("id in ?", backupHistoryIds).
|
Where("id in ?", backupHistoryIds).
|
||||||
|
Where("deleting = false").
|
||||||
Scopes(gormx.UndeleteScope).
|
Scopes(gormx.UndeleteScope).
|
||||||
Find(toEntity).
|
Find(toEntity).
|
||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *dbBackupHistoryRepoImpl) GetLatestHistory(instanceId uint64, dbName string, bi *entity.BinlogInfo) (*entity.DbBackupHistory, error) {
|
func (repo *dbBackupHistoryRepoImpl) GetLatestHistoryForBinlog(instanceId uint64, dbName string, bi *entity.BinlogInfo) (*entity.DbBackupHistory, error) {
|
||||||
history := &entity.DbBackupHistory{}
|
history := &entity.DbBackupHistory{}
|
||||||
db := global.Db
|
db := global.Db
|
||||||
err := db.Model(repo.GetModel()).
|
err := db.Model(repo.GetModel()).
|
||||||
@@ -48,6 +49,8 @@ func (repo *dbBackupHistoryRepoImpl) GetLatestHistory(instanceId uint64, dbName
|
|||||||
Where(db.Where("binlog_sequence < ?", bi.Sequence).
|
Where(db.Where("binlog_sequence < ?", bi.Sequence).
|
||||||
Or(db.Where("binlog_sequence = ?", bi.Sequence).
|
Or(db.Where("binlog_sequence = ?", bi.Sequence).
|
||||||
Where("binlog_position <= ?", bi.Position))).
|
Where("binlog_position <= ?", bi.Position))).
|
||||||
|
Where("binlog_sequence > 0").
|
||||||
|
Where("deleting = false").
|
||||||
Scopes(gormx.UndeleteScope).
|
Scopes(gormx.UndeleteScope).
|
||||||
Order("binlog_sequence desc, binlog_position desc").
|
Order("binlog_sequence desc, binlog_position desc").
|
||||||
First(history).Error
|
First(history).Error
|
||||||
@@ -57,10 +60,12 @@ func (repo *dbBackupHistoryRepoImpl) GetLatestHistory(instanceId uint64, dbName
|
|||||||
return history, err
|
return history, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *dbBackupHistoryRepoImpl) GetEarliestHistory(instanceId uint64) (*entity.DbBackupHistory, bool, error) {
|
func (repo *dbBackupHistoryRepoImpl) GetEarliestHistoryForBinlog(instanceId uint64) (*entity.DbBackupHistory, bool, error) {
|
||||||
history := &entity.DbBackupHistory{}
|
history := &entity.DbBackupHistory{}
|
||||||
db := global.Db.Model(repo.GetModel())
|
db := global.Db.Model(repo.GetModel())
|
||||||
err := db.Where("db_instance_id = ?", instanceId).
|
err := db.Where("db_instance_id = ?", instanceId).
|
||||||
|
Where("binlog_sequence > 0").
|
||||||
|
Where("deleting = false").
|
||||||
Scopes(gormx.UndeleteScope).
|
Scopes(gormx.UndeleteScope).
|
||||||
Order("binlog_sequence").
|
Order("binlog_sequence").
|
||||||
First(history).Error
|
First(history).Error
|
||||||
@@ -79,7 +84,7 @@ func (repo *dbBackupHistoryRepoImpl) UpdateDeleting(deleting bool, backupHistory
|
|||||||
Where("id in ?", backupHistoryId).
|
Where("id in ?", backupHistoryId).
|
||||||
Where("restoring = false").
|
Where("restoring = false").
|
||||||
Scopes(gormx.UndeleteScope).
|
Scopes(gormx.UndeleteScope).
|
||||||
Update("restoring", deleting)
|
Update("deleting", deleting)
|
||||||
if db.Error != nil {
|
if db.Error != nil {
|
||||||
return false, db.Error
|
return false, db.Error
|
||||||
}
|
}
|
||||||
@@ -103,3 +108,15 @@ func (repo *dbBackupHistoryRepoImpl) UpdateRestoring(restoring bool, backupHisto
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (repo *dbBackupHistoryRepoImpl) ZeroBinlogInfo(backupHistoryId uint64) error {
|
||||||
|
return global.Db.Model(repo.GetModel()).
|
||||||
|
Where("id = ?", backupHistoryId).
|
||||||
|
Where("restoring = false").
|
||||||
|
Scopes(gormx.UndeleteScope).
|
||||||
|
Updates(&map[string]any{
|
||||||
|
"binlog_file_name": "",
|
||||||
|
"binlog_sequence": 0,
|
||||||
|
"binlog_position": 0,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"mayfly-go/internal/db/domain/entity"
|
"mayfly-go/internal/db/domain/entity"
|
||||||
"mayfly-go/internal/db/domain/repository"
|
"mayfly-go/internal/db/domain/repository"
|
||||||
"mayfly-go/pkg/base"
|
"mayfly-go/pkg/base"
|
||||||
|
"mayfly-go/pkg/global"
|
||||||
"mayfly-go/pkg/gormx"
|
"mayfly-go/pkg/gormx"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -82,7 +83,7 @@ func (repo *dbBinlogHistoryRepoImpl) Upsert(_ context.Context, history *entity.D
|
|||||||
First(old).Error
|
First(old).Error
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
return db.Model(old).Select("create_time", "file_size", "first_event_time").Updates(history).Error
|
return db.Model(old).Select("create_time", "file_size", "first_event_time", "last_event_time").Updates(history).Error
|
||||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
return db.Create(history).Error
|
return db.Create(history).Error
|
||||||
default:
|
default:
|
||||||
@@ -103,9 +104,10 @@ func (repo *dbBinlogHistoryRepoImpl) InsertWithBinlogFiles(ctx context.Context,
|
|||||||
history := &entity.DbBinlogHistory{
|
history := &entity.DbBinlogHistory{
|
||||||
CreateTime: time.Now(),
|
CreateTime: time.Now(),
|
||||||
FileName: fileOnServer.Name,
|
FileName: fileOnServer.Name,
|
||||||
FileSize: fileOnServer.Size,
|
FileSize: fileOnServer.RemoteSize,
|
||||||
Sequence: fileOnServer.Sequence,
|
Sequence: fileOnServer.Sequence,
|
||||||
FirstEventTime: fileOnServer.FirstEventTime,
|
FirstEventTime: fileOnServer.FirstEventTime,
|
||||||
|
LastEventTime: fileOnServer.LastEventTime,
|
||||||
DbInstanceId: instanceId,
|
DbInstanceId: instanceId,
|
||||||
}
|
}
|
||||||
histories = append(histories, history)
|
histories = append(histories, history)
|
||||||
@@ -122,3 +124,13 @@ func (repo *dbBinlogHistoryRepoImpl) InsertWithBinlogFiles(ctx context.Context,
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (repo *dbBinlogHistoryRepoImpl) GetHistoriesBeforeSequence(ctx context.Context, instanceId uint64, binlogSeq int64, histories *[]*entity.DbBinlogHistory) error {
|
||||||
|
return global.Db.Model(repo.GetModel()).
|
||||||
|
Where("db_instance_id = ?", instanceId).
|
||||||
|
Where("sequence < ?", binlogSeq).
|
||||||
|
Scopes(gormx.UndeleteScope).
|
||||||
|
Order("id").
|
||||||
|
Find(histories).
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,20 +12,12 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ repository.DbJobBase = (*dbJobBaseImpl[entity.DbJob])(nil)
|
var _ repository.DbJobBase[entity.DbJob] = (*dbJobBaseImpl[entity.DbJob])(nil)
|
||||||
|
|
||||||
type dbJobBaseImpl[T entity.DbJob] struct {
|
type dbJobBaseImpl[T entity.DbJob] struct {
|
||||||
base.RepoImpl[T]
|
base.RepoImpl[T]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dbJobBaseImpl[T]) GetById(e entity.DbJob, id uint64, cols ...string) error {
|
|
||||||
return d.RepoImpl.GetById(e.(T), id, cols...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dbJobBaseImpl[T]) UpdateById(ctx context.Context, e entity.DbJob, columns ...string) error {
|
|
||||||
return d.RepoImpl.UpdateById(ctx, e.(T), columns...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dbJobBaseImpl[T]) UpdateLastStatus(ctx context.Context, job entity.DbJob) error {
|
func (d *dbJobBaseImpl[T]) UpdateLastStatus(ctx context.Context, job entity.DbJob) error {
|
||||||
return d.UpdateById(ctx, job.(T), "last_status", "last_result", "last_time")
|
return d.UpdateById(ctx, job.(T), "last_status", "last_result", "last_time")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,9 +84,9 @@ func (d *dbRestoreRepoImpl) UpdateEnabled(_ context.Context, jobId uint64, enabl
|
|||||||
cond := map[string]any{
|
cond := map[string]any{
|
||||||
"id": jobId,
|
"id": jobId,
|
||||||
}
|
}
|
||||||
desc := "任务已禁用"
|
desc := "已禁用"
|
||||||
if enabled {
|
if enabled {
|
||||||
desc = "任务已启用"
|
desc = "已启用"
|
||||||
}
|
}
|
||||||
return d.Updates(cond, map[string]any{
|
return d.Updates(cond, map[string]any{
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type instanceRepoImpl struct {
|
|||||||
base.RepoImpl[*entity.DbInstance]
|
base.RepoImpl[*entity.DbInstance]
|
||||||
}
|
}
|
||||||
|
|
||||||
func newInstanceRepo() repository.Instance {
|
func NewInstanceRepo() repository.Instance {
|
||||||
return &instanceRepoImpl{base.RepoImpl[*entity.DbInstance]{M: new(entity.DbInstance)}}
|
return &instanceRepoImpl{base.RepoImpl[*entity.DbInstance]{M: new(entity.DbInstance)}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
ioc.Register(newInstanceRepo(), ioc.WithComponentName("DbInstanceRepo"))
|
ioc.Register(NewInstanceRepo(), ioc.WithComponentName("DbInstanceRepo"))
|
||||||
ioc.Register(newDbRepo(), ioc.WithComponentName("DbRepo"))
|
ioc.Register(newDbRepo(), ioc.WithComponentName("DbRepo"))
|
||||||
ioc.Register(newDbSqlRepo(), ioc.WithComponentName("DbSqlRepo"))
|
ioc.Register(newDbSqlRepo(), ioc.WithComponentName("DbSqlRepo"))
|
||||||
ioc.Register(newDbSqlExecRepo(), ioc.WithComponentName("DbSqlExecRepo"))
|
ioc.Register(newDbSqlExecRepo(), ioc.WithComponentName("DbSqlExecRepo"))
|
||||||
|
|||||||
@@ -104,8 +104,16 @@ func (c *Cli) Close() {
|
|||||||
c.sftpClient.Close()
|
c.sftpClient.Close()
|
||||||
c.sftpClient = nil
|
c.sftpClient = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sshTunnelMachineId uint64
|
||||||
if c.Info.SshTunnelMachine != nil {
|
if c.Info.SshTunnelMachine != nil {
|
||||||
logx.Infof("关闭机器的隧道信息: machineId=%d, sshTunnelMachineId=%d", c.Info.Id, c.Info.SshTunnelMachine.Id)
|
sshTunnelMachineId = c.Info.SshTunnelMachine.Id
|
||||||
|
}
|
||||||
|
if c.Info.TempSshMachineId != 0 {
|
||||||
|
sshTunnelMachineId = c.Info.TempSshMachineId
|
||||||
|
}
|
||||||
|
if sshTunnelMachineId != 0 {
|
||||||
|
logx.Infof("关闭机器的隧道信息: machineId=%d, sshTunnelMachineId=%d", c.Info.Id, sshTunnelMachineId)
|
||||||
CloseSshTunnelMachine(int(c.Info.SshTunnelMachine.Id), c.Info.GetTunnelId())
|
CloseSshTunnelMachine(int(c.Info.SshTunnelMachine.Id), c.Info.GetTunnelId())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ func ErrIsNil(err error, msgAndParams ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ErrNotNil(err error, msg string, params ...any) {
|
||||||
|
if err == nil {
|
||||||
|
panic(errorx.NewBiz(fmt.Sprintf(msg, params...)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ErrIsNilAppendErr(err error, msg string) {
|
func ErrIsNilAppendErr(err error, msg string) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errorx.NewBiz(fmt.Sprintf(msg, err.Error())))
|
panic(errorx.NewBiz(fmt.Sprintf(msg, err.Error())))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import "fmt"
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
AppName = "mayfly-go"
|
AppName = "mayfly-go"
|
||||||
Version = "v1.7.2"
|
Version = "v1.7.3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAppInfo() string {
|
func GetAppInfo() string {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ var (
|
|||||||
type JobKey = string
|
type JobKey = string
|
||||||
type RunJobFunc[T Job] func(ctx context.Context, job T) error
|
type RunJobFunc[T Job] func(ctx context.Context, job T) error
|
||||||
type NextJobFunc[T Job] func() (T, bool)
|
type NextJobFunc[T Job] func() (T, bool)
|
||||||
type RunnableJobFunc[T Job] func(job T, next NextJobFunc[T]) (bool, error)
|
type RunnableJobFunc[T Job] func(job T, nextRunning NextJobFunc[T]) (bool, error)
|
||||||
type ScheduleJobFunc[T Job] func(job T) (deadline time.Time, err error)
|
type ScheduleJobFunc[T Job] func(job T) (deadline time.Time, err error)
|
||||||
type UpdateJobFunc[T Job] func(ctx context.Context, job T) error
|
type UpdateJobFunc[T Job] func(ctx context.Context, job T) error
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -50,6 +50,7 @@ CREATE TABLE IF NOT EXISTS "t_db_backup" (
|
|||||||
"db_name" text(64) NOT NULL,
|
"db_name" text(64) NOT NULL,
|
||||||
"repeated" integer(1),
|
"repeated" integer(1),
|
||||||
"interval" integer(20),
|
"interval" integer(20),
|
||||||
|
"max_save_days" integer(8) NOT NULL DEFAULT '0',
|
||||||
"start_time" datetime,
|
"start_time" datetime,
|
||||||
"enabled" integer(1),
|
"enabled" integer(1),
|
||||||
"enabled_desc" text(64),
|
"enabled_desc" text(64),
|
||||||
@@ -81,8 +82,8 @@ CREATE TABLE IF NOT EXISTS "t_db_backup_history" (
|
|||||||
"create_time" datetime,
|
"create_time" datetime,
|
||||||
"is_deleted" integer(1) NOT NULL,
|
"is_deleted" integer(1) NOT NULL,
|
||||||
"delete_time" datetime,
|
"delete_time" datetime,
|
||||||
"restoring" integer(1),
|
"restoring" integer(1) NOT NULL DEFAULT '0',
|
||||||
"deleting" integer(1),
|
"deleting" integer(1) NOT NULL DEFAULT '0',
|
||||||
PRIMARY KEY ("id")
|
PRIMARY KEY ("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -112,6 +113,7 @@ CREATE TABLE IF NOT EXISTS "t_db_binlog_history" (
|
|||||||
"file_size" integer(20),
|
"file_size" integer(20),
|
||||||
"sequence" integer(20),
|
"sequence" integer(20),
|
||||||
"first_event_time" datetime,
|
"first_event_time" datetime,
|
||||||
|
"last_event_time" datetime,
|
||||||
"create_time" datetime,
|
"create_time" datetime,
|
||||||
"is_deleted" integer(4) NOT NULL,
|
"is_deleted" integer(4) NOT NULL,
|
||||||
"delete_time" datetime,
|
"delete_time" datetime,
|
||||||
@@ -738,6 +740,8 @@ CREATE TABLE IF NOT EXISTS "t_sys_resource" (
|
|||||||
);
|
);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (1, 0, 'Aexqq77l/', 1, 1, '首页', '/home', 10000000, '{"component":"home/Home","icon":"HomeFilled","isAffix":true,"isKeepAlive":true,"routeName":"Home"}', 1, 'admin', 1, 'admin', '2021-05-25 16:44:41', '2023-03-14 14:27:07', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (1, 0, 'Aexqq77l/', 1, 1, '首页', '/home', 10000000, '{"component":"home/Home","icon":"HomeFilled","isAffix":true,"isKeepAlive":true,"routeName":"Home"}', 1, 'admin', 1, 'admin', '2021-05-25 16:44:41', '2023-03-14 14:27:07', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (2, 0, '12sSjal1/', 1, 1, '机器管理', '/machine', 49999998, '{"icon":"Monitor","isKeepAlive":true,"redirect":"machine/list","routeName":"Machine"}', 1, 'admin', 1, 'admin', '2021-05-25 16:48:16', '2022-10-06 14:58:49', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (2, 0, '12sSjal1/', 1, 1, '机器管理', '/machine', 49999998, '{"icon":"Monitor","isKeepAlive":true,"redirect":"machine/list","routeName":"Machine"}', 1, 'admin', 1, 'admin', '2021-05-25 16:48:16', '2022-10-06 14:58:49', 0, NULL);
|
||||||
|
INSERT INTO t_sys_resource (id, pid, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, ui_path, is_deleted, delete_time) VALUES(1707206386, 2, 1, 1, '机器操作', 'machines-op', 1, '{"component":"ops/machine/MachineOp","icon":"Monitor","isKeepAlive":true,"routeName":"MachineOp"}', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-06 15:59:46', '2024-02-06 16:24:21', 'PDPt6217/', 0, NULL);
|
||||||
|
INSERT INTO t_sys_resource (id, pid, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, ui_path, is_deleted, delete_time) VALUES(1707206421, 1707206386, 2, 1, '基本权限', 'machine-op', 1707206421, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-06 16:00:22', '2024-02-06 16:00:22', 'PDPt6217/kQXTYvuM/', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (3, 2, '12sSjal1/lskeiql1/', 1, 1, '机器列表', 'machines', 20000000, '{"component":"ops/machine/MachineList","icon":"Monitor","isKeepAlive":true,"routeName":"MachineList"}', 2, 'admin', 1, 'admin', '2021-05-25 16:50:04', '2023-03-15 17:14:44', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (3, 2, '12sSjal1/lskeiql1/', 1, 1, '机器列表', 'machines', 20000000, '{"component":"ops/machine/MachineList","icon":"Monitor","isKeepAlive":true,"routeName":"MachineList"}', 2, 'admin', 1, 'admin', '2021-05-25 16:50:04', '2023-03-15 17:14:44', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (4, 0, 'Xlqig32x/', 1, 1, '系统管理', '/sys', 60000001, '{"icon":"Setting","isKeepAlive":true,"redirect":"/sys/resources","routeName":"sys"}', 1, 'admin', 1, 'admin', '2021-05-26 15:20:20', '2022-10-06 14:59:53', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (4, 0, 'Xlqig32x/', 1, 1, '系统管理', '/sys', 60000001, '{"icon":"Setting","isKeepAlive":true,"redirect":"/sys/resources","routeName":"sys"}', 1, 'admin', 1, 'admin', '2021-05-26 15:20:20', '2022-10-06 14:59:53', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (5, 4, 'Xlqig32x/UGxla231/', 1, 1, '资源管理', 'resources', 9999999, '{"component":"system/resource/ResourceList","icon":"Menu","isKeepAlive":true,"routeName":"ResourceList"}', 1, 'admin', 1, 'admin', '2021-05-26 15:23:07', '2023-03-14 15:44:34', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (5, 4, 'Xlqig32x/UGxla231/', 1, 1, '资源管理', 'resources', 9999999, '{"component":"system/resource/ResourceList","icon":"Menu","isKeepAlive":true,"routeName":"ResourceList"}', 1, 'admin', 1, 'admin', '2021-05-26 15:23:07', '2023-03-14 15:44:34', 0, NULL);
|
||||||
@@ -830,8 +834,8 @@ INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight,
|
|||||||
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (153, 150, 'Jra0n7De/pLOA2UYz/', 2, 1, '删除', 'db:sync:del', 1703641342, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:42:22', '2023-12-27 09:42:22', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (153, 150, 'Jra0n7De/pLOA2UYz/', 2, 1, '删除', 'db:sync:del', 1703641342, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:42:22', '2023-12-27 09:42:22', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (154, 150, 'Jra0n7De/VBt68CDx/', 2, 1, '启停', 'db:sync:status', 1703641364, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:42:45', '2023-12-27 09:42:45', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (154, 150, 'Jra0n7De/VBt68CDx/', 2, 1, '启停', 'db:sync:status', 1703641364, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:42:45', '2023-12-27 09:42:45', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (155, 150, 'Jra0n7De/PigmSGVg/', 2, 1, '日志', 'db:sync:log', 1704266866, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-01-03 15:27:47', '2024-01-03 15:27:47', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (155, 150, 'Jra0n7De/PigmSGVg/', 2, 1, '日志', 'db:sync:log', 1704266866, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-01-03 15:27:47', '2024-01-03 15:27:47', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (161, 49, 'dbms23ax/xleaiec2/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (160, 49, 'dbms23ax/xleaiec2/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (160, 49, 'dbms23ax/xleaiec2/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, type, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES (161, 49, 'dbms23ax/xleaiec2/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 0, NULL);
|
||||||
|
|
||||||
-- Table: t_sys_role
|
-- Table: t_sys_role
|
||||||
CREATE TABLE IF NOT EXISTS "t_sys_role" (
|
CREATE TABLE IF NOT EXISTS "t_sys_role" (
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ CREATE TABLE `t_db_backup` (
|
|||||||
`db_name` varchar(64) NOT NULL COMMENT '数据库名称',
|
`db_name` varchar(64) NOT NULL COMMENT '数据库名称',
|
||||||
`repeated` tinyint(1) DEFAULT NULL COMMENT '是否重复执行',
|
`repeated` tinyint(1) DEFAULT NULL COMMENT '是否重复执行',
|
||||||
`interval` bigint(20) DEFAULT NULL COMMENT '备份周期',
|
`interval` bigint(20) DEFAULT NULL COMMENT '备份周期',
|
||||||
|
`max_save_days` int(8) NOT NULL DEFAULT '0' COMMENT '最大保留天数',
|
||||||
`start_time` datetime DEFAULT NULL COMMENT '首次备份时间',
|
`start_time` datetime DEFAULT NULL COMMENT '首次备份时间',
|
||||||
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
|
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
|
||||||
`enabled_desc` varchar(64) NULL COMMENT '任务启用描述',
|
`enabled_desc` varchar(64) NULL COMMENT '任务启用描述',
|
||||||
@@ -144,8 +145,8 @@ CREATE TABLE `t_db_backup_history` (
|
|||||||
`create_time` datetime DEFAULT NULL COMMENT '历史备份创建时间',
|
`create_time` datetime DEFAULT NULL COMMENT '历史备份创建时间',
|
||||||
`is_deleted` tinyint(1) NOT NULL DEFAULT 0,
|
`is_deleted` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
`delete_time` datetime DEFAULT NULL,
|
`delete_time` datetime DEFAULT NULL,
|
||||||
`restoring` int(1) NOT NULL DEFAULT '0' COMMENT '备份历史恢复标识',
|
`restoring` tinyint(1) NOT NULL DEFAULT '0' COMMENT '备份历史恢复标识',
|
||||||
`deleting` int(1) NOT NULL DEFAULT '0' COMMENT '备份历史删除标识',
|
`deleting` tinyint(1) NOT NULL DEFAULT '0' COMMENT '备份历史删除标识',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_db_backup_id` (`db_backup_id`) USING BTREE,
|
KEY `idx_db_backup_id` (`db_backup_id`) USING BTREE,
|
||||||
KEY `idx_db_instance_id` (`db_instance_id`) USING BTREE,
|
KEY `idx_db_instance_id` (`db_instance_id`) USING BTREE,
|
||||||
@@ -232,6 +233,7 @@ CREATE TABLE `t_db_binlog_history` (
|
|||||||
`file_size` bigint(20) DEFAULT NULL COMMENT 'BINLOG文件大小',
|
`file_size` bigint(20) DEFAULT NULL COMMENT 'BINLOG文件大小',
|
||||||
`sequence` bigint(20) DEFAULT NULL COMMENT 'BINLOG序列号',
|
`sequence` bigint(20) DEFAULT NULL COMMENT 'BINLOG序列号',
|
||||||
`first_event_time` datetime DEFAULT NULL COMMENT '首次事件时间',
|
`first_event_time` datetime DEFAULT NULL COMMENT '首次事件时间',
|
||||||
|
`last_event_time` datetime DEFAULT NULL COMMENT '最新事件时间',
|
||||||
`create_time` datetime DEFAULT NULL,
|
`create_time` datetime DEFAULT NULL,
|
||||||
`is_deleted` tinyint(4) NOT NULL DEFAULT 0,
|
`is_deleted` tinyint(4) NOT NULL DEFAULT 0,
|
||||||
`delete_time` datetime DEFAULT NULL,
|
`delete_time` datetime DEFAULT NULL,
|
||||||
@@ -701,6 +703,8 @@ BEGIN;
|
|||||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1, 0, 'Aexqq77l/', 1, 1, '首页', '/home', 10000000, '{"component":"home/Home","icon":"HomeFilled","isAffix":true,"isKeepAlive":true,"routeName":"Home"}', 1, 'admin', 1, 'admin', '2021-05-25 16:44:41', '2023-03-14 14:27:07', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1, 0, 'Aexqq77l/', 1, 1, '首页', '/home', 10000000, '{"component":"home/Home","icon":"HomeFilled","isAffix":true,"isKeepAlive":true,"routeName":"Home"}', 1, 'admin', 1, 'admin', '2021-05-25 16:44:41', '2023-03-14 14:27:07', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(2, 0, '12sSjal1/', 1, 1, '机器管理', '/machine', 49999998, '{"icon":"Monitor","isKeepAlive":true,"redirect":"machine/list","routeName":"Machine"}', 1, 'admin', 1, 'admin', '2021-05-25 16:48:16', '2022-10-06 14:58:49', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(2, 0, '12sSjal1/', 1, 1, '机器管理', '/machine', 49999998, '{"icon":"Monitor","isKeepAlive":true,"redirect":"machine/list","routeName":"Machine"}', 1, 'admin', 1, 'admin', '2021-05-25 16:48:16', '2022-10-06 14:58:49', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(3, 2, '12sSjal1/lskeiql1/', 1, 1, '机器列表', 'machines', 20000000, '{"component":"ops/machine/MachineList","icon":"Monitor","isKeepAlive":true,"routeName":"MachineList"}', 2, 'admin', 1, 'admin', '2021-05-25 16:50:04', '2023-03-15 17:14:44', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(3, 2, '12sSjal1/lskeiql1/', 1, 1, '机器列表', 'machines', 20000000, '{"component":"ops/machine/MachineList","icon":"Monitor","isKeepAlive":true,"routeName":"MachineList"}', 2, 'admin', 1, 'admin', '2021-05-25 16:50:04', '2023-03-15 17:14:44', 0, NULL);
|
||||||
|
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1707206386, 2, 1, 1, '机器操作', 'machines-op', 1, '{"component":"ops/machine/MachineOp","icon":"Monitor","isKeepAlive":true,"routeName":"MachineOp"}', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-06 15:59:46', '2024-02-06 16:24:21', 'PDPt6217/', 0, NULL);
|
||||||
|
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1707206421, 1707206386, 2, 1, '基本权限', 'machine-op', 1707206421, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-06 16:00:22', '2024-02-06 16:00:22', 'PDPt6217/kQXTYvuM/', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(4, 0, 'Xlqig32x/', 1, 1, '系统管理', '/sys', 60000001, '{"icon":"Setting","isKeepAlive":true,"redirect":"/sys/resources","routeName":"sys"}', 1, 'admin', 1, 'admin', '2021-05-26 15:20:20', '2022-10-06 14:59:53', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(4, 0, 'Xlqig32x/', 1, 1, '系统管理', '/sys', 60000001, '{"icon":"Setting","isKeepAlive":true,"redirect":"/sys/resources","routeName":"sys"}', 1, 'admin', 1, 'admin', '2021-05-26 15:20:20', '2022-10-06 14:59:53', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(5, 4, 'Xlqig32x/UGxla231/', 1, 1, '资源管理', 'resources', 9999999, '{"component":"system/resource/ResourceList","icon":"Menu","isKeepAlive":true,"routeName":"ResourceList"}', 1, 'admin', 1, 'admin', '2021-05-26 15:23:07', '2023-03-14 15:44:34', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(5, 4, 'Xlqig32x/UGxla231/', 1, 1, '资源管理', 'resources', 9999999, '{"component":"system/resource/ResourceList","icon":"Menu","isKeepAlive":true,"routeName":"ResourceList"}', 1, 'admin', 1, 'admin', '2021-05-26 15:23:07', '2023-03-14 15:44:34', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(11, 4, 'Xlqig32x/lxqSiae1/', 1, 1, '角色管理', 'roles', 10000001, '{"component":"system/role/RoleList","icon":"Menu","isKeepAlive":true,"routeName":"RoleList"}', 1, 'admin', 1, 'admin', '2021-05-27 11:15:35', '2023-03-14 15:44:22', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(11, 4, 'Xlqig32x/lxqSiae1/', 1, 1, '角色管理', 'roles', 10000001, '{"component":"system/role/RoleList","icon":"Menu","isKeepAlive":true,"routeName":"RoleList"}', 1, 'admin', 1, 'admin', '2021-05-27 11:15:35', '2023-03-14 15:44:22', 0, NULL);
|
||||||
@@ -792,8 +796,8 @@ INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight
|
|||||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(152, 150, 'Jra0n7De/zvAMo2vk/', 2, 1, '编辑', 'db:sync:save', 1703641320, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:42:00', '2023-12-27 09:42:12', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(152, 150, 'Jra0n7De/zvAMo2vk/', 2, 1, '编辑', 'db:sync:save', 1703641320, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:42:00', '2023-12-27 09:42:12', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(151, 150, 'Jra0n7De/uAnHZxEV/', 2, 1, '基本权限', 'db:sync', 1703641202, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:40:02', '2023-12-27 09:40:02', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(151, 150, 'Jra0n7De/uAnHZxEV/', 2, 1, '基本权限', 'db:sync', 1703641202, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-27 09:40:02', '2023-12-27 09:40:02', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(150, 36, 'Jra0n7De/', 1, 1, '数据同步', 'sync', 1693040707, '{"component":"ops/db/SyncTaskList","icon":"Coin","isKeepAlive":true,"routeName":"SyncTaskList"}', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-22 09:51:34', '2023-12-27 10:16:57', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(150, 36, 'Jra0n7De/', 1, 1, '数据同步', 'sync', 1693040707, '{"component":"ops/db/SyncTaskList","icon":"Coin","isKeepAlive":true,"routeName":"SyncTaskList"}', 12, 'liuzongyang', 12, 'liuzongyang', '2023-12-22 09:51:34', '2023-12-27 10:16:57', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(161, 49, 'dbms23ax/xleaiec2/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(160, 49, 'dbms23ax/xleaiec2/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 0, NULL);
|
||||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(160, 49, 'dbms23ax/xleaiec2/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 0, NULL);
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(161, 49, 'dbms23ax/xleaiec2/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 0, NULL);
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `ui_path`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `is_deleted`, `delete_time`)
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(160, 49, 'dbms23ax/xleaiec2/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 0, NULL);
|
||||||
VALUES (161, 49, 'dbms23ax/xleaiec2/3NUXQFIO/', 2, 1, '数据库备份', 'db:backup', 1705973876, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:37:56', '2024-01-23 09:37:56', 0, NULL),
|
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(161, 49, 'dbms23ax/xleaiec2/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 0, NULL);
|
||||||
(160, 49, 'dbms23ax/xleaiec2/ghErkTdb/', 2, 1, '数据库恢复', 'db:restore', 1705973909, 'null', 1, 'admin', 1, 'admin', '2024-01-23 09:38:29', '2024-01-23 09:38:29', 0, NULL);
|
|
||||||
|
|
||||||
ALTER TABLE `t_db_backup`
|
ALTER TABLE `t_db_backup`
|
||||||
ADD COLUMN `enabled_desc` varchar(64) NULL COMMENT '任务启用描述' AFTER `enabled`;
|
ADD COLUMN `enabled_desc` varchar(64) NULL COMMENT '任务启用描述' AFTER `enabled`;
|
||||||
@@ -9,5 +8,5 @@ ALTER TABLE `t_db_restore`
|
|||||||
ADD COLUMN `enabled_desc` varchar(64) NULL COMMENT '任务启用描述' AFTER `enabled`;
|
ADD COLUMN `enabled_desc` varchar(64) NULL COMMENT '任务启用描述' AFTER `enabled`;
|
||||||
|
|
||||||
ALTER TABLE `t_db_backup_history`
|
ALTER TABLE `t_db_backup_history`
|
||||||
ADD COLUMN `restoring` int(1) NOT NULL DEFAULT '0' COMMENT '备份历史恢复标识',
|
ADD COLUMN `restoring` tinyint(1) NOT NULL DEFAULT '0' COMMENT '备份历史恢复标识',
|
||||||
ADD COLUMN `deleting` int(1) NOT NULL DEFAULT '0' COMMENT '备份历史删除标识';
|
ADD COLUMN `deleting` tinyint(1) NOT NULL DEFAULT '0' COMMENT '备份历史删除标识';
|
||||||
|
|||||||
8
server/resources/script/sql/v1.7/v1.7.3.sql
Normal file
8
server/resources/script/sql/v1.7/v1.7.3.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE `t_db_backup`
|
||||||
|
ADD COLUMN `max_save_days` int(8) NOT NULL DEFAULT '0' COMMENT '最大保存天数' AFTER `interval`;
|
||||||
|
|
||||||
|
ALTER TABLE `t_db_binlog_history`
|
||||||
|
ADD COLUMN `last_event_time` datetime NULL DEFAULT NULL COMMENT '最新事件时间' AFTER `first_event_time`;
|
||||||
|
|
||||||
|
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1707206386, 2, 1, 1, '机器操作', 'machines-op', 1, '{"component":"ops/machine/MachineOp","icon":"Monitor","isKeepAlive":true,"routeName":"MachineOp"}', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-06 15:59:46', '2024-02-06 16:24:21', 'PDPt6217/', 0, NULL);
|
||||||
|
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1707206421, 1707206386, 2, 1, '基本权限', 'machine-op', 1707206421, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-06 16:00:22', '2024-02-06 16:00:22', 'PDPt6217/kQXTYvuM/', 0, NULL);
|
||||||
Reference in New Issue
Block a user