大家好,今天咱们继续完善商品服务里的商品规格模块。
众所周知,一个电商的商品设计是比较复杂的,咱们这里不过多的深究商品设计的每个表是否合理,是否漏写之类的问题,主要是为了搞明白 kratos 的使用和微服务相关的调用关系。当然我真正的编写时也会尽可能的让此项目的商品设计合理一些。但大量的表设计呀,重复性的 curd 就不会再文章中体现了,具体的代码参看 GitHub 上的源码。当然你觉得不合理的地方,欢迎给项目提 PR。
商品规格参数 商品参数,也有人管它们叫商品规格参数,信息如下图所示,一般可以分为规格分组、规格属性及属性值。这些特殊的规格参数,会影响商品 SKU 的信息, 我们选择不同的颜色、版本等规格,会影响我们 SKU 的记录,也就是对应的销售价格和商品的库存量。
商品属性参数信息如下图所示,一般可以分为分组、属性及属性值。这些信息基本不影响商品 SKU,只是作为商品的一些参数信息展示。
咱们这里为了方便商品的管理,使得数据更加有规律,实现更好的弹性设计,各自设置为一个模块。然后每个单独的模块都会跟上一篇文章中创建的商品类型进行关联。在创建一个具体的商品的时候,更好的使用商品类型下的商品规格以及商品属性信息。
编写代码 设计商品规格表
data 层新增 specifications.go
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package dataimport ( "context" "errors" "goods/internal/biz" "goods/internal/domain" "time" "github.com/go-kratos/kratos/v2/log" "gorm.io/gorm" )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"` }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 }func NewSpecificationRepo (data *Data, logger log.Logger) biz .SpecificationRepo { return &specificationRepo{ data: data, log: log.NewHelper(logger), } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package serviceimport ( "context" v1 "goods/api/goods/v1" "goods/internal/domain" )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 编写一个逻辑吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package domaintype 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package bizimport ( "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 tx Transaction 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), } }func (s *SpecificationUsecase) CreateSpecification (ctx context.Context, r *domain.Specification) (int64 , error) { var ( id int64 err error ) if r.IsTypeIDEmpty() { return id, errors.New("请选择商品类型进行绑定" ) } 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 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ...type GoodsTypeRepo interface { ... IsExistsByID(context.Context, int64 ) (*domain.GoodsType, error) } ... ...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 的事务 的文章。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 ...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 }
测试 还是使用上一次介绍的工具,如图:
你可以少写参数或故意写错一些参数来验证,写的判断逻辑是否生效,这里就不演示了。
结束语 本篇只提供了一个商品规格参数的创建方法,其他方法没有在文章中体现,单元测试方法也没有编写,重复性的工作这里就不编写了,通过前几篇的文章,相信你可以自己完善剩余的方法。
下一篇开始编写本文中提到的商品属性,敬请期待。
感谢您的耐心阅读,动动手指点个赞吧。
关注我获取更新