项目之删除评论、修改评论及架构--Kafka简介(14)

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

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中,添加新的函数,先测试使用:

在已经获取到commentIdcontent的基础上,添加$.ajax()处理即可:

如果修改成功后,需要将表单收起,可以在发表成功后添加(因为此前已经为表单区域绑定了ID):

代码语言:javascript复制
$('#updateComment'   commentId).collapse('hide');

82. 关于“采纳答案”

建议事先在Question类中添加静态成员内部接口:

代码语言:javascript复制
public static interface Status {
    String[] TEXTS = ["未回复", "待解决", "已解决"];
    int UNRESOLVED = 0;
    int TO_BE_SOLVED = 1;
    int RESOLVED = 2;
}

Answer类中也作同样的处理:

代码语言:javascript复制
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类似的异步队列的产品还有:RabbitMQZeroMQRocketMQ等。

0 人点赞