gin 框架:4 图书管理系统

在这个示例中将开发一个项目,该项目主要是用于属性图书管理系统,以及项目的开发流程等操作

图书管理服务:

  • 用户服务:登录,注册
  • 书籍服务:对书籍的增删改查的操作

1 初始化项目环境

1 项目结构:

├── Readme.md   // 项目说明(帮助你快速的属性和了解项目) 
├── config      // 配置文件(mysql配置 ip 端口 用户名 密码,不能写死到代码中) 
├── controller  // CLD:服务入口,负责处理路由、参数校验、请求转发 
├── service     // CLD:逻辑(服务)层,负责业务逻辑处理,用于验证用户名密码是否正确等这种操作
├── dao         // CLD:负责数据与存储相关功能(mysql、redis、ES等) 
│   ├── mysql
├── model       // 模型(定义表结构) 
├── logging     // 日志处理 
├── main.go     // 项目启动入口 
├── middleware  // 中间件 
├── pkg         // 公共服务(所有模块都能访问的服务) 
├── router      // 路由(路由分发) 

2 创建数据库

mysql>  create database books charset utf8;

2 代码实现

项目完整地址:https://github.com/As9530272755/bookManager

2.1 添加路由层

2.1.1 router/init_router.go

1 创建 router 文件,并在该文件中定义路由分成,从而避免在 main 函数中代码太过臃肿

$ mkdir router
$ touch init_router.go

2 编写 init_router.go 文件,用于实例化引擎

package router

import "github.com/gin-gonic/gin"

/*
加载其他路由文件中的路由
*/

// 实例化并返回引擎
func Init_router() *gin.Engine {
    r := gin.Default()
    return r
}

2.1.2 router/test_router.go

用于测试

1 创建文件

$ touch router/test_router.go

2 编写 test_router.go 文件,该文件用来测试我们的业务逻辑

package router

import "github.com/gin-gonic/gin"

// 接收由 init_router 传递的 *gin.Engine 参数,这样的话就可以在 LoadTestRouter 方法中实现对不同 URL 的处理
func LoadTestRouter(r *gin.Engine) {
    r.GET("/test", func(c *gin.Context) {
        c.String(200, "LoadTestRouter")
    })
}

3 但是我们会发现现在这个 LoadTestRouter 方法并不能对外提供访问,因为还没注册,所以还需要修改 init_router 程序,将其注册

package router

import "github.com/gin-gonic/gin"

/*
加载其他路由文件中的路由
*/

// 这个方法作用加载或者初始化其他文件中的路由
func Init_router() *gin.Engine {
    r := gin.Default()

    // 传递 gin.Engine ,实现注册
    LoadTestRouter(r)
    return r
}

2.1.3 main.go

现在我们编写 main 函数,使其运行该程序用于测试是否能够访问 test URL

package main

import (
    "bookManager/router"
)

func main() {
    // 1.将实例化 router 服务的方法拆分到 router 文件下
    r := router.Init_router()

    // 2.启动
    r.Run()
}

postman 访问成功

以上就是简单的路由分层,将不同功能的实例化,交给不同的路由函数来进行处理

2.2 连接数据库

1 创建 dao 文件夹,dao 文件夹用于连接数据库

$ mkdir dao/mysql       # 这里是用于链接 mysql ,当然还可以在 dao 文件下创建其他数据库的文件如 redis、pgsql 等
$ touch dao/mysql/mysql.go

2.2.1 dao/mysql/mysql.go

1 编写 mysql.go

package mysql

import (
    "log"

    // 由于该 package 是 mysql 所以为了避免冲突这里定义别名为 gmysql
    gmysql "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

// 定义全局 DB ,以便其他 package 函数之间调用
var DB *gorm.DB

func InitMysql() {
    dsn := "root:123456@tcp(10.0.0.134:3306)/books?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(gmysql.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Panic("数据库连接失败:", err)
    }

    // 将实例化之后的 db 赋值给 DB
    DB = db
}

2 main 函数调用 InitMysql()

package main

import (
    "bookManager/dao/mysql"
    "bookManager/router"
    "fmt"
)

func main() {
    // 初始化 mysql 数据库
    mysql.InitMysql()
    fmt.Println("测试连接数据", mysql.DB)

    // 1.将实例化 router 服务的方法拆分到 router 文件下
    r := router.Init_router()

    // 2.启动
    r.Run()
}
$ go run main.go 
测试连接数据 &{0xc0005ce120 <nil> 0 0xc0005ac1c0 1}         # 测试成功
...省略...

2.3 定义多对多表结构

上面我们创建链接 mysql,那么接下来就需要定义我的数据结构模型

一个用户可以借多本书,一本书也可以被多个人借用,比如 user、book 等数据信息

一般定义表结构都需要在 model 文件夹中定义

# 创建 model 文件夹
$ mkdir model

# 定义 user 表
$ touch model/user.go

2.3.1 定义表结构

1 编写 user.go

package model

/*
 json:"username":定义 json 反向解析名字
 gorm:"not null":定义字段在数据库中不能为空
 binding:"required":定义用户在请求的时候不能传入空值
*/
// 定义 user 表结构
type User struct {
    Id       int    `json:"id" gorm:"primaryKey"` // 自定义主键
    Username string `json:"username" gorm:"not null" binding:"required"`
    Password string `json:"password" gorm:"not null" binding:"required"`
    Token    string `json:"token"`
}

// 自定义表名,因为默认 gorm 会在我们的表后面添加 s
func (User) TableName() string {
    return "user"
}

2 编写 book.go

package model

// 定义 Book 表结构
type Book struct {
    Id       int64  `json:"id" gorm:"primaryKey"`
    BookName string `json:"bookname" binding:"required"`
    Desc     string `json:"desc" binding:"required"`

    // 与 user 表进行关联,一本书可以被多人借阅
    // gorm:"many2mant :在 gorm 中定义多对多的字段实现自定义映射关系,也就是 book 与 user 表相关联并创建出第三张表 book_user
    Users []User `gorm:"many2many:book_user""` // book_user 表示第三张关联表名为 book_user
}

func (Book) TableName() string {
    return "book"
}

3 编写 user_m2m_book.go 关联表

package model

// 自定义第三张表关联关系
/*
该结构体包含了用户与书籍关系
*/
type BookUser struct {
    // 下面两个元素分别是 User、Book 结构体的主键
    BookID int64 `gorm:"primaryKey"`
    UserID int64 `gorm:"primaryKey"`
}

2.3.2 自动创建

再回到 dao/mysql/mysql.go 文件中,通过 AutoMigrate() 方法实现表的自动创建

1 修改 mysql.go 文件

package mysql

import (
    "bookManager/model"
    "log"

    // 由于该 package 是 mysql 所以为了避免冲突这里定义别名为 gmysql
    gmysql "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

// 定义全局 DB ,以便其他 package 函数之间调用
var DB *gorm.DB

func InitMysql() {
    dsn := "root:123456@tcp(10.0.0.134:3306)/books?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(gmysql.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Panic("数据库连接失败:", err)
    }

    // 将实例化之后的 db 赋值给 DB
    DB = db

    // 创建表
    if err := DB.AutoMigrate(&model.User{}, &model.Book{}); err != nil {
        log.Panic(err)
    }
}

2 执行程序

$ go run main.go

2.3.3 验证数据库表结构

查看数据库

# 查看对应的几张表已经创建完成
mysql> show tables;
+-----------------+
| Tables_in_books |
+-----------------+
| book            |
| book_user       |
| user            |
+-----------------+
3 rows in set (0.00 sec)

# 查看 user 表结构
mysql> desc user;
+----------+------------+------+-----+---------+----------------+
| Field    | Type       | Null | Key | Default | Extra          |
+----------+------------+------+-----+---------+----------------+
| id       | bigint(20) | NO   | PRI | NULL    | auto_increment |
| username | longtext   | NO   |     | NULL    |                |
| password | longtext   | NO   |     | NULL    |                |
| token    | longtext   | YES  |     | NULL    |                |
+----------+------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

# 查看 book 表结构
mysql> desc book;
+-----------+------------+------+-----+---------+----------------+
| Field     | Type       | Null | Key | Default | Extra          |
+-----------+------------+------+-----+---------+----------------+
| id        | bigint(20) | NO   | PRI | NULL    | auto_increment |
| book_name | longtext   | YES  |     | NULL    |                |
| desc      | longtext   | YES  |     | NULL    |                |
+-----------+------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

# 查看 book_user 表结构
mysql> desc book_user;
+---------+------------+------+-----+---------+-------+
| Field   | Type       | Null | Key | Default | Extra |
+---------+------------+------+-----+---------+-------+
| book_id | bigint(20) | NO   | PRI | NULL    |       |
| user_id | bigint(20) | NO   | PRI | NULL    |       |
+---------+------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

可以看到当我们创建 book 表的时候指定了与 user 表进行关联并且第三张关联表名为 book_user ,所以 book_user 表就被自动创建

2.4 用户相关函数

现在实现了对应的链接数据初始化表功能以及测试 http 页面功能,接下来我们开始对登录等功能进行开发

2.4.1 创建用户

2.4.1.1 router/api-router.go

1 编写 api-router

package router

import (
    "bookManager/dao/mysql"
    "bookManager/model"

    "github.com/gin-gonic/gin"
)

func LoadApiRouter(r *gin.Engine) {
    // 写一个处理注册的路由
    r.POST("/register", RegisterHanlder)
}

func RegisterHanlder(c *gin.Context) {
    user := new(model.User)

    // shouldBind 方法参数校验和参数绑定,获取 json 中复杂数据,这里用于用户注册
    if err := c.ShouldBind(user); err != nil {
        c.JSON(400, gin.H{"msg": err.Error()})
        return
    }

    // 创建用户
    mysql.DB.Create(user)

    // 回显
    c.JSON(200, gin.H{"msg": "注册成功"})
}

2 编写 init-router ,将 api-router 路由注册

如果说我只传递 username 字段少传入 password 字段,可以看到直接报错,提示 password 字段为必须传入

此时 postman 传入数据验证,这里我传入 json 格式,然后可以看到注册成功


数据库验证

mysql> select * from user;
+----+----------+----------+-------+
| id | username | password | token |
+----+----------+----------+-------+
|  1 | lisi     | 123456   |       |
+----+----------+----------+-------+
1 rows in set (0.01 sec)

用户注册功能已经验证,但是可以看到当前的 RegisterHanlder() 函数与路由函数都放在一个文件中,这样就显得代码十分冗余,所以我们需要将 RegisterHanlder() 这种处理逻辑的代码块写到 controller 目录中从而实现代码分层

2.4.2 代码分层

2.4.2.1 controller/user.go

1 创建 controller 并创建 user.go 文件

$ mkdir controller
$ cd controller/
controller$ touch user.go

2 将刚才在 api-router.go 中的 RegisterHanlder() 写到 user.go 中,实现代码分层

package controller

import (
    "bookManager/dao/mysql"
    "bookManager/model"

    "github.com/gin-gonic/gin"
)

// 注册功能
func RegisterHanlder(c *gin.Context) {
    user := new(model.User)

    // shouldBind 方法参数校验和参数绑定,获取 json 中复杂数据,这里用于用户注册
    if err := c.ShouldBind(user); err != nil {
        c.JSON(400, gin.H{"msg": err.Error()})
        return
    }

    // 创建用户
    mysql.DB.Create(user)

    // 回显
    c.JSON(200, gin.H{"msg": "注册成功"})
}

3 接在我们开始写登录功能,这里我在新写了一个 LoginHanlder 的函数

package controller

import (
    "bookManager/dao/mysql"
    "bookManager/model"
    "fmt"

    "github.com/google/uuid"

    "github.com/gin-gonic/gin"
)

// 注册功能
func RegisterHanlder(c *gin.Context) {
    user := new(model.User)

    // shouldBind 方法参数校验和参数绑定,获取 json 中复杂数据,这里用于用户注册
    if err := c.ShouldBind(user); err != nil {
        c.JSON(400, gin.H{"msg": err.Error()})
        return
    }

    // 创建用户
    mysql.DB.Create(user)

    // 回显
    c.JSON(200, gin.H{"msg": "注册成功"})
}

// 登录功能
func LoginHanlder(c *gin.Context) {
    // 用于存放用户从 web 页面输入的数据信息
    user := new(model.User)

    // 对用户输入的数据进行校验不能为空,如果是空则表示错误
    if err := c.ShouldBind(user); err != nil {
        c.JSON(400, gin.H{"err": err.Error()})
        return
    }

    // 判断当前输入的用户名和密码是否正确
    u := model.User{Username: user.Username, Password: user.Password}

    // 通过 where().First().Row() ,对 u 这个实例结构体进行查找,如果未能查找到任何数据那么 rows = nil ,就直接响应客户端账号或密码错误
    if rows := mysql.DB.Where(&u).First(&u).Row(); rows == nil {
        c.JSON(403, gin.H{"msg": "用户名或密码错误!"})
        return
    }

    // 随机生成字符串做为 token
    token := uuid.New().String()

    // 将 token 写入到数据库
    mysql.DB.Model(&u).Update("token", token)

    c.JSON(200, gin.H{"msg": "登录成功", "token": token})

}

4 在 api-router.go 中注册 LoginHandler 方法

package router

import (
    "bookManager/controller"

    "github.com/gin-gonic/gin"
)

func LoadApiRouter(r *gin.Engine) {
    // 注册 /register 路由
    r.POST("/register", controller.RegisterHanlder)

    // 注册 login 路由
    r.POST("/login", controller.LoginHanlder)
}

2.4.2.2 验证

输入正确的用户密码登录

输入错误的用户密码

用户相关的开发工作自此就算是开发完毕了,接下来就需要对图书管理相关内容进行开发

2.4.3 查看当前用户信息

2.4.3.1 controller/user.go

在该程序文件中编写下面代码块

// 获取所有用户信息
func ListUserHandler(c *gin.Context) {
    listUser := []model.User{}

    mysql.DB.Find(&listUser)
    c.JSON(200, gin.H{"用户信息": listUser})
}

2.4.3.2 router/api-router.go

注册路由

    r.GET("/listuser", controller.ListUserHandler)

2.4.3.3 验证

2.4.4 删除用户

2.4.3.1 controller/user.go

// 删除用户
func DeleteUserHandler(c *gin.Context) {
    // 获取 url id
    ID := c.Param("id")
    userId, _ := strconv.Atoi(ID)

    // 指定对 user 表中的 id 字段继续删除
    mysql.DB.Where("id = ?", userId).Delete(&model.User{})
    c.JSON(200, gin.H{"msg": "用户删除成功!!"})
}

2.4.3.2 router/api-router.go

注册路由

    r.DELETE("/deluser/:id", controller.DeleteUserHandler)

2.4.3.3 验证

删除 id=1 的书籍

# 第一次查看 id=1 为 lisi
mysql> select * from user;
+----+----------+----------+--------------------------------------+
| id | username | password | token                                |
+----+----------+----------+--------------------------------------+
|  1 | lisi     | 123444   | 9a019f80-b761-4f41-bc7e-ea3cf7e285ca |
|  2 | 张三     | 12345    |                                      |
+----+----------+----------+--------------------------------------+
2 rows in set (0.00 sec)

# 第二次查看 id=1 已被删除
mysql> select * from user;
+----+----------+----------+-------+
| id | username | password | token |
+----+----------+----------+-------+
|  2 | 张三     | 12345    |       |
+----+----------+----------+-------+
1 row in set (0.00 sec)

2.5 图书管理开发

接下来我们还需要在 controller 目录中创建 book.go 然后对图书进行 CURD ,

2.5.1 添加书籍

2.5.1.1 controller/book.go

首先我们先开发如何增加一本书的功能

package controller

import (
    "bookManager/dao/mysql"
    "bookManager/model"

    "github.com/gin-gonic/gin"
)

// 添加书籍
func AddBookHandler(c *gin.Context) {
    pbook := new(model.Book)
    if err := c.ShouldBind(pbook); err != nil {
        c.JSON(400, gin.H{"err msg": err.Error()})
        return
    }

    mysql.DB.Create(pbook)
    c.JSON(200, gin.H{"msg": "创建成功"})
}

2.5.1.2 router/api-router.go

现在我们将 book 路由注册即可

package router

import (
    "bookManager/controller"

    "github.com/gin-gonic/gin"
)

func LoadApiRouter(r *gin.Engine) {
    // 注册 /register 路由
    r.POST("/register", controller.RegisterHanlder)

    // 注册 login 路由
    r.POST("/login", controller.LoginHanlder)

    // 实现版本划分,这样在访问的时候就需要加上 /api/v1 的前缀
    v1 := r.Group("/api/v1")

    // 注册添加数据路由
    v1.POST("/book", controller.AddBookHandler)

}

2.5.1.3 创建book验证

1 未传递任何数据可以看到得到的是一个错误的信息

2 传递数据

3 数据库验证数据

mysql> select * from book;
+----+--------------+--------------------------+
| id | book_name    | desc                     |
+----+--------------+--------------------------+
|  1 | 你侬我侬     | 这是陈奕迅的爱意         |
+----+--------------+--------------------------+
1 row in set (0.00 sec)

2.5.2 列出所有书籍

2.5.2.1 controller/book.go

我们在 controller/book.go 中添加一个列出所有书籍的函数

package controller

import (
    "bookManager/dao/mysql"
    "bookManager/model"

    "github.com/gin-gonic/gin"
)

// 添加书籍
func AddBookHandler(c *gin.Context) {
    pbook := new(model.Book)
    if err := c.ShouldBind(pbook); err != nil {
        c.JSON(400, gin.H{"err msg": err.Error()})
        return
    }

    mysql.DB.Create(pbook)
    c.JSON(200, gin.H{"msg": "创建成功"})
}

// 查看数据列表
func GetBookHandler(c *gin.Context) {
    // 由于查询多本数据通过 [] 查询
    listBook := []model.Book{}
    mysql.DB.Find(&listBook)
    c.JSON(200, gin.H{"books": listBook})
}

2.5.2.2 router/api-router.go

注册列出所有书籍路由

package router

import (
    "bookManager/controller"

    "github.com/gin-gonic/gin"
)

func LoadApiRouter(r *gin.Engine) {
    // 注册 /register 路由
    r.POST("/register", controller.RegisterHanlder)

    // 注册 login 路由
    r.POST("/login", controller.LoginHanlder)

    // 实现版本划分,这样在访问的时候就需要加上 /api/v1 的前缀
    v1 := r.Group("/api/v1")

    // 注册添加数据路由
    v1.POST("/book", controller.AddBookHandler)

    // 注册获取书籍路由
    v1.GET("/book", controller.GetBookHandler)

}

2.5.2.3 验证

访问验证

http://10.0.0.134:8080/api/v1/listbook

2.5.3 查看单条书籍

2.5.3.1 controller/book.go

1 现在接着我们在 book.go 中写入,如下图

package controller

import (
    "bookManager/dao/mysql"
    "bookManager/model"
    "log"
    "strconv"

    "github.com/gin-gonic/gin"
)

// 添加书籍
func AddBookHandler(c *gin.Context) {
    pbook := new(model.Book)
    if err := c.ShouldBind(pbook); err != nil {
        c.JSON(400, gin.H{"err msg": err.Error()})
        return
    }

    mysql.DB.Create(pbook)
    c.JSON(200, gin.H{"msg": "创建成功"})
}

// 查看数据列表
func GetBookHandler(c *gin.Context) {
    // 由于查询多本数据通过 [] 查询
    listBook := []model.Book{}
    mysql.DB.Find(&listBook)
    c.JSON(200, gin.H{"books": listBook})
}

// 查看指定书籍通过 http://10.0.0.134/book/4 这种 URL 获取 id 等于 4 的书籍
func GetBookDetailHandler(c *gin.Context) {
    // 通过 c.Param 获取 ID
    bookIdStr := c.Param("id")

    // 将通过 URL 获取的 id 转换为 int 类型,
    bookIdInt, err := strconv.Atoi(bookIdStr)
    if err != nil {
        log.Panic(err)
    }

    // 将 int 类型强制转换为
    book := model.Book{Id: int64(bookIdInt)}

    if rows := mysql.DB.Where(&book).First(&book).Row(); rows == nil {
        c.JSON(400, gin.H{"msg": "未能查询到该书籍"})
        return
    }

    c.JSON(200, gin.H{"msg": "已经查询到该书籍", "书籍信息": book})
}

2.5.3.2 router/api-router.go

注册路由

package router

import (
    "bookManager/controller"

    "github.com/gin-gonic/gin"
)

func LoadApiRouter(r *gin.Engine) {
    // 注册 /register 路由
    r.POST("/register", controller.RegisterHanlder)

    // 注册 login 路由
    r.POST("/login", controller.LoginHanlder)

    // 实现版本划分,这样在访问的时候就需要加上 /api/v1 的前缀
    v1 := r.Group("/api/v1")

    // 注册添加数据路由
    v1.POST("/book", controller.AddBookHandler)

    // 注册获取书籍路由
    v1.GET("/book", controller.GetBookHandler)

    // 注册查询单本书籍路由 /:id 获取 URL 中的 id 字段
    v1.GET("/book/:id", controller.GetBookDetailHandler)

}

2.5.3.3 访问验证

查询 ID 为 1 的书籍

查询 ID 为 2 的书籍,因为没有该 ID 的书籍所以查询失败

2.5.4 修改书籍信息操作

接下来我们将编写一个修改书籍信息的这么一个动作

2.5.4.1 controller/book.go

接着我们需要在 controller/book.go 中添加新的一个函数从而实现该功能

// 修改书籍
func UpdateBookHandler(c *gin.Context) {
    // 获取 url 的 ID 字段
    Id := c.Param("id")
    // 转换为 int 类型
    oldBookId, _ := strconv.Atoi(Id)

    // 获取用户在 body 中传入的修改书籍信息
    Book := new(model.Book)

    // 校验数据
    if err := c.ShouldBind(Book); err != nil {
        if err := c.ShouldBind(Book); err != nil {
            c.JSON(400, gin.H{"err msg": err.Error()})
            return
        }
    }

    // 通过 id 过滤我们想要更新的书籍内容为用户在 body 字段传入的信息
    mysql.DB.Model(model.Book{Id: int64(oldBookId)}).Updates(&Book)
    c.JSON(200, gin.H{"msg": "更新", "更新后:": Book})
}

2.5.4.2 router/api-router.go

实现注册路由

    // 注册修改书籍路由,这里通过 PUT 方法更新数据
    v1.PUT("/book/:id", controller.UpdateBookHandler)

2.5.4.3 访问验证

在上面添加书籍中我们添加了一本 "你侬我侬" 的书籍

而在这里我将其修改问周杰伦的青花瓷

数据库后台查看已经修改成功

mysql> select * from book;
+----+-----------+--------------+
| id | book_name | desc         |
+----+-----------+--------------+
|  1 | 青花瓷    | 杰伦歌曲     |
+----+-----------+--------------+
1 row in set (0.00 sec)
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇