牛客网项目项目第三章学习笔记

一、过滤敏感词

本小节内容

  • 前缀树
    • 名称:Trie、字典树、查找树
    • 特点:查找效率高,消耗内存大
    • 应用:字符串检索、词频统计、字符串排序等
  • 敏感词过滤器
    • 定义前缀树
    • 根据敏感词,初始化前缀树
    • 编写过滤敏感词的方法

图片说明

步骤1:构建敏感词库

  只有到末端的时候,才能算是敏感词。下面完成敏感词过滤的功能,将敏感词放在文件中以便读取。resources目录下新建一个sensitive-words.txt文件。文件中随便定义几个敏感词。

图片说明

图片说明

步骤2:定义前缀树

  由于不用暴露给外部访问,因此设计为内部类。且该类可以放入自定义的工具包util下。设计为SensitiveFilter类,

@Component
public class 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 getSubNode(Character c) {
            return subNodes.get(c);
        }

    }
}

步骤3:初始化前缀树

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

步骤4:实现过滤方法并进行相关测试

    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 (position < text.length()) {
            char c = text.charAt(position);

            // 跳过符号
            if (isSymbol(c)) {
                // 若指针1处于根节点,将此符号计入结果,让指针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;
                // 重新指向根节点
                tempNode = rootNode;
            } else if (tempNode.isKeywordEnd()) {
                // 发现敏感词,将begin~position字符串替换掉
                sb.append(REPLACEMENT);
                // 进入下一个位置
                begin = ++position;
                // 重新指向根节点
                tempNode = rootNode;
            } else {
                // 检查下一个字符
                position++;
            }
        }

        // 将最后一批字符计入结果
        sb.append(text.substring(begin));

        return sb.toString();
    }

    // 判断是否为符号
    private boolean isSymbol(Character c) {
        // 0x2E80~0x9FFF 是东亚文字范围
        return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
    }

测试代码

@Test
    public void testSensitiveFilter() {
        String text = "这里可以赌博,可以**,可以吸毒,可以***,哈哈哈!";
        text = sensitiveFilter.filter(text);
        System.out.println(text);

        text = "这里可以☆赌☆博☆,可以☆嫖☆娼☆,可以☆吸☆毒☆,可以☆***☆,哈哈哈!";
        text = sensitiveFilter.filter(text);
        System.out.println(text);
        text = "i***";
        text = sensitiveFilter.filter(text);
        System.out.println(text);

        text = "☆f☆a☆b☆c☆";
        text = sensitiveFilter.filter(text);
        System.out.println(text);
    }

  通过编写test方法测试,发现上面的过滤方法不完全正确,在fabcd和abc都是敏感词,且出现fabc时,abc不会被过滤。
  下面为一种解决办法,仍以指针3结尾,但重新改进了,解决了上面的问题,但是仍有一些敏感词不能过滤,如在库中设fabcd和bcd为敏感词时,☆f☆a☆b☆c☆将不能过滤abc,而库中只设计abc时,则可以过滤☆f☆a☆b☆c☆中的abc,这是由于边界问题没考虑完全而出现的问题。
具体原因看这里

    public String filter(String text) {
        //若是空字符串 返回空
        if (StringUtils.isBlank(text)) {
            return null;
        }
        // 根节点
        // 每次在目标字符串中找到一个敏感词,完成替换之后,都要再次从根节点遍历树开始一次新的过滤
        TrieNode tempNode = rootNode;
        // begin指针作用是目标字符串每次过滤的开头
        int begin = 0;
        // position指针的作用是指向待过滤的字符
        // 若position指向的字符是敏感词的结尾,那么text.subString(begin,position+1)就是一个敏感词
        int position = 0;
        //过滤后的结果
        StringBuilder result = new StringBuilder();

        //开始遍历 position移动到目标字符串尾部是 循环结束
        while (position < text.length()) {
            // 最开始时begin指向0 是第一次过滤的开始
            // position也是0
            char c = text.charAt(position);

            //忽略用户故意输入的符号 例如嫖※娼 忽略※后 前后字符其实也是敏感词
            if (isSymbol(c)) {
                //判断当前节点是否为根节点
                //若是根节点 则代表目标字符串第一次过滤或者目标字符串中已经被遍历了一部分
                //因为每次过滤掉一个敏感词时,都要将tempNode重新置为根节点,以重新去前缀树中继续过滤目标字符串剩下的部分
                //因此若是根节点,代表一次新的过滤刚开始,可以直接将该特殊符号字符放入到结果字符串中
                if (tempNode == rootNode) {
                    //将用户输入的符号添加到result中
                    result.append(c);
                    //此时将单词begin指针向后移动一位,以开始新的一个单词过滤
                    begin++;
                }
                //若当前节点不是根节点,那说明符号字符后的字符还需要继续过滤
                //所以单词开头位begin不变化,position向后移动一位继续过滤
                position++;
                continue;
            }
            //判断当前节点的子节点是否有目标字符c
            tempNode = tempNode.getSubNode(c);
            //如果没有 代表当前beigin-position之间的字符串不是敏感词
            // 但begin+1-position却不一定不是敏感词
            if (tempNode == null) {
                //所以只将begin指向的字符放入过滤结果
                result.append(text.charAt(begin));
                //position和begin都指向begin+1
                position = ++begin;
                //再次过滤
                tempNode = rootNode;
            } else if (tempNode.isKeywordEnd()) {
                //如果找到了子节点且子节点是敏感词的结尾
                //则当前begin-position间的字符串是敏感词 将敏感词替换掉
                result.append(REPLACEMENT);
                //begin移动到敏感词的下一位
                begin = ++position;
                //再次过滤
                tempNode = rootNode;
                //&& begin < position - 1
            } else if (position + 1 == text.length()) {
                //特殊情况
                //虽然position指向的字符在树中存在,但不是敏感词结尾,并且position到了目标字符串末尾(这个重要)
                //因此begin-position之间的字符串不是敏感词 但begin+1-position之间的不一定不是敏感词
                //所以只将begin指向的字符放入过滤结果
                result.append(text.charAt(begin));
                //position和begin都指向begin+1
                position = ++begin;
                //再次过滤
                tempNode = rootNode;
            } else {
                //position指向的字符在树中存在,但不是敏感词结尾,并且position没有到目标字符串末尾
                position++;
            }
        }
        return begin < text.length() ? result.append(text.substring(begin)).toString() : result.toString();
    }

图片说明

最终解法:设置为二指针,并进行相关改进,但是同时注意越界,以及最后一位出现符号时,应当重新起头,比较下一个字符。其实刚刚的三指针改进版本也加上这个判断也可以完成屏蔽,这就是没有考虑清楚的边界,会导致程序不健壮。

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)){
                //若指针1处于根节点,将此符号计入结果,让指针2向下走一步
                if(tempNode==rootNode){
                    sb.append(c);
                    begin++;
                }
                //如果最后一位仍然是符号,那就比较下一个,不能直接加,否则会越界
                if(position==text.length()-1){
                    begin++;
                    position=begin;
                    continue;
                }

                //无论符号在开头或者中间指针3都向下走一步
                position++;
                continue;
            }

            // 检查下级节点
            tempNode = tempNode.getSubNode(c);
            if (tempNode == null) {
                // 以begin开头的字符串不是敏感词
                sb.append(text.charAt(begin));
                // 进入下一个位置
                position = ++begin;
                // 重新指向根节点
                tempNode = rootNode;
            } else if (tempNode.isKeywordEnd()) {
                // 发现敏感词,将begin~position字符串替换掉
                sb.append(REPLACEMENT);
                // 进入下一个位置
                begin = ++position;
                // 重新指向根节点
                tempNode = rootNode;
            } else {
                // 检查下一个字符
                if (position < text.length() - 1) {
                    position++;
                }
            }
        }

        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.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。由于是和前端交互,并且这里是演示,因此直接从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/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>
</html>

访问上述静态页面。

图片说明

点击发送后,控制台打印如下信息。
图片说明

2.完成发布帖子功能

1.先从dao层开始写,主要是完成Mapper类或者是Mapper配置文件,以及基础类的代码。评论类DiscussPost类之前完成了。这里就不用写基础类。

@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语句。

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

注意,只有登录了才能发帖。这里给一个403

@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.修改前端页面与之交互的部分。

可以看到,其实很高大上的发帖,其实就是数据库中的,给某个用户的一个表,其中包含了其发表了所有文章,而内容也在表当中。

三、帖子详情

本节内容

  • DiscussPostMapper
  • DiscussPostService
  • DiscussPostController
  • index.html
    • 在帖子标题上增加访问详情页面的链接
  • discuss-detail.html
    • 处理静态资源的访问路径
    • 复用index.html的header区域
    • 显示标题、作者、发布时间、帖子正文等内容

1.DiscussPostMapper增加查询帖子详情(dao层)

根据主键id查

DiscussPost selectDiscussPostById(int id);

2.配置mapper.xml(dao层)

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

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

5.处理首页让每个帖子有个链接,以及相关前端页面。将帖子的内容填充到对应模板中,显示。

四、事务管理

什么是事务

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

事务的隔离性

  • 常见的并发异常
    • 第一类丢失更新、第二类丢失更新。
    • 脏读、不可重复读、幻读。
  • 常见的隔离级别
    • Read Uncommitted:读取未提交的数据。
    • Read Committed:读取已提交的数据。
    • Repeatable Read:可重复读。
    • Serializable:串行化

第一类的丢失更新

某一个事务的回滚,导致另外一个事务已更新的数据丢失了。
图片说明

第二类丢失更新

某一个事务的提交,导致另外一个事务已更新的数据丢失了。
图片说明

脏读

某一个事务,读取了另外一个事务未提交的数据。
图片说明

不可重复读

某一个事务,对同一个数据前后读取的结果不一致。
图片说明

幻读

某一个事务,对同一个表前后查询到的行数不一致。
图片说明

事务隔离级别
图片说明

实现机制

  • 悲观锁(数据库)
    • 共享锁(S锁)
      事务A对某数据加了共享锁后,其他事务只能对该数据加共享锁,但不能加排他锁。
    • 排他锁(X锁)
      事务A对某数据加了排他锁后,其他事务对该数据既不能加共享锁,也不能加排他锁。
  • 乐观锁(自定义)
    • 版本号、时间戳等
      在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,否则就更新数据(版本号+1)。

      Spring事务管理

  • 声明式事务
    • 通过XML配置,声明某方法的事务特征。
    • 通过注解,声明某方法的事务特征。
  • 编程式事务
    • 通过 TransactionTemplate 管理事务,并通过它执行数据库的操作。

声明式事务示例演示
1.注意,虽然只是示例,但是这里是在service层添加的代码。在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/0在发生错误后,事务回滚,因此数据库中没有发生变化。

演示编程式事务

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

依旧无变化,发生回滚。

五、显示评论

  • 数据层
    • 根据实体查询一页评论数据。
    • 根据实体查询评论的数量。
  • 业务层
    • 处理查询评论的业务。
    • 处理查询评论数量的业务。
  • 表现层
    • 显示帖子详情数据时,
    • 同时显示该帖子所有的评论数据。

1.数据层之建表

  首先设计表并建表,由于评论有很多中,当前虽然针对的事帖子的评论,考虑到后续可能有扩展及其他功能的评论,所以为了公用一套逻辑,可以设计一个评论类型。

CREATE TABLE `comment` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `entity_type` int(11) DEFAULT NULL,
  `entity_id` int(11) DEFAULT NULL,
  `target_id` int(11) DEFAULT NULL,
  `content` text,
  `status` int(11) DEFAULT NULL,
  `create_time` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_user_id` (`user_id`),
  KEY `index_entity_id` (`entity_id`)
) ENGINE=InnoDB AUTO_INCREMENT=232 DEFAULT CHARSET=utf8;
  • entity_type:评论的类型,比如帖子的评论,评论用户评论的评论 - -
  • entity_id:评论的帖子是哪一个
  • target_id:记录评论指向的人
  • content:评论的内容
  • status:表明状态
  • create_time:创建的时间

2.dao层开发

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;
//再加上对应的get set方法等等
}

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.nowcoder.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="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

4.写service层。

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

5.controller层

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

6.前端页面的更改。

六、添加评论

  • 数据层
    • 增加评论数据。
    • 修改帖子的评论数量。
  • 业务层
    • 处理添加评论的业务:
    • 先增加评论、再更新帖子的评论数量。
  • 表现层
    • 处理添加评论数据的请求。
    • 设置添加评论的表单。

1.dao层由于评论类已经有,因此先添加对应的mapper接口。

@Repository
public interface CommentMapper {

    List<Comment> selectCommentsByEntity(@Param("entityType")int entityType,@Param("entityId") int entityId, @Param("offset")int offset,@Param("limit") int limit);

    int selectCountByEntity(@Param("entityType")int entityType,@Param("entityId") int entityId);

    int insertComment(Comment comment);
}

2.dao层在mapper配置文件中中添加对应的sql语句

    <insert id="insertComment" parameterType="Comment">
        insert into comment(<include refid="insertFields"></include>)
        values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})
    </insert>

3.由于评论表的设计中,有统计回帖数量,这里应该同时更新帖子的评论(回帖)的数量。因此在评论的类和mapper及配置文件中也做出相关调整。(DiscussPostMapper接口及其对应的mapper.xml文件中修改)

4.sercive层

注意有两个service层需要做出修改(dao层也是两个需要修改的)。1个是Comment Service层,在向上返回时,需要查出所有本帖子的评论,因此帖子service层要添加更新评论的代码。另一个DiscussPostService,因为评论的种类很多,如果是帖子的评论,需要更新对应的数据库。
Comment Service层

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

DiscussPostService中添加
图片说明

5.controller层

1.单独创建一个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.前端页面的处理。

全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务