kratos 框架商城微服务实战之商品服务 (十一)

本文最后更新于:2 年前

大家好,今天咱们继续写商品服务中的商品查询接口,主要是引入的 Elasticsearch 搜索服务,通过输入关键词来进行商品搜索,废话少说咱们开始

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

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

准备工作

增加配置

不特殊强调的话,此篇文章修改的代码都是 goods 服务下的代码

  • config 目录下新增如下代码:
1
2
3
4
5
6
7
8
...

data:
...
elastic:
addr: http://127.0.0.1:9200 // 这个位置是你链接 es 的地址

...
  • conf 目录下的 conf.proto 文件修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 修改之后别忘记执行 make config 命令

syntax = "proto3";

...

message Data {
...

message Elastic {
string addr = 1;
}

Database database = 1;
Redis redis = 2;
Elastic elastic = 3;
}


...
  • data 目录下的 data.go 文件新增 es 链接服务
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

import (

...

"github.com/olivere/elastic/v7" // package 别忘记 mod

...
)

// ProviderSet is data providers.
var ProviderSet = wire.NewSet(
NewData,
NewDB, NewTransaction, NewRedis, NewElasticsearch, // 别忘记把链接方法引入

...

NewEsGoodsRepo, // 别忘记把初始化es repo 方法引入
)

type Data struct {
db *gorm.DB
rdb *redis.Client
EsClient *elastic.Client
}

...

// NewData .
func NewData(c *conf.Data, logger log.Logger, db *gorm.DB, rdb *redis.Client, es *elastic.Client) (*Data, func(), error) {
cleanup := func() {
log.NewHelper(logger).Info("closing the data resources")
}
return &Data{
db: db,
rdb: rdb,
EsClient: es,
}, cleanup, nil
}

...

func NewElasticsearch(c *conf.Data) *elastic.Client {
es, err := elastic.NewClient(elastic.SetURL(c.Elastic.Addr), elastic.SetSniff(false),
elastic.SetTraceLog(slog.New(os.Stdout, "shop", slog.LstdFlags)))
if err != nil {
panic(err)
}

return es
}
  • data 目录下新增 es_goods.go 文件

根据商品的功能,我们来设计我们的mapping结构,创建对应的索引。这里我把 es__goods 当做一个全新的表 repo ,然后进行常规操作,新建 esGoodsRepo ,把它注入到 data.go 的 wire 里面,前面的代码已经注入了。 ES package 用的是 olivere/elastic

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package data

import (
"context"
"encoding/json"
"goods/internal/biz"
"goods/internal/domain"
"strconv"

"github.com/go-kratos/kratos/v2/log"
"github.com/olivere/elastic/v7"
)

// GetIndexName 设计商品的索引 goods
func (esGoodsRepo) GetIndexName() string {
return "goods"
}

// GetMapping 设计商品的 mapping 结构
func (esGoodsRepo) GetMapping() string {
goodsMapping := `
{
"mappings": {
"properties": {
"id": {
"type": "integer"
},
"brands_id": {
"type": "integer"
},
"category_id": {
"type": "integer"
},
"type_id": {
"type": "integer"
},
"click_num": {
"type": "integer"
},
"fav_num": {
"type": "integer"
},
"is_hot": {
"type": "boolean"
},
"is_new": {
"type": "boolean"
},
"market_price": {
"type": "integer"
},
"name": {
"type": "text",
"analyzer": "ik_max_word"
},
"brand_name": {
"type": "keyword",
"index": false,
"dec_values": false,
},
"category_name": {
"type": "keyword",
"index": false,
"dec_values": false,
},
"type_name": {
"type": "keyword",
"index": false,
"dec_values": false,
},
"goods_brief": {
"type": "text",
"analyzer": "ik_max_word"
},
"on_sale": {
"type": "boolean"
},
"ship_free": {
"type": "boolean"
},
"shop_price": {
"type": "integer"
},
"sold_num": {
"type": "integer"
},
"sku": {
"type": "nested",
"sku_id": {
"type": "integer",
},
"sku_name": {
"type": "text",
"analyzer": "ik_max_word"
},
"sku_price": {
"type": "integer",
},
}
}
}
}`
return goodsMapping
}

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

// NewEsGoodsRepo .
func NewEsGoodsRepo(data *Data, logger log.Logger) biz.EsGoodsRepo {
return &esGoodsRepo{
data: data,
log: log.NewHelper(logger),
}
}

编写接口

实现接口

加入商品的部分信息到 es 服务中,是在商品创建、更新、删除的时候进行操作的,由于咱们前面已经写过了商品的创建,这里只需要在创建成功之后,进一步创建 es 数据就行了。

  • goods.proto 文件 新增查询方法

    修改之后别忘记执行 make api 命令

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
57
58
59
60
61
62
syntax = "proto3";

...


service Goods {

    ...

// 商品接口
rpc CreateGoods(CreateGoodsRequest) returns (CreateGoodsResponse);

rpc GoodsList(GoodsFilterRequest) returns(GoodsListResponse);

}


// 根据前期设计的 mapping 结构,来构建查询的时候所需的字段
// 当然不是说 mapping 中没有的就不能进行查询了
message GoodsFilterRequest {
string keywords = 1;
int32 categoryId = 2;
int32 brandId = 3;
int64 minPrice = 4;
int64 maxPrice = 5;
bool isHot = 6;
bool isNew = 7;
bool isTab = 8;
int64 clickNum = 9;
int64 soldNum = 10;
int64 favNum = 11;
int64 pages = 12;
int64 pagePerNums = 13;
int64 id = 14;
}

// 返回的每个商品的具体信息,这里为了演示
// 并没有加入 sku 、brand 等信息,可自行添加
message GoodsInfoResponse {
int64 id = 1;
int32 categoryId = 2;
int32 brandId = 3;
string name = 4;
string goodsSn = 5;
int64 clickNum = 6;
int64 soldNum = 7;
int64 favNum = 8;
int64 marketPrice = 9;
string goodsBrief = 10;
string goodsDesc = 11;
bool shipFree = 12;
string images = 13;
repeated string goodsImages = 14;
bool isNew = 15;
bool isHot = 16;
bool onSale = 17;
}

message GoodsListResponse {
int64 total = 1;
repeated GoodsInfoResponse list = 2;
}

修改创建商品的方法

  • domain 目录下新增 es_goods.go 文件,来进行 repo 和 api 的交互
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
57
58
59
60
61
62
63
package domain

import "github.com/olivere/elastic/v7"

// 构建查询的时候转换的结构
type ESGoodsFilter struct {
ID int64
CategoryID int32
BrandsID int32
Keywords string
OnSale bool
ShipFree bool
IsNew bool
IsHot bool
ClickNum int64
SoldNum int64
FavNum int64
MaxPrice int64
MinPrice int64
Pages int64
PagePerNums int64
}

// 构建插入es 的时候所需的结构,json 的意思是,存入到es 中显示的字段名
type ESGoods struct {
ID int64 `json:"id"`
CategoryID int32 `json:"category_id"`
CategoryName string `json:"category_name"`
BrandsID int32 `json:"brands_id"`
BrandName string `json:"brand_name"`
TypeID int64 `json:"type_id"`
TypeName string `json:"type_name"`
OnSale bool `json:"on_sale"`
ShipFree bool `json:"ship_free"`
IsNew bool `json:"is_new"`
IsHot bool `json:"is_hot"`
Name string `json:"name"`
GoodsTags string `json:"goods_tags"`
ClickNum int64 `json:"click_num"`
SoldNum int64 `json:"sold_num"`
FavNum int64 `json:"fav_num"`
MarketPrice int64 `json:"market_price"`
GoodsBrief string `json:"goods_brief"`
Pages int64 `json:"pages"`
PagePerNums int64 `json:"page_pre_num"`
Sku []EsSku `json:"sku"`
}
type EsSku struct {
SkuID int64 `json:"sku_id"`
SkuName string `json:"sku_name"`
SkuPrice int64 `json:"sku_price"`
}

// es 公共的查询语法,用过来不同条件拼接查询sql
type EsSearch struct {
MustQuery []elastic.Query
MustNotQuery []elastic.Query
ShouldQuery []elastic.Query
Filters []elastic.Query
Sorters []elastic.Sorter
Form int64 // 分页
Size int64
}
  • 修改biz/goods.go ,新增插入 es 的逻辑
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package biz

import (
"context"
"encoding/json"
"fmt"
"goods/internal/domain"

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

type GoodsRepo interface {
CreateGoods(ctx context.Context, goods *domain.Goods) (*domain.Goods, error)
}

type GoodsUsecase struct {
repo GoodsRepo

...

esGoodsRepo EsGoodsRepo // 新增的 es 的repo
log *log.Helper
}

func NewGoodsUsecase(repo GoodsRepo, skuRepo GoodsSkuRepo, tx Transaction, gRepo GoodsTypeRepo, cRepo CategoryRepo,
bRepo BrandRepo, sRepo SpecificationRepo, aRepo GoodsAttrRepo, iRepo InventoryRepo, es EsGoodsRepo, logger log.Logger) *GoodsUsecase {

return &GoodsUsecase{
repo: repo,

...

esGoodsRepo: es, // 新增的 es 的repo
log: log.NewHelper(logger),
}
}


func (g GoodsUsecase) CreateGoods(ctx context.Context, r *domain.Goods) (*domain.GoodsInfoResponse, error) {
var (
err error
goods *domain.Goods
EsGoods domain.ESGoods
)

...


err = g.tr.ExecTx(ctx, func(ctx context.Context) error {
// 更新商品表

...


// 此处为新增,注意因为咱们之前判断品牌、分类等是否存在,由于并没有使用
// 使用了 _ 进行忽略,需要放开来使用
// esModel
{
EsGoods.Sku = append(EsGoods.Sku, domain.EsSku{
SkuID: skuInfo.ID,
SkuName: skuInfo.SkuName,
SkuPrice: skuInfo.Price,
})
EsGoods.BrandsID = brand.ID
EsGoods.BrandName = brand.Name
EsGoods.CategoryID = cate.ID
EsGoods.CategoryName = cate.Name
EsGoods.TypeID = goodsType.ID
EsGoods.TypeName = goodsType.Name
EsGoods.Name = goodsType.Name
EsGoods.ID = goods.ID
EsGoods.OnSale = goods.OnSale
EsGoods.ShipFree = goods.ShipFree
EsGoods.IsNew = goods.IsNew
EsGoods.IsHot = goods.IsHot
EsGoods.Name = goods.Name
EsGoods.GoodsTags = goods.GoodsTags
EsGoods.ClickNum = goods.ClickNum
EsGoods.SoldNum = goods.SoldNum
EsGoods.FavNum = goods.FavNum
EsGoods.MarketPrice = goods.MarketPrice
EsGoods.GoodsBrief = goods.GoodsBrief

}

// 插入 EsGoods 的方法
err = g.esGoodsRepo.InsertEsGoods(ctx, EsGoods)
if err != nil {
return err
}
}
return nil
})

if err != nil {
return nil, err
}
return &domain.GoodsInfoResponse{GoodsID: goods.ID}, nil
}
  • 新增 biz/es_goods.go 文件来满足使用 es repo 的需求
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
package biz

import (
"context"
"goods/internal/domain"

"github.com/go-kratos/kratos/v2/log"
"github.com/olivere/elastic/v7"
)

type EsGoodsRepo interface {
InsertEsGoods(context.Context, domain.ESGoods) error // 存储 es 的方法
}

type EsGoodsUsecase struct {
repo GoodsRepo
esRepo EsGoodsRepo
categoryRepo CategoryRepo
log *log.Helper
}

func NewEsGoodsUsecase(repo GoodsRepo, es EsGoodsRepo, cRepo CategoryRepo, logger log.Logger) *EsGoodsUsecase {
return &EsGoodsUsecase{
repo: repo, // 商品的repo
esRepo: es, // es 商品的repo
categoryRepo: cRepo, // 分类的 repo
log: log.NewHelper(logger),
}
}
  • data/es_goods.go 实现 InsertEsGoods 方法
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
package data

import (
"context"
"encoding/json"
"goods/internal/biz"
"goods/internal/domain"
"strconv"

"github.com/go-kratos/kratos/v2/log"
"github.com/olivere/elastic/v7"
)

...

func (p esGoodsRepo) InsertEsGoods(ctx context.Context, esModel domain.ESGoods) error {
// 新建 mapping 和 index
exists, err := p.data.EsClient.IndexExists(p.GetIndexName()).Do(ctx)
if err != nil {
panic(err)
}
if !exists {
_, err = p.data.EsClient.CreateIndex(p.GetIndexName()).BodyString(p.GetMapping()).Do(ctx)
if err != nil {
return err
}
}

_, err = p.data.EsClient.Index().Index(p.GetIndexName()).BodyJson(esModel).Id(strconv.Itoa(int(esModel.ID))).Do(ctx)
if err != nil {
return err
}
return nil
}

测试新增 ES 数据

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

可以看到成功插入。访问 es 的服务来验证是否插入成功。

es 访问链接:http://127.0.0.1:5601/app/dev_tools#/console

执行 es 命令进行查询:

1
2
3
4
5
6
GET goods/_search
{
"query": {
"match_all": {}
}
}

看到结果如图:

成功插入,截图中的 id 跟上图的 id 不一致,是因为我不小心执行两遍一样的创建请求。😂 下面的数据图没截出来。

编写查询请求

  • service/goods.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
57
58
59
60
61
package service

import (
"context"
v1 "goods/api/goods/v1"
"goods/internal/domain"
)

...


func (g *GoodsService) GoodsList(ctx context.Context, r *v1.GoodsFilterRequest) (*v1.GoodsListResponse, error) {
// 转换成domain定义的结构体
goodsFilter := &domain.ESGoodsFilter{
ID: r.Id,
CategoryID: r.CategoryId,
BrandsID: r.BrandId,
Keywords: r.Keywords,
IsNew: r.IsNew,
IsHot: r.IsHot,
ClickNum: r.ClickNum,
SoldNum: r.SoldNum,
FavNum: r.FavNum,
MaxPrice: r.MaxPrice,
MinPrice: r.MinPrice,
Pages: r.Pages,
PagePerNums: r.PagePerNums,
}

// 调用 biz/es_goods.go 下的方法
result, err := g.esGoods.GoodsList(ctx, goodsFilter)
if err != nil {
return nil, err
}
response := v1.GoodsListResponse{
Total: result.Total,
}
for _, goods := range result.List {
res := v1.GoodsInfoResponse{
Id: goods.ID,
CategoryId: goods.CategoryID,
BrandId: goods.BrandsID,
Name: goods.Name,
GoodsSn: goods.GoodsSn,
ClickNum: goods.ClickNum,
SoldNum: goods.SoldNum,
FavNum: goods.FavNum,
MarketPrice: goods.MarketPrice,
GoodsBrief: goods.GoodsBrief,
GoodsDesc: goods.GoodsBrief,
ShipFree: goods.ShipFree,
Images: goods.GoodsFrontImage,
GoodsImages: goods.GoodsImages,
IsNew: goods.IsNew,
IsHot: goods.IsHot,
OnSale: goods.OnSale,
}
response.List = append(response.List, &res)
}
return &response, nil
}
  • biz/es_goods.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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package biz

import (
"context"
"goods/internal/domain"

"github.com/go-kratos/kratos/v2/log"
"github.com/olivere/elastic/v7"
)

type EsGoodsRepo interface {
GoodsList(ctx context.Context, es *domain.EsSearch) ([]int64, int64, error) // repo 的方法
InsertEsGoods(context.Context, domain.ESGoods) error
}

...


func (g EsGoodsUsecase) GoodsList(ctx context.Context, req *domain.ESGoodsFilter) (*domain.GoodsListResponse, error) {
// 组织 es 查询条件
var es domain.EsSearch
if req.Keywords != "" {
es.ShouldQuery = append(es.ShouldQuery, elastic.NewMultiMatchQuery(req.Keywords, "name", "goods_brief", "sku.sku_name"))
}
if req.IsHot {
es.ShouldQuery = append(es.ShouldQuery, elastic.NewTermQuery("is_hot", req.IsHot)) // 精确字段查询
}
if req.ClickNum > 0 {
es.ShouldQuery = append(es.ShouldQuery, elastic.NewFieldSort("click_num").Desc()) // 根据某个字段排序
}
if req.MinPrice > 0 {
es.ShouldQuery = append(es.ShouldQuery, elastic.NewRangeQuery("shop_price").Gte(req.MinPrice)) // 区间筛选 gte 大于=
}
if req.MaxPrice > 0 {
es.ShouldQuery = append(es.ShouldQuery, elastic.NewRangeQuery("shop_price").Lte(req.MaxPrice)) // lte 小于=
}
if req.BrandsID > 0 {
es.ShouldQuery = append(es.ShouldQuery, elastic.NewTermQuery("brands_id", req.BrandsID))
}
// 通过 category 去查询商品,这里涉及到一个问题,就是商品分类是多级的,所以特殊处理一下
if req.CategoryID > 0 {
// 查询分类是否存在
cate, err := g.categoryRepo.GetCategoryByID(ctx, req.CategoryID)
if err != nil {
return nil, err
}
categoryIds, err := g.categoryRepo.GetCategoryAll(ctx, cate.Level, req.CategoryID)
if err != nil {
return nil, err
}
es.ShouldQuery = append(es.ShouldQuery, elastic.NewTermsQuery("category_id", categoryIds...))
}
// 分页处理
switch {
case req.PagePerNums > 100:
req.PagePerNums = 100
case req.PagePerNums <= 0:
req.PagePerNums = 10
}
if req.Pages == 0 {
req.Pages = 1
}
es.Form = (req.Pages - 1) * req.PagePerNums
es.Size = req.PagePerNums

// 去 es repo 中查询获得的商品ID
res := &domain.GoodsListResponse{}
goodsIds, total, err := g.esRepo.GoodsList(ctx, &es)
if err != nil {
return nil, err
}
res.Total = total
if err != nil {
return nil, err
}

// 根据es返回的商品ID 获取详细的商品信息
goodsList, err := g.repo.GoodsListByIDs(ctx, goodsIds...)
if err != nil {
return nil, err
}
res.List = goodsList

// TODO 根据返回的商品信息,查询所有分类、查询所有品牌、查询所有sku 的信息进行组合,这里就不写了

return res, nil
}

商品分类查询的方法,这里就不贴代码了,无非就是先去 biz/category.go 的 categoryRepo interface 定一个一个接口,然后再去 data 下的 categoryRepo 实现方法然后把 ID 返回来

  • data/es_goods.go 文件下实现 GoodsList 方法
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
package data

import (
"context"
"encoding/json"
"goods/internal/biz"
"goods/internal/domain"
"strconv"

"github.com/go-kratos/kratos/v2/log"
"github.com/olivere/elastic/v7"
)

...

func (p esGoodsRepo) GoodsList(ctx context.Context, filter *domain.EsSearch) ([]int64, int64, error) {
boolQuery := elastic.NewBoolQuery()
boolQuery.Must(filter.MustQuery...)
boolQuery.MustNot(filter.MustNotQuery...)
boolQuery.Should(filter.ShouldQuery...)
boolQuery.Filter(filter.Filters...)

result, err := p.data.EsClient.Search().
Index(p.GetIndexName()).
Query(boolQuery).
SortBy(filter.Sorters...).
From(int(filter.Form)).
Size(int(filter.Size)).
Do(ctx)

if err != nil {
return nil, 0, err
}

// 取出来商品ID
goodsIds := make([]int64, 0)
for _, value := range result.Hits.Hits {
goods := domain.ESGoods{}
_ = json.Unmarshal(value.Source, &goods)
goodsIds = append(goodsIds, goods.ID)
}
return goodsIds, result.Hits.TotalHits.Value, nil

}


  • data/goods.go文件下实现GoodsListByIDs 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
...


func (g goodsRepo) GoodsListByIDs(c context.Context, ids ...int64) ([]*domain.Goods, error) {
var l []*Goods
if err := g.data.DB(c).Where("id IN (?)", ids).Find(&l).Error; err != nil {
return nil, errors.NotFound("GOODS_NOT_FOUND", "商品不存在")
}
var res []*domain.Goods
for _, item := range l {
res = append(res, item.ToDomain())
}
return res, nil
}

测试查询

这里使用的是 goodsBrief 里面的信息来当关键词构建的请求,其他的效果,请自己进行测试 😂

可以看到结果也出来了。

结束语

本篇只提供了一个商品创建的方法,更新的方法、删除的方法并没有体现,其实原理都一样,处理好自己的逻辑之后,调用 es 不同的命令就行了。
比如新增调用的是 index

1
2
_, err = p.data.EsClient.Index().Index(p.GetIndexName()).BodyJson(esModel).Id(strconv.Itoa(int(esModel.ID))).Do(ctx)

更新只需要把 ID 定好调用:update

1
2
_, err = p.data.EsClient.Update().Index(p.GetIndexName()).
Doc(esModel).Id("自己的ID").Do(ctx)

最近这几天忙成狗,总算是肝出来了 Elasticsearch 进行商品检索,之后准备先不完善商品服务剩余的接口,而是去写订单服务。

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

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