Go-kratos 框架商城微服务实战之商品服务 (十) 商品创建

kratos 框架商城微服务实战之商品服务 (十)

大家好,今天咱们终于可以写商品服务中的商品模块了,前面花了 4 篇文章,都是为了创建一个完整的商品所需的数据,就这对比一些电商平台,像商品的售后信息、运费模版、商品促销活动信息等,还有很多需要补的,不过那些都不算有太大影响。咱们还是主要先把整个项目跑起来。

众所周知,一个电商的商品设计是比较复杂的,咱们这里不过多的深究商品设计的每个表是否合理,是否漏写之类的问题,主要是为了搞明白 kratos 的使用和微服务相关的调用关系。当然我真正的编写时也会尽可能的让此项目的商品设计合理一些。但大量的表设计呀,重复性的 curd 就不会在文章中体现了,具体的代码参看 GitHub 上的源码。当然你觉得不合理的地方,欢迎给项目提 PR。

注:竖排 … 代码省略,为了保持文章的篇幅简洁,我会将一些不必要的代码使用竖排的 . 来代替,你在复制本文代码块的时候,切记不要将 . 也一同复制进去。

⚠️ ⚠️ ⚠️ 接下来新增或修改的代码, wire 注入的文件中需要修改的代码,都不会再本文中提及了。例如 biz、service 层的修改,自己编写的过程中,千万不要忘记 wire 注入,更不要忘记,执行 make wire 命令,重新生成项目的 wire 文件。具体使用方法可参考 kratos 官方文档 ⚠️ ⚠️ ⚠️

商品信息

设计商品表

  • data 目录下新建数据表相关的文件

base.go 文件内容:

package data

import (
    "database/sql/driver"
    "encoding/json"
    "gorm.io/gorm"
    "time"
)

type GormList []string

func (g GormList) Value() (driver.Value, error) {
    return json.Marshal(g)
}

func (g *GormList) Scan(value interface{}) error {
    return json.Unmarshal(value.([]byte), &g)
}

type BaseFields struct {
    ID        int64          `gorm:"primarykey;type:int" json:"id"` // bigint
    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"`
}

goods.go文件内容

package data

import (
    "github.com/go-kratos/kratos/v2/log"
    "golang.org/x/net/context"
    "goods/internal/biz"
    "goods/internal/domain"
)

// Goods 商品表
type Goods struct {
    BaseFields
    CategoryID int32 `gorm:"index:category_id;type:int;comment:分类ID;not null"`
    BrandsID   int32 `gorm:"index:brand_id;type:int;comment:品牌ID ;not null"`
    TypeID     int64 `gorm:"index:type_id;type:int;comment:商品类型ID ;not null"`

    Name            string   `gorm:"type:varchar(100);not null;comment:商品名称"`
    NameAlias       string   `gorm:"type:varchar(100);not null;comment:商品别名"`
    GoodsSn         string   `gorm:"type:varchar(100);not null;comment:商品编号"`
    GoodsTags       string   `gorm:"type:varchar(100);not null;comment:商品标签"`
    MarketPrice     int64    `gorm:"type:int;default:0;not null;comment:商品展示价格"`
    GoodsBrief      string   `gorm:"type:varchar(100);not null;comment:商品简介"`
    GoodsFrontImage string   `gorm:"type:varchar(200);not null;comment:商品封面图"`
    GoodsImages     GormList `gorm:"type:varchar(1000);not null;comment:商品的介绍图"` // 切片类型转为 json 到数据库,取出来是切片类型

    OnSale   bool  `gorm:"default:false;comment:是否上架;not null "`
    ShipFree bool  `gorm:"default:false;comment:是否免运费; not null"`
    ShipID   int32 `gorm:"type:int;comment:运费模版ID;not null"`
    IsNew    bool  `gorm:"default:false;comment:是否新品;not null"`
    IsHot    bool  `gorm:"comment:是否热卖商品;default:false;not null"`

    ClickNum int64 `gorm:"default:0;type:int; comment 商品详情点击数"`
    SoldNum  int64 `gorm:"default:0;type:int; comment 商品销售数"`
    FavNum   int64 `gorm:"default:0;type:int; comment 商品收藏数"`

    // 售前服务信息、售后服务信息、商品促销活动信息
}

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

// NewGoodsRepo .
func NewGoodsRepo(data *Data, logger log.Logger) biz.GoodsRepo {
    return &goodsRepo{
        data: data,
        log:  log.NewHelper(logger),
    }
}

func (p *Goods) ToDomain() *domain.Goods {
    return &domain.Goods{
        ID:              p.ID,
        CategoryID:      p.CategoryID,
        BrandsID:        p.BrandsID,
        TypeID:          p.TypeID,
        Name:            p.Name,
        NameAlias:       p.NameAlias,
        GoodsSn:         p.GoodsSn,
        GoodsTags:       p.GoodsTags,
        MarketPrice:     p.MarketPrice,
        GoodsBrief:      p.GoodsBrief,
        GoodsFrontImage: p.GoodsFrontImage,
        GoodsImages:     p.GoodsImages,
        OnSale:          p.OnSale,
        ShipFree:        p.ShipFree,
        ShipID:          p.ShipID,
        IsNew:           p.IsNew,
        IsHot:           p.IsHot,
        ClickNum:        p.ClickNum,
        SoldNum:         p.SoldNum,
        FavNum:          p.FavNum,
    }
}
  • 新建 goods_sku.go 文件
package data

import (
    "github.com/go-kratos/kratos/v2/log"
    "golang.org/x/net/context"
    "goods/internal/biz"
    "goods/internal/domain"
)

// GoodsSku 商品SKU 表
type GoodsSku struct {
    BaseFields
    GoodsID        int64  `gorm:"index:goods_id;type:int;comment:商品ID;not null"`
    GoodsSn        string `gorm:"type:varchar(100);not null;comment:商品编号"`
    GoodsName      string `gorm:"type:varchar(100);not null;comment:商品名称"`
    SkuName        string `gorm:"type:varchar(100);comment:SKU名称;not null"`
    SkuCode        string `gorm:"type:varchar(100);comment:SKUCode;not null"`
    BarCode        string `gorm:"type:varchar(100);comment:条码;not null"`
    Price          int64  `gorm:"type:int;comment:商品售价;not null"`
    PromotionPrice int64  `gorm:"type:int;comment:商品促销售价;not null"`
    Points         int64  `gorm:"type:int;comment:赠送积分;not null"`
    RemarksInfo    string `gorm:"type:varchar(100);comment:备注信息;not null"`
    Pic            string `gorm:"type:varchar(500);not null;comment:规格参数对应的图片" json:"pic"`
    OnSale         bool   `gorm:"comment:是否上架;default:false;not null"`
    AttrInfo       string `gorm:"type:varchar(2000);comment:商品属性信息JSON;not null"`
    Inventory      int64  `gorm:"type:int;comment:商品SKU库存冗余字段;not null"`
}

// GoodsSpecificationSku 商品规格和商品Sku关联表
type GoodsSpecificationSku struct {
    BaseFields
    SkuID           int64  `gorm:"index:sku_id;type:int;comment:商品SKU_ID;not null"`
    SkuCode         string `gorm:"type:varchar(100);comment:商品SKU_Code;not null"`
    SpecificationId int64  `gorm:"index:specification_id;type:int;comment:商品规格ID;not null"`
    ValueId         int64  `gorm:"index:value_id;type:int;comment:商品规格值表ID;not null"`
}

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

// NewGoodsSkuRepoRepo .
func NewGoodsSkuRepoRepo(data *Data, logger log.Logger) biz.GoodsSkuRepo {
    return &goodsSkuRepo{
        data: data,
        log:  log.NewHelper(logger),
    }
}

func (p *GoodsSku) ToDomain() *domain.GoodsSku {
    return &domain.GoodsSku{
        ID:             p.ID,
        GoodsID:        p.GoodsID,
        GoodsSn:        p.GoodsSn,
        GoodsName:      p.GoodsName,
        SkuName:        p.SkuName,
        SkuCode:        p.SkuCode,
        BarCode:        p.BarCode,
        Price:          p.Price,
        PromotionPrice: p.PromotionPrice,
        Points:         p.Points,
        RemarksInfo:    p.RemarksInfo,
        Pic:            p.Pic,
        Inventory:      p.Inventory,
        OnSale:         p.OnSale,
        AttrInfo:       p.AttrInfo,
    }
}
  • 新建 inventory.go 文件

添加商品 sku 库存的方法比较简单这些先写了。

package data

import (
    "context"
    "github.com/go-kratos/kratos/v2/log"
    "goods/internal/biz"
    "goods/internal/domain"
)

type GoodsInventory struct {
    BaseFields
    SkuID     int64 `gorm:"index:sku_id;type:int;comment:商品SKU_ID;not null"`
    Inventory int64 `gorm:"type:int;comment:商品库存;not null"`
}

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

// NewInventoryRepo .
func NewInventoryRepo(data *Data, logger log.Logger) biz.InventoryRepo {
    return &inventoryRepo{
        data: data,
        log:  log.NewHelper(logger),
    }
}

func (p *GoodsInventory) ToDomain() *domain.Inventory {
    return &domain.Inventory{
        ID:        p.ID,
        SkuID:     p.SkuID,
        Inventory: p.Inventory,
    }
}

func (i inventoryRepo) Create(ctx context.Context, inventory *domain.Inventory) (*domain.Inventory, error) {
    info := GoodsInventory{
        SkuID:     inventory.SkuID,
        Inventory: inventory.Inventory,
    }
    if err := i.data.DB(ctx).Save(&info).Error; err != nil {
        return nil, err
    }
    return info.ToDomain(), nil
}

新建domain 层下的文件

  • goods.go
package domain

type Goods struct {
    ID              int64
    CategoryID      int32
    BrandsID        int32
    TypeID          int64
    Name            string
    NameAlias       string
    GoodsSn         string
    GoodsTags       string
    MarketPrice     int64
    GoodsBrief      string
    GoodsFrontImage string
    GoodsImages     []string
    OnSale          bool
    ShipFree        bool
    ShipID          int32
    IsNew           bool
    IsHot           bool
    ClickNum        int64
    SoldNum         int64
    FavNum          int64
    Sku             []*GoodsSku
}

type GoodsInfoResponse struct {
    GoodsID int64
}
  • goods_sku.go
package domain

type GoodsSku struct {
    ID             int64
    GoodsID        int64
    GoodsSn        string
    GoodsName      string
    SkuName        string
    SkuCode        string
    BarCode        string
    Price          int64
    PromotionPrice int64
    Points         int64
    RemarksInfo    string
    Pic            string
    Inventory      int64
    OnSale         bool
    AttrInfo       string
    Specification  []*SpecificationInfo
    GroupAttr      []*GroupAttr
}

type SpecificationInfo struct {
    SpecificationID      int64
    SpecificationValueID int64
}

type GroupAttr struct {
    GroupId   int64   `json:"group_id"`
    GroupName string  `json:"group_name"`
    Attr      []*Attr `json:"attr"`
}

type Attr struct {
    AttrID        int64  `json:"attr_id"`
    AttrName      string `json:"attr_name"`
    AttrValueID   int64  `json:"attr_value_id"`
    AttrValueName string `json:"attr_value_name"`
}

type GoodsSpecificationSku struct {
    ID              int64
    SkuID           int64
    SkuCode         string
    SpecificationId int64
    ValueId         int64
}
  • inventory.go
package domain

type Inventory struct {
    ID        int64
    SkuID     int64
    Inventory int64
}

构造商品创建方法

  • 修改 goods.proto 文件
syntax = "proto3";

...

service Goods {
    rpc CreateGoods(CreateGoodsRequest) returns (CreateGoodsResponse);
}


message CreateGoodsRequest {
  int64 id = 1;
  int32 categoryId = 2 [(validate.rules).int32.gte = 1];
  int32 brandId = 3 [(validate.rules).int32.gte = 1];
  int64 typeId = 4 [(validate.rules).int64.gte = 1];
  string name = 5 [(validate.rules).string.min_len = 1];
  string nameAlias = 6;
  string goodsTags = 7;
  string goodsSn = 8 [(validate.rules).string.min_len = 1];
  int64 shopPrice = 9;
  int64 marketPrice = 10;
  int64 inventory = 11;
  string goodsBrief = 12;
  string goodsFrontImage = 13;
  repeated string goodsImages = 14;
  bool shipFree = 15;
  int32 shipId = 16;
  bool isNew = 17;
  bool isHot = 18;
  bool onSale = 19;
  // 根据商品类型 选择商品规格和商品属性信息 
  message goodsSku {
    int64 id = 1;
    int64 goodsId = 2;
    string skuName = 3 [(validate.rules).string.min_len = 1];
    string code = 4 [(validate.rules).string.min_len = 1];
    string barCode = 5 [(validate.rules).string.min_len = 1];
    int64 price = 6;
    int64 promotionPrice = 7;
    int64 points = 8;
    string image = 9;
    int32 sort = 10;
    int64 inventory = 11;
    // 商品规格
    message specification {
      int64 sId = 1 [(validate.rules).int64.gte = 1];
      int64 vId = 2 [(validate.rules).int64.gte = 1];
    }
    repeated specification specificationInfo = 12;
    // 商品属性组
    message groupAttr {
      int64 groupId = 1 [(validate.rules).int64.gte = 1];
      string groupName = 2 [(validate.rules).string.min_len = 1];
      message attr {
        int64 attrId = 1 [(validate.rules).int64.gte = 1];
        string attrName = 2 [(validate.rules).string.min_len = 1];
        int64 attrValueId = 3 [(validate.rules).int64.gte = 1];
        string attrValueName = 4 [(validate.rules).string.min_len = 1];
      }
      repeated attr attrInfo = 3;
    }
    repeated groupAttr groupAttrInfo = 13;
  }
  repeated goodsSku sku = 20;
}


message CreateGoodsResponse {
  int64 ID = 1;
}
  • 修改 service 目录下的 goods.go
package service

import (
    "context"
    v1 "goods/api/goods/v1"
    "goods/internal/domain"
)

// CreateGoods 创建商品
func (g *GoodsService) CreateGoods(ctx context.Context, r *v1.CreateGoodsRequest) (*v1.CreateGoodsResponse, error) {
    var goodsSku []*domain.GoodsSku
    for _, sku := range r.Sku {
        res := &domain.GoodsSku{
            GoodsName:      r.Name,
            GoodsSn:        r.GoodsSn,
            SkuName:        sku.SkuName,
            SkuCode:        sku.Code,
            BarCode:        sku.BarCode,
            Price:          sku.Price,
            PromotionPrice: sku.PromotionPrice,
            Points:         sku.Points,
            Pic:            sku.Image,
            Inventory:      sku.Inventory,
            OnSale:         r.OnSale,
        }

        for _, specification := range sku.SpecificationInfo {
            s := &domain.SpecificationInfo{
                SpecificationID:      specification.SId,
                SpecificationValueID: specification.VId,
            }
            res.Specification = append(res.Specification, s)
        }
        for _, attrGroup := range sku.GroupAttrInfo {
            group := &domain.GroupAttr{
                GroupId:   attrGroup.GroupId,
                GroupName: attrGroup.GroupName,
            }
            for _, attr := range attrGroup.AttrInfo {
                s := &domain.Attr{
                    AttrID:        attr.AttrId,
                    AttrName:      attr.AttrName,
                    AttrValueID:   attr.AttrValueId,
                    AttrValueName: attr.AttrValueName,
                }
                group.Attr = append(group.Attr, s)
            }
            res.GroupAttr = append(res.GroupAttr, group)
        }
        goodsSku = append(goodsSku, res)
    }

    goodsInfo := &domain.Goods{
        ID:              r.Id,
        CategoryID:      r.CategoryId,
        BrandsID:        r.BrandId,
        TypeID:          r.TypeId,
        Name:            r.Name,
        NameAlias:       r.NameAlias,
        GoodsSn:         r.GoodsSn,
        GoodsTags:       r.GoodsTags,
        MarketPrice:     r.MarketPrice,
        GoodsBrief:      r.GoodsBrief,
        GoodsFrontImage: r.GoodsFrontImage,
        GoodsImages:     r.GoodsImages,
        OnSale:          r.OnSale,
        ShipFree:        r.ShipFree,
        ShipID:          r.ShipId,
        IsNew:           r.IsNew,
        IsHot:           r.IsHot,
        Sku:             goodsSku,
    }

    result, err := g.g.CreateGoods(ctx, goodsInfo)
    if err != nil {
        return nil, err
    }
    return &v1.CreateGoodsResponse{ID: result.GoodsID}, nil

}
  • 修改 biz 目录下的 goods.go
package biz

import (
    "context"
    "encoding/json"
    "errors"
    "github.com/go-kratos/kratos/v2/log"
    "goods/internal/domain"
)

type GoodsRepo interface {
    CreateGoods(ctx context.Context, goods *domain.Goods) (*domain.Goods, error)
}

type GoodsUsecase struct {
    repo              GoodsRepo
    tr                Transaction
    skuRepo           GoodsSkuRepo
    categoryRepo      CategoryRepo
    brandRepo         BrandRepo
    typeRepo          GoodsTypeRepo
    specificationRepo SpecificationRepo
    goodsAttrRepo     GoodsAttrRepo
    inventoryRepo     InventoryRepo
    log               *log.Helper
}

func NewGoodsUsecase(repo GoodsRepo, skuRepo GoodsSkuRepo, tx Transaction, gRepo GoodsTypeRepo, cRepo CategoryRepo,
    bRepo BrandRepo, sRepo SpecificationRepo, aRepo GoodsAttrRepo, iRepo InventoryRepo, logger log.Logger) *GoodsUsecase {

    return &GoodsUsecase{
        repo:              repo,
        skuRepo:           skuRepo,
        tr:                tx,
        typeRepo:          gRepo,
        categoryRepo:      cRepo,
        brandRepo:         bRepo,
        specificationRepo: sRepo,
        goodsAttrRepo:     aRepo,
        inventoryRepo:     iRepo,
        log:               log.NewHelper(logger),
    }
}

func (g GoodsUsecase) CreateGoods(ctx context.Context, r *domain.Goods) (*domain.GoodsInfoResponse, error) {
    var (
        err   error
        goods *domain.Goods
    )
    // 判断品牌是否存在
    _, err = g.brandRepo.IsBrandByID(ctx, r.BrandsID)
    if err != nil {
        return nil, errors.New("品牌不存在")
    }

    // 判断分类是否存在
    _, err = g.categoryRepo.GetCategoryByID(ctx, r.CategoryID)
    if err != nil {
        return nil, errors.New("分类不存在")
    }
    // 判断商品类型是否存在
    _, err = g.typeRepo.IsExistsByID(ctx, r.TypeID)
    if err != nil {
        return nil, errors.New("商品类型不存在")
    }
    // 判断商品规格和属性是否存在
    for _, sku := range r.Sku {
        var sIDs []*int64
        for _, info := range sku.Specification {
            sIDs = append(sIDs, &info.SpecificationID)
        }

        specList, err := g.specificationRepo.ListByIds(ctx, sIDs...)
        if err != nil {
            return nil, err
        }
        for _, sId := range sIDs {
            info := specList.FindById(*sId)
            if info == nil {
                return nil, errors.New("商品规格不存在")
            }
        }
        var attrIDs []int64
        for _, attr := range sku.GroupAttr {
            for _, id := range attr.Attr {
                attrIDs = append(attrIDs, id.AttrID)
            }
        }
        attrList, err := g.goodsAttrRepo.ListByIds(ctx, attrIDs...)
        if err != nil {
            return nil, err
        }

        for _, attr := range sku.GroupAttr {
            for _, id := range attr.Attr {
                attrIDs = append(attrIDs, id.AttrID)
                true := attrList.IsNotExist(attr.GroupId, id.AttrID)
                if true {
                    return nil, errors.New("商品属性不存在")
                }
            }
        }
    }

    err = g.tr.ExecTx(ctx, func(ctx context.Context) error {
        // 更新商品表
        goods, err = g.repo.CreateGoods(ctx, &domain.Goods{
            CategoryID:      r.CategoryID,
            BrandsID:        r.BrandsID,
            TypeID:          r.TypeID,
            Name:            r.Name,
            NameAlias:       r.NameAlias,
            GoodsSn:         r.GoodsSn,
            GoodsTags:       r.GoodsTags,
            MarketPrice:     r.MarketPrice,
            GoodsBrief:      r.GoodsBrief,
            GoodsFrontImage: r.GoodsFrontImage,
            GoodsImages:     r.GoodsImages,
            OnSale:          r.OnSale,
            IsNew:           r.IsNew,
            IsHot:           r.IsHot,
            ShipFree:        r.ShipFree,
            ShipID:          r.ShipID,
        })
        if err != nil {
            return err
        }
        // 更新商品 SKU 表
        for _, v := range r.Sku {
            res := &domain.GoodsSku{
                GoodsID:        goods.ID,
                GoodsSn:        goods.GoodsSn,
                GoodsName:      goods.Name,
                SkuName:        v.SkuName,
                SkuCode:        v.SkuCode,
                BarCode:        v.BarCode,
                Price:          v.Price,
                PromotionPrice: v.PromotionPrice,
                Points:         v.Points,
                RemarksInfo:    v.RemarksInfo,
                Pic:            v.Pic,
                Inventory:      v.Inventory,
                OnSale:         v.OnSale,
            }

            goodsAttr, err := json.Marshal(v.GroupAttr)
            if err != nil {
                return err
            }
            res.AttrInfo = string(goodsAttr)

            // 插入 sku 表
            skuInfo, err := g.skuRepo.Create(ctx, res)
            if err != nil {
                return err
            }

            // 插入库存表
            _, err = g.inventoryRepo.Create(ctx, &domain.Inventory{
                SkuID:     skuInfo.ID,
                Inventory: skuInfo.Inventory,
            })
            if err != nil {
                return err
            }
            // 插入 sku 规格关联关系表
            var skuRelation []*domain.GoodsSpecificationSku
            for _, spec := range v.Specification {
                skuRelation = append(skuRelation, &domain.GoodsSpecificationSku{
                    SkuID:           skuInfo.ID,
                    SkuCode:         skuInfo.SkuCode,
                    SpecificationId: spec.SpecificationID,
                    ValueId:         spec.SpecificationValueID,
                })
            }

            // 插入商品规格关联关系表
            err = g.skuRepo.CreateSkuRelation(ctx, skuRelation)
            if err != nil {
                return err
            }

        }
        return nil
    })

    if err != nil {
        return nil, err
    }
    return &domain.GoodsInfoResponse{GoodsID: goods.ID}, nil
}

实现业务逻辑方法

  • 判断品牌

biz 目录下的 brand.go 新增方法

package biz

...

type BrandRepo interface {

    ...

    IsBrandByID(context.Context, int32) (*domain.Brand, error)

}

data 录下的 brand.go 文件中实现 IsBrandByID 方法

package data

...


func (r *BrandRepo) IsBrandByID(ctx context.Context, id int32) (*domain.Brand, error) {
    var b Brand
    if err := r.data.db.Table("brands").Where("id = ?", id).First(&b).Error; err != nil {
        return nil, err
    }

    return b.ToDomain(), nil
}
  • 判断分类

biz 目录下的 category.go 新增方法

package biz

...

type CategoryRepo interface {
    ...

    GetCategoryByID(ctx context.Context, id int32) (*CategoryInfo, error)
}

data 目录下的category.go 文件中实现 GetCategoryByID


package data

...


func (r *CategoryRepo) GetCategoryByID(ctx context.Context, id int32) (*biz.CategoryInfo, error) {
    var categories Category
    if res := r.data.db.First(&categories, id); res.RowsAffected == 0 {
        return nil, errors.New("商品分类不存在")
    }

    info := &biz.CategoryInfo{
        ID:             categories.ID,
        Name:           categories.Name,
        ParentCategory: categories.ParentCategoryID,
        Level:          categories.Level,
        IsTab:          categories.IsTab,
        Sort:           categories.Sort,
    }
    return info, nil
}
  • 查询商品类型

biz 目录下的 goods_type.go 新增方法

package biz

...

type GoodsTypeRepo interface {

    ...

    IsExistsByID(context.Context, int64) (*domain.GoodsType, error)
}

data 目录下的goods_type.go 文件中实现 IsExistsByID

package data

...

func (g *goodsTypeRepo) IsExistsByID(ctx context.Context, typeID int64) (*domain.GoodsType, error) {
    var goodsType GoodsType
    if res := g.data.db.First(&goodsType, typeID); res.RowsAffected == 0 {
        return nil, errors.New("商品类型不存在")
    }
    return goodsType.ToDomain(), nil
}
  • 判断商品规格和属性

biz 目录下的 specifications.go 新增方法

package biz

...


type SpecificationRepo interface {

    ...

    ListByIds(ctx context.Context, id ...*int64) (domain.SpecificationList, error)
}

data 目录下的specifications.go 文件中实现 ListByIds

package data

...

func (g *specificationRepo) ListByIds(ctx context.Context, id ...*int64) (domain.SpecificationList, error) {
    var l []*SpecificationsAttr
    if err := g.data.DB(ctx).Where("id IN (?)", id).Find(&l).Error; err != nil {
        return nil, err
    }

    var res domain.SpecificationList
    for _, item := range l {
        res = append(res, item.ToDomain())
    }
    return res, nil
}

domain 目录下的 specification.go 文件中编写验证方法

package domain

...

type SpecificationList []*Specification

func (p SpecificationList) FindById(id int64) *Specification {
    for _, item := range p {
        if item.ID == id {
            return item
        }
    }
    return nil
}

biz 目录下的 goods_attr.go 新增方法

package biz

...

type GoodsAttrRepo interface {

    ...

    ListByIds(ctx context.Context, id ...int64) (domain.GoodsAttrList, error)
}

data 目录下的goods_attr.go 文件中实现 ListByIds

package data

...


func (g *goodsAttrRepo) ListByIds(ctx context.Context, ids ...int64) (domain.GoodsAttrList, error) {
    var l []*GoodsAttr
    if err := g.data.DB(ctx).Where("id IN (?)", ids).Find(&l).Error; err != nil {
        return nil, errors.New("属性不存在")
    }

    var res domain.GoodsAttrList
    for _, item := range l {
        res = append(res, item.ToDomain())
    }
    return res, nil
}

domain 目录下的 goods_attr.go 文件中编写验证方法

package domian

...

type GoodsAttrList []*GoodsAttr

func (p GoodsAttrList) IsNotExist(groupId, attrId int64) bool {
    for _, item := range p {
        if item.GroupID != groupId && item.ID != attrId {
            return true
        }
    }
    return false
}

编写入库的业务逻辑

  • data 目录下的 goods.go实现 CreateGoods 方法

package data

...

func (g goodsRepo) CreateGoods(c context.Context, goods *domain.Goods) (*domain.Goods, error) {
    product := &Goods{
        CategoryID:      goods.CategoryID,
        BrandsID:        goods.BrandsID,
        TypeID:          goods.TypeID,
        Name:            goods.Name,
        NameAlias:       goods.NameAlias,
        GoodsSn:         goods.GoodsSn,
        GoodsTags:       goods.GoodsTags,
        MarketPrice:     goods.MarketPrice,
        GoodsBrief:      goods.GoodsBrief,
        GoodsFrontImage: goods.GoodsFrontImage,
        GoodsImages:     goods.GoodsImages,
        OnSale:          goods.OnSale,
        ShipFree:        goods.ShipFree,
        ShipID:          goods.ShipID,
        IsNew:           goods.IsNew,
        IsHot:           goods.IsHot,
    }

    result := g.data.DB(c).Save(product)
    if result.Error != nil {
        return nil, result.Error
    }
    return product.ToDomain(), nil
}
  • biz目录下新建goods_sku.go
package biz

import (
    "context"
    "github.com/go-kratos/kratos/v2/log"
    "goods/internal/domain"
)

type Sku struct {
    ID             int64
    GoodsID        int64
    GoodsSn        string
    GoodsName      string
    SkuName        string
    SkuCode        string
    BarCode        string
    Price          int64
    PromotionPrice int64
    Points         int64
    RemarksInfo    string
    Pic            string
    Inventory      int64
    OnSale         bool
    AttrInfo       string
}

type GoodsSkuRepo interface {
    Create(context.Context, *domain.GoodsSku) (*domain.GoodsSku, error)
    CreateSkuRelation(context.Context, []*domain.GoodsSpecificationSku) error
}

type GoodsSkuUsecase struct {
    repo GoodsSkuRepo
    log  *log.Helper
}

func NewGoodsSkuUsecase(repo GoodsSkuRepo, logger log.Logger) *GoodsSkuUsecase {
    return &GoodsSkuUsecase{repo: repo, log: log.NewHelper(logger)}
}
  • data 目录下的 goods_sku.go实现 Create 方法和 CreateSkuRelation
package data

...


func (g *goodsSkuRepo) Create(ctx context.Context, req *domain.GoodsSku) (*domain.GoodsSku, error) {
    sku := &GoodsSku{
        GoodsID:        req.GoodsID,
        GoodsSn:        req.GoodsSn,
        GoodsName:      req.GoodsName,
        SkuName:        req.SkuName,
        SkuCode:        req.SkuCode,
        BarCode:        req.BarCode,
        Price:          req.Price,
        PromotionPrice: req.PromotionPrice,
        Points:         req.Points,
        RemarksInfo:    req.RemarksInfo,
        Pic:            req.Pic,
        OnSale:         req.OnSale,
        AttrInfo:       req.AttrInfo,
        Inventory:      req.Inventory,
    }

    if err := g.data.DB(ctx).Save(sku).Error; err != nil {
        return nil, err
    }
    return sku.ToDomain(), nil
}

func (g *goodsSkuRepo) CreateSkuRelation(ctx context.Context, req []*domain.GoodsSpecificationSku) error {
    var info []*GoodsSpecificationSku
    for _, sku := range req {
        i := GoodsSpecificationSku{
            SkuID:           sku.SkuID,
            SkuCode:         sku.SkuCode,
            SpecificationId: sku.SpecificationId,
            ValueId:         sku.ValueId,
        }
        info = append(info, &i)
    }
    result := g.data.DB(ctx).Table("goods_specification_skus").Save(&info)
    return result.Error
}
  • biz目录下新建inventory.go文件
package biz

import (
    "context"
    "github.com/go-kratos/kratos/v2/log"
    "goods/internal/domain"
)

type InventoryRepo interface {
    Create(context.Context, *domain.Inventory) (*domain.Inventory, error)
}

type InventoryUsecase struct {
    repo InventoryRepo
    log  *log.Helper
}

func NewInventoryUsecase(repo InventoryRepo, logger log.Logger) *InventoryUsecase {
    return &InventoryUsecase{repo: repo, log: log.NewHelper(logger)}
}

创建库存的方法在上面已经写过了,商品规格和商品 sku 的关联关系创建方法上面也已经写过了。

测试

没错还是通过 BloomRPC 工具进行测试。

如图创建成功了,这里的参数太多了,如果你跟我用的是同一个工具,它会自动构建所需的参数,你只需根据自己的需要简单修改一下就可以了。

结束语

本篇只提供了一个商品创建的方法,其他方法没有在文章中体现,单元测试方法也没有编写,重复性的工作这里就不编写了,通过前几篇的文章,相信你可以自己完善剩余的方法。

经过这么多天的编写,商品服务算是告一断落了,当然商品管理的 HTTP 服务还是会编写的,以后会新建一个商城后台管理的前端的项目来管理商品,他们是通过 HTTP 服务进行的,并不是直接调用这里的 rpc 服务。后期也会加入 Elasticsearch 搜索服务进行商品检索。

下一篇准备开始写购物车和订单服务,敬请期待

感谢您的耐心阅读,动动手指点个赞吧。

本作品采用《CC 协议》,转载必须注明作者和本文链接
微信搜索:上帝喜爱笨人
讨论数量: 1

哪位大佬能实现 biz 层 CreateGoods 方法的单元测试?感谢

2年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!