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
}
测试创建属性组#
编写商品属性相关方法#
创建属性信息#
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
AttrId int64
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
}
测试新建属性信息#
结束语#
本篇只提供了一个商品属性信息的创建方法,其他方法没有在文章中体现,单元测试方法也没有编写,重复性的工作这里就不编写了,通过前几篇的文章,相信你可以自己完善剩余的方法。
下一篇就开始真正的商品创建了,敬请期待。
感谢您的耐心阅读,动动手指点个赞吧。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: