前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >项目之显示问题和回答问题(12)

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

作者头像
海拥
发布2021-08-23 16:18:53
6480
发布2021-08-23 16:18:53
举报
文章被收录于专栏:全栈技术全栈技术

56. 老师主页显示问题列表-持久层

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

老师主页显示的问题列表应该显示出老师自己发表的问题,和学生指定该老师回答的问题。

这样的列表数据可以使用此前的QuestionVO来表示每一个问题的数据,列表则使用List<QuestionVO>来表示。

需要执行的SQL语句大致是:

代码语言:javascript
复制
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;
(b) 在接口中添加抽象方法
代码语言:javascript
复制
/**
 * 查询老师的问题列表
 *
 * @param teacherId 老师的id
 * @return 老师发表的问题和希望该老师回复的问题的列表
 */
List<QuestionVO> findTeacherQuestions(Integer teacherId);
© 配置SQL映射
代码语言:javascript
复制
<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>
(d) 单元测试
代码语言:javascript
复制
@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);
    }
}

57. 老师主页显示问题列表-业务层

(a)
(b) 接口与抽象方法

原本存在抽象方法:

代码语言:javascript
复制
PageInfo<QuestionVO> getQuestionsByUserId(Integer userId, Integer page);

改为:

代码语言:javascript
复制
PageInfo<QuestionVO> getQuestionsByUserId(Integer userId, Integer type, Integer page);
© 实现业务方法

为了便于阅读程序源代码,先在User类中声明2个静态常量:

代码语言:javascript
复制
/**
 * 账号类型:学生
 */
public static final Integer TYPE_STUDENT = 0;
/**
 * 账号类型:老师
 */
public static final Integer TYPE_TEACHER = 1;

在原本存在的getQuestionsByUserId()方法的参数列表中添加参数,与以上抽象方法保持一致,然后,在实现过程中:

代码语言:javascript
复制
@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);
    }
    // 后续代码不变
}
(d) 单元测试

由于修改了业务方法的声明,当前控制器层的调用会因为参数不匹配而报错,将无法进行单元测试,所以,先处理完控制器层再测试。

58. 老师主页显示问题列表-控制器层

在原来的获取学生问题列表的方法中,调用业务方法时多添加type值即可,该值来自UserInfo参数:

代码语言:javascript
复制
@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);
}

完成后,应该分别测试学生账号登录后显示列表和老师账号登录后显示列表。

59. 老师主页显示问题列表-前端页面

引用index.html中的处理即可!也就是说:在index.html中将列表区域设置为th:fragment,然后在index_teacher.html中通过th:replace直接引用即可!

另外,关于点击问题的标题就可以跳转到“问题详情”页面,需要将跳转的<a>标签的href属性改为:

代码语言:javascript
复制
v-bind:href="'question/detail.html?' + question.id"

60. 显示问题详情-持久层

(a) 规划SQL语句

目前需要根据id显示问题的详情,在页面中需要显示的数据有:标题、正文、标签、收藏(暂未实现)、浏览次数、发布者、发布时间,目前,因为涉及问题的多个标签,只有QuestionVO才可以包含以上所有信息,在查询时,也需要把以上相关信息都查出来,结合使用QuestionVO封装结果,只需要查询question这1张表的数据即可。需要执行的SQL语句大致是:

代码语言:javascript
复制
select * from question where id=?

注意:在设计SQL语句时,条件越简单越好,应该只添加最核心的、用于保证本意的条件,其它的条件尽量在业务层中完成!

(b) 接口中的抽象方法

QuestionMapper接口中添加:

代码语言:javascript
复制
/**
 * 根据问题id查询问题详情
 *
 * @param id 问题的id
 * @return 匹配的问题详情,如果没有匹配的数据,则返回null
 */
QuestionVO findById(Integer id);
© 配置SQL语句

QuestionMapper.xml中配置以上抽象方法映射的SQL语句:

代码语言:javascript
复制
<select id="findById" resultMap="QuestionVOMap">
    SELECT
        *
    FROM
        question
    WHERE
        id=#{id}
</select>
(d) 单元测试

QuestionMapperTests中编写并执行单元测试(测试结果中,tags属性值目前为null):

代码语言:javascript
复制
@Test
void findById() {
    Integer id = 5;
    QuestionVO questionVO = mapper.findById(id);
    log.debug("question >>> {}", questionVO);
}

61. 显示问题详情-业务层

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

本次需要执行的是“根据id获取问题的详情”,首先,可能存在“数据不存在”,这种情况下应该抛出对应的异常,所以,需要创建:

代码语言:javascript
复制
public class QuestionNotFoundException extends ServiceException {}

同时,还应该检查数据的其它管理属性,例如is_public字段的值,或is_delete字段的值,此处就不再反复演示。

小技巧:如果当前设计的是某种查询功能的业务,例如获取某1个数据,或者获取某种数据列表,可能需要:

  • 检查数据是否存在;
  • 检查数据的管理属性;
  • 检查是否具有权限访问该数据(例如是不是自己的,或是否具有权限);
(b) 接口中的抽象方法

IQuestionService中添加:

代码语言:javascript
复制
/**
 * 根据提问的id查找问题详情
 *
 * @param id 问题的id
 * @return 匹配的问题的详情
 */
QuestionVO getQuestionById(Integer id);
© 实现业务方法

QuestionServiceImpl中实现以上方法:

代码语言:javascript
复制
/**
 * 根据标签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;
}
(d) 单元测试

QuestionServiceTests中测试:

代码语言:javascript
复制
@Test
void getQuestionById() {
    Integer id = 6;
    QuestionVO questionVO = service.getQuestionById(id);
    log.debug("question >>> {}", questionVO);
}

62. 显示问题详情-控制器层

(a) 处理异常

先在R.State中创建新的异常对应的错误码。

然后在GlobalExceptionHandler中处理新创建的QuestionNotFoundException

(b) 设计请求

请求路径:/api/v1/questions/{id}

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

请求方式:GET

响应结果:R<QuestionVO>

© 处理请求
代码语言:javascript
复制
// http://localhost:8080/api/v1/questions/6
@GetMapping("/{id}")
public R<QuestionVO> getQuestionById(@PathVariable("id") Integer id) {
    return R.ok(questionService.getQuestionById(id));
}
(d) 测试

在浏览器访问http://localhost:8080/api/v1/questions/6。

63. 显示问题详情-前端页面

前端页面需要使用的details.js

代码语言:javascript
复制
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();
    }
});

64. 回答问题-持久层

直接使用MyBatis Plus提供的insert()方法即可实现插入回复的数据。

65. 回答问题-业务层

(a) 规划业务流程、业务逻辑,创建必要的异常

此次的业务是向answer表中插入数据,没有唯一的字段,也不与其它表存在关联,所以,在插入之前不需要执行检查,在数据完整的情况下,直接插入数据即可。

小技巧:通常,在以增、删、改为主的业务中,都伴随着查询操作,特别是删、改的业务,至少都应该检查数据是否存在,当前用户是否具备删、改数据的权限,如果是以增为主的业务,主要检查是否存在某些数据需要唯一 (例如在用户注册时,用户名或手机号等数据就可能要求唯一,则需要事先检查),如果增加时还涉及其它表的数据,也可以需要检查数据关联等问题。

(b) 接口中的抽象方法

dto包中创建AnswerDTO类:

代码语言:javascript
复制
@Data
public class AnswerDTO {
    private Integer questionId;
    private String content;
}

IAnswerService中添加抽象方法:

代码语言:javascript
复制
/**
 * 提交问题的回复
 *
 * @param answerDTO    客户端提交的回复对象
 * @param userId       当前登录的用户id
 * @param userNickName 当前登录的用户昵称
 */
void post(AnswerDTO answerDTO, Integer userId, String userNickName);
© 实现业务

AnswerServiceImpl中规划业务方法的具体步骤:

代码语言:javascript
复制
@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
}

具体实现以上业务:

代码语言:javascript
复制
@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("回复问题失败!服务器忙,请稍后再次尝试!");
        }
    }

}
(d) 单元测试
代码语言:javascript
复制
@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);
        }
    }

}

66. 回答问题-控制器层

(a) 处理异常

本次业务层并没有抛出新的异常(从未处理过的异常),则无需处理!

(b) 设计请求

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

请求参数:Integer questionId, String content, @AuthenticationPriciple UserInfo userInfo

请求方式:POST

响应结果:R<Void>

© 处理请求

先在AnswerDTO中为属性添加注解,用于验证请求参数的有效性:

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

    @NotNull(message="问题id不允许为空!")
    private Integer questionId;
    @NotBlank(message="必须填写回复的内容!")
    private String content;

}

AnswerController中处理请求:

代码语言:javascript
复制
@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();
    }

}
(d) 测试

http://localhost:8080/api/v1/answers/post?questionId=1&content=666

67. 回答问题-前端页面

关于postAnswer.js代码:

代码语言:javascript
复制
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);
                    }
                }
            });
        }
    }
});
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-08-02 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 56. 老师主页显示问题列表-持久层
    • (a) 规划需要执行的SQL语句
      • (b) 在接口中添加抽象方法
        • © 配置SQL映射
          • (d) 单元测试
          • 57. 老师主页显示问题列表-业务层
            • (a)
              • (b) 接口与抽象方法
                • © 实现业务方法
                  • (d) 单元测试
                  • 58. 老师主页显示问题列表-控制器层
                  • 59. 老师主页显示问题列表-前端页面
                  • 60. 显示问题详情-持久层
                    • (a) 规划SQL语句
                      • (b) 接口中的抽象方法
                        • © 配置SQL语句
                          • (d) 单元测试
                          • 61. 显示问题详情-业务层
                            • (a) 规划业务并创建所需的异常
                              • (b) 接口中的抽象方法
                                • © 实现业务方法
                                  • (d) 单元测试
                                  • 62. 显示问题详情-控制器层
                                    • (a) 处理异常
                                      • (b) 设计请求
                                        • © 处理请求
                                          • (d) 测试
                                          • 63. 显示问题详情-前端页面
                                          • 64. 回答问题-持久层
                                          • 65. 回答问题-业务层
                                            • (a) 规划业务流程、业务逻辑,创建必要的异常
                                              • (b) 接口中的抽象方法
                                                • © 实现业务
                                                  • (d) 单元测试
                                                  • 66. 回答问题-控制器层
                                                    • (a) 处理异常
                                                      • (b) 设计请求
                                                        • © 处理请求
                                                          • (d) 测试
                                                          • 67. 回答问题-前端页面
                                                          领券
                                                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档