1. 过滤敏感词
- 前缀树
- 名称:Title、字典树、查找树
- 应用特点:查找效率高,消耗内存大
- 应用:字符串检索、字符串检索、词频统计、字符串排序等‘’
- 敏感词过滤
- 定义前缀树
- 根据敏感词,初始化前缀树
- 编写过滤敏感词的方法
1.1 定义敏感词
在resources目录下,新建一个sensitive-words.txt文件,里面的词便是要过滤的敏感词
1.2 定义前缀树
在util包下,新建一个SensitiveFilter类,用来过滤敏感词,为了复用方便,将此类托管给Spring容器,即加@Component注解。
定义前缀树:因为不会被外界访问,所以在SensitiveFilter类定义内部类,用来定义前缀树
package com.ateam.community.util;
/**
* @author wsh
* @date 2021-11-23 16:28
* @description
*/
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
// 前缀树
private class TrieNode {
// 关键词结束的标识
private boolean isKeyWordEnd = false;
// 子节点(key是下级字符,value是下级节点)
private Map<Character, TrieNode> subNode = new HashMap<>();
public boolean isKeyWordEnd() {
return isKeyWordEnd;
}
public void setKeyWordEnd(boolean keyWordEnd) {
isKeyWordEnd = keyWordEnd;
}
// 添加子节点
public void addSubNode(Character c, TrieNode node) {
subNode.put(c, node);
}
// 获取子节点
public TrieNode getSubNode(Character c) {
return subNode.get(c);
}
}
}
1.3 根据敏感词初始化前缀树
package com.ateam.community.util;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiPredicate;
/**
* @author wsh
* @date 2021-11-23 16:28
* @description
*/
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
// 根节点
private TrieNode rootNode = new TrieNode();
@PostConstruct
public void init() {
try (
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(is));
) {
String keyWord;
while ((keyWord = br.readLine()) != null) {
// 添加到前缀树
this.addKeyWord(keyWord);
}
} catch (Exception e) {
logger.error("加载敏感词文件失败:" + e.getMessage());
}
}
// 将一个敏感词添加到前缀树中
private void addKeyWord(String keyWord) {
TrieNode temNode = rootNode;
for (int i = 0; i < keyWord.length(); i++) {
char c = keyWord.charAt(i);
TrieNode subNode = temNode.getSubNode(c);
if (subNode == null) {
// 初始化子节点
subNode = new TrieNode();
temNode.addSubNode(c,subNode);
}
// 指针 指向 子节点,进入下一轮循环
temNode = subNode;
// 设置结束标识
if (i == keyWord.length() - 1) {
temNode.setKeyWordEnd(true);
}
}
}
// 前缀树
private class TrieNode {
// 关键词结束的标识
private boolean isKeyWordEnd = false;
// 子节点(key是下级字符,value是下级节点)
private Map<Character, TrieNode> subNode = new HashMap<>();
public boolean isKeyWordEnd() {
return isKeyWordEnd;
}
public void setKeyWordEnd(boolean keyWordEnd) {
isKeyWordEnd = keyWordEnd;
}
// 添加子节点
public void addSubNode(Character c, TrieNode node) {
subNode.put(c, node);
}
// 获取子节点
public TrieNode getSubNode(Character c) {
return subNode.get(c);
}
}
}
1.4 过滤敏感词的方法
在上述类中,新增加一个方法filter
package com.ateam.community.util;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiPredicate;
/**
* @author wsh
* @date 2021-11-23 16:28
* @description
*/
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
//替换符
private static final String REPLACEMENT = "****";
// 根节点
private TrieNode rootNode = new TrieNode();
@PostConstruct
public void init() {
try (
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(is));
) {
String keyWord;
while ((keyWord = br.readLine()) != null) {
// 添加到前缀树
this.addKeyWord(keyWord);
}
} catch (Exception e) {
logger.error("加载敏感词文件失败:" + e.getMessage());
}
}
// 将一个敏感词添加到前缀树中
private void addKeyWord(String keyWord) {
TrieNode temNode = rootNode;
for (int i = 0; i < keyWord.length(); i++) {
char c = keyWord.charAt(i);
TrieNode subNode = temNode.getSubNode(c);
if (subNode == null) {
// 初始化子节点
subNode = new TrieNode();
temNode.addSubNode(c,subNode);
}
// 指针 指向 子节点,进入下一轮循环
temNode = subNode;
// 设置结束标识
if (i == keyWord.length() - 1) {
temNode.setKeyWordEnd(true);
}
}
}
/**
* 过滤敏感词
* @param text
* @return 过滤后的文本
*/
public String filter(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
// 指针1
TrieNode temNode = rootNode;
// 指针2
int begin = 0;
// 指针3
int position = 0;
// 结果
StringBuilder sb = new StringBuilder();
while (position < text.length()) {
char c = text.charAt(position);
// 跳过符号
if (isSymbol(c)) {
// 若指针在根节点,将此符号计入结果,让指针2向下走一步
if (temNode == rootNode) {
sb.append(c);
begin++;
}
// 无论符号在开头或中间,指针3都向下走一步
position++;
continue;
}
// 检查下级节点
temNode = temNode.getSubNode(c);
if (temNode == null) {
// 以 begin 开头的字符串不是敏感词
sb.append(text.charAt(begin));
// 进入下一个位置
position = ++begin;
// 重新指向根节点
temNode = rootNode;
} else if (temNode.isKeyWordEnd) {
// 发现敏感词,将begin-position字符串替换掉
sb.append(REPLACEMENT);
// 进入下一位置
begin = ++position;
// 重新指向根节点
temNode = rootNode;
} else {
// 检查下一个字符
position++;
}
}
// 将最后一批字符计入结果
/*
③指针提前到终点,②指针还没有到,且包含的字符不是敏感词
*/
sb.append(text.substring(begin));
return sb.toString();
}
// 判断是否为符号
private boolean isSymbol(Character c) {
// 特殊符号返回 false ,取反则 特殊符号 返回true
// 0x2E80 - 0x9FFF 是东亚文字
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
// 前缀树
private class TrieNode {
// 关键词结束的标识
private boolean isKeyWordEnd = false;
// 子节点(key是下级字符,value是下级节点)
private Map<Character, TrieNode> subNode = new HashMap<>();
public boolean isKeyWordEnd() {
return isKeyWordEnd;
}
public void setKeyWordEnd(boolean keyWordEnd) {
isKeyWordEnd = keyWordEnd;
}
// 添加子节点
public void addSubNode(Character c, TrieNode node) {
subNode.put(c, node);
}
// 获取子节点
public TrieNode getSubNode(Character c) {
return subNode.get(c);
}
}
}
1.5 前缀树过滤敏感词简述
前缀树特点:
- 根节点没有任何字符(是空的),除了根结点以外,每个结点只有一个字符;
- 从根结点开始,到某一个末结点,途径的子结点对应的所有字符,应该是一个敏感词;
- 每个末结点到跟结点包含的所有子结点的字符各不相同。
- 首先建立一个敏感词前缀树
敏感词为:abc,bf,be
注:图中的x代表的含义是,敏感词结束的标识,在代码中可以用Boolean值来表示 - 准备好待处理的字符串:xwabfabcff,声明3个指针,第一个指针指向前缀树的根节点,第二个和第三个指针默认指向待处理字符串的第一个字符
声明一个StringBuilder,用来存储处理后的字符串
3. ②指针和③指针,现在都指向“x”,与前缀树中①指针下一级比较,没有“x”,“x”保留,存入到StringBuilder中
②指针和③指针向下移动:
- 现在②指针和③指针向“w”,同理,与前缀树相比较,没有,将“w”存入到StringBuilder中,两个指针向下移动:
- 现在②指针和③指针向“a”,与前缀树中①指针下一级比较,有“a”,①指针下移
判断前缀树上的“a”,有么有×,即敏感词结束标识,没有,说明“a”不是敏感词,还需要继续判断 - 这时疑似发现敏感词,即“a”后面跟着的字符和“a”可能组成敏感词。②指针停在“a”不动,③指针继续下移,指向“b”
与前缀树中①指针下一级比较,有“b”,①指针下移
判断前缀树上的“b”,有么有×,即敏感词结束标识,没有,说明“ab”不是敏感词,还需要继续判断 - 这时疑似发现敏感词,即“ab”后面跟着的字符和“ab”可能组成敏感词。②指针还是停在“a”不动,③指针现在下移,指向“f”
与前缀树中①指针下一级比较,没有“f”,说明从②指针到③指针的字符,即“abf”不是敏感词。
此时,将“a””存入到StringBuilder中,①指针回到根节点,②指针下移,指向“b”,③指针也回到“b”,从新开始判断
- 现在②指针和③指针向“b”,与前缀树中①指针下一级比较,有“b”,①指针下移
判断前缀树上的“b”,有么有×,即敏感词结束标识,没有,说明“b”不是敏感词,还需要继续判断 - 这时疑似发现敏感词,即“b”后面跟着的字符和“b”可能组成敏感词。②指针还是停在“b”不动,③指针现在下移,指向“f”
与前缀树中①指针下一级比较,有“f”,①指针下移
判断前缀树上①指针指向的“f”,有么有×,即敏感词结束标识,有,说明已找到敏感词,即“bf”。
将敏感词“bf”替换成“”,存到StringBuilder中
9. 已找到敏感词,然后,将①指针回到根节点,②指针指向此时③指针的下一位,即“a”,③指针也下移一位
10. ②指针和③指针,现在都指向“x”,与前缀树中①指针下一级比较,有“a”,①指针下移到“a”
判断前缀树上的“a”,有么有×,即敏感词结束标识,没有,说明“a”不是敏感词,还需要继续判断。
疑似发现敏感词,即“a”后面跟着的字符和“a”可能组成敏感词,②指针不动,③指针下移一位,指向“b”
与前缀树中①指针下一级比较,有“b”,①指针下移到“b”
判断前缀树上的“b”,有么有×,即敏感词结束标识,没有,说明“ab”不是敏感词,还需要继续判断。
疑似发现敏感词,即“ab”后面跟着的字符和“ab”可能组成敏感词,②指针不动,③指针下移一位,指向“c”
与前缀树中①指针下一级比较,有“c”,①指针下移到“c”
判断前缀树上的“c”,有么有×,即敏感词结束标识,有,说明“abc”是敏感词。
将敏感词“abc”替换成“”,存到StringBuilder中
11. 已找到敏感词,然后,将①指针回到根节点,②指针指向此时③指针的下一位,即“f”,③指针也下移一位 ,即“f”
11. ②指针和③指针,现在都指向“f”,与前缀树中①指针下一级比较,没有“f”,“f”保留,存入到StringBuilder中
②指针和③指针向下移动移动一位,指向“f”
与前缀树中①指针下一级比较,没有“f”,“f”保留,存入到StringBuilder中
②指针和③指针已到待检验字符串末尾,整个判断结束,将StringBuilder中的内容返回。
2. 发布帖子
- AJAX
- Asynchronous JavaScript and XML
- 异步的JavaScript于XML,不是一门新的技术,只是一个新的术语
- 使用AJAX,网页能够将增量更新呈现在页面上,而不需要刷新整个页面
- 虽然X代表XML,但目前JSON的使用比XML更加普通
- https://developer.mozilla.org/zh-CN/docs/Web/Guide/AJAX
- 示例
- 使用jQuery发送AJAX请求
- 实践
- 采用AJAX请求,实现发布帖子
2.1 AJA使用示例
- 导入fastjson包
<!-- fastJson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
- 在util包下CommunityUtil类中,新加几个封装Json的方法
//Json处理
public static String getJSONString(int code, String msg, Map<String,Object> map) {
JSONObject json = new JSONObject();
json.put("code",code);
json.put("msg",msg);
if (map != null) {
for (String key : map.keySet()) {
json.put(key, map.get(key));
}
}
return json.toJSONString();
}
public static String getJSONString(int code, String msg) {
JSONObject json = new JSONObject();
json.put("code",code);
json.put("msg",msg);
return json.toJSONString();
}
public static String getJSONString(int code) {
JSONObject json = new JSONObject();
json.put("code",code);
return json.toJSONString();
}
- 在AlphaController中写一个方法,测试一下
// ajax 示例
@RequestMapping(value = "/ajax", method = RequestMethod.POST)
@ResponseBody
public String testAjax(String name, int age) {
System.out.println(name);
System.out.println(age);
return CommunityUtil.getJSONString(0,"操作成功");
}
4.在html包下使用ajax-demo.html下发送post请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AJAX</title>
</head>
<body>
<p>
<input type="button" value="发送" onclick="send();">
</p>
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
<script>
function send() {
$.post(
"/community/ajax",
{"name":"wsh","age":23},
function (data) {
console.log(typeof (data));
console.log(data);
data = $.parseJSON(data);
console.log(typeof(data));
console.log(data.code);
console.log(data.msg);
}
);
}
</script>
</body>
</html>
- 测试结果
2.2 发布帖子
- 数据访问层:在dao包下DisscussPostMapper中新增方法
package com.ateam.community.dao;
import com.ateam.community.entity.DiscussPost;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @author wsh
* @date 2021-11-13 15:13
* @description
*/
@Mapper
public interface DiscussPostMapper {
// orderMode 排序模型,0 还是按原来的 ,1 是按热度
List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit);
//@Param 注解用于给参数取别名
//如果只有一个参数,并且在<if>里使用(动态sql),则必须添加别名
int selectDiscussPostRows(@Param("userId") int userId);
int insertDiscussPost(DiscussPost discussPost);
}
- 写对应的mapper.xml,即DiscussPost.xml
<sql id="insetFields">
user_id, title, content, discuss_type, status, create_time, comment_count, score
</sql>
<insert id="insertDiscussPost" parameterType="com.ateam.community.entity.DiscussPost" keyProperty="id">
insert into discuss_post (<include refid="insetFields"></include>)
values (#{userId},#{title},#{content},#{discussType},#{status},#{createTime},#{commentCount},#{score})
</insert>
- 服务层,在service包下DiscussPostService
@Autowired
private SensitiveFilter sensitiveFilter;
public int addDiscussPost(DiscussPost discussPost){
if (discussPost == null) {
throw new IllegalArgumentException("参数不能为空!");
}
// 转义HTML标记
discussPost.setTitle(HtmlUtils.htmlEscape(discussPost.getTitle()));
discussPost.setContent(HtmlUtils.htmlEscape(discussPost.getContent()));
// 过滤敏感词
discussPost.setTitle(sensitiveFilter.filter(discussPost.getTitle()));
discussPost.setContent(sensitiveFilter.filter(discussPost.getContent()));
return discussPostMapper.insertDiscussPost(discussPost);
}
- 视图层,在controller包,新建DiscussPostController类
package com.ateam.community.controller;
import com.ateam.community.entity.*;
import com.ateam.community.event.EventProducer;
import com.ateam.community.service.CommentService;
import com.ateam.community.service.DiscussPostService;
import com.ateam.community.service.LikeService;
import com.ateam.community.service.UserService;
import com.ateam.community.util.CommunityConstant;
import com.ateam.community.util.CommunityUtil;
import com.ateam.community.util.HostHolder;
import com.ateam.community.util.RedisKeyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.*;
/**
* @author wsh
* @date 2021-11-24 11:19
* @description
*/
@Controller
@RequestMapping(value = "/discuss")
public class DiscussPostController implements CommunityConstant {
@Autowired
private DiscussPostService discussPostService;
@Autowired
private HostHolder hostHolder;
@Autowired
private UserService userService;
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(value = "/add", method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title, String content) {
User user = hostHolder.getUser();
if (user == null) {
return CommunityUtil.getJSONString(403,"你还没有登录哦!");
}
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
discussPostService.addDiscussPost(post);
// 报错额情况,将来回一种处理
return CommunityUtil.getJSONString(0,"发布成功");
}
}
- 修改index.html中
<button type="button" class="btn btn-primary btn-sm position-absolute rt-0"
data-toggle="modal" data-target="#publishModal" th:if="${loginUser!=null}">我要发布</button>
</div>
<!-- 弹出框 -->
<div class="modal fade" id="publishModal" tabindex="-1" role="dialog" aria-labelledby="publishModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="publishModalLabel">新帖发布</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" name="title">
</div>
<div class="form-group">
<label for="message-text" class="col-form-label">正文:</label>
<textarea class="form-control" id="message-text" rows="15" name="content"></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="publishBtn">发布</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>
- 给发布按钮绑定事件
在index.js中修改:
$(function(){
$("#publishBtn").click(publish);
});
function publish() {
$("#publishModal").modal("hide");
// 获取标题和内容
var title = $("#recipient-name").val();
var content = $("#message-text").val();
// 发送异步请求(POST)
$.post(
CONTEXT_PATH + "/discuss/add",
{"title":title,"content":content},
function(data) {
data = $.parseJSON(data);
// 在提示框中显示返回消息
$("#hintBody").text(data.msg);
// 显示提示框
$("#hintModal").modal("show");
// 2秒后,自动隐藏提示框
setTimeout(function(){
$("#hintModal").modal("hide");
// 刷新页面
if(data.code == 0) {
window.location.reload();
}
}, 2000);
}
);
}
3. 帖子详情
- DiscussPostMapper
- DiscussPostService
- DispostPostController
- index.html
- 在帖子标题上增加访问详情页面的连接
- discuss-detail.html
- 处理静态资源的访问路径
- 复用index.html的header区域
- 显示标题、作者、发布时间、帖子正文等内容
- 数据处理层,在dao包下DiscussPostMapper类中增加查询帖子详情的方法
DiscussPost selectDiscussPostById(int id);
- 在对应的mppaer.xml中编写SQL语句
<select id="selectDiscussPostById" resultType="com.ateam.community.entity.DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
- 服务层,在service包下DiscussPostService类中,增加响应的方法
public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit, int orderMode) {
return discussPostMapper.selectDiscussPosts(userId, offset, limit,orderMode);
}
- 视图层,在controller包下DispostPostController类中,增加处理查询帖子详情的请求
// 只要是javabean在方法参数中,SpringMVC会将其分装到model中
@RequestMapping(value = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
// 帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post",post);
// 查询帖子的作者
User user = userService.findUserById(post.getUserId());
model.addAttribute("user",user);
return "/site/discuss-detail";
}
- 增加访问详情页面的链接
- 处理disscuss-detail.html页面
处理静态资源的访问路径
复用index.html的header区域
显示标题、作者、发布时间、帖子正文等内容
4. 事务管理
回顾
- 什么是事务
- 事务是由N步数据库操作序列组成的逻辑执行单元,这系列操作要么全部执行,要么全放弃执行
- 事务的特性(ACID)
- 原子性(Atomicity):事务是应用中不可再分的最小执行体
- 一致性(Consistency):事务执行的结果,须使数据从一个一致性状态,变为另一个一致性状态
- 隔离性(Isolation):各个事务的执行互不干扰,任何事务的内部操作对其他的事务都是隔离的
- 持久性(Durablity):事务一旦提交,对数据所做的任何改变都要记录到永久存储器中
事务的隔离性
- 常见的并发异常
- 第一类丢失更新、第二类丢失更新
- 脏读、不可重复读、幻读
- 常见的隔离级别
- Read Uncommitted:读取为提交的数据
- Read Committed:读取已提交的数据
- Repeatable Read:可重复读
- Serializable:串行化
4.1 常见的并发异常的隔离级别
第一类的丢失更新
某一个事务的回滚,导致另外一个事务已更新的数据丢失了。
第二类丢失更新
某一个事务的提交,导致另外一个事务已更新的数据丢失了
脏读
某一个事务,读取了另外一个事务未提交的数据
不可重复读
某一个事务,对同一个数据前后读取的结果不一致
幻读
某一个事务,对同一个表前后查询的行数不一致
事务管理级别
实现机制
- 悲观锁(数据库)
- 共享锁(S锁)
事务A对某数据加了共享锁后,其他事务只能对该数据加共享锁,但不能加排他锁 - 排他锁(X锁)
事务A对某数据加了排他锁后,其他事务对该数据既不能加共享锁,也不能加排他锁
- 共享锁(S锁)
- 乐观锁(自定义)
- 版本号、时间戳等
在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,否则就更新数据(版本号+1)
- 版本号、时间戳等
4.2 Spring事务管理
- 声明式事务
- 通过XML配置,声明某方法的事务特征
- 通过注解,声明某方法的事务特征
- 编程式事务
- 通过TransactionTemplate管理事务,并通过它执行数据库的操作
- 通过TransactionTemplate管理事务,并通过它执行数据库的操作
4.2.1 演示声明式事务
- 在AlphaService中写一个新方法加上@Transaction注解
/*
传播机制:两个不同的业务可能有不同的隔离级别且可能一个业务使用了另一个业务,传播机制就是解决不同隔离级别同时出现的情况。
REQUIRED:支持当前事务(外部事务),如不存在则创建新事物
REQUIRES_NEW:创建一个新事物,并且暂停当前事务(外部事务)
NESTED:如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就会和REQUIRED一样
*/
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public Object save1() {
// 新增用户
User user = new User();
user.setUsername("alpha");
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("alpha@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("hello");
post.setContent("新人报道");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc");
return "ok";
}
- 在test包下,编写一个测试类,测试事务
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class TransactionTests {
@Autowired
private AlphaService alphaService;
@Test
public void testSave(){
System.out.println(alphaService.save1());
}
}
测试结果:数据库中没有插入任何数据
4.2.2 演示编程式事务
- 在AlphaService中写一个新方法
@Autowired
private TransactionTemplate transactionTemplate;
public Object save2() {
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
return transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus status) {
// 新增用户
User user = new User();
user.setUsername("alpha-go");
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5("123456" + user.getSalt()));
user.setEmail("alpha-go@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/999t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("你好");
post.setContent("新人报道");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc");
return "ok";
}
});
}
- 在test包下事务个测试类,测试事务
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class TransactionTests {
@Autowired
private AlphaService alphaService;
@Test
public void testSave(){
System.out.println(alphaService.save1());
}
@Test
public void testSave2(){
Object save2 = alphaService.save2();
System.out.println(save2);
}
}
测试结果:发现数据库中并没有插入任何数据
5. 显示评论
- 数据层
- 根据实体查询一页的评论数据
- 根据实体查询评论的数量
- 业务层
- 处理查询评论的业务
- 处理查询评论数量的业务
- 表示层
- 显示帖子详情数据时,同时显示帖子所有的评论数据
- 显示帖子详情数据时,同时显示帖子所有的评论数据
5.1 数据层
- 评论表comment结构
- id:自增
- user_id:该条评论对应的用户的id
- entity_type:评论的类型,帖子的评论,帖子评论的评论
- entity_id:帖子的id或评论的id
- target_id:记录评论指向的用户
- content:评论的内容
- status:该条评论的状态
- create_time:改条评论创建的时间
- 编写comment表对应的实体类
public class Comment {
private int id;
private int userId;
private int entityType;
private int entityId;
private int targetId;
private String content;
private int status;
private Date createTime;
}
- 在到包下,创建对应的Mappeer类
@Mapper
public interface CommentMapper {
List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset, int limit);
int selectCountByEntity(int entityType, int entityId);
}
- 在mapper包下,编写对应的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.ateam.community.dao.CommentMapper">
<sql id="selectFields">
id, user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<sql id="insertFields">
user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<select id="selectCommentsByEntity" resultType="com.ateam.community.entity.Comment">
select <include refid="selectFields"></include>
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
order by create_time asc
limit #{offset}, #{limit}
</select>
<select id="selectCountByEntity" resultType="int">
select count(id)
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
</select>
</mapper>
注:写完mppaer.xml文件后,要先测试一下,这里就不在演示了
5.2 业务层
在service包 下,新建CommentService类
@Resource
private CommentMapper commentMapper;
public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit){
return commentMapper.selectCommentsByEntity(entityType,entityId,offset,limit);
}
public int findCommentCount(int entityType, int entityId){
return commentMapper.selectCountByEntity(entityType,entityId);
}
}
5.3 表现层
- controller:处理请求
- thymeleaf:渲染页面,展现数据
- 在Controller包下的DiscussPostController类中增加响应的内容
// 只要是javabean在方法参数中,SpringMVC会将其分装到model中
@RequestMapping(value = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
// 帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post",post);
// 查询帖子的作者
User user = userService.findUserById(post.getUserId());
model.addAttribute("user",user);
// 点赞数量
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, discussPostId);
// 点赞状态
int likeStatus = hostHolder.getUser() == null ? 0 :
likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_POST, discussPostId);
//
model.addAttribute("likeCount",likeCount);
model.addAttribute("likeStatus",likeStatus);
// 评论分页信息
page.setLimit(5);
page.setPath("/discuss/detail/" + discussPostId);
page.setRows(post.getCommentCount());
/*
评论:给帖子的评论
回复:给评论的评论
*/
// 评论列表
List<Comment> commentList = commentService.findCommentsByEntity(ENTITY_TYPE_POST,
post.getId(), page.getOffset(), page.getLimit());
ArrayList<Map<String, Object>> commentVoList = new ArrayList<>();
// 评论VO列表
if (commentList != null) {
for (Comment comment : commentList) {
// 评论VO
HashMap<String, Object> commentVo = new HashMap<>();
// 评论
commentVo.put("comment",comment);
// 作者
commentVo.put("user",userService.findUserById(comment.getUserId()));
// 点赞数量
likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, comment.getId());
// 点赞状态
likeStatus = hostHolder.getUser() == null ? 0 :
likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, comment.getId());
//
commentVo.put("likeCount",likeCount);
commentVo.put("likeStatus",likeStatus);
// 回复列表
List<Comment> replyList = commentService.findCommentsByEntity(ENTITY_TYPE_COMMENT, comment.getId(),
0, Integer.MAX_VALUE);
// 回复VO列表
ArrayList<Map<String, Object>> replyVoList = new ArrayList<>();
if (replyList != null) {
for (Comment reply : replyList) {
HashMap<String, Object> replyVo = new HashMap<>();
// 回复
replyVo.put("reply",reply);
// 作者
replyVo.put("user",userService.findUserById(reply.getUserId()));
// 回复的目标
User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getUserId());
replyVo.put("target",target);
// 点赞数量
likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, reply.getId());
// 点赞状态
likeStatus = hostHolder.getUser() == null ? 0 :
likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, reply.getId());
//
replyVo.put("likeCount",likeCount);
replyVo.put("likeStatus",likeStatus);
replyVoList.add(replyVo);
}
}
commentVo.put("replys", replyVoList);
// 回复数量
int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("replyCount", replyCount);
commentVoList.add(commentVo);
}
}
model.addAttribute("comments",commentVoList);
return "/site/discuss-detail";
}
- 修改discuss-detail.html页面
<div class="container mt-3">
<!-- 回帖数量 -->
<div class="row">
<div class="col-8">
<h6><b class="square"></b> <i th:text="${post.commentCount}">30</i>条回帖</h6>
</div>
<div class="col-4 text-right">
<a href="#replyform" class="btn btn-primary btn-sm"> 回 帖 </a>
</div>
</div>
<!-- 回帖列表 -->
<ul class="list-unstyled mt-4">
<!-- 第1条回帖 -->
<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="cvo:${comments}">
<a th:href="@{|/user/profile/${cvo.user.id}|}">
<img th:src="${cvo.user.headerUrl}" class="align-self-start mr-4 rounded-circle user-header" alt="用户头像" >
</a>
<div class="media-body">
<div class="mt-0">
<span class="font-size-12 text-success" th:utext="${cvo.user.username}">掉脑袋切切</span>
<span class="badge badge-secondary float-right floor">
<i th:text="${page.offset + cvoStat.count}">1</i></span>
</div>
<div class="mt-2" th:text="${cvo.comment.content}">
这开课时间是不是有点晚啊。。。
</div>
<div class="mt-4 text-muted font-size-12">
<span>发布于 <b th:text="${#dates.format(cvo.comment.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b></span>
<ul class="d-inline float-right">
<li class="d-inline ml-2">
<a href="javascript:;" th:onclick="|like(this,2,${cvo.comment.id},${cvo.comment.userId},${post.id});|" class="text-primary">
<b th:text="${cvo.likeStatus==1?'已赞':'赞'}">赞</b>(<i th:text="${cvo.likeCount}">1</i>)
</a>
</li>
<li class="d-inline ml-2">|</li>
<li class="d-inline ml-2"><a href="#" class="text-primary">回复(<i th:text="${cvo.replyCount}">2</i>)</a></li>
</ul>
</div>
<!-- 回复列表 -->
<ul class="list-unstyled mt-4 bg-gray p-3 font-size-12 text-muted">
<!-- 第1条回复 -->
<li class="pb-3 pt-3 mb-3 border-bottom" th:each="rvo:${cvo.replys}">
<div>
<span th:if="${rvo.target==null}">
<b class="text-info" th:text="${rvo.user.username}">寒江雪</b>:
</span>
<span th:if="${rvo.target!=null}">
<i class="text-info" th:text="${rvo.user.username}">xx</i> 回复
<b class="text-info" th:text="${rvo.target.username}">寒江雪</b>:
</span>
<span th:utext="${rvo.reply.content}">这个是直播时间哈,觉得晚的话可以直接看之前的完整录播的~</span>
</div>
<div class="mt-3">
<span th:text="${#dates.format(rvo.reply.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</span>
<ul class="d-inline float-right">
<li class="d-inline ml-2">
<a href="javascript:;" th:onclick="|like(this,2,${rvo.reply.id},${rvo.reply.userId},${post.id});|" class="text-primary">
<b th:text="${rvo.likeStatus==1?'已赞':'赞'}">赞</b>(<i th:text="${rvo.likeCount}">1</i>)
</a>
</li>
<li class="d-inline ml-2">|</li>
<li class="d-inline ml-2"><a th:href="|#huifu-${rvoStat.count}|" data-toggle="collapse" class="text-primary">回复</a></li>
</ul>
<div th:id="|huifu-${rvoStat.count}|" class="mt-4 collapse">
<form method="post" th:action="@{|/comment/add/${post.id}|}">
<div>
<input type="text" class="input-size" name="content" th:placeholder="|回复${rvo.user.username}|"/>
<input type="hidden" name="entityType" value="2">
<input type="hidden" name="entityId" th:value="${cvo.comment.id}">
<input type="hidden" name="targetId" th:value="${rvo.user.id}">
</div>
<div class="text-right mt-2">
<button type="submit" class="btn btn-primary btn-sm"> 回 复 </button>
</div>
</form>
</div>
</div>
</li>
<!-- 回复输入框 -->
<li class="pb-3 pt-3">
<form method="post" th:action="@{|/comment/add/${post.id}|}">
<div>
<input type="text" name="content" class="input-size" placeholder="请输入你的观点"/>
<input type="hidden" name="entityType" value="2">
<input type="hidden" name="entityId" th:value="${cvo.comment.id}">
</div>
<div class="text-right mt-2">
<button type="submit" class="btn btn-primary btn-sm"> 回 复 </button>
</div>
</form>
</li>
</ul>
</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>
6. 添加评论
- 数据层
- 增加评论数据
- 修改帖子的评论数据
- 业务层
- 处理添加评论的业务:
先增加评论、在更新帖子的评论数量
- 处理添加评论的业务:
- 表现层
- 处理添加评论数据的请求
- 设置添加评论的表单
6.1 数据层
- 在dao包下CommentMapper类中增加插入评论的方法
int insertComment(Comment comment);
- 在mapper包下,编写对应的SQL语句
<?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.ateam.community.dao.DiscussPostMapper">
<sql id="selectFields">
id, user_id, title, content, discuss_type, status, create_time, comment_count, score
</sql>
<sql id="insetFields">
user_id, title, content, discuss_type, status, create_time, comment_count, score
</sql>
<select id="selectDiscussPosts" resultType="com.ateam.community.entity.DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where status != 2
<if test="userId != 0">
and user_id = #{userId}
</if>
<if test="orderMode==0">
order by discuss_type desc, create_time desc
</if>
<if test="orderMode==1">
order by discuss_type desc, score desc, create_time desc
</if>
limit #{offset}, #{limit}
</select>
<select id="selectDiscussPostRows" resultType="java.lang.Integer">
select count(id)
from discuss_post
where status != 2
<if test="userId != 0">
and user_id = #{userId}
</if>
</select>
<insert id="insertDiscussPost" parameterType="com.ateam.community.entity.DiscussPost" keyProperty="id">
insert into discuss_post (<include refid="insetFields"></include>)
values (#{userId},#{title},#{content},#{discussType},#{status},#{createTime},#{commentCount},#{score})
</insert>
</mapper>
- 在dao下DiscussPostMapper类中增加修改帖子评论数据的方法
@Mapper
public interface DiscussPostMapper {
// orderMode 排序模型,0 还是按原来的 ,1 是按热度
List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit, int orderMode);
//@Param 注解用于给参数取别名
//如果只有一个参数,并且在<if>里使用(动态sql),则必须添加别名
int selectDiscussPostRows(@Param("userId") int userId);
int insertDiscussPost(DiscussPost discussPost);
DiscussPost selectDiscussPostById(int id);
int updateCommentCount(int id, int commentCount);
int updateDiscussType(int id, int discussType);
}
- 在mapper包下,编写对应的SQL语句
<update id="updateCommentCount">
update discuss_post
set comment_count = #{commentCount}
where id = #{id}
</update>
6.2 业务层
- 在service包下CommentService类中,增加方法
@Service
public class CommentService implements CommunityConstant {
@Resource
private CommentMapper commentMapper;
@Autowired
private DiscussPostService discussPostService;
// 增加评论
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public int addComment(Comment comment) {
if (comment == null) {
throw new IllegalArgumentException("参数不能为空!");
}
// if (comment.getContent() == null || comment.getContent().length() == 0) {
// throw new RuntimeException("评论内容不能为空!");
// }
// 添加评论
comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
comment.setContent(sensitiveFilter.filter(comment.getContent()));
int rows = commentMapper.insertComment(comment);
// 更新 帖子 的平路的数量
if (comment.getEntityType() == ENTITY_TYPE_POST) {
int count = commentMapper.selectCountByEntity(comment.getEntityType(),comment.getEntityId());
discussPostService.updateCommentCount(comment.getEntityId(),count);
}
return rows;
}
}
- 在service包下DiscussPostService类中,增加方法
public int updateCommentCount(int id, int commentCount) {
return discussPostMapper.updateCommentCount(id,commentCount);
}
- 在util包下CommunityCosntant中,增加几个常量,表示实体的类别
/**
* 实体类型:帖子
*/
int ENTITY_TYPE_POST = 1;
/**
* 实体类型:评论
*/
int ENTITY_TYPE_COMMENT = 2;
/**
* 实体类型:用户
*/
int ENTITY_TYPE_USER = 3;
6.3 表现层
- 在controller包下,新创建一个CommentController方法
package com.ateam.community.controller;
import com.ateam.community.annotation.LoginRequired;
import com.ateam.community.entity.Comment;
import com.ateam.community.entity.DiscussPost;
import com.ateam.community.entity.Event;
import com.ateam.community.event.EventProducer;
import com.ateam.community.service.CommentService;
import com.ateam.community.service.DiscussPostService;
import com.ateam.community.util.CommunityConstant;
import com.ateam.community.util.HostHolder;
import com.ateam.community.util.RedisKeyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.Date;
import java.util.Map;
/**
* @author wsh
* @date 2021-11-27 15:19
* @description
*/
@Controller
@RequestMapping(value = "/comment")
public class CommentController implements CommunityConstant {
@Autowired
private CommentService commentService;
@Autowired
private HostHolder hostHolder;
@Autowired
private DiscussPostService discussPostService;
@RequestMapping(value = "/add/{discussPostId}", method = RequestMethod.POST)
@LoginRequired
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment){
comment.setUserId(hostHolder.getUser().getId());
comment.setStatus(0);
comment.setCreateTime(new Date());
commentService.addComment(comment);
return "redirect:/discuss/detail/" + discussPostId;
}
}
- 处理页面
- 给帖子回复
- 给评论回复
- 给某个用户回复
7. 私信列表和私信详情
- 私信列表
- 查询当前用户的会话列表,每个会话只显示一条最新的私信
- 支持分页显示
- 私信详情
- 查询某个会话所包含的私信
- 支持分页显示
7.1 私信列表
7.1.1 私信message表的设计
- from_id:发送私信用户的id
- to_id:接收私信用户的id
- conversation_id:是通信双方id的拼接,规定小的id在前,大的id在后,中间用“_”连接
7.1.2 数据层
- 在entity包下,新建一个私信表message的实体类
- 在dao包下,新建一个MessageMapper类
@Mapper
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);
}
- 在mapper包下,编写对应的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.ateam.community.dao.MessageMapper">
<sql id="selectFields">
id, from_id, to_id, conversation_id, content, status, create_time
</sql>
<sql id="insertFields">
from_id, to_id, conversation_id, content, status, create_time
</sql>
<select id="selectConversations" resultType="com.ateam.community.entity.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>
<!-- 统计每个会话最大id 的数量-->
<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="com.ateam.community.entity.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>
7.1.3 业务层
在service包下新建一个MessageService类
@Service
public class MessageService {
@Resource
private MessageMapper messageMapper;
@Autowired
private SensitiveFilter sensitiveFilter;
// 获得私信会话列表
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);
}
}
7.1.4 表示层
- 在controller包下新建一个MessageController类
@Controller
public class MessageController implements CommunityConstant {
@Autowired
private MessageService messageService;
@Autowired
private HostHolder hostHolder;
@Autowired
private UserService userService;
// 私信列表
@RequestMapping(value = "/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());
ArrayList<Map<String, Object>> conversations = new ArrayList<>();
if (conversationList != null) {
for (Message message : conversationList) {
HashMap<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";
}
}
- 处理letter.html页面
- 先把页面改成thymeleaf的形式
- 再往页面里填充数据
7.2 私信详情
当点击一个私信时,进入私信详情页面,且将该条私信状态修改为已读
7.2.1 数据层
在dao下MessageMapper类中,新加方法
// 修改消息状态
int updateStatus(List<Integer> ids, int status);
在mapper包下message-mapper.xml中,编写对应的SQL语句
<update id="updateStatus">
update message set status = #{status}
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
7.2.2 访问层
public int readMessage(List<Integer> ids) {
return messageMapper.updateStatus(ids,1);
}
7.2.3 表现层
- 在MessageController中,添加新方法
@RequestMapping(value = "/letter/detail/{conversationId}", method = RequestMethod.GET)
public String getLetterDetail(@PathVariable("conversationId") String conversationId, Model model, Page page) {
// 分页信息
page.setLimit(5);
page.setPath("/letter/detail/" + conversationId);
page.setRows(messageService.findLetterCount(conversationId));
// 私信列表
List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
ArrayList<Map<String, Object>> letters = new ArrayList<>();
if (letterList != null) {
for (Message message : letterList) {
HashMap<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) {
ArrayList<Integer> ids = new ArrayList<>();
for (Message message : letterList) {
if (hostHolder.getUser().getId() == message.getToId() && message.getStatus() == 0) {
ids.add(message.getId());
}
}
return ids;
}
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);
}
}
- 处理页面
<!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>ATeam-私信详情</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="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 th:href="@{|/user/profile/${map.fromUser.id}|}">
<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>
<!-- 尾部 -->
<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(电话) 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
<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>
<script>
function back() {
location.href = CONTEXT_PATH + "/letter/list";
}
</script>
</body>
</html>
注意:
这里有一个地方和平时处理不太一样
8. 发送私信
- 发送私信
- 采用异步的方式发送私信
- 发送成功后刷新私信列表
- 设置已读
- 访问私信详情时,将显示的私信设置已读状态
- 访问私信详情时,将显示的私信设置已读状态
8.1 数据层
- 在dao包下MessageMapper中,新增一个方法
// 新增一个消息
int insertMessage(Message message);
- 编写对应的SQL语句
<sql id="insertFields">
from_id, to_id, conversation_id, content, status, create_time
</sql>
<insert id="insertMessage" parameterType="com.ateam.community.entity.Message" keyProperty="id">
insert into message (<include refid="insertFields"></include>)
values (#{fromId}, #{toId}, #{conversationId}, #{content}, #{status}, #{createTime})
</insert>
8.2 业务层
在service包下,MessageService类中新加一个方法
public int addMessage(Message message) {
// 过滤
message.setContent(HtmlUtils.htmlEscape(message.getContent()));
message.setContent(sensitiveFilter.filter(message.getContent()));
return messageMapper.insertMessage(message);
}
8.3 表现层
- 在controller包下MessageController类中新加一个方法
@RequestMapping(value = "/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,"目标用户不存在!");
}
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.addMessage(message);
return CommunityUtil.getJSONString(0);
}
- letter.html页面
发送的逻辑,写在letter.js中
legger.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();
// ajax-post请求
$.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();
}
- letter-detail.html页面
这里与letter.html中不同的是,这里的“发给”框,值是会话的用户名
设置已读:在7.2章节已完成
9. 统一处理异常’
- @ControllerAdvice
- 用于修饰类,表示该类是Controller的全局配置类
- 在此类中,可以对Controller进行如下三种全局配置:异常处理方案、绑定数据方案、绑定参数方案
- @ExceptionHandler
- 用于修饰方法,该方法会在Controller出现异常后被调用,用于处理捕获到的异常
- @ModelAttribute
- 用于修饰方法,该方法会在Controller方法执行前被调用,用于为Model对象绑定参数
- @DataBinder
- 用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器
- 用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器
9.1 Spring Boot 自动处理方式
- 把报错的错误码作为页面名,放到指定目录下,报出来的错误会自动显示相应错误的页面。
9.2 使用@ControllerAdvice和@ExceptionHandler处理异常
- 写一个跳转到错误页面的Controller,这里在HomeController类中添加
@RequestMapping(value = "/error", method = RequestMethod.GET)
public String getErrorPage(){
return "/error/500";
}
- 在controller包下,新建advice包并创建处理ExceptionAdvice类
package com.ateam.community.controller.advice;
import com.ateam.community.util.CommunityUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author wsh
* @date 2021-11-28 16:08
* @description
*/
@ControllerAdvice(annotations = Controller.class) // 只扫描 带有 Controller的 bean
public class ExceptionAdvice {
private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);
@ExceptionHandler
public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) {
logger.error("服务器发送异常:" + e.getMessage());
for (StackTraceElement element : e.getStackTrace()) {
logger.error(element.toString());
}
// 判断请求方式
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) {
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(1,"服务器异常!"));
} catch (IOException ioException) {
logger.error("响应获得输出流空指针异常:" + ioException.getMessage());
}
} else {
try {
response.sendRedirect(request.getContextPath() + "/error");
} catch (IOException ioException) {
logger.error("响应重定向到错误页面空指针异常:" + ioException.getMessage());
}
}
}
}
这里有一个小知识点:判断一个请求是否是异步请求
// 判断请求方式
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) {
// 异步请求
} else {
// 同步请求
}
10. 统一记录日志
需求:对所有的*Service方法记录日志
10.1 AOP
10.1 AOP的概念
- Aspect Oriented Programing,即面向方面(切面)编程
- AOP是一种编程思想,是OOP的补充,可以进一步提高编程的效率
10.2 AOP术语
- Target:已处理完业务逻辑的代码为目标对象
- JoinPoint:目标对象上有很多地方能被织入代码的位置交叫连接点
- Pointcut:切入点声明到织入到哪些位置
- Advice:通知及到底要处理什么样的逻辑(增强什么样的逻辑)
10.3 AOP的实现
- AspectJ
- AspectJ是语言级的实现,它扩展了Java语言,定义了AOP语法
- AspectJ在编译期织入代码,它有一个专门的编译器,用来生成遵守Java字节码规范的class文件
- SpringAOP
- Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类加载器
- Spring AOP在运行时通过代理的方式织入代码,只支持方法类型的连接点
- Spring支持对AspectJ的集成
10.4 Spring AOP
- JDK动态代理
- Java提供的动态代理技术,可以在运行时创建接口的代理实例
- Spring AOP默认采用此种方式,在接口的代理实例中织入代码
- CGLib动态代理
- 采用底层的字节码技术,在运行时创建子类代理实例
- 当目标对象不存在接口时,Spring AOP会采用此种方式,在子类实例中织入代码
10.2 Spring AOP 举例
- 导包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 新建aspect包,编写一个AlphaAspect类
@Component
@Aspect
public class AlphaAspect {
@Pointcut("execution(* com.ateam.community.service.*.*(..))")
//方法名字随便取
public void pointcut(){
}
@Before("pointcut()")
public void before(){
System.out.println("before");
}
@After("pointcut()")
public void after(){
System.out.println("after");
}
@AfterReturning("pointcut()")
public void afterReturning(){
System.out.println("afterReturning");
}
@AfterThrowing("pointcut()")
public void afterThrowing(){
System.out.println("afterThrowing");
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("aroundBefore");
Object obj = joinPoint.proceed();
System.out.println("aroundAfter");
return obj;
}
}
10.3 统一记录日志
在aspect包下,新建ServiceLogAspect类
package com.ateam.community.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author wsh
* @date 2021-11-28 22:27
* @description
*/
@Component
@Aspect
public class ServiceLogAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
@Pointcut("execution(* com.ateam.community.service.*.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
// 用户[1.2.3.4],在[xxx],访问了[com.ateam.community.service.xxx()].
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
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));
}
}
效果:
输出:
...
用户[127.0.0.1],在[2020-05-18 22:27:06],访问了
[com.hsw.community.service.UserService.findUserById]
...