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
}
转载自:https://juejin.cn/post/7037786702659715085