Go + gin 极速搭建 EcommerceSys 电商系统
Go 进阶:Go + gin 极速搭建EcommerceSys电商系统
前言
本章节适合有一定基础的 Golang 初学者,通过简单的项目实践来加深对 Golang 的基本语法和 Web 开发的理解。
项目结构
项目流程图
- 技术栈
- 项目结构
- 项目路由
- 项目模型
项目初始化
- 初始化项目文件夹
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.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)
}
// 返回密码哈希的字符串形式
return string(bytes)
}
- 实现注册功能
// 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, "成功注册!")
}
}
- 密码校验器
// 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
}
- 实现登录功能
// 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,
})
}
}
实现购物车接口
- 管理员添加商品
// 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, "成功添加产品")
}
}
- 实现购物车功能:编写
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")
}
}
连接数据库
- 实现
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
}
数据库业务逻辑
- 实现购物车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
}
实现中间件鉴权
- 实现身份验证中间件:
打开
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
- 实现
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
示范
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/
完结撒花
- 感谢你赐予我前进的力量