9--商品与秒杀核心的实现

在模型与缓存就绪之后,我们便可以着手商品与秒杀核心的实现

一. Repository层-商品与活动相关逻辑

repository/product.gorepository/seckill.go

package repository

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

// CreateProduct 创建商品
func CreateProduct(p *model.Product) (*model.Product, error) {
	// 这里我们使用Create方法来创建一个新的商品,如果创建失败会返回错误
	if err := DB.Create(p).Error; err != nil {
		return nil, err
	}

	// 如果创建成功,返回创建的商品信息
	return p, nil
}

// GetProductByID 根据ID获取商品
func GetProductByID(id uint) (*model.Product, error) {
	var product model.Product

	// 这里我们使用First方法来获取单个商品,如果没有找到会返回错误
	if err := DB.First(&product, id).Error; err != nil {
		return nil, err
	}

	// 如果找到了商品,返回商品信息
	return &product, nil
}

// GetProductList 获取商品列表
func GetProductList() ([]*model.Product, error) {
	var products []*model.Product
	err := DB.Find(&products).Error

	// 如果找到了商品,返回商品列表,否则返回错误
	return products, err
}

// UpdateProductStock 更新商品库存
func UpdateProductStock(id uint, stock int) error {
	// 这里我们使用Model方法来更新商品的库存,如果更新失败会返回错误
	return DB.Model(&model.Product{}).Where("id = ?", id).Update("stock", stock).Error
}
package repository

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

// CreateSeckill 创建秒杀活动
func CreateSeckill(s *model.Seckill) error {
	return DB.Create(s).Error
}

// GetSeckillByID 根据ID获取秒杀活动
func GetSeckillByID(id uint) (*model.Seckill, error) {
	var seckill model.Seckill

	// 这里我们使用First方法来获取单个秒杀活动,如果没有找到会返回错误
	if err := DB.First(&seckill, id).Error; err != nil {
		return nil, err
	}

	// 如果找到了秒杀活动,返回秒杀活动信息
	return &seckill, nil
}

// GetActiveSeckillList 获取秒杀活动列表
func GetActiveSeckillList() ([]*model.Seckill, error) {
	var list []*model.Seckill

	// 寻找status值为1(即正在活跃的秒杀活动)
	err := DB.Where("status = ?", 1).Find(&list).Error
	return list, err
}

// GetAllSeckillList 获取所有秒杀活动列表
func GetAllSeckillList() ([]*model.Seckill, error) {
	var list []*model.Seckill
	err := DB.Find(&list).Error
	return list, err
}

// UpdateSeckillStatus 更新秒杀活动状态
// status值 0: 未开始, 1: 进行中, 2: 已结束
func UpdateSeckillStatus(id uint, status int) error {
	return DB.Model(&model.Seckill{}).Where("id = ?", id).Update("status", status).Error
}
函数 目的
CreateProduct 后台添加商品,秒杀活动的商品来源
GetProductById 创建秒杀活动前,校验商品是否存在
CreateSeckill 创建秒杀活动,将活动信息持久化到 MySQL
GetSeckillById 用户抢购时,校验秒杀活动是否合法
GetActiveSeckillList 用户浏览当前可参与的秒杀活动列表
GetAllSeckillList 用户浏览有记录的所有秒杀活动列表

二. DTO结构体的定义

1) 商品

internal/api/handlers/dto/product.go

package dto

// CreateProductRequest 定义了创建商品请求的DTO结构体
type CreateProductRequest struct {
	Name        string  `json:"name" binding:"required"`        // 商品名称,必填
	Description string  `json:"description"`                    // 商品描述
	Price       float64 `json:"price" binding:"required,gt=0"`  // 商品价格,必填,必须大于0
	Stock       int     `json:"stock" binding:"required,gte=0"` // 商品库存,必填,必须大于等于0
}
结构体 目的
CreateProductRequest 接收创建商品的请求参数(名字、描述、价格、库存)

2) 秒杀

internal/api/handlers/dto/seckill.go

package dto

// CreateSeckillRequest 定义了创建秒杀活动请求的DTO结构体
type CreateSeckillRequest struct {
	ProductID uint    `json:"product_id" binding:"required"` // ProductID 商品ID,关联到具体参与秒杀的商品,不能为空
	Name      string  `json:"name" binding:"required"`       // Name 秒杀活动名称,用于标识和展示活动,不能为空
	Price     float64 `json:"price" binding:"required,gt=0"` // Price 秒杀价格,单位通常为元,必须大于0(gt=0 表示 greater than 0)
	Stock     int     `json:"stock" binding:"required,gt=0"` // Stock 秒杀库存,表示可秒杀的商品数量,必须大于0(gt=0 表示 greater than 0)
	StartTime int64   `json:"start_time" binding:"required"` // StartTime 秒杀活动开始时间
	EndTime   int64   `json:"end_time" binding:"required"`   // EndTime 秒杀活动结束时间
}

// SeckillPurchaseRequest 定义了用户参与秒杀下单请求的DTO结构体
type SeckillPurchaseRequest struct {
	SeckillID uint `json:"seckill_id" binding:"required"` // 秒杀活动ID,必填
}
结构体 目的
CreateSeckillRequest 接收创建秒杀活动的请求参数(商品ID、价格、库存、时间段)
SeckillPurchaseRequest 接收用户抢购请求,只传 SeckillId(用户ID从 JWT 中取)

三. 创建逻辑与下单逻辑

1) 商品

internal/service/product.go

package service

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

// 商品创建逻辑
func CreateProduct(req *dto.CreateProductRequest) (*model.Product, error) {
	// 创建商品
	product := &model.Product{
		Name:        req.Name,
		Description: req.Description,
		Price:       req.Price,
		Stock:       req.Stock,
	}
	return repository.CreateProduct(product)
}

// 商品列表获取逻辑
func GetProductList() ([]*model.Product, error) {
	return repository.GetProductList()
}
函数 目的
CreateProduct 创建商品
GetProductList 获取商品列表

2) 秒杀

internal/service/seckill.go

package service

import (
	"errors"
	"time"

	"github.com/Chuan81/secgo-mall/internal/api/handlers/dto"
	"github.com/Chuan81/secgo-mall/internal/cache"
	"github.com/Chuan81/secgo-mall/internal/model"
	"github.com/Chuan81/secgo-mall/internal/repository"
)

// CreateSeckill 创建秒杀活动
func CreateSeckill(req *dto.CreateSeckillRequest) (*model.Seckill, error) {
	// 校验商品是否存在
	_, err := repository.GetProductByID(req.ProductID)
	if err != nil {
		return nil, errors.New("product not found")
	}

	// 根据时间自动设置状态
	now := time.Now()
	startTime := time.Unix(req.StartTime, 0)
	endTime := time.Unix(req.EndTime, 0)

	var status int
	if now.Before(startTime) {
		status = 0 // 未开始
	} else if now.After(endTime) {
		status = 2 // 已结束
	} else {
		status = 1 // 进行中
	}

	// 创建秒杀活动
	seckill := &model.Seckill{
		ProductId: req.ProductID,
		Name:      req.Name,
		Price:     req.Price,
		Stock:     req.Stock,
		StartTime: time.Unix(req.StartTime, 0),
		EndTime:   time.Unix(req.EndTime, 0),
		Status:    0, // 未开始
	}

	if err := repository.CreateSeckill(seckill); err != nil {
		return nil, err
	}

	// Redis库存预热
	cache.PreloadStock(seckill.SeckillId, seckill.Stock)

	return seckill, nil
}

// GetActiveSeckillList 获取活跃的秒杀活动列表
func GetActiveSeckillList() ([]*model.Seckill, error) {
	return repository.GetActiveSeckillList()
}

// GetAllSeckillList 获取历史秒杀活动列表
func GetAllSeckillList() ([]*model.Seckill, error) {
	return repository.GetAllSeckillList()
}

// SeckillPurchase 秒杀活动下单逻辑
// 返回值 1: 成功, 0: 库存不足, -1: 系统错误, -2: 活动未开始, -3: 活动已结束
func SeckillPurchase(seckillId, userId uint) (int, error) {
	// 校验秒杀活动是否存在
	seckill, err := repository.GetSeckillByID(seckillId)
	if err != nil {
		return -1, errors.New("seckill not found")
	}

	// 获取当前时间
	now := time.Now()

	// 校验秒杀活动是否正在进行
	if now.Before(seckill.StartTime) {
		return -2, errors.New("seckill not started")
	}
	if now.After(seckill.EndTime) {
		return -3, errors.New("seckill ended")
	}

	// 调用Lua脚本原子扣减Redis库存
	result, err := cache.DeductStock(seckillId)
	if err != nil {
		return -1, err
	}
	// 库存不足
	if result == 0 {
		return 0, errors.New("stock not enough")
	}
	// 库存未初始化
	if result == -1 {
		return -1, errors.New("stock not initialized")
	}

	return 1, nil // 返回订单创建成功信息
}
函数 目的
CreateSeckill 核心:创建秒杀活动时,同时将库存预热到 Redis
SeckillPurchase 核心:校验活动状态 → Lua 脚本扣减 Redis → 后续发 MQ 落库
GetActiveSeckillList 获取活跃秒杀活动列表
GetActiveSeckillList 获取历史秒杀活动列表

为什么要先查商品?

秒杀活动必须依附于一个已存在的商品,不能凭空创建活动。

为什么要预热到 Redis?

避免活动开始时大量请求直接打 MySQL,Redis 单线程承接所有库存扣减。

四. Handler

1) 商品

internal/api/handlers/product.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"
)

// CreateProduct 创建商品
func CreateProduct(c *gin.Context) {
	var req dto.CreateProductRequest

	// 绑定请求参数到DTO结构体,并进行验证
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Fail(c, 400, "invalid request parameters: "+err.Error())
		return
	}

	// 调用服务层的创建商品逻辑
	product, err := service.CreateProduct(&req)
	if err != nil {
		response.Fail(c, 500, "create product failed: "+err.Error())
		return
	}

	// 创建成功,返回商品信息
	response.Sucess(c, product)
}

// GetProductList 获取商品列表
func GetProductList(c *gin.Context) {
	// 调用服务层的获取商品列表逻辑
	productList, err := service.GetProductList()
	if err != nil {
		response.Fail(c, 500, "failed to get product list")
		return
	}

	// 获取成功,返回商品列表
	response.Sucess(c, productList)
}
函数 目的
CreateProduct 接收 HTTP 请求,调用 Service,返回统一响应
GetProductList 调用Service, 返回商品列表

2) 秒杀

internal/api/handlers/seckill.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"
)

// CreateSeckill 创建秒杀活动
func CreateSeckill(c *gin.Context) {
	var req dto.CreateSeckillRequest

	// 绑定请求参数到DTO结构体,并进行验证
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Fail(c, 400, "invalid request parameters: "+err.Error())
		return
	}

	// 调用服务层的创建秒杀活动逻辑
	seckill, err := service.CreateSeckill(&req)
	if err != nil {
		response.Fail(c, 500, "create seckill failed: "+err.Error())
		return
	}

	// 创建成功,返回秒杀活动信息
	response.Sucess(c, seckill)
}

// GetActiveSeckillList 获取活跃的秒杀活动列表
func GetActiveSeckillList(c *gin.Context) {
	// 调用服务层的获取活跃秒杀活动列表逻辑
	activeSeckillList, err := service.GetActiveSeckillList()
	if err != nil {
		response.Fail(c, 500, "failed to get active seckill list")
		return
	}

	// 获取成功,返回秒杀活动列表
	response.Sucess(c, activeSeckillList)
}

// GetAllSeckillList 获取所有的秒杀活动列表
func GetAllSeckillList(c *gin.Context) {
	// 调用服务层的获取所有秒杀活动列表逻辑
	allSeckillList, err := service.GetAllSeckillList()
	if err != nil {
		response.Fail(c, 500, "failed to get all seckill list")
		return
	}

	// 获取成功,返回秒杀活动列表
	response.Sucess(c, allSeckillList)
}

// SeckillPurchase 秒杀活动下单
func SeckillPurchase(c *gin.Context) {
	var req dto.SeckillPurchaseRequest

	// 绑定请求参数到DTO结构体,并进行验证
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Fail(c, 400, "invalid parameters")
		return
	}

	uid, _ := c.Get("uid") // 获取下单用户的UID

	// 调用服务层的秒杀活动下单逻辑
	code, err := service.SeckillPurchase(req.SeckillID, uid.(uint))
	if err != nil {
		response.Fail(c, code, "seckill purchase failed: "+err.Error())
		return
	}

	// 下单成功,返回处理中信息
	response.Sucess(c, gin.H{"message": "purchase success, order is being processed"})
}
函数 目的
CreateSeckill 接收 HTTP 请求,调用 Service,返回统一响应
SeckillPurchase 接收抢购请求,从 c.Get("uid") 取 JWT 中的用户ID,调用 Service
GetActiveSeckillList 调用Service, 返回活跃秒杀活动列表
GetAllSeckillList 调用Service, 返回所有秒杀活动列表

五. 添加路由

main.go

// ...
	// 需要鉴权的私有路由
	protected := r.Group("/api")
	protected.Use(middleware.JWTAuth()) // 使用JWT鉴权中间件
	{
		// 在这里定义需要鉴权的路由
		protected.GET("/user/info", func(c *gin.Context) {
			uid, _ := c.Get("uid")
			username, _ := c.Get("username")
			response.Sucess(c, gin.H{
				"uid":      uid,
				"username": username,
			})
		})

		// 创建商品
		protected.POST("/product/create", handlers.CreateProduct)

		// 获取商品列表
		protected.GET("/product/list", handlers.GetProductList)

		// 创建秒杀活动
		protected.POST("/seckill/create", handlers.CreateSeckill) // 新
		
		// 获取历史秒杀活动列表
		protected.GET("/seckill/list/all", handlers.GetAllSeckillList) // 新

		// 获取用户现在可以参加的秒杀活动列表
		protected.GET("/seckill/list/active", handlers.GetActiveSeckillList) // 新

		// 下单
		protected.POST("/seckill/purchase", handlers.SeckillPurchase) // 新
	}
// ...

六. 商品与订单流程图

stateDiagram-v2 [*] --> 创建商品 创建商品 --> 写入MySQL: repository.CreateProduct() 写入MySQL --> 返回商品信息 返回商品信息 --> [*] [*] --> 查询商品列表 查询商品列表 --> 读取MySQL: repository.GetProductList() 读取MySQL --> 返回商品列表 返回商品列表 --> [*] [*] --> 创建秒杀活动 创建秒杀活动 --> 校验商品存在: repository.GetProductByID() 校验商品存在 --> 商品不存在: product = nil 校验商品存在 --> 商品存在: product ≠ nil 商品不存在 --> 返回错误: product not found 返回错误 --> [*] 商品存在 --> 写入秒杀活动MySQL: repository.CreateSeckill() 写入秒杀活动MySQL --> 预热库存到Redis: cache.PreloadStock() 预热库存到Redis --> 预热成功: stock > 0 预热成功 --> 返回秒杀活动信息 返回秒杀活动信息 --> [*]

七. 运行测试

PS C:\Code\Projects\Secgo-Mall> go run .\cmd\secgo-mall\main.go
2026/04/12 13:30:05 MySQL connected successfully.
Connected to Redis successfully
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /api/user/register        --> github.com/Chuan81/secgo-mall/internal/api/handlers.Register (3 handlers)
[GIN-debug] POST   /api/user/login           --> github.com/Chuan81/secgo-mall/internal/api/handlers.Login (3 handlers)
[GIN-debug] GET    /api/user/info            --> main.main.func1 (4 handlers)
[GIN-debug] POST   /api/product/create       --> github.com/Chuan81/secgo-mall/internal/api/handlers.CreateProduct (4 handlers)
[GIN-debug] GET    /api/product/list         --> github.com/Chuan81/secgo-mall/internal/api/handlers.GetProductList (4 handlers)
[GIN-debug] POST   /api/seckill/create       --> github.com/Chuan81/secgo-mall/internal/api/handlers.CreateSeckill (4 handlers)
[GIN-debug] GET    /api/seckill/list         --> github.com/Chuan81/secgo-mall/internal/api/handlers.GetActiveSeckillList (4 handlers)
[GIN-debug] POST   /api/seckill/purchase     --> github.com/Chuan81/secgo-mall/internal/api/handlers.SeckillPurchase (4 handlers)
[GIN-debug] GET    /ping                     --> main.main.func2 (3 handlers)
2026/04/12 13:30:05 Starting Secgo-Mall server on :8080
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8080
posted @ 2026-04-17 17:38  Chuan81  阅读(19)  评论(0)    收藏  举报