仿牛客论坛项目全面大总结

1.核心功能:

 

     - 发帖、评论、私信、转发;
     - 点赞、关注、通知、搜索;
     - 权限、统计、调度、监控;
2.核心技术:
     - Spring Boot、SSM
     - Redis、Kafka、ElasticSearch
     - Spring Security、Quatz、Caffeine
3.项目亮点:
     - 项目构建在Spring Boot+SSM框架之上,并统一的进行了状态管理、事务管理、异常处理;
     - 利用Redis实现了点赞和关注功能;
     - 利用Kafka实现了异步的站内通知;
     - 利用ElasticSearch实现了全文搜索功能,可准确匹配搜索结果,并高亮显示关键词;
     - 利用Caffeine+Redis实现了两级缓存,并优化了热门帖子的访问。
     - 利用Spring Security实现了权限控制,实现了多重角色、URL级别的权限管理;
     - 利用HyperLogLog、Bitmap分别实现了UV、DAU的统计功能,100万用户数据只需*M内存空间;
     - 利用Quartz实现了任务调度功能,并实现了定时计算帖子分数、定时清理垃圾文件等功能;
     - 利用Actuator对应用的Bean、缓存、日志、路径等多个维度进行了监控,并通过自定义的端点对数据库连接进行了监控。
实现了项目部署上线,日访问量到达200+(很欣慰了)
仿牛客论坛项目全面大总结_第1张图片

 开始总结

图中的下方三行是基石。
原图中带下划线的和带@的内容是重点内容。
算法:前缀树过滤敏感词。
数据结构:Redis的数据结构。Redis有许多种数据结构,每种数据结构适合解决什么样的问题,适合缓存什么样的数据。
Kafka:重点是生产者与消费者模式,只有你理解了,才能明白什么时候用哪些功能来解决什么问题。
ElasticSearch:重点是它的数据结构,以索引(表)方式来存。建议找文章自己学习ES的索引。
Caffeine:本地缓存,有一定局限性,把它和Redis一起回顾,因为它们都是缓存。
从运维角度来讲整个项目(就是分布式部署)
仿牛客论坛项目全面大总结_第2张图片
静态资源(图片)和动态资源(controller)是部署在两个服务器上的。比如:牛客网的是在域名 nowcoder.com 上,牛客网的图片是在 static.nowcoder.com 上,这两个是不同的域名,会对应到两个不同的服务器。
客户端找nginx请求动态资源,找CDN请求静态资源。
nginx:能对服务器做反向代理,如果你有多个服务器,它能对服务器做负载均衡。nginx会主从备份,即有两台服务器,一台处理客户端请求,一台只是备份。
CDN:把你的资源部署到各地的多个服务器上,用户访问网站时,会从最近的服务器加载资源。
有多台服务器,nginx会把客户端请求发送到某一台服务器,每台服务器中都有项目community和本地缓存。
数据库通常要做读写分离的,一台数据库复制读,一台数据库负责写。负责写的数据库在写入数据以后,会把数据同步到负责读取的数据库里。
为什么Redis、Kafka、ElasticSearch都是部署多个,但是数据库却只进行主从备份?因为数据库部署和它们一样做集群式部署的话的话,那么就要处理分布式事务,这个比较麻烦,所以我们一般尽量避免这种情况。而且,一般的业务也达不到要部署多个数据库,因为在访问数据库前,会先访问本地缓存,再访问Redis,最后都没有才访问数据库。
我们用的文件服务器是七牛云。

咋们就从核心功能处开始捋,一个一个功能点开始总结实现过程:

核心功能:

   发帖:

      点击发布帖子按钮,输入帖子标题内容等信息后发布,以JS异步请求的方式进行提交,首先判断该线程中有无用户信息(在登录成功是会将用户信息与本线程的id进行键值对存储,保证了多线程情况下安全)即判断有无登录,已登录则初始化帖子信息将其添加至DB中,并且触发发帖事件,采用kafka信息队列来完成,将帖子信息添加至ES中,通知再将其存入Redis中计算帖子的分值,用来在最热列表中展示出来:

添加帖子

// 发布帖子
    @RequestMapping(path = "/add",method = RequestMethod.POST)
    @ResponseBody
    public String addDiscussPost(String title,String content){
        User users = hostHolder.getUsers();
        if(users == null){
            return CommunityUtil.getJSONString(403,"你还没有登录!");
        }

        DiscussPost post=new DiscussPost();
        post.setUserId(users.getId());
        post.setTitle(title);
        post.setContent(content);
        post.setCreateTime(new Date());
        discussPostService.addDiscussPost(post);

        // 触发发帖事件  添加至es中
        Event event = new Event()
                .setTopic(TOPIC_PUBLISH)
                .setUserId(users.getId())
                .setEntityType(ENTITY_TYPE_POST)
                .setEntityId(post.getId());
        eventProducer.fireEvent(event);

        // 计算帖子分数
        String redisKey= RediskeyUtil.getPostScoreKey();
        redisTemplate.opsForSet().add(redisKey,post.getId());

        return CommunityUtil.getJSONString(0,"发布成功!");
    }
kafak消费者身份,ES同样添加数据
// 消费  发帖事件 es添加数据
    @KafkaListener(topics = {TOPIC_PUBLISH})
    public void handlePublishMessage(ConsumerRecord record){
        if(record == null && record.value() == null){
            logger.error("消息的内容为空!");
            return;
        }

        Event event = JSONObject.parseObject(record.value().toString(), Event.class);
        if(event == null){
            logger.error("消息格式错误!");
            return;
        }

        DiscussPost discussPost = 
        discussPostService.selectDiscussPostById(event.getEntityId());
        elasticsearchService.saveDiscussPost(discussPost);
    }

评论:

  评论这一块数据库表comment结构得先捋一下,entity_type字段(int型)1:表示此条评论是对帖子进行的一个评论,2:表示此条评论是对1级评论进行的2级评论;entityId字段(int型)如果entity_type字段为1则此字段存储的是帖子的id,如果entity_type字段为2则此字段存储的是对哪个1级评论进行的评论;targetId字段(int型)可为空,为空并且entityId为2则表示此评论没有哪个2级评论进行评论,不为空且entityId为2则表示此评论是对一个2级评论的回复评论。仿牛客论坛项目全面大总结_第3张图片

 填写完评论内容后点击回复按钮,以POST形式进行提交,附带帖子id、entity_type字段、entityId字段、entity_type字段信息;

entity_type字段为1、表示是对帖子进行的评论


		

entity_type字段为2、并且没有targetId字段信息,表示是对1级评论进行的评论

							
  • entity_type字段为2、并且有targetId字段信息,表示是对2级评论进行的评论

    提交以后完善comnent的信息,评论时间等等信息。触发评论事件kafak生产者,评论类系统通知,通知相关用户收到系统通知,某某在某处给你的评论进行回复。触发发帖事件 添加至es中。同时在Redis中更新帖子的分值。

    提交表单,添加评论

    // 添加评论
        @RequestMapping(path = "/add/{discussPostId}",method = RequestMethod.POST)
        public String addComment(@PathVariable("discussPostId") int discussPostId , Comment comment){
            comment.setUserId(hostHolder.getUsers().getId());
            comment.setStatus(0);
            comment.setCreateTime(new Date());
            commentService.addComment(comment);
    
            // 触发评论事件   系统通知  信息队列    评论类系统通知
            Event event = new Event()
                    .setTopic(TOPIC_COMMENT)                                          // 通知主题
                    .setUserId(hostHolder.getUsers().getId())                         // 发起用户id
                    .setEntityType(comment.getEntityType())                           // 实体类型
                    .setEntityId(comment.getEntityId())                               // 实体id
                    .setData("postId", discussPostId);                                // 帖子id
            if(comment.getEntityType() == ENTITY_TYPE_POST){
                DiscussPost discussPost = discussPostService.selectDiscussPostById(comment.getEntityId());
                event.setEntityUserId(discussPost.getUserId());                       // 接收用户id
            }else if(comment.getEntityType() == ENTITY_TYPE_COMMENT){
                Comment comment1 = commentService.findCommentById(comment.getEntityId());
                event.setEntityUserId(comment1.getUserId());                          // 接收用户id
            }
            eventProducer.fireEvent(event);              // 发送
    
            // 触发发帖事件  添加至es中
            if(comment.getEntityType() == ENTITY_TYPE_POST){
                event = new Event()
                        .setTopic(TOPIC_PUBLISH)
                        .setUserId(comment.getUserId())
                        .setEntityType(ENTITY_TYPE_POST)
                        .setEntityId(discussPostId);
                eventProducer.fireEvent(event);
            }
    
            // 计算帖子分数
            String redisKey= RediskeyUtil.getPostScoreKey();
            redisTemplate.opsForSet().add(redisKey,discussPostId );
    
            return "redirect:/discuss/detail/"+discussPostId;
        }

    kafka消费者,处理系统通知,包括点赞状态,评论通知,关注通知

    // 消费者 接收处理通知
        @KafkaListener(topics = {TOPIC_COMMENT,TOPIC_FOLLOW,TOPIC_LIKE})
        public void handleCommentMessage(ConsumerRecord record){
            if(record == null && record.value() == null){
                logger.error("消息的内容为空!");
                return;
            }
    
            Event event = JSONObject.parseObject(record.value().toString(), Event.class);
            if(event == null){
                logger.error("消息格式错误!");
                return;
            }
    
            // 发送站内通知
            Message message=new Message();
            message.setFromId(SYSTEM_USER_ID);                   // 系统通知
            message.setToId(event.getEntityUserId());            // 接收用户id
            message.setConversationId(event.getTopic());         // 通知类型   关注/点赞/回复
            message.setCreateTime(new Date());                   // 时间
    
            Map content=new HashMap<>();
            content.put("userId",event.getUserId());             // 发起者用户id
            content.put("entityType",event.getEntityType());     // 实体类型  关注/评论/回复
            content.put("entityId",event.getEntityId());         // 实体id
    
            if(!event.getData().isEmpty()){                      // 遍历添加其他数据
                for(Map.Entry entry : event.getData().entrySet()){
                    content.put(entry.getKey(), entry.getValue());
                }
            }
    
            message.setContent(JSONObject.toJSONString(content));   // 将对象转化为JSON格式存储
            messageService.addMessage(message);
        }

    私信、

    进入信息页面,点击发私信按钮,填写发送用户,以及私信内容。以JS异步方式提交,根据发送用户判断是否存在此用户,没有则返回目标用户不存在,有则完善私信message信息,接收者,发送者,发送时间等添加至数据库中

     // 发送私信
        @RequestMapping(path = "/letter/send",method = RequestMethod.POST)
        @ResponseBody
        public String sendLetter(String toName,String content){
            // 目标用户
            User target = userService.selectByName(toName);
            if(target == null){
                return CommunityUtil.getJSONString(1,"目标用户不存在!");
            }
    
            Message message=new Message();
            message.setFromId(hostHolder.getUsers().getId());  // 发送者
            message.setToId(target.getId());                   // 接收者
            if(message.getFromId() < message.getToId()){
                message.setConversationId(message.getFromId()+"_"+message.getToId());
            }else{
                message.setConversationId(message.getToId()+"_"+message.getFromId());
            }
            message.setContent(content);
            message.setCreateTime(new Date());
            messageService.addMessage(message);
    
            return CommunityUtil.getJSONString(0);
        }

    转发、

    说白了就是将html界面转化为Png图片,并且上传至七牛云上进行保存,返回访问图片的链接,

    kafka生成事件

    // 分享网页 转化成长图
        @RequestMapping(path = "/share",method = RequestMethod.GET)
        @ResponseBody
        public String share(String htmlUrl){
            // 文件名
            String fileName = CommunityUtil.generateUUID();
    
            // 异步生成长图
            Event event=new Event()
                    .setTopic(TOPIC_SHARE)
                    .setData("htmlUrl",htmlUrl)
                    .setData("fileName",fileName)
                    .setData("suffix",".png");
            eventProducer.fireEvent(event);
    
            // 返回访问路劲
            Map map=new HashMap<>();
            //map.put("shareUrl",domain + contextPath + "/share/image/" + fileName);
            map.put("shareUrl", shareBucketUrl + "/" + fileName);	//就改了下这里
    
            return CommunityUtil.getJSONString(0,null,map);
        }
     消费者处理图片,生产图片以后将图片上传是七牛云
    // 消费分享事件
        @KafkaListener(topics = TOPIC_SHARE)
        public void handleShareMessage(ConsumerRecord record){
            if(record == null && record.value() == null){
                logger.error("消息的内容为空!");
                return;
            }
    
            Event event = JSONObject.parseObject(record.value().toString(), Event.class);
            if(event == null){
                logger.error("消息格式错误!");
                return;
            }
    
            String htmlUrl = (String) event.getData().get("htmlUrl");
            String fileName = (String) event.getData().get("fileName");
            String suffix = (String) event.getData().get("suffix");
    
            String cmd = wkImageCommand + " --quality 75 " + htmlUrl + " " + wkImageStorage + "/" + fileName + suffix;
    
            try {
                Runtime.getRuntime().exec(cmd);
    
                logger.info("生成长图成功: " + cmd);
            } catch (IOException e) {
                logger.info("生成长图失败: " + e.getMessage());
            }
    
            
    
            // 启用定时器,监视该图片,一旦生成了,则上传至七牛云.
            UploadTask task = new UploadTask(fileName, suffix);
            Future future = taskScheduler.scheduleAtFixedRate(task, 500);//每隔半秒钟执行一遍。Future里封装了任务的状态,还可以用来停止定时器。
            task.setFuture(future);//停止定时器应该在run()方法里停止,在达成某个条件之后,于是,要把返回的future传入UploadTask类的对象task里
        }
    
    监视图片的上传
     /*
        以下情况导致上传失败,但上传失败了,不能够线程就不停止了,线程不停止的话,时间久了,会有很多线程因为这种原因不停止,服务器就被撑爆了。
        所以要考虑到这些情况,即使出现这些极端情况,也一定要停掉定时器。于是要增加两个属性(开始时间,上传次数):
        1.图片一直无法生成到本地
        2.网络不好无法上传图片/七牛云的服务器挂了,无法上传图片
         */
        class UploadTask implements Runnable {
    
            // 文件名称
            private String fileName;
            // 文件后缀
            private String suffix;
            // 启动任务的返回值
            private Future future;
            // 开始时间
            private long startTime;
            // 上传次数
            private int uploadTimes;
    
            public UploadTask(String fileName, String suffix) {
                this.fileName = fileName;
                this.suffix = suffix;
                this.startTime = System.currentTimeMillis();
            }
    
            public void setFuture(Future future) {
                this.future = future;
            }
    
            @Override
            public void run() {
                // 生成失败
                if (System.currentTimeMillis() - startTime > 60000) {//30秒,还没上传成功,大概率是生成图片失败
                    logger.error("执行时间过长,终止任务:" + fileName);
                    future.cancel(true);//这行代码用来终止任务.
                    return;
                }
                // 上传失败
                if (uploadTimes >= 3) {//上传3次,还不成功,大概率是网络不好/服务器挂了。文件不存在时不进行上传操作
                    logger.error("上传次数过多,终止任务:" + fileName);
                    future.cancel(true);
                    return;
                }
    
                String path = wkImageStorage + "/" + fileName + suffix;
                File file = new File(path);
                if (file.exists()) {
                    logger.info(String.format("开始第%d次上传[%s].", ++uploadTimes, fileName));
                    // 设置响应信息
                    StringMap policy = new StringMap();
                    policy.put("returnBody", CommunityUtil.getJSONString(0));
                    // 生成上传凭证
                    Auth auth = Auth.create(accessKey, secretKey);
                    String uploadToken = auth.uploadToken(shareBucketName, fileName, 3600, policy);//凭证uploadToken过期时间,3600秒,1小时
                    // 指定上传机房
                    UploadManager manager = new UploadManager(new Configuration(Region.region1()));//zone1()是上传到华北地区了。即setting.js里的指定上传到的服务器域名“url: "http://upload-z1.qiniup.com",”
                    try {
                        // 开始上传图片
                        Response response = manager.put(
                                path, fileName, uploadToken, null, "image/" + suffix, false);//第三个参数是上传文件类型,变量mime,值“image/.png”
                        // 处理响应结果
                        JSONObject json = JSONObject.parseObject(response.bodyString());//把返回的JSON格式字符串转为JSON对象
                        if (json == null || json.get("code") == null || !json.get("code").toString().equals("0")) {
                            logger.info(String.format("第%d次上传失败[%s].", uploadTimes, fileName));
                        } else {
                            logger.info(String.format("第%d次上传成功[%s].", uploadTimes, fileName));
                            future.cancel(true);
                        }
                    } catch (QiniuException e) {
                        logger.info(String.format("第%d次上传失败[%s].", uploadTimes, fileName));
                    }
                } else {//如果文件不存在就什么也不做,过一会这个定时任务又会被调一次,再来上传
                    logger.info("等待图片生成[" + fileName + "].");
                }
            }
        }

     - 点赞

    进入帖子的详情页面,可以对此帖子进行点赞,同时还可以对评论、对评论的2级评论进行点赞,以JS方式提交,返回点赞后状态,因此在不同的地方进行点赞所携带的参数也不一样

    仿牛客论坛项目全面大总结_第4张图片

     对帖子进行的点赞,携带的参数是entityType 帖子类型(回复/评论等...)、entityId 评论id、   entityUserId 收到赞的用户id、postId帖子的id

  • nice 11
  •  对1级评论进行的点赞,携带的参数是entityType 帖子类型(回复/评论等...)、entityId 评论id、   entityUserId 收到赞的用户id、postId帖子的id
      对2级评论进行的点赞,携带的参数是entityType 帖子类型(回复/评论等...)、entityId 评论id、   entityUserId 收到赞的用户id、postId帖子的id

    处理点赞的响应,由于用户量大点赞时会非常的频繁操作考虑到数据库的性能原因将点赞的各类信息存入Redis中,通过写好的工具类把所已知的点赞信息封装成唯一的字符串,方便进行增删操作,例如某个实体的赞:like:user:entityType:entityId 将此作为Redis中的key,value则为执行点赞的用户id,为了方便进行统计单个用户所获得的赞数量,也在Resis中储存,例如某个用户收到的赞like:user:userId 将此作为Redis中的key,value则为所获得的点赞数。生成唯一的key后首先要判断是否已经赞过,再次点击则视为取消点赞。

    // 点赞  写入redis 同时记录每个用户收到的赞
        public void like(int userId,int entityType,int entityId,int entityUserId){                             //entityType 帖子类型(回复/评论等...)   entityId 评论id   entityUserId 收到赞的用户id
            // 事务控制
            redisTemplate.execute(new SessionCallback() {
                @Override
                public Object execute(RedisOperations operations) throws DataAccessException {
                    String entityLikeKey = RediskeyUtil.getEntityLikeKey(entityType,entityId);
                    String userLikeKey = RediskeyUtil.getUserLikeKey(entityUserId);
    
                    // 判断是否此用户对此以及赞过
                    Boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);
    
                    operations.multi();
    
                    if(isMember){
                        // 赞过 则 取消赞
                        operations.opsForSet().remove(entityLikeKey,userId);
    
                        // -- 用户收到的赞
                        // like:user:userId -> int   详情下方示例1  redisTemplate.opsForValue().set( like:user:userId , i);
                        operations.opsForValue().decrement(userLikeKey);                               // --i
                    }else{
                        // 没赞 则 赞
                        // like:entity:entityType:entityId -> (userid1,userid2,userid3,.....)   详情下方示例2  redisTemplate.opsForSet().add( like:entity:entityType:entityId  , "101", "102", "103", "104", "105",.....);
                        operations.opsForSet().add(entityLikeKey,userId);
    
                        // ++ 用户收到的赞
                        // like:user:userId -> int  详情下方示例1   redisTemplate.opsForValue().set( like:user:userId , i);
                        operations.opsForValue().increment(userLikeKey);                               // ++i
                    }
                    return operations.exec();
                }
            });
        }

    处理此处的点赞数量,根据例如某个实体的赞:like:user:entityType:entityId 这个key可以轻松在Redis当中此处value的个数,即为此处的点赞数量

    // 查询某实体点赞的数量   此帖已收到的赞
        public long findEntityLikeCount(int entityType,int entityId){
            String entityLikeKey=RediskeyUtil.getEntityLikeKey(entityType,entityId);
            return redisTemplate.opsForSet().size(entityLikeKey);
        }

    处理此处相当于现已登录的用户而言是否已经赞过,赞过则显示已赞,再次点击视为取消点赞,没有赞过则显示赞,点击视为点赞,返回值为int型,可拓展更多的功能,例如踩

    // 查询某人对某实体的点赞状态   显示 已赞/赞 1/0   (int 类型数据  可拓展 踩 等功能)
        public int findEntityLikeStatus(int userId,int entityType,int entityId){
            String entityLikeKey=RediskeyUtil.getEntityLikeKey(entityType,entityId);
            return redisTemplate.opsForSet().isMember(entityLikeKey,userId) ? 1 : 0;
        }
    生产者:触发点赞事件,发送系统通知,点赞类型的通知,用时为了持久化存储写入数据库当中,此为异步处理不影响性能,同时在Redis中更新帖子的分值。
    @RequestMapping(path = "/like",method = RequestMethod.POST)
        @ResponseBody
        public String like(int entityType,int entityId,int entityUserId,int postId){             //entityType 帖子类型(回复/评论等...)   entityId 评论id   entityUserId 收到赞的用户id
            User user = hostHolder.getUsers();
    
            // 点赞
            likeService.like(user.getId(),entityType,entityId,entityUserId);
            // 点赞数量
            long likeCount = likeService.findEntityLikeCount(entityType, entityId);
            // 点赞状态
            int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
            // 返回结果
            Map map=new HashMap();
            map.put("likeCount",likeCount);
            map.put("likeStatus",likeStatus);
    
            // 触发点赞事件  发送系统通知  点赞类型通知
            if(likeStatus == 1){
                Event event=new Event()
                        .setTopic(TOPIC_LIKE)                            // 通知主题
                        .setUserId(hostHolder.getUsers().getId())        // 发起用户id
                        .setEntityType(entityType)                       // 实体类型
                        .setEntityId(entityId)                           // 实体id
                        .setEntityUserId(entityUserId)                   // 接收用户id
                        .setData("postId",postId);                       // 点赞处id
                eventProducer.fireEvent(event);                          // 发送通知
            }
    
            // 计算帖子分数
            if(entityType == ENTITY_TYPE_POST){
                String redisKey= RediskeyUtil.getPostScoreKey();
                redisTemplate.opsForSet().add(redisKey,postId);
            }
    
            // 以JSON方式返回
            return CommunityUtil.getJSONString(0,null,map);
        }

    关注

    进入其他用户的个人主页,点击关注按钮可对其进行关注,同时还可取消对其的关注,以JS方式提交,返回点赞后状态,携带entityType 类型、entityId 关注的用户id。同时不加重数据库的承载进行Redis的存储,生成唯一的key:followee:userId:entityType value为关注用户的id,还要将生成此用户是关注用户的粉丝,生成唯一的key:followee:entityType:entityId value为此用户的id。这样在查询我的关注或我的粉丝的十分的方便

    // g关注用户
        public void follow(int userId,int entityType,int entityId){                                        // userId 该用户id        entityType 类型 3       entityId 关注的用户id
            redisTemplate.execute(new SessionCallback() {
                @Override
                public Object execute(RedisOperations operations) throws DataAccessException {
                    String followeeKey = RediskeyUtil.getFolloweekey(userId,entityType);
                    String followerKey = RediskeyUtil.getFollowerkey(entityType,entityId);
    
                    operations.multi();
    
                    // 我的关注
                    operations.opsForZSet().add(followeeKey,entityId,System.currentTimeMillis());
                    // redisTemplate.opsForZSet().add(  followee:userId:entityType , entityId, System.currentTimeMillis());          followee:userId:entityType:本用户   entityId:关注的用户id
    
                    // 他的粉丝
                    operations.opsForZSet().add(followerKey,userId,System.currentTimeMillis());
                    // redisTemplate.opsForZSet().add(  followee:entityType:entityId , userId, System.currentTimeMillis());          followee:entityType:entityId:本用户   userId:粉丝的用户id
    
                    return operations.exec();
                }
            });
        }

    触发关注类型的系统通知,通知其用户被此用户进行了关注

    // 关注用户
        @RequestMapping(path = "/follow",method = RequestMethod.POST)
        @ResponseBody
        public String follow(int entityType,int entityId){                              // entityType 类型 3       entityId 关注的用户id
            User user = hostHolder.getUsers();
    
            followService.follow(user.getId(),entityType,entityId);
    
            // 触发关注事件 系统通知 关注类型通知
            Event event = new Event()
                    .setTopic(TOPIC_FOLLOW)
                    .setUserId(user.getId())
                    .setEntityType(entityType)
                    .setEntityId(entityId)
                    .setEntityUserId(entityId);
            eventProducer.fireEvent(event);
    
            return CommunityUtil.getJSONString(0,"已关注");
        }

    通知

    先解析一波数据库在存储通知类型数据的时候的表结构,在message表中,to_id为所属用户的id,conversation_id为通知的类型(点赞/关注/评论),content存储的此条通知的详情

    假如所属用户id统一为175

    例如点赞类,conversation_id为like content为:{"entityType":2,"entityId":250,"postId":295,"userId":111} :entityType表示此点赞发生在评论处,entityId表示此点赞发生在评论id为250处,postId表示此点赞发生在帖子id为295处,userId表示对其点赞的是用户id111,根据以上信息可以精确定位到发生点赞的地点;

    例如关注类,conversation_id为follow content为:{"entityType":3,"entityId":175,"userId":171}:entityType表示是关注类型的通知,entityId表示是关注的用户id为175,userId表示用户171对其进行的关注;

    例如评论类,conversation_id为comment content为:{"entityType":1,"entityId":295,"postId":295,"userId":171}:entityType表示是1级评论的通知,在帖子295处进行的1级评论,评论所属用户171。

     通过以三种通知类型以及用户id从数据库当中读取通知,再以HashMap解析content存储的数据

     // 系统通知
        @RequestMapping(path = "/notice/list" , method = RequestMethod.GET)
        public String getNoticeList(Model model){
            User user = hostHolder.getUsers();
    
            //查询评论类通知
            Message message = messageService.findLatestNotice(user.getId(), TOPIC_COMMENT);
            Map messageVo=new HashMap();
            if(message != null){
                messageVo.put("message",message);
    
                // 获取JSON格式数据
                String content = HtmlUtils.htmlUnescape(message.getContent());
                Map data = JSONObject.parseObject(content,HashMap.class);
    
                // 获取详情信息
                messageVo.put("user",userService.selectById((Integer) data.get("userId")));
                messageVo.put("entityType",data.get("entityType"));
                messageVo.put("entityId",data.get("entityId"));
                messageVo.put("postId",data.get("postId"));
    
                // 主题所包含的数量
                int count = messageService.findNoticeCount(user.getId(),TOPIC_COMMENT);
                messageVo.put("count",count);
    
                // 主题未读的通知数量
                int unread = messageService.findNoticeUnreadCount(user.getId(),TOPIC_COMMENT);
                messageVo.put("unread",unread);
                model.addAttribute("commentNotice",messageVo);
            }
    
            //查询点赞类通知
            message = messageService.findLatestNotice(user.getId(), TOPIC_LIKE);
            messageVo=new HashMap();
            if(message != null){
                messageVo.put("message",message);
    
                // 获取JSON格式数据
                String content = HtmlUtils.htmlUnescape(message.getContent());
                Map data = JSONObject.parseObject(content,HashMap.class);
    
                // 获取详情信息
                messageVo.put("user",userService.selectById((Integer) data.get("userId")));
                messageVo.put("entityType",data.get("entityType"));
                messageVo.put("entityId",data.get("entityId"));
                messageVo.put("postId",data.get("postId"));
    
                // 主题所包含的数量
                int count = messageService.findNoticeCount(user.getId(),TOPIC_LIKE);
                messageVo.put("count",count);
    
                // 主题未读的通知数量
                int unread = messageService.findNoticeUnreadCount(user.getId(),TOPIC_LIKE);
                messageVo.put("unread",unread);
                model.addAttribute("likeNotice",messageVo);
            }
    
            //查询关注类通知
            message = messageService.findLatestNotice(user.getId(), TOPIC_FOLLOW);
            messageVo=new HashMap();
            if(message != null){
                messageVo.put("message",message);
    
                // 获取JSON格式数据
                String content = HtmlUtils.htmlUnescape(message.getContent());
                Map data = JSONObject.parseObject(content,HashMap.class);
    
                // 获取详情信息
                messageVo.put("user",userService.selectById((Integer) data.get("userId")));
                messageVo.put("entityType",data.get("entityType"));
                messageVo.put("entityId",data.get("entityId"));
    
                // 主题所包含的数量
                int count = messageService.findNoticeCount(user.getId(),TOPIC_FOLLOW);
                messageVo.put("count",count);
    
                // 主题未读的通知数量
                int unread = messageService.findNoticeUnreadCount(user.getId(),TOPIC_FOLLOW);
                messageVo.put("unread",unread);
                model.addAttribute("followNotice",messageVo);
            }
    
            // 查询未读消息数量
            int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
            model.addAttribute("letterUnreadCount", letterUnreadCount);
            int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null);
            model.addAttribute("noticeUnreadCount", noticeUnreadCount);
    
            return "site/notice";
         }

    你可能感兴趣的:(笔记,java,论坛类项目,java,redis,分布式)