go-zero 实战进阶 - 用户管理 rpc 服务

597 阅读4分钟

我们通过一个系列文章跟大家详细展示一个 go-zero 微服务实例,整个系列分十三篇文章,目录结构如下:

  1. go-zero 实战 - 服务划分与项目创建
  2. go-zero 实战 - User API Gateway
  3. go-zero 实战 - User Login
  4. go-zero 实战 - User Register
  5. go-zero 实战 - User Userinfo
  6. go-zero 实战 - Food API Gateway
  7. go-zero 实战 - Food Search
  8. go-zero 实战 - Food AddFood
  9. go-zero 实战 - Food DeleteFood
  10. go-zero 实战 - Food Foodlist
  11. go-zero 实战进阶 - rpc 服务
  12. go-zero 实战进阶 - 用户管理 rpc 服务
  13. go-zero 实战进阶 - 食材管理 rpc 服务

期望通过本系列文章带你在本地利用 go-zero 快速开发一个《食谱指南》系统,让你快速上手微服务。

生成 usermanage rpc 服务

  • 进入 rpc 服务工作区
$ cd FoodGuides/service/usermanage/rpc
  • 创建 proto 文件
$ goctl rpc -o user.proto
  • 编辑 proto 文件
syntax = "proto3";

package user;
option go_package="./user";

message LoginRequest {
  string email = 1;
  string password = 2;
}

message RegisterRequest {
  string Username = 1;
  string Email = 2;
  string Password = 3;
}

message UserinfoRequest {
  string Userid = 1;
  string Token = 2;
}

message Response {
  int64 id = 1;
  string email = 2;
  string username = 3;
  string accessToken = 4;
  int64 accessExpire = 5;
  int64 refreshAfter = 6;
}

service User {
  rpc Login(LoginRequest) returns(Response);
  rpc Register(RegisterRequest) returns(Response);
  rpc Userinfo(UserinfoRequest) returns(Response);
}

我们定义了三个接口:Login, RegisterUserInfo

  • 运行模板生成命令生成 user-rpc 服务
$ goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=.
Done.
  • 添加下载依赖包
$ go mod tidy

编写 user rpc 服务

修改配置文件

$ vim rpc/etc/user.yaml

Name: user.rpc
ListenOn: 0.0.0.0:9999

Etcd:
  Hosts:
  - 127.0.0.1:2379
  Key: user.rpc

Mysql:
  DataSource: root:123456@tcp(127.0.0.1:9528)/foodguides?charset=utf8mb4&parseTime=True&loc=Local

AccessSecret: ad879037-d3fd-tghj-112d-6bfc35d54b7d
AccessExpire: 86400

Salt: HWVOFkGgPTryzICwd7qnJaZR9KQ2i8xe

添加配置的实例化

$ vim rpc/internal/config/config.go 

package config

import "github.com/zeromicro/go-zero/zrpc"

type Config struct {
    zrpc.RpcServerConf

    Mysql struct {
       DataSource string
    }

    AccessSecret string
    AccessExpire int64

    Salt string
}

注册服务上下文 user model 的依赖

$ vim rpc/internal/svc/servicecontext.go

package svc

import (
    "FoodGuides/service/usermanage/model"
    "FoodGuides/service/usermanage/rpc/internal/config"
    "github.com/zeromicro/go-zero/core/stores/sqlx"
)

type ServiceContext struct {
    Config    config.Config
    UserModel model.UserModel
}

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

添加用户登录逻辑 Login

api/internal/logic/loginlogic.go 文件中的 Login 方法的实现逻辑复制到 rpc 服务下的同名文件同名方法中,再稍作修改。

$ vim rpc/internal/logic/loginlogic.go

package logic

import (
    "FoodGuides/common/cryptx"
    "FoodGuides/common/jwtx"
    "FoodGuides/service/usermanage/model"
    "context"
    "errors"
    "time"

    "FoodGuides/service/usermanage/rpc/internal/svc"
    "FoodGuides/service/usermanage/rpc/user"

    "github.com/zeromicro/go-zero/core/logx"
)

type LoginLogic struct {
    ctx    context.Context
    svcCtx *svc.ServiceContext
    logx.Logger
}

func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
    return &LoginLogic{
       ctx:    ctx,
       svcCtx: svcCtx,
       Logger: logx.WithContext(ctx),
    }
}

func (l *LoginLogic) Login(in *user.LoginRequest) (*user.Response, error) {
    // 查询用户是否存在
    res, err := l.svcCtx.UserModel.FindOneByEmail(l.ctx, in.Email)
    if err != nil {
       if err == model.ErrNotFound {
          return nil, errors.New("用户不存在")
       }
       return nil, err
    }

    // 判断密码是否正确
    password := cryptx.PasswordEncrypt(l.svcCtx.Config.Salt, in.Password)
    if password != res.Password {
       return nil, errors.New("密码错误")
    }

    now := time.Now().Unix()
    accessExpire := l.svcCtx.Config.AccessExpire
    jwtToken, err := jwtx.GetJwtToken(l.svcCtx.Config.AccessSecret, now, accessExpire, res.Id)
    if err != nil {
       return nil, err
    }

    return &user.Response{
       Id:           res.Id,
       Email:        res.Email,
       Username:     res.Name,
       AccessToken:  jwtToken,
       AccessExpire: now + accessExpire,
       RefreshAfter: now + accessExpire/2,
    }, nil
}

添加用户注册逻辑 Register

同样的,将 api/internal/logic/registerlogic.go 文件中的 Register 方法的实现逻辑复制到 rpc 服务下的同名文件同名方法中,再稍作修改。

$ vim rpc/internal/logic/registerlogic.go

package logic

import (
    "FoodGuides/common/cryptx"
    "FoodGuides/common/jwtx"
    "FoodGuides/service/usermanage/model"
    "context"
    "errors"
    "time"

    "FoodGuides/service/usermanage/rpc/internal/svc"
    "FoodGuides/service/usermanage/rpc/user"

    "github.com/zeromicro/go-zero/core/logx"
)

type RegisterLogic struct {
    ctx    context.Context
    svcCtx *svc.ServiceContext
    logx.Logger
}

func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
    return &RegisterLogic{
       ctx:    ctx,
       svcCtx: svcCtx,
       Logger: logx.WithContext(ctx),
    }
}

func (l *RegisterLogic) Register(in *user.RegisterRequest) (*user.Response, error) {
    // 判断邮箱是否已经被注册
    _, err := l.svcCtx.UserModel.FindOneByEmail(l.ctx, in.Email)
    if err == nil {
       return nil, errors.New("该邮箱已注册")
    }
    if err != model.ErrNotFound {
       return nil, err
    }

    newUser := model.User{
       Name:     in.Username,
       Password: cryptx.PasswordEncrypt(l.svcCtx.Config.Salt, in.Password),
       Email:    in.Email,
    }

    // 插入一条新的用户数据
    res, err := l.svcCtx.UserModel.Insert(l.ctx, &newUser)
    if err != nil {
       return nil, err
    }

    newUser.Id, err = res.LastInsertId()
    if err != nil {
       return nil, err
    }

    now := time.Now().Unix()
    accessExpire := l.svcCtx.Config.AccessExpire

    var jwtToken string
    jwtToken, err = jwtx.GetJwtToken(
       l.svcCtx.Config.AccessSecret,
       now,
       accessExpire,
       newUser.Id,
    )
    if err != nil {
       return nil, err
    }
    
    return &user.Response{
       Id:       newUser.Id,
       Email:    newUser.Email,
       Username: newUser.Name,
       AccessToken:  jwtToken,
       AccessExpire: now + accessExpire,
       RefreshAfter: now + accessExpire/2,
    }, nil
}

添加用户信息逻辑 UserInfo

同样的,将 api/internal/logic/userinfologic.go 文件中的 UserInfo 方法的实现逻辑复制到 rpc 服务下的同名文件同名方法中,再稍作修改。

$ vim rpc/internal/logic/userinfologic.go

package logic

import (
    "FoodGuides/service/usermanage/model"
    "context"
    "errors"
    "strconv"

    "FoodGuides/service/usermanage/rpc/internal/svc"
    "FoodGuides/service/usermanage/rpc/user"

    "github.com/zeromicro/go-zero/core/logx"
)

type UserinfoLogic struct {
    ctx    context.Context
    svcCtx *svc.ServiceContext
    logx.Logger
}

func NewUserinfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserinfoLogic {
    return &UserinfoLogic{
       ctx:    ctx,
       svcCtx: svcCtx,
       Logger: logx.WithContext(ctx),
    }
}

func (l *UserinfoLogic) Userinfo(in *user.UserinfoRequest) (*user.Response, error) {
    uid, _ := strconv.ParseInt(in.Userid, 10, 64)

    // 查询用户是否存在
    res, err := l.svcCtx.UserModel.FindOne(l.ctx, uid)
    if err != nil {
       if err == model.ErrNotFound {
          return nil, errors.New("用户不存在")
       }
       return nil, err
    }
    
    return &user.Response{
       Id:       res.Id,
       Email:    res.Email,
       Username: res.Name,
    }, nil
}

至此,有关用户管理 rpc 服务的接口逻辑代码都已完成。接下来我们要做的是修改 api 服务中的逻辑,即改为 API Gateway 代码调用 rpc 服务,而不是直接将逻辑代码全放在 api 服务中。

优化 user api 服务

修改 user-api.yaml 配置文件

  • 删除 MysqlSalt 两个配置项,因为我们在 rpc 配置文件中重新配置了,这里就不再需要了
  • 添加 user rpc 依赖配置项(来源于 rpc/ect/user.yaml)
$ vim api/etc/user-api.yaml

Name: user-api
Host: 0.0.0.0
Port: 8888

Auth:
  AccessSecret: ad879037-d3fd-tghj-112d-6bfc35d54b7d
  AccessExpire: 86400

UserRpc:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: user.rpc

修改 config.go 文件

同步 user-api.yaml 配置文件的修改内容

$ vim api/internal/config/config.go

package config

import (
    "github.com/zeromicro/go-zero/rest"
    "github.com/zeromicro/go-zero/zrpc"
)

type Config struct {
    rest.RestConf
    
    Auth struct {
       AccessSecret string
       AccessExpire int64
    }
    
    UserRpc zrpc.RpcClientConf
}

注册服务上下文 user rpc 的依赖

  • 删除 UserModel 的声明,并添加 UserRpc 的声明
  • 注册上下文 user rpc 的依赖
$ vim api/internal/svc/servicecontext.go

package svc

import (
    "FoodGuides/service/usermanage/api/internal/config"
    "FoodGuides/service/usermanage/rpc/userclient"
    "github.com/zeromicro/go-zero/zrpc"
)

type ServiceContext struct {
    Config  config.Config
    UserRpc userclient.User
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
       Config:  c,
       UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)),
    }
}

优化用户登录逻辑

改为调用 user rpc 服务进行登录:

$ vim api/internal/logic/loginlogic.go

package logic

import (
    "FoodGuides/service/usermanage/api/internal/svc"
    "FoodGuides/service/usermanage/api/internal/types"
    "FoodGuides/service/usermanage/rpc/userclient"
    "context"

    "github.com/zeromicro/go-zero/core/logx"
)

type LoginLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
    return &LoginLogic{
       Logger: logx.WithContext(ctx),
       ctx:    ctx,
       svcCtx: svcCtx,
    }
}

func (l *LoginLogic) Login(req *types.LoginRequest) (*types.LoginResponse, error) {
    res, err := l.svcCtx.UserRpc.Login(l.ctx, &userclient.LoginRequest{
       Email:    req.Email,
       Password: req.Password,
    })
    if err != nil {
       return nil, err
    }

    token := types.JwtToken{
       AccessToken:  res.AccessToken,
       AccessExpire: res.AccessExpire,
       RefreshAfter: res.RefreshAfter,
    }

    response := types.UserReply{
       Id:       res.Id,
       Username: res.Username,
       Email:    res.Email,
       JwtToken: token,
    }

    return &types.LoginResponse{UserReply: response}, nil
}

优化用户注册逻辑

$ vim api/internal/logic/registerlogic.go

package logic

import (
    "FoodGuides/service/usermanage/api/internal/svc"
    "FoodGuides/service/usermanage/api/internal/types"
    "FoodGuides/service/usermanage/rpc/userclient"
    "context"

    "github.com/zeromicro/go-zero/core/logx"
)

type RegisterLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
    return &RegisterLogic{
       Logger: logx.WithContext(ctx),
       ctx:    ctx,
       svcCtx: svcCtx,
    }
}

func (l *RegisterLogic) Register(req *types.RegisterRequest) (*types.RegisterResponse, error) {
    res, err := l.svcCtx.UserRpc.Register(l.ctx, &userclient.RegisterRequest{
       Username: req.Username,
       Email:    req.Email,
       Password: req.Password,
    })
    if err != nil {
       return nil, err
    }

    token := types.JwtToken{
       AccessToken:  res.AccessToken,
       AccessExpire: res.AccessExpire,
       RefreshAfter: res.RefreshAfter,
    }

    response := types.UserReply{
       Id:       res.Id,
       Username: res.Username,
       Email:    res.Email,
       JwtToken: token,
    }

    return &types.RegisterResponse{UserReply: response}, nil
}

优化用户信息逻辑

$ vim api/internal/logic/userinfologic.go

package logic

import (
    "FoodGuides/service/usermanage/api/internal/svc"
    "FoodGuides/service/usermanage/api/internal/types"
    "FoodGuides/service/usermanage/rpc/userclient"
    "context"
    "encoding/json"
    "strconv"

    "github.com/zeromicro/go-zero/core/logx"
)

type UserInfoLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserInfoLogic {
    return &UserInfoLogic{
       Logger: logx.WithContext(ctx),
       ctx:    ctx,
       svcCtx: svcCtx,
    }
}

func (l *UserInfoLogic) UserInfo() (*types.UserInfoResponse, error) {
    // 通过 l.ctx.Value("uid") 可获取 jwt 载体中 `uid` 信息
    uid, _ := l.ctx.Value("uid").(json.Number).Int64()

    user, err := l.svcCtx.UserRpc.Userinfo(l.ctx, &userclient.UserinfoRequest{
       Userid: strconv.FormatInt(uid, 10),
    })
    if err != nil {
       return nil, err
    }

    response := types.UserReply{
       Id:       user.Id,
       Username: user.Username,
       Email:    user.Email,
    }

    return &types.UserInfoResponse{UserReply: response}, nil
}

启动 user rpc 服务

$ cd FoodGuides/service/usermanage/rpc
$ go run user.go -f etc/user.yaml
Starting rpc server at 0.0.0.0:9999...

运行上述命令后,如果报如下错误,请本机下载并安装 etcd 服务,点击 etcd.exe 运行 etcd 服务后重新执行上述启动 user rpc 服务的命令。

{"level":"warn","ts":"2023-10-20T15:44:45.125405+0800","logger":"etcd-client","caller":"v3@v3.5.9/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"etcd-endpoints://0xc0004668c0/127.0.0.1:2379","attempt":0,"error":"rpc error: code = DeadlineExceeded desc = latest balancer error: last connection error: connection error: desc = \"transport: Error while dialing: dial tcp 127.0.0.1:2379: connectex: No connection could be made because the target machine actively refused it.\""}
{"@timestamp":"2023-10-20T15:44:45.125+08:00","caller":"zrpc/server.go:88","content":"context deadline exceeded","level":"error"}
panic: context deadline exceeded

# windows系统下etcd的安装与使用

启动 user api 服务

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

测试服务

我们用 Postman 尝试请求 LoginRegisterUserinfo 这三个接口,测试服务是否正常。测试方法在对应的文章末尾都有提及,仅供参考:

上一篇《go-zero 实战进阶 - rpc 服务》

下一篇《go-zero 实战进阶 - 食材管理 rpc 服务》