[ 项目记录 2功能 | 青训营笔记 ]

115 阅读5分钟

好了开始写功能了,害,时间有点不够了没加mq回头再研究一下,配置就不贴出来了,贴几个代表性的接口,用户端的jwt,鉴权,加密,还有数据库的一些,视频端使用FFmpeg生成截图,使用腾讯的存储cos来存放视频,用redis来存放点赞数据,当然在性能优化上还有很长的路,先实现一下功能


User 服务

用户注册与登录操作

user注册 服务端
// Register implements the UserserviceImpl interface.
func (s *UserserviceImpl) Register(ctx context.Context, req *userservice.UserRegisterReq) (resp *userservice.UserRegisterResp, err error) {
    // 检查用户是否存在
    q := query.Q
    checkRes, _ := utils.CheckUser(q, req.Username, req.Password)
    if checkRes != nil {
        err = fmt.Errorf("注册失败:用户已存在 %w", err)
        return
    }
    // 不存在,密码加密存入数据库
    pwd := utils.ScryptPwd(req.Password)
    newUser := &model.TUser{Name: req.Username, Password: pwd}
    err = q.WithContext(context.Background()).TUser.Create(newUser)
    if err != nil {
        err = fmt.Errorf("注册失败: %w", err)
        return
    }
    // 生成 token
    token, err := jwt.CreateToken(newUser.ID)
    if err != nil {
        err = fmt.Errorf("token生成失败: %w", err)
        return
    }
    // 返回数据
    resp = &userservice.UserRegisterResp{
        StatusCode: 0,
        StatusMsg:  "登录成功",
        UserId:     newUser.ID,
        Token:      token,
    }
    return
}
网关
// UserRegister .
// @router /douyin/user/register [POST]
func UserRegister(ctx context.Context, c *app.RequestContext) {
    var err error
    var req api.UserRegisterReq
    // 获取json信息
    username := c.Query("username")
    password := c.Query("password")
    hlog.Info("start call login rpc api")
    hlog.Infof("name: %v, pass: %v", username, password)
    // 绑定信息
    err = c.BindAndValidate(&req)
    if err != nil {
        c.JSON(consts.StatusBadRequest, utils.H{"status_code": 1, "status_msg": err.Error()})
        return
    }
    // RPC 传给user
    registerResponse, err := rpc.UserRpcClient.Register(context.Background(), &userservice.UserRegisterReq{
        Username: username,
        Password: password,
    })
    if err != nil {
        c.JSON(consts.StatusOK, utils.H{"status_code": 1, "status_msg": err.Error()})
        return
    }
    // 返回
    resp := &api.UserRegisterResp{
        StatusCode: registerResponse.StatusCode,
        StatusMsg:  registerResponse.StatusMsg,
        UserID:     registerResponse.UserId,
        Token:      registerResponse.Token,
    }

    c.JSON(consts.StatusOK, resp)
}
创建 token
func CreateToken(userId int64) (string, error) {
    expireTime := time.Now().Add(24 * 7 * time.Hour) // 过期时间为7天
    nowTime := time.Now()                            // 当前时间
    claims := Claims{
        UserId: userId,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: expireTime.Unix(), // 过期时间戳
            IssuedAt:  nowTime.Unix(),    // 当前时间戳
            Issuer:    "mini-tiktok",     // 颁发者签名
        },
    }
    tokenStruct := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return tokenStruct.SignedString([]byte(consts.SecretKey))
}
加密

使用 scrypt

// ScryptPwd 加密
func ScryptPwd(password string) string {
    const PwdHashByte = 10
    salt := make([]byte, 8)
    salt = []byte{200, 20, 9, 29, 15, 50, 80, 7} // 盐值

    key, err := scrypt.Key([]byte(password), salt, 16384, 8, 1, PwdHashByte)
    if err != nil {
        log.Fatal(err)
    }
    FinPwd := base64.StdEncoding.EncodeToString(key)
    return FinPwd
}

或者可以使用 bcrypt

// 加密
func ScryptPw(password string) string {
    const cost = 10

    HashPw, err := bcrypt.GenerateFromPassword([]byte(password), cost)
    if err != nil {
        log.Fatal(err)
    }

    return string(HashPw)
}
// 解密
bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
登录

检测是否存在 --> 密码是否正确 --> 颁发token --> 返回

ok 结束

关键点:token颁发,加密,sql查询

    res, _ = user.WithContext(context.Background()).
        Where(user.Name.Eq(username)).
        First()

查询列表操作

// FriendList implements the UserserviceImpl interface.
func (s *UserserviceImpl) FriendList(ctx context.Context, req *userservice.RelationFriendListReq) (resp *userservice.RelationFriendListResp, err error) {
    resp = &userservice.RelationFriendListResp{}
    qFriend := query.Q.TFriend
    qUser := query.Q.TUser
    qFollow := query.Q.TFollow
    // 格式转换
    id, _ := strconv.ParseInt(req.UserId, 10, 64)
    // 查询 查看用户的好友
    friendUsers, err := qFriend.WithContext(ctx).Select(qFriend.FriendID).Where(qFriend.UserID.Eq(id)).Find()
    if err != nil {
        if err.Error() == "record not found" {
            resp.StatusCode = 0
            resp.StatusMsg = "用户没有好友"
            resp.UserList = nil
            return resp, nil
        }
        return nil, err
    }
    userIds := make([]int64, len(friendUsers))
    // 抽离出粉丝的用户 id
    for i, user := range friendUsers {
        userIds[i] = user.FriendID
    }
    // 对关注的用户进行查询
    queryUsers, _ := qUser.WithContext(ctx).Where(qUser.ID.In(userIds...)).Find()
    users := make([]userservice.User, len(queryUsers))
    claims, _ := jwt.CheckToken(req.Token)
    // 如果查看用户与当前登录用户是好友,不需要返回自身的数据
    // 如果这个数大于 -1 ,说明登陆用户与查看用户是好友,将此数据进行剔除
    whetherExistCurrentUser := -1
    for i, queryUser := range queryUsers {

        if queryUser.ID == claims.UserId {
            whetherExistCurrentUser = i
            continue
        }
        users[i].Id = queryUser.ID
        users[i].Name = queryUser.Name
        users[i].FollowerCount = queryUser.FollowerCount
        users[i].FollowCount = queryUser.FollowCount
    }
    // 进行剔除登录用户的数据
    if whetherExistCurrentUser >= 0 {
        users = append(users[:whetherExistCurrentUser], users[whetherExistCurrentUser+1:]...)
    }
    // 如果查看的用户是自己,就不需要查询是否已经关注
    if id == claims.UserId {
        for i := 0; i < len(users); i++ {
            users[i].IsFollow = true
            resp.UserList = append(resp.UserList, &users[i])
        }
    } else {
        for i := 0; i < len(users); i++ {
            whetherToCare, err := qFollow.WithContext(ctx).
                Where(qFollow.UserID.Eq(claims.UserId), qFollow.FollowerID.Eq(users[i].Id)).First()
            if err == nil && whetherToCare != nil {
                users[i].IsFollow = true
            } else {
                users[i].IsFollow = false
            }
            resp.UserList = append(resp.UserList, &users[i])
        }
    }
    resp.StatusMsg = "查询成功"
    resp.StatusCode = 0
    return resp, nil
}

关键点:

append添加时要在循环里创造一个新的对象再在尾部加入,否则会一直添加最后一项

        var us userservice.User
        us = user
        list = append(list, &us)

gorm - gen 的连表查询及替代

left-join

queryComment.WithContext(ctx).
        Select(queryComment.Content, queryComment.CreateDate, queryUser.ID, queryUser.Name).LeftJoin(&queryUser, queryUser.ID.EqCol(queryComment.UserID)).Where(queryComment.VideoID.Eq(req.VideoId)).Scan(&result)

flowerIds...是数组

users, err := queryUser.WithContext(ctx).Select(queryUser.ID, queryUser.Name,
        queryUser.FollowCount, queryUser.FollowerCount).Where(queryUser.ID.In(followerIds...)).Find()

Video 服务

发布视频

这里采用腾讯的对象存储 cos

对象存储 功能概览-产品简介-文档中心-腾讯云 (tencent.com)

对象存储 快速入门-SDK 文档-文档中心-腾讯云 (tencent.com)

// PublishAction implements the VideoServiceImpl interface.
func (s *VideoServiceImpl) PublishAction(ctx context.Context, req *videoservice.PublishActionReq) (resp *videoservice.PublishActionResp, err error) {
    resp = &videoservice.PublishActionResp{}
    l := len(req.Data)
    klog.Infof("视频长度:%d", l)
    // mb不知道为什么thrift的byte生成出来的int8啊啊啊
    bytes := make([]byte, l)
    for i, _ := range req.Data {
        bytes[i] = byte(req.Data[i])
    }
    // 生成唯一通识码
    uuidv4, _ := uuid.NewUUID()
    uuidname := uuidv4.String()
    path := fmt.Sprintf("%s.mp4", uuidname)

    tv := query.Q.TVideo
    cliams, _ := jwt.CheckToken(req.Token)
    userId := cliams.UserId
    // 将视频保存到cos里
    videoPath, photoPath, err := cos.SaveUploadedFile(ctx, bytes, path)
    if err != nil {
        return
    }
    // 将元数据存入数据库
    url := config.GlobalConfigs.CosConfig.Url
    err = tv.WithContext(context.Background()).
        Create(&model.TVideo{
            AuthorID:      userId,
            PlayURL:       fmt.Sprintf("%s%s", url, videoPath),
            CoverURL:      fmt.Sprintf("%s%s", url, photoPath),
            FavoriteCount: 0,
            CommentCount:  0,
            IsFavorite:    false,
            Title:         req.Title,
            //CreateDate:    time.Now(),
        })
    if err != nil {
        klog.Error("Error uploading file:", err)
        err = fmt.Errorf("视频保存失败:%w", err)
        return
    }
    return
}

保存视频到cos

// SaveUploadedFile 保存视频到cos
func SaveUploadedFile(ctx context.Context, file []byte, videoFileName string) (saveVideoPath, savePhotoPath string, err error) {
    saveVideoPath = fmt.Sprintf("%s%s", "/video/", videoFileName)
    savePhotoPath = fmt.Sprintf("%s%s.jpg", "/photo/", strings.Split(videoFileName, ".")[0])
    // 保存视频到临时文件夹
    tmpVideoPath := fmt.Sprintf("%s/dousheng-%s", os.TempDir(), videoFileName)
    // perm权限
    err = os.WriteFile(tmpVideoPath, file, 0666)
    if err != nil {
        err = fmt.Errorf("上传失败:%w", err)
        return
    }
    defer os.Remove(tmpVideoPath)
    // TODO 队列防ffmpeg并发冲突
    // 使用 cmd 命令调用 ffmpeg 生成截图 ,传入的参数一为 视频的真实路径,参数二为生成图片保存的真实路径
    tempPhotoPath := fmt.Sprintf("%s/test-%s.jpg", os.TempDir(), strings.Split(videoFileName, ".")[0])
    cmd := exec.Command("ffmpeg", "-i", tmpVideoPath, tempPhotoPath,
        "-ss", "00:00:00", "-r", "1", "-vframes", "1", "-an", "-vcodec", "mjpeg")
    _ = cmd.Run()
    defer os.Remove(tempPhotoPath)
    var wg conc.WaitGroup
    // 上传视频到cos
    wg.Go(func() {
        _, e := client.Object.Put(ctx, saveVideoPath, bytes.NewReader(file), nil)
        if e != nil {
            err = fmt.Errorf("上传失败:%w", e)
        }
    })
    // 截图上传到cos
    wg.Go(func() {
        _, err = client.Object.PutFromFile(ctx, savePhotoPath, tempPhotoPath, nil)
        if err != nil {
            err = fmt.Errorf("上传失败:%w", err)
        }
    })
    wg.Wait()
    return
}

同时感觉这里可以用七牛的储存 Go SDK_SDK 下载_对象存储 - 七牛开发者中心 (qiniu.com)

// UpLoadFile 上传文件函数
func UpLoadFile(file multipart.File, fileSize int64) (string, int) {
    putPolicy := storage.PutPolicy{
        Scope: Bucket,
    }
    mac := qbox.NewMac(AccessKey, SecretKey)
    upToken := putPolicy.UploadToken(mac)

    cfg := setConfig()

    putExtra := storage.PutExtra{}

    formUploader := storage.NewFormUploader(&cfg)
    ret := storage.PutRet{}

    err := formUploader.PutWithoutKey(context.Background(), &ret, upToken, file, fileSize, &putExtra)
    if err != nil {
        return "", e.ERROR
    }
    url := Sever + ret.Key
    return url, e.SUCCESS
}

关键点:

uuid的使用

存储对象如腾讯,七牛等

点赞

我们这里用了 redis

    redis := cache.RedisCache.RedisClient
    // 查询对应的点赞数
    val, err2 := redis.HGet(context.Background(), "videos", strconv.FormatInt(videoID, 10)).Result()
    // 判断当前用户是否点赞
    result, err := redis.SIsMember(context.Background(), "post_set"+":"+consts.FavoriteActionPrefix+strconv.FormatInt(req.VideoId, 10), strconv.FormatInt(claims.UserId, 10)).Result()
    // redis数据库中删除关联
    _, err1 := redis.SRem(context.Background(), "post_set"+":"+consts.FavoriteActionPrefix+strconv.FormatInt(req.VideoId, 10), strconv.FormatInt(claims.UserId, 10)).Result()

关键点:

go-redis的使用

未完待续....