用户业务逻辑处理

💟以下的文章是管大佬要的学习资料,分享给大家,也当一个记录。原出处,我无从寻找,非常抱歉!
————————————————————————————————————

本节核心内容

这一节是核心小节,讲解如何处理用户业务,这也是 API 的核心功能。本小节会讲解实际开发中需要的一些重要功能点,并根据笔者的开发经验,给出一些建议。功能点包括:

  • 各种场景的业务逻辑处理
    • 创建用户
    • 删除用户
    • 更新用户
    • 查询用户列表
    • 查询指定用户的信息
  • 数据库的 CURD 操作

本小节源码下载路径:demo07

可先下载源码到本地,结合源码理解后续内容,边学边练。

本小节的代码是基于 demo06 来开发的。

配置路由信息

需要先在 router/router.go 文件中,配置路由信息:

func Load(g *gin.Engine, mw ...gin.HandlerFunc) *gin.Engine {
    ...
    // 用户路由设置
    u := g.Group("/v1/user")
    {
        u.POST("", user.Create)         // 创建用户
        u.DELETE("/:id", user.Delete)   // 删除用户 
        u.PUT("/:id", user.Update)      // 更新用户
        u.GET("", user.List)            // 用户列表
        u.GET("/:username", user.Get)   // 获取指定用户的详细信息
    }
    ...
    return g
}

在 RESTful API 开发中,API 经常会变动,为了兼容老的 API,引入了版本的概念,比如上例中的 /v1/user,说明该 API 版本是 v1

很多 RESTful API 最佳实践文章中均建议使用版本控制,笔者这里也建议对 API 使用版本控制。

注册新的错误码

pkg/errno/code.go 文件中(详见 demo07/pkg/errno/code.go),新增如下错误码:

var (
    // Common errors
        ...

    ErrValidation       = &Errno{Code: 20001, Message: "Validation failed."}
    ErrDatabase         = &Errno{Code: 20002, Message: "Database error."}
    ErrToken            = &Errno{Code: 20003, Message: "Error occurred while signing the JSON web token."}

    // user errors
    ErrEncrypt           = &Errno{Code: 20101, Message: "Error occurred while encrypting the user password."}
    ErrTokenInvalid      = &Errno{Code: 20103, Message: "The token was invalid."}
    ErrPasswordIncorrect = &Errno{Code: 20104, Message: "The password was incorrect."}
)

新增用户

更新 handler/user/create.goCreate() 的逻辑,更新后的内容见 demo07/handler/user/create.go

创建用户逻辑:

  1. 从 HTTP 消息体获取参数(用户名和密码)
  2. 参数校验
  3. 加密密码
  4. 在数据库中添加数据记录
  5. 返回结果(这里是用户名)

从 HTTP 消息体解析参数,前面小节已经介绍了。

参数校验这里用的是 gopkg.in/go-playground/validator.v9 包(详见 go-playground/validator),实际开发过程中,该包可能不能满足校验需求,这时候可在程序中加入自己的校验逻辑,比如在 handler/user/creater.go 中添加校验函数 checkParam

package user

import (
    ...
)

// Create creates a new user account.
func Create(c *gin.Context) {
    log.Info("User Create function called.", lager.Data{"X-Request-Id": util.GetReqID(c)})
    var r CreateRequest
    if err := c.Bind(&r); err != nil {
        SendResponse(c, errno.ErrBind, nil)
        return
    }

    if err := r.checkParam(); err != nil {
        SendResponse(c, err, nil)
        return
    }
        ...
}

func (r *CreateRequest) checkParam() error {
    if r.Username == "" {
        return errno.New(errno.ErrValidation, nil).Add("username is empty.")
    }

    if r.Password == "" {
        return errno.New(errno.ErrValidation, nil).Add("password is empty.")
    }

    return nil
}

例子通过 Encrypt() 对密码进行加密:

// Encrypt the user password.
func (u *UserModel) Encrypt() (err error) {
    u.Password, err = auth.Encrypt(u.Password)
    return
}      

Encrypt() 函数引用 auth.Encrypt() 来进行密码加密,具体实现见 demo07/pkg/auth/auth.go

最后例子通过 u.Create() 函数来向数据库中添加记录,ORM 用的是 gormgorm 详细用法请参考 GORM 指南。在 Create() 函数中引用的数据库实例是 DB.Self,该实例在 API 启动之前已经完成初始化。DB 是个全局变量,可以直接引用。

在实际开发中,为了安全,数据库中是禁止保存密码的明文信息的,密码需要加密保存。

笔者将接收和处理相关的 Go 结构体统一放在 handler/user/user.go 文件中,这样可以使程序结构更清晰,功能更聚焦。当然每个人习惯不一样,读者根据自己的习惯放置即可。handler/user/user.goUserInfo 结构体的处理,也出于相同的目的。

删除用户

删除用户代码详见 demo07/handler/user/delete.go

删除时,首先根据 URL 路径 DELETE http://127.0.0.1/v1/user/1 解析出 id 的值 1,该 id 实际上就是数据库中的 id 索引,调用 model.DeleteUser() 函数删除,函数详见 demo07/model/user.go

更新用户

更新用户代码详见 demo07/handler/user/update.go

更新用户逻辑跟创建用户差不多,在更新完数据库字段后,需要指定 gorm model 中的 id 字段的值,因为 gorm 在更新时默认是按照 id 来匹配记录的。通过解析 PUT http://127.0.0.1/v1/user/1 来获取 id。

查询用户列表

查询用户列表代码详见 demo07/handler/user/list.go

一般在 handler 中主要做解析参数、返回数据操作,简单的逻辑也可以在 handler 中做,像新增用户、删除用户、更新用户,代码量不大,所以也可以放在 handler 中。有些代码量很大的逻辑就不适合放在 handler 中,因为这样会导致 handler 逻辑不是很清晰,这时候实际处理的部分通常放在 service 包中。比如本例的 LisUser() 函数:

package user
   
import (
    "apiserver/service"
    ...
)  
   
// List list the users in the database.
func List(c *gin.Context) {
    ...
    infos, count, err := service.ListUser(r.Username, r.Offset, r.Limit)
    if err != nil {
        SendResponse(c, err, nil)
        return
    }
    ...
}

查询一个 REST 资源列表,通常需要做分页,如果不做分页返回的列表过多,会导致 API 响应很慢,前端体验也不好。本例中的查询函数做了分页,收到的请求中传入的 offsetlimit 参数,分别对应于 MySQL 的 offsetlimit

service.ListUser() 函数用来做具体的查询处理,代码详见 demo07/service/service.go

ListUser() 函数中用了 sync 包来做并行查询,以使响应延时更小。在实际开发中,查询数据后,通常需要对数据做一些处理,比如 ListUser() 函数中会对每个用户记录返回一个 sayHello 字段。sayHello 只是简单输出了一个 Hello shortId 字符串,其中 shortId 是通过 util.GenShortId() 来生成的(GenShortId 实现详见 demo07/util/util.go)。像这类操作通常会增加 API 的响应延时,如果列表条目过多,列表中的每个记录都要做一些类似的逻辑处理,这会使得整个 API 延时很高,所以笔者在实际开发中通常会做并行处理。根据笔者经验,效果提升十分明显。

读者应该已经注意到了,在 ListUser() 实现中,有 sync.MutexIdMap 等部分代码,使用 sync.Mutex 是因为在并发处理中,更新同一个变量为了保证数据一致性,通常需要做锁处理。

使用 IdMap 是因为查询的列表通常需要按时间顺序进行排序,一般数据库查询后的列表已经排过序了,但是为了减少延时,程序中用了并发,这时候会打乱排序,所以通过 IdMap 来记录并发处理前的顺序,处理后再重新复位。

获取指定用户的详细信息

代码详见 demo07/handler/user/get.go

获取指定用户信息时,首先根据 URL 路径 GET http://127.0.0.1/v1/user/admin 解析出 username 的值 admin,然后调用 model.GetUser() 函数查询该用户的数据库记录并返回,函数详见 demo07/model/user.go

编译并运行

  1. 下载 apiserver_demos 源码包(如前面已经下载过,请忽略此步骤)
$ git clone https://github.com/lexkong/apiserver_demos

  1. apiserver_demos/demo07 复制为 $GOPATH/src/apiserver
$ cp -a apiserver_demos/demo07/ $GOPATH/src/apiserver

  1. 在 apiserver 目录下编译源码
$ cd $GOPATH/src/apiserver
$ gofmt -w .
$ go tool vet .
$ go build -v .

创建用户

$ curl -XPOST -H "Content-Type: application/json" http://127.0.0.1:8080/v1/user -d'{"username":"kong","password":"kong123"}'

{
  "code": 0,
  "message": "OK",
  "data": {
    "username": "kong"
  }
}

查询用户列表

$ curl -XGET -H "Content-Type: application/json" http://127.0.0.1:8080/v1/user -d'{"offset": 0, "limit": 20}'

{
  "code": 0,
  "message": "OK",
  "data": {
    "totalCount": 2,
    "userList": [
      {
        "id": 2,
        "username": "kong",
        "sayHello": "Hello qhXO5iIig",
        "password": "$2a$10$vE9jG71oyzstWVwB/QfU3u00Pxb.ye8hFIDvnyw60nHBv/xsJZoUO",
        "createdAt": "2018-06-02 14:47:54",
        "updatedAt": "2018-06-02 14:47:54"
      },
      {
        "id": 0,
        "username": "admin",
        "sayHello": "Hello qhXO5iSmgz",
        "password": "$2a$10$veGcArz47VGj7l9xN7g2iuT9TF21jLI1YGXarGzvARNdnt4inC9PG",
        "createdAt": "2018-05-28 00:25:33",
        "updatedAt": "2018-05-28 00:25:33"
      }
    ]
  }
}

可以看到,新增了一个用户 kong,数据库 id 索引为 2admin 用户是上一节中初始化数据库时初始化的。

笔者建议在 API 设计时,对资源列表进行分页。

获取用户详细信息

$ curl -XGET -H "Content-Type: application/json" http://127.0.0.1:8080/v1/user/kong

{
  "code": 0,
  "message": "OK",
  "data": {
    "username": "kong",
    "password": "$2a$10$vE9jG71oyzstWVwB/QfU3u00Pxb.ye8hFIDvnyw60nHBv/xsJZoUO"
  }
}

更新用户

查询用户列表 部分,会返回用户的数据库索引。例如,用户 kong 的数据库 id 索引是 2,所以这里调用如下 URL 更新 kong 用户:

$ curl -XPUT -H "Content-Type: application/json" http://127.0.0.1:8080/v1/user/2 -d'{"username":"kong","password":"kongmodify"}'

{
  "code": 0,
  "message": "OK",
  "data": null
}

获取 kong 用户信息:

$ curl -XGET -H "Content-Type: application/json" http://127.0.0.1:8080/v1/user/kong

{
  "code": 0,
  "message": "OK",
  "data": {
    "username": "kong",
    "password": "$2a$10$E0kwtmtLZbwW/bDQ8qI8e.eHPqhQOW9tvjwpyo/p05f/f4Qvr3OmS"
  }
}

可以看到密码已经改变(旧密码为 $2a$10$vE9jG71oyzstWVwB/QfU3u00Pxb.ye8hFIDvnyw60nHBv/xsJZoUO)。

删除用户

查询用户列表 部分,会返回用户的数据库索引。例如,用户 kong 的数据库 id 索引是 2,所以这里调用如下 URL 删除 kong 用户:

$ curl -XDELETE -H "Content-Type: application/json" http://127.0.0.1:8080/v1/user/2

{
  "code": 0,
  "message": "OK",
  "data": null
}

获取用户列表:

$ curl -XGET -H "Content-Type: application/json" http://127.0.0.1:8080/v1/user -d'{"offset": 0, "limit": 20}'

{
  "code": 0,
  "message": "OK",
  "data": {
    "totalCount": 1,
    "userList": [
      {
        "id": 0,
        "username": "admin",
        "sayHello": "Hello EnqntiSig",
        "password": "$2a$10$veGcArz47VGj7l9xN7g2iuT9TF21jLI1YGXarGzvARNdnt4inC9PG",
        "createdAt": "2018-05-28 00:25:33",
        "updatedAt": "2018-05-28 00:25:33"
      }
    ]
  }
}

可以看到用户 kong 未出现在用户列表中,说明他已被成功删除。

小结

本小节通过对用户增删改查和查询列表的操作,介绍了实际开发中如何对 REST 资源进行操作,并结合笔者的实际开发经验给出了一些开发习惯和建议。

推荐阅读更多精彩内容