- 私信列表
- 查询当前用户的会话列表, 每个会话只显示一条最新的私信。
- 支持分页显示。
- 私信详情
- 查询某个会话所包含的私信。
- 支持分页显示。
效果图
数据库
首先看一下数据库的设计,私信相当于一个对话功能,那么两个人就组成一个对话,这段对话里有A发给B的消息,有B发给A的消息,那么我们把发送者和接收者的id拼接到一起形成一个会话id,把id小的拼接在前面。还有一种私信是系统通知,那么只需要把from_id指定一个固定的值,这里指定为1。
私信列表
实体类
新建Message实体类,对应数据库字段。
public class Message {
private int id;
private int fromId;
private int toId;
private String conversationId;
private String content;
private int status;
private Date createTime;
// get()\set()方法
....
}
数据层
新建MessageMapper,定义好方法。私信列表需要显示的内容:
1、显示当前用户的所有会话,那么每个会话只显示最新的一条消息就行了
2、还需要显示当前用户一共有多少会话
3、每个会话包含的所有私信
4、每个会话包含的所有私信数量
5、还有每个会话未读的私信数量
package com.neu.langsam.community.dao;
import com.neu.langsam.community.entity.Message;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
@Mapper
@Repository
public interface MessageMapper {
//查询当前用户的会话列表,针对每个会话只返回一条最新的私信
List<Message> selectConversations(int userId,int offset,int limit);
//查询当前用户的会话数量
int selectConversationCount(int userId);
//查询某个会话所包含的私信列表
List<Message> selectLetters(String conversationId,int offset,int limit);
//查询某个会话所包含的私信数量
int selectLetterCount(String conversationId);
//查询未读私信的数量
int selectLetterUnreadCount(int userId,String conversationId);
}
新建message-mapper.xml实现上面的方法。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nowcoder.community.dao.MessageMapper">
<sql id="selectFields">
id, from_id, to_id, conversation_id, content, status, create_time
</sql>
//status=2为已删除,from_id=1是系统通知
//按照对话id进行分组,查出每个组里最大的id,那就是最新的私信
<select id="selectConversations" 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 (from_id = #{userId} or to_id = #{userId})
group by conversation_id
)
order by id desc
limit #{offset}, #{limit}
</select>
<select id="selectConversationCount" resultType="int">
select count(m.maxid) from (
select max(id) as maxid from message
where status != 2
and from_id != 1
and (from_id = #{userId} or to_id = #{userId})
group by conversation_id
) as m
</select>
<select id="selectLetters" resultType="Message">
select <include refid="selectFields"></include>
from message
where status != 2
and from_id != 1
and conversation_id = #{conversationId}
order by id desc
limit #{offset}, #{limit}
</select>
<select id="selectLetterCount" resultType="int">
select count(id)
from message
where status != 2
and from_id != 1
and conversation_id = #{conversationId}
</select>
<select id="selectLetterUnreadCount" resultType="int">
select count(id)
from message
where status = 0
and from_id != 1
and to_id = #{userId}
<if test="conversationId!=null">
and conversation_id = #{conversationId}
</if>
</select>
</mapper>
测试一下写的对不对
@Test
public void testSelectLetters() {
List<Message> list = messageMapper.selectConversations(111, 0, 20);
for (Message message : list) {
System.out.println(message);
}
int count = messageMapper.selectConversationCount(111);
System.out.println(count);
list = messageMapper.selectLetters("111_112", 0, 10);
for (Message message : list) {
System.out.println(message);
}
count = messageMapper.selectLetterCount("111_112");
System.out.println(count);
count = messageMapper.selectLetterUnreadCount(131, "111_131");
System.out.println(count);
}
业务层
新建MessageService,比较简单,之间返回查询到的数据。
package com.neu.langsam.community.service;
import com.neu.langsam.community.dao.MessageMapper;
import com.neu.langsam.community.entity.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MessageService {
@Autowired
private MessageMapper messageMapper;
// 查询当前用户的会话列表, 每个会话只显示一条最新的私信
public List<Message> findConversations(int userId, int offset, int limit) {
return messageMapper.selectConversations(userId, offset, limit);
}
public int findConversationCount(int userId) {
return messageMapper.selectConversationCount(userId);
}
public List<Message> findLetters(String conversationId, int offset, int limit) {
return messageMapper.selectLetters(conversationId, offset, limit);
}
public int findLetterCount(String conversationId) {
return messageMapper.selectLetterCount(conversationId);
}
public int findLetterUnreadCount(int userId, String conversationId) {
return messageMapper.selectLetterUnreadCount(userId, conversationId);
}
}
表现层
要加载私信列表,从hostholder获取当前用户。将会话和私信内容,未读数量等封装到List<map<>>里
package com.neu.langsam.community.controller;
import com.neu.langsam.community.entity.Message;
import com.neu.langsam.community.entity.Page;
import com.neu.langsam.community.entity.User;
import com.neu.langsam.community.service.MessageService;
import com.neu.langsam.community.service.UserService;
import com.neu.langsam.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
public class MessageController {
@Autowired
private MessageService messageService;
@Autowired
private HostHolder hostHolder;
@Autowired
private UserService userService;
//私信列表
@RequestMapping(path = "/letter/list", method = RequestMethod.GET)
public String getLetterList(Model model, Page page) {
User user = hostHolder.getUser();
//分页信息
page.setLimit(5);
page.setPath("/letter/list");
page.setRows(messageService.findConversationCount(user.getId()));
//会话列表
List<Message> conversationList = messageService.findConversations(user.getId(), page.getOffset(), page.getLimit());
List<Map<String, Object>> conversations = new ArrayList<>();
if (conversationList != null) {
for (Message message : conversationList) {
Map<String, Object> map = new HashMap<>();
map.put("conversation", message);
map.put("letterCount", messageService.findLetterCount(message.getConversationId()));
map.put("unreadCount", messageService.findLetterUnreadCount(user.getId(), message.getConversationId()));
int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();
map.put("target", userService.findUserById(targetId));
conversations.add(map);
}
}
model.addAttribute("conversations", conversations);
//查询未读消息数量
int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
model.addAttribute("letterUnreadCount", letterUnreadCount);
return "/site/letter";
}
}
前端
1、index.html
修改“消息”链接
<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser!=null}">
<a class="nav-link position-relative" th:href="@{/letter/list}">消息<span class="badge badge-danger">12</span></a>
</li>
2、letter.html
A、声明thymeleaf模板 ;处理静态资源(.css .js文件)
<!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" href="https://static.nowcoder.com/images/logo_87_87.png"/>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" th:href="@{/css/global.css}" />
<link rel="stylesheet" th:href="@{/css/letter.css}" />
<title>牛客网-私信列表</title>
</head>
<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>
B、处理显示列表
<!-- 私信列表 -->
<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>
C、复用分页
<!-- 分页 -->
<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>
...
</ul>
</nav>
私信详情
然后继续开发会话详情功能。当前端点击会话列表中的某一个时,传入会话id,查询该会话id下的所有私信。
页面上还需要显示“来自XXX的私信”,拆解conversationid,与当前登录 id 不相等的 id 作为 XXX。
变现层
@RequestMapping(path = "/letter/detail/{conversationId}",method = RequestMethod.GET)
public String getLetterDetail(@PathVariable("conversationId") String conversationId,Page page,Model model){
//分页信息
page.setLimit(5);
page.setPath("/letter/detail/"+conversationId);
page.setRows(messageService.findLetterCount(conversationId));
//私信列表
List<Message> letterList=messageService.findLetters(conversationId,page.getOffset(),page.getLimit());
List<Map<String,Object>> letters=new ArrayList<>();
if(letterList!=null){
for (Message message:letterList){
Map<String,Object> map=new HashMap<>();
map.put("letter",message);
map.put("fromUser",userService.findUserById(message.getFromId()));
letters.add(map);
}
}
model.addAttribute("letters",letters);
//私信目标
model.addAttribute("target",getLetterTarget(conversationId));
return "/site/letter-detail";
}
private User getLetterTarget(String conversationId){
String[] ids=conversationId.split("_");
int id0=Integer.parseInt(ids[0]);
int id1=Integer.parseInt(ids[1]);
if (hostHolder.getUser().getId()==id0){
return userService.findUserById(id1);
}else {
return userService.findUserById(id0);
}
}
前端
1、letter.html
处理跳转连接
<!-- 私信列表 -->
...
<a th:href="@{|/letter/detail/${map.conversation.conversationId}|}" th:utext="${map.conversation.content}">米粉车, 你来吧!</a>
2、letter-detail.html
A、声明thymeleaf模板 ;处理静态资源(.css .js文件)
B、处理内容
<!-- 内容 -->
<div class="main">
<div class="container">
<div class="row">
<div class="col-8">
<h6><b class="square"></b> 来自 <i class="text-success" th:utext="${target.username}">落基山脉下的闲人</i> 的私信</h6>
</div>
<div class="col-4 text-right">
// 返回这里 使用了js函数
<button type="button" class="btn btn-secondary btn-sm" onclick="back();">返回</button>
<button type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#sendModal">给TA私信</button>
</div>
</div>
...
<!-- 私信列表 -->
<ul class="list-unstyled mt-4">
<li class="media pb-3 pt-3 mb-2" th:each="map:${letters}">
<a href="profile.html">
<img th:src="${map.fromUser.headerUrl}" class="mr-4 rounded-circle user-header" alt="用户头像" >
</a>
<div class="toast show d-lg-block" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="mr-auto" th:utext="${map.fromUser.username}">落基山脉下的闲人</strong>
<small th:text="${#dates.format(map.letter.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-25 15:49:32</small>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="toast-body" th:utext="${map.letter.content}">
君不见, 黄河之水天上来, 奔流到海不复回!
</div>
</div>
</li>
</ul>
<!-- 分页 -->
<nav class="mt-5" th:replace="index::pagination">
“返回”按钮的路径
<script>
function back() {
location.href = CONTEXT_PATH + "/letter/list";
}
</script>