1. 业务开发
下面是核心的前端调用代码,具体页面效果根据自身模版的风格进行调整。
[!tip] dataType是字典的评论类型,前台在对接业务时需注意,请传递字典label值。 注意:基于业务需求以及安全性考虑,评论插件web端和会员层接口只能保存或查看类型为content的文章评论,如果涉及新的业务场景,建议新增web端接口处理。
1.1. 前端开发
后台需要在自定义字典中增加字典值;找到评论类型,再添加新的字典,如(名称label:文章,数据值value:content)数据值作为数据保存,名称作为展示;
1.1.1. 游客查看文章(content)类型评论
代码片段参考
下面提供的是展示方法是将获取所有评论数据,然后根据top_id(顶级评论id)comment_id(父评论id)和自身id的关系来处理形成评论层级关系
//vue代码片段
...
<div v-for="comment in commentDataList">
{{comment.peopleName?comment.peopleName:'游客'}}
{{comment.commentTime}}
{{comment.commentContent}}
<div v-for="childComment in comment.childCommentDataLists">
{{comment.peopleName?comment.peopleName:'游客'}}
{{comment.commentTime}}
{{comment.commentContent}}
</div>
</div>
...
...
ms.http.get('/comment/list.do', that.form).then(function (res) {
if (res.result) {
that.total = res.data.total; //总数
that.commentList = res.data.rows; //评论记录
// 第一级评论
var topComment = that.commentList.filter(c => !c.topId || c.topId==0)
topComment.forEach(function (data) {
// 如果当前这条评论存在图片
...
if (data.commentPicture){
var commentPicture = JSON.parse(data.commentPicture)
// 存储当前评论的图片
data['pictureList'] = []
commentPicture.forEach(function (picture){
data['pictureList'].push(picture.path)
})
}
...
//初始化子评论列表
data.childCommentDataLists = [];
// 当前遍历到评论的所有子评论
var childComments = allComment.filter(item => item.topId && item.topId==data.id)
childComments.forEach(function (childData) {
// 如果父评论为顶级评论则直接赋值
if (childData.commentId == data.id){
childData.parentName = data.peopleName;
} else {
// 找到当前遍历子评论的父评论
var parentComment = childComments.find(function (item) {
return item.id = childData.commentId;
})
if (parentComment){
childData.parentName = parentComment.peopleName;
}
}
// 将处理完的数据push到顶级评论的子集中
data.childCommentDataLists.push(childData);
});
// 存放顶级评论
that.commentDataList.push(data)
}
}
})
...
[!tip] 上面的案例比较适合处理层级关系不深的评论列表,三层及以上则需要去维护一大串的代码逻辑,如果要处理多层,推荐"懒加载"的方式,第一次只查询顶级评论,当用户点击某个评论的下拉列表再次去请求该评论下的子评论,以此类推。
1.1.2. 发布文章(content)类型评论
代码片段
//vue代码片段
...
// dataType无需传递,后端接口做了限制处理
form: {
commentId:'',//父评论编号
commentContent: '',//评论内容
dataTitle: '${field.title}',//文章标题
dataId: '${field.id}',//文章id
commentPicture: [],// 评论图片JSON
commentFileJson: [],// 附件JSON
commentPoints: 0// 评论打分
},
...
...
ms.http.post('/people/comment/save.do', that.form).then(function (res) {
if (res.result) {
this.$message({
title: '成功',
message: '评论成功',
type: 'success'
});
} else {
this.$message({
title: '失败',
message: '评论失败',
type: 'error'
});
}
})
...
[!tip] 父子评论需注意正确传递commentId参数; 游客模式和会员有一些差异,参考http://doc.mingsoft.net/plugs/ping-lun-cha-jian/jie-shao.html#%E6%8E%A5%E5%8F%A3
1.1.3. 新增或查看其他类型评论,如商品
在新业务模块中前台和会员端的评论接口需要重写;后台的评论接口无需调整,通过前端传递不同的datatype区分。
如下:以保存接口为例,其他接口如查询处理逻辑一致,限制datatype类型即可
<!-- 在新业务模块中调整前端评论保存接口请求地址,其余参数组织与上方发布评论接口一致 -->
ms.http.post('新业务的接口地址', that.form).then(function (res) {
if (res.result) {
} else {
}
})
// 后端:在新业务模块中对应的web端或会员端新增接口,限制类型为商品
public ResultData save(@ModelAttribute @Parameter(hidden = true) CommentEntity comment) {
// 业务处理
···
// 获取当前模块评论类型,如商品
// 注意datatype值为 自定义字典中 评论类型对应的dict_label值
comment.setDataType(Const.COMMENT_DATA_TYPE);
commentBiz.saveComment(comment);
return ResultData.build().success();
}
1.1.4. 参数
图片、附件等参数需要json格式,其他参数正常处理即可 参考如下代码片段 代码片段:
···
that.form.commentFileJson.push({
url: file.url?file.url:file.path,
name: file.name,
path: response.data,
uid: file.uid,
status: 'success'
});
that.form.commentFileJson.push({
url: file.url?file.url:file.path,
name: file.name,
path: response.data,
uid: file.uid,
status: 'success'
});
···
// 将封装好的图片数据转为JSON
if (that.form.commentPicture && that.form.commentPicture != '[]'){
that.form.commentPicture = JSON.stringify(that.form.commentPicture)
}
// 将封装好的附件数据转为JSON
if (that.form.commentFileJson && that.form.commentFileJson != '[]'){
that.form.commentFileJson = JSON.stringify(that.form.commentFileJson)
}
···
1.2. 后端开发
1.2.1. 约定
配置名称约定
各评论数据业务有独立评论配置,评论配置名称约定为 评论类型字典中当前业务的名称 + 评论配置;
如文章的评论配置名称为: 文章评论配置
1.2.2. 配置
评论类型字典配置
在自定义模型 => 自定义字典 评论类型字典中 新增业务对应的评论类型名称
自定义业务评论配置
下方是自定义配置
评论配置
模型json模板,title属性需要按照上面配置名称约定去设置,其余不变
{
"searchJson": "[\n]\n",
"field": "[\n {\n \"model\":\"enableComment\",\n \"key\":\"ENABLE_COMMENT\",\n \"field\":\"ENABLE_COMMENT\",\n \"javaType\":\"Boolean\",\n \"jdbcType\":\"VARCHAR\",\n \"name\":\"开启评论\",\n \"type\":\"switch\",\n \"length\":\"11\",\n \"isShow\":false,\n \"isNoRepeat\":false,\n \"isSearch\":false,\n \"isRequired\":false\n }\n ,{\n \"model\":\"enableVisitor\",\n \"key\":\"ENABLE_VISITOR\",\n \"field\":\"ENABLE_VISITOR\",\n \"javaType\":\"Boolean\",\n \"jdbcType\":\"VARCHAR\",\n \"name\":\"开启游客模式\",\n \"type\":\"switch\",\n \"length\":\"11\",\n \"isShow\":false,\n \"isNoRepeat\":false,\n \"isSearch\":false,\n \"isRequired\":false\n }\n ,{\n \"model\":\"enableAudit\",\n \"key\":\"ENABLE_AUDIT\",\n \"field\":\"ENABLE_AUDIT\",\n \"javaType\":\"Boolean\",\n \"jdbcType\":\"VARCHAR\",\n \"name\":\"开启审核\",\n \"type\":\"switch\",\n \"length\":\"11\",\n \"isShow\":false,\n \"isNoRepeat\":false,\n \"isSearch\":false,\n \"isRequired\":false\n }\n]\n\n",
"html": "\n<template id=\"custom-model\">\n <el-form ref=\"form\" :model=\"form\" :rules=\"rules\" label-width=\"120px\" label-position=\"right\" size=\"small\" :disabled=\"disabled\" v-loading=\"loading\">\n <!--开启评论-->\n \n <el-form-item label=\"开启评论\" prop=\"enableComment\">\n <el-switch v-model=\"form.enableComment\"\n :disabled=\"false\">\n </el-switch>\n <div class=\"ms-form-tip\">\n开启评论后允许发布评论 </div>\n </el-form-item>\n \n <!--开启游客模式-->\n \n <el-form-item label=\"开启游客模式\" prop=\"enableVisitor\">\n <el-switch v-model=\"form.enableVisitor\"\n :disabled=\"false\">\n </el-switch>\n <div class=\"ms-form-tip\">\n开启后,无需登录也能评论;不开启推荐 </div>\n </el-form-item>\n \n <!--开启审核-->\n \n <el-form-item label=\"开启审核\" prop=\"enableAudit\">\n <el-switch v-model=\"form.enableAudit\"\n :disabled=\"false\">\n </el-switch>\n <div class=\"ms-form-tip\">\n开启后,评论信息审核通过后才在前台显示 </div>\n </el-form-item>\n \n </el-form>\n</template>\n",
"title": "商品评论配置",
"script": "var custom_model = Vue.component(\"custom-model\",{\n el: '#custom-model',\n data:function() {\n return {\n\t\t\tloading:false,\n disabled:false,\n modelId:0,\n modelName: \"评论配置\",\n //表单数据\n form: {\n linkId:0,\n // 开启评论\n enableComment:true,\n // 开启游客模式\n enableVisitor:false,\n // 开启审核\n enableAudit:true,\n },\n\n rules:{\n },\n }\n },\n watch:{\n \n //开启评论 \n \"form.enableComment\":function(nev,old){\n if(typeof(nev)=='string') {\n this.form.enableComment = (nev=='true');\n } else if(typeof(nev)=='undefined') {\n this.form.enableComment = false;\n } \n },\n \n //开启游客模式 \n \"form.enableVisitor\":function(nev,old){\n if(typeof(nev)=='string') {\n this.form.enableVisitor = (nev=='true');\n } else if(typeof(nev)=='undefined') {\n this.form.enableVisitor = false;\n } \n },\n \n //开启审核 \n \"form.enableAudit\":function(nev,old){\n if(typeof(nev)=='string') {\n this.form.enableAudit = (nev=='true');\n } else if(typeof(nev)=='undefined') {\n this.form.enableAudit = false;\n } \n },\n },\n components:{\n },\n computed:{\n },\n methods: {\n \tlink:function(e,field,binds){\n \t\tlet that = this;\n binds.forEach(function(item){\n \t\t\t\tms.http.post(ms.manager+'/project/form/link.do', {id:that.modelId,field:item.field,value:e}).then(function (res) {\n if(res.result && res.data) {\n that.form[ms.util.camelCaseString(item.field)]=res.data[0][item.target];\n }else{\n that.$notify({\n title: '失败',\n message: res.msg,\n type: 'warning'\n });\n }\n })\n\n });\n \t},\n update: function (row) {\n var that = this;\n ms.http.post(ms.manager+\"/comment/commentConfig/update.do\", row).then(function (data) {\n if (data.result) {\n that.$notify({\n title: '成功',\n message: '更新成功',\n type: 'success'\n });\n\n } else {\n that.$notify({\n title: '失败',\n message: data.msg,\n type: 'warning'\n });\n }\n });\n }, validate:function(){\n var b = false\n this.$refs.form.validate(function(valid){\n b = valid;\n });\n return b;\n },\n getFormData() {\n var that = this;\n var form = JSON.parse(JSON.stringify(that.form));\n form.modelId = that.modelId;\n return form;\n },\n save:function(callback) {\n var that = this;\n var url = this.formURL.save.url;\n if (that.form.id > 0) {\n url = this.formURL.update.url;\n }\n this.$refs.form.validate(function(valid) {\n if (valid) {\n var form = JSON.parse(JSON.stringify(that.form));\n form.modelId = that.modelId;\n ms.http.post(url, form).then(function (res) {\n if(callback) {\n callback(res);\n }\n }).catch(function(err){\n callback(err.response.data);\n });\n } else{\n callback({\n result:false,msg:'请检查表单输入项'\n });\n }\n })\n },\n //获取当前评论配置\n get:function(id) {\n var that = this;\n that.loading = true;\n ms.http.get(this.formURL.get.url, Object.assign({\"modelId\":that.modelId},this.formURL.get.params)).then(function (res) {\n if(res.result&&res.data){\n that.form = res.data;\n that.loading = false;\n } else {\n that.loading = false;\n }\n }).catch(function (err) {\n console.log(err);\n that.loading = false;\n });\n },\n\n },\n created:function() {\n var that = this;\n //渲染create\n that.get(this.form.linkId);\n }\n});",
"tableName": "COMMENT_CONFIG"
}
自定义配置中导入配置模型json
在业务菜单增加业务评论管理权限
如文章管理下管理文章评论,需要在菜单增加功能权限
comment:commentData:业务数据对应评论类型的数据值:view(reply|audit|del)
业务数据页面的操作列增加 回复列表 操作按钮
如文章管理的列表页增加回复列表,示例如下
...
<!-- 页面部分 操作列增加 回复列表 -->
<el-link v-if="ms.util.includes(ms.managerPermissions,'comment:commentData:'+commentDataType+':view') || ms.util.includes(ms.managerPermissions,'comment:comment:view')" type="primary" :underline="false" @click="viewComment(scope.row.id)" >回复列表</el-link>
...
<!-- vue data部分 定义commentDataType -->
data: function(){
return {
...
commentDataType: "content",
}
}
<!--vue method 增加viewComment实现-->
viewComment:function (id){
var that = this;
var backUrl = encodeURIComponent("/leader/box/index.do");
ms.util.openSystemUrl("/comment/data/index.do?dataType="+that.commentDataType+"&backUrl="+backUrl+"&dataId="+id);
},
点击回复列表查看该文章的评论数据列表
1.2.3. 前台、会员层添加评论
参考comment模块 action /web/CommentAction|/people/CommentAction
1.2.4. 通用评论保存业务
在业务开发时注意查看biz.ICommentBiz中的方法注释,有些参数校验或者字段默认赋值等操作已经在业务层统一处理,评论的dataType保存在数据库存储的都是字典的value值;
下面以业务层saveComment为例 biz.impl.commentBizImpl中saveComment方法代码片段
··· 基本数据校验
// 判断评论类型
String dataLabel = DictUtil.getDictLabel("评论类型", comment.getDataType());
if (StringUtils.isBlank(dataLabel)){
throw new BusinessException(BundleUtil.getBaseString("err.error",
BundleUtil.getString(net.mingsoft.comment.constant.Const.RESOURCES,"comment.type")));
}
// 是否开启评论
// todo 规定在各个子业务模块定义对应的评论配置,评论配置命名为 dataType在自定义字典中评论类型对应的dictLabel值,如文章 +"评论配置"
String configName = dataLabel + "评论配置";
if (!ConfigUtil.getBoolean(configName,"enableComment")){
throw new BusinessException(BundleUtil.getBaseString("fail",
BundleUtil.getString(net.mingsoft.comment.constant.Const.RESOURCES,"comment")));
}
···
// 涉及评论是否审核,开启取到配置为true,需要取反,false为未审核
comment.setCommentAudit(!ConfigUtil.getBoolean("评论配置", "enableAudit"));
// 评论ip
HashMap<String, String> map = new HashMap<>();
map.put("ipv4",BasicUtil.getIp());
map.put("addr",IpUtils.getRealAddressByIp(BasicUtil.getIp()));
comment.setCommentIp(JSONUtil.toJsonStr(map));
//评论时间
comment.setCommentTime(new Date());
comment.setUpdateDate(new Date());
comment.setCreateDate(new Date());
// 初始化评论点赞数
comment.setCommentLike(0);
// 附件及图片
if (StringUtils.isBlank(comment.getCommentPicture()) || !comment.getCommentPicture().matches("^\\[.{1,}]$")) {
comment.setCommentPicture("");
}
if (StringUtils.isBlank(comment.getCommentFileJson()) || !comment.getCommentFileJson().matches("^\\[.{1,}]$")) {
comment.setCommentFileJson("");
}
// 设置顶级评论id 判断当前回复的评论是否是顶级评论,不是则将top_id设置为当前回复评论的top_id
setTopId(comment);
return commentDao.insert(comment);
1.2.5. 保存评论
保存时需注意,一些参数需要按指定格式传参,如peopleInfo字段需要json结构。
处理完参数调用统一的业务层方法即可。
下面是action.people.commentAction save方法的代码片段:
// 判断登陆设置peopleId
PeopleEntity people = this.getPeopleBySession();
PeopleUserEntity peopleUserEntity = (PeopleUserEntity) peopleUserBiz.getEntity(people.getIntId());
// 设置会员相关信息
if (peopleUserEntity != null){
Map commentPeopleInfo = new HashMap();
// 这里可以根据业务需求填充用户数据
commentPeopleInfo.put("puIcon",peopleUserEntity.getPuIcon());
String peopleInfo = JSONUtil.toJsonStr(commentPeopleInfo);
comment.setPeopleInfo(peopleInfo);
comment.setPeopleName(peopleUserEntity.getPuNickname());
comment.setPeopleId(peopleUserEntity.getPeopleId());
}
// 获取当前模块评论类型
comment.setDataType(Const.COMMENT_DATA_TYPE);
// 业务层再对评论数据做处理 如 ip、时间、评论关系等
commentBiz.saveComment(comment);
1.2.6. 删除评论
后台管理可以批量删除评论,默认代码中前台只允许用户删除自己的评论,注意如果重写action的代码要对用户的id进行校验
代码参考:
PeopleEntity people = this.getPeopleBySession();
if (StringUtils.isBlank(id)){
return ResultData.build().error(getResString("err.not.exist",this.getResString("comment")));
}
CommentEntity comment = commentBiz.getById(id);
if (comment==null || !people.getId().equals(comment.getPeopleId())){
return ResultData.build().error(getResString("err.not.exist",this.getResString("comment")));
}
级联删除: 从产品角度来讲,可以允许父评论删除后保留子评论,增加逻辑删除业务,界面则不显示评论内容; 如果需要级联删除业务,建议在数据库设置外键关联,在数据库层面级联删除评论
附件、图片等处理:推荐使用我们的清理插件,可以清理一些删除评论后遗留的无用文件http://doc.mingsoft.net/plugs/qing-li-cha-jian/jie-shao.html
在默认代码中,评论log表中统计数量会随评论表中记录数增加,但是不会随着删除而减少,这是我们觉得比较合理的一个设计;当然如果并不需要这么做的话,请在删除时参考如下代码处理记录数。
//查询评论记录是否有该评论
CommentsLogEntity data = commentsLogBiz.getOne(new QueryWrapper<CommentsLogEntity>().eq("data_id",dataId).eq("data_type",dataType));
//评论数-1
data.setCommentsCount(data.getCommentsCount()-1);
commentsLogBiz.updateById(data);
1.2.7. 修改评论
不支持修改评论
1.2.8. 查询评论
建议调用web端查询接口,people目录下的action接口需要会员登陆
web代码参考:
//只显示审核通过的评论
comment.setCommentAudit(true);
// 确保第一次只查询出父评论
BasicUtil.startPage();
List<CommentEntity> list = commentBiz.query(comment);
return ResultData.build().success().data(new EUListBean(list,(int)BasicUtil.endPage(list).getTotal()));
1.2.9. 业务对接
在开发时,对接其他业务如文章评论、商品评论等
在这些业务删除时,可以考虑aop处理同时删除相关联的评论数据