项目亮点之赞踩功能

#赞踩功能的实现

##1.LikeController

@Controller
public class LikeController {
    @Autowired
    HostHolder hostHolder;
    @Autowired
    LikeService likeService;
    @Autowired
    NewsService newsService;
    @Autowired
    EventProducer eventProducer;

    @RequestMapping(path = {"/like"}, method = {RequestMethod.GET, RequestMethod.POST})
    @ResponseBody
    public String like(@Param("newId") int newsId) {
       //在likeService中进行点赞具体操作
        long likeCount = likeService.like(hostHolder.getUser().getId(), EntityType.ENTITY_NEWS, newsId);
        // 更新new的被点赞数,其实就是调用DAO层更新数据库里的likeCount
        newsService.updateLikeCount(newsId, (int) likeCount);
        return ToutiaoUtil.getJSONString(0, String.valueOf(likeCount));
    }
    @RequestMapping(value = "/dislike",method = {RequestMethod.POST,RequestMethod.GET})
    @ResponseBody
    public String dislike(@RequestParam("newsId") int newsId){
        int userId=hostHolder.getUser().getId();
        long likeCount=likeService.disLike(userId, EntityType.ENTITY_NEWS,newsId);
        //更新数据库里的likecount值
        newsService.updateLikeCount(newsId,(int)likeCount);
        return ToutiaoUtil.getJSONString(0,String.valueOf(likeCount));
    }
}

##2.前端:

###home.html

<div class="post">
    <div class="votebar">
     有关点赞的操作
        #if ($vo.like > 0)
        <button class="click-like up pressed" data-id="$!{vo.news.id}" title="赞同"><i class="vote-arrow"></i><span class="count">$!{vo.news.likeCount}</span></button>
        #else
        <button class="click-like up" data-id="$!{vo.news.id}" title="赞同"><i class="vote-arrow"></i><span class="count">$!{vo.news.likeCount}</span></button>
        #end
      #有关灭赞的操作
        #if($vo.like < 0)
        <button class="click-dislike down pressed" data-id="$!{vo.news.id}" title="反对"><i class="vote-arrow"></i></button>
        #else
        <button class="click-dislike down" data-id="$!{vo.news.id}" title="反对"><i class="vote-arrow"></i></button>
        #end

可以看到点赞和撤赞操作都有对应的button

###static/scripts/main/site/home.js

(function (window, undefined) {
    var PopupLogin = Base.getClass('main.component.PopupLogin');
    var PopupUpload = Base.getClass('main.component.PopupUpload');
    var ActionUtil = Base.getClass('main.util.Action');

    Base.ready({
        initialize: fInitialize,
        binds: {
            //.表示class #表示id
            'click .js-login': fClickLogin,
            'click .js-share': fClickShare
        },
        events: {
          //一个点赞操作,一个撤赞操作
            'click button.click-like': fClickLike,
            'click button.click-dislike': fClickDisLike
        }
    });
  //其他方法略过

    function fClickLike(oEvent) {
        var that = this;
        var oEl = $(oEvent.currentTarget);
        var sId = $.trim(oEl.attr('data-id'));
        // 已经操作过 || 不存在Id || 正在提交 ,则忽略
        if (oEl.hasClass('pressed') || !sId || that.actioning) {
            return;
        }
        that.actioning = true;
        ActionUtil.like({
            newsId: sId,
            call: function (oResult) {
                oEl.find('span.count').html(oResult.msg);
                oEl.addClass('pressed');
                oEl.parent().find('.click-dislike').removeClass('pressed');
            },
            error: function () {
                alert('出现错误,请重试');
            },
            always: function () {
                that.actioning = false;
            }
        });
    }

    function fClickDisLike(oEvent) {
        var that = this;
        var oEl = $(oEvent.currentTarget);
        var sId = $.trim(oEl.attr('data-id'));
        // 已经操作过 || 不存在Id || 正在提交 ,则忽略
        if (oEl.hasClass('pressed') || !sId || that.actioning) {
            return;
        }
        that.actioning = true;
        ActionUtil.dislike({
            newsId: sId,
            call: function (oResult) {
                oEl.addClass('pressed');
                var oLikeBtn = oEl.parent().find('.click-like');
                oLikeBtn.removeClass('pressed');
                oLikeBtn.find('span.count').html(oResult.msg);
            },
            error: function () {
                alert('出现错误,请重试');
            },
            always: function () {
                that.actioning = false;
            }
        });
    }

})(window);

###static/scripts/main/util/action.js中

/**
 * 喜欢
 * @param   {Object} oConf
 *  @param  {String} oConf.newsId 对象id
 *  @param  {Function} oConf.call 成功回调
 *  @param  {Function} oConf.error 失败回调
 *  @param  {Function} oConf.always 操作的回调
 */
function fLike(oConf) {
    var that = this;
    that.post({
        url: '/like',
        data: {newsId: oConf.newsId},
        call: oConf.call,
        error: oConf.error,
        always: oConf.always
    });
}

/**
 * 不喜欢
 * @param   {Object} oConf
 *  @param  {String} oConf.newsId 对象id
 *  @param  {Function} oConf.call 成功回调
 *  @param  {Function} oConf.error 失败回调
 *  @param  {Function} oConf.always 操作的回调
 */
function fDislike(oConf) {
    var that = this;
    that.post({
        url: '/dislike',
        data: {newsId: oConf.newsId},
        call: oConf.call,
        error: oConf.error,
        always: oConf.always
    });
}

简单的说就是当我们点赞时,会激发url:”/like”也就是会调用LikeController中与/like绑定的方法

##3.LikeService

###com.nowcoder.service.LikeService

@Service
public class LikeService {
    @Autowired
    JedisAdapter jedisAdapter;
    //判断某个用户对某一项元素是否喜欢,思路就是在redis中找likeKey对应的set里是否有这个点赞人的userid
    public int getLikeStatus(int userId,int entityType,int entityId){
        String likeKey= RedisKeyUtil.getLikeKey(entityType,entityId);
        if (jedisAdapter.sismember(likeKey,String.valueOf(userId))){
            return 1;
        }
        String dislikeKey= RedisKeyUtil.getDisLikeKey(entityType,entityId);
        return jedisAdapter.sismember(dislikeKey,String.valueOf(userId))?-1:0;
    }
    /**
     * 喜欢点赞操作
     * 返回 喜欢的数量
     */
    public long like(int userId,int entityType,int entityId){
        String likeKey=RedisKeyUtil.getLikeKey(entityType,entityId);
        //把用户加到喜欢的likeKey的KV中,就是说这条咨询有多少人喜欢
        //也就是说redis里面不同的likeKey作为k,每个likeKey代表的是不同类型(entityType)的特定ID的新闻有多少人喜欢
        //V是一个set集合,里面就是为这条新闻点赞的userID们
        jedisAdapter.sadd(likeKey,String.valueOf(userId));
        //删除不喜欢中的userId
        String disLikeKey=RedisKeyUtil.getDisLikeKey(entityType,entityId);
        jedisAdapter.srem(disLikeKey,String.valueOf(userId));
        //在返回有多少人喜欢
        return jedisAdapter.scard(likeKey);
    }
    /**
     * 不喜欢点赞操作
     * 返回 喜欢的数量
     */
    public long disLike(int userId,int entityType,int entityId){
        String disLikeKey=RedisKeyUtil.getDisLikeKey(entityType,entityId);
        //把用户加到不喜欢的disLikeKey的KV中
        jedisAdapter.sadd(disLikeKey,String.valueOf(userId));
        //删除喜欢中的userId
        String likeKey=RedisKeyUtil.getLikeKey(entityType,entityId);
        jedisAdapter.srem(likeKey,String.valueOf(userId));
        //在返回有多少人喜欢
        return jedisAdapter.scard(likeKey);
    }
}

###com.nowcoder.util.ToutiaoUtil

public class RedisKeyUtil {
    public static String SPILT=":";
    public  static String BIZ_LIKE="LIKE";
    public  static String BIZ_DISLIKE="DISLIKE";
    private static String BIZ_EVENT = "EVENT";

    public static String getEventQueueKey() {
        return BIZ_EVENT;
    }

    public static String getLikeKey(int entityId,int entityType){
        return BIZ_LIKE+SPILT+String.valueOf(entityType)+SPILT+String.valueOf(entityId);
    }
    public static String getDisLikeKey(int entityId,int entityType){
        return BIZ_DISLIKE+SPILT+String.valueOf(entityType)+SPILT+String.valueOf(entityId);
    }

}

在redis中点赞事件存储的kv键值对的名:

点赞:LIKE:int( entityType):int(entityId)

撤赞:DISLIKE:int( entityType):int(entityId)

redis 常见数据结构以及使用场景分析

##1.String

常用命令: set,get,decr,incr,mget 等。 String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用;

常规计数:微博数,粉丝数等。

###举例:

        jedis.set("hello", "world");//设置指定 key 的值
        print(1, jedis.get("hello"));//world
        jedis.rename("hello", "newhello");//重命名
        print(1, jedis.get("newhello"));//world
        jedis.setex("hello2", 15, "world");//为指定的 key 设置值及其过期时间

        // 数值操作
        jedis.set("pv", "100");
        jedis.incr("pv");//默认加1
        print(2, jedis.get("pv"));//101
        jedis.decrBy("pv", 5);//减5
        print(2, jedis.get("pv"));//96
        print(3, jedis.keys("*"));//[newhello, hello2, pv]

##2.Hash

常用命令: hget,hset,hgetall 等。

Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅 仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等等。

###举例:

        // hash, 可变字段
        String userKey = "userxx";
        jedis.hset(userKey, "name", "jim");//hash中添键值对
        jedis.hset(userKey, "age", "12");
        jedis.hset(userKey, "phone", "18666666666");
        print(12, jedis.hget(userKey, "name"));//jim
        print(13, jedis.hgetAll(userKey));//{phone=18666666666, name=jim, age=12}
        jedis.hdel(userKey, "phone");
        print(14, jedis.hgetAll(userKey));//{name=jim, age=12},删除了个phone
        print(15, jedis.hexists(userKey, "email"));//false
        print(16, jedis.hexists(userKey, "age"));//true
        print(17, jedis.hkeys(userKey));//[name, age]
        print(18, jedis.hvals(userKey));//[jim, 12]
        jedis.hsetnx(userKey, "school", "zju");//不存在就添加 
        jedis.hsetnx(userKey, "name", "yxy");//存在就不添加
        print(19, jedis.hgetAll(userKey));//{school=zju, name=jim, age=12}

##3.List

常用命令: lpush,rpush,lpop,rpop,lrange等

list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表, 消息列表等功能都可以用Redis的 list 结构来实现。

Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。 另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。

###举例:

        String listName = "list";
        jedis.del(listName);
        for (int i = 0; i < 10; ++i) {
            jedis.lpush(listName, "a" + String.valueOf(i));
        }
        print(4, jedis.lrange(listName, 0, 12)); // 最近来访10个id:[a9, a8, a7, a6, a5, a4, a3, a2, a1, a0]
        print(5, jedis.llen(listName));//10
        print(6, jedis.lpop(listName));//a9
        print(7, jedis.llen(listName));//9,出去了个a9
        print(8, jedis.lrange(listName, 2, 6)); //[a6, a5, a4, a3, a2]
        print(9, jedis.lindex(listName, 3));//a5

##4.Set

常用命令: sadd,spop,smembers,sunion 等
set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。

当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在 一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。

比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常 方便的实现如共同关注、共同粉丝、共同喜好等功能。

###项目中用到的:

/**
 * 喜欢点赞操作
 * 返回 喜欢的数量
 */
public long like(int userId,int entityType,int entityId){
    String likeKey=RedisKeyUtil.getLikeKey(entityType,entityId);
    //把用户加到喜欢的likeKey的KV中,就是说这条咨询有多少人喜欢
    jedisAdapter.sadd(likeKey,String.valueOf(userId));
    //删除不喜欢中的userId
    String disLikeKey=RedisKeyUtil.getDisLikeKey(entityType,entityId);
    jedisAdapter.srem(disLikeKey,String.valueOf(userId));
    //在返回有多少人喜欢
    return jedisAdapter.scard(likeKey);
}

补充:redis中的sadd对应redis中的sadd

SADD KEY_NAME VALUE1..VALUEN

Redis Sadd 命令将一个或多个成员元素加入到集合中,==已经存在于集合的成员元素将被忽略==。

假如集合 key 不存在,则创建一个只包含添加的元素作成员的集合。

当集合 key 不是集合类型时,返回一个错误。

实例:

redis 127.0.0.1:6379> SADD myset "hello"
(integer) 1
redis 127.0.0.1:6379> SADD myset "foo"
(integer) 1
redis 127.0.0.1:6379> SADD myset "hello"
(integer) 0
redis 127.0.0.1:6379> SMEMBERS myset
1) "hello"
2) "foo"
SISMEMBER KEY VALUE 

==返回值==:

如果成员元素是集合的成员,返回 1 。 如果成员元素不是集合的成员,或 key 不存在,返回 0 。

实例:

redis 127.0.0.1:6379> SADD myset1 "hello"
(integer) 1
redis 127.0.0.1:6379> SISMEMBER myset1 "hello"
(integer) 1
redis 127.0.0.1:6379> SISMEMBER myset1 "world"
(integer) 0

###举例:

        // 集合,点赞用户群, 共同好友
        String likeKey1 = "newsLike1";
        String likeKey2 = "newsLike2";
        for (int i = 0; i < 10; ++i) {
            jedis.sadd(likeKey1, String.valueOf(i));
            jedis.sadd(likeKey2, String.valueOf(i * 2));
        }
        print(20, jedis.smembers(likeKey1));//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        print(21, jedis.smembers(likeKey2));//[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
        print(22, jedis.sunion(likeKey1, likeKey2));//取俩个set并集 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18]
        print(23, jedis.sdiff(likeKey1, likeKey2));//求不同 [1, 9, 3, 5, 7]
        print(24, jedis.sinter(likeKey1, likeKey2));//求交集 [0, 2, 4, 6, 8]
        print(25, jedis.sismember(likeKey1, "12"));//false
        print(26, jedis.sismember(likeKey2, "12"));//true
        jedis.srem(likeKey1, "5");//删除操作
        print(27, jedis.smembers(likeKey1));//[0, 1, 2, 3, 4, 6, 7, 8, 9]
        // 从1移动到2
        jedis.smove(likeKey2, likeKey1, "14");//将前一个集合中的指定元素移到后一个集合中
        print(28, jedis.smembers(likeKey1));//[0, 1, 2, 3, 4, 6, 7, 8, 9, 14]
        print(29, jedis.scard(likeKey1));//求集合中元素的总个数 10

##5.Sorted Set

常用命令: zadd,zrange,zrem,zcard等
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。

举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维 度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。

###举例:

        // 排序集合,有限队列,排行榜
        String rankKey = "rankKey";
        jedis.zadd(rankKey, 15, "Jim");
        jedis.zadd(rankKey, 60, "Ben");
        jedis.zadd(rankKey, 90, "Lee");
        jedis.zadd(rankKey, 75, "Lucy");
        jedis.zadd(rankKey, 80, "Mei");
        print(30, jedis.zcard(rankKey));//集合中元素的总个数 5
        print(31, jedis.zcount(rankKey, 61, 100));//指定范围内元素的个数 3
        // 改错卷了
        print(32, jedis.zscore(rankKey, "Lucy"));//75.0
        jedis.zincrby(rankKey, 2, "Lucy");//增加指定数字
        print(33, jedis.zscore(rankKey, "Lucy"));//77.0
        jedis.zincrby(rankKey, 2, "Luc");//增加指定数字,没有就创建
        print(34, jedis.zscore(rankKey, "Luc"));//2.0
        print(35, jedis.zcount(rankKey, 0, 100));//指定范围内元素的个数 5
        // 1-4 名 Luc
        print(36, jedis.zrange(rankKey, 0, 10));//在指定范围内根据value排key,默认升序 [Luc, Jim, Ben, Lucy, Mei, Lee]
        print(36, jedis.zrange(rankKey, 1, 3));//[Jim, Ben, Lucy],默认从头到尾数
        print(36, jedis.zrevrange(rankKey, 1, 3));//[Mei, Lucy, Ben] 从尾到头数
        for (Tuple tuple : jedis.zrangeByScoreWithScores(rankKey, "60", "100")) {
            print(37, tuple.getElement() + ":" + String.valueOf(tuple.getScore()));
        }

        print(38, jedis.zrank(rankKey, "Ben"));//从头到尾排第几 2
        print(39, jedis.zrevrank(rankKey, "Ben"));//从尾到头排第几 3

点赞流程分析:

@Controller
public class HomeController {
    @Autowired
    NewsService newsService;

    @Autowired
    UserService userService;
    @Autowired
    HostHolder hostHolder;
    @Autowired
    LikeService likeService;

    private List<ViewObject> getNews(int userId, int offset, int limit) {
        List<News> newsList = newsService.getLatestNews(userId, offset, limit);
        int localUserId = hostHolder.getUser() != null ? hostHolder.getUser().getId() : 0;
        List<ViewObject> vos = new ArrayList<>();
        for (News news : newsList) {
            ViewObject vo = new ViewObject();
            vo.set("news", news);
            vo.set("user", userService.getUser(news.getUserId()));
            if(localUserId!=0){
                //从这在redis中取值
                vo.set("like", likeService.getLikeStatus(localUserId, EntityType.ENTITY_NEWS, news.getId()));
            }else{
                vo.set("like",0);
            }
            vos.add(vo);
        }
        return vos;
    }
    @RequestMapping(path = {"/", "/index"}, method = {RequestMethod.GET, RequestMethod.POST})
    public String index(Model model,@RequestParam(value = "pop", defaultValue = "0") int pop) {
        model.addAttribute("vos", getNews(0, 0, 10));
        if (hostHolder.getUser() != null) {
            pop = 0;
        }
        model.addAttribute("pop", pop);
        return "home";
    }

}

从上述代码我们能知道:

项目首页,也就是home.html,通过Model对象接受到从后端传来的vos链表对象。取到的key对应的value是从redis中去的,这就是真正使用redis的原因,存储在内存中,取值速度快。


   转载规则


《项目亮点之赞踩功能》 xuxinghua 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
互联网协议入门 互联网协议入门
概述五层模型互联网的实现,分成好几层。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持。 用户接触到的,只是最上面的一层,根本没有感觉到下面的层。要理解互联网,必须从最下层开始,自下而上理解每一层的功能。 如何分层有不同的模型
下一篇 
项目亮点之异步队列 项目亮点之异步队列
项目中简单实现异步消息队列
  目录
I I