mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
feat: 标签支持拖拽移动与机器支持执行命令查看
This commit is contained in:
@@ -14,10 +14,10 @@
|
|||||||
"asciinema-player": "^3.7.0",
|
"asciinema-player": "^3.7.0",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"countup.js": "^2.8.0",
|
"countup.js": "^2.8.0",
|
||||||
"cropperjs": "^1.6.1",
|
"cropperjs": "^1.6.1",
|
||||||
"echarts": "^5.5.0",
|
"echarts": "^5.5.0",
|
||||||
"element-plus": "^2.7.0",
|
"element-plus": "^2.7.1",
|
||||||
"js-base64": "^3.7.7",
|
"js-base64": "^3.7.7",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"trzsz": "^1.1.5",
|
"trzsz": "^1.1.5",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"vue": "^3.4.23",
|
"vue": "^3.4.23",
|
||||||
"vue-router": "^4.3.1",
|
"vue-router": "^4.3.2",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"xterm-addon-search": "^0.13.0",
|
"xterm-addon-search": "^0.13.0",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.75.0",
|
"sass": "^1.75.0",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.2.9",
|
"vite": "^5.2.10",
|
||||||
"vue-eslint-parser": "^9.4.2"
|
"vue-eslint-parser": "^9.4.2"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, reactive, onMounted } from 'vue';
|
import { toRefs, reactive, onMounted } from 'vue';
|
||||||
import { tagApi } from '../tag/api';
|
import { tagApi } from '../tag/api';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
|
||||||
//定义事件
|
//定义事件
|
||||||
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
|
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
|
||||||
@@ -42,6 +43,10 @@ const props = defineProps({
|
|||||||
selectTags: {
|
selectTags: {
|
||||||
type: [Array<any>],
|
type: [Array<any>],
|
||||||
},
|
},
|
||||||
|
tagType: {
|
||||||
|
type: Number,
|
||||||
|
default: TagResourceTypeEnum.Tag.value,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -54,7 +59,7 @@ const { tags } = toRefs(state);
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
state.selectTags = props.selectTags;
|
state.selectTags = props.selectTags;
|
||||||
state.tags = await tagApi.getTagTrees.request({ type: -1 });
|
state.tags = await tagApi.getTagTrees.request({ type: props.tagType });
|
||||||
});
|
});
|
||||||
|
|
||||||
const changeTag = () => {
|
const changeTag = () => {
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
: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"
|
width="1000"
|
||||||
@open="getTermOps()"
|
@open="getTermOps()"
|
||||||
>
|
>
|
||||||
<page-table ref="pageTableRef" :page-api="machineApi.termOpRecs" :lazy="true" height="100%" v-model:query-form="query" :columns="columns">
|
<page-table ref="pageTableRef" :page-api="machineApi.termOpRecs" :lazy="true" height="100%" v-model:query-form="query" :columns="columns">
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-button @click="playRec(data)" loading-icon="loading" :loading="data.playRecLoding" type="primary" link>回放</el-button>
|
<el-button @click="playRec(data)" loading-icon="loading" :loading="data.playRecLoding" type="primary" link>回放</el-button>
|
||||||
|
<el-button @click="showExecCmds(data)" type="primary" link>命令</el-button>
|
||||||
</template>
|
</template>
|
||||||
</page-table>
|
</page-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -26,6 +27,17 @@
|
|||||||
>
|
>
|
||||||
<div ref="playerRef" id="rc-player"></div>
|
<div ref="playerRef" id="rc-player"></div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog :title="title" v-model="execCmdsDialogVisible" :close-on-click-modal="false" :destroy-on-close="true" width="500">
|
||||||
|
<el-table :data="state.execCmds" max-height="480" stripe size="small">
|
||||||
|
<el-table-column prop="cmd" label="命令" show-overflow-tooltip min-width="150px"> </el-table-column>
|
||||||
|
<el-table-column prop="time" label="执行时间" min-width="80" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
{{ dateFormat(new Date(scope.row.time * 1000).toString()) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -36,6 +48,7 @@ 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 PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
|
import { dateFormat } from '@/common/utils/date';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: { type: Boolean },
|
visible: { type: Boolean },
|
||||||
@@ -50,7 +63,7 @@ const columns = [
|
|||||||
TableColumn.new('createTime', '开始时间').isTime().setMinWidth(150),
|
TableColumn.new('createTime', '开始时间').isTime().setMinWidth(150),
|
||||||
TableColumn.new('endTime', '结束时间').isTime().setMinWidth(150),
|
TableColumn.new('endTime', '结束时间').isTime().setMinWidth(150),
|
||||||
TableColumn.new('recordFilePath', '文件路径').setMinWidth(200),
|
TableColumn.new('recordFilePath', '文件路径').setMinWidth(200),
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(60).fixedRight().alignCenter(),
|
TableColumn.new('action', '操作').isSlot().setMinWidth(120).fixedRight().alignCenter(),
|
||||||
];
|
];
|
||||||
|
|
||||||
const playerRef = ref(null);
|
const playerRef = ref(null);
|
||||||
@@ -63,11 +76,12 @@ const state = reactive({
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
machineId: 0,
|
machineId: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
playerDialogVisible: false,
|
playerDialogVisible: false,
|
||||||
|
execCmdsDialogVisible: false,
|
||||||
|
execCmds: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { dialogVisible, query, playerDialogVisible } = toRefs(state);
|
const { dialogVisible, query, playerDialogVisible, execCmdsDialogVisible } = toRefs(state);
|
||||||
|
|
||||||
watch(props, async (newValue: any) => {
|
watch(props, async (newValue: any) => {
|
||||||
const visible = newValue.visible;
|
const visible = newValue.visible;
|
||||||
@@ -82,6 +96,11 @@ const getTermOps = async () => {
|
|||||||
pageTableRef.value.search();
|
pageTableRef.value.search();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showExecCmds = (data: any) => {
|
||||||
|
state.execCmds = JSON.parse(data.execCmds);
|
||||||
|
state.execCmdsDialogVisible = true;
|
||||||
|
};
|
||||||
|
|
||||||
let player: any = null;
|
let player: any = null;
|
||||||
|
|
||||||
const playRec = async (rec: any) => {
|
const playRec = async (rec: any) => {
|
||||||
|
|||||||
@@ -37,6 +37,10 @@
|
|||||||
@node-contextmenu="nodeContextmenu"
|
@node-contextmenu="nodeContextmenu"
|
||||||
@node-click="treeNodeClick"
|
@node-click="treeNodeClick"
|
||||||
:default-expanded-keys="defaultExpandedKeys"
|
:default-expanded-keys="defaultExpandedKeys"
|
||||||
|
draggable
|
||||||
|
:allow-drop="allowDrop"
|
||||||
|
:allow-drag="allowDrag"
|
||||||
|
@node-drop="handleDrop"
|
||||||
:expand-on-click-node="false"
|
:expand-on-click-node="false"
|
||||||
:filter-node-method="filterNode"
|
:filter-node-method="filterNode"
|
||||||
>
|
>
|
||||||
@@ -265,6 +269,82 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const allowDrop = (draggingNode: any, dropNode: any, type: any) => {
|
||||||
|
// 不允许同层级移动
|
||||||
|
if (type != 'inner') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropNodeData = dropNode.data;
|
||||||
|
const draggingNodeData = draggingNode.data;
|
||||||
|
const dropTagType = dropNodeData.type;
|
||||||
|
const draggingTagType = draggingNodeData.type;
|
||||||
|
|
||||||
|
// 目标节点只允许为标签类型
|
||||||
|
if (dropTagType != TagResourceTypeEnum.Tag.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目标节点下没有子节点
|
||||||
|
if (!dropNodeData.children) {
|
||||||
|
// 都为标签类型允许移动
|
||||||
|
if (dropTagType == draggingTagType && dropTagType == TagResourceTypeEnum.Tag.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目标节点为标签,允许移动
|
||||||
|
if (dropTagType == TagResourceTypeEnum.Tag.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let child of dropNodeData.children) {
|
||||||
|
// 当前移动节点若在目标节点下有相同code,则不允许移动
|
||||||
|
if (draggingNodeData.code == child.code) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childType = child.type;
|
||||||
|
// 移动节点非标签类型时(资源标签),并且子节点存在标签类型,则不允许移动,因为资源只允许放在叶子标签类型下
|
||||||
|
if (draggingTagType != TagResourceTypeEnum.Tag.value && childType == TagResourceTypeEnum.Tag.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动节点为标签类型时(资源标签),并且子节点存在资源类型,则不允许移动
|
||||||
|
if (draggingTagType == TagResourceTypeEnum.Tag.value && childType != TagResourceTypeEnum.Tag.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowDrag = (node: any) => {
|
||||||
|
const tagType = node.data.type;
|
||||||
|
return (
|
||||||
|
tagType == TagResourceTypeEnum.Tag.value ||
|
||||||
|
tagType == TagResourceTypeEnum.Db.value ||
|
||||||
|
tagType == TagResourceTypeEnum.Redis.value ||
|
||||||
|
tagType == TagResourceTypeEnum.Machine.value ||
|
||||||
|
tagType == TagResourceTypeEnum.Mongo.value
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = async (draggingNode: any, dropNode: any) => {
|
||||||
|
const draggingData = draggingNode.data;
|
||||||
|
const dropData = dropNode.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tagApi.movingTag.request({
|
||||||
|
fromPath: draggingData.codePath,
|
||||||
|
toPath: dropData.codePath,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const parseTagPath = (tagPath: string) => {
|
const parseTagPath = (tagPath: string) => {
|
||||||
if (!tagPath) {
|
if (!tagPath) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const tagApi = {
|
|||||||
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}'),
|
||||||
|
movingTag: Api.newPost('/tag-trees/moving'),
|
||||||
|
|
||||||
getResourceTagPaths: Api.newGet('/tag-trees/resources/{resourceType}/tag-paths'),
|
getResourceTagPaths: Api.newGet('/tag-trees/resources/{resourceType}/tag-paths'),
|
||||||
countTagResource: Api.newGet('/tag-trees/resources/count'),
|
countTagResource: Api.newGet('/tag-trees/resources/count'),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ require (
|
|||||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||||
github.com/sijms/go-ora/v2 v2.8.10
|
github.com/sijms/go-ora/v2 v2.8.10
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
|
github.com/veops/go-ansiterm v0.0.5
|
||||||
go.mongodb.org/mongo-driver v1.14.0 // mongo
|
go.mongodb.org/mongo-driver v1.14.0 // mongo
|
||||||
golang.org/x/crypto v0.22.0 // ssh
|
golang.org/x/crypto v0.22.0 // ssh
|
||||||
golang.org/x/oauth2 v0.18.0
|
golang.org/x/oauth2 v0.18.0
|
||||||
@@ -70,6 +71,7 @@ require (
|
|||||||
github.com/kr/fs v0.1.0 // indirect
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
github.com/leodido/go-urn v1.2.4 // indirect
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
@@ -77,6 +79,7 @@ require (
|
|||||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.3 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"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/jsonx"
|
||||||
"mayfly-go/pkg/utils/stringx"
|
"mayfly-go/pkg/utils/stringx"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -76,7 +77,24 @@ func (m *machineTermOpAppImpl) TermConn(ctx context.Context, cli *mcm.Cli, wsCon
|
|||||||
recorder = mcm.NewRecorder(f)
|
recorder = mcm.NewRecorder(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
mts, err := mcm.NewTerminalSession(stringx.Rand(16), wsConn, cli, rows, cols, recorder)
|
createTsParam := &mcm.CreateTerminalSessionParam{
|
||||||
|
SessionId: stringx.Rand(16),
|
||||||
|
Cli: cli,
|
||||||
|
WsConn: wsConn,
|
||||||
|
Rows: rows,
|
||||||
|
Cols: cols,
|
||||||
|
Recorder: recorder,
|
||||||
|
LogCmd: cli.Info.EnableRecorder == 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTsParam.CmdFilterFuncs = []mcm.CmdFilterFunc{func(cmd string) error {
|
||||||
|
// if strings.HasPrefix(cmd, "rm") {
|
||||||
|
// return errorx.NewBiz("该命令已被禁用...")
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// }}
|
||||||
|
|
||||||
|
mts, err := mcm.NewTerminalSession(createTsParam)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -87,6 +105,7 @@ func (m *machineTermOpAppImpl) TermConn(ctx context.Context, cli *mcm.Cli, wsCon
|
|||||||
if termOpRecord != nil {
|
if termOpRecord != nil {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
termOpRecord.EndTime = &now
|
termOpRecord.EndTime = &now
|
||||||
|
termOpRecord.ExecCmds = jsonx.ToStr(mts.GetExecCmds())
|
||||||
return m.Insert(ctx, termOpRecord)
|
return m.Insert(ctx, termOpRecord)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type MachineTermOp struct {
|
|||||||
MachineId uint64 `json:"machineId"`
|
MachineId uint64 `json:"machineId"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
RecordFilePath string `json:"recordFilePath"` // 回放文件路径
|
RecordFilePath string `json:"recordFilePath"` // 回放文件路径
|
||||||
|
ExecCmds string `json:"execCmds"` // 执行的命令
|
||||||
|
|
||||||
CreateTime *time.Time `json:"createTime"`
|
CreateTime *time.Time `json:"createTime"`
|
||||||
CreatorId uint64 `json:"creatorId"`
|
CreatorId uint64 `json:"creatorId"`
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Terminal struct {
|
type Terminal struct {
|
||||||
SshSession *ssh.Session
|
SshSession *ssh.Session
|
||||||
|
|
||||||
StdinPipe io.WriteCloser
|
StdinPipe io.WriteCloser
|
||||||
StdoutReader *bufio.Reader
|
StdoutReader *bufio.Reader
|
||||||
}
|
}
|
||||||
@@ -44,7 +45,7 @@ func (t *Terminal) Write(p []byte) (int, error) {
|
|||||||
return t.StdinPipe.Write(p)
|
return t.StdinPipe.Write(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) ReadRune() (r rune, size int, err error) {
|
func (t *Terminal) ReadRune() (rune, int, error) {
|
||||||
return t.StdoutReader.ReadRune()
|
return t.StdoutReader.ReadRune()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
214
server/internal/machine/mcm/terminal_handler.go
Normal file
214
server/internal/machine/mcm/terminal_handler.go
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package mcm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"mayfly-go/pkg/errorx"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/veops/go-ansiterm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 命令过滤函数,若返回error,则不执行该命令
|
||||||
|
type CmdFilterFunc func(cmd string) error
|
||||||
|
|
||||||
|
const (
|
||||||
|
CR = 0x0d // 单个字节 13 通常表示发送一个 CR(Carriage Return,回车)字符 \r
|
||||||
|
EOT = 0x03 // 通过向标准输入发送单个字节 3 通常表示发送一个 EOT(End of Transmission)信号。EOT 是一种控制字符,在通信中用于指示数据传输的结束。发送 EOT 信号可以被用来终止当前的交互或数据传输
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExecutedCmd struct {
|
||||||
|
Cmd string `json:"cmd"` // 执行的命令
|
||||||
|
Time int64 `json:"time"` // 执行时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
// TerminalHandler 终端处理器
|
||||||
|
type TerminalHandler struct {
|
||||||
|
Filters []CmdFilterFunc
|
||||||
|
ExecutedCmds []*ExecutedCmd // 已执行的命令
|
||||||
|
|
||||||
|
Parser *Parser
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreWriteHandle 写入数据至终端前的处理,可进行过滤等操作
|
||||||
|
func (tf *TerminalHandler) PreWriteHandle(p []byte) error {
|
||||||
|
tf.Parser.AppendInputData(p)
|
||||||
|
|
||||||
|
// 不是回车命令,则表示命令未结束
|
||||||
|
if bytes.LastIndex(p, []byte{CR}) != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// time.Sleep(time.Millisecond * 30)
|
||||||
|
command := tf.Parser.GetCmd()
|
||||||
|
// 重置终端输入输出
|
||||||
|
tf.Parser.Reset()
|
||||||
|
|
||||||
|
if command == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行命令过滤器
|
||||||
|
for _, filter := range tf.Filters {
|
||||||
|
if err := filter(command); err != nil {
|
||||||
|
msg := fmt.Sprintf("\r\n%s%s", tf.Parser.Ps1, GetErrorContent(err.Error()))
|
||||||
|
return errorx.NewBiz(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录执行命令
|
||||||
|
tf.ExecutedCmds = append(tf.ExecutedCmds, &ExecutedCmd{
|
||||||
|
Cmd: command,
|
||||||
|
Time: time.Now().Unix(),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRead 处理从终端读取的数据进行操作
|
||||||
|
func (tf *TerminalHandler) HandleRead(data []byte) error {
|
||||||
|
tf.Parser.AppendOutData(data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
Output *ansiterm.ByteStream
|
||||||
|
InputData []byte
|
||||||
|
OutputData []byte
|
||||||
|
Ps1 string
|
||||||
|
|
||||||
|
vimState bool
|
||||||
|
commandState bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParser(width, height int) *Parser {
|
||||||
|
return &Parser{
|
||||||
|
Output: NewParserByteStream(width, height),
|
||||||
|
vimState: false,
|
||||||
|
commandState: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParserByteStream(width, height int) *ansiterm.ByteStream {
|
||||||
|
screen := ansiterm.NewScreen(width, height)
|
||||||
|
stream := ansiterm.InitByteStream(screen, false)
|
||||||
|
stream.Attach(screen)
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
enterMarks = [][]byte{
|
||||||
|
[]byte("\x1b[?1049h"),
|
||||||
|
[]byte("\x1b[?1048h"),
|
||||||
|
[]byte("\x1b[?1047h"),
|
||||||
|
[]byte("\x1b[?47h"),
|
||||||
|
[]byte("\x1b[?25l"),
|
||||||
|
}
|
||||||
|
|
||||||
|
exitMarks = [][]byte{
|
||||||
|
[]byte("\x1b[?1049l"),
|
||||||
|
[]byte("\x1b[?1048l"),
|
||||||
|
[]byte("\x1b[?1047l"),
|
||||||
|
[]byte("\x1b[?47l"),
|
||||||
|
}
|
||||||
|
|
||||||
|
screenMarks = [][]byte{
|
||||||
|
{0x1b, 0x5b, 0x4b, 0x0d, 0x0a},
|
||||||
|
{0x1b, 0x5b, 0x34, 0x6c},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Parser) AppendInputData(data []byte) {
|
||||||
|
if len(p.InputData) == 0 {
|
||||||
|
// 如 "root@cloud-s0ervh-hh87:~# " 获取前一段用户名等提示内容
|
||||||
|
p.Ps1 = p.GetOutput()
|
||||||
|
}
|
||||||
|
p.InputData = append(p.InputData, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) AppendOutData(data []byte) {
|
||||||
|
// 非编辑等状态,则追加输出内容
|
||||||
|
if !p.State(data) {
|
||||||
|
p.OutputData = append(p.OutputData, data...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCmd 获取执行的命令
|
||||||
|
func (p *Parser) GetCmd() string {
|
||||||
|
// "root@cloud-s0ervh-hh87:~# ls"
|
||||||
|
s := p.GetOutput()
|
||||||
|
// Ps1 = "root@cloud-s0ervh-hh87:~# "
|
||||||
|
return strings.TrimPrefix(s, p.Ps1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) Reset() {
|
||||||
|
p.Output.Listener.Reset()
|
||||||
|
p.OutputData = nil
|
||||||
|
p.InputData = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) GetOutput() string {
|
||||||
|
p.Output.Feed(p.OutputData)
|
||||||
|
|
||||||
|
res := parseOutput(p.Output.Listener.Display())
|
||||||
|
if len(res) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return res[len(res)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOutput(data []string) (output []string) {
|
||||||
|
for _, line := range data {
|
||||||
|
if strings.TrimSpace(line) != "" {
|
||||||
|
output = append(output, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) State(b []byte) bool {
|
||||||
|
if !p.vimState && IsEditEnterMode(b) {
|
||||||
|
if !isNewScreen(b) {
|
||||||
|
p.vimState = true
|
||||||
|
p.commandState = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.vimState && IsEditExitMode(b) {
|
||||||
|
// 重置终端输入输出
|
||||||
|
p.Reset()
|
||||||
|
p.vimState = false
|
||||||
|
p.commandState = true
|
||||||
|
}
|
||||||
|
return p.vimState
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNewScreen(p []byte) bool {
|
||||||
|
return matchMark(p, screenMarks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsEditEnterMode(p []byte) bool {
|
||||||
|
return matchMark(p, enterMarks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsEditExitMode(p []byte) bool {
|
||||||
|
return matchMark(p, exitMarks)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchMark(p []byte, marks [][]byte) bool {
|
||||||
|
for _, item := range marks {
|
||||||
|
if bytes.Contains(p, item) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetErrorContent 包装返回终端错误消息
|
||||||
|
func GetErrorContent(msg string) string {
|
||||||
|
return fmt.Sprintf("\033[1;31m%s\033[0m", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetErrorContentRn 包装返回终端错误消息, 并自动回车换行
|
||||||
|
func GetErrorContentRn(msg string) string {
|
||||||
|
return fmt.Sprintf("\r\n%s", GetErrorContent(msg))
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package mcm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mayfly-go/pkg/errorx"
|
"mayfly-go/pkg/errorx"
|
||||||
"mayfly-go/pkg/logx"
|
"mayfly-go/pkg/logx"
|
||||||
@@ -27,6 +28,7 @@ type TerminalSession struct {
|
|||||||
ID string
|
ID string
|
||||||
wsConn *websocket.Conn
|
wsConn *websocket.Conn
|
||||||
terminal *Terminal
|
terminal *Terminal
|
||||||
|
handler *TerminalHandler
|
||||||
recorder *Recorder
|
recorder *Recorder
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -34,7 +36,21 @@ type TerminalSession struct {
|
|||||||
tick *time.Ticker
|
tick *time.Ticker
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTerminalSession(sessionId string, ws *websocket.Conn, cli *Cli, rows, cols int, recorder *Recorder) (*TerminalSession, error) {
|
type CreateTerminalSessionParam struct {
|
||||||
|
SessionId string
|
||||||
|
Cli *Cli
|
||||||
|
WsConn *websocket.Conn
|
||||||
|
Rows int
|
||||||
|
Cols int
|
||||||
|
Recorder *Recorder
|
||||||
|
LogCmd bool // 是否记录命令
|
||||||
|
CmdFilterFuncs []CmdFilterFunc // 命令过滤器
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTerminalSession(param *CreateTerminalSessionParam) (*TerminalSession, error) {
|
||||||
|
sessionId, rows, cols := param.SessionId, param.Rows, param.Cols
|
||||||
|
cli, ws, recorder := param.Cli, param.WsConn, param.Recorder
|
||||||
|
|
||||||
terminal, err := NewTerminal(cli)
|
terminal, err := NewTerminal(cli)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -52,12 +68,19 @@ func NewTerminalSession(sessionId string, ws *websocket.Conn, cli *Cli, rows, co
|
|||||||
recorder.WriteHeader(rows-3, cols)
|
recorder.WriteHeader(rows-3, cols)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var handler *TerminalHandler
|
||||||
|
// 记录命令或者存在命令过滤器时,则创建对应的终端处理器
|
||||||
|
if param.LogCmd || param.CmdFilterFuncs != nil {
|
||||||
|
handler = &TerminalHandler{Parser: NewParser(120, 40), Filters: param.CmdFilterFuncs}
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
tick := time.NewTicker(time.Millisecond * time.Duration(60))
|
tick := time.NewTicker(time.Millisecond * time.Duration(60))
|
||||||
ts := &TerminalSession{
|
ts := &TerminalSession{
|
||||||
ID: sessionId,
|
ID: sessionId,
|
||||||
wsConn: ws,
|
wsConn: ws,
|
||||||
terminal: terminal,
|
terminal: terminal,
|
||||||
|
handler: handler,
|
||||||
recorder: recorder,
|
recorder: recorder,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
@@ -66,7 +89,7 @@ func NewTerminalSession(sessionId string, ws *websocket.Conn, cli *Cli, rows, co
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 清除终端内容
|
// 清除终端内容
|
||||||
WriteMessage(ws, "\033[2J\033[3J\033[1;1H")
|
ts.WriteToWs("\033[2J\033[3J\033[1;1H")
|
||||||
return ts, nil
|
return ts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +112,14 @@ func (r TerminalSession) Stop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取终端会话执行的所有命令
|
||||||
|
func (r TerminalSession) GetExecCmds() []*ExecutedCmd {
|
||||||
|
if r.handler != nil {
|
||||||
|
return r.handler.ExecutedCmds
|
||||||
|
}
|
||||||
|
return []*ExecutedCmd{}
|
||||||
|
}
|
||||||
|
|
||||||
func (ts TerminalSession) readFromTerminal() {
|
func (ts TerminalSession) readFromTerminal() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -116,20 +147,27 @@ func (ts TerminalSession) writeToWebsocket() {
|
|||||||
case <-ts.ctx.Done():
|
case <-ts.ctx.Done():
|
||||||
return
|
return
|
||||||
case <-ts.tick.C:
|
case <-ts.tick.C:
|
||||||
if len(buf) > 0 {
|
if len(buf) == 0 {
|
||||||
s := string(buf)
|
continue
|
||||||
if err := WriteMessage(ts.wsConn, s); err != nil {
|
|
||||||
logx.Error("机器ssh终端发送消息至websocket失败: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 如果记录器存在,则记录操作回放信息
|
|
||||||
if ts.recorder != nil {
|
|
||||||
ts.recorder.Lock()
|
|
||||||
ts.recorder.WriteData(OutPutType, s)
|
|
||||||
ts.recorder.Unlock()
|
|
||||||
}
|
|
||||||
buf = []byte{}
|
|
||||||
}
|
}
|
||||||
|
if ts.handler != nil {
|
||||||
|
ts.handler.HandleRead(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := string(buf)
|
||||||
|
if err := ts.WriteToWs(s); err != nil {
|
||||||
|
logx.Error("机器ssh终端发送消息至websocket失败: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果记录器存在,则记录操作回放信息
|
||||||
|
if ts.recorder != nil {
|
||||||
|
ts.recorder.Lock()
|
||||||
|
ts.recorder.WriteData(OutPutType, s)
|
||||||
|
ts.recorder.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = []byte{}
|
||||||
case data := <-ts.dataChan:
|
case data := <-ts.dataChan:
|
||||||
if data != utf8.RuneError {
|
if data != utf8.RuneError {
|
||||||
p := make([]byte, utf8.RuneLen(data))
|
p := make([]byte, utf8.RuneLen(data))
|
||||||
@@ -149,16 +187,15 @@ type WsMsg struct {
|
|||||||
Rows int `json:"rows"`
|
Rows int `json:"rows"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 接收客户端ws发送过来的消息,并写入终端会话中。
|
// receiveWsMsg 接收客户端ws发送过来的消息,并写入终端会话中。
|
||||||
func (ts *TerminalSession) receiveWsMsg() {
|
func (ts *TerminalSession) receiveWsMsg() {
|
||||||
wsConn := ts.wsConn
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ts.ctx.Done():
|
case <-ts.ctx.Done():
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
// read websocket msg
|
// read websocket msg
|
||||||
_, wsData, err := wsConn.ReadMessage()
|
_, wsData, err := ts.wsConn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Debugf("机器ssh终端读取websocket消息失败: %s", err.Error())
|
logx.Debugf("机器ssh终端读取websocket消息失败: %s", err.Error())
|
||||||
return
|
return
|
||||||
@@ -166,7 +203,7 @@ func (ts *TerminalSession) receiveWsMsg() {
|
|||||||
// 解析消息
|
// 解析消息
|
||||||
msgObj, err := parseMsg(wsData)
|
msgObj, err := parseMsg(wsData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteMessage(wsConn, "\r\n\033[1;31m提示: 消息内容解析失败...\033[0m")
|
ts.WriteToWs(GetErrorContentRn("消息内容解析失败..."))
|
||||||
logx.Error("机器ssh终端消息解析失败: ", err)
|
logx.Error("机器ssh终端消息解析失败: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -179,14 +216,25 @@ func (ts *TerminalSession) receiveWsMsg() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case Data:
|
case Data:
|
||||||
|
data := []byte(msgObj.Msg)
|
||||||
|
if ts.handler != nil {
|
||||||
|
if err := ts.handler.PreWriteHandle(data); err != nil {
|
||||||
|
ts.WriteToWs(err.Error())
|
||||||
|
// 发送命令终止指令
|
||||||
|
ts.terminal.Write([]byte{EOT})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, err := ts.terminal.Write([]byte(msgObj.Msg))
|
_, err := ts.terminal.Write([]byte(msgObj.Msg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Debugf("机器ssh终端写入消息失败: %s", err)
|
logx.Errorf("写入数据至ssh终端失败: %s", err)
|
||||||
|
ts.WriteToWs(GetErrorContentRn(fmt.Sprintf("写入数据至ssh终端失败: %s", err.Error())))
|
||||||
}
|
}
|
||||||
case Ping:
|
case Ping:
|
||||||
_, err := ts.terminal.SshSession.SendRequest("ping", true, nil)
|
_, err := ts.terminal.SshSession.SendRequest("ping", true, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteMessage(wsConn, "\r\n\033[1;31m提示: 终端连接已断开...\033[0m")
|
ts.WriteToWs(GetErrorContentRn("终端连接已断开..."))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,8 +242,9 @@ func (ts *TerminalSession) receiveWsMsg() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteMessage(ws *websocket.Conn, msg string) error {
|
// WriteToWs 将消息写入websocket连接
|
||||||
return ws.WriteMessage(websocket.TextMessage, []byte(msg))
|
func (ts *TerminalSession) WriteToWs(msg string) error {
|
||||||
|
return ts.wsConn.WriteMessage(websocket.TextMessage, []byte(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析消息
|
// 解析消息
|
||||||
|
|||||||
@@ -7,3 +7,8 @@ type TagTree struct {
|
|||||||
|
|
||||||
Pid uint64 `json:"pid"`
|
Pid uint64 `json:"pid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MovingTag struct {
|
||||||
|
FromPath string `json:"fromPath" binding:"required"`
|
||||||
|
ToPath string `json:"toPath" binding:"required"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,44 +20,48 @@ type TagTree struct {
|
|||||||
|
|
||||||
func (p *TagTree) GetTagTree(rc *req.Ctx) {
|
func (p *TagTree) GetTagTree(rc *req.Ctx) {
|
||||||
tagType := entity.TagType(rc.QueryInt("type"))
|
tagType := entity.TagType(rc.QueryInt("type"))
|
||||||
// 超管返回所有标签树
|
accountTags := p.TagTreeApp.GetAccountTags(rc.GetLoginAccount().Id, &entity.TagTreeQuery{Type: tagType})
|
||||||
if rc.GetLoginAccount().Id == consts.AdminId {
|
if len(accountTags) == 0 {
|
||||||
var tagTrees vo.TagTreeVOS
|
rc.ResData = []any{}
|
||||||
p.TagTreeApp.ListByQuery(&entity.TagTreeQuery{Type: tagType}, &tagTrees)
|
|
||||||
rc.ResData = tagTrees.ToTrees(0)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户可以操作访问的标签路径
|
allTags := p.complteTags(accountTags)
|
||||||
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标签开头的子标签
|
|
||||||
var tags []*entity.TagTree
|
|
||||||
p.TagTreeApp.ListByQuery(&entity.TagTreeQuery{CodePathLikes: collx.MapKeys(rootTag), Type: tagType}, &tags)
|
|
||||||
|
|
||||||
tagTrees := make(vo.TagTreeVOS, 0)
|
tagTrees := make(vo.TagTreeVOS, 0)
|
||||||
for _, tag := range tags {
|
for _, tag := range allTags {
|
||||||
tagPath := tag.CodePath
|
tagTrees = append(tagTrees, tag)
|
||||||
root := strings.Split(tagPath, "/")[0] + entity.CodePathSeparator
|
}
|
||||||
// 获取用户可操作的标签路径列表
|
rc.ResData = tagTrees.ToTrees(0)
|
||||||
accountTagPaths := rootTag[root]
|
}
|
||||||
for _, accountTagPath := range accountTagPaths {
|
|
||||||
if strings.HasPrefix(tagPath, accountTagPath) || strings.HasPrefix(accountTagPath, tagPath) {
|
// complteTags 补全标签信息,使其能构造为树结构
|
||||||
tagTrees = append(tagTrees, tag)
|
func (p *TagTree) complteTags(resourceTags []*entity.TagTree) []*entity.TagTree {
|
||||||
break
|
codePath2Tag := collx.ArrayToMap(resourceTags, func(tag *entity.TagTree) string {
|
||||||
}
|
return tag.CodePath
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如tagPath = tag1/tag2/tag3/ 需要转为该路径所关联的所有标签路径即 tag1/ tag1/tag2/ tag1/tag2/tag3/三个相关联标签,才可以构造成一棵树
|
||||||
|
allTagPaths := make([]string, 0)
|
||||||
|
for _, tagPath := range collx.MapKeys(codePath2Tag) {
|
||||||
|
allTagPaths = append(allTagPaths, entity.GetAllCodePath(tagPath)...)
|
||||||
|
}
|
||||||
|
allTagPaths = collx.ArrayDeduplicate(allTagPaths)
|
||||||
|
|
||||||
|
notExistCodePaths := make([]string, 0)
|
||||||
|
for _, tagPath := range allTagPaths {
|
||||||
|
if _, ok := codePath2Tag[tagPath]; !ok {
|
||||||
|
notExistCodePaths = append(notExistCodePaths, tagPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 未存在需要补全的标签信息,则返回
|
||||||
|
if len(notExistCodePaths) == 0 {
|
||||||
|
return resourceTags
|
||||||
|
}
|
||||||
|
|
||||||
rc.ResData = tagTrees.ToTrees(0)
|
var tags []*entity.TagTree
|
||||||
|
p.TagTreeApp.ListByQuery(&entity.TagTreeQuery{CodePaths: notExistCodePaths}, &tags)
|
||||||
|
// 完善需要补充的标签信息
|
||||||
|
return append(resourceTags, tags...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TagTree) ListByQuery(rc *req.Ctx) {
|
func (p *TagTree) ListByQuery(rc *req.Ctx) {
|
||||||
@@ -82,6 +86,13 @@ func (p *TagTree) DelTagTree(rc *req.Ctx) {
|
|||||||
biz.ErrIsNil(p.TagTreeApp.Delete(rc.MetaCtx, uint64(rc.PathParamInt("id"))))
|
biz.ErrIsNil(p.TagTreeApp.Delete(rc.MetaCtx, uint64(rc.PathParamInt("id"))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *TagTree) MovingTag(rc *req.Ctx) {
|
||||||
|
movingForm := &form.MovingTag{}
|
||||||
|
req.BindJsonAndValid(rc, movingForm)
|
||||||
|
rc.ReqParam = movingForm
|
||||||
|
biz.ErrIsNil(p.TagTreeApp.MovingTag(rc.MetaCtx, movingForm.FromPath, movingForm.ToPath))
|
||||||
|
}
|
||||||
|
|
||||||
// 获取用户可操作的标签路径
|
// 获取用户可操作的标签路径
|
||||||
func (p *TagTree) TagResources(rc *req.Ctx) {
|
func (p *TagTree) TagResources(rc *req.Ctx) {
|
||||||
resourceType := int8(rc.PathParamInt("rtype"))
|
resourceType := int8(rc.PathParamInt("rtype"))
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ type TagTree interface {
|
|||||||
// ChangeParentTag 变更指定类型标签的父标签
|
// ChangeParentTag 变更指定类型标签的父标签
|
||||||
ChangeParentTag(ctx context.Context, tagType entity.TagType, tagCode string, parentTagType entity.TagType, newParentCode string) error
|
ChangeParentTag(ctx context.Context, tagType entity.TagType, tagCode string, parentTagType entity.TagType, newParentCode string) error
|
||||||
|
|
||||||
|
// MovingTag 移动标签
|
||||||
|
MovingTag(ctx context.Context, fromTagPath string, toTagPath string) error
|
||||||
|
|
||||||
// DeleteTagByParam 删除标签,会删除该标签下所有子标签信息以及团队关联的标签信息
|
// DeleteTagByParam 删除标签,会删除该标签下所有子标签信息以及团队关联的标签信息
|
||||||
DeleteTagByParam(ctx context.Context, param *DelResourceTagParam) error
|
DeleteTagByParam(ctx context.Context, param *DelResourceTagParam) error
|
||||||
|
|
||||||
@@ -300,13 +303,7 @@ func (p *tagTreeAppImpl) SaveResourceTag(ctx context.Context, param *SaveResourc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除team关联的标签
|
return p.deleteByIds(ctx, delTagIds)
|
||||||
if err := p.tagTreeTeamRepo.DeleteByWheres(ctx, collx.M{"tag_id in ?": delTagIds}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.DeleteByWheres(ctx, collx.M{"id in ?": delTagIds}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -382,6 +379,34 @@ func (p *tagTreeAppImpl) ChangeParentTag(ctx context.Context, tagType entity.Tag
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *tagTreeAppImpl) MovingTag(ctx context.Context, fromTagPath string, toTagPath string) error {
|
||||||
|
fromTag := &entity.TagTree{CodePath: fromTagPath}
|
||||||
|
if err := p.GetBy(fromTag); err != nil {
|
||||||
|
return errorx.NewBiz("移动标签不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
toTag := &entity.TagTree{CodePath: toTagPath}
|
||||||
|
if err := p.GetBy(toTag); err != nil {
|
||||||
|
return errorx.NewBiz("目标标签不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取要移动标签的所有子标签
|
||||||
|
var childrenTags []*entity.TagTree
|
||||||
|
p.ListByQuery(&entity.TagTreeQuery{CodePathLike: fromTagPath}, &childrenTags)
|
||||||
|
|
||||||
|
// 获取父路径, 若fromTagPath=tag1/tag2/1|xxx则返回 tag1/tag2/
|
||||||
|
fromParentPath := entity.GetParentPath(fromTagPath, 0)
|
||||||
|
for _, childTag := range childrenTags {
|
||||||
|
// 替换path,若childPath = tag1/tag2/1|xxx/11|yyy, toTagPath=tag3/tag4则替换为tag3/tag4/1|xxx/11|yyy/
|
||||||
|
childTag.CodePath = strings.Replace(childTag.CodePath, fromParentPath, toTagPath, 1)
|
||||||
|
if err := p.UpdateById(ctx, childTag); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *DelResourceTagParam) error {
|
func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *DelResourceTagParam) error {
|
||||||
// 获取资源编号对应的资源标签信息
|
// 获取资源编号对应的资源标签信息
|
||||||
var resourceTags []*entity.TagTree
|
var resourceTags []*entity.TagTree
|
||||||
@@ -410,16 +435,7 @@ func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *DelResourc
|
|||||||
return item.Id
|
return item.Id
|
||||||
})
|
})
|
||||||
// 删除code_path下的所有子标签
|
// 删除code_path下的所有子标签
|
||||||
if err := p.DeleteByWheres(ctx, collx.M{
|
return p.deleteByIds(ctx, childrenTagIds)
|
||||||
"id in ?": childrenTagIds,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除team关联的标签
|
|
||||||
if err := p.tagTreeTeamRepo.DeleteByWheres(ctx, collx.M{"tag_id in ?": childrenTagIds}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -484,13 +500,8 @@ func (p *tagTreeAppImpl) Delete(ctx context.Context, id uint64) error {
|
|||||||
return errorx.NewBiz("您无权删除该标签")
|
return errorx.NewBiz("您无权删除该标签")
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.Tx(ctx, func(ctx context.Context) error {
|
return p.DeleteTagByParam(ctx, &DelResourceTagParam{
|
||||||
return p.DeleteTagByParam(ctx, &DelResourceTagParam{
|
Id: id,
|
||||||
Id: id,
|
|
||||||
})
|
|
||||||
}, func(ctx context.Context) error {
|
|
||||||
// 删除该标签关联的团队信息
|
|
||||||
return p.tagTreeTeamRepo.DeleteByCond(ctx, &entity.TagTreeTeam{TagId: id})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,3 +544,14 @@ func (p *tagTreeAppImpl) toTags(parentTags []*entity.TagTree, param *ResourceTag
|
|||||||
|
|
||||||
return tags
|
return tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *tagTreeAppImpl) deleteByIds(ctx context.Context, tagIds []uint64) error {
|
||||||
|
if err := p.DeleteByWheres(ctx, collx.M{
|
||||||
|
"id in ?": tagIds,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除team关联的标签
|
||||||
|
return p.tagTreeTeamRepo.DeleteByWheres(ctx, collx.M{"tag_id in ?": tagIds})
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ type TagTreeQuery struct {
|
|||||||
model.Model
|
model.Model
|
||||||
|
|
||||||
Type TagType `json:"type"`
|
Type TagType `json:"type"`
|
||||||
Code string `json:"code"` // 标识
|
|
||||||
Codes []string
|
Codes []string
|
||||||
CodePath string `json:"codePath"` // 标识路径
|
CodePaths []string // 标识路径
|
||||||
CodePaths []string
|
Name string `json:"name"` // 名称
|
||||||
Name string `json:"name"` // 名称
|
CodePathLike string // 标识符路径模糊查询
|
||||||
CodePathLike string // 标识符路径模糊查询
|
|
||||||
CodePathLikes []string
|
CodePathLikes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,23 +51,7 @@ func (pt *TagTree) IsRoot() bool {
|
|||||||
|
|
||||||
// GetParentPath 获取父标签路径, 如CodePath = test/test1/test2/ -> index = 0 => test/test1/ index = 1 => test/
|
// GetParentPath 获取父标签路径, 如CodePath = test/test1/test2/ -> index = 0 => test/test1/ index = 1 => test/
|
||||||
func (pt *TagTree) GetParentPath(index int) string {
|
func (pt *TagTree) GetParentPath(index int) string {
|
||||||
// 去除末尾的斜杠
|
return GetParentPath(pt.CodePath, index)
|
||||||
codePath := strings.TrimSuffix(pt.CodePath, "/")
|
|
||||||
|
|
||||||
// 使用 Split 方法将路径按斜杠分割成切片
|
|
||||||
paths := strings.Split(codePath, "/")
|
|
||||||
|
|
||||||
// 确保索引在有效范围内
|
|
||||||
if index < 0 {
|
|
||||||
index = 0
|
|
||||||
} else if index > len(paths)-2 {
|
|
||||||
index = len(paths) - 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按索引拼接父标签路径
|
|
||||||
parentPath := strings.Join(paths[:len(paths)-index-1], "/")
|
|
||||||
|
|
||||||
return parentPath + "/"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTagPath 获取标签段路径,不获取对应资源相关路径
|
// GetTagPath 获取标签段路径,不获取对应资源相关路径
|
||||||
@@ -141,6 +125,45 @@ func GetCodeByPath(tagType TagType, codePaths ...string) []string {
|
|||||||
return collx.ArrayDeduplicate[string](codes)
|
return collx.ArrayDeduplicate[string](codes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetParentPath 获取父标签路径, 如CodePath = test/test1/test2/ -> index = 0 => test/test1/ index = 1 => test/
|
||||||
|
func GetParentPath(codePath string, index int) string {
|
||||||
|
// 去除末尾的斜杠
|
||||||
|
codePath = strings.TrimSuffix(codePath, CodePathSeparator)
|
||||||
|
|
||||||
|
// 使用 Split 方法将路径按斜杠分割成切片
|
||||||
|
paths := strings.Split(codePath, CodePathSeparator)
|
||||||
|
|
||||||
|
// 确保索引在有效范围内
|
||||||
|
if index < 0 {
|
||||||
|
index = 0
|
||||||
|
} else if index > len(paths)-2 {
|
||||||
|
index = len(paths) - 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按索引拼接父标签路径
|
||||||
|
parentPath := strings.Join(paths[:len(paths)-index-1], CodePathSeparator)
|
||||||
|
|
||||||
|
return parentPath + CodePathSeparator
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCodePath 根据表情路径获取所有相关的标签codePath
|
||||||
|
func GetAllCodePath(codePath string) []string {
|
||||||
|
// 去除末尾的斜杠
|
||||||
|
codePath = strings.TrimSuffix(codePath, CodePathSeparator)
|
||||||
|
|
||||||
|
// 使用 Split 方法将路径按斜杠分割成切片
|
||||||
|
paths := strings.Split(codePath, CodePathSeparator)
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
var partialPath string
|
||||||
|
for _, path := range paths {
|
||||||
|
partialPath += path + CodePathSeparator
|
||||||
|
result = append(result, partialPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// TagPathSection 标签路径段
|
// TagPathSection 标签路径段
|
||||||
type TagPathSection struct {
|
type TagPathSection struct {
|
||||||
Type TagType `json:"type"` // 类型: -1.普通标签; 其他值则为对应的资源类型
|
Type TagType `json:"type"` // 类型: -1.普通标签; 其他值则为对应的资源类型
|
||||||
|
|||||||
23
server/internal/tag/domain/entity/tag_tree_test.go
Normal file
23
server/internal/tag/domain/entity/tag_tree_test.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetPathSection(t *testing.T) {
|
||||||
|
fromPath := "tag1/tag2/1|xx/"
|
||||||
|
childPath := "tag1/tag2/1|xx/11|yy/"
|
||||||
|
toPath := "tag3/"
|
||||||
|
parentSection := GetTagPathSections(GetParentPath(fromPath, 0))
|
||||||
|
|
||||||
|
childSection := GetTagPathSections(childPath)
|
||||||
|
res := toPath + childSection[len(GetTagPathSections(fromPath)):].ToCodePath()
|
||||||
|
res1 := toPath + childSection[len(parentSection):].ToCodePath()
|
||||||
|
|
||||||
|
pPath := GetParentPath(fromPath, 0)
|
||||||
|
r := strings.Replace(childPath, pPath, toPath, 1)
|
||||||
|
r1 := strings.Replace(fromPath, pPath, toPath, 1)
|
||||||
|
fmt.Println(res, res1, r, r1)
|
||||||
|
}
|
||||||
@@ -16,17 +16,13 @@ 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.type, 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 p.id, p.type, 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)
|
params := make([]any, 0)
|
||||||
if condition.Name != "" {
|
if condition.Name != "" {
|
||||||
sql = sql + " AND p.name LIKE ?"
|
sql = sql + " AND p.name LIKE ?"
|
||||||
params = append(params, "%"+condition.Name+"%")
|
params = append(params, "%"+condition.Name+"%")
|
||||||
}
|
}
|
||||||
if condition.CodePath != "" {
|
|
||||||
sql = sql + " AND p.code_path = ?"
|
|
||||||
params = append(params, condition.CodePath)
|
|
||||||
}
|
|
||||||
if len(condition.Codes) > 0 {
|
if len(condition.Codes) > 0 {
|
||||||
sql = sql + " AND p.code IN (?)"
|
sql = sql + " AND p.code IN (?)"
|
||||||
params = append(params, condition.Codes)
|
params = append(params, condition.Codes)
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ func InitTagTreeRouter(router *gin.RouterGroup) {
|
|||||||
|
|
||||||
req.NewDelete(":id", m.DelTagTree).Log(req.NewLogSave("标签树-删除信息")).RequiredPermissionCode("tag:del"),
|
req.NewDelete(":id", m.DelTagTree).Log(req.NewLogSave("标签树-删除信息")).RequiredPermissionCode("tag:del"),
|
||||||
|
|
||||||
|
req.NewPost("/moving", m.MovingTag).Log(req.NewLogSave("标签树-移动标签")).RequiredPermissionCode("tag:save"),
|
||||||
|
|
||||||
req.NewGet("/resources/:rtype/tag-paths", m.TagResources),
|
req.NewGet("/resources/:rtype/tag-paths", m.TagResources),
|
||||||
|
|
||||||
req.NewGet("/resources/count", m.CountTagResource),
|
req.NewGet("/resources/count", m.CountTagResource),
|
||||||
|
|||||||
@@ -493,6 +493,7 @@ CREATE TABLE `t_machine_term_op` (
|
|||||||
`machine_id` bigint NOT NULL COMMENT '机器id',
|
`machine_id` bigint NOT NULL COMMENT '机器id',
|
||||||
`username` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '登录用户名',
|
`username` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '登录用户名',
|
||||||
`record_file_path` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '终端回放文件路径',
|
`record_file_path` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '终端回放文件路径',
|
||||||
|
`exec_cmds` TEXT NULL COMMENT '执行的命令记录'
|
||||||
`creator_id` bigint unsigned DEFAULT NULL,
|
`creator_id` bigint unsigned DEFAULT NULL,
|
||||||
`creator` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
`creator` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
||||||
`create_time` datetime NOT NULL,
|
`create_time` datetime NOT NULL,
|
||||||
|
|||||||
@@ -143,4 +143,8 @@ UPDATE t_tag_tree SET is_deleted = 1, delete_time = NOW() WHERE `type` = 2;
|
|||||||
UPDATE t_tag_tree SET `type` = 2 WHERE `type` = 100;
|
UPDATE t_tag_tree SET `type` = 2 WHERE `type` = 100;
|
||||||
|
|
||||||
ALTER TABLE t_tag_tree DROP COLUMN pid;
|
ALTER TABLE t_tag_tree DROP COLUMN pid;
|
||||||
ALTER TABLE t_tag_tree_team DROP COLUMN tag_path;
|
ALTER TABLE t_tag_tree_team DROP COLUMN tag_path;
|
||||||
|
|
||||||
|
-- 新增记录执行命令字段
|
||||||
|
ALTER TABLE t_machine_term_op ADD exec_cmds TEXT NULL COMMENT '执行的命令记录';
|
||||||
|
ALTER TABLE t_machine_term_op CHANGE exec_cmds exec_cmds TEXT NULL COMMENT '执行的命令记录' AFTER record_file_path;
|
||||||
|
|||||||
Reference in New Issue
Block a user