Go 进阶:Go + gin 极速搭建 EcommerceSys 电商系统
前言
本章节适合有一定基础的 Golang 初学者,通过简单的项目实践来加深对 Golang 的基本语法和 Web 开发的理解。
具体请联系作者
项目结构
项目流程图
-
技术栈
-
项目结构
-
项目路由
4. 项目模型
项目初始化
- 初始化项目文件夹
md ecommerce-sys
- 初始化
mod
文件
cd ecommerce-sys
go mod init github.com/your_username/ecommerce-sys
注意,此处的
your_username
请替换为你的 GitHub 用户名本项目中,将会使用自己的 GitHub 用户名,请自行修改
- 检查
go.mod
文件是否创建成功并启动VS Code
dir # linux 下使用 ls 命令
code .
- 创建
ecommerce-sys
数据库
打开
MongoDB
,输入以下命令创建ecommerce-sys
数据库:
use database_name
其中,
database_name
请替换为你自己喜欢的数据库名称。
- 初始化项目结构
一行代码在项目根目录下创建目录和空文件
# 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
- 安装
gin
包 和Air
包
go get -u github.com/gin-gonic/gin
go install github.com/air-verse/air@latest
- 配置
Air
热重载
将具有默认设置的 .air.toml
配置文件初始化到当前目录
air init
Air 配置教程:如果有特殊需要请自行参考
如果以上都正常,您只需执行 air
命令,就能使用 .air.toml
文件中的配置热重载你的项目了。
air
搭建项目骨架
- 编写
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 查询商品
}
- 编写
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))
}
- 编写
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 // 未导出字段,只能在包内访问 }
知识小课堂:结构体标签中的
json
和bson
有什么不同?
在 Go 语言的结构体定义中,标签(tag)用于指示序列化库如何处理字段。常见的标签包括json
和bson
:
json
标签:用于指定当结构体字段被序列化为 JSON 时,使用的字段名。例如:type User struct { Name string `json:"name"` }
在这个例子中,即使
Name
在Go
代码中是大写的,在 JSON 输出中,它将会被序列化为小写的"name"
键。
- bson 标签:用于指定当结构体字段被序列化为 BSON(MongoDB 的文档格式)时,使用的字段名。例如:
type User struct { ID primitive.ObjectID `bson:"_id"` }
在这个例子中,
ID
字段会被映射到MongoDB
文档的_id
字段,这是MongoDB
中常用的主键字段名。标签中的
_
和不同点bson:"_id"
标签:_id
是MongoDB
的标准字段名,表示文档的唯一标识符。Go
语言中的字段名可以不同,但通过bson
标签,你可以将其映射到 MongoDB 的_id
字段。type User struct { UserID primitive.ObjectID `bson:"_id"` }
这里,
UserID
字段会被存储为MongoDB
中的_id
字段。
使用标签的好处:通过json
和bson
标签,你可以将Go
结构体字段名与JSON
或BSON
中的字段名分开管理,这在处理不同的命名约定时非常有用。标签也可以控制序列化和反序列化时的行为,比如忽略某些字段或者使用自定义名称。
- 搭建
controllers
控制器骨架
- 首先,搭建
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 {
}
- 其次,搭建
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 {
}
- 最后,搭建
controllers/address.go
业务逻辑层骨架
package controllers
import (
)
func AddAdress() gin.HandlerFunc {
}
func EditHomeAddress() gin.HandlerFunc {
}
func EditWorkAddress() gin.HandlerFunc {
}
func DeleteAddress() gin.HandlerFunc {
}
- 配置
database
数据库
- 首先,搭建
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() {
}
- 其次,搭建
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
}
编写业务逻辑
实现登录注册接口
- 编写
controllers/controllers.go
业务逻辑层
- 密码哈希处理
// HashPassword 接受一个明文密码,并返回其加密后的哈希值。
func HashPassword(password string) string {
// 生成密码哈希
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
if err != nil {
// 如果生成哈希过程中发生错误,则记录错误并引发Panic
log.Panic(err)
}
// 返回密码哈