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];
  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 后面加入:
--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
    Title  string
    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
}

测试创建属性组

编写商品属性相关方法

创建属性信息

  • servicegoods_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
}
  • domaingoods_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
    AttrId  int64
    GroupID int64
    Value   string
}

func (p GoodsAttrValue) IsValueEmpty() bool {
    return p.Value == ""
}
  • bizgoods_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
}
  • datagoods_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
}

测试新建属性信息

结束语

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

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

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

本作品采用《CC 协议》,转载必须注明作者和本文链接
微信搜索:上帝喜爱笨人
讨论数量: 2

个人认为文章的代码有点贴的乱 应该是先biz(domain)层,然后到data层,然后service层这样好点 不然跟着文章打代码就会一堆红线一堆报错的 思路也不容易跟上

1年前 评论
Aliliin (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!