商品服務
服務結構
創建 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}
}