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

    @Override
    public 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;
    }

    @Override
    public 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 {

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


}

发送系统通知

触发事件

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

处理事件

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

image

编写实体类Event

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<>();//存储事件的数据(额外的有扩展性)

    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);//直接存入data
        return this;
    }
}

  • 给setter加返回值,返回Event对象,好处是之后修改方便;
  • setData传入key和value,不用手动put了,也是方便。

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

@Component
public class EventProducer {

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

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

        //发送站内通知,假设后台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());
        //设置content
        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);
    }

}

调用生产者

评论时

 @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{
    @Autowired
    private LikeService likeService;

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private EventProducer eventProducer;

    @RequestMapping(path = "/like", method = RequestMethod.POST)
    @ResponseBody
    public 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 {
    @Autowired
    private FollowService followService;

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private UserService userService;

    @Autowired
    private EventProducer eventProducer;

    @RequestMapping(path = "/follow", method = RequestMethod.POST)
    @ResponseBody
    public String follow(int entityType, int entityId) {
        User user = hostHolder.getUser();
        followService.follow(user.getId(), entityType, entityId);

        //触发关注事件,关注才通知,取消关注不通知
        //关注只针对人,连接的不是帖子详情, 不需要postId
        Event 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)//异步的
    @ResponseBody
    public 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 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
        <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"));
            //关注类的通知不需要postId

            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(点系统通知能连接到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 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>

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.mfbz.cn/a/588177.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MATLAB 数据导入

MATLAB 数据导入&#xff08;ImportData&#xff09; 在MATLAB中导入数据意味着从外部文件加载数据。该importdata功能允许加载不同格式的各种数据文件。它具有以下五种形式 序号 功能说明 1 A importdata(filename) 从filename表示的文件中将数据加载到数组A中。 2 A i…

Electron+Vue3+Vite+ElectronForge整合-全部ts开发 - 一键启动两个服务 一键打包两个服务

说明 本文介绍一下 Electron Vue3 Vite Electron Forge 的高级整合操作。vue3 : 使用 TS 的语法开发&#xff1b; Electron : 使用 TS 的语法开发。 补充 &#xff1a; 目前Electron的开发还是以JS为主&#xff0c;不过我们可以直接使用TS开发&#xff0c;在执行和打包时&a…

UE5 蓝图入门

基础节点创建&#xff1a; 常量&#xff1a; 按住 1 &#xff0c;点击鼠标左键&#xff0c;创建常量 二维向量&#xff1a; 按住 2 &#xff0c;点击鼠标左键&#xff0c;创建二维向量 三维向量&#xff1a; 按住 3 &#xff0c;点击鼠标左键 按 c 键打出一个注释框 参考视…

C# Winform父窗体打开新的子窗体前,关闭其他子窗体

随着Winform项目越来越多&#xff0c;界面上显示的窗体越来越多&#xff0c;窗体管理变得更加繁琐。有时候我们要打开新窗体&#xff0c;然后关闭多余的其他窗体&#xff0c;这个时候如果一个一个去关闭就会变得很麻烦&#xff0c;而且可能还会出现遗漏的情况。这篇文章介绍了三…

HR招聘测评,如何进行人才测评?

说起“人才测评”几个字&#xff0c;相信大家都不会陌生&#xff0c;很多人&#xff0c;尤其是求职者来说&#xff0c;则更加熟悉。在求职应聘中&#xff0c;已经有越来越多的企业开始采用人才测评进行人员选拔。了解人才测评的含义&#xff0c;知道人才测评如何进行&#xff0…

打破失联困境:门店如何利用AI智能名片B2B2C商城小程序重构与消费者的紧密连接?

在如今这个消费者行为日益碎片化的时代&#xff0c;门店经营者们时常感叹&#xff1a;消费者进店如同一场不期而遇的缘分&#xff0c;然而一旦离开门店&#xff0c;就仿佛消失在茫茫人海中&#xff0c;难以再觅其踪迹。这种“进店靠缘分&#xff0c;离店就失联”的困境&#xf…

本地大语言模型LLM的高效运行专家 | Ollama

Ollama简介 Ollama是一个开源的大型语言模型服务工具&#xff0c;它帮助用户快速在本地运行大模型。通过简单的安装指令&#xff0c;用户可以执行一条命令就在本地运行开源大型语言模型&#xff0c;如Llama 2。Ollama极大地简化了在Docker容器内部署和管理LLM的过程&#xff0…

平面模型上提取凸凹多边形------pcl

平面模型上提取凸凹多边形 pcl::PointCloud<pcl::PointXYZ>::Ptr PclTool::ExtractConvexConcavePolygons(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud) {pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);p…

政安晨:【Keras机器学习示例演绎】(二十八)—— 使用 卷积神经网络与循环神经网络 架构进行视频分类

目录 数据收集 设置 定义超参数 数据准备 序列模型 推论 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: TensorFlow与Keras机器学习实战 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正…

Android Handler用法

Android Handler用法 为什么要设计Handler机制&#xff1f;Handler的用法1、创建Handler2、Handler通信2.1 sendMessage 方式2.2 post 方式 Handler常用方法1、延时执行2、周期执行 HandlerThread用法主线程-创建Handler子线程-创建Handler FAQMessage是如何创建主线程中Looper…

微服务保护和分布式事务(Sentinel、Seata)笔记

一、雪崩问题的解决的服务保护技术了解 二、Sentinel 2.1Sentinel入门 1.Sentinel的安装 &#xff08;1&#xff09;下载Sentinel的tar安装包先 &#xff08;2&#xff09;将jar包放在任意非中文、不包含特殊字符的目录下&#xff0c;重命名为 sentinel-dashboard.jar &…

Docker容器---Harbor私有仓库部署与管理

一、搭建本地私有仓库 1、下载registry镜像 [rootlocalhost ~]#docker pull registry Using default tag: latest latest: Pulling from library/registry 79e9f2f55bf5: Pull complete 0d96da54f60b: Pull complete 5b27040df4a2: Pull complete e2ead8259a04: Pull comp…

vulnhub靶场之FunBox-1

一.环境搭建 1.靶场描述 Boot2Root ! This is a reallife szenario, but easy going. You have to enumerate and understand the szenario to get the root-flag in round about 20min. This VM is created/tested with Virtualbox. Maybe it works with vmware. If you n…

NASA数据集——NASA 标准二级(L2)暗目标(DT)气溶胶产品每 6 分钟在全球范围内对陆地和海洋上空的气溶胶光学厚度(AOT)产品

VIIRS/NOAA20 Dark Target Aerosol 6-Min L2 Swath 6 km 简介 NOAA-20&#xff08;前身为联合极地卫星系统-1&#xff08;JPSS-1&#xff09;&#xff09;--可见红外成像辐射计套件&#xff08;VIIRS&#xff09;NASA 标准二级&#xff08;L2&#xff09;暗目标&#xff08;D…

集合的基本操作

集合&#xff1a; 在java当中&#xff0c;含有着一些不同的存储数据的相关集合。分为单列集合&#xff08;Collection&#xff09;和双列集合(Map)。 Collection 首先学习Collection来进行展示&#xff1a; 以框框为例子&#xff0c;蓝色的代表的是接口&#xff0c;而红色的…

【Linux极简教程】常见实用命令不断更新中......

【Linux极简教程】常见实用命令不断更新中...... 常见问题1.Waiting for cache lock: Could not get lock /var/lib/dpkg/lock. It is held by process xxxx(dpkg) 常见问题 1.Waiting for cache lock: Could not get lock /var/lib/dpkg/lock. It is held by process xxxx(dp…

机器学习:基于Sklearn、XGBoost,使用逻辑回归、支持向量机和XGBClassifier预测股票价格

前言 系列专栏&#xff1a;机器学习&#xff1a;高级应用与实践【项目实战100】【2024】✨︎ 在本专栏中不仅包含一些适合初学者的最新机器学习项目&#xff0c;每个项目都处理一组不同的问题&#xff0c;包括监督和无监督学习、分类、回归和聚类&#xff0c;而且涉及创建深度学…

C语言——队列的实现

队列按照先进先出&#xff08;FIFO&#xff0c;First In First Out&#xff09;的原则管理数据。这意味着最先进入队列的元素会被最先移出&#xff0c;类似于排队等候服务的情况。队列通常有两个主要操作&#xff1a;入队&#xff08;enqueue&#xff09;&#xff0c;将元素添加…

DSP实时分析平台设计方案:924-6U CPCI振动数据DSP实时分析平台

6U CPCI振动数据DSP实时分析平台 一、产品概述 基于CPCI结构完成40路AD输入&#xff0c;30路DA输出的信号处理平台&#xff0c;处理平台采用双DSPFPGA的结构&#xff0c;DSP采用TI公司新一代DSP TMS320C6678&#xff0c;FPGA采用Xilinx V5 5VLX110T-1FF1136芯片&#xff…

《QT实用小工具·五十》动态增删数据与平滑缩放移动的折线图

1、概述 源码放在文章末尾 该项目实现了带动画、带交互的折线图&#xff0c;包含如下特点&#xff1a; 动态增删数值 自适应显示坐标轴数值 鼠标悬浮显示十字对准线 鼠标靠近点自动贴附 支持直线与平滑曲线效果 自定义点的显示类型与大小 自适应点的数值显示位置 根据指定锚点…
最新文章