15分钟学 Go 实战项目五 :简单电子商务网站(3W字完整例子)
简单的电子商务网站开发实战
项目概述
目标
- 实现用户注册登录功能
- 开发商品浏览和搜索功能
- 实现购物车管理
- 完成订单处理流程
技术栈
类别 | 技术选择 | 说明 |
---|---|---|
Web框架 | Gin | 高性能HTTP框架 |
数据库 | MySQL | 存储用户和商品信息 |
缓存 | Redis | 购物车和会话管理 |
ORM | GORM | 数据库操作 |
认证 | JWT | 用户身份验证 |
一、项目结构
二、核心代码实现
1. 数据库模型定义
package model
import (
"time"
"gorm.io/gorm"
)
// User 用户模型
type User struct {
gorm.Model
Username string `gorm:"unique;not null" json:"username"`
Password string `gorm:"not null" json:"-"`
Email string `gorm:"unique;not null" json:"email"`
Role string `gorm:"default:'user'" json:"role"`
}
// Product 商品模型
type Product struct {
gorm.Model
Name string `gorm:"not null" json:"name"`
Description string `json:"description"`
Price float64 `gorm:"not null" json:"price"`
Stock int `gorm:"not null" json:"stock"`
CategoryID uint `json:"category_id"`
}
// CartItem 购物车项目
type CartItem struct {
gorm.Model
UserID uint `gorm:"not null" json:"user_id"`
ProductID uint `gorm:"not null" json:"product_id"`
Quantity int `gorm:"not null" json:"quantity"`
Product Product `gorm:"foreignKey:ProductID" json:"product"`
}
// Order 订单
type Order struct {
gorm.Model
UserID uint `gorm:"not null" json:"user_id"`
TotalAmount float64 `gorm:"not null" json:"total_amount"`
Status string `gorm:"default:'pending'" json:"status"`
Items []OrderItem `gorm:"foreignKey:OrderID" json:"items"`
}
// OrderItem 订单项目
type OrderItem struct {
gorm.Model
OrderID uint `gorm:"not null" json:"order_id"`
ProductID uint `gorm:"not null" json:"product_id"`
Quantity int `gorm:"not null" json:"quantity"`
Price float64 `gorm:"not null" json:"price"`
Product Product `gorm:"foreignKey:ProductID" json:"product"`
}
2. 用户认证中间件
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/dgrijalva/jwt-go"
"net/http"
"strings"
"time"
)
const (
secretKey = "your-secret-key"
)
// Claims JWT claims结构
type Claims struct {
UserID uint `json:"user_id"`
Role string `json:"role"`
jwt.StandardClaims
}
// GenerateToken 生成JWT token
func GenerateToken(userID uint, role string) (string, error) {
claims := Claims{
UserID: userID,
Role: role,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
IssuedAt: time.Now().Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secretKey))
}
// AuthMiddleware 认证中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(secretKey), nil
})
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
c.Abort()
return
}
// 将用户信息存储到上下文
c.Set("userID", claims.UserID)
c.Set("role", claims.Role)
c.Next()
}
}
// AdminRequired 管理员权限中间件
func AdminRequired() gin.HandlerFunc {
return func(c *gin.Context) {
role, exists := c.Get("role")
if !exists || role != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Admin privileges required"})
c.Abort()
return
}
c.Next()
}
}
3. 购物车服务实现
package service
import (
"errors"
"github.com/your/project/internal/model"
"gorm.io/gorm"
)
type CartService struct {
db *gorm.DB
}
func NewCartService(db *gorm.DB) *CartService {
return &CartService{db: db}
}
// AddToCart 添加商品到购物车
func (s *CartService) AddToCart(userID uint, productID uint, quantity int) error {
// 检查商品是否存在
var product model.Product
if err := s.db.First(&product, productID).Error; err != nil {
return errors.New("product not found")
}
// 检查库存
if product.Stock < quantity {
return errors.New("insufficient stock")
}
// 查找购物车中是否已存在该商品
var cartItem model.CartItem
result := s.db.Where("user_id = ? AND product_id = ?", userID, productID).First(&cartItem)
if result.Error == nil {
// 更新数量
cartItem.Quantity += quantity
return s.db.Save(&cartItem).Error
}
// 创建新的购物车项
cartItem = model.CartItem{
UserID: userID,
ProductID: productID,
Quantity: quantity,
}
return s.db.Create(&cartItem).Error
}
// GetCart 获取购物车内容
func (s *CartService) GetCart(userID uint) ([]model.CartItem, error) {
var cartItems []model.CartItem
err := s.db.Preload("Product").Where("user_id = ?", userID).Find(&cartItems).Error
return cartItems, err
}
// UpdateCartItem 更新购物车项目数量
func (s *CartService) UpdateCartItem(userID uint, itemID uint, quantity int) error {
var cartItem model.CartItem
if err := s.db.Where("id = ? AND user_id = ?", itemID, userID).First(&cartItem).Error; err != nil {
return errors.New("cart item not found")
}
// 检查库存
var product model.Product
if err := s.db.First(&product, cartItem.ProductID).Error; err != nil {
return errors.New("product not found")
}
if product.Stock < quantity {
return errors.New("insufficient stock")
}
cartItem.Quantity = quantity
return s.db.Save(&cartItem).Error
}
// RemoveFromCart 从购物车移除商品
func (s *CartService) RemoveFromCart(userID uint, itemID uint) error {
result := s.db.Where("id = ? AND user_id = ?", itemID, userID).Delete(&model.CartItem{})
if result.RowsAffected == 0 {
return errors.New("cart item not found")
}
return result.Error
}
// ClearCart 清空购物车
func (s *CartService) ClearCart(userID uint) error {
return s.db.Where("user_id = ?", userID).Delete(&model.CartItem{}).Error
}
// GetCartTotal 计算购物车总金额
func (s *CartService) GetCartTotal(userID uint) (float64, error) {
var total float64
var cartItems []model.CartItem
err := s.db.Preload("Product").Where("user_id = ?", userID).Find(&cartItems).Error
if err != nil {
return 0, err
}
for _, item := range cartItems {
total += item.Product.Price * float64(item.Quantity)
}
return total, nil
}
4. API路由定义
package main
import (
"github.com/gin-gonic/gin"
"github.com/your/project/internal/handler"
"github.com/your/project/internal/middleware"
)
func setupRouter(h *handler.Handler) *gin.Engine {
r := gin.Default()
// 公开路由
public := r.Group("/api/v1")
{
// 用户认证
public.POST("/register", h.Register)
public.POST("/login", h.Login)
// 商品
public.GET("/products", h.ListProducts)
public.GET("/products/:id", h.GetProduct)
public.GET("/products/search", h.SearchProducts)
}
// 需要认证的路由
authorized := r.Group("/api/v1")
authorized.Use(middleware.AuthMiddleware())
{
// 用户相关
authorized.GET("/profile", h.GetProfile)
authorized.PUT("/profile", h.UpdateProfile)
// 购物车
authorized.GET("/cart", h.GetCart)
authorized.POST("/cart", h.AddToCart)
authorized.PUT("/cart/:id", h.UpdateCartItem)
authorized.DELETE("/cart/:id", h.RemoveFromCart)
authorized.DELETE("/cart", h.ClearCart)
// 订单
authorized.POST("/orders", h.CreateOrder)
authorized.GET("/orders", h.ListOrders)
authorized.GET("/orders/:id", h.GetOrder)
}
// 管理员路由
admin := r.Group("/api/v1/admin")
admin.Use(middleware.AuthMiddleware(), middleware.AdminRequired())
{
// 商品管理
admin.POST("/products", h.CreateProduct)
admin.PUT("/products/:id", h.UpdateProduct)
admin.DELETE("/products/:id", h.DeleteProduct)
// 订单管理
admin.GET("/orders", h.ListAllOrders)
admin.PUT("/orders/:id/status", h.UpdateOrderStatus)
}
return r
}
三、流程图示
1. 用户注册登录流程
2. 购物车操作流程
四、处理器实现
1. 用户处理器
package handler
import (
"github.com/gin-gonic/gin"
"github.com/your/project/internal/model"
"github.com/your/project/internal/service"
"net/http"
)
type UserHandler struct {
userService *service.UserService
}
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
// Register 用户注册
func (h *UserHandler) Register(c *gin.Context) {
var input struct {
Username string `json:"username" binding:"required,min=3,max=32"`
Password string `json:"password" binding:"required,min=6"`
Email string `json:"email" binding:"required,email"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := h.userService.CreateUser(input.Username, input.Password, input.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
token, err := service.GenerateToken(user.ID, user.Role)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error generating token"})
return
}
c.JSON(http.StatusCreated, gin.H{
"token": token,
"user": user,
})
}
// Login 用户登录
func (h *UserHandler) Login(c *gin.Context) {
var input struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := h.userService.ValidateUser(input.Username, input.Password)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
token, err := service.GenerateToken(user.ID, user.Role)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error generating token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"user": user,
})
}
// GetProfile 获取用户信息
func (h *UserHandler) GetProfile(c *gin.Context) {
userID, _ := c.Get("userID")
user, err := h.userService.GetUserByID(userID.(uint))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, gin.H{"user": user})
}
// UpdateProfile 更新用户信息
func (h *UserHandler) UpdateProfile(c *gin.Context) {
userID, _ := c.Get("userID")
var input struct {
Email string `json:"email" binding:"omitempty,email"`
Password string `json:"password" binding:"omitempty,min=6"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := h.userService.UpdateUser(userID.(uint), input.Email, input.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"user": user})
}
2. 购物车处理器
package handler
import (
"github.com/gin-gonic/gin"
"github.com/your/project/internal/service"
"net/http"
"strconv"
)
type CartHandler struct {
cartService *service.CartService
}
func NewCartHandler(cartService *service.CartService) *CartHandler {
return &CartHandler{cartService: cartService}
}
// GetCart 获取购物车内容
func (h *CartHandler) GetCart(c *gin.Context) {
userID := c.GetUint("userID")
cartItems, err := h.cartService.GetCart(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
total, err := h.cartService.GetCartTotal(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"items": cartItems,
"total": total,
})
}
// AddToCart 添加商品到购物车
func (h *CartHandler) AddToCart(c *gin.Context) {
userID := c.GetUint("userID")
var input struct {
ProductID uint `json:"product_id" binding:"required"`
Quantity int `json:"quantity" binding:"required,min=1"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := h.cartService.AddToCart(userID, input.ProductID, input.Quantity)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Item added to cart"})
}
// UpdateCartItem 更新购物车商品数量
func (h *CartHandler) UpdateCartItem(c *gin.Context) {
userID := c.GetUint("userID")
itemID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item ID"})
return
}
var input struct {
Quantity int `json:"quantity" binding:"required,min=1"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err = h.cartService.UpdateCartItem(userID, uint(itemID), input.Quantity)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Cart item updated"})
}
// RemoveFromCart 从购物车移除商品
func (h *CartHandler) RemoveFromCart(c *gin.Context) {
userID := c.GetUint("userID")
itemID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid item ID"})
return
}
err = h.cartService.RemoveFromCart(userID, uint(itemID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Item removed from cart"})
}
// ClearCart 清空购物车
func (h *CartHandler) ClearCart(c *gin.Context) {
userID := c.GetUint("userID")
err := h.cartService.ClearCart(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Cart cleared"})
}
五、API接口说明
用户相关接口
接口 | 方法 | 描述 | 请求参数 | 返回数据 |
---|---|---|---|---|
/api/v1/register | POST | 用户注册 | username, password, email | token, user |
/api/v1/login | POST | 用户登录 | username, password | token, user |
/api/v1/profile | GET | 获取用户信息 | - | user |
/api/v1/profile | PUT | 更新用户信息 | email, password | user |
购物车相关接口
接口 | 方法 | 描述 | 请求参数 | 返回数据 |
---|---|---|---|---|
/api/v1/cart | GET | 获取购物车 | - | items, total |
/api/v1/cart | POST | 添加商品 | product_id, quantity | message |
/api/v1/cart/:id | PUT | 更新商品数量 | quantity | message |
/api/v1/cart/:id | DELETE | 删除商品 | - | message |
/api/v1/cart | DELETE | 清空购物车 | - | message |
六、测试案例
1. 用户服务测试
package service_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/your/project/internal/service"
"github.com/your/project/internal/model"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupTestDB(t *testing.T) *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
err = db.AutoMigrate(&model.User{})
assert.NoError(t, err)
return db
}
func TestUserService_CreateUser(t *testing.T) {
db := setupTestDB(t)
userService := service.NewUserService(db)
tests := []struct {
name string
username string
password string
email string
wantErr bool
}{
{
name: "Valid user creation",
username: "testuser",
password: "password123",
email: "test@example.com",
wantErr: false,
},
{
name: "Duplicate username",
username: "testuser",
password: "password123",
email: "test2@example.com",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user, err := userService.CreateUser(tt.username, tt.password, tt.email)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, tt.username, user.Username)
assert.Equal(t, tt.email, user.Email)
assert.NotEqual(t, tt.password, user.Password) // 密码应该已加密
})
}
}
func TestUserService_ValidateUser(t *testing.T) {
db := setupTestDB(t)
userService := service.NewUserService(db)
// 创建测试用户
testUser, err := userService.CreateUser("testuser", "password123", "test@example.com")
assert.NoError(t, err)
tests := []struct {
name string
username string
password string
wantErr bool
}{
{
name: "Valid credentials",
username: "testuser",
password: "password123",
wantErr: false,
},
{
name: "Invalid password",
username: "testuser",
password: "wrongpassword",
wantErr: true,
},
{
name: "Non-existent user",
username: "nonexistent",
password: "password123",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user, err := userService.ValidateUser(tt.username, tt.password)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, testUser.ID, user.ID)
})
}
}
2. 购物车服务测试
package service_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/your/project/internal/service"
"github.com/your/project/internal/model"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupCartTestDB(t *testing.T) *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
err = db.AutoMigrate(&model.User{}, &model.Product{}, &model.CartItem{})
assert.NoError(t, err)
return db
}
func createTestProduct(db *gorm.DB) *model.Product {
product := &model.Product{
Name: "Test Product",
Description: "Test Description",
Price: 99.99,
Stock: 100,
}
db.Create(product)
return product
}
func TestCartService_AddToCart(t *testing.T) {
db := setupCartTestDB(t)
cartService := service.NewCartService(db)
product := createTestProduct(db)
tests := []struct {
name string
userID uint
productID uint
quantity int
wantErr bool
}{
{
name: "Valid addition to cart",
userID: 1,
productID: product.ID,
quantity: 5,
wantErr: false,
},
{
name: "Exceed stock quantity",
userID: 1,
productID: product.ID,
quantity: 101,
wantErr: true,
},
{
name: "Non-existent product",
userID: 1,
productID: 999,
quantity: 1,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := cartService.AddToCart(tt.userID, tt.productID, tt.quantity)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
// 验证购物车项目
var cartItem model.CartItem
err = db.Where("user_id = ? AND product_id = ?", tt.userID, tt.productID).First(&cartItem).Error
assert.NoError(t, err)
assert.Equal(t, tt.quantity, cartItem.Quantity)
})
}
}
func TestCartService_GetCartTotal(t *testing.T) {
db := setupCartTestDB(t)
cartService := service.NewCartService(db)
// 创建测试产品
product1 := &model.Product{Name: "Product 1", Price: 10.00, Stock: 100}
product2 := &model.Product{Name: "Product 2", Price: 20.00, Stock: 100}
db.Create(product1)
db.Create(product2)
// 添加商品到购物车
cartService.AddToCart(1, product1.ID, 2) // 2 * 10.00 = 20.00
cartService.AddToCart(1, product2.ID, 1) // 1 * 20.00 = 20.00
// 计算总价
total, err := cartService.GetCartTotal(1)
assert.NoError(t, err)
assert.Equal(t, 40.00, total) // 20.00 + 20.00 = 40.00
}
func TestCartService_RemoveFromCart(t *testing.T) {
db := setupCartTestDB(t)
cartService := service.NewCartService(db)
product := createTestProduct(db)
// 先添加商品到购物车
err := cartService.AddToCart(1, product.ID, 1)
assert.NoError(t, err)
// 获取购物车项目ID
var cartItem model.CartItem
err = db.Where("user_id = ? AND product_id = ?", 1, product.ID).First(&cartItem).Error
assert.NoError(t, err)
// 测试移除
err = cartService.RemoveFromCart(1, cartItem.ID)
assert.NoError(t, err)
// 验证商品已被移除
var count int64
db.Model(&model.CartItem{}).Where("id = ?", cartItem.ID).Count(&count)
assert.Equal(t, int64(0), count)
}
七、性能优化建议
1. 数据库优化
-
索引优化
- 为常用查询字段添加索引
- 用户表:username, email
- 商品表:category_id, price
- 订单表:user_id, status
-
查询优化
- 使用预加载避免N+1问题
- 分页加载大量数据
- 适当使用缓存
2. 缓存策略
3. 购物车优化实现
package service
import (
"context"
"encoding/json"
"fmt"
"github.com/go-redis/redis/v8"
"github.com/your/project/internal/model"
"gorm.io/gorm"
"time"
)
type CachedCartService struct {
db *gorm.DB
redis *redis.Client
}
func NewCachedCartService(db *gorm.DB, redis *redis.Client) *CachedCartService {
return &CachedCartService{
db: db,
redis: redis,
}
}
// 生成购物车缓存key
func cartKey(userID uint) string {
return fmt.Sprintf("cart:user:%d", userID)
}
// AddToCart 添加商品到购物车(带缓存)
func (s *CachedCartService) AddToCart(userID uint, productID uint, quantity int) error {
ctx := context.Background()
// 检查商品并锁定库存
var product model.Product
err := s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Lock().First(&product, productID).Error; err != nil {
return err
}
if product.Stock < quantity {
return fmt.Errorf("insufficient stock")
}
return nil
})
if err != nil {
return err
}
// 更新数据库
cartItem := model.CartItem{
UserID: userID,
ProductID: productID,
Quantity: quantity,
Product: product,
}
if err := s.db.Create(&cartItem).Error; err != nil {
return err
}
// 更新缓存
cartItemJSON, err := json.Marshal(cartItem)
if err != nil {
return err
}
key := cartKey(userID)
pipe := s.redis.Pipeline()
pipe.HSet(ctx, key, fmt.Sprintf("item:%d", cartItem.ID), cartItemJSON)
pipe.Expire(ctx, key, 24*time.Hour) // 设置过期时间
_, err = pipe.Exec(ctx)
return err
}
// GetCart 获取购物车内容(带缓存)
func (s *CachedCartService) GetCart(userID uint) ([]model.CartItem, error) {
ctx := context.Background()
key := cartKey(userID)
// 尝试从缓存获取
cartData, err := s.redis.HGetAll(ctx, key).Result()
if err == nil && len(cartData) > 0 {
items := make([]model.CartItem, 0, len(cartData))
for _, itemJSON := range cartData {
var item model.CartItem
if err := json.Unmarshal([]byte(itemJSON), &item); err != nil {
continue
}
items = append(items, item)
}
return items, nil
}
// 缓存未命中,从数据库获取
var items []model.CartItem
if err := s.db.Preload("Product").Where("user_id = ?", userID).Find(&items).Error; err != nil {
return nil, err
}
// 更新缓存
pipe := s.redis.Pipeline()
for _, item := range items {
itemJSON, err := json.Marshal(item)
if err != nil {
continue
}
pipe.HSet(ctx, key, fmt.Sprintf("item:%d", item.ID), itemJSON)
}
pipe.Expire(ctx, key, 24*time.Hour)
pipe.Exec(ctx)
return items, nil
}
// RemoveFromCart 从购物车移除商品(带缓存)
func (s *CachedCartService) RemoveFromCart(userID uint, itemID uint) error {
ctx := context.Background()
// 删除数据库记录
if err := s.db.Where("id = ? AND user_id = ?", itemID, userID).Delete(&model.CartItem{}).Error; err != nil {
return err
}
// 删除缓存
key := cartKey(userID)
s.redis.HDel(ctx, key, fmt.Sprintf("item:%d", itemID))
return nil
}
// UpdateCartItem 更新购物车商品数量(带缓存)
func (s *CachedCartService) UpdateCartItem(userID uint, itemID uint, quantity int) error {
ctx := context.Background()
// 更新数据库
var cartItem model.CartItem
err := s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("id = ? AND user_id = ?", itemID, userID).First(&cartItem).Error; err != nil {
return err
}
// 检查库存
var product model.Product
if err := tx.First(&product, cartItem.ProductID).Error; err != nil {
return err
}
if product.Stock < quantity {
return fmt.Errorf("insufficient stock")
}
cartItem.Quantity = quantity
return tx.Save(&cartItem).Error
})
if err != nil {
return err
}
// 更新缓存
itemJSON, err := json.Marshal(cartItem)
if err != nil {
return err
}
key := cartKey(userID)
return s.redis.HSet(ctx, key, fmt.Sprintf("item:%d", itemID), itemJSON).Err()
}
八、错误处理和日志记录
1. 自定义错误类型
package errors
import "fmt"
// ErrorType 错误类型
type ErrorType string
const (
ErrorTypeValidation ErrorType = "VALIDATION_ERROR"
ErrorTypeNotFound ErrorType = "NOT_FOUND"
ErrorTypeUnauthorized ErrorType = "UNAUTHORIZED"
ErrorTypeDatabase ErrorType = "DATABASE_ERROR"
ErrorTypeInternal ErrorType = "INTERNAL_ERROR"
)
// AppError 应用错误
type AppError struct {
Type ErrorType `json:"type"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
func (e *AppError) Error() string {
if e.Details != "" {
return fmt.Sprintf("%s: %s (%s)", e.Type, e.Message, e.Details)
}
return fmt.Sprintf("%s: %s", e.Type, e.Message)
}
// 创建各类错误的工厂函数
func NewValidationError(message string, details string) *AppError {
return &AppError{
Type: ErrorTypeValidation,
Message: message,
Details: details,
}
}
func NewNotFoundError(message string) *AppError {
return &AppError{
Type: ErrorTypeNotFound,
Message: message,
}
}
func NewUnauthorizedError(message string) *AppError {
return &AppError{
Type: ErrorTypeUnauthorized,
Message: message,
}
}
func NewDatabaseError(err error) *AppError {
return &AppError{
Type: ErrorTypeDatabase,
Message: "Database operation failed",
Details: err.Error(),
}
}
func NewInternalError(err error) *AppError {
return &AppError{
Type: ErrorTypeInternal,
Message: "Internal server error",
Details: err.Error(),
}
}
2. 日志中间件
package middleware
import (
"bytes"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"io/ioutil"
"time"
)
// responseWriter 自定义响应写入器
type responseWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w responseWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
// LoggingMiddleware 日志中间件
func LoggingMiddleware(logger *logrus.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
// 开始时间
start := time.Now()
// 读取请求体
var requestBody []byte
if c.Request.Body != nil {
requestBody, _ = ioutil.ReadAll(c.Request.Body)
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(requestBody))
}
// 包装响应写入器
w := &responseWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer}
c.Writer = w
// 处理请求
c.Next()
// 请求处理时间
duration := time.Since(start)
// 获取用户ID(如果有)
userID, exists := c.Get("userID")
if !exists {
userID = "anonymous"
}
// 构建日志字段
fields := logrus.Fields{
"client_ip": c.ClientIP(),
"duration": duration,
"method": c.Request.Method,
"path": c.Request.URL.Path,
"status": c.Writer.Status(),
"user_id": userID,
"referrer": c.Request.Referer(),
"user_agent": c.Request.UserAgent(),
}
// 添加请求体(如果不是太大)
if len(requestBody) > 0 && len(requestBody) < 1024 {
fields["request_body"] = string(requestBody)
}
// 添加响应体(如果不是太大)
responseBody := w.body.String()
if len(responseBody) > 0 && len(responseBody) < 1024 {
fields["response_body"] = responseBody
}
// 根据状态码选择日志级别
if c.Writer.Status() >= 500 {
logger.WithFields(fields).Error("Server error")
} else if c.Writer.Status() >= 400 {
logger.WithFields(fields).Warn("Client error")
} else {
logger.WithFields(fields).Info("Request processed")
}
}
}
九、项目部署和监控
1. Docker部署配置
# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
# 安装依赖
COPY go.mod go.sum ./
RUN go mod download
# 复制源代码
COPY . .
# 编译
RUN CGO_ENABLED=0 GOOS=linux go build -o main cmd/server/main.go
# 最终镜像
FROM alpine:latest
WORKDIR /app
# 从builder阶段复制编译后的程序
COPY --from=builder /app/main .
COPY --from=builder /app/configs ./configs
# 暴露端口
EXPOSE 8080
# 启动程序
CMD ["./main"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- DB_HOST=mysql
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=secret
- DB_NAME=ecommerce
- REDIS_HOST=redis
- REDIS_PORT=6379
- JWT_SECRET=your-secret-key
depends_on:
- mysql
- redis
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: ecommerce
volumes:
- mysql-data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:alpine
volumes:
- redis-data:/data
ports:
- "6379:6379"
volumes:
mysql-data:
redis-data:
2. 性能监控实现
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"time"
)
var (
httpRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
httpRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
},
[]string{"method", "path"},
)
activeRequests = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "http_requests_active",
Help: "Number of active HTTP requests",
},
)
responseSize = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_response_size_bytes",
Help: "Size of HTTP responses in bytes",
Buckets: []float64{100, 1000, 10000, 100000, 1000000},
},
[]string{"method", "path"},
)
)
// MetricsMiddleware 性能监控中间件
func MetricsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// 增加活跃请求计数
activeRequests.Inc()
// 请求处理完成后执行
defer func() {
// 减少活跃请求计数
activeRequests.Dec()
// 记录请求总数
status := c.Writer.Status()
httpRequestsTotal.WithLabelValues(
c.Request.Method,
c.FullPath(),
string(rune(status)),
).Inc()
// 记录请求处理时间
duration := time.Since(start).Seconds()
httpRequestDuration.WithLabelValues(
c.Request.Method,
c.FullPath(),
).Observe(duration)
// 记录响应大小
responseSize.WithLabelValues(
c.Request.Method,
c.FullPath(),
).Observe(float64(c.Writer.Size()))
}()
c.Next()
}
}
// PerformanceMonitor 系统性能监控
type PerformanceMonitor struct {
requestCount int64
errorCount int64
responseTimes []float64
slowThreshold float64 // 慢请求阈值(秒)
}
func NewPerformanceMonitor() *PerformanceMonitor {
return &PerformanceMonitor{
slowThreshold: 1.0, // 默认1秒为慢请求
responseTimes: make([]float64, 0),
}
}
// RecordMetrics 记录性能指标
func (pm *PerformanceMonitor) RecordMetrics(c *gin.Context, start time.Time) {
duration := time.Since(start).Seconds()
// 记录响应时间
pm.responseTimes = append(pm.responseTimes, duration)
// 记录请求计数
pm.requestCount++
// 记录错误计数
if c.Writer.Status() >= 400 {
pm.errorCount++
}
// 记录慢请求
if duration > pm.slowThreshold {
logrus.WithFields(logrus.Fields{
"path": c.Request.URL.Path,
"method": c.Request.Method,
"duration": duration,
}).Warn("Slow request detected")
}
}
// GetMetrics 获取性能指标统计
func (pm *PerformanceMonitor) GetMetrics() map[string]interface{} {
var avgResponseTime float64
if len(pm.responseTimes) > 0 {
total := 0.0
for _, t := range pm.responseTimes {
total += t
}
avgResponseTime = total / float64(len(pm.responseTimes))
}
return map[string]interface{}{
"total_requests": pm.requestCount,
"error_rate": float64(pm.errorCount) / float64(pm.requestCount),
"avg_response_time": avgResponseTime,
}
}
3. 健康检查接口
package handler
import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/go-redis/redis/v8"
"net/http"
"context"
"time"
)
type HealthHandler struct {
db *gorm.DB
redis *redis.Client
}
func NewHealthHandler(db *gorm.DB, redis *redis.Client) *HealthHandler {
return &HealthHandler{
db: db,
redis: redis,
}
}
// 健康检查响应结构
type HealthCheck struct {
Status string `json:"status"`
Version string `json:"version"`
Services map[string]string `json:"services"`
Timestamp time.Time `json:"timestamp"`
Environment string `json:"environment"`
}
// Check 健康检查处理函数
func (h *HealthHandler) Check(c *gin.Context) {
ctx := context.Background()
services := make(map[string]string)
// 检查数据库连接
sqlDB, err := h.db.DB()
if err != nil {
services["database"] = "error: " + err.Error()
} else if err := sqlDB.Ping(); err != nil {
services["database"] = "error: " + err.Error()
} else {
services["database"] = "healthy"
}
// 检查Redis连接
if err := h.redis.Ping(ctx).Err(); err != nil {
services["redis"] = "error: " + err.Error()
} else {
services["redis"] = "healthy"
}
// 确定整体状态
status := "healthy"
for _, s := range services {
if s != "healthy" {
status = "degraded"
break
}
}
response := HealthCheck{
Status: status,
Version: "1.0.0", // 从配置中获取
Services: services,
Timestamp: time.Now(),
Environment: "production", // 从环境变量获取
}
c.JSON(http.StatusOK, response)
}
// DetailedHealth 详细的健康检查
func (h *HealthHandler) DetailedHealth(c *gin.Context) {
ctx := context.Background()
details := make(map[string]interface{})
// 数据库详细信息
if sqlDB, err := h.db.DB(); err == nil {
dbStats := make(map[string]interface{})
dbStats["max_open_connections"] = sqlDB.Stats().MaxOpenConnections
dbStats["open_connections"] = sqlDB.Stats().OpenConnections
dbStats["in_use"] = sqlDB.Stats().InUse
dbStats["idle"] = sqlDB.Stats().Idle
details["database"] = dbStats
}
// Redis详细信息
if info, err := h.redis.Info(ctx).Result(); err == nil {
details["redis"] = map[string]interface{}{
"info": info,
}
}
// 系统信息
details["system"] = map[string]interface{}{
"goroutines": runtime.NumGoroutine(),
"cgocalls": runtime.NumCgoCall(),
"cpus": runtime.NumCPU(),
}
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"details": details,
"timestamp": time.Now(),
})
}
// MetricsHandler 指标收集处理器
func (h *HealthHandler) MetricsHandler(c *gin.Context) {
metrics := make(map[string]interface{})
// 收集数据库指标
if sqlDB, err := h.db.DB(); err == nil {
stats := sqlDB.Stats()
metrics["database"] = map[string]interface{}{
"connections": {
"max_open": stats.MaxOpenConnections,
"open": stats.OpenConnections,
"in_use": stats.InUse,
"idle": stats.Idle,
},
"queries": {
"wait_count": stats.WaitCount,
"wait_duration": stats.WaitDuration,
"max_idle_closed": stats.MaxIdleClosed,
"max_lifetime_closed": stats.MaxLifetimeClosed,
},
}
}
// 收集系统指标
var m runtime.MemStats
runtime.ReadMemStats(&m)
metrics["system"] = map[string]interface{}{
"memory": {
"alloc": m.Alloc,
"total_alloc": m.TotalAlloc,
"sys": m.Sys,
"num_gc": m.NumGC,
},
"goroutines": runtime.NumGoroutine(),
}
c.JSON(http.StatusOK, metrics)
}
十、项目总结与最佳实践
1. 项目架构总览
2. 性能优化总结
优化点 | 实现方式 | 效果 |
---|---|---|
数据库查询 | 索引优化、连接池 | 提高查询速度 |
缓存策略 | Redis缓存热点数据 | 减少数据库压力 |
并发处理 | Go协程池 | 提高并发性能 |
API响应 | GZIP压缩、分页 | 优化响应时间 |
代码结构 | 模块化、依赖注入 | 提高可维护性 |
3. 安全措施
-
认证和授权
- JWT Token验证
- 角色权限控制
- 请求频率限制
-
数据安全
- 密码加密存储
- SQL注入防护
- XSS防护
-
传输安全
- HTTPS
- 数据加密
- 安全Headers
4. 开发建议
-
代码规范
- 使用 gofmt 格式化代码 - 遵循 Go 官方代码规范 - 编写完整的单元测试 - 使用 golangci-lint 进行代码检查
-
错误处理
- 自定义错误类型 - 统一错误处理中间件 - 详细的错误日志记录
-
文档维护
- API文档使用Swagger - 代码注释完整 - README文档及时更新
5. 部署注意事项
-
环境配置
- 使用环境变量
- 配置文件分环境
- 敏感信息加密
-
监控告警
- 设置合理的监控指标
- 配置告警阈值
- 建立告警机制
-
备份策略
- 定期数据备份
- 多副本存储
- 灾难恢复预案
通过以上内容,我们完成了一个功能完整、性能优化、安全可靠的电子商务网站后端系统。项目涵盖了用户管理、商品管理、购物车功能、订单处理等核心功能,并实现了必要的性能优化和安全措施。在实际开发中,可以根据具体需求进行适当的调整和扩展。