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

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


前言

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

项目结构

项目流程图

  1. 技术栈
    请添加图片描述

  2. 项目结构
    请添加图片描述

  3. 项目路由

请添加图片描述
4. 项目模型
请添加图片描述

项目初始化

  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/

完结撒花

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fenderisfine

蟹蟹你的奶茶😘

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值