既然是问答系统,登录成功后,我们可以提出一些问题,等待其他人评论回答。
功能分析:
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。