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

本文最后更新于:2 个月前

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

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

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

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

商品信息

设计商品表

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

base.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
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文件内容

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
72
73
74
75
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 文件
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
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 库存的方法比较简单这些先写了。

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
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
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
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
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
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
1
2
3
4
5
6
7
package domain

type Inventory struct {
ID int64
SkuID int64
Inventory int64
}

构造商品创建方法

  • 修改 goods.proto 文件
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
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
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
72
73
74
75
76
77
78
79
80
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
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
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 新增方法

1
2
3
4
5
6
7
8
9
10
11
package biz

...

type BrandRepo interface {

...

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

}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 新增方法

1
2
3
4
5
6
7
8
9
package biz

...

type CategoryRepo interface {
...

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

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 新增方法

1
2
3
4
5
6
7
8
9
10
package biz

...

type GoodsTypeRepo interface {

...

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

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

1
2
3
4
5
6
7
8
9
10
11
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 新增方法

1
2
3
4
5
6
7
8
9
10
11
package biz

...


type SpecificationRepo interface {

...

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 文件中编写验证方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 新增方法

1
2
3
4
5
6
7
8
9
10
package biz

...

type GoodsAttrRepo interface {

...

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 文件中编写验证方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 方法
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

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
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
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
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
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文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 搜索服务进行商品检索。

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

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