11 Commits

Author SHA1 Message Date
meilin.huang
e4d949a64b fix: machine file bug 2024-12-26 12:17:58 +08:00
zongyangleo
3f6fb5afef !129 fix: db 相关bug
* fix: db 相关bug
2024-12-26 04:11:28 +00:00
meilin.huang
68f553f4b0 refactor: remove router、ioc is adjusted to inject by type 2024-12-16 23:29:18 +08:00
meilin.huang
7f2a49ba3c fix: 机器文件内容写入导致内容清空、feat: ioc支持根据类型注入 2024-12-13 12:15:24 +08:00
meilin.huang
e56788af3e refactor: dbm 2024-12-08 13:04:23 +08:00
meilin.huang
ebc89e056f fix: fixed some minor issues 2024-11-28 20:11:16 +08:00
zongyangleo
d07cd74a8c !127 fix: 达梦特殊字段类型覆盖解析
* fix: 达梦特殊字段类型覆盖解析
2024-11-28 10:44:49 +00:00
meilin.huang
6cc15ebeda feat: tag refactor & other 2024-11-26 17:32:44 +08:00
zongyangleo
2b712cd548 !126 feat: 解析达梦特殊字段
* feat: 解析达梦特殊字段
2024-11-26 04:04:09 +00:00
meilin.huang
cda2963e1c fix: i18n & other optimizations 2024-11-23 17:23:18 +08:00
meilin.huang
bffa9c2676 fix: i18n 2024-11-21 20:12:16 +08:00
341 changed files with 6783 additions and 6202 deletions

View File

@@ -1,5 +1,5 @@
# 构建前端资源
FROM m.daocloud.io/docker.io/node:18-bookworm-slim as fe-builder
FROM m.daocloud.io/docker.io/node:18-bookworm-slim AS fe-builder
WORKDIR /mayfly
@@ -10,7 +10,7 @@ RUN yarn config set registry 'https://registry.npmmirror.com' && \
yarn build
# 构建后端资源
FROM m.daocloud.io/docker.io/golang:1.23 as be-builder
FROM m.daocloud.io/docker.io/golang:1.23 AS be-builder
ENV GOPROXY https://goproxy.cn
WORKDIR /mayfly

View File

@@ -1,4 +1,4 @@
# 🌈mayfly-go
# 🌈Dromara mayfly-go
<p align="center">
<a href="./README_EN.md">English</a> |

View File

@@ -1,4 +1,4 @@
# 🌈mayfly-go
# 🌈Dromara mayfly-go
<p align="center">
<a href="./README.md">中文介绍</a> |

View File

@@ -56,6 +56,7 @@ function build() {
if [ "${os}" == "windows" ];then
execFileName="${execFileName}.exe"
fi
go mod tidy
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -ldflags=-w -o ${execFileName} main.go
if [ -d ${toFolder} ] ; then
@@ -77,7 +78,7 @@ function build() {
echo_green "Copy resources such as scripts [config.yml.example、mayfly-go.sql、mayfly-go.sqlite、readme.txt、startup.sh、shutdown.sh]"
cp ${server_folder}/config.yml.example ${toFolder}
cp ${server_folder}/readme.txt ${toFolder}
cp ${server_folder}/readme_cn.txt ${toFolder}
cp ${server_folder}/readme_en.txt ${toFolder}
cp ${server_folder}/resources/script/startup.sh ${toFolder}
cp ${server_folder}/resources/script/shutdown.sh ${toFolder}
cp ${server_folder}/resources/script/sql/mayfly-go.sql ${toFolder}

View File

@@ -11,7 +11,7 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^11.2.0",
"@vueuse/core": "^12.0.0",
"asciinema-player": "^3.8.1",
"axios": "^1.6.2",
"clipboard": "^2.0.11",
@@ -19,26 +19,26 @@
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"element-plus": "^2.8.8",
"element-plus": "^2.9.1",
"js-base64": "^3.7.7",
"jsencrypt": "^3.3.2",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"monaco-editor": "^0.52.0",
"monaco-editor": "^0.52.2",
"monaco-sql-languages": "^0.12.2",
"monaco-themes": "^0.4.4",
"nprogress": "^0.2.0",
"pinia": "^2.2.6",
"qrcode.vue": "^3.5.1",
"pinia": "^2.3.0",
"qrcode.vue": "^3.6.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.3",
"sortablejs": "^1.15.6",
"splitpanes": "^3.1.5",
"sql-formatter": "^15.4.5",
"trzsz": "^1.1.5",
"uuid": "^9.0.1",
"vue": "^3.5.13",
"vue-i18n": "^10.0.4",
"vue-router": "^4.4.5",
"vue-i18n": "^10.0.5",
"vue-router": "^4.5.0",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
"xterm-addon-search": "^0.13.0",
@@ -52,16 +52,16 @@
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.2.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/compiler-sfc": "^3.5.13",
"code-inspector-plugin": "^0.4.5",
"dotenv": "^16.3.1",
"eslint": "^8.35.0",
"eslint-plugin-vue": "^9.28.0",
"eslint-plugin-vue": "^9.31.0",
"prettier": "^3.2.5",
"sass": "^1.81.0",
"typescript": "^5.6.3",
"vite": "^5.4.11",
"sass": "^1.82.0",
"typescript": "^5.7.2",
"vite": "^6.0.3",
"vue-eslint-parser": "^9.4.3"
},
"browserslist": [

View File

@@ -30,7 +30,12 @@ export function isTrue(condition: boolean, msg: string) {
* @param msg 错误消息
*/
export function notBlank(obj: any, msg: string) {
isTrue(obj, msg);
if (obj == null || obj == undefined || obj == '') {
throw new AssertError(msg);
}
if (Array.isArray(obj) && obj.length == 0) {
throw new AssertError(msg);
}
}
/**

View File

@@ -19,15 +19,22 @@ export const ResourceTypeEnum = {
// 标签关联的资源类型
export const TagResourceTypeEnum = {
AuthCert: EnumValue.of(-2, '公共凭证').setExtra({ icon: 'Ticket' }),
PublicAuthCert: EnumValue.of(-2, '公共凭证').setExtra({ icon: 'Ticket' }),
Tag: EnumValue.of(-1, '标签').setExtra({ icon: 'CollectionTag' }),
Machine: ResourceTypeEnum.Machine,
Db: ResourceTypeEnum.Db,
DbInstance: ResourceTypeEnum.Db,
Redis: ResourceTypeEnum.Redis,
Mongo: ResourceTypeEnum.Mongo,
AuthCert: EnumValue.of(5, '授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
MachineAuthCert: EnumValue.of(11, '机器-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
DbAuthCert: EnumValue.of(21, '数据库-授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
DbName: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
};
// 标签关联的资源类型路径
export const TagResourceTypePath = {
MachineAuthCert: `${TagResourceTypeEnum.Machine.value}/${TagResourceTypeEnum.AuthCert.value}`,
DbInstanceAuthCert: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}`,
Db: `${TagResourceTypeEnum.DbInstance.value}/${TagResourceTypeEnum.AuthCert.value}/${TagResourceTypeEnum.Db.value}`,
};

View File

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

View File

@@ -7,7 +7,7 @@ export default {
getPublicKey: () => request.get('/common/public-key'),
getConfigValue: (params: any) => request.get('/sys/configs/value', params),
getServerConf: () => request.get('/sys/configs/server'),
oauth2LoginConfig: () => request.get('/auth/oauth2-config'),
oauth2LoginConfig: () => request.get('/auth/oauth2/config'),
changePwd: (param: any) => request.post('/sys/accounts/change-pwd', param),
captcha: () => request.get('/sys/captcha'),
logout: () => request.post('/auth/accounts/logout'),

View File

@@ -0,0 +1,47 @@
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
import syssocket from './syssocket';
import { h, reactive } from 'vue';
import { ElNotification } from 'element-plus';
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
export function initSysMsgs() {
registerDbSqlExecProgress();
}
const sqlExecNotifyMap: Map<string, any> = new Map();
function registerDbSqlExecProgress() {
syssocket.registerMsgHandler('execSqlFileProgress', function (message: any) {
const content = JSON.parse(message.msg);
const id = content.id;
let progress = sqlExecNotifyMap.get(id);
if (content.terminated) {
if (progress != undefined) {
progress.notification?.close();
sqlExecNotifyMap.delete(id);
progress = undefined;
}
return;
}
if (progress == undefined) {
progress = {
props: reactive(buildProgressProps()),
notification: undefined,
};
}
progress.props.progress.title = content.title;
progress.props.progress.executedStatements = content.executedStatements;
if (!sqlExecNotifyMap.has(id)) {
progress.notification = ElNotification({
duration: 0,
title: message.title,
message: h(ProgressNotify, progress.props),
type: syssocket.getMsgType(message.type),
showClose: false,
});
sqlExecNotifyMap.set(id, progress);
}
});
}

View File

@@ -1,9 +1,9 @@
import Config from './config';
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';
import { ElNotification } from 'element-plus';
class SysSocket {
/**
@@ -23,7 +23,6 @@ class SysSocket {
0: 'error',
1: 'success',
2: 'info',
22: 'info',
};
/**
@@ -57,21 +56,16 @@ class SysSocket {
return;
}
// 默认通知处理
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
}
let msg = message.msg;
let duration = 0;
ElNotification({
duration: duration,
title: message.title,
message: msg,
type: type,
});
console.log(message)
})
.open((event: any) => console.log(event))
.close(() => {

View File

@@ -6,7 +6,7 @@
<el-form-item>
<template #label>
<el-space :size="4">
<span>{{ `${item?.label}` }}</span>
<span>{{ `${$t(item?.label)}` }}</span>
<el-tooltip v-if="item.tooltip" :content="item?.tooltip" placement="top">
<SvgIcon name="QuestionFilled" />
</el-tooltip>
@@ -21,8 +21,8 @@
</GridItem>
<GridItem suffix>
<div class="operation">
<el-button type="primary" :icon="Search" @click="search" plain> 搜索 </el-button>
<el-button :icon="Delete" @click="reset"> 重置 </el-button>
<el-button type="primary" :icon="Search" @click="search" plain> {{ $t('common.search') }} </el-button>
<el-button :icon="Delete" @click="reset"> {{ $t('common.reset') }} </el-button>
<el-button v-if="showCollapse" type="primary" link class="search-isOpen" @click="collapsed = !collapsed">
{{ collapsed ? '展开' : '合并' }}
<el-icon class="el-icon--right">

View File

@@ -196,6 +196,9 @@ watch(
(newValue: any) => {
if (!monacoEditorIns.hasTextFocus()) {
state.languageMode = props.language;
if (newValue == null) {
newValue = '';
}
monacoEditorIns?.setValue(newValue);
}
}

View File

@@ -1,5 +1,5 @@
<template>
<el-descriptions border size="small" :title="`${progress.title}`">
<el-descriptions border size="small" :title="`${props.progress.title}`">
<el-descriptions-item label="时间">{{ state.elapsedTime }}</el-descriptions-item>
<el-descriptions-item label="已处理">{{ progress.executedStatements }}</el-descriptions-item>
</el-descriptions>

View File

@@ -31,10 +31,10 @@
<!-- 按钮 -->
<div class="search-buttons">
<el-button class="terminal-search-button search-button-prev" type="primary" size="small" @click="searchKeywords(false)">
{{ $t('components.terminal.previous') }}}}
{{ $t('components.terminal.previous') }}
</el-button>
<el-button class="terminal-search-button search-button-next" type="primary" size="small" @click="searchKeywords(true)">
{{ $t('components.terminal.next') }}}}
{{ $t('components.terminal.next') }}
</el-button>
<el-button class="terminal-search-button search-button-next" type="primary" size="small" @click="closeSearch">
{{ $t('components.terminal.close') }}

View File

@@ -27,7 +27,7 @@ export function useI18nPleaseSelect(labelI18nKey: string) {
* @returns
*/
export async function useI18nDeleteConfirm(name: string = '') {
return useI18nConfirm('common.deleteConfirm', { name });
return useI18nConfirm('common.deleteConfirm2', { name });
}
/**

View File

@@ -45,13 +45,15 @@ export default {
previousStep: 'Previous Step',
nextStep: 'Next Step',
copy: 'Copy',
search: 'Search',
pleaseInput: 'Please enter {label}',
pleaseSelect: 'Please select {label}',
formValidationError: 'Please fill in the form information correctly',
formValidationError: 'Please check the form',
createTitle: 'Create {name}',
editTitle: 'Edit {name}',
detailTitle: '{name} Details',
deleteConfirm: 'This operation will delete [{name}]. Do you want to continue?',
deleteConfirm: 'Sure to delete?',
deleteConfirm2: 'This operation will delete [{name}]. Do you want to continue?',
saveSuccess: 'save successfully',
deleteSuccess: 'delete successfully',
operateSuccess: 'operate successfully',

View File

@@ -73,6 +73,21 @@ export default {
newTabRunSql: 'NewTab Run SQL',
formatSql: 'Format SQL',
alias: 'Alias',
tableName: 'Name',
addColumn: 'Add Column',
addDefaultColumn: 'Add Default Column',
addIndex: 'Add Index',
length: 'Length',
numScale: 'Scale',
defaultValue: 'Default Value',
notNull: 'Not Null',
primaryKey: 'Pri',
autoIncrement: 'Auto Incr',
unique: 'Unique',
uniqueIndex: 'Unique Index',
normalIndex: 'Normal Index',
execTime: 'execution time',
oneClickCopy: 'One click copy',
asc: 'Asc',
@@ -199,7 +214,7 @@ export default {
getDbNamesModeAssign: 'Specifying the db name',
ignore: 'Ignore',
replate: 'Replate',
replace: 'Replate',
running: 'Running',
waitRun: 'Wait Run',

View File

@@ -107,6 +107,9 @@ export default {
cronjobRunState: 'Run State',
execResRecordType: 'Result record type',
cronExpression: 'Cron Expression',
noRecord: 'Not recorded',
onErrorRecord: 'Record on error',
record: 'Record',
// file
upload: 'Upload',

View File

@@ -19,7 +19,6 @@ export default {
newTabOpen: 'New tab opens',
redisSelectErr: 'Select redis first',
flushDbTips: 'Make sure to clear all keys of the [{db}] library?',
keyNotEmpty: 'The Key cannot be empty',
// info
redisInfoTitle: 'Redis server information',

View File

@@ -45,13 +45,15 @@ export default {
previousStep: '上一步',
nextStep: '下一步',
copy: '复制',
search: '搜索',
pleaseInput: '请输入{label}',
pleaseSelect: '请选择{label}',
formValidationError: '请正确填写表单信息',
formValidationError: '信息填写有误,请检查',
createTitle: '创建{name}',
editTitle: '编辑{name}',
detailTitle: '{name}详情',
deleteConfirm: '此操作将删除【{name}】, 是否继续',
deleteConfirm: '确定删除',
deleteConfirm2: '此操作将删除【{name}】, 是否继续?',
saveSuccess: '保存成功',
deleteSuccess: '删除成功',
operateSuccess: '操作成功',

View File

@@ -72,6 +72,21 @@ export default {
newTabRunSql: '新标签执行SQL',
formatSql: '格式化SQL',
alias: '别名',
tableName: '表名',
addColumn: '添加列',
addDefaultColumn: '添加默认列',
addIndex: '添加索引',
length: '长度',
numScale: '精度',
defaultValue: '默认值',
notNull: '非空',
primaryKey: '主键',
autoIncrement: '自增',
unique: '唯一',
uniqueIndex: '唯一索引',
normalIndex: '普通索引',
execTime: '执行时间',
oneClickCopy: '一键复制',
asc: '升序',
@@ -195,7 +210,7 @@ export default {
getDbNamesModeAssign: '指定库名',
ignore: '忽略',
replate: '替换',
replace: '替换',
running: '运行中',
waitRun: '待运行',

View File

@@ -108,6 +108,9 @@ export default {
cronjobRunState: '运行状态',
execResRecordType: '结果记录类型',
cronExpression: 'cron表达式',
noRecord: '不记录',
onErrorRecord: '错误时记录',
record: '记录',
// file
upload: '上传',

View File

@@ -17,7 +17,6 @@ export default {
newTabOpen: '新tab打开',
redisSelectErr: '请先选择redis',
flushDbTips: '确定清空[{db}]库的所有key?',
keyNotEmpty: 'Key不能为空',
// info
redisInfoTitle: 'Redis服务器信息',

View File

@@ -18,11 +18,13 @@ import '@/theme/index.scss';
import '@/assets/font/font.css';
import '@/assets/iconfont/iconfont.js';
import { getThemeConfig } from './common/utils/storage';
import { initSysMsgs } from './common/sysmsgs';
const app = createApp(App);
registElSvgIcon(app);
directive(app);
initSysMsgs();
app.use(pinia).use(router).use(i18n).use(ElementPlus, { size: getThemeConfig()?.globalComponentSize }).mount('#app');

View File

@@ -12,30 +12,25 @@
/* Form 表单
------------------------------- */
.el-form {
// .el-form {
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
.el-form-item:last-of-type {
margin-bottom: 0 !important;
}
// // 修复行内表单最后一个 el-form-item 位置下移问题
// &.el-form--inline {
// .el-form-item--large.el-form-item:last-of-type {
// margin-bottom: 22px !important;
// }
// 修复行内表单最后一个 el-form-item 位置下移问题
&.el-form--inline {
.el-form-item--large.el-form-item:last-of-type {
margin-bottom: 22px !important;
}
// .el-form-item--default.el-form-item:last-of-type,
// .el-form-item--small.el-form-item:last-of-type {
// margin-bottom: 18px !important;
// }
// }
.el-form-item--default.el-form-item:last-of-type,
.el-form-item--small.el-form-item:last-of-type {
margin-bottom: 18px !important;
}
}
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
.el-form-item .el-form-item__label .el-icon {
margin-right: 0px;
}
}
// .el-form-item .el-form-item__label .el-icon {
// margin-right: 0px;
// }
// }
/* Alert 警告
------------------------------- */

View File

@@ -37,7 +37,7 @@
</el-form-item>
<el-form-item ref="tagSelectRef" prop="codePaths" :label="$t('tag.relateTag')">
<tag-tree-check height="300px" v-model="form.codePaths" :tag-type="[TagResourceTypeEnum.DbName.value, TagResourceTypeEnum.Redis.value]" />
<tag-tree-check height="300px" v-model="form.codePaths" :tag-type="[TagResourceTypePath.Db, TagResourceTypeEnum.Redis.value]" />
</el-form-item>
<el-divider content-position="left">{{ $t('flow.approvalNode') }}</el-divider>
@@ -94,7 +94,7 @@ import Sortable from 'sortablejs';
import { randomUuid } from '../../common/utils/string';
import { ProcdefStatus } from './enums';
import TagTreeCheck from '../ops/component/TagTreeCheck.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { useI18nFormValidate, useI18nPleaseInput, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';

View File

@@ -95,7 +95,7 @@ const parseBizForm = async (bizFormStr: string) => {
const dbRes = await dbApi.dbs.request({ id: bizForm.dbId });
state.db = dbRes.list?.[0];
tagApi.listByQuery.request({ type: TagResourceTypeEnum.DbName.value, codes: state.db.code }).then((res) => {
tagApi.listByQuery.request({ type: TagResourceTypeEnum.Db.value, codes: state.db.code }).then((res) => {
state.db.codePaths = res.map((item: any) => item.codePath);
});
};

View File

@@ -68,7 +68,7 @@ watch(
);
const changeResourceCode = async (db: any) => {
emit('changeResourceCode', TagResourceTypeEnum.DbName.value, db.code);
emit('changeResourceCode', TagResourceTypeEnum.Db.value, db.code);
};
const validateBizForm = async () => {

View File

@@ -122,7 +122,12 @@
<template #header>
<el-row justify="center">
<div class="resource-num pointer-icon" @click="toPage('db')">
<SvgIcon class="mb5 mr5" :size="28" :name="TagResourceTypeEnum.Db.extra.icon" :color="TagResourceTypeEnum.Db.extra.iconColor" />
<SvgIcon
class="mb5 mr5"
:size="28"
:name="TagResourceTypeEnum.DbInstance.extra.icon"
:color="TagResourceTypeEnum.DbInstance.extra.iconColor"
/>
<span class="">{{ state.db.num }}</span>
</div>
</el-row>
@@ -388,7 +393,7 @@ const handleAvatarSuccess = (response: any, uploadFile: any) => {
// 初始化数字滚动
const initData = async () => {
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.MachineAuthCert.value, pageSize: state.defaultLogSize })
.request({ resourceType: TagResourceTypeEnum.Machine.value, pageSize: state.defaultLogSize })
.then(async (res: any) => {
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
state.machine.tagInfos = tagInfos;
@@ -396,7 +401,7 @@ const initData = async () => {
});
resourceOpLogApi.getAccountResourceOpLogs
.request({ resourceType: TagResourceTypeEnum.DbName.value, pageSize: state.defaultLogSize })
.request({ resourceType: TagResourceTypeEnum.DbInstance.value, pageSize: state.defaultLogSize })
.then(async (res: any) => {
const tagInfos = await getAllTagInfoByCodePaths(res.list?.map((item: any) => item.codePath));
state.db.tagInfos = tagInfos;

View File

@@ -38,7 +38,11 @@
:value="TagResourceTypeEnum.Machine.value"
/>
<el-option :key="TagResourceTypeEnum.Db.value" :label="TagResourceTypeEnum.Db.label" :value="TagResourceTypeEnum.Db.value" />
<el-option
:key="TagResourceTypeEnum.DbInstance.value"
:label="TagResourceTypeEnum.DbInstance.label"
:value="TagResourceTypeEnum.DbInstance.value"
/>
<el-option
:key="TagResourceTypeEnum.Redis.value"
:label="$t(TagResourceTypeEnum.Redis.label)"
@@ -154,7 +158,7 @@ const DefaultForm = {
username: '',
ciphertextType: AuthCertCiphertextTypeEnum.Password.value,
type: AuthCertTypeEnum.Private.value,
resourceType: TagResourceTypeEnum.AuthCert.value,
resourceType: TagResourceTypeEnum.PublicAuthCert.value,
resourceCode: '',
ciphertext: '',
extra: {} as any,

View File

@@ -57,7 +57,7 @@ import { isPrefixSubsequence } from '@/common/utils/string';
const props = defineProps({
resourceType: {
type: [Number],
type: [Number, String],
required: true,
},
defaultExpandedKeys: {

View File

@@ -58,7 +58,7 @@ const props = defineProps({
default: 'calc(100vh - 330px)',
},
tagType: {
type: [Number, Array<Number>],
type: [Number, Array<Number>, String, Array<String>],
default: TagResourceTypeEnum.Tag.value,
},
nodeKey: {

View File

@@ -43,7 +43,7 @@ import { tagApi } from '../tag/api';
const props = defineProps({
resourceType: {
type: [Number],
type: [Number, String],
required: true,
},
tagPathNodeType: {

View File

@@ -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 {
/**
@@ -160,9 +160,9 @@ export class NodeType {
* @param resourceType 资源类型
* @returns
*/
export function getTagPathSearchItem(resourceType: number) {
export function getTagPathSearchItem(resourceType: any) {
return SearchItem.select('tagPath', 'common.tag').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,7 +180,7 @@ export function getTagPathSearchItem(resourceType: number) {
*/
export function getTagTypeCodeByPath(codePath: string) {
const result: any = {};
if (!codePath) return result
if (!codePath) return result;
const parts = codePath.split('/'); // 切分字符串并保留数字和对应的值部分
for (let part of parts) {
@@ -208,7 +208,7 @@ export function getTagTypeCodeByPath(codePath: string) {
* @returns
*/
export async function getAllTagInfoByCodePaths(codePaths: string[]) {
if (!codePaths) return
if (!codePaths) return;
const allTypeAndCode: any = {};
for (let codePath of codePaths) {
@@ -222,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;
}

View File

@@ -198,7 +198,7 @@ const getAuthCerts = async () => {
const inst: any = props.instance;
const res = await resourceAuthCertApi.listByQuery.request({
resourceCode: inst.code,
resourceType: TagResourceTypeEnum.Db.value,
resourceType: TagResourceTypeEnum.DbInstance.value,
pageSize: 100,
});
state.authCerts = res.list || [];

View File

@@ -66,7 +66,7 @@ const searchItems = [
const columns = ref([
TableColumn.new('db', 'db.db'),
TableColumn.new('table', 'db.type'),
TableColumn.new('table', 'db.table'),
TableColumn.new('type', 'db.stmtType').typeTag(DbSqlExecTypeEnum).setAddWidth(10),
TableColumn.new('creator', 'db.execUser'),
TableColumn.new('sql', 'SQL').canBeautify(),

View File

@@ -9,13 +9,13 @@
v-model:selection-data="state.selectionData"
:columns="columns"
@page-num-change="
(args) => {
(args: any) => {
state.query.pageNum = args.pageNum;
search();
}
"
@page-size-change="
(args) => {
(args: any) => {
state.query.pageSize = args.pageNum;
search();
}
@@ -85,14 +85,14 @@ 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 { ElMessage } from 'element-plus';
import { hasPerms } from '@/components/auth/auth';
import TerminalLog from '@/components/terminal/TerminalLog.vue';
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
import { getClientId } from '@/common/utils/storage';
import FileInfo from '@/components/file/FileInfo.vue';
import { DbTransferFileStatusEnum } from './enums';
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nPleaseSelect, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg, useI18nPleaseSelect } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
@@ -179,14 +179,13 @@ const state = reactive({
},
btnOk: async function () {
await useI18nFormValidate(runFormRef);
console.log(state.runDialog.runForm);
if (state.runDialog.runForm.targetDbType !== state.runDialog.runForm.dbType) {
ElMessage.warning(t('db.targetDbTypeSelectError', { dbType: state.runDialog.runForm.dbType }));
return false;
}
state.runDialog.runForm.clientId = getClientId();
await dbApi.dbTransferFileRun.request(state.runDialog.runForm);
useI18nSaveSuccessMsg();
useI18nOperateSuccessMsg();
state.runDialog.cancel();
await search();
},
@@ -228,9 +227,7 @@ const openLog = function (data: any) {
// 运行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;
};

View File

@@ -89,7 +89,7 @@
<ResourceAuthCertTableEdit
v-model="form.authCerts"
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Db.value"
:resource-type="TagResourceTypeEnum.DbInstance.value"
:test-conn-btn-loading="testConnBtnLoading"
@test-conn="testConn"
:disable-ciphertext-type="[AuthCertCiphertextTypeEnum.PrivateKey.value]"
@@ -128,6 +128,7 @@ import { AuthCertCiphertextTypeEnum } from '../tag/enums';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import { useI18nFormValidate, useI18nPleaseInput, useI18nPleaseSelect, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
import { notBlankI18n } from '@/common/assert';
const { t } = useI18n();
@@ -258,6 +259,7 @@ const testConn = async (authCert: any) => {
const btnOk = async () => {
await useI18nFormValidate(dbForm);
state.submitForm = await getReqForm();
notBlankI18n(state.submitForm.authCerts, 'db.acName');
await saveInstanceExec();
useI18nSaveSuccessMsg();
state.form.id = saveInstanceRes as any;

View File

@@ -13,9 +13,9 @@
>
<template #tableHeader>
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)">{{ $t('common.create') }}</el-button>
<el-button v-auth="perms.delInstance" :disabled="selectionData.length < 1" @click="deleteInstance()" type="danger" icon="delete">{{
$t('common.delete')
}}</el-button>
<el-button v-auth="perms.delInstance" :disabled="selectionData.length < 1" @click="deleteInstance()" type="danger" icon="delete">
{{ $t('common.delete') }}
</el-button>
</template>
<template #tagPath="{ data }">
@@ -76,7 +76,6 @@
<script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { dbApi } from './api';
import { formatDate } from '@/common/utils/format';
import PageTable from '@/components/pagetable/PageTable.vue';
@@ -88,7 +87,7 @@ import { SearchItem } from '@/components/SearchForm';
import ResourceAuthCert from '../component/ResourceAuthCert.vue';
import ResourceTags from '../component/ResourceTags.vue';
import { getTagPathSearchItem } from '../component/tag';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { TagResourceTypePath } from '@/common/commonEnum';
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
@@ -110,10 +109,7 @@ const perms = {
saveDb: 'db:save',
};
const searchItems = [
SearchItem.input('keyword', 'common.keyword').withPlaceholder('db.keywordPlaceholder'),
getTagPathSearchItem(TagResourceTypeEnum.Db.value),
];
const searchItems = [SearchItem.input('keyword', 'common.keyword').withPlaceholder('db.keywordPlaceholder'), getTagPathSearchItem(TagResourceTypePath.Db)];
const columns = ref([
TableColumn.new('tags[0].tagPath', 'tag.relateTag').isSlot('tagPath').setAddWidth(20),
@@ -209,7 +205,7 @@ const editInstance = async (data: any) => {
const deleteInstance = async () => {
try {
useI18nDeleteConfirm(state.selectionData.map((x: any) => x.name).join('、'));
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.name).join('、'));
await dbApi.deleteInstance.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
useI18nDeleteSuccessMsg();
search();

View File

@@ -4,7 +4,7 @@
<Pane size="20" max-size="30">
<tag-tree
:default-expanded-keys="state.defaultExpendKey"
:resource-type="TagResourceTypeEnum.DbName.value"
:resource-type="TagResourceTypePath.Db"
:tag-path-node-type="NodeTypeTagPath"
ref="tagTreeRef"
>
@@ -246,7 +246,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { getDbDialect, schemaDbTypes } from './dialect/index';
import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
import { Pane, Splitpanes } from 'splitpanes';
import { useEventListener, useStorage } from '@vueuse/core';
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
@@ -597,7 +597,7 @@ const autoOpenDb = (codePath: string) => {
const typeAndCodes: any = getTagTypeCodeByPath(codePath);
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
const dbCode = typeAndCodes[TagResourceTypeEnum.DbName.value][0];
const dbCode = typeAndCodes[TagResourceTypeEnum.Db.value][0];
state.defaultExpendKey = [tagPath, dbCode];
setTimeout(() => {

View File

@@ -136,11 +136,11 @@
<el-tab-pane :label="$t('db.fieldMap')" :name="fieldTab" :disabled="!baseFieldCompleted">
<el-form-item prop="fieldMap" :label="$t('db.fieldMap')" required>
<el-table :data="form.fieldMap" :max-height="400" size="small">
<el-table :data="form.fieldMap" :max-height="650" size="small">
<el-table-column prop="src" :label="$t('db.srcField')" :width="200" />
<el-table-column prop="target" :label="$t('db.targetField')">
<template #default="scope">
<el-select v-model="scope.row.target">
<el-select v-model="scope.row.target" allow-create filterable>
<el-option
v-for="item in state.targetColumnList"
:key="item.columnName"
@@ -155,15 +155,15 @@
</el-tab-pane>
<el-tab-pane :label="$t('db.sqlPreview')" :name="sqlPreviewTab" :disabled="!baseFieldCompleted">
<el-form-item prop="fieldMap" :label="$t('db.selectSql')">
<el-input type="textarea" v-model="state.previewDataSql" readonly :input-style="{ height: '170px' }" />
</el-form-item>
<el-form-item prop="fieldMap" :label="$t('db.insertSql')">
<el-input type="textarea" v-model="state.previewInsertSql" readonly :input-style="{ height: '170px' }" />
</el-form-item>
<el-form-item prop="isReplace" v-if="compatibleDuplicateStrategy(form.targetDbType!)" :label="$t('db.keyDuplicateStrategy')">
<EnumSelect :enums="DbDataSyncDuplicateStrategyEnum" v-model="form.duplicateStrategy" @change="handleDuplicateStrategy" />
</el-form-item>
<el-form-item prop="fieldMap" :label="$t('db.selectSql')">
<el-input type="textarea" v-model="state.previewDataSql" readonly :input-style="{ height: '300px' }" />
</el-form-item>
<el-form-item prop="fieldMap" :label="$t('db.insertSql')">
<el-input type="textarea" v-model="state.previewInsertSql" readonly :input-style="{ height: '300px' }" />
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
@@ -502,11 +502,13 @@ const handleGetSrcFields = async () => {
sql,
});
if (!res.columns) {
if (res.length && !res[0].columns) {
ElMessage.warning(t('db.notColumnSql'));
return;
}
let data = res[0];
let filedMap: any = {};
if (state.form.fieldMap && state.form.fieldMap.length > 0) {
state.form.fieldMap.forEach((a: any) => {
@@ -514,11 +516,11 @@ const handleGetSrcFields = async () => {
});
}
state.srcTableFields = res.columns.map((a: any) => a.name);
state.srcTableFields = data.columns.map((a: any) => a.name);
state.form.fieldMap = res.columns.map((a: any) => ({ src: a.name, target: filedMap[a.name] || '' }));
state.form.fieldMap = data.columns.map((a: any) => ({ src: a.name, target: filedMap[a.name] || '' }));
state.previewRes = res;
state.previewRes = data;
};
const handleGetTargetFields = async () => {

View File

@@ -3,7 +3,7 @@
v-bind="$attrs"
v-model="selectNode"
@change="changeNode"
:resource-type="TagResourceTypeEnum.Db.value"
:resource-type="TagResourceTypePath.Db"
:tag-path-node-type="NodeTypeTagPath"
>
<template #iconPrefix>
@@ -17,7 +17,7 @@
</template>
<script setup lang="ts">
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
import { dbApi } from '@/views/ops/db/api';
import { sleep } from '@/common/utils/loading';

View File

@@ -104,7 +104,7 @@
</el-row>
<db-table-data
v-if="!dt.errorMsg"
:ref="(el) => (dt.dbTableRef = el)"
:ref="(el: any) => (dt.dbTableRef = el)"
:db-id="dbId"
:db="dbName"
:data="dt.data"
@@ -128,12 +128,12 @@
</template>
<script lang="ts" setup>
import { h, nextTick, onMounted, reactive, ref, toRefs, unref } from 'vue';
import { nextTick, onMounted, reactive, ref, toRefs, unref } from 'vue';
import { getToken } from '@/common/utils/storage';
import { notBlank } from '@/common/assert';
import { format as sqlFormatter } from 'sql-formatter';
import config from '@/common/config';
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { editor } from 'monaco-editor';
@@ -144,9 +144,6 @@ import { dbApi } from '../../api';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { joinClientParams } from '@/common/request';
import { buildProgressProps } from '@/components/progress-notify/progress-notify';
import ProgressNotify from '@/components/progress-notify/progress-notify.vue';
import syssocket from '@/common/syssocket';
import SvgIcon from '@/components/svgIcon/index.vue';
import { Pane, Splitpanes } from 'splitpanes';
import { useI18n } from 'vue-i18n';
@@ -303,17 +300,17 @@ const onRunSql = async (newTab = false) => {
const sqls = splitSql(sql);
// 简单截取前十个字符
const sqlPrefix = sql.slice(0, 10).toLowerCase();
const nonQuery =
sqlPrefix.startsWith('update') ||
sqlPrefix.startsWith('insert') ||
sqlPrefix.startsWith('delete') ||
sqlPrefix.startsWith('alert') ||
sqlPrefix.startsWith('drop') ||
sqlPrefix.startsWith('create');
if (sqls.length == 1) {
const oneSql = sqls[0];
// 简单截取前十个字符
const sqlPrefix = oneSql.slice(0, 10).toLowerCase();
const nonQuery =
sqlPrefix.startsWith('update') ||
sqlPrefix.startsWith('insert') ||
sqlPrefix.startsWith('delete') ||
sqlPrefix.startsWith('alter') ||
sqlPrefix.startsWith('drop') ||
sqlPrefix.startsWith('create');
let execRemark;
if (nonQuery) {
const res: any = await ElMessageBox.prompt(t('db.enterExecRemarkTips'), 'Tip', {
@@ -323,7 +320,7 @@ const onRunSql = async (newTab = false) => {
});
execRemark = res.value;
}
runSql(sql, execRemark, newTab);
runSql(oneSql, execRemark, newTab);
} else {
let isFirst = true;
for (let s of sqls) {
@@ -593,44 +590,8 @@ const replaceSelection = (str: string, selection: any) => {
});
};
/**
* sql文件执行进度通知缓存
*/
const sqlExecNotifyMap: Map<string, any> = new Map();
const beforeUpload = (file: File) => {
ElMessage.success(t('db.scriptFileUploadRunning', { filename: file.name }));
syssocket.registerMsgHandler('execSqlFileProgress', function (message: any) {
const content = JSON.parse(message.msg);
const id = content.id;
let progress = sqlExecNotifyMap.get(id);
if (content.terminated) {
if (progress != undefined) {
progress.notification?.close();
sqlExecNotifyMap.delete(id);
progress = undefined;
}
return;
}
if (progress == undefined) {
progress = {
props: reactive(buildProgressProps()),
notification: undefined,
};
}
progress.props.progress.title = content.title;
progress.props.progress.executedStatements = content.executedStatements;
if (!sqlExecNotifyMap.has(id)) {
progress.notification = ElNotification({
duration: 0,
title: message.title,
message: h(ProgressNotify, progress.props),
type: syssocket.getMsgType(message.type),
showClose: false,
});
sqlExecNotifyMap.set(id, progress);
}
});
};
// 执行sql成功

View File

@@ -1,36 +1,54 @@
<template>
<div class="string-input-container w100" v-if="dataType == DataType.String || dataType == DataType.Number">
<el-input
:ref="(el: any) => focus && el?.focus()"
:ref="
(el: any) => {
nextTick(() => {
focus && el?.focus();
});
}
"
:disabled="disabled"
@blur="handleBlur"
:class="`w100 mb4 ${showEditorIcon ? 'string-input-container-show-icon' : ''}`"
size="small"
v-model="itemValue"
:placeholder="placeholder"
:placeholder="placeholder ?? $t('common.pleaseInput')"
/>
<SvgIcon v-if="showEditorIcon" @mousedown="openEditor" class="string-input-container-icon" name="FullScreen" :size="10" />
</div>
<el-date-picker
v-else-if="dataType == DataType.Date"
:ref="(el: any) => focus && el?.focus()"
:ref="
(el: any) => {
nextTick(() => {
focus && el?.focus();
});
}
"
:disabled="disabled"
@change="emit('blur')"
@change="handleBlur"
@blur="handleBlur"
class="edit-time-picker mb4"
popper-class="edit-time-picker-popper"
size="small"
v-model="itemValue"
:clearable="false"
type="Date"
type="date"
value-format="YYYY-MM-DD"
:placeholder="`date-${placeholder}`"
:placeholder="`date-${placeholder ?? $t('common.pleaseSelect')}`"
/>
<el-date-picker
v-else-if="dataType == DataType.DateTime"
:ref="(el: any) => focus && el?.focus()"
:ref="
(el: any) => {
nextTick(() => {
focus && el?.focus();
});
}
"
:disabled="disabled"
@change="handleBlur"
@blur="handleBlur"
@@ -41,12 +59,18 @@
:clearable="false"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
:placeholder="`datetime-${placeholder}`"
:placeholder="`datetime-${placeholder ?? $t('common.pleaseSelect')}`"
/>
<el-time-picker
v-else-if="dataType == DataType.Time"
:ref="(el: any) => focus && el?.focus()"
:ref="
(el: any) => {
nextTick(() => {
focus && el?.focus();
});
}
"
:disabled="disabled"
@change="handleBlur"
@blur="handleBlur"
@@ -56,12 +80,12 @@
v-model="itemValue"
:clearable="false"
value-format="HH:mm:ss"
:placeholder="`time-${placeholder}`"
:placeholder="`time-${placeholder ?? $t('common.pleaseSelect')}`"
/>
</template>
<script lang="ts" setup>
import { computed, ref, Ref } from 'vue';
import { computed, nextTick, ref, Ref } from 'vue';
import { ElInput, ElMessage } from 'element-plus';
import { DataType } from '../../dialect/index';
import SvgIcon from '@/components/svgIcon/index.vue';

View File

@@ -155,7 +155,7 @@
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch } from 'vue';
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch, Ref } from 'vue';
import { ElInput, ElMessage } from 'element-plus';
import { copyToClipboard } from '@/common/utils/string';
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
@@ -164,7 +164,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
import { exportCsv, exportFile } from '@/common/utils/export';
import { formatDate } from '@/common/utils/format';
import { useIntervalFn, useStorage } from '@vueuse/core';
import { ColumnTypeSubscript, compatibleMysql, DataType, DbDialect, getDbDialect } from '../../dialect/index';
import { ColumnTypeSubscript, DataType, DbDialect, getDbDialect } from '../../dialect/index';
import ColumnFormItem from './ColumnFormItem.vue';
import DbTableDataForm from './DbTableDataForm.vue';
import { useI18n } from 'vue-i18n';
@@ -239,9 +239,11 @@ const cmHeaderFixed = new ContextmenuItem('fixed', 'db.fixed')
})
.withHideFunc((data: any) => data.fixed);
const cmHeaderCancenFixed = new ContextmenuItem('cancelFixed', 'db.cancelFiexd')
const cmHeaderCancelFixed = new ContextmenuItem('cancelFixed', 'db.cancelFiexd')
.withIcon('Minus')
.withOnClick((data: any) => (data.fixed = false))
.withOnClick((data: any) => {
data.fixed = false;
})
.withHideFunc((data: any) => !data.fixed);
/** 表数据 contextmenu items **/
@@ -253,7 +255,7 @@ const cmDataCopyCell = new ContextmenuItem('copyValue', 'common.copy')
})
.withHideFunc(() => {
// 选中多条则隐藏该复制按钮
return selectionRowsMap.size > 1;
return selectionRowsMap.value.size > 1;
});
const cmDataDel = new ContextmenuItem('deleteData', 'common.delete')
@@ -307,7 +309,7 @@ class UpdatedRow {
/**
* 修改到的列信息, columnName -> tablecelldata
*/
columnsMap: Map<string, TableCellData> = new Map();
columnsMap = new Map<string, TableCellData>();
}
class TableCellData {
@@ -319,16 +321,16 @@ class TableCellData {
let dbDialect: DbDialect = null as any;
let nowSortColumn = null as any;
let nowSortColumn = ref(null) as any;
// 当前正在更新的单元格
let nowUpdateCell: NowUpdateCell = null as any;
let nowUpdateCell: Ref<NowUpdateCell> = ref(null) as any;
// 选中的数据, key->rowIndex value->primaryKeyValue
const selectionRowsMap: Map<number, any> = new Map();
const selectionRowsMap = ref(new Map<number, any>());
// 更新单元格 key-> rowIndex value -> 更新行
const cellUpdateMap: Map<number, UpdatedRow> = new Map();
const cellUpdateMap = ref(new Map<number, UpdatedRow>());
// 数据加载时间计时器
const { pause, resume } = useIntervalFn(() => {
@@ -452,24 +454,11 @@ onBeforeUnmount(() => {
endLoading();
});
const formatDataValues = (datas: any) => {
// mysql数据暂不做处理
if (compatibleMysql(getNowDbInst().type)) {
return;
}
for (let data of datas) {
for (let column of props.columns!) {
data[column.columnName] = getFormatTimeValue(dbDialect.getDataType(column.dataType), data[column.columnName]);
}
}
};
const setTableData = (datas: any) => {
tableRef.value?.scrollTo({ scrollLeft: 0, scrollTop: 0 });
selectionRowsMap.clear();
cellUpdateMap.clear();
formatDataValues(datas);
selectionRowsMap.value.clear();
cellUpdateMap.value.clear();
// formatDataValues(datas);
state.datas = datas;
setTableColumns(props.columns);
};
@@ -481,7 +470,6 @@ const setTableColumns = (columns: any) => {
x.dataType = dbDialect.getDataType(x.columnType);
x.dataTypeSubscript = ColumnTypeSubscript[x.dataType];
x.remark = `${x.columnType} ${x.columnComment ? ' | ' + x.columnComment : ''}`;
return {
...x,
key: columnName,
@@ -520,7 +508,7 @@ const cancelLoading = async () => {
* @param colIndex ci
*/
const canEdit = (rowIndex: number, colIndex: number) => {
return state.table && nowUpdateCell?.rowIndex == rowIndex && nowUpdateCell?.colIndex == colIndex;
return state.table && nowUpdateCell.value?.rowIndex == rowIndex && nowUpdateCell.value?.colIndex == colIndex;
};
/**
@@ -529,7 +517,7 @@ const canEdit = (rowIndex: number, colIndex: number) => {
* @param columnName cn
*/
const isUpdated = (rowIndex: number, columnName: string) => {
return cellUpdateMap.get(rowIndex)?.columnsMap.get(columnName);
return cellUpdateMap.value.get(rowIndex)?.columnsMap.get(columnName);
};
/**
@@ -537,7 +525,7 @@ const isUpdated = (rowIndex: number, columnName: string) => {
* @param rowIndex
*/
const isSelection = (rowIndex: number): boolean => {
return selectionRowsMap.get(rowIndex);
return selectionRowsMap.value.get(rowIndex);
};
/**
@@ -549,16 +537,14 @@ const isSelection = (rowIndex: number): boolean => {
const selectionRow = (rowIndex: number, rowData: any, isMultiple = false) => {
if (isMultiple) {
// 如果重复点击,则取消改选中数据
if (selectionRowsMap.get(rowIndex)) {
selectionRowsMap.delete(rowIndex);
triggerRefresh();
if (selectionRowsMap.value.get(rowIndex)) {
selectionRowsMap.value.delete(rowIndex);
return;
}
} else {
selectionRowsMap.clear();
selectionRowsMap.value.clear();
}
selectionRowsMap.set(rowIndex, rowData);
triggerRefresh();
selectionRowsMap.value.set(rowIndex, rowData);
};
/**
@@ -584,7 +570,7 @@ const headerContextmenuClick = (event: any, data: any) => {
const { clientX, clientY } = event;
state.contextmenu.dropdown.x = clientX;
state.contextmenu.dropdown.y = clientY;
state.contextmenu.items = [cmHeaderAsc, cmHeaderDesc, cmHeaderFixed, cmHeaderCancenFixed];
state.contextmenu.items = [cmHeaderAsc, cmHeaderDesc, cmHeaderFixed, cmHeaderCancelFixed];
contextmenuRef.value.openContextmenu(data);
};
@@ -606,7 +592,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
* 表排序字段变更
*/
const onTableSortChange = async (sort: any) => {
nowSortColumn = sort;
nowSortColumn.value = sort;
cancelUpdateFields();
emits('sortChange', sort);
};
@@ -615,7 +601,7 @@ const onTableSortChange = async (sort: any) => {
* 执行删除数据事件
*/
const onDeleteData = async () => {
const deleteDatas = Array.from(selectionRowsMap.values());
const deleteDatas = Array.from(selectionRowsMap.value.values());
const db = state.db;
const dbInst = getNowDbInst();
dbInst.promptExeSql(db, await dbInst.genDeleteByPrimaryKeysSql(db, state.table, deleteDatas as any), null, () => {
@@ -624,7 +610,7 @@ const onDeleteData = async () => {
};
const onEditRowData = () => {
const selectionDatas = Array.from(selectionRowsMap.values());
const selectionDatas = Array.from(selectionRowsMap.value.values());
if (selectionDatas.length > 1) {
ElMessage.warning(t('db.onlySelectOneData'));
return;
@@ -636,14 +622,14 @@ const onEditRowData = () => {
};
const onGenerateInsertSql = async () => {
const selectionDatas = Array.from(selectionRowsMap.values());
const selectionDatas = Array.from(selectionRowsMap.value.values());
state.genTxtDialog.txt = await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas);
state.genTxtDialog.title = 'SQL';
state.genTxtDialog.visible = true;
};
const onGenerateJson = async () => {
const selectionDatas = Array.from(selectionRowsMap.values());
const selectionDatas = Array.from(selectionRowsMap.value.values());
// 按列字段重新排序对象key
const jsonObj = [];
for (let selectionData of selectionDatas) {
@@ -685,12 +671,12 @@ const onExportSql = async () => {
};
const onEnterEditMode = (rowData: any, column: any, rowIndex = 0, columnIndex = 0) => {
if (!state.table) {
// 不存在表,或者已经在编辑中,则不处理
if (!state.table || nowUpdateCell.value) {
return;
}
triggerRefresh();
nowUpdateCell = {
nowUpdateCell.value = {
rowIndex: rowIndex,
colIndex: columnIndex,
oldValue: rowData[column.dataKey],
@@ -699,24 +685,23 @@ const onEnterEditMode = (rowData: any, column: any, rowIndex = 0, columnIndex =
};
const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
if (!nowUpdateCell) {
if (!nowUpdateCell.value) {
return;
}
const oldValue = nowUpdateCell.oldValue;
const oldValue = nowUpdateCell.value.oldValue;
const newValue = rowData[column.dataKey];
// 未改变单元格值
if (oldValue == newValue) {
nowUpdateCell = null as any;
triggerRefresh();
nowUpdateCell.value = null as any;
return;
}
let updatedRow = cellUpdateMap.get(rowIndex);
let updatedRow = cellUpdateMap.value.get(rowIndex);
if (!updatedRow) {
updatedRow = new UpdatedRow();
updatedRow.rowData = rowData;
cellUpdateMap.set(rowIndex, updatedRow);
cellUpdateMap.value.set(rowIndex, updatedRow);
}
const columnName = column.dataKey;
@@ -724,7 +709,7 @@ const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
if (cellData) {
// 多次修改情况,可能又修改回原值,则移除该修改单元格
if (cellData.oldValue == newValue) {
cellUpdateMap.delete(rowIndex);
cellUpdateMap.value.delete(rowIndex);
}
} else {
cellData = new TableCellData();
@@ -732,21 +717,20 @@ const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
updatedRow.columnsMap.set(columnName, cellData);
}
nowUpdateCell = null as any;
triggerRefresh();
nowUpdateCell.value = null as any;
changeUpdatedField();
};
const submitUpdateFields = async () => {
const dbInst = getNowDbInst();
if (cellUpdateMap.size == 0) {
if (cellUpdateMap.value.size == 0) {
return;
}
const db = state.db;
let res = '';
for (let updateRow of cellUpdateMap.values()) {
for (let updateRow of cellUpdateMap.value.values()) {
const rowData = { ...updateRow.rowData };
let updateColumnValue: any = {};
@@ -763,14 +747,13 @@ const submitUpdateFields = async () => {
}
dbInst.promptExeSql(db, res, null, () => {
triggerRefresh();
cellUpdateMap.clear();
cellUpdateMap.value.clear();
changeUpdatedField();
});
};
const cancelUpdateFields = () => {
const updateRows = cellUpdateMap.values();
const updateRows = cellUpdateMap.value.values();
// 恢复原值
for (let updateRow of updateRows) {
const rowData = updateRow.rowData;
@@ -778,12 +761,12 @@ const cancelUpdateFields = () => {
rowData[k] = v.oldValue;
});
}
cellUpdateMap.clear();
cellUpdateMap.value.clear();
changeUpdatedField();
};
const changeUpdatedField = () => {
emits('changeUpdatedField', cellUpdateMap);
emits('changeUpdatedField', cellUpdateMap.value);
};
const rowClass = (row: any) => {
@@ -793,44 +776,6 @@ const rowClass = (row: any) => {
return '';
};
/**
* 根据数据库返回的时间字段类型,获取格式化后的时间值
* @param dataType getDataType返回的数据类型
* @param originValue 原始值
* @return 格式化后的值
*/
const getFormatTimeValue = (dataType: DataType, originValue: string): string => {
if (!originValue || dataType === DataType.Number || dataType === DataType.String) {
return originValue;
}
// 把Z去掉
originValue = originValue.replace('Z', '');
switch (dataType) {
case DataType.Time:
return formatDate(originValue, 'HH:mm:ss');
case DataType.Date:
return formatDate(originValue, 'YYYY-MM-DD');
case DataType.DateTime:
return formatDate(originValue, 'YYYY-MM-DD HH:mm:ss');
default:
return originValue;
}
};
/**
* 触发响应式实时刷新,否则需要滑动或移动才能使样式实时生效
*/
const triggerRefresh = () => {
// 改变columns等属性值才能触发slot中的if条件等, 暂不知为啥
if (state.columns[0].opTimes) {
state.columns[0].opTimes = state.columns[0].opTimes + 1;
} else {
state.columns[0].opTimes = 1;
}
};
const scrollLeftValue = ref(0);
const onTableScroll = (param: any) => {
scrollLeftValue.value = param.scrollLeft;

View File

@@ -592,7 +592,7 @@ const onSelectByCondition = async () => {
*/
const onTableSortChange = async (sort: any) => {
const sortType = sort.order == 'desc' ? 'DESC' : 'ASC';
state.orderBy = `ORDER BY ${sort.columnName} ${sortType}`;
state.orderBy = `ORDER BY ${state.dbDialect.quoteIdentifier(sort.columnName)} ${sortType}`;
await onRefresh();
};

View File

@@ -7,25 +7,31 @@
<el-form label-position="left" ref="formRef" :model="tableData" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item prop="tableName" label="表名">
<el-form-item prop="tableName" :label="$t('db.tableName')">
<el-input style="width: 80%" v-model="tableData.tableName" size="small"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="tableComment" label="备注">
<el-form-item prop="tableComment" :label="$t('db.comment')">
<el-input style="width: 80%" v-model="tableData.tableComment" size="small"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-tabs v-model="activeName">
<el-tab-pane label="字段" name="1">
<el-tab-pane :label="$t('db.column')" name="1">
<el-table ref="tableRef" :data="tableData.fields.res" :max-height="tableData.height">
<el-table-column :prop="item.prop" :label="item.label" v-for="item in tableData.fields.colNames" :key="item.prop" :width="item.width">
<el-table-column
:prop="item.prop"
:label="$t(item.label)"
v-for="item in tableData.fields.colNames"
:key="item.prop"
:width="item.width"
>
<template #default="scope">
<el-input v-if="item.prop === 'name'" size="small" v-model="scope.row.name" />
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type">
<el-select v-else-if="item.prop === 'type'" filterable size="small" v-model="scope.row.type" allow-create>
<el-option
v-for="pgsqlType in getDbDialect(dbType!).getInfo().columnTypes"
:key="pgsqlType.dataType"
@@ -35,7 +41,7 @@
<span v-if="pgsqlType.dataType === pgsqlType.udtName"
>{{ pgsqlType.dataType }}{{ pgsqlType.desc && '' + pgsqlType.desc }}</span
>
<span v-else>{{ pgsqlType.dataType }}别名{{ pgsqlType.udtName }} {{ pgsqlType.desc }}</span>
<span v-else>{{ pgsqlType.dataType }}{{ $t('db.alias') }}: {{ pgsqlType.udtName }} {{ pgsqlType.desc }}</span>
</el-option>
</el-select>
@@ -58,22 +64,22 @@
<el-input v-else-if="item.prop === 'remark'" size="small" v-model="scope.row.remark" />
<el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteRow(scope.$index)">
<el-popconfirm v-else-if="item.prop === 'action'" :title="$t('common.deleteConfirm')" @confirm="deleteRow(scope.$index)">
<template #reference>
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
<el-link type="danger" plain size="small" :underline="false">{{ $t('common.delete') }}</el-link>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-row style="margin-top: 20px">
<el-button @click="addDefaultRows()" link type="warning" icon="plus">添加默认列</el-button>
<el-button @click="addRow()" link type="primary" icon="plus">添加列</el-button>
<el-button @click="addDefaultRows()" link type="warning" icon="plus">{{ $t('db.addDefaultColumn') }}</el-button>
<el-button @click="addRow()" link type="primary" icon="plus">{{ $t('db.addColumn') }}</el-button>
</el-row>
</el-tab-pane>
<el-tab-pane label="索引" name="2">
<el-tab-pane :label="$t('db.index')" name="2">
<el-table :data="tableData.indexs.res" :max-height="tableData.height">
<el-table-column :prop="item.prop" :label="item.label" v-for="item in tableData.indexs.colNames" :key="item.prop">
<el-table-column :prop="item.prop" :label="$t(item.label)" v-for="item in tableData.indexs.colNames" :key="item.prop">
<template #default="scope">
<el-input v-if="item.prop === 'indexName'" size="small" disabled v-model="scope.row.indexName"></el-input>
@@ -84,7 +90,6 @@
collapse-tags
collapse-tags-tooltip
filterable
placeholder="请选择字段"
@change="indexChanges(scope.row)"
style="width: 100%"
>
@@ -100,9 +105,9 @@
<el-input v-if="item.prop === 'indexComment'" size="small" v-model="scope.row.indexComment"> </el-input>
<el-popconfirm v-else-if="item.prop === 'action'" title="确定删除?" @confirm="deleteIndex(scope.$index)">
<el-popconfirm v-else-if="item.prop === 'action'" :title="$t('common.deleteConfirm')" @confirm="deleteIndex(scope.$index)">
<template #reference>
<el-link type="danger" plain size="small" :underline="false">删除</el-link>
<el-link type="danger" plain size="small" :underline="false">{{ $t('common.delete') }}</el-link>
</template>
</el-popconfirm>
</template>
@@ -110,14 +115,14 @@
</el-table>
<el-row style="margin-top: 20px">
<el-button @click="addIndex()" link type="primary" icon="plus">添加索引</el-button>
<el-button @click="addIndex()" link type="primary" icon="plus">{{ $t('db.addIndex') }}</el-button>
</el-row>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<el-button @click="cancel()">取消</el-button>
<el-button :loading="btnloading" @click="submit()" type="primary">保存</el-button>
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
<el-button :loading="btnloading" @click="submit()" type="primary">{{ $t('common.save') }}</el-button>
</template>
</el-drawer>
</template>
@@ -129,6 +134,9 @@ import SqlExecBox from '../sqleditor/SqlExecBox';
import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
import { DbInst } from '../../db';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
visible: {
@@ -177,52 +185,52 @@ const state = reactive({
colNames: [
{
prop: 'name',
label: '字段名称',
label: 'db.columnName',
width: 200,
},
{
prop: 'type',
label: '字段类型',
label: 'common.type',
width: 120,
},
{
prop: 'length',
label: '长度',
label: 'db.length',
width: 120,
},
{
prop: 'numScale',
label: '小数精度',
label: 'db.numScale',
width: 120,
},
{
prop: 'value',
label: '默认值',
label: 'db.defaultValue',
width: 120,
},
{
prop: 'notNull',
label: '非空',
label: 'db.notNull',
width: 60,
},
{
prop: 'pri',
label: '主键',
label: 'db.primaryKey',
width: 60,
},
{
prop: 'auto_increment',
label: '自增',
label: 'db.autoIncrement',
width: 60,
},
{
prop: 'remark',
label: '备注',
label: 'db.comment',
},
{
prop: 'action',
label: '操作',
label: 'common.operation',
width: 70,
},
] as ColName[],
@@ -233,27 +241,27 @@ const state = reactive({
colNames: [
{
prop: 'indexName',
label: '索引名',
label: 'common.name',
},
{
prop: 'columnNames',
label: '列名',
label: 'db.columnName',
},
{
prop: 'unique',
label: '唯一',
label: 'db.unique',
},
{
prop: 'indexType',
label: '类型',
label: 'common.type',
},
{
prop: 'indexComment',
label: '备注',
label: 'db.comment',
},
{
prop: 'action',
label: '操作',
label: 'common.operation',
},
],
columns: [{ name: '', remark: '' }],
@@ -336,7 +344,7 @@ const deleteIndex = (index: any) => {
const submit = async () => {
let sql = genSql();
if (!sql) {
ElMessage.warning('没有更改');
ElMessage.warning(t('db.noChange'));
return;
}
SqlExecBox({
@@ -472,10 +480,10 @@ const indexChanges = (row: any) => {
}
let suffix = row.unique ? 'udx' : 'idx';
let commentSuffix = row.unique ? '唯一索引' : '普通索引';
let commentSuffix = row.unique ? t('db.uniqueIndex') : t('db.normalIndex');
// 以表名为前缀
row.indexName = `${tableData.value.tableName}_${name}_${suffix}`.replaceAll(' ', '');
row.indexComment = `${tableData.value.tableName}(${name.replaceAll('_', ',')})${commentSuffix}`;
row.indexComment = `${tableData.value.tableName} ${t('db.table')} (${name.replaceAll('_', ',')})${commentSuffix}`;
};
const disableEditIncr = () => {

View File

@@ -123,7 +123,7 @@ export class DbInst {
},
kind: monaco.languages.CompletionItemKind.File,
detail: tableComment,
insertText: dbDialect.quoteIdentifier(tableName) + ' ',
insertText: dbDialect.quoteIdentifier(tableName),
range,
sortText: 300 + index + '',
});
@@ -147,7 +147,7 @@ export class DbInst {
},
kind: monaco.languages.CompletionItemKind.Property,
detail: '', // 不显示detail, 否则选中时备注等会被遮挡
insertText: dbDialect.quoteIdentifier(fieldName) + ' ', // create_time
insertText: dbDialect.quoteIdentifier(fieldName), // create_time
range,
sortText: 100 + index + '', // 使用表字段声明顺序排序,排序需为字符串类型
});
@@ -261,7 +261,8 @@ export class DbInst {
* @returns count sql
*/
getDefaultCountSql = (table: string, condition?: string) => {
return `SELECT COUNT(*) count FROM ${this.wrapName(table)} ${condition ? 'WHERE ' + condition : ''}`;
return `SELECT COUNT(*) count
FROM ${this.wrapName(table)} ${condition ? 'WHERE ' + condition : ''}`;
};
// 获取指定表的默认查询sql
@@ -303,7 +304,8 @@ export class DbInst {
colNames.push(this.wrapName(colName));
values.push(dbDialect.wrapValue(column.dataType, data[colName]));
}
sqls.push(`INSERT INTO ${schema}${this.wrapName(table)} (${colNames.join(', ')}) VALUES(${values.join(', ')})`);
sqls.push(`INSERT INTO ${schema}${this.wrapName(table)} (${colNames.join(', ')})
VALUES (${values.join(', ')})`);
}
return sqls.join(';\n') + ';';
}
@@ -322,7 +324,8 @@ export class DbInst {
schema = this.wrapName(dbArr[1]) + '.';
}
let sql = `UPDATE ${schema}${this.wrapName(table)} SET `;
let sql = `UPDATE ${schema}${this.wrapName(table)}
SET `;
// 主键列信息
const primaryKey = await this.loadTableColumn(dbName, table);
let primaryKeyType = primaryKey.dataType;
@@ -350,7 +353,9 @@ export class DbInst {
const primaryKey = await this.loadTableColumn(db, table);
const primaryKeyColumnName = primaryKey.columnName;
const ids = datas.map((d: any) => `${this.getDialect().wrapValue(primaryKey.dataType, d[primaryKeyColumnName])}`).join(',');
return `DELETE FROM ${this.wrapName(table)} WHERE ${this.wrapName(primaryKeyColumnName)} IN (${ids})`;
return `DELETE
FROM ${this.wrapName(table)}
WHERE ${this.wrapName(primaryKeyColumnName)} IN (${ids})`;
}
/*
@@ -459,7 +464,7 @@ export class DbInst {
* @returns
*/
static isNumber(columnType: string) {
return columnType && columnType.match(/int|double|float|number|decimal|byte|bit/gi);
return columnType && columnType.match(/(int|uint|double|float|number|decimal|byte|bit)/gi);
}
/**
@@ -814,7 +819,7 @@ export function registerDbCompletionItemProvider(dbId: number, db: string, dbs:
},
kind: monaco.languages.CompletionItemKind.File,
detail: tableComment,
insertText: dbDialect.quoteIdentifier(tableName) + ' ',
insertText: dbDialect.quoteIdentifier(tableName),
range,
sortText: 300 + index + '',
});
@@ -833,7 +838,17 @@ export function registerDbCompletionItemProvider(dbId: number, db: string, dbs:
});
}
function getTableName4SqlCtx(sql: string, alias: string = '', defaultDb: string): { tableName: string; tableAlias: string; db: string } | undefined {
function getTableName4SqlCtx(
sql: string,
alias: string = '',
defaultDb: string
):
| {
tableName: string;
tableAlias: string;
db: string;
}
| undefined {
// 去除多余的换行、空格和制表符
sql = sql.replace(/[\r\n\s\t]+/g, ' ');

View File

@@ -54,6 +54,21 @@ const DM_TYPE_LIST: sqlColumnType[] = [
{ udtName: 'BLOB', dataType: 'BLOB', desc: '变长的二进制大对象', space: '', range: '100G-1' },
{ udtName: 'CLOB', dataType: 'CLOB', desc: '同TEXT', space: '', range: '100G-1' },
{ udtName: 'BFILE', dataType: 'BFILE', desc: '二进制文件', space: '', range: '100G-1' },
// 达梦系统特有的字段类型 从 ALL_TYPES 表中查询得到
{udtName: 'SYSGEO.ST_CURVE', dataType: 'SYSGEO.ST_CURVE', desc: '表示一条曲线,可以是圆弧、抛物线等', space: '', range: ''},
{udtName: 'SYSGEO.ST_LINESTRING', dataType: 'SYSGEO.ST_LINESTRING', desc: '表示一条或多条连续的线段', space: '', range: ''},
{udtName: 'SYSGEO.ST_GEOMCOLLECTION', dataType: 'SYSGEO.ST_GEOMCOLLECTION', desc: '表示一个几何对象集合,可以包含多个不同类型的几何对象', space: '', range: ''},
{udtName: 'SYSGEO.ST_GEOMETRY', dataType: 'SYSGEO.ST_GEOMETRY', desc: '通用几何对象类型,可以表示点、线、面等任何几何形状', space: '', range: ''},
{udtName: 'SYSGEO.ST_MULTICURVE', dataType: 'SYSGEO.ST_MULTICURVE', desc: '表示多个曲线的集合', space: '', range: ''},
{udtName: 'SYSGEO.ST_MULTILINESTRING', dataType: 'SYSGEO.ST_MULTILINESTRING', desc: '表示多个线串的集合', space: '', range: ''},
{udtName: 'SYSGEO.ST_MULTIPOINT', dataType: 'SYSGEO.ST_MULTIPOINT', desc: '表示多个点的集合', space: '', range: ''},
{udtName: 'SYSGEO.ST_MULTIPOLYGON', dataType: 'SYSGEO.ST_MULTIPOLYGON', desc: '表示多个多边形的集合', space: '', range: ''},
{udtName: 'SYSGEO.ST_MULTISURFACE', dataType: 'SYSGEO.ST_MULTISURFACE', desc: '表示多个表面的集合', space: '', range: ''},
{udtName: 'SYSGEO.ST_POINT', dataType: 'SYSGEO.ST_POINT', desc: '表示一个点', space: '', range: ''},
{udtName: 'SYSGEO.ST_POLYGON', dataType: 'SYSGEO.ST_POLYGON', desc: '表示一个多边形', space: '', range: ''},
{udtName: 'SYSGEO.ST_SURFACE', dataType: 'SYSGEO.ST_SURFACE', desc: '表示一个表面,通常是一个多边形', space: '', range: ''},
];
// 参考官方文档https://eco.dameng.com/document/dm/zh-cn/pm/function.html

View File

@@ -209,8 +209,7 @@ class PostgresqlDialect implements DbDialect {
}
quoteIdentifier = (name: string) => {
// 后端sql解析器暂不支持pgsql
return name;
return `"${name}"`;
};
matchType(text: string, arr: string[]): boolean {

View File

@@ -22,7 +22,7 @@ export const DbSqlExecStatusEnum = {
export const DbDataSyncDuplicateStrategyEnum = {
None: EnumValue.of(-1, 'db.none'),
Ignore: EnumValue.of(1, 'db.ingore'),
Ignore: EnumValue.of(1, 'db.ignore'),
Replace: EnumValue.of(2, 'db.replace'),
};

View File

@@ -54,7 +54,7 @@
</div>
<el-divider content-position="left">{{ $t('common.other') }}</el-divider>
<el-form-item prop="enableRecorder" :label="$t('machine.sshTunnel')">
<el-form-item prop="enableRecorder" :label="$t('machine.terminalPlayback')">
<el-checkbox v-model="form.enableRecorder" :true-value="1" :false-value="-1"></el-checkbox>
</el-form-item>

View File

@@ -71,13 +71,12 @@
<template #status="{ data }">
<el-switch
v-auth:disabled="'machine:update'"
:width="52"
v-model="data.status"
:active-value="1"
:inactive-value="-1"
inline-prompt
active-text="启用"
inactive-text="停用"
:active-text="$t('common.enable')"
:inactive-text="$t('common.disable')"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
@change="changeStatus(data)"
></el-switch>
@@ -270,7 +269,7 @@ import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { formatByteSize, formatDate } from '@/common/utils/format';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { TagResourceTypePath } from '@/common/commonEnum';
import { SearchItem } from '@/components/SearchForm';
import { getTagPathSearchItem } from '../component/tag';
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
@@ -312,7 +311,7 @@ const perms = {
const searchItems = [
SearchItem.input('keyword', 'common.keyword').withPlaceholder('machine.keywordPlaceholder'),
getTagPathSearchItem(TagResourceTypeEnum.MachineAuthCert.value),
getTagPathSearchItem(TagResourceTypePath.MachineAuthCert),
];
const columns = [
@@ -320,7 +319,7 @@ const columns = [
TableColumn.new('name', 'common.name'),
TableColumn.new('ipPort', 'Ip:Port').isSlot().setAddWidth(50),
TableColumn.new('authCerts[0].username', 'machine.acName').isSlot('authCert').setAddWidth(10),
TableColumn.new('status', 'common.status').isSlot().setMinWidth(85),
TableColumn.new('status', 'common.status').isSlot(),
TableColumn.new('stat', 'machine.runningStat').isSlot().setAddWidth(55),
TableColumn.new('fs', 'machine.fs').isSlot().setAddWidth(25),
TableColumn.new('remark', 'common.remark'),

View File

@@ -6,7 +6,7 @@
<tag-tree
class="machine-terminal-tree"
ref="tagTreeRef"
:resource-type="TagResourceTypeEnum.MachineAuthCert.value"
:resource-type="TagResourceTypePath.MachineAuthCert"
:tag-path-node-type="NodeTypeTagPath"
:default-expanded-keys="state.defaultExpendKey"
>
@@ -168,7 +168,7 @@ import { useRouter } from 'vue-router';
import { getMachineTerminalSocketUrl, machineApi } from './api';
import { formatDate } from '@/common/utils/format';
import { hasPerms } from '@/components/auth/auth';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
import TagTree from '../component/TagTree.vue';
import { Pane, Splitpanes } from 'splitpanes';
@@ -378,7 +378,7 @@ const autoOpenTerminal = (codePath: string) => {
const machineCode = typeAndCodes[TagResourceTypeEnum.Machine.value][0];
state.defaultExpendKey = [tagPath, machineCode];
const authCertName = typeAndCodes[TagResourceTypeEnum.MachineAuthCert.value][0];
const authCertName = typeAndCodes[TagResourceTypeEnum.PublicAuthCert.value][0];
setTimeout(() => {
// 置空
autoOpenResourceStore.setMachineCodePath('');

View File

@@ -39,7 +39,11 @@
/></el-form-item>
<el-form-item ref="tagSelectRef" prop="codePaths" :label="$t('machine.relateMachine')">
<tag-tree-check height="200px" :tag-type="TagResourceTypeEnum.Machine.value" v-model="form.codePaths" />
<tag-tree-check
height="200px"
:tag-type="`${TagResourceTypeEnum.Machine.value}/${TagResourceTypeEnum.AuthCert.value}`"
v-model="form.codePaths"
/>
</el-form-item>
</el-form>

View File

@@ -33,9 +33,9 @@ export const CronJobStatusEnum = {
// 计划任务保存执行结果类型
export const CronJobSaveExecResTypeEnum = {
No: EnumValue.of(-1, '不记录').tagTypeDanger(),
OnError: EnumValue.of(1, '错误时记录').tagTypeWarning(),
Yes: EnumValue.of(2, '记录').tagTypeSuccess(),
No: EnumValue.of(-1, 'machine.noRecord').tagTypeDanger(),
OnError: EnumValue.of(1, 'machine.onErrorRecord').tagTypeWarning(),
Yes: EnumValue.of(2, 'machine.record').tagTypeSuccess(),
};
// 计划任务执行记录状态

View File

@@ -185,22 +185,19 @@
</el-table-column>
<el-table-column prop="mode" :label="$t('machine.attribute')" width="110"> </el-table-column>
<el-table-column
v-if="$props.protocol == MachineProtocolEnum.Ssh.value"
prop="username"
:label="$t('machine.user')"
min-width="55"
show-overflow-tooltip
>
<el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" :label="$t('machine.user')" min-width="70" show-overflow-tooltip>
<template #default="scope">
{{ userMap.get(scope.row.uid)?.uname || scope.row.uid }}
</template>
</el-table-column>
<el-table-column
v-if="$props.protocol == MachineProtocolEnum.Ssh.value"
prop="groupname"
:label="$t('machine.group')"
min-width="55"
show-overflow-tooltip
>
<el-table-column v-if="$props.protocol == MachineProtocolEnum.Ssh.value" :label="$t('machine.group')" min-width="70" show-overflow-tooltip>
<template #default="scope">
{{ groupMap.get(scope.row.gid)?.gname || scope.row.gid }}
</template>
</el-table-column>
<el-table-column prop="modTime" :label="$t('machine.modificationTime')" width="160" sortable> </el-table-column>
<el-table-column :width="130">
@@ -276,8 +273,8 @@
</el-form-item>
<el-form-item prop="type" :label="$t('common.type')">
<el-radio-group v-model="createFileDialog.type">
<el-radio value="d" label="d">{{ $t('machine.file') }}</el-radio>
<el-radio value="-" label="-">{{ $t('machine.folder') }}</el-radio>
<el-radio value="d" label="d">{{ $t('machine.folder') }}</el-radio>
<el-radio value="-" label="-">{{ $t('machine.file') }}</el-radio>
</el-radio-group>
</el-form-item>
</div>
@@ -303,7 +300,7 @@
<script lang="ts" setup>
import { computed, onMounted, reactive, ref, toRefs } from 'vue';
import { ElInput, ElMessage, ElMessageBox } from 'element-plus';
import { ElInput, ElMessage } from 'element-plus';
import { machineApi } from '../api';
import { joinClientParams } from '@/common/request';
@@ -334,8 +331,8 @@ const folderUploadRef: any = ref();
const folderType = 'd';
const userMap = new Map<number, any>();
const groupMap = new Map<number, any>();
const userMap = ref(new Map<number, any>());
const groupMap = ref(new Map<number, any>());
// 路径分隔符
const pathSep = '/';
@@ -382,13 +379,13 @@ onMounted(async () => {
if (props.protocol == MachineProtocolEnum.Ssh.value) {
machineApi.users.request({ id: machineId }).then((res: any) => {
for (let user of res) {
userMap.set(user.uid, user);
userMap.value.set(user.uid, user);
}
});
machineApi.groups.request({ id: machineId }).then((res: any) => {
for (let group of res) {
groupMap.set(group.gid, group);
groupMap.value.set(group.gid, group);
}
});
}
@@ -565,11 +562,6 @@ const lsFile = async (path: string) => {
path,
});
for (const file of res) {
if (props.protocol == MachineProtocolEnum.Ssh.value) {
file.username = userMap.get(file.uid)?.uname || file.uid;
file.groupname = groupMap.get(file.gid)?.gname || file.gid;
}
const type = file.type;
if (type == folderType) {
file.isFolder = true;

View File

@@ -33,7 +33,7 @@
:title="$t('machine.cmdConfig')"
v-model="dialogVisible"
:show-close="false"
width="600px"
size="40%"
:destroy-on-close="true"
:close-on-click-modal="false"
>
@@ -78,7 +78,11 @@
</el-form-item>
<el-form-item ref="tagSelectRef" prop="codePaths" :label="$t('machine.relateMachine')">
<tag-tree-check height="calc(100vh - 430px)" :tag-type="TagResourceTypeEnum.MachineAuthCert.value" v-model="form.codePaths" />
<tag-tree-check
height="calc(100vh - 430px)"
:tag-type="`${TagResourceTypeEnum.Machine.value}/${TagResourceTypeEnum.AuthCert.value}`"
v-model="form.codePaths"
/>
</el-form-item>
</el-form>
<template #footer>

View File

@@ -121,10 +121,10 @@
<el-dialog width="400px" :title="$t('mongo.createDbAndColl')" v-model="createDbDialog.visible" :destroy-on-close="true">
<el-form :model="createDbDialog.form" label-width="auto">
<el-form-item prop="dbName" :title="$t('mongo.dbName')" required>
<el-form-item prop="dbName" :label="$t('mongo.dbName')" required>
<el-input v-model="createDbDialog.form.dbName" clearable></el-input>
</el-form-item>
<el-form-item prop="collectionName" :title="$t('mongo.collName')" required>
<el-form-item prop="collectionName" :label="$t('mongo.collName')" required>
<el-input v-model="createDbDialog.form.collectionName" clearable></el-input>
</el-form-item>
</el-form>

View File

@@ -160,7 +160,7 @@
<div style="text-align: center; margin-top: 10px"></div>
<el-dialog :title="$t('redis.addKey')" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
<el-form ref="keyForm" label-width="auto">
<el-form ref="keyForm" label-width="auto" :rules="keyFormRules" :model="newKeyDialog.keyInfo">
<el-form-item prop="key" label="Key" required>
<el-input v-model.trim="newKeyDialog.keyInfo.key"></el-input>
</el-form-item>
@@ -187,9 +187,9 @@
<script lang="ts" setup>
import { redisApi } from './api';
import { ref, defineAsyncComponent, toRefs, reactive, onMounted, nextTick, Ref, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { isTrue, notBlank, notNull } from '@/common/assert';
import { ref, defineAsyncComponent, toRefs, reactive, onMounted, nextTick, Ref, watch, useTemplateRef } from 'vue';
import { ElMessageBox } from 'element-plus';
import { isTrue, notNull } from '@/common/assert';
import { copyToClipboard } from '@/common/utils/string';
import { TagTreeNode, NodeType, getTagTypeCodeByPath } from '../component/tag';
import TagTree from '../component/TagTree.vue';
@@ -202,13 +202,21 @@ import { RedisInst } from './redis';
import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg, useI18nPleaseInput } from '@/hooks/useI18n';
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
const { t } = useI18n();
const contextmenuRef = ref();
const keyFormRules = {
key: [
{
required: true,
message: useI18nPleaseInput('Key'),
trigger: ['change', 'blur'],
},
],
};
const cmCopyKey = new ContextmenuItem('copyValue', 'Copy')
.withIcon('CopyDocument')
@@ -301,8 +309,11 @@ const treeProps = {
const defaultCount = 250;
const contextmenuRef = ref();
const keyTreeRef: any = ref(null);
const tagTreeRef: any = ref(null);
const keyFormRef = useTemplateRef('keyForm');
const redisInst: Ref<RedisInst> = ref(new RedisInst());
const state = reactive({
@@ -573,9 +584,9 @@ const cancelNewKey = () => {
};
const newKey = async () => {
await useI18nFormValidate(keyFormRef);
const keyInfo = state.newKeyDialog.keyInfo;
const key = keyInfo.key;
notBlank(key, t('redis.keyNotEmpty'));
showKeyDetail(
{

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false">
<el-drawer :title="title" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>

View File

@@ -344,7 +344,7 @@ const allowDrag = (node: any) => {
const tagType = node.data.type;
return (
tagType == TagResourceTypeEnum.Tag.value ||
tagType == TagResourceTypeEnum.Db.value ||
tagType == TagResourceTypeEnum.DbInstance.value ||
tagType == TagResourceTypeEnum.Redis.value ||
tagType == TagResourceTypeEnum.Machine.value ||
tagType == TagResourceTypeEnum.Mongo.value

View File

@@ -7,7 +7,7 @@ export const tagApi = {
delTagTree: Api.newDelete('/tag-trees/{id}'),
movingTag: Api.newPost('/tag-trees/moving'),
getResourceTagPaths: Api.newGet('/tag-trees/resources/{resourceType}/tag-paths'),
getResourceTagPaths: Api.newGet('/tag-trees/resources/tag-paths'),
countTagResource: Api.newGet('/tag-trees/resources/count'),
getRelateTagIds: Api.newGet('/tag-trees/relate/{relateType}/{relateId}'),

View File

@@ -14,7 +14,7 @@
ref="relatePageTableRef"
:pageable="false"
:page-api="accountApi.roles"
v-model:query-form="releateQuery as any"
v-model:query-form="releateQuery"
:columns="relatedColumns"
:tool-button="false"
lazy
@@ -133,6 +133,8 @@ const state = reactive({
},
releateQuery: {
id: 0, //账号id
pageNum: 1,
pageSize: 1000,
},
showResourceDialog: {
title: '',

View File

@@ -3,17 +3,17 @@
<el-dialog :title="title" :destroy-on-close="true" v-model="dialogVisible" width="800px">
<el-form :model="form" :inline="true" ref="menuForm" :rules="rules" label-width="auto">
<el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item class="w100" prop="type" :label="$t('common.type')" required>
<enum-select :enums="ResourceTypeEnum" v-model="form.type" :disabled="typeDisabled" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item class="w100" prop="name" :label="$t('common.name')" required>
<el-input v-model.trim="form.name" auto-complete="off"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-form-item class="w100" prop="code" label="path|code">
<template #label>
path|code
@@ -26,12 +26,12 @@
<el-input v-model.trim="form.code" :placeholder="$t('system.menu.menuCodePlaceholder')" auto-complete="off"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-if="form.type === menuTypeValue">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<el-form-item class="w100" :label="$t('system.menu.icon')">
<icon-selector v-model="form.meta.icon" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-if="form.type === menuTypeValue">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<el-form-item class="w100">
<template #label>
{{ $t('system.menu.routerName') }}
@@ -44,7 +44,7 @@
<el-input v-model.trim="form.meta.routeName"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-if="form.type === menuTypeValue">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<el-form-item class="w100" prop="code">
<template #label>
{{ $t('system.menu.componentPath') }}
@@ -57,7 +57,7 @@
<el-input v-model.trim="form.meta.component"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-if="form.type === menuTypeValue">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<el-form-item class="w100" prop="isKeepAlive">
<template #label>
{{ $t('system.menu.isCache') }}
@@ -72,7 +72,7 @@
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-if="form.type === menuTypeValue">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<el-form-item class="w100">
<template #label>
{{ $t('system.menu.isHide') }}
@@ -87,14 +87,14 @@
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-if="form.type === menuTypeValue">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<el-form-item class="w100" prop="code" :label="$t('system.menu.tagIsDelete')">
<el-select v-model="form.meta.isAffix" class="w100">
<el-option v-for="item in trueFalseOption" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-if="form.type === menuTypeValue">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<el-form-item class="w100" prop="linkType">
<template #label>
{{ $t('system.menu.externalLink') }}
@@ -111,7 +111,7 @@
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-if="form.type === menuTypeValue && form.meta.linkType > 0">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue && form.meta.linkType > 0">
<el-form-item prop="code" :label="$t('system.menu.linkAddress')" class="w100">
<el-input v-model.trim="form.meta.link" :placeholder="$t('system.menu.linkPlaceholder')"></el-input>
</el-form-item>

View File

@@ -3,8 +3,8 @@ module mayfly-go
go 1.23.0
require (
gitee.com/chunanyong/dm v1.8.16
gitee.com/liuzongyang/libpq v1.0.9
gitee.com/chunanyong/dm v1.8.17
gitee.com/liuzongyang/libpq v1.10.11
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/emirpasic/gods v1.18.1
github.com/gin-gonic/gin v1.10.0
@@ -32,9 +32,9 @@ require (
github.com/tidwall/gjson v1.18.0
github.com/veops/go-ansiterm v0.0.5
go.mongodb.org/mongo-driver v1.16.0 // mongo
golang.org/x/crypto v0.29.0 // ssh
golang.org/x/oauth2 v0.23.0
golang.org/x/sync v0.9.0
golang.org/x/crypto v0.31.0 // ssh
golang.org/x/oauth2 v0.24.0
golang.org/x/sync v0.10.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
// gorm
@@ -93,8 +93,8 @@ require (
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect

View File

@@ -4,23 +4,19 @@ import (
"fmt"
"io/fs"
"mayfly-go/pkg/config"
"mayfly-go/pkg/ioc"
"mayfly-go/pkg/middleware"
"mayfly-go/pkg/req"
"mayfly-go/static"
"net/http"
"reflect"
"github.com/gin-gonic/gin"
)
// 初始化路由函数
type InitRouterFunc func(router *gin.RouterGroup)
var (
initRouterFuncs = make([]InitRouterFunc, 0)
)
// 添加初始化路由函数,由各个默认自行添加
func AddInitRouterFunc(initRouterFunc InitRouterFunc) {
initRouterFuncs = append(initRouterFuncs, initRouterFunc)
type RouterApi interface {
// ReqConfs 获取请求配置信息
ReqConfs() *req.Confs
}
func InitRouter() *gin.Engine {
@@ -46,11 +42,17 @@ func InitRouter() *gin.Engine {
// 设置路由组
api := router.Group(serverConfig.ContextPath + "/api")
// 调用所有模块注册的初始化路由函数
for _, initRouterFunc := range initRouterFuncs {
initRouterFunc(api)
// 获取所有实现了RouterApi接口的实例并注册对应路由
ras := ioc.GetBeansByType[RouterApi](reflect.TypeOf((*RouterApi)(nil)).Elem())
for _, ra := range ras {
confs := ra.ReqConfs()
if group := confs.Group; group != "" {
req.BatchSetGroup(api.Group(group), confs.Confs)
} else {
req.BatchSetGroup(api, confs.Confs)
}
}
initRouterFuncs = nil
return router
}

View File

@@ -6,15 +6,14 @@ import (
"mayfly-go/internal/auth/api/form"
"mayfly-go/internal/auth/config"
"mayfly-go/internal/auth/imsg"
msgapp "mayfly-go/internal/msg/application"
"mayfly-go/internal/auth/pkg/captcha"
"mayfly-go/internal/auth/pkg/otp"
sysapp "mayfly-go/internal/sys/application"
sysentity "mayfly-go/internal/sys/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/cache"
"mayfly-go/pkg/captcha"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/otp"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/cryptox"
@@ -24,8 +23,24 @@ import (
)
type AccountLogin struct {
AccountApp sysapp.Account `inject:""`
MsgApp msgapp.Msg `inject:""`
accountApp sysapp.Account `inject:"T"`
}
func (a *AccountLogin) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
// 用户账号密码登录
req.NewPost("/login", a.Login).Log(req.NewLogSaveI(imsg.LogAccountLogin)).DontNeedToken(),
req.NewGet("/refreshToken", a.RefreshToken).DontNeedToken(),
// 用户退出登录
req.NewPost("/logout", a.Logout),
// 用户otp双因素校验
req.NewPost("/otp-verify", a.OtpVerify).DontNeedToken(),
}
return req.NewConfs("/auth/accounts", reqs[:]...)
}
/** 用户账号密码登录 **/
@@ -51,7 +66,7 @@ func (a *AccountLogin) Login(rc *req.Ctx) {
biz.ErrIsNilAppendErr(err, "decryption password error: %s")
account := &sysentity.Account{Username: username}
err = a.AccountApp.GetByCond(model.NewModelCond(account).Columns("Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret"))
err = a.accountApp.GetByCond(model.NewModelCond(account).Columns("Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret"))
failCountKey := fmt.Sprintf("account:login:failcount:%s", username)
nowFailCount := cache.GetInt(failCountKey)
@@ -109,7 +124,7 @@ func (a *AccountLogin) OtpVerify(rc *req.Ctx) {
update := &sysentity.Account{OtpSecret: otpSecret}
update.Id = accountId
biz.ErrIsNil(update.OtpSecretEncrypt())
biz.ErrIsNil(a.AccountApp.Update(context.Background(), update))
biz.ErrIsNil(a.accountApp.Update(context.Background(), update))
}
la := &sysentity.Account{Username: otpInfo.Username}

View File

@@ -0,0 +1,10 @@
package api
import "mayfly-go/pkg/ioc"
func InitIoc() {
ioc.Register(new(AccountLogin))
ioc.Register(new(LdapLogin))
ioc.Register(new(Oauth2Login))
ioc.Register(new(Captcha))
}

View File

@@ -0,0 +1,25 @@
package api
import (
"mayfly-go/internal/auth/pkg/captcha"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
)
type Captcha struct {
}
func (c *Captcha) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
req.NewGet("", c.GenerateCaptcha).DontNeedToken(),
}
return req.NewConfs("/sys/captcha", reqs[:]...)
}
func (c *Captcha) GenerateCaptcha(rc *req.Ctx) {
id, image, err := captcha.Generate()
biz.ErrIsNilAppendErr(err, "failed to generate the CAPTCHA: %s")
rc.ResData = collx.M{"base64Captcha": image, "cid": id}
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"mayfly-go/internal/auth/config"
"mayfly-go/internal/auth/imsg"
"mayfly-go/internal/auth/pkg/otp"
msgapp "mayfly-go/internal/msg/application"
msgentity "mayfly-go/internal/msg/domain/entity"
sysapp "mayfly-go/internal/sys/application"
@@ -12,7 +13,6 @@ import (
"mayfly-go/pkg/biz"
"mayfly-go/pkg/cache"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/otp"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/jsonx"

View File

@@ -7,12 +7,11 @@ import (
"mayfly-go/internal/auth/api/form"
"mayfly-go/internal/auth/config"
"mayfly-go/internal/auth/imsg"
msgapp "mayfly-go/internal/msg/application"
"mayfly-go/internal/auth/pkg/captcha"
sysapp "mayfly-go/internal/sys/application"
sysentity "mayfly-go/internal/sys/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/cache"
"mayfly-go/pkg/captcha"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
@@ -28,8 +27,16 @@ import (
)
type LdapLogin struct {
AccountApp sysapp.Account `inject:""`
MsgApp msgapp.Msg `inject:""`
accountApp sysapp.Account `inject:"T"`
}
func (l *LdapLogin) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
req.NewGet("/enabled", l.GetLdapEnabled).DontNeedToken(),
req.NewPost("/login", l.Login).Log(req.NewLogSaveI(imsg.LogLdapLogin)).DontNeedToken(),
}
return req.NewConfs("/auth/ldap", reqs[:]...)
}
// @router /auth/ldap/enabled [get]
@@ -80,7 +87,7 @@ func (a *LdapLogin) Login(rc *req.Ctx) {
func (a *LdapLogin) getUser(userName string, cols ...string) (*sysentity.Account, error) {
account := &sysentity.Account{Username: userName}
if err := a.AccountApp.GetByCond(model.NewModelCond(account).Columns(cols...)); err != nil {
if err := a.accountApp.GetByCond(model.NewModelCond(account).Columns(cols...)); err != nil {
return nil, err
}
return account, nil
@@ -90,10 +97,10 @@ func (a *LdapLogin) createUser(userName, displayName string) {
account := &sysentity.Account{Username: userName}
account.FillBaseInfo(model.IdGenTypeNone, nil)
account.Name = displayName
biz.ErrIsNil(a.AccountApp.Create(context.TODO(), account))
biz.ErrIsNil(a.accountApp.Create(context.TODO(), account))
// 将 LADP 用户本地密码设置为空,不允许本地登录
account.Password = cryptox.PwdHash("")
biz.ErrIsNil(a.AccountApp.Update(context.TODO(), account))
biz.ErrIsNil(a.accountApp.Update(context.TODO(), account))
}
func (a *LdapLogin) getOrCreateUserWithLdap(ctx context.Context, userName string, password string, cols ...string) (*sysentity.Account, error) {

View File

@@ -9,7 +9,6 @@ import (
"mayfly-go/internal/auth/config"
"mayfly-go/internal/auth/domain/entity"
"mayfly-go/internal/auth/imsg"
msgapp "mayfly-go/internal/msg/application"
sysapp "mayfly-go/internal/sys/application"
sysentity "mayfly-go/internal/sys/domain/entity"
"mayfly-go/pkg/biz"
@@ -29,9 +28,28 @@ import (
)
type Oauth2Login struct {
Oauth2App application.Oauth2 `inject:""`
AccountApp sysapp.Account `inject:""`
MsgApp msgapp.Msg `inject:""`
oauth2App application.Oauth2 `inject:"T"`
accountApp sysapp.Account `inject:"T"`
}
func (o *Oauth2Login) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
req.NewGet("/config", o.Oauth2Config).DontNeedToken(),
// oauth2登录
req.NewGet("/login", o.OAuth2Login).DontNeedToken(),
req.NewGet("/bind", o.OAuth2Bind),
// oauth2回调地址
req.NewGet("/callback", o.OAuth2Callback).Log(req.NewLogSaveI(imsg.LogOauth2Callback)).DontNeedToken(),
req.NewGet("/status", o.Oauth2Status),
req.NewGet("/unbind", o.Oauth2Unbind).Log(req.NewLogSaveI(imsg.LogOauth2Unbind)),
}
return req.NewConfs("/auth/oauth2", reqs[:]...)
}
func (a *Oauth2Login) OAuth2Login(rc *req.Ctx) {
@@ -100,22 +118,22 @@ func (a *Oauth2Login) OAuth2Callback(rc *req.Ctx) {
account := new(sysentity.Account)
account.Id = accountId
err = a.AccountApp.GetByCond(model.NewModelCond(account).Columns("username"))
err = a.accountApp.GetByCond(model.NewModelCond(account).Columns("username"))
biz.ErrIsNilAppendErr(err, "this account does not exist")
rc.ReqParam = collx.Kvs("username", account.Username, "type", "bind")
err = a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
err = a.oauth2App.GetOAuthAccount(&entity.Oauth2Account{
AccountId: accountId,
}, "account_id", "identity")
biz.IsTrue(err != nil, "the account has been linked by another user")
err = a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
err = a.oauth2App.GetOAuthAccount(&entity.Oauth2Account{
Identity: userId,
}, "account_id", "identity")
biz.IsTrue(err != nil, "you are bound to another account")
now := time.Now()
err = a.Oauth2App.BindOAuthAccount(&entity.Oauth2Account{
err = a.oauth2App.BindOAuthAccount(&entity.Oauth2Account{
AccountId: accountId,
Identity: userId,
CreateTime: &now,
@@ -136,7 +154,7 @@ func (a *Oauth2Login) OAuth2Callback(rc *req.Ctx) {
func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *config.Oauth2Login) {
// 查询用户是否存在
oauthAccount := &entity.Oauth2Account{Identity: userId}
err := a.Oauth2App.GetOAuthAccount(oauthAccount, "account_id", "identity")
err := a.oauth2App.GetOAuthAccount(oauthAccount, "account_id", "identity")
ctx := rc.MetaCtx
var accountId uint64
isFirst := false
@@ -156,9 +174,9 @@ func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *config.Oa
Name: userId,
Username: userId,
}
biz.ErrIsNil(a.AccountApp.Create(context.TODO(), account))
biz.ErrIsNil(a.accountApp.Create(context.TODO(), account))
// 绑定
err := a.Oauth2App.BindOAuthAccount(&entity.Oauth2Account{
err := a.oauth2App.BindOAuthAccount(&entity.Oauth2Account{
AccountId: account.Id,
Identity: oauthAccount.Identity,
CreateTime: &now,
@@ -172,7 +190,7 @@ func (a *Oauth2Login) doLoginAction(rc *req.Ctx, userId string, oauth *config.Oa
}
// 进行登录
account, err := a.AccountApp.GetById(accountId, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret")
account, err := a.accountApp.GetById(accountId, "Id", "Name", "Username", "Password", "Status", "LastLoginTime", "LastLoginIp", "OtpSecret")
biz.ErrIsNilAppendErr(err, "get user info error: %s")
clientIp := getIpAndRegion(rc)
@@ -207,7 +225,7 @@ func (a *Oauth2Login) Oauth2Status(ctx *req.Ctx) {
oauth2LoginConfig := config.GetOauth2Login()
res.Enable = oauth2LoginConfig.Enable
if res.Enable {
err := a.Oauth2App.GetOAuthAccount(&entity.Oauth2Account{
err := a.oauth2App.GetOAuthAccount(&entity.Oauth2Account{
AccountId: ctx.GetLoginAccount().Id,
}, "account_id", "identity")
res.Bind = err == nil
@@ -217,7 +235,7 @@ func (a *Oauth2Login) Oauth2Status(ctx *req.Ctx) {
}
func (a *Oauth2Login) Oauth2Unbind(rc *req.Ctx) {
a.Oauth2App.Unbind(rc.GetLoginAccount().Id)
a.oauth2App.Unbind(rc.GetLoginAccount().Id)
}
// 获取oauth2登录配置信息因为有些字段是敏感字段故单独使用接口获取

View File

@@ -16,20 +16,20 @@ type Oauth2 interface {
}
type oauth2AppImpl struct {
Oauth2AccountRepo repository.Oauth2Account `inject:""`
oauth2AccountRepo repository.Oauth2Account `inject:"T"`
}
func (a *oauth2AppImpl) GetOAuthAccount(condition *entity.Oauth2Account, cols ...string) error {
return a.Oauth2AccountRepo.GetByCond(model.NewModelCond(condition).Columns(cols...))
return a.oauth2AccountRepo.GetByCond(model.NewModelCond(condition).Columns(cols...))
}
func (a *oauth2AppImpl) BindOAuthAccount(e *entity.Oauth2Account) error {
if e.Id == 0 {
return a.Oauth2AccountRepo.Insert(context.Background(), e)
return a.oauth2AccountRepo.Insert(context.Background(), e)
}
return a.Oauth2AccountRepo.UpdateById(context.Background(), e)
return a.oauth2AccountRepo.UpdateById(context.Background(), e)
}
func (a *oauth2AppImpl) Unbind(accountId uint64) {
a.Oauth2AccountRepo.DeleteByCond(context.Background(), &entity.Oauth2Account{AccountId: accountId})
a.oauth2AccountRepo.DeleteByCond(context.Background(), &entity.Oauth2Account{AccountId: accountId})
}

View File

@@ -11,5 +11,5 @@ type oauth2AccountRepoImpl struct {
}
func newAuthAccountRepo() repository.Oauth2Account {
return &oauth2AccountRepoImpl{base.RepoImpl[*entity.Oauth2Account]{M: new(entity.Oauth2Account)}}
return &oauth2AccountRepoImpl{}
}

View File

@@ -2,15 +2,15 @@ package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/auth/api"
"mayfly-go/internal/auth/application"
"mayfly-go/internal/auth/infrastructure/persistence"
"mayfly-go/internal/auth/router"
)
func init() {
initialize.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()
})
initialize.AddInitRouterFunc(router.Init)
}

View File

@@ -1,60 +0,0 @@
package router
import (
"mayfly-go/internal/auth/api"
"mayfly-go/internal/auth/imsg"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ioc"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
func Init(router *gin.RouterGroup) {
accountLogin := new(api.AccountLogin)
biz.ErrIsNil(ioc.Inject(accountLogin))
ldapLogin := new(api.LdapLogin)
biz.ErrIsNil(ioc.Inject(ldapLogin))
oauth2Login := new(api.Oauth2Login)
biz.ErrIsNil(ioc.Inject(oauth2Login))
rg := router.Group("/auth")
reqs := [...]*req.Conf{
// 用户账号密码登录
req.NewPost("/accounts/login", accountLogin.Login).Log(req.NewLogSaveI(imsg.LogAccountLogin)).DontNeedToken(),
req.NewGet("/accounts/refreshToken", accountLogin.RefreshToken).DontNeedToken(),
// 用户退出登录
req.NewPost("/accounts/logout", accountLogin.Logout),
// 用户otp双因素校验
req.NewPost("/accounts/otp-verify", accountLogin.OtpVerify).DontNeedToken(),
/*--------oauth2登录相关----------*/
req.NewGet("/oauth2-config", oauth2Login.Oauth2Config).DontNeedToken(),
// oauth2登录
req.NewGet("/oauth2/login", oauth2Login.OAuth2Login).DontNeedToken(),
req.NewGet("/oauth2/bind", oauth2Login.OAuth2Bind),
// oauth2回调地址
req.NewGet("/oauth2/callback", oauth2Login.OAuth2Callback).Log(req.NewLogSaveI(imsg.LogOauth2Callback)).DontNeedToken(),
req.NewGet("/oauth2/status", oauth2Login.Oauth2Status),
req.NewGet("/oauth2/unbind", oauth2Login.Oauth2Unbind).Log(req.NewLogSaveI(imsg.LogOauth2Unbind)),
// LDAP 登录
req.NewGet("/ldap/enabled", ldapLogin.GetLdapEnabled).DontNeedToken(),
req.NewPost("/ldap/login", ldapLogin.Login).Log(req.NewLogSaveI(imsg.LogLdapLogin)).DontNeedToken(),
}
req.BatchSetGroup(rg, reqs[:])
}

View File

@@ -0,0 +1,7 @@
package api
import "mayfly-go/pkg/ioc"
func InitIoc() {
ioc.Register(new(Common))
}

View File

@@ -9,8 +9,17 @@ import (
type Common struct {
}
func (c *Common) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
// 获取公钥
req.NewGet("/public-key", c.RasPublicKey).DontNeedToken(),
}
return req.NewConfs("/common", reqs[:]...)
}
func (i *Common) RasPublicKey(rc *req.Ctx) {
publicKeyStr, err := cryptox.GetRsaPublicKey()
biz.ErrIsNilAppendErr(err, "rsa生成公私钥失败")
biz.ErrIsNilAppendErr(err, "rsa - failed to genenrate public key")
rc.ResData = publicKeyStr
}

View File

@@ -16,10 +16,10 @@ const (
// RedisConnExpireTime = 2 * time.Minute
// MongoConnExpireTime = 2 * time.Minute
ResourceTypeMachine int8 = 1
ResourceTypeDb int8 = 2
ResourceTypeRedis int8 = 3
ResourceTypeMongo int8 = 4
ResourceTypeMachine int8 = 1
ResourceTypeDbInstance int8 = 2
ResourceTypeRedis int8 = 3
ResourceTypeMongo int8 = 4
// imsg起始编号
ImsgNumSys = 10000

View File

@@ -2,9 +2,11 @@ package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/common/router"
"mayfly-go/internal/common/api"
)
func init() {
initialize.AddInitRouterFunc(router.Init)
initialize.AddInitIocFunc(func() {
api.InitIoc()
})
}

View File

@@ -1,17 +0,0 @@
package router
import (
"mayfly-go/internal/common/api"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
func InitCommonRouter(router *gin.RouterGroup) {
common := router.Group("common")
c := &api.Common{}
{
// 获取公钥
req.NewGet("public-key", c.RasPublicKey).DontNeedToken().Group(common)
}
}

View File

@@ -1,7 +0,0 @@
package router
import "github.com/gin-gonic/gin"
func Init(router *gin.RouterGroup) {
InitCommonRouter(router)
}

View File

@@ -0,0 +1,13 @@
package api
import "mayfly-go/pkg/ioc"
func InitIoc() {
ioc.Register(new(Dashbord))
ioc.Register(new(Db))
ioc.Register(new(Instance))
ioc.Register(new(DbSqlExec))
ioc.Register(new(DbSql))
ioc.Register(new(DataSyncTask))
ioc.Register(new(DbTransferTask))
}

View File

@@ -1,7 +1,6 @@
package api
import (
"mayfly-go/internal/db/application"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/req"
@@ -9,14 +8,21 @@ import (
)
type Dashbord struct {
TagTreeApp tagapp.TagTree `inject:""`
DbApp application.Db `inject:""`
tagTreeApp tagapp.TagTree `inject:"T"`
}
func (d *Dashbord) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
req.NewGet("dashbord", d.Dashbord),
}
return req.NewConfs("/dbs", reqs[:]...)
}
func (m *Dashbord) Dashbord(rc *req.Ctx) {
accountId := rc.GetLoginAccount().Id
tagCodePaths := m.TagTreeApp.GetAccountTagCodePaths(accountId, tagentity.TagTypeDbName, "")
tagCodePaths := m.tagTreeApp.GetAccountTags(accountId, &tagentity.TagTreeQuery{Types: collx.AsArray(tagentity.TagTypeDb)}).GetCodePaths()
rc.ResData = collx.M{
"dbNum": len(tagCodePaths),

View File

@@ -9,7 +9,6 @@ import (
"mayfly-go/internal/db/application/dto"
"mayfly-go/internal/db/config"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/dbm/sqlparser"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/imsg"
"mayfly-go/internal/event"
@@ -26,9 +25,7 @@ import (
"mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/cryptox"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/utils/writerx"
"mayfly-go/pkg/ws"
"strings"
"time"
@@ -36,11 +33,46 @@ import (
)
type Db struct {
InstanceApp application.Instance `inject:"DbInstanceApp"`
DbApp application.Db `inject:""`
DbSqlExecApp application.DbSqlExec `inject:""`
MsgApp msgapp.Msg `inject:""`
TagApp tagapp.TagTree `inject:"TagTreeApp"`
instanceApp application.Instance `inject:"T"`
dbApp application.Db `inject:"T"`
dbSqlExecApp application.DbSqlExec `inject:"T"`
msgApp msgapp.Msg `inject:"T"`
tagApp tagapp.TagTree `inject:"T"`
}
func (d *Db) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
// 获取数据库列表
req.NewGet("", d.Dbs),
req.NewPost("", d.Save).Log(req.NewLogSaveI(imsg.LogDbSave)),
req.NewDelete(":dbId", d.DeleteDb).Log(req.NewLogSaveI(imsg.LogDbDelete)),
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.NewLogI(imsg.LogDbRunSql)),
req.NewPost(":dbId/exec-sql-file", d.ExecSqlFile).Log(req.NewLogSaveI(imsg.LogDbRunSqlFile)).RequiredPermissionCode("db:sqlscript:run"),
req.NewGet(":dbId/dump", d.DumpSql).Log(req.NewLogSaveI(imsg.LogDbDump)).NoRes(),
req.NewGet(":dbId/t-infos", d.TableInfos),
req.NewGet(":dbId/t-index", d.TableIndex),
req.NewGet(":dbId/c-metadata", d.ColumnMA),
req.NewGet(":dbId/hint-tables", d.HintTables),
req.NewPost(":dbId/copy-table", d.CopyTable),
}
return req.NewConfs("/dbs", reqs[:]...)
}
// @router /api/dbs [get]
@@ -48,18 +80,21 @@ func (d *Db) Dbs(rc *req.Ctx) {
queryCond, page := req.BindQueryAndPage[*entity.DbQuery](rc, new(entity.DbQuery))
// 不存在可访问标签id即没有可操作数据
codes := d.TagApp.GetAccountTagCodes(rc.GetLoginAccount().Id, int8(tagentity.TagTypeDbName), queryCond.TagPath)
if len(codes) == 0 {
tags := d.tagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeDbInstance, tagentity.TagTypeAuthCert, tagentity.TagTypeDb)),
CodePathLikes: collx.AsArray(queryCond.TagPath),
})
if len(tags) == 0 {
rc.ResData = model.EmptyPageResult[any]()
return
}
queryCond.Codes = codes
queryCond.Codes = tags.GetCodes()
var dbvos []*vo.DbListVO
res, err := d.DbApp.GetPageList(queryCond, page, &dbvos)
res, err := d.dbApp.GetPageList(queryCond, page, &dbvos)
biz.ErrIsNil(err)
instances, _ := d.InstanceApp.GetByIds(collx.ArrayMap(dbvos, func(i *vo.DbListVO) uint64 {
instances, _ := d.instanceApp.GetByIds(collx.ArrayMap(dbvos, func(i *vo.DbListVO) uint64 {
return i.InstanceId
}))
instancesMap := collx.ArrayToMap(instances, func(i *entity.DbInstance) uint64 {
@@ -84,7 +119,7 @@ func (d *Db) Save(rc *req.Ctx) {
rc.ReqParam = form
biz.ErrIsNil(d.DbApp.SaveDb(rc.MetaCtx, db))
biz.ErrIsNil(d.dbApp.SaveDb(rc.MetaCtx, db))
}
func (d *Db) DeleteDb(rc *req.Ctx) {
@@ -94,7 +129,7 @@ func (d *Db) DeleteDb(rc *req.Ctx) {
ctx := rc.MetaCtx
for _, v := range ids {
biz.ErrIsNil(d.DbApp.Delete(ctx, cast.ToUint64(v)))
biz.ErrIsNil(d.dbApp.Delete(ctx, cast.ToUint64(v)))
}
}
@@ -104,9 +139,9 @@ func (d *Db) ExecSql(rc *req.Ctx) {
form := req.BindJsonAndValid(rc, new(form.DbSqlExecForm))
dbId := getDbId(rc)
dbConn, err := d.DbApp.GetDbConn(dbId, form.Db)
dbConn, err := d.dbApp.GetDbConn(dbId, form.Db)
biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.CodePath...), "%s")
biz.ErrIsNilAppendErr(d.tagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.CodePath...), "%s")
global.EventBus.Publish(rc.MetaCtx, event.EventTopicResourceOp, dbConn.Info.CodePath[0])
sqlStr, err := cryptox.AesDecryptByLa(form.Sql, rc.GetLoginAccount())
@@ -115,7 +150,7 @@ func (d *Db) ExecSql(rc *req.Ctx) {
rc.ReqParam = fmt.Sprintf("%s %s\n-> %s", dbConn.Info.GetLogDesc(), form.ExecId, sqlStr)
biz.NotEmpty(form.Sql, "sql cannot be empty")
execReq := &application.DbSqlExecReq{
execReq := &dto.DbSqlExecReq{
DbId: dbId,
Db: form.Db,
Remark: form.Remark,
@@ -127,7 +162,7 @@ func (d *Db) ExecSql(rc *req.Ctx) {
ctx, cancel := context.WithTimeout(rc.MetaCtx, time.Duration(config.GetDbms().SqlExecTl)*time.Second)
defer cancel()
execRes, err := d.DbSqlExecApp.Exec(ctx, execReq)
execRes, err := d.dbSqlExecApp.Exec(ctx, execReq)
biz.ErrIsNil(err)
rc.ResData = execRes
}
@@ -155,52 +190,17 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
dbName := getDbName(rc)
clientId := rc.Query("clientId")
dbConn, err := d.DbApp.GetDbConn(dbId, dbName)
dbConn, err := d.dbApp.GetDbConn(dbId, dbName)
biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.CodePath...), "%s")
biz.ErrIsNilAppendErr(d.tagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.CodePath...), "%s")
rc.ReqParam = fmt.Sprintf("filename: %s -> %s", filename, dbConn.Info.GetLogDesc())
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(i18n.T(imsg.SqlScriptRunFail), fmt.Sprintf("[%s][%s] execution failure: [%s]", filename, dbConn.Info.GetLogDesc(), errInfo)).WithClientId(clientId))
}
}()
executedStatements := 0
progressId := stringx.Rand(32)
laId := rc.GetLoginAccount().Id
defer ws.SendJsonMsg(ws.UserId(laId), clientId, msgdto.InfoSysMsg(i18n.T(imsg.SqlScripRunProgress), &progressMsg{
Id: progressId,
Title: filename,
ExecutedStatements: executedStatements,
Terminated: true,
}).WithCategory(progressCategory))
ticker := time.NewTicker(time.Second * 1)
defer ticker.Stop()
err = sqlparser.SQLSplit(file, func(sql string) error {
select {
case <-ticker.C:
ws.SendJsonMsg(ws.UserId(laId), clientId, msgdto.InfoSysMsg(i18n.T(imsg.SqlScripRunProgress), &progressMsg{
Id: progressId,
Title: filename,
ExecutedStatements: executedStatements,
Terminated: false,
}).WithCategory(progressCategory))
default:
}
executedStatements++
_, err = dbConn.Exec(sql)
return err
})
biz.ErrIsNilAppendErr(err, "%s")
d.MsgApp.CreateAndSend(rc.GetLoginAccount(), msgdto.SuccessSysMsg(i18n.T(imsg.SqlScriptRunSuccess), fmt.Sprintf("sql脚本执行完成%s", rc.ReqParam)).WithClientId(clientId))
biz.ErrIsNil(d.dbSqlExecApp.ExecReader(rc.MetaCtx, &dto.SqlReaderExec{
Reader: file,
Filename: filename,
DbConn: dbConn,
ClientId: clientId,
}))
}
// 数据库dump
@@ -223,9 +223,9 @@ func (d *Db) DumpSql(rc *req.Ctx) {
needData := dumpType == "2" || dumpType == "3"
la := rc.GetLoginAccount()
dbConn, err := d.DbApp.GetDbConn(dbId, dbName)
dbConn, err := d.dbApp.GetDbConn(dbId, dbName)
biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(la.Id, dbConn.Info.CodePath...), "%s")
biz.ErrIsNilAppendErr(d.tagApp.CanAccess(la.Id, dbConn.Info.CodePath...), "%s")
now := time.Now()
filename := fmt.Sprintf("%s-%s.%s.sql%s", dbConn.Info.Name, dbName, now.Format("20060102150405"), extName)
@@ -245,11 +245,11 @@ func (d *Db) DumpSql(rc *req.Ctx) {
if len(msg) > 0 {
msg = "DB dump error: " + msg
rc.GetWriter().Write([]byte(msg))
d.MsgApp.CreateAndSend(la, msgdto.ErrSysMsg(i18n.T(imsg.DbDumpErr), msg))
d.msgApp.CreateAndSend(la, msgdto.ErrSysMsg(i18n.T(imsg.DbDumpErr), msg))
}
}()
biz.ErrIsNil(d.DbApp.DumpDb(rc.MetaCtx, &dto.DumpDb{
biz.ErrIsNil(d.dbApp.DumpDb(rc.MetaCtx, &dto.DumpDb{
DbId: dbId,
DbName: dbName,
Tables: tables,
@@ -351,7 +351,7 @@ func (d *Db) CopyTable(rc *req.Ctx) {
form := &form.DbCopyTableForm{}
copy := req.BindJsonAndCopyTo[*dbi.DbCopyTable](rc, form, new(dbi.DbCopyTable))
conn, err := d.DbApp.GetDbConn(form.Id, form.Db)
conn, err := d.dbApp.GetDbConn(form.Id, form.Db)
biz.ErrIsNilAppendErr(err, "copy table error: %s")
err = conn.GetDialect().CopyTable(copy)
@@ -374,7 +374,7 @@ func getDbName(rc *req.Ctx) string {
}
func (d *Db) getDbConn(rc *req.Ctx) *dbi.DbConn {
dc, err := d.DbApp.GetDbConn(getDbId(rc), getDbName(rc))
dc, err := d.dbApp.GetDbConn(getDbId(rc), getDbName(rc))
biz.ErrIsNil(err)
return dc
}

View File

@@ -5,6 +5,7 @@ import (
"mayfly-go/internal/db/api/vo"
"mayfly-go/internal/db/application"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/imsg"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/cryptox"
@@ -15,19 +16,48 @@ import (
)
type DataSyncTask struct {
DataSyncTaskApp application.DataSyncTask `inject:"DbDataSyncTaskApp"`
dataSyncTaskApp application.DataSyncTask `inject:"T"`
}
func (d *DataSyncTask) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
// 获取任务列表 /datasync
req.NewGet("", d.Tasks),
req.NewGet(":taskId/logs", d.Logs).RequiredPermissionCode("db:sync:log"),
// 保存任务 /datasync/save
req.NewPost("save", d.SaveTask).Log(req.NewLogSaveI(imsg.LogDataSyncSave)).RequiredPermissionCode("db:sync:save"),
// 获取单个详情 /datasync/:taskId
req.NewGet(":taskId", d.GetTask),
// 删除任务 /datasync/:taskId/del
req.NewDelete(":taskId/del", d.DeleteTask).Log(req.NewLogSaveI(imsg.LogDataSyncDelete)).RequiredPermissionCode("db:sync:del"),
// 启停用任务 /datasync/status
req.NewPost(":taskId/status", d.ChangeStatus).Log(req.NewLogSaveI(imsg.LogDataSyncChangeStatus)).RequiredPermissionCode("db:sync:status"),
// 立即执行任务 /datasync/run
req.NewPost(":taskId/run", d.Run),
// 停止正在执行中的任务
req.NewPost(":taskId/stop", d.Stop),
}
return req.NewConfs("/datasync/tasks", reqs[:]...)
}
func (d *DataSyncTask) Tasks(rc *req.Ctx) {
queryCond, page := req.BindQueryAndPage[*entity.DataSyncTaskQuery](rc, new(entity.DataSyncTaskQuery))
res, err := d.DataSyncTaskApp.GetPageList(queryCond, page, new([]vo.DataSyncTaskListVO))
res, err := d.dataSyncTaskApp.GetPageList(queryCond, page, new([]vo.DataSyncTaskListVO))
biz.ErrIsNil(err)
rc.ResData = res
}
func (d *DataSyncTask) Logs(rc *req.Ctx) {
queryCond, page := req.BindQueryAndPage[*entity.DataSyncLogQuery](rc, new(entity.DataSyncLogQuery))
res, err := d.DataSyncTaskApp.GetTaskLogList(queryCond, page, new([]vo.DataSyncLogListVO))
res, err := d.dataSyncTaskApp.GetTaskLogList(queryCond, page, new([]vo.DataSyncLogListVO))
biz.ErrIsNil(err)
rc.ResData = res
}
@@ -44,7 +74,7 @@ func (d *DataSyncTask) SaveTask(rc *req.Ctx) {
form.DataSql = sql
rc.ReqParam = form
biz.ErrIsNil(d.DataSyncTaskApp.Save(rc.MetaCtx, task))
biz.ErrIsNil(d.dataSyncTaskApp.Save(rc.MetaCtx, task))
}
func (d *DataSyncTask) DeleteTask(rc *req.Ctx) {
@@ -53,21 +83,21 @@ func (d *DataSyncTask) DeleteTask(rc *req.Ctx) {
ids := strings.Split(taskId, ",")
for _, v := range ids {
biz.ErrIsNil(d.DataSyncTaskApp.Delete(rc.MetaCtx, cast.ToUint64(v)))
biz.ErrIsNil(d.dataSyncTaskApp.Delete(rc.MetaCtx, cast.ToUint64(v)))
}
}
func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
form := &form.DataSyncTaskStatusForm{}
task := req.BindJsonAndCopyTo[*entity.DataSyncTask](rc, form, new(entity.DataSyncTask))
_ = d.DataSyncTaskApp.UpdateById(rc.MetaCtx, task)
_ = d.dataSyncTaskApp.UpdateById(rc.MetaCtx, task)
if task.Status == entity.DataSyncTaskStatusEnable {
task, err := d.DataSyncTaskApp.GetById(task.Id)
task, err := d.dataSyncTaskApp.GetById(task.Id)
biz.ErrIsNil(err, "task not found")
d.DataSyncTaskApp.AddCronJob(rc.MetaCtx, task)
d.dataSyncTaskApp.AddCronJob(rc.MetaCtx, task)
} else {
d.DataSyncTaskApp.RemoveCronJobById(task.Id)
d.dataSyncTaskApp.RemoveCronJobById(task.Id)
}
// 记录请求日志
rc.ReqParam = form
@@ -76,7 +106,7 @@ func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
func (d *DataSyncTask) Run(rc *req.Ctx) {
taskId := d.getTaskId(rc)
rc.ReqParam = taskId
_ = d.DataSyncTaskApp.RunCronJob(rc.MetaCtx, taskId)
_ = d.dataSyncTaskApp.RunCronJob(rc.MetaCtx, taskId)
}
func (d *DataSyncTask) Stop(rc *req.Ctx) {
@@ -86,12 +116,12 @@ func (d *DataSyncTask) Stop(rc *req.Ctx) {
task := new(entity.DataSyncTask)
task.Id = taskId
task.RunningState = entity.DataSyncTaskRunStateStop
_ = d.DataSyncTaskApp.UpdateById(rc.MetaCtx, task)
_ = d.dataSyncTaskApp.UpdateById(rc.MetaCtx, task)
}
func (d *DataSyncTask) GetTask(rc *req.Ctx) {
taskId := d.getTaskId(rc)
dbEntity, _ := d.DataSyncTaskApp.GetById(taskId)
dbEntity, _ := d.dataSyncTaskApp.GetById(taskId)
rc.ResData = dbEntity
}

View File

@@ -7,6 +7,7 @@ import (
"mayfly-go/internal/db/application"
"mayfly-go/internal/db/application/dto"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/imsg"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
@@ -20,10 +21,35 @@ import (
)
type Instance struct {
InstanceApp application.Instance `inject:"DbInstanceApp"`
DbApp application.Db `inject:""`
ResourceAuthCertApp tagapp.ResourceAuthCert `inject:""`
TagApp tagapp.TagTree `inject:"TagTreeApp"`
instanceApp application.Instance `inject:"T"`
dbApp application.Db `inject:"T"`
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"T"`
tagApp tagapp.TagTree `inject:"T"`
}
func (d *Instance) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
// 获取数据库列表
req.NewGet("", d.Instances),
req.NewPost("/test-conn", d.TestConn),
req.NewPost("", d.SaveInstance).Log(req.NewLogSaveI(imsg.LogDbInstSave)),
req.NewGet(":instanceId", d.GetInstance),
// 获取数据库实例的所有数据库名
req.NewPost("/databases", d.GetDatabaseNames),
// 根据授权凭证名获取其所有库名
req.NewGet("/databases/:ac", d.GetDatabaseNamesByAc),
req.NewGet(":instanceId/server-info", d.GetDbServer),
req.NewDelete(":instanceId", d.DeleteInstance).Log(req.NewLogSaveI(imsg.LogDbInstDelete)),
}
return req.NewConfs("/instances", reqs[:]...)
}
// Instances 获取数据库实例信息
@@ -31,27 +57,31 @@ type Instance struct {
func (d *Instance) Instances(rc *req.Ctx) {
queryCond, page := req.BindQueryAndPage[*entity.InstanceQuery](rc, new(entity.InstanceQuery))
tagCodePaths := d.TagApp.GetAccountTagCodePaths(rc.GetLoginAccount().Id, tagentity.TagTypeDbAuthCert, queryCond.TagPath)
tags := d.tagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeDbInstance, tagentity.TagTypeAuthCert)),
CodePathLikes: collx.AsArray(queryCond.TagPath),
})
// 不存在可操作的数据库,即没有可操作数据
if len(tagCodePaths) == 0 {
if len(tags) == 0 {
rc.ResData = model.EmptyPageResult[any]()
return
}
dbInstCodes := tagentity.GetCodeByPath(tagentity.TagTypeDb, tagCodePaths...)
tagCodePaths := tags.GetCodePaths()
dbInstCodes := tagentity.GetCodesByCodePaths(tagentity.TagTypeDbInstance, tagCodePaths...)
queryCond.Codes = dbInstCodes
var instvos []*vo.InstanceListVO
res, err := d.InstanceApp.GetPageList(queryCond, page, &instvos)
res, err := d.instanceApp.GetPageList(queryCond, page, &instvos)
biz.ErrIsNil(err)
// 填充授权凭证信息
d.ResourceAuthCertApp.FillAuthCertByAcNames(tagentity.GetCodeByPath(tagentity.TagTypeDbAuthCert, tagCodePaths...), collx.ArrayMap(instvos, func(vos *vo.InstanceListVO) tagentity.IAuthCert {
d.resourceAuthCertApp.FillAuthCertByAcNames(tagentity.GetCodesByCodePaths(tagentity.TagTypeAuthCert, tagCodePaths...), collx.ArrayMap(instvos, func(vos *vo.InstanceListVO) tagentity.IAuthCert {
return vos
})...)
// 填充标签信息
d.TagApp.FillTagInfo(tagentity.TagType(consts.ResourceTypeDb), collx.ArrayMap(instvos, func(insvo *vo.InstanceListVO) tagentity.ITagResource {
d.tagApp.FillTagInfo(tagentity.TagType(consts.ResourceTypeDbInstance), collx.ArrayMap(instvos, func(insvo *vo.InstanceListVO) tagentity.ITagResource {
return insvo
})...)
@@ -62,7 +92,7 @@ func (d *Instance) TestConn(rc *req.Ctx) {
form := &form.InstanceForm{}
instance := req.BindJsonAndCopyTo[*entity.DbInstance](rc, form, new(entity.DbInstance))
biz.ErrIsNil(d.InstanceApp.TestConn(instance, form.AuthCerts[0]))
biz.ErrIsNil(d.instanceApp.TestConn(instance, form.AuthCerts[0]))
}
// SaveInstance 保存数据库实例信息
@@ -72,7 +102,7 @@ func (d *Instance) SaveInstance(rc *req.Ctx) {
instance := req.BindJsonAndCopyTo[*entity.DbInstance](rc, form, new(entity.DbInstance))
rc.ReqParam = form
id, err := d.InstanceApp.SaveDbInstance(rc.MetaCtx, &dto.SaveDbInstance{
id, err := d.instanceApp.SaveDbInstance(rc.MetaCtx, &dto.SaveDbInstance{
DbInstance: instance,
AuthCerts: form.AuthCerts,
TagCodePaths: form.TagCodePaths,
@@ -85,7 +115,7 @@ func (d *Instance) SaveInstance(rc *req.Ctx) {
// @router /api/instances/:instance [GET]
func (d *Instance) GetInstance(rc *req.Ctx) {
dbId := getInstanceId(rc)
dbEntity, err := d.InstanceApp.GetById(dbId)
dbEntity, err := d.instanceApp.GetById(dbId)
biz.ErrIsNilAppendErr(err, "get db instance failed: %s")
rc.ResData = dbEntity
}
@@ -98,7 +128,7 @@ func (d *Instance) DeleteInstance(rc *req.Ctx) {
ids := strings.Split(idsStr, ",")
for _, v := range ids {
biz.ErrIsNilAppendErr(d.InstanceApp.Delete(rc.MetaCtx, cast.ToUint64(v)), "delete db instance failed: %s")
biz.ErrIsNilAppendErr(d.instanceApp.Delete(rc.MetaCtx, cast.ToUint64(v)), "delete db instance failed: %s")
}
}
@@ -106,13 +136,13 @@ func (d *Instance) DeleteInstance(rc *req.Ctx) {
func (d *Instance) GetDatabaseNames(rc *req.Ctx) {
form := &form.InstanceDbNamesForm{}
instance := req.BindJsonAndCopyTo[*entity.DbInstance](rc, form, new(entity.DbInstance))
res, err := d.InstanceApp.GetDatabases(instance, form.AuthCert)
res, err := d.instanceApp.GetDatabases(instance, form.AuthCert)
biz.ErrIsNil(err)
rc.ResData = res
}
func (d *Instance) GetDatabaseNamesByAc(rc *req.Ctx) {
res, err := d.InstanceApp.GetDatabasesByAc(rc.PathParam("ac"))
res, err := d.instanceApp.GetDatabasesByAc(rc.PathParam("ac"))
biz.ErrIsNil(err)
rc.ResData = res
}
@@ -120,7 +150,7 @@ func (d *Instance) GetDatabaseNamesByAc(rc *req.Ctx) {
// 获取数据库实例server信息
func (d *Instance) GetDbServer(rc *req.Ctx) {
instanceId := getInstanceId(rc)
conn, err := d.DbApp.GetDbConnByInstanceId(instanceId)
conn, err := d.dbApp.GetDbConnByInstanceId(instanceId)
biz.ErrIsNil(err)
res, err := conn.GetMetadata().GetDbServer()
biz.ErrIsNil(err)

View File

@@ -10,7 +10,22 @@ import (
)
type DbSql struct {
DbSqlApp application.DbSql `inject:""`
dbSqlApp application.DbSql `inject:"T"`
}
func (d *DbSql) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
// 用户sql相关
req.NewPost(":dbId/sql", d.SaveSql),
req.NewGet(":dbId/sql", d.GetSql),
req.NewDelete(":dbId/sql", d.DeleteSql),
req.NewGet(":dbId/sql-names", d.GetSqlNames),
}
return req.NewConfs("/dbs", reqs[:]...)
}
// @router /api/db/:dbId/sql [post]
@@ -25,14 +40,14 @@ func (d *DbSql) SaveSql(rc *req.Ctx) {
// 获取用于是否有该dbsql的保存记录有则更改否则新增
dbSql := &entity.DbSql{Type: dbSqlForm.Type, DbId: dbId, Name: dbSqlForm.Name, Db: dbSqlForm.Db}
dbSql.CreatorId = account.Id
e := d.DbSqlApp.GetByCond(dbSql)
e := d.dbSqlApp.GetByCond(dbSql)
// 更新sql信息
dbSql.Sql = dbSqlForm.Sql
if e == nil {
d.DbSqlApp.UpdateById(rc.MetaCtx, dbSql)
d.dbSqlApp.UpdateById(rc.MetaCtx, dbSql)
} else {
d.DbSqlApp.Insert(rc.MetaCtx, dbSql)
d.dbSqlApp.Insert(rc.MetaCtx, dbSql)
}
}
@@ -43,7 +58,7 @@ func (d *DbSql) GetSqlNames(rc *req.Ctx) {
// 获取用于是否有该dbsql的保存记录有则更改否则新增
dbSql := &entity.DbSql{Type: 1, DbId: dbId, Db: dbName}
dbSql.CreatorId = rc.GetLoginAccount().Id
sqls, _ := d.DbSqlApp.ListByCond(model.NewModelCond(dbSql).Columns("id", "name"))
sqls, _ := d.dbSqlApp.ListByCond(model.NewModelCond(dbSql).Columns("id", "name"))
rc.ResData = sqls
}
@@ -55,7 +70,7 @@ func (d *DbSql) DeleteSql(rc *req.Ctx) {
dbSql.Name = rc.Query("name")
dbSql.Db = rc.Query("db")
biz.ErrIsNil(d.DbSqlApp.DeleteByCond(rc.MetaCtx, dbSql))
biz.ErrIsNil(d.dbSqlApp.DeleteByCond(rc.MetaCtx, dbSql))
}
// @router /api/db/:dbId/sql [get]
@@ -67,7 +82,7 @@ func (d *DbSql) GetSql(rc *req.Ctx) {
dbSql.CreatorId = rc.GetLoginAccount().Id
dbSql.Name = rc.Query("name")
e := d.DbSqlApp.GetByCond(dbSql)
e := d.dbSqlApp.GetByCond(dbSql)
if e != nil {
return
}

View File

@@ -13,7 +13,16 @@ import (
)
type DbSqlExec struct {
DbSqlExecApp application.DbSqlExec `inject:""`
dbSqlExecApp application.DbSqlExec `inject:"T"`
}
func (d *DbSqlExec) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
// 获取所有数据库sql执行记录列表
req.NewGet("/sql-execs", d.DbSqlExecs),
}
return req.NewConfs("/dbs", reqs[:]...)
}
func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) {
@@ -24,7 +33,7 @@ func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) {
return cast.ToInt8(val)
})
}
res, err := d.DbSqlExecApp.GetPageList(queryCond, page, new([]entity.DbSqlExec))
res, err := d.dbSqlExecApp.GetPageList(queryCond, page, new([]entity.DbSqlExec))
biz.ErrIsNil(err)
rc.ResData = res
}

View File

@@ -2,52 +2,73 @@ package api
import (
"context"
"fmt"
"mayfly-go/internal/db/api/form"
"mayfly-go/internal/db/api/vo"
"mayfly-go/internal/db/application"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/dbm/sqlparser"
"mayfly-go/internal/db/application/dto"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/imsg"
fileapp "mayfly-go/internal/file/application"
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/i18n"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/ws"
"strings"
"time"
"github.com/may-fly/cast"
)
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:""`
FileApp fileapp.File `inject:""`
dbTransferTask application.DbTransferTask `inject:"T"`
dbTransferFile application.DbTransferFile `inject:"T"`
dbApp application.Db `inject:"T"`
tagApp tagapp.TagTree `inject:"T"`
dbSqlExecApp application.DbSqlExec `inject:"T"`
fileApp fileapp.File `inject:"T"`
}
func (d *DbTransferTask) ReqConfs() *req.Confs {
reqs := [...]*req.Conf{
// 获取任务列表
req.NewGet("", d.Tasks),
// 保存任务
req.NewPost("save", d.SaveTask).Log(req.NewLogSaveI(imsg.LogDtsSave)).RequiredPermissionCode("db:transfer:save"),
// 删除任务
req.NewDelete(":taskId/del", d.DeleteTask).Log(req.NewLogSaveI(imsg.LogDtsDelete)).RequiredPermissionCode("db:transfer:del"),
// 启停用任务
req.NewPost(":taskId/status", d.ChangeStatus).Log(req.NewLogSaveI(imsg.LogDtsChangeStatus)).RequiredPermissionCode("db:transfer:status"),
// 立即执行任务
req.NewPost(":taskId/run", d.Run).Log(req.NewLogI(imsg.LogDtsRun)).RequiredPermissionCode("db:transfer:run"),
// 停止正在执行中的任务
req.NewPost(":taskId/stop", d.Stop).Log(req.NewLogSaveI(imsg.LogDtsStop)).RequiredPermissionCode("db:transfer:run"),
// 导出文件管理-列表
req.NewGet("/files/:taskId", d.Files),
// 导出文件管理-删除
req.NewPost("/files/del/:fileId", d.FileDel).Log(req.NewLogSaveI(imsg.LogDtsDeleteFile)).RequiredPermissionCode("db:transfer:files:del"),
req.NewPost("/files/run", d.FileRun).Log(req.NewLogSaveI(imsg.LogDtsRunSqlFile)).RequiredPermissionCode("db:transfer:files:run"),
}
return req.NewConfs("/dbTransfer", reqs[:]...)
}
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))
res, err := d.dbTransferTask.GetPageList(queryCond, page, new([]vo.DbTransferTaskListVO))
biz.ErrIsNil(err)
if res.List != nil {
list := res.List.(*[]vo.DbTransferTaskListVO)
for _, item := range *list {
item.RunningState = entity.DbTransferTaskRunStateSuccess
if d.DbTransferTask.IsRunning(item.Id) {
if d.dbTransferTask.IsRunning(item.Id) {
item.RunningState = entity.DbTransferTaskRunStateRunning
}
}
@@ -61,7 +82,7 @@ func (d *DbTransferTask) SaveTask(rc *req.Ctx) {
task := req.BindJsonAndCopyTo[*entity.DbTransferTask](rc, reqForm, new(entity.DbTransferTask))
rc.ReqParam = reqForm
biz.ErrIsNil(d.DbTransferTask.Save(rc.MetaCtx, task))
biz.ErrIsNil(d.dbTransferTask.Save(rc.MetaCtx, task))
}
func (d *DbTransferTask) DeleteTask(rc *req.Ctx) {
@@ -73,17 +94,17 @@ func (d *DbTransferTask) DeleteTask(rc *req.Ctx) {
return cast.ToUint64(val)
})
biz.ErrIsNil(d.DbTransferTask.DeleteById(rc.MetaCtx, uids...))
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)
_ = d.dbTransferTask.UpdateById(rc.MetaCtx, task)
task, err := d.DbTransferTask.GetById(task.Id)
task, err := d.dbTransferTask.GetById(task.Id)
biz.ErrIsNil(err, "task not found")
d.DbTransferTask.AddCronJob(rc.MetaCtx, task)
d.dbTransferTask.AddCronJob(rc.MetaCtx, task)
// 记录请求日志
rc.ReqParam = form
@@ -91,18 +112,18 @@ func (d *DbTransferTask) ChangeStatus(rc *req.Ctx) {
func (d *DbTransferTask) Run(rc *req.Ctx) {
taskId := uint64(rc.PathParamInt("taskId"))
logId, _ := d.DbTransferTask.CreateLog(rc.MetaCtx, taskId)
go d.DbTransferTask.Run(rc.MetaCtx, taskId, logId)
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) {
biz.ErrIsNil(d.DbTransferTask.Stop(rc.MetaCtx, uint64(rc.PathParamInt("taskId"))))
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))
res, err := d.dbTransferFile.GetPageList(queryCond, page, new([]vo.DbTransferFileListVO))
biz.ErrIsNil(err)
rc.ResData = res
}
@@ -116,71 +137,29 @@ func (d *DbTransferTask) FileDel(rc *req.Ctx) {
for _, v := range ids {
uIds = append(uIds, cast.ToUint64(v))
}
biz.ErrIsNil(d.DbTransferFile.Delete(rc.MetaCtx, uIds...))
biz.ErrIsNil(d.dbTransferFile.Delete(rc.MetaCtx, uIds...))
}
func (d *DbTransferTask) FileRun(rc *req.Ctx) {
fm := req.BindJsonAndValid(rc, &form.DbTransferFileRunForm{})
rc.ReqParam = fm
tFile, err := d.DbTransferFile.GetById(fm.Id)
tFile, err := d.dbTransferFile.GetById(fm.Id)
biz.IsTrue(tFile != nil && err == nil, "file not found")
targetDbConn, err := d.DbApp.GetDbConn(fm.TargetDbId, fm.TargetDbName)
targetDbConn, err := d.dbApp.GetDbConn(fm.TargetDbId, fm.TargetDbName)
biz.ErrIsNilAppendErr(err, "failed to connect to the target database: %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(i18n.T(imsg.SqlScriptRunFail), fmt.Sprintf("[%s][%s] run failed: [%s]", tFile.FileKey, targetDbConn.Info.GetLogDesc(), errInfo)).WithClientId(fm.ClientId))
}
}()
biz.ErrIsNilAppendErr(d.tagApp.CanAccess(rc.GetLoginAccount().Id, targetDbConn.Info.CodePath...), "%s")
filename, reader, err := d.fileApp.GetReader(context.TODO(), tFile.FileKey)
biz.ErrIsNil(err)
go func() {
d.fileRun(rc.GetLoginAccount(), fm, tFile, targetDbConn)
biz.ErrIsNil(d.dbSqlExecApp.ExecReader(rc.MetaCtx, &dto.SqlReaderExec{
Reader: reader,
Filename: filename,
DbConn: targetDbConn,
ClientId: fm.ClientId,
}))
}()
}
func (d *DbTransferTask) fileRun(la *model.LoginAccount, fm *form.DbTransferFileRunForm, tFile *entity.DbTransferFile, targetDbConn *dbi.DbConn) {
filename, reader, err := d.FileApp.GetReader(context.TODO(), tFile.FileKey)
executedStatements := 0
progressId := stringx.Rand(32)
laId := la.Id
ticker := time.NewTicker(time.Second * 1)
defer ticker.Stop()
if err != nil {
biz.ErrIsNilAppendErr(err, "failed to connect to the target database: %s")
}
err = sqlparser.SQLSplit(reader, func(sql string) error {
select {
case <-ticker.C:
ws.SendJsonMsg(ws.UserId(laId), fm.ClientId, msgdto.InfoSqlProgressMsg(i18n.T(imsg.SqlScripRunProgress), &progressMsg{
Id: progressId,
Title: filename,
ExecutedStatements: executedStatements,
Terminated: false,
}).WithCategory(progressCategory))
default:
}
executedStatements++
_, err = targetDbConn.Exec(sql)
return err
})
if err != nil {
biz.ErrIsNilAppendErr(err, "sql execution failed: %s")
}
d.MsgApp.CreateAndSend(la, msgdto.SuccessSysMsg(i18n.T(imsg.SqlScriptRunSuccess), fmt.Sprintf("sql execution successfully: %s", filename)).WithClientId(fm.ClientId))
}

View File

@@ -52,20 +52,15 @@ type Db interface {
type dbAppImpl struct {
base.AppImpl[*entity.Db, repository.Db]
dbSqlRepo repository.DbSql `inject:"DbSqlRepo"`
dbInstanceApp Instance `inject:"DbInstanceApp"`
dbSqlExecApp DbSqlExec `inject:"DbSqlExecApp"`
tagApp tagapp.TagTree `inject:"TagTreeApp"`
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"ResourceAuthCertApp"`
dbSqlRepo repository.DbSql `inject:"T"`
dbInstanceApp Instance `inject:"T"`
dbSqlExecApp DbSqlExec `inject:"T"`
tagApp tagapp.TagTree `inject:"T"`
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"T"`
}
var _ (Db) = (*dbAppImpl)(nil)
// 注入DbRepo
func (d *dbAppImpl) InjectDbRepo(repo repository.Db) {
d.Repo = repo
}
// 分页获取数据库信息列表
func (d *dbAppImpl) GetPageList(condition *entity.DbQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return d.GetRepo().GetDbList(condition, pageParam, toEntity, orderBy...)
@@ -94,11 +89,11 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db) error {
return d.tagApp.RelateTagsByCodeAndType(ctx, &tagdto.RelateTagsByCodeAndType{
Tags: []*tagdto.ResourceTag{{
Code: dbEntity.Code,
Type: tagentity.TagTypeDbName,
Type: tagentity.TagTypeDb,
Name: dbEntity.Name,
}},
ParentTagCode: authCert.Name,
ParentTagType: tagentity.TagTypeDbAuthCert,
ParentTagType: tagentity.TagTypeAuthCert,
})
})
}
@@ -136,12 +131,12 @@ func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db) error {
return d.UpdateById(ctx, dbEntity)
}, func(ctx context.Context) error {
if old.Name != dbEntity.Name {
if err := d.tagApp.UpdateTagName(ctx, tagentity.TagTypeDbName, old.Code, dbEntity.Name); err != nil {
if err := d.tagApp.UpdateTagName(ctx, tagentity.TagTypeDb, old.Code, dbEntity.Name); err != nil {
return err
}
}
if authCert.Name != old.AuthCertName {
return d.tagApp.ChangeParentTag(ctx, tagentity.TagTypeDbName, old.Code, tagentity.TagTypeDbAuthCert, authCert.Name)
return d.tagApp.ChangeParentTag(ctx, tagentity.TagTypeDb, old.Code, tagentity.TagTypeAuthCert, authCert.Name)
}
return nil
})
@@ -170,7 +165,7 @@ func (d *dbAppImpl) Delete(ctx context.Context, id uint64) error {
}, func(ctx context.Context) error {
return d.tagApp.DeleteTagByParam(ctx, &tagdto.DelResourceTag{
ResourceCode: db.Code,
ResourceType: tagentity.TagTypeDbName,
ResourceType: tagentity.TagTypeDb,
})
})
}
@@ -191,7 +186,7 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbi.DbConn, error) {
if err != nil {
return nil, err
}
di.CodePath = d.tagApp.ListTagPathByTypeAndCode(int8(tagentity.TagTypeDbName), db.Code)
di.CodePath = d.tagApp.ListTagPathByTypeAndCode(int8(tagentity.TagTypeDb), db.Code)
di.Id = db.Id
checkDb := di.GetDatabase()
@@ -214,7 +209,7 @@ func (d *dbAppImpl) GetDbConnByInstanceId(instanceId uint64) (*dbi.DbConn, error
return nil, errorx.NewBiz("failed to get database list")
}
if len(dbs) == 0 {
return nil, errorx.NewBiz("DB instance [%d] Database is not configured, please configure it first", instanceId)
return nil, errorx.NewBiz("DB instance [%d] database is not configured, please configure it first", instanceId)
}
// 使用该实例关联的已配置数据库中的第一个库进行连接并返回
@@ -227,6 +222,10 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
if reqParam.Log != nil {
log = reqParam.Log
}
progress := dto.DefaultDumpProgress
if reqParam.Progress != nil {
progress = reqParam.Progress
}
writer := writerx.NewStringWriter(reqParam.Writer)
defer writer.Close()
@@ -249,23 +248,16 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
// 获取目标元数据仅生成sql用于生成建表语句和插入数据不能用于查询
targetDialect := dbConn.GetDialect()
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,
}}
targetDialect = meta.GetDialect(dbConn)
targetDialect = dbi.GetDialect(reqParam.TargetDbType)
}
srcMeta := dbConn.GetMetadata()
srcDialect := dbConn.GetDialect()
if len(tables) == 0 {
log("Gets the table information that can be export...")
log("gets the table information that can be export...")
ti, err := srcMeta.GetTables()
if err != nil {
log(fmt.Sprintf("Failed to get table info %s", err.Error()))
log(fmt.Sprintf("failed to get table info %s", err.Error()))
}
biz.ErrIsNil(err)
tables = make([]string, len(ti))
@@ -275,105 +267,133 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
log(fmt.Sprintf("Get %d tables", len(tables)))
}
if len(tables) == 0 {
log("No table to export. End export")
log("no table to export. end export")
return errorx.NewBiz("there is no table to export")
}
log("Querying column information...")
log("querying column information...")
// 查询列信息后面生成建表ddl和insert都需要列信息
columns, err := srcMeta.GetColumns(tables...)
if err != nil {
log(fmt.Sprintf("Failed to query column information: %s", err.Error()))
log(fmt.Sprintf("failed to query column information: %s", err.Error()))
}
biz.ErrIsNil(err)
// 以表名分组,存放每个表的列信息
columnMap := make(map[string][]dbi.Column)
for _, column := range columns {
if err := dbi.ConvToTargetDbColumn(dbConn.Info.Type, cmp.Or(reqParam.TargetDbType, dbConn.Info.Type), targetDialect, &column); err != nil {
return err
}
columnMap[column.TableName] = append(columnMap[column.TableName], column)
}
// 按表名排序
sort.Strings(tables)
quoteSchema := srcDialect.QuoteIdentifier(dbConn.Info.CurrentSchema())
quoteSchema := srcDialect.Quoter().Quote(dbConn.Info.CurrentSchema())
dumpHelper := targetDialect.GetDumpHelper()
dataHelper := targetDialect.GetDataHelper()
targetSqlGenerator := targetDialect.GetSQLGenerator()
targetDialectQuote := targetDialect.Quoter().Quote
// 遍历获取每个表的信息
for _, tableName := range tables {
log(fmt.Sprintf("Get table [%s] information...", tableName))
quoteTableName := targetDialect.QuoteIdentifier(tableName)
log(fmt.Sprintf("get table [%s] information...", tableName))
quoteTableName := targetDialectQuote(tableName)
// 查询表信息,主要是为了查询表注释
tbs, err := srcMeta.GetTables(tableName)
if err != nil {
log(fmt.Sprintf("Failed to get table [%s] information: %s", tableName, err.Error()))
log(fmt.Sprintf("failed to get table [%s] information: %s", tableName, err.Error()))
return err
}
if len(tbs) <= 0 {
log(fmt.Sprintf("Failed to get table [%s] information: No table information was retrieved", tableName))
return errorx.NewBiz(fmt.Sprintf("Failed to get table information: %s", tableName))
}
tabInfo := dbi.Table{
TableName: tableName,
TableComment: tbs[0].TableComment,
log(fmt.Sprintf("failed to get table [%s] information: No table information was retrieved", tableName))
return errorx.NewBiz("Failed to get table information: %s", tableName)
}
tableInfo := tbs[0]
columns := columnMap[tableName]
// 生成表结构信息
if reqParam.DumpDDL {
log(fmt.Sprintf("Generate table [%s] DDL...", tableName))
log(fmt.Sprintf("generate table [%s] DDL...", tableName))
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- Table structure: %s \n-- ----------------------------\n", tableName))
tbDdlArr := targetDialect.GenerateTableDDL(columnMap[tableName], tabInfo, true)
tbDdlArr := targetSqlGenerator.GenTableDDL(tableInfo, columns, true)
for _, ddl := range tbDdlArr {
writer.WriteString(ddl + ";\n")
if _, err := writer.WriteString(ddl + ";\n"); err != nil {
return err
}
}
progress(tableName, dbi.StmtTypeDDL, len(tbDdlArr), true)
}
// 生成insert sql数据在索引前加速insert
if reqParam.DumpData {
log(fmt.Sprintf("Generate table [%s] DML...", tableName))
log(fmt.Sprintf("generate table [%s] DML...", tableName))
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- Data: %s \n-- ----------------------------\n", tableName))
dumpHelper.BeforeInsert(writer, quoteTableName)
// 获取列信息
quoteColNames := make([]string, 0)
for _, col := range columnMap[tableName] {
quoteColNames = append(quoteColNames, targetDialect.QuoteIdentifier(col.ColumnName))
}
_, _ = 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)))
dataCount := 0
rows := make([][]any, 0)
_, err = dbConn.WalkTableRows(ctx, tableName, func(row map[string]any, _ []*dbi.QueryColumn) error {
rowValues := make([]any, len(columns))
for i, col := range columns {
rowValues[i] = row[col.ColumnName]
}
rows = append(rows, rowValues)
dataCount++
if dataCount%500 != 0 {
return nil
}
beforeInsert := dumpHelper.BeforeInsertSql(quoteSchema, quoteTableName)
insertSQL := fmt.Sprintf("%s INSERT INTO %s (%s) values(%s)", beforeInsert, quoteTableName, strings.Join(quoteColNames, ", "), strings.Join(rowValues, ", "))
writer.WriteString(insertSQL + ";\n")
writer.WriteString(beforeInsert)
insertSql := targetSqlGenerator.GenInsert(tableName, columns, rows, dbi.DuplicateStrategyNone)
if _, err := writer.WriteString(strings.Join(insertSql, ";\n") + ";\n"); err != nil {
return err
}
progress(tableName, dbi.StmtTypeInsert, dataCount, false)
rows = make([][]any, 0)
return nil
})
dumpHelper.AfterInsert(writer, tableName, columnMap[tableName])
if err != nil {
return err
}
if len(rows) > 0 {
beforeInsert := dumpHelper.BeforeInsertSql(quoteSchema, quoteTableName)
writer.WriteString(beforeInsert)
insertSql := targetSqlGenerator.GenInsert(tableName, columns, rows, dbi.DuplicateStrategyNone)
if _, err := writer.WriteString(strings.Join(insertSql, ";\n") + ";\n"); err != nil {
return err
}
}
dumpHelper.AfterInsert(writer, tableName, columns)
progress(tableName, dbi.StmtTypeInsert, dataCount, true)
}
log(fmt.Sprintf("Get table [%s] index information...", tableName))
log(fmt.Sprintf("get table [%s] index information...", tableName))
indexs, err := srcMeta.GetTableIndex(tableName)
if err != nil {
log(fmt.Sprintf("Failed to get table [%s] index information: %s", tableName, err.Error()))
log(fmt.Sprintf("failed to get table [%s] index information: %s", tableName, err.Error()))
return err
}
if len(indexs) > 0 {
// 最后添加索引
log(fmt.Sprintf("Generate table [%s] index...", tableName))
log(fmt.Sprintf("generate table [%s] index...", tableName))
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- Table Index: %s \n-- ----------------------------\n", tableName))
sqlArr := targetDialect.GenerateIndexDDL(indexs, tabInfo)
sqlArr := targetSqlGenerator.GenIndexDDL(tableInfo, indexs)
for _, sqlStr := range sqlArr {
writer.WriteString(sqlStr + ";\n")
if _, err := writer.WriteString(sqlStr + ";\n"); err != nil {
return err
}
}
progress(tableName, dbi.StmtTypeDDL, len(sqlArr), true)
}
}
return nil

View File

@@ -1,6 +1,7 @@
package application
import (
"cmp"
"context"
"database/sql"
"encoding/json"
@@ -14,8 +15,8 @@ import (
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/scheduler"
"mayfly-go/pkg/utils/collx"
"regexp"
"strconv"
"strings"
"time"
@@ -46,15 +47,13 @@ type DataSyncTask interface {
type dataSyncAppImpl struct {
base.AppImpl[*entity.DataSyncTask, repository.DataSyncTask]
dbDataSyncLogRepo repository.DataSyncLog `inject:"DbDataSyncLogRepo"`
dbDataSyncLogRepo repository.DataSyncLog `inject:"T"`
dbApp Db `inject:"DbApp"`
dbApp Db `inject:"T"`
}
var (
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`)
whereReg = regexp.MustCompile(`(?i)where`)
)
func (app *dataSyncAppImpl) InjectDbDataSyncTaskRepo(repo repository.DataSyncTask) {
@@ -145,28 +144,28 @@ func (app *dataSyncAppImpl) RunCronJob(ctx context.Context, id uint64) error {
updSql := ""
orderSql := ""
if task.UpdFieldVal != "0" && task.UpdFieldVal != "" && task.UpdField != "" {
srcConn, err := app.dbApp.GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
if err != nil {
logx.ErrorfContext(ctx, "data source connection unavailable: %s", err.Error())
return
}
task.UpdFieldVal = strings.Trim(task.UpdFieldVal, " ")
// 判断UpdFieldVal数据类型
var updFieldValType dbi.DataType
if _, err = strconv.Atoi(task.UpdFieldVal); err != nil {
if dateTimeReg.MatchString(task.UpdFieldVal) || dateTimeIsoReg.MatchString(task.UpdFieldVal) {
updFieldValType = dbi.DataTypeDateTime
} else {
updFieldValType = dbi.DataTypeString
}
} else {
updFieldValType = dbi.DataTypeNumber
srcConn, err := app.dbApp.GetDbConn(uint64(task.SrcDbId), task.SrcDbName)
if err != nil {
logx.ErrorfContext(ctx, "failed to connect to the source database: %s", err.Error())
return
}
wrapUpdFieldVal := srcConn.GetDialect().GetDataHelper().WrapValue(task.UpdFieldVal, updFieldValType)
updSql = fmt.Sprintf("and %s > %s", task.UpdField, wrapUpdFieldVal)
updFieldDataType := dbi.DefaultDbDataType
srcConn.WalkQueryRows(context.Background(), task.DataSql, func(row map[string]any, columns []*dbi.QueryColumn) error {
for _, column := range columns {
if strings.EqualFold(column.Name, cmp.Or(task.UpdFieldSrc, task.UpdField)) {
updFieldDataType = column.DbDataType
break
}
}
return errorx.NewBiz("get column data type")
})
updSql = fmt.Sprintf("and %s > %s", task.UpdField, updFieldDataType.DataType.SQLValue(task.UpdFieldVal))
orderSql = "order by " + task.UpdField + " asc "
}
// 正则判断DataSql是否以where .*结尾如果是则不添加where 1 = 1
@@ -176,9 +175,9 @@ func (app *dataSyncAppImpl) RunCronJob(ctx context.Context, id uint64) error {
}
// 组装查询sql
sql := fmt.Sprintf("%s %s %s %s", task.DataSql, where, updSql, orderSql)
sqlStr := fmt.Sprintf("%s %s %s %s", task.DataSql, where, updSql, orderSql)
log, err := app.doDataSync(ctx, sql, task)
log, err := app.doDataSync(ctx, sqlStr, task)
if err != nil {
log.ErrText = fmt.Sprintf("execution failure: %s", err.Error())
logx.ErrorContext(ctx, log.ErrText)
@@ -221,15 +220,13 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
}
}()
srcDialect := srcConn.GetDialect()
// task.FieldMap为json数组字符串 [{"src":"id","target":"id"}]转为map
var fieldMap []map[string]string
err = json.Unmarshal([]byte(task.FieldMap), &fieldMap)
if err != nil {
return syncLog, errorx.NewBiz("there was an error parsing the field map json: %s", err.Error())
}
var updFieldType dbi.DataType
var updFieldType *dbi.DbDataType
// 记录本次同步数据总数
total := 0
@@ -243,15 +240,28 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
updFieldName = strings.Split(task.UpdField, ".")[1]
}
targetTableColumns, err := targetConn.GetMetadata().GetColumns(task.TargetTableName)
if err != nil {
return syncLog, errorx.NewBiz("failed to get target table columns: %s", err.Error())
}
targetColumnName2Column := collx.ArrayToMap(targetTableColumns, func(column dbi.Column) string {
return column.ColumnName
})
// 目标库对应的insert columns
targetInsertColumns := collx.ArrayMap[map[string]string, dbi.Column](fieldMap, func(val map[string]string) dbi.Column {
return targetColumnName2Column[val["target"]]
})
_, err = srcConn.WalkQueryRows(context.Background(), sql, func(row map[string]any, columns []*dbi.QueryColumn) error {
if len(queryColumns) == 0 {
queryColumns = columns
// 遍历columns 取task.UpdField的字段类型
updFieldType = dbi.DataTypeString
updFieldType = dbi.DefaultDbDataType
for _, column := range columns {
if strings.EqualFold(column.Name, updFieldName) {
updFieldType = srcDialect.GetDataHelper().GetDataType(column.Type)
updFieldType = column.DbDataType
break
}
}
@@ -260,7 +270,7 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
total++
result = append(result, row)
if total%batchSize == 0 {
if err := app.srcData2TargetDb(result, fieldMap, columns, updFieldType, updFieldName, task, srcDialect, targetConn, targetDbTx); err != nil {
if err := app.srcData2TargetDb(result, fieldMap, updFieldType, updFieldName, task, targetConn, targetDbTx, targetInsertColumns); err != nil {
return err
}
@@ -283,7 +293,7 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
// 处理剩余的数据
if len(result) > 0 {
if err := app.srcData2TargetDb(result, fieldMap, queryColumns, updFieldType, updFieldName, task, srcDialect, targetConn, targetDbTx); err != nil {
if err := app.srcData2TargetDb(result, fieldMap, updFieldType, updFieldName, task, targetConn, targetDbTx, targetInsertColumns); err != nil {
targetDbTx.Rollback()
return syncLog, err
}
@@ -291,7 +301,7 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
// 如果是mssql暂不手动提交事务否则报错 mssql: The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
if err := targetDbTx.Commit(); err != nil {
if targetConn.Info.Type != dbi.DbTypeMssql {
if targetConn.Info.Type != dbi.ToDbType("mssql") {
return syncLog, errorx.NewBiz("data synchronization - The target database transaction failed to commit: %s", err.Error())
}
}
@@ -307,75 +317,52 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
return syncLog, nil
}
func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap []map[string]string, columns []*dbi.QueryColumn, updFieldType dbi.DataType, updFieldName string, task *entity.DataSyncTask, srcDialect dbi.Dialect, targetDbConn *dbi.DbConn, targetDbTx *sql.Tx) error {
// 遍历src字段列表取出字段对应的类型
var srcColumnTypes = make(map[string]string)
for _, column := range columns {
srcColumnTypes[column.Name] = column.Type
}
func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap []map[string]string, updFieldType *dbi.DbDataType, updFieldName string, task *entity.DataSyncTask, targetDbConn *dbi.DbConn, targetDbTx *sql.Tx, targetInsertColumns []dbi.Column) error {
// 遍历res组装数据
var data = make([]map[string]any, 0)
for _, record := range srcRes {
var rowData = make(map[string]any)
var targetData = make([]map[string]any, 0)
for _, srcData := range srcRes {
var data = make(map[string]any)
// 遍历字段映射, target字段的值为src字段取值
for _, item := range fieldMap {
srcField := item["src"]
targetField := item["target"]
// target字段的值为src字段取值
rowData[targetField] = record[srcField]
data[item["target"]] = srcData[item["src"]]
}
data = append(data, rowData)
targetData = append(targetData, data)
}
tragetValues := make([][]any, 0)
for _, item := range targetData {
var values = make([]any, 0)
for _, column := range targetInsertColumns {
values = append(values, item[column.ColumnName])
}
tragetValues = append(tragetValues, values)
}
// 执行插入
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 = srcDialect.GetDataHelper().FormatData(updFieldVal, updFieldType)
task.UpdFieldVal = updFieldType.DataType.SQLValue(updFieldVal)
}
// 如果指定了更新字段,则以更新字段取值
if task.UpdFieldSrc != "" {
setUpdateFieldVal(task.UpdFieldSrc)
} else {
setUpdateFieldVal(updFieldName)
}
setUpdateFieldVal(cmp.Or(task.UpdFieldSrc, updFieldName))
// 获取目标库字段数组
targetWrapColumns := make([]string, 0)
// 获取源库字段数组
srcColumns := make([]string, 0)
srcFieldTypes := make(map[string]dbi.DataType)
targetDialect := targetDbConn.GetDialect()
for _, item := range fieldMap {
targetField := item["target"]
srcField := item["target"]
srcFieldTypes[srcField] = srcDialect.GetDataHelper().GetDataType(srcColumnTypes[item["src"]])
targetWrapColumns = append(targetWrapColumns, targetDialect.QuoteIdentifier(targetField))
srcColumns = append(srcColumns, srcField)
}
// 目标数据中取出源库字段对应的值
values := make([][]any, 0)
for _, record := range data {
rawValue := make([]any, 0)
for _, column := range srcColumns {
// 某些情况如oracle需要转换时间类型的字符串为time类型
res := srcDialect.GetDataHelper().ParseData(record[column], srcFieldTypes[column])
rawValue = append(rawValue, res)
// 生成目标数据库批量插入sql并执行
sqls := targetDialect.GetSQLGenerator().GenInsert(task.TargetTableName, targetInsertColumns, tragetValues, cmp.Or(task.DuplicateStrategy, dbi.DuplicateStrategyNone))
for _, sql := range sqls {
_, err := targetDbTx.Exec(sql)
if err != nil {
return err
}
values = append(values, rawValue)
}
// 目标数据库执行sql批量插入
_, err := targetDialect.BatchInsert(targetDbTx, task.TargetTableName, targetWrapColumns, values, task.DuplicateStrategy)
if err != nil {
return err
}
// 运行过程中,判断状态是否为已关闭,是则结束运行,否则继续运行

View File

@@ -47,18 +47,13 @@ type Instance interface {
type instanceAppImpl struct {
base.AppImpl[*entity.DbInstance, repository.Instance]
tagApp tagapp.TagTree `inject:"TagTreeApp"`
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"ResourceAuthCertApp"`
dbApp Db `inject:"DbApp"`
tagApp tagapp.TagTree `inject:"T"`
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"T"`
dbApp Db `inject:"T"`
}
var _ (Instance) = (*instanceAppImpl)(nil)
// 注入DbInstanceRepo
func (app *instanceAppImpl) InjectDbInstanceRepo(repo repository.Instance) {
app.Repo = repo
}
// GetPageList 分页获取数据库实例
func (app *instanceAppImpl) GetPageList(condition *entity.InstanceQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return app.GetRepo().GetInstanceList(condition, pageParam, toEntity, orderBy...)
@@ -84,7 +79,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *dto.Sa
instanceEntity := instance.DbInstance
// 默认tcp连接
instanceEntity.Network = instanceEntity.GetNetwork()
resourceType := consts.ResourceTypeDb
resourceType := consts.ResourceTypeDbInstance
authCerts := instance.AuthCerts
tagCodePaths := instance.TagCodePaths
@@ -145,7 +140,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *dto.Sa
})
}, func(ctx context.Context) error {
if instanceEntity.Name != oldInstance.Name {
if err := app.tagApp.UpdateTagName(ctx, tagentity.TagTypeDb, oldInstance.Code, instanceEntity.Name); err != nil {
if err := app.tagApp.UpdateTagName(ctx, tagentity.TagTypeDbInstance, oldInstance.Code, instanceEntity.Name); err != nil {
return err
}
}
@@ -172,12 +167,12 @@ func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error
// 删除该实例关联的授权凭证信息
return app.resourceAuthCertApp.RelateAuthCert(ctx, &tagdto.RelateAuthCert{
ResourceCode: instance.Code,
ResourceType: tagentity.TagType(consts.ResourceTypeDb),
ResourceType: tagentity.TagType(consts.ResourceTypeDbInstance),
})
}, func(ctx context.Context) error {
return app.tagApp.DeleteTagByParam(ctx, &tagdto.DelResourceTag{
ResourceCode: instance.Code,
ResourceType: tagentity.TagType(consts.ResourceTypeDb),
ResourceType: tagentity.TagType(consts.ResourceTypeDbInstance),
})
}, func(ctx context.Context) error {
// 删除所有库配置
@@ -260,7 +255,7 @@ func (m *instanceAppImpl) genDbInstanceResourceTag(me *entity.DbInstance, authCe
return &tagdto.ResourceTag{
Code: val.Name,
Name: val.Username,
Type: tagentity.TagTypeDbAuthCert,
Type: tagentity.TagTypeAuthCert,
}
})
@@ -276,7 +271,7 @@ func (m *instanceAppImpl) genDbInstanceResourceTag(me *entity.DbInstance, authCe
authCertName2DbTags[db.AuthCertName] = append(authCertName2DbTags[db.AuthCertName], &tagdto.ResourceTag{
Code: db.Code,
Name: db.Name,
Type: tagentity.TagTypeDbName,
Type: tagentity.TagTypeDb,
})
}
@@ -287,7 +282,7 @@ func (m *instanceAppImpl) genDbInstanceResourceTag(me *entity.DbInstance, authCe
return &tagdto.ResourceTag{
Code: me.Code,
Type: tagentity.TagTypeDb,
Type: tagentity.TagTypeDbInstance,
Name: me.Name,
Children: authCertTags,
}

View File

@@ -13,8 +13,3 @@ type DbSql interface {
type dbSqlAppImpl struct {
base.AppImpl[*entity.DbSql, repository.DbSql]
}
// 注入DbSqlRepo
func (d *dbSqlAppImpl) InjectDbSqlRepo(repo repository.DbSql) {
d.Repo = repo
}

View File

@@ -3,6 +3,7 @@ package application
import (
"context"
"fmt"
"mayfly-go/internal/db/application/dto"
"mayfly-go/internal/db/config"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/dbm/sqlparser"
@@ -12,26 +13,21 @@ import (
"mayfly-go/internal/db/imsg"
flowapp "mayfly-go/internal/flow/application"
flowentity "mayfly-go/internal/flow/domain/entity"
msgapp "mayfly-go/internal/msg/application"
msgdto "mayfly-go/internal/msg/application/dto"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/jsonx"
"os"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/ws"
"strings"
)
type DbSqlExecReq struct {
DbId uint64
Db string
Sql string // 需要执行的sql支持多条
SqlFile *os.File // sql文件
Remark string // 执行备注
DbConn *dbi.DbConn
CheckFlow bool // 是否检查存储审批流程
}
type sqlExecParam struct {
DbConn *dbi.DbConn
Sql string // 执行的sql
@@ -41,18 +37,25 @@ type sqlExecParam struct {
SqlExecRecord *entity.DbSqlExec // sql执行记录
}
type DbSqlExecRes struct {
Sql string `json:"sql"` // 执行的sql
ErrorMsg string `json:"errorMsg"` // 若执行失败,则将失败内容记录到该字段
Columns []*dbi.QueryColumn `json:"columns"` // 响应的列信
Res []map[string]any `json:"res"` // 响应结果
// progressCategory sql文件执行进度消息类型
const progressCategory = "execSqlFileProgress"
// progressMsg sql文件执行进度消
type progressMsg struct {
Id string `json:"id"`
Title string `json:"title"`
ExecutedStatements int `json:"executedStatements"`
Terminated bool `json:"terminated"`
}
type DbSqlExec interface {
flowapp.FlowBizHandler
// 执行sql
Exec(ctx context.Context, execSqlReq *DbSqlExecReq) ([]*DbSqlExecRes, error)
Exec(ctx context.Context, execSqlReq *dto.DbSqlExecReq) ([]*dto.DbSqlExecRes, error)
// ExecReader 从reader中读取sql并执行
ExecReader(ctx context.Context, execReader *dto.SqlReaderExec) error
// 根据条件删除sql执行记录
DeleteBy(ctx context.Context, condition *entity.DbSqlExec) error
@@ -62,13 +65,14 @@ type DbSqlExec interface {
}
type dbSqlExecAppImpl struct {
dbApp Db `inject:"DbApp"`
dbSqlExecRepo repository.DbSqlExec `inject:"DbSqlExecRepo"`
dbApp Db `inject:"T"`
dbSqlExecRepo repository.DbSqlExec `inject:"T"`
flowProcdefApp flowapp.Procdef `inject:"ProcdefApp"`
flowProcdefApp flowapp.Procdef `inject:"T"`
msgApp msgapp.Msg `inject:"T"`
}
func createSqlExecRecord(ctx context.Context, execSqlReq *DbSqlExecReq, sql string) *entity.DbSqlExec {
func createSqlExecRecord(ctx context.Context, execSqlReq *dto.DbSqlExecReq, sql string) *entity.DbSqlExec {
dbSqlExecRecord := new(entity.DbSqlExec)
dbSqlExecRecord.DbId = execSqlReq.DbId
dbSqlExecRecord.Db = execSqlReq.Db
@@ -79,7 +83,7 @@ func createSqlExecRecord(ctx context.Context, execSqlReq *DbSqlExecReq, sql stri
return dbSqlExecRecord
}
func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *DbSqlExecReq) ([]*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *dto.DbSqlExecReq) ([]*dto.DbSqlExecRes, error) {
dbConn := execSqlReq.DbConn
execSql := execSqlReq.Sql
sp := dbConn.GetDialect().GetSQLParser()
@@ -89,13 +93,13 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *DbSqlExecReq) (
flowProcdef = d.flowProcdefApp.GetProcdefByCodePath(ctx, dbConn.Info.CodePath...)
}
allExecRes := make([]*DbSqlExecRes, 0)
allExecRes := make([]*dto.DbSqlExecRes, 0)
stmts, err := sp.Parse(execSql)
// sql解析失败则使用默认方式切割
if err != nil {
sqlparser.SQLSplit(strings.NewReader(execSql), func(oneSql string) error {
var execRes *DbSqlExecRes
var execRes *dto.DbSqlExecRes
var err error
dbSqlExecRecord := createSqlExecRecord(ctx, execSqlReq, oneSql)
@@ -120,7 +124,7 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *DbSqlExecReq) (
// 执行错误
if err != nil {
if execRes == nil {
execRes = &DbSqlExecRes{Sql: oneSql}
execRes = &dto.DbSqlExecRes{Sql: oneSql}
}
execRes.ErrorMsg = err.Error()
} else {
@@ -133,7 +137,7 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *DbSqlExecReq) (
}
for _, stmt := range stmts {
var execRes *DbSqlExecRes
var execRes *dto.DbSqlExecRes
var err error
sql := stmt.GetText()
@@ -172,7 +176,7 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *DbSqlExecReq) (
if err != nil {
if execRes == nil {
execRes = &DbSqlExecRes{Sql: sql}
execRes = &dto.DbSqlExecRes{Sql: sql}
}
execRes.ErrorMsg = err.Error()
} else {
@@ -184,6 +188,64 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *DbSqlExecReq) (
return allExecRes, nil
}
func (d *dbSqlExecAppImpl) ExecReader(ctx context.Context, execReader *dto.SqlReaderExec) error {
dbConn := execReader.DbConn
clientId := execReader.ClientId
filename := stringx.Truncate(execReader.Filename, 20, 10, "...")
la := contextx.GetLoginAccount(ctx)
needSendMsg := la != nil && clientId != ""
defer func() {
if err := recover(); err != nil {
errInfo := anyx.ToString(err)
logx.Errorf("exec sql reader error: %s", errInfo)
if needSendMsg {
errInfo = stringx.Truncate(errInfo, 300, 10, "...")
d.msgApp.CreateAndSend(la, msgdto.ErrSysMsg(i18n.T(imsg.SqlScriptRunFail), fmt.Sprintf("[%s][%s] execution failure: [%s]", filename, dbConn.Info.GetLogDesc(), errInfo)).WithClientId(clientId))
}
}
}()
executedStatements := 0
progressId := stringx.Rand(32)
if needSendMsg {
defer ws.SendJsonMsg(ws.UserId(la.Id), clientId, msgdto.InfoSysMsg(i18n.T(imsg.SqlScripRunProgress), &progressMsg{
Id: progressId,
Title: filename,
ExecutedStatements: executedStatements,
Terminated: true,
}).WithCategory(progressCategory))
}
err := sqlparser.SQLSplit(execReader.Reader, func(sql string) error {
if executedStatements%50 == 0 {
if needSendMsg {
ws.SendJsonMsg(ws.UserId(la.Id), clientId, msgdto.InfoSysMsg(i18n.T(imsg.SqlScripRunProgress), &progressMsg{
Id: progressId,
Title: filename,
ExecutedStatements: executedStatements,
Terminated: false,
}).WithCategory(progressCategory))
}
}
executedStatements++
if _, err := dbConn.Exec(sql); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
if needSendMsg {
d.msgApp.CreateAndSend(la, msgdto.SuccessSysMsg(i18n.T(imsg.SqlScriptRunSuccess), "execution success").WithClientId(clientId))
}
return nil
}
type FlowDbExecSqlBizForm struct {
DbId uint64 `json:"dbId"` // 库id
DbName string `json:"dbName"` // 库名
@@ -211,7 +273,7 @@ func (d *dbSqlExecAppImpl) FlowBizHandle(ctx context.Context, bizHandleParam *fl
return nil, err
}
execRes, err := d.Exec(contextx.NewLoginAccount(&model.LoginAccount{Id: procinst.CreatorId, Username: procinst.Creator}), &DbSqlExecReq{
execRes, err := d.Exec(contextx.NewLoginAccount(&model.LoginAccount{Id: procinst.CreatorId, Username: procinst.Creator}), &dto.DbSqlExecReq{
DbId: execSqlBizForm.DbId,
Db: execSqlBizForm.DbName,
Sql: execSqlBizForm.Sql,
@@ -257,7 +319,7 @@ func (d *dbSqlExecAppImpl) saveSqlExecLog(dbSqlExecRecord *entity.DbSqlExec, res
}
}
func (d *dbSqlExecAppImpl) doSelect(ctx context.Context, sqlExecParam *sqlExecParam) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doSelect(ctx context.Context, sqlExecParam *sqlExecParam) (*dto.DbSqlExecRes, error) {
maxCount := config.GetDbms().MaxResultSet
selectStmt := sqlExecParam.Stmt
selectSql := sqlExecParam.Sql
@@ -314,7 +376,7 @@ func (d *dbSqlExecAppImpl) doSelect(ctx context.Context, sqlExecParam *sqlExecPa
return d.doQuery(ctx, sqlExecParam.DbConn, selectSql)
}
func (d *dbSqlExecAppImpl) doOtherRead(ctx context.Context, sqlExecParam *sqlExecParam) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doOtherRead(ctx context.Context, sqlExecParam *sqlExecParam) (*dto.DbSqlExecRes, error) {
selectSql := sqlExecParam.Sql
sqlExecParam.SqlExecRecord.Type = entity.DbSqlExecTypeQuery
@@ -327,7 +389,7 @@ func (d *dbSqlExecAppImpl) doOtherRead(ctx context.Context, sqlExecParam *sqlExe
return d.doQuery(ctx, sqlExecParam.DbConn, selectSql)
}
func (d *dbSqlExecAppImpl) doExecDDL(ctx context.Context, sqlExecParam *sqlExecParam) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doExecDDL(ctx context.Context, sqlExecParam *sqlExecParam) (*dto.DbSqlExecRes, error) {
selectSql := sqlExecParam.Sql
sqlExecParam.SqlExecRecord.Type = entity.DbSqlExecTypeDDL
@@ -340,7 +402,7 @@ func (d *dbSqlExecAppImpl) doExecDDL(ctx context.Context, sqlExecParam *sqlExecP
return d.doExec(ctx, sqlExecParam.DbConn, selectSql)
}
func (d *dbSqlExecAppImpl) doUpdate(ctx context.Context, sqlExecParam *sqlExecParam) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doUpdate(ctx context.Context, sqlExecParam *sqlExecParam) (*dto.DbSqlExecRes, error) {
dbConn := sqlExecParam.DbConn
if procdef := sqlExecParam.Procdef; procdef != nil {
@@ -365,7 +427,7 @@ func (d *dbSqlExecAppImpl) doUpdate(ctx context.Context, sqlExecParam *sqlExecPa
tableSources := updatestmt.TableSources.TableSources
// 不支持多表更新记录旧值
if len(tableSources) != 1 {
logx.ErrorContext(ctx, "Update SQL - logging old values only supports single-table updates")
logx.ErrorContext(ctx, "update SQL - logging old values only supports single-table updates")
return d.doExec(ctx, dbConn, sqlExecParam.Sql)
}
@@ -379,21 +441,21 @@ func (d *dbSqlExecAppImpl) doUpdate(ctx context.Context, sqlExecParam *sqlExecPa
}
if tableName == "" {
logx.ErrorContext(ctx, "Update SQL - failed to get table name")
logx.ErrorContext(ctx, "update SQL - failed to get table name")
return d.doExec(ctx, dbConn, sqlExecParam.Sql)
}
execRecord.Table = tableName
whereStr := updatestmt.Where.GetText()
if whereStr == "" {
logx.ErrorContext(ctx, "Update SQL - there is no where condition")
if updatestmt.Where == nil {
logx.ErrorContext(ctx, "update SQL - there is no where condition")
return d.doExec(ctx, dbConn, sqlExecParam.Sql)
}
whereStr := updatestmt.Where.GetText()
// 获取表主键列名,排除使用别名
primaryKey, err := dbConn.GetMetadata().GetPrimaryKey(tableName)
if err != nil {
logx.ErrorfContext(ctx, "Update SQL - failed to get primary key column: %s", err.Error())
logx.ErrorfContext(ctx, "update SQL - failed to get primary key column: %s", err.Error())
return d.doExec(ctx, dbConn, sqlExecParam.Sql)
}
@@ -417,12 +479,12 @@ func (d *dbSqlExecAppImpl) doUpdate(ctx context.Context, sqlExecParam *sqlExecPa
nowRec++
res = append(res, row)
if nowRec == maxRec {
return errorx.NewBiz(fmt.Sprintf("Update SQL - the maximum number of updated queries is exceeded: %d", maxRec))
return errorx.NewBiz(fmt.Sprintf("update SQL - the maximum number of updated queries is exceeded: %d", maxRec))
}
return nil
})
if err != nil {
logx.ErrorfContext(ctx, "Update SQL - failed to get the updated old value: %s", err.Error())
logx.ErrorfContext(ctx, "update SQL - failed to get the updated old value: %s", err.Error())
return d.doExec(ctx, dbConn, sqlExecParam.Sql)
}
execRecord.OldValue = jsonx.ToStr(res)
@@ -430,7 +492,7 @@ func (d *dbSqlExecAppImpl) doUpdate(ctx context.Context, sqlExecParam *sqlExecPa
return d.doExec(ctx, dbConn, sqlExecParam.Sql)
}
func (d *dbSqlExecAppImpl) doDelete(ctx context.Context, sqlExecParam *sqlExecParam) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doDelete(ctx context.Context, sqlExecParam *sqlExecParam) (*dto.DbSqlExecRes, error) {
if procdef := sqlExecParam.Procdef; procdef != nil {
if needStartProc := procdef.MatchCondition(DbSqlExecFlowBizType, collx.Kvs("stmtType", "delete")); needStartProc {
return nil, errorx.NewBizI(ctx, imsg.ErrNeedSubmitWorkTicket)
@@ -454,7 +516,7 @@ func (d *dbSqlExecAppImpl) doDelete(ctx context.Context, sqlExecParam *sqlExecPa
tableSources := deletestmt.TableSources.TableSources
// 不支持多表删除记录旧值
if len(tableSources) != 1 {
logx.ErrorContext(ctx, "Delete SQL - logging old values only supports single-table deletion")
logx.ErrorContext(ctx, "delete SQL - logging old values only supports single-table deletion")
return d.doExec(ctx, dbConn, sqlExecParam.Sql)
}
@@ -468,14 +530,14 @@ func (d *dbSqlExecAppImpl) doDelete(ctx context.Context, sqlExecParam *sqlExecPa
}
if tableName == "" {
logx.ErrorContext(ctx, "Delete SQL - failed to get table name")
logx.ErrorContext(ctx, "delete SQL - failed to get table name")
return d.doExec(ctx, dbConn, sqlExecParam.Sql)
}
execRecord.Table = tableName
whereStr := deletestmt.Where.GetText()
if whereStr == "" {
logx.ErrorContext(ctx, "Delete SQL - there is no where condition")
logx.ErrorContext(ctx, "delete SQL - there is no where condition")
return d.doExec(ctx, dbConn, sqlExecParam.Sql)
}
@@ -487,7 +549,7 @@ func (d *dbSqlExecAppImpl) doDelete(ctx context.Context, sqlExecParam *sqlExecPa
return d.doExec(ctx, dbConn, sqlExecParam.Sql)
}
func (d *dbSqlExecAppImpl) doInsert(ctx context.Context, sqlExecParam *sqlExecParam) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doInsert(ctx context.Context, sqlExecParam *sqlExecParam) (*dto.DbSqlExecRes, error) {
if procdef := sqlExecParam.Procdef; procdef != nil {
if needStartProc := procdef.MatchCondition(DbSqlExecFlowBizType, collx.Kvs("stmtType", "insert")); needStartProc {
return nil, errorx.NewBizI(ctx, imsg.ErrNeedSubmitWorkTicket)
@@ -513,19 +575,19 @@ func (d *dbSqlExecAppImpl) doInsert(ctx context.Context, sqlExecParam *sqlExecPa
return d.doExec(ctx, sqlExecParam.DbConn, sqlExecParam.Sql)
}
func (d *dbSqlExecAppImpl) doQuery(ctx context.Context, dbConn *dbi.DbConn, sql string) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doQuery(ctx context.Context, dbConn *dbi.DbConn, sql string) (*dto.DbSqlExecRes, error) {
cols, res, err := dbConn.QueryContext(ctx, sql)
if err != nil {
return nil, err
}
return &DbSqlExecRes{
return &dto.DbSqlExecRes{
Sql: sql,
Columns: cols,
Res: res,
}, nil
}
func (d *dbSqlExecAppImpl) doExec(ctx context.Context, dbConn *dbi.DbConn, sql string) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doExec(ctx context.Context, dbConn *dbi.DbConn, sql string) (*dto.DbSqlExecRes, error) {
rowsAffected, err := dbConn.ExecContext(ctx, sql)
if err != nil {
return nil, err
@@ -534,7 +596,7 @@ func (d *dbSqlExecAppImpl) doExec(ctx context.Context, dbConn *dbi.DbConn, sql s
res := make([]map[string]any, 0)
res = append(res, collx.Kvs("rowsAffected", rowsAffected))
return &DbSqlExecRes{
return &dto.DbSqlExecRes{
Columns: []*dbi.QueryColumn{
{Name: "rowsAffected", Type: "number"},
},

Some files were not shown because too many files have changed in this diff Show More