diff --git a/backend/internal/handler/instance_group.go b/backend/internal/handler/instance_group.go index 04b022d..4f3c973 100644 --- a/backend/internal/handler/instance_group.go +++ b/backend/internal/handler/instance_group.go @@ -46,13 +46,53 @@ func UpdateInstanceGroup(c *gin.Context) { return } - var input map[string]interface{} + var input struct { + Name *string `json:"name"` + Description *string `json:"description"` + DBType *string `json:"db_type"` + Permissions *string `json:"permissions"` + IsActive *bool `json:"is_active"` + WorkflowID *int `json:"workflow_id"` + AccessType *string `json:"access_type"` + AllowedDatabases *string `json:"allowed_databases"` + } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } - if err := model.DB.Model(&group).Updates(input).Error; err != nil { + updates := map[string]interface{}{} + if input.Name != nil { + updates["name"] = *input.Name + } + if input.Description != nil { + updates["description"] = *input.Description + } + if input.DBType != nil { + updates["db_type"] = *input.DBType + } + if input.Permissions != nil { + updates["permissions"] = *input.Permissions + } + if input.IsActive != nil { + updates["is_active"] = *input.IsActive + } + if input.WorkflowID != nil { + updates["workflow_id"] = *input.WorkflowID + } + if input.AccessType != nil { + updates["access_type"] = *input.AccessType + } + if input.AllowedDatabases != nil { + updates["allowed_databases"] = *input.AllowedDatabases + } + + if len(updates) == 0 { + c.JSON(http.StatusOK, enrichGroup(group)) + return + } + + if err := model.DB.Model(&group).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } diff --git a/backend/internal/handler/scheduled_task.go b/backend/internal/handler/scheduled_task.go new file mode 100644 index 0000000..64be898 --- /dev/null +++ b/backend/internal/handler/scheduled_task.go @@ -0,0 +1,97 @@ +package handler + +import ( + "net/http" + "time" + + "sql-platform/internal/middleware" + "sql-platform/internal/model" + + "github.com/gin-gonic/gin" +) + +// GET /api/v1/tickets/scheduled/list +// Lists tickets that have scheduled_at set and status is "approved" (waiting for scheduled execution). +func ListScheduledTasks(c *gin.Context) { + userID, _, role := middleware.GetCurrentUser(c) + + query := model.DB.Model(&model.Ticket{}). + Where("scheduled_at IS NOT NULL AND scheduled_at != '' AND status IN ?", []string{"approved", "scheduled"}). + Order("scheduled_at ASC") + + // Filter by user role (same logic as ListTickets) + switch role { + case "developer": + query = query.Where("applicant_id = ?", userID) + case "dba", "auditor": + query = query.Where( + "applicant_id = ? OR id IN (SELECT ticket_id FROM ticket_approvals WHERE approver_id = ?)", + userID, userID, + ) + // admin sees everything + } + + var total int64 + query.Count(&total) + + var tickets []model.Ticket + query.Find(&tickets) + + // Enrich with instance_name and instance_db_type + type ScheduledTicketItem struct { + model.Ticket + InstanceName string `json:"instance_name"` + InstanceDBType string `json:"instance_db_type"` + } + + items := make([]ScheduledTicketItem, 0, len(tickets)) + for _, t := range tickets { + item := ScheduledTicketItem{ + Ticket: t, + } + var instance model.Instance + if err := model.DB.Select("name, db_type").First(&instance, t.InstanceID).Error; err == nil { + item.InstanceName = instance.Name + item.InstanceDBType = instance.DBType + } + items = append(items, item) + } + + c.JSON(http.StatusOK, gin.H{ + "items": items, + "total": total, + }) +} + +// POST /api/v1/tickets/:id/cancel-schedule +// Cancels a scheduled task by clearing scheduled_at and setting status to "cancelled". +func CancelScheduledTask(c *gin.Context) { + id := c.Param("id") + userID, _, role := middleware.GetCurrentUser(c) + + var ticket model.Ticket + if err := model.DB.First(&ticket, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"detail": "Ticket not found"}) + return + } + + // Only the applicant or admin/dba can cancel + if ticket.ApplicantID != userID && role != "admin" && role != "dba" { + c.JSON(http.StatusForbidden, gin.H{"detail": "Permission denied"}) + return + } + + // Only allow cancellation of scheduled tickets + if !ticket.ScheduledAt.Valid { + c.JSON(http.StatusBadRequest, gin.H{"detail": "Ticket is not scheduled"}) + return + } + + status := "cancelled" + ticket.Status = &status + ticket.ScheduledAt.Valid = false + ticket.ScheduledAt.Time = time.Time{} + model.DB.Save(&ticket) + + c.JSON(http.StatusOK, gin.H{"message": "Scheduled task cancelled", "ticket_id": ticket.ID}) +} diff --git a/backend/router/router.go b/backend/router/router.go index eb52673..7afd9cd 100644 --- a/backend/router/router.go +++ b/backend/router/router.go @@ -27,10 +27,12 @@ func Setup(r *gin.Engine) { // Tickets auth.GET("/tickets/statistics", handler.TicketStatistics) auth.GET("/tickets", handler.ListTickets) + auth.GET("/tickets/scheduled/list", handler.ListScheduledTasks) auth.GET("/tickets/:id", handler.GetTicket) auth.POST("/tickets", handler.CreateTicket) auth.POST("/tickets/:id/approve", handler.ApproveTicket) auth.POST("/tickets/:id/execute", handler.ExecuteTicket) + auth.POST("/tickets/:id/cancel-schedule", handler.CancelScheduledTask) // Instances auth.GET("/instances", handler.ListInstances) diff --git a/frontend/src/pages/WorkflowPage.tsx b/frontend/src/pages/WorkflowPage.tsx index 6e33609..d25e5e5 100644 --- a/frontend/src/pages/WorkflowPage.tsx +++ b/frontend/src/pages/WorkflowPage.tsx @@ -625,7 +625,7 @@ export default function WorkflowPage() { {wf.is_active ? '已启用' : '已禁用'} - {wf.steps.length} 个阶段 + {(wf.steps || []).length} 个阶段 @@ -633,9 +633,10 @@ export default function WorkflowPage() { {/* Flow visualization */}
- {wf.steps.map((step, i) => { + {(wf.steps || []).map((step, i) => { const StepIcon = STEP_TYPE_ICONS[step.step_type] || Shield; const uids: number[] = step.approver_ids ? JSON.parse(step.approver_ids) : []; + const stepsArr = wf.steps || []; return (
@@ -649,7 +650,7 @@ export default function WorkflowPage() { )}
- {i < wf.steps.length - 1 && } + {i < stepsArr.length - 1 && }
); })} @@ -662,7 +663,7 @@ export default function WorkflowPage() { 流程步骤详情 - {wf.steps.map((step, si) => { + {(wf.steps || []).map((step, si) => { const StepIcon = STEP_TYPE_ICONS[step.step_type] || Shield; const colorClass = STEP_TYPE_COLORS[step.step_type] || STEP_TYPE_COLORS.serial; const uids: number[] = step.approver_ids ? JSON.parse(step.approver_ids) : [];