mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-04 00:10: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;
|
||||
}
|
||||
|
||||
@@ -4,5 +4,4 @@
|
||||
@use './media/media.scss';
|
||||
@use './waves.scss';
|
||||
@use './dark.scss';
|
||||
@use './iconSelector.scss';
|
||||
@use './splitpanes.scss';
|
||||
@use './iconSelector.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>
|
||||
<el-button @click="onCancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="onConfirm">{{ $t('common.confirm') }}</el-button>
|
||||
</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">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<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>
|
||||
</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,17 +84,22 @@ const handleClose = () => {
|
||||
};
|
||||
|
||||
const updateContent = async () => {
|
||||
await updateFileContent.request({
|
||||
content: state.content,
|
||||
id: props.fileId,
|
||||
path: props.path,
|
||||
machineId: props.machineId,
|
||||
authCertName: props.authCertName,
|
||||
protocol: props.protocol,
|
||||
});
|
||||
useI18nSaveSuccessMsg();
|
||||
handleClose();
|
||||
state.content = '';
|
||||
try {
|
||||
saveing.value = true;
|
||||
await updateFileContent.request({
|
||||
content: fileContent.value,
|
||||
id: props.fileId,
|
||||
path: props.path,
|
||||
machineId: props.machineId,
|
||||
authCertName: props.authCertName,
|
||||
protocol: props.protocol,
|
||||
});
|
||||
useI18nSaveSuccessMsg();
|
||||
handleClose();
|
||||
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 {
|
||||
m.addCronJob(mcj)
|
||||
add++
|
||||
}
|
||||
if add >= int(total) {
|
||||
return
|
||||
}
|
||||
|
||||
pageParam.PageNum = pageParam.PageNum + 1
|
||||
m.GetPageList(cond, pageParam)
|
||||
if err := m.CursorByCond(&entity.MachineCronJob{Status: entity.MachineCronJobStatusEnable}, func(mcj *entity.MachineCronJob) error {
|
||||
m.addCronJob(mcj)
|
||||
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,7 +8,8 @@ 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:实时交互]
|
||||
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:脚本内容"` // 脚本内容
|
||||
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 {
|
||||
entry.lastActive = time.Now()
|
||||
// 再次检查是否已创建(防止并发)
|
||||
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,13 +226,49 @@ 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()
|
||||
delete(p.cache, key)
|
||||
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
|
||||
}
|
||||
|
||||
// 生成缓存键
|
||||
func generateCacheKey() string {
|
||||
return stringx.RandUUID()
|
||||
|
||||
@@ -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)
|
||||
wrapper.lastActive = time.Now()
|
||||
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