8 Commits

Author SHA1 Message Date
meilin.huang
7eb4d064ea feat: 机器脚本新增分配、组件属性类型不匹配警告调整 2025-06-16 20:13:03 +08:00
meilin.huang
cc66fcddf5 refactor: 动态路由调整&分隔面板使用element自带组件 2025-06-09 21:18:55 +08:00
meilin.huang
aac4c2b42b fix: 机器计划任务、数据库迁移任务初始化问题修复 2025-06-01 20:39:54 +08:00
meilin.huang
7a17042276 refactor: pool get options支持不创建连接 2025-05-29 20:24:48 +08:00
Coder慌
42fbfd3c47 !136 fix: 连接池修复
Merge pull request !136 from zongyangleo/dev_0529
2025-05-29 04:39:31 +00:00
zongyangleo
e273ade0b0 fix: 连接池修复 2025-05-29 11:38:29 +08:00
meilin.huang
bcaa4563ac fix: ssh tunnel检测导致死锁问题调整 2025-05-27 22:56:54 +08:00
meilin.huang
e0c01d4561 fix: 移除隧道连接时检测是否正在使用 2025-05-26 22:33:51 +08:00
93 changed files with 920 additions and 542 deletions

View File

@@ -11,42 +11,41 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@logicflow/core": "^2.0.13", "@logicflow/core": "^2.0.16",
"@logicflow/extension": "^2.0.18", "@logicflow/extension": "^2.0.21",
"@vueuse/core": "^13.2.0", "@vueuse/core": "^13.3.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0", "@xterm/addon-search": "^0.15.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"asciinema-player": "^3.9.0", "asciinema-player": "^3.10.0",
"axios": "^1.6.2", "axios": "^1.6.2",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-plus": "^2.9.11", "element-plus": "^2.10.2",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"monaco-sql-languages": "^0.14.0", "monaco-sql-languages": "^0.15.0",
"monaco-themes": "^0.4.5", "monaco-themes": "^0.4.5",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^3.0.2", "pinia": "^3.0.3",
"qrcode.vue": "^3.6.0", "qrcode.vue": "^3.6.0",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"sortablejs": "^1.15.6", "sortablejs": "^1.15.6",
"splitpanes": "^4.0.3",
"sql-formatter": "^15.6.1", "sql-formatter": "^15.6.1",
"trzsz": "^1.1.5", "trzsz": "^1.1.5",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vue": "^3.5.14", "vue": "^3.5.16",
"vue-i18n": "^11.1.3", "vue-i18n": "^11.1.5",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",
"vuedraggable": "^4.1.0" "vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.1.6", "@tailwindcss/vite": "^4.1.9",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/node": "^18.14.0", "@types/node": "^18.14.0",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
@@ -54,18 +53,18 @@
"@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4", "@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.2.4", "@vitejs/plugin-vue": "^5.2.4",
"@vue/compiler-sfc": "^3.5.14", "@vue/compiler-sfc": "^3.5.16",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"code-inspector-plugin": "^0.20.9", "code-inspector-plugin": "^0.20.9",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint": "^9.25.1", "eslint": "^9.27.0",
"eslint-plugin-vue": "^10.0.0", "eslint-plugin-vue": "^10.2.0",
"postcss": "^8.5.3", "postcss": "^8.5.4",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"sass": "^1.89.0", "sass": "^1.89.2",
"tailwindcss": "^4.1.7", "tailwindcss": "^4.1.9",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vite": "^6.3.5", "vite": "npm:rolldown-vite@latest",
"vite-plugin-progress": "0.0.7", "vite-plugin-progress": "0.0.7",
"vue-eslint-parser": "^10.1.3" "vue-eslint-parser": "^10.1.3"
}, },

View File

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

View File

@@ -42,4 +42,5 @@ export function exportFile(filename: string, content: string) {
link.setAttribute('download', `${filename}`); link.setAttribute('download', `${filename}`);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); // 下载完成后移除元素
} }

View File

@@ -12,8 +12,9 @@ const props = defineProps({
required: true, required: true,
}, },
value: { value: {
type: [Object, String, Number], type: [Object, String, Number, null],
required: true, required: true,
default: () => null,
}, },
}); });
@@ -40,7 +41,7 @@ onMounted(() => {
}); });
const convert = (value: any) => { const convert = (value: any) => {
const enumValue = EnumValue.getEnumByValue(props.enums, value) as any; const enumValue = EnumValue.getEnumByValue(props.enums, value);
if (!enumValue) { if (!enumValue) {
state.enumLabel = '-'; state.enumLabel = '-';
state.type = 'danger'; state.type = 'danger';
@@ -50,8 +51,8 @@ const convert = (value: any) => {
state.enumLabel = enumValue?.label || ''; state.enumLabel = enumValue?.label || '';
if (enumValue.tag) { if (enumValue.tag) {
state.color = enumValue.tag.color; state.color = enumValue.tag.color || '';
state.type = enumValue.tag.type; state.type = enumValue.tag.type || defaultType;
} else { } else {
state.type = defaultType; state.type = defaultType;
} }

View File

@@ -68,7 +68,7 @@
trigger="click" trigger="click"
> >
<div v-for="(item, index) in tableColumns" :key="index"> <div v-for="(item, index) in tableColumns" :key="index">
<el-checkbox v-model="item.show" :label="$t(item.label)" :true-value="true" :false-value="false" /> <el-checkbox v-model="item.show" :label="$t(item.label)" :true-value="1" :false-value="0" />
</div> </div>
<template #reference> <template #reference>
<el-button icon="Operation" circle :size="props.size"></el-button> <el-button icon="Operation" circle :size="props.size"></el-button>

View File

@@ -71,9 +71,9 @@ export class TableColumn {
formatFunc: Function; formatFunc: Function;
/** /**
* 是否显示该列 * 是否显示该列,1显示 0不显示
*/ */
show: boolean = true; show: number = 1;
/** /**
* 是否展示美化按钮主要用于美化json文本等 * 是否展示美化按钮主要用于美化json文本等

View File

@@ -59,7 +59,6 @@ export default {
docJsonError: 'Document JSON Format Error', docJsonError: 'Document JSON Format Error',
sortParams: 'Sort', sortParams: 'Sort',
otherParams: 'Other', otherParams: 'Other',
previewParams: 'Preview',
closeIndexConfirm: 'This operation will close index [{name}]. Do you want to continue?', closeIndexConfirm: 'This operation will close index [{name}]. Do you want to continue?',
openIndexConfirm: 'This operation will open index [{name}]. Do you want to continue?', openIndexConfirm: 'This operation will open index [{name}]. Do you want to continue?',
clearCacheConfirm: 'This operation will clear index [{name}] cache. Do you want to continue?', clearCacheConfirm: 'This operation will clear index [{name}] cache. Do you want to continue?',

View File

@@ -87,6 +87,8 @@ export default {
scriptResultEnumRealTime: 'Real-time', scriptResultEnumRealTime: 'Real-time',
scriptTypeEnumPrivate: 'Private', scriptTypeEnumPrivate: 'Private',
scriptTypeEnumPublic: 'Public', scriptTypeEnumPublic: 'Public',
category: 'Category',
categoryTips: 'support input new category and selection',
// security // security
cmdConfig: 'Command Config', cmdConfig: 'Command Config',

View File

@@ -25,8 +25,8 @@ export default {
success: 'Success', success: 'Success',
menuCodeTips: `The menu type is the access path (if the menu path does not begin with '/', the access address will automatically concatenate the parent menu path), otherwise it is the unique code of the resource`, menuCodeTips: `The menu type is the access path (if the menu path does not begin with '/', the access address will automatically concatenate the parent menu path), otherwise it is the unique code of the resource`,
menuCodePlaceholder: `A menu that does not begin with '/' will automatically concatenate the parent menu path`, menuCodePlaceholder: `A menu that does not begin with '/' will automatically concatenate the parent menu path`,
routerNameTips: 'For component caching to work, match the vue component name, such as ResourceLis', routerNameTips:
componentPathTips: 'Access path components, such as: ` system/resource/ResourceList `, default in ` views ` directory', 'For component caching to work, the key for route.ts in the frontend module should match the vue component name, such as ResourceList',
isCacheTips: `If yes is selected, it will be 'keepalive' cached (reentering the page without refreshing the page and requesting data again), and needs the route name to match the vue component name`, isCacheTips: `If yes is selected, it will be 'keepalive' cached (reentering the page without refreshing the page and requesting data again), and needs the route name to match the vue component name`,
isHideTips: isHideTips:
'Select Hide and the route will not appear in the menu bar, but it will still be accessible. Disabled will not be able to access and operate', 'Select Hide and the route will not appear in the menu bar, but it will still be accessible. Disabled will not be able to access and operate',

View File

@@ -58,7 +58,6 @@ export default {
docJsonError: '文档JSON格式错误', docJsonError: '文档JSON格式错误',
sortParams: '排序', sortParams: '排序',
otherParams: '其他', otherParams: '其他',
previewParams: '预览',
closeIndexConfirm: '将会关闭索引:[{name}]。 确认继续吗?', closeIndexConfirm: '将会关闭索引:[{name}]。 确认继续吗?',
openIndexConfirm: '将会打开索引:[{name}]。 确认继续吗?', openIndexConfirm: '将会打开索引:[{name}]。 确认继续吗?',
clearCacheConfirm: '将会清除索引:[{name}]缓存。 确认继续吗?', clearCacheConfirm: '将会清除索引:[{name}]缓存。 确认继续吗?',

View File

@@ -88,6 +88,8 @@ export default {
scriptResultEnumRealTime: '实时交互', scriptResultEnumRealTime: '实时交互',
scriptTypeEnumPrivate: '私有', scriptTypeEnumPrivate: '私有',
scriptTypeEnumPublic: '公共', scriptTypeEnumPublic: '公共',
category: '分类',
categoryTips: '支持输入新分类并选择',
// security // security
cmdConfig: '命令配置', cmdConfig: '命令配置',

View File

@@ -25,8 +25,7 @@ export default {
success: '成功', success: '成功',
menuCodeTips: `菜单类型则为访问路径(若菜单路径不以'/'开头则访问地址会自动拼接父菜单路径)、否则为资源唯一编码`, menuCodeTips: `菜单类型则为访问路径(若菜单路径不以'/'开头则访问地址会自动拼接父菜单路径)、否则为资源唯一编码`,
menuCodePlaceholder: `菜单不以'/'开头则自动拼接父菜单路径`, menuCodePlaceholder: `菜单不以'/'开头则自动拼接父菜单路径`,
routerNameTips: '与vue的组件名一致才可使组件缓存生效如ResourceList', routerNameTips: '前端模块下route.ts中对应的key与vue的组件名一致才可使组件缓存生效如ResourceList',
componentPathTips: '访问的组件路径,如:`system/resource/ResourceList`,默认在`views`目录下',
isCacheTips: '选择是则会被`keep-alive`缓存(重新进入页面不会刷新页面及重新请求数据)需要路由名与vue的组件名一致', isCacheTips: '选择是则会被`keep-alive`缓存(重新进入页面不会刷新页面及重新请求数据)需要路由名与vue的组件名一致',
isHideTips: '选择隐藏则路由将不会出现在菜单栏中,但仍然可以访问。禁用则不可访问与操作', isHideTips: '选择隐藏则路由将不会出现在菜单栏中,但仍然可以访问。禁用则不可访问与操作',
externalLinkTips: '内嵌: 以iframe展示、外链: 新标签打开', externalLinkTips: '内嵌: 以iframe展示、外链: 新标签打开',

View File

@@ -2,7 +2,7 @@
<div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb"> <div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb">
<SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'" @click="onThemeConfigChange" /> <SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'" @click="onThemeConfigChange" />
<el-breadcrumb class="layout-navbars-breadcrumb-hide"> <el-breadcrumb class="layout-navbars-breadcrumb-hide">
<transition-group name="breadcrumb" mode="out-in"> <transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="v.meta.title"> <el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="v.meta.title">
<span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span"> <span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" /> <SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />

View File

@@ -11,8 +11,6 @@ import 'element-plus/dist/index.css';
import 'element-plus/theme-chalk/dark/css-vars.css'; import 'element-plus/theme-chalk/dark/css-vars.css';
import { i18n } from '@/i18n/index'; import { i18n } from '@/i18n/index';
import 'splitpanes/dist/splitpanes.css';
import '@/theme/index.scss'; import '@/theme/index.scss';
import '@/theme/tailwind.css'; import '@/theme/tailwind.css';
import '@/assets/font/font.css'; import '@/assets/font/font.css';

View File

@@ -9,15 +9,21 @@ import { RouteRecordRaw } from 'vue-router';
import { LAYOUT_ROUTE_NAME } from './staticRouter'; import { LAYOUT_ROUTE_NAME } from './staticRouter';
/** /**
* 获取目录下的 .vue、.tsx 全部文件 * 获取目录下的 route.ts 全部文件
* @method import.meta.glob * @method import.meta.glob
* @link 参考https://cn.vitejs.dev/guide/features.html#json * @link 参考https://cn.vitejs.dev/guide/features.html#json
*/ */
const viewsModules: Record<string, Function> = import.meta.glob(['../views/**/*.{vue,tsx}']); const routeModules: Record<string, any> = import.meta.glob(['../views/**/route.{ts,js}'], { eager: true });
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...viewsModules });
// 后端控制路由:执行路由数据初始化 // 后端控制路由:执行路由数据初始化
export async function initBackendRoutes() { export async function initBackendRoutes() {
let allModuleRoutes = {};
for (const path in routeModules) {
// 获取默认导出的路由
const routes = routeModules[path]?.default;
allModuleRoutes = { ...allModuleRoutes, ...routes };
}
const token = getToken(); // 获取浏览器缓存 token 值 const token = getToken(); // 获取浏览器缓存 token 值
if (!token) { if (!token) {
// 无 token 停止执行下一步 // 无 token 停止执行下一步
@@ -29,7 +35,7 @@ export async function initBackendRoutes() {
const cacheList: Array<string> = []; const cacheList: Array<string> = [];
// 处理路由component // 处理路由component
const routes = backEndRouterConverter(menuRoute, (router: any) => { const routes = backEndRouterConverter(allModuleRoutes, menuRoute, (router: any) => {
// 可能为false时不存在isKeepAlive属性 // 可能为false时不存在isKeepAlive属性
if (!router.meta.isKeepAlive) { if (!router.meta.isKeepAlive) {
router.meta.isKeepAlive = false; router.meta.isKeepAlive = false;
@@ -77,9 +83,8 @@ type RouterConvCallbackFunc = (router: any) => void;
* @param name ==> title路由标题 相当于route.meta.title * @param name ==> title路由标题 相当于route.meta.title
* *
* @param meta ==> 路由菜单元信息 * @param meta ==> 路由菜单元信息
* @param meta.routeName ==> route.name -> 路由 name (对应页面组件 name, 可用作 KeepAlive 缓存标识 && 按钮权限筛选) * @param meta.routeName ==> route.name -> 路由 name (对应页面组件 name, 可用作 KeepAlive 缓存标识 && 按钮权限筛选) -> 对应模块下route.ts字段key
* @param meta.redirect ==> route.redirect -> 路由重定向地址 * @param meta.redirect ==> route.redirect -> 路由重定向地址
* @param meta.component ==> 文件路径
* @param meta.icon ==> 菜单和面包屑对应的图标 * @param meta.icon ==> 菜单和面包屑对应的图标
* @param meta.isHide ==> 是否在菜单中隐藏 (通常列表详情页需要隐藏) * @param meta.isHide ==> 是否在菜单中隐藏 (通常列表详情页需要隐藏)
* @param meta.isFull ==> 菜单是否全屏 (示例:数据大屏页面) * @param meta.isFull ==> 菜单是否全屏 (示例:数据大屏页面)
@@ -88,7 +93,7 @@ type RouterConvCallbackFunc = (router: any) => void;
* @param meta.linkType ==> 外链类型, 内嵌: 以iframe展示、外链: 新标签打开 * @param meta.linkType ==> 外链类型, 内嵌: 以iframe展示、外链: 新标签打开
* @param meta.link ==> 外链地址 * @param meta.link ==> 外链地址
* */ * */
export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') { export function backEndRouterConverter(allModuleRoutes: any, routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
if (!routes) { if (!routes) {
return []; return [];
} }
@@ -101,12 +106,6 @@ export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCall
// 将json字符串的meta转为对象 // 将json字符串的meta转为对象
item.meta = JSON.parse(item.meta); item.meta = JSON.parse(item.meta);
// 将meta.comoponet 解析为route.component
if (item.meta.component) {
item.component = dynamicImport(dynamicViewsModules, item.meta.component);
delete item.meta['component'];
}
let path = item.code; let path = item.code;
// 如果不是以 / 开头,则路径需要拼接父路径 // 如果不是以 / 开头,则路径需要拼接父路径
if (!path.startsWith('/')) { if (!path.startsWith('/')) {
@@ -121,6 +120,8 @@ export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCall
// route.name == resource.meta.routeName // route.name == resource.meta.routeName
item.name = item.meta.routeName; item.name = item.meta.routeName;
// routerName == 模块下route.ts 字段key == 组件名
item.component = allModuleRoutes[item.meta.routeName];
delete item.meta['routeName']; delete item.meta['routeName'];
// route.redirect == resource.meta.redirect // route.redirect == resource.meta.redirect
@@ -130,35 +131,9 @@ export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCall
} }
// 存在回调,则执行回调 // 存在回调,则执行回调
callbackFunc && callbackFunc(item); callbackFunc && callbackFunc(item);
item.children && backEndRouterConverter(item.children, callbackFunc, item.path); item.children && backEndRouterConverter(allModuleRoutes, item.children, callbackFunc, item.path);
routeItems.push(item); routeItems.push(item);
} }
return routeItems; return routeItems;
} }
/**
* 后端路由 component 转换函数
* @param dynamicViewsModules 获取目录下的 .vue、.tsx 全部文件
* @param component 当前要处理项 component
* @returns 返回处理成函数后的 component
*/
export function dynamicImport(dynamicViewsModules: Record<string, Function>, component: string) {
const keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => {
const k = key.replace(/..\/views|../, '');
return k.startsWith(`${component}`) || k.startsWith(`/${component}`);
});
if (matchKeys?.length === 1) {
return dynamicViewsModules[matchKeys[0]];
}
if (matchKeys?.length > 1) {
console.error('匹配到多个相似组件路径, 可添加后缀.vue或.tsx进行区分或者重命名组件名, 请调整...', matchKeys);
return null;
}
console.warn(`未匹配到[${component}]组件名对应的组件文件`);
return null;
}

View File

@@ -4,5 +4,4 @@
@use './media/media.scss'; @use './media/media.scss';
@use './waves.scss'; @use './waves.scss';
@use './dark.scss'; @use './dark.scss';
@use './iconSelector.scss'; @use './iconSelector.scss';
@use './splitpanes.scss';

View File

@@ -1,36 +0,0 @@
/** splitpanes **/
.splitpanes.default-theme .splitpanes {
background: transparent !important;
}
.splitpanes.default-theme .splitpanes__pane {
background: transparent !important;
}
.default-theme.splitpanes--vertical>.splitpanes__splitter {
border-left: 1px solid var(--bg-main-color);
}
.default-theme.splitpanes--horizontal>.splitpanes__splitter {
border-top: 1px solid var(--bg-main-color);
}
// 竖线样式
.splitpanes.default-theme .splitpanes__splitter::before,
.splitpanes.default-theme .splitpanes__splitter::after {
background-color: var(--el-color-info-light-5);
}
.splitpanes.default-theme .splitpanes__splitter:hover::before,
.splitpanes.default-theme .splitpanes__splitter:hover::after {
background-color: var(--el-color-success);
}
.splitpanes.default-theme .splitpanes__splitter {
min-width: 6px;
background: var(--el-color-info-light-8) !important;
}
.splitpanes.default-theme .splitpanes__splitter:hover {
background: var(--el-color-success-light-8) !important;
}

View File

@@ -2,5 +2,4 @@
declare module 'jsoneditor'; declare module 'jsoneditor';
declare module 'asciinema-player'; declare module 'asciinema-player';
declare module 'vue-grid-layout'; declare module 'vue-grid-layout';
declare module 'splitpanes';
declare module 'uuid'; declare module 'uuid';

View File

@@ -21,7 +21,7 @@
<enum-tag :enums="FlowBizType" :value="procinst.bizType"></enum-tag> <enum-tag :enums="FlowBizType" :value="procinst.bizType"></enum-tag>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="1" :label="$t('flow.initiator')"> <el-descriptions-item :span="1" :label="$t('flow.initiator')">
<AccountInfo :username="procinst.creator" /> <AccountInfo :username="procinst.creator || ''" />
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :span="1" :label="$t('flow.procinstStatus')"> <el-descriptions-item :span="1" :label="$t('flow.procinstStatus')">

View File

@@ -10,7 +10,7 @@
<el-table-column :label="$t('flow.approver')" min-width="100"> <el-table-column :label="$t('flow.approver')" min-width="100">
<template #default="scope"> <template #default="scope">
<AccountInfo :username="scope.row.handler" /> <AccountInfo :username="scope.row.handler || ''" />
</template> </template>
</el-table-column> </el-table-column>

View File

@@ -0,0 +1,5 @@
export default {
ProcdefList: () => import('@/views/flow/ProcdefList.vue'),
ProcinstList: () => import('@/views/flow/ProcinstList.vue'),
ProcinstTaskList: () => import('@/views/flow/ProcinstTaskList.vue'),
};

View File

@@ -0,0 +1,3 @@
export default {
Home: () => import('@/views/home/Home.vue'),
};

View File

@@ -0,0 +1,4 @@
export default {
ChannelList: () => import('@/views/msg/channel/ChannelList.vue'),
TmplList: () => import('@/views/msg/tmpl/TmplList.vue'),
};

View File

@@ -19,6 +19,7 @@ const props = defineProps({
authCerts: { authCerts: {
type: [Array<any>], type: [Array<any>],
required: true, required: true,
default: () => [],
}, },
}); });

View File

@@ -1,17 +1,16 @@
<template> <template>
<Splitpanes class="default-theme" @resize="handleResize"> <el-splitter @resize="handleResize">
<Pane :size="leftPaneSize" max-size="30"> <el-splitter-panel :size="leftPaneSize + '%'" max="30%">
<slot name="left"></slot> <slot name="left"></slot>
</Pane> </el-splitter-panel>
<Pane> <el-splitter-panel>
<slot name="right"></slot> <slot name="right"></slot>
</Pane> </el-splitter-panel>
</Splitpanes> </el-splitter>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Pane, Splitpanes } from 'splitpanes';
import { useWindowSize } from '@vueuse/core'; import { useWindowSize } from '@vueuse/core';
import { computed } from 'vue'; import { computed } from 'vue';

View File

@@ -26,6 +26,7 @@ const props = defineProps({
tags: { tags: {
type: [Array<any>], type: [Array<any>],
required: true, required: true,
default: () => [],
}, },
}); });
</script> </script>

View File

@@ -19,10 +19,9 @@
:default-expanded-keys="props.defaultExpandedKeys" :default-expanded-keys="props.defaultExpandedKeys"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<span <div
:id="node.key" :id="node.key"
@dblclick="treeNodeDblclick(data, node)" class="w-full node-container flex items-center cursor-pointer select-none"
class="node-container flex items-center cursor-pointer select-none"
:class="data.type.nodeDblclickFunc ? 'select-none' : ''" :class="data.type.nodeDblclickFunc ? 'select-none' : ''"
> >
<span v-if="data.type.value == TagTreeNode.TagPath"> <span v-if="data.type.value == TagTreeNode.TagPath">
@@ -44,7 +43,7 @@
<span class="absolute right-2.5 mt-0.5 text-[10px] text-gray-400"> <span class="absolute right-2.5 mt-0.5 text-[10px] text-gray-400">
<slot :node="node" :data="data" name="suffix"></slot> <slot :node="node" :data="data" name="suffix"></slot>
</span> </span>
</span> </div>
</template> </template>
</el-tree> </el-tree>
@@ -153,7 +152,16 @@ const loadNode = async (node: any, resolve: (data: any) => void, reject: () => v
return resolve(nodes); return resolve(nodes);
}; };
const treeNodeClick = async (data: any) => { let lastNodeClickTime = 0;
const treeNodeClick = async (data: any, node: any) => {
const currentClickNodeTime = Date.now();
if (currentClickNodeTime - lastNodeClickTime < 300) {
treeNodeDblclick(data, node);
return;
}
lastNodeClickTime = currentClickNodeTime;
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) { if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
emit('nodeClick', data); emit('nodeClick', data);
await data.type.nodeClickFunc(data); await data.type.nodeClickFunc(data);
@@ -170,7 +178,6 @@ const treeNodeDblclick = (data: any, node: any) => {
node.expand(); node.expand();
} }
// emit('nodeDblick', data);
if (!data.disabled && data.type.nodeDblclickFunc) { if (!data.disabled && data.type.nodeDblclickFunc) {
data.type.nodeDblclickFunc(data); data.type.nodeDblclickFunc(data);
} }

View File

@@ -89,7 +89,7 @@ import { Rules } from '@/common/rule';
const props = defineProps({ const props = defineProps({
instance: { instance: {
type: [Boolean, Object], type: [Boolean, Object, null],
}, },
db: { db: {
type: [Boolean, Object], type: [Boolean, Object],

View File

@@ -153,7 +153,7 @@
:close-on-click-modal="false" :close-on-click-modal="false"
v-model="sqlExecLogDialog.visible" v-model="sqlExecLogDialog.visible"
:destroy-on-close="true" :destroy-on-close="true"
body-class="h-250" body-class="h-[65vh]"
> >
<db-sql-exec-log :db-id="sqlExecLogDialog.dbId" :dbs="sqlExecLogDialog.dbs" /> <db-sql-exec-log :db-id="sqlExecLogDialog.dbId" :dbs="sqlExecLogDialog.dbs" />
</el-dialog> </el-dialog>
@@ -365,7 +365,7 @@ const editDb = (data: any) => {
state.dbEditDialog.data = { ...data }; state.dbEditDialog.data = { ...data };
} else { } else {
state.dbEditDialog.data = { state.dbEditDialog.data = {
instanceId: props.instance.id, instanceId: props.instance?.id,
}; };
} }
state.dbEditDialog.title = data ? useI18nEditTitle('db.db') : useI18nCreateTitle('db.db'); state.dbEditDialog.title = data ? useI18nEditTitle('db.db') : useI18nCreateTitle('db.db');
@@ -373,7 +373,7 @@ const editDb = (data: any) => {
}; };
const confirmEditDb = async (db: any) => { const confirmEditDb = async (db: any) => {
db.instanceId = props.instance.id; db.instanceId = props.instance?.id;
await dbApi.saveDb.request(db); await dbApi.saveDb.request(db);
useI18nSaveSuccessMsg(); useI18nSaveSuccessMsg();
search(); search();

View File

@@ -46,6 +46,7 @@ import { DbSqlExecTypeEnum, DbSqlExecStatusEnum } from './enums';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import { formatDate } from '@/common/utils/format';
const props = defineProps({ const props = defineProps({
dbId: { dbId: {
@@ -62,6 +63,21 @@ const searchItems = [
SearchItem.slot('db', 'db.db', 'dbSelect'), SearchItem.slot('db', 'db.db', 'dbSelect'),
SearchItem.input('table', 'db.table'), SearchItem.input('table', 'db.table'),
SearchItem.select('type', 'db.stmtType').withEnum(DbSqlExecTypeEnum), SearchItem.select('type', 'db.stmtType').withEnum(DbSqlExecTypeEnum),
SearchItem.input('keyword', 'common.keyword'),
SearchItem.datePicker('execTimeRange', 'db.execTime')
.withSpan(2)
.withOneProps('type', 'datetimerange')
.withOneProps('format', 'YYYY-MM-DD HH:mm:ss')
.withOneProps('value-format', 'YYYY-MM-DD HH:mm:ss')
.bindEvent('change', (value: any) => {
if (!value) {
state.query.startTime = '';
state.query.endTime = '';
return;
}
state.query.startTime = formatDate(value[0]);
state.query.endTime = formatDate(value[1]);
}),
]; ];
const columns = ref([ const columns = ref([
@@ -88,6 +104,9 @@ const state = reactive({
table: '', table: '',
status: [DbSqlExecStatusEnum.Success.value, DbSqlExecStatusEnum.Fail.value].join(','), status: [DbSqlExecStatusEnum.Success.value, DbSqlExecStatusEnum.Fail.value].join(','),
type: null, type: null,
keyword: '',
startTime: '',
endTime: '',
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
}, },

View File

@@ -1,28 +1,25 @@
<template> <template>
<div class="db-transfer-file"> <div class="db-transfer-file">
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="false" :destroy-on-close="true" width="1000px"> <el-dialog
@open="search()"
:title="title"
v-model="dialogVisible"
:close-on-click-modal="false"
:destroy-on-close="true"
body-class="h-[65vh]"
width="1000px"
>
<page-table <page-table
ref="pageTableRef" ref="pageTableRef"
:data="state.tableData"
v-model:query-form="state.query" v-model:query-form="state.query"
:page-api="dbApi.dbTransferFileList"
:lazy="true"
:show-selection="true" :show-selection="true"
v-model:selection-data="state.selectionData" v-model:selection-data="state.selectionData"
:columns="columns" :columns="columns"
@page-num-change="
(args: any) => {
state.query.pageNum = args.pageNum;
search();
}
"
@page-size-change="
(args: any) => {
state.query.pageSize = args.pageNum;
search();
}
"
> >
<template #tableHeader> <template #tableHeader>
<el-button v-auth="perms.del" :disabled="state.selectionData.length < 1" @click="del()" type="danger" icon="delete"> <el-button v-auth="perms.del" :disabled="state.selectionData.length < 1" @click="onDel()" type="danger" icon="delete">
{{ $t('common.delete') }} {{ $t('common.delete') }}
</el-button> </el-button>
</template> </template>
@@ -41,17 +38,20 @@
<template #action="{ data }"> <template #action="{ data }">
<el-button <el-button
v-if="actionBtns[perms.run] && data.status === DbTransferFileStatusEnum.Success.value" v-if="actionBtns[perms.run] && data.status === DbTransferFileStatusEnum.Success.value"
@click="openRun(data)" @click="onOpenRun(data)"
type="primary" type="primary"
link link
>{{ $t('db.run') }}</el-button
> >
<el-button v-if="data.logId" @click="openLog(data)" type="success" link>{{ $t('db.log') }}</el-button> {{ $t('db.run') }}
</el-button>
<el-button v-if="data.logId" @click="onOpenLog(data)" type="success" link>{{ $t('db.log') }}</el-button>
</template> </template>
</page-table> </page-table>
<TerminalLog v-model:log-id="state.logsDialog.logId" v-model:visible="state.logsDialog.visible" :title="state.logsDialog.title" />
</el-dialog> </el-dialog>
<TerminalLog v-model:log-id="state.logsDialog.logId" v-model:visible="state.logsDialog.visible" :title="state.logsDialog.title" />
<el-dialog :title="state.runDialog.title" v-model="state.runDialog.visible" :destroy-on-close="true" width="600px"> <el-dialog :title="state.runDialog.title" v-model="state.runDialog.visible" :destroy-on-close="true" width="600px">
<el-form :model="state.runDialog.runForm" ref="runFormRef" label-width="auto" :rules="state.runDialog.formRules"> <el-form :model="state.runDialog.runForm" ref="runFormRef" label-width="auto" :rules="state.runDialog.formRules">
<el-form-item :label="$t('db.dbFileType')" prop="dbType"> <el-form-item :label="$t('db.dbFileType')" prop="dbType">
@@ -70,17 +70,15 @@
</el-form> </el-form>
<template #footer> <template #footer>
<div> <el-button @click="state.runDialog.onCancel()">{{ $t('common.cancel') }}</el-button>
<el-button @click="state.runDialog.cancel()">{{ $t('common.cancel') }}</el-button> <el-button type="primary" :loading="state.runDialog.loading" @click="state.runDialog.onConfirm">{{ $t('common.confirm') }}</el-button>
<el-button type="primary" :loading="state.runDialog.loading" @click="state.runDialog.btnOk">{{ $t('common.confirm') }}</el-button>
</div>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, reactive, Ref, ref, watch } from 'vue'; import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue';
import { dbApi } from '@/views/ops/db/api'; import { dbApi } from '@/views/ops/db/api';
import { getDbDialect } from '@/views/ops/db/dialect'; import { getDbDialect } from '@/views/ops/db/dialect';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
@@ -109,6 +107,8 @@ const props = defineProps({
const dialogVisible = defineModel<boolean>('visible', { default: false }); const dialogVisible = defineModel<boolean>('visible', { default: false });
const pageTableRef: Ref<any> = useTemplateRef('pageTableRef');
const columns = ref([ const columns = ref([
TableColumn.new('fileKey', 'db.file').setMinWidth(280).isSlot(), TableColumn.new('fileKey', 'db.file').setMinWidth(280).isSlot(),
TableColumn.new('createTime', 'db.execTime').setMinWidth(180).isTime(), TableColumn.new('createTime', 'db.execTime').setMinWidth(180).isTime(),
@@ -168,11 +168,11 @@ const state = reactive({
targetDbType: '', targetDbType: '',
}, },
loading: false, loading: false,
cancel: function () { onCancel: function () {
state.runDialog.visible = false; state.runDialog.visible = false;
state.runDialog.runForm = {} as any; state.runDialog.runForm = {} as any;
}, },
btnOk: async function () { onConfirm: async function () {
await useI18nFormValidate(runFormRef); await useI18nFormValidate(runFormRef);
if (state.runDialog.runForm.targetDbType !== state.runDialog.runForm.dbType) { if (state.runDialog.runForm.targetDbType !== state.runDialog.runForm.dbType) {
ElMessage.warning(t('db.targetDbTypeSelectError', { dbType: state.runDialog.runForm.dbType })); ElMessage.warning(t('db.targetDbTypeSelectError', { dbType: state.runDialog.runForm.dbType }));
@@ -181,7 +181,7 @@ const state = reactive({
state.runDialog.runForm.clientId = getClientId(); state.runDialog.runForm.clientId = getClientId();
await dbApi.dbTransferFileRun.request(state.runDialog.runForm); await dbApi.dbTransferFileRun.request(state.runDialog.runForm);
useI18nOperateSuccessMsg(); useI18nOperateSuccessMsg();
state.runDialog.cancel(); state.runDialog.onCancel();
await search(); await search();
}, },
onSelectRunTargetDb: function (param: any) { onSelectRunTargetDb: function (param: any) {
@@ -195,14 +195,13 @@ const state = reactive({
}); });
const search = async () => { const search = async () => {
const { total, list } = await dbApi.dbTransferFileList.request(state.query); pageTableRef.value?.search();
state.tableData = list; // const { total, list } = await dbApi.dbTransferFileList.request(state.query);
pageTableRef.value.total = total; // state.tableData = list;
// pageTableRef.value.total = total;
}; };
const pageTableRef: Ref<any> = ref(null); const onDel = async function () {
const del = async function () {
try { try {
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).join('、')); await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).join('、'));
await dbApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') }); await dbApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
@@ -213,7 +212,7 @@ const del = async function () {
} }
}; };
const openLog = function (data: any) { const onOpenLog = function (data: any) {
state.logsDialog.logId = data.logId; state.logsDialog.logId = data.logId;
state.logsDialog.visible = true; state.logsDialog.visible = true;
state.logsDialog.title = t('db.log'); state.logsDialog.title = t('db.log');
@@ -221,7 +220,7 @@ const openLog = function (data: any) {
}; };
// 运行sql弹出选择需要运行的库默认运行当前数据库需要保证数据库类型与sql文件一致 // 运行sql弹出选择需要运行的库默认运行当前数据库需要保证数据库类型与sql文件一致
const openRun = function (data: any) { const onOpenRun = function (data: any) {
state.runDialog.runForm = { id: data.id, dbType: data.fileDbType } as any; state.runDialog.runForm = { id: data.id, dbType: data.fileDbType } as any;
state.runDialog.visible = true; state.runDialog.visible = true;
}; };

View File

@@ -155,7 +155,7 @@ const state = reactive({
}, },
dbEditDialog: { dbEditDialog: {
visible: false, visible: false,
instance: null as any, instance: {},
title: '', title: '',
}, },
}); });

View File

@@ -7,7 +7,7 @@
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
<el-tooltip :show-after="1000" class="box-item" effect="dark" content="format sql" placement="top"> <el-tooltip :show-after="1000" class="box-item" effect="dark" content="format sql" placement="top">
<el-link @click="formatSql()" type="primary" underline="never" icon="MagicStick"> </el-link> <el-link @click="onFormatSql()" type="primary" underline="never" icon="MagicStick"> </el-link>
</el-tooltip> </el-tooltip>
<el-divider direction="vertical" border-style="dashed" /> <el-divider direction="vertical" border-style="dashed" />
@@ -39,19 +39,13 @@
</div> </div>
</div> </div>
<Splitpanes <el-splitter style="height: calc(100vh - 200px)" layout="vertical" @resize-end="onResizeTableHeight">
@pane-maximize="resizeTableHeight({ panes: [{ size: 0 }] })" <el-splitter-panel :size="state.editorSize" max="80%">
@resize="resizeTableHeight"
horizontal
class="default-theme"
style="height: calc(100vh - 233px)"
>
<Pane :size="state.editorSize" max-size="80">
<MonacoEditor ref="monacoEditorRef" class="mt-1" v-model="state.sql" language="sql" height="100%" :id="'MonacoTextarea-' + getKey()" /> <MonacoEditor ref="monacoEditorRef" class="mt-1" v-model="state.sql" language="sql" height="100%" :id="'MonacoTextarea-' + getKey()" />
</Pane> </el-splitter-panel>
<Pane :size="100 - state.editorSize"> <el-splitter-panel>
<div class="mt-1 sql-exec-res !h-full"> <div class="sql-exec-res !h-full">
<el-tabs <el-tabs
class="!h-full !w-full" class="!h-full !w-full"
v-if="state.execResTabs.length > 0" v-if="state.execResTabs.length > 0"
@@ -128,8 +122,8 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
</Pane> </el-splitter-panel>
</Splitpanes> </el-splitter>
</div> </div>
</template> </template>
@@ -151,9 +145,9 @@ import { dbApi } from '../../api';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue'; import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { joinClientParams } from '@/common/request'; import { joinClientParams } from '@/common/request';
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
import { Pane, Splitpanes } from 'splitpanes';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useI18nSaveSuccessMsg } from '@/hooks/useI18n'; import { useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { useDebounceFn, useEventListener } from '@vueuse/core';
const emits = defineEmits(['saveSqlSuccess']); const emits = defineEmits(['saveSqlSuccess']);
@@ -241,10 +235,11 @@ onMounted(async () => {
console.log('in query mounted'); console.log('in query mounted');
// 第一个pane为sql editor // 第一个pane为sql editor
resizeTableHeight({ panes: [{ size: state.editorSize }] }); onResizeTableHeight(0, [-1]);
window.onresize = () => { useEventListener(
resizeTableHeight({ panes: [{ size: state.editorSize }] }); 'resize',
}; useDebounceFn(() => onResizeTableHeight(0, [-1]), 200)
);
// 默认新建一个结果集tab // 默认新建一个结果集tab
state.execResTabs.push(new ExecResTab(1)); state.execResTabs.push(new ExecResTab(1));
@@ -279,12 +274,24 @@ const onRemoveTab = (targetId: number) => {
} }
}; };
const resizeTableHeight = (e: any) => { const onResizeTableHeight = (index: number, sizes: number[]) => {
if (!sizes || sizes.length === 0) {
return;
}
const vh = window.innerHeight; const vh = window.innerHeight;
state.editorSize = e.panes[0].size; const plitpaneHeight = vh - 200;
const plitpaneHeight = vh - 233;
const editorHeight = plitpaneHeight * (state.editorSize / 100); let editorHeight = sizes[0];
state.tableDataHeight = plitpaneHeight - editorHeight - 40 + 'px'; if (editorHeight < 0 || editorHeight > plitpaneHeight - 43) {
// 默认占50%
editorHeight = plitpaneHeight / 2;
}
let tableDataHeight = plitpaneHeight - editorHeight - 43;
state.editorSize = editorHeight;
state.tableDataHeight = tableDataHeight + 'px';
}; };
const getKey = () => { const getKey = () => {
@@ -535,7 +542,7 @@ const saveSql = async () => {
/** /**
* 格式化sql * 格式化sql
*/ */
const formatSql = () => { const onFormatSql = () => {
let selection = monacoEditor.getSelection(); let selection = monacoEditor.getSelection();
if (!selection) { if (!selection) {
return; return;
@@ -715,7 +722,7 @@ const initMonacoEditor = () => {
// @param editor The editor instance is passed in as a convenience // @param editor The editor instance is passed in as a convenience
run: async function () { run: async function () {
try { try {
await formatSql(); await onFormatSql();
} catch (e: any) { } catch (e: any) {
e.message && ElMessage.error(e.message); e.message && ElMessage.error(e.message);
} }

View File

@@ -1,10 +1,9 @@
<template> <template>
<el-dialog v-model="visible" :title="title" :destroy-on-close="true" width="600px"> <el-dialog v-model="visible" :title="title" :destroy-on-close="true" width="600px" body-class="h-[65vh] overflow-auto">
<el-form ref="dataForm" :model="modelValue" :show-message="false" label-width="auto" size="small"> <el-form ref="dataForm" :model="modelValue" scroll-to-error :show-message="false" label-width="auto" size="small">
<el-form-item <el-form-item
v-for="column in columns" v-for="column in columns"
:key="column.columnName" :key="column.columnName"
class="mb-1 w-full"
:prop="column.columnName" :prop="column.columnName"
:required="props.tableName != '' && !column.nullable && !column.isPrimaryKey && !column.autoIncrement" :required="props.tableName != '' && !column.nullable && !column.isPrimaryKey && !column.autoIncrement"
> >
@@ -24,10 +23,8 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer v-if="props.tableName"> <template #footer v-if="props.tableName">
<span class="dialog-footer"> <el-button @click="onCloseDialog">{{ $t('common.cancel') }}</el-button>
<el-button @click="closeDialog">{{ $t('common.cancel') }}</el-button> <el-button type="primary" @click="onConfirm">{{ $t('common.confirm') }}</el-button>
<el-button type="primary" @click="confirm">{{ $t('common.confirm') }}</el-button>
</span>
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
@@ -79,12 +76,12 @@ const setOldValue = () => {
} }
}; };
const closeDialog = () => { const onCloseDialog = () => {
visible.value = false; visible.value = false;
modelValue.value = {}; modelValue.value = {};
}; };
const confirm = async () => { const onConfirm = async () => {
await useI18nFormValidate(dataForm); await useI18nFormValidate(dataForm);
const dbInst = props.dbInst; const dbInst = props.dbInst;
@@ -107,7 +104,7 @@ const confirm = async () => {
} }
dbInst.promptExeSql(db, sql, null, () => { dbInst.promptExeSql(db, sql, null, () => {
closeDialog(); onCloseDialog();
emit('submitSuccess'); emit('submitSuccess');
}); });
}; };

View File

@@ -0,0 +1,6 @@
export default {
InstanceList: () => import('@/views/ops/db/InstanceList.vue'),
SqlExec: () => import('@/views/ops/db/SqlExec.vue'),
SyncTaskList: () => import('@/views/ops/db/SyncTaskList.vue'),
DbTransferList: () => import('@/views/ops/db/DbTransferList.vue'),
};

View File

@@ -116,7 +116,6 @@
<template #footer> <template #footer>
<div> <div>
<el-button size="small" @click="onPreviewParam" icon="view">{{ t('es.previewParams') }}</el-button>
<el-button size="small" @click="onClearParam" icon="refresh">{{ t('common.reset') }}</el-button> <el-button size="small" @click="onClearParam" icon="refresh">{{ t('common.reset') }}</el-button>
<!-- <el-button size="small" @click="onSaveParam" type="primary" icon="check">{{ t('common.save') }}</el-button>--> <!-- <el-button size="small" @click="onSaveParam" type="primary" icon="check">{{ t('common.save') }}</el-button>-->
@@ -472,7 +471,7 @@ const onSaveParam = () => {
// 保存查询条件 // 保存查询条件
}; };
const onPreviewParam = () => { const onSearch = () => {
parseParams(); parseParams();
MonacoEditorBox({ MonacoEditorBox({
content: JSON.stringify(state.search, null, 2), content: JSON.stringify(state.search, null, 2),
@@ -480,7 +479,10 @@ const onPreviewParam = () => {
language: 'json', language: 'json',
width: state.searchBoxWidth, width: state.searchBoxWidth,
canChangeLang: false, canChangeLang: false,
options: { wordWrap: 'on', tabSize: 2, readOnly: true }, // 自动换行 options: { wordWrap: 'on', tabSize: 2, readOnly: false }, // 自动换行
confirmFn: (val: string) => {
emit('search', JSON.parse(val));
},
}); });
}; };
@@ -572,11 +574,6 @@ const parseParams = () => {
delete state.search['minimum_should_match']; delete state.search['minimum_should_match'];
} }
}; };
const onSearch = () => {
parseParams();
emit('search', state.search);
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -0,0 +1,4 @@
export default {
EsInstanceList: () => import('@/views/ops/es/EsInstanceList.vue'),
EsOperation: () => import('@/views/ops/es/EsOperation.vue'),
};

View File

@@ -61,10 +61,8 @@
</el-form> </el-form>
<template #footer> <template #footer>
<div> <el-button @click="onCancel()">{{ $t('common.cancel') }}</el-button>
<el-button @click="onCancel()">{{ $t('common.cancel') }}</el-button> <el-button type="primary" :loading="saveBtnLoading" @click="onConfirm">{{ $t('common.confirm') }}</el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="onConfirm">{{ $t('common.confirm') }}</el-button>
</div>
</template> </template>
</el-drawer> </el-drawer>
</div> </div>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="h-full"> <div class="h-full">
<ResourceOpPanel @resized="onResizeTagTree"> <ResourceOpPanel @resize="onResizeTagTree">
<template #left> <template #left>
<tag-tree <tag-tree
ref="tagTreeRef" ref="tagTreeRef"

View File

@@ -1,15 +1,20 @@
<template> <template>
<div> <div>
<el-dialog <el-drawer
:title="title" :title="title"
v-model="dialogVisible" v-model="dialogVisible"
:close-on-click-modal="false" :close-on-click-modal="false"
:before-close="cancel" :before-close="onCancel"
:show-close="true" :show-close="true"
:destroy-on-close="true" :destroy-on-close="true"
width="1000px" size="1000px"
header-class="!mb-1"
> >
<el-form :model="form" :rules="rules" ref="scriptForm" label-width="auto"> <template #header>
<DrawerHeader :header="title" :back="onCancel" />
</template>
<el-form :model="form" :rules="rules" ref="scriptForm" label-position="top">
<el-form-item prop="name" :label="$t('common.name')" required> <el-form-item prop="name" :label="$t('common.name')" required>
<el-input v-model="form.name"></el-input> <el-input v-model="form.name"></el-input>
</el-form-item> </el-form-item>
@@ -22,6 +27,12 @@
<EnumSelect :enums="ScriptResultEnum" v-model="form.type" default-first-option /> <EnumSelect :enums="ScriptResultEnum" v-model="form.type" default-first-option />
</el-form-item> </el-form-item>
<el-form-item prop="category" :label="$t('machine.category')">
<el-select v-model="form.category" filterable allow-create :placeholder="$t('machine.categoryTips')">
<el-option v-for="item in categorys" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item class="!w-full"> <el-form-item class="!w-full">
<template #label> <template #label>
<el-tooltip placement="top"> <el-tooltip placement="top">
@@ -43,14 +54,12 @@
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <el-button @click="onCancel()">{{ $t('common.cancel') }}</el-button>
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button> <el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="onConfirm">
<el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="btnOk"> {{ $t('common.save') }}
{{ $t('common.save') }} </el-button>
</el-button>
</div>
</template> </template>
</el-dialog> </el-drawer>
</div> </div>
</template> </template>
@@ -64,6 +73,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
import EnumSelect from '@/components/enumselect/EnumSelect.vue'; import EnumSelect from '@/components/enumselect/EnumSelect.vue';
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n'; import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
import { Rules } from '@/common/rule'; import { Rules } from '@/common/rule';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
const props = defineProps({ const props = defineProps({
data: { data: {
@@ -93,6 +103,7 @@ const rules = {
const { isCommon, machineId } = toRefs(props); const { isCommon, machineId } = toRefs(props);
const scriptForm: any = ref(null); const scriptForm: any = ref(null);
const categorys = ref([]);
const state = reactive({ const state = reactive({
params: [] as any, params: [] as any,
@@ -104,6 +115,7 @@ const state = reactive({
script: '', script: '',
params: '', params: '',
type: null, type: null,
category: '',
}, },
btnLoading: false, btnLoading: false,
}); });
@@ -114,6 +126,9 @@ watch(props, (newValue: any) => {
if (!dialogVisible.value) { if (!dialogVisible.value) {
return; return;
} }
machineApi.scriptCategorys.request().then((res: any) => {
categorys.value = res;
});
if (newValue.data) { if (newValue.data) {
state.form = { ...newValue.data }; state.form = { ...newValue.data };
if (state.form.params) { if (state.form.params) {
@@ -125,7 +140,7 @@ watch(props, (newValue: any) => {
} }
}); });
const btnOk = async () => { const onConfirm = async () => {
state.form.machineId = isCommon.value ? 9999999 : (machineId?.value as any); state.form.machineId = isCommon.value ? 9999999 : (machineId?.value as any);
await useI18nFormValidate(scriptForm); await useI18nFormValidate(scriptForm);
if (state.params) { if (state.params) {
@@ -134,11 +149,11 @@ const btnOk = async () => {
machineApi.saveScript.request(state.form).then(() => { machineApi.saveScript.request(state.form).then(() => {
useI18nSaveSuccessMsg(); useI18nSaveSuccessMsg();
emit('submitSuccess'); emit('submitSuccess');
cancel(); onCancel();
}); });
}; };
const cancel = () => { const onCancel = () => {
dialogVisible.value = false; dialogVisible.value = false;
emit('cancel'); emit('cancel');
state.params = []; state.params = [];

View File

@@ -68,7 +68,7 @@
:close-on-click-modal="false" :close-on-click-modal="false"
:modal="false" :modal="false"
@close="closeTermnial" @close="closeTermnial"
body-class="h-[560px]" body-class="h-[65vh]"
draggable draggable
append-to-body append-to-body
> >
@@ -99,6 +99,7 @@ import { DynamicFormDialog } from '@/components/dynamic-form';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n'; import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
import { OptionsApi } from '@/components/SearchForm/index';
const { t } = useI18n(); const { t } = useI18n();
@@ -117,11 +118,24 @@ const pageTableRef: Ref<any> = ref(null);
const state = reactive({ const state = reactive({
selectionData: [], selectionData: [],
searchItems: [SearchItem.select('type', 'common.type').withEnum(ScriptTypeEnum)], searchItems: [
SearchItem.select('type', 'common.type').withEnum(ScriptTypeEnum),
SearchItem.select('category', 'machine.category').withOptionsApi(
OptionsApi.new(machineApi.scriptCategorys, {}).withConvertFn((res) => {
return res.map((x: any) => {
return {
label: x,
value: x,
};
});
})
),
],
columns: [ columns: [
TableColumn.new('name', 'common.name'), TableColumn.new('name', 'common.name'),
TableColumn.new('description', 'common.remark'), TableColumn.new('description', 'common.remark'),
TableColumn.new('type', 'common.type').typeTag(ScriptResultEnum), TableColumn.new('type', 'common.type').typeTag(ScriptResultEnum),
TableColumn.new('category', 'machine.category'),
TableColumn.new('action', 'common.operation').isSlot().setMinWidth(140).alignCenter(), TableColumn.new('action', 'common.operation').isSlot().setMinWidth(140).alignCenter(),
], ],
query: { query: {

View File

@@ -23,6 +23,7 @@ export const machineApi = {
// 删除机器 // 删除机器
del: Api.newDelete('/machines/{id}'), del: Api.newDelete('/machines/{id}'),
scripts: Api.newGet('/machines/{machineId}/scripts'), scripts: Api.newGet('/machines/{machineId}/scripts'),
scriptCategorys: Api.newGet('/machines/scripts/categorys'),
runScript: Api.newGet('/machines/scripts/{scriptId}/{ac}/run'), runScript: Api.newGet('/machines/scripts/{scriptId}/{ac}/run'),
saveScript: Api.newPost('/machines/{machineId}/scripts'), saveScript: Api.newPost('/machines/{machineId}/scripts'),
deleteScript: Api.newDelete('/machines/{machineId}/scripts/{scriptId}'), deleteScript: Api.newDelete('/machines/{machineId}/scripts/{scriptId}'),

View File

@@ -9,7 +9,7 @@
:show-close="true" :show-close="true"
:destroy-on-close="true" :destroy-on-close="true"
width="65%" width="65%"
body-class="h-200" body-class="h-[65vh]"
> >
<page-table <page-table
ref="pageTableRef" ref="pageTableRef"

View File

@@ -15,14 +15,14 @@
<template #footer> <template #footer>
<el-button @click="handleClose">{{ $t('common.cancel') }}</el-button> <el-button @click="handleClose">{{ $t('common.cancel') }}</el-button>
<el-button v-auth="'machine:file:write'" type="primary" @click="updateContent">{{ $t('common.save') }}</el-button> <el-button v-loading="saveing" v-auth="'machine:file:write'" type="primary" @click="updateContent">{{ $t('common.save') }}</el-button>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, reactive, toRefs, watch } from 'vue'; import { computed, reactive, Ref, ref, toRefs, watch } from 'vue';
import { machineApi } from '../api'; import { machineApi } from '../api';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue'; import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { useI18nSaveSuccessMsg } from '@/hooks/useI18n'; import { useI18nSaveSuccessMsg } from '@/hooks/useI18n';
@@ -42,9 +42,10 @@ const emit = defineEmits(['cancel', 'update:machineId']);
const updateFileContent = machineApi.updateFileContent; const updateFileContent = machineApi.updateFileContent;
const saveing: Ref<any> = ref(false);
const state = reactive({ const state = reactive({
loadingContent: false, loadingContent: false,
content: '',
fileType: '', fileType: '',
}); });
@@ -83,17 +84,22 @@ const handleClose = () => {
}; };
const updateContent = async () => { const updateContent = async () => {
await updateFileContent.request({ try {
content: state.content, saveing.value = true;
id: props.fileId, await updateFileContent.request({
path: props.path, content: fileContent.value,
machineId: props.machineId, id: props.fileId,
authCertName: props.authCertName, path: props.path,
protocol: props.protocol, machineId: props.machineId,
}); authCertName: props.authCertName,
useI18nSaveSuccessMsg(); protocol: props.protocol,
handleClose(); });
state.content = ''; useI18nSaveSuccessMsg();
handleClose();
fileContent.value = '';
} finally {
saveing.value = false;
}
}; };
const getFileType = (path: string) => { const getFileType = (path: string) => {

View File

@@ -0,0 +1,6 @@
export default {
MachineList: () => import('@/views/ops/machine/MachineList.vue'),
MachineOp: () => import('@/views/ops/machine/MachineOp.vue'),
CronJobList: () => import('@/views/ops/machine/cronjob/CronJobList.vue'),
SecurityConfList: () => import('@/views/ops/machine/security/SecurityConfList.vue'),
};

View File

@@ -0,0 +1,4 @@
export default {
MongoList: () => import('@/views/ops/mongo/MongoList.vue'),
MongoDataOp: () => import('@/views/ops/mongo/MongoDataOp.vue'),
};

View File

@@ -43,8 +43,8 @@
</template> </template>
<template #right> <template #right>
<Splitpanes class="default-theme"> <el-splitter>
<Pane size="35" max-size="50"> <el-splitter-panel size="35%" max="50%">
<div class="key-list-vtree h-full card !p-1"> <div class="key-list-vtree h-full card !p-1">
<el-scrollbar> <el-scrollbar>
<el-row :gutter="5"> <el-row :gutter="5">
@@ -141,9 +141,9 @@
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" /> <contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
</div> </div>
</Pane> </el-splitter-panel>
<Pane> <el-splitter-panel>
<div class="h-full card !p-1 key-deatil"> <div class="h-full card !p-1 key-deatil">
<el-tabs class="h-full" @tab-remove="removeDataTab" v-model="state.activeName"> <el-tabs class="h-full" @tab-remove="removeDataTab" v-model="state.activeName">
<el-tab-pane class="h-full" closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key"> <el-tab-pane class="h-full" closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
@@ -151,8 +151,8 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
</Pane> </el-splitter-panel>
</Splitpanes> </el-splitter>
</template> </template>
</ResourceOpPanel> </ResourceOpPanel>
@@ -196,7 +196,6 @@ import { keysToTree, sortByTreeNodes, keysToList } from './utils';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu'; import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { sleep } from '@/common/utils/loading'; import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import { Splitpanes, Pane } from 'splitpanes';
import { RedisInst } from './redis'; import { RedisInst } from './redis';
import { useAutoOpenResource } from '@/store/autoOpenResource'; import { useAutoOpenResource } from '@/store/autoOpenResource';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';

View File

@@ -1,6 +1,13 @@
<template> <template>
<div> <div>
<el-dialog :title="title" v-model="dialogVisible" :show-close="true" width="1000px" @close="close()"> <el-dialog
:title="title"
v-model="dialogVisible"
:show-close="true"
width="1000px"
@close="close()"
body-class="h-[65vh] overflow-y-auto overflow-x-hidden"
>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :lg="16" :md="16"> <el-col :lg="16" :md="16">
<el-descriptions class="redis-info info-server" :title="$t('redis.redisInfoTitle')" :column="3" size="small" border> <el-descriptions class="redis-info info-server" :title="$t('redis.redisInfoTitle')" :column="3" size="small" border>
@@ -22,9 +29,9 @@
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :lg="12" :md="12"> <el-col :lg="12" :md="12">
<el-descriptions class="redis-info info-cluster" :title="$t('redis.node')" :column="3" size="small" border> <el-descriptions class="redis-info info-cluster" :title="$t('redis.node')" :column="3" size="small" border>
<el-descriptions-item :label="$t('redis.clusterEnable')">{{ info.Cluster.cluster_enabled }}</el-descriptions-item> <el-descriptions-item :label="$t('redis.clusterEnable')">{{ info.Cluster?.cluster_enabled }}</el-descriptions-item>
<el-descriptions-item label="DB">{{ info.Cluster.databases }}</el-descriptions-item> <el-descriptions-item label="DB">{{ info.Cluster?.databases }}</el-descriptions-item>
<el-descriptions-item :label="$t('redis.nodeCount')">{{ info.Cluster.nodecount }}</el-descriptions-item> <el-descriptions-item :label="$t('redis.nodeCount')">{{ info.Cluster?.nodecount }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-col> </el-col>

View File

@@ -0,0 +1,4 @@
export default {
RedisList: () => import('@/views/ops/redis/RedisList.vue'),
DataOperation: () => import('@/views/ops/redis/DataOperation.vue'),
};

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="tag-tree-list card h-full flex"> <div class="tag-tree-list card !p-2 h-full flex">
<Splitpanes class="default-theme"> <el-splitter>
<Pane size="30" min-size="25" max-size="35" class="flex flex-col flex-1"> <el-splitter-panel size="30%" min="25%" max="35%" class="flex flex-col flex-1">
<div class="card !p-1 !mb-1 !mr-1 flex justify-between"> <div class="card !p-1 !mb-1 !mr-1 flex justify-between">
<div class="mb-1"> <div class="mb-1">
<el-input v-model="filterTag" clearable :placeholder="$t('tag.nameFilterPlaceholder')" class="mr-2 !w-[200px]" /> <el-input v-model="filterTag" clearable :placeholder="$t('tag.nameFilterPlaceholder')" class="mr-2 !w-[200px]" />
@@ -63,9 +63,9 @@
</template> </template>
</el-tree> </el-tree>
</el-scrollbar> </el-scrollbar>
</Pane> </el-splitter-panel>
<Pane min-size="40" size="70"> <el-splitter-panel>
<div class="ml-2 h-full"> <div class="ml-2 h-full">
<el-tabs class="h-full" @tab-change="onTabChange" v-model="state.activeTabName" v-if="currentTag"> <el-tabs class="h-full" @tab-change="onTabChange" v-model="state.activeTabName" v-if="currentTag">
<el-tab-pane :label="$t('common.detail')" :name="TagDetail"> <el-tab-pane :label="$t('common.detail')" :name="TagDetail">
@@ -117,6 +117,7 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane <el-tab-pane
class="h-full"
:disabled="currentTag.type != TagResourceTypeEnum.Tag.value" :disabled="currentTag.type != TagResourceTypeEnum.Tag.value"
:label="`Redis (${resourceCount.redis || 0})`" :label="`Redis (${resourceCount.redis || 0})`"
:name="RedisTag" :name="RedisTag"
@@ -134,8 +135,8 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
</Pane> </el-splitter-panel>
</Splitpanes> </el-splitter>
<el-dialog width="500px" :title="saveTabDialog.title" :before-close="onCancelSaveTag" v-model="saveTabDialog.visible"> <el-dialog width="500px" :title="saveTabDialog.title" :before-close="onCancelSaveTag" v-model="saveTabDialog.visible">
<el-form ref="tagForm" :rules="rules" :model="saveTabDialog.form" label-width="auto"> <el-form ref="tagForm" :rules="rules" :model="saveTabDialog.form" label-width="auto">
@@ -167,7 +168,6 @@ import { tagApi } from './api';
import { formatDate } from '@/common/utils/format'; import { formatDate } from '@/common/utils/format';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index'; import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index';
import { useUserInfo } from '@/store/userInfo'; import { useUserInfo } from '@/store/userInfo';
import { Splitpanes, Pane } from 'splitpanes';
import { TagResourceTypeEnum } from '@/common/commonEnum'; import { TagResourceTypeEnum } from '@/common/commonEnum';
import EnumTag from '@/components/enumtag/EnumTag.vue'; import EnumTag from '@/components/enumtag/EnumTag.vue';
import EnumValue from '@/common/Enum'; import EnumValue from '@/common/Enum';

View File

@@ -0,0 +1,5 @@
export default {
TagTreeList: () => import('@/views/ops/tag/TagTreeList.vue'),
TeamList: () => import('@/views/ops/tag/TeamList.vue'),
AuthCertList: () => import('@/views/ops/tag/AuthCertList.vue'),
};

View File

@@ -58,6 +58,10 @@ import config from '@/common/config';
import { joinClientParams } from '@/common/request'; import { joinClientParams } from '@/common/request';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
defineOptions({
name: 'Personal',
});
const { t } = useI18n(); const { t } = useI18n();
const state = reactive({ const state = reactive({

View File

@@ -0,0 +1,3 @@
export default {
Personal: () => import('@/views/personal/index.vue'),
};

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<el-popover <el-popover
v-if="props.username" v-if="props.username && props.username != '-'"
@show="getAccountInfo(props.username)" @show="getAccountInfo(props.username)"
placement="top-start" placement="top-start"
:title="$t('system.account.accountInfo')" :title="$t('system.account.accountInfo')"

View File

@@ -33,16 +33,6 @@
<el-input v-model.trim="form.meta.routeName"></el-input> <el-input v-model.trim="form.meta.routeName"></el-input>
</FormItemTooltip> </FormItemTooltip>
</el-col> </el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<FormItemTooltip
class="!w-full"
:label="$t('system.menu.componentPath')"
prop="meta.component"
:tooltip="$t('system.menu.componentPathTips')"
>
<el-input v-model.trim="form.meta.component"></el-input>
</FormItemTooltip>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue"> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
<FormItemTooltip class="!w-full" :label="$t('system.menu.isCache')" prop="meta.isKeepAlive" :tooltip="$t('system.menu.isCacheTips')"> <FormItemTooltip class="!w-full" :label="$t('system.menu.isCache')" prop="meta.isKeepAlive" :tooltip="$t('system.menu.isCacheTips')">
<el-select v-model="form.meta.isKeepAlive" class="!w-full"> <el-select v-model="form.meta.isKeepAlive" class="!w-full">

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="card system-resource-list h-full flex"> <div class="card !p-2 system-resource-list h-full flex">
<Splitpanes class="default-theme"> <el-splitter>
<Pane size="30" min-size="25" max-size="35" class="flex flex-col flex-1"> <el-splitter-panel size="30%" max="35%" min="25%" class="flex flex-col flex-1">
<div class="card !p-1 mr-1 flex justify-between"> <div class="card !p-1 mr-1 flex justify-between">
<div class="mb-1"> <div class="mb-1">
<el-input v-model="filterResource" clearable :placeholder="$t('system.menu.filterPlaceholder')" class="mr-2 !w-[200px]" /> <el-input v-model="filterResource" clearable :placeholder="$t('system.menu.filterPlaceholder')" class="mr-2 !w-[200px]" />
@@ -60,9 +60,9 @@
</template> </template>
</el-tree> </el-tree>
</el-scrollbar> </el-scrollbar>
</Pane> </el-splitter-panel>
<Pane min-size="40" size="70"> <el-splitter-panel>
<div class="ml-2"> <div class="ml-2">
<el-tabs v-model="state.activeTabName" @tab-click="onTabClick" v-if="currentResource"> <el-tabs v-model="state.activeTabName" @tab-click="onTabClick" v-if="currentResource">
<el-tab-pane :label="$t('common.detail')" :name="ResourceDetail"> <el-tab-pane :label="$t('common.detail')" :name="ResourceDetail">
@@ -78,9 +78,6 @@
<el-descriptions-item v-if="currentResource.type == menuTypeValue" :label="$t('system.menu.routerName')"> <el-descriptions-item v-if="currentResource.type == menuTypeValue" :label="$t('system.menu.routerName')">
{{ currentResource.meta.routeName }} {{ currentResource.meta.routeName }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item v-if="currentResource.type == menuTypeValue" :label="$t('system.menu.componentPath')">
{{ currentResource.meta.component }}
</el-descriptions-item>
<el-descriptions-item v-if="currentResource.type == menuTypeValue" :label="$t('system.menu.isCache')"> <el-descriptions-item v-if="currentResource.type == menuTypeValue" :label="$t('system.menu.isCache')">
{{ currentResource.meta.isKeepAlive ? $t('system.menu.yes') : $t('system.menu.no') }} {{ currentResource.meta.isKeepAlive ? $t('system.menu.yes') : $t('system.menu.no') }}
</el-descriptions-item> </el-descriptions-item>
@@ -126,8 +123,8 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
</Pane> </el-splitter-panel>
</Splitpanes> </el-splitter>
<ResourceEdit <ResourceEdit
:title="dialogForm.title" :title="dialogForm.title"
@@ -152,7 +149,6 @@ import { resourceApi } from '../api';
import { formatDate } from '@/common/utils/format'; import { formatDate } from '@/common/utils/format';
import EnumTag from '@/components/enumtag/EnumTag.vue'; import EnumTag from '@/components/enumtag/EnumTag.vue';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu'; import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { Splitpanes, Pane } from 'splitpanes';
import { isPrefixSubsequence } from '@/common/utils/string'; import { isPrefixSubsequence } from '@/common/utils/string';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg } from '@/hooks/useI18n'; import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg } from '@/hooks/useI18n';

View File

@@ -0,0 +1,7 @@
export default {
AccountList: () => import('@/views/system/account/AccountList.vue'),
ResourceList: () => import('@/views/system/resource/ResourceList.vue'),
RoleList: () => import('@/views/system/role/RoleList.vue'),
ConfigList: () => import('@/views/system/config/ConfigList.vue'),
SyslogList: () => import('@/views/system/syslog/SyslogList.vue'),
};

View File

@@ -2,7 +2,7 @@
<div class="h-full"> <div class="h-full">
<page-table :page-api="logApi.list" :search-items="searchItems" v-model:query-form="query" :columns="columns"> <page-table :page-api="logApi.list" :search-items="searchItems" v-model:query-form="query" :columns="columns">
<template #creator="{ data }"> <template #creator="{ data }">
<account-info :username="data.creator" /> <account-info :username="data.creator || ''" />
</template> </template>
</page-table> </page-table>
</div> </div>

View File

@@ -58,11 +58,21 @@ const viteConfig: UserConfig = {
entryFileNames: `assets/[name]-[hash].js`, entryFileNames: `assets/[name]-[hash].js`,
chunkFileNames: `assets/[name]-[hash].js`, chunkFileNames: `assets/[name]-[hash].js`,
assetFileNames: `assets/[name]-[hash].[ext]`, assetFileNames: `assets/[name]-[hash].[ext]`,
compact: true, advancedChunks: {
manualChunks: { groups: [
vue: ['vue', 'vue-router', 'pinia'], {
echarts: ['echarts'], name: 'vue',
monaco: ['monaco-editor'], test: /(vue|vue-router|pinia)/i,
},
{
name: 'echarts',
test: /(echarts)/i,
},
{
name: 'monaco',
test: /(monaco-editor)/i,
},
],
}, },
}, },
}, },

View File

@@ -24,22 +24,22 @@ require (
github.com/mojocn/base64Captcha v1.3.8 // github.com/mojocn/base64Captcha v1.3.8 //
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.9 github.com/pkg/sftp v1.13.9
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.5.0
github.com/redis/go-redis/v9 v9.8.0 github.com/redis/go-redis/v9 v9.10.0
github.com/robfig/cron/v3 v3.0.1 // github.com/robfig/cron/v3 v3.0.1 //
github.com/sijms/go-ora/v2 v2.8.24 github.com/sijms/go-ora/v2 v2.8.24
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
github.com/veops/go-ansiterm v0.0.5 github.com/veops/go-ansiterm v0.0.5
go.mongodb.org/mongo-driver v1.16.0 // mongo go.mongodb.org/mongo-driver v1.16.0 // mongo
golang.org/x/crypto v0.38.0 // ssh golang.org/x/crypto v0.39.0 // ssh
golang.org/x/oauth2 v0.30.0 golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.14.0 golang.org/x/sync v0.15.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
// gorm // gorm
gorm.io/driver/mysql v1.5.7 gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.26.1 gorm.io/gorm v1.30.0
) )
require ( require (
@@ -94,7 +94,7 @@ require (
golang.org/x/image v0.23.0 // indirect golang.org/x/image v0.23.0 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect google.golang.org/protobuf v1.34.1 // indirect
modernc.org/libc v1.22.5 // indirect modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.5.0 // indirect

View File

@@ -19,7 +19,7 @@ type DataSyncTaskListVO struct {
type DataSyncLogListVO struct { type DataSyncLogListVO struct {
CreateTime *time.Time `json:"createTime"` CreateTime *time.Time `json:"createTime"`
DataSqlFull string `json:"dataSqlFull"` DataSqlFull string `json:"dataSqlFull"`
ResNum string `json:"resNum"` ResNum int `json:"resNum"`
ErrText string `json:"errText"` ErrText string `json:"errText"`
Status *int `json:"status"` Status *int `json:"status"`
} }

View File

@@ -17,16 +17,6 @@ func InitIoc() {
func Init() { func Init() {
sync.OnceFunc(func() { sync.OnceFunc(func() {
//if err := GetDbBackupApp().Init(); err != nil {
// panic(fmt.Sprintf("初始化 DbBackupApp 失败: %v", err))
//}
//if err := GetDbRestoreApp().Init(); err != nil {
// panic(fmt.Sprintf("初始化 DbRestoreApp 失败: %v", err))
//}
//if err := GetDbBinlogApp().Init(); err != nil {
// panic(fmt.Sprintf("初始化 DbBinlogApp 失败: %v", err))
//}
GetDataSyncTaskApp().InitCronJob() GetDataSyncTaskApp().InitCronJob()
GetDbTransferTaskApp().InitCronJob() GetDbTransferTaskApp().InitCronJob()
GetDbTransferTaskApp().TimerDeleteTransferFile() GetDbTransferTaskApp().TimerDeleteTransferFile()

View File

@@ -68,7 +68,7 @@ func (d *dbAppImpl) GetPageList(condition *entity.DbQuery, orderBy ...string) (*
func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db) error { func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db) error {
// 查找是否存在 // 查找是否存在
oldDb := &entity.Db{Name: dbEntity.Name, InstanceId: dbEntity.InstanceId} oldDb := &entity.Db{Name: dbEntity.Name, InstanceId: dbEntity.InstanceId, AuthCertName: dbEntity.AuthCertName}
authCert, err := d.resourceAuthCertApp.GetAuthCert(dbEntity.AuthCertName) authCert, err := d.resourceAuthCertApp.GetAuthCert(dbEntity.AuthCertName)
if err != nil { if err != nil {

View File

@@ -76,6 +76,7 @@ func (app *dataSyncAppImpl) Save(ctx context.Context, taskEntity *entity.DataSyn
taskEntity.TaskKey = uuid.New().String() taskEntity.TaskKey = uuid.New().String()
err = app.Insert(ctx, taskEntity) err = app.Insert(ctx, taskEntity)
} else { } else {
taskEntity.TaskKey = ""
err = app.UpdateById(ctx, taskEntity) err = app.UpdateById(ctx, taskEntity)
} }
@@ -107,15 +108,13 @@ func (app *dataSyncAppImpl) AddCronJob(ctx context.Context, taskEntity *entity.D
// 根据状态添加新的任务 // 根据状态添加新的任务
if taskEntity.Status == entity.DataSyncTaskStatusEnable { if taskEntity.Status == entity.DataSyncTaskStatusEnable {
taskId := taskEntity.Id taskId := taskEntity.Id
logx.Infof("start add the data sync task job: %s, cron[%s]", taskEntity.TaskName, taskEntity.TaskCron)
if err := scheduler.AddFunByKey(key, taskEntity.TaskCron, func() { if err := scheduler.AddFunByKey(key, taskEntity.TaskCron, func() {
logx.Infof("start the data synchronization task: %d", taskId) if err := app.RunCronJob(context.Background(), taskId); err != nil {
cancelCtx, cancelFunc := context.WithCancel(ctx) logx.Errorf("the data sync task failed to execute at a scheduled time: %s", err.Error())
defer cancelFunc()
if err := app.RunCronJob(cancelCtx, taskId); err != nil {
logx.Errorf("the data synchronization task failed to execute at a scheduled time: %s", err.Error())
} }
}); err != nil { }); err != nil {
logx.ErrorTrace("add db data sync cron job failed", err) logx.ErrorTrace("add db data sync job failed", err)
} }
} }
} }
@@ -133,6 +132,9 @@ func (app *dataSyncAppImpl) RunCronJob(ctx context.Context, id uint64) error {
if err != nil { if err != nil {
return errorx.NewBiz("task not found") return errorx.NewBiz("task not found")
} }
logx.InfofContext(ctx, "start the data sync task: %s => %s", task.TaskName, task.TaskKey)
if task.RunningState == entity.DataSyncTaskRunStateRunning { if task.RunningState == entity.DataSyncTaskRunStateRunning {
return errorx.NewBiz("the task is in progress") return errorx.NewBiz("the task is in progress")
} }
@@ -140,8 +142,6 @@ func (app *dataSyncAppImpl) RunCronJob(ctx context.Context, id uint64) error {
// 标记该任务运行中 // 标记该任务运行中
app.MarkRunning(id) app.MarkRunning(id)
logx.InfofContext(ctx, "start the data synchronization task: %s => %s", task.TaskName, task.TaskKey)
go func() { go func() {
// 通过占位符格式化sql // 通过占位符格式化sql
updSql := "" updSql := ""
@@ -408,32 +408,11 @@ func (app *dataSyncAppImpl) InitCronJob() {
// 修改执行中状态为待执行 // 修改执行中状态为待执行
_ = app.UpdateByCond(context.TODO(), &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateReady}, &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateRunning}) _ = app.UpdateByCond(context.TODO(), &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateReady}, &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateRunning})
// 把所有正常任务添加到定时任务中 if err := app.CursorByCond(&entity.DataSyncTaskQuery{Status: entity.DataSyncTaskStatusEnable}, func(dst *entity.DataSyncTask) error {
cond := new(entity.DataSyncTaskQuery) app.AddCronJob(contextx.NewTraceId(), dst)
cond.PageNum = 1 return nil
cond.PageSize = 100 }); err != nil {
cond.Status = entity.DataSyncTaskStatusEnable logx.ErrorTrace("the db data sync task failed to initialize: %v", err)
tasks, err := app.GetPageList(cond)
if err != nil {
logx.ErrorTrace("the data synchronization task failed to initialize", err)
return
}
total := tasks.Total
add := 0
for {
for _, job := range tasks.List {
app.AddCronJob(contextx.NewTraceId(), job)
add++
}
if add >= int(total) {
return
}
cond.PageNum = cond.PageNum + 1
tasks, _ = app.GetPageList(cond)
} }
} }

View File

@@ -81,7 +81,6 @@ func createSqlExecRecord(ctx context.Context, execSqlReq *dto.DbSqlExecReq, sql
dbSqlExecRecord.Sql = sql dbSqlExecRecord.Sql = sql
dbSqlExecRecord.Remark = execSqlReq.Remark dbSqlExecRecord.Remark = execSqlReq.Remark
dbSqlExecRecord.Status = entity.DbSqlExecStatusSuccess dbSqlExecRecord.Status = entity.DbSqlExecStatusSuccess
dbSqlExecRecord.FillBaseInfo(model.IdGenTypeNone, contextx.GetLoginAccount(ctx))
return dbSqlExecRecord return dbSqlExecRecord
} }
@@ -130,7 +129,7 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *dto.DbSqlExecRe
} }
execRes.ErrorMsg = err.Error() execRes.ErrorMsg = err.Error()
} else { } else {
d.saveSqlExecLog(dbSqlExecRecord, dbSqlExecRecord.Res) d.saveSqlExecLog(ctx, dbSqlExecRecord, dbSqlExecRecord.Res)
} }
allExecRes = append(allExecRes, execRes) allExecRes = append(allExecRes, execRes)
return nil return nil
@@ -191,7 +190,7 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *dto.DbSqlExecRe
} }
execRes.ErrorMsg = err.Error() execRes.ErrorMsg = err.Error()
} else { } else {
d.saveSqlExecLog(dbSqlExecRecord, execRes.Res) d.saveSqlExecLog(ctx, dbSqlExecRecord, execRes.Res)
} }
allExecRes = append(allExecRes, execRes) allExecRes = append(allExecRes, execRes)
} }
@@ -318,10 +317,10 @@ func (d *dbSqlExecAppImpl) GetPageList(condition *entity.DbSqlExecQuery, orderBy
} }
// 保存sql执行记录如果是查询类则根据系统配置判断是否保存 // 保存sql执行记录如果是查询类则根据系统配置判断是否保存
func (d *dbSqlExecAppImpl) saveSqlExecLog(dbSqlExecRecord *entity.DbSqlExec, res any) { func (d *dbSqlExecAppImpl) saveSqlExecLog(ctx context.Context, dbSqlExecRecord *entity.DbSqlExec, res any) {
if dbSqlExecRecord.Type != entity.DbSqlExecTypeQuery { if dbSqlExecRecord.Type != entity.DbSqlExecTypeQuery {
dbSqlExecRecord.Res = jsonx.ToStr(res) dbSqlExecRecord.Res = jsonx.ToStr(res)
d.dbSqlExecRepo.Insert(context.TODO(), dbSqlExecRecord) d.dbSqlExecRepo.Insert(ctx, dbSqlExecRecord)
return return
} }
@@ -329,7 +328,7 @@ func (d *dbSqlExecAppImpl) saveSqlExecLog(dbSqlExecRecord *entity.DbSqlExec, res
dbSqlExecRecord.Table = "-" dbSqlExecRecord.Table = "-"
dbSqlExecRecord.OldValue = "-" dbSqlExecRecord.OldValue = "-"
dbSqlExecRecord.Type = entity.DbSqlExecTypeQuery dbSqlExecRecord.Type = entity.DbSqlExecTypeQuery
d.dbSqlExecRepo.Insert(context.TODO(), dbSqlExecRecord) d.dbSqlExecRepo.Insert(ctx, dbSqlExecRecord)
} }
} }

View File

@@ -143,32 +143,11 @@ func (app *dbTransferAppImpl) InitCronJob() {
// 把所有运行中的文件状态设置为失败 // 把所有运行中的文件状态设置为失败
_ = app.transferFileApp.UpdateByCond(context.TODO(), &entity.DbTransferFile{Status: entity.DbTransferFileStatusFail}, &entity.DbTransferFile{Status: entity.DbTransferFileStatusRunning}) _ = app.transferFileApp.UpdateByCond(context.TODO(), &entity.DbTransferFile{Status: entity.DbTransferFileStatusFail}, &entity.DbTransferFile{Status: entity.DbTransferFileStatusRunning})
// 把所有需要定时执行的任务添加到定时任务中 if err := app.CursorByCond(&entity.DbTransferTaskQuery{Status: entity.DbTransferTaskStatusEnable, CronAble: entity.DbTransferTaskCronAbleEnable}, func(dtt *entity.DbTransferTask) error {
cond := new(entity.DbTransferTaskQuery) app.AddCronJob(contextx.NewTraceId(), dtt)
cond.PageNum = 1 return nil
cond.PageSize = 100 }); err != nil {
logx.ErrorTrace("the db data transfer task failed to initialize", err)
cond.Status = entity.DbTransferTaskStatusEnable
cond.CronAble = entity.DbTransferTaskCronAbleEnable
jobs := []entity.DbTransferTask{}
pr, _ := app.GetPageList(cond)
if nil == pr || pr.Total == 0 {
return
}
total := pr.Total
add := 0
for {
for _, job := range jobs {
app.AddCronJob(contextx.NewTraceId(), &job)
add++
}
if add >= int(total) {
return
}
cond.PageNum++
_, _ = app.GetPageList(cond)
} }
} }

View File

@@ -9,6 +9,7 @@ import (
_ "mayfly-go/internal/db/dbm/oracle" _ "mayfly-go/internal/db/dbm/oracle"
_ "mayfly-go/internal/db/dbm/postgres" _ "mayfly-go/internal/db/dbm/postgres"
_ "mayfly-go/internal/db/dbm/sqlite" _ "mayfly-go/internal/db/dbm/sqlite"
"mayfly-go/internal/machine/mcm"
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
"mayfly-go/pkg/pool" "mayfly-go/pkg/pool"
) )
@@ -17,6 +18,22 @@ var (
poolGroup = pool.NewPoolGroup[*dbi.DbConn]() 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 从连接池中获取连接信息 // GetDbConn 从连接池中获取连接信息
func GetDbConn(ctx context.Context, dbId uint64, database string, getDbInfo func() (*dbi.DbInfo, error)) (*dbi.DbConn, error) { func GetDbConn(ctx context.Context, dbId uint64, database string, getDbInfo func() (*dbi.DbInfo, error)) (*dbi.DbConn, error) {
connId := dbi.GetDbConnId(dbId, database) connId := dbi.GetDbConnId(dbId, database)

View File

@@ -65,6 +65,9 @@ type DbSqlExecQuery struct {
Table string `json:"table" form:"table"` Table string `json:"table" form:"table"`
Type int8 `json:"type" form:"type"` // 类型 Type int8 `json:"type" form:"type"` // 类型
FlowBizKey string `json:"flowBizKey" form:"flowBizKey"` FlowBizKey string `json:"flowBizKey" form:"flowBizKey"`
Keyword string `json:"keyword" form:"keyword"`
StartTime string `json:"startTime" form:"startTime"`
EndTime string `json:"endTime" form:"endTime"`
Status []int8 Status []int8
CreatorId uint64 CreatorId uint64

View File

@@ -24,6 +24,9 @@ func (d *dbSqlExecRepoImpl) GetPageList(condition *entity.DbSqlExecQuery, orderB
Eq("creator_id", condition.CreatorId). Eq("creator_id", condition.CreatorId).
Eq("flow_biz_key", condition.FlowBizKey). Eq("flow_biz_key", condition.FlowBizKey).
In("status", condition.Status). In("status", condition.Status).
Like("sql", condition.Keyword).
Ge("create_time", condition.StartTime).
Le("create_time", condition.EndTime).
RLike("db", condition.Db).OrderBy(orderBy...) RLike("db", condition.Db).OrderBy(orderBy...)
return d.PageByCond(qd, condition.PageParam) return d.PageByCond(qd, condition.PageParam)
} }

View File

@@ -8,6 +8,7 @@ import (
"mayfly-go/internal/es/domain/repository" "mayfly-go/internal/es/domain/repository"
"mayfly-go/internal/es/esm/esi" "mayfly-go/internal/es/esm/esi"
"mayfly-go/internal/es/imsg" "mayfly-go/internal/es/imsg"
"mayfly-go/internal/machine/mcm"
"mayfly-go/internal/pkg/consts" "mayfly-go/internal/pkg/consts"
tagapp "mayfly-go/internal/tag/application" tagapp "mayfly-go/internal/tag/application"
tagdto "mayfly-go/internal/tag/application/dto" tagdto "mayfly-go/internal/tag/application/dto"
@@ -40,6 +41,22 @@ var _ Instance = &instanceAppImpl{}
var poolGroup = pool.NewPoolGroup[*esi.EsConn]() 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 { type instanceAppImpl struct {
base.AppImpl[*entity.EsInstance, repository.EsInstance] base.AppImpl[*entity.EsInstance, repository.EsInstance]
@@ -53,7 +70,7 @@ func (app *instanceAppImpl) GetPageList(condition *entity.InstanceQuery, orderBy
} }
func (app *instanceAppImpl) DoConn(ctx context.Context, instanceId uint64, fn func(*esi.EsConn) error) error { func (app *instanceAppImpl) DoConn(ctx context.Context, instanceId uint64, fn func(*esi.EsConn) error) error {
p, err := poolGroup.GetChanPool(fmt.Sprintf("es-%d", instanceId), func() (*esi.EsConn, error) { p, err := poolGroup.GetCachePool(fmt.Sprintf("es-%d", instanceId), func() (*esi.EsConn, error) {
return app.createConn(context.Background(), instanceId) return app.createConn(context.Background(), instanceId)
}) })
@@ -234,6 +251,8 @@ func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error
return errorx.NewBiz("db instnace not found") return errorx.NewBiz("db instnace not found")
} }
poolGroup.Close(fmt.Sprintf("es-%d", instanceId))
return app.Tx(ctx, func(ctx context.Context) error { return app.Tx(ctx, func(ctx context.Context) error {
// 删除该实例 // 删除该实例
return app.DeleteById(ctx, instanceId) return app.DeleteById(ctx, instanceId)

View File

@@ -18,7 +18,7 @@ import (
type EsVersion string type EsVersion string
type EsInfo struct { type EsInfo struct {
model.ExtraData // 连接需要的其他额外参数json字符串如oracle数据库需要指定sid等 model.ExtraData // 连接需要的其他额外参数json字符串
InstanceId uint64 // 实例id InstanceId uint64 // 实例id
Name string Name string

View File

@@ -32,6 +32,7 @@ type MachineScriptForm struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
MachineId uint64 `json:"machineId" binding:"required"` MachineId uint64 `json:"machineId" binding:"required"`
Type int `json:"type" binding:"required"` Type int `json:"type" binding:"required"`
Category string `json:"category"`
Description string `json:"description" binding:"required"` Description string `json:"description" binding:"required"`
Params string `json:"params"` Params string `json:"params"`
Script string `json:"script" binding:"required"` Script string `json:"script" binding:"required"`

View File

@@ -28,6 +28,8 @@ func (ms *MachineScript) ReqConfs() *req.Confs {
// 获取指定机器脚本列表 // 获取指定机器脚本列表
req.NewGet(":machineId/scripts", ms.MachineScripts), req.NewGet(":machineId/scripts", ms.MachineScripts),
req.NewGet("/scripts/categorys", ms.MachineScriptCategorys),
req.NewPost(":machineId/scripts", ms.SaveMachineScript).Log(req.NewLogSave("机器-保存脚本")).RequiredPermissionCode("machine:script:save"), req.NewPost(":machineId/scripts", ms.SaveMachineScript).Log(req.NewLogSave("机器-保存脚本")).RequiredPermissionCode("machine:script:save"),
req.NewDelete(":machineId/scripts/:scriptId", ms.DeleteMachineScript).Log(req.NewLogSave("机器-删除脚本")).RequiredPermissionCode("machine:script:del"), req.NewDelete(":machineId/scripts/:scriptId", ms.DeleteMachineScript).Log(req.NewLogSave("机器-删除脚本")).RequiredPermissionCode("machine:script:del"),
@@ -39,12 +41,18 @@ func (ms *MachineScript) ReqConfs() *req.Confs {
} }
func (m *MachineScript) MachineScripts(rc *req.Ctx) { func (m *MachineScript) MachineScripts(rc *req.Ctx) {
condition := &entity.MachineScript{MachineId: GetMachineId(rc)} condition := &entity.MachineScript{MachineId: GetMachineId(rc), Category: rc.Query("category")}
res, err := m.machineScriptApp.GetPageList(condition, rc.GetPageParam()) res, err := m.machineScriptApp.GetPageList(condition, rc.GetPageParam())
biz.ErrIsNil(err) biz.ErrIsNil(err)
rc.ResData = model.PageResultConv[*entity.MachineScript, *vo.MachineScriptVO](res) rc.ResData = model.PageResultConv[*entity.MachineScript, *vo.MachineScriptVO](res)
} }
func (m *MachineScript) MachineScriptCategorys(rc *req.Ctx) {
res, err := m.machineScriptApp.GetScriptCategorys(rc.MetaCtx)
biz.ErrIsNil(err)
rc.ResData = res
}
func (m *MachineScript) SaveMachineScript(rc *req.Ctx) { func (m *MachineScript) SaveMachineScript(rc *req.Ctx) {
form, machineScript := req.BindJsonAndCopyTo[*form.MachineScriptForm, *entity.MachineScript](rc) form, machineScript := req.BindJsonAndCopyTo[*form.MachineScriptForm, *entity.MachineScript](rc)

View File

@@ -49,6 +49,7 @@ type MachineScriptVO struct {
Name *string `json:"name"` Name *string `json:"name"`
Script *string `json:"script"` Script *string `json:"script"`
Type *int `json:"type"` Type *int `json:"type"`
Category string `json:"category"`
Description *string `json:"description"` Description *string `json:"description"`
Params *string `json:"params"` Params *string `json:"params"`
MachineId *uint64 `json:"machineId"` MachineId *uint64 `json:"machineId"`

View File

@@ -101,28 +101,11 @@ func (m *machineCronJobAppImpl) InitCronJob() {
} }
}() }()
pageParam := model.PageParam{ if err := m.CursorByCond(&entity.MachineCronJob{Status: entity.MachineCronJobStatusEnable}, func(mcj *entity.MachineCronJob) error {
PageSize: 100, m.addCronJob(mcj)
PageNum: 1, return nil
} }); err != nil {
logx.ErrorTrace("the machine cronjob failed to initialize: %v", err)
var mcjs []*entity.MachineCronJob
cond := &entity.MachineCronJob{Status: entity.MachineCronJobStatusEnable}
pr, _ := m.GetPageList(cond, pageParam)
total := pr.Total
add := 0
for {
for _, mcj := range mcjs {
m.addCronJob(mcj)
add++
}
if add >= int(total) {
return
}
pageParam.PageNum = pageParam.PageNum + 1
m.GetPageList(cond, pageParam)
} }
} }

View File

@@ -7,6 +7,7 @@ import (
"mayfly-go/pkg/base" "mayfly-go/pkg/base"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/utils/collx"
) )
type MachineScript interface { type MachineScript interface {
@@ -15,11 +16,16 @@ type MachineScript interface {
// 分页获取机器脚本信息列表 // 分页获取机器脚本信息列表
GetPageList(condition *entity.MachineScript, pageParam model.PageParam, orderBy ...string) (*model.PageResult[*entity.MachineScript], error) GetPageList(condition *entity.MachineScript, pageParam model.PageParam, orderBy ...string) (*model.PageResult[*entity.MachineScript], error)
// GetScriptCategorys 获取脚本分类
GetScriptCategorys(ctx context.Context) ([]string, error)
Save(ctx context.Context, entity *entity.MachineScript) error Save(ctx context.Context, entity *entity.MachineScript) error
Delete(ctx context.Context, id uint64) Delete(ctx context.Context, id uint64)
} }
var _ (MachineScript) = (*machineScriptAppImpl)(nil)
type machineScriptAppImpl struct { type machineScriptAppImpl struct {
base.AppImpl[*entity.MachineScript, repository.MachineScript] base.AppImpl[*entity.MachineScript, repository.MachineScript]
@@ -33,6 +39,15 @@ func (m *machineScriptAppImpl) GetPageList(condition *entity.MachineScript, page
return m.GetRepo().GetPageList(condition, pageParam, orderBy...) return m.GetRepo().GetPageList(condition, pageParam, orderBy...)
} }
func (m *machineScriptAppImpl) GetScriptCategorys(ctx context.Context) ([]string, error) {
scripts, err := m.ListByCond(new(entity.MachineScript), "category")
if err != nil {
return nil, err
}
return collx.ArrayRemoveBlank(collx.ArrayDeduplicate(collx.ArrayMap(scripts, func(script *entity.MachineScript) string { return script.Category }))), nil
}
// 保存机器脚本 // 保存机器脚本
func (m *machineScriptAppImpl) Save(ctx context.Context, ms *entity.MachineScript) error { func (m *machineScriptAppImpl) Save(ctx context.Context, ms *entity.MachineScript) error {
// 如果机器id不为公共脚本id则校验机器是否存在 // 如果机器id不为公共脚本id则校验机器是否存在

View File

@@ -8,7 +8,8 @@ type MachineScript struct {
Name string `json:"name" gorm:"not null;size:255;comment:脚本名"` // 脚本名 Name string `json:"name" gorm:"not null;size:255;comment:脚本名"` // 脚本名
MachineId uint64 `json:"machineId" gorm:"not null;comment:机器id[0:公共]"` // 机器id MachineId uint64 `json:"machineId" gorm:"not null;comment:机器id[0:公共]"` // 机器id
Type int `json:"type" gorm:"comment:脚本类型[1: 有结果2无结果3实时交互]"` // 脚本类型[1: 有结果2无结果3实时交互] Type int `json:"type" gorm:"comment:脚本类型[1: 有结果2无结果3实时交互]"` // 脚本类型[1: 有结果2无结果3实时交互]
Description string `json:"description" gorm:"size:255;comment:脚本描述"` // 脚本描述 Category string `json:"category" gorm:"size:20;comment:分类"`
Params string `json:"params" gorm:"size:500;comment:脚本入参"` // 参数列表json Description string `json:"description" gorm:"size:255;comment:脚本描述"` // 脚本描述
Script string `json:"script" gorm:"type:text;comment:脚本内容"` // 脚本内容 Params string `json:"params" gorm:"size:500;comment:脚本入参"` // 参数列表json
Script string `json:"script" gorm:"type:text;comment:脚本内容"` // 脚本内容
} }

View File

@@ -2,6 +2,7 @@ package mcm
import ( import (
"context" "context"
"fmt"
"mayfly-go/pkg/pool" "mayfly-go/pkg/pool"
) )
@@ -9,6 +10,27 @@ var (
poolGroup = pool.NewPoolGroup[*Cli]() poolGroup = pool.NewPoolGroup[*Cli]()
) )
func init() {
AddCheckSshTunnelMachineUseFunc(func(machineId int) bool {
// 遍历所有redis连接实例若存在redis实例使用该ssh隧道机器则返回true表示还在使用中...
items := poolGroup.AllPool()
for _, v := range items {
if v.Stats().TotalConns == 0 {
continue // 连接池中没有连接,跳过
}
cli, err := v.Get(context.Background())
if err != nil {
continue // 获取连接失败,跳过
}
sshTunnelMachine := cli.Info.SshTunnelMachine
if sshTunnelMachine != nil && sshTunnelMachine.Id == uint64(machineId) {
return true
}
}
return false
})
}
// 从缓存中获取客户端信息,不存在则回调获取机器信息函数,并新建。 // 从缓存中获取客户端信息,不存在则回调获取机器信息函数,并新建。
// @param 机器的授权凭证名 // @param 机器的授权凭证名
func GetMachineCli(ctx context.Context, authCertName string, getMachine func(string) (*MachineInfo, error)) (*Cli, error) { func GetMachineCli(ctx context.Context, authCertName string, getMachine func(string) (*MachineInfo, error)) (*Cli, error) {
@@ -30,16 +52,15 @@ func GetMachineCli(ctx context.Context, authCertName string, getMachine func(str
// 删除指定机器缓存客户端,并关闭客户端连接 // 删除指定机器缓存客户端,并关闭客户端连接
func DeleteCli(id uint64) { func DeleteCli(id uint64) {
for _, pool := range poolGroup.AllPool() { for _, p := range poolGroup.AllPool() {
if pool.Stats().TotalConns == 0 { conn, err := p.Get(context.Background(), pool.WithGetNoUpdateLastActive(), pool.WithGetNoNewConn())
continue
}
conn, err := pool.Get(context.Background())
if err != nil { if err != nil {
continue continue
} }
if conn.Info.Id == id { if conn.Info.Id == id {
pool.Close() poolGroup.Close(conn.Info.AuthCertName)
} }
} }
// 删除隧道
tunnelPoolGroup.Close(fmt.Sprintf("machine-tunnel-%d", id))
} }

View File

@@ -10,10 +10,15 @@ import (
"mayfly-go/pkg/utils/netx" "mayfly-go/pkg/utils/netx"
"net" "net"
"sync" "sync"
"time"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
// type SshTunnelAble interface {
// GetSshTunnelMachineId() int
// }
var ( var (
// 所有检测ssh隧道机器是否被使用的函数 // 所有检测ssh隧道机器是否被使用的函数
checkSshTunnelMachineHasUseFuncs []CheckSshTunnelMachineHasUseFunc checkSshTunnelMachineHasUseFuncs []CheckSshTunnelMachineHasUseFunc
@@ -132,7 +137,20 @@ func GetSshTunnelMachine(ctx context.Context, machineId int, getMachine func(uin
logx.Infof("connect to the ssh tunnel machine for the first time[%d][%s:%d]", machineId, mi.Ip, mi.Port) logx.Infof("connect to the ssh tunnel machine for the first time[%d][%s:%d]", machineId, mi.Ip, mi.Port)
return stm, err return stm, err
}) }, pool.WithIdleTimeout[*SshTunnelMachine](50*time.Minute), pool.WithOnConnClose(func(conn *SshTunnelMachine) error {
mid := int(conn.mi.Id)
logx.Debugf("periodically check if the ssh tunnel machine [%d] is still in use...", mid)
for _, checkUseFunc := range checkSshTunnelMachineHasUseFuncs {
// 如果一个在使用则返回不关闭,不继续后续检查
if checkUseFunc(mid) {
return fmt.Errorf("ssh tunnel machine [%s] is still in use", conn.mi.Name)
}
}
return nil
}))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -142,18 +160,19 @@ func GetSshTunnelMachine(ctx context.Context, machineId int, getMachine func(uin
// 关闭ssh隧道机器的指定隧道 // 关闭ssh隧道机器的指定隧道
func CloseSshTunnelMachine(machineId uint64, tunnelId string) { func CloseSshTunnelMachine(machineId uint64, tunnelId string) {
//sshTunnelMachine := mcIdPool[machineId] sshTunnelMachinePool, ok := tunnelPoolGroup.Get(fmt.Sprintf("machine-tunnel-%d", machineId))
//if sshTunnelMachine == nil { if !ok {
// return return
//} }
// sshTunnelMachine, err := sshTunnelMachinePool.Get(context.Background())
//sshTunnelMachine.mutex.Lock() if err != nil {
//defer sshTunnelMachine.mutex.Unlock() return
//t := sshTunnelMachine.tunnels[tunnelId] }
//if t != nil { t := sshTunnelMachine.tunnels[tunnelId]
// t.Close() if t != nil {
// delete(sshTunnelMachine.tunnels, tunnelId) t.Close()
//} delete(sshTunnelMachine.tunnels, tunnelId)
}
} }
type Tunnel struct { type Tunnel struct {

View File

@@ -2,6 +2,7 @@ package mgm
import ( import (
"context" "context"
"mayfly-go/internal/machine/mcm"
"mayfly-go/pkg/pool" "mayfly-go/pkg/pool"
) )
@@ -9,6 +10,22 @@ var (
poolGroup = pool.NewPoolGroup[*MongoConn]() poolGroup = pool.NewPoolGroup[*MongoConn]()
) )
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
})
}
// 从缓存中获取mongo连接信息, 若缓存中不存在则会使用回调函数获取mongoInfo进行连接并缓存 // 从缓存中获取mongo连接信息, 若缓存中不存在则会使用回调函数获取mongoInfo进行连接并缓存
func GetMongoConn(ctx context.Context, mongoId uint64, getMongoInfo func() (*MongoInfo, error)) (*MongoConn, error) { func GetMongoConn(ctx context.Context, mongoId uint64, getMongoInfo func() (*MongoInfo, error)) (*MongoConn, error) {
pool, err := poolGroup.GetCachePool(getConnId(mongoId), func() (*MongoConn, error) { pool, err := poolGroup.GetCachePool(getConnId(mongoId), func() (*MongoConn, error) {

View File

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

View File

@@ -174,8 +174,7 @@ func (r *redisAppImpl) Delete(ctx context.Context, id uint64) error {
} }
// 如果存在连接,则关闭所有库连接信息 // 如果存在连接,则关闭所有库连接信息
for _, dbStr := range strings.Split(re.Db, ",") { for _, dbStr := range strings.Split(re.Db, ",") {
db, _ := strconv.Atoi(dbStr) rdm.CloseConn(re.Id, cast.ToInt(dbStr))
rdm.CloseConn(re.Id, db)
} }
return r.Tx(ctx, func(ctx context.Context) error { return r.Tx(ctx, func(ctx context.Context) error {

View File

@@ -2,16 +2,31 @@ package rdm
import ( import (
"context" "context"
"mayfly-go/internal/machine/mcm"
"mayfly-go/pkg/pool" "mayfly-go/pkg/pool"
) )
func init() {
}
var ( var (
poolGroup = pool.NewPoolGroup[*RedisConn]() poolGroup = pool.NewPoolGroup[*RedisConn]()
) )
func init() {
mcm.AddCheckSshTunnelMachineUseFunc(func(machineId int) bool {
// 遍历所有redis连接实例若存在redis实例使用该ssh隧道机器则返回true表示还在使用中...
items := poolGroup.AllPool()
for _, v := range items {
rc, err := v.Get(context.Background(), pool.WithGetNoUpdateLastActive(), pool.WithGetNoNewConn())
if err != nil {
continue // 获取连接失败,跳过
}
if rc.Info.SshTunnelMachineId == machineId {
return true
}
}
return false
})
}
// 从缓存中获取redis连接信息, 若缓存中不存在则会使用回调函数获取redisInfo进行连接并缓存 // 从缓存中获取redis连接信息, 若缓存中不存在则会使用回调函数获取redisInfo进行连接并缓存
func GetRedisConn(ctx context.Context, redisId uint64, db int, getRedisInfo func() (*RedisInfo, error)) (*RedisConn, error) { func GetRedisConn(ctx context.Context, redisId uint64, db int, getRedisInfo func() (*RedisInfo, error)) (*RedisConn, error) {
p, err := poolGroup.GetCachePool(getConnId(redisId, db), func() (*RedisConn, error) { p, err := poolGroup.GetCachePool(getConnId(redisId, db), func() (*RedisConn, error) {

View File

@@ -3,6 +3,7 @@ package migrations
import ( import (
esentity "mayfly-go/internal/es/domain/entity" esentity "mayfly-go/internal/es/domain/entity"
flowentity "mayfly-go/internal/flow/domain/entity" flowentity "mayfly-go/internal/flow/domain/entity"
machineentity "mayfly-go/internal/machine/domain/entity"
sysentity "mayfly-go/internal/sys/domain/entity" sysentity "mayfly-go/internal/sys/domain/entity"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"time" "time"
@@ -14,6 +15,7 @@ import (
func V1_10() []*gormigrate.Migration { func V1_10() []*gormigrate.Migration {
var migrations []*gormigrate.Migration var migrations []*gormigrate.Migration
migrations = append(migrations, V1_10_0()...) migrations = append(migrations, V1_10_0()...)
migrations = append(migrations, V1_10_1()...)
return migrations return migrations
} }
@@ -132,3 +134,22 @@ func V1_10_0() []*gormigrate.Migration {
}, },
} }
} }
func V1_10_1() []*gormigrate.Migration {
return []*gormigrate.Migration{
{
ID: "20250610-v1.10.1",
Migrate: func(tx *gorm.DB) error {
if !tx.Migrator().HasColumn(&machineentity.MachineScript{}, "category") {
if err := tx.Migrator().AddColumn(&machineentity.MachineScript{}, "category"); err != nil {
return err
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return nil
},
},
}
}

View File

@@ -62,6 +62,9 @@ type App[T model.ModelI] interface {
// @param cond 可为*model.QueryCond也可以为普通查询model // @param cond 可为*model.QueryCond也可以为普通查询model
CountByCond(cond any) int64 CountByCond(cond any) int64
// CursorByCond 根据指定条件遍历model表数据
CursorByCond(cond any, handler func(T) error) error
// Tx 执行事务操作 // Tx 执行事务操作
Tx(ctx context.Context, funcs ...func(context.Context) error) (err error) Tx(ctx context.Context, funcs ...func(context.Context) error) (err error)
} }
@@ -152,6 +155,29 @@ func (ai *AppImpl[T, R]) CountByCond(cond any) int64 {
return ai.GetRepo().CountByCond(cond) return ai.GetRepo().CountByCond(cond)
} }
func (ai *AppImpl[T, R]) CursorByCond(cond any, handler func(T) error) error {
offset := 0
batchSize := 200
for {
data, err := ai.GetRepo().SelectByCondWithOffset(cond, batchSize, offset)
if err != nil {
return err
}
if len(data) == 0 {
break
}
for _, item := range data {
if err := handler(item); err != nil {
return err
}
}
offset += len(data)
}
return nil
}
// Tx 执行事务操作 // Tx 执行事务操作
func (ai *AppImpl[T, R]) Tx(ctx context.Context, funcs ...func(context.Context) error) (err error) { func (ai *AppImpl[T, R]) Tx(ctx context.Context, funcs ...func(context.Context) error) (err error) {
dbCtx := ctx dbCtx := ctx

View File

@@ -89,6 +89,9 @@ type Repo[T model.ModelI] interface {
// CountByCond 根据指定条件统计model表的数量 // CountByCond 根据指定条件统计model表的数量
CountByCond(cond any) int64 CountByCond(cond any) int64
// SelectByCondWithOffset 根据条件查询数据并支持 offset + limit 分页
SelectByCondWithOffset(cond any, limit int, offset int) ([]T, error)
} }
var _ (Repo[*model.Model]) = (*RepoImpl[*model.Model])(nil) var _ (Repo[*model.Model]) = (*RepoImpl[*model.Model])(nil)
@@ -251,6 +254,15 @@ func (br *RepoImpl[T]) CountByCond(cond any) int64 {
return gormx.CountByCond(br.GetModel(), toQueryCond(cond)) return gormx.CountByCond(br.GetModel(), toQueryCond(cond))
} }
func (br *RepoImpl[T]) SelectByCondWithOffset(cond any, limit int, offset int) ([]T, error) {
var models []T
err := gormx.NewQuery(br.GetModel(), toQueryCond(cond)).GenGdb().Limit(limit).Offset(offset).Find(&models).Error
if err != nil {
return nil, err
}
return models, nil
}
// NewModel 新建模型实例 // NewModel 新建模型实例
func (br *RepoImpl[T]) NewModel() T { func (br *RepoImpl[T]) NewModel() T {
newModel := reflect.New(br.getModelType()).Interface() newModel := reflect.New(br.getModelType()).Interface()

View File

@@ -12,6 +12,6 @@ func (la *LoginAccount) GetAesKey() string {
// 系统账号 // 系统账号
var SysAccount = &LoginAccount{ var SysAccount = &LoginAccount{
Id: 1, Id: 0,
Username: "system", Username: "-",
} }

View File

@@ -2,41 +2,34 @@ package pool
import ( import (
"context" "context"
"math/rand"
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/stringx" "mayfly-go/pkg/utils/stringx"
"sync" "sync"
"time" "time"
) )
var CachePoolDefaultConfig = PoolConfig{
MaxConns: 1,
IdleTimeout: 60 * time.Minute,
WaitTimeout: 10 * time.Second,
HealthCheckInterval: 10 * time.Minute,
}
type cacheEntry[T Conn] struct { type cacheEntry[T Conn] struct {
conn T conn T
lastActive time.Time lastActive time.Time
} }
func (e *cacheEntry[T]) Close() {
if err := e.conn.Close(); err != nil {
logx.Errorf("cache pool - closing connection error: %v", err)
}
}
type CachePool[T Conn] struct { type CachePool[T Conn] struct {
factory func() (T, error) factory func() (T, error)
mu sync.RWMutex mu sync.RWMutex
cache map[string]*cacheEntry[T] // 使用字符串键的缓存 cache map[string]*cacheEntry[T] // 使用字符串键的缓存
config PoolConfig config PoolConfig[T]
closeCh chan struct{} closeCh chan struct{}
closed bool closed bool
} }
func NewCachePool[T Conn](factory func() (T, error), opts ...Option) *CachePool[T] { func NewCachePool[T Conn](factory func() (T, error), opts ...Option[T]) *CachePool[T] {
config := CachePoolDefaultConfig config := PoolConfig[T]{
MaxConns: 1,
IdleTimeout: 60 * time.Minute,
WaitTimeout: 10 * time.Second,
HealthCheckInterval: 10 * time.Minute,
}
for _, opt := range opts { for _, opt := range opts {
opt(&config) opt(&config)
} }
@@ -53,20 +46,40 @@ func NewCachePool[T Conn](factory func() (T, error), opts ...Option) *CachePool[
} }
// Get 获取连接(自动创建或复用缓存连接) // Get 获取连接(自动创建或复用缓存连接)
func (p *CachePool[T]) Get(ctx context.Context) (T, error) { func (p *CachePool[T]) Get(ctx context.Context, opts ...GetOption) (T, error) {
var zero T var zero T
options := defaultGetOptions // 默认更新 lastActive
for _, apply := range opts {
apply(&options)
}
// 先尝试加读锁,仅用于查找可用连接 // 先尝试加读锁,仅用于查找可用连接
p.mu.RLock() p.mu.RLock()
for _, entry := range p.cache { if len(p.cache) >= p.config.MaxConns {
if time.Since(entry.lastActive) <= p.config.IdleTimeout { keys := make([]string, 0, len(p.cache))
p.mu.RUnlock() // 找到后释放读锁 for k := range p.cache {
return entry.conn, nil keys = append(keys, k)
} }
randomKey := keys[rand.Intn(len(keys))]
entry := p.cache[randomKey]
conn := entry.conn
if options.updateLastActive {
// 更新最后活跃时间
entry.lastActive = time.Now()
}
p.mu.RUnlock()
return conn, nil
} }
p.mu.RUnlock() p.mu.RUnlock()
// 没有找到可用连接,升级为写锁进行清理和创建 if !options.newConn {
return zero, ErrNoAvailableConn
}
// 没有找到可用连接,升级为写锁进行创建
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
@@ -74,15 +87,15 @@ func (p *CachePool[T]) Get(ctx context.Context) (T, error) {
return zero, ErrPoolClosed return zero, ErrPoolClosed
} }
// 再次检查是否已有可用连接(防止并发创建 // 再次检查是否已创建(防止并发)
for key, entry := range p.cache { if len(p.cache) >= p.config.MaxConns {
if time.Since(entry.lastActive) <= p.config.IdleTimeout { for _, entry := range p.cache {
entry.lastActive = time.Now() if options.updateLastActive {
// 更新最后活跃时间
entry.lastActive = time.Now()
}
return entry.conn, nil return entry.conn, nil
} }
// 清理超时连接
entry.Close()
delete(p.cache, key)
} }
// 创建新连接 // 创建新连接
@@ -151,8 +164,9 @@ func (p *CachePool[T]) Close() {
p.closed = true p.closed = true
close(p.closeCh) close(p.closeCh)
for _, entry := range p.cache { for key, entry := range p.cache {
entry.Close() // 强制关闭连接
p.closeConn(key, entry, true)
} }
// 触发关闭回调 // 触发关闭回调
@@ -212,13 +226,49 @@ func (p *CachePool[T]) cleanupIdle() {
cutoff := time.Now().Add(-p.config.IdleTimeout) cutoff := time.Now().Add(-p.config.IdleTimeout)
for key, entry := range p.cache { for key, entry := range p.cache {
if entry.lastActive.Before(cutoff) { if entry.lastActive.Before(cutoff) || !p.ping(entry.conn) {
entry.Close() logx.Infof("cache pool - cleaning up idle connection, key: %s", key)
delete(p.cache, key) // 如果连接超时或不可用,则关闭连接
p.closeConn(key, entry, false)
} }
} }
} }
func (p *CachePool[T]) ping(conn T) bool {
done := make(chan struct{})
var result bool
go func() {
result = conn.Ping() == nil
close(done)
}()
select {
case <-done:
return result
case <-time.After(2 * time.Second): // 设置超时
logx.Debug("ping timeout")
return false // 超时认为不可用
}
}
func (p *CachePool[T]) closeConn(key string, entry *cacheEntry[T], force bool) bool {
if !force {
// 如果不是强制关闭且有连接关闭回调,则调用回调
// 如果回调返回错误,则不关闭连接
if onConnClose := p.config.OnConnClose; onConnClose != nil {
if err := onConnClose(entry.conn); err != nil {
logx.Infof("cache pool - connection close callback returned error, skip closing connection:: %v", err)
return false
}
}
}
if err := entry.conn.Close(); err != nil {
logx.Errorf("cache pool - closing connection error: %v", err)
}
delete(p.cache, key)
return true
}
// 生成缓存键 // 生成缓存键
func generateCacheKey() string { func generateCacheKey() string {
return stringx.RandUUID() return stringx.RandUUID()

View File

@@ -10,13 +10,6 @@ import (
"time" "time"
) )
var ChanPoolDefaultConfig = PoolConfig{
MaxConns: 5,
IdleTimeout: 60 * time.Minute,
WaitTimeout: 10 * time.Second,
HealthCheckInterval: 10 * time.Minute,
}
// chanConn 封装连接及其元数据 // chanConn 封装连接及其元数据
type chanConn[T Conn] struct { type chanConn[T Conn] struct {
conn T conn T
@@ -41,7 +34,7 @@ type ChanPool[T Conn] struct {
mu sync.RWMutex mu sync.RWMutex
factory func() (T, error) factory func() (T, error)
idleConns chan *chanConn[T] idleConns chan *chanConn[T]
config PoolConfig config PoolConfig[T]
currentConns int32 currentConns int32
stats PoolStats stats PoolStats
closeChan chan struct{} // 用于关闭健康检查 goroutine closeChan chan struct{} // 用于关闭健康检查 goroutine
@@ -56,9 +49,14 @@ type PoolStats struct {
WaitCount int64 // 等待连接次数 WaitCount int64 // 等待连接次数
} }
func NewChannelPool[T Conn](factory func() (T, error), opts ...Option) *ChanPool[T] { func NewChannelPool[T Conn](factory func() (T, error), opts ...Option[T]) *ChanPool[T] {
// 1. 初始化配置(使用默认值 + Option 覆盖) // 1. 初始化配置(使用默认值 + Option 覆盖)
config := ChanPoolDefaultConfig config := PoolConfig[T]{
MaxConns: 5,
IdleTimeout: 60 * time.Minute,
WaitTimeout: 10 * time.Second,
HealthCheckInterval: 10 * time.Minute,
}
for _, opt := range opts { for _, opt := range opts {
opt(&config) opt(&config)
} }
@@ -76,12 +74,17 @@ func NewChannelPool[T Conn](factory func() (T, error), opts ...Option) *ChanPool
return p return p
} }
func (p *ChanPool[T]) Get(ctx context.Context) (T, error) { func (p *ChanPool[T]) Get(ctx context.Context, opts ...GetOption) (T, error) {
connChan := make(chan T, 1) connChan := make(chan T, 1)
errChan := make(chan error, 1) errChan := make(chan error, 1)
options := defaultGetOptions // 默认更新 lastActive
for _, apply := range opts {
apply(&options)
}
go func() { go func() {
conn, err := p.get() conn, err := p.get(options)
if err != nil { if err != nil {
errChan <- err errChan <- err
} else { } else {
@@ -110,12 +113,12 @@ func (p *ChanPool[T]) Get(ctx context.Context) (T, error) {
} }
} }
func (p *ChanPool[T]) get() (T, error) { func (p *ChanPool[T]) get(opts getOptions) (T, error) {
var zero T
// 检查连接池是否已关闭 // 检查连接池是否已关闭
p.mu.RLock() p.mu.RLock()
if p.closed { if p.closed {
p.mu.RUnlock() p.mu.RUnlock()
var zero T
return zero, ErrPoolClosed return zero, ErrPoolClosed
} }
p.mu.RUnlock() p.mu.RUnlock()
@@ -125,9 +128,14 @@ func (p *ChanPool[T]) get() (T, error) {
case wrapper := <-p.idleConns: case wrapper := <-p.idleConns:
atomic.AddInt32(&p.stats.IdleConns, -1) atomic.AddInt32(&p.stats.IdleConns, -1)
atomic.AddInt32(&p.stats.ActiveConns, 1) atomic.AddInt32(&p.stats.ActiveConns, 1)
wrapper.lastActive = time.Now() if opts.updateLastActive {
wrapper.lastActive = time.Now()
}
return wrapper.conn, nil return wrapper.conn, nil
default: default:
if !opts.newConn {
return zero, ErrNoAvailableConn
}
return p.createConn() return p.createConn()
} }
} }

View File

@@ -5,51 +5,95 @@ import (
"time" "time"
) )
var ErrPoolClosed = errors.New("pool is closed") var (
ErrPoolClosed = errors.New("pool is closed")
ErrNoAvailableConn = errors.New("no available connection")
)
// PoolConfig 连接池配置 // PoolConfig 连接池配置
type PoolConfig struct { type PoolConfig[T Conn] struct {
MaxConns int // 最大连接数 MaxConns int // 最大连接数
IdleTimeout time.Duration // 空闲连接超时时间 IdleTimeout time.Duration // 空闲连接超时时间
WaitTimeout time.Duration // 获取连接超时时间 WaitTimeout time.Duration // 获取连接超时时间
HealthCheckInterval time.Duration // 健康检查间隔 HealthCheckInterval time.Duration // 健康检查间隔
OnPoolClose func() error // 连接池关闭时的回调 OnPoolClose func() error // 连接池关闭时的回调
OnConnClose func(conn T) error // 连接关闭时的回调,若err != nil则不关闭连接
} }
// Option 函数类型,用于配置 Pool // Option 函数类型,用于配置 Pool
type Option func(*PoolConfig) type Option[T Conn] func(*PoolConfig[T])
// WithMaxConns 设置最大连接数 // WithMaxConns 设置最大连接数
func WithMaxConns(maxConns int) Option { func WithMaxConns[T Conn](maxConns int) Option[T] {
return func(c *PoolConfig) { return func(c *PoolConfig[T]) {
c.MaxConns = maxConns c.MaxConns = maxConns
} }
} }
// WithIdleTimeout 设置空闲超时 // WithIdleTimeout 设置空闲超时
func WithIdleTimeout(timeout time.Duration) Option { func WithIdleTimeout[T Conn](timeout time.Duration) Option[T] {
return func(c *PoolConfig) { return func(c *PoolConfig[T]) {
c.IdleTimeout = timeout c.IdleTimeout = timeout
} }
} }
// WithWaitTimeout 设置等待超时 // WithWaitTimeout 设置等待超时
func WithWaitTimeout(timeout time.Duration) Option { func WithWaitTimeout[T Conn](timeout time.Duration) Option[T] {
return func(c *PoolConfig) { return func(c *PoolConfig[T]) {
c.WaitTimeout = timeout c.WaitTimeout = timeout
} }
} }
// WithHealthCheckInterval 设置健康检查间隔 // WithHealthCheckInterval 设置健康检查间隔
func WithHealthCheckInterval(interval time.Duration) Option { func WithHealthCheckInterval[T Conn](interval time.Duration) Option[T] {
return func(c *PoolConfig) { return func(c *PoolConfig[T]) {
c.HealthCheckInterval = interval c.HealthCheckInterval = interval
} }
} }
// WithOnPoolClose 设置连接池关闭回调 // WithOnPoolClose 设置连接池关闭回调
func WithOnPoolClose(fn func() error) Option { func WithOnPoolClose[T Conn](fn func() error) Option[T] {
return func(c *PoolConfig) { return func(c *PoolConfig[T]) {
c.OnPoolClose = fn c.OnPoolClose = fn
} }
} }
// WithOnConnClose 设置连接关闭回调, 若返回的错误不为nil则不关闭连接
func WithOnConnClose[T Conn](fn func(conn T) error) Option[T] {
return func(c *PoolConfig[T]) {
c.OnConnClose = fn
}
}
/**** GetOption Config ****/
// GetOption 用于配置 Get 的行为
type GetOption func(*getOptions)
// 控制 Get 行为的选项
type getOptions struct {
updateLastActive bool // 是否更新 lastActive默认 true
newConn bool // 连接不存在时是否创建新连接,默认 true
}
var (
defaultGetOptions = getOptions{
updateLastActive: true,
newConn: true,
}
)
// WithGetNoUpdateLastActive 返回一个 Option禁用更新 lastActive
func WithGetNoUpdateLastActive() GetOption {
return func(o *getOptions) {
o.updateLastActive = false
}
}
// WithGetNoCreateConn 禁用获取时连接不存在创建连接
func WithGetNoNewConn() GetOption {
return func(o *getOptions) {
o.newConn = false
}
}

View File

@@ -1,8 +1,11 @@
package pool package pool
import ( import (
"fmt"
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
"runtime"
"sync" "sync"
"time"
"golang.org/x/sync/singleflight" "golang.org/x/sync/singleflight"
) )
@@ -11,19 +14,23 @@ type PoolGroup[T Conn] struct {
mu sync.RWMutex mu sync.RWMutex
poolGroup map[string]Pool[T] poolGroup map[string]Pool[T]
createGroup singleflight.Group createGroup singleflight.Group
closingWg sync.WaitGroup
closingMu sync.Mutex
closingCh chan struct{} // 添加关闭通道
} }
func NewPoolGroup[T Conn]() *PoolGroup[T] { func NewPoolGroup[T Conn]() *PoolGroup[T] {
return &PoolGroup[T]{ return &PoolGroup[T]{
poolGroup: make(map[string]Pool[T]), poolGroup: make(map[string]Pool[T]),
createGroup: singleflight.Group{}, createGroup: singleflight.Group{},
closingCh: make(chan struct{}),
} }
} }
func (pg *PoolGroup[T]) GetOrCreate( func (pg *PoolGroup[T]) GetOrCreate(
key string, key string,
poolFactory func() Pool[T], poolFactory func() Pool[T],
opts ...Option, opts ...Option[T],
) (Pool[T], error) { ) (Pool[T], error) {
// 先尝试读锁获取 // 先尝试读锁获取
pg.mu.RLock() pg.mu.RLock()
@@ -63,41 +70,115 @@ func (pg *PoolGroup[T]) GetOrCreate(
} }
// GetChanPool 获取或创建 ChannelPool 类型连接池 // GetChanPool 获取或创建 ChannelPool 类型连接池
func (pg *PoolGroup[T]) GetChanPool(key string, factory func() (T, error), opts ...Option) (Pool[T], error) { func (pg *PoolGroup[T]) GetChanPool(key string, factory func() (T, error), opts ...Option[T]) (Pool[T], error) {
return pg.GetOrCreate(key, func() Pool[T] { return pg.GetOrCreate(key, func() Pool[T] {
return NewChannelPool(factory, opts...) return NewChannelPool(factory, opts...)
}, opts...) }, opts...)
} }
// GetCachePool 获取或创建 CachePool 类型连接池 // GetCachePool 获取或创建 CachePool 类型连接池
func (pg *PoolGroup[T]) GetCachePool(key string, factory func() (T, error), opts ...Option) (Pool[T], error) { func (pg *PoolGroup[T]) GetCachePool(key string, factory func() (T, error), opts ...Option[T]) (Pool[T], error) {
return pg.GetOrCreate(key, func() Pool[T] { return pg.GetOrCreate(key, func() Pool[T] {
return NewCachePool(factory, opts...) return NewCachePool(factory, opts...)
}, opts...) }, opts...)
} }
// Get 获取指定 key 的连接池
func (pg *PoolGroup[T]) Get(key string) (Pool[T], bool) {
pg.mu.RLock()
defer pg.mu.RUnlock()
if p, ok := pg.poolGroup[key]; ok {
return p, true
}
return nil, false
}
// 添加一个异步关闭的辅助函数
func (pg *PoolGroup[T]) asyncClose(pool Pool[T], key string) {
pg.closingMu.Lock()
pg.closingWg.Add(1)
pg.closingMu.Unlock()
go func() {
defer func() {
pg.closingMu.Lock()
pg.closingWg.Done()
pg.closingMu.Unlock()
}()
// 设置超时检测
done := make(chan struct{})
go func() {
pool.Close()
close(done)
}()
// 等待关闭完成或超时
select {
case <-done:
logx.Infof("pool group - pool closed successfully, key: %s", key)
case <-time.After(10 * time.Second):
logx.Errorf("pool group - pool close timeout, key: %s", key)
// 打印当前 goroutine 的堆栈信息
buf := make([]byte, 1<<16)
runtime.Stack(buf, true)
logx.Errorf("pool group - goroutine stack trace:\n%s", buf)
}
}()
}
func (pg *PoolGroup[T]) Close(key string) error { func (pg *PoolGroup[T]) Close(key string) error {
pg.mu.Lock() pg.mu.Lock()
defer pg.mu.Unlock()
if p, ok := pg.poolGroup[key]; ok { if p, ok := pg.poolGroup[key]; ok {
logx.Infof("pool group - close pool, key: %s", key) logx.Infof("pool group - closing pool, key: %s", key)
p.Close()
pg.createGroup.Forget(key) pg.createGroup.Forget(key)
delete(pg.poolGroup, key) delete(pg.poolGroup, key)
pg.mu.Unlock()
pg.asyncClose(p, key)
return nil
} }
pg.mu.Unlock()
return nil return nil
} }
func (pg *PoolGroup[T]) CloseAll() { func (pg *PoolGroup[T]) CloseAll() {
pg.mu.Lock() pg.mu.Lock()
defer pg.mu.Unlock() pools := make(map[string]Pool[T], len(pg.poolGroup))
for k, v := range pg.poolGroup {
for key := range pg.poolGroup { pools[k] = v
pg.poolGroup[key].Close()
pg.createGroup.Forget(key)
} }
pg.poolGroup = make(map[string]Pool[T]) pg.poolGroup = make(map[string]Pool[T])
pg.mu.Unlock()
// 异步关闭所有池
for key, pool := range pools {
pg.asyncClose(pool, key)
}
}
// 添加一个用于监控连接池关闭状态的方法
func (pg *PoolGroup[T]) WaitForClose(timeout time.Duration) error {
// 创建一个新的通道用于通知等待完成
done := make(chan struct{})
// 启动一个 goroutine 来等待所有关闭操作完成
go func() {
pg.closingWg.Wait()
close(done)
}()
// 等待完成或超时
select {
case <-done:
return nil
case <-time.After(timeout):
// 在超时时打印当前状态
pg.mu.RLock()
remainingPools := len(pg.poolGroup)
pg.mu.RUnlock()
logx.Errorf("pool group - close timeout, remaining pools: %d", remainingPools)
return fmt.Errorf("wait for pool group close timeout after %v", timeout)
}
} }
func (pg *PoolGroup[T]) AllPool() map[string]Pool[T] { func (pg *PoolGroup[T]) AllPool() map[string]Pool[T] {

View File

@@ -17,7 +17,7 @@ type Conn interface {
// Pool 连接池接口 // Pool 连接池接口
type Pool[T Conn] interface { type Pool[T Conn] interface {
// 核心方法 // 核心方法
Get(ctx context.Context) (T, error) Get(ctx context.Context, opts ...GetOption) (T, error)
Put(T) error Put(T) error
Close() Close()

View File

@@ -88,10 +88,10 @@ func newMockConn(id int) *mockConn {
func TestChanPool_Basic(t *testing.T) { func TestChanPool_Basic(t *testing.T) {
var idGen int var idGen int
pool := NewChannelPool(func() (Conn, error) { pool := NewChannelPool(func() (*mockConn, error) {
idGen++ idGen++
return newMockConn(idGen), nil return newMockConn(idGen), nil
}, WithMaxConns(2), WithIdleTimeout(time.Second)) }, WithMaxConns[*mockConn](2), WithIdleTimeout[*mockConn](time.Second))
ctx := context.Background() ctx := context.Background()
conn1, _ := pool.Get(ctx) conn1, _ := pool.Get(ctx)
@@ -112,9 +112,9 @@ func TestChanPool_Basic(t *testing.T) {
} }
func TestChanPool_WaitTimeout(t *testing.T) { func TestChanPool_WaitTimeout(t *testing.T) {
pool := NewChannelPool(func() (Conn, error) { pool := NewChannelPool(func() (*mockConn, error) {
return newMockConn(1), nil return newMockConn(1), nil
}, WithMaxConns(1), WithWaitTimeout(100*time.Millisecond)) }, WithMaxConns[*mockConn](1), WithWaitTimeout[*mockConn](100*time.Millisecond))
ctx := context.Background() ctx := context.Background()
conn1, _ := pool.Get(ctx) conn1, _ := pool.Get(ctx)
@@ -132,9 +132,9 @@ func TestChanPool_WaitTimeout(t *testing.T) {
} }
func TestChanPool_ContextCancel(t *testing.T) { func TestChanPool_ContextCancel(t *testing.T) {
pool := NewChannelPool(func() (Conn, error) { pool := NewChannelPool(func() (*mockConn, error) {
return newMockConn(1), nil return newMockConn(1), nil
}, WithMaxConns(1)) }, WithMaxConns[*mockConn](1))
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
conn, _ := pool.Get(ctx) conn, _ := pool.Get(ctx)
@@ -145,9 +145,9 @@ func TestChanPool_ContextCancel(t *testing.T) {
} }
func TestChanPool_Resize(t *testing.T) { func TestChanPool_Resize(t *testing.T) {
pool := NewChannelPool(func() (Conn, error) { pool := NewChannelPool(func() (*mockConn, error) {
return newMockConn(1), nil return newMockConn(1), nil
}, WithMaxConns(2)) }, WithMaxConns[*mockConn](2))
ctx := context.Background() ctx := context.Background()
conn1, _ := pool.Get(ctx) conn1, _ := pool.Get(ctx)
conn2, _ := pool.Get(ctx) conn2, _ := pool.Get(ctx)
@@ -158,9 +158,9 @@ func TestChanPool_Resize(t *testing.T) {
} }
func TestChanPool_HealthCheck(t *testing.T) { func TestChanPool_HealthCheck(t *testing.T) {
pool := NewChannelPool(func() (Conn, error) { pool := NewChannelPool(func() (*mockConn, error) {
return newMockConn(1), nil return newMockConn(1), nil
}, WithMaxConns(1), WithIdleTimeout(10*time.Millisecond), WithHealthCheckInterval(10*time.Millisecond)) }, WithMaxConns[*mockConn](1), WithIdleTimeout[*mockConn](10*time.Millisecond), WithHealthCheckInterval[*mockConn](10*time.Millisecond))
ctx := context.Background() ctx := context.Background()
conn, _ := pool.Get(ctx) conn, _ := pool.Get(ctx)
_ = pool.Put(conn) _ = pool.Put(conn)
@@ -176,26 +176,26 @@ func TestChanPool_HealthCheck(t *testing.T) {
func TestCachePool_Basic(t *testing.T) { func TestCachePool_Basic(t *testing.T) {
var idGen int var idGen int
pool := NewCachePool(func() (Conn, error) { pool := NewCachePool(func() (*mockConn, error) {
idGen++ idGen++
return newMockConn(idGen), nil return newMockConn(idGen), nil
}, WithMaxConns(2), WithIdleTimeout(time.Second)) }, WithMaxConns[*mockConn](2), WithIdleTimeout[*mockConn](time.Second))
ctx := context.Background() ctx := context.Background()
conn1, _ := pool.Get(ctx) conn1, _ := pool.Get(ctx)
_ = pool.Put(conn1)
conn2, _ := pool.Get(ctx) conn2, _ := pool.Get(ctx)
if conn1 != conn2 { if conn1 != conn2 {
t.Fatal("缓存池应复用同一连接") t.Fatal("缓存池应复用同一连接")
} }
_ = pool.Put(conn1)
_ = pool.Put(conn2) _ = pool.Put(conn2)
pool.Close() pool.Close()
} }
func TestCachePool_TimeoutCleanup(t *testing.T) { func TestCachePool_TimeoutCleanup(t *testing.T) {
pool := NewCachePool(func() (Conn, error) { pool := NewCachePool(func() (*mockConn, error) {
return newMockConn(1), nil return newMockConn(1), nil
}, WithMaxConns(1), WithIdleTimeout(10*time.Millisecond), WithHealthCheckInterval(10*time.Millisecond)) }, WithMaxConns[*mockConn](1), WithIdleTimeout[*mockConn](10*time.Millisecond), WithHealthCheckInterval[*mockConn](10*time.Millisecond))
ctx := context.Background() ctx := context.Background()
conn, _ := pool.Get(ctx) conn, _ := pool.Get(ctx)
_ = pool.Put(conn) _ = pool.Put(conn)
@@ -209,10 +209,10 @@ func TestCachePool_TimeoutCleanup(t *testing.T) {
func TestCachePool_OverMaxConns(t *testing.T) { func TestCachePool_OverMaxConns(t *testing.T) {
var idGen int var idGen int
pool := NewCachePool(func() (Conn, error) { pool := NewCachePool(func() (*mockConn, error) {
idGen++ idGen++
return newMockConn(idGen), nil return newMockConn(idGen), nil
}, WithMaxConns(1)) }, WithMaxConns[*mockConn](1))
ctx := context.Background() ctx := context.Background()
conn1, _ := pool.Get(ctx) conn1, _ := pool.Get(ctx)
_ = pool.Put(conn1) _ = pool.Put(conn1)
@@ -231,9 +231,9 @@ func TestCachePool_OverMaxConns(t *testing.T) {
} }
func TestCachePool_Resize(t *testing.T) { func TestCachePool_Resize(t *testing.T) {
pool := NewCachePool(func() (Conn, error) { pool := NewCachePool(func() (*mockConn, error) {
return newMockConn(1), nil return newMockConn(1), nil
}, WithMaxConns(2)) }, WithMaxConns[*mockConn](2))
ctx := context.Background() ctx := context.Background()
conn1, _ := pool.Get(ctx) conn1, _ := pool.Get(ctx)
_ = pool.Put(conn1) _ = pool.Put(conn1)
@@ -288,9 +288,9 @@ func TestPoolGroup_Concurrent(t *testing.T) {
// ========== 压力测试 ========== // ========== 压力测试 ==========
func BenchmarkChanPool_Concurrent(b *testing.B) { func BenchmarkChanPool_Concurrent(b *testing.B) {
pool := NewChannelPool(func() (Conn, error) { pool := NewChannelPool(func() (*mockConn, error) {
return newMockConn(1), nil return newMockConn(1), nil
}, WithMaxConns(100)) }, WithMaxConns[*mockConn](100))
b.ResetTimer() b.ResetTimer()
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
@@ -307,9 +307,9 @@ func BenchmarkChanPool_Concurrent(b *testing.B) {
} }
func BenchmarkCachePool_Concurrent(b *testing.B) { func BenchmarkCachePool_Concurrent(b *testing.B) {
pool := NewCachePool(func() (Conn, error) { pool := NewCachePool(func() (*mockConn, error) {
return newMockConn(1), nil return newMockConn(1), nil
}, WithMaxConns(100)) }, WithMaxConns[*mockConn](100))
b.ResetTimer() b.ResetTimer()
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
@@ -332,9 +332,9 @@ func TestChanPool_Stress(t *testing.T) {
iterations = 1000 iterations = 1000
) )
pool := NewChannelPool(func() (Conn, error) { pool := NewChannelPool(func() (*mockConn, error) {
return newMockConn(1), nil return newMockConn(1), nil
}, WithMaxConns(20), WithWaitTimeout(time.Second)) }, WithMaxConns[*mockConn](20), WithWaitTimeout[*mockConn](time.Second))
var wg sync.WaitGroup var wg sync.WaitGroup
var errCount int32 var errCount int32
@@ -389,9 +389,9 @@ func TestCachePool_Stress(t *testing.T) {
iterations = 1000 iterations = 1000
) )
pool := NewCachePool(func() (Conn, error) { pool := NewCachePool(func() (*mockConn, error) {
return newMockConn(1), nil return newMockConn(1), nil
}, WithMaxConns(20), WithIdleTimeout(time.Minute)) }, WithMaxConns[*mockConn](20), WithIdleTimeout[*mockConn](time.Minute))
var wg sync.WaitGroup var wg sync.WaitGroup
var errCount int32 var errCount int32
@@ -430,11 +430,11 @@ func TestCachePool_Stress(t *testing.T) {
// 测试连接池在连接失效时的行为 // 测试连接池在连接失效时的行为
func TestChanPool_InvalidConn(t *testing.T) { func TestChanPool_InvalidConn(t *testing.T) {
pool := NewChannelPool(func() (Conn, error) { pool := NewChannelPool(func() (*mockConn, error) {
conn := newMockConn(1) conn := newMockConn(1)
conn.pingErr = errors.New("connection invalid") conn.pingErr = errors.New("connection invalid")
return conn, nil return conn, nil
}, WithMaxConns(1), WithHealthCheckInterval(10*time.Millisecond)) }, WithMaxConns[*mockConn](1), WithHealthCheckInterval[*mockConn](10*time.Millisecond))
ctx := context.Background() ctx := context.Background()
conn, _ := pool.Get(ctx) conn, _ := pool.Get(ctx)
@@ -458,9 +458,9 @@ func TestChanPool_InvalidConn(t *testing.T) {
// 测试连接池在并发关闭时的行为 // 测试连接池在并发关闭时的行为
func TestChanPool_ConcurrentClose(t *testing.T) { func TestChanPool_ConcurrentClose(t *testing.T) {
pool := NewChannelPool(func() (Conn, error) { pool := NewChannelPool(func() (*mockConn, error) {
return newMockConn(1), nil return newMockConn(1), nil
}, WithMaxConns(10)) }, WithMaxConns[*mockConn](10))
var wg sync.WaitGroup var wg sync.WaitGroup
const goroutines = 10 const goroutines = 10
@@ -564,6 +564,12 @@ func TestPoolGroup_ConcurrentAccess(t *testing.T) {
} }
wg.Wait() wg.Wait()
// 等待所有池关闭完成
err := group.WaitForClose(10 * time.Second)
if err != nil {
t.Errorf("等待池关闭超时: %v", err)
}
// 验证所有池都已关闭 // 验证所有池都已关闭
pools = group.AllPool() pools = group.AllPool()
if len(pools) != 0 { if len(pools) != 0 {
@@ -597,6 +603,12 @@ func TestPoolGroup_ConcurrentClose(t *testing.T) {
} }
wg.Wait() wg.Wait()
// 等待所有池关闭完成
err := group.WaitForClose(10 * time.Second)
if err != nil {
t.Errorf("等待池关闭超时: %v", err)
}
// 验证所有池都已关闭 // 验证所有池都已关闭
pools := group.AllPool() pools := group.AllPool()
if len(pools) != 0 { if len(pools) != 0 {