大家好,好久不见,由于公司的工作实在太忙,耽搁了好久,实在抱歉。今天咱们开始写商城里面的购物车服务,废话少说咱们开始。
注:竖排 … 代码省略,为了保持文章的篇幅简洁,我会将一些不必要的代码使用竖排的 . 来代替,你在复制本文代码块的时候,切记不要将 . 也一同复制进去。文章写的不清晰的地方可通过 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
|
购物车表设计
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" "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), } }
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
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
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) {
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("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 填写正确,就创建成功了。
结束语
在啰嗦几句,最近这几天忙成狗,天天写前端代码,各种样式,交互效果,都要写吐了。打算合理规划一下时间,争取早日把整个项目跑通。
这里特别感谢一下一直支持观看此系列的同学,更感谢你们的打赏、点赞、分享
关注我获取更新