2
头图

Preamble

We will show you a go-zero microservice example in detail through a series of articles. The whole series is divided into ten articles. The directory structure is as follows:

  1. Environment construction
  2. service split
  3. User service
  4. product service
  5. Order service
  6. Payment Services (this article)
  7. RPC Service Auth Authentication
  8. service monitoring
  9. link tracking
  10. Distributed transaction

I hope that through this series, you can quickly develop a mall system using go-zero in the Docker environment on the local machine, so that you can quickly get started with microservices.

Complete example code: https://github.com/nivin-studio/go-zero-mall

First, let's take a look at the overall service split diagram:

6 Payment Services (pay)

  • Enter the service workspace
 $ cd mall/service/pay

6.1 Generate pay model model

  • create sql file
 $ vim model/pay.sql
  • write sql file
 CREATE TABLE `pay` (
    `id` bigint unsigned NOT NULL AUTO_INCREMENT,
    `uid` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
    `oid` bigint unsigned NOT NULL DEFAULT '0' COMMENT '订单ID',
    `amount` int(10) unsigned NOT NULL DEFAULT '0'  COMMENT '产品金额',
    `source` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '支付方式',
    `status` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '支付状态',
    `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_uid` (`uid`),
    KEY `idx_oid` (`oid`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4;
  • Run the template generation command
 $ goctl model mysql ddl -src ./model/pay.sql -dir ./model -c

6.2 Generate pay api service

  • create api file
 $ vim api/pay.api
  • write api file
 type (
    // 支付创建
    CreateRequest {
        Uid    int64 `json:"uid"`
        Oid    int64 `json:"oid"`
        Amount int64 `json:"amount"`
    }
    CreateResponse {
        Id int64 `json:"id"`
    }
    // 支付创建

    // 支付详情
    DetailRequest {
        Id int64 `json:"id"`
    }
    DetailResponse {
        Id     int64 `json:"id"`
        Uid    int64 `json:"uid"`
        Oid    int64 `json:"oid"`
        Amount int64 `json:"amount"`
        Source int64 `json:"source"`
        Status int64 `json:"status"`
    }
    // 支付详情

    // 支付回调
    CallbackRequest {
        Id     int64 `json:"id"`
        Uid    int64 `json:"uid"`
        Oid    int64 `json:"oid"`
        Amount int64 `json:"amount"`
        Source int64 `json:"source"`
        Status int64 `json:"status"`
    }
    CallbackResponse {
    }
    // 支付回调

)

@server(
    jwt: Auth
)
service Pay {
    @handler Create
    post /api/pay/create(CreateRequest) returns (CreateResponse)
    
    @handler Detail
    post /api/pay/detail(DetailRequest) returns (DetailResponse)
    
    @handler Callback
    post /api/pay/callback(CallbackRequest) returns (CallbackResponse)
}
  • Run the template generation command
 $ goctl api go -api ./api/pay.api -dir ./api

6.3 Generate pay rpc service

  • Create proto file
 $ vim rpc/pay.proto
  • write proto file
 syntax = "proto3";

package payclient;

option go_package = "pay";

// 支付创建
message CreateRequest {
    int64 Uid = 1;
    int64 Oid = 2;
    int64 Amount = 3;
}
message CreateResponse {
    int64 id = 1;
}
// 支付创建

// 支付详情
message DetailRequest {
    int64 id = 1;
}
message DetailResponse {
    int64 id = 1;
    int64 Uid = 2;
    int64 Oid = 3;
    int64 Amount = 4;
    int64 Source = 5;
    int64 Status = 6;
}
// 支付详情

// 支付详情
message CallbackRequest {
    int64 id = 1;
    int64 Uid = 2;
    int64 Oid = 3;
    int64 Amount = 4;
    int64 Source = 5;
    int64 Status = 6;
}
message CallbackResponse {
}
// 支付详情


service Pay {
    rpc Create(CreateRequest) returns(CreateResponse);
    rpc Detail(DetailRequest) returns(DetailResponse);
    rpc Callback(CallbackRequest) returns(CallbackResponse);
}
  • Run the template generation command
 $ goctl rpc proto -src ./rpc/pay.proto -dir ./rpc

6.4 Write pay rpc service

6.4.1 Modify the configuration file

  • Modify the pay.yaml configuration file
 $ vim rpc/etc/pay.yaml
  • Modify the service listening address, the port number is 0.0.0.0:9003, Etcd service configuration, Mysql service configuration, CacheRedis service configuration
 Name: pay.rpc
ListenOn: 0.0.0.0:9003

Etcd:
  Hosts:
  - etcd:2379
  Key: pay.rpc

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

CacheRedis:
- Host: redis:6379
  Type: node
  Pass:

6.4.2 Add pay model dependency

  • Added Mysql service configuration, CacheRedis instantiation of service configuration
 $ 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
}
  • Dependency of registration service context pay model
 $ vim rpc/internal/svc/servicecontext.go
 package svc

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

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

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

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

6.4.3 Add user rpc, order rpc dependencies

  • Add user rpc, order rpc service configuration
 $ vim rpc/etc/pay.yaml
 Name: pay.rpc
ListenOn: 0.0.0.0:9003
Etcd:
  Hosts:
  - etcd:2379
  Key: pay.rpc

...

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

OrderRpc:
  Etcd:
    Hosts:
    - etcd:2379
    Key: order.rpc
  • Added user rpc, order rpc instantiation of service configuration
 $ 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

    UserRpc  zrpc.RpcClientConf
    OrderRpc zrpc.RpcClientConf
}
  • Dependency of registration service context user rpc, order rpc
 $ vim rpc/internal/svc/servicecontext.go
 package svc

import (
    "mall/service/order/rpc/orderclient"
    "mall/service/pay/model"
    "mall/service/pay/rpc/internal/config"
    "mall/service/user/rpc/userclient"

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

type ServiceContext struct {
    Config config.Config
    
    PayModel model.PayModel

    UserRpc  userclient.User
    OrderRpc orderclient.Order
}

func NewServiceContext(c config.Config) *ServiceContext {
    conn := sqlx.NewMysql(c.Mysql.DataSource)
    return &ServiceContext{
        Config:   c,
        PayModel: model.NewPayModel(conn, c.CacheRedis),
        UserRpc:  userclient.NewUser(zrpc.MustNewClient(c.UserRpc)),
        OrderRpc: orderclient.NewOrder(zrpc.MustNewClient(c.OrderRpc)),
    }
}

6.4.4 Add payment creation logic Create

  • Add according to oid query order payment record PayModel method FindOneByOid
 $ vim model/paymodel.go
 package model

...

var (
    ...

    cachePayIdPrefix  = "cache:pay:id:"
    cachePayOidPrefix = "cache:pay:oid:"
)

type (
    PayModel interface {
        Insert(data *Pay) (sql.Result, error)
        FindOne(id int64) (*Pay, error)
        FindOneByOid(oid int64) (*Pay, error)
        Update(data *Pay) error
        Delete(id int64) error
    }

    ...
)

...

func (m *defaultPayModel) FindOneByOid(oid int64) (*Pay, error) {
    payOidKey := fmt.Sprintf("%s%v", cachePayOidPrefix, oid)
    var resp Pay
    err := m.QueryRow(&resp, payOidKey, func(conn sqlx.SqlConn, v interface{}) error {
        query := fmt.Sprintf("select %s from %s where `oid` = ? limit 1", payRows, m.table)
        return conn.QueryRow(v, query, oid)
    })
    switch err {
    case nil:
        return &resp, nil
    case sqlc.ErrNotFound:
        return nil, ErrNotFound
    default:
        return nil, err
    }
}

......
  • Add payment creation logic

    In the payment flow creation process, verify whether the user exists by calling order rpc user rpc query, and then verify whether the order exists by calling ---ca129ae7c4c2299e5219853f800a6b04---service query, and then check whether the order has been created for payment through the query library Flow, and finally create a drop library.

 $ vim rpc/internal/logic/createlogic.go
 package logic

import (
    "context"

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

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

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

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

func (l *CreateLogic) Create(in *pay.CreateRequest) (*pay.CreateResponse, error) {
    // 查询用户是否存在
    _, err := l.svcCtx.UserRpc.UserInfo(l.ctx, &user.UserInfoRequest{
        Id: in.Uid,
    })
    if err != nil {
        return nil, err
    }

    // 查询订单是否存在
    _, err = l.svcCtx.OrderRpc.Detail(l.ctx, &order.DetailRequest{
        Id: in.Oid,
    })
    if err != nil {
        return nil, err
    }

    // 查询订单是否已经创建支付
    _, err = l.svcCtx.PayModel.FindOneByOid(in.Oid)
    if err == nil {
        return nil, status.Error(100, "订单已创建支付")
    }

    newPay := model.Pay{
        Uid:    in.Uid,
        Oid:    in.Oid,
        Amount: in.Amount,
        Source: 0,
        Status: 0,
    }

    res, err := l.svcCtx.PayModel.Insert(&newPay)
    if err != nil {
        return nil, status.Error(500, err.Error())
    }

    newPay.Id, err = res.LastInsertId()
    if err != nil {
        return nil, status.Error(500, err.Error())
    }

    return &pay.CreateResponse{
        Id: newPay.Id,
    }, nil
}

6.4.5 Add payment details logic Detail

 $ vim rpc/internal/logic/detaillogic.go
 package logic

import (
    "context"

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

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

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

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

func (l *DetailLogic) Detail(in *pay.DetailRequest) (*pay.DetailResponse, error) {
    // 查询支付是否存在
    res, err := l.svcCtx.PayModel.FindOne(in.Id)
    if err != nil {
        if err == model.ErrNotFound {
            return nil, status.Error(100, "支付不存在")
        }
        return nil, status.Error(500, err.Error())
    }

    return &pay.DetailResponse{
        Id:     res.Id,
        Uid:    res.Uid,
        Oid:    res.Oid,
        Amount: res.Amount,
        Source: res.Source,
        Status: res.Status,
    }, nil
}

6.4.6 Add payment callback logic Callback

In the payment flow callback process, verify whether the user exists by calling order rpc user rpc query, and then verify whether the order exists by calling ---ad99f556372743c99d909bf7e9b53d6c---service query, and then judge whether the order payment flow exists through the query library, And whether the callback payment amount is consistent with the flow payment amount in the library, and finally update the payment flow status and update the order status by calling the order rpc service.

 $ vim rpc/internal/logic/callbacklogic.go
 package logic

import (
    "context"

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

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

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

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

func (l *CallbackLogic) Callback(in *pay.CallbackRequest) (*pay.CallbackResponse, error) {
    // 查询用户是否存在
    _, err := l.svcCtx.UserRpc.UserInfo(l.ctx, &user.UserInfoRequest{
        Id: in.Uid,
    })
    if err != nil {
        return nil, err
    }

    // 查询订单是否存在
    _, err = l.svcCtx.OrderRpc.Detail(l.ctx, &order.DetailRequest{
        Id: in.Oid,
    })
    if err != nil {
        return nil, err
    }

    // 查询支付是否存在
    res, err := l.svcCtx.PayModel.FindOne(in.Id)
    if err != nil {
        if err == model.ErrNotFound {
            return nil, status.Error(100, "支付不存在")
        }
        return nil, status.Error(500, err.Error())
    }
    // 支付金额与订单金额不符
    if in.Amount != res.Amount {
        return nil, status.Error(100, "支付金额与订单金额不符")
    }

    res.Source = in.Source
    res.Status = in.Status

    err = l.svcCtx.PayModel.Update(res)
    if err != nil {
        return nil, status.Error(500, err.Error())
    }

    // 更新订单支付状态
    _, err = l.svcCtx.OrderRpc.Paid(l.ctx, &order.PaidRequest{
        Id: in.Oid,
    })
    if err != nil {
        return nil, status.Error(500, err.Error())
    }

    return &pay.CallbackResponse{}, nil
}

6.5 Write pay api service

6.5.1 Modify the configuration file

  • Modify the pay.yaml configuration file
 $ vim api/etc/pay.yaml
  • Modify the service address, the port number is 0.0.0.0:8003, Mysql service configuration, CacheRedis service configuration, Auth verification
 Name: Pay
Host: 0.0.0.0
Port: 8003

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

CacheRedis:
- Host: redis:6379
  Type: node
  Pass:

Auth:
  AccessSecret: uOvKLmVfztaXGpNYd4Z0I1SiT7MweJhl
  AccessExpire: 86400

6.5.2 Add pay rpc dependency

  • Add pay rpc Service Configuration
 $ vim api/etc/pay.yaml
 Name: Pay
Host: 0.0.0.0
Port: 8003

......

PayRpc:
  Etcd:
    Hosts:
    - etcd:2379
    Key: pay.rpc
  • Added pay rpc instantiation of service configuration
 $ 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
    }

    PayRpc zrpc.RpcClientConf
}
  • Dependency of registered service context pay rpc
 $ vim api/internal/svc/servicecontext.go
 package svc

import (
    "mall/service/pay/api/internal/config"
    "mall/service/pay/rpc/payclient"

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

type ServiceContext struct {
    Config config.Config
    
    PayRpc payclient.Pay
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config: c,
        PayRpc: payclient.NewPay(zrpc.MustNewClient(c.PayRpc)),
    }
}

6.5.3 Add payment creation logic Create

 $ vim api/internal/logic/createlogic.go
 package logic

import (
    "context"

    "mall/service/pay/api/internal/svc"
    "mall/service/pay/api/internal/types"
    "mall/service/pay/rpc/pay"

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

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

func NewCreateLogic(ctx context.Context, svcCtx *svc.ServiceContext) CreateLogic {
    return CreateLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

func (l *CreateLogic) Create(req types.CreateRequest) (resp *types.CreateResponse, err error) {
    res, err := l.svcCtx.PayRpc.Create(l.ctx, &pay.CreateRequest{
        Uid:    req.Uid,
        Oid:    req.Oid,
        Amount: req.Amount,
    })
    if err != nil {
        return nil, err
    }

    return &types.CreateResponse{
        Id: res.Id,
    }, nil
}

6.5.4 Add payment details logic Detail

 $ vim api/internal/logic/detaillogic.go
 package logic

import (
    "context"

    "mall/service/pay/api/internal/svc"
    "mall/service/pay/api/internal/types"
    "mall/service/pay/rpc/pay"

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

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

func NewDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) DetailLogic {
    return DetailLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

func (l *DetailLogic) Detail(req types.DetailRequest) (resp *types.DetailResponse, err error) {
    res, err := l.svcCtx.PayRpc.Detail(l.ctx, &pay.DetailRequest{
        Id: req.Id,
    })
    if err != nil {
        return nil, err
    }

    return &types.DetailResponse{
        Id:     req.Id,
        Uid:    res.Uid,
        Oid:    res.Oid,
        Amount: res.Amount,
        Source: res.Source,
        Status: res.Status,
    }, nil
}

6.5.5 Add payment callback logic Callback

 $ vim api/internal/logic/callbacklogic.go
 package logic

import (
    "context"

    "mall/service/pay/api/internal/svc"
    "mall/service/pay/api/internal/types"
    "mall/service/pay/rpc/pay"

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

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

func NewCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) CallbackLogic {
    return CallbackLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

func (l *CallbackLogic) Callback(req types.CallbackRequest) (resp *types.CallbackResponse, err error) {
    _, err = l.svcCtx.PayRpc.Callback(l.ctx, &pay.CallbackRequest{
        Id:     req.Id,
        Uid:    req.Uid,
        Oid:    req.Oid,
        Amount: req.Amount,
        Source: req.Source,
        Status: req.Status,
    })
    if err != nil {
        return nil, err
    }

    return &types.CallbackResponse{}, nil
}

6.6 Start pay rpc service

Tip: The startup service needs to be started in the golang container
 $ cd mall/service/pay/rpc
$ go run pay.go -f etc/pay.yaml
Starting rpc server at 127.0.0.1:9003...

6.7 Start the pay api service

Tip: The startup service needs to be started in the golang container
 $ cd mall/service/pay/api
$ go run pay.go -f etc/pay.yaml
Starting server at 0.0.0.0:8003...

project address

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

Welcome go-zero and star support us!

WeChat exchange group

Follow the official account of " Microservice Practice " and click on the exchange group to get the QR code of the community group.


kevinwan
931 声望3.5k 粉丝

go-zero作者