目录
一、发表答案--持久层
运行代码生成器项目,输入表名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 地址也可以直接看到新的结果!