feat: 执行结果结构化-每条SQL记录status/affected_rows/duration_ms + 去掉工单详情优先级字段

后端:
- 新增 SqlExecItem/SqlExecResult 结构体
- MySQL/Redis/gh-ost 三条执行路径全部改为 JSON 结构化结果
- 每条 SQL 记录完整语句、状态、影响行数、耗时(ms)、错误信息
- 总耗时/开始结束时间一并记录

前端:
- ExecuteResult 接口匹配后端 SqlExecResult
- 表格展示每条 SQL 执行结果(序号/SQL/状态/影响行数/耗时/错误)
- 汇总卡片: 状态/总耗时/SQL数量
- 去掉工单详情中的优先级和变更原因字段
- 保留 plain text fallback 兼容旧数据

E2E验证: INSERT+UPDATE+ALTER 三语句全部成功, 结构化结果正确
This commit is contained in:
logan committed 2026-06-03 02:15:37 +08:00
1 parent 1364c737ef
commit bec2cafec2
2 files changed
+140 -73

No files matched your search

+82 -18
View File
@@ -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)
}
}
}