青训营抖音项目实现-3 | 青训营笔记

83 阅读9分钟

青训营抖音项目实现-3 | 青训营笔记

前面讲了数据库表,登录方面的内容。现在开始具体实现抖音项目的16个接口

青训营项目的接口定义有点问题,应该将除StatusCode: 0, StatusMsg: ""之外的所有数据放在data字段里。目前我只能定义很多个返回值。。。

1、用户相关三个接口

1.1、register

func UserRegister(c *gin.Context) {
    // 构建返回值
   successResponse := UserLoginResponse{
      Response: utils.Response{
         StatusCode: 0,
         StatusMsg:  "",
      },
      Token:  "",
      UserId: 0,
   }

   failureResponse := UserLoginResponse{
      Response: utils.Response{
         StatusCode: 1,
         StatusMsg:  "",
      },
      Token:  "",
      UserId: 0,
   }

   // 1、解析参数
   username := c.Query("username")
   password := c.Query("password")
   // 2、验证参数
   if !utils.VerifyParam(username, password) {
      failureResponse.StatusMsg = "用户名或密码长度大于32字符"
      c.JSON(409, failureResponse)
      return
   }
   // 3、生成盐值和加密后的密码
   salt := utils.Salt()
   cryptoPassword := utils.CryptUserPassword(password, salt)
   // 4、创建用户
   user := models.User{
      Model:         gorm.Model{},
      Name:          username,
      FollowCount:   0,
      FollowerCount: 0,
      Password:      cryptoPassword,
      Salt:          salt,
   }
   // 5、gorm创建用户
   // 5.1、事务开始
   tx := driver.Db.Debug().Begin()
   defer func() {
      if r := recover(); r != nil {
         tx.Rollback()
      }
   }()
   // 5.2、创建用户
   if err := tx.Model(user).Create(&user).Error; err != nil {
      tx.Rollback()
      failureResponse.StatusMsg = "创建用户失败" + err.Error()
      c.JSON(409, failureResponse)
      return
   }
   successResponse.UserId = user.ID
   // 5.2.1、用户登录
   auth := Auth{
      UserID:           user.ID,
      UserName:         username,
      FollowCount:      0,
      FollowerCount:    0,
      RegisteredClaims: jwt.RegisteredClaims{},
   }
   token, err := MakeToken(&auth)
   if err != nil {
      tx.Rollback()
      failureResponse.StatusMsg = "创建token失败" + err.Error()
      c.JSON(409, failureResponse)
      return
   }
   successResponse.Token = token
   // 5.3、事务提交
   if err := tx.Commit().Error; err != nil {
      tx.Rollback()
      failureResponse.StatusMsg = "创建用户失败" + err.Error()
      c.JSON(409, failureResponse)
      return
   }
   // 结果返回
   c.JSON(http.StatusOK, successResponse)

}

所有的写入数据库都使用事务进行处理。

该接口比较简单,但是由于是初步实现导致有很多下面的重复代码,之后会对项目进行重构,形成model-controller-service三层架构

      tx.Rollback()
      failureResponse.StatusMsg = "创建用户失败" + err.Error()
      c.JSON(409, failureResponse)
      return

业务部分:

  1. 创建用户
  2. 获取随机盐值
  3. 加密密码和盐值
  4. 写入数据库
  5. 构造Auth
  6. 构造jwt

所有的接口基本都是以下四个步骤组成

  1. 获取参数
  2. 验证参数
  3. 具体业务处理
  4. 装配返回值并返回

后面的接口我只展示具体业务处理部分

1.2、login

// 3、解析用户名和密码是否与数据库一致
// 3.1、根据用户名查询user信息,用户名必须唯一,可以添加unique索引。仅仅是查询,无需使用事务
var user models.User
if err := driver.Db.Debug().Model(user).Where(" name = ?", username).Find(&user).Error; err != nil {
   failureResponse.StatusMsg = "查询数据库失败" + err.Error()
   c.JSON(409, failureResponse)
   return
}
// 4、获取到盐值,加密后判断是否一致
if !utils.VerifyUserPassword(user.Salt, password, user.Password) {
   failureResponse.StatusMsg = "验证失败"
   c.JSON(409, failureResponse)
   return
}
// 5、验证成功,创建token
auth := Auth{
   UserID:           user.ID,
   UserName:         user.Name,
   FollowCount:      user.FollowCount,
   FollowerCount:    user.FollowerCount,
   RegisteredClaims: jwt.RegisteredClaims{},
}
token, err := MakeToken(&auth)
if err != nil {
   failureResponse.StatusMsg = "创建token失败" + err.Error()
   c.JSON(409, failureResponse)
   return
}

登录接口更简单,获取用户名密码,查询数据库获取加密后的密码和盐值,加密后进行对比。一样则构造Auth和token返回

1.3、info

// 2、查询数据库获取用户信息
var user models.User
if err := driver.Db.Debug().Model(user).Where(" id = ?", uint(id)).Find(&user).Error; err != nil {
   failureResponse.StatusMsg = "查询数据库失败" + err.Error()
   c.JSON(409, failureResponse)
   return
}
// 2.1、查询关注表,查看登录用户是否关注了user_id对应的用户
isFollow := false
var relation models.Relation
if err := driver.Db.Debug().Model(relation).Where(" user_id = ? ", auth.UserID).Where("target_id = ?", user.ID).Where("exist=1").Where("type=1 or type=2").Find(&relation).Error; err != nil {
   failureResponse.StatusMsg = "查询数据库失败" + err.Error()
   c.JSON(409, failureResponse)
   return

}
if relation.ID != 0 {
   isFollow = true
}
// 3、填充返回的用户信息

用户信息稍微复杂一点,因为接口中存在一个isFollow字段,因此我们需要查询user_id代表的用户和auth认证的用户,然后查询关注表relation查看auth是否关注user_id

下面仔细分析一下查询关系表的语句

driver.Db
.Debug()
.Model(relation)
.Where(" user_id = ? ", auth.UserID)
.Where("target_id = ?", user.ID)
.Where("exist=1").Where("")
.Find(&relation).Error

gorm多个where是and的关系,model指定是哪张表,如果实现了Table函数也可以使用Table("表名"),Find查找并存储到给定的参数中,Error是获取语句执行的错误,没有错误返回nil。

因此该语句形成的sql为

select * from relation where user_id= auth.UserID and target_id=user.ID and exist=1 and (type=1 or type=2)

2、关系相关操作

2.1、action

action是我认为非常复杂的一个接口。

tx := driver.Db.Debug().Begin()
defer func() {
   if r := recover(); r != nil {
      tx.Rollback()
   }
}()
// 3、查询数据库获取两个用户信息,使用for update加锁(用户一般都存在)
var user, targetUser models.User
if err := driver.Db.Debug().Model(user).Set("gorm:query_option", "FOR UPDATE").Where("id = ?", auth.UserID).Find(&user).Error; err != nil {
   failureResponse.StatusMsg = "查询数据库错误" + err.Error()
   c.JSON(409, failureResponse)
   return
}
//TODO
// 用户不存在的情况,布隆过滤器?
if err := driver.Db.Debug().Model(targetUser).Set("gorm:query_option", "FOR UPDATE").Where("id = ?", id).Find(&targetUser).Error; err != nil {
   failureResponse.StatusMsg = "查询数据库错误" + err.Error()
   c.JSON(409, failureResponse)
   return
}
// 用户不存在
if targetUser.ID == 0 {
   failureResponse.StatusMsg = "用户不存在"
   c.JSON(409, failureResponse)
   return
}
// 4.3查询两个关系数据
var relation1, relation2 models.Relation
if err := driver.Db.
   Debug().
   Model(relation1).
   Where("user_id = ?", user.ID).
   Where("target_id = ?", targetUser.ID).
   Find(&relation1).Error; err != nil {
   failureResponse.StatusMsg = "查询数据库出错" + err.Error()
   c.JSON(409, failureResponse)
   return
}
if err := driver.Db.
   Debug().
   Model(relation2).
   Where("user_id = ?", targetUser.ID).
   Where("target_id = ?", user.ID).
   Find(&relation2).Error; err != nil {
   failureResponse.StatusMsg = "查询数据库出错" + err.Error()
   c.JSON(409, failureResponse)
   return
}
// 由于不能确定这两个关系同时存在,因此不要使用for update加锁(使用for update时确保索引存在。不存在会锁住表)
// for update在数据存在时加的是行级锁,不存在加的是间隙锁。之后进行insert时容易形成死锁
if action == 1 {
   if relation1.ID == 0 {
      relation1.UserID = user.ID
      relation1.TargetID = targetUser.ID
      relation1.Exist = true

      //数据不存在,第一次关注,创建数据
      if relation2.ID != 0 && relation2.Exist && relation2.Type == 1 {
         // target关注了user,修改两个关系数据为Type=2
         relation2.Type = 2
         relation1.Type = 2
         if err := tx.Model(relation2).Updates(&relation2).Error; err != nil {
            failureResponse.StatusMsg = "更新数据库失败" + err.Error()
            c.JSON(409, failureResponse)
            tx.Rollback()
            return
         }

      } else {
         // 只需要增加一条Type为1的数据
         relation1.Type = 1

      }

      if err := tx.Model(relation1).Create(&relation1).Error; err != nil {
         failureResponse.StatusMsg = "创建关系失败" + err.Error()
         c.JSON(409, failureResponse)
         tx.Rollback()
         return
      }
   } else if relation1.Exist {
      // 关注操作且数据库显示已关注,错误
      failureResponse.StatusMsg = "关注信息已存在,您已关注无需再次关注"
      c.JSON(409, failureResponse)
      tx.Rollback()
      return

   } else {
      relation1.Exist = true
      // user关注过target,但是取消了,因此存在一条exist=false的数据,修改exist为true
      if relation2.ID != 0 && relation2.Exist && relation2.Type == 1 {
         // target关注了user,修改两个关系数据为Type=2
         relation2.Type = 2
         relation1.Type = 2
         if err := tx.Model(relation2).Updates(&relation2).Error; err != nil {
            failureResponse.StatusMsg = "更新数据库失败" + err.Error()
            c.JSON(409, failureResponse)
            tx.Rollback()
            return
         }
      } else {
         // 修改数据为Type=1
         relation1.Type = 1
      }

      if err := tx.Model(relation1).Updates(&relation1).Error; err != nil {
         failureResponse.StatusMsg = "更新数据库失败" + err.Error()
         c.JSON(409, failureResponse)
         tx.Rollback()
         return
      }
   }
   user.FollowCount += 1
   targetUser.FollowerCount += 1
} else if action == 2 {
   // 取消关注,数据不存在直接报错
   if relation1.ID == 0 || !relation1.Exist {
      failureResponse.StatusMsg = "关注数据不存在,无需取消关注"
      c.JSON(409, failureResponse)
      tx.Rollback()
      return
   } else {
      //数据存在
      if relation1.Type == 1 {
         // Type为1,只需要将Exist改为false
         relation1.Exist = false
         if err := tx.Model(relation1).Save(&relation1).Error; err != nil {
            failureResponse.StatusMsg = "更新数据库失败" + err.Error()
            c.JSON(409, failureResponse)
            tx.Rollback()
            return
         }
      } else {
         // Type为2,修改relation2的Type为1
         relation1.Exist = false
         relation1.Type = 1
         if err := tx.Model(relation1).Save(&relation1).Error; err != nil {
            failureResponse.StatusMsg = "更新数据库失败" + err.Error()
            c.JSON(409, failureResponse)
            tx.Rollback()
            return
         }
         relation2.Type = 1
         if err := tx.Model(relation2).Updates(&relation2).Error; err != nil {
            failureResponse.StatusMsg = "更新数据库失败" + err.Error()
            c.JSON(409, failureResponse)
            tx.Rollback()
            return
         }
      }

   }
   user.FollowCount -= 1
   targetUser.FollowerCount -= 1
}
//注意使用gorm有可能修改到零值的需要使用Save而不能使用updates
// 5、修改用户的关注数和粉丝数
if err := tx.Model(user).Save(&user).Error; err != nil {
   failureResponse.StatusMsg = "更新数据库失败" + err.Error()
   c.JSON(409, failureResponse)
   tx.Rollback()
   return
}
if err := tx.Model(targetUser).Save(&targetUser).Error; err != nil {
   failureResponse.StatusMsg = "更新数据库失败" + err.Error()
   c.JSON(409, failureResponse)
   tx.Rollback()
   return
}
if err := tx.Commit().Error; err != nil {
   failureResponse.StatusMsg = "更新数据库失败" + err.Error()
   c.JSON(409, failureResponse)
   tx.Rollback()
   return
}

首先查找到两个用户auth和点赞的对象target,for update,因为后面会更新。for update必须保证是在索引上使用,不然会从行级锁直接变成表锁!!!最好保证该记录存在,如果不存在会从行级锁变成间隙锁(具体可google)。

  1. 关系表的关系分析:

    1. user关注targetUser,则表中存在一条 userID=user.ID,targetID=targetUser.ID,Type=1的数据

    2. targetUser关注user,则表中存在一条 userID=targetUser.ID,targetID=user.ID,Type=1的数据

    3. 两个人互相关注,则存在两条数据,Type都等于2

然后查询关系表,获取两个关系

根据action的类型分别处理

点赞

情况分析:

  1. 关系1不存在,关系2存在且exist=1,对应着target关注了user而user没有关注target。那么需要修改关系2的Type为2,增加关系1且Type=2
  2. 关系1存在但exist=0,关系2存在且exist=1,对应着target关注了user而user没有关注target。那么需要修改关系2的Type为2,修改关系1的Type=2
  3. 关系1存在且exist=1,已关注直接返回
  4. 关系1不存在,关系2不存在或关系2的exist=0,对应着target没有关注了user且user没有关注target。那么需要增加关系1且Type=1
  5. 关系1存在但exist=0,关系2不存在或关系2的exist=0,对应着target没有关注了user且user没有关注target。那么需要修改关系1的Type=1

取消点赞

情况分析:

  1. 关系1不存在或者关系1的exist=0,直接返回错误
  2. 关系1存在且exist=1,关系2存在且exist=1,对应着target关注了user且user关注了target。那么需要修改关系1的Type为1,exist=0,修改关系2的Type=1
  3. 关系1存在且exist=1,关系2不存在或者存在exist=0,那么需要修改关系1的Type为1,exist=0

2.2 获取关注列表

userList := make([]User, len(relations))

for i := 0; i < len(relations); i++ {
   var user models.User
   var relation models.Relation
   // 此处为TargetID
   userList[i].ID = relations[i].TargetID
   if err := driver.Db.Debug().Model(user).Where("id = ?", userList[i].ID).Find(&user).Error; err != nil {
      failureResponse.StatusMsg = "查询数据库失败" + err.Error()
      c.JSON(http.StatusOK, failureResponse)
      return
   }
   userList[i].Name = user.Name
   userList[i].FollowerCount = user.FollowerCount
   userList[i].FollowCount = user.FollowCount
   userList[i].IsFollow = false
   if err := driver.Db.Debug().Model(relation).Where("user_id = ?", auth.UserID).Where("target_id = ?", relations[i].TargetID).Where("exist=1").Where("type=1 or type=2").Find(&relation).Error; err != nil {
      failureResponse.StatusMsg = "查询数据库失败" + err.Error()
      c.JSON(http.StatusOK, failureResponse)
      return
   }
   // 再次判断是否存在
   if relation.ID != 0 && relation.Exist {
      userList[i].IsFollow = true
   }

}

极其简单,不介绍

2.3 获取粉丝列表

userList := make([]User, len(relations))

for i := 0; i < len(relations); i++ {
   var user models.User
   var relation models.Relation
   // 此处为UserID
   userList[i].ID = relations[i].UserID
   if err := driver.Db.Debug().Model(user).Where("id = ?", userList[i].ID).Find(&user).Error; err != nil {
      failureResponse.StatusMsg = "查询数据库失败" + err.Error()
      c.JSON(http.StatusOK, failureResponse)
      return
   }
   userList[i].Name = user.Name
   userList[i].FollowerCount = user.FollowerCount
   userList[i].FollowCount = user.FollowCount
   userList[i].IsFollow = false
   if err := driver.Db.Debug().Model(relation).Where("user_id = ?", auth.UserID).Where("target_id = ?", relations[i].UserID).Where("exist=1").Where("type=1 or type=2").Find(&relation).Error; err != nil {
      failureResponse.StatusMsg = "查询数据库失败" + err.Error()
      c.JSON(http.StatusOK, failureResponse)
      return
   }
   // 再次判断是否存在
   if relation.ID != 0 && relation.Exist {
      userList[i].IsFollow = true
   }

}

2.4 获取好友列表

userList := make([]User, len(relations))
for i := 0; i < len(relations); i++ {
   var user models.User
   var relation models.Relation
   userList[i].ID = relations[i].TargetID
   if err := driver.Db.Debug().Model(user).Where("id = ?", userList[i].ID).Find(&user).Error; err != nil {
      failureResponse.StatusMsg = "查询数据库失败" + err.Error()
      c.JSON(http.StatusOK, failureResponse)
      return
   }
   userList[i].Name = user.Name
   userList[i].FollowerCount = user.FollowerCount
   userList[i].FollowCount = user.FollowCount
   userList[i].IsFollow = false
   if err := driver.Db.Debug().Model(relation).Where("user_id = ?", auth.UserID).Where("target_id = ?", relations[i].TargetID).Where("exist=1").Where("type=1 or type=2").Find(&relation).Error; err != nil {
      failureResponse.StatusMsg = "查询数据库失败" + err.Error()
      c.JSON(http.StatusOK, failureResponse)
      return
   }
   // 再次判断是否存在
   if relation.ID != 0 && relation.Exist {
      userList[i].IsFollow = true
   }

}

3、发布

3.1、发布视频接口

auth := Auth{}.GetAuth(c)

successResponse := utils.Response{
   StatusCode: 0,
   StatusMsg:  "",
}
failureResponse := utils.Response{
   StatusCode: 1,
   StatusMsg:  "",
}
// 1、解析参数
form, err := c.MultipartForm()
if err != nil {
   failureResponse.StatusMsg = "解析form表单错误"
   c.JSON(409, failureResponse)
   return
}
title := c.PostForm("title")
// 2、验证参数
data := form.File["data"]
if len(data) != 1 {
   failureResponse.StatusMsg = "视频数量只能为1"
   c.JSON(409, failureResponse)
   return
}
video, err := data[0].Open()
if err != nil {
   failureResponse.StatusMsg = "读取视频时错误"
   c.JSON(409, failureResponse)
   return
}

uid := uuid.New().String()
videoURL := storage.OSS.Put(uid+data[0].Filename, video)
//coverURL := storage.OSS.Put(uid+".jpeg", snapshot)
coverURL := videoURL + "?x-oss-process=video/snapshot,t_7000,f_jpg,w_800,h_600,m_fast"
videoModel := models.Video{
   Model:         gorm.Model{},
   AuthorID:      auth.UserID,
   Title:         title,
   CommentCount:  0,
   FavoriteCount: 0,
   PlayURL:       videoURL,
   CoverURL:      coverURL,
}
tx := driver.Db.Debug().Begin()
defer func() {
   if r := recover(); r != nil {
      tx.Rollback()
   }
}()
if err := tx.Model(videoModel).Create(&videoModel).Error; err != nil {
   failureResponse.StatusMsg = "创建视频错误" + err.Error()
   c.JSON(409, failureResponse)
   return
}
if err := tx.Commit().Error; err != nil {
   tx.Rollback()
   failureResponse.StatusMsg = "创建视频错误" + err.Error()
   c.JSON(409, failureResponse)
   return
}
// 4、装配返回值
c.JSON(http.StatusOK, successResponse)
return

发布接口的参数是form表单传入,传入的文件使用c.MultipartForm()获取。

该接口主要分为以下几步:

  1. 获取文件
  2. 上传到OSS获取URL
  3. 创建Video数据
  4. 写入数据库