9--商品与秒杀核心的实现
在模型与缓存就绪之后,我们便可以着手商品与秒杀核心的实现
一. Repository层-商品与活动相关逻辑
repository/product.go 和 repository/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

浙公网安备 33010602011771号