抖声后端开发记录(2)| 青训营笔记

78 阅读4分钟

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

互动接口

小伙伴也写了很多。这里我写了favorite相关的部分。

点赞

app提供了点赞和撤销点赞两个接口。所以我们也需要实现这两个方法。从数据库开始:

表结构定义,采用双主键:

type Favorite struct {
	UserId    int64 `gorm:"primary_key;autoIncrement:false" json:"user_id,omitempty"`
	VideoId   int64 `gorm:"primary_key;autoIncrement:false" json:"video_id,omitempty"`
	CreatedAt int64 `json:"created_at,omitempty"`
	UpdatedAt int64 `json:"updated_at,omitempty"`
}

gorm的软删除比较坑,这里直接删了不用软删除。

新Favorite的建立部分,这里其实是一个factory了。

type FavouriteOption func(f *Favorite)

func SetUserID(id int64) FavouriteOption {
	return func(f *Favorite) {
		f.UserId = id
	}
}

func SetVideoID(id int64) FavouriteOption {
	return func(f *Favorite) {
		f.VideoId = id
	}
}

func NewFavourite(option ...FavouriteOption) *Favorite {
	f := &Favorite{
		UserId:  0,
		VideoId: 0,
	}
	for _, opt := range option {
		opt(f)
	}
	return f
}

DAO建立:

type FavoriteDao struct {
	db *gorm.DB
}

var (
	favoriteDao *FavoriteDao
	once        sync.Once
)

func NewFavouriteInstance() *FavoriteDao {
	once.Do(func() {
		favoriteDao = &FavoriteDao{
			db: db,
		}
	})
	return favoriteDao
}

看看需求,需要用到favorite表的地方有:点赞操作,取消点赞操作。获取喜欢列表的操作。

这边就可以提供三个接口,一个是Create对应点赞,Delete对应取消点赞,ListFavoirte对应喜欢列表操作。这里我们是使用userID作为list的条件。我还建了一个videoId的版本,但好像没用上:

func (dao *FavoriteDao) Create(f *Favorite) error {
	return dao.db.Save(f).Error
}

func (dao *FavoriteDao) Delete(f *Favorite) error {
	return dao.db.Delete(f).Error
}

func (dao *FavoriteDao) ListFavouriteByUserId(userId int64) (*[]Favorite, error) {
	var f []Favorite
	result := dao.db.Where("user_id = ?", userId).Find(&f)
	return &f, result.Error
}

func (dao *FavoriteDao) ListFavouriteByVideoId(videoId int64) (*[]Favorite, error) {
	var f []Favorite
	result := dao.db.Where("video_id = ?", videoId).Find(&f)
	return &f, result.Error
}

随后封装服务。我们的服务很简单,甚至你都可以不写服务之间用这几个接口。但这里存在一个问题,我们需要与其他表进行交互,这里要用到事务。

首先定义了两个常量,来表示哪个是删除哪个是增加:

var (
	ACTION_CREATE int = 1
	ACTION_DEL    int = -1
)

我们的favorite会在video表和user表上都做修改,也就是我们的增加或者删除操作都涉及到了三个表。最近才看到gorm好像可以之间使用引用实体,可以省去这些麻烦事了,但是我们的框架已经定好了,也就不改了。

func TransacFavorRecordUpdateFavorCount(f *Favorite, userid int64, videoid int64, action int) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		switch action {
		case ACTION_CREATE:
			if e := tx.Model(f).Save(f).Error; e != nil {
				return e
			}
		case ACTION_DEL:
			if e := tx.Model(f).Delete(f).Error; e != nil {
				return e
			}
		}
		if e := tx.Model(&User{Id: userid}).UpdateColumn("favorite_count", gorm.Expr("favorite_count + ?", action)).Error; e != nil {
			return e
		}
		if e := tx.Model(&Video{Id: videoid}).UpdateColumn("favorite_count", gorm.Expr("favorite_count + ?", action)).Error; e != nil {
			return e
		}
		return nil
	})
	return err
}

完成后开始封装服务:

func Do(f *repository.Favorite, userid int64, videoid int64) error {
	return repository.TransacFavorRecordUpdateFavorCount(f, userid, videoid, repository.ACTION_CREATE)
	// return repository.NewFavouriteInstance().Create(f)
}

func Undo(f *repository.Favorite, userid int64, videoid int64) error {
	return repository.TransacFavorRecordUpdateFavorCount(f, userid, videoid, repository.ACTION_DEL)
	// return repository.NewFavouriteInstance().Delete(f)
}

func ListByUserId(userId int64) (*[]repository.Favorite, error) {
	return repository.NewFavouriteInstance().ListFavouriteByUserId(userId)
}

func ListByVideoId(videoId int64) (*[]repository.Favorite, error) {
	return repository.NewFavouriteInstance().ListFavouriteByVideoId(videoId)
}

服务完成后,开始重构handle的处理流程。

对于点击操作:

func FavoriteAction(c *gin.Context) {
	var err error

	id := c.GetInt64("UserID")

	// are there need a video legal verify?

	video, _ := strconv.ParseInt(c.Query("video_id"), 10, 64)

	favour := repository.Favorite{UserId: id, VideoId: video}

	actioType := c.Query("action_type")

	switch actioType {
	case "1":
		err = service_favor.Do(&favour, id, video)
	case "2":
		err = service_favor.Undo(&favour, id, video)
	}

	if err != nil {
		log.Printf("[WARN] Favourite action on User[id: %d] faild, ERR: %s", id, err)
		c.JSON(http.StatusOK, Response{StatusCode: 2, StatusMsg: "Action faild."})
	}
	c.JSON(http.StatusOK, Response{StatusCode: 0, StatusMsg: "Success"})
}

这边其实已经用上jwt中间件了,所以不需要再做用户的校验了(所以这里你就看看剩下的逻辑吧,如果你没有校验的中间件,还是要做用户校验的)。

我们取得发起这个动作的用户id和目标视频id。获取操作类型,是删除还是创建。

如果是创建就DO如果删除就UnDo。

随后返回是否成功即可。

List部分有点长,这里拆开来看吧。

这里的Favoritelist也包含请求他人的favoritelist,所以不能之间从上下文中来拿userid了,而是要看query中的userid。

	id, err = strconv.ParseInt(c.Query("user_id"), 10, 64)
	if err != nil {
		c.JSON(http.StatusOK, UserResponse{
			Response: Response{
				StatusCode: 1,
				StatusMsg:  "Invaild user id",
			},
		})
	}

通过id拉facorite列表。

	favors, err := service_favor.ListByUserId(id)
	if err != nil {
		log.Printf("[WARN] User[id: %d] Fetch favourite list faild, ERR: %s.", id, err)
		c.JSON(http.StatusOK, VideoListResponse{
			Response: Response{
				StatusCode: 3,
				StatusMsg:  "Fetch favorite list faild.",
			},
			VideoList: nil,
		})
		return
	}

从返回的favor中拿videoID然后拉videolist。

	videoIds := make([]int64, 0, len(*favors))
	for _, f := range *favors {
		videoIds = append(videoIds, f.VideoId)
	}

	videos, err := service_video.GetVideoBySet(videoIds)
	if err != nil {
		log.Printf("[WARN] User[id: %d] Fetch favourite list faild, ERR: %s.", id, err)
		c.JSON(http.StatusOK, VideoListResponse{
			Response: Response{
				StatusCode: 3,
				StatusMsg:  "Fetch favorite list faild.",
			},
			VideoList: nil,
		})
		return
	}

将得到的video转换为可接口指定的类型返回即可。

	var respVideoList []Video
	for _, video := range *videos {
		respVideoList = append(respVideoList, *RepoVideoToCon(&video))
	}

	c.JSON(http.StatusOK, VideoListResponse{
		Response: Response{
			StatusCode: 0,
		},
		VideoList: respVideoList,
	})

favorite结束。