gozero 实战 User Login

2023年 10月 9日 112.8k 0

在上一篇 《go-zero 实战 - User API Gateway》 中,我们通过定义 user.api 文件中的内容对 API 类型进行了声明,并通过 goctl api 命令,一键快速生成一个 api 服务,如果仅仅是启动一个 go-zeroapi 演示项目,你甚至都不用编码,就可以完成一个 api 服务开发及正常运行。

本篇将在上一篇的基础上完成以下工作内容:

  • 创建本地数据库表 user,并定义表结构,以便存储用户基本信息;
  • 使用 goctl model 自动生成 mysql CRUD 代码;
  • 完成用户登录接口的逻辑。
  • 创建本地数据库表 user,并定义表结构

    在本机 mysql 中创建 foodguides 数据库,并新建 user 表。

    CREATE TABLE `user` (
                            `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户Id',
                            `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名称',
                            `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户密码',
                            `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户邮箱',
                            `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
                            `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                            PRIMARY KEY (`id`),
                            UNIQUE KEY `name_index` (`name`),
                            UNIQUE KEY `email_index` (`email`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
    

    执行如下 SQL 语句新增一条数据:

    INSERT INTO `foodguides`.`user`(`id`, `name`, `password`, `email`) VALUES (1, 'RenPanPan', '809161', 'renpanpan1990@163.com');
    

    使用 goctl model 生成 mysql CRUD 代码

    usermanage 下创建 model 文件夹。

    $ mkdir -p usermanage/model
    $ cd usermanage/model
    

    model 下新建 user.sql 文件,将上述创建 user 表的 SQL 语句复制到 user.sql 文件中。

    model 目录下执行如下命令生成 CRUD 代码:

    $ goctl model mysql ddl --src user.sql --dir .
    

    当你看到 Done. 输出则代表生成成功了,接下来我们来看一下生成的代码内容:

    ➜  model:
    # 列出当前目录下的文件  
    $ ls  
    user.sql usermodel.go usermodel_gen.go vars.go  
    # 查看目录树  
    $ tree  
    .  
    ├── user.sql  
    ├── usermodel.go  // CRUD 代码
    ├── usermodel_gen.go  
    └── vars.go // 定义常量和变量
    

    完成用户登录接口的逻辑

    编辑 usermanage/api/etc 下的 user-api.yaml 文件,追加如下内容:

    DataSource: root:123456@tcp(127.0.0.1:9528)/foodguides?&parseTime=True&loc=Local
    AccessSecret: ad879037-d3fd-tghj-112d-6bfc35d54b7d
    AccessExpire: 86400
    

    Tips

    如何配置 DataSource?

    编辑 usermanage/api/internal/config 下的 config.go 文件,新增 DataSource, AccessSecret, AccessExpire 三个变量:

    type Config struct {
        rest.RestConf
        DataSource   string
        AccessSecret string
        AccessExpire int64
    }
    

    编辑 usermanage/api/internal/svc 下的 serviceContext.go 文件,新增 Model 变量 ,新增实例化代码:

    type ServiceContext struct {
        Config config.Config
        Model  model.UserModel
    }
    
    func NewServiceContext(c config.Config) *ServiceContext {
        return &ServiceContext{
           Config: c,
           Model:  model.NewUserModel(sqlx.NewMysql(c.DataSource)),
        }
    }
    

    编辑 usermanage/api/internal/logic 下的 loginlogic.go 文件, 修改 Login 方法如下所示:

    func (l *LoginLogic) Login(req *types.LoginRequest) (*types.LoginResponse, error) {
        res, err := l.svcCtx.Model.FindOneByEmail(l.ctx, req.Email)
        if err == nil {
           if req.Password == res.Password {
              now := time.Now().Unix()
              accessExpire := l.svcCtx.Config.AccessExpire
              jwtToken, err := renpanpan.GetJwtToken(l.svcCtx.Config.AccessSecret, now, accessExpire, strconv.FormatInt(res.Id, 10))
              if err != nil {
                 return nil, err
              }
              token := types.JwtToken{
                 AccessToken:  jwtToken,
                 AccessExpire: now + accessExpire,
                 RefreshAfter: now + accessExpire/2,
              }
              response := types.UserReply{
                 Id:       res.Id,
                 Username: res.Name,
                 Email:    res.Email,
                 JwtToken: token,
              }
              return &types.LoginResponse{UserReply: response}, nil
           } else {
              return nil, errors.New("密码错误")
           }
        }
        return nil, err
    }
    

    为了简单起见,在 Login 方法中,我只是简单比较了数据库存储的密码是否和请求传的密码参数是否相等(真正的项目此处逻辑应该包含密码加密验证的等相关代码),当两者相等时,即认为用户登录成功,我们将为用户生成一个 token 返回给客户端。

    注意:上面的代码可能会因为找不到 renpanpan 类型的 GetJwtToken 方法而报错,我们将在接下来的修改 Response 返回格式中实现它,GetJwtToken 方法的实现可参考 官网示例。

    修改 Response 返回格式

    我希望客户端接口请求数据的返回格式是这样子的:

    {
        "code": 1,
        "msg": "",
        "data": {
            "id": 1,
            "username": "",
            "email": "renpanpan1990@163.com.com",
            "accessToken": "user-login-token",
            "accessExpire": 1611470394,
            "refreshAfter": 1611427194
        }
    }
    

    目前如果需要实现这种格式响应,有 2 种做法:

  • 自定义响应格式
  • 使用 go-zero 扩展包来实现
  • 我们用做法 1 来演示一下,做法 2 可参考官方文档的演示实例 HTTP 扩展。

    Foodguides 文件夹下新增名为 renpanpan 文件夹,并创建 renpanpan.go 文件,新增如下代码:

    package renpanpan
    
    import "github.com/golang-jwt/jwt/v4"
    
    type HttpResponse struct {
        Code    int         `json:"code"`
        Message string      `json:"msg"`
        Data    interface{} `json:"result"`
    }
    
    func SuccessResponse(resData interface{}, message string) HttpResponse {
        return HttpResponse{Code: 1, Message: message, Data: resData}
    }
    
    func FailureResponse(resData interface{}, message string, code int) HttpResponse {
        return HttpResponse{Code: code, Message: message, Data: resData}
    }
    
    // GetJwtToken 
    // @secretKey: JWT 加解密密钥
    // @iat: 时间戳
    // @seconds: 过期时间,单位秒
    // @payload: 数据载体
    func GetJwtToken(secretKey string, iat, seconds int64, payload string) (string, error) {
        claims := make(jwt.MapClaims)
        claims["exp"] = iat + seconds
        claims["iat"] = iat
        claims["payload"] = payload
        token := jwt.New(jwt.SigningMethodHS256)
        token.Claims = claims
        return token.SignedString([]byte(secretKey))
    }
    

    usermanage/api/internal/handler 下的 loginhandler.go 文件中,对 LoginHandler 方法做如下修改:

    func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
           var req types.LoginRequest
           if err := httpx.Parse(r, &req); err != nil {
              // httpx.ErrorCtx(r.Context(), w, err)
              httpx.OkJson(w, renpanpan.FailureResponse(nil, err.Error(), 1000))
              return
           }
    
           l := logic.NewLoginLogic(r.Context(), svcCtx)
           resp, err := l.Login(&req)
           if err != nil {
              // httpx.ErrorCtx(r.Context(), w, err)
              httpx.OkJson(w, renpanpan.FailureResponse(nil, err.Error(), 1000))
           } else {
              // httpx.OkJsonCtx(r.Context(), w, resp)
              httpx.OkJson(w, renpanpan.SuccessResponse(resp, "登录成功"))
           }
        }
    }
    

    启动服务

    运行如下命令以启动 user api 服务, 运行成功后,user api 则运行在本机的 8888 端口:

    ➜  FoodGuides:
    $ go run usermanage/api/user.go -f usermanage/api/etc/user-api.yaml
    Starting server at 0.0.0.0:8888...
    

    我们用 Postman 尝试请求 /users/login 接口:

  • PostmanBody 选项中选择 raw,并设置 JSON 内容为 {"email": "renpanpan1990@163.com","password": "809161"}
  • 点击发送请求按钮,有如下截图的响应说明接口运行正常。
  • image.png

    至此,我们就完成了用户登录接口的搭建,下一篇我们将采用相同的方法搭建用户注册接口。

    上一篇《go-zero 实战 - User API Gateway》

    下一篇《go-zero 实战 - User Register》

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论