• 24、订单和购物车-srv服务


    一、订单表结构与接口

    1 - 订单表结构与创建

    • order_srv/model/order.go
    package model
    
    import "time"
    
    type ShoppingCart struct { // 购物车
    	BaseModel
    	User    int32 `gorm:"type:int;index"` // 在购物车列表中我们需要查询当前用户的购物车记录
    	Goods   int32 `gorm:"type:int;index"` // 加索引:我们需要查询时候, 1. 会影响插入性能 2. 会占用磁盘
    	Nums    int32 `gorm:"type:int"`       // 商品的数量
    	Checked bool  // 是否选中
    }
    
    func (ShoppingCart) TableName() string {
    	return "shoppingcart"
    }
    
    type OrderInfo struct {
    	BaseModel
    
    	User    int32  `gorm:"type:int;index"`
    	OrderSn string `gorm:"type:varchar(30);index"` // 订单号,我们平台自己生成的订单号
    	PayType string `gorm:"type:varchar(20) comment 'alipay(支付宝), wechat(微信)'"`
    
    	// status大家可以考虑使用iota来做
    	Status     string `gorm:"type:varchar(20)  comment 'PAYING(待支付), TRADE_SUCCESS(成功), TRADE_CLOSED(超时关闭), WAIT_BUYER_PAY(交易创建), TRADE_FINISHED(交易结束)'"`
    	TradeNo    string `gorm:"type:varchar(100) comment '交易号'"` // 交易号就是支付宝的订单号,查账
    	OrderMount float32
    	PayTime    *time.Time `gorm:"type:datetime"`
    
    	Address      string `gorm:"type:varchar(100)"`
    	SignerName   string `gorm:"type:varchar(20)"`
    	SingerMobile string `gorm:"type:varchar(11)"`
    	Post         string `gorm:"type:varchar(20)"`
    }
    
    func (OrderInfo) TableName() string {
    	return "orderinfo"
    }
    
    type OrderGoods struct {
    	BaseModel
    
    	Order int32 `gorm:"type:int;index"`
    	Goods int32 `gorm:"type:int;index"`
    
    	//把商品的信息保存下来了 , 字段冗余, 高并发系统中我们一般都不会遵循三范式, 做镜像记录
    	GoodsName  string `gorm:"type:varchar(100);index"`
    	GoodsImage string `gorm:"type:varchar(200)"`
    	GoodsPrice float32
    	Nums       int32 `gorm:"type:int"`
    }
    
    func (OrderGoods) TableName() string {
    	return "ordergoods"
    }
    
    
    • 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
    • 数据库创建:mxshop_order_srv
      在这里插入图片描述
    • order_srv/model/main/main.go:自动创建表
    package main
    
    import (
    	"fmt"
    	"gorm.io/driver/mysql"
    	"gorm.io/gorm"
    	"gorm.io/gorm/logger"
    	"gorm.io/gorm/schema"
    	"log"
    	"nd/order_srv/global"
    	"nd/order_srv/initialize"
    	"nd/order_srv/model"
    	"os"
    	"time"
    )
    
    func main() {
    	initialize.InitConfig()
    	dsn := fmt.Sprintf("root:jiushi@tcp(%s:3306)/mxshop_order_srv?charset=utf8mb4&parseTime=True&loc=Local", global.ServerConfig.MysqlInfo.Host)
    
    	newLogger := logger.New(
    		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
    		logger.Config{
    			SlowThreshold: time.Second, // 慢 SQL 阈值
    			LogLevel:      logger.Info, // Log level
    			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.ShoppingCart{}, &model.OrderInfo{}, &model.OrderGoods{})
    }
    
    
    • 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

    2 - 订单接口定义

    • order_srv/proto/order.protoprotoc --go_out=. --go_opt=paths=import --go-grpc_out=. --go-grpc_opt=paths=import *.proto
    syntax = "proto3";
    import "google/protobuf/empty.proto";
    option go_package = ".;proto";
    
    
    service Order {
      //购物车
      rpc CartItemList(UserInfo) returns(CartItemListResponse); //获取用户的购物车信息
      rpc CreateCartItem(CartItemRequest) returns(ShopCartInfoResponse); //添加商品到购物车
      rpc UpdateCartItem(CartItemRequest) returns(google.protobuf.Empty); //修改购物车信息
      rpc DeleteCartItem(CartItemRequest) returns(google.protobuf.Empty); //删除购物车条目
    
      //订单
      rpc CreateOrder(OrderRequest) returns (OrderInfoResponse); //创建订单
      rpc OrderList(OrderFilterRequest) returns (OrderListResponse); // 订单列表
      rpc OrderDetail(OrderRequest) returns (OrderInfoDetailResponse); // 订单详情
      rpc UpdateOrderStatus(OrderStatus) returns (google.protobuf.Empty); // 修改订单状态
    }
    
    message UserInfo {
      int32 id = 1;
    }
    
    message OrderStatus {
      int32 id = 1;
      string orderSn = 2;
      string status = 3;
    }
    
    message CartItemRequest {
      int32 id = 1;
      int32 userId = 2;
      int32 goodsId = 3;
      string goodsName = 4;
      string goodsImage = 5;
      float goodsPrice = 6;
      int32 nums = 7;
      bool checked = 8;
    }
    
    message OrderRequest {
      int32 id = 1;
      int32 userId = 2;
      string address = 3;
      string name = 4;
      string mobile = 5;
      string post = 6;
    }
    
    message OrderInfoResponse {
      int32 id = 1;
      int32 userId = 2;
      string orderSn = 3;
      string payType = 4;
      string status = 5;
      string post = 6;
      float total = 7;
      string address = 8;
      string name = 9;
      string mobile = 10;
      string addTime = 11;
    }
    
    message ShopCartInfoResponse {
      int32 id = 1;
      int32 userId = 2;
      int32 goodsId = 3;
      int32 nums = 4;
      bool checked = 5;
    }
    
    message OrderItemResponse {
      int32 id = 1;
      int32 orderId = 2;
      int32 goodsId = 3;
      string goodsName = 4;
      string goodsImage = 5;
      float goodsPrice = 6;
      int32 nums = 7;
    }
    
    message OrderInfoDetailResponse {
      OrderInfoResponse orderInfo = 1;
      repeated OrderItemResponse goods = 2;
    }
    
    message OrderFilterRequest {
      int32 userId = 1;
      int32 pages = 2;
      int32 pagePerNums = 3;
    }
    
    message OrderListResponse {
      int32 total = 1;
      repeated OrderInfoResponse data = 2;
    }
    
    message CartItemListResponse {
      int32 total = 1;
      repeated ShopCartInfoResponse data = 2;
    }
    
    
    
    • 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

    3 - 启动订单服务

    • nacos新建命名空间orders
      在这里插入图片描述
    {
      "name": "order_srv",
      "host": "192.168.124.9",
      "tags": ["imooc", "bobby", "order", "srv"],
      "mysql": {
        "host": "192.168.124.51",
        "port": 3306,
        "user": "root",
        "password": "jiushi",
        "db": "mxshop_order_srv"
      },
      "consul": {
        "host": "192.168.124.51",
        "port": 8500
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • yaml修改:order_srv/config_zsz.yaml
    host: '192.168.78.131'
    port: 8848
    namespace: '4fd0ed12-957c-490e-a116-99672c761433'
    user: 'nacos'
    password: 'nacos'
    dataid: 'order_srv.json'
    group: 'comp'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • order_srv/main.goproto.RegisterOrderServer(server, &proto.UnimplementedOrderServer{})
    package main
    
    import (
    	"flag"
    	"fmt"
    	"net"
    	"os"
    	"os/signal"
    	"syscall"
    
    	"github.com/satori/go.uuid"
    	"go.uber.org/zap"
    	"google.golang.org/grpc"
    	"google.golang.org/grpc/health"
    	"google.golang.org/grpc/health/grpc_health_v1"
    
    	"nd/order_srv/global"
    	"nd/order_srv/initialize"
    	"nd/order_srv/proto"
    	"nd/order_srv/utils"
    	"nd/order_srv/utils/register/consul"
    )
    
    func main() {
    	IP := flag.String("ip", "0.0.0.0", "ip地址")
    	Port := flag.Int("port", 50060, "端口号") // 这个修改为0,如果我们从命令行带参数启动的话就不会为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.RegisterOrderServer(server, &proto.UnimplementedOrderServer{})
    	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())
    
    	//服务注册
    	register_client := consul.NewRegistryClient(global.ServerConfig.ConsulInfo.Host, global.ServerConfig.ConsulInfo.Port)
    	serviceId := fmt.Sprintf("%s", uuid.NewV4())
    	err = register_client.Register(global.ServerConfig.Host, *Port, global.ServerConfig.Name, global.ServerConfig.Tags, serviceId)
    	if err != nil {
    		zap.S().Panic("服务注册失败:", err.Error())
    	}
    	zap.S().Debugf("启动服务器, 端口: %d", *Port)
    
    	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 = register_client.DeRegister(serviceId); err != nil {
    		zap.S().Info("注销失败:", err.Error())
    	} else {
    		zap.S().Info("注销成功:")
    	}
    }
    
    
    • 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

    在这里插入图片描述


    二、购物车接口

    1 - 获取用户购物车

    func (*OrderServer) CartItemList(ctx context.Context, req *proto.UserInfo) (*proto.CartItemListResponse, error) {
    	//获取用户的购物车列表
    	var shopCarts []model.ShoppingCart
    	var rsp proto.CartItemListResponse
    
    	if result := global.DB.Where(&model.ShoppingCart{User: req.Id}).Find(&shopCarts); result.Error != nil {
    		return nil, result.Error
    	} else {
    		rsp.Total = int32(result.RowsAffected)
    	}
    
    	for _, shopCart := range shopCarts {
    		rsp.Data = append(rsp.Data, &proto.ShopCartInfoResponse{
    			Id:      shopCart.ID,
    			UserId:  shopCart.User,
    			GoodsId: shopCart.Goods,
    			Nums:    shopCart.Nums,
    			Checked: shopCart.Checked,
    		})
    	}
    	return &rsp, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    2 - 添加商品到购物车

    func (*OrderServer) CreateCartItem(ctx context.Context, req *proto.CartItemRequest) (*proto.ShopCartInfoResponse, error) {
    	//将商品添加到购物车 1. 购物车中原本没有这件商品 - 新建一个记录 2. 这个商品之前添加到了购物车- 合并
    	var shopCart model.ShoppingCart
    
    	if result := global.DB.Where(&model.ShoppingCart{Goods: req.GoodsId, User: req.UserId}).First(&shopCart); result.RowsAffected == 1 {
    		//如果记录已经存在,则合并购物车记录, 更新操作
    		shopCart.Nums += req.Nums
    	} else {
    		//插入操作
    		shopCart.User = req.UserId
    		shopCart.Goods = req.GoodsId
    		shopCart.Nums = req.Nums
    		shopCart.Checked = false
    	}
    
    	global.DB.Save(&shopCart)
    	return &proto.ShopCartInfoResponse{Id: shopCart.ID}, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3 - 更新购物车

    func (*OrderServer) UpdateCartItem(ctx context.Context, req *proto.CartItemRequest) (*emptypb.Empty, error) {
    	//更新购物车记录,更新数量和选中状态
    	var shopCart model.ShoppingCart
    
    	if result := global.DB.First(&shopCart, req.Id); result.RowsAffected == 0 {
    		return nil, status.Errorf(codes.NotFound, "购物车记录不存在")
    	}
    
    	shopCart.Checked = req.Checked
    	if req.Nums > 0 {
    		shopCart.Nums = req.Nums
    	}
    	global.DB.Save(&shopCart)
    
    	return &emptypb.Empty{}, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    4 - 删除购物车

    func (*OrderServer) DeleteCartItem(ctx context.Context, req *proto.CartItemRequest) (*emptypb.Empty, error) {
    	if result := global.DB.Where("goods=? and user=?", req.GoodsId, req.UserId).Delete(&model.ShoppingCart{}); result.RowsAffected == 0 {
    		return nil, status.Errorf(codes.NotFound, "购物车记录不存在")
    	}
    	return &emptypb.Empty{}, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    三、查询订单与订单详情

    1 - 查询订单列表

    func (*OrderServer) OrderList(ctx context.Context, req *proto.OrderFilterRequest) (*proto.OrderListResponse, error) {
    	var orders []model.OrderInfo
    	var rsp proto.OrderListResponse
    
    	var total int64
    	global.DB.Where(&model.OrderInfo{User: req.UserId}).Count(&total)
    	rsp.Total = int32(total)
    
    	//分页
    	global.DB.Scopes(Paginate(int(req.Pages), int(req.PagePerNums))).Where(&model.OrderInfo{User: req.UserId}).Find(&orders)
    	for _, order := range orders {
    		rsp.Data = append(rsp.Data, &proto.OrderInfoResponse{
    			Id:      order.ID,
    			UserId:  order.User,
    			OrderSn: order.OrderSn,
    			PayType: order.PayType,
    			Status:  order.Status,
    			Post:    order.Post,
    			Total:   order.OrderMount,
    			Address: order.Address,
    			Name:    order.SignerName,
    			Mobile:  order.SingerMobile,
    			AddTime: order.CreatedAt.Format("2006-01-02 15:04:05"),
    		})
    	}
    	return &rsp, nil
    }
    
    • 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

    2 - 查询订单详情

    func (*OrderServer) OrderDetail(ctx context.Context, req *proto.OrderRequest) (*proto.OrderInfoDetailResponse, error) {
    	var order model.OrderInfo
    	var rsp proto.OrderInfoDetailResponse
    	//这个订单的id是否是当前用户的订单, 如果在web层用户传递过来一个id的订单, web层应该先查询一下订单id是否是当前用户的
    	//在个人中心可以这样做,但是如果是后台管理系统,web层如果是后台管理系统 那么只传递order的id,如果是电商系统还需要一个用户的id
    	if result := global.DB.Where(&model.OrderInfo{BaseModel: model.BaseModel{ID: req.Id}, User: req.UserId}).First(&order); result.RowsAffected == 0 {
    		return nil, status.Errorf(codes.NotFound, "订单不存在")
    	}
    	orderInfo := proto.OrderInfoResponse{}
    	orderInfo.Id = order.ID
    	orderInfo.UserId = order.User
    	orderInfo.OrderSn = order.OrderSn
    	orderInfo.PayType = order.PayType
    	orderInfo.Status = order.Status
    	orderInfo.Post = order.Post
    	orderInfo.Total = order.OrderMount
    	orderInfo.Address = order.Address
    	orderInfo.Name = order.SignerName
    	orderInfo.Mobile = order.SingerMobile
    	rsp.OrderInfo = &orderInfo
    	var orderGoods []model.OrderGoods
    	if result := global.DB.Where(&model.OrderGoods{Order: order.ID}).Find(&orderGoods); result.Error != nil {
    		return nil, result.Error
    	}
    	for _, orderGood := range orderGoods {
    		rsp.Goods = append(rsp.Goods, &proto.OrderItemResponse{
    			GoodsId:    orderGood.Goods,
    			GoodsName:  orderGood.GoodsName,
    			GoodsPrice: orderGood.GoodsPrice,
    			GoodsImage: orderGood.GoodsImage,
    			Nums:       orderGood.Nums,
    		})
    	}
    	return &rsp, nil
    }
    
    • 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

    四、创建订单

    1 - 创建订单流程分析

    • 创建订单流程
      • ①.从购物车中获取到选中的商品
      • ②.商品的价格自己查询 - 访问商品服务 (跨微服务)
      • ③.库存的扣减 - 访问库存服务 (跨微服务)
      • ④.订单的基本信息表 - 订单的商品信息表
      • ⑤.从购物车中删除已购买的记录

    2 - 订单的跨服务实现

    • 跨微服务的proto:因为这里需要用到商品服务和库存服务,所以需要把商品的proto和库存的proto文件拷贝到订单服务的proto中;路径 —— order_srv/proto
      在这里插入图片描述
    • order_srv/global/global.go
      • 添加两个rpc的client:GoodsSrvClient,InventorySrvClient
    package global
    
    import (
    	"gorm.io/gorm"
    	"nd/order_srv/config"
    	"nd/order_srv/proto"
    )
    
    var (
    	DB           *gorm.DB
    	ServerConfig config.ServerConfig
    	NacosConfig  config.NacosConfig
    
    	GoodsSrvClient     proto.GoodsClient
    	InventorySrvClient proto.InventoryClient
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • order_srv/config/config.go:添加商品和库存的微服务配置
    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 SrvConfig struct {
    	Name string `mapstructure:"name" json:"name"`
    }
    
    type ServerConfig struct {
    	Name       string       `mapstructure:"name" json:"name"`
    	Host       string       `mapstructure:"host" json:"host"`
    	Tags       []string     `mapstructure:"tags" json:"tags"`
    	MysqlInfo  MysqlConfig  `mapstructure:"mysql" json:"mysql"`
    	ConsulInfo ConsulConfig `mapstructure:"consul" json:"consul"`
    
    	//商品微服务的配置
    	GoodsSrvInfo SrvConfig `mapstructure:"goods_srv" json:"goods_srv"`
    	//库存微服务的配置
    	InventorySrvInfo SrvConfig `mapstructure:"inventory_srv" json:"inventory_srv"`
    }
    
    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"`
    }
    
    
    • 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
    • nacos中添加商品和库存的配置
    {
      "name": "order_srv",
      "host": "192.168.124.9",
      "tags": ["imooc", "bobby", "order", "srv"],
      "mysql": {
        "host": "192.168.124.51",
        "port": 3306,
        "user": "root",
        "password": "jiushi",
        "db": "mxshop_order_srv"
      },
      "goods_srv":{
        "name":"goods_srv"
      },
      "inventory_srv":{
        "name":"inventory_srv"
      },
      "consul": {
        "host": "192.168.124.51",
        "port": 8500
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • order_srv/initialize/init_srv_conn.go:初始化连接商品和库存的微服务
    package initialize
    
    import (
    	"fmt"
    	"google.golang.org/grpc/credentials/insecure"
    
    	_ "github.com/mbobakov/grpc-consul-resolver" // It's important
    	"go.uber.org/zap"
    	"google.golang.org/grpc"
    
    	"nd/order_srv/global"
    	"nd/order_srv/proto"
    )
    
    func InitSrvConn() {
    	consulInfo := global.ServerConfig.ConsulInfo
    	goodsConn, err := grpc.Dial(
    		fmt.Sprintf("consul://%s:%d/%s?wait=14s", consulInfo.Host, consulInfo.Port, global.ServerConfig.GoodsSrvInfo.Name),
    		grpc.WithTransportCredentials(insecure.NewCredentials()),
    		grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
    		//grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer())),
    	)
    	if err != nil {
    		zap.S().Fatal("[InitSrvConn] 连接 【商品服务失败】")
    	}
    
    	global.GoodsSrvClient = proto.NewGoodsClient(goodsConn)
    
    	//初始化库存服务连接
    	invConn, err := grpc.Dial(
    		fmt.Sprintf("consul://%s:%d/%s?wait=14s", consulInfo.Host, consulInfo.Port, global.ServerConfig.InventorySrvInfo.Name),
    		grpc.WithTransportCredentials(insecure.NewCredentials()),
    		grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
    		//grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer())),
    	)
    	if err != nil {
    		zap.S().Fatal("[InitSrvConn] 连接 【库存服务失败】")
    	}
    
    	global.InventorySrvClient = proto.NewInventoryClient(invConn)
    }
    
    
    • 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
    • order_srv/main.go:添加商品和库存微服务的初始化
    func main() {
    	IP := flag.String("ip", "0.0.0.0", "ip地址")
    	Port := flag.Int("port", 50060, "端口号") // 这个修改为0,如果我们从命令行带参数启动的话就不会为0
    
    	//初始化
    	initialize.InitLogger()
    	initialize.InitConfig()
    	initialize.InitDB()
    	initialize.InitSrvConn()
    	zap.S().Info(global.ServerConfig)
    
    	//省略。。。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3 - 创建订单接口(本地mysql事务)

    因为涉及到了库存的扣减,需要使用分布式的事务;
    我们这里先实现本地mysql事务,后续再优化成分布式的事务

    • order_srv/handler/handle_order.go
    func (*OrderServer) CreateOrder(ctx context.Context, req *proto.OrderRequest) (*proto.OrderInfoResponse, error) {
    	/*
    		新建订单
    			1. 从购物车中获取到选中的商品
    			2. 商品的价格自己查询 - 访问商品服务 (跨微服务)
    			3. 库存的扣减 - 访问库存服务 (跨微服务)
    			4. 订单的基本信息表 - 订单的商品信息表
    			5. 从购物车中删除已购买的记录
    	*/
    
    	var goodsIds []int32
    	var shopCarts []model.ShoppingCart
    	goodsNumsMap := make(map[int32]int32)
    	if result := global.DB.Where(&model.ShoppingCart{User: req.UserId, Checked: true}).Find(&shopCarts); result.RowsAffected == 0 {
    		return nil, status.Errorf(codes.InvalidArgument, "没有选中结算的商品")
    	}
    
    	for _, shopCarts := range shopCarts {
    		goodsIds = append(goodsIds, shopCarts.Goods)
    		goodsNumsMap[shopCarts.Goods] = shopCarts.Nums
    	}
    
    	//跨服务调用 - 商品微服务 —— 批量查询商品与价格
    	goods, err := global.GoodsSrvClient.BatchGetGoods(context.Background(), &proto.BatchGoodsIdInfo{Id: goodsIds})
    	if err != nil {
    		return nil, status.Errorf(codes.Internal, "批量查询商品信息失败")
    	}
    
    	var orderAmount float32
    	var orderGoods []*model.OrderGoods
    	var goodsInvInfo []*proto.GoodsInvInfo
    	for _, good := range goods.Data {
    		orderAmount += good.ShopPrice * float32(goodsNumsMap[good.Id])
    		orderGoods = append(orderGoods, &model.OrderGoods{
    			Goods:      good.Id,
    			GoodsName:  good.Name,
    			GoodsImage: good.GoodsFrontImage,
    			GoodsPrice: good.ShopPrice,
    			Nums:       goodsNumsMap[good.Id],
    		})
    		goodsInvInfo = append(goodsInvInfo, &proto.GoodsInvInfo{
    			GoodsId: good.Id,
    			Num:     goodsNumsMap[good.Id],
    		})
    	}
    	//跨服务调用 - 库存微服务 —— 扣减库存
    	if _, err = global.InventorySrvClient.Sell(context.Background(), &proto.SellInfo{GoodsInfo: goodsInvInfo}); err != nil {
    		return nil, status.Errorf(codes.ResourceExhausted, "扣减库存失败")
    	}
    
    	tx := global.DB.Begin()
    	//生成订单表
    	order := model.OrderInfo{
    		OrderSn:      GenerateOrderSn(req.UserId),
    		OrderMount:   orderAmount,
    		Address:      req.Address,
    		SignerName:   req.Name,
    		SingerMobile: req.Mobile,
    		Post:         req.Post,
    		User:         req.UserId,
    	}
    	if result := tx.Save(&order); result.RowsAffected == 0 {
    		tx.Rollback()
    		return nil, status.Errorf(codes.Internal, "创建订单失败")
    	}
    
    	for _, orderGood := range orderGoods {
    		orderGood.Order = order.ID
    	}
    
    	// 批量插入orderGoods
    	if result := tx.CreateInBatches(orderGoods, 100); result.RowsAffected == 0 {
    		tx.Rollback()
    		return nil, status.Errorf(codes.Internal, "创建订单失败")
    	}
    
    	// 删除购物车的记录
    	if result := tx.Where(&model.ShoppingCart{User: req.UserId, Checked: true}).Delete(&model.ShoppingCart{}); result.RowsAffected == 0 {
    		tx.Rollback()
    		return nil, status.Errorf(codes.Internal, "创建订单失败")
    	}
    
    	//提交事务
    	tx.Commit()
    	return &proto.OrderInfoResponse{Id: order.ID, OrderSn: order.OrderSn, Total: order.OrderMount}, nil
    }
    
    • 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

    4 - 更新订单状态接口

    • order_srv/handler/handle_order.go
    func (*OrderServer) UpdateOrderStatus(ctx context.Context, req *proto.OrderStatus) (*emptypb.Empty, error) {
    	//先查询,再更新 实际上有两条sql执行, select 和 update语句
    	if result := global.DB.Model(&model.OrderInfo{}).Where("order_sn = ?", req.OrderSn).Update("status", req.Status); result.RowsAffected == 0 {
    		return nil, status.Errorf(codes.NotFound, "订单不存在")
    	}
    	return &emptypb.Empty{}, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    五、订单服务接口调试

    测试接口前,要注意 order_srv/main.go 中 proto.RegisterOrderServer(server, &handler.OrderServer{}) ,这个注册的实现
    否则会一直报空实现错误

    1 - 通用测试初始化

    • order_srv/tests/test_config.go
    package tests
    
    import (
    	"google.golang.org/grpc"
    	"google.golang.org/grpc/credentials/insecure"
    	"nd/order_srv/proto"
    )
    
    var (
    	TargetAddr  = "127.0.0.1:50060"
    	OrderClient proto.OrderClient
    	Conn        *grpc.ClientConn
    )
    
    func Init() {
    	var err error
    	Conn, err = grpc.Dial(TargetAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    	if err != nil {
    		panic(err)
    	}
    	OrderClient = proto.NewOrderClient(Conn)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    2 - 购物车相关接口测试

    • order_srv/tests/order/test_shop_cart.go
    package main
    
    import (
    	"context"
    	"fmt"
    	"nd/order_srv/proto"
    	"nd/order_srv/tests"
    )
    
    func TestCartItemList(userId int32) {
    	rsp, err := tests.OrderClient.CartItemList(context.Background(), &proto.UserInfo{
    		Id: userId,
    	})
    	if err != nil {
    		panic(err)
    	}
    	for _, item := range rsp.Data {
    		fmt.Println(item.Id, item.GoodsId, item.Nums)
    	}
    }
    
    func TestCreateCartItem(userId, nums, goodsId int32) {
    	rsp, err := tests.OrderClient.CreateCartItem(context.Background(), &proto.CartItemRequest{
    		UserId:  userId,
    		Nums:    nums,
    		GoodsId: goodsId,
    	})
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println(rsp.Id)
    }
    
    func TestUpdateCartItem(id int32) {
    	_, err := tests.OrderClient.UpdateCartItem(context.Background(), &proto.CartItemRequest{
    		Id:      id,
    		Checked: true,
    	})
    	if err != nil {
    		panic(err)
    	}
    }
    
    func main() {
    	tests.Init()
    	TestCreateCartItem(1, 1, 26)
    	TestCreateCartItem(1, 1, 27)
    	TestCartItemList(1)
    	TestUpdateCartItem(1)
    	tests.Conn.Close()
    }
    
    
    • 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

    在这里插入图片描述

    3 - 创建订单接口测试

    • order_srv/tests/order/test_order.go:创建订单接口测试
    package main
    
    import (
    	"context"
    	"nd/order_srv/proto"
    	"nd/order_srv/tests"
    )
    
    func TestCreateOrder() {
    	_, err := tests.OrderClient.CreateOrder(context.Background(), &proto.OrderRequest{
    		UserId:  1,
    		Address: "北京市",
    		Name:    "bobby",
    		Mobile:  "18787878787",
    		Post:    "请尽快发货",
    	})
    	if err != nil {
    		panic(err)
    	}
    }
    
    func main() {
    	tests.Init()
    
    	TestCreateOrder()
    
    	tests.Conn.Close()
    }
    
    
    • 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
    • 先测试购物车都没选中的情况

    在这里插入图片描述

    • 接下来测试有购物车的情况:这里需要注意,我们需要开启商品微服务、库存微服务、订单微服务3个微服务才可以测试;并且要保证端口不冲突

    在这里插入图片描述

    • 这里还需要注意修改库存微服务中的扣减库存方法:之前的redis地址我们是写死的,如果你换了机器后需要进行修改;因为我的笔记是在不同的三台机器上切换来切换去,所以调试这个bug花了好久(泪崩),这也是微服务的痛点
    • inventory_srv/handler/inventory.go

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    4 - 订单详情接口测试

    • order_srv/tests/order/test_order.go
    func TestGetOrderDetail(orderId int32) {
    	rsp, err := tests.OrderClient.OrderDetail(context.Background(), &proto.OrderRequest{
    		Id: orderId,
    	})
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println(rsp.OrderInfo.OrderSn)
    	for _, good := range rsp.Goods {
    		fmt.Println(good.GoodsName)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    5 - 获取订单列表接口测试

    • order_srv/tests/order/test_order.go
    func TestOrderList() {
    	rsp, err := tests.OrderClient.OrderList(context.Background(), &proto.OrderFilterRequest{})
    	//rsp, err := tests.OrderClient.OrderList(context.Background(), &proto.OrderFilterRequest{
    	//	UserId: 1,
    	//})
    	if err != nil {
    		panic(err)
    	}
    	for _, order := range rsp.Data {
    		fmt.Println(order.OrderSn)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    • 这里我们再测试下gorm忽略零值的情况
    • 不传递userid的情况
      在这里插入图片描述
    • 传递userid的情况

    在这里插入图片描述

    在这里插入图片描述


    六、完整源码

    • 完整源码下载mxshop_srvsV8.8.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配置集导入文件
      • other_import/nacos_config_export_inventory.zip:nacos的inventory的配置导入文件
      • other_import/nacos_config_export_orders.zip:nacos的orders的配置导入文件
  • 相关阅读:
    Pytorch优化器全总结(一)SGD、ASGD、Rprop、Adagrad
    【正点原子STM32连载】第五十八章 USB虚拟串口(Slave)实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1
    PDF或PPT中的某个图或表无损、高清的插入word里的方法,再转成pdf后放大6400%倍仍是高清图片...
    从 Clickhouse 到 Snowflake(一): 云原生
    7年坚定投身“高清头显”,纳德光学聚焦打造高清头显领导品牌
    golang 解决invalid argument tmp (type interface {}) for len
    从零到一搭建基础架构(3)-base模块搭建上篇
    IT运维面试必须具备的排障知识题库
    操作系统:进程退出,进程等待,进程替换,简单的shell命令行解释器
    Redis事务和锁机制
  • 原文地址:https://blog.csdn.net/qq23001186/article/details/126289737