mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-03 16:00:25 +08:00
feat: 标签支持拖拽移动与机器支持执行命令查看
This commit is contained in:
@@ -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"`
|
||||
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
)
|
||||
|
||||
type Terminal struct {
|
||||
SshSession *ssh.Session
|
||||
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 {
|
||||
s := string(buf)
|
||||
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 len(buf) == 0 {
|
||||
continue
|
||||
}
|
||||
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:
|
||||
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) {
|
||||
tagTrees = append(tagTrees, tag)
|
||||
break
|
||||
}
|
||||
for _, tag := range allTags {
|
||||
tagTrees = append(tagTrees, tag)
|
||||
}
|
||||
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,13 +500,8 @@ 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})
|
||||
return p.DeleteTagByParam(ctx, &DelResourceTagParam{
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,12 +6,10 @@ type TagTreeQuery struct {
|
||||
model.Model
|
||||
|
||||
Type TagType `json:"type"`
|
||||
Code string `json:"code"` // 标识
|
||||
Codes []string
|
||||
CodePath string `json:"codePath"` // 标识路径
|
||||
CodePaths []string
|
||||
Name string `json:"name"` // 名称
|
||||
CodePathLike 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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
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