Take you ten days to easily handle the Go micro service series

preface

We will show you a go zero micro service example in detail through a series of articles. The whole series is divided into ten articles, and the directory structure is as follows:

  1. Environment construction
  2. Service splitting
  3. User services
  4. Product service
  5. Order service
  6. Payment service (this article)
  7. RPC service Auth authentication
  8. Service monitoring
  9. Link tracking
  10. Distributed transaction

Through this series, we hope to take you to quickly develop a mall system by using the Docker environment and go zero on the machine, so that you can quickly start micro services.

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

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

6 payment service (pay)

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

6.1 generate pay 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 'user ID',
	`oid` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'order ID',
	`amount` int(10) unsigned NOT NULL DEFAULT '0'  COMMENT 'Product amount',
	`source` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT 'Payment method',
	`status` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT 'Payment status',
	`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 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
  • Writing api files
type (
	// Payment creation
	CreateRequest {
		Uid    int64 `json:"uid"`
		Oid    int64 `json:"oid"`
		Amount int64 `json:"amount"`
	}
	CreateResponse {
		Id int64 `json:"id"`
	}
	// Payment creation

	// Payment details
	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"`
	}
	// Payment details

	// Payment callback
	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 {
	}
	// Payment callback

)

@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 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";

// Payment creation
message CreateRequest {
    int64 Uid = 1;
    int64 Oid = 2;
    int64 Amount = 3;
}
message CreateResponse {
	int64 id = 1;
}
// Payment creation

// Payment details
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;
}
// Payment details

// Payment details
message CallbackRequest {
    int64 id = 1;
    int64 Uid = 2;
    int64 Oid = 3;
    int64 Amount = 4;
    int64 Source = 5;
    int64 Status = 6;
}
message CallbackResponse {
}
// Payment details


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

6.4 write pay rpc service

6.4.1 modify configuration file

  • Modify pay Yaml profile
$ vim rpc/etc/pay.yaml
  • Modify the service listening address with the port number of 0.0.0.0:9003, Etcd service configuration, Mysql service configuration and 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

  • Add Mysql service configuration and instantiate CacheRedis 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
}
  • Register the dependency of 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 and order rpc dependency

  • Add user RPC and 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
  • Add user RPC and order RPC service configuration instantiation
$ 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
}
  • Register the dependency of service context user RPC and 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

  • Add PayModel method FindOneByOid to query order payment records according to oid
$ 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, the user rpc service is called to query and verify whether the user exists, then the order rpc service is called to query and verify whether the order exists, and then the query library is used to judge whether the payment flow has been created for this order, and finally the drop library is created.

$ 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) {
	// Query whether the user exists
	_, err := l.svcCtx.UserRpc.UserInfo(l.ctx, &user.UserInfoRequest{
		Id: in.Uid,
	})
	if err != nil {
		return nil, err
	}

	// Query whether the order exists
	_, err = l.svcCtx.OrderRpc.Detail(l.ctx, &order.DetailRequest{
		Id: in.Oid,
	})
	if err != nil {
		return nil, err
	}

	// Query whether payment has been created for the order
	_, err = l.svcCtx.PayModel.FindOneByOid(in.Oid)
	if err == nil {
		return nil, status.Error(100, "Payment order created")
	}

	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 Detail 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) {
	// Query whether payment exists
	res, err := l.svcCtx.PayModel.FindOne(in.Id)
	if err != nil {
		if err == model.ErrNotFound {
			return nil, status.Error(100, "Payment does not exist")
		}
		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

In the payment flow callback process, verify whether the user exists by calling the user rpc service query, and then verify whether the order exists by calling the order rpc service query, and then judge whether the payment flow of this order exists by querying the library, and whether the callback payment amount is consistent with the flow payment amount in the library, 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) {
	// Query whether the user exists
	_, err := l.svcCtx.UserRpc.UserInfo(l.ctx, &user.UserInfoRequest{
		Id: in.Uid,
	})
	if err != nil {
		return nil, err
	}

	// Query whether the order exists
	_, err = l.svcCtx.OrderRpc.Detail(l.ctx, &order.DetailRequest{
		Id: in.Oid,
	})
	if err != nil {
		return nil, err
	}

	// Query whether payment exists
	res, err := l.svcCtx.PayModel.FindOne(in.Id)
	if err != nil {
		if err == model.ErrNotFound {
			return nil, status.Error(100, "Payment does not exist")
		}
		return nil, status.Error(500, err.Error())
	}
	// The payment amount is inconsistent with the order amount
	if in.Amount != res.Amount {
		return nil, status.Error(100, "The payment amount is inconsistent with the order amount")
	}

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

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

	// Update order payment status
	_, 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 writing pay api service

6.5.1 modify configuration file

  • Modify pay Yaml profile
$ vim api/etc/pay.yaml
  • Modify the service address, port number is 0.0.0.0:8003, Mysql service configuration, CacheRedis service configuration, Auth authentication configuration
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
  • Add the instantiation of pay rpc 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
}
  • Register the dependency of 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

$ 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 Detail 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

$ 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: start the service 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 pay api service

Tip: start the service 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 to go zero and star support us!

Wechat communication group

Focus on the "micro service practice" official account and click on the exchange group to get the community community's two-dimensional code.

Keywords: Go Web Development Microservices go-zero

Added by drumhrd on Fri, 28 Jan 2022 15:48:49 +0200