项目之显示问题和回答问题(12)

举报
海拥 发表于 2021/08/05 00:50:09 2021/08/05
【摘要】 56. 老师主页显示问题列表-持久层 (a) 规划需要执行的SQL语句 老师主页显示的问题列表应该显示出老师自己发表的问题,和学生指定该老师回答的问题。 这样的列表数据可以使用此前的QuestionVO来表示每一个问题的数据,列表则使用List<QuestionVO>来表示。 需要执行的SQL语句大致是: select question.* fro...

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

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。