9-RESTful设计

思考并回答以下问题:

实验介绍

在本次实验中,我们将学习如何在Gin框架中设计RESTful API,这有助于我们更灵活有效的操作资源,以方便在大型网络应用中扩展业务逻辑。RESTful设计风格已经成为Web应用程序接口的主流设计方式之一,广泛应用于互联网行业。本实验将帮助您更好的掌握如何在Gin框架中设计RESTful API。

知识点

  • RESTful概念
  • Gin实现RESTful

RESTful概念

RESTful的英文全称是”Representational State Transfer”,简称REST,是一种基于HTTP协议的网络应用程序接口设计风格。它利用HTTP协议的GET、POST、PUT、DELETE等方法来实现客户端和服务器之间的数据交换和互操作性。RESTful的核心思想是资源(Resource)和操作(Method),即将每个资源(如文章、用户)抽象成一个URI(统一资源标识符),然后使用HTTP方法对其进行操作。比如,使用GET方法获取某个资源的信息,使用POST方法创建新的资源,使用PUT方法更新已有资源,使用DELETE方法删除某个资源。

RESTful设计风格的优点在于它具有简单、灵活、可扩展、易于维护等特点。同时,由于它使用标准的HTTP方法和状态码,使得客户端和服务器之间的交互更加明确和易于理解。因此,RESTful设计风格已经成为Web应用程序接口的主流设计方式之一,广泛应用于互联网行业。

我们以一个电商网站为例,通常会有产品(Product)、订单(Order)、用户(User)等概念,或资源。这些概念都可以通过接口暴露出来供客户端进行业务操作。一个典型的业务流程就是用户查找产品,然后下订单,订单派发完结。这个流程将涉及产品、订单、用户三个模型,而且还有所关联。在这个网络应用中,最好的方式,就是用RESTful API设计风格,将产品、订单、用户三种资源区分开来。

对于每个概念来说,我们一般需要对其增删改查,这就是比较基础的接口,每个模型需要5个。增删改查4个操作,为什么需要5个基础接口呢?这是因为对于查询我们既需要查询单个资源,还需要查询资源列表。下面是产品的基础接口。

HTTP请求方法
HTTP请求路径
描述
GET /products 产品列表查询
GET /products/:id 单个产品查询
POST /products 新增产品
PUT /products/:id 更改产品
DELETE /products/:id 删除产品

如上表,我们已经利用HTTP请求方法区别了增删改查4种操作,HTTP请求路径对应相关需要操作的资源。

HTTP请求方法通常对应以下操作。

  • GET:获取资源的信息,可以理解为读取操作。
  • POST:创建新的资源,可以理解为写入操作。
  • PUT:更新已有的资源,可以理解为修改操作。
  • DELETE:删除指定的资源,可以理解为删除操作。
  • PATCH:更新部分资源,可以理解为修改操作的一种扩展方式。

在这里,PUT请求方法用于更新整个资源,而PATCH请求方法则用于更新部分资源,即客户端只需要提交需要更新的属性及其新值,服务器会将其更新到对应的资源上,不影响其他属性的值。

Gin实现RESTful

实际上,我们已经在上一个实验数据库集成中实现了用户和产品的RESTful API。现在我们将在此基础上加入订单的RESTful API。

首先,为了保持一致,我们需要将产品接口改为MongoDB。编辑routes/products.go,代码如下。

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
package routes

import (
"context"
"gin-course/db"
"gin-course/models"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"net/http"
)

var productCollection *mongo.Collection

func init() {
productCollection = db.MongoClient.Database("test").Collection("products")
}

// RegisterProducts 注册路由
func RegisterProducts(app *gin.Engine) {
app.GET("/products", getProducts)
app.GET("/products/:id", getProduct)
app.POST("/products", postProduct)
app.PUT("/products/:id", putProduct)
app.DELETE("/products/:id", deleteProduct)
app.POST("/products/:id/orders", postSubmitProductOrder)
}

// 获取产品列表
func getProducts(c *gin.Context) {
// 查询所有产品
cur, err := productCollection.Find(context.Background(), bson.D{})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cur.Close(context.Background())

// 将查询结果转为切片
var products []models.Product
err = cur.All(context.Background(), &products)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// 返回查询结果
c.JSON(http.StatusOK, products)
}

// 获取产品
func getProduct(c *gin.Context) {
// 从 URL 参数中获取产品 ID
id, err := primitive.ObjectIDFromHex(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid product ID"})
return
}

// 查询产品
var product models.Product
err = productCollection.FindOne(context.Background(), bson.M{"_id": id}).Decode(&product)
if err != nil {
if err == mongo.ErrNoDocuments {
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// 返回查询结果
c.JSON(http.StatusOK, product)
}

// 新增产品
func postProduct(c *gin.Context) {
// 解析请求体中的 JSON 数据
var product models.Product
err := c.BindJSON(&product)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// 插入产品
product.Id = primitive.NewObjectID()
res, err := productCollection.InsertOne(context.Background(), &product)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// 返回插入结果
c.JSON(http.StatusCreated, res.InsertedID)
}

// 修改产品
func putProduct(c *gin.Context) {
// 获取产品 ID
id, err := primitive.ObjectIDFromHex(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid product ID"})
return
}

// 构造查询条件
filter := bson.M{"_id": id}

// 构造更新文档
var product models.Product
if err := c.ShouldBindJSON(&product); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
update := bson.M{
"$set": bson.M{
"name": product.Name,
"price": product.Price,
},
}

// 执行更新操作
result, err := productCollection.UpdateOne(context.Background(), filter, update)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if result.ModifiedCount == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
return
}

c.JSON(http.StatusOK, gin.H{"message": "product updated"})
}

// 删除产品
func deleteProduct(c *gin.Context) {
// 获取产品 ID
id, err := primitive.ObjectIDFromHex(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid product ID"})
return
}

// 构造查询条件
filter := bson.M{"_id": id}

// 执行删除操作
result, err := productCollection.DeleteOne(context.Background(), filter)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if result.DeletedCount == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
return
}

c.JSON(http.StatusOK, gin.H{"message": "product deleted"})
}

你可以按照上一个实验中数据库集成中的测试方式,对产品的增删改查基础接口进行测试。

现在,我们需要添加对产品提交订单、取消订单,以及订单的查询接口。

我们添加订单模型,在models目录下新增文件order.go,输入以下内容。

1
2
3
4
5
6
7
8
9
10
11
12
package models

import "go.mongodb.org/mongo-driver/bson/primitive"

type Order struct {
Id primitive.ObjectID `json:"id" bson:"_id"`
ProductId primitive.ObjectID `json:"product_id" bson:"product_id"`
Price float32 `json:"price" bson:"price"`
Units int `json:"num" bson:"num"`
Amount float32 `json:"amount" bson:"amount"`
Status string `json:"status" bson:"status"`
}

然后,我们在routes/products.go添加以下方法。
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
// 提交产品订单
func postSubmitProductOrder(c *gin.Context) {
// 获取产品 ID
id, err := primitive.ObjectIDFromHex(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid product ID"})
return
}

// 查询产品
var product models.Product
err = productCollection.FindOne(context.Background(), bson.M{"_id": id}).Decode(&product)
if err != nil {
if err == mongo.ErrNoDocuments {
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// 解析请求体中的 JSON 数据
var order models.Order
err = c.BindJSON(&order)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
order.ProductId = id
order.Price = product.Price
order.Amount = float32(order.Units) * product.Price
order.Status = "success"

// 插入订单
order.Id = primitive.NewObjectID()
res, err := orderCollection.InsertOne(context.Background(), &order)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// 返回插入结果
c.JSON(http.StatusCreated, res.InsertedID)
}

没错,这就是针对某个产品下单的接口。

现在,我们需要实现查询订单和取消订单的接口。创建一个新文件routes/orders.go,输入以下内容。

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package routes

import (
"context"
"gin-course/db"
"gin-course/models"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"net/http"
)

var orderCollection *mongo.Collection

func init() {
orderCollection = db.MongoClient.Database("test").Collection("orders")
}

// RegisterOrders 注册路由
func RegisterOrders(app *gin.Engine) {
app.GET("/orders", getOrders)
app.GET("/orders/:id", getOrder)
app.POST("/orders/:id/cancel", postCancelOrder)
}

// 获取订单列表
func getOrders(c *gin.Context) {
// 查询所有订单
cur, err := orderCollection.Find(context.Background(), bson.D{})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cur.Close(context.Background())

// 将查询结果转为切片
var orders []models.Order
err = cur.All(context.Background(), &orders)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// 返回查询结果
c.JSON(http.StatusOK, orders)
}

// 获取订单
func getOrder(c *gin.Context) {
// 从 URL 参数中获取订单 ID
id, err := primitive.ObjectIDFromHex(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid order ID"})
return
}

// 查询订单
var order models.Order
err = orderCollection.FindOne(context.Background(), bson.M{"_id": id}).Decode(&order)
if err != nil {
if err == mongo.ErrNoDocuments {
c.JSON(http.StatusNotFound, gin.H{"error": "order not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// 返回查询结果
c.JSON(http.StatusOK, order)
}

// 取消订单
func postCancelOrder(c *gin.Context) {
// 从 URL 参数中获取订单 ID
id, err := primitive.ObjectIDFromHex(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid order ID"})
return
}

// 查询订单
var order models.Order
err = orderCollection.FindOne(context.Background(), bson.M{"_id": id}).Decode(&order)
if err != nil {
if err == mongo.ErrNoDocuments {
c.JSON(http.StatusNotFound, gin.H{"error": "order not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// 取消订单
order.Status = "canceled"
_, err = orderCollection.UpdateOne(context.Background(), bson.M{"_id": id}, bson.D{{"$set", order}})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

// 返回查询结果
c.JSON(http.StatusOK, order)
}

还有别忘了在main.go中注册路由。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"gin-course/routes"
"github.com/gin-gonic/gin"
)

func main() {
// 创建 Gin 引擎
app := gin.New()

// 加载 templates 目录下的所有模板文件
app.LoadHTMLGlob("templates/*")

// 注册路由 users
routes.RegisterUsers(app)
routes.RegisterProducts(app)
routes.RegisterOrders(app)

// 运行 Gin 引擎
app.Run(":8080")
}

现在我们可以开始进行下订单的业务流程了。我们首先需要生成一个产品,再用这个产品来完成下单、取消操作。

跟之前一样的,用ThunderClient插件,创建一个新的HTTP POST请求,URL为http://localhost:8080/products,Body为{"name":"TestProduct","price":8.8},新建一个产品,得到产品ID。

然后,我们再来下单,创建另一个HTTP POST请求,URL为http://localhost:8080/products/642038e3ee094972c2b48902/orders(这里642038e3ee094972c2b48902为新创建的产品ID),Body为{"units":1},然后Send,会得到订单ID。

接下来,我们用/orders来查询订单,可以得到如下结果。

最后,你可以调用/orders/:id/cancel这个接口来取消订单,然后查看状态是否改变。

实验总结

本次实验主要讲解了Gin框架中的RESTful接口设计,以及如何实现一个简单的电商订单系统。在Gin框架中,我们也学习了数据库集成,这也是RESTful设计的基础。有了RESTful接口风格设计的概念,我们能够更灵活、高效的开发业务逻辑相关的接口。

0%