项目之显示回答和显示评论(13)
67. 显示回答列表-持久层
(a) 规划SQL语句
显示某问题的回答列表,需要执行的SQL语句大致是:
select * from answer where question_id=16 order by status_of_accept desc, created_time desc
- 1
以上查询只是针对一张表,并且是查询所有字段,则使用实体类就可以封装以上查询到的数据!但是,本次查询应该另创建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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
(b) 接口的抽象方法
在AnswerMapper
中添加抽象方法:
/**
* 根据问题的id查询回答的列表
*
* @param questionId 问题的id
* @return 该问题的所有回答的列表
*/
List<AnswerVO> findListByQuestionId(Integer questionId);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
注意:尽管可以使用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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
然后,配置以上抽象方法对应的SQL语句:
<select id="findListByQuestionId" resultMap="AnswerVO_Map"> SELECT * FROM answer WHERE question_id=#{questionId} ORDER BY status_of_accept DESC, created_time DESC
</select>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
(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); } }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
68. 显示回答列表-业务层
(a) 设计业务并创建必要的异常
无
(b) 接口的抽象方法
在IAnswerService
中添加:
/**
* 根据问题的id查询回答的列表
*
* @param questionId 问题的id
* @return 该问题的所有回答的列表
*/
List<AnswerVO> getListByQuestionId(Integer questionId);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
© 实现业务
在AnswerServiceImpl
中实现:
@Override
public List<AnswerVO> getListByQuestionId(Integer questionId) { return answerMapper.findListByQuestionId(questionId);
}
- 1
- 2
- 3
- 4
(d) 单元测试
在AnswerServiceTests
中编写并执行单元测试:
@Test
void getListByQuestionId() { Integer questionId = 16; List<AnswerVO> answers = service.getListByQuestionId(questionId); for (AnswerVO answer : answers) { log.debug("AnswerVO >>> {}", answer); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
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));
}
- 1
- 2
- 3
- 4
- 5
(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: '这个问题不好说啊' } ] }, // 忽略后续代码
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
并在页面中应用Vue。
测试无误后,添加:
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; } });
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
71. 发表回答后实时更新回答列表
在JavaScript中,定义了unshift(arg)
函数,可以将参数添加到数组中并且作为数组的第1个元素!
当老师填写”回答“后,就可以将”回答“的数据添加到Vue的”回答列表“中,且作为第1个元素,则页面就会显示刚刚提交的”回答“数据,例如:
answersApp.answers.unshift(answer);
- 1
要实现以上效果,必须保证”服务器端响应的结果中包含新提交的回答数据“!
先在IAnswerService
中修改原方法的声明,将返回值从void
改为Answer
:
/**
* 提交问题的回复
*
* @param answerDTO 客户端提交的回复对象
* @param userId 当前登录的用户id
* @param userNickName 当前登录的用户昵称
* @return 新创建的“回答”对象
*/
Answer post(AnswerDTO answerDTO, Integer userId, String userNickName);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
然后,在AnswerServiceImpl
实现类,也修改原方法的声明,并返回对象:
@Override
public Answer post(AnswerDTO answerDTO, Integer userId, String userNickName) { // 原有代码不变 // 在最后补充返回 return answer;
}
- 1
- 2
- 3
- 4
- 5
- 6
在控制器中,将处理”提交回答“请求的方法改为返回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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
最后,在前端页面中,当成功的提交了”回答“后:
// 获取服务器端返回的新回答案的数据
let answer = json.data;
// unshift():在数组顶部添加元素
answersApp.answers.unshift(answer);
- 1
- 2
- 3
- 4
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
在ICommentService
中添加抽象方法:
/**
* 发表评论
*
* @param commentDTO 评论的数据
* @param userId 当前登录的用户的id
* @param userNickName 当前登录的用户的昵称
* @return 成功发表的评论数据
*/
Comment post(CommentDTO commentDTO, Integer userId, String userNickName);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
(c)实现业务
先规划业务的实现步骤:
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对象
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
具体实现:
@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; }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
(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); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
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); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
(d)测试
http://localhost:8080/api/v1/comments/post?answerId=4&content=Comment—2
76. 添加评论-前端页面
目前,每个”回答“下方都有”添加评论“按钮,但是,不同的”添加评论“按钮是会互相影响的,是因为:
修改为:
然后,修改评论的表单,以保证可以提交请求,且提交请求时可以获取评论内容:
测试提交请求:
postComment: function(answerId) { let content = $('#commentContent' + answerId).val(); alert("准备发表答案id=" + answerId + "的评论,评论内容是:" + content);
}
- 1
- 2
- 3
- 4
完成提交请求:
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); } } });
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
77. 显示评论列表-持久层
(a)规划需要执行的SQL语句
应该在查询”回答“列表时,就一并查出每个”回答“对应的若干条”评论“!
将涉及关联查询,需要执行的SQL语句大致是:
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
在测试以上SQL语句可以执行之前,还需要对SQL语句做进一步的调整,因为以上SQL语句的查询结果中存在多个名称相同的列,MyBatis框架在处理时,如果存在同名的列,只会处理靠前的列的数据,靠后的列的数据会被无视!所以,必须使用自定义别名的方式,使得查询结果中的每一列的名称都不相同!
则需要将以上SQL语句改为:
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
(b)接口与抽象方法
使用AnswerMapper
原有的List<AnswerVO> findListByQuestionId(Integer questionId)
方法即可,只不过,需要修改AnswerVO
类,在这个类中添加private List<Comment> comments;
用于表示每一个”回答“数据中有若干个”评论“。
(c)配置SQL语句
<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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
(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);
- 1
- 2
- 3
- 4
- 5
注意:如果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: [] }
]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
最后,每次发表”评论“之后,新的评论并不会自动显示到页面,所以,在发表成功后,还应该:
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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
文章来源: haiyong.blog.csdn.net,作者:海拥✘,版权归原作者所有,如需转载,请联系作者。
原文链接:haiyong.blog.csdn.net/article/details/107739447
- 点赞
- 收藏
- 关注作者
评论(0)