overtop 发表于 2022-3-21 13:40:45

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

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

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

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

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

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


编写代码

设计商品属性表

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

定义数据库表结构
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 文件新增创建方法:
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];
stringdesc = 4;
bool status = 5;
int32 sort = 6 [(validate.rules).int32.gte = 1];
}

message AttrGroupResponse {
int64 id = 1;
int64 typeId = 2;
string title = 3;
stringdesc = 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}];
stringdesc = 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;
stringdesc = 5;
bool status = 6;
int32 sort = 7;
repeated AttrValueResponse attrValue = 8;
}
修改 makefile 文件


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

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

修改完的内容如下:
...

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()

具体修改如下:
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 文件
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 文件

定义接收参数结构体
package domain

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

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

biz 层新建 goods_attr.go 文件

定义处理逻辑的方法
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 方法
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
}

测试创建属性组


编写商品属性相关方法

创建属性信息

service 层 goods_attr.go文件新建方法
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
}
domain 层 goods_attr.go 文件

新建处理商品属性信息的参数结构体
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
AttrIdint64
GroupID int64
Value   string
}

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

biz 层 goods_attr.go 文件

新建处理商品属性信息的方法
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
}

data 层 goods_attr.go 文件

实现 GoodsAttrRepo 定义的方法
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
}

测试新建属性信息


结束语

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

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



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


GitHub: https://github.com/aliliin/kratos-shop
页: [1]
查看完整版本: Go-kratos 框架商城微服务实战之商品服务 (九)