帶你十天輕鬆搞定 Go 微服務系列(三)

語言: CN / TW / HK

序言

我們通過一個系列文章跟大家詳細展示一個 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

首先,我們來更新一下上篇文章中的服務拆分圖片,由於微信公眾號手機和電腦端不同步,導致美化的圖片沒有跟大家見面,特此補上,如圖:

3 使用者服務(user)

  • 進入服務工作區
$ cd mall/service/user

3.1 生成 user model 模型

  • 建立 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

3.2 生成 user api 服務

  • 建立 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

3.3 生成 user rpc 服務

  • 建立 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 編寫 user rpc 服務

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:[email protected](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 編寫 user api 服務

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:[email protected](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 中自定義的引數

3.6 啟動 user rpc 服務

提示:啟動服務需要在 golang 容器中啟動

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

3.7 啟動 user api 服務

提示:啟動服務需要在 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

https://gitee.com/kevwan/go-zero

歡迎使用 go-zerostar 支援我們!

微信交流群

關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。