没想到就两个礼拜左右没有碰一起的项目代码,就忘得差不多了。还熟悉了一下代码,跪了。
okey~接下来的话,来看到今天实现的效果:
每次就是这样的一个评论区
并且在评论成功之后,还要发送消息给博主。
然后如果博主在线的话,那么就要实时进行一个推送,告诉博主,然后博主可以在消息的一个页面看到这个消息:
整个过程的话,和一个正常的博文社区,例如CSDN,掘金,思否,知乎是类似的。
然后我们要做的就是实现这个东西。
那么实现这个功能的话,大概由一下几个点需要明确一下:
那么在这里的话,由于篇幅问题,加上这个消息推送服务并不一定是需要的,并且消息的实现也稍微麻烦一点儿,因此本文消息部分的话会缩略一下。重点是咱们的这个评论的一个实现。
OKey,那么在开始之前的话,我们也是需要去确定一下这个评论的数据结构的,这个结构的话是这样的:
comments:[
{
name:'Huterox',
id:19870621,
commentid: 777,
headImg:'https://ae01.alicdn.com/kf/Hd60a3f7c06fd47ae85624badd32ce54dv.jpg',
comment:'Huterox is best',
time:'2022年9月16日 18:43',
inputShow:false,
reply:[
{
from:'Huterox03',
fromId:19891221,
replyid: 666,
fromHeadImg:'https://ae01.alicdn.com/kf/H94c78935ffa64e7e977544d19ecebf06L.jpg',
to:'Huterox',
toId:19870621,
comment:'66666666666666',
time:'2022年9月26日 20:43',
inputShow:false
},
]
}
]
是的这里的话,我将这个评论部分拆分为两个部分,就是图中这样的两个部分:
那么在我们的数据库里面的话也是这样设计的:
这里说明一下的就是 blogs_com_coment 这个表其实就是回复的那个表。没办法语文没学好,原来评论的评论可以叫做回复(giao)。
okey,那么这个的话就是我们大概的一个数据结构的样子。
okey,我们按照功能来开始进行一个简要的分析和概述。
那么在开始之前的话,我们先来看到整个评论组件的完整代码。是的这里的话还没有去做封装,主要是方便我进行一个博文的编写,同时,也方便给别人用,如果你的流程和我的类似,你直接CV然后改数据结构即可。当然还有个原因是方便我调试,开发阶段。
那么我们完整的代码是这样的:
<template>
<div>
<div v-if="!isLoginFlag" style="height: 300px;width: 100%">
<el-empty image="/static/image/go_login.gif" :description="badTips">el-empty>
div>
<div v-if="isLoginFlag">
<div v-clickoutside="hideReplyBtn" @click="inputFocus" class="my-reply">
<el-avatar class="header-img" :size="40" :src="myHeader">el-avatar>
<div class="reply-info" >
<div
tabindex="0"
contenteditable="true"
id="replyInput"
spellcheck="false"
placeholder="输入评论..."
class="reply-input"
@focus="showReplyBtn"
@input="onDivInput($event)"
>
div>
div>
<div class="reply-btn-box" v-show="btnShow">
<el-button class="reply-btn" size="medium" @click="sendComment" type="primary">发表评论el-button>
div>
div>
<div v-for="(item,i) in comments" :key="i" class="author-title reply-father">
<el-avatar
class="header-img" :size="40" :src="item.headImg"
@click="gotoSpace(item.id)"
>el-avatar>
<div class="author-info">
<span class="author-name">{{item.name}}span>
<span class="author-time">{{item.time}}span>
div>
<div class="icon-btn">
<span @click="showReplyInput(i,item.name,item.id)">
<i style="font-size: 6px" class="iconfont el-icon-s-comment">回复i>
span>
div>
<div class="talk-box">
<p>
<span class="reply">{{item.comment}}span>
p>
div>
<div class="reply-box">
<div v-for="(reply,j) in item.reply" :key="j" class="author-title">
<el-avatar class="header-img" :size="40" :src="reply.fromHeadImg"
@click="gotoSpace(reply.fromId)"
>el-avatar>
<div class="author-info">
<span class="author-name">{{reply.from}}span>
<span class="author-time">{{reply.time}}span>
div>
<div class="icon-btn">
<span @click="showReplyInput(i,reply.from,reply.id)">
<i style="font-size: 6px" class="iconfont el-icon-s-comment">回复i>
span>
div>
<div class="talk-box">
<p>
<span style="font-size: 8px">@ {{reply.to}}:span>
<span class="reply">{{reply.comment}}span>
p>
div>
<div class="reply-box">
div>
div>
div>
<div v-show="_inputShow(i)" class="my-reply my-comment-reply">
<el-avatar class="header-img" :size="40" :src="myHeader">el-avatar>
<div class="reply-info" >
<div tabindex="0" contenteditable="true" spellcheck="false" placeholder="输入回复..." @input="onDivInput($event)" class="reply-input reply-comment-input">div>
div>
<div class=" reply-btn-box">
<el-button class="reply-btn" size="medium" @click="sendCommentReply(i,j)" type="primary">点击回复el-button>
div>
div>
div>
<div>
<el-pagination
background
layout="total, prev, pager, next"
:current-page="page"
page-size=5
@current-change="handleCurrentChange"
:total=total>
el-pagination>
div>
div>
div>
template>
<script>
const clickoutside = {
// 初始化指令
bind(el, binding, vnode) {
function documentHandler(e) {
// 这里判断点击的元素是否是本身,是本身,则返回
if (el.contains(e.target)) {
return false;
}
// 判断指令中是否绑定了函数
if (binding.expression) {
// 如果绑定了函数 则调用那个函数,此处binding.value就是handleClose方法
binding.value(e);
}
}
// 给当前元素绑定个私有变量,方便在unbind中可以解除事件监听
el.vueClickOutside = documentHandler;
document.addEventListener('click', documentHandler);
},
update() {},
unbind(el, binding) {
// 解除事件监听
document.removeEventListener('click', el.vueClickOutside);
delete el.vueClickOutside;
},
};
export default {
name:'comment_article',
// 接受父组件的值
props: {
blogid: String,
myuserid: String,
isLoginFlag: String,
blogTitle: String,
blogUserid: String,
loginToken: String,
required: true
},
data(){
return{
total: 10,
page: 1,
limit: 5,
badTips: "登录后才能查看和发表评论呦~",
btnShow: false,
index:'0',
replyComment:'',
myName:'Lana Del Rey',
myHeader:'https://ae01.alicdn.com/kf/Hd60a3f7c06fd47ae85624badd32ce54dv.jpg',
myId:19870621,
to:'',
toId:-1,
comments:[
]
}
},
directives: {clickoutside},
created() {
this.getMyInfo(this.myuserid);
this.getComments();
},
methods: {
gotoSpace(userid){
this.$router.push({path: "/userinfo",query:{'userid':userid}});
},
handleCurrentChange(val){
//换页
this.page = val;
this.getComments();
},
getComments(){
//得到当前博文的评论
this.axios({
url: "/blog/blog/comment/getCommentList",
method: 'post',
headers: {
"userid": this.myuserid,
"loginType": "PcType",
"loginToken": this.loginToken,
},
data: {
'userid': this.myuserid,
"blogid": this.blogid,
'page': this.page,
'limit': this.limit
}
}).then((res) => {
res = res.data;
if(res.code === 0) {
this.comments = res.page.list;
this.total = res.page.totalCount;
this.page = res.page.currPage;
}else {
this.isLoginFlag=false;
this.badTips = res.msg;
}
}).catch(reason => {
this.isLoginFlag=false;
this.badTips = "当前访问异常";
})
},
sendReply(comment,i,j){
let commentid = null;
let commentUserid = null;
let commentNickname = null;
let commentUserimg = null;
if(j){
commentid = comment[j].replyid;
commentUserid = comment[j].fromId;
commentNickname = comment[j].from;
commentUserimg = comment[j].fromHeadImg;
}else {
commentid = comment.commentid;
commentUserid = comment.id;
commentNickname = comment.name;
commentUserimg = comment.headImg;
}
//发送回复
this.axios({
url: "/blog/blog/comment/upReply",
method: 'post',
headers: {
"userid": this.myuserid,
"loginType": "PcType",
"loginToken": this.loginToken,
},
data:{
"userid": this.myuserid,
"commentid": commentid,
"commentUserid": commentUserid,
"commentNickname": commentNickname,
"commentUserimg": commentUserimg,
"userNickname": this.myName,
"userimg": this.myHeader,
"blogCommentid": comment.commentid,
"comment": this.replyComment,
}
}).then((res)=> {
res = res.data;
if(res.code===0){
this.comments[i].reply.push(res.replyR)
this.replyComment = ''
document.getElementsByClassName("reply-comment-input")[i].innerHTML = ""
}else {
}
})
},
sendBlogComment(){
//发送博文评论
this.axios({
url: "/blog/blog/comment/upComment",
method: 'post',
headers: {
"userid": this.myuserid,
"loginType": "PcType",
"loginToken": this.loginToken,
},
data:{
"userid": this.myuserid,
"blogid": this.blogid,
"blogTitle": this.blogTitle,
"blogUserid": this.blogUserid,
"userNickName": this.myName,
"userImg": this.myHeader,
"content": this.replyComment,
}
}).then((res)=> {
res = res.data;
if(res.code===0){
//此时说明发送成功
let input = document.getElementById('replyInput')
//接受到返回的一个参数
this.comments.push(res.commentR)
this.replyComment = ''
input.innerHTML = '';
this.total+=1;
}else {
//发送有点问题
this.$message.error(res.msg);
}
})
},
getMyInfo(myuserid){
this.axios({
url: "/user/user/userinfo/myinfo",
method: 'get',
params: {
'userid': myuserid,
}
}).then((res) => {
let userpic_base = "https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg";
res = res.data;
if(res.code === 0){
this.myId = this.myuserid;
this.myName = res.myinfo.myName;
this.myHeader = res.myinfo.myHeader;
if(!this.myHeader){this.myHeader=userpic_base}
}else {
this.isLoginFlag=false;
this.badTips = res.msg;
this.loginToken = localStorage.getExpire("LoginToken");
}
}).catch(reason => {
this.isLoginFlag=false;
this.badTips = "网络异常";
})
},
inputFocus(){
var replyInput = document.getElementById('replyInput');
replyInput.style.padding= "8px 8px"
replyInput.style.border ="2px solid #0aaaee"
replyInput.focus()
},
showReplyBtn(){
this.btnShow = true
},
hideReplyBtn(){
this.btnShow = false
replyInput.style.padding= "10px"
replyInput.style.border ="none"
},
showReplyInput(i,name,id){
this.comments[this.index].inputShow = false
this.index =i
this.comments[i].inputShow = true
this.to = name
this.toId = id
},
_inputShow(i){
return this.comments[i].inputShow
},
sendComment(){
if(!this.replyComment){
this.$message({
showClose: true,
type:'warning',
message:'评论不能为空'
})
}else{
this.sendBlogComment()
}
},
sendCommentReply(i,j){
if(!this.replyComment){
this.$message({
showClose: true,
type:'warning',
message:'评论不能为空'
})
}else{
this.sendReply(this.comments[i],i,j)
}
},
onDivInput: function(e) {
this.replyComment = e.target.innerHTML;
},
dateStr(date){
//获取js 时间戳
var time=new Date().getTime();
//去掉 js 时间戳后三位,与php 时间戳保持一致
time=parseInt((time-date)/1000);
//存储转换值
var s;
if(time<60*10){//十分钟内
return '刚刚';
}else if((time<60*60)&&(time>=60*10)){
//超过十分钟少于1小时
s = Math.floor(time/60);
return s+"分钟前";
}else if((time<60*60*24)&&(time>=60*60)){
//超过1小时少于24小时
s = Math.floor(time/60/60);
return s+"小时前";
}else if((time<60*60*24*30)&&(time>=60*60*24)){
//超过1天少于30天内
s = Math.floor(time/60/60/24);
return s+"天前";
}else{
//超过30天ddd
var date= new Date(parseInt(date));
return date.getFullYear()+"/"+(date.getMonth()+1)+"/"+date.getDate();
}
}
},
}
script>
<style lang="stylus" scoped>
.my-reply
padding 10px
background-color #fafbfc
.header-img
display inline-block
vertical-align top
.reply-info
display inline-block
margin-left 5px
width 90%
@media screen and (max-width:1200px) {
width 80%
}
.reply-input
min-height 20px
line-height 22px
padding 10px 10px
color #888686
background-color #fff
border-radius 5px
&:empty:before
content attr(placeholder)
&:focus:before
content none
&:focus
padding 8px 8px
border 2px solid #0aaaee
box-shadow none
outline none
.reply-btn-box
height 25px
margin 10px 0
.reply-btn
position relative
float right
margin-right 15px
.my-comment-reply
margin-left 50px
.reply-input
width flex
.author-title:not(:last-child)
border-bottom: 1px solid rgba(178,186,194,.3)
.author-title
padding 10px
.header-img
display inline-block
vertical-align top
.author-info
display inline-block
margin-left 5px
width 60%
height 40px
line-height 20px
>span
display block
cursor pointer
overflow hidden
white-space nowrap
text-overflow ellipsis
.author-name
color #000
font-size 10px
font-weight bold
.author-time
font-size 8px
.icon-btn
width 30%
padding 0 !important
float right
@media screen and (max-width : 1200px){
width 20%
padding 7px
}
>span
cursor pointer
.iconfont
margin 0 5px
.talk-box
margin 0 50px
>p
margin 0
.reply
font-size 8px
color #000
.reply-box
margin 10px 0 0 50px
background-color #efefef
style>
这块的话我把样式代码啥的都给出来了,够意思了吧。
那么在这里的话,是这样的:
当初始化的时候呢,组件需要接收这几个参数:
blogid: String,
myuserid: String,
isLoginFlag: String,
blogTitle: String,
blogUserid: String,
loginToken: String,
这个很明显嘛,这个评论的话是需要用户登录了之后才可以使用的,那么登录了之后就可以拿到这几个东西。
如果父组件传递给自组件说用户没有登录,那么我们就显示这个呗:
<div v-if="!isLoginFlag" style="height: 300px;width: 100%">
<el-empty image="/static/image/go_login.gif" :description="badTips">el-empty>
div>
之后的话,如果一切就绪,那么这个时候就要去请求到我们的后端,来拿到数据了。
那么对于我们的数据部分的话,这个没啥好说的,我们可以直接看到部分实现代码:
public R getCommentList(GetCommentList getCommentList) throws Exception {
/*
* 1.先去查询当前博文下所有的对博文的评论
* 2.得到当前博文的评论,去得到对这个评论的所有回复
* */
Map<String, Object> params_comment = blogCommentQueryParamsHandler.getBlogCommentQueryParams(getCommentList);
PageUtils page = commentService.queryPage(params_comment);
//此时开始对我们的结果进行处理
List<BlogCommentR> result = new ArrayList<>();
List<CommentEntity> commentEntities = (List<CommentEntity>) page.getList();
for (CommentEntity entity:commentEntities){
BlogCommentR blogCommentR = commentOVHandler.ConversionBlogCommentR(entity);
Long blog_commentid = entity.getCommentid();
List<ReplyR> replyRS = new ArrayList<>();
List<ComComentEntity> comComs = comComentService.list(blogCommentQueryParamsHandler.getBlogCommentReplyQueryParams(blog_commentid));
for(ComComentEntity comComentEntity:comComs){
replyRS.add(commentOVHandler.ConversionReplyR(comComentEntity));
}
blogCommentR.setReply(replyRS);
result.add(blogCommentR);
}
PageUtils pageUtils = new PageUtils(
result,
page.getTotalCount(),
page.getPageSize(),
page.getCurrPage()
);
return R.ok().put("page",pageUtils);
}
那么在这里的话,我要说明一下的就是,我们的这个数据库当中的字段和我们要返回给前端的字段是不太一样的。这个一方面是为了安全,另一方面是因为当时表的命名凸出了我这个“语言表达匮乏”的现象,就比较抽象,说以的话,用OV转换一下,当然也对一些不友好的数据进行一些处理。
这个的话,因人而异。
之后的话是对我们的评论进行一个发布。
那么在这里的话主要一个就是说要对用户发表的一些评论进行过滤。比如Huterox 很帅,这个是可以被允许的,但是Huterox单身狗这个是不被允许的(尽管这是个事实)。
okey,我们先看到这个过滤,这个过滤的话,就是单纯的文本匹配。虽然我以前是打算用NLP去处理的,实在不行上个文本分类得了,但是这个稍微麻烦一点,算力的消耗也大。所以就直接使用了简单一点的方案。
代码如下(其实以前给过):
/**
* 结束类型定义
**/
public enum EndType {
/**
* 有下一个,结束
*/
HAS_NEXT, IS_END
}
import java.util.List;
/**
* 敏感词标记
*/
public class FlagIndex {
/**
* 标记结果
*/
private boolean flag;
/**
* 是否黑名单词汇
*/
private boolean isWhiteWord;
/**
* 标记索引
*/
private List<Integer> index;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public List<Integer> getIndex() {
return index;
}
public void setIndex(List<Integer> index) {
this.index = index;
}
public boolean isWhiteWord() {
return isWhiteWord;
}
public void setWhiteWord(boolean whiteWord) {
isWhiteWord = whiteWord;
}
}
/**
* 初始化敏感词库,将敏感词加入到HashMap中,构建DFA算法模型
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public class WordContext {
/**
* 敏感词字典
*/
private final Map wordMap = new HashMap(2048);
/**
* 是否已初始化
*/
private boolean init;
/**
* 黑名单列表
*/
private final String blackList;
/**
* 白名单列表
*/
private final String whiteList;
public WordContext() {
this.blackList = "/blacklist.txt";
this.whiteList = "/whitelist.txt";
initKeyWord();
}
public WordContext(String blackList, String whiteList) {
this.blackList = blackList;
this.whiteList = whiteList;
initKeyWord();
}
/**
* 获取初始化的敏感词列表
*
* @return 敏感词列表
*/
public Map getWordMap() {
return wordMap;
}
/**
* 初始化
*/
private synchronized void initKeyWord() {
try {
if (!init) {
// 将敏感词库加入到HashMap中
addWord(readWordFile(blackList), WordType.BLACK);
// 将非敏感词库也加入到HashMap中
addWord(readWordFile(whiteList), WordType.WHITE);
}
init = true;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 读取敏感词库,将敏感词放入HashSet中,构建一个DFA算法模型:
* 中 = { isEnd = 0 国 = {
* isEnd = 1 人 = {isEnd = 0 民 = {isEnd = 1} } 男 = { isEnd = 0 人 = { isEnd = 1 }
* } } } 五 = { isEnd = 0 星 = { isEnd = 0 红 = { isEnd = 0 旗 = { isEnd = 1 } } } }
*/
public void addWord(Iterable<String> wordList, WordType wordType) {
Map nowMap;
Map<String, String> newWorMap;
// 迭代keyWordSet
for (String key : wordList) {
nowMap = wordMap;
for (int i = 0; i < key.length(); i++) {
// 转换成char型
char keyChar = key.charAt(i);
// 获取
Object wordMap = nowMap.get(keyChar);
// 如果存在该key,直接赋值
if (wordMap != null) {
nowMap = (Map) wordMap;
} else {
// 不存在则构建一个map,同时将isEnd设置为0,因为他不是最后一个
newWorMap = new HashMap<>(4);
// 不是最后一个
newWorMap.put("isEnd", String.valueOf(EndType.HAS_NEXT.ordinal()));
nowMap.put(keyChar, newWorMap);
nowMap = newWorMap;
}
if (i == key.length() - 1) {
// 最后一个
nowMap.put("isEnd", String.valueOf(EndType.IS_END.ordinal()));
nowMap.put("isWhiteWord", String.valueOf(wordType.ordinal()));
}
}
}
}
/**
* 在线删除敏感词
*
* @param wordList 敏感词列表
* @param wordType 黑名单 BLACk,白名单WHITE
*/
public void removeWord(Iterable<String> wordList, WordType wordType) {
Map nowMap;
for (String key : wordList) {
List<Map> cacheList = new ArrayList<>();
nowMap = wordMap;
for (int i = 0; i < key.length(); i++) {
char keyChar = key.charAt(i);
Object map = nowMap.get(keyChar);
if (map != null) {
nowMap = (Map) map;
cacheList.add(nowMap);
} else {
return;
}
if (i == key.length() - 1) {
char[] keys = key.toCharArray();
boolean cleanable = false;
char lastChar = 0;
for (int j = cacheList.size() - 1; j >= 0; j--) {
Map cacheMap = cacheList.get(j);
if (j == cacheList.size() - 1) {
if (String.valueOf(WordType.BLACK.ordinal()).equals(cacheMap.get("isWhiteWord"))) {
if (wordType == WordType.WHITE) {
return;
}
}
if (String.valueOf(WordType.WHITE.ordinal()).equals(cacheMap.get("isWhiteWord"))) {
if (wordType == WordType.BLACK) {
return;
}
}
cacheMap.remove("isWhiteWord");
cacheMap.remove("isEnd");
if (cacheMap.size() == 0) {
cleanable = true;
continue;
}
}
if (cleanable) {
Object isEnd = cacheMap.get("isEnd");
if (String.valueOf(EndType.IS_END.ordinal()).equals(isEnd)) {
cleanable = false;
}
cacheMap.remove(lastChar);
}
lastChar = keys[j];
}
if (cleanable) {
wordMap.remove(lastChar);
}
}
}
}
}
/**
* 读取敏感词库中的内容,将内容添加到set集合中
*/
private Set<String> readWordFile(String file) throws Exception {
Set<String> set;
// 字符编码
String encoding = "UTF-8";
try (InputStreamReader read = new InputStreamReader(
this.getClass().getResourceAsStream(file), encoding)) {
set = new HashSet<>();
BufferedReader bufferedReader = new BufferedReader(read);
String txt;
// 读取文件,将文件内容放入到set中
while ((txt = bufferedReader.readLine()) != null) {
set.add(txt);
}
}
// 关闭文件流
return set;
}
}
public class WordFilter {
/**
* 敏感词表
*/
private final Map wordMap;
/**
* 构造函数
*/
public WordFilter(WordContext context) {
this.wordMap = context.getWordMap();
}
/**
* 替换敏感词
*
* @param text 输入文本
*/
public String replace(final String text) {
return replace(text, 0, '*');
}
/**
* 替换敏感词
*
* @param text 输入文本
* @param symbol 替换符号
*/
public String replace(final String text, final char symbol) {
return replace(text, 0, symbol);
}
/**
* 替换敏感词
*
* @param text 输入文本
* @param skip 文本距离
* @param symbol 替换符号
*/
public String replace(final String text, final int skip, final char symbol) {
char[] charset = text.toCharArray();
for (int i = 0; i < charset.length; i++) {
FlagIndex fi = getFlagIndex(charset, i, skip);
if (fi.isFlag()) {
if (!fi.isWhiteWord()) {
for (int j : fi.getIndex()) {
charset[j] = symbol;
}
} else {
i += fi.getIndex().size() - 1;
}
}
}
return new String(charset);
}
/**
* 是否包含敏感词
*
* @param text 输入文本
*/
public boolean include(final String text) {
return include(text, 0);
}
/**
* 是否包含敏感词
*
* @param text 输入文本
* @param skip 文本距离
*/
public boolean include(final String text, final int skip) {
boolean include = false;
char[] charset = text.toCharArray();
for (int i = 0; i < charset.length; i++) {
FlagIndex fi = getFlagIndex(charset, i, skip);
if(fi.isFlag()) {
if (fi.isWhiteWord()) {
i += fi.getIndex().size() - 1;
} else {
include = true;
break;
}
}
}
return include;
}
/**
* 获取敏感词数量
*
* @param text 输入文本
*/
public int wordCount(final String text) {
return wordCount(text, 0);
}
/**
* 获取敏感词数量
*
* @param text 输入文本
* @param skip 文本距离
*/
public int wordCount(final String text, final int skip) {
int count = 0;
char[] charset = text.toCharArray();
for (int i = 0; i < charset.length; i++) {
FlagIndex fi = getFlagIndex(charset, i, skip);
if (fi.isFlag()) {
if(fi.isWhiteWord()) {
i += fi.getIndex().size() - 1;
} else {
count++;
}
}
}
return count;
}
/**
* 获取敏感词列表
*
* @param text 输入文本
*/
public List<String> wordList(final String text) {
return wordList(text, 0);
}
/**
* 获取敏感词列表
*
* @param text 输入文本
* @param skip 文本距离
*/
public List<String> wordList(final String text, final int skip) {
List<String> wordList = new ArrayList<>();
char[] charset = text.toCharArray();
for (int i = 0; i < charset.length; i++) {
FlagIndex fi = getFlagIndex(charset, i, skip);
if (fi.isFlag()) {
if(fi.isWhiteWord()) {
i += fi.getIndex().size() - 1;
} else {
StringBuilder builder = new StringBuilder();
for (int j : fi.getIndex()) {
char word = text.charAt(j);
builder.append(word);
}
wordList.add(builder.toString());
}
}
}
return wordList;
}
/**
* 获取标记索引
*
* @param charset 输入文本
* @param begin 检测起始
* @param skip 文本距离
*/
private FlagIndex getFlagIndex(final char[] charset, final int begin, final int skip) {
FlagIndex fi = new FlagIndex();
Map current = wordMap;
boolean flag = false;
int count = 0;
List<Integer> index = new ArrayList<>();
for (int i = begin; i < charset.length; i++) {
char word = charset[i];
Map mapTree = (Map) current.get(word);
if (count > skip || (i == begin && Objects.isNull(mapTree))) {
break;
}
if (Objects.nonNull(mapTree)) {
current = mapTree;
count = 0;
index.add(i);
} else {
count++;
if (flag && count > skip) {
break;
}
}
if ("1".equals(current.get("isEnd"))) {
flag = true;
}
if ("1".equals(current.get("isWhiteWord"))) {
fi.setWhiteWord(true);
break;
}
}
fi.setFlag(flag);
fi.setIndex(index);
return fi;
}
}
public enum WordType {
/**
* 黑名单/白名单
*/
BLACK, WHITE
}
然后的话注意在这里:
准备你的黑白名单
格式如下:
这个的话很好找。
之后的话要使用的话也简单,你可以直接new一个,也可以写一个配置类,这样的话用Spring的IOC去管。
我这里的话就写了个配置类去玩。
@Configuration
public class WordFilterConfig {
@Bean
public WordContext wordContext(){
return new WordContext();
}
@Bean
public WordFilter wordFilter(WordContext wordContext){
return new WordFilter(wordContext);
}
}
之后的话就是我们后端的一些服务的保存了。
public R upBlogComment(UpComment upComment) {
String content = upComment.getContent();
String userid = upComment.getUserid();
if(redisUtils.hasKey(RedisTransKey.getBlogUpComment(userid))){
return R.error(BizCodeEnum.OVER_COMMENT.getCode(), BizCodeEnum.OVER_COMMENT.getMsg());
}
int count = wordFilter.wordCount(content);
if(count>=threshold*content.length()){
return R.error(BizCodeEnum.BAD_COMMENT.getCode(), BizCodeEnum.BAD_COMMENT.getMsg());
}
content = wordFilter.replace(content, '*');
CommentEntity commentEntity = new CommentEntity();
commentEntity.setComment(content);
commentEntity.setBlogid(upComment.getBlogid());
commentEntity.setCreateTime(DateUtils.getCurrentTime());
commentEntity.setUserNickname(upComment.getUserNickName());
commentEntity.setUserImg(upComment.getUserImg());
commentEntity.setUserid(upComment.getUserid());
commentService.save(commentEntity);
//同时这里也是需要去通知博主的,后面再做一个异步编排
if(!userid.equals(upComment.getBlogUserid())){
BlogCommentMsgQ blogCommentMsgQ = new BlogCommentMsgQ();
blogCommentMsgQ.setBlogid(upComment.getBlogid());
blogCommentMsgQ.setBlogtitle(upComment.getBlogTitle());
blogCommentMsgQ.setFromeid(upComment.getUserid());
blogCommentMsgQ.setFromnickname(upComment.getUserNickName());
//这个userid是指要发送给谁,在这里是博主的id
blogCommentMsgQ.setUserid(upComment.getBlogUserid());
blogCommentMsgQ.setFromimg(upComment.getUserImg());
blogCommentMsgQ.setMsg(upComment.getContent());
feignBlogCommentMsgService.blogComment(blogCommentMsgQ);
}
redisUtils.set(RedisTransKey.setBlogUpComment(upComment.getUserid())
, 1, 30, TimeUnit.SECONDS
);
BlogCommentR blogCommentR = commentOVHandler.ConversionBlogCommentR(commentEntity);
return R.ok().put("commentR",blogCommentR);
}
这里唯一要注意到的就是这个消息的转发,这个转发是这样的,这里的话搭建了一个Netty服务器。
这个服务器和客户端保持了长连接,然后别的服务通过http这种短连接调用我这个Netty服务器开放的API消息发送接口,然后这个Netty服务器就会对消息进行转发和存储了。
这里的话没有使用消息队列,什么MQ之类的,一方面是我们这种玩法其实和消息队列差不多。第二个就是和MQ这种东西做一个解耦,这种服务如果走MQ的话对MQ的负载有点大,如果不是价值高一点的目标的话,我是不愿意上MQ的,直接用Netty去转发还是挺香的。
当然你要是前端也上MQ,然后分任务部署几个MQ也可以,反正我消费不起。
那么上面的代码的话,也是和你的具体业务相关的,我这个只是我这里的业务的一些代码是这样写的。
之后的话就是咱们的这个回复,回复的话我这里其实就简单了。
public R upReply(UpReply upReply) {
String userid = upReply.getUserid();
if(redisUtils.hasKey(RedisTransKey.getBlogUpComment(userid))){
return R.error(BizCodeEnum.OVER_COMMENT.getCode(), BizCodeEnum.OVER_COMMENT.getMsg());
}
String comment = upReply.getComment();
int count = wordFilter.wordCount(comment);
if(count>=threshold*comment.length()){
return R.error(BizCodeEnum.BAD_COMMENT.getCode(), BizCodeEnum.BAD_COMMENT.getMsg());
}
ComComentEntity comComentEntity = new ComComentEntity();
//这里的话我们只需要做一个转存就好了
BeanUtils.copyProperties(upReply,comComentEntity);
comComentEntity.setCreateTime(DateUtils.getCurrentTime());
comComentService.save(comComentEntity);
redisUtils.set(RedisTransKey.setBlogUpComment(upReply.getUserid())
, 1, 30, TimeUnit.SECONDS
);
ReplyR replyR = commentOVHandler.ConversionReplyR(comComentEntity);
return R.ok().put("replyR",replyR);
}
这里的话注意的就是评论成功之后,记得把这个上传的评论,回复给返回到前端去,这样的话前端自己把数据加到列表当中。
当然这里有个弊端的就是,当别人回复你的时候,你自己看不到的,你得刷新之后才看得到。当然主要是这个玩意,没必要做实时的。
okey~ 水完了~
之后的话,大概实测了一下,就是那个网页当中的这个玩意:
如果上GPT2来做的话,有点顶不住。所以还是不用这个,还是用先前Seq2Seq做的那个来,用新的好一点的数据集重新训练。然后再加上一点优化,就差不多了。