图书管理系统
1. 创建项目
2. 配置goproxy
GOPROXY=https://goproxy.cn
3. 添加格式化工具
4. 定义目录结构
|---- Readme.md //项目说明
|---- config // 配置文件(mysql配置,ip,端口,用户名,密码等)
|---- controller // CLD服务入口,负责处理路由,参数校验,请求转发
|---- service // CLD,逻辑(服务)层,负责业务逻辑处理
|---- dao
|---- mysql
|---- model // 模型(定义表结构)
|---- logging // 日志处理
|---- main.go // 项目启动入口
|---- middleware // 中间件
|---- pkg // 公共服务,所有模块都能访问的服务
|---- router // 路由
4.2 实际目录的结构
— d:\bookmanage
│ go.mod
│ go.sum
│ main.go
│
├─.idea
│ .gitignore
│ bookmanage.iml
│ modules.xml
│ watcherTasks.xml
│ workspace.xml
│
├─controller
│ book.go
│ users.go
│
├─dao
│ └─mysql
│ mysql.go
│
├─model
│ book.go
│ user.go
│ user_m2m_book.go
│
└─router
api_router.go
init_router.go
test_router.go
5. 创建数据库
mysql> create database books charset utf8;
6. 项目要实现的功能
- 图书管理服务
- 用户服务: 登录,注册
- 书籍服务: 对书籍的增删改查
7. main.go
7.1 安装gin
go: D:\bookmanage\go.mod already exists
PS D:\bookmanage> go get github.com/gin-gonic/gin
go: added github.com/gin-contrib/sse v0.1.0
go: added github.com/gin-gonic/gin v1.8.1
go: added github.com/go-playground/locales v0.14.0
go: added github.com/go-playground/universal-translator v0.18.0
go: added github.com/go-playground/validator/v10 v10.10.0
go: added github.com/goccy/go-json v0.9.7
go: added github.com/json-iterator/go v1.1.12
go: added github.com/leodido/go-urn v1.2.1
go: added github.com/mattn/go-isatty v0.0.14
go: added github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421
go: added github.com/modern-go/reflect2 v1.0.2
go: added github.com/pelletier/go-toml/v2 v2.0.1
go: added github.com/ugorji/go/codec v1.2.7
go: added golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
go: added golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
go: added golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069
go: added golang.org/x/text v0.3.6
go: added google.golang.org/protobuf v1.28.0
go: added gopkg.in/yaml.v2 v2.4.0
此时go.mod下确认有此条信息
github.com/gin-gonic/gin v1.8.1 // indirect
7.2 简单的测试下
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
// 1. 初始化
r := gin.Default()
// 2. 定义路由
r.GET("/", func(c *gin.Context) {
c.String(200, "Success")
})
fmt.Println("http://192.168.31.1:8080/")
r.Run(":8080")
}
8. 路由分层
8.1 新建router目录
PS D:\bookmanage> mkdir router
8.2 总的路由入口
router/init_router.go
package router
import "github.com/gin-gonic/gin"
/*
加载其他路由文件中的路由
*/
func InitRouter() *gin.Engine {
r := gin.Default()
return r
}
修改main.go
将
r := gin.Default()
改为
r := router.InitRouter()
8.3 测试路由
router/test_router.go
package router
import "github.com/gin-gonic/gin"
func LoadTestRouter(r *gin.Engine) {
r.GET("/test", func(c *gin.Context) {
c.String(200, "test")
})
}
修改router/init_router.go
package router
import "github.com/gin-gonic/gin"
/*
加载其他路由文件中的路由
*/
func InitRouter() *gin.Engine {
r := gin.Default()
LoadTestRouter(r) // 加入这行,这样就能加载/test了
return r
}
9. 数据库连接
9.1 安装2个Package
PS D:\bookmanage> go get gorm.io/driver/mysql
PS D:\bookmanage> go get gorm.io/gorm
9.2 定义全局数据库连接
9.2.1 数据库连接
dao/mysql/mysql.go
定义全局变量DB为数据库连接
import (
"fmt"
gmysql "gorm.io/driver/mysql"
"gorm.io/gorm"
)
// 全局定义,其他地方可以直接使用
var DB *gorm.DB
// 初始化mysql连接
func InitMysql() {
dsn := "root:123456@tcp(192.168.31.24:3306)/books?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(gmysql.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Printf("数据库连接失败", err)
}
DB = db
}
这样就能在其他地方实现调用
9.2.2 main.go
func main() {
// 初始化mysql
mysql.InitMysql() // <<-----这条就调用了初始化数据库
fmt.Println(mysql.DB, 1111111111)
// 1. 实例化router服务的方法拆分到router.InitRouter
r := router.InitRouter()
// 2. 定义路由
r.GET("/", func(c *gin.Context) {
c.String(200, "Success")
})
fmt.Println("http://192.168.31.1:8080/")
r.Run(":8080")
}
结果: 获取到了数据库连接
&{0xc000130240 <nil> 0 0xc0003b2700 1} 1111111111
9.3 定义表结构
9.3.1 创建model目录
mkdir model
9.3.2 user表
model/user.go
参数 | 含义 |
---|---|
json:“username” | json反向解析名字 |
gorm:not null | 字段在数据库内不能为空 |
binding:“required” | 请求时不能为空,shouldbind参数校验 |
package model
type User struct {
Id int64 `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"`
}
// 表名默认会添加s,自定义表名
func (User) TableName() string {
return "user"
}
9.3.3 book表
package model
type Book struct {
Id int64 `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"not null" binding:"required"`
Desc string `json:"desc"`
Users []User `gorm:"many2many:book_users"`
}
func (Book) TableName() string {
return "book"
}
9.3.4 book_users表
package model
// 用户与书籍关联关系表
type BookUser struct {
UserID int64 `gorm:"primaryKey"`
BookID int64 `gorm:"primaryKey"`
}
9.3.5 InitMysql
dao/mysql/mysql.go
// 初始化mysql连接
func InitMysql() {
dsn := "root:123456@tcp(192.168.31.24:3306)/books?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(gmysql.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Printf("数据库连接失败:", err)
}
DB = db
// <<----------追加以下3行-------------->>
if err:= DB.AutoMigrate(model.User{},model.Book{});err!=nil {
fmt.Printf("数据库创建失败:",err)
}
}
此时重新运行main.go,书库库和表都将被创建
mysql> use books
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+-----------------+
| Tables_in_books |
+-----------------+
| user |
| book |
| book_users |
+-----------------+
3 rows in set (0.00 sec)
mysql> desc user;
+----------+------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+------------+------+-----+---------+----------------+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| username | longtext | YES | | NULL | |
| password | longtext | YES | | NULL | |
| token | longtext | YES | | NULL | |
+----------+------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)
mysql> desc book;
+-------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id | varchar(191) | NO | PRI | NULL | |
| name | longtext | YES | | NULL | |
| desc | longtext | YES | | NULL | |
+-------+--------------+------+-----+---------+-------+
3 rows in set (0.00 sec)
mysql> desc book_users;
+---------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+-------+
| book_id | varchar(191) | NO | PRI | NULL | |
| user_id | bigint(20) | NO | PRI | NULL | |
+---------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)
10. 创建用户
router/api_router.go
package router
import (
"bookmanage/dao/mysql"
"bookmanage/model"
"github.com/gin-gonic/gin"
)
func LoadApiRouter(r *gin.Engine) {
r.POST("/register", RegisterHandler)
}
func RegisterHandler(c *gin.Context) {
p := new(model.User)
// 参数校验和绑定
if err := c.BindJSON(p); err != nil {
c.JSON(400, gin.H{"msg": err.Error()})
return
}
// 入库
mysql.DB.Create(p)
c.JSON(200, gin.H{"msg": "success"})
}
router/init_router.go
func InitRouter() *gin.Engine {
r := gin.Default()
LoadTestRouter(r)
LoadApiRouter(r) // <<------追加
return r
}
如果此时什么都不带请求就会有报错
加上username和password参数后提交成功
数据库下对应记录也被正常创建
mysql> select * from user;
+----+----------+----------+-------+
| id | username | password | token |
+----+----------+----------+-------+
| 1 | qiuqin | root123 | |
+----+----------+----------+-------+
1 row in set (0.00 sec)
11. 用户注册和登录
11.1 创建逻辑分层目录
mkdir controller
11.2 逻辑分层整理
controller/user.go
将原来api下面的RegisterHandler移动到这个文件下
package controller
import (
"bookmanage/dao/mysql"
"bookmanage/model"
"github.com/gin-gonic/gin"
)
// 注册功能
func RegisterHandler(c *gin.Context) {
p := new(model.User)
// 参数校验和绑定
if err := c.BindJSON(p); err != nil {
c.JSON(400, gin.H{"msg": err.Error()})
return
}
// 入库
mysql.DB.Create(p)
c.JSON(200, gin.H{"msg": "success"})
}
11.3 逻辑分层整理
router/api_router.go
package router
import (
"bookmanage/controller"
"github.com/gin-gonic/gin"
)
func LoadApiRouter(r *gin.Engine) {
r.POST("/register", controller.RegisterHandler) //<<---这里将func指向controller下
}
11.4 安装uuid包
PS D:\bookmanage> go get github.com/google/uuid
go: downloading github.com/google/uuid v1.3.0
go: added github.com/google/uuid v1.3.0
11.5 追加登录功能
controller/user.go
// 登录功能
func LoginHandler(c *gin.Context) {
p := new(model.User)
// 参数校验和绑定
if err := c.BindJSON(p); err != nil {
c.JSON(400, gin.H{"msg": err.Error()})
return
}
// 判断用户名密码是否正确
u := model.User{Username: p.Username, Password: p.Password}
// 判断rows是否有数据
if rows := mysql.DB.Where(&u).First(&u).Row(); rows == nil {
c.JSON(403, gin.H{"msg": "用户名密码错误"})
return
}
//随机生成一个字符串作为Token
token := uuid.New().String()
mysql.DB.Model(&u).Update("token", token)
c.JSON(200, gin.H{"msg": "success"})
}
11.6 Login路由追加
router/init_router.go
追加login路由
func LoadApiRouter(r *gin.Engine) {
r.POST("/register", controller.RegisterHandler)
r.POST("/login", controller.LoginHandler)
}
重启服务后访问测试.
没有输入用户名密码时无法登陆成功
如果密码错误
输入正确的值时,会返回token
同时Token被写入数据库对应用户记录下
ysql> select * from user;
+----+----------+----------+--------------------------------------+
| id | username | password | token |
+----+----------+----------+--------------------------------------+
| 1 | qiuqin | root123 | f1bbf482-37e6-479d-9e15-a60dd2907f37 |
+----+----------+----------+--------------------------------------+
1 row in set (0.00 sec)
12. 书籍
12.1 创建书籍
12.1.1 创建书籍func
controller/book.go
// Create book ifno
func CreateBookHandler(c *gin.Context) {
p := new(model.Book)
if err := c.ShouldBind(p); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
mysql.DB.Create(p)
c.JSON(200, gin.H{"msg": "书籍记录新增成功!"})
}
12.1.2 书籍的路由
router/api_router.go
func LoadApiRouter(r *gin.Engine) {
r.POST("/register", controller.RegisterHandler)
r.POST("/login", controller.LoginHandler)
v1 := r.Group("/api/v1")
v1.POST("/book", controller.CreateBookHandler)
}
重启服务后
如果没有提交参数,则会报错,因为这里的name是必须的
正确填入书名后,返回:书籍记录新增成功!
数据库该记录也被追加成功
12.2 查看书籍清单
12.2.1 查看书籍清单func
controller/book.go
// Get Book list
func GetBookListHandler(c *gin.Context) {
books := []model.Book{}
mysql.DB.Find(&books)
c.JSON(200, gin.H{"books": books})
}
12.2.2 路由配置
router/api_router.go
func LoadApiRouter(r *gin.Engine) {
r.POST("/register", controller.RegisterHandler)
r.POST("/login", controller.LoginHandler)
v1 := r.Group("/api/v1")
v1.POST("/book", controller.CreateBookHandler)
v1.GET("/book", controller.GetBookListHandler) //<<-----追加这条
}
这样用GET访问http://192.168.31.1:8080/api/v1/book 就可以获取到书籍信息
当有多条记录时,会通过json一起返回
12.3 单条数据查询
12.3.1 单条数据查询func
controller/book.go
// Get Book detail
func BookDetailHandler(c *gin.Context) {
bookId, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
book := model.Book{Id: bookId}
mysql.DB.Find(&book)
c.JSON(200, gin.H{"book": book})
}
12.3.2 路由配置
router/api_router.go
func LoadApiRouter(r *gin.Engine) {
r.POST("/register", controller.RegisterHandler)
r.POST("/login", controller.LoginHandler)
v1 := r.Group("/api/v1")
v1.POST("/book", controller.CreateBookHandler)
v1.GET("/book", controller.GetBookListHandler)
v1.GET("/book/:id", controller.BookDetailHandler)
}
重启服务后可以访问到记录
12.4 更新记录
12.4.1 更新记录func
controller/book.go
// update Book detail
func UpdateBookHandler(c *gin.Context) {
p := new(model.Book)
if err := c.ShouldBind(p); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
oldBookId := &model.Book{Id: p.Id}
var newBook model.Book
if p.Name != "" {
newBook.Name = p.Name
}
if p.Desc != "" {
newBook.Desc = p.Desc
}
mysql.DB.Model(&oldBookId).Updates(newBook)
c.JSON(200, gin.H{"msg": newBook})
}
12.4.2 路由配置
router/api_router.go
func LoadApiRouter(r *gin.Engine) {
r.POST("/register", controller.RegisterHandler)
r.POST("/login", controller.LoginHandler)
v1 := r.Group("/api/v1")
v1.POST("/book", controller.CreateBookHandler)
v1.GET("/book", controller.GetBookListHandler)
v1.GET("/book/:id", controller.BookDetailHandler)
v1.PUT("/book", controller.UpdateBookHandler) // <-----添加这条
}
重启服务后