• 带你十天轻松搞定 Go 微服务系列(三)


    序言

    我们通过一个系列文章跟大家详细展示一个 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: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 编写 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: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 中自定义的参数

    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

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

    微信交流群

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

  • 相关阅读:
    iOS开发之窗口和视图
    GCD
    禁止非法用户登录综合设置
    大数减法(C++实现)
    大数加法(C++实现)
    迷宫问题 (BFS ➕输出路径)
    Pots (BFS ➕ 输出路径)
    Shuffle'm Up (map ➕ BFS)
    Prime Path (BFS)
    速算24点
  • 原文地址:https://www.cnblogs.com/kevinwan/p/15840059.html
Copyright © 2020-2023  润新知