项目初始版本上线,有时间写点东西记录一下项目中的心得体会,通过这个项目学习了很多,要写下来的有很多,先从评论功能开始吧。由于项目需要增加评论功能,之前并无此方面的经验,因此项目开始的一段时间都在寻思着如何进行评论功能的设计。上网搜索一波发现有很多优秀的第三方评论插件可以使用,本来准备直接采用的,但是心里始终有点疙瘩,可能是评论数据放在别人那里不放心的原因,或可能是想一探这些评论系统的究竟,因此最终决定自行设计开发这么一套评论功能。效果截图如下所示,采用的是MySQL数据库,编程语言用的Java。
实现效果如下图:
评论功能实现效果图
二、评论系统概述
评论功能是用户表达对某一主题的想法的很好的一种方式,优秀的评论系统能很好地提高社区的活跃度。各大主流网站也都提供了相应的评论支持。比如:贴吧,新闻类门户网站(APP),UC浏览器等等。
各大网站侧重点不同,对评论功能的要求就不一样,设计出来的评论系统自然就不一样。可能会有:①只可以进行评论,不可以回复,②既可以进行评论,也可以进行回复,然后在这个基础上可能会增加一些额外的功能,比如评论的折叠,审核,优选等。另外,一个良好的UI显示也是非常重要的,能给用户一个直观的视觉上的体验也是评论功能不可或缺的一个要素,毕竟用户都是具有很强的审美能力的,用户的使用体验决定了项目的需求。
对于本项目,设计的是,既可以评论,也可以进行回复,评论和回复分开存储。在显示上,评论和回复显示的位置不同,回复相较于评论向右靠一些,这样,看起来比较舒适,当然也可以设置成其他的样式。评论会显示评论者头像,回复不会。
评论的管理:后台系统应该具备基本的评论管理功能,比如:删除,折叠,优选,排序。这些功能的实现依赖于数据库表的设计,所以,在开始设计的时候,要想清楚自己的项目需要哪些功能。
三、数据库表的设计
本评论功能采用评论和回复分离的方式进行存储,一共设计了两张表,一张用户评论表(comment),一张针对评论的回复表(comment_reply)。评论表主要保存对文章或者回答的评论,回复表保存对每一条评论的回复。
评论表(comment)如下图:主要包括了:评论ID(作为回复表的主键),回答(文章)ID,评论者ID,评论内容,点赞数,评论时间,审核状态
comment表设计
评论回复表(comment_reply)如下图:主要包括了:评论ID,用户ID,被回复人ID,回复内容,点赞数,回复时间。
comment_reply表设计
两张表通过comment_id联系起来(并没有设置主外键,主要是不想维护起来太麻烦),获取某一答案的评论及回复步骤:根据answer_id找到所有的评论,然后,遍历所有的评论,根据comment_id查询到所有的回复(评论者的基本信息,例如头像,名称等需要额外查询)。需要注意的是,在评论和回复数据较多的情况下做好分页处理。
四、程序的实现
采用Java语言进行编程的实现,使用的SSM框架。主要的功能代码如下所示(因为项目有通知功能,看的时候可以略过这部分,跟单纯的评论功能没有太大关系,但是一般要有通知,后面有时间会写站内通知的设计与开发博客):
4.1 添加评论代码如下:(获取到评论相关的参数,然后进行向数据库表插入)
Java
public int addComment(Comment comment) {
try {
Answer commentAnswer = answerMapper.selectByPrimaryKey(comment.getAnswerId());
Long commentId = IDUtils.genItemId();//评论ID
Date createtime = new Date();
//1,填补comment对象的其他参数,进行插入
comment.setCommentId(commentId);
comment.setState(1);//状态: 0 待审核,1通过,2不通过
comment.setPraseCount(0);//一开始插入的点赞数设置为0
comment.setCreatetime(createtime);
comment.setUpdatetime(createtime);
commentMapper.insert(comment);//插入comment记录
//2,跟新Answer的相关一条数据,提示评论数+1
commentAnswer.setCommentNum((commentAnswer.getCommentNum()==null?0:commentAnswer.getCommentNum()) + 1);
answerMapper.updateByPrimaryKeySelective(commentAnswer);
//3,向提醒表插一条数据。这条评论是发给谁的,通知表里面的userId就是谁
if (comment.getUserId() != commentAnswer.getUserId()) { //自己评论自己不会有通知
Remind remind = new Remind();
remind.setRemindId(commentId);
remind.setUserId(commentAnswer.getUserId());
remind.setFromUserId(comment.getUserId());
//commentType:1评论回答,2评论别人的评论,3关注,4支持,5反对,6添加回答
remind.setRemindType(1);
//已读:0否,1是
remind.setReadStatus(0);//否
remind.setCreatetime(createtime);
//插入通知内容,以json的形式存储
RemindContent remindComment = new RemindContent();
remindComment.setContentId(commentAnswer.getAnswerId());
remind.setContent(JsonUtils.objectToJson(remindComment));//通知内容。回答问题的Id
remindMapper.insert(remind);
}
//返回1代表成功
return 1;
} catch (Exception e) {
e.printStackTrace();
return 2;
}
}
4.2 添加回复代码:(前台会传来评论的ID,然后,封装成回复对象进行插入,一个评论ID会对应很多回复)
Java
public int addCommentReply(CommentReply commentReply) {
Long commentId = IDUtils.genItemId();//评论ID
Date createtime = new Date();
commentReply.setPraseCount(0);
commentReply.setCreatetime(createtime);
int retVal = commentReplyMapper.insert(commentReply);
//3,向提醒表插一条数据。这条评论是发给谁的,通知表里面的userId就是谁
if (commentReply.getReplyuserId() != commentReply.getUserId()) {
Remind remind = new Remind();
remind.setRemindId(commentId);
remind.setUserId(commentReply.getReplyuserId());
remind.setFromUserId(commentReply.getUserId());
//commentType:1评论回答,2评论别人的评论,3关注,4支持,5反对,6添加回答
remind.setRemindType(2);
//已读:0是,1否
remind.setReadStatus(1);
remind.setCreatetime(createtime);
remind.setContent(commentReply.getCommentId()+"");
remindMapper.insert(remind);
}
return retVal;
}
4.3获取某一回答的评论和回复(评论分页返回,但是回复没有分页,后面会优化,使用的是pagehelper插件):
Java
public PageBean listAnswerComments(Long answerId,Integer pageNum,Integer pageSize) {
try {
CommentExample commentExample = new CommentExample();
commentExample.setOrderByClause(“createtime DESC”);
Criteria commentCriteria = commentExample.createCriteria();
commentCriteria.andAnswerIdEqualTo(answerId);
PageHelper.startPage(pageNum, pageSize);
List commentList = commentMapper.selectByExampleWithBLOBs(commentExample);//获取具有分页结果的评论数据
List commentStatusList = new ArrayList<>();
for (Comment comment : commentList) {
CommentStatus commentStatus = new CommentStatus(); //评论返回的具体对象
CommentReplyExample example = new CommentReplyExample();
com.pn.mini.model.CommentReplyExample.Criteria criteria = example.createCriteria();
criteria.andCommentIdEqualTo(comment.getCommentId());
List commentReplyList = commentReplyMapper.selectByExample(example);
List commentReplyStatusList = new ArrayList<>();
for (CommentReply commentReply2 : commentReplyList) {
UserBaseInfo commentUser = userBaseInfoMapper.selectByPrimaryKey(commentReply2.getUserId());
UserBaseInfo commentReplyUser = userBaseInfoMapper.selectByPrimaryKey(commentReply2.getReplyuserId());
CommentReplyStatus commentReplyStatus = new CommentReplyStatus();
commentReplyStatus.setCommentId(commentReply2.getCommentId());
commentReplyStatus.setContent(commentReply2.getContent());
commentReplyStatus.setCreatetime(commentReply2.getCreatetime());
commentReplyStatus.setPraseCount(commentReply2.getPraseCount());
commentReplyStatus.setReplyuserId(commentReply2.getReplyuserId());
commentReplyStatus.setReplyuserName(commentReplyUser.getUserName());
commentReplyStatus.setUserId(commentUser.getUserId());
commentReplyStatus.setUserName(commentUser.getUserName());
commentReplyStatusList.add(commentReplyStatus);
}
UserBaseInfo commentUserBaseInfo = userBaseInfoMapper.selectByPrimaryKey(comment.getUserId());
CommentIntegrate commentIntegrate = new CommentIntegrate();
commentIntegrate.setAnswerId(comment.getAnswerId());
commentIntegrate.setAvatar(commentUserBaseInfo.getAvatar());
commentIntegrate.setCommentId(comment.getCommentId());
commentIntegrate.setContent(comment.getContent());
commentIntegrate.setCreatetime(comment.getCreatetime());
commentIntegrate.setPraseCount(comment.getPraseCount());
commentIntegrate.setState(comment.getState());
commentIntegrate.setUpdatetime(comment.getUpdatetime());
commentIntegrate.setUserId(comment.getUserId());
commentIntegrate.setUserName(commentUserBaseInfo.getUserName());
//拼接一条评论的返回对象
commentStatus.setCommentIntegrate(commentIntegrate);
commentStatus.setCommentReplyStatusList(commentReplyStatusList);
commentStatusList.add(commentStatus);
}
PageBean recCommentItemBean = null;//接口返回的对象
PageInfo pageInfo = new PageInfo<>(commentList);
recCommentItemBean = new PageBean<>(commentStatusList);
recCommentItemBean.setDataList(commentStatusList);
recCommentItemBean.setPageNum(pageInfo.getPageNum());
recCommentItemBean.setPages(pageInfo.getPages());
recCommentItemBean.setPageSize(pageInfo.getPageSize());
recCommentItemBean.setSize(pageInfo.getSize());
recCommentItemBean.setTotal(pageInfo.getTotal());
return recCommentItemBean;
} catch (Exception e) {
e.printStackTrace(); //出现异常返回null,controller根据此判断此处调用是否成功
return null;
}
}
4.4 优化思考:
① 回复没有分页返回,回复数据量大的时候需要分页,在在获取回复的时候分页一下即可。
② 获取一条信息,需要再去查询用户表,获取用户的信息,这样就会导致获取一条回答的评论和回复需要查询N次数据表,思考的是增加冗余字段(用户名,用户头像),然后减少这方面的查询开销,当用户头像和名称更改的时候,同步更改这里面的数据,但是一般用户的这方面信息更改较少,总的来说,增加这个冗余字段还是能很大程度提高效率的。
③优化后的数据库表如下(忽略hot_value这样的字段,不同项目有不同需求):
comment表_优化后
comment_reply_优化后
五、总结与反思(后续优化的方向)
虽然评论功能开发完毕,在目前也可以正常的使用,待使用程序的用户的增加,流量的扩大后仍需要继续优化,不然在用户的使用体验上可能会很糟糕,尤其是当数据量大的时候,用户访问可能会感觉到有些慢。不足之处其一:在于获取评论的回复,每次读取数据的时候,需要遍历每一条评论,然后去查找这个评论下的所有回复,之后返回这些数据,这样就会造成获取一片文章的评论需要多次查找数据库,效率就会很低,下一步准备从数据库设计和程序实现两个方面去思考如何优化;其二在于:所有文章的评论都在一张表里面,评论的回复也都在一张表里面,这样就会导致表的条目很多,下一步优化的思路集中于分表操作,具体的实现还在思考中。。。。
评论功能的设计还有很多需要优化的地方,欢迎对这方面有了解的小伙伴一起交流