diff --git a/backend/internal/handler/ticket.go b/backend/internal/handler/ticket.go index 8a2fddc..bae87a2 100644 --- a/backend/internal/handler/ticket.go +++ b/backend/internal/handler/ticket.go @@ -427,12 +427,13 @@ func ApproveTicket(c *gin.Context) { if input.Action == "reject" { newStatus = "rejected" } else if input.Action == "approve" { - // Check if there are more pending approvals - var pendingCount int64 - model.DB.Model(&model.TicketApproval{}).Where("ticket_id = ? AND action IS NULL", id).Count(&pendingCount) - if pendingCount > 0 { + // Check if there are more pending AUDIT approvals (exclude execute type) + var pendingAuditCount int64 + model.DB.Model(&model.TicketApproval{}).Where("ticket_id = ? AND action IS NULL AND step_type != 'execute'", id).Count(&pendingAuditCount) + if pendingAuditCount > 0 { newStatus = "reviewing" } + // If all audit steps done → "approved" (ready for execution) } ticket.Status = &newStatus @@ -603,25 +604,36 @@ func ExecuteTicket(c *gin.Context) { execTimeout := getSettingInt("executor_timeout", 300) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(execTimeout)*time.Second) defer cancel() - // Dry-run: EXPLAIN pre-check + // Dry-run: EXPLAIN pre-check (skip DDL statements) if getSetting("executor_dry_run", "true") == "true" { + ddlPrefixes := []string{"CREATE", "ALTER", "DROP", "TRUNCATE", "RENAME"} statements := splitSQL(ticket.SQLContent) for _, stmt := range statements { stmt = strings.TrimSpace(stmt) - if stmt == "" || strings.HasPrefix(strings.ToUpper(stmt), "EXPLAIN") { + if stmt == "" { + continue + } + upperStmt := strings.ToUpper(stmt) + if strings.HasPrefix(upperStmt, "EXPLAIN") { + continue + } + // Skip DDL — EXPLAIN does not work on DDL statements + isDDL := false + for _, prefix := range ddlPrefixes { + if strings.HasPrefix(upperStmt, prefix) { + isDDL = true + break + } + } + if isDDL { continue } - var explainRows []string rows, err := db.QueryContext(ctx, "EXPLAIN "+stmt) if err != nil { execErr = fmt.Sprintf("预检查失败(EXPLAIN): %v", err) break } - for rows.Next() { - rows.Close() - break - } - _ = explainRows + rows.Close() } if execErr != "" { failStatus := "failed" @@ -753,8 +765,8 @@ func CancelTicket(c *gin.Context) { } } - if ticket.Status != nil && (*ticket.Status != "pending" && *ticket.Status != "reviewing") { - c.JSON(http.StatusBadRequest, gin.H{"detail": "Can only cancel pending or reviewing tickets"}) + if ticket.Status != nil && (*ticket.Status != "pending" && *ticket.Status != "reviewing" && *ticket.Status != "approved") { + c.JSON(http.StatusBadRequest, gin.H{"detail": "只能撤销待审核、审核中或已通过(待执行)的工单"}) return } diff --git a/frontend/src/constants/menu.ts b/frontend/src/constants/menu.ts index da1ab7c..588bd34 100644 --- a/frontend/src/constants/menu.ts +++ b/frontend/src/constants/menu.ts @@ -12,12 +12,12 @@ export const menuItems = [ // 管理 — 数据源 + 权限 { id: 'instances', label: '数据源', icon: 'Database', section: 'admin', roles: ['admin', 'dba'], permission: 'instances.manage' }, + { id: 'workflow', label: '审批流程', icon: 'GitBranch', section: 'admin', roles: ['admin', 'dba'], permission: 'workflow.manage' }, { id: 'users', label: '用户管理', icon: 'Users', section: 'admin', roles: ['admin'], permission: 'users.manage' }, { id: 'roles', label: '角色权限', icon: 'ShieldCheck', section: 'admin', roles: ['admin'], permission: 'roles.manage' }, { id: 'instance-groups', label: '权限组', icon: 'FolderLock', section: 'admin', roles: ['admin'], permission: 'instance-groups.manage' }, // 系统 - { id: 'workflow', label: '审批流程', icon: 'GitBranch', section: 'system', roles: ['admin', 'dba', 'developer', 'auditor'], permission: 'workflow.manage' }, { id: 'audit-logs', label: '审计日志', icon: 'ScrollText', section: 'system', roles: ['admin', 'dba', 'auditor'], permission: 'audit-logs.view' }, { id: 'auto-task', label: '自动任务', icon: 'Timer', section: 'system', roles: ['admin'], permission: 'auto-task.manage' }, { id: 'settings', label: '系统设置', icon: 'Settings', section: 'system', roles: ['admin'], permission: 'settings.manage' }, diff --git a/frontend/src/pages/TicketDetailPage.tsx b/frontend/src/pages/TicketDetailPage.tsx index 2423fe1..9076018 100644 --- a/frontend/src/pages/TicketDetailPage.tsx +++ b/frontend/src/pages/TicketDetailPage.tsx @@ -232,7 +232,7 @@ export default function TicketDetailPage() { canApprove: canApproveRole && (ticket.status === 'pending' || ticket.status === 'reviewing'), canExecute: adminOrDba && ticket.status === 'approved', canRollback: adminOrDba && (ticket.status === 'success' || ticket.status === 'failed'), - canCancel: applicant && (ticket.status === 'pending' || ticket.status === 'reviewing'), + canCancel: applicant && (ticket.status === 'pending' || ticket.status === 'reviewing' || ticket.status === 'approved'), }; }, [ticket, auth.user.role, auth.user.id]); diff --git a/frontend/src/pages/TicketsPage.tsx b/frontend/src/pages/TicketsPage.tsx index 3dbad4d..e57cd95 100644 --- a/frontend/src/pages/TicketsPage.tsx +++ b/frontend/src/pages/TicketsPage.tsx @@ -490,7 +490,7 @@ export default function TicketsPage() { )} - {(ticket.status === 'pending' || ticket.status === 'reviewing') && ( + {(ticket.status === 'pending' || ticket.status === 'reviewing' || ticket.status === 'approved') && (