仿牛客社区项目(第三章)(上)

第二章:SpringBoot进阶,开发社区核心功能

一、过滤敏感词

  • 前缀树

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

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

1. 添加敏感词字典

resource 包下添加敏感词字典 sensitive-words.txt,内容如下:

赌博
嫖娼
吸毒
开票

 

2. 工具类

util 包下创建 SentisiveFile 类,即敏感词的过滤算法,代码如下:

@Component
public class SensitiveFilter {

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

    // 替换符
    private static final String REPLACEMENT="***";

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

    @PostConstruct
    public void init() {

        try (
            InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        ) {
            String keyword;
            while ((keyword = reader.readLine()) != null) {
                // 添加到前缀树
                this.addKeyword(keyword);
            }
        } catch (Exception 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.addSunNode(c, subNode);
            }

            // 指向子节点,进入下下一轮循环
            tempNode = subNode;

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

    /**
     * 过滤敏感词
     * @param text 待过滤的文本
     * @return 过滤后的文本
     */
    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()){
            if(position < text.length()) {
                Character c = text.charAt(position);

                // 跳过符号
                if (isSymbol(c)) {
                    if (tempNode == rootNode) {
                        begin++;
                        sb.append(c);
                    }
                    position++;
                    continue;
                }

                // 检查下级节点
                tempNode = tempNode.getSubNode(c);
                if (tempNode == null) {
                    // 以begin开头的字符串不是敏感词
                    sb.append(text.charAt(begin));
                    // 进入下一个位置
                    position = ++begin;
                    // 重新指向根节点
                    tempNode = rootNode;
                }
                // 发现敏感词
                else if (tempNode.isKeywordEnd()) {
                    sb.append(REPLACEMENT);
                    begin = ++position;
                }
                // 检查下一个字符
                else {
                    position++;
                }
            }
            // position遍历越界仍未匹配到敏感词
            else{
                sb.append(text.charAt(begin));
                position = ++begin;
                tempNode = rootNode;
            }
        }
        return sb.toString();
    }


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

    //  前缀树
    private class TrieNode {

        // 关键词结束标识
        private  boolean isKeywordEnd = false;

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

        public boolean isKeywordEnd() {
            return isKeywordEnd;
        }

        public void setKeywordEnd(boolean keywordEnd) {
            isKeywordEnd = keywordEnd;
        }

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

        // 获取子节点
        public TrieNode getSubNode(Character c) {
            return sunNodes.get(c);
        }

    }

}

 

3. 功能测试

创建 SensitiveTests 测试方法,代码如下:

@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class SensitiveTests {

    @Autowired
    private SensitiveFilter sensitiveFilter;

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

        String text1 = "这里可@以@赌@博,可以@吸毒@,可以开票@,哈哈哈!";
        text = sensitiveFilter.filter(text1);
        System.out.println(text1);
    }
}

 
运行结果:
在这里插入图片描述
 

二、发布帖子

  • AJAX

    • Asynchronous JavaScript and XML
    • 异步的 JavaScriptXML,不是一门新技术,只是一个新的术语。
    • 使用 AJAX,网页能够将增量更新呈现在页面上,而不需要刷新整个页面。
    • 虽然X代表XML,但目前JSON的使用比XML更加普遍。
  • 示例

    • 使用 jQuery 发送AJAX 请求。
  • 实践

    • 采用AJAX请求,实现发布帖子的功能。

1. 工具类

CommunityUtil 类中添加getJSONString方法及其重载方法,代码如下:

public class CommunityUtil {

    // 生成随机字符串
    public static String generateUUID() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    // MD5加密
    public static String md5(String key) {
        if (StringUtils.isBlank(key)) {
            return null;
        }
        return DigestUtils.md5DigestAsHex(key.getBytes());
    }

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

    // 测试
    public static void main(String[] args) {
        Map<String, Object> map = new HashMap<>();
        map.put("name", "zhangsan");
        map.put("age", 25);
        System.out.println(getJSONString(0, "ok", map));
    }

}

 

2. dao层

DiscussPostMapper 类中添加 insertDiscussPost 方法,代码如下:

@Mapper
public interface DiscussPostMapper {

    List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit);

    // @Param注解用于给参数取别名
    // 如果只有一个参数,并且在<if>里使用,则必须加别名
    // 根据userId查询帖子数量
    int selectDiscussPostRows(@Param("userId") int userId);

    int insertDiscussPost(DiscussPost discussPost);

}

 
对应 discusspost-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.DiscussPostMapper">

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

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

    <select id="selectDiscussPosts" resultType="DiscussPost">
        select
        <include refid="selectFields"></include>
        from discuss_post
        where status != 2
        <if test="userId!=0">
            and user_id = #{userId}
        </if>
        order by type desc, create_time desc
        limit #{offset}, #{limit}
    </select>

    <select id="selectDiscussPostRows" resultType="int">
        select count(id)
        from discuss_post
        where status != 2
        <if test="userId!=0">
            and user_id = #{userId}
        </if>
    </select>


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

</mapper>

 

3. service层

DiscussPostService 类中添加 addDiscussPost 方法,代码如下:

@Service
public class DiscussPostService {

    @Autowired
    private DiscussPostMapper discussPostMapper;


    @Autowired
    private SensitiveFilter sensitiveFilter;

    public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit) {
        return discussPostMapper.selectDiscussPosts(userId, offset, 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);
    }

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

 

4. controller层

controller 包下添加 DiscussPostController类,代码如下:

@Controller
@RequestMapping("/discuss")
public class DiscussPostController {

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private UserService userService;

    @RequestMapping(path = "/add", method = RequestMethod.POST)
    @ResponseBody
    public String addDiscussPost(String title, String content) {
        User user = hostHolder.getUser();
        if (user == null) {
            return CommunityUtil.getJSONString(403,"你还没有登录哦!");
        }

        DiscussPost post = new DiscussPost();
        post.setUserId(user.getId());
        post.setTitle(title);
        post.setContent(content);
        post.setCreateTime(new Date());
        discussPostService.addDiscussPost(post);

        // 报错的情况,会统一处理
        return CommunityUtil.getJSONString(0, "发布成功!");
    }
}

 

5. View层

在首页显示的 “我要发布” 按钮,如果用户没有登录则 不显示此按钮,代码如下:

<button type="button" class="btn btn-primary btn-sm position-absolute rt-0" data-toggle="modal"
    data-target="#publishModal" th:if="${loginUser!=null}">我要发布
</button>

 

6. 功能测试

在这里插入图片描述
 

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

在这里插入图片描述
 

三、帖子详情

  • DiscussPostMapper

  • DiscussPosService

  • DiscussPostController

  • index.html

    • 在帖子标题上增加访问详情页面的链接
  • discuss-detail.html

    • 处理静态资源的访问路径
    • 复用index.htmlheader区域
    • 显示标题、作者、发布时间、帖子正文等内容

1. dao层

DiscussPostMapper 类中添加 selectDiscussPostById 方法,代码如下:

@Mapper
public interface DiscussPostMapper {

    List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit);

    // @Param注解用于给参数取别名
    // 如果只有一个参数,并且在<if>里使用,则必须加别名
    // 根据userId查询帖子数量
    int selectDiscussPostRows(@Param("userId") int userId);

    int insertDiscussPost(DiscussPost discussPost);

    DiscussPost selectDiscussPostById(int id);
}

 

discusspost-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.DiscussPostMapper">

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

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

    <select id="selectDiscussPosts" resultType="DiscussPost">
        select
        <include refid="selectFields"></include>
        from discuss_post
        where status != 2
        <if test="userId!=0">
            and user_id = #{userId}
        </if>
        order by type desc, create_time desc
        limit #{offset}, #{limit}
    </select>

    <select id="selectDiscussPostRows" resultType="int">
        select count(id)
        from discuss_post
        where status != 2
        <if test="userId!=0">
            and user_id = #{userId}
        </if>
    </select>


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

    <select id="selectDiscussPostById" resultType="DiscussPost">
        select <include refid="selectFields"></include>
        from discuss_post
        where id = #{id}
    </select>
</mapper>

 

2. service层

DiscussPostService 类中添加 findDiscussPostById,代码如下:

@Service
public class DiscussPostService {

    @Autowired
    private DiscussPostMapper discussPostMapper;


    @Autowired
    private SensitiveFilter sensitiveFilter;

    public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit) {
        return discussPostMapper.selectDiscussPosts(userId, offset, 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);
    }

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

 

3. controller层

DiscussPostController 类中添加 getDiscussPost 方法,代码如下:

@Controller
@RequestMapping("/discuss")
public class DiscussPostController {

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private HostHolder hostHolder;

    @Autowired
    private UserService userService;

    @RequestMapping(path = "/add", method = RequestMethod.POST)
    @ResponseBody
    public String addDiscussPost(String title, String content) {
        User user = hostHolder.getUser();
        if (user == null) {
            return CommunityUtil.getJSONString(403,"你还没有登录哦!");
        }

        DiscussPost post = new DiscussPost();
        post.setUserId(user.getId());
        post.setTitle(title);
        post.setContent(content);
        post.setCreateTime(new Date());
        discussPostService.addDiscussPost(post);

        // 报错的情况,会统一处理
        return CommunityUtil.getJSONString(0, "发布成功!");
    }

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

 

4. View层

index.html 的帖子标题上增加访问详情页面的链接:

<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</a>

 
discuss-detail.html:

    <!-- 内容 -->
    <div class="main">
        <!-- 帖子详情 -->
        <div class="container">
            <!-- 标题 -->
            <h6 class="mb-4">
                <img src="http://static.nowcoder.com/images/img/icons/ico-discuss.png"/>
                <span th:utext="${post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</span>
                <div class="float-right">
                    <button type="button" class="btn btn-danger btn-sm">置顶</button>
                    <button type="button" class="btn btn-danger btn-sm">加精</button>
                    <button type="button" class="btn btn-danger btn-sm">删除</button>
                </div>
            </h6>
            <!-- 作者 -->
            <div class="media pb-3 border-bottom">
                <a href="profile.html">
                    <img th:src="${user.headerUrl}" class="align-self-start mr-4 rounded-circle user-header" alt="用户头像" >
                </a>
                <div class="media-body">
                    <div class="mt-0 text-warning" th:utext="${user.username}">寒江雪</div>
                    <div class="text-muted mt-3">
                        发布于 <b th:text="${#dates.format(post.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b>
                        <ul class="d-inline float-right">
                            <li class="d-inline ml-2"><a href="#" class="text-primary">赞 11</a></li>
                            <li class="d-inline ml-2">|</li>
                            <li class="d-inline ml-2"><a href="#replyform" class="text-primary">回帖 7</a></li>
                        </ul>
                    </div>
                </div>
            </div>
            <!-- 正文 -->
            <div class="mt-4 mb-3 content" th:utext="${post.content}">

 

5. 功能测试

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

 
 
创作不易,如果有帮助到你,请给文章点个赞和收藏,让更多的人看到!!!
关注博主不迷路,内容持续更新中。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java技术一点通

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值