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

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

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

众所周知,一个电商的商品设计是比较复杂的,咱们这里不过多的深究商品设计的每个表是否合理,是否漏写之类的问题,主要是为了搞明白 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 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 方法
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 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 编写一个逻辑吧

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 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
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 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 的判断逻辑
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
// 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 的事务 的文章。

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
}

测试

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

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

结束语

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

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

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

关注我获取更新