第3章 Spring Boot进阶,开发社区核心功能(上)

1. 过滤敏感词

image-20220714102936221

1. 创建一个储存要过滤的敏感词的文本文件
  1. 首先创建一个文本文件储存要过滤的敏感词

image-20220714163540442

在下面的工具类中我们会读取这个文本文件,这里提前给出

@PostConstruct   // 这个注解表示当容器实例化这个bean(服务启动的时候)之后在调用构造器之后这个方法会自动的调用
public void init(){
    try(
            // 读取写有“敏感词”的文件,getClass表示从程序编译之后的target/classes读配置文件,读之后是字节流
            // java7语法,在这里的句子最后会自动执行close语句
            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());
    }
}
2. 开发过滤敏感词的工具类
  1. 开发过滤敏感词组件

为了方便以后复用,我们把过滤敏感词写成一个工具类SensitiveFilter。

@Component
public class SensitiveFilter {

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

    // 当检测到敏感词后我们要把敏感词替换成什么符号
    private static final String REPLACEMENT = "***";

    // 根节点
    private TrieNode rootNode = new TrieNode();

    @PostConstruct   // 这个注解表示当容器实例化这个bean(服务启动的时候)之后在调用构造器之后这个方法会自动的调用
    public void init(){
        try(
                // 读取写有“敏感词”的文件,getClass表示从程序编译之后的target/classes读配置文件,读之后是字节流
                // java7语法,在这里的句子最后会自动执行close语句
                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为空,初始化子节点;subNode不为空,直接用就可以了
                subNode = new TrieNode();
                tempNode.addSubNode(c, subNode);
            }
            // 指针指向子节点,进入下一轮循环
            tempNode = subNode;
        }
        // 最后要设置结束标识
        tempNode.setKeywordEnd(true);
    }

    /**
     * 过滤敏感词
     * @param text 待过滤的文本
     * @return  过滤后的文本
     */
    public String filter(String text){
        if(StringUtils.isBlank(text)){
            // 待过滤的文本为空,直接返回null
            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、3向右走一步
                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字符串替换掉,存 REPLACEMENT (里面是***)
                sb.append(REPLACEMENT);
                // 进入下一个位置
                begin = ++position;
                // 重新指向根节点
                tempNode = rootNode;
            } else {
                // 检查下一个字符
                position++;
            }
        }
        return sb.toString();
    }

    // 判断是否为特殊符号,是则返回true,不是则返回false
    private boolean isSymbol(Character c){
        // CharUtils.isAsciiAlphanumeric(c)方法:a、b、1、2···返回true,特殊字符返回false
        // 0x2E80  ~  0x9FFF 是东亚的文字范围,东亚文字范围我们不认为是符号
        return  !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
    }

    // 前缀树
    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);
        }
    }
}

image-20220714164533306

上面就是过滤敏感词工具类的全部代码,接下来我们来解释一下开发步骤

开发过滤敏感词组件分为三步:

  1. 定义前缀树(Tree)

我们将定义前缀树写为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);
    }
}

image-20220714164751200

  1. 根据敏感词,初始化前缀树

将敏感词添加到前缀树中

// 将一个敏感词添加到前缀树中
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为空,初始化子节点;subNode不为空,直接用就可以了
            subNode = new TrieNode();
            tempNode.addSubNode(c, subNode);
        }
        // 指针指向子节点,进入下一轮循环
        tempNode = subNode;
    }
    // 最后要设置结束标识
    tempNode.setKeywordEnd(true);
}

image-20220714164834667

  1. 编写过滤敏感词的方法

如何过滤文本中的敏感词:

特殊符号怎么处理:

image-20220714155813693

敏感词前缀树初始化完毕之后,过滤文本中的敏感词的算法应该如下:

定义三个指针:

  • 指针1指向Tree树
  • 指针2指向待过滤字符串段
  • 指针3指向待过滤字符串段
/**
 * 过滤敏感词
 * @param text 待过滤的文本
 * @return  过滤后的文本
 */
public String filter(String text){
    if(StringUtils.isBlank(text)){
        // 待过滤的文本为空,直接返回null
        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、3向右走一步
            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字符串替换掉,存 REPLACEMENT (里面是***)
            sb.append(REPLACEMENT);
            // 进入下一个位置
            begin = ++position;
            // 重新指向根节点
            tempNode = rootNode;
        } else {
            // 检查下一个字符
            position++;
        }
    }
    return sb.toString();
}

// 判断是否为特殊符号,是则返回true,不是则返回false
private boolean isSymbol(Character c){
    // CharUtils.isAsciiAlphanumeric(c)方法:a、b、1、2···返回true,特殊字符返回false
    // 0x2E80  ~  0x9FFF 是东亚的文字范围,东亚文字范围我们不认为是符号
    return  !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}

image-20220714164949096

最后:建议在测试类中测试一下

image-20220714165045793

经测试,过滤敏感词的工具类开发完成,这个工具会在接下来的发布帖子的功能中用到。

2. 发布帖子

image-20220715090347434

异步请求:当前网页不刷新,还要访问服务器,服务器会返回一些结果,通过这些结果提炼出来的数据对网页做一个局部的刷新(提示、样式等),实现异步请求的技术是AJAX。

演示一下使用 JQuery 发送异步请求的示例

引一个FastJson的包,用这个包里面的 api 处理json字符串的转换,效率更高一点,性能更好一点。

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

首先在 CommunityUtil 工具类中加几个处理json字符串的方法,因为服务器给浏览器返回json字符串,

// 返回JSON字符串
// 参数:编码、提示信息、Map(Map里面封装业务数据)
public static String getJSONString(int code, String msg, Map<String, Object> map){
    /*
    封装成json对象,然后把json对象转换成字符串就得到了一个json格式的字符串
     */
    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);
}

image-20220715092035069

image-20220715110308895

写一个html页面 ajax-demo.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);
                    // 将字符串转换成json对象
                    data = $.parseJSON(data);
                    console.log(typeof (data));
                    console.log(data.code);
                    console.log(data.msg);

                }
            );
        }
    </script>
</body>
</html>
引入 JQuery:在html页面body内加上下面这句话

<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>

image-20220715110615918

测试:

image-20220715110705634


接下来我们来开发发布帖子的功能

  1. 数据访问层(dao)

增加插入帖子的方法

int insertDiscussPost(DiscussPost discussPost);
/*
 声明插入帖子的功能
 */

image-20220715163618969

然后是对应的mapper配置文件

<sql id="insertFields" >
    user_id, title, content, type, status, create_time, comment_count, score
</sql>

    <insert id="insertDiscussPost" parameterType="com.nowcoder.community.entity.DiscussPost">
        insert into discuss_post(<include refid="insertFields"></include>)
        values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
    </insert>

image-20220715163713940

  1. 业务层(service)

@Service
public class DiscussPostService {

    @Autowired
    private DiscussPostMapper discussPostMapper;

    @Autowired
    private SensitiveFilter sensitiveFilter;

    public List<DiscussPost> findDiscussPosts(int userId, int offest, int limit){
        return discussPostMapper.selectDiscussPosts(userId, offest, limit);
    }

    public int findDiscussPostRows(int userId){
        return discussPostMapper.selectDiscussPostRows(userId);
    }

    // 插入帖子
    public int addDiscussPost(DiscussPost post){
        if(post == null){
            throw new IllegalArgumentException("参数不能为空!");
        }
        // 转义HTML标记,对帖子中的标签做一个处理(为了防止帖子中的标签对源码产生不好的影响)
        post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
        post.setContent(HtmlUtils.htmlEscape(post.getContent()));
        // 过滤敏感词
        post.setTitle(sensitiveFilter.filter(post.getTitle()));
        post.setContent(sensitiveFilter.filter(post.getContent()));

        return discussPostMapper.insertDiscussPost(post);
    }

}

image-20220715163935303

  1. 视图层(Controller和页面)

DiscussPostController:

@Component
@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());
        post.setTitle(title);
        post.setContent(content);
        post.setCreateTime(new Date());
        // 帖子类型和状态、得分等默认就是0,不用设置
        discussPostService.addDiscussPost(post);
        // 报错的情况将来统一处理
        return CommunityUtil.getJSONString(0, "发布成功!");   // 0 表示是一个正确的状态
    }
}

image-20220715164101990

index.html:

image-20220715162807617

image-20220715164330898

index.js:

主页的发布帖子功能里面有js,我们修改一下js

image-20220715164523573

测试

image-20220715164559219

接着在数据库中看一下标签是否被转义以及敏感词是否被过滤

image-20220715164722154

3. 帖子详情

image-20220715164957608

在首页的帖子列表页面上,可以随便选一个帖子,然后点它的标题,我们就可以打开一个显示帖子详细信息的页面,然后把帖子的详细内容显示完整,这就是帖子详情的功能。

  1. 数据访问层(dao)

dao接口中增加一个查询帖子的功能:

DiscussPost selectDiscussPostById(int id);
/*
根据帖子id查询帖子的详细内容
 */

image-20220715170009432

然后编写它对应的mapper配置文件

<sql id="selectFields" >
  id, user_id, title, content, type, status, create_time, comment_count, score
</sql>

<select id="selectDiscussPostById" resultType="com.nowcoder.community.entity.DiscussPost" parameterType="Integer">
    select <include refid="selectFields"></include>
    from discuss_post
    where id = #{id}
</select>

image-20220715170328372

  1. 业务层(service)

在DiscussPostService中增加一个查询帖子的方法

public DiscussPost findDiscussPostById(int id){
    return discussPostMapper.selectDiscussPostById(id);
}

image-20220715170551475

  1. 表现层(controller和html)

DiscussPostController:

@Autowired
private UserService userService;

@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model){
    // 由id查询帖子
    DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
    model.addAttribute("post", post);
    // 由于discussPost显示的只有用户的id,我们显示在页面上的肯定是用户的username而不是id,所以我们还需要查一下username
    User user = userService.findUserById(post.getUserId());
    model.addAttribute("user", user);

    return "/site/discuss-detail";
}

index.html:

我们要在首页的每一个帖子上的标题上加一个链接,链接能够访问到上面DiscussPostController中的查询帖子的方法。

image-20220715172853067

discuss-detail.html:

接着我们要处理详情页面数据的展现

image-20220715174406096

image-20220715174442425

image-20220715174624907

image-20220715174704274

测试

image-20220715174824972

image-20220715174851109

4. 事务管理

事务管理常见知识

image-20220716070756484

image-20220716071047348

第一类丢失更新

image-20220716073106097

第二类丢失更新

image-20220716073356286

脏读

image-20220716073511481

不可重复读

image-20220716073549458

幻读

image-20220716073736147

不同的隔离级别可以解决的问题

image-20220716074350851

image-20220716074710087

Spring中的事务管理

image-20220716074946222

Spring中的事务管理有两种方式:

  • 声明式事务

    • 通过xml配置,声明某方法的事务特征

    • 通过注解,声明某方法的事务特征

  • 编程式事务

    • 通过 TransactionTemplate 管理事务,并通过它执行数据库的操作。

两者选一种即可,建议选第一种,因为比较简单,当然如果业务比较复杂,仅仅只是想管理一小部分使用第二种

写一个demo演示Spring中的事务管理

1. 声明式事务管理事务

这里我们通过加注解声明某方法的事务特征

@Service
public class AlphaService {
    @Autowired
    private AlphaDao alphaDao;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private DiscussPostMapper discussPostMapper;

    /*
    isolation 事务的隔离级别
    propagation 事务
的传播机制:
        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());         // 虽然上面我们没有给user设置id,但是执行过数据库操作之后,数据库给user的id
        post.setTitle("Hello");
        post.setContent("新人报道!");
        post.setCreateTime(new Date());
        discussPostMapper.insertDiscussPost(post);

        // 报错
        Integer.valueOf("abc");         // 将 "abc" 这个字符串转换为整数,肯定转不了,报错

        return "ok";
    }
}

image-20220716084142050

测试:

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class TransactionTests {

    @Autowired
    private AlphaService alphaService;

    @Test
    public void testSave1() {
        Object obj = alphaService.save1();
        System.out.println(obj);
    }

}

image-20220716084645109

2. 编程式事务管理

通过 TransactionTemplate 管理事务

@Service
public class AlphaService {
    @Autowired
    private AlphaDao alphaDao;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private DiscussPostMapper discussPostMapper;

    /*
    这个类是Spring自动创建的,我们无须配置,直接注入即可
    */
    @Autowired
    private TransactionTemplate transactionTemplate;

    public Object save2(){
        transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);  // 设置隔离级别
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 设置传播机制

        return transactionTemplate.execute(new TransactionCallback<Object>() {

            @Override
            public Object doInTransaction(TransactionStatus status) {
                // 新增用户
                User user = new User();
                user.setUsername("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";
            }
        });
    }
}

image-20220716090819431

测试:

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class TransactionTests {

    @Autowired
    private AlphaService alphaService;

    @Test
    public void testSave2() {
        Object obj = alphaService.save2();
        System.out.println(obj);
    }

}

image-20220716091005794

5. 显示评论

image-20220716102835471

数据访问层(dao)

  • 根据实体查询一页评论数据
  • 根据实体查询评论的数量

数据库中的评论表

CREATE TABLE `comment` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,	
  `entity_type` int(11) DEFAULT NULL,	# 回复的类型,1代表回复帖子,2代表回复评论
  `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 代表评论的帖子的 id,比如评论的帖子的 id

target_id 代表指向目标评论的人的 id,比如评论别人的帖子,target_id 表示评论的人的 id

content 代表评论的内容

status 代表评论的状态,0 代表正常,1 代表删除或者不可用
*/

image-20220716133615324

comment表对应的实体类

public class Comment {

    private int id;
    private int userId;
    private int entityType;
    private int entityId;
    private int targetId;
    private String content;
    private int status;
    private Date createTime;

   	// 为了以免影响阅读体验,get、set、toString方法没有粘上来,但其实是有的
}

dao接口:

@Mapper
public interface CommentMapper {

    /*
     分页查询评论
     参数:1.评论的类型  2.评论的是哪个评论的id  3.起始页  4.每页限制条数
     */
    List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset, int limit);

    /*
     查询评论的总条数
     参数:1.评论的类型  2.评论的是哪个评论的id
     */
    int selectCountByEntity(int entityType, int entityId);

}

image-20220716141613738

dao接口对应的mapper配置文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.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                 <!--status=0表示这个数据是有效的-->
        and entity_type = #{entityType}
        and entity_id = #{entityId}
        order by create_time
        limit #{offset}, #{limit}
    </select>

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

</mapper>

image-20220716141717466

业务层(Service)

  • 处理查询评论的业务
  • 处理查询评论数量的业务

CommentService:

@Service
public class CommentService {

    @Autowired
    private CommentMapper commentMapper;

    public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit) {
        return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);
    }

    public int findCommentCount(int entityType, int entityId) {
        return commentMapper.selectCountByEntity(entityType, entityId);
    }
    
}

image-20220716142806853

表现层(Controller和themeleaf模板)

  • 显示帖子详情数据时,同时显示该帖子所有的评论数据

CommunityConstant 常量接口中设置两个常量分别表示评论的类型:

/**
 * 回复的实体类型:帖子
 */
int ENTITY_TYPE_POST = 1;

/**
 * 回复的实体类型:评论
 */
int ENTITY_TYPE_COMMENT = 2;

image-20220716163557691

DiscussPostController 中的getDiscussPost方法补充一些代码去在页面上展示具体的评论

@Component
@RequestMapping("/discuss")
public class DiscussPostController implements CommunityConstant {

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private UserService userService;

    @Autowired
    private CommentService commentService;

    @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());
        post.setTitle(title);
        post.setContent(content);
        post.setCreateTime(new Date());
        // 帖子类型和状态、得分等默认就是0,不用设置
        discussPostService.addDiscussPost(post);
        // 报错的情况将来统一处理
        return CommunityUtil.getJSONString(0, "发布成功!");   // 0 表示是一个正确的状态
    }

    @RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
    public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page){
        // 由id查询帖子
        DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
        model.addAttribute("post", post);
        // 由于discussPost显示的只有用户的id,我们显示在页面上的肯定是用户的username而不是id,所以我们还需要查一下username
        User user = userService.findUserById(post.getUserId());
        model.addAttribute("user", user);
        /*
        评论:给帖子的评论
        回复:给评论的评论,因为不止帖子有评论,评论也可能有评论
         */
        page.setLimit(5);               // 设置每页显示的条数
        page.setPath("/discuss/detail/" + discussPostId);   // 设置查询的controller方法路径
        page.setRows(post.getCommentCount());               // 一共有多少条评论的数据

        // 评论列表,因为评论里面有user_id、target_id 我们还需要查user表查到username
        List<Comment> commentList = commentService.findCommentsByEntity(
                ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit()
        );

        // 评论VO列表 (VO 意思是在页面上显示的对象 View Object )
        List<Map<String, Object>> commentVoList = new ArrayList<>();
        if(commentList != null){
            for (Comment comment : commentList) {
                // 每个评论的VO
                Map<String, Object> commentVo = new HashMap<>();
                // 评论
                commentVo.put("comment", comment);
                // 作者
                commentVo.put("user", userService.findUserById(comment.getUserId()));

                // 回复列表
                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) {
                        // 评论的每一个回复Vo
                        Map<String, Object> replyVo = new HashMap<>();
                        // 回复
                        replyVo.put("reply", reply);
                        // 作者
                        replyVo.put("user", userService.findUserById(reply.getUserId()));
                        // 回复目标,等于0表示只是普通的评论,
                        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";
    }


}

首先让 DiscussPostController 实现常量接口 CommunityConstant

image-20220716163659056

然后是方法的详细内容

image-20220716165422659

image-20220716165631778

接下来我们就需要处理模板了:

先处理一下首页 index.html 帖子的评论数量分页逻辑复用

image-20220716170524784

image-20220716182707387

接下来是处理帖子的详细页面 discuss-detail.html

image-20220716184810233

image-20220716185206321

image-20220716185341721

cvoStat 状态的隐含对象:循环变量名后面加上 Stat

cvoStat.count 循环到了第几次

测试

image-20220716185438265

image-20220716185538821

测试成功

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值