package handler import ( "log" "net/http" "runtime/debug" "github.com/gin-gonic/gin" ) // requireTaskOwner 校验请求者(AuthFromHeaderOrQuery 已注入的 uid)是该 task 的提交者。 // 用于公开 by-id 端点(SSE 流 / 报告导出):这些资源始终由本人的客户端访问(用户看/导出 // 自己提交的运行),故按 owner 判权即安全。非本人 → 403,返回 false。 func (h *Handler) requireTaskOwner(c *gin.Context, taskID string) bool { uid := userID(c) if uid == "" || h.db.TaskOwner(c.Request.Context(), taskID) != uid { c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"}) return false } return true } // safeGo 起一个带 panic 兜底的后台 goroutine:panic 只记日志(含 name + stack)不外抛。 // 为什么必须有:Go 里未 recover 的 panic 会崩掉**整个进程**,而 gin.Recovery() 只保护 // 请求 goroutine、不覆盖 handler 派生的后台 goroutine(定时器/推送/探针)。一个后台任务的 // 意外 panic 不该拖垮整个 gateway、连带所有在途 HTTP。 func safeGo(name string, fn func()) { go safeCall(name, fn) } // safeCall 同步执行 fn 并兜底 panic。用于定时器**单轮**内部:单轮 panic 不该终止整个 ticker, // 兜住后下一轮照常继续(若把 recover 只放在 safeGo 外层,单轮 panic 会让整个循环 goroutine 结束)。 func safeCall(name string, fn func()) { defer func() { if r := recover(); r != nil { log.Printf("[panic] 后台任务 %q panic 已兜底: %v\n%s", name, r, debug.Stack()) } }() fn() }