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

devtools/2024/9/23 10:29:24/

阻塞队列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_106">安装和启动kafka

  • 使用homebrew进行安装:
brew install kafka

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

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

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

引入依赖

配置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数据锁死怎么办

测试

发现报错,空指针异常:

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.ppmy.cn/devtools/22784.html

相关文章

PDF高效编辑器,一键转换将PDF文件转换成HTML文件,开启文件处理新篇章!

文件格式的转换与处理已成为我们日常工作中不可或缺的一部分。为了满足广大用户对高效、便捷文件处理的需求&#xff0c;我们荣幸地推出了全新的PDF高效编辑器——PDF到HTML一键转换工具&#xff01;这款工具将为您带来前所未有的文件处理体验&#xff0c;让您轻松驾驭文件格式…

不同状态空间模型的实验对比(二)

对五个下游任务进行了实验比较&#xff0c;包括单/多标签分类、视觉对象跟踪、像素级分割、图像到文本生成和人/车辆再识别。 论文&#xff1a;https://arxiv.org/abs/2404.09516 作者单位&#xff1a;安徽大学、哈尔滨工业大学、北京大学更多相关工作将在以下GitHub上不断更新…

All in One mini主机搭建全屋主路由方案----自己实现自己的路由器,实现路由器自由!

1 接线 首先&#xff0c;需要保证家里当前状态是有网的状态&#xff08;路由器有网并正常工作&#xff09; 将鼠标键盘接在mini主机的USB口&#xff0c;HDMP/DP/VGA等接上显示器。从路由器的lan口接一根网线出来接在mini主机的ETH0上&#xff0c;接在mini主机上保证mini主机在…

短期动态IP介绍

IP代理作为一门新兴的网络技术&#xff0c;备受市场与用户的青睐。主要是帮助用户解除地域限制&#xff0c;登录海外网站&#xff0c;浏览海外用户信息。 代理商实际上是依托于海外服务器&#xff0c;将国内IP地址通过代理服务器隐匿后替换为海外IP地址&#xff0c;再由代理服…

每天一个数据分析题(二百九十七)

帮助了解当前每个维度项的业务行为结果的整体情况的是哪种指标计算方法&#xff1f; A. 常规求和 B. 累计求和 C. 常规计数 D. 非重复计数 题目来源于CDA模拟题库 点击此处获取答案 cda数据分析考试&#xff1a;点击进入

百度竞价开户详解:步骤、优势与注意事项

随着互联网的普及&#xff0c;网络营销已成为企业不可或缺的一部分。其中&#xff0c;百度竞价作为一种高效的网络推广方式&#xff0c;受到了越来越多企业的青睐。本文将详细介绍百度竞价开户的流程、优势以及注意事项&#xff0c;帮助企业更好地利用这一工具提升品牌知名度和…

chrome 安装devtools

chrome 安装devtools 下载安装 链接&#xff1a;https://github.com/vuejs/devtools 选择对应版本&#xff1a; 安装yarn 下载 npm install -g yarn --registryhttps://registry.npmmirror.com进入下载的目录安装依赖 yarn install --registryhttps://registry.npmmirror.…

.2700勒索病毒,网络安全面临的新威胁

一、.2700勒索病毒的定义与危害 .2700勒索病毒是一种通过锁定被感染者计算机系统或文件&#xff0c;并施以敲诈勒索的新型计算机病毒。它通过计算机漏洞、邮件投递、恶意木马程序、网页后门等方式进行传播。一旦感染&#xff0c;磁盘上几乎所有格式的文件都会被加密&#xff0c…