13.Spring 整合 Kafka + 发送系统通知 + 显示系统通知

news2024/11/13 9:49:20

目录

1.Spring 整合 Kafka

2.发送系统通知

2.1 封装事件对象 

2.2 开发事件的生产者和消费者

2.3 触发事件:在评论、点赞、关注后通知​编辑

3.显示系统通知

3.1 通知列表

3.1.1 数据访问层

3.1.2 业务层

3.1.3 表现层

3.2 开发通知详情

3.2.1 开发数据访问层

3.1.2 业务层

3.1.3 表现层

3.3 未读消息


1.Spring 整合 Kafka

  • 引入依赖:kafka
  • 配置 Kafka:配置 server、consumer
  • 访问 Kafka:生产者——kafkaTemplate.send(topic, data);   消费者——@KafkaListener(topics = {"test"})  public void handleMessage(ConsumerRecord record) { }

 引入依赖 :

<!-- https://mvnrepository.com/artifact/org.springframework.kafka/spring-kafka -->
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
    <version>3.1.0</version>
</dependency>

配置 Kafka:

# KafkaProperties
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=test-consumer-group
spring.kafka.consumer.enable-auto-commit=true
spring.kafka.consumer.auto-commit-interval=3000

测试类:

package com.example.demo;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = DemoApplication.class)
public class KafkaTests {

    @Autowired
    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 {

    @Autowired
    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());
    }


}

2.发送系统通知

  • 触发事件:评论后,发布通知;点赞后,发布通知;关注后,发布通知
  • 处理事件:封装事件对象;开发事件的生产者;开发事件的消费者

2.1 封装事件对象 

定义一个对象对事件封装 事件触发相关一切信息,在 entity 包下创建 Event 实体类

package com.example.demo.entity;

import java.util.HashMap;
import java.util.Map;

/**
 * 发送系统消息实体类
 */
public class Event {

    private String topic;//主题
    private int userId;
    private int entityType;//发生在哪个实体类
    private int entityId;//实体id
    private int entityUserId;//实体作者
    private Map<String, Object> data = new HashMap<>();//处理其他事件,会有数据进行记录,将额外的数据存到 map 中,具有扩展性

    public String getTopic() {
        return topic;
    }

    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;
    }

}

2.2 开发事件的生产者和消费者

新建 event 包,创建 EventProducer 类(开发事件的生产者):

  • 添加 @Component 注解 
  • 调用 KafkaTemlate
  • 生产者提供处理事件的方法:将事件发布到指定的主题
package com.example.demo.event;

import com.alibaba.fastjson.JSONObject;
import com.example.demo.entity.Event;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

/**
 * 开发事件的生产者
 */

@Component
public class EventProducer {

    @Autowired
    private KafkaTemplate kafkaTemplate;

    public void fireEvent(Event event) {
        //将事件发布到指定的主题
        kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
    }
}

创建 EventConsumer 类(开发事件的消费者):

  • 添加 @Component 注解 
  • 如果出现异常需要记录日志,声明日志
  • 最终需要往 message 中插入数据,需要注入 MessageService
  • 添加方法处理事件消费主题:添加 @KafkaListener(topics = 主题)
  • 定义主题常量,在 CommunityConstant 接口类中添加
    /**
     * 主题: 评论
     */
    String TOPIC_COMMENT = "comment";

    /**
     * 主题: 点赞
     */
    String TOPIC_LIKE = "like";

    /**
     * 主题: 关注
     */
    String TOPIC_FOLLOW = "follow";

    //后边需要用到,这里先声明
    /**
     * 系统用户ID
     */
    int SYSTEM_USER_ID = 1;
  • 定义消费者方法:添加参数接收相关数据,首先判断数据是否为空,为空记录日志;不为空的话是一个 JSON 字符串,然后将字符串解析成相应的事件,再去判断对象是否为空,为空记录日志
  • 不为空(此时内容和格式都正确),发送站内通知:构造 Massage 对象、设置系统用户id(消息的发布者)、设置消息的接收者(entityUserId)、设置会话 id、设置创建当前时间、设置具体内容(内容里面包含对象,对象包含的是页面拼接话语的数据——这件事谁触发的、对哪个实体做操作、点击查看链接到帖子详情页面即需要知道帖子 id)
  • 使用 Map 存储具体内容:用户 id、实体类型、实体 id、触发事件还可能又有额外的数据也需要存入 Map 中(以 key、vlaue 形式存储),最终存储到 Massage 对象中(最终存储 JSON 字符串)
  • 上述即构造完成 Massage 对象,再进行存储
package com.example.demo.event;

import com.alibaba.fastjson.JSONObject;
import com.example.demo.entity.Event;
import com.example.demo.entity.Message;
import com.example.demo.service.MessageService;
import com.example.demo.util.CommunityConstant;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;



@Component
public class EventConsumer implements CommunityConstant {

    //如果出现异常需要记录日志,声明日志
    private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);

    //最终需要往 message 中插入数据,需要注入 MessageService
    @Autowired
    private MessageService messageService;

    //添加方法处理事件消费主题:添加 @KafkaListener(topics = 主题)
    @KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
    //定义消费者方法:添加参数接收相关数据,首先判断数据是否为空,为空记录日志;
    public void handleCommentMessage(ConsumerRecord record) {
        if (record == null || record.value() == null) {
            logger.error("消息的内容为空!");
            return;
        }

        // 不为空的话是一个 JSON 字符串,然后将字符串解析成相应的事件,再去判断对象是否为空,为空记录日志
        Event event = JSONObject.parseObject(record.value().toString(), Event.class);

        //再去判断对象是否为空,为空记录日志
        if (event == null) {
            logger.error("消息格式错误!");
            return;
        }

        //不为空(此时内容和格式都正确),发送站内通知
        Message message = new Message();//构造 Massage 对象
        message.setFromId(SYSTEM_USER_ID);//设置系统用户id(消息的发布者)
        message.setToId(event.getEntityUserId());//设置消息的接收者(entityUserId)
        message.setConversationId(event.getTopic());//设置会话 id
        message.setCreateTime(new Date());//设置创建当前时间

        //设置具体内容(内容里面包含对象,对象包含的是页面拼接话语的数据
        //这件事谁触发的、对哪个实体做操作、点击查看链接到帖子详情页面即需要知道帖子 id)

        //使用 Map 存储具体内容:用户 id、实体类型、实体 id、触发事件还可能又有额外的数据也需要存入 Map 中(以 key、vlaue 形式存储)
        //最终存储到 Massage 对象中(最终存储 JSON 字符串)
        Map<String, Object> content = new HashMap<>();//使用 Map 存储具体内容
        content.put("userId", event.getUserId());//用户 id、实体类型
        content.put("entityType", event.getEntityType());//实体类型
        content.put("entityId", event.getEntityId());//实体 id

        //发事件还可能又有额外的数据也需要存入 Map 中(以 key、vlaue 形式存储)
        if (!event.getData().isEmpty()) {
            for (Map.Entry<String, Object> entry : event.getData().entrySet()) {
                content.put(entry.getKey(), entry.getValue());
            }
        }

        //最终存储到 Massage 对象中(最终存储 JSON 字符串)
        message.setContent(JSONObject.toJSONString(content));

        //再进行存储
        messageService.addMessage(message);
    }
}

2.3 触发事件:在评论、点赞、关注后通知

打开 CommentController 类进行修改:

  • 处理事件需要注入 EventProducer
  • 添加评论之后在进行通知,在 addComment 之后进行通知
  • 触发评论事件:构造事件对象(包含事件相关内容——事件主题(实现接口)、事件登录者触发、实体类型、实体 id、点击查看链接(需要帖子 id)),实体作者不可以在这里拼接,因为评论的目标可能是帖子也可能是评论
  • 实体作者需要进行判断
  • 评论目标是帖子:注入 DiscussPostService,查询帖子 id,补充实体作者
  • 评论目标是评论在 CommentMapper 类中添加选择评论 id 的方法
//选择评论 id 
Comment selectCommentById(int id);
  • 在 resources 资源文件下 mapper 包的 comment-mapper.xml 添加实现方法
    <select id="selectCommentById" resultType="Comment">
        select <include refid="selectFields"></include>
        from comment
        where id = #{id}
    </select>
  • 在 CommentService 类中实现方法:
    //调用 选择评论 id 的方法
    public Comment findCommentById(int id) {
        return commentMapper.selectCommentById(id);
    }
  • 评论目标是评论:这个时候就可以根据评论查询帖子 id,补充实体作者
  • 调用 eventProducer 发布消息

打开 LikeController 类进行修改:类似上述操作

点赞在点赞之后进行触发事件:

  • 点赞进行通知(likeStatus == 1),取消赞则不需要通知
  • 得到帖子 id,需要重构点赞方法:传入帖子 id
  • 最后触发事件
    public String like(int entityType, int entityId, int entityUserId, int postId) {
        //获取当前用户
        User user = hostHolder.getUser();

        //实现点赞:调用 LikeService
        likeService.like(user.getId(), entityType, entityId, entityUserId);

        //统计点赞数量、点赞状态返回页面,页面根据返回值做数量和状态显示
        // 数量
        long likeCount = likeService.findEntityLikeCount(entityType, entityId);
        // 状态
        int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
        // 返回的结果(用 Map 封装)
        Map<String, Object> map = new HashMap<>();
        map.put("likeCount", likeCount);
        map.put("likeStatus", likeStatus);

        // 触发点赞事件
        //点赞进行通知(likeStatus == 1),取消赞则不需要通知
        if (likeStatus == 1) {
            Event event = new Event()
                    .setTopic(TOPIC_LIKE)
                    .setUserId(hostHolder.getUser().getId())
                    .setEntityType(entityType)
                    .setEntityId(entityId)
                    .setEntityUserId(entityUserId)
                    .setData("postId", postId);//得到帖子 id,需要重构点赞方法:传入帖子 id
            eventProducer.fireEvent(event);
        }

        //返回页面
        return CommunityUtil.getJSONString(0,null,map);
    }
  •  重构点赞方法:传入帖子 id ,需要修改帖子详情页面传入帖子 id(discuss-detail.html):
<a href="javascript:;" th:onclick="|like(this,1,${post.id},${post.userId},${post.id});|" class="text-primary">

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

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

在 discuss.js 中修改 like 方法:

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);
            }
        }
    );
}

打开 FollowController 类进行修改:

  • 在关注之后(followService.follow)进行触发事件
    //关注请求:当前登陆用户关注某一个实体,传入实体参数并且注入 HostHolder
    @RequestMapping(path = "/follow", method = RequestMethod.POST)
    @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(hostHolder.getUser().getId())
                .setEntityType(entityType)
                .setEntityId(entityId)
                .setEntityUserId(entityId);
        eventProducer.fireEvent(event);

        return CommunityUtil.getJSONString(0, "已关注!");
    }

在 ServiceLogAspect 类中需要稍微进行修改:attributes 是和请求有关的对象,在 ServiceLogAspect 类所有的 Service 都是通过 Controller 进行访问。而刚才我们写了一个 EventConsumer(消费者)并且调用了 Service,不是通过 Controller 调用,这次调用得不到 Request,会出现空指针异常:

package com.example.demo.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;

@Component
@Aspect
public class ServiceLogAspect {
    //实例化 Logger
    private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);

    //声明切点:所有业务组件都去处理
    @Pointcut("execution(* com.example.demo.service.*.*(..))")
    public void pointcut() {

    }
    //使用前置通知在业务组件一开始记录日志
    @Before("pointcut()")
    public void before(JoinPoint joinPoint) {
        //记录格式:用户[1.2.3.4],在[xxx],访问了[com.example.demo.service.xxx()].

        //用户 ip 通过 request 获取,获取 request:RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes attributes =(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        //消费者调用了
        if (attributes == null) {
            return;
        }
        HttpServletRequest request = attributes.getRequest();
        String ip = request.getRemoteHost();
        //拼接时间:new Date,然后实例化
        String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        //访问某个类某个方法(类名 + 方法名):给方法添加 JoinPoint 连接点参数,连接点指代程序植入的目标方法
        String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
        //全部拼接
        logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));
    }
}

评论:

点赞:

关注:

3.显示系统通知

  • 通知列表:显示评论、点赞、关注三种类型的通知
  • 通知详情:分页显示某一类主题所包含的通知
  • 未读消息:在页面头部显示所有的未读消息数量

3.1 通知列表

3.1.1 数据访问层

通知我们当时存放在 Message 表中,因此对 MessageMapper 类增加方法:

  • 查询某个主题(评论、点赞、关注)下最新的通知:查询某个用户通知,传入用户 id、并且传入主题
  • 查询某个主题所包含的通知数量:返回整数
  • 查询未读通知数量
    //查询某个主题(评论、点赞、关注)下最新的通知
    Message selectLatestNotice(int userId, String topic);

    //查询某个主题所包含的通知数量
    int selectNoticeCount(int userId, String topic);
    
    //查询未读通知数量
    int selectNoticeUnreadCount(int userId, String topic);

方法申明之后再去实现方法,在对应的 message-mapper.xml 中去实现

  • 查询某个主题(评论、点赞、关注)下最新的通知:询最新的 id并且状态(status != 2,代表私信没有被删除);发送通知是由系统通知用户(from_id = 1),避免和普通私信混淆;接收者为用户 id;主题是在 conversation_id 中存储
  • 查询某个主题所包含的通知数量:同上(只是查询的是数量,不是最新 id)
  • 查询未读通知数量:同上(未读则 status = 0),还需要查询整个通知的未读数量(如果不传入 topic,那么意味着查询的就是整个通知的未读数量)
    <!--查询某个主题(评论、点赞、关注)下最新的通知-->
    <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>

3.1.2 业务层

通知业务层在 MessageService 类中添加方法:

  • 查询最新的通知:直接调用 selectLatestNotice
  • 查询通知数量:直接调用上述方法即可
  • 查询未读数量:直接调用上述方法即可
    //查询最新的通知:直接调用 selectLatestNotice
    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);
    }

3.1.3 表现层

处理 Controller,处理请求,处理模板

在 MessageController 类中处理请求:新增方法,显示通知列表

  • 声明访问路径,查询为 GET 请求,传入 Model,向模板传数据
  • 获取当前对象,查询三类通知(评论、点赞、关注)
  • 查询评论类通知:实现接口传入常量,查询到数据后,再做一些补充的数据(例如数量、user等),创建 Map 用来聚合数据;如果查询到的数据不为空进一步查询更多数据存放到 Map 中
  • 首先在 Map 中存入 message,但是在 message 中 content 是一个字符串,需要还原成对象放入 Map 中就可以方便访问数据

  • 还原成对象:使用 HtmlUtils 工具对 html 的标签、字符进行转义得到的 content 就没有转义字符,再转化相应的对象,再存入 Map 中(user、entityType、entityId、postId)
  • 还需要查询某一类的数量和未读数量,存放到 Map 中;最后传入模板
  • 查询点赞类通知:与上述评论通知大致相同(类型为点赞—— TOPIC_LIKE)
  • 查询关注类通知:(类型为关注—— TOPIC_FOLLOW),关注点击跳转链接,跳转到关注的人,而不是跳转到帖子,所以不需要在 Map 中存 帖子 id
  • 显示系统通知的未读数量和朋友私信的未读数量

  • 之前编写过查询私信列表的时候,没有开发通知列表,现在我们可以一边显示朋友私信的未读数量,一边显示系统通知的未读数量,在私信列表方法中的查询未读消息数量后添加显示系统通知的未读数量
  • @RequestMapping(path = "/letter/list", method = RequestMethod.GET)
        public String getLetterList(Model model, Page page) {
    
            //设置分页信息(每页显示多少条数据、分页路径、一共多少条数据——查询当前会话的数据、传入 userId,需要获取 User)
            User user = hostHolder.getUser();
            page.setLimit(5);
            page.setPath("/letter/list");
            page.setRows(messageService.findConversationCount(user.getId()));
    
            //查询会话列表得到数据(显示未读数量、每一次会话的未读数量、会话中包含多少条数据)
            //声明集合,用 Map 封装,将多个数据存入 Map 中
            List<Message> conversationList = messageService.findConversations(
                    user.getId(), page.getOffset(), page.getLimit());
            List<Map<String, Object>> conversations = new ArrayList<>();
            //遍历列表,新建 HashMap 重构数据:存入遍历的每一次数据、存入未读详细数据(用户 id、会话 id)
            // 、存入多少条数量(会话 id)、显示当前用户相对应的用户头像
            if (conversationList != null) {
                for (Message message : conversationList) {
                    Map<String, Object> map = new HashMap<>();
                    map.put("conversation", message);
                    map.put("letterCount", messageService.findLetterCount(message.getConversationId()));
                    map.put("unreadCount", messageService.findLetterUnreadCount(user.getId(), message.getConversationId()));
    
                    //寻找目标 id:如果当前用户是消息的发起者,目标就是接收人;如果当前对象是消息的接收者,目标就是发起者
                    int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();
    
                    //将目标对象存入 HashMap 中(注入 UserService)
                    map.put("target", userService.findUserById(targetId));
                    //将得到的 HashMap 存入集合当中
                    conversations.add(map);
                }
            }
    
            //最后传入模板中
            model.addAttribute("conversations", conversations);
    
            // 查询未读消息数量(查询整个用户所有的未读消息数量),传入 Model 中显示,返回 Model 路径(/site/letter)
            int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
            model.addAttribute("letterUnreadCount", letterUnreadCount);
            // 查询系统通知的未读数量
            int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null);
            model.addAttribute("noticeUnreadCount", noticeUnreadCount);
    
            return "/site/letter";
        }
    }
  • 最后返回到模板
    //新增方法,显示通知列表
    //声明访问路径,查询为 GET 请求,传入 Model,向模板传数据
    @RequestMapping(path = "/notice/list", method = RequestMethod.GET)
    public String getNoticeList(Model model) {
        //获取当前对象,查询三类通知(评论、点赞、关注)
        User user = hostHolder.getUser();

        //查询评论类通知

        //实现接口传入常量,查询到数据后,再做一些补充的数据(例如数量、user等),创建 Map 用来聚合数据;如果查询到的数据不为空进一步查询更多数据存放到 Map 中
        Message message = messageService.findLatestNotice(user.getId(), TOPIC_COMMENT);
        Map<String, Object> messageVO = new HashMap<>();

        //如果在if内,如 message为null,messageVo根本没有存放这个message,所以放外边
        messageVO.put("message", message);
        if (message != null) {


            //但是在 message 中 content 是一个字符串,需要还原成对象放入 Map 中就可以方便访问数据
            //使用 HtmlUtils 工具对 html 的标签、字符进行转义得到的 content 就没有转义字符
            String content = HtmlUtils.htmlUnescape(message.getContent());
            //再转化相应的对象
            Map<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"));

            //还需要查询某一类的数量和未读数量,存放到 Map 中;最后传入模板

            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<>();
        messageVO.put("message", message);
        if (message != null) {

            String content = HtmlUtils.htmlUnescape(message.getContent());
            Map<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<>();

        messageVO.put("message", message);
        if (message != null) {


            String content = HtmlUtils.htmlUnescape(message.getContent());
            Map<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";
    }

最后处理模板:letter.html:

<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>

处理通知模板(notice.html):声明模板

3.2 开发通知详情

3.2.1 开发数据访问层

对 MessageMapper 类增加方法:支持分页

    // 查询某个主题所包含的通知列表
    List<Message> selectNotices(int userId, String topic, int offset, int limit);

方法申明之后再去实现方法,在对应的 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>

3.1.2 业务层

通知业务层在 MessageService 类中添加方法:

    //查询某个主题所包含的通知列表
    public List<Message> findNotices(int userId, String topic, int offset, int limit) {
        return messageMapper.selectNotices(userId, topic, offset, limit);
    }

3.1.3 表现层

在 MessageController 类中处理请求:新增方法,显示通知详情

  • 声明访问路径(查询某一类通知,通过路径传入 topic),查询为 GET 请求,传入 Model,向模板传数据
  • 方法中参数引入(使用 @PathVariable 得到路径中的 topic,并且支持分页,传入 Model)
  • 获取当前用户(查询当前用户的详细信息),对分页进行设置(分页条数、路径、行数)
  • 查询某个主题的通知(集合)
  • 申明集合 (存入Map,用来存入聚合数据),实例化新的 ArrayList
  • 如果这个集合非空,聚合数据
  • 遍历集合,为了存聚合数据,实例化 Map,存入通知、内容(content 是一个字符串,进行转义处理,再还原成对象放入 Map)
  • 还原成对象:使用 HtmlUtils 工具对 html 的标签、字符进行转义得到的 content 就没有转义字符,再转化相应的对象,再存入 Map 中(user、entityType、entityId、postId)
  • 补充通知的作者
  • 将 map 存入集合中,最终将数据存入 Model 中
  • 还需要设置已读
  • 最终返回给模板
    //显示通知详情

    //声明访问路径(查询某一类通知,通过路径传入 topic),查询为 GET 请求,传入 Model,向模板传数据
    @RequestMapping(path = "/notice/detail/{topic}", method = RequestMethod.GET)
    //方法中参数引入(使用 @PathVariable 得到路径中的 topic,并且支持分页,传入 Model)
    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());
        //申明集合 (存入Map,用来存入聚合数据),实例化新的 ArrayList
        List<Map<String, Object>> noticeVoList = new ArrayList<>();
        //如果这个集合非空,聚合数据
        if (noticeList != null) {
            //遍历集合,为了存聚合数据,实例化 Map
            for (Message notice : noticeList) {
                Map<String, Object> map = new HashMap<>();
                // 通知
                map.put("notice", notice);
                // 内容:content 是一个字符串,进行转义处理,再还原成对象放入 Map)
                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";
    }

处理模板:点击通知列表访问页面、处理详情页面(notice-detail.html)

3.3 未读消息

未读消息 = 朋友私信 + 系统通知(使用拦截器处理)

在 interceptor 包下创建 MessageInterceptor 类(拦截器)

  • 添加注解 @Component
  • 实现接口 HandlerInterceptor
  • 查新当前用户的未读数量,注入 HostHolder,注入 MessageService
  • 在 调用 Controller 之后模板之前进行拦截,重写 postHandle 方法
package com.example.demo.controller.interceptor;

import com.example.demo.entity.User;
import com.example.demo.service.MessageService;
import com.example.demo.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 未读消息 = 朋友私信 + 系统通知(使用拦截器处理)
 */
@Component
public class MessageInterceptor implements HandlerInterceptor {

    //查新当前用户的未读数量,注入 HostHolder,注入 MessageService
    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private MessageService messageService;

    //在 调用 Controller 之后模板之前进行拦截,重写 postHandle 方法
    @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);
        }
    }
}

在 config 中配置拦截器(WebMvcConfig 类):

  • 注入拦截器,进行拦截
package com.example.demo.config;
import com.example.demo.controller.interceptor.LoginRequiredInterceptor;
import com.example.demo.controller.interceptor.LoginTicketInterceptor;
import com.example.demo.controller.interceptor.MessageInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器
 */

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;

    //登录状态拦截器
    @Autowired
    private LoginRequiredInterceptor loginRequiredInterceptor;

    //未读消息拦截器
    @Autowired
    private MessageInterceptor messageInterceptor;

    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");
    }

}

在 index.html 进行修改(显示消息位置):

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1308606.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mac安装pnpm与使用

1、什么是pnpm&#xff1f; pnpm 全称 performant npm&#xff0c;意思是高性能的 npm。pnpm 由 npm/yarn 衍生而来&#xff0c;解决了 npm/yarn 内部潜在的 bug&#xff0c;极大的优化了性能&#xff0c;扩展了使用场景。被誉为 “最先进的包管理工具”。 2、pnpm特点 速度…

Eolink Apikit 如何进行 Websocket 接口测试?

什么是 websocket &#xff1f; WebSocket 是 HTML5 下一种新的协议&#xff08;websocket协议本质上是一个基于 tcp 的协议&#xff09;。 它实现了浏览器与服务器全双工通信&#xff0c;能更好的节省服务器资源和带宽并达到实时通讯的目的 Websocket 是一个持久化的协议。…

限流常用算法以及基于Sentinel的微服务限流及熔断

一、服务限流的作用及实现 在没有任何保护机制的情况下&#xff0c;如果所有的流量都进入服务器&#xff0c;很可能造成服务器宕机导致整个系统不可用&#xff0c;从而造成巨大的损失。为了保证系统在这些场景中仍然能够稳定运行&#xff0c;就需要采取一定的系统保护策略&…

docker compose部署wordpress

准备机器&#xff1a; 192.168.58.151 &#xff08;关闭防火墙和selinux&#xff09; 安装好docker服务 &#xff08;详细参照&#xff1a;http://t.csdnimg.cn/usG0s 中的国内源安装docker&#xff09; 部署wordpress: 创建目录&#xff1a; [rootdocker ~]# mkdir…

【什么是POI,为什么它会导致内存溢出?】

什么是POI&#xff0c;为什么它会导致内存溢出 什么是POIExcel并没看到的那么小POI的溢出原理 拓展知识几种Workbook格式 什么是POI Apache POl&#xff0c;是一个非常流行的文档处理工具&#xff0c;通常大家会选择用它来处理Excel文件。但是在实际使用的时候经常会遇到内存溢…

VR汽车技术服务虚拟仿真实训平台更好地辅助职业上岗培训

VR汽车虚拟仿真教学软件是一种基于虚拟现实技术的教学辅助工具。它能够模拟真实的汽车环境和操作场景&#xff0c;让学生能够通过虚拟仿真来学习和实践汽车相关知识和技能。 与传统的教学方式相比&#xff0c;VR汽车虚拟仿真教学软件具有更高的视觉沉浸感和互动性&#xff0c;能…

网络时间服务器

本章主要介绍网络时间服务器。 使用chrony配置时间服务器 配置chrony客户端向服务器同步时间 1 时间同步的必要性 一些服务对时间要求非常严格&#xff0c;例如&#xff0c;图所示的由三台服务器搭建的ceph集群。 这三台服务器的时间必须保持一致&#xff0c;如果不一致&#…

https网站连接图标四种状态(安全、没有完全安全、过期和危险)

浏览 Web 时&#xff0c;地址栏中会显示一个图标&#xff0c;指示与要访问的网站的连接的安全性。 此图标可帮助您确定是否可以安全发送和接收网站的信息。 连接会告知发送到站点和从站点发送的信息&#xff08;如密码、地址或信用卡&#xff09;是否安全发送&#xff0c;且无法…

Linux环境下maven的安装

到官网下载maven 步入下面的地址选择合适的maven版本 https://dlcdn.apache.org/maven/ 以笔者为例&#xff0c;选择了3.5.4这个版本 将maven上传到Linux服务器并解压 tar -zxvf apache-maven-3.5.4-bin.tar.gz配置环境变量 我们使用vim编辑profile vim /etc/profile环境…

day03、关系模型之基本概念

关系模型之基本概念 1.关系模型概述1.1 关系模型三要素基本结构&#xff1a;relation/Table基本操作:relation operator 2.什么是关系3.关系模型中的完整性约束 本视频来源于B站&#xff0c;战德臣老师 1.关系模型概述 1.1 关系模型三要素 基本结构&#xff1a;relation/Table…

MATLAB——二维小波的单层分解

%% 学习目标&#xff1a;二维小波的单层分解 %% 二维小波适合图像处理和分析&#xff0c;将图像分解为4个图像 两个维度 低通&#xff0c;高通 clear all; close all; load woman.mat; %% which woman.mat Yind2gray(X,map); %将索引图像转换为灰度图像 [c…

Python机器学习19——常用六种机器学习的异常值监测方法(孤立森林,数据支持描述,自编码器,高斯混合,DBSCAN,LOF)

案例背景 异常值监测是机器学习的一个重要领域&#xff0c;博主以前做预测多&#xff0c;异常值监测涉及得少&#xff0c;但之后的工作可能需要做异常值方面的工作&#xff0c;所以大致总结了一下常用的机器学习来做异常值监测的方法以及代码。 标题的这些机器学习方法基本都…

Github 2023-12-13 开源项目日报 Top10

根据Github Trendings的统计&#xff0c;今日(2023-12-13统计)共有10个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量非开发语言项目5Python项目2TypeScript项目1Jupyter Notebook项目1JavaScript项目1PHP项目1 从零开始构建技术…

matlab操作方法(三)——matlab中的数字图像(读取、显示、保存)

数字图像是二维图像用有限数字数值像素的表示。通常像素在计算机中保存为二维整数数组的光栅图像&#xff0c;这些值经常用压缩格式进行传输和存储。 二值图像&#xff1a;图像中每个像素的亮度值仅可以取自0或1的图像&#xff0c;因此也称为1bit图像 灰度图像&#xff1a;图…

flutter 代码混淆

Flutter 应用混淆&#xff1a; Flutter 应用的混淆非常简单&#xff0c;只需要在构建 release 版应用时结合使用 --obfuscate 和 --split-debug-info 这两个参数即可。 –obfuscate --split-debug-info 用来指定输出调试文件的位置&#xff0c;该命令会生成一个符号映射表。目前…

Axure 9基本元件,表单及表格元件简介,表单案例

目录 一.基本元件 1.元件基本介绍 2.基本元件的使用 二.表单及表格元件 三.表单案例 四.简单简历绘制 一.基本元件 1.元件基本介绍 概述 - 在Axure RP中&#xff0c;元件是**构建原型图的基础模块**。 将元件从元件库里拖拽到画布中&#xff0c;即可添加元件到你的原型…

静态HTTP应用:理解其工作原理与优势

随着互联网的普及和发展&#xff0c;Web应用已经成为人们日常生活和工作中不可或缺的一部分。而静态HTTP应用作为Web应用的一种重要形式&#xff0c;也越来越受到开发者的青睐。本文将带你了解静态HTTP应用的工作原理和优势&#xff0c;让你更好地理解这种应用形式。 一、静态…

【学习笔记】Linux(基础知识)

第1章 Linux概况 1.1 Linux起源 四个重要的支柱: ①Unix操作系统; ②Minix操作系统; ③GNU计划; ④Internet网络。 1. Unix操作系统 UNIX的诞生 1971年,用汇编语言首先开发成功16位UNIX系统 1973年,用C语言重写了UNIX系统 创始人:Ken Thompson & Dennis Ritch…

STM32的DMA的五大问题

1&#xff0c;DMA控制器的内部结构 STM32中的DMA控制器是一种用于在外设和存储器之间传输数据的专用硬件。DMA控制器的内部结构主要包括以下几个关键部分&#xff1a; 通道&#xff1a; DMA控制器可以有多个通道&#xff0c;每个通道独立管理一个数据传输任务。通道的数量取决于…

【解决】Windows 11检测提示电脑不支持 TPM 2.0(注意从DTPM改为PTT)

win11升级&#xff0c;tpm不兼容 写在最前面1. 打开电脑健康状况检查2. 开启tpm3. 微星主板AMD平台开启TPM2.0解决电脑健康状况检查显示可以安装win11&#xff0c;但是系统更新里显示无法更新 写在最前面 我想在台式电脑上用win11的专注模式&#xff0c;但win10不支持 1. 打…