【Go】基于GoFiber从零开始搭建一个GoWeb后台管理系统(三)日志管理(登录日志、操作日志)、用户登录模块

第一篇:【Go】基于GoFiber从零开始搭建一个GoWeb后台管理系统(一)搭建项目

第二篇:【Go】基于GoFiber从零开始搭建一个GoWeb后台管理系统(二)日志输出中间件、校验token中间件、配置路由、基础工具函数。

前两篇我们搭好了项目,并且配置好了中间件和路由,这篇开始我们可以正式写业务代码了。

在这里插入图片描述

日志管理

首先我们先来实现日志管理这个模块,像登录、退出、操作数据库(增、删、改)都会记录日志保存到sys_log表中。

数据库表我在第一篇已经全部写出来了,这里就不再重复了。

先看看记录的数据:

【Go】基于GoFiber从零开始搭建一个GoWeb后台管理系统(三)日志管理(登录日志、操作日志)、用户登录模块_第1张图片

日志管理这个模块,其实接口的话就一个接口:列表查询。

router.go

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

congroller层: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层: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
}

记录操作日志中间件

列表查询这个没什么,这个模块主要难在怎么获取到需要的数据,然后处理好添加到数据库中。

在Java中,我们可以用注解+aop的方式,设置和获取对应的信息,比如通过注解设置 title、type,通过aop切面获取 请求的接口、前端传给后端的参数、后端返回给前端的数据等等。

在go中,我们想实现类似的功能,需要用中间件,不过中间件只能实现类似Java中aop的功能,也就是说中间件只能获取请求到的信息和返回的信息。像设置 title、type 就只能自己定义标准,然后根据请求的接口来判断、设置 title、type。

title:就是接口名称,type:就是操作类型。

思路

我的思路就是:每个接口按模块来划分,像注册路由的时候,我们也都是一个模块的接口全部放到一个路由组中。然后接口命名也保持统一风格。这样我们一个模块的所有接口都有一个共同的前缀。

比如用户管理模块,它的路由是这样的:

// 用户管理路由
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)                 // 上传头像
	}
}

这里面的接口,都是 /sys/user 开头的;其他模块也是同理,所以我们定义一个标准就是接口以什么开头就属于哪个模块。以 /sys/user 开头的接口都是用户管理模块的,以 /sys/dept 开头的都是部门管理模块。

然后接口命名保持统一风格,增、删、改、查这些接口命名风格统一(主要是增、删、改)

  • 新增接口统一用 insert 命名或开头
  • 修改接口统一用 update 命名或开头
  • 删除接口统一用 delete 命名或开头
  • 上传接口统一用 upload 命名或开头
  • 导入接口统一用 imports 命名或开头

这样我们判断 操作类型 时就好判断了。比如 /sys/user/insert 接口我们就可以判断 type = 新增,title = 用户新增

代码

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()
}

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

这样我们就可以获取到操作日志了。然后登录日志的话,我们在登录接口设置更好一点。

用户登录、退出

接下来我们来实现登录模块。

// 登录路由
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) // 用户退出
	}
}

登录模块有上面这几个接口,除了登录退出,还有获取登录的验证码,还有登录的时候用户输入用户名、密码调用登录接口时,前端需要将用户名、密码根据RSA公钥进行加密传输,后端接收到参数时也需要根据RSA私钥进行解密,获取到真正的用户名和密码。

登录时校验密码,有错误次数限定超过了这个账号就会锁定15或30分钟,在这期间不允许再次登录。

controller层:sys_login.go

这里因为我没有前端用的是apipost测试的,所以RSA加解密那块我注释掉了。

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层:sys_login.go

其实这个文件的这几个登录、token相关的方法,本来是应该放在 sys_user.go 文件里的,然后因为 sys_user.go 代码有点多,我想了下最后还是决定单独建个 sys_login.go 文件分开来。

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
}

关于数据权限

数据权限这一块,仅作一个参考,我感觉我现在这个实现方式,也还是有点点问题,但是又想不到其他特别好的解决办法,就暂时先用这种方式吧。。。

我现在的实现方式:

  • 用户登录,获取用户的部门id,根据这个部门id获取它下面的子部门数据;
  • 获取到子部门数据后,以用户部门id为key,部门数据、子部门数据为value,缓存到redis中;
  • 过滤数据或判断数据权限时,就是按当前用户的部门id,去redis中找有没有对应的key,有就直接获取,没有就添加到redis中;
  • 以用户部门id为key,这样只要是这个部门的用户登录,都是共用一个数据权限信息;
  • 新增部门时,判断它的 parentId 有没有存在于redis,存在则需要更新 parentId 的数据权限(把这个新增的部门加到这个 parentId 的子级部门数据中)

上面的实现方式还存在一个问题:新增部门时要更新 parentId 的数据权限,但是修改部门要不要更新呢?修改的时候是只更新当前 id 的缓存,还是 parentId 的缓存也要一起更新?(暂时不知道要怎么做,就先这样吧)

最后

ok,以上就是本篇文章的全部内容了,等我更完这个项目的全部文章,我会放出完整代码的地址,欢迎大家多多点赞支持下,最后可以关注我不迷路~

你可能感兴趣的:(Go,GoWeb,golang,go-fiber,gorm,go-redis)