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

2021-08-23 16:18:53 浏览数 (1)

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);
                    }
                }
            });
        }
    }
});

0 人点赞