目录
1.私信列表
1.1 数据访问层
1.2 业务层
1.3 表现层
1.4 私信详情
2.发送列表
2.1 数据访问层
2.2 业务层
2.3 表现层
2.4 设置已读状态
1.私信列表
- 私信列表:查询当前用户的会话列表,每个会话只显示一条最新的私信、支持分页列表
- 私信详情:查询某个会话所包含的私信、支持分页显示
在 entity 包下新建 Message 实体类:
package com.example.demo.entity;
import java.util.Date;
/**
* 私信列表实体类
*/
public class Message {
private int id;
private int fromId;
private int toId;
private String conversationId;//会话 id
private String content;
private int status;
private Date createTime;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getFromId() {
return fromId;
}
public void setFromId(int fromId) {
this.fromId = fromId;
}
public int getToId() {
return toId;
}
public void setToId(int toId) {
this.toId = toId;
}
public String getConversationId() {
return conversationId;
}
public void setConversationId(String conversationId) {
this.conversationId = conversationId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", fromId=" + fromId +
", toId=" + toId +
", conversationId='" + conversationId + '\'' +
", content='" + content + '\'' +
", status=" + status +
", createTime=" + createTime +
'}';
}
}
1.1 数据访问层
在 dao 包下创建 MessageMapper 接口:
- 查询当前用户的会话列表,针对每个会话只返回一条最新的私信(根据用户id查询,并且支持分页功能)
- 查询当前用户的会话数量(根据用户 id 查询)
- 查询某个会话所包含的私信列表(根据会话 id 查询,并且支持分页功能)
- 查询某个会话所包含的私信数量(根据会话 id 查询)
- 查询未读私信的数量(根据用户 id 和 会话 id查询,会话 id 动态拼接)
package com.example.demo.dao;
import com.example.demo.entity.Message;
import java.util.List;
/**
* 私信列表数据访问层逻辑
*/
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);
}
在 resources 资源文件下的 mapper 中添加配置文件message-mapper:
<?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.example.demo.dao.MessageMapper">
<!---私信列表配置文件-->
<!--公用 sql-->
<sql id="selectFields">
id, from_id, to_id, conversation_id, content, status, create_time
</sql>
<!--查询当前用户的会话列表-->
<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>
1.2 业务层
在 service 包下新建 MessageService 类(业务查询):
- 定义五个方法,调用 Mapper,实现业务即可,注入 MessageMapper
package com.example.demo.service;
import com.example.demo.dao.MessageMapper;
import com.example.demo.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);
}
}
1.3 表现层
在 controller 类下新建 MessageController 类处理私信详情请求:
- 添加处理私信列表的方法:声明私信路径,请求为 GET 请求;传入 Model、分页 Page
- 实现查询功能,注入 MessageService
- 查询当前用户私信用户,注入 HostHolder
- 设置分页信息(每页显示多少条数据、分页路径、一共多少条数据——查询当前会话的数据、传入 userId,需要获取 User)
- 查询会话列表得到数据
- 显示未读数量、每一次会话的未读数量、会话中包含多少条数据
- 声明集合,用 Map 封装,将多个数据存入 Map 中
- 遍历列表,新建 HashMap 重构数据:存入遍历的每一次数据、存入未读详细数据(用户 id、会话 id)、存入多少条数量(会话 id)、显示当前用户相对应的用户头像
- 寻找目标 id:如果当前用户是消息的发起者,目标就是接收人;如果当前对象是消息的接收者,目标就是发起者
- 将目标对象存入 HashMap 中(注入 UserService)
- 将得到的 HashMap 存入集合当中,最后传入模板中
- 查询未读消息数量(查询整个用户所有的未读消息数量),传入 Model 中显示,返回 Model 路径(/site/letter)
package com.example.demo.controller;
import com.example.demo.entity.Message;
import com.example.demo.entity.Page;
import com.example.demo.entity.User;
import com.example.demo.service.MessageService;
import com.example.demo.service.UserService;
import com.example.demo.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 {
//实现查询功能,注入 MessageService
@Autowired
private MessageService messageService;
//查询当前用户私信用户,注入 HostHolder
@Autowired
private HostHolder hostHolder;
@Autowired
private UserService userService;
//添加处理私信列表的方法
@RequestMapping(path = "/letter/list", method = RequestMethod.GET)
public String getLetterList(Model model, Page page) {
//设置分页信息(每页显示多少条数据、分页路径、一共多少条数据——查询当前会话的数据、传入 userId,需要获取 User)
User user = hostHolder.getUser();
page.setLimit(5);
page.setPath("/letter/list");
page.setRows(messageService.findConversationCount(user.getId()));
//查询会话列表得到数据(显示未读数量、每一次会话的未读数量、会话中包含多少条数据)
//声明集合,用 Map 封装,将多个数据存入 Map 中
List<Message> conversationList = messageService.findConversations(
user.getId(), page.getOffset(), page.getLimit());
List<Map<String, Object>> conversations = new ArrayList<>();
//遍历列表,新建 HashMap 重构数据:存入遍历的每一次数据、存入未读详细数据(用户 id、会话 id)
// 、存入多少条数量(会话 id)、显示当前用户相对应的用户头像
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()));
//寻找目标 id:如果当前用户是消息的发起者,目标就是接收人;如果当前对象是消息的接收者,目标就是发起者
int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();
//将目标对象存入 HashMap 中(注入 UserService)
map.put("target", userService.findUserById(targetId));
//将得到的 HashMap 存入集合当中
conversations.add(map);
}
}
//最后传入模板中
model.addAttribute("conversations", conversations);
// 查询未读消息数量(查询整个用户所有的未读消息数量),传入 Model 中显示,返回 Model 路径(/site/letter)
int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
model.addAttribute("letterUnreadCount", letterUnreadCount);
return "/site/letter";
}
}
1.4 私信详情
在 controller 类下 MessageController 类添加私信详情方法:
- 声明访问路径(点击按钮,查看会话详情,需要传入会话 id,在路径中包含会话 id),查询方法为 GET 请求
- 方法中获取路径中的会话 id 参数,并且支持分页,把模板传入
- 设置分页信息(每页显示多少条数据、分页路径、行数)
- 定义私信列表:分页查询(会话 id、分页),用集合表示,Message 封装
- 对发信人进行补充:声明集合,存放 Map
- 遍历集合:实例化 HashMap,然后放入私信,把 fromId 转化为 fromUser,将 Map 放入集合中
- 将集合发送给模板
- 补充上述的来自某个人的私信(当前登陆用户与之对话目标的名字,查询当前登录与之对话的用户显示出来)(创建私有方法传入 会话 id,拆封会话 id,与当前用户进行判断)
- 查询私信目标,返回模板
//添加私信详情方法
//声明访问路径(点击按钮,查看会话详情,需要传入会话 id,在路径中包含会话 id),查询方法为 GET 请求
@RequestMapping(path = "/letter/detail/{conversationId}", method = RequestMethod.GET)
//方法中获取路径中的会话 id 参数,并且支持分页,把模板传入
public String getLetterDetail(@PathVariable("conversationId") String conversationId, Page page, Model model) {
//设置分页信息(每页显示多少条数据、分页路径、行数)
page.setLimit(5);
page.setPath("/letter/datail/" + conversationId);
page.setRows(messageService.findLetterCount(conversationId));
//定义私信列表:分页查询(会话 id、分页),用集合表示,Message 封装
List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
//对发信人进行补充:声明集合,存放 Map
List<Map<String, Object>> letters = new ArrayList<>();
//遍历集合:实例化 HashMap,然后放入私信,把 fromId 转化为 fromUser,将 Map 放入集合中
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";
}
//创建私有方法传入 会话 id,拆封会话 id,与当前用户进行判断
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);
}
}
2.发送列表
- 发送私信:采用异步的方式发送私信、发送成功后刷新私信列表
- 设置已读:访问私信详情时,将显示的私信设置为已读状态
2.1 数据访问层
在 dao 包下的 MessageMapper 中添加消息、修改消息的状态的方法:
//新增消息
int insertMessage(Message message);
//修改消息的状态:修改的状态为多个id,使用集合
int updateStatues(List<Integer> ids, int status);
在配置文件 message.xml 实现SQL:
<!--增加消息公共 sql-->
<sql id="insertFields">
from_id, to_id, conversation_id, content, status, create_time
</sql>
<!--增加消息-->
<insert id="insertMessage" parameterType="Message" keyProperty="id">
insert into message(<include refid="insertFields"></include>)
values(#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})
</insert>
<!--修改消息的状态-->
<update id="updateStatus">
update message set status = #{status}
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
2.2 业务层
在 MessageService 中新增消息方法:
- 过滤标签
- 过滤敏感词
- 在创建一个方法:把消息变成已读(支持一次读取多条数据,使用集合)
//注入敏感词
@Autowired
private SensitiveFilter sensitiveFilter;
//增加消息的方法
public int addMessage(Message message) {
message.setContent(HtmlUtils.htmlEscape(message.getContent()));
message.setContent(sensitiveFilter.filter(message.getContent()));
return messageMapper.insertMessage(message);
}
//创建一个方法:把消息变成已读(支持一次读取多条数据,使用集合)
public int readMessage(List<Integer> ids) {
return messageMapper.updateStatus(ids, 1);
}
2.3 表现层
在 MessageController 中去写增加消息的请求方法:
- 声明访问路径,请求方式为 POST 请求,提交数据
- 异步请求添加 @ResponseBody
- 页面表单需要传入 发私信给谁(接收人用户名)、私信内容
- 通过用户名查询用户得到 id,在 Userservice 添加查询方法:
//通过用户名查询用户得到 id
public User findUserByName(String username) {
return userMapper.selectByName(username);
}
- 利用当前数据构造要插入的对象(从当前用户取、发送给谁、会话 id(两个id使用 _ 拼接),id 小的在前面、内容、当前时间)
- 做插入调用 messageService
- 如果没报错,给页面返回状态0
- 如果报错,后面统一处理异常
//发送私信的方法
@RequestMapping(path = "/letter/send", method = RequestMethod.POST)
@ResponseBody //异步请求
//页面表单需要传入 发私信给谁(接收人用户名)、私信内容
public String sendLetter(String toName, String content) {
User target = userService.findUserByName(toName);
if (target == null) {
return CommunityUtil.getJSONString(1, "目标用户不存在!");
}
//利用当前数据构造要插入的对象(从当前用户取、发送给谁、会话 id(两个id使用 _ 拼接),id 小的在前面、内容、当前时间)
Message message = new Message();
message.setFromId(hostHolder.getUser().getId());
message.setToId(target.getId());
if (message.getFromId() < message.getToId()) {
message.setConversationId(message.getFromId() + "_" + message.getToId());
} else {
message.setConversationId(message.getToId() + "_" + message.getFromId());
}
message.setContent(content);
message.setCreateTime(new Date());
//做插入调用 messageService
messageService.addMessage(message);
//如果没报错,给页面返回状态0;如果报错,后面统一处理异常
return CommunityUtil.getJSONString(0);
}
前端页面 index.xml:
<!-- 内容 -->
<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">
<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>
<!-- 弹出框 -->
<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">×</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" th:value="${target.username}">
</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 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">
<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>
前端页面 letter.js:
$(function(){
$("#sendBtn").click(send_letter);
$(".close").click(delete_msg);
});
function send_letter() {
$("#sendModal").modal("hide");
var toName = $("#recipient-name").val();
var content = $("#message-text").val();
$.post(
CONTEXT_PATH + "/letter/send",
{"toName":toName,"content":content},
function(data) {
data = $.parseJSON(data);
if(data.code == 0) {
$("#hintBody").text("发送成功!");
} else {
$("#hintBody").text(data.msg);
}
$("#hintModal").modal("show");
setTimeout(function(){
$("#hintModal").modal("hide");
location.reload();
}, 2000);
}
);
}
function delete_msg() {
// TODO 删除数据
$(this).parents(".media").remove();
}
当 xiaowen 看数据时,将显示的私信设置为已读状态
2.4 设置已读状态
在 私信详情方法 补充将私信列表中未读消息提取出来,自动设置为已读:
- 在集合中提取消息,补充方法:传入私信列表、实例化集合
- 遍历数据,判断当前用户是否为接收者,是才能去变为已读并且消息的状态是不是0
//添加私信详情方法
//声明访问路径(点击按钮,查看会话详情,需要传入会话 id,在路径中包含会话 id),查询方法为 GET 请求
@RequestMapping(path = "/letter/detail/{conversationId}", method = RequestMethod.GET)
//方法中获取路径中的会话 id 参数,并且支持分页,把模板传入
public String getLetterDetail(@PathVariable("conversationId") String conversationId, Page page, Model model) {
//设置分页信息(每页显示多少条数据、分页路径、行数)
page.setLimit(5);
page.setPath("/letter/detail/" + conversationId);
page.setRows(messageService.findLetterCount(conversationId));
//定义私信列表:分页查询(会话 id、分页),用集合表示,Message 封装
List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
//对发信人进行补充:声明集合,存放 Map
List<Map<String, Object>> letters = new ArrayList<>();
//遍历集合:实例化 HashMap,然后放入私信,把 fromId 转化为 fromUser,将 Map 放入集合中
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));
// 设置已读
List<Integer> ids = getLetterIds(letterList);
if (!ids.isEmpty()) {
messageService.readMessage(ids);
}
//查询私信目标,返回模板
return "/site/letter-detail";
}
//在集合中提取消息
private List<Integer> getLetterIds(List<Message> letterList) {
List<Integer> ids = new ArrayList<>();
if (letterList != null) {
for (Message message : letterList) {
if (hostHolder.getUser().getId() == message.getToId() && message.getStatus() == 0) {
ids.add(message.getId());
}
}
}
return ids;
}