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,在外部无权限访问
结构体中json
和bson
的不同
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.go
和cart.go
- databasesetup.go
主要用来处理数据库连接还有获取用户和产品的集合,稍微多加的一点就是写了个连接数据库时的超时限制,其他的都是很简单的内容
1
2
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
|
- cart.go
这里先定义了一些报错,然后写业务逻辑
主要任务有:
- 将指定产品添加到用户的购物车
- 从用户购物车中移除指定产品
- 处理用户购物车的购买过程
- 立即购买
代码太长就不放了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
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
}
}
|