牛客网第三章

一、过滤敏感词

前缀树
名称:Trie、字典树、查找树
特点:查找效率高,消耗内存大
应用:字符串检索、词频统计、字符串排序等

敏感词过滤器
定义前缀树
根据敏感词,初始化前缀树
编写过滤敏感词的方法

在这里插入图片描述

在这里插入图片描述

定义敏感词

为简便处理定义一个敏感词文件,resources目录下新建一个sensitive-words.txt文件

在这里插入图片描述

定义前缀树

因为不会被外界访问,所以在util包下的SensitiveFilter类中定义了内部类

//前缀树
    private class TrieNode
    {
        //关键词结束标识
        private boolean isKeyWordEnd = false;//是否是敏感词结尾

        //子节点(key是下级字符,value是下级节点)
        private Map<Character,TrieNode> subNodes = new HashMap<>();

        public boolean isKeyWordEnd()
        {
            return isKeyWordEnd;
        }

        public void setKeyWordEnd(boolean keyWordEnd)
        {
            isKeyWordEnd = keyWordEnd;
        }

        //添加子节点
        public void addSubNode(Character c,TrieNode node)
        {
            subNodes.put(c,node);
        }

        //获取子节点
        public TrieNode getTrieNode(Character c)
        {
            return subNodes.get(c);//通过key查value
        }
    }

根据敏感词初始化前缀树

@Component
public class SensitiveFilter
{

    private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);

    //把敏感词替换为
    private static final String REPLACEMENT = "***";

    //初始化trie的根节点
    private TrieNode rootNode = new TrieNode();//空

    @PostConstruct//初始化,当容器实例化Bean以后,此注解的方法就会被自动调用
    public void init()
    {

        try (
                InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
                BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        )
        {
            String keyword;
            while ((keyword = reader.readLine()) != null)
            {
                //添加到前缀树
                this.addKeyWord(keyword);
            }
        } catch (IOException e)
        {
            logger.error("加载敏感词文件失败:" + e.getMessage());
        }

    }

    //将敏感词添加到前缀树
    private void addKeyWord(String keyword)//keyword就是txt里面的内容
    {
        TrieNode tempNode = rootNode;
        for (int i = 0; i < keyword.length(); i++)
        {
            char c = keyword.charAt(i);
            TrieNode subNode = tempNode.getSubNode(c);//往下走
            if (subNode == null)
            {
                //初始化子节点
                subNode = new TrieNode();
                tempNode.addSubNode(c, subNode);
            }
            //指针指向子节点,进行下一轮循环
            tempNode = subNode;

            //设置结束标识
            if (i == keyword.length() - 1)
            {
                tempNode.setKeyWordEnd(true);
            }
        }
    }

过滤敏感词方法

依然是在这个类中定义方法

//实现检索过程,返回的是替换掉敏感词的字符串
    //现在这个方法是啥呢,就是前缀树已经构建完成了,然后根据字符串遍历
    public String filter(String text)
    {
        if (StringUtils.isBlank(text))
        {
            return null;
        }
        //指针1,指向树
        TrieNode tempNode = rootNode;
        //指针2
        int begin = 0;
        //指针3
        int position = 0;
        //结果字符串
        StringBuilder sb = new StringBuilder();

        while (begin < text.length())
        {
            char c = text.charAt(position);

            //跳过符号
            if (isSymbol(c))
            {
                //如果是特殊字符,记录结果,指针2向下走
                if (tempNode == rootNode)
                {
                    sb.append(c);
                    begin++;
                }
                //无论符号在哪里,指针3都要走
                position++;
                continue;
            }

            //检查下级节点中的字是否被前缀树包含
            tempNode = tempNode.getSubNode(c);
            if (tempNode == null)
            {
                //以begin开头的字符串不是敏感词
                sb.append(text.charAt(begin));
                //进入下一个位置
                position = ++begin;//begin++,让后面节点和前置节点在一个char上
                //指针重新指向根节点
                tempNode = rootNode;
            }else if (tempNode.isKeyWordEnd())//如果没结束,返回值是false,直接position++继续检查
            {
                //发现敏感词,将begin~position字符串替换掉
                sb.append(REPLACEMENT);
                //进入下一个位置
                begin = ++ position;
                tempNode = rootNode;
            }else
            {
                position++;//继续检查
            }
        }
        //将最后一批字符计入结果
        sb.append(text.substring(begin));

        return sb.toString();
    }

二、发布帖子

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请求,实现发布帖子的功能。

1.导入Fastjson

<dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>fastjson</artifactId>
       <version>1.2.68</version>
</dependency>

2.在CommunityUtil类中写几个封装成Json的方法

//获取JSON字符串,msg是提示信息,map灵活使用
    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)
        {
            //通过遍历可以来遍历map
            for (String key : map.keySet())
            {
                json.put(key, map.get(key));//把map中的key和value存入JSON
            }
        }
        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

// ajax示例
    @RequestMapping(path = "/ajax", method = RequestMethod.POST)//异步提交一般用post
    @ResponseBody
    public String testAjax(String name, int 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>
    <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/alpha/ajax",
                {"name":"张三","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>

在这里插入图片描述
在这里插入图片描述

为什么要用 HtmlUtils.htmlEscape? 因为有些同学在恶意注册的时候,会使用诸如 这样的名称,会导致网页打开就弹出一个对话框。 那么在转义之后,就没有这个问题了。

在这里插入图片描述
1.老规矩先写插入帖子的dao层

package com.nowcoder.community.dao;

import com.nowcoder.community.entity.DiscussPost;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface DiscussPostMapper
{
    /*
* 开发个人主页的时候会用到userId,可以看到自己发布的帖子,首页是不需要的,需要实现动态sql,如果是0就不拼接,不是0就拼接
* offset是起始行号,limit是每一页最多显示多少条数据
* */
    List<DiscussPost> selectDiscussPosts(int userId,int offset,int limit);

    //如果需要动态拼一个条件,而且方法有且只有一个条件,必须写别名
    int selectDiscussPostRows(@Param("userId") int userId);//查一共有多少帖子//param起别名

    //增加帖子
    int insertDiscussPost(DiscussPost discussPost);


}

2.写对应mapper.xml

<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类中

    public int addDiscussPost(DiscussPost post)
    {
        if (post == null)
        {
            throw new IllegalArgumentException("参数不能为空!");
        }
        //把前端代码中的<>也转化
        //转义HTML标记
        post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
        post.setContent(HtmlUtils.htmlEscape(post.getTitle()));
        //过滤,只有这两个需要转义和过滤
        post.setTitle(sensitiveFilter.filter(post.getTitle()));
        post.setContent(sensitiveFilter.filter(post.getContent()));

        return discussPostMapper.insertDiscussPost(post);
    }

4.controller层新建一个DiscussPostController类

package com.nowcoder.community.controller;

import com.nowcoder.community.entity.DiscussPost;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.DiscussPostService;
import com.nowcoder.community.util.CommunityUtil;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;

@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)
        {
            //403代表没有权限
            return CommunityUtil.getJSONString(403,"你还没有登录");
        }
        DiscussPost post = new DiscussPost();
        post.setUserId(user.getId());//因为user的主键id是没法被get的,所以大可放心的用!
        post.setTitle(title);
        post.setContent(content);
        post.setCreateTime(new Date());
        discussPostService.addDiscussPost(post);
        //报错会统一处理,不在这儿写啦89
        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">&times;</span>
                                </button>
                            </div>
                            <div class="modal-body">
                                <form>
                                    <div class="form-group">
                                        <label for="recipient-name" class="col-form-label">标题:</label>
                                        <input type="text" class="form-control" id="recipient-name">
                                    </div>
                                    <div class="form-group">
                                        <label for="message-text" class="col-form-label">正文:</label>
                                        <textarea class="form-control" id="message-text" rows="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="com.nowcoder.community.entity.DiscussPost">
        select
        <include refid="selectFields"></include>
        from discuss_post
        where id = #{id}
    </select>

3.写service层

 //查询帖子详细信息
    public DiscussPost findDiscussPostById(int id)
    {
        return discussPostMapper.selectDiscussPostById(id);
    }

如果想把userId和id变成头像啥的,可以有两种方法
1.在mapper中写一个关联查询,这样的结果就是可能有些业务并不需要关联查询,就查多了,有冗余
2.因为已经在视图层了,可以注入UserService,通过UserService查两次,但是会慢,后期学Redis之后在内存中访问会快

3.controller层

//查询帖子详细信息
    @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);
        //作者
        User user = userService.findUserById(post.getUserId());
        model.addAttribute("user",user);

        return "/site/discuss-detail";
    }
再重复一下,想要前端利用后端这些属性,需要用model封装一下,调用addAttribute()方法,例如model.addAttribute("user",user) 就是告诉前端,这是个叫做user的东西,你可以用里面的属性 然后就是,Controller里面返回的是项目中的位置,第一层不用写,所以如下所示

在这里插入图片描述
4.处理首页让每个帖子有个链接

在这里插入图片描述
5.处理discuss-detail页面

先改成Thymeleaf模板的格式在这里插入图片描述
接着填充数据
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

四、事务管理

回顾
什么是事务
事务是由N步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行。

事务的特性(ACID)
原子性(Atomicity):事务是应用中不可再分的最小执行体。
一致性(Consistency):事务执行的结果,须使数据从一个一致性状态,变为另一个一致性状态。
隔离性(Isolation):各个事务的执行互不干扰,任何事务的内部操作对其他的事务都是隔离的。
持久性(Durability):事务一旦提交,对数据所做的任何改变都要记录到永久存储器中。

事务的隔离性

常见的并发异常

第一类丢失更新、第二类丢失更新。每一个浏览器的访问都算作一次线程
脏读、不可重复读、幻读。

常见的隔离级别
Read Uncommitted:读取未提交的数据。
Read Committed:读取已提交的数据。
Repeatable Read:可重复读。
Serializable:串行化(要加锁)

详见笔记

演示声明式事务

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

2.写个测试方法调用这个方法,发现数据库中并没有插入任何数据

演示编程式事务

1.在加一个方法

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("beta");
                user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
                user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
                user.setEmail("beta@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";
            }
        });
    }

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类

package com.nowcoder.community.dao;

import com.nowcoder.community.entity.Comment;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;
@Mapper
public interface CommentMapper
{
    List<Comment> selectCommentsByEntity(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.nowcoder.community.dao.CommentMapper">

    <sql id="selectFields">
        id, user_id, entity_type, entity_id, target_id, content, status, create_time
    </sql>


    <select id="selectCommentsByEntity" resultType="com.nowcoder.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 desc
        limit #{offset},#{limit}
    </select>


    <select id="selectCountByEntity" resultType="java.lang.Integer">
        select count(id)
        from comment
        where status = 0
        and entity_type = #{entityType}
        and entity_id = #{entityId}
    </select>
</mapper>

别忘了写完测试一波

业务层

很简单

package com.nowcoder.community.service;

import com.nowcoder.community.dao.CommentMapper;
import com.nowcoder.community.entity.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CommentService
{
    @Autowired
    private CommentMapper commentMapper;

    public List<Comment> findCommentByEntity(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);
    }
}

表现层

controller:处理请求
页面:展现数据,通过Page类来展现分类
1.直接在DiscussPostController中加写内容

@RequestMapping(path = "/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);

        //评论分页信息
        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());
        //评论VO列表--viewobject
        //用map来封装给页面的数据
        List<Map<String, Object>> commentVoList = new ArrayList<>();
        if (commentList != null)
        {
            for (Comment comment : commentList)
            {

                Map<String, Object> commentVo = new HashMap<>();
                //添加评论
                commentVo.put("comment", comment);
                //添加评论作者
                commentVo.put("user", userService.findUserById(comment.getUserId()));//注意这里不能直接写user,这里的user是作者,而非是评论帖子的人

                //回复列表,不做分页了
                List<Comment> replyList = commentService.findCommentsByEntity(
                        ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);

                //回复的Vo列表
                List<Map<String, Object>> replyVoList = new ArrayList<>();
                if (replyList != null)
                {
                    for (Comment reply : replyList)
                    {
                        Map<String, Object> replyVo = new HashMap<>();
                        //先存回复
                        replyVo.put("reply", reply);
                        //再存作者
                        replyVo.put("user", userService.findUserById(reply.getUserId()));

                        //处理targetid,看回复的目标
                        User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
                        replyVo.put("target", target);
                        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";
    }

2.处理模板
在这里插入图片描述
还有一堆慢慢写就没事

3.分页处理直接复用index页面的分页就可
在这里插入图片描述
为都一样并且controller中查询恢复帖子的列表也是根据当前页面offset和limit查的

六、添加评论

数据层
增加评论数据。
修改帖子的评论数量。

业务层
处理添加评论的业务:
先增加评论、再更新帖子的评论数量。

表现层
处理添加评论数据的请求。
设置添加评论的表单。

数据层
1.增加插入评论的mapper在这里插入图片描述
在这里插入图片描述
2.增加帖子回复数量的mapper在这里插入图片描述
在这里插入图片描述
业务层
1.DiscussPostService在这里插入图片描述
2.CommentService

@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
    public int addComment(Comment comment)
    {
        //内容过滤,html与敏感词
        if (comment == null)
        {
            throw new IllegalArgumentException("参数不能为空!");
        }
        //先过滤标签
        comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));

        //过滤敏感词
        comment.setContent(sensitiveFilter.filter(comment.getContent()));
        //添加评论
        int rows = commentMapper.insertComment(comment);

        //更新帖子的评论数量,要根据type判断是给谁评论的

        if (comment.getEntityType() == ENTITY_TYPE_POST)
        {
            int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());//entityid表示的是评论哪一个帖子
            discussPostService.updateCommentCount(comment.getEntityId(), count);
        }


        return rows;
    }

表现层
1.单独创建一个Controller—CommentController

package com.nowcoder.community.controller;

import com.nowcoder.community.entity.Comment;
import com.nowcoder.community.service.CommentService;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
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;

@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

package com.nowcoder.community.dao;

import com.nowcoder.community.entity.Message;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

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

}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nowcoder.community.dao.MessageMapper">


    <sql id="selectFields">
        id, from_id, to_id, conversation_id, content, status, create_time
    </sql>
    <select id="selectConversations" resultType="com.nowcoder.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
        )/*查询所有对话的最大的id,让他来代表*/
        order by id desc
        limit #{offset}, #{limit}
    </select>
    <select id="selectConversationCount" resultType="java.lang.Integer">
        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.nowcoder.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="java.lang.Integer">
        select count(id)
        from message
        where status != 2
          and from_id != 1
          and conversation_id = #{conversationId}
    </select>
    <select id="selectLetterUnreadCount" resultType="java.lang.Integer"></select>


</mapper>

业务层

新建个MessageService即可

package com.nowcoder.community.service;

import com.nowcoder.community.dao.MessageMapper;
import com.nowcoder.community.entity.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MessageService
{
    @Autowired
    private MessageMapper messageMapper;

    //查询会话
    public List<Message> findConversations(int userId, int offset, int limit)
    {
        return messageMapper.selectConversations(userId, offset, limit);
    }

    public int findConversationCount(int userId)
    {
        return messageMapper.selectConversationCount(userId);
    }

    public List<Message> findLetters(String conversationId, int offset, int limit)
    {
        return messageMapper.selectLetters(conversationId, offset, limit);
    }

    public int findLetterCount(String conversationId)
    {
        return messageMapper.selectLetterCount(conversationId);
    }

    public int findLetterUnreadCount(int userId, String conversationId)
    {
        return messageMapper.selectLetterUnreadCount(userId, conversationId);
    }
}

表现层

1.新建一个MessageController

package com.nowcoder.community.controller;

import com.nowcoder.community.entity.Message;
import com.nowcoder.community.entity.Page;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.service.MessageService;
import com.nowcoder.community.service.UserService;
import com.nowcoder.community.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
public class MessageController
{
    @Autowired
    private MessageService messageService;

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private UserService userService;

    //处理私信列表
    @RequestMapping(path = "/letter/list", method = RequestMethod.GET)
    public String getLetterList(Model model, Page page)
    {
        User user = hostHolder.getUser();
        //处理分页信息
        page.setLimit(5);
        page.setPath("/letter/list");
        //一共多少条
        page.setRows(messageService.findConversationCount(user.getId()));

        //会话列表
        List<Message> conversationList =
                messageService.findConversations(user.getId(), page.getOffset(), page.getLimit());

        List<Map<String, Object>> conversations = new ArrayList<>();
        if (conversationList != null)
        {
            for (Message message : conversationList)
            {
                Map<String, Object> map = new HashMap<>();
                map.put("conversation", message);//先传入消息
                map.put("letterCount", messageService.findLetterCount(message.getConversationId()));//会话包含的私信数量
                map.put("unreadCount", messageService.findLetterUnreadCount(user.getId(), message.getConversationId()));//显示每条对话的未读消息
                int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();
                map.put("target", userService.findUserById(targetId));

                conversations.add(map);

            }
        }
        model.addAttribute("conversations", conversations);


        //查询未读消息数量,因为是找所有的未读消息,所以不要传conversationid
        int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);

        model.addAttribute("letterUnreadCount", letterUnreadCount);

        return "/site/letter";
    }

    //私信详情
    @RequestMapping(path = "/letter/detail/{conversationId}", method = RequestMethod.GET)
    public String getLetterDetail(@PathVariable("conversationId") String conversationId, Page page, Model model)
    {
        //处理分页信息
        page.setLimit(5);
        page.setPath("/letter/detail" + conversationId);
        //一共多少条
        page.setRows(messageService.findLetterCount(conversationId));

        List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());

        List<Map<String, Object>> letters = new ArrayList<>();
        //为什么要对letterList进行判断呢,因为letterList才是得到的所有私信
        if (letterList != null)
        {
            for (Message message : letterList)
            {
                Map<String, Object> map = new HashMap<>();
                map.put("letter", message);
                map.put("fromUser", userService.findUserById(message.getFromId()));
                letters.add(map);
            }
        }
        model.addAttribute("letters", letters);

        // 私信目标
        model.addAttribute("target", getLetterTarget(conversationId));

        return "/site/letter-detail";
    }

    private User getLetterTarget(String conversationId)
    {
        String[] ids = conversationId.split("_");
        int id0 = Integer.parseInt(ids[0]);
        int id1 = Integer.parseInt(ids[1]);

        if (hostHolder.getUser().getId() == id0)
        {
            return userService.findUserById(id1);
        } else
        {
            return userService.findUserById(id0);
        }
    }

}

2.处理视图

先改成Thymeleaf的形式
往里边填充数据即可没啥好说的轻车熟路在这里插入图片描述

八、私信之私信详情

在这里插入图片描述
私信详情

查询某个会话所包含的私信。
支持分页显示。

@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.处理页面在这里插入图片描述

私信详情返回后处理已读消息个数在这里插入图片描述解决方法
在这里插入图片描述
点开详情前先把看过的数减了在这里插入图片描述

在这里插入图片描述

九、发送私信

在这里插入图片描述
发送私信

采用异步的方式发送私信。
发送成功后刷新私信列表。

数据层

在这里插入图片描述

业务层

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

表现层

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)
        {
            //这里找的是私信的目标,用hostholder得到当前用户,那么要发送的就是id1了
            return userService.findUserById(id1);
        } else
        {
            return userService.findUserById(id0);
        }
    }

    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());//获取的是私信的id
                }
            }
        }

        return ids;
    }

    @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, "目标用户不存在!");
        }

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

十、统一处理异常

@ControllerAdvice
用于修饰类,表示该类是Controller的全局配置类。
在此类中,可以对Controller进行如下三种全局配置:
异常处理方案、绑定数据方案、绑定参数方案。

@ExceptionHandler—异常处理方案
用于修饰方法,该方法会在Controller出现异常后被调用,用于处理捕获到的异常。

@ModelAttribute—绑定数据方案(想象下Page类被自动封装进Model里)
用于修饰方法,该方法会在Controller方法执行前被调用,用于为Model对象绑定参数。

@DataBinder—绑定参数方案(想象下Page类的使用)
用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器。在这里插入图片描述
往上抛最终处理异常在表现层

SpringBoot自动处理方式

1.把报错的错误码作为页面名放到如下目录下,当报出来相关错误会自动显示报错的页面。在这里插入图片描述

@ControllerAdvice和@ExceptionHandler处理异常

1.写一个跳转到处理页面的controller,这里在HomeController里写

 @RequestMapping(path = "/error",method = RequestMethod.GET)
    public String getErrorPage()
    {
        return "/error/500";
    }

2.在controller包下新建advice包并创建处理异常类

package com.nowcoder.community.controller.advice;

import com.nowcoder.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;

@ControllerAdvice(annotations = Controller.class)//限制范围,只扫描带有Controller注解的Bean
public class ExceptionAdvice
{
    private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);

    //加注解表示处理异常
    @ExceptionHandler(Exception.class)
    public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException//后面两个参数来处理响应
    {
        logger.error("服务器发生异常:" + e.getMessage());
        for (StackTraceElement element : e.getStackTrace())
        {
            logger.error(element.toString());//详细输出所有错误信息
        }

        //通过Request来判断是普通请求还是异步请求,从请求的消息头来获取值
        String xRequestedWith = request.getHeader("x-requested-with");
        if ("XMLHttpRequest" == xRequestedWith)//以xml方式返回,因此是异步请求,普通方式返回html
        {
            response.setContentType("application/plain;charset=utf-8");//plain表示返回普通格式,浏览器将字符串转换回JSON
            PrintWriter writer = response.getWriter();
            writer.write(CommunityUtil.getJSONString(1,"服务器异常"));
        }else
        {
            response.sendRedirect(request.getContextPath()+"/error");//普通请求就直接重定向
        }
    }
}

十一、统一记录日志

需求:对所有的service记录日志在这里插入图片描述
记录日志不是也无需求,不要写在业务里面

用AOP

AOP的概念
Aspect Oriented Programing,即面向方面(切面)编程。
AOP是一种编程思想,是对OOP的补充,可以进一步提高编程的效率。在这里插入图片描述
AOP的术语
针对aspectJ编程,和其他的Service无关,把aspect置入到目标,置入的位置叫连接点
在这里插入图片描述
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类

package com.nowcoder.community.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

//@Component//不属于特定的某一层
//@Aspect
public class AlphaAspect
{
    //定义切点,加了注解就是切点
    @Pointcut("execution(* com.nowcoder.community.service.*.*(..))")//第一个*是返回值,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 proceedingJoinPoint) throws Throwable//在连接点上下都执行
    {
        System.out.println("around before");
        Object obj = proceedingJoinPoint.proceed();//调用目标组件被处理的方法
        System.out.println("around after");
        return obj;
    }


}

正式处理业务

package com.nowcoder.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.RequestAttributes;
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;

@Component
@Aspect
public class ServiceLogAspect
{
    private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);

    @Pointcut("execution(* com.nowcoder.community.service.*.*(..))")//第一个*是返回值,service包下面的所有类的所有的方法的所有参数都要处理
    public void pointcut()
    {

    }

    @Before("pointcut()")
    public void before(JoinPoint joinPoint)//joinpoint是要加强/需要加日志的方法
    {
        //用户(xxx)在xx时间访问量com.nowcoder.community.service.xxx方法
        //获取用户ip,通过request
        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();
        //这里%s是占位符
        logger.info(String.format("用户[%s]","在[%s],访问了[%s].",ip,now,target));
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LeetCode和牛客网都是在线编程练习平台,供程序员进行算法和编程题目的练习。两个平台有一些区别。 LeetCode是一个以算法为主的平台,题目描述简练,直奔主题,通常使用英文进行描述。LeetCode注重算法思维和解题能力的锻炼,题目更偏向于算法和数据结构的应用。许多人在LeetCode上进行练习,习惯了它的题目风格和解题模式。 而牛客网是一个综合性的在线编程平台,除了算法题,还包括面试题、笔试题、实习生项目等。牛客网的题目描述相对LeetCode来说更加贴近实际场景,有更多的描述和背景信息。这也可能导致牛客网的题目相对较长,需要花费一些时间来阅读和理解。 不同的人对于这两个平台的喜好和适应程度各有差异。有的人可能习惯于LeetCode的简练风格,而对牛客网的题目描述感到吃力;有的人则喜欢牛客网提供的更多背景和场景信息。 总的来说,LeetCode和牛客网都是很好的编程练习平台,可以根据个人的需要和喜好进行选择和使用。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [LeetCode和牛客网的对比](https://blog.csdn.net/zr1076311296/article/details/51606300)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值