diff --git a/backend/internal/handler/ticket.go b/backend/internal/handler/ticket.go index 2525df0..93efe68 100644 --- a/backend/internal/handler/ticket.go +++ b/backend/internal/handler/ticket.go @@ -16,6 +16,24 @@ import ( "github.com/gin-gonic/gin" ) +// SqlExecItem records the execution result of a single SQL statement +type SqlExecItem struct { + SQL string `json:"sql"` + Status string `json:"status"` // success / fail + AffectedRows int64 `json:"affected_rows"` + DurationMs int64 `json:"duration_ms"` + Error string `json:"error,omitempty"` +} + +// SqlExecResult is the structured execution result stored as JSON +type SqlExecResult struct { + Status string `json:"status"` // success / failed + TotalMs int64 `json:"total_ms"` + Items []SqlExecItem `json:"items"` + StartTime string `json:"start_time,omitempty"` + EndTime string `json:"end_time,omitempty"` +} + // GET /api/v1/tickets/statistics func TicketStatistics(c *gin.Context) { userID, _, role := middleware.GetCurrentUser(c) @@ -547,16 +565,34 @@ func ExecuteTicket(c *gin.Context) { 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 } + execStart := time.Now() result, err := client.Do(ctx, args...).Result() + stmtMs := time.Since(execStart).Milliseconds() + execRes := SqlExecResult{ + Status: "success", + TotalMs: stmtMs, + StartTime: execStart.UTC().Format(time.RFC3339), + EndTime: time.Now().UTC().Format(time.RFC3339), + } if err != nil { - execErr = fmt.Sprintf("%s failed: %v", cmd, err) + execRes.Status = "failed" + execRes.Items = []SqlExecItem{{ + SQL: ticket.SQLContent, Status: "fail", DurationMs: stmtMs, Error: err.Error(), + }} + execErr = err.Error() } else { - execResult = fmt.Sprintf("[OK] %s → %v", ticket.SQLContent, result) + resultStr := fmt.Sprintf("%v", result) + execRes.Items = []SqlExecItem{{ + SQL: ticket.SQLContent, Status: "success", DurationMs: stmtMs, + Error: resultStr, + }} + } + if jsonBytes, jsonErr := json.Marshal(execRes); jsonErr == nil { + execResult = string(jsonBytes) } } } @@ -582,12 +618,31 @@ func ExecuteTicket(c *gin.Context) { execTimeout := getSettingInt("executor_timeout", 300) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(execTimeout)*time.Second) defer cancel() - result, ghErr := ghost.RunGhOst(ctx, *ghostTask, getGhostPath()) - if ghErr != nil { - execErr = fmt.Sprintf("gh-ost 执行失败: %s\n%s", result.Error, ghErr.Error()) - } else { - execResult = fmt.Sprintf("[gh-ost OK] %s\n%s", truncateStr(ticket.SQLContent, 80), result.Output) - } + execStart := time.Now() + result, ghErr := ghost.RunGhOst(ctx, *ghostTask, getGhostPath()) + stmtMs := time.Since(execStart).Milliseconds() + ghRes := SqlExecResult{ + Status: "success", + TotalMs: stmtMs, + StartTime: execStart.UTC().Format(time.RFC3339), + EndTime: time.Now().UTC().Format(time.RFC3339), + } + if ghErr != nil { + ghRes.Status = "failed" + ghRes.Items = []SqlExecItem{{ + SQL: ticket.SQLContent, Status: "fail", DurationMs: stmtMs, + Error: fmt.Sprintf("%s\n%s", result.Error, ghErr.Error()), + }} + execErr = ghErr.Error() + } else { + ghRes.Items = []SqlExecItem{{ + SQL: ticket.SQLContent, Status: "success", DurationMs: stmtMs, + Error: result.Output, + }} + } + if jsonBytes, jsonErr := json.Marshal(ghRes); jsonErr == nil { + execResult = string(jsonBytes) + } // Skip normal execution path goto executionDone } @@ -644,26 +699,35 @@ func ExecuteTicket(c *gin.Context) { return } } - // Split multi-statement by semicolons + // Split multi-statement by semicolons + execStart := time.Now() statements := splitSQL(ticket.SQLContent) - var results []string + execRes := SqlExecResult{Status: "success", StartTime: execStart.UTC().Format(time.RFC3339)} for _, stmt := range statements { stmt = strings.TrimSpace(stmt) if stmt == "" { continue } - _, err := db.ExecContext(ctx, stmt) + stmtStart := time.Now() + sqlResult, err := db.ExecContext(ctx, stmt) + stmtMs := time.Since(stmtStart).Milliseconds() if err != nil { - results = append(results, fmt.Sprintf("[FAIL] %s: %v", truncateStr(stmt, 50), err)) - // stop_on_error + execRes.Items = append(execRes.Items, SqlExecItem{ + SQL: stmt, Status: "fail", DurationMs: stmtMs, Error: err.Error(), + }) + execRes.Status = "failed" execErr = err.Error() break - } else { - results = append(results, fmt.Sprintf("[OK] %s", truncateStr(stmt, 50))) } + affected, _ := sqlResult.RowsAffected() + execRes.Items = append(execRes.Items, SqlExecItem{ + SQL: stmt, Status: "success", AffectedRows: affected, DurationMs: stmtMs, + }) } - if len(results) > 0 { - execResult = strings.Join(results, "\n") + execRes.TotalMs = time.Since(execStart).Milliseconds() + execRes.EndTime = time.Now().UTC().Format(time.RFC3339) + if jsonBytes, err := json.Marshal(execRes); err == nil { + execResult = string(jsonBytes) } } } diff --git a/frontend/src/pages/TicketsPage.tsx b/frontend/src/pages/TicketsPage.tsx index e57cd95..6812a97 100644 --- a/frontend/src/pages/TicketsPage.tsx +++ b/frontend/src/pages/TicketsPage.tsx @@ -84,16 +84,20 @@ interface TicketStatistics { failed: number; } -interface ExecuteResult { - status: string; +interface SqlExecItem { + sql: string; + status: string; // success / fail affected_rows: number; duration_ms: number; - validation_passed: boolean; - warnings: string[]; - errors: string[]; - raw_output: string; - before_snapshot?: string; - after_snapshot?: string; + error: string; +} + +interface ExecuteResult { + status: string; // success / failed + total_ms: number; + items: SqlExecItem[]; + start_time?: string; + end_time?: string; } /* ------------------------------------------------------------------ */ @@ -725,8 +729,6 @@ function TicketDetailDialog({ ticket: initialTicket, onClose, canApprove, canExe { label: '目标库', value: ticket.target_database }, { label: '数据库类型', value: ticket.db_type.toUpperCase() }, { label: '创建时间', value: new Date(ticket.create_time).toLocaleString() }, - { label: '变更原因', value: ticket.reason || '-' }, - { label: '优先级', value: ticket.priority === 'high' ? '高' : ticket.priority === 'low' ? '低' : '中' }, ].map((item, idx) => (
- {execResult.raw_output}
-
- | # | +SQL 语句 | +执行结果 | +影响行数 | +执行时间 | +错误信息 | +
|---|---|---|---|---|---|
| {idx + 1} | +
+ {item.sql}
+ |
+ + + {item.status === 'success' ? '成功' : '失败'} + + | +{item.affected_rows} | +{item.duration_ms}ms | ++ {item.error && ( + + {item.error.length > 80 ? item.error.substring(0, 80) + '...' : item.error} + + )} + | +