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)
	}
	// 返回密码哈
上一篇:Linux网络基础:HTTPS 网络传输协议


下一篇:【前端安全】burpsuite前端jsEncrypter插件详解