超详细!利用SpringBoot+SpringCloud做一个问答项目(十六)

目录

一、发表答案--持久层

二、发表答案--业务层

三、发表答案--控制器层

四、发表答案--前端页面

五、显示答案列表--持久层

六、显示答案列表--业务层

七、显示答案列表--控制器层

八、显示答案列表--前端页面

九、发表评论--持久层

十、发表评论--业务层

十一、发表评论--控制器层

十二、发表评论--前端页面

十三、显示评论列表

一、发表答案--持久层


运行代码生成器项目,输入表名answer,并将生成的各文件复制到对应的项目中,并修改代码中的错误,由于接下来还会开发“评论”相关功能,再次运行代码生成器项目,输入表名comment,重复制刚才的步骤,将各代码文件复制到正确的位置。

发表答案时持久层需要执行操作是“插入答案数据”,该功能在Mybatis Plus框架中已经实现了,无须开发!

在执行发表答案之前,还应该检查对应的“问题”是否存在,如果“问题”不存在,是不允许发表的!这项检查可以通过此前已经完成的QuestionMapper中的findById()来实现,无须再次开发。

 

二、发表答案--业务层


在 dto 包中创建 PostAnswerDTO 类:

@Data
@Accessors(chain=true)
public class PostAnswerDTO implements Serializable {
private Integer questionId;
private String content;
}

可以自行在以上类的属性之前添加注解,以配置数据格式的规则。

在 IAnswerService 中添加抽象方法:

public interface IAnswerService extends IService<Answer> {
/**
* 发表答案
*
* @param postAnswerDTO 客户端提交的答案
* @param userId 当前登录的用户的id
* @param userNickName 当前登录的用户名
*/
void post(PostAnswerDTO postAnswerDTO, Integer userId, String
userNickName);
}

在 AnswerServiceImpl 中设计实现步骤并实现:

@Service
public class AnswerServiceImpl extends ServiceImpl<AnswerMapper, Answer>
implements IAnswerService {
    @Autowired
    QuestionMapper questionMapper;
    @Autowired
    AnswerMapper answerMapper;
    @Override
    public void post(PostAnswerDTO postAnswerDTO, Integer userId, String
    userNickName) {
        // 基于参数questionId调用questionMapper.findById()查询“问题”数据
        QuestionDetailVO question =
        questionMapper.findById(postAnswerDTO.getQuestionId());
        // 判断以上查询结果是否为null
        if (question == null) {
            // 是:抛出QuestionNotFoundException
            throw new QuestionNotFoundException("发表答案失败!问题数据不存
            在!");
        }
        // 创建当前时间对象now
        LocalDateTime now = LocalDateTime.now();
        // 创建Answer对象
        Answer answer = new Answer()
        // 补全Answer对象的属性:content < 参数
            .setContent(postAnswerDTO.getContent())
            // 补全Answer对象的属性:userId, userNickName < 参数
            .setUserId(userId)
            .setUserNickName(userNickName)
            // 补全Answer对象的属性:questionId < 参数
            .setQuestionId(postAnswerDTO.getQuestionId())
            // 补全Answer对象的属性:isAccepted < 0
            .setIsAccepted(0)
            // 补全Answer对象的属性:gtmCreate, gmtModified < now
            .setGmtCreate(now)
            .setGmtModified(now);
            // 基于Answer对象调用answerMapper.insert()插入“答案”数据,并获取返回值
        int rows = answerMapper.insert(answer);
        // 判断以上返回值是否不为1
        if (rows != 1) {
            // 是:抛出InsertException
            throw new InsertException("发表答案失败!服务器忙,请稍后再次尝试!");
        }
    }
}

在 AnswerServiceTests (不存在,需创建)中编写并执行单元测试:

@SpringBootTest
@Slf4j
public class AnswerServiceTests {
    @Autowired
    IAnswerService answerService;
    @Test
    void post() {
    try {
        PostAnswerDTO postAnswerDTO = new PostAnswerDTO()
            .setQuestionId(10)
            .setContent("测试发表10号问题的答案");
        Integer userId = 1;
        String userNickName = "超人";
        answerService.post(postAnswerDTO, userId, userNickName);
        log.debug("发表答案成功!");
    } catch (ServiceException e) {
        log.debug("发表答案失败,错误类型:{}", e.getClass().getName());
        log.debug("原因:{}", e.getMessage());
       }
    }
}

三、发表答案--控制器层


关于控制器层:

@RestController
@RequestMapping("/v1/answers")
public class AnswerController {
    @Autowired
    IAnswerService answerService;
    // 【不可用】http://localhost:8081/v1/answers/post?
    questionId=1&content=haha
    // http://localhost/api-question/v1/answers/post?
    questionId=1&content=haha
    @RequestMapping("/post")
    public R post(@Valid PostAnswerDTO postAnswerDTO, BindingResult
    bindingResult,
    @AuthenticationPrincipal LoginUserInfo loginUserInfo) {
    // 参考“发布问题”
    if (bindingResult.hasErrors()) {
        String errorMessage =
        bindingResult.getFieldError().getDefaultMessage();
        throw new IllegalParameterException(errorMessage);
    }
    answerService.post(postAnswerDTO, loginUserInfo.getId(),
    loginUserInfo.getNickname());
    return R.ok();
    }
}

完成后,重启项目,在浏览器中,通过以上网关的路径进行测试。

四、发表答案--前端页面


在 detail.html 中,先删除页面底部原本就存在的初始化summernote的代码:

并引用 init_summernote.js 来完成初始化summernote:

在页面中,找到包含了“答案列表”和“发表答案的表单”的区域,添加 id 属性,准备创建VUE对象:

为了把即将要写的代码与此前完成的区分开(主要是避免出错),在 static/js/question 下创建 detail2.js 文件,并引用该文件:

则可以在 detail2.js 中创建对应的VUE对象:

然后,在页面中绑定表单控件:

然后,实现点击效果:

五、显示答案列表--持久层


显示答案列表需要执行的SQL语句大致是:

select id, content, user_id, user_nick_name, is_accepted, gmt_create from
answer where question_id=? order by gmt_create desc

在 straw-commons 中创建 AnswerListItemVO :

@Data
@Accessors(chain = true)
public class AnswerListItemVO implements Serializable {
    private Integer id;
    private String content;
    private Integer userId;
    private String userNickName;
    private Integer isAccepted;
    private LocalDateTime gmtCreate;
}

在 straw-api-question 的 AnswerMapper.java 接口中添加抽象方法:

@Repository
public interface AnswerMapper extends BaseMapper<Answer> {
    /**
    * 查询某“问题”的答案列表
    *
    * @param questionId “问题”的id
    * @return 该“问题”的答案列表
    */
    List<AnswerListItemVO> findByQuestionId(Integer questionId);
}

在 AnswerMapper.xml 中配置:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.tedu.straw.api.question.mapper.AnswerMapper">
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="cn.tedu.straw.commons.model.Answer">
        <id column="id" property="id" />
        <result column="content" property="content" />
        <result column="user_id" property="userId" />
        <result column="user_nick_name" property="userNickName" />
        <result column="question_id" property="questionId" />
        <result column="is_accepted" property="isAccepted" />
        <result column="gmt_create" property="gmtCreate" />
        <result column="gmt_modified" property="gmtModified" />
    </resultMap>
    <resultMap id="AnswerListItemMap"
    type="cn.tedu.straw.commons.vo.AnswerListItemVO">
        <id column="id" property="id" />
        <result column="content" property="content" />
        <result column="user_id" property="userId" />
        <result column="user_nick_name" property="userNickName" />
        <result column="is_accepted" property="isAccepted" />
        <result column="gmt_create" property="gmtCreate" />
    </resultMap>
    <select id="findByQuestionId" resultMap="AnswerListItemMap">
        SELECT
            id, content, user_id,
            user_nick_name, is_accepted, gmt_create
        FROM
            answer
        WHERE
            question_id=#{questionId}
        ORDER BY
            gmt_create DESC
    </select>
</mapper>

在 AnswerMapperTests (不存在,需创建)中编写并执行单元测试(需事先准备测试数据):

@SpringBootTest
@Slf4j
public class AnswerMapperTests {
    @Autowired
    AnswerMapper mapper;
    @Test
    void findByQuestionId() {
        Integer questionId = 16;
        List<AnswerListItemVO> answers =
        mapper.findByQuestionId(questionId);
        log.debug("根据问题id={}查询到{}个答案:", questionId, answers.size());
        for (AnswerListItemVO answer : answers) {
            log.debug("答案 >>> {}", answer);
        }
    }
}

六、显示答案列表--业务层


在 IAnswerService 中添加抽象方法:

/**
* 查询某“问题”的答案列表
*
* @param questionId “问题”的id
* @return 该“问题”的答案列表
*/
List<AnswerListItemVO> getAnswerList(Integer questionId);

在 AnswerServiceImpl 中实现以上方法:

@Override
public List<AnswerListItemVO> getAnswerList(Integer questionId) {
return answerMapper.findByQuestionId(questionId);
}

在 AnswerServiceTests 中测试:

@Test
void getAnswerList() {
    Integer questionId = 16;
    List<AnswerListItemVO> answers = service.getAnswerList(questionId);
    log.debug("根据问题id={}查询到{}个答案:", questionId, answers.size());
    for (AnswerListItemVO answer : answers) {
        log.debug("答案 >>> {}", answer);
    }
}

七、显示答案列表--控制器层


在 AnswerController 中添加处理请求的方法:

// http://localhost:8081/v1/answers?questionId=1
// http://localhost/api-question/v1/answers?questionId=1
@GetMapping("")
public R<List<AnswerListItemVO>> getAnswerList(Integer questionId) {
    return R.ok(answerService.getAnswerList(questionId));
}

完成后,重启项目,分别使用以上2个测试URL在浏览器中测试访问。

八、显示答案列表--前端页面


将页面中的元素绑定Vue属性:

完成后,重启项目,在“问题详情”页面即可看到模拟的“答案列表”数据。

然后,在detail2.js的Vue中添加函数,并在页面加载时调用该函数向服务器端请求获取答案列表,并更新Vue属性以显示数据:

至此,页面刚刚打开时,就可以显示该“问题”的答案列表!

然后,还应该调整为“发表答案成功后刷新答案列表”,为了使得发表答案后可以跳转到页面的答案列表顶部,先在答案列表顶部添加锚点:

然后,当“发表答案”成功后,先将富文本编辑区域还原成默认状态,再调用loadAnswers()更新列表数据,并跳转到锚点位置:

九、发表评论--持久层


发表评论的数据操作依然是向“评论”数据表中插入数据,可以使用Mybatis Plus的insert()功能直接实现。

与“发表答案”相同,评论是针对某条答案的,所以,在发表评论之前应该检查这条“答案”是否存在,所以,需要实现“根据答案的id查询答案数据”的功能,需要执行的SQL语句大致是:

select * from answer where id=?

其实,在Mybatis Plus中也定义了selectById功能,所以,这项检查的代码也不需要自行开发!

另外,在“发表评论”时也可以检查“问题”是否存在,但是,一般没有这个必要性!因为“问题”应该是存在的,否则“答案”就不存在!即使“问题”曾经是存在的,但是,在发表评论之前被删除而导致不存在,当前开发发表评论时也可以不检查,因为删除问题时应该把对应的答案一并删除,会导致答案不存在,所以,最终,只需要检查“答案”是否存在即可!

十、发表评论--业务层


创建AnswerNotFoundException异常类,用于表示“答案数据不存在”:

/**
 * 答案数据不存在
 */
public class AnswerNotFoundException extends ServiceException {
	// 构造方法
}

创建PostCommentDTO类:

@Data
@Accessors(chain=true)
public class PostCommentDTO implements Serializable {
    private Integer answerId;
    private String content;
}

ICommentService中添加抽象方法:

/**
 * 发表评论
 *
 * @param postCommentDTO 评论数据
 * @param userId         当前登录的用户的id
 * @param userNickName   当前登录的用户名
 */
void post(PostCommentDTO postCommentDTO, Integer userId, String userNickName);

CommentServiceImpl中设计抽象方法的实现步骤并实现:

@Service
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements ICommentService {

    @Autowired
    AnswerMapper answerMapper;
    @Autowired
    CommentMapper commentMapper;

    @Override
    public void post(PostCommentDTO postCommentDTO, Integer userId, String userNickName) {
        // 基于参数answerId调用answerMapper.selectById()查询“答案”数据
        Answer answer = answerMapper.selectById(postCommentDTO.getAnswerId());
        // 判断查询到的“答案”是否为null
        if (answer == null) {
            // 是:抛出AnswerNotFoundException
            throw new AnswerNotFoundException("发表评论失败!评论的答案不存在!");
        }

        // 创建当前时间对象now
        LocalDateTime now = LocalDateTime.now();
        // 创建Comment对象
        Comment comment = new Comment()
            // 向Comment对象中补全数据:userId, userNickName < 参数
            .setUserId(userId)
            .setUserNickName(userNickName)
            // 向Comment对象中补全数据:answerId < 参数
            .setAnswerId(postCommentDTO.getAnswerId())
            // 向Comment对象中补全数据:content < 参数
            .setContent(postCommentDTO.getContent())
            // 向Comment对象中补全数据:gmtCreate, gmtModified < now
            .setGmtCreate(now)
            .setGmtModified(now);
        // 基于以上Comment对象调用commentMapper.insert()插入评论数据,并获取返回值
        int rows = commentMapper.insert(comment);
        // 判断返回值是否不为1
        if (rows != 1) {
            // 是:抛出InsertException
            throw new InsertException("发表评论失败!服务器忙,请稍后再次尝试!");
        }
    }

}

CommentServiceTests中测试:

@SpringBootTest
@Slf4j
public class CommentServiceTests {

    @Autowired
    ICommentService service;

    @Test
    void post() {
        try {
            PostCommentDTO postCommentDTO = new PostCommentDTO()
                    .setAnswerId(10000)
                    .setContent("测试评论10号答案");
            Integer userId = 1;
            String userNickName = "超人";
            service.post(postCommentDTO, userId, userNickName);
            log.debug("发表评论成功!");
        } catch (ServiceException e) {
            log.debug("发表评论失败,错误类型:{}", e.getClass().getName());
            log.debug("原因:{}", e.getMessage());
        }
    }

}

十一、发表评论--控制器层


先在R.State中声明与AnswerNotFoundException对应的状态码,并在GlobalExceptionHandler中处理该异常。

CommentController中添加处理请求的方法:

@RestController
@RequestMapping("/v1/comments")
public class CommentController {

    @Autowired
    ICommentService commentService;

    // 【不可用】http://localhost:8081/v1/comments/post?answerId=1&content=hehe
    // http://localhost/api-question/v1/comments/post?answerId=1&content=hehe
    @RequestMapping("/post")
    public R<Void> post(@Valid PostCommentDTO postCommentDTO,
        BindingResult bindingResult,
        @AuthenticationPrincipal LoginUserInfo loginUserInfo) {
        // 参考“发表答案”
        if (bindingResult.hasErrors()) {
            String errorMessage = bindingResult.getFieldError().getDefaultMessage();
            throw new IllegalParameterException(errorMessage);
        }
        commentService.post(postCommentDTO, loginUserInfo.getId(), loginUserInfo.getNickname());
        return R.ok();
    }

}

完成后,重启项目,通过以上网关的URL测试。

十二、发表评论--前端页面


在页面中,每个“答案”的下方都有“添加评论”按钮,点击任何一个“添加评论”按钮,都会使得每个“答案”下面的评论区域展开或收起,是因为页面代码的设计中:

以上2处的名称是必须对应的,才可以实现展开和收起,同时,这个代码区域是循环生成的,所以,每个“答案”的这个区域都是同样的id值,才会导致全部同时展开或收起!为了解决这个问题,应该使得这2值是对应的,但是,每个“答案”对应的这2处的值都不相同!可以在以上值中添加“答案”的id值作为值的一部分:

则可以解决“发表评论”区域全部一起展开或收起的问题!

然后,为“发表评论”的表单绑定提交事件的响应,并为输入评论的控件绑定id以便于获取评论正文:

关于Vue中的函数:

完成后,重启项目,测试发表评论,在数据库中直接查询是否已经插入新的评论数据。

十三、显示评论列表


关于显示评论列表,不可以“根据每一个答案查询对应的评论列表”,否则,如果某个“问题”有10个答案,则需要客户端向服务器发送10次请求,分别获取每一个答案的评论列表!应该在查询“答案”列表的同时把对应的“评论”也都查询出来!需要执行的SQL语句大致是:

SELECT
    answer.id, answer.user_id, answer.user_nick_name,
    answer.content, answer.is_accepted, answer.gmt_create,
    comment.id AS commentId,
    comment.user_id AS commentUserId,
    comment.user_nick_name AS commentUserNickName,
    comment.content AS commentContent,
    comment.gmt_create AS commentGmtCreate
FROM
    answer
LEFT JOIN
    comment
ON
    answer.id = comment.answer_id
WHERE
    question_id=6
ORDER BY
    answer.gmt_create DESC,
    comment.gmt_create DESC

目前,并没有哪个类适合封装以上查询结果,需要改版AnswerListItemVO类,在原有的基础之上,使用1个List集合用于封装该“答案”中的若干个“评论”数据!所以,也需要创建一个VO类,用于表示此次查询的“评论”数据:

@Data
@Accessors(chain = true)
public class CommentListItemVO implements Serializable {

    private Integer id;
    private Integer userId;
    private String userNickName;
    private String content;
    private LocalDateTime gmtCreate;

}

并在AnswerListItemVO中补充该属性:

接下来,配置AnswerMapper.xml中的查询,将原有的查询改为:

<resultMap id="AnswerListItemMap" type="cn.tedu.straw.commons.vo.AnswerListItemVO">
    <id column="id" property="id" />
    <result column="content" property="content" />
    <result column="user_id" property="userId" />
    <result column="user_nick_name" property="userNickName" />
    <result column="is_accepted" property="isAccepted" />
    <result column="gmt_create" property="gmtCreate" />
    <!-- 查询结果中的1对多关系需要使用collection节点来配置 -->
    <!-- property属性:封装查询结果的类中的属性名 -->
    <!-- ofType属性:封装查询结果的类中的List集合中的元素类型 -->
    <collection property="comments"
                ofType="cn.tedu.straw.commons.vo.CommentListItemVO">
        <id column="commentId" property="id" />
        <result column="commentContent" property="content" />
        <result column="commentUserId" property="userId" />
        <result column="commentUserNickName" property="userNickName" />
        <result column="commentGmtCreate" property="gmtCreate" />
    </collection>
</resultMap>

<select id="findByQuestionId" resultMap="AnswerListItemMap">
    SELECT
        answer.id, answer.user_id, answer.user_nick_name,
        answer.content, answer.is_accepted, answer.gmt_create,
        comment.id AS commentId,
        comment.user_id AS commentUserId,
        comment.user_nick_name AS commentUserNickName,
        comment.content AS commentContent,
        comment.gmt_create AS commentGmtCreate
    FROM
        answer
    LEFT JOIN
        comment
    ON
        answer.id = comment.answer_id
    WHERE
        question_id=#{questionId}
    ORDER BY
        answer.gmt_create DESC,
        comment.gmt_create DESC
</select>

完成后,执行AnswerMapperTests中原有的测试,可以看到某“问题”的答案列表中还包含评论列表,甚至重启项目后,通过原有的例如 http://localhost/api-question/v1/answers?questionId=6 地址也可以直接看到新的结果!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值