项目之显示问题和回答问题(12)
56. 老师主页显示问题列表-持久层
(a) 规划需要执行的SQL语句
老师主页显示的问题列表应该显示出老师自己发表的问题,和学生指定该老师回答的问题。
这样的列表数据可以使用此前的QuestionVO
来表示每一个问题的数据,列表则使用List<QuestionVO>
来表示。
需要执行的SQL语句大致是:
select question.*
from question
left join user_question
on question.id=user_question.question_id
where question.user_id=? or user_question.user_id=? and is_delete=0
order by status, modified_time desc;
- 1
- 2
- 3
- 4
- 5
- 6
(b) 在接口中添加抽象方法
/**
* 查询老师的问题列表
*
* @param teacherId 老师的id
* @return 老师发表的问题和希望该老师回复的问题的列表
*/
List<QuestionVO> findTeacherQuestions(Integer teacherId);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
© 配置SQL映射
<select id="findTeacherQuestions" resultMap="QuestionVOMap"> SELECT question.* FROM question LEFT JOIN user_question ON question.id=user_question.question_id WHERE question.user_id=#{teacherId} OR user_question.user_id=#{teacherId} AND is_delete=0 ORDER BY status, modified_time DESC
</select>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
(d) 单元测试
@Test
void findTeacherQuestions() { Integer teacherId = 3; List<QuestionVO> questions = mapper.findTeacherQuestions(teacherId); log.debug("question count={}", questions.size()); for (QuestionVO question : questions) { log.debug(">>> {}", question); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
57. 老师主页显示问题列表-业务层
(a)
(b) 接口与抽象方法
原本存在抽象方法:
PageInfo<QuestionVO> getQuestionsByUserId(Integer userId, Integer page);
- 1
改为:
PageInfo<QuestionVO> getQuestionsByUserId(Integer userId, Integer type, Integer page);
- 1
© 实现业务方法
为了便于阅读程序源代码,先在User
类中声明2个静态常量:
/**
* 账号类型:学生
*/
public static final Integer TYPE_STUDENT = 0;
/**
* 账号类型:老师
*/
public static final Integer TYPE_TEACHER = 1;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
在原本存在的getQuestionsByUserId()
方法的参数列表中添加参数,与以上抽象方法保持一致,然后,在实现过程中:
@Override
public PageInfo<QuestionVO> getQuestionsByUserId(Integer userId, Integer type, Integer page) { // 设置分页参数 PageHelper.startPage(page, pageSize); // 根据账号类型,调用持久层不同的方法查询问题列表,该列表中的数据只有标签的id,并不包括标签数据 List<QuestionVO> questions; if (type == User.TYPE_STUDENT) { questions = questionMapper.findStudentQuestions(userId); } else { questions = questionMapper.findTeacherQuestions(userId); } // 后续代码不变
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
(d) 单元测试
由于修改了业务方法的声明,当前控制器层的调用会因为参数不匹配而报错,将无法进行单元测试,所以,先处理完控制器层再测试。
58. 老师主页显示问题列表-控制器层
在原来的获取学生问题列表的方法中,调用业务方法时多添加type
值即可,该值来自UserInfo
参数:
@GetMapping("/my")
public R<PageInfo<QuestionVO>> getMyQuestions(Integer page, @AuthenticationPrincipal UserInfo userInfo) { if (page == null || page < 1) { page = 1; } PageInfo<QuestionVO> questions = questionService.getQuestionsByUserId(userInfo.getId(), userInfo.getType(), page); return R.ok(questions);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
完成后,应该分别测试学生账号登录后显示列表和老师账号登录后显示列表。
59. 老师主页显示问题列表-前端页面
引用index.html
中的处理即可!也就是说:在index.html
中将列表区域设置为th:fragment
,然后在index_teacher.html
中通过th:replace
直接引用即可!
另外,关于点击问题的标题就可以跳转到“问题详情”页面,需要将跳转的<a>
标签的href
属性改为:
v-bind:href="'question/detail.html?' + question.id"
- 1
60. 显示问题详情-持久层
(a) 规划SQL语句
目前需要根据id显示问题的详情,在页面中需要显示的数据有:标题、正文、标签、收藏(暂未实现)、浏览次数、发布者、发布时间,目前,因为涉及问题的多个标签,只有QuestionVO
才可以包含以上所有信息,在查询时,也需要把以上相关信息都查出来,结合使用QuestionVO
封装结果,只需要查询question
这1张表的数据即可。需要执行的SQL语句大致是:
select * from question where id=?
- 1
注意:在设计SQL语句时,条件越简单越好,应该只添加最核心的、用于保证本意的条件,其它的条件尽量在业务层中完成!
(b) 接口中的抽象方法
在QuestionMapper
接口中添加:
/**
* 根据问题id查询问题详情
*
* @param id 问题的id
* @return 匹配的问题详情,如果没有匹配的数据,则返回null
*/
QuestionVO findById(Integer id);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
© 配置SQL语句
在QuestionMapper.xml
中配置以上抽象方法映射的SQL语句:
<select id="findById" resultMap="QuestionVOMap"> SELECT * FROM question WHERE id=#{id}
</select>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
(d) 单元测试
在QuestionMapperTests
中编写并执行单元测试(测试结果中,tags
属性值目前为null
):
@Test
void findById() { Integer id = 5; QuestionVO questionVO = mapper.findById(id); log.debug("question >>> {}", questionVO);
}
- 1
- 2
- 3
- 4
- 5
- 6
61. 显示问题详情-业务层
(a) 规划业务并创建所需的异常
本次需要执行的是“根据id获取问题的详情”,首先,可能存在“数据不存在”,这种情况下应该抛出对应的异常,所以,需要创建:
public class QuestionNotFoundException extends ServiceException {}
- 1
同时,还应该检查数据的其它管理属性,例如is_public
字段的值,或is_delete
字段的值,此处就不再反复演示。
小技巧:如果当前设计的是某种查询功能的业务,例如获取某1个数据,或者获取某种数据列表,可能需要:
- 检查数据是否存在;
- 检查数据的管理属性;
- 检查是否具有权限访问该数据(例如是不是自己的,或是否具有权限);
(b) 接口中的抽象方法
在IQuestionService
中添加:
/**
* 根据提问的id查找问题详情
*
* @param id 问题的id
* @return 匹配的问题的详情
*/
QuestionVO getQuestionById(Integer id);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
© 实现业务方法
在QuestionServiceImpl
中实现以上方法:
/**
* 根据标签id获取标签(TagVO)数据的集合
*
* @param tagIdsStr 由若干个标签id组成的字符串,各id之间使用 , 分隔
* @return 签(TagVO)数据的集合
*/
private List<TagVO> getTagsByIds(String tagIdsStr) { // 拆分 String[] tagIds = tagIdsStr.split(", "); // 创建用于存放若干个标签的集合 List<TagVO> tags = new ArrayList<>(); // 遍历数组,从缓存中找出对应的TagVO for (String tagId : tagIds) { // 从缓存中取出对应的TagVO Integer id = Integer.valueOf(tagId); TagVO tag = tagService.getTagVOById(id); // 将取出的TagVO添加到QuestionVO对象中 tags.add(tag); } // 返回 return tags;
}
@Override
public QuestionVO getQuestionById(Integer id) { // 实现过程中,先通过持久层查询数据,并判断查询结果是否为null,如果为null,则抛出异常。 QuestionVO questionVO = questionMapper.findById(id); if (questionVO == null) { throw new QuestionNotFoundException("获取问题详情失败,尝试访问的数据不存在!"); } // 根据查询结果中的tagIds确定tags的值。 questionVO.setTags(getTagsByIds(questionVO.getTagIds())); // 返回查询结果 return questionVO;
}
- 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
(d) 单元测试
在QuestionServiceTests
中测试:
@Test
void getQuestionById() { Integer id = 6; QuestionVO questionVO = service.getQuestionById(id); log.debug("question >>> {}", questionVO);
}
- 1
- 2
- 3
- 4
- 5
- 6
62. 显示问题详情-控制器层
(a) 处理异常
先在R.State
中创建新的异常对应的错误码。
然后在GlobalExceptionHandler
中处理新创建的QuestionNotFoundException
。
(b) 设计请求
请求路径:/api/v1/questions/{id}
请求参数:@PathVariable("id") Integer id
请求方式:GET
响应结果:R<QuestionVO>
© 处理请求
// http://localhost:8080/api/v1/questions/6
@GetMapping("/{id}")
public R<QuestionVO> getQuestionById(@PathVariable("id") Integer id) { return R.ok(questionService.getQuestionById(id));
}
- 1
- 2
- 3
- 4
- 5
(d) 测试
在浏览器访问http://localhost:8080/api/v1/questions/6。
63. 显示问题详情-前端页面
前端页面需要使用的details.js
:
let questionInfoApp = new Vue({ el: '#questionInfoApp', data: { question: { title: 'Vue中的v-text和v-html有什么区别?', content: '感觉都是用来设置标签内部显示的内容的,区别在哪里呢?', userNickName: '天下无敌', createdTimeText: '58分钟前', hits: 998, tags: [ { id: 5, name: 'Java SE' }, { id: 7, name: 'Spring' }, { id: 16, name: 'Mybatis' } ] } }, methods: { loadQuestion: 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/questions/' + id, success: function(json) { if (json.state == 2000) { questionInfoApp.question = json.data; } else { alert(json.message); location.href = "/index.html"; } } }); } }, created: function () { this.loadQuestion(); }
});
- 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
- 43
- 44
- 45
- 46
- 47
64. 回答问题-持久层
直接使用MyBatis Plus提供的insert()
方法即可实现插入回复的数据。
65. 回答问题-业务层
(a) 规划业务流程、业务逻辑,创建必要的异常
此次的业务是向answer
表中插入数据,没有唯一的字段,也不与其它表存在关联,所以,在插入之前不需要执行检查,在数据完整的情况下,直接插入数据即可。
小技巧:通常,在以增、删、改为主的业务中,都伴随着查询操作,特别是删、改的业务,至少都应该检查数据是否存在,当前用户是否具备删、改数据的权限,如果是以增为主的业务,主要检查是否存在某些数据需要唯一 (例如在用户注册时,用户名或手机号等数据就可能要求唯一,则需要事先检查),如果增加时还涉及其它表的数据,也可以需要检查数据关联等问题。
(b) 接口中的抽象方法
在dto
包中创建AnswerDTO
类:
@Data
public class AnswerDTO { private Integer questionId; private String content;
}
- 1
- 2
- 3
- 4
- 5
在IAnswerService
中添加抽象方法:
/**
* 提交问题的回复
*
* @param answerDTO 客户端提交的回复对象
* @param userId 当前登录的用户id
* @param userNickName 当前登录的用户昵称
*/
void post(AnswerDTO answerDTO, Integer userId, String userNickName);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
© 实现业务
在AnswerServiceImpl
中规划业务方法的具体步骤:
@Autowired
private AnswerMapper answerMapper;
public void post(AnswerDTO answerDTO, Integer userId, String userNickName) { // 创建Answer对象 // 补全answer对象的属性值:content <<< 参数answerDTO中的content // 补全answer对象的属性值:count_of_likes <<< 0 // 补全answer对象的属性值:user_id <<< 参数userId // 补全answer对象的属性值:user_nick_name <<< 参数userNickName // 补全answer对象的属性值:question_id <<< 参数answerDTO中的questionId // 补全answer对象的属性值:created_time <<< 当前时间 // 补全answer对象的属性值:status_of_accept <<< 0 // 调用int answerMapper.insert(Answer answer)方法插入“回复”的数据,并获取返回结果 // 判断返回值是否不为1 // 是:抛出InsertException
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
具体实现以上业务:
@Service
public class AnswerServiceImpl extends ServiceImpl<AnswerMapper, Answer> implements IAnswerService { @Autowired private AnswerMapper answerMapper; @Override public void post(AnswerDTO answerDTO, Integer userId, String userNickName) { // 创建Answer对象 Answer answer = new Answer(); // 补全answer对象的属性值:content <<< 参数answerDTO中的content answer.setContent(answerDTO.getContent()); // 补全answer对象的属性值:count_of_likes <<< 0 answer.setCountOfLikes(0); // 补全answer对象的属性值:user_id <<< 参数userId answer.setUserId(userId); // 补全answer对象的属性值:user_nick_name <<< 参数userNickName answer.setUserNickName(userNickName); // 补全answer对象的属性值:question_id <<< 参数answerDTO中的questionId answer.setQuestionId(answerDTO.getQuestionId()); // 补全answer对象的属性值:created_time <<< 当前时间 answer.setCreatedTime(LocalDateTime.now()); // 补全answer对象的属性值:status_of_accept <<< 0 answer.setStatusOfAccept(0); // 调用int answerMapper.insert(Answer answer)方法插入“回复”的数据,并获取返回结果 int rows = answerMapper.insert(answer); // 判断返回值是否不为1 if (rows != 1) { // 是:抛出InsertException throw new InsertException("回复问题失败!服务器忙,请稍后再次尝试!"); } }
}
- 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
(d) 单元测试
@SpringBootTest
@Slf4j
public class AnswerServiceTests { @Autowired IAnswerService service; @Test void post() { try { AnswerDTO answerDTO = new AnswerDTO() .setQuestionId(1) .setContent("HAHAHA!!!"); Integer userId = 2; String userNickName = "天下第一"; service.post(answerDTO, userId, userNickName); log.debug("OK"); } catch (ServiceException e) { log.debug("failure >>> ", e); } }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
66. 回答问题-控制器层
(a) 处理异常
本次业务层并没有抛出新的异常(从未处理过的异常),则无需处理!
(b) 设计请求
请求路径:/api/v1/answers/post
请求参数:Integer questionId
, String content
, @AuthenticationPriciple UserInfo userInfo
请求方式:POST
响应结果:R<Void>
© 处理请求
先在AnswerDTO
中为属性添加注解,用于验证请求参数的有效性:
@Data
@Accessors(chain = true)
public class AnswerDTO { @NotNull(message="问题id不允许为空!") private Integer questionId; @NotBlank(message="必须填写回复的内容!") private String content;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
在AnswerController
中处理请求:
@RestController
@RequestMapping("/api/v1/answers")
public class AnswerController { @Autowired private IAnswerService answerService; // http://localhost:8080/api/v1/answers/post?questionId=1&content=666 @RequestMapping("/post") public R<Void> post(@Validated AnswerDTO answerDTO, BindingResult bindingResult, @AuthenticationPrincipal UserInfo userInfo) { if (bindingResult.hasErrors()) { String message = bindingResult.getFieldError().getDefaultMessage(); throw new ParameterValidationException(message); } answerService.post(answerDTO, userInfo.getId(), userInfo.getNickname()); return R.ok(); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
(d) 测试
http://localhost:8080/api/v1/answers/post?questionId=1&content=666
67. 回答问题-前端页面
关于postAnswer.js
代码:
let writeAnswerApp = new Vue({ el: '#writeAnswerApp', data: { }, methods: { postAnswer: function () { let questionId = location.search.substring(1); let content = $('#summernote').val(); // 注意:以下data表示提交到服务器端的数据 // 属性名称必须与AnswerDTO的属性名称保持一致 let data = { questionId: questionId, content: content } $.ajax({ url: '/api/v1/answers/post', data: data, type: 'post', success: function (json) { if (json.state == 2000) { alert('回复成功!'); // 应该将数据显示到列表 // 如果要上传图片,必须启动静态资源服务器 // $('#form-post-answer')[0].reset(); $('#summernote').summernote('reset'); } 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
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
文章来源: haiyong.blog.csdn.net,作者:海拥✘,版权归原作者所有,如需转载,请联系作者。
原文链接:haiyong.blog.csdn.net/article/details/107739380
- 点赞
- 收藏
- 关注作者
评论(0)