package middleware import ( "base-go-v2/internal/logx" "base-go-v2/internal/reponse" "base-go-v2/internal/router" "os" "path/filepath" "runtime" "strings" "sync" ) // 仅初始化一次的项目信息 var ( initOnce sync.Once projectBase string // 例:base-farmework projectAbsPrefix string // 例:/abs/path/to/base-farmework/ projectAbsPrefixLower string // 小写版,用于不区分大小写前缀比较(Windows 安全) ) func ErrorMiddleware() router.HandlerFunc { initOnce.Do(initProjectInfo) return func(c *router.Context) error { err := c.Next() if err == nil { return nil } errs := c.Errors() if len(errs) == 0 { return nil } lastErr := errs[len(errs)-1] stack := lastErr.StackTrace() location := extractBusinessLocation(stack) logx.Logger.Error("请求错误", logx.String("path", string(c.RequestCtx.Path())), logx.String("method", string(c.RequestCtx.Method())), logx.String("location", location), logx.String("error", err.Error()), logx.String("stack", stack), ) return response.Fail(c).Message("服务器内部错误").Send() } } // 仅在注册时执行:确定 go.mod 根目录并缓存前缀 func initProjectInfo() { // 以当前源文件为起点向上找 go.mod _, file, _, _ := runtime.Caller(0) if root, ok := findGoModRoot(file); ok { projectBase = filepath.Base(root) p := filepath.ToSlash(root) if !strings.HasSuffix(p, "/") { p += "/" } projectAbsPrefix = p projectAbsPrefixLower = strings.ToLower(p) } } // extractBusinessLocation:获取 AddError 后的业务 Handler 文件行(仅返回 /.go:) func extractBusinessLocation(stack string) string { lines := strings.Split(stack, "\n") foundAddError := false countGo := 0 for _, raw := range lines { line := strings.TrimSpace(raw) if strings.Contains(line, "AddError") { foundAddError = true continue } if !foundAddError { continue } if strings.Contains(line, ".go:") { countGo++ if countGo == 2 { // 去掉 " +0x..." 偏移 if i := strings.Index(line, " +"); i != -1 { line = line[:i] } return trimToProject(line) } } } return "" } // 将绝对路径裁剪为 "/.go:" func trimToProject(goLine string) string { // 统一斜杠 line := strings.ReplaceAll(goLine, "\\", "/") // 用最后一个冒号分离(避免 Windows 盘符) idx := strings.LastIndex(line, ":") if idx == -1 || idx+1 >= len(line) { return line } filePath, lineNo := line[:idx], line[idx+1:] // 优先:前缀裁剪(O(1)),比 filepath.Rel 更快 if projectAbsPrefix != "" { fp := filepath.ToSlash(filePath) fpl := strings.ToLower(fp) if strings.HasPrefix(fpl, projectAbsPrefixLower) { rel := fp[len(projectAbsPrefix):] return projectBase + "/" + rel + ":" + lineNo } } // 其次:internal/cmd/pkg 作为锚点 segs := strings.Split(filePath, "/") for i, s := range segs { if s == "internal" || s == "cmd" || s == "pkg" { if i > 0 { return strings.Join(segs[i-1:], "/") + ":" + lineNo } } } // 启发式:目录名包含 '-' 或 '_',当作仓库名 for i, s := range segs { if strings.ContainsAny(s, "-_") { return strings.Join(segs[i:], "/") + ":" + lineNo } } // 兜底:末尾 4 级 start := len(segs) - 4 if start < 0 { start = 0 } return strings.Join(segs[start:], "/") + ":" + lineNo } // 从给定文件路径向上查找包含 go.mod 的目录(仅注册时调用一次) func findGoModRoot(filePath string) (string, bool) { dir := filepath.Clean(filepath.Dir(filepath.FromSlash(filePath))) for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir, true } parent := filepath.Dir(dir) if parent == dir { return "", false } dir = parent } }