Day27:阻塞队列、Kafka入门、发送系统通知、显示系统

阻塞队列BlockingQueue

BlockingQueue

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

生产者消费者模式

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

image

(Thread1生产者,Thread2消费者)

实现类

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • PriorityBlockingQueue、SynchronousQueue、DelayQueue等。
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
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> blockingQueue){this.queue = blockingQueue;}@Overridepublic void run() {try {for (int i = 0; i < 100; i++) {Thread.sleep(20);queue.put(i);System.out.println(Thread.currentThread().getName() + "生产:" + i);}} catch (InterruptedException e) {e.printStackTrace();}}
}class Consumer implements Runnable{private BlockingQueue<Integer> queue;public Consumer(BlockingQueue<Integer> blockingQueue){this.queue = blockingQueue;}@Overridepublic void run() {try {while (true) {Thread.sleep(new Random().nextInt(1000));int i = queue.take();System.out.println(Thread.currentThread().getName() + "消费:" + i);}} catch (InterruptedException e) {e.printStackTrace();}}
}

Kafka入门

Kafka简介

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

Kafka特点

  • 高吞吐量、消息持久化(存到硬盘中,顺序读取效率很高)、高可靠性(分布式)、高扩展性(加服务器很简单)。

Kafka术语

  • Broker(服务器)、Zookeeper(管理集群)
  • Topic(发布订阅模式的主题,存放消息的位置)、Partition、Offset
  • 主从复制:Leader Replica(主副本,分布式用于响应请求) 、Follower Replica(从副本,只用于备份)

image

image

安装和启动kafka

  • 使用homebrew进行安装:
brew install kafka
  • 配置zookepper和kafka

在/opt/homebrew/etc/kafka文件夹下

一个是zookeeper.propeties改数据存放路径,一个是server.properties改日志路径

  • 启动zookepper和kafka
brew services start zookeeper
brew services start kafka
  • 创建主题(消息的分类和位置)

kafka-topics --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test

iris@MateBook kafka % kafka-topics --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test
Created topic test.
  • 列出主题

kafka-topics --list --bootstrap-server localhost:9092

iris@MateBook kafka % kafka-topics --list --bootstrap-server localhost:9092 
test
  • 生产消息

kafka-console-producer --broker-list localhost:9092 --topic test

  • 消费消息(重新开一个shell)

kafka-console-consumer --bootstrap-server localhost:9092 --topic test --from-beginning

[外链图片转存中…(img-l01uFwLR-1714057590711)]

Spring整合Kafka

引入依赖

  • spring-kafka

配置Kafka

  • 配置server、consumer

访问Kafka

- 生产者  

kafkaTemplate.send(topic, data);
- 消费者
@KafkaListener(topics = {“test”})
public void handleMessage(ConsumerRecord record) {}

引入依赖

<dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId>
</dependency>

配置Kafka

# Kafka Properties
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-interv  000//自动提交频率

编写测试类

package com.nowcoder.community;import com.newcoder.community.CommunityApplication;
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 = CommunityApplication.class)
public class KafkaTests {@Autowiredprivate KafkaProducer kafkaProducer;@Testpublic void testKafka() {kafkaProducer.sendMessage("test", "你好");kafkaProducer.sendMessage("test", "在吗");try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}}}@Component
class KafkaProducer {@Autowiredprivate 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());}}

发送系统通知

触发事件

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

处理事件

  • 封装事件对象(不单单拼字符串,用一个对象表示)
  • 开发事件的生产者
  • 开发事件的消费者

image

编写实体类Event

public class Event {private String topic;private int userId;private int entityType;//事件发生在哪个实体上private int entityId;//事件的实体的idprivate int entityUserId;//事件的作者private Map<String, Object> data = new HashMap<>();//存储事件的数据(额外的有扩展性)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);//直接存入datareturn this;}
}
  • 给setter加返回值,返回Event对象,好处是之后修改方便;
  • setData传入key和value,不用手动put了,也是方便。

开发生产者(主动生产,什么时候想触发事件就调用)

@Component
public class EventProducer {@Autowiredprivate KafkaTemplate kafkaTemplate;//处理事件public void fireEvent(Event event){//将事件发布到指定的主题kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));}
}

开发消费者(被动接受)

@Component
public class EventConsumer implements CommunityConstant {//需要记日志private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);@Autowiredprivate 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;}//发送站内通知,假设后台id是1,系统用户,原来的from to id没必要,存主题。Message message= new Message();message.setFromId(SYSTEM_USER_ID);message.setToId(event.getEntityUserId());message.setConversationId(event.getTopic());message.setCreateTime(new Date());//设置contentMap<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);}}

调用生产者

评论时

 @RequestMapping(path = "add/{discussPostId}", method = RequestMethod.POST)public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment, Model model) {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;}
  • findCommentById在DAO和commentService中补充。

点赞时

@Controller
public class LikeController implements CommunityConstant{@Autowiredprivate LikeService likeService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate EventProducer eventProducer;@RequestMapping(path = "/like", method = RequestMethod.POST)@ResponseBodypublic String like(int entityType, int entityId, int entityUserId, int 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);}}
  • 点赞才触发,取消赞不触发;
  • 重构了该方法,添加postId参数,之后要改前端。

关注时

@Controller
public class FollowController implements CommunityConstant {@Autowiredprivate FollowService followService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate UserService userService;@Autowiredprivate EventProducer eventProducer;@RequestMapping(path = "/follow", method = RequestMethod.POST)@ResponseBodypublic String follow(int entityType, int entityId) {User user = hostHolder.getUser();followService.follow(user.getId(), entityType, entityId);//触发关注事件,关注才通知,取消关注不通知//关注只针对人,连接的不是帖子详情, 不需要postIdEvent event = new Event().setTopic(TOPIC_FOLLOW).setUserId(hostHolder.getUser().getId()).setEntityType(entityType).setEntityId(entityId).setEntityUserId(entityId);eventProducer.fireEvent(event);return CommunityUtil.getJsonString(0, "已关注!");}//取消关注@RequestMapping(path = "/unfollow", method = RequestMethod.POST)//异步的@ResponseBodypublic String unfollow(int entityType, int entityId) {User user = hostHolder.getUser();followService.unfollow(user.getId(), entityType, entityId);return CommunityUtil.getJsonString(0, "已取消关注!");}
  • 关注只针对人,连接的不是帖子详情, 不需要postId

修改discuss-detail.html和js

Kafka数据锁死怎么办

  • 删除kafka-logs文件

测试

发现报错,空指针异常:

image

原因是之前写AOP记录日志的代码时有一个参数可能会为null,这里加个简单的判断:

@Before("pointcut()")public void before(JoinPoint joinPoint) {//用户[1.2.3.4],在[xxx]时间,访问了[com.newcoder.community.service.xxx()]。logger.debug("before");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));}
  • 原因是这个日志会记录所有调用Service的情况,但只有访问页面的有request,消息队列中消费者使用的service的没有request,request就是null,比较坑。

修复后发现恢复正常了:

image

显示系统通知

通知列表

  • 显示评论、点赞、关注三种类型的通知(只总的显示三类通知,最后只需要显示最后一条通知)

通知详情

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

未读消息

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

通知列表

DAO层

为MessageMapper添加三个方法
 //查询某个主题下最新的通知Message selectLatestNotice(int userId, String topic);// 查询某个主题所包含的通知数量int selectNoticeCount(int userId, String topic);// 查询未读的通知的数量int selectNoticeUnreadCount(int userId, String topic);
修改Message-mapper.xml
<select id="selectLatestNotice" resultType="Message">select <include refid="selectFields"></include>from messagewhere id in (select max(id) from messagewhere status != 2) and from_id = 1and to_id = #{userId}and conversation_id = #{topic}</select><select id="selectNoticeCount" resultType="int">select count(id)from messagewhere status != 2and from_id = 1and to_id = #{userId}and conversation_id = #{topic}</select><select id="selectNoticeUnreadCount" resultType="int">select count(id)from messagewhere status = 0and from_id = 1<if test = "topic!=null">and conversation_id = #{topic}</if>and to_id = #{userId}</select>

Service层

为MessegeService添加以下方法:

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

Controller层

分别查评论、点赞和关注通知,以及总的未读通知数量(之前未读私信数量也一起实现,因为页面时整体展现的)。

@RequestMapping(path = "/notice", method = RequestMethod.GET)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());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_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());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<>();// 封装成集合if (message != null) {messageVO.put("message", message);// 转义(去掉转意字符)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"));//关注类的通知不需要postIdint 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(点系统通知能连接到notice)

<li class="nav-item"><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></li>

notice.html

<!-- 评论类通知 --><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 th:href="@{/notice/detail/comment}">用户<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>

(其他两个一样的)

通知详情

DAO层

// 查询某个主题所包含的通知列表List<Message> selectNotices(int userId, String topic, int offset, int limit);
<select id="selectNotices" resultType="Message">select <include refid="selectFields"></include>from messagewhere status != 2and from_id = 1and to_id = #{userId}and conversation_id = #{topic}order by create_time desclimit #{offset}, #{limit}
</select>

Service层

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

Controller层

@RequestMapping(path = "/notice/detail/{topic}", method = RequestMethod.GET)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跳转到对应的detail:

<a th:href="@{/notice/detail/comment}">用户<i th:utext="${commentNotice.user.username}">nowcoder</i>评论了你的<b th:text="${commentNotice.entityType==1?'帖子':'回复'}">帖子</b> ...</a>

notice-detail.html:

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"><link rel="icon" th:href= "@{/img/captcha.png}" /><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"><link rel="stylesheet" th:href="@{/css/global.css}" /><link rel="stylesheet" th:href="@{/css/letter.css}" /><title>私信列表</title>
</head>
<body><div class="nk-container"><!-- 头部 --><header class="bg-dark sticky-top" th:replace="index::header"><div class="container"><!-- 导航 --><nav class="navbar navbar-expand-lg navbar-dark"><!-- logo --><a class="navbar-brand" href="#"></a><button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><!-- 功能 --><div class="collapse navbar-collapse" id="navbarSupportedContent"><ul class="navbar-nav mr-auto"><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="../index.html">首页</a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link position-relative" href="letter.html">消息<span class="badge badge-danger">12</span></a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="register.html">注册</a></li><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" href="login.html">登录</a></li><li class="nav-item ml-3 btn-group-vertical dropdown"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><img src="http://images.nowcoder.com/head/1t.png" class="rounded-circle" style="width:30px;"/></a><div class="dropdown-menu" aria-labelledby="navbarDropdown"><a class="dropdown-item text-center" href="profile.html">个人主页</a><a class="dropdown-item text-center" href="setting.html">账号设置</a><a class="dropdown-item text-center" href="login.html">退出登录</a><div class="dropdown-divider"></div><span class="dropdown-item text-center text-secondary">nowcoder</span></div></li></ul><!-- 搜索 --><form class="form-inline my-2 my-lg-0" action="search.html"><input class="form-control mr-sm-2" type="search" aria-label="Search" /><button class="btn btn-outline-light my-2 my-sm-0" type="submit">搜索</button></form></div></nav></div></header><!-- 内容 --><div class="main"><div class="container"><div class="position-relative"><!-- 选项 --><ul class="nav nav-tabs mb-3"><li class="nav-item"><a class="nav-link position-relative active" 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" th:href="@{/notice/list}">系统通知<span class="badge badge-danger" th:text="${noticeUnreadCount}" th:if="${noticeUnreadCount!=0}">27</span></a></li></ul><button type="button" class="btn btn-primary btn-sm position-absolute rt-0" data-toggle="modal" data-target="#sendModal">发私信</button></div><!-- 弹出框 --><div class="modal fade" id="sendModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"><div class="modal-dialog modal-lg" role="document"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="exampleModalLabel">发私信</h5><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><div class="modal-body"><form><div class="form-group"><label for="recipient-name" class="col-form-label">发给:</label><input type="text" class="form-control" id="recipient-name"></div><div class="form-group"><label for="message-text" class="col-form-label">内容:</label><textarea class="form-control" id="message-text" rows="10"></textarea></div></form></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button><button type="button" class="btn btn-primary" id="sendBtn">发送</button></div></div></div></div>		<!-- 提示框 --><div class="modal fade" id="hintModal" tabindex="-1" role="dialog" aria-labelledby="hintModalLabel" aria-hidden="true"><div class="modal-dialog modal-lg" role="document"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="hintModalLabel">提示</h5></div><div class="modal-body" id="hintBody">发送完毕!</div></div></div></div>				<!-- 私信列表 --><ul class="list-unstyled"><li class="media pb-3 pt-3 mb-3 border-bottom position-relative" th:each="map:${conversations}"><span class="badge badge-danger" th:text="${map.unreadCount}" th:if="${map.unreadCount!=0}">3</span><a href="profile.html"><img th:src="${map.target.headerUrl}" class="mr-4 rounded-circle user-header" alt="用户头像" ></a><div class="media-body"><h6 class="mt-0 mb-3"><span class="text-success" th:utext="${map.target.username}">落基山脉下的闲人</span><span class="float-right text-muted font-size-12" th:text="${#dates.format(map.conversation.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-28 14:13:25</span></h6><div><a th:href="@{|/letter/detail/${map.conversation.conversationId}|}" th:utext="${map.conversation.content}">米粉车, 你来吧!</a><ul class="d-inline font-size-12 float-right"><li class="d-inline ml-2"><a href="#" class="text-primary"><i th:text="${map.letterCount}">5</i>条会话</a></li></ul></div></div></li></ul><!-- 分页 --><nav class="mt-5" th:replace="index::pagination"><ul class="pagination justify-content-center"><li class="page-item"><a class="page-link" href="#">首页</a></li><li class="page-item disabled"><a class="page-link" href="#">上一页</a></li><li class="page-item active"><a class="page-link" href="#">1</a></li><li class="page-item"><a class="page-link" href="#">2</a></li><li class="page-item"><a class="page-link" href="#">3</a></li><li class="page-item"><a class="page-link" href="#">4</a></li><li class="page-item"><a class="page-link" href="#">5</a></li><li class="page-item"><a class="page-link" href="#">下一页</a></li><li class="page-item"><a class="page-link" href="#">末页</a></li></ul></nav></div></div><!-- 尾部 --><footer class="bg-dark"><div class="container"><div class="row"><!-- 二维码 --><div class="col-4 qrcode"><img src="https://uploadfiles.nowcoder.com/app/app_download.png" class="img-thumbnail" style="width:136px;" /></div><!-- 公司信息 --><div class="col-8 detail-info"><div class="row"><div class="col"><ul class="nav"><li class="nav-item"><a class="nav-link text-light" href="#">关于我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">加入我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">意见反馈</a></li><li class="nav-item"><a class="nav-link text-light" href="#">企业服务</a></li><li class="nav-item"><a class="nav-link text-light" href="#">联系我们</a></li><li class="nav-item"><a class="nav-link text-light" href="#">免责声明</a></li><li class="nav-item"><a class="nav-link text-light" href="#">友情链接</a></li></ul></div></div><div class="row"><div class="col"><ul class="nav btn-group-vertical company-info"><li class="nav-item text-white-50">公司地址:北京市朝阳区大屯路东金泉时代3-2708北京牛客科技有限公司</li><li class="nav-item text-white-50">联系方式:010-60728802(电话)&nbsp;&nbsp;&nbsp;&nbsp;admin@nowcoder.com</li><li class="nav-item text-white-50">牛客科技©2018 All rights reserved</li><li class="nav-item text-white-50">京ICP备14055008-4 &nbsp;&nbsp;&nbsp;&nbsp;<img src="http://static.nowcoder.com/company/images/res/ghs.png" style="width:18px;" />京公网安备 11010502036488</li></ul></div></div></div></div></div></footer></div><script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" crossorigin="anonymous"></script><script th:src="@{/js/global.js}"></script><script th:src="@{/js/letter.js}"></script>
</body>
</html>

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

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

相关文章

firebase:一款功能强大的Firebase数据库安全漏洞与错误配置检测工具

关于firebase firebase是一款针对Firebase数据库的安全工具&#xff0c;该工具基于Python 3开发&#xff0c;可以帮助广大研究人员针对目标Firebase数据库执行安全漏洞扫描、漏洞测试和错误配置检测等任务。 该工具专为红队研究人员设计&#xff0c;请在获得授权许可后再进行安…

制作一个RISC-V的操作系统十六-系统调用

文章目录 用户态和内核态mstatus设置模式切换核心流程封装代码背景解释代码示例解析解释目的 用户态和内核态 mstatus设置 此时UIE设置为1和MPIE为1&#xff0c;MPP设置为0 代表当前权限允许UIE中断发生&#xff0c;并且在第一个mret后将权限恢复为用户态&#xff0c;同时MIE也…

保存钉钉群直播回放下载:直播回放下载步骤详解

今天&#xff0c;我们就来拨开云雾&#xff0c;揭开保存钉钉群直播回放的神秘面纱。教会你们如何下载钉钉群直播回放 首先用到的工具我全部打包好了&#xff0c;有需要的自己下载一下 钉钉群直播回放工具下载&#xff1a;https://pan.baidu.com/s/1WVMNGoKcTwR_NDpvFP2O2A?p…

ASP.NET Core日志管理(Serilog)

.net 6 web api项目添加日志(Serilog)管理,将日志输出到控制台、文件、数据库 Nuget安装:Serilog.AspNetCore 1、用于日志输出到控制台Serilog.Formatting.Compact 2、用于日志输出到SQLServer数据库Serilog.Sinks.MSSqlServer 3、用于日志输出到文件Serilog.Sinks.RollingF…

pycharm中文件误删或者误操作,怎么恢复

恢复pycharm中文件误删或者误操作 恢复方法&#xff1a;1.xxxx.py文件误删2.xxxx.py文件内操作 在日常学习或练手时总会有一些迷之操作&#xff0c;一些文件被误删或者一些文件越改越糟&#xff0c;想要恢复操作之前的文件。 恢复方法&#xff1a; 1.选则误删文件的上级目录&…

mysql 数据转excel文件

mysql 数据转excel文件 缘由 为售后拉取数据&#xff0c;用navicat太墨迹了&#xff0c;用python写一个main方法跑一下&#xff1b; 1.抽取共同方法&#xff0c;封装成传入mysql&#xff0c;直接下载成excel&#xff1b; 2.写入所有sql语句&#xff0c;传入参数&#xff1b; 代…

Unity ParticleSystem 入门

概述 在项目的制作过程成&#xff0c;一定少不了粒子系统的使用吧&#xff0c;如果你想在项目粒子效果&#xff0c;那这部分的内容一定不要错过喔&#xff01;我添加了理解和注释更好理解一点&#xff01; 这次的内容比较多&#xff0c;右侧有目录&#xff0c;可以帮助快速导…

渗透之sql盲注

sql盲注&#xff1a;sql盲注意思是我们并不能在web页面中看到具体的信息&#xff0c;我们只能通过输入的语句的真假来判断。从而拿到我们想要的信息。 我们通常使用ascii值来进行盲注。 目录 手动注入&#xff1a; 脚本注入&#xff1a; 使用sqlmap工具注入&#xff1a; 手…

【面试经典 150 | 分治】排序链表

文章目录 写在前面Tag题目来源解题思路方法一&#xff1a;链表转数组方法二&#xff1a;自顶向下归并排序方法三&#xff1a;自底向上的归并排序 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内…

利用STM32实现语音识别功能

引言 随着物联网和智能设备的普及&#xff0c;语音识别技术正逐渐成为用户交互的主流方式之一。 STM32微控制器具备处理高效率语音识别算法的能力&#xff0c;使其成为实现低成本、低功耗语音交互系统的理想选择。 本教程将介绍如何在STM32平台上开发和部署一个基础的语音识…

Java进阶-Java Stream API详解与使用

本文全面介绍了 Java Stream API 的概念、功能以及如何在 Java 中有效地使用它进行集合和数据流的处理。通过详细解释和示例&#xff0c;文章展示了 Java Stream API 在简化代码、提高效率以及支持函数式编程方面的优势。文中还比较了 Java Stream API 与其他集合处理库的异同&…

【记录】Springboot项目集成docker实现一键部署

公司管理平台完成后&#xff0c;为了方便其他不懂开发的同事部署和测试&#xff0c;集成docker进行一键部署&#xff0c;也为后面自动化部署做准备。本文做个简单记录。 1、安装docker yum install https://download.docker.com/linux/fedora/30/x86_64/stable/Packages/cont…

Ubuntu GUI使用Root用户登录指南

Ubuntu GUI使用Root用户登录指南 一、前言 默认情况下&#xff0c;Ubuntu 禁用了 root 账户&#xff0c;我们必须使用 sudo 命令来执行任何需要 root 权限的任务&#xff0c;比如像这样删除一个系统配置文件&#xff08;操作危险&#xff0c;请勿尝试&#xff09;&#xff1a;…

是机遇?是未来?拥抱 AI Agent ,拥抱 AI 2.0时代~

✍️ 作者&#xff1a;哈哥撩编程&#xff08;视频号同名&#xff09; 博客专家全国博客之星第四名超级个体COC上海社区主理人特约讲师谷歌亚马逊演讲嘉宾科技博主极星会首批签约作者 &#x1f3c6; 推荐专栏&#xff1a; &#x1f3c5; 程序员&#xff1a;职场关键角色通识宝…

Devops部署maven项目

这里讲下应用k8s集群devops持续集成部署maven项目的流程。 failed to verify certificate: x509: certificate signed by unknown authority 今天在执行kubectl get nodes的时候报的证书验证问题&#xff0c;看了一圈首次搭建k8s的都是高频出现的问题。 couldn’t get curren…

代码随想录-二叉树(节点)

目录 104. 二叉树的最大深度 题目描述&#xff1a; 输入输出描述&#xff1a; 思路和想法&#xff1a; 111. 二叉树的最小深度 题目描述&#xff1a; 输入输出描述&#xff1a; 思路和想法&#xff1a; 222. 完全二叉树的节点个数 题目描述&#xff1a; ​输入输出描…

Flutter笔记:Widgets Easier组件库(3)使用按钮组件

Flutter笔记 Widgets Easier组件库&#xff08;3&#xff09;&#xff1a;使用按钮组件 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddre…

IDEA 开发找到 java-web 发布到 tomcat 的路径

使用 IDEA 开发 java web 应用&#xff0c;有没有遇到需要找到 tomcat 路径的问题 为什么要找 tomcat 路径呢&#xff1f; 拿我的项目来举例&#xff0c;有统一的线上线下 logback.xml 配置&#xff0c;配置时业务、框架日志输出到 file&#xff0c;少量的启动日志输出到 con…

mysql从入门到起飞+面试基础题

mysql基础 MySQL基础 企业面试题1 代码 select m.id,m.num from ( select t.id as id,count(1) num from ( select ra.requester_id as id from RequestAccepted raunion all select ra.accepter_id as id from RequestAccepted ra ) t group by t.id ) m group by id ord…

maven插件:dockerfile-maven-plugin和docker-maven-plugin

Maven插件dockerfile-maven-plugin和docker-maven-plugin都是为Java开发人员提供了一种便捷的方式&#xff0c;通过Maven构建流程来自动化创建、管理和推送Docker镜像。虽然它们有着相似的目标&#xff0c;即集成Docker与Maven项目&#xff0c;但这两个插件在实现细节、功能侧重…