Go-kratos 框架商城微服务实战之商品服务 (八) 商品规格

大家好,今天咱们继续完善商品服务里的商品规格模块。

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

商品规格参数

商品参数,也有人管它们叫商品规格参数,信息如下图所示,一般可以分为规格分组、规格属性及属性值。这些特殊的规格参数,会影响商品 SKU 的信息, 我们选择不同的颜色、版本等规格,会影响我们 SKU 的记录,也就是对应的销售价格和商品的库存量。

商品属性参数信息如下图所示,一般可以分为分组、属性及属性值。这些信息基本不影响商品 SKU,只是作为商品的一些参数信息展示。

咱们这里为了方便商品的管理,使得数据更加有规律,实现更好的弹性设计,各自设置为一个模块。然后每个单独的模块都会跟上一篇文章中创建的商品类型进行关联。在创建一个具体的商品的时候,更好的使用商品类型下的商品规格以及商品属性信息。

编写代码

设计商品规格表

  • data 层新增 specifications.go 文件
package data

import (
    "context"
    "errors"
    "goods/internal/biz"
    "goods/internal/domain"
    "time"

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

// SpecificationsAttr 规格参数信息表
type SpecificationsAttr struct {
    ID        int64          `gorm:"primarykey;type:int" json:"id"`
    TypeID    int64          `gorm:"index:type_id;type:int;comment:商品类型ID;not null"`
    Name      string         `gorm:"type:varchar(250);not null;comment:规格参数名称" json:"name"`
    Sort      int32          `gorm:"comment:规格排序;default:99;not null;type:int" json:"sort"`
    Status    bool           `gorm:"comment:参数状态;default:false" json:"status"`
    IsSKU     bool           `gorm:"comment:是否通用的SKU持有;default:false" json:"is_sku"`
    IsSelect  bool           `gorm:"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"`
}

// SpecificationsAttrValue 规格参数信息选项表
type SpecificationsAttrValue struct {
    ID        int64          `gorm:"primarykey;type:int" json:"id"`
    AttrId    int64          `gorm:"index:attr_id;type:int;comment:规格ID;not null"`
    Value     string         `gorm:"type:varchar(250);not null;comment:规格参数信息值" json:"value"`
    Sort      int32          `gorm:"comment:规格参数值排序;default:99;not null;type:int" json:"sort"`
    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 specificationRepo struct {
    data *Data
    log  *log.Helper
}

// NewSpecificationRepo .
func NewSpecificationRepo(data *Data, logger log.Logger) biz.SpecificationRepo {
    return &specificationRepo{
        data: data,
        log:  log.NewHelper(logger),
    }
}
  • goods.proto 新增 rpc 方法
syntax = "proto3";

...

service Goods {

    ...

    // 商品规格或属性的信息
    rpc CreateGoodsSpecification(SpecificationRequest) returns(SpecificationResponse);
}

...

message SpecificationValue {
  int64 id = 1;
  int64 attrId = 2;
  string value = 3 [(validate.rules).string.min_len = 3];
  int32 sort = 4 [(validate.rules).string.min_len = 3];
}

message SpecificationRequest {
  int64 id = 1;
  int64 typeId = 2 [(validate.rules).string.min_len = 1];
  string name = 3 [(validate.rules).string.min_len = 3];
  int32 sort = 4 [(validate.rules).string.min_len = 1];
  bool status = 5;
  bool isSku = 6;
  bool isSelect = 7;
  repeated SpecificationValue specificationValue = 8;
}

message SpecificationResponse {
  int64 id = 1;
}

...
  • service 层新增 specifications.go
package service

import (
    "context"

    v1 "goods/api/goods/v1"
    "goods/internal/domain"
)

// CreateGoodsSpecification 创建商品规格版本
func (g *GoodsService) CreateGoodsSpecification(ctx context.Context, r *v1.SpecificationRequest) (*v1.SpecificationResponse, error) {
    var value []*domain.SpecificationValue
    // 组织规格参数值
    if r.SpecificationValue != nil {
        for _, v := range r.SpecificationValue {
            res := &domain.SpecificationValue{
                Value: v.Value,
                Sort:  v.Sort,
            }
            value = append(value, res)
        }
    }

    id, err := g.s.CreateSpecification(ctx, &domain.Specification{
        TypeID:             r.TypeId,
        Name:               r.Name,
        Sort:               r.Sort,
        Status:             r.Status,
        IsSKU:              r.IsSku,
        IsSelect:           r.IsSelect,
        SpecificationValue: value,
    })

    if err != nil {
        return nil, err
    }
    return &v1.SpecificationResponse{
        Id: id,
    }, nil
}
  • domain 层新增 specifications.go

这里上一篇介绍的 domain 又出现,开始在 domain 编写一个逻辑吧

package domain

type Specification struct {
    ID                 int64
    TypeID             int64
    Name               string
    Sort               int32
    Status             bool
    IsSKU              bool
    IsSelect           bool
    SpecificationValue []*SpecificationValue
}

type SpecificationValue struct {
    ID     int64
    AttrId int64
    Value  string
    Sort   int32
}

// 新增判断类型不能为空的方法
func (b *Specification) IsTypeIDEmpty() bool {
    return b.TypeID == 0
}

// 规格下面的参数不能为空的方法
func (b *Specification) IsValueEmpty() bool {
    return b.SpecificationValue == nil
}
  • biz 层新增 specifications.go
package biz

import (
    "context"
    "errors"
    "goods/internal/domain"

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

type SpecificationRepo interface {
    CreateSpecification(context.Context, *domain.Specification) (int64, error)
    CreateSpecificationValue(context.Context, int64, []*domain.SpecificationValue) error
}

type SpecificationUsecase struct {
    repo  SpecificationRepo
    gRepo GoodsTypeRepo // 加入商品类型的 repo,用来查询类型是否存在
    tx    Transaction // 引入 gorm 事务
    log   *log.Helper
}

func NewSpecificationUsecase(repo SpecificationRepo, type GoodsTypeRepo, tx Transaction,
    logger log.Logger) *SpecificationUsecase {

    return &SpecificationUsecase{
        repo:  repo,
        gRepo: type,
        tx:    tx,
        log:   log.NewHelper(logger),
    }
}

// CreateSpecification 创建商品规格
func (s *SpecificationUsecase) CreateSpecification(ctx context.Context, r *domain.Specification) (int64, error) {
    var (
        id  int64
        err error
    )
  // domain 下编写的判断 typeid 的方法
    if r.IsTypeIDEmpty() {
        return id, errors.New("请选择商品类型进行绑定")
    }

  // domain 下编写的 value 的方法
    if r.IsValueEmpty() {
        return id, errors.New("请填写商品规格下的参数")
    }

    // 去查询有没有这个类型
    _, err = s.gRepo.IsExistsByID(ctx, r.TypeID)
    if err != nil {
        return id, err
    }

    // 引入事务
    err = s.tx.ExecTx(ctx, func(ctx context.Context) error {
        id, err = s.repo.CreateSpecification(ctx, r) // 插入规格
        if err != nil {
            return err
        }

        err = s.repo.CreateSpecificationValue(ctx, id, r.SpecificationValue) // 插入规格对应的值
        if err != nil {
            return err
        }
        return nil
    })
    return id, err
}
  • 处理 good_type 的判断逻辑
// biz 层
...
type GoodsTypeRepo interface {
  ...
    IsExistsByID(context.Context, int64) (*domain.GoodsType, error)
}

...

// 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("商品类型不存在")
    }

    res := &domain.GoodsType{
        ID:        goodsType.ID,
        Name:      goodsType.Name,
        TypeCode:  goodsType.TypeCode,
        NameAlias: goodsType.NameAlias,
        IsVirtual: goodsType.IsVirtual,
        Desc:      goodsType.Desc,
        Sort:      goodsType.Sort,
    }
    return res, nil
}
  • data 层 specifications.go 新增方法

    注意这里调用 repo 的方式,用的是 g.data.DB(ctx) 而不是之前的 g.data.db,这里是因为引入了 GORM MySQL 的事务,如果你对在 kratos 使用 GORM MySQL 的事务还不是很熟悉的话,请查看我之前写的一篇 kratos 中使用 GORM MySQL 的事务 的文章。


...


func (g *specificationRepo) CreateSpecification(ctx context.Context, req *domain.Specification) (int64, error) {
    s := &SpecificationsAttr{
        TypeID:    req.TypeID,
        Name:      req.Name,
        Sort:      req.Sort,
        Status:    req.Status,
        IsSKU:     req.IsSKU,
        IsSelect:  req.IsSelect,
        CreatedAt: time.Time{},
        UpdatedAt: time.Time{},
    }
    result := g.data.DB(ctx).Save(s)
    return s.ID, result.Error
}

func (g *specificationRepo) CreateSpecificationValue(ctx context.Context, AttrId int64, req []*domain.SpecificationValue) error {
    var value []*SpecificationsAttrValue
    for _, v := range req {
        res := &SpecificationsAttrValue{
            AttrId:    AttrId,
            Value:     v.Value,
            Sort:      v.Sort,
            CreatedAt: time.Time{},
            UpdatedAt: time.Time{},
        }
        value = append(value, res)
    }
    result := g.data.DB(ctx).Create(&value)
    return result.Error
}

测试

还是使用上一次介绍的工具,如图:

你可以少写参数或故意写错一些参数来验证,写的判断逻辑是否生效,这里就不演示了。

结束语

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

下一篇开始编写本文中提到的商品属性,敬请期待。

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

本作品采用《CC 协议》,转载必须注明作者和本文链接
微信搜索:上帝喜爱笨人
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

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