go语言 gin grom jwt 登陆token验证 增删改查 分页 完整 图书管理系统

因为 依赖的swagger 中 在浏览器中 不能注册,无法测试接口

所以改为postman进行测试 配合swagger查询参数

postamn官方下载需要登陆,这里提供一个不用登陆的版本

通过网盘分享的文件:Postman-win64-10.13.6-Setup 免登录.exe
链接: https://pan.baidu.com/s/1OBxASJrmBv2tqm_qZH1Ykg?pwd=2ziv 提取码: 2ziv

http://192.168.0.103:8080/swagger/index.html

上面的是swagger的登陆地址

注册好后登陆

登陆 后 返回 jwt token 填写到需要的接口 token中就可以用了

依赖安装

go get github.com/gin-gonic/gin@v1.9.1

go get gorm.io/gorm@v1.25.4

go get gorm.io/driver/mysql@v1.5.4

go get github.com/golang-jwt/jwt/v5@v5.2.1

go get golang.org/x/crypto@latest

安装Swag CLI(生成文档用,全局可用)

go install github.com/swaggo/swag/cmd/swag@v1.16.1

安装Gin适配插件和静态文件

go get github.com/swaggo/gin-swagger@v1.6.0

go get github.com/swaggo/files@v1.0.1

运行后swagger init 初始化docs

main.go

Go 复制代码
// @title 图书管理系统API文档
// @version 1.0
// @description 基于Go + Gin + GORM实现的图书管理系统,包含用户注册/登录、JWT认证、分类-图书一对多管理、分页查询功能
// @termsOfService http://swagger.io/terms/

// @contact.name 开发者
// @contact.url http://example.com/support
// @contact.email support@example.com

// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html

// @host localhost:8080
// @BasePath /
// @schemes http

// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
package main

import (
	"errors"
	_ "nandn/docs" // 替换为你的docs文件夹绝对路径(Windows)
	"net/http"
	"strconv"
	"time"

	"nandn/model" // 导入model包,用于业务逻辑处理

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
	"github.com/swaggo/files"
	"github.com/swaggo/gin-swagger"
	"golang.org/x/crypto/bcrypt"
	"gorm.io/gorm"
)

// ---------------------- 本地结构体定义(新增分页相关) ----------------------
// BaseResponse 统一接口响应结构体
type BaseResponse struct {
	Code int         `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data"`
}

// PaginationReq 分页请求参数(通用)
type PaginationReq struct {
	Page     int `form:"page" binding:"omitempty,gte=1"`      // 页码,默认1
	PageSize int `form:"page_size" binding:"omitempty,gte=1"` // 页大小,默认10
}

// PaginationRes 分页响应结构体(通用)
type PaginationRes struct {
	Total     int         `json:"total"`      // 总条数
	Page      int         `json:"page"`       // 当前页码
	PageSize  int         `json:"page_size"`  // 页大小
	TotalPage int         `json:"total_page"` // 总页数
	List      interface{} `json:"list"`       // 数据列表
}

// RegisterReq 用户注册请求结构体
type RegisterReq struct {
	Username string `json:"username" binding:"required,min=3,max=30"`
	Password string `json:"password" binding:"required,min=6,max=20"`
	Email    string `json:"email" binding:"omitempty,email"`
}

// RegisterResData 用户注册响应数据结构体
type RegisterResData struct {
	UserID   uint   `json:"user_id"`
	Username string `json:"username"`
	Email    string `json:"email"`
}

// LoginReq 用户登录请求结构体
type LoginReq struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

// LoginResData 用户登录响应数据结构体
type LoginResData struct {
	UserID   uint   `json:"user_id"`
	Username string `json:"username"`
	Token    string `json:"token"`
	ExpireAt string `json:"expire_at"`
}

// Category 分类结构体(本地定义,Swagger可解析)
type Category struct {
	ID        uint   `json:"id"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
	Name      string `json:"name"`
	Books     []Book `json:"books,omitempty"`
}

// Book 图书结构体(本地定义,Swagger可解析)
type Book struct {
	ID         uint     `json:"id"`
	CreatedAt  string   `json:"created_at"`
	UpdatedAt  string   `json:"updated_at"`
	Title      string   `json:"title"`
	Author     string   `json:"author"`
	Price      float64  `json:"price"`
	CategoryID uint     `json:"category_id"`
	Category   Category `json:"category,omitempty"`
}

// CategoryCreateReq 分类创建请求结构体
type CategoryCreateReq struct {
	Name string `json:"name" binding:"required,min=2,max=50"`
}

// BookCreateReq 图书创建请求结构体
type BookCreateReq struct {
	Title      string  `json:"title" binding:"required,min=2,max=100"`
	Author     string  `json:"author" binding:"required,min=2,max=50"`
	Price      float64 `json:"price" binding:"required,gte=0"`
	CategoryID uint    `json:"category_id" binding:"required,gte=1"`
}

// ---------------------- 全局配置与JWT声明 ----------------------
// JWT密钥(生产环境请存储在环境变量中)
var jwtSecret = []byte("book_manage_2026_jwt_secret_key")

// Claims JWT自定义声明
type Claims struct {
	UserID   uint   `json:"user_id"`
	Username string `json:"username"`
	jwt.RegisteredClaims
}

func main() {
	// 1. 初始化数据库
	model.InitDB()

	// 2. 创建Gin引擎
	r := gin.Default()

	// 3. 注册Swagger路由(公开访问)
	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

	// 4. 公开路由(无需登录,用户注册/登录)
	publicGroup := r.Group("/api/public")
	{
		publicGroup.POST("/register", userRegister)
		publicGroup.POST("/login", userLogin)
	}

	// 5. 受保护路由(需要登录验证,图书/分类相关)
	protectedGroup := r.Group("/api")
	protectedGroup.Use(JWTAuthMiddleware()) // 全局添加JWT权限中间件
	{
		// 分类相关
		categoryGroup := protectedGroup.Group("/categories")
		{
			categoryGroup.POST("", createCategory)
			categoryGroup.GET("/:id", getCategoryWithBooks)
			categoryGroup.GET("", getAllCategories) // 支持分页
		}

		// 图书相关
		bookGroup := protectedGroup.Group("/books")
		{
			bookGroup.POST("", createBook)
			bookGroup.GET("", getAllBooks) // 支持分页
			bookGroup.GET("/:id", getBookWithCategory)
		}
	}

	// 6. 启动HTTP服务
	err := r.Run(":8080")
	if err != nil {
		panic("服务启动失败:" + err.Error())
	}
}

// ---------------------- 通用工具函数 ----------------------
// getPaginationParams 解析分页参数(默认page=1,page_size=10)
func getPaginationParams(c *gin.Context) (page, pageSize int) {
	// 解析页码,默认1
	pageStr := c.DefaultQuery("page", "1")
	page, _ = strconv.Atoi(pageStr)
	if page < 1 {
		page = 1
	}

	// 解析页大小,默认10,最大限制50(防止一次性查询过多数据)
	pageSizeStr := c.DefaultQuery("page_size", "10")
	pageSize, _ = strconv.Atoi(pageSizeStr)
	if pageSize < 1 {
		pageSize = 10
	} else if pageSize > 50 {
		pageSize = 50
	}

	return page, pageSize
}

// calcPagination 计算分页参数(总条数→总页数)
func calcPagination(total, page, pageSize int) PaginationRes {
	totalPage := total / pageSize
	if total%pageSize != 0 {
		totalPage += 1
	}
	return PaginationRes{
		Total:     total,
		Page:      page,
		PageSize:  pageSize,
		TotalPage: totalPage,
	}
}

// ---------------------- JWT工具函数 ----------------------
// generateToken 生成JWT令牌
func generateToken(userID uint, username string) (string, error) {
	// 1. 设置令牌过期时间(2小时)
	expirationTime := time.Now().Add(2 * time.Hour)

	// 2. 构建自定义声明
	claims := &Claims{
		UserID:   userID,
		Username: username,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(expirationTime),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			Issuer:    "book-management-system",
		},
	}

	// 3. 创建JWT令牌(HS256算法)
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// 4. 签名并生成字符串令牌
	tokenString, err := token.SignedString(jwtSecret)
	if err != nil {
		return "", err
	}

	return tokenString, nil
}

// JWTAuthMiddleware JWT权限校验中间件
func JWTAuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 1. 从请求头中获取Token(格式:Bearer xxx)
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusUnauthorized, BaseResponse{
				Code: 401,
				Msg:  "未携带登录令牌,请先登录",
				Data: nil,
			})
			c.Abort()
			return
		}

		// 2. 校验Token格式(是否以Bearer开头)
		var tokenString string
		parts := []rune(authHeader)
		if len(parts) > 7 && string(parts[:7]) == "Bearer " {
			tokenString = string(parts[7:])
		} else {
			c.JSON(http.StatusUnauthorized, BaseResponse{
				Code: 401,
				Msg:  "令牌格式错误,应为 Bearer + 令牌字符串",
				Data: nil,
			})
			c.Abort()
			return
		}

		// 3. 解析Token并校验有效性
		claims := &Claims{}
		token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
			// 校验签名算法
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, errors.New("不支持的令牌签名算法")
			}
			return jwtSecret, nil
		})

		// 4. 处理解析错误
		if err != nil || !token.Valid {
			c.JSON(http.StatusUnauthorized, BaseResponse{
				Code: 401,
				Msg:  "令牌无效或已过期,请重新登录",
				Data: nil,
			})
			c.Abort()
			return
		}

		// 5. 令牌有效,将用户信息存入Gin上下文
		c.Set("userID", claims.UserID)
		c.Set("username", claims.Username)

		// 6. 继续执行后续接口逻辑
		c.Next()
	}
}

// ---------------------- 用户相关业务逻辑 ----------------------
// userRegister 用户注册
// @Summary 用户注册
// @Description 注册新用户,用户名唯一,密码加密存储,无需授权
// @Tags 公开接口-用户管理
// @Accept json
// @Produce json
// @Param req body RegisterReq true "注册请求参数"
// @Success 201 {object} BaseResponse{data=RegisterResData} "注册成功"
// @Failure 400 {object} BaseResponse "参数校验失败"
// @Failure 409 {object} BaseResponse "用户名已被注册"
// @Failure 500 {object} BaseResponse "服务器内部错误"
// @Router /api/public/register [post]
func userRegister(c *gin.Context) {
	// 1. 绑定并校验请求参数
	var req RegisterReq
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, BaseResponse{
			Code: 400,
			Msg:  "参数校验失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 2. 校验用户名是否已存在
	var existingUser model.User
	if err := model.DB.Where("username = ?", req.Username).First(&existingUser).Error; err != gorm.ErrRecordNotFound {
		c.JSON(http.StatusConflict, BaseResponse{
			Code: 409,
			Msg:  "用户名已被注册,请更换用户名",
			Data: nil,
		})
		return
	}

	// 3. 密码加密(bcrypt算法)
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
	if err != nil {
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "密码加密失败,请重试",
			Data: nil,
		})
		return
	}

	// 4. 构建用户对象并写入数据库
	user := model.User{
		Username: req.Username,
		Password: string(hashedPassword),
		Email:    req.Email,
	}

	if err := model.DB.Create(&user).Error; err != nil {
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "注册失败,请重试:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 5. 构建响应数据并返回
	resData := RegisterResData{
		UserID:   user.ID,
		Username: user.Username,
		Email:    user.Email,
	}

	c.JSON(http.StatusCreated, BaseResponse{
		Code: 200,
		Msg:  "用户注册成功,请登录",
		Data: resData,
	})
}

// userLogin 用户登录
// @Summary 用户登录
// @Description 用户登录并生成JWT令牌,用于访问受保护接口,无需授权
// @Tags 公开接口-用户管理
// @Accept json
// @Produce json
// @Param req body LoginReq true "登录请求参数"
// @Success 200 {object} BaseResponse{data=LoginResData} "登录成功,返回JWT令牌"
// @Failure 400 {object} BaseResponse "参数校验失败"
// @Failure 401 {object} BaseResponse "用户名或密码错误"
// @Failure 500 {object} BaseResponse "服务器内部错误/生成令牌失败"
// @Router /api/public/login [post]
func userLogin(c *gin.Context) {
	// 1. 绑定并校验请求参数
	var req LoginReq
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, BaseResponse{
			Code: 400,
			Msg:  "参数校验失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 2. 查询用户是否存在
	var user model.User
	if err := model.DB.Where("username = ?", req.Username).First(&user).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			c.JSON(http.StatusUnauthorized, BaseResponse{
				Code: 401,
				Msg:  "用户名或密码错误",
				Data: nil,
			})
			return
		}
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "查询用户失败,请重试:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 3. 校验密码(对比明文密码与加密密码)
	err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
	if err != nil {
		c.JSON(http.StatusUnauthorized, BaseResponse{
			Code: 401,
			Msg:  "用户名或密码错误",
			Data: nil,
		})
		return
	}

	// 4. 生成JWT令牌
	tokenString, err := generateToken(user.ID, user.Username)
	if err != nil {
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "生成登录令牌失败,请重试",
			Data: nil,
		})
		return
	}

	// 5. 构建响应数据并返回
	resData := LoginResData{
		UserID:   user.ID,
		Username: user.Username,
		Token:    tokenString,
		ExpireAt: time.Now().Add(2 * time.Hour).Format("2006-01-02 15:04:05"),
	}

	c.JSON(http.StatusOK, BaseResponse{
		Code: 200,
		Msg:  "登录成功",
		Data: resData,
	})
}

// ---------------------- 分类相关业务逻辑 ----------------------
// createCategory 创建分类
// @Summary 创建图书分类
// @Description 创建新的图书分类,分类名称唯一,需携带有效JWT令牌
// @Tags 受保护接口-分类管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param req body CategoryCreateReq true "分类创建请求参数"
// @Success 201 {object} BaseResponse{data=Category} "分类创建成功"
// @Failure 400 {object} BaseResponse "参数校验失败"
// @Failure 401 {object} BaseResponse "未授权或令牌无效"
// @Failure 500 {object} BaseResponse "服务器内部错误/分类创建失败"
// @Router /api/categories [post]
func createCategory(c *gin.Context) {
	// 1. 绑定并校验请求参数
	var req CategoryCreateReq
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, BaseResponse{
			Code: 400,
			Msg:  "参数校验失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 2. 构建分类模型并写入数据库
	categoryModel := model.Category{Name: req.Name}
	if err := model.DB.Create(&categoryModel).Error; err != nil {
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "创建分类失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 3. 转换为本地Category结构体,构建响应数据
	categoryRes := Category{
		ID:        categoryModel.ID,
		CreatedAt: categoryModel.CreatedAt.Format("2006-01-02 15:04:05"),
		UpdatedAt: categoryModel.UpdatedAt.Format("2006-01-02 15:04:05"),
		Name:      categoryModel.Name,
	}

	// 4. 返回响应结果
	c.JSON(http.StatusCreated, BaseResponse{
		Code: 200,
		Msg:  "分类创建成功",
		Data: categoryRes,
	})
}

// getCategoryWithBooks 查询单个分类(包含关联的图书)
// @Summary 查询单个分类
// @Description 查询指定ID的分类,并返回该分类下的所有图书,需携带有效JWT令牌
// @Tags 受保护接口-分类管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "分类ID"
// @Success 200 {object} BaseResponse{data=Category} "查询成功"
// @Failure 401 {object} BaseResponse "未授权或令牌无效"
// @Failure 404 {object} BaseResponse "分类不存在"
// @Failure 500 {object} BaseResponse "服务器内部错误/查询失败"
// @Router /api/categories/{id} [get]
func getCategoryWithBooks(c *gin.Context) {
	// 1. 获取路径参数中的分类ID
	categoryID := c.Param("id")

	// 2. 查询分类(预加载关联的图书)
	var categoryModel model.Category
	if err := model.DB.Preload("Books").First(&categoryModel, categoryID).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			c.JSON(http.StatusNotFound, BaseResponse{
				Code: 404,
				Msg:  "分类不存在",
				Data: nil,
			})
			return
		}
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "查询分类失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 3. 转换图书列表为本地Book结构体
	var bookResList []Book
	for _, bookModel := range categoryModel.Books {
		bookRes := Book{
			ID:         bookModel.ID,
			CreatedAt:  bookModel.CreatedAt.Format("2006-01-02 15:04:05"),
			UpdatedAt:  bookModel.UpdatedAt.Format("2006-01-02 15:04:05"),
			Title:      bookModel.Title,
			Author:     bookModel.Author,
			Price:      bookModel.Price,
			CategoryID: bookModel.CategoryID,
		}
		bookResList = append(bookResList, bookRes)
	}

	// 4. 转换分类为本地Category结构体
	categoryRes := Category{
		ID:        categoryModel.ID,
		CreatedAt: categoryModel.CreatedAt.Format("2006-01-02 15:04:05"),
		UpdatedAt: categoryModel.UpdatedAt.Format("2006-01-02 15:04:05"),
		Name:      categoryModel.Name,
		Books:     bookResList,
	}

	// 5. 返回响应结果
	c.JSON(http.StatusOK, BaseResponse{
		Code: 200,
		Msg:  "查询成功",
		Data: categoryRes,
	})
}

// getAllCategories 查询所有分类(支持分页)
// @Summary 查询所有分类
// @Description 查询系统中所有的图书分类,支持分页(page=页码,page_size=页大小),需携带有效JWT令牌
// @Tags 受保护接口-分类管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "页码(默认1)" minimum(1)
// @Param page_size query int false "页大小(默认10,最大50)" minimum(1) maximum(50)
// @Success 200 {object} BaseResponse{data=PaginationRes{list=[]Category}} "查询成功,返回分页分类列表"
// @Failure 401 {object} BaseResponse "未授权或令牌无效"
// @Failure 500 {object} BaseResponse "服务器内部错误/查询失败"
// @Router /api/categories [get]
func getAllCategories(c *gin.Context) {
	// 1. 获取分页参数
	page, pageSize := getPaginationParams(c)

	// 2. 查询总条数
	var total int64
	if err := model.DB.Model(&model.Category{}).Count(&total).Error; err != nil {
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "查询分类总数失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 3. 分页查询分类列表
	var categoryModels []model.Category
	offset := (page - 1) * pageSize
	if err := model.DB.Offset(offset).Limit(pageSize).Find(&categoryModels).Error; err != nil {
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "查询分类列表失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 4. 转换为本地Category结构体列表
	var categoryResList []Category
	for _, categoryModel := range categoryModels {
		categoryRes := Category{
			ID:        categoryModel.ID,
			CreatedAt: categoryModel.CreatedAt.Format("2006-01-02 15:04:05"),
			UpdatedAt: categoryModel.UpdatedAt.Format("2006-01-02 15:04:05"),
			Name:      categoryModel.Name,
		}
		categoryResList = append(categoryResList, categoryRes)
	}

	// 5. 构建分页响应
	paginationRes := calcPagination(int(total), page, pageSize)
	paginationRes.List = categoryResList

	// 6. 返回响应结果
	c.JSON(http.StatusOK, BaseResponse{
		Code: 200,
		Msg:  "查询成功",
		Data: paginationRes,
	})
}

// ---------------------- 图书相关业务逻辑 ----------------------
// createBook 创建图书(关联指定分类)
// @Summary 创建图书
// @Description 创建新的图书,并关联到指定分类,需携带有效JWT令牌
// @Tags 受保护接口-图书管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param req body BookCreateReq true "图书创建请求参数"
// @Success 201 {object} BaseResponse{data=Book} "图书创建成功"
// @Failure 400 {object} BaseResponse "参数校验失败"
// @Failure 401 {object} BaseResponse "未授权或令牌无效"
// @Failure 404 {object} BaseResponse "关联的分类不存在"
// @Failure 500 {object} BaseResponse "服务器内部错误/图书创建失败"
// @Router /api/books [post]
func createBook(c *gin.Context) {
	// 1. 绑定并校验请求参数
	var req BookCreateReq
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, BaseResponse{
			Code: 400,
			Msg:  "参数校验失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 2. 校验分类是否存在
	var categoryModel model.Category
	if err := model.DB.First(&categoryModel, req.CategoryID).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			c.JSON(http.StatusNotFound, BaseResponse{
				Code: 404,
				Msg:  "关联的分类不存在",
				Data: nil,
			})
			return
		}
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "校验分类失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 3. 构建图书模型并写入数据库
	bookModel := model.Book{
		Title:      req.Title,
		Author:     req.Author,
		Price:      req.Price,
		CategoryID: req.CategoryID,
	}
	if err := model.DB.Create(&bookModel).Error; err != nil {
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "创建图书失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 4. 转换为本地Book结构体,构建响应数据
	bookRes := Book{
		ID:         bookModel.ID,
		CreatedAt:  bookModel.CreatedAt.Format("2006-01-02 15:04:05"),
		UpdatedAt:  bookModel.UpdatedAt.Format("2006-01-02 15:04:05"),
		Title:      bookModel.Title,
		Author:     bookModel.Author,
		Price:      bookModel.Price,
		CategoryID: bookModel.CategoryID,
	}

	// 5. 返回响应结果
	c.JSON(http.StatusCreated, BaseResponse{
		Code: 200,
		Msg:  "图书创建成功",
		Data: bookRes,
	})
}

// getAllBooks 查询所有图书(支持分页)
// @Summary 查询所有图书
// @Description 查询系统中所有的图书,支持分页(page=页码,page_size=页大小),需携带有效JWT令牌
// @Tags 受保护接口-图书管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "页码(默认1)" minimum(1)
// @Param page_size query int false "页大小(默认10,最大50)" minimum(1) maximum(50)
// @Success 200 {object} BaseResponse{data=PaginationRes{list=[]Book}} "查询成功,返回分页图书列表"
// @Failure 401 {object} BaseResponse "未授权或令牌无效"
// @Failure 500 {object} BaseResponse "服务器内部错误/查询失败"
// @Router /api/books [get]
func getAllBooks(c *gin.Context) {
	// 1. 获取分页参数
	page, pageSize := getPaginationParams(c)

	// 2. 查询总条数
	var total int64
	if err := model.DB.Model(&model.Book{}).Count(&total).Error; err != nil {
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "查询图书总数失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 3. 分页查询图书列表
	var bookModels []model.Book
	offset := (page - 1) * pageSize
	if err := model.DB.Offset(offset).Limit(pageSize).Find(&bookModels).Error; err != nil {
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "查询图书列表失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 4. 转换为本地Book结构体列表
	var bookResList []Book
	for _, bookModel := range bookModels {
		bookRes := Book{
			ID:         bookModel.ID,
			CreatedAt:  bookModel.CreatedAt.Format("2006-01-02 15:04:05"),
			UpdatedAt:  bookModel.UpdatedAt.Format("2006-01-02 15:04:05"),
			Title:      bookModel.Title,
			Author:     bookModel.Author,
			Price:      bookModel.Price,
			CategoryID: bookModel.CategoryID,
		}
		bookResList = append(bookResList, bookRes)
	}

	// 5. 构建分页响应
	paginationRes := calcPagination(int(total), page, pageSize)
	paginationRes.List = bookResList

	// 6. 返回响应结果
	c.JSON(http.StatusOK, BaseResponse{
		Code: 200,
		Msg:  "查询成功",
		Data: paginationRes,
	})
}

// getBookWithCategory 查询单本图书(包含关联的分类)
// @Summary 查询单本图书
// @Description 查询指定ID的图书,并返回该图书所属的分类,需携带有效JWT令牌
// @Tags 受保护接口-图书管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "图书ID"
// @Success 200 {object} BaseResponse{data=Book} "查询成功"
// @Failure 401 {object} BaseResponse "未授权或令牌无效"
// @Failure 404 {object} BaseResponse "图书不存在"
// @Failure 500 {object} BaseResponse "服务器内部错误/查询失败"
// @Router /api/books/{id} [get]
func getBookWithCategory(c *gin.Context) {
	// 1. 获取路径参数中的图书ID
	bookID := c.Param("id")

	// 2. 查询图书(预加载关联的分类)
	var bookModel model.Book
	if err := model.DB.Preload("Category").First(&bookModel, bookID).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			c.JSON(http.StatusNotFound, BaseResponse{
				Code: 404,
				Msg:  "图书不存在",
				Data: nil,
			})
			return
		}
		c.JSON(http.StatusInternalServerError, BaseResponse{
			Code: 500,
			Msg:  "查询图书失败:" + err.Error(),
			Data: nil,
		})
		return
	}

	// 3. 转换关联分类为本地Category结构体
	categoryRes := Category{
		ID:        bookModel.Category.ID,
		CreatedAt: bookModel.Category.CreatedAt.Format("2006-01-02 15:04:05"),
		UpdatedAt: bookModel.Category.UpdatedAt.Format("2006-01-02 15:04:05"),
		Name:      bookModel.Category.Name,
	}

	// 4. 转换图书为本地Book结构体
	bookRes := Book{
		ID:         bookModel.ID,
		CreatedAt:  bookModel.CreatedAt.Format("2006-01-02 15:04:05"),
		UpdatedAt:  bookModel.UpdatedAt.Format("2006-01-02 15:04:05"),
		Title:      bookModel.Title,
		Author:     bookModel.Author,
		Price:      bookModel.Price,
		CategoryID: bookModel.CategoryID,
		Category:   categoryRes,
	}

	// 5. 返回响应结果
	c.JSON(http.StatusOK, BaseResponse{
		Code: 200,
		Msg:  "查询成功",
		Data: bookRes,
	})
}

model.go

Go 复制代码
package model

import (
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// 全局DB对象,供外部调用
var DB *gorm.DB

// 1. 分类模型(移除所有GORM自定义类型,仅使用Go原生类型,兼容GORM软删除)
type Category struct {
	ID        uint      `gorm:"primaryKey;autoIncrement" json:"id"` // 主键自增
	CreatedAt time.Time `json:"created_at"`                         // 创建时间(Go原生类型)
	UpdatedAt time.Time `json:"updated_at"`                         // 更新时间(Go原生类型)
	// 移除gorm.DeletedAt,使用gorm:"softDelete:unix"标签实现软删除(数据库层生效,Swagger无需解析)
	// 字段类型使用int64,存储unix时间戳,json:"-" 表示不返回给前端,也不被Swagger解析
	DeletedAt int64  `gorm:"softDelete:unix;index" json:"-"`               // 软删除字段(仅数据库层生效,隐藏Swagger和前端响应)
	Name      string `gorm:"type:varchar(50);not null;unique" json:"name"` // 分类名称
	Books     []Book `gorm:"foreignKey:CategoryID" json:"books"`           // 关联图书
}

// 2. 图书模型(移除所有GORM自定义类型,兼容软删除)
type Book struct {
	ID         uint      `gorm:"primaryKey;autoIncrement" json:"id"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`
	DeletedAt  int64     `gorm:"softDelete:unix;index" json:"-"`           // 软删除字段(隐藏,仅数据库生效)
	Title      string    `gorm:"type:varchar(100);not null" json:"title"`  // 图书标题
	Author     string    `gorm:"type:varchar(50);not null" json:"author"`  // 作者
	Price      float64   `gorm:"type:decimal(10,2);not null" json:"price"` // 价格
	CategoryID uint      `gorm:"not null" json:"category_id"`              // 外键
	Category   Category  `gorm:"foreignKey:CategoryID" json:"category"`    // 反向关联分类
}

// 3. 用户模型(移除所有GORM自定义类型,兼容软删除)
type User struct {
	ID        uint      `gorm:"primaryKey;autoIncrement" json:"id"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
	DeletedAt int64     `gorm:"softDelete:unix;index" json:"-"`                   // 软删除字段(隐藏,仅数据库生效)
	Username  string    `gorm:"type:varchar(30);not null;unique" json:"username"` // 用户名
	Password  string    `gorm:"type:varchar(100);not null" json:"-"`              // 密码(隐藏返回)
	Email     string    `gorm:"type:varchar(50);unique" json:"email"`             // 邮箱
}

// 初始化数据库连接并自动迁移表结构
func InitDB() {
	// 1. 配置MySQL连接信息(替换为你的数据库账号、密码、数据库名)
	dsn := "root:Xe293358@tcp(127.0.0.1:3306)/book_manage?charset=utf8mb4&parseTime=True&loc=Local"

	// 2. 连接数据库
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info), // 打印SQL日志(开发环境)
	})
	if err != nil {
		panic("数据库连接失败:" + err.Error())
	}

	// 3. 自动迁移表结构(新增User表,原有表结构保持不变)
	err = db.AutoMigrate(&Category{}, &Book{}, &User{})
	if err != nil {
		panic("表结构迁移失败:" + err.Error())
	}

	// 4. 赋值全局DB对象
	sqlDB, _ := db.DB()
	// 设置数据库连接池参数
	sqlDB.SetMaxIdleConns(10)                  // 最大空闲连接数
	sqlDB.SetMaxOpenConns(100)                 // 最大打开连接数
	sqlDB.SetConnMaxLifetime(10 * time.Minute) // 连接最大存活时间

	DB = db
}
相关推荐
liuyunshengsir2 天前
golang Gin 框架下的大数据量 CSV 流式下载
开发语言·golang·gin
我不是8神7 天前
gin与gorm框架知识点总结
ios·iphone·gin
西京刀客8 天前
golang路由与框架选型(对比原生net/http、httprouter、Gin)
http·golang·gin
天天向上102414 天前
在 Go 的 Gin Web 框架中,获取 HTTP 请求参数有多种方式
前端·golang·gin
迷途的小子19 天前
go-gin binding 标签详解
java·golang·gin
L Jiawen20 天前
【Go · Gin】基础知识
开发语言·golang·gin
ChineHe21 天前
Gin框架基础篇009_日志中间件详解
golang·web·gin
昵称为空C22 天前
go+gin 入门指南
go·gin
乐观主义现代人22 天前
gin 框架学习之路
学习·gin