From f0439f4f80dae8c354f82820bda97b019501e574 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Tue, 2 Dec 2025 20:12:33 -0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=97=A5=E5=BF=97):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E9=94=99=E8=AF=AF=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=94=B6=E9=9B=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 log_message 表用于存储客户端错误日志 - 实现客户端日志上报接口 POST /v1/common/log/message/report - 添加管理端日志查询接口 GET /v1/admin/log/message/error/list 和 GET /v1/admin/log/message/error/detail - 实现日志指纹去重和限流机制 - 完善相关模型、逻辑和文档说明 --- doc/说明文档.md | 39 +++++++ .../database/02120_log_message.down.sql | 1 + .../migrate/database/02120_log_message.up.sql | 27 +++++ .../log/getErrorLogMessageDetailHandler.go | 17 +++ .../log/getErrorLogMessageListHandler.go | 19 +++ .../handler/common/logMessageReportHandler.go | 24 ++++ internal/handler/routes.go | 9 ++ .../log/getErrorLogMessageDetailLogic.go | 49 ++++++++ .../admin/log/getErrorLogMessageListLogic.go | 59 ++++++++++ .../logic/common/logMessageReportLogic.go | 108 ++++++++++++++++++ internal/model/logmessage/default.go | 50 ++++++++ internal/model/logmessage/entity.go | 27 +++++ internal/model/logmessage/model.go | 52 +++++++++ internal/svc/serviceContext.go | 37 +++--- internal/types/types.go | 74 ++++++++++++ 15 files changed, 575 insertions(+), 17 deletions(-) create mode 100644 doc/说明文档.md create mode 100644 initialize/migrate/database/02120_log_message.down.sql create mode 100644 initialize/migrate/database/02120_log_message.up.sql create mode 100644 internal/handler/admin/log/getErrorLogMessageDetailHandler.go create mode 100644 internal/handler/admin/log/getErrorLogMessageListHandler.go create mode 100644 internal/handler/common/logMessageReportHandler.go create mode 100644 internal/logic/admin/log/getErrorLogMessageDetailLogic.go create mode 100644 internal/logic/admin/log/getErrorLogMessageListLogic.go create mode 100644 internal/logic/common/logMessageReportLogic.go create mode 100644 internal/model/logmessage/default.go create mode 100644 internal/model/logmessage/entity.go create mode 100644 internal/model/logmessage/model.go diff --git a/doc/说明文档.md b/doc/说明文档.md new file mode 100644 index 0000000..6235307 --- /dev/null +++ b/doc/说明文档.md @@ -0,0 +1,39 @@ +# 报错日志收集(log_message) + +## 项目规划 +- 目标:新增 `log_message` 表与采集/查询接口,用于 APP / PC / Web 客户端错误日志收集与分析。 +- 范围: + - 创建 MySQL 表与迁移脚本(02105)。 + - 新增客户端上报接口 `POST /v1/common/log/message/report`。 + - 新增管理端查询接口 `GET /v1/admin/log/message/error/list` 与 `GET /v1/admin/log/message/error/detail`。 + +## 实施方案 +- 表结构:见 `initialize/migrate/database/02120_log_message.up.sql`。 +- 模型:`internal/model/logmessage/`(实体、默认 CRUD、筛选)。 +- 路由:在 `internal/handler/routes.go` 注册公共与管理端路由。 +- 逻辑: + - 上报逻辑:`internal/logic/common/logMessageReportLogic.go`(限流、指纹去重、入库)。 + - 管理查询:`internal/logic/admin/log/getErrorLogMessageListLogic.go`、`getErrorLogMessageDetailLogic.go`。 +- 类型:`internal/types/types.go` 新增请求/响应结构。 + +## 进度记录 +- 2025-12-02: + - 完成表与索引创建迁移文件。 + - 完成模型与服务注入。 + - 完成公共上报接口与限流、去重逻辑;编译验证通过。 + - 完成管理端列表与详情接口;编译验证通过。 + - 待办:根据运营需求调整限流阈值与日志保留策略。 + +## 接口规范 +- 上报:`POST /v1/common/log/message/report` + - 请求:platform、appVersion、osName、osVersion、deviceId、level、errorCode、message、stack、context、occurredAt + - 响应:`{ code, msg, data: { id } }` +- 管理端列表:`GET /v1/admin/log/message/error/list` + - 筛选:platform、level、user_id、device_id、error_code、keyword、start、end;分页:page、size + - 响应:`{ total, list }` +- 管理端详情:`GET /v1/admin/log/message/error/detail?id=...` + - 响应:完整字段 + +## 保留策略与安全 +- 限流:按设备/IP 每分钟 120 条(可配置)。 +- 隐私:避免采集敏感数据;服务端对大字段做长度限制与截断。 diff --git a/initialize/migrate/database/02120_log_message.down.sql b/initialize/migrate/database/02120_log_message.down.sql new file mode 100644 index 0000000..fcf3226 --- /dev/null +++ b/initialize/migrate/database/02120_log_message.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `log_message`; diff --git a/initialize/migrate/database/02120_log_message.up.sql b/initialize/migrate/database/02120_log_message.up.sql new file mode 100644 index 0000000..6a33d37 --- /dev/null +++ b/initialize/migrate/database/02120_log_message.up.sql @@ -0,0 +1,27 @@ +CREATE TABLE `log_message` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `platform` VARCHAR(32) NOT NULL, + `app_version` VARCHAR(32) NULL, + `os_name` VARCHAR(32) NULL, + `os_version` VARCHAR(32) NULL, + `device_id` VARCHAR(64) NULL, + `user_id` BIGINT NULL DEFAULT NULL, + `session_id` VARCHAR(64) NULL, + `level` TINYINT UNSIGNED NOT NULL DEFAULT 3, + `error_code` VARCHAR(64) NULL, + `message` TEXT NOT NULL, + `stack` MEDIUMTEXT NULL, + `context` JSON NULL, + `client_ip` VARCHAR(45) NULL, + `user_agent` VARCHAR(255) NULL, + `locale` VARCHAR(16) NULL, + `digest` VARCHAR(64) NULL, + `occurred_at` DATETIME NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_digest` (`digest`), + KEY `idx_platform_time` (`platform`, `created_at`), + KEY `idx_user_time` (`user_id`, `created_at`), + KEY `idx_device_time` (`device_id`, `created_at`), + KEY `idx_error_code` (`error_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/internal/handler/admin/log/getErrorLogMessageDetailHandler.go b/internal/handler/admin/log/getErrorLogMessageDetailHandler.go new file mode 100644 index 0000000..57c8499 --- /dev/null +++ b/internal/handler/admin/log/getErrorLogMessageDetailHandler.go @@ -0,0 +1,17 @@ +package log + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +func GetErrorLogMessageDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + id := c.Query("id") + l := log.NewGetErrorLogMessageDetailLogic(c.Request.Context(), svcCtx) + resp, err := l.GetErrorLogMessageDetail(id) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/getErrorLogMessageListHandler.go b/internal/handler/admin/log/getErrorLogMessageListHandler.go new file mode 100644 index 0000000..f5a8623 --- /dev/null +++ b/internal/handler/admin/log/getErrorLogMessageListHandler.go @@ -0,0 +1,19 @@ +package log + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +func GetErrorLogMessageListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetErrorLogMessageListRequest + _ = c.ShouldBind(&req) + l := log.NewGetErrorLogMessageListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetErrorLogMessageList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/logMessageReportHandler.go b/internal/handler/common/logMessageReportHandler.go new file mode 100644 index 0000000..c514676 --- /dev/null +++ b/internal/handler/common/logMessageReportHandler.go @@ -0,0 +1,24 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +func ReportLogMessageHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ReportLogMessageRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + l := common.NewReportLogMessageLogic(c.Request.Context(), svcCtx) + resp, err := l.ReportLogMessage(&req, c) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index c20c506..410d16d 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -233,6 +233,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Filter traffic log details adminLogGroupRouter.GET("/traffic/details", adminLog.FilterTrafficLogDetailsHandler(serverCtx)) + + // Error message log list + adminLogGroupRouter.GET("/message/error/list", adminLog.GetErrorLogMessageListHandler(serverCtx)) + + // Error message log detail + adminLogGroupRouter.GET("/message/error/detail", adminLog.GetErrorLogMessageDetailHandler(serverCtx)) } adminMarketingGroupRouter := router.Group("/v1/admin/marketing") @@ -652,6 +658,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get Tos Content commonGroupRouter.GET("/site/tos", common.GetTosHandler(serverCtx)) + + // Report client error log + commonGroupRouter.POST("/log/message/report", common.ReportLogMessageHandler(serverCtx)) } publicAnnouncementGroupRouter := router.Group("/v1/public/announcement") diff --git a/internal/logic/admin/log/getErrorLogMessageDetailLogic.go b/internal/logic/admin/log/getErrorLogMessageDetailLogic.go new file mode 100644 index 0000000..85eb7b6 --- /dev/null +++ b/internal/logic/admin/log/getErrorLogMessageDetailLogic.go @@ -0,0 +1,49 @@ +package log + +import ( + "context" + "strconv" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type GetErrorLogMessageDetailLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetErrorLogMessageDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetErrorLogMessageDetailLogic { + return &GetErrorLogMessageDetailLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx } +} + +func (l *GetErrorLogMessageDetailLogic) GetErrorLogMessageDetail(idStr string) (resp *types.GetErrorLogMessageDetailResponse, err error) { + if idStr == "" { return &types.GetErrorLogMessageDetailResponse{}, nil } + id, _ := strconv.ParseInt(idStr, 10, 64) + row, err := l.svcCtx.LogMessageModel.FindOne(l.ctx, id) + if err != nil { return nil, err } + var uid int64 + if row.UserId != nil { uid = *row.UserId } + var occurred int64 + if row.OccurredAt != nil { occurred = row.OccurredAt.UnixMilli() } + return &types.GetErrorLogMessageDetailResponse{ + Id: row.Id, + Platform: row.Platform, + AppVersion: row.AppVersion, + OsName: row.OsName, + OsVersion: row.OsVersion, + DeviceId: row.DeviceId, + UserId: uid, + SessionId: row.SessionId, + Level: row.Level, + ErrorCode: row.ErrorCode, + Message: row.Message, + Stack: row.Stack, + ClientIP: row.ClientIP, + UserAgent: row.UserAgent, + Locale: row.Locale, + OccurredAt: occurred, + CreatedAt: row.CreatedAt.UnixMilli(), + }, nil +} diff --git a/internal/logic/admin/log/getErrorLogMessageListLogic.go b/internal/logic/admin/log/getErrorLogMessageListLogic.go new file mode 100644 index 0000000..3c172ea --- /dev/null +++ b/internal/logic/admin/log/getErrorLogMessageListLogic.go @@ -0,0 +1,59 @@ +package log + +import ( + "context" + "time" + 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" +) + +type GetErrorLogMessageListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetErrorLogMessageListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetErrorLogMessageListLogic { + return &GetErrorLogMessageListLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx } +} + +func (l *GetErrorLogMessageListLogic) GetErrorLogMessageList(req *types.GetErrorLogMessageListRequest) (resp *types.GetErrorLogMessageListResponse, err error) { + var start, end time.Time + if req.Start > 0 { start = time.UnixMilli(req.Start) } + if req.End > 0 { end = time.UnixMilli(req.End) } + rows, total, err := l.svcCtx.LogMessageModel.Filter(l.ctx, &logmessage.FilterParams{ + Page: req.Page, + Size: req.Size, + Platform: req.Platform, + Level: req.Level, + UserID: req.UserId, + DeviceID: req.DeviceId, + ErrorCode: req.ErrorCode, + Keyword: req.Keyword, + Start: start, + End: end, + }) + if err != nil { return nil, err } + list := make([]types.ErrorLogMessage, 0, len(rows)) + for _, r := range rows { + var uid int64 + if r.UserId != nil { uid = *r.UserId } + list = append(list, types.ErrorLogMessage{ + Id: r.Id, + Platform: r.Platform, + AppVersion: r.AppVersion, + OsName: r.OsName, + OsVersion: r.OsVersion, + DeviceId: r.DeviceId, + UserId: uid, + SessionId: r.SessionId, + Level: r.Level, + ErrorCode: r.ErrorCode, + Message: r.Message, + CreatedAt: r.CreatedAt.UnixMilli(), + }) + } + return &types.GetErrorLogMessageListResponse{ Total: total, List: list }, nil +} diff --git a/internal/logic/common/logMessageReportLogic.go b/internal/logic/common/logMessageReportLogic.go new file mode 100644 index 0000000..2427c18 --- /dev/null +++ b/internal/logic/common/logMessageReportLogic.go @@ -0,0 +1,108 @@ +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 "" +} diff --git a/internal/model/logmessage/default.go b/internal/model/logmessage/default.go new file mode 100644 index 0000000..46b876a --- /dev/null +++ b/internal/model/logmessage/default.go @@ -0,0 +1,50 @@ +package logmessage + +import ( + "context" + "gorm.io/gorm" +) + +type ( + Model interface { + logMessageModel + customLogMessageModel + } + logMessageModel interface { + Insert(ctx context.Context, data *LogMessage) error + FindOne(ctx context.Context, id int64) (*LogMessage, error) + Update(ctx context.Context, data *LogMessage) error + Delete(ctx context.Context, id int64) error + } + customModel struct{ + *defaultModel + } + defaultModel struct{ + *gorm.DB + } +) + +func newDefaultModel(db *gorm.DB) *defaultModel { + return &defaultModel{DB: db} +} + +func (m *defaultModel) Insert(ctx context.Context, data *LogMessage) error { + return m.WithContext(ctx).Create(data).Error +} + +func (m *defaultModel) FindOne(ctx context.Context, id int64) (*LogMessage, error) { + var v LogMessage + err := m.WithContext(ctx).Where("id = ?", id).First(&v).Error + if err != nil { + return nil, err + } + return &v, nil +} + +func (m *defaultModel) Update(ctx context.Context, data *LogMessage) error { + return m.WithContext(ctx).Where("`id` = ?", data.Id).Save(data).Error +} + +func (m *defaultModel) Delete(ctx context.Context, id int64) error { + return m.WithContext(ctx).Where("`id` = ?", id).Delete(&LogMessage{}).Error +} diff --git a/internal/model/logmessage/entity.go b/internal/model/logmessage/entity.go new file mode 100644 index 0000000..233e3d9 --- /dev/null +++ b/internal/model/logmessage/entity.go @@ -0,0 +1,27 @@ +package logmessage + +import "time" + +type LogMessage struct { + Id int64 `gorm:"primaryKey;AUTO_INCREMENT"` + Platform string `gorm:"type:varchar(32);not null"` + AppVersion string `gorm:"type:varchar(32);default:null"` + OsName string `gorm:"type:varchar(32);default:null"` + OsVersion string `gorm:"type:varchar(32);default:null"` + DeviceId string `gorm:"type:varchar(64);default:null"` + UserId *int64 `gorm:"type:bigint;default:null"` + SessionId string `gorm:"type:varchar(64);default:null"` + Level uint8 `gorm:"type:tinyint(1);not null;default:3"` + ErrorCode string `gorm:"type:varchar(64);default:null"` + Message string `gorm:"type:text;not null"` + Stack string `gorm:"type:mediumtext;default:null"` + Context string `gorm:"type:json;default:null"` + ClientIP string `gorm:"type:varchar(45);default:null"` + UserAgent string `gorm:"type:varchar(255);default:null"` + Locale string `gorm:"type:varchar(16);default:null"` + Digest string `gorm:"type:varchar(64);uniqueIndex:uniq_digest;default:null"` + OccurredAt *time.Time `gorm:"type:datetime;default:null"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` +} + +func (LogMessage) TableName() string { return "log_message" } diff --git a/internal/model/logmessage/model.go b/internal/model/logmessage/model.go new file mode 100644 index 0000000..217bfaf --- /dev/null +++ b/internal/model/logmessage/model.go @@ -0,0 +1,52 @@ +package logmessage + +import ( + "context" + "time" + "gorm.io/gorm" +) + +func NewModel(db *gorm.DB) Model { + return &customModel{ defaultModel: newDefaultModel(db) } +} + +type FilterParams struct { + Page int + Size int + Platform string + Level uint8 + UserID int64 + DeviceID string + ErrorCode string + Keyword string + Start time.Time + End time.Time +} + +type customLogMessageModel interface { + Filter(ctx context.Context, filter *FilterParams) ([]*LogMessage, int64, error) +} + +func (m *customModel) Filter(ctx context.Context, filter *FilterParams) ([]*LogMessage, int64, error) { + tx := m.WithContext(ctx).Model(&LogMessage{}).Order("id DESC") + if filter == nil { + filter = &FilterParams{ Page: 1, Size: 10 } + } + if filter.Page < 1 { filter.Page = 1 } + if filter.Size < 1 { filter.Size = 10 } + if filter.Platform != "" { tx = tx.Where("`platform` = ?", filter.Platform) } + if filter.Level != 0 { tx = tx.Where("`level` = ?", filter.Level) } + if filter.UserID != 0 { tx = tx.Where("`user_id` = ?", filter.UserID) } + if filter.DeviceID != "" { tx = tx.Where("`device_id` = ?", filter.DeviceID) } + if filter.ErrorCode != "" { tx = tx.Where("`error_code` = ?", filter.ErrorCode) } + if !filter.Start.IsZero() { tx = tx.Where("`created_at` >= ?", filter.Start) } + if !filter.End.IsZero() { tx = tx.Where("`created_at` <= ?", filter.End) } + if filter.Keyword != "" { + like := "%" + filter.Keyword + "%" + tx = tx.Where("`message` LIKE ? OR `stack` LIKE ?", like, like) + } + var total int64 + var rows []*LogMessage + err := tx.Count(&total).Limit(filter.Size).Offset((filter.Page-1)*filter.Size).Find(&rows).Error + return rows, total, err +} diff --git a/internal/svc/serviceContext.go b/internal/svc/serviceContext.go index cb4d003..266375e 100644 --- a/internal/svc/serviceContext.go +++ b/internal/svc/serviceContext.go @@ -19,6 +19,7 @@ import ( "github.com/perfect-panel/server/internal/model/coupon" "github.com/perfect-panel/server/internal/model/document" "github.com/perfect-panel/server/internal/model/log" + logmessage "github.com/perfect-panel/server/internal/model/logmessage" "github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/internal/model/payment" "github.com/perfect-panel/server/internal/model/subscribe" @@ -44,14 +45,15 @@ type ServiceContext struct { ExchangeRate float64 //NodeCache *cache.NodeCacheClient - AuthModel auth.Model - AdsModel ads.Model - LogModel log.Model - NodeModel node.Model - UserModel user.Model - OrderModel order.Model - ClientModel client.Model - TicketModel ticket.Model + AuthModel auth.Model + AdsModel ads.Model + LogModel log.Model + LogMessageModel logmessage.Model + NodeModel node.Model + UserModel user.Model + OrderModel order.Model + ClientModel client.Model + TicketModel ticket.Model //ServerModel server.Model SystemModel system.Model CouponModel coupon.Model @@ -96,15 +98,16 @@ func NewServiceContext(c config.Config) *ServiceContext { Queue: NewAsynqClient(c), ExchangeRate: 1.0, //NodeCache: cache.NewNodeCacheClient(rds), - AuthLimiter: authLimiter, - AdsModel: ads.NewModel(db, rds), - LogModel: log.NewModel(db), - NodeModel: node.NewModel(db, rds), - AuthModel: auth.NewModel(db, rds), - UserModel: user.NewModel(db, rds), - OrderModel: order.NewModel(db, rds), - ClientModel: client.NewSubscribeApplicationModel(db), - TicketModel: ticket.NewModel(db, rds), + AuthLimiter: authLimiter, + AdsModel: ads.NewModel(db, rds), + LogModel: log.NewModel(db), + LogMessageModel: logmessage.NewModel(db), + NodeModel: node.NewModel(db, rds), + AuthModel: auth.NewModel(db, rds), + UserModel: user.NewModel(db, rds), + OrderModel: order.NewModel(db, rds), + ClientModel: client.NewSubscribeApplicationModel(db), + TicketModel: ticket.NewModel(db, rds), //ServerModel: server.NewModel(db, rds), SystemModel: system.NewModel(db, rds), CouponModel: coupon.NewModel(db, rds), diff --git a/internal/types/types.go b/internal/types/types.go index 76cb24d..e8eade2 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1234,6 +1234,80 @@ type MessageLog struct { CreatedAt int64 `json:"created_at"` } +type ReportLogMessageRequest struct { + Platform string `json:"platform" validate:"required"` + AppVersion string `json:"appVersion"` + OsName string `json:"osName"` + OsVersion string `json:"osVersion"` + DeviceId string `json:"deviceId"` + UserId int64 `json:"userId"` + SessionId string `json:"sessionId"` + Level uint8 `json:"level"` + ErrorCode string `json:"errorCode"` + Message string `json:"message" validate:"required"` + Stack string `json:"stack"` + Context map[string]interface{} `json:"context"` + OccurredAt int64 `json:"occurredAt"` +} + +type ReportLogMessageResponse struct { + Id int64 `json:"id"` +} + +type GetErrorLogMessageListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Platform string `form:"platform"` + Level uint8 `form:"level"` + UserId int64 `form:"user_id"` + DeviceId string `form:"device_id"` + ErrorCode string `form:"error_code"` + Keyword string `form:"keyword"` + Start int64 `form:"start"` + End int64 `form:"end"` +} + +type ErrorLogMessage struct { + Id int64 `json:"id"` + Platform string `json:"platform"` + AppVersion string `json:"app_version"` + OsName string `json:"os_name"` + OsVersion string `json:"os_version"` + DeviceId string `json:"device_id"` + UserId int64 `json:"user_id"` + SessionId string `json:"session_id"` + Level uint8 `json:"level"` + ErrorCode string `json:"error_code"` + Message string `json:"message"` + CreatedAt int64 `json:"created_at"` +} + +type GetErrorLogMessageListResponse struct { + Total int64 `json:"total"` + List []ErrorLogMessage `json:"list"` +} + +type GetErrorLogMessageDetailResponse struct { + Id int64 `json:"id"` + Platform string `json:"platform"` + AppVersion string `json:"app_version"` + OsName string `json:"os_name"` + OsVersion string `json:"os_version"` + DeviceId string `json:"device_id"` + UserId int64 `json:"user_id"` + SessionId string `json:"session_id"` + Level uint8 `json:"level"` + ErrorCode string `json:"error_code"` + Message string `json:"message"` + Stack string `json:"stack"` + Context map[string]interface{} `json:"context"` + ClientIP string `json:"client_ip"` + UserAgent string `json:"user_agent"` + Locale string `json:"locale"` + OccurredAt int64 `json:"occurred_at"` + CreatedAt int64 `json:"created_at"` +} + type MigrateServerNodeResponse struct { Succee uint64 `json:"succee"` Fail uint64 `json:"fail"`