BlockingQueue
生产者消费者模式
实现类
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();
}
}
}
运行 main 方法查看结果
Kafka简介
Kafka特点
Kafka术语
小知识:对硬盘的顺序读取,速度是很快的,甚至要快于对内存的随机读取,这也是 kafka 既能持久化又能高性能的原因
【配置】
将素材中的 kafka_2.12-2.2.0.tgz
解压到某个目录即可
配置一下 E:\kafka_2.12-2.2.0\config 目录下的 zookeeper.properties
dataDir=d:/work/data/zookeeper
配置 server.properties
60行
log.dirs=d:/work/data/kafka-logs
【启动】
打开一个 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
再打开一个 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
自动创建了对应的文件夹
再打开一个 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
再打开一个 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
关于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"
实例删除成功。
引入依赖
配置Kafka
访问Kafka
kafkaTemplate.send(topic, data);
@KafkaListener(topics = {"test"})
public void handleMessage(ConsumerRecord record) {}
pom.xml
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
E:\kafka_2.12-2.2.0\config 下的 consumer.properties
改一下 group.id
group.id=community-consumer-group
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
新建测试类
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());
}
}
先启动 zookeeper,再启动 kafka,最后执行测试方法看看能不能读取到信息
触发事件
处理事件
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;
}
}
新建一个包 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));
}
}
消费者
CommunityConstant
//主题:评论
String TOPIC_COMMENT = "comment";
//主题:点赞
String TOPIC_LIKE = "like";
//主题:关注
String TOPIC_FOLLOW = "follow";
//系统用户ID
int SYSTEM_USER_ID = 1;
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);
}
}
CommentMapper 补充方法
Comment selectCommentById(int id);
comment-mapper.xml 实现该方法
<select id="selectCommentById" resultType="Comment">
select <include refid="selectFields"></include>
from comment
where id = #{id}
</select>
CommentService 增加方法
public Comment findCommentById(int id) {
return commentMapper.selectCommentById(id);
}
CommentController
public class CommentController implements CommunityConstant {
@Resource
private EventProducer eventProducer;
@Resource
private DiscussPostService discussPostService;
完善 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;
}
LikeController
public class LikeController implements CommunityConstant {
@Resource
private EventProducer eventProducer;
完善 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);
}
FollowController
@Resource
private EventProducer eventProducer;
完善 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, "已关注!");
}
discuss-detail.html
87行
<a href="javascript:;" th:onclick="|like(this, 1, ${post.id}, ${post.userId}, ${post.id});|" class="text-primary">
139行
<a href="javascript:;" th:onclick="|like(this, 2, ${cvo.comment.id},${cvo.comment.userId}, ${post.id})|" class="text-primary">
165行
<a href="javascript:;" th:onclick="|like(this, 2, ${rvo.reply.id}, ${rvo.reply.userId}, ${post.id})|" class="text-primary">
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);
}
}
);
}
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));
}
启动程序,测试(确保 zookeeper 和 kafka 已经提前启动了),登陆之后随便找个帖子进行回帖,给评论点赞、关注某个人
去数据库 message 表中查看是否有数据
通知列表
通知详情
未读消息
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);
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>
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);
}
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";
}
getLetterList 方法的 return 之前加上两行:
int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null);
model.addAttribute("noticeUnreadCount", noticeUnreadCount);
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>
notice.html
2行
<html lang="en" xmlns:th="http://www.thymeleaf.org">
8-9行
<link rel="stylesheet" th:href="@{/css/global.css}" />
<link rel="stylesheet" th:href="@{/css/letter.css}" />
15行
<!-- 头部 -->
<header class="bg-dark sticky-top" th:replace="index::header">
197行
<script th:src="@{/js/global.js}"></script>
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>
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>
MessageMapper
//查询某个主题所包含的通知列表
List<Message> selectNotices(@Param("userId") int userId,
@Param("topic") String topic,
@Param("offset") int offset,
@Param("limit") 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>
MessageService
public List<Message> findNotices(int userId, String topic, int offset, int limit) {
return messageMapper.selectNotices(userId, topic, offset, limit);
}
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";
}
notice.html
94行
<a th:href="@{/notice/detail/comment}">
116行
<a th:href="@{/notice/detail/like}">
138行
<a th:href="@{/notice/detail/follow}">
notice-detail.html
2行
<html lang="en" xmlns:th="http://www.thymeleaf.org">
8-9行
<link rel="stylesheet" th:href="@{/css/global.css}" />
<link rel="stylesheet" th:href="@{/css/letter.css}" />
15行
<!-- 头部 -->
<header class="bg-dark sticky-top" th:replace="index::header">
删除 91-150行的 li 标签
175-176行
<script th:src="@{/js/global.js}"></script>
<script th:src="@{/js/letter.js}"></script>
70行
<button type="button" class="btn btn-secondary btn-sm" onclick="back();">返回</button>
在最后面多加一个 script 标签
<script>
function back() {
location.href = CONTEXT_PATH + "/notice/list";
}
</script>
76行
<li class="media pb-3 pt-3 mb-2" th:each="map:${notices}">
77行
<img th:src="${map.fromUser.headerUrl}" class="mr-4 rounded-circle user-header" alt="系统图标">
80行
<strong class="mr-auto" th:utext="${map.fromUser.username}">落基山脉下的闲人</strong>
81行
<small th:text="${#dates.format(map.notice.createTime, 'yyyy-MM-dd HH:mm:ss')}">2019-04-25 15:49:32</small>
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>
110行
<!-- 分页 -->
<nav class="mt-5" th:replace="index::pagination">
新建一个拦截器
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);
}
}
}
配置它
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");
}
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>
提前开启 zookeeper 和 kafka,启动项目,测试,登录 niuke,给 hahaha 关注,点赞、评论,再登录 hahaha 查看消息