Golang Gin框架中间件实现操作日志统一管理

在大部分后台管理中都会出现操作日志模块的需求,一般情况你会想到创建一个操作日志表 opLog 然后就是在每个 api 的地放调用插入数据即可。

如上所述,这样的设计确实十分灵活可以让每个写api的开发人员自由的操作内容,但是也比较费时间和精力。

解决答方案

利用gin框架的use挂起中间件,将操作日志进行统一抽象的被动调用。

先给出如下的表结构字段设计,

这里我是放在mongodb中的:

// 后台管理员操作日志表
type OperationLog struct {
	Id            primitive.ObjectID `form:"_id" json:"_id" bson:"_id"`            // 操作id
	OperationId   int64              `json:"operationId" bson:"operationId"`       // 操作员Id
	OperationName string             `json:"operation_name" bson:"operation_name"` // 操作员name
	Ip            string             `json:"ip" bson:"ip"`                         // 操作地址Ip
	Operation     string             `json:"operation" bson:"operation"`           // 操作 增删改
	Business      string             `json:"business" bson:"business"`             // 业务名称
	Tables        string             `json:"tables" bson:"tables"`                 // 直接相关表(一个)
	Data          interface{}        `json:"data" bson:"data"`                     // 新数据的保存 只保留主键id
	CreatedAt     int64              `json:"created_at" bson:"created_at"`
}

具体如下:

  • 全部获取都需要 ctx *gin.Context
  • 利用 获取 userId 方法获取 operationId (一般都是从你的token产生出提取userId即可)
  • ip := ctx.ClientIP() 直接调用获取客户端ip地址
  • ctx.Request.Method 获取api实际的方法对应 新增,编辑,删除操作即可
  • 开发任务只需要维护一个表面+业务 和 HandlerFunc 映射数据即可

根据每个api 调用的 HandlerFunc 都是唯一不同的,即可获取 Tables、Business

  • 最后将代理抽象利用 ctx.Set() 和 ctx.Get() 获取 相关数据 id 即可。
// 用与 handle 函数与 表名及其业务描述的映射
type HandleTableAndDesc map[string]string // value 值填入 {最直接关联的唯一表名},{业务描述}

var HandleTableName = HandleTableAndDesc{
	"InsertWithdrawalOrder": "withdrawal,生成提现订单",
	"UpdateStatus":          "withdrawal,提现操作状态",
	"TaskShelf":             "task_base,任务上架",
	"UploadIcon":            "photo,上传任务图片",
	"StaffFreeze":           "staffs,冻结解冻管理员",
	"UserFreeze":            "cend_user,冻结解冻用户",
}

具体代码封装:

var Engine = gin.Default()

func init() {
	swagger.RegSwagger(Engine)
	Engine.Use(operationlog.Operation(contracts.HandleTableName))
	
	Engine.POST("/stat/withdrawal", controllers.InsertWithdrawalOrder)
	Engine.PUT("/stat/withdrawal", controllers.UpdateStatus)          
	Engine.GET("/stat/withdrawal", controllers.GetWithdrawalList)  
}

具体的 operationlog 包 是核心代码如下:

func Operation(handlerTableName map[string]string) gin.HandlerFunc {
	return func(c *gin.Context) {
		if c.Request.Method == "" {
			c.Next() // 注意 next()方法的作用是跳过该调用链去直接后面的中间件以及api路由
		}
		// 1. 获取当前用户ID
		ok, userId := xxx.GetUserInfo(c)
		if !ok {
			userId = ""
		}
		// 2. 获取表名 这里会把前面 "加载的包名.HandlerName"
		handlerName := strings.Split(c.HandlerName(), ".")[1]
		if _, ok := handlerTableName[handlerName]; !ok {
			c.Next() // 如果映射中没有找到表关系,说明没有手动加入
		}

		tableDesc := strings.Split(handlerTableName[handlerName], ",")
		if len(tableDesc) != 2 {
			return // 存在映射,但是不完全则返回
		}

		tableName := tableDesc[0] // 0表名
		desc := tableDesc[1]      // 1业务描述
		// 3. 获取Ip
		ip := c.ClientIP()
		opMethod := &OperationMethod{
			C:      c,
			Table:  tableName,
			Desc:   desc,
			Ip:     ip,
			UserId: userId,
		}
		// 4. 根据method方法名确定操作行为
		switch c.Request.Method {
		case "PUT":
			err := opMethod.putOperation()
			if err != nil {
				c.Next()
			}
		case "POST":
			err := opMethod.postOperation()
			if err != nil {
				c.Next()
			}
		case "DELETE":
			err := opMethod.deleteOperation()
			if err != nil {
				c.Next()
			}
		default:
			return
		}
	}
}

type OperationMethod struct {
	C      *gin.Context
	Table  string `json:"table"`
	Desc   string `json:"desc"`
	Ip     string `json:"ip"`
	UserId string `json:"userId"`
}

type bodyId struct {
	Id interface{} `json:"_id" bson:"_id"`
}

func (o *OperationMethod) putOperation() (err error) {
	// 更新操作要拿到操作id的话注意一定要统计使用的业务Id 对应 mongo 为 _id
	// 获取body中的数据
	var bodyBytes []byte
	if o.C.Request.Body != nil {
		bodyBytes, _ = ioutil.ReadAll(o.C.Request.Body)
	}
	o.C.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) // 返回body中的原值

	bodyId := &bodyId{}
	var putId interface{}
	_ = json.Unmarshal(bodyBytes, bodyId)
	if bodyId.Id == nil {
		o.C.Next()
		putId, _ = o.C.Get("updateId")
	} else {
		putId = bodyId.Id
	}

	// 插入操作日志表中
	userId, _ := strconv.Atoi(o.UserId)
	staff := entities.Staff{}
	staffData, err := staff.GetNameById(uint64(userId))
	if err != nil {
		return err
	}

	operationLog := entities.OperationLog{
		OperationId:   int64(userId),
		Operation:     "PUT",
		OperationName: staffData.Name,
		Ip:            o.Ip,
		Business:      o.Desc,
		Tables:        o.Table,
		Data:          putId,
	}

	err = operationLog.InsertLog()
	if err != nil {
		return err
	}

	return
}

func (o *OperationMethod) postOperation() (err error) {
	// 新建操作 需要记录新插入的数据
	// 获取插入数据的Id
	o.C.Next()                         // 直接跳过调用链 让其开始走api
	insertId, _ := o.C.Get("insertId") // 若不存在 也应该先插入操作记录 只是业务操作Insert没有返回id
	userId, _ := strconv.Atoi(o.UserId)
	staff := entities.Staff{}
	staffData, err := staff.GetNameById(uint64(userId))
	if err != nil {
		return
	}

	operationLog := entities.OperationLog{
		OperationId:   int64(userId),
		Operation:     "POST",
		OperationName: staffData.Name,
		Ip:            o.Ip,
		Business:      o.Desc,
		Tables:        o.Table,
		Data:          insertId,
	}

	err = operationLog.InsertLog()
	if err != nil {
		return
	}

	return
}

func (o *OperationMethod) deleteOperation() (err error) {
	mongo.Collection(o.Table)
	// 删除操作 需要对删除的旧数据进行记录
	var ids []string
	if len(o.C.QueryArray("ids")) != 0 {
		ids = o.C.QueryArray("ids")
		ids = make([]string, len(ids))
	} else if len(o.C.QueryArray("_id")) != 0 {
		ids = o.C.QueryArray("_id")
		ids = make([]string, len(ids))
	}

	idsAll := "" // 对应敏感业务 软删除可以查询到删除的具体数据
	for _, v := range ids {
		if len(ids) == 1 {
			idsAll = idsAll + v
		} else {
			idsAll = idsAll + v + ","
		}
	}

	userId, _ := strconv.Atoi(o.UserId)
	staff := entities.Staff{}
	staffData, err := staff.GetNameById(uint64(userId))
	if err != nil {
		return
	}

	operationLog := entities.OperationLog{
		OperationId:   int64(userId),
		Operation:     "DELETE",
		OperationName: staffData.Name,
		Ip:            o.Ip,
		Business:      o.Desc,
		Tables:        o.Table,
		Data:          idsAll,
	}

	err = operationLog.InsertLog()
	if err != nil {
		return
	}

	return
}

为了获取 更新和新增的相关id,需要在调用具体api时,返回对应的id。然后利用gin.Context 调用 ctx.Set(“insertId”, id) 进行存储,这样方便中间件调用ctx.Get(“insertId”),进行获取数据即可。