仿牛客网
内容部分引用至 https://blog.csdn.net/weixin_44406146
仿牛客网第三章
一、过滤敏感词
-
前缀树
-
- 名称:Trie、字典树、查找树
- 特点:查找效率高,消耗内存大
- 应用:字符串检索、词频统计、字符串排序等
-
敏感词过滤器
-
- 定义前缀树
- 根据敏感词,初始化前缀树
- 编写过滤敏感词的方法
定义敏感词
为简便处理定义一个敏感词文件,resources目录下新建一个sensitive-words.txt文件
定义前缀树
因为不会被外界访问,所以在util包下的SensitiveFilter类中定义了内部类
@Component
public class SensitiveFilter {
//定义前缀树
private class TireNode{
//关键词结束的标识
private boolean isKeywordEnd = false;
//子节点(key是下级字符,value是下级节点)
private Map<Character,TireNode> subNodes = new HashMap<>();
//添加子节点
public void addSubNode(Character c,TireNode node){
subNodes.put(c,node);
}
//获取子节点
public TireNode getSubNode(Character c){
return subNodes.get(c);
}
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
}
}
根据敏感词初始化前缀树
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
//要替换的符号
private static final String REPLACEMENT="***";
//根节点
private TireNode rootNode = new TireNode();
@PostConstruct //服务启动初始化bean时构造器之后执行
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) {
if(StringUtils.isBlank(keyword)) return;
char[] arr = keyword.toCharArray();
TireNode tmpNode = rootNode;
for(int i=0;i<arr.length;i++){
TireNode subNode = tmpNode.getSubNode(arr[i]);
if(subNode==null){
//初始化化子节点
subNode = new TireNode();
tmpNode.addSubNode(arr[i],subNode);
}
tmpNode = subNode;
}
tmpNode.setKeywordEnd(true);
}
//定义前缀树
private class TireNode{
//关键词结束的标识
private boolean isKeywordEnd = false;
//子节点(key是下级字符,value是下级节点)
private Map<Character,TireNode> subNodes = new HashMap<>();
//添加子节点
public void addSubNode(Character c,TireNode node){
subNodes.put(c,node);
}
//获取子节点
public TireNode getSubNode(Character c){
return subNodes.get(c);
}
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
}
}
过滤敏感词方法
依然是在这个类中定义方法
/**
* 过滤敏感词
* @param text 待过虑的词
* @return
*/
public String filter(String text){
if(StringUtils.isBlank(text)) return null;
//指针1 指向树
TireNode tmpNode = rootNode;
//指针2
int begin = 0;
//指针3
int position = 0;
//结果
StringBuilder sb = new StringBuilder();
while(position<text.length()){
char c = text.charAt(position);
//跳过符号
if(isSysbol(c)){
//若指针1处于根节点,将此符号计入结果,让指针2向下走一步
if(tmpNode==rootNode){
sb.append(c);
begin++;
}
//无论符号在开头或者中间指针3都向下走一步
position++;
continue;
}
//检查下级节点
tmpNode = tmpNode.getSubNode(c);
if(tmpNode==null){
//以begin为开头的字符不是敏感词
sb.append(text.charAt(begin));
//进入下一个词的判断
position = ++begin;
tmpNode = rootNode;
}else if(tmpNode.isKeywordEnd()){
//发现敏感词以begin开头,position结尾的词
sb.append(REPLACEMENT);
begin = ++position;
tmpNode = rootNode;
}else{
//继续检查下一个字符
position++;
}
}
//将最后一批字符记录
sb.append(text.substring(begin));
return sb.toString();
}
//判读是否为符号
private boolean isSysbol(Character c){
//c<0x2E80||c>0x9FFF 东亚文字之外
return !CharUtils.isAsciiAlphanumeric(c)&&(c<0x2E80||c>0x9FFF);
}
二、发布帖子
-
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请求,实现发布帖子的功能。
AJAX使用示例
1.导入Fastjson
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
2.在CommunityUtil类中写几个封装成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){
return getJsonString(code,msg,null);
}
public static String getJsonString(int code){
return getJsonString(code,null,null);
}
3.在AlphaController中写一个实例controller
@RequestMapping(path = "/ajax",method = RequestMethod.POST)
@ResponseBody
public String testAjax(String name,String age){
System.out.println(name);
System.out.println(age);
return CommunityUtil.getJsonString(0,"操作成功");
}
4.为了方便直接写一个静态的html测试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ajax</title>
</head>
<body>
<input type="button" value="发送" onclick="send();">
</body>
<!--引入jQuery-->
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
<script>
function send() {
$.post(
"/community/demo/ajax",
{"name":"张三","age":12},
function(data) {
console.log(typeof(data))
console.log(data)
## $.parseJson 表明将传来的data数据转变为JSon对象
data = $.parseJSON(data)
console.log(typeof(data))
console.log(data.code)
console.log(data.msg)
}
);
}
</script>
</html>
完成发布帖子功能
1.老规矩先写插入帖子的dao层
@Repository
public interface DiscussPostMapper {
/**
* @param userId 考虑查看我的帖子的情况下设置动态sql
* @param offset
* @param limit
* @return
*/
List<DiscussPost> selectDiscussPosts(int userId,int offset,int limit);
//如果需要动态拼接条件(<if>里使用)并且这个方法有且只有一个参数需要用@Param起别名
//@Param用于给参数取别名
int selectDiscussPostRows(@Param("userId") int userId);
int insertDiscussPost(DiscussPost discussPost);
}
2.写对应mapper.xml
<sql id="insertFields">
user_id, title, content, type, status, create_time, comment_count, score
</sql>
<insert id="insertDiscussPost" parameterType="DiscussPost">
insert into discuss_post (<include refid="insertFields"></include>)
values (#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score});
</insert>
3.写Service层在DiscussPostService类中
@Autowired
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);
}
4.controller层新建一个DiscussPostController类
@Controller
@RequestMapping("/discuss")
public class DiscussPostController {
@Autowired
private DiscussPostService discussPostService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path = "/add",method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title,String content){
User user = hostHolder.getUser();
if(user==null){
//返回Json数据
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,"发布成功");
}
}
5.回头看index.html中发布相关
<button type="button" class="btn btn-primary btn-sm position-absolute rt-0" data-toggle="modal" data-target="#publishModal">我要发布</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">
</div>
<div class="form-group">
<label for="message-text" class="col-form-label">正文:</label>
<textarea class="form-control" id="message-text" rows="15"></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>
看这已经定义好了逻辑,我们改写这
6.改写上述js
$(function(){
$("#publishBtn").click(publish);
});
function publish() {
/*把弹框隐藏*/
$("#publishModal").modal("hide");
//获取标题和内容
var title = $("#recipient-name").val();
var content = $("#message-text").val();
//发送异步请求
$.post(
//global.js中定义的
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);
}
)
}
7.另外有个问题
我要发布按钮在登录前不应该显示,嗨呀其实也无所谓,因为发布时判断是否登录了嘛。。
这里改着玩玩
三、帖子详情
-
DiscussPostMapper
-
DiscussPostService
-
DiscussPostController
-
index.html
-
- 在帖子标题上增加访问详情页面的链接
-
discuss-detail.html
-
- 处理静态资源的访问路径
- 复用index.html的header区域
- 显示标题、作者、发布时间、帖子正文等内容
1.DiscussPostMapper增加查询帖子详情
DiscussPost selectDiscussPostById(int id);
2.配置mapper.xml
<select id="selectDiscussPostById" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
3.写service层
public DiscussPost findDiscussPostById(int id){
return discussPostMapper.selectDiscussPostById(id);
}
3.controller层
@Autowired
private UserService userService;
@RequestMapping(path = "/detail/{discussPostId}",method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model){
//查询这个铁子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post",post);
//根据userId查名字
User user = userService.findUserById(post.getUserId());
model.addAttribute("user",user);
return "site/discuss-detail";
}
4.处理首页让每个帖子有个链接
5.处理discuss-detail页面
先改成Thymeleaf模板的格式
接着填充数据
四、事务管理
回顾
什么是事务
- 事务是由N步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行。
事务的特性(ACID)
- 原子性(Atomicity):事务是应用中不可再分的最小执行体。
- 一致性(Consistency):事务执行的结果,须使数据从一个一致性状态,变为另一个一致性状态。
- 隔离性(Isolation):各个事务的执行互不干扰,任何事务的内部操作对其他的事务都是隔离的。
- 持久性(Durability):事务一旦提交,对数据所做的任何改变都要记录到永久存储器中。
事务的隔离性
-
常见的并发异常
-
- 第一类丢失更新、第二类丢失更新。
- 脏读、不可重复读、幻读。
-
常见的隔离级别
-
- Read Uncommitted:读取未提交的数据。
- Read Committed:读取已提交的数据。
- Repeatable Read:可重复读。
- Serializable:串行化
第一类的丢失更新
某一个事务的回滚,导致另外一个事务已更新的数据丢失了。
第二类丢失更新
某一个事务的提交,导致另外一个事务已更新的数据丢失了。
脏读
某一个事务,读取了另外一个事务未提交的数据。
不可重复读
某一个事务,对同一个数据前后读取的结果不一致。
幻读
某一个事务,对同一个表前后查询到的行数不一致。
事务隔离级别
实现机制
-
悲观锁(数据库)
-
- 共享锁(S锁)
事务A对某数据加了共享锁后,其他事务只能对该数据加共享锁,但不能加排他锁。 - 排他锁(X锁)
事务A对某数据加了排他锁后,其他事务对该数据既不能加共享锁,也不能加排他锁。
- 共享锁(S锁)
-
乐观锁(自定义)
-
- 版本号、时间戳等
在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,否则就更新数据(版本号+1)。
- 版本号、时间戳等
Spring事务管理
-
声明式事务
-
- 通过XML配置,声明某方法的事务特征。
- 通过注解,声明某方法的事务特征。
-
编程式事务
-
- 通过 TransactionTemplate 管理事务,并通过它执行数据库的操作。
演示声明式事务
1.在AlphaService中写一个新方法加@Transaction注解
/**
* 传播机制--两个不同的业务都有可能有不同隔离级别且可能一个业务使用了另一个业务,
* 传播机制就是解决不同隔离隔离级别同时出现的情况。
* Propagation.REQUIRED:支持当前事务,就是调用者事务,如果不存在那就创建新事务
* Propagation.REQUIRES_NEW:创建一个事务,并且暂停当前事务(外部事务)
* Propagation.NESTED:如果存在外部事务,那么就会嵌套在外部事务之中,A调B,B有独立提交和回滚的能力
* 否则和REQUIRED一样。
*/
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public Object save1(){
//新增用户
User user = new User();
user.setUsername("hsw");
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5("123"+user.getSalt()));
user.setEmail("hsw@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);
int i = 1/0;
return "ok";
}
2.写个测试方法调用这个方法,发现数据库中并没有插入任何数据
演示编程式事务
1.在加一个方法
@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 transactionStatus) {
//新增用户
User user = new User();
user.setUsername("hsw");
user.setSalt(CommunityUtil.generateUUID().substring(0,5));
user.setPassword(CommunityUtil.md5("123"+user.getSalt()));
user.setEmail("hsw@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);
int i = 1/0;
return "ok";
}
});
}
2.测试发现也没有插入数据
五、显示评论
-
数据层
-
- 根据实体查询一页评论数据。
- 根据实体查询评论的数量。
-
业务层
-
- 处理查询评论的业务。
- 处理查询评论数量的业务。
-
表现层
-
- 显示帖子详情数据时,
- 同时显示该帖子所有的评论数据。
根据评论的数据库我们创建一套实体类
id表明这个评论发出的早晚顺序
user_id 表明这个评论发出的用户
entity_type 表明这个评论的类型(是属于帖子的评论,还是评论的评论,还是问题的评论)
entity_id 表明这个评论的帖子是哪一个
target_id 表明这个帖子所指向的地址
content 表明的是的是帖子的内容
status 表明的是这个评论的状态
create_time 表明的是这个帖子创立的时间
数据层
先看comment表结构
- entity_type:评论的类型,比如帖子的评论,评论用户评论的评论 - -
- entity_id:评论的帖子是哪一个
- target_id:记录评论指向的人
- content:评论的内容
- status:表明状态 0为正常的 1为删除的或者是错误的
- create_time:创建的时间
1.写个实体类
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;
2.写个创建新的Mapper类
@Repository
public interface CommentMapper {
List<Comment> selectCommentByEntity(int entityType,int entityId,int offset,int limit);
int selectCountByEntity(int entityType,int entityId);
}
3.写对应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.hsw.community.dao.CommentMapper">
<sql id="selectFields">
id, user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<select id="selectCommentByEntity" resultType="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>
别忘了写完测试一波
业务层
很简单
@Service
public class CommentService {
@Autowired
private CommentMapper commentMapper;
public List<Comment> findCommentsByEntity(int entityType,int entityId,int offset,int limit){
return commentMapper.selectCommentByEntity(entityType,entityId,offset,limit);
}
public int findCommentCount(int entityType,int entityId){
return commentMapper.selectCountByEntity(entityType,entityId);
}
}
表现层
- controller:处理请求
- 页面:展现数据
1.直接在DiscussPostController中加写内容
@Autowired
private UserService userService;
@Autowired
private CommentService commentService;
@RequestMapping(path = "/detail/{discussPostId}",method = RequestMethod.GET)
//如果参数中有bean,最终springmvc都会存在model中,所以Page会存到model中
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page){ //如果参数中有bean,最终springmvc都会存在model中
//查询这个铁子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post",post);
//根据userId查名字
User user = userService.findUserById(post.getUserId());
model.addAttribute("user",user);
//查评论的分页信息
page.setLimit(5);
page.setPath("/discuss/detail/"+discussPostId);
page.setRows(post.getCommentCount()); //帖子相关字段中冗余存了一个commentcount
//帖子的评论:称为--评论
//评论的评论:称为--回复
//评论列表
List<Comment> comments = commentService.findCommentsByEntity(
ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
List<Map<String,Object>> commentVoList = new ArrayList<>();
if(comments!=null){
for(Comment c:comments){
//评论Vo :Vo的意思是viewObject的意思 视图对象
Map<String,Object> commentVo = new HashMap<>();
//放评论
commentVo.put("comment",c);
//放作者
commentVo.put("user",userService.findUserById(c.getUserId()));
//回复列表
List<Comment> replys = commentService.findCommentsByEntity(ENTITY_COMMENT, c.getId(), 0, Integer.MAX_VALUE);//不分页了
//回复的Vo列表
List<Map<String,Object>> replyVoList = new ArrayList<>();
if(replys!=null){
for(Comment r:replys){
Map<String,Object> replyVo = new HashMap<>();
//放回复
replyVo.put("reply","r");
//放回复者user
replyVo.put("user",userService.findUserById(r.getUserId()));
//放被回复者,如果有的话
User target = r.getTargetId() == 0 ? null : userService.findUserById(r.getTargetId());
replyVo.put("target",target);
replyVoList.add(replyVo);
}
}
//回复加入进来
commentVo.put("replys",replyVoList);
//一条评论回复的数量
int replyCount = commentService.findCommentCount(ENTITY_COMMENT, c.getId());
commentVo.put("replyCount",replyCount);
//加入评论Vo
commentVoList.add(commentVo);
}
}
//传给模板
model.addAttribute("comments",commentVoList);
return "site/discuss-detail";
}
2.处理模板
还有一堆慢慢写就没事
3.分页处理直接复用index页面的分页就可
因为都一样并且controller中查询恢复帖子的列表也是根据当前页面offset和limit查的
六、添加评论
-
数据层
-
- 增加评论数据。
- 修改帖子的评论数量。
-
业务层
-
- 处理添加评论的业务:
先增加评论、再更新帖子的评论数量。
- 处理添加评论的业务:
-
表现层
-
- 处理添加评论数据的请求。
- 设置添加评论的表单。
数据层
1.增加插入评论的mapper
2.增加帖子回复数量的mapper
业务层
1.DiscussPostService
2.CommentService
@Autowired
private DiscussPostService discussPostService;
@Autowired
private SensitiveFilter sensitiveFilter;
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public int addComment(Comment comment){
if(comment==null){
throw new IllegalArgumentException("评论不能为空");
}
//处理内容
comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
comment.setContent(sensitiveFilter.filter(comment.getContent()));
//添加评论
int rows = commentMapper.insertComment(comment);
//如果是给帖子回复
if(comment.getEntityType()== CommunityContant.ENTITY_TYPE_POST){
int count = commentMapper.selectCountByEntity(CommunityContant.ENTITY_TYPE_POST, comment.getEntityId());
discussPostService.updateCommentCount(comment.getEntityId(),count);
}
return rows;
}
表现层
1.单独创建一个Controller—CommentController
@Controller
@RequestMapping("/comment")
public class CommentController {
@Autowired
private CommentService commentService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path = "/add/{discussPostId}",method = RequestMethod.POST)
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;
}
}
2.处理页面
- 给帖子回复
- 给评论回复
- 给某个人回复
七、私信之显示私信列表
-
私信列表
-
- 查询当前用户的会话列表,每个会话只显示一条最新的私信。
- 支持分页显示。
个人想法:
首先创建message实体类,创建message-mapper。我们发现message-mapper可能有这么几种方法会在展示层上用到,我们可以先写。首先是我们在会话列表中需要展示最新的私信,我们还需要得出总的会话行数,进行分页的操作。第二,我们点进私信详情时,展示与之对话的私信,并且也将分页功能实现
表设计
- conversasion_id:表明通信的双方id拼接,规定小的id在前大的在后
数据层
1.写entity
2.写mapper
@Repository
public interface MessageMapper {
//查询当前用户的会话列表,针对每个会话只返回一条最新的私信
List<Message> selectConversation(int userId, int offset, int limit);
//查询当前用户的会话数量
int selectConversationCount(int userId);
//查询某个会话所包含的私信列表
List<Message> selectLetter(String conversationId,int offset,int limit);
//查询某个会话所包含的私信数量
int selectLetterCount(String conversationId);
//查询未读的私信数量
int selectLetterUnreadCount(int userId,String conversationId);
}
<?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.hsw.community.dao.MessageMapper">
<sql id="selectFields">
id, from_id, to_id, conversation_id, content, status, create_time
</sql>
<select id="selectConversation" 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 create_time 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="selectLetter" resultType="Message">
select <include refid="selectFields"></include>
from message
where status!=2
and from_id!=1
and conversation_id=#{conversationId}
order by create_time 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>
业务层
新建个MessageService即可
@Service
public class MessageService {
@Autowired
private MessageMapper messageMapper;
public List<Message> findConversations(int userId, int offset, int limit) {
return messageMapper.selectConversation(userId, offset, limit);
}
public int findConversationCount(int userId) {
return messageMapper.selectConversationCount(userId);
}
public List<Message> findLetters(String conversationId, int offset, int limit) {
return messageMapper.selectLetter(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.新建一个MessageController
@Controller
public class MessageController {
@Autowired
private HostHolder hostHolder;
@Autowired
private MessageService messageService;
@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()));
//拿到对话对方的userId
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";
}
}
2.处理视图
- 先改成Thymeleaf的形式
- 往里边填充数据即可没啥好说的轻车熟路
八、私信之私信详情
-
私信详情
-
- 查询某个会话所包含的私信。
- 支持分页显示。
表现层
1.新增controller方法
@RequestMapping(path = "/letter/detail/{conversationId}",method = RequestMethod.GET)
public String getLetterDetail(@PathVariable("conversationId")String conversationId,Page page,Model model){
//设置分页信息
page.setPath("/letter/detail/"+conversationId);
page.setLimit(5);
page.setRows(messageService.findLetterCount(conversationId));
//私信列表
List<Message> lettersList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
List<Map<String,Object>> letters = new ArrayList<>();
if(lettersList!=null){
for(Message message:lettersList){
Map<String,Object> map = new HashMap<>();
map.put("letter",message);
map.put("fromUser",userService.findUserById(message.getFromId()));
letters.add(map);
}
}
model.addAttribute("letters",letters);
//判断和谁对话
User target = getLetterTarget(conversationId);
model.addAttribute("target",target);
return "/site/letter-detail";
}
private User getLetterTarget(String conversationId){
User user = hostHolder.getUser();
String[] ids = conversationId.split("_");
int id1 = Integer.parseInt(ids[0]);
int id2 = Integer.parseInt(ids[1]);
return user.getId()==id1?userService.findUserById(id2):userService.findUserById(id1);
}
2.处理页面
私信详情返回后处理已读消息个数
解决方法
点开详情前先把看过的数减了
九、发送私信
-
发送私信
-
- 采用异步的方式发送私信。
- 发送成功后刷新私信列表。
数据层
业务层
表现层
1.controller中
@RequestMapping(path = "/letter/send",method = RequestMethod.POST)
@ResponseBody
public String sendLetter(String toName,String content){
//System.out.println(toName+content);
User user = userService.selectByName(toName);
if(user==null){
return CommunityUtil.getJsonString(1,"目标用户不存在");
}
Message message = new Message();
message.setFromId(hostHolder.getUser().getId());
message.setToId(user.getId());
String conversationId = user.getId()<hostHolder.getUser().getId()?
user.getId()+"_"+hostHolder.getUser().getId():
hostHolder.getUser().getId()+"_"+user.getId();
message.setConversationId(conversationId);
message.setContent(content);
message.setCreateTime(new Date());
message.setStatus(0);
messageService.addMessage(message);
return CommunityUtil.getJsonString(0);
}
2.letter.html页面中
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);
}
);
}
3.letter-detail.html中同理
关于读消息后减未读数量
这里老师写了他的方法,因为上边我自己试着写了所以这里就不记录他写的了。
十、统一处理异常
-
@ControllerAdvice
-
- 用于修饰类,表示该类是Controller的全局配置类。
- 在此类中,可以对Controller进行如下三种全局配置:
异常处理方案、绑定数据方案、绑定参数方案。
-
@ExceptionHandler—异常处理方案
-
- 用于修饰方法,该方法会在Controller出现异常后被调用,用于处理捕获到的异常。
-
@ModelAttribute—绑定数据方案(想象下Page类被自动封装进Model里)
-
- 用于修饰方法,该方法会在Controller方法执行前被调用,用于为Model对象绑定参数。
-
@DataBinder—绑定参数方案(想象下Page类的使用)
-
- 用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器。
- 用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器。
往上抛最终处理异常在表现层
SpringBoot自动处理方式
1.把报错的错误码作为页面名放到如下目录下,当报出来相关错误会自动显示报错的页面。
@ControllerAdvice和@ExceptionHandler处理异常
1.写一个跳转到处理页面的controller,这里在HomeController里写
@RequestMapping(path = "/error",method = RequestMethod.GET)
public String getErrorPage(){
return "/error/500";
}
2.在controller包下新建advice包并创建处理异常类
@ControllerAdvice(annotations = Controller.class)
public class HandleException {
private static final Logger logger = LoggerFactory.getLogger(HandleException.class);
@ExceptionHandler({Exception.class})
//public voic 修饰 ,参数可以传进去很多可以查资料
public void handleException(Exception e,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
logger.error("服务器发生异常 "+e.getMessage());
for(StackTraceElement element:e.getStackTrace()){
logger.error(element.toString());
}
//判断是html请求还是json请求,学习这个找法
String xRequestWith = request.getHeader("x-requested-with");
if("XMLHttpRequest".equals(xRequestWith)){
//response.setContentType("application/json"); //浏览器会自动转成json数据
//浏览器会自动返回普通字符串
response.setContentType("application/plain;charSet=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJsonString(1,"服务器异常"));
}else{
response.sendRedirect(request.getContextPath()+"/error");
}
}
}
很方便
十一、统一记录日志
需求:对所有的service记录日志
AOP
AOP的概念
- Aspect Oriented Programing,即面向方面(切面)编程。
- AOP是一种编程思想,是对OOP的补充,可以进一步提高编程的效率。
AOP的术语
- Target:已处理完业务逻辑的代码为目标对象
- Joinpoint:目标对象上有很多地方能被织入代码叫连接点
- Pointcut:切点声明到底织入到哪些位置
- Advice:通知声明到底要处理什么样的逻辑
AOP的实现
-
AspectJ
-
- AspectJ是语言级的实现,它扩展了Java语言,定义了AOP语法。
- AspectJ在编译期织入代码,它有一个专门的编译器,用来生成遵守Java字节码规范的class文件。
-
Spring AOP
-
- Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器。
- Spring AOP在运行时通过代理的方式织入代码,只支持方法类型的连接点。
- Spring支持对AspectJ的集成。
Spring AOP
-
JDK动态代理
-
- Java提供的动态代理技术,可以在运行时创建接口的代理实例。
- Spring AOP默认采用此种方式,在接口的代理实例中织入代码。
-
CGLib动态代理
-
- 采用底层的字节码技术,在运行时创建子类代理实例。
- 当目标对象不存在接口时,Spring AOP会采用此种方式,在子类实例中织入代码。
来个小例子
1.导入一个包
2.新建aspect包写一个AlphaAspect类
@Component
@Aspect
public class AlphaAspect {
@Pointcut("execution(* com.hsw.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;
}
}
正式处理业务
@Component
@Aspect
public class ServiceLoggerAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLoggerAspect.class);
@Pointcut("execution(* com.hsw.community.service.*.*(..))")
public void pointcut(){
}
@Before("pointcut()")
public void before(JoinPoint joinPoint){
//用户{1.2.3.4},在{xxxx},访问了{com.hsw.community.service.xxx}
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String ip = request.getRemoteHost();
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
String TypeName = joinPoint.getSignature().getDeclaringTypeName(); //类名
String methodName = joinPoint.getSignature().getName();//方法名
String target = TypeName+"."+methodName;
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]
...