Compare commits

..

9 Commits

Author SHA1 Message Date
meilin.huang
9bb9861d88 refactor: 参数绑定等优化 2026-02-07 13:12:07 +08:00
meilin.huang
403d1c45e5 refactor: sshtunnel等 2026-02-01 13:35:23 +08:00
meilin.huang
400db0402a refactor: 包优化&其他问题修复 2026-01-25 14:16:16 +08:00
fanzhouqi
f0ae178183 !143 fix:mysql查询时,如果出现列名一样,会覆盖数据,github issues #124
* fix:mysql查询时,如果出现列名一样,会覆盖数据,github issues #124
2026-01-23 09:13:14 +00:00
meilin.huang
4641e448d2 fix: 数据库迁移、同步保存时定时任务未清除问题 2026-01-21 12:22:54 +08:00
meilin.huang
f0de65b7ce refactor: 协程启动优化、tagviews调整 2026-01-20 19:45:46 +08:00
meilin.huang
0472c5101f feat: 数据库迁移至文件支持zip格式、go启动协程时panic优化、菜单资源优化 2026-01-18 20:40:11 +08:00
meilin.huang
185cd6f82b fix: postgres导出调整等 2026-01-14 20:38:06 +08:00
meilin.huang
aa6ad39b83 feat: 新增数据库导出按钮权限等 2026-01-07 21:27:25 +08:00
221 changed files with 3084 additions and 2050 deletions

View File

@@ -57,7 +57,7 @@ function build() {
execFileName="${execFileName}.exe"
fi
go mod tidy
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -ldflags=-w -o ${execFileName} main.go
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -trimpath -ldflags=-w -o ${execFileName} main.go
if [ -d ${toFolder} ] ; then
echo_green "The desired folder already exists. Clear the folder"

View File

@@ -11,35 +11,35 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@logicflow/core": "^2.1.4",
"@logicflow/extension": "^2.1.6",
"@vueuse/core": "^14.1.0",
"@logicflow/core": "^2.1.7",
"@logicflow/extension": "^2.1.9",
"@vueuse/core": "^14.2.0",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-search": "^0.16.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"asciinema-player": "^3.13.5",
"asciinema-player": "^3.14.0",
"axios": "^1.6.2",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.19",
"echarts": "^6.0.0",
"element-plus": "^2.13.0",
"element-plus": "^2.13.2",
"js-base64": "^3.7.8",
"jsencrypt": "^3.5.4",
"monaco-editor": "^0.55.1",
"monaco-sql-languages": "^0.15.1",
"nprogress": "^0.2.0",
"pinia": "^3.0.4",
"qrcode.vue": "^3.6.0",
"qrcode.vue": "^3.8.0",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.6",
"sql-formatter": "^15.6.12",
"sql-formatter": "^15.7.0",
"trzsz": "^1.1.5",
"uuid": "^13.0.0",
"vue": "^v3.6.0-beta.1",
"vue-i18n": "^11.2.7",
"vue-router": "^4.6.4",
"vue": "^v3.6.0-beta.2",
"vue-i18n": "^11.2.8",
"vue-router": "^5.0.2",
"vuedraggable": "^4.1.0",
"xlsx": "^0.18.5"
},

View File

@@ -13,9 +13,6 @@ export function getBaseApiUrl() {
const config = {
baseApiUrl: `${(window as any).globalConfig.BaseApiUrl || location.protocol + '//' + getBaseApiUrl()}/api`,
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.10.4',
};
export default config;

View File

@@ -225,10 +225,12 @@ const initMonacoEditorIns = () => {
let options = Object.assign(defaultOptions, props.options as any);
monacoEditorIns = monaco.editor.create(monacoTextareaRef.value, options);
// 监听内容改变,双向绑定
monacoEditorIns.onDidChangeModelContent(() => {
modelValue.value = monacoEditorIns.getModel()?.getValue();
});
if (!options.readOnly) {
// 监听内容改变,双向绑定
monacoEditorIns.onDidChangeModelContent(() => {
modelValue.value = monacoEditorIns.getModel()?.getValue();
});
}
};
const changeLanguage = (value: any) => {

View File

@@ -34,7 +34,7 @@ const websocketUrl = ref(props.wsUrl);
const { data } = useWebSocket(websocketUrl);
const editorRef: any = useTemplateRef('editorRef');
const editorRef = useTemplateRef<InstanceType<typeof MonacoEditor>>('editorRef');
const modelValue = defineModel<string>('modelValue', {
type: String,
@@ -56,9 +56,9 @@ const reload = (wsUrl: string) => {
};
const revealLastLine = () => {
const editor = editorRef.value.getEditor();
const lineCount = editor?.getModel().getLineCount();
editor.revealLine(lineCount);
const editor = editorRef.value?.getEditor();
const lineCount = editor?.getModel()?.getLineCount();
editor?.revealLine(lineCount || 0);
};
defineExpose({

View File

@@ -169,6 +169,7 @@ export default {
transfer2Db: 'Transfer to DB',
transfer2File: 'Transfer to File',
fileSaveDays: 'File retention days',
fileType: 'File Type',
transferStrategy: 'Transfer Strategy',
day: 'Day',
transferFull: 'Full',

View File

@@ -47,10 +47,12 @@ export default {
machineSecurityCmdSvae: 'Cmd Config-Save',
machineSecurityCmdDelete: 'Cmd Config-Delete',
db: 'Database',
dbms: 'DBMS',
dbDataOp: 'Data Operation',
dbDataOpBase: 'Base Permission',
dbDataOpSqlScriptRun: 'SQL Script Run',
dbDataOpBase: 'DB-Base Permission',
dbDataOpSqlScriptRun: 'DB-SQL Script Run',
dbDataExport: 'DB-Data Export',
dbInstance: 'DB Instance',
dbInstanceBase: 'Base Permission',
dbInstanceSave: 'Save Instance',
@@ -77,22 +79,28 @@ export default {
dbTransferFileRun: 'Transfer File-Run',
redis: 'Redis',
redisDataOp: 'Data Operation',
redisDataOpBase: 'Base Permission',
redisDataOpSave: 'Save Data',
redisDataOpDelete: 'Delete Data',
redisSave: 'Save Redis',
redisDel: 'Delete Redis',
redisDataOp: 'Redis - Data Operation',
redisDataOpBase: 'Redis - Base Permission',
redisDataOpSave: 'Redis - Save Data',
redisDataOpDelete: 'Redis - Delete Data',
redisManage: 'Redis Manage',
redisManageBase: 'Base Permission',
redisManageBase: 'Redis - Base Permission',
mongo: 'Mongo',
mongoDataOp: 'Data Operation',
mongoDataOpBase: 'Base Permission',
mongoDataOpSave: 'Save Data',
mongoDataOpDelete: 'Delete Data',
mongoSave: 'Save Mongo',
mongoDel: 'Delete Mongo',
mongoDataOp: 'Mongo - Data Operation',
mongoDataOpBase: 'Mongo - Base Permission',
mongoDataOpSave: 'Mongo - Save Data',
mongoDataOpDelete: 'Mongo - Delete Data',
mongoManage: 'Mongo Manage',
mongoManageBase: 'Base Permission',
mongoManageBase: 'Mongo - Base Permission',
containerManageBase: 'Container Manage - Base Permission',
container: 'Container',
containerSave: 'Save Container',
containerDel: 'Delete Container',
flow: 'Flow',
myTask: 'My Task',

View File

@@ -165,6 +165,7 @@ export default {
transfer2Db: '迁移到数据库',
transfer2File: '迁移到文件',
fileSaveDays: '文件保留天数',
fileType: '文件类型',
transferStrategy: '迁移策略',
day: '天',
transferFull: '全量',

View File

@@ -47,17 +47,19 @@ export default {
machineSecurityCmdSvae: '机器-命令配置-保存',
machineSecurityCmdDelete: '机器-命令配置-删除',
db: '数据库',
dbms: 'DBMS',
dbDataOp: '数据操作',
dbDataOpBase: 'Db-数据操作-基本权限',
dbDataOpSqlScriptRun: 'Db-SQL脚本执行',
dbDataOp: 'DB-数据操作',
dbDataOpBase: 'DB-数据操作-基本权限',
dbDataOpSqlScriptRun: 'DB-SQL脚本执行',
dbDataExport: 'DB-数据导出',
dbInstance: '数据库实例',
dbInstanceBase: 'Db-基本权限',
dbInstanceSave: 'Db-保存实例',
dbInstanceDelete: 'Db-删除实例',
dbInstanceBase: 'DB-基本权限',
dbInstanceSave: 'DB-保存实例',
dbInstanceDelete: 'DB-删除实例',
dbBase: '数据库基本权限',
dbSave: 'Db-保存数据库',
dbDelete: 'Db-删除数据库',
dbSave: 'DB-保存数据库',
dbDelete: 'DB-删除数据库',
dbDataSync: '数据同步',
dbDataSyncBase: '基本权限',
dbDataSyncSave: '保存同步',
@@ -77,6 +79,8 @@ export default {
dbTransferFileRun: '迁移文件-执行',
redis: 'Redis',
redisSave: 'Redis-保存',
redisDel: 'Redis-删除',
redisDataOp: 'Redis-数据操作',
redisDataOpBase: 'Redis-数据操作-基本权限',
redisDataOpSave: 'Redis-数据操作-数据保存',
@@ -85,6 +89,8 @@ export default {
redisManageBase: 'Redis-管理-基本权限',
mongo: 'Mongo',
mongoSave: 'Mongo-保存',
mongoDel: 'Mongo-删除',
mongoDataOp: '数据操作',
mongoDataOpBase: 'Mongo-数据操作-基本权限',
mongoDataOpSave: 'Mongo-数据操作-数据保存',
@@ -92,7 +98,9 @@ export default {
mongoManage: 'Mongo管理',
mongoManageBase: 'Mongo-管理-基本权限',
containerManageBase: '容器-管理-基本权限',
container: '容器',
containerSave: '容器-保存',
containerDel: '容器-删除',
flow: '工单流程',
myTask: '我的任务',

View File

@@ -4,7 +4,7 @@
<span class="logo-title">
{{ `${themeConfig.globalTitle}` }}
<sub
><span style="font-size: 10px; color: goldenrod">{{ ` ${config.version}` }}</span></sub
><span style="font-size: 10px; color: goldenrod">{{ ` ${themeConfig.version}` }}</span></sub
>
</span>
</div>
@@ -17,7 +17,6 @@
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import config from '@/common/config';
const { themeConfig } = storeToRefs(useThemeConfig());

View File

@@ -308,17 +308,7 @@
<!-- 其它设置 -->
<el-divider content-position="left">{{ $t('layout.config.otherSetting') }}</el-divider>
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.tagsStyle') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-select v-model="themeConfig.tagsStyle" placeholder="请选择" size="small" style="width: 90px">
<el-option label="风格1" value="tags-style-one"></el-option>
<el-option label="风格2" value="tags-style-two"></el-option>
<el-option label="风格3" value="tags-style-three"></el-option>
</el-select>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
<div class="layout-breadcrumb-seting-bar-flex mt-3.5!">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.animation') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-select v-model="themeConfig.animation" size="small" style="width: 90px">
@@ -328,7 +318,7 @@
</el-select>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5 !mb-5.5">
<div class="layout-breadcrumb-seting-bar-flex mt-3.5! mb-5.5!">
<div class="layout-breadcrumb-seting-bar-flex-label">
{{ $t('layout.config.columnsAsideStyle') }}
</div>

View File

@@ -1,7 +1,7 @@
<template>
<div class="layout-navbars-tagsview" :class="{ 'layout-navbars-tagsview-shadow': themeConfig.layout === 'classic' }">
<el-scrollbar ref="scrollbarRef" @wheel.prevent="onHandleScroll">
<ul class="layout-navbars-tagsview-ul" :class="setTagsStyle" ref="tagsUlRef">
<ul class="layout-navbars-tagsview-ul" ref="tagsUlRef">
<li
v-for="(v, k) in tagsViews"
:key="k"
@@ -18,26 +18,20 @@
>
<SvgIcon :name="v.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="themeConfig.isTagsviewIcon" />
<span>{{ $t(v.title) }}</span>
<template v-if="isActive(v)">
<SvgIcon
name="RefreshRight"
class="!text-[14px] ml-1 layout-navbars-tagsview-ul-li-refresh"
class="text-[14px]! ml-1 layout-navbars-tagsview-ul-li-icon layout-navbars-tagsview-ul-li-refresh"
@click.stop="refreshCurrentTagsView($route.fullPath)"
/>
<SvgIcon
name="Close"
class="!text-[14px] layout-navbars-tagsview-ul-li-icon layout-icon-active"
class="text-[14px]! layout-navbars-tagsview-ul-li-icon layout-navbars-tagsview-ul-li-close layout-icon-active"
v-if="!v.isAffix"
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
/>
</template>
<SvgIcon
name="Close"
class="!text-[14px] layout-navbars-tagsview-ul-li-icon layout-icon-three"
v-if="!v.isAffix"
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
/>
</li>
</ul>
</el-scrollbar>
@@ -46,7 +40,7 @@
</template>
<script lang="ts" setup name="layoutTagsView">
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, getCurrentInstance, watch } from 'vue';
import { reactive, onMounted, ref, nextTick, onBeforeUpdate, getCurrentInstance, watch } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import screenfull from 'screenfull';
import { storeToRefs } from 'pinia';
@@ -105,11 +99,6 @@ const state = reactive({
},
});
// 动态设置 tagsView 风格样式
const setTagsStyle = computed(() => {
return themeConfig.value.tagsStyle;
});
// 存储 tagsViewList 到浏览器临时缓存中,页面刷新时,保留记录
const addBrowserSetSession = (tagsViewList: Array<object>) => {
setTagViews(tagsViewList);
@@ -403,163 +392,120 @@ onBeforeRouteUpdate((to) => {
});
</script>
<style scoped lang="scss">
<style scoped lang="css">
.layout-navbars-tagsview {
background-color: var(--bg-main-color);
border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
position: relative;
z-index: 4;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
:deep(.el-scrollbar__wrap) {
overflow-x: auto !important;
}
.layout-navbars-tagsview :deep(.el-scrollbar__wrap) {
overflow-x: auto !important;
}
&-ul {
list-style: none;
margin: 0;
padding: 0;
height: 34px;
display: flex;
align-items: center;
color: var(--el-text-color-regular);
font-size: 12px;
white-space: nowrap;
padding: 0 15px;
.layout-navbars-tagsview-ul {
list-style: none;
margin: 0;
padding: 0;
height: 38px;
display: flex;
align-items: center;
color: var(--el-text-color-regular);
font-size: 13px;
white-space: nowrap;
padding: 0 15px;
}
&-li {
height: 26px;
line-height: 26px;
display: flex;
align-items: center;
border: 1px solid var(--el-border-color-lighter);
padding: 0 15px;
margin-right: 5px;
border-radius: 2px;
position: relative;
z-index: 0;
cursor: pointer;
justify-content: space-between;
.layout-navbars-tagsview-ul-li {
height: 30px;
line-height: 30px;
display: flex;
align-items: center;
border-radius: 6px;
padding: 0 12px;
margin-right: 6px;
position: relative;
z-index: 0;
cursor: pointer;
justify-content: space-between;
transition: all 0.3s ease;
border: 1px solid var(--el-border-color, #dcdfe6);
box-sizing: border-box;
background-color: var(--el-bg-color, #fafafa);
color: var(--el-text-color-regular, #606266);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
&:hover {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
border-color: var(--el-color-primary-light-5);
}
.layout-navbars-tagsview-ul-li:not(.is-active):hover {
background-color: var(--el-fill-color-blank, #f5f7fa);
color: var(--el-text-color-primary, #303133);
border-color: var(--el-color-primary-light-7, #c6e2ff);
transform: translateY(-1px);
}
&-iconfont {
position: relative;
left: -5px;
font-size: 12px;
}
.layout-navbars-tagsview-ul-li-iconfont {
position: relative;
left: -3px;
font-size: 12px;
margin-right: 4px;
}
&-icon {
border-radius: 100%;
position: relative;
height: 14px;
width: 14px;
text-align: center;
line-height: 14px;
right: -5px;
.layout-navbars-tagsview-ul-li-icon {
border-radius: 4px;
position: relative;
height: 18px;
width: 18px;
text-align: center;
line-height: 18px;
right: -3px;
margin-left: 4px;
transition: all 0.25s ease;
color: var(--el-text-color-secondary, #909399);
display: flex;
align-items: center;
justify-content: center;
}
&:hover {
color: var(--el-color-white);
background-color: var(--el-color-primary-light-3);
}
}
.layout-navbars-tagsview-ul-li-icon:hover {
background-color: var(--el-color-info-light-7);
border-radius: 4px;
}
.layout-icon-active {
display: block;
}
.layout-icon-active {
display: flex;
align-items: center;
justify-content: center;
}
.layout-icon-three {
display: none;
}
}
.layout-navbars-tagsview-ul .is-active {
color: var(--el-color-primary, #409eff);
background: var(--el-color-primary-light-9, #ecf5ff);
border-color: var(--el-color-primary-light-5, #409eff);
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);
}
.is-active {
color: var(--el-color-white);
background: var(--el-color-primary);
border-color: var(--el-color-primary);
transition: border-color 3s ease;
}
}
.layout-navbars-tagsview-ul .is-active .layout-navbars-tagsview-ul-li-icon {
color: var(--el-color-primary, #409eff);
}
// 风格2
.tags-style-two {
.layout-navbars-tagsview-ul-li {
margin-right: 0 !important;
border: none !important;
position: relative;
border-radius: 3px !important;
.layout-navbars-tagsview-ul .is-active .layout-navbars-tagsview-ul-li-icon:hover {
background-color: var(--el-color-primary);
color: var(--el-color-white);
transform: scale(1.1);
}
.layout-icon-active {
display: none;
}
.layout-navbars-tagsview-ul .is-active .layout-navbars-tagsview-ul-li-close:hover {
background-color: var(--el-color-danger);
color: var(--el-color-white);
border-radius: 4px;
}
.layout-icon-three {
display: block;
}
&:hover {
background: none !important;
}
}
.is-active {
background: none !important;
color: var(--el-color-primary) !important;
}
}
// 风格3
.tags-style-three {
align-items: flex-end;
.tgs-style-three-svg {
-webkit-mask-image:
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CgogPGc+CiAgPHRpdGxlPkxheWVyIDE8L3RpdGxlPgogIDxwYXRoIHRyYW5zZm9ybT0icm90YXRlKC0wLjEzMzUwNiA1MC4xMTkyIDUwKSIgaWQ9InN2Z18xIiBkPSJtMTAwLjExOTE5LDEwMGMtNTUuMjI4LDAgLTEwMCwtNDQuNzcyIC0xMDAsLTEwMGwwLDEwMGwxMDAsMHoiIG9wYWNpdHk9InVuZGVmaW5lZCIgc3Ryb2tlPSJudWxsIiBmaWxsPSIjRjhFQUU3Ii8+CiAgPHBhdGggZD0ibS0wLjYzNzY2LDcuMzEyMjhjMC4xMTkxOSwwIDAuMjE3MzcsMC4wNTc5NiAwLjQ3Njc2LDAuMTE5MTljMC4yMzIsMC4wNTQ3NyAwLjI3MzI5LDAuMDM0OTEgMC4zNTc1NywwLjExOTE5YzAuMDg0MjgsMC4wODQyOCAwLjM1NzU3LDAgMC40NzY3NiwwbDAuMTE5MTksMGwwLjIzODM4LDAiIGlkPSJzdmdfMiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHBhdGggZD0ibTI4LjkyMTM0LDY5LjA1MjQ0YzAsMC4xMTkxOSAwLDAuMjM4MzggMCwwLjM1NzU3bDAsMC4xMTkxOWwwLDAuMTE5MTkiIGlkPSJzdmdfMyIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z180IiBoZWlnaHQ9IjAiIHdpZHRoPSIxLjMxMTA4IiB5PSI2LjgzNTUyIiB4PSItMC4wNDE3MSIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z181IiBoZWlnaHQ9IjEuNzg3ODQiIHdpZHRoPSIwLjExOTE5IiB5PSI2OC40NTY1IiB4PSIyOC45MjEzNCIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z182IiBoZWlnaHQ9IjQuODg2NzciIHdpZHRoPSIxOS4wNzAzMiIgeT0iNTEuMjkzMjEiIHg9IjM2LjY2ODY2IiBzdHJva2U9Im51bGwiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+'),
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CiA8Zz4KICA8dGl0bGU+TGF5ZXIgMTwvdGl0bGU+CiAgPHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoLTg5Ljc2MjQgNy4zMzAxNCA1NS4xMjUyKSIgc3Ryb2tlPSJudWxsIiBpZD0ic3ZnXzEiIGZpbGw9IiNGOEVBRTciIGQ9Im02Mi41NzQ0OSwxMTcuNTIwODZjLTU1LjIyOCwwIC0xMDAsLTQ0Ljc3MiAtMTAwLC0xMDBsMCwxMDBsMTAwLDB6IiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgogIDxwYXRoIGQ9Im0tMC42Mzc2Niw3LjMxMjI4YzAuMTE5MTksMCAwLjIxNzM3LDAuMDU3OTYgMC40NzY3NiwwLjExOTE5YzAuMjMyLDAuMDU0NzcgMC4yNzMyOSwwLjAzNDkxIDAuMzU3NTcsMC4xMTkxOWMwLjA4NDI4LDAuMDg0MjggMC4zNTc1NywwIDAuNDc2NzYsMGwwLjExOTE5LDBsMC4yMzgzOCwwIiBpZD0ic3ZnXzIiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxwYXRoIGQ9Im0yOC45MjEzNCw2OS4wNTI0NGMwLDAuMTE5MTkgMCwwLjIzODM4IDAsMC4zNTc1N2wwLDAuMTE5MTlsMCwwLjExOTE5IiBpZD0ic3ZnXzMiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNCIgaGVpZ2h0PSIwIiB3aWR0aD0iMS4zMTEwOCIgeT0iNi44MzU1MiIgeD0iLTAuMDQxNzEiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNSIgaGVpZ2h0PSIxLjc4Nzg0IiB3aWR0aD0iMC4xMTkxOSIgeT0iNjguNDU2NSIgeD0iMjguOTIxMzQiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNiIgaGVpZ2h0PSI0Ljg4Njc3IiB3aWR0aD0iMTkuMDcwMzIiIHk9IjUxLjI5MzIxIiB4PSIzNi42Njg2NiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiA8L2c+Cjwvc3ZnPg=='),
url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><rect rx='8' width='100%' height='100%' fill='%23F8EAE7'/></svg>");
-webkit-mask-size:
18px 30px,
20px 30px,
calc(100% - 30px) calc(100% + 17px);
-webkit-mask-position:
right bottom,
left bottom,
center top;
-webkit-mask-repeat: no-repeat;
}
.layout-navbars-tagsview-ul-li {
padding: 0 5px;
border-width: 15px 27px 15px;
border-style: solid;
border-color: transparent;
margin: 0 -15px;
.layout-icon-active {
display: none;
}
.layout-icon-three {
display: block;
}
&:hover {
@extend .tgs-style-three-svg;
background: var(--tagsview3-active-background-color);
color: unset;
}
}
.is-active {
@extend .tgs-style-three-svg;
background: var(--tagsview3-active-background-color) !important;
color: var(--el-color-primary) !important;
z-index: 1;
}
}
.layout-navbars-tagsview-ul .is-active .layout-navbars-tagsview-ul-li-refresh:hover {
background-color: var(--el-color-primary);
color: var(--el-color-white);
border-radius: 4px;
}
.layout-navbars-tagsview-shadow {

View File

@@ -98,8 +98,6 @@ export const useThemeConfig = defineStore('themeConfig', {
/* 其它设置
------------------------------- */
// 默认 Tagsview 风格,可选 1、 tags-style-one 2、 tags-style-two 3、 tags-style-three
tagsStyle: 'tags-style-three',
// 默认主页面切换动画,可选 1、 slide-right 2、 slide-left 3、 opacitys
animation: 'slide-right',
// 默认分栏高亮风格,可选 1、 圆角 columns-round 2、 卡片 columns-card
@@ -137,6 +135,7 @@ export const useThemeConfig = defineStore('themeConfig', {
appSlogan: 'common.appSlogan',
// 网站logo icon, base64编码内容
logoIcon: logoIcon,
version: 'latest',
// 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn
globalI18n: 'zh-cn',
// 默认全局组件大小,可选值"<|large|default|small>",默认 ''
@@ -155,12 +154,15 @@ export const useThemeConfig = defineStore('themeConfig', {
if (tc) {
this.themeConfig = tc;
document.documentElement.style.cssText = getLocal('themeConfigStyle');
} else {
getServerConf().then((res) => {
this.themeConfig.globalI18n = res.i18n;
});
}
getServerConf().then((res) => {
this.themeConfig.globalI18n = res.i18n;
this.themeConfig.version = res.version;
});
this.themeConfig.defaultListPageSize = calculatePageSizeByScreenHeight();
// 根据后台系统配置初始化
getSysStyleConfig().then((res) => {
if (res?.title) {
@@ -215,3 +217,34 @@ export const useThemeConfig = defineStore('themeConfig', {
},
},
});
// 计算每页显示数量的方法
const calculatePageSizeByScreenHeight = (): number => {
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
// 计算页面其他部分的高度(这是一个大概的估算)
// 包括顶部导航、面包屑、搜索区域、分页控件等
const headerHeight = 60; // 页面顶部导航高度
const subHeaderHeight = 50; // 子页面头部或其他内容高度
const searchFormHeight = 100; // 搜索表单高度,如果显示的话
const tableHeaderHeight = 44; // 表格头部高度
const paginationHeight = 40; // 分页控件高度
const paddingMarginHeight = 30; // 额外的内外边距
// 计算可用于表格内容的高度
const availableContentHeight =
windowHeight - headerHeight - subHeaderHeight - searchFormHeight - tableHeaderHeight - paginationHeight - paddingMarginHeight;
// 根据表格尺寸确定行高
const rowHeight = 40;
// 计算理论上的行数
const calculatedRows = Math.floor(availableContentHeight / rowHeight);
// 设置限制范围
const minPageSize = 10;
const maxPageSize = 30;
// 确保返回值在合理范围内,且至少有基本的行数
return Math.max(minPageSize, Math.min(maxPageSize, calculatedRows));
};

View File

@@ -40,7 +40,6 @@ declare interface ThemeConfigState {
isInvert: boolean;
isWatermark: boolean;
watermarkText: Array<string>;
tagsStyle: string;
animation: string;
columnsAsideStyle: string;
layout: string;
@@ -49,6 +48,7 @@ declare interface ThemeConfigState {
globalViceTitle: string;
appSlogan: string;
logoIcon: string;
version: string;
globalI18n: string;
globalComponentSize: string;
terminalTheme: string;

View File

@@ -27,16 +27,14 @@
</el-table-column>
<el-table-column :label="$t('flow.approvalRemark')" min-width="150">
<template #default="scope">
{{ scope.row.remark }}
</template>
<template #default="scope"> {{ scope.row.remark }} </template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane :label="$t('common.basic')" :name="basicTabName">
<el-form-item prop="auditRule" :label="$t('flow.aiAuditRule')">
<el-input v-model="form.auditRule" type="textarea" :rows="10" :placeholder="$t('flow.aiAuditRuleTip')" clearable />
<MonacoEditor class="w-full!" height="calc(100vh - 330px)" v-model="form.auditRule" language="markdown" />
</el-form-item>
</el-tab-pane>
</el-tabs>
@@ -45,6 +43,7 @@
import { notEmpty } from '@/common/assert';
import { formatDate } from '@/common/utils/format';
import EnumTag from '@/components/enumtag/EnumTag.vue';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { useI18nPleaseInput } from '@/hooks/useI18n';
import { ProcinstTaskStatus } from '@/views/flow/enums';
import { computed } from 'vue';

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div>
<div class="card !p-1 flex items-center justify-between">
<div class="card p-1! flex items-center justify-between">
<div>
<el-link @click="onRunSql()" underline="never" class="ml-3.5" icon="VideoPlay"> </el-link>
<el-divider direction="vertical" border-style="dashed" />
@@ -39,32 +39,32 @@
</div>
</div>
<el-splitter style="height: calc(100vh - 200px)" layout="vertical" @resize-end="onResizeTableHeight">
<el-splitter style="height: calc(100vh - 220px)" layout="vertical" @resize-end="onResizeTableHeight">
<el-splitter-panel :size="state.editorSize" max="80%">
<MonacoEditor ref="monacoEditorRef" class="mt-1" v-model="state.sql" language="sql" height="100%" :id="'MonacoTextarea-' + getKey()" />
</el-splitter-panel>
<el-splitter-panel>
<div class="sql-exec-res !h-full">
<div class="sql-exec-res h-full!">
<el-tabs
class="!h-full !w-full"
class="h-full! w-full!"
v-if="state.execResTabs.length > 0"
@tab-remove="onRemoveTab"
@tab-change="active"
v-model="state.activeTab"
>
<el-tab-pane class="!h-full" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
<el-tab-pane class="h-full!" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
<template #label>
<el-popover :show-after="1000" placement="top-start" :title="$t('db.execInfo')" trigger="hover" :width="300">
<template #reference>
<div>
<span>
<span v-if="dt.loading">
<SvgIcon class="!mb-0.5 is-loading" name="Loading" color="var(--el-color-primary)" />
<SvgIcon class="mb-0.5! is-loading" name="Loading" color="var(--el-color-primary)" />
</span>
<span v-else>
<SvgIcon class="!mb-0.5" v-if="!dt.errorMsg" name="CircleCheck" color="var(--el-color-success)" />
<SvgIcon class="!mb-0.5" v-if="dt.errorMsg" name="CircleClose" color="var(--el-color-error)" />
<SvgIcon class="mb-0.5!" v-if="!dt.errorMsg" name="CircleCheck" color="var(--el-color-success)" />
<SvgIcon class="mb-0.5!" v-if="dt.errorMsg" name="CircleClose" color="var(--el-color-error)" />
</span>
</span>
@@ -289,7 +289,7 @@ const onResizeTableHeight = (index: number, sizes: number[]) => {
editorHeight = plitpaneHeight / 2;
}
let tableDataHeight = plitpaneHeight - editorHeight - 43;
let tableDataHeight = plitpaneHeight - editorHeight - 47;
state.editorSize = editorHeight;
state.tableDataHeight = tableDataHeight + 'px';
@@ -332,6 +332,7 @@ const onRunSql = async (newTab = false) => {
* 执行多条SQL并合并结果
*/
const runMultipleSqls = async (sqls: string[], newTab: boolean) => {
state.execResTabs = [];
// 分类SQL语句
const nonQuerySqls: string[] = []; // 影响行数类SQL (UPDATE, INSERT, DELETE等)
const querySqls: string[] = []; // 查询类SQL (SELECT等)
@@ -399,7 +400,7 @@ const runNonQuerySqls = async (sqls: string[], newTab: boolean) => {
const result: any = (data.value as any)[0];
results.push({
sql: result.sql,
rowsAffected: result.res?.[0]?.rowsAffected,
rowsAffected: result.res?.[0].rowsAffected,
error: result.errorMsg || '-',
});
} catch (error: any) {
@@ -412,9 +413,9 @@ const runNonQuerySqls = async (sqls: string[], newTab: boolean) => {
// 设置表格列
state.execResTabs[i].tableColumn = [
{ columnName: 'sql', columnType: 'string', show: true },
{ columnName: 'rowsAffected', columnType: 'number', show: true },
{ columnName: 'error', columnType: 'string', show: true },
{ columnName: 'SQL', key: 'sql', columnType: 'string', show: true },
{ columnName: 'RowsAffected', key: 'rowsAffected', columnType: 'number', show: true },
{ columnName: 'Error', key: 'error', columnType: 'string', show: true },
];
state.execResTabs[i].data = results;
@@ -486,6 +487,7 @@ const runSql = async (sql: string, remark = '', newTab = false) => {
state.execResTabs[i].tableColumn = colAndData.columns.map((x: any) => {
return {
columnName: x.name,
key: x.key,
columnType: x.type,
show: true,
};

View File

@@ -39,7 +39,7 @@
<span v-if="column.dataTypeSubscript === 'icon-clock'">
<SvgIcon :size="9" name="Clock" style="cursor: unset" />
</span>
<span class="!text-[8px]" v-else>{{ column.dataTypeSubscript }}</span>
<span class="text-[8px]!" v-else>{{ column.dataTypeSubscript }}</span>
</div>
<div v-if="showColumnTip">
@@ -53,7 +53,7 @@
<div
v-if="dbConfig.showColumnComment"
style="color: var(--el-color-info-light-3)"
class="!text-[10px] el-text el-text--small is-truncated"
class="text-[10px]! el-text el-text--small is-truncated"
>
{{ column.columnComment }}
</div>
@@ -77,9 +77,7 @@
<!-- 排序箭头图标 -->
<SvgIcon
v-if="
column.title == nowSortColumn?.columnName &&
!showColumnActions[column.key] &&
!columnActionVisible[column.key]
column.key == nowSortColumn?.key && !showColumnActions[column.key] && !columnActionVisible[column.key]
"
:color="'var(--el-color-primary)'"
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
@@ -135,7 +133,7 @@
<div v-else @dblclick="onEnterEditMode(rowData, column, rowIndex, columnIndex)">
<div v-if="canEdit(rowIndex, columnIndex)">
<ColumnFormItem
v-model="rowData[column.dataKey!]"
v-model="rowData[column.key!]"
:data-type="column.dataType"
@blur="onExitEditMode(rowData, column, rowIndex)"
:column-name="column.columnName"
@@ -143,11 +141,11 @@
/>
</div>
<div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active ml-0.5 mr-0.5' : 'ml-0.5 mr-0.5'">
<span v-if="rowData[column.dataKey!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
<div v-else :class="isUpdated(rowIndex, column.key) ? 'update_field_active ml-0.5 mr-0.5' : 'ml-0.5 mr-0.5'">
<span v-if="rowData[column.key!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
<span v-else :title="rowData[column.dataKey!]" class="el-text el-text--small is-truncated">
{{ rowData[column.dataKey!] }}
<span v-else :title="rowData[column.key!]" class="el-text el-text--small is-truncated">
{{ rowData[column.key!] }}
</span>
</div>
</div>
@@ -160,7 +158,7 @@
<SvgIcon class="is-loading" name="loading" color="var(--el-color-primary)" :size="28" />
<el-text class="ml-1" tag="b">{{ $t('db.execTime') }} - {{ state.execTime.toFixed(1) }}s</el-text>
</div>
<div v-if="loading && abortFn" class="!mt-2">
<div v-if="loading && abortFn" class="mt-2!">
<el-button @click="cancelLoading" type="info" size="small" plain>{{ $t('common.cancel') }}</el-button>
</div>
</div>
@@ -275,7 +273,7 @@ const columnActionVisible = ref({} as any);
const cmDataCopyCell = new ContextmenuItem('copyValue', 'common.copy')
.withIcon('CopyDocument')
.withOnClick(async (data: any) => {
await copyToClipboard(data.rowData[data.column.dataKey]);
await copyToClipboard(data.rowData[data.column.key]);
})
.withHideFunc(() => {
// 选中多条则隐藏该复制按钮
@@ -303,16 +301,23 @@ const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
const cmDataGenJson = new ContextmenuItem('genJson', 'db.genJson').withIcon('tickets').withOnClick(() => onGenerateJson());
const cmDataExportCsv = new ContextmenuItem('exportCsv', 'db.exportCsv').withIcon('document').withOnClick(() => onExportCsv());
const cmDataExportCsv = new ContextmenuItem('exportCsv', 'db.exportCsv')
.withIcon('document')
.withOnClick(() => onExportCsv())
.withPermission('db:data:export');
const cmDataExportExcel = new ContextmenuItem('exportExcel', 'db.exportExcel').withIcon('document').withOnClick(() => onExportExcel());
const cmDataExportExcel = new ContextmenuItem('exportExcel', 'db.exportExcel')
.withIcon('document')
.withOnClick(() => onExportExcel())
.withPermission('db:data:export');
const cmDataExportSql = new ContextmenuItem('exportSql', 'db.exportSql')
.withIcon('document')
.withOnClick(() => onExportSql())
.withHideFunc(() => {
return state.table == '';
});
})
.withPermission('db:data:export');
class NowUpdateCell {
rowIndex: number;
@@ -402,7 +407,6 @@ const dbConfig = useStorage('dbConfig', DbThemeConfig);
const rowNoColumn = {
title: 'No.',
key: 'tableDataRowNo',
dataKey: 'tableDataRowNo',
width: 45,
fixed: true,
align: 'center',
@@ -508,8 +512,6 @@ const setTableColumns = (columns: any) => {
x.remark = `${x.columnType} ${x.columnComment ? ' | ' + x.columnComment : ''}`;
return {
...x,
key: columnName,
dataKey: columnName,
width: DbInst.flexColumnWidth(columnName, state.datas),
title: columnName,
align: x.dataType == DataType.Number ? 'right' : 'left',
@@ -558,21 +560,21 @@ const hideColumnAction = () => {
const handleColumnCommand = (column: any, command: string) => {
switch (command) {
case 'sort-asc':
onTableSortChange({ columnName: column.dataKey, order: 'asc' });
onTableSortChange({ key: column.key, order: 'asc' });
break;
case 'sort-desc':
onTableSortChange({ columnName: column.dataKey, order: 'desc' });
onTableSortChange({ key: column.key, order: 'desc' });
break;
case 'fix':
state.columns.forEach((col: any) => {
if (col.dataKey == column.dataKey) {
if (col.key == column.key) {
col.fixed = true;
}
});
break;
case 'unfix':
state.columns.forEach((col: any) => {
if (col.dataKey == column.dataKey) {
if (col.key == column.key) {
col.fixed = false;
}
});
@@ -711,7 +713,7 @@ const onGenerateJson = async () => {
let obj: any = {};
for (let column of state.columns) {
if (column.show) {
obj[column.title] = selectionData[column.dataKey];
obj[column.title] = selectionData[column.key];
}
}
jsonObj.push(obj);
@@ -768,7 +770,7 @@ const onEnterEditMode = (rowData: any, column: any, rowIndex = 0, columnIndex =
nowUpdateCell.value = {
rowIndex: rowIndex,
colIndex: columnIndex,
oldValue: rowData[column.dataKey],
oldValue: rowData[column.key],
dataType: column.dataType,
};
};
@@ -778,7 +780,7 @@ const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
return;
}
const oldValue = nowUpdateCell.value.oldValue;
const newValue = rowData[column.dataKey];
const newValue = rowData[column.key];
// 未改变单元格值
if (oldValue == newValue) {
@@ -793,7 +795,7 @@ const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
cellUpdateMap.value.set(rowIndex, updatedRow);
}
const columnName = column.dataKey;
const columnName = column.key;
let cellData = updatedRow.columnsMap.get(columnName);
if (cellData) {
// 多次修改情况,可能又修改回原值,则移除该修改单元格

View File

@@ -152,7 +152,7 @@
<el-text
id="copyValue"
style="color: var(--el-color-info-light-3)"
class="is-truncated !text-[12px] mt-1"
class="is-truncated text-[12px]! mt-1"
@click="copyToClipboard(sql)"
:title="sql"
>{{ sql }}</el-text
@@ -392,6 +392,7 @@ const selectData = async () => {
const columns = await getNowDbInst().loadColumns(props.dbName, props.tableName);
columns.forEach((x: any) => {
x.show = true;
x.key = x.columnName;
});
state.columns = columns;
}
@@ -592,7 +593,7 @@ const onSelectByCondition = async () => {
*/
const onTableSortChange = async (sort: any) => {
const sortType = sort.order == 'desc' ? 'DESC' : 'ASC';
state.orderBy = `ORDER BY ${state.dbDialect.quoteIdentifier(sort.columnName)} ${sortType}`;
state.orderBy = `ORDER BY ${state.dbDialect.quoteIdentifier(sort.key)} ${sortType}`;
await onRefresh();
};

View File

@@ -3,7 +3,9 @@
<el-row class="mb-1">
<el-popover v-model:visible="state.dumpInfo.visible" trigger="click" :width="470" placement="right">
<template #reference>
<el-button :disabled="state.dumpInfo.tables?.length == 0" class="ml-1" type="success" size="small">{{ $t('db.dump') }}</el-button>
<el-button v-auth="'db:data:export'" :disabled="state.dumpInfo.tables?.length == 0" class="ml-1" type="success" size="small">
{{ $t('db.dump') }}
</el-button>
</template>
<el-form-item :label="$t('db.exportContent')">
<el-radio-group v-model="dumpInfo.type">

View File

@@ -86,13 +86,13 @@
@tab-remove="onRemoveTab"
@tab-change="onTabChange"
v-model="state.activeName"
class="!h-full w-full"
class="h-full! w-full"
>
<el-tab-pane class="!h-full" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
<el-tab-pane class="h-full!" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
<template #label>
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
<template #reference>
<span @contextmenu.prevent="onTabContextmenu(dt, $event)" class="!text-[12px]">{{ dt.label }}</span>
<span @contextmenu.prevent="onTabContextmenu(dt, $event)" class="text-[12px]!">{{ dt.label }}</span>
</template>
<template #default>
<el-descriptions :column="1" size="small">

View File

@@ -59,8 +59,8 @@
</el-form-item>
<el-form-item v-if="form.mode === 2">
<el-row class="w-full!">
<el-col :span="12">
<el-row :gutter="10">
<el-col :span="10">
<el-form-item prop="targetFileDbType" :label="$t('db.dbFileType')" :required="form.mode === 2">
<el-select v-model="form.targetFileDbType" clearable filterable>
<el-option
@@ -79,7 +79,13 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="6">
<el-form-item :label="$t('db.fileType')">
<el-select v-model="form.extra.fileType" :options="fileTypeOptions"> </el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('db.fileSaveDays')">
<el-input-number v-model="form.fileSaveDays" :min="-1" :max="1000">
<template #suffix>
@@ -138,10 +144,8 @@
</el-form>
<template #footer>
<div>
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
</div>
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
</template>
</el-drawer>
</div>
@@ -187,6 +191,11 @@ const rules = {
cron: [Rules.requiredSelect('cron')],
};
const fileTypeOptions = [
{ label: '.sql', value: 'sql' },
{ label: '.zip', value: 'zip' },
];
const dbForm: any = ref(null);
type FormData = {
@@ -215,6 +224,7 @@ type FormData = {
deleteTable?: 1 | 2;
checkedKeys: string;
runningState: 1 | 2;
extra: { fileType: string };
};
const basicFormData = {
@@ -226,6 +236,7 @@ const basicFormData = {
deleteTable: 1,
checkedKeys: '',
runningState: 1,
extra: { fileType: fileTypeOptions[0].value },
} as FormData;
const srcTableList = ref<{ tableName: string; tableComment: string }[]>([]);
@@ -264,6 +275,7 @@ watch(dialogVisible, async (newValue: boolean) => {
if (!newValue) {
return;
}
const propsData = props.data as any;
if (!propsData?.id) {
let d = {} as FormData;
@@ -275,8 +287,8 @@ watch(dialogVisible, async (newValue: boolean) => {
return;
}
state.form = deepClone(props.data) as FormData;
let { srcDbId, targetDbId } = state.form;
const form = deepClone(props.data) as FormData;
let { srcDbId, targetDbId } = form;
// 初始化src数据源
if (srcDbId) {
@@ -301,11 +313,14 @@ watch(dialogVisible, async (newValue: boolean) => {
}
// 初始化勾选迁移表
srcTreeRef.value.setCheckedKeys(state.form.checkedKeys.split(','));
srcTreeRef.value.setCheckedKeys(form.checkedKeys.split(','));
// 初始化默认值
state.form.cronAble = state.form.cronAble || 0;
state.form.mode = state.form.mode || 1;
form.cronAble = form.cronAble || -1;
form.mode = form.mode || 1;
form.extra = form.extra || { fileType: fileTypeOptions[0].value };
state.form = form;
});
watch(

View File

@@ -12,8 +12,10 @@
lazy
>
<template #tableHeader>
<el-button type="primary" icon="plus" @click="editContainerConf(false)" plain>{{ $t('common.create') }}</el-button>
<el-button type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteConf" plain>{{ $t('common.delete') }}</el-button>
<el-button v-auth="'container:save'" type="primary" icon="plus" @click="editContainerConf(false)" plain>{{ $t('common.create') }}</el-button>
<el-button v-auth="'container:del'" type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteConf" plain>
{{ $t('common.delete') }}
</el-button>
</template>
<template #tagPath="{ data }">
@@ -22,7 +24,7 @@
<template #action="{ data }">
<el-button @click="showDetail(data)" link>{{ $t('common.detail') }}</el-button>
<el-button type="primary" link @click="editContainerConf(data)">{{ $t('common.edit') }}</el-button>
<el-button v-auth="'container:save'" type="primary" link @click="editContainerConf(data)">{{ $t('common.edit') }}</el-button>
</template>
</page-table>

View File

@@ -1,19 +1,19 @@
<template>
<div class="file-manage">
<el-dialog :title="$t('machine.process')" v-model="dialogVisible" :destroy-on-close="true" :show-close="true" :before-close="handleClose" width="65%">
<div class="card !p-1">
<div class="card p-1!">
<el-row>
<el-col :span="4">
<el-input size="small" :placeholder="$t('machine.processName')" v-model="params.name" plain clearable></el-input>
</el-col>
<el-col :span="4" class="ml-1">
<el-select class="!w-full" @change="getProcess" size="small" v-model="params.sortType" :placeholder="$t('machine.selectSortType')">
<el-select @change="getProcess" size="small" v-model="params.sortType" :placeholder="$t('machine.selectSortType')">
<el-option key="cpu" :label="$t('machine.cpuDesc')" value="1"> </el-option>
<el-option key="cpu" :label="$t('machine.memDesc')" value="2"> </el-option>
</el-select>
</el-col>
<el-col :span="4" class="ml-1">
<el-select class="!w-full" @change="getProcess" size="small" v-model="params.count" :placeholder="$t('machine.selectProcessNum')">
<el-select @change="getProcess" size="small" v-model="params.count" :placeholder="$t('machine.selectProcessNum')">
<el-option key="10" label="10" value="10"> </el-option>
<el-option key="15" label="15" value="15"> </el-option>
<el-option key="20" label="20" value="20"> </el-option>
@@ -26,7 +26,7 @@
</el-row>
</div>
<el-table :data="processList" size="small" style="width: 100%">
<el-table :data="processList" size="small" :height="500">
<el-table-column prop="user" label="USER" :min-width="50"> </el-table-column>
<el-table-column prop="pid" label="PID" :min-width="50" show-overflow-tooltip></el-table-column>
<el-table-column prop="cpu" label="%CPU" :min-width="40"> </el-table-column>

View File

@@ -70,5 +70,5 @@ export function getMachineTerminalSocketUrl(authCertName: any) {
}
export function getMachineRdpSocketUrl(authCertName: any) {
return `/machines/rdp/${authCertName}`;
return `/api/machines/rdp/${authCertName}`;
}

View File

@@ -741,7 +741,11 @@ function getParentPath(filePath: string) {
const deleteFile = async (files: any) => {
try {
await useI18nDeleteConfirm(files.map((x: any) => `[${x.path}]`).join('\n'));
let confirmMsg = files.map((x: any) => `[${x.path}]`).join('\n');
if (confirmMsg.length > 400) {
confirmMsg = confirmMsg.substring(0, 400) + '...';
}
await useI18nDeleteConfirm(confirmMsg);
state.loading = true;
await machineApi.rmFile.request({
fileId: props.fileId,

View File

@@ -1,7 +1,7 @@
<template>
<div class="h-full machine-terminal-tabs">
<el-tabs v-if="state.tabs.size > 0" type="card" @tab-remove="onRemoveTab" v-model="state.activeTermName" class="!h-full w-full">
<el-tab-pane class="!h-full flex flex-col" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
<el-tab-pane class="h-full! flex flex-col" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
<template #label>
<el-popconfirm @confirm="handleReconnect(dt, true)" :title="$t('machine.reConnTips')">
<template #reference>

View File

@@ -85,12 +85,18 @@ const NodeTypeAuthCert = new NodeType(12)
(await node.ctx?.addResourceComponent(MachineOpComp)).openTerminal(node.params);
})
.withContextMenuItems([
new ContextmenuItem('term', 'machine.openTerminal').withIcon('Monitor').withOnClick(async (node: TagTreeNode) => {
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params);
}),
new ContextmenuItem('term-ex', 'machine.newTabOpenTerminal').withIcon('Monitor').withOnClick(async (node: TagTreeNode) => {
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params, true);
}),
new ContextmenuItem('term', 'machine.openTerminal')
.withIcon('Monitor')
.withPermission('machine:terminal')
.withOnClick(async (node: TagTreeNode) => {
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params);
}),
new ContextmenuItem('term-ex', 'machine.newTabOpenTerminal')
.withIcon('Monitor')
.withPermission('machine:terminal')
.withOnClick(async (node: TagTreeNode) => {
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params, true);
}),
new ContextmenuItem('files', 'machine.fileManage').withIcon('FolderOpened').withOnClick(async (node: any) => {
(await node.ctx?.addResourceComponent(MachineOpComp)).showFileManage(node.params);
}),

View File

@@ -12,8 +12,10 @@
lazy
>
<template #tableHeader>
<el-button type="primary" icon="plus" @click="editMongo(false)" plain>{{ $t('common.create') }}</el-button>
<el-button type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteMongo" plain>{{ $t('common.delete') }}</el-button>
<el-button v-auth="'mongo:save'" type="primary" icon="plus" @click="editMongo(false)" plain>{{ $t('common.create') }}</el-button>
<el-button v-auth="'mongo:del'" type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteMongo" plain>
{{ $t('common.delete') }}
</el-button>
</template>
<template #tagPath="{ data }">
@@ -25,7 +27,7 @@
<el-button @click="showUsers(data.id)" link type="success">cmd</el-button>
<el-button @click="editMongo(data)" link type="primary">{{ $t('common.edit') }}</el-button>
<el-button v-auth="'mongo:save'" @click="editMongo(data)" link type="primary">{{ $t('common.edit') }}</el-button>
</template>
</page-table>

View File

@@ -12,8 +12,10 @@
lazy
>
<template #tableHeader>
<el-button type="primary" icon="plus" @click="editRedis(false)" plain>{{ $t('common.create') }}</el-button>
<el-button type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteRedis" plain>{{ $t('common.delete') }}</el-button>
<el-button v-auth="'redis:save'" type="primary" icon="plus" @click="editRedis(false)" plain>{{ $t('common.create') }}</el-button>
<el-button v-auth="'redis:del'" type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteRedis" plain>
{{ $t('common.delete') }}
</el-button>
</template>
<template #tagPath="{ data }">
@@ -27,7 +29,7 @@
<el-button @click="onShowClusterInfo(data)" v-if="data.mode === 'cluster'" type="primary" link>{{ $t('redis.clusterInfo') }}</el-button>
<el-button @click="showDetail(data)" link>{{ $t('common.detail') }}</el-button>
<el-button type="primary" link @click="editRedis(data)">{{ $t('common.edit') }}</el-button>
<el-button v-auth="'redis:save'" type="primary" link @click="editRedis(data)">{{ $t('common.edit') }}</el-button>
</template>
</page-table>

View File

@@ -32,7 +32,7 @@
<template #dropdown>
<el-dropdown-menu>
<template v-for="item in contextMenuItems" :key="item.clickId">
<el-dropdown-item v-if="!item.isHide(props.data)" :command="item">
<el-dropdown-item v-if="!item.isHide(props.data) && hasPerm(item.permission)" :command="item">
<SvgIcon v-if="item.icon" :name="item.icon" class="mr-1" />{{ $t(item.txt) }}
</el-dropdown-item>
</template>
@@ -54,6 +54,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
import { ContextmenuItem } from '@/components/contextmenu';
import { ResourceOpCtx, TagTreeNode } from '@/views/ops/component/tag';
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
import { hasPerm } from '@/components/auth/auth';
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey, undefined);

View File

@@ -2,7 +2,7 @@
<div class="h-full">
<el-splitter @resize="onResizeOpPanel">
<el-splitter-panel size="24%" max="40%">
<el-card class="h-full flex" body-class="bg-(--el-bg-color) !p-0 flex flex-col w-full">
<el-card class="h-full flex" body-class="!p-0 flex flex-col w-full">
<div class="tag-tree-header flex justify-between items-center">
<el-input v-model="filterText" :placeholder="$t('tag.tagFilterPlaceholder')" clearable size="small" class="tag-tree-search w-full">
<template #prefix>

View File

@@ -375,6 +375,7 @@ const allowDrop = (draggingNode: any, dropNode: any, type: any) => {
// 只有权限节点可移动至菜单节点下 或者移动菜单
return (
(draggingNode.data.type == permissionTypeValue && dropNode.data.type == menuTypeValue) ||
(draggingNode.data.type == permissionTypeValue && dropNode.data.type == permissionTypeValue) ||
(draggingNode.data.type == menuTypeValue && dropNode.data.type == menuTypeValue)
);
}

View File

@@ -18,20 +18,19 @@ jwt:
expire-time: 720
# refreshToken过期时间单位分钟
refresh-token-expire-time: 4320
# 资源密码aes加密key
aes:
key: 1111111111111111
# 若存在mysql配置优先使用mysql
mysql:
host: mysql:3306
# 数据库配置dialect支持mysql、sqlite
db:
dialect: mysql
address: mysql:3306
name: mayfly-go
username: root
password: 111049
db-name: mayfly-go
config: charset=utf8&loc=Local&parseTime=true
max-idle-conns: 5
sqlite:
path: ./mayfly-go.sqlite
max-idle-conns: 5
# db:
# dialect: sqlite
# address: ./mayfly-go.db
# max-idle-conns: 5
# 若同时部署多台机器则需要配置redis信息用于缓存权限码、验证码、公私钥等
# redis:
# host: localhost
@@ -55,3 +54,6 @@ log:
# max-age: 60
# # 是否使用 gzip 压缩方式压缩轮转后的日志文件
# compress: true
# 资源密码aes加密key
aes:
key: 1111111111111111

View File

@@ -6,8 +6,8 @@ require (
gitee.com/chunanyong/dm v1.8.22
gitee.com/liuzongyang/libpq v1.10.11
github.com/antlr4-go/antlr/v4 v4.13.1
github.com/cloudwego/eino v0.7.13
github.com/cloudwego/eino-ext/components/model/openai v0.1.6
github.com/cloudwego/eino v0.7.32
github.com/cloudwego/eino-ext/components/model/openai v0.1.8
github.com/docker/docker v28.5.0+incompatible
github.com/docker/go-connections v0.6.0
github.com/gin-gonic/gin v1.11.0
@@ -18,17 +18,17 @@ require (
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.30.1
github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20250930013652-2d71241a3bb9
github.com/microsoft/go-mssqldb v1.9.3
github.com/microsoft/go-mssqldb v1.9.6
github.com/mojocn/base64Captcha v1.3.8 //
github.com/opencontainers/image-spec v1.1.1
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.10
github.com/pquerna/otp v1.5.0
github.com/redis/go-redis/v9 v9.17.2
github.com/redis/go-redis/v9 v9.17.3
github.com/robfig/cron/v3 v3.0.1 //
github.com/sijms/go-ora/v2 v2.9.0
github.com/spf13/cast v1.10.0
@@ -36,7 +36,7 @@ require (
github.com/tidwall/gjson v1.18.0
github.com/veops/go-ansiterm v0.0.5
go.mongodb.org/mongo-driver/v2 v2.3.0 // mongo
golang.org/x/crypto v0.46.0 // ssh
golang.org/x/crypto v0.47.0 // ssh
golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
@@ -59,7 +59,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.10 // indirect
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.13 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
@@ -109,6 +109,7 @@ require (
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
github.com/tidwall/match v1.2.0 // indirect
@@ -132,11 +133,11 @@ require (
golang.org/x/arch v0.21.0 // indirect
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
golang.org/x/image v0.31.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect

View File

@@ -2,51 +2,42 @@ package agent
import (
"context"
"errors"
"io"
"mayfly-go/internal/ai/config"
aimodel "mayfly-go/internal/ai/model"
"mayfly-go/pkg/logx"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/flow/agent"
"github.com/cloudwego/eino/flow/agent/react"
"github.com/cloudwego/eino/schema"
)
// GetAiAgent 获取AI Agent
func GetAiAgent(ctx context.Context, aiConfig *config.AIModelConfig, tools ...tool.BaseTool) (*react.Agent, error) {
aiModel := aimodel.GetAIModelByConfig(aiConfig)
if aiModel == nil {
return nil, errors.New("no supported AI model found")
}
toolableChatModel, err := aiModel.GetChatModel(ctx, aiConfig)
// GetAgent 获取AI Agent
func GetAgent(ctx context.Context, aiConfig *config.AIModelConfig, tools ...tool.BaseTool) (adk.Agent, error) {
toolableChatModel, err := aimodel.GetChatModel(ctx, aiConfig)
if err != nil {
return nil, err
}
// 初始化所需的 tools
toolsConf := compose.ToolsNodeConfig{
Tools: tools,
}
// 创建 agent
return react.NewAgent(ctx, &react.AgentConfig{
ToolCallingModel: toolableChatModel,
ToolsConfig: toolsConf,
MaxStep: len(toolsConf.Tools)*1 + 3,
MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message {
return input
},
toolsConfig := adk.ToolsConfig{}
toolsConfig.Tools = tools
chatAgent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "ops_expert",
Description: "一位拥有20多年系统管理、数据库管理和基础设施优化经验的专业DevOps专家。",
Instruction: `你现在是一位专业的数据库管理员、Redis管理员和安全审核专家请根据用户的问题给出最合适的答案。`,
Model: toolableChatModel,
ToolsConfig: toolsConfig,
})
if err != nil {
return nil, err
}
return chatAgent, nil
}
type AiAgent struct {
*react.Agent
}
// NewAiAgent 创建AI Agent并注册指定类型的工具
func NewAiAgent(ctx context.Context, toolTypes ...ToolType) (*AiAgent, error) {
// GetOpsExpertAgent 获取运维专家agent
func GetOpsExpertAgent(ctx context.Context, toolTypes ...ToolType) (*AiAgent, error) {
tools := make([]tool.BaseTool, 0)
for _, toolType := range toolTypes {
if t, exists := GetTools(toolType); exists {
@@ -54,7 +45,7 @@ func NewAiAgent(ctx context.Context, toolTypes ...ToolType) (*AiAgent, error) {
}
}
agent, err := GetAiAgent(ctx, config.GetAiModel(), tools...)
agent, err := GetAgent(ctx, config.GetAiModel(), tools...)
if err != nil {
return nil, err
}
@@ -63,84 +54,52 @@ func NewAiAgent(ctx context.Context, toolTypes ...ToolType) (*AiAgent, error) {
}, nil
}
// Chat 聊天,返回消息流通道
func (aiAgent *AiAgent) Chat(ctx context.Context, sysPrompt string, question string) (chan *schema.Message, chan error) {
ch := make(chan *schema.Message, 512)
errCh := make(chan error, 1)
type AiAgent struct {
adk.Agent
}
// Run 运行,并返回最终结果
func (aiAgent *AiAgent) Run(ctx context.Context, sysPrompt string, question string) (string, error) {
if sysPrompt == "" {
sysPrompt = "你现在是一位拥有20年实战经验的顶级系统运维专家精通Linux操作系统、数据库管理如MySQL、PostgreSQL、NoSQL数据库如Redis、MongoDB以及搜索引擎如Elasticsearch。"
}
agentOption := []agent.AgentOption{}
runner := adk.NewRunner(ctx, adk.RunnerConfig{
EnableStreaming: false,
Agent: aiAgent.Agent,
CheckPointStore: NewInMemoryStore(),
})
go func() {
defer close(ch)
defer close(errCh)
sr, err := aiAgent.Stream(ctx, []*schema.Message{
{
Role: schema.System,
Content: sysPrompt,
},
{
Role: schema.User,
Content: question,
},
}, agentOption...)
if err != nil {
errCh <- err // 将错误发送到错误通道
return
}
defer sr.Close()
iter := runner.Run(ctx, []adk.Message{
{
Role: schema.System,
Content: sysPrompt,
},
{
Role: schema.User,
Content: question,
},
})
for {
msg, err := sr.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
logx.Errorf("failed to recv response: %v", err)
break
}
// logx.Debugf("stream: %s", msg.String())
ch <- msg
}
}()
return ch, errCh
}
// GetChatMsg 获取完整的聊天回复内容
func (aiAgent *AiAgent) GetChatMsg(ctx context.Context, sysPrompt string, question string) (string, error) {
msgChan, errChan := aiAgent.Chat(ctx, sysPrompt, question)
res := ""
// 使用 select 同时监听消息通道和错误通道
for {
select {
case msg, ok := <-msgChan:
if !ok {
// 消息通道已关闭,说明正常结束
// 检查错误通道是否有错误
select {
case err := <-errChan:
if err != nil {
return "", err
}
default:
return res, nil
}
return res, nil
}
res += msg.Content
case err := <-errChan:
// 优先检查错误通道
if err != nil {
return "", err
}
case <-ctx.Done():
// 上下文被取消
return "", ctx.Err()
event, ok := iter.Next()
if !ok {
break
}
err := event.Err
if err != nil {
logx.Error(err.Error())
return res, err
}
LogEvent(event)
msg := event.Output.MessageOutput.Message
if msg != nil {
res = msg.Content
}
}
return res, nil
}

View File

@@ -0,0 +1,121 @@
package agent
import (
"fmt"
"io"
"log"
"mayfly-go/pkg/logx"
"strings"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/schema"
)
func LogEvent(event *adk.AgentEvent) {
logx.Debugf("agent name: %s, path: %s", event.AgentName, event.RunPath)
if event.Output != nil && event.Output.MessageOutput != nil {
if m := event.Output.MessageOutput.Message; m != nil {
if len(m.Content) > 0 {
if m.Role == schema.Tool {
logx.Debugf("agent tool response: %s", m.Content)
} else {
logx.Debugf("agent answer: %s", m.Content)
}
}
if len(m.ToolCalls) > 0 {
for _, tc := range m.ToolCalls {
logx.Debugf("agent tool name: %s", tc.Function.Name)
logx.Debugf("agent tool arguments: %s", tc.Function.Arguments)
}
}
} else if s := event.Output.MessageOutput.MessageStream; s != nil {
toolMap := map[int][]*schema.Message{}
var contentStart bool
charNumOfOneRow := 0
maxCharNumOfOneRow := 120
for {
chunk, err := s.Recv()
if err != nil {
if err == io.EOF {
break
}
logx.Debugf("agent error: %v", err)
return
}
if chunk.Content != "" {
if !contentStart {
contentStart = true
if chunk.Role == schema.Tool {
logx.Debugf("agent tool response: ")
} else {
logx.Debugf("agent answer: ")
}
}
charNumOfOneRow += len(chunk.Content)
if strings.Contains(chunk.Content, "\n") {
charNumOfOneRow = 0
} else if charNumOfOneRow >= maxCharNumOfOneRow {
logx.Debugf("\n")
charNumOfOneRow = 0
}
logx.Debugf("%v", chunk.Content)
}
if len(chunk.ToolCalls) > 0 {
for _, tc := range chunk.ToolCalls {
index := tc.Index
if index == nil {
logx.Error("index is nil")
}
toolMap[*index] = append(toolMap[*index], &schema.Message{
Role: chunk.Role,
ToolCalls: []schema.ToolCall{
{
ID: tc.ID,
Type: tc.Type,
Index: tc.Index,
Function: schema.FunctionCall{
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
},
},
},
})
}
}
}
for _, msgs := range toolMap {
m, err := schema.ConcatMessages(msgs)
if err != nil {
log.Fatalf("ConcatMessage failed: %v", err)
return
}
logx.Debugf("agent tool name: %s", m.ToolCalls[0].Function.Name)
logx.Debugf("agent tool arguments: %s", m.ToolCalls[0].Function.Arguments)
}
}
}
if event.Action != nil {
if event.Action.TransferToAgent != nil {
logx.Debugf("agent action: transfer to %v", event.Action.TransferToAgent.DestAgentName)
}
if event.Action.Interrupted != nil {
for _, ic := range event.Action.Interrupted.InterruptContexts {
str, ok := ic.Info.(fmt.Stringer)
if ok {
logx.Debugf("\n%s", str.String())
} else {
logx.Debugf("\n%v", ic.Info)
}
}
}
if event.Action.Exit {
logx.Debugf("agent action: exit")
}
}
if event.Err != nil {
logx.Debugf("agent error: %v", event.Err)
}
}

View File

@@ -0,0 +1,27 @@
package agent
import (
"context"
"github.com/cloudwego/eino/compose"
)
func NewInMemoryStore() compose.CheckPointStore {
return &inMemoryStore{
mem: map[string][]byte{},
}
}
type inMemoryStore struct {
mem map[string][]byte
}
func (i *inMemoryStore) Set(ctx context.Context, key string, value []byte) error {
i.mem[key] = value
return nil
}
func (i *inMemoryStore) Get(ctx context.Context, key string) ([]byte, bool, error) {
v, ok := i.mem[key]
return v, ok, nil
}

View File

@@ -0,0 +1,101 @@
package agent
import (
"bytes"
"errors"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/jsonx"
"regexp"
"strings"
)
// ParseLLMJSON 尝试从大模型输出中解析 JSON
func ParseLLMJSON[T any](raw string) (*T, error) {
candidates := extractJSONCandidates(raw)
var lastErr error
for _, c := range candidates {
if v, err := jsonx.To[T](c); err == nil {
return v, nil
} else {
lastErr = err
}
}
if lastErr == nil {
lastErr = errors.New("no json candidate found")
}
return nil, lastErr
}
// ParseLLMJSON2Map 解析 LLM 返回的JSON为map
func ParseLLMJSON2Map(raw string) (collx.M, error) {
if res, err := ParseLLMJSON[collx.M](raw); err != nil {
return nil, err
} else {
return *res, nil
}
}
func extractJSONCandidates(raw string) []string {
var results []string
text := strings.TrimSpace(raw)
// 1. 优先提取 code block 中的 JSON对象 or 数组)
codeBlockRe := regexp.MustCompile(
"(?s)```(?:json)?\\s*([\\[{].*?[\\]}])\\s*```",
)
matches := codeBlockRe.FindAllStringSubmatch(text, -1)
for _, m := range matches {
results = append(results, strings.TrimSpace(m[1]))
}
// 2. 如果没找到 code block尝试从全文裁剪 JSON
if len(results) == 0 {
if clipped := clipJSONValue(text); clipped != "" {
results = append(results, clipped)
}
}
return results
}
func clipJSONValue(s string) string {
objIdx := strings.Index(s, "{")
arrIdx := strings.Index(s, "[")
start := -1
var open, close byte
switch {
case objIdx != -1 && (arrIdx == -1 || objIdx < arrIdx):
start = objIdx
open, close = '{', '}'
case arrIdx != -1:
start = arrIdx
open, close = '[', ']'
default:
return ""
}
var buf bytes.Buffer
depth := 0
for i := start; i < len(s); i++ {
ch := s[i]
buf.WriteByte(ch)
switch ch {
case open:
depth++
case close:
depth--
if depth == 0 {
return buf.String()
}
}
}
return ""
}

View File

@@ -0,0 +1,108 @@
package agent
import (
"testing"
)
// TestParseLLMJSON 测试 ParseLLMJSON 函数
func TestParseLLMJSON(t *testing.T) {
// 定义测试用例结构体
tests := []struct {
name string
input string
expected any
hasError bool
}{
{
name: "Valid JSON Object",
input: "```json\n{\n \"name\": \"Alice\",\n \"age\": \"30\"\n}\n```",
expected: map[string]any{
"name": "Alice",
"age": "30",
},
hasError: false,
},
{
name: "Valid JSON Object",
input: "```\n{\n \"name\": \"Alice\",\n \"age\": \"40\"\n}\n```",
expected: map[string]any{
"name": "Alice",
"age": "40",
},
hasError: false,
},
{
name: "Valid JSON Object",
input: "aaabbbccc```\n{\n \"name\": \"Alice\",\n \"age\": \"50\"\n}\n```dddd",
expected: map[string]any{
"name": "Alice",
"age": "50",
},
hasError: false,
},
{
name: "Valid JSON Array",
input: "```json\n[\n {\"id\": \"1\", \"value\": \"foo\"},\n {\"id\": \"2\", \"value\": \"bar\"}\n]\n```",
expected: []map[string]any{
{"id": "1", "value": "foo"},
{"id": "2", "value": "bar"},
},
hasError: false,
},
{
name: "Valid JSON Array",
input: "aaaa```json\n[\n {\"id\": \"11\", \"value\": \"foo\"},\n {\"id\": \"22\", \"value\": \"bar\"}\n]\n```",
expected: []map[string]any{
{"id": "11", "value": "foo"},
{"id": "22", "value": "bar"},
},
hasError: false,
},
{
name: "Invalid JSON Format",
input: "This is not a valid JSON",
expected: nil,
hasError: true,
},
{
name: "Empty Input",
input: "",
expected: nil,
hasError: true,
},
}
// 执行测试用例
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var result any
var err error
// 根据 expected 类型调用不同的 ParseLLMJSON 方法
switch tt.expected.(type) {
case map[string]any:
result, err = ParseLLMJSON[map[string]any](tt.input)
case []map[string]any:
result, err = ParseLLMJSON[[]map[string]any](tt.input)
default:
result, err = ParseLLMJSON[any](tt.input)
}
// 验证错误情况
if tt.hasError {
if err == nil {
t.Errorf("expected an error but got none")
}
return
}
// 验证无错误情况下的结果
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
t.Logf("%v", result)
})
}
}

View File

@@ -2,7 +2,6 @@ package api
import (
"fmt"
"mayfly-go/internal/ai/prompt"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/req"
@@ -116,16 +115,5 @@ func (a *AiDB) GenerateSql(rc *req.Ctx) {
func generateSqlPrompt(dbType, text string, tables []string) string {
// 使用prompt包中的GetPrompt函数获取提示词模板
// 如果没有找到模板,则使用默认模板
tableStr := strings.Join(tables, ", ")
promptTemplate := prompt.GetPrompt("SQL_GENERATE", dbType, tableStr)
if promptTemplate == "" {
promptTemplate = "你是一位专业的SQL开发工程师请根据用户的自然语言描述生成符合%s语法的SQL语句。\n"
if len(tables) > 0 {
promptTemplate += "相关表名:" + tableStr + "\n"
}
promptTemplate += "请确保生成的SQL语句语法正确仅返回SQL语句不要包含其他解释内容。"
promptTemplate = fmt.Sprintf(promptTemplate, dbType)
}
return promptTemplate
return ""
}

View File

@@ -2,12 +2,12 @@ package init
import (
"mayfly-go/internal/ai/api"
"mayfly-go/initialize"
"mayfly-go/pkg/starter"
)
func init() {
// 注册AI模块的IoC组件
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
api.InitIoc()
})
}

View File

@@ -2,17 +2,28 @@ package model
import (
"context"
"errors"
"fmt"
"mayfly-go/internal/ai/config"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/collx"
"github.com/cloudwego/eino/components/model"
)
// 定义响应格式常量
const (
ResponseFormatJSON = "json_object"
ResponseFormatText = "text"
)
func init() {
RegisterAIModel(new(Openai))
}
var (
aiModelMap = map[string]AIModel{}
chatModels = collx.SM[string,model.ToolCallingChatModel]{}
)
type AIModel interface {
@@ -20,8 +31,8 @@ type AIModel interface {
// SupportModel 支持的模型
SupportModel() string
// GetChatModel 获取聊天模型
GetChatModel(ctx context.Context, aiConfig *config.AIModelConfig) (model.ToolCallingChatModel, error)
// NewChatModel 创建chat模型
NewChatModel(ctx context.Context, aiConfig *config.AIModelConfig) (model.ToolCallingChatModel, error)
}
// RegisterAIModel 注册AI模型
@@ -38,3 +49,41 @@ func GetAIModel(name string) AIModel {
func GetAIModelByConfig(aiConfig *config.AIModelConfig) AIModel {
return GetAIModel(aiConfig.ModelType)
}
// GetChatModel 获取Chat模型
func GetChatModel(ctx context.Context, aiConfig *config.AIModelConfig) (model.ToolCallingChatModel, error) {
aiModel := GetAIModelByConfig(aiConfig)
if aiModel == nil {
return nil, errors.New("no supported AI model found")
}
cacheKey := generateCacheKey(aiConfig)
if chatModel, ok := chatModels.Load(cacheKey); ok {
logx.Debugf("ai model [%s/%s] - get chat model from cache", aiConfig.ModelType, aiConfig.Model)
return chatModel, nil
}
// 删除已存在的缓存
chatModels.Clear()
chatModel, err := aiModel.NewChatModel(ctx, aiConfig)
if err != nil {
return nil, err
}
logx.Debugf("ai model [%s/%s] - new chat model", aiConfig.ModelType,aiConfig.Model)
chatModels.Store(cacheKey, chatModel)
return chatModel, nil
}
// generateCacheKey 生成基于 aiConfig 关键字段的缓存键
func generateCacheKey(config *config.AIModelConfig) string {
return fmt.Sprintf("%s_%s_%s_%s_%d_%f",
config.ModelType,
config.Model,
config.BaseUrl,
config.ApiKey,
config.MaxTokens,
config.Temperature,
)
}

View File

@@ -16,7 +16,7 @@ func (o *Openai) SupportModel() string {
return "openai"
}
func (o *Openai) GetChatModel(ctx context.Context, aiConfig *config.AIModelConfig) (model.ToolCallingChatModel, error) {
func (o *Openai) NewChatModel(ctx context.Context, aiConfig *config.AIModelConfig) (model.ToolCallingChatModel, error) {
return openai.NewChatModel(ctx, &openai.ChatModelConfig{
BaseURL: aiConfig.BaseUrl,
Model: aiConfig.Model,

View File

@@ -2,51 +2,20 @@ package prompt
import (
"embed"
"fmt"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/stringx"
"strings"
)
const (
FLOW_BIZ_AUDIT = "FLOW_BIZ_AUDIT"
SQL_GENERATE = "SQL_GENERATE"
)
//go:embed prompts.txt
//go:embed prompts/*.md
var prompts embed.FS
// prompt缓存 key: XXX_YYY value: 内容
var promptCache = make(map[string]string, 20)
// GetPrompt 获取本地prompts文件内容并进行模板解析
func GetPrompt(filename string, values any) (string, error) {
// 自动添加 prompts/ 前缀
fullPath := "prompts/" + filename
// 获取本地文件的prompt内容并进行解析获取对应key的prompt内容
func GetPrompt(key string, formatValues ...any) string {
prompt := promptCache[key]
if prompt != "" {
return fmt.Sprintf(prompt, formatValues...)
}
bytes, err := prompts.ReadFile("prompts.txt")
bytes, err := prompts.ReadFile(fullPath)
if err != nil {
logx.Error("failed to read prompt file: prompts.txt, err: %v", err)
return ""
return "", err
}
allPrompts := string(bytes)
propmts := strings.Split(allPrompts, "---------------------------------------")
var res string
for _, keyAndPrompt := range propmts {
keyAndPrompt = stringx.TrimSpaceAndBr(keyAndPrompt)
// 获取第一行的Key信息如--XXX_YYY
info := strings.SplitN(keyAndPrompt, "\n", 2)
// prompt即去除第一行的key与备注信息
prompt := info[1]
// 获取keyXXX_YYY
promptKey := strings.Split(strings.Split(info[0], " ")[0], "--")[1]
if key == promptKey {
res = prompt
}
promptCache[promptKey] = prompt
}
return fmt.Sprintf(res, formatValues...)
return stringx.TemplateParse(string(bytes), values)
}

View File

@@ -1,38 +0,0 @@
--FLOW_BIZ_AUDIT 流程业务审核
你现在是一位专业的数据库管理员、Redis管理员和安全审核专家。请根据以下审核规则分析用户提供的内容并以严格的JSON格式返回分析结果。
- 当用户询问表结构时,禁止凭经验回答,可以使用 QueryTableInfo 工具获取真实表DDL数据进行核验字段等。
审核规则:
%s
待审核内容为结构化的业务操作请求,包含以下要素:
1. 操作指令可能包含单条或多条SQL语句和/或Redis命令
2. 数据库上下文:每条指令关联的目标数据库信息,包括:
- 数据库唯一标识(ID)
- 数据库实例名称
- 数据库类型(如MySQL、PostgreSQL、Redis等)
请根据指令类型和目标数据库类型,应用相应的安全审核规则进行逐条验证。若存在任何不符合安全规范的指令,整体审核结果应判定为不通过。
请严格遵循以下要求:
1. 仅输出有效的JSON对象不要包含任何解释性文字
2. 禁止包含任何Markdown格式包括但不限于```json、```等代码引用符号)
3. JSON格式必须严格包含以下字段且无额外内容
{
"allowExecute": boolean, // 是否允许执行操作true或false
"suggestion": string // 具体的建议内容,如"通过"或"拒绝原因"等。如果是多条命令审核,请详细说明哪条命令存在问题
}
---------------------------------------
--SQL_GENERATE 生成SQL
你是一位专业的SQL开发工程师请根据用户的自然语言描述生成符合%s语法的SQL语句。
相关表名:%s
请确保生成的SQL语句
1. 语法正确,符合指定数据库类型的标准
2. 逻辑清晰,准确表达用户的需求
3. 仅返回SQL语句不要包含任何解释或说明
4. 避免使用可能导致性能问题的写法
5. 确保SQL语句的安全性防止SQL注入等安全问题
如果用户的需求不明确或无法完全实现,请说明原因。

View File

@@ -0,0 +1,141 @@
# 系统角色
你是一位 **专业的数据库管理员DBA**、**Redis 管理员** 和 **安全审核专家**
你的唯一职责是:
**对用户提交的结构化业务操作请求进行安全审核,并输出是否允许执行的最终裁决。**
⚠️ **重要限制**
- 不执行任何实际操作
- 不提供 SQL 改写或优化建议
- 不基于经验或推测回答任何数据库事实
---
# 审核规则
{{.rule}}
---
# 核心强制原则(必须严格遵守)
## 1. 禁止猜测与推测
- ❌ 严禁基于经验、习惯或主观推测回答以下内容:
- 表结构
- 字段信息(名称、类型等)
- 索引信息(主键、唯一索引、普通索引)
- ✅ 仅允许基于 **真实、已验证的数据库元数据** 进行审核判断
---
## 2. 工具调用强制性
### 触发条件
当审核规则中出现以下任一要求时,**必须调用对应工具获取真实数据**
- 校验表是否存在
- 校验字段是否存在或字段类型
- 校验主键、唯一索引、普通索引
- 判断 SQL 是否依赖索引或命中索引安全规则
### 严格限制
- ❌ 禁止在未调用工具的情况下直接给出审核结论
- ❌ 禁止基于经验或假设进行判断
---
## 3. 信息不足即不通过
- 若审核所需的事实信息缺失、无法确认,或未通过工具校验
-**必须直接判定为不通过allowExecute = false**
---
# 工具说明
## 工具QueryTableInfo
### 功能说明
用于查询指定数据库表的真实结构信息,包括:
- 字段名称
- 字段类型
- 主键字段
- 索引信息(唯一索引 / 普通索引)
### 调用规则
- 当审核规则要求校验 **表、字段或索引** 时:
-**必须调用该工具**
-**不得跳过、替代或基于经验判断**
### 参数约束
- 工具参数必须 **完全来源于 SQL 解析结果**
- ❌ 禁止编造表名、字段名或数据库信息
---
# 输入说明
每条业务操作请求包含以下内容:
1. SQL 或 Redis 指令(单条或多条)
2. 数据库上下文信息:
- 数据库唯一标识ID
- 数据库实例名称
- 数据库类型MySQL / PostgreSQL / Redis 等)
---
# 输出说明(极其重要)
## 输出格式强制要求
-**最终只能输出一个 JSON 对象**
-**不得输出任何额外文字、解释、Markdown、日志或调试信息**
-**输出结果必须可被 JSON.parse 成功解析**
-**即使审核失败,也必须输出 JSON**
---
## 输出结构(字段不可增删)
```json
{
"allowExecute": boolean,
"suggestion": string
}
```
---
## 字段说明
### allowExecute
- true所有指令均符合安全规范且所需信息已完整校验
- false存在任意不符合安全规范的指令或审核信息不足无法判断
### suggestion
- **通过**:填写“通过”或简要确认说明
- **不通过**
- 必须明确指出 **具体不通过原因**
- 若包含多条指令,需明确指出 **哪一条指令** 存在问题
- 表述需简洁、明确,不得模糊或推测
---
# 最终裁决规则(不可违背)
- 只要存在 **任意一条** 不符合安全规范的指令
**整体审核结果必须为不通过**
- ❌ 禁止输出以下非确定性结论:
- “部分通过”
- “建议执行”
- “可能有风险”
- “视情况而定”

View File

@@ -47,7 +47,7 @@ func (a *AccountLogin) ReqConfs() *req.Confs {
// @router /auth/accounts/login [post]
func (a *AccountLogin) Login(rc *req.Ctx) {
loginForm := req.BindJson[*form.LoginForm](rc)
loginForm := req.BindJson[form.LoginForm](rc)
ctx := rc.MetaCtx
accountLoginSecurity := config.GetAccountLoginSecurity()
@@ -96,7 +96,7 @@ type OtpVerifyInfo struct {
// OTP双因素校验
func (a *AccountLogin) OtpVerify(rc *req.Ctx) {
otpVerify := req.BindJson[*form.OtpVerfiy](rc)
otpVerify := req.BindJson[form.OtpVerfiy](rc)
ctx := rc.MetaCtx
tokenKey := fmt.Sprintf("otp:token:%s", otpVerify.OtpToken)

View File

@@ -13,6 +13,7 @@ import (
"mayfly-go/pkg/biz"
"mayfly-go/pkg/cache"
"mayfly-go/pkg/global"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/netx"
@@ -57,7 +58,9 @@ func LastLoginCheck(ctx context.Context, account *sysentity.Account, accountLogi
res["refresh_token"] = refreshToken
// 不进行otp二次校验则直接返回accessToken
// 保存登录消息
go saveLogin(ctx, account, loginIp)
gox.Go(func() {
saveLogin(ctx, account, loginIp)
})
}
// 赋值otp状态

View File

@@ -47,7 +47,7 @@ func (a *LdapLogin) GetLdapEnabled(rc *req.Ctx) {
// @router /auth/ldap/login [post]
func (a *LdapLogin) Login(rc *req.Ctx) {
loginForm := req.BindJson[*form.LoginForm](rc)
loginForm := req.BindJson[form.LoginForm](rc)
ctx := rc.MetaCtx
accountLoginSecurity := config.GetAccountLoginSecurity()
// 判断是否有开启登录验证码校验

View File

@@ -5,5 +5,5 @@ import (
)
func InitIoc() {
ioc.Register(new(oauth2AppImpl), ioc.WithComponentName("Oauth2App"))
ioc.Register(new(oauth2AppImpl))
}

View File

@@ -5,5 +5,5 @@ import (
)
func InitIoc() {
ioc.Register(newAuthAccountRepo(), ioc.WithComponentName("Oauth2AccountRepo"))
ioc.Register(newAuthAccountRepo())
}

View File

@@ -1,14 +1,14 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/auth/api"
"mayfly-go/internal/auth/application"
"mayfly-go/internal/auth/infra/persistence"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()

View File

@@ -1,12 +1,12 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/common/api"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
api.InitIoc()
})
}

View File

@@ -4,59 +4,36 @@ import (
"context"
"mayfly-go/internal/db/application"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/jsonx"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"github.com/cloudwego/eino/components/tool/utils"
)
func GetQueryTableInfo() tool.InvokableTool {
return &QueryTableInfo{}
type QueryTableInfoParam struct {
DbId uint64 `json:"dbId" jsonschema_description:"数据库ID"`
DbName string `json:"dbName" jsonschema_description:"数据库名称"`
TableName string `json:"tableName" jsonschema_description:"表名"`
}
type QueryTableInfo struct {
type QueryTableInfoOutput struct {
DDL string `json:"ddl" jsonschema_description:"表DDL"`
}
var _ tool.InvokableTool = (*QueryTableInfo)(nil)
func (q QueryTableInfo) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "QueryTableInfo",
Desc: "查询数据库表的详细信息,包括表结构、字段定义、索引等。当用户需要了解某个表的结构时使用此工具。",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"dbId": {
Type: schema.Number,
Desc: "数据库ID",
Required: true,
},
"dbName": {
Type: schema.String,
Desc: "数据库名称",
Required: true,
},
"tableName": {
Type: schema.String,
Desc: "表名",
Required: true,
},
}),
}, nil
}
func (q QueryTableInfo) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
logx.Debugf("开始查询数据库表信息: %s", argumentsInJSON)
m, err := jsonx.ToMap(argumentsInJSON)
if err != nil {
return "arguments json invalid", err
}
tableName := m.GetStr("tableName")
conn, err := application.GetDbApp().GetDbConn(ctx, uint64(m.GetInt64("dbId")), m.GetStr("dbName"))
if err != nil {
return "获取数据库连接失败", err
}
return conn.GetMetadata().GetTableDDL(tableName, false)
func GetQueryTableInfo() (tool.InvokableTool, error) {
return utils.InferTool("QueryTableInfo",
"当需要了解某个表结构时请调用此工具。使用它来查询数据库表的DDL信息包括表结构、字段定义、索引等。",
func(ctx context.Context, param *QueryTableInfoParam) (*QueryTableInfoOutput, error) {
conn, err := application.GetDbApp().GetDbConn(ctx, param.DbId, param.DbName)
if err != nil {
return nil, err
}
ddl, err := conn.GetMetadata().GetTableDDL(param.TableName, false)
if err != nil {
return nil, err
}
output := &QueryTableInfoOutput{DDL: ddl}
return output, nil
},
)
}

View File

@@ -1,7 +1,14 @@
package tools
import "mayfly-go/internal/ai/agent"
import (
"mayfly-go/internal/ai/agent"
"mayfly-go/pkg/logx"
)
func Init() {
agent.RegisterTool(agent.ToolTypeDb, GetQueryTableInfo())
if queryTableTool, err := GetQueryTableInfo(); err != nil {
logx.Errorf("agent tool - 获取QueryTableInfo工具失败: %v", err)
} else {
agent.RegisterTool(agent.ToolTypeDb, queryTableTool)
}
}

View File

@@ -74,7 +74,7 @@ func (d *Db) ReqConfs() *req.Confs {
// @router /api/dbs [get]
func (d *Db) Dbs(rc *req.Ctx) {
queryCond := req.BindQuery[*entity.DbQuery](rc)
queryCond := req.BindQuery[entity.DbQuery](rc)
// 不存在可访问标签id即没有可操作数据
tags := d.tagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
@@ -112,7 +112,7 @@ func (d *Db) Dbs(rc *req.Ctx) {
}
func (d *Db) Save(rc *req.Ctx) {
form, db := req.BindJsonAndCopyTo[*form.DbForm, *entity.Db](rc)
form, db := req.BindJsonAndCopyTo[form.DbForm, entity.Db](rc)
rc.ReqParam = form
biz.ErrIsNil(d.dbApp.SaveDb(rc.MetaCtx, db))
@@ -132,7 +132,7 @@ func (d *Db) DeleteDb(rc *req.Ctx) {
/** 数据库操作相关、执行sql等 ***/
func (d *Db) ExecSql(rc *req.Ctx) {
form := req.BindJson[*form.DbSqlExecForm](rc)
form := req.BindJson[form.DbSqlExecForm](rc)
ctx, cancel := context.WithTimeout(rc.MetaCtx, time.Duration(config.GetDbms().SqlExecTl)*time.Second)
defer cancel()
@@ -339,7 +339,7 @@ func (d *Db) GetSchemas(rc *req.Ctx) {
}
func (d *Db) CopyTable(rc *req.Ctx) {
form, copy := req.BindJsonAndCopyTo[*form.DbCopyTableForm, *dbi.DbCopyTable](rc)
form, copy := req.BindJsonAndCopyTo[form.DbCopyTableForm, dbi.DbCopyTable](rc)
conn, err := d.dbApp.GetDbConn(rc.MetaCtx, form.Id, form.Db)
biz.ErrIsNilAppendErr(err, "copy table error: %s")

View File

@@ -50,21 +50,21 @@ func (d *DataSyncTask) ReqConfs() *req.Confs {
}
func (d *DataSyncTask) Tasks(rc *req.Ctx) {
queryCond := req.BindQuery[*entity.DataSyncTaskQuery](rc)
queryCond := req.BindQuery[entity.DataSyncTaskQuery](rc)
res, err := d.dataSyncTaskApp.GetPageList(queryCond)
biz.ErrIsNil(err)
rc.ResData = model.PageResultConv[*entity.DataSyncTask, *vo.DataSyncTaskListVO](res)
}
func (d *DataSyncTask) Logs(rc *req.Ctx) {
queryCond := req.BindQuery[*entity.DataSyncLogQuery](rc)
queryCond := req.BindQuery[entity.DataSyncLogQuery](rc)
res, err := d.dataSyncTaskApp.GetTaskLogList(queryCond)
biz.ErrIsNil(err)
rc.ResData = model.PageResultConv[*entity.DataSyncLog, *vo.DataSyncLogListVO](res)
}
func (d *DataSyncTask) SaveTask(rc *req.Ctx) {
form, task := req.BindJsonAndCopyTo[*form.DataSyncTaskForm, *entity.DataSyncTask](rc)
form, task := req.BindJsonAndCopyTo[form.DataSyncTaskForm, entity.DataSyncTask](rc)
// 解码base64 sql
sqlStr, err := utils.AesDecryptByLa(task.DataSql, rc.GetLoginAccount())
@@ -87,7 +87,7 @@ func (d *DataSyncTask) DeleteTask(rc *req.Ctx) {
}
func (d *DataSyncTask) ChangeStatus(rc *req.Ctx) {
form := req.BindJson[*form.DataSyncTaskStatusForm](rc)
form := req.BindJson[form.DataSyncTaskStatusForm](rc)
rc.ReqParam = form
task, err := d.dataSyncTaskApp.GetById(form.Id)

View File

@@ -55,7 +55,7 @@ func (d *Instance) ReqConfs() *req.Confs {
// Instances 获取数据库实例信息
// @router /api/instances [get]
func (d *Instance) Instances(rc *req.Ctx) {
queryCond := req.BindQuery[*entity.InstanceQuery](rc)
queryCond := req.BindQuery[entity.InstanceQuery](rc)
tags := d.tagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeDbInstance, tagentity.TagTypeAuthCert)),
@@ -90,14 +90,14 @@ func (d *Instance) Instances(rc *req.Ctx) {
}
func (d *Instance) TestConn(rc *req.Ctx) {
form, instance := req.BindJsonAndCopyTo[*form.InstanceForm, *entity.DbInstance](rc)
form, instance := req.BindJsonAndCopyTo[form.InstanceForm, entity.DbInstance](rc)
biz.ErrIsNil(d.instanceApp.TestConn(rc.MetaCtx, instance, form.AuthCerts[0]))
}
// SaveInstance 保存数据库实例信息
// @router /api/instances [post]
func (d *Instance) SaveInstance(rc *req.Ctx) {
form, instance := req.BindJsonAndCopyTo[*form.InstanceForm, *entity.DbInstance](rc)
form, instance := req.BindJsonAndCopyTo[form.InstanceForm, entity.DbInstance](rc)
rc.ReqParam = form
id, err := d.instanceApp.SaveDbInstance(rc.MetaCtx, &dto.SaveDbInstance{
@@ -132,7 +132,7 @@ func (d *Instance) DeleteInstance(rc *req.Ctx) {
// 获取数据库实例的所有数据库名
func (d *Instance) GetDatabaseNames(rc *req.Ctx) {
form, instance := req.BindJsonAndCopyTo[*form.InstanceDbNamesForm, *entity.DbInstance](rc)
form, instance := req.BindJsonAndCopyTo[form.InstanceDbNamesForm, entity.DbInstance](rc)
res, err := d.instanceApp.GetDatabases(rc.MetaCtx, instance, form.AuthCert)
biz.ErrIsNil(err)
rc.ResData = res

View File

@@ -30,7 +30,7 @@ func (d *DbSql) ReqConfs() *req.Confs {
// @router /api/db/:dbId/sql [post]
func (d *DbSql) SaveSql(rc *req.Ctx) {
dbSqlForm := req.BindJson[*form.DbSqlSaveForm](rc)
dbSqlForm := req.BindJson[form.DbSqlSaveForm](rc)
rc.ReqParam = dbSqlForm
dbId := getDbId(rc)

View File

@@ -26,7 +26,7 @@ func (d *DbSqlExec) ReqConfs() *req.Confs {
}
func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) {
queryCond := req.BindQuery[*entity.DbSqlExecQuery](rc)
queryCond := req.BindQuery[entity.DbSqlExecQuery](rc)
if statusStr := rc.Query("status"); statusStr != "" {
queryCond.Status = collx.ArrayMap[string, int8](strings.Split(statusStr, ","), func(val string) int8 {
return cast.ToInt8(val)

View File

@@ -11,6 +11,7 @@ import (
fileapp "mayfly-go/internal/file/application"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"strings"
@@ -60,7 +61,7 @@ func (d *DbTransferTask) ReqConfs() *req.Confs {
}
func (d *DbTransferTask) Tasks(rc *req.Ctx) {
queryCond := req.BindQuery[*entity.DbTransferTaskQuery](rc)
queryCond := req.BindQuery[entity.DbTransferTaskQuery](rc)
res, err := d.dbTransferTaskApp.GetPageList(queryCond)
biz.ErrIsNil(err)
@@ -77,7 +78,7 @@ func (d *DbTransferTask) Tasks(rc *req.Ctx) {
}
func (d *DbTransferTask) SaveTask(rc *req.Ctx) {
reqForm, task := req.BindJsonAndCopyTo[*form.DbTransferTaskForm, *entity.DbTransferTask](rc)
reqForm, task := req.BindJsonAndCopyTo[form.DbTransferTaskForm, entity.DbTransferTask](rc)
rc.ReqParam = reqForm
biz.ErrIsNil(d.dbTransferTaskApp.Save(rc.MetaCtx, task))
@@ -93,7 +94,7 @@ func (d *DbTransferTask) DeleteTask(rc *req.Ctx) {
}
func (d *DbTransferTask) ChangeStatus(rc *req.Ctx) {
form := req.BindJson[*form.DbTransferTaskStatusForm](rc)
form := req.BindJson[form.DbTransferTaskStatusForm](rc)
rc.ReqParam = form
task, err := d.dbTransferTaskApp.GetById(form.Id)
@@ -116,7 +117,7 @@ func (d *DbTransferTask) Stop(rc *req.Ctx) {
}
func (d *DbTransferTask) Files(rc *req.Ctx) {
queryCond := req.BindQuery[*entity.DbTransferFileQuery](rc)
queryCond := req.BindQuery[entity.DbTransferFileQuery](rc)
res, err := d.dbTransferFileApp.GetPageList(queryCond)
biz.ErrIsNil(err)
@@ -136,7 +137,7 @@ func (d *DbTransferTask) FileDel(rc *req.Ctx) {
}
func (d *DbTransferTask) FileRun(rc *req.Ctx) {
fm := req.BindJson[*form.DbTransferFileRunForm](rc)
fm := req.BindJson[form.DbTransferFileRunForm](rc)
rc.ReqParam = fm
@@ -150,12 +151,13 @@ func (d *DbTransferTask) FileRun(rc *req.Ctx) {
filename, reader, err := d.fileApp.GetReader(context.TODO(), tFile.FileKey)
biz.ErrIsNil(err)
go func() {
biz.ErrIsNil(d.dbSqlExecApp.ExecReader(rc.MetaCtx, &dto.SqlReaderExec{
gox.GoCtx(rc.MetaCtx, func(ctx context.Context) {
biz.ErrIsNil(d.dbSqlExecApp.ExecReader(ctx, &dto.SqlReaderExec{
Reader: reader,
Filename: filename,
DbConn: targetDbConn,
ClientId: fm.ClientId,
}))
}()
})
}

View File

@@ -4,7 +4,6 @@ type DataSyncTaskForm struct {
Id uint64 `json:"id"`
TaskName string `binding:"required" json:"taskName"`
TaskCron string `binding:"required" json:"taskCron"`
TaskKey string `json:"taskKey"`
Status int `binding:"required" json:"status"`
SrcDbId int64 `binding:"required" json:"srcDbId"`

View File

@@ -1,6 +1,10 @@
package form
import "mayfly-go/pkg/model"
type DbTransferTaskForm struct {
model.ExtraData
Id uint64 `json:"id"`
TaskName string `binding:"required" json:"taskName"` // 任务名称
@@ -28,14 +32,17 @@ type DbTransferTaskForm struct {
TargetInstName string `json:"targetInstName"` // 目标库实例名
TargetTagPath string `json:"targetTagPath"` // 目标库tagPath
}
type DbTransferTaskStatusForm struct {
Id uint64 `binding:"required" json:"taskId" form:"taskId"`
Status int8 `json:"status" form:"status"`
}
type DbTransferFileForm struct {
Id uint64 `json:"id"`
FileName string `json:"fileName" form:"fileName"`
}
type DbTransferFileRunForm struct {
Id uint64 `json:"id"` // 文件ID
TargetDbId uint64 `json:"targetDbId" form:"targetDbId"` // 需要执行sql的数据库id

View File

@@ -1,10 +1,13 @@
package vo
import (
"mayfly-go/pkg/model"
"time"
)
type DbTransferTaskListVO struct {
model.ExtraData
Id uint64 `json:"id"`
CreateTime *time.Time `json:"createTime"`
Creator string `json:"creator"`

View File

@@ -6,13 +6,13 @@ import (
)
func InitIoc() {
ioc.Register(new(instanceAppImpl), ioc.WithComponentName("DbInstanceApp"))
ioc.Register(new(dbAppImpl), ioc.WithComponentName("DbApp"))
ioc.Register(new(dbSqlExecAppImpl), ioc.WithComponentName("DbSqlExecApp"))
ioc.Register(new(dbSqlAppImpl), ioc.WithComponentName("DbSqlApp"))
ioc.Register(new(dataSyncAppImpl), ioc.WithComponentName("DbDataSyncTaskApp"))
ioc.Register(new(dbTransferAppImpl), ioc.WithComponentName("DbTransferTaskApp"))
ioc.Register(new(dbTransferFileAppImpl), ioc.WithComponentName("DbTransferFileApp"))
ioc.Register(new(instanceAppImpl))
ioc.Register(new(dbAppImpl))
ioc.Register(new(dbSqlExecAppImpl))
ioc.Register(new(dbSqlAppImpl))
ioc.Register(new(dataSyncAppImpl))
ioc.Register(new(dbTransferAppImpl))
ioc.Register(new(dbTransferFileAppImpl))
}
func Init() {

View File

@@ -36,10 +36,9 @@ type Db interface {
// 删除数据库信息
Delete(ctx context.Context, id uint64) error
// 获取数据库连接实例
// @param id 数据库id
//
// @param dbName 数据库名
// GetDbConn 获取数据库连接实例
// - dbId: 数据库id
// - dbName: 数据库名
GetDbConn(ctx context.Context, dbId uint64, dbName string) (*dbi.DbConn, error)
// 根据数据库实例id获取连接随机返回该instanceId下已连接的conn若不存在则是使用该instanceId关联的db进行连接并返回。
@@ -228,7 +227,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
}
writer := writerx.NewStringWriter(reqParam.Writer)
defer writer.Close()
dbId := reqParam.DbId
dbName := reqParam.DbName
tables := reqParam.Tables
@@ -291,14 +290,16 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
// 按表名排序
sort.Strings(tables)
quoteSchema := srcDialect.Quoter().Quote(dbConn.Info.CurrentSchema())
dumpHelper := targetDialect.GetDumpHelper()
targetDumpHelper := targetDialect.GetDumpHelper()
targetSqlGenerator := targetDialect.GetSQLGenerator()
targetDialectQuote := targetDialect.Quoter().Quote
// targetDialectQuote := targetDialect.Quoter().Quote
srcDialectQuote := srcDialect.Quoter().Quote
// 遍历获取每个表的信息
for _, tableName := range tables {
log(fmt.Sprintf("get table [%s] information...", tableName))
quoteTableName := targetDialectQuote(tableName)
// targetQuoteTableName := targetDialectQuote(tableName)
srcQuoteTableName := srcDialectQuote(tableName)
// 查询表信息,主要是为了查询表注释
tbs, err := srcMeta.GetTables(tableName)
@@ -332,22 +333,22 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
log(fmt.Sprintf("generate table [%s] DML...", tableName))
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- Data: %s \n-- ----------------------------\n", tableName))
dumpHelper.BeforeInsert(writer, quoteTableName)
targetDumpHelper.BeforeInsert(writer, tableName)
dataCount := 0
rows := make([][]any, 0)
_, err = dbConn.WalkTableRows(ctx, quoteTableName, func(row map[string]any, _ []*dbi.QueryColumn) error {
_, err = dbConn.WalkTableRows(ctx, srcQuoteTableName, 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 {
if dataCount%100 != 0 {
return nil
}
beforeInsert := dumpHelper.BeforeInsertSql(quoteSchema, quoteTableName)
beforeInsert := targetDumpHelper.BeforeInsertSql(quoteSchema, tableName)
if beforeInsert != "" {
writer.WriteString(beforeInsert)
}
@@ -365,7 +366,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
}
if len(rows) > 0 {
beforeInsert := dumpHelper.BeforeInsertSql(quoteSchema, quoteTableName)
beforeInsert := targetDumpHelper.BeforeInsertSql(quoteSchema, tableName)
if beforeInsert != "" {
writer.WriteString(beforeInsert)
}
@@ -375,7 +376,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
}
}
dumpHelper.AfterInsert(writer, tableName, columns)
targetDumpHelper.AfterInsert(writer, tableName, columns)
progress(tableName, dbi.StmtTypeInsert, dataCount, true)
}

View File

@@ -13,6 +13,7 @@ import (
"mayfly-go/pkg/cache"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
@@ -70,7 +71,13 @@ func (app *dataSyncAppImpl) Save(ctx context.Context, taskEntity *entity.DataSyn
taskEntity.TaskKey = uuid.New().String()
err = app.Insert(ctx, taskEntity)
} else {
taskEntity.TaskKey = ""
if taskEntity.TaskKey == "" {
task, err := app.GetById(taskEntity.Id)
if err != nil {
return errorx.NewBiz("db sync task not found")
}
taskEntity.TaskKey = task.TaskKey
}
err = app.UpdateById(ctx, taskEntity)
}
if err != nil {
@@ -126,7 +133,13 @@ func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
CreateTime: &now,
Status: entity.DataSyncTaskStateFail, // 默认失败
}
defer app.endRunning(task, syncLog)
defer gox.Recover(func(err error) {
syncLog.ErrText = i18n.T(imsg.DataSyncFailMsg, "msg", err.Error())
logx.ErrorContext(ctx, syncLog.ErrText)
syncLog.Status = entity.DataSyncTaskStateFail
})
// 通过占位符格式化sql
updSql := ""

View File

@@ -298,7 +298,7 @@ func (d *dbSqlExecAppImpl) FlowBizHandle(ctx context.Context, bizHandleParam *fl
return nil, nil
}
execSqlBizForm, err := jsonx.To[*FlowDbExecSqlBizForm](procinst.BizForm)
execSqlBizForm, err := jsonx.To[FlowDbExecSqlBizForm](procinst.BizForm)
if err != nil {
return nil, errorx.NewBizf("failed to parse the business form information: %s", err.Error())
}
@@ -603,7 +603,7 @@ func (d *dbSqlExecAppImpl) doExec(ctx context.Context, dbConn *dbi.DbConn, sql s
return &dto.DbSqlExecRes{
Columns: []*dbi.QueryColumn{
{Name: "rowsAffected", Type: "number"},
{Name: "rowsAffected", Key:"rowsAffected", Type: "number"},
},
Res: res,
Sql: sql,

View File

@@ -1,6 +1,7 @@
package application
import (
"archive/zip"
"cmp"
"context"
"fmt"
@@ -17,6 +18,7 @@ import (
"mayfly-go/pkg/cache"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/scheduler"
@@ -75,11 +77,19 @@ func (app *dbTransferAppImpl) Save(ctx context.Context, taskEntity *entity.DbTra
taskEntity.TaskKey = uuid.New().String()
err = app.Insert(ctx, taskEntity)
} else {
if taskEntity.TaskKey == "" {
task, err := app.GetById(taskEntity.Id)
if err != nil {
return errorx.NewBiz("db transfer task not found")
}
taskEntity.TaskKey = task.TaskKey
}
err = app.UpdateById(ctx, taskEntity)
}
if err != nil {
return err
}
app.addCronJob(ctx, taskEntity)
return nil
}
@@ -131,7 +141,6 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64) (uint64, e
}
logId, _ := app.CreateLog(ctx, taskId)
start := time.Now()
// 修改状态与关联日志id
task.LogId = logId
task.RunningState = entity.DbTransferTaskRunStateRunning
@@ -142,7 +151,7 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64) (uint64, e
// 标记该任务开始执行
app.MarkRunning(taskId)
go func() {
gox.Go(func() {
// 获取源库连接、目标库连接判断连接可用性否则记录日志xx连接不可用
// 获取源库表信息
srcConn, err := app.dbApp.GetDbConn(ctx, uint64(task.SrcDbId), task.SrcDbName)
@@ -171,19 +180,21 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64) (uint64, e
// 迁移到文件或数据库
switch task.Mode {
case entity.DbTransferTaskModeFile:
app.transfer2File(ctx, taskId, logId, task, srcConn, start, tables)
app.transfer2File(ctx, logId, task, tables)
case entity.DbTransferTaskModeDb:
app.transfer2Db(ctx, taskId, logId, task, srcConn, start, tables)
app.transfer2Db(ctx, logId, task, tables)
default:
app.EndTransfer(ctx, logId, taskId, "error in transfer mode, only migrating to files or databases is currently supported", err, nil)
return
}
}()
})
return logId, nil
}
func (app *dbTransferAppImpl) transfer2Db(ctx context.Context, taskId uint64, logId uint64, task *entity.DbTransferTask, srcConn *dbi.DbConn, start time.Time, tables []dbi.Table) {
func (app *dbTransferAppImpl) transfer2Db(ctx context.Context, logId uint64, task *entity.DbTransferTask, tables []dbi.Table) {
startTime := time.Now()
taskId := task.Id
defer app.MarkStop(taskId)
defer app.logApp.Flush(logId, true)
@@ -203,13 +214,18 @@ func (app *dbTransferAppImpl) transfer2Db(ctx context.Context, taskId uint64, lo
for _, tables := range tableGroups {
errGroup.Go(func() error {
defer gox.Recover()
if !app.IsRunning(taskId) {
return errorx.NewBiz("transfer stopped")
}
currentDumpTable := tables[0]
pr, pw := io.Pipe()
go func() {
defer pr.Close()
gox.Go(func () {
defer pw.Close()
err := app.dbApp.DumpDb(ctx, &dto.DumpDb{
LogId: logId,
DbId: uint64(task.SrcDbId),
@@ -240,20 +256,16 @@ func (app *dbTransferAppImpl) transfer2Db(ctx context.Context, taskId uint64, lo
},
})
if err != nil {
app.Log(ctx, logId, fmt.Sprintf("db dump failed: %s", err.Error()))
pr.CloseWithError(err)
return
}
}()
})
if err != nil {
pw.CloseWithError(err)
app.EndTransfer(ctx, logId, taskId, "transfer table failed", err, nil)
return err
}
tx, _ := targetConn.Begin()
err = sqlparser.SQLSplit(pr, ';', func(stmt string) error {
if _, err := targetConn.TxExec(tx, stmt); err != nil {
app.EndTransfer(ctx, logId, taskId, fmt.Sprintf("执行sql出错: %s", stmt), err, nil)
app.Log(ctx, logId, fmt.Sprintf("sql exec failed: %s", stmt), err, nil)
pw.CloseWithError(err)
return err
}
@@ -275,10 +287,11 @@ func (app *dbTransferAppImpl) transfer2Db(ctx context.Context, taskId uint64, lo
app.EndTransfer(ctx, logId, taskId, "transfer table failed", err, nil)
return
}
app.EndTransfer(ctx, logId, taskId, fmt.Sprintf("execute transfer task [taskId = %d] complete, time: %v", taskId, time.Since(start)), nil, nil)
app.EndTransfer(ctx, logId, taskId, fmt.Sprintf("execute transfer task [taskId = %d] complete, time: %v", taskId, time.Since(startTime)), nil, nil)
}
func (app *dbTransferAppImpl) transfer2File(ctx context.Context, taskId uint64, logId uint64, task *entity.DbTransferTask, srcConn *dbi.DbConn, start time.Time, tables []dbi.Table) {
func (app *dbTransferAppImpl) transfer2File(ctx context.Context, logId uint64, task *entity.DbTransferTask, tables []dbi.Table) {
taskId := task.Id
// 1、新增迁移文件数据
nowTime := time.Now()
tFile := &entity.DbTransferFile{
@@ -290,8 +303,9 @@ func (app *dbTransferAppImpl) transfer2File(ctx context.Context, taskId uint64,
}
_ = app.transferFileApp.Save(ctx, tFile)
filename := fmt.Sprintf("dtf_%s.sql", timex.TimeNo())
fileKey, writer, saveFileFunc, err := app.fileApp.NewWriter(ctx, "", filename)
fileType := cmp.Or(task.GetExtraString("fileType"), "sql")
filename := fmt.Sprintf("dtf_%s.%s", timex.TimeNo(), fileType)
fileKey, writer, closeFunc, err := app.fileApp.NewWriter(ctx, "", filename)
if err != nil {
app.EndTransfer(ctx, logId, taskId, "create file error", err, nil)
return
@@ -305,11 +319,30 @@ func (app *dbTransferAppImpl) transfer2File(ctx context.Context, taskId uint64,
go func() {
var err error
defer saveFileFunc(&err)
defer closeFunc(&err)
defer app.MarkStop(taskId)
defer app.logApp.Flush(logId, true)
defer gox.Recover(func(e error) {
err = e
app.EndTransfer(ctx, logId, taskId, "transfer to file panic", e, nil)
tFile.Status = entity.DbTransferFileStatusFail
app.transferFileApp.UpdateById(ctx, tFile)
})
ctx = context.Background()
if fileType == "zip" {
zipWriter := zip.NewWriter(writer)
defer zipWriter.Close()
// 创建SQL文件在ZIP内的条目
writer, err = zipWriter.Create(strings.Replace(filename, "zip", "sql", 1))
if err != nil {
app.EndTransfer(ctx, logId, taskId, "create zip file error", err, nil)
return
}
}
err = app.dbApp.DumpDb(ctx, &dto.DumpDb{
LogId: logId,
DbId: uint64(task.SrcDbId),
@@ -333,7 +366,7 @@ func (app *dbTransferAppImpl) transfer2File(ctx context.Context, taskId uint64,
tFile.Status = entity.DbTransferFileStatusSuccess
tFile.FileKey = fileKey
_ = app.transferFileApp.UpdateById(ctx, tFile)
app.transferFileApp.UpdateById(ctx, tFile)
}()
}
@@ -358,6 +391,7 @@ func (app *dbTransferAppImpl) Stop(ctx context.Context, taskId uint64) error {
func (d *dbTransferAppImpl) TimerDeleteTransferFile() {
logx.Debug("start deleting transfer files periodically...")
scheduler.AddFun("@every 100m", func() {
defer gox.Recover()
dts, err := d.ListByCond(model.NewCond().Eq("mode", entity.DbTransferTaskModeFile).Ge("file_save_days", 1))
if err != nil {
logx.Errorf("the task to periodically get database transfer to file failed: %s", err.Error())
@@ -385,12 +419,6 @@ func (app *dbTransferAppImpl) addCronJob(ctx context.Context, taskEntity *entity
// 根据状态添加新的任务
if taskEntity.Status == entity.DbTransferTaskStatusEnable && taskEntity.CronAble == entity.DbTransferTaskCronAbleEnable {
if key == "" {
taskEntity.TaskKey = uuid.New().String()
key = taskEntity.TaskKey
_ = app.UpdateById(ctx, taskEntity)
}
taskId := taskEntity.Id
if err := scheduler.AddFunByKey(key, taskEntity.Cron, func() {
logx.Infof("start the transfer task: %d", taskId)

View File

@@ -22,7 +22,7 @@ type DumpDb struct {
LogId uint64
Writer io.WriteCloser
Writer io.Writer
TargetDbType dbi.DbType
Log func(msg string)

View File

@@ -43,6 +43,7 @@ var DefaultDbDataType = NewDbDataType("string", DTString).WithCT(CTVarchar)
type Column struct {
TableName string `json:"tableName"` // 表名
ColumnName string `json:"columnName"` // 列名
ColumnType string `json:"columnType"` // 完整列类型带有数据类型以及长度、精度等。如varchar(2000)decimal(20,2)
DataType string `json:"dataType"` // 数据类型
ColumnComment string `json:"columnComment"` // 列备注
IsPrimaryKey bool `json:"isPrimaryKey"` // 是否为主键
@@ -57,6 +58,10 @@ type Column struct {
// GetColumnType 获取完整的列类型拼接数据类型与长度等。如varchar(2000)decimal(20,2)
func (c *Column) GetColumnType() string {
if c.ColumnType != "" {
return c.ColumnType
}
if c.CharMaxLength > 0 {
return fmt.Sprintf("%s(%d)", c.DataType, c.CharMaxLength)
}
@@ -178,6 +183,8 @@ func SQLValueBool(val any) string {
return fmt.Sprintf("%v", cast.ToBool(val))
}
// SQLValueString 转换为SQL字符串值处理特殊字符与单引号
// 适用于普通字符串类型,会将双引号等特殊字符进行转义
func SQLValueString(val any) string {
if val == nil {
return NULL
@@ -197,6 +204,26 @@ func SQLValueString(val any) string {
return fmt.Sprintf("'%s'", quoted)
}
// SQLValuePreserveSpecialChars 转换为SQL字符串值保留特殊字符如双引号、换行符等
// 仅转义单引号为两个单引号SQL标准转义方式
func SQLValuePreserveSpecialChars(val any) string {
if val == nil {
return NULL
}
strVal, ok := val.(string)
if !ok {
return fmt.Sprintf("%v", val)
}
// 直接处理 SQL 特殊字符,保留双引号和其他字符
// 只转义单引号为两个单引号SQL 标准转义方式)
escapedStr := strings.ReplaceAll(strVal, "'", "''")
// 返回 SQL 字符串,保持原始的双引号、换行符等
return fmt.Sprintf("'%s'", escapedStr)
}
var (
DTBit = &DataType{
Name: "bit",
@@ -266,6 +293,13 @@ var (
SQLValue: SQLValueString,
}
// DTStringPreserveSpecial 用于需要保留双引号换行符等特殊字符的字符串类型
DTStringPreserveSpecial = &DataType{
Name: "string",
Valuer: ValuerString,
SQLValue: SQLValuePreserveSpecialChars,
}
DTDate = &DataType{
Name: "date",
Valuer: ValuerDate,

View File

@@ -5,7 +5,9 @@ import (
"database/sql"
"errors"
"fmt"
"strconv"
"mayfly-go/internal/machine/mcm"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
)
@@ -26,15 +28,12 @@ type DbConn struct {
// 关闭连接
func (d *DbConn) Close() error {
if d.db != nil {
defer mcm.CloseSshTunnel(d.Info)
if err := d.db.Close(); err != nil {
logx.Errorf("关闭数据库实例[%s]连接失败: %v", d.Id, err)
return err
}
logx.Debugf("dbm - conn close success, connId: %s", d.Id)
// TODO 关闭实例隧道会影响其他正在使用的连接,所以暂时不关闭
// if d.Info.useSshTunnel {
// mcm.CloseSshTunnelMachine(uint64(d.Info.SshTunnelMachineId), fmt.Sprintf("db:%d", d.Info.Id))
// }
d.db = nil
}
@@ -42,14 +41,10 @@ func (d *DbConn) Close() error {
}
func (d *DbConn) Ping() error {
if d.db == nil {
return fmt.Errorf("db is nil")
}
stats := d.db.Stats()
logx.Debugf("[%s] db stats -> open: %d, idle: %d, inUse: %d, maxOpen: %d", d.Info.Name, stats.OpenConnections, stats.Idle, stats.InUse, stats.MaxOpenConnections)
if stats.OpenConnections == 0 {
return errors.New("no open connections")
logx.Infof("[%s]-[%s] db stats: no open connections", d.Info.Name, d.Info.Database)
}
return d.db.Ping()
@@ -58,6 +53,7 @@ func (d *DbConn) Ping() error {
// 执行数据库查询返回的列信息
type QueryColumn struct {
Name string `json:"name"` // 列名
Key string `json:"key"` // 列唯一标识
Type string `json:"type"` // 数据类型
DbDataType *DbDataType `json:"-"`
@@ -67,6 +63,7 @@ type QueryColumn struct {
func NewQueryColumn(colName string, columnType *DbDataType) *QueryColumn {
return &QueryColumn{
Name: colName,
Key: colName,
Type: columnType.DataType.Name,
DbDataType: columnType,
valuer: columnType.DataType.Valuer(),
@@ -77,6 +74,7 @@ func (qc *QueryColumn) getValuePtr() any {
return qc.valuer.NewValuePtr()
}
// value 获取列值
func (qc *QueryColumn) value() any {
return qc.valuer.Value()
}
@@ -244,7 +242,12 @@ func (d *DbConn) walkQueryRows(ctx context.Context, selectSql string, walkFn Wal
rowData := make(map[string]any, lenCols)
// 把values中的数据复制到row中
for i := range scans {
rowData[cols[i].Name] = cols[i].value()
colname := cols[i].Name
if _, e := rowData[colname]; e {
colname = colname + strconv.Itoa(i)
cols[i].Key = colname
}
rowData[colname] = cols[i].value()
}
if err = walkFn(rowData, cols); err != nil {
logx.ErrorfContext(ctx, "[%s] cursor traversal query result set error, exit traversal: %s", selectSql, err.Error())

View File

@@ -43,11 +43,24 @@ type DbInfo struct {
CodePath []string
SshTunnelMachineId int
useSshTunnel bool // 是否使用系统自己实现的ssh隧道连接,而非库自带的
RemoteAddr string `json:"-"` // ssh隧道远程地址格式 ip:port
Meta Meta
}
var _ (mcm.SshTunnelAble) = (*DbInfo)(nil)
func (di *DbInfo) GetSshTunnelMachineId() int64 {
return int64(di.SshTunnelMachineId)
}
func (di *DbInfo) GetRemoteAddr() string {
if di.RemoteAddr != "" {
return di.RemoteAddr
}
return fmt.Sprintf("%s:%d", di.Host, di.Port)
}
// 获取记录日志的描述
func (di *DbInfo) GetLogDesc() string {
return fmt.Sprintf("DB[id=%d, tag=%s, name=%s, ip=%s:%d, database=%s]", di.Id, di.CodePath, di.Name, di.Host, di.Port, di.Database)
@@ -68,6 +81,9 @@ func (di *DbInfo) Conn(ctx context.Context, meta Meta) (*DbConn, error) {
di.Database = database
}
if err := di.IfUseSshTunnelChangeIpPort(ctx); err != nil {
return nil, err
}
conn, err := meta.GetSqlDb(ctx, di)
if err != nil {
logx.Errorf("db connection failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
@@ -99,17 +115,17 @@ func (di *DbInfo) Conn(ctx context.Context, meta Meta) (*DbConn, error) {
func (di *DbInfo) IfUseSshTunnelChangeIpPort(ctx context.Context) error {
// 开启ssh隧道
if di.SshTunnelMachineId > 0 {
sshTunnelMachine, err := GetSshTunnel(ctx, di.SshTunnelMachineId)
di.RemoteAddr = di.GetRemoteAddr()
sshTunnelMachine, err := machineapp.GetMachineApp().GetSshTunnelMachine(ctx, int(di.SshTunnelMachineId))
if err != nil {
return err
}
exposedIp, exposedPort, err := sshTunnelMachine.OpenSshTunnel(fmt.Sprintf("db:%d", di.Id), di.Host, di.Port)
exposedIp, exposedPort, err := sshTunnelMachine.OpenSshTunnel(di)
if err != nil {
return err
}
di.Host = exposedIp
di.Port = exposedPort
di.useSshTunnel = true
}
return nil
}

View File

@@ -131,6 +131,9 @@ func ConvToTargetDbColumn(srcDbType DbType, targetDbType DbType, targetDialect D
return nil
}
// 需要转换至异构数据库时需要将该字段清空否则如mysql可以查出该值其他数据库可能不行会导致Column.GetColumnType错误。
column.ColumnType = ""
srcMap := commonTypeConverters[srcDbType]
if srcMap == nil {
return fmt.Errorf("src database type [%s] not suport transfer", srcDbType)

View File

@@ -9,7 +9,6 @@ import (
_ "mayfly-go/internal/db/dbm/oracle"
_ "mayfly-go/internal/db/dbm/postgres"
_ "mayfly-go/internal/db/dbm/sqlite"
"mayfly-go/internal/machine/mcm"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/pool"
)
@@ -18,22 +17,6 @@ var (
poolGroup = pool.NewPoolGroup[*dbi.DbConn]()
)
func init() {
mcm.AddCheckSshTunnelMachineUseFunc(func(machineId int) bool {
items := poolGroup.AllPool()
for _, v := range items {
conn, err := v.Get(context.Background(), pool.WithGetNoUpdateLastActive(), pool.WithGetNoNewConn())
if err != nil {
continue // 获取连接失败,跳过
}
if conn.Info.SshTunnelMachineId == machineId {
return true
}
}
return false
})
}
// GetDbConn 从连接池中获取连接信息
func GetDbConn(ctx context.Context, dbId uint64, database string, getDbInfo func() (*dbi.DbInfo, error)) (*dbi.DbConn, error) {
connId := dbi.GetDbConnId(dbId, database)

View File

@@ -3,11 +3,10 @@ package dm
import (
"fmt"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/utils/stringx"
"strings"
"time"
_ "gitee.com/chunanyong/dm"
)
type DMDialect struct {
@@ -38,7 +37,7 @@ func (dd *DMDialect) CopyTable(copy *dbi.DbCopyTable) error {
// 复制数据
if copy.CopyData {
go func() {
gox.Go(func() {
// 设置允许填充自增列之后,显示指定列名可以插入自增列\
identityInsert := fmt.Sprintf("set identity_insert \"%s\" on", newTableName)
// 获取列名
@@ -50,8 +49,7 @@ func (dd *DMDialect) CopyTable(copy *dbi.DbCopyTable) error {
columnStr := strings.Join(columnArr, ",")
// 插入新数据并显示指定列
_, _ = dd.dc.Exec(fmt.Sprintf("%s insert into \"%s\" (%s) select %s from \"%s\"", identityInsert, newTableName, columnStr, columnStr, tableName))
}()
})
}
return err
}

View File

@@ -8,6 +8,8 @@ import (
"mayfly-go/pkg/utils/collx"
"net/url"
"strings"
_ "gitee.com/chunanyong/dm"
)
func init() {
@@ -36,11 +38,6 @@ func (dm *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
dbParam += "&" + d.Params
}
err := d.IfUseSshTunnelChangeIpPort(ctx)
if err != nil {
return nil, err
}
dsn := fmt.Sprintf("dm://%s:%s@%s:%d%s", d.Username, url.PathEscape(d.Password), d.Host, d.Port, dbParam)
return sql.Open(driverName, dsn)
}

View File

@@ -3,6 +3,7 @@ package mssql
import (
"fmt"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/logx"
"strings"
"time"
@@ -42,7 +43,7 @@ func (md *MssqlDialect) CopyTable(copy *dbi.DbCopyTable) error {
}
// 复制数据
if copy.CopyData {
go func() {
gox.Go(func() {
// 查询所有的列
columns, err := msMetadata.GetColumns(copy.TableName)
if err != nil {
@@ -71,7 +72,7 @@ func (md *MssqlDialect) CopyTable(copy *dbi.DbCopyTable) error {
if err != nil {
logx.Warnf("复制表[%s]数据失败: %s", copy.TableName, err.Error())
}
}()
})
}
return err

View File

@@ -25,10 +25,6 @@ type Meta struct {
}
func (mm *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
err := d.IfUseSshTunnelChangeIpPort(ctx)
if err != nil {
return nil, err
}
query := url.Values{}
// The application name (default is go-mssqldb)
query.Add("app name", "mayfly")

View File

@@ -5,6 +5,7 @@ import (
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/dbm/sqlparser"
"mayfly-go/internal/db/dbm/sqlparser/mysql"
"mayfly-go/pkg/gox"
"time"
)
@@ -42,9 +43,9 @@ func (md *MysqlDialect) CopyTable(copy *dbi.DbCopyTable) error {
// 复制数据
if copy.CopyData {
go func() {
gox.Go(func() {
_, _ = md.dc.Exec(fmt.Sprintf("insert into %s select * from %s", newTableName, tableName))
}()
})
}
return err
}

View File

@@ -6,9 +6,8 @@ import (
"fmt"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/pkg/utils/collx"
"net"
"github.com/go-sql-driver/mysql"
_ "github.com/go-sql-driver/mysql"
)
func init() {
@@ -26,23 +25,14 @@ type Meta struct {
}
func (mm *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
// SSH Conect
if d.SshTunnelMachineId > 0 {
sshTunnelMachine, err := dbi.GetSshTunnel(ctx, d.SshTunnelMachineId)
if err != nil {
return nil, err
}
mysql.RegisterDialContext(d.Network, func(ctx context.Context, addr string) (net.Conn, error) {
return sshTunnelMachine.GetDialConn("tcp", addr)
})
}
d.Network = "tcp"
// 设置dataSourceName -> 更多参数参考https://github.com/go-sql-driver/mysql#dsn-data-source-name
dsn := fmt.Sprintf("%s:%s@%s(%s:%d)/%s?parseTime=true&timeout=8s", d.Username, d.Password, d.Network, d.Host, d.Port, d.Database)
if d.Params != "" {
dsn = fmt.Sprintf("%s&%s", dsn, d.Params)
}
const driverName = "mysql"
return sql.Open(driverName, dsn)
return sql.Open("mysql", dsn)
}
func (mm *Meta) GetDialect(conn *dbi.DbConn) dbi.Dialect {

View File

@@ -97,10 +97,10 @@ func (md *MysqlMetadata) GetColumns(tableNames ...string) ([]dbi.Column, error)
columns := make([]dbi.Column, 0)
for _, re := range res {
column := dbi.Column{
TableName: cast.ToString(re["tableName"]),
ColumnName: cast.ToString(re["columnName"]),
ColumnType: cast.ToString(re["columnType"]),
DataType: cast.ToString(re["dataType"]),
ColumnComment: cast.ToString(re["columnComment"]),
Nullable: cast.ToString(re["nullable"]) == "YES",

View File

@@ -25,11 +25,6 @@ type Meta struct {
}
func (om *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
err := d.IfUseSshTunnelChangeIpPort(ctx)
if err != nil {
return nil, err
}
// 参数参考 https://github.com/sijms/go-ora?tab=readme-ov-file#other-connection-options
urlOptions := make(map[string]string)

View File

@@ -18,12 +18,13 @@ var (
Money = dbi.NewDbDataType("money", dbi.DTString).WithCT(dbi.CTVarchar)
Char = dbi.NewDbDataType("char", dbi.DTString).WithCT(dbi.CTChar)
Nchar = dbi.NewDbDataType("nchar", dbi.DTString).WithCT(dbi.CTVarchar)
Varchar = dbi.NewDbDataType("varchar", dbi.DTString).WithCT(dbi.CTVarchar)
Text = dbi.NewDbDataType("text", dbi.DTString).WithCT(dbi.CTText).WithFixColumn(dbi.ClearCharMaxLength)
Json = dbi.NewDbDataType("json", dbi.DTString).WithCT(dbi.CTJSON).WithFixColumn(dbi.ClearCharMaxLength)
Bytea = dbi.NewDbDataType("bytea", dbi.DTString).WithCT(dbi.CTBinary)
Char = dbi.NewDbDataType("char", dbi.DTStringPreserveSpecial).WithCT(dbi.CTChar)
Nchar = dbi.NewDbDataType("nchar", dbi.DTStringPreserveSpecial).WithCT(dbi.CTVarchar)
Varchar = dbi.NewDbDataType("varchar", dbi.DTStringPreserveSpecial).WithCT(dbi.CTVarchar)
Text = dbi.NewDbDataType("text", dbi.DTStringPreserveSpecial).WithCT(dbi.CTText).WithFixColumn(dbi.ClearCharMaxLength)
Json = dbi.NewDbDataType("json", dbi.DTStringPreserveSpecial).WithCT(dbi.CTJSON).WithFixColumn(dbi.ClearCharMaxLength)
Jsonb = dbi.NewDbDataType("jsonb", dbi.DTStringPreserveSpecial).WithCT(dbi.CTJSON).WithFixColumn(dbi.ClearCharMaxLength)
Bytea = dbi.NewDbDataType("bytea", dbi.DTStringPreserveSpecial).WithCT(dbi.CTBinary)
Date = dbi.NewDbDataType("date", dbi.DTDate).WithCT(dbi.CTDate).WithFixColumn(dbi.ClearCharMaxLength)
Time = dbi.NewDbDataType("time", dbi.DTTime).WithCT(dbi.CTTime).WithFixColumn(dbi.ClearCharMaxLength)

View File

@@ -3,6 +3,7 @@ package postgres
import (
"fmt"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/pkg/gox"
"time"
"github.com/spf13/cast"
@@ -26,9 +27,9 @@ func (pd *PgsqlDialect) CopyTable(copy *dbi.DbCopyTable) error {
// 复制数据
if copy.CopyData {
go func() {
gox.Go(func() {
_, _ = pd.dc.Exec(fmt.Sprintf("insert into %s select * from %s", newTableName, tableName))
}()
})
}
// 查询旧表的自增字段名 重新设置新表的序列序列器

View File

@@ -31,7 +31,7 @@ func (dh *DumpHelper) AfterInsert(writer io.Writer, tableName string, columns []
// 设置自增序列当前值
for _, column := range columns {
if column.AutoIncrement {
seq := fmt.Sprintf("SELECT setval('%s_%s_seq', (SELECT max(%s) FROM %s));\n", tableName, column.ColumnName, column.ColumnName, tableName)
seq := fmt.Sprintf("SELECT setval('%s_%s_seq', (SELECT max(%s) FROM \"%s\"));\n", tableName, column.ColumnName, column.ColumnName, tableName)
writer.Write([]byte(seq))
}
}

View File

@@ -3,16 +3,12 @@ package postgres
import (
"context"
"database/sql"
"database/sql/driver"
"fmt"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/netx"
"net"
"strings"
"time"
pq "gitee.com/liuzongyang/libpq"
_ "gitee.com/liuzongyang/libpq"
)
func init() {
@@ -39,17 +35,6 @@ type Meta struct {
}
func (pm *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
driverName := "postgres"
// SSH Conect
if d.SshTunnelMachineId > 0 {
// 如果使用了隧道,则使用`postgres:ssh:隧道机器id`注册名
driverName = fmt.Sprintf("postgres:ssh:%d", d.SshTunnelMachineId)
if !collx.ArrayContains(sql.Drivers(), driverName) {
sql.Register(driverName, &PqSqlDialer{sshTunnelMachineId: d.SshTunnelMachineId})
}
sql.Drivers()
}
db := d.Database
var dbParam string
existSchema := false
@@ -86,7 +71,7 @@ func (pm *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
dsn = fmt.Sprintf("%s %s", dsn, pm.Param)
}
return sql.Open(driverName, dsn)
return sql.Open("postgres", dsn)
}
func (pm *Meta) GetDialect(conn *dbi.DbConn) dbi.Dialect {
@@ -101,7 +86,7 @@ func (pm *Meta) GetDbDataTypes() []*dbi.DbDataType {
return collx.AsArray(
Bool, Int2, Int4, Int8, Numeric, Decimal, Smallserial, Serial, Bigserial, Largeserial,
Money,
Char, Nchar, Varchar, Text, Json,
Char, Nchar, Varchar, Text, Json, Jsonb,
Date, Time, Timestamp,
Bytea,
)
@@ -110,30 +95,3 @@ func (pm *Meta) GetDbDataTypes() []*dbi.DbDataType {
func (pm *Meta) GetCommonTypeConverter() dbi.CommonTypeConverter {
return &commonTypeConverter{}
}
// pgsql dialer
type PqSqlDialer struct {
sshTunnelMachineId int
}
func (pd *PqSqlDialer) Open(name string) (driver.Conn, error) {
return pq.DialOpen(pd, name)
}
func (pd *PqSqlDialer) Dial(network, address string) (net.Conn, error) {
// todo context.Background可能存在问题
sshTunnel, err := dbi.GetSshTunnel(context.Background(), pd.sshTunnelMachineId)
if err != nil {
return nil, err
}
if sshConn, err := sshTunnel.GetDialConn("tcp", address); err == nil {
// 将ssh conn包装否则会返回错误: ssh: tcpChan: deadline not supported
return &netx.WrapSshConn{Conn: sshConn}, nil
} else {
return nil, err
}
}
func (pd *PqSqlDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
return pd.Dial(network, address)
}

View File

@@ -3,6 +3,7 @@ package sqlite
import (
"fmt"
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/pkg/gox"
"strings"
"time"
)
@@ -36,10 +37,10 @@ func (sd *SqliteDialect) CopyTable(copy *dbi.DbCopyTable) error {
// 使用异步线程插入数据
if copy.CopyData {
go func() {
gox.Go(func() {
// 执行插入语句
_, _ = sd.dc.Exec(fmt.Sprintf("INSERT INTO \"%s\" SELECT * FROM \"%s\"", newTableName, tableName))
}()
})
}
return err

View File

@@ -6,6 +6,7 @@ import (
type DbTransferTask struct {
model.Model
model.ExtraData
TaskName string `json:"taskName" gorm:"size:255;not null;"` // 任务名称
TaskKey string `json:"taskKey" gorm:"size:100;not null;"` // 定时任务唯一uuid key

View File

@@ -5,12 +5,12 @@ import (
)
func InitIoc() {
ioc.Register(NewInstanceRepo(), ioc.WithComponentName("DbInstanceRepo"))
ioc.Register(newDbRepo(), ioc.WithComponentName("DbRepo"))
ioc.Register(newDbSqlRepo(), ioc.WithComponentName("DbSqlRepo"))
ioc.Register(newDbSqlExecRepo(), ioc.WithComponentName("DbSqlExecRepo"))
ioc.Register(newDataSyncTaskRepo(), ioc.WithComponentName("DbDataSyncTaskRepo"))
ioc.Register(newDataSyncLogRepo(), ioc.WithComponentName("DbDataSyncLogRepo"))
ioc.Register(newDbTransferTaskRepo(), ioc.WithComponentName("DbTransferTaskRepo"))
ioc.Register(newDbTransferFileRepo(), ioc.WithComponentName("DbTransferFileRepo"))
ioc.Register(NewInstanceRepo())
ioc.Register(newDbRepo())
ioc.Register(newDbSqlRepo())
ioc.Register(newDbSqlExecRepo())
ioc.Register(newDataSyncTaskRepo())
ioc.Register(newDataSyncLogRepo())
ioc.Register(newDbTransferTaskRepo())
ioc.Register(newDbTransferFileRepo())
}

View File

@@ -1,22 +1,22 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/db/ai/tools"
"mayfly-go/internal/db/api"
"mayfly-go/internal/db/application"
"mayfly-go/internal/db/infra/persistence"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()
})
initialize.AddInitFunc(application.Init)
initialize.AddTerminateFunc(Terminate)
starter.AddInitFunc(application.Init)
starter.AddTerminateFunc(Terminate)
// 注册AI数据库工具
tools.Init()
}

View File

@@ -9,6 +9,7 @@ import (
"mayfly-go/internal/docker/imsg"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/anyx"
@@ -97,9 +98,9 @@ func (d *Container) GetContainersStats(rc *req.Ctx) {
var mu sync.Mutex
allStats := make([]vo.ContainerStats, 0)
for _, c := range cs {
go func(item container.Summary) {
gox.Go(func() {
defer wg.Done()
if item.State != "running" {
if c.State != "running" {
return
}
@@ -125,7 +126,7 @@ func (d *Container) GetContainersStats(rc *req.Ctx) {
mu.Lock()
allStats = append(allStats, cs)
mu.Unlock()
}(c)
})
}
wg.Wait()
@@ -222,7 +223,7 @@ func (d *Container) ContainerLogs(rc *req.Ctx) {
biz.ErrIsNil(err)
defer logs.Close()
go func() {
gox.Go(func() {
for {
select {
case <-ctx.Done():
@@ -236,7 +237,7 @@ func (d *Container) ContainerLogs(rc *req.Ctx) {
}
}
}
}()
})
buf := make([]byte, 1024)
for {

View File

@@ -35,7 +35,7 @@ func (cc *ContainerConf) ReqConfs() *req.Confs {
}
func (cc *ContainerConf) GetContainerPage(rc *req.Ctx) {
condition := req.BindQuery[*entity.ContainerQuery](rc)
condition := req.BindQuery[entity.ContainerQuery](rc)
tags := cc.tagTreeApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeContainer)),
@@ -68,7 +68,7 @@ func (cc *ContainerConf) GetContainerPage(rc *req.Ctx) {
}
func (c *ContainerConf) Save(rc *req.Ctx) {
machineForm, container := req.BindJsonAndCopyTo[*form.ContainerSave, *entity.Container](rc)
machineForm, container := req.BindJsonAndCopyTo[form.ContainerSave, entity.Container](rc)
rc.ReqParam = machineForm
biz.ErrIsNil(c.containerApp.SaveContainer(rc.MetaCtx, &dto.SaveContainer{

View File

@@ -5,7 +5,7 @@ import (
)
func InitIoc() {
ioc.Register(new(containerAppImpl), ioc.WithComponentName("ContainerApp"))
ioc.Register(new(containerAppImpl))
}
func GetContainerApp() Container {

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"mayfly-go/internal/machine/mcm"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/pool"
@@ -149,7 +150,7 @@ func (c Client) ContainerAttach(containerID string, wsConn *websocket.Conn, rows
wsConn.WriteMessage(websocket.TextMessage, []byte("\033[2J\033[3J\033[1;1H")) // 清屏
// 转发容器输出到前端
go func() {
gox.Go(func() {
buf := make([]byte, 1024)
for {
select {
@@ -169,7 +170,7 @@ func (c Client) ContainerAttach(containerID string, wsConn *websocket.Conn, rows
wsConn.WriteMessage(websocket.TextMessage, buf[:n])
}
}
}()
})
for {
select {

View File

@@ -5,5 +5,5 @@ import (
)
func InitIoc() {
ioc.Register(newContainerRepo(), ioc.WithComponentName("ContainerRepo"))
ioc.Register(newContainerRepo())
}

View File

@@ -1,14 +1,14 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/docker/api"
"mayfly-go/internal/docker/application"
"mayfly-go/internal/docker/infra/persistence"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()

View File

@@ -51,7 +51,7 @@ func (d *Instance) ReqConfs() *req.Confs {
}
func (d *Instance) Instances(rc *req.Ctx) {
queryCond := req.BindQuery[*entity.InstanceQuery](rc)
queryCond := req.BindQuery[entity.InstanceQuery](rc)
// 只查询实例,兼容没有录入密码的实例
instTags := d.tagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
@@ -92,7 +92,7 @@ func (d *Instance) Instances(rc *req.Ctx) {
}
func (d *Instance) TestConn(rc *req.Ctx) {
fm, instance := req.BindJsonAndCopyTo[*form.InstanceForm, *entity.EsInstance](rc)
fm, instance := req.BindJsonAndCopyTo[form.InstanceForm, entity.EsInstance](rc)
var ac *tagentity.ResourceAuthCert
if len(fm.AuthCerts) > 0 {
@@ -104,7 +104,7 @@ func (d *Instance) TestConn(rc *req.Ctx) {
rc.ResData = res
}
func (d *Instance) SaveInstance(rc *req.Ctx) {
fm, instance := req.BindJsonAndCopyTo[*form.InstanceForm, *entity.EsInstance](rc)
fm, instance := req.BindJsonAndCopyTo[form.InstanceForm, entity.EsInstance](rc)
rc.ReqParam = fm
id, err := d.inst.SaveInst(rc.MetaCtx, &dto.SaveEsInstance{

View File

@@ -6,7 +6,7 @@ import (
)
func InitIoc() {
ioc.Register(new(instanceAppImpl), ioc.WithComponentName("EsInstanceApp"))
ioc.Register(new(instanceAppImpl))
}
func Init() {

View File

@@ -8,7 +8,6 @@ import (
"mayfly-go/internal/es/domain/repository"
"mayfly-go/internal/es/esm/esi"
"mayfly-go/internal/es/imsg"
"mayfly-go/internal/machine/mcm"
"mayfly-go/internal/pkg/consts"
tagapp "mayfly-go/internal/tag/application"
tagdto "mayfly-go/internal/tag/application/dto"
@@ -41,21 +40,6 @@ var _ Instance = &instanceAppImpl{}
var poolGroup = pool.NewPoolGroup[*esi.EsConn]()
func init() {
mcm.AddCheckSshTunnelMachineUseFunc(func(machineId int) bool {
items := poolGroup.AllPool()
for _, v := range items {
conn, err := v.Get(context.Background(), pool.WithGetNoUpdateLastActive(), pool.WithGetNoNewConn())
if err != nil {
continue // 获取连接失败,跳过
}
if conn.Info.SshTunnelMachineId == machineId {
return true
}
}
return false
})
}
type instanceAppImpl struct {
base.AppImpl[*entity.EsInstance, repository.EsInstance]

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