diff --git a/beans/beans.go b/beans/beans.go new file mode 100644 index 0000000..5254575 --- /dev/null +++ b/beans/beans.go @@ -0,0 +1,9 @@ +package beans + +type ResponseEmpty struct { +} +type Page struct { + PageNum int `p:"pageNum"` //当前页码 + PageSize int `p:"pageSize"` //每页数 + Total int //总页数 +} diff --git a/log/consts/log_const.go b/log/consts/log_const.go new file mode 100644 index 0000000..ca33ca3 --- /dev/null +++ b/log/consts/log_const.go @@ -0,0 +1,15 @@ +package consts + +// OperationType 操作类型常量 +type OperationType string + +const ( + OperationCreate OperationType = "create" // 创建 + OperationUpdate OperationType = "update" // 更新 + OperationDelete OperationType = "delete" // 删除 +) + +// OperationLogCollection 操作日志集合名称常量 +const ( + OperationLogCollection = "operation_logs" // 操作日志集合名称 +) diff --git a/log/controller/log_controller.go b/log/controller/log_controller.go new file mode 100644 index 0000000..e9cb10c --- /dev/null +++ b/log/controller/log_controller.go @@ -0,0 +1,76 @@ +package controller + +import ( + "context" + "strings" + + "gitee.com/red-future---jilin-g/common/beans" + "gitee.com/red-future---jilin-g/common/log/model/dto" + "gitee.com/red-future---jilin-g/common/log/service" +) + +type operationLog struct{} + +// OperationLog 操作日志控制器 +var OperationLog = new(operationLog) + +// GetByID 根据ID获取操作日志 +// @Summary 获取操作日志详情 +// @Description 根据日志ID获取操作日志的详细信息 +func (c *operationLog) GetByID(ctx context.Context, req *dto.GetLogReq) (res *dto.GetLogResp, err error) { + logInfo, err := service.OperationLog.GetByID(ctx, req.ID) + if err != nil { + return + } + + res = &dto.GetLogResp{ + OperationLogInfo: *logInfo, + } + return +} + +// List 查询操作日志列表(通用方法,支持根据不同条件动态查询) +// @Summary 查询操作日志列表 +// @Description 根据多个条件查询操作日志列表 +func (c *operationLog) List(ctx context.Context, req *dto.ListLogsReq) (res *dto.ListLogsResp, err error) { + // 处理排序字段 + var sortFields []string + if req.SortFields != "" { + sortFields = strings.Split(req.SortFields, ",") + } + + logs, total, err := service.OperationLog.List(ctx, req, sortFields...) + if err != nil { + return + } + + res = &dto.ListLogsResp{ + Logs: logs, + Total: total, + } + return +} + +// RecordCreate 记录创建操作日志 +// @Summary 记录创建操作日志 +// @Description 记录数据创建操作的行为日志 +func (c *operationLog) RecordCreate(ctx context.Context, req *dto.RecordCreateLogReq) (res *beans.ResponseEmpty, err error) { + err = service.OperationLog.RecordCreate(ctx, req.Module, req.Service, req.Resource, req.ResourceID, req.Description, req.AfterData) + return +} + +// RecordUpdate 记录更新操作日志 +// @Summary 记录更新操作日志 +// @Description 记录数据更新操作的行为日志 +func (c *operationLog) RecordUpdate(ctx context.Context, req *dto.RecordUpdateLogReq) (res *beans.ResponseEmpty, err error) { + err = service.OperationLog.RecordUpdate(ctx, req.Module, req.Service, req.Resource, req.ResourceID, req.Description, req.BeforeData, req.AfterData) + return +} + +// RecordDelete 记录删除操作日志 +// @Summary 记录删除操作日志 +// @Description 记录数据删除操作的行为日志 +func (c *operationLog) RecordDelete(ctx context.Context, req *dto.RecordDeleteLogReq) (res *beans.ResponseEmpty, err error) { + err = service.OperationLog.RecordDelete(ctx, req.Module, req.Service, req.Resource, req.ResourceID, req.Description, req.BeforeData) + return +} diff --git a/log/dao/log_dao.go b/log/dao/log_dao.go new file mode 100644 index 0000000..4b75dec --- /dev/null +++ b/log/dao/log_dao.go @@ -0,0 +1,142 @@ +package dao + +import ( + "context" + "time" + + "gitee.com/red-future---jilin-g/common/log/consts" + "gitee.com/red-future---jilin-g/common/log/model/dto" + "gitee.com/red-future---jilin-g/common/log/model/entity" + "gitee.com/red-future---jilin-g/common/mongo" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +type log struct{} + +// Log 日志数据访问对象 +var Log = &log{} + +// Create 创建日志记录 +func (d *log) Create(ctx context.Context, log *entity.OperationLog) error { + _, err := mongo.DB().Insert(ctx, []interface{}{log}, consts.OperationLogCollection) + return err +} + +// CreateBatch 批量创建日志记录 +func (d *log) CreateBatch(ctx context.Context, logs []*entity.OperationLog) error { + if len(logs) == 0 { + return nil + } + + documents := make([]interface{}, len(logs)) + for i, log := range logs { + documents[i] = log + } + + _, err := mongo.DB().Insert(ctx, documents, consts.OperationLogCollection) + return err +} + +// GetByID 根据ID获取日志 +func (d *log) GetByID(ctx context.Context, id string) (*entity.OperationLog, error) { + objectID, err := bson.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + + filter := bson.M{"_id": objectID} + var log entity.OperationLog + err = mongo.DB().FindOne(ctx, filter, &log, consts.OperationLogCollection) + if err != nil { + return nil, err + } + return &log, nil +} + +// List 查询日志列表(通用方法,通过filter动态拼接查询条件) +func (d *log) List(ctx context.Context, filter *dto.ListLogsReq, sortFields ...string) ([]*entity.OperationLog, int64, error) { + bsonFilter := buildFilter(filter) + + total, err := mongo.DB().Count(ctx, bsonFilter, consts.OperationLogCollection) + if err != nil { + return nil, 0, err + } + + var findOptions []options.Lister[options.FindOptions] + if filter.PageNum > 0 && filter.PageSize > 0 { + findOptions = append(findOptions, options.Find().SetSkip(int64((filter.PageNum-1)*filter.PageSize)).SetLimit(int64(filter.PageSize))) + } + + if len(sortFields) > 0 { + sort := bson.D{} + for _, field := range sortFields { + var order int + if len(field) > 0 && field[0] == '-' { + order = -1 + field = field[1:] + } else { + order = 1 + } + sort = append(sort, bson.E{Key: field, Value: order}) + } + findOptions = append(findOptions, options.Find().SetSort(sort)) + } else { + findOptions = append(findOptions, options.Find().SetSort(bson.D{{Key: "createdAt", Value: -1}})) + } + + var logs []*entity.OperationLog + err = mongo.DB().Find(ctx, bsonFilter, &logs, consts.OperationLogCollection, findOptions...) + if err != nil { + return nil, 0, err + } + + return logs, total, nil +} + +// buildFilter 构建MongoDB查询过滤器 +func buildFilter(filter interface{}) bson.M { + bsonFilter := make(bson.M) + + // 从ListLogsReq结构体中提取字段值 + if req, ok := filter.(*dto.ListLogsReq); ok { + if req.Module != "" { + bsonFilter["module"] = req.Module + } + if req.Service != "" { + bsonFilter["service"] = req.Service + } + if req.Operation != "" { + bsonFilter["operation"] = req.Operation + } + if req.Resource != "" { + bsonFilter["resource"] = req.Resource + } + if req.ResourceID != "" { + bsonFilter["resource_id"] = req.ResourceID + } + if req.UserID != "" { + bsonFilter["user_id"] = req.UserID + } + + // 处理时间范围字段 + if req.StartTime != "" || req.EndTime != "" { + timeFilter := bson.M{} + if req.StartTime != "" { + if startTime, err := time.Parse("2006-01-02 15:04:05", req.StartTime); err == nil { + timeFilter["$gte"] = startTime + } + } + if req.EndTime != "" { + if endTime, err := time.Parse("2006-01-02 15:04:05", req.EndTime); err == nil { + timeFilter["$lte"] = endTime + } + } + if len(timeFilter) > 0 { + bsonFilter["createdAt"] = timeFilter + } + } + } + + return bsonFilter +} diff --git a/log/model/dto/log_dto.go b/log/model/dto/log_dto.go new file mode 100644 index 0000000..148e141 --- /dev/null +++ b/log/model/dto/log_dto.go @@ -0,0 +1,96 @@ +package dto + +import ( + "gitee.com/red-future---jilin-g/common/beans" + "github.com/gogf/gf/v2/frame/g" +) + +// ========== 操作日志查询相关DTO ========== + +// GetLogReq 获取操作日志请求 +type GetLogReq struct { + g.Meta `path:"/getLog" method:"get" tags:"操作日志" summary:"获取操作日志详情" dc:"根据日志ID获取操作日志的详细信息"` + ID string `json:"id" v:"required" dc:"日志ID"` +} + +// GetLogResp 获取操作日志响应 +type GetLogResp struct { + OperationLogInfo +} + +// OperationLogInfo 操作日志信息 +type OperationLogInfo struct { + ID string `json:"id" dc:"日志ID"` + Module string `json:"module" dc:"模块名"` + Service string `json:"service" dc:"服务名"` + Operation string `json:"operation" dc:"操作类型"` + Resource string `json:"resource" dc:"资源类型"` + ResourceID string `json:"resource_id" dc:"资源ID"` + UserID interface{} `json:"user_id" dc:"操作人ID"` + UserName string `json:"user_name" dc:"操作人名称"` + IPAddress string `json:"ip_address" dc:"操作IP地址"` + UserAgent string `json:"user_agent" dc:"用户代理"` + Description string `json:"description" dc:"操作描述"` + BeforeData map[string]interface{} `json:"before_data" dc:"操作前的数据"` + AfterData map[string]interface{} `json:"after_data" dc:"操作后的数据"` + ExtraData map[string]interface{} `json:"extra_data" dc:"额外数据"` + CreatedAt string `json:"created_at" dc:"创建时间"` + UpdatedAt string `json:"updated_at" dc:"更新时间"` +} + +// ListLogsReq 查询操作日志列表请求(通用方法,支持根据不同条件动态查询) +type ListLogsReq struct { + g.Meta `path:"/listLogs" method:"get" tags:"操作日志" summary:"查询操作日志列表" dc:"根据多个条件查询操作日志列表"` + beans.Page + Module string `json:"module" dc:"模块名(可选)"` + Service string `json:"service" dc:"服务名(可选)"` + Operation string `json:"operation" dc:"操作类型(可选)"` + Resource string `json:"resource" dc:"资源类型(可选)"` + ResourceID string `json:"resource_id" dc:"资源ID(可选)"` + UserID string `json:"user_id" dc:"用户ID(可选)"` + StartTime string `json:"start_time" dc:"开始时间(可选)"` + EndTime string `json:"end_time" dc:"结束时间(可选)"` + SortFields string `json:"sort_fields" dc:"排序字段,多个用逗号分隔,如:-createdAt,module(可选)"` +} + +// ListLogsResp 查询操作日志列表响应 +type ListLogsResp struct { + Logs []OperationLogInfo `json:"logs" dc:"日志列表"` + Total int64 `json:"total" dc:"总数"` +} + +// ========== 记录操作日志DTO ========== + +// RecordCreateLogReq 记录创建操作日志请求 +type RecordCreateLogReq struct { + g.Meta `path:"/recordCreateLog" method:"post" tags:"操作日志" summary:"记录创建操作日志" dc:"记录数据创建操作的行为日志"` + Module string `json:"module" v:"required" dc:"模块名"` + Service string `json:"service" v:"required" dc:"服务名"` + Resource string `json:"resource" v:"required" dc:"资源类型"` + ResourceID string `json:"resource_id" v:"required" dc:"资源ID"` + Description string `json:"description" dc:"操作描述"` + AfterData map[string]interface{} `json:"after_data" dc:"操作后的数据"` +} + +// RecordUpdateLogReq 记录更新操作日志请求 +type RecordUpdateLogReq struct { + g.Meta `path:"/recordUpdateLog" method:"post" tags:"操作日志" summary:"记录更新操作日志" dc:"记录数据更新操作的行为日志"` + Module string `json:"module" v:"required" dc:"模块名"` + Service string `json:"service" v:"required" dc:"服务名"` + Resource string `json:"resource" v:"required" dc:"资源类型"` + ResourceID string `json:"resource_id" v:"required" dc:"资源ID"` + Description string `json:"description" dc:"操作描述"` + BeforeData map[string]interface{} `json:"before_data" dc:"操作前的数据"` + AfterData map[string]interface{} `json:"after_data" dc:"操作后的数据"` +} + +// RecordDeleteLogReq 记录删除操作日志请求 +type RecordDeleteLogReq struct { + g.Meta `path:"/recordDeleteLog" method:"post" tags:"操作日志" summary:"记录删除操作日志" dc:"记录数据删除操作的行为日志"` + Module string `json:"module" v:"required" dc:"模块名"` + Service string `json:"service" v:"required" dc:"服务名"` + Resource string `json:"resource" v:"required" dc:"资源类型"` + ResourceID string `json:"resource_id" v:"required" dc:"资源ID"` + Description string `json:"description" dc:"操作描述"` + BeforeData map[string]interface{} `json:"before_data" dc:"操作前的数据"` +} diff --git a/log/model/entity/log.go b/log/model/entity/log.go new file mode 100644 index 0000000..e180a77 --- /dev/null +++ b/log/model/entity/log.go @@ -0,0 +1,24 @@ +package entity + +import ( + "gitee.com/red-future---jilin-g/common/do" +) + +// OperationLog 操作日志实体 - 用于记录数据增删改操作行为 +type OperationLog struct { + do.MongoBaseDO `bson:",inline"` + + Module string `bson:"module" json:"module"` // 模块名:如 order, wallet, market 等 + Service string `bson:"service" json:"service"` // 服务名:具体的微服务名称 + Operation string `bson:"operation" json:"operation"` // 操作类型:create, update, delete + Resource string `bson:"resource" json:"resource"` // 资源类型:如 order, wallet, product 等 + ResourceID string `bson:"resource_id" json:"resource_id"` // 资源ID:具体操作的数据ID,如订单号、钱包ID等 + UserID interface{} `bson:"user_id" json:"user_id"` // 操作人ID + UserName string `bson:"user_name" json:"user_name"` // 操作人名称 + IPAddress string `bson:"ip_address" json:"ip_address"` // 操作IP地址 + UserAgent string `bson:"user_agent" json:"user_agent"` // 用户代理 + Description string `bson:"description" json:"description"` // 操作描述 + BeforeData map[string]interface{} `bson:"before_data,omitempty" json:"before_data"` // 操作前的数据(用于update/delete) + AfterData map[string]interface{} `bson:"after_data,omitempty" json:"after_data"` // 操作后的数据(用于create/update) + ExtraData map[string]interface{} `bson:"extra_data,omitempty" json:"extra_data"` // 额外数据 +} diff --git a/log/service/log_service.go b/log/service/log_service.go new file mode 100644 index 0000000..ba99f15 --- /dev/null +++ b/log/service/log_service.go @@ -0,0 +1,121 @@ +package service + +import ( + "context" + + "gitee.com/red-future---jilin-g/common/log/consts" + "gitee.com/red-future---jilin-g/common/log/dao" + "gitee.com/red-future---jilin-g/common/log/model/dto" + logEntity "gitee.com/red-future---jilin-g/common/log/model/entity" + "gitee.com/red-future---jilin-g/common/utils" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gconv" +) + +type operationLog struct{} + +// OperationLog 操作日志服务 +var OperationLog = &operationLog{} + +// RecordCreate 记录创建操作 +func (s *operationLog) RecordCreate(ctx context.Context, module, service, resource, resourceID, description string, afterData map[string]interface{}) error { + return s.record(ctx, module, service, string(consts.OperationCreate), resource, resourceID, description, nil, afterData, nil) +} + +// RecordUpdate 记录更新操作 +func (s *operationLog) RecordUpdate(ctx context.Context, module, service, resource, resourceID, description string, beforeData, afterData map[string]interface{}) error { + return s.record(ctx, module, service, string(consts.OperationUpdate), resource, resourceID, description, beforeData, afterData, nil) +} + +// RecordDelete 记录删除操作 +func (s *operationLog) RecordDelete(ctx context.Context, module, service, resource, resourceID, description string, beforeData map[string]interface{}) error { + return s.record(ctx, module, service, string(consts.OperationDelete), resource, resourceID, description, beforeData, nil, nil) +} + +// BatchRecordCreate 批量记录创建操作 +func (s *operationLog) BatchRecordCreate(ctx context.Context, logs []*logEntity.OperationLog) error { + return dao.Log.CreateBatch(ctx, logs) +} + +// GetByID 根据ID获取操作日志 +func (s *operationLog) GetByID(ctx context.Context, id string) (*dto.OperationLogInfo, error) { + log, err := dao.Log.GetByID(ctx, id) + if err != nil { + return nil, err + } + + var logInfo dto.OperationLogInfo + gconv.Struct(log, &logInfo) + logInfo.ID = log.Id.Hex() + logInfo.CreatedAt = gtime.New(log.CreatedAt).Format("Y-m-d H:i:s") + logInfo.UpdatedAt = gtime.New(log.UpdatedAt).Format("Y-m-d H:i:s") + return &logInfo, nil +} + +// List 查询操作日志列表 +func (s *operationLog) List(ctx context.Context, filter interface{}, sortFields ...string) ([]dto.OperationLogInfo, int64, error) { + logs, total, err := dao.Log.List(ctx, filter, sortFields...) + if err != nil { + return nil, 0, err + } + + var logInfos []dto.OperationLogInfo + err = gconv.Slice(logs, &logInfos) + if err != nil { + return nil, 0, err + } + + // 处理特殊字段 + for i, log := range logs { + logInfos[i].ID = log.Id.Hex() + logInfos[i].CreatedAt = gtime.New(log.CreatedAt).Format("Y-m-d H:i:s") + logInfos[i].UpdatedAt = gtime.New(log.UpdatedAt).Format("Y-m-d H:i:s") + } + + return logInfos, total, nil +} + +// record 记录操作日志的通用方法 +func (s *operationLog) record(ctx context.Context, module, service, operation, resource, resourceID, description string, beforeData, afterData, extraData map[string]interface{}) error { + // 获取用户信息 + user, err := utils.GetUserInfo(ctx) + if err != nil { + return err + } + + // 获取请求信息 + ipAddress, userAgent := getHTTPRequestInfo(ctx) + + var userName string + if user.UserName != nil { + userName = gconv.String(user.UserName) + } + + log := &logEntity.OperationLog{ + Module: module, + Service: service, + Operation: operation, + Resource: resource, + ResourceID: resourceID, + UserID: user.UserName, + UserName: userName, + IPAddress: ipAddress, + UserAgent: userAgent, + Description: description, + BeforeData: beforeData, + AfterData: afterData, + ExtraData: extraData, + } + + return dao.Log.Create(ctx, log) +} + +// getHTTPRequestInfo 从上下文中获取HTTP请求信息 +func getHTTPRequestInfo(ctx context.Context) (ipAddress, userAgent string) { + request := g.RequestFromCtx(ctx) + if request != nil { + return request.GetClientIp(), request.Header.Get("User-Agent") + } + return "", "" +}