仿牛客网论坛网站开发流程二


过滤敏感词

可以使用JDK自带的replace方法替换敏感词,但在实际应用中敏感词比较多、字符串可能比较长(发布的一篇文章)这种情况下用replace去替换性能就比较差,使用前缀树来实现过滤敏感词的算法。

  • 前缀树

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

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

配置一个敏感词txt放在resource文件中。

创建一个SensitiveUtil工具类

@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 (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);  
            }  
        }  
    }  
  
    //过滤敏感词  
    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);  
    }  
  
    // 前缀树  
    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);  
        }  
    }  
}

发布帖子

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

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

pom.xml文件中导入相关的配置文件依赖

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

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

在DiscussPostMapper中增加插入数据的方法。

@Insert("insert into discuss_post (user_id,title,content,create_time) values (#{userId},#{title},#{content},#{createTime})")  
int insertDiscussPost(DiscussPost discussPost);

DiscussPostService中增加调用的实现。

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

创建DiscussPostController类

@Controller
@RequestMapping("/discuss")
public class DiscussPostController implements CommunityConstant {
    @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, "发布成功!");
    }
}

index.js

$(function(){  
   $("#publishBtn").click(publish);  
});  

function publish() {  
   $("#publishModal").modal("hide");  
  
   // 获取标题和内容  
   var title = $("#recipient-name").val();  
   var content = $("#message-text").val();  
   // 发送异步请求(POST)  
   $.post(  
      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);  
      }  
   );  
}

帖子详情

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

DiscussPostMapper

@Select("select * from discuss_post where id = #{id}")  
DiscussPost selectDiscussPostById(int id);

DiscussPostController

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

处理index首页让每个帖子有个链接

<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">XXX</a>

处理discuss-detail页面

<!-- 作者 -->  
<div class="media pb-3 border-bottom">  
   <a th:href="@{|/user/profile/${user.id}|}">  
      <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')}">2023-04-15 15:32:18</b>  
         <ul class="d-inline float-right">
 .....

<!-- 正文 -->  
<div class="mt-4 mb-3 content" th:utext="${post.content}">  
   XXXXXXXXXXXX
</div>

事务管理

事务的特性(ACID)

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

常见的隔离级别

  • Read Uncommitted:读取未提交的数据。
  • Read Committed:读取已提交的数据。
  • Repeatable Read:可重复读。
  • Serializable:串行化

Spring事务管理

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

demo

声明式

/**
     * 传播机制--两个不同的业务都有可能有不同隔离级别且可能一个业务使用了另一个业务,
     * 传播机制就是解决不同隔离隔离级别同时出现的情况。
     * Propagation.REQUIRED:支持当前事务,就是调用者事务,如果不存在那就创建新事务
     * Propagation.REQUIRES_NEW:创建一个事务,并且暂停当前事务(外部事务)
     * Propagation.NESTED:如果存在外部事务,那么就会嵌套在外部事务之中,A调B,B有独立提交和回滚的能力
     * 否则和REQUIRED一样。
     */
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
    public Object save1(){
        //新增用户
        User user = new User();
        user.setUsername("hsw");
        user.setSalt(CommunityUtil.generateUUID().substring(0,5));
        user.setPassword(CommunityUtil.md5("123"+user.getSalt()));
        user.setEmail("hsw@qq.com");
        user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
        user.setCreateTime(new Date());
        userMapper.insertUser(user);
        //新增帖子
        DiscussPost post = new DiscussPost();
        post.setUserId(user.getId());
        post.setTitle("hello");
        post.setContent("新人报道");
        post.setCreateTime(new Date());
        discussPostMapper.insertDiscussPost(post);
        int i = 1/0;
        return "ok";
    }

编程式

@Autowired
   private TransactionTemplate transactionTemplate;
   public Object save2(){
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
       return transactionTemplate.execute(new TransactionCallback<Object>() {
           @Override
           public Object doInTransaction(TransactionStatus transactionStatus) {
               //新增用户
               User user = new User();
               user.setUsername("hsw");
               user.setSalt(CommunityUtil.generateUUID().substring(0,5));
               user.setPassword(CommunityUtil.md5("123"+user.getSalt()));
               user.setEmail("hsw@qq.com");
               user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
               user.setCreateTime(new Date());
               userMapper.insertUser(user);
               //新增帖子
               DiscussPost post = new DiscussPost();
               post.setUserId(user.getId());
               post.setTitle("hello");
               post.setContent("新人报道");
               post.setCreateTime(new Date());
               discussPostMapper.insertDiscussPost(post);
               int i = 1/0;
               return "ok";
           }
       });
   }

评论详情

创建实体类Comment

@Data  
@NoArgsConstructor  
@AllArgsConstructor  
@ToString  
@TableName("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;  
  
}
  • user_id: 这个评论发出的用户
  • entity_type: 评论的类型,比如帖子的评论,评论用户评论的评论
  • entity_id:评论的帖子是哪一个
  • target_id:记录评论指向的人
  • content:评论的内容
  • status:表明状态 0为正常的 1为删除的或者是错误的
  • create_time:创建的时间

CommentMapper

@Mapper  
public interface CommentMapper extends BaseMapper<Comment> {  
  
    @Select("select * from comment where status = 0 and entity_type = #{entityType} and " +  
            "entity_id = #{entityId} " +  
            "order by create_time asc " +  
            "limit #{offset}, #{limit}")  
    List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset, int limit);  
  
    @Select("select count(id) from comment where status = 0 and entity_type = #{entityType} and entity_id = #{entityId}")  
    int selectCountByEntity(int entityType, int entityId);  
  
    @Insert("insert into comment (user_id,entity_type,entity_id,target_id,content,status,create_time) " +  
            "values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})")  
    int insertComment(Comment comment);  

	@Select("select * from comment where id = #{id}")  
	Comment selectCommentById(int id);  
	  
	@Select("select count(entity_id) from comment where user_id = #{userId} and entity_type = 1")  
	int selectCountByUserId(int userId);  
	  
	@Select("select * from comment where status = 0 and entity_type = #{entityType} and " +  
	        "user_id = #{userId} " +  
	        "order by create_time asc " +  
	        "limit #{offset}, #{limit}")  
	List<Comment> selectCommentsByUserId(int entityType,int userId, int offset, int limit);
	}

CommentService

@Service  
public class CommentService implements CommunityConstant {  
    @Autowired  
    private CommentMapper commentMapper;  
    @Autowired  
    private SensitiveFilter sensitiveFilter;  
    @Autowired  
    private DiscussPostService discussPostService;  
  
    public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit) {  
        return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);  
    }  
  
    public List<Comment> findCommentsByUserId(int entityType,int userId, int offset, int limit){  
        return commentMapper.selectCommentsByUserId(entityType,userId,offset,limit);  
    }  
  
    public int findCommentCount(int entityType, int entityId) {  
        return commentMapper.selectCountByEntity(entityType, entityId);  
    }  
  
    @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() == ENTITY_TYPE_POST) {  
            int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());  
            discussPostService.updateCommentCount(comment.getEntityId(), count);  
        }  
  
        return rows;  
    }  
  
    public Comment findCommentById(int id) {  
        return commentMapper.selectCommentById(id);  
    }  
  
    public int findCommentCount(int userId){  
        return commentMapper.selectCountByUserId(userId);  
    }  
  
}

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(CommunityConstant.ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());  
  
    // 评论列表  
    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(  
                    CommunityConstant.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()));  
                    // 回复目标 0是对帖的回复,id是对回复的回复  
                    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(CommunityConstant.ENTITY_TYPE_COMMENT, comment.getId());  
            commentVo.put("replyCount", replyCount);  
  
            commentVoList.add(commentVo);  
        }  
    }  
    model.addAttribute("comments", commentVoList);  
  
    return "/site/discuss-detail";  
}

修改discuss-detail.html


文章作者: Aiaa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Aiaa !
  目录