178 lines
4.2 KiB
Go
178 lines
4.2 KiB
Go
package middleware
|
||
|
||
import (
|
||
"base-framework/pkg/logx"
|
||
"base-framework/pkg/reponse"
|
||
"base-framework/pkg/router"
|
||
"base-framework/pkg/routinex"
|
||
"base-framework/pkg/utils/strutil"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strings"
|
||
"sync"
|
||
)
|
||
|
||
// 仅初始化一次的项目信息
|
||
var (
|
||
initOnce sync.Once
|
||
projectBase string // 例:base-farmework
|
||
projectAbsPrefix string // 例:/abs/path/to/base-farmework/
|
||
projectAbsPrefixLower string // 小写版,用于不区分大小写前缀比较(Windows 安全)
|
||
)
|
||
|
||
// Error 中间件:捕获 c.Errors() 并返回统一的 ErrorInfo
|
||
func Error() router.HandlerFunc {
|
||
// 注册时预热(只做一次)
|
||
initOnce.Do(initProjectInfo)
|
||
|
||
return func(c *router.Context) {
|
||
c.Next()
|
||
|
||
errs := c.Errors()
|
||
if len(errs) == 0 {
|
||
return
|
||
}
|
||
|
||
lastErr := errs[len(errs)-1]
|
||
stack := lastErr.StackTrace()
|
||
location := extractBusinessLocation(stack)
|
||
trackId := routinex.Get(logx.TrackID)
|
||
|
||
var trackIDStr string
|
||
if trackId != nil {
|
||
trackIDStr = strutil.ToString(trackId) // 用你之前写的工具类
|
||
}
|
||
|
||
var info = reponse.ErrorInfo{
|
||
Method: c.Request.Method,
|
||
Path: c.Request.URL.Path,
|
||
Message: lastErr.Error(),
|
||
//Stack: stack,
|
||
Location: location,
|
||
TrackId: trackIDStr,
|
||
}
|
||
|
||
logx.Logger.Error(
|
||
"系统异常",
|
||
logx.String("trackID", trackIDStr),
|
||
logx.String("method", c.Request.Method),
|
||
logx.String("path", c.Request.URL.Path),
|
||
logx.String("location", location),
|
||
logx.String("stack", stack),
|
||
)
|
||
|
||
reponse.Error(c).ErrorInfo(info).Send()
|
||
c.Abort()
|
||
}
|
||
}
|
||
|
||
// 仅在注册时执行:确定 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 文件行(仅返回 <project>/<subpath>.go:<line>)
|
||
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 ""
|
||
}
|
||
|
||
// 将绝对路径裁剪为 "<project>/<subpath>.go:<line>"
|
||
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
|
||
}
|
||
}
|