# bookManage **Repository Path**: gin-devops/book-manage ## Basic Information - **Project Name**: bookManage - **Description**: bookManage 图书管理系统-后端 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-09-12 - **Last Updated**: 2023-09-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 图书管理系统 ## 1 初始化项目环境 ### 1.1 go常用项目结构 bookManage项目结构 ```shell . ├── README.md # 项目说明(帮助你快速的属性和了解项目) ├── config # 配置文件(mysql配置 ip 端口 用户名 密码,不能写死到代码中) ├── controller # CLD:服务入口,负责处理路由、参数校验、请求转发 ├── service # CLD:逻辑(服务)层,负责业务逻辑处理 ├── dao # CLD:负责数据与存储相关功能(mysql、redis、ES等) │ ├── mysql ├── model # 模型(定义表结构) ├── logging # 日志处理 ├── main.go # 项目启动入口 ├── middleware # 中间件 ├── pkg # 公共服务(所有模块都能访问的服务) ├── router # 路由(路由分发 ``` ### 1.2 创建数据库 ```shell mysql> create database books charset utf8; ``` ### 1.3 当前项目架构 ```shell go mod init bookManage ``` 图书管理服务: 1. 用户服务:登录,注册 2. 书籍服务:对书籍的增删改查的操作 ```shell . ├── controller # CLD:服务入口,负责处理路由、参数校验、请求转发 │ ├── book.go │ └── user.go ├── dao # CLD:负责数据与存储相关功能(mysql、redis、ES等) │ └── mysql │ └── mysql.go ├── main.go # 项目启动入口 ├── middleware # 中间件: token验证 │ └── auth.go ├── model # 模型 │ ├── book.go │ ├── user.go │ └── user_m2m_book.go └── router # 路由 ├── api_router.go ├── init_router.go └── test_router.go ``` ## 2 添加路由分层 ### 2.1 main.go ```go // bookManage project main.go package main import "bookManage/router" /** * @author hujianli */ func main() { //1、初始化路由分层,将实例化router服务的方法拆分到router文件下 r := router.InitRouter() r.Run(":8888") } ``` ### 2.2 router/init_router.go ```go package router import ( "github.com/gin-gonic/gin" ) /** * @author hujianli * 加载其他路由文件中的路由 */ // 这个方法作用:初始化其他文件中的路由 func InitRouter() *gin.Engine { //初始化gin服务 r := gin.Default() TestRouters(r) //SetupApiRouters(r) return r } ``` ### 2.3 router/test_router.go ```go package router import "github.com/gin-gonic/gin" /** * @author hujianli */ func TestRouters(r *gin.Engine) { v1 := r.Group("/api/v1") v1.GET("test", TestHandler) } // 测试路由访问,http://127.0.0.1:8888/api/v1/test func TestHandler(c *gin.Context) { c.String(200, "ok") } ``` 测试路由访问,`http://127.0.0.1:8888/api/v1/test`说明路由分组已经跑通了。 ## 3 初始化mysql连接 ### 3.1 main.go ```go // bookManage project main.go package main import ( "bookManage/dao/mysql" "bookManage/router" ) /** * @author hujianli */ func main() { // 初始化mysql连接 mysql.InitMysql() //初始化路由分层,将实例化router服务的方法拆分到router文件下 r := router.InitRouter() r.Run(":8888") } ``` ### 3.2 dao/mysql/mysql.go ```go // mysql project mysql.go package mysql import ( "fmt" gmysql "gorm.io/driver/mysql" "gorm.io/gorm" ) /** * @author hujianli */ var DB *gorm.DB func InitMysql() { //1、连接数据库 dsn := "root:124456@tcp(127.0.0.1:4406)/books?charset=utf8mb4&parseTime=True&loc=Local" db, err := gorm.Open(gmysql.Open(dsn), &gorm.Config{}) if err != nil { fmt.Println("初始化mysql连接错误", err) } DB = db } ``` ## 4 定义多对多表结构 书和用户是多对多的关系。 书可以供多个用户借阅,一个用户也可以借阅过多本书,所以是多对多 ### 4.1 model/user.go ```go package model /** * @author hujianli */ type User struct { Id int64 `gorm:"primary_key" json:"id"` Username string `gorm:"not null" json:"username" binging:"required"` Password string `gorm:"not null" json:"password" binding:"required"` Token string `json:"token"` } func (User) TableName() string { return "user" } ``` ### 4.2 model/book.go ```go package model /* @author hujianli */ type Book struct { Id uint `gorm:"primary_key" json:"id"` // Name string `gorm:"not null" json:"name" binding:"required"` Name string `gorm:"not null" json:"name"` Desc string `json:"desc"` //一本书多个用户借阅过,一个用户也可以借阅过多本书,所以是多对多 Users []User `gorm:"many2many:book_users;"` } func (Book) TableName() string { return "book" } ``` ### 4.3 model/user_m2m_book.go ```go package model /* @author hujianli */ // 自定义第三张表 type BookUser struct { UserID uint `gorm:"primaryKey"` BookID uint `gorm:"primaryKey"` } ``` ### 4.4 自动生成表结构 dao/mysql/mysql.go ```go // mysql project mysql.go package mysql import ( "fmt" "bookManage/model" gmysql "gorm.io/driver/mysql" "gorm.io/gorm" ) /** * @author hujianli */ var DB *gorm.DB func InitMysql() { //1、连接数据库 dsn := "root:123456@tcp(127.0.0.1:3306)/books?charset=utf8mb4&parseTime=True&loc=Local" db, err := gorm.Open(gmysql.Open(dsn), &gorm.Config{}) if err != nil { fmt.Println("初始化mysql连接错误", err) } DB = db //自动创建表结构 if err := DB.AutoMigrate(model.User{}, model.Book{}); err != nil { fmt.Println("自动创建表结构失败:", err) } } ``` ## 5 注册登录 ### 5.1 router/init_router.go ```go package router import ( "github.com/gin-gonic/gin" ) /** * @author hujianli * 加载其他路由文件中的路由 */ // 这个方法作用:初始化其他文件中的路由 func InitRouter() *gin.Engine { //初始化gin服务 r := gin.Default() TestRouters(r) SetupApiRouters(r) return r } ``` ### 5.2 router/api_router.go ```go package router import ( "bookManage/controller" "github.com/gin-gonic/gin" ) /** * @author hujianli */ func SetupApiRouters(r *gin.Engine) { r.POST("/register", controller.RegisterHandler) r.POST("/login", controller.LoginHandler) } ``` ### 5.3 controller/user.go ```go package controller import ( "bookManage/dao/mysql" "bookManage/model" "crypto/sha256" "encoding/hex" "github.com/gin-gonic/gin" "github.com/google/uuid" ) /** @author hujianli */ // 密码加密函数 func encryptPassword(password string) string { hash := sha256.Sum256([]byte(password)) encryptedPassword := hex.EncodeToString(hash[:]) return encryptedPassword } // 比对密码函数 func comparePasswords(hashedPassword string, password string) bool { encryptedPassword := encryptPassword(password) return hashedPassword == encryptedPassword } // 注册 func RegisterHandler(c *gin.Context) { p := new(model.User) //参数校验 if err := c.ShouldBindJSON(p); err != nil { c.JSON(400, gin.H{"err": err.Error()}) return } // 这里应该是客户端已加密后的密码 password := p.Password p.Password = encryptPassword(password) mysql.DB.Create(p) c.JSON(200, gin.H{"msg": p}) } // 登录 func LoginHandler(c *gin.Context) { p := new(model.User) //参数校验 if err := c.ShouldBindJSON(p); err != nil { c.JSON(400, gin.H{"err": err.Error()}) return } //查找用户 var user model.User result := mysql.DB.Where(&model.User{Username: p.Username}).First(&user) if result.Error != nil { c.JSON(403, gin.H{"msg": "用户名密码错误"}) return } //比对密码 if !comparePasswords(user.Password, p.Password) { c.JSON(403, gin.H{"msg": "用户名密码错误"}) return } //生成token token := uuid.New().String() mysql.DB.Model(&user).Update("token", token) c.JSON(200, gin.H{"token": token}) } ``` ### 5.4 测试注册功能 > 建议使用的工具: postman ```shell POST: http://127.0.0.1:8888/register # 携带数据: # body-raw-json: { "username": "hujianli", "password": "oschina" } ``` postman返回信息如下 ```shell { "msg": { "id": 1, "username": "hujianli", "password": "94b377d9c64df9fc85f8c87de846cb44dc64ba40beec3139d4cc1a10fd3860a7", "token": "" } } ``` ### 5.5 登录获取token ```shell POST: http://127.0.0.1:8888/login # 携带数据: # body-raw-json: { "username": "hujianli", "password": "oschina" } ``` postman返回信息如下 ```shell { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQ1NzQwODEsImlkIjoxfQ.z3dCZxKUh--bADn1zxBRZ9sXx90JXpVfE3xCbnW-5xU" } ``` ## 6.图书管理 ### 6.1 router/api_router.go ```go package router import ( "bookManage/controller" "github.com/gin-gonic/gin" ) /** * @author hujianli */ func SetupApiRouters(r *gin.Engine) { r.POST("/register", controller.RegisterHandler) r.POST("/login", controller.LoginHandler) v1 := r.Group("/api/v1") // 书籍CRUD v1.POST("book", controller.CreateBookHandler) v1.GET("book", controller.GetBookListHandler) v1.GET("book/:id", controller.GetBookDetailHandler) v1.PUT("book", controller.UpdateBookHandler) v1.DELETE("book/:id", controller.DeleteBookHandler) // 添加一条借阅记录 v1.POST("bookuser", controller.CreateBookUsersHandler) // 查找书籍的借阅记录 v1.GET("bookuser", controller.GetBookrecordsHandler) } ``` ### 6.2 controller/book.go ```go package controller import ( "bookManage/dao/mysql" "bookManage/model" "strconv" "github.com/gin-gonic/gin" ) /** * @author hujianli */ // 无论是在获取书籍列表还是获取特定书籍详情时,都将返回一个新的结构体 BookResponse, // 其中用户的密码和 Token 字段已被排除。这样就可以保证在视图中不返回用户表的密码和 Token 信息。 type BookResponse struct { ID uint `json:"id"` Name string `json:"name"` Desc string `json:"desc"` UserIDs []uint `json:"user_ids"` Usernames []string `json:"usernames"` } // 增 func CreateBookHandler(c *gin.Context) { p := new(model.Book) if err := c.ShouldBindJSON(p); err != nil { c.JSON(400, gin.H{"err": err.Error()}) return } mysql.DB.Create(p) c.JSON(200, gin.H{"msg": "success"}) } // 查看书籍列表 func GetBookListHandler(c *gin.Context) { books := []model.Book{} mysql.DB.Preload("Users").Find(&books) // //mysql.DB.Find(&books)//只查书籍,不查关联User // c.JSON(200, gin.H{"books": books}) // 构建响应对象 bookResponses := []BookResponse{} for _, book := range books { userIDs := []uint{} usernames := []string{} for _, user := range book.Users { userIDs = append(userIDs, user.Id) usernames = append(usernames, user.Username) } bookResponse := BookResponse{ ID: book.Id, Name: book.Name, Desc: book.Desc, UserIDs: userIDs, Usernames: usernames, } bookResponses = append(bookResponses, bookResponse) } c.JSON(200, gin.H{"books": bookResponses}) } // 查看指定书籍 func GetBookDetailHandler(c *gin.Context) { pipelineIdStr := c.Param("id") //获取URL参数 bookId, _ := strconv.ParseUint(pipelineIdStr, 10, 64) book := model.Book{Id: uint(bookId)} mysql.DB.Preload("Users").Find(&book) // mysql.DB.Find(&book) //只查书籍,不查关联User userIDs := []uint{} usernames := []string{} for _, user := range book.Users { userIDs = append(userIDs, user.Id) usernames = append(usernames, user.Username) } bookResponse := BookResponse{ ID: book.Id, Name: book.Name, Desc: book.Desc, UserIDs: userIDs, Usernames: usernames, } c.JSON(200, gin.H{"book": bookResponse}) // c.JSON(200, gin.H{"books": book}) } // 改 func UpdateBookHandler(c *gin.Context) { p := new(model.Book) if err := c.ShouldBindJSON(p); err != nil { c.JSON(400, gin.H{"err": err.Error()}) return } oldBook := &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(&oldBook).Updates(newBook) c.JSON(200, gin.H{"book": newBook}) } // 删除 func DeleteBookHandler(c *gin.Context) { pipelineIdStr := c.Param("id") //获取URL参数 //字符串,进制,64表示int64 bookId, _ := strconv.ParseUint(pipelineIdStr, 10, 64) //删除book时,也删除四三张表中的用户对应关系记录 mysql.DB.Select("Users").Delete(&model.Book{Id: uint(bookId)}) c.JSON(200, gin.H{"msg": "success"}) } // 增加一条借阅记录 func CreateBookUsersHandler(c *gin.Context) { u := new(model.BookUser) if err := c.ShouldBindJSON(u); err != nil { c.JSON(400, gin.H{"err": err.Error()}) return } mysql.DB.Create(u) c.JSON(200, gin.H{"msg": "success"}) } // 查看书籍的借阅记录 func GetBookrecordsHandler(c *gin.Context) { book := new(model.Book) if err := c.ShouldBindJSON(book); err != nil { c.JSON(400, gin.H{"err": err.Error()}) return } mysql.DB.Preload("Users").Find(&book) usernames := []string{} for i := 0; i < len(book.Users); i++ { usernames = append(usernames, strconv.FormatInt(int64(book.Users[i].Id), 10)) usernames = append(usernames, book.Users[i].Username) } c.JSON(200, usernames) } ``` ### 6.3 创建图书 ```shell POST:http://127.0.0.1:8888/api/v1/book/ # 携带数据: # body-raw-json: { "name": "水浒传", "desc": "武松打虎" } ``` ### 6.4 查看图书列表 ```shell GET:http://127.0.0.1:8888/api/v1/book/ ``` postman返回信息如下 ```json { "books": [ { "id": 1, "name": "三国演义", "desc": "三顾茅庐请诸葛亮、曹操煮酒论英雄", "user_ids": [ 1, 2 ], "usernames": [ "hujianli1", "hujianli2" ] }, { "id": 2, "name": "西游记", "desc": "你挑着担,我牵着马,历经千辛万苦...", "user_ids": [], "usernames": [] }, { "id": 3, "name": "红楼梦", "desc": "刘姥姥走进了大观园.", "user_ids": [], "usernames": [] }, { "id": 4, "name": "水浒传", "desc": "武松打虎", "user_ids": [], "usernames": [] } ] } ``` ### 6.5 查看图书详情 ```shell GET:http://127.0.0.1:8888/api/v1/book/3/ # 没有这本书,返回空数据 GET:http://127.0.0.1:8888/api/v1/book/1/ ``` postman返回信息如下 ```json { "book": { "id": 1, "name": "三国演义", "desc": "三顾茅庐请诸葛亮、曹操煮酒论英雄", "user_ids": [ 1, 2 ], "usernames": [ "hujianli1", "hujianli2" ] } } ``` ### 6.6 修改图书信息 ```shell PUT:http://127.0.0.1:8888/api/v1/book/ # 携带数据: # body-raw-json: { "id": 1, "name": "三国演义", "desc": "三顾茅庐请诸葛亮、曹操煮酒论英雄" } ``` postman返回信息如下 ```json { "book": { "id": 0, "name": "三国演义", "desc": "三顾茅庐请诸葛亮、曹操煮酒论英雄", "Users": null } } ``` ### 6.7 删除图书信息 ```shell DELETE: http://127.0.0.1:8888/api/v1/book/1/ ``` postman返回信息如下 ```json { "msg": "success" } ``` ### 6.8 添加一条借阅记录 ```shell POST: http://127.0.0.1:8888/api/v1/bookuser/ # 携带数据: # body-raw-json: { "bookid": 1, "userid": 2 } ``` postman返回信息如下 ```json { "msg": "success" } ``` ### 6.9 查看书籍的借阅记录 ```shell GET: http://127.0.0.1:8888/api/v1/bookuser # 请求数据 # body: { "id": 3 } ``` postman返回信息如下 ```json [ "1", "hujianli1", "2", "hujianli2" ] ``` ## 7.中间件身份验证 ### 7.1 middleware/auth.go ```go package middleware import ( "bookManage/dao/mysql" "bookManage/model" "github.com/gin-gonic/gin" ) func AuthMiddleware() func(c *gin.Context) { return func(c *gin.Context) { //客户端携带Token有三种方式 // 1、放在请求头 // 2、放在请求体 // 3、放在URL //token验证成功,返回c.Next继续,否则返回c.Abort()直接返回 token := c.Request.Header.Get("token") var u model.User //如果没有当前用户 if rows := mysql.DB.Where("token=?", token).First(&u).RowsAffected; rows != 1 { c.JSON(403, gin.H{"msg": "当前token错误"}) c.Abort() return } //将当前请求的userID信息保存到请求的上下文c上 c.Set("UserId", u.Id) c.Next() } } ``` ### 7.2 router/api_router.go ```go package router import ( "bookManage/controller" "bookManage/middleware" "github.com/gin-gonic/gin" ) /** * @author hujianli */ func SetupApiRouters(r *gin.Engine) { r.POST("/register", controller.RegisterHandler) r.POST("/login", controller.LoginHandler) v1 := r.Group("/api/v1") //添加中间验证 v1.Use(middleware.AuthMiddleware()) // 书籍CRUD v1.POST("book", controller.CreateBookHandler) ..... } ``` ### 7.3 测试登录功能 ```shell POST: http://127.0.0.1:8888/login # 携带数据: # body-raw-json: { "username": "hujianli1", "password": "oschina" } ``` postman返回信息如下 ```shell { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQ1OTQ1NzIsImlkIjoxfQ.k2JAkllOHSxl-wYCpm4GR4KHuX3Wm_sarLMAaOIAM7A" } ``` ### 7.4 测试无token获取图书列表 ```shell GET: http://127.0.0.1:8888/api/v1/book ``` postman返回信息如下 ```json { "msg": "当前token错误" } ``` ### 7.5 携带token访问 在 Postman 中传入 Token 有以下几种方式: ```shell 放在请求头(Header)中: 打开 Postman。 在请求的 Headers 部分,添加一个键值对,键为 "token",值为您要传递的 Token。 ``` 放在请求体(Body)中: ```shell 打开 Postman。 在请求的 Body 部分,选择 "raw"(原始数据)选项。 在输入框中输入 Token。 ``` 放在 URL 参数中: ```shell 打开 Postman。 在请求的 URL 后面追加参数,例如:http://example.com/api?token=your_token ``` ```shell GET: http://127.0.0.1:8888/api/v1/book # Headers 部分,添加一个键值对,键为 "token" token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQ1OTQ1NzIsImlkIjoxfQ.k2JAkllOHSxl-wYCpm4GR4KHuX3Wm_sarLMAaOIAM7A" ``` postman返回信息如下 ```json { "books": [ { "id": 1, "name": "三国演义", "desc": "三顾茅庐请诸葛亮、曹操煮酒论英雄", "user_ids": [ 1, 2 ], "usernames": [ "hujianli1", "hujianli2" ] }, { "id": 2, "name": "西游记", "desc": "你挑着担,我牵着马,历经千辛万苦...", "user_ids": [], "usernames": [] }, { "id": 3, "name": "红楼梦", "desc": "刘姥姥走进了大观园.", "user_ids": [], "usernames": [] }, { "id": 4, "name": "水浒传", "desc": "武松打虎", "user_ids": [], "usernames": [] } ] } ``` ## 8. go常用项目结构 ```shell ├──Readme.md/ # 项目说明(帮助你快速的属性和了解项目) ├──config/ # 配置文件(mysq配置ip端口用户名密码,不能写死到代码中) ├──controller/ # CLD:服务入口,负责处理路由、参数校验、请求转发 ├──service/ # CLD:逻辑(服务)层,负责业务逻辑处理 ├──dao/ # CLD:负责数据与存储相关功能(mysql、redis、ES等) ├──mysql ├──model/ # 模型(定义表结构) ├──logging/ # 日志处理 ├──main.go/ # 项目启动入口 ├──middleware/ # 中间件 ├──pkg/ # 公共服务(所有模块都能访问的服务) ├──router/ # 路由(路由分发) ```