仿牛客网社区首页

总体介绍

项目功能分为注册登录、然后用户登录之后可以发布帖子,查看帖子,点赞,回帖,对回帖进行评论和回复,还能通过点击帖子发布者头像关注发布者

注册功能

注册需要账号、密码、确认密码、邮箱,首先service会判断输入是否为空,如果不为空进入下一步,分别通过账号、邮箱查找数据,如果数据不为空提示对应已注册信息。如果前面条件都不满足,进入下一步,创建一个user对象封装好对应信息,同时设置一个随机头像,将数据插入数据库,此时用户还不能使用,需要通过邮件激活,user对象的状态才会发生改变,插入数据之后,就会给邮箱发送邮件

链接有激活地址,这个地址拼装的是user的Id和一个激活码,点击链接会访问url映射的方法,并将这两个数据作为参数传入到方法中,发邮件之前activation_code已经封装在user对象传入数据库中,在激活方法中会有三种情况激活成功、已激活不用重复激活、激活码不正确请重新激活,激活成功后账户就能够使用此时user.status为1。需要注意的是密码在传入后端的时候会先拼接一个salt字段,salt字段是随机的5个字符构成的字符串,拼接之后才会进行md5加密存入到数据库中,同时也将salt字符串传入数据库中

登录

这部分功能除了判空以外,登录上的有四种情况,账号不正确,密码不正确,账号未激活,验证码不正确。登录成功之后会创建登录凭证对象,登录凭证有userid,凭证码,status(状态是否有效),过期时间,登录凭证主要用来记录用户的登录操作,留存一个记录,然后将登录凭证存入cookie中,每次请求拦截器都会从cookie中获取ticket,如果ticket不为空,状态有效,当前时间小于过期时间,就会通过ticket中的userid信息获取user对象,存入threadlocal本地线程中,目的就是在本次请求中持有用户,访问完地址映射的方法之后,然后在posthandle方法中,获取本地线程存放的user对象,放入modelandview中返回给客户端,在模板渲染完之后清除threadlocal中的user对象

目前的功能是,用户登录之后可以发布帖子,查看帖子,点赞,回复帖子,还可以通过点击头像关注帖子的发布者


登录之后的功能

发布帖子,点赞,详情帖子查看,回帖,回复,回复帖子下的回复,通过点击发布者头像关注发布者

帖子详情查看

discusspost

283

154

我要发布标题

我要发布正文

comment

id

userid

type

entityid

targetid

238

154

1

283

0

回帖正文

239

154

2

238

0

回帖下的回复

240

154

2

238

154

回帖下的回复的回复

以上是帖子详情页面,一个帖子,一个回帖,回帖下的回复,回帖下回复的回复,数据库记录

后端代码

  @RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
    public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
        // 帖子
        DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
        model.addAttribute("post", post);
        // 作者
        User user = userService.findUserById(post.getUserId());
        model.addAttribute("user", user);
        // 点赞数量
        long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, discussPostId);
        model.addAttribute("likeCount", likeCount);
        // 点赞状态
        int likeStatus = hostHolder.getUser() == null ? 0 :
                likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_POST, discussPostId);
        model.addAttribute("likeStatus", likeStatus);

        // 评论分页信息
        page.setLimit(5);
        page.setPath("/discuss/detail/" + discussPostId);
        page.setRows(post.getCommentCount());

        // 评论: 给帖子的评论
        // 回复: 给评论的评论
        // 评论列表,回帖
        List<Comment> commentList = commentService.findCommentsByEntity(
                ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
        // 评论VO列表
        List<Map<String, Object>> commentVoList = new ArrayList<>();
        if (commentList != null) {
            for (Comment comment : commentList) {
                // 评论VO
                Map<String, Object> commentVo = new HashMap<>();
                // 评论
                commentVo.put("comment", comment);
                // 作者
                commentVo.put("user", userService.findUserById(comment.getUserId()));
                // 点赞数量
                likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, comment.getId());
                commentVo.put("likeCount", likeCount);
                // 点赞状态
                likeStatus = hostHolder.getUser() == null ? 0 :
                        likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, comment.getId());
                commentVo.put("likeStatus", likeStatus);

                // 回复列表
                List<Comment> replyList = commentService.findCommentsByEntity(
                        ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);
                // 回复VO列表
                List<Map<String, Object>> replyVoList = new ArrayList<>();
                if (replyList != null) {//replyList是回帖下的回复
                    for (Comment reply : replyList) {
                        Map<String, Object> replyVo = new HashMap<>();
                        // 回复
                        replyVo.put("reply", reply);
                        // 作者
                        replyVo.put("user", userService.findUserById(reply.getUserId()));
                        // 回复目标
                        User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());//如果是回复的回复,则targetid是回复的回复记录id,如果没有则只是回帖的回复
                        replyVo.put("target", target);
                        // 点赞数量
                        likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, reply.getId());
                        replyVo.put("likeCount", likeCount);
                        // 点赞状态
                        likeStatus = hostHolder.getUser() == null ? 0 :
                                likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, reply.getId());
                        replyVo.put("likeStatus", likeStatus);

                        replyVoList.add(replyVo);
                    }
                }
                commentVo.put("replys", replyVoList);

                // 回复数量
                int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
                commentVo.put("replyCount", replyCount);

                commentVoList.add(commentVo);
            }
        }

        model.addAttribute("comments", commentVoList);

        return "/site/discuss-detail";
    }

当前帖子的内容通过discusspost对象储存,再通过discusspost的id找到comment中entity_id为discusspostid的记录封装成commentList,当前只找到一条就是commentid为238的数据,再通过遍历commentList,此时取到list中唯一保存的comment对象就是238,通过238再找到entity_id为238的comment此时找到两条一个commentid为239一个240,两条记录封装成replyList,再遍历replyList,取每一个回复comment对象进行加工,加工成replyvolist,然后存入commentvolist,注意遍历的时候遍历的是commentlist,

从commentlist(调用dao层方法返回的数据集合)遍历出的每一个comment对象会和一个replyvolist,作者,点赞数量,点赞状态,replycount封装成一个commentvo map集合存入commentvolist中

replylist(调用dao层返回的回复数据集合),遍历replylist集合,每一个list集合中存放的是回复comment对象,再将这些回复comment和其作者、回复目标(有则存入target user 对象,无则null,前端会通过这个判断

                                        <span th:if="${rvo.target==null}">
                                            <b class="text-info" th:text="${rvo.user.username}">寒江雪</b>:&nbsp;&nbsp;
                                        </span>
                                        <span th:if="${rvo.target!=null}">
                                            <i class="text-info" th:text="${rvo.user.username}">Sissi</i> 回复
                                            <b class="text-info" th:text="${rvo.target.username}">寒江雪</b>:&nbsp;&nbsp;
                                        </span>

)、点赞数量、点赞状态封装成一个replyvo map集合存入replyvolist,replyvolist集合最后会放到commentvolist中整体返回给前端

点赞

点赞只能在帖子详情页面进行点赞,可以对帖子点赞,回帖点赞,回复点赞,回复的回复点赞

帖子点赞前端

回帖点赞前端

回复点赞前端

回复的回复

redis中存放的key有两种类型(红色部分为固定部分)

  1. key like:entity:entitiyType:entitiyId value 存放的是userId

  1. key like:user:userId value 存放的是int数据

每一回点击点赞标签,在后端就会判断帖子key中是否包含当前登录的userid,有则代表此时是需要取消点赞,删掉帖子key中包含的当前登录的userid信息,同时,这个帖子的发布者userid的key中int数据自减一,代表帖子发布者获得的赞数量减一。如果是进行点赞操作,业务逻辑与取消点赞业务逻辑相反。

点赞状态查询

当前用户查看点赞状态,只有两种,一个就是已点赞,一个就是未点赞

当 当前用户进入详情页面,后端也会判断,在redis中通过当前查看的帖子类型、id构造的key中通过ismember方法查看value中否有当前登录的userid,如果有就是已点赞状态,没有就是无点赞

其他点赞信息
    //查询某实体点赞的数量
    public long findEntityLikeCount(int entityType,int entityId){
        String entityLikeKey=RedisKeyUtil.getEntityLikeKey(entityType,entityId);
        return redisTemplate.opsForSet().size(entityLikeKey);
    }
    // 查询某人对某实体的点赞状态
    public int findEntityLikeStatus(int userId, int entityType, int entityId) {
        String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
        return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;//isMember返回的是true或false
    }
    //查询某用户获得的赞
    public int findUserLikeCount(int userId){
        String userLikeKey=RedisKeyUtil.getUserLikeKey(userId);
        Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey);
        return count==null?0:count.intValue();//intValue是拆箱操作
    }

发送系统通知

评论、点赞、关注/取消关注发布通知。在业务处理的时候是作为事件(在kafka中是当作消息来处理)进行处理,首先要创建事件对象,当触发事件的时候将事件相关的消息拼接进去,最终会把事件中的数据存进数据库中,消息是以json字符串格式进行数据发送,在监听方法中将json数据取出转成event对象,取出相应信息,再通过这些信息通过主题类型找出另外所需的数据,封装成message对象,存入message表中。

// 触发评论事件
        Event event = new Event()
                .setTopic(TOPIC_COMMENT)
                .setUserId(hostHolder.getUser().getId())
                .setEntityType(comment.getEntityType())
                .setEntityId(comment.getEntityId())
                .setData("postId", discussPostId); //存放的是
        if (comment.getEntityType() == ENTITY_TYPE_POST) {
            DiscussPost target = discussPostService.findDiscussPostById(comment.getEntityId());
            event.setEntityUserId(target.getUserId());
        } else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) {
            Comment target = commentService.findCommentById(comment.getEntityId());
            event.setEntityUserId(target.getUserId());
        }
        eventProducer.fireEvent(event);
    @KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
    public void handleCommentMessage(ConsumerRecord record) {
        if (record == null || record.value() == null) {
            logger.error("消息的内容为空!");
            return;
        }

        Event event = JSONObject.parseObject(record.value().toString(), Event.class);//将json字符串转换成指定的实体类对象
        if (event == null) {
            logger.error("消息格式错误!");
            return;
        }

        // 发送站内通知
        Message message = new Message();
        message.setFromId(SYSTEM_USER_ID);
        message.setToId(event.getEntityUserId());
        message.setConversationId(event.getTopic());
        message.setCreateTime(new Date());

        Map<String, Object> content = new HashMap<>();
        content.put("userId", event.getUserId());//前端还需要拼成一个字符串用户xx评论了xx这些需要以下三个信息,所以需要存起来
        content.put("entityType", event.getEntityType());
        content.put("entityId", event.getEntityId());

        //event的data属性是map类型,有的主题会往data中存数据,之后将data中map的数据遍历赋值给content
        if (!event.getData().isEmpty()) {
            for (Map.Entry<String, Object> entry : event.getData().entrySet()) {//遍历map的其中一个方式
                content.put(entry.getKey(), entry.getValue());
            }
        }
        message.setContent(JSONObject.toJSONString(content));
        messageService.addMessage(message);
    }

关注type是3,对帖子点赞是1,对评论点赞是2

message表结构如下,其中status代表消息是否已读

from_id

to_id

conversation_id

content

status

create_time

365

1

154

comment

{&quot;entityType&quot;:2,&quot;entityId&quot;:238,&quot;postId&quot;:283,&quot;userId&quot;:154}

0

2023-03-14 20:11:00.0

364

1

154

comment

{&quot;entityType&quot;:2,&quot;entityId&quot;:238,&quot;postId&quot;:283,&quot;userId&quot;:154}

0

2023-03-14 20:10:25.0

363

1

154

comment

{&quot;entityType&quot;:1,&quot;entityId&quot;:283,&quot;postId&quot;:283,&quot;userId&quot;:154}

0

2023-03-14 20:09:08.0

kafka

broker:就是kafka的一个服务器

zookeeper:独立的软件不是kafka的东西,用来管理集群

topic:主题,消息队列一般有两种实现方式,一种是点对点,每个数据只会被一个消费者消费就是点对点,还有一种就是发布订阅的方式,可以有很多消费者订阅这个位置,然后读取消息,一个数据可以被多个消费者同时读到,或者先后读到

partition:分区,对主题进行分区,为了增加并发能力

offset:这消息在分区内存放的索引位置,序列

replica:副本,对数据做备份,对分区备份多份数据,分为leader副本和follower副本

通过spring提供的一个kafkatemplate类发送消息,生产者是主动调的,消费者是被动调的

kafkaListener:kafka的监听器,可以监听一个或多个主题,将消息封装进consumerrecord对象中

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值