feat: 数据库迁移至文件支持文件保存天数等

This commit is contained in:
meilin.huang
2024-10-22 20:39:44 +08:00
parent ea3c70a8a8
commit 44a1bd626e
19 changed files with 1043 additions and 1041 deletions

View File

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

View File

@@ -1,152 +1,137 @@
<template>
<div class="db-transfer-edit">
<el-dialog
:title="title"
v-model="dialogVisible"
:before-close="cancel"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
width="850px"
>
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基本信息" :name="basicTab">
<el-form-item>
<el-row class="w100" style="padding-bottom: 20px">
<el-col :span="18">
<el-form-item prop="taskName" label="任务名" required>
<el-input v-model.trim="form.taskName" placeholder="请输入任务名" auto-complete="off" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item prop="status" label="启用状态">
<el-switch
v-model="form.status"
inline-prompt
active-text="启用"
inactive-text="禁用"
:active-value="1"
:inactive-value="-1"
/>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-divider content-position="left">基本信息</el-divider>
<el-form-item>
<el-row class="w100" style="padding-bottom: 20px">
<el-col :span="8">
<el-form-item prop="cronAble" label="定时迁移" required>
<el-radio-group v-model="form.cronAble">
<el-radio label="是" :value="1" />
<el-radio label="否" :value="-1" />
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="cron" label="cron" :required="form.cronAble == 1">
<CrontabInput v-model="form.cron" />
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item prop="taskName" label="任务名" required>
<el-input v-model.trim="form.taskName" placeholder="请输入任务名" auto-complete="off" />
</el-form-item>
<el-form-item prop="srcDbId" label="源数据库" required>
<db-select-tree
placeholder="请选择源数据库"
v-model:db-id="form.srcDbId"
v-model:inst-name="form.srcInstName"
v-model:db-name="form.srcDbName"
v-model:tag-path="form.srcTagPath"
v-model:db-type="form.srcDbType"
@select-db="onSelectSrcDb"
/>
</el-form-item>
<el-form-item>
<el-row class="w100">
<el-col :span="12">
<el-form-item prop="status" label="启用状态">
<el-switch v-model="form.status" inline-prompt active-text="启用" inactive-text="禁用" :active-value="1" :inactive-value="-1" />
</el-form-item>
</el-col>
<el-form-item>
<el-row class="w100">
<el-col :span="13">
<el-form-item prop="mode" label="迁移方式" required>
<el-radio-group v-model="form.mode">
<el-radio label="迁移到数据库" :value="1" />
<el-radio label="迁移到文件(自动命名)" :value="2" />
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="11">
<el-form-item prop="strategy" label="迁移策略" required>
<el-radio-group v-model="form.strategy">
<el-radio label="全量" :value="1" />
<el-radio label="增量(暂不可用)" :value="2" disabled />
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-col :span="12">
<el-form-item prop="cronAble" label="定时迁移" required>
<el-radio-group v-model="form.cronAble">
<el-radio label="是" :value="1" />
<el-radio label="否" :value="-1" />
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item v-if="form.mode === 2" prop="targetFileDbType" label="文件数据库类型" :required="form.mode === 2">
<el-select style="width: 100%" v-model="form.targetFileDbType" placeholder="请选择数据库类型" clearable filterable>
<el-option
v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
:key="key"
:value="dbTypeAndDialect[0]"
:label="dbTypeAndDialect[1].getInfo().name"
>
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
{{ dbTypeAndDialect[1].getInfo().name }}
</el-option>
<template #prefix>
<SvgIcon :name="getDbDialect(form.targetFileDbType!).getInfo().icon" :size="20" />
</template>
</el-select>
</el-form-item>
<el-form-item prop="cron" label="cron" :required="form.cronAble == 1">
<CrontabInput v-model="form.cron" />
</el-form-item>
<el-form-item v-if="form.mode == 1" prop="targetDbId" label="目标数据库" :required="form.mode === 1">
<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>
<el-form-item prop="srcDbId" label="源数据库" class="w100" required>
<db-select-tree
placeholder="请选择数据库"
v-model:db-id="form.srcDbId"
v-model:inst-name="form.srcInstName"
v-model:db-name="form.srcDbName"
v-model:tag-path="form.srcTagPath"
v-model:db-type="form.srcDbType"
@select-db="onSelectSrcDb"
/>
</el-form-item>
<el-form-item prop="nameCase" label="转换表、字段名" required>
<el-radio-group v-model="form.nameCase">
<el-radio label="" :value="1" />
<el-radio label="大写" :value="2" />
<el-radio label="小写" :value="3" />
</el-radio-group>
</el-form-item>
<!--<el-form-item prop="deleteTable" label="创建前删除表" required>-->
<!-- <el-radio-group v-model="form.deleteTable">-->
<!-- <el-radio label="是" :value="1" />-->
<!-- <el-radio label="否" :value="2" />-->
<!-- </el-radio-group>-->
<!--</el-form-item>-->
</el-tab-pane>
<el-tab-pane label="数据库对象" :name="tableTab" :disabled="!baseFieldCompleted">
<el-form-item>
<el-input v-model="state.filterSrcTableText" style="width: 240px" placeholder="过滤表" />
</el-form-item>
<el-form-item>
<el-tree
ref="srcTreeRef"
style="width: 760px; max-height: 400px; overflow-y: auto"
default-expand-all
:expand-on-click-node="false"
:data="state.srcTableTree"
node-key="id"
show-checkbox
@check-change="handleSrcTableCheckChange"
:filter-node-method="filterSrcTableTreeNode"
/>
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-form-item prop="mode" label="迁移方式" required>
<el-radio-group v-model="form.mode">
<el-radio label="迁移到数据库" :value="1" />
<el-radio label="迁移到文件(自动命名)" :value="2" />
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.mode === 2">
<el-row class="w100">
<el-col :span="12">
<el-form-item prop="targetFileDbType" label="文件数据库类型" :required="form.mode === 2">
<el-select v-model="form.targetFileDbType" placeholder="数据库类型" clearable filterable>
<el-option
v-for="(dbTypeAndDialect, key) in getDbDialectMap()"
:key="key"
:value="dbTypeAndDialect[0]"
:label="dbTypeAndDialect[1].getInfo().name"
>
<SvgIcon :name="dbTypeAndDialect[1].getInfo().icon" :size="20" />
{{ dbTypeAndDialect[1].getInfo().name }}
</el-option>
<template #prefix>
<SvgIcon :name="getDbDialect(form.targetFileDbType!).getInfo().icon" :size="20" />
</template>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="文件保留天数">
<el-input-number v-model="form.fileSaveDays" :min="-1" :max="1000">
<template #suffix>
<span></span>
</template>
</el-input-number>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item prop="strategy" label="迁移策略" required>
<el-radio-group v-model="form.strategy">
<el-radio label="全量" :value="1" />
<el-radio label="增量(暂不可用)" :value="2" disabled />
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.mode == 1" prop="targetDbId" label="目标数据库" class="w100" :required="form.mode === 1">
<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>
<el-form-item prop="nameCase" label="转换表、字段名" required>
<el-radio-group v-model="form.nameCase">
<el-radio label="无" :value="1" />
<el-radio label="大写" :value="2" />
<el-radio label="小写" :value="3" />
</el-radio-group>
</el-form-item>
<el-divider content-position="left">数据库对象</el-divider>
<el-form-item>
<el-input v-model="state.filterSrcTableText" placeholder="过滤表" size="small" />
</el-form-item>
<el-form-item class="w100">
<el-tree
ref="srcTreeRef"
class="w100"
style="max-height: 200px; overflow-y: auto"
default-expand-all
:expand-on-click-node="false"
:data="state.srcTableTree"
node-key="id"
show-checkbox
@check-change="handleSrcTableCheckChange"
:filter-node-method="filterSrcTableTreeNode"
/>
</el-form-item>
</el-form>
<template #footer>
@@ -155,12 +140,12 @@
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { computed, 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 DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
@@ -168,6 +153,7 @@ import CrontabInput from '@/components/crontab/CrontabInput.vue';
import { getDbDialect, getDbDialectMap } from '@/views/ops/db/dialect';
import SvgIcon from '@/components/svgIcon/index.vue';
import _ from 'lodash';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
const props = defineProps({
data: {
@@ -223,9 +209,6 @@ const rules = {
const dbForm: any = ref(null);
const basicTab = 'basic';
const tableTab = 'table';
type FormData = {
id?: number;
taskName: string;
@@ -234,6 +217,7 @@ type FormData = {
cron: string;
mode: 1 | 2;
targetFileDbType?: string;
fileSaveDays?: number;
dbType: 1 | 2;
srcDbId?: number;
srcDbName?: string;
@@ -270,7 +254,6 @@ const srcTableListDisabled = ref(false);
const defaultKeys = ['tab-check', 'all', 'table-list'];
const state = reactive({
tabActiveName: 'basic',
form: basicFormData,
submitForm: {} as any,
srcTableFields: [] as string[],
@@ -293,20 +276,14 @@ const state = reactive({
],
});
const { tabActiveName, form, submitForm } = toRefs(state);
const { form, submitForm } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDbTransferTask.useApi(submitForm);
// 基础字段信息是否填写完整
const baseFieldCompleted = computed(() => {
return state.form.srcDbId && (state.form.targetDbId || state.form.targetFileDbType);
});
watch(dialogVisible, async (newValue: boolean) => {
if (!newValue) {
return;
}
state.tabActiveName = 'basic';
const propsData = props.data as any;
if (!propsData?.id) {
let d = {} as FormData;
@@ -447,10 +424,4 @@ const cancel = () => {
emit('cancel');
};
</script>
<style lang="scss">
.db-transfer-edit {
.el-select {
width: 100%;
}
}
</style>
<style lang="scss"></style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,33 @@
<template>
<div class="sync-task-edit">
<el-dialog
:title="title"
v-model="dialogVisible"
:before-close="cancel"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
width="850px"
>
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="45%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName" style="height: 450px">
<el-tabs v-model="tabActiveName">
<el-tab-pane label="基本信息" :name="basicTab">
<el-form-item>
<el-row>
<el-col :span="11">
<el-col :span="12">
<el-form-item prop="taskName" label="任务名" required>
<el-input v-model.trim="form.taskName" placeholder="请输入同步任务名" auto-complete="off" />
</el-form-item>
</el-col>
<el-col :span="11">
<el-col :span="12">
<el-form-item prop="taskCron" label="cron" required>
<CrontabInput v-model="form.taskCron" />
</el-form-item>
</el-col>
<el-col :span="2">
<el-form-item prop="status" label="状态" label-width="60" required>
<el-switch
v-model="form.status"
inline-prompt
active-text="启用"
inactive-text="禁用"
:active-value="1"
:inactive-value="-1"
/>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
<el-form-item prop="status" label="状态" label-width="60" required>
<el-switch v-model="form.status" inline-prompt active-text="启用" inactive-text="禁用" :active-value="1" :inactive-value="-1" />
</el-form-item>
<el-form-item prop="srcDbId" label="源数据库" required>
<db-select-tree
placeholder="请选择源数据库"
@@ -220,7 +207,18 @@
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-dialog>
</el-drawer>
<!-- <el-dialog
:title="title"
v-model="dialogVisible"
:before-close="cancel"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
width="850px"
>
</el-dialog> -->
</div>
</template>
@@ -233,6 +231,7 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { DbInst, registerDbCompletionItemProvider } from '@/views/ops/db/db';
import { compatibleDuplicateStrategy, DbType, DuplicateStrategy, getDbDialect } from '@/views/ops/db/dialect';
import CrontabInput from '@/components/crontab/CrontabInput.vue';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
const props = defineProps({
data: {

View File

@@ -10,46 +10,46 @@ import {
RowDefinition,
sqlColumnType,
} from './index';
import {DbInst} from '@/views/ops/db/db';
import {language as sqlLanguage} from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
import { DbInst } from '@/views/ops/db/db';
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
export {SqliteDialect};
export { SqliteDialect };
// 参考官方文档https://www.sqlite.org/datatype3.html
const SQLITE_TYPE_LIST: sqlColumnType[] = [
// INTEGER
{udtName: 'int', dataType: 'int', desc: '', space: '', range: ''},
{udtName: 'integer', dataType: 'integer', desc: '', space: '', range: ''},
{udtName: 'tinyint', dataType: 'tinyint', desc: '', space: '', range: ''},
{udtName: 'smallint', dataType: 'smallint', desc: '', space: '', range: ''},
{udtName: 'mediumint', dataType: 'mediumint', desc: '', space: '', range: ''},
{udtName: 'bigint', dataType: 'bigint', desc: '', space: '', range: ''},
{udtName: 'unsigned big int', dataType: 'unsigned big int', desc: '', space: '', range: ''},
{udtName: 'int2', dataType: 'int2', desc: '', space: '', range: ''},
{udtName: 'int8', dataType: 'int8', desc: '', space: '', range: ''},
{ udtName: 'int', dataType: 'int', desc: '', space: '', range: '' },
{ udtName: 'integer', dataType: 'integer', desc: '', space: '', range: '' },
{ udtName: 'tinyint', dataType: 'tinyint', desc: '', space: '', range: '' },
{ udtName: 'smallint', dataType: 'smallint', desc: '', space: '', range: '' },
{ udtName: 'mediumint', dataType: 'mediumint', desc: '', space: '', range: '' },
{ udtName: 'bigint', dataType: 'bigint', desc: '', space: '', range: '' },
{ udtName: 'unsigned big int', dataType: 'unsigned big int', desc: '', space: '', range: '' },
{ udtName: 'int2', dataType: 'int2', desc: '', space: '', range: '' },
{ udtName: 'int8', dataType: 'int8', desc: '', space: '', range: '' },
// TEXT
{udtName: 'character', dataType: 'character', desc: '', space: '', range: ''},
{udtName: 'varchar', dataType: 'varchar', desc: '', space: '', range: ''},
{udtName: 'varying character', dataType: 'varying character', desc: '', space: '', range: ''},
{udtName: 'nchar', dataType: 'nchar', desc: '', space: '', range: ''},
{udtName: 'native character', dataType: 'native character', desc: '', space: '', range: ''},
{udtName: 'nvarchar', dataType: 'nvarchar', desc: '', space: '', range: ''},
{udtName: 'text', dataType: 'text', desc: '', space: '', range: ''},
{udtName: 'clob', dataType: 'clob', desc: '', space: '', range: ''},
{ udtName: 'character', dataType: 'character', desc: '', space: '', range: '' },
{ udtName: 'varchar', dataType: 'varchar', desc: '', space: '', range: '' },
{ udtName: 'varying character', dataType: 'varying character', desc: '', space: '', range: '' },
{ udtName: 'nchar', dataType: 'nchar', desc: '', space: '', range: '' },
{ udtName: 'native character', dataType: 'native character', desc: '', space: '', range: '' },
{ udtName: 'nvarchar', dataType: 'nvarchar', desc: '', space: '', range: '' },
{ udtName: 'text', dataType: 'text', desc: '', space: '', range: '' },
{ udtName: 'clob', dataType: 'clob', desc: '', space: '', range: '' },
// blob
{udtName: 'blob', dataType: 'blob', desc: '', space: '', range: ''},
{udtName: 'no datatype specified', dataType: 'no datatype specified', desc: '', space: '', range: ''},
{ udtName: 'blob', dataType: 'blob', desc: '', space: '', range: '' },
{ udtName: 'no datatype specified', dataType: 'no datatype specified', desc: '', space: '', range: '' },
// REAL
{udtName: 'real', dataType: 'real', desc: '', space: '', range: ''},
{udtName: 'double', dataType: 'double', desc: '', space: '', range: ''},
{udtName: 'double precision', dataType: 'double precision', desc: '', space: '', range: ''},
{udtName: 'float', dataType: 'float', desc: '', space: '', range: ''},
{ udtName: 'real', dataType: 'real', desc: '', space: '', range: '' },
{ udtName: 'double', dataType: 'double', desc: '', space: '', range: '' },
{ udtName: 'double precision', dataType: 'double precision', desc: '', space: '', range: '' },
{ udtName: 'float', dataType: 'float', desc: '', space: '', range: '' },
// NUMERIC
{udtName: 'numeric', dataType: 'numeric', desc: '', space: '', range: ''},
{udtName: 'decimal', dataType: 'decimal', desc: '', space: '', range: ''},
{udtName: 'boolean', dataType: 'boolean', desc: '', space: '', range: ''},
{udtName: 'date', dataType: 'date', desc: '', space: '', range: ''},
{udtName: 'datetime', dataType: 'datetime', desc: '', space: '', range: ''},
{ udtName: 'numeric', dataType: 'numeric', desc: '', space: '', range: '' },
{ udtName: 'decimal', dataType: 'decimal', desc: '', space: '', range: '' },
{ udtName: 'boolean', dataType: 'boolean', desc: '', space: '', range: '' },
{ udtName: 'date', dataType: 'date', desc: '', space: '', range: '' },
{ udtName: 'datetime', dataType: 'datetime', desc: '', space: '', range: '' },
];
const addCustomKeywords = ['PRAGMA', 'database_list', 'sqlite_master'];
@@ -57,61 +57,61 @@ const addCustomKeywords = ['PRAGMA', 'database_list', 'sqlite_master'];
// 参考官方文档https://www.sqlite.org/lang_corefunc.html
const functions: EditorCompletionItem[] = [
// 字符函数
{label: 'abs', insertText: 'abs(X)', description: '返回给定数值的绝对值'},
{label: 'changes', insertText: 'changes()', description: '返回最近增删改影响的行数'},
{label: 'coalesce', insertText: 'coalesce(X,Y,...)', description: '返回第一个不为空的值'},
{label: 'hex', insertText: 'hex(X)', description: '返回给定字符的hex值'},
{label: 'ifnull', insertText: 'ifnull(X,Y)', description: '返回第一个不为空的值'},
{label: 'iif', insertText: 'iif(X,Y,Z)', description: '如果x为真则返回y否则返回z'},
{label: 'instr', insertText: 'instr(X,Y)', description: '返回字符y在x的第n个位置'},
{label: 'length', insertText: 'length(X)', description: '返回给定字符的长度'},
{label: 'load_extension', insertText: 'load_extension(X[,Y])', description: '加载扩展块'},
{label: 'lower', insertText: 'lower(X)', description: '返回小写字符'},
{label: 'ltrim', insertText: 'ltrim(X[,Y])', description: '左trim'},
{label: 'nullif', insertText: 'nullif(X,Y)', description: '比较两值相等则返回null否则返回第一个值'},
{label: 'printf', insertText: "printf('%s',...)", description: '字符串格式化拼接,如%s %d'},
{label: 'quote', insertText: 'quote(X)', description: '把字符串用引号包起来'},
{label: 'random', insertText: 'random()', description: '生成随机数'},
{label: 'randomblob', insertText: 'randomblob(N)', description: '生成一个包含N个随机字节的BLOB'},
{label: 'replace', insertText: 'replace(X,Y,Z)', description: '替换字符串'},
{label: 'round', insertText: 'round(X[,Y])', description: '将数值四舍五入到指定的小数位数'},
{label: 'rtrim', insertText: 'rtrim(X[,Y])', description: '右trim'},
{label: 'sign', insertText: 'sign(X)', description: '返回数字符号 1正 -1负 0零 null'},
{label: 'soundex', insertText: 'soundex(X)', description: '返回字符串X的soundex编码字符串'},
{label: 'sqlite_compileoption_get', insertText: 'sqlite_compileoption_get(N)', description: '获取指定编译选项的值'},
{ label: 'abs', insertText: 'abs(X)', description: '返回给定数值的绝对值' },
{ label: 'changes', insertText: 'changes()', description: '返回最近增删改影响的行数' },
{ label: 'coalesce', insertText: 'coalesce(X,Y,...)', description: '返回第一个不为空的值' },
{ label: 'hex', insertText: 'hex(X)', description: '返回给定字符的hex值' },
{ label: 'ifnull', insertText: 'ifnull(X,Y)', description: '返回第一个不为空的值' },
{ label: 'iif', insertText: 'iif(X,Y,Z)', description: '如果x为真则返回y否则返回z' },
{ label: 'instr', insertText: 'instr(X,Y)', description: '返回字符y在x的第n个位置' },
{ label: 'length', insertText: 'length(X)', description: '返回给定字符的长度' },
{ label: 'load_extension', insertText: 'load_extension(X[,Y])', description: '加载扩展块' },
{ label: 'lower', insertText: 'lower(X)', description: '返回小写字符' },
{ label: 'ltrim', insertText: 'ltrim(X[,Y])', description: '左trim' },
{ label: 'nullif', insertText: 'nullif(X,Y)', description: '比较两值相等则返回null否则返回第一个值' },
{ label: 'printf', insertText: "printf('%s',...)", description: '字符串格式化拼接,如%s %d' },
{ label: 'quote', insertText: 'quote(X)', description: '把字符串用引号包起来' },
{ label: 'random', insertText: 'random()', description: '生成随机数' },
{ label: 'randomblob', insertText: 'randomblob(N)', description: '生成一个包含N个随机字节的BLOB' },
{ label: 'replace', insertText: 'replace(X,Y,Z)', description: '替换字符串' },
{ label: 'round', insertText: 'round(X[,Y])', description: '将数值四舍五入到指定的小数位数' },
{ label: 'rtrim', insertText: 'rtrim(X[,Y])', description: '右trim' },
{ label: 'sign', insertText: 'sign(X)', description: '返回数字符号 1正 -1负 0零 null' },
{ label: 'soundex', insertText: 'soundex(X)', description: '返回字符串X的soundex编码字符串' },
{ label: 'sqlite_compileoption_get', insertText: 'sqlite_compileoption_get(N)', description: '获取指定编译选项的值' },
{
label: 'sqlite_compileoption_used',
insertText: 'sqlite_compileoption_used(X)',
description: '检查SQLite编译时是否使用了指定的编译选项'
description: '检查SQLite编译时是否使用了指定的编译选项',
},
{label: 'sqlite_source_id', insertText: 'sqlite_source_id()', description: '获取sqlite源代码标识符'},
{label: 'sqlite_version', insertText: 'sqlite_version()', description: '获取sqlite版本'},
{label: 'substr', insertText: 'substr(X,Y[,Z])', description: '截取字符串'},
{label: 'substring', insertText: 'substring(X,Y[,Z])', description: '截取字符串'},
{label: 'trim', insertText: 'trim(X[,Y])', description: '去除给定字符串前后的字符,默认空格'},
{label: 'typeof', insertText: 'typeof(X)', description: '返回X的基本类型null,integer,real,text,blob'},
{label: 'unicode', insertText: 'unicode(X)', description: '返回与字符串X的第一个字符相对应的数字unicode代码点'},
{label: 'unlikely', insertText: 'unlikely(X)', description: '返回大写字符'},
{label: 'upper', insertText: 'upper(X)', description: '返回由0x00的N个字节组成的BLOB'},
{label: 'zeroblob', insertText: 'zeroblob(N)', description: '返回分组中的平均值'},
{label: 'avg', insertText: 'avg(X)', description: '返回总条数'},
{label: 'count', insertText: 'count(*)', description: '返回分组中用给定非空字符串连接的值'},
{label: 'group_concat', insertText: 'group_concat(X[,Y])', description: '返回分组中最大值'},
{label: 'max', insertText: 'max(X)', description: '返回分组中最小值'},
{label: 'min', insertText: 'min(X)', description: '返回分组中非空值的总和。'},
{label: 'sum', insertText: 'sum(X)', description: '返回分组中非空值的总和。'},
{label: 'total', insertText: 'total(X)', description: '返回YYYY-MM-DD格式的字符串'},
{label: 'date', insertText: 'date(time-value[, modifier, ...])', description: '返回HH:MM:SS格式的字符串'},
{ label: 'sqlite_source_id', insertText: 'sqlite_source_id()', description: '获取sqlite源代码标识符' },
{ label: 'sqlite_version', insertText: 'sqlite_version()', description: '获取sqlite版本' },
{ label: 'substr', insertText: 'substr(X,Y[,Z])', description: '截取字符串' },
{ label: 'substring', insertText: 'substring(X,Y[,Z])', description: '截取字符串' },
{ label: 'trim', insertText: 'trim(X[,Y])', description: '去除给定字符串前后的字符,默认空格' },
{ label: 'typeof', insertText: 'typeof(X)', description: '返回X的基本类型null,integer,real,text,blob' },
{ label: 'unicode', insertText: 'unicode(X)', description: '返回与字符串X的第一个字符相对应的数字unicode代码点' },
{ label: 'unlikely', insertText: 'unlikely(X)', description: '返回大写字符' },
{ label: 'upper', insertText: 'upper(X)', description: '返回由0x00的N个字节组成的BLOB' },
{ label: 'zeroblob', insertText: 'zeroblob(N)', description: '返回分组中的平均值' },
{ label: 'avg', insertText: 'avg(X)', description: '返回总条数' },
{ label: 'count', insertText: 'count(*)', description: '返回分组中用给定非空字符串连接的值' },
{ label: 'group_concat', insertText: 'group_concat(X[,Y])', description: '返回分组中最大值' },
{ label: 'max', insertText: 'max(X)', description: '返回分组中最小值' },
{ label: 'min', insertText: 'min(X)', description: '返回分组中非空值的总和。' },
{ label: 'sum', insertText: 'sum(X)', description: '返回分组中非空值的总和。' },
{ label: 'total', insertText: 'total(X)', description: '返回YYYY-MM-DD格式的字符串' },
{ label: 'date', insertText: 'date(time-value[, modifier, ...])', description: '返回HH:MM:SS格式的字符串' },
{
label: 'time',
insertText: 'time(time-value[, modifier, ...])',
description: '将日期和时间字符串转换为特定的日期和时间格式'
description: '将日期和时间字符串转换为特定的日期和时间格式',
},
{label: 'datetime', insertText: 'datetime(time-value[, modifier, ...])', description: '计算日期和时间的儒略日数'},
{ label: 'datetime', insertText: 'datetime(time-value[, modifier, ...])', description: '计算日期和时间的儒略日数' },
{
label: 'julianday',
insertText: 'julianday(time-value[, modifier, ...])',
description: '将日期和时间格式化为指定的字符串'
description: '将日期和时间格式化为指定的字符串',
},
];
@@ -123,17 +123,17 @@ class SqliteDialect implements DbDialect {
return sqliteDialectInfo;
}
let {keywords, operators, builtinVariables} = sqlLanguage;
let { keywords, operators, builtinVariables } = sqlLanguage;
let editorCompletions: EditorCompletion = {
keywords: keywords
.filter((a: string) => addCustomKeywords.indexOf(a) === -1)
.map((a: string): EditorCompletionItem => ({label: a, description: 'keyword'}))
.concat(commonCustomKeywords.map((a): EditorCompletionItem => ({label: a, description: 'keyword'})))
.concat(addCustomKeywords.map((a): EditorCompletionItem => ({label: a, description: 'keyword'}))),
operators: operators.map((a: string): EditorCompletionItem => ({label: a, description: 'operator'})),
.map((a: string): EditorCompletionItem => ({ label: a, description: 'keyword' }))
.concat(commonCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' })))
.concat(addCustomKeywords.map((a): EditorCompletionItem => ({ label: a, description: 'keyword' }))),
operators: operators.map((a: string): EditorCompletionItem => ({ label: a, description: 'operator' })),
functions,
variables: builtinVariables.map((a: string): EditorCompletionItem => ({label: a, description: 'var'})),
variables: builtinVariables.map((a: string): EditorCompletionItem => ({ label: a, description: 'var' })),
};
sqliteDialectInfo = {
@@ -149,10 +149,7 @@ class SqliteDialect implements DbDialect {
getDefaultSelectSql(db: string, table: string, condition: string, orderBy: string, pageNum: number, limit: number) {
return `SELECT *
FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
pageNum,
limit
)};`;
FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(pageNum, limit)};`;
}
getPageSql(pageNum: number, limit: number) {
@@ -170,7 +167,7 @@ class SqliteDialect implements DbDialect {
notNull: true,
pri: true,
auto_increment: true,
remark: '主键ID'
remark: '主键ID',
},
{
name: 'creator_id',
@@ -181,7 +178,7 @@ class SqliteDialect implements DbDialect {
notNull: true,
pri: false,
auto_increment: false,
remark: '创建人id'
remark: '创建人id',
},
{
name: 'creator',
@@ -214,7 +211,7 @@ class SqliteDialect implements DbDialect {
notNull: true,
pri: false,
auto_increment: false,
remark: '修改人id'
remark: '修改人id',
},
{
name: 'updator',
@@ -225,7 +222,7 @@ class SqliteDialect implements DbDialect {
notNull: true,
pri: false,
auto_increment: false,
remark: '修改姓名'
remark: '修改姓名',
},
{
name: 'update_time',
@@ -299,11 +296,15 @@ class SqliteDialect implements DbDialect {
return sql.join(';');
}
getModifyColumnSql(tableData: any, tableName: string, changeData: {
del: RowDefinition[];
add: RowDefinition[];
upd: RowDefinition[]
}): string {
getModifyColumnSql(
tableData: any,
tableName: string,
changeData: {
del: RowDefinition[];
add: RowDefinition[];
upd: RowDefinition[];
}
): string {
// sqlite修改表结构需要先删除再创建
// 1.删除旧表索引 DROP INDEX "main"."aa";
@@ -341,17 +342,15 @@ class SqliteDialect implements DbDialect {
// 生成sql
sql.push(
`INSERT INTO ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} (${insertFields.join(',')})
SELECT ${queryFields.join(
','
)}
SELECT ${queryFields.join(',')}
FROM ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(oldTableName)}`
);
// 5.创建索引
tableData.indexs.res.forEach((a: any) => {
a.indexName &&
sql.push(
`CREATE
sql.push(
`CREATE
${a.unique ? 'UNIQUE' : ''} INDEX
${this.quoteIdentifier(tableData.db)}
.
@@ -361,7 +360,7 @@ class SqliteDialect implements DbDialect {
(
${a.columnNames.join(',')}
)`
);
);
});
return sql.join(';') + ';';

View File

@@ -33,6 +33,7 @@
:before-close="cancelSaveTeam"
:destroy-on-close="true"
:close-on-click-modal="false"
size="40%"
>
<template #header>
<DrawerHeader :header="addTeamDialog.form.id ? '编辑团队' : '添加团队'" :back="cancelSaveTeam" />

View File

@@ -8,6 +8,7 @@ type DbTransferTaskForm struct {
Cron string `json:"cron"` // 定时任务cron表达式
Mode int `binding:"required" json:"mode"` // 数据迁移方式1、迁移到数据库 2、迁移到文件
TargetFileDbType string `json:"targetFileDbType"` // 目标文件数据库类型
FileSaveDays int `json:"fileSaveDays"` // 文件保存天数
Status int `json:"status" form:"status"` // 启用状态 1启用 -1禁用
CheckedKeys string `binding:"required" json:"checkedKeys"` // 选中需要迁移的表

View File

@@ -19,6 +19,7 @@ type DbTransferTaskListVO struct {
Cron string `json:"cron"` // 定时任务cron表达式
Mode int `json:"mode"` // 数据迁移方式1、迁移到数据库 2、迁移到文件
TargetFileDbType string `json:"targetFileDbType"` // 目标文件数据库类型
FileSaveDays int `json:"fileSaveDays"` // 文件保存天数
CheckedKeys string `json:"checkedKeys"` // 选中需要迁移的表
DeleteTable int `json:"deleteTable"` // 创建表前是否删除表

View File

@@ -31,8 +31,10 @@ func Init() {
//if err := GetDbBinlogApp().Init(); err != nil {
// panic(fmt.Sprintf("初始化 DbBinlogApp 失败: %v", err))
//}
GetDataSyncTaskApp().InitCronJob()
GetDbTransferTaskApp().InitCronJob()
GetDbTransferTaskApp().TimerDeleteTransferFile()
InitDbFlowHandler()
})()
}
@@ -56,6 +58,7 @@ func GetDbBinlogApp() *DbBinlogApp {
func GetDataSyncTaskApp() DataSyncTask {
return ioc.Get[DataSyncTask]("DbDataSyncTaskApp")
}
func GetDbTransferTaskApp() DbTransferTask {
return ioc.Get[DbTransferTask]("DbTransferTaskApp")
}

View File

@@ -53,6 +53,9 @@ type DbTransferTask interface {
IsRunning(taskId uint64) bool
Stop(ctx context.Context, taskId uint64) error
// TimerDeleteTransferFile 定时删除迁移文件
TimerDeleteTransferFile()
}
type dbTransferAppImpl struct {
@@ -80,6 +83,9 @@ func (app *dbTransferAppImpl) Save(ctx context.Context, taskEntity *entity.DbTra
} else {
err = app.UpdateById(ctx, taskEntity)
}
if err != nil {
return err
}
// 视情况添加或删除任务
task, err := app.GetById(taskEntity.Id)
if err != nil {
@@ -549,6 +555,29 @@ func (app *dbTransferAppImpl) transferIndex(_ context.Context, tableInfo dbi.Tab
return targetDialect.CreateIndex(tableInfo, indexs)
}
func (d *dbTransferAppImpl) TimerDeleteTransferFile() {
logx.Debug("开始定时删除迁移文件...")
scheduler.AddFun("@every 100m", func() {
dts, err := d.ListByCond(model.NewCond().Eq("mode", entity.DbTransferTaskModeFile).Ge("file_save_days", 1))
if err != nil {
logx.Errorf("定时获取数据库迁移至文件任务失败: %s", err.Error())
return
}
for _, dt := range dts {
needDelFiles, err := d.transferFileApp.ListByCond(model.NewCond().Eq("task_id", dt.Id).Le("create_time", time.Now().AddDate(0, 0, -dt.FileSaveDays)))
if err != nil {
logx.Errorf("定时获取迁移文件失败: %s", err.Error())
continue
}
for _, nf := range needDelFiles {
if err := d.transferFileApp.Delete(context.Background(), nf.Id); err != nil {
logx.Errorf("定时删除迁移文件失败: %s", err.Error())
}
}
}
})
}
// MarkRunning 标记任务执行中
func (app *dbTransferAppImpl) MarkRunning(taskId uint64) {
cache.Set(fmt.Sprintf("mayfly:db:transfer:%d", taskId), 1, -1)

View File

@@ -54,7 +54,7 @@ func (app *dbTransferFileAppImpl) Delete(ctx context.Context, id ...uint64) erro
// 删除对应的文件
for _, file := range arr {
_ = app.fileApp.Remove(ctx, file.FileKey)
app.fileApp.Remove(ctx, file.FileKey)
}
// 删除数据

View File

@@ -89,7 +89,7 @@ func (sd *SqliteMetaData) GetTables(tableNames ...string) ([]dbi.Table, error) {
func (sd *SqliteMetaData) getDataTypes(dataType string) (string, string, string) {
matches := dataTypeRegexp.FindStringSubmatch(dataType)
if len(matches) == 0 {
return "", "", ""
return dataType, "", ""
}
return matches[1], matches[2], matches[3]
}
@@ -133,7 +133,7 @@ func (sd *SqliteMetaData) GetColumns(tableNames ...string) ([]dbi.Column, error)
} else {
column.CharMaxLength = cast.ToInt(length)
}
column.DataType = dbi.ColumnDataType(dataType)
column.DataType = dbi.ColumnDataType(strings.ToLower(dataType))
columnHelper.FixColumn(&column)
columns = append(columns, column)

View File

@@ -15,6 +15,7 @@ type DbTransferTask struct {
Cron string `orm:"column(cron)" json:"cron"` // 定时任务cron表达式
Mode int8 `orm:"column(mode)" json:"mode"` // 数据迁移方式1、迁移到数据库 2、迁移到文件
TargetFileDbType string `orm:"column(target_file_db_type)" json:"targetFileDbType"` // 目标文件数据库类型
FileSaveDays int `json:"fileSaveDays"` // 文件保存天数
TaskKey string `orm:"column(key)" json:"taskKey"` // 定时任务唯一uuid key
CheckedKeys string `orm:"column(checked_keys)" json:"checkedKeys"` // 选中需要迁移的表

View File

@@ -1,7 +1,13 @@
package application
import (
"context"
"mayfly-go/internal/event"
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/pkg/eventbus"
"mayfly-go/pkg/global"
"mayfly-go/pkg/ioc"
"sync"
)
func InitIoc() {
@@ -13,6 +19,26 @@ func InitIoc() {
ioc.Register(new(machineCmdConfAppImpl), ioc.WithComponentName("MachineCmdConfApp"))
}
func Init() {
sync.OnceFunc(func() {
GetMachineCronJobApp().InitCronJob()
GetMachineApp().TimerUpdateStats()
GetMachineTermOpApp().TimerDeleteTermOp()
global.EventBus.Subscribe(event.EventTopicDeleteMachine, "machineFile", func(ctx context.Context, event *eventbus.Event) error {
me := event.Val.(*entity.Machine)
return GetMachineFileApp().DeleteByCond(ctx, &entity.MachineFile{MachineId: me.Id})
})
global.EventBus.Subscribe(event.EventTopicDeleteMachine, "machineScript", func(ctx context.Context, event *eventbus.Event) error {
me := event.Val.(*entity.Machine)
return GetMachineScriptApp().DeleteByCond(ctx, &entity.MachineScript{MachineId: me.Id})
})
})()
}
func GetMachineApp() Machine {
return ioc.Get[Machine]("MachineApp")
}

View File

@@ -1,15 +1,10 @@
package init
import (
"context"
"mayfly-go/initialize"
"mayfly-go/internal/event"
"mayfly-go/internal/machine/application"
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/infrastructure/persistence"
"mayfly-go/internal/machine/router"
"mayfly-go/pkg/eventbus"
"mayfly-go/pkg/global"
)
func init() {
@@ -18,23 +13,5 @@ func init() {
application.InitIoc()
})
initialize.AddInitRouterFunc(router.Init)
initialize.AddInitFunc(Init)
}
func Init() {
application.GetMachineCronJobApp().InitCronJob()
application.GetMachineApp().TimerUpdateStats()
application.GetMachineTermOpApp().TimerDeleteTermOp()
global.EventBus.Subscribe(event.EventTopicDeleteMachine, "machineFile", func(ctx context.Context, event *eventbus.Event) error {
me := event.Val.(*entity.Machine)
return application.GetMachineFileApp().DeleteByCond(ctx, &entity.MachineFile{MachineId: me.Id})
})
global.EventBus.Subscribe(event.EventTopicDeleteMachine, "machineScript", func(ctx context.Context, event *eventbus.Event) error {
me := event.Val.(*entity.Machine)
return application.GetMachineScriptApp().DeleteByCond(ctx, &entity.MachineScript{MachineId: me.Id})
})
initialize.AddInitFunc(application.Init)
}

View File

@@ -4,7 +4,7 @@ import "fmt"
const (
AppName = "mayfly-go"
Version = "v1.8.9"
Version = "v1.9.0"
)
func GetAppInfo() string {

View File

@@ -69,6 +69,7 @@ CREATE TABLE `t_db_transfer_task` (
`task_key` varchar(100) NULL comment '定时任务唯一uuid key',
`mode` TINYINT(3) NOT NULL DEFAULT 1 comment '数据迁移方式1、迁移到数据库 2、迁移到文件',
`target_file_db_type` varchar(200) NULL comment '目标文件语言类型类型枚举同target_db_type',
`file_save_days` int NULL comment '文件保存天数',
`status` tinyint(3) NOT NULL DEFAULT '1' comment '启用状态 1启用 -1禁用',
`upd_field_src` varchar(100) DEFAULT NULL COMMENT '更新值来源字段,默认同更新字段,如果查询结果指定了字段别名且与原更新字段不一致,则取这个字段值为当前更新值',
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',

View File

@@ -11,7 +11,7 @@ INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `we
-- 新增数据库迁移相关的系统配置
DELETE FROM `t_sys_config` WHERE `key` = 'DbBackupRestore';
UPDATE `t_sys_config` SET param = '[{"name":"uploadMaxFileSize","model":"uploadMaxFileSize","placeholder":"允许上传的最大文件大小(1MB、2GB等)"},{"model":"termOpSaveDays","name":"终端记录保存时间","placeholder":"终端记录保存时间(单位天)"},{"model":"guacdHost","name":"guacd服务ip","placeholder":"guacd服务ip默认 127.0.0.1","required":false},{"name":"guacd服务端口","model":"guacdPort","placeholder":"guacd服务端口默认 4822","required":false},{"model":"guacdFilePath","name":"guacd服务文件存储位置","placeholder":"guacd服务文件存储位置用于挂载RDP文件夹"}]' WHERE `key`='MachineConfig';
UPDATE `t_sys_config` SET params = '[{"name":"uploadMaxFileSize","model":"uploadMaxFileSize","placeholder":"允许上传的最大文件大小(1MB、2GB等)"},{"model":"termOpSaveDays","name":"终端记录保存时间","placeholder":"终端记录保存时间(单位天)"},{"model":"guacdHost","name":"guacd服务ip","placeholder":"guacd服务ip默认 127.0.0.1","required":false},{"name":"guacd服务端口","model":"guacdPort","placeholder":"guacd服务端口默认 4822","required":false},{"model":"guacdFilePath","name":"guacd服务文件存储位置","placeholder":"guacd服务文件存储位置用于挂载RDP文件夹"}]' WHERE `key`='MachineConfig';
INSERT INTO `t_sys_config` (`name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES('文件配置', 'FileConfig', '[{"model":"basePath","name":"基础路径","placeholder":"默认为可执行文件对应目录下./file"}]', '{"basePath":"./file"}', '系统文件相关配置', 'admin,', '2024-10-20 22:30:01', 1, 'admin', '2024-10-21 13:51:17', 1, 'admin', 0, NULL);
-- 数据库迁移到文件
@@ -22,6 +22,7 @@ ALTER TABLE `t_db_transfer_task`
ADD COLUMN `task_key` varchar(100) NULL comment '定时任务唯一uuid key',
ADD COLUMN `mode` TINYINT(3) NOT NULL DEFAULT 1 comment '数据迁移方式1、迁移到数据库 2、迁移到文件',
ADD COLUMN `target_file_db_type` varchar(200) NULL comment '目标文件语言类型类型枚举同target_db_type',
ADD COLUMN `file_save_days` int NULL comment '文件保存天数',
ADD COLUMN `status` tinyint(3) NOT NULL DEFAULT '1' comment '启用状态 1启用 -1禁用';
UPDATE `t_db_transfer_task` SET mode = 1 WHERE 1=1;