Go-kratos 框架商城微服務實戰之購物車服務 (十二)

Aliliin發表於2022-04-27

大家好,好久不見,由於公司的工作實在太忙,耽擱了好久,實在抱歉。今天我們們開始寫商城裡面的購物車服務,廢話少說我們們開始。

注:豎排 … 程式碼省略,為了保持文章的篇幅簡潔,我會將一些不必要的程式碼使用豎排的 . 來代替,你在複製本文程式碼塊的時候,切記不要將 . 也一同複製進去。文章寫的不清晰的地方可通過 GitHub 原始碼進行檢視, 也感謝您指出不足之處,歡迎大佬指教。

⚠️ ⚠️ ⚠️ 接下來新增或修改的程式碼, wire 注入的檔案中需要修改的程式碼,都不會再本文中提及了。例如 biz、data、service 層的修改,自己編寫的過程中,千萬不要忘記 wire 注入,更不要忘記,執行 make wire 命令,重新生成專案的 wire 檔案。具體使用方法可參考 kratos 官方文件 ⚠️ ⚠️ ⚠️

由於前面幾篇文章(第一篇和第四篇)都已經寫過了,如何初始化一個 kratos 專案,並修改部分主要的檔案,來變成自己的服務並編寫業務。所以此篇文章就不做重複性的工作了,這篇編寫一個購物車的建立,讓購物車服務先存在,廢話少說開始寫。

Cart 服務目錄存放的位置

// 整體的專案 目錄結構如下
|-- kratos-shop
    |-- service
        |-- user // 原先的使用者服務
        |-- cart // 新增的購物車服務
    |-- shop //  interface

購物車表設計

  • data 目錄下新建資料表相關的檔案

cart.go 檔案內容:

package data

import (
    "cart/internal/domain" // 引入 domain 層
    "context"
    "time"

    "github.com/go-kratos/kratos/v2/errors"
    "gorm.io/gorm"

    "cart/internal/biz"

    "github.com/go-kratos/kratos/v2/log"
)

type ShopCart struct {
    ID         int64          `gorm:"primarykey;type:int" json:"id"`
    UserId     int64          `gorm:"type:int;not null;comment:使用者id" json:"user_id"`
    GoodsId    int64          `gorm:"type:int;not null;comment:商品id" json:"goods_id"`
    SkuId      int64          `gorm:"type:int;not null;comment:sku_id" json:"sku_id"`
    GoodsPrice int64          `gorm:"type:int;not null;comment:商品價格" json:"goods_price"`
    GoodsNum   int32          `gorm:"type:int;not null;comment:商品數量" json:"goods_num"`
    GoodsSn    string         `gorm:"type:varchar(500);default:;comment:商品編號"`
    GoodsName  string         `gorm:"type:varchar(500);default:;comment:商品名稱"`
    IsSelect   bool           `gorm:"type:tinyint;comment:是否選中;default:false" json:"is_select"`
    CreatedAt  time.Time      `gorm:"column:add_time" json:"created_at"`
    UpdatedAt  time.Time      `gorm:"column:update_time" json:"updated_at"`
    DeletedAt  gorm.DeletedAt `json:"deleted_at"`
}

type cartRepo struct {
    data *Data
    log  *log.Helper
}

func NewCartRepo(data *Data, logger log.Logger) biz.CartRepo {
    return &cartRepo{
        data: data,
        log:  log.NewHelper(logger),
    }
}

//  domain 層轉換
func (p *ShopCart) ToDomain() *domain.ShopCart {
    return &domain.ShopCart{
        ID:         p.ID,
        UserId:     p.UserId,
        GoodsId:    p.GoodsId,
        SkuId:      p.SkuId,
        GoodsPrice: p.GoodsPrice,
        GoodsNum:   p.GoodsNum,
        GoodsSn:    p.GoodsSn,
        GoodsName:  p.GoodsName,
        IsSelect:   p.IsSelect,
    }
}

新建domain 層下的檔案

internal/domain/cart.go

cart.go 檔案內容:

package domain

type ShopCart struct {
    ID         int64
    UserId     int64
    GoodsId    int64
    SkuId      int64
    GoodsPrice int64
    GoodsNum   int32
    GoodsSn    string
    GoodsName  string
    IsSelect   bool
}

購物車方法

定義購物車建立方法

api/cart/v1/cart.proto

  • cart.proto 檔案建立內容:
syntax = "proto3";

package cart.v1;

import "validate/validate.proto";

option go_package = "cart/api/cart/v1;v1";

// 購物車
service Cart {
  rpc CreateCart (CreateCartRequest) returns (CartInfoReply); // 新增商品進購物車
  ...
}

message CartInfoReply {
  int64 id = 1;
  int64 userId = 2;
  int64 goodsId = 3;
  string goodsSn = 4;
  string goodsName = 5;
  int64 skuId = 6;
  int64 goodsPrice = 7;
  int32 goodsNum = 8;
  bool isSelect = 9;
}

message CreateCartRequest {
 int64 id = 1;
 int64 userId = 2 [(validate.rules).int64 = {gt:0}];
 int64 goodsId = 3 [(validate.rules).int64 = {gt:0}];
 string goodsSn = 4 [(validate.rules).string.min_len = 1];
 string goodsName = 5 [(validate.rules).string.min_len = 1];
 int64 skuId = 6 [(validate.rules).int64 = {gt:0}];
 int64 goodsPrice = 7 [(validate.rules).int64 = {gt:0}];
 int32 goodsNum = 8 [(validate.rules).int32 = {gt:0}];
 bool isSelect = 9 [(validate.rules).bool.const = true];
}

實現購物車建立方法

internal/service/cart.go

  • cart.go 內容如下:
package service

import (
    "cart/internal/biz"
    "cart/internal/domain"
    "context"

    v1 "cart/api/cart/v1"
)

type CartService struct {
    v1.UnimplementedCartServer
    cart *biz.CartUsecase
}

func NewCartService(cart *biz.CartUsecase) *CartService {
    return &CartService{cart: cart}
}

func (s *CartService) CreateCart(ctx context.Context, req *v1.CreateCartRequest) (*v1.CartInfo, error) {

  // biz 層定義的方法,經過 domian 層進行引數轉換
    rv, err := s.cart.CreateCart(ctx, &domain.ShopCart{
        UserId:     req.UserId,
        GoodsId:    req.GoodsId,
        SkuId:      req.SkuId,
        GoodsPrice: req.GoodsPrice,
        GoodsNum:   req.GoodsNum,
        GoodsSn:    req.GoodsSn,
        GoodsName:  req.GoodsName,
        IsSelect:   req.IsSelect,
    })

    if err != nil {
        return nil, err
    }

    return &v1.CartInfo{
        Id:         rv.ID,
        UserId:     rv.UserId,
        GoodsId:    rv.GoodsId,
        GoodsSn:    rv.GoodsSn,
        GoodsName:  rv.GoodsName,
        SkuId:      rv.SkuId,
        GoodsPrice: rv.GoodsPrice,
        GoodsNum:   rv.GoodsNum,
        IsSelect:   rv.IsSelect,
    }, nil
}
  • internal/biz/cart.go 內容如下:
package biz

import (
    "cart/internal/domain"
    "context"

    "github.com/go-kratos/kratos/v2/log"
)

type CartRepo interface {
    Create(ctx context.Context, c *domain.ShopCart) (*domain.ShopCart, error)
}

type CartUsecase struct {
    repo CartRepo
    log  *log.Helper
}

func NewCartUsecase(repo CartRepo, logger log.Logger) *CartUsecase {
    return &CartUsecase{repo: repo, log: log.NewHelper(logger)}
}

func (uc *CartUsecase) CreateCart(ctx context.Context, c *domain.ShopCart) (*domain.ShopCart, error) {
    return uc.repo.Create(ctx, c)
}
  • internal/data/cart.go 新增內容:

...

func (r *cartRepo) Create(ctx context.Context, c *domain.ShopCart) (*domain.ShopCart, error) {
    var shopCart ShopCart
    if result := r.data.db.Where(&ShopCart{UserId: c.UserId, SkuId: c.SkuId}).First(&shopCart); result.RowsAffected == 1 {
        shopCart.GoodsNum += c.GoodsNum
    } else {
        shopCart.UserId = c.UserId
        shopCart.GoodsId = c.GoodsId
        shopCart.SkuId = c.SkuId
        shopCart.GoodsPrice = c.GoodsPrice
        shopCart.GoodsNum = c.GoodsNum
        shopCart.GoodsSn = c.GoodsSn
        shopCart.GoodsName = c.GoodsName
        shopCart.IsSelect = c.IsSelect
    }

    if result := r.data.db.Save(&shopCart); result.Error != nil {
        return nil, errors.InternalServer("CREATE_CART_NOT_FOUND", "建立購物車失敗")
    }

    return shopCart.ToDomain(), nil
}

這裡可以看到,建立購物車資料的時候,只判斷了使用者是否有同等商品的購物車資料。除此之外並未進行其他業務邏輯的判斷,比如,使用者是否存在、商品是否正確,而是拿到資料之後,直接進行儲存入庫。

這裡基於我們們專案整體架構的設計,service 層下的所有個體服務,尤其涉及到登入之後的服務,都假設使用者已經通過了 bff 層 也就是 service 同級目錄的 shop 或者 admin 的業務邏輯驗證,servcie 服務只提供粒度夠細,較少業務無關,有利於重用,利於單元測試的方法。bff 層的程式碼更多的是面向使用者的業務邏輯的。

拿這個建立購物介面來說,shop 服務中使用者建立購物車介面的方法裡面,就會判斷使用者是否登入(使用者服務)、商品是否存在(商品服務)、庫存(庫存服務)是否可以插入購物之類的邏輯驗證。

  • 新增 cart_test.go 檔案

    internal/data/cart_test.go,
    這裡和第二篇文章的單元測試流程是相同,一樣的初始化測試的程式碼就不貼出來了。

  • 編寫測試 data 下的 create 方法

package data_test

import (
    "cart/internal/biz"
    "cart/internal/data"
    "cart/internal/domain"

    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"
)

var _ = Describe("Cart", func() {
    var ro biz.CartRepo
    BeforeEach(func() {
        ro = data.NewCartRepo(Db, nil)
    })
    // 設定 It 塊來新增單個規格
    It("CreateCart", func() {
        cartData := domain.ShopCart{
            UserId:     1,
            GoodsId:    1,
            SkuId:      1,
            GoodsPrice: 1000,
            GoodsNum:   10,
            GoodsSn:    "20232232231",
            GoodsName:  "Mate 40 Pro",
            IsSelect:   true,
        }
        c, err := ro.Create(ctx, &cartData)
        Ω(err).ShouldNot(HaveOccurred())
        Ω(c.UserId).Should(Equal(int64(1)))
        Ω(c.GoodsNum).Should(Equal(int32(10)))

        // 二次驗證建立相同商品的資料,只增加商品數量
        cartData2 := domain.ShopCart{
            UserId:     1,
            GoodsId:    1,
            SkuId:      1,
            GoodsPrice: 1000,
            GoodsNum:   10,
            GoodsSn:    "20232232231",
            GoodsName:  "Mate 40 Pro",
            IsSelect:   true,
        }
        c2, err := ro.Create(ctx, &cartData2)
        Ω(err).ShouldNot(HaveOccurred())
        Ω(c2.UserId).Should(Equal(int64(1)))
        Ω(c2.GoodsNum).Should(Equal(int32(20)))
    })

})

結果如圖共 1 個測試 1 個通過 0 個失敗。

沒錯還是通過 BloomRPC 工具進行測試。


如圖故意不把 goodsid 的引數設定為 0 ,併為通過介面的引數驗證。

如圖把 goodsid 填寫正確,就建立成功了。

在囉嗦幾句,最近這幾天忙成狗,天天寫前端程式碼,各種樣式,互動效果,都要寫吐了。打算合理規劃一下時間,爭取早日把整個專案跑通。

這裡特別感謝一下一直支援觀看此係列的同學,更感謝你們的打賞、點贊、分享

感謝您的耐心閱讀,動動手指點個贊吧。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
微信搜尋:上帝喜愛笨人

相關文章