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

news2024/11/17 23:34:28

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

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

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

在这里插入图片描述

日志管理

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

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

先看看记录的数据:

在这里插入图片描述

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1309308.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

赛氪为第五届全球校园人工智能算法精英大赛决赛选手保驾护航

12月10日&#xff0c;以“智青春算未来”为主题的2023年第五届全球校园人工智能算法精英大赛全国总决赛在河海大学江宁校区举行。本次大赛由江苏省人工智能学会主办&#xff0c;自9月份启动以来&#xff0c;共吸引了全国近400所高校的3000多支参赛团队参加。经过校赛、省赛选拔…

Linux(统信UOS) 发布.Net Core,并开启Https,绑定证书

实际开发中&#xff0c;有时会需要为小程序或者需要使用https的应用提供API接口服务&#xff0c;这就需要为.Net Core 配置https&#xff0c;配置起来很简单&#xff0c;只需要在配置文件appsettings.json中添加下面的内容即可 "Kestrel": {"Endpoints": …

LeetCode力扣每日一题(Java)66、加一

每日一题在昨天断开了一天&#xff0c;是因为作者沉迷吉他&#xff0c;无法自拔……竟然把每日一题给忘了&#xff0c;所以今天&#xff0c;发两篇每日一题&#xff0c;把昨天的给补上 一、题目 二、解题思路 1、我的思路 其实乍一看这道题还是比较简单的&#xff0c;就是让…

C/C++ 汇总区间-返回恰好覆盖数组中所有数字的最小有序区间范围列表

给定一个无重复元素的有序数数组ums。返回恰好覆盖数组中所有数字的最小有序区间范围列表。 也就是说&#xff0c;nums 的每个元素都恰好被某个区间范围所覆盖&#xff0c;并且不存在属于某个范围但不属于 nums 的数字x。 列表中的每个区间范围 [a,b] 应该按如下格式输出&…

zabbix——实现高效网络监控

在当今的数字化时代&#xff0c;网络和服务器的健康状况对于企业的正常运营至关重要。为了及时发现和解决潜在的问题&#xff0c;许多企业选择使用网络监控工具来追踪服务器的性能和网络参数。其中&#xff0c;Zabbix是一个功能强大且开源的网络监控工具&#xff0c;被广泛应用…

unity 2d 入门 飞翔小鸟 死亡闪烁特效(十三)

一、c#脚本 using System.Collections; using System.Collections.Generic; using UnityEngine;public class Bling : MonoBehaviour {public Texture img;public float speed;public static bool changeWhite false;private float alpha0f;// Start is called before the fi…

EasyRecovery(数据恢复软件) 2024中文绿色无需激活版下载

EasyRecovery是一款功能强大且专业的数据恢复软件&#xff0c;软件能够对电脑误删的文件进行恢复&#xff0c;包括格式化硬盘是数据恢复、手机U盘数据恢复等&#xff0c;小编今天给大家带来的是根据官软件解压后直接使用。感兴趣的朋友快来下载使用吧。 EasyRecovery-2024mac最…

PCL显示double类型的点云方案

前言 我们知道&#xff0c;PCL和OpenGL等三维可视化软件一样&#xff0c;为了兼顾性能需求&#xff0c;只支持加载Float类型的点云数据&#xff0c;但是当我们对精度要求太高时&#xff0c;Float类型支持不了我们的精度要求&#xff0c;我们的数据必须用Double类型存储才可以&…

Vue 3 开发中遇到的问题及解决方案(fix bug)

开发环境&#xff1a;mac系统&#xff0c;node版本&#xff1a; 16.15.0 版本兼容问题 vite v3.2.4 building for development... hasInjectionContext is not exported by node_modules/pinia/node_modules/vue-demi/lib/index.mjs, imported by node_modules/pinia/dist/pini…

【MySQL命令】show slave status\G 超详细全面解释

这个命令是DBA日常运维中常用来查看主从状态的命令&#xff0c;很多备份&#xff0c;监控工具也会使用到该命令监控主从状态是否正常&#xff0c;主从延迟&#xff0c;获取位点信息等。作为常用日常命令&#xff0c;一定要完全理解该命令的输出。今天主要结合 官方文档 和 实际…

AndroidStudio使用小技巧(持续更新中)

改过一次后&#xff0c;当时还记着&#xff1b;再拿起来&#xff0c;就忘记了。还是记下来吧。 使用鼠标滚轮设置文本的字体大小 左上角 File->Setting->Editor 启动时设置是否自动打开项目 老版本AS会有 Reopen last project on startup新版本AS有 Reopen projects o…

Stable Diffusion - High-Resolution Image Synthesis with Latent Diffusion Models

Paper name High-Resolution Image Synthesis with Latent Diffusion Models Paper Reading Note Paper URL: https://arxiv.org/abs/2112.10752 Code URL: https://github.com/CompVis/latent-diffusion TL;DR 2021 年 runway 和慕尼黑路德维希马克西米利安大学出品的文…

《从入门到精通:AJAX基础知识解析,前端开发中利器》基础篇

目录 学习目标&#xff1a; 学习目录&#xff1a; 学习时间&#xff1a; 学习内容&#xff1a; 什么是 AJAX&#xff1f; 怎么用 AJAX &#xff1f; 认识 URL 协议 域名 资源路径 获取 - 新闻列表 URL 查询参数 axios&#xff0d;查询参数 常用请求方法和数据提…

【docker】镜像使用(Nginx 示例)

查看本地镜像列表 docker images删除本地镜像 # docker rmi [容器 ID]docker rmi a6bd71f48f68 查找镜像 docker search nginx 参数介绍 NAME: 镜像仓库源的名称DESCRIPTION: 镜像的描述OFFICIAL: 是否 docker 官方发布STARS: 点赞、喜欢AUTOMATED: 自动构建。 拉去镜像 …

动态雨滴个人主页导航页

资源入口 动态雨滴玻璃掉落主页&#xff0c;和之前的两个一个性质&#xff0c;css全部重构&#xff0c;canvas绘制的下雨背景&#xff0c;可以自己去换图片。 原版&#xff1a; 已二次开发&#xff08;可添加多个导航格及自适应手机端&#xff09; ------本页内容已结束&#x…

DHTMLX Suite v8.3发布!深化JavaScript UI小部件库使用体验

DHTMLX UI 组件库允许您更快地构建跨平台、跨浏览器 Web 和移动应用程序。它包括一组丰富的即用式 HTML5 组件&#xff0c;这些组件可以轻松组合到单个应用程序界面中。 DHTMLX Suite v8.3已于近日正式发布啦&#xff01;这个更新附带了一组新特性和改进&#xff0c;旨在促进您…

销售技巧培训之如何提升网络销售技巧

销售技巧培训之如何提升网络销售技巧 随着互联网的普及&#xff0c;网络销售已经成为了一种重要的销售方式。对于许多企业来说&#xff0c;网络销售已经成为了一种重要的销售渠道。但是&#xff0c;要想在网络销售中取得成功&#xff0c;就需要掌握一些网络销售技巧。本文将介…

php 和 python 跨界 合作 phpy搭建 已解决

需求介绍 1、在日常功能开发中&#xff0c;难免会使用python的计算库&#xff0c;同时自己要是一名PHP开发工程师。就在最近有相应的需求&#xff0c;索性使用phpy来进行功能开发 安装 windows版本 phpy 是识沃团队最新推出的开源项目&#xff0c;目标是为 PHP 引入 Python 生…

『App自动化测试之Appium应用篇』| 元素定位工具uiautomatorviewer从简介、特点、启动到使用的完整过程

『App自动化测试之Appium应用篇』| 元素定位工具uiautomatorviewer从简介、安装、配置到使用的完整过程 1 uiautomatorviewer简介2 uiautomatorviewer特点3 uiautomatorviewer启用4 Work Bar5 Screenshot6 控件布局7 控件属性8 连接设备9 Error while obtaining UI问题排查 1 u…

客户端游戏开发者要不要学习服务端开发的相关技能?

引言 大家好&#xff0c;今天继续分享点个人经验。 玩过游戏的小伙伴都知道&#xff0c;游戏会有单机和联网之分。 单机游戏通常以高质量3A大作著称&#xff0c;它的特点是独立的运行环境&#xff0c;不受网络的限制。 联机游戏则以全球互联、实时同步以及社交互动等特点发…