电商API实战 #

本节将使用Gin框架构建一个电商系统的后端API,包括商品管理、购物车、订单系统、支付集成等核心功能。

项目结构 #

text
ecommerce-api/
├── main.go
├── config/
│   └── config.go
├── models/
│   ├── user.go
│   ├── product.go
│   ├── category.go
│   ├── cart.go
│   ├── order.go
│   └── payment.go
├── handlers/
│   ├── product_handler.go
│   ├── cart_handler.go
│   ├── order_handler.go
│   └── payment_handler.go
├── services/
│   ├── product_service.go
│   ├── cart_service.go
│   ├── order_service.go
│   └── payment_service.go
├── middleware/
│   ├── auth.go
│   └── response.go
└── router/
    └── router.go

数据模型 #

商品模型 #

go
// models/product.go
package models

import (
    "time"
    "gorm.io/gorm"
)

type Product struct {
    ID          uint           `json:"id" gorm:"primaryKey"`
    Name        string         `json:"name" gorm:"size:200;not null;index"`
    Description string         `json:"description" gorm:"type:text"`
    Price       float64        `json:"price" gorm:"type:decimal(10,2);not null"`
    OriginalPrice float64      `json:"original_price" gorm:"type:decimal(10,2)"`
    Stock       int            `json:"stock" gorm:"default:0"`
    Sales       int            `json:"sales" gorm:"default:0"`
    CategoryID  uint           `json:"category_id" gorm:"index"`
    BrandID     uint           `json:"brand_id" gorm:"index"`
    MainImage   string         `json:"main_image" gorm:"size:255"`
    Images      string         `json:"images" gorm:"type:text"`
    Status      int            `json:"status" gorm:"default:1"` // 0: off, 1: on
    SortOrder   int            `json:"sort_order" gorm:"default:0"`
    CreatedAt   time.Time      `json:"created_at"`
    UpdatedAt   time.Time      `json:"updated_at"`
    DeletedAt   gorm.DeletedAt `json:"-" gorm:"index"`
    
    Category    *Category      `json:"category,omitempty" gorm:"foreignKey:CategoryID"`
    Skus        []ProductSku   `json:"skus,omitempty" gorm:"foreignKey:ProductID"`
}

type ProductSku struct {
    ID          uint           `json:"id" gorm:"primaryKey"`
    ProductID   uint           `json:"product_id" gorm:"index;not null"`
    SkuCode     string         `json:"sku_code" gorm:"uniqueIndex;size:50;not null"`
    Name        string         `json:"name" gorm:"size:100;not null"`
    Price       float64        `json:"price" gorm:"type:decimal(10,2);not null"`
    Stock       int            `json:"stock" gorm:"default:0"`
    Image       string         `json:"image" gorm:"size:255"`
    Specs       string         `json:"specs" gorm:"type:json"`
    CreatedAt   time.Time      `json:"created_at"`
    UpdatedAt   time.Time      `json:"updated_at"`
    DeletedAt   gorm.DeletedAt `json:"-" gorm:"index"`
}

func (Product) TableName() string {
    return "products"
}

func (ProductSku) TableName() string {
    return "product_skus"
}

分类模型 #

go
// models/category.go
package models

import (
    "time"
    "gorm.io/gorm"
)

type Category struct {
    ID          uint           `json:"id" gorm:"primaryKey"`
    Name        string         `json:"name" gorm:"size:50;not null"`
    ParentID    uint           `json:"parent_id" gorm:"default:0;index"`
    Icon        string         `json:"icon" gorm:"size:255"`
    SortOrder   int            `json:"sort_order" gorm:"default:0"`
    Status      int            `json:"status" gorm:"default:1"`
    CreatedAt   time.Time      `json:"created_at"`
    UpdatedAt   time.Time      `json:"updated_at"`
    DeletedAt   gorm.DeletedAt `json:"-" gorm:"index"`
    
    Children    []Category     `json:"children,omitempty" gorm:"foreignKey:ParentID"`
    Products    []Product      `json:"products,omitempty" gorm:"foreignKey:CategoryID"`
}

func (Category) TableName() string {
    return "categories"
}

购物车模型 #

go
// models/cart.go
package models

import (
    "time"
    "gorm.io/gorm"
)

type CartItem struct {
    ID        uint           `json:"id" gorm:"primaryKey"`
    UserID    uint           `json:"user_id" gorm:"index;not null"`
    ProductID uint           `json:"product_id" gorm:"index;not null"`
    SkuID     uint           `json:"sku_id" gorm:"index"`
    Quantity  int            `json:"quantity" gorm:"not null"`
    Selected  bool           `json:"selected" gorm:"default:true"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
    
    Product   *Product       `json:"product,omitempty" gorm:"foreignKey:ProductID"`
    Sku       *ProductSku    `json:"sku,omitempty" gorm:"foreignKey:SkuID"`
}

func (CartItem) TableName() string {
    return "cart_items"
}

type CartResponse struct {
    Items      []CartItemResponse `json:"items"`
    TotalCount int                `json:"total_count"`
    TotalPrice float64            `json:"total_price"`
    SelectedCount int             `json:"selected_count"`
    SelectedPrice float64         `json:"selected_price"`
}

type CartItemResponse struct {
    ID        uint    `json:"id"`
    ProductID uint    `json:"product_id"`
    ProductName string `json:"product_name"`
    ProductImage string `json:"product_image"`
    SkuID     uint    `json:"sku_id"`
    SkuName   string  `json:"sku_name"`
    Price     float64 `json:"price"`
    Quantity  int     `json:"quantity"`
    Selected  bool    `json:"selected"`
    Subtotal  float64 `json:"subtotal"`
}

func (c *CartItem) ToResponse() *CartItemResponse {
    resp := &CartItemResponse{
        ID:        c.ID,
        ProductID: c.ProductID,
        SkuID:     c.SkuID,
        Quantity:  c.Quantity,
        Selected:  c.Selected,
    }
    
    if c.Product != nil {
        resp.ProductName = c.Product.Name
        resp.ProductImage = c.Product.MainImage
    }
    
    if c.Sku != nil {
        resp.SkuName = c.Sku.Name
        resp.Price = c.Sku.Price
    } else if c.Product != nil {
        resp.Price = c.Product.Price
    }
    
    resp.Subtotal = resp.Price * float64(resp.Quantity)
    
    return resp
}

订单模型 #

go
// models/order.go
package models

import (
    "time"
    "gorm.io/gorm"
)

type Order struct {
    ID              uint           `json:"id" gorm:"primaryKey"`
    OrderNo         string         `json:"order_no" gorm:"uniqueIndex;size:32;not null"`
    UserID          uint           `json:"user_id" gorm:"index;not null"`
    Status          int            `json:"status" gorm:"default:0"`
    TotalAmount     float64        `json:"total_amount" gorm:"type:decimal(10,2);not null"`
    PayAmount       float64        `json:"pay_amount" gorm:"type:decimal(10,2);not null"`
    FreightAmount   float64        `json:"freight_amount" gorm:"type:decimal(10,2);default:0"`
    DiscountAmount  float64        `json:"discount_amount" gorm:"type:decimal(10,2);default:0"`
    
    ReceiverName    string         `json:"receiver_name" gorm:"size:50;not null"`
    ReceiverPhone   string         `json:"receiver_phone" gorm:"size:20;not null"`
    ReceiverProvince string        `json:"receiver_province" gorm:"size:50"`
    ReceiverCity    string         `json:"receiver_city" gorm:"size:50"`
    ReceiverDistrict string        `json:"receiver_district" gorm:"size:50"`
    ReceiverAddress string         `json:"receiver_address" gorm:"size:200;not null"`
    
    PaymentMethod   string         `json:"payment_method" gorm:"size:20"`
    PaymentTime     *time.Time     `json:"payment_time"`
    DeliveryTime    *time.Time     `json:"delivery_time"`
    ReceiveTime     *time.Time     `json:"receive_time"`
    
    Remark          string         `json:"remark" gorm:"size:500"`
    CreatedAt       time.Time      `json:"created_at"`
    UpdatedAt       time.Time      `json:"updated_at"`
    DeletedAt       gorm.DeletedAt `json:"-" gorm:"index"`
    
    Items           []OrderItem    `json:"items,omitempty" gorm:"foreignKey:OrderID"`
    User            *User          `json:"user,omitempty" gorm:"foreignKey:UserID"`
}

type OrderItem struct {
    ID          uint           `json:"id" gorm:"primaryKey"`
    OrderID     uint           `json:"order_id" gorm:"index;not null"`
    ProductID   uint           `json:"product_id" gorm:"index;not null"`
    SkuID       uint           `json:"sku_id" gorm:"index"`
    ProductName string         `json:"product_name" gorm:"size:200;not null"`
    SkuName     string         `json:"sku_name" gorm:"size:100"`
    Image       string         `json:"image" gorm:"size:255"`
    Price       float64        `json:"price" gorm:"type:decimal(10,2);not null"`
    Quantity    int            `json:"quantity" gorm:"not null"`
    TotalAmount float64        `json:"total_amount" gorm:"type:decimal(10,2);not null"`
    CreatedAt   time.Time      `json:"created_at"`
    UpdatedAt   time.Time      `json:"updated_at"`
    DeletedAt   gorm.DeletedAt `json:"-" gorm:"index"`
}

func (Order) TableName() string {
    return "orders"
}

func (OrderItem) TableName() string {
    return "order_items"
}

const (
    OrderStatusPending   = 0
    OrderStatusPaid      = 1
    OrderStatusShipped   = 2
    OrderStatusDelivered = 3
    OrderStatusCancelled = 4
    OrderStatusRefunded  = 5
)

func (o *Order) CanCancel() bool {
    return o.Status == OrderStatusPending
}

func (o *Order) CanPay() bool {
    return o.Status == OrderStatusPending
}

func (o *Order) CanShip() bool {
    return o.Status == OrderStatusPaid
}

func (o *Order) CanReceive() bool {
    return o.Status == OrderStatusShipped
}

支付模型 #

go
// models/payment.go
package models

import (
    "time"
    "gorm.io/gorm"
)

type Payment struct {
    ID           uint           `json:"id" gorm:"primaryKey"`
    PaymentNo    string         `json:"payment_no" gorm:"uniqueIndex;size:32;not null"`
    OrderID      uint           `json:"order_id" gorm:"index;not null"`
    OrderNo      string         `json:"order_no" gorm:"size:32;not null"`
    UserID       uint           `json:"user_id" gorm:"index;not null"`
    Amount       float64        `json:"amount" gorm:"type:decimal(10,2);not null"`
    Method       string         `json:"method" gorm:"size:20;not null"`
    Status       int            `json:"status" gorm:"default:0"`
    TradeNo      string         `json:"trade_no" gorm:"size:64"`
    PaidAt       *time.Time     `json:"paid_at"`
    CreatedAt    time.Time      `json:"created_at"`
    UpdatedAt    time.Time      `json:"updated_at"`
    DeletedAt    gorm.DeletedAt `json:"-" gorm:"index"`
    
    Order        *Order         `json:"order,omitempty" gorm:"foreignKey:OrderID"`
}

func (Payment) TableName() string {
    return "payments"
}

const (
    PaymentStatusPending  = 0
    PaymentStatusSuccess  = 1
    PaymentStatusFailed   = 2
    PaymentStatusRefunded = 3
)

商品服务 #

go
// services/product_service.go
package services

import (
    "ecommerce-api/models"
    "ecommerce-api/utils"
    "errors"
    
    "gorm.io/gorm"
)

type ProductService struct {
    db *gorm.DB
}

func NewProductService(db *gorm.DB) *ProductService {
    return &ProductService{db: db}
}

type CreateProductRequest struct {
    Name          string  `json:"name" binding:"required,max=200"`
    Description   string  `json:"description"`
    Price         float64 `json:"price" binding:"required,gt=0"`
    OriginalPrice float64 `json:"original_price"`
    Stock         int     `json:"stock" binding:"gte=0"`
    CategoryID    uint    `json:"category_id" binding:"required"`
    MainImage     string  `json:"main_image"`
    Images        string  `json:"images"`
    Status        int     `json:"status"`
    Skus          []CreateSkuRequest `json:"skus"`
}

type CreateSkuRequest struct {
    SkuCode string  `json:"sku_code" binding:"required"`
    Name    string  `json:"name" binding:"required"`
    Price   float64 `json:"price" binding:"required,gt=0"`
    Stock   int     `json:"stock" binding:"gte=0"`
    Image   string  `json:"image"`
    Specs   string  `json:"specs"`
}

type ProductListQuery struct {
    utils.Pagination
    Keyword    string `form:"keyword"`
    CategoryID uint   `form:"category_id"`
    Status     *int   `form:"status"`
    MinPrice   float64 `form:"min_price"`
    MaxPrice   float64 `form:"max_price"`
    SortBy     string `form:"sort_by"`
    SortDesc   bool   `form:"sort_desc"`
}

func (s *ProductService) Create(req *CreateProductRequest) (*models.Product, error) {
    product := &models.Product{
        Name:          req.Name,
        Description:   req.Description,
        Price:         req.Price,
        OriginalPrice: req.OriginalPrice,
        Stock:         req.Stock,
        CategoryID:    req.CategoryID,
        MainImage:     req.MainImage,
        Images:        req.Images,
        Status:        req.Status,
    }
    
    err := s.db.Transaction(func(tx *gorm.DB) error {
        if err := tx.Create(product).Error; err != nil {
            return err
        }
        
        if len(req.Skus) > 0 {
            for i := range req.Skus {
                req.Skus[i].ProductID = product.ID
            }
            var skus []models.ProductSku
            for _, skuReq := range req.Skus {
                skus = append(skus, models.ProductSku{
                    ProductID: product.ID,
                    SkuCode:   skuReq.SkuCode,
                    Name:      skuReq.Name,
                    Price:     skuReq.Price,
                    Stock:     skuReq.Stock,
                    Image:     skuReq.Image,
                    Specs:     skuReq.Specs,
                })
            }
            if err := tx.Create(&skus).Error; err != nil {
                return err
            }
        }
        
        return nil
    })
    
    if err != nil {
        return nil, err
    }
    
    return s.GetByID(product.ID)
}

func (s *ProductService) GetByID(id uint) (*models.Product, error) {
    var product models.Product
    err := s.db.Preload("Category").Preload("Skus").First(&product, id).Error
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("商品不存在")
        }
        return nil, err
    }
    return &product, nil
}

func (s *ProductService) List(query *ProductListQuery) ([]models.Product, int64, error) {
    var products []models.Product
    var total int64
    
    q := s.db.Model(&models.Product{}).Preload("Category")
    
    if query.Keyword != "" {
        q = q.Where("name LIKE ?", "%"+query.Keyword+"%")
    }
    
    if query.CategoryID > 0 {
        q = q.Where("category_id = ?", query.CategoryID)
    }
    
    if query.Status != nil {
        q = q.Where("status = ?", *query.Status)
    }
    
    if query.MinPrice > 0 {
        q = q.Where("price >= ?", query.MinPrice)
    }
    
    if query.MaxPrice > 0 {
        q = q.Where("price <= ?", query.MaxPrice)
    }
    
    q.Count(&total)
    
    orderBy := "created_at DESC"
    if query.SortBy != "" {
        order := "ASC"
        if query.SortDesc {
            order = "DESC"
        }
        orderBy = query.SortBy + " " + order
    }
    
    q = q.Order(orderBy)
    q = q.Offset(query.GetOffset()).Limit(query.GetLimit())
    
    if err := q.Find(&products).Error; err != nil {
        return nil, 0, err
    }
    
    return products, total, nil
}

func (s *ProductService) UpdateStock(id uint, quantity int) error {
    return s.db.Model(&models.Product{}).Where("id = ? AND stock >= ?", id, quantity).
        Update("stock", gorm.Expr("stock - ?", quantity)).Error
}

购物车服务 #

go
// services/cart_service.go
package services

import (
    "ecommerce-api/models"
    "errors"
    
    "gorm.io/gorm"
)

type CartService struct {
    db *gorm.DB
}

func NewCartService(db *gorm.DB) *CartService {
    return &CartService{db: db}
}

type AddToCartRequest struct {
    ProductID uint `json:"product_id" binding:"required"`
    SkuID     uint `json:"sku_id"`
    Quantity  int  `json:"quantity" binding:"required,gt=0"`
}

func (s *CartService) Add(userID uint, req *AddToCartRequest) (*models.CartItem, error) {
    var product models.Product
    if err := s.db.First(&product, req.ProductID).Error; err != nil {
        return nil, errors.New("商品不存在")
    }
    
    if product.Status != 1 {
        return nil, errors.New("商品已下架")
    }
    
    if req.SkuID > 0 {
        var sku models.ProductSku
        if err := s.db.First(&sku, req.SkuID).Error; err != nil {
            return nil, errors.New("SKU不存在")
        }
        if sku.Stock < req.Quantity {
            return nil, errors.New("库存不足")
        }
    } else {
        if product.Stock < req.Quantity {
            return nil, errors.New("库存不足")
        }
    }
    
    var cartItem models.CartItem
    err := s.db.Where("user_id = ? AND product_id = ? AND sku_id = ?", 
        userID, req.ProductID, req.SkuID).First(&cartItem).Error
    
    if err == nil {
        cartItem.Quantity += req.Quantity
        if err := s.db.Save(&cartItem).Error; err != nil {
            return nil, err
        }
    } else if errors.Is(err, gorm.ErrRecordNotFound) {
        cartItem = models.CartItem{
            UserID:    userID,
            ProductID: req.ProductID,
            SkuID:     req.SkuID,
            Quantity:  req.Quantity,
            Selected:  true,
        }
        if err := s.db.Create(&cartItem).Error; err != nil {
            return nil, err
        }
    } else {
        return nil, err
    }
    
    return s.GetByID(cartItem.ID)
}

func (s *CartService) GetByID(id uint) (*models.CartItem, error) {
    var item models.CartItem
    err := s.db.Preload("Product").Preload("Sku").First(&item, id).Error
    if err != nil {
        return nil, err
    }
    return &item, nil
}

func (s *CartService) GetByUserID(userID uint) (*models.CartResponse, error) {
    var items []models.CartItem
    err := s.db.Preload("Product").Preload("Sku").
        Where("user_id = ?", userID).
        Order("created_at DESC").
        Find(&items).Error
    if err != nil {
        return nil, err
    }
    
    resp := &models.CartResponse{
        Items: make([]models.CartItemResponse, len(items)),
    }
    
    for i, item := range items {
        resp.Items[i] = *item.ToResponse()
        resp.TotalCount += item.Quantity
        resp.TotalPrice += resp.Items[i].Subtotal
        
        if item.Selected {
            resp.SelectedCount += item.Quantity
            resp.SelectedPrice += resp.Items[i].Subtotal
        }
    }
    
    return resp, nil
}

func (s *CartService) UpdateQuantity(userID, itemID uint, quantity int) error {
    var item models.CartItem
    if err := s.db.Where("id = ? AND user_id = ?", itemID, userID).First(&item).Error; err != nil {
        return errors.New("购物车项不存在")
    }
    
    if quantity <= 0 {
        return s.db.Delete(&item).Error
    }
    
    return s.db.Model(&item).Update("quantity", quantity).Error
}

func (s *CartService) UpdateSelected(userID, itemID uint, selected bool) error {
    return s.db.Model(&models.CartItem{}).
        Where("id = ? AND user_id = ?", itemID, userID).
        Update("selected", selected).Error
}

func (s *CartService) SelectAll(userID uint, selected bool) error {
    return s.db.Model(&models.CartItem{}).
        Where("user_id = ?", userID).
        Update("selected", selected).Error
}

func (s *CartService) Delete(userID, itemID uint) error {
    return s.db.Where("id = ? AND user_id = ?", itemID, userID).Delete(&models.CartItem{}).Error
}

func (s *CartService) Clear(userID uint) error {
    return s.db.Where("user_id = ?", userID).Delete(&models.CartItem{}).Error
}

func (s *CartService) GetSelectedItems(userID uint) ([]models.CartItem, error) {
    var items []models.CartItem
    err := s.db.Preload("Product").Preload("Sku").
        Where("user_id = ? AND selected = ?", userID, true).
        Find(&items).Error
    return items, err
}

订单服务 #

go
// services/order_service.go
package services

import (
    "ecommerce-api/models"
    "errors"
    "fmt"
    "time"
    
    "gorm.io/gorm"
)

type OrderService struct {
    db *gorm.DB
    cartService *CartService
}

func NewOrderService(db *gorm.DB, cartService *CartService) *OrderService {
    return &OrderService{db: db, cartService: cartService}
}

type CreateOrderRequest struct {
    AddressID   uint   `json:"address_id" binding:"required"`
    Remark      string `json:"remark"`
    CouponID    uint   `json:"coupon_id"`
}

type Address struct {
    Name     string `json:"name" binding:"required"`
    Phone    string `json:"phone" binding:"required"`
    Province string `json:"province" binding:"required"`
    City     string `json:"city" binding:"required"`
    District string `json:"district" binding:"required"`
    Address  string `json:"address" binding:"required"`
}

func (s *OrderService) Create(userID uint, req *CreateOrderRequest, addr *Address) (*models.Order, error) {
    cartItems, err := s.cartService.GetSelectedItems(userID)
    if err != nil {
        return nil, err
    }
    
    if len(cartItems) == 0 {
        return nil, errors.New("请选择要购买的商品")
    }
    
    var totalAmount float64
    var orderItems []models.OrderItem
    
    for _, item := range cartItems {
        var price float64
        var skuName string
        var image string
        
        if item.Sku != nil {
            price = item.Sku.Price
            skuName = item.Sku.Name
            image = item.Sku.Image
            if item.Sku.Stock < item.Quantity {
                return nil, fmt.Errorf("商品 %s 库存不足", item.Sku.Name)
            }
        } else if item.Product != nil {
            price = item.Product.Price
            image = item.Product.MainImage
            if item.Product.Stock < item.Quantity {
                return nil, fmt.Errorf("商品 %s 库存不足", item.Product.Name)
            }
        }
        
        subtotal := price * float64(item.Quantity)
        totalAmount += subtotal
        
        orderItems = append(orderItems, models.OrderItem{
            ProductID:   item.ProductID,
            SkuID:       item.SkuID,
            ProductName: item.Product.Name,
            SkuName:     skuName,
            Image:       image,
            Price:       price,
            Quantity:    item.Quantity,
            TotalAmount: subtotal,
        })
    }
    
    orderNo := generateOrderNo()
    
    order := &models.Order{
        OrderNo:          orderNo,
        UserID:           userID,
        Status:           models.OrderStatusPending,
        TotalAmount:      totalAmount,
        PayAmount:        totalAmount,
        FreightAmount:    0,
        DiscountAmount:   0,
        ReceiverName:     addr.Name,
        ReceiverPhone:    addr.Phone,
        ReceiverProvince: addr.Province,
        ReceiverCity:     addr.City,
        ReceiverDistrict: addr.District,
        ReceiverAddress:  addr.Address,
        Remark:           req.Remark,
    }
    
    err = s.db.Transaction(func(tx *gorm.DB) error {
        if err := tx.Create(order).Error; err != nil {
            return err
        }
        
        for i := range orderItems {
            orderItems[i].OrderID = order.ID
        }
        
        if err := tx.Create(&orderItems).Error; err != nil {
            return err
        }
        
        for _, item := range cartItems {
            if item.SkuID > 0 {
                if err := tx.Model(&models.ProductSku{}).
                    Where("id = ?", item.SkuID).
                    Update("stock", gorm.Expr("stock - ?", item.Quantity)).Error; err != nil {
                    return err
                }
            } else {
                if err := tx.Model(&models.Product{}).
                    Where("id = ?", item.ProductID).
                    Update("stock", gorm.Expr("stock - ?", item.Quantity)).Error; err != nil {
                    return err
                }
            }
        }
        
        if err := tx.Where("user_id = ? AND selected = ?", userID, true).
            Delete(&models.CartItem{}).Error; err != nil {
            return err
        }
        
        return nil
    })
    
    if err != nil {
        return nil, err
    }
    
    return s.GetByID(order.ID)
}

func (s *OrderService) GetByID(id uint) (*models.Order, error) {
    var order models.Order
    err := s.db.Preload("Items").First(&order, id).Error
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("订单不存在")
        }
        return nil, err
    }
    return &order, nil
}

func (s *OrderService) GetByOrderNo(orderNo string) (*models.Order, error) {
    var order models.Order
    err := s.db.Preload("Items").Where("order_no = ?", orderNo).First(&order).Error
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("订单不存在")
        }
        return nil, err
    }
    return &order, nil
}

func (s *OrderService) List(userID uint, status *int, page, pageSize int) ([]models.Order, int64, error) {
    var orders []models.Order
    var total int64
    
    q := s.db.Model(&models.Order{}).Where("user_id = ?", userID)
    
    if status != nil {
        q = q.Where("status = ?", *status)
    }
    
    q.Count(&total)
    
    offset := (page - 1) * pageSize
    if err := q.Preload("Items").Order("created_at DESC").
        Offset(offset).Limit(pageSize).Find(&orders).Error; err != nil {
        return nil, 0, err
    }
    
    return orders, total, nil
}

func (s *OrderService) Cancel(userID, orderID uint) error {
    order, err := s.GetByID(orderID)
    if err != nil {
        return err
    }
    
    if order.UserID != userID {
        return errors.New("无权操作此订单")
    }
    
    if !order.CanCancel() {
        return errors.New("订单状态不允许取消")
    }
    
    return s.db.Transaction(func(tx *gorm.DB) error {
        if err := tx.Model(order).Update("status", models.OrderStatusCancelled).Error; err != nil {
            return err
        }
        
        for _, item := range order.Items {
            if item.SkuID > 0 {
                if err := tx.Model(&models.ProductSku{}).
                    Where("id = ?", item.SkuID).
                    Update("stock", gorm.Expr("stock + ?", item.Quantity)).Error; err != nil {
                    return err
                }
            } else {
                if err := tx.Model(&models.Product{}).
                    Where("id = ?", item.ProductID).
                    Update("stock", gorm.Expr("stock + ?", item.Quantity)).Error; err != nil {
                    return err
                }
            }
        }
        
        return nil
    })
}

func (s *OrderService) PaySuccess(orderNo string, tradeNo string) error {
    order, err := s.GetByOrderNo(orderNo)
    if err != nil {
        return err
    }
    
    now := time.Now()
    
    return s.db.Model(order).Updates(map[string]interface{}{
        "status":        models.OrderStatusPaid,
        "payment_time":  &now,
        "payment_method": "alipay",
    }).Error
}

func generateOrderNo() string {
    return fmt.Sprintf("%d%06d", time.Now().UnixNano()/1e6, time.Now().Nanosecond()/1000)
}

支付服务 #

go
// services/payment_service.go
package services

import (
    "ecommerce-api/models"
    "errors"
    "fmt"
    "time"
    
    "gorm.io/gorm"
)

type PaymentService struct {
    db           *gorm.DB
    orderService *OrderService
}

func NewPaymentService(db *gorm.DB, orderService *OrderService) *PaymentService {
    return &PaymentService{db: db, orderService: orderService}
}

type PaymentRequest struct {
    OrderID uint   `json:"order_id" binding:"required"`
    Method  string `json:"method" binding:"required,oneof=alipay wechat"`
}

type PaymentResponse struct {
    PaymentNo string `json:"payment_no"`
    PayURL    string `json:"pay_url"`
}

func (s *PaymentService) Create(userID uint, req *PaymentRequest) (*PaymentResponse, error) {
    order, err := s.orderService.GetByID(req.OrderID)
    if err != nil {
        return nil, err
    }
    
    if order.UserID != userID {
        return nil, errors.New("无权操作此订单")
    }
    
    if !order.CanPay() {
        return nil, errors.New("订单状态不允许支付")
    }
    
    paymentNo := generatePaymentNo()
    
    payment := &models.Payment{
        PaymentNo: paymentNo,
        OrderID:   order.ID,
        OrderNo:   order.OrderNo,
        UserID:    userID,
        Amount:    order.PayAmount,
        Method:    req.Method,
        Status:    models.PaymentStatusPending,
    }
    
    if err := s.db.Create(payment).Error; err != nil {
        return nil, err
    }
    
    payURL := s.generatePayURL(payment, req.Method)
    
    return &PaymentResponse{
        PaymentNo: paymentNo,
        PayURL:    payURL,
    }, nil
}

func (s *PaymentService) generatePayURL(payment *models.Payment, method string) string {
    switch method {
    case "alipay":
        return fmt.Sprintf("https://openapi.alipay.com/gateway.do?payment_no=%s&amount=%.2f", 
            payment.PaymentNo, payment.Amount)
    case "wechat":
        return fmt.Sprintf("weixin://wxpay/bizpayurl?pr=%s", payment.PaymentNo)
    default:
        return ""
    }
}

func (s *PaymentService) Callback(paymentNo string, tradeNo string) error {
    var payment models.Payment
    if err := s.db.Where("payment_no = ?", paymentNo).First(&payment).Error; err != nil {
        return errors.New("支付记录不存在")
    }
    
    if payment.Status != models.PaymentStatusPending {
        return errors.New("支付状态异常")
    }
    
    now := time.Now()
    
    return s.db.Transaction(func(tx *gorm.DB) error {
        if err := tx.Model(&payment).Updates(map[string]interface{}{
            "status":   models.PaymentStatusSuccess,
            "trade_no": tradeNo,
            "paid_at":  &now,
        }).Error; err != nil {
            return err
        }
        
        if err := s.orderService.PaySuccess(payment.OrderNo, tradeNo); err != nil {
            return err
        }
        
        return nil
    })
}

func generatePaymentNo() string {
    return fmt.Sprintf("P%d%06d", time.Now().UnixNano()/1e6, time.Now().Nanosecond()/1000)
}

订单处理器 #

go
// handlers/order_handler.go
package handlers

import (
    "ecommerce-api/middleware"
    "ecommerce-api/services"
    "ecommerce-api/utils"
    "strconv"
    
    "github.com/gin-gonic/gin"
)

type OrderHandler struct {
    service *services.OrderService
}

func NewOrderHandler(service *services.OrderService) *OrderHandler {
    return &OrderHandler{service: service}
}

func (h *OrderHandler) Create(c *gin.Context) {
    var req services.CreateOrderRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        utils.BadRequest(c, "请求参数错误")
        return
    }
    
    var addr services.Address
    if err := c.ShouldBindJSON(&struct {
        services.CreateOrderRequest
        services.Address `json:"address"`
    }{}); err != nil {
        utils.BadRequest(c, "地址信息错误")
        return
    }
    
    userID := middleware.GetUserID(c)
    
    order, err := h.service.Create(userID, &req, &addr)
    if err != nil {
        utils.BadRequest(c, err.Error())
        return
    }
    
    utils.Created(c, order)
}

func (h *OrderHandler) GetByID(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        utils.BadRequest(c, "无效的订单ID")
        return
    }
    
    userID := middleware.GetUserID(c)
    
    order, err := h.service.GetByID(uint(id))
    if err != nil {
        utils.NotFound(c, err.Error())
        return
    }
    
    if order.UserID != userID {
        utils.Forbidden(c, "无权查看此订单")
        return
    }
    
    utils.Success(c, order)
}

func (h *OrderHandler) List(c *gin.Context) {
    userID := middleware.GetUserID(c)
    
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
    
    var status *int
    if s := c.Query("status"); s != "" {
        st, _ := strconv.Atoi(s)
        status = &st
    }
    
    orders, total, err := h.service.List(userID, status, page, pageSize)
    if err != nil {
        utils.InternalError(c, "获取订单列表失败")
        return
    }
    
    utils.SuccessWithPage(c, orders, total, page, pageSize)
}

func (h *OrderHandler) Cancel(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        utils.BadRequest(c, "无效的订单ID")
        return
    }
    
    userID := middleware.GetUserID(c)
    
    if err := h.service.Cancel(userID, uint(id)); err != nil {
        utils.BadRequest(c, err.Error())
        return
    }
    
    utils.Success(c, gin.H{"message": "订单已取消"})
}

路由配置 #

go
// router/router.go
package router

import (
    "ecommerce-api/handlers"
    "ecommerce-api/middleware"
    "ecommerce-api/models"
    "ecommerce-api/services"
    
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

func Setup(db *gorm.DB) *gin.Engine {
    r := gin.Default()
    
    db.AutoMigrate(
        &models.User{},
        &models.Product{},
        &models.ProductSku{},
        &models.Category{},
        &models.CartItem{},
        &models.Order{},
        &models.OrderItem{},
        &models.Payment{},
    )
    
    productService := services.NewProductService(db)
    cartService := services.NewCartService(db)
    orderService := services.NewOrderService(db, cartService)
    paymentService := services.NewPaymentService(db, orderService)
    
    productHandler := handlers.NewProductHandler(productService)
    cartHandler := handlers.NewCartHandler(cartService)
    orderHandler := handlers.NewOrderHandler(orderService)
    paymentHandler := handlers.NewPaymentHandler(paymentService)
    
    api := r.Group("/api/v1")
    {
        products := api.Group("/products")
        {
            products.GET("", productHandler.List)
            products.GET("/:id", productHandler.GetByID)
            
            admin := products.Use(middleware.JWTAuth(), middleware.RequireAdmin())
            {
                admin.POST("", productHandler.Create)
                admin.PUT("/:id", productHandler.Update)
                admin.DELETE("/:id", productHandler.Delete)
            }
        }
        
        cart := api.Group("/cart")
        cart.Use(middleware.JWTAuth())
        {
            cart.GET("", cartHandler.List)
            cart.POST("", cartHandler.Add)
            cart.PUT("/:id", cartHandler.UpdateQuantity)
            cart.PUT("/:id/selected", cartHandler.UpdateSelected)
            cart.DELETE("/:id", cartHandler.Delete)
            cart.DELETE("", cartHandler.Clear)
        }
        
        orders := api.Group("/orders")
        orders.Use(middleware.JWTAuth())
        {
            orders.GET("", orderHandler.List)
            orders.POST("", orderHandler.Create)
            orders.GET("/:id", orderHandler.GetByID)
            orders.PUT("/:id/cancel", orderHandler.Cancel)
        }
        
        payment := api.Group("/payment")
        payment.Use(middleware.JWTAuth())
        {
            payment.POST("/create", paymentHandler.Create)
            payment.POST("/callback", paymentHandler.Callback)
        }
    }
    
    return r
}

小结 #

本节构建了一个电商系统的后端API:

  1. 商品管理:商品、SKU、分类管理
  2. 购物车:添加、修改、删除、选择
  3. 订单系统:创建、取消、状态流转
  4. 支付集成:支付创建、回调处理
  5. 库存管理:下单扣减、取消恢复

这个电商API具备完整的电商核心功能,可以根据需求继续扩展优惠券、物流、评价等功能。

最后更新:2026-03-28