评论功能
一、数据库设计
- 数据库
edu_comment
CREATE TABLE `edu_comment` (
`id` CHAR(19) NOT NULL COMMENT '讲师ID',
`course_id` VARCHAR(19) NOT NULL DEFAULT '' COMMENT '课程id',
`teacher_id` CHAR(19) NOT NULL DEFAULT '' COMMENT '讲师id',
`member_id` VARCHAR(19) NOT NULL DEFAULT '' COMMENT '会员id',
`nickname` VARCHAR(50) DEFAULT NULL COMMENT '会员昵称',
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '会员头像',
`content` VARCHAR(500) DEFAULT NULL COMMENT '评论内容',
`is_deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` DATETIME NOT NULL COMMENT '创建时间',
`gmt_modified` DATETIME NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_course_id` (`course_id`),
KEY `idx_teacher_id` (`teacher_id`),
KEY `idx_member_id` (`member_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='评论';
二、后端设计接口
1、在service-edu模块,生成课程评论代码
(1)使用mp代码生成器生成
public class CodeGenerator {
@Test
public void run() {
// 1、创建代码生成器
AutoGenerator mpg = new AutoGenerator();
// 2、全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
//C:\Users\86185\Desktop\guli_parent
gc.setOutputDir("C:\\Users\\86185\\Desktop\\guli_parent\\service\\service-edu" + "/src/main/java"); //输出目录
gc.setAuthor("czy"); //作者名
gc.setOpen(false); //生成后是否打开资源管理器
gc.setFileOverride(false); //重新生成时文件是否覆盖
gc.setServiceName("%sService"); //去掉Service接口的首字母I
gc.setIdType(IdType.ID_WORKER_STR); //主键策略
gc.setDateType(DateType.ONLY_DATE);//定义生成的实体类中日期类型
gc.setSwagger2(true);//开启Swagger2模式
mpg.setGlobalConfig(gc);
// 3、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 4、包配置
PackageConfig pc = new PackageConfig();
//生成包:com.czy.eduservice
pc.setModuleName("eduservice"); //模块名
pc.setParent("com.czy");
//生成包:com.czy.controller
pc.setController("controller");
pc.setEntity("entity");
pc.setService("service");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);
// 5、策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("edu_comment");//根据数据库哪张表生成,有多张表就加逗号继续填写
strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
strategy.setTablePrefix(pc.getModuleName() + "_"); //生成实体时去掉表前缀
strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作
strategy.setRestControllerStyle(true); //restful api风格控制器
strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符
mpg.setStrategy(strategy);
// 6、执行
mpg.execute();
}
}
2、在service-ucenter模块,创建接口
com.czy.educenter.controller.UcenterMemberController
评论前先登录,查询用户信息
@RestController
@RequestMapping("/educenter/member")
@CrossOrigin
public class UcenterMemberController {
@Autowired
private UcenterMemberService memberService;
//2.评论前先登录,查询用户信息
@PostMapping("/getMemberInfoById/{memberId}")
public UcenterMember getMemberInfoById(@PathVariable String memberId){
UcenterMember member = memberService.getById(memberId);
UcenterMember memberVo = new UcenterMember();
BeanUtils.copyProperties(member,memberVo);
return memberVo;
}
}
3、封装返回给评论服务的对象
com.czy.educenter.entity.UcenterMember
用于返回给调用的edu评论entity
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="UcenterMember对象", description="会员表")
public class UcenterMember implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "会员id")
@TableId(value = "id", type = IdType.ID_WORKER_STR)
private String id;
@ApiModelProperty(value = "微信openid")
private String openid;
@ApiModelProperty(value = "手机号")
private String mobile;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "昵称")
private String nickname;
@ApiModelProperty(value = "性别 1 女,2 男")
private Integer sex;
@ApiModelProperty(value = "年龄")
private Integer age;
@ApiModelProperty(value = "用户头像")
private String avatar;
@ApiModelProperty(value = "用户签名")
private String sign;
@ApiModelProperty(value = "是否禁用 1(true)已禁用, 0(false)未禁用")
private Boolean isDisabled;
@ApiModelProperty(value = "逻辑删除 1(true)已删除, 0(false)未删除")
private Boolean isDeleted;
@ApiModelProperty(value = "创建时间")
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
@ApiModelProperty(value = "更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;
}
4、创建课程评论controller
com.czy.eduservice.controller.EduCommentController
@RestController
@RequestMapping("/eduservice/comment")
@CrossOrigin
public class EduCommentController {
@Autowired
private EduCommentService eduCommentService;
@Autowired
private UcenterClient ucenterClient;
//根据课程id_分页查询课程评论的方法
@GetMapping("/getCommentPage/{page}/{limit}")
public R getCommentPage(@PathVariable Long page,@PathVariable Long limit,String courseId){
Page<EduComment> commentPage = new Page<>(page, limit);
QueryWrapper<EduComment> wrapper = new QueryWrapper<>();
//判断课程id是否为空
if (!StringUtils.isEmpty(courseId)){
wrapper.eq("course_id",courseId);
}
//按最新排序
wrapper.orderByDesc("gmt_create");
//数据会被封装到commentPage中
eduCommentService.page(commentPage,wrapper);
List<EduComment> commentList = commentPage.getRecords();
long current = commentPage.getCurrent();//当前页
long size = commentPage.getSize();//一页记录数
long total = commentPage.getTotal();//总记录数
long pages = commentPage.getPages();//总页数
boolean hasPrevious = commentPage.hasPrevious();//是否有上页
boolean hasNext = commentPage.hasNext();//是否有下页
HashMap<String, Object> map = new HashMap<>();
map.put("current",current);
map.put("size",size);
map.put("total",total);
map.put("pages",pages);
map.put("hasPrevious",hasPrevious);
map.put("hasNext",hasNext);
map.put("list",commentList);
return R.ok().data(map);
}
//添加评论
@PostMapping("/auth/addComment")
public R addComment(HttpServletRequest request,@RequestBody EduComment eduComment){
String memberId = JwtUtils.getMemberIdByJwtToken(request);
//判断用户是否登录
if (StringUtils.isEmpty(memberId)){
throw new GuliException(20001,"请先登录");
}
eduComment.setMemberId(memberId);
//远程调用ucenter根据用户id获取用户信息
UcenterMember memberVo = ucenterClient.getMemberInfoById(memberId);
eduComment.setAvatar(memberVo.getAvatar());
eduComment.setNickname(memberVo.getNickname());
eduComment.setGmtCreate(memberVo.getGmtCreate());
eduComment.setGmtModified(memberVo.getGmtModified());
//保存评论
eduCommentService.save(eduComment);
return R.ok();
}
}
5、Open Feign远程调用类
- UcenterClient接口
com.czy.eduservice.client.UcenterClient
@Component
@FeignClient(name = "service-ucenter",fallback = UcenterClientImpl.class)
public interface UcenterClient {
@PostMapping("/educenter/member/getMemberInfoById/{memberId}")
public UcenterMember getMemberInfoById(@PathVariable String memberId);
}
- UcenterClientImpl
com.czy.eduservice.client.impl.UcenterClientImpl
@Component
public class UcenterClientImpl implements UcenterClient {
@Override
public UcenterMember getMemberInfoById(String memberId) {
return null;
}
}
三、前端部分
- api方法_commonedu.js
guli-front\api\commonedu.js
import request from '@/utils/request'
export default {
getPageList(page, limit, courseId) {
return request({
url: `/eduservice/comment/getCommentPage/${page}/${limit}`,
method: 'get',
params: courseId
})
},
addComment(comment) {
return request({
url: `/eduservice/comment/auth/addComment`,
method: 'post',
data: comment
})
}
}
guli-front\pages/course/_id.vue
把刚刚定义的接口引入,然后编写guli-front\pages/course/_id.vue的js脚本
<script>
import courseApi from '@/api/course'
import comment from "@/api/commonedu"
export default {
data() {
return {
chapterList: [],
course: {
courseId: "",
},
data: {},
page: 1,
limit: 4,
total: 10,
comment: {
content: "",
courseId: "",
teacherId: "",
},
isbuyCourse: false,
};
},
methods:{
initComment() {
comment
.getPageList(this.page, this.limit, this.course.courseId)
.then((response) => {
this.data = response.data.data;
console.log(response.data.data);
});
},
addComment() {
this.comment.courseId = this.course.courseId;
this.comment.teacherId = this.course.teacherId;
comment.addComment(this.comment).then((response) => {
if (response.data.success) {
this.$message({
message: "评论成功",
type: "success",
});
this.comment.content = "";
this.initComment();
}
});
},
gotoPage(page) {
comment.getPageList(page, this.limit, this.courseId).then((response) => {
this.data = response.data.data;
});
},
}
,
asyncData({ params, error }) {
return courseApi.getCourseInfo(params.id)
.then(response => {
return {
courseWebVo: response.data.data.courseWebVo,
chapterVideoList: response.data.data.chapterVideoList
}
})
},
created() {
this.course.courseId = this.$route.params.id;
//获取课程详细信息
//this.getCourseInfo();
this.initComment();
},
};
</script>
前端页面
<template>
<div id="aCoursesList" class="bg-fa of">
<!-- /课程详情 开始 -->
<section class="container">
<section class="path-wrap txtOf hLh30">
<a href="#" title class="c-999 fsize14">首页</a>
\
<a href="#" title class="c-999 fsize14">{
{courseWebVo.subjectLevelOne}}</a>
\
<span class="c-333 fsize14">{
{courseWebVo.subjectLevelTwo}}</span>
</section>
<div>
<article class="c-v-pic-wrap" style="height: 357px;">
<section class="p-h-video-box" id="videoPlay">
<img :src="courseWebVo.cover" :alt="courseWebVo.title" class="dis c-v-pic">
</section>
</article>
<aside class="c-attr-wrap">
<section class="ml20 mr15">
<h2 class="hLh30 txtOf mt15">
<span class="c-fff fsize24">{
{courseWebVo.title}}</span>
</h2>
<section class="c-attr-jg">
<span class="c-fff">价格:</span>
<b class="c-yellow" style="font-size:24px;">¥{
{courseWebVo.price}}</b>
</section>
<section class="c-attr-mt c-attr-undis">
<span class="c-fff fsize14">主讲: {
{courseWebVo.teacherName}} </span>
</section>
<section class="c-attr-mt of">
<span class="ml10 vam">
<em class="icon18 scIcon"></em>
<a class="c-fff vam" title="收藏" href="#" >收藏</a>
</span>
</section>
<section class="c-attr-mt">
<a href="#" title="立即观看" class="comm-btn c-btn-3">立即观看</a>
</section>
</section>
</aside>
<aside class="thr-attr-box">
<ol class="thr-attr-ol clearfix">
<li>
<p> </p>
<aside>
<span class="c-fff f-fM">购买数</span>
<br>
<h6 class="c-fff f-fM mt10">{
{courseWebVo.buyCount}}</h6>
</aside>
</li>
<li>
<p> </p>
<aside>
<span class="c-fff f-fM">课时数</span>
<br>
<h6 class="c-fff f-fM mt10">20</h6>
</aside>
</li>
<li>
<p> </p>
<aside>
<span class="c-fff f-fM">浏览数</span>
<br>
<h6 class="c-fff f-fM mt10">501</h6>
</aside>
</li>
</ol>
</aside>
<div class="clear"></div>
</div>
<!-- /课程封面介绍 -->
<div class="mt20 c-infor-box">
<article class="fl col-7">
<section class="mr30">
<div class="i-box">
<div>
<section id="c-i-tabTitle" class="c-infor-tabTitle c-tab-title">
<a name="c-i" class="current" title="课程详情">课程详情</a>
</section>
</div>
<article class="ml10 mr10 pt20">
<div>
<h6 class="c-i-content c-infor-title">
<span>课程介绍</span>
</h6>
<div class="course-txt-body-wrap">
<section class="course-txt-body">
<p v-html="courseWebVo.description">{
{courseWebVo.description}}</p>
</section>
</div>
</div>
<!-- /课程介绍 -->
<div class="mt50">
<h6 class="c-g-content c-infor-title">
<span>课程大纲</span>
</h6>
<section class="mt20">
<div class="lh-menu-wrap">
<menu id="lh-menu" class="lh-menu mt10 mr10">
<ul>
<!-- 文件目录 -->
<li class="lh-menu-stair" v-for="chapter in chapterVideoList" :key="chapter.id">
<a href="javascript: void(0)" :title="chapter.title" class="current-1">
<em class="lh-menu-i-1 icon18 mr10"></em>{
{chapter.title}}
</a>
<ol class="lh-menu-ol" style="display: block;">
<li class="lh-menu-second ml30" v-for="video in chapter.children" :key="video.id">
<a :href="'/player/'+video.videoSourceId" target="_blank">
<span class="fr">
<i class="free-icon vam mr10">免费试听</i>
</span>
<em class="lh-menu-i-2 icon16 mr5"> </em>{
{video.title}}
</a>
</li>
</ol>
</li>
</ul>
</menu>
</div>
</section>
</div>
<!-- /课程大纲 -->
</article>
</div>
</section>
</article>
<aside class="fl col-3">
<div class="i-box">
<div>
<section class="c-infor-tabTitle c-tab-title">
<a title href="javascript:void(0)">主讲讲师</a>
</section>
<section class="stud-act-list">
<ul style="height: auto;">
<li>
<div class="u-face">
<a href="#">
<img :src="courseWebVo.avatar" width="50" height="50" alt>
</a>
</div>
<section class="hLh30 txtOf">
<a class="c-333 fsize16 fl" href="#">{
{courseWebVo.teacherName}}</a>
</section>
<section class="hLh20 txtOf">
<span class="c-999">{
{courseWebVo.intro}}</span>
</section>
</li>
</ul>
</section>
</div>
</div>
</aside>
<div class="clear"></div>
<!-- 评论模板开始 -->
<div class="mt50 commentHtml">
<div>
<h6 class="c-c-content c-infor-title" id="i-art-comment">
<span class="commentTitle">课程评论</span>
</h6>
<section class="lh-bj-list pr mt20 replyhtml">
<ul>
<li class="unBr">
<aside class="noter-pic">
<img
width="50"
height="50"
class="picImg"
src="~/assets/img/avatar-boy.gif"
/>
</aside>
<div class="of">
<section class="n-reply-wrap">
<fieldset>
<textarea
name=""
v-model="comment.content"
placeholder="输入您要评论的文字"
id="commentContent"
></textarea>
</fieldset>
<p class="of mt5 tar pl10 pr10">
<span class="fl"
><tt
class="c-red commentContentmeg"
style="display: none"
></tt
></span>
<input
type="button"
@click="addComment()"
value="回复"
class="lh-reply-btn"
/>
</p>
</section>
</div>
</li>
</ul>
</section>
<section class="">
<section class="question-list lh-bj-list pr">
<ul class="pr10">
<li v-for="comment in data.list" :key="comment.id">
<aside class="noter-pic">
<img
width="50"
height="50"
class="picImg"
:src="comment.avatar"
/>
</aside>
<div class="of">
<span class="fl">
<font class="fsize12 c-blue">{
{ comment.nickname }}</font>
<font class="fsize12 c-999 ml5">评论:</font></span
>
</div>
<div class="noter-txt mt5">
<p>{
{ comment.content }}</p>
</div>
<div class="of mt5">
<span class="fr"
><font class="fsize12 c-999ml5">{
{
comment.gmtCreate
}}</font></span
>
</div>
</li>
</ul>
</section>
</section>
<!-- 公共分页 开始 -->
<div class="paging">
<!-- undisable这个class是否存在,取决于数据属性hasPrevious -->
<a
:class="{ undisable: !data.hasPrevious }"
href="#"
title="首页"
@click.prevent="gotoPage(1)"
>首</a
>
<a
:class="{ undisable: !data.hasPrevious }"
href="#"
title="前一页"
@click.prevent="gotoPage(data.current - 1)"
><</a
>
<a
v-for="page in data.pages"
:key="page"
:class="{
current: data.current == page,
undisable: data.current == page,
}"
:title="'第' + page + '页'"
href="#"
@click.prevent="gotoPage(page)"
>{
{ page }}</a
>
<a
:class="{ undisable: !data.hasNext }"
href="#"
title="后一页"
@click.prevent="gotoPage(data.current + 1)"
>></a
>
<a
:class="{ undisable: !data.hasNext }"
href="#"
title="末页"
@click.prevent="gotoPage(data.pages)"
>末</a
>
<div class="clear" />
</div>
<!-- 公共分页 结束 -->
<!-- 评论模板结束 -->
</div>
</div>
</div>
</section>
<!-- /课程详情 结束 -->
</div>
</template>
运行,随便发点评论