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:
1 parent
88e5f4e7ee
commit
2957075345
4 files changed
+146
-6
No files matched your search
@@ -46,13 +46,53 @@ func UpdateInstanceGroup(c *gin.Context) {
|
|||||||
return
|
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 {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||||
return
|
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()})
|
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||||
return
|
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})
|
||||||
|
}
|
||||||
@@ -27,10 +27,12 @@ func Setup(r *gin.Engine) {
|
|||||||
// Tickets
|
// Tickets
|
||||||
auth.GET("/tickets/statistics", handler.TicketStatistics)
|
auth.GET("/tickets/statistics", handler.TicketStatistics)
|
||||||
auth.GET("/tickets", handler.ListTickets)
|
auth.GET("/tickets", handler.ListTickets)
|
||||||
|
auth.GET("/tickets/scheduled/list", handler.ListScheduledTasks)
|
||||||
auth.GET("/tickets/:id", handler.GetTicket)
|
auth.GET("/tickets/:id", handler.GetTicket)
|
||||||
auth.POST("/tickets", handler.CreateTicket)
|
auth.POST("/tickets", handler.CreateTicket)
|
||||||
auth.POST("/tickets/:id/approve", handler.ApproveTicket)
|
auth.POST("/tickets/:id/approve", handler.ApproveTicket)
|
||||||
auth.POST("/tickets/:id/execute", handler.ExecuteTicket)
|
auth.POST("/tickets/:id/execute", handler.ExecuteTicket)
|
||||||
|
auth.POST("/tickets/:id/cancel-schedule", handler.CancelScheduledTask)
|
||||||
|
|
||||||
// Instances
|
// Instances
|
||||||
auth.GET("/instances", handler.ListInstances)
|
auth.GET("/instances", handler.ListInstances)
|
||||||
|
|||||||
@@ -625,7 +625,7 @@ export default function WorkflowPage() {
|
|||||||
{wf.is_active ? '已启用' : '已禁用'}
|
{wf.is_active ? '已启用' : '已禁用'}
|
||||||
</span>
|
</span>
|
||||||
<span className="px-2 py-0.5 bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] text-xs rounded">
|
<span className="px-2 py-0.5 bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] text-xs rounded">
|
||||||
{wf.steps.length} 个阶段
|
{(wf.steps || []).length} 个阶段
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -633,9 +633,10 @@ export default function WorkflowPage() {
|
|||||||
|
|
||||||
{/* Flow visualization */}
|
{/* Flow visualization */}
|
||||||
<div className="flex items-center gap-1 mt-4 p-4 bg-[var(--dg-bg-tertiary)] rounded-lg overflow-x-auto">
|
<div className="flex items-center gap-1 mt-4 p-4 bg-[var(--dg-bg-tertiary)] rounded-lg overflow-x-auto">
|
||||||
{wf.steps.map((step, i) => {
|
{(wf.steps || []).map((step, i) => {
|
||||||
const StepIcon = STEP_TYPE_ICONS[step.step_type] || Shield;
|
const StepIcon = STEP_TYPE_ICONS[step.step_type] || Shield;
|
||||||
const uids: number[] = step.approver_ids ? JSON.parse(step.approver_ids) : [];
|
const uids: number[] = step.approver_ids ? JSON.parse(step.approver_ids) : [];
|
||||||
|
const stepsArr = wf.steps || [];
|
||||||
return (
|
return (
|
||||||
<div key={step.id || i} className="flex items-center gap-1 flex-shrink-0">
|
<div key={step.id || i} className="flex items-center gap-1 flex-shrink-0">
|
||||||
<div className={`px-3 py-2 rounded-lg border ${STEP_TYPE_COLORS[step.step_type] || STEP_TYPE_COLORS.serial} flex items-center gap-2`}>
|
<div className={`px-3 py-2 rounded-lg border ${STEP_TYPE_COLORS[step.step_type] || STEP_TYPE_COLORS.serial} flex items-center gap-2`}>
|
||||||
@@ -649,7 +650,7 @@ export default function WorkflowPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{i < wf.steps.length - 1 && <ArrowRight className="w-4 h-4 text-[var(--dg-text-muted)] flex-shrink-0" />}
|
{i < stepsArr.length - 1 && <ArrowRight className="w-4 h-4 text-[var(--dg-text-muted)] flex-shrink-0" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -662,7 +663,7 @@ export default function WorkflowPage() {
|
|||||||
<GitBranch className="w-4 h-4 text-[var(--dg-text-muted)]" />
|
<GitBranch className="w-4 h-4 text-[var(--dg-text-muted)]" />
|
||||||
流程步骤详情
|
流程步骤详情
|
||||||
</h4>
|
</h4>
|
||||||
{wf.steps.map((step, si) => {
|
{(wf.steps || []).map((step, si) => {
|
||||||
const StepIcon = STEP_TYPE_ICONS[step.step_type] || Shield;
|
const StepIcon = STEP_TYPE_ICONS[step.step_type] || Shield;
|
||||||
const colorClass = STEP_TYPE_COLORS[step.step_type] || STEP_TYPE_COLORS.serial;
|
const colorClass = STEP_TYPE_COLORS[step.step_type] || STEP_TYPE_COLORS.serial;
|
||||||
const uids: number[] = step.approver_ids ? JSON.parse(step.approver_ids) : [];
|
const uids: number[] = step.approver_ids ? JSON.parse(step.approver_ids) : [];
|
||||||
|
|||||||
Reference in new issue
Block a user