青训营大项目互动接口评论部分(问题修改) | 青训营笔记

79 阅读5分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 14 天

增加评论检查

在控制层代码执行添加评论的业务操作之前,首先要对传入的评论进行检查。这部分还是很重要的,尽管gin框架提供了ShouldBind操作保证传入的请求参数的对应性和一些基本的限制,gorm框架提供了执行DML时的参数检查。但是,它们并没有很好地检查评论的字符串的合法性。一般评论区中的评论都需要有长度的限制,比如经过网上调查得到的结果是抖音的评论区长度限制是100,并且要保证评论的编码和给出的字符是合法的。这部分代码在两个框架上通过设置完成是很困难的(例如gin框架中给出的validate的len只能保证字符串的长度等于特定数值),因此将这部分代码挪到控制层去运行。

我定义了函数func validCommentContent(string)bool来实现字符串的长度和字符编码合法性判断。

// 检查发来的字符串是否符合要求
func validCommentContent(s string) bool {
	// 明显过长过短或是编码不正确
	if len(s) > COMMENT_MAX_LEN*CODE_POINT_MAX_BYTES ||
		len(s) == 0 ||
		!utf8.ValidString(s) {
		return false
	}
	// 转换成Unicode过长
	if cnt := utf8.RuneCountInString(s); cnt > COMMENT_MAX_LEN {
		return false
	}
	// 检查内容是否可打印,是否为全空格类
	blank := true
	for _, r := range s {
		if !unicode.IsPrint(r) {
			return false
		}
		if !unicode.IsSpace(r) {
			blank = false
		}
	}
	return !blank
}

上面的代码可以看出,我认为评论应该是以UTF-8的编码格式保存的编码,这里与MySQL的字符串和交互编码类型utf8mb4做对应。并且由于我们是使用MySQL的VARCHAR来存储评论的,因此我们的评论本身就不宜过长,再加上我对短视频类APP关于评论区长度的限制的调研,认为长度限制最大为100是比较合适的。由于UTF-8是变长编码,因此我首先对字符串可能的最大长度做一个判断,即变长编码最长的字节数表达一个字符的情况,要记得go语言中的内置函数len作用在string类型上返回的是字节数,因此这里是估算最大长度不能超过,当然空评论也不能算作合法的。接下来确定了最大长度没有超过后,我再检查整个字符串是否完全符合UTF-8编码,这里因为已经确定了字符串的最大长度,因此接下来所有的字符串遍历检查的执行时间上界是可以预测的并且不会过多地拖慢执行速度。如果确定是合法的UTF-8编码了,接下来就是统计总字符数,并且与评论最长限制做比较,其间也要检查字符串中不是所有的字符都是空白符号的空白评论。做完这些检查后如果字符串仍然能够满足合法性,认为给出的要发布的评论是合法的,可以进行下一步的发布评论业务操作。

视频合法性验证

上面的部分介绍了请求参数有一些需要在开始正式业务代码之前进行验证,上一节验证的是需要发布评论时评论内容的验证,这一节要验证的东西更加重要,甚至涉及到数据库数据的一致性,那就是视频是否已经存在。因为,如果一个视频不存在,但是你却可以对着一个不存在的视频发表评论甚至是读取评论,那是绝对不可能的,更何况这个视频的位置可能未来真的会出现视频,这不仅会导致读写到不存在的数据,甚至会导致视频表中的评论数不正确的问题,一些恶意用户就可以通过申请读取不存在的视频的评论或是在不存在的视频上发表或删除评论的方法对数据库进行攻击,不止影响数据的一致性和完整性甚至威胁整个服务的可用性。

上面已经阐述了对请求的视频ID进行合法性验证的重要性和不验证它可能带来的危害。好在的是项目前期,项目组长已经在设计数据库表时对评论中的视频ID和用户ID设置了外键,这样就保证了即使有非法的评论发布操作真的发送到了数据库也不会执行成功,一定程度上保护了数据库数据的一致性。但是我们知道,在阿里的开发手册中明确规定了不能在数据库中使用外键和与其相关的级联操作,因为这样会对高并发时的写入操作带来更多的开销,并且对数据库的分布式扩展性有影响因为分库分表还是会逃脱外键的约束。同时,虽然可以保证操作的执行失败,但是传输请求的网络开销不能忽视,尤其是在插入操作中还有评论字符串;不仅如此,对于评论列表操作,单从评论表是无法得知到底是视频不存在还是视频没有评论,查找的开销也不能忽视。因此还是需要对是所有评论相关的请求在请求执行之前先检查视频是否已经存在。

这里我使用直接先从数据库中的视频表中查询相应视频ID的行是否存在。如果不存在返回视频本身不存在,这点我是使用gorm的查找操作Take在找不到结果时返回的错误gorm.ErrRecordNotFound来完成的。

func videoExist(videoID uint) (bool, error) {
	_, err := models.QueryVideoCommentCount(videoID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}