目录
- 前言
- 一、需求分析
-
- 1 - 数据库实体分析
- 2 - 商品微服务接口分析
- 3 - 商品服务表结构设计
- 4 - 生成表结构与数据导入
- 二、启动商品服务
-
- 1 - proto接口
- 2 - handle中实现接口
- 3 - nacos配置
- 4 - 修改yaml配置
- 5 - 读取Tags和本机host
- 三、品牌、商品分类、轮播接口
-
- 1 - 接口测试
- 2 - 品牌列表查询实现
- 3 - 品牌新建、删除、更新接口
- 4 - 轮播图的CRUD
- 5 - 商品分类列表接口
- 6 - 商品分类子分类接口
- 7 - 商品新建、删除和更新接口
- 8 - 品牌分类相关接口
- 四、商品接口
-
- 1 - 商品列表接口实现
- 2 - 批量获取商品、获取商品详情
- 3 - 新增、修改、更新商品
- 五、完整源码
前言
- 之前我们已经通过grpc从零开始到实现了用户的微服务,接来我们将实现商品微服务
一、需求分析
1 - 数据库实体分析
2 - 商品微服务接口分析
3 - 商品服务表结构设计
- goods_srv/model/base.go:公共的base字段;自定义的GormList类型
package model
import (
"database/sql/driver"
"encoding/json"
"gorm.io/gorm"
"time"
)
type BaseModel struct {
ID int32 `gorm:"primarykey;type:int" json:"id"`
CreatedAt time.Time `gorm:"column:add_time" json:"-"`
UpdatedAt time.Time `gorm:"column:update_time" json:"-"`
DeletedAt gorm.DeletedAt `json:"-"`
IsDeleted bool `json:"-"`
}
type GormList []string
func (g GormList) Value() (driver.Value, error) {
return json.Marshal(g)
}
func (g *GormList) Scan(value interface{}) error {
return json.Unmarshal(value.([]byte), &g)
}
- goods_srv/model/goods.go:商品表结构类型定义
package model
type Category struct {
BaseModel
Name string `gorm:"type:varchar(20);not null" json:"name"`
ParentCategoryID int32 `json:"parent"`
ParentCategory *Category `json:"-"`
SubCategory []*Category `gorm:"foreignKey:ParentCategoryID;references:ID" json:"sub_category"`
Level int32 `gorm:"type:int;not null;default:1;comment:'1为1级类目,2为2级...'" json:"level"`
IsTab bool `gorm:"default:false;not null;comment:'能否展示在Tab栏'" json:"is_tab"`
}
type Brands struct {
BaseModel
Name string `gorm:"type:varchar(20);not null;comment:'品牌名称'"`
Logo string `gorm:"type:varchar(200);default:'';not null;comment:'品牌图标'"`
}
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
}
func (GoodsCategoryBrand) TableName() string {
return "goodscategorybrand"
}
type Banner struct {
BaseModel
Image 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 {
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;comment:'是否上架'"`
ShipFree bool `gorm:"default:false;not null;comment:'是否免运费'"`
IsNew bool `gorm:"default:false;not null;comment:'是否新品'"`
IsHot bool `gorm:"default:false;not null;comment:'是否热卖商品'"`
Name string `gorm:"type:varchar(50);not null"`
GoodsSn string `gorm:"type:varchar(50);not null;comment:'商家的内部编号'"`
ClickNum int32 `gorm:"type:int;default:0;not null;comment:'点击数'"`
SoldNum int32 `gorm:"type:int;default:0;not null;comment:'销售量'"`
FavNum int32 `gorm:"type:int;default:0;not null;comment:'收藏数'"`
MarketPrice float32 `gorm:"not null;comment:'商品价格'"`
ShopPrice float32 `gorm:"not null;comment:'实际价格'"`
GoodsBrief string `gorm:"type:varchar(100);not null;comment:'商品简介'"`
Images GormList `gorm:"type:varchar(1000);not null;comment:'商品图片'"`
DescImages GormList `gorm:"type:varchar(1000);not null;comment:'商品图片'"`
GoodsFrontImage string `gorm:"type:varchar(200);not null;comment:'商品展示图'"`
}
4 - 生成表结构与数据导入
- 新建数据库:mxshop_goods_srv
- goods_srv/model/main/main.go:ip和数据库账号密码自行修改
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"nd/goods_srv/model"
"os"
"time"
)
func main() {
dsn := "root:jiushi@tcp(192.168.124.51:3306)/mxshop_goods_srv?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Info,
Colorful: true,
},
)
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{})
}
二、启动商品服务
1 - proto接口
- goods_srv/proto/goods.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);
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 DeleteBanner(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 CategoryListRequest {
int32 id = 1;
int32 level = 2;
}
message CategoryInfoRequest {
int32 id = 1;
string name = 2;
int32 parentCategory = 3;
int32 level = 4;
bool isTab = 5;
}
message DeleteCategoryRequest {
int32 id = 1;
}
message QueryCategoryRequest {
int32 id = 1;
string name = 2;
}
message CategoryInfoResponse {
int32 id = 1;
string name = 2;
int32 parentCategory = 3;
int32 level = 4;
bool isTab = 5;
}
message CategoryListResponse {
int32 total = 1;
repeated CategoryInfoResponse data = 2;
string jsonData = 3;
}
message SubCategoryListResponse {
int32 total = 1;
CategoryInfoResponse info = 2;
repeated CategoryInfoResponse subCategorys = 3;
}
message CategoryBrandFilterRequest {
int32 pages = 1;
int32 pagePerNums = 2;
}
message FilterRequest {
int32 pages = 1;
int32 pagePerNums = 2;
}
message CategoryBrandRequest{
int32 id = 1;
int32 categoryId = 2;
int32 brandId = 3;
}
message CategoryBrandResponse{
int32 id = 1;
BrandInfoResponse brand = 2;
CategoryInfoResponse category = 3;
}
message BannerRequest {
int32 id = 1;
int32 index = 2;
string image = 3;
string url = 4;
}
message BannerResponse {
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 BrandInfoResponse {
int32 id = 1;
string name = 2;
string logo = 3;
}
message BrandListResponse {
int32 total = 1;
repeated BrandInfoResponse data = 2;
}
message BannerListResponse {
int32 total = 1;
repeated BannerResponse data = 2;
}
message CategoryBrandListResponse {
int32 total = 1;
repeated CategoryBrandResponse data = 2;
}
message BatchGoodsIdInfo {
repeated int32 id = 1;
}
message DeleteGoodsInfo {
int32 id = 1;
}
message CategoryBriefInfoResponse {
int32 id = 1;
string name = 2;
}
message CategoryFilterRequest {
int32 id = 1;
bool isTab = 2;
}
message GoodInfoRequest {
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 GoodsReduceRequest {
int32 GoodsId = 1;
int32 nums = 2;
}
message BatchCategoryInfoRequest {
repeated int32 id = 1;
int32 goodsNums = 2;
int32 brandNums = 3;
}
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 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;
}
message GoodsListResponse {
int32 total = 1;
repeated GoodsInfoResponse data = 2;
}
- 生成pb文件:
protoc --go_out=. --go_opt=paths=import --go-grpc_out=. --go-grpc_opt=paths=import *.proto
2 - handle中实现接口
- goods_srv/handler/handle_goods.go:这里我们可以先不用实现,使用
proto.UnimplementedGoodsServer
package handler
import (
"context"
"google.golang.org/protobuf/types/known/emptypb"
"nd/goods_srv/proto"
)
type GoodsServer struct {
proto.UnimplementedGoodsServer
}
3 - nacos配置
- 添加命名空间goods
- 克隆之前user_srv的配置到goods_srv中
- 编辑goods_srv.json的配置:将Tags配置也放入,以及本机的Host也加入配置
4 - 修改yaml配置
host: '192.168.124.51'
port: 8848
namespace: 'b2f00e3b-4e22-4e87-8129-9c02b5f1220d'
user: 'nacos'
password: 'nacos'
dataid: 'goods_srv.json'
group: 'dev'
host: '192.168.124.51'
port: 8848
namespace: 'b2f00e3b-4e22-4e87-8129-9c02b5f1220d'
user: 'nacos'
password: 'nacos'
dataid: 'goods_srv.json'
group: 'pro'
5 - 读取Tags和本机host
- goods_srv/config/config.go:ServerConfig中添加Tags
package config
type MysqlConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
Name string `mapstructure:"db" json:"db"`
User string `mapstructure:"user" json:"user"`
Password string `mapstructure:"password" json:"password"`
}
type ConsulConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
}
type ServerConfig struct {
Name string `mapstructure:"name" json:"name"`
Tags []string `mapstructure:"tags" json:"tags"`
MysqlInfo MysqlConfig `mapstructure:"mysql" json:"mysql"`
ConsulInfo ConsulConfig `mapstructure:"consul" json:"consul"`
}
type NacosConfig struct {
Host string `mapstructure:"host"`
Port uint64 `mapstructure:"port"`
Namespace string `mapstructure:"namespace"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DataId string `mapstructure:"dataid"`
Group string `mapstructure:"group"`
}
- goods_srv/main.go:
- 注册时使用:
registration.Tags = global.ServerConfig.Tags
- 健康检查与服务注册的host使用:
global.ServerConfig.Host
package main
import (
"flag"
"fmt"
"github.com/hashicorp/consul/api"
"github.com/satori/go.uuid"
"go.uber.org/zap"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"nd/goods_srv/global"
"nd/goods_srv/handler"
"nd/goods_srv/initialize"
"nd/goods_srv/proto"
"nd/goods_srv/utils"
"net"
"os"
"os/signal"
"syscall"
"google.golang.org/grpc"
)
func main() {
IP := flag.String("ip", "0.0.0.0", "ip地址")
Port := flag.Int("port", 0, "端口号")
initialize.InitLogger()
initialize.InitConfig()
initialize.InitDB()
zap.S().Info(global.ServerConfig)
flag.Parse()
zap.S().Info("ip: ", *IP)
if *Port == 0 {
*Port, _ = utils.GetFreePort()
}
zap.S().Info("port: ", *Port)
server := grpc.NewServer()
proto.RegisterGoodsServer(server, &handler.GoodsServer{})
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),
Timeout: "5s",
Interval: "5s",
DeregisterCriticalServiceAfter: "15s",
}
registration := new(api.AgentServiceRegistration)
registration.Name = global.ServerConfig.Name
serviceID := fmt.Sprintf("%s", uuid.NewV4())
registration.ID = serviceID
registration.Port = *Port
registration.Tags = global.ServerConfig.Tags
registration.Address = global.ServerConfig.Host
registration.Check = check
err = client.Agent().ServiceRegister(registration)
if err != nil {
panic(err)
}
go func() {
err = server.Serve(lis)
if err != nil {
panic("failed to start grpc:" + err.Error())
}
}()
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
if err = client.Agent().ServiceDeregister(serviceID); err != nil {
zap.S().Info("注销失败")
}
zap.S().Info("注销成功")
}
三、品牌、商品分类、轮播接口
1 - 接口测试
- goods_srv/main.go:为了让测试的时候端口不会变化,我们在main中修改port为50058
func main() {
IP := flag.String("ip", "0.0.0.0", "ip地址")
Port := flag.Int("port", 50058, "端口号")
- goods_srv/tests/test_brands.go:添加测试方法
package main
import (
"context"
"fmt"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc"
"nd/goods_srv/proto"
)
var brandClient proto.GoodsClient
var conn *grpc.ClientConn
func TestGetBrandList() {
rsp, err := brandClient.BrandList(context.Background(), &proto.BrandFilterRequest{})
if err != nil {
panic(err)
}
fmt.Println(rsp.Total)
for _, brand := range rsp.Data {
fmt.Println(brand.Name)
}
}
func Init() {
var err error
conn, err = grpc.Dial("127.0.0.1:50058", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
brandClient = proto.NewGoodsClient(conn)
}
func main() {
Init()
TestGetBrandList()
conn.Close()
}
- 因为之前使用的是proto.UnimplementedGoodsServer:我们期望的返回是rpc接口未定义
2 - 品牌列表查询实现
- goods_srv/handler/handle_base.go:添加分页的逻辑
package handler
import "gorm.io/gorm"
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page == 0 {
page = 1
}
switch {
case pageSize > 100:
pageSize = 100
case pageSize <= 0:
pageSize = 10
}
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
- goods_srv/handler/handle_brands.go:实现BrandList接口
package handler
import (
"context"
"nd/goods_srv/global"
"nd/goods_srv/model"
"nd/goods_srv/proto"
)
func (s *GoodsServer) BrandList(ctx context.Context, req *proto.BrandFilterRequest) (*proto.BrandListResponse, error) {
brandListResponse := proto.BrandListResponse{}
var brands []model.Brands
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)
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
}
- goods_srv/tests/test_brands.go:测试接口实现
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"nd/goods_srv/proto"
)
var brandClient proto.GoodsClient
var conn *grpc.ClientConn
func TestGetBrandList() {
rsp, err := brandClient.BrandList(context.Background(), &proto.BrandFilterRequest{})
if err != nil {
panic(err)
}
fmt.Println(rsp.Total)
for _, brand := range rsp.Data {
fmt.Println(brand.Name)
}
}
func Init() {
var err error
conn, err = grpc.Dial("127.0.0.1:50058", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
brandClient = proto.NewGoodsClient(conn)
}
func main() {
Init()
TestGetBrandList()
conn.Close()
}
3 - 品牌新建、删除、更新接口
- goods_srv/handler/handle_brands.go:实现CreateBrand、DeleteBrand、UpdateBrand
package handler
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"nd/goods_srv/global"
"nd/goods_srv/model"
"nd/goods_srv/proto"
)
func (s *GoodsServer) BrandList(ctx context.Context, req *proto.BrandFilterRequest) (*proto.BrandListResponse, error) {
brandListResponse := proto.BrandListResponse{}
var brands []model.Brands
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)
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
}
func (s *GoodsServer) CreateBrand(ctx context.Context, req *proto.BrandRequest) (*proto.BrandInfoResponse, error) {
if result := global.DB.Where("name=?", req.Name).First(&model.Brands{}); result.RowsAffected == 1 {
return nil, status.Errorf(codes.InvalidArgument, "品牌已存在")
}
brand := &model.Brands{
Name: req.Name,
Logo: req.Logo,
}
global.DB.Save(brand)
return &proto.BrandInfoResponse{Id: brand.ID}, nil
}
func (s *GoodsServer) DeleteBrand(ctx context.Context, req *proto.BrandRequest) (*emptypb.Empty, error) {
if result := global.DB.Delete(&model.Brands{}, req.Id); result.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "品牌不存在")
}
return &emptypb.Empty{}, nil
}
func (s *GoodsServer) UpdateBrand(ctx context.Context, req *proto.BrandRequest) (*emptypb.Empty, error) {
brands := model.Brands{}
if result := global.DB.First(&brands); result.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "品牌不存在")
}
if req.Name != "" {
brands.Name = req.Name
}
if req.Logo != "" {
brands.Logo = req.Logo
}
global.DB.Save(&brands)
return &emptypb.Empty{}, nil
}
4 - 轮播图的CRUD
- goods_srv/handler/handle_banner.go
package handler
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"nd/goods_srv/global"
"nd/goods_srv/model"
"nd/goods_srv/proto"
)
func (s *GoodsServer) BannerList(ctx context.Context, req *emptypb.Empty) (*proto.BannerListResponse, error) {
bannerListResponse := proto.BannerListResponse{}
var banners []model.Banner
result := global.DB.Find(&banners)
bannerListResponse.Total = int32(result.RowsAffected)
var bannerReponses []*proto.BannerResponse
for _, banner := range banners {
bannerReponses = append(bannerReponses, &proto.BannerResponse{
Id: banner.ID,
Image: banner.Image,
Index: banner.Index,
Url: banner.Url,
})
}
bannerListResponse.Data = bannerReponses
return &bannerListResponse, nil
}
func (s *GoodsServer) CreateBanner(ctx context.Context, req *proto.BannerRequest) (*proto.BannerResponse, error) {
banner := model.Banner{}
banner.Image = req.Image
banner.Index = req.Index
banner.Url = req.Url
global.DB.Save(&banner)
return &proto.BannerResponse{Id: banner.ID}, nil
}
func (s *GoodsServer) DeleteBanner(ctx context.Context, req *proto.BannerRequest) (*emptypb.Empty, error) {
if result := global.DB.Delete(&model.Banner{}, req.Id); result.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "轮播图不存在")
}
return &emptypb.Empty{}, nil
}
func (s *GoodsServer) UpdateBanner(ctx context.Context, req *proto.BannerRequest) (*emptypb.Empty, error) {
var banner model.Banner
if result := global.DB.First(&banner, req.Id); result.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "轮播图不存在")
}
if req.Url != "" {
banner.Url = req.Url
}
if req.Image != "" {
banner.Image = req.Image
}
if req.Index != 0 {
banner.Index = req.Index
}
global.DB.Save(&banner)
return &emptypb.Empty{}, nil
}
5 - 商品分类列表接口
- goods_srv/handler/handle_category.go
func (s *GoodsServer) GetAllCategorysList(context.Context, *emptypb.Empty) (*proto.CategoryListResponse, error) {
var categorys []model.Category
global.DB.Where(&model.Category{Level: 1}).Preload("SubCategory.SubCategory").Find(&categorys)
b, _ := json.Marshal(&categorys)
return &proto.CategoryListResponse{JsonData: string(b)}, nil
}
- goods_srv/tests/category/test_category.go:添加测试方法
package main
import (
"context"
"fmt"
"github.com/golang/protobuf/ptypes/empty"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"nd/goods_srv/tests"
"nd/goods_srv/proto"
)
var brandClient proto.GoodsClient
var conn *grpc.ClientConn
func TestGetCategoryList() {
rsp, err := brandClient.GetAllCategorysList(context.Background(), &empty.Empty{})
if err != nil {
panic(err)
}
fmt.Println(rsp.Total)
fmt.Println(rsp.JsonData)
}
func Init() {
var err error
conn, err = grpc.Dial(tests.TargetAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
brandClient = proto.NewGoodsClient(conn)
}
func main() {
Init()
TestGetCategoryList()
conn.Close()
}
6 - 商品分类子分类接口
- goods_srv/handler/handle_category.go:
func (s *GoodsServer) GetSubCategory(ctx context.Context, req *proto.CategoryListRequest) (*proto.SubCategoryListResponse, error) {
categoryListResponse := proto.SubCategoryListResponse{}
var category model.Category
if result := global.DB.First(&category, req.Id); result.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品分类不存在")
}
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
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,
})
}
categoryListResponse.SubCategorys = subCategoryResponse
return &categoryListResponse, nil
}
- goods_srv/tests/category/test_category.go
func TestGetSubCategoryList() {
rsp, err := brandClient.GetSubCategory(context.Background(), &proto.CategoryListRequest{
Id: 1001,
})
if err != nil {
panic(err)
}
fmt.Println(rsp.SubCategorys)
}
7 - 商品新建、删除和更新接口
- goods_srv/handler/handle_category.go
func (s *GoodsServer) CreateCategory(ctx context.Context, req *proto.CategoryInfoRequest) (*proto.CategoryInfoResponse, error) {
category := model.Category{}
cMap := map[string]interface{}{}
cMap["name"] = req.Name
cMap["level"] = req.Level
cMap["is_tab"] = req.IsTab
if req.Level != 1 {
cMap["parent_category_id"] = req.ParentCategory
}
tx := global.DB.Model(&model.Category{}).Create(cMap)
fmt.Println(tx)
return &proto.CategoryInfoResponse{Id: int32(category.ID)}, nil
}
func (s *GoodsServer) DeleteCategory(ctx context.Context, req *proto.DeleteCategoryRequest) (*emptypb.Empty, error) {
if result := global.DB.Delete(&model.Category{}, req.Id); result.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品分类不存在")
}
return &emptypb.Empty{}, nil
}
func (s *GoodsServer) UpdateCategory(ctx context.Context, req *proto.CategoryInfoRequest) (*emptypb.Empty, error) {
var category model.Category
if result := global.DB.First(&category, req.Id); result.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品分类不存在")
}
if req.Name != "" {
category.Name = req.Name
}
if req.ParentCategory != 0 {
category.ParentCategoryID = req.ParentCategory
}
if req.Level != 0 {
category.Level = req.Level
}
if req.IsTab {
category.IsTab = req.IsTab
}
global.DB.Save(&category)
return &emptypb.Empty{}, nil
}
8 - 品牌分类相关接口
- goods_srv/handler/handle_category_brand.go
package handler
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"nd/goods_srv/global"
"nd/goods_srv/model"
"nd/goods_srv/proto"
)
func (s *GoodsServer) CategoryBrandList(ctx context.Context, req *proto.CategoryBrandFilterRequest) (*proto.CategoryBrandListResponse, error) {
var categoryBrands []model.GoodsCategoryBrand
categoryBrandListResponse := proto.CategoryBrandListResponse{}
var total int64
global.DB.Model(&model.GoodsCategoryBrand{}).Count(&total)
categoryBrandListResponse.Total = int32(total)
global.DB.Preload("Category").Preload("Brands").Scopes(Paginate(int(req.Pages), int(req.PagePerNums))).Find(&categoryBrands)
var categoryResponses []*proto.CategoryBrandResponse
for _, categoryBrand := range categoryBrands {
categoryResponses = append(categoryResponses, &proto.CategoryBrandResponse{
Category: &proto.CategoryInfoResponse{
Id: categoryBrand.Category.ID,
Name: categoryBrand.Category.Name,
Level: categoryBrand.Category.Level,
IsTab: categoryBrand.Category.IsTab,
ParentCategory: categoryBrand.Category.ParentCategoryID,
},
Brand: &proto.BrandInfoResponse{
Id: categoryBrand.Brands.ID,
Name: categoryBrand.Brands.Name,
Logo: categoryBrand.Brands.Logo,
},
})
}
categoryBrandListResponse.Data = categoryResponses
return &categoryBrandListResponse, nil
}
func (s *GoodsServer) GetCategoryBrandList(ctx context.Context, req *proto.CategoryInfoRequest) (*proto.BrandListResponse, error) {
brandListResponse := proto.BrandListResponse{}
var category model.Category
if result := global.DB.Find(&category, req.Id).First(&category); result.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "商品分类不存在")
}
var categoryBrands []model.GoodsCategoryBrand
if result := global.DB.Preload("Brands").Where(&model.GoodsCategoryBrand{CategoryID: req.Id}).Find(&categoryBrands); result.RowsAffected > 0 {
brandListResponse.Total = int32(result.RowsAffected)
}
var brandInfoResponses []*proto.BrandInfoResponse
for _, categoryBrand := range categoryBrands {
brandInfoResponses = append(brandInfoResponses, &proto.BrandInfoResponse{
Id: categoryBrand.Brands.ID,
Name: categoryBrand.Brands.Name,
Logo: categoryBrand.Brands.Logo,
})
}
brandListResponse.Data = brandInfoResponses
return &brandListResponse, nil
}
func (s *GoodsServer) CreateCategoryBrand(ctx context.Context, req *proto.CategoryBrandRequest) (*proto.CategoryBrandResponse, error) {
var category model.Category
if result := global.DB.First(&category, req.CategoryId); result.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "商品分类不存在")
}
var brand model.Brands
if result := global.DB.First(&brand, req.BrandId); result.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "品牌不存在")
}
categoryBrand := model.GoodsCategoryBrand{
CategoryID: req.CategoryId,
BrandsID: req.BrandId,
}
global.DB.Save(&categoryBrand)
return &proto.CategoryBrandResponse{Id: categoryBrand.ID}, nil
}
func (s *GoodsServer) DeleteCategoryBrand(ctx context.Context, req *proto.CategoryBrandRequest) (*emptypb.Empty, error) {
if result := global.DB.Delete(&model.GoodsCategoryBrand{}, req.Id); result.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "品牌分类不存在")
}
return &emptypb.Empty{}, nil
}
func (s *GoodsServer) UpdateCategoryBrand(ctx context.Context, req *proto.CategoryBrandRequest) (*emptypb.Empty, error) {
var categoryBrand model.GoodsCategoryBrand
if result := global.DB.First(&categoryBrand, req.Id); result.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "品牌分类不存在")
}
var category model.Category
if result := global.DB.First(&category, req.CategoryId); result.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "商品分类不存在")
}
var brand model.Brands
if result := global.DB.First(&brand, req.BrandId); result.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "品牌不存在")
}
categoryBrand.CategoryID = req.CategoryId
categoryBrand.BrandsID = req.BrandId
global.DB.Save(&categoryBrand)
return &emptypb.Empty{}, nil
}
- goods_srv/tests/category_brand/test_category_brand.go:测试接口
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"nd/goods_srv/proto"
"nd/goods_srv/tests"
)
var brandClient proto.GoodsClient
var conn *grpc.ClientConn
func TestGetCategoryBrandList() {
rsp, err := brandClient.CategoryBrandList(context.Background(), &proto.CategoryBrandFilterRequest{})
if err != nil {
panic(err)
}
fmt.Println(rsp.Total)
fmt.Println(rsp.Data)
}
func Init() {
var err error
conn, err = grpc.Dial(tests.TargetAddr, grpc.WithInsecure())
if err != nil {
panic(err)
}
brandClient = proto.NewGoodsClient(conn)
}
func main() {
Init()
TestGetCategoryBrandList()
conn.Close()
}
四、商品接口
1 - 商品列表接口实现
- goods_srv/handler/handle_goods.go:根据条件筛选查询商品
package handler
import (
"context"
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"nd/goods_srv/global"
"nd/goods_srv/model"
"nd/goods_srv/proto"
)
type GoodsServer struct {
proto.UnimplementedGoodsServer
}
func ModelToResponse(goods model.Goods) proto.GoodsInfoResponse {
return proto.GoodsInfoResponse{
Id: goods.ID,
CategoryId: goods.CategoryID,
Name: goods.Name,
GoodsSn: goods.GoodsSn,
ClickNum: goods.ClickNum,
SoldNum: goods.SoldNum,
FavNum: goods.FavNum,
MarketPrice: goods.MarketPrice,
ShopPrice: goods.ShopPrice,
GoodsBrief: goods.GoodsBrief,
ShipFree: goods.ShipFree,
GoodsFrontImage: goods.GoodsFrontImage,
IsNew: goods.IsNew,
IsHot: goods.IsHot,
OnSale: goods.OnSale,
DescImages: goods.DescImages,
Images: goods.Images,
Category: &proto.CategoryBriefInfoResponse{
Id: goods.Category.ID,
Name: goods.Category.Name,
},
Brand: &proto.BrandInfoResponse{
Id: goods.Brands.ID,
Name: goods.Brands.Name,
Logo: goods.Brands.Logo,
},
}
}
func (s *GoodsServer) GoodsList(ctx context.Context, req *proto.GoodsFilterRequest) (*proto.GoodsListResponse, error) {
goodsListResponse := &proto.GoodsListResponse{}
var goods []model.Goods
localDB := global.DB.Model(model.Goods{})
if req.KeyWords != "" {
localDB = localDB.Where("name LIKE ?", "%"+req.KeyWords+"%")
}
if req.IsHot {
localDB = localDB.Where(model.Goods{IsHot: true})
}
if req.IsNew {
localDB = localDB.Where(model.Goods{IsNew: true})
}
if req.PriceMin > 0 {
localDB = localDB.Where("shop_price >= ?", req.PriceMin)
}
if req.PriceMax > 0 {
localDB = localDB.Where("shop_price <= ?", req.PriceMax)
}
if req.Brand > 0 {
localDB = localDB.Where("brand_id = ?", req.Brand)
}
var subQuery string
if req.TopCategory > 0 {
var category model.Category
if result := global.DB.First(&category, req.TopCategory); result.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品分类不存在")
}
if category.Level == 1 {
subQuery = fmt.Sprintf("select id from category where parent_category_id in (select id from category WHERE parent_category_id=%d)", req.TopCategory)
} else if category.Level == 2 {
subQuery = fmt.Sprintf("select id from category WHERE parent_category_id=%d", req.TopCategory)
} else if category.Level == 3 {
subQuery = fmt.Sprintf("select id from category WHERE id=%d", req.TopCategory)
}
localDB = localDB.Where(fmt.Sprintf("category_id in (%s)", subQuery))
}
var count int64
localDB.Count(&count)
goodsListResponse.Total = int32(count)
result := localDB.Preload("Category").Preload("Brands").Scopes(Paginate(int(req.Pages), int(req.PagePerNums))).Find(&goods)
if result.Error != nil {
return nil, result.Error
}
for _, good := range goods {
goodsInfoResponse := ModelToResponse(good)
goodsListResponse.Data = append(goodsListResponse.Data, &goodsInfoResponse)
}
return goodsListResponse, nil
}
- goods_srv/tests/goods/test_goods.go:goods接口测试
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"nd/goods_srv/proto"
"nd/goods_srv/tests"
)
var brandClient proto.GoodsClient
var conn *grpc.ClientConn
func Init() {
var err error
conn, err = grpc.Dial(tests.TargetAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
brandClient = proto.NewGoodsClient(conn)
}
func TestGetGoodsList() {
rsp, err := brandClient.GoodsList(context.Background(), &proto.GoodsFilterRequest{
PriceMin: 10,
KeyWords: "水",
})
if err != nil {
panic(err)
}
fmt.Println(rsp.Total)
for _, good := range rsp.Data {
fmt.Println(good.Name, good.ShopPrice)
}
}
func main() {
Init()
TestGetGoodsList()
conn.Close()
}
- 为了方便测试,我们先注释掉Images和DescImages
IsHot: goods.IsHot,
OnSale: goods.OnSale,
Category: &proto.CategoryBriefInfoResponse{
Id: goods.Category.ID,
Name: goods.Category.Name,
},
GoodsBrief string `gorm:"type:varchar(100);not null;comment:'商品简介'"`
GoodsFrontImage string `gorm:"type:varchar(200);not null;comment:'商品展示图'"`
2 - 批量获取商品、获取商品详情
- goods_srv/handler/handle_goods.go
func (s *GoodsServer) BatchGetGoods(ctx context.Context, req *proto.BatchGoodsIdInfo) (*proto.GoodsListResponse, error) {
goodsListResponse := &proto.GoodsListResponse{}
var goods []model.Goods
result := global.DB.Where(req.Id).Find(&goods)
for _, good := range goods {
goodsInfoResponse := ModelToResponse(good)
goodsListResponse.Data = append(goodsListResponse.Data, &goodsInfoResponse)
}
goodsListResponse.Total = int32(result.RowsAffected)
return goodsListResponse, nil
}
func (s *GoodsServer) GetGoodsDetail(ctx context.Context, req *proto.GoodInfoRequest) (*proto.GoodsInfoResponse, error) {
var goods model.Goods
if result := global.DB.Preload("Category").Preload("Brands").First(&goods, req.Id); result.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品不存在")
}
goodsInfoResponse := ModelToResponse(goods)
return &goodsInfoResponse, nil
}
- goods_srv/tests/goods/test_goods.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"nd/goods_srv/proto"
"nd/goods_srv/tests"
)
var brandClient proto.GoodsClient
var conn *grpc.ClientConn
func Init() {
var err error
conn, err = grpc.Dial(tests.TargetAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
brandClient = proto.NewGoodsClient(conn)
}
func TestGetGoodsList() {
rsp, err := brandClient.GoodsList(context.Background(), &proto.GoodsFilterRequest{
PriceMin: 10,
KeyWords: "水",
})
if err != nil {
panic(err)
}
fmt.Println(rsp.Total)
for _, good := range rsp.Data {
fmt.Println(good.Name, good.ShopPrice)
}
}
func TestBatchGetGoods() {
rsp, err := brandClient.BatchGetGoods(context.Background(), &proto.BatchGoodsIdInfo{
Id: []int32{29, 30, 31},
})
if err != nil {
panic(err)
}
fmt.Println(rsp.Total)
for _, good := range rsp.Data {
fmt.Println(good.Name, good.ShopPrice)
}
}
func TestGetGoodsDetail() {
rsp, err := brandClient.GetGoodsDetail(context.Background(), &proto.GoodInfoRequest{
Id: 28,
})
if err != nil {
panic(err)
}
fmt.Println(rsp.Name)
fmt.Println(rsp.ShopPrice)
fmt.Println(rsp.MarketPrice)
}
func main() {
Init()
TestBatchGetGoods()
fmt.Println("----------------------------")
TestGetGoodsDetail()
conn.Close()
}
3 - 新增、修改、更新商品
- goods_srv/handler/handle_goods.go
func (s *GoodsServer) CreateGoods(ctx context.Context, req *proto.CreateGoodsInfo) (*proto.GoodsInfoResponse, error) {
var category model.Category
if result := global.DB.First(&category, req.CategoryId); result.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "商品分类不存在")
}
var brand model.Brands
if result := global.DB.First(&brand, req.BrandId); result.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "品牌不存在")
}
goods := model.Goods{
Brands: brand,
BrandsID: brand.ID,
Category: category,
CategoryID: category.ID,
Name: req.Name,
GoodsSn: req.GoodsSn,
MarketPrice: req.MarketPrice,
ShopPrice: req.ShopPrice,
GoodsBrief: req.GoodsBrief,
ShipFree: req.ShipFree,
GoodsFrontImage: req.GoodsFrontImage,
IsNew: req.IsNew,
IsHot: req.IsHot,
OnSale: req.OnSale,
}
tx := global.DB.Begin()
result := tx.Save(&goods)
if result.Error != nil {
tx.Rollback()
return nil, result.Error
}
tx.Commit()
return &proto.GoodsInfoResponse{
Id: goods.ID,
}, nil
}
func (s *GoodsServer) DeleteGoods(ctx context.Context, req *proto.DeleteGoodsInfo) (*emptypb.Empty, error) {
if result := global.DB.Delete(&model.Goods{BaseModel: model.BaseModel{ID: req.Id}}, req.Id); result.Error != nil {
return nil, status.Errorf(codes.NotFound, "商品不存在")
}
return &emptypb.Empty{}, nil
}
func (s *GoodsServer) UpdateGoods(ctx context.Context, req *proto.CreateGoodsInfo) (*emptypb.Empty, error) {
var goods model.Goods
if result := global.DB.First(&goods, req.Id); result.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "商品不存在")
}
var category model.Category
if result := global.DB.First(&category, req.CategoryId); result.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "商品分类不存在")
}
var brand model.Brands
if result := global.DB.First(&brand, req.BrandId); result.RowsAffected == 0 {
return nil, status.Errorf(codes.InvalidArgument, "品牌不存在")
}
goods.Brands = brand
goods.BrandsID = brand.ID
goods.Category = category
goods.CategoryID = category.ID
goods.Name = req.Name
goods.GoodsSn = req.GoodsSn
goods.MarketPrice = req.MarketPrice
goods.ShopPrice = req.ShopPrice
goods.GoodsBrief = req.GoodsBrief
goods.ShipFree = req.ShipFree
goods.GoodsFrontImage = req.GoodsFrontImage
goods.IsNew = req.IsNew
goods.IsHot = req.IsHot
goods.OnSale = req.OnSale
tx := global.DB.Begin()
result := tx.Save(&goods)
if result.Error != nil {
tx.Rollback()
return nil, result.Error
}
tx.Commit()
return &emptypb.Empty{}, nil
}
五、完整源码
- 完整源码下载:mxshop_srvsV8.2.rar
- 源码说明:(nacos的ip配置自行修改,全局变量DEV_CONFIG设置:1=zsz,2=comp,3=home)
- goods_srv/model/sql/mxshop_goods.sql:包含了建表语句
- other_import/api.json:YApi的导入文件
- other_import/nacos_config_export_user.zip:nacos的user配置集导入文件
- other_import/nacos_config_export_goods.zip:nacos的goods配置集导入文件