续 框架(二)

132 阅读12分钟

续 完成新增评论的功能

实体类新增对应属性

上次课我们在comment表中新增了一个列user_nick_name

这个列在实体类中还没有对应的属性,所以要在Comment类中添加这个列对应的属性

/**
 * 用户昵称
 */
@TableField("user_nick_name")
private String userNickName;

question\answer\comment的关系

image-20211207163925155.png

上图所示

question和answer是一对多的关系

answer和comment也是一对多的关系

最终在页面上的效果可能为

image-20211207164155194.png

下面我们就要针对评论开发各种功能

我们要依次完成增,查,删,改

创建CommentVO类

和讲师新增回答流程类似

我们新增评论也需要VO类型

vo包中创建CommentVO代码如下

@Data
@Accessors(chain = true)
public class CommentVO implements Serializable {

    @NotNull(message = "回答id不能为空")
    private Integer answerId;

    @NotBlank(message = "评论内容不能为空")
    private String content;

}

编写控制层接收表单信息

CommentController类添加方法接收新增评论的表单信息

@RestController
@RequestMapping("/v1/comments")
@Slf4j
public class CommentController {

    @Autowired
    private ICommentService commentService;

    // @PostMapping等价于@PostMapping("")
    @PostMapping
    public Comment postComment(
            @Validated CommentVO commentVO,
            BindingResult result,
            @AuthenticationPrincipal UserDetails user){
            log.debug("接收到表单信息:{}",commentVO);
            if(result.hasErrors()){
                String msg=result.getFieldError().getDefaultMessage();
                throw new ServiceException(msg);
            }
            //  这里调用业务逻辑层方法

            // 暂时返回null,以便编译通过
            return null;
    }
}

页面的绑定和js代码

页面中所有添加评论的连接点击之后都会展开页面中所有添加评论的表单,这是个bug

我们需要实现点击哪个添加评论的连接,只展开当前添加评论的表单,不影响其他回答的同样位置

这个bug是因为多个回答在循环过程中生成可多个相同id的div导致的

我们使用answerid来在循环中区分不同的评论连接,解决这个bug

image-20211207175353244.png

detail_teacher.html的360行附近

<p class="text-left text-dark">
  <a class="btn btn-primary mx-2"
     href="#">采纳答案</a>
  <a class="btn btn-outline-primary" data-toggle="collapse"
     href="#collapseExample1"
     role="button" aria-expanded="false"
     aria-controls="collapseExample"
     :href="'#addComment'+answer.id">
    <i class="fa fa-edit"></i>添加评论
  </a>
</p>
<div class="collapse" id="collapseExample1"
      :id="'addComment'+answer.id">
  <div class="card card-body border-light">
    <form action="#" method="post"
          class="needs-validation" novalidate
          @submit.prevent="postComment(answer.id)">
      <div class="form-group">
        <textarea class="form-control" name="content" rows="3" required></textarea>
        <div class="invalid-feedback">
          评论内容不能为空!
        </div>
      </div>
      <button type="submit" class="btn btn-primary my-1 float-right">提交评论</button>
    </form>
  </div>
</div>

vue绑定了html之后开始编写能够完成新增评论的vue代码

继续在question_detail.js文件中编写

在answersApp对象中新增方法,代码如下

postComment:function(answerId){
    // 新增评论需要answerId和评论内容content
    // 参数answerId已经提供,需要我们编码获取的就是content
    // 使用jquery的后代选择器获得textarea对象
    let textarea=$("#addComment"+answerId+" textarea");
    let content=textarea.val();
    // 创建表单
    let form=new FormData();
    form.append("answerId",answerId);
    form.append("content",content);
    axios({
        url:"/v1/comments",
        method:"post",
        data:form
    }).then(function(response){
        // 暂时返回null 这里什么都不用写了
    })
}

重启服务,在新增评论的表单中提交数据

检查idea控制台收到的信息是否正确

开发业务逻辑层

ICommentService添加新增评论的方法

public interface ICommentService extends IService<Comment> {

    // 新增评论的业务逻辑层方法
    Comment saveComment(CommentVO commentVO,String username);
}

CommentServiceImpl实现

@Service
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements ICommentService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private CommentMapper commentMapper;

    @Override
    public Comment saveComment(CommentVO commentVO, String username) {
        User user=userMapper.findUserByUsername(username);
        Comment comment=new Comment()
                .setUserId(user.getId())
                .setUserNickName(user.getNickname())
                .setAnswerId(commentVO.getAnswerId())
                .setContent(commentVO.getContent())
                .setCreatetime(LocalDateTime.now());
        int num=commentMapper.insert(comment);
        if(num!=1){
            throw new ServiceException("数据库忙");
        }
        // 千万别忘了返回comment
        return comment;
    }
}

完善控制层调用

CommentController类中

调用业务逻辑层方法完成新增评论

//  这里调用业务逻辑层方法
Comment comment=commentService.saveComment(
                            commentVO,user.getUsername());
// 千万别忘了返回新增成功的评论对象comment
return comment;

再次重启服务

操作新增评论,就能真正将评论新增到数据库了!

显示评论列表

查询评论列表的思路

我们现在要显示当前问题的所有回答的所有评论

而且需要将对应的评论显示在对应的回答下

实现这样的效果有两种思路

image-20211208103231654.png

上图所示思路

左侧:在现有基础上,分别查询每个回答各自对应的评论,通过多次连接数据库将每个回答的评论查询并显示出来

​ 这么做缺点非常明显,当前问题的回答越多,连库次数越多,效率低

右侧:重构查询所有回答的方法,在查询当前问题所有回答的同时就将每个回答对应的评论也查询出来

​ 这么做的优点是无论当前问题多少回答,都只连接一次数据库,但是我们需要学习Mybatis关联查询操作比较复杂,但是效率明显高于左侧解决方案

编写执行查询的sql语句

Mybatis框架执行任何操作都需要明确sql操作

所以我们先编写sql语句

目标: 查询指定问题id(例如149)的所有回答以及这个回答的所有评论

SELECT * FROM answer a
LEFT JOIN comment c ON a.id=c.answer_id
WHERE quest_id=149
ORDER BY a.id

上面sql语句的查询结果包含

所有回答以及包含所有回答对应的评论

我们要实现每个回答能够包含所有评论的数据结构

修改Answer实体类

Answer实体类要添加一个Comment类型的集合满足查询出的回答包含它的所有评论的结构

Answer类末尾添加属性:

/**
 * 当前回答的所有评论集合
 */
@TableField(exist = false)
private List<Comment> comments=new ArrayList<>();

Answer类中声明一个List<Comment>类型的属性,用于保存当前回答的所有评论

关联查询和Answer类的映射

所谓映射就是查询结果的每个列和java类的每个属性的对应关系

正常情况下,列名和java属性的映射都是自动的

但是当我们数据库管理查询的列名重名严重,所以需要起别名

然后Mybatis关联查询需要使用特殊的方式进行操作,然后进行映射配置完成查询操作

123.PNG

我们需要对之前编写的查询sql语句进行别名的设置

SELECT 
	a.id,
	a.content,
	a.like_count,
	a.user_id,
	a.user_nick_name,
	a.quest_id,
	a.createtime,
	a.accept_status,
	c.id 					comment_id,
	c.user_id 			comment_user_id,
	c.user_nick_name  comment_user_nick_name,
	c.answer_id  		comment_answer_id,
	c.content			comment_content,
	c.createtime		comment_createtime
FROM answer a
LEFT JOIN comment c ON a.id=c.answer_id
WHERE quest_id=149
ORDER BY a.id

编写Mapper的Xml文件

我们要利用Mybatis框架提供的关联查询功能实现查询所有回答包含所有评论

这样的功能需要在Mapper的配置文件中完成,包含编写映射的配置

我们可以直接将mapper包中xml文件夹里的AnswerMapper.xml文件复制

粘贴到resources文件下的mapper文件夹中(mapper文件夹不存在就创建它)

image-20211208114429531.png

最好Rebuild一下!!

修改后的xml文件内容如下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
namespace
指定当前xml文件对应的Mapper接口
必须指定一个存在的Mapper接口,当前xml文件才能正常运作
-->
<mapper namespace="cn.tedu.knows.portal.mapper.AnswerMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="answerCommentMap" type="cn.tedu.knows.portal.model.Answer">
        <id     column="id" property="id" />
        <result column="content" property="content" />
        <result column="like_count" property="likeCount" />
        <result column="user_id" property="userId" />
        <result column="user_nick_name" property="userNickName" />
        <result column="quest_id" property="questId" />
        <result column="createtime" property="createtime" />
        <result column="accept_status" property="acceptStatus" />
        <!--
            配置collection标签,
            将查询结果中Comment对象保存在当前回答Answer对象中
            property指定要保存Comment对象的属性
            ofType指定集合泛型的类型,这里是List<Comment>所以是Comment的全类名
        -->
        <collection property="comments"
                    ofType="cn.tedu.knows.portal.model.Comment">
            <id column="comment_id" property="id" />
            <result column="comment_user_id" property="userId" />
            <result column="comment_user_nick_name" property="userNickName" />
            <result column="comment_answer_id" property="answerId" />
            <result column="comment_content" property="content" />
            <result column="comment_createtime" property="createtime" />
        </collection>
    </resultMap>
    <!--
    select标签,表示一个查询方法,id和AnswerMapper中的一个方法名一致
    注意不建议在标签内打注释,如果一定要打,写数据库sql语句注释,写xml注释会报错
    resultMap是指定当前查询的返回值映射关系的名称
    这个名称要和上面<resultMap>的id对应
    -->
    <select id="findAnswersByQuestionId" resultMap="answerCommentMap">
        SELECT
            a.id,
            a.content,
            a.like_count,
            a.user_id,
            a.user_nick_name,
            a.quest_id,
            a.createtime,
            a.accept_status,
            c.id            comment_id,
            c.user_id        comment_user_id,
            c.user_nick_name    comment_user_nick_name,
            c.answer_id       comment_answer_id,
            c.content        comment_content,
            c.createtime      comment_createtime
        FROM answer a
        LEFT JOIN comment c ON a.id=c.answer_id
        WHERE quest_id=#{id}
        ORDER BY a.id
    </select>
</mapper>

AnswerMapper接口中的代码要随上面xml文件编写对应的方法

@Repository
public interface AnswerMapper extends BaseMapper<Answer> {

    // Mybatis关联查询,实现查询指定问题的所有回答包含所有回答的评论
    // 它对应resources中mapper里AnswerMapper.xml中的同名的xml配置
    List<Answer> findAnswersByQuestionId(Integer questionId);

}

这个Mapper必须测试

测试代码如下

@Autowired
AnswerMapper answerMapper;
@Test
public void testAnswer(){
    List<Answer> answers=
            answerMapper.findAnswersByQuestionId(149);
    for(Answer a:answers){
        System.out.println(a);
    }
}

修改业务逻辑层代码

之前学习的章节实现了根据问题id查询回答列表的功能更

只是回答列表中不包含回答的评论

我们找到AnswerServiceImpl类中的getAnswersByQuestionId方法

将其中的查询问题列表的代码修改为关联查询的方法

@Override
public List<Answer> getAnswersByQuestionId(Integer questionId) {
    // 执行关联查询,获得包含所有评论的回答列表
    List<Answer> answers=answerMapper
                        .findAnswersByQuestionId(questionId);
    // 千万别忘了返回answers
    return answers;
}

重启服务,可以发送同步请求

localhost:8080/v1/answers/question/149

修改评论列表的Vue绑定

控制层和axios代码都不需要修改

我们要修改的就是页面上对评论列表进行绑定的Vue代码

它是在当前回答列表中的一部分,实际上是在循环遍历所有回答的过程中再遍历所有评论

detail_teacher.html

310行附近

<div class="card-footer">

  <p class="text-success fa fa-comment">
    <span v-text="answer.comments.length">1</span>条评论
      <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑  -->
  </p>
  <ul class="list-unstyled mt-3">
    <li class="media my-2"
        v-for="comment in answer.comments">
        <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑  -->
      <img style="width: 50px;height: 50px;border-radius: 50%;"
           src="../img/user.jpg" class="mr-3"
           alt="...">
      <div class="media-body">
        <h6 class="mt-0 mb-1">
          <span v-text="comment.userNickName">李四</span>:
            <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑  -->
        </h6>
        <p class="text-dark">
          <span class="text-monospace"
                v-text="comment.content">
              <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑  -->
            明白了,谢谢老师!
          </span>
          <!-- 其他代码略 -->
</div>     

重启服务,访问页面观察是否能够正确显示每个回答的评论

新增评论时立即显示评论内容

和新增讲师回复时遇到的问题类似

现在我们新增一个评论也需要刷新网页才能显示在评论列表中

我们不想刷新页面,就像将新增的评论显示在评论列表中

但是和新增回复不同,我们新增的评论对象需要添加到对应的回答的评论列表中

找到对应评论列表的依据是answerId能匹配

所以修改我们之前写好的postComment的then方法中的内容

代码如下

.then(function(response){
    // 获得新增成功的评论
    let comment=response.data;
    // 获得所有回答
    let answers=answersApp.answers;
    // 遍历所有回答
    for(let i=0;i<answers.length;i++){
        // 判断当前回答的id是否和新增评论的answerId一致
        if(answers[i].id==answerId){
            // 如果一致,表示新增的评论对象就是当前回答的
            // 所以添加到当前回答的评论列表中
            answers[i].comments.push(comment);
            // 清空输入评论框中的内容
            textarea.val("");
            break;
        }
    }
})

重启服务,测试新增评论的效果

开发删除评论的功能

删除评论的实现思路

删除评论对用户来说是一个"不可逆"的操作

所以为了减少用户误删除的情况,所有不可逆操作都会有二次确认或者是类似的提示

最简单的做法是使用浏览器给的确认弹框(confirm),但是不友好

我们的做法是在点击删除链接后弹出二次确认图标

image-20211208161645615.png

还有,我们需要知道删除评论的基本业务逻辑

  • 讲师登录,可以删除任何人的评论
  • 学生登录,只能删除自己发表的评论

完成二次确认效果

detail_teacher.html页面的355行附近

<a class="ml-2  fa fa-close " style="font-size: small"
   data-toggle="collapse"  role="button"
   aria-expanded="false" aria-controls="collapseExample"
   onclick="$(this).next().toggle(300)">
  删除
</a>
<a class="badge badge-pill badge-danger text-white"
  style="display: none;cursor:pointer"
  @click="removeComment(comment.id)">
  <i class="fa fa-close"></i>
</a>

编写业务逻辑层代码

按id删除数据的方法由MybatisPlus提供,无需编写数据访问层

ICommentService接口添加删除评论的方法

// 按评论id删除评论的业务逻辑层方法
boolean removeComment(Integer commentId,String username);

CommentServiceImpl实现代码如下

@Override
public boolean removeComment(Integer commentId, String username) {
    User user=userMapper.findUserByUsername(username);
    //判断是不是讲师
    if(user.getType().equals(1)){
        // 如果是讲师,允许删除任何评论
        int num=commentMapper.deleteById(commentId);
        return num==1;
    }
    // 不是讲师删除评论要判断当前登录的用户是不是评论的发布者
    // 而评论的发布者id在comment对象中,我们需要先通过id查询到它
    Comment comment=commentMapper.selectById(commentId);
    // 判断评论的发布者是不是登录用户
    if(comment.getUserId().equals(user.getId())){
        // 如果是评论的发布者在删除评论,直接删除
        int num=commentMapper.deleteById(commentId);
        return num==1;
    }
    throw new ServiceException("您不能删除别人的评论!");
}

表连接(关联查询)回顾

常用的表连接

两大类

1.内连接

​ inner join :sql语句中inner可以省略

2.外连接

​ 外连接又分左连接和右连接

​ left outer join

​ right outer join

​ 其中outer可以省略

内连接:要求两张表必须有对应数据们才能查询出来

如果有的数据在另一张表中没有对应数据,那么它不会显示在查询结果中

外连接:要求定义一张主表

left join就是左侧表是主表 right join就是右侧表是主表

外连接主表中的所有数据一定会被查询出来

如果主表中的数据没有和被连接表的数据对应,它会对应null查询出一次

续 开发删除评论的功能

编写控制层代码

上次课完成了页面的二次确认和业务逻辑层代码

下面我们接着开发控制层

CommentController添加删除评论的控制器方法

// 按id删除评论的控制层方法
// /v1/comments/23/delete
@GetMapping("/{id}/delete")
public String removeComment(
        @PathVariable Integer id,
        @AuthenticationPrincipal UserDetails user){
    boolean isDelete=commentService
            .removeComment(id,user.getUsername());
    if(isDelete){
        return "ok";
    }else{
        return "fail";
    }

}

编写js代码

在编写二次确认效果时,我们已经编写了删除方法的调用

现在就差js代码中编写方法以及axios请求控制了

仍然在question_detail.js文件中的answersApp对象中添加方法

removeComment:function(commentId){
    axios({
        url:"/v1/comment/"+commentId+"/delete",
        method:"get"
    }).then(function(response){
        console.log(response.data);
    })
}

重启服务

测试登录小明删除其他人的评论,会获得异常提示信息

再登录讲师或李四同学删除自己的评论,测试显示ok表示成功!

删除评论同时删除页面信息

和之前添加评论一样,需要刷新页面才能实现页面中被删除的评论消失的效果

我们也要实现删除评论后,页面中对应的评论立即消失的效果

原理上,我们需要通过编写js代码,才能将这个评论从数组中移除,以实现页面中评论消失的效果

我们需要借助一个新的写法

detail_teacher.html的315行附近

<li class="media my-2"
  v-for="(comment,index) in answer.comments">

detail_teacher.html的344行附近

<a class="badge badge-pill badge-danger text-white"
   style="display: none;cursor: pointer"
   @click="removeComment(comment.id , index , answer.comments)" >
    							 <!-- ↑↑↑↑↑    ↑↑↑↑↑↑↑↑↑↑↑↑↑  -->
  <i class="fa fa-close"></i>
</a>

最后修改删除评论的js代码实现移除效果

//                               ↓↓↓↓↓  ↓↓↓↓↓↓↓
removeComment:function(commentId,index, comments){
    axios({
        url:"/v1/comments/"+commentId+"/delete",
        method:"get"
    }).then(function(response){
        console.log(response.data);
        if(response.data=="ok"){
            // splice表示要从数组对象中删除元素
            // 两个参数([删除的起始下标],[删除的个数])
            comments.splice(index,1);
        }
    })
}

重启服务,测试删除效果

开发修改评论的功能

实现点击编辑链接展开表单的效果

我们要实现编辑评论的效果

编辑评论的输入框也有只前添加评论输入框的问题

就是点击编辑链接,所有编辑评论的输入框都展开

这里的处理方式和之前解决添加评论的处理思路一致,只是因为一个回答会有多个评论的原因

我们需要使用评论id来区分它们

detail_teacher.html页面的330行附近

<a class="text-primary ml-2"
   style="font-size: small" data-toggle="collapse" href="#editCommemt1"
   role="button" aria-expanded="false" 
   aria-controls="collapseExample"
   :href="'#editComment'+comment.id" >
    <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑-->
  <i class="fa fa-edit"></i>编辑
</a>

detail_teacher.html页面的350行附近

<div class="collapse" id="editCommemt1"
    :id="'editComment'+comment.id">
     <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑-->
  <div class="card card-body border-light">
    <form action="" method="post" class="needs-validation" novalidate
       @submit.prevent="updateComment(comment.id,index,answer)" >
           <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑-->
      <div class="form-group">
        <textarea class="form-control"
                  id="textareaComment1" name="content" rows="4"
                  required
                  v-text="comment.content"></textarea>
                <!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑-->
        <div class="invalid-feedback">
          内容不能为空!
        </div>
      </div>
      <button type="submit" class="btn btn-primary my-1 float-right">提交修改</button>
    </form>
  </div>
</div>

重启服务测试,检查编辑链接是否只展开自己的输入框

编写修改评论的js代码

下面开始编写js代码,将修改评论的信息提交给控制器

question_detail.js文件的answersApp对象中继续添加方法

updateComment:function(commentId,index,answer){
    //需要获得修改后的评论内容,使用后代选择器获得对象和评论内容
    let textarea=$("#editComment"+commentId+" textarea");
    let content=textarea.val();
    // 如果内容是空就不修改了
    if(!content){
        return;
    }
    // 创建表单,封装CommentVO对象的数据
    // 这里利用SpringValidation验证,所以answerId也要传过去,但是并不使用
    let form=new FormData();
    form.append("answerId",answer.id);
    form.append("content",content);
    axios({
        url:"/v1/comments/"+commentId+"/update",
        method:"post",
        data:form
    }).then(function(response){
        // 控制器返回的是修改成功后的评论对象,直接获取
        let comment=response.data;
        // 本次修改操作没有变化数组元素的数量(长度没变化)
        // 数组元素数量不变化时,Vue不会自动更新页面上的内容
        // 既然它不会自动修改,就需要我们手动修改
        // Vue提供了我们手动修改元素内容的方法,
        // 这个方法引起的变化会显示在页面上
        // Vue.set([要修改的数组],[要修改的元素下标],[修改成什么])
        Vue.set(answer.comments,index,comment);
        // 修改成功后,编辑框自动收缩
        $("#editComment"+commentId).collapse("hide");
    })
}

编写控制器代码

上面的代码指定了将信息发送到控制器的路径

但是控制器还没有编写

CommentController类中添加方法来处理这个请求

//   /v1/comments/23/update
@PostMapping("/{id}/update")
public Comment updateComment(
        @PathVariable Integer id,
        @Validated CommentVO commentVO,
        BindingResult result,
        @AuthenticationPrincipal UserDetails user){
    log.debug("接收到表单信息:{}",commentVO);
    log.debug("要修改的评论id:{}",id);
    if(result.hasErrors()){
        String msg=result.getFieldError().getDefaultMessage();
        throw new ServiceException(msg);
    }
    // 这里调用业务逻辑层
    return null;
}

重启服务进行测试

无视页面效果,观察idea控制台是否能够输出正确的信息

编写业务逻辑层

ICommentService接口中添加方法

// 修改评论内容的业务逻辑层方法
Comment updateComment(Integer commentId, 
                      CommentVO commentVO,String username);     

CommentServiceImpl类中实现代码

@Override
@Transactional
public Comment updateComment(Integer commentId, CommentVO commentVO, String username) {
    // 获得用户信息
    User user=userMapper.findUserByUsername(username);
    // 按id查询当前要修改的评论对象
    Comment comment=commentMapper.selectById(commentId);
    // 如果是讲师或者是评论的发布者,可以修改评论
    if(user.getType().equals(1) ||
        comment.getUserId().equals(user.getId())){
        // 将要修改的属性直接赋值
        comment.setContent(commentVO.getContent());
        // 执行修改,将修改后的对象提交到数据库
        int num=commentMapper.updateById(comment);
        if(num!=1){
            throw new ServiceException("数据库忙!");
        }
        // 如果num是1就是修改成功了!返回修改成功的对象
        return comment;
    }
    throw new ServiceException("您不能修改别人的评论");
}

控制层调用业务逻辑层代码

CommentController类中updateComment方法添加调用

// 这里调用业务逻辑层
Comment comment=commentService.updateComment(
        id,commentVO,user.getUsername());
// 千万别忘了返回comment
return comment;

为了在用户修改他人评论时给出正确提示,防止bug

完善一下调用修改的js代码

axios的then方法修改为

.then(function(response){
    console.log("使用typeof判断返回值的类型:"
                        +typeof(response.data))
    if(typeof(response.data)=="object") {
        // 控制器返回的是修改成功后的评论对象,直接获取
        let comment = response.data;
        // 本次修改操作没有变化数组元素的数量(长度没变化)
        // 数组元素数量不变化时,Vue不会自动更新页面上的内容
        // 既然它不会自动修改,就需要我们手动修改
        // Vue提供了我们手动修改元素内容的方法,
        // 这个方法引起的变化会显示在页面上
        // Vue.set([要修改的数组],[要修改的元素下标],[修改成什么])
        Vue.set(answer.comments, index, comment);
        // 修改成功后,编辑框自动收缩
        $("#editComment" + commentId).collapse("hide");
    }else{
        alert(response.data);
    }
})

重启服务测试修改评论的功能

开发采纳答案功能

image-20211209113211860.png

开发显示学生问题详情页

image-20211209113519798.png

它们的区别如上图

我们可以直接复制讲师问题详情页为学生问题详情页,然后将学生问题详情页中不需要的内容删除即可

先删除提供给大家的detail_student.html,然后复制detail_teacher.html, 就地粘贴,重命名为detail_student.html即可

然后按上图删除新复制的学生问题详情页中的内容

回到学生首页index_student.html ,实现学生首页点击问题标题跳转详情页的效果

index_student.html的207行附近

<a class="text-dark" href="question/detail.html"
  v-text="question.title"
  :href="'/question/detail_student.html?'+question.id">
  eclipse 如何导入项目?
</a>

重启服务,登录学生,在学生首页点击标题跳转到问题详情页

采纳答案的业务思路

课程中我们只实现学生采纳答案的功能

但是如果是一个完整的问答系统,采纳答案功能不应该只有学生能够完成

业务上采纳答案可能有下面几种情况

  • 学生采纳答案:学生认为自己的问题已经解决,在学生的问题详情页中点击"采纳答案"按钮来完成采纳业务
  • 讲师采纳:如果经过一段时间学生都没有再有新的评论,讲师认为该问题已经解决,讲师可以在讲师问题详情页点击"采纳答案"按钮来完成采纳业务
  • 系统自动采纳:当一个问题长时间没有任何角色的用户进行操作时,系统会根据设计好的规则采纳回答

下面来描述一下学生采纳答案的业务流程

  1. 采纳答案也是需要二次确认的
  2. 业务逻辑层要判断当前采纳答案的用户是不是问题的提问者
  3. 采纳答案涉及两次数据修改操作,分别是
    1. 修改answer表的accept_status为1
    2. 修噶question表的status为2

达内知道项目的采纳答案的业务约束

​ 达内知道项目不限制一个问题只能采纳一个回答

​ 达内知道项目不限制一个已经解决的问题继续回答和评论

采纳答案的二次确认

采纳答案的二次确认和删除的基本一致

只是删除的红× 变成采纳的绿√

detail_student.html的367行附近

<a class="btn btn-primary mx-2 text-white"
   style="cursor: pointer"
   onclick="$(this).next().toggle(300)"
>采纳答案</a>
<a class="badge badge-pill badge-success text-white"
    style="display:none;cursor:pointer"
    @click="answerSolved(answer.id)">
  <i class="fa fa-check"></i>
</a>

重启服务,访问学生详情页,测试二次确认效果

我们的代码还绑定了js采纳回答方法的调用

编写js代码调用axios

question_detail.js文件中answersApp对象里新增js方法

answerSolved:function(answerId){
    axios({
        url:"/v1/answers/"+answerId+"/solved",
        method:"get"
    }).then(function(response){
        console.log(response.data);
    })
}

编写采纳答案的数据访问层

上面的业务流程分析中,我们确定了要修改的表示answer和question

我们要分别去编写这两个修改方法

AnswerMapper编写修改accept_status的方法

// 根据answerid修改采纳状态的方法
/*
JVM底层编译运行程序时,默认是不会保存局部变量名称的
由于方法的参数也是局部变量,所以参数的名称在编译时就消失了,运行时不能保存
导致Mybatis默认情况下多个参数时,是不能直接使用参数名称对应#{}中的内容的
但是SpringBoot官方脚手架创建的java项目JVM的参数进行了修改,
使得方法的局部变量名称也能保存,所以直接编写变量名就可以对应#{}里的名称
但是使用阿里的脚手架创建的SpringBoot项目就没有进行JVM的参数的修改,
就不能直接编写变量名称,会导致程序报错
最终为了保证程序能够顺利运行,最好在参数前添加@Param注解来标记对应的名称
 */
@Update("update answer set accept_status=#{acceptStatus} " +
        " where id=#{answerId}")
int updateAcceptStatus(@Param("acceptStatus") Integer acceptStatus,
                       @Param("answerId") Integer answerId);

// update [表名] set xxx=xxx  where xxx=xxx

QuestionMapper中添加修改问题状态的代码

编写方式和上面方法类似

@Update("update question set status=#{status} " +
        " where id=#{questionId}")
int updateStatus(@Param("status") Integer status,
                 @Param("questionId") Integer questionId );

定义Question类的状态常量

我们先确定Question类status列3个取值的含义

一般情况下,凡是数据库表中的列表示状态值,而且状态是3个以及3个以上时,一般都会在对应的实体类中定义常量来表示其含义

Question类添加常量如下

public class Question implements Serializable {

    private static final long serialVersionUID = 1L;

    public static final Integer POSTED=0;  //已提交\未回复
    public static final Integer SOLVING=1; //正在采纳\已回复
    public static final Integer SOLVED=2;  //已采纳\已解决
// 以下代码略
}

开发业务逻辑层代码

IAnswerService接口中添加采纳答案的业务逻辑层方法

// 根据答案id采纳答案的业务逻辑层方法
boolean accept(Integer answerId,String username);

AnswerServiceImpl类实现方法如下

@Autowired
private QuestionMapper questionMapper;

@Override
// 因为当前方法中有两次修改操作,必须添加事务
@Transactional
public boolean accept(Integer answerId, String username) {
    User user=userMapper.findUserByUsername(username);
    // 先根据answerId查询出answer的信息
    Answer answer=answerMapper.selectById(answerId);
    // 再根据answer中的questId查询question对象的信息
    Question question=questionMapper.selectById(answer.getQuestId());
    // 判断当前登录用户是不是问题的发布者
    if(user.getId().equals(question.getUserId())){
        // 如果当前问题是当前登录用户发布的,进行采纳流程
        // 先修改answer状态
        int num=answerMapper.updateAcceptStatus(
                                            1,answerId);
        if(num!=1){
            throw new ServiceException("数据库忙!");
        }
        // 再修改question状态
        num=questionMapper.updateStatus(
                                Question.SOLVED,question.getId());
        if(num!=1){
            throw new ServiceException("数据库忙!");
        }
        return true;
    }
    return false;
}

控制层调用

AnswerController添加采纳答案的控制层方法

// 采纳答案的控制层方法
@GetMapping("/{answerId}/solved")
public String solved(
        @PathVariable Integer answerId,
        @AuthenticationPrincipal UserDetails user){
    // 调用业务逻辑层方法
    boolean accepted=answerService.accept(
                    answerId,  user.getUsername());
    if(accepted){
        return "采纳完成";
    }else{
        return "您不能采纳别人的问题!";
    }

}

重启服务测试

到此为止,我们达内知道项目的所有单体功能开发就告一段落了

java项目的分类

java当前开发程序主要分为两大类

1.企业级应用

一般指一个企业或机构内部使用的网站或服务器程序

包括的领域不限于:医疗,教育,军事,政府,金融,企事业单位等

该类型项目特征是使用的人群比较固定,不是对全国乃至全世界开放的网站

因为使用的人数较少,所以对网站的性能没有强烈要求

但是企业级应用的业务流程和权限管理通常情况下是比较复杂的

2.互联网应用

一般指全国乃至全世界都可以访问的网站或服务器

京东,淘宝,饿了么,高德,微博,抖音,qq音乐,爱奇艺

它们对网站性能有强烈的要求

需要在非常繁忙的情况下还能迅速给出响应

对性能的要求非常高,可概括为:高并发,高可用,高性能

它的特征就是业务比较简单,但是需要在高并发,高可用,高性能情况下运行

英文

solve:采纳

js操作数组常用api

 push() 向数组的末尾添加一个或更多元素,并返回新的长度
 pop() 删除并返回数组的最后一个元素
 unshift() 向数组的开头添加一个或更多元素,并返回新的长度
 shift() 删除并返回数组的第一个元素
 splice(index, howmany) 从index位置删除howmany个数组元素
 splice(index, howmany, item) 从index位置删除howmany个数据元素并添加item元素
 sort() 对数组的元素进行排序
 reverse() 	颠倒数组中元素的顺序
 concat() 连接两个或更多的数组,并返回结果
 join()	把数组的所有元素放入一个字符串,元素通过指定的分隔符进行分隔

nums=[0,1,2,3,4,5,6]

nums.splice(3) [0,1,2]

nums.splice(3,2) [0,1,2,5,6]

nums.splice(3,1) [0,1,2,4,5,6]