带你十天轻松搞定 Go 微服务系列(三)
阅读原文时间:2022年01月28日阅读:1

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

  1. 环境搭建
  2. 服务拆分
  3. 用户服务(本文)
  4. 产品服务
  5. 订单服务
  6. 支付服务
  7. RPC 服务 Auth 验证
  8. 服务监控
  9. 链路追踪
  10. 分布式事务

期望通过本系列带你在本机利用 Docker 环境利用 go-zero 快速开发一个商城系统,让你快速上手微服务。

完整示例代码:https://github.com/nivin-studio/go-zero-mall

首先,我们来更新一下上篇文章中的服务拆分图片,由于微信公众号手机和电脑端不同步,导致美化的图片没有跟大家见面,特此补上,如图:

  • 进入服务工作区

    $ cd mall/service/user

  • 创建 sql 文件

    $ vim model/user.sql

  • 编写 sql 文件

    CREATE TABLE user (
    id bigint unsigned NOT NULL AUTO_INCREMENT,
    name varchar(255) NOT NULL DEFAULT '' COMMENT '用户姓名',
    gender tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '用户性别',
    mobile varchar(255) NOT NULL DEFAULT '' COMMENT '用户电话',
    password varchar(255) 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 idx_mobile_unique (mobile)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

  • 运行模板生成命令

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

  • 创建 api 文件

    $ vim api/user.api

  • 编写 api 文件

    type (
    // 用户登录
    LoginRequest {
    Mobile string json:"mobile"
    Password string json:"password"
    }
    LoginResponse {
    AccessToken string json:"accessToken"
    AccessExpire int64 json:"accessExpire"
    }
    // 用户登录

    // 用户注册
    RegisterRequest {
    Name string json:"name"
    Gender int64 json:"gender"
    Mobile string json:"mobile"
    Password string json:"password"
    }
    RegisterResponse {
    Id int64 json:"id"
    Name string json:"name"
    Gender int64 json:"gender"
    Mobile string json:"mobile"
    }
    // 用户注册

    // 用户信息
    UserInfoResponse {
    Id int64 json:"id"
    Name string json:"name"
    Gender int64 json:"gender"
    Mobile string json:"mobile"
    }
    // 用户信息
    )

    service User {
    @handler Login
    post /api/user/login(LoginRequest) returns (LoginResponse)

    @handler Register
    post /api/user/register(RegisterRequest) returns (RegisterResponse)
    }

    @server(
    jwt: Auth
    )
    service User {
    @handler UserInfo
    post /api/user/userinfo() returns (UserInfoResponse)
    }

  • 运行模板生成命令

    $ goctl api go -api ./api/user.api -dir ./api

  • 创建 proto 文件

    $ vim rpc/user.proto

  • 编写 proto 文件

    syntax = "proto3";

    package userclient;

    option go_package = "user";

    // 用户登录
    message LoginRequest {
    string Mobile = 1;
    string Password = 2;
    }
    message LoginResponse {
    int64 Id = 1;
    string Name = 2;
    int64 Gender = 3;
    string Mobile = 4;
    }
    // 用户登录

    // 用户注册
    message RegisterRequest {
    string Name = 1;
    int64 Gender = 2;
    string Mobile = 3;
    string Password = 4;
    }
    message RegisterResponse {
    int64 Id = 1;
    string Name = 2;
    int64 Gender = 3;
    string Mobile = 4;
    }
    // 用户注册

    // 用户信息
    message UserInfoRequest {
    int64 Id = 1;
    }
    message UserInfoResponse {
    int64 Id = 1;
    string Name = 2;
    int64 Gender = 3;
    string Mobile = 4;
    }
    // 用户信息

    service User {
    rpc Login(LoginRequest) returns(LoginResponse);
    rpc Register(RegisterRequest) returns(RegisterResponse);
    rpc UserInfo(UserInfoRequest) returns(UserInfoResponse);
    }

  • 运行模板生成命令

    $ goctl rpc proto -src ./rpc/user.proto -dir ./rpc

  • 添加下载依赖包

    回到 mall 项目根目录执行如下命令:

    $ go mod tidy

3.4.1 修改配置文件

  • 修改 user.yaml 配置文件

    $ vim rpc/etc/user.yaml

  • 修改服务监听地址,端口号为0.0.0.0:9000,Etcd 服务配置,Mysql 服务配置,CacheRedis 服务配置

    Name: user.rpc
    ListenOn: 0.0.0.0:9000

    Etcd:
    Hosts:

    • etcd:2379
      Key: user.rpc

    Mysql:
    DataSource: root:123456@tcp(mysql:3306)/mall?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai

    CacheRedis:

    • Host: redis:6379
      Type: node
      Pass:

3.4.2 添加 user model 依赖

  • 添加 Mysql 服务配置,CacheRedis 服务配置的实例化

    $ vim rpc/internal/config/config.go

    package config

    import (
    "github.com/tal-tech/go-zero/core/stores/cache"
    "github.com/tal-tech/go-zero/zrpc"
    )

    type Config struct {
    zrpc.RpcServerConf

    Mysql struct {
    DataSource string
    }

    CacheRedis cache.CacheConf
    }

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

    $ vim rpc/internal/svc/servicecontext.go

    package svc

    import (
    "mall/service/user/model"
    "mall/service/user/rpc/internal/config"

    "github.com/tal-tech/go-zero/core/stores/sqlx"
    )

    type ServiceContext struct {
    Config config.Config

    UserModel model.UserModel
    }

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

3.4.3 添加用户注册逻辑 Register

  • 添加密码加密工具

    在根目录 common 新建 crypt 工具库,此工具方法主要用于密码的加密处理。

    $ vim common/cryptx/crypt.go

    package cryptx

    import (
    "fmt"

    "golang.org/x/crypto/scrypt"
    )

    func PasswordEncrypt(salt, password string) string {
    dk, _ := scrypt.Key([]byte(password), []byte(salt), 32768, 8, 1, 32)
    return fmt.Sprintf("%x", string(dk))
    }

  • 添加密码加密 Salt 配置

    $ vim rpc/etc/user.yaml

    Name: user.rpc
    ListenOn: 0.0.0.0:9000

    Salt: HWVOFkGgPTryzICwd7qnJaZR9KQ2i8xe

    $ vim rpc/internal/config/config.go

    package config

    import (
    "github.com/tal-tech/go-zero/core/stores/cache"
    "github.com/tal-tech/go-zero/zrpc"
    )

    type Config struct {

    Salt string
    }

  • 添加用户注册逻辑

    用户注册流程,先判断注册手机号是否已经被注册,手机号未被注册,将用户信息写入数据库,用户密码需要进行加密存储。

    $ vim rpc/internal/logic/registerlogic.go

    package logic

    import (
    "context"

    "mall/common/cryptx"
    "mall/service/user/model"
    "mall/service/user/rpc/internal/svc"
    "mall/service/user/rpc/user"

    "github.com/tal-tech/go-zero/core/logx"
    "google.golang.org/grpc/status"
    )

    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.RegisterResponse, error) {
    // 判断手机号是否已经注册
    _, err := l.svcCtx.UserModel.FindOneByMobile(in.Mobile)
    if err == nil {
    return nil, status.Error(100, "该用户已存在")
    }

    if err == model.ErrNotFound {

    newUser := model.User{
      Name:     in.Name,
      Gender:   in.Gender,
      Mobile:   in.Mobile,
      Password: cryptx.PasswordEncrypt(l.svcCtx.Config.Salt, in.Password),
    }
    
    res, err := l.svcCtx.UserModel.Insert(&newUser)
    if err != nil {
      return nil, status.Error(500, err.Error())
    }
    
    newUser.Id, err = res.LastInsertId()
    if err != nil {
      return nil, status.Error(500, err.Error())
    }
    
    return &user.RegisterResponse{
      Id:     newUser.Id,
      Name:   newUser.Name,
      Gender: newUser.Gender,
      Mobile: newUser.Mobile,
    }, nil

    }

    return nil, status.Error(500, err.Error())
    }

3.4.4 添加用户登录逻辑 Login

用户登录流程,通过手机号查询判断用户是否是注册用户,如果是注册用户,需要将用户输入的密码进行加密与数据库中用户加密密码进行对比验证。

$ vim rpc/internal/logic/loginlogic.go


package logic

import (
  "context"

  "mall/common/cryptx"
  "mall/service/user/model"
  "mall/service/user/rpc/internal/svc"
  "mall/service/user/rpc/user"

  "github.com/tal-tech/go-zero/core/logx"
  "google.golang.org/grpc/status"
)

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.LoginResponse, error) {
  // 查询用户是否存在
  res, err := l.svcCtx.UserModel.FindOneByMobile(in.Mobile)
  if err != nil {
    if err == model.ErrNotFound {
      return nil, status.Error(100, "用户不存在")
    }
    return nil, status.Error(500, err.Error())
  }

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

  return &user.LoginResponse{
    Id:     res.Id,
    Name:   res.Name,
    Gender: res.Gender,
    Mobile: res.Mobile,
  }, nil
}

3.4.5 添加用户信息逻辑 UserInfo

$ vim rpc/internal/logic/userinfologic.go


package logic

import (
  "context"

  "mall/service/user/model"
  "mall/service/user/rpc/internal/svc"
  "mall/service/user/rpc/user"

  "github.com/tal-tech/go-zero/core/logx"
  "google.golang.org/grpc/status"
)

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.UserInfoResponse, error) {
  // 查询用户是否存在
  res, err := l.svcCtx.UserModel.FindOne(in.Id)
  if err != nil {
    if err == model.ErrNotFound {
      return nil, status.Error(100, "用户不存在")
    }
    return nil, status.Error(500, err.Error())
  }

  return &user.UserInfoResponse{
    Id:     res.Id,
    Name:   res.Name,
    Gender: res.Gender,
    Mobile: res.Mobile,
  }, nil
}

3.5.1 修改配置文件

  • 修改 user.yaml 配置文件

    $ vim api/etc/user.yaml

  • 修改服务地址,端口号为0.0.0.0:8000,Mysql 服务配置,CacheRedis 服务配置,Auth 验证配置

    Name: User
    Host: 0.0.0.0
    Port: 8000

    Mysql:
    DataSource: root:123456@tcp(mysql:3306)/mall?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai

    CacheRedis:

    • Host: redis:6379
      Pass:
      Type: node

    Auth:
    AccessSecret: uOvKLmVfztaXGpNYd4Z0I1SiT7MweJhl
    AccessExpire: 86400

3.5.2 添加 user rpc 依赖

  • 添加 user rpc 服务配置

    $ vim api/etc/user.yaml

    Name: User
    Host: 0.0.0.0
    Port: 8000

    ……

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

  • 添加 user rpc 服务配置的实例化

    $ vim api/internal/config/config.go

    package config

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

    type Config struct {
    rest.RestConf

    Auth struct {
    AccessSecret string
    AccessExpire int64
    }

    UserRpc zrpc.RpcClientConf
    }

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

    $ vim api/internal/svc/servicecontext.go

    package svc

    import (
    "mall/service/user/api/internal/config"
    "mall/service/user/rpc/userclient"

    "github.com/tal-tech/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)),
    }
    }

3.5.3 添加用户注册逻辑 Register

$ vim api/internal/logic/registerlogic.go


package logic

import (
  "context"

  "mall/service/user/api/internal/svc"
  "mall/service/user/api/internal/types"
  "mall/service/user/rpc/userclient"

  "github.com/tal-tech/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) (resp *types.RegisterResponse, err error) {
  res, err := l.svcCtx.UserRpc.Register(l.ctx, &userclient.RegisterRequest{
    Name:     req.Name,
    Gender:   req.Gender,
    Mobile:   req.Mobile,
    Password: req.Password,
  })
  if err != nil {
    return nil, err
  }

  return &types.RegisterResponse{
    Id:     res.Id,
    Name:   res.Name,
    Gender: res.Gender,
    Mobile: res.Mobile,
  }, nil
}

3.5.4 添加用户登录逻辑 Login

  • 添加 JWT 工具

    在根目录 common 新建 jwtx 工具库,用于生成用户 token

    $ vim common/jwtx/jwt.go

    package jwtx

    import "github.com/golang-jwt/jwt"

    func GetToken(secretKey string, iat, seconds, uid int64) (string, error) {
    claims := make(jwt.MapClaims)
    claims["exp"] = iat + seconds
    claims["iat"] = iat
    claims["uid"] = uid
    token := jwt.New(jwt.SigningMethodHS256)
    token.Claims = claims
    return token.SignedString([]byte(secretKey))
    }

  • 添加用户登录逻辑

    通过调用 user rpc 服务进行登录验证,登录成功后,使用用户信息生成对应的 token 以及 token 的有效期。

    $ vim api/internal/logic/loginlogic.go

    package logic

    import (
    "context"
    "time"

    "mall/common/jwtx"
    "mall/service/user/api/internal/svc"
    "mall/service/user/api/internal/types"
    "mall/service/user/rpc/userclient"

    "github.com/tal-tech/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) (resp *types.LoginResponse, err error) {
    res, err := l.svcCtx.UserRpc.Login(l.ctx, &userclient.LoginRequest{
    Mobile: req.Mobile,
    Password: req.Password,
    })
    if err != nil {
    return nil, err
    }

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

    accessToken, err := jwtx.GetToken(l.svcCtx.Config.Auth.AccessSecret, now, accessExpire, res.Id)
    if err != nil {
    return nil, err
    }

    return &types.LoginResponse{
    AccessToken: accessToken,
    AccessExpire: now + accessExpire,
    }, nil
    }

3.5.5 添加用户信息逻辑 UserInfo

$ vim api/internal/logic/userinfologic.go


package logic

import (
  "context"
  "encoding/json"

  "mall/service/user/api/internal/svc"
  "mall/service/user/api/internal/types"
  "mall/service/user/rpc/userclient"

  "github.com/tal-tech/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() (resp *types.UserInfoResponse, err error) {
  uid, _ := l.ctx.Value("uid").(json.Number).Int64()
  res, err := l.svcCtx.UserRpc.UserInfo(l.ctx, &userclient.UserInfoRequest{
    Id: uid,
  })
  if err != nil {
    return nil, err
  }

  return &types.UserInfoResponse{
    Id:     res.Id,
    Name:   res.Name,
    Gender: res.Gender,
    Mobile: res.Mobile,
  }, nil
}

通过 l.ctx.Value("uid") 可获取 jwt token 中自定义的参数

提示:启动服务需要在 golang 容器中启动

$ cd mall/service/user/rpc
$ go run user.go -f etc/user.yaml
Starting rpc server at 127.0.0.1:9000...

提示:启动服务需要在 golang 容器中启动

$ cd mall/service/user/api
$ go run user.go -f etc/user.yaml
Starting server at 0.0.0.0:8000...

https://github.com/zeromicro/go-zero

欢迎使用 go-zerostar 支持我们!

关注『微服务实践』公众号并点击 交流群 获取社区群二维码。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章