9 Commits

Author SHA1 Message Date
meilin.huang
d711a36749 feat: v1.7.3 2024-02-08 09:53:48 +08:00
meilin.huang
9dbf104ef1 refactor: 机器操作界面调整 2024-02-07 21:14:29 +08:00
zongyangleo
20eb06fb28 !101 feat: 新增机器操作菜单
* feat: 新增机器操作菜单
2024-02-07 06:37:59 +00:00
meilin.huang
9c20bdef39 Merge branch 'dev' of https://gitee.com/objs/mayfly-go into dev 2024-02-06 15:33:31 +08:00
zongyangleo
3fdd98a390 !99 feat: DBMS新增kingbaseES、vastbase,还有一些优化
* refactor: 重构机器列表展示
* fix:修复编辑表问题
* refactor: 优化下拉实例显示
* feat: DBMS新增kingbaseES(已测试postgres、oracle兼容模式) 、vastbase
2024-02-06 07:32:03 +00:00
meilin.huang
d4f456c0cf Merge branch 'dev' of https://gitee.com/objs/mayfly-go into dev 2024-02-06 15:17:39 +08:00
kanzihuang
f2b6e15cf4 !100 定时清理数据库备份数据
* feat: 优化数据库 BINLOG 同步机制
* feat: 删除数据库实例前需删除关联的数据库备份与恢复任务
* refactor: 重构数据库备份与恢复模块
* feat: 定时清理数据库备份历史和本地 Binlog 文件
* feat: 压缩数据库备份文件
2024-02-06 07:16:56 +00:00
meilin.huang
6be0ea6aed fix: dbms数据行编辑 2024-02-01 12:05:41 +08:00
meilin.huang
eee08be2cc feat: 数据库支持编辑行数据 2024-01-31 20:41:41 +08:00
87 changed files with 1497 additions and 527 deletions

View File

@@ -17,7 +17,7 @@
"countup.js": "^2.7.0", "countup.js": "^2.7.0",
"cropperjs": "^1.5.11", "cropperjs": "^1.5.11",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"element-plus": "^2.5.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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -71,7 +71,7 @@
<el-descriptions-item label-align="right"> <el-descriptions-item label-align="right">
<template #label> <template #label>
<div> <div>
<SvgIcon :name="getDbDialect(nowDbInst.type).getInfo().icon" :size="18" /> <SvgIcon :name="nowDbInst.getDialect().getInfo().icon" :size="18" />
实例 实例
</div> </div>
</template> </template>

View File

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

View File

@@ -6,6 +6,9 @@
:resource-type="TagResourceTypeEnum.Db.value" :resource-type="TagResourceTypeEnum.Db.value"
:tag-path-node-type="NodeTypeTagPath" :tag-path-node-type="NodeTypeTagPath"
> >
<template #iconPrefix>
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
</template>
<template #prefix="{ data }"> <template #prefix="{ data }">
<SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" /> <SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" /> <SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
@@ -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);

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,121 @@
<template>
<el-dialog v-model="visible" :title="title" :destroy-on-close="true" width="600px">
<el-form ref="dataForm" :model="modelValue" :show-message="false" label-width="auto" size="small">
<el-form-item
v-for="column in columns"
:key="column.columnName"
class="w100 mb5"
:prop="column.columnName"
:required="column.nullable != 'YES' && !column.isPrimaryKey && !column.isIdentity"
>
<template #label>
<span class="pointer" :title="`${column.columnType} | ${column.columnComment}`">
{{ column.columnName }}
</span>
</template>
<ColumnFormItem
v-model="modelValue[`${column.columnName}`]"
:data-type="dbInst.getDialect().getDataType(column.columnType)"
:placeholder="`${column.columnType} ${column.columnComment}`"
:column-name="column.columnName"
:disabled="column.isIdentity"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="confirm">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from 'vue';
import ColumnFormItem from './ColumnFormItem.vue';
import { DbInst } from '../../db';
import { ElMessage } from 'element-plus';
export interface ColumnFormItemProps {
dbInst: DbInst;
dbName: string;
tableName: string;
columns: any[];
title?: string; // dialog title
}
const props = withDefaults(defineProps<ColumnFormItemProps>(), {
title: '',
});
const modelValue = defineModel<any>('modelValue');
const visible = defineModel<boolean>('visible', {
default: false,
});
const emit = defineEmits(['submitSuccess']);
const dataForm: any = ref(null);
let oldValue = null as any;
onMounted(() => {
setOldValue();
});
watch(visible, (newValue) => {
if (newValue) {
setOldValue();
}
});
const setOldValue = () => {
// 空对象则为insert操作否则为update
if (Object.keys(modelValue.value).length > 0) {
oldValue = Object.assign({}, modelValue.value);
}
};
const closeDialog = () => {
visible.value = false;
modelValue.value = {};
};
const confirm = async () => {
dataForm.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('请正确填写数据信息');
return false;
}
const dbInst = props.dbInst;
const data = modelValue.value;
const db = props.dbName;
const tableName = props.tableName;
let sql = '';
if (oldValue) {
const updateColumnValue = {};
Object.keys(oldValue).forEach((key) => {
// 如果新旧值不相等,则为需要更新的字段
if (oldValue[key] !== modelValue.value[key]) {
updateColumnValue[key] = modelValue.value[key];
}
});
sql = await dbInst.genUpdateSql(db, tableName, updateColumnValue, oldValue);
} else {
sql = await dbInst.genInsertSql(db, tableName, [data], true);
}
dbInst.promptExeSql(db, sql, null, () => {
closeDialog();
emit('submitSuccess');
});
});
};
</script>
<style lang="scss"></style>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
import { DialectInfo } from './index';
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
let kbpgDialectInfo: DialectInfo;
export class KingbaseEsDialect extends PostgresqlDialect {
getInfo(): DialectInfo {
if (kbpgDialectInfo) {
return kbpgDialectInfo;
}
kbpgDialectInfo = {} as DialectInfo;
Object.assign(kbpgDialectInfo, super.getInfo());
kbpgDialectInfo.name = 'KingbaseES';
kbpgDialectInfo.icon = 'iconfont icon-kingbase';
return kbpgDialectInfo;
}
}

View File

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

View File

@@ -0,0 +1,18 @@
import { DialectInfo } from './index';
import { PostgresqlDialect } from '@/views/ops/db/dialect/postgres_dialect';
let vastDialectInfo: DialectInfo;
export class VastbaseDialect extends PostgresqlDialect {
getInfo(): DialectInfo {
if (vastDialectInfo) {
return vastDialectInfo;
}
vastDialectInfo = {} as DialectInfo;
Object.assign(vastDialectInfo, super.getInfo());
vastDialectInfo.name = 'VastbaseG100';
vastDialectInfo.icon = 'iconfont icon-vastbase';
return vastDialectInfo;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,402 @@
<template>
<div class="flex-all-center">
<!-- 文档 https://antoniandre.github.io/splitpanes/ -->
<Splitpanes class="default-theme" @resized="onResizeTagTree">
<Pane size="20" max-size="30">
<tag-tree
class="machine-terminal-tree"
ref="tagTreeRef"
:resource-type="TagResourceTypeEnum.Machine.value"
:tag-path-node-type="NodeTypeTagPath"
>
<template #prefix="{ data }">
<SvgIcon v-if="data.icon && data.params.status == 1" :name="data.icon.name" :color="data.icon.color" />
<SvgIcon v-if="data.icon && data.params.status == -1" :name="data.icon.name" color="var(--el-color-danger)" />
</template>
<template #suffix="{ data }">
<span style="color: #c4c9c4; font-size: 9px" v-if="data.type.value == MachineNodeType.Machine">{{
` ${data.params.username}@${data.params.ip}:${data.params.port}`
}}</span>
</template>
</tag-tree>
</Pane>
<Pane>
<div class="machine-terminal-tabs card pd5">
<el-tabs
v-if="state.tabs.size > 0"
type="card"
@tab-remove="onRemoveTab"
@tab-change="onTabChange"
style="width: 100%"
v-model="state.activeTermName"
class="h100"
>
<el-tab-pane class="h100" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
<template #label>
<el-popconfirm @confirm="handleReconnect(dt.key)" title="确认重新连接?">
<template #reference>
<el-icon class="mr5" :color="dt.status == 1 ? '#67c23a' : '#f56c6c'" :title="dt.status == 1 ? '' : '点击重连'"
><Connection />
</el-icon>
</template>
</el-popconfirm>
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
<template #reference>
<div>
<span class="machine-terminal-tab-label">{{ dt.label }}</span>
</div>
</template>
<template #default>
<el-descriptions :column="1" size="small">
<el-descriptions-item label="机器名"> {{ dt.params?.name }} </el-descriptions-item>
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
<el-descriptions-item label="username"> {{ dt.params?.username }} </el-descriptions-item>
<el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
</el-descriptions>
</template>
</el-popover>
</template>
<div class="terminal-wrapper" :style="{ height: `calc(100vh - 155px)` }">
<TerminalBody
@status-change="terminalStatusChange(dt.key, $event)"
:ref="(el) => setTerminalRef(el, dt.key)"
:socket-url="dt.socketUrl"
/>
</div>
</el-tab-pane>
</el-tabs>
<el-dialog v-model="infoDialog.visible">
<el-descriptions title="详情" :column="3" border>
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
<el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.data.username }}</el-descriptions-item>
<el-descriptions-item :span="1" label="认证方式">
{{ infoDialog.data.authCertId > 1 ? '授权凭证' : '密码' }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data.remark }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="SSH隧道">{{ infoDialog.data.sshTunnelMachineId > 0 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="1.5" label="终端回放">{{ infoDialog.data.enableRecorder == 1 ? '是' : '否' }} </el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data.modifier }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
<script-manage :title="serviceDialog.title" v-model:visible="serviceDialog.visible" v-model:machineId="serviceDialog.machineId" />
<file-conf-list :title="fileDialog.title" v-model:visible="fileDialog.visible" v-model:machineId="fileDialog.machineId" />
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title" />
</div>
</Pane>
</Splitpanes>
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router';
import { machineApi, getMachineTerminalSocketUrl } from './api';
import { dateFormat } from '@/common/utils/date';
import { hasPerms } from '@/components/auth/auth';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { NodeType, TagTreeNode } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { Splitpanes, Pane } from 'splitpanes';
import { ContextmenuItem } from '@/components/contextmenu/index';
// 组件
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
const FileConfList = defineAsyncComponent(() => import('./file/FileConfList.vue'));
const MachineStats = defineAsyncComponent(() => import('./MachineStats.vue'));
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
import TerminalBody from '@/components/terminal/TerminalBody.vue';
import { TerminalStatus } from '@/components/terminal/common';
const router = useRouter();
const perms = {
addMachine: 'machine:add',
updateMachine: 'machine:update',
delMachine: 'machine:del',
terminal: 'machine:terminal',
closeCli: 'machine:close-cli',
};
// 该用户拥有的的操作列按钮权限使用v-if进行判断v-auth对el-dropdown-item无效
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
class MachineNodeType {
static Machine = 1;
}
const state = reactive({
params: {
pageNum: 1,
pageSize: 0,
ip: null,
name: null,
tagPath: '',
},
infoDialog: {
visible: false,
data: null as any,
},
serviceDialog: {
visible: false,
machineId: 0,
title: '',
},
processDialog: {
visible: false,
machineId: 0,
},
fileDialog: {
visible: false,
machineId: 0,
title: '',
},
machineStatsDialog: {
visible: false,
stats: null,
title: '',
machineId: 0,
},
machineRecDialog: {
visible: false,
machineId: 0,
title: '',
},
activeTermName: '',
tabs: new Map<string, any>(),
});
const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineRecDialog } = toRefs(state);
const tagTreeRef: any = ref(null);
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: any) => {
// 加载标签树下的机器列表
state.params.tagPath = node.key;
state.params.pageNum = 1;
state.params.pageSize = 1000;
const res = await search();
// 把list 根据name字段排序
res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
return res.list.map((x: any) =>
new TagTreeNode(x.id, x.name, NodeTypeMachine(x))
.withParams(x)
.withDisabled(x.status == -1)
.withIcon({
name: 'Monitor',
color: '#409eff',
})
.withIsLeaf(true)
);
});
let openIds = {};
const NodeTypeMachine = (machine: any) => {
let contextMenuItems = [];
contextMenuItems.push(new ContextmenuItem('term', '打开终端').withIcon('Monitor').withOnClick(() => openTerminal(machine)));
contextMenuItems.push(new ContextmenuItem('term-ex', '打开终端(新窗口)').withIcon('Monitor').withOnClick(() => openTerminal(machine, true)));
contextMenuItems.push(new ContextmenuItem('detail', '详情').withIcon('More').withOnClick(() => showInfo(machine)));
contextMenuItems.push(new ContextmenuItem('status', '状态').withIcon('Compass').withOnClick(() => showMachineStats(machine)));
contextMenuItems.push(new ContextmenuItem('process', '进程').withIcon('DataLine').withOnClick(() => showProcess(machine)));
if (actionBtns[perms.updateMachine] && machine.enableRecorder == 1) {
contextMenuItems.push(new ContextmenuItem('edit', '终端回放').withIcon('Compass').withOnClick(() => showRec(machine)));
}
contextMenuItems.push(new ContextmenuItem('files', '文件管理').withIcon('FolderOpened').withOnClick(() => showFileManage(machine)));
contextMenuItems.push(new ContextmenuItem('scripts', '脚本管理').withIcon('Files').withOnClick(() => serviceManager(machine)));
return new NodeType(MachineNodeType.Machine).withContextMenuItems(contextMenuItems).withNodeDblclickFunc(() => {
// for (let k of state.tabs.keys()) {
// // 存在该机器相关的终端tab则直接激活该tab
// if (k.startsWith(`${machine.id}_${machine.username}_`)) {
// state.activeTermName = k;
// onTabChange();
// return;
// }
// }
openTerminal(machine);
});
};
const openTerminal = (machine: any, ex?: boolean) => {
// 新窗口打开
if (ex) {
const { href } = router.resolve({
path: `/machine/terminal`,
query: {
id: machine.id,
name: machine.name,
},
});
window.open(href, '_blank');
return;
}
let { name, id, username } = machine;
// 同一个机器的终端打开多次key后添加下划线和数字区分
openIds[id] = openIds[id] ? ++openIds[id] : 1;
let sameIndex = openIds[id];
let key = `${id}_${username}_${sameIndex}`;
// 只保留name的10个字超出部分只保留前后4个字符中间用省略号代替
let label = name.length > 10 ? name.slice(0, 4) + '...' + name.slice(-4) : name;
state.tabs.set(key, {
key,
label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
params: machine,
socketUrl: getMachineTerminalSocketUrl(id),
});
state.activeTermName = key;
fitTerminal();
};
const serviceManager = (row: any) => {
state.serviceDialog.machineId = row.id;
state.serviceDialog.visible = true;
state.serviceDialog.title = `${row.name} => ${row.ip}`;
};
/**
* 显示机器状态统计信息
*/
const showMachineStats = async (machine: any) => {
state.machineStatsDialog.machineId = machine.id;
state.machineStatsDialog.title = `机器状态: ${machine.name} => ${machine.ip}`;
state.machineStatsDialog.visible = true;
};
const search = async () => {
const res = await machineApi.list.request(state.params);
return res;
};
const showFileManage = (selectionData: any) => {
state.fileDialog.visible = true;
state.fileDialog.machineId = selectionData.id;
state.fileDialog.title = `${selectionData.name} => ${selectionData.ip}`;
};
const showInfo = (info: any) => {
state.infoDialog.data = info;
state.infoDialog.visible = true;
};
const showProcess = (row: any) => {
state.processDialog.machineId = row.id;
state.processDialog.visible = true;
};
const showRec = (row: any) => {
state.machineRecDialog.title = `${row.name}[${row.ip}]-终端回放记录`;
state.machineRecDialog.machineId = row.id;
state.machineRecDialog.visible = true;
};
const onRemoveTab = (targetName: string) => {
let activeTermName = state.activeTermName;
const tabNames = [...state.tabs.keys()];
for (let i = 0; i < tabNames.length; i++) {
const tabName = tabNames[i];
if (tabName !== targetName) {
continue;
}
const nextTab = tabNames[i + 1] || tabNames[i - 1];
if (nextTab) {
activeTermName = nextTab;
} else {
activeTermName = '';
}
let info = state.tabs.get(targetName);
if (info) {
terminalRefs[info.key]?.close();
}
state.tabs.delete(targetName);
state.activeTermName = activeTermName;
onTabChange();
}
};
const terminalStatusChange = (key: string, status: TerminalStatus) => {
state.tabs.get(key).status = status;
};
const terminalRefs: any = {};
const setTerminalRef = (el: any, key: any) => {
if (key) {
terminalRefs[key] = el;
}
};
const onResizeTagTree = () => {
fitTerminal();
};
const onTabChange = () => {
fitTerminal();
};
const fitTerminal = () => {
setTimeout(() => {
let info = state.tabs.get(state.activeTermName);
if (info) {
terminalRefs[info.key]?.resize();
terminalRefs[info.key]?.focus();
}
}, 100);
};
const handleReconnect = (key: string) => {
terminalRefs[key].init();
};
</script>
<style lang="scss">
.machine-terminal-tabs {
height: calc(100vh - 108px);
--el-tabs-header-height: 30px;
.el-tabs {
--el-tabs-header-height: 30px;
}
.machine-terminal-tab-label {
font-size: 12px;
}
.el-tabs__header {
margin-bottom: 5px;
}
.el-tabs__item {
padding: 0 8px !important;
}
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package application
import ( import (
"context" "context"
"math"
"mayfly-go/internal/db/domain/entity" "mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository" "mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
@@ -11,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
}

View File

@@ -2,11 +2,14 @@ package application
import ( import (
"context" "context"
"errors"
"gorm.io/gorm"
"mayfly-go/internal/db/dbm" "mayfly-go/internal/db/dbm"
"mayfly-go/internal/db/dbm/dbi" "mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity" "mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository" "mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/base" "mayfly-go/pkg/base"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
) )
@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 '备份历史删除标识';

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