78. 删除评论-持久层
(a)规划需要执行的SQL语句
需要执行的SQL语句大致是:
代码语言:javascript复制delete from comment where id=?
通常,在处理增、删、改之前,还存在相关的检查,特别是删、改的操作之前,都应该检查被操作的数据是否存在、是否具有权限对该数据进行操作,及可能存在的其它业务规则。
关于数据是否存在,可以通过查询来得到答案:
代码语言:javascript复制select * from comment where id=?
关于操作数据的权限,可以暂将业务规则设计为“评论的发表者,或任何老师,都可以删除评论”。关于“是否是评论的发表者”,通过以上查询结果中的user_id
结合登录状态中的user_id
即可进行判断,关于“是否是老师”直接通过登录状态中的type
属性即可判断。
通常,条件非常简单的数据操作都可以直接由MyBatis Plus中的功能直接完成,可以不必专门的开发相关功能!
(b)接口的抽象方法
无
(c)配置SQL语句
无
(d)单元测试
代码语言:javascript复制@SpringBootTest
@Slf4j
public class CommentsMapperTests {
@Autowired
CommentMapper mapper;
@Test
void deleteById() {
Integer id = 4;
int rows = mapper.deleteById(id);
log.debug("delete ok, rows={}", rows);
}
@Test
void selectById() {
Integer id = 4;
Comment comment = mapper.selectById(id);
log.debug("query ok, result={}", comment);
}
}
79. 删除评论-业务层
(a)创建异常
创建“评论数据不存在”的异常:
代码语言:javascript复制public class CommentNotFoundException extends ServiceException {}
创建“权限不足”的异常:
代码语言:javascript复制public class PermissionDeniedException extends ServiceException {}
创建“删除数据失败”的异常:
代码语言:javascript复制public class DeleteException extends ServiceException {}
(b)接口的抽象方法
代码语言:javascript复制/**
* 删除评论
*
* @param commentId 评论数据的id
* @param userId 当前登录的用户的id
* @param userType 当前登录的用户的类型
* @return 成功删除的评论数据
*/
Comment delete(Integer commentId, Integer userId, Integer userType);
(c)实现业务
关于业务的实现步骤:
代码语言:javascript复制public Comment delete(Integer commentId, Integer userId, Integer userType) {
// 根据参数commentId调用mapper.selectById()查询被删除的“评论”的信息
// 判断查询结果是否为null
// 是:该“评论”不存在,抛出CommentNotFoundException异常
// 基于查询结果中的userId,结合参数userId,判断查询结果数据是否是当前登录用户的,
// 或基于参数userType,判断当前登录的用户的身份是“老师”,
// 如果这2个条件都不符合,则不允许删除,抛出PermissionDeniedException
// 根据参数commentId调用mapper.deleteById()执行删除,并获取返回的受影响行数
// 判断返回值是否不为1
// 是:抛出DeleteException
// 返回查询结果
}
具体的实现业务:
代码语言:javascript复制@Override
public Comment delete(Integer commentId, Integer userId, Integer userType) {
// 根据参数commentId调用mapper.selectById()查询被删除的“评论”的信息
Comment comment = commentMapper.selectById(commentId);
// 判断查询结果是否为null
if (comment == null) {
// 是:该“评论”不存在,抛出CommentNotFoundException异常
throw new CommentNotFoundException("删除评论失败!尝试访问的评论数据不存在!");
}
// 基于查询结果中的userId,结合参数userId,判断查询结果数据是否是当前登录用户的,
// 或基于参数userType,判断当前登录的用户的身份是“老师”,
// 注意:关于userId的判断,必须使用equals()方法
// 使用 == 或 != 来判断Integer类型的数据,前提必须是数值范围一定在 -128至127 之间
// 例如:Integer i1 = 128; Integer i2 = 128;
// 使用 == 来判断以上 i1 和 i2 时,结果将是false
if (!comment.getUserId().equals(userId) && userType != User.TYPE_TEACHER) {
// 如果这2个条件都不符合,则不允许删除,抛出PermissionDeniedException
throw new PermissionDeniedException("删除评论失败!仅发布者和老师可以删除此条评论!");
}
// 根据参数commentId调用mapper.deleteById()执行删除,并获取返回的受影响行数
int rows = commentMapper.deleteById(commentId);
// 判断返回值是否不为1
if (rows != 1) {
// 是:抛出DeleteException
throw new DeleteException("删除评论失败!删除时出现未知错误,请联系系统管理员!");
}
// 返回查询结果
return comment;
}
(d)单元测试
代码语言:javascript复制@Test
void delete() {
try {
Integer commentId = 10;
Integer userId = 5;
Integer userType = User.TYPE_TEACHER;
Comment comment = service.delete(commentId, userId, userType);
log.debug("OK, comment >>> {}", comment);
} catch (ServiceException e) {
log.debug("【删除评论失败】");
log.debug("错误类型:{}", e.getClass().getName());
log.debug("错误原因:{}", e.getMessage());
}
}
80. 删除评论-控制器层
(a)处理异常
需要在R.State
中添加各异常对应的状态码,然后在GloableExceptionHandler
中处理业务层创建的3种异常。
(b)设计请求
请求路径:/api/v1/comments/{commentId}/delete
请求参数:@PathVariable("commentId") Integer commentId
, @AuthenticationPriciple UserInfo userInfo
请求类型:POST
响应结果:R<Comment>
(c)处理请求
代码语言:javascript复制// http://localhost:8080/api/v1/comments/11/delete
@RequestMapping("/{commentId}/delete")
public R<Comment> delete(@PathVariable("commentId") Integer commentId,
@AuthenticationPrincipal UserInfo userInfo) {
Comment comment = commentService.delete(commentId, userInfo.getId(), userInfo.getType());
return R.ok(comment);
}
(d)测试
http://localhost:8080/api/v1/comments/14/delete
81. 删除评论-前端页面
在Vue中,如果需要遍历某个数组,并且,在遍历时需要获取每个数组元素的下标,在遍历时,可以:
代码语言:javascript复制v-for="(comment, index) in comments"
以上代码中的comment
就是被遍历到的数组元素的数据,而index
就是数组元素的下标,在Vue 2.x中规定,在遍历时,可以在in
的左侧使用括号框住数组元素名称和数组下标,在括号中的最后一个名称即表示数组下标,名称是可以自定义的!
使用以上代码后,在被遍历的代码区域内,就可以通过index
表示下标!
所以,关于遍历整个评论列表:
在“删除评论”的<a>
标签中配置:
然后,在answers.js
中定义新的函数:
完成以上内容后,即可测试页面的“删除”的点击效果,如果无误,则在以上函数中继续补充后续发请求并处理结果的代码。
完整的代码如下:
82. 修改评论-持久层
(a)规划需要执行的SQL语句
修改评论需要执行的SQL语句大致是:
代码语言:javascript复制update comment set content=? where id=?
以上功能可以通过MyBatis Plus的updateById()
方法直接实现。
另外,在执行修改之前,也应该对被修改的数据执行检查,检查逻辑与“删除”时可以相同。
(b)接口的抽象方法
无
(c)配置SQL语句
无
(d)单元测试
代码语言:javascript复制@Test
void updateById() {
Comment comment = new Comment();
comment.setId(15);
comment.setContent("项目的功能就快开发完了!!!");
int rows = mapper.updateById(comment);
log.debug("update over. rows={}", rows);
}
83. 修改评论-业务层
(a)创建异常
需要创建“修改数据失败”的异常:
代码语言:javascript复制public class UpdateException extends ServiceException { }
(b)接口的抽象方法
代码语言:javascript复制/**
* 修改评论
*
* @param commentId 评论数据的id
* @param content 评论的正文
* @param userId 当前登录的用户的id
* @param userType 当前登录的用户的类型
* @return 成功修改的评论数据
*/
Comment update(Integer commentId, String content, Integer userId, Integer userType);
(c)实现业务
关于业务的实现步骤:
代码语言:javascript复制public Comment update(Integer commentId, String content, Integer userId, Integer type) {
// 根据参数commentId调用mapper.selectById()查询被编辑的“评论”的信息
// 判断查询结果(result)是否为null
// 是:该“评论”不存在,抛出CommentNotFoundException异常
// 基于查询结果中的userId,结合参数userId,判断查询结果数据是否是当前登录用户的,
// 或基于参数userType,判断当前登录的用户的身份是“老师”,
// 如果这2个条件都不符合,则不允许删除,抛出PermissionDeniedException
// 创建新的Comment comment对象
// 将commentId, content封装到comment中
// 根据comment调用mapper.updateById()执行修改,并获取返回的受影响行数
// 判断返回值是否不为1
// 是:抛出UpdateException
// 将content封装到result中
// 返回查询结果
}
具体实现:
代码语言:javascript复制@Override
public Comment update(Integer commentId, String content, Integer userId, Integer userType) {
// 根据参数commentId调用mapper.selectById()查询被修改的“评论”的信息
Comment result = commentMapper.selectById(commentId);
// 判断查询结果是否为null
if (result == null) {
// 是:该“评论”不存在,抛出CommentNotFoundException异常
throw new CommentNotFoundException("修改评论失败!尝试访问的评论数据不存在!");
}
// 基于查询结果中的userId,结合参数userId,判断查询结果数据是否是当前登录用户的,
// 或基于参数userType,判断当前登录的用户的身份是“老师”,
if (!result.getUserId().equals(userId) && userType != User.TYPE_TEACHER) {
// 如果这2个条件都不符合,则不允许修改,抛出PermissionDeniedException
throw new PermissionDeniedException("修改评论失败!仅发布者和老师可以修改此条评论!");
}
// 创建新的Comment comment对象
Comment comment = new Comment();
// 将commentId, content封装到comment中
comment.setId(commentId);
comment.setContent(content);
// 根据comment调用mapper.updateById()执行修改,并获取返回的受影响行数
int rows = commentMapper.updateById(comment);
// 判断返回值是否不为1
if (rows != 1) {
// 是:抛出UpdateException
throw new UpdateException("修改评论失败!服务器忙,请稍后再次尝试!");
}
// 将content封装到result中
result.setContent(content);
// 返回查询结果
return result;
}
(d)单元测试
代码语言:javascript复制@Test
void update() {
try {
Integer commentId = 12;
String content = "New Content!!!";
Integer userId = 12;
Integer userType = User.TYPE_STUDENT;
Comment comment = service.update(commentId, content, userId, userType);
log.debug("OK, comment >>> {}", comment);
} catch (ServiceException e) {
log.debug("【update评论失败】");
log.debug("错误类型:{}", e.getClass().getName());
log.debug("错误原因:{}", e.getMessage());
}
}
80. 修改评论-控制器层
(a)处理异常
需要处理UpdateException
(b)设计请求
请求路径:/api/v1/comments/{commentId}/update
请求参数:@PathVariable("commentId") Integer commentId
, String content
, @AuthenticationPriciple UserInfo userInfo
请求类型:POST
响应结果:R<Comment>
(c)处理请求
代码语言:javascript复制// http://localhost:8080/api/v1/comments/11/update?content=Hello,Kafka!!!
@RequestMapping("/{commentId}/update")
public R<Comment> update(@PathVariable("commentId") Integer commentId,
String content,
@AuthenticationPrincipal UserInfo userInfo) {
Comment comment = commentService.update(commentId, content,
userInfo.getId(), userInfo.getType());
return R.ok(comment);
}
(d)测试
http://localhost:8080/api/v1/comments/10/update?content=NewContent
81. 修改评论-前端页面
在评论列表中,每个评论都有一个专属的表单用于修改评论,默认全部是收起的,当点击“编辑”时,会将其展开,再次点击,会收起!
由于评论列表的每一项都是遍历生成的,所以,这些“编辑”链接的目标及各表单所在匹域的ID全部是相同的,则会导致点击任何一个“编辑”会展开所有表单,再次点击会收起所有表单!必须先调各“编辑”链接的目录和表单所在匹域的ID。
关于表单所在区域的调整:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TBqL1C8p-1596299955578)(image-20200731152343997.png)]
然后调整“编辑”链接到的目标位置:
至此,点击页面上的“编辑”后,修改评论的表单的展开和收起应该是正常的!
接下来,在表单控件中设置默认值,使得展开时即显示原来的评论正文:
然后,为文本域控件分配ID,便于后续获取文本域中填写的评论正文:
最后,还需要为表单绑定提交事件的响应函数:
在answers.js
中,添加新的函数,先测试使用:
在已经获取到commentId
和content
的基础上,添加$.ajax()
处理即可:
如果修改成功后,需要将表单收起,可以在发表成功后添加(因为此前已经为表单区域绑定了ID):
代码语言:javascript复制$('#updateComment' commentId).collapse('hide');
82. 关于“采纳答案”
建议事先在Question
类中添加静态成员内部接口:
public static interface Status {
String[] TEXTS = ["未回复", "待解决", "已解决"];
int UNRESOLVED = 0;
int TO_BE_SOLVED = 1;
int RESOLVED = 2;
}
在Answer
类中也作同样的处理:
public static interface Status {
String[] TEXTS = {"未采纳", "已采纳"};
int TO_BE_ACCEPT = 0;
int ACCEPTED = 1;
}
“采纳答案”时,需要修改(UPDATE)2张数据表的值:
- 将
question
表的status
字段值更新为Question.Status.RESOLVED
; - 将
answer
表的status_of_accept
字段值更新为Answer.Status.ACCEPTED
。
在业务层,由于需要一次性更新2张表,需要使用@Transactional
。
如果开发简单版,则只更新以上2张表中的这2个字段值即可!
如果开发较难的版本,可以添加规则“每个问题只能采纳1个答案,并且,一旦采纳,所有人将不允许添加答案或评论,也不允许对现有的答案或评论进行编辑、删除”。
83. 架构–Kafka简介
当客户端向服务器端发送请求后,服务器端会使用多线程的方式来处理不同客户端的请求!但是,如果客户端数量非常多,并且每个客户端的请求在被处理时耗时较长,就会导致在服务器端存在大量的线程同时处于运行状态,都 在处理数据,显然内存中的数据量也会非常大!
其实,并不是所有的请求都是非常急迫的需要被处理的!对于部分请求来说,可能使用“同步”的方式使得这些请求“排队”被处理,反而可以减轻服务器的压力!
Kafka的最基本功能就是:发出消息,接收消息。当使用Kafka时,可以在控制器中收到客户端的请求时,直接调用Kafka来发出消息,后续,Kafka就会收到所发出的消息,然后进行处理,其间,发出到接收之间,可能是存在队列的!基于这样的机制,还可以通过Kafka实现一些“通知”、“推送”的效果!
在学习Kafka之前,应该大致了解一些相关的概念与术语:
Producer
:生产者:生产消息,并发出消息;Consumer
:消费者:接收消息,并处理消息;Topic
:主题:用于对消息进行分类;Record
:记录:被队列传输处理的消息记录;Broker
:中间商:用于实现数据存储的主机服务器,在集群中使用时,也负责复制消息。
在具体的表现方面,Kafka有点像是Tomcat,只需要将其服务开启即可,项目中的程序就可以向Kafka服务器发送消息,则Kafka服务器接收到消息后,可以对消息队列进行处理,后续,项目中的程序就按照队列中的顺序来处理消息。
与Kafka类似的异步队列的产品还有:RabbitMQ
、ZeroMQ
、RocketMQ
等。