抖音极简版项目问题及解决方案 | 青训营笔记

246 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第五篇笔记。

前面几篇主要都是课程笔记,这篇记录一下项目进行过程中遇到的问题以及相应的解决方案,供自己和后面相同的项目参考。

相关链接

1 视频封面生成

问题

抖音app上传界面没有提供封面的上传,但feed流获取的视频包含预览图片的url,因此需要生成预览封面。

QQ截图20220615214504.png

解决方案

使用开源的ffmpeg工具截取视频的某一帧作为封面,最佳实现应该是截取具有代表性的一帧,这里就偷懒截取第一帧了。

实现

  1. 下载ffmpeg.exe程序,放在项目文件夹
  2. 使用go语言设置参数,并调用cmd命令运行ffmpeg.exe来截取视频的第一帧作为视频封面。
input := path + videoName
output := path + pictureName

cmdArguments := []string{"-i", input, "-t", "1", "-r", "1", "-q:v", "2", "-f", "image2", output}
cmd := exec.Command("ffmpeg", cmdArguments...)

var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
   //log.Fatal(err)
   return "", err
}

2 GORM查询出错

问题 有个功能是判断用户是否给视频点赞,实际项目中通过用户userId和视频videoId查询点赞表记录,来获取是否点赞。当用户未登录时也可以调用这个函数,当时的实现是用户未登录时传入userId为0,这样就解决了点赞状态的获取问题。理论上未登录用户肯定是没有任何给视频点赞的记录,但实际测试中所有视频均显示点赞。 问题分析 经过一步步测试,最终发现问题出在查询语句上。

result := db.Where(&Favorite{UserId: userId, VideoId: videoId}).Limit(1).Find(&favorite)

当用户userId为0时,GORM会默认忽略这个条件,因此实际执行的语句相当于

result := db.Where(&Favorite{VideoId: videoId}).Limit(1).Find(&favorite)

解决方案

  1. 使用带占位符的where语句
  2. 使用map条件传入参数
  3. 将未登录时的用户userId由0改为-1

3 Token加密

问题 多数服务都是通过传递token来获取,因此需要保证不同用户不同时间有不同的token且不能随意被篡改,并且token应该要做到无状态

解决方案 采用带时间戳的加密算法来生成token,传入的token仅通过解密就验证是否合法,并获取userId和expireTime来判断是否过期,而不必经过数据库查询。

实现

// aes加密并生成token
func GenerateToken(userId int, keyString string) (string, bool) {
   timeStamp := time.Now().Unix()
   data := []byte(fmt.Sprint(userId) + "." + fmt.Sprint(timeStamp))
   key := []byte(keyString)
   crypt, err := aesEncrypt(data, key)
   if err != nil {
      fmt.Println(err)
      return "", false
   }
   token := base64.StdEncoding.EncodeToString(crypt)
   return token, true
}

//验证和解密
func ParseToken(token string, keyString string) (string, bool) {
   key := []byte(keyString)
   data, err := base64.StdEncoding.DecodeString(token)
   if err != nil {
      fmt.Println(err)
      return err.Error(), false
   }
   plain, err := aesDecrypt(data, key)
   if err != nil {
      fmt.Println(err)
      return err.Error(), false
   }
   plainText := fmt.Sprintf("%s", plain)
   return plainText, strings.Contains(plainText, ".")
}

必要时也可以直接获取token中的时间戳,考虑到每次操作要更新token的过期时间,因此将token存在数据库了,每次查询并更新过期时间。