From cdb52aa4a9e13773aaffd17dedf093852ebd7fa0 Mon Sep 17 00:00:00 2001 From: logan Date: Sun, 31 May 2026 21:40:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B7=A5=E5=8D=95=E5=AE=A1=E6=89=B9?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E6=94=AF=E6=8C=81=20workflow=20=E6=8C=87?= =?UTF-8?q?=E5=AE=9A=E7=94=A8=E6=88=B7=E5=AE=A1=E6=89=B9=20+=20Redis=20?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 工单创建时读取 workflow_steps 的 approver_ids 创建审批记录 - 有指定用户:为每个用户创建审批记录(任一通过即可) - 无指定用户:按 approver_role 角色匹配 fallback - 无 workflow:默认 admin 审批 2. Redis 工单执行:复用 getRedisClient + parseCommandParts 执行写命令 - 支持 SET/DEL/HSET 等所有写操作 - 解析 db0/db1 格式的目标库 - 返回 [OK] 命令 → 结果 --- backend/internal/handler/ticket.go | 117 ++++++++++++++++++++++++----- 1 file changed, 100 insertions(+), 17 deletions(-) diff --git a/backend/internal/handler/ticket.go b/backend/internal/handler/ticket.go index 11836cb..cb29b9e 100644 --- a/backend/internal/handler/ticket.go +++ b/backend/internal/handler/ticket.go @@ -1,6 +1,7 @@ package handler import ( + "context" "encoding/json" "fmt" "net/http" @@ -211,23 +212,76 @@ func CreateTicket(c *gin.Context) { return } - // Auto-create approval record: step 1, approver = instance owner or admin - stepName := "审核" - stepType := "audit" - approverRole := "admin" - // Find first admin as default approver - var adminUser model.User - if err := model.DB.Where("role = ? AND is_active = 1", "admin").First(&adminUser).Error; err == nil { - adminID := adminUser.ID - approval := model.TicketApproval{ - TicketID: ticket.ID, - StepNumber: 1, - StepName: &stepName, - StepType: &stepType, - ApproverID: &adminID, - ApproverRole: &approverRole, + // Create approval records based on workflow or default to admin + if input.WorkflowID != nil && *input.WorkflowID > 0 { + // Load workflow steps + var wfSteps []model.WorkflowStep + model.DB.Where("workflow_id = ?", *input.WorkflowID).Order("step_number").Find(&wfSteps) + for _, ws := range wfSteps { + if ws.StepType == "submit" { + continue // submit step needs no approver + } + // Parse approver_ids from JSON + var approverIDs []int + if ws.ApproverIDs != nil && *ws.ApproverIDs != "" { + json.Unmarshal([]byte(*ws.ApproverIDs), &approverIDs) + } + // Create one approval record per approver (any one can approve) + if len(approverIDs) > 0 { + for _, uid := range approverIDs { + uidCopy := uid + stepName := ws.Name + stepType := ws.StepType + approval := model.TicketApproval{ + TicketID: ticket.ID, + StepNumber: ws.StepNumber, + StepName: &stepName, + StepType: &stepType, + ApproverID: &uidCopy, + } + model.DB.Create(&approval) + } + } else { + // No specific users — use role-based fallback + stepName := ws.Name + stepType := ws.StepType + var roleUser model.User + role := "admin" + if ws.ApproverRole != nil && *ws.ApproverRole != "" { + role = *ws.ApproverRole + } + if model.DB.Where("role = ? AND is_active = 1", role).First(&roleUser).Error == nil { + rid := roleUser.ID + approval := model.TicketApproval{ + TicketID: ticket.ID, + StepNumber: ws.StepNumber, + StepName: &stepName, + StepType: &stepType, + ApproverID: &rid, + ApproverRole: &role, + } + model.DB.Create(&approval) + } + } + } + } else { + // No workflow — default: first admin as approver + stepName := "审核" + stepType := "audit" + approverRole := "admin" + var adminUser model.User + if err := model.DB.Where("role = ? AND is_active = 1", "admin").First(&adminUser).Error; err == nil { + adminID := adminUser.ID + approval := model.TicketApproval{ + TicketID: ticket.ID, + StepNumber: 1, + StepName: &stepName, + StepType: &stepType, + ApproverID: &adminID, + ApproverRole: &approverRole, + } + model.DB.Create(&approval) } - model.DB.Create(&approval) } c.JSON(http.StatusCreated, ticket) @@ -339,7 +393,36 @@ func ExecuteTicket(c *gin.Context) { var execErr string if ticket.DBType == "redis" { - execErr = "Redis command execution not yet supported in ticket workflow" + // Execute Redis command (write operations allowed in ticket workflow) + redisDB := 0 + if ticket.TargetDatabase != "" { + if n, err := strconv.Atoi(strings.TrimPrefix(ticket.TargetDatabase, "db")); err == nil && n >= 0 { + redisDB = n + } + } + client, err := getRedisClient(&instance, redisDB) + if err != nil { + execErr = fmt.Sprintf("Redis connection failed: %v", err) + } else { + defer client.Close() + ctx := context.Background() + parts := parseCommandParts(ticket.SQLContent) + if len(parts) == 0 { + execErr = "empty command" + } else { + cmd := strings.ToUpper(parts[0]) + args := make([]interface{}, len(parts)) + for i, p := range parts { + args[i] = p + } + result, err := client.Do(ctx, args...).Result() + if err != nil { + execErr = fmt.Sprintf("%s failed: %v", cmd, err) + } else { + execResult = fmt.Sprintf("[OK] %s → %v", ticket.SQLContent, result) + } + } + } } else { // MySQL/PG execution db, err := getDBConnection(&instance, ticket.TargetDatabase)