Compare commits

3 Commits

Author SHA1 Message Date
meilin.huang
3017460cc7 refactor: 去除无用组件等 2025-11-16 09:11:09 +08:00
Coder慌
4836a770c4 !139 feat(es):增加ES实例中对HTTP/HTTPS协议的支持,默认使用HTTP协议,使用https时默认证书免校验
Merge pull request !139 from davidathena/dev
2025-10-28 11:25:47 +00:00
fudawei
e6c89fad1b feat(es):增加ES实例中对HTTPS协议的支持,默认证书免校验 2025-10-23 15:29:27 +08:00
34 changed files with 293 additions and 1274 deletions

View File

@@ -13,38 +13,38 @@
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@logicflow/core": "^2.1.3", "@logicflow/core": "^2.1.3",
"@logicflow/extension": "^2.1.5", "@logicflow/extension": "^2.1.5",
"@vueuse/core": "^13.9.0", "@vueuse/core": "^14.0.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0", "@xterm/addon-search": "^0.15.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"asciinema-player": "^3.11.1", "asciinema-player": "^3.12.1",
"axios": "^1.6.2", "axios": "^1.6.2",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.18", "dayjs": "^1.11.19",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"element-plus": "^2.11.4", "element-plus": "^2.11.8",
"js-base64": "^3.7.8", "js-base64": "^3.7.8",
"jsencrypt": "^3.5.4", "jsencrypt": "^3.5.4",
"monaco-editor": "^0.54.0", "monaco-editor": "^0.54.0",
"monaco-sql-languages": "^0.15.1", "monaco-sql-languages": "^0.15.1",
"monaco-themes": "^0.4.7", "monaco-themes": "^0.4.7",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^3.0.3", "pinia": "^3.0.4",
"qrcode.vue": "^3.6.0", "qrcode.vue": "^3.6.0",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"sortablejs": "^1.15.6", "sortablejs": "^1.15.6",
"sql-formatter": "^15.6.8", "sql-formatter": "^15.6.10",
"trzsz": "^1.1.5", "trzsz": "^1.1.5",
"uuid": "^13.0.0", "uuid": "^13.0.0",
"vue": "^v3.5.22", "vue": "^v3.6.0-alpha.3",
"vue-i18n": "^11.1.12", "vue-i18n": "^11.1.12",
"vue-router": "^4.6.3", "vue-router": "^4.6.3",
"vuedraggable": "^4.1.0" "vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.17",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/node": "^22.13.14", "@types/node": "^22.13.14",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
@@ -52,16 +52,16 @@
"@typescript-eslint/eslint-plugin": "^8.35.0", "@typescript-eslint/eslint-plugin": "^8.35.0",
"@typescript-eslint/parser": "^8.35.0", "@typescript-eslint/parser": "^8.35.0",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@vue/compiler-sfc": "^3.5.18", "@vue/compiler-sfc": "^3.5.22",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"code-inspector-plugin": "^1.0.4", "code-inspector-plugin": "^1.0.4",
"eslint": "^9.29.0", "eslint": "^9.29.0",
"eslint-plugin-vue": "^10.5.0", "eslint-plugin-vue": "^10.5.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.1", "prettier": "^3.6.1",
"sass": "^1.93.2", "sass": "^1.94.0",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.17",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"vite": "npm:rolldown-vite@latest", "vite": "npm:rolldown-vite@latest",
"vite-plugin-progress": "0.0.7", "vite-plugin-progress": "0.0.7",
"vue-eslint-parser": "^10.2.0" "vue-eslint-parser": "^10.2.0"

View File

@@ -1,6 +1,7 @@
export default { export default {
es: { es: {
keywordPlaceholder: 'host / name / code', keywordPlaceholder: 'host / name / code',
protocol: 'Protocol',
port: 'Port', port: 'Port',
size: 'size', size: 'size',
docs: 'docs', docs: 'docs',

View File

@@ -1,6 +1,7 @@
export default { export default {
es: { es: {
keywordPlaceholder: 'host / 名称 / 编号', keywordPlaceholder: 'host / 名称 / 编号',
protocol: '协议',
port: '端口', port: '端口',
size: '存储大小', size: '存储大小',
docs: '文档数', docs: '文档数',

View File

@@ -1,246 +0,0 @@
<template>
<div>
<el-dialog :title="title" :model-value="visible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="38%">
<el-form :model="state.form" ref="backupForm" label-width="auto" :rules="rules">
<el-form-item prop="dbNames" label="数据库名称">
<el-select
v-model="state.dbNamesSelected"
multiple
clearable
collapse-tags
collapse-tags-tooltip
filterable
:disabled="state.editOrCreate"
:filter-method="filterDbNames"
placeholder="数据库名称"
style="width: 100%"
>
<template #header>
<el-checkbox v-model="checkAllDbNames" :indeterminate="indeterminateDbNames" @change="handleCheckAll"> 全选 </el-checkbox>
</template>
<el-option v-for="db in state.dbNamesFiltered" :key="db" :label="db" :value="db" />
</el-select>
</el-form-item>
<el-form-item prop="name" label="任务名称">
<el-input v-model="state.form.name" type="text" placeholder="任务名称"></el-input>
</el-form-item>
<el-form-item prop="startTime" label="开始时间">
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
</el-form-item>
<el-form-item prop="intervalDay" label="备份周期(天)">
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="单位:天"></el-input>
</el-form-item>
<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>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="state.btnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, toRefs, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import type { CheckboxValueType } from 'element-plus';
const props = defineProps({
data: {
type: [Boolean, Object],
},
title: {
type: String,
},
dbId: {
type: [Number],
required: true,
},
});
const visible = defineModel<boolean>('visible', {
default: false,
});
//定义事件
const emit = defineEmits(['cancel', 'val-change']);
const rules = {
dbNames: [
{
required: true,
message: '请选择需要备份的数据库',
trigger: ['change', 'blur'],
},
],
intervalDay: [
{
required: true,
pattern: /^[1-9]\d*$/,
message: '请输入正整数',
trigger: ['change', 'blur'],
},
],
startTime: [
{
required: true,
message: '请选择开始时间',
trigger: ['change', 'blur'],
},
],
maxSaveDays: [
{
required: true,
pattern: /^[0-9]\d*$/,
message: '请输入非负整数',
trigger: ['change', 'blur'],
},
],
};
const backupForm: any = ref(null);
const state = reactive({
form: {
id: 0,
dbId: 0,
dbNames: '',
name: '',
intervalDay: 1,
startTime: null as any,
repeated: true,
maxSaveDays: 0,
},
btnLoading: false,
dbNamesSelected: [] as any,
dbNamesWithoutBackup: [] as any,
dbNamesFiltered: [] as any,
filterString: '',
editOrCreate: false,
});
const { dbNamesSelected, dbNamesWithoutBackup } = toRefs(state);
const checkAllDbNames = ref(false);
const indeterminateDbNames = ref(false);
watch(visible, (newValue: any) => {
if (newValue) {
init(props.data);
}
});
const init = (data: any) => {
state.dbNamesSelected = [];
state.form.dbId = props.dbId;
if (data) {
state.editOrCreate = true;
state.dbNamesWithoutBackup = [data.dbName];
state.dbNamesSelected = [data.dbName];
state.form.id = data.id;
state.form.dbNames = data.dbName;
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 = 1;
const now = new Date();
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
state.form.maxSaveDays = 0;
getDbNamesWithoutBackup();
}
};
const getDbNamesWithoutBackup = async () => {
if (props.dbId > 0) {
state.dbNamesWithoutBackup = await dbApi.getDbNamesWithoutBackup.request({ dbId: props.dbId });
}
};
const btnOk = async () => {
backupForm.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('请正确填写信息');
return false;
}
state.form.repeated = true;
const reqForm = { ...state.form };
let api = dbApi.createDbBackup;
if (props.data) {
api = dbApi.saveDbBackup;
}
try {
state.btnLoading = true;
await api.request(reqForm);
ElMessage.success('保存成功');
emit('val-change', state.form);
cancel();
} finally {
state.btnLoading = false;
}
});
};
const cancel = () => {
visible.value = false;
emit('cancel');
};
const checkDbSelect = (val: string[]) => {
const selected = val.filter((dbName: string) => {
return dbName.includes(state.filterString);
});
if (selected.length === 0) {
checkAllDbNames.value = false;
indeterminateDbNames.value = false;
return;
}
if (selected.length === state.dbNamesFiltered.length) {
checkAllDbNames.value = true;
indeterminateDbNames.value = false;
return;
}
indeterminateDbNames.value = true;
};
watch(dbNamesSelected, (val: string[]) => {
checkDbSelect(val);
state.form.dbNames = val.join(' ');
});
watch(dbNamesWithoutBackup, (val: string[]) => {
state.dbNamesFiltered = val.map((dbName: string) => dbName);
});
const handleCheckAll = (val: CheckboxValueType) => {
const selected = state.dbNamesSelected.filter((dbName: string) => {
return !state.dbNamesFiltered.includes(dbName);
});
if (val) {
state.dbNamesSelected = selected.concat(state.dbNamesFiltered);
} else {
state.dbNamesSelected = selected;
}
};
const filterDbNames = (filterString: string) => {
state.dbNamesFiltered = state.dbNamesWithoutBackup.filter((dbName: string) => {
return dbName.includes(filterString);
});
state.filterString = filterString;
checkDbSelect(state.dbNamesSelected);
};
</script>
<style lang="scss"></style>

View File

@@ -1,155 +0,0 @@
<template>
<div class="db-backup-history">
<page-table
height="100%"
ref="pageTableRef"
:page-api="dbApi.getDbBackupHistories"
:show-selection="true"
v-model:selection-data="state.selectedData"
:searchItems="searchItems"
:before-query-fn="beforeQueryFn"
v-model:query-form="query"
:columns="columns"
>
<template #dbSelect>
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
</el-select>
</template>
<template #tableHeader>
<el-button type="primary" icon="back" @click="restoreDbBackupHistory(null)">立即恢复</el-button>
<el-button type="danger" icon="delete" @click="deleteDbBackupHistory(null)">删除</el-button>
</template>
<template #action="{ data }">
<div>
<el-button @click="restoreDbBackupHistory(data)" type="primary" link>立即恢复</el-button>
<el-button @click="deleteDbBackupHistory(data)" type="danger" link>删除</el-button>
</div>
</template>
</page-table>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, Ref, ref } from 'vue';
import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { ElMessage, ElMessageBox } from 'element-plus';
const pageTableRef: Ref<any> = ref(null);
const props = defineProps({
dbId: {
type: [Number],
required: true,
},
dbNames: {
type: [Array<String>],
required: true,
},
});
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
const columns = [
TableColumn.new('dbName', '数据库名称'),
TableColumn.new('name', '备份名称'),
TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new('lastResult', '恢复结果'),
TableColumn.new('lastTime', '恢复时间').isTime(),
TableColumn.new('action', '操作').isSlot().setMinWidth(160).fixedRight(),
];
const emptyQuery = {
dbId: 0,
dbName: '',
pageNum: 1,
pageSize: 10,
};
const state = reactive({
data: [],
total: 0,
query: emptyQuery,
/**
* 选中的数据
*/
selectedData: [],
});
const { query } = toRefs(state);
const beforeQueryFn = (query: any) => {
query.dbId = props.dbId;
return query;
};
const search = async () => {
await pageTableRef.value.search();
};
const deleteDbBackupHistory = async (data: any) => {
let backupHistoryId: string;
if (data) {
backupHistoryId = data.id;
} else if (state.selectedData.length > 0) {
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要删除的数据库备份历史');
return;
}
await ElMessageBox.confirm(`确定删除 “数据库备份历史” 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbBackupHistory.request({ dbId: props.dbId, backupHistoryId: backupHistoryId });
await search();
ElMessage.success('删除成功');
};
const restoreDbBackupHistory = async (data: any) => {
let backupHistoryId: string;
if (data) {
backupHistoryId = data.id;
} else if (state.selectedData.length > 0) {
const pluralDbNames: string[] = [];
const dbNames: Map<string, boolean> = new Map();
state.selectedData.forEach((item: any) => {
if (!dbNames.has(item.dbName)) {
dbNames.set(item.dbName, false);
return;
}
if (!dbNames.get(item.dbName)) {
dbNames.set(item.dbName, true);
pluralDbNames.push(item.dbName);
}
});
if (pluralDbNames.length > 0) {
ElMessage.error('多次选择相同数据库:' + pluralDbNames.join(', '));
return;
}
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要恢复的数据库备份历史');
return;
}
await ElMessageBox.confirm(`确定从 “数据库备份历史” 中恢复数据库吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.restoreDbBackupHistory.request({
dbId: props.dbId,
backupHistoryId: backupHistoryId,
});
await search();
ElMessage.success('成功创建数据库恢复任务');
};
</script>
<style lang="scss"></style>

View File

@@ -1,194 +0,0 @@
<template>
<div class="db-backup">
<page-table
height="100%"
ref="pageTableRef"
:page-api="dbApi.getDbBackups"
:show-selection="true"
v-model:selection-data="state.selectedData"
:searchItems="searchItems"
:before-query-fn="beforeQueryFn"
v-model:query-form="query"
:columns="columns"
>
<template #dbSelect>
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
</el-select>
</template>
<template #tableHeader>
<el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button>
<el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</el-button>
<el-button type="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
<el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
</template>
<template #action="{ data }">
<div>
<el-button @click="editDbBackup(data)" type="primary" link>编辑</el-button>
<el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
<el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
<el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
<el-button @click="deleteDbBackup(data)" type="danger" link>删除</el-button>
</div>
</template>
</page-table>
<db-backup-edit
@val-change="search"
:title="dbBackupEditDialog.title"
:dbId="dbId"
:data="dbBackupEditDialog.data"
v-model:visible="dbBackupEditDialog.visible"
></db-backup-edit>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, defineAsyncComponent, Ref, ref } from 'vue';
import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { ElMessage, ElMessageBox } from 'element-plus';
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
const pageTableRef: Ref<any> = ref(null);
const props = defineProps({
dbId: {
type: [Number],
required: true,
},
dbNames: {
type: [Array<String>],
required: true,
},
});
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
const columns = [
TableColumn.new('dbName', '数据库名称'),
TableColumn.new('name', '任务名称'),
TableColumn.new('startTime', '启动时间').isTime(),
TableColumn.new('intervalDay', '备份周期'),
TableColumn.new('enabledDesc', '是否启用'),
TableColumn.new('lastResult', '执行结果'),
TableColumn.new('lastTime', '执行时间').isTime(),
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
];
const emptyQuery = {
dbId: 0,
dbName: '',
pageNum: 1,
pageSize: 10,
repeated: true,
};
const state = reactive({
data: [],
total: 0,
query: emptyQuery,
dbBackupEditDialog: {
visible: false,
data: null as any,
title: '创建数据库备份任务',
},
/**
* 选中的数据
*/
selectedData: [],
});
const { query, dbBackupEditDialog } = toRefs(state);
const beforeQueryFn = (query: any) => {
query.dbId = props.dbId;
return query;
};
const search = async () => {
await pageTableRef.value.search();
};
const createDbBackup = async () => {
state.dbBackupEditDialog.data = null;
state.dbBackupEditDialog.title = '创建数据库备份任务';
state.dbBackupEditDialog.visible = true;
};
const editDbBackup = async (data: any) => {
state.dbBackupEditDialog.data = data;
state.dbBackupEditDialog.title = '修改数据库备份任务';
state.dbBackupEditDialog.visible = true;
};
const enableDbBackup = async (data: any) => {
let backupId: String;
if (data) {
backupId = data.id;
} else if (state.selectedData.length > 0) {
backupId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要启用的备份任务');
return;
}
await dbApi.enableDbBackup.request({ dbId: props.dbId, backupId: backupId });
await search();
ElMessage.success('启用成功');
};
const disableDbBackup = async (data: any) => {
let backupId: String;
if (data) {
backupId = data.id;
} else if (state.selectedData.length > 0) {
backupId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要禁用的备份任务');
return;
}
await dbApi.disableDbBackup.request({ dbId: props.dbId, backupId: backupId });
await search();
ElMessage.success('禁用成功');
};
const startDbBackup = async (data: any) => {
let backupId: String;
if (data) {
backupId = data.id;
} else if (state.selectedData.length > 0) {
backupId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要启用的备份任务');
return;
}
await dbApi.startDbBackup.request({ dbId: props.dbId, backupId: backupId });
await search();
ElMessage.success('备份任务启动成功');
};
const deleteDbBackup = async (data: any) => {
let backupId: string;
if (data) {
backupId = data.id;
} else if (state.selectedData.length > 0) {
backupId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要删除的数据库备份任务');
return;
}
await ElMessageBox.confirm(`确定删除 “数据库备份任务” 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbBackup.request({ dbId: props.dbId, backupId: backupId });
await search();
ElMessage.success('删除成功');
};
</script>
<style lang="scss"></style>

View File

@@ -1,311 +0,0 @@
<template>
<div>
<el-dialog :title="title" :model-value="visible" :before-close="cancel" :close-on-click-modal="false" width="38%">
<el-form :model="state.form" ref="restoreForm" label-width="auto" :rules="rules">
<el-form-item label="恢复方式">
<el-radio-group :disabled="state.editOrCreate" v-model="state.restoreMode">
<el-radio label="point-in-time">指定时间点</el-radio>
<el-radio label="backup-history">指定备份</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="dbName" label="数据库名称">
<el-select
:disabled="state.editOrCreate"
@change="changeDatabase"
v-model="state.form.dbName"
placeholder="数据库名称"
filterable
clearable
class="!w-full"
>
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="state.restoreMode == 'point-in-time'" prop="pointInTime" label="恢复时间点">
<el-date-picker :disabled="state.editOrCreate" v-model="state.form.pointInTime" type="datetime" placeholder="恢复时间点" />
</el-form-item>
<el-form-item v-if="state.restoreMode == 'backup-history'" prop="dbBackupHistoryId" label="数据库备份">
<el-select
:disabled="state.editOrCreate"
@change="changeHistory"
v-model="state.history"
value-key="id"
placeholder="数据库备份"
filterable
clearable
class="!w-full"
>
<el-option
v-for="item in state.histories"
:key="item.id"
:label="item.name + (item.binlogFileName ? ' ' : ' 不') + '支持指定时间点恢复'"
:value="item"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item prop="startTime" label="开始时间">
<el-date-picker :disabled="state.editOrCreate" v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="state.btnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus';
const props = defineProps({
data: {
type: [Boolean, Object],
},
title: {
type: String,
},
dbId: {
type: [Number],
required: true,
},
dbNames: {
type: Array,
required: true,
},
});
//定义事件
const emit = defineEmits(['cancel', 'val-change']);
const visible = defineModel<boolean>('visible', {
default: false,
});
const validatePointInTime = (rule: any, value: any, callback: any) => {
if (value > new Date()) {
callback(new Error('恢复时间点晚于当前时间'));
return;
}
if (!state.histories || state.histories.length == 0) {
callback(new Error('数据库没有备份记录'));
return;
}
let last = null;
for (const history of state.histories) {
if (!history.binlogFileName || history.binlogFileName.length === 0) {
break;
}
if (new Date(history.createTime) < value) {
callback();
return;
}
last = history;
}
if (!last) {
callback(new Error('现有数据库备份不支持指定时间恢复'));
return;
}
callback(last.name + ' 之前的数据库备份不支持指定时间恢复');
};
const rules = {
dbName: [
{
required: true,
message: '请选择需要恢复的数据库',
trigger: ['change', 'blur'],
},
],
pointInTime: [
{
required: true,
validator: validatePointInTime,
trigger: ['change', 'blur'],
},
],
dbBackupHistoryId: [
{
required: true,
message: '请选择数据库备份',
trigger: ['change', 'blur'],
},
],
intervalDay: [
{
required: true,
pattern: /^[1-9]\d*$/,
message: '请输入正整数',
trigger: ['change', 'blur'],
},
],
startTime: [
{
required: true,
message: '请选择开始时间',
trigger: ['change', 'blur'],
},
],
};
const restoreForm: any = ref(null);
const state = reactive({
form: {
id: 0,
dbId: 0,
dbName: null as any,
intervalDay: 0,
startTime: null as any,
repeated: null as any,
dbBackupId: null as any,
dbBackupHistoryId: null as any,
dbBackupHistoryName: null as any,
pointInTime: null as any,
},
btnLoading: false,
dbNamesSelected: [] as any,
dbNamesWithoutRestore: [] as any,
editOrCreate: false,
histories: [] as any,
history: null as any,
restoreMode: null as any,
});
onMounted(async () => {
await init(props.data);
});
watch(visible, (newValue: any) => {
if (newValue) {
init(props.data);
}
});
/**
* 改变表单中的数据库字段,方便表单错误提示。如全部删光,可提示请添加数据库
*/
const changeDatabase = async () => {
await getBackupHistories(props.dbId, state.form.dbName);
};
const changeHistory = async () => {
if (state.history) {
state.form.dbBackupId = state.history.dbBackupId;
state.form.dbBackupHistoryId = state.history.id;
state.form.dbBackupHistoryName = state.history.name;
}
};
const init = async (data: any) => {
state.dbNamesSelected = [];
state.form.dbId = props.dbId;
if (data) {
state.editOrCreate = true;
state.dbNamesWithoutRestore = [data.dbName];
state.dbNamesSelected = [data.dbName];
state.form.id = data.id;
state.form.dbName = data.dbName;
state.form.intervalDay = data.intervalDay;
state.form.pointInTime = data.pointInTime;
state.form.startTime = data.startTime;
state.form.dbBackupId = data.dbBackupId;
state.form.dbBackupHistoryId = data.dbBackupHistoryId;
state.form.dbBackupHistoryName = data.dbBackupHistoryName;
if (data.pointInTime) {
state.restoreMode = 'point-in-time';
} else {
state.restoreMode = 'backup-history';
}
state.history = {
dbBackupId: data.dbBackupId,
id: data.dbBackupHistoryId,
name: data.dbBackupHistoryName,
createTime: data.createTime,
};
await getBackupHistories(props.dbId, data.dbName);
} else {
state.form.dbName = '';
state.editOrCreate = false;
state.form.intervalDay = 0;
state.form.repeated = false;
state.form.pointInTime = new Date();
state.form.startTime = new Date();
state.histories = [];
state.history = null;
state.restoreMode = 'point-in-time';
await getDbNamesWithoutRestore();
}
};
const getDbNamesWithoutRestore = async () => {
if (props.dbId > 0) {
state.dbNamesWithoutRestore = await dbApi.getDbNamesWithoutRestore.request({ dbId: props.dbId });
}
};
const btnOk = async () => {
restoreForm.value.validate(async (valid: any) => {
if (valid) {
await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
if (state.restoreMode == 'point-in-time') {
state.form.dbBackupId = 0;
state.form.dbBackupHistoryId = 0;
state.form.dbBackupHistoryName = '';
} else {
state.form.pointInTime = null;
}
state.form.repeated = false;
state.form.intervalDay = 0;
const reqForm = { ...state.form };
let api = dbApi.createDbRestore;
if (props.data) {
api = dbApi.saveDbRestore;
}
api.request(reqForm).then(() => {
ElMessage.success('成功创建数据库恢复任务');
emit('val-change', state.form);
state.btnLoading = true;
setTimeout(() => {
state.btnLoading = false;
}, 1000);
cancel();
});
} else {
ElMessage.error('请正确填写信息');
return false;
}
});
};
const cancel = () => {
visible.value = false;
emit('cancel');
};
const getBackupHistories = async (dbId: Number, dbName: String) => {
if (!dbId || !dbName) {
state.histories = [];
return;
}
const data = await dbApi.getDbBackupHistories.request({ dbId, dbName });
if (!data || !data.list) {
ElMessage.error('该数据库没有备份记录,无法创建数据库恢复任务');
state.histories = [];
return;
}
state.histories = data.list;
};
</script>
<style lang="scss"></style>

View File

@@ -1,195 +0,0 @@
<template>
<div class="db-restore">
<page-table
height="100%"
ref="pageTableRef"
:page-api="dbApi.getDbRestores"
:show-selection="true"
v-model:selection-data="state.selectedData"
:searchItems="searchItems"
:before-query-fn="beforeQueryFn"
v-model:query-form="query"
:columns="columns"
>
<template #dbSelect>
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
<el-option v-for="item in dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
</el-select>
</template>
<template #tableHeader>
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
<el-button type="primary" icon="video-play" @click="enableDbRestore(null)">启用</el-button>
<el-button type="primary" icon="video-pause" @click="disableDbRestore(null)">禁用</el-button>
<el-button type="danger" icon="delete" @click="deleteDbRestore(null)">删除</el-button>
</template>
<template #action="{ data }">
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
<el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
<el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
<el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
</template>
</page-table>
<db-restore-edit
@val-change="search"
:title="dbRestoreEditDialog.title"
:dbId="dbId"
:dbNames="dbNames"
:data="dbRestoreEditDialog.data"
v-model:visible="dbRestoreEditDialog.visible"
></db-restore-edit>
<el-dialog v-model="infoDialog.visible" title="数据库恢复">
<el-descriptions :column="1" border>
<el-descriptions-item :span="1" label="数据库名称">{{ infoDialog.data.dbName }}</el-descriptions-item>
<el-descriptions-item v-if="infoDialog.data.pointInTime" :span="1" label="恢复时间点">{{
formatDate(infoDialog.data.pointInTime)
}}</el-descriptions-item>
<el-descriptions-item v-if="!infoDialog.data.pointInTime" :span="1" label="数据库备份">{{
infoDialog.data.dbBackupHistoryName
}}</el-descriptions-item>
<el-descriptions-item :span="1" label="开始时间">{{ formatDate(infoDialog.data.startTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行时间">{{ formatDate(infoDialog.data.lastTime) }}</el-descriptions-item>
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, defineAsyncComponent, Ref, ref } from 'vue';
import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/pagetable/SearchForm';
import { ElMessage, ElMessageBox } from 'element-plus';
import { formatDate } from '@/common/utils/format';
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
const pageTableRef: Ref<any> = ref(null);
const props = defineProps({
dbId: {
type: [Number],
required: true,
},
dbNames: {
type: [Array<String>],
required: true,
},
});
// const queryConfig = [TableQuery.slot('dbName', '数据库名称', 'dbSelect')];
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
const columns = [
TableColumn.new('dbName', '数据库名称'),
TableColumn.new('startTime', '启动时间').isTime(),
TableColumn.new('enabledDesc', '是否启用'),
TableColumn.new('lastTime', '执行时间').isTime(),
TableColumn.new('lastResult', '执行结果'),
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
];
const emptyQuery = {
dbId: props.dbId,
dbName: '',
pageNum: 1,
pageSize: 10,
repeated: false,
};
const state = reactive({
data: [],
total: 0,
query: emptyQuery,
dbRestoreEditDialog: {
visible: false,
data: null as any,
title: '创建数据库恢复任务',
},
infoDialog: {
visible: false,
data: null as any,
},
/**
* 选中的数据
*/
selectedData: [],
});
const { query, dbRestoreEditDialog, infoDialog } = toRefs(state);
const beforeQueryFn = (query: any) => {
query.dbId = props.dbId;
return query;
};
const search = async () => {
await pageTableRef.value.search();
};
const createDbRestore = async () => {
state.dbRestoreEditDialog.data = null;
state.dbRestoreEditDialog.title = '数据库恢复';
state.dbRestoreEditDialog.visible = true;
};
const deleteDbRestore = async (data: any) => {
let restoreId: string;
if (data) {
restoreId = data.id;
} else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要删除的数据库恢复任务');
return;
}
await ElMessageBox.confirm(`确定删除 “数据库恢复任务” 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await dbApi.deleteDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
await search();
ElMessage.success('删除成功');
};
const showDbRestore = async (data: any) => {
state.infoDialog.data = data;
state.infoDialog.visible = true;
};
const enableDbRestore = async (data: any) => {
let restoreId: string;
if (data) {
restoreId = data.id;
} else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要启用的数据库恢复任务');
return;
}
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
await search();
ElMessage.success('启用成功');
};
const disableDbRestore = async (data: any) => {
let restoreId: string;
if (data) {
restoreId = data.id;
} else if (state.selectedData.length > 0) {
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
} else {
ElMessage.error('请选择需要禁用的数据库恢复任务');
return;
}
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
await search();
ElMessage.success('禁用成功');
};
</script>
<style lang="scss"></style>

View File

@@ -59,36 +59,13 @@ export const dbApi = {
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'), enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'), disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
saveDbRestore: Api.newPut('/dbs/{dbId}/restores/{id}'), saveDbRestore: Api.newPut('/dbs/{dbId}/restores/{id}'),
// 数据同步相关
datasyncTasks: Api.newGet('/datasync/tasks'),
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler(async (param: any) => await encryptField(param, 'dataSql')),
getDatasyncTask: Api.newGet('/datasync/tasks/{taskId}'),
deleteDatasyncTask: Api.newDelete('/datasync/tasks/{taskId}/del'),
updateDatasyncTaskStatus: Api.newPost('/datasync/tasks/{taskId}/status'),
runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
// 数据库迁移相关
dbTransferTasks: Api.newGet('/dbTransfer'),
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
updateDbTransferTaskStatus: Api.newPost('/dbTransfer/{taskId}/status'),
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
}; };
export const dbSqlExecApi = { export const dbSqlExecApi = {
// 根据业务key获取sql执行信息 // 根据业务key获取sql执行信息
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'), getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
}; };
const encryptField = async (param: any, field: string) => { export const encryptField = async (param: any, field: string) => {
// sql编码处理 // sql编码处理
if (!param['_encrypted'] && param[field]) { if (!param['_encrypted'] && param[field]) {
// 判断是开发环境就打印sql // 判断是开发环境就打印sql

View File

@@ -19,39 +19,3 @@ export const DbSqlExecStatusEnum = {
Success: EnumValue.of(2, 'common.success').setTagType('success'), Success: EnumValue.of(2, 'common.success').setTagType('success'),
Fail: EnumValue.of(-2, 'common.fail').setTagType('danger'), Fail: EnumValue.of(-2, 'common.fail').setTagType('danger'),
}; };
export const DbDataSyncDuplicateStrategyEnum = {
None: EnumValue.of(-1, 'db.none'),
Ignore: EnumValue.of(1, 'db.ignore'),
Replace: EnumValue.of(2, 'db.replace'),
};
export const DbDataSyncRecentStateEnum = {
Success: EnumValue.of(1, 'common.success').setTagType('success'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
};
export const DbDataSyncLogStatusEnum = {
Success: EnumValue.of(1, 'common.success').setTagType('success'),
Running: EnumValue.of(2, 'db.running').setTagType('primary'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
};
export const DbDataSyncRunningStateEnum = {
Running: EnumValue.of(1, 'db.running').setTagType('success'),
WaitRun: EnumValue.of(2, 'db.waitRun').setTagType('primary'),
Stop: EnumValue.of(3, 'db.stop').setTagType('danger'),
};
export const DbTransferRunningStateEnum = {
Success: EnumValue.of(2, 'common.success').setTagType('success'),
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
Stop: EnumValue.of(-2, 'db.stop').setTagType('warning'),
};
export const DbTransferFileStatusEnum = {
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
Success: EnumValue.of(2, 'common.success').setTagType('success'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
};

View File

@@ -1,4 +1,4 @@
export default { export default {
SyncTaskList: () => import('@/views/ops/db/SyncTaskList.vue'), SyncTaskList: () => import('@/views/ops/db/sync/SyncTaskList.vue'),
DbTransferList: () => import('@/views/ops/db/DbTransferList.vue'), DbTransferList: () => import('@/views/ops/db/transfer/DbTransferList.vue'),
}; };

View File

@@ -209,7 +209,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, reactive, ref, toRefs, watch } from 'vue'; import { computed, reactive, ref, toRefs, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue'; import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue'; import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
@@ -218,11 +217,13 @@ import { compatibleDuplicateStrategy, DbType, getDbDialect } from '@/views/ops/d
import CrontabInput from '@/components/crontab/CrontabInput.vue'; import CrontabInput from '@/components/crontab/CrontabInput.vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue'; import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import EnumSelect from '@/components/enumselect/EnumSelect.vue'; import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { DbDataSyncDuplicateStrategyEnum } from './enums';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n'; import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FormItemTooltip from '@/components/form/FormItemTooltip.vue'; import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
import { Rules } from '@/common/rule'; import { Rules } from '@/common/rule';
import { DbDataSyncDuplicateStrategyEnum } from '@/views/ops/db/sync/enums';
import { dbSyncApi } from '@/views/ops/db/sync/api';
import { dbApi } from '@/views/ops/db/api';
const { t } = useI18n(); const { t } = useI18n();
@@ -306,7 +307,7 @@ const state = reactive({
const { tabActiveName, form, submitForm, fieldMapTableHeight } = toRefs(state); const { tabActiveName, form, submitForm, fieldMapTableHeight } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDatasyncTask.useApi(submitForm); const { isFetching: saveBtnLoading, execute: saveExec } = dbSyncApi.saveDatasyncTask.useApi(submitForm);
// //
const baseFieldCompleted = computed(() => { const baseFieldCompleted = computed(() => {
@@ -326,7 +327,7 @@ watch(dialogVisible, async (newValue: boolean) => {
return; return;
} }
let data = await dbApi.getDatasyncTask.request({ taskId: propsData?.id }); let data = await dbSyncApi.getDatasyncTask.request({ taskId: propsData?.id });
state.form = data; state.form = data;
if (!state.form.duplicateStrategy) { if (!state.form.duplicateStrategy) {
state.form.duplicateStrategy = -1; state.form.duplicateStrategy = -1;

View File

@@ -2,7 +2,7 @@
<div class="h-full"> <div class="h-full">
<page-table <page-table
ref="pageTableRef" ref="pageTableRef"
:page-api="dbApi.datasyncTasks" :page-api="dbSyncApi.datasyncTasks"
:searchItems="searchItems" :searchItems="searchItems"
v-model:query-form="query" v-model:query-form="query"
:show-selection="true" :show-selection="true"
@@ -50,13 +50,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue'; import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth'; import { hasPerms } from '@/components/auth/auth';
import { SearchItem } from '@/components/pagetable/SearchForm'; import { SearchItem } from '@/components/pagetable/SearchForm';
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from './enums';
import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n'; import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { dbSyncApi } from '@/views/ops/db/sync/api';
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from '@/views/ops/db/sync/enums';
const DataSyncTaskEdit = defineAsyncComponent(() => import('./SyncTaskEdit.vue')); const DataSyncTaskEdit = defineAsyncComponent(() => import('./SyncTaskEdit.vue'));
const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue')); const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue'));
@@ -143,14 +143,14 @@ const edit = async (data: any) => {
const run = async (id: any) => { const run = async (id: any) => {
await useI18nConfirm('db.runConfirm'); await useI18nConfirm('db.runConfirm');
await dbApi.runDatasyncTask.request({ taskId: id }); await dbSyncApi.runDatasyncTask.request({ taskId: id });
useI18nOperateSuccessMsg(); useI18nOperateSuccessMsg();
setTimeout(search, 1000); setTimeout(search, 1000);
}; };
const stop = async (id: any) => { const stop = async (id: any) => {
await useI18nConfirm('db.stopConfirm'); await useI18nConfirm('db.stopConfirm');
await dbApi.stopDatasyncTask.request({ taskId: id }); await dbSyncApi.stopDatasyncTask.request({ taskId: id });
useI18nOperateSuccessMsg(); useI18nOperateSuccessMsg();
search(); search();
}; };
@@ -163,7 +163,7 @@ const log = async (data: any) => {
const updStatus = async (id: any, status: 1 | -1) => { const updStatus = async (id: any, status: 1 | -1) => {
try { try {
await dbApi.updateDatasyncTaskStatus.request({ taskId: id, status }); await dbSyncApi.updateDatasyncTaskStatus.request({ taskId: id, status });
useI18nOperateSuccessMsg(); useI18nOperateSuccessMsg();
search(); search();
} catch (err) { } catch (err) {
@@ -174,7 +174,7 @@ const updStatus = async (id: any, status: 1 | -1) => {
const del = async () => { const del = async () => {
try { try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、')); await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
await dbApi.deleteDatasyncTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') }); await dbSyncApi.deleteDatasyncTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg(); useI18nDeleteSuccessMsg();
search(); search();
} catch (err) { } catch (err) {

View File

@@ -6,7 +6,7 @@
<el-switch v-model="realTime" @change="watchPolling" inline-prompt :active-text="$t('db.realTime')" :inactive-text="$t('db.noRealTime')" /> <el-switch v-model="realTime" @change="watchPolling" inline-prompt :active-text="$t('db.realTime')" :inactive-text="$t('db.noRealTime')" />
<el-button @click="search" icon="Refresh" circle size="small" :loading="realTime" class="ml-2"></el-button> <el-button @click="search" icon="Refresh" circle size="small" :loading="realTime" class="ml-2"></el-button>
</template> </template>
<page-table ref="logTableRef" :page-api="dbApi.datasyncLogs" v-model:query-form="query" :tool-button="false" :columns="columns" size="small"> <page-table ref="logTableRef" :page-api="dbSyncApi.datasyncLogs" v-model:query-form="query" :tool-button="false" :columns="columns" size="small">
</page-table> </page-table>
</el-dialog> </el-dialog>
</div> </div>
@@ -14,10 +14,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, Ref, ref, toRefs, watch } from 'vue'; import { reactive, Ref, ref, toRefs, watch } from 'vue';
import { dbApi } from '@/views/ops/db/api';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { DbDataSyncLogStatusEnum } from './enums'; import { dbSyncApi } from '@/views/ops/db/sync/api';
import { DbDataSyncLogStatusEnum } from '@/views/ops/db/sync/enums';
const props = defineProps({ const props = defineProps({
taskId: { taskId: {

View File

@@ -0,0 +1,14 @@
import Api from '@/common/Api';
import { encryptField } from '@/views/ops/db/api';
export const dbSyncApi = {
// 数据同步相关
datasyncTasks: Api.newGet('/datasync/tasks'),
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler(async (param: any) => await encryptField(param, 'dataSql')),
getDatasyncTask: Api.newGet('/datasync/tasks/{taskId}'),
deleteDatasyncTask: Api.newDelete('/datasync/tasks/{taskId}/del'),
updateDatasyncTaskStatus: Api.newPost('/datasync/tasks/{taskId}/status'),
runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
};

View File

@@ -0,0 +1,24 @@
import { EnumValue } from '@/common/Enum';
export const DbDataSyncDuplicateStrategyEnum = {
None: EnumValue.of(-1, 'db.none'),
Ignore: EnumValue.of(1, 'db.ignore'),
Replace: EnumValue.of(2, 'db.replace'),
};
export const DbDataSyncRecentStateEnum = {
Success: EnumValue.of(1, 'common.success').setTagType('success'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
};
export const DbDataSyncLogStatusEnum = {
Success: EnumValue.of(1, 'common.success').setTagType('success'),
Running: EnumValue.of(2, 'db.running').setTagType('primary'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
};
export const DbDataSyncRunningStateEnum = {
Running: EnumValue.of(1, 'db.running').setTagType('success'),
WaitRun: EnumValue.of(2, 'db.waitRun').setTagType('primary'),
Stop: EnumValue.of(3, 'db.stop').setTagType('danger'),
};

View File

@@ -0,0 +1 @@
Db sync (数据库迁移模块)

View File

@@ -12,7 +12,7 @@
<el-input v-model.trim="form.taskName" auto-complete="off" /> <el-input v-model.trim="form.taskName" auto-complete="off" />
</el-form-item> </el-form-item>
<el-row class="!w-full"> <el-row class="w-full!">
<el-col :span="12"> <el-col :span="12">
<el-form-item prop="status" :label="$t('common.status')" label-position="left"> <el-form-item prop="status" :label="$t('common.status')" label-position="left">
<el-switch <el-switch
@@ -40,7 +40,7 @@
<CrontabInput v-model="form.cron" /> <CrontabInput v-model="form.cron" />
</el-form-item> </el-form-item>
<el-form-item prop="srcDbId" :label="$t('db.srcDb')" class="!w-full" required> <el-form-item prop="srcDbId" :label="$t('db.srcDb')" class="w-full!" required>
<db-select-tree <db-select-tree
v-model:db-id="form.srcDbId" v-model:db-id="form.srcDbId"
v-model:inst-name="form.srcInstName" v-model:inst-name="form.srcInstName"
@@ -59,7 +59,7 @@
</el-form-item> </el-form-item>
<el-form-item v-if="form.mode === 2"> <el-form-item v-if="form.mode === 2">
<el-row class="!w-full"> <el-row class="w-full!">
<el-col :span="12"> <el-col :span="12">
<el-form-item prop="targetFileDbType" :label="$t('db.dbFileType')" :required="form.mode === 2"> <el-form-item prop="targetFileDbType" :label="$t('db.dbFileType')" :required="form.mode === 2">
<el-select v-model="form.targetFileDbType" clearable filterable> <el-select v-model="form.targetFileDbType" clearable filterable>
@@ -98,7 +98,7 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item v-if="form.mode == 1" prop="targetDbId" :label="$t('db.targetDb')" class="!w-full" :required="form.mode === 1"> <el-form-item v-if="form.mode == 1" prop="targetDbId" :label="$t('db.targetDb')" class="w-full!" :required="form.mode === 1">
<db-select-tree <db-select-tree
v-model:db-id="form.targetDbId" v-model:db-id="form.targetDbId"
v-model:inst-name="form.targetInstName" v-model:inst-name="form.targetInstName"
@@ -121,11 +121,11 @@
<el-form-item> <el-form-item>
<el-input v-model="state.filterSrcTableText" placeholder="filter table" size="small" /> <el-input v-model="state.filterSrcTableText" placeholder="filter table" size="small" />
</el-form-item> </el-form-item>
<el-form-item class="!w-full"> <el-form-item class="w-full!">
<el-tree <el-tree
ref="srcTreeRef" ref="srcTreeRef"
class="!w-full" class="w-full! overflow-y-auto"
style="max-height: 200px; overflow-y: auto" style="max-height: 200px"
default-expand-all default-expand-all
:expand-on-click-node="false" :expand-on-click-node="false"
:data="state.srcTableTree" :data="state.srcTableTree"
@@ -149,7 +149,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, reactive, ref, toRefs, watch } from 'vue'; import { nextTick, reactive, ref, toRefs, watch } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue'; import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import CrontabInput from '@/components/crontab/CrontabInput.vue'; import CrontabInput from '@/components/crontab/CrontabInput.vue';
@@ -160,6 +160,8 @@ import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Rules } from '@/common/rule'; import { Rules } from '@/common/rule';
import { deepClone } from '@/common/utils/object'; import { deepClone } from '@/common/utils/object';
import { dbApi } from '@/views/ops/db/api';
import { dbTransferApi } from '@/views/ops/db/transfer/api';
const { t } = useI18n(); const { t } = useI18n();
@@ -256,7 +258,7 @@ const state = reactive({
const { form, submitForm } = toRefs(state); const { form, submitForm } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDbTransferTask.useApi(submitForm); const { isFetching: saveBtnLoading, execute: saveExec } = dbTransferApi.saveDbTransferTask.useApi(submitForm);
watch(dialogVisible, async (newValue: boolean) => { watch(dialogVisible, async (newValue: boolean) => {
if (!newValue) { if (!newValue) {

View File

@@ -12,7 +12,7 @@
<page-table <page-table
ref="pageTableRef" ref="pageTableRef"
v-model:query-form="state.query" v-model:query-form="state.query"
:page-api="dbApi.dbTransferFileList" :page-api="dbTransferApi.dbTransferFileList"
:lazy="true" :lazy="true"
:show-selection="true" :show-selection="true"
v-model:selection-data="state.selectionData" v-model:selection-data="state.selectionData"
@@ -79,7 +79,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue'; import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue';
import { dbApi } from '@/views/ops/db/api';
import { getDbDialect } from '@/views/ops/db/dialect'; import { getDbDialect } from '@/views/ops/db/dialect';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
@@ -89,10 +88,11 @@ import TerminalLog from '@/components/terminal/TerminalLog.vue';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue'; import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import { getClientId } from '@/common/utils/storage'; import { getClientId } from '@/common/utils/storage';
import FileInfo from '@/components/file/FileInfo.vue'; import FileInfo from '@/components/file/FileInfo.vue';
import { DbTransferFileStatusEnum } from './enums';
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n'; import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Rules } from '@/common/rule'; import { Rules } from '@/common/rule';
import { dbTransferApi } from '@/views/ops/db/transfer/api';
import { DbTransferFileStatusEnum } from '@/views/ops/db/transfer/enums';
const { t } = useI18n(); const { t } = useI18n();
@@ -179,7 +179,7 @@ const state = reactive({
return false; return false;
} }
state.runDialog.runForm.clientId = getClientId(); state.runDialog.runForm.clientId = getClientId();
await dbApi.dbTransferFileRun.request(state.runDialog.runForm); await dbTransferApi.dbTransferFileRun.request(state.runDialog.runForm);
useI18nOperateSuccessMsg(); useI18nOperateSuccessMsg();
state.runDialog.onCancel(); state.runDialog.onCancel();
await search(); await search();
@@ -196,7 +196,7 @@ const state = reactive({
const search = async () => { const search = async () => {
pageTableRef.value?.search(); pageTableRef.value?.search();
// const { total, list } = await dbApi.dbTransferFileList.request(state.query); // const { total, list } = await dbTransferApi.dbTransferFileList.request(state.query);
// state.tableData = list; // state.tableData = list;
// pageTableRef.value.total = total; // pageTableRef.value.total = total;
}; };
@@ -204,7 +204,7 @@ const search = async () => {
const onDel = async function () { const onDel = async function () {
try { try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).join('、')); await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).join('、'));
await dbApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') }); await dbTransferApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg(); useI18nDeleteSuccessMsg();
await search(); await search();
} catch (err) { } catch (err) {

View File

@@ -2,7 +2,7 @@
<div class="h-full"> <div class="h-full">
<page-table <page-table
ref="pageTableRef" ref="pageTableRef"
:page-api="dbApi.dbTransferTasks" :page-api="dbTransferApi.dbTransferTasks"
:searchItems="searchItems" :searchItems="searchItems"
v-model:query-form="query" v-model:query-form="query"
:show-selection="true" :show-selection="true"
@@ -84,19 +84,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue'; import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth'; import { hasPerms } from '@/components/auth/auth';
import { SearchItem } from '@/components/pagetable/SearchForm'; import { SearchItem } from '@/components/pagetable/SearchForm';
import { getDbDialect } from '@/views/ops/db/dialect'; import { getDbDialect } from '@/views/ops/db/dialect';
import { DbTransferRunningStateEnum } from './enums';
import TerminalLog from '@/components/terminal/TerminalLog.vue'; import TerminalLog from '@/components/terminal/TerminalLog.vue';
import DbTransferFile from './DbTransferFile.vue';
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n'; import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { dbTransferApi } from '@/views/ops/db/transfer/api';
import { DbTransferRunningStateEnum } from '@/views/ops/db/transfer/enums';
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue')); const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
const DbTransferFile = defineAsyncComponent(() => import('./DbTransferFile.vue'));
const { t } = useI18n(); const { t } = useI18n();
@@ -189,7 +189,7 @@ const edit = async (data: any) => {
const stop = async (id: any) => { const stop = async (id: any) => {
await useI18nConfirm('db.stopConfirm'); await useI18nConfirm('db.stopConfirm');
await dbApi.stopDbTransferTask.request({ taskId: id }); await dbTransferApi.stopDbTransferTask.request({ taskId: id });
useI18nOperateSuccessMsg(); useI18nOperateSuccessMsg();
search(); search();
}; };
@@ -204,7 +204,7 @@ const onOpenLog = (data: any) => {
const onReRun = async (data: any) => { const onReRun = async (data: any) => {
await useI18nConfirm('db.runConfirm'); await useI18nConfirm('db.runConfirm');
try { try {
let res = await dbApi.runDbTransferTask.request({ taskId: data.id }); let res = await dbTransferApi.runDbTransferTask.request({ taskId: data.id });
useI18nOperateSuccessMsg(); useI18nOperateSuccessMsg();
// id // id
onOpenLog({ logId: res, state: DbTransferRunningStateEnum.Running.value }); onOpenLog({ logId: res, state: DbTransferRunningStateEnum.Running.value });
@@ -225,7 +225,7 @@ const openFiles = async (data: any) => {
}; };
const updStatus = async (id: any, status: 1 | -1) => { const updStatus = async (id: any, status: 1 | -1) => {
try { try {
await dbApi.updateDbTransferTaskStatus.request({ taskId: id, status }); await dbTransferApi.updateDbTransferTaskStatus.request({ taskId: id, status });
useI18nOperateSuccessMsg(); useI18nOperateSuccessMsg();
search(); search();
} catch (err) { } catch (err) {
@@ -236,7 +236,7 @@ const updStatus = async (id: any, status: 1 | -1) => {
const del = async () => { const del = async () => {
try { try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、')); await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
await dbApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') }); await dbTransferApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg(); useI18nDeleteSuccessMsg();
search(); search();
} catch (err) { } catch (err) {

View File

@@ -0,0 +1,16 @@
import Api from '@/common/Api';
export const dbTransferApi = {
// 数据库迁移相关
dbTransferTasks: Api.newGet('/dbTransfer'),
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
updateDbTransferTaskStatus: Api.newPost('/dbTransfer/{taskId}/status'),
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
};

View File

@@ -0,0 +1,14 @@
import { EnumValue } from '@/common/Enum';
export const DbTransferRunningStateEnum = {
Success: EnumValue.of(2, 'common.success').setTagType('success'),
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
Stop: EnumValue.of(-2, 'db.stop').setTagType('warning'),
};
export const DbTransferFileStatusEnum = {
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
Success: EnumValue.of(2, 'common.success').setTagType('success'),
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
};

View File

@@ -0,0 +1 @@
Db transfer (数据库迁移模块)

View File

@@ -19,6 +19,13 @@
<el-form-item prop="version" :label="t('common.version')"> <el-form-item prop="version" :label="t('common.version')">
<el-input v-model.trim="form.version" auto-complete="off" disabled></el-input> <el-input v-model.trim="form.version" auto-complete="off" disabled></el-input>
</el-form-item> </el-form-item>
<!-- 增加协议下拉框 http和https默认http-->
<el-form-item prop="protocol" :label="t('es.protocol')">
<el-select v-model="form.protocol" placeholder="http">
<el-option label="http" value="http"></el-option>
<el-option label="https" value="https"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="host" label="Host" required> <el-form-item prop="host" label="Host" required>
<el-col :span="18"> <el-col :span="18">
@@ -105,6 +112,7 @@ const DefaultForm = {
id: null, id: null,
code: '', code: '',
name: null, name: null,
protocol: 'http',
host: '', host: '',
version: '', version: '',
port: 9200, port: 9200,

View File

@@ -569,9 +569,9 @@ const parseParams = () => {
// minimum_should_match 需要结合should使用默认为1表示至少一个should条件满足 // minimum_should_match 需要结合should使用默认为1表示至少一个should条件满足
if (should.length > 0) { if (should.length > 0) {
state.search['minimum_should_match'] = Math.max(1, state.minimum_should_match); state.search.query.bool['minimum_should_match'] = Math.max(1, state.minimum_should_match);
} else { } else {
delete state.search['minimum_should_match']; delete state.search.query.bool['minimum_should_match'];
} }
}; };
</script> </script>

View File

@@ -3,7 +3,7 @@ module mayfly-go
go 1.25 go 1.25
require ( require (
gitee.com/chunanyong/dm v1.8.20 gitee.com/chunanyong/dm v1.8.21
gitee.com/liuzongyang/libpq v1.10.11 gitee.com/liuzongyang/libpq v1.10.11
github.com/antlr4-go/antlr/v4 v4.13.1 github.com/antlr4-go/antlr/v4 v4.13.1
github.com/docker/docker v28.5.0+incompatible github.com/docker/docker v28.5.0+incompatible
@@ -24,9 +24,9 @@ require (
github.com/mojocn/base64Captcha v1.3.8 // github.com/mojocn/base64Captcha v1.3.8 //
github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/image-spec v1.1.1
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.9 github.com/pkg/sftp v1.13.10
github.com/pquerna/otp v1.5.0 github.com/pquerna/otp v1.5.0
github.com/redis/go-redis/v9 v9.14.0 github.com/redis/go-redis/v9 v9.16.0
github.com/robfig/cron/v3 v3.0.1 // github.com/robfig/cron/v3 v3.0.1 //
github.com/sijms/go-ora/v2 v2.9.0 github.com/sijms/go-ora/v2 v2.9.0
github.com/spf13/cast v1.10.0 github.com/spf13/cast v1.10.0
@@ -34,14 +34,14 @@ require (
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
github.com/veops/go-ansiterm v0.0.5 github.com/veops/go-ansiterm v0.0.5
go.mongodb.org/mongo-driver/v2 v2.3.0 // mongo go.mongodb.org/mongo-driver/v2 v2.3.0 // mongo
golang.org/x/crypto v0.43.0 // ssh golang.org/x/crypto v0.44.0 // ssh
golang.org/x/oauth2 v0.32.0 golang.org/x/oauth2 v0.33.0
golang.org/x/sync v0.17.0 golang.org/x/sync v0.18.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
// gorm // gorm
gorm.io/driver/mysql v1.6.0 gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.0 gorm.io/gorm v1.31.1
) )
require ( require (
@@ -117,11 +117,11 @@ require (
golang.org/x/arch v0.21.0 // indirect golang.org/x/arch v0.21.0 // indirect
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
golang.org/x/image v0.31.0 // indirect golang.org/x/image v0.31.0 // indirect
golang.org/x/mod v0.28.0 // indirect golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.45.0 // indirect golang.org/x/net v0.46.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.30.0 // indirect golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.37.0 // indirect golang.org/x/tools v0.38.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/protobuf v1.36.10 // indirect
modernc.org/libc v1.66.10 // indirect modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect

View File

@@ -6,6 +6,7 @@ import (
type InstanceForm struct { type InstanceForm struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Protocol string `binding:"required" json:"protocol"`
Name string `binding:"required" json:"name"` Name string `binding:"required" json:"name"`
Host string `binding:"required" json:"host"` Host string `binding:"required" json:"host"`
Port int `binding:"required" json:"port"` Port int `binding:"required" json:"port"`

View File

@@ -12,6 +12,7 @@ type InstanceListVO struct {
Id *int64 `json:"id"` Id *int64 `json:"id"`
Code string `json:"code"` Code string `json:"code"`
Name *string `json:"name"` Name *string `json:"name"`
Protocol *string `json:"protocol"`
Host *string `json:"host"` Host *string `json:"host"`
Port *int `json:"port"` Port *int `json:"port"`
Version *string `json:"version"` Version *string `json:"version"`

View File

@@ -10,6 +10,7 @@ type EsInstance struct {
Code string `json:"code" gorm:"size:32;not null;"` Code string `json:"code" gorm:"size:32;not null;"`
Name string `json:"name" gorm:"size:32;not null;"` Name string `json:"name" gorm:"size:32;not null;"`
Protocol string `json:"protocol" gorm:"size:10;not null;"`
Host string `json:"host" gorm:"size:255;not null;"` Host string `json:"host" gorm:"size:255;not null;"`
Port int `json:"port"` Port int `json:"port"`
Network string `json:"network" gorm:"size:20;"` Network string `json:"network" gorm:"size:20;"`

View File

@@ -1,6 +1,7 @@
package esi package esi
import ( import (
"crypto/tls"
"fmt" "fmt"
"mayfly-go/internal/machine/mcm" "mayfly-go/internal/machine/mcm"
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
@@ -52,6 +53,16 @@ func (d *EsConn) StartProxy() error {
d.proxy = httputil.NewSingleHostReverseProxy(targetURL) d.proxy = httputil.NewSingleHostReverseProxy(targetURL)
// 设置 proxy buffer pool // 设置 proxy buffer pool
d.proxy.BufferPool = NewBufferPool() d.proxy.BufferPool = NewBufferPool()
// Configure TLS to skip certificate verification for non-compliant certificates
if targetURL.Scheme == "https" {
d.proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
return nil return nil
} }

View File

@@ -23,6 +23,7 @@ type EsInfo struct {
InstanceId uint64 // 实例id InstanceId uint64 // 实例id
Name string Name string
Protocol string // 协议默认http
Host string Host string
Port int Port int
Network string Network string
@@ -90,7 +91,14 @@ func (di *EsInfo) Ping() (map[string]any, error) {
// ExecApi 执行api // ExecApi 执行api
func (di *EsInfo) ExecApi(method, path string, data any, timeoutSecond ...int) (map[string]any, error) { func (di *EsInfo) ExecApi(method, path string, data any, timeoutSecond ...int) (map[string]any, error) {
request := httpx.NewReq(di.baseUrl + path) var request *httpx.Req
// Use insecure TLS client for HTTPS connections to handle non-compliant certificates
if di.Protocol == "https" {
request = httpx.NewReqWithInsecureTLS(di.baseUrl + path)
} else {
request = httpx.NewReq(di.baseUrl + path)
}
if di.authorization != "" { if di.authorization != "" {
request.Header("Authorization", di.authorization) request.Header("Authorization", di.authorization)
} }
@@ -117,6 +125,11 @@ func (di *EsInfo) ExecApi(method, path string, data any, timeoutSecond ...int) (
// 如果使用了ssh隧道将其host port改变其本地映射host port // 如果使用了ssh隧道将其host port改变其本地映射host port
func (di *EsInfo) IfUseSshTunnelChangeIpPort(ctx context.Context) error { func (di *EsInfo) IfUseSshTunnelChangeIpPort(ctx context.Context) error {
// 设置默认协议
if di.Protocol == "" {
di.Protocol = "http"
}
// 开启ssh隧道 // 开启ssh隧道
if di.SshTunnelMachineId > 0 { if di.SshTunnelMachineId > 0 {
stm, err := GetSshTunnel(ctx, di.SshTunnelMachineId) stm, err := GetSshTunnel(ctx, di.SshTunnelMachineId)
@@ -130,9 +143,9 @@ func (di *EsInfo) IfUseSshTunnelChangeIpPort(ctx context.Context) error {
di.Host = exposedIp di.Host = exposedIp
di.Port = exposedPort di.Port = exposedPort
di.useSshTunnel = true di.useSshTunnel = true
di.baseUrl = fmt.Sprintf("http://%s:%d", exposedIp, exposedPort) di.baseUrl = fmt.Sprintf("%s://%s:%d", di.Protocol, exposedIp, exposedPort)
} else { } else {
di.baseUrl = fmt.Sprintf("http://%s:%d", di.Host, di.Port) di.baseUrl = fmt.Sprintf("%s://%s:%d", di.Protocol, di.Host, di.Port)
} }
return nil return nil
} }

View File

@@ -20,6 +20,7 @@ func V1_10() []*gormigrate.Migration {
migrations = append(migrations, V1_10_1()...) migrations = append(migrations, V1_10_1()...)
migrations = append(migrations, V1_10_2()...) migrations = append(migrations, V1_10_2()...)
migrations = append(migrations, V1_10_3()...) migrations = append(migrations, V1_10_3()...)
migrations = append(migrations, V1_10_4()...)
return migrations return migrations
} }
@@ -326,3 +327,28 @@ func V1_10_3() []*gormigrate.Migration {
}, },
} }
} }
func V1_10_4() []*gormigrate.Migration {
return []*gormigrate.Migration{
{
ID: "20251023-v1.10.4",
Migrate: func(tx *gorm.DB) error {
// 给EsInstance表添加protocol列默认值为http, 20251023,fudawei
if !tx.Migrator().HasColumn(&esentity.EsInstance{}, "protocol") {
// 先添加可为空的列
if err := tx.Exec("ALTER TABLE t_es_instance ADD COLUMN protocol VARCHAR(10) DEFAULT 'http'").Error; err != nil {
return err
}
// 更新所有现有记录为默认值http
if err := tx.Exec("UPDATE t_es_instance SET protocol = 'http' WHERE protocol IS NULL OR protocol = ''").Error; err != nil {
return err
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return nil
},
},
}
}

View File

@@ -2,6 +2,7 @@ package httpx
import ( import (
"bytes" "bytes"
"crypto/tls"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -41,6 +42,16 @@ func NewReq(url string) *Req {
return &Req{url: url, client: http.Client{}} return &Req{url: url, client: http.Client{}}
} }
// 创建一个请求(不验证TLS证书)
func NewReqWithInsecureTLS(url string) *Req {
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
return &Req{url: url, client: http.Client{Transport: transport}}
}
func (r *Req) Url(url string) *Req { func (r *Req) Url(url string) *Req {
r.url = url r.url = url
return r return r

View File

@@ -9,28 +9,60 @@ import (
// 心跳间隔 // 心跳间隔
const heartbeatInterval = 25 * time.Second const heartbeatInterval = 25 * time.Second
// 单个用户的全部的连接, key->clientId, value->Client // UserClient 用户全部的连接
type UserClients map[string]*Client type UserClient struct {
clients map[string]*Client // key->clientId, value->Client
func (ucs UserClients) GetByCid(clientId string) *Client { mutex sync.RWMutex
return ucs[clientId]
} }
func (ucs UserClients) AddClient(client *Client) { func NewUserClient() *UserClient {
ucs[client.ClientId] = client return &UserClient{
clients: make(map[string]*Client),
}
} }
func (ucs UserClients) DeleteByCid(clientId string) { // AllClients 获取全部的连接
delete(ucs, clientId) func (ucs *UserClient) AllClients() []*Client {
ucs.mutex.RLock()
defer ucs.mutex.RUnlock()
result := make([]*Client, 0, len(ucs.clients))
for _, client := range ucs.clients {
result = append(result, client)
}
return result
} }
func (ucs UserClients) Count() int { // GetByCid 获取指定客户端ID的客户端
return len(ucs) func (ucs *UserClient) GetByCid(clientId string) *Client {
ucs.mutex.RLock()
defer ucs.mutex.RUnlock()
return ucs.clients[clientId]
}
// AddClient 添加客户端
func (ucs *UserClient) AddClient(client *Client) {
ucs.mutex.Lock()
defer ucs.mutex.Unlock()
ucs.clients[client.ClientId] = client
}
// DeleteByCid 删除指定客户端ID的客户端
func (ucs *UserClient) DeleteByCid(clientId string) {
ucs.mutex.Lock()
defer ucs.mutex.Unlock()
delete(ucs.clients, clientId)
}
// Count 返回客户端数量
func (ucs *UserClient) Count() int {
ucs.mutex.RLock()
defer ucs.mutex.RUnlock()
return len(ucs.clients)
} }
// 连接管理 // 连接管理
type ClientManager struct { type ClientManager struct {
UserClientsMap sync.Map // 全部的用户连接, key->userid, value->UserClients UserClientMap sync.Map // 全部的用户连接, key->userid, value->*UserClient
ConnectChan chan *Client // 连接处理 ConnectChan chan *Client // 连接处理
DisConnectChan chan *Client // 断开连接处理 DisConnectChan chan *Client // 断开连接处理
@@ -39,7 +71,7 @@ type ClientManager struct {
func NewClientManager() (clientManager *ClientManager) { func NewClientManager() (clientManager *ClientManager) {
return &ClientManager{ return &ClientManager{
UserClientsMap: sync.Map{}, UserClientMap: sync.Map{},
ConnectChan: make(chan *Client, 10), ConnectChan: make(chan *Client, 10),
DisConnectChan: make(chan *Client, 10), DisConnectChan: make(chan *Client, 10),
MsgChan: make(chan *Msg, 100), MsgChan: make(chan *Msg, 100),
@@ -77,18 +109,18 @@ func (manager *ClientManager) CloseClient(client *Client) {
// 根据用户id关闭客户端连接 // 根据用户id关闭客户端连接
func (manager *ClientManager) CloseByUid(userId UserId) { func (manager *ClientManager) CloseByUid(userId UserId) {
userClients := manager.GetByUid(userId) userClient := manager.GetByUid(userId)
for _, client := range userClients { for _, client := range userClient.AllClients() {
manager.CloseClient(client) manager.CloseClient(client)
} }
} }
// 获取所有的客户端 // 获取所有的客户端
func (manager *ClientManager) AllUserClient() map[UserId]UserClients { func (manager *ClientManager) AllUserClient() map[UserId]*UserClient {
result := make(map[UserId]UserClients) result := make(map[UserId]*UserClient)
manager.UserClientsMap.Range(func(key, value any) bool { manager.UserClientMap.Range(func(key, value any) bool {
userId := key.(UserId) userId := key.(UserId)
userClients := value.(UserClients) userClients := value.(*UserClient)
result[userId] = userClients result[userId] = userClients
return true return true
}) })
@@ -96,9 +128,9 @@ func (manager *ClientManager) AllUserClient() map[UserId]UserClients {
} }
// 通过userId获取用户所有客户端信息 // 通过userId获取用户所有客户端信息
func (manager *ClientManager) GetByUid(userId UserId) UserClients { func (manager *ClientManager) GetByUid(userId UserId) *UserClient {
if value, ok := manager.UserClientsMap.Load(userId); ok { if value, ok := manager.UserClientMap.Load(userId); ok {
return value.(UserClients) return value.(*UserClient)
} }
return nil return nil
} }
@@ -114,7 +146,7 @@ func (manager *ClientManager) GetByUidAndCid(uid UserId, clientId string) *Clien
// 客户端数量 // 客户端数量
func (manager *ClientManager) Count() int { func (manager *ClientManager) Count() int {
count := 0 count := 0
manager.UserClientsMap.Range(func(key, value any) bool { manager.UserClientMap.Range(func(key, value any) bool {
count++ count++
return true return true
}) })
@@ -148,7 +180,7 @@ func (manager *ClientManager) WriteMessage() {
// cid为空则向该用户所有客户端发送该消息 // cid为空则向该用户所有客户端发送该消息
userClients := manager.GetByUid(uid) userClients := manager.GetByUid(uid)
for _, cli := range userClients { for _, cli := range userClients.AllClients() {
if err := cli.WriteMsg(msg); err != nil { if err := cli.WriteMsg(msg); err != nil {
logx.Warnf("ws send message failed - [uid=%d, cid=%s]: %s", uid, cli.ClientId, err.Error()) logx.Warnf("ws send message failed - [uid=%d, cid=%s]: %s", uid, cli.ClientId, err.Error())
} }
@@ -165,10 +197,10 @@ func (manager *ClientManager) HeartbeatTimer() {
for { for {
<-ticker.C <-ticker.C
//发送心跳 //发送心跳
manager.UserClientsMap.Range(func(key, value any) bool { manager.UserClientMap.Range(func(key, value any) bool {
userId := key.(UserId) userId := key.(UserId)
clis := value.(UserClients) userClient := value.(*UserClient)
for _, cli := range clis { for _, cli := range userClient.AllClients() {
if cli == nil || cli.WsConn == nil { if cli == nil || cli.WsConn == nil {
continue continue
} }
@@ -208,24 +240,24 @@ func (manager *ClientManager) doDisconnect(client *Client) {
func (manager *ClientManager) addUserClient2Map(client *Client) { func (manager *ClientManager) addUserClient2Map(client *Client) {
// 先尝试加载现有的UserClients // 先尝试加载现有的UserClients
if value, ok := manager.UserClientsMap.Load(client.UserId); ok { if value, ok := manager.UserClientMap.Load(client.UserId); ok {
userClients := value.(UserClients) userClient := value.(*UserClient)
userClients.AddClient(client) userClient.AddClient(client)
} else { } else {
// 创建新的UserClients // 创建新的UserClients
userClients := make(UserClients) userClient := NewUserClient()
userClients.AddClient(client) userClient.AddClient(client)
manager.UserClientsMap.Store(client.UserId, userClients) manager.UserClientMap.Store(client.UserId, userClient)
} }
} }
func (manager *ClientManager) delUserClient4Map(client *Client) { func (manager *ClientManager) delUserClient4Map(client *Client) {
if value, ok := manager.UserClientsMap.Load(client.UserId); ok { if value, ok := manager.UserClientMap.Load(client.UserId); ok {
userClients := value.(UserClients) userClient := value.(*UserClient)
userClients.DeleteByCid(client.ClientId) userClient.DeleteByCid(client.ClientId)
// 如果用户所有客户端都关闭则移除manager中的UserClientsMap值 // 如果用户所有客户端都关闭则移除manager中的UserClientsMap值
if userClients.Count() == 0 { if userClient.Count() == 0 {
manager.UserClientsMap.Delete(client.UserId) manager.UserClientMap.Delete(client.UserId)
} }
} }
} }