项目之显示回答和显示评论(13)

2021-08-23 16:19:07 浏览数 (1)

67. 显示回答列表-持久层

(a) 规划SQL语句

显示某问题的回答列表,需要执行的SQL语句大致是:

代码语言:javascript复制
select * from answer where question_id=16 order by status_of_accept desc, created_time desc

以上查询只是针对一张表,并且是查询所有字段,则使用实体类就可以封装以上查询到的数据!但是,本次查询应该另创建VO类用于封装查询结果,因为后续显示回答时,每个“回答”还可以存在若干个“评论”,则在“回答”的数据中,应该存在List<评论>的属性,由于当前还没有开发“评论”,所以,暂时无法设计这个属性,但是,后续一定用得上,基于“实体类需要对应数据表”,所以,迟早需要改为VO类来表示此次查询结果,就直接使用VO类了!

所以,应该事先创建AnswerVO类,声明与Answer实体类完全相同的属性即可。

代码语言:javascript复制
@Data
@Accessors(chain = true)
public class AnswerVO {

    private Integer id;
    private String content;
    private Integer countOfLikes;
    private Integer userId;
    private String userNickName;
    private Integer questionId;
    private LocalDateTime createdTime;
    private Integer statusOfAccept;

}
(b) 接口的抽象方法

AnswerMapper中添加抽象方法:

代码语言:javascript复制
/**
 * 根据问题的id查询回答的列表
 *
 * @param questionId 问题的id
 * @return 该问题的所有回答的列表
 */
List<AnswerVO> findListByQuestionId(Integer questionId);

注意:尽管可以使用MyBatis Plus实现查询功能,本次操作必须在持久层自定义抽象方法并配置SQL语句,因为当开发了“评论”后,当前查询需要改为关联查询!

© 配置SQL

AnswerMapper.xml中,将原有的<resultMap>复制,修改idtype,将应用于配置以上抽象方法的查询:

代码语言:javascript复制
<resultMap id="AnswerVO_Map" type="cn.tedu.straw.portal.vo.AnswerVO">
    <id column="id" property="id" />
    <result column="content" property="content" />
    <result column="count_of_likes" property="countOfLikes" />
    <result column="user_id" property="userId" />
    <result column="user_nick_name" property="userNickName" />
    <result column="question_id" property="questionId" />
    <result column="created_time" property="createdTime" />
    <result column="status_of_accept" property="statusOfAccept" />
</resultMap>

然后,配置以上抽象方法对应的SQL语句:

代码语言:javascript复制
<select id="findListByQuestionId" resultMap="AnswerVO_Map">
    SELECT
        *
    FROM
        answer
    WHERE
        question_id=#{questionId}
    ORDER BY
        status_of_accept DESC, created_time DESC
</select>
(d) 单元测试

创建AnswerMapperTests,编写并执行单元测试:

代码语言:javascript复制
@SpringBootTest
@Slf4j
public class AnswerMapperTests {

    @Autowired
    AnswerMapper mapper;

    @Test
    void findListByQuestionId() {
        Integer questionId = 16;
        List<AnswerVO> answers = mapper.findListByQuestionId(questionId);
        for (AnswerVO answer : answers) {
            log.debug("AnswerVO >>> {}", answer);
        }
    }

}

68. 显示回答列表-业务层

(a) 设计业务并创建必要的异常

(b) 接口的抽象方法

IAnswerService中添加:

代码语言:javascript复制
/**
 * 根据问题的id查询回答的列表
 *
 * @param questionId 问题的id
 * @return 该问题的所有回答的列表
 */
List<AnswerVO> getListByQuestionId(Integer questionId);
© 实现业务

AnswerServiceImpl中实现:

代码语言:javascript复制
@Override
public List<AnswerVO> getListByQuestionId(Integer questionId) {
    return answerMapper.findListByQuestionId(questionId);
}
(d) 单元测试

AnswerServiceTests中编写并执行单元测试:

代码语言:javascript复制
@Test
void getListByQuestionId() {
    Integer questionId = 16;
    List<AnswerVO> answers = service.getListByQuestionId(questionId);
    for (AnswerVO answer : answers) {
        log.debug("AnswerVO >>> {}", answer);
    }
}

69. 显示回答列表-控制器层

(a) 处理异常

(b) 设计请求

请求路径:/api/v1/answers/question/{questionId}

请求参数:@PathVariable("questionId") Integer questionId

请求方式:GET

响应结果:R<List<AnswerVO>>

© 处理请求

AnswerController中处理请求:

代码语言:javascript复制
// http://localhost:8080/api/v1/answers/question/16
@GetMapping("/question/{questionId}")
public R<List<AnswerVO>> getListByQuestionId(@PathVariable("questionId") Integer questionId) {
    return R.ok(answerService.getListByQuestionId(questionId));
}
(d) 测试

http://localhost:8080/api/v1/answers/question/16

70. 显示回答列表-前端界面

将原postAnswers.js改名为answers.js,并在detail.html中修改<script src="/js/question/postAnswers.js"><script src="/js/question/answers.js">

detail.html中调整Vue对象的位置,使用id为answersApp,在answers.js中,修改Vue对象对应页面元素的el值。

修改完成后,在answers.js中添加模拟的”回答“列表:

代码语言:javascript复制
let answersApp = new Vue({
    el: '#answersApp',
    data: {
        answers: [
            { userNickName: '成老师', createdTimeText: '37分钟前', content: '说了半天还是没答案啊' },
            { userNickName: '小刘老师', createdTimeText: '3小时前', content: '你们能不能好好说' },
            { userNickName: '范老师', createdTimeText: '6小时前', content: '你们说了几天也等于啥也没说' },
            { userNickName: '王老师', createdTimeText: '2天前', content: '不好说也得好好说' },
            { userNickName: '大刘老师', createdTimeText: '3天前', content: '这个问题不好说啊' }
        ]
    },
    // 忽略后续代码
}

并在页面中应用Vue。

测试无误后,添加:

代码语言:javascript复制
loadAnswers: function() {
    let id = location.search;
    if (!id) {
        alert("非法访问!参数不足!");
        location.href = '/index.html';
        return;
    }
    id = id.substring(1);
    if (!id || isNaN(id)) { // is not a number
        alert("非法访问!参数不足!");
        location.href = '/index.html';
        return;
    }
    $.ajax({
        url: '/api/v1/answers/question/'   id,
        success: function(json) {
            let answers = json.data;
            for (let i = 0; i < answers.length; i  ) {
                answers[i].createdTimeText = getCreatedTimeText(answers[i].createdTime);
            }
            answersApp.answers = answers;
        }
    });
}

71. 发表回答后实时更新回答列表

在JavaScript中,定义了unshift(arg)函数,可以将参数添加到数组中并且作为数组的第1个元素!

当老师填写”回答“后,就可以将”回答“的数据添加到Vue的”回答列表“中,且作为第1个元素,则页面就会显示刚刚提交的”回答“数据,例如:

代码语言:javascript复制
answersApp.answers.unshift(answer);

要实现以上效果,必须保证”服务器端响应的结果中包含新提交的回答数据“!

先在IAnswerService中修改原方法的声明,将返回值从void改为Answer

代码语言:javascript复制
/**
 * 提交问题的回复
 *
 * @param answerDTO    客户端提交的回复对象
 * @param userId       当前登录的用户id
 * @param userNickName 当前登录的用户昵称
 * @return 新创建的“回答”对象
 */
Answer post(AnswerDTO answerDTO, Integer userId, String userNickName);

然后,在AnswerServiceImpl实现类,也修改原方法的声明,并返回对象:

代码语言:javascript复制
@Override
public Answer post(AnswerDTO answerDTO, Integer userId, String userNickName) {
    // 原有代码不变
    // 在最后补充返回
    return answer;
}

在控制器中,将处理”提交回答“请求的方法改为返回R<Answer>

代码语言:javascript复制
// http://localhost:8080/api/v1/answers/post?questionId=1&content=666
@RequestMapping("/post")
public R<Answer> post(@Validated AnswerDTO answerDTO,
                      BindingResult bindingResult,
                      @AuthenticationPrincipal UserInfo userInfo) {
    if (bindingResult.hasErrors()) {
        String message = bindingResult.getFieldError().getDefaultMessage();
        throw new ParameterValidationException(message);
    }
    Answer answer = answerService.post(answerDTO, userInfo.getId(), userInfo.getNickname());
    return R.ok(answer);
}

最后,在前端页面中,当成功的提交了”回答“后:

代码语言:javascript复制
// 获取服务器端返回的新回答案的数据
let answer = json.data;
// unshift():在数组顶部添加元素
answersApp.answers.unshift(answer);

72. 导入评论表并生成文件

导入”评论“的数据表,并运行straw-generator代码生成器项目,生成相关的文件,将这些文件复制到straw-portal项目中。

73. 添加评论-持久层

(a)规划需要执行的SQL语句

添加评论的本质是向数据表中插入数据,由MyBatis Plus已经生成了对应的功能。

(b)接口的抽象方法

(c)配置SQL语句

(d)单元测试

74. 添加评论-业务层

(a)规划业务并创建异常

(b)接口的抽象方法

dto包下创建CommentDTO

代码语言:javascript复制
@Data
@Accessors(chain=true)
public class CommentDTO {
    private Integer answerId;
    private String content;
}

ICommentService中添加抽象方法:

代码语言:javascript复制
/**
 * 发表评论
 *
 * @param commentDTO   评论的数据
 * @param userId       当前登录的用户的id
 * @param userNickName 当前登录的用户的昵称
 * @return 成功发表的评论数据
 */
Comment post(CommentDTO commentDTO, Integer userId, String userNickName);
(c)实现业务

先规划业务的实现步骤:

代码语言:javascript复制
public Comment post(CommentDTO commentDTO, Integer userId, String userNickName) {
    // 创建Comment对象
    // 向Comment对象中封装数据:user_id			>>> 参数userId
    // 向Comment对象中封装数据:user_nick_name	>>> 参数userNickName
    // 向Comment对象中封装数据:answer_id		>>> commentDTO
    // 向Comment对象中封装数据:content			>>> commentDTO
    // 向Comment对象中封装数据:created_time		>>> 创建当前时间对象
    // 调用int commentMapper.insert(Comment comment)方法插入评论数据,获取返回的受影响行数
    // 判断返回值是否不为1
    // 是:抛出InsertException
    // 返回Comment对象
}

具体实现:

代码语言:javascript复制
@Service
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements ICommentService {
    
    @Autowired
    private CommentMapper commentMapper;

    @Override
    public Comment post(CommentDTO commentDTO, Integer userId, String userNickName) {
        // 创建Comment对象
        // 向Comment对象中封装数据:user_id			>>> 参数userId
        // 向Comment对象中封装数据:user_nick_name	>>> 参数userNickName
        // 向Comment对象中封装数据:answer_id		>>> commentDTO
        // 向Comment对象中封装数据:content			>>> commentDTO
        // 向Comment对象中封装数据:created_time		>>> 创建当前时间对象
        Comment comment = new Comment()
                .setUserId(userId)
                .setUserNickName(userNickName)
                .setAnswerId(commentDTO.getAnswerId())
                .setContent(commentDTO.getContent())
                .setCreatedTime(LocalDateTime.now());
        // 调用int commentMapper.insert(Comment comment)方法插入评论数据,获取返回的受影响行数
        int rows = commentMapper.insert(comment);
        // 判断返回值是否不为1
        if (rows != 1) {
            // 是:抛出InsertException
            throw new InsertException("发表评论失败!服务器忙,请稍后再次尝试!");
        }
        // 返回Comment对象
        return comment;
    }
}
(d)单元测试

创建CommentServiceTests测试类,编写并执行单元测试:

代码语言:javascript复制
@SpringBootTest
@Slf4j
public class CommentServiceTests {

    @Autowired
    ICommentService service;

    @Test
    void post() {
        CommentDTO commentDTO = new CommentDTO()
                .setAnswerId(4)
                .setContent("测试评论---1");
        Integer userId = 15;
        String userNickName = "机器猫";
        Comment comment = service.post(commentDTO, userId, userNickName);
        log.debug("OK, comment >>> {}", comment);
    }

}

75. 添加评论-控制器层

(a)处理新的异常

(b)设计请求

请求路径:/api/v1/comments/post

请求参数:CommentDTO commentDTO, UserInfo userInfo

请求类型:post

响应数据:R<Comment>

(c)处理请求

CommentController中处理请求:

代码语言:javascript复制
@RestController
@RequestMapping("/api/v1/comments")
public class CommentController {

    @Autowired
    private ICommentService commentService;

    // http://localhost:8080/api/v1/comments/post?answerId=4&content=Comment---2
    @RequestMapping("/post")
    public R<Comment> post(CommentDTO commentDTO,
        @AuthenticationPrincipal UserInfo userInfo) {
        Comment comment = commentService.post(commentDTO, userInfo.getId(), userInfo.getNickname());
        return R.ok(comment);
    }

}
(d)测试

http://localhost:8080/api/v1/comments/post?answerId=4&content=Comment—2

76. 添加评论-前端页面

目前,每个”回答“下方都有”添加评论“按钮,但是,不同的”添加评论“按钮是会互相影响的,是因为:

修改为:

然后,修改评论的表单,以保证可以提交请求,且提交请求时可以获取评论内容:

测试提交请求:

代码语言:javascript复制
postComment: function(answerId) {
    let content = $('#commentContent'   answerId).val();
    alert("准备发表答案id="   answerId   "的评论,评论内容是:"   content);
}

完成提交请求:

代码语言:javascript复制
postComment: function(answerId) {
    let content = $('#commentContent'   answerId).val();
    // alert("准备发表答案id="   answerId   "的评论,评论内容是:"   content);
    let data = {
        answerId: answerId,
        content: content
    }
    $.ajax({
        url: '/api/v1/comments/post',
        data: data,
        type: 'post',
        success: function(json) {
            if (json.state == 2000) {
                alert("发表评论成功!");
                console.log("服务器端返回的新发表的评论数据 >>> "   json.data);
                // 未完,仍需处理页面的显示
            } else {
                alert(json.message);
            }
        }
    });
}

77. 显示评论列表-持久层

(a)规划需要执行的SQL语句

应该在查询”回答“列表时,就一并查出每个”回答“对应的若干条”评论“!

将涉及关联查询,需要执行的SQL语句大致是:

代码语言:javascript复制
SELECT
	*
FROM
	answer
LEFT JOIN
	comment
ON
	answer.id=comment.answer_id
WHERE
	question_id=?
ORDER BY
	status_of_accept DESC,
	answer.created_time DESC,
	comment.created_time DESC

在测试以上SQL语句可以执行之前,还需要对SQL语句做进一步的调整,因为以上SQL语句的查询结果中存在多个名称相同的列,MyBatis框架在处理时,如果存在同名的列,只会处理靠前的列的数据,靠后的列的数据会被无视!所以,必须使用自定义别名的方式,使得查询结果中的每一列的名称都不相同!

则需要将以上SQL语句改为:

代码语言:javascript复制
SELECT
	answer.*,
	comment.id 				AS comment_id,
	comment.user_id 		AS comment_user_id,
	comment.user_nick_name 	AS comment_user_nick_name,
	comment.answer_id 		AS comment_answer_id,
	comment.content 		AS comment_content,
	comment.created_time 	AS comment_created_time
FROM
	answer
LEFT JOIN
	comment
ON
	answer.id=comment.answer_id
WHERE
	question_id=?
ORDER BY
	status_of_accept DESC,
	answer.created_time DESC,
	comment.created_time DESC
(b)接口与抽象方法

使用AnswerMapper原有的List<AnswerVO> findListByQuestionId(Integer questionId)方法即可,只不过,需要修改AnswerVO类,在这个类中添加private List<Comment> comments;用于表示每一个”回答“数据中有若干个”评论“。

(c)配置SQL语句
代码语言:javascript复制
<resultMap id="AnswerVO_Map" type="cn.tedu.straw.portal.vo.AnswerVO">
    <id column="id" property="id" />
    <result column="content" property="content" />
    <result column="count_of_likes" property="countOfLikes" />
    <result column="user_id" property="userId" />
    <result column="user_nick_name" property="userNickName" />
    <result column="question_id" property="questionId" />
    <result column="created_time" property="createdTime" />
    <result column="status_of_accept" property="statusOfAccept" />
    <collection property="comments"
        ofType="cn.tedu.straw.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_created_time" property="createdTime" />
    </collection>
</resultMap>

<select id="findListByQuestionId" resultMap="AnswerVO_Map">
    SELECT
        answer.*,
        comment.id 				AS comment_id,
        comment.user_id 		AS comment_user_id,
        comment.user_nick_name 	AS comment_user_nick_name,
        comment.answer_id 		AS comment_answer_id,
        comment.content 		AS comment_content,
        comment.created_time 	AS comment_created_time
    FROM
        answer
    LEFT JOIN
        comment
    ON
        answer.id=comment.answer_id
    WHERE
        question_id=#{questionId}
    ORDER BY
        status_of_accept DESC,
        answer.created_time DESC,
        comment.created_time DESC
</select>
(d)单元测试

直接运行原本已经开发的单元测试即可!

关于”回答列表“的业务层和控制器层都是不需要调整的,直接测试控制器层,可以看到返回的JSON数据就已经包含了List<Comment> comments的数据!

77. 显示评论列表-前端页面

首先,需要调整的是”显示回答列表“中的评论数量:

然后,遍历”回答“中的”评论列表“:

经过以上调整后,显示每个”回答“时,都会尝试显示该”回答“匹配的”评论列表“,即读取answer中的comments,但是,新发表”回答“时,插入到顶部的”回答“数据是服务器端响应的,并不包含comments,会导致读取该项的comments失败,为了避免这个问题,同时基于”新的回答肯定还没有被评论“,在将新的”回答“插入到顶部之前,为其补充空的comments属性,即:

代码语言:javascript复制
let answer = json.data;
answer.createdTimeText = getCreatedTimeText(answer.createdTime);
answer.comments = []; // 在将新的”回答“插入到顶部之前,为其补充空的comments属性
// unshift():在数组顶部添加元素
answersApp.answers.unshift(answer);

注意:如果Vue对象的answers属性存在模拟数据,这些模拟数据中也必须包含在页面显示时所使用到的属性,例如comments等,否则,在浏览器的控制台会报错,因为这些模拟数据会在页面打开之初就加载!当然,这些报错不影响程序的正常使用,因为打开页面之后,会直接发出异步请求,获取服务器响应的真实数据!所以,这些错只是短暂存在的!删除这些模拟数据就不会存在这些问题,如果要保留模拟数据,必须保证各属性都已经被声明,例如:

代码语言:javascript复制
answers: [
    { userNickName: '成老师', createdTimeText: '37分钟前', content: '说了半天还是没答案啊', comments: [] },
    { userNickName: '小刘老师', createdTimeText: '3小时前', content: '你们能不能好好说', comments: [] },
    { userNickName: '范老师', createdTimeText: '6小时前', content: '你们说了几天也等于啥也没说', comments: [] },
    { userNickName: '王老师', createdTimeText: '2天前', content: '不好说也得好好说', comments: [] },
    { userNickName: '大刘老师', createdTimeText: '3天前', content: '这个问题不好说啊', comments: [] }
]

最后,每次发表”评论“之后,新的评论并不会自动显示到页面,所以,在发表成功后,还应该:

代码语言:javascript复制
if (json.state == 2000) {
    alert("发表评论成功!");
    // 从服务器端返回的数据中获取“评论”数据对象
    let comment = json.data;
    // 由于当前页面的数据answers包含多条“回答”
    // 需要先找到本次评论对应的“回答”
    // 则遍历整个answers(即所有“回答”),检查id与参数answerId是否相同
    for (let i = 0; i < answersApp.answers.length; i  ) {
        if (answersApp.answers[i].id == answerId) {
            answersApp.answers[i].comments.unshift(comment);
            break;
        }
    }
} else {
    alert(json.message);
}

0 人点赞