fix: 修复3个前端报错 — 实例分组编辑500/定时任务404/工作流null

1. instance_group.go: UpdateInstanceGroup 改用结构体输入,过滤系统字段,防止GORM更新主键导致500
2. scheduled_task.go: 新增 ListScheduledTasks + CancelScheduledTask handler
3. router.go: 注册 /tickets/scheduled/list 和 /tickets/:id/cancel-schedule 路由
4. WorkflowPage.tsx: 4处 wf.steps 添加 || [] 空值保护
This commit is contained in:
logan committed 2026-05-25 23:39:16 +08:00
1 parent 88e5f4e7ee
commit 2957075345
4 files changed
+146 -6

No files matched your search

+42 -2
View File
@@ -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
}
@@ -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})
}
+2
View File
@@ -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)