因为 依赖的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
}