mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-03 16:00:25 +08:00
!114 feat:rdp优化,mssql迁移优化,term支持trzsz
* fix: 合并代码 * refactor: rdp优化,mssql迁移优化,term支持trzsz
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
"sortablejs": "^1.15.2",
|
||||
"splitpanes": "^3.1.5",
|
||||
"sql-formatter": "^15.0.2",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,17 +6,28 @@
|
||||
<SvgIcon name="FolderOpened" @click="openFilesystem" :size="20" class="pointer-icon mr10" title="文件管理" />
|
||||
<SvgIcon name="FullScreen" @click="state.fullscreen ? closeFullScreen() : openFullScreen()" :size="20" class="pointer-icon mr10" title="全屏" />
|
||||
|
||||
<el-popconfirm @confirm="connect(0, 0)" title="确认重新连接?">
|
||||
<template #reference>
|
||||
<SvgIcon name="Refresh" :size="20" class="pointer-icon mr10" title="重新连接" />
|
||||
<el-dropdown>
|
||||
<SvgIcon name="Monitor" :size="20" class="pointer-icon mr10" title="发送快捷键" style="color: #fff" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="openSendKeyboard(['65507', '65513', '65535'])"> Ctrl + Alt + Delete </el-dropdown-item>
|
||||
<el-dropdown-item @click="openSendKeyboard(['65507', '65513', '65288'])"> Ctrl + Alt + Backspace </el-dropdown-item>
|
||||
<el-dropdown-item @click="openSendKeyboard(['65515', '100'])"> Windows + D </el-dropdown-item>
|
||||
<el-dropdown-item @click="openSendKeyboard(['65515', '101'])"> Windows + E </el-dropdown-item>
|
||||
<el-dropdown-item @click="openSendKeyboard(['65515', '114'])"> Windows + R </el-dropdown-item>
|
||||
<el-dropdown-item @click="openSendKeyboard(['65515'])"> Windows </el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</el-dropdown>
|
||||
|
||||
<SvgIcon name="Refresh" @click="connect(0, 0)" :size="20" class="pointer-icon mr10" title="重新连接" />
|
||||
</div>
|
||||
<clipboard-dialog ref="clipboardRef" v-model:visible="state.clipboardDialog.visible" @close="closePaste" @submit="onsubmitClipboard" />
|
||||
|
||||
<el-dialog destroy-on-close :title="state.filesystemDialog.title" v-model="state.filesystemDialog.visible" :close-on-click-modal="false" width="70%">
|
||||
<machine-file
|
||||
:machine-id="state.filesystemDialog.machineId"
|
||||
:auth-cert-name="state.filesystemDialog.authCertName"
|
||||
:protocol="state.filesystemDialog.protocol"
|
||||
:file-id="state.filesystemDialog.fileId"
|
||||
:path="state.filesystemDialog.path"
|
||||
@@ -36,6 +47,10 @@ import { TerminalExpose } from '@/components/terminal-rdp/index';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||
import { exitFullscreen, launchIntoFullscreen, unWatchFullscreenChange, watchFullscreenChange } from '@/components/terminal-rdp/guac/screen';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { debounce } from 'lodash';
|
||||
import { ClientState, TunnelState } from '@/components/terminal-rdp/guac/states';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const viewportRef = ref({} as any);
|
||||
const displayRef = ref({} as any);
|
||||
@@ -43,7 +58,11 @@ const clipboardRef = ref({} as any);
|
||||
|
||||
const props = defineProps({
|
||||
machineId: {
|
||||
type: [Number, String],
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
authCert: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
clipboardList: {
|
||||
@@ -76,6 +95,7 @@ const state = reactive({
|
||||
},
|
||||
filesystemDialog: {
|
||||
visible: false,
|
||||
authCertName: '',
|
||||
machineId: 0,
|
||||
protocol: 1,
|
||||
title: '',
|
||||
@@ -142,6 +162,11 @@ const installClipboard = () => {
|
||||
state.client.onclipboard = clipboard.onClipboard;
|
||||
};
|
||||
|
||||
const installResize = () => {
|
||||
// 在resize事件结束后300毫秒执行
|
||||
useEventListener('resize', debounce(resize, 300));
|
||||
};
|
||||
|
||||
const installDisplay = () => {
|
||||
let { width, height, force } = state.size;
|
||||
state.display = state.client.getDisplay();
|
||||
@@ -172,7 +197,7 @@ const installDisplay = () => {
|
||||
};
|
||||
|
||||
const installClient = () => {
|
||||
let tunnel = new Guacamole.WebSocketTunnel(getMachineRdpSocketUrl(props.machineId)) as any;
|
||||
let tunnel = new Guacamole.WebSocketTunnel(getMachineRdpSocketUrl(props.authCert)) as any;
|
||||
if (state.client) {
|
||||
state.display?.scale(0);
|
||||
uninstallKeyboard();
|
||||
@@ -191,15 +216,18 @@ const installClient = () => {
|
||||
console.log('statechange', st);
|
||||
state.status = st;
|
||||
switch (st) {
|
||||
case 0: // 'CONNECTING'
|
||||
case TunnelState.CONNECTING: // 'CONNECTING'
|
||||
break;
|
||||
case 1: // 'OPEN'
|
||||
case TunnelState.OPEN: // 'OPEN'
|
||||
state.status = TerminalStatus.Connected;
|
||||
emit('statusChange', TerminalStatus.Connected);
|
||||
break;
|
||||
case 2: // 'CLOSED'
|
||||
case TunnelState.CLOSED: // 'CLOSED'
|
||||
state.status = TerminalStatus.Disconnected;
|
||||
emit('statusChange', TerminalStatus.Disconnected);
|
||||
break;
|
||||
case 3: // 'UNSTABLE'
|
||||
case TunnelState.UNSTABLE: // 'UNSTABLE'
|
||||
state.status = TerminalStatus.Error;
|
||||
emit('statusChange', TerminalStatus.Error);
|
||||
break;
|
||||
}
|
||||
@@ -207,25 +235,25 @@ const installClient = () => {
|
||||
|
||||
state.client.onstatechange = (clientState: any) => {
|
||||
console.log('clientState', clientState);
|
||||
return;
|
||||
switch (clientState) {
|
||||
case 0:
|
||||
// states.IDLE;
|
||||
case ClientState.IDLE:
|
||||
console.log('连接空闲');
|
||||
break;
|
||||
case 1:
|
||||
break;
|
||||
case 2:
|
||||
case ClientState.CONNECTING:
|
||||
console.log('连接中...');
|
||||
break;
|
||||
case 3:
|
||||
case ClientState.WAITING:
|
||||
console.log('等待服务器响应...');
|
||||
break;
|
||||
case ClientState.CONNECTED:
|
||||
console.log('连接成功...');
|
||||
// states.CONNECTED;
|
||||
window.addEventListener('resize', resize);
|
||||
viewportRef.value.addEventListener('mouseenter', resize);
|
||||
clipboard.setRemoteClipboard(state.client);
|
||||
break;
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 4:
|
||||
case 5:
|
||||
case ClientState.DISCONNECTING:
|
||||
console.log('断开连接中...');
|
||||
break;
|
||||
case ClientState.DISCONNECTED:
|
||||
console.log('已断开连接...');
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -273,20 +301,25 @@ const resize = () => {
|
||||
|
||||
let box = elm.parentElement;
|
||||
|
||||
let pixelDensity = window.devicePixelRatio || 1;
|
||||
const width = box.clientWidth * pixelDensity;
|
||||
const height = box.clientHeight * pixelDensity;
|
||||
state.size.width = width;
|
||||
state.size.height = height;
|
||||
state.size.width = box.clientWidth;
|
||||
state.size.height = box.clientHeight;
|
||||
|
||||
const width = parseInt(String(box.clientWidth));
|
||||
const height = parseInt(String(box.clientHeight));
|
||||
|
||||
if (state.display.getWidth() !== width || state.display.getHeight() !== height) {
|
||||
if (state.status !== TerminalStatus.Connected) {
|
||||
connect(width, height);
|
||||
} else {
|
||||
state.client.sendSize(width, height);
|
||||
}
|
||||
}
|
||||
// setting timeout so display has time to get the correct size
|
||||
setTimeout(() => {
|
||||
const scale = Math.min(box.clientWidth / Math.max(state.display.getWidth(), 1), box.clientHeight / Math.max(state.display.getHeight(), 1));
|
||||
state.display.scale(scale);
|
||||
console.log(state.size);
|
||||
}, 100);
|
||||
// setTimeout(() => {
|
||||
// const scale = Math.min(box.clientWidth / Math.max(state.display.getWidth(), 1), box.clientHeight / Math.max(state.display.getHeight(), 1));
|
||||
// state.display.scale(scale);
|
||||
// console.log(state.size, scale);
|
||||
// }, 100);
|
||||
};
|
||||
|
||||
const handleMouseState = (mouseState: any, showCursor = false) => {
|
||||
@@ -318,6 +351,7 @@ const connect = (width: number, height: number, force = false) => {
|
||||
installMouse();
|
||||
installTouchpad();
|
||||
installClipboard();
|
||||
installResize();
|
||||
};
|
||||
|
||||
const disconnect = () => {
|
||||
@@ -348,6 +382,7 @@ const onsubmitClipboard = (val: string) => {
|
||||
const openFilesystem = async () => {
|
||||
state.filesystemDialog.protocol = 2;
|
||||
state.filesystemDialog.machineId = props.machineId;
|
||||
state.filesystemDialog.authCertName = props.authCert;
|
||||
state.filesystemDialog.fileId = props.machineId;
|
||||
state.filesystemDialog.path = '/';
|
||||
state.filesystemDialog.title = `远程桌面文件管理`;
|
||||
@@ -392,6 +427,19 @@ const closeFullScreen = function () {
|
||||
unWatchFullscreenChange(watchFullscreen);
|
||||
};
|
||||
|
||||
const openSendKeyboard = (keys: string[]) => {
|
||||
if (!state.client) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
state.client.sendKeyEvent(1, keys[i]);
|
||||
}
|
||||
for (let j = 0; j < keys.length; j++) {
|
||||
state.client.sendKeyEvent(0, keys[j]);
|
||||
}
|
||||
ElMessage.success('发送组合键成功');
|
||||
};
|
||||
|
||||
const exposes = {
|
||||
connect,
|
||||
disconnect,
|
||||
|
||||
@@ -1,55 +1,78 @@
|
||||
export default {
|
||||
export const ClientState = {
|
||||
/**
|
||||
* The Guacamole connection has not yet been attempted.
|
||||
* The client is idle, with no active connection.
|
||||
*
|
||||
* @type String
|
||||
* @type number
|
||||
*/
|
||||
IDLE : "IDLE",
|
||||
IDLE: 0,
|
||||
|
||||
/**
|
||||
* The Guacamole connection is being established.
|
||||
* The client is in the process of establishing a connection.
|
||||
*
|
||||
* @type String
|
||||
* @type {!number}
|
||||
*/
|
||||
CONNECTING : "CONNECTING",
|
||||
CONNECTING: 1,
|
||||
|
||||
/**
|
||||
* The Guacamole connection has been successfully established, and the
|
||||
* client is now waiting for receipt of initial graphical data.
|
||||
* The client is waiting on further information or a remote server to
|
||||
* establish the connection.
|
||||
*
|
||||
* @type String
|
||||
* @type {!number}
|
||||
*/
|
||||
WAITING : "WAITING",
|
||||
WAITING: 2,
|
||||
|
||||
/**
|
||||
* The Guacamole connection has been successfully established, and
|
||||
* initial graphical data has been received.
|
||||
* The client is actively connected to a remote server.
|
||||
*
|
||||
* @type String
|
||||
* @type {!number}
|
||||
*/
|
||||
CONNECTED : "CONNECTED",
|
||||
CONNECTED: 3,
|
||||
|
||||
/**
|
||||
* The Guacamole connection has terminated successfully. No errors are
|
||||
* indicated.
|
||||
* The client is in the process of disconnecting from the remote server.
|
||||
*
|
||||
* @type String
|
||||
* @type {!number}
|
||||
*/
|
||||
DISCONNECTED : "DISCONNECTED",
|
||||
DISCONNECTING: 4,
|
||||
|
||||
/**
|
||||
* The Guacamole connection has terminated due to an error reported by
|
||||
* the client. The associated error code is stored in statusCode.
|
||||
* The client has completed the connection and is no longer connected.
|
||||
*
|
||||
* @type String
|
||||
* @type {!number}
|
||||
*/
|
||||
CLIENT_ERROR : "CLIENT_ERROR",
|
||||
DISCONNECTED: 5,
|
||||
};
|
||||
|
||||
export const TunnelState = {
|
||||
/**
|
||||
* A connection is in pending. It is not yet known whether connection was
|
||||
* successful.
|
||||
*
|
||||
* @type {!number}
|
||||
*/
|
||||
CONNECTING: 0,
|
||||
|
||||
/**
|
||||
* The Guacamole connection has terminated due to an error reported by
|
||||
* the tunnel. The associated error code is stored in statusCode.
|
||||
* Connection was successful, and data is being received.
|
||||
*
|
||||
* @type String
|
||||
* @type {!number}
|
||||
*/
|
||||
TUNNEL_ERROR : "TUNNEL_ERROR"
|
||||
}
|
||||
OPEN: 1,
|
||||
|
||||
/**
|
||||
* The connection is closed. Connection may not have been successful, the
|
||||
* tunnel may have been explicitly closed by either side, or an error may
|
||||
* have occurred.
|
||||
*
|
||||
* @type {!number}
|
||||
*/
|
||||
CLOSED: 2,
|
||||
|
||||
/**
|
||||
* The connection is open, but communication through the tunnel appears to
|
||||
* be disrupted, and the connection may close as a result.
|
||||
*
|
||||
* @type {!number}
|
||||
*/
|
||||
UNSTABLE: 3,
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import { debounce } from 'lodash';
|
||||
import { TerminalStatus } from './common';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import themes from './themes';
|
||||
import { TrzszFilter } from 'trzsz';
|
||||
|
||||
const props = defineProps({
|
||||
// mounted时,是否执行init方法
|
||||
@@ -101,7 +102,6 @@ function init() {
|
||||
}
|
||||
nextTick(() => {
|
||||
initTerm();
|
||||
initSocket();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,23 +119,10 @@ function initTerm() {
|
||||
|
||||
term.open(terminalRef.value);
|
||||
|
||||
// 注册自适应组件
|
||||
const fitAddon = new FitAddon();
|
||||
state.addon.fit = fitAddon;
|
||||
term.loadAddon(fitAddon);
|
||||
fitTerminal();
|
||||
// 注册窗口大小监听器
|
||||
useEventListener('resize', debounce(fitTerminal, 400));
|
||||
initSocket();
|
||||
|
||||
// 注册搜索组件
|
||||
const searchAddon = new SearchAddon();
|
||||
state.addon.search = searchAddon;
|
||||
term.loadAddon(searchAddon);
|
||||
|
||||
// 注册 url link组件
|
||||
const weblinks = new WebLinksAddon();
|
||||
state.addon.weblinks = weblinks;
|
||||
term.loadAddon(weblinks);
|
||||
// 注册插件
|
||||
loadAddon();
|
||||
|
||||
// 注册自定义快捷键
|
||||
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
||||
@@ -153,7 +140,6 @@ function initSocket() {
|
||||
if (!props.socketUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
|
||||
// 监听socket连接
|
||||
socket.onopen = () => {
|
||||
@@ -161,10 +147,6 @@ function initSocket() {
|
||||
pingInterval = setInterval(sendPing, 15000);
|
||||
state.status = TerminalStatus.Connected;
|
||||
|
||||
// 注册 terminal 事件
|
||||
term.onResize((event) => sendResize(event.cols, event.rows));
|
||||
term.onData((event) => sendCmd(event));
|
||||
|
||||
focus();
|
||||
|
||||
// 如果有初始要执行的命令,则发送执行命令
|
||||
@@ -184,12 +166,60 @@ function initSocket() {
|
||||
console.log('terminal socket close...', e.reason);
|
||||
state.status = TerminalStatus.Disconnected;
|
||||
};
|
||||
}
|
||||
|
||||
// 监听socket消息
|
||||
socket.onmessage = (msg: any) => {
|
||||
// msg.data是真正后端返回的数据
|
||||
write2Term(msg.data);
|
||||
};
|
||||
function loadAddon() {
|
||||
// 注册自适应组件
|
||||
const fitAddon = new FitAddon();
|
||||
state.addon.fit = fitAddon;
|
||||
term.loadAddon(fitAddon);
|
||||
fitTerminal();
|
||||
// 注册窗口大小监听器
|
||||
useEventListener('resize', debounce(fitTerminal, 400));
|
||||
|
||||
// 注册搜索组件
|
||||
const searchAddon = new SearchAddon();
|
||||
state.addon.search = searchAddon;
|
||||
term.loadAddon(searchAddon);
|
||||
|
||||
// 注册 url link组件
|
||||
const weblinks = new WebLinksAddon();
|
||||
state.addon.weblinks = weblinks;
|
||||
term.loadAddon(weblinks);
|
||||
|
||||
// 注册 trzsz
|
||||
// initialize trzsz filter
|
||||
const trzsz = new TrzszFilter({
|
||||
// write the server output to the terminal
|
||||
writeToTerminal: (data: any) => term.write(typeof data === 'string' ? data : new Uint8Array(data)),
|
||||
// send the user input to the server
|
||||
sendToServer: sendCmd,
|
||||
// the terminal columns
|
||||
terminalColumns: term.cols,
|
||||
// there is a windows shell
|
||||
isWindowsShell: false,
|
||||
});
|
||||
|
||||
// let trzsz process the server output
|
||||
socket?.addEventListener('message', (e) => trzsz.processServerOutput(e.data));
|
||||
// let trzsz process the user input
|
||||
term.onData((data) => trzsz.processTerminalInput(data));
|
||||
term.onBinary((data) => trzsz.processBinaryInput(data));
|
||||
term.onResize((size) => {
|
||||
sendResize(size.cols, size.rows);
|
||||
// tell trzsz the terminal columns has been changed
|
||||
trzsz.setTerminalColumns(size.cols);
|
||||
});
|
||||
window.addEventListener('resize', () => state.addon.fit.fit());
|
||||
// enable drag files or directories to upload
|
||||
terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault());
|
||||
terminalRef.value.addEventListener('drop', (event: any) => {
|
||||
event.preventDefault();
|
||||
trzsz
|
||||
.uploadFiles(event.dataTransfer.items)
|
||||
.then(() => console.log('upload success'))
|
||||
.catch((err) => console.log(err));
|
||||
});
|
||||
}
|
||||
|
||||
// 写入内容至终端
|
||||
|
||||
@@ -51,15 +51,15 @@ const extra = computed(() => {
|
||||
// 定时获取最新日志
|
||||
const { pause, resume } = useIntervalFn(() => {
|
||||
writeLog();
|
||||
}, 2000);
|
||||
}, 500);
|
||||
|
||||
watch(
|
||||
() => logId.value,
|
||||
(logId: number) => {
|
||||
terminalRef.value?.clear();
|
||||
if (!logId) {
|
||||
return;
|
||||
}
|
||||
terminalRef.value?.clear();
|
||||
writeLog();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -34,11 +34,9 @@
|
||||
<template #action="{ data }">
|
||||
<!-- 删除、启停用、编辑 -->
|
||||
<el-button v-if="actionBtns[perms.save]" @click="edit(data)" type="primary" link>编辑</el-button>
|
||||
<el-button :disabled="state.runBtnDisabled" v-if="actionBtns[perms.log]" type="primary" link @click="log(data)">日志</el-button>
|
||||
<el-button v-if="actionBtns[perms.log]" type="primary" link @click="log(data)">日志</el-button>
|
||||
<el-button v-if="data.runningState === 1" @click="stop(data.id)" type="danger" link>停止</el-button>
|
||||
<el-button :disabled="state.runBtnDisabled" 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" type="primary" link @click="reRun(data)">运行</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
@@ -116,7 +114,6 @@ const state = reactive({
|
||||
data: null as any,
|
||||
running: false,
|
||||
},
|
||||
runBtnDisabled: false,
|
||||
});
|
||||
|
||||
const { selectionData, query, editDialog, logsDialog } = toRefs(state);
|
||||
@@ -153,7 +150,7 @@ const stop = async (id: any) => {
|
||||
search();
|
||||
};
|
||||
|
||||
const log = async (data: any) => {
|
||||
const log = (data: any) => {
|
||||
state.logsDialog.logId = data.logId;
|
||||
state.logsDialog.visible = true;
|
||||
state.logsDialog.title = '数据库迁移日志';
|
||||
@@ -167,16 +164,17 @@ const reRun = async (data: any) => {
|
||||
type: 'warning',
|
||||
});
|
||||
try {
|
||||
state.runBtnDisabled = true;
|
||||
await dbApi.runDbTransferTask.request({ taskId: data.id });
|
||||
let res = await dbApi.runDbTransferTask.request({ taskId: data.id });
|
||||
console.log(res);
|
||||
ElMessage.success('运行成功');
|
||||
// 拿到日志id之后,弹出日志弹窗
|
||||
log({ logId: res, state: 1 });
|
||||
} catch (e) {
|
||||
state.runBtnDisabled = false;
|
||||
//
|
||||
}
|
||||
// 延迟2秒执行,后端异步执行
|
||||
setTimeout(() => {
|
||||
search();
|
||||
state.runBtnDisabled = false;
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
|
||||
@@ -237,7 +237,11 @@ watch(props, (newValue: any) => {
|
||||
}
|
||||
if (newValue.data) {
|
||||
state.form = { ...newValue.data };
|
||||
try {
|
||||
state.extra = JSON.parse(state.form.extra);
|
||||
} catch (e) {
|
||||
state.extra = {};
|
||||
}
|
||||
} else {
|
||||
state.form = { port: null, type: DbType.mysql } as any;
|
||||
state.form.authCerts = [];
|
||||
|
||||
@@ -337,7 +337,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
])
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
let { id, db, type, flowProcdefKey } = params;
|
||||
let { id, db, type, flowProcdefKey, schema } = params;
|
||||
// 获取当前库的所有表信息
|
||||
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||
state.reloadStatus = false;
|
||||
@@ -352,6 +352,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
|
||||
id,
|
||||
db,
|
||||
type,
|
||||
schema,
|
||||
flowProcdefKey: flowProcdefKey,
|
||||
key: key,
|
||||
parentKey: parentNode.key,
|
||||
@@ -684,7 +685,7 @@ const onEditTable = async (data: any) => {
|
||||
};
|
||||
|
||||
const onDeleteTable = async (data: any) => {
|
||||
let { db, id, tableName, parentKey, flowProcdefKey } = data.params;
|
||||
let { db, id, tableName, parentKey, flowProcdefKey, schema } = data.params;
|
||||
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
@@ -692,7 +693,10 @@ const onDeleteTable = async (data: any) => {
|
||||
});
|
||||
|
||||
// 执行sql
|
||||
dbApi.sqlExec.request({ id, db, sql: `drop table ${getDbDialect(state.nowDbInst.type).quoteIdentifier(tableName)}` }).then(() => {
|
||||
let dialect = getDbDialect(state.nowDbInst.type);
|
||||
let schemaStr = schema ? `${dialect.quoteIdentifier(schema)}.` : '';
|
||||
|
||||
dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then(() => {
|
||||
if (flowProcdefKey) {
|
||||
ElMessage.success('工单提交成功');
|
||||
return;
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
<machine-rdp
|
||||
v-if="dt.params.protocol != MachineProtocolEnum.Ssh.value"
|
||||
:machine-id="dt.params.id"
|
||||
:auth-cert="dt.authCert"
|
||||
:ref="(el: any) => setTerminalRef(el, dt.key)"
|
||||
@status-change="terminalStatusChange(dt.key, $event)"
|
||||
/>
|
||||
@@ -343,7 +344,8 @@ const openTerminal = (machine: any, ex?: boolean) => {
|
||||
const { href } = router.resolve({
|
||||
path: `/machine/terminal-rdp`,
|
||||
query: {
|
||||
id: machine.id,
|
||||
machineId: machine.id,
|
||||
ac: ac,
|
||||
name: machine.name,
|
||||
},
|
||||
});
|
||||
@@ -367,6 +369,7 @@ const openTerminal = (machine: any, ex?: boolean) => {
|
||||
key,
|
||||
label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
|
||||
params: machine,
|
||||
authCert: ac,
|
||||
socketUrl: getMachineTerminalSocketUrl(ac),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
<template>
|
||||
<div class="terminal-wrapper" ref="terminalWrapperRef">
|
||||
<machine-rdp ref="rdpRef" :machine-id="route.query.ac" />
|
||||
<machine-rdp ref="rdpRef" :auth-cert="state.authCert" :machine-id="state.machineId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { TerminalExpose } from '@/components/terminal-rdp';
|
||||
const route = useRoute();
|
||||
|
||||
const rdpRef = ref({} as TerminalExpose);
|
||||
const terminalWrapperRef = ref({} as any);
|
||||
|
||||
const state = computed(() => {
|
||||
return {
|
||||
authCert: route.query.ac as string,
|
||||
machineId: Number(route.query.machineId),
|
||||
};
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
let width = terminalWrapperRef.value.clientWidth;
|
||||
let height = terminalWrapperRef.value.clientHeight;
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
<SvgIcon :size="15" name="folder" color="#007AFF" />
|
||||
</span>
|
||||
<span v-else>
|
||||
<SvgIcon :size="15" name="document" />
|
||||
<SvgIcon :size="15" :name="scope.row.icon" />
|
||||
</span>
|
||||
|
||||
<span class="ml5" style="display: inline-block; width: 90%">
|
||||
@@ -520,8 +520,84 @@ const lsFile = async (path: string) => {
|
||||
const type = file.type;
|
||||
if (type == folderType) {
|
||||
file.isFolder = true;
|
||||
file.iocn = 'folder';
|
||||
} else {
|
||||
file.isFolder = false;
|
||||
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||||
|
||||
switch (fileExtension) {
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
file.icon = 'iconfont icon-word';
|
||||
break;
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
file.icon = 'iconfont icon-excel';
|
||||
break;
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
file.icon = 'iconfont icon-ppt';
|
||||
break;
|
||||
case 'pdf':
|
||||
file.icon = 'iconfont icon-pdf';
|
||||
break;
|
||||
case 'xml':
|
||||
file.icon = 'iconfont icon-xml';
|
||||
break;
|
||||
case 'html':
|
||||
file.icon = 'iconfont icon-html';
|
||||
break;
|
||||
case 'yaml':
|
||||
case 'yml':
|
||||
file.icon = 'iconfont icon-yaml';
|
||||
break;
|
||||
case 'css':
|
||||
file.icon = 'iconfont icon-file-css';
|
||||
break;
|
||||
case 'js':
|
||||
case 'ts':
|
||||
file.icon = 'iconfont icon-file-js';
|
||||
break;
|
||||
case 'mp4':
|
||||
case 'rmvb':
|
||||
file.icon = 'iconfont icon-file-video';
|
||||
break;
|
||||
case 'mp3':
|
||||
file.icon = 'iconfont icon-file-audio';
|
||||
break;
|
||||
case 'bmp':
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'png':
|
||||
case 'tif':
|
||||
case 'gif':
|
||||
case 'pcx':
|
||||
case 'tga':
|
||||
case 'exif':
|
||||
case 'svg':
|
||||
case 'psd':
|
||||
case 'ai':
|
||||
case 'webp':
|
||||
file.icon = 'iconfont icon-file-image';
|
||||
break;
|
||||
case 'md':
|
||||
file.icon = 'iconfont icon-md';
|
||||
break;
|
||||
case 'txt':
|
||||
file.icon = 'iconfont icon-txt';
|
||||
break;
|
||||
case 'zip':
|
||||
case 'rar':
|
||||
case '7z':
|
||||
case 'gz':
|
||||
case 'tar':
|
||||
case 'tgz':
|
||||
file.icon = 'iconfont icon-file-zip';
|
||||
break;
|
||||
default:
|
||||
file.icon = 'iconfont icon-file';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
@@ -631,8 +707,9 @@ const downloadFile = (data: any) => {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute(
|
||||
'href',
|
||||
`${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/download?path=${data.path}&machineId=${props.machineId}&authCertName=${props.authCertName}&protocol=${props.protocol}&${joinClientParams()}`
|
||||
`${config.baseApiUrl}/machines/${props.machineId}/files/${props.fileId}/download?path=${data.path}&machineId=${props.machineId}&authCertName=${props.authCertName}&fileId=${props.fileId}&protocol=${props.protocol}&${joinClientParams()}`
|
||||
);
|
||||
a.setAttribute('target', '_blank');
|
||||
a.click();
|
||||
};
|
||||
|
||||
|
||||
@@ -43,7 +43,10 @@ func (d *DbTransferTask) DeleteTask(rc *req.Ctx) {
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) Run(rc *req.Ctx) {
|
||||
go d.DbTransferTask.Run(rc.MetaCtx, uint64(rc.PathParamInt("taskId")))
|
||||
taskId := uint64(rc.PathParamInt("taskId"))
|
||||
logId, _ := d.DbTransferTask.CreateLog(rc.MetaCtx, taskId)
|
||||
go d.DbTransferTask.Run(rc.MetaCtx, taskId, logId)
|
||||
rc.ResData = logId
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) Stop(rc *req.Ctx) {
|
||||
|
||||
@@ -34,7 +34,9 @@ type DbTransferTask interface {
|
||||
|
||||
InitJob()
|
||||
|
||||
Run(ctx context.Context, taskId uint64)
|
||||
CreateLog(ctx context.Context, taskId uint64) (uint64, error)
|
||||
|
||||
Run(ctx context.Context, taskId uint64, logId uint64)
|
||||
|
||||
Stop(ctx context.Context, taskId uint64) error
|
||||
}
|
||||
@@ -81,19 +83,24 @@ func (app *dbTransferAppImpl) InitJob() {
|
||||
_ = gormx.Updates(taskParam, taskParam, updateMap)
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64) {
|
||||
func (app *dbTransferAppImpl) CreateLog(ctx context.Context, taskId uint64) (uint64, error) {
|
||||
logId, err := app.logApp.CreateLog(ctx, &sysapp.CreateLogReq{
|
||||
Description: "DBMS-执行数据迁移",
|
||||
ReqParam: collx.Kvs("taskId", taskId),
|
||||
Type: sysentity.SyslogTypeRunning,
|
||||
Resp: "开始执行数据迁移...",
|
||||
})
|
||||
return logId, err
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64, logId uint64) {
|
||||
task, err := app.GetById(new(entity.DbTransferTask), taskId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
logId, err := app.logApp.CreateLog(ctx, &sysapp.CreateLogReq{
|
||||
Description: "DBMS-执行数据迁移",
|
||||
ReqParam: collx.Kvs("taskId", task.Id),
|
||||
Type: sysentity.SyslogTypeRunning,
|
||||
Resp: "开始执行数据迁移...",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logx.Errorf("创建DBMS-执行数据迁移日志失败:%v", err)
|
||||
return
|
||||
|
||||
@@ -65,7 +65,7 @@ select a.owner,
|
||||
case when t.INFO2 & 0x01 = 0x01 then 1 else 0 end as IS_IDENTITY,
|
||||
case when t2.constraint_type = 'P' then 1 else 0 end as IS_PRIMARY_KEY
|
||||
from all_tab_columns a
|
||||
left join user_col_comments b
|
||||
left join all_col_comments b
|
||||
on b.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
|
||||
and b.table_name = a.table_name
|
||||
and a.column_name = b.column_name
|
||||
@@ -74,8 +74,8 @@ from all_tab_columns a
|
||||
join SYS.all_objects c2 on c1.id = c2.object_id and c2.object_type = 'TABLE') t
|
||||
on t.object_name = a.table_name and t.owner = a.owner and t.NAME = a.column_name
|
||||
left join (select uc.OWNER, uic.column_name, uic.table_name, uc.constraint_type
|
||||
from user_ind_columns uic
|
||||
left join user_constraints uc on uic.index_name = uc.index_name) t2
|
||||
from all_ind_columns uic
|
||||
left join all_constraints uc on uic.index_name = uc.index_name) t2
|
||||
on t2.table_name = t.object_name and a.column_name = t2.column_name and t2.OWNER = a.owner
|
||||
where a.owner = (SELECT SF_GET_SCHEMA_NAME_BY_ID(CURRENT_SCHID))
|
||||
and a.table_name in (%s)
|
||||
|
||||
@@ -2,6 +2,7 @@ package mssql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/pkg/utils/anyx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
@@ -194,6 +195,19 @@ func (ch *ColumnHelper) ToColumn(commonColumn *dbi.Column) {
|
||||
} else {
|
||||
commonColumn.DataType = dbi.ColumnDataType(ctype)
|
||||
ch.FixColumn(commonColumn)
|
||||
// 修复数据库迁移字段长度
|
||||
dataType := string(commonColumn.DataType)
|
||||
if collx.ArrayAnyMatches([]string{"nvarchar", "nchar"}, dataType) {
|
||||
commonColumn.CharMaxLength = commonColumn.CharMaxLength * 2
|
||||
}
|
||||
|
||||
if collx.ArrayAnyMatches([]string{"char"}, dataType) {
|
||||
// char最大长度4000
|
||||
if commonColumn.CharMaxLength >= 4000 {
|
||||
commonColumn.DataType = "ntext"
|
||||
commonColumn.CharMaxLength = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,12 +228,29 @@ func (ch *ColumnHelper) FixColumn(column *dbi.Column) {
|
||||
// 如果是nvarchar,可视长度减半
|
||||
column.CharMaxLength = column.CharMaxLength / 2
|
||||
}
|
||||
|
||||
if collx.ArrayAnyMatches([]string{"char"}, dataType) {
|
||||
// char最大长度4000
|
||||
if column.CharMaxLength >= 4000 {
|
||||
column.DataType = "ntext"
|
||||
column.CharMaxLength = 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type DumpHelper struct {
|
||||
dbi.DefaultDumpHelper
|
||||
}
|
||||
|
||||
// mssql 在insert语句前后不能识别begin和commit语句
|
||||
func (dh *DumpHelper) BeforeInsert(writer io.Writer, tableName string) {
|
||||
}
|
||||
|
||||
// mssql 在insert语句前后不能识别begin和commit语句
|
||||
func (dh *DumpHelper) AfterInsert(writer io.Writer, tableName string, columns []dbi.Column) {
|
||||
}
|
||||
|
||||
func (dh *DumpHelper) BeforeInsertSql(quoteSchema string, tableName string) string {
|
||||
return fmt.Sprintf("set identity_insert %s.%s on ", quoteSchema, tableName)
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ func (md *MssqlMetaData) GenerateIndexDDL(indexs []dbi.Index, tableInfo dbi.Tabl
|
||||
colNames[i] = meta.QuoteIdentifier(name)
|
||||
}
|
||||
|
||||
sqls = append(sqls, fmt.Sprintf("create %s NONCLUSTERED index %s on %s.%s(%s)", unique, index.IndexName, md.dc.Info.CurrentSchema(), tbName, strings.Join(colNames, ",")))
|
||||
sqls = append(sqls, fmt.Sprintf("create %s NONCLUSTERED index %s on %s.%s(%s)", unique, index.IndexName, meta.QuoteIdentifier(md.dc.Info.CurrentSchema()), meta.QuoteIdentifier(tbName), strings.Join(colNames, ",")))
|
||||
if index.IndexComment != "" {
|
||||
comment := meta.QuoteEscape(index.IndexComment)
|
||||
comments = append(comments, fmt.Sprintf("EXECUTE sp_addextendedproperty N'MS_Description', N'%s', N'SCHEMA', N'%s', N'TABLE', N'%s', N'INDEX', N'%s'", comment, md.dc.Info.CurrentSchema(), tbName, index.IndexName))
|
||||
@@ -304,17 +304,18 @@ func (md *MssqlMetaData) genColumnBasicSql(column dbi.Column) string {
|
||||
// 获取建表ddl
|
||||
func (md *MssqlMetaData) GenerateTableDDL(columns []dbi.Column, tableInfo dbi.Table, dropBeforeCreate bool) []string {
|
||||
tbName := tableInfo.TableName
|
||||
schemaName := md.dc.Info.CurrentSchema()
|
||||
meta := md.dc.GetMetaData()
|
||||
|
||||
sqlArr := make([]string, 0)
|
||||
|
||||
// 删除表
|
||||
if dropBeforeCreate {
|
||||
sqlArr = append(sqlArr, fmt.Sprintf("DROP TABLE IF EXISTS %s", meta.QuoteIdentifier(tbName)))
|
||||
sqlArr = append(sqlArr, fmt.Sprintf("DROP TABLE IF EXISTS %s.%s", meta.QuoteIdentifier(schemaName), meta.QuoteIdentifier(tbName)))
|
||||
}
|
||||
|
||||
// 组装建表语句
|
||||
createSql := fmt.Sprintf("CREATE TABLE %s (\n", meta.QuoteIdentifier(tbName))
|
||||
createSql := fmt.Sprintf("CREATE TABLE %s.%s (\n", meta.QuoteIdentifier(schemaName), meta.QuoteIdentifier(tbName))
|
||||
fields := make([]string, 0)
|
||||
pks := make([]string, 0)
|
||||
columnComments := make([]string, 0)
|
||||
|
||||
@@ -232,11 +232,6 @@ func (m *machineFileAppImpl) MkDir(opParam *MachineFileOpParam) (*mcm.MachineInf
|
||||
}
|
||||
|
||||
func (m *machineFileAppImpl) CreateFile(opParam *MachineFileOpParam) (*mcm.MachineInfo, error) {
|
||||
mi, sftpCli, err := m.GetMachineSftpCli(opParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path := opParam.Path
|
||||
if opParam.Protocol == entity.MachineProtocolRdp {
|
||||
path = m.GetRdpFilePath(opParam.MachineId, path)
|
||||
@@ -245,6 +240,10 @@ func (m *machineFileAppImpl) CreateFile(opParam *MachineFileOpParam) (*mcm.Machi
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mi, sftpCli, err := m.GetMachineSftpCli(opParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := sftpCli.Create(path)
|
||||
if err != nil {
|
||||
return nil, errorx.NewBiz("创建文件失败: %s", err.Error())
|
||||
|
||||
@@ -19,10 +19,11 @@ import (
|
||||
func DoConnect(query url.Values, parameters map[string]string, ac string) (Tunnel, error) {
|
||||
conf := NewGuacamoleConfiguration()
|
||||
|
||||
parameters["enable-wallpaper"] = "true" // 允许显示墙纸
|
||||
//parameters["resize-method"] = "reconnect"
|
||||
parameters["client-name"] = "mayfly"
|
||||
parameters["enable-wallpaper"] = "true"
|
||||
parameters["resize-method"] = "display-update"
|
||||
parameters["enable-font-smoothing"] = "true"
|
||||
parameters["enable-desktop-composition"] = "true"
|
||||
parameters["enable-desktop-composition"] = "false"
|
||||
parameters["enable-menu-animations"] = "false"
|
||||
parameters["disable-bitmap-caching"] = "true"
|
||||
parameters["disable-offscreen-caching"] = "true"
|
||||
@@ -60,7 +61,7 @@ func DoConnect(query url.Values, parameters map[string]string, ac string) (Tunne
|
||||
|
||||
//conf.ConnectionID = uuid.New().String()
|
||||
|
||||
conf.AudioMimetypes = []string{"audio/L8", "audio/L16"}
|
||||
conf.AudioMimetypes = []string{"audio/L16", "rate=44100", "channels=2"}
|
||||
conf.ImageMimetypes = []string{"image/jpeg", "image/png", "image/webp"}
|
||||
|
||||
logx.Debug("Connecting to guacd")
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
|
||||
DELETE
|
||||
FROM `t_sys_config`
|
||||
WHERE `key` in ('DbQueryMaxCount', 'DbSaveQuerySQL');
|
||||
|
||||
-- DBMS配置变动
|
||||
DELETE FROM `t_sys_config` WHERE `key` in ('DbQueryMaxCount', 'DbSaveQuerySQL');
|
||||
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);
|
||||
|
||||
ALTER TABLE t_db_instance CHANGE sid extra varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '连接需要的额外参数,如oracle数据库需要sid等';
|
||||
ALTER TABLE t_db_instance MODIFY COLUMN extra varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '连接需要的额外参数,如oracle数据库需要sid等';
|
||||
ALTER TABLE `t_db_instance` CHANGE sid extra varchar(255) NULL COMMENT '连接需要的额外参数,如oracle数据库需要sid等';
|
||||
ALTER TABLE `t_db_instance` MODIFY COLUMN extra varchar(255) NULL COMMENT '连接需要的额外参数,如oracle数据库需要sid等';
|
||||
|
||||
-- 数据迁移相关
|
||||
CREATE TABLE `t_db_transfer_task` (
|
||||
@@ -37,7 +34,6 @@ CREATE TABLE `t_db_transfer_task` (
|
||||
`log_id` bigint(20) NOT NULL COMMENT '日志id',
|
||||
PRIMARY KEY (`id`)
|
||||
) 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(1709194669, 36, 1, 1, '数据库迁移', 'transfer', 1709194669, '{"component":"ops/db/DbTransferList","icon":"Switch","isKeepAlive":true,"routeName":"DbTransferList"}', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:17:50', '2024-02-29 16:24:59', 'SmLcpu6c/', 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(1709194694, 1709194669, 2, 1, '基本权限', 'db:transfer', 1709194694, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:18:14', '2024-02-29 16:18:14', 'SmLcpu6c/A9vAm4J8/', 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(1709196697, 1709194669, 2, 1, '编辑', 'db:transfer:save', 1709196697, 'null', 12, 'liuzongyang', 12, 'liuzongyang', '2024-02-29 16:51:37', '2024-02-29 16:51:37', 'SmLcpu6c/5oJwPzNb/', 0, NULL);
|
||||
@@ -45,143 +41,131 @@ 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);
|
||||
|
||||
ALTER TABLE t_sys_log ADD extra varchar(5000) NULL;
|
||||
ALTER TABLE t_sys_log ADD extra text NULL;
|
||||
ALTER TABLE t_sys_log MODIFY COLUMN resp text NULL;
|
||||
|
||||
-- rdp相关
|
||||
ALTER TABLE `t_machine` ADD COLUMN `protocol` tinyint(2) NULL COMMENT '协议 1、SSH 2、RDP' AFTER `name`;
|
||||
update `t_machine` set `protocol` = 1 where `protocol` is NULL;
|
||||
delete from `t_sys_config` where `key` = 'MachineConfig';
|
||||
INSERT INTO t_sys_config ( name, `key`, params, value, remark, permission, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted, delete_time) VALUES('机器相关配置', '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('机器相关配置', '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);
|
||||
|
||||
-- 授权凭证相关
|
||||
CREATE TABLE `t_resource_auth_cert` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) DEFAULT NULL COMMENT '账号名称',
|
||||
`resource_code` varchar(36) DEFAULT NULL COMMENT '资源编码',
|
||||
`resource_type` tinyint NOT NULL COMMENT '资源类型',
|
||||
`type` tinyint DEFAULT NULL COMMENT '凭证类型',
|
||||
`username` varchar(100) DEFAULT NULL COMMENT '用户名',
|
||||
`ciphertext` varchar(5000) DEFAULT NULL COMMENT '密文内容',
|
||||
`ciphertext_type` tinyint NOT NULL COMMENT '密文类型(-1.公共授权凭证 1.密码 2.秘钥)',
|
||||
`extra` varchar(200) DEFAULT NULL COMMENT '账号需要的其他额外信息(如秘钥口令等)',
|
||||
`remark` varchar(50) DEFAULT NULL COMMENT '备注',
|
||||
`create_time` datetime NOT NULL,
|
||||
`creator_id` bigint NOT NULL,
|
||||
`creator` varchar(36) NOT NULL,
|
||||
`update_time` datetime NOT NULL,
|
||||
`modifier_id` bigint NOT NULL,
|
||||
`modifier` varchar(36) NOT NULL,
|
||||
`is_deleted` tinyint DEFAULT '0',
|
||||
`delete_time` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_resource_code` (`resource_code`) USING BTREE,
|
||||
KEY `idx_name` (`name`) USING BTREE
|
||||
) COMMENT='资源授权凭证表';
|
||||
|
||||
ALTER TABLE t_tag_tree ADD `type` tinyint NOT NULL DEFAULT '-1' COMMENT '类型: -1.普通标签; 其他值则为对应的资源类型';
|
||||
ALTER TABLE t_tag_tree ADD `type` tinyint NOT NULL DEFAULT '-1' COMMENT '类型: -1.普通标签; 其他值则为对应的资源类型' AFTER `code_path`;
|
||||
ALTER TABLE t_db_instance ADD `code` varchar(36) NULL COMMENT '唯一编号' AFTER id;
|
||||
ALTER TABLE t_db ADD auth_cert_name varchar(36) NULL COMMENT '授权凭证名' AFTER instance_id;
|
||||
ALTER TABLE t_tag_tree MODIFY COLUMN code_path varchar(555) NOT NULL COMMENT '标识符路径';
|
||||
|
||||
BEGIN;
|
||||
INSERT
|
||||
INTO
|
||||
t_tag_tree (pid,
|
||||
code,
|
||||
code_path,
|
||||
type,
|
||||
name,
|
||||
create_time,
|
||||
creator_id,
|
||||
creator,
|
||||
update_time,
|
||||
modifier_id,
|
||||
modifier,
|
||||
is_deleted)
|
||||
select
|
||||
INSERT INTO t_tag_tree ( pid, CODE, code_path, type, NAME, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted )
|
||||
SELECT
|
||||
tag_id,
|
||||
resource_code,
|
||||
CONCAT(tag_path ,resource_type , '|', resource_code, '/'),
|
||||
resource_type,
|
||||
resource_code,
|
||||
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),
|
||||
DATE_FORMAT( NOW(), '%Y-%m-%d %H:%i:%s' ),
|
||||
1,
|
||||
'admin',
|
||||
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),
|
||||
DATE_FORMAT( NOW(), '%Y-%m-%d %H:%i:%s' ),
|
||||
1,
|
||||
'admin',
|
||||
0
|
||||
from
|
||||
FROM
|
||||
t_tag_resource
|
||||
WHERE
|
||||
is_deleted = 0;
|
||||
|
||||
DROP TABLE t_tag_resource;
|
||||
COMMIT;
|
||||
|
||||
-- 资源授权凭证
|
||||
CREATE TABLE `t_resource_auth_cert` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号名称',
|
||||
`resource_code` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '资源编码',
|
||||
`resource_type` tinyint NOT NULL COMMENT '资源类型',
|
||||
`type` tinyint DEFAULT NULL COMMENT '凭证类型',
|
||||
`username` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名',
|
||||
`ciphertext` varchar(5000) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密文内容',
|
||||
`ciphertext_type` tinyint NOT NULL COMMENT '密文类型(-1.公共授权凭证 1.密码 2.秘钥)',
|
||||
`extra` varchar(200) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号需要的其他额外信息(如秘钥口令等)',
|
||||
`remark` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注',
|
||||
`create_time` datetime NOT NULL,
|
||||
`creator_id` bigint NOT NULL,
|
||||
`creator` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
`update_time` datetime NOT NULL,
|
||||
`modifier_id` bigint NOT NULL,
|
||||
`modifier` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
`is_deleted` tinyint DEFAULT '0',
|
||||
`delete_time` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_resource_code` (`resource_code`) USING BTREE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='资源授权凭证表';
|
||||
|
||||
|
||||
Begin;
|
||||
-- 迁移机器表账号
|
||||
INSERT
|
||||
INTO
|
||||
t_resource_auth_cert (name,
|
||||
resource_code,
|
||||
resource_type,
|
||||
type,
|
||||
username,
|
||||
ciphertext,
|
||||
ciphertext_type,
|
||||
create_time,
|
||||
creator_id,
|
||||
creator,
|
||||
update_time,
|
||||
modifier_id,
|
||||
modifier,
|
||||
is_deleted)
|
||||
select
|
||||
CONCAT('machine_', code, '_' username),
|
||||
code,
|
||||
1,
|
||||
1,
|
||||
username,
|
||||
password,
|
||||
1,
|
||||
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),
|
||||
1,
|
||||
'admin',
|
||||
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),
|
||||
1,
|
||||
'admin',
|
||||
0
|
||||
from
|
||||
-- 迁移machine表账号密码
|
||||
INSERT INTO `t_resource_auth_cert` ( `name`, `resource_code`, `resource_type`, `username`, `ciphertext`, `ciphertext_type`, `type`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted` )
|
||||
SELECT
|
||||
CONCAT( 'machine_', CODE, '_', username ) name,
|
||||
CODE resource_code,
|
||||
1 resource_type,
|
||||
username username,
|
||||
CASE
|
||||
WHEN auth_cert_id = - 1
|
||||
OR auth_cert_id IS NULL THEN
|
||||
`password` ELSE concat( 'auth_cert_', auth_cert_id )
|
||||
END ciphertext,
|
||||
CASE
|
||||
WHEN auth_cert_id = - 1
|
||||
OR auth_cert_id IS NULL THEN
|
||||
1 ELSE - 1
|
||||
END ciphertext_type,
|
||||
1 type,
|
||||
DATE_FORMAT( NOW(), '%Y-%m-%d %H:%i:%s' ) create_time,
|
||||
1 creator_id,
|
||||
'admin' creator,
|
||||
DATE_FORMAT( NOW(), '%Y-%m-%d %H:%i:%s' ) update_time,
|
||||
1 modifier_id,
|
||||
'admin' modifier,
|
||||
0 is_deleted
|
||||
FROM
|
||||
t_machine
|
||||
WHERE
|
||||
is_deleted = 0;
|
||||
|
||||
-- 迁移公共密钥
|
||||
INSERT INTO `t_resource_auth_cert` ( `name`, `remark`, `resource_code`, `resource_type`, `username`, `ciphertext`, `extra`, `ciphertext_type`, `type`, `create_time`, `creator_id`, `creator`, `update_time`, `modifier_id`, `modifier`, `is_deleted` )
|
||||
SELECT
|
||||
concat( 'auth_cert_', id ) `name`,
|
||||
`name` remark,
|
||||
concat( 'auth_cert_code_', id ) resource_code,
|
||||
-2 resource_type,
|
||||
t.username username,
|
||||
`password` ciphertext,
|
||||
case when passphrase is not null and passphrase !='' then concat('{"passphrase":"', passphrase,'"}') else null end extra,
|
||||
auth_method ciphertext_type,
|
||||
2 type,
|
||||
DATE_FORMAT( NOW(), '%Y-%m-%d %H:%i:%s' ) create_time,
|
||||
1 creator_id,
|
||||
'admin' creator,
|
||||
DATE_FORMAT( NOW(), '%Y-%m-%d %H:%i:%s' ) update_time,
|
||||
1 modifier_id,
|
||||
'admin' modifier,
|
||||
0 is_deleted
|
||||
FROM
|
||||
t_auth_cert a
|
||||
join (select `ciphertext`, `username` from `t_resource_auth_cert` GROUP BY `ciphertext`, `username` ) t on t.ciphertext = concat( 'auth_cert_', a.id )
|
||||
;
|
||||
|
||||
-- 关联机器账号到tag_tree
|
||||
INSERT
|
||||
INTO
|
||||
t_tag_tree (pid,
|
||||
code,
|
||||
code_path,
|
||||
type,
|
||||
name,
|
||||
create_time,
|
||||
creator_id,
|
||||
creator,
|
||||
update_time,
|
||||
modifier_id,
|
||||
modifier,
|
||||
is_deleted)
|
||||
INSERT INTO t_tag_tree ( pid, CODE, code_path, type, NAME, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted )
|
||||
SELECT
|
||||
tt.id,
|
||||
rac.`name`,
|
||||
CONCAT(tt.code_path, '11|' ,rac.`name`, '/'),
|
||||
11,
|
||||
rac.`username`,
|
||||
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),
|
||||
DATE_FORMAT( NOW(), '%Y-%m-%d %H:%i:%s' ),
|
||||
1,
|
||||
'admin',
|
||||
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),
|
||||
DATE_FORMAT( NOW(), '%Y-%m-%d %H:%i:%s' ),
|
||||
1,
|
||||
'admin',
|
||||
0
|
||||
@@ -192,65 +176,29 @@ FROM
|
||||
WHERE
|
||||
tt.`is_deleted` = 0;
|
||||
|
||||
-- 删除机器表 账号相关字段
|
||||
ALTER TABLE t_machine DROP COLUMN username;
|
||||
ALTER TABLE t_machine DROP COLUMN password;
|
||||
ALTER TABLE t_machine DROP COLUMN auth_cert_id;
|
||||
|
||||
UPDATE t_sys_resource SET pid=93, ui_path='Tag3fhad/exahgl32/', weight=19999999, meta='{"component":"ops/tag/AuthCertList","icon":"Ticket","isKeepAlive":true,"routeName":"AuthCertList"}' WHERE id=103;
|
||||
UPDATE t_sys_resource SET ui_path='Tag3fhad/exahgl32/egxahg24/', weight=10000000 WHERE id=104;
|
||||
UPDATE t_sys_resource SET ui_path='Tag3fhad/exahgl32/yglxahg2/', weight=20000000 WHERE id=105;
|
||||
UPDATE t_sys_resource SET ui_path='Tag3fhad/exahgl32/Glxag234/', weight=30000000 WHERE id=106;
|
||||
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(1712717290, 0, 'tLb8TKLB/', 1, 1, '无页面权限', 'empty', 1712717290, '{"component":"empty","icon":"Menu","isHide":true,"isKeepAlive":true,"routeName":"empty"}', 1, 'admin', 1, 'admin', '2024-04-10 10:48:10', '2024-04-10 10:48:10', 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(1712717337, 1712717290, 'tLb8TKLB/m2abQkA8/', 2, 1, '授权凭证密文查看', 'authcert:showciphertext', 1712717337, 'null', 1, 'admin', 1, 'admin', '2024-04-10 10:48:58', '2024-04-10 10:48:58', 0, NULL);
|
||||
commit;
|
||||
|
||||
-- 关联数据库账号至授权凭证表
|
||||
begin;
|
||||
ALTER TABLE t_db_instance ADD code varchar(36) NULL COMMENT '唯一编号';
|
||||
ALTER TABLE t_db_instance CHANGE code code varchar(36) NULL COMMENT '唯一编号' AFTER id;
|
||||
|
||||
UPDATE t_db_instance SET code = CONCAT('db_code_', id);
|
||||
|
||||
INSERT
|
||||
INTO
|
||||
t_resource_auth_cert (name,
|
||||
resource_code,
|
||||
resource_type,
|
||||
type,
|
||||
username,
|
||||
ciphertext,
|
||||
ciphertext_type,
|
||||
create_time,
|
||||
creator_id,
|
||||
creator,
|
||||
update_time,
|
||||
modifier_id,
|
||||
modifier,
|
||||
is_deleted)
|
||||
select
|
||||
CONCAT(code, '_', username),
|
||||
code,
|
||||
-- 迁移数据库账号至授权凭证表
|
||||
UPDATE t_db_instance SET `code` = CONCAT('db_code_', id);
|
||||
INSERT INTO t_resource_auth_cert ( NAME, resource_code, resource_type, type, username, ciphertext, ciphertext_type, create_time, creator_id, creator, update_time, modifier_id, modifier, is_deleted )
|
||||
SELECT
|
||||
CONCAT( CODE, '_', username ),
|
||||
CODE,
|
||||
2,
|
||||
1,
|
||||
username,
|
||||
password,
|
||||
PASSWORD,
|
||||
1,
|
||||
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),
|
||||
DATE_FORMAT( NOW(), '%Y-%m-%d %H:%i:%s' ),
|
||||
1,
|
||||
'admin',
|
||||
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),
|
||||
DATE_FORMAT( NOW(), '%Y-%m-%d %H:%i:%s' ),
|
||||
1,
|
||||
'admin',
|
||||
0
|
||||
from
|
||||
FROM
|
||||
t_db_instance
|
||||
WHERE
|
||||
is_deleted = 0;
|
||||
|
||||
ALTER TABLE t_db ADD auth_cert_name varchar(36) NULL COMMENT '授权凭证名';
|
||||
ALTER TABLE t_db CHANGE auth_cert_name auth_cert_name varchar(36) NULL COMMENT '授权凭证名' AFTER instance_id;
|
||||
|
||||
UPDATE
|
||||
t_db d
|
||||
SET
|
||||
@@ -265,5 +213,11 @@ SET
|
||||
WHERE
|
||||
di.id = d.instance_id);
|
||||
|
||||
ALTER TABLE t_tag_tree MODIFY COLUMN code_path varchar(555) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '标识符路径';
|
||||
UPDATE `t_sys_resource` SET pid=93, ui_path='Tag3fhad/exahgl32/', weight=19999999, meta='{"component":"ops/tag/AuthCertList","icon":"Ticket","isKeepAlive":true,"routeName":"AuthCertList"}' WHERE id=103;
|
||||
UPDATE `t_sys_resource` SET ui_path='Tag3fhad/exahgl32/egxahg24/', weight=10000000 WHERE id=104;
|
||||
UPDATE `t_sys_resource` SET ui_path='Tag3fhad/exahgl32/yglxahg2/', weight=20000000 WHERE id=105;
|
||||
UPDATE `t_sys_resource` SET ui_path='Tag3fhad/exahgl32/Glxag234/', weight=30000000 WHERE id=106;
|
||||
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(1712717290, 0, 'tLb8TKLB/', 1, 1, '无页面权限', 'empty', 1712717290, '{"component":"empty","icon":"Menu","isHide":true,"isKeepAlive":true,"routeName":"empty"}', 1, 'admin', 1, 'admin', '2024-04-10 10:48:10', '2024-04-10 10:48:10', 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(1712717337, 1712717290, 'tLb8TKLB/m2abQkA8/', 2, 1, '授权凭证密文查看', 'authcert:showciphertext', 1712717337, 'null', 1, 'admin', 1, 'admin', '2024-04-10 10:48:58', '2024-04-10 10:48:58', 0, NULL);
|
||||
commit;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user