[Go] Build a GoWeb backend management system from scratch based on GoFiber (3) Log management (login log, operation log), user login module

Part 1:[Go] Build a GoWeb backend management system from scratch based on GoFiber (1) Construction project

Part 2:[Go] Build a GoWeb backend management system from scratch based on GoFiber (2) Log output middleware, verification token middleware, configuration routing, basics Tool function.

In the first two articles, we set up the project and configured the middleware and routing. From this article on, we can officially write the business code.

Insert image description here

Log management

First, let's implement the log management module. Logs such as login, logout, and database operations (add, delete, modify) will be recorded and saved in the sys_log table.

I have already written out all the database tables in the first article, so I won’t repeat them here.

Let’s take a look at the recorded data first:

Insert image description here

The log management module actually has only one interface: list query.

router.go

// 日志管理路由
func logRouter(app *fiber.App) {
    
    
	log := app.Group("/sys/log")
	{
    
    
		log.Get("/list", api.LogController{
    
    }.GetPage) // 日志列表
	}
}

conroller layer: sys_log.go

package sys

import (
	"github.com/gofiber/fiber/v2"
	"go-web2/app/common/config"
	"go-web2/app/model/sys"
	"time"
)

type LogController struct{
    
    }

// 日志列表分页
func (LogController) GetPage(c *fiber.Ctx) error {
    
    
	syslog := sys.SysLog{
    
    }
	syslog.IP = c.Query("code")
	name := c.Query("name")
	createTime, _ := time.Parse("2006-01-02", c.Query("startDate"))
	syslog.CreatorId = &name
	syslog.CreateTime = createTime
	pageSize := c.QueryInt("pageSize", 10)
	pageNum := c.QueryInt("pageNum", 1)
	return c.Status(200).JSON(config.Success(syslog.GetPage(pageSize, pageNum)))
}

model layer: sys_log.go

package sys

import (
	"fmt"
	"github.com/google/uuid"
	"go-web2/app/common/config"
	"strings"
	"time"
)

// 操作日志管理
type SysLog struct {
    
    
	config.BaseModel
	IP     string `gorm:"ip" json:"ip"`         // 用户请求IP
	Title  string `gorm:"title" json:"title"`   // 用户请求的标题
	Type   string `gorm:"type" json:"title"`    // 操作类型(其他 登录 退出 新增 修改 删除 上传 导入 设置状态 设置密码)
	Method string `gorm:"method" json:"method"` // 用户请求的方法
	Url    string `gorm:"url" json:"url"`       // 请求url
	Info   string `gorm:"info" json:"info"`     // 详细信息
	State  string `gorm:"state" json:"state"`   // 状态(操作成功 操作失败)
}

// 获取表名
func (SysLog) TableName() string {
    
    
	return "sys_log"
}

// 列表
func (e *SysLog) GetPage(pageSize int, pageNum int) config.PageInfo {
    
    
	var list []SysLog // 查询结果
	var total int64   // 总数
	query := config.DB.Table(e.TableName())
	var creatorId string
	if e.CreatorId != nil {
    
    
		creatorId = *e.CreatorId
	}
	if creatorId != "" {
    
    
		query.Where("creator_id like ?", fmt.Sprintf("%%%s%%", creatorId))
	}
	if e.IP != "" {
    
    
		query.Where("ip like ?", fmt.Sprintf("%%%s%%", e.IP))
	}
	if !e.CreateTime.IsZero() {
    
    
		query.Where("DATE_FORMAT(create_time,'%Y-%m-%d') = ?", e.CreateTime.Format("2006-01-02"))
	}
	offset := (pageNum - 1) * pageSize                                                 // 计算跳过的记录数
	query.Debug().Order("create_time desc").Offset(offset).Limit(pageSize).Find(&list) // 分页查询,根据offset和limit来查询
	query.Count(&total)
	return config.PageInfo{
    
    list, total}
}

// 新增
func (e *SysLog) Insert() (err error) {
    
    
	e.Id = strings.ReplaceAll(uuid.NewString(), "-", "")
	e.CreateTime = time.Now()
	config.DB.Create(e)
	return
}

Record operation log middleware

There is nothing wrong with list query. The main difficulty with this module is how to obtain the required data and then process it and add it to the database.

In Java, we can use annotations + aop to set and obtain corresponding information, such as setting title and type through annotations, and obtaining the requested interface, parameters passed from the front end to the back end through the aop aspect, and data returned from the back end to the front end. etc.

In go, if we want to achieve similar functions, we need to use middleware, but middleware can only implement functions similar to aop in Java, which means that middleware can only obtain the requested information and returned information. For example, when setting title and type, you can only define the standard yourself, and then judge and set the title and type according to the requested interface.

title: is the interface name, type: is the operation type.

Ideas

My idea is: each interface is divided by module. When registering routes, we also put all the interfaces of a module into a routing group. Then the interface naming also maintains a unified style. In this way, all interfaces of our module have a common prefix.

For example, in the user management module, its routing is as follows:

// 用户管理路由
func userRouter(app *fiber.App) {
    
    
	controller := api.UserController{
    
    }
	user := app.Group("/sys/user")
	{
    
    
		user.Get("/getLoginUser", controller.GetLoginUser)      // 获取当前登录的用户
		user.Get("/list", controller.GetPage)                   // 用户列表
		user.Get("/getById/:id", controller.GetById)            // 根据id获取用户
		user.Post("/insert", controller.Insert)                 // 新增用户
		user.Post("/update", controller.Update)                 // 修改用户
		user.Delete("/delete", controller.Delete)               // 删除用户
		user.Post("/updatePassword", controller.UpdatePassword) // 修改密码
		user.Post("/resetPassword", controller.ResetPassword)   // 重置密码
		user.Post("/upload", controller.Upload)                 // 上传头像
	}
}

The interfaces here all start with /sys/user; the same applies to other modules, so we define a standard that the interface begins with which module it belongs to. The interfaces starting with /sys/user are for the user management module, and the interfaces starting with /sys/dept are for the department management module.

Then the interface naming should maintain a unified style, and the naming style of these interfaces for addition, deletion, modification, and query should be unified (mainly addition, deletion, and modification)

  • New interfaces are uniformly named or begin with insert.
  • Modify interfaces to be named or start with update
  • Delete interfaces must be named or begin with delete
  • The upload interface is uniformly named or begins with upload.
  • Import interfaces are uniformly named or begin with imports

This will make it easier for us to judge the operation type. For example, for the /sys/user/insert interface, we can judge type = 新增,title = 用户新增.

code

middleware.go

// 路由接口前缀(名称),用于在日志中间件中,获取请求接口的title
var RouteNames = map[string]string{
    
    
	"/sys/logout":          "用户退出",
	"/sys/safe":            "安全设置",
	"/sys/user":            "用户",
	"/sys/dept":            "部门",
	"/sys/role":            "角色",
	"/sys/menu":            "菜单",
	"/sys/dict":            "字典",
	"/sys/dict/deleteType": "字典类型",
}

// 保存日志到数据库:操作日志
func SysLogInit(c *fiber.Ctx) error {
    
    
	path := c.Path() // 获取当前请求的路径
	// 跳过get请求
	if c.Method() == fiber.MethodGet || strings.Contains(path, "/sys/login") {
    
    
		return c.Next()
	}
	var entity model.SysLog
	re := regexp.MustCompile(`^/(.*?)(?:\?.*)?$`) // 根据正则解析接口
	match := re.FindStringSubmatch(path)
	if len(match) > 1 {
    
    
		api := match[1]
		if !strings.HasPrefix(api, "/") {
    
    
			api = "/" + api
		}
		method := ""
		title := RouteNames[api] // 直接根据接口名获取接口名称
		if title == "" {
    
             // 如果获取不到则只获取接口前缀,根据前缀拿到当前接口是属于哪个模块的
			split := strings.Split(api, "/") // api根据 / 分割
			str := ""
			for i, s := range split {
    
    
				if s == "" {
    
    
					continue
				}
				if i > 0 {
    
    
					str += "/"
				}
				str += s
				title = RouteNames[str] // 获取到接口前缀
				if title != "" {
    
    
					method = split[i+1] // 如果拿到了接口前缀,那么当前索引+1就是具体的接口名
					break
				}
			}
		}
		entity.State = "操作成功"
		if strings.Contains(api, "/sys/logout") {
    
    
			entity.Type = "退出"
			entity.Info = "退出成功"
			entity.Title = "用户退出"
		} else if strings.Contains(path, "insert") {
    
    
			entity.Type = "新增"
		} else if strings.Contains(path, "update") && !strings.Contains(path, "updateState") && !strings.Contains(path, "updatePassword") {
    
    
			entity.Type = "修改"
		} else if strings.Contains(path, "delete") {
    
    
			entity.Type = "删除"
		} else if strings.Contains(path, "updateState") {
    
    
			entity.Type = "设置状态"
			entity.Title = "设置" + title + "状态"
		} else if strings.Contains(path, "updatePassword") {
    
    
			entity.Type = "修改密码"
			entity.Title = "修改密码"
		} else if strings.Contains(path, "resetPassword") {
    
    
			entity.Type = "重置密码"
			entity.Title = "重置密码"
		} else if strings.Contains(path, "upload") {
    
    
			entity.Type = "上传"
		} else if strings.Contains(path, "imports") {
    
    
			entity.Type = "导入"
		} else {
    
    
			entity.Type = "其他"
		}
		if entity.Title == "" {
    
    
			// 新增用户、修改用户、删除用户、设置用户状态、上传用户、导入用户、字典类型删除......
			entity.Title = entity.Type + title
		}
		entity.Info = entity.Title + "成功"
		// 调用下一个中间件或路由处理程序,用来获取响应给前端的数据
		c.Next()
		code := c.Response().StatusCode()
		if code != 200 {
    
    
			entity.State = "操作失败"
			entity.Info = "未知异常"
		} else {
    
    
			var result config.Result
			json.Unmarshal(c.Response().Body(), &result)
			if result.Code != 0 {
    
    
				entity.State = "操作失败"
				entity.Info = entity.Title + "失败:" + result.Message
			}
		}
		if method == "" {
    
    
			methods := strings.Split(api, "/")      // 当前接口根据 / 分割
			entity.Method = methods[len(methods)-1] // 获取当前请求的方法
		} else {
    
    
			entity.Method = method
		}
		entity.IP = c.IP() // 获取用户IP
		entity.Url = api   // 获取当前请求的路径
		token := c.Get(config.TokenHeader)
		if token != "" {
    
    
			user := model.GetLoginUser(token)
			entity.CreatorId = &user.UserName
			entity.Info = user.UserName + " " + entity.Info
		}
		entity.Insert()
	}
	return c.Next()
}

// 省略其他代码.....

In this way we can obtain the operation log. Then for logging in, we need to set up the login interface better.

User login and logout

Next we implement the login module.

// 登录路由
func loginRouter(app *fiber.App) {
    
    
	controller := api.LoginController{
    
    }
	login := app.Group("/sys")
	{
    
    
		login.Get("/getKey", controller.GetKey)    // 获取RSA公钥
		login.Get("/getCode", controller.GetCode)  // 获取验证码
		login.Post("/login", controller.Login)     // 用户登录
		login.Delete("/logout", controller.Logout) // 用户退出
	}
}

The login module has the above interfaces. In addition to logging in and out, it also obtains the login verification code.There is also a login interface when the user enters the user name and password when logging in. , the front end needs to encrypt and transmit the username and password based on the RSA public key, and when the backend receives the parameters, it also needs to decrypt based on the RSA private key to obtain the real username and password.

Verify the password when logging in. If the number of errors exceeds the limit, the account will be locked for 15 or 30 minutes, and no login is allowed during this period.

controller层:sys_login.go

Here, because I don’t have a front-end and use apipost for testing, I commented out the RSA encryption and decryption section.

package sys

import (
	"fmt"
	"github.com/gofiber/fiber/v2"
	"github.com/pkg/errors"
	"go-web2/app/common/config"
	"go-web2/app/common/util"
	"go-web2/app/model/sys"
	"time"
)

type LoginController struct{
    
    }

// 获取公钥
func (LoginController) GetKey(c *fiber.Ctx) error {
    
    
	return c.Status(200).JSON(config.Success(util.GetPublicKey()))
}

// 获取验证码
func (LoginController) GetCode(c *fiber.Ctx) error {
    
    
	id, base64 := util.GenerateCaptcha(4, 100, 42)
	code := make(map[string]string)
	code["codeId"] = id
	code["code"] = base64
	return c.Status(200).JSON(config.Success(code))
}

// 登录
func (LoginController) Login(c *fiber.Ctx) error {
    
    
	//code := c.FormValue("code")
	//codeId := c.FormValue("codeId")
	userName := c.FormValue("userName")
	password := c.FormValue("password")
	// 解密
	//userName = util.RSADecrypt(userName)
	//password = util.RSADecrypt(password)
	// 校验验证码是否正确
	//b := util.CaptVerify(codeId, code)
	//if !b {
    
    
	//	return c.Status(200).JSON(config.Error("验证码错误或已过期"))
	//}
	var syslog = sys.SysLog{
    
    IP: c.IP(), Title: "用户登录", Type: "登录", Method: "login", Url: "/sys/login", State: "登录成功"}
	syslog.CreatorId = &userName
	// 校验用户名和密码
	safe := sys.SysSafe{
    
    }
	safe.GetById()
	user, result := passwordErrorNum(userName, password, safe)
	if result.Code != 0 {
    
    
		syslog.State = "登录失败"
		syslog.Info = result.Message
		syslog.Insert()
		return c.Status(200).JSON(result)
	}
	i := safe.IdleTimeSetting //如果系统闲置时间为0,设置token和session永不过期
	// 登录
	token := ""
	if i == 0 {
    
    
		token = user.Login("", -1) // 永不过期
	} else {
    
    
		token = user.Login("", config.TokenExpire) // 默认保持登录为30分钟
	}
	syslog.Info = userName + "登录成功"
	syslog.Insert()
	return c.Status(200).JSON(config.Success(token))
}

// 退出
func (LoginController) Logout(c *fiber.Ctx) error {
    
    
	token := c.Get(config.TokenHeader) // 获取请求头中的 Token
	sys.Logout(token)                  // 退出登录
	return c.Status(200).JSON(config.Success(nil))
}

/**
 * 判断账号是否锁定
 */
func lockedUser(currentTime, errorCount int64, userName string) (error, bool) {
    
    
	flag := false
	exists, _ := config.RedisConn.Exists(config.ERROR_COUNT + userName).Result()
	// 如果没有错误次数,直接返回
	if exists == 0 {
    
    
		return nil, flag
	}
	loginTime, _ := config.RedisConn.HGet(config.ERROR_COUNT+userName, "loginTime").Int64()
	i, _ := config.RedisConn.HGet(config.ERROR_COUNT+userName, "errorNum").Int64()
	if i >= errorCount && currentTime < loginTime {
    
    
		diff := loginTime - currentTime // 计算时间差
		minutes := int(diff / 60)       // 将差值转换为分钟
		msg := fmt.Sprintf("账号锁定中,还没到允许登录的时间,请%d分钟后再尝试", minutes)
		return errors.New(msg), flag
	} else {
    
    
		flag = true
	}
	return nil, flag
}

// 校验用户名和密码
func passwordErrorNum(userName, password string, safe sys.SysSafe) (*sys.SysUser, config.Result) {
    
    
	user := sys.SysUser{
    
    }
	user.UserName = userName
	//查询用户
	err := user.GetUser()
	if err != nil || user.Id == "" {
    
    
		return nil, config.ErrorCode(1001, "用户不存在或密码错误")
	}
	//根据前端输入的密码(明文),和加密的密码、盐值进行比较,判断输入的密码是否正确
	authenticate := util.AuthenticatePassword(password, user.Password)
	if authenticate {
    
    
		//密码正确错误次数清零
		config.RedisConn.Del(config.ERROR_COUNT + userName)
	} else {
    
    
		// 获取当前时间的时间戳(单位:秒)
		currentTime := time.Now().Unix()
		flag := false
		//错误3次,锁定15分钟后才可登陆 允许时间加上定义的登陆时间(毫秒)
		str := "15"
		var errorCount int64 = 3
		timeStamp := currentTime + 900
		//密码登录限制(0:连续错3次,锁定账号15分钟。1:连续错5次,锁定账号30分钟)
		if safe.PwdLoginLimit == 1 {
    
    
			errorCount = 5
			str = "30"
			timeStamp = currentTime + 1800
		}
		//判断账号是否锁定
		err, flag = lockedUser(currentTime, errorCount, userName)
		if err != nil {
    
    
			return nil, config.ErrorCode(1004, err.Error())
		}
		exists, _ := config.RedisConn.Exists(config.ERROR_COUNT + userName).Result()
		if exists == 0 {
    
     // 键不存在,第一次登录
			loginMap := map[string]any{
    
    
				"errorNum":  1,
				"loginTime": timeStamp,
			}
			config.RedisConn.HMSet(config.ERROR_COUNT+userName, loginMap)
		} else {
    
    
			i, _ := config.RedisConn.HGet(config.ERROR_COUNT+userName, "errorNum").Int64()
			if flag && i == errorCount {
    
    
				config.RedisConn.HSet(config.ERROR_COUNT+userName, "errorNum", 1)
			} else {
    
    
				config.RedisConn.HIncrBy(config.ERROR_COUNT+userName, "errorNum", 1)
			}
			config.RedisConn.HSet(config.ERROR_COUNT+userName, "loginTime", timeStamp)
		}
		i, _ := config.RedisConn.HGet(config.ERROR_COUNT+userName, "errorNum").Int64()
		if i == errorCount {
    
    
			return nil, config.ErrorCode(1004, fmt.Sprintf("您的密码已错误%d次,现已被锁定,请%s分钟后再尝试", errorCount, str))
		}
		return nil, config.ErrorCode(1000, fmt.Sprintf("密码错误,总登录次数%d次,剩余次数: %d", errorCount, (errorCount-i)))
	}
	return &user, config.Success(nil)
}

model layer: sys_login.go

In fact, the login and token-related methods of this file were originally supposed to be placed in the sys_user.go file. Since sys_user.go has a lot of code, I thought about it and finally decided to create a separate sys_login.go file. Come.

package sys

import (
	"encoding/json"
	"fmt"
	"go-web2/app/common/config"
	"go-web2/app/common/util"
	"strconv"
	"strings"
	"time"
)

// ======================================= 登录相关 =======================================

// 用户登录:user 用户信息 loginType 登录类型 expire 有效期
func (user *SysUser) Login(loginType string, expire time.Duration) string {
    
    
	str := util.MD5(user.UserName) // 用户名md5加密
	// 删除所有以当前用户名开头的key
	keys, _, _ := config.RedisConn.Scan(uint64(0), config.CachePrefix+str+"*", 1000).Result()
	for i := range keys {
    
    
		config.RedisConn.Del(keys[i])
	}
	// 设置登录类型前缀
	if len(loginType) > 0 {
    
    
		str = loginType + "_" + str
	}
	token := str + util.GenerateRandomToken(32) // 生成token
	user.Token = token
	userJson, _ := json.Marshal(user)
	loginMap := map[string]any{
    
    
		"token":      token,
		"createTime": time.Now().Unix(),
		"user":       string(userJson),
	}
	// 将用户信息map设置到redis中
	config.RedisConn.HMSet(config.CachePrefix+token, loginMap)
	// 设置有效期
	if expire > 0 {
    
    
		config.RedisConn.Expire(config.CachePrefix+token, expire)
	}
	// 判断当前用户部门是否存在数据权限设置
	exists, _ := config.RedisConn.Exists(config.DATA_SCOPE + user.DeptId).Result()
	if exists == 0 {
    
    
		SetDataScope(user.DeptId) // 如果没有,则需要设置
	}
	return token
}

// 用户退出
func Logout(token string) {
    
    
	config.RedisConn.Del(config.CachePrefix + token)
}

// 获取当前用户的剩余有效时长,返回秒数,返回 -2 时,key已过期
func GetTimeOut(token string) int {
    
    
	// 使用 TTL 命令获取 key 的剩余有效时长,如果 key 不存在或已过期,TTL 将返回 -2
	ttl, err := config.RedisConn.TTL(config.CachePrefix + token).Result()
	if err != nil {
    
    
		return -2
	}
	return int(ttl.Seconds())
}

// 获取当前用户
func GetLoginUser(token string) *SysUser {
    
    
	val, _ := config.RedisConn.HGet(config.CachePrefix+token, "user").Result()
	user := SysUser{
    
    }
	json.Unmarshal([]byte(val), &user)
	// 判断当前用户部门是否存在数据权限设置
	exists, _ := config.RedisConn.Exists(config.DATA_SCOPE + user.DeptId).Result()
	if exists == 0 {
    
    
		SetDataScope(user.DeptId) // 如果没有,则需要设置
	}
	dataScope := config.RedisConn.HGetAll(config.DATA_SCOPE + user.DeptId).Val()
	user.AncestorId = dataScope["ancestorId"]
	user.AncestorName = dataScope["ancestorName"]
	user.ChildId = dataScope["childId"]
	user.ChildName = dataScope["childName"]
	return &user
}

// 获取当前用户id
func GetLoginId(token string) *string {
    
    
	user := GetLoginUser(token)
	return &user.Id
}

// 获取当前用户token的创建时间
func GetCreateTime(token string) int64 {
    
    
	val, _ := config.RedisConn.HGet(config.CachePrefix+token, "createTime").Result()
	r, _ := strconv.ParseInt(val, 10, 64)
	return r
}

// 刷新过期时间
func UpdateTimeOut(token string, expire time.Duration) {
    
    
	if expire.Seconds() < 0 {
    
    
		// -1 永不过期,Persist 将删除key的过期时间,使其永不过期
		config.RedisConn.Persist(config.CachePrefix + token)
	} else {
    
    
		config.RedisConn.Expire(config.CachePrefix+token, expire)
	}
}

// 更新用户信息
func (user *SysUserView) UpdateUser(token string) {
    
    
	config.RedisConn.HSet(config.CachePrefix+token, "user", user)
}

// ======================================= 数据权限相关 =======================================

// 设置当前部门的数据范围
func SetDataScope(deptId string) {
    
    
	if deptId != "" {
    
    
		dept := SysDept{
    
    }
		dept.Id = deptId
		// 这里的数据权限条件存了部门id和名称,如果没有特殊要求的话,只用部门id也可以的。
		// 但是因为我的项目的业务原因,需要用到部门名称来过滤数据(因为有的表的数据判断是哪个部门的数据,用的不是部门id而是部门名称)
		childId, childName := GetDeptChild(deptId)     // 当前部门及子部门id和名称
		ancestorId, ancestorName := dept.GetAncestor() // 当前部门祖级id和名称
		dataScope := map[string]any{
    
    
			"ancestorId":   ancestorId,
			"ancestorName": ancestorName,
			"childId":      childId,
			"childName":    childName,
		}
		// 将数据范围信息map设置到redis中,并设置有效期为2小时
		config.RedisConn.HMSet(config.DATA_SCOPE+dept.Id, dataScope)
		config.RedisConn.Expire(config.DATA_SCOPE+dept.Id, time.Second*7200)
	}
}

// 获取数据范围条件
func GetDataScope(token string, ignoreAdmin, isId bool) string {
    
    
	if token == "" {
    
    
		return ""
	}
	loginUser := GetLoginUser(token)
	// ignoreAdmin=true 表示不管是不是管理员,都要过滤数据; ignoreAdmin=false 表示只有非管理员角色才需要过滤数据
	if ignoreAdmin || (!ignoreAdmin && loginUser.RoleKey != "CJGLY") {
    
    
		if isId {
    
    
			return loginUser.ChildId
		} else {
    
    
			return loginUser.ChildName
		}
	}
	return ""
}

// 统一的数据过滤 fieldName 要查询的字段,ignoreAdmin 是否忽略超级管理员(true 忽略 false 不忽略),isId 表示是用id还是用name查询
// dataScope 数据范围(1 所有数据 2 所在部门及子部门数据 3 所在部门数据 4 仅本人数据 5 自定义数据)
func AppendQueryDataScope(token, fieldName, dataScope string, ignoreAdmin, isId bool) string {
    
    
	str := GetDataScope(token, ignoreAdmin, isId)
	sql := ""
	if str != "" {
    
    
		// 根据 当前用户的数据范围 拼接查询条件语句 scope 数据范围、过滤条件、fieldName 查询的字段名
		if dataScope == "5" {
    
    
			// 自定义数据范围(暂不需要)
			// 5 和其他数字的范围取并集,用 or 连接,并且它们的外层不要忘了用括号括起来
		} else if dataScope == "2" {
    
    
			// 所在部门及子部门数据(用FIND_IN_SET查询)
			sql = fmt.Sprintf("FIND_IN_SET(%s,'%s')", fieldName, str)
		} else if dataScope == "3" {
    
    
			// 所在部门数据(用等于查询)
			sql = fmt.Sprintf("%s = '%s'", fieldName, str)
		} else if dataScope == "4" {
    
    
			// 仅本人数据直接用等于查询
			sql = fmt.Sprintf("%s = '%s'", fieldName, str)
		}
	}
	return sql
}

// 校验是否有数据权限(新增、修改、删除数据时):verified 需要校验的值
func CheckDataScope(token, verified string, ignoreAdmin, isId bool) bool {
    
    
	scope := GetDataScope(token, ignoreAdmin, isId)
	// 当scope不是空值时,判断需要校验的值是否包含在scope中,不包含说明没有权限
	if scope != "" && !util.IsContain(strings.Split(scope, ","), verified) {
    
    
		return false
	}
	return true
}

About data permissions

The data permissions are just for reference. I feel that my current implementation method still has some problems, but I can't think of any other particularly good solutions, so I will use this method for the time being. . .

How I implement it now:

  • The user logs in, obtains the user's department ID, and obtains the sub-department data below it based on this department ID;
  • After obtaining the sub-department data, use the user department id as the key, department data and sub-department data as the value, and cache them in redis;
  • When filtering data or judging data permissions, go to redis to see if there is a corresponding key according to the current user's department ID. If there is, get it directly. If not, add it to redis;
  • Use the user department id as the key, so that as long as users in this department log in, they will share the same data permission information;
  • When adding a new department, determine whether its parentId exists in redis. If it exists, you need to update the data permissions of parentId (add this new department to the child department data of this parentId)

There is still a problem with the above implementation: when adding a new department, the data permission of parentId needs to be updated, but does it need to be updated when modifying the department? When modifying, should only the cache of the current id be updated, or should the cache of parentId also be updated? (I don’t know how to do it yet, so let’s do this for now)

at last

ok, the above is the entire content of this article. When I finish updating all the articles on this project, I will release the address of the complete code. Welcome everyone to like and support. Finally, you can follow me to not get lost~

Guess you like

Origin blog.csdn.net/weixin_43165220/article/details/134930629