这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天
今天和大家分享gorm的概念和常见操作。
用户模块设计
业务设计
作为基础用户模块,考虑到项目方案文档中现有的业务需求:用户注册,用户登录,用户信息 , 分别得出以下业务场景:
- 用户利用名称(唯一)和密码进行注册,注册成功后返回用户ID;
- 用户输入账号密码,登录成功后返回id和token;
- 查看用户卡片,返回当前用户的信息和关注关系。
Controller
基于上面的业务需求分析,可以设计如下controller层请求返回函数:
type UserLoginResponse struct {
Response
UserId int `json:"user_id,omitempty"`
Token string `json:"token"`
}
type UserResponse struct {
Response
User User `json:"user"`
}
// UserInfo 获取用户详情
func UserInfo(c context.Context, ctx *app.RequestContext) {
user := new(User)
user.Id, _ = strconv.Atoi(ctx.Query("user_id"))
... // 省略错误返回和service层调用步骤
ctx.JSON(consts.StatusOK, UserResponse{
Response: Response{
StatusCode: g.StatusCodeOk,
},
User: *user,
})
}
// UserRegister 用户注册
func UserRegister(c context.Context, ctx *app.RequestContext) {
name := ctx.Query("username")
pw := ctx.Query("password")
userId, token, err := service.UserRegister(name, pw)
... // 省略错误返回
ctx.JSON(consts.StatusOK, UserLoginResponse{
Response: Response{
StatusCode: g.StatusCodeOk,
},
UserId: userId,
Token: token,
})
}
// UserLogin 用户登录
func UserLogin(c context.Context, ctx *app.RequestContext) {
name := ctx.Query("username")
pw := ctx.Query("password")
userId, token, err := service.UserLogin(name, pw)
... // 省略返回(基本结构和用户注册返回一致)
}
Service
在service层,由于用户信息获取需要调用多个表进行联表查询,所以可以设计出如下service层函数:
// UserInfo 获取用户信息详情 userId为当前看的主页的用户id,myId是根据token判断而来的当前登录的user_id
func UserInfo(myId int, userId int) (Id, FollowCount, FollowerCount int, Name string, IsFollow bool, err error) {}
// UserRegister 用户注册 返回userId和token
func UserRegister(name, password string) (userId int, token string, err error) {}
// UserLogin 用户登录 返回userId和token
func UserLogin(name, password string) (userId int, token string, err error) {}
Model
model层设计:
// Users
// CreateUser 增加用户
func CreateUser(user *User) (db *gorm.DB, err error) {}
// GetUser 通过名称和user_id查询记录 limit 1
func GetUser(user *User) (err error) {}
// Follows
// IsFollow 根据用户id和当前id查看是否关注
func IsFollow(userId, followId int) bool {}
// GetFollowerCount 获取当前用户的粉丝人数
func GetFollowerCount(followId int) (count int64) {}
// GetFollowCount 获取当前用户的关注人数
func GetFollowCount(userId int) (count int64) {}
另一方面,作为用户模块,也需要考虑到用户鉴权设计。
这里采用了使用路由组进行中间件分类的方式,将路由组分为以下两组:
- loggedGroup: 需要登陆后才可以访问的api接口,在这个路由组中,需要进行一系列的鉴权操作
- publicGroup:无需登录便可以访问的api接口,这个路由组中不需要进行鉴权操作
路由配置详情:
// 初始化全局中间件
middleware.InitMiddleWareForDefault(h)
// 该路由组无需使用token中间件鉴权
publicGroup := InitGroup(h, "")
// 该路由组需要使用token鉴权中间件
loggedGroup := InitGroup(h, "", middleware.Jwt())
publicGroup.GET("/feed", api.GetFeedList)
loggedGroup.GET("/favorite/action", api.GetFollowerList)
publicGroup.POST("/user/register", api.UserRegister)
publicGroup.POST("/user/login", api.UserLogin)
loggedGroup.GET("/user", api.UserInfo)
publicGroup.GET("/publish/action", api.GetFollowerList)
loggedGroup.GET("/publish/list", api.PublishList)
publicGroup.GET("/favorite/list", api.GetFavoriteList)
publicGroup.GET("/comment/list", api.GetCommentList) // 查看视频评论列表
loggedGroup.POST("/comment/action", api.GetCommentAction) // 修改视频评论
loggedGroup.POST("/relation/action", api.PublishVideo)
loggedGroup.GET("/relation/follow/list", api.GetFollowerList)
loggedGroup.GET("/relation/follower/list", api.GetFollowerList)
loggedGroup.GET("/relation/friend/list", api.GetFollowerList)
publicGroup.GET("/message/chat", api.GetFollowerList)
publicGroup.GET("/message/action", api.GetFollowerList)
鉴权操作则使用最佳常用的jwt进行。hertz官方提供了相关的jwt中间件和相关的工具包可以很方便的构建一个jwt框架。这里为了进一步学习jwt的原理和鉴权过程,以及鉴权的作用,采用了最基础的jwt工具包:go-jwt,从0搭建了一个最小可用的jwt鉴权服务,并且在中间件设计了鉴权中间件用于判断用户传递的token是否有效。
根据jwt的构成结构可知,一个token字符串,分为头部的储存加密算法的字符串,中间部分的储存主要数据负载的字符串,和最后一部分通过前两部分和私有密钥进行加密的字符串。
我们这里使用go-jwt进行jwt封装:
这里我们需要在Claims中加入用户的id和name用来标识用户,同时在jwt中加入id也作为其他用户获取当前登录用户id的重要途径,这一点会在后面的中间件中重点介绍:
// ParseToken 解析token
func ParseToken(str string) (UserClaims, error) {
c := new(UserClaims)
token, err := jwt.ParseWithClaims(str, c, func(token *jwt.Token) (interface{}, error) {
return []byte(g.Config.Auth.Jwt.SecretKey), nil
})
if err != nil {
return *c, errors.New("token不合法")
}
if token.Valid != true {
return *c, errors.New("token不合法")
}
return *c, err
}
// GenerateToken 生成7天的token
func GenerateToken(user model.User) (str string) {
c := &UserClaims{
Id: user.Id,
Name: user.Name,
Expire: int(g.Config.Auth.Jwt.ExpiresTime + time.Now().Unix()), // 7天
}
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 根据自定义key生成tokenString
str, _ = claims.SignedString([]byte(g.Config.Auth.Jwt.SecretKey))
return
}
在jwt中间件中,我们解析token,然后得到claims,分析其中的expire,查看token是否过期,如果或者出现了鉴权失败等情况,我们终止中间件并且返回具体信息:
- 查看token是否合法:claims, err := service.ParseToken(token)
- 查看token是否过期:claims.Expire < int(time.Now().Unix())
- 在当前请求生命周期设置suer_id,在请求中可以获取当前user_id,方便查看当前的用户id
最终,我们通过c.Next()继续请求。
这样,在controller层处理请求时,可以通过c.Get("user_id")来获取当前请求user_id。