问答系统(三):问题模块

     既然是问答系统,登录成功后,我们可以提出一些问题,等待其他人评论回答。

功能分析:

1、提出的问题需要进行敏感词过滤:问题不是是一些不正当言论或其他的XXX

2、提出的问题可能有很多,需要进行一个分页展示

一、model

public class Question {
    private int id;
    //问题主题
    private String title;
    //问题内容
    private String content;
    private Date createdDate;
    //谁提出的
    private int userId;
    //评论总数
    private int commentCount;
}

二、Dao:基本的CRUD

注意点:因为基于注解来写的,对于负责的SQL语句还是建议用mapper

例如:这种需要做判断的,查出当前用户的10条问题或者查出显示在主页的10条问题

@Select("<script> \n"+
            "select * from question <if test=\"userId != 0\">" +
            "    WHERE user_id = #{userId}" +
            "    </if>" +
            "    ORDER BY id DESC" +
            "    LIMIT #{offset},#{limit} " +
            "</script> ")

public interface QuestionDAO {
    String TABLE_NAME = " question ";
    String INSERT_FIELDS = " title, content, created_date, user_id, comment_count ";
    String SELECT_FIELDS = " id, " + INSERT_FIELDS;

    @Insert({"insert into ", TABLE_NAME, "(", INSERT_FIELDS,
            ") values (#{title},#{content},#{createdDate},#{userId},#{commentCount})"})
    int addQuestion(Question question);

    @Select("<script> \n"+
            "select * from question <if test=\"userId != 0\">" +
            "    WHERE user_id = #{userId}" +
            "    </if>" +
            "    ORDER BY id DESC" +
            "    LIMIT #{offset},#{limit} " +
            "</script> ")
    List<Question> selectLatestQuestions(@Param("userId") int userId, @Param("offset") int offset,
                                         @Param("limit") int limit);

    @Select({"select ", SELECT_FIELDS, " from ", TABLE_NAME, " where id=#{id}"})
    Question getById(int id);

    //更新问题评论数
    @Update({"update ", TABLE_NAME, " set comment_count = #{commentCount} where id=#{id}"})
    int updateCommentCount(@Param("id") int id, @Param("commentCount") int commentCount);

}

三、Service:除了基本的业务,主要是标签和敏感词过滤

public class QuestionService {
    @Autowired
    QuestionDAO questionDAO;

    @Autowired
    SensitiveService sensitiveService;

    public Question getById(int id) {
        return questionDAO.getById(id);
    }

    public int addQuestion(Question question) {
        //问题标题和内容做标签过滤,保护系统
        question.setTitle(HtmlUtils.htmlEscape(question.getTitle()));
        question.setContent(HtmlUtils.htmlEscape(question.getContent()));
        // 问题标题和内容敏感词过滤
        question.setTitle(sensitiveService.filter(question.getTitle()));
        question.setContent(sensitiveService.filter(question.getContent()));
        return questionDAO.addQuestion(question) > 0 ? question.getId() : 0;
    }

    public List<Question> getLatestQuestions(int userId, int offset, int limit) {
        return questionDAO.selectLatestQuestions(userId, offset, limit);
    }

    public int updateCommentCount(int id, int count) {
        return questionDAO.updateCommentCount(id, count);
    }
}

1、标签过滤:使用了spring提供的工具HtmlUtils过滤标签

2、敏感词过滤:使用了前缀树

(1)、我们需要一个SensitiveWords.txt ,里面放置了我们自定义的敏感词

(2)、建立一个前缀树,把敏感词放进树中,作为树的节点

(3)、取出问题的标题和内容的内容,进行判断。如果是敏感词就替换为我们指定的字符:XXX,***

(4)、我们在类实现InitializingBean接口,InitializingBean接口为bean提供了初始化方法的方式, 它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。我们把SensitiveWords.txt的读取和初始化树节点的逻辑放在这里,这样优化用户体验。

算法实现:三个指针,指向树的根节点a,字符串的第一个b和字符串的第一个c
     1、把b与a比较,判断是不是敏感词节点,是,c不动,b、a移动;继续比较
     2、 不是敏感词的下一节点,把c加入字符数组,c,b,a移动
  

public class SensitiveService implements InitializingBean {
    private static final Logger logger = LoggerFactory.getLogger(SensitiveService.class);
    //默认敏感词替换符
    private static final String DEFAULT_REPLACEMENT = "敏感词";
    
    //内部类
    private class TrieNode {
        // true 关键词的终结 ; false 继续         
        private boolean end = false;

        //用一个map结构构造树:key为字符,value是对应的节点        
        private Map<Character, TrieNode> subNodes = new HashMap<>();

        //向指定位置添加节点树       
        void addSubNode(Character key, TrieNode node) {
            subNodes.put(key, node);
        }

        //获取节点        
        TrieNode getSubNode(Character key) {
            return subNodes.get(key);
        }

        boolean isKeywordEnd() {
            return end;
        }

        void setKeywordEnd(boolean end) {
            this.end = end;
        }

        public int getSubNodeCount() {
            return subNodes.size();
        }
    }
    
    private TrieNode rootNode = new TrieNode();
    
    //判断这个字符是否为数字或东亚文字,如果既不是数字又不是东亚文字,那就是一些其他符号,也要过滤到
    private boolean isSymbol(char c) {
        int ic = (int) c;
        // 0x2E80-0x9FFF 东亚文字范围,如果既不是东亚文字又不是数字字母
        return !CharUtils.isAsciiAlphanumeric(c) && (ic < 0x2E80 || ic > 0x9FFF);
    }
    
    //增加字符进树的方法
    private void addWord(String lineTxt) {
        TrieNode tempNode = rootNode;
        // 循环每个字节
        for (int i = 0; i < lineTxt.length(); ++i) {
            Character c = lineTxt.charAt(i);
            // 过滤符号
            if (isSymbol(c)) {
                continue;
            }
            //拿到这个字符对应节点
            TrieNode node = tempNode.getSubNode(c);
            //没有这个节点,就构造一个,并且存入数据
            if (node == null) { 
                node = new TrieNode();
                tempNode.addSubNode(c, node);
            }
            //节点移动到当前节点位置
            tempNode = node;

            if (i == lineTxt.length() - 1) {
                // 关键词结束, 设置结束标志
                tempNode.setKeywordEnd(true);
            }
        }
    }

    //初始化字典树
    @Override
    public void afterPropertiesSet() throws Exception {
        rootNode = new TrieNode();

        try {
            /*获取当前线程加载txt,Java读取txt文件的内容
             * 步骤:1:先获得文件句柄
             * 2:获得文件句柄当做是输入一个字节码流,需要对这个输入流进行读取
             * 3:读取到输入流后,需要读取生成字节流
             * 4:一行一行的输出。readline()。
             * 5:trim() 函数移除字符串两侧的空白字符或其他预定义字符
             */
            InputStream is = Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream("SensitiveWords.txt");
            InputStreamReader read = new InputStreamReader(is);
            BufferedReader bufferedReader = new BufferedReader(read);
            String lineTxt;
            while ((lineTxt = bufferedReader.readLine()) != null) {
                lineTxt = lineTxt.trim();
                addWord(lineTxt);
            }
            read.close();
        } catch (Exception e) {
            logger.error("读取敏感词文件失败" + e.getMessage());
        }
    }

    /**
     * 过滤敏感词
     */
    public String filter(String text) {
        //如果输入内容为空,直接返回
        if (StringUtils.isBlank(text)) {
            return text;
        }
        String replacement = DEFAULT_REPLACEMENT;
        //使用一个StringBuilder来存储结果
        StringBuilder result = new StringBuilder();

        TrieNode tempNode = rootNode;
        int begin = 0; // 回滚数
        int position = 0; // 当前比较的位置

        while (position < text.length()) {
            char c = text.charAt(position);
             //如果不是符号就跳过
            if (isSymbol(c)) {
                if (tempNode == rootNode) {
                    result.append(c);
                    ++begin;
                }
                ++position;
                continue;
            }

            tempNode = tempNode.getSubNode(c);

            // 当前位置的匹配结束
            if (tempNode == null) {
                // 以begin开始的字符串不存在敏感词
                result.append(text.charAt(begin));
                // 跳到下一个字符开始测试
                position = begin + 1;
                begin = position;
                // 回到树初始节点
                tempNode = rootNode;
            } else if (tempNode.isKeywordEnd()) {
                // 发现敏感词, 从begin到position的位置用replacement替换掉
                result.append(replacement);
                position = position + 1;
                begin = position;
                tempNode = rootNode;
            } else {
                ++position;
            }
        }

        result.append(text.substring(begin));

        return result.toString();
    }



}

四、Controller

  @RequestMapping(value = "/question/add", method = {RequestMethod.POST})
    @ResponseBody
    public String addQuestion(@RequestParam("title") String title, @RequestParam("content") String content) {
        try {
            Question question = new Question();
            question.setContent(content);
            question.setCreatedDate(new Date());
            question.setTitle(title);
            if (hostHolder.getUser() == null) {
                question.setUserId(WendaUtil.ANONYMOUS_USERID);
                // return WendaUtil.getJSONString(999);
            } else {
                question.setUserId(hostHolder.getUser().getId());
            }
            if (questionService.addQuestion(question) > 0) {
                eventProducer.fireEvent(new EventModel(EventType.ADD_QUESTION)
                        .setActorId(question.getUserId()).setEntityId(question.getId())
                .setExt("title", question.getTitle()).setExt("content", question.getContent()));
                return WendaUtil.getJSONString(0);
            }
        } catch (Exception e) {
            logger.error("增加题目失败" + e.getMessage());
        }
        return WendaUtil.getJSONString(1, "失败");
    }

对于这个字典树的:理解好算法,其他都不难。实际就是字符串的移动,树结构也就是一个map。

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值