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