mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 08:20:25 +08:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					7eb4d064ea | ||
| 
						 | 
					cc66fcddf5 | ||
| 
						 | 
					aac4c2b42b | ||
| 
						 | 
					7a17042276 | ||
| 
						 | 
					42fbfd3c47 | ||
| 
						 | 
					e273ade0b0 | ||
| 
						 | 
					bcaa4563ac | ||
| 
						 | 
					e0c01d4561 | 
@@ -11,42 +11,41 @@
 | 
			
		||||
    },
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "@element-plus/icons-vue": "^2.3.1",
 | 
			
		||||
        "@logicflow/core": "^2.0.13",
 | 
			
		||||
        "@logicflow/extension": "^2.0.18",
 | 
			
		||||
        "@vueuse/core": "^13.2.0",
 | 
			
		||||
        "@logicflow/core": "^2.0.16",
 | 
			
		||||
        "@logicflow/extension": "^2.0.21",
 | 
			
		||||
        "@vueuse/core": "^13.3.0",
 | 
			
		||||
        "@xterm/addon-fit": "^0.10.0",
 | 
			
		||||
        "@xterm/addon-search": "^0.15.0",
 | 
			
		||||
        "@xterm/addon-web-links": "^0.11.0",
 | 
			
		||||
        "@xterm/xterm": "^5.5.0",
 | 
			
		||||
        "asciinema-player": "^3.9.0",
 | 
			
		||||
        "asciinema-player": "^3.10.0",
 | 
			
		||||
        "axios": "^1.6.2",
 | 
			
		||||
        "clipboard": "^2.0.11",
 | 
			
		||||
        "crypto-js": "^4.2.0",
 | 
			
		||||
        "dayjs": "^1.11.13",
 | 
			
		||||
        "echarts": "^5.6.0",
 | 
			
		||||
        "element-plus": "^2.9.11",
 | 
			
		||||
        "element-plus": "^2.10.2",
 | 
			
		||||
        "js-base64": "^3.7.7",
 | 
			
		||||
        "jsencrypt": "^3.3.2",
 | 
			
		||||
        "mitt": "^3.0.1",
 | 
			
		||||
        "monaco-editor": "^0.52.2",
 | 
			
		||||
        "monaco-sql-languages": "^0.14.0",
 | 
			
		||||
        "monaco-sql-languages": "^0.15.0",
 | 
			
		||||
        "monaco-themes": "^0.4.5",
 | 
			
		||||
        "nprogress": "^0.2.0",
 | 
			
		||||
        "pinia": "^3.0.2",
 | 
			
		||||
        "pinia": "^3.0.3",
 | 
			
		||||
        "qrcode.vue": "^3.6.0",
 | 
			
		||||
        "screenfull": "^6.0.2",
 | 
			
		||||
        "sortablejs": "^1.15.6",
 | 
			
		||||
        "splitpanes": "^4.0.3",
 | 
			
		||||
        "sql-formatter": "^15.6.1",
 | 
			
		||||
        "trzsz": "^1.1.5",
 | 
			
		||||
        "uuid": "^9.0.1",
 | 
			
		||||
        "vue": "^3.5.14",
 | 
			
		||||
        "vue-i18n": "^11.1.3",
 | 
			
		||||
        "vue": "^3.5.16",
 | 
			
		||||
        "vue-i18n": "^11.1.5",
 | 
			
		||||
        "vue-router": "^4.5.1",
 | 
			
		||||
        "vuedraggable": "^4.1.0"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@tailwindcss/vite": "^4.1.6",
 | 
			
		||||
        "@tailwindcss/vite": "^4.1.9",
 | 
			
		||||
        "@types/crypto-js": "^4.2.2",
 | 
			
		||||
        "@types/node": "^18.14.0",
 | 
			
		||||
        "@types/nprogress": "^0.2.0",
 | 
			
		||||
@@ -54,18 +53,18 @@
 | 
			
		||||
        "@typescript-eslint/eslint-plugin": "^6.7.4",
 | 
			
		||||
        "@typescript-eslint/parser": "^6.7.4",
 | 
			
		||||
        "@vitejs/plugin-vue": "^5.2.4",
 | 
			
		||||
        "@vue/compiler-sfc": "^3.5.14",
 | 
			
		||||
        "@vue/compiler-sfc": "^3.5.16",
 | 
			
		||||
        "autoprefixer": "^10.4.21",
 | 
			
		||||
        "code-inspector-plugin": "^0.20.9",
 | 
			
		||||
        "dotenv": "^16.3.1",
 | 
			
		||||
        "eslint": "^9.25.1",
 | 
			
		||||
        "eslint-plugin-vue": "^10.0.0",
 | 
			
		||||
        "postcss": "^8.5.3",
 | 
			
		||||
        "eslint": "^9.27.0",
 | 
			
		||||
        "eslint-plugin-vue": "^10.2.0",
 | 
			
		||||
        "postcss": "^8.5.4",
 | 
			
		||||
        "prettier": "^3.5.3",
 | 
			
		||||
        "sass": "^1.89.0",
 | 
			
		||||
        "tailwindcss": "^4.1.7",
 | 
			
		||||
        "sass": "^1.89.2",
 | 
			
		||||
        "tailwindcss": "^4.1.9",
 | 
			
		||||
        "typescript": "^5.8.2",
 | 
			
		||||
        "vite": "^6.3.5",
 | 
			
		||||
        "vite": "npm:rolldown-vite@latest",
 | 
			
		||||
        "vite-plugin-progress": "0.0.7",
 | 
			
		||||
        "vue-eslint-parser": "^10.1.3"
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ const config = {
 | 
			
		||||
    baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
 | 
			
		||||
 | 
			
		||||
    // 系统版本
 | 
			
		||||
    version: 'v1.10.0',
 | 
			
		||||
    version: 'v1.10.1',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
 
 | 
			
		||||
@@ -42,4 +42,5 @@ export function exportFile(filename: string, content: string) {
 | 
			
		||||
    link.setAttribute('download', `${filename}`);
 | 
			
		||||
    document.body.appendChild(link);
 | 
			
		||||
    link.click();
 | 
			
		||||
    document.body.removeChild(link); // 下载完成后移除元素
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,9 @@ const props = defineProps({
 | 
			
		||||
        required: true,
 | 
			
		||||
    },
 | 
			
		||||
    value: {
 | 
			
		||||
        type: [Object, String, Number],
 | 
			
		||||
        type: [Object, String, Number, null],
 | 
			
		||||
        required: true,
 | 
			
		||||
        default: () => null,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -40,7 +41,7 @@ onMounted(() => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const convert = (value: any) => {
 | 
			
		||||
    const enumValue = EnumValue.getEnumByValue(props.enums, value) as any;
 | 
			
		||||
    const enumValue = EnumValue.getEnumByValue(props.enums, value);
 | 
			
		||||
    if (!enumValue) {
 | 
			
		||||
        state.enumLabel = '-';
 | 
			
		||||
        state.type = 'danger';
 | 
			
		||||
@@ -50,8 +51,8 @@ const convert = (value: any) => {
 | 
			
		||||
 | 
			
		||||
    state.enumLabel = enumValue?.label || '';
 | 
			
		||||
    if (enumValue.tag) {
 | 
			
		||||
        state.color = enumValue.tag.color;
 | 
			
		||||
        state.type = enumValue.tag.type;
 | 
			
		||||
        state.color = enumValue.tag.color || '';
 | 
			
		||||
        state.type = enumValue.tag.type || defaultType;
 | 
			
		||||
    } else {
 | 
			
		||||
        state.type = defaultType;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@
 | 
			
		||||
                                trigger="click"
 | 
			
		||||
                            >
 | 
			
		||||
                                <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>
 | 
			
		||||
                                <template #reference>
 | 
			
		||||
                                    <el-button icon="Operation" circle :size="props.size"></el-button>
 | 
			
		||||
 
 | 
			
		||||
@@ -71,9 +71,9 @@ export class TableColumn {
 | 
			
		||||
    formatFunc: Function;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 是否显示该列
 | 
			
		||||
     * 是否显示该列,1显示 0不显示
 | 
			
		||||
     */
 | 
			
		||||
    show: boolean = true;
 | 
			
		||||
    show: number = 1;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 是否展示美化按钮(主要用于美化json文本等)
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,6 @@ export default {
 | 
			
		||||
        docJsonError: 'Document JSON Format Error',
 | 
			
		||||
        sortParams: 'Sort',
 | 
			
		||||
        otherParams: 'Other',
 | 
			
		||||
        previewParams: 'Preview',
 | 
			
		||||
        closeIndexConfirm: 'This operation will close 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?',
 | 
			
		||||
 
 | 
			
		||||
@@ -87,6 +87,8 @@ export default {
 | 
			
		||||
        scriptResultEnumRealTime: 'Real-time',
 | 
			
		||||
        scriptTypeEnumPrivate: 'Private',
 | 
			
		||||
        scriptTypeEnumPublic: 'Public',
 | 
			
		||||
        category: 'Category',
 | 
			
		||||
        categoryTips: 'support input new category and selection',
 | 
			
		||||
 | 
			
		||||
        // security
 | 
			
		||||
        cmdConfig: 'Command Config',
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,8 @@ export default {
 | 
			
		||||
            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`,
 | 
			
		||||
            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',
 | 
			
		||||
            componentPathTips: 'Access path components, such as: ` system/resource/ResourceList `, default in ` views ` directory',
 | 
			
		||||
            routerNameTips:
 | 
			
		||||
                '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`,
 | 
			
		||||
            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',
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,6 @@ export default {
 | 
			
		||||
        docJsonError: '文档JSON格式错误',
 | 
			
		||||
        sortParams: '排序',
 | 
			
		||||
        otherParams: '其他',
 | 
			
		||||
        previewParams: '预览',
 | 
			
		||||
        closeIndexConfirm: '将会关闭索引:[{name}]。 确认继续吗?',
 | 
			
		||||
        openIndexConfirm: '将会打开索引:[{name}]。 确认继续吗?',
 | 
			
		||||
        clearCacheConfirm: '将会清除索引:[{name}]缓存。 确认继续吗?',
 | 
			
		||||
 
 | 
			
		||||
@@ -88,6 +88,8 @@ export default {
 | 
			
		||||
        scriptResultEnumRealTime: '实时交互',
 | 
			
		||||
        scriptTypeEnumPrivate: '私有',
 | 
			
		||||
        scriptTypeEnumPublic: '公共',
 | 
			
		||||
        category: '分类',
 | 
			
		||||
        categoryTips: '支持输入新分类并选择',
 | 
			
		||||
 | 
			
		||||
        // security
 | 
			
		||||
        cmdConfig: '命令配置',
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,7 @@ export default {
 | 
			
		||||
            success: '成功',
 | 
			
		||||
            menuCodeTips: `菜单类型则为访问路径(若菜单路径不以'/'开头则访问地址会自动拼接父菜单路径)、否则为资源唯一编码`,
 | 
			
		||||
            menuCodePlaceholder: `菜单不以'/'开头则自动拼接父菜单路径`,
 | 
			
		||||
            routerNameTips: '与vue的组件名一致才可使组件缓存生效,如ResourceList',
 | 
			
		||||
            componentPathTips: '访问的组件路径,如:`system/resource/ResourceList`,默认在`views`目录下',
 | 
			
		||||
            routerNameTips: '前端模块下route.ts中对应的key,与vue的组件名一致才可使组件缓存生效,如ResourceList',
 | 
			
		||||
            isCacheTips: '选择是则会被`keep-alive`缓存(重新进入页面不会刷新页面及重新请求数据),需要路由名与vue的组件名一致',
 | 
			
		||||
            isHideTips: '选择隐藏则路由将不会出现在菜单栏中,但仍然可以访问。禁用则不可访问与操作',
 | 
			
		||||
            externalLinkTips: '内嵌: 以iframe展示、外链: 新标签打开',
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
    <div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb">
 | 
			
		||||
        <SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'" @click="onThemeConfigChange" />
 | 
			
		||||
        <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">
 | 
			
		||||
                    <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" />
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,6 @@ import 'element-plus/dist/index.css';
 | 
			
		||||
import 'element-plus/theme-chalk/dark/css-vars.css';
 | 
			
		||||
import { i18n } from '@/i18n/index';
 | 
			
		||||
 | 
			
		||||
import 'splitpanes/dist/splitpanes.css';
 | 
			
		||||
 | 
			
		||||
import '@/theme/index.scss';
 | 
			
		||||
import '@/theme/tailwind.css';
 | 
			
		||||
import '@/assets/font/font.css';
 | 
			
		||||
 
 | 
			
		||||
@@ -9,15 +9,21 @@ import { RouteRecordRaw } from 'vue-router';
 | 
			
		||||
import { LAYOUT_ROUTE_NAME } from './staticRouter';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 获取目录下的 .vue、.tsx 全部文件
 | 
			
		||||
 * 获取目录下的 route.ts 全部文件
 | 
			
		||||
 * @method import.meta.glob
 | 
			
		||||
 * @link 参考:https://cn.vitejs.dev/guide/features.html#json
 | 
			
		||||
 */
 | 
			
		||||
const viewsModules: Record<string, Function> = import.meta.glob(['../views/**/*.{vue,tsx}']);
 | 
			
		||||
const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...viewsModules });
 | 
			
		||||
const routeModules: Record<string, any> = import.meta.glob(['../views/**/route.{ts,js}'], { eager: true });
 | 
			
		||||
 | 
			
		||||
// 后端控制路由:执行路由数据初始化
 | 
			
		||||
export async function initBackendRoutes() {
 | 
			
		||||
    let allModuleRoutes = {};
 | 
			
		||||
    for (const path in routeModules) {
 | 
			
		||||
        // 获取默认导出的路由
 | 
			
		||||
        const routes = routeModules[path]?.default;
 | 
			
		||||
        allModuleRoutes = { ...allModuleRoutes, ...routes };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const token = getToken(); // 获取浏览器缓存 token 值
 | 
			
		||||
    if (!token) {
 | 
			
		||||
        // 无 token 停止执行下一步
 | 
			
		||||
@@ -29,7 +35,7 @@ export async function initBackendRoutes() {
 | 
			
		||||
 | 
			
		||||
    const cacheList: Array<string> = [];
 | 
			
		||||
    // 处理路由(component)
 | 
			
		||||
    const routes = backEndRouterConverter(menuRoute, (router: any) => {
 | 
			
		||||
    const routes = backEndRouterConverter(allModuleRoutes, menuRoute, (router: any) => {
 | 
			
		||||
        // 可能为false时不存在isKeepAlive属性
 | 
			
		||||
        if (!router.meta.isKeepAlive) {
 | 
			
		||||
            router.meta.isKeepAlive = false;
 | 
			
		||||
@@ -77,9 +83,8 @@ type RouterConvCallbackFunc = (router: any) => void;
 | 
			
		||||
 * @param name ==> title,路由标题 相当于route.meta.title
 | 
			
		||||
 *
 | 
			
		||||
 * @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.component ==> 文件路径
 | 
			
		||||
 * @param meta.icon ==> 菜单和面包屑对应的图标
 | 
			
		||||
 * @param meta.isHide ==> 是否在菜单中隐藏 (通常列表详情页需要隐藏)
 | 
			
		||||
 * @param meta.isFull ==> 菜单是否全屏 (示例:数据大屏页面)
 | 
			
		||||
@@ -88,7 +93,7 @@ type RouterConvCallbackFunc = (router: any) => void;
 | 
			
		||||
 * @param meta.linkType ==> 外链类型, 内嵌: 以iframe展示、外链: 新标签打开
 | 
			
		||||
 * @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) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
@@ -101,12 +106,6 @@ export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCall
 | 
			
		||||
        // 将json字符串的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;
 | 
			
		||||
        // 如果不是以 / 开头,则路径需要拼接父路径
 | 
			
		||||
        if (!path.startsWith('/')) {
 | 
			
		||||
@@ -121,6 +120,8 @@ export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCall
 | 
			
		||||
 | 
			
		||||
        // route.name == resource.meta.routeName
 | 
			
		||||
        item.name = item.meta.routeName;
 | 
			
		||||
        // routerName == 模块下route.ts 字段key == 组件名
 | 
			
		||||
        item.component = allModuleRoutes[item.meta.routeName];
 | 
			
		||||
        delete item.meta['routeName'];
 | 
			
		||||
 | 
			
		||||
        // route.redirect == resource.meta.redirect
 | 
			
		||||
@@ -130,35 +131,9 @@ export function backEndRouterConverter(routes: any, callbackFunc: RouterConvCall
 | 
			
		||||
        }
 | 
			
		||||
        // 存在回调,则执行回调
 | 
			
		||||
        callbackFunc && callbackFunc(item);
 | 
			
		||||
        item.children && backEndRouterConverter(item.children, callbackFunc, item.path);
 | 
			
		||||
        item.children && backEndRouterConverter(allModuleRoutes, item.children, callbackFunc, item.path);
 | 
			
		||||
        routeItems.push(item);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,4 +5,3 @@
 | 
			
		||||
@use './waves.scss';
 | 
			
		||||
@use './dark.scss';
 | 
			
		||||
@use './iconSelector.scss';
 | 
			
		||||
@use './splitpanes.scss';
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								frontend/src/types/shim.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								frontend/src/types/shim.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -2,5 +2,4 @@
 | 
			
		||||
declare module 'jsoneditor';
 | 
			
		||||
declare module 'asciinema-player';
 | 
			
		||||
declare module 'vue-grid-layout';
 | 
			
		||||
declare module 'splitpanes';
 | 
			
		||||
declare module 'uuid';
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@
 | 
			
		||||
                        <enum-tag :enums="FlowBizType" :value="procinst.bizType"></enum-tag>
 | 
			
		||||
                    </el-descriptions-item>
 | 
			
		||||
                    <el-descriptions-item :span="1" :label="$t('flow.initiator')">
 | 
			
		||||
                        <AccountInfo :username="procinst.creator" />
 | 
			
		||||
                        <AccountInfo :username="procinst.creator || ''" />
 | 
			
		||||
                    </el-descriptions-item>
 | 
			
		||||
 | 
			
		||||
                    <el-descriptions-item :span="1" :label="$t('flow.procinstStatus')">
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
 | 
			
		||||
                <el-table-column :label="$t('flow.approver')" min-width="100">
 | 
			
		||||
                    <template #default="scope">
 | 
			
		||||
                        <AccountInfo :username="scope.row.handler" />
 | 
			
		||||
                        <AccountInfo :username="scope.row.handler || ''" />
 | 
			
		||||
                    </template>
 | 
			
		||||
                </el-table-column>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								frontend/src/views/flow/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/views/flow/route.ts
									
									
									
									
									
										Normal 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'),
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										3
									
								
								frontend/src/views/home/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/views/home/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
export default {
 | 
			
		||||
    Home: () => import('@/views/home/Home.vue'),
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										4
									
								
								frontend/src/views/msg/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/src/views/msg/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
export default {
 | 
			
		||||
    ChannelList: () => import('@/views/msg/channel/ChannelList.vue'),
 | 
			
		||||
    TmplList: () => import('@/views/msg/tmpl/TmplList.vue'),
 | 
			
		||||
};
 | 
			
		||||
@@ -19,6 +19,7 @@ const props = defineProps({
 | 
			
		||||
    authCerts: {
 | 
			
		||||
        type: [Array<any>],
 | 
			
		||||
        required: true,
 | 
			
		||||
        default: () => [],
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,16 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <Splitpanes class="default-theme" @resize="handleResize">
 | 
			
		||||
        <Pane :size="leftPaneSize" max-size="30">
 | 
			
		||||
    <el-splitter @resize="handleResize">
 | 
			
		||||
        <el-splitter-panel :size="leftPaneSize + '%'" max="30%">
 | 
			
		||||
            <slot name="left"></slot>
 | 
			
		||||
        </Pane>
 | 
			
		||||
        </el-splitter-panel>
 | 
			
		||||
 | 
			
		||||
        <Pane>
 | 
			
		||||
        <el-splitter-panel>
 | 
			
		||||
            <slot name="right"></slot>
 | 
			
		||||
        </Pane>
 | 
			
		||||
    </Splitpanes>
 | 
			
		||||
        </el-splitter-panel>
 | 
			
		||||
    </el-splitter>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { Pane, Splitpanes } from 'splitpanes';
 | 
			
		||||
import { useWindowSize } from '@vueuse/core';
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ const props = defineProps({
 | 
			
		||||
    tags: {
 | 
			
		||||
        type: [Array<any>],
 | 
			
		||||
        required: true,
 | 
			
		||||
        default: () => [],
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -19,10 +19,9 @@
 | 
			
		||||
                :default-expanded-keys="props.defaultExpandedKeys"
 | 
			
		||||
            >
 | 
			
		||||
                <template #default="{ node, data }">
 | 
			
		||||
                    <span
 | 
			
		||||
                    <div
 | 
			
		||||
                        :id="node.key"
 | 
			
		||||
                        @dblclick="treeNodeDblclick(data, node)"
 | 
			
		||||
                        class="node-container flex items-center cursor-pointer select-none"
 | 
			
		||||
                        class="w-full node-container flex items-center cursor-pointer select-none"
 | 
			
		||||
                        :class="data.type.nodeDblclickFunc ? 'select-none' : ''"
 | 
			
		||||
                    >
 | 
			
		||||
                        <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">
 | 
			
		||||
                            <slot :node="node" :data="data" name="suffix"></slot>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </template>
 | 
			
		||||
            </el-tree>
 | 
			
		||||
 | 
			
		||||
@@ -153,7 +152,16 @@ const loadNode = async (node: any, resolve: (data: any) => void, reject: () => v
 | 
			
		||||
    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) {
 | 
			
		||||
        emit('nodeClick', data);
 | 
			
		||||
        await data.type.nodeClickFunc(data);
 | 
			
		||||
@@ -170,7 +178,6 @@ const treeNodeDblclick = (data: any, node: any) => {
 | 
			
		||||
        node.expand();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // emit('nodeDblick', data);
 | 
			
		||||
    if (!data.disabled && data.type.nodeDblclickFunc) {
 | 
			
		||||
        data.type.nodeDblclickFunc(data);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -89,7 +89,7 @@ import { Rules } from '@/common/rule';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    instance: {
 | 
			
		||||
        type: [Boolean, Object],
 | 
			
		||||
        type: [Boolean, Object, null],
 | 
			
		||||
    },
 | 
			
		||||
    db: {
 | 
			
		||||
        type: [Boolean, Object],
 | 
			
		||||
 
 | 
			
		||||
@@ -153,7 +153,7 @@
 | 
			
		||||
            :close-on-click-modal="false"
 | 
			
		||||
            v-model="sqlExecLogDialog.visible"
 | 
			
		||||
            :destroy-on-close="true"
 | 
			
		||||
            body-class="h-250"
 | 
			
		||||
            body-class="h-[65vh]"
 | 
			
		||||
        >
 | 
			
		||||
            <db-sql-exec-log :db-id="sqlExecLogDialog.dbId" :dbs="sqlExecLogDialog.dbs" />
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
@@ -365,7 +365,7 @@ const editDb = (data: any) => {
 | 
			
		||||
        state.dbEditDialog.data = { ...data };
 | 
			
		||||
    } else {
 | 
			
		||||
        state.dbEditDialog.data = {
 | 
			
		||||
            instanceId: props.instance.id,
 | 
			
		||||
            instanceId: props.instance?.id,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    state.dbEditDialog.title = data ? useI18nEditTitle('db.db') : useI18nCreateTitle('db.db');
 | 
			
		||||
@@ -373,7 +373,7 @@ const editDb = (data: any) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const confirmEditDb = async (db: any) => {
 | 
			
		||||
    db.instanceId = props.instance.id;
 | 
			
		||||
    db.instanceId = props.instance?.id;
 | 
			
		||||
    await dbApi.saveDb.request(db);
 | 
			
		||||
    useI18nSaveSuccessMsg();
 | 
			
		||||
    search();
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,7 @@ import { DbSqlExecTypeEnum, DbSqlExecStatusEnum } from './enums';
 | 
			
		||||
import PageTable from '@/components/pagetable/PageTable.vue';
 | 
			
		||||
import { TableColumn } from '@/components/pagetable';
 | 
			
		||||
import { SearchItem } from '@/components/SearchForm';
 | 
			
		||||
import { formatDate } from '@/common/utils/format';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    dbId: {
 | 
			
		||||
@@ -62,6 +63,21 @@ const searchItems = [
 | 
			
		||||
    SearchItem.slot('db', 'db.db', 'dbSelect'),
 | 
			
		||||
    SearchItem.input('table', 'db.table'),
 | 
			
		||||
    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([
 | 
			
		||||
@@ -88,6 +104,9 @@ const state = reactive({
 | 
			
		||||
        table: '',
 | 
			
		||||
        status: [DbSqlExecStatusEnum.Success.value, DbSqlExecStatusEnum.Fail.value].join(','),
 | 
			
		||||
        type: null,
 | 
			
		||||
        keyword: '',
 | 
			
		||||
        startTime: '',
 | 
			
		||||
        endTime: '',
 | 
			
		||||
        pageNum: 1,
 | 
			
		||||
        pageSize: 10,
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +1,25 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <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
 | 
			
		||||
                ref="pageTableRef"
 | 
			
		||||
                :data="state.tableData"
 | 
			
		||||
                v-model:query-form="state.query"
 | 
			
		||||
                :page-api="dbApi.dbTransferFileList"
 | 
			
		||||
                :lazy="true"
 | 
			
		||||
                :show-selection="true"
 | 
			
		||||
                v-model:selection-data="state.selectionData"
 | 
			
		||||
                :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>
 | 
			
		||||
                    <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') }}
 | 
			
		||||
                    </el-button>
 | 
			
		||||
                </template>
 | 
			
		||||
@@ -41,17 +38,20 @@
 | 
			
		||||
                <template #action="{ data }">
 | 
			
		||||
                    <el-button
 | 
			
		||||
                        v-if="actionBtns[perms.run] && data.status === DbTransferFileStatusEnum.Success.value"
 | 
			
		||||
                        @click="openRun(data)"
 | 
			
		||||
                        @click="onOpenRun(data)"
 | 
			
		||||
                        type="primary"
 | 
			
		||||
                        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>
 | 
			
		||||
            </page-table>
 | 
			
		||||
            <TerminalLog v-model:log-id="state.logsDialog.logId" v-model:visible="state.logsDialog.visible" :title="state.logsDialog.title" />
 | 
			
		||||
        </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-form :model="state.runDialog.runForm" ref="runFormRef" label-width="auto" :rules="state.runDialog.formRules">
 | 
			
		||||
                <el-form-item :label="$t('db.dbFileType')" prop="dbType">
 | 
			
		||||
@@ -70,17 +70,15 @@
 | 
			
		||||
            </el-form>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <el-button @click="state.runDialog.cancel()">{{ $t('common.cancel') }}</el-button>
 | 
			
		||||
                    <el-button type="primary" :loading="state.runDialog.loading" @click="state.runDialog.btnOk">{{ $t('common.confirm') }}</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <el-button @click="state.runDialog.onCancel()">{{ $t('common.cancel') }}</el-button>
 | 
			
		||||
                <el-button type="primary" :loading="state.runDialog.loading" @click="state.runDialog.onConfirm">{{ $t('common.confirm') }}</el-button>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<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 { getDbDialect } from '@/views/ops/db/dialect';
 | 
			
		||||
import PageTable from '@/components/pagetable/PageTable.vue';
 | 
			
		||||
@@ -109,6 +107,8 @@ const props = defineProps({
 | 
			
		||||
 | 
			
		||||
const dialogVisible = defineModel<boolean>('visible', { default: false });
 | 
			
		||||
 | 
			
		||||
const pageTableRef: Ref<any> = useTemplateRef('pageTableRef');
 | 
			
		||||
 | 
			
		||||
const columns = ref([
 | 
			
		||||
    TableColumn.new('fileKey', 'db.file').setMinWidth(280).isSlot(),
 | 
			
		||||
    TableColumn.new('createTime', 'db.execTime').setMinWidth(180).isTime(),
 | 
			
		||||
@@ -168,11 +168,11 @@ const state = reactive({
 | 
			
		||||
            targetDbType: '',
 | 
			
		||||
        },
 | 
			
		||||
        loading: false,
 | 
			
		||||
        cancel: function () {
 | 
			
		||||
        onCancel: function () {
 | 
			
		||||
            state.runDialog.visible = false;
 | 
			
		||||
            state.runDialog.runForm = {} as any;
 | 
			
		||||
        },
 | 
			
		||||
        btnOk: async function () {
 | 
			
		||||
        onConfirm: async function () {
 | 
			
		||||
            await useI18nFormValidate(runFormRef);
 | 
			
		||||
            if (state.runDialog.runForm.targetDbType !== 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();
 | 
			
		||||
            await dbApi.dbTransferFileRun.request(state.runDialog.runForm);
 | 
			
		||||
            useI18nOperateSuccessMsg();
 | 
			
		||||
            state.runDialog.cancel();
 | 
			
		||||
            state.runDialog.onCancel();
 | 
			
		||||
            await search();
 | 
			
		||||
        },
 | 
			
		||||
        onSelectRunTargetDb: function (param: any) {
 | 
			
		||||
@@ -195,14 +195,13 @@ const state = reactive({
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const search = async () => {
 | 
			
		||||
    const { total, list } = await dbApi.dbTransferFileList.request(state.query);
 | 
			
		||||
    state.tableData = list;
 | 
			
		||||
    pageTableRef.value.total = total;
 | 
			
		||||
    pageTableRef.value?.search();
 | 
			
		||||
    // const { total, list } = await dbApi.dbTransferFileList.request(state.query);
 | 
			
		||||
    // state.tableData = list;
 | 
			
		||||
    // pageTableRef.value.total = total;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const pageTableRef: Ref<any> = ref(null);
 | 
			
		||||
 | 
			
		||||
const del = async function () {
 | 
			
		||||
const onDel = async function () {
 | 
			
		||||
    try {
 | 
			
		||||
        await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).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.visible = true;
 | 
			
		||||
    state.logsDialog.title = t('db.log');
 | 
			
		||||
@@ -221,7 +220,7 @@ const openLog = function (data: any) {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 运行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.visible = true;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -155,7 +155,7 @@ const state = reactive({
 | 
			
		||||
    },
 | 
			
		||||
    dbEditDialog: {
 | 
			
		||||
        visible: false,
 | 
			
		||||
        instance: null as any,
 | 
			
		||||
        instance: {},
 | 
			
		||||
        title: '',
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
                    <el-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
 | 
			
		||||
                    <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-divider direction="vertical" border-style="dashed" />
 | 
			
		||||
 | 
			
		||||
@@ -39,19 +39,13 @@
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <Splitpanes
 | 
			
		||||
            @pane-maximize="resizeTableHeight({ panes: [{ size: 0 }] })"
 | 
			
		||||
            @resize="resizeTableHeight"
 | 
			
		||||
            horizontal
 | 
			
		||||
            class="default-theme"
 | 
			
		||||
            style="height: calc(100vh - 233px)"
 | 
			
		||||
        >
 | 
			
		||||
            <Pane :size="state.editorSize" max-size="80">
 | 
			
		||||
        <el-splitter style="height: calc(100vh - 200px)" layout="vertical" @resize-end="onResizeTableHeight">
 | 
			
		||||
            <el-splitter-panel :size="state.editorSize" max="80%">
 | 
			
		||||
                <MonacoEditor ref="monacoEditorRef" class="mt-1" v-model="state.sql" language="sql" height="100%" :id="'MonacoTextarea-' + getKey()" />
 | 
			
		||||
            </Pane>
 | 
			
		||||
            </el-splitter-panel>
 | 
			
		||||
 | 
			
		||||
            <Pane :size="100 - state.editorSize">
 | 
			
		||||
                <div class="mt-1 sql-exec-res !h-full">
 | 
			
		||||
            <el-splitter-panel>
 | 
			
		||||
                <div class="sql-exec-res !h-full">
 | 
			
		||||
                    <el-tabs
 | 
			
		||||
                        class="!h-full !w-full"
 | 
			
		||||
                        v-if="state.execResTabs.length > 0"
 | 
			
		||||
@@ -128,8 +122,8 @@
 | 
			
		||||
                        </el-tab-pane>
 | 
			
		||||
                    </el-tabs>
 | 
			
		||||
                </div>
 | 
			
		||||
            </Pane>
 | 
			
		||||
        </Splitpanes>
 | 
			
		||||
            </el-splitter-panel>
 | 
			
		||||
        </el-splitter>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -151,9 +145,9 @@ import { dbApi } from '../../api';
 | 
			
		||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
 | 
			
		||||
import { joinClientParams } from '@/common/request';
 | 
			
		||||
import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import { Pane, Splitpanes } from 'splitpanes';
 | 
			
		||||
import { useI18n } from 'vue-i18n';
 | 
			
		||||
import { useI18nSaveSuccessMsg } from '@/hooks/useI18n';
 | 
			
		||||
import { useDebounceFn, useEventListener } from '@vueuse/core';
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits(['saveSqlSuccess']);
 | 
			
		||||
 | 
			
		||||
@@ -241,10 +235,11 @@ onMounted(async () => {
 | 
			
		||||
    console.log('in query mounted');
 | 
			
		||||
 | 
			
		||||
    // 第一个pane为sql editor
 | 
			
		||||
    resizeTableHeight({ panes: [{ size: state.editorSize }] });
 | 
			
		||||
    window.onresize = () => {
 | 
			
		||||
        resizeTableHeight({ panes: [{ size: state.editorSize }] });
 | 
			
		||||
    };
 | 
			
		||||
    onResizeTableHeight(0, [-1]);
 | 
			
		||||
    useEventListener(
 | 
			
		||||
        'resize',
 | 
			
		||||
        useDebounceFn(() => onResizeTableHeight(0, [-1]), 200)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // 默认新建一个结果集tab
 | 
			
		||||
    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;
 | 
			
		||||
    state.editorSize = e.panes[0].size;
 | 
			
		||||
    const plitpaneHeight = vh - 233;
 | 
			
		||||
    const editorHeight = plitpaneHeight * (state.editorSize / 100);
 | 
			
		||||
    state.tableDataHeight = plitpaneHeight - editorHeight - 40 + 'px';
 | 
			
		||||
    const plitpaneHeight = vh - 200;
 | 
			
		||||
 | 
			
		||||
    let editorHeight = sizes[0];
 | 
			
		||||
    if (editorHeight < 0 || editorHeight > plitpaneHeight - 43) {
 | 
			
		||||
        // 默认占50%
 | 
			
		||||
        editorHeight = plitpaneHeight / 2;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let tableDataHeight = plitpaneHeight - editorHeight - 43;
 | 
			
		||||
 | 
			
		||||
    state.editorSize = editorHeight;
 | 
			
		||||
    state.tableDataHeight = tableDataHeight + 'px';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getKey = () => {
 | 
			
		||||
@@ -535,7 +542,7 @@ const saveSql = async () => {
 | 
			
		||||
/**
 | 
			
		||||
 * 格式化sql
 | 
			
		||||
 */
 | 
			
		||||
const formatSql = () => {
 | 
			
		||||
const onFormatSql = () => {
 | 
			
		||||
    let selection = monacoEditor.getSelection();
 | 
			
		||||
    if (!selection) {
 | 
			
		||||
        return;
 | 
			
		||||
@@ -715,7 +722,7 @@ const initMonacoEditor = () => {
 | 
			
		||||
        // @param editor The editor instance is passed in as a convenience
 | 
			
		||||
        run: async function () {
 | 
			
		||||
            try {
 | 
			
		||||
                await formatSql();
 | 
			
		||||
                await onFormatSql();
 | 
			
		||||
            } catch (e: any) {
 | 
			
		||||
                e.message && ElMessage.error(e.message);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <el-dialog v-model="visible" :title="title" :destroy-on-close="true" width="600px">
 | 
			
		||||
        <el-form ref="dataForm" :model="modelValue" :show-message="false" label-width="auto" size="small">
 | 
			
		||||
    <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" scroll-to-error :show-message="false" label-width="auto" size="small">
 | 
			
		||||
            <el-form-item
 | 
			
		||||
                v-for="column in columns"
 | 
			
		||||
                :key="column.columnName"
 | 
			
		||||
                class="mb-1 w-full"
 | 
			
		||||
                :prop="column.columnName"
 | 
			
		||||
                :required="props.tableName != '' && !column.nullable && !column.isPrimaryKey && !column.autoIncrement"
 | 
			
		||||
            >
 | 
			
		||||
@@ -24,10 +23,8 @@
 | 
			
		||||
            </el-form-item>
 | 
			
		||||
        </el-form>
 | 
			
		||||
        <template #footer v-if="props.tableName">
 | 
			
		||||
            <span class="dialog-footer">
 | 
			
		||||
                <el-button @click="closeDialog">{{ $t('common.cancel') }}</el-button>
 | 
			
		||||
                <el-button type="primary" @click="confirm">{{ $t('common.confirm') }}</el-button>
 | 
			
		||||
            </span>
 | 
			
		||||
            <el-button @click="onCloseDialog">{{ $t('common.cancel') }}</el-button>
 | 
			
		||||
            <el-button type="primary" @click="onConfirm">{{ $t('common.confirm') }}</el-button>
 | 
			
		||||
        </template>
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -79,12 +76,12 @@ const setOldValue = () => {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const closeDialog = () => {
 | 
			
		||||
const onCloseDialog = () => {
 | 
			
		||||
    visible.value = false;
 | 
			
		||||
    modelValue.value = {};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const confirm = async () => {
 | 
			
		||||
const onConfirm = async () => {
 | 
			
		||||
    await useI18nFormValidate(dataForm);
 | 
			
		||||
 | 
			
		||||
    const dbInst = props.dbInst;
 | 
			
		||||
@@ -107,7 +104,7 @@ const confirm = async () => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dbInst.promptExeSql(db, sql, null, () => {
 | 
			
		||||
        closeDialog();
 | 
			
		||||
        onCloseDialog();
 | 
			
		||||
        emit('submitSuccess');
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								frontend/src/views/ops/db/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/views/ops/db/route.ts
									
									
									
									
									
										Normal 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'),
 | 
			
		||||
};
 | 
			
		||||
@@ -116,7 +116,6 @@
 | 
			
		||||
 | 
			
		||||
        <template #footer>
 | 
			
		||||
            <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="onSaveParam" type="primary" icon="check">{{ t('common.save') }}</el-button>-->
 | 
			
		||||
 | 
			
		||||
@@ -472,7 +471,7 @@ const onSaveParam = () => {
 | 
			
		||||
    // 保存查询条件
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onPreviewParam = () => {
 | 
			
		||||
const onSearch = () => {
 | 
			
		||||
    parseParams();
 | 
			
		||||
    MonacoEditorBox({
 | 
			
		||||
        content: JSON.stringify(state.search, null, 2),
 | 
			
		||||
@@ -480,7 +479,10 @@ const onPreviewParam = () => {
 | 
			
		||||
        language: 'json',
 | 
			
		||||
        width: state.searchBoxWidth,
 | 
			
		||||
        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'];
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onSearch = () => {
 | 
			
		||||
    parseParams();
 | 
			
		||||
    emit('search', state.search);
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								frontend/src/views/ops/es/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/src/views/ops/es/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
export default {
 | 
			
		||||
    EsInstanceList: () => import('@/views/ops/es/EsInstanceList.vue'),
 | 
			
		||||
    EsOperation: () => import('@/views/ops/es/EsOperation.vue'),
 | 
			
		||||
};
 | 
			
		||||
@@ -61,10 +61,8 @@
 | 
			
		||||
            </el-form>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <div>
 | 
			
		||||
                <el-button @click="onCancel()">{{ $t('common.cancel') }}</el-button>
 | 
			
		||||
                <el-button type="primary" :loading="saveBtnLoading" @click="onConfirm">{{ $t('common.confirm') }}</el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-drawer>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="h-full">
 | 
			
		||||
        <ResourceOpPanel @resized="onResizeTagTree">
 | 
			
		||||
        <ResourceOpPanel @resize="onResizeTagTree">
 | 
			
		||||
            <template #left>
 | 
			
		||||
                <tag-tree
 | 
			
		||||
                    ref="tagTreeRef"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-dialog
 | 
			
		||||
        <el-drawer
 | 
			
		||||
            :title="title"
 | 
			
		||||
            v-model="dialogVisible"
 | 
			
		||||
            :close-on-click-modal="false"
 | 
			
		||||
            :before-close="cancel"
 | 
			
		||||
            :before-close="onCancel"
 | 
			
		||||
            :show-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-input v-model="form.name"></el-input>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
@@ -22,6 +27,12 @@
 | 
			
		||||
                    <EnumSelect :enums="ScriptResultEnum" v-model="form.type" default-first-option />
 | 
			
		||||
                </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">
 | 
			
		||||
                    <template #label>
 | 
			
		||||
                        <el-tooltip placement="top">
 | 
			
		||||
@@ -43,14 +54,12 @@
 | 
			
		||||
            </el-form>
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <div class="dialog-footer">
 | 
			
		||||
                    <el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
 | 
			
		||||
                    <el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="btnOk">
 | 
			
		||||
                <el-button @click="onCancel()">{{ $t('common.cancel') }}</el-button>
 | 
			
		||||
                <el-button v-auth="'machine:script:save'" type="primary" :loading="btnLoading" @click="onConfirm">
 | 
			
		||||
                    {{ $t('common.save') }}
 | 
			
		||||
                </el-button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
        </el-drawer>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -64,6 +73,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
 | 
			
		||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
 | 
			
		||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
 | 
			
		||||
import { Rules } from '@/common/rule';
 | 
			
		||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
    data: {
 | 
			
		||||
@@ -93,6 +103,7 @@ const rules = {
 | 
			
		||||
 | 
			
		||||
const { isCommon, machineId } = toRefs(props);
 | 
			
		||||
const scriptForm: any = ref(null);
 | 
			
		||||
const categorys = ref([]);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    params: [] as any,
 | 
			
		||||
@@ -104,6 +115,7 @@ const state = reactive({
 | 
			
		||||
        script: '',
 | 
			
		||||
        params: '',
 | 
			
		||||
        type: null,
 | 
			
		||||
        category: '',
 | 
			
		||||
    },
 | 
			
		||||
    btnLoading: false,
 | 
			
		||||
});
 | 
			
		||||
@@ -114,6 +126,9 @@ watch(props, (newValue: any) => {
 | 
			
		||||
    if (!dialogVisible.value) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    machineApi.scriptCategorys.request().then((res: any) => {
 | 
			
		||||
        categorys.value = res;
 | 
			
		||||
    });
 | 
			
		||||
    if (newValue.data) {
 | 
			
		||||
        state.form = { ...newValue.data };
 | 
			
		||||
        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);
 | 
			
		||||
    await useI18nFormValidate(scriptForm);
 | 
			
		||||
    if (state.params) {
 | 
			
		||||
@@ -134,11 +149,11 @@ const btnOk = async () => {
 | 
			
		||||
    machineApi.saveScript.request(state.form).then(() => {
 | 
			
		||||
        useI18nSaveSuccessMsg();
 | 
			
		||||
        emit('submitSuccess');
 | 
			
		||||
        cancel();
 | 
			
		||||
        onCancel();
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
const onCancel = () => {
 | 
			
		||||
    dialogVisible.value = false;
 | 
			
		||||
    emit('cancel');
 | 
			
		||||
    state.params = [];
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@
 | 
			
		||||
            :close-on-click-modal="false"
 | 
			
		||||
            :modal="false"
 | 
			
		||||
            @close="closeTermnial"
 | 
			
		||||
            body-class="h-[560px]"
 | 
			
		||||
            body-class="h-[65vh]"
 | 
			
		||||
            draggable
 | 
			
		||||
            append-to-body
 | 
			
		||||
        >
 | 
			
		||||
@@ -99,6 +99,7 @@ import { DynamicFormDialog } from '@/components/dynamic-form';
 | 
			
		||||
import { SearchItem } from '@/components/SearchForm';
 | 
			
		||||
import { useI18n } from 'vue-i18n';
 | 
			
		||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
 | 
			
		||||
import { OptionsApi } from '@/components/SearchForm/index';
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
 | 
			
		||||
@@ -117,11 +118,24 @@ const pageTableRef: Ref<any> = ref(null);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    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: [
 | 
			
		||||
        TableColumn.new('name', 'common.name'),
 | 
			
		||||
        TableColumn.new('description', 'common.remark'),
 | 
			
		||||
        TableColumn.new('type', 'common.type').typeTag(ScriptResultEnum),
 | 
			
		||||
        TableColumn.new('category', 'machine.category'),
 | 
			
		||||
        TableColumn.new('action', 'common.operation').isSlot().setMinWidth(140).alignCenter(),
 | 
			
		||||
    ],
 | 
			
		||||
    query: {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ export const machineApi = {
 | 
			
		||||
    // 删除机器
 | 
			
		||||
    del: Api.newDelete('/machines/{id}'),
 | 
			
		||||
    scripts: Api.newGet('/machines/{machineId}/scripts'),
 | 
			
		||||
    scriptCategorys: Api.newGet('/machines/scripts/categorys'),
 | 
			
		||||
    runScript: Api.newGet('/machines/scripts/{scriptId}/{ac}/run'),
 | 
			
		||||
    saveScript: Api.newPost('/machines/{machineId}/scripts'),
 | 
			
		||||
    deleteScript: Api.newDelete('/machines/{machineId}/scripts/{scriptId}'),
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
            :show-close="true"
 | 
			
		||||
            :destroy-on-close="true"
 | 
			
		||||
            width="65%"
 | 
			
		||||
            body-class="h-200"
 | 
			
		||||
            body-class="h-[65vh]"
 | 
			
		||||
        >
 | 
			
		||||
            <page-table
 | 
			
		||||
                ref="pageTableRef"
 | 
			
		||||
 
 | 
			
		||||
@@ -15,14 +15,14 @@
 | 
			
		||||
 | 
			
		||||
            <template #footer>
 | 
			
		||||
                <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>
 | 
			
		||||
        </el-dialog>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<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 MonacoEditor from '@/components/monaco/MonacoEditor.vue';
 | 
			
		||||
import { useI18nSaveSuccessMsg } from '@/hooks/useI18n';
 | 
			
		||||
@@ -42,9 +42,10 @@ const emit = defineEmits(['cancel', 'update:machineId']);
 | 
			
		||||
 | 
			
		||||
const updateFileContent = machineApi.updateFileContent;
 | 
			
		||||
 | 
			
		||||
const saveing: Ref<any> = ref(false);
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
    loadingContent: false,
 | 
			
		||||
    content: '',
 | 
			
		||||
    fileType: '',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -83,8 +84,10 @@ const handleClose = () => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const updateContent = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        saveing.value = true;
 | 
			
		||||
        await updateFileContent.request({
 | 
			
		||||
        content: state.content,
 | 
			
		||||
            content: fileContent.value,
 | 
			
		||||
            id: props.fileId,
 | 
			
		||||
            path: props.path,
 | 
			
		||||
            machineId: props.machineId,
 | 
			
		||||
@@ -93,7 +96,10 @@ const updateContent = async () => {
 | 
			
		||||
        });
 | 
			
		||||
        useI18nSaveSuccessMsg();
 | 
			
		||||
        handleClose();
 | 
			
		||||
    state.content = '';
 | 
			
		||||
        fileContent.value = '';
 | 
			
		||||
    } finally {
 | 
			
		||||
        saveing.value = false;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getFileType = (path: string) => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								frontend/src/views/ops/machine/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/views/ops/machine/route.ts
									
									
									
									
									
										Normal 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'),
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										4
									
								
								frontend/src/views/ops/mongo/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/src/views/ops/mongo/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
export default {
 | 
			
		||||
    MongoList: () => import('@/views/ops/mongo/MongoList.vue'),
 | 
			
		||||
    MongoDataOp: () => import('@/views/ops/mongo/MongoDataOp.vue'),
 | 
			
		||||
};
 | 
			
		||||
@@ -43,8 +43,8 @@
 | 
			
		||||
            </template>
 | 
			
		||||
 | 
			
		||||
            <template #right>
 | 
			
		||||
                <Splitpanes class="default-theme">
 | 
			
		||||
                    <Pane size="35" max-size="50">
 | 
			
		||||
                <el-splitter>
 | 
			
		||||
                    <el-splitter-panel size="35%" max="50%">
 | 
			
		||||
                        <div class="key-list-vtree h-full card !p-1">
 | 
			
		||||
                            <el-scrollbar>
 | 
			
		||||
                                <el-row :gutter="5">
 | 
			
		||||
@@ -141,9 +141,9 @@
 | 
			
		||||
 | 
			
		||||
                            <contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </Pane>
 | 
			
		||||
                    </el-splitter-panel>
 | 
			
		||||
 | 
			
		||||
                    <Pane>
 | 
			
		||||
                    <el-splitter-panel>
 | 
			
		||||
                        <div class="h-full card !p-1 key-deatil">
 | 
			
		||||
                            <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">
 | 
			
		||||
@@ -151,8 +151,8 @@
 | 
			
		||||
                                </el-tab-pane>
 | 
			
		||||
                            </el-tabs>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </Pane>
 | 
			
		||||
                </Splitpanes>
 | 
			
		||||
                    </el-splitter-panel>
 | 
			
		||||
                </el-splitter>
 | 
			
		||||
            </template>
 | 
			
		||||
        </ResourceOpPanel>
 | 
			
		||||
 | 
			
		||||
@@ -196,7 +196,6 @@ import { keysToTree, sortByTreeNodes, keysToList } from './utils';
 | 
			
		||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
 | 
			
		||||
import { sleep } from '@/common/utils/loading';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import { Splitpanes, Pane } from 'splitpanes';
 | 
			
		||||
import { RedisInst } from './redis';
 | 
			
		||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
 | 
			
		||||
import { storeToRefs } from 'pinia';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,13 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <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-col :lg="16" :md="16">
 | 
			
		||||
                    <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-col :lg="12" :md="12">
 | 
			
		||||
                    <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="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.clusterEnable')">{{ info.Cluster?.cluster_enabled }}</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>
 | 
			
		||||
                </el-col>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								frontend/src/views/ops/redis/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/src/views/ops/redis/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
export default {
 | 
			
		||||
    RedisList: () => import('@/views/ops/redis/RedisList.vue'),
 | 
			
		||||
    DataOperation: () => import('@/views/ops/redis/DataOperation.vue'),
 | 
			
		||||
};
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="tag-tree-list card h-full flex">
 | 
			
		||||
        <Splitpanes class="default-theme">
 | 
			
		||||
            <Pane size="30" min-size="25" max-size="35" class="flex flex-col flex-1">
 | 
			
		||||
    <div class="tag-tree-list card !p-2 h-full flex">
 | 
			
		||||
        <el-splitter>
 | 
			
		||||
            <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="mb-1">
 | 
			
		||||
                        <el-input v-model="filterTag" clearable :placeholder="$t('tag.nameFilterPlaceholder')" class="mr-2 !w-[200px]" />
 | 
			
		||||
@@ -63,9 +63,9 @@
 | 
			
		||||
                        </template>
 | 
			
		||||
                    </el-tree>
 | 
			
		||||
                </el-scrollbar>
 | 
			
		||||
            </Pane>
 | 
			
		||||
            </el-splitter-panel>
 | 
			
		||||
 | 
			
		||||
            <Pane min-size="40" size="70">
 | 
			
		||||
            <el-splitter-panel>
 | 
			
		||||
                <div class="ml-2 h-full">
 | 
			
		||||
                    <el-tabs class="h-full" @tab-change="onTabChange" v-model="state.activeTabName" v-if="currentTag">
 | 
			
		||||
                        <el-tab-pane :label="$t('common.detail')" :name="TagDetail">
 | 
			
		||||
@@ -117,6 +117,7 @@
 | 
			
		||||
                        </el-tab-pane>
 | 
			
		||||
 | 
			
		||||
                        <el-tab-pane
 | 
			
		||||
                            class="h-full"
 | 
			
		||||
                            :disabled="currentTag.type != TagResourceTypeEnum.Tag.value"
 | 
			
		||||
                            :label="`Redis (${resourceCount.redis || 0})`"
 | 
			
		||||
                            :name="RedisTag"
 | 
			
		||||
@@ -134,8 +135,8 @@
 | 
			
		||||
                        </el-tab-pane>
 | 
			
		||||
                    </el-tabs>
 | 
			
		||||
                </div>
 | 
			
		||||
            </Pane>
 | 
			
		||||
        </Splitpanes>
 | 
			
		||||
            </el-splitter-panel>
 | 
			
		||||
        </el-splitter>
 | 
			
		||||
 | 
			
		||||
        <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">
 | 
			
		||||
@@ -167,7 +168,6 @@ import { tagApi } from './api';
 | 
			
		||||
import { formatDate } from '@/common/utils/format';
 | 
			
		||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index';
 | 
			
		||||
import { useUserInfo } from '@/store/userInfo';
 | 
			
		||||
import { Splitpanes, Pane } from 'splitpanes';
 | 
			
		||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
			
		||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
 | 
			
		||||
import EnumValue from '@/common/Enum';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								frontend/src/views/ops/tag/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/views/ops/tag/route.ts
									
									
									
									
									
										Normal 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'),
 | 
			
		||||
};
 | 
			
		||||
@@ -58,6 +58,10 @@ import config from '@/common/config';
 | 
			
		||||
import { joinClientParams } from '@/common/request';
 | 
			
		||||
import { useI18n } from 'vue-i18n';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
    name: 'Personal',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
 | 
			
		||||
const state = reactive({
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								frontend/src/views/personal/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/views/personal/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
export default {
 | 
			
		||||
    Personal: () => import('@/views/personal/index.vue'),
 | 
			
		||||
};
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <el-popover
 | 
			
		||||
            v-if="props.username"
 | 
			
		||||
            v-if="props.username && props.username != '-'"
 | 
			
		||||
            @show="getAccountInfo(props.username)"
 | 
			
		||||
            placement="top-start"
 | 
			
		||||
            :title="$t('system.account.accountInfo')"
 | 
			
		||||
 
 | 
			
		||||
@@ -33,16 +33,6 @@
 | 
			
		||||
                            <el-input v-model.trim="form.meta.routeName"></el-input>
 | 
			
		||||
                        </FormItemTooltip>
 | 
			
		||||
                    </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">
 | 
			
		||||
                        <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">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="card system-resource-list h-full flex">
 | 
			
		||||
        <Splitpanes class="default-theme">
 | 
			
		||||
            <Pane size="30" min-size="25" max-size="35" class="flex flex-col flex-1">
 | 
			
		||||
    <div class="card !p-2 system-resource-list h-full flex">
 | 
			
		||||
        <el-splitter>
 | 
			
		||||
            <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="mb-1">
 | 
			
		||||
                        <el-input v-model="filterResource" clearable :placeholder="$t('system.menu.filterPlaceholder')" class="mr-2 !w-[200px]" />
 | 
			
		||||
@@ -60,9 +60,9 @@
 | 
			
		||||
                        </template>
 | 
			
		||||
                    </el-tree>
 | 
			
		||||
                </el-scrollbar>
 | 
			
		||||
            </Pane>
 | 
			
		||||
            </el-splitter-panel>
 | 
			
		||||
 | 
			
		||||
            <Pane min-size="40" size="70">
 | 
			
		||||
            <el-splitter-panel>
 | 
			
		||||
                <div class="ml-2">
 | 
			
		||||
                    <el-tabs v-model="state.activeTabName" @tab-click="onTabClick" v-if="currentResource">
 | 
			
		||||
                        <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')">
 | 
			
		||||
                                    {{ currentResource.meta.routeName }}
 | 
			
		||||
                                </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')">
 | 
			
		||||
                                    {{ currentResource.meta.isKeepAlive ? $t('system.menu.yes') : $t('system.menu.no') }}
 | 
			
		||||
                                </el-descriptions-item>
 | 
			
		||||
@@ -126,8 +123,8 @@
 | 
			
		||||
                        </el-tab-pane>
 | 
			
		||||
                    </el-tabs>
 | 
			
		||||
                </div>
 | 
			
		||||
            </Pane>
 | 
			
		||||
        </Splitpanes>
 | 
			
		||||
            </el-splitter-panel>
 | 
			
		||||
        </el-splitter>
 | 
			
		||||
 | 
			
		||||
        <ResourceEdit
 | 
			
		||||
            :title="dialogForm.title"
 | 
			
		||||
@@ -152,7 +149,6 @@ import { resourceApi } from '../api';
 | 
			
		||||
import { formatDate } from '@/common/utils/format';
 | 
			
		||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
 | 
			
		||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
 | 
			
		||||
import { Splitpanes, Pane } from 'splitpanes';
 | 
			
		||||
import { isPrefixSubsequence } from '@/common/utils/string';
 | 
			
		||||
import { useI18n } from 'vue-i18n';
 | 
			
		||||
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg } from '@/hooks/useI18n';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/src/views/system/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/views/system/route.ts
									
									
									
									
									
										Normal 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'),
 | 
			
		||||
};
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
    <div class="h-full">
 | 
			
		||||
        <page-table :page-api="logApi.list" :search-items="searchItems" v-model:query-form="query" :columns="columns">
 | 
			
		||||
            <template #creator="{ data }">
 | 
			
		||||
                <account-info :username="data.creator" />
 | 
			
		||||
                <account-info :username="data.creator || ''" />
 | 
			
		||||
            </template>
 | 
			
		||||
        </page-table>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -58,11 +58,21 @@ const viteConfig: UserConfig = {
 | 
			
		||||
                entryFileNames: `assets/[name]-[hash].js`,
 | 
			
		||||
                chunkFileNames: `assets/[name]-[hash].js`,
 | 
			
		||||
                assetFileNames: `assets/[name]-[hash].[ext]`,
 | 
			
		||||
                compact: true,
 | 
			
		||||
                manualChunks: {
 | 
			
		||||
                    vue: ['vue', 'vue-router', 'pinia'],
 | 
			
		||||
                    echarts: ['echarts'],
 | 
			
		||||
                    monaco: ['monaco-editor'],
 | 
			
		||||
                advancedChunks: {
 | 
			
		||||
                    groups: [
 | 
			
		||||
                        {
 | 
			
		||||
                            name: 'vue',
 | 
			
		||||
                            test: /(vue|vue-router|pinia)/i,
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            name: 'echarts',
 | 
			
		||||
                            test: /(echarts)/i,
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            name: 'monaco',
 | 
			
		||||
                            test: /(monaco-editor)/i,
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -24,22 +24,22 @@ require (
 | 
			
		||||
	github.com/mojocn/base64Captcha v1.3.8 // 验证码
 | 
			
		||||
	github.com/pkg/errors v0.9.1
 | 
			
		||||
	github.com/pkg/sftp v1.13.9
 | 
			
		||||
	github.com/pquerna/otp v1.4.0
 | 
			
		||||
	github.com/redis/go-redis/v9 v9.8.0
 | 
			
		||||
	github.com/pquerna/otp v1.5.0
 | 
			
		||||
	github.com/redis/go-redis/v9 v9.10.0
 | 
			
		||||
	github.com/robfig/cron/v3 v3.0.1 // 定时任务
 | 
			
		||||
	github.com/sijms/go-ora/v2 v2.8.24
 | 
			
		||||
	github.com/stretchr/testify v1.10.0
 | 
			
		||||
	github.com/tidwall/gjson v1.18.0
 | 
			
		||||
	github.com/veops/go-ansiterm v0.0.5
 | 
			
		||||
	go.mongodb.org/mongo-driver v1.16.0 // mongo
 | 
			
		||||
	golang.org/x/crypto v0.38.0 // ssh
 | 
			
		||||
	golang.org/x/crypto v0.39.0 // ssh
 | 
			
		||||
	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/yaml.v3 v3.0.1
 | 
			
		||||
	// gorm
 | 
			
		||||
	gorm.io/driver/mysql v1.5.7
 | 
			
		||||
	gorm.io/gorm v1.26.1
 | 
			
		||||
	gorm.io/driver/mysql v1.6.0
 | 
			
		||||
	gorm.io/gorm v1.30.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
@@ -94,7 +94,7 @@ require (
 | 
			
		||||
	golang.org/x/image v0.23.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.34.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
 | 
			
		||||
	modernc.org/libc v1.22.5 // indirect
 | 
			
		||||
	modernc.org/mathutil v1.5.0 // indirect
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ type DataSyncTaskListVO struct {
 | 
			
		||||
type DataSyncLogListVO struct {
 | 
			
		||||
	CreateTime  *time.Time `json:"createTime"`
 | 
			
		||||
	DataSqlFull string     `json:"dataSqlFull"`
 | 
			
		||||
	ResNum      string     `json:"resNum"`
 | 
			
		||||
	ResNum      int        `json:"resNum"`
 | 
			
		||||
	ErrText     string     `json:"errText"`
 | 
			
		||||
	Status      *int       `json:"status"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,16 +17,6 @@ func InitIoc() {
 | 
			
		||||
 | 
			
		||||
func Init() {
 | 
			
		||||
	sync.OnceFunc(func() {
 | 
			
		||||
		//if err := GetDbBackupApp().Init(); err != nil {
 | 
			
		||||
		//	panic(fmt.Sprintf("初始化 DbBackupApp 失败: %v", err))
 | 
			
		||||
		//}
 | 
			
		||||
		//if err := GetDbRestoreApp().Init(); err != nil {
 | 
			
		||||
		//	panic(fmt.Sprintf("初始化 DbRestoreApp 失败: %v", err))
 | 
			
		||||
		//}
 | 
			
		||||
		//if err := GetDbBinlogApp().Init(); err != nil {
 | 
			
		||||
		//	panic(fmt.Sprintf("初始化 DbBinlogApp 失败: %v", err))
 | 
			
		||||
		//}
 | 
			
		||||
 | 
			
		||||
		GetDataSyncTaskApp().InitCronJob()
 | 
			
		||||
		GetDbTransferTaskApp().InitCronJob()
 | 
			
		||||
		GetDbTransferTaskApp().TimerDeleteTransferFile()
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@ func (d *dbAppImpl) GetPageList(condition *entity.DbQuery, orderBy ...string) (*
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -76,6 +76,7 @@ func (app *dataSyncAppImpl) Save(ctx context.Context, taskEntity *entity.DataSyn
 | 
			
		||||
		taskEntity.TaskKey = uuid.New().String()
 | 
			
		||||
		err = app.Insert(ctx, taskEntity)
 | 
			
		||||
	} else {
 | 
			
		||||
		taskEntity.TaskKey = ""
 | 
			
		||||
		err = app.UpdateById(ctx, taskEntity)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -107,15 +108,13 @@ func (app *dataSyncAppImpl) AddCronJob(ctx context.Context, taskEntity *entity.D
 | 
			
		||||
	// 根据状态添加新的任务
 | 
			
		||||
	if taskEntity.Status == entity.DataSyncTaskStatusEnable {
 | 
			
		||||
		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() {
 | 
			
		||||
			logx.Infof("start the data synchronization task: %d", taskId)
 | 
			
		||||
			cancelCtx, cancelFunc := context.WithCancel(ctx)
 | 
			
		||||
			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())
 | 
			
		||||
			if err := app.RunCronJob(context.Background(), taskId); err != nil {
 | 
			
		||||
				logx.Errorf("the data sync task failed to execute at a scheduled time: %s", err.Error())
 | 
			
		||||
			}
 | 
			
		||||
		}); 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 {
 | 
			
		||||
		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 {
 | 
			
		||||
		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)
 | 
			
		||||
 | 
			
		||||
	logx.InfofContext(ctx, "start the data synchronization task: %s => %s", task.TaskName, task.TaskKey)
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		// 通过占位符格式化sql
 | 
			
		||||
		updSql := ""
 | 
			
		||||
@@ -408,32 +408,11 @@ func (app *dataSyncAppImpl) InitCronJob() {
 | 
			
		||||
	// 修改执行中状态为待执行
 | 
			
		||||
	_ = app.UpdateByCond(context.TODO(), &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateReady}, &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateRunning})
 | 
			
		||||
 | 
			
		||||
	// 把所有正常任务添加到定时任务中
 | 
			
		||||
	cond := new(entity.DataSyncTaskQuery)
 | 
			
		||||
	cond.PageNum = 1
 | 
			
		||||
	cond.PageSize = 100
 | 
			
		||||
	cond.Status = entity.DataSyncTaskStatusEnable
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	if err := app.CursorByCond(&entity.DataSyncTaskQuery{Status: entity.DataSyncTaskStatusEnable}, func(dst *entity.DataSyncTask) error {
 | 
			
		||||
		app.AddCronJob(contextx.NewTraceId(), dst)
 | 
			
		||||
		return nil
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		logx.ErrorTrace("the db data sync task failed to initialize: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,6 @@ func createSqlExecRecord(ctx context.Context, execSqlReq *dto.DbSqlExecReq, sql
 | 
			
		||||
	dbSqlExecRecord.Sql = sql
 | 
			
		||||
	dbSqlExecRecord.Remark = execSqlReq.Remark
 | 
			
		||||
	dbSqlExecRecord.Status = entity.DbSqlExecStatusSuccess
 | 
			
		||||
	dbSqlExecRecord.FillBaseInfo(model.IdGenTypeNone, contextx.GetLoginAccount(ctx))
 | 
			
		||||
	return dbSqlExecRecord
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -130,7 +129,7 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *dto.DbSqlExecRe
 | 
			
		||||
				}
 | 
			
		||||
				execRes.ErrorMsg = err.Error()
 | 
			
		||||
			} else {
 | 
			
		||||
				d.saveSqlExecLog(dbSqlExecRecord, dbSqlExecRecord.Res)
 | 
			
		||||
				d.saveSqlExecLog(ctx, dbSqlExecRecord, dbSqlExecRecord.Res)
 | 
			
		||||
			}
 | 
			
		||||
			allExecRes = append(allExecRes, execRes)
 | 
			
		||||
			return nil
 | 
			
		||||
@@ -191,7 +190,7 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *dto.DbSqlExecRe
 | 
			
		||||
			}
 | 
			
		||||
			execRes.ErrorMsg = err.Error()
 | 
			
		||||
		} else {
 | 
			
		||||
			d.saveSqlExecLog(dbSqlExecRecord, execRes.Res)
 | 
			
		||||
			d.saveSqlExecLog(ctx, dbSqlExecRecord, execRes.Res)
 | 
			
		||||
		}
 | 
			
		||||
		allExecRes = append(allExecRes, execRes)
 | 
			
		||||
	}
 | 
			
		||||
@@ -318,10 +317,10 @@ func (d *dbSqlExecAppImpl) GetPageList(condition *entity.DbSqlExecQuery, orderBy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 保存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 {
 | 
			
		||||
		dbSqlExecRecord.Res = jsonx.ToStr(res)
 | 
			
		||||
		d.dbSqlExecRepo.Insert(context.TODO(), dbSqlExecRecord)
 | 
			
		||||
		d.dbSqlExecRepo.Insert(ctx, dbSqlExecRecord)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -329,7 +328,7 @@ func (d *dbSqlExecAppImpl) saveSqlExecLog(dbSqlExecRecord *entity.DbSqlExec, res
 | 
			
		||||
		dbSqlExecRecord.Table = "-"
 | 
			
		||||
		dbSqlExecRecord.OldValue = "-"
 | 
			
		||||
		dbSqlExecRecord.Type = entity.DbSqlExecTypeQuery
 | 
			
		||||
		d.dbSqlExecRepo.Insert(context.TODO(), dbSqlExecRecord)
 | 
			
		||||
		d.dbSqlExecRepo.Insert(ctx, dbSqlExecRecord)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -143,32 +143,11 @@ func (app *dbTransferAppImpl) InitCronJob() {
 | 
			
		||||
	// 把所有运行中的文件状态设置为失败
 | 
			
		||||
	_ = app.transferFileApp.UpdateByCond(context.TODO(), &entity.DbTransferFile{Status: entity.DbTransferFileStatusFail}, &entity.DbTransferFile{Status: entity.DbTransferFileStatusRunning})
 | 
			
		||||
 | 
			
		||||
	// 把所有需要定时执行的任务添加到定时任务中
 | 
			
		||||
	cond := new(entity.DbTransferTaskQuery)
 | 
			
		||||
	cond.PageNum = 1
 | 
			
		||||
	cond.PageSize = 100
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	if err := app.CursorByCond(&entity.DbTransferTaskQuery{Status: entity.DbTransferTaskStatusEnable, CronAble: entity.DbTransferTaskCronAbleEnable}, func(dtt *entity.DbTransferTask) error {
 | 
			
		||||
		app.AddCronJob(contextx.NewTraceId(), dtt)
 | 
			
		||||
		return nil
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		logx.ErrorTrace("the db data transfer task failed to initialize", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import (
 | 
			
		||||
	_ "mayfly-go/internal/db/dbm/oracle"
 | 
			
		||||
	_ "mayfly-go/internal/db/dbm/postgres"
 | 
			
		||||
	_ "mayfly-go/internal/db/dbm/sqlite"
 | 
			
		||||
	"mayfly-go/internal/machine/mcm"
 | 
			
		||||
	"mayfly-go/pkg/logx"
 | 
			
		||||
	"mayfly-go/pkg/pool"
 | 
			
		||||
)
 | 
			
		||||
@@ -17,6 +18,22 @@ var (
 | 
			
		||||
	poolGroup = pool.NewPoolGroup[*dbi.DbConn]()
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	mcm.AddCheckSshTunnelMachineUseFunc(func(machineId int) bool {
 | 
			
		||||
		items := poolGroup.AllPool()
 | 
			
		||||
		for _, v := range items {
 | 
			
		||||
			conn, err := v.Get(context.Background(), pool.WithGetNoUpdateLastActive(), pool.WithGetNoNewConn())
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue // 获取连接失败,跳过
 | 
			
		||||
			}
 | 
			
		||||
			if conn.Info.SshTunnelMachineId == machineId {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDbConn 从连接池中获取连接信息
 | 
			
		||||
func GetDbConn(ctx context.Context, dbId uint64, database string, getDbInfo func() (*dbi.DbInfo, error)) (*dbi.DbConn, error) {
 | 
			
		||||
	connId := dbi.GetDbConnId(dbId, database)
 | 
			
		||||
 
 | 
			
		||||
@@ -65,6 +65,9 @@ type DbSqlExecQuery struct {
 | 
			
		||||
	Table      string `json:"table" form:"table"`
 | 
			
		||||
	Type       int8   `json:"type" form:"type"` // 类型
 | 
			
		||||
	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
 | 
			
		||||
	CreatorId uint64
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,9 @@ func (d *dbSqlExecRepoImpl) GetPageList(condition *entity.DbSqlExecQuery, orderB
 | 
			
		||||
		Eq("creator_id", condition.CreatorId).
 | 
			
		||||
		Eq("flow_biz_key", condition.FlowBizKey).
 | 
			
		||||
		In("status", condition.Status).
 | 
			
		||||
		Like("sql", condition.Keyword).
 | 
			
		||||
		Ge("create_time", condition.StartTime).
 | 
			
		||||
		Le("create_time", condition.EndTime).
 | 
			
		||||
		RLike("db", condition.Db).OrderBy(orderBy...)
 | 
			
		||||
	return d.PageByCond(qd, condition.PageParam)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import (
 | 
			
		||||
	"mayfly-go/internal/es/domain/repository"
 | 
			
		||||
	"mayfly-go/internal/es/esm/esi"
 | 
			
		||||
	"mayfly-go/internal/es/imsg"
 | 
			
		||||
	"mayfly-go/internal/machine/mcm"
 | 
			
		||||
	"mayfly-go/internal/pkg/consts"
 | 
			
		||||
	tagapp "mayfly-go/internal/tag/application"
 | 
			
		||||
	tagdto "mayfly-go/internal/tag/application/dto"
 | 
			
		||||
@@ -40,6 +41,22 @@ var _ Instance = &instanceAppImpl{}
 | 
			
		||||
 | 
			
		||||
var poolGroup = pool.NewPoolGroup[*esi.EsConn]()
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	mcm.AddCheckSshTunnelMachineUseFunc(func(machineId int) bool {
 | 
			
		||||
		items := poolGroup.AllPool()
 | 
			
		||||
		for _, v := range items {
 | 
			
		||||
			conn, err := v.Get(context.Background(), pool.WithGetNoUpdateLastActive(), pool.WithGetNoNewConn())
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue // 获取连接失败,跳过
 | 
			
		||||
			}
 | 
			
		||||
			if conn.Info.SshTunnelMachineId == machineId {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type instanceAppImpl struct {
 | 
			
		||||
	base.AppImpl[*entity.EsInstance, repository.EsInstance]
 | 
			
		||||
 | 
			
		||||
@@ -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 {
 | 
			
		||||
	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)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
@@ -234,6 +251,8 @@ func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error
 | 
			
		||||
		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.DeleteById(ctx, instanceId)
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ import (
 | 
			
		||||
type EsVersion string
 | 
			
		||||
 | 
			
		||||
type EsInfo struct {
 | 
			
		||||
	model.ExtraData // 连接需要的其他额外参数(json字符串),如oracle数据库需要指定sid等
 | 
			
		||||
	model.ExtraData // 连接需要的其他额外参数(json字符串)
 | 
			
		||||
 | 
			
		||||
	InstanceId uint64 // 实例id
 | 
			
		||||
	Name       string
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,7 @@ type MachineScriptForm struct {
 | 
			
		||||
	Name        string `json:"name" binding:"required"`
 | 
			
		||||
	MachineId   uint64 `json:"machineId" binding:"required"`
 | 
			
		||||
	Type        int    `json:"type" binding:"required"`
 | 
			
		||||
	Category    string `json:"category"`
 | 
			
		||||
	Description string `json:"description" binding:"required"`
 | 
			
		||||
	Params      string `json:"params"`
 | 
			
		||||
	Script      string `json:"script" binding:"required"`
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,8 @@ func (ms *MachineScript) ReqConfs() *req.Confs {
 | 
			
		||||
		// 获取指定机器脚本列表
 | 
			
		||||
		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.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) {
 | 
			
		||||
	condition := &entity.MachineScript{MachineId: GetMachineId(rc)}
 | 
			
		||||
	condition := &entity.MachineScript{MachineId: GetMachineId(rc), Category: rc.Query("category")}
 | 
			
		||||
	res, err := m.machineScriptApp.GetPageList(condition, rc.GetPageParam())
 | 
			
		||||
	biz.ErrIsNil(err)
 | 
			
		||||
	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) {
 | 
			
		||||
	form, machineScript := req.BindJsonAndCopyTo[*form.MachineScriptForm, *entity.MachineScript](rc)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,7 @@ type MachineScriptVO struct {
 | 
			
		||||
	Name        *string `json:"name"`
 | 
			
		||||
	Script      *string `json:"script"`
 | 
			
		||||
	Type        *int    `json:"type"`
 | 
			
		||||
	Category    string  `json:"category"`
 | 
			
		||||
	Description *string `json:"description"`
 | 
			
		||||
	Params      *string `json:"params"`
 | 
			
		||||
	MachineId   *uint64 `json:"machineId"`
 | 
			
		||||
 
 | 
			
		||||
@@ -101,28 +101,11 @@ func (m *machineCronJobAppImpl) InitCronJob() {
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	pageParam := model.PageParam{
 | 
			
		||||
		PageSize: 100,
 | 
			
		||||
		PageNum:  1,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 {
 | 
			
		||||
	if err := m.CursorByCond(&entity.MachineCronJob{Status: entity.MachineCronJobStatusEnable}, func(mcj *entity.MachineCronJob) error {
 | 
			
		||||
		m.addCronJob(mcj)
 | 
			
		||||
			add++
 | 
			
		||||
		}
 | 
			
		||||
		if add >= int(total) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		pageParam.PageNum = pageParam.PageNum + 1
 | 
			
		||||
		m.GetPageList(cond, pageParam)
 | 
			
		||||
		return nil
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		logx.ErrorTrace("the machine cronjob failed to initialize: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import (
 | 
			
		||||
	"mayfly-go/pkg/base"
 | 
			
		||||
	"mayfly-go/pkg/errorx"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"mayfly-go/pkg/utils/collx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MachineScript interface {
 | 
			
		||||
@@ -15,11 +16,16 @@ type MachineScript interface {
 | 
			
		||||
	// 分页获取机器脚本信息列表
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
	Delete(ctx context.Context, id uint64)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ (MachineScript) = (*machineScriptAppImpl)(nil)
 | 
			
		||||
 | 
			
		||||
type machineScriptAppImpl struct {
 | 
			
		||||
	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...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
	// 如果机器id不为公共脚本id,则校验机器是否存在
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ type MachineScript struct {
 | 
			
		||||
	Name        string `json:"name" gorm:"not null;size:255;comment:脚本名"`     // 脚本名
 | 
			
		||||
	MachineId   uint64 `json:"machineId" gorm:"not null;comment:机器id[0:公共]"`  // 机器id
 | 
			
		||||
	Type        int    `json:"type" gorm:"comment:脚本类型[1: 有结果;2:无结果;3:实时交互]"` // 脚本类型[1: 有结果;2:无结果;3:实时交互]
 | 
			
		||||
	Category    string `json:"category" gorm:"size:20;comment:分类"`
 | 
			
		||||
	Description string `json:"description" gorm:"size:255;comment:脚本描述"` // 脚本描述
 | 
			
		||||
	Params      string `json:"params" gorm:"size:500;comment:脚本入参"`      // 参数列表json
 | 
			
		||||
	Script      string `json:"script" gorm:"type:text;comment:脚本内容"`     // 脚本内容
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package mcm
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mayfly-go/pkg/pool"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -9,6 +10,27 @@ var (
 | 
			
		||||
	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 机器的授权凭证名
 | 
			
		||||
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) {
 | 
			
		||||
	for _, pool := range poolGroup.AllPool() {
 | 
			
		||||
		if pool.Stats().TotalConns == 0 {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		conn, err := pool.Get(context.Background())
 | 
			
		||||
	for _, p := range poolGroup.AllPool() {
 | 
			
		||||
		conn, err := p.Get(context.Background(), pool.WithGetNoUpdateLastActive(), pool.WithGetNoNewConn())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if conn.Info.Id == id {
 | 
			
		||||
			pool.Close()
 | 
			
		||||
			poolGroup.Close(conn.Info.AuthCertName)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// 删除隧道
 | 
			
		||||
	tunnelPoolGroup.Close(fmt.Sprintf("machine-tunnel-%d", id))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,15 @@ import (
 | 
			
		||||
	"mayfly-go/pkg/utils/netx"
 | 
			
		||||
	"net"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// type SshTunnelAble interface {
 | 
			
		||||
// 	GetSshTunnelMachineId() int
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// 所有检测ssh隧道机器是否被使用的函数
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
		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 {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -142,18 +160,19 @@ func GetSshTunnelMachine(ctx context.Context, machineId int, getMachine func(uin
 | 
			
		||||
 | 
			
		||||
// 关闭ssh隧道机器的指定隧道
 | 
			
		||||
func CloseSshTunnelMachine(machineId uint64, tunnelId string) {
 | 
			
		||||
	//sshTunnelMachine := mcIdPool[machineId]
 | 
			
		||||
	//if sshTunnelMachine == nil {
 | 
			
		||||
	//	return
 | 
			
		||||
	//}
 | 
			
		||||
	//
 | 
			
		||||
	//sshTunnelMachine.mutex.Lock()
 | 
			
		||||
	//defer sshTunnelMachine.mutex.Unlock()
 | 
			
		||||
	//t := sshTunnelMachine.tunnels[tunnelId]
 | 
			
		||||
	//if t != nil {
 | 
			
		||||
	//	t.Close()
 | 
			
		||||
	//	delete(sshTunnelMachine.tunnels, tunnelId)
 | 
			
		||||
	//}
 | 
			
		||||
	sshTunnelMachinePool, ok := tunnelPoolGroup.Get(fmt.Sprintf("machine-tunnel-%d", machineId))
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	sshTunnelMachine, err := sshTunnelMachinePool.Get(context.Background())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	t := sshTunnelMachine.tunnels[tunnelId]
 | 
			
		||||
	if t != nil {
 | 
			
		||||
		t.Close()
 | 
			
		||||
		delete(sshTunnelMachine.tunnels, tunnelId)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Tunnel struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package mgm
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"mayfly-go/internal/machine/mcm"
 | 
			
		||||
	"mayfly-go/pkg/pool"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -9,6 +10,22 @@ var (
 | 
			
		||||
	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进行连接并缓存
 | 
			
		||||
func GetMongoConn(ctx context.Context, mongoId uint64, getMongoInfo func() (*MongoInfo, error)) (*MongoConn, error) {
 | 
			
		||||
	pool, err := poolGroup.GetCachePool(getConnId(mongoId), func() (*MongoConn, error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import "fmt"
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	AppName = "mayfly-go"
 | 
			
		||||
	Version = "v1.10.0"
 | 
			
		||||
	Version = "v1.10.1"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetAppInfo() string {
 | 
			
		||||
 
 | 
			
		||||
@@ -174,8 +174,7 @@ func (r *redisAppImpl) Delete(ctx context.Context, id uint64) error {
 | 
			
		||||
	}
 | 
			
		||||
	// 如果存在连接,则关闭所有库连接信息
 | 
			
		||||
	for _, dbStr := range strings.Split(re.Db, ",") {
 | 
			
		||||
		db, _ := strconv.Atoi(dbStr)
 | 
			
		||||
		rdm.CloseConn(re.Id, db)
 | 
			
		||||
		rdm.CloseConn(re.Id, cast.ToInt(dbStr))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return r.Tx(ctx, func(ctx context.Context) error {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,16 +2,31 @@ package rdm
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"mayfly-go/internal/machine/mcm"
 | 
			
		||||
	"mayfly-go/pkg/pool"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	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进行连接并缓存
 | 
			
		||||
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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ package migrations
 | 
			
		||||
import (
 | 
			
		||||
	esentity "mayfly-go/internal/es/domain/entity"
 | 
			
		||||
	flowentity "mayfly-go/internal/flow/domain/entity"
 | 
			
		||||
	machineentity "mayfly-go/internal/machine/domain/entity"
 | 
			
		||||
	sysentity "mayfly-go/internal/sys/domain/entity"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -14,6 +15,7 @@ import (
 | 
			
		||||
func V1_10() []*gormigrate.Migration {
 | 
			
		||||
	var migrations []*gormigrate.Migration
 | 
			
		||||
	migrations = append(migrations, V1_10_0()...)
 | 
			
		||||
	migrations = append(migrations, V1_10_1()...)
 | 
			
		||||
	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
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,9 @@ type App[T model.ModelI] interface {
 | 
			
		||||
	// @param cond 可为*model.QueryCond也可以为普通查询model
 | 
			
		||||
	CountByCond(cond any) int64
 | 
			
		||||
 | 
			
		||||
	//  CursorByCond 根据指定条件遍历model表数据
 | 
			
		||||
	CursorByCond(cond any, handler func(T) error) error
 | 
			
		||||
 | 
			
		||||
	// Tx 执行事务操作
 | 
			
		||||
	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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 执行事务操作
 | 
			
		||||
func (ai *AppImpl[T, R]) Tx(ctx context.Context, funcs ...func(context.Context) error) (err error) {
 | 
			
		||||
	dbCtx := ctx
 | 
			
		||||
 
 | 
			
		||||
@@ -89,6 +89,9 @@ type Repo[T model.ModelI] interface {
 | 
			
		||||
 | 
			
		||||
	// CountByCond 根据指定条件统计model表的数量
 | 
			
		||||
	CountByCond(cond any) int64
 | 
			
		||||
 | 
			
		||||
	// SelectByCondWithOffset 根据条件查询数据并支持 offset + limit 分页
 | 
			
		||||
	SelectByCondWithOffset(cond any, limit int, offset int) ([]T, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 新建模型实例
 | 
			
		||||
func (br *RepoImpl[T]) NewModel() T {
 | 
			
		||||
	newModel := reflect.New(br.getModelType()).Interface()
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,6 @@ func (la *LoginAccount) GetAesKey() string {
 | 
			
		||||
 | 
			
		||||
// 系统账号
 | 
			
		||||
var SysAccount = &LoginAccount{
 | 
			
		||||
	Id:       1,
 | 
			
		||||
	Username: "system",
 | 
			
		||||
	Id:       0,
 | 
			
		||||
	Username: "-",
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,41 +2,34 @@ package pool
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"mayfly-go/pkg/logx"
 | 
			
		||||
	"mayfly-go/pkg/utils/stringx"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var CachePoolDefaultConfig = PoolConfig{
 | 
			
		||||
	MaxConns:            1,
 | 
			
		||||
	IdleTimeout:         60 * time.Minute,
 | 
			
		||||
	WaitTimeout:         10 * time.Second,
 | 
			
		||||
	HealthCheckInterval: 10 * time.Minute,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type cacheEntry[T Conn] struct {
 | 
			
		||||
	conn       T
 | 
			
		||||
	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 {
 | 
			
		||||
	factory func() (T, error)
 | 
			
		||||
	mu      sync.RWMutex
 | 
			
		||||
	cache   map[string]*cacheEntry[T] // 使用字符串键的缓存
 | 
			
		||||
	config  PoolConfig
 | 
			
		||||
	config  PoolConfig[T]
 | 
			
		||||
	closeCh chan struct{}
 | 
			
		||||
	closed  bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewCachePool[T Conn](factory func() (T, error), opts ...Option) *CachePool[T] {
 | 
			
		||||
	config := CachePoolDefaultConfig
 | 
			
		||||
func NewCachePool[T Conn](factory func() (T, error), opts ...Option[T]) *CachePool[T] {
 | 
			
		||||
	config := PoolConfig[T]{
 | 
			
		||||
		MaxConns:            1,
 | 
			
		||||
		IdleTimeout:         60 * time.Minute,
 | 
			
		||||
		WaitTimeout:         10 * time.Second,
 | 
			
		||||
		HealthCheckInterval: 10 * time.Minute,
 | 
			
		||||
	}
 | 
			
		||||
	for _, opt := range opts {
 | 
			
		||||
		opt(&config)
 | 
			
		||||
	}
 | 
			
		||||
@@ -53,20 +46,40 @@ func NewCachePool[T Conn](factory func() (T, error), opts ...Option) *CachePool[
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
 | 
			
		||||
	options := defaultGetOptions // 默认更新 lastActive
 | 
			
		||||
	for _, apply := range opts {
 | 
			
		||||
		apply(&options)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 先尝试加读锁,仅用于查找可用连接
 | 
			
		||||
	p.mu.RLock()
 | 
			
		||||
	for _, entry := range p.cache {
 | 
			
		||||
		if time.Since(entry.lastActive) <= p.config.IdleTimeout {
 | 
			
		||||
			p.mu.RUnlock() // 找到后释放读锁
 | 
			
		||||
			return entry.conn, nil
 | 
			
		||||
	if len(p.cache) >= p.config.MaxConns {
 | 
			
		||||
		keys := make([]string, 0, len(p.cache))
 | 
			
		||||
		for k := range p.cache {
 | 
			
		||||
			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()
 | 
			
		||||
 | 
			
		||||
	// 没有找到可用连接,升级为写锁进行清理和创建
 | 
			
		||||
	if !options.newConn {
 | 
			
		||||
		return zero, ErrNoAvailableConn
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 没有找到可用连接,升级为写锁进行创建
 | 
			
		||||
	p.mu.Lock()
 | 
			
		||||
	defer p.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
@@ -74,15 +87,15 @@ func (p *CachePool[T]) Get(ctx context.Context) (T, error) {
 | 
			
		||||
		return zero, ErrPoolClosed
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 再次检查是否已有可用连接(防止并发创建)
 | 
			
		||||
	for key, entry := range p.cache {
 | 
			
		||||
		if time.Since(entry.lastActive) <= p.config.IdleTimeout {
 | 
			
		||||
	// 再次检查是否已创建(防止并发)
 | 
			
		||||
	if len(p.cache) >= p.config.MaxConns {
 | 
			
		||||
		for _, entry := range p.cache {
 | 
			
		||||
			if options.updateLastActive {
 | 
			
		||||
				// 更新最后活跃时间
 | 
			
		||||
				entry.lastActive = time.Now()
 | 
			
		||||
			}
 | 
			
		||||
			return entry.conn, nil
 | 
			
		||||
		}
 | 
			
		||||
		// 清理超时连接
 | 
			
		||||
		entry.Close()
 | 
			
		||||
		delete(p.cache, key)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建新连接
 | 
			
		||||
@@ -151,8 +164,9 @@ func (p *CachePool[T]) Close() {
 | 
			
		||||
	p.closed = true
 | 
			
		||||
	close(p.closeCh)
 | 
			
		||||
 | 
			
		||||
	for _, entry := range p.cache {
 | 
			
		||||
		entry.Close()
 | 
			
		||||
	for key, entry := range p.cache {
 | 
			
		||||
		// 强制关闭连接
 | 
			
		||||
		p.closeConn(key, entry, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 触发关闭回调
 | 
			
		||||
@@ -212,11 +226,47 @@ func (p *CachePool[T]) cleanupIdle() {
 | 
			
		||||
 | 
			
		||||
	cutoff := time.Now().Add(-p.config.IdleTimeout)
 | 
			
		||||
	for key, entry := range p.cache {
 | 
			
		||||
		if entry.lastActive.Before(cutoff) {
 | 
			
		||||
			entry.Close()
 | 
			
		||||
		if entry.lastActive.Before(cutoff) || !p.ping(entry.conn) {
 | 
			
		||||
			logx.Infof("cache pool - cleaning up idle connection, key: %s", 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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 生成缓存键
 | 
			
		||||
 
 | 
			
		||||
@@ -10,13 +10,6 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ChanPoolDefaultConfig = PoolConfig{
 | 
			
		||||
	MaxConns:            5,
 | 
			
		||||
	IdleTimeout:         60 * time.Minute,
 | 
			
		||||
	WaitTimeout:         10 * time.Second,
 | 
			
		||||
	HealthCheckInterval: 10 * time.Minute,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// chanConn 封装连接及其元数据
 | 
			
		||||
type chanConn[T Conn] struct {
 | 
			
		||||
	conn       T
 | 
			
		||||
@@ -41,7 +34,7 @@ type ChanPool[T Conn] struct {
 | 
			
		||||
	mu           sync.RWMutex
 | 
			
		||||
	factory      func() (T, error)
 | 
			
		||||
	idleConns    chan *chanConn[T]
 | 
			
		||||
	config       PoolConfig
 | 
			
		||||
	config       PoolConfig[T]
 | 
			
		||||
	currentConns int32
 | 
			
		||||
	stats        PoolStats
 | 
			
		||||
	closeChan    chan struct{} // 用于关闭健康检查 goroutine
 | 
			
		||||
@@ -56,9 +49,14 @@ type PoolStats struct {
 | 
			
		||||
	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 覆盖)
 | 
			
		||||
	config := ChanPoolDefaultConfig
 | 
			
		||||
	config := PoolConfig[T]{
 | 
			
		||||
		MaxConns:            5,
 | 
			
		||||
		IdleTimeout:         60 * time.Minute,
 | 
			
		||||
		WaitTimeout:         10 * time.Second,
 | 
			
		||||
		HealthCheckInterval: 10 * time.Minute,
 | 
			
		||||
	}
 | 
			
		||||
	for _, opt := range opts {
 | 
			
		||||
		opt(&config)
 | 
			
		||||
	}
 | 
			
		||||
@@ -76,12 +74,17 @@ func NewChannelPool[T Conn](factory func() (T, error), opts ...Option) *ChanPool
 | 
			
		||||
	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)
 | 
			
		||||
	errChan := make(chan error, 1)
 | 
			
		||||
 | 
			
		||||
	options := defaultGetOptions // 默认更新 lastActive
 | 
			
		||||
	for _, apply := range opts {
 | 
			
		||||
		apply(&options)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		conn, err := p.get()
 | 
			
		||||
		conn, err := p.get(options)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			errChan <- err
 | 
			
		||||
		} 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()
 | 
			
		||||
	if p.closed {
 | 
			
		||||
		p.mu.RUnlock()
 | 
			
		||||
		var zero T
 | 
			
		||||
		return zero, ErrPoolClosed
 | 
			
		||||
	}
 | 
			
		||||
	p.mu.RUnlock()
 | 
			
		||||
@@ -125,9 +128,14 @@ func (p *ChanPool[T]) get() (T, error) {
 | 
			
		||||
	case wrapper := <-p.idleConns:
 | 
			
		||||
		atomic.AddInt32(&p.stats.IdleConns, -1)
 | 
			
		||||
		atomic.AddInt32(&p.stats.ActiveConns, 1)
 | 
			
		||||
		if opts.updateLastActive {
 | 
			
		||||
			wrapper.lastActive = time.Now()
 | 
			
		||||
		}
 | 
			
		||||
		return wrapper.conn, nil
 | 
			
		||||
	default:
 | 
			
		||||
		if !opts.newConn {
 | 
			
		||||
			return zero, ErrNoAvailableConn
 | 
			
		||||
		}
 | 
			
		||||
		return p.createConn()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,51 +5,95 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ErrPoolClosed = errors.New("pool is closed")
 | 
			
		||||
var (
 | 
			
		||||
	ErrPoolClosed      = errors.New("pool is closed")
 | 
			
		||||
	ErrNoAvailableConn = errors.New("no available connection")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PoolConfig 连接池配置
 | 
			
		||||
type PoolConfig struct {
 | 
			
		||||
type PoolConfig[T Conn] struct {
 | 
			
		||||
	MaxConns            int           // 最大连接数
 | 
			
		||||
	IdleTimeout         time.Duration // 空闲连接超时时间
 | 
			
		||||
	WaitTimeout         time.Duration // 获取连接超时时间
 | 
			
		||||
	HealthCheckInterval time.Duration // 健康检查间隔
 | 
			
		||||
	OnPoolClose         func() error  // 连接池关闭时的回调
 | 
			
		||||
 | 
			
		||||
	OnConnClose func(conn T) error // 连接关闭时的回调,若err != nil则不关闭连接
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Option 函数类型,用于配置 Pool
 | 
			
		||||
type Option func(*PoolConfig)
 | 
			
		||||
type Option[T Conn] func(*PoolConfig[T])
 | 
			
		||||
 | 
			
		||||
// WithMaxConns 设置最大连接数
 | 
			
		||||
func WithMaxConns(maxConns int) Option {
 | 
			
		||||
	return func(c *PoolConfig) {
 | 
			
		||||
func WithMaxConns[T Conn](maxConns int) Option[T] {
 | 
			
		||||
	return func(c *PoolConfig[T]) {
 | 
			
		||||
		c.MaxConns = maxConns
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithIdleTimeout 设置空闲超时
 | 
			
		||||
func WithIdleTimeout(timeout time.Duration) Option {
 | 
			
		||||
	return func(c *PoolConfig) {
 | 
			
		||||
func WithIdleTimeout[T Conn](timeout time.Duration) Option[T] {
 | 
			
		||||
	return func(c *PoolConfig[T]) {
 | 
			
		||||
		c.IdleTimeout = timeout
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithWaitTimeout 设置等待超时
 | 
			
		||||
func WithWaitTimeout(timeout time.Duration) Option {
 | 
			
		||||
	return func(c *PoolConfig) {
 | 
			
		||||
func WithWaitTimeout[T Conn](timeout time.Duration) Option[T] {
 | 
			
		||||
	return func(c *PoolConfig[T]) {
 | 
			
		||||
		c.WaitTimeout = timeout
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithHealthCheckInterval 设置健康检查间隔
 | 
			
		||||
func WithHealthCheckInterval(interval time.Duration) Option {
 | 
			
		||||
	return func(c *PoolConfig) {
 | 
			
		||||
func WithHealthCheckInterval[T Conn](interval time.Duration) Option[T] {
 | 
			
		||||
	return func(c *PoolConfig[T]) {
 | 
			
		||||
		c.HealthCheckInterval = interval
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WithOnPoolClose 设置连接池关闭回调
 | 
			
		||||
func WithOnPoolClose(fn func() error) Option {
 | 
			
		||||
	return func(c *PoolConfig) {
 | 
			
		||||
func WithOnPoolClose[T Conn](fn func() error) Option[T] {
 | 
			
		||||
	return func(c *PoolConfig[T]) {
 | 
			
		||||
		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
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,11 @@
 | 
			
		||||
package pool
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mayfly-go/pkg/logx"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/sync/singleflight"
 | 
			
		||||
)
 | 
			
		||||
@@ -11,19 +14,23 @@ type PoolGroup[T Conn] struct {
 | 
			
		||||
	mu          sync.RWMutex
 | 
			
		||||
	poolGroup   map[string]Pool[T]
 | 
			
		||||
	createGroup singleflight.Group
 | 
			
		||||
	closingWg   sync.WaitGroup
 | 
			
		||||
	closingMu   sync.Mutex
 | 
			
		||||
	closingCh   chan struct{} // 添加关闭通道
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewPoolGroup[T Conn]() *PoolGroup[T] {
 | 
			
		||||
	return &PoolGroup[T]{
 | 
			
		||||
		poolGroup:   make(map[string]Pool[T]),
 | 
			
		||||
		createGroup: singleflight.Group{},
 | 
			
		||||
		closingCh:   make(chan struct{}),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pg *PoolGroup[T]) GetOrCreate(
 | 
			
		||||
	key string,
 | 
			
		||||
	poolFactory func() Pool[T],
 | 
			
		||||
	opts ...Option,
 | 
			
		||||
	opts ...Option[T],
 | 
			
		||||
) (Pool[T], error) {
 | 
			
		||||
	// 先尝试读锁获取
 | 
			
		||||
	pg.mu.RLock()
 | 
			
		||||
@@ -63,41 +70,115 @@ func (pg *PoolGroup[T]) GetOrCreate(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 NewChannelPool(factory, opts...)
 | 
			
		||||
	}, opts...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 NewCachePool(factory, 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 {
 | 
			
		||||
	pg.mu.Lock()
 | 
			
		||||
	defer pg.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	if p, ok := pg.poolGroup[key]; ok {
 | 
			
		||||
		logx.Infof("pool group - close pool, key: %s", key)
 | 
			
		||||
		p.Close()
 | 
			
		||||
		logx.Infof("pool group - closing pool, key: %s", key)
 | 
			
		||||
		pg.createGroup.Forget(key)
 | 
			
		||||
		delete(pg.poolGroup, key)
 | 
			
		||||
		pg.mu.Unlock()
 | 
			
		||||
		pg.asyncClose(p, key)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	pg.mu.Unlock()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pg *PoolGroup[T]) CloseAll() {
 | 
			
		||||
	pg.mu.Lock()
 | 
			
		||||
	defer pg.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	for key := range pg.poolGroup {
 | 
			
		||||
		pg.poolGroup[key].Close()
 | 
			
		||||
		pg.createGroup.Forget(key)
 | 
			
		||||
	pools := make(map[string]Pool[T], len(pg.poolGroup))
 | 
			
		||||
	for k, v := range pg.poolGroup {
 | 
			
		||||
		pools[k] = v
 | 
			
		||||
	}
 | 
			
		||||
	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] {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ type Conn interface {
 | 
			
		||||
// Pool 连接池接口
 | 
			
		||||
type Pool[T Conn] interface {
 | 
			
		||||
	// 核心方法
 | 
			
		||||
	Get(ctx context.Context) (T, error)
 | 
			
		||||
	Get(ctx context.Context, opts ...GetOption) (T, error)
 | 
			
		||||
	Put(T) error
 | 
			
		||||
	Close()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -88,10 +88,10 @@ func newMockConn(id int) *mockConn {
 | 
			
		||||
 | 
			
		||||
func TestChanPool_Basic(t *testing.T) {
 | 
			
		||||
	var idGen int
 | 
			
		||||
	pool := NewChannelPool(func() (Conn, error) {
 | 
			
		||||
	pool := NewChannelPool(func() (*mockConn, error) {
 | 
			
		||||
		idGen++
 | 
			
		||||
		return newMockConn(idGen), nil
 | 
			
		||||
	}, WithMaxConns(2), WithIdleTimeout(time.Second))
 | 
			
		||||
	}, WithMaxConns[*mockConn](2), WithIdleTimeout[*mockConn](time.Second))
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	conn1, _ := pool.Get(ctx)
 | 
			
		||||
@@ -112,9 +112,9 @@ func TestChanPool_Basic(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestChanPool_WaitTimeout(t *testing.T) {
 | 
			
		||||
	pool := NewChannelPool(func() (Conn, error) {
 | 
			
		||||
	pool := NewChannelPool(func() (*mockConn, error) {
 | 
			
		||||
		return newMockConn(1), nil
 | 
			
		||||
	}, WithMaxConns(1), WithWaitTimeout(100*time.Millisecond))
 | 
			
		||||
	}, WithMaxConns[*mockConn](1), WithWaitTimeout[*mockConn](100*time.Millisecond))
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	conn1, _ := pool.Get(ctx)
 | 
			
		||||
@@ -132,9 +132,9 @@ func TestChanPool_WaitTimeout(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestChanPool_ContextCancel(t *testing.T) {
 | 
			
		||||
	pool := NewChannelPool(func() (Conn, error) {
 | 
			
		||||
	pool := NewChannelPool(func() (*mockConn, error) {
 | 
			
		||||
		return newMockConn(1), nil
 | 
			
		||||
	}, WithMaxConns(1))
 | 
			
		||||
	}, WithMaxConns[*mockConn](1))
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
	conn, _ := pool.Get(ctx)
 | 
			
		||||
@@ -145,9 +145,9 @@ func TestChanPool_ContextCancel(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestChanPool_Resize(t *testing.T) {
 | 
			
		||||
	pool := NewChannelPool(func() (Conn, error) {
 | 
			
		||||
	pool := NewChannelPool(func() (*mockConn, error) {
 | 
			
		||||
		return newMockConn(1), nil
 | 
			
		||||
	}, WithMaxConns(2))
 | 
			
		||||
	}, WithMaxConns[*mockConn](2))
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	conn1, _ := pool.Get(ctx)
 | 
			
		||||
	conn2, _ := pool.Get(ctx)
 | 
			
		||||
@@ -158,9 +158,9 @@ func TestChanPool_Resize(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestChanPool_HealthCheck(t *testing.T) {
 | 
			
		||||
	pool := NewChannelPool(func() (Conn, error) {
 | 
			
		||||
	pool := NewChannelPool(func() (*mockConn, error) {
 | 
			
		||||
		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()
 | 
			
		||||
	conn, _ := pool.Get(ctx)
 | 
			
		||||
	_ = pool.Put(conn)
 | 
			
		||||
@@ -176,26 +176,26 @@ func TestChanPool_HealthCheck(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
func TestCachePool_Basic(t *testing.T) {
 | 
			
		||||
	var idGen int
 | 
			
		||||
	pool := NewCachePool(func() (Conn, error) {
 | 
			
		||||
	pool := NewCachePool(func() (*mockConn, error) {
 | 
			
		||||
		idGen++
 | 
			
		||||
		return newMockConn(idGen), nil
 | 
			
		||||
	}, WithMaxConns(2), WithIdleTimeout(time.Second))
 | 
			
		||||
	}, WithMaxConns[*mockConn](2), WithIdleTimeout[*mockConn](time.Second))
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	conn1, _ := pool.Get(ctx)
 | 
			
		||||
	_ = pool.Put(conn1)
 | 
			
		||||
	conn2, _ := pool.Get(ctx)
 | 
			
		||||
	if conn1 != conn2 {
 | 
			
		||||
		t.Fatal("缓存池应复用同一连接")
 | 
			
		||||
	}
 | 
			
		||||
	_ = pool.Put(conn1)
 | 
			
		||||
	_ = pool.Put(conn2)
 | 
			
		||||
	pool.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCachePool_TimeoutCleanup(t *testing.T) {
 | 
			
		||||
	pool := NewCachePool(func() (Conn, error) {
 | 
			
		||||
	pool := NewCachePool(func() (*mockConn, error) {
 | 
			
		||||
		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()
 | 
			
		||||
	conn, _ := pool.Get(ctx)
 | 
			
		||||
	_ = pool.Put(conn)
 | 
			
		||||
@@ -209,10 +209,10 @@ func TestCachePool_TimeoutCleanup(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
func TestCachePool_OverMaxConns(t *testing.T) {
 | 
			
		||||
	var idGen int
 | 
			
		||||
	pool := NewCachePool(func() (Conn, error) {
 | 
			
		||||
	pool := NewCachePool(func() (*mockConn, error) {
 | 
			
		||||
		idGen++
 | 
			
		||||
		return newMockConn(idGen), nil
 | 
			
		||||
	}, WithMaxConns(1))
 | 
			
		||||
	}, WithMaxConns[*mockConn](1))
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	conn1, _ := pool.Get(ctx)
 | 
			
		||||
	_ = pool.Put(conn1)
 | 
			
		||||
@@ -231,9 +231,9 @@ func TestCachePool_OverMaxConns(t *testing.T) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCachePool_Resize(t *testing.T) {
 | 
			
		||||
	pool := NewCachePool(func() (Conn, error) {
 | 
			
		||||
	pool := NewCachePool(func() (*mockConn, error) {
 | 
			
		||||
		return newMockConn(1), nil
 | 
			
		||||
	}, WithMaxConns(2))
 | 
			
		||||
	}, WithMaxConns[*mockConn](2))
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	conn1, _ := pool.Get(ctx)
 | 
			
		||||
	_ = pool.Put(conn1)
 | 
			
		||||
@@ -288,9 +288,9 @@ func TestPoolGroup_Concurrent(t *testing.T) {
 | 
			
		||||
// ========== 压力测试 ==========
 | 
			
		||||
 | 
			
		||||
func BenchmarkChanPool_Concurrent(b *testing.B) {
 | 
			
		||||
	pool := NewChannelPool(func() (Conn, error) {
 | 
			
		||||
	pool := NewChannelPool(func() (*mockConn, error) {
 | 
			
		||||
		return newMockConn(1), nil
 | 
			
		||||
	}, WithMaxConns(100))
 | 
			
		||||
	}, WithMaxConns[*mockConn](100))
 | 
			
		||||
 | 
			
		||||
	b.ResetTimer()
 | 
			
		||||
	b.RunParallel(func(pb *testing.PB) {
 | 
			
		||||
@@ -307,9 +307,9 @@ func BenchmarkChanPool_Concurrent(b *testing.B) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BenchmarkCachePool_Concurrent(b *testing.B) {
 | 
			
		||||
	pool := NewCachePool(func() (Conn, error) {
 | 
			
		||||
	pool := NewCachePool(func() (*mockConn, error) {
 | 
			
		||||
		return newMockConn(1), nil
 | 
			
		||||
	}, WithMaxConns(100))
 | 
			
		||||
	}, WithMaxConns[*mockConn](100))
 | 
			
		||||
 | 
			
		||||
	b.ResetTimer()
 | 
			
		||||
	b.RunParallel(func(pb *testing.PB) {
 | 
			
		||||
@@ -332,9 +332,9 @@ func TestChanPool_Stress(t *testing.T) {
 | 
			
		||||
		iterations = 1000
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	pool := NewChannelPool(func() (Conn, error) {
 | 
			
		||||
	pool := NewChannelPool(func() (*mockConn, error) {
 | 
			
		||||
		return newMockConn(1), nil
 | 
			
		||||
	}, WithMaxConns(20), WithWaitTimeout(time.Second))
 | 
			
		||||
	}, WithMaxConns[*mockConn](20), WithWaitTimeout[*mockConn](time.Second))
 | 
			
		||||
 | 
			
		||||
	var wg sync.WaitGroup
 | 
			
		||||
	var errCount int32
 | 
			
		||||
@@ -389,9 +389,9 @@ func TestCachePool_Stress(t *testing.T) {
 | 
			
		||||
		iterations = 1000
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	pool := NewCachePool(func() (Conn, error) {
 | 
			
		||||
	pool := NewCachePool(func() (*mockConn, error) {
 | 
			
		||||
		return newMockConn(1), nil
 | 
			
		||||
	}, WithMaxConns(20), WithIdleTimeout(time.Minute))
 | 
			
		||||
	}, WithMaxConns[*mockConn](20), WithIdleTimeout[*mockConn](time.Minute))
 | 
			
		||||
 | 
			
		||||
	var wg sync.WaitGroup
 | 
			
		||||
	var errCount int32
 | 
			
		||||
@@ -430,11 +430,11 @@ func TestCachePool_Stress(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
// 测试连接池在连接失效时的行为
 | 
			
		||||
func TestChanPool_InvalidConn(t *testing.T) {
 | 
			
		||||
	pool := NewChannelPool(func() (Conn, error) {
 | 
			
		||||
	pool := NewChannelPool(func() (*mockConn, error) {
 | 
			
		||||
		conn := newMockConn(1)
 | 
			
		||||
		conn.pingErr = errors.New("connection invalid")
 | 
			
		||||
		return conn, nil
 | 
			
		||||
	}, WithMaxConns(1), WithHealthCheckInterval(10*time.Millisecond))
 | 
			
		||||
	}, WithMaxConns[*mockConn](1), WithHealthCheckInterval[*mockConn](10*time.Millisecond))
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	conn, _ := pool.Get(ctx)
 | 
			
		||||
@@ -458,9 +458,9 @@ func TestChanPool_InvalidConn(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
// 测试连接池在并发关闭时的行为
 | 
			
		||||
func TestChanPool_ConcurrentClose(t *testing.T) {
 | 
			
		||||
	pool := NewChannelPool(func() (Conn, error) {
 | 
			
		||||
	pool := NewChannelPool(func() (*mockConn, error) {
 | 
			
		||||
		return newMockConn(1), nil
 | 
			
		||||
	}, WithMaxConns(10))
 | 
			
		||||
	}, WithMaxConns[*mockConn](10))
 | 
			
		||||
 | 
			
		||||
	var wg sync.WaitGroup
 | 
			
		||||
	const goroutines = 10
 | 
			
		||||
@@ -564,6 +564,12 @@ func TestPoolGroup_ConcurrentAccess(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
 | 
			
		||||
	// 等待所有池关闭完成
 | 
			
		||||
	err := group.WaitForClose(10 * time.Second)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("等待池关闭超时: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 验证所有池都已关闭
 | 
			
		||||
	pools = group.AllPool()
 | 
			
		||||
	if len(pools) != 0 {
 | 
			
		||||
@@ -597,6 +603,12 @@ func TestPoolGroup_ConcurrentClose(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
 | 
			
		||||
	// 等待所有池关闭完成
 | 
			
		||||
	err := group.WaitForClose(10 * time.Second)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("等待池关闭超时: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 验证所有池都已关闭
 | 
			
		||||
	pools := group.AllPool()
 | 
			
		||||
	if len(pools) != 0 {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user