Gin 实现基础 CRUD 接口

前面2篇讲了关于 gin + mysql + jwt + rbac 等基础 web搭建操作, 主要目的还是学习 go 语言的一些应用工具,
然后本篇继续来实现一个名为 notice 的公告模块, 包含数据的增删查改, 这个几乎是咱们讨饭的重点, 当然接口逻辑都是比较简单, 体验一下就好.

项目目录

我是在 mac 终端下, 用命令行来模拟一下 tree 的功能, 体现目录结构.

cj@mini gin-shop-admin % 
find . -path './.git' -prune -o -print | sed -e 's;[^/]*/;|____;g;s;____|; |;g'
.
|____go.mod
|____internal
| |____middleware
| | |____auth.go
| |____db
| | |____db.go
|____go.sum
|____api
| |____routers.go
| |____handlers
| | |____auth
| | | |____views.go
| | |____user
| | | |____menu.go
| | | |____views.go
| | |____notice
| | | |____views.go
| | |____rule
| | | |____views.go
|____.vscode
| |____extensions.json
|____tmp
| |____runner-build
|____main.go
|____pkg
| |____utils
| | |____response.go
| | |____tools.go
| |____jwtt
| | |____jwt.go
cj@mini gin-shop-admin % 

数据库表

主要是 notice 表, 字段如下:

-- ###### 公告模块 
drop table if exists notice;
create table notice (
	id int not null auto_increment primary key comment '自增id'
	, title varchar(200) not null comment '公告标题'
	, content text not null comment '公告内容'
	, orders int not null default 50
	, create_time datetime default current_timestamp
	, update_time datetime default current_timestamp on update current_timestamp
);

模块路由

api/routers.go

package api

import (
	"github.com/gin-gonic/gin"
	"youge.com/api/handlers/auth"
	"youge.com/api/handlers/notice"
  
	"youge.com/internal/middleware"
)

// 统一注册入口
func RegisterAllRouters(r *gin.Engine) {
	// 登录认证模块
	authGroup := r.Group("/api/auth")
	{
		// auth.POST("/register", Register)
		authGroup.POST("/login", auth.Login)
	}

	// 用户管理模块

	// 公告模块
	noticeGroup := r.Group("api/notice")
	noticeGroup.Use(middleware.JWT()) // 也是需要 token 认证
	{
		noticeGroup.GET("/notice", notice.GetNoticeList)              // get
		noticeGroup.POST("/notice", notice.CreateNotice)              // add
		noticeGroup.PUT("/notice/:id", notice.UpdateNotice)           // update
		noticeGroup.DELETE("/notice/:id/delete", notice.DeleteNotice) // del
	}
	
  // ... 更多模块

}

对应的接口逻辑实现都放在 /api/handlers/notice/views.go 里面, 不做分层了哈, 直接干.

GET - 查询公告接口

前端: http://localhost:8000/api/notice/notice?page=1&limit=10

后端: /api/handlers/notice/views.go => GetNoticeList

  • 分页查询, 小数据版的 limit, offset 实现
  • 请求参数验证 + 默认值实现
package notice

import (
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"youge.com/internal/db"
	"youge.com/pkg/utils"
)

// 公告表
type Notice struct {
	ID        int       `db:"id" json:"id"`
	Title     string    `db:"title" json:"title"`
	Content   string    `db:"content" json:"content"`
	Orders    string    `db:"orders" json:"orders"`
	CreatedAt time.Time `db:"create_time" json:"create_time"`
	UpdatedAt time.Time `db:"update_time" json:"update_time"`
}

// 请求参数处理, omitempty, 表示可选
type NoticeRequest struct {
	Page  int `form:"page" binding:"omitempty,min=1"`
	Limit int `form:"limit" binding:"omitempty,min=1,max=1000"`
}

// 接口: 获取通知列表
func GetNoticeList(c *gin.Context) {

	// 绑定并验证参数
	var req NoticeRequest
	if err := c.ShouldBindQuery(&req); err != nil {
		utils.BadRequest(c, "请求参数错误")
		return
	}

	// 设置默认值, page=1, limit=10
	if req.Page == 0 {
		req.Page = 1
	}

	if req.Limit == 0 {
		req.Limit = 10
	}

	// 分页查询数据
	// 页面偏移量
	offset := (req.Page - 1) * req.Limit
	mysql := `
	select 
		id
		, title
		, content
		, orders
		, create_time
		, update_time
	from notice 
	order by create_time desc
	limit ?, ?;
	`
	var noticeList []Notice
	if err := db.Query(&noticeList, mysql, offset, req.Limit); err != nil {
		utils.BadRequest(c, "获取数据失败")
		return
	}

	// 查询总条数
	var total int
	// 这个sql 肯定不会错, 除非表不在, 不校验了
	db.Get(&total, "select count(*) as total from notice;")

	// 返回结果
	utils.Success(c, gin.H{
		"list":       noticeList,
		"totalCount": total,
		"limit":      req.Limit,
	})
}

补充参数验证的工具函数

主要为了对标一下 fastapi 这个我觉得超级棒的 web 框架, 现在放弃的原因主要是, go 可能性能更为主流.

这里安装一下第三方库, 它和 Python 的 pydantic 差不多呢.

go get "github.com/go-playground/validator/v10"

工具函数放在了 pkg/utils/tools.go 中了.

package utils

import (
	"errors"

	"github.com/gin-gonic/gin"
	"github.com/go-playground/validator/v10"
)

// 验证和绑定前端 json 请求体
func BindAndValidate(c *gin.Context, request interface{}) error {
	// 1. 绑定请求体到结构体
	if err := c.ShouldBindJSON(request); err != nil {
		return err
	}

	// 2. 验证结构体字段
	validate := validator.New()
	if err := validate.Struct(request); err != nil {
		return err
	}

	return nil
}

POST - 新增公告接口

前端: http://localhost:8000/api/notice/notice

前端请求体 json : {"title": "test01", "content": "test123"}

后端: /api/handlers/notice/views.go => CreateNotice

func CreateNotice(c *gin.Context) {
	// 验证前端传过来的 json 参数
	var req struct {
		Title   string `json:"title" validate:"required,min=1,max=200"`
		Content string `json:"content" validate:"required"`
	}

	if err := utils.BindAndValidate(c, &req); err != nil {
		utils.BadRequest(c, "请求参数错误")
		return
	}

	// 数据写入数据库
	mysql := `
	insert into notice(title, content) 
	values (?, ?);
	`
	rows, err := db.Exec(mysql, req.Title, req.Content)
	if err != nil {
		utils.BadRequest(c, "新增公告失败")
		return
	}

	utils.Success(c, gin.H{
		"affectedRows": rows,
	})
}

PUT - 修改公告接口

前端 : http://localhost:8000/api/notice/notice/9

后端: /api/handlers/notice/views.go => UpdateNotice

func UpdateNotice(c *gin.Context) {
	// 获取并校验路径参数
	idStr := c.Param("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		utils.BadRequest(c, "请求参数错误")
		return
	}

	// 根据 id 修改数据库内容
	// 直接通过 var 定义的结构体是匿名的, 用于局部, 而 type 是整个包内
	var req struct {
		Title   string `json:"title" validate:"required,min=1,max=200"`
		Content string `json:"content" validate:"required"`
	}

	if err := utils.BindAndValidate(c, &req); err != nil {
		utils.BadRequest(c, "请求参数错误")
		return
	}

	mysql := `
	update notice 
	set title = ?, content = ? 
	where id = ?;
	`
	rows, err := db.Exec(mysql, req.Title, req.Content, id)
	// id 不存在 或者 更新异常都提示失败
	if err != nil || rows == 0 {
		utils.BadRequest(c, "更新数据错误")
		return
	}

	utils.Success(c, gin.H{
		"affectedRows": rows,
	})
}

DELETE - 删除公告接口

前端 : http://localhost:8000/api/notice/notice/9/delete

后端: /api/handlers/notice/views.go => DeleteNotice

func DeleteNotice(c *gin.Context) {
  
	idStr := c.Param("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		utils.BadRequest(c, "请求参数错误")
		return
	}
	// 根据 id 进行删除, 错误就不处理得了, 不会错的
	rows, _ := db.Exec("delete from notice where id = ?", id)
	utils.Success(c, gin.H{
		"affectedRows": rows,
	})
}

至此, 一个常用的 CRUD 接口就写完了, 其他模块都是大同小异的, 只是复杂度不同而已.

posted @ 2025-05-05 18:17  致于数据科学家的小陈  阅读(58)  评论(0)    收藏  举报