feat: 支持关联多标签、计划任务立即执行、标签相关操作优化

This commit is contained in:
meilin.huang
2023-12-05 23:03:51 +08:00
parent b347bd7ef5
commit 57361d8241
107 changed files with 1819 additions and 825 deletions

View File

@@ -30,7 +30,7 @@
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"sql-formatter": "^14.0.0", "sql-formatter": "^14.0.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vue": "^3.3.9", "vue": "^3.3.10",
"vue-router": "^4.2.5", "vue-router": "^4.2.5",
"xterm": "^5.3.0", "xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0", "xterm-addon-fit": "^0.8.0",
@@ -52,7 +52,7 @@
"prettier": "^3.0.3", "prettier": "^3.0.3",
"sass": "^1.69.0", "sass": "^1.69.0",
"typescript": "^5.3.2", "typescript": "^5.3.2",
"vite": "^5.0.3", "vite": "^5.0.5",
"vue-eslint-parser": "^9.3.1" "vue-eslint-parser": "^9.3.1"
}, },
"browserslist": [ "browserslist": [

View File

@@ -0,0 +1,9 @@
import EnumValue from './Enum';
// 标签关联的资源类型
export const TagResourceTypeEnum = {
Machine: EnumValue.of(1, '机器'),
Db: EnumValue.of(2, '数据库'),
Redis: EnumValue.of(3, 'redis'),
Mongo: EnumValue.of(4, 'mongo'),
};

View File

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

View File

@@ -40,3 +40,7 @@ export const NextLoading = {
}); });
}, },
}; };
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -265,7 +265,7 @@ const state = reactive({
tableMaxHeight: window.innerHeight - 240 + 'px', tableMaxHeight: window.innerHeight - 240 + 'px',
}); });
const { pageSizes, isOpenMoreQuery, defaultQueryCount, queryForm_, inputWidth_, loadingData, tableMaxHeight } = toRefs(state); const { pageSizes, isOpenMoreQuery, defaultQueryCount, queryForm_, inputWidth_, formatVal, loadingData, tableMaxHeight } = toRefs(state);
watch( watch(
() => props.queryForm, () => props.queryForm,
@@ -335,6 +335,15 @@ const calcuTableHeight = () => {
state.tableMaxHeight = window.innerHeight - 240 + 'px'; state.tableMaxHeight = window.innerHeight - 240 + 'px';
}; };
const formatText = (data: any) => {
state.formatVal = '';
try {
state.formatVal = JSON.stringify(JSON.parse(data), null, 4);
} catch (e) {
state.formatVal = data;
}
};
const getRowQueryItem = (row: number) => { const getRowQueryItem = (row: number) => {
// 第一行需要加个查询等按钮列 // 第一行需要加个查询等按钮列
if (row === 1) { if (row === 1) {

View File

@@ -0,0 +1,45 @@
<template>
<div style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer; vertical-align: middle">
<el-popover :show-after="500" @show="getTags" placement="top-start" width="230" trigger="hover">
<template #reference>
<div>
<!-- <el-button type="primary" link size="small">标签</el-button> -->
<SvgIcon name="view" :size="16" color="var(--el-color-primary)" />
</div>
</template>
<el-tag effect="plain" v-for="tag in tags" :key="tag" class="ml5" type="success" size="small">{{ tag.tagPath }}</el-tag>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { reactive, toRefs } from 'vue';
import { tagApi } from '../tag/api';
import SvgIcon from '@/components/svgIcon/index.vue';
const props = defineProps({
resourceCode: {
type: [String],
required: true,
},
resourceType: {
type: [Number],
required: true,
},
});
const state = reactive({
tags: [] as any,
});
const { tags } = toRefs(state);
const getTags = async () => {
state.tags = await tagApi.getTagResources.request({
resourceCode: props.resourceCode,
resourceType: props.resourceType,
});
};
</script>
<style lang="scss"></style>

View File

@@ -43,17 +43,22 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, reactive, ref, watch, toRefs, onUnmounted } from 'vue'; import { onMounted, reactive, ref, watch, toRefs, onUnmounted } from 'vue';
import { TagTreeNode } from './tag'; import { NodeType, TagTreeNode } from './tag';
import TagInfo from './TagInfo.vue'; import TagInfo from './TagInfo.vue';
import { Contextmenu } from '@/components/contextmenu'; import { Contextmenu } from '@/components/contextmenu';
import { useViewport } from '@/common/use'; import { useViewport } from '@/common/use';
import { tagApi } from '../tag/api';
const props = defineProps({ const props = defineProps({
load: { resourceType: {
type: Function, type: [Number],
required: false, required: true,
}, },
loadTags: { tagPathNodeType: {
type: [NodeType],
required: true,
},
load: {
type: Function, type: Function,
required: false, required: false,
}, },
@@ -109,6 +114,18 @@ const filterNode = (value: string, data: any) => {
return data.label.includes(value); return data.label.includes(value);
}; };
/**
* 加载标签树节点
*/
const loadTags = async () => {
const tags = await tagApi.getResourceTagPaths.request({ resourceType: props.resourceType });
const tagNodes = [];
for (let tagPath of tags) {
tagNodes.push(new TagTreeNode(tagPath, tagPath, props.tagPathNodeType));
}
return tagNodes;
};
/** /**
* 加载树节点 * 加载树节点
* @param { Object } node * @param { Object } node
@@ -120,8 +137,8 @@ const loadNode = async (node: any, resolve: any) => {
} }
let nodes = []; let nodes = [];
try { try {
if (node.level == 0 && props.loadTags) { if (node.level == 0) {
nodes = await props.loadTags(node); nodes = await loadTags();
} else if (props.load) { } else if (props.load) {
nodes = await props.load(node); nodes = await props.load(node);
} else { } else {

View File

@@ -2,14 +2,14 @@
<div> <div>
<el-tree-select <el-tree-select
v-bind="$attrs" v-bind="$attrs"
@check="changeTag" v-model="selectTags"
@change="changeTag"
style="width: 100%" style="width: 100%"
:data="tags" :data="tags"
placeholder="请选择关联标签" placeholder="请选择关联标签"
:render-after-expand="true" :render-after-expand="true"
:default-expanded-keys="[selectTags]" :default-expanded-keys="[selectTags]"
show-checkbox show-checkbox
check-strictly
node-key="id" node-key="id"
:props="{ :props="{
value: 'id', value: 'id',
@@ -33,35 +33,46 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useAttrs, toRefs, reactive, onMounted } from 'vue'; import { toRefs, reactive, onMounted } from 'vue';
import { tagApi } from '../tag/api'; import { tagApi } from '../tag/api';
const attrs = useAttrs();
// //
const emit = defineEmits(['changeTag', 'update:tagPath']); const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
const props = defineProps({
resourceCode: {
type: [String],
required: true,
},
resourceType: {
type: [Number],
required: true,
},
});
const state = reactive({ const state = reactive({
tags: [], tags: [],
// idid // idid
selectTags: null as any, selectTags: [],
}); });
const { tags, selectTags } = toRefs(state); const { tags, selectTags } = toRefs(state);
onMounted(async () => { onMounted(async () => {
if (attrs.modelValue) { if (props.resourceCode) {
state.selectTags = attrs.modelValue; const resourceTags = await tagApi.getTagResources.request({
resourceCode: props.resourceCode,
resourceType: props.resourceType,
});
state.selectTags = resourceTags.map((x: any) => x.tagId);
changeTag();
} }
state.tags = await tagApi.getTagTrees.request(null); state.tags = await tagApi.getTagTrees.request(null);
}); });
const changeTag = (tag: any, checkInfo: any) => { const changeTag = () => {
if (checkInfo.checkedNodes.length > 0) { emit('changeTag', state.selectTags);
emit('update:tagPath', tag.codePath);
emit('changeTag', tag);
} else {
emit('update:tagPath', null);
}
}; };
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -10,8 +10,19 @@
width="38%" width="38%"
> >
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto"> <el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-form-item prop="tagId" label="标签" required> <el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" /> <tag-tree-select
@change-tag="
(tagIds) => {
form.tagId = tagIds;
tagSelectRef.validate();
}
"
multiple
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Db.value"
style="width: 100%"
/>
</el-form-item> </el-form-item>
<el-form-item prop="instanceId" label="数据库实例" required> <el-form-item prop="instanceId" label="数据库实例" required>
@@ -77,7 +88,8 @@
import { toRefs, reactive, watch, ref } from 'vue'; import { toRefs, reactive, watch, ref } from 'vue';
import { dbApi } from './api'; import { dbApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import TagSelect from '../component/TagSelect.vue'; import TagTreeSelect from '../component/TagTreeSelect.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({ const props = defineProps({
visible: { visible: {
@@ -128,6 +140,7 @@ const rules = {
}; };
const dbForm: any = ref(null); const dbForm: any = ref(null);
const tagSelectRef: any = ref(null);
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
@@ -135,9 +148,9 @@ const state = reactive({
databaseList: [] as any, databaseList: [] as any,
form: { form: {
id: null, id: null,
tagId: null as any, tagId: [],
tagPath: null as any,
name: null, name: null,
code: '',
database: '', database: '',
remark: '', remark: '',
instanceId: null as any, instanceId: null as any,
@@ -148,13 +161,14 @@ const state = reactive({
const { dialogVisible, allDatabases, databaseList, form, btnLoading } = toRefs(state); const { dialogVisible, allDatabases, databaseList, form, btnLoading } = toRefs(state);
watch(props, (newValue: any) => { watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;
if (!state.dialogVisible) { if (!state.dialogVisible) {
return; return;
} }
if (newValue.db) { if (newValue.db) {
state.form = { ...newValue.db }; state.form = { ...newValue.db };
// 将数据库名使用空格切割,获取所有数据库列表 // 将数据库名使用空格切割,获取所有数据库列表
state.databaseList = newValue.db.database.split(' '); state.databaseList = newValue.db.database.split(' ');
} else { } else {

View File

@@ -46,10 +46,7 @@
</template> </template>
<template #tagPath="{ data }"> <template #tagPath="{ data }">
<tag-info :tag-path="data.tagPath" /> <resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Db.value" />
<span class="ml5">
{{ data.tagPath }}
</span>
</template> </template>
<template #host="{ data }"> <template #host="{ data }">
@@ -167,12 +164,15 @@ import config from '@/common/config';
import { joinClientParams } from '@/common/request'; import { joinClientParams } from '@/common/request';
import { isTrue } from '@/common/assert'; import { isTrue } from '@/common/assert';
import { dateFormat } from '@/common/utils/date'; import { dateFormat } from '@/common/utils/date';
import TagInfo from '../component/TagInfo.vue'; import ResourceTag from '../component/ResourceTag.vue';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable'; import { TableColumn, TableQuery } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth'; import { hasPerms } from '@/components/auth/auth';
import DbSqlExecLog from './DbSqlExecLog.vue'; import DbSqlExecLog from './DbSqlExecLog.vue';
import { DbType } from './dialect'; import { DbType } from './dialect';
import { tagApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { useRoute } from 'vue-router';
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue')); const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
@@ -185,12 +185,12 @@ const perms = {
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), TableQuery.slot('instanceId', '实例', 'instanceSelect')]; const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), TableQuery.slot('instanceId', '实例', 'instanceSelect')];
const columns = ref([ const columns = ref([
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
TableColumn.new('instanceName', '实例名'), TableColumn.new('instanceName', '实例名'),
TableColumn.new('type', '类型'), TableColumn.new('type', '类型'),
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40), TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
TableColumn.new('username', 'username'), TableColumn.new('username', 'username'),
TableColumn.new('name', '名称'), TableColumn.new('name', '名称'),
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
TableColumn.new('remark', '备注'), TableColumn.new('remark', '备注'),
]); ]);
@@ -198,6 +198,7 @@ const columns = ref([
const actionBtns = hasPerms([perms.base, perms.saveDb]); const actionBtns = hasPerms([perms.base, perms.saveDb]);
const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(); const actionColumn = TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter();
const route = useRoute();
const pageTableRef: any = ref(null); const pageTableRef: any = ref(null);
const state = reactive({ const state = reactive({
@@ -214,7 +215,7 @@ const state = reactive({
* 查询条件 * 查询条件
*/ */
query: { query: {
tagPath: null, tagPath: '',
instanceId: null, instanceId: null,
pageNum: 1, pageNum: 1,
pageSize: 0, pageSize: 0,
@@ -269,6 +270,10 @@ onMounted(async () => {
const search = async () => { const search = async () => {
try { try {
pageTableRef.value.loading(true); pageTableRef.value.loading(true);
if (route.query.tagPath) {
state.query.tagPath = route.query.tagPath as string;
}
let res: any = await dbApi.dbs.request(state.query); let res: any = await dbApi.dbs.request(state.query);
// 切割数据库 // 切割数据库
res.list?.forEach((e: any) => { res.list?.forEach((e: any) => {
@@ -297,7 +302,7 @@ const onBeforeCloseInfoDialog = () => {
}; };
const getTags = async () => { const getTags = async () => {
state.tags = await dbApi.dbTags.request(null); state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Db.value });
}; };
const getInstances = async (instanceName = '') => { const getInstances = async (instanceName = '') => {

View File

@@ -2,7 +2,7 @@
<div class="db-sql-exec"> <div class="db-sql-exec">
<el-row> <el-row>
<el-col :span="5"> <el-col :span="5">
<tag-tree ref="tagTreeRef" :loadTags="loadTags"> <tag-tree :resource-type="TagResourceTypeEnum.Db.value" :tag-path-node-type="NodeTypeTagPath" ref="tagTreeRef">
<template #prefix="{ data }"> <template #prefix="{ data }">
<span v-if="data.type.value == SqlExecNodeType.DbInst"> <span v-if="data.type.value == SqlExecNodeType.DbInst">
<el-popover :show-after="500" placement="right-start" title="数据库实例信息" trigger="hover" :width="250"> <el-popover :show-after="500" placement="right-start" title="数据库实例信息" trigger="hover" :width="250">
@@ -160,6 +160,8 @@ import { dispposeCompletionItemProvider } from '@/components/monaco/completionIt
import SvgIcon from '@/components/svgIcon/index.vue'; import SvgIcon from '@/components/svgIcon/index.vue';
import { ContextmenuItem } from '@/components/contextmenu'; import { ContextmenuItem } from '@/components/contextmenu';
import { getDbDialect } from './dialect/index'; import { getDbDialect } from './dialect/index';
import { sleep } from '@/common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue')); const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue'));
const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue')); const DbTableDataOp = defineAsyncComponent(() => import('./component/table/DbTableDataOp.vue'));
@@ -213,11 +215,16 @@ const nodeClickChangeDb = (nodeData: TagTreeNode) => {
// tagpath 节点类型 // tagpath 节点类型
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => { const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const dbInfos = instMap.get(parentNode.key); const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
const dbInfos = dbInfoRes.list;
if (!dbInfos) { if (!dbInfos) {
return []; return [];
} }
// 防止过快加载会出现一闪而过,对眼睛不好
await sleep(100);
return dbInfos?.map((x: any) => { return dbInfos?.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x); return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
}); });
}); });
@@ -396,35 +403,6 @@ const setHeight = () => {
state.tablesOpHeight = window.innerHeight - 220 + 'px'; state.tablesOpHeight = window.innerHeight - 220 + 'px';
}; };
/**
* instmap; tagPaht -> info[]
*/
const instMap: Map<string, any[]> = new Map();
const getInsts = async () => {
const res = await dbApi.dbs.request({ pageNum: 1, pageSize: 1000 });
if (!res.total) return;
for (const db of res.list) {
const tagPath = db.tagPath;
let dbInsts = instMap.get(tagPath) || [];
dbInsts.push(db);
instMap.set(tagPath, dbInsts?.sort());
}
};
/**
* 加载标签树节点
*/
const loadTags = async () => {
await getInsts();
const tagPaths = instMap.keys();
const tagNodes = [];
for (let tagPath of tagPaths) {
tagNodes.push(new TagTreeNode(tagPath, tagPath, NodeTypeTagPath));
}
return tagNodes;
};
// 选择数据库,改变当前正在操作的数据库信息 // 选择数据库,改变当前正在操作的数据库信息
const changeDb = (db: any, dbName: string) => { const changeDb = (db: any, dbName: string) => {
state.nowDbInst = DbInst.getOrNewInst(db); state.nowDbInst = DbInst.getOrNewInst(db);

View File

@@ -71,7 +71,7 @@
:ref="(el: any) => el?.focus()" :ref="(el: any) => el?.focus()"
@blur="onExitEditMode(rowData, column, rowIndex)" @blur="onExitEditMode(rowData, column, rowIndex)"
class="w100" class="w100"
input-style="text-align: center; height: 27px;" input-style="text-align: center; height: 26px;"
size="small" size="small"
v-model="rowData[column.dataKey!]" v-model="rowData[column.dataKey!]"
></el-input> ></el-input>

View File

@@ -4,8 +4,19 @@
<el-form :model="form" ref="machineForm" :rules="rules" label-width="auto"> <el-form :model="form" ref="machineForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName"> <el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic"> <el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签"> <el-form-item ref="tagSelectRef" prop="tagId" label="标签">
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" /> <tag-tree-select
multiple
@change-tag="
(tagIds) => {
form.tagId = tagIds;
tagSelectRef.validate();
}
"
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Machine.value"
style="width: 100%"
/>
</el-form-item> </el-form-item>
<el-form-item prop="name" label="名称" required> <el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input> <el-input v-model.trim="form.name" placeholder="请输入机器别名" auto-complete="off"></el-input>
@@ -71,9 +82,10 @@
import { toRefs, reactive, watch, ref } from 'vue'; import { toRefs, reactive, watch, ref } from 'vue';
import { machineApi } from './api'; import { machineApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import TagSelect from '../component/TagSelect.vue'; import TagTreeSelect from '../component/TagTreeSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue'; import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import AuthCertSelect from './authcert/AuthCertSelect.vue'; import AuthCertSelect from './authcert/AuthCertSelect.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({ const props = defineProps({
visible: { visible: {
@@ -95,7 +107,7 @@ const rules = {
{ {
required: true, required: true,
message: '请选择标签', message: '请选择标签',
trigger: ['blur', 'change'], trigger: ['change'],
}, },
], ],
name: [ name: [
@@ -126,17 +138,11 @@ const rules = {
trigger: ['change', 'blur'], trigger: ['change', 'blur'],
}, },
], ],
password: [
{
required: true,
message: '请输入授权密码',
trigger: ['change', 'blur'],
},
],
}; };
const machineForm: any = ref(null); const machineForm: any = ref(null);
const authCertSelectRef: any = ref(null); const authCertSelectRef: any = ref(null);
const tagSelectRef: any = ref(null);
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
@@ -146,14 +152,14 @@ const state = reactive({
authType: 1, authType: 1,
form: { form: {
id: null, id: null,
code: '',
ip: null, ip: null,
port: 22, port: 22,
name: null, name: null,
authCertId: null as any, authCertId: null as any,
username: '', username: '',
password: '', password: '',
tagId: null as any, tagId: [],
tagPath: null as any,
remark: '', remark: '',
sshTunnelMachineId: null as any, sshTunnelMachineId: null as any,
enableRecorder: -1, enableRecorder: -1,
@@ -173,6 +179,7 @@ watch(props, async (newValue: any) => {
state.tabActiveName = 'basic'; state.tabActiveName = 'basic';
if (newValue.machine) { if (newValue.machine) {
state.form = { ...newValue.machine }; state.form = { ...newValue.machine };
// 如果凭证类型为公共的,则表示使用授权凭证认证 // 如果凭证类型为公共的,则表示使用授权凭证认证
const authCertId = (state.form as any).authCertId; const authCertId = (state.form as any).authCertId;
if (authCertId > 0) { if (authCertId > 0) {
@@ -181,7 +188,7 @@ watch(props, async (newValue: any) => {
state.authType = 1; state.authType = 1;
} }
} else { } else {
state.form = { port: 22 } as any; state.form = { port: 22, tagId: [] } as any;
state.authType = 1; state.authType = 1;
} }
}); });

View File

@@ -24,13 +24,6 @@
<el-button v-auth="perms.delMachine" :disabled="selectionData.length < 1" @click="deleteMachine()" type="danger" icon="delete">删除</el-button> <el-button v-auth="perms.delMachine" :disabled="selectionData.length < 1" @click="deleteMachine()" type="danger" icon="delete">删除</el-button>
</template> </template>
<template #tagPath="{ data }">
<tag-info :tag-path="data.tagPath" />
<span class="ml5">
{{ data.tagPath }}
</span>
</template>
<template #ipPort="{ data }"> <template #ipPort="{ data }">
<el-link :disabled="data.status == -1" @click="showMachineStats(data)" type="primary" :underline="false"> <el-link :disabled="data.status == -1" @click="showMachineStats(data)" type="primary" :underline="false">
{{ `${data.ip}:${data.port}` }} {{ `${data.ip}:${data.port}` }}
@@ -82,6 +75,10 @@
></el-switch> ></el-switch>
</template> </template>
<template #tagPath="{ data }">
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Machine.value" />
</template>
<template #action="{ data }"> <template #action="{ data }">
<span v-auth="'machine:terminal'"> <span v-auth="'machine:terminal'">
<el-tooltip :show-after="500" content="按住ctrl则为新标签打开" placement="top"> <el-tooltip :show-after="500" content="按住ctrl则为新标签打开" placement="top">
@@ -190,15 +187,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, toRefs, reactive, onMounted, defineAsyncComponent } from 'vue'; import { ref, toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { machineApi, getMachineTerminalSocketUrl } from './api'; import { machineApi, getMachineTerminalSocketUrl } from './api';
import { dateFormat } from '@/common/utils/date'; import { dateFormat } from '@/common/utils/date';
import TagInfo from '../component/TagInfo.vue'; import ResourceTag from '../component/ResourceTag.vue';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable'; import { TableColumn, TableQuery } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth'; import { hasPerms } from '@/components/auth/auth';
import { formatByteSize } from '@/common/utils/format'; import { formatByteSize } from '@/common/utils/format';
import { tagApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
// 组件 // 组件
const TerminalDialog = defineAsyncComponent(() => import('@/components/terminal/TerminalDialog.vue')); const TerminalDialog = defineAsyncComponent(() => import('@/components/terminal/TerminalDialog.vue'));
@@ -210,6 +209,7 @@ const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue')); const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
const router = useRouter(); const router = useRouter();
const route = useRoute();
const pageTableRef: any = ref(null); const pageTableRef: any = ref(null);
const terminalDialogRef: any = ref(null); const terminalDialogRef: any = ref(null);
@@ -224,13 +224,13 @@ const perms = {
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), TableQuery.text('ip', 'IP'), TableQuery.text('name', '名称')]; const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect'), TableQuery.text('ip', 'IP'), TableQuery.text('name', '名称')];
const columns = ref([ const columns = ref([
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
TableColumn.new('name', '名称'), TableColumn.new('name', '名称'),
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50), TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(50), TableColumn.new('stat', '运行状态').isSlot().setAddWidth(50),
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(20), TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(20),
TableColumn.new('username', '用户名'), TableColumn.new('username', '用户名'),
TableColumn.new('status', '状态').isSlot().setMinWidth(85), TableColumn.new('status', '状态').isSlot().setMinWidth(85),
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
TableColumn.new('remark', '备注'), TableColumn.new('remark', '备注'),
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(), TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
]); ]);
@@ -245,7 +245,7 @@ const state = reactive({
pageSize: 0, pageSize: 0,
ip: null, ip: null,
name: null, name: null,
tagPath: null, tagPath: '',
}, },
// 列表数据 // 列表数据
data: { data: {
@@ -360,7 +360,7 @@ const closeCli = async (row: any) => {
}; };
const getTags = async () => { const getTags = async () => {
state.tags = await machineApi.tagList.request(null); state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Machine.value });
}; };
const openFormDialog = async (machine: any) => { const openFormDialog = async (machine: any) => {
@@ -434,6 +434,9 @@ const showFileManage = (selectionData: any) => {
const search = async () => { const search = async () => {
try { try {
pageTableRef.value.loading(true); pageTableRef.value.loading(true);
if (route.query.tagPath) {
state.params.tagPath = route.query.tagPath as string;
}
const res = await machineApi.list.request(state.params); const res = await machineApi.list.request(state.params);
state.data = res; state.data = res;
} finally { } finally {

View File

@@ -7,31 +7,44 @@
:before-close="handleClose" :before-close="handleClose"
:close-on-click-modal="false" :close-on-click-modal="false"
:destroy-on-close="true" :destroy-on-close="true"
width="800"
>
<page-table
height="100%"
v-model:query-form="query"
:data="data"
:columns="columns"
:total="total"
v-model:page-size="query.pageSize"
v-model:page-num="query.pageNum"
@pageChange="getTermOps()"
>
<template #action="{ data }">
<el-button @click="playRec(data)" loading-icon="loading" :loading="data.playRecLoding" type="primary" link>回放</el-button>
</template>
</page-table>
</el-dialog>
<el-dialog
:title="title"
v-model="playerDialogVisible"
:before-close="handleClosePlayer"
:close-on-click-modal="false"
:destroy-on-close="true"
width="70%" width="70%"
> >
<div class="toolbar">
<el-select @change="getUsers" v-model="operateDate" placeholder="操作日期" filterable>
<el-option v-for="item in operateDates" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-select class="ml10" @change="getRecs" filterable v-model="user" placeholder="请选择操作人">
<el-option v-for="item in users" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-select class="ml10" @change="playRec" filterable v-model="rec" placeholder="请选择操作记录">
<el-option v-for="item in recs" :key="item" :label="item" :value="item"> </el-option>
</el-select>
<el-divider direction="vertical" border-style="dashed" />
快捷键-> space[空格键]: 暂停/播放
</div>
<div ref="playerRef" id="rc-player"></div> <div ref="playerRef" id="rc-player"></div>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { toRefs, watch, ref, reactive } from 'vue'; import { toRefs, watch, ref, reactive, nextTick } from 'vue';
import { machineApi } from './api'; import { machineApi } from './api';
import * as AsciinemaPlayer from 'asciinema-player'; import * as AsciinemaPlayer from 'asciinema-player';
import 'asciinema-player/dist/bundle/asciinema-player.css'; import 'asciinema-player/dist/bundle/asciinema-player.css';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
const props = defineProps({ const props = defineProps({
visible: { type: Boolean }, visible: { type: Boolean },
@@ -41,67 +54,75 @@ const props = defineProps({
const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']); const emit = defineEmits(['update:visible', 'cancel', 'update:machineId']);
const columns = [
TableColumn.new('creator', '操作者').setMinWidth(120),
TableColumn.new('createTime', '开始时间').isTime().setMinWidth(150),
TableColumn.new('endTime', '结束时间').isTime().setMinWidth(150),
TableColumn.new('recordFilePath', '文件路径').setMinWidth(200),
TableColumn.new('action', '操作').isSlot().setMinWidth(60).fixedRight().alignCenter(),
];
const playerRef = ref(null); const playerRef = ref(null);
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
title: '', title: '',
machineId: 0, machineId: 0,
operateDates: [], data: [],
users: [], total: 0,
recs: [], query: {
operateDate: '', pageNum: 1,
user: '', pageSize: 10,
rec: '', },
playerDialogVisible: false,
}); });
const { dialogVisible, title, operateDates, operateDate, users, recs, user, rec } = toRefs(state); const { dialogVisible, query, data, total, playerDialogVisible } = toRefs(state);
watch(props, async (newValue: any) => { watch(props, async (newValue: any) => {
const visible = newValue.visible; const visible = newValue.visible;
if (visible) { if (visible) {
state.machineId = newValue.machineId; state.machineId = newValue.machineId;
state.title = newValue.title; state.title = newValue.title;
await getOperateDate(); await getTermOps();
} }
state.dialogVisible = visible; state.dialogVisible = visible;
}); });
const getOperateDate = async () => { const getTermOps = async () => {
const res = await machineApi.recDirNames.request({ path: state.machineId }); const res = await machineApi.termOpRecs.request({ id: state.machineId, ...state.query });
state.operateDates = res as any; state.data = res.list;
}; state.total = res.total;
const getUsers = async (operateDate: string) => {
state.users = [];
state.user = '';
state.recs = [];
state.rec = '';
const res = await machineApi.recDirNames.request({ path: `${state.machineId}/${operateDate}` });
state.users = res as any;
};
const getRecs = async (user: string) => {
state.recs = [];
state.rec = '';
const res = await machineApi.recDirNames.request({ path: `${state.machineId}/${state.operateDate}/${user}` });
state.recs = res as any;
}; };
let player: any = null; let player: any = null;
const playRec = async (rec: string) => { const playRec = async (rec: any) => {
if (player) { try {
player.dispose(); if (player) {
player.dispose();
}
rec.playRecLoding = true;
const content = await machineApi.termOpRec.request({
recId: rec.id,
id: rec.machineId,
});
state.playerDialogVisible = true;
nextTick(() => {
player = AsciinemaPlayer.create(`data:text/plain;base64,${content}`, playerRef.value, {
autoPlay: true,
speed: 1.0,
idleTimeLimit: 2,
});
});
} finally {
rec.playRecLoding = false;
} }
const content = await machineApi.recDirNames.request({ };
isFile: '1',
path: `${state.machineId}/${state.operateDate}/${state.user}/${rec}`, const handleClosePlayer = () => {
}); state.playerDialogVisible = false;
player = AsciinemaPlayer.create(`data:text/plain;base64,${content}`, playerRef.value, {
autoPlay: true,
speed: 1.0,
idleTimeLimit: 2,
});
}; };
/** /**
@@ -111,12 +132,8 @@ const handleClose = () => {
emit('update:visible', false); emit('update:visible', false);
emit('update:machineId', null); emit('update:machineId', null);
emit('cancel'); emit('cancel');
state.operateDates = []; state.data = [];
state.users = []; state.total = 0;
state.recs = [];
state.operateDate = '';
state.user = '';
state.rec = '';
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -124,5 +141,15 @@ const handleClose = () => {
.el-overlay .el-overlay-dialog .el-dialog .el-dialog__body { .el-overlay .el-overlay-dialog .el-dialog .el-dialog__body {
padding: 0px !important; padding: 0px !important;
} }
#rc-player {
.ap-terminal {
font-size: 14px !important;
}
.ap-player {
height: 550px !important;
}
}
} }
</style> </style>

View File

@@ -43,7 +43,10 @@ export const machineApi = {
// 删除配置的文件or目录 // 删除配置的文件or目录
delConf: Api.newDelete('/machines/{machineId}/files/{id}'), delConf: Api.newDelete('/machines/{machineId}/files/{id}'),
terminal: Api.newGet('/api/machines/{id}/terminal'), terminal: Api.newGet('/api/machines/{id}/terminal'),
recDirNames: Api.newGet('/machines/rec/names'), // 机器终端操作记录列表
termOpRecs: Api.newGet('/machines/{id}/term-recs'),
// 机器终端操作记录详情
termOpRec: Api.newGet('/machines/{id}/term-recs/{recId}'),
}; };
export const authCertApi = { export const authCertApi = {
@@ -59,6 +62,7 @@ export const cronJobApi = {
relateCronJobIds: Api.newGet('/machine-cronjobs/cronjob-ids'), relateCronJobIds: Api.newGet('/machine-cronjobs/cronjob-ids'),
save: Api.newPost('/machine-cronjobs'), save: Api.newPost('/machine-cronjobs'),
delete: Api.newDelete('/machine-cronjobs/{id}'), delete: Api.newDelete('/machine-cronjobs/{id}'),
run: Api.newPost('/machine-cronjobs/run/{key}'),
execList: Api.newGet('/machine-cronjobs/execs'), execList: Api.newGet('/machine-cronjobs/execs'),
}; };

View File

@@ -13,9 +13,9 @@
ref="pageTableRef" ref="pageTableRef"
:query="queryConfig" :query="queryConfig"
v-model:query-form="params" v-model:query-form="params"
:data="data.list" :data="state.data.list"
:columns="columns" :columns="columns"
:total="data.total" :total="state.data.total"
v-model:page-size="params.pageSize" v-model:page-size="params.pageSize"
v-model:page-num="params.pageNum" v-model:page-num="params.pageNum"
@pageChange="search()" @pageChange="search()"
@@ -88,7 +88,7 @@ const state = reactive({
const machineMap: Map<number, any> = new Map(); const machineMap: Map<number, any> = new Map();
const { dialogVisible, params, data } = toRefs(state); const { dialogVisible, params } = toRefs(state);
watch(props, async (newValue: any) => { watch(props, async (newValue: any) => {
state.dialogVisible = newValue.visible; state.dialogVisible = newValue.visible;

View File

@@ -24,6 +24,9 @@
</template> </template>
<template #action="{ data }"> <template #action="{ data }">
<el-button :disabled="data.status == CronJobStatusEnum.Disable.value" v-auth="perms.saveCronJob" type="primary" @click="runCronJob(data)" link
>执行</el-button
>
<el-button v-auth="perms.saveCronJob" type="primary" @click="openFormDialog(data)" link>编辑</el-button> <el-button v-auth="perms.saveCronJob" type="primary" @click="openFormDialog(data)" link>编辑</el-button>
<el-button type="primary" @click="showExec(data)" link>执行记录</el-button> <el-button type="primary" @click="showExec(data)" link>执行记录</el-button>
</template> </template>
@@ -111,6 +114,11 @@ const openFormDialog = async (data: any) => {
state.cronJobEdit.visible = true; state.cronJobEdit.visible = true;
}; };
const runCronJob = async (data: any) => {
await cronJobApi.run.request({ key: data.key });
ElMessage.success('执行成功');
};
const deleteCronJob = async () => { const deleteCronJob = async () => {
try { try {
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】计划任务信息? 该操作将同时删除执行记录`, '提示', { await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】计划任务信息? 该操作将同时删除执行记录`, '提示', {

View File

@@ -2,7 +2,7 @@
<div> <div>
<el-row> <el-row>
<el-col :span="5"> <el-col :span="5">
<tag-tree :loadTags="loadTags"> <tag-tree :resource-type="TagResourceTypeEnum.Mongo.value" :tag-path-node-type="NodeTypeTagPath">
<template #prefix="{ data }"> <template #prefix="{ data }">
<span v-if="data.type.value == MongoNodeType.Mongo"> <span v-if="data.type.value == MongoNodeType.Mongo">
<el-popover :show-after="500" placement="right-start" title="mongo实例信息" trigger="hover" :width="250"> <el-popover :show-after="500" placement="right-start" title="mongo实例信息" trigger="hover" :width="250">
@@ -172,6 +172,8 @@ import { isTrue, notBlank } from '@/common/assert';
import { TagTreeNode, NodeType } from '../component/tag'; import { TagTreeNode, NodeType } from '../component/tag';
import TagTree from '../component/TagTree.vue'; import TagTree from '../component/TagTree.vue';
import { formatByteSize } from '@/common/utils/format'; import { formatByteSize } from '@/common/utils/format';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { sleep } from '@/common/utils/loading';
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue')); const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
@@ -192,12 +194,15 @@ class MongoNodeType {
// tagpath 节点类型 // tagpath 节点类型
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => { const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
// 点击标签 -> 显示mongo信息列表 const res = await mongoApi.mongoList.request({ tagPath: parentNode.key });
const mongoInfos = instMap.get(parentNode.key); if (!res.total) {
if (!mongoInfos) {
return []; return [];
} }
const mongoInfos = res.list;
await sleep(100);
return mongoInfos?.map((x: any) => { return mongoInfos?.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeMongo).withParams(x); return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeMongo).withParams(x);
}); });
}); });
@@ -278,35 +283,6 @@ const nowColl = computed(() => {
return getNowDataTab(); return getNowDataTab();
}); });
/**
* instmap; tagPaht -> mongo info[]
*/
const instMap: Map<string, any[]> = new Map();
const getInsts = async () => {
const res = await mongoApi.mongoList.request({ pageNum: 1, pageSize: 1000 });
if (!res.total) return;
for (const mongoInfo of res.list) {
const tagPath = mongoInfo.tagPath;
let mongoInsts = instMap.get(tagPath) || [];
mongoInsts.push(mongoInfo);
instMap.set(tagPath, mongoInsts);
}
};
/**
* 加载标签树树节点
*/
const loadTags = async () => {
await getInsts();
const tagPaths = instMap.keys();
const tagNodes = [];
for (let tagPath of tagPaths) {
tagNodes.push(new TagTreeNode(tagPath, tagPath, NodeTypeTagPath));
}
return tagNodes;
};
const changeCollection = async (id: any, schema: string, collection: string) => { const changeCollection = async (id: any, schema: string, collection: string) => {
const label = `${id}:\`${schema}\`.${collection}`; const label = `${id}:\`${schema}\`.${collection}`;
let dataTab = state.dataTabs[label]; let dataTab = state.dataTabs[label];

View File

@@ -4,8 +4,19 @@
<el-form :model="form" ref="mongoForm" :rules="rules" label-width="85px"> <el-form :model="form" ref="mongoForm" :rules="rules" label-width="85px">
<el-tabs v-model="tabActiveName"> <el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic"> <el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签" required> <el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" /> <tag-tree-select
@change-tag="
(tagIds) => {
form.tagId = tagIds;
tagSelectRef.validate();
}
"
multiple
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Mongo.value"
style="width: 100%"
/>
</el-form-item> </el-form-item>
<el-form-item prop="name" label="名称" required> <el-form-item prop="name" label="名称" required>
@@ -45,8 +56,9 @@
import { toRefs, reactive, watch, ref } from 'vue'; import { toRefs, reactive, watch, ref } from 'vue';
import { mongoApi } from './api'; import { mongoApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import TagSelect from '../component/TagSelect.vue'; import TagTreeSelect from '../component/TagTreeSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue'; import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({ const props = defineProps({
visible: { visible: {
@@ -88,16 +100,18 @@ const rules = {
}; };
const mongoForm: any = ref(null); const mongoForm: any = ref(null);
const tagSelectRef: any = ref(null);
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
tabActiveName: 'basic', tabActiveName: 'basic',
form: { form: {
id: null, id: null,
code: '',
name: null, name: null,
uri: null, uri: null,
sshTunnelMachineId: null as any, sshTunnelMachineId: null as any,
tagId: null as any, tagId: [],
tagPath: null as any,
}, },
btnLoading: false, btnLoading: false,
testConnBtnLoading: false, testConnBtnLoading: false,
@@ -146,7 +160,7 @@ const testConn = async () => {
const btnOk = async () => { const btnOk = async () => {
mongoForm.value.validate(async (valid: boolean) => { mongoForm.value.validate(async (valid: boolean) => {
if (valid) { if (valid) {
mongoApi.saveMongo.request(getReqForm).then(() => { mongoApi.saveMongo.request(getReqForm()).then(() => {
ElMessage.success('保存成功'); ElMessage.success('保存成功');
emit('val-change', state.form); emit('val-change', state.form);
state.btnLoading = true; state.btnLoading = true;

View File

@@ -25,10 +25,7 @@
</template> </template>
<template #tagPath="{ data }"> <template #tagPath="{ data }">
<tag-info :tag-path="data.tagPath" /> <resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Mongo.value" />
<span class="ml5">
{{ data.tagPath }}
</span>
</template> </template>
<template #action="{ data }"> <template #action="{ data }">
@@ -57,21 +54,25 @@
import { mongoApi } from './api'; import { mongoApi } from './api';
import { defineAsyncComponent, ref, toRefs, reactive, onMounted } from 'vue'; import { defineAsyncComponent, ref, toRefs, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import TagInfo from '../component/TagInfo.vue'; import ResourceTag from '../component/ResourceTag.vue';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable'; import { TableColumn, TableQuery } from '@/components/pagetable';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { tagApi } from '../tag/api';
import { useRoute } from 'vue-router';
const MongoEdit = defineAsyncComponent(() => import('./MongoEdit.vue')); const MongoEdit = defineAsyncComponent(() => import('./MongoEdit.vue'));
const MongoDbs = defineAsyncComponent(() => import('./MongoDbs.vue')); const MongoDbs = defineAsyncComponent(() => import('./MongoDbs.vue'));
const MongoRunCommand = defineAsyncComponent(() => import('./MongoRunCommand.vue')); const MongoRunCommand = defineAsyncComponent(() => import('./MongoRunCommand.vue'));
const pageTableRef: any = ref(null); const pageTableRef: any = ref(null);
const route = useRoute();
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect')]; const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect')];
const columns = ref([ const columns = ref([
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
TableColumn.new('name', '名称'), TableColumn.new('name', '名称'),
TableColumn.new('uri', '连接uri'), TableColumn.new('uri', '连接uri'),
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(20).alignCenter(),
TableColumn.new('createTime', '创建时间').isTime(), TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new('creator', '创建人'), TableColumn.new('creator', '创建人'),
TableColumn.new('action', '操作').isSlot().setMinWidth(170).fixedRight().alignCenter(), TableColumn.new('action', '操作').isSlot().setMinWidth(170).fixedRight().alignCenter(),
@@ -89,7 +90,7 @@ const state = reactive({
query: { query: {
pageNum: 1, pageNum: 1,
pageSize: 0, pageSize: 0,
tagPath: null, tagPath: '',
}, },
mongoEditDialog: { mongoEditDialog: {
visible: false, visible: false,
@@ -134,6 +135,11 @@ const deleteMongo = async () => {
const search = async () => { const search = async () => {
try { try {
pageTableRef.value.loading(true); pageTableRef.value.loading(true);
if (route.query.tagPath) {
state.query.tagPath = route.query.tagPath as string;
}
const res = await mongoApi.mongoList.request(state.query); const res = await mongoApi.mongoList.request(state.query);
state.list = res.list; state.list = res.list;
state.total = res.total; state.total = res.total;
@@ -143,7 +149,7 @@ const search = async () => {
}; };
const getTags = async () => { const getTags = async () => {
state.tags = await mongoApi.mongoTags.request(null); state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Mongo.value });
}; };
const editMongo = async (data: any) => { const editMongo = async (data: any) => {

View File

@@ -4,7 +4,7 @@
<el-col :span="5"> <el-col :span="5">
<el-row type="flex" justify="space-between"> <el-row type="flex" justify="space-between">
<el-col :span="24" class="flex-auto"> <el-col :span="24" class="flex-auto">
<tag-tree :loadTags="loadTags"> <tag-tree :resource-type="TagResourceTypeEnum.Redis.value" :tag-path-node-type="NodeTypeTagPath">
<template #prefix="{ data }"> <template #prefix="{ data }">
<span v-if="data.type.value == RedisNodeType.Redis"> <span v-if="data.type.value == RedisNodeType.Redis">
<el-popover :show-after="500" placement="right-start" title="redis实例信息" trigger="hover" :width="250"> <el-popover :show-after="500" placement="right-start" title="redis实例信息" trigger="hover" :width="250">
@@ -181,6 +181,8 @@ import { TagTreeNode, NodeType } from '../component/tag';
import TagTree from '../component/TagTree.vue'; import TagTree from '../component/TagTree.vue';
import { keysToTree, sortByTreeNodes, keysToList } from './utils'; import { keysToTree, sortByTreeNodes, keysToList } from './utils';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu'; import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
import { sleep } from '../../../common/utils/loading';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue')); const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
@@ -212,11 +214,15 @@ class RedisNodeType {
// tagpath 节点类型 // tagpath 节点类型
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => { const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const redisInfos = instMap.get(parentNode.key); const res = await redisApi.redisList.request({ tagPath: parentNode.key });
if (!redisInfos) { if (!res.total) {
return []; return [];
} }
const redisInfos = res.list;
await sleep(100);
return redisInfos.map((x: any) => { return redisInfos.map((x: any) => {
x.tagPath = parentNode.key;
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeRedis).withParams(x); return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeRedis).withParams(x);
}); });
}); });
@@ -321,34 +327,34 @@ const setHeight = () => {
state.keyTreeHeight = window.innerHeight - 174 + 'px'; state.keyTreeHeight = window.innerHeight - 174 + 'px';
}; };
/** // /**
* instmap; tagPaht -> redis info[] // * instmap; tagPaht -> redis info[]
*/ // */
const instMap: Map<string, any[]> = new Map(); // const instMap: Map<string, any[]> = new Map();
const getInsts = async () => { // const getInsts = async () => {
const res = await redisApi.redisList.request({ pageNum: 1, pageSize: 1000 }); // const res = await redisApi.redisList.request({ pageNum: 1, pageSize: 1000 });
if (!res.total) return; // if (!res.total) return;
for (const redisInfo of res.list) { // for (const redisInfo of res.list) {
const tagPath = redisInfo.tagPath; // const tagPath = redisInfo.tagPath;
let redisInsts = instMap.get(tagPath) || []; // let redisInsts = instMap.get(tagPath) || [];
redisInsts.push(redisInfo); // redisInsts.push(redisInfo);
instMap.set(tagPath, redisInsts); // instMap.set(tagPath, redisInsts);
} // }
}; // };
/** // /**
* 加载标签树节点 // * 加载标签树节点
*/ // */
const loadTags = async () => { // const loadTags = async () => {
await getInsts(); // await getInsts();
const tagPaths = instMap.keys(); // const tagPaths = instMap.keys();
const tagNodes = []; // const tagNodes = [];
for (let tagPath of tagPaths) { // for (let tagPath of tagPaths) {
tagNodes.push(new TagTreeNode(tagPath, tagPath, NodeTypeTagPath)); // tagNodes.push(new TagTreeNode(tagPath, tagPath, NodeTypeTagPath));
} // }
return tagNodes; // return tagNodes;
}; // };
const scan = async (appendKey = false) => { const scan = async (appendKey = false) => {
isTrue(state.scanParam.id != null, '请先选择redis'); isTrue(state.scanParam.id != null, '请先选择redis');

View File

@@ -4,8 +4,19 @@
<el-form :model="form" ref="redisForm" :rules="rules" label-width="auto"> <el-form :model="form" ref="redisForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName"> <el-tabs v-model="tabActiveName">
<el-tab-pane label="基础信息" name="basic"> <el-tab-pane label="基础信息" name="basic">
<el-form-item prop="tagId" label="标签" required> <el-form-item ref="tagSelectRef" prop="tagId" label="标签" required>
<tag-select v-model="form.tagId" v-model:tag-path="form.tagPath" style="width: 100%" /> <tag-tree-select
@change-tag="
(tagIds) => {
form.tagId = tagIds;
tagSelectRef.validate();
}
"
multiple
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Redis.value"
style="width: 100%"
/>
</el-form-item> </el-form-item>
<el-form-item prop="name" label="名称" required> <el-form-item prop="name" label="名称" required>
<el-input v-model.trim="form.name" placeholder="请输入redis名称" auto-complete="off"></el-input> <el-input v-model.trim="form.name" placeholder="请输入redis名称" auto-complete="off"></el-input>
@@ -87,8 +98,9 @@ import { toRefs, reactive, watch, ref } from 'vue';
import { redisApi } from './api'; import { redisApi } from './api';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { RsaEncrypt } from '@/common/rsa'; import { RsaEncrypt } from '@/common/rsa';
import TagSelect from '../component/TagSelect.vue'; import TagTreeSelect from '../component/TagTreeSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue'; import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({ const props = defineProps({
visible: { visible: {
@@ -143,13 +155,15 @@ const rules = {
}; };
const redisForm: any = ref(null); const redisForm: any = ref(null);
const tagSelectRef: any = ref(null);
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
tabActiveName: 'basic', tabActiveName: 'basic',
form: { form: {
id: null, id: null,
tagId: null as any, code: '',
tagPath: null as any, tagId: [],
name: null, name: null,
mode: 'standalone', mode: 'standalone',
host: '', host: '',

View File

@@ -25,10 +25,7 @@
</template> </template>
<template #tagPath="{ data }"> <template #tagPath="{ data }">
<tag-info :tag-path="data.tagPath" /> <resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Redis.value" />
<span class="ml5">
{{ data.tagPath }}
</span>
</template> </template>
<template #action="{ data }"> <template #action="{ data }">
@@ -167,18 +164,22 @@ import { ref, toRefs, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import RedisEdit from './RedisEdit.vue'; import RedisEdit from './RedisEdit.vue';
import { dateFormat } from '@/common/utils/date'; import { dateFormat } from '@/common/utils/date';
import TagInfo from '../component/TagInfo.vue'; import ResourceTag from '../component/ResourceTag.vue';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn, TableQuery } from '@/components/pagetable'; import { TableColumn, TableQuery } from '@/components/pagetable';
import { tagApi } from '../tag/api';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import { useRoute } from 'vue-router';
const pageTableRef: any = ref(null); const pageTableRef: any = ref(null);
const route = useRoute();
const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect')]; const queryConfig = [TableQuery.slot('tagPath', '标签', 'tagPathSelect')];
const columns = ref([ const columns = ref([
TableColumn.new('tagPath', '标签路径').isSlot().setAddWidth(20),
TableColumn.new('name', '名称'), TableColumn.new('name', '名称'),
TableColumn.new('host', 'host:port'), TableColumn.new('host', 'host:port'),
TableColumn.new('mode', 'mode'), TableColumn.new('mode', 'mode'),
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
TableColumn.new('remark', '备注'), TableColumn.new('remark', '备注'),
TableColumn.new('action', '操作').isSlot().setMinWidth(200).fixedRight().alignCenter(), TableColumn.new('action', '操作').isSlot().setMinWidth(200).fixedRight().alignCenter(),
]); ]);
@@ -189,7 +190,7 @@ const state = reactive({
total: 0, total: 0,
selectionData: [], selectionData: [],
query: { query: {
tagPath: null, tagPath: '',
pageNum: 1, pageNum: 1,
pageSize: 0, pageSize: 0,
}, },
@@ -269,6 +270,11 @@ const onShowClusterInfo = async (redis: any) => {
const search = async () => { const search = async () => {
try { try {
pageTableRef.value.loading(true); pageTableRef.value.loading(true);
if (route.query.tagPath) {
state.query.tagPath = route.query.tagPath as string;
}
const res = await redisApi.redisList.request(state.query); const res = await redisApi.redisList.request(state.query);
state.redisTable = res.list; state.redisTable = res.list;
state.total = res.total; state.total = res.total;
@@ -278,7 +284,7 @@ const search = async () => {
}; };
const getTags = async () => { const getTags = async () => {
state.tags = await redisApi.redisTags.request(null); state.tags = await tagApi.getResourceTagPaths.request({ resourceType: TagResourceTypeEnum.Redis.value });
}; };
const editRedis = async (data: any) => { const editRedis = async (data: any) => {

View File

@@ -79,6 +79,24 @@
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
<el-dialog :title="`[ ${resourceDialog.tagPath} ] 关联的资源`" v-model="resourceDialog.visible" width="500px">
<el-table max-height="300" :data="resourceDialog.data">
<el-table-column property="resourceType" label="资源类型" min-width="50" show-overflow-tooltip>
<template #default="scope">
{{ EnumValue.getLabelByValue(TagResourceTypeEnum, scope.row.resourceType) }}
</template>
</el-table-column>
<el-table-column property="count" label="数量" min-width="50" show-overflow-tooltip> </el-table-column>
<el-table-column label="操作" min-width="50" show-overflow-tooltip>
<template #default="scope">
<el-button @click="showResources(scope.row.resourceType, resourceDialog.tagPath)" link type="success">查看</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" /> <contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
</div> </div>
</template> </template>
@@ -89,6 +107,9 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import { tagApi } from './api'; import { tagApi } from './api';
import { dateFormat } from '@/common/utils/date'; import { dateFormat } from '@/common/utils/date';
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index'; import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index';
import { TagResourceTypeEnum } from '../../../common/commonEnum';
import EnumValue from '@/common/Enum';
import { useRouter } from 'vue-router';
interface Tree { interface Tree {
id: number; id: number;
@@ -97,6 +118,8 @@ interface Tree {
children?: Tree[]; children?: Tree[];
} }
const router = useRouter();
const tagForm: any = ref(null); const tagForm: any = ref(null);
const tagTreeRef: any = ref(null); const tagTreeRef: any = ref(null);
const filterTag = ref(''); const filterTag = ref('');
@@ -123,6 +146,14 @@ const contextmenuDel = new ContextmenuItem('delete', '删除')
}) })
.withOnClick((data: any) => deleteTag(data)); .withOnClick((data: any) => deleteTag(data));
const contextmenuShowRelateResource = new ContextmenuItem('showRelateResources', '查看关联资源')
.withIcon('view')
.withHideFunc((data: any) => {
// 存在子标签,则不允许查看关联资源
return data.children;
})
.withOnClick((data: any) => showRelateResource(data));
const state = reactive({ const state = reactive({
data: [], data: [],
saveTabDialog: { saveTabDialog: {
@@ -136,6 +167,12 @@ const state = reactive({
// 资源类型选择是否选 // 资源类型选择是否选
data: null as any, data: null as any,
}, },
resourceDialog: {
title: '',
visible: false,
tagPath: '',
data: null as any,
},
// 展开的节点 // 展开的节点
defaultExpandedKeys: [] as any, defaultExpandedKeys: [] as any,
contextmenu: { contextmenu: {
@@ -143,11 +180,11 @@ const state = reactive({
x: 0, x: 0,
y: 0, y: 0,
}, },
items: [contextmenuInfo, contextmenuEdit, contextmenuAdd, contextmenuDel], items: [contextmenuInfo, contextmenuEdit, contextmenuAdd, contextmenuDel, contextmenuShowRelateResource],
}, },
}); });
const { data, saveTabDialog, infoDialog, defaultExpandedKeys } = toRefs(state); const { data, saveTabDialog, infoDialog, resourceDialog, defaultExpandedKeys } = toRefs(state);
const props = { const props = {
label: 'name', label: 'name',
@@ -205,7 +242,7 @@ const info = async (data: any) => {
const showSaveTagDialog = (data: any) => { const showSaveTagDialog = (data: any) => {
if (data) { if (data) {
state.saveTabDialog.form.pid = data.id; state.saveTabDialog.form.pid = data.id;
state.saveTabDialog.title = `新增 [${data.codePath}] 子标签信息`; state.saveTabDialog.title = `新增[ ${data.codePath} ]子标签信息`;
} else { } else {
state.saveTabDialog.title = '新增根标签信息'; state.saveTabDialog.title = '新增根标签信息';
} }
@@ -221,6 +258,49 @@ const showEditTagDialog = (data: any) => {
state.saveTabDialog.visible = true; state.saveTabDialog.visible = true;
}; };
const showRelateResource = async (data: any) => {
const resourceMap = new Map();
state.resourceDialog.tagPath = data.codePath;
const tagResources = await tagApi.getTagResources.request({ tagId: data.id });
for (let tagResource of tagResources) {
const resourceType = tagResource.resourceType;
const exist = resourceMap.get(resourceType);
if (exist) {
exist.count = exist.count + 1;
} else {
resourceMap.set(resourceType, { resourceType, count: 1, tagPath: tagResource.tagPath });
}
}
state.resourceDialog.data = Array.from(resourceMap.values());
state.resourceDialog.visible = true;
};
const showResources = (resourceType: any, tagPath: string) => {
state.resourceDialog.visible = false;
setTimeout(() => {
let toPath = '';
if (resourceType == TagResourceTypeEnum.Machine.value) {
toPath = '/machine/machines';
}
if (resourceType == TagResourceTypeEnum.Db.value) {
toPath = '/dbms/dbs';
}
if (resourceType == TagResourceTypeEnum.Redis.value) {
toPath = '/redis/manage';
}
if (resourceType == TagResourceTypeEnum.Mongo.value) {
toPath = '/mongo/mongo-manage';
}
router.push({
path: toPath,
query: {
tagPath,
},
});
}, 350);
};
const saveTag = async () => { const saveTag = async () => {
tagForm.value.validate(async (valid: any) => { tagForm.value.validate(async (valid: any) => {
if (valid) { if (valid) {

View File

@@ -1,12 +1,14 @@
import Api from '@/common/Api'; import Api from '@/common/Api';
export const tagApi = { export const tagApi = {
getAccountTags: Api.newGet('/tag-trees/account-has'),
listByQuery: Api.newGet('/tag-trees/query'), listByQuery: Api.newGet('/tag-trees/query'),
getTagTrees: Api.newGet('/tag-trees'), getTagTrees: Api.newGet('/tag-trees'),
saveTagTree: Api.newPost('/tag-trees'), saveTagTree: Api.newPost('/tag-trees'),
delTagTree: Api.newDelete('/tag-trees/{id}'), delTagTree: Api.newDelete('/tag-trees/{id}'),
getResourceTagPaths: Api.newGet('/tag-trees/resources/{resourceType}/tag-paths'),
getTagResources: Api.newGet('/tag-trees/resources'),
getTeams: Api.newGet('/teams'), getTeams: Api.newGet('/teams'),
saveTeam: Api.newPost('/teams'), saveTeam: Api.newPost('/teams'),
delTeam: Api.newDelete('/teams/{id}'), delTeam: Api.newDelete('/teams/{id}'),

View File

@@ -7,10 +7,10 @@
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8"
integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==
"@babel/parser@^7.23.3": "@babel/parser@^7.23.5":
version "7.23.4" version "7.23.5"
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.23.4.tgz#409fbe690c333bb70187e2de4021e1e47a026661" resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.23.5.tgz#37dee97c4752af148e1d38c34b856b2507660563"
integrity sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ== integrity sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==
"@babel/runtime@^7.21.0": "@babel/runtime@^7.21.0":
version "7.21.5" version "7.21.5"
@@ -440,6 +440,16 @@
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz#8ae96573236cdb12de6850a6d929b5537ec85390" resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz#8ae96573236cdb12de6850a6d929b5537ec85390"
integrity sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg== integrity sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==
"@vue/compiler-core@3.3.10":
version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.10.tgz#9ca4123a1458df43db641aaa8b7d1e636aa22545"
integrity sha512-doe0hODR1+i1menPkRzJ5MNR6G+9uiZHIknK3Zn5OcIztu6GGw7u0XUzf3AgB8h/dfsZC9eouzoLo3c3+N/cVA==
dependencies:
"@babel/parser" "^7.23.5"
"@vue/shared" "3.3.10"
estree-walker "^2.0.2"
source-map-js "^1.0.2"
"@vue/compiler-core@3.3.4": "@vue/compiler-core@3.3.4":
version "3.3.4" version "3.3.4"
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz#7fbf591c1c19e1acd28ffd284526e98b4f581128" resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz#7fbf591c1c19e1acd28ffd284526e98b4f581128"
@@ -450,15 +460,13 @@
estree-walker "^2.0.2" estree-walker "^2.0.2"
source-map-js "^1.0.2" source-map-js "^1.0.2"
"@vue/compiler-core@3.3.9": "@vue/compiler-dom@3.3.10":
version "3.3.9" version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.9.tgz#df1fc7947dcef5c2e12d257eae540057707f47d1" resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.10.tgz#183811252be6aff4ac923f783124bb1590301907"
integrity sha512-+/Lf68Vr/nFBA6ol4xOtJrW+BQWv3QWKfRwGSm70jtXwfhZNF4R/eRgyVJYoxFRhdCTk/F6g99BP0ffPgZihfQ== integrity sha512-NCrqF5fm10GXZIK0GrEAauBqdy+F2LZRt3yNHzrYjpYBuRssQbuPLtSnSNjyR9luHKkWSH8we5LMB3g+4z2HvA==
dependencies: dependencies:
"@babel/parser" "^7.23.3" "@vue/compiler-core" "3.3.10"
"@vue/shared" "3.3.9" "@vue/shared" "3.3.10"
estree-walker "^2.0.2"
source-map-js "^1.0.2"
"@vue/compiler-dom@3.3.4": "@vue/compiler-dom@3.3.4":
version "3.3.4" version "3.3.4"
@@ -468,28 +476,20 @@
"@vue/compiler-core" "3.3.4" "@vue/compiler-core" "3.3.4"
"@vue/shared" "3.3.4" "@vue/shared" "3.3.4"
"@vue/compiler-dom@3.3.9": "@vue/compiler-sfc@3.3.10":
version "3.3.9" version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.9.tgz#67315ea4193d9d18c7a710889b8f90f7aa3914d2" resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.10.tgz#8eb97d42f276089ec58fd0565ef3a813bceeaa87"
integrity sha512-nfWubTtLXuT4iBeDSZ5J3m218MjOy42Vp2pmKVuBKo2/BLcrFUX8nCSr/bKRFiJ32R8qbdnnnBgRn9AdU5v0Sg== integrity sha512-xpcTe7Rw7QefOTRFFTlcfzozccvjM40dT45JtrE3onGm/jBLZ0JhpKu3jkV7rbDFLeeagR/5RlJ2Y9SvyS0lAg==
dependencies: dependencies:
"@vue/compiler-core" "3.3.9" "@babel/parser" "^7.23.5"
"@vue/shared" "3.3.9" "@vue/compiler-core" "3.3.10"
"@vue/compiler-dom" "3.3.10"
"@vue/compiler-sfc@3.3.9": "@vue/compiler-ssr" "3.3.10"
version "3.3.9" "@vue/reactivity-transform" "3.3.10"
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.9.tgz#5900906baba1a90389200d81753ad0f7ceb98a83" "@vue/shared" "3.3.10"
integrity sha512-wy0CNc8z4ihoDzjASCOCsQuzW0A/HP27+0MDSSICMjVIFzk/rFViezkR3dzH+miS2NDEz8ywMdbjO5ylhOLI2A==
dependencies:
"@babel/parser" "^7.23.3"
"@vue/compiler-core" "3.3.9"
"@vue/compiler-dom" "3.3.9"
"@vue/compiler-ssr" "3.3.9"
"@vue/reactivity-transform" "3.3.9"
"@vue/shared" "3.3.9"
estree-walker "^2.0.2" estree-walker "^2.0.2"
magic-string "^0.30.5" magic-string "^0.30.5"
postcss "^8.4.31" postcss "^8.4.32"
source-map-js "^1.0.2" source-map-js "^1.0.2"
"@vue/compiler-sfc@^3.3.4": "@vue/compiler-sfc@^3.3.4":
@@ -508,6 +508,14 @@
postcss "^8.1.10" postcss "^8.1.10"
source-map-js "^1.0.2" source-map-js "^1.0.2"
"@vue/compiler-ssr@3.3.10":
version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.10.tgz#5a1b14a358cb3960a4edbce0ade90548e452fcaa"
integrity sha512-12iM4jA4GEbskwXMmPcskK5wImc2ohKm408+o9iox3tfN9qua8xL0THIZtoe9OJHnXP4eOWZpgCAAThEveNlqQ==
dependencies:
"@vue/compiler-dom" "3.3.10"
"@vue/shared" "3.3.10"
"@vue/compiler-ssr@3.3.4": "@vue/compiler-ssr@3.3.4":
version "3.3.4" version "3.3.4"
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz#9d1379abffa4f2b0cd844174ceec4a9721138777" resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz#9d1379abffa4f2b0cd844174ceec4a9721138777"
@@ -516,19 +524,22 @@
"@vue/compiler-dom" "3.3.4" "@vue/compiler-dom" "3.3.4"
"@vue/shared" "3.3.4" "@vue/shared" "3.3.4"
"@vue/compiler-ssr@3.3.9":
version "3.3.9"
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.9.tgz#3b3dbfa5368165fa4ff74c060503b4087ec1beed"
integrity sha512-NO5oobAw78R0G4SODY5A502MGnDNiDjf6qvhn7zD7TJGc8XDeIEw4fg6JU705jZ/YhuokBKz0A5a/FL/XZU73g==
dependencies:
"@vue/compiler-dom" "3.3.9"
"@vue/shared" "3.3.9"
"@vue/devtools-api@^6.5.0": "@vue/devtools-api@^6.5.0":
version "6.5.0" version "6.5.0"
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07" resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07"
integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q== integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==
"@vue/reactivity-transform@3.3.10":
version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.10.tgz#b045776cc954bb57883fd305db7a200d42993768"
integrity sha512-0xBdk+CKHWT+Gev8oZ63Tc0qFfj935YZx+UAynlutnrDZ4diFCVFMWixn65HzjE3S1iJppWOo6Tt1OzASH7VEg==
dependencies:
"@babel/parser" "^7.23.5"
"@vue/compiler-core" "3.3.10"
"@vue/shared" "3.3.10"
estree-walker "^2.0.2"
magic-string "^0.30.5"
"@vue/reactivity-transform@3.3.4": "@vue/reactivity-transform@3.3.4":
version "3.3.4" version "3.3.4"
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz#52908476e34d6a65c6c21cd2722d41ed8ae51929" resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz#52908476e34d6a65c6c21cd2722d41ed8ae51929"
@@ -540,59 +551,48 @@
estree-walker "^2.0.2" estree-walker "^2.0.2"
magic-string "^0.30.0" magic-string "^0.30.0"
"@vue/reactivity-transform@3.3.9": "@vue/reactivity@3.3.10":
version "3.3.9" version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.9.tgz#5d894dd9a42a422a2db309babb385f9a2529b52f" resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.10.tgz#78fe3da319276d9e6d0f072037532928c472a287"
integrity sha512-HnUFm7Ry6dFa4Lp63DAxTixUp8opMtQr6RxQCpDI1vlh12rkGIeYqMvJtK+IKyEfEOa2I9oCkD1mmsPdaGpdVg== integrity sha512-H5Z7rOY/JLO+e5a6/FEXaQ1TMuOvY4LDVgT+/+HKubEAgs9qeeZ+NhADSeEtrNQeiKLDuzeKc8v0CUFpB6Pqgw==
dependencies: dependencies:
"@babel/parser" "^7.23.3" "@vue/shared" "3.3.10"
"@vue/compiler-core" "3.3.9"
"@vue/shared" "3.3.9"
estree-walker "^2.0.2"
magic-string "^0.30.5"
"@vue/reactivity@3.3.9": "@vue/runtime-core@3.3.10":
version "3.3.9" version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.9.tgz#e28e8071bd74edcdd9c87b667ad00e8fbd8d6920" resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.10.tgz#d7b78c5c0500b856cf9447ef81d4a1b1438fd5bb"
integrity sha512-VmpIqlNp+aYDg2X0xQhJqHx9YguOmz2UxuUJDckBdQCNkipJvfk9yA75woLWElCa0Jtyec3lAAt49GO0izsphw== integrity sha512-DZ0v31oTN4YHX9JEU5VW1LoIVgFovWgIVb30bWn9DG9a7oA415idcwsRNNajqTx8HQJyOaWfRKoyuP2P2TYIag==
dependencies: dependencies:
"@vue/shared" "3.3.9" "@vue/reactivity" "3.3.10"
"@vue/shared" "3.3.10"
"@vue/runtime-core@3.3.9": "@vue/runtime-dom@3.3.10":
version "3.3.9" version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.9.tgz#c835b77f7dc7ae5f251e93f277b54963ea1b5c31" resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.10.tgz#130dfffb8fee8051671aaf80c5104d2020544950"
integrity sha512-xxaG9KvPm3GTRuM4ZyU8Tc+pMVzcu6eeoSRQJ9IE7NmCcClW6z4B3Ij6L4EDl80sxe/arTtQ6YmgiO4UZqRc+w== integrity sha512-c/jKb3ny05KJcYk0j1m7Wbhrxq7mZYr06GhKykDMNRRR9S+/dGT8KpHuNQjv3/8U4JshfkAk6TpecPD3B21Ijw==
dependencies: dependencies:
"@vue/reactivity" "3.3.9" "@vue/runtime-core" "3.3.10"
"@vue/shared" "3.3.9" "@vue/shared" "3.3.10"
"@vue/runtime-dom@3.3.9":
version "3.3.9"
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.9.tgz#68081d981695a229d72f431fed0b0cdd9161ce53"
integrity sha512-e7LIfcxYSWbV6BK1wQv9qJyxprC75EvSqF/kQKe6bdZEDNValzeRXEVgiX7AHI6hZ59HA4h7WT5CGvm69vzJTQ==
dependencies:
"@vue/runtime-core" "3.3.9"
"@vue/shared" "3.3.9"
csstype "^3.1.2" csstype "^3.1.2"
"@vue/server-renderer@3.3.9": "@vue/server-renderer@3.3.10":
version "3.3.9" version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.9.tgz#ffb41bc9c7afafcc608d0c500e9d6b0af7d68fad" resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.10.tgz#f23d151f0e5021ebdc730052d9934c9178486742"
integrity sha512-w0zT/s5l3Oa3ZjtLW88eO4uV6AQFqU8X5GOgzq7SkQQu6vVr+8tfm+OI2kDBplS/W/XgCBuFXiPw6T5EdwXP0A== integrity sha512-0i6ww3sBV3SKlF3YTjSVqKQ74xialMbjVYGy7cOTi7Imd8ediE7t72SK3qnvhrTAhOvlQhq6Bk6nFPdXxe0sAg==
dependencies: dependencies:
"@vue/compiler-ssr" "3.3.9" "@vue/compiler-ssr" "3.3.10"
"@vue/shared" "3.3.9" "@vue/shared" "3.3.10"
"@vue/shared@3.3.10":
version "3.3.10"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.10.tgz#1583a8d85a957d8b819078c465d2a11db7914b2f"
integrity sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==
"@vue/shared@3.3.4": "@vue/shared@3.3.4":
version "3.3.4" version "3.3.4"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.4.tgz#06e83c5027f464eef861c329be81454bc8b70780" resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.4.tgz#06e83c5027f464eef861c329be81454bc8b70780"
integrity sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ== integrity sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==
"@vue/shared@3.3.9":
version "3.3.9"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.9.tgz#df740d26d338faf03e09ca662a8031acf66051db"
integrity sha512-ZE0VTIR0LmYgeyhurPTpy4KzKsuDyQbMSdM49eKkMnT5X4VfFBLysMzjIZhLEFQYjjOVVfbvUDHckwjDFiO2eA==
"@vueuse/core@^9.1.0": "@vueuse/core@^9.1.0":
version "9.2.0" version "9.2.0"
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-9.2.0.tgz" resolved "https://registry.npmmirror.com/@vueuse/core/-/core-9.2.0.tgz"
@@ -1518,10 +1518,10 @@ nanoid@^3.1.30:
resolved "https://registry.npmmirror.com/nanoid/download/nanoid-3.1.30.tgz" resolved "https://registry.npmmirror.com/nanoid/download/nanoid-3.1.30.tgz"
integrity sha1-Y/k8xUjSoRPcXfvGO/oJ4rm2Q2I= integrity sha1-Y/k8xUjSoRPcXfvGO/oJ4rm2Q2I=
nanoid@^3.3.6: nanoid@^3.3.7:
version "3.3.6" version "3.3.7"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
@@ -1660,12 +1660,12 @@ postcss@^8.1.10:
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.1" source-map-js "^1.0.1"
postcss@^8.4.31: postcss@^8.4.32:
version "8.4.31" version "8.4.32"
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==
dependencies: dependencies:
nanoid "^3.3.6" nanoid "^3.3.7"
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.2" source-map-js "^1.0.2"
@@ -1938,13 +1938,13 @@ uuid@^9.0.1:
resolved "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" resolved "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
vite@^5.0.3: vite@^5.0.5:
version "5.0.3" version "5.0.5"
resolved "https://registry.npmmirror.com/vite/-/vite-5.0.3.tgz#febf6801604c618234de331bd04382cf9a149ec6" resolved "https://registry.npmmirror.com/vite/-/vite-5.0.5.tgz#3eebe3698e3b32cea36350f58879258fec858a3c"
integrity sha512-WgEq8WEKpZ8c0DL4M1+E+kBZEJyjBmGVrul6z8Ljfhv+PPbNF4aGq014DwNYxGz2FGq6NKL0N8usdiESWd2l2w== integrity sha512-OekeWqR9Ls56f3zd4CaxzbbS11gqYkEiBtnWFFgYR2WV8oPJRRKq0mpskYy/XaoCL3L7VINDhqqOMNDiYdGvGg==
dependencies: dependencies:
esbuild "^0.19.3" esbuild "^0.19.3"
postcss "^8.4.31" postcss "^8.4.32"
rollup "^4.2.0" rollup "^4.2.0"
optionalDependencies: optionalDependencies:
fsevents "~2.3.3" fsevents "~2.3.3"
@@ -1979,16 +1979,16 @@ vue-router@^4.2.5:
dependencies: dependencies:
"@vue/devtools-api" "^6.5.0" "@vue/devtools-api" "^6.5.0"
vue@^3.3.9: vue@^3.3.10:
version "3.3.9" version "3.3.10"
resolved "https://registry.npmmirror.com/vue/-/vue-3.3.9.tgz#219a2ec68e8d4d0b0180460af0f5b9299b3f3f1f" resolved "https://registry.npmmirror.com/vue/-/vue-3.3.10.tgz#6e19c1982ee655a14babe1610288b90005f02ab1"
integrity sha512-sy5sLCTR8m6tvUk1/ijri3Yqzgpdsmxgj6n6yl7GXXCXqVbmW2RCXe9atE4cEI6Iv7L89v5f35fZRRr5dChP9w== integrity sha512-zg6SIXZdTBwiqCw/1p+m04VyHjLfwtjwz8N57sPaBhEex31ND0RYECVOC1YrRwMRmxFf5T1dabl6SGUbMKKuVw==
dependencies: dependencies:
"@vue/compiler-dom" "3.3.9" "@vue/compiler-dom" "3.3.10"
"@vue/compiler-sfc" "3.3.9" "@vue/compiler-sfc" "3.3.10"
"@vue/runtime-dom" "3.3.9" "@vue/runtime-dom" "3.3.10"
"@vue/server-renderer" "3.3.9" "@vue/server-renderer" "3.3.10"
"@vue/shared" "3.3.9" "@vue/shared" "3.3.10"
which@^2.0.1: which@^2.0.1:
version "2.0.2" version "2.0.2"

View File

@@ -1,14 +1,11 @@
package api package api
import ( import (
"mayfly-go/internal/common/consts"
dbapp "mayfly-go/internal/db/application" dbapp "mayfly-go/internal/db/application"
dbentity "mayfly-go/internal/db/domain/entity"
machineapp "mayfly-go/internal/machine/application" machineapp "mayfly-go/internal/machine/application"
machineentity "mayfly-go/internal/machine/domain/entity"
mongoapp "mayfly-go/internal/mongo/application" mongoapp "mayfly-go/internal/mongo/application"
mongoentity "mayfly-go/internal/mongo/domain/entity"
redisapp "mayfly-go/internal/redis/application" redisapp "mayfly-go/internal/redis/application"
redisentity "mayfly-go/internal/redis/domain/entity"
tagapp "mayfly-go/internal/tag/application" tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/req" "mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx" "mayfly-go/pkg/utils/collx"
@@ -24,19 +21,12 @@ type Index struct {
func (i *Index) Count(rc *req.Ctx) { func (i *Index) Count(rc *req.Ctx) {
accountId := rc.GetLoginAccount().Id accountId := rc.GetLoginAccount().Id
tagIds := i.TagApp.ListTagIdByAccountId(accountId)
var mongoNum int64 mongoNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMongo, ""))
var redisNum int64 machienNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeMachine, ""))
var dbNum int64 dbNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeDb, ""))
var machienNum int64 redisNum := len(i.TagApp.GetAccountResourceCodes(accountId, consts.TagResourceTypeRedis, ""))
if len(tagIds) > 0 {
mongoNum = i.MongoApp.Count(&mongoentity.MongoQuery{TagIds: tagIds})
machienNum = i.MachineApp.Count(&machineentity.MachineQuery{TagIds: tagIds})
dbNum = i.DbApp.Count(&dbentity.DbQuery{TagIds: tagIds})
redisNum = i.RedisApp.Count(&redisentity.RedisQuery{TagIds: tagIds})
}
rc.ResData = collx.M{ rc.ResData = collx.M{
"mongoNum": mongoNum, "mongoNum": mongoNum,
"machineNum": machienNum, "machineNum": machienNum,

View File

@@ -10,9 +10,14 @@ const (
RedisConnExpireTime = 30 * time.Minute RedisConnExpireTime = 30 * time.Minute
MongoConnExpireTime = 30 * time.Minute MongoConnExpireTime = 30 * time.Minute
/**** 开发测试使用 ****/ /**** 开发测试使用 ****/
// MachineConnExpireTime = 4 * time.Minute // MachineConnExpireTime = 4 * time.Minute
// DbConnExpireTime = 2 * time.Minute // DbConnExpireTime = 2 * time.Minute
// RedisConnExpireTime = 2 * time.Minute // RedisConnExpireTime = 2 * time.Minute
// MongoConnExpireTime = 2 * time.Minute // MongoConnExpireTime = 2 * time.Minute
TagResourceTypeMachine = 1
TagResourceTypeDb = 2
TagResourceTypeRedis = 3
TagResourceTypeMongo = 4
) )

View File

@@ -3,6 +3,7 @@ package api
import ( import (
"fmt" "fmt"
"io" "io"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/db/api/form" "mayfly-go/internal/db/api/form"
"mayfly-go/internal/db/api/vo" "mayfly-go/internal/db/api/vo"
"mayfly-go/internal/db/application" "mayfly-go/internal/db/application"
@@ -41,29 +42,25 @@ func (d *Db) Dbs(rc *req.Ctx) {
queryCond, page := ginx.BindQueryAndPage[*entity.DbQuery](rc.GinCtx, new(entity.DbQuery)) queryCond, page := ginx.BindQueryAndPage[*entity.DbQuery](rc.GinCtx, new(entity.DbQuery))
// 不存在可访问标签id即没有可操作数据 // 不存在可访问标签id即没有可操作数据
tagIds := d.TagApp.ListTagIdByAccountId(rc.GetLoginAccount().Id) codes := d.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeDb, queryCond.TagPath)
if len(tagIds) == 0 { if len(codes) == 0 {
rc.ResData = model.EmptyPageResult[any]() rc.ResData = model.EmptyPageResult[any]()
return return
} }
queryCond.Codes = codes
queryCond.TagIds = tagIds
res, err := d.DbApp.GetPageList(queryCond, page, new([]vo.DbListVO)) res, err := d.DbApp.GetPageList(queryCond, page, new([]vo.DbListVO))
biz.ErrIsNil(err) biz.ErrIsNil(err)
rc.ResData = res rc.ResData = res
} }
func (d *Db) DbTags(rc *req.Ctx) {
rc.ResData = d.TagApp.ListTagByAccountIdAndResource(rc.GetLoginAccount().Id, new(entity.Db))
}
func (d *Db) Save(rc *req.Ctx) { func (d *Db) Save(rc *req.Ctx) {
form := &form.DbForm{} form := &form.DbForm{}
db := ginx.BindJsonAndCopyTo[*entity.Db](rc.GinCtx, form, new(entity.Db)) db := ginx.BindJsonAndCopyTo[*entity.Db](rc.GinCtx, form, new(entity.Db))
rc.ReqParam = form rc.ReqParam = form
biz.ErrIsNil(d.DbApp.Save(rc.MetaCtx, db)) biz.ErrIsNil(d.DbApp.Save(rc.MetaCtx, db, form.TagId...))
} }
func (d *Db) DeleteDb(rc *req.Ctx) { func (d *Db) DeleteDb(rc *req.Ctx) {
@@ -90,7 +87,7 @@ func (d *Db) ExecSql(rc *req.Ctx) {
dbId := getDbId(g) dbId := getDbId(g)
dbConn, err := d.DbApp.GetDbConn(dbId, form.Db) dbConn, err := d.DbApp.GetDbConn(dbId, form.Db)
biz.ErrIsNil(err) biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath), "%s") biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath...), "%s")
rc.ReqParam = fmt.Sprintf("%s\n-> %s", dbConn.Info.GetLogDesc(), form.Sql) rc.ReqParam = fmt.Sprintf("%s\n-> %s", dbConn.Info.GetLogDesc(), form.Sql)
biz.NotEmpty(form.Sql, "sql不能为空") biz.NotEmpty(form.Sql, "sql不能为空")
@@ -161,7 +158,7 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
dbConn, err := d.DbApp.GetDbConn(dbId, dbName) dbConn, err := d.DbApp.GetDbConn(dbId, dbName)
biz.ErrIsNil(err) biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath), "%s") biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath...), "%s")
rc.ReqParam = fmt.Sprintf("filename: %s -> %s", filename, dbConn.Info.GetLogDesc()) rc.ReqParam = fmt.Sprintf("filename: %s -> %s", filename, dbConn.Info.GetLogDesc())
defer func() { defer func() {
@@ -230,7 +227,7 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
} }
dbConn, err = d.DbApp.GetDbConn(dbId, stmtUse.DBName.String()) dbConn, err = d.DbApp.GetDbConn(dbId, stmtUse.DBName.String())
biz.ErrIsNil(err) biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(laId, dbConn.Info.TagPath), "%s") biz.ErrIsNilAppendErr(d.TagApp.CanAccess(laId, dbConn.Info.TagPath...), "%s")
execReq.DbConn = dbConn execReq.DbConn = dbConn
} }
// 需要记录执行记录 // 需要记录执行记录
@@ -270,7 +267,7 @@ func (d *Db) DumpSql(rc *req.Ctx) {
la := rc.GetLoginAccount() la := rc.GetLoginAccount()
db, err := d.DbApp.GetById(new(entity.Db), dbId) db, err := d.DbApp.GetById(new(entity.Db), dbId)
biz.ErrIsNil(err, "该数据库不存在") biz.ErrIsNil(err, "该数据库不存在")
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(la.Id, db.TagPath), "%s") biz.ErrIsNilAppendErr(d.TagApp.CanAccess(la.Id, d.TagApp.ListTagPathByResource(consts.TagResourceTypeDb, db.Code)...), "%s")
now := time.Now() now := time.Now()
filename := fmt.Sprintf("%s.%s.sql%s", db.Name, now.Format("20060102150405"), extName) filename := fmt.Sprintf("%s.%s.sql%s", db.Name, now.Format("20060102150405"), extName)

View File

@@ -1,13 +1,12 @@
package form package form
type DbForm struct { type DbForm struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Name string `binding:"required" json:"name"` Name string `binding:"required" json:"name"`
Database string `json:"database"` Database string `json:"database"`
Remark string `json:"remark"` Remark string `json:"remark"`
TagId uint64 `binding:"required" json:"tagId"` TagId []uint64 `binding:"required" json:"tagId"`
TagPath string `binding:"required" json:"tagPath"` InstanceId uint64 `binding:"required" json:"instanceId"`
InstanceId uint64 `binding:"required" json:"instanceId"`
} }
type DbSqlSaveForm struct { type DbSqlSaveForm struct {

View File

@@ -4,11 +4,10 @@ import "time"
type DbListVO struct { type DbListVO struct {
Id *int64 `json:"id"` Id *int64 `json:"id"`
Code string `json:"code"`
Name *string `json:"name"` Name *string `json:"name"`
Database *string `json:"database"` Database *string `json:"database"`
Remark *string `json:"remark"` Remark *string `json:"remark"`
TagId *int64 `json:"tagId"`
TagPath *string `json:"tagPath"`
InstanceId *int64 `json:"instanceId"` InstanceId *int64 `json:"instanceId"`
InstanceName *string `json:"instanceName"` InstanceName *string `json:"instanceName"`

View File

@@ -2,11 +2,12 @@ package application
import ( import (
"mayfly-go/internal/db/infrastructure/persistence" "mayfly-go/internal/db/infrastructure/persistence"
tagapp "mayfly-go/internal/tag/application"
) )
var ( var (
instanceApp Instance = newInstanceApp(persistence.GetInstanceRepo()) instanceApp Instance = newInstanceApp(persistence.GetInstanceRepo())
dbApp Db = newDbApp(persistence.GetDbRepo(), persistence.GetDbSqlRepo(), instanceApp) dbApp Db = newDbApp(persistence.GetDbRepo(), persistence.GetDbSqlRepo(), instanceApp, tagapp.GetTagTreeApp())
dbSqlExecApp DbSqlExec = newDbSqlExecApp(persistence.GetDbSqlExecRepo()) dbSqlExecApp DbSqlExec = newDbSqlExecApp(persistence.GetDbSqlExecRepo())
dbSqlApp DbSql = newDbSqlApp(persistence.GetDbSqlRepo()) dbSqlApp DbSql = newDbSqlApp(persistence.GetDbSqlRepo())
) )

View File

@@ -2,13 +2,16 @@ package application
import ( import (
"context" "context"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/db/dbm" "mayfly-go/internal/db/dbm"
"mayfly-go/internal/db/domain/entity" "mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository" "mayfly-go/internal/db/domain/repository"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/base" "mayfly-go/pkg/base"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/utils/collx" "mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/utils/structx" "mayfly-go/pkg/utils/structx"
"strings" "strings"
) )
@@ -21,7 +24,7 @@ type Db interface {
Count(condition *entity.DbQuery) int64 Count(condition *entity.DbQuery) int64
Save(ctx context.Context, entity *entity.Db) error Save(ctx context.Context, entity *entity.Db, tagIds ...uint64) error
// 删除数据库信息 // 删除数据库信息
Delete(ctx context.Context, id uint64) error Delete(ctx context.Context, id uint64) error
@@ -32,10 +35,11 @@ type Db interface {
GetDbConn(dbId uint64, dbName string) (*dbm.DbConn, error) GetDbConn(dbId uint64, dbName string) (*dbm.DbConn, error)
} }
func newDbApp(dbRepo repository.Db, dbSqlRepo repository.DbSql, dbInstanceApp Instance) Db { func newDbApp(dbRepo repository.Db, dbSqlRepo repository.DbSql, dbInstanceApp Instance, tagApp tagapp.TagTree) Db {
app := &dbAppImpl{ app := &dbAppImpl{
dbSqlRepo: dbSqlRepo, dbSqlRepo: dbSqlRepo,
dbInstanceApp: dbInstanceApp, dbInstanceApp: dbInstanceApp,
tagApp: tagApp,
} }
app.Repo = dbRepo app.Repo = dbRepo
return app return app
@@ -46,6 +50,7 @@ type dbAppImpl struct {
dbSqlRepo repository.DbSql dbSqlRepo repository.DbSql
dbInstanceApp Instance dbInstanceApp Instance
tagApp tagapp.TagTree
} }
// 分页获取数据库信息列表 // 分页获取数据库信息列表
@@ -57,7 +62,7 @@ func (d *dbAppImpl) Count(condition *entity.DbQuery) int64 {
return d.GetRepo().Count(condition) return d.GetRepo().Count(condition)
} }
func (d *dbAppImpl) Save(ctx context.Context, dbEntity *entity.Db) error { func (d *dbAppImpl) Save(ctx context.Context, dbEntity *entity.Db, tagIds ...uint64) error {
// 查找是否存在 // 查找是否存在
oldDb := &entity.Db{Name: dbEntity.Name, InstanceId: dbEntity.InstanceId} oldDb := &entity.Db{Name: dbEntity.Name, InstanceId: dbEntity.InstanceId}
err := d.GetBy(oldDb) err := d.GetBy(oldDb)
@@ -66,7 +71,15 @@ func (d *dbAppImpl) Save(ctx context.Context, dbEntity *entity.Db) error {
if err == nil { if err == nil {
return errorx.NewBiz("该实例下数据库名已存在") return errorx.NewBiz("该实例下数据库名已存在")
} }
return d.Insert(ctx, dbEntity)
resouceCode := stringx.Rand(16)
dbEntity.Code = resouceCode
return d.Tx(ctx, func(ctx context.Context) error {
return d.Insert(ctx, dbEntity)
}, func(ctx context.Context) error {
return d.tagApp.RelateResource(ctx, resouceCode, consts.TagResourceTypeDb, tagIds)
})
} }
// 如果存在该库,则校验修改的库是否为该库 // 如果存在该库,则校验修改的库是否为该库
@@ -94,7 +107,11 @@ func (d *dbAppImpl) Save(ctx context.Context, dbEntity *entity.Db) error {
d.dbSqlRepo.DeleteByCond(ctx, &entity.DbSql{DbId: dbId, Db: v}) d.dbSqlRepo.DeleteByCond(ctx, &entity.DbSql{DbId: dbId, Db: v})
} }
return d.UpdateById(ctx, dbEntity) return d.Tx(ctx, func(ctx context.Context) error {
return d.UpdateById(ctx, dbEntity)
}, func(ctx context.Context) error {
return d.tagApp.RelateResource(ctx, oldDb.Code, consts.TagResourceTypeDb, tagIds)
})
} }
func (d *dbAppImpl) Delete(ctx context.Context, id uint64) error { func (d *dbAppImpl) Delete(ctx context.Context, id uint64) error {
@@ -138,11 +155,11 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbm.DbConn, error) {
// 密码解密 // 密码解密
instance.PwdDecrypt() instance.PwdDecrypt()
return toDbInfo(instance, dbId, dbName, db.TagPath), nil return toDbInfo(instance, dbId, dbName, d.tagApp.ListTagPathByResource(consts.TagResourceTypeDb, db.Code)...), nil
}) })
} }
func toDbInfo(instance *entity.DbInstance, dbId uint64, database string, tagPath string) *dbm.DbInfo { func toDbInfo(instance *entity.DbInstance, dbId uint64, database string, tagPath ...string) *dbm.DbInfo {
di := new(dbm.DbInfo) di := new(dbm.DbInfo)
di.Id = dbId di.Id = dbId
di.Database = database di.Database = database

View File

@@ -48,6 +48,7 @@ func (app *instanceAppImpl) Count(condition *entity.InstanceQuery) int64 {
} }
func (app *instanceAppImpl) TestConn(instanceEntity *entity.DbInstance) error { func (app *instanceAppImpl) TestConn(instanceEntity *entity.DbInstance) error {
instanceEntity.Network = instanceEntity.GetNetwork()
dbConn, err := toDbInfo(instanceEntity, 0, "", "").Conn() dbConn, err := toDbInfo(instanceEntity, 0, "", "").Conn()
if err != nil { if err != nil {
return err return err

View File

@@ -20,7 +20,7 @@ type DbInfo struct {
Params string Params string
Database string Database string
TagPath string TagPath []string
SshTunnelMachineId int SshTunnelMachineId int
} }

View File

@@ -7,10 +7,9 @@ import (
type Db struct { type Db struct {
model.Model model.Model
Code string `orm:"column(code)" json:"code"`
Name string `orm:"column(name)" json:"name"` Name string `orm:"column(name)" json:"name"`
Database string `orm:"column(database)" json:"database"` Database string `orm:"column(database)" json:"database"`
Remark string `json:"remark"` Remark string `json:"remark"`
TagId uint64
TagPath string
InstanceId uint64 InstanceId uint64
} }

View File

@@ -17,6 +17,7 @@ type DbQuery struct {
Database string `orm:"column(database)" json:"database"` Database string `orm:"column(database)" json:"database"`
Remark string `json:"remark"` Remark string `json:"remark"`
Codes []string
TagIds []uint64 `orm:"column(tag_id)"` TagIds []uint64 `orm:"column(tag_id)"`
TagPath string `form:"tagPath"` TagPath string `form:"tagPath"`

View File

@@ -23,11 +23,9 @@ func (d *dbRepoImpl) GetDbList(condition *entity.DbQuery, pageParam *model.PageP
Joins("JOIN t_db_instance inst ON db.instance_id = inst.id"). Joins("JOIN t_db_instance inst ON db.instance_id = inst.id").
Eq("db.instance_id", condition.InstanceId). Eq("db.instance_id", condition.InstanceId).
Like("db.database", condition.Database). Like("db.database", condition.Database).
In("db.tag_id", condition.TagIds). In("db.code", condition.Codes).
RLike("db.tag_path", condition.TagPath).
Eq0("db."+model.DeletedColumn, model.ModelUndeleted). Eq0("db."+model.DeletedColumn, model.ModelUndeleted).
Eq0("inst."+model.DeletedColumn, model.ModelUndeleted). Eq0("inst."+model.DeletedColumn, model.ModelUndeleted)
OrderByAsc("db.tag_path")
return gormx.PageQuery(qd, pageParam, toEntity) return gormx.PageQuery(qd, pageParam, toEntity)
} }

View File

@@ -25,8 +25,6 @@ func InitDbRouter(router *gin.RouterGroup) {
// 获取数据库列表 // 获取数据库列表
req.NewGet("", d.Dbs), req.NewGet("", d.Dbs),
req.NewGet("/tags", d.DbTags),
req.NewPost("", d.Save).Log(req.NewLogSave("db-保存数据库信息")), req.NewPost("", d.Save).Log(req.NewLogSave("db-保存数据库信息")),
req.NewDelete(":dbId", d.DeleteDb).Log(req.NewLogSave("db-删除数据库信息")), req.NewDelete(":dbId", d.DeleteDb).Log(req.NewLogSave("db-删除数据库信息")),

View File

@@ -7,11 +7,10 @@ type MachineForm struct {
Port int `json:"port" binding:"required"` // 端口号 Port int `json:"port" binding:"required"` // 端口号
// 资产授权凭证信息列表 // 资产授权凭证信息列表
AuthCertId int `json:"authCertId"` AuthCertId int `json:"authCertId"`
TagId uint64 `json:"tagId" binding:"required"` TagId []uint64 `json:"tagId" binding:"required"`
TagPath string `json:"tagPath" binding:"required"` Username string `json:"username"`
Username string `json:"username"` Password string `json:"password"`
Password string `json:"password"`
Remark string `json:"remark"` Remark string `json:"remark"`
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id

View File

@@ -3,6 +3,7 @@ package api
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/machine/api/form" "mayfly-go/internal/machine/api/form"
"mayfly-go/internal/machine/api/vo" "mayfly-go/internal/machine/api/vo"
"mayfly-go/internal/machine/application" "mayfly-go/internal/machine/application"
@@ -17,34 +18,32 @@ import (
"mayfly-go/pkg/req" "mayfly-go/pkg/req"
"mayfly-go/pkg/utils/anyx" "mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/collx" "mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/ws" "mayfly-go/pkg/ws"
"os" "os"
"path" "path"
"sort"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
type Machine struct { type Machine struct {
MachineApp application.Machine MachineApp application.Machine
TagApp tagapp.TagTree MachineTermOpApp application.MachineTermOp
TagApp tagapp.TagTree
} }
func (m *Machine) Machines(rc *req.Ctx) { func (m *Machine) Machines(rc *req.Ctx) {
condition, pageParam := ginx.BindQueryAndPage(rc.GinCtx, new(entity.MachineQuery)) condition, pageParam := ginx.BindQueryAndPage(rc.GinCtx, new(entity.MachineQuery))
// 不存在可访问标签id即没有可操作数据 // 不存在可访问标签id即没有可操作数据
tagIds := m.TagApp.ListTagIdByAccountId(rc.GetLoginAccount().Id) codes := m.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeMachine, condition.TagPath)
if len(tagIds) == 0 { if len(codes) == 0 {
rc.ResData = model.EmptyPageResult[any]() rc.ResData = model.EmptyPageResult[any]()
return return
} }
condition.TagIds = tagIds condition.Codes = codes
res, err := m.MachineApp.GetMachineList(condition, pageParam, new([]*vo.MachineVO)) res, err := m.MachineApp.GetMachineList(condition, pageParam, new([]*vo.MachineVO))
biz.ErrIsNil(err) biz.ErrIsNil(err)
@@ -67,10 +66,6 @@ func (m *Machine) Machines(rc *req.Ctx) {
rc.ResData = res rc.ResData = res
} }
func (m *Machine) MachineTags(rc *req.Ctx) {
rc.ResData = m.TagApp.ListTagByAccountIdAndResource(rc.GetLoginAccount().Id, new(entity.Machine))
}
func (m *Machine) MachineStats(rc *req.Ctx) { func (m *Machine) MachineStats(rc *req.Ctx) {
cli, err := m.MachineApp.GetCli(GetMachineId(rc.GinCtx)) cli, err := m.MachineApp.GetCli(GetMachineId(rc.GinCtx))
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s") biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
@@ -85,7 +80,7 @@ func (m *Machine) SaveMachine(rc *req.Ctx) {
machineForm.Password = "******" machineForm.Password = "******"
rc.ReqParam = machineForm rc.ReqParam = machineForm
biz.ErrIsNil(m.MachineApp.Save(rc.MetaCtx, me)) biz.ErrIsNil(m.MachineApp.Save(rc.MetaCtx, me, machineForm.TagId...))
} }
func (m *Machine) TestConn(rc *req.Ctx) { func (m *Machine) TestConn(rc *req.Ctx) {
@@ -140,7 +135,7 @@ func (m *Machine) GetProcess(rc *req.Ctx) {
cli, err := m.MachineApp.GetCli(GetMachineId(rc.GinCtx)) cli, err := m.MachineApp.GetCli(GetMachineId(rc.GinCtx))
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s") biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath), "%s") biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath...), "%s")
res, err := cli.Run(cmd) res, err := cli.Run(cmd)
biz.ErrIsNilAppendErr(err, "获取进程信息失败: %s") biz.ErrIsNilAppendErr(err, "获取进程信息失败: %s")
@@ -154,7 +149,7 @@ func (m *Machine) KillProcess(rc *req.Ctx) {
cli, err := m.MachineApp.GetCli(GetMachineId(rc.GinCtx)) cli, err := m.MachineApp.GetCli(GetMachineId(rc.GinCtx))
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s") biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath), "%s") biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath...), "%s")
res, err := cli.Run("sudo kill -9 " + pid) res, err := cli.Run("sudo kill -9 " + pid)
biz.ErrIsNil(err, "终止进程失败: %s", res) biz.ErrIsNil(err, "终止进程失败: %s", res)
@@ -180,59 +175,35 @@ func (m *Machine) WsSSH(g *gin.Context) {
cli, err := m.MachineApp.GetCli(GetMachineId(g)) cli, err := m.MachineApp.GetCli(GetMachineId(g))
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s") biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath), "%s") biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath...), "%s")
cols := ginx.QueryInt(g, "cols", 80) cols := ginx.QueryInt(g, "cols", 80)
rows := ginx.QueryInt(g, "rows", 40) rows := ginx.QueryInt(g, "rows", 40)
var recorder *mcm.Recorder
if cli.Info.EnableRecorder == 1 {
now := time.Now()
// 回放文件路径为: 基础配置路径/机器id/操作日期/操作者账号/操作时间.cast
recPath := fmt.Sprintf("%s/%d/%s/%s", config.GetMachine().TerminalRecPath, cli.Info.Id, now.Format("20060102"), rc.GetLoginAccount().Username)
os.MkdirAll(recPath, 0766)
fileName := path.Join(recPath, fmt.Sprintf("%s.cast", now.Format("20060102_150405")))
f, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0766)
biz.ErrIsNilAppendErr(err, "创建终端回放记录文件失败: %s")
defer f.Close()
recorder = mcm.NewRecorder(f)
}
mts, err := mcm.NewTerminalSession(stringx.Rand(16), wsConn, cli, rows, cols, recorder)
biz.ErrIsNilAppendErr(err, "\033[1;31m连接失败: %s\033[0m")
// 记录系统操作日志 // 记录系统操作日志
rc.WithLog(req.NewLogSave("机器-终端操作")) rc.WithLog(req.NewLogSave("机器-终端操作"))
rc.ReqParam = cli.Info rc.ReqParam = cli.Info
req.LogHandler(rc) req.LogHandler(rc)
mts.Start() err = m.MachineTermOpApp.TermConn(rc.MetaCtx, cli, wsConn, rows, cols)
defer mts.Stop() biz.ErrIsNilAppendErr(err, "\033[1;31m连接失败: %s\033[0m")
} }
// 获取机器终端回放记录的相应文件夹名或文件内容 func (m *Machine) MachineTermOpRecords(rc *req.Ctx) {
func (m *Machine) MachineRecDirNames(rc *req.Ctx) { mid := GetMachineId(rc.GinCtx)
readPath := rc.GinCtx.Query("path") res, err := m.MachineTermOpApp.GetPageList(&entity.MachineTermOp{MachineId: mid}, ginx.GetPageParam(rc.GinCtx), new([]entity.MachineTermOp))
biz.NotEmpty(readPath, "path不能为空") biz.ErrIsNil(err)
path_ := path.Join(config.GetMachine().TerminalRecPath, readPath) rc.ResData = res
}
// 如果是读取文件内容,则读取对应回放记录文件内容,否则读取文件夹名列表。小小偷懒一会不想再加个接口 func (m *Machine) MachineTermOpRecord(rc *req.Ctx) {
isFile := rc.GinCtx.Query("isFile") recId, _ := strconv.Atoi(rc.GinCtx.Param("recId"))
if isFile == "1" { termOp, err := m.MachineTermOpApp.GetById(new(entity.MachineTermOp), uint64(recId))
bytes, err := os.ReadFile(path_) biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(err, "还未有相应终端操作记录: %s")
rc.ResData = base64.StdEncoding.EncodeToString(bytes)
return
}
files, err := os.ReadDir(path_) bytes, err := os.ReadFile(path.Join(config.GetMachine().TerminalRecPath, termOp.RecordFilePath))
biz.ErrIsNilAppendErr(err, "还未有相应终端操作记录: %s") biz.ErrIsNilAppendErr(err, "读取终端操作记录失败: %s")
var names []string rc.ResData = base64.StdEncoding.EncodeToString(bytes)
for _, f := range files {
names = append(names, f.Name())
}
sort.Sort(sort.Reverse(sort.StringSlice(names)))
rc.ResData = names
} }
func GetMachineId(g *gin.Context) uint64 { func GetMachineId(g *gin.Context) uint64 {

View File

@@ -63,6 +63,12 @@ func (m *MachineCronJob) GetRelateCronJobIds(rc *req.Ctx) {
rc.ResData = m.MachineCronJobApp.GetRelateMachineIds(uint64(ginx.QueryInt(rc.GinCtx, "machineId", -1))) rc.ResData = m.MachineCronJobApp.GetRelateMachineIds(uint64(ginx.QueryInt(rc.GinCtx, "machineId", -1)))
} }
func (m *MachineCronJob) RunCronJob(rc *req.Ctx) {
cronJobKey := ginx.PathParam(rc.GinCtx, "key")
biz.NotEmpty(cronJobKey, "cronJob key不能为空")
m.MachineCronJobApp.RunCronJob(cronJobKey)
}
func (m *MachineCronJob) CronJobExecs(rc *req.Ctx) { func (m *MachineCronJob) CronJobExecs(rc *req.Ctx) {
cond, pageParam := ginx.BindQueryAndPage[*entity.MachineCronJobExec](rc.GinCtx, new(entity.MachineCronJobExec)) cond, pageParam := ginx.BindQueryAndPage[*entity.MachineCronJobExec](rc.GinCtx, new(entity.MachineCronJobExec))
res, err := m.MachineCronJobApp.GetExecPageList(cond, pageParam, new([]entity.MachineCronJobExec)) res, err := m.MachineCronJobApp.GetExecPageList(cond, pageParam, new([]entity.MachineCronJobExec))

View File

@@ -69,7 +69,7 @@ func (m *MachineScript) RunMachineScript(rc *req.Ctx) {
} }
cli, err := m.MachineApp.GetCli(machineId) cli, err := m.MachineApp.GetCli(machineId)
biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s") biz.ErrIsNilAppendErr(err, "获取客户端连接失败: %s")
biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath), "%s") biz.ErrIsNilAppendErr(m.TagApp.CanAccess(rc.GetLoginAccount().Id, cli.Info.TagPath...), "%s")
res, err := cli.Run(script) res, err := cli.Run(script)
// 记录请求参数 // 记录请求参数

View File

@@ -13,6 +13,7 @@ type AuthCertBaseVO struct {
type MachineVO struct { type MachineVO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"` Name string `json:"name"`
Ip string `json:"ip"` Ip string `json:"ip"`
Port int `json:"port"` Port int `json:"port"`
@@ -28,8 +29,8 @@ type MachineVO struct {
ModifierId *int64 `json:"modifierId"` ModifierId *int64 `json:"modifierId"`
Remark *string `json:"remark"` Remark *string `json:"remark"`
EnableRecorder int8 `json:"enableRecorder"` EnableRecorder int8 `json:"enableRecorder"`
TagId uint64 `json:"tagId"` // TagId uint64 `json:"tagId"`
TagPath string `json:"tagPath"` // TagPath string `json:"tagPath"`
HasCli bool `json:"hasCli" gorm:"-"` HasCli bool `json:"hasCli" gorm:"-"`
Stat map[string]any `json:"stat" gorm:"-"` Stat map[string]any `json:"stat" gorm:"-"`

View File

@@ -2,12 +2,14 @@ package application
import ( import (
"mayfly-go/internal/machine/infrastructure/persistence" "mayfly-go/internal/machine/infrastructure/persistence"
tagapp "mayfly-go/internal/tag/application"
) )
var ( var (
machineApp Machine = newMachineApp( machineApp Machine = newMachineApp(
persistence.GetMachineRepo(), persistence.GetMachineRepo(),
GetAuthCertApp(), GetAuthCertApp(),
tagapp.GetTagTreeApp(),
) )
machineFileApp MachineFile = newMachineFileApp( machineFileApp MachineFile = newMachineFileApp(
@@ -28,6 +30,8 @@ var (
persistence.GetMachineCronJobExecRepo(), persistence.GetMachineCronJobExecRepo(),
GetMachineApp(), GetMachineApp(),
) )
machineTermOpApp MachineTermOp = newMachineTermOpApp(persistence.GetMachineTermOpRepo())
) )
func GetMachineApp() Machine { func GetMachineApp() Machine {
@@ -49,3 +53,7 @@ func GetAuthCertApp() AuthCert {
func GetMachineCronJobApp() MachineCronJob { func GetMachineCronJobApp() MachineCronJob {
return machineCropJobApp return machineCropJobApp
} }
func GetMachineTermOpApp() MachineTermOp {
return machineTermOpApp
}

View File

@@ -3,17 +3,20 @@ package application
import ( import (
"context" "context"
"fmt" "fmt"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/machine/api/vo" "mayfly-go/internal/machine/api/vo"
"mayfly-go/internal/machine/domain/entity" "mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/domain/repository" "mayfly-go/internal/machine/domain/repository"
"mayfly-go/internal/machine/infrastructure/cache" "mayfly-go/internal/machine/infrastructure/cache"
"mayfly-go/internal/machine/mcm" "mayfly-go/internal/machine/mcm"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/base" "mayfly-go/pkg/base"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/gormx" "mayfly-go/pkg/gormx"
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/scheduler" "mayfly-go/pkg/scheduler"
"mayfly-go/pkg/utils/stringx"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@@ -22,7 +25,7 @@ import (
type Machine interface { type Machine interface {
base.App[*entity.Machine] base.App[*entity.Machine]
Save(ctx context.Context, m *entity.Machine) error Save(ctx context.Context, m *entity.Machine, tagIds ...uint64) error
// 测试机器连接 // 测试机器连接
TestConn(me *entity.Machine) error TestConn(me *entity.Machine) error
@@ -30,8 +33,6 @@ type Machine interface {
// 调整机器状态 // 调整机器状态
ChangeStatus(ctx context.Context, id uint64, status int8) error ChangeStatus(ctx context.Context, id uint64, status int8) error
Count(condition *entity.MachineQuery) int64
Delete(ctx context.Context, id uint64) error Delete(ctx context.Context, id uint64) error
// 分页获取机器信息列表 // 分页获取机器信息列表
@@ -50,9 +51,10 @@ type Machine interface {
GetMachineStats(machineId uint64) (*mcm.Stats, error) GetMachineStats(machineId uint64) (*mcm.Stats, error)
} }
func newMachineApp(machineRepo repository.Machine, authCertApp AuthCert) Machine { func newMachineApp(machineRepo repository.Machine, authCertApp AuthCert, tagApp tagapp.TagTree) Machine {
app := &machineAppImpl{ app := &machineAppImpl{
authCertApp: authCertApp, authCertApp: authCertApp,
tagApp: tagApp,
} }
app.Repo = machineRepo app.Repo = machineRepo
return app return app
@@ -62,6 +64,8 @@ type machineAppImpl struct {
base.AppImpl[*entity.Machine, repository.Machine] base.AppImpl[*entity.Machine, repository.Machine]
authCertApp AuthCert authCertApp AuthCert
tagApp tagapp.TagTree
} }
// 分页获取机器信息列表 // 分页获取机器信息列表
@@ -69,11 +73,7 @@ func (m *machineAppImpl) GetMachineList(condition *entity.MachineQuery, pagePara
return m.GetRepo().GetMachineList(condition, pageParam, toEntity, orderBy...) return m.GetRepo().GetMachineList(condition, pageParam, toEntity, orderBy...)
} }
func (m *machineAppImpl) Count(condition *entity.MachineQuery) int64 { func (m *machineAppImpl) Save(ctx context.Context, me *entity.Machine, tagIds ...uint64) error {
return m.GetRepo().Count(condition)
}
func (m *machineAppImpl) Save(ctx context.Context, me *entity.Machine) error {
oldMachine := &entity.Machine{Ip: me.Ip, Port: me.Port, Username: me.Username} oldMachine := &entity.Machine{Ip: me.Ip, Port: me.Port, Username: me.Username}
if me.SshTunnelMachineId > 0 { if me.SshTunnelMachineId > 0 {
oldMachine.SshTunnelMachineId = me.SshTunnelMachineId oldMachine.SshTunnelMachineId = me.SshTunnelMachineId
@@ -85,9 +85,16 @@ func (m *machineAppImpl) Save(ctx context.Context, me *entity.Machine) error {
if err == nil { if err == nil {
return errorx.NewBiz("该机器信息已存在") return errorx.NewBiz("该机器信息已存在")
} }
resouceCode := stringx.Rand(16)
me.Code = resouceCode
// 新增机器,默认启用状态 // 新增机器,默认启用状态
me.Status = entity.MachineStatusEnable me.Status = entity.MachineStatusEnable
return m.Insert(ctx, me)
return m.Tx(ctx, func(ctx context.Context) error {
return m.Insert(ctx, me)
}, func(ctx context.Context) error {
return m.tagApp.RelateResource(ctx, resouceCode, consts.TagResourceTypeMachine, tagIds)
})
} }
// 如果存在该库,则校验修改的库是否为该库 // 如果存在该库,则校验修改的库是否为该库
@@ -97,7 +104,11 @@ func (m *machineAppImpl) Save(ctx context.Context, me *entity.Machine) error {
// 关闭连接 // 关闭连接
mcm.DeleteCli(me.Id) mcm.DeleteCli(me.Id)
return m.UpdateById(ctx, me) return m.Tx(ctx, func(ctx context.Context) error {
return m.UpdateById(ctx, me)
}, func(ctx context.Context) error {
return m.tagApp.RelateResource(ctx, oldMachine.Code, consts.TagResourceTypeMachine, tagIds)
})
} }
func (m *machineAppImpl) TestConn(me *entity.Machine) error { func (m *machineAppImpl) TestConn(me *entity.Machine) error {
@@ -214,7 +225,7 @@ func (m *machineAppImpl) toMachineInfo(me *entity.Machine) (*mcm.MachineInfo, er
mi.Ip = me.Ip mi.Ip = me.Ip
mi.Port = me.Port mi.Port = me.Port
mi.Username = me.Username mi.Username = me.Username
mi.TagPath = me.TagPath mi.TagPath = m.tagApp.ListTagPathByResource(consts.TagResourceTypeMachine, me.Code)
mi.EnableRecorder = me.EnableRecorder mi.EnableRecorder = me.EnableRecorder
if me.UseAuthCert() { if me.UseAuthCert() {

View File

@@ -44,6 +44,10 @@ type MachineCronJob interface {
// 初始化计划任务 // 初始化计划任务
InitCronJob() InitCronJob()
// 执行cron job
// @param key cron job key
RunCronJob(key string)
} }
type machineCropJobAppImpl struct { type machineCropJobAppImpl struct {
@@ -183,30 +187,7 @@ func (m *machineCropJobAppImpl) InitCronJob() {
} }
} }
func (m *machineCropJobAppImpl) addCronJob(mcj *entity.MachineCronJob) { func (m *machineCropJobAppImpl) RunCronJob(key string) {
var key string
isDisable := mcj.Status == entity.MachineCronJobStatusDisable
if mcj.Id == 0 {
key = stringx.Rand(16)
mcj.Key = key
if isDisable {
return
}
} else {
key = mcj.Key
}
if isDisable {
scheduler.RemoveByKey(key)
return
}
scheduler.AddFunByKey(key, mcj.Cron, func() {
go m.runCronJob(key)
})
}
func (m *machineCropJobAppImpl) runCronJob(key string) {
// 简单使用redis分布式锁防止多实例同一时刻重复执行 // 简单使用redis分布式锁防止多实例同一时刻重复执行
if lock := rediscli.NewLock(key, 30*time.Second); lock != nil { if lock := rediscli.NewLock(key, 30*time.Second); lock != nil {
if !lock.Lock() { if !lock.Lock() {
@@ -229,6 +210,28 @@ func (m *machineCropJobAppImpl) runCronJob(key string) {
} }
} }
func (m *machineCropJobAppImpl) addCronJob(mcj *entity.MachineCronJob) {
var key string
isDisable := mcj.Status == entity.MachineCronJobStatusDisable
if mcj.Id == 0 {
key = stringx.Rand(16)
mcj.Key = key
if isDisable {
return
}
} else {
key = mcj.Key
}
if isDisable {
scheduler.RemoveByKey(key)
return
}
scheduler.AddFunByKey(key, mcj.Cron, func() {
go m.RunCronJob(key)
})
}
func (m *machineCropJobAppImpl) runCronJob0(mid uint64, cronJob *entity.MachineCronJob) { func (m *machineCropJobAppImpl) runCronJob0(mid uint64, cronJob *entity.MachineCronJob) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {

View File

@@ -0,0 +1,93 @@
package application
import (
"context"
"fmt"
"mayfly-go/internal/machine/config"
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/domain/repository"
"mayfly-go/internal/machine/mcm"
"mayfly-go/pkg/base"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/stringx"
"os"
"path"
"time"
"github.com/gorilla/websocket"
)
type MachineTermOp interface {
base.App[*entity.MachineTermOp]
// 终端连接操作
TermConn(ctx context.Context, cli *mcm.Cli, wsConn *websocket.Conn, rows, cols int) error
GetPageList(condition *entity.MachineTermOp, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
}
func newMachineTermOpApp(machineTermOpRepo repository.MachineTermOp) MachineTermOp {
return &machineTermOpAppImpl{
base.AppImpl[*entity.MachineTermOp, repository.MachineTermOp]{Repo: machineTermOpRepo},
}
}
type machineTermOpAppImpl struct {
base.AppImpl[*entity.MachineTermOp, repository.MachineTermOp]
}
func (a *machineTermOpAppImpl) TermConn(ctx context.Context, cli *mcm.Cli, wsConn *websocket.Conn, rows, cols int) error {
var recorder *mcm.Recorder
var termOpRecord *entity.MachineTermOp
// 开启终端操作记录
if cli.Info.EnableRecorder == 1 {
now := time.Now()
la := contextx.GetLoginAccount(ctx)
termOpRecord = new(entity.MachineTermOp)
termOpRecord.CreateTime = &now
termOpRecord.Creator = la.Username
termOpRecord.CreatorId = la.Id
termOpRecord.MachineId = cli.Info.Id
termOpRecord.Username = cli.Info.Username
// 回放文件路径为: 基础配置路径/操作日期(202301)/day/hour/randstr.cast
recRelPath := path.Join(now.Format("200601"), fmt.Sprintf("%d", now.Day()), fmt.Sprintf("%d", now.Hour()))
// 文件绝对路径
recAbsPath := path.Join(config.GetMachine().TerminalRecPath, recRelPath)
os.MkdirAll(recAbsPath, 0766)
filename := fmt.Sprintf("%s.cast", stringx.RandByChars(18, stringx.LowerChars))
f, err := os.OpenFile(path.Join(recAbsPath, filename), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0766)
if err != nil {
return errorx.NewBiz("创建终端回放记录文件失败: %s", err.Error())
}
defer f.Close()
termOpRecord.RecordFilePath = path.Join(recRelPath, filename)
recorder = mcm.NewRecorder(f)
}
mts, err := mcm.NewTerminalSession(stringx.Rand(16), wsConn, cli, rows, cols, recorder)
if err != nil {
return err
}
mts.Start()
defer mts.Stop()
if termOpRecord != nil {
now := time.Now()
termOpRecord.EndTime = &now
return a.Insert(ctx, termOpRecord)
}
return nil
}
func (a *machineTermOpAppImpl) GetPageList(condition *entity.MachineTermOp, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return a.GetRepo().GetPageList(condition, pageParam, toEntity)
}

View File

@@ -8,14 +8,13 @@ import (
type Machine struct { type Machine struct {
model.Model model.Model
Code string `json:"code"`
Name string `json:"name"` Name string `json:"name"`
Ip string `json:"ip"` // IP地址 Ip string `json:"ip"` // IP地址
Port int `json:"port"` // 端口号 Port int `json:"port"` // 端口号
Username string `json:"username"` // 用户名 Username string `json:"username"` // 用户名
Password string `json:"password"` // 密码 Password string `json:"password"` // 密码
AuthCertId int `json:"authCertId"` // 授权凭证id AuthCertId int `json:"authCertId"` // 授权凭证id
TagId uint64
TagPath string
Status int8 `json:"status"` // 状态 1:启用2:停用 Status int8 `json:"status"` // 状态 1:启用2:停用
Remark string `json:"remark"` // 备注 Remark string `json:"remark"` // 备注
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id

View File

@@ -0,0 +1,19 @@
package entity
import (
"mayfly-go/pkg/model"
"time"
)
type MachineTermOp struct {
model.DeletedModel
MachineId uint64 `json:"machineId"`
Username string `json:"username"`
RecordFilePath string `json:"recordFilePath"` // 回放文件路径
CreateTime *time.Time `json:"createTime"`
CreatorId uint64 `json:"creatorId"`
Creator string `json:"creator"`
EndTime *time.Time `json:"endTime"`
}

View File

@@ -6,7 +6,8 @@ type MachineQuery struct {
Status int8 `json:"status" form:"status"` Status int8 `json:"status" form:"status"`
Ip string `json:"ip" form:"ip"` // IP地址 Ip string `json:"ip" form:"ip"` // IP地址
TagPath string `json:"tagPath" form:"tagPath"` TagPath string `json:"tagPath" form:"tagPath"`
TagIds []uint64
Codes []string
} }
type AuthCertQuery struct { type AuthCertQuery struct {

View File

@@ -12,6 +12,4 @@ type Machine interface {
// 分页获取机器信息列表 // 分页获取机器信息列表
GetMachineList(condition *entity.MachineQuery, pageParam *model.PageParam, toEntity *[]*vo.MachineVO, orderBy ...string) (*model.PageResult[*[]*vo.MachineVO], error) GetMachineList(condition *entity.MachineQuery, pageParam *model.PageParam, toEntity *[]*vo.MachineVO, orderBy ...string) (*model.PageResult[*[]*vo.MachineVO], error)
Count(condition *entity.MachineQuery) int64
} }

View File

@@ -0,0 +1,14 @@
package repository
import (
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/pkg/base"
"mayfly-go/pkg/model"
)
type MachineTermOp interface {
base.Repo[*entity.MachineTermOp]
// 分页获取机器终端执行记录列表
GetPageList(condition *entity.MachineTermOp, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
}

View File

@@ -26,9 +26,7 @@ func (m *machineRepoImpl) GetMachineList(condition *entity.MachineQuery, pagePar
Eq("status", condition.Status). Eq("status", condition.Status).
Like("ip", condition.Ip). Like("ip", condition.Ip).
Like("name", condition.Name). Like("name", condition.Name).
In("tag_id", condition.TagIds). In("code", condition.Codes)
RLike("tag_path", condition.TagPath).
OrderByAsc("tag_path")
if condition.Ids != "" { if condition.Ids != "" {
// ,分割id转为id数组 // ,分割id转为id数组
@@ -40,12 +38,3 @@ func (m *machineRepoImpl) GetMachineList(condition *entity.MachineQuery, pagePar
return gormx.PageQuery(qd, pageParam, toEntity) return gormx.PageQuery(qd, pageParam, toEntity)
} }
func (m *machineRepoImpl) Count(condition *entity.MachineQuery) int64 {
where := make(map[string]any)
if len(condition.TagIds) > 0 {
where["tag_id"] = condition.TagIds
}
return m.CountByCond(where)
}

View File

@@ -0,0 +1,22 @@
package persistence
import (
"mayfly-go/internal/machine/domain/entity"
"mayfly-go/internal/machine/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/gormx"
"mayfly-go/pkg/model"
)
type machineTermOpRepoImpl struct {
base.RepoImpl[*entity.MachineTermOp]
}
func newMachineTermOpRepoImpl() repository.MachineTermOp {
return &machineTermOpRepoImpl{base.RepoImpl[*entity.MachineTermOp]{M: new(entity.MachineTermOp)}}
}
func (m *machineTermOpRepoImpl) GetPageList(condition *entity.MachineTermOp, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
qd := gormx.NewQuery(condition).WithCondModel(condition).WithOrderBy(orderBy...)
return gormx.PageQuery(qd, pageParam, toEntity)
}

View File

@@ -10,6 +10,7 @@ var (
machineCropJobRepo repository.MachineCronJob = newMachineCronJobRepo() machineCropJobRepo repository.MachineCronJob = newMachineCronJobRepo()
machineCropJobExecRepo repository.MachineCronJobExec = newMachineCronJobExecRepo() machineCropJobExecRepo repository.MachineCronJobExec = newMachineCronJobExecRepo()
machineCronJobRelateRepo repository.MachineCronJobRelate = newMachineCropJobRelateRepo() machineCronJobRelateRepo repository.MachineCronJobRelate = newMachineCropJobRelateRepo()
machineTermOpRepo repository.MachineTermOp = newMachineTermOpRepoImpl()
) )
func GetMachineRepo() repository.Machine { func GetMachineRepo() repository.Machine {
@@ -39,3 +40,7 @@ func GetMachineCronJobExecRepo() repository.MachineCronJobExec {
func GetMachineCronJobRelateRepo() repository.MachineCronJobRelate { func GetMachineCronJobRelateRepo() repository.MachineCronJobRelate {
return machineCronJobRelateRepo return machineCronJobRelateRepo
} }
func GetMachineTermOpRepo() repository.MachineTermOp {
return machineTermOpRepo
}

View File

@@ -25,7 +25,7 @@ type MachineInfo struct {
SshTunnelMachine *MachineInfo `json:"-"` // ssh隧道机器 SshTunnelMachine *MachineInfo `json:"-"` // ssh隧道机器
EnableRecorder int8 `json:"-"` // 是否启用终端回放记录 EnableRecorder int8 `json:"-"` // 是否启用终端回放记录
TagPath string `json:"tagPath"` TagPath []string `json:"tagPath"`
} }
func (m *MachineInfo) UseSshTunnel() bool { func (m *MachineInfo) UseSshTunnel() bool {

View File

@@ -140,6 +140,7 @@ type WsMsg struct {
Rows int `json:"rows"` Rows int `json:"rows"`
} }
// 接收客户端ws发送过来的消息并写入终端会话中。
func (ts *TerminalSession) receiveWsMsg() { func (ts *TerminalSession) receiveWsMsg() {
wsConn := ts.wsConn wsConn := ts.wsConn
for { for {

View File

@@ -11,8 +11,9 @@ import (
func InitMachineRouter(router *gin.RouterGroup) { func InitMachineRouter(router *gin.RouterGroup) {
m := &api.Machine{ m := &api.Machine{
MachineApp: application.GetMachineApp(), MachineApp: application.GetMachineApp(),
TagApp: tagapp.GetTagTreeApp(), MachineTermOpApp: application.GetMachineTermOpApp(),
TagApp: tagapp.GetTagTreeApp(),
} }
machines := router.Group("machines") machines := router.Group("machines")
@@ -22,8 +23,6 @@ func InitMachineRouter(router *gin.RouterGroup) {
reqs := [...]*req.Conf{ reqs := [...]*req.Conf{
req.NewGet("", m.Machines), req.NewGet("", m.Machines),
req.NewGet("/tags", m.MachineTags),
req.NewGet(":machineId/stats", m.MachineStats), req.NewGet(":machineId/stats", m.MachineStats),
req.NewGet(":machineId/process", m.GetProcess), req.NewGet(":machineId/process", m.GetProcess),
@@ -40,8 +39,11 @@ func InitMachineRouter(router *gin.RouterGroup) {
req.NewDelete(":machineId/close-cli", m.CloseCli).Log(req.NewLogSave("关闭机器客户端")).RequiredPermissionCode("machine:close-cli"), req.NewDelete(":machineId/close-cli", m.CloseCli).Log(req.NewLogSave("关闭机器客户端")).RequiredPermissionCode("machine:close-cli"),
// 获取机器终端回放记录的相应文件夹名或文件名,目前具有保存机器信息的权限标识才有权限查看终端回放 // 获取机器终端回放记录列表,目前具有保存机器信息的权限标识才有权限查看终端回放
req.NewGet("rec/names", m.MachineRecDirNames).RequiredPermission(saveMachineP), req.NewGet(":machineId/term-recs", m.MachineTermOpRecords).RequiredPermission(saveMachineP),
// 获取机器终端回放记录
req.NewGet(":machineId/term-recs/:recId", m.MachineTermOpRecord).RequiredPermission(saveMachineP),
} }
req.BatchSetGroup(machines, reqs[:]) req.BatchSetGroup(machines, reqs[:])

View File

@@ -26,6 +26,8 @@ func InitMachineCronJobRouter(router *gin.RouterGroup) {
req.NewDelete(":ids", cj.Delete).Log(req.NewLogSave("删除机器计划任务")), req.NewDelete(":ids", cj.Delete).Log(req.NewLogSave("删除机器计划任务")),
req.NewPost("/run/:key", cj.RunCronJob).Log(req.NewLogSave("手动执行计划任务")),
req.NewGet("/execs", cj.CronJobExecs), req.NewGet("/execs", cj.CronJobExecs),
} }

View File

@@ -1,12 +1,11 @@
package form package form
type Mongo struct { type Mongo struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Uri string `binding:"required" json:"uri"` Uri string `binding:"required" json:"uri"`
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
Name string `binding:"required" json:"name"` Name string `binding:"required" json:"name"`
TagId uint64 `binding:"required" json:"tagId"` TagId []uint64 `binding:"required" json:"tagId"`
TagPath string `binding:"required" json:"tagPath"`
} }
type MongoCommand struct { type MongoCommand struct {

View File

@@ -2,6 +2,7 @@ package api
import ( import (
"context" "context"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/mongo/api/form" "mayfly-go/internal/mongo/api/form"
"mayfly-go/internal/mongo/application" "mayfly-go/internal/mongo/application"
"mayfly-go/internal/mongo/domain/entity" "mayfly-go/internal/mongo/domain/entity"
@@ -30,22 +31,18 @@ func (m *Mongo) Mongos(rc *req.Ctx) {
queryCond, page := ginx.BindQueryAndPage[*entity.MongoQuery](rc.GinCtx, new(entity.MongoQuery)) queryCond, page := ginx.BindQueryAndPage[*entity.MongoQuery](rc.GinCtx, new(entity.MongoQuery))
// 不存在可访问标签id即没有可操作数据 // 不存在可访问标签id即没有可操作数据
tagIds := m.TagApp.ListTagIdByAccountId(rc.GetLoginAccount().Id) codes := m.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeMongo, queryCond.TagPath)
if len(tagIds) == 0 { if len(codes) == 0 {
rc.ResData = model.EmptyPageResult[any]() rc.ResData = model.EmptyPageResult[any]()
return return
} }
queryCond.TagIds = tagIds queryCond.Codes = codes
res, err := m.MongoApp.GetPageList(queryCond, page, new([]entity.Mongo)) res, err := m.MongoApp.GetPageList(queryCond, page, new([]entity.Mongo))
biz.ErrIsNil(err) biz.ErrIsNil(err)
rc.ResData = res rc.ResData = res
} }
func (m *Mongo) MongoTags(rc *req.Ctx) {
rc.ResData = m.TagApp.ListTagByAccountIdAndResource(rc.GetLoginAccount().Id, new(entity.Mongo))
}
func (m *Mongo) TestConn(rc *req.Ctx) { func (m *Mongo) TestConn(rc *req.Ctx) {
form := &form.Mongo{} form := &form.Mongo{}
mongo := ginx.BindJsonAndCopyTo[*entity.Mongo](rc.GinCtx, form, new(entity.Mongo)) mongo := ginx.BindJsonAndCopyTo[*entity.Mongo](rc.GinCtx, form, new(entity.Mongo))
@@ -63,7 +60,7 @@ func (m *Mongo) Save(rc *req.Ctx) {
}(form.Uri) }(form.Uri)
rc.ReqParam = form rc.ReqParam = form
biz.ErrIsNil(m.MongoApp.Save(rc.MetaCtx, mongo)) biz.ErrIsNil(m.MongoApp.Save(rc.MetaCtx, mongo, form.TagId...))
} }
func (m *Mongo) DeleteMongo(rc *req.Ctx) { func (m *Mongo) DeleteMongo(rc *req.Ctx) {

View File

@@ -1,9 +1,12 @@
package application package application
import "mayfly-go/internal/mongo/infrastructure/persistence" import (
"mayfly-go/internal/mongo/infrastructure/persistence"
tagapp "mayfly-go/internal/tag/application"
)
var ( var (
mongoApp Mongo = newMongoAppImpl(persistence.GetMongoRepo()) mongoApp Mongo = newMongoAppImpl(persistence.GetMongoRepo(), tagapp.GetTagTreeApp())
) )
func GetMongoApp() Mongo { func GetMongoApp() Mongo {

View File

@@ -2,12 +2,15 @@ package application
import ( import (
"context" "context"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/mongo/domain/entity" "mayfly-go/internal/mongo/domain/entity"
"mayfly-go/internal/mongo/domain/repository" "mayfly-go/internal/mongo/domain/repository"
"mayfly-go/internal/mongo/mgm" "mayfly-go/internal/mongo/mgm"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/base" "mayfly-go/pkg/base"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/utils/stringx"
) )
type Mongo interface { type Mongo interface {
@@ -16,11 +19,9 @@ type Mongo interface {
// 分页获取机器脚本信息列表 // 分页获取机器脚本信息列表
GetPageList(condition *entity.MongoQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) GetPageList(condition *entity.MongoQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
Count(condition *entity.MongoQuery) int64
TestConn(entity *entity.Mongo) error TestConn(entity *entity.Mongo) error
Save(ctx context.Context, entity *entity.Mongo) error Save(ctx context.Context, entity *entity.Mongo, tagIds ...uint64) error
// 删除数据库信息 // 删除数据库信息
Delete(ctx context.Context, id uint64) error Delete(ctx context.Context, id uint64) error
@@ -30,14 +31,18 @@ type Mongo interface {
GetMongoConn(id uint64) (*mgm.MongoConn, error) GetMongoConn(id uint64) (*mgm.MongoConn, error)
} }
func newMongoAppImpl(mongoRepo repository.Mongo) Mongo { func newMongoAppImpl(mongoRepo repository.Mongo, tagApp tagapp.TagTree) Mongo {
return &mongoAppImpl{ app := &mongoAppImpl{
base.AppImpl[*entity.Mongo, repository.Mongo]{Repo: mongoRepo}, tagApp: tagApp,
} }
app.Repo = mongoRepo
return app
} }
type mongoAppImpl struct { type mongoAppImpl struct {
base.AppImpl[*entity.Mongo, repository.Mongo] base.AppImpl[*entity.Mongo, repository.Mongo]
tagApp tagapp.TagTree
} }
// 分页获取数据库信息列表 // 分页获取数据库信息列表
@@ -45,10 +50,6 @@ func (d *mongoAppImpl) GetPageList(condition *entity.MongoQuery, pageParam *mode
return d.GetRepo().GetList(condition, pageParam, toEntity, orderBy...) return d.GetRepo().GetList(condition, pageParam, toEntity, orderBy...)
} }
func (d *mongoAppImpl) Count(condition *entity.MongoQuery) int64 {
return d.GetRepo().Count(condition)
}
func (d *mongoAppImpl) Delete(ctx context.Context, id uint64) error { func (d *mongoAppImpl) Delete(ctx context.Context, id uint64) error {
mgm.CloseConn(id) mgm.CloseConn(id)
return d.GetRepo().DeleteById(ctx, id) return d.GetRepo().DeleteById(ctx, id)
@@ -63,22 +64,45 @@ func (d *mongoAppImpl) TestConn(me *entity.Mongo) error {
return nil return nil
} }
func (d *mongoAppImpl) Save(ctx context.Context, m *entity.Mongo) error { func (d *mongoAppImpl) Save(ctx context.Context, m *entity.Mongo, tagIds ...uint64) error {
oldMongo := &entity.Mongo{Name: m.Name}
err := d.GetBy(oldMongo)
if m.Id == 0 { if m.Id == 0 {
return d.GetRepo().Insert(ctx, m) if err == nil {
return errorx.NewBiz("该名称已存在")
}
resouceCode := stringx.Rand(16)
m.Code = resouceCode
return d.Tx(ctx, func(ctx context.Context) error {
return d.Insert(ctx, m)
}, func(ctx context.Context) error {
return d.tagApp.RelateResource(ctx, resouceCode, consts.TagResourceTypeMongo, tagIds)
})
}
// 如果存在该库,则校验修改的库是否为该库
if err == nil && oldMongo.Id != m.Id {
return errorx.NewBiz("该名称已存在")
} }
// 先关闭连接 // 先关闭连接
mgm.CloseConn(m.Id) mgm.CloseConn(m.Id)
return d.GetRepo().UpdateById(ctx, m) return d.Tx(ctx, func(ctx context.Context) error {
return d.UpdateById(ctx, m)
}, func(ctx context.Context) error {
return d.tagApp.RelateResource(ctx, oldMongo.Code, consts.TagResourceTypeMongo, tagIds)
})
} }
func (d *mongoAppImpl) GetMongoConn(id uint64) (*mgm.MongoConn, error) { func (d *mongoAppImpl) GetMongoConn(id uint64) (*mgm.MongoConn, error) {
return mgm.GetMongoConn(id, func() (*mgm.MongoInfo, error) { return mgm.GetMongoConn(id, func() (*mgm.MongoInfo, error) {
mongo, err := d.GetById(new(entity.Mongo), id) me, err := d.GetById(new(entity.Mongo), id)
if err != nil { if err != nil {
return nil, errorx.NewBiz("mongo信息不存在") return nil, errorx.NewBiz("mongo信息不存在")
} }
return mongo.ToMongoInfo(), nil return me.ToMongoInfo(d.tagApp.ListTagPathByResource(consts.TagResourceTypeMongo, me.Code)...), nil
}) })
} }

View File

@@ -9,16 +9,16 @@ import (
type Mongo struct { type Mongo struct {
model.Model model.Model
Code string `orm:"column(code)" json:"code"`
Name string `orm:"column(name)" json:"name"` Name string `orm:"column(name)" json:"name"`
Uri string `orm:"column(uri)" json:"uri"` Uri string `orm:"column(uri)" json:"uri"`
SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
TagId uint64 `json:"tagId"`
TagPath string `json:"tagPath"`
} }
// 转换为mongoInfo进行连接 // 转换为mongoInfo进行连接
func (me *Mongo) ToMongoInfo() *mgm.MongoInfo { func (me *Mongo) ToMongoInfo(tagPath ...string) *mgm.MongoInfo {
mongoInfo := new(mgm.MongoInfo) mongoInfo := new(mgm.MongoInfo)
structx.Copy(mongoInfo, me) structx.Copy(mongoInfo, me)
mongoInfo.TagPath = tagPath
return mongoInfo return mongoInfo
} }

View File

@@ -10,5 +10,5 @@ type MongoQuery struct {
SshTunnelMachineId uint64 // ssh隧道机器id SshTunnelMachineId uint64 // ssh隧道机器id
TagPath string `json:"tagPath" form:"tagPath"` TagPath string `json:"tagPath" form:"tagPath"`
TagIds []uint64 Codes []string
} }

View File

@@ -11,6 +11,4 @@ type Mongo interface {
// 分页获取列表 // 分页获取列表
GetList(condition *entity.MongoQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) GetList(condition *entity.MongoQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
Count(condition *entity.MongoQuery) int64
} }

View File

@@ -20,16 +20,6 @@ func newMongoRepo() repository.Mongo {
func (d *mongoRepoImpl) GetList(condition *entity.MongoQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) { func (d *mongoRepoImpl) GetList(condition *entity.MongoQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
qd := gormx.NewQuery(new(entity.Mongo)). qd := gormx.NewQuery(new(entity.Mongo)).
Like("name", condition.Name). Like("name", condition.Name).
In("tag_id", condition.TagIds). In("code", condition.Codes)
RLike("tag_path", condition.TagPath).
OrderByAsc("tag_path")
return gormx.PageQuery(qd, pageParam, toEntity) return gormx.PageQuery(qd, pageParam, toEntity)
} }
func (d *mongoRepoImpl) Count(condition *entity.MongoQuery) int64 {
where := make(map[string]any)
if len(condition.TagIds) > 0 {
where["tag_id"] = condition.TagIds
}
return gormx.CountByCond(new(entity.Mongo), where)
}

View File

@@ -21,8 +21,8 @@ type MongoInfo struct {
Uri string `json:"-"` Uri string `json:"-"`
TagPath string `json:"tagPath"` TagPath []string `json:"tagPath"`
SshTunnelMachineId int `json:"-"` // ssh隧道机器id SshTunnelMachineId int `json:"-"` // ssh隧道机器id
} }
func (mi *MongoInfo) Conn() (*MongoConn, error) { func (mi *MongoInfo) Conn() (*MongoConn, error) {

View File

@@ -23,8 +23,6 @@ func InitMongoRouter(router *gin.RouterGroup) {
// 获取所有mongo列表 // 获取所有mongo列表
req.NewGet("", ma.Mongos), req.NewGet("", ma.Mongos),
req.NewGet("/tags", ma.MongoTags),
req.NewPost("/test-conn", ma.TestConn), req.NewPost("/test-conn", ma.TestConn),
req.NewPost("", ma.Save).Log(req.NewLogSave("mongo-保存信息")), req.NewPost("", ma.Save).Log(req.NewLogSave("mongo-保存信息")),

View File

@@ -1,17 +1,16 @@
package form package form
type Redis struct { type Redis struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Host string `json:"host" binding:"required"` Host string `json:"host" binding:"required"`
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
Mode string `json:"mode"` Mode string `json:"mode"`
Db string `json:"db"` Db string `json:"db"`
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
TagId uint64 `binding:"required" json:"tagId"` TagId []uint64 `binding:"required" json:"tagId"`
TagPath string `binding:"required" json:"tagPath"` Remark string `json:"remark"`
Remark string `json:"remark"`
} }
type Rename struct { type Rename struct {

View File

@@ -2,6 +2,7 @@ package api
import ( import (
"context" "context"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/redis/api/form" "mayfly-go/internal/redis/api/form"
"mayfly-go/internal/redis/api/vo" "mayfly-go/internal/redis/api/vo"
"mayfly-go/internal/redis/application" "mayfly-go/internal/redis/application"
@@ -31,22 +32,18 @@ func (r *Redis) RedisList(rc *req.Ctx) {
queryCond, page := ginx.BindQueryAndPage[*entity.RedisQuery](rc.GinCtx, new(entity.RedisQuery)) queryCond, page := ginx.BindQueryAndPage[*entity.RedisQuery](rc.GinCtx, new(entity.RedisQuery))
// 不存在可访问标签id即没有可操作数据 // 不存在可访问标签id即没有可操作数据
tagIds := r.TagApp.ListTagIdByAccountId(rc.GetLoginAccount().Id) codes := r.TagApp.GetAccountResourceCodes(rc.GetLoginAccount().Id, consts.TagResourceTypeRedis, queryCond.TagPath)
if len(tagIds) == 0 { if len(codes) == 0 {
rc.ResData = model.EmptyPageResult[any]() rc.ResData = model.EmptyPageResult[any]()
return return
} }
queryCond.TagIds = tagIds queryCond.Codes = codes
res, err := r.RedisApp.GetPageList(queryCond, page, new([]vo.Redis)) res, err := r.RedisApp.GetPageList(queryCond, page, new([]vo.Redis))
biz.ErrIsNil(err) biz.ErrIsNil(err)
rc.ResData = res rc.ResData = res
} }
func (r *Redis) RedisTags(rc *req.Ctx) {
rc.ResData = r.TagApp.ListTagByAccountIdAndResource(rc.GetLoginAccount().Id, new(entity.Redis))
}
func (r *Redis) TestConn(rc *req.Ctx) { func (r *Redis) TestConn(rc *req.Ctx) {
form := &form.Redis{} form := &form.Redis{}
redis := ginx.BindJsonAndCopyTo[*entity.Redis](rc.GinCtx, form, new(entity.Redis)) redis := ginx.BindJsonAndCopyTo[*entity.Redis](rc.GinCtx, form, new(entity.Redis))
@@ -72,7 +69,7 @@ func (r *Redis) Save(rc *req.Ctx) {
form.Password = "****" form.Password = "****"
rc.ReqParam = form rc.ReqParam = form
biz.ErrIsNil(r.RedisApp.Save(rc.MetaCtx, redis)) biz.ErrIsNil(r.RedisApp.Save(rc.MetaCtx, redis, form.TagId...))
} }
// 获取redis实例密码由于数据库是加密存储故提供该接口展示原文密码 // 获取redis实例密码由于数据库是加密存储故提供该接口展示原文密码
@@ -229,7 +226,7 @@ func (r *Redis) checkKeyAndGetRedisConn(rc *req.Ctx) (*rdm.RedisConn, string) {
func (r *Redis) getRedisConn(rc *req.Ctx) *rdm.RedisConn { func (r *Redis) getRedisConn(rc *req.Ctx) *rdm.RedisConn {
ri, err := r.RedisApp.GetRedisConn(getIdAndDbNum(rc.GinCtx)) ri, err := r.RedisApp.GetRedisConn(getIdAndDbNum(rc.GinCtx))
biz.ErrIsNil(err) biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.GetLoginAccount().Id, ri.Info.TagPath), "%s") biz.ErrIsNilAppendErr(r.TagApp.CanAccess(rc.GetLoginAccount().Id, ri.Info.TagPath...), "%s")
return ri return ri
} }

View File

@@ -4,14 +4,13 @@ import "time"
type Redis struct { type Redis struct {
Id *int64 `json:"id"` Id *int64 `json:"id"`
Code *string `json:"code"`
Name *string `json:"name"` Name *string `json:"name"`
Host *string `json:"host"` Host *string `json:"host"`
Db string `json:"db"` Db string `json:"db"`
Mode *string `json:"mode"` Mode *string `json:"mode"`
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
Remark *string `json:"remark"` Remark *string `json:"remark"`
TagId *uint64 `json:"tagId"`
TagPath *string `json:"tagPath"`
CreateTime *time.Time `json:"createTime"` CreateTime *time.Time `json:"createTime"`
Creator *string `json:"creator"` Creator *string `json:"creator"`
CreatorId *int64 `json:"creatorId"` CreatorId *int64 `json:"creatorId"`

View File

@@ -1,9 +1,12 @@
package application package application
import "mayfly-go/internal/redis/infrastructure/persistence" import (
"mayfly-go/internal/redis/infrastructure/persistence"
tagapp "mayfly-go/internal/tag/application"
)
var ( var (
redisApp Redis = newRedisApp(persistence.GetRedisRepo()) redisApp Redis = newRedisApp(persistence.GetRedisRepo(), tagapp.GetTagTreeApp())
) )
func GetRedisApp() Redis { func GetRedisApp() Redis {

View File

@@ -2,12 +2,15 @@ package application
import ( import (
"context" "context"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/redis/domain/entity" "mayfly-go/internal/redis/domain/entity"
"mayfly-go/internal/redis/domain/repository" "mayfly-go/internal/redis/domain/repository"
"mayfly-go/internal/redis/rdm" "mayfly-go/internal/redis/rdm"
tagapp "mayfly-go/internal/tag/application"
"mayfly-go/pkg/base" "mayfly-go/pkg/base"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/utils/stringx"
"strconv" "strconv"
"strings" "strings"
) )
@@ -18,12 +21,10 @@ type Redis interface {
// 分页获取机器脚本信息列表 // 分页获取机器脚本信息列表
GetPageList(condition *entity.RedisQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) GetPageList(condition *entity.RedisQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
Count(condition *entity.RedisQuery) int64
// 测试连接 // 测试连接
TestConn(re *entity.Redis) error TestConn(re *entity.Redis) error
Save(ctx context.Context, re *entity.Redis) error Save(ctx context.Context, re *entity.Redis, tagIds ...uint64) error
// 删除数据库信息 // 删除数据库信息
Delete(ctx context.Context, id uint64) error Delete(ctx context.Context, id uint64) error
@@ -34,14 +35,18 @@ type Redis interface {
GetRedisConn(id uint64, db int) (*rdm.RedisConn, error) GetRedisConn(id uint64, db int) (*rdm.RedisConn, error)
} }
func newRedisApp(redisRepo repository.Redis) Redis { func newRedisApp(redisRepo repository.Redis, tagApp tagapp.TagTree) Redis {
return &redisAppImpl{ app := &redisAppImpl{
base.AppImpl[*entity.Redis, repository.Redis]{Repo: redisRepo}, tagApp: tagApp,
} }
app.Repo = redisRepo
return app
} }
type redisAppImpl struct { type redisAppImpl struct {
base.AppImpl[*entity.Redis, repository.Redis] base.AppImpl[*entity.Redis, repository.Redis]
tagApp tagapp.TagTree
} }
// 分页获取redis列表 // 分页获取redis列表
@@ -49,10 +54,6 @@ func (r *redisAppImpl) GetPageList(condition *entity.RedisQuery, pageParam *mode
return r.GetRepo().GetRedisList(condition, pageParam, toEntity, orderBy...) return r.GetRepo().GetRedisList(condition, pageParam, toEntity, orderBy...)
} }
func (r *redisAppImpl) Count(condition *entity.RedisQuery) int64 {
return r.GetRepo().Count(condition)
}
func (r *redisAppImpl) TestConn(re *entity.Redis) error { func (r *redisAppImpl) TestConn(re *entity.Redis) error {
db := 0 db := 0
if re.Db != "" { if re.Db != "" {
@@ -67,7 +68,7 @@ func (r *redisAppImpl) TestConn(re *entity.Redis) error {
return nil return nil
} }
func (r *redisAppImpl) Save(ctx context.Context, re *entity.Redis) error { func (r *redisAppImpl) Save(ctx context.Context, re *entity.Redis, tagIds ...uint64) error {
// 查找是否存在该库 // 查找是否存在该库
oldRedis := &entity.Redis{Host: re.Host} oldRedis := &entity.Redis{Host: re.Host}
if re.SshTunnelMachineId > 0 { if re.SshTunnelMachineId > 0 {
@@ -80,7 +81,15 @@ func (r *redisAppImpl) Save(ctx context.Context, re *entity.Redis) error {
return errorx.NewBiz("该实例已存在") return errorx.NewBiz("该实例已存在")
} }
re.PwdEncrypt() re.PwdEncrypt()
return r.Insert(ctx, re)
resouceCode := stringx.Rand(16)
re.Code = resouceCode
return r.Tx(ctx, func(ctx context.Context) error {
return r.Insert(ctx, re)
}, func(ctx context.Context) error {
return r.tagApp.RelateResource(ctx, resouceCode, consts.TagResourceTypeRedis, tagIds)
})
} }
// 如果存在该库,则校验修改的库是否为该库 // 如果存在该库,则校验修改的库是否为该库
@@ -94,8 +103,13 @@ func (r *redisAppImpl) Save(ctx context.Context, re *entity.Redis) error {
rdm.CloseConn(re.Id, db) rdm.CloseConn(re.Id, db)
} }
} }
re.PwdEncrypt() re.PwdEncrypt()
return r.UpdateById(ctx, re) return r.Tx(ctx, func(ctx context.Context) error {
return r.UpdateById(ctx, re)
}, func(ctx context.Context) error {
return r.tagApp.RelateResource(ctx, oldRedis.Code, consts.TagResourceTypeRedis, tagIds)
})
} }
// 删除Redis信息 // 删除Redis信息
@@ -122,6 +136,6 @@ func (r *redisAppImpl) GetRedisConn(id uint64, db int) (*rdm.RedisConn, error) {
} }
re.PwdDecrypt() re.PwdDecrypt()
return re.ToRedisInfo(db), nil return re.ToRedisInfo(db, r.tagApp.ListTagPathByResource(consts.TagResourceTypeRedis, re.Code)...), nil
}) })
} }

View File

@@ -13,6 +13,6 @@ type RedisQuery struct {
SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
Remark string Remark string
TagIds []uint64 Codes []string
TagPath string `form:"tagPath"` TagPath string `form:"tagPath"`
} }

View File

@@ -10,6 +10,7 @@ import (
type Redis struct { type Redis struct {
model.Model model.Model
Code string `orm:"column(code)" json:"code"`
Name string `orm:"column(name)" json:"name"` Name string `orm:"column(name)" json:"name"`
Host string `orm:"column(host)" json:"host"` Host string `orm:"column(host)" json:"host"`
Mode string `json:"mode"` Mode string `json:"mode"`
@@ -18,8 +19,6 @@ type Redis struct {
Db string `orm:"column(database)" json:"db"` Db string `orm:"column(database)" json:"db"`
SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
Remark string Remark string
TagId uint64
TagPath string
} }
func (r *Redis) PwdEncrypt() { func (r *Redis) PwdEncrypt() {
@@ -33,9 +32,10 @@ func (r *Redis) PwdDecrypt() {
} }
// 转换为redisInfo进行连接 // 转换为redisInfo进行连接
func (re *Redis) ToRedisInfo(db int) *rdm.RedisInfo { func (re *Redis) ToRedisInfo(db int, tagPath ...string) *rdm.RedisInfo {
redisInfo := new(rdm.RedisInfo) redisInfo := new(rdm.RedisInfo)
structx.Copy(redisInfo, re) structx.Copy(redisInfo, re)
redisInfo.Db = db redisInfo.Db = db
redisInfo.TagPath = tagPath
return redisInfo return redisInfo
} }

View File

@@ -11,6 +11,4 @@ type Redis interface {
// 分页获取机器信息列表 // 分页获取机器信息列表
GetRedisList(condition *entity.RedisQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) GetRedisList(condition *entity.RedisQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
Count(condition *entity.RedisQuery) int64
} }

View File

@@ -20,17 +20,6 @@ func newRedisRepo() repository.Redis {
func (r *redisRepoImpl) GetRedisList(condition *entity.RedisQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) { func (r *redisRepoImpl) GetRedisList(condition *entity.RedisQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
qd := gormx.NewQuery(new(entity.Redis)). qd := gormx.NewQuery(new(entity.Redis)).
Like("host", condition.Host). Like("host", condition.Host).
In("tag_id", condition.TagIds). In("code", condition.Codes)
RLike("tag_path", condition.TagPath).
OrderByAsc("tag_path")
return gormx.PageQuery(qd, pageParam, toEntity) return gormx.PageQuery(qd, pageParam, toEntity)
} }
func (r *redisRepoImpl) Count(condition *entity.RedisQuery) int64 {
where := make(map[string]any)
if len(condition.TagIds) > 0 {
where["tag_id"] = condition.TagIds
}
return gormx.CountByCond(new(entity.Redis), where)
}

View File

@@ -31,9 +31,9 @@ type RedisInfo struct {
Username string `json:"-"` Username string `json:"-"`
Password string `json:"-"` Password string `json:"-"`
Name string `json:"-"` Name string `json:"-"`
TagPath string `json:"tagPath"` TagPath []string `json:"tagPath"`
SshTunnelMachineId int `json:"-"` SshTunnelMachineId int `json:"-"`
} }
func (r *RedisInfo) Conn() (*RedisConn, error) { func (r *RedisInfo) Conn() (*RedisConn, error) {

View File

@@ -26,8 +26,6 @@ func InitRedisRouter(router *gin.RouterGroup) {
// 获取redis list // 获取redis list
req.NewGet("", rs.RedisList), req.NewGet("", rs.RedisList),
req.NewGet("/tags", rs.RedisTags),
req.NewPost("/test-conn", rs.TestConn), req.NewPost("/test-conn", rs.TestConn),
req.NewPost("", rs.Save).Log(req.NewLogSave("redis-保存信息")), req.NewPost("", rs.Save).Log(req.NewLogSave("redis-保存信息")),

View File

@@ -43,9 +43,10 @@ func (m *syslogAppImpl) SaveFromReq(req *req.Ctx) {
syslog.CreateTime = time.Now() syslog.CreateTime = time.Now()
syslog.Creator = lg.Username syslog.Creator = lg.Username
syslog.CreatorId = lg.Id syslog.CreatorId = lg.Id
syslog.Description = req.GetLogInfo().Description
if req.GetLogInfo().LogResp { logInfo := req.GetLogInfo()
syslog.Description = logInfo.Description
if logInfo.LogResp {
respB, _ := json.Marshal(req.ResData) respB, _ := json.Marshal(req.ResData)
syslog.Resp = string(respB) syslog.Resp = string(respB)
} }

View File

@@ -2,34 +2,62 @@ package api
import ( import (
"fmt" "fmt"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/tag/api/vo" "mayfly-go/internal/tag/api/vo"
"mayfly-go/internal/tag/application" "mayfly-go/internal/tag/application"
"mayfly-go/internal/tag/domain/entity" "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz" "mayfly-go/pkg/biz"
"mayfly-go/pkg/ginx" "mayfly-go/pkg/ginx"
"mayfly-go/pkg/req" "mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"sort"
"strings" "strings"
"golang.org/x/exp/maps"
) )
type TagTree struct { type TagTree struct {
TagTreeApp application.TagTree TagTreeApp application.TagTree
} TagResourceApp application.TagResource
func (p *TagTree) GetAccountTags(rc *req.Ctx) {
tagPaths := p.TagTreeApp.ListTagByAccountId(rc.GetLoginAccount().Id)
allTagPath := make([]string, 0)
if len(tagPaths) > 0 {
tags := p.TagTreeApp.ListTagByPath(tagPaths...)
for _, v := range tags {
allTagPath = append(allTagPath, v.CodePath)
}
}
rc.ResData = allTagPath
} }
func (p *TagTree) GetTagTree(rc *req.Ctx) { func (p *TagTree) GetTagTree(rc *req.Ctx) {
var tagTrees vo.TagTreeVOS // 超管返回所有标签树
p.TagTreeApp.ListByQuery(new(entity.TagTreeQuery), &tagTrees) if rc.GetLoginAccount().Id == consts.AdminId {
var tagTrees vo.TagTreeVOS
p.TagTreeApp.ListByQuery(new(entity.TagTreeQuery), &tagTrees)
rc.ResData = tagTrees.ToTrees(0)
return
}
// 获取用户可以操作访问的标签路径
tagPaths := p.TagTreeApp.ListTagByAccountId(rc.GetLoginAccount().Id)
rootTag := make(map[string][]string, 0)
for _, accountTagPath := range tagPaths {
root := strings.Split(accountTagPath, "/")[0] + entity.CodePathSeparator
tags := rootTag[root]
tags = append(tags, accountTagPath)
rootTag[root] = tags
}
// 获取所有以root标签开头的子标签
tags := p.TagTreeApp.ListTagByPath(maps.Keys(rootTag)...)
tagTrees := make(vo.TagTreeVOS, 0)
for _, tag := range tags {
tagPath := tag.CodePath
root := strings.Split(tagPath, "/")[0] + entity.CodePathSeparator
// 获取用户可操作的标签路径列表
accountTagPaths := rootTag[root]
for _, accountTagPath := range accountTagPaths {
if strings.HasPrefix(tagPath, accountTagPath) || strings.HasPrefix(accountTagPath, tagPath) {
tagTrees = append(tagTrees, tag)
break
}
}
}
rc.ResData = tagTrees.ToTrees(0) rc.ResData = tagTrees.ToTrees(0)
} }
@@ -46,7 +74,7 @@ func (p *TagTree) SaveTagTree(rc *req.Ctx) {
tagTree := &entity.TagTree{} tagTree := &entity.TagTree{}
ginx.BindJsonAndValid(rc.GinCtx, tagTree) ginx.BindJsonAndValid(rc.GinCtx, tagTree)
rc.ReqParam = fmt.Sprintf("tagTreeId: %d, tagName: %s, codePath: %s", tagTree.Id, tagTree.Name, tagTree.CodePath) rc.ReqParam = fmt.Sprintf("tagTreeId: %d, tagName: %s, code: %s", tagTree.Id, tagTree.Name, tagTree.Code)
biz.ErrIsNil(p.TagTreeApp.Save(rc.MetaCtx, tagTree)) biz.ErrIsNil(p.TagTreeApp.Save(rc.MetaCtx, tagTree))
} }
@@ -54,3 +82,23 @@ func (p *TagTree) SaveTagTree(rc *req.Ctx) {
func (p *TagTree) DelTagTree(rc *req.Ctx) { func (p *TagTree) DelTagTree(rc *req.Ctx) {
biz.ErrIsNil(p.TagTreeApp.Delete(rc.MetaCtx, uint64(ginx.PathParamInt(rc.GinCtx, "id")))) biz.ErrIsNil(p.TagTreeApp.Delete(rc.MetaCtx, uint64(ginx.PathParamInt(rc.GinCtx, "id"))))
} }
// 获取用户可操作的资源标签路径
func (p *TagTree) TagResources(rc *req.Ctx) {
resourceType := int8(ginx.PathParamInt(rc.GinCtx, "rtype"))
tagResources := p.TagTreeApp.GetAccountTagResources(rc.GetLoginAccount().Id, resourceType, "")
tagPath2Resource := collx.ArrayToMap[entity.TagResource, string](tagResources, func(tagResource entity.TagResource) string {
return tagResource.TagPath
})
tagPaths := maps.Keys(tagPath2Resource)
sort.Strings(tagPaths)
rc.ResData = tagPaths
}
// 资源标签关联信息查询
func (p *TagTree) QueryTagResources(rc *req.Ctx) {
var trs []*entity.TagResource
p.TagResourceApp.ListByQuery(ginx.BindQuery(rc.GinCtx, new(entity.TagResourceQuery)), &trs)
rc.ResData = trs
}

View File

@@ -1,28 +1,17 @@
package vo package vo
import "time" import (
"mayfly-go/internal/tag/domain/entity"
)
type TagTreeVO struct { type TagTreeVOS []*entity.TagTree
Id int `json:"id"`
Pid int `json:"pid"`
Name string `json:"name"`
Code string `json:"code"`
CodePath string `json:"codePath"`
Remark string `json:"remark"`
Creator string `json:"creator"`
CreateTime time.Time `json:"createTime"`
Modifier string `json:"modifier"`
UpdateTime time.Time `json:"updateTime"`
}
type TagTreeVOS []TagTreeVO
type TagTreeItem struct { type TagTreeItem struct {
TagTreeVO *entity.TagTree
Children []TagTreeItem `json:"children"` Children []TagTreeItem `json:"children"`
} }
func (m *TagTreeVOS) ToTrees(pid int) []TagTreeItem { func (m *TagTreeVOS) ToTrees(pid uint64) []TagTreeItem {
var resourceTree []TagTreeItem var resourceTree []TagTreeItem
list := m.findChildren(pid) list := m.findChildren(pid)
@@ -31,15 +20,15 @@ func (m *TagTreeVOS) ToTrees(pid int) []TagTreeItem {
} }
for _, v := range list { for _, v := range list {
Children := m.ToTrees(int(v.Id)) Children := m.ToTrees(v.Id)
resourceTree = append(resourceTree, TagTreeItem{v, Children}) resourceTree = append(resourceTree, TagTreeItem{v, Children})
} }
return resourceTree return resourceTree
} }
func (m *TagTreeVOS) findChildren(pid int) []TagTreeVO { func (m *TagTreeVOS) findChildren(pid uint64) []*entity.TagTree {
child := []TagTreeVO{} child := []*entity.TagTree{}
for _, v := range *m { for _, v := range *m {
if v.Pid == pid { if v.Pid == pid {

View File

@@ -1,21 +1,14 @@
package application package application
import ( import (
dbapp "mayfly-go/internal/db/application"
machineapp "mayfly-go/internal/machine/application"
mongoapp "mayfly-go/internal/mongo/application"
redisapp "mayfly-go/internal/redis/application"
"mayfly-go/internal/tag/infrastructure/persistence" "mayfly-go/internal/tag/infrastructure/persistence"
) )
var ( var (
tagTreeApp TagTree = newTagTreeApp( tagTreeApp TagTree = newTagTreeApp(
persistence.GetTagTreeRepo(), persistence.GetTagTreeRepo(),
GetTagResourceApp(),
persistence.GetTagTreeTeamRepo(), persistence.GetTagTreeTeamRepo(),
machineapp.GetMachineApp(),
redisapp.GetRedisApp(),
dbapp.GetDbApp(),
mongoapp.GetMongoApp(),
) )
teamApp Team = newTeamApp( teamApp Team = newTeamApp(
@@ -23,6 +16,8 @@ var (
persistence.GetTeamMemberRepo(), persistence.GetTeamMemberRepo(),
persistence.GetTagTreeTeamRepo(), persistence.GetTagTreeTeamRepo(),
) )
tagResourceApp TagResource = newTagResourceApp(persistence.GetTagResourceRepo())
) )
func GetTagTreeApp() TagTree { func GetTagTreeApp() TagTree {
@@ -32,3 +27,7 @@ func GetTagTreeApp() TagTree {
func GetTeamApp() Team { func GetTeamApp() Team {
return teamApp return teamApp
} }
func GetTagResourceApp() TagResource {
return tagResourceApp
}

View File

@@ -0,0 +1,27 @@
package application
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/internal/tag/domain/repository"
"mayfly-go/pkg/base"
)
type TagResource interface {
base.App[*entity.TagResource]
ListByQuery(condition *entity.TagResourceQuery, toEntity any)
}
func newTagResourceApp(tagResourceRepo repository.TagResource) TagResource {
tagResourceApp := &tagResourceAppImpl{}
tagResourceApp.Repo = tagResourceRepo
return tagResourceApp
}
type tagResourceAppImpl struct {
base.AppImpl[*entity.TagResource, repository.TagResource]
}
func (tr *tagResourceAppImpl) ListByQuery(condition *entity.TagResourceQuery, toEntity any) {
tr.Repo.SelectByCondition(condition, toEntity)
}

View File

@@ -2,21 +2,16 @@ package application
import ( import (
"context" "context"
dbapp "mayfly-go/internal/db/application" "mayfly-go/internal/common/consts"
dbentity "mayfly-go/internal/db/domain/entity"
machineapp "mayfly-go/internal/machine/application"
machineentity "mayfly-go/internal/machine/domain/entity"
mongoapp "mayfly-go/internal/mongo/application"
mongoentity "mayfly-go/internal/mongo/domain/entity"
redisapp "mayfly-go/internal/redis/application"
redisentity "mayfly-go/internal/redis/domain/entity"
"mayfly-go/internal/tag/domain/entity" "mayfly-go/internal/tag/domain/entity"
"mayfly-go/internal/tag/domain/repository" "mayfly-go/internal/tag/domain/repository"
"mayfly-go/pkg/base" "mayfly-go/pkg/base"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/global" "mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/gormx"
"strings" "strings"
"golang.org/x/exp/maps"
) )
type TagTree interface { type TagTree interface {
@@ -28,38 +23,41 @@ type TagTree interface {
Delete(ctx context.Context, id uint64) error Delete(ctx context.Context, id uint64) error
// 获取账号id拥有的可访问的标签id // 获取指定账号有权限操作的资源信息列表
ListTagIdByAccountId(accountId uint64) []uint64 // @param accountId 账号id
// @param resourceType 资源类型
// @param tagPath 访问指定的标签路径下关联的资源
GetAccountTagResources(accountId uint64, resourceType int8, tagPath string) []entity.TagResource
// 获取指定tagPath数组开头的所有标签id // 获取指定账号有权限操作的资源codes
ListTagIdByPath(tagPath ...string) []uint64 GetAccountResourceCodes(accountId uint64, resourceType int8, tagPath string) []string
// 关联资源
// @resourceCode 资源唯一编号
// @resourceType 资源类型
// @tagIds 资源关联的标签
RelateResource(ctx context.Context, resourceCode string, resourceType int8, tagIds []uint64) error
// 根据资源信息获取对应的标签路径列表
ListTagPathByResource(resourceType int8, resourceCode string) []string
// 根据tagPath获取自身及其所有子标签信息 // 根据tagPath获取自身及其所有子标签信息
ListTagByPath(tagPath ...string) []entity.TagTree ListTagByPath(tagPath ...string) []*entity.TagTree
// 根据账号id获取其可访问标签信息 // 根据账号id获取其可访问标签信息
ListTagByAccountId(accountId uint64) []string ListTagByAccountId(accountId uint64) []string
// 查询账号id可访问的资源相关联的标签信息
// @param model对应资源的实体信息如Machinie、Db等等
ListTagByAccountIdAndResource(accountId uint64, model any) []string
// 账号是否有权限访问该标签关联的资源信息 // 账号是否有权限访问该标签关联的资源信息
CanAccess(accountId uint64, tagPath string) error CanAccess(accountId uint64, tagPath ...string) error
} }
func newTagTreeApp(tagTreeRepo repository.TagTree, func newTagTreeApp(tagTreeRepo repository.TagTree,
tagResourceApp TagResource,
tagTreeTeamRepo repository.TagTreeTeam, tagTreeTeamRepo repository.TagTreeTeam,
machineApp machineapp.Machine, ) TagTree {
redisApp redisapp.Redis,
dbApp dbapp.Db,
mongoApp mongoapp.Mongo) TagTree {
tagTreeApp := &tagTreeAppImpl{ tagTreeApp := &tagTreeAppImpl{
tagTreeTeamRepo: tagTreeTeamRepo, tagTreeTeamRepo: tagTreeTeamRepo,
machineApp: machineApp, tagResourceApp: tagResourceApp,
redisApp: redisApp,
dbApp: dbApp,
mongoApp: mongoApp,
} }
tagTreeApp.Repo = tagTreeRepo tagTreeApp.Repo = tagTreeRepo
return tagTreeApp return tagTreeApp
@@ -69,13 +67,11 @@ type tagTreeAppImpl struct {
base.AppImpl[*entity.TagTree, repository.TagTree] base.AppImpl[*entity.TagTree, repository.TagTree]
tagTreeTeamRepo repository.TagTreeTeam tagTreeTeamRepo repository.TagTreeTeam
machineApp machineapp.Machine tagResourceApp TagResource
redisApp redisapp.Redis
mongoApp mongoapp.Mongo
dbApp dbapp.Db
} }
func (p *tagTreeAppImpl) Save(ctx context.Context, tag *entity.TagTree) error { func (p *tagTreeAppImpl) Save(ctx context.Context, tag *entity.TagTree) error {
accountId := contextx.GetLoginAccount(ctx).Id
// 新建项目树节点信息 // 新建项目树节点信息
if tag.Id == 0 { if tag.Id == 0 {
if strings.Contains(tag.Code, entity.CodePathSeparator) { if strings.Contains(tag.Code, entity.CodePathSeparator) {
@@ -86,10 +82,18 @@ func (p *tagTreeAppImpl) Save(ctx context.Context, tag *entity.TagTree) error {
if err != nil { if err != nil {
return errorx.NewBiz("父节点不存在") return errorx.NewBiz("父节点不存在")
} }
if p.tagResourceApp.CountByCond(&entity.TagResource{TagId: tag.Pid}) > 0 {
return errorx.NewBiz("该父标签已关联资源, 无法添加子标签")
}
tag.CodePath = parentTag.CodePath + tag.Code + entity.CodePathSeparator tag.CodePath = parentTag.CodePath + tag.Code + entity.CodePathSeparator
} else { } else {
tag.CodePath = tag.Code + entity.CodePathSeparator tag.CodePath = tag.Code + entity.CodePathSeparator
} }
if err := p.CanAccess(accountId, tag.CodePath); err != nil {
return errorx.NewBiz("无权添加该标签")
}
// 判断该路径是否存在 // 判断该路径是否存在
var hasLikeTags []entity.TagTree var hasLikeTags []entity.TagTree
p.GetRepo().SelectByCondition(&entity.TagTreeQuery{CodePathLike: tag.CodePath}, &hasLikeTags) p.GetRepo().SelectByCondition(&entity.TagTreeQuery{CodePathLike: tag.CodePath}, &hasLikeTags)
@@ -110,52 +114,114 @@ func (p *tagTreeAppImpl) ListByQuery(condition *entity.TagTreeQuery, toEntity an
p.GetRepo().SelectByCondition(condition, toEntity) p.GetRepo().SelectByCondition(condition, toEntity)
} }
func (p *tagTreeAppImpl) ListTagIdByAccountId(accountId uint64) []uint64 { func (p *tagTreeAppImpl) GetAccountTagResources(accountId uint64, resourceType int8, tagPath string) []entity.TagResource {
// 获取该账号可操作的标签路径 tagResourceQuery := &entity.TagResourceQuery{
return p.ListTagIdByPath(p.ListTagByAccountId(accountId)...) ResourceType: resourceType,
}
var tagResources []entity.TagResource
var accountTagPaths []string
if accountId != consts.AdminId {
// 获取账号有权限操作的标签路径列表
accountTagPaths = p.ListTagByAccountId(accountId)
if len(accountTagPaths) == 0 {
return tagResources
}
}
tagResourceQuery.TagPath = tagPath
tagResourceQuery.TagPathLikes = accountTagPaths
p.tagResourceApp.ListByQuery(tagResourceQuery, &tagResources)
return tagResources
} }
func (p *tagTreeAppImpl) ListTagByPath(tagPaths ...string) []entity.TagTree { func (p *tagTreeAppImpl) GetAccountResourceCodes(accountId uint64, resourceType int8, tagPath string) []string {
var tags []entity.TagTree tagResources := p.GetAccountTagResources(accountId, resourceType, tagPath)
// resouce code去重
code2Resource := collx.ArrayToMap[entity.TagResource, string](tagResources, func(val entity.TagResource) string {
return val.ResourceCode
})
return maps.Keys(code2Resource)
}
func (p *tagTreeAppImpl) RelateResource(ctx context.Context, resourceCode string, resourceType int8, tagIds []uint64) error {
var oldTagResources []*entity.TagResource
p.tagResourceApp.ListByQuery(&entity.TagResourceQuery{ResourceType: resourceType, ResourceCode: resourceCode}, &oldTagResources)
var addTagIds, delTagIds []uint64
if len(oldTagResources) == 0 {
addTagIds = tagIds
} else {
oldTagIds := collx.ArrayMap[*entity.TagResource, uint64](oldTagResources, func(tr *entity.TagResource) uint64 {
return tr.TagId
})
addTagIds, delTagIds, _ = collx.ArrayCompare[uint64](tagIds, oldTagIds, func(u1, u2 uint64) bool { return u1 == u2 })
}
return p.Tx(ctx, func(ctx context.Context) error {
if len(addTagIds) > 0 {
addTagResource := make([]*entity.TagResource, 0)
for _, tagId := range addTagIds {
tag, err := p.GetById(new(entity.TagTree), tagId)
if err != nil {
return errorx.NewBiz("存在错误标签id")
}
addTagResource = append(addTagResource, &entity.TagResource{
ResourceCode: resourceCode,
ResourceType: resourceType,
TagId: tagId,
TagPath: tag.CodePath,
})
}
if err := p.tagResourceApp.BatchInsert(ctx, addTagResource); err != nil {
return err
}
}
if len(delTagIds) > 0 {
for _, tagId := range delTagIds {
cond := &entity.TagResource{ResourceCode: resourceCode, ResourceType: resourceType, TagId: tagId}
if err := p.tagResourceApp.DeleteByCond(ctx, cond); err != nil {
return err
}
}
}
return nil
})
}
func (p *tagTreeAppImpl) ListTagPathByResource(resourceType int8, resourceCode string) []string {
var trs []*entity.TagResource
p.tagResourceApp.ListByQuery(&entity.TagResourceQuery{ResourceType: resourceType, ResourceCode: resourceCode}, &trs)
return collx.ArrayMap(trs, func(tr *entity.TagResource) string {
return tr.TagPath
})
}
func (p *tagTreeAppImpl) ListTagByPath(tagPaths ...string) []*entity.TagTree {
var tags []*entity.TagTree
p.GetRepo().SelectByCondition(&entity.TagTreeQuery{CodePathLikes: tagPaths}, &tags) p.GetRepo().SelectByCondition(&entity.TagTreeQuery{CodePathLikes: tagPaths}, &tags)
return tags return tags
} }
func (p *tagTreeAppImpl) ListTagIdByPath(tagPaths ...string) []uint64 {
tagIds := make([]uint64, 0)
if len(tagPaths) == 0 {
return tagIds
}
tags := p.ListTagByPath(tagPaths...)
for _, v := range tags {
tagIds = append(tagIds, v.Id)
}
return tagIds
}
func (p *tagTreeAppImpl) ListTagByAccountId(accountId uint64) []string { func (p *tagTreeAppImpl) ListTagByAccountId(accountId uint64) []string {
return p.tagTreeTeamRepo.SelectTagPathsByAccountId(accountId) return p.tagTreeTeamRepo.SelectTagPathsByAccountId(accountId)
} }
func (p *tagTreeAppImpl) ListTagByAccountIdAndResource(accountId uint64, entity any) []string { func (p *tagTreeAppImpl) CanAccess(accountId uint64, tagPath ...string) error {
var res []string if accountId == consts.AdminId {
return nil
tagIds := p.ListTagIdByAccountId(accountId)
if len(tagIds) == 0 {
return res
} }
global.Db.Model(entity).Distinct("tag_path").Where("tag_id in ?", tagIds).Scopes(gormx.UndeleteScope).Order("tag_path asc").Find(&res)
return res
}
func (p *tagTreeAppImpl) CanAccess(accountId uint64, tagPath string) error {
tagPaths := p.ListTagByAccountId(accountId) tagPaths := p.ListTagByAccountId(accountId)
// 判断该资源标签是否为该账号拥有的标签或其子标签 // 判断该资源标签是否为该账号拥有的标签或其子标签
for _, v := range tagPaths { for _, v := range tagPaths {
if strings.HasPrefix(tagPath, v) { for _, tp := range tagPath {
return nil if strings.HasPrefix(tp, v) {
return nil
}
} }
} }
@@ -163,21 +229,23 @@ func (p *tagTreeAppImpl) CanAccess(accountId uint64, tagPath string) error {
} }
func (p *tagTreeAppImpl) Delete(ctx context.Context, id uint64) error { func (p *tagTreeAppImpl) Delete(ctx context.Context, id uint64) error {
tagIds := [1]uint64{id} accountId := contextx.GetLoginAccount(ctx).Id
if p.machineApp.Count(&machineentity.MachineQuery{TagIds: tagIds[:]}) > 0 { tag, err := p.GetById(new(entity.TagTree), id)
return errorx.NewBiz("请先删除该标签关联的机器信息") if err != nil {
return errorx.NewBiz("该标签不存在")
} }
if p.redisApp.Count(&redisentity.RedisQuery{TagIds: tagIds[:]}) > 0 { if err := p.CanAccess(accountId, tag.CodePath); err != nil {
return errorx.NewBiz("请先删除该标签关联的redis信息") return errorx.NewBiz("您无权删除该标签")
}
if p.dbApp.Count(&dbentity.DbQuery{TagIds: tagIds[:]}) > 0 {
return errorx.NewBiz("请先删除该标签关联的数据库信息")
}
if p.mongoApp.Count(&mongoentity.MongoQuery{TagIds: tagIds[:]}) > 0 {
return errorx.NewBiz("请先删除该标签关联的Mongo信息")
} }
p.DeleteById(ctx, id) if p.tagResourceApp.CountByCond(&entity.TagResource{TagId: id}) > 0 {
// 删除该标签关联的团队信息 return errorx.NewBiz("请先移除该标签关联的资源")
return p.tagTreeTeamRepo.DeleteByCond(ctx, &entity.TagTreeTeam{TagId: id}) }
return p.Tx(ctx, func(ctx context.Context) error {
return p.DeleteById(ctx, id)
}, func(ctx context.Context) error {
// 删除该标签关联的团队信息
return p.tagTreeTeamRepo.DeleteByCond(ctx, &entity.TagTreeTeam{TagId: id})
})
} }

View File

@@ -13,3 +13,16 @@ type TagTreeQuery struct {
CodePathLike string // 标识符路径模糊查询 CodePathLike string // 标识符路径模糊查询
CodePathLikes []string CodePathLikes []string
} }
type TagResourceQuery struct {
model.Model
TagPath string `json:"string"` // 标签路径
TagId uint64 `json:"tagId" form:"tagId"`
ResourceType int8 `json:"resourceType" form:"resourceType"` // 资源编码
ResourceCode string `json:"resourceCode" form:"resourceCode"` // 资源编码
ResourceCodes []string // 资源编码列表
TagPathLike string // 标签路径模糊查询
TagPathLikes []string
}

View File

@@ -0,0 +1,15 @@
package entity
import (
"mayfly-go/pkg/model"
)
// 标签资源关联
type TagResource struct {
model.Model
TagId uint64 `json:"tagId"`
TagPath string `json:"tagPath"` // 标签路径
ResourceCode string `json:"resourceCode"` // 资源标识
ResourceType int8 `json:"resourceType"` // 资源类型
}

View File

@@ -0,0 +1,12 @@
package repository
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/base"
)
type TagResource interface {
base.Repo[*entity.TagResource]
SelectByCondition(condition *entity.TagResourceQuery, toEntity any, orderBy ...string)
}

View File

@@ -5,6 +5,7 @@ import "mayfly-go/internal/tag/domain/repository"
var ( var (
tagTreeRepo repository.TagTree = newTagTreeRepo() tagTreeRepo repository.TagTree = newTagTreeRepo()
tagTreeTeamRepo repository.TagTreeTeam = newTagTreeTeamRepo() tagTreeTeamRepo repository.TagTreeTeam = newTagTreeTeamRepo()
tagResourceRepo repository.TagResource = newTagResourceRepo()
teamRepo repository.Team = newTeamRepo() teamRepo repository.Team = newTeamRepo()
teamMemberRepo repository.TeamMember = newTeamMemberRepo() teamMemberRepo repository.TeamMember = newTeamMemberRepo()
) )
@@ -17,6 +18,10 @@ func GetTagTreeTeamRepo() repository.TagTreeTeam {
return tagTreeTeamRepo return tagTreeTeamRepo
} }
func GetTagResourceRepo() repository.TagResource {
return tagResourceRepo
}
func GetTeamRepo() repository.Team { func GetTeamRepo() repository.Team {
return teamRepo return teamRepo
} }

View File

@@ -0,0 +1,65 @@
package persistence
import (
"mayfly-go/internal/tag/domain/entity"
"mayfly-go/internal/tag/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/gormx"
)
type tagResourceRepoImpl struct {
base.RepoImpl[*entity.TagResource]
}
func newTagResourceRepo() repository.TagResource {
return &tagResourceRepoImpl{base.RepoImpl[*entity.TagResource]{M: new(entity.TagResource)}}
}
func (p *tagResourceRepoImpl) SelectByCondition(condition *entity.TagResourceQuery, toEntity any, orderBy ...string) {
sql := "SELECT tr.resource_type, tr.resource_code, tr.tag_id, tr.tag_path FROM t_tag_resource tr WHERE tr.is_deleted = 0 "
params := make([]any, 0)
if condition.ResourceType != 0 {
sql = sql + " AND tr.resource_type = ?"
params = append(params, condition.ResourceType)
}
if condition.ResourceCode != "" {
sql = sql + " AND tr.resource_code = ?"
params = append(params, condition.ResourceCode)
}
if len(condition.ResourceCodes) > 0 {
sql = sql + " AND tr.resource_code IN (?)"
params = append(params, condition.ResourceCodes)
}
if condition.TagId != 0 {
sql = sql + " AND tr.tag_id = ?"
params = append(params, condition.TagId)
}
if condition.TagPath != "" {
sql = sql + " AND tr.tag_path = ?"
params = append(params, condition.TagPath)
}
if condition.TagPathLike != "" {
sql = sql + " AND tr.tag_path LIKE ?"
params = append(params, condition.TagPathLike+"%")
}
if len(condition.TagPathLikes) > 0 {
sql = sql + " AND ("
for i, v := range condition.TagPathLikes {
if i == 0 {
sql = sql + "tr.tag_path LIKE ?"
} else {
sql = sql + " OR tr.tag_path LIKE ?"
}
params = append(params, v+"%")
}
sql = sql + ")"
}
sql = sql + " ORDER BY tr.tag_path"
gormx.GetListBySql2Model(sql, toEntity, params...)
}

View File

@@ -1,12 +1,10 @@
package persistence package persistence
import ( import (
"fmt"
"mayfly-go/internal/tag/domain/entity" "mayfly-go/internal/tag/domain/entity"
"mayfly-go/internal/tag/domain/repository" "mayfly-go/internal/tag/domain/repository"
"mayfly-go/pkg/base" "mayfly-go/pkg/base"
"mayfly-go/pkg/gormx" "mayfly-go/pkg/gormx"
"strings"
) )
type tagTreeRepoImpl struct { type tagTreeRepoImpl struct {
@@ -19,37 +17,40 @@ func newTagTreeRepo() repository.TagTree {
func (p *tagTreeRepoImpl) SelectByCondition(condition *entity.TagTreeQuery, toEntity any, orderBy ...string) { func (p *tagTreeRepoImpl) SelectByCondition(condition *entity.TagTreeQuery, toEntity any, orderBy ...string) {
sql := "SELECT DISTINCT(p.id), p.pid, p.code, p.code_path, p.name, p.remark, p.create_time, p.creator, p.update_time, p.modifier FROM t_tag_tree p WHERE p.is_deleted = 0 " sql := "SELECT DISTINCT(p.id), p.pid, p.code, p.code_path, p.name, p.remark, p.create_time, p.creator, p.update_time, p.modifier FROM t_tag_tree p WHERE p.is_deleted = 0 "
params := make([]any, 0)
if condition.Name != "" { if condition.Name != "" {
sql = sql + " AND p.name LIKE '%" + condition.Name + "%'" sql = sql + " AND p.name LIKE ?"
params = append(params, "%"+condition.Name+"%")
} }
if condition.CodePath != "" { if condition.CodePath != "" {
sql = fmt.Sprintf("%s AND p.code_path = '%s'", sql, condition.CodePath) sql = sql + " AND p.code_path = ?"
params = append(params, condition.CodePath)
} }
if len(condition.CodePaths) > 0 { if len(condition.CodePaths) > 0 {
strCodePaths := make([]string, 0) sql = sql + " AND p.code_path IN (?)"
// 将字符串用''包裹 params = append(params, condition.CodePaths)
for _, v := range condition.CodePaths {
strCodePaths = append(strCodePaths, fmt.Sprintf("'%s'", v))
}
sql = fmt.Sprintf("%s AND p.code_path IN (%s)", sql, strings.Join(strCodePaths, ","))
} }
if condition.CodePathLike != "" { if condition.CodePathLike != "" {
sql = fmt.Sprintf("%s AND p.code_path LIKE '%s'", sql, condition.CodePathLike+"%") sql = sql + " AND p.code_path LIKE ?"
params = append(params, condition.CodePathLike+"%")
} }
if condition.Pid != 0 { if condition.Pid != 0 {
sql = fmt.Sprintf("%s AND p.pid = %d ", sql, condition.Pid) sql = sql + " AND p.pid = ?"
params = append(params, condition.Pid)
} }
if len(condition.CodePathLikes) > 0 { if len(condition.CodePathLikes) > 0 {
sql = sql + " AND (" sql = sql + " AND ("
for i, v := range condition.CodePathLikes { for i, v := range condition.CodePathLikes {
if i == 0 { if i == 0 {
sql = sql + fmt.Sprintf("p.code_path LIKE '%s'", v+"%") sql = sql + "p.code_path LIKE ?"
} else { } else {
sql = sql + fmt.Sprintf(" OR p.code_path LIKE '%s'", v+"%") sql = sql + " OR p.code_path LIKE ?"
} }
params = append(params, v+"%")
} }
sql = sql + ")" sql = sql + ")"
} }
sql = sql + " ORDER BY p.code_path" sql = sql + " ORDER BY p.code_path"
gormx.GetListBySql2Model(sql, toEntity) gormx.GetListBySql2Model(sql, toEntity, params...)
} }

View File

@@ -10,7 +10,8 @@ import (
func InitTagTreeRouter(router *gin.RouterGroup) { func InitTagTreeRouter(router *gin.RouterGroup) {
m := &api.TagTree{ m := &api.TagTree{
TagTreeApp: application.GetTagTreeApp(), TagTreeApp: application.GetTagTreeApp(),
TagResourceApp: application.GetTagResourceApp(),
} }
tagTree := router.Group("/tag-trees") tagTree := router.Group("/tag-trees")
@@ -22,12 +23,13 @@ func InitTagTreeRouter(router *gin.RouterGroup) {
// 根据条件获取标签 // 根据条件获取标签
req.NewGet("query", m.ListByQuery), req.NewGet("query", m.ListByQuery),
// 获取登录账号拥有的标签信息
req.NewGet("account-has", m.GetAccountTags),
req.NewPost("", m.SaveTagTree).Log(req.NewLogSave("标签树-保存信息")).RequiredPermissionCode("tag:save"), req.NewPost("", m.SaveTagTree).Log(req.NewLogSave("标签树-保存信息")).RequiredPermissionCode("tag:save"),
req.NewDelete(":id", m.DelTagTree).Log(req.NewLogSave("标签树-删除信息")).RequiredPermissionCode("tag:del"), req.NewDelete(":id", m.DelTagTree).Log(req.NewLogSave("标签树-删除信息")).RequiredPermissionCode("tag:del"),
req.NewGet("/resources/:rtype/tag-paths", m.TagResources),
req.NewGet("/resources", m.QueryTagResources),
} }
req.BatchSetGroup(tagTree, reqs[:]) req.BatchSetGroup(tagTree, reqs[:])

View File

@@ -2,6 +2,9 @@ package base
import ( import (
"context" "context"
"fmt"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/global"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"gorm.io/gorm" "gorm.io/gorm"
@@ -62,6 +65,9 @@ type App[T model.ModelI] interface {
// 根据指定条件统计model表的数量, cond为条件可以为map等 // 根据指定条件统计model表的数量, cond为条件可以为map等
CountByCond(cond any) int64 CountByCond(cond any) int64
// 执行事务操作
Tx(ctx context.Context, funcs ...func(context.Context) error) (err error)
} }
// 基础application接口实现 // 基础application接口实现
@@ -162,3 +168,28 @@ func (ai *AppImpl[T, R]) ListByCondOrder(cond any, list any, order ...string) er
func (ai *AppImpl[T, R]) CountByCond(cond any) int64 { func (ai *AppImpl[T, R]) CountByCond(cond any) int64 {
return ai.GetRepo().CountByCond(cond) return ai.GetRepo().CountByCond(cond)
} }
// 执行事务操作
func (ai *AppImpl[T, R]) Tx(ctx context.Context, funcs ...func(context.Context) error) (err error) {
tx := global.Db.Begin()
dbCtx := contextx.WithDb(ctx, tx)
defer func() {
// 移除当前已执行完成的的数据库事务实例
contextx.RmDb(ctx)
if r := recover(); r != nil {
tx.Rollback()
err = fmt.Errorf("%v", err)
}
}()
for _, f := range funcs {
err = f(dbCtx)
if err != nil {
tx.Rollback()
return
}
}
err = tx.Commit().Error
return
}

View File

@@ -6,6 +6,7 @@ import (
"mayfly-go/pkg/contextx" "mayfly-go/pkg/contextx"
"mayfly-go/pkg/gormx" "mayfly-go/pkg/gormx"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/utils/anyx"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -70,10 +71,13 @@ type Repo[T model.ModelI] interface {
// 基础repo接口 // 基础repo接口
type RepoImpl[T model.ModelI] struct { type RepoImpl[T model.ModelI] struct {
M any // 模型实例 M T // 模型实例
} }
func (br *RepoImpl[T]) Insert(ctx context.Context, e T) error { func (br *RepoImpl[T]) Insert(ctx context.Context, e T) error {
if db := contextx.GetDb(ctx); db != nil {
return br.InsertWithDb(ctx, db, e)
}
return gormx.Insert(br.setBaseInfo(ctx, e)) return gormx.Insert(br.setBaseInfo(ctx, e))
} }
@@ -82,6 +86,10 @@ func (br *RepoImpl[T]) InsertWithDb(ctx context.Context, db *gorm.DB, e T) error
} }
func (br *RepoImpl[T]) BatchInsert(ctx context.Context, es []T) error { func (br *RepoImpl[T]) BatchInsert(ctx context.Context, es []T) error {
if db := contextx.GetDb(ctx); db != nil {
return br.BatchInsertWithDb(ctx, db, es)
}
for _, e := range es { for _, e := range es {
br.setBaseInfo(ctx, e) br.setBaseInfo(ctx, e)
} }
@@ -97,6 +105,10 @@ func (br *RepoImpl[T]) BatchInsertWithDb(ctx context.Context, db *gorm.DB, es []
} }
func (br *RepoImpl[T]) UpdateById(ctx context.Context, e T) error { func (br *RepoImpl[T]) UpdateById(ctx context.Context, e T) error {
if db := contextx.GetDb(ctx); db != nil {
return br.UpdateByIdWithDb(ctx, db, e)
}
return gormx.UpdateById(br.setBaseInfo(ctx, e)) return gormx.UpdateById(br.setBaseInfo(ctx, e))
} }
@@ -109,6 +121,10 @@ func (br *RepoImpl[T]) Updates(cond any, udpateFields map[string]any) error {
} }
func (br *RepoImpl[T]) DeleteById(ctx context.Context, id uint64) error { func (br *RepoImpl[T]) DeleteById(ctx context.Context, id uint64) error {
if db := contextx.GetDb(ctx); db != nil {
return br.DeleteByIdWithDb(ctx, db, id)
}
return gormx.DeleteById(br.getModel(), id) return gormx.DeleteById(br.getModel(), id)
} }
@@ -117,6 +133,10 @@ func (br *RepoImpl[T]) DeleteByIdWithDb(ctx context.Context, db *gorm.DB, id uin
} }
func (br *RepoImpl[T]) DeleteByCond(ctx context.Context, cond any) error { func (br *RepoImpl[T]) DeleteByCond(ctx context.Context, cond any) error {
if db := contextx.GetDb(ctx); db != nil {
return br.DeleteByCondWithDb(ctx, db, cond)
}
return gormx.DeleteByCond(br.getModel(), cond) return gormx.DeleteByCond(br.getModel(), cond)
} }
@@ -152,8 +172,8 @@ func (br *RepoImpl[T]) CountByCond(cond any) int64 {
} }
// 获取表的模型实例 // 获取表的模型实例
func (br *RepoImpl[T]) getModel() any { func (br *RepoImpl[T]) getModel() T {
biz.IsTrue(br.M != nil, "base.RepoImpl的M字段不能为空") biz.IsTrue(!anyx.IsBlank(br.M), "base.RepoImpl的M字段不能为空")
return br.M return br.M
} }

View File

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

View File

@@ -3,7 +3,10 @@ package contextx
import ( import (
"context" "context"
"mayfly-go/pkg/model" "mayfly-go/pkg/model"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/stringx" "mayfly-go/pkg/utils/stringx"
"gorm.io/gorm"
) )
type CtxKey string type CtxKey string
@@ -11,6 +14,7 @@ type CtxKey string
const ( const (
LoginAccountKey CtxKey = "loginAccount" LoginAccountKey CtxKey = "loginAccount"
TraceIdKey CtxKey = "traceId" TraceIdKey CtxKey = "traceId"
DbKey CtxKey = "db"
) )
func NewLoginAccount(la *model.LoginAccount) context.Context { func NewLoginAccount(la *model.LoginAccount) context.Context {
@@ -44,3 +48,30 @@ func GetTraceId(ctx context.Context) string {
} }
return "" return ""
} }
// 将事务db放置context中使用stack保存。以便多个方法调用实现方法内部各自的事务操作
func WithDb(ctx context.Context, db *gorm.DB) context.Context {
if dbStack, ok := ctx.Value(DbKey).(*collx.Stack[*gorm.DB]); ok {
dbStack.Push(db)
return ctx
}
dbStack := new(collx.Stack[*gorm.DB])
dbStack.Push(db)
return context.WithValue(ctx, DbKey, dbStack)
}
// 获取当前操作的栈顶事务数据库实例
func GetDb(ctx context.Context) *gorm.DB {
if dbStack, ok := ctx.Value(DbKey).(*collx.Stack[*gorm.DB]); ok {
return dbStack.Top()
}
return nil
}
func RmDb(ctx context.Context) *gorm.DB {
if dbStack, ok := ctx.Value(DbKey).(*collx.Stack[*gorm.DB]); ok {
return dbStack.Pop()
}
return nil
}

View File

@@ -103,12 +103,8 @@ func ErrorRes(g *gin.Context, err any) {
switch t := err.(type) { switch t := err.(type) {
case errorx.BizError: case errorx.BizError:
g.JSON(http.StatusOK, model.Error(t)) g.JSON(http.StatusOK, model.Error(t))
case error:
g.JSON(http.StatusOK, model.ServerError())
case string:
g.JSON(http.StatusOK, model.ServerError())
default: default:
logx.Errorf("未知错误: %v", t) logx.ErrorTrace("服务器错误", t)
g.JSON(http.StatusOK, model.ServerError()) g.JSON(http.StatusOK, model.ServerError())
} }
} }

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