Skip to content

Commit b9788e9

Browse files
author
piexlMax(奇淼
committed
feat(错误日志): 添加异步处理错误日志功能
1 parent a469ea2 commit b9788e9

File tree

12 files changed

+279
-131
lines changed

12 files changed

+279
-131
lines changed

server/api/v1/system/sys_auto_code.go

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
package system
22

33
import (
4-
"fmt"
54
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
6-
"github.com/goccy/go-json"
7-
"io"
8-
"strings"
95

106
"github.com/flipped-aurora/gin-vue-admin/server/global"
117
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
12-
"github.com/flipped-aurora/gin-vue-admin/server/utils/request"
138
"github.com/gin-gonic/gin"
149
"go.uber.org/zap"
1510
)
@@ -108,48 +103,15 @@ func (autoApi *AutoCodeApi) GetColumn(c *gin.Context) {
108103

109104
func (autoApi *AutoCodeApi) LLMAuto(c *gin.Context) {
110105
var llm common.JSONMap
111-
err := c.ShouldBindJSON(&llm)
112-
if err != nil {
106+
if err := c.ShouldBindJSON(&llm); err != nil {
113107
response.FailWithMessage(err.Error(), c)
114108
return
115109
}
116-
if global.GVA_CONFIG.AutoCode.AiPath == "" {
117-
response.FailWithMessage("请先前往插件市场个人中心获取AiPath并填入config.yaml中", c)
118-
return
119-
}
120-
121-
path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", llm["mode"]))
122-
res, err := request.HttpRequest(
123-
path,
124-
"POST",
125-
nil,
126-
nil,
127-
llm,
128-
)
129-
if err != nil {
130-
global.GVA_LOG.Error("大模型生成失败!", zap.Error(err))
131-
response.FailWithMessage("大模型生成失败"+err.Error(), c)
132-
return
133-
}
134-
var resStruct response.Response
135-
b, err := io.ReadAll(res.Body)
136-
defer res.Body.Close()
110+
data, err := autoCodeService.LLMAuto(c.Request.Context(), llm)
137111
if err != nil {
138112
global.GVA_LOG.Error("大模型生成失败!", zap.Error(err))
139113
response.FailWithMessage("大模型生成失败"+err.Error(), c)
140114
return
141115
}
142-
err = json.Unmarshal(b, &resStruct)
143-
if err != nil {
144-
global.GVA_LOG.Error("大模型生成失败!", zap.Error(err))
145-
response.FailWithMessage("大模型生成失败"+err.Error(), c)
146-
return
147-
}
148-
149-
if resStruct.Code == 7 {
150-
global.GVA_LOG.Error("大模型生成失败!"+resStruct.Msg, zap.Error(err))
151-
response.FailWithMessage("大模型生成失败"+resStruct.Msg, c)
152-
return
153-
}
154-
response.OkWithData(resStruct.Data, c)
116+
response.OkWithData(data, c)
155117
}

server/api/v1/system/sys_error.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,33 @@ func (sysErrorApi *SysErrorApi) GetSysErrorList(c *gin.Context) {
167167
PageSize: pageInfo.PageSize,
168168
}, "获取成功", c)
169169
}
170+
171+
// GetSysErrorSolution 触发错误日志的异步处理
172+
// @Tags SysError
173+
// @Summary 根据ID触发处理:标记为处理中,1分钟后自动改为处理完成
174+
// @Security ApiKeyAuth
175+
// @Accept application/json
176+
// @Produce application/json
177+
// @Param id query string true "错误日志ID"
178+
// @Success 200 {object} response.Response{msg=string} "处理已提交"
179+
// @Router /sysError/getSysErrorSolution [get]
180+
func (sysErrorApi *SysErrorApi) GetSysErrorSolution(c *gin.Context) {
181+
// 创建业务用Context
182+
ctx := c.Request.Context()
183+
184+
// 兼容 id 与 ID 两种参数
185+
ID := c.Query("id")
186+
if ID == "" {
187+
response.FailWithMessage("缺少参数: id", c)
188+
return
189+
}
190+
191+
err := sysErrorService.GetSysErrorSolution(ctx, ID)
192+
if err != nil {
193+
global.GVA_LOG.Error("处理触发失败!", zap.Error(err))
194+
response.FailWithMessage("处理触发失败:"+err.Error(), c)
195+
return
196+
}
197+
198+
response.OkWithMessage("已提交至AI处理", c)
199+
}

server/core/internal/zap_core.go

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
package internal
22

33
import (
4-
"github.com/flipped-aurora/gin-vue-admin/server/global"
5-
"go.uber.org/zap"
6-
"go.uber.org/zap/zapcore"
7-
"os"
8-
"time"
4+
"context"
5+
"fmt"
6+
"github.com/flipped-aurora/gin-vue-admin/server/global"
7+
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
8+
"github.com/flipped-aurora/gin-vue-admin/server/service"
9+
astutil "github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
10+
"github.com/flipped-aurora/gin-vue-admin/server/utils/stacktrace"
11+
"go.uber.org/zap"
12+
"go.uber.org/zap/zapcore"
13+
"os"
14+
"strings"
15+
"time"
916
)
1017

1118
type ZapCore struct {
@@ -54,13 +61,75 @@ func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapco
5461
}
5562

5663
func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
57-
for i := 0; i < len(fields); i++ {
58-
if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" {
59-
syncer := z.WriteSyncer(fields[i].String)
60-
z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level)
61-
}
62-
}
63-
return z.Core.Write(entry, fields)
64+
for i := 0; i < len(fields); i++ {
65+
if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" {
66+
syncer := z.WriteSyncer(fields[i].String)
67+
z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level)
68+
}
69+
}
70+
// 先写入原日志目标
71+
err := z.Core.Write(entry, fields)
72+
73+
// 捕捉 Error 及以上级别日志并入库,且可提取 zap.Error(err) 的错误内容
74+
if entry.Level >= zapcore.ErrorLevel {
75+
// 避免与 GORM zap 写入互相递归:跳过由 gorm logger writer 触发的日志
76+
if strings.Contains(entry.Caller.File, "gorm_logger_writer.go") {
77+
return err
78+
}
79+
// 避免重复记录 panic 恢复日志,panic 由 GinRecovery 单独捕捉入库
80+
if strings.Contains(entry.Message, "[Recovery from panic]") {
81+
return err
82+
}
83+
84+
form := "后端"
85+
level := entry.Level.String()
86+
// 生成基础信息
87+
info := entry.Message
88+
89+
// 提取 zap.Error(err) 内容
90+
var errStr string
91+
for i := 0; i < len(fields); i++ {
92+
f := fields[i]
93+
if f.Type == zapcore.ErrorType || f.Key == "error" || f.Key == "err" {
94+
if f.Interface != nil {
95+
errStr = fmt.Sprintf("%v", f.Interface)
96+
} else if f.String != "" {
97+
errStr = f.String
98+
}
99+
break
100+
}
101+
}
102+
if errStr != "" {
103+
info = fmt.Sprintf("%s | 错误: %s", info, errStr)
104+
}
105+
106+
// 附加来源与堆栈信息
107+
if entry.Caller.File != "" {
108+
info = fmt.Sprintf("%s \n 源文件:%s:%d", info, entry.Caller.File, entry.Caller.Line)
109+
}
110+
stack := entry.Stack
111+
if stack != "" {
112+
info = fmt.Sprintf("%s \n 调用栈:%s", info, stack)
113+
// 解析最终业务调用方,并提取其方法源码
114+
if frame, ok := stacktrace.FindFinalCaller(stack); ok {
115+
fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line)
116+
if exErr == nil {
117+
info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc)
118+
} else {
119+
info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr)
120+
}
121+
}
122+
}
123+
124+
// 使用后台上下文,避免依赖 gin.Context
125+
ctx := context.Background()
126+
_ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(ctx, &system.SysError{
127+
Form: &form,
128+
Info: &info,
129+
Level: level,
130+
})
131+
}
132+
return err
64133
}
65134

66135
func (z *ZapCore) Sync() error {

server/core/zap.go

Lines changed: 9 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
package core
22

33
import (
4-
"context"
5-
"fmt"
6-
"github.com/flipped-aurora/gin-vue-admin/server/core/internal"
7-
"github.com/flipped-aurora/gin-vue-admin/server/global"
8-
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
9-
"github.com/flipped-aurora/gin-vue-admin/server/service"
10-
"github.com/flipped-aurora/gin-vue-admin/server/utils"
11-
astutil "github.com/flipped-aurora/gin-vue-admin/server/utils/ast"
12-
"github.com/flipped-aurora/gin-vue-admin/server/utils/stacktrace"
13-
"go.uber.org/zap"
14-
"go.uber.org/zap/zapcore"
15-
"os"
16-
"strings"
4+
"fmt"
5+
"github.com/flipped-aurora/gin-vue-admin/server/core/internal"
6+
"github.com/flipped-aurora/gin-vue-admin/server/global"
7+
"github.com/flipped-aurora/gin-vue-admin/server/utils"
8+
"go.uber.org/zap"
9+
"go.uber.org/zap/zapcore"
10+
"os"
1711
)
1812

1913
// Zap 获取 zap.Logger
@@ -30,53 +24,8 @@ func Zap() (logger *zap.Logger) {
3024
core := internal.NewZapCore(levels[i])
3125
cores = append(cores, core)
3226
}
33-
// 通过 Hooks 捕捉 Error 及以上级别日志,写入系统错误表
34-
dbHook := zap.Hooks(func(entry zapcore.Entry) error {
35-
// 仅处理 Error 及以上级别
36-
if entry.Level < zapcore.ErrorLevel {
37-
return nil
38-
}
39-
// 避免与 GORM zap 写入互相递归:跳过由 gorm logger writer 触发的日志
40-
if strings.Contains(entry.Caller.File, "gorm_logger_writer.go") {
41-
return nil
42-
}
43-
// 避免重复记录 panic 恢复日志,panic 由 GinRecovery 单独捕捉入库
44-
if strings.Contains(entry.Message, "[Recovery from panic]") {
45-
return nil
46-
}
47-
48-
form := "后端"
49-
level := entry.Level.String()
50-
// 尽可能携带来源与堆栈信息(使用 runtime 采集并过滤 zap 内部栈)
51-
info := entry.Message
52-
if entry.Caller.File != "" {
53-
info = fmt.Sprintf("错误信息:%s", info)
54-
}
55-
stack := entry.Stack
56-
if stack != "" {
57-
info = fmt.Sprintf("%s \n 调用栈:%s", info, stack)
58-
// 解析最终业务调用方,并提取其方法源码
59-
if frame, ok := stacktrace.FindFinalCaller(stack); ok {
60-
fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line)
61-
if exErr == nil {
62-
info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc)
63-
} else {
64-
info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr)
65-
}
66-
}
67-
}
68-
69-
// 使用后台上下文,避免依赖 gin.Context
70-
ctx := context.Background()
71-
_ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(ctx, &system.SysError{
72-
Form: &form,
73-
Info: &info,
74-
Level: level,
75-
})
76-
return nil
77-
})
78-
79-
logger = zap.New(zapcore.NewTee(cores...), dbHook)
27+
// 构建基础 logger(错误级别的入库逻辑已在自定义 ZapCore 中处理)
28+
logger = zap.New(zapcore.NewTee(cores...))
8029
// 启用 Error 及以上级别的堆栈捕捉,确保 entry.Stack 可用
8130
opts := []zap.Option{zap.AddStacktrace(zapcore.ErrorLevel)}
8231
if global.GVA_CONFIG.Zap.ShowLine {

server/model/system/sys_error.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ type SysError struct {
1111
Form *string `json:"form" form:"form" gorm:"comment:错误来源;column:form;type:text;" binding:"required"` //错误来源
1212
Info *string `json:"info" form:"info" gorm:"comment:错误内容;column:info;type:text;"` //错误内容
1313
Level string `json:"level" form:"level" gorm:"comment:日志等级;column:level;"`
14-
Solution *string `json:"solution" form:"solution" gorm:"comment:解决方案;column:solution;"` //解决方案
14+
Solution *string `json:"solution" form:"solution" gorm:"comment:解决方案;column:solution;type:text"` //解决方案
15+
Status string `json:"status" form:"status" gorm:"comment:处理状态;column:status;type:varchar(20);default:未处理;"` //处理状态:未处理/处理中/处理完成
1516
}
1617

1718
// TableName 错误日志 SysError自定义表名 sys_error

server/router/system/sys_error.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,20 @@ type SysErrorRouter struct{}
99

1010
// InitSysErrorRouter 初始化 错误日志 路由信息
1111
func (s *SysErrorRouter) InitSysErrorRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) {
12-
sysErrorRouter := Router.Group("sysError").Use(middleware.OperationRecord())
13-
sysErrorRouterWithoutRecord := Router.Group("sysError")
14-
sysErrorRouterWithoutAuth := PublicRouter.Group("sysError")
15-
{
16-
sysErrorRouter.DELETE("deleteSysError", sysErrorApi.DeleteSysError) // 删除错误日志
17-
sysErrorRouter.DELETE("deleteSysErrorByIds", sysErrorApi.DeleteSysErrorByIds) // 批量删除错误日志
18-
sysErrorRouter.PUT("updateSysError", sysErrorApi.UpdateSysError) // 更新错误日志
19-
}
20-
{
21-
sysErrorRouterWithoutRecord.GET("findSysError", sysErrorApi.FindSysError) // 根据ID获取错误日志
22-
sysErrorRouterWithoutRecord.GET("getSysErrorList", sysErrorApi.GetSysErrorList) // 获取错误日志列表
23-
}
24-
{
25-
sysErrorRouterWithoutAuth.POST("createSysError", sysErrorApi.CreateSysError) // 新建错误日志
26-
}
12+
sysErrorRouter := Router.Group("sysError").Use(middleware.OperationRecord())
13+
sysErrorRouterWithoutRecord := Router.Group("sysError")
14+
sysErrorRouterWithoutAuth := PublicRouter.Group("sysError")
15+
{
16+
sysErrorRouter.DELETE("deleteSysError", sysErrorApi.DeleteSysError) // 删除错误日志
17+
sysErrorRouter.DELETE("deleteSysErrorByIds", sysErrorApi.DeleteSysErrorByIds) // 批量删除错误日志
18+
sysErrorRouter.PUT("updateSysError", sysErrorApi.UpdateSysError) // 更新错误日志
19+
sysErrorRouter.GET("getSysErrorSolution", sysErrorApi.GetSysErrorSolution) // 触发错误日志处理
20+
}
21+
{
22+
sysErrorRouterWithoutRecord.GET("findSysError", sysErrorApi.FindSysError) // 根据ID获取错误日志
23+
sysErrorRouterWithoutRecord.GET("getSysErrorList", sysErrorApi.GetSysErrorList) // 获取错误日志列表
24+
}
25+
{
26+
sysErrorRouterWithoutAuth.POST("createSysError", sysErrorApi.CreateSysError) // 新建错误日志
27+
}
2728
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package system
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"github.com/flipped-aurora/gin-vue-admin/server/global"
8+
"github.com/flipped-aurora/gin-vue-admin/server/model/common"
9+
commonResp "github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
10+
"github.com/flipped-aurora/gin-vue-admin/server/utils/request"
11+
"github.com/goccy/go-json"
12+
"io"
13+
"strings"
14+
)
15+
16+
// LLMAuto 调用大模型服务,返回生成结果数据
17+
// 入参为通用 JSONMap,需包含 mode(例如 ai/butler/eye/painter 等)以及业务 prompt/payload
18+
func (s *AutoCodeService) LLMAuto(ctx context.Context, llm common.JSONMap) (interface{}, error) {
19+
if global.GVA_CONFIG.AutoCode.AiPath == "" {
20+
return nil, errors.New("请先前往插件市场个人中心获取AiPath并填入config.yaml中")
21+
}
22+
23+
// 构建调用路径:{AiPath} 中的 {FUNC} 由 mode 替换
24+
mode := fmt.Sprintf("%v", llm["mode"]) // 统一转字符串,避免 nil 造成路径异常
25+
path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", mode))
26+
27+
res, err := request.HttpRequest(
28+
path,
29+
"POST",
30+
nil,
31+
nil,
32+
llm,
33+
)
34+
if err != nil {
35+
return nil, fmt.Errorf("大模型生成失败: %w", err)
36+
}
37+
defer res.Body.Close()
38+
39+
var resStruct commonResp.Response
40+
b, err := io.ReadAll(res.Body)
41+
if err != nil {
42+
return nil, fmt.Errorf("读取大模型响应失败: %w", err)
43+
}
44+
if err = json.Unmarshal(b, &resStruct); err != nil {
45+
return nil, fmt.Errorf("解析大模型响应失败: %w", err)
46+
}
47+
if resStruct.Code == 7 { // 业务约定:7 表示模型生成失败
48+
return nil, fmt.Errorf("大模型生成失败: %s", resStruct.Msg)
49+
}
50+
return resStruct.Data, nil
51+
}

0 commit comments

Comments
 (0)