Go语言 基于gin框架从0开始构建一个bbs server(六)- redis点赞功能,最新最热列表

源码地址

redis 数据库 设计

假设业务逻辑 是 点赞 给点赞数+1 点踩 给点赞数-1

使用zset 有序集合 来设计 数据库 就可以了

zset A:

valus 是帖子id score 帖子的点赞数量

另外 我们要考虑 每个用户 只能给一个帖子 投一次,你可以点赞 也可以点踩,但是你不能多次重复点赞 或者点踩

zset 帖子ID:

values 是 给这个帖子投票的 用户的id, score 就是1 或者 -1 代表这个用户对这个帖子点赞或者点踩

具体实现

首先注册路由,这种点赞的 显然需要校验是否登录

v1.POST("/like/", middleware.JWTAuthMiddleWare(), controllers.PostLikeHandler)
复制代码

定义两种错误以及参数

type ParamLikeData struct {
   // 帖子id
   PostId int64 `json:"post_id,string" binding:"required"`
   // 1 点赞 -1 点踩 oneof 是限制这个值只能为多少
   Direction int64 `json:"direction,string" binding:"required,oneof=1 -1"`
}

const (
   DirectionLike   = 1
   DirectionUnLike = -1
)
复制代码

var ErrAleadyLike = errors.New("不能重复点赞")
var ErrAleadyUnLike = errors.New("不能重复点踩")
复制代码

controller实现

// 点赞 点踩
func PostLikeHandler(c *gin.Context) {

   p := new(models.ParamLikeData)

   if err := c.ShouldBindJSON(p); err != nil {
      zap.L().Error("PostLikeHandler with invalid param", zap.Error(err))
      // 因为有的错误 比如json格式不对的错误 是不属于validator错误的 自然无法翻译,所以这里要做类型判断
      errs, ok := err.(validator.ValidationErrors)
      if !ok {
         ResponseError(c, CodeInvalidParam)
      } else {
         ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans)))
      }
      return
   }
   id, err := getCurrentUserId(c)
   if err != nil {
      ResponseError(c, CodeNoLogin)
      return
   }

   // 业务处理
   err = logic.PostLike(p, id)
   if err != nil {
      // 可以把
      if errors.Is(err, logic.ErrAleadyLike) || errors.Is(err, logic.ErrAleadyUnLike) {
         ResponseErrorWithMsg(c, CodeInvalidParam, err.Error())
      } else {
         ResponseError(c, CodeServerBusy)
      }
      return
   }
   ResponseSuccess(c, "success")
}
复制代码

看下logic层

func PostLike(postData *models.ParamLikeData, userId int64) error {
   // 查询之前有没有点过赞
   direction, flag := redis.CheckLike(postData.PostId, userId)

   if flag {
      // 如果之前点过赞 则要判断 这次是否是重复点赞
      if direction == postData.Direction && direction == models.DirectionLike {
         return ErrAleadyLike
      }
      // 如果之前点过赞 则要判断 这次是否是重复 点踩
      if direction == postData.Direction && direction == models.DirectionUnLike {
         return ErrAleadyUnLike
      }
   }

   err := redis.DoLike(postData.PostId, userId, postData.Direction)
   if err != nil {
      return err
   }

   err = redis.AddLike(postData.PostId, postData.Direction)
   if err != nil {
      return err
   }
   return nil
}
复制代码

dao层 主要就是熟悉一下 redis的基本操作

package redis

import (
   "go_web_app/utils"

   "github.com/go-redis/redis"

   "go.uber.org/zap"
)

func getRedisKeyForLikeUserSet(postId int64) string {
   key := KeyPostLikeZetPrefix + utils.Int64ToString(postId)
   zap.L().Debug("getRedisKeyForLikeUserSet", zap.String("setKey", key))
   return key
}

// CheckLike 判断之前有没有投过票 true 代表之前 投过 false 代表之前没有投过
func CheckLike(postId int64, userId int64) (int64, bool) {
   like := rdb.ZScore(getRedisKeyForLikeUserSet(postId), utils.Int64ToString(userId))
   result, err := like.Result()
   if err != nil {
      zap.L().Error("checkLike error", zap.Error(err))
      return 0, false
   }
   zap.L().Info("checkLike val", zap.Float64(utils.Int64ToString(userId), like.Val()))
   return int64(result), true
}

// DoLike 点赞 或者点踩 记录这个用户对这个帖子的行为
func DoLike(postId int64, userId int64, direction int64) error {
   value := redis.Z{
      Score:  float64(direction),
      Member: utils.Int64ToString(userId),
   }
   _, err := rdb.ZAdd(getRedisKeyForLikeUserSet(postId), value).Result()
   if err != nil {
      zap.L().Error("doLike error", zap.Error(err))
      return err
   }
   return nil
}

// AddLike 用户对帖子点赞之后 要去更新该帖子的 点赞数量
func AddLike(postId int64, direction int64) error {
   _, err := rdb.ZIncrBy(KeyLikeNumberZSet, float64(direction), utils.Int64ToString(postId)).Result()
   if err != nil {
      zap.L().Error("AddLike error", zap.Error(err))
      return err
   }
   return nil
}
复制代码

用事务来改进

之前的写法虽然看上去没错,但是依旧有隐患,因为 我们点赞实际上涉及到2个写入的操作, 这里应该 一荣俱荣 一损俱损。也就是说要使用 事务 来把 两个写入操作 一起包起来

// DoLike 点赞 或者点踩 记录这个用户对这个帖子的行为
func DoLike(postId int64, userId int64, direction int64) error {
   pipeLine := rdb.TxPipeline()
   value := redis.Z{
      Score:  float64(direction),
      Member: utils.Int64ToString(userId),
   }
   pipeLine.ZAdd(getRedisKeyForLikeUserSet(postId), value)
   pipeLine.ZIncrBy(KeyLikeNumberZSet, float64(direction), utils.Int64ToString(postId))
   _, err := pipeLine.Exec()
   if err != nil {
      zap.L().Error("doLike error", zap.Error(err))
      return err
   }
   return nil
}
复制代码

最新最热列表

最新列表很好理解,无非就是按照create_time 排序而已

那么最热列表呢 其实就是按照点赞数量 从高到低排序

最热列表怎么做?

其实就是发帖的时候,发帖成功就放到我们的 点赞数量 的redis zset中。

然后最热列表的时候 就zset 按照 点赞数量来排序 然后返回给我们 对应的 帖子id 列表

有了这个id的切片 我们再去 mysql 查询帖子的详情 就可以了。

具体看下如何做这个需求

首先 是在 发表帖子的时候 顺便在redis中新增一条记录

先写一下 redis的操作

// AddPost 每次发表帖子成功 都去 zset里面 新增一条记录
func AddPost(postId int64) error {
   _, err := rdb.ZAdd(KeyLikeNumberZSet, redis.Z{
      Score:  0,
      Member: utils.Int64ToString(postId),
   }).Result()
   if err != nil {
      zap.L().Error("AddPost", zap.Error(err))
      return err
   }
   return nil
}
复制代码

然后改一下 我们发帖的logic层 就可以了

func CreatePost(post *models.Post) (msg string, err error) {
   // 雪花算法 生成帖子id
   post.Id = snowflake.GenId()
   zap.L().Debug("createPostLogic", zap.Int64("postId", post.Id))
   err = mysql.InsertPost(post)
   if err != nil {
      return "failed", err
   }
   // 去点赞数量的 zset 新增一条记录
   err = redis.AddPost(post.Id)
   if err != nil {
      return "", err
   }
   //发表帖子成功时 要把帖子id 回给 请求方
   return strconv.FormatInt(post.Id, 10), nil
}
复制代码

然后就是写我们的list 接口了

首先定义一下 参数

type ParamListData struct {
   PageSize int64  `form:"pageSize" binding:"required"`
   PageNum  int64  `form:"pageNum" binding:"required"`
   Order    string `form:"order" binding:"required,oneof=time hot"`
}
复制代码
const (
   DirectionLike   = 1
   DirectionUnLike = -1
   // 按照帖子时间排序
   OrderByTime = "time"
   // 按照点赞数量排序
   OrderByHot = "hot"
)
复制代码

然后就是在controller层 解析参数

func GetPostListHandler2(c *gin.Context) {
   // 获取参数和参数校验
   p := new(models.ParamListData)
   // 校验下参数
   if err := c.ShouldBindQuery(p); err != nil {
      zap.L().Error("CreatePostHandler with invalid param", zap.Error(err))
      errs, ok := err.(validator.ValidationErrors)
      if !ok {
         ResponseError(c, CodeInvalidParam)
      } else {
         ResponseErrorWithMsg(c, CodeInvalidParam, removeTopStruct(errs.Translate(trans)))
      }
      return
   }

   apiList, err := logic.GetPostList2(p)
   if err != nil {
      return
   }
   ResponseSuccess(c, apiList)
}
复制代码

重点看下 logic层

func GetPostList2(params *models.ParamListData) (apiPostDetailList []*models.ApiPostDetail, err error) {
   // 最热
   if params.Order == models.OrderByHot {
      // 先去redis 里面取 最新的数据
      ids, err := redis.GetPostIdsByScore(params.PageSize, params.PageNum)
      if err != nil {
         return nil, err
      }
      postLists, err := mysql.GetPostListByIds(ids)
      if err != nil {
         return nil, err
      }
      return rangeInitApiPostDetail(postLists)

   } else if params.Order == models.OrderByTime {
      //最新
      return GetPostList(params.PageSize, params.PageNum)
   }
   return nil, nil
}
复制代码

最新的查询很简单 其实就是之前我们的帖子列表查询里面 sql语句 增加个order by create_time desc 即可 这里就不重复写了

看下redis层怎么写

//  按照点赞数 降序排列
func GetPostIdsByScore(pageSize int64, pageNum int64) (ids []string, err error) {
   start := (pageNum - 1) * pageSize
   stop := start + pageSize - 1
   ids, err = rdb.ZRevRange(KeyLikeNumberZSet, start, stop).Result()
   if err != nil {
      zap.L().Error("GetPostIdsByScore", zap.Error(err))
      return nil, err
   }
   return ids, err
}
复制代码

拿到 降序排列的帖子id 以后 就可以去mysql 查询具体数据了

func GetPostListByIds(ids []string) (postList []*models.Post, err error) {
   //FIND_IN_SET 按照给定的顺序 来返回结果集
   sqlStr := "select post_id,title,content,author_id,community_id,create_time,update_time" +
      " from post where post_id in (?) order by FIND_IN_SET(post_id,?)"
   query, args, err := sqlx.In(sqlStr, ids, strings.Join(ids, ","))
   if err != nil {
      return nil, err
   }
   query = db.Rebind(query)
   err = db.Select(&postList, query, args...)
   if err != nil {
      zap.L().Error("GetPostListByIds", zap.Error(err))
      return nil, err
   }
   return postList, nil
}
复制代码

这样一来 就是帖子的集合 已经查找完毕了

最后一步 就是把我们的post 数据封装成 api数据即可

这个逻辑 我们之前写过,这里仅仅是抽出来,方便我们2个logic 都可以调用

func rangeInitApiPostDetail(posts []*models.Post) (apiPostDetailList []*models.ApiPostDetail, err error) {
   for _, post := range posts {
      //再查 作者 名称
      username, err := mysql.GetUserNameById(post.AuthorId)
      if err != nil {
         zap.L().Warn("no author ")
         err = nil
         return nil, err
      }
      //再查板块实体
      community, err := GetCommunityById(post.CommunityId)
      if err != nil {
         zap.L().Warn("no community ")
         err = nil
         return nil, err
      }
      apiPostDetail := new(models.ApiPostDetail)
      apiPostDetail.AuthorName = username
      apiPostDetail.Community = community
      apiPostDetail.Post = post
      apiPostDetailList = append(apiPostDetailList, apiPostDetail)
   }
   return apiPostDetailList, nil
}
复制代码

猜你喜欢

转载自juejin.im/post/7037786702659715085