文章目录
- 简介
- 建表
- proto
- handler
- 商品
- 小结
简介
- 商品微服务主要在于表的设计,建哪些表?表之间的关系是怎样的?
- 主要代码就是 CURD
- 表和字段的设计是一个比较有挑战性的工作,比较难说清楚,也需要经验的积累,这里关注点在微服务,暂时先 “借鉴” 别人的设计
- 一般需要多次迭代才能设计出比较合理的表结构
- 我们将商品相关的服务都放在这部分,方便建表和管理;微服务并不是越多越好,分成太多服务可能会让系统太复杂难以管理
- 如何划分微服务需要积累经验,也有针对性的方法可以套用,需要单独学习
建表
- 新建商品微服务 Service 层,直接把 user_srv 的目录拷过来,整体替换相关路径
- 表设计及相关操作,主要有四张表
- 放个商品表的结构
// 商品表 // 某一个具体的商品 type Goods struct { BaseModel CategoryID int32 `gorm:"type:int;not null"` Category Category // 一对多/一对一 BrandsID int32 `gorm:"type:int;not null"` Brands Brands // 一对一 OnSale bool `gorm:"default:false;not null"` ShipFree bool `gorm:"default:false;not null"` IsNew bool `gorm:"default:false;not null"` IsHot bool `gorm:"default:false;not null"` Name string `gorm:"type:varchar(50);not null"` GoodsSn string `gorm:"type:varchar(50);not null"` ClickNum int32 `gorm:"type:int;default:0;not null"` SoldNum int32 `gorm:"type:int;default:0;not null"` FavNum int32 `gorm:"type:int;default:0;not null"` MarketPrice float32 `gorm:"not null"` ShopPrice float32 `gorm:"not null"` GoodsBrief string `gorm:"type:varchar(100);not null"` Images GormList `gorm:"type:varchar(1000);not null"` DescImages GormList `gorm:"type:varchar(1000);not null"` GoodsFrontImage string `gorm:"type:varchar(200);not null"` }
- 对字段的限制一般都要求
not null
,也就是必须有值,然后限制用户必须输入或者我们设置default
- 设置为 null 带来的问题
- 定义 model
- 注意表之间的关联关系,查看 gorm 文档了解使用方式
- 生成表结构
- 新建数据库,直接在(109.128)机器上操作,方便启动项目
CREATE DATABASE IF NOT EXISTS shop_goods_srv DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
- 一个微服务对应一个数据库,要和其他微服务隔离开
- 还是在 model/main 下面运行一次 main 方法即可
- 运行 sql 文件,导入数据;将上面创建的表结构全部删掉(不删也行),因为 SQL 文件里包含了建表语句
proto
- proto 文件的设计又是一个重点,首先要根据页面进行需求分析(前端页面和后台管理页面),需要哪些接口?传递哪些参数?会有什么响应?
- 接口定义也不是一蹴而就的,后面可能会修改,不必在一开始追求完美
protoc -I . goods.proto --go_out=plugins=grpc:.
生成 stub 文件- 遇到点小问题,empty.proto 找不到,拷过来放到 proto/google/protobuf 下面试试
- 定义 Service 层(Server端)接口(handler),直接搜
GoodsServer
,将接口定义拷过来- 这里接口太多,我们分在不同 handler 文件实现,不着急实现,先把接口定义拷过去修改一下引用,因为没有实现的话项目无法启动,
- 再修改 main.go 和 nacos 配置文件(本地/中心配置),启动项目看能否注册成功
handler
- 具体实现各接口,先从品牌和轮播图开始,这里放个获取品牌列表的例子
// 品牌 func (s *GoodsServer) BrandList(ctx context.Context, req *proto.BrandFilterRequest) (*proto.BrandListResponse, error) { brandListResponse := proto.BrandListResponse{} var brands []model.Brands // 分页:第几页,每页数量 // Scopes 需要传一个匿名函数进去,在里面做一些判断并查询数据 result := global.DB.Scopes(Paginate(int(req.Pages), int(req.PagePerNums))).Find(&brands) if result.Error != nil { return nil, result.Error } var total int64 global.DB.Model(&model.Brands{}).Count(&total) // 单独再获取一次品牌总数 brandListResponse.Total = int32(total) // 准备响应数据,单个品牌的BrandInfoResponse 和品牌列表的 brandListResponse,要根据 proto 定义的 message 安排好 var brandResponses []*proto.BrandInfoResponse for _, brand := range brands { brandResponses = append(brandResponses, &proto.BrandInfoResponse{ Id: brand.ID, Name: brand.Name, Logo: brand.Logo, }) } brandListResponse.Data = brandResponses return &brandListResponse, nil }
- 回答一个上一篇的问题:查询数据时不传表名,传入 model 定义的 struct 就可以吗?
- 是的,比如
DB.First(&model.Brands{})
,因为 struct 名就是表名,改表名也是通过绑在 struct 上的函数进行的
- 是的,比如
- 商品分类
- 先看 GetAllCategorysList,这里比较 tricky,因为有多级分类,需要同时查询两级 subCategory
- 我们希望得到的数据格式
[ { "id":xxx, "name":"", "level":1, "is_tab":false, "parent":13xxx, "sub_category":[ "id":xxx, "name":"", "level":1, "is_tab":false, "sub_category":[ "id":xxx, "name":"", ] ] } ]
- 通过 gorm 提供的预加载(反向查询)实现
- 条件查询一级目录:
global.DB.Where(&model.Category{Level: 1})
- 反向查询两级子类目:
Preload("SubCategory.SubCategory")
- 再贴一个获取子分类的函数,grpc 的接口实现都是三步走
// 获取子分类,传入一级或二级类别ID func (s *GoodsServer) GetSubCategory(ctx context.Context, req *proto.CategoryListRequest) (*proto.SubCategoryListResponse, error) { // 1.准备返回值 categoryListResponse := proto.SubCategoryListResponse{} // total, info(parent), sub // 2.根据请求参数查询数据+判空 var category model.Category if result := global.DB.First(&category, req.Id); result.RowsAffected == 0 { return nil, status.Errorf(codes.NotFound, "商品分类不存在") } // 3.填充返回值各字段-Info categoryListResponse.Info = &proto.CategoryInfoResponse{ Id: category.ID, Name: category.Name, Level: category.Level, IsTab: category.IsTab, ParentCategory: category.ParentCategoryID, } var subCategorys []model.Category var subCategoryResponse []*proto.CategoryInfoResponse // 如果要获取三级子分类,默认获取下面一级 //preloads := "SubCategory" //if category.Level == 1 { // preloads = "SubCategory.SubCategory" //} // 只有在一级时才有获取三级的需求,但是这里没必要,这里把这个接口做的小一点,限制在只获取下一级,需要三级的话再在二级一一请求 global.DB.Where(&model.Category{ParentCategoryID: req.Id}).Find(&subCategorys) for _, subCategory := range subCategorys { subCategoryResponse = append(subCategoryResponse, &proto.CategoryInfoResponse{ Id: subCategory.ID, Name: subCategory.Name, Level: subCategory.Level, IsTab: subCategory.IsTab, ParentCategory: subCategory.ParentCategoryID, }) } // 3.填充返回值各字段-SubCategorys categoryListResponse.SubCategorys = subCategoryResponse return &categoryListResponse, nil }
- 这里需要写接口测试,可以在 test 目录写 UT 或者每个 handler 新建目录在 main 函数运行看效果(一个目录下只能有一个main)
- 品牌分类
- 查询品牌和分类数据表,这是个中间表,内容较多,相关操作也较为复杂一点
- model 定义如下
type GoodsCategoryBrand struct { BaseModel CategoryID int32 `gorm:"type:int;index:idx_category_brand,unique"` // 外键 Category Category // 外键约束,约束谁就指向谁 BrandsID int32 `gorm:"type:int;index:idx_category_brand,unique"` // 外键 Brands Brands }
- 查询品牌和分类数据表,这是个中间表,内容较多,相关操作也较为复杂一点
- 这些接口都是给 web 层调用,但有的是给用户准备数据,有的是给后台管理员准备
商品
- 商品接口,关系到最终展示给用户的数据,最重要
- 注:这部分有很多入口:搜索、新品、热门、价格区间、商品分类,都需要调用接口获取对应商品,这些都可以看做是查询条件
- 注意下面几点
- 条件是可以叠加的,所有用
localDB
接收前面过滤出的数据体 - 分类条件要考虑层级
- 条件是可以叠加的,所有用
- 后续会集成 elastic 用于搜索
小结
- 快速开发了商品微服务的 Service 层,服务配置项还是和 user_srv 相同,包括 nacos,mysql,consul,serverConfig,不需要更改定义
- 接下来是商品微服务 API 层的快速开发