内容简介:项目地址:这一节我们将实现用户的注册登录,以及关注的后台功能在model目录下新建user.go文件。我们将和user相关的结构体都定义在里面。
项目地址: github
概述
这一节我们将实现用户的注册登录,以及关注的后台功能
定义用户模型
在model目录下新建user.go文件。我们将和user相关的结构体都定义在里面。
package model import "time" type Gender int const ( Man Gender = iota + 1 Woman Unknown ) type UserState int const ( Unsigned UserState = iota + 1 Normal Forbidden Freeze ) type User struct { Id uint64 `graphql:"id"` Username string `graphql:"username"` Email string `graphql:"email"` Password string `graphql:"-"` Avatar string `graphql:"avatar"` Gender Gender `graphql:"gender"` Introduce *string `graphql:"introduce"` State UserState `graphql:"state"` Root bool `graphql:"root"` CreatedAt time.Time `graphql:"createdAt"` UpdatedAt time.Time `graphql:"updatedAt"` DeletedAt *time.Time `graphql:"deletedAt"` Count UserCount `graphql:"-"` } type UserCount struct { Uid uint64 `graphql:"-"` FansNum int `graphql:"fansNum"` FollowNum int `graphql:"followNum"` ArticleNum int `graphql:"articleNum"` Words int `graphql:"words"` LikeNum int `graphql:"likeNum"` CreatedAt time.Time `graphql:"-"` UpdatedAt time.Time `graphql:"-"` DeletedAt time.Time `graphql:"-"` } type UserFollow struct { Id int64 `graphql:"-"` Uid int64 `graphql:"-"` Fuid int64 `graphql:"-"` CreatedAt time.Time `graphql:"createdAt"` UpdatedAt time.Time `graphql:"updatedAt"` DeletedAt *time.Time `graphql:"deletedAt"` }
可以看到,我们使用了tag:graphql来标识结构体中字段在GraphQL中的命名,其中"-"表示忽略该字段。
在User的定义中,Introduce和DeletedAt使用了指针,这是因为在graphql库中,对于字段可以为空或者非空的判定,正是根据是否是指针类型来判断的。
graphql库中的GraphQL类型,大多与 Go 中类型保持一致,比如int,string这种类型,因为在Go中不可能为空,所以映射到GraphQL中时也是非空类型。若需要定义一个空的基本类型,需要使用指针。
将用户模型映射到GraphQL中
我们刚刚定义了关于用户的三个结构体,那么如何将这些模型在GraphQL中体现呢?
首先,我们需要在handler目录下,新建graphql.go文件,用来整合处理所有的模型注册事件。
package resolve func Register(){ }
现在graphql中只有一个空的Register函数。我们需要为它添加内容。在同级目录下新建user.go文件。
package resolve import "github.com/shyptr/graphql/schemabuilder" func registerUser(schema *schemabuilder.Schema) { }
schema正是graphql中用于将我们定义好的数据类型映射到GraphQL中的媒介。
我们先来分析一下,对于用户数据而言,如果我们要从前端界面获取用户数据,需要哪些数据。如下列了在简书项目中,我们要使用到的数据。
- 用户基本信息,Id,用户名,头像,邮箱等
- 用户计数,包括文章数,粉丝数等
- 用户关系网,譬如粉丝列表,关注列表
- 用户发表的内容,文章,评论等
由于我们现在只涉及用户,所有文章评论等暂不考虑。那么接下来就该注册这些数据到GraphQL中了。
修改handler.user.go。
func registerUser(schema *schemabuilder.Schema) { // 枚举类型映射 schema.Enum("Gender", model.Gender(0), map[string]model.Gender{ "Man": model.Man, "Woman": model.Woman, "Unknown": model.Unknown, }) schema.Enum("UserState", model.UserState(0), map[string]model.UserState{ "Unsigned": model.Unsigned, "Forbidden": model.Forbidden, "Freeze": model.Freeze, }) // 将user结构体映射到graphql user := schema.Object("User", model.User{}) // 粉丝数,关注数,文章数,字数,被点赞数 user.FieldFunc("FansNum", func(u model.User) int { return u.Count.FansNum }) user.FieldFunc("FollowNum", func(u model.User) int { return u.Count.FollowNum }) user.FieldFunc("ArticleNum", func(u model.User) int { return u.Count.ArticleNum }) user.FieldFunc("Words", func(u model.User) int { return u.Count.Words }) user.FieldFunc("LikeNum", func(u model.User) int { return u.Count.LikeNum }) // 粉丝列表 user.FieldFunc("Fans", func() []model.User { return nil }) // 关注列表 user.FieldFunc("Followed", func() []model.User { return nil }) query := schema.Query() // 获取用户信息 query.FieldFunc("User", func() model.User { return model.User{} }) }
Enum将我们定义的枚举类型,转换成字符类型的枚举值列表在GraphQL中展示。
在graphql中,结构体被定义为Object,对于tag:graphql不为"-"的字段,graphql会自动处理,无需单独定义。
而像粉丝数,粉丝列表这样的字段,则需要调用object的FieldFunc方法进行注册。该方法第一个参数是这个字段的名称,第二个参数则是这个字段的解析函数。
这里粉丝列表和关注列表,我们没有准备在这里实现解析函数的逻辑。handler中应该只是像路由一样,作为转发,对于复杂逻辑,应该在resolve中单独定义。
对于用户数据的获取,这里就定义完了。剩下的就是对用户的动作的定义。
- 注册
- 登录
- 关注
- 取消关注
这就是我们这一节的大头了。仍然修改handler.user.go文件,添加内容。
mutation := schema.Mutation() // 注册 mutation.FieldFunc("SignUp", func() model.User { return model.User{} }) // 登录 mutation.FieldFunc("SingIn", func() model.User { return model.User{} }) // 关注 mutation.FieldFunc("Follow", func() {}) // 取消关注 mutation.FieldFunc("UnFollow", func() {})
修改handler.graphql.go文件。
func Register(){ schema:=schemabuilder.NewSchema() registerUser(schema) }
编写解析函数
现在我们终于要和数据库打交道了。
我们在前面定义了一个查询单个用户的query字段,用户的信息在定义中,包括基本信息,计数,关注与被关注情况。
我们修改model.user.go文件,添加如下内容。
func GetUser(tx *sqlog.DB, id uint64, username, email string) (User, error) { rows, err := PSql.Select("id,username,email,password,avatar,gender,introduce,state,root,created_at,updated_at,deleted_at"). From(`"user"`). Where("deleted_at is null"). WhereExpr( sqlex.IF{id != 0, sqlex.Eq{"id": id}}, ). WhereExpr( sqlex.Or{ sqlex.IF{username != "", sqlex.Eq{"username": username}}, sqlex.IF{email != "", sqlex.Eq{"email": email}}, }, ). RunWith(tx).Query() if err != nil { return User{}, err } var user User defer rows.Close() if rows.Next() { err := rows.Scan(&user.Id, &user.Username, &user.Email, &user.Password, &user.Avatar, &user.Gender, &user.Introduce, &user.State, &user.Root, &user.CreatedAt, &user.UpdatedAt, &user.DeletedAt) if err != nil { return user, err } } return user, nil } func GetUserCount(tx *sqlog.DB, id uint64) (UserCount, error) { rows, err := PSql.Select("fans_num,follow_num,article_num,words,like_num"). From("user_count"). Where("uid=$1", id). Where("deleted_at is null"). RunWith(tx).Query() if err != nil { return UserCount{}, err } var c UserCount defer rows.Close() if rows.Next() { err := rows.Scan(&c.FansNum, &c.FollowNum, &c.ArticleNum, &c.Words, &c.LikeNum) if err != nil { return c, err } } return c, nil } func GetUserFollower(tx *sqlog.DB, id uint64) ([]uint64, error) { rows, err := PSql.Select("fuid"). From("user_follow"). Where("uid=$1", id). Where("deleted_at is null"). RunWith(tx).Query() if err != nil { return nil, err } var fs []uint64 defer rows.Close() for rows.Next() { var f uint64 err := rows.Scan(&f) if err != nil { return nil, err } fs = append(fs, f) } return fs, nil } func GetFollowUser(tx *sqlog.DB, id uint64) ([]uint64, error) { rows, err := PSql.Select("uid"). From("user_follow"). Where("fuid=$1", id). Where("deleted_at is null"). RunWith(tx).Query() if err != nil { return nil, err } var fs []uint64 defer rows.Close() for rows.Next() { var f uint64 err := rows.Scan(&f) if err != nil { return nil, err } fs = append(fs, f) } return fs, nil }
在resolve下新建user.go文件。
package resolve import ( "context" "fmt" "github.com/shyptr/jianshu/model" "github.com/shyptr/jianshu/util" ) type userResolver struct{} var UserResolver userResolver type idArgs struct { Id int64 `graphql:"id"` } // 根据用户ID查询用户信息 func (u userResolver) User(ctx context.Context, args idArgs) (model.User, error) { logger := util.GetLogger() defer util.PutLogger(logger) user, err := model.GetUser(args.Id) if err != nil { logger.Error().Caller().Err(err).Send() return model.User{}, fmt.Errorf("查询用户信息失败") } count, err := model.GetUserCount(args.Id) if err != nil { logger.Error().Caller().Err(err).Send() return model.User{}, fmt.Errorf("查询用户信息失败") } user.Count = count return user, nil } // 粉丝列表 func (u userResolver) Followers(ctx context.Context, user model.User) ([]model.User, error) { logger := ctx.Value("logger").(zerolog.Logger) tx := ctx.Value("tx").(*sqlog.DB) ids, err := model.GetUserFollower(tx, user.Id) if err != nil { logger.Error().Caller().Err(err).Send() return nil, fmt.Errorf("查询用户信息失败") } var users []model.User for _, id := range ids { user, err := u.User(ctx, IdArgs{id}) if err != nil { return nil, err } users = append(users, user) } return users, nil } // 关注列表 func (u userResolver) Follows(ctx context.Context, user model.User) ([]model.User, error) { logger := ctx.Value("logger").(zerolog.Logger) tx := ctx.Value("tx").(*sqlog.DB) ids, err := model.GetFollowUser(tx, user.Id) if err != nil { logger.Error().Caller().Err(err).Send() return nil, fmt.Errorf("查询用户信息失败") } var users []model.User for _, id := range ids { user, err := u.User(ctx, IdArgs{id}) if err != nil { return nil, err } users = append(users, user) } return users, nil }
关于查询的逻辑,都很简单,没有可以多说的。重点还是接下来的注册和登录。
唯一需要注意的是,关注列表和粉丝列表,由于在GraphQL中是属于User的字段,所以id的参数并不需要从客户端传入,而是从source获取。
在这里source就是我们通过query查询到的User结构体了,所以在函数参数处,我们没有使用args,而是用了user model.Use。
修改model.user.go文件,添加内容如下。
type UserArg struct { Username string `graphql:"username" validate:"min=6,max=16"` Email string `graphql:"email" validate:"email"` Password string `graphql:"password" validate:"min=8"` Avatar string `graphql:"-"` } func InsertUser(tx *sqlog.DB, arg UserArg) (uint64, error) { id, err := idfetcher.NextID() if err != nil { return id, err } result, err := PSql.Insert(`"user"`). Columns("id,username,email,password,avatar"). Values(id, arg.Username, arg.Email, arg.Password,arg.Avatar). RunWith(tx).Exec() if err != nil { return id, err } affected, _ := result.RowsAffected() if affected == 0 { return id, fmt.Errorf("保存用户信息失败") } return id, nil } func InsertUserCount(tx *sqlog.DB, id uint64) error { result, err := PSql.Insert("user_count").Columns("uid").Values(id).RunWith(tx).Exec() if err != nil { return err } affected, _ := result.RowsAffected() if affected == 0 { return fmt.Errorf("保存用户信息失败") } return nil } func InsertUserFollow(tx *sqlog.DB, uid uint64, fuid uint64) error { id, err := idfetcher.NextID() if err != nil { return err } result, err := PSql.Insert("user_follow").Columns("id,uid,fuid").Values(id, uid, fuid).RunWith(tx).Exec() if err != nil { return err } affected, _ := result.RowsAffected() if affected == 0 { return fmt.Errorf("关注失败") } return nil } func DeleteUserFollow(tx *sqlog.DB, id uint64, fuid uint64) error { result, err := PSql.Update("user_follow"). Set("deleted_at", time.Now()). Where(sqlex.Eq{"uid": id, "fuid": fuid}). RunWith(tx).Exec() if err != nil { return err } affected, _ := result.RowsAffected() if affected == 0 { return fmt.Errorf("取消关注失败") } return nil }
我们这里先写注册登录的逻辑。
修改resolve/user.go文件,新增内容。
// 注册 func (u userResolver) SingUp(ctx context.Context, args model.UserArg) (user model.User, err error) { logger := ctx.Value("logger").(zerolog.Logger) tx := ctx.Value("tx").(*sqlog.DB) err = u.ValidUsername(ctx, usernameArg{Username: args.Username}) if err != nil { return model.User{}, err } err = u.ValidEmail(ctx, emailArg{Email: args.Email}) if err != nil { return model.User{}, err } // 密码加密 password, err := bcrypt.GenerateFromPassword([]byte(args.Password), 10) if err != nil { logger.Error().Caller().Err(err).Send() return model.User{}, errors.New("注册失败") } args.Password = string(password) id, err := model.InsertUser(tx, args) if err != nil { logger.Error().Caller().Err(err).Send() return model.User{}, errors.New("注册失败") } err = model.InsertUserCount(tx, id) if err != nil { logger.Error().Caller().Err(err).Send() return model.User{}, errors.New("注册失败") } // TODO:邮箱验证 user, _ = model.GetUser(tx, id, "", "") return user, nil } func (u userResolver) SignIn(ctx context.Context, args struct { Username string `graphql:"username"` // 邮箱或者用户名 Password string `graphql:"password"` RememberMe bool `graphql:"rememberme"` }) (user model.User, err error) { // 验证账号密码 logger := ctx.Value("logger").(zerolog.Logger) tx := ctx.Value("tx").(*sqlog.DB) user, err = model.GetUser(tx, 0, args.Username, args.Username) if err != nil { logger.Error().Caller().AnErr("登录失败", err).Send() return model.User{}, errors.New("登录失败") } if user.Id == 0 { return model.User{}, errors.New("用户不存在!") } // 验证密码 err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(args.Password)) if err != nil { return model.User{}, err } // 生成token,并设置cookie var age int if args.RememberMe { age = 7 * 24 } token, err := util.GeneraToken(user.Id, age) if err != nil { logger.Error().Caller().AnErr("生成token失败", err).Send() return model.User{}, errors.New("登录失败") } c := ctx.(*graphql.Context) http.SetCookie(c.Writer, &http.Cookie{ Name: "me", Value: token, Path: "/", Expires: time.Now(), MaxAge: int(time.Hour) * age, }) return user, nil }
在注册时,我们对用户名和邮箱进行了一次唯一性校验。同时也可以看到我们在model.UserArg上增加了validate的tag。
这是因为graphql默认引入了validator库,用于参数的校验。当然,validate的使用需要手动开启,默认情况下是不开启的。
注册时,简书项目使用了Go的扩展库 golang.org/x/crypto 下的bcrypt包。bcrypt包使用base64编码,实现了Provos和Mazières的bcrypt自适应散列算法。
该算法的具体内容可以看这篇论文 《A Future-Adaptable Password Scheme》
。可以看到,我们在加密的时候传了一个10到GenerateFromPassword函数中。这里可以理解为表示该密码被破译需要花费的代价。
这个cost值越高,加密就越复杂,越难以破解。当然相应的,加密的过程耗费的时间也越长,使用时应权衡好具体数值。
登录最主要的就是session的管理。关于token,session,cookie的区别,这里不多赘述,网上已经有很多详解了。在这里,我们使用了jwt-go库。
将用户的Id作为session信息,通过加密得到token,并将token存入客户端的cookie中。即我们的session信息,是通过cookie存储的。
最后是关注与取消关注的解析函数,修改resolve/user.go。
// 关注 func (u userResolver) Follow(ctx context.Context, args struct { Id uint64 `graphql:"id"` }) error { logger := ctx.Value("logger").(zerolog.Logger) tx := ctx.Value("tx").(*sqlog.DB) userId := ctx.Value("userId").(uint64) err := model.InsertUserFollow(tx, args.Id, userId) if err != nil { logger.Error().Caller().AnErr("关注失败", err).Send() return errors.New("关注失败") } // TODO: 发送通知 return nil } // 取消关注 func (u userResolver) CancelFollow(ctx context.Context, args struct { Id uint64 `graphql:"id"` }) error { logger := ctx.Value("logger").(zerolog.Logger) tx := ctx.Value("tx").(*sqlog.DB) userId := ctx.Value("userId").(uint64) err := model.DeleteUserFollow(tx, args.Id, userId) if err != nil { logger.Error().Caller().AnErr("取消关注失败", err).Send() return errors.New("取消关注失败") } return nil }
将复杂解析函数注册到GraphQL对应字段
现在我们的业务逻辑都在解析函数中编写完成了。接下来就要将这些函数注册到对应的字段上去。
修改handler/user.go文件。
func registerUser(schema *schemabuilder.Schema) { // 枚举类型映射 schema.Enum("Gender", model.Gender(0), map[string]model.Gender{ "Man": model.Man, "Woman": model.Woman, "Unknown": model.Unknown, }) schema.Enum("UserState", model.UserState(0), map[string]model.UserState{ "Unsigned": model.Unsigned, "Forbidden": model.Forbidden, "Freeze": model.Freeze, }) // 将user结构体映射到graphql user := schema.Object("User", model.User{}) // 粉丝数,关注数,文章数,字数,被点赞数 user.FieldFunc("FansNum", func(u model.User) int { return u.Count.FansNum }) user.FieldFunc("FollowNum", func(u model.User) int { return u.Count.FollowNum }) user.FieldFunc("ArticleNum", func(u model.User) int { return u.Count.ArticleNum }) user.FieldFunc("Words", func(u model.User) int { return u.Count.Words }) user.FieldFunc("LikeNum", func(u model.User) int { return u.Count.LikeNum }) // 粉丝列表 user.FieldFunc("Fans", resolve.UserResolver.Followers) // 关注列表 user.FieldFunc("Followed", resolve.UserResolver.Follows) query := schema.Query() // 获取用户信息 query.FieldFunc("User", resolve.UserResolver.User) mutation := schema.Mutation() // 注册 mutation.FieldFunc("SignUp", resolve.UserResolver.SingUp, middleware.BasicAuth(), middleware.LoginNeed()) // 登录 mutation.FieldFunc("SingIn", resolve.UserResolver.SignIn, middleware.BasicAuth(), middleware.NotLogin()) // 关注 mutation.FieldFunc("Follow", resolve.UserResolver.Follow, middleware.BasicAuth(), middleware.LoginNeed()) // 取消关注 mutation.FieldFunc("UnFollow", resolve.UserResolver.CancelFollow, middleware.BasicAuth(), middleware.LoginNeed()) }
启动GraphiQL
现在我们在main方法中调用handler的Register方法。
修改handler/graphql.go文件。
func Register(mux *http.ServeMux) { builder := schemabuilder.NewSchema() registerUser(builder) schema, err := builder.Build() if err != nil { log.Fatalln(err) } introspection.AddIntrospectionToSchema(schema) mux.Handle("/", graphql.GraphiQLHandler("/graphql")) mux.Handle("/graphql", graphql.HTTPHandler(schema)) }
修改main函数。
handler.Register(mux)
命令启动项目。
go run cmd/jianshu/main.go
最终效果如下。
作者个人博客地址: https://unrotten.org
作者微信公众号:
欢迎关注我们的微信公众号,每天学习Go知识
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 从零开始Gin Web+Vue商城的搭建(四)-- 重构用户模块和框架设计
- Node.js模块系统 (创建模块与加载模块)
- 黑客基础,Metasploit模块简介,渗透攻击模块、攻击载荷模块
- 022.Python模块序列化模块(json,pickle)和math模块
- 024.Python模块OS模块
- 023.Python的随机模块和时间模块
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序员面试金典(第5版)
[美] Gayle Laakmann McDowell / 李琳骁、漆 犇 / 人民邮电出版社 / 2013-11 / 59.00
本书是原谷歌资深面试官的经验之作,层层紧扣程序员面试的每一个环节,全面而详尽地介绍了程序员应当如何应对面试,才能在面试中脱颖而出。第1~7 章主要涉及面试流程解析、面试官的幕后决策及可能提出的问题、面试前的准备工作、对面试结果的处理等内容;第8~9 章从数据结构、概念与算法、知识类问题和附加面试题4 个方面,为读者呈现了出自微软、苹果、谷歌等多家知名公司的150 道编程面试题,并针对每一道面试题目......一起来看看 《程序员面试金典(第5版)》 这本书的介绍吧!