好了开始写功能了,害,时间有点不够了没加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的使用
未完待续....