mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30:25 +08:00
!124 一些更新和bug
* fix: 代码合并 * feat:支持数据库版本兼容,目前兼容了oracle11g部分特性 * fix: 修改数据同步bug,数据sql里指定修改字段别,导致未正确记录修改字段值 * feat: 数据库迁移支持定时迁移和迁移到sql文件
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import Config from './config';
|
||||
import { ElNotification } from 'element-plus';
|
||||
import {ElNotification} from 'element-plus';
|
||||
import SocketBuilder from './SocketBuilder';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
import {getToken} from '@/common/utils/storage';
|
||||
|
||||
import { joinClientParams } from './request';
|
||||
import {joinClientParams} from './request';
|
||||
|
||||
class SysSocket {
|
||||
/**
|
||||
@@ -19,10 +19,11 @@ class SysSocket {
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
messageTypes = {
|
||||
messageTypes: any = {
|
||||
0: 'error',
|
||||
1: 'success',
|
||||
2: 'info',
|
||||
22: 'info',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -57,12 +58,20 @@ class SysSocket {
|
||||
}
|
||||
|
||||
const type = this.getMsgType(message.type);
|
||||
let msg = message.msg
|
||||
let duration = 0
|
||||
if (message.type == 22) {
|
||||
let obj = JSON.parse(msg);
|
||||
msg = `文件:${obj['title']} 执行成功: ${obj['executedStatements']} 条`
|
||||
duration = 2000
|
||||
}
|
||||
ElNotification({
|
||||
duration: 0,
|
||||
duration: duration,
|
||||
title: message.title,
|
||||
message: message.msg,
|
||||
message: msg,
|
||||
type: type,
|
||||
});
|
||||
console.log(message)
|
||||
})
|
||||
.open((event: any) => console.log(event))
|
||||
.close(() => {
|
||||
|
||||
@@ -14,11 +14,11 @@ export function hasPerm(code: string) {
|
||||
|
||||
/**
|
||||
* 判断用户是否拥有权限对象里对应的code
|
||||
* @param perms { save: "xxx:save"}
|
||||
* @returns {"xxx:save": true} key->permission code
|
||||
* @param permCodes
|
||||
*/
|
||||
export function hasPerms(permCodes: any[]) {
|
||||
const res = {};
|
||||
const res = {} as { [key: string]: boolean };
|
||||
for (let permCode of permCodes) {
|
||||
if (hasPerm(permCode)) {
|
||||
res[permCode] = true;
|
||||
|
||||
@@ -156,8 +156,8 @@
|
||||
<el-row v-if="props.pageable" class="mt20" type="flex" justify="end">
|
||||
<el-pagination
|
||||
:small="props.size == 'small'"
|
||||
@current-change="handlePageNumChange"
|
||||
@size-change="handlePageSizeChange"
|
||||
@current-change="pageNumChange"
|
||||
@size-change="pageSizeChange"
|
||||
style="text-align: right"
|
||||
layout="prev, pager, next, total, sizes"
|
||||
:total="total"
|
||||
@@ -185,7 +185,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { usePageTable } from '@/hooks/usePageTable';
|
||||
import { ElTable } from 'element-plus';
|
||||
|
||||
const emit = defineEmits(['update:selectionData', 'pageChange']);
|
||||
const emit = defineEmits(['update:selectionData', 'pageSizeChange', 'pageNumChange']);
|
||||
|
||||
export interface PageTableProps {
|
||||
size?: string;
|
||||
@@ -257,6 +257,15 @@ const changeSimpleFormItem = (searchItem: SearchItem) => {
|
||||
nowSearchItem.value = searchItem;
|
||||
};
|
||||
|
||||
const pageSizeChange = (val: number) => {
|
||||
emit('pageSizeChange', val);
|
||||
handlePageSizeChange(val);
|
||||
};
|
||||
const pageNumChange = (val: number) => {
|
||||
emit('pageNumChange', val);
|
||||
handlePageNumChange(val);
|
||||
};
|
||||
|
||||
let { tableData, total, loading, search, reset, getTableData, handlePageNumChange, handlePageSizeChange } = usePageTable(
|
||||
props.pageable,
|
||||
props.pageApi,
|
||||
@@ -353,6 +362,7 @@ defineExpose({
|
||||
tableRef: tableRef,
|
||||
search: getTableData,
|
||||
getData,
|
||||
total,
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -109,7 +109,7 @@ const state = reactive({
|
||||
mouse: null as any,
|
||||
touchpad: null as any,
|
||||
errorMessage: '',
|
||||
arguments: {},
|
||||
arguments: {} as any,
|
||||
status: TerminalStatus.NoConnected,
|
||||
size: {
|
||||
height: 710,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -48,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch, toRefs, nextTick } from 'vue';
|
||||
import { nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { NodeType, TagTreeNode } from './tag';
|
||||
import TagInfo from './TagInfo.vue';
|
||||
import { Contextmenu } from '@/components/contextmenu';
|
||||
@@ -147,10 +147,10 @@ const loadNode = async (node: any, resolve: (data: any) => void, reject: () => v
|
||||
return resolve(nodes);
|
||||
};
|
||||
|
||||
const treeNodeClick = (data: any) => {
|
||||
const treeNodeClick = async (data: any) => {
|
||||
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
||||
emit('nodeClick', data);
|
||||
data.type.nodeClickFunc(data);
|
||||
await data.type.nodeClickFunc(data);
|
||||
}
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { OptionsApi, SearchItem } from '@/components/SearchForm';
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { tagApi } from '../tag/api';
|
||||
import {OptionsApi, SearchItem} from '@/components/SearchForm';
|
||||
import {ContextmenuItem} from '@/components/contextmenu';
|
||||
import {TagResourceTypeEnum} from '@/common/commonEnum';
|
||||
import {tagApi} from '../tag/api';
|
||||
|
||||
export class TagTreeNode {
|
||||
/**
|
||||
@@ -162,7 +162,7 @@ export class NodeType {
|
||||
*/
|
||||
export function getTagPathSearchItem(resourceType: number) {
|
||||
return SearchItem.select('tagPath', '标签').withOptionsApi(
|
||||
OptionsApi.new(tagApi.getResourceTagPaths, { resourceType }).withConvertFn((res: any) => {
|
||||
OptionsApi.new(tagApi.getResourceTagPaths, {resourceType}).withConvertFn((res: any) => {
|
||||
return res.map((x: any) => {
|
||||
return {
|
||||
label: x,
|
||||
@@ -180,6 +180,7 @@ export function getTagPathSearchItem(resourceType: number) {
|
||||
*/
|
||||
export function getTagTypeCodeByPath(codePath: string) {
|
||||
const result: any = {};
|
||||
if (!codePath) return result
|
||||
const parts = codePath.split('/'); // 切分字符串并保留数字和对应的值部分
|
||||
|
||||
for (let part of parts) {
|
||||
@@ -207,6 +208,7 @@ export function getTagTypeCodeByPath(codePath: string) {
|
||||
* @returns
|
||||
*/
|
||||
export async function getAllTagInfoByCodePaths(codePaths: string[]) {
|
||||
if (!codePaths) return
|
||||
const allTypeAndCode: any = {};
|
||||
|
||||
for (let codePath of codePaths) {
|
||||
@@ -220,7 +222,7 @@ export async function getAllTagInfoByCodePaths(codePaths: string[]) {
|
||||
if (type == TagResourceTypeEnum.Tag.value) {
|
||||
continue;
|
||||
}
|
||||
const tagInfo = await tagApi.listByQuery.request({ type: type, codes: allTypeAndCode[type] });
|
||||
const tagInfo = await tagApi.listByQuery.request({type: type, codes: allTypeAndCode[type]});
|
||||
allTypeAndCode[type] = tagInfo;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,46 @@
|
||||
<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 prop="taskName" label="任务名" required>
|
||||
<el-input v-model.trim="form.taskName" placeholder="请输入任务名" auto-complete="off" />
|
||||
<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-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="srcDbId" label="源数据库" required>
|
||||
<db-select-tree
|
||||
placeholder="请选择源数据库"
|
||||
@@ -27,7 +64,45 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="targetDbId" label="目标数据库" required>
|
||||
<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-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 v-if="form.mode == 1" prop="targetDbId" label="目标数据库" :required="form.mode === 1">
|
||||
<db-select-tree
|
||||
placeholder="请选择目标数据库"
|
||||
v-model:db-id="form.targetDbId"
|
||||
@@ -39,26 +114,19 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="strategy" label="迁移策略" required>
|
||||
<el-select v-model="form.strategy" filterable placeholder="迁移策略">
|
||||
<el-option label="全量" :value="1" />
|
||||
<el-option label="增量(暂不可用)" disabled :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="nameCase" label="转换表、字段名" required>
|
||||
<el-select v-model="form.nameCase">
|
||||
<el-option label="无" :value="1" />
|
||||
<el-option label="大写" :value="2" />
|
||||
<el-option label="小写" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="deleteTable" label="创建前删除表" required>
|
||||
<el-select v-model="form.deleteTable">
|
||||
<el-option label="是" :value="1" />
|
||||
<el-option label="否" :value="2" />
|
||||
</el-select>
|
||||
<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>
|
||||
@@ -96,6 +164,10 @@ import { computed, 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';
|
||||
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';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@@ -111,7 +183,43 @@ const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
||||
|
||||
const dialogVisible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const rules = {};
|
||||
const rules = {
|
||||
taskName: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入任务名',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
srcDbId: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择源库',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
targetDbId: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择目标库',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
targetFileDbType: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择目标文件语言类型',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
cron: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择cron表达式',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dbForm: any = ref(null);
|
||||
|
||||
@@ -121,6 +229,12 @@ const tableTab = 'table';
|
||||
type FormData = {
|
||||
id?: number;
|
||||
taskName: string;
|
||||
status: number;
|
||||
cronAble: 1 | -1;
|
||||
cron: string;
|
||||
mode: 1 | 2;
|
||||
targetFileDbType?: string;
|
||||
dbType: 1 | 2;
|
||||
srcDbId?: number;
|
||||
srcDbName?: string;
|
||||
srcDbType?: string;
|
||||
@@ -140,6 +254,9 @@ type FormData = {
|
||||
};
|
||||
|
||||
const basicFormData = {
|
||||
mode: 1,
|
||||
status: 1,
|
||||
cronAble: -1,
|
||||
strategy: 1,
|
||||
nameCase: 1,
|
||||
deleteTable: 1,
|
||||
@@ -182,7 +299,7 @@ const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDbTransferTa
|
||||
|
||||
// 基础字段信息是否填写完整
|
||||
const baseFieldCompleted = computed(() => {
|
||||
return state.form.srcDbId && state.form.targetDbId && state.form.targetDbName;
|
||||
return state.form.srcDbId && (state.form.targetDbId || state.form.targetFileDbType);
|
||||
});
|
||||
|
||||
watch(dialogVisible, async (newValue: boolean) => {
|
||||
@@ -200,8 +317,7 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
state.form = props.data as FormData;
|
||||
state.form = _.cloneDeep(props.data) as FormData;
|
||||
let { srcDbId, targetDbId } = state.form;
|
||||
|
||||
// 初始化src数据源
|
||||
@@ -228,6 +344,10 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
|
||||
// 初始化勾选迁移表
|
||||
srcTreeRef.value.setCheckedKeys(state.form.checkedKeys.split(','));
|
||||
|
||||
// 初始化默认值
|
||||
state.form.cronAble = state.form.cronAble || 0;
|
||||
state.form.mode = state.form.mode || 1;
|
||||
});
|
||||
|
||||
watch(
|
||||
|
||||
303
mayfly_go_web/src/views/ops/db/DbTransferFile.vue
Normal file
303
mayfly_go_web/src/views/ops/db/DbTransferFile.vue
Normal file
@@ -0,0 +1,303 @@
|
||||
<template>
|
||||
<div class="db-transfer-file">
|
||||
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true" width="900px">
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:data="state.tableData"
|
||||
v-model:query-form="state.query"
|
||||
:show-selection="true"
|
||||
v-model:selection-data="state.selectionData"
|
||||
:columns="columns"
|
||||
@page-num-change="
|
||||
(args) => {
|
||||
state.query.pageNum = args.pageNum;
|
||||
search();
|
||||
}
|
||||
"
|
||||
@page-size-change="
|
||||
(args) => {
|
||||
state.query.pageSize = args.pageNum;
|
||||
search();
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<el-button v-auth="perms.del" :disabled="state.selectionData.length < 1" @click="del()" type="danger" icon="delete">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #fileDbType="{ data }">
|
||||
<span>
|
||||
<SvgIcon :name="getDbDialect(data.fileDbType).getInfo().icon" :size="18" />
|
||||
{{ data.fileDbType }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #status="{ data }">
|
||||
<span>
|
||||
<el-tag v-if="data.status == 1" class="ml-2" type="primary">执行中</el-tag>
|
||||
<el-tag v-else-if="data.status == 2" class="ml-2" type="success">成功</el-tag>
|
||||
<el-tag v-else-if="data.status == -1" class="ml-2" type="danger">失败</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
<template #action="{ data }">
|
||||
<el-button v-if="actionBtns[perms.run] && data.status === 2" @click="openRun(data)" type="primary" link>执行</el-button>
|
||||
<el-button v-if="actionBtns[perms.rename] && data.status === 2" @click="rename(data)" type="warning" link>重命名</el-button>
|
||||
<el-button v-if="actionBtns[perms.down] && data.status === 2" @click="down(data)" type="primary" link>下载</el-button>
|
||||
<el-button v-if="data.logId" @click="openLog(data)" type="success" link>日志</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
<TerminalLog v-model:log-id="state.logsDialog.logId" v-model:visible="state.logsDialog.visible" :title="state.logsDialog.title" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :title="state.renameDialog.title" v-model="state.renameDialog.visible" :destroy-on-close="true" width="400px">
|
||||
<el-form :model="state.renameDialog.renameForm" ref="renameFormRef" label-width="auto">
|
||||
<el-form-item label="文件名" prop="fileName" required>
|
||||
<el-input v-model="state.renameDialog.renameForm.fileName" placeholder="请输入文件名" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button @click="state.renameDialog.cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="state.renameDialog.loading" @click="state.renameDialog.btnOk">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog :title="state.runDialog.title" v-model="state.runDialog.visible" :destroy-on-close="true" width="600px">
|
||||
<el-form :model="state.runDialog.runForm" ref="runFormRef" label-width="auto" :rules="state.runDialog.formRules">
|
||||
<el-form-item label="文件数据库类型" prop="dbType">
|
||||
<SvgIcon :name="getDbDialect(state.runDialog.runForm.dbType).getInfo().icon" :size="18" /> {{ state.runDialog.runForm.dbType }}
|
||||
</el-form-item>
|
||||
<el-form-item label="选择目标数据库" prop="targetDbId" required>
|
||||
<db-select-tree
|
||||
placeholder="请选择目标数据库"
|
||||
v-model:db-id="state.runDialog.runForm.targetDbId"
|
||||
v-model:inst-name="state.runDialog.runForm.targetInstName"
|
||||
v-model:db-name="state.runDialog.runForm.targetDbName"
|
||||
v-model:tag-path="state.runDialog.runForm.targetTagPath"
|
||||
v-model:db-type="state.runDialog.runForm.targetDbType"
|
||||
@select-db="state.runDialog.onSelectRunTargetDb"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button @click="state.runDialog.cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="state.runDialog.loading" @click="state.runDialog.btnOk">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, Ref, ref, watch } from 'vue';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
||||
import config from '@/common/config';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||
import { getClientId } from '@/common/utils/storage';
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: [Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const dialogVisible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('fileName', '文件名').setMinWidth(150),
|
||||
TableColumn.new('createTime', '创建时间').setMinWidth(180).isTime(),
|
||||
TableColumn.new('fileDbType', 'sql语言').setMinWidth(90).isSlot(),
|
||||
TableColumn.new('status', '状态').isSlot(),
|
||||
]);
|
||||
|
||||
const perms = {
|
||||
del: 'db:transfer:files:del',
|
||||
down: 'db:transfer:files:down',
|
||||
rename: 'db:transfer:files:rename',
|
||||
run: 'db:transfer:files:run',
|
||||
};
|
||||
|
||||
const actionBtns = hasPerms([perms.del, perms.down, perms.rename, perms.run]);
|
||||
|
||||
const actionWidth = ((actionBtns[perms.rename] ? 1 : 0) + (actionBtns[perms.down] ? 1 : 0) + (actionBtns[perms.run] ? 1 : 0) + 1) * 55;
|
||||
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(actionWidth).fixedRight().alignCenter();
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
columns.value.push(actionColumn);
|
||||
}
|
||||
});
|
||||
|
||||
const renameFormRef: any = ref(null);
|
||||
const runFormRef: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
query: {
|
||||
taskId: props.data?.id,
|
||||
name: null,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
logsDialog: {
|
||||
logId: 0,
|
||||
title: '数据库迁移日志',
|
||||
visible: false,
|
||||
data: null as any,
|
||||
running: false,
|
||||
},
|
||||
runDialog: {
|
||||
title: '指定数据库执行sql文件',
|
||||
visible: false,
|
||||
data: null as any,
|
||||
formRules: {
|
||||
targetDbId: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择目标数据库',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
},
|
||||
runForm: {
|
||||
id: 0,
|
||||
dbType: '',
|
||||
clientId: '',
|
||||
targetDbId: 0,
|
||||
targetDbName: '',
|
||||
targetTagPath: '',
|
||||
targetInstName: '',
|
||||
targetDbType: '',
|
||||
},
|
||||
loading: false,
|
||||
cancel: function () {
|
||||
state.runDialog.visible = false;
|
||||
state.runDialog.runForm = {} as any;
|
||||
},
|
||||
btnOk: function () {
|
||||
runFormRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
console.log(state.runDialog.runForm);
|
||||
if (state.runDialog.runForm.targetDbType !== state.runDialog.runForm.dbType) {
|
||||
ElMessage.warning(`请选择[${state.runDialog.runForm.dbType}]数据库`);
|
||||
return false;
|
||||
}
|
||||
state.runDialog.runForm.clientId = getClientId();
|
||||
await dbApi.dbTransferFileRun.request(state.runDialog.runForm);
|
||||
ElMessage.success('保存成功');
|
||||
state.runDialog.cancel();
|
||||
await search();
|
||||
});
|
||||
},
|
||||
onSelectRunTargetDb: function (param: any) {
|
||||
if (param.type !== state.runDialog.runForm.dbType) {
|
||||
ElMessage.warning(`请选择[${state.runDialog.runForm.dbType}]数据库`);
|
||||
}
|
||||
},
|
||||
},
|
||||
renameDialog: {
|
||||
visible: false,
|
||||
title: '文件重命名',
|
||||
renameForm: {
|
||||
id: 0,
|
||||
fileName: '',
|
||||
},
|
||||
loading: false,
|
||||
cancel: function () {
|
||||
state.renameDialog.visible = false;
|
||||
state.renameDialog.renameForm = { id: 0, fileName: '' };
|
||||
},
|
||||
btnOk: function () {
|
||||
renameFormRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
await dbApi.dbTransferFileRename.request(state.renameDialog.renameForm);
|
||||
ElMessage.success('保存成功');
|
||||
state.renameDialog.cancel();
|
||||
await search();
|
||||
});
|
||||
},
|
||||
},
|
||||
selectionData: [], // 选中的数据
|
||||
tableData: [],
|
||||
});
|
||||
|
||||
const search = async () => {
|
||||
const { total, list } = await dbApi.dbTransferFileList.request(state.query);
|
||||
state.tableData = list;
|
||||
pageTableRef.value.total = total;
|
||||
};
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
const del = async function () {
|
||||
try {
|
||||
await ElMessageBox.confirm(`将会删除sql文件,确定删除?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
ElMessage.success('删除成功');
|
||||
await search();
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const down = function (data: any) {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('target', '_blank');
|
||||
a.setAttribute('href', `${config.baseApiUrl}/dbTransfer/files/down/${data.fileUuid}?${joinClientParams()}`);
|
||||
a.click();
|
||||
a.remove();
|
||||
};
|
||||
const rename = function (data: any) {
|
||||
state.renameDialog.visible = true;
|
||||
const { id, fileName } = data;
|
||||
state.renameDialog.renameForm = { id, fileName };
|
||||
};
|
||||
const openLog = function (data: any) {
|
||||
state.logsDialog.logId = data.logId;
|
||||
state.logsDialog.visible = true;
|
||||
state.logsDialog.title = '数据库迁移日志';
|
||||
state.logsDialog.running = data.state === 1;
|
||||
};
|
||||
|
||||
// 运行sql,弹出选择需要运行的库,默认运行当前数据库,需要保证数据库类型与sql文件一致
|
||||
const openRun = function (data: any) {
|
||||
console.log(data);
|
||||
state.runDialog.runForm = { id: data.id, dbType: data.fileDbType } as any;
|
||||
console.log(state.runDialog.runForm);
|
||||
state.runDialog.visible = true;
|
||||
};
|
||||
|
||||
watch(dialogVisible, async (newValue: boolean) => {
|
||||
if (!newValue) {
|
||||
return;
|
||||
}
|
||||
state.query.taskId = props.data?.id;
|
||||
state.query.pageNum = 1;
|
||||
state.query.pageSize = 10;
|
||||
|
||||
await search();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -36,16 +36,38 @@
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<template #status="{ data }">
|
||||
<span v-if="actionBtns[perms.status]">
|
||||
<el-switch
|
||||
v-model="data.status"
|
||||
@click="updStatus(data.id, data.status)"
|
||||
inline-prompt
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
:active-value="1"
|
||||
:inactive-value="-1"
|
||||
/>
|
||||
</span>
|
||||
<span v-else>
|
||||
<el-tag v-if="data.status == 1" class="ml-2" type="success">启用</el-tag>
|
||||
<el-tag v-else class="ml-2" type="danger">禁用</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<!-- 删除、启停用、编辑 -->
|
||||
<el-button v-if="actionBtns[perms.save]" @click="edit(data)" type="primary" link>编辑</el-button>
|
||||
<el-button v-if="actionBtns[perms.log]" type="primary" link @click="log(data)">日志</el-button>
|
||||
<el-button v-if="actionBtns[perms.log]" type="warning" link @click="log(data)">日志</el-button>
|
||||
<el-button v-if="data.runningState === 1" @click="stop(data.id)" type="danger" link>停止</el-button>
|
||||
<el-button v-if="actionBtns[perms.run] && data.runningState !== 1" type="primary" link @click="reRun(data)">运行</el-button>
|
||||
<el-button v-if="actionBtns[perms.run] && data.runningState !== 1 && data.status === 1" type="success" link @click="reRun(data)"
|
||||
>运行</el-button
|
||||
>
|
||||
<el-button v-if="actionBtns[perms.files] && data.mode === 2" type="success" link @click="openFiles(data)">文件</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<db-transfer-edit @val-change="search" :title="editDialog.title" v-model:visible="editDialog.visible" v-model:data="editDialog.data" />
|
||||
<db-transfer-file :title="filesDialog.title" v-model:visible="filesDialog.visible" v-model:data="filesDialog.data" />
|
||||
|
||||
<TerminalLog v-model:log-id="logsDialog.logId" v-model:visible="logsDialog.visible" :title="logsDialog.title" />
|
||||
</div>
|
||||
@@ -62,6 +84,7 @@ import { SearchItem } from '@/components/SearchForm';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import { DbTransferRunningStateEnum } from './enums';
|
||||
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
||||
import DbTransferFile from './DbTransferFile.vue';
|
||||
|
||||
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
|
||||
|
||||
@@ -71,6 +94,7 @@ const perms = {
|
||||
status: 'db:transfer:status',
|
||||
log: 'db:transfer:log',
|
||||
run: 'db:transfer:run',
|
||||
files: 'db:transfer:files',
|
||||
};
|
||||
|
||||
const searchItems = [SearchItem.input('name', '名称')];
|
||||
@@ -78,17 +102,17 @@ const searchItems = [SearchItem.input('name', '名称')];
|
||||
const columns = ref([
|
||||
TableColumn.new('taskName', '任务名').setMinWidth(150).isSlot(),
|
||||
TableColumn.new('srcDb', '源库').setMinWidth(150).isSlot(),
|
||||
TableColumn.new('targetDb', '目标库').setMinWidth(150).isSlot(),
|
||||
// TableColumn.new('targetDb', '目标库').setMinWidth(150).isSlot(),
|
||||
TableColumn.new('runningState', '执行状态').typeTag(DbTransferRunningStateEnum),
|
||||
TableColumn.new('creator', '创建人'),
|
||||
TableColumn.new('createTime', '创建时间').isTime(),
|
||||
TableColumn.new('status', '状态').isSlot(),
|
||||
TableColumn.new('modifier', '修改人'),
|
||||
TableColumn.new('updateTime', '修改时间').isTime(),
|
||||
]);
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.save, perms.del, perms.status, perms.log, perms.run]);
|
||||
const actionWidth = ((actionBtns[perms.save] ? 1 : 0) + (actionBtns[perms.log] ? 1 : 0) + (actionBtns[perms.run] ? 1 : 0)) * 55;
|
||||
const actionBtns = hasPerms([perms.save, perms.del, perms.status, perms.log, perms.run, perms.files]);
|
||||
const actionWidth =
|
||||
((actionBtns[perms.save] ? 1 : 0) + (actionBtns[perms.log] ? 1 : 0) + (actionBtns[perms.run] ? 1 : 0) + (actionBtns[perms.files] ? 1 : 0)) * 55;
|
||||
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(actionWidth).fixedRight().alignCenter();
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
@@ -120,9 +144,15 @@ const state = reactive({
|
||||
data: null as any,
|
||||
running: false,
|
||||
},
|
||||
filesDialog: {
|
||||
taskId: 0,
|
||||
title: '迁移文件列表',
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, editDialog, logsDialog } = toRefs(state);
|
||||
const { selectionData, query, editDialog, logsDialog, filesDialog } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
@@ -137,10 +167,10 @@ const search = () => {
|
||||
const edit = async (data: any) => {
|
||||
if (!data) {
|
||||
state.editDialog.data = null;
|
||||
state.editDialog.title = '新增数据库迁移任务';
|
||||
state.editDialog.title = '新增数据库迁移任务(迁移不会对源库造成修改)';
|
||||
} else {
|
||||
state.editDialog.data = data;
|
||||
state.editDialog.title = '修改数据库迁移任务';
|
||||
state.editDialog.title = '修改数据库迁移任务(迁移不会对源库造成修改)';
|
||||
}
|
||||
state.editDialog.visible = true;
|
||||
};
|
||||
@@ -184,6 +214,22 @@ const reRun = async (data: any) => {
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const openFiles = async (data: any) => {
|
||||
state.filesDialog.visible = true;
|
||||
state.filesDialog.title = '迁移文件管理';
|
||||
state.filesDialog.taskId = data.id;
|
||||
state.filesDialog.data = data;
|
||||
};
|
||||
const updStatus = async (id: any, status: 1 | -1) => {
|
||||
try {
|
||||
await dbApi.updateDbTransferTaskStatus.request({ taskId: id, status });
|
||||
ElMessage.success(`${status === 1 ? '启用' : '禁用'}成功`);
|
||||
search();
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const del = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除任务?`, '提示', {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -69,36 +69,73 @@
|
||||
<monaco-editor height="150px" class="task-sql" language="sql" v-model="form.dataSql" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="targetTableName" label="目标库表" required>
|
||||
<el-select v-model="form.targetTableName" filterable placeholder="请选择目标数据库表">
|
||||
<el-option
|
||||
v-for="item in state.targetTableList"
|
||||
:key="item.tableName"
|
||||
:label="item.tableName + (item.tableComment && '-' + item.tableComment)"
|
||||
:value="item.tableName"
|
||||
/>
|
||||
</el-select>
|
||||
<el-form-item>
|
||||
<el-row class="w100">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="targetTableName" label="目标库表" required>
|
||||
<el-select v-model="form.targetTableName" filterable placeholder="请选择目标数据库表">
|
||||
<el-option
|
||||
v-for="item in state.targetTableList"
|
||||
:key="item.tableName"
|
||||
:label="item.tableName + (item.tableComment && '-' + item.tableComment)"
|
||||
:value="item.tableName"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="pageSize" label="分页大小" required>
|
||||
<el-input type="number" v-model.number="form.pageSize" placeholder="同步数据时查询的每页数据大小" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<el-form-item prop="pageSize" label="分页大小" required>
|
||||
<el-input type="number" v-model.number="form.pageSize" placeholder="同步数据时查询的每页数据大小" auto-complete="off" />
|
||||
<el-form-item class="w100" prop="updField">
|
||||
<template #label>
|
||||
更新字段
|
||||
<el-tooltip content="查询数据源的时候会带上这个字段当前最大值,支持带别名,如:t.create_time" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-tooltip content="查询数据源的时候会带上这个字段当前最大值,支持带别名,如:t.create_time" placement="top">
|
||||
<el-form-item prop="updField" label="更新字段" required>
|
||||
<el-input v-model.trim="form.updField" placeholder="查询数据源的时候会带上这个字段当前最大值" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
<el-form-item class="w100" prop="updFieldVal">
|
||||
<template #label>
|
||||
更新值
|
||||
<el-tooltip content="记录更新字段当前值,如:当前时间,当前日期等,下次查询数据时会带上该值条件" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input v-model.trim="form.updFieldVal" placeholder="更新字段当前最大值" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item prop="updFieldVal" label="更新值">
|
||||
<el-input v-model.trim="form.updFieldVal" placeholder="更新字段当前最大值" auto-complete="off" />
|
||||
<el-form-item class="w100" prop="updFieldSrc">
|
||||
<template #label>
|
||||
值来源
|
||||
<el-tooltip
|
||||
content="从查询结果中取更新值的字段名,默认同更新字段,如果查询结果指定了字段别名且与原更新字段不一致,则取这个字段值为当前更新值"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input v-model.trim="form.updFieldSrc" placeholder="更新值来源" auto-complete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -253,6 +290,7 @@ type FormData = {
|
||||
pageSize?: number;
|
||||
updField?: string;
|
||||
updFieldVal?: string;
|
||||
updFieldSrc?: string;
|
||||
fieldMap?: { src: string; target: string }[];
|
||||
status?: 1 | 2;
|
||||
duplicateStrategy?: -1 | 1 | 2;
|
||||
@@ -326,7 +364,7 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
const db = dbInfoRes.list[0];
|
||||
// 初始化实例
|
||||
db.databases = db.database?.split(' ').sort() || [];
|
||||
state.srcDbInst = DbInst.getOrNewInst(db);
|
||||
state.srcDbInst = await DbInst.getOrNewInst(db);
|
||||
state.form.srcDbType = state.srcDbInst.type;
|
||||
state.form.srcInstName = db.name;
|
||||
}
|
||||
@@ -338,7 +376,7 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
const db = dbInfoRes.list[0];
|
||||
// 初始化实例
|
||||
db.databases = db.database?.split(' ').sort() || [];
|
||||
state.targetDbInst = DbInst.getOrNewInst(db);
|
||||
state.targetDbInst = await DbInst.getOrNewInst(db);
|
||||
state.form.targetDbType = state.targetDbInst.type;
|
||||
state.form.targetInstName = db.name;
|
||||
}
|
||||
@@ -397,12 +435,12 @@ const refreshPreviewInsertSql = () => {
|
||||
const onSelectSrcDb = async (params: any) => {
|
||||
// 初始化数据源
|
||||
params.databases = params.dbs; // 数据源里需要这个值
|
||||
state.srcDbInst = DbInst.getOrNewInst(params);
|
||||
state.srcDbInst = await DbInst.getOrNewInst(params);
|
||||
registerDbCompletionItemProvider(params.id, params.db, params.dbs, params.type);
|
||||
};
|
||||
|
||||
const onSelectTargetDb = async (params: any) => {
|
||||
state.targetDbInst = DbInst.getOrNewInst(params);
|
||||
state.targetDbInst = await DbInst.getOrNewInst(params);
|
||||
await loadDbTables(params.id, params.db);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Api from '@/common/Api';
|
||||
import { AesEncrypt } from '@/common/crypto';
|
||||
import {AesEncrypt} from '@/common/crypto';
|
||||
|
||||
export const dbApi = {
|
||||
// 获取权限列表
|
||||
@@ -26,6 +26,8 @@ export const dbApi = {
|
||||
deleteDbSql: Api.newDelete('/dbs/{id}/sql'),
|
||||
// 获取数据库sql执行记录
|
||||
getSqlExecs: Api.newGet('/dbs/sql-execs'),
|
||||
// 获取数据库兼容版本
|
||||
getCompatibleDbVersion: Api.newGet('/dbs/{id}/version'),
|
||||
|
||||
instances: Api.newGet('/instances'),
|
||||
getInstance: Api.newGet('/instances/{instanceId}'),
|
||||
@@ -72,9 +74,15 @@ export const dbApi = {
|
||||
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}'),
|
||||
dbTransferFileRename: Api.newPost('/dbTransfer/files/rename'),
|
||||
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
|
||||
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
|
||||
};
|
||||
|
||||
export const dbSqlExecApi = {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
|
||||
<el-option
|
||||
v-for="pgsqlType in getDbDialect(dbType).getInfo().columnTypes"
|
||||
v-for="pgsqlType in getDbDialect(dbType!).getInfo().columnTypes"
|
||||
:key="pgsqlType.dataType"
|
||||
:value="pgsqlType.udtName"
|
||||
:label="pgsqlType.dataType"
|
||||
@@ -127,7 +127,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, toRefs, watch } from 'vue';
|
||||
import { computed, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import SqlExecBox from '../sqleditor/SqlExecBox';
|
||||
import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
|
||||
@@ -152,12 +152,15 @@ const props = defineProps({
|
||||
dbType: {
|
||||
type: String,
|
||||
},
|
||||
version: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
||||
|
||||
let dbDialect = getDbDialect(props.dbType);
|
||||
let dbDialect = computed(() => getDbDialect(props.dbType!, props.version));
|
||||
|
||||
type ColName = {
|
||||
prop: string;
|
||||
@@ -271,7 +274,7 @@ const { dialogVisible, btnloading, activeName, tableData } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue) => {
|
||||
state.dialogVisible = newValue.visible;
|
||||
dbDialect = getDbDialect(newValue.dbType);
|
||||
dbDialect.value = getDbDialect(newValue.dbType!);
|
||||
});
|
||||
|
||||
// 切换到索引tab时,刷新索引字段下拉选项
|
||||
@@ -306,11 +309,11 @@ const addRow = () => {
|
||||
};
|
||||
|
||||
const addIndex = () => {
|
||||
state.tableData.indexs.res.push(dbDialect.getDefaultIndex());
|
||||
state.tableData.indexs.res.push(dbDialect.value.getDefaultIndex());
|
||||
};
|
||||
|
||||
const addDefaultRows = () => {
|
||||
state.tableData.fields.res.push(...dbDialect.getDefaultRows());
|
||||
state.tableData.fields.res.push(...dbDialect.value.getDefaultRows());
|
||||
};
|
||||
|
||||
const deleteRow = (index: any) => {
|
||||
@@ -331,7 +334,7 @@ const submit = async () => {
|
||||
sql: sql,
|
||||
dbId: props.dbId as any,
|
||||
db: props.db as any,
|
||||
dbType: dbDialect.getInfo().formatSqlDialect,
|
||||
dbType: dbDialect.value.getInfo().formatSqlDialect,
|
||||
runSuccessCallback: () => {
|
||||
emit('submit-sql', { tableName: state.tableData.tableName });
|
||||
// cancel();
|
||||
@@ -411,21 +414,22 @@ const genSql = () => {
|
||||
let data = state.tableData;
|
||||
// 创建表
|
||||
if (!props.data?.edit) {
|
||||
let createTable = dbDialect.getCreateTableSql(data);
|
||||
let createTable = dbDialect.value.getCreateTableSql(data);
|
||||
let createIndex = '';
|
||||
if (data.indexs.res.length > 0) {
|
||||
createIndex = dbDialect.getCreateIndexSql(data);
|
||||
createIndex = dbDialect.value.getCreateIndexSql(data);
|
||||
}
|
||||
return createTable + ';' + createIndex;
|
||||
} else {
|
||||
// 修改列
|
||||
let changeColData = filterChangedData(state.tableData.fields.oldFields, state.tableData.fields.res, 'name');
|
||||
let colSql = changeColData.changed ? dbDialect.getModifyColumnSql(data, data.tableName, changeColData) : '';
|
||||
let colSql = changeColData.changed ? dbDialect.value.getModifyColumnSql(data, data.tableName, changeColData) : '';
|
||||
// 修改索引
|
||||
let changeIdxData = filterChangedData(state.tableData.indexs.oldIndexs, state.tableData.indexs.res, 'indexName');
|
||||
let idxSql = changeIdxData.changed ? dbDialect.getModifyIndexSql(data, data.tableName, changeIdxData) : '';
|
||||
let idxSql = changeIdxData.changed ? dbDialect.value.getModifyIndexSql(data, data.tableName, changeIdxData) : '';
|
||||
// 修改表名,表注释
|
||||
let tableInfoSql = data.tableName !== data.oldTableName || data.tableComment !== data.oldTableComment ? dbDialect.getModifyTableInfoSql(data) : '';
|
||||
let tableInfoSql =
|
||||
data.tableName !== data.oldTableName || data.tableComment !== data.oldTableComment ? dbDialect.value.getModifyTableInfoSql(data) : '';
|
||||
|
||||
let sqlArr = [];
|
||||
colSql && sqlArr.push(colSql);
|
||||
|
||||
@@ -42,6 +42,8 @@ export class DbInst {
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/** 兼容版本 */
|
||||
version: string;
|
||||
/**
|
||||
* dbName -> db
|
||||
*/
|
||||
@@ -379,7 +381,7 @@ export class DbInst {
|
||||
* @param inst 数据库实例,后端返回的列表接口中的信息
|
||||
* @returns DbInst
|
||||
*/
|
||||
static getOrNewInst(inst: any) {
|
||||
static async getOrNewInst(inst: any) {
|
||||
if (!inst) {
|
||||
throw new Error('inst不能为空');
|
||||
}
|
||||
@@ -401,6 +403,9 @@ export class DbInst {
|
||||
dbInst.type = inst.type;
|
||||
dbInst.databases = inst.databases;
|
||||
|
||||
// 获取兼容版本信息
|
||||
dbInst.version = await dbApi.getCompatibleDbVersion.request({ id: inst.id, db: dbInst.databases[0] });
|
||||
|
||||
dbInstCache.set(dbInst.id, dbInst);
|
||||
return dbInst;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { MysqlDialect } from './mysql_dialect';
|
||||
import { PostgresqlDialect } from './postgres_dialect';
|
||||
import { DMDialect } from '@/views/ops/db/dialect/dm_dialect';
|
||||
import { OracleDialect } from '@/views/ops/db/dialect/oracle_dialect';
|
||||
import { MariadbDialect } from '@/views/ops/db/dialect/mariadb_dialect';
|
||||
import { SqliteDialect } from '@/views/ops/db/dialect/sqlite_dialect';
|
||||
import { MssqlDialect } from '@/views/ops/db/dialect/mssql_dialect';
|
||||
import { GaussDialect } from '@/views/ops/db/dialect/gauss_dialect';
|
||||
import { KingbaseEsDialect } from '@/views/ops/db/dialect/kingbaseES_dialect';
|
||||
import { VastbaseDialect } from '@/views/ops/db/dialect/vastbase_dialect';
|
||||
import {MysqlDialect} from './mysql_dialect';
|
||||
import {PostgresqlDialect} from './postgres_dialect';
|
||||
import {DMDialect} from '@/views/ops/db/dialect/dm_dialect';
|
||||
import {OracleDialect} from '@/views/ops/db/dialect/oracle_dialect';
|
||||
import {MariadbDialect} from '@/views/ops/db/dialect/mariadb_dialect';
|
||||
import {SqliteDialect} from '@/views/ops/db/dialect/sqlite_dialect';
|
||||
import {MssqlDialect} from '@/views/ops/db/dialect/mssql_dialect';
|
||||
import {GaussDialect} from '@/views/ops/db/dialect/gauss_dialect';
|
||||
import {KingbaseEsDialect} from '@/views/ops/db/dialect/kingbaseES_dialect';
|
||||
import {VastbaseDialect} from '@/views/ops/db/dialect/vastbase_dialect';
|
||||
import {Oracle11Dialect} from "@/views/ops/db/dialect/oracle11_dialect";
|
||||
|
||||
export interface sqlColumnType {
|
||||
udtName: string;
|
||||
@@ -37,6 +38,7 @@ export interface IndexDefinition {
|
||||
indexType: string;
|
||||
indexComment?: string;
|
||||
}
|
||||
|
||||
export const commonCustomKeywords = ['GROUP BY', 'ORDER BY', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'SELECT * FROM'];
|
||||
|
||||
export interface EditorCompletionItem {
|
||||
@@ -212,7 +214,11 @@ export interface DbDialect {
|
||||
* @param tableName 表名
|
||||
* @param changeData 改变信息
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 生成编辑索引sql
|
||||
@@ -249,17 +255,21 @@ export enum DuplicateStrategy {
|
||||
let mysqlDialect = new MysqlDialect();
|
||||
|
||||
let dbType2DialectMap: Map<string, DbDialect> = new Map();
|
||||
let dbType2DialectVersionMap: Map<string, DbDialect> = new Map();
|
||||
|
||||
export const registerDbDialect = (dbType: string, dd: DbDialect) => {
|
||||
dbType2DialectMap.set(dbType, dd);
|
||||
};
|
||||
export const registerDbDialectVersion = (dbType: string, dd: DbDialect) => {
|
||||
dbType2DialectVersionMap.set(dbType, dd);
|
||||
};
|
||||
|
||||
export const getDbDialectMap = () => {
|
||||
return dbType2DialectMap;
|
||||
};
|
||||
|
||||
export const getDbDialect = (dbType?: string): DbDialect => {
|
||||
return dbType2DialectMap.get(dbType!) || mysqlDialect;
|
||||
export const getDbDialect = (dbType: string, version = ''): DbDialect => {
|
||||
return dbType2DialectVersionMap.get(dbType + version) || dbType2DialectMap.get(dbType) || mysqlDialect;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -282,6 +292,7 @@ export const QuoteEscape = (str: string): string => {
|
||||
registerDbDialect(DbType.gauss, new GaussDialect());
|
||||
registerDbDialect(DbType.dm, new DMDialect());
|
||||
registerDbDialect(DbType.oracle, new OracleDialect());
|
||||
registerDbDialectVersion(DbType.oracle + '11', new Oracle11Dialect()); // oracle 11g及以前版本的一些语法兼容
|
||||
registerDbDialect(DbType.sqlite, new SqliteDialect());
|
||||
registerDbDialect(DbType.mssql, new MssqlDialect());
|
||||
registerDbDialect(DbType.kingbaseEs, new KingbaseEsDialect());
|
||||
|
||||
55
mayfly_go_web/src/views/ops/db/dialect/oracle11_dialect.ts
Normal file
55
mayfly_go_web/src/views/ops/db/dialect/oracle11_dialect.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/** oracle 11g 及以前的版本的一些语法兼容 */
|
||||
import {OracleDialect} from '@/views/ops/db/dialect/oracle_dialect';
|
||||
import {DialectInfo, RowDefinition} from '@/views/ops/db/dialect/index';
|
||||
|
||||
let oracle11DialectInfo: DialectInfo;
|
||||
|
||||
export class Oracle11Dialect extends OracleDialect {
|
||||
|
||||
getInfo(): DialectInfo {
|
||||
if (oracle11DialectInfo) {
|
||||
return oracle11DialectInfo;
|
||||
}
|
||||
|
||||
oracle11DialectInfo = {} as DialectInfo;
|
||||
Object.assign(oracle11DialectInfo, super.getInfo());
|
||||
oracle11DialectInfo.name = 'Oracle11x';
|
||||
return oracle11DialectInfo;
|
||||
}
|
||||
|
||||
// 重写创建自增列sql
|
||||
|
||||
genColumnBasicSql(cl: RowDefinition, create: boolean, data = {}): string {
|
||||
let length = this.getTypeLengthSql(cl);
|
||||
// 默认值
|
||||
let defVal = this.getDefaultValueSql(cl, false, data);
|
||||
// 忽略自增配置,11g不支持直接设置自增列,需要单独设置自增序列
|
||||
// 如果有原名以原名为准
|
||||
let name = cl.oldName && cl.name !== cl.oldName ? cl.oldName : cl.name;
|
||||
let baseSql = ` ${this.quoteIdentifier(name)} ${cl.type}${length}`;
|
||||
return ` ${baseSql} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
|
||||
}
|
||||
|
||||
getDefaultValueSql(cl: RowDefinition, create?: boolean, data?: any): string {
|
||||
if (cl.value) {
|
||||
return ` DEFAULT ${cl.value}`;
|
||||
} else if (cl.auto_increment) {
|
||||
return ` DEFAULT ${data.tableName}_${cl.name}_SEQ.NEXTVAL`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
getOtherCreateTableSql(data: any): string {
|
||||
// 通过字段自增信息创建自增序列
|
||||
|
||||
let result = '';
|
||||
data.fields.res.forEach((field: RowDefinition) => {
|
||||
let seqName = `${data.tableName}_${field.name}_SEQ`;
|
||||
if (field.auto_increment) {
|
||||
result += `CREATE SEQUENCE ${seqName} START WITH 1 INCREMENT BY 1 CACHE 20`;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
DuplicateStrategy,
|
||||
EditorCompletion,
|
||||
EditorCompletionItem,
|
||||
QuoteEscape,
|
||||
IndexDefinition,
|
||||
QuoteEscape,
|
||||
RowDefinition,
|
||||
sqlColumnType,
|
||||
} from './index';
|
||||
@@ -85,10 +85,10 @@ const replaceFunctions: EditorCompletionItem[] = [
|
||||
{ label: 'CURRENT_DATE', insertText: 'CURRENT_DATE', description: '获取当前日期' },
|
||||
{ label: 'CURRENT_TIMESTAMP', insertText: 'TIMESTAMP', description: '获取当前时间' },
|
||||
// 转换函数
|
||||
{ label: 'TO_CHAR', insertText: 'TO_CHAR(d|n[,fmt])', description: '把日期和数字转换为制定格式的字符串' },
|
||||
{ label: 'TO_CHAR', insertText: `TO_CHAR(d|n, 'yyyy-MM-dd HH24:mi:ss')`, description: '把日期和数字转换为制定格式的字符串' },
|
||||
{ label: 'TO_DATE', insertText: `TO_DATE(X, 'yyyy-MM-dd HH24:mi:ss')`, description: '把一个字符串以fmt格式转换成一个日期类型' },
|
||||
{ label: 'TO_NUMBER', insertText: 'TO_NUMBER(X,[,fmt])', description: '把一个字符串以fmt格式转换为一个数字' },
|
||||
{ label: 'TO_TIMESTAMP', insertText: 'TO_TIMESTAMP(X,[,fmt])', description: '把一个字符串以fmt格式转换为日期类型' },
|
||||
{ label: 'TO_NUMBER', insertText: `TO_NUMBER(X, 'yyyy-MM-dd HH24:mi:ss')`, description: '把一个字符串以fmt格式转换为一个数字' },
|
||||
{ label: 'TO_TIMESTAMP', insertText: `TO_TIMESTAMP(X, 'yyyy-MM-dd HH24:mi:ss.ff')`, description: '把一个字符串以fmt格式转换为日期类型' },
|
||||
// 其他
|
||||
{ label: 'NVL', insertText: 'NVL(X,VALUE)', description: '如果X为空,返回value,否则返回X' },
|
||||
{ label: 'NVL2', insertText: 'NVL2(x,value1,value2)', description: '如果x非空,返回value1,否则返回value2' },
|
||||
@@ -293,7 +293,7 @@ class OracleDialect implements DbDialect {
|
||||
return '';
|
||||
}
|
||||
|
||||
genColumnBasicSql(cl: RowDefinition, create: boolean): string {
|
||||
genColumnBasicSql(cl: RowDefinition, create: boolean, data = {}): string {
|
||||
let length = this.getTypeLengthSql(cl);
|
||||
// 默认值
|
||||
let defVal = this.getDefaultValueSql(cl);
|
||||
@@ -309,6 +309,11 @@ class OracleDialect implements DbDialect {
|
||||
return incr ? baseSql : ` ${baseSql} ${defVal} ${cl.notNull ? 'NOT NULL' : ''} `;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars
|
||||
getOtherCreateTableSql(data: any) {
|
||||
return '';
|
||||
}
|
||||
|
||||
getCreateTableSql(data: any): string {
|
||||
let schemaArr = data.db.split('/');
|
||||
let schema = schemaArr.length > 1 ? schemaArr[schemaArr.length - 1] : schemaArr[0];
|
||||
@@ -322,7 +327,7 @@ class OracleDialect implements DbDialect {
|
||||
// 创建表结构
|
||||
let fields: string[] = [];
|
||||
data.fields.res.forEach((item: any) => {
|
||||
item.name && fields.push(this.genColumnBasicSql(item, true));
|
||||
item.name && fields.push(this.genColumnBasicSql(item, true, data));
|
||||
// 列注释
|
||||
if (item.remark) {
|
||||
columCommentSql += ` COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(item.name)} is '${QuoteEscape(item.remark)}'; `;
|
||||
@@ -344,7 +349,9 @@ class OracleDialect implements DbDialect {
|
||||
tableCommentSql = ` COMMENT ON TABLE ${dbTable} is '${QuoteEscape(data.tableComment)}'; `;
|
||||
}
|
||||
|
||||
return createSql + tableCommentSql + columCommentSql;
|
||||
// 其余建表信息,如:自增字段在老版本的使用方式是创建自增序列
|
||||
let other = this.getOtherCreateTableSql(data);
|
||||
return createSql + tableCommentSql + columCommentSql + other;
|
||||
}
|
||||
|
||||
getCreateIndexSql(tableData: any): string {
|
||||
@@ -391,7 +398,7 @@ class OracleDialect implements DbDialect {
|
||||
commentArr.push(commentSql);
|
||||
}
|
||||
}
|
||||
modifyArr.push(` MODIFY (${this.genColumnBasicSql(a, false)})`);
|
||||
modifyArr.push(` MODIFY (${this.genColumnBasicSql(a, false, tableData)})`);
|
||||
if (a.pri) {
|
||||
priArr.add(`${this.quoteIdentifier(a.name)}`);
|
||||
}
|
||||
@@ -400,7 +407,7 @@ class OracleDialect implements DbDialect {
|
||||
|
||||
if (changeData.add.length > 0) {
|
||||
changeData.add.forEach((a) => {
|
||||
modifyArr.push(` ADD (${this.genColumnBasicSql(a, false)})`);
|
||||
modifyArr.push(` ADD (${this.genColumnBasicSql(a, false, tableData)})`);
|
||||
if (a.remark) {
|
||||
commentArr.push(`COMMENT ON COLUMN ${dbTable}.${this.quoteIdentifier(a.name)} is '${QuoteEscape(a.remark)}'`);
|
||||
}
|
||||
|
||||
@@ -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,74 +57,87 @@ 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: 'sqlite_compileoption_used', insertText: 'sqlite_compileoption_used(X)', 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: 'time', insertText: 'time(time-value[, modifier, ...])', description: '将日期和时间字符串转换为特定的日期和时间格式' },
|
||||
{ label: 'datetime', insertText: 'datetime(time-value[, modifier, ...])', description: '计算日期和时间的儒略日数' },
|
||||
{ label: 'julianday', insertText: 'julianday(time-value[, modifier, ...])', 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编译时是否使用了指定的编译选项'
|
||||
},
|
||||
{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: '将日期和时间字符串转换为特定的日期和时间格式'
|
||||
},
|
||||
{label: 'datetime', insertText: 'datetime(time-value[, modifier, ...])', description: '计算日期和时间的儒略日数'},
|
||||
{
|
||||
label: 'julianday',
|
||||
insertText: 'julianday(time-value[, modifier, ...])',
|
||||
description: '将日期和时间格式化为指定的字符串'
|
||||
},
|
||||
];
|
||||
|
||||
let sqliteDialectInfo: DialectInfo;
|
||||
|
||||
class SqliteDialect implements DbDialect {
|
||||
getInfo(): DialectInfo {
|
||||
if (sqliteDialectInfo) {
|
||||
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 = {
|
||||
name: 'Sqlite',
|
||||
name: 'Sqlite3',
|
||||
icon: 'iconfont icon-sqlite',
|
||||
defaultPort: 0,
|
||||
formatSqlDialect: 'sql',
|
||||
@@ -135,10 +148,11 @@ 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
|
||||
)};`;
|
||||
return `SELECT *
|
||||
FROM ${this.quoteIdentifier(table)} ${condition ? 'WHERE ' + condition : ''} ${orderBy ? orderBy : ''} ${this.getPageSql(
|
||||
pageNum,
|
||||
limit
|
||||
)};`;
|
||||
}
|
||||
|
||||
getPageSql(pageNum: number, limit: number) {
|
||||
@@ -147,8 +161,28 @@ class SqliteDialect implements DbDialect {
|
||||
|
||||
getDefaultRows(): RowDefinition[] {
|
||||
return [
|
||||
{ name: 'id', type: 'integer', length: '', numScale: '', value: '', notNull: true, pri: true, auto_increment: true, remark: '主键ID' },
|
||||
{ name: 'creator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '创建人id' },
|
||||
{
|
||||
name: 'id',
|
||||
type: 'integer',
|
||||
length: '',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: true,
|
||||
auto_increment: true,
|
||||
remark: '主键ID'
|
||||
},
|
||||
{
|
||||
name: 'creator_id',
|
||||
type: 'bigint',
|
||||
length: '20',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '创建人id'
|
||||
},
|
||||
{
|
||||
name: 'creator',
|
||||
type: 'varchar',
|
||||
@@ -171,8 +205,28 @@ class SqliteDialect implements DbDialect {
|
||||
auto_increment: false,
|
||||
remark: '创建时间',
|
||||
},
|
||||
{ name: 'updator_id', type: 'bigint', length: '20', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改人id' },
|
||||
{ name: 'updator', type: 'varchar', length: '100', numScale: '', value: '', notNull: true, pri: false, auto_increment: false, remark: '修改姓名' },
|
||||
{
|
||||
name: 'updator_id',
|
||||
type: 'bigint',
|
||||
length: '20',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '修改人id'
|
||||
},
|
||||
{
|
||||
name: 'updator',
|
||||
type: 'varchar',
|
||||
length: '100',
|
||||
numScale: '',
|
||||
value: '',
|
||||
notNull: true,
|
||||
pri: false,
|
||||
auto_increment: false,
|
||||
remark: '修改姓名'
|
||||
},
|
||||
{
|
||||
name: 'update_time',
|
||||
type: 'datetime',
|
||||
@@ -211,6 +265,7 @@ class SqliteDialect implements DbDialect {
|
||||
}
|
||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${nullAble} ${defVal} `;
|
||||
}
|
||||
|
||||
getCreateTableSql(data: any): string {
|
||||
// 创建表结构
|
||||
let fields: string[] = [];
|
||||
@@ -219,7 +274,9 @@ class SqliteDialect implements DbDialect {
|
||||
});
|
||||
|
||||
return `CREATE TABLE ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(data.tableName)}
|
||||
( ${fields.join(',')} )`;
|
||||
(
|
||||
${fields.join(',')}
|
||||
)`;
|
||||
}
|
||||
|
||||
getCreateIndexSql(data: any): string {
|
||||
@@ -227,13 +284,26 @@ class SqliteDialect implements DbDialect {
|
||||
let sql = [] as string[];
|
||||
data.indexs.res.forEach((a: any) => {
|
||||
sql.push(
|
||||
`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(data.db)}.${this.quoteIdentifier(a.indexName)} ON "${data.tableName}" (${a.columnNames.join(',')})`
|
||||
`CREATE
|
||||
${a.unique ? 'UNIQUE' : ''} INDEX
|
||||
${this.quoteIdentifier(data.db)}
|
||||
.
|
||||
${this.quoteIdentifier(a.indexName)}
|
||||
ON
|
||||
"${data.tableName}"
|
||||
(
|
||||
${a.columnNames.join(',')}
|
||||
)`
|
||||
);
|
||||
});
|
||||
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";
|
||||
@@ -270,17 +340,28 @@ class SqliteDialect implements DbDialect {
|
||||
});
|
||||
// 生成sql
|
||||
sql.push(
|
||||
`INSERT INTO ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} (${insertFields.join(',')}) SELECT ${queryFields.join(
|
||||
','
|
||||
)} FROM ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(oldTableName)}`
|
||||
`INSERT INTO ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(tableName)} (${insertFields.join(',')})
|
||||
SELECT ${queryFields.join(
|
||||
','
|
||||
)}
|
||||
FROM ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(oldTableName)}`
|
||||
);
|
||||
|
||||
// 5.创建索引
|
||||
tableData.indexs.res.forEach((a: any) => {
|
||||
a.indexName &&
|
||||
sql.push(
|
||||
`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(tableData.db)}.${this.quoteIdentifier(a.indexName)} ON "${tableName}" (${a.columnNames.join(',')})`
|
||||
);
|
||||
sql.push(
|
||||
`CREATE
|
||||
${a.unique ? 'UNIQUE' : ''} INDEX
|
||||
${this.quoteIdentifier(tableData.db)}
|
||||
.
|
||||
${this.quoteIdentifier(a.indexName)}
|
||||
ON
|
||||
"${tableName}"
|
||||
(
|
||||
${a.columnNames.join(',')}
|
||||
)`
|
||||
);
|
||||
});
|
||||
|
||||
return sql.join(';') + ';';
|
||||
@@ -308,7 +389,14 @@ class SqliteDialect implements DbDialect {
|
||||
|
||||
if (indexData.length > 0) {
|
||||
indexData.forEach((a) => {
|
||||
sql.push(`CREATE ${a.unique ? 'UNIQUE' : ''} INDEX ${this.quoteIdentifier(a.indexName)} ON ${tableName} (${a.columnNames.join(',')})`);
|
||||
sql.push(`CREATE
|
||||
${a.unique ? 'UNIQUE' : ''} INDEX
|
||||
${this.quoteIdentifier(a.indexName)}
|
||||
ON
|
||||
${tableName}
|
||||
(
|
||||
${a.columnNames.join(',')}
|
||||
)`);
|
||||
});
|
||||
}
|
||||
return sql.join(';');
|
||||
|
||||
@@ -19,7 +19,7 @@ const viteConfig: UserConfig = {
|
||||
vue(),
|
||||
CodeInspectorPlugin({
|
||||
bundler: 'vite',
|
||||
editor: VITE_EDITOR,
|
||||
editor: VITE_EDITOR as any,
|
||||
}),
|
||||
],
|
||||
root: process.cwd(),
|
||||
|
||||
@@ -96,7 +96,6 @@ require (
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/cryptox"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"mayfly-go/pkg/utils/writer"
|
||||
"mayfly-go/pkg/ws"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -257,7 +258,7 @@ func (d *Db) DumpSql(rc *req.Ctx) {
|
||||
Tables: tables,
|
||||
DumpDDL: needStruct,
|
||||
DumpData: needData,
|
||||
Writer: rc.GetWriter(),
|
||||
Writer: writer.NewGzipWriter(rc.GetWriter()),
|
||||
}))
|
||||
|
||||
rc.ReqParam = collx.Kvs("db", dbConn.Info, "database", dbName, "tables", tablesStr, "dumpType", dumpType)
|
||||
@@ -338,6 +339,11 @@ func (d *Db) GetTableDDL(rc *req.Ctx) {
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (d *Db) GetVersion(rc *req.Ctx) {
|
||||
version := d.getDbConn(rc).GetMetaData().GetCompatibleDbVersion()
|
||||
rc.ResData = version
|
||||
}
|
||||
|
||||
func (d *Db) GetSchemas(rc *req.Ctx) {
|
||||
res, err := d.getDbConn(rc).GetMetaData().GetSchemas()
|
||||
biz.ErrIsNilAppendErr(err, "获取schemas失败: %s")
|
||||
|
||||
@@ -1,24 +1,54 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mayfly-go/internal/db/api/form"
|
||||
"mayfly-go/internal/db/api/vo"
|
||||
"mayfly-go/internal/db/application"
|
||||
"mayfly-go/internal/db/config"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/dbm/sqlparser"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
msgdto "mayfly-go/internal/msg/application/dto"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/anyx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"mayfly-go/pkg/ws"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DbTransferTask struct {
|
||||
DbTransferTask application.DbTransferTask `inject:"DbTransferTaskApp"`
|
||||
DbTransferFile application.DbTransferFile `inject:"DbTransferFileApp"`
|
||||
DbApp application.Db `inject:""`
|
||||
TagApp tagapp.TagTree `inject:"TagTreeApp"`
|
||||
MsgApp msgapp.Msg `inject:""`
|
||||
DbSqlExecApp application.DbSqlExec `inject:""`
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) Tasks(rc *req.Ctx) {
|
||||
queryCond, page := req.BindQueryAndPage[*entity.DbTransferTaskQuery](rc, new(entity.DbTransferTaskQuery))
|
||||
res, err := d.DbTransferTask.GetPageList(queryCond, page, new([]vo.DbTransferTaskListVO))
|
||||
biz.ErrIsNil(err)
|
||||
|
||||
list := res.List.(*[]vo.DbTransferTaskListVO)
|
||||
for _, item := range *list {
|
||||
item.RunningState = entity.DbTransferTaskRunStateSuccess
|
||||
if d.DbTransferTask.IsRunning(item.Id) {
|
||||
item.RunningState = entity.DbTransferTaskRunStateRunning
|
||||
}
|
||||
}
|
||||
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
@@ -35,11 +65,27 @@ func (d *DbTransferTask) DeleteTask(rc *req.Ctx) {
|
||||
rc.ReqParam = taskId
|
||||
ids := strings.Split(taskId, ",")
|
||||
|
||||
uids := make([]uint64, len(ids))
|
||||
for _, v := range ids {
|
||||
value, err := strconv.Atoi(v)
|
||||
biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s")
|
||||
biz.ErrIsNil(d.DbTransferTask.Delete(rc.MetaCtx, uint64(value)))
|
||||
uids = append(uids, uint64(value))
|
||||
}
|
||||
|
||||
biz.ErrIsNil(d.DbTransferTask.DeleteById(rc.MetaCtx, uids...))
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) ChangeStatus(rc *req.Ctx) {
|
||||
form := &form.DbTransferTaskStatusForm{}
|
||||
task := req.BindJsonAndCopyTo[*entity.DbTransferTask](rc, form, new(entity.DbTransferTask))
|
||||
_ = d.DbTransferTask.UpdateById(rc.MetaCtx, task)
|
||||
|
||||
task, err := d.DbTransferTask.GetById(task.Id)
|
||||
biz.ErrIsNil(err, "该任务不存在")
|
||||
d.DbTransferTask.AddCronJob(rc.MetaCtx, task)
|
||||
|
||||
// 记录请求日志
|
||||
rc.ReqParam = form
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) Run(rc *req.Ctx) {
|
||||
@@ -52,3 +98,140 @@ func (d *DbTransferTask) Run(rc *req.Ctx) {
|
||||
func (d *DbTransferTask) Stop(rc *req.Ctx) {
|
||||
biz.ErrIsNil(d.DbTransferTask.Stop(rc.MetaCtx, uint64(rc.PathParamInt("taskId"))))
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) Files(rc *req.Ctx) {
|
||||
queryCond, page := req.BindQueryAndPage[*entity.DbTransferFileQuery](rc, new(entity.DbTransferFileQuery))
|
||||
res, err := d.DbTransferFile.GetPageList(queryCond, page, new([]vo.DbTransferFileListVO))
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) FileRename(rc *req.Ctx) {
|
||||
fm := &form.DbTransferFileForm{}
|
||||
tFile := req.BindJsonAndCopyTo[*entity.DbTransferFile](rc, fm, new(entity.DbTransferFile))
|
||||
_ = d.DbTransferFile.UpdateById(rc.MetaCtx, tFile)
|
||||
rc.ReqParam = fm
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) FileDel(rc *req.Ctx) {
|
||||
fileId := rc.PathParam("fileId")
|
||||
rc.ReqParam = fileId // 记录操作日志
|
||||
ids := strings.Split(fileId, ",")
|
||||
|
||||
uIds := make([]uint64, len(ids))
|
||||
for _, v := range ids {
|
||||
value, err := strconv.Atoi(v)
|
||||
biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s")
|
||||
uIds = append(uIds, uint64(value))
|
||||
}
|
||||
biz.ErrIsNil(d.DbTransferFile.Delete(rc.MetaCtx, uIds...))
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) FileDown(rc *req.Ctx) {
|
||||
fileUuid := rc.PathParam("fileUuid")
|
||||
if fileUuid == "" {
|
||||
panic(errorx.NewBiz("文件id不能为空"))
|
||||
}
|
||||
|
||||
tFile := &entity.DbTransferFile{FileUuid: fileUuid}
|
||||
|
||||
err := d.DbTransferFile.GetByCond(model.NewModelCond(tFile).Dest(tFile))
|
||||
biz.ErrIsNilAppendErr(err, "查询文件出错 %s")
|
||||
|
||||
// 拼接文件地址,并把文件流输出到客户端
|
||||
brc := config.GetDbBackupRestore()
|
||||
filePath := filepath.Join(fmt.Sprintf("%s/%d/%s.sql", brc.TransferPath, tFile.TaskId, fileUuid))
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
biz.ErrIsNilAppendErr(err, "读取文件失败:%s")
|
||||
|
||||
defer file.Close()
|
||||
|
||||
// Get the file information to set the correct response headers
|
||||
fileInfo, err := file.Stat()
|
||||
biz.ErrIsNilAppendErr(err, "读取文件失败:%s")
|
||||
|
||||
rc.ReqParam = tFile // 记录操作日志
|
||||
// 如果文件名不以 .sql 结尾,则加上 .sql
|
||||
if !strings.HasSuffix(tFile.FileName, ".sql") {
|
||||
tFile.FileName += ".sql"
|
||||
}
|
||||
|
||||
rc.Header("Content-Type", "application/octet-stream")
|
||||
rc.Header("Content-Disposition", "attachment; filename="+tFile.FileName)
|
||||
rc.Header("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))
|
||||
_, err = io.Copy(rc.GetWriter(), file)
|
||||
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) FileRun(rc *req.Ctx) {
|
||||
|
||||
fm := req.BindJsonAndValid(rc, &form.DbTransferFileRunForm{})
|
||||
|
||||
rc.ReqParam = fm
|
||||
|
||||
tFile, err := d.DbTransferFile.GetById(fm.Id)
|
||||
biz.IsTrue(tFile != nil && err == nil, "文件不存在")
|
||||
|
||||
targetDbConn, err := d.DbApp.GetDbConn(fm.TargetDbId, fm.TargetDbName)
|
||||
biz.ErrIsNilAppendErr(err, "连接目标数据库失败: %s")
|
||||
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, targetDbConn.Info.CodePath...), "%s")
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
errInfo := anyx.ToString(err)
|
||||
if len(errInfo) > 300 {
|
||||
errInfo = errInfo[:300] + "..."
|
||||
}
|
||||
d.MsgApp.CreateAndSend(rc.GetLoginAccount(), msgdto.ErrSysMsg("sql脚本执行失败", fmt.Sprintf("[%s][%s]执行失败: [%s]", tFile.FileName, targetDbConn.Info.GetLogDesc(), errInfo)).WithClientId(fm.ClientId))
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
d.fileRun(rc.GetLoginAccount(), fm, tFile, targetDbConn)
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) fileRun(la *model.LoginAccount, fm *form.DbTransferFileRunForm, tFile *entity.DbTransferFile, targetDbConn *dbi.DbConn) {
|
||||
|
||||
filePath := d.DbTransferFile.GetFilePath(tFile)
|
||||
_, err := os.Stat(filePath)
|
||||
biz.ErrIsNilAppendErr(err, "sql文件不存在:%s")
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
biz.ErrIsNilAppendErr(err, "sql文件读取出错:%s")
|
||||
|
||||
executedStatements := 0
|
||||
progressId := stringx.Rand(32)
|
||||
laId := la.Id
|
||||
|
||||
ticker := time.NewTicker(time.Second * 1)
|
||||
defer ticker.Stop()
|
||||
|
||||
if err != nil {
|
||||
biz.ErrIsNilAppendErr(err, "连接目标数据库失败: %s")
|
||||
}
|
||||
|
||||
err = sqlparser.SQLSplit(file, func(sql string) error {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ws.SendJsonMsg(ws.UserId(laId), fm.ClientId, msgdto.InfoSqlProgressMsg("sql脚本执行进度", &progressMsg{
|
||||
Id: progressId,
|
||||
Title: tFile.FileName,
|
||||
ExecutedStatements: executedStatements,
|
||||
Terminated: false,
|
||||
}).WithCategory(progressCategory))
|
||||
default:
|
||||
}
|
||||
executedStatements++
|
||||
_, err = targetDbConn.Exec(sql)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
biz.ErrIsNilAppendErr(err, "执行sql失败: %s")
|
||||
}
|
||||
|
||||
d.MsgApp.CreateAndSend(la, msgdto.SuccessSysMsg("sql脚本执行成功", fmt.Sprintf("sql脚本执行完成:%s", tFile.FileName)).WithClientId(fm.ClientId))
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type DataSyncTaskForm struct {
|
||||
PageSize int `binding:"required" json:"pageSize"`
|
||||
UpdField string `binding:"required" json:"updField"`
|
||||
UpdFieldVal string `binding:"required" json:"updFieldVal"`
|
||||
UpdFieldSrc string `json:"updFieldSrc"`
|
||||
|
||||
TargetDbId int64 `binding:"required" json:"targetDbId"`
|
||||
TargetDbName string `binding:"required" json:"targetDbName"`
|
||||
|
||||
@@ -1,20 +1,43 @@
|
||||
package form
|
||||
|
||||
type DbTransferTaskForm struct {
|
||||
Id uint64 `json:"id"`
|
||||
TaskName string `binding:"required" json:"taskName"` // 任务名称
|
||||
CheckedKeys string `binding:"required" json:"checkedKeys"` // 选中需要迁移的表
|
||||
DeleteTable int `binding:"required" json:"deleteTable"` // 创建表前是否删除表 1是 2否
|
||||
NameCase int `binding:"required" json:"nameCase"` // 表名、字段大小写转换 1无 2大写 3小写
|
||||
Strategy int `binding:"required" json:"strategy"` // 迁移策略 1全量 2增量
|
||||
SrcDbId int `binding:"required" json:"srcDbId"` // 源库id
|
||||
SrcDbName string `binding:"required" json:"srcDbName"` // 源库名
|
||||
SrcDbType string `binding:"required" json:"srcDbType"` // 源库类型
|
||||
SrcInstName string `binding:"required" json:"srcInstName"` // 源库实例名
|
||||
SrcTagPath string `binding:"required" json:"srcTagPath"` // 源库tagPath
|
||||
TargetDbId int `binding:"required" json:"targetDbId"` // 目标库id
|
||||
TargetDbName string `binding:"required" json:"targetDbName"` // 目标库名
|
||||
TargetDbType string `binding:"required" json:"targetDbType"` // 目标库类型
|
||||
TargetInstName string `binding:"required" json:"targetInstName"` // 目标库实例名
|
||||
TargetTagPath string `binding:"required" json:"targetTagPath"` // 目标库tagPath
|
||||
Id uint64 `json:"id"`
|
||||
|
||||
TaskName string `binding:"required" json:"taskName"` // 任务名称
|
||||
CronAble int `json:"cronAble"` // 是否定时 1是 -1否
|
||||
Cron string `json:"cron"` // 定时任务cron表达式
|
||||
Mode int `binding:"required" json:"mode"` // 数据迁移方式,1、迁移到数据库 2、迁移到文件
|
||||
TargetFileDbType string `json:"targetFileDbType"` // 目标文件数据库类型
|
||||
Status int `json:"status" form:"status"` // 启用状态 1启用 -1禁用
|
||||
|
||||
CheckedKeys string `binding:"required" json:"checkedKeys"` // 选中需要迁移的表
|
||||
DeleteTable int `binding:"required" json:"deleteTable"` // 创建表前是否删除表 1是 2否
|
||||
NameCase int `binding:"required" json:"nameCase"` // 表名、字段大小写转换 1无 2大写 3小写
|
||||
Strategy int `binding:"required" json:"strategy"` // 迁移策略 1全量 2增量
|
||||
|
||||
SrcDbId int `binding:"required" json:"srcDbId"` // 源库id
|
||||
SrcDbName string `binding:"required" json:"srcDbName"` // 源库名
|
||||
SrcDbType string `binding:"required" json:"srcDbType"` // 源库类型
|
||||
SrcInstName string `binding:"required" json:"srcInstName"` // 源库实例名
|
||||
SrcTagPath string `binding:"required" json:"srcTagPath"` // 源库tagPath
|
||||
|
||||
TargetDbId int `json:"targetDbId"` // 目标库id
|
||||
TargetDbName string `json:"targetDbName"` // 目标库名
|
||||
TargetDbType string `json:"targetDbType"` // 目标库类型
|
||||
TargetInstName string `json:"targetInstName"` // 目标库实例名
|
||||
TargetTagPath string `json:"targetTagPath"` // 目标库tagPath
|
||||
}
|
||||
type DbTransferTaskStatusForm struct {
|
||||
Id uint64 `binding:"required" json:"taskId" form:"taskId"`
|
||||
Status int `json:"status" form:"status"`
|
||||
}
|
||||
type DbTransferFileForm struct {
|
||||
Id uint64 `json:"id"`
|
||||
FileName string `json:"fileName" form:"fileName"`
|
||||
}
|
||||
type DbTransferFileRunForm struct {
|
||||
Id uint64 `json:"id"` // 文件ID
|
||||
TargetDbId uint64 `json:"targetDbId" form:"targetDbId"` // 需要执行sql的数据库id
|
||||
TargetDbName string `json:"targetDbName" form:"targetDbName"` // 需要执行sql的数据库名
|
||||
ClientId string `json:"clientId" form:"clientId"` // 客户端的唯一id,用于消息回传
|
||||
}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
package vo
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type DbTransferTaskListVO struct {
|
||||
Id *int64 `json:"id"`
|
||||
Id uint64 `json:"id"`
|
||||
CreateTime *time.Time `json:"createTime"`
|
||||
Creator string `json:"creator"`
|
||||
UpdateTime *time.Time `json:"updateTime"`
|
||||
Modifier string `json:"modifier"`
|
||||
|
||||
RunningState int `json:"runningState"`
|
||||
LogId uint64 `json:"logId"`
|
||||
TaskName string `json:"taskName"` // 任务名称
|
||||
RunningState int8 `json:"runningState"`
|
||||
LogId uint64 `json:"logId"`
|
||||
TaskName string `json:"taskName"` // 任务名称
|
||||
Status int `json:"status"` // 任务状态 1启用 -1禁用
|
||||
CronAble int `json:"cronAble"` // 是否定时 1是 -1否
|
||||
Cron string `json:"cron"` // 定时任务cron表达式
|
||||
Mode int `json:"mode"` // 数据迁移方式,1、迁移到数据库 2、迁移到文件
|
||||
TargetFileDbType string `json:"targetFileDbType"` // 目标文件数据库类型
|
||||
|
||||
CheckedKeys string `json:"checkedKeys"` // 选中需要迁移的表
|
||||
DeleteTable int `json:"deleteTable"` // 创建表前是否删除表
|
||||
|
||||
13
server/internal/db/api/vo/db_transfer_file.go
Normal file
13
server/internal/db/api/vo/db_transfer_file.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package vo
|
||||
|
||||
import "time"
|
||||
|
||||
type DbTransferFileListVO struct {
|
||||
Id *int64 `json:"id"`
|
||||
CreateTime *time.Time `json:"createTime"`
|
||||
Status int8 `json:"status"`
|
||||
FileDbType string `json:"fileDbType"`
|
||||
FileName string `json:"fileName"`
|
||||
FileUuid string `json:"fileUuid"`
|
||||
LogId uint64 `json:"logId"` // 日志ID
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/pkg/ioc"
|
||||
"sync"
|
||||
)
|
||||
@@ -13,6 +12,7 @@ func InitIoc() {
|
||||
ioc.Register(new(dbSqlAppImpl), ioc.WithComponentName("DbSqlApp"))
|
||||
ioc.Register(new(dataSyncAppImpl), ioc.WithComponentName("DbDataSyncTaskApp"))
|
||||
ioc.Register(new(dbTransferAppImpl), ioc.WithComponentName("DbTransferTaskApp"))
|
||||
ioc.Register(new(dbTransferFileAppImpl), ioc.WithComponentName("DbTransferFileApp"))
|
||||
|
||||
ioc.Register(newDbScheduler(), ioc.WithComponentName("DbScheduler"))
|
||||
ioc.Register(new(DbBackupApp), ioc.WithComponentName("DbBackupApp"))
|
||||
@@ -22,17 +22,17 @@ func InitIoc() {
|
||||
|
||||
func Init() {
|
||||
sync.OnceFunc(func() {
|
||||
if err := GetDbBackupApp().Init(); err != nil {
|
||||
panic(fmt.Sprintf("初始化 DbBackupApp 失败: %v", err))
|
||||
}
|
||||
if err := GetDbRestoreApp().Init(); err != nil {
|
||||
panic(fmt.Sprintf("初始化 DbRestoreApp 失败: %v", err))
|
||||
}
|
||||
if err := GetDbBinlogApp().Init(); err != nil {
|
||||
panic(fmt.Sprintf("初始化 DbBinlogApp 失败: %v", err))
|
||||
}
|
||||
//if err := GetDbBackupApp().Init(); err != nil {
|
||||
// panic(fmt.Sprintf("初始化 DbBackupApp 失败: %v", err))
|
||||
//}
|
||||
//if err := GetDbRestoreApp().Init(); err != nil {
|
||||
// panic(fmt.Sprintf("初始化 DbRestoreApp 失败: %v", err))
|
||||
//}
|
||||
//if err := GetDbBinlogApp().Init(); err != nil {
|
||||
// panic(fmt.Sprintf("初始化 DbBinlogApp 失败: %v", err))
|
||||
//}
|
||||
GetDataSyncTaskApp().InitCronJob()
|
||||
GetDbTransferTaskApp().InitJob()
|
||||
GetDbTransferTaskApp().InitCronJob()
|
||||
InitDbFlowHandler()
|
||||
})()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/application/dto"
|
||||
@@ -224,7 +225,13 @@ func (d *dbAppImpl) GetDbConnByInstanceId(instanceId uint64) (*dbi.DbConn, error
|
||||
}
|
||||
|
||||
func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
|
||||
writer := newGzipWriter(reqParam.Writer)
|
||||
|
||||
log := dto.DefaultDumpLog
|
||||
if reqParam.Log != nil {
|
||||
log = reqParam.Log
|
||||
}
|
||||
|
||||
writer := reqParam.Writer
|
||||
defer writer.Close()
|
||||
dbId := reqParam.DbId
|
||||
dbName := reqParam.DbName
|
||||
@@ -238,23 +245,47 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
|
||||
writer.WriteString("\n-- 导出平台: mayfly-go")
|
||||
writer.WriteString(fmt.Sprintf("\n-- 导出时间: %s ", time.Now().Format("2006-01-02 15:04:05")))
|
||||
writer.WriteString(fmt.Sprintf("\n-- 导出数据库: %s ", dbName))
|
||||
writer.WriteString(fmt.Sprintf("\n-- 数据库方言: %s ", cmp.Or(reqParam.TargetDbType, dbConn.Info.Type)))
|
||||
writer.WriteString("\n-- ----------------------------\n\n")
|
||||
|
||||
dbMeta := dbConn.GetMetaData()
|
||||
// 获取目标元数据,仅生成sql,用于生成建表语句和插入数据,不能用于查询
|
||||
targetMeta := dbConn.GetMetaData()
|
||||
if reqParam.TargetDbType != "" && dbConn.Info.Type != reqParam.TargetDbType {
|
||||
// 创建一个假连接,仅用于调用方言生成sql,不做数据库连接操作
|
||||
meta := dbi.GetMeta(reqParam.TargetDbType)
|
||||
dbConn := &dbi.DbConn{Info: &dbi.DbInfo{
|
||||
Type: reqParam.TargetDbType,
|
||||
Meta: meta,
|
||||
}}
|
||||
|
||||
targetMeta = meta.GetMetaData(dbConn)
|
||||
}
|
||||
|
||||
srcMeta := dbConn.GetMetaData()
|
||||
if len(tables) == 0 {
|
||||
ti, err := dbMeta.GetTables()
|
||||
log("获取可导出的表信息...")
|
||||
ti, err := srcMeta.GetTables()
|
||||
if err != nil {
|
||||
log(fmt.Sprintf("获取表信息失败 %s", err.Error()))
|
||||
}
|
||||
biz.ErrIsNil(err)
|
||||
tables = make([]string, len(ti))
|
||||
for i, table := range ti {
|
||||
tables[i] = table.TableName
|
||||
}
|
||||
log(fmt.Sprintf("获取到%d张表", len(tables)))
|
||||
}
|
||||
if len(tables) == 0 {
|
||||
log("不存在可导出的表, 结束导出")
|
||||
return errorx.NewBiz("不存在可导出的表")
|
||||
}
|
||||
|
||||
log("查询列信息...")
|
||||
// 查询列信息,后面生成建表ddl和insert都需要列信息
|
||||
columns, err := dbMeta.GetColumns(tables...)
|
||||
columns, err := srcMeta.GetColumns(tables...)
|
||||
if err != nil {
|
||||
log(fmt.Sprintf("查询列信息失败:%s", err.Error()))
|
||||
}
|
||||
biz.ErrIsNil(err)
|
||||
|
||||
// 以表名分组,存放每个表的列信息
|
||||
@@ -266,21 +297,24 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
|
||||
// 按表名排序
|
||||
sort.Strings(tables)
|
||||
|
||||
quoteSchema := dbMeta.QuoteIdentifier(dbConn.Info.CurrentSchema())
|
||||
dumpHelper := dbMeta.GetDumpHelper()
|
||||
dataHelper := dbMeta.GetDataHelper()
|
||||
quoteSchema := srcMeta.QuoteIdentifier(dbConn.Info.CurrentSchema())
|
||||
dumpHelper := targetMeta.GetDumpHelper()
|
||||
dataHelper := targetMeta.GetDataHelper()
|
||||
|
||||
// 遍历获取每个表的信息
|
||||
for _, tableName := range tables {
|
||||
quoteTableName := dbMeta.QuoteIdentifier(tableName)
|
||||
log(fmt.Sprintf("获取表[%s]信息...", tableName))
|
||||
quoteTableName := targetMeta.QuoteIdentifier(tableName)
|
||||
|
||||
writer.TryFlush()
|
||||
// 查询表信息,主要是为了查询表注释
|
||||
tbs, err := dbMeta.GetTables(tableName)
|
||||
tbs, err := srcMeta.GetTables(tableName)
|
||||
if err != nil {
|
||||
log(fmt.Sprintf("获取表[%s]信息失败: %s", tableName, err.Error()))
|
||||
return err
|
||||
}
|
||||
if len(tbs) <= 0 {
|
||||
log(fmt.Sprintf("获取表[%s]信息失败: 没有查询到表信息", tableName))
|
||||
return errorx.NewBiz(fmt.Sprintf("获取表信息失败:%s", tableName))
|
||||
}
|
||||
tabInfo := dbi.Table{
|
||||
@@ -290,8 +324,9 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
|
||||
|
||||
// 生成表结构信息
|
||||
if reqParam.DumpDDL {
|
||||
log(fmt.Sprintf("生成表[%s]DDL...", tableName))
|
||||
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表结构: %s \n-- ----------------------------\n", tableName))
|
||||
tbDdlArr := dbMeta.GenerateTableDDL(columnMap[tableName], tabInfo, true)
|
||||
tbDdlArr := targetMeta.GenerateTableDDL(columnMap[tableName], tabInfo, true)
|
||||
for _, ddl := range tbDdlArr {
|
||||
writer.WriteString(ddl + ";\n")
|
||||
}
|
||||
@@ -299,16 +334,17 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
|
||||
|
||||
// 生成insert sql,数据在索引前,加速insert
|
||||
if reqParam.DumpData {
|
||||
log(fmt.Sprintf("生成表[%s]DML...", tableName))
|
||||
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表数据: %s \n-- ----------------------------\n", tableName))
|
||||
|
||||
dumpHelper.BeforeInsert(writer, quoteTableName)
|
||||
// 获取列信息
|
||||
quoteColNames := make([]string, 0)
|
||||
for _, col := range columnMap[tableName] {
|
||||
quoteColNames = append(quoteColNames, dbMeta.QuoteIdentifier(col.ColumnName))
|
||||
quoteColNames = append(quoteColNames, targetMeta.QuoteIdentifier(col.ColumnName))
|
||||
}
|
||||
|
||||
_, _ = dbConn.WalkTableRows(ctx, quoteTableName, func(row map[string]any, _ []*dbi.QueryColumn) error {
|
||||
_, _ = dbConn.WalkTableRows(ctx, tableName, func(row map[string]any, _ []*dbi.QueryColumn) error {
|
||||
rowValues := make([]string, len(columnMap[tableName]))
|
||||
for i, col := range columnMap[tableName] {
|
||||
rowValues[i] = dataHelper.WrapValue(row[col.ColumnName], dataHelper.GetDataType(string(col.DataType)))
|
||||
@@ -323,15 +359,18 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
|
||||
dumpHelper.AfterInsert(writer, tableName, columnMap[tableName])
|
||||
}
|
||||
|
||||
indexs, err := dbMeta.GetTableIndex(tableName)
|
||||
log(fmt.Sprintf("获取表[%s]索引信息...", tableName))
|
||||
indexs, err := srcMeta.GetTableIndex(tableName)
|
||||
if err != nil {
|
||||
log(fmt.Sprintf("获取表[%s]索引信息失败:%s", tableName, err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(indexs) > 0 {
|
||||
// 最后添加索引
|
||||
log(fmt.Sprintf("生成表[%s]索引...", tableName))
|
||||
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表索引: %s \n-- ----------------------------\n", tableName))
|
||||
sqlArr := dbMeta.GenerateIndexDDL(indexs, tabInfo)
|
||||
sqlArr := targetMeta.GenerateIndexDDL(indexs, tabInfo)
|
||||
for _, sqlStr := range sqlArr {
|
||||
writer.WriteString(sqlStr + ";\n")
|
||||
}
|
||||
|
||||
@@ -52,8 +52,9 @@ type dataSyncAppImpl struct {
|
||||
}
|
||||
|
||||
var (
|
||||
dateTimeReg = regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`)
|
||||
whereReg = regexp.MustCompile(`(?i)where`)
|
||||
dateTimeReg = regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`)
|
||||
dateTimeIsoReg = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*$`)
|
||||
whereReg = regexp.MustCompile(`(?i)where`)
|
||||
)
|
||||
|
||||
func (app *dataSyncAppImpl) InjectDbDataSyncTaskRepo(repo repository.DataSyncTask) {
|
||||
@@ -155,7 +156,7 @@ func (app *dataSyncAppImpl) RunCronJob(ctx context.Context, id uint64) error {
|
||||
// 判断UpdFieldVal数据类型
|
||||
var updFieldValType dbi.DataType
|
||||
if _, err = strconv.Atoi(task.UpdFieldVal); err != nil {
|
||||
if dateTimeReg.MatchString(task.UpdFieldVal) {
|
||||
if dateTimeReg.MatchString(task.UpdFieldVal) || dateTimeIsoReg.MatchString(task.UpdFieldVal) {
|
||||
updFieldValType = dbi.DataTypeDateTime
|
||||
} else {
|
||||
updFieldValType = dbi.DataTypeString
|
||||
@@ -328,13 +329,22 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
||||
|
||||
data = append(data, rowData)
|
||||
}
|
||||
// 解决字段大小写问题
|
||||
updFieldVal := srcRes[len(srcRes)-1][strings.ToUpper(updFieldName)]
|
||||
if updFieldVal == "" || updFieldVal == nil {
|
||||
updFieldVal = srcRes[len(srcRes)-1][strings.ToLower(updFieldName)]
|
||||
|
||||
setUpdateFieldVal := func(field string) {
|
||||
// 解决字段大小写问题
|
||||
updFieldVal := srcRes[len(srcRes)-1][strings.ToUpper(field)]
|
||||
if updFieldVal == "" || updFieldVal == nil {
|
||||
updFieldVal = srcRes[len(srcRes)-1][strings.ToLower(field)]
|
||||
}
|
||||
task.UpdFieldVal = srcMetaData.GetDataHelper().FormatData(updFieldVal, updFieldType)
|
||||
}
|
||||
|
||||
task.UpdFieldVal = srcMetaData.GetDataHelper().FormatData(updFieldVal, updFieldType)
|
||||
// 如果指定了更新字段,则以更新字段取值
|
||||
if task.UpdFieldSrc != "" {
|
||||
setUpdateFieldVal(task.UpdFieldSrc)
|
||||
} else {
|
||||
setUpdateFieldVal(updFieldName)
|
||||
}
|
||||
|
||||
// 获取目标库字段数组
|
||||
targetWrapColumns := make([]string, 0)
|
||||
|
||||
@@ -17,14 +17,16 @@ import (
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/jsonx"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DbSqlExecReq struct {
|
||||
DbId uint64
|
||||
Db string
|
||||
Sql string // 需要执行的sql,支持多条
|
||||
Remark string // 执行备注
|
||||
Sql string // 需要执行的sql,支持多条
|
||||
SqlFile *os.File // sql文件
|
||||
Remark string // 执行备注
|
||||
DbConn *dbi.DbConn
|
||||
CheckFlow bool // 是否检查存储审批流程
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"mayfly-go/internal/db/application/dto"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
@@ -11,10 +14,14 @@ import (
|
||||
sysentity "mayfly-go/internal/sys/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/cache"
|
||||
"mayfly-go/pkg/contextx"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/scheduler"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/writer"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -32,20 +39,27 @@ type DbTransferTask interface {
|
||||
|
||||
Delete(ctx context.Context, id uint64) error
|
||||
|
||||
InitJob()
|
||||
InitCronJob()
|
||||
|
||||
AddCronJob(ctx context.Context, taskEntity *entity.DbTransferTask)
|
||||
|
||||
RemoveCronJobById(taskId uint64)
|
||||
|
||||
CreateLog(ctx context.Context, taskId uint64) (uint64, error)
|
||||
|
||||
Run(ctx context.Context, taskId uint64, logId uint64)
|
||||
|
||||
IsRunning(taskId uint64) bool
|
||||
|
||||
Stop(ctx context.Context, taskId uint64) error
|
||||
}
|
||||
|
||||
type dbTransferAppImpl struct {
|
||||
base.AppImpl[*entity.DbTransferTask, repository.DbTransferTask]
|
||||
|
||||
dbApp Db `inject:"DbApp"`
|
||||
logApp sysapp.Syslog `inject:"SyslogApp"`
|
||||
dbApp Db `inject:"DbApp"`
|
||||
logApp sysapp.Syslog `inject:"SyslogApp"`
|
||||
fileApp DbTransferFile `inject:"DbTransferFileApp"`
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) InjectDbTransferTaskRepo(repo repository.DbTransferTask) {
|
||||
@@ -58,23 +72,97 @@ func (app *dbTransferAppImpl) GetPageList(condition *entity.DbTransferTaskQuery,
|
||||
|
||||
func (app *dbTransferAppImpl) Save(ctx context.Context, taskEntity *entity.DbTransferTask) error {
|
||||
var err error
|
||||
if taskEntity.Id == 0 {
|
||||
if taskEntity.Id == 0 { // 新建时生成key
|
||||
taskEntity.TaskKey = uuid.New().String()
|
||||
err = app.Insert(ctx, taskEntity)
|
||||
} else {
|
||||
err = app.UpdateById(ctx, taskEntity)
|
||||
}
|
||||
return err
|
||||
// 视情况添加或删除任务
|
||||
task, err := app.GetById(taskEntity.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
app.AddCronJob(ctx, task)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) Delete(ctx context.Context, id uint64) error {
|
||||
if err := app.DeleteById(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
app.RemoveCronJobById(id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) InitJob() {
|
||||
app.UpdateByCond(context.TODO(), &entity.DbTransferTask{RunningState: entity.DbTransferTaskRunStateStop}, &entity.DbTransferTask{RunningState: entity.DbTransferTaskRunStateRunning})
|
||||
func (app *dbTransferAppImpl) AddCronJob(ctx context.Context, taskEntity *entity.DbTransferTask) {
|
||||
key := taskEntity.TaskKey
|
||||
// 先移除旧的任务
|
||||
scheduler.RemoveByKey(key)
|
||||
|
||||
// 根据状态添加新的任务
|
||||
if taskEntity.Status == entity.DbTransferTaskStatusEnable && taskEntity.CronAble == entity.DbTransferTaskCronAbleEnable {
|
||||
if key == "" {
|
||||
taskEntity.TaskKey = uuid.New().String()
|
||||
key = taskEntity.TaskKey
|
||||
_ = app.UpdateById(ctx, taskEntity)
|
||||
}
|
||||
|
||||
taskId := taskEntity.Id
|
||||
scheduler.AddFunByKey(key, taskEntity.Cron, func() {
|
||||
logx.Infof("开始执行同步任务: %d", taskId)
|
||||
logId, _ := app.CreateLog(ctx, taskId)
|
||||
app.Run(ctx, taskId, logId)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) InitCronJob() {
|
||||
// 重启后,把正在运行的状态设置为停止
|
||||
_ = app.UpdateByCond(context.TODO(), &entity.DbTransferTask{RunningState: entity.DbTransferTaskRunStateStop}, &entity.DbTransferTask{RunningState: entity.DbTransferTaskRunStateRunning})
|
||||
ent := &entity.DbTransferTask{}
|
||||
list, err := app.ListByCond(model.NewModelCond(ent).Columns("id"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(list) > 0 {
|
||||
// 移除所有正在运行的任务
|
||||
for _, task := range list {
|
||||
app.MarkStop(task.Id)
|
||||
}
|
||||
}
|
||||
// 把所有运行中的文件状态设置为失败
|
||||
_ = app.fileApp.UpdateByCond(context.TODO(), &entity.DbTransferFile{Status: entity.DbTransferFileStatusFail}, &entity.DbTransferFile{Status: entity.DbTransferFileStatusRunning})
|
||||
|
||||
// 把所有需要定时执行的任务添加到定时任务中
|
||||
pageParam := &model.PageParam{
|
||||
PageSize: 100,
|
||||
PageNum: 1,
|
||||
}
|
||||
cond := new(entity.DbTransferTaskQuery)
|
||||
cond.Status = entity.DbTransferTaskStatusEnable
|
||||
cond.CronAble = entity.DbTransferTaskCronAbleEnable
|
||||
jobs := new([]entity.DbTransferTask)
|
||||
|
||||
pr, _ := app.GetPageList(cond, pageParam, jobs)
|
||||
if nil == pr || pr.Total == 0 {
|
||||
return
|
||||
}
|
||||
total := pr.Total
|
||||
add := 0
|
||||
|
||||
for {
|
||||
for _, job := range *jobs {
|
||||
app.AddCronJob(contextx.NewTraceId(), &job)
|
||||
add++
|
||||
}
|
||||
if add >= int(total) {
|
||||
return
|
||||
}
|
||||
pageParam.PageNum++
|
||||
_, _ = app.GetPageList(cond, pageParam, jobs)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) CreateLog(ctx context.Context, taskId uint64) (uint64, error) {
|
||||
@@ -88,7 +176,6 @@ func (app *dbTransferAppImpl) CreateLog(ctx context.Context, taskId uint64) (uin
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint64) {
|
||||
defer app.logApp.Flush(logId, true)
|
||||
|
||||
task, err := app.GetById(taskId)
|
||||
if err != nil {
|
||||
@@ -97,7 +184,7 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint
|
||||
}
|
||||
|
||||
if app.IsRunning(taskId) {
|
||||
logx.Warnf("[%d]该任务正在运行中...", taskId)
|
||||
logx.Panicf("[%d]该任务正在运行中...", taskId)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -111,8 +198,7 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint
|
||||
}
|
||||
|
||||
// 标记该任务开始执行
|
||||
app.MarkRuning(taskId)
|
||||
defer app.MarkStop(taskId)
|
||||
app.MarkRunning(taskId)
|
||||
|
||||
// 获取源库连接、目标库连接,判断连接可用性,否则记录日志:xx连接不可用
|
||||
// 获取源库表信息
|
||||
@@ -121,15 +207,9 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint
|
||||
app.EndTransfer(ctx, logId, taskId, "获取源库连接失败", err, nil)
|
||||
return
|
||||
}
|
||||
// 获取目标库表信息
|
||||
targetConn, err := app.dbApp.GetDbConn(uint64(task.TargetDbId), task.TargetDbName)
|
||||
if err != nil {
|
||||
app.EndTransfer(ctx, logId, taskId, "获取目标库连接失败", err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取迁移表信息
|
||||
var tables []dbi.Table
|
||||
|
||||
if task.CheckedKeys == "all" {
|
||||
tables, err = srcConn.GetMetaData().GetTables()
|
||||
if err != nil {
|
||||
@@ -145,8 +225,28 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移到文件或数据库
|
||||
if task.Mode == entity.DbTransferTaskModeFile {
|
||||
app.transfer2File(ctx, taskId, logId, task, srcConn, start, tables)
|
||||
} else if task.Mode == entity.DbTransferTaskModeDb {
|
||||
defer app.MarkStop(taskId)
|
||||
defer app.logApp.Flush(logId, true)
|
||||
app.transfer2Db(ctx, taskId, logId, task, srcConn, start, tables)
|
||||
} else {
|
||||
app.EndTransfer(ctx, logId, taskId, "迁移模式出错,目前仅支持迁移到文件或数据库", err, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) transfer2Db(ctx context.Context, taskId uint64, logId uint64, task *entity.DbTransferTask, srcConn *dbi.DbConn, start time.Time, tables []dbi.Table) {
|
||||
// 获取目标库表信息
|
||||
targetConn, err := app.dbApp.GetDbConn(uint64(task.TargetDbId), task.TargetDbName)
|
||||
if err != nil {
|
||||
app.EndTransfer(ctx, logId, taskId, "获取目标库连接失败", err, nil)
|
||||
return
|
||||
}
|
||||
// 迁移表
|
||||
if err = app.transferTables(ctx, logId, task, srcConn, targetConn, tables); err != nil {
|
||||
if err = app.transferDbTables(ctx, logId, task, srcConn, targetConn, tables); err != nil {
|
||||
app.EndTransfer(ctx, logId, taskId, "迁移表失败", err, nil)
|
||||
return
|
||||
}
|
||||
@@ -154,6 +254,67 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint
|
||||
app.EndTransfer(ctx, logId, taskId, fmt.Sprintf("执行迁移完成,执行迁移任务[taskId = %d]完成, 耗时:%v", taskId, time.Since(start)), nil, nil)
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) transfer2File(ctx context.Context, taskId uint64, logId uint64, task *entity.DbTransferTask, srcConn *dbi.DbConn, start time.Time, tables []dbi.Table) {
|
||||
|
||||
// 1、新增迁移文件数据
|
||||
nowTime := time.Now()
|
||||
tFile := &entity.DbTransferFile{
|
||||
TaskId: taskId,
|
||||
CreateTime: &nowTime,
|
||||
Status: entity.DbTransferFileStatusRunning,
|
||||
FileDbType: cmp.Or(task.TargetFileDbType, task.TargetDbType),
|
||||
FileName: fmt.Sprintf("%s.sql", task.TaskName), // 用于下载和展示
|
||||
FileUuid: uuid.New().String(), // 用于存放到磁盘
|
||||
LogId: logId,
|
||||
}
|
||||
_ = app.fileApp.Save(ctx, tFile)
|
||||
|
||||
// 新建一个文件,文件位置为 {transferPath}/{taskId}/{uuid}.sql
|
||||
filePath := app.fileApp.GetFilePath(tFile)
|
||||
|
||||
// 从tables提取表名
|
||||
tableNames := make([]string, 0)
|
||||
for _, table := range tables {
|
||||
tableNames = append(tableNames, table.TableName)
|
||||
}
|
||||
// 2、把源库数据迁移到文件
|
||||
app.Log(ctx, logId, fmt.Sprintf("开始迁移表数据到文件: %s", filePath))
|
||||
|
||||
app.Log(ctx, logId, fmt.Sprintf("目标库文件语言类型: %s", task.TargetFileDbType))
|
||||
|
||||
go func() {
|
||||
defer app.MarkStop(taskId)
|
||||
defer app.logApp.Flush(logId, true)
|
||||
ctx = context.Background()
|
||||
err := app.dbApp.DumpDb(ctx, &dto.DumpDb{
|
||||
LogId: logId,
|
||||
DbId: uint64(task.SrcDbId),
|
||||
DbName: task.SrcDbName,
|
||||
TargetDbType: dbi.DbType(task.TargetFileDbType),
|
||||
Tables: tableNames,
|
||||
DumpDDL: true,
|
||||
DumpData: true,
|
||||
Writer: writer.NewFileWriter(filePath),
|
||||
Log: func(msg string) { // 记录日志
|
||||
app.Log(ctx, logId, msg)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
app.EndTransfer(ctx, logId, taskId, "数据库迁移失败", err, nil)
|
||||
tFile.Status = entity.DbTransferFileStatusFail
|
||||
_ = app.fileApp.UpdateById(ctx, tFile)
|
||||
// 删除文件
|
||||
_ = os.Remove(filePath)
|
||||
return
|
||||
}
|
||||
app.EndTransfer(ctx, logId, taskId, "数据库迁移完成", err, nil)
|
||||
|
||||
tFile.Status = entity.DbTransferFileStatusSuccess
|
||||
_ = app.fileApp.UpdateById(ctx, tFile)
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) Stop(ctx context.Context, taskId uint64) error {
|
||||
task, err := app.GetById(taskId)
|
||||
if err != nil {
|
||||
@@ -173,7 +334,7 @@ func (app *dbTransferAppImpl) Stop(ctx context.Context, taskId uint64) error {
|
||||
}
|
||||
|
||||
// 迁移表
|
||||
func (app *dbTransferAppImpl) transferTables(ctx context.Context, logId uint64, task *entity.DbTransferTask, srcConn *dbi.DbConn, targetConn *dbi.DbConn, tables []dbi.Table) error {
|
||||
func (app *dbTransferAppImpl) transferDbTables(ctx context.Context, logId uint64, task *entity.DbTransferTask, srcConn *dbi.DbConn, targetConn *dbi.DbConn, tables []dbi.Table) error {
|
||||
tableNames := make([]string, 0)
|
||||
tableMap := make(map[string]dbi.Table) // 以表名分组,存放表信息
|
||||
for _, table := range tables {
|
||||
@@ -255,10 +416,7 @@ func (app *dbTransferAppImpl) transferTables(ctx context.Context, logId uint64,
|
||||
})
|
||||
}
|
||||
|
||||
if err := errGroup.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return errGroup.Wait()
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) transferData(ctx context.Context, logId uint64, taskId uint64, tableName string, targetColumns []dbi.Column, srcConn *dbi.DbConn, targetConn *dbi.DbConn) (int, error) {
|
||||
@@ -386,8 +544,8 @@ func (app *dbTransferAppImpl) transferIndex(_ context.Context, tableInfo dbi.Tab
|
||||
return targetDialect.CreateIndex(tableInfo, indexs)
|
||||
}
|
||||
|
||||
// MarkRuning 标记任务执行中
|
||||
func (app *dbTransferAppImpl) MarkRuning(taskId uint64) {
|
||||
// MarkRunning 标记任务执行中
|
||||
func (app *dbTransferAppImpl) MarkRunning(taskId uint64) {
|
||||
cache.Set(fmt.Sprintf("mayfly:db:transfer:%d", taskId), 1, -1)
|
||||
}
|
||||
|
||||
@@ -434,3 +592,10 @@ func (app *dbTransferAppImpl) EndTransfer(ctx context.Context, logId uint64, tas
|
||||
task.RunningState = transferState
|
||||
app.UpdateById(context.Background(), task)
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) RemoveCronJobById(taskId uint64) {
|
||||
task, err := app.GetById(taskId)
|
||||
if err == nil {
|
||||
scheduler.RemoveByKey(task.TaskKey)
|
||||
}
|
||||
}
|
||||
|
||||
78
server/internal/db/application/db_transfer_file.go
Normal file
78
server/internal/db/application/db_transfer_file.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"mayfly-go/internal/db/config"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type DbTransferFile interface {
|
||||
base.App[*entity.DbTransferFile]
|
||||
|
||||
// GetPageList 分页获取数据库实例
|
||||
GetPageList(condition *entity.DbTransferFileQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
|
||||
Save(ctx context.Context, instanceEntity *entity.DbTransferFile) error
|
||||
|
||||
Delete(ctx context.Context, id ...uint64) error
|
||||
|
||||
GetFilePath(ent *entity.DbTransferFile) string
|
||||
}
|
||||
|
||||
var _ DbTransferFile = (*dbTransferFileAppImpl)(nil)
|
||||
|
||||
type dbTransferFileAppImpl struct {
|
||||
base.AppImpl[*entity.DbTransferFile, repository.DbTransferFile]
|
||||
}
|
||||
|
||||
func (app *dbTransferFileAppImpl) InjectDbTransferFileRepo(repo repository.DbTransferFile) {
|
||||
app.Repo = repo
|
||||
}
|
||||
|
||||
func (app *dbTransferFileAppImpl) GetPageList(condition *entity.DbTransferFileQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.GetRepo().GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
func (app *dbTransferFileAppImpl) Save(ctx context.Context, taskEntity *entity.DbTransferFile) error {
|
||||
var err error
|
||||
if taskEntity.Id == 0 {
|
||||
err = app.Insert(ctx, taskEntity)
|
||||
} else {
|
||||
err = app.UpdateById(ctx, taskEntity)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (app *dbTransferFileAppImpl) Delete(ctx context.Context, id ...uint64) error {
|
||||
|
||||
arr, err := app.GetByIds(id, "task_id", "file_uuid")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除对应的文件
|
||||
for _, file := range arr {
|
||||
_ = os.Remove(app.GetFilePath(file))
|
||||
}
|
||||
|
||||
// 删除数据
|
||||
return app.DeleteById(ctx, id...)
|
||||
}
|
||||
|
||||
func (app *dbTransferFileAppImpl) GetFilePath(ent *entity.DbTransferFile) string {
|
||||
brc := config.GetDbBackupRestore()
|
||||
if ent.FileUuid == "" {
|
||||
ent.FileUuid = uuid.New().String()
|
||||
}
|
||||
|
||||
filePath := filepath.Join(fmt.Sprintf("%s/%d/%s.sql", brc.TransferPath, ent.TaskId, ent.FileUuid))
|
||||
|
||||
return filePath
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/utils/writer"
|
||||
)
|
||||
|
||||
type SaveDbInstance struct {
|
||||
@@ -19,5 +20,13 @@ type DumpDb struct {
|
||||
DumpDDL bool // 是否dump ddl
|
||||
DumpData bool // 是否dump data
|
||||
|
||||
Writer io.Writer
|
||||
LogId uint64
|
||||
|
||||
Writer writer.CustomWriter
|
||||
Log func(msg string)
|
||||
TargetDbType dbi.DbType
|
||||
}
|
||||
|
||||
func DefaultDumpLog(msg string) {
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -33,7 +34,8 @@ func GetDbms() *Dbms {
|
||||
}
|
||||
|
||||
type DbBackupRestore struct {
|
||||
BackupPath string // 备份文件路径呢
|
||||
BackupPath string // 备份文件路径呢
|
||||
TransferPath string // 数据库迁移文件存储路径
|
||||
}
|
||||
|
||||
// 获取数据库备份配置
|
||||
@@ -43,11 +45,8 @@ func GetDbBackupRestore() *DbBackupRestore {
|
||||
|
||||
dbrc := new(DbBackupRestore)
|
||||
|
||||
backupPath := jm["backupPath"]
|
||||
if backupPath == "" {
|
||||
backupPath = "./db/backup"
|
||||
}
|
||||
dbrc.BackupPath = filepath.Join(backupPath)
|
||||
dbrc.BackupPath = filepath.Join(cmp.Or(jm["backupPath"], "./db/backup"))
|
||||
dbrc.TransferPath = filepath.Join(cmp.Or(jm["transferPath"], "./db/transfer"))
|
||||
|
||||
return dbrc
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ type QueryColumn struct {
|
||||
Type string `json:"type"` // 类型
|
||||
}
|
||||
|
||||
func (d *DbConn) GetDb() *sql.DB {
|
||||
return d.db
|
||||
}
|
||||
|
||||
// 执行查询语句
|
||||
// 依次返回 列信息数组(顺序),结果map,错误
|
||||
func (d *DbConn) Query(querySql string, args ...any) ([]*QueryColumn, []map[string]any, error) {
|
||||
|
||||
@@ -47,6 +47,9 @@ type DbInfo struct {
|
||||
Params string
|
||||
Database string // 若有schema的库则为'database/scheam'格式
|
||||
|
||||
Version DbVersion // 数据库版本信息,用于语法兼容
|
||||
DefaultVersion bool // 经过查询数据库版本信息后,是否仍然使用默认版本
|
||||
|
||||
CodePath []string
|
||||
SshTunnelMachineId int
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package dbi
|
||||
|
||||
import "database/sql"
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
var (
|
||||
metas = make(map[DbType]Meta)
|
||||
@@ -16,6 +18,8 @@ func GetMeta(dt DbType) Meta {
|
||||
return metas[dt]
|
||||
}
|
||||
|
||||
type DbVersion string
|
||||
|
||||
// 数据库元信息,如获取sql.DB、Dialect等
|
||||
type Meta interface {
|
||||
// 根据数据库信息获取sql.DB
|
||||
|
||||
@@ -16,6 +16,9 @@ type MetaData interface {
|
||||
// GetDbServer 获取数据库服务实例信息
|
||||
GetDbServer() (*DbServer, error)
|
||||
|
||||
// GetCompatibleDbVersion 获取兼容版本信息,如果有兼容版本,则需要实现对应版本的特殊方言处理器,以及前端的方言兼容版本
|
||||
GetCompatibleDbVersion() DbVersion
|
||||
|
||||
// GetDbNames 获取数据库名称列表
|
||||
GetDbNames() ([]string, error)
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ type BaseMetaData interface {
|
||||
// 默认库
|
||||
DefaultDb() string
|
||||
|
||||
DbVersion() string
|
||||
|
||||
// 用于引用 SQL 标识符(关键字)的字符串
|
||||
GetIdentifierQuoteString() string
|
||||
|
||||
@@ -43,10 +45,17 @@ type BaseMetaData interface {
|
||||
type DefaultMetaData struct {
|
||||
}
|
||||
|
||||
func (dd *DefaultMetaData) GetCompatibleDbVersion() DbVersion {
|
||||
return ""
|
||||
}
|
||||
func (dd *DefaultMetaData) DefaultDb() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (dd *DefaultMetaData) DbVersion() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (dd *DefaultMetaData) GetIdentifierQuoteString() string {
|
||||
return `"`
|
||||
}
|
||||
|
||||
@@ -73,3 +73,30 @@ FROM ALL_TAB_COLUMNS a
|
||||
WHERE a.OWNER = (SELECT sys_context('USERENV', 'CURRENT_SCHEMA') FROM DUAL)
|
||||
AND a.TABLE_NAME in (%s)
|
||||
order by a.COLUMN_ID
|
||||
---------------------------------------
|
||||
--ORACLE11_COLUMN_MA 11版本的列信息
|
||||
SELECT a.TABLE_NAME as TABLE_NAME,
|
||||
a.COLUMN_NAME as COLUMN_NAME,
|
||||
case
|
||||
when a.NULLABLE = 'Y' then 'YES'
|
||||
when a.NULLABLE = 'N' then 'NO'
|
||||
else 'NO' end as NULLABLE,
|
||||
a.DATA_TYPE as DATA_TYPE,
|
||||
a.DATA_LENGTH as CHAR_MAX_LENGTH,
|
||||
a.DATA_PRECISION as NUM_PRECISION,
|
||||
a.DATA_SCALE as NUM_SCALE,
|
||||
b.COMMENTS as COLUMN_COMMENT,
|
||||
a.DATA_DEFAULT as COLUMN_DEFAULT,
|
||||
CASE WHEN d.pri IS NOT NULL THEN 1 ELSE 0 END as IS_PRIMARY_KEY
|
||||
FROM ALL_TAB_COLUMNS a
|
||||
LEFT JOIN ALL_COL_COMMENTS b
|
||||
on a.OWNER = b.OWNER AND a.TABLE_NAME = b.TABLE_NAME AND a.COLUMN_NAME = b.COLUMN_NAME
|
||||
LEFT JOIN (select ac.TABLE_NAME, ac.OWNER, cc.COLUMN_NAME, 1 as pri
|
||||
from ALL_CONSTRAINTS ac
|
||||
join ALL_CONS_COLUMNS cc on cc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME AND cc.OWNER = ac.OWNER
|
||||
where cc.CONSTRAINT_NAME IS NOT NULL
|
||||
AND ac.CONSTRAINT_TYPE = 'P') d
|
||||
on d.OWNER = a.OWNER AND d.TABLE_NAME = a.TABLE_NAME AND d.COLUMN_NAME = a.COLUMN_NAME
|
||||
WHERE a.OWNER = (SELECT sys_context('USERENV', 'CURRENT_SCHEMA') FROM DUAL)
|
||||
AND a.TABLE_NAME in (%s)
|
||||
order by a.COLUMN_ID
|
||||
@@ -9,13 +9,13 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
dbi.Register(dbi.DbTypeDM, new(DmMeta))
|
||||
dbi.Register(dbi.DbTypeDM, new(Meta))
|
||||
}
|
||||
|
||||
type DmMeta struct {
|
||||
type Meta struct {
|
||||
}
|
||||
|
||||
func (md *DmMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
func (dm *Meta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
driverName := "dm"
|
||||
db := d.Database
|
||||
var dbParam string
|
||||
@@ -40,11 +40,11 @@ func (md *DmMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
return sql.Open(driverName, dsn)
|
||||
}
|
||||
|
||||
func (md *DmMeta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
|
||||
func (dm *Meta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
|
||||
return &DMDialect{dc: conn}
|
||||
}
|
||||
|
||||
func (md *DmMeta) GetMetaData(conn *dbi.DbConn) *dbi.MetaDataX {
|
||||
func (dm *Meta) GetMetaData(conn *dbi.DbConn) *dbi.MetaDataX {
|
||||
return dbi.NewMetaDataX(&DMMetaData{
|
||||
dc: conn,
|
||||
})
|
||||
|
||||
@@ -11,14 +11,14 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
meta := new(MssqlMeta)
|
||||
meta := new(Meta)
|
||||
dbi.Register(dbi.DbTypeMssql, meta)
|
||||
}
|
||||
|
||||
type MssqlMeta struct {
|
||||
type Meta struct {
|
||||
}
|
||||
|
||||
func (md *MssqlMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
func (mm *Meta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
err := d.IfUseSshTunnelChangeIpPort()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -53,10 +53,10 @@ func (md *MssqlMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
return sql.Open(driverName, dsn)
|
||||
}
|
||||
|
||||
func (md *MssqlMeta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
|
||||
func (mm *Meta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
|
||||
return &MssqlDialect{dc: conn}
|
||||
}
|
||||
|
||||
func (md *MssqlMeta) GetMetaData(conn *dbi.DbConn) *dbi.MetaDataX {
|
||||
func (mm *Meta) GetMetaData(conn *dbi.DbConn) *dbi.MetaDataX {
|
||||
return dbi.NewMetaDataX(&MssqlMetaData{dc: conn})
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ var (
|
||||
"longblob": dbi.CommonTypeLongblob,
|
||||
"longtext": dbi.CommonTypeLongtext,
|
||||
"mediumblob": dbi.CommonTypeBlob,
|
||||
"mediumtext": dbi.CommonTypeText,
|
||||
"mediumtext": dbi.CommonTypeMediumtext,
|
||||
"bit": dbi.CommonTypeBit,
|
||||
"set": dbi.CommonTypeVarchar,
|
||||
"smallint": dbi.CommonTypeSmallint,
|
||||
@@ -60,7 +60,7 @@ var (
|
||||
dbi.CommonTypeLongtext: "longtext",
|
||||
dbi.CommonTypeBinary: "binary",
|
||||
dbi.CommonTypeMediumblob: "blob",
|
||||
dbi.CommonTypeMediumtext: "text",
|
||||
dbi.CommonTypeMediumtext: "mediumtext",
|
||||
dbi.CommonTypeVarbinary: "varbinary",
|
||||
dbi.CommonTypeInt: "int",
|
||||
dbi.CommonTypeBit: "bit",
|
||||
|
||||
@@ -11,15 +11,15 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
meta := new(MysqlMeta)
|
||||
meta := new(Meta)
|
||||
dbi.Register(dbi.DbTypeMysql, meta)
|
||||
dbi.Register(dbi.DbTypeMariadb, meta)
|
||||
}
|
||||
|
||||
type MysqlMeta struct {
|
||||
type Meta struct {
|
||||
}
|
||||
|
||||
func (md *MysqlMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
func (mm *Meta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
// SSH Conect
|
||||
if d.SshTunnelMachineId > 0 {
|
||||
sshTunnelMachine, err := dbi.GetSshTunnel(d.SshTunnelMachineId)
|
||||
@@ -39,10 +39,10 @@ func (md *MysqlMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
return sql.Open(driverName, dsn)
|
||||
}
|
||||
|
||||
func (md *MysqlMeta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
|
||||
func (mm *Meta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
|
||||
return &MysqlDialect{dc: conn}
|
||||
}
|
||||
|
||||
func (md *MysqlMeta) GetMetaData(conn *dbi.DbConn) *dbi.MetaDataX {
|
||||
func (mm *Meta) GetMetaData(conn *dbi.DbConn) *dbi.MetaDataX {
|
||||
return dbi.NewMetaDataX(&MysqlMetaData{dc: conn})
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ import (
|
||||
var (
|
||||
// 数字类型
|
||||
numberTypeRegexp = regexp.MustCompile(`(?i)int|double|float|number|decimal|byte|bit`)
|
||||
dateTimeReg = regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`)
|
||||
dateTimeIsoReg = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*$`)
|
||||
|
||||
// 日期时间类型
|
||||
datetimeTypeRegexp = regexp.MustCompile(`(?i)date|timestamp`)
|
||||
|
||||
@@ -83,6 +86,9 @@ func (dc *DataHelper) GetDataType(dbColumnType string) dbi.DataType {
|
||||
|
||||
func (dc *DataHelper) FormatData(dbColumnValue any, dataType dbi.DataType) string {
|
||||
str := anyx.ToString(dbColumnValue)
|
||||
if dateTimeReg.MatchString(str) || dateTimeIsoReg.MatchString(str) {
|
||||
dataType = dbi.DataTypeDateTime
|
||||
}
|
||||
switch dataType {
|
||||
// oracle把日期类型数据格式化输出
|
||||
case dbi.DataTypeDateTime: // "2024-01-02T22:08:22.275697+08:00"
|
||||
@@ -122,7 +128,7 @@ func (dc *DataHelper) WrapValue(dbColumnValue any, dataType dbi.DataType) string
|
||||
val = strings.Replace(val, "\n", "\\n", -1)
|
||||
return fmt.Sprintf("'%s'", val)
|
||||
case dbi.DataTypeDate, dbi.DataTypeDateTime, dbi.DataTypeTime:
|
||||
return fmt.Sprintf("to_timestamp('%s', 'yyyy-mm-dd hh24:mi:ss')", dc.FormatData(dbColumnValue, dataType))
|
||||
return fmt.Sprintf("to_date('%s', 'yyyy-mm-dd hh24:mi:ss')", dc.FormatData(dbColumnValue, dataType))
|
||||
}
|
||||
return fmt.Sprintf("'%s'", dbColumnValue)
|
||||
}
|
||||
|
||||
@@ -12,13 +12,17 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
dbi.Register(dbi.DbTypeOracle, new(OraMeta))
|
||||
dbi.Register(dbi.DbTypeOracle, new(Meta))
|
||||
}
|
||||
|
||||
type OraMeta struct {
|
||||
const (
|
||||
DbVersionOracle11 dbi.DbVersion = "11"
|
||||
)
|
||||
|
||||
type Meta struct {
|
||||
}
|
||||
|
||||
func (md *OraMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
func (om *Meta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
err := d.IfUseSshTunnelChangeIpPort()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -75,13 +79,38 @@ func (md *OraMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return conn, err
|
||||
}
|
||||
|
||||
func (om *OraMeta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
|
||||
func (om *Meta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
|
||||
return &OracleDialect{dc: conn}
|
||||
}
|
||||
|
||||
func (om *OraMeta) GetMetaData(conn *dbi.DbConn) *dbi.MetaDataX {
|
||||
func (om *Meta) GetMetaData(conn *dbi.DbConn) *dbi.MetaDataX {
|
||||
|
||||
// 查询数据库版本信息,以做兼容性处理
|
||||
if conn.Info.Version == "" && !conn.Info.DefaultVersion {
|
||||
if conn.GetDb() != nil {
|
||||
_, res, _ := conn.Query("select VERSION from v$instance")
|
||||
if len(res) > 0 {
|
||||
version := cast.ToString(res[0]["VERSION"])
|
||||
// 11开头为11g版本
|
||||
if strings.HasPrefix(version, "11") {
|
||||
conn.Info.Version = DbVersionOracle11
|
||||
conn.Info.DefaultVersion = false
|
||||
} else {
|
||||
conn.Info.DefaultVersion = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if conn.Info.Version == DbVersionOracle11 {
|
||||
md := &OracleMetaData11{}
|
||||
md.dc = conn
|
||||
md.version = DbVersionOracle11
|
||||
return dbi.NewMetaDataX(md)
|
||||
}
|
||||
return dbi.NewMetaDataX(&OracleMetaData{dc: conn})
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ type OracleMetaData struct {
|
||||
dbi.DefaultMetaData
|
||||
|
||||
dc *dbi.DbConn
|
||||
|
||||
version dbi.DbVersion
|
||||
}
|
||||
|
||||
func (od *OracleMetaData11) GetCompatibleDbVersion() dbi.DbVersion {
|
||||
return od.version
|
||||
}
|
||||
|
||||
func (od *OracleMetaData) GetDbServer() (*dbi.DbServer, error) {
|
||||
@@ -306,10 +312,19 @@ end`
|
||||
if len(columnComments) > 0 {
|
||||
sqlArr = append(sqlArr, columnComments...)
|
||||
}
|
||||
otherSql := od.GenerateTableOtherDDL(tableInfo, quoteTableName, columns)
|
||||
if len(otherSql) > 0 {
|
||||
sqlArr = append(sqlArr, otherSql...)
|
||||
}
|
||||
|
||||
return sqlArr
|
||||
}
|
||||
|
||||
// 11g及以下版本会设置自增序列
|
||||
func (od *OracleMetaData) GenerateTableOtherDDL(tableInfo dbi.Table, quoteTableName string, columns []dbi.Column) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取建表ddl
|
||||
func (od *OracleMetaData) GetTableDDL(tableName string, dropBeforeCreate bool) (string, error) {
|
||||
|
||||
|
||||
109
server/internal/db/dbm/oracle/metadata11.go
Normal file
109
server/internal/db/dbm/oracle/metadata11.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"strings"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
)
|
||||
|
||||
const (
|
||||
ORACLE11_COLUMN_MA_KEY = "ORACLE11_COLUMN_MA"
|
||||
)
|
||||
|
||||
type OracleMetaData11 struct {
|
||||
OracleMetaData
|
||||
}
|
||||
|
||||
// 获取列元信息, 如列名等
|
||||
func (od *OracleMetaData11) GetColumns(tableNames ...string) ([]dbi.Column, error) {
|
||||
meta := od.dc.GetMetaData()
|
||||
tableName := strings.Join(collx.ArrayMap[string, string](tableNames, func(val string) string {
|
||||
return fmt.Sprintf("'%s'", meta.RemoveQuote(val))
|
||||
}), ",")
|
||||
|
||||
// 如果表数量超过了1000,需要分批查询
|
||||
if len(tableNames) > 1000 {
|
||||
columns := make([]dbi.Column, 0)
|
||||
for i := 0; i < len(tableNames); i += 1000 {
|
||||
end := i + 1000
|
||||
if end > len(tableNames) {
|
||||
end = len(tableNames)
|
||||
}
|
||||
tables := tableNames[i:end]
|
||||
cols, err := od.GetColumns(tables...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
columns = append(columns, cols...)
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
_, res, err := od.dc.Query(fmt.Sprintf(dbi.GetLocalSql(ORACLE_META_FILE, ORACLE11_COLUMN_MA_KEY), tableName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
columnHelper := meta.GetColumnHelper()
|
||||
columns := make([]dbi.Column, 0)
|
||||
for _, re := range res {
|
||||
column := dbi.Column{
|
||||
TableName: cast.ToString(re["TABLE_NAME"]),
|
||||
ColumnName: cast.ToString(re["COLUMN_NAME"]),
|
||||
DataType: dbi.ColumnDataType(cast.ToString(re["DATA_TYPE"])),
|
||||
CharMaxLength: cast.ToInt(re["CHAR_MAX_LENGTH"]),
|
||||
ColumnComment: cast.ToString(re["COLUMN_COMMENT"]),
|
||||
Nullable: cast.ToString(re["NULLABLE"]) == "YES",
|
||||
IsPrimaryKey: cast.ToInt(re["IS_PRIMARY_KEY"]) == 1,
|
||||
IsIdentity: cast.ToInt(re["IS_IDENTITY"]) == 1,
|
||||
ColumnDefault: cast.ToString(re["COLUMN_DEFAULT"]),
|
||||
NumPrecision: cast.ToInt(re["NUM_PRECISION"]),
|
||||
NumScale: cast.ToInt(re["NUM_SCALE"]),
|
||||
}
|
||||
|
||||
columnHelper.FixColumn(&column)
|
||||
columns = append(columns, column)
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
func (od *OracleMetaData11) genColumnBasicSql(column dbi.Column) string {
|
||||
meta := od.dc.GetMetaData()
|
||||
colName := meta.QuoteIdentifier(column.ColumnName)
|
||||
|
||||
if column.IsIdentity {
|
||||
// 11g以前的版本 如果是自增,自增列数据类型必须是number,不需要设置默认值和空值,建表后设置自增序列
|
||||
return fmt.Sprintf(" %s NUMBER", colName)
|
||||
}
|
||||
|
||||
nullAble := ""
|
||||
if !column.Nullable {
|
||||
nullAble = " NOT NULL"
|
||||
}
|
||||
|
||||
defVal := ""
|
||||
if column.ColumnDefault != "" {
|
||||
defVal = fmt.Sprintf(" DEFAULT %v", column.ColumnDefault)
|
||||
}
|
||||
|
||||
columnSql := fmt.Sprintf(" %s %s%s%s", colName, column.GetColumnType(), defVal, nullAble)
|
||||
return columnSql
|
||||
}
|
||||
|
||||
// 11g及以下版本会设置自增序列和触发器
|
||||
func (od *OracleMetaData11) GenerateTableOtherDDL(tableInfo dbi.Table, quoteTableName string, columns []dbi.Column) []string {
|
||||
result := make([]string, 0)
|
||||
for _, col := range columns {
|
||||
if col.IsIdentity {
|
||||
seqName := fmt.Sprintf("%s_%s_seq", tableInfo.TableName, col.ColumnName)
|
||||
trgName := fmt.Sprintf("%s_%s_trg", tableInfo.TableName, col.ColumnName)
|
||||
result = append(result, fmt.Sprintf("CREATE SEQUENCE %s START WITH 1 INCREMENT BY 1", seqName))
|
||||
result = append(result, fmt.Sprintf("CREATE OR REPLACE TRIGGER %s BEFORE INSERT ON %s FOR EACH ROW WHEN (NEW.%s IS NULL) BEGIN SELECT %s.nextval INTO :new.%s FROM dual; END", trgName, quoteTableName, col.ColumnName, seqName, col.ColumnName))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -15,22 +15,22 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
meta := new(PostgresMeta)
|
||||
meta := new(Meta)
|
||||
dbi.Register(dbi.DbTypePostgres, meta)
|
||||
dbi.Register(dbi.DbTypeKingbaseEs, meta)
|
||||
dbi.Register(dbi.DbTypeVastbase, meta)
|
||||
|
||||
gauss := &PostgresMeta{
|
||||
gauss := &Meta{
|
||||
Param: "dbtype=gauss",
|
||||
}
|
||||
dbi.Register(dbi.DbTypeGauss, gauss)
|
||||
}
|
||||
|
||||
type PostgresMeta struct {
|
||||
type Meta struct {
|
||||
Param string
|
||||
}
|
||||
|
||||
func (pm *PostgresMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
func (pm *Meta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
driverName := "postgres"
|
||||
// SSH Conect
|
||||
if d.SshTunnelMachineId > 0 {
|
||||
@@ -81,11 +81,11 @@ func (pm *PostgresMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
return sql.Open(driverName, dsn)
|
||||
}
|
||||
|
||||
func (pm *PostgresMeta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
|
||||
func (pm *Meta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
|
||||
return &PgsqlDialect{dc: conn}
|
||||
}
|
||||
|
||||
func (pm *PostgresMeta) GetMetaData(conn *dbi.DbConn) *dbi.MetaDataX {
|
||||
func (pm *Meta) GetMetaData(conn *dbi.DbConn) *dbi.MetaDataX {
|
||||
return dbi.NewMetaDataX(&PgsqlMetaData{dc: conn})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
dbi.Register(dbi.DbTypeSqlite, new(SqliteMeta))
|
||||
dbi.Register(dbi.DbTypeSqlite, new(Meta))
|
||||
}
|
||||
|
||||
type SqliteMeta struct {
|
||||
type Meta struct {
|
||||
}
|
||||
|
||||
func (md *SqliteMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
func (md *Meta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
// 用host字段来存sqlite的文件路径
|
||||
// 检查文件是否存在,否则报错,基于sqlite会自动创建文件,为了服务器文件安全,所以先确定文件存在再连接,不自动创建
|
||||
if _, err := os.Stat(d.Host); err != nil {
|
||||
@@ -29,10 +29,10 @@ func (md *SqliteMeta) GetSqlDb(d *dbi.DbInfo) (*sql.DB, error) {
|
||||
return db, err
|
||||
}
|
||||
|
||||
func (sm *SqliteMeta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
|
||||
func (sm *Meta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
|
||||
return &SqliteDialect{dc: conn}
|
||||
}
|
||||
|
||||
func (sm *SqliteMeta) GetMetaData(conn *dbi.DbConn) *dbi.MetaDataX {
|
||||
func (sm *Meta) GetMetaData(conn *dbi.DbConn) *dbi.MetaDataX {
|
||||
return dbi.NewMetaDataX(&SqliteMetaData{dc: conn})
|
||||
}
|
||||
|
||||
@@ -22,8 +22,9 @@ type DataSyncTask struct {
|
||||
SrcTagPath string `orm:"column(src_tag_path)" json:"srcTagPath"`
|
||||
DataSql string `orm:"column(data_sql)" json:"dataSql"` // 数据源查询sql
|
||||
PageSize int `orm:"column(page_size)" json:"pageSize"` // 配置分页sql查询的条数
|
||||
UpdField string `orm:"column(upd_field)" json:"updField"` //更新字段, 选择由哪个字段为更新字段,查询数据源的时候会带上这个字段,如:where update_time > {最近更新的最大值}
|
||||
UpdField string `orm:"column(upd_field)" json:"updField"` // 更新字段, 选择由哪个字段为更新字段,查询数据源的时候会带上这个字段,如:where update_time > {最近更新的最大值}
|
||||
UpdFieldVal string `orm:"column(upd_field_val)" json:"updFieldVal"` // 更新字段当前值
|
||||
UpdFieldSrc string `orm:"column(upd_field_src)" json:"updFieldSrc"` // 更新值来源, 如select name as user_name from user; 则updFieldSrc的值为user_name
|
||||
|
||||
// 目标数据库信息
|
||||
TargetDbId int64 `orm:"column(target_db_id)" json:"targetDbId"`
|
||||
|
||||
@@ -7,9 +7,15 @@ import (
|
||||
type DbTransferTask struct {
|
||||
model.Model
|
||||
|
||||
RunningState DbTransferRunningState `orm:"column(running_state)" json:"runningState"` // 运行状态
|
||||
LogId uint64 `json:"logId"`
|
||||
TaskName string `orm:"column(task_name)" json:"taskName"` // 任务名称
|
||||
RunningState int8 `orm:"column(running_state)" json:"runningState"` // 运行状态
|
||||
LogId uint64 `json:"logId"`
|
||||
TaskName string `orm:"column(task_name)" json:"taskName"` // 任务名称
|
||||
Status int8 `orm:"column(status)" json:"status"` // 启用状态 1启用 -1禁用
|
||||
CronAble int8 `orm:"column(cron_able)" json:"cronAble"` // 是否定时 1是 -1否
|
||||
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"` // 目标文件数据库类型
|
||||
TaskKey string `orm:"column(key)" json:"taskKey"` // 定时任务唯一uuid key
|
||||
|
||||
CheckedKeys string `orm:"column(checked_keys)" json:"checkedKeys"` // 选中需要迁移的表
|
||||
DeleteTable int `orm:"column(delete_table)" json:"deleteTable"` // 创建表前是否删除表
|
||||
@@ -34,14 +40,18 @@ func (d *DbTransferTask) TableName() string {
|
||||
return "t_db_transfer_task"
|
||||
}
|
||||
|
||||
type DbTransferRunningState int8
|
||||
|
||||
const (
|
||||
DbTransferTaskStatusEnable int = 1 // 启用状态
|
||||
DbTransferTaskStatusDisable int = -1 // 禁用状态
|
||||
DbTransferTaskStatusEnable int8 = 1 // 启用状态
|
||||
DbTransferTaskStatusDisable int8 = -1 // 禁用状态
|
||||
|
||||
DbTransferTaskRunStateSuccess DbTransferRunningState = 2 // 执行成功
|
||||
DbTransferTaskRunStateRunning DbTransferRunningState = 1 // 运行中状态
|
||||
DbTransferTaskRunStateFail DbTransferRunningState = -1 // 执行失败
|
||||
DbTransferTaskRunStateStop DbTransferRunningState = -2 // 手动终止
|
||||
DbTransferTaskCronAbleEnable int8 = 1 // 是否定时 1是
|
||||
DbTransferTaskCronAbleDisable int8 = -1 // 是否定时 -1否
|
||||
|
||||
DbTransferTaskModeDb int8 = 1 // 数据迁移方式,1、迁移到数据库
|
||||
DbTransferTaskModeFile int8 = 2 // 数据迁移方式,2、迁移到文件
|
||||
|
||||
DbTransferTaskRunStateSuccess int8 = 2 // 执行成功
|
||||
DbTransferTaskRunStateRunning int8 = 1 // 运行中状态
|
||||
DbTransferTaskRunStateFail int8 = -1 // 执行失败
|
||||
DbTransferTaskRunStateStop int8 = -2 // 手动终止
|
||||
)
|
||||
|
||||
28
server/internal/db/domain/entity/db_transfer_file.go
Normal file
28
server/internal/db/domain/entity/db_transfer_file.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DbTransferFile struct {
|
||||
model.IdModel
|
||||
IsDeleted int8 `orm:"column(is_deleted)" json:"-"` // 是否删除 1是 0否
|
||||
CreateTime *time.Time `orm:"column(create_time)" json:"createTime"` // 创建时间,默认当前时间戳
|
||||
Status int8 `orm:"column(status)" json:"status"` // 状态 1、执行中 2、执行成功 3、执行失败
|
||||
TaskId uint64 `orm:"column(task_id)" json:"taskId"` // 迁移任务ID
|
||||
LogId uint64 `orm:"column(log_id)" json:"logId"` // 日志ID
|
||||
FileDbType string `orm:"column(file_db_type)" json:"fileDbType"` // sql文件数据库类型
|
||||
FileName string `orm:"column(file_name)" json:"fileName"` // 显式文件名
|
||||
FileUuid string `orm:"column(file_uuid)" json:"fileUuid"` // 文件真实id,拼接后可以下载
|
||||
}
|
||||
|
||||
func (d *DbTransferFile) TableName() string {
|
||||
return "t_db_transfer_files"
|
||||
}
|
||||
|
||||
const (
|
||||
DbTransferFileStatusRunning int8 = 1
|
||||
DbTransferFileStatusSuccess int8 = 2
|
||||
DbTransferFileStatusFail int8 = -1
|
||||
)
|
||||
@@ -20,7 +20,13 @@ type DataSyncLogQuery struct {
|
||||
}
|
||||
|
||||
type DbTransferTaskQuery struct {
|
||||
Name string `json:"name" form:"name"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Status int8 `json:"status" form:"status"`
|
||||
CronAble int8 `json:"cronAble" form:"cronAble"`
|
||||
}
|
||||
type DbTransferFileQuery struct {
|
||||
TaskId uint64 `json:"task_id" form:"taskId"`
|
||||
Name string `json:"name" form:"name"`
|
||||
}
|
||||
|
||||
type DbTransferLogQuery struct {
|
||||
|
||||
14
server/internal/db/domain/repository/db_transfer_file.go
Normal file
14
server/internal/db/domain/repository/db_transfer_file.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type DbTransferFile interface {
|
||||
base.Repo[*entity.DbTransferFile]
|
||||
|
||||
// 分页获取数据库实例信息列表
|
||||
GetPageList(condition *entity.DbTransferFileQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
}
|
||||
@@ -18,7 +18,9 @@ func newDbTransferTaskRepo() repository.DbTransferTask {
|
||||
// 分页获取数据库信息列表
|
||||
func (d *dbTransferTaskRepoImpl) GetTaskList(condition *entity.DbTransferTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
qd := model.NewCond().
|
||||
Like("task_name", condition.Name)
|
||||
Like("task_name", condition.Name).
|
||||
Eq("status", condition.Status).
|
||||
Eq("cron_able", condition.CronAble)
|
||||
//Eq("status", condition.Status)
|
||||
return d.PageByCondToAny(qd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type dbTransferFileRepoImpl struct {
|
||||
base.RepoImpl[*entity.DbTransferFile]
|
||||
}
|
||||
|
||||
func newDbTransferFileRepo() repository.DbTransferFile {
|
||||
return &dbTransferFileRepoImpl{base.RepoImpl[*entity.DbTransferFile]{M: new(entity.DbTransferFile)}}
|
||||
}
|
||||
|
||||
// 分页获取数据库信息列表
|
||||
func (d *dbTransferFileRepoImpl) GetPageList(condition *entity.DbTransferFileQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
qd := model.NewCond().
|
||||
Eq("task_id", condition.TaskId).
|
||||
OrderByDesc("create_time")
|
||||
//Eq("status", condition.Status)
|
||||
return d.PageByCondToAny(qd, pageParam, toEntity)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ func InitIoc() {
|
||||
ioc.Register(newDataSyncTaskRepo(), ioc.WithComponentName("DbDataSyncTaskRepo"))
|
||||
ioc.Register(newDataSyncLogRepo(), ioc.WithComponentName("DbDataSyncLogRepo"))
|
||||
ioc.Register(newDbTransferTaskRepo(), ioc.WithComponentName("DbTransferTaskRepo"))
|
||||
ioc.Register(newDbTransferFileRepo(), ioc.WithComponentName("DbTransferFileRepo"))
|
||||
|
||||
ioc.Register(NewDbBackupRepo(), ioc.WithComponentName("DbBackupRepo"))
|
||||
ioc.Register(NewDbBackupHistoryRepo(), ioc.WithComponentName("DbBackupHistoryRepo"))
|
||||
|
||||
@@ -30,6 +30,8 @@ func InitDbRouter(router *gin.RouterGroup) {
|
||||
|
||||
req.NewGet(":dbId/t-create-ddl", d.GetTableDDL),
|
||||
|
||||
req.NewGet(":dbId/version", d.GetVersion),
|
||||
|
||||
req.NewGet(":dbId/pg/schemas", d.GetSchemas),
|
||||
|
||||
req.NewPost(":dbId/exec-sql", d.ExecSql).Log(req.NewLog("db-执行Sql")),
|
||||
|
||||
@@ -20,16 +20,32 @@ func InitDbTransferRouter(router *gin.RouterGroup) {
|
||||
req.NewGet("", d.Tasks),
|
||||
|
||||
// 保存任务 /datasync/save
|
||||
req.NewPost("save", d.SaveTask).Log(req.NewLogSave("datasync-保存数据迁移任务信息")).RequiredPermissionCode("db:transfer:save"),
|
||||
req.NewPost("save", d.SaveTask).Log(req.NewLogSave("dts-保存数据迁移任务信息")).RequiredPermissionCode("db:transfer:save"),
|
||||
|
||||
// 删除任务 /datasync/:taskId/del
|
||||
req.NewDelete(":taskId/del", d.DeleteTask).Log(req.NewLogSave("datasync-删除数据迁移任务信息")).RequiredPermissionCode("db:transfer:del"),
|
||||
req.NewDelete(":taskId/del", d.DeleteTask).Log(req.NewLogSave("dts-删除数据迁移任务信息")).RequiredPermissionCode("db:transfer:del"),
|
||||
|
||||
// 启停用任务 /datasync/status
|
||||
req.NewPost(":taskId/status", d.ChangeStatus).Log(req.NewLogSave("dts-启停任务")).RequiredPermissionCode("db:transfer:status"),
|
||||
|
||||
// 立即执行任务 /datasync/run
|
||||
req.NewPost(":taskId/run", d.Run).Log(req.NewLog("DBMS-执行数据迁移任务")).RequiredPermissionCode("db:transfer:run"),
|
||||
req.NewPost(":taskId/run", d.Run).Log(req.NewLog("dts-执行数据迁移任务")).RequiredPermissionCode("db:transfer:run"),
|
||||
|
||||
// 停止正在执行中的任务
|
||||
req.NewPost(":taskId/stop", d.Stop).Log(req.NewLogSave("DBMS-终止数据迁移任务")),
|
||||
req.NewPost(":taskId/stop", d.Stop).Log(req.NewLogSave("dts-终止数据迁移任务")).RequiredPermissionCode("db:transfer:run"),
|
||||
|
||||
// 导出文件管理-列表
|
||||
req.NewGet("/files/:taskId", d.Files),
|
||||
|
||||
req.NewPost("/files/rename", d.FileRename).Log(req.NewLogSave("dts-删除迁移文件")).RequiredPermissionCode("db:transfer:files:rename"),
|
||||
|
||||
// 导出文件管理-删除
|
||||
req.NewPost("/files/del/:fileId", d.FileDel).Log(req.NewLogSave("dts-删除迁移文件")).RequiredPermissionCode("db:transfer:files:del"),
|
||||
|
||||
req.NewPost("/files/run", d.FileRun).Log(req.NewLogSave("dts-执行sql文件")).RequiredPermissionCode("db:transfer:files:run"),
|
||||
|
||||
// 导出文件管理-下载
|
||||
req.NewGet("/files/down/:fileUuid", d.FileDown).Log(req.NewLogSave("dts-下载迁移文件")).RequiredPermissionCode("db:transfer:files:down"),
|
||||
}
|
||||
|
||||
req.BatchSetGroup(instances, reqs[:])
|
||||
|
||||
@@ -7,6 +7,7 @@ import "mayfly-go/pkg/utils/anyx"
|
||||
const SuccessSysMsgType = 1
|
||||
const ErrorSysMsgType = 0
|
||||
const InfoSysMsgType = 2
|
||||
const InfoTypeSqlExecProgress = 22
|
||||
|
||||
// websocket消息
|
||||
type SysMsg struct {
|
||||
@@ -42,6 +43,9 @@ func (sm *SysMsg) WithClientId(clientId string) *SysMsg {
|
||||
func InfoSysMsg(title string, msg any) *SysMsg {
|
||||
return &SysMsg{Type: InfoSysMsgType, Title: title, Msg: anyx.ToString(msg)}
|
||||
}
|
||||
func InfoSqlProgressMsg(title string, msg any) *SysMsg {
|
||||
return &SysMsg{Type: InfoTypeSqlExecProgress, Title: title, Msg: anyx.ToString(msg)}
|
||||
}
|
||||
|
||||
// 成功消息
|
||||
func SuccessSysMsg(title string, msg any) *SysMsg {
|
||||
|
||||
51
server/pkg/utils/writer/file_writer.go
Normal file
51
server/pkg/utils/writer/file_writer.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package writer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type FileWriter struct {
|
||||
tryFlushCount int
|
||||
writer *os.File
|
||||
aborted bool
|
||||
}
|
||||
|
||||
func NewFileWriter(filePath string) *FileWriter {
|
||||
if filePath == "" {
|
||||
panic("filePath is empty")
|
||||
}
|
||||
|
||||
// 使用filepath.Dir函数提取文件夹路径
|
||||
dir := filepath.Dir(filePath)
|
||||
if dir != "" {
|
||||
// 检查文件夹路径,不存在则创建
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, os.ModePerm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fw, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &FileWriter{writer: fw}
|
||||
}
|
||||
|
||||
func (f *FileWriter) Close() {
|
||||
f.writer.Close()
|
||||
}
|
||||
|
||||
func (f *FileWriter) TryFlush() {
|
||||
}
|
||||
func (f *FileWriter) Write(b []byte) (n int, err error) {
|
||||
return f.writer.Write(b)
|
||||
}
|
||||
|
||||
func (f *FileWriter) WriteString(data string) {
|
||||
io.WriteString(f.writer, data)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package application
|
||||
package writer
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
@@ -6,17 +6,17 @@ import (
|
||||
"mayfly-go/pkg/biz"
|
||||
)
|
||||
|
||||
type gzipWriter struct {
|
||||
type GzipWriter struct {
|
||||
tryFlushCount int
|
||||
writer *gzip.Writer
|
||||
aborted bool
|
||||
}
|
||||
|
||||
func newGzipWriter(writer io.Writer) *gzipWriter {
|
||||
return &gzipWriter{writer: gzip.NewWriter(writer)}
|
||||
func NewGzipWriter(writer io.Writer) *GzipWriter {
|
||||
return &GzipWriter{writer: gzip.NewWriter(writer)}
|
||||
}
|
||||
|
||||
func (g *gzipWriter) WriteString(data string) {
|
||||
func (g *GzipWriter) WriteString(data string) {
|
||||
if g.aborted {
|
||||
return
|
||||
}
|
||||
@@ -26,7 +26,7 @@ func (g *gzipWriter) WriteString(data string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (g *gzipWriter) Write(p []byte) (n int, err error) {
|
||||
func (g *GzipWriter) Write(p []byte) (n int, err error) {
|
||||
if g.aborted {
|
||||
return
|
||||
}
|
||||
@@ -38,11 +38,11 @@ func (g *gzipWriter) Write(p []byte) (n int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (g *gzipWriter) Close() {
|
||||
func (g *GzipWriter) Close() {
|
||||
g.writer.Close()
|
||||
}
|
||||
|
||||
func (g *gzipWriter) TryFlush() {
|
||||
func (g *GzipWriter) TryFlush() {
|
||||
if g.tryFlushCount%1000 == 0 {
|
||||
g.writer.Flush()
|
||||
}
|
||||
10
server/pkg/utils/writer/writer.go
Normal file
10
server/pkg/utils/writer/writer.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package writer
|
||||
|
||||
import "io"
|
||||
|
||||
type CustomWriter interface {
|
||||
io.Writer
|
||||
WriteString(data string)
|
||||
Close()
|
||||
TryFlush()
|
||||
}
|
||||
Binary file not shown.
@@ -64,6 +64,13 @@ CREATE TABLE `t_db_transfer_task` (
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||
`is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除',
|
||||
`task_name` varchar(100) NULL comment '任务名',
|
||||
`cron_able` TINYINT(3) NOT NULL DEFAULT 0 comment '是否定时 1是 -1否',
|
||||
`cron` VARCHAR(20) NULL comment '定时任务cron表达式',
|
||||
`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',
|
||||
`status` tinyint(3) NOT NULL DEFAULT '1' comment '启用状态 1启用 -1禁用',
|
||||
`upd_field_src` varchar(100) DEFAULT NULL COMMENT '更新值来源字段,默认同更新字段,如果查询结果指定了字段别名且与原更新字段不一致,则取这个字段值为当前更新值',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
`checked_keys` text NOT NULL COMMENT '选中需要迁移的表',
|
||||
`delete_table` tinyint(4) NOT NULL COMMENT '创建表前是否删除表 1是 -1否',
|
||||
@@ -84,6 +91,21 @@ CREATE TABLE `t_db_transfer_task` (
|
||||
PRIMARY KEY (`id`)
|
||||
) COMMENT='数据库迁移任务表';
|
||||
|
||||
DROP TABLE IF EXISTS `t_db_transfer_files`;
|
||||
CREATE TABLE `t_db_transfer_files` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`is_deleted` tinyint(3) NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||
`delete_time` datetime COMMENT '删除时间',
|
||||
`status` tinyint(3) NOT NULL DEFAULT 1 COMMENT '状态,1、执行中 2、执行失败 3、 执行成功',
|
||||
`task_id` bigint COMMENT '迁移任务ID',
|
||||
`log_id` bigint COMMENT '日志ID',
|
||||
`file_db_type` varchar(200) COMMENT 'sql文件数据库类型',
|
||||
`file_name` varchar(200) COMMENT '显式文件名 默认: 年月日时分秒.zip',
|
||||
`file_uuid` varchar(50) COMMENT '文件真实uuid,拼接后可以下载',
|
||||
PRIMARY KEY (id)
|
||||
) COMMENT '数据库迁移文件管理';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for t_db_sql
|
||||
-- ----------------------------
|
||||
@@ -647,7 +669,7 @@ INSERT INTO `t_sys_config` (name, `key`, params, value, remark, permission, crea
|
||||
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('ldap登录配置', 'LdapLogin', '[{"name":"是否启用","model":"enable","placeholder":"是否启用","options":"true,false"},{"name":"host","model":"host","placeholder":"host"},{"name":"port","model":"port","placeholder":"port"},{"name":"bindDN","model":"bindDN","placeholder":"LDAP 服务的管理员账号,如: \\"cn=admin,dc=example,dc=com\\""},{"name":"bindPwd","model":"bindPwd","placeholder":"LDAP 服务的管理员密码"},{"name":"baseDN","model":"baseDN","placeholder":"用户所在的 base DN, 如: \\"ou=users,dc=example,dc=com\\""},{"name":"userFilter","model":"userFilter","placeholder":"过滤用户的方式, 如: \\"(uid=%s)、(&(objectClass=organizationalPerson)(uid=%s))\\""},{"name":"uidMap","model":"uidMap","placeholder":"用户id和 LDAP 字段名之间的映射关系,如: cn"},{"name":"udnMap","model":"udnMap","placeholder":"用户姓名(dispalyName)和 LDAP 字段名之间的映射关系,如: displayName"},{"name":"emailMap","model":"emailMap","placeholder":"用户email和 LDAP 字段名之间的映射关系"},{"name":"skipTLSVerify","model":"skipTLSVerify","placeholder":"客户端是否跳过 TLS 证书验证","options":"true,false"},{"name":"安全协议","model":"securityProtocol","placeholder":"安全协议(为Null不使用安全协议),如: StartTLS, LDAPS","options":"Null,StartTLS,LDAPS"}]', '', 'ldap登录相关配置', 'admin,', '2023-08-25 21:47:20', 1, 'admin', '2023-08-25 22:56:07', 1, 'admin', 0, NULL);
|
||||
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('系统全局样式设置', 'SysStyleConfig', '[{"model":"logoIcon","name":"logo图标","placeholder":"系统logo图标(base64编码, 建议svg格式,不超过10k)","required":false},{"model":"title","name":"菜单栏标题","placeholder":"系统菜单栏标题展示","required":false},{"model":"viceTitle","name":"登录页标题","placeholder":"登录页标题展示","required":false},{"model":"useWatermark","name":"是否启用水印","placeholder":"是否启用系统水印","options":"true,false","required":false},{"model":"watermarkContent","name":"水印补充信息","placeholder":"额外水印信息","required":false}]', '{"title":"mayfly-go","viceTitle":"mayfly-go","logoIcon":"","useWatermark":"true","watermarkContent":""}', '系统icon、标题、水印信息等配置', 'all', '2024-01-04 15:17:18', 1, 'admin', '2024-01-05 09:40:44', 1, 'admin', 0, NULL);
|
||||
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('机器相关配置', 'MachineConfig', '[{"name":"终端回放存储路径","model":"terminalRecPath","placeholder":"终端回放存储路径"},{"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文件夹"},{"name":"guacd服务记录存储位置","model":"guacdRecPath","placeholder":"guacd服务记录存储位置,用于记录rdp操作记录"}]', '{"terminalRecPath":"./rec","uploadMaxFileSize":"1000MB","termOpSaveDays":"30","guacdHost":"","guacdPort":"","guacdFilePath":"./guacd/rdp-file","guacdRecPath":"./guacd/rdp-rec"}', '机器相关配置,如终端回放路径等', 'all', '2023-07-13 16:26:44', 1, 'admin', '2024-04-06 12:25:03', 1, 'admin', 0, NULL);
|
||||
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('数据库备份恢复', 'DbBackupRestore', '[{"model":"backupPath","name":"备份路径","placeholder":"备份文件存储路径"}]', '{"backupPath":"./db/backup"}', '', 'admin,', '2023-12-29 09:55:26', 1, 'admin', '2023-12-29 15:45:24', 1, 'admin', 0, NULL);
|
||||
INSERT INTO `t_sys_config` (`id`, `name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES(10, '数据库备份恢复', 'DbBackupRestore', '[{"model":"backupPath","name":"备份路径","placeholder":"备份文件存储路径"},{"model":"transferPath","name":"迁移路径","placeholder":"数据库迁移文件存储路径"}]', '{"backupPath":"./db/backup","transferPath":"./db/transfer"}', '数据库备份恢复', 'all', '2023-12-29 09:55:26', 1, 'admin', '2024-08-27 15:22:22', 12, 'liuzongyang', 0, NULL);
|
||||
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('Mysql可执行文件', 'MysqlBin', '[{"model":"path","name":"路径","placeholder":"可执行文件路径","required":true},{"model":"mysql","name":"mysql","placeholder":"mysql命令路径(空则为 路径/mysql)","required":false},{"model":"mysqldump","name":"mysqldump","placeholder":"mysqldump命令路径(空则为 路径/mysqldump)","required":false},{"model":"mysqlbinlog","name":"mysqlbinlog","placeholder":"mysqlbinlog命令路径(空则为 路径/mysqlbinlog)","required":false}]', '{"mysql":"","mysqldump":"","mysqlbinlog":"","path":"./db/mysql/bin"}', '', 'admin,', '2023-12-29 10:01:33', 1, 'admin', '2023-12-29 13:34:40', 1, 'admin', 0, NULL);
|
||||
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('MariaDB可执行文件', 'MariadbBin', '[{"model":"path","name":"路径","placeholder":"可执行文件路径","required":true},{"model":"mysql","name":"mysql","placeholder":"mysql命令路径(空则为 路径/mysql)","required":false},{"model":"mysqldump","name":"mysqldump","placeholder":"mysqldump命令路径(空则为 路径/mysqldump)","required":false},{"model":"mysqlbinlog","name":"mysqlbinlog","placeholder":"mysqlbinlog命令路径(空则为 路径/mysqlbinlog)","required":false}]', '{"mysql":"","mysqldump":"","mysqlbinlog":"","path":"./db/mariadb/bin"}', '', 'admin,', '2023-12-29 10:01:33', 1, 'admin', '2023-12-29 13:34:40', 1, 'admin', 0, NULL);
|
||||
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('DBMS配置', 'DbmsConfig', '[{"model":"querySqlSave","name":"记录查询sql","placeholder":"是否记录查询类sql","options":"true,false"},{"model":"maxResultSet","name":"最大结果集","placeholder":"允许sql查询的最大结果集数。注: 0=不限制","options":""},{"model":"sqlExecTl","name":"sql执行时间限制","placeholder":"超过该时间(单位:秒),执行将被取消"}]', '{"querySqlSave":"false","maxResultSet":"0","sqlExecTl":"60"}', 'DBMS相关配置', 'admin,', '2024-03-06 13:30:51', 1, 'admin', '2024-03-06 14:07:16', 1, 'admin', 0, NULL);
|
||||
@@ -831,6 +853,10 @@ INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `we
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709196723, 1709194669, 2, 1, '启停', 'db:transfer:status', 1709196723, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:52:04', '2024-02-29 16:52:04', 'SmLcpu6c/hGiLN1VT/', 0, NULL);
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709196737, 1709194669, 2, 1, '日志', 'db:transfer:log', 1709196737, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:52:17', '2024-02-29 16:52:17', 'SmLcpu6c/CZhNIbWg/', 0, NULL);
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1709196755, 1709194669, 2, 1, '运行', 'db:transfer:run', 1709196755, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:52:36', '2024-02-29 16:52:36', 'SmLcpu6c/b6yHt6V2/', 0, NULL);
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724376022, 1709194669, 2, 1, '文件-删除', 'db:transfer:files:del', 1724376022, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-23 09:20:23', '2024-08-23 14:50:21', 'SmLcpu6c/HIURtJJA/', 0, NULL);
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724395850, 1709194669, 2, 1, '文件-下载', 'db:transfer:files:down', 1724395850, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-23 14:50:51', '2024-08-23 14:50:51', 'SmLcpu6c/FmqK4azt/', 0, NULL);
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724398262, 1709194669, 2, 1, '文件', 'db:transfer:files', 1724376021, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-23 15:31:02', '2024-08-23 15:31:16', 'SmLcpu6c/btVtrbhk/', 0, NULL);
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724817775, 1709194669, 2, 1, '文件-重命名', 'db:transfer:files:rename', 1724376021, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-28 12:02:56', '2024-08-28 12:03:01', 'SmLcpu6c/zu4fvnuA/', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1714032002, 1713875842, '12sSjal1/UnWIUhW0/0tJwC3Gf/', 2, 1, '命令配置-删除', 'cmdconf:del', 1714032002, 'null', 1, 'admin', 1, 'admin', '2024-04-25 16:00:02', '2024-04-25 16:00:02', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1714031981, 1713875842, '12sSjal1/UnWIUhW0/tEzIKecl/', 2, 1, '命令配置-保存', 'cmdconf:save', 1714031981, 'null', 1, 'admin', 1, 'admin', '2024-04-25 15:59:41', '2024-04-25 15:59:41', 0, NULL);
|
||||
INSERT INTO t_sys_resource (id, pid, ui_path, `type`, status, name, code, weight, meta, creator_id, creator, modifier_id, modifier, create_time, update_time, is_deleted, delete_time) VALUES(1713875842, 2, '12sSjal1/UnWIUhW0/', 1, 1, '安全配置', 'security', 1713875842, '{"component":"ops/machine/security/SecurityConfList","icon":"Setting","isKeepAlive":true,"routeName":"SecurityConfList"}', 1, 'admin', 1, 'admin', '2024-04-23 20:37:22', '2024-04-23 20:37:22', 0, NULL);
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
|
||||
-- 数据同步新增字段
|
||||
ALTER TABLE `t_db_data_sync_task`
|
||||
ADD COLUMN `upd_field_src` varchar(100) DEFAULT NULL COMMENT '更新值来源字段,默认同更新字段,如果查询结果指定了字段别名且与原更新字段不一致,则取这个字段值为当前更新值';
|
||||
|
||||
-- 新增数据库迁移到文件的菜单资源
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724376022, 1709194669, 2, 1, '文件-删除', 'db:transfer:files:del', 1724376022, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-23 09:20:23', '2024-08-23 14:50:21', 'SmLcpu6c/HIURtJJA/', 0, NULL);
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724395850, 1709194669, 2, 1, '文件-下载', 'db:transfer:files:down', 1724395850, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-23 14:50:51', '2024-08-23 14:50:51', 'SmLcpu6c/FmqK4azt/', 0, NULL);
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724398262, 1709194669, 2, 1, '文件', 'db:transfer:files', 1724376021, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-23 15:31:02', '2024-08-23 15:31:16', 'SmLcpu6c/btVtrbhk/', 0, NULL);
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724817775, 1709194669, 2, 1, '文件-重命名', 'db:transfer:files:rename', 1724376021, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-28 12:02:56', '2024-08-28 12:03:01', 'SmLcpu6c/zu4fvnuA/', 0, NULL);
|
||||
INSERT INTO `t_sys_resource` (`id`, `pid`, `type`, `status`, `name`, `code`, `weight`, `meta`, `creator_id`, `creator`, `modifier_id`, `modifier`, `create_time`, `update_time`, `ui_path`, `is_deleted`, `delete_time`) VALUES(1724998419, 1709194669, 2, 1, '文件-执行', 'db:transfer:files:run', 1724998419, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-08-30 14:13:39', '2024-08-30 14:13:39', 'SmLcpu6c/qINungml/', 0, NULL);
|
||||
|
||||
-- 新增数据库迁移相关的系统配置
|
||||
DELETE FROM `t_sys_config` WHERE `key` = 'DbBackupRestore';
|
||||
INSERT INTO `t_sys_config` (`id`, `name`, `key`, `params`, `value`, `remark`, `permission`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted`, `delete_time`) VALUES(10, '数据库备份恢复', 'DbBackupRestore', '[{"model":"backupPath","name":"备份路径","placeholder":"备份文件存储路径"},{"model":"transferPath","name":"迁移路径","placeholder":"数据库迁移文件存储路径"}]', '{"backupPath":"./db/backup","transferPath":"./db/transfer"}', '数据库备份恢复', 'all', '2023-12-29 09:55:26', 1, 'admin', '2024-08-27 15:22:22', 12, 'liuzongyang', 0, NULL);
|
||||
|
||||
-- 数据库迁移到文件
|
||||
ALTER TABLE `t_db_transfer_task`
|
||||
ADD COLUMN `task_name` varchar(100) NULL comment '任务名' after `delete_time`;
|
||||
ADD COLUMN `task_name` varchar(100) NULL comment '任务名',
|
||||
ADD COLUMN `cron_able` TINYINT(3) NOT NULL DEFAULT 0 comment '是否定时 1是 -1否',
|
||||
ADD COLUMN `cron` VARCHAR(20) NULL comment '定时任务cron表达式',
|
||||
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 `status` tinyint(3) NOT NULL DEFAULT '1' comment '启用状态 1启用 -1禁用';
|
||||
|
||||
UPDATE `t_db_transfer_task` SET mode = 1 WHERE 1=1;
|
||||
UPDATE `t_db_transfer_task` SET cron_able = -1 WHERE 1=1;
|
||||
UPDATE `t_db_transfer_task` SET task_name = '未命名' WHERE task_name = '' or task_name is null;
|
||||
|
||||
CREATE TABLE `t_db_transfer_files` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`is_deleted` tinyint(3) NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||
`delete_time` datetime COMMENT '删除时间',
|
||||
`status` tinyint(3) NOT NULL DEFAULT 1 COMMENT '状态,1、执行中 2、执行失败 3、 执行成功',
|
||||
`task_id` bigint COMMENT '迁移任务ID',
|
||||
`log_id` bigint COMMENT '日志ID',
|
||||
`file_db_type` varchar(200) COMMENT 'sql文件数据库类型',
|
||||
`file_name` varchar(200) COMMENT '显式文件名 默认: 年月日时分秒.zip',
|
||||
`file_uuid` varchar(50) COMMENT '文件真实uuid,拼接后可以下载',
|
||||
PRIMARY KEY (id)
|
||||
) COMMENT '数据库迁移文件管理';
|
||||
|
||||
|
||||
ALTER TABLE `t_flow_procdef`
|
||||
ADD COLUMN `condition` text NULL comment '触发审批的条件(计算结果返回1则需要启用该流程)';
|
||||
Reference in New Issue
Block a user