package common import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "net" "strings" "time" "github.com/gin-gonic/gin" logmessage "github.com/perfect-panel/server/internal/model/logmessage" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) type ReportLogMessageLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewReportLogMessageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ReportLogMessageLogic { return &ReportLogMessageLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx } } func (l *ReportLogMessageLogic) ReportLogMessage(req *types.ReportLogMessageRequest, c *gin.Context) (resp *types.ReportLogMessageResponse, err error) { ip := clientIP(c) ua := c.GetHeader("User-Agent") locale := c.GetHeader("Accept-Language") // 简单限流:设备ID优先,其次IP limitKey := "logmsg:" + strings.TrimSpace(req.DeviceId) if limitKey == "logmsg:" { limitKey = "logmsg:" + ip } count, _ := l.svcCtx.Redis.Incr(l.ctx, limitKey).Result() if count == 1 { _ = l.svcCtx.Redis.Expire(l.ctx, limitKey, 60*time.Second).Err() } if count > 120 { // 每分钟最多120条 return nil, errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "too many reports") } // 指纹生成 h := sha256.New() h.Write([]byte(strings.Join([]string{req.Message, req.Stack, req.ErrorCode, req.AppVersion, req.Platform}, "|"))) digest := hex.EncodeToString(h.Sum(nil)) var ctxStr string if req.Context != nil { if b, e := json.Marshal(req.Context); e == nil { ctxStr = string(b) } } var occurredAt *time.Time if req.OccurredAt > 0 { t := time.UnixMilli(req.OccurredAt) occurredAt = &t } var userIdPtr *int64 if req.UserId > 0 { userIdPtr = &req.UserId } row := &logmessage.LogMessage{ Platform: req.Platform, AppVersion: req.AppVersion, OsName: req.OsName, OsVersion: req.OsVersion, DeviceId: req.DeviceId, UserId: userIdPtr, SessionId: req.SessionId, Level: req.Level, ErrorCode: req.ErrorCode, Message: safeTruncate(req.Message, 1024*64), Stack: safeTruncate(req.Stack, 1024*1024), Context: ctxStr, ClientIP: ip, UserAgent: safeTruncate(ua, 255), Locale: safeTruncate(locale, 16), Digest: digest, OccurredAt: occurredAt, } if err = l.svcCtx.LogMessageModel.Insert(l.ctx, row); err != nil { // 唯一指纹冲突时尝试查询已有记录返回ID ex, _, findErr := l.svcCtx.LogMessageModel.Filter(l.ctx, &logmessage.FilterParams{ Keyword: req.Message, Page: 1, Size: 1 }) if findErr == nil && len(ex) > 0 { return &types.ReportLogMessageResponse{ Id: ex[0].Id }, nil } l.Errorf("[ReportLogMessage] insert error: %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "insert log_message failed: %v", err) } return &types.ReportLogMessageResponse{ Id: row.Id }, nil } func safeTruncate(s string, n int) string { if len(s) <= n { return s } return s[:n] } func clientIP(c *gin.Context) string { ip := c.ClientIP() if ip != "" { return ip } host, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)) if err == nil && host != "" { return host } return "" }