Contents

Study Golang「3」

study golang / demo5

后端:mongodb+gorm+MVC+gin+air 前端:vue+vite+ts 仓库:https://github.com/LTX-GOD/study-golang-demo

项目架构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
demo5/
├── main.go
├── .air.toml
├── ecommerce-sys
├── go.mod
├── go.sum
├── controllers/
│   ├── address.go
│   ├── cart.go
│   └── controllers.go
├── database/
│   ├── cart.go
│   └── databasesetup.go
├── middleware/
│   └── middleware.go
├── models/
│   └── models.go
├── routes/
│   └── routes.go
├── tokens/
│   └── tokengen.go
├── tmp/
│   ├── build-errors.log
│   └── main
└── static/
    ├── form.html
    └── index.html

关于Air包

1
2
3
go install github.com/air-verse/air@latest //拉包
air init //初始化
air //启动热重载

关于mongodb

这里本地包docker上去的

1
2
docker exec -it mongodb sh
use gotest

关于后端项目

router & main

路由文件中我只存了关于用户的,其他的存在了main.go里面,原因是这个练手项目的作者第一开始没写完,我后面自己补完的,包括前端的内容。

然后在main里面,我没有把端口写死,选择环境变量注入的方法

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

这样的好处是包docker的时候灵活一点

因为项目是前后端分离,加上前端后还需要解决跨域问题

1
2
3
4
5
6
7
	// 配置CORS
	config := cors.DefaultConfig()
	config.AllowOrigins = []string{"http://localhost:5173", "http://localhost:3000", "http://localhost:8080"}
	config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
	config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With", "token"}
	config.AllowCredentials = true
	router.Use(cors.New(config))

models

这里定义的有点多

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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 `json:"_id" bson:"_id"`
	Product_Name *string            `json:"product_name"`
	Price        *string            `json:"price"`
	Rating       *string            `json:"rating"`
	Image        *string            `json:"image"`
}

type ProductUser struct {
	Product_ID   primitive.ObjectID `json:"_id" bson:"_id"`
	Product_Name *string            `json:"product_name"`
	Price        *string            `json:"price"`
	Rating       *string            `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

加入首字母小写,就类似private,在外部无权限访问

结构体中jsonbson的不同

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

即使定义的是Name,在json输出中也会被序列化成name

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

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

database

这里分成两个文件进行编写,分别是databasesetup.gocart.go

  • databasesetup.go 主要用来处理数据库连接还有获取用户和产品的集合,稍微多加的一点就是写了个连接数据库时的超时限制,其他的都是很简单的内容
1
2
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
  • cart.go 这里先定义了一些报错,然后写业务逻辑 主要任务有:
    1. 将指定产品添加到用户的购物车
    2. 从用户购物车中移除指定产品
    3. 处理用户购物车的购买过程
    4. 立即购买 代码太长就不放了bro

controllers

这里写的也比较乱

  • controllers.go:处理密码哈希、注册、密码校验、登录、添加商品、购物车逻辑(增、删、查、购买、下单)
  • cart.go:提供接口处理功能,比如加购物车、移除商品、查看购物车、下单等,也就是main.go哪里的api接口
  • addre.go:提供用户地址接口,实现增加、编辑、删除的功能

代码很多,就不放了,部分还不是特别完善,像后面两个都是自己实现的,比较潦草

middleware

这里主要用来实现中间件鉴权

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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()
	}
}

tokens

  • 生成jwttoken
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
type SignedDetails struct {
	Email string
	Name  string
	Uid   string
	jwt.StandardClaims
}

// UserData 是存储用户数据的 MongoDB 集合引用
var UserData *mongo.Collection = database.UserData(database.Client, "Users")

// 从环境变量中读取JWT的签名和认证
var SECRET_KEY = os.Getenv("SECRET_KEY")

// TokenGenerator 生成一个签名的访问令牌和一个签名的刷新令牌。
func TokenGenerator(email string, name string, uid string) (signedtoken string, signedrefeshtoken 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(), //刷新有效七天
		},
	}

	//HS256访问令牌
	token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(SECRET_KEY))
	if err != nil {
		return "", "", err
	}

	//刷新
	refreshtoken, err := jwt.NewWithClaims(jwt.SigningMethodHS384, refreshclaims).SignedString([]byte(SECRET_KEY))
	if err != nil {
		log.Panic(err)
		return
	}

	return token, refreshtoken, err
}
  • 实现校验功能
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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, ""
}
  • 实现刷新功能
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
	}
}