Go 进阶:Go + gin 极速搭建EcommerceSys电商系统


前言

本章节适合有一定基础的 Golang 初学者,通过简单的项目实践来加深对 Golang 的基本语法和 Web 开发的理解。

项目结构

项目流程图

  1. 技术栈

技术栈

  1. 项目结构

项目结构图

  1. 项目路由

项目路由图

  1. 项目模型

项目模型图

项目初始化

  1. 初始化项目文件夹
md ecommerce-sys
  1. 初始化 mod 文件
cd ecommerce-sys
go mod init github.com/your_username/ecommerce-sys

注意,此处的 your_username 请替换为你的 GitHub 用户名
本项目中,将会使用自己的 GitHub 用户名,请自行修改

  1. 检查 go.mod 文件是否创建成功并启动 VS Code
dir # linux 下使用 ls 命令

code .
  1. 创建 ecommerce-sys 数据库

打开 MongoDB ,输入以下命令创建 ecommerce-sys 数据库:

use database_name

其中,database_name 请替换为你自己喜欢的数据库名称。

  1. 初始化项目结构

一行代码在项目根目录下创建目录和空文件

# Windows 系统
mkdir controllers database middleware models routes tokens & echo. > controllers\address.go & echo. > controllers\cart.go & echo. > controllers\controllers.go & echo. > database\cart.go & echo. > database\databasetup.go & echo. > middleware\middleware.go & echo. > models\models.go & echo. > routes\routes.go & echo. > tokens\tokengen.go

# Linux 系统
mkdir -p controllers database middleware models routes tokens && touch controllers/address.go controllers/cart.go controllers/controllers.go database/cart.go database/databasetup.go middleware/middleware.go models/models.go routes/routes.go tokens/tokengen.go
  1. 安装 gin 包 和 Air
go get -u github.com/gin-gonic/gin
go install github.com/air-verse/air@latest
  1. 配置 Air 热重载

将具有默认设置的 .air.toml 配置文件初始化到当前目录

air init

Air 配置教程:如果有特殊需要请自行参考

如果以上都正常,您只需执行 air 命令,就能使用 .air.toml 文件中的配置热重载你的项目了。

air

搭建项目骨架

  1. 编写 routes/routes.go 文件

为什么要最先编写路由?

优先选择编写路由文件的原因在于路由决定了用户访问的 URL 所对应的页面和内容。也就是说,路由是用户请求的起点。

因为所有操作都从请求接口开始,定义好路由可以帮助我们明确应用的整体结构。

在路由确定之后,我们可以进一步编写控制器和模型,这样可以确保应用的各个部分都能协调工作。

虽然每个人的开发习惯和业务逻辑可能不同,但从路由入手通常是一个推荐的方法,它能帮助你更清晰地组织代码, 并且让你曾经觉得难以完成的独立开发一个项目变得轻松可行。

package routes

import (
	"github.com/Done-0/ecommerce-sys/controllers"
	"github.com/gin-gonic/gin"
)

// UserRoutes 定义用户相关的路由
func UserRoutes(incomingRoutes *gin.Engine) { // 创建 *gin.Engine 实例, 即 incomingRoutes 参数
	incomingRoutes.POST("/users/signup", controllers.SignUp()) // 注册
	incomingRoutes.POST("/users/login", controllers.Login()) // 登录
	incomingRoutes.POST("/admin/addproduct", controllers.ProductViewerAdmin()) // 管理员浏览商品
	incomingRoutes.GET("/users/productview", controllers.SearchProduct()) // 查询所有商品
	incomingRoutes.GET("/users/search", controllers.SearchProductByQuery()) // 通过 ID 查询商品
}
  1. 编写 main.go 文件
package main

import (
	"os"
	"log"

	"github.com/Done-0/ecommerce-sys/routes"
	"github.com/Done-0/ecommerce-sys/controllers"
	"github.com/Done-0/ecommerce-sys/database"
	"github.com/Done-0/ecommerce-sys/middleware"
	"github.com/gin-gonic/gin"
)

func main() {
	// 获取环境变量PORT的值, 如果不存在则赋值8000
	port := os.Getenv("PORT")
	if port == "" {
		port = "8000"
	}

	// 创建应用程序实例
	app := controllers.NewApplication(
		database.ProductData(database.Client, "Products"),
		database.UserData(database.Client, "Users"),
	)

	router := gin.New()
	router.Use(gin.Logger())
	router.Use(gin.Recovery())

	// 注册
	routes.UserRoutes(router) // 调用routs包中的UserRoutes函数,注册路由,并命名为router
	router.Use(middleware.Authentication())


	// 定义用户路由之外的路由
	router.GET("/addtocart", app.AddToCart())
	router.GET("/removeitem", app.RemoveItem())
	router.GET("/cartcheckout", app.BuyFromCart())
	router.GET("/instantbuy", app.InstantBuy())

	log.Fatal(router.Run(":" + port))
}
  1. 编写 models/models.go 文件
package models

import (
	"time"
	"go.mongodb.org/mongo-driver/bson/primitive"
)

type User struct {
	ID	            	primitive.ObjectID		  `json:"_id" bson:"_id"`
	Name	            *string					  `json:"name" validate:"required,min=6,max=30"`
	Password	        *string					  `json:"password" validate:"required,min=6,max=30"`
	Email				*string					  `json:"email" validate:"email,required"`
	Phone				*string					  `json:"phone" validate:"required"`
	Token				*string					  `json:"token"`
	Refresh_Token		*string					  `json:"refresh_token"`
	Created_At			time.Time				  `json:"created_at"`
	Updated_At			time.Time				  `json:"updated_at"`
	User_ID				string					  `json:"user_id"`
	// 切片本身已经是一个引用类型,能够提供对底层数据的引用,因此不加*号
	UserCart			[]ProductUser 			  `json:"usercart" bson:"usercart"`
	Address_Details		[]Address				  `json:"address" bson:"address"`
	Order_Status		[]Order					  `json:"order" bson:"order"`
}

type Product struct {
	Product_ID          primitive.ObjectID		  `bson:"_id"`
	Product_Name	    *string					  `json:"product_name"`
	//uint64: 是一种无符号 64 位整数类型。它可以存储从 0 到 2^64-1 之间的整数。
	Price				*uint64  				  `json:"price"`
	Rating				*uint8					  `json:"rating"`
	// Image 只存储一个网址,则为 string 类型
	Image				*string  				  `json:"image"`
}

type ProductUser struct {
	Product_ID			primitive.ObjectID		  `bson:"_id"`
	Product_Name		*string					  `json:"product_name"`
	Price				*uint64					  `json:"price"`
	Rating				*uint8					  `json:"rating"`
	Image 				*string					  `json:"image"`
}

type Address struct {
	Address_id			primitive.ObjectID		   `bson:"_id"`
	House				*string					   `json:"house_name" bson:"house_name"`
	Street				*string					   `json:"street_name" bson:"street_name"`
	City				*string					   `json:"city_name" bson:"city_name"`
	PostalCode			*string					   `json:"postalcode" bson:"postalcode"`
}

type Order struct {
	Order_ID			primitive.ObjectID		   `bson:"_id"`
	Order_Cart			[]ProductUser			   `json:"order_list" bson:"order_list"`
	Ordered_At			time.Time				   `json:"ordered_at" bson:"ordered_at"`
	Price				int						   `json:"price" bson:"price"`
	Discount			*int					   `json:"discount" bson:"discount"`
	Payment_Method		Payment					   `json:"payment_method" bson:"payment_method"`
}

type Payment struct {
	Digital				bool		
	COD					bool
}

知识小课堂:**为什么结构体中字段名的首字母大写?**​在 Go 语言中,结构体字段名的首字母决定了该字段的可见性:

  • 首字母大写的字段名:这些字段是 “导出” 的,意味着它们可以在包外部访问。这类似于其他编程语言中的 “public” 访问级别。例如:
type User struct {
   Name  string  // 导出字段,可以在包外访问
}

在这个例子中,Name 字段是导出的,可以在其他包中通过 user.Name 访问。

  • 首字母小写的字段名:这些字段是 “未导出” 的,仅在定义它们的包内部可见。这类似于其他编程语言中的 “private” 访问级别。例如:
type User struct {
   name  string  // 未导出字段,只能在包内访问
}

知识小课堂:**结构体标签中的 jsonbson有什么不同?**​在 Go 语言的结构体定义中,标签(tag)用于指示序列化库如何处理字段。常见的标签包括 jsonbson

  • json 标签:用于指定当结构体字段被序列化为 JSON 时,使用的字段名。例如:
type User struct {
    Name  string  `json:"name"`
}  

在这个例子中,即使 NameGo 代码中是大写的,在 JSON 输出中,它将会被序列化为小写的 "name" 键。

  • bson 标签:用于指定当结构体字段被序列化为 BSON(MongoDB 的文档格式)时,使用的字段名。例如:
type User struct {
    ID  primitive.ObjectID  `bson:"_id"`
}  

在这个例子中,ID 字段会被映射到 MongoDB 文档的 _id 字段,这是 MongoDB 中常用的主键字段名。

标签中的 _ 和不同点bson:"_id" 标签:_idMongoDB 的标准字段名,表示文档的唯一标识符。Go 语言中的字段名可以不同,但通过 bson 标签,你可以将其映射到 MongoDB 的 _id 字段。

type User struct {
   UserID  primitive.ObjectID  `bson:"_id"`
}

这里,UserID 字段会被存储为 MongoDB 中的 _id 字段。
使用标签的好处:通过 jsonbson 标签,你可以将 Go 结构体字段名与 JSONBSON 中的字段名分开管理,这在处理不同的命名约定时非常有用。标签也可以控制序列化和反序列化时的行为,比如忽略某些字段或者使用自定义名称。

  1. 搭建 controllers 控制器骨架
  1. 首先,搭建 controllers/controllers.go 业务逻辑层骨架
package controllers

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"time"
	"github.com/Done-0/ecommerce-sys/database"
	"github.com/Done-0/ecommerce-sys/models"
	generate "github.com/Done-0/ecommerce-sys/tokens"
	"github.com/go-playground/validator/v10"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"golang.org/x/crypto/bcrypt"
	"github.com/gin-gonic/gin"
)

// 用户路由逻辑函数

func HashPassword (password string) string {

}


func VertifyPassword (userPassword string, givenPassword string) (bool, string) {

}



func SignUp () gin.HandleFunc {

}


func Login () gin.HandlerFunc {

}


func ProductViewerAdmin () gin.HandlerFunc {

}


func SearchProduct() gin.HandlerFunc {

}

func SearchProductByQuery() gin.HandlerFunc {

}
  1. 其次,搭建 controllers/cart.go 业务逻辑层骨架
package controllers

import (

)

type Application struct {
	prodCollection *mongo.Collection // 用于存储与产品相关的 MongoDB 集合。
	userCollection *mongo.Collection // 用于存储与用户相关的 MongoDB 集合。
}

// NewApplication 创建一个新的 Application 实例。
// prodCollection 和 userCollection 是 MongoDB 的集合。
func NewApplication(prodCollection, userCollection *mongo.Collection) *Application {
	// 确保传入的集合有效
	if prodCollection == nil || userCollection == nil {
		// 可以在这里处理空指针情况,确保传入的集合有效
		log.Fatal("prodCollection or userCollection is nil")
	}
	// 如果参数有效,函数创建并返回一个 Application 实例,并将 prodCollection 和 userCollection 分别初始化为传入的集合。
	return &Application{
		prodCollection: prodCollection,
		userCollection: userCollection,
	}
}


func AddToCart() gin.HandlerFunc {

}

func RemoveItem() gin.HandlerFunc {

}

func GetItemFromCart() gin.HandlerFunc {

}

func BuyFromCart() gin.HandlerFunc {

}

func InstantBuy() gin.HandlerFunc {

}
  1. 最后,搭建 controllers/address.go 业务逻辑层骨架
package controllers

import (

)

func AddAdress() gin.HandlerFunc {

}

func EditHomeAddress() gin.HandlerFunc {

}

func EditWorkAddress() gin.HandlerFunc {

}
func DeleteAddress() gin.HandlerFunc {

}
  1. 配置 database 数据库
  1. 首先,搭建 database/cart.go 数据库层骨架
package database

import (

)

var (
	ErrCantFindProduct = errors.New("can't find the product")  // 表示找不到产品的错误。
	ErrCantDecodeProducts = errors.New("can't find the product")  // 表示解码产品失败的错误
	ErrUserIdIsNotValid = errors.New("this user is not valid")  // 表示用户 ID 无效的错误。
	ErrCantUpdateUser = errors.New("cannot add this product to the cart")  // 表示无法更新用户的错误。
	ErrCantRemoveItemCart = errors.New("cannot remove this item from the cart")  // 表示无法从购物车中移除项的错误。
	ErrCantGetItem = errors.New("was unnable to get the item from the cart")  //表示无法从购物车中获取项的错误。
	ErrCantBuyCartItem = errors.New("cannot update the purchase")  // 表示无法更新购买的错误。
)

func AddProductToCart() {

}

func RemoveCartItem() {

}

func BuyItemFromCart() {

}

func InstantBuyer() {

}
  1. 其次,搭建 database/databasetup.go 数据库层骨架
package database

import (
	"context"
	"log"
	"fmt"
	"time"

	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

func DBSet() *mongo.Client {
	// 创建一个带有 10 秒超时限制的上下文 ctx
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// 使用 mongo.Connect 方法创建并连接到 MongoDB 客户端
	client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
	if err != nil {
		log.Fatal(err)
	}

	// 使用 Ping 方法检查连接是否成功
	err = client.Ping(ctx, nil)
	if err != nil {
		log.Println("failed to connect to mongodb :(", err)
		return nil
	}

	fmt.Println("Successfully connected to mongodb")

	// 连接成功,返回配置完成的 MongoDB 客户端实例
	return client
}
// 调用 DBSet() 函数,获取一个 MongoDB 客户端实例,并将其赋值给全局变量 Client
// Client 可以在程序的其他部分使用,以与 MongoDB 进行交互。
var Client *mongo.Client = DBSet()

func UserData(client *mongo.Client, collectionName string) *mongo.Collection{
	// 从数据库 "Ecommerce" 中获取指定名称的集合
	var collection *mongo.Collection = client.Database("Ecommerce").Collection(collectionName)
	// 返回获取到的集合
	return collection
}

func ProductData(client *mongo.Client, collectionName string) *mongo.Collection{
	// 从数据库 "Ecommerce" 中获取指定名称的集合
	var productCollection *mongo.Collection = client.Database("Ecommerce").Collection(collectionName)
	// 返回获取到的集合
	return productCollection
}

编写业务逻辑

实现登录注册接口

  1. 编写 controllers/controllers.go 业务逻辑层
  1. 密码哈希处理
// HashPassword 接受一个明文密码,并返回其加密后的哈希值。
func HashPassword(password string) string {
	// 生成密码哈希
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
	if err != nil {
		// 如果生成哈希过程中发生错误,则记录错误并引发Panic
		log.Panic(err)
	}
	// 返回密码哈希的字符串形式
	return string(bytes)
}
  1. 实现注册功能
// SignUp 处理用户注册请求,返回一个 gin.HandlerFunc。
func SignUp() gin.HandlerFunc {

	return func(c *gin.Context) {
		// 设置请求超时
		var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
		defer cancel()

		var user models.User
		// 解析请求的 JSON 数据到 user 结构体
		if err := c.BindJSON(&user); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// 验证用户数据
		validationErr := Validate.Struct(user)
		if validationErr != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": validationErr})
			return
		}

		// 检查邮箱是否已被注册
		count, err := UserCollection.CountDocuments(ctx, bson.M{"email": user.Email})
		if err != nil {
			log.Panic(err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": err})
			return
		}

		if count > 0 {
			c.JSON(http.StatusBadRequest, gin.H{"error": "用户已存在!"})
			return
		}

		// 检查手机号码是否已被注册
		count, err = UserCollection.CountDocuments(ctx, bson.M{"phone": user.Phone})
		if err != nil {
			log.Panic(err)
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		if count > 0 {
			c.JSON(http.StatusBadRequest, gin.H{"error": "此号码已被注册!"})
			return
		}

		// 对密码进行哈希处理
		password := HashPassword(*user.Password)
		user.Password = &password

		// 设置创建时间和更新时间
		user.Created_At, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
		user.Updated_At, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))

		// 生成用户 ID 和令牌
		user.ID = primitive.NewObjectID()
		user.User_ID = user.ID.Hex()
		token, refreshtoken, _ := generate.TokenGenerator(*user.Email, *user.Name, user.User_ID)
		user.Token = &token
		user.Refresh_Token = &refreshtoken

		// 初始化用户购物车、地址和订单状态
		user.UserCart = make([]models.ProductUser, 0)
		user.Address_Details = make([]models.Address, 0)
		user.Order_Status = make([]models.Order, 0)

		// 将用户数据插入数据库
		_, inserterr := UserCollection.InsertOne(ctx, user)
		if inserterr != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "用户创建失败"})
			return
		}

		// 返回注册成功响应
		c.JSON(http.StatusCreated, "成功注册!")
	}
}
  1. 密码校验器
// VerifyPassword 验证用户输入的密码是否与存储的哈希密码匹配。
func VerifyPassword(userPassword string, givenPassword string) (bool, string) {
	err := bcrypt.CompareHashAndPassword([]byte(userPassword), []byte(givenPassword))
	valid := true
	msg := ""

	if err != nil {
		msg = "用户名或密码错误"
		valid = false
	}
	return valid, msg
}
  1. 实现登录功能
// Login 处理用户登录请求,返回一个 gin.HandlerFunc。
func Login() gin.HandlerFunc {

	return func(c *gin.Context) {
		// 设置请求超时
		var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
		defer cancel()

		var founduser models.User
		var user models.User
		// 解析请求的 JSON 数据到 user 结构体
		if err := c.BindJSON(&user); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// 根据邮箱查找用户
		err := UserCollection.FindOne(ctx, bson.M{"email": user.Email}).Decode(&founduser)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "登录或密码错误"})
			return
		}

		// 验证密码
		PasswordIsValid, msg := VerifyPassword(*user.Password, *founduser.Password)
		if !PasswordIsValid {
			c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
			fmt.Println(msg)
			return
		}

		// 生成新的令牌
		token, refreshToken, _ := generate.TokenGenerator(*founduser.Email, *founduser.Name, founduser.User_ID)

		// 更新用户的令牌
		generate.UpdateAllTokens(token, refreshToken, founduser.User_ID)

		// 返回用户信息和新令牌
		c.JSON(http.StatusFound, gin.H{
			"user":  founduser,
			"token": token,
			"refreshToken": refreshToken,
		})
	}
}

实现购物车接口

  1. 管理员添加商品
// ProductViewerAdmin 处理管理员添加产品请求,返回一个 gin.HandlerFunc。
func ProductViewerAdmin() gin.HandlerFunc {

	return func(c *gin.Context) {
		// 设置请求超时
		var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
		defer cancel()

		var products models.Product
		// 解析请求的 JSON 数据到 products 结构体
		if err := c.BindJSON(&products); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// 为产品分配一个新的 ObjectID
		products.Product_ID = primitive.NewObjectID()

		// 将产品插入到数据库中
		_, anyerr := ProductCollection.InsertOne(ctx, products)
		if anyerr != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "未能插入产品"})
			return
		}

		// 返回成功响应
		c.JSON(http.StatusOK, "成功添加产品")
	}
}
  1. 实现购物车功能:编写 controllers/cart.go 业务逻辑层
  • 用户购物车之 :
// AddToCart 处理添加商品到购物车的请求,返回一个 gin.HandlerFunc。
func (app *Application) AddToCart() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 获取查询参数 "id" 和 "userID"
		productQueryID := c.Query("id")
		userQueryID := c.Query("userID")

		// 如果产品ID为空,记录日志并返回 400 错误
		if productQueryID == "" {
			log.Println("product id is empty, please provide a valid product id")
			_ = c.AbortWithError(http.StatusBadRequest, errors.New("product id is empty"))
			return
		}

		// 如果用户ID为空,记录日志并返回 400 错误
		if userQueryID == "" {
			log.Println("user id is empty")
			_ = c.AbortWithError(http.StatusBadRequest, errors.New("user id is empty"))
			return
		}

		// 将产品ID从字符串转换为 ObjectID
		ProductID, err := primitive.ObjectIDFromHex(productQueryID)
		if err != nil {
			log.Println("Invalid product ID format:", err)
			c.AbortWithStatus(http.StatusBadRequest)
			return
		}

		// 设置一个 5 秒的上下文超时时间
		var ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		// 调用数据库方法添加商品到购物车
		err = database.AddProductToCart(ctx, app.prodCollection, app.userCollection, ProductID, userQueryID)
		if err != nil {
			log.Println("Error adding product to cart:", err)
			c.IndentedJSON(http.StatusInternalServerError, err.Error())
			return
		}

		// 成功添加商品,返回 200 状态和成功消息
		c.IndentedJSON(http.StatusOK, "Successfully added product to the cart")
	}
}
  • 用户购物车 :
// RemoveItem 处理移除购物车商品的请求,返回一个 gin.HandlerFunc。
func (app *Application) RemoveItem() gin.HandlerFunc {

	return func(c *gin.Context) {
		// 获取查询参数 "id" 和 "userID"
		productQueryID := c.Query("id")
		userQueryID := c.Query("userID")

		// 如果产品ID为空,记录日志并返回 400 错误
		if productQueryID == "" {
			log.Println("product id is empty, please provide a valid product id")
			_ = c.AbortWithError(http.StatusBadRequest, errors.New("product id is empty"))
			return
		}

		// 如果用户ID为空,记录日志并返回 400 错误
		if userQueryID == "" {
			log.Println("user id is empty")
			_ = c.AbortWithError(http.StatusBadRequest, errors.New("user id is empty"))
			return
		}

		// 将产品ID从字符串转换为 ObjectID
		ProductID, err := primitive.ObjectIDFromHex(productQueryID)
		if err != nil {
			log.Println("Invalid product ID format:", err)
			c.AbortWithStatus(http.StatusBadRequest)
			return
		}

		// 设置一个 5 秒的上下文超时时间
		var ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		// 调用数据库方法移除购物车商品
		err = database.RemoveCartItem(ctx, app.prodCollection, app.userCollection, ProductID, userQueryID)
		if err != nil {
			log.Println("Error removing cart item:", err)
			c.IndentedJSON(http.StatusInternalServerError, err.Error())
			return
		}

		// 成功移除商品,返回 200 状态和成功消息
		c.IndentedJSON(http.StatusOK, "Successfully removed the item")
	}
}
  • 用户购物车 :
// GetItemFromCart 处理从购物车获取用户信息的请求
func GetItemFromCart() gin.HandlerFunc {

	return func(c *gin.Context) {
		// 从查询参数中获取用户 ID
		user_id := c.Query("id")

		// 如果没有提供用户 ID,返回 404 错误
		if user_id == "" {
			c.JSON(http.StatusNotFound, gin.H{"error": "invalid id"})
			c.Abort()
			return
		}

		// 将用户 ID 从字符串转换为 MongoDB 的 ObjectID 类型
		usert_id, _ := primitive.ObjectIDFromHex(user_id)

		// 创建一个带有超时的上下文,用于数据库操作
		var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
		defer cancel()

		// 定义一个结构体来保存从数据库查询到的用户信息
		var filledcart models.User
		// 根据用户 ID 从数据库中查找用户
		err := UserCollection.FindOne(ctx, bson.D{primitive.E{Key: "_id", Value: usert_id}}).Decode(&filledcart)

		// 如果查询失败,返回 500 错误并记录日志
		if err != nil {
			log.Println(err)
			c.IndentedJSON(500, "not found")
			return
		}

		// 执行 MongoDB 聚合查询
		// 1. $match 阶段:匹配指定的用户 ID
		filter_match := bson.D{{Key: "$match", Value: bson.D{primitive.E{Key: "_id", Value: usert_id}}}}
		// 2. $unwind 阶段:将用户购物车数组拆分成多个文档
		unwind := bson.D{{Key: "$unwind", Value: bson.D{primitive.E{Key: "path", Value: "$usercart"}}}}
		// 3. $group 阶段:按用户 ID 分组,并计算购物车中商品价格的总和
		grouping := bson.D{{Key: "$group", Value: bson.D{primitive.E{Key: "_id", Value: "$_id"}, {Key: "total", Value: bson.D{primitive.E{Key: "$sum", Value: "$usercart.price"}}}}}}
		// 执行聚合查询
		pointcursor, err := UserCollection.Aggregate(ctx, mongo.Pipeline{filter_match, unwind, grouping})
		if err != nil {
			log.Println(err)
		}

		// 定义一个切片来保存聚合查询结果
		var listing []bson.M
		// 将聚合查询结果解析到切片中
		if err = pointcursor.All(ctx, &listing); err != nil {
			log.Print(err)
			c.AbortWithStatus(http.StatusInternalServerError)
		}

		// 遍历聚合查询结果并将其发送到客户端
		for _, json := range listing {
			c.IndentedJSON(200, json["total"])
			c.IndentedJSON(200, filledcart.UserCart)
		}

		// 完成上下文
		ctx.Done()
	}
}
  • 购物车内购买商品:
// BuyFromCart 处理从购物车购买商品的请求,返回一个 gin.HandlerFunc。
func (app *Application) BuyFromCart() gin.HandlerFunc {

	return func(c *gin.Context) {
		// 获取查询参数 "id"
		UserQueryID := c.Query("id")

		// 如果用户ID为空,记录日志并返回 400 错误
		if UserQueryID == "" {
			log.Panicln("用户ID为空")
			_ = c.AbortWithError(http.StatusBadRequest, errors.New("UserID 为空"))
			return
		}

		var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
		defer cancel()

		// 调用数据库操作来从购物车中购买商品
		err := database.BuyItemFromCart(ctx, app.userCollection, UserQueryID)
		if err != nil {
			// 如果操作失败,返回 500 错误和具体的错误信息
			c.IndentedJSON(http.StatusInternalServerError, err.Error())
			return
		}

		// 操作成功,返回 200 状态和成功消息
		c.IndentedJSON(http.StatusOK, "成功下单")
	}
}
  • 商品页面立即下单:
// InstantBuy 处理即时购买的请求,返回一个 gin.HandlerFunc。
func (app *Application) InstantBuy() gin.HandlerFunc {

	return func(c *gin.Context) {
		// 获取查询参数 "id" 和 "userID"
		productQueryID := c.Query("id")
		userQueryID := c.Query("userID")

		// 如果产品ID为空,记录日志并返回 400 错误
		if productQueryID == "" {
			log.Println("product id is empty, please provide a valid product id")
			_ = c.AbortWithError(http.StatusBadRequest, errors.New("product id is empty"))
			return
		}

		// 如果用户ID为空,记录日志并返回 400 错误
		if userQueryID == "" {
			log.Println("user id is empty")
			_ = c.AbortWithError(http.StatusBadRequest, errors.New("user id is empty"))
			return
		}

		// 将产品ID从字符串转换为 ObjectID
		ProductID, err := primitive.ObjectIDFromHex(productQueryID)
		if err != nil {
			log.Println("Invalid product ID format:", err)
			c.AbortWithStatus(http.StatusBadRequest)
			return
		}

		// 设置一个 5 秒的上下文超时时间
		var ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		// 调用数据库方法进行即时购买
		err = database.InstantBuyer(ctx, app.prodCollection, app.userCollection, ProductID, userQueryID)
		if err != nil {
			log.Println("Error processing instant buy:", err)
			c.IndentedJSON(http.StatusInternalServerError, err.Error())
			return
		}

		// 成功下单,返回 200 状态和成功消息
		c.IndentedJSON(http.StatusOK, "Successfully placed the order")
	}
}

连接数据库

  1. 实现 mongodb 数据库连接
package database

import (
	"context"
	"log"
	"fmt"
	"time"

	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

func DBSet() *mongo.Client {
	// 创建一个带有 10 秒超时限制的上下文 ctx
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// 使用 mongo.Connect 方法创建并连接到 MongoDB 客户端
	client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
	if err != nil {
		log.Fatal(err)
	}

	// 使用 Ping 方法检查连接是否成功
	err = client.Ping(ctx, nil)
	if err != nil {
		log.Println("failed to connect to mongodb :(", err)
		return nil
	}

	fmt.Println("Successfully connected to mongodb")

	// 连接成功,返回配置完成的 MongoDB 客户端实例
	return client
}

// 调用 DBSet() 函数,获取一个 MongoDB 客户端实例,并将其赋值给全局变量 Client
// Client 可以在程序的其他部分使用,以与 MongoDB 进行交互。
var Client *mongo.Client = DBSet()

func UserData(client *mongo.Client, collectionName string) *mongo.Collection{
	// 从数据库 "Ecommerce" 中获取指定名称的集合
	var collection *mongo.Collection = client.Database("Ecommerce").Collection(collectionName)
	// 返回获取到的集合
	return collection
}

func ProductData(client *mongo.Client, collectionName string) *mongo.Collection{
	// 从数据库 "Ecommerce" 中获取指定名称的集合
	var productCollection *mongo.Collection = client.Database("Ecommerce").Collection(collectionName)
	// 返回获取到的集合
	return productCollection
}

数据库业务逻辑

  1. 实现购物车curd功能:编写 database/cart.go 业务逻辑层
  • 数据库之添加商品至用户购物车:
package database

import (
	"context"
	"errors"
	"log"
	"time"
	"github.com/Done-0/ecommerce-sys/models"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
)

var (
	ErrCantFindProduct = errors.New("can't find the product")  // 表示找不到产品的错误。
	ErrCantDecodeProducts = errors.New("can't find the product")  // 表示解码产品失败的错误
	ErrUserIdIsNotValid = errors.New("this user is not valid")  // 表示用户 ID 无效的错误。
	ErrCantUpdateUser = errors.New("cannot add this product to the cart")  // 表示无法更新用户的错误。
	ErrCantRemoveItemCart = errors.New("cannot remove this item from the cart")  // 表示无法从购物车中移除项的错误。
	ErrCantGetItem = errors.New("was unnable to get the item from the cart")  //表示无法从购物车中获取项的错误。
	ErrCantBuyCartItem = errors.New("cannot update the purchase")  // 表示无法更新购买的错误。
)

// AddProductToCart 将指定产品添加到用户的购物车
func AddProductToCart(ctx context.Context, prodCollection, userCollection *mongo.Collection, productID primitive.ObjectID, userID string) error {
    // 从产品集合中查找指定产品
    searchfromdb, err := prodCollection.Find(ctx, bson.M{"_id": productID})
    if err != nil {
        log.Println(err)
        return ErrCantFindProduct
    }
  
    // 将查找结果解码到productcart中
    var productcart []models.ProductUser
    err = searchfromdb.All(ctx, &productcart)
    if err != nil {
        log.Println(err)
        return ErrCantDecodeProducts
    }

    // 将用户ID转换为ObjectID类型
    id, err := primitive.ObjectIDFromHex(userID)
    if err != nil {
        log.Println(err)
        return ErrUserIdIsNotValid
    }

    // 更新用户购物车,添加产品
    filter := bson.D{primitive.E{Key: "_id", Value: id}}
    update := bson.D{
        {Key: "$push", Value: bson.D{
            primitive.E{Key: "usercart", Value: bson.D{
                primitive.E{Key: "$each", Value: productcart},
            }},
        }},
    }

    // 执行更新操作
    _, err = userCollection.UpdateOne(ctx, filter, update)
    if err != nil {
        return ErrCantUpdateUser
    }
    return nil
}
  • 数据库之从用户购物车中移除指定产品:
// RemoveCartItem 从用户购物车中移除指定产品
func RemoveCartItem(ctx context.Context, prodCollection, userCollection *mongo.Collection, productID primitive.ObjectID, userID string) error {
    // 将用户ID转换为ObjectID类型
    id, err := primitive.ObjectIDFromHex(userID)
    if err != nil {
        log.Println(err)
        return ErrUserIdIsNotValid
    }

    // 定义过滤条件和更新操作
    filter := bson.D{primitive.E{Key: "_id", Value: id}}
    update := bson.M{"$pull": bson.M{"usercart": bson.M{"_id": productID}}}

    // 执行更新操作,移除购物车中的产品
    _, err = userCollection.UpdateMany(ctx, filter, update)
    if err != nil {
        return ErrCantRemoveItemCart
    }
    return nil
}
  • 数据库之处理用户购物车的购买过程:
// BuyItemFromCart 处理用户购物车的购买过程
func BuyItemFromCart(ctx context.Context, userCollection *mongo.Collection, userID string) error {
    // 将用户ID转换为ObjectID类型
    id, err := primitive.ObjectIDFromHex(userID)
    if err != nil {
        log.Println(err)
        return ErrUserIdIsNotValid
    }
  
    // 初始化订单对象
    var getcartitems models.User
    var ordercart models.Order
    ordercart.Order_ID = primitive.NewObjectID() // 生成新的订单ID
    ordercart.Ordered_At = time.Now()            // 订单时间为当前时间
    ordercart.Order_Cart = make([]models.ProductUser, 0) // 初始化购物车中的产品列表
    ordercart.Payment_Method.COD = true           // 设置支付方式为货到付款

    // 聚合操作:计算购物车中所有商品的总金额
    unwind := bson.D{{Key: "$unwind", Value: bson.D{primitive.E{Key: "path", Value: "$usercart"}}}}
    grouping := bson.D{{Key: "$group", Value: bson.D{
        primitive.E{Key: "_id", Value: "$_id"},
        primitive.E{Key: "total", Value: bson.D{primitive.E{Key: "$sum", Value: "$usercart.price"}}},
    }}}
    currentresults, err := userCollection.Aggregate(ctx, mongo.Pipeline{unwind, grouping})
    if err != nil {
        panic(err)
    }
  
    var getusercart []bson.M
    if err := currentresults.All(ctx, &getusercart); err != nil {
        panic(err)
    }
  
    // 计算总价格
    var total_price int32
    for _, user_item := range getusercart {
        price := user_item["total"]
        total_price = price.(int32)
    }
    ordercart.Price = int(total_price)

    // 将订单信息添加到用户的订单列表中
    filter := bson.D{primitive.E{Key: "_id", Value: id}}
    update := bson.D{
        primitive.E{Key: "$push", Value: bson.D{
            primitive.E{Key: "orders", Value: ordercart},
        }},
    }
    _, err = userCollection.UpdateMany(ctx, filter, update)
    if err != nil {
        log.Println(err)
    }

    // 从用户文档中读取购物车内容
    err = userCollection.FindOne(ctx, bson.D{primitive.E{Key: "_id", Value: id}}).Decode(&getcartitems)
    if err != nil {
        log.Println(err)
    }

    // 将购物车中的所有商品添加到订单列表中
    filterNew := bson.D{primitive.E{Key: "_id", Value: id}}
    updateNew := bson.M{"$push": bson.M{"order.$[].order_list": bson.M{"$each": getcartitems.UserCart}}}
    _, err = userCollection.UpdateOne(ctx, filterNew, updateNew)
    if err != nil {
        log.Println(err)
    }

    // 清空用户的购物车
    usercart_empty := make([]models.ProductUser, 0)
    filterNewCart := bson.D{primitive.E{Key: "_id", Value: id}}
    updateNewCart := bson.D{
        primitive.E{Key: "$set", Value: bson.D{
            primitive.E{Key: "usercart", Value: usercart_empty},
        }},
    }
    _, err = userCollection.UpdateOne(ctx, filterNewCart, updateNewCart)
    if err != nil {
        return ErrCantBuyCartItem
    }
    return nil
}
  • 数据库之立即购买:
// InstantBuyer 立即购买
func InstantBuyer(ctx context.Context, prodCollection, userCollection *mongo.Collection, productID primitive.ObjectID, userID string) error {
    // 将用户ID从十六进制字符串转换为 ObjectID 类型
	id, err := primitive.ObjectIDFromHex(userID)
	if err != nil {
		log.Println(err)
		return ErrUserIdIsNotValid
	}

	var product_details models.ProductUser
	var orders_detail models.Order

	// 创建一个新的订单
	orders_detail.Order_ID = primitive.NewObjectID()
	orders_detail.Ordered_At = time.Now()
	orders_detail.Order_Cart = make([]models.ProductUser, 0)
	orders_detail.Payment_Method.COD = true

    // 从产品集合中获取产品详细信息
	err = prodCollection.FindOne(ctx, bson.D{primitive.E{Key:"_id", Value:productID}}).Decode(&product_details)
	if err != nil {
		log.Println(err)
	}
	orders_detail.Price = int(*product_details.Price)

    // 更新用户集合,将新订单添加到用户的订单列表中
	filter := bson.D{primitive.E{Key:"_id", Value:id}}
	update := bson.D{{Key:"$push", Value:bson.D{primitive.E{Key:"orders", Value:orders_detail}}}}
	userCollection.UpdateOne(ctx, filter, update)

    // 更新用户集合,将产品详细信息添加到订单的产品列表中
	filter2 := bson.D{primitive.E{Key:"_id", Value:id}}
	update2 := bson.M{"$push":bson.M{"order.$[].order_list": product_details}}

	_, err = userCollection.UpdateOne(ctx, filter2, update2)
	if err != nil {
		log.Println(err)
	}
	return nil
}

实现中间件鉴权

  1. 实现身份验证中间件:

打开 middleware/middleware.go 文件

package middleware

import (
	"net/http"

	token "github.com/Done-0/ecommerce-sys/tokens"
	"github.com/gin-gonic/gin"
)

func Authentication() gin.HandlerFunc {
	return func(c *gin.Context) {
		ClientToken := c.Request.Header.Get("token")
		if ClientToken == "" {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "No authorization header founded"})
			c.Abort()
			return
		}
		claims, err := token.ValidateToken(ClientToken)
		if err != "" {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err})
			c.Abort()
			return
		}

		c.Set("email", claims.Email)
		c.Set("uid", claims.Uid)
		c.Next()
	}
}

实现 JWTToken

  1. 实现 JWTToken
  • JWTToken 生成参照
package tokens

import (
	"context"
	"log"
	"os"
	"time"

	"github.com/Done-0/ecommerce-sys/database"
	jwt "github.com/dgrijalva/jwt-go"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

// SignedDetails 包含 JWT 令牌中的用户信息和标准声明
type SignedDetails struct {
	Email string
	Name  string
	Uid   string
	jwt.StandardClaims
}
  • JWTToken 生成
// UserData 是存储用户数据的 MongoDB 集合引用
var UserData *mongo.Collection = database.UserData(database.Client, "Users")

// SECRET_KEY 用于 JWT 签名和验证,从环境变量中读取
var SECRET_KEY = os.Getenv("SECRET_KEY")


// // TokenGenerator 生成一个签名的访问令牌和一个签名的刷新令牌。
func TokenGenerator(email string, name string, uid string) (signedtoken string, signedrefreshtoken string, err error) {
	// 创建一个包含用户信息和过期时间的声明
	claims := &SignedDetails {
		Email: email,
		Name: name,
		Uid: uid,
		StandardClaims: jwt.StandardClaims {
			ExpiresAt: time.Now().Local().Add(time.Hour * time.Duration(24)).Unix(), // 令牌有效期为24小时
		},
	}

	// 创建一个仅包含过期时间的声明,用于刷新令牌
	refreshclaims := &SignedDetails{
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: time.Now().Local().Add(time.Hour * time.Duration(24*7)).Unix(), // 刷新令牌有效期为7天
		},
	}

	// 使用HS256算法签名访问令牌
	token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(SECRET_KEY))
	if err != nil {
		return "", "", err // 返回错误信息
	}

	// 使用HS384算法签名刷新令牌
	refreshtoken, err := jwt.NewWithClaims(jwt.SigningMethodHS384, refreshclaims).SignedString([]byte(SECRET_KEY))
	if err != nil {
		log.Panic(err) // 记录错误并引发恐慌
		return
	}

	// 返回生成的访问令牌、刷新令牌及任何错误信息
	return token, refreshtoken, err
}
  • JWTToken 校验
// ValidateToken 验证给定的签名令牌是否有效,并返回其声明。
func ValidateToken(signedtoken string) (claims *SignedDetails, msg string) {
	// 解析并验证签名令牌,使用提供的密钥和声明类型
	token, err := jwt.ParseWithClaims(signedtoken, &SignedDetails{}, func(token *jwt.Token) (interface{}, error) {
		return []byte(SECRET_KEY), nil // 使用SECRET_KEY作为签名密钥
	})
	if err != nil {
		msg = err.Error() // 如果解析过程中出现错误,设置错误信息并返回
		return
	}

	// 断言token.Claims为*SignedDetails类型,并进行类型检查
	claims, ok := token.Claims.(*SignedDetails)
	if !ok {
		msg = "Invalid token" // 如果断言失败,说明令牌无效,设置错误信息并返回
		return
	}

	// 检查令牌的过期时间
	if claims.ExpiresAt < time.Now().Local().Unix() {
		msg = "Token expired" // 如果令牌已过期,设置错误信息并返回
		return
	}

	// 如果所有检查都通过,返回令牌中的声明和一个空消息
	return claims, ""
}
  • JWTToken 刷新
// UpdateAllTokens 更新用户的访问令牌和刷新令牌,并记录更新时间。
func UpdateAllTokens(signedtoken string, signedrefreshtoken string, userid string) {

	// 创建一个带有超时的上下文,超时时间为100秒
	var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
	defer cancel() // 确保函数返回时取消上下文

	var updateobj primitive.D

	// 构建更新对象,包括访问令牌、刷新令牌和更新时间
	updateobj = append(updateobj, bson.E{Key: "token", Value: signedtoken})
	updateobj = append(updateobj, bson.E{Key: "refresh_token", Value: signedrefreshtoken})
	updated_at, _ := time.Parse(time.RFC3339, time.Now().Format(time.RFC3339)) // 格式化当前时间为RFC3339格式

	updateobj = append(updateobj, bson.E{Key: "updated_at", Value: updated_at})

	// 设置Upsert选项,表示如果用户不存在则插入新记录
	upsert := true
	filter := bson.M{"user_id": userid} // 设置过滤条件,匹配指定的用户ID
	opt := options.UpdateOptions{
		Upsert: &upsert,
	}

	// 执行更新操作,将更新对象应用到符合过滤条件的文档中
	_, err := UserData.UpdateOne(ctx, filter, bson.D{
		{Key: "$set", Value: updateobj},
	}, &opt)

	// 处理更新操作中的错误
	if err != nil {
		log.Panic(err) // 记录错误并引发恐慌
		return
	}
}

docker-compose 示范

  1. docker-compose 示范
version: '3.8'

services:
  mongo:
    platform: linux/amd64  # 如果你的系统是 AMD64 架构
    image: mongo:latest
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: development
      MONGO_INITDB_ROOT_PASSWORD: testpassword

  mongo-express:
    platform: linux/amd64  # 如果你的系统是 AMD64 架构
    image: mongo-express:latest
    ports:
      - "8081:8081"
    environment:
      ME_CONFIG_MONGO_INITDB_ROOT_USERNAME: development
      ME_CONFIG_MONGO_INITDB_ROOT_PASSWORD: testpassword
      ME_CONFIG_MONGODB_URL: mongodb://development:testpassword@mongo:27017/

完结撒花