10--RabbitMQ + 订单模块

当前抢购成功后,Redis 库存已扣减,但 MySQL 中没有真正生成订单(order表)。
需要通过消息队列异步创建订单,实现削峰填谷

为此,我们先安装RabbitMQ依赖
go get -u github.com/rabbitmq/amqp091-go

注: 事先安装RabbitMQ


一. Config配置

RedisMySQL一样,想要接入RabbitMQ就需要我们修改Config

config/config.go

// RabbitMQConfig RabbitMQ配置
type RabbitMQConfig struct {
    Host     string `mapstructure:"host"`     // RabbitMQ服务器地址
    Port     int    `mapstructure:"port"`     // 端口
    User     string `mapstructure:"user"`      // 用户名
    Password string `mapstructure:"password"`   // 密码
    VHost    string `mapstructure:"vhost"`     // 虚拟主机
}

// Config 整个配置树
type Config struct {
	Server   ServerConfig   `mapstructure:"server"`   // 服务器相关配置
	Database DatabaseConfig `mapstructure:"database"` // 数据库相关配置
	JWT      JWTConfig      `mapstructure:"jwt"`      // JWT相关配置
	Redis    RedisConfig    `mapstructure:"redis"`    // Redis相关配置
	RabbitMQ RabbitMQConfig `mapstructure:"rabbitmq"`  // 新增 RabbitMQ相关配置
}

config/config.yaml

rabbitmq:
  host: "127.0.0.1"
  port: 5672 # 替换端口号
  user: "guest" # 替换用户名
  password: "guest" # 替换密码
  vhost: "/"

二. MQ初始化

internal/mq/rabbitmq.go

package mq

import (
	"fmt"

	"github.com/Chuan81/secgo-mall/config"
	amqp "github.com/rabbitmq/amqp091-go"
)

var Conn *amqp.Connection
var Channel *amqp.Channel

// OrderQueue 订单队列名称
const OrderQueue = "order_queue"

// InitRabbitMQ 初始化RabbitMQ连接和通道
func InitRabbitMQ() {
	rCfg := config.GlobalConfig.RabbitMQ // 读取config文件
	// 拼接AMQP连接URL
	url := fmt.Sprintf(
		"amqp://%s:%s@%s:%d/%s",
		rCfg.User,
		rCfg.Password,
		rCfg.Host,
		rCfg.Port,
		rCfg.Vhost,
	)

	var err error
	// 建立TCP连接
	Conn, err = amqp.Dial(url)
	if err != nil {
		panic("RabbitMQ connection error:" + err.Error())
	}

	// 创建一个Channel(相当于socket连接,用于收发消息)
	Channel, err = Conn.Channel()
	if err != nil {
		panic("RabbitMQ channel error:" + err.Error())
	}

	// 声明一个持久化队列(如不存在自动创建)
	_, err = Channel.QueueDeclare(
		OrderQueue,
		true,
		false,
		false,
		false,
		nil,
	)
	if err != nil {
		panic("RabbitMQ queue declare error:" + err.Error())
	}

	println("RabbitMQ connected successful")
}

三. 订单消息结构体

internal/api/handlers/dto/order.go

package dto

// GetOrderListRequest 获取全部订单列表请求
type GetOrderListRequest struct {
}

// GetOrderByUserIDRequest 根据用户ID查询订单列表
type GetOrderByUserIDRequest struct {
	UserID uint `form:"user_id" binding:"required"`
}

internal/mq/message.go

package mq

// OrderMessage 定义发送到RabbitMQ的订单消息结构
type OrderMessage struct {
	UserId    uint    `json:"user_id"`    // 用户ID
	SeckillId uint    `json:"seckill_id"` // 秒杀活动ID
	ProductId uint    `json:"product_id"` // 商品ID
	Amount    float64 `json:"amount"`     // 订单金额
}

四. 订单Repository

internal/repository/order.go

package repository

import (
	"github.com/Chuan81/secgo-mall/internal/model"
)

// CreateOrder 创建订单
func CreateOrder(order *model.Order) error {
	return DB.Create(order).Error
}

// GetOrderByID 根据ID获取订单
func GetOrderByID(id uint) (*model.Order, error) {
	var order model.Order
	err := DB.First(&order, id).Error
	return &order, err
}

// GetOrderByUserID 根据用户ID获取订单列表
func GetOrderByUserID(userId uint) ([]*model.Order, error) {
	var orders []*model.Order
	err := DB.Where("user_id = ?", userId).Find(&orders).Error
	return orders, err
}

// UpdateOrderStatus 更新订单状态
func UpdateOrderStatus(orderId uint, status int) error {
	return DB.Model(&model.Order{}).Where("order_id = ?", orderId).Update("status", status).Error
}

五. 订单Service

internal/service/order.go

package service

import (
	"github.com/Chuan81/secgo-mall/internal/model"
	"github.com/Chuan81/secgo-mall/internal/repository"
)

// GetOrderList 获取用户订单列表
func GetOrderList(userId uint) ([]*model.Order, error) {
	return repository.GetOrderByUserID(userId)
}

六. 生产者与消费者

1) 生产者

internal/mq/producer.go

package mq

import (
	"encoding/json"

	amqp "github.com/rabbitmq/amqp091-go"
)

// SendOrderMessage 发送订单消息到RabbitMQ
func SendOrderMessage(msg *OrderMessage) error {
	// 将订单消息反序列化为JSON
	body, err := json.Marshal(msg)
	if err != nil {
		return err
	}

	// 发布消息到指定队列
	err = Channel.Publish(
		"",         // exchange(使用默认exchange)
		OrderQueue, // routing key(队列名)
		false,      // mandatory
		false,      // immediate
		amqp.Publishing{
			ContentType:  "application/json", // 消息类型
			Body:         body,               // 消息内容
			DeliveryMode: amqp.Persistent,    // 消息持久化
		},
	)
	return err
}

2) 消费者

internal/mq/consumer.go

package mq

import (
	"encoding/json"
	"errors"
	"log"

	"github.com/Chuan81/secgo-mall/internal/model"
	"gorm.io/gorm"
)

var DB *gorm.DB

func SetDB(db *gorm.DB) {
	DB = db
}

// StartConsumer 启动消费者
func StartConsumer() {
	// 订阅队列
	msgs, err := Channel.Consume(
		OrderQueue, // queue
		"",         // consumer
		false,      // auto-ack
		false,      // exclusive
		false,      // no-local
		false,      // no-wait
		nil,        // args
	)
	if err != nil {
		log.Fatalf("Failed to register a consumer: %v", err)
	}

	// 启动协程处理消息
	go func() {
		for msg := range msgs { // 处理消息
			var orderMsg OrderMessage

			// 将消息反序列化为订单消息
			if err := json.Unmarshal(msg.Body, &orderMsg); err != nil {
				log.Printf("Failed to unmarshal message: %v", err)
				msg.Nack(false, false) // 丢弃这条消息
				continue
			}

			//
			if err := CreateOrder(&orderMsg); err != nil {
				log.Printf("Failed to create order: %v", err)
				msg.Nack(false, true) // 重新入队,稍后重试
				continue
			}

			msg.Ack(false) // 确认消息处理成功
			log.Printf("Order created: user=%d, seckill=%d", orderMsg.UserId, orderMsg.SeckillId)
		}
	}()
}

// CreateOrder 消费者调用,创建订单(带事务)
func CreateOrder(msg *OrderMessage) error {
	// 开启事务,保证库存扣减和订单创建原子性
	tx := DB.Begin()

	// 扣减商品真实库存(乐观锁,防止超卖)
	// 条件: product_id匹配 且 stock > 0
	result := tx.Model(&model.Product{}).
		Where("product_id = ? AND stock > 0", msg.ProductId).
		Update("stock", gorm.Expr("stock - 1"))
	if result.Error != nil {
		tx.Rollback()
		return result.Error
	}
	if result.RowsAffected == 0 {
		tx.Rollback()
		return errors.New("stock not enough")
	}

	// 创建订单
	order := &model.Order{
		UserId:    msg.UserId,
		ProductId: msg.ProductId,
		SeckillId: msg.SeckillId,
		Amount:    msg.Amount,
		Status:    0, // 待支付
	}
	if err := tx.Create(order).Error; err != nil {
		tx.Rollback()
		return err
	}

	// 提交事务
	return tx.Commit().Error
}

七. 秒杀service扣减真实库存

上期我们实现internal/service/seckill.go
我们只完成到了扣减Redis预热库存
在此基础上我们需要引入消息队列,并传入订单消息,完成MySQL中真实库存的扣减
而后GORM便会在order表中创建订单

我们只需要在internal/service/seckill.goSeckillPurchase函数末尾处添加以下代码即可

// ...
func SeckillPurchase(seckillId, userId uint) (int, error) {
	// ...
	
	// 扣减成功,发送订单消息到MQ
	orderMsg := &mq.OrderMessage{
		UserId:    userId,
		SeckillId: seckillId,
		ProductId: seckill.ProductId,
		Amount:    seckill.Price,
	}
	if err := mq.SendOrderMessage(orderMsg); err != nil {
		// MQ 发送失败日志告警(库存已扣,不应回滚)
		println("MQ send failed:", err.Error())
	}

	return 1, nil // (原先代码) 返回订单创建成功信息
}

至此我们真正完成了下单逻辑

八. 订单查询Handler

internal/api/handlers/order.go

package handlers

import (
	"github.com/Chuan81/secgo-mall/internal/api/handlers/dto"
	"github.com/Chuan81/secgo-mall/internal/service"
	"github.com/Chuan81/secgo-mall/pkg/response"
	"github.com/gin-gonic/gin"
)

// GetOrderList 获取当前登录用户的订单列表
func GetOrderList(c *gin.Context) {
	uid, _ := c.Get("uid")

	orderList, err := service.GetOrderList(uid.(uint))
	if err != nil {
		response.Fail(c, 500, "failed to get order list")
		return
	}

	response.Sucess(c, orderList)
}

// GetOrderByUserID 根据用户ID查询订单列表(管理员用)
func GetOrderByUserID(c *gin.Context) {
	var req dto.GetOrderByUserIDRequest
	if uid, err := strconv.Atoi(c.Param("user_id")); err == nil {
		req.UserID = uint(uid)
	} else {
		response.Fail(c, 400, "invalid parameters")
		return
	}

	orderList, err := service.GetOrderList(req.UserID)
	if err != nil {
		response.Fail(c, 500, "failed to get order list")
		return
	}

	response.Sucess(c, orderList)
}

九. RabbitMQ的启动与订单查询路由

cmd/secgo-mall/main.go

// ...
func main() {
	// ...
	
	// 初始化Redis连接
	cache.InitRedis()

	// 初始化RabbitMQ并启动消费者 
	mq.InitRabbitMQ() //新
	mq.StartConsumer() //新
	mq.SetDB(database.DB) //新

	// 使用全局配置设置Gin的运行模式
	gin.SetMode(config.GlobalConfig.Server.Mode)

	// ...
	
	// 需要鉴权的私有路由
	protected := r.Group("/api")
	protected.Use(middleware.JWTAuth()) // 使用JWT鉴权中间件
	{
		// ...
		
		// 用户获取名下订单列表
		protected.GET("/order/list", handlers.GetOrderList) // 新
		// 管理员根据用户ID获取订单列表
		protected.GET("/order/list/:user_id", handlers.GetOrderByUserID) // 新
	}
	
	// ...
}
// ...

顺利运行,在进行下一步开发前,我们先对现有的逻辑进行一些测试,详情还请跳转10.5章

posted @ 2026-04-17 17:39  Chuan81  阅读(13)  评论(0)    收藏  举报