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

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

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

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

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

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

商品属性信息

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

编写代码

设计商品属性表

商品参数按分组的方式进行管理,除了设置一些分组选项名称以外,跟商品规格类似,其中的参数也是可以填写多个列表选项值的。比如:基本信息(属性组):机身材质(属性名称):玻璃后盖、塑胶边框(属性信息)。

  • data 层新增 goods_attr.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
package data

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

// GoodsAttrGroup 商品属性分组表 手机 -> 主体->屏幕,操作系统,网络支持,基本信息
type GoodsAttrGroup struct {
ID int64 `gorm:"primarykey;type:int" json:"id"`
GoodsTypeID int64 `gorm:"index:goods_type_id;type:int;comment:商品类型ID;not null"`
Title string `gorm:"type:varchar(100);comment:属性名;not null"`
Desc string `gorm:"type:varchar(200);comment:属性描述;default:false;not null"`
Status bool `gorm:"comment:状态;default:false;not null"`
Sort int32 `gorm:"type:int;comment:商品属性排序字段;not null"`
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"`
}

// GoodsAttr 商品属性表 主体->产品名称,上市月份,机身宽度
type GoodsAttr struct {
ID int64 `gorm:"primarykey;type:int" json:"id"`
GoodsTypeID int64 `gorm:"index:goods_type_id;type:int;comment:商品类型ID;not null"`
GroupID int64 `gorm:"index:attr_group_id;type:int;comment:商品属性分组ID;not null"`
Title string `gorm:"type:varchar(100);comment:属性名;not null"`
Desc string `gorm:"type:varchar(200);comment:属性描述;default:false;not null"`
Status bool `gorm:"comment:状态;default:false;not null"`
Sort int32 `gorm:"type:int;comment:商品属性排序字段;not null"`
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 GoodsAttrValue struct {
ID int64 `gorm:"primarykey;type:int" json:"id"`
AttrId int64 `gorm:"index:property_name_id;type:int;comment:属性表ID;not null"`
GroupID int64 `gorm:"index:attr_group_id;type:int;comment:商品属性分组ID;not null"`
Value string `gorm:"type:varchar(100);comment:属性值;not null"`
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 goodsAttrRepo struct {
data *Data
log *log.Helper
}

// NewGoodsAttrRepo .
func NewGoodsAttrRepo(data *Data, logger log.Logger) biz.GoodsAttrRepo {
return &goodsAttrRepo{
data: data,
log: log.NewHelper(logger),
}
}

// 转换为 Domain 结构体
func (p *GoodsAttrGroup) ToDomain() *domain.AttrGroup {
return &domain.AttrGroup{
ID: p.ID,
TypeID: p.GoodsTypeID,
Title: p.Title,
Desc: p.Desc,
Status: p.Status,
Sort: p.Sort,
}
}

func (p *GoodsAttr) ToDomain() *domain.GoodsAttr {
return &domain.GoodsAttr{
ID: p.ID,
TypeID: p.GoodsTypeID,
GroupID: p.GroupID,
Title: p.Title,
Sort: p.Sort,
Status: p.Status,
Desc: p.Desc,
}
}

func (p *GoodsAttrValue) ToDomain() *domain.GoodsAttrValue {
return &domain.GoodsAttrValue{
ID: p.ID,
AttrId: p.AttrId,
GroupID: p.GroupID,
Value: p.Value,
}
}

定义商品属性方法

  • 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
70
71
syntax = "proto3";

...

service Goods {

...

// 商品参数属性分组名
rpc CreateAttrGroup(AttrGroupRequest) returns(AttrGroupResponse);
// 商品参数属性名和值
rpc CreateAttrValue(AttrValueRequest) returns(AttrResponse);

}

...

message AttrGroupRequest {
int64 id = 1;
int64 typeId = 2 [(validate.rules).int64.gte = 1];
string title = 3 [(validate.rules).string.min_len = 3];
string desc = 4;
bool status = 5;
int32 sort = 6 [(validate.rules).int32.gte = 1];
}

message AttrGroupResponse {
int64 id = 1;
int64 typeId = 2;
string title = 3;
string desc = 4;
bool status = 5;
int32 sort = 6;
}


message AttrValueRequest {
int64 id = 1;
int64 attrId = 2;
int64 groupId = 3 [(validate.rules).int64.gte = 1];
string value = 4 [(validate.rules).string.min_len = 3];
}

message AttrRequest {
int64 id = 1;
int64 typeId = 2 [(validate.rules).int64.gte = 1];
int64 groupId = 3 [(validate.rules).int64.gte = 1];
string title = 4 [(validate.rules).string = {min_len: 1}];
string desc = 5;
bool status = 6;
int32 sort = 7 [(validate.rules).int32.gte = 1];
repeated AttrValueRequest attrValue = 8;
}

message AttrValueResponse {
int64 id = 1;
int64 attrId = 2;
int64 groupId = 3;
string value = 4;
}

message AttrResponse {
int64 id = 1;
int64 typeId = 2;
int64 groupId = 3;
string title = 4;
string desc = 5;
bool status = 6;
int32 sort = 7;
repeated AttrValueResponse attrValue = 8;
}

修改 makefile 文件

之前好几篇文章都没具体说明如何使用 proto-gen-validate Validate 中间件生成代码进行参数校验,有好多小伙伴问,为啥 proto 中设置了 validate 的规则,但是不生效。这里说明一下。

  • 修改服务 makefile 文件,在命令 api 后面加入:
1
2
--validate_out=paths=source_relative,lang=go:. \

修改完的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
...

api:
protoc --proto_path=. \
--proto_path=./third_party \
--go_out=paths=source_relative:. \
--go-http_out=paths=source_relative:. \
--go-grpc_out=paths=source_relative:. \
--openapi_out==paths=source_relative:. \
--validate_out=paths=source_relative,lang=go:. \
$(API_PROTO_FILES)

...
  • 修改 server 目录下 grpc.go 文件

    如果是 http 服务就修改 http.go 文件

主要是在 grpc.Middleware 中添加 validate.Validator()

具体修改如下:

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

import (

...

"github.com/go-kratos/kratos/v2/middleware/validate"

...
)

// NewGRPCServer new a gRPC server.
func NewGRPCServer(c *conf.Server, greeter *service.GoodsService, logger log.Logger) *grpc.Server {
var opts = []grpc.ServerOption{
grpc.Middleware(
recovery.Recovery(),
validate.Validator(), // 此次为新增
logging.Server(logger),
),
}

...
}

编写商品属性组相关方法

创建属性组

  • service 层新建 goods_attr.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
package service

import (
"context"

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

// CreateAttrGroup 创建属性组
func (g *GoodsService) CreateAttrGroup(ctx context.Context, r *v1.AttrGroupRequest) (*v1.AttrGroupResponse, error) {
result, err := g.ga.CreateAttrGroup(ctx, &domain.AttrGroup{
TypeID: r.TypeId,
Title: r.Title,
Desc: r.Desc,
Status: r.Status,
Sort: r.Sort,
})
if err != nil {
return nil, err
}

return &v1.AttrGroupResponse{
Id: result.ID,
TypeId: result.TypeID,
Title: result.Title,
Desc: result.Desc,
Status: result.Status,
Sort: result.Sort,
}, nil
}
  • domain 层新建 goods_attr.go 文件

定义接收参数结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package domain

type AttrGroup struct {
ID int64
TypeID int64
Title string
Desc string
Status bool
Sort int32
}

func (p AttrGroup) IsTypeIDEmpty() bool {
return p.TypeID == 0
}

  • biz 层新建 goods_attr.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
package biz

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

type GoodsAttrRepo interface {
CreateGoodsGroupAttr(context.Context, *domain.AttrGroup) (*domain.AttrGroup, error)
}

type GoodsAttrUsecase struct {
repo GoodsAttrRepo
typeRepo GoodsTypeRepo // 引入goods type 的 repo
tx Transaction // 引入事务
log *log.Helper
}

func NewGoodsAttrUsecase(repo GoodsAttrRepo, tx Transaction, gRepo GoodsTypeRepo, logger log.Logger) *GoodsAttrUsecase {
return &GoodsAttrUsecase{
repo: repo,
tx: tx,
typeRepo: gRepo,
log: log.NewHelper(logger),
}
}

func (ga *GoodsAttrUsecase) CreateAttrGroup(ctx context.Context, r *domain.AttrGroup) (*domain.AttrGroup, error) {
if r.IsTypeIDEmpty() {
return nil, errors.New("请选择商品类型进行绑定")
}

_, err := ga.typeRepo.IsExistsByID(ctx, r.TypeID)
if err != nil {
return nil, err
}

attr, err := ga.repo.CreateGoodsGroupAttr(ctx, r)
if err != nil {
return nil, err
}
return attr, nil
}
  • data 层新增 CreateGoodsGroupAttr 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package data

...

func (g *goodsAttrRepo) CreateGoodsGroupAttr(ctx context.Context, a *domain.AttrGroup) (*domain.AttrGroup, error) {
group := GoodsAttrGroup{
GoodsTypeID: a.TypeID,
Title: a.Title,
Desc: a.Desc,
Status: a.Status,
Sort: a.Sort,
}

result := g.data.db.Save(&group)
if result.Error != nil {
return nil, result.Error
}

return group.ToDomain(), nil
}

测试创建属性组

编写商品属性相关方法

创建属性信息

  • servicegoods_attr.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
package service

...


// CreateAttrValue 创建属性名称和值
func (g *GoodsService) CreateAttrValue(ctx context.Context, r *v1.AttrRequest) (*v1.AttrResponse, error) {
var value []*domain.GoodsAttrValue
for _, v := range r.AttrValue {
res := &domain.GoodsAttrValue{
GroupID: v.GroupId,
Value: v.Value,
}
value = append(value, res)
}

info, err := g.ga.CreateAttrValue(ctx, &domain.GoodsAttr{
TypeID: r.TypeId,
GroupID: r.GroupId,
Title: r.Title,
Sort: r.Sort,
Status: r.Status,
Desc: r.Desc,
GoodsAttrValue: value,
})
if err != nil {
return nil, err
}
var AttrValue []*v1.AttrValueResponse
for _, v := range info.GoodsAttrValue {
result := &v1.AttrValueResponse{
Id: v.ID,
AttrId: v.AttrId,
GroupId: v.GroupID,
Value: v.Value,
}
AttrValue = append(AttrValue, result)
}
return &v1.AttrResponse{
Id: info.ID,
TypeId: info.TypeID,
GroupId: info.GroupID,
Title: info.Title,
Desc: info.Desc,
Status: info.Status,
Sort: info.Sort,
AttrValue: AttrValue,
}, nil
}
  • domaingoods_attr.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
package domain

...


type GoodsAttr struct {
ID int64
TypeID int64
GroupID int64
Title string
Sort int32
Status bool
Desc string
GoodsAttrValue []*GoodsAttrValue
}

func (p GoodsAttr) IsTypeIDEmpty() bool {
return p.TypeID == 0
}

type GoodsAttrValue struct {
ID int64
AttrId int64
GroupID int64
Value string
}

func (p GoodsAttrValue) IsValueEmpty() bool {
return p.Value == ""
}

  • bizgoods_attr.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 biz

...

type GoodsAttrRepo interface {
CreateGoodsGroupAttr(context.Context, *domain.AttrGroup) (*domain.AttrGroup, error)
IsExistsGroupByID(ctx context.Context, id int64) (*domain.AttrGroup, error)
CreateGoodsAttr(context.Context, *domain.GoodsAttr) (*domain.GoodsAttr, error)
CreateGoodsAttrValue(context.Context, []*domain.GoodsAttrValue) ([]*domain.GoodsAttrValue, error)
}

...


// CreateAttrValue 创建商品属性和属性信息
func (ga *GoodsAttrUsecase) CreateAttrValue(ctx context.Context, r *domain.GoodsAttr) (*domain.GoodsAttr, error) {
var (
attrInfo *domain.GoodsAttr
attrValue []*domain.GoodsAttrValue
err error
)
if r.IsTypeIDEmpty() {
return nil, errors.New("请选择商品类型进行绑定")
}

_, err = ga.typeRepo.IsExistsByID(ctx, r.TypeID)
if err != nil {
return nil, err
}

_, err = ga.repo.IsExistsGroupByID(ctx, r.GroupID)
if err != nil {
return nil, err
}

// 没错这里又是引入了 事务
err = ga.tx.ExecTx(ctx, func(ctx context.Context) error {
attrInfo, err = ga.repo.CreateGoodsAttr(ctx, r)
if err != nil {
return err
}
var value []*domain.GoodsAttrValue
for _, v := range r.GoodsAttrValue {
if v.IsValueEmpty() {
return errors.New("商品属性不能为空")
}
res := &domain.GoodsAttrValue{
AttrId: attrInfo.ID,
GroupID: v.GroupID,
Value: v.Value,
}
value = append(value, res)
}
attrValue, err = ga.repo.CreateGoodsAttrValue(ctx, value)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}

return &domain.GoodsAttr{
ID: attrInfo.ID,
TypeID: attrInfo.TypeID,
GroupID: attrInfo.GroupID,
Title: attrInfo.Title,
Sort: attrInfo.Sort,
Status: attrInfo.Status,
Desc: attrInfo.Desc,
GoodsAttrValue: attrValue,
}, nil
}

  • datagoods_attr.go 文件

实现 GoodsAttrRepo 定义的方法

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
package data

...


func (g *goodsAttrRepo) IsExistsGroupByID(ctx context.Context, groupId int64) (*domain.AttrGroup, error) {
var group GoodsAttrGroup
if res := g.data.db.First(&group, groupId); res.RowsAffected == 0 {
return nil, errors.New("商品属性组不存在")
}
return group.ToDomain(), nil
}

func (g *goodsAttrRepo) CreateGoodsAttr(ctx context.Context, a *domain.GoodsAttr) (*domain.GoodsAttr, error) {
attr := GoodsAttr{
GoodsTypeID: a.TypeID,
GroupID: a.GroupID,
Title: a.Title,
Desc: a.Desc,
Status: a.Status,
Sort: a.Sort,
}

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

func (g *goodsAttrRepo) CreateGoodsAttrValue(ctx context.Context, r []*domain.GoodsAttrValue) ([]*domain.GoodsAttrValue, error) {
var attrValue []*GoodsAttrValue
for _, v := range r {
attr := GoodsAttrValue{
AttrId: v.AttrId,
GroupID: v.GroupID,
Value: v.Value,
}
attrValue = append(attrValue, &attr)
}

if err := g.data.DB(ctx).Create(&attrValue).Error; err != nil {
return nil, err
}

var res []*domain.GoodsAttrValue
for _, v := range attrValue {
value := v.ToDomain()
res = append(res, value)
}
return res, nil
}

测试新建属性信息

结束语

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

下一篇就开始真正的商品创建了,敬请期待。

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

关注我获取更新