在这个示例中将开发一个项目,该项目主要是用于属性图书管理系统,以及项目的开发流程等操作
图书管理服务:
- 用户服务:登录,注册
- 书籍服务:对书籍的增删改查的操作
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)