mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
feat: 标签支持拖拽移动与机器支持执行命令查看
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
"countup.js": "^2.8.0",
|
||||
"cropperjs": "^1.6.1",
|
||||
"echarts": "^5.5.0",
|
||||
"element-plus": "^2.7.0",
|
||||
"element-plus": "^2.7.1",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -35,7 +35,7 @@
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.4.23",
|
||||
"vue-router": "^4.3.1",
|
||||
"vue-router": "^4.3.2",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"xterm-addon-search": "^0.13.0",
|
||||
@@ -57,7 +57,7 @@
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.75.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.9",
|
||||
"vite": "^5.2.10",
|
||||
"vue-eslint-parser": "^9.4.2"
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, onMounted } from 'vue';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
|
||||
@@ -42,6 +43,10 @@ const props = defineProps({
|
||||
selectTags: {
|
||||
type: [Array<any>],
|
||||
},
|
||||
tagType: {
|
||||
type: Number,
|
||||
default: TagResourceTypeEnum.Tag.value,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
@@ -54,7 +59,7 @@ const { tags } = toRefs(state);
|
||||
|
||||
onMounted(async () => {
|
||||
state.selectTags = props.selectTags;
|
||||
state.tags = await tagApi.getTagTrees.request({ type: -1 });
|
||||
state.tags = await tagApi.getTagTrees.request({ type: props.tagType });
|
||||
});
|
||||
|
||||
const changeTag = () => {
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
:before-close="handleClose"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
width="800"
|
||||
width="1000"
|
||||
@open="getTermOps()"
|
||||
>
|
||||
<page-table ref="pageTableRef" :page-api="machineApi.termOpRecs" :lazy="true" height="100%" v-model:query-form="query" :columns="columns">
|
||||
<template #action="{ data }">
|
||||
<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>
|
||||
</page-table>
|
||||
</el-dialog>
|
||||
@@ -26,6 +27,17 @@
|
||||
>
|
||||
<div ref="playerRef" id="rc-player"></div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -36,6 +48,7 @@ import * as AsciinemaPlayer from 'asciinema-player';
|
||||
import 'asciinema-player/dist/bundle/asciinema-player.css';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { dateFormat } from '@/common/utils/date';
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean },
|
||||
@@ -50,7 +63,7 @@ const columns = [
|
||||
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(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(120).fixedRight().alignCenter(),
|
||||
];
|
||||
|
||||
const playerRef = ref(null);
|
||||
@@ -63,11 +76,12 @@ const state = reactive({
|
||||
pageSize: 10,
|
||||
machineId: 0,
|
||||
},
|
||||
|
||||
playerDialogVisible: false,
|
||||
execCmdsDialogVisible: false,
|
||||
execCmds: [],
|
||||
});
|
||||
|
||||
const { dialogVisible, query, playerDialogVisible } = toRefs(state);
|
||||
const { dialogVisible, query, playerDialogVisible, execCmdsDialogVisible } = toRefs(state);
|
||||
|
||||
watch(props, async (newValue: any) => {
|
||||
const visible = newValue.visible;
|
||||
@@ -82,6 +96,11 @@ const getTermOps = async () => {
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const showExecCmds = (data: any) => {
|
||||
state.execCmds = JSON.parse(data.execCmds);
|
||||
state.execCmdsDialogVisible = true;
|
||||
};
|
||||
|
||||
let player: any = null;
|
||||
|
||||
const playRec = async (rec: any) => {
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
@node-contextmenu="nodeContextmenu"
|
||||
@node-click="treeNodeClick"
|
||||
:default-expanded-keys="defaultExpandedKeys"
|
||||
draggable
|
||||
:allow-drop="allowDrop"
|
||||
:allow-drag="allowDrag"
|
||||
@node-drop="handleDrop"
|
||||
:expand-on-click-node="false"
|
||||
: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) => {
|
||||
if (!tagPath) {
|
||||
return [];
|
||||
|
||||
@@ -5,6 +5,7 @@ export const tagApi = {
|
||||
getTagTrees: Api.newGet('/tag-trees'),
|
||||
saveTagTree: Api.newPost('/tag-trees'),
|
||||
delTagTree: Api.newDelete('/tag-trees/{id}'),
|
||||
movingTag: Api.newPost('/tag-trees/moving'),
|
||||
|
||||
getResourceTagPaths: Api.newGet('/tag-trees/resources/{resourceType}/tag-paths'),
|
||||
countTagResource: Api.newGet('/tag-trees/resources/count'),
|
||||
|
||||
@@ -30,6 +30,7 @@ require (
|
||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||
github.com/sijms/go-ora/v2 v2.8.10
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/veops/go-ansiterm v0.0.5
|
||||
go.mongodb.org/mongo-driver v1.14.0 // mongo
|
||||
golang.org/x/crypto v0.22.0 // ssh
|
||||
golang.org/x/oauth2 v0.18.0
|
||||
@@ -70,6 +71,7 @@ require (
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // 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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // 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/pmezard/go-difflib v1.0.0 // 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/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/scheduler"
|
||||
"mayfly-go/pkg/utils/jsonx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"os"
|
||||
"path"
|
||||
@@ -76,7 +77,24 @@ func (m *machineTermOpAppImpl) TermConn(ctx context.Context, cli *mcm.Cli, wsCon
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -87,6 +105,7 @@ func (m *machineTermOpAppImpl) TermConn(ctx context.Context, cli *mcm.Cli, wsCon
|
||||
if termOpRecord != nil {
|
||||
now := time.Now()
|
||||
termOpRecord.EndTime = &now
|
||||
termOpRecord.ExecCmds = jsonx.ToStr(mts.GetExecCmds())
|
||||
return m.Insert(ctx, termOpRecord)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -11,6 +11,7 @@ type MachineTermOp struct {
|
||||
MachineId uint64 `json:"machineId"`
|
||||
Username string `json:"username"`
|
||||
RecordFilePath string `json:"recordFilePath"` // 回放文件路径
|
||||
ExecCmds string `json:"execCmds"` // 执行的命令
|
||||
|
||||
CreateTime *time.Time `json:"createTime"`
|
||||
CreatorId uint64 `json:"creatorId"`
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
type Terminal struct {
|
||||
SshSession *ssh.Session
|
||||
|
||||
StdinPipe io.WriteCloser
|
||||
StdoutReader *bufio.Reader
|
||||
}
|
||||
@@ -44,7 +45,7 @@ func (t *Terminal) Write(p []byte) (int, error) {
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/logx"
|
||||
@@ -27,6 +28,7 @@ type TerminalSession struct {
|
||||
ID string
|
||||
wsConn *websocket.Conn
|
||||
terminal *Terminal
|
||||
handler *TerminalHandler
|
||||
recorder *Recorder
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@@ -34,7 +36,21 @@ type TerminalSession struct {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -52,12 +68,19 @@ func NewTerminalSession(sessionId string, ws *websocket.Conn, cli *Cli, rows, co
|
||||
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())
|
||||
tick := time.NewTicker(time.Millisecond * time.Duration(60))
|
||||
ts := &TerminalSession{
|
||||
ID: sessionId,
|
||||
wsConn: ws,
|
||||
terminal: terminal,
|
||||
handler: handler,
|
||||
recorder: recorder,
|
||||
ctx: ctx,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
for {
|
||||
select {
|
||||
@@ -116,20 +147,27 @@ func (ts TerminalSession) writeToWebsocket() {
|
||||
case <-ts.ctx.Done():
|
||||
return
|
||||
case <-ts.tick.C:
|
||||
if len(buf) > 0 {
|
||||
if len(buf) == 0 {
|
||||
continue
|
||||
}
|
||||
if ts.handler != nil {
|
||||
ts.handler.HandleRead(buf)
|
||||
}
|
||||
|
||||
s := string(buf)
|
||||
if err := WriteMessage(ts.wsConn, s); err != nil {
|
||||
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:
|
||||
if data != utf8.RuneError {
|
||||
p := make([]byte, utf8.RuneLen(data))
|
||||
@@ -149,16 +187,15 @@ type WsMsg struct {
|
||||
Rows int `json:"rows"`
|
||||
}
|
||||
|
||||
// 接收客户端ws发送过来的消息,并写入终端会话中。
|
||||
// receiveWsMsg 接收客户端ws发送过来的消息,并写入终端会话中。
|
||||
func (ts *TerminalSession) receiveWsMsg() {
|
||||
wsConn := ts.wsConn
|
||||
for {
|
||||
select {
|
||||
case <-ts.ctx.Done():
|
||||
return
|
||||
default:
|
||||
// read websocket msg
|
||||
_, wsData, err := wsConn.ReadMessage()
|
||||
_, wsData, err := ts.wsConn.ReadMessage()
|
||||
if err != nil {
|
||||
logx.Debugf("机器ssh终端读取websocket消息失败: %s", err.Error())
|
||||
return
|
||||
@@ -166,7 +203,7 @@ func (ts *TerminalSession) receiveWsMsg() {
|
||||
// 解析消息
|
||||
msgObj, err := parseMsg(wsData)
|
||||
if err != nil {
|
||||
WriteMessage(wsConn, "\r\n\033[1;31m提示: 消息内容解析失败...\033[0m")
|
||||
ts.WriteToWs(GetErrorContentRn("消息内容解析失败..."))
|
||||
logx.Error("机器ssh终端消息解析失败: ", err)
|
||||
return
|
||||
}
|
||||
@@ -179,14 +216,25 @@ func (ts *TerminalSession) receiveWsMsg() {
|
||||
}
|
||||
}
|
||||
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))
|
||||
if err != nil {
|
||||
logx.Debugf("机器ssh终端写入消息失败: %s", err)
|
||||
logx.Errorf("写入数据至ssh终端失败: %s", err)
|
||||
ts.WriteToWs(GetErrorContentRn(fmt.Sprintf("写入数据至ssh终端失败: %s", err.Error())))
|
||||
}
|
||||
case Ping:
|
||||
_, err := ts.terminal.SshSession.SendRequest("ping", true, nil)
|
||||
if err != nil {
|
||||
WriteMessage(wsConn, "\r\n\033[1;31m提示: 终端连接已断开...\033[0m")
|
||||
ts.WriteToWs(GetErrorContentRn("终端连接已断开..."))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -194,8 +242,9 @@ func (ts *TerminalSession) receiveWsMsg() {
|
||||
}
|
||||
}
|
||||
|
||||
func WriteMessage(ws *websocket.Conn, msg string) error {
|
||||
return ws.WriteMessage(websocket.TextMessage, []byte(msg))
|
||||
// WriteToWs 将消息写入websocket连接
|
||||
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"`
|
||||
}
|
||||
|
||||
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) {
|
||||
tagType := entity.TagType(rc.QueryInt("type"))
|
||||
// 超管返回所有标签树
|
||||
if rc.GetLoginAccount().Id == consts.AdminId {
|
||||
var tagTrees vo.TagTreeVOS
|
||||
p.TagTreeApp.ListByQuery(&entity.TagTreeQuery{Type: tagType}, &tagTrees)
|
||||
rc.ResData = tagTrees.ToTrees(0)
|
||||
accountTags := p.TagTreeApp.GetAccountTags(rc.GetLoginAccount().Id, &entity.TagTreeQuery{Type: tagType})
|
||||
if len(accountTags) == 0 {
|
||||
rc.ResData = []any{}
|
||||
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标签开头的子标签
|
||||
var tags []*entity.TagTree
|
||||
p.TagTreeApp.ListByQuery(&entity.TagTreeQuery{CodePathLikes: collx.MapKeys(rootTag), Type: tagType}, &tags)
|
||||
|
||||
allTags := p.complteTags(accountTags)
|
||||
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) {
|
||||
for _, tag := range allTags {
|
||||
tagTrees = append(tagTrees, tag)
|
||||
break
|
||||
}
|
||||
rc.ResData = tagTrees.ToTrees(0)
|
||||
}
|
||||
|
||||
// complteTags 补全标签信息,使其能构造为树结构
|
||||
func (p *TagTree) complteTags(resourceTags []*entity.TagTree) []*entity.TagTree {
|
||||
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) {
|
||||
@@ -82,6 +86,13 @@ func (p *TagTree) DelTagTree(rc *req.Ctx) {
|
||||
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) {
|
||||
resourceType := int8(rc.PathParamInt("rtype"))
|
||||
|
||||
@@ -77,6 +77,9 @@ type TagTree interface {
|
||||
// ChangeParentTag 变更指定类型标签的父标签
|
||||
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(ctx context.Context, param *DelResourceTagParam) error
|
||||
|
||||
@@ -300,13 +303,7 @@ func (p *tagTreeAppImpl) SaveResourceTag(ctx context.Context, param *SaveResourc
|
||||
}
|
||||
}
|
||||
|
||||
// 删除team关联的标签
|
||||
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 p.deleteByIds(ctx, delTagIds)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -382,6 +379,34 @@ func (p *tagTreeAppImpl) ChangeParentTag(ctx context.Context, tagType entity.Tag
|
||||
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 {
|
||||
// 获取资源编号对应的资源标签信息
|
||||
var resourceTags []*entity.TagTree
|
||||
@@ -410,16 +435,7 @@ func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *DelResourc
|
||||
return item.Id
|
||||
})
|
||||
// 删除code_path下的所有子标签
|
||||
if err := p.DeleteByWheres(ctx, collx.M{
|
||||
"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 p.deleteByIds(ctx, childrenTagIds)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -484,14 +500,9 @@ func (p *tagTreeAppImpl) Delete(ctx context.Context, id uint64) error {
|
||||
return errorx.NewBiz("您无权删除该标签")
|
||||
}
|
||||
|
||||
return p.Tx(ctx, func(ctx context.Context) error {
|
||||
return p.DeleteTagByParam(ctx, &DelResourceTagParam{
|
||||
Id: id,
|
||||
})
|
||||
}, func(ctx context.Context) error {
|
||||
// 删除该标签关联的团队信息
|
||||
return p.tagTreeTeamRepo.DeleteByCond(ctx, &entity.TagTreeTeam{TagId: id})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *tagTreeAppImpl) toTags(parentTags []*entity.TagTree, param *ResourceTag) []*entity.TagTree {
|
||||
@@ -533,3 +544,14 @@ func (p *tagTreeAppImpl) toTags(parentTags []*entity.TagTree, param *ResourceTag
|
||||
|
||||
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,10 +6,8 @@ type TagTreeQuery struct {
|
||||
model.Model
|
||||
|
||||
Type TagType `json:"type"`
|
||||
Code string `json:"code"` // 标识
|
||||
Codes []string
|
||||
CodePath string `json:"codePath"` // 标识路径
|
||||
CodePaths []string
|
||||
CodePaths []string // 标识路径
|
||||
Name string `json:"name"` // 名称
|
||||
CodePathLike string // 标识符路径模糊查询
|
||||
CodePathLikes []string
|
||||
|
||||
@@ -51,23 +51,7 @@ func (pt *TagTree) IsRoot() bool {
|
||||
|
||||
// GetParentPath 获取父标签路径, 如CodePath = test/test1/test2/ -> index = 0 => test/test1/ index = 1 => test/
|
||||
func (pt *TagTree) GetParentPath(index int) string {
|
||||
// 去除末尾的斜杠
|
||||
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 + "/"
|
||||
return GetParentPath(pt.CodePath, index)
|
||||
}
|
||||
|
||||
// GetTagPath 获取标签段路径,不获取对应资源相关路径
|
||||
@@ -141,6 +125,45 @@ func GetCodeByPath(tagType TagType, codePaths ...string) []string {
|
||||
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 标签路径段
|
||||
type TagPathSection struct {
|
||||
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) {
|
||||
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)
|
||||
if condition.Name != "" {
|
||||
sql = sql + " AND p.name LIKE ?"
|
||||
params = append(params, "%"+condition.Name+"%")
|
||||
}
|
||||
if condition.CodePath != "" {
|
||||
sql = sql + " AND p.code_path = ?"
|
||||
params = append(params, condition.CodePath)
|
||||
}
|
||||
if len(condition.Codes) > 0 {
|
||||
sql = sql + " AND p.code IN (?)"
|
||||
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.NewPost("/moving", m.MovingTag).Log(req.NewLogSave("标签树-移动标签")).RequiredPermissionCode("tag:save"),
|
||||
|
||||
req.NewGet("/resources/:rtype/tag-paths", m.TagResources),
|
||||
|
||||
req.NewGet("/resources/count", m.CountTagResource),
|
||||
|
||||
@@ -493,6 +493,7 @@ CREATE TABLE `t_machine_term_op` (
|
||||
`machine_id` bigint NOT NULL COMMENT '机器id',
|
||||
`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 '终端回放文件路径',
|
||||
`exec_cmds` TEXT NULL COMMENT '执行的命令记录'
|
||||
`creator_id` bigint unsigned DEFAULT NULL,
|
||||
`creator` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
|
||||
`create_time` datetime NOT NULL,
|
||||
|
||||
@@ -144,3 +144,7 @@ UPDATE t_tag_tree SET `type` = 2 WHERE `type` = 100;
|
||||
|
||||
ALTER TABLE t_tag_tree DROP COLUMN pid;
|
||||
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