青训营抖音项目实现-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
业务部分:
- 创建用户
- 获取随机盐值
- 加密密码和盐值
- 写入数据库
- 构造Auth
- 构造jwt
所有的接口基本都是以下四个步骤组成
- 获取参数
- 验证参数
- 具体业务处理
- 装配返回值并返回
后面的接口我只展示具体业务处理部分
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)。
-
关系表的关系分析:
-
user关注targetUser,则表中存在一条 userID=user.ID,targetID=targetUser.ID,Type=1的数据
-
targetUser关注user,则表中存在一条 userID=targetUser.ID,targetID=user.ID,Type=1的数据
-
两个人互相关注,则存在两条数据,Type都等于2
-
然后查询关系表,获取两个关系
根据action的类型分别处理
点赞
情况分析:
- 关系1不存在,关系2存在且exist=1,对应着target关注了user而user没有关注target。那么需要修改关系2的Type为2,增加关系1且Type=2
- 关系1存在但exist=0,关系2存在且exist=1,对应着target关注了user而user没有关注target。那么需要修改关系2的Type为2,修改关系1的Type=2
- 关系1存在且exist=1,已关注直接返回
- 关系1不存在,关系2不存在或关系2的exist=0,对应着target没有关注了user且user没有关注target。那么需要增加关系1且Type=1
- 关系1存在但exist=0,关系2不存在或关系2的exist=0,对应着target没有关注了user且user没有关注target。那么需要修改关系1的Type=1
取消点赞
情况分析:
- 关系1不存在或者关系1的exist=0,直接返回错误
- 关系1存在且exist=1,关系2存在且exist=1,对应着target关注了user且user关注了target。那么需要修改关系1的Type为1,exist=0,修改关系2的Type=1
- 关系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()获取。
该接口主要分为以下几步:
- 获取文件
- 上传到OSS获取URL
- 创建Video数据
- 写入数据库