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
实体类完全相同的属性即可。
@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
中添加抽象方法:
/**
* 根据问题的id查询回答的列表
*
* @param questionId 问题的id
* @return 该问题的所有回答的列表
*/
List<AnswerVO> findListByQuestionId(Integer questionId);
注意:尽管可以使用MyBatis Plus实现查询功能,本次操作必须在持久层自定义抽象方法并配置SQL语句,因为当开发了“评论”后,当前查询需要改为关联查询!
© 配置SQL
在AnswerMapper.xml
中,将原有的<resultMap>
复制,修改id
和type
,将应用于配置以上抽象方法的查询:
<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
,编写并执行单元测试:
@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
中添加:
/**
* 根据问题的id查询回答的列表
*
* @param questionId 问题的id
* @return 该问题的所有回答的列表
*/
List<AnswerVO> getListByQuestionId(Integer questionId);
© 实现业务
在AnswerServiceImpl
中实现:
@Override
public List<AnswerVO> getListByQuestionId(Integer questionId) {
return answerMapper.findListByQuestionId(questionId);
}
(d) 单元测试
在AnswerServiceTests
中编写并执行单元测试:
@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
中处理请求:
// 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
中添加模拟的”回答“列表:
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
:
/**
* 提交问题的回复
*
* @param answerDTO 客户端提交的回复对象
* @param userId 当前登录的用户id
* @param userNickName 当前登录的用户昵称
* @return 新创建的“回答”对象
*/
Answer post(AnswerDTO answerDTO, Integer userId, String userNickName);
然后,在AnswerServiceImpl
实现类,也修改原方法的声明,并返回对象:
@Override
public Answer post(AnswerDTO answerDTO, Integer userId, String userNickName) {
// 原有代码不变
// 在最后补充返回
return answer;
}
在控制器中,将处理”提交回答“请求的方法改为返回R<Answer>
:
// 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
:
@Data
@Accessors(chain=true)
public class CommentDTO {
private Integer answerId;
private String content;
}
在ICommentService
中添加抽象方法:
/**
* 发表评论
*
* @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
测试类,编写并执行单元测试:
@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
中处理请求:
@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
属性,即:
let answer = json.data;
answer.createdTimeText = getCreatedTimeText(answer.createdTime);
answer.comments = []; // 在将新的”回答“插入到顶部之前,为其补充空的comments属性
// unshift():在数组顶部添加元素
answersApp.answers.unshift(answer);
注意:如果Vue对象的answers
属性存在模拟数据,这些模拟数据中也必须包含在页面显示时所使用到的属性,例如comments
等,否则,在浏览器的控制台会报错,因为这些模拟数据会在页面打开之初就加载!当然,这些报错不影响程序的正常使用,因为打开页面之后,会直接发出异步请求,获取服务器响应的真实数据!所以,这些错只是短暂存在的!删除这些模拟数据就不会存在这些问题,如果要保留模拟数据,必须保证各属性都已经被声明,例如:
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);
}