kratos 框架商城微服务实战之购物车服务 (十二)

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

大家好,好久不见,由于公司的工作实在太忙,耽搁了好久,实在抱歉。今天咱们开始写商城里面的购物车服务,废话少说咱们开始。

注:竖排 … 代码省略,为了保持文章的篇幅简洁,我会将一些不必要的代码使用竖排的 . 来代替,你在复制本文代码块的时候,切记不要将 . 也一同复制进去。文章写的不清晰的地方可通过 GitHub 源码进行查看, 也感谢您指出不足之处,欢迎大佬指教。

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

准备工作

由于前面几篇文章(第一篇和第四篇)都已经写过了,如何初始化一个 kratos 项目,并修改部分主要的文件,来变成自己的服务并编写业务。所以此篇文章就不做重复性的工作了,这篇编写一个购物车的创建,让购物车服务先存在,废话少说开始写。

Cart 服务目录存放的位置

1
2
3
4
5
6
// 整体的项目 目录结构如下
|-- kratos-shop
|-- service
|-- user // 原先的用户服务
|-- cart // 新增的购物车服务
|-- shop // interface

购物车表设计

  • data 目录下新建数据表相关的文件

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

import (
"cart/internal/domain" // 引入 domain 层
"context"
"time"

"github.com/go-kratos/kratos/v2/errors"
"gorm.io/gorm"

"cart/internal/biz"

"github.com/go-kratos/kratos/v2/log"
)

type ShopCart struct {
ID int64 `gorm:"primarykey;type:int" json:"id"`
UserId int64 `gorm:"type:int;not null;comment:用户id" json:"user_id"`
GoodsId int64 `gorm:"type:int;not null;comment:商品id" json:"goods_id"`
SkuId int64 `gorm:"type:int;not null;comment:sku_id" json:"sku_id"`
GoodsPrice int64 `gorm:"type:int;not null;comment:商品价格" json:"goods_price"`
GoodsNum int32 `gorm:"type:int;not null;comment:商品数量" json:"goods_num"`
GoodsSn string `gorm:"type:varchar(500);default:;comment:商品编号"`
GoodsName string `gorm:"type:varchar(500);default:;comment:商品名称"`
IsSelect bool `gorm:"type:tinyint;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"`
}

type cartRepo struct {
data *Data
log *log.Helper
}

func NewCartRepo(data *Data, logger log.Logger) biz.CartRepo {
return &cartRepo{
data: data,
log: log.NewHelper(logger),
}
}

// domain 层转换
func (p *ShopCart) ToDomain() *domain.ShopCart {
return &domain.ShopCart{
ID: p.ID,
UserId: p.UserId,
GoodsId: p.GoodsId,
SkuId: p.SkuId,
GoodsPrice: p.GoodsPrice,
GoodsNum: p.GoodsNum,
GoodsSn: p.GoodsSn,
GoodsName: p.GoodsName,
IsSelect: p.IsSelect,
}
}

新建domain 层下的文件

internal/domain/cart.go

cart.go 文件内容:

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

type ShopCart struct {
ID int64
UserId int64
GoodsId int64
SkuId int64
GoodsPrice int64
GoodsNum int32
GoodsSn string
GoodsName string
IsSelect bool
}

购物车方法

定义购物车创建方法

api/cart/v1/cart.proto

  • cart.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
syntax = "proto3";

package cart.v1;

import "validate/validate.proto";

option go_package = "cart/api/cart/v1;v1";

// 购物车
service Cart {
rpc CreateCart (CreateCartRequest) returns (CartInfoReply); // 添加商品进购物车
...
}

message CartInfoReply {
int64 id = 1;
int64 userId = 2;
int64 goodsId = 3;
string goodsSn = 4;
string goodsName = 5;
int64 skuId = 6;
int64 goodsPrice = 7;
int32 goodsNum = 8;
bool isSelect = 9;
}

message CreateCartRequest {
int64 id = 1;
int64 userId = 2 [(validate.rules).int64 = {gt:0}];
int64 goodsId = 3 [(validate.rules).int64 = {gt:0}];
string goodsSn = 4 [(validate.rules).string.min_len = 1];
string goodsName = 5 [(validate.rules).string.min_len = 1];
int64 skuId = 6 [(validate.rules).int64 = {gt:0}];
int64 goodsPrice = 7 [(validate.rules).int64 = {gt:0}];
int32 goodsNum = 8 [(validate.rules).int32 = {gt:0}];
bool isSelect = 9 [(validate.rules).bool.const = true];
}

实现购物车创建方法

internal/service/cart.go

  • cart.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

import (
"cart/internal/biz"
"cart/internal/domain"
"context"

v1 "cart/api/cart/v1"
)

type CartService struct {
v1.UnimplementedCartServer
cart *biz.CartUsecase
}

func NewCartService(cart *biz.CartUsecase) *CartService {
return &CartService{cart: cart}
}

func (s *CartService) CreateCart(ctx context.Context, req *v1.CreateCartRequest) (*v1.CartInfo, error) {

// biz 层定义的方法,经过 domian 层进行参数转换
rv, err := s.cart.CreateCart(ctx, &domain.ShopCart{
UserId: req.UserId,
GoodsId: req.GoodsId,
SkuId: req.SkuId,
GoodsPrice: req.GoodsPrice,
GoodsNum: req.GoodsNum,
GoodsSn: req.GoodsSn,
GoodsName: req.GoodsName,
IsSelect: req.IsSelect,
})

if err != nil {
return nil, err
}

return &v1.CartInfo{
Id: rv.ID,
UserId: rv.UserId,
GoodsId: rv.GoodsId,
GoodsSn: rv.GoodsSn,
GoodsName: rv.GoodsName,
SkuId: rv.SkuId,
GoodsPrice: rv.GoodsPrice,
GoodsNum: rv.GoodsNum,
IsSelect: rv.IsSelect,
}, nil
}
  • internal/biz/cart.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
package biz

import (
"cart/internal/domain"
"context"

"github.com/go-kratos/kratos/v2/log"
)

type CartRepo interface {
Create(ctx context.Context, c *domain.ShopCart) (*domain.ShopCart, error)
}

type CartUsecase struct {
repo CartRepo
log *log.Helper
}

func NewCartUsecase(repo CartRepo, logger log.Logger) *CartUsecase {
return &CartUsecase{repo: repo, log: log.NewHelper(logger)}
}

func (uc *CartUsecase) CreateCart(ctx context.Context, c *domain.ShopCart) (*domain.ShopCart, error) {
return uc.repo.Create(ctx, c)
}

  • internal/data/cart.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

...

func (r *cartRepo) Create(ctx context.Context, c *domain.ShopCart) (*domain.ShopCart, error) {
var shopCart ShopCart
if result := r.data.db.Where(&ShopCart{UserId: c.UserId, SkuId: c.SkuId}).First(&shopCart); result.RowsAffected == 1 {
shopCart.GoodsNum += c.GoodsNum
} else {
shopCart.UserId = c.UserId
shopCart.GoodsId = c.GoodsId
shopCart.SkuId = c.SkuId
shopCart.GoodsPrice = c.GoodsPrice
shopCart.GoodsNum = c.GoodsNum
shopCart.GoodsSn = c.GoodsSn
shopCart.GoodsName = c.GoodsName
shopCart.IsSelect = c.IsSelect
}

if result := r.data.db.Save(&shopCart); result.Error != nil {
return nil, errors.InternalServer("CREATE_CART_NOT_FOUND", "创建购物车失败")
}

return shopCart.ToDomain(), nil
}

这里可以看到,创建购物车数据的时候,只判断了用户是否有同等商品的购物车数据。除此之外并未进行其他业务逻辑的判断,比如,用户是否存在、商品是否正确,而是拿到数据之后,直接进行存储入库。

这里基于咱们项目整体架构的设计,service 层下的所有个体服务,尤其涉及到登录之后的服务,都假设用户已经通过了 bff 层 也就是 service 同级目录的 shop 或者 admin 的业务逻辑验证,servcie 服务只提供粒度够细,较少业务无关,有利于重用,利于单元测试的方法。bff 层的代码更多的是面向用户的业务逻辑的。

拿这个创建购物接口来说,shop 服务中用户创建购物车接口的方法里面,就会判断用户是否登录(用户服务)、商品是否存在(商品服务)、库存(库存服务)是否可以插入购物之类的逻辑验证。

单元测试

  • 新增 cart_test.go 文件

    internal/data/cart_test.go,
    这里和第二篇文章的单元测试流程是相同,一样的初始化测试的代码就不贴出来了。

  • 编写测试 data 下的 create 方法

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_test

import (
"cart/internal/biz"
"cart/internal/data"
"cart/internal/domain"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Cart", func() {
var ro biz.CartRepo
BeforeEach(func() {
ro = data.NewCartRepo(Db, nil)
})
// 设置 It 块来添加单个规格
It("CreateCart", func() {
cartData := domain.ShopCart{
UserId: 1,
GoodsId: 1,
SkuId: 1,
GoodsPrice: 1000,
GoodsNum: 10,
GoodsSn: "20232232231",
GoodsName: "Mate 40 Pro",
IsSelect: true,
}
c, err := ro.Create(ctx, &cartData)
Ω(err).ShouldNot(HaveOccurred())
Ω(c.UserId).Should(Equal(int64(1)))
Ω(c.GoodsNum).Should(Equal(int32(10)))

// 二次验证创建相同商品的数据,只增加商品数量
cartData2 := domain.ShopCart{
UserId: 1,
GoodsId: 1,
SkuId: 1,
GoodsPrice: 1000,
GoodsNum: 10,
GoodsSn: "20232232231",
GoodsName: "Mate 40 Pro",
IsSelect: true,
}
c2, err := ro.Create(ctx, &cartData2)
Ω(err).ShouldNot(HaveOccurred())
Ω(c2.UserId).Should(Equal(int64(1)))
Ω(c2.GoodsNum).Should(Equal(int32(20)))
})

})

结果如图共 1 个测试 1 个通过 0 个失败。

工具验证创建方法

没错还是通过 BloomRPC 工具进行测试。


如图故意不把 goodsid 的参数设置为 0 ,并为通过接口的参数验证。

如图把 goodsid 填写正确,就创建成功了。

结束语

在啰嗦几句,最近这几天忙成狗,天天写前端代码,各种样式,交互效果,都要写吐了。打算合理规划一下时间,争取早日把整个项目跑通。

这里特别感谢一下一直支持观看此系列的同学,更感谢你们的打赏、点赞、分享

关注我获取更新