• 【牛客讨论区】第五章:kafka


    1. 阻塞队列

    BlockingQueue

    • 解决线程通信的问题。
    • 阻塞方法:put、take。

    生产者消费者模式

    • 生产者:产生数据的线程。
    • 消费者:使用数据的线程。

    实现类

    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • PriorityBlockingQueue、SynchronousQueue、DelayQueue等

    在这里插入图片描述


    test 目录下新建测试类

    package com.nowcoder.community;
    
    import java.util.Random;
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;
    
    public class BlockingQueueTests {
    
        public static void main(String[] args) {
            BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
            new Thread(new Producer(queue)).start();
            new Thread(new Consumer(queue)).start();
            new Thread(new Consumer(queue)).start();
            new Thread(new Consumer(queue)).start();
        }
    }
    class Producer implements Runnable{
    
        private BlockingQueue<Integer> queue;
    
        public Producer(BlockingQueue<Integer> queue) {
            this.queue = queue;
        }
    
        @Override
        public void run() {
            try {
                for (int i = 0; i < 100; i++) {
                    queue.put(i);
                    System.out.println(Thread.currentThread().getName() + "生产:" + queue.size());
                    Thread.sleep(20);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    class Consumer implements Runnable {
    
        private BlockingQueue<Integer> queue;
    
        public Consumer(BlockingQueue<Integer> queue) {
            this.queue = queue;
        }
    
        @Override
        public void run() {
            try {
                while (true) {
                    queue.take();
                    System.out.println(Thread.currentThread().getName() + "消费了:" + queue.size());
                    Thread.sleep(new Random().nextInt(1000));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    运行 main 方法查看结果

    2. Kafka入门

    Kafka简介

    • Kafka是一个分布式的流媒体平台。
    • 应用:消息系统、日志收集、用户行为追踪、流式处理。

    Kafka特点

    • 高吞吐量、消息持久化、高可靠性、高扩展性。

    Kafka术语

    • Broker、Zookeeper
    • Topic、Partition、Offset
    • Leader Replica 、Follower Replica

    http://kafka.apache.org


    小知识:对硬盘的顺序读取,速度是很快的,甚至要快于对内存的随机读取,这也是 kafka 既能持久化又能高性能的原因

    【配置】
    将素材中的 kafka_2.12-2.2.0.tgz 解压到某个目录即可
    配置一下 E:\kafka_2.12-2.2.0\config 目录下的 zookeeper.properties


    在这里插入图片描述

    dataDir=d:/work/data/zookeeper
    
    • 1

    配置 server.properties
    60行

    log.dirs=d:/work/data/kafka-logs
    
    • 1

    【启动】
    打开一个 cmd ,启动 zookeeper

    C:\Users\15642>e:
    
    E:\>cd e:/kafka_2.12-2.2.0
    
    e:\kafka_2.12-2.2.0>bin\windows\zookeeper-server-start.bat config\zookeeper.properties
    
    • 1
    • 2
    • 3
    • 4
    • 5

    再打开一个 cmd,启动 kafka

    C:\Users\15642>e:
    
    E:\>cd e:/kafka_2.12-2.2.0
    
    e:\kafka_2.12-2.2.0>bin\windows\kafka-server-start.bat config\server.properties
    
    • 1
    • 2
    • 3
    • 4
    • 5

    自动创建了对应的文件夹

    在这里插入图片描述

    再打开一个 cmd
    使用 kafka 的生产者,创建主题,发送消息

    C:\Users\15642>e:
    
    E:\>cd e:\kafka_2.12-2.2.0\bin\windows
    
    e:\kafka_2.12-2.2.0\bin\windows>kafka-topics.bat --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test
    
    e:\kafka_2.12-2.2.0\bin\windows>kafka-topics.bat --list --bootstrap-server localhost:9092
    test
    
    e:\kafka_2.12-2.2.0\bin\windows>kafka-console-producer.bat --broker-list localhost:9092 --topic test
    >hello
    >word
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    再打开一个 cmd
    使用 kafka 的消费者,读取 test 主题下的消息

    C:\Users\15642>e:
    
    E:\>cd kafka_2.12-2.2.0\bin\windows
    
    E:\kafka_2.12-2.2.0\bin\windows>kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test --from-beginning
    hello
    word
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    关于Kafka使用的重要提示

    现象:在windows的命令行里启动kafka之后,当关闭命令行窗口时,就会强制关闭kafka。这种关闭方式为暴力关闭,很可能会导致kafka无法完成对日志文件的解锁。届时,再次启动kafka的时候,就会提示日志文件被锁,无法成功启动。
    方案:将kafka的日志文件全部删除,再次启动即可。
    建议:不要暴力关闭kafka,建议通过在命令行执行kafka-server-stop命令来关闭它。
    其他:将来在Linux上部署kafka之后,采用后台运行的方式,就会避免这样的问题。

    【关闭】
    新打开一个 cmd,先关闭 kafka,再关闭 zookeeper

    C:\Users\15642>e:
    
    E:\>cd kafka_2.12-2.2.0\bin\windows
    
    E:\kafka_2.12-2.2.0\bin\windows>kafka-server-stop.bat
    删除实例 \\DESKTOP-R3O58DJ\ROOT\CIMV2:Win32_Process.Handle="16672"
    实例删除成功。
    
    E:\kafka_2.12-2.2.0\bin\windows>zookeeper-server-stop.bat
    删除实例 \\DESKTOP-R3O58DJ\ROOT\CIMV2:Win32_Process.Handle="9048"
    实例删除成功。
    删除实例 \\DESKTOP-R3O58DJ\ROOT\CIMV2:Win32_Process.Handle="8956"
    实例删除成功。
    删除实例 \\DESKTOP-R3O58DJ\ROOT\CIMV2:Win32_Process.Handle="7388"
    实例删除成功。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3. Spring整合Kafka

    引入依赖

    • spring-kafka

    配置Kafka

    • 配置server、consumer

    访问Kafka

    • 生产者
    kafkaTemplate.send(topic, data); 
    
    • 1
    • 消费者
    @KafkaListener(topics = {"test"}) 
    public void handleMessage(ConsumerRecord record) {}
    
    • 1
    • 2

    pom.xml

    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4

    E:\kafka_2.12-2.2.0\config 下的 consumer.properties
    改一下 group.id

    group.id=community-consumer-group
    
    • 1

    application.properties

    # KafkaProperties
    spring.kafka.bootstrap-servers=localhost:9092
    spring.kafka.consumer.group-id=community-consumer-group
    spring.kafka.consumer.enable-auto-commit=true
    spring.kafka.consumer.auto-commit-interval=3000
    
    • 1
    • 2
    • 3
    • 4
    • 5

    新建测试类

    package com.nowcoder.community;
    
    import org.apache.kafka.clients.consumer.ConsumerRecord;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.kafka.annotation.KafkaListener;
    import org.springframework.kafka.core.KafkaTemplate;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    
    @SpringBootTest
    public class KafkaTests {
    
        @Resource
        private KafkaProducer kafkaProducer;
    
        @Test
        public void testKafka() {
            kafkaProducer.sendMessage("test", "你好");
            kafkaProducer.sendMessage("test", "在吗");
    
            try {
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    @Component
    class KafkaProducer {
    
        @Resource
        private KafkaTemplate kafkaTemplate;
    
        public void sendMessage(String topic, String content) {
            kafkaTemplate.send(topic, content);
        }
    }
    
    @Component
    class KafkaConsumer {
    
        @KafkaListener(topics = {"test"})
        public void handleMessage(ConsumerRecord record) {
            System.out.println(record.value());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    先启动 zookeeper,再启动 kafka,最后执行测试方法看看能不能读取到信息

    在这里插入图片描述

    4. 发送系统通知

    触发事件

    • 评论后,发布通知
    • 点赞后,发布通知
    • 关注后,发布通知

    处理事件

    • 封装事件对象
    • 开发事件的生产者
    • 开发事件的消费者

    4.1 entity

    package com.nowcoder.community.entity;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class Event {
    
        private String topic;
        private int userId;
        private int entityType;
        private int entityId;
        private int entityUserId;
        private Map<String, Object> data = new HashMap<>();
    
        public String getTopic() {
            return topic;
        }
        //改动所有 set 方法,方便连续调用 set.set.set
        public Event setTopic(String topic) {
            this.topic = topic;
            return this;
        }
    
        public int getUserId() {
            return userId;
        }
    
        public Event setUserId(int userId) {
            this.userId = userId;
            return this;
        }
    
        public int getEntityType() {
            return entityType;
        }
    
        public Event setEntityType(int entityType) {
            this.entityType = entityType;
            return this;
        }
    
        public int getEntityId() {
            return entityId;
        }
    
        public Event setEntityId(int entityId) {
            this.entityId = entityId;
            return this;
        }
    
        public int getEntityUserId() {
            return entityUserId;
        }
    
        public Event setEntityUserId(int entityUserId) {
            this.entityUserId = entityUserId;
            return this;
        }
    
        public Map<String, Object> getData() {
            return data;
        }
    
        public Event setData(String key, Object value) {
            this.data.put(key, value);
            return this;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    新建一个包 event,封装生产者和消费者

    生产者

    package com.nowcoder.community.event;
    
    import com.alibaba.fastjson.JSONObject;
    import com.nowcoder.community.entity.Event;
    import org.springframework.kafka.core.KafkaTemplate;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    
    @Component
    public class EventProducer {
    
        @Resource
        private KafkaTemplate kafkaTemplate;
    
        // 处理事件
        public void fireEvent(Event event) {
            // 将事件发布到指定的主题
            kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    消费者

    CommunityConstant

    //主题:评论
    String TOPIC_COMMENT = "comment";
    //主题:点赞
    String TOPIC_LIKE = "like";
    //主题:关注
    String TOPIC_FOLLOW = "follow";
    //系统用户ID
    int SYSTEM_USER_ID = 1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    package com.nowcoder.community.event;
    
    import com.alibaba.fastjson.JSONObject;
    import com.nowcoder.community.entity.Event;
    import com.nowcoder.community.entity.Message;
    import com.nowcoder.community.service.MessageService;
    import com.nowcoder.community.util.CommunityConstant;
    import org.apache.kafka.clients.consumer.ConsumerRecord;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.kafka.annotation.KafkaListener;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    @Component
    public class EventConsumer implements CommunityConstant {
    
        public static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);
    
        @Resource
        private MessageService messageService;
    
        @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);
            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());
            content.put("entityType", event.getEntityType());
            content.put("entityId", event.getEntityId());
    
            if (!event.getData().isEmpty()) {
                for (Map.Entry<String, Object> entry : event.getData().entrySet()) {
                    content.put(entry.getKey(), entry.getValue());
                }
            }
    
            message.setContent(JSONObject.toJSONString(content));
            messageService.addMessage(message);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    4.2 controller

    CommentMapper 补充方法

    Comment selectCommentById(int id);
    
    • 1

    comment-mapper.xml 实现该方法

    <select id="selectCommentById" resultType="Comment">
        select <include refid="selectFields"></include>
        from comment
        where id = #{id}
    </select>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    CommentService 增加方法

    public Comment findCommentById(int id) {
        return commentMapper.selectCommentById(id);
    }
    
    • 1
    • 2
    • 3

    CommentController

    public class CommentController implements CommunityConstant {
    
    	@Resource
    	private EventProducer eventProducer;
    
    	@Resource
        private DiscussPostService discussPostService;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    完善 addComment 方法

    @PostMapping("/add/{discussPostId}")
    public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
        comment.setUserId(hostHolder.getUser().getId());
        comment.setStatus(0);
        comment.setCreateTime(new Date());
        commentService.addComment(comment);
    
        //触发评论事件
        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);
    
        return "redirect:/discuss/detail/" + discussPostId;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    LikeController

    public class LikeController implements CommunityConstant {
    
    	@Resource
    	private EventProducer eventProducer;
    
    • 1
    • 2
    • 3
    • 4

    完善 like 方法

    @PostMapping("/like")
    @ResponseBody
    public String like(int entityType, int entityId, int entityUserId, int postId) { //加了一个形参postId
        User user = hostHolder.getUser();
        // 点赞
        likeService.like(user.getId(), entityType, entityId, entityUserId);
        // 数量
        long likeCount = likeService.findEntityLikeCount(entityType, entityId);
        // 状态
        int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
        // 返回的结果
        Map<String, Object> map = new HashMap<>();
        map.put("likeCount", likeCount);
        map.put("likeStatus", likeStatus);
    
        //触发点赞事件
        if (likeStatus == 1) { //点赞才触发,取消赞就不用了
            Event event = new Event()
                    .setTopic(TOPIC_LIKE)
                    .setUserId(hostHolder.getUser().getId())
                    .setEntityType(entityType)
                    .setEntityId(entityId)
                    .setEntityUserId(entityUserId)
                    .setData("postId", postId);
            eventProducer.fireEvent(event);
        }
    
        return CommunityUtil.getJSONString(0, null, map);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    FollowController

    @Resource
    private EventProducer eventProducer;
    
    • 1
    • 2

    完善 follow 方法

    @PostMapping("/follow")
    @ResponseBody
    public String follow(int entityType, int entityId) {
        User user = hostHolder.getUser();
        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, "已关注!");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    discuss-detail.html
    87行

    <a href="javascript:;" th:onclick="|like(this, 1, ${post.id}, ${post.userId}, ${post.id});|" class="text-primary">
    
    • 1

    139行

    <a href="javascript:;" th:onclick="|like(this, 2, ${cvo.comment.id},${cvo.comment.userId}, ${post.id})|" class="text-primary">
    
    • 1

    165行

    <a href="javascript:;" th:onclick="|like(this, 2, ${rvo.reply.id}, ${rvo.reply.userId}, ${post.id})|" class="text-primary">
    
    • 1

    discuss.js

    function like(btn, entityType, entityId, entityUserId, postId) {
        $.post(
            CONTEXT_PATH + "/like",
            {"entityType":entityType, "entityId":entityId, "entityUserId":entityUserId, "postId":postId},
            function (data) {
                data = $.parseJSON(data);
                if (data.code == 0) {
                    $(btn).children("i").text(data.likeCount);
                    $(btn).children("b").text(data.likeStatus==1?'已赞':'赞');
                } else {
                    alert(data.msg);
                }
            }
        );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    ServiceLogAspect
    完善 before 方法

    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
        //日志格式:用户[ip]在[时间点],访问了[com.community.service.xxx()]
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {//增加判空
            return;
        }
        HttpServletRequest request = attributes.getRequest();
        String ip = request.getRemoteHost();
        String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
        logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    启动程序,测试(确保 zookeeper 和 kafka 已经提前启动了),登陆之后随便找个帖子进行回帖,给评论点赞、关注某个人

    在这里插入图片描述

    去数据库 message 表中查看是否有数据

    在这里插入图片描述

    5. 显示系统通知

    通知列表

    • 显示评论、点赞、关注三种类型的通知

    通知详情

    • 分页显示某一类主题所包含的通知

    未读消息

    • 在页面头部显示所有的未读消息数量

    5.1 通知列表

    MessageMapper
    增加三个方法

    //查询某个主题下最新的通知
    Message selectLatestNotice(@Param("userId") int userId, @Param("topic") String topic);
    
    //查询某个主题所包含的通知的数量
    int selectNoticeCount(@Param("userId") int userId, @Param("topic") String topic);
    
    //查询未读的通知的数量
    int selectNoticeUnreadCount(@Param("userId") int userId, @Param("topic") String topic);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    message-mapper.xml
    实现三个方法

    <select id="selectLatestNotice" resultType="Message">
        select <include refid="selectFields"></include>
        from message
        where id in (
            select max(id) from message
            where status != 2
            and from_id = 1
            and to_id = #{userId}
            and conversation_id = #{topic}
        )
    </select>
    
    <select id="selectNoticeCount" resultType="int">
        select count(id) from message
        where status != 2
            and from_id = 1
            and to_id = #{userId}
            and conversation_id = #{topic}
    </select>
    
    <select id="selectNoticeUnreadCount" resultType="int">
        select count(id) from message
        where status = 0
            and from_id = 1
            and to_id = #{userId}
            <if test="topic!=null">
                and conversation_id = #{topic}
            </if>
    </select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    MessageService
    增加方法

    public Message findLatestNotice(int userId, String topic) {
        return messageMapper.selectLatestNotice(userId, topic);
    }
    
    public int findNoticeCount(int userId, String topic) {
        return messageMapper.selectNoticeCount(userId, topic);
    }
    
    public int findNoticeUnreadCount(int userId, String topic) {
        return messageMapper.selectNoticeUnreadCount(userId, topic);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    MessageController
    增加方法

    public class MessageController implements CommunityConstant {
    
    	@GetMapping("/notice/list")
        public String getNoticeList(Model model) {
            User user = hostHolder.getUser();
            //查询评论类的通知
            Message message = messageService.findLatestNotice(user.getId(), TOPIC_COMMENT);
            Map<String, Object> messageVO = new HashMap<>();
            if (message != null) {
                messageVO.put("message", message);
    
                String content = HtmlUtils.htmlUnescape(message.getContent());
                HashMap<String, Object> data = JSONObject.parseObject(content, HashMap.class);
    
                messageVO.put("user", userService.findUserById((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);
    
                String content = HtmlUtils.htmlUnescape(message.getContent());
                HashMap<String, Object> data = JSONObject.parseObject(content, HashMap.class);
    
                messageVO.put("user", userService.findUserById((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);
    
                String content = HtmlUtils.htmlUnescape(message.getContent());
                HashMap<String, Object> data = JSONObject.parseObject(content, HashMap.class);
    
                messageVO.put("user", userService.findUserById((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";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    getLetterList 方法的 return 之前加上两行:

    int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null);
    model.addAttribute("noticeUnreadCount", noticeUnreadCount);
    
    • 1
    • 2

    letter.html
    74行

    <a class="nav-link position-relative" th:href="@{/notice/list}">
    	系统通知<span class="badge badge-danger" th:text="${noticeUnreadCount}" th:if="${noticeUnreadCount != 0}">27</span>
    </a>
    
    • 1
    • 2
    • 3

    notice.html
    2行

    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
    • 1

    8-9行

    <link rel="stylesheet" th:href="@{/css/global.css}" />
    <link rel="stylesheet" th:href="@{/css/letter.css}" />
    
    • 1
    • 2

    15行

    <!-- 头部 -->
    <header class="bg-dark sticky-top" th:replace="index::header">
    
    • 1
    • 2

    197行

    <script th:src="@{/js/global.js}"></script>
    
    • 1

    67行 ul 标签

    <!-- 选项 -->
    <ul class="nav nav-tabs mb-3">
    	<li class="nav-item">
    		<a class="nav-link position-relative" th:href="@{/letter/list}">朋友私信
    			<span class="badge badge-danger" th:text="${letterUnreadCount}" th:if="${letterUnreadCount != 0}">3</span>
    		</a>
    	</li>
    	<li class="nav-item">
    		<a class="nav-link position-relative active" th:href="@{/notice/list}">
    			系统通知<span class="badge badge-danger" th:text="${noticeUnreadCount}" th:if="${noticeUnreadCount != 0}">27</span>
    		</a>
    	</li>
    </ul>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    82行 ul 标签

    <!-- 通知列表 -->
    <ul class="list-unstyled">
    	<!--评论类通知-->
    	<li class="media pb-3 pt-3 mb-3 border-bottom position-relative" th:if="${commentNotice.message!=null}">
    		<span class="badge badge-danger" th:text="${commentNotice.unread!=0?commentNotice.unread:''}">3</span>
    		<img src="http://static.nowcoder.com/images/head/reply.png" class="mr-4 user-header" alt="通知图标">
    		<div class="media-body">
    			<h6 class="mt-0 mb-3">
    				<span>评论</span>
    				<span class="float-right text-muted font-size-12"
    					th:text="${#dates.format(commentNotice.message.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-28 14:13:25</span>
    			</h6>
    			<div>
    				<a href="#">
    					用户
    					<i th:utext="${commentNotice.user.username}">nowcoder</i>
    					评论了你的<b th:text="${commentNotice.entityType==1?'帖子':'回复'}">帖子</b> ...
    				</a>
    				<ul class="d-inline font-size-12 float-right">
    					<li class="d-inline ml-2"><span class="text-primary"><i th:text="${commentNotice.count}">3</i> 条会话</span></li>
    				</ul>
    			</div>
    		</div>
    	</li>
    	<!--点赞类通知-->
    	<li class="media pb-3 pt-3 mb-3 border-bottom position-relative" th:if="${likeNotice.message!=null}">
    		<span class="badge badge-danger" th:text="${likeNotice.unread!=0?likeNotice.unread:''}">3</span>
    		<img src="http://static.nowcoder.com/images/head/like.png" class="mr-4 user-header" alt="通知图标">
    		<div class="media-body">
    			<h6 class="mt-0 mb-3">
    				<span></span>
    				<span class="float-right text-muted font-size-12"
    					  th:text="${#dates.format(likeNotice.message.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-28 14:13:25</span>
    			</h6>
    			<div>
    				<a href="#">
    					用户
    					<i th:utext="${likeNotice.user.username}">nowcoder</i>
    					点赞了你的<b th:text="${likeNotice.entityType==1?'帖子':'回复'}">帖子</b> ...
    				</a>
    				<ul class="d-inline font-size-12 float-right">
    					<li class="d-inline ml-2"><span class="text-primary"><i th:text="${likeNotice.count}">3</i> 条会话</span></li>
    				</ul>
    			</div>
    		</div>
    	</li>
    	<!--关注类通知-->
    	<li class="media pb-3 pt-3 mb-3 border-bottom position-relative" th:if="${followNotice.message!=null}">
    		<span class="badge badge-danger" th:text="${followNotice.unread!=0?followNotice.unread:''}">3</span>
    		<img src="http://static.nowcoder.com/images/head/follow.png" class="mr-4 user-header" alt="通知图标">
    		<div class="media-body">
    			<h6 class="mt-0 mb-3">
    				<span>关注</span>
    				<span class="float-right text-muted font-size-12"
    					  th:text="${#dates.format(followNotice.message.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-28 14:13:25</span>
    			</h6>
    			<div>
    				<a href="#">
    					用户
    					<i th:utext="${followNotice.user.username}">nowcoder</i>
    					关注了你 ...</a>
    				<ul class="d-inline font-size-12 float-right">
    					<li class="d-inline ml-2"><span class="text-primary"><i th:text="${followNotice.count}">3</i> 条会话</span></li>
    				</ul>
    			</div>
    		</div>
    	</li>					
    </ul>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    5.2 通知详情

    MessageMapper

    //查询某个主题所包含的通知列表
    List<Message> selectNotices(@Param("userId") int userId,
                                @Param("topic") String topic,
                                @Param("offset") int offset,
                                @Param("limit") int limit);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    message-mapper.xml

    <select id="selectNotices" resultType="Message">
        select <include refid="selectFields"></include>
        from message
        where status != 2
        and from_id = 1
        and to_id = #{userId}
        and conversation_id = #{topic}
        order by create_time desc
        limit #{offset}, #{limit}
    </select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    MessageService

    public List<Message> findNotices(int userId, String topic, int offset, int limit) {
        return messageMapper.selectNotices(userId, topic, offset, limit);
    }
    
    • 1
    • 2
    • 3

    MessageController

    @GetMapping("/notice/detail/{topic}")
    public String getNoticeDetail(@PathVariable("topic") String topic, Page page, Model model) {
        User user = hostHolder.getUser();
    
        page.setLimit(5);
        page.setPath("/notice/detail/" + topic);
        page.setRows(messageService.findNoticeCount(user.getId(), topic));
    
        List<Message> noticeList = messageService.findNotices(user.getId(), topic, page.getOffset(), page.getLimit());
        List<Map<String, Object>> noticeVoList = new ArrayList<>();
        if (noticeList != null) {
            for (Message notice : noticeList) {
                Map<String, Object> map = new HashMap<>();
                //通知
                map.put("notice", notice);
                //内容
                String content = HtmlUtils.htmlUnescape(notice.getContent());
                Map<String, Object> data = JSONObject.parseObject(content, HashMap.class);
                map.put("user", userService.findUserById((Integer) data.get("userId")));
                map.put("entityType", data.get("entityType"));
                map.put("entityId", data.get("entityId"));
                map.put("postId", data.get("postId"));
                //通知的作者
                map.put("fromUser", userService.findUserById(notice.getFromId()));
                noticeVoList.add(map);
            }
        }
        model.addAttribute("notices", noticeVoList);
        //设置已读
        List<Integer> ids = getLetterIds(noticeList);
        if (!ids.isEmpty()) {
            messageService.readMessage(ids);
        }
        return "/site/notice-detail";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    notice.html
    94行

    <a th:href="@{/notice/detail/comment}">
    
    • 1

    116行

    <a th:href="@{/notice/detail/like}">
    
    • 1

    138行

    <a th:href="@{/notice/detail/follow}">
    
    • 1

    notice-detail.html
    2行

    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
    • 1

    8-9行

    <link rel="stylesheet" th:href="@{/css/global.css}" />
    <link rel="stylesheet" th:href="@{/css/letter.css}" />
    
    • 1
    • 2

    15行

    <!-- 头部 -->
    <header class="bg-dark sticky-top" th:replace="index::header">
    
    • 1
    • 2

    删除 91-150行的 li 标签

    175-176行

    <script th:src="@{/js/global.js}"></script>
    <script th:src="@{/js/letter.js}"></script>
    
    • 1
    • 2

    70行

    <button type="button" class="btn btn-secondary btn-sm" onclick="back();">返回</button>
    
    • 1

    在最后面多加一个 script 标签

    <script>
    	function back() {
    		location.href = CONTEXT_PATH + "/notice/list";
    	}
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    76行

    <li class="media pb-3 pt-3 mb-2" th:each="map:${notices}">
    
    • 1

    77行

    <img th:src="${map.fromUser.headerUrl}" class="mr-4 rounded-circle user-header" alt="系统图标">
    
    • 1

    80行

    <strong class="mr-auto" th:utext="${map.fromUser.username}">落基山脉下的闲人</strong>
    
    • 1

    81行

    <small th:text="${#dates.format(map.notice.createTime, 'yyyy-MM-dd HH:mm:ss')}">2019-04-25 15:49:32</small>
    
    • 1

    86行 div 标签

    <div class="toast-body">
    	<span th:if="${topic.equals('comment')}">
    		用户
    		<i th:utext="${map.user.username}">nowcoder</i>
    		评论了你的<b th:text="${map.entityType==1?'帖子':'回复'}">帖子</b>,
    		<a class="text-primary" th:href="@{|/discuss/detail/${map.postId}|}">点击查看</a> !
    	</span>
    	<span th:if="${topic.equals('like')}">
    		用户
    		<i th:utext="${map.user.username}">nowcoder</i>
    		点赞了你的<b th:text="${map.entityType==1?'帖子':'回复'}">帖子</b>,
    		<a class="text-primary" th:href="@{|/discuss/detail/${map.postId}|}">点击查看</a> !
    	</span>
    	<span th:if="${topic.equals('follow')}">
    		用户
    		<i th:utext="${map.user.username}">nowcoder</i>
    		关注了你
    		<a class="text-primary" th:href="@{|/user/profile/${map.user.id}|}">点击查看</a> !
    	</span>
    </div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    110行

    <!-- 分页 -->
    <nav class="mt-5" th:replace="index::pagination">
    
    • 1
    • 2

    5.3 未读消息

    新建一个拦截器

    package com.nowcoder.community.controller.interceptor;
    
    import com.nowcoder.community.entity.User;
    import com.nowcoder.community.service.MessageService;
    import com.nowcoder.community.util.HostHolder;
    import org.springframework.stereotype.Component;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.annotation.Resource;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    @Component
    public class MessageInterceptor implements HandlerInterceptor {
    
        @Resource
        private HostHolder hostHolder;
    
        @Resource
        private MessageService messageService;
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            User user = hostHolder.getUser();
            if (user != null && modelAndView != null) {
                int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
                int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null);
                modelAndView.addObject("allUnreadCount", letterUnreadCount + noticeUnreadCount);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    配置它
    WebMvcConfig

    @Resource
    private MessageInterceptor messageInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginTicketInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    
        registry.addInterceptor(loginRequiredInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    
        registry.addInterceptor(messageInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    index.html
    30行

    <a class="nav-link position-relative" th:href="@{/letter/list}">消息<span class="badge badge-danger" th:text="${allUnreadCount!=0?allUnreadCount:''}">12</span></a>
    
    • 1

    提前开启 zookeeper 和 kafka,启动项目,测试,登录 niuke,给 hahaha 关注,点赞、评论,再登录 hahaha 查看消息

    在这里插入图片描述

  • 相关阅读:
    说透缓存一致性与内存屏障
    为什么说制造企业需要部署MES管理系统
    golang使用mongo-driver操作——增(进阶)
    论文摘要的写作技巧
    现场感言讲稿的标准模板
    华为OD机试之最小调整顺序次数、特异性双端队列(Java源码)
    把备考高项当成项目:我的信息系统项目管理师项目实施方案
    Jmeter中如何获取Post请求参数中某个键的值
    This download does NOT match an earlier download recorded in go.sum.
    华为机试真题 Java 实现【最小调整顺序次数】【2022.11 Q4 新题】
  • 原文地址:https://blog.csdn.net/QinLaoDeMaChu/article/details/125497298