商品服务
服务结构
创建 goods 服务,将之前 user 服务的基本结构迁移到 goods 服务上,完整目录是:
mxshop_srvs
user_srv
…
tmp
…
goods_srv
config
config.go 配置的读取表
global
global.go 数据库、日志初始化、全局变量定义
handler
xxx.go 拦截器
initialize
xxx.go 初始化信息
model
xxx.go 数据库表、数据库对象
proto proto 相关信息
xxx.proto
xxx.pb.go
xxx_grpc.pb.go
tests 测试信息
utils
xxx.go 工具
config-debug.yaml 配置信息
main.go 启动类
数据表结构
在 model 目录中创建需要的表结构对应的数据对象:
- 创建基础数据类:base.go:
package modelimport ("gorm.io/gorm""time"
)type BaseModel struct {ID int32 `gorm:"primarykey";type:int` // 注意这里对应数据库的int,我们进行统一定义,避免出现问题,若数据量过大也可以采用bigintCreatedAt time.Time `gorm:"column:add_time"`UpdatedAt time.Time `gorm:"column:update_time"`DeletedAt gorm.DeletedAtIsDeleted bool
}
- 创建商品表
package model// 商品分类数据对象:一级分类、二级分类...
type Category struct {BaseModelName string `gorm:"type:varchar(20);not null;"` // 分类名ParentCategoryID int32 // 父分类IDParentCategory *Category // 父分类对象 此处因为是自己指向自己,必须使用指针Level int32 `gorm:"type:int;not null;default:1"` // 分类级别IsTab bool `gorm:"default:false;not null"` // 是否显示在 Tab 栏
}// 品牌数据对象
type Brands struct {BaseModelName string `gorm:"type:varchar(20);not null"`Logo string `gorm:"type:varchar(200);default:'';not null"`
}// 品牌 / 类型 对应表
type GoodsCategoryBrand struct {BaseModelCategoryID int32 `gorm:"type:int;index:idx_category_brand,unique"`Category CategoryBrandsID int32 `gorm:"type:int;index:idx_category_brand,unique"`Brands Brands
}// 轮播图数据对象
type Banner struct {BaseModelImage string `gorm:"type:varchar(200);not null"`Url string `gorm:"type:varchar(200);not null"`Index int32 `gorm:"type:int;default:1;not null"`
}
- 商品对象的创建,注意:商品对象的创建较为复杂,单独拎出来处理:
// 商品表
type Goods struct {BaseModelCategoryID int32 `gorm:"type:int;not null"`Category CategoryBrandsID int32 `gorm:"type:int;not null"`Brands BrandsOnSale bool `gorm:"default:false;not null"` // 是否上架ShipFree bool `gorm:"default:false;not null"` // 是否xxxISNew 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"`
}
这里需要注意的是 对于 图片列表的处理,我们单独存储一个图片是没问题的,但是如果需要存储多个图片的话,我们就有两种方式选择了:
- 建立一个图片表,表里是所有的图片,每个图片存储一个归属商品,但这样的缺陷是无法避免连表操作,到后期数据量极大的时候,这种程度的连表能够造成极大的性能隐患。
- 直接将图片路径形成 json 格式的字符串,存储在表中,在 代码中通过 marshal 和 unmarshal 进行编码和解码,再进行图片的存取,这种方式有效规避了连表带来的性能损耗。
故而这里选用第二种方式。
这里就需要我们在 model/Base.go 中添加编解码的工具代码
type GormList []string// 设定这种变量在插入数据库的时候怎么插入:
// 这里是 将json格式的内容转换为 字符串再进行插入
func (g GormList) Value() (driver.Value, error) {return json.Marshal(g)
}// 设定在从数据库中取数据时,自动将数据转换为 []string 的列表
// 从数据库中取出来的时候是 GormList 数据类型,并将它的地址传入这个方法,直接修改其地址中的内容,将其修改为 []string
func (g *GormList) Scan(value interface{}) error {return json.Unmarshal(value.([]byte), &g)
}type BaseModel struct {ID int32 `gorm:"primarykey";type:int` // 注意这里对应数据库的int,我们进行统一定义,避免出现问题,若数据量过大也可以采用bigintCreatedAt time.Time `gorm:"column:add_time"`UpdatedAt time.Time `gorm:"column:update_time"`DeletedAt gorm.DeletedAtIsDeleted bool
}
这样我们存入的数据或者取出的数据只要是 定义为了 GormList 格式,就会在存入时自动转为字符串,取出是自动转为 json
之后进行数据库创建操作
这里我们默认数据库自行创建完成了,利用 gorm 来建表:
goods_srv/model/main/main.go
package mainimport ("log""os""time""gorm.io/driver/mysql""gorm.io/gorm""gorm.io/gorm/logger""gorm.io/gorm/schema""mxshop_srvs/goods_srv/model"
)// 这里的代码是用来在数据库中建表的
func main() {dsn := "root:123456@tcp(192.168.202.140:3306)/mxshop_goods_srv?charset=utf8mb4&parseTime=True&loc=Local"// 添加日志信息newLogger := logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), // io writerlogger.Config{SlowThreshold: time.Second, // Slow SQL thresholdLogLevel: logger.Info, // Log levelIgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for loggerParameterizedQueries: true, // Don't include params in the SQL logColorful: true, // Disable color,true is colorful, false to black and white},)db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{// 阻止向创建的数据库表后添加复数NamingStrategy: schema.NamingStrategy{SingularTable: true,},// 将日添加到配置中Logger: newLogger,})if err != nil {panic(err)}// 建表_ = db.AutoMigrate(&model.Category{}, &model.Brands{}, &model.GoodsCategoryBrand{}, &model.Banner{}, &model.Goods{})
}
这里相当于是一个商品的单元测试,用来做一些一次性的事情
protobuf 数据定义、定义所有的接口和请求和返回信息
注意这里,一定是先确定好需要的所有的需要的接口信息,再进行后续的接口定义操作
下面是全量的详细proto信息:
syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = ".;proto";// 在这里定义一个个的接口
service Goods {// 商品部分// 获取商品的接口,包括条件获取rpc GoodsList(GoodsFilterRequest) returns(GoodsListResponse);// 批量查询商品信息的接口,避免查商品时发生一个一个调用服务、一条一条查的低效情况rpc BatchGetGoods(BatchGoodsIdInfo) returns(GoodsListResponse);// 添加商品rpc CreateGoods(CreateGoodsInfo) returns(GoodsInfoResponse);// 删除商品,没有明确需要返回的信息,返回一个占位符rpc DeleteGoods(DeleteGoodsInfo) returns(google.protobuf.Empty);// 更新商品信息rpc UpdateGoods(CreateGoodsInfo) returns(google.protobuf.Empty);// 获取商品信息(单独获取)rpc GetGoodsDetail(GoodInfoRequest) returns(GoodsInfoResponse);// 分类部分// 获取所有商品分类rpc GetAllCategorysList(google.protobuf.Empty) returns(CategoryListResponse);// 获取子分类 todo 把这个补齐,文件在桌面上,视频已经看完了rpc GetSubCategory(CategoryListRequest) returns(SubCategoryListResponse);rpc CreateCategory(CategoryInfoRequest) returns(CategoryInfoResponse);rpc DeleteCategory(DeleteCategoryRequest) returns(google.protobuf.Empty);rpc UpdateCategory(CategoryInfoRequest) returns(google.protobuf.Empty);// 品牌部分rpc BrandList(BrandFilterRequest) returns(BrandListResponse);rpc CreateBrand(BrandRequest) returns(BrandInfoResponse);rpc DeleteBrand(BrandRequest) returns(google.protobuf.Empty);rpc UpdateBrand(BrandRequest) returns(google.protobuf.Empty);// 轮播图部分rpc BannerList(google.protobuf.Empty) returns(BannerListResponse);rpc CreateBanner(BannerRequest) returns(BannerResponse);rpc DeleteBranner(BannerRequest) returns(google.protobuf.Empty);rpc UpdateBanner(BannerRequest) returns(google.protobuf.Empty);// 品牌分类信息// 过滤需要的品牌、分类信息rpc CategoryBrandList(CategoryBrandFilterRequest) returns(CategoryBrandListResponse);// 获取某个分类下所有品牌的接口rpc GetCategoryBrandList(CategoryInfoRequest) returns(BrandListResponse);rpc CreateCategoryBrand(CategoryBrandRequest) returns(CategoryBrandResponse);rpc DeleteCategoryBrand(CategoryBrandRequest) returns(google.protobuf.Empty);rpc UpdateCategoryBrand(CategoryBrandRequest) returns(google.protobuf.Empty);
}// 在过滤商品时传入的条件信息
message GoodsFilterRequest {int32 priceMin = 1;int32 priceMax = 2;bool isHot = 3;bool isNew = 4;bool isTab = 5;int32 topCategory = 6;int32 pages = 7;int32 pagePerNums = 8;string keyWords = 9;int32 brand = 10;
}// 单独的一条关联信息
message CategoryBrandRequest{int32 id = 1;int32 categoryId = 2;int32 brandId = 3;
}// 返回的品牌、分类信息集合、也就是联系信息
message CategoryBrandListResponse {int32 total = 1;repeated CategoryBrandResponse data = 2;
}// 返回一个品牌信息、一个分类信息
message CategoryBrandResponse{int32 id = 1;BrandInfoResponse brand = 2;CategoryInfoResponse category = 3;
}// 轮播图的返回结果
message BannerListResponse {int32 total = 1;repeated BannerResponse data = 2;
}// 过滤品牌、分类信息请求
message CategoryBrandFilterRequest {int32 pages = 1;int32 pagePerNums = 2;
}// 单个轮播图
message BannerResponse {int32 id = 1;int32 index = 2;string image = 3;string url = 4;
}// 单个轮播图的请求
message BannerRequest {int32 id = 1;int32 index = 2;string image = 3;string url = 4;
}// 过滤品牌请求的信息
message BrandFilterRequest {int32 pages = 1;int32 pagePerNums = 2;
}// 品牌查询请求
message BrandRequest {int32 id = 1;string name = 2;string logo = 3;
}// 创建分类的请求信息
message CategoryInfoRequest {int32 id = 1;string name = 2;int32 parentCategory = 3;int32 level = 4;bool isTab = 5;
}// 传入删除信息的ID
message DeleteCategoryRequest {int32 id = 1;
}// 商品ID 列表,便于批量查询
message BatchGoodsIdInfo {repeated int32 id = 1;
}// 获取分类信息集合
message CategoryListResponse {int32 total = 1;repeated CategoryInfoResponse data = 2;string jsonData = 3;
}// 获取子分类集合(需要传入选中分类的id,level选传)
message CategoryListRequest {int32 id = 1;int32 level = 2;
}// 子分类的返回
message SubCategoryListResponse {int32 total = 1;CategoryInfoResponse info = 2; // 将本分类的所有信息返回repeated CategoryInfoResponse subCategorys = 3; // 将子分类的所有信息返回
}// 分类信息
message CategoryInfoResponse {int32 id = 1;string name = 2;int32 parentCategory = 3;int32 level = 4;bool isTab = 5;
}// 获取单独商品详情
message GoodInfoRequest {int32 id = 1;
}// 商品列表的返回信息
message GoodsListResponse {int32 total = 1;repeated GoodsInfoResponse data = 2;
}// 单个商品的信息
message GoodsInfoResponse {int32 id = 1;int32 categoryId = 2;string name = 3;string goodsSn = 4;int32 clickNum = 5;int32 soldNum = 6;int32 favNum = 7;float marketPrice = 9;float shopPrice = 10;string goodsBrief = 11;string goodsDesc = 12;bool shipFree = 13;repeated string images = 14;repeated string descImages = 15;string goodsFrontImage = 16;bool isNew = 17;bool isHot = 18;bool onSale = 19;int64 addTime = 20;CategoryBriefInfoResponse category = 21;BrandInfoResponse brand = 22;
}// 删除时传入一个ID
message DeleteGoodsInfo {int32 id = 1;
}// 创建商品去要传递的信息
message CreateGoodsInfo {int32 id = 1;string name = 2;string goodsSn = 3;int32 stocks = 7;float marketPrice = 8;float shopPrice = 9;string goodsBrief = 10;string goodsDesc = 11;bool shipFree = 12;repeated string images = 13;repeated string descImages = 14;string goodsFrontImage = 15;bool isNew = 16;bool isHot = 17;bool onSale = 18;int32 categoryId = 19;int32 brandId = 20;
}// 商品分类的简要信息
message CategoryBriefInfoResponse {int32 id = 1;string name = 2;
}message CategoryFilterRequest {int32 id = 1;string name = 2;
}// 品牌单个信息
message BrandInfoResponse {int32 id = 1;string name = 2;string logo = 3;
}// 品牌列表信息
message BrandListResponse {int32 total = 1;repeated BrandInfoResponse data = 2;
}
protobuf 文件的生成
在对应的文件夹目录下输入:
// 标准版
protoc --go_out=. xxxx.proto
// gprc 定制版
protoc --go_out=. --go-grpc_out=. *.proto
就可以在当前目录下创建好我们所需要的 xxxx.pb.go 文件,这个文件就是我们的以proto 作为传输协议的正式接口文件。
注意:此命令需要 protoc 环境完善,并配置好完整的环境变量(protoc 的环境变量)
protobuf 的构建
此时,我们发现,我们的接口过于多了,这就需要我们分开进行,我们在 .pb.go 文件中找到: GoodsServer,这里面定义的就是所有的接口,我们在 handler 文件中将所有的接口进行定义:
handler
banner.go
brands.go
goods.go
category.go
category_brand.go
示例定义(goods.go):
测试定义:若我们希望进行快速测试,就可以给 自己的 GoodsServer 添加一个属性,有这个属性存在,就可以进行服务器快速测试。
package handlerimport ("mxshop_srvs/goods_srv/proto"
)type GoodsServer struct {proto.UnimplementedGoodsServer
}
记得修改 main文件中的 注册:
proto.RegisterGoodsServer(server, &handler.GoodsServer{})
之后进行Nacos 相关配置:
创建一个新的命名空间、添加文件:goods-srv.json :
{"name": "goods-srv","tags": ["imooc", "bobby", "goods", "srv"],"web-host": "192.168.10.108","mysql": {"host": "192.168.202.140","port": 3306,"db": "mxshop_goods_srv","user": "root","password": "123456"},"consul": {"host": "192.168.202.140","port": 8500}
}
之后修改启动的配置文件的命名空间的ID
记得同步修改 config.go 中,新添加了一个 Tags 标签
type ServerConfig struct {// 这里是为了配置服务端口使用的,后期会移植到//Host string `mapstruce:"host" json:"host"`//Port int `mapstruct:"port" json:"port"`Name string `mapstructure:"name" json:"name"`Tags []string `mapstructure:"tags" json:"tags"`MysqlInfo MysqlConfig `mapstructure:"mysql" json:"mysql"`ConsulInfo ConsulConfig `mapstructure:"consul" json:"consul"`WebHost string `json:"web-host"`
}
然后在 main 中读取:
registration.Tags = global.ServerConfig.Tags
GRPC服务启动的相关信息
main.go:
package mainimport ("flag""fmt""go.uber.org/zap""mxshop_srvs/goods_srv/global""mxshop_srvs/goods_srv/initialize""mxshop_srvs/goods_srv/utils""net""os""os/signal""syscall""github.com/hashicorp/consul/api""github.com/satori/go.uuid""google.golang.org/grpc""google.golang.org/grpc/health""google.golang.org/grpc/health/grpc_health_v1""mxshop_srvs/goods_srv/handler""mxshop_srvs/goods_srv/proto"
)func main() {// 由于ip和端口号有可能需要用户输入,所以这里摘出来// flag 包是一个命令行工具包,允许从命令行中设置参数IP := flag.String("ip", "0.0.0.0", "ip地址")Port := flag.Int("port", 0, "端口号")initialize.InitLogger()initialize.InitConfig()flag.Parse()fmt.Println("ip: ", *IP)// 设置端口号自动获取if *Port == 0 {*Port, _ = utils.GetFreePort()}fmt.Println("port: ", *Port)// 创建新服务器server := grpc.NewServer()// 注册自己的已实现的方法进来proto.RegisterGoodsServer(server, &handler.GoodsServer{})//lis, err := net.Listen("tcp", fmt.Sprintf("192.168.202.140:8021"))lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))if err != nil {panic("failed to listen" + err.Error())}// 绑定服务健康检查grpc_health_v1.RegisterHealthServer(server, health.NewServer())// 服务注册cfg := api.DefaultConfig()cfg.Address = fmt.Sprintf("%s:%d", global.ServerConfig.ConsulInfo.Host, global.ServerConfig.ConsulInfo.Port)client, err := api.NewClient(cfg)if err != nil {panic(err)}check := &api.AgentServiceCheck{GRPC: fmt.Sprintf("%s:%d", global.ServerConfig.Host, *Port),Interval: "5s",//Timeout: "10s",DeregisterCriticalServiceAfter: "30s",}registration := new(api.AgentServiceRegistration)registration.Address = global.ServerConfig.Host//registration.Address = "127.0.0.1"//registration.ID = global.ServerConfig.Name // 此处修改为使用 UUID 生成serviceID := fmt.Sprintf("%s", uuid.NewV4()) // 此处修改为使用 UUID 生成registration.ID = serviceIDregistration.Port = *Portregistration.Tags = global.ServerConfig.Tagsregistration.Name = global.ServerConfig.Nameregistration.Check = checkerr = client.Agent().ServiceRegister(registration)if err != nil {panic(err)}//err = server.Serve(lis)// 注意此处是阻塞式的所以需要一个 goroutine 来进行异步操作// 将自己的服务绑定端口go func() {err = server.Serve(lis)if err != nil {panic("fail to start grpc" + err.Error())}}()// 创建一个通道quit := make(chan os.Signal)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)// 阻塞住,若接到请求则放通,直接将服务注销<-quitif err = client.Agent().ServiceDeregister(serviceID); err != nil {zap.S().Info("注销失败...")}zap.S().Info("注销成功")
}
InitConfig:(配置文件的相关信息)
intialize/config.go:
package initializeimport ("encoding/json""fmt""github.com/nacos-group/nacos-sdk-go/clients""github.com/nacos-group/nacos-sdk-go/vo""github.com/nacos-group/nacos-sdk-go/common/constant""github.com/spf13/viper""go.uber.org/zap""mxshop_srvs/goods_srv/global"
)func GetEnvInfo(env string) bool {viper.AutomaticEnv()var rs boolrs = viper.GetBool(env)return rsreturn true
}func InitConfig() {debug := GetEnvInfo("MXSHOP-DEBUG")zap.S().Info(fmt.Sprintf("------------", debug))configFileNamePrefix := "config"configFileName := fmt.Sprintf("goods_srv/%s-pro.yaml", configFileNamePrefix)if debug {configFileName = fmt.Sprintf("goods_srv/%s-debug.yaml", configFileNamePrefix)}v := viper.New()v.SetConfigFile(configFileName)if err := v.ReadInConfig(); err != nil {panic(err)}// 将配置文件进行解析if err := v.Unmarshal(&global.NacosConfig); err != nil {panic(err)}sc := []constant.ServerConfig{{IpAddr: global.NacosConfig.Host,Port: global.NacosConfig.Port,},}cc := constant.ClientConfig{TimeoutMs: 5000,NamespaceId: global.NacosConfig.Namespace,CacheDir: "tmp/nacos/cache",NotLoadCacheAtStart: true,LogDir: "tmp/nacos/log",LogLevel: "debug",}configClient, err := clients.CreateConfigClient(map[string]interface{}{"serverConfigs": sc,"clientConfig": cc,})if err != nil {zap.S().Fatalf("%s", err.Error())}content, err := configClient.GetConfig(vo.ConfigParam{DataId: global.NacosConfig.Dataid,Group: global.NacosConfig.Group,})if err != nil {zap.S().Fatalf("%s", err.Error())}err = configClient.ListenConfig(vo.ConfigParam{DataId: global.NacosConfig.Dataid,Group: global.NacosConfig.Group,OnChange: func(namespace, group, dataId, data string) {fmt.Println("配置文件发生变化")fmt.Println("namespace: " + namespace)fmt.Println("group: " + group)fmt.Println("dataId: " + dataId)fmt.Println("data: " + data)},})if err != nil {zap.S().Fatalf("%s", err.Error())}err = json.Unmarshal([]byte(content), &global.ServerConfig)if err != nil {zap.S().Fatalf("%s", err.Error())}zap.S().Info(global.ServerConfig)
}
此处还需要注意配置文件和 Nacos 的配置文件:
config-debug.yml
host: '192.168.202.140'
port: 8848
namespace: '043d2547-bd1e-44df-b097-75f649848099'
user: 'nacos'
password: 'nacos'
dataid: 'goods-srv.json'
group: 'dev'
Nacos配置:
{"name": "goods-srv","host": "192.168.10.107","mysql": {"host": "192.168.202.140","port": 3306,"db": "mxshop_user_srv","user": "root","password": "123456"},"consul": {"host": "192.168.202.140","port": 8500}
}