Gin 实现相册模块的 CRUD 接口 (上)

前面的 菜单管理, 角色管理 都是基于 RBAC权限 的练习展开的, 主要的技能点还是 SQL 的理解和 JSON 的处理.

本篇要实现的一个应用功能模块, 即 相册管理, 包含两部分, 相册的增删改查, 以及某个相册下图片的增删改查,

技能点除了 SQL 外, 图片部分还实现上传云服务器, 这里用 阿里云OSS 的 SDK, 本篇先引入相册管理, 一步步来吧.

项目目录

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

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
| | |____role
| | | |____views.go
| | |____auth
| | | |____views.go
| | |____user
| | | |____menu.go
| | | |____views.go
| | |____image
| | | |____views.go
| | | |____image.go
| | |____notice
| | | |____views.go
| | |____rule
| | | |____views.go
|____.vscode
| |____extensions.json
|____tmp
| |____runner-build
|____main.go
|____pkg
| |____utils
| | |____response.go
| | |____tools.go
| | |____imageOss.go
| |____jwtt
| | |____jwt.go
cj@mini gin-shop-admin % 

也是直接开干!

数据库表

上篇部分主要涉及 image_class

-- 相册分类
drop table if exists image_class;

create table image_class(
	id int auto_increment primary key comment '自增id'
	, name varchar(50) default null comment '图库分类名称'
	, orders int default 50 comment '排序'
		, create_time datetime default current_timestamp
	, update_time datetime default current_timestamp on update current_timestamp
);

删除部分也会用到 image 表, 即相册下的图片, 更多逻辑放下篇了.

-- 图片信息
drop table if exists image;

create table image(
	id int auto_increment primary key comment '自增id'
	, url varchar(255) default null comment '图片oss链接'
	, name varchar(50) default null comment '图片名称'
	, path varchar(255) default null comment '图片oss短路径'
	, image_class_id int default null comment '相册id'
	, 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/image"
	"youge.com/internal/middleware"
)

// 统一注册入口
func RegisterAllRouters(r *gin.Engine) {
	// 登录认证模块

	// 用户管理模块

	// 公告模块

	// 菜单权限管理模块

	// 角色管理模块

	// 图库管理模块
	imageGroup := r.Group("api/image")
	// ruleGroup.Use(middleware.JWT())
	{
		// 图库相关
		imageGroup.GET("/image_class", image.GetImageClass)
		imageGroup.POST("/image_class", image.CreateImageClass)
		imageGroup.PUT("/image_class/:id", image.UpdateImageClass)
		imageGroup.DELETE("/image_class/:id/delete", image.DeleteImageClass)

		// todo: 图片相关

}

对应的接口逻辑实现都放在 /api/handlers/image/views.go 里面, 也是不做分层啦.

GET 查询相册接口

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

后端: /api/handlers/image/views.go => GetImageClassList

package image

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

// 01: 相册信息表
type ImageClass struct {
	Id     int    `db:"id" json:"id"`
	Name   string `db:"name" json:"name"`
	Orders int    `db:"orders" json:"orders"`
}

// 接口: 获取相册明细
func GetImageClass(c *gin.Context) {
	// 分页查询参数验证
	var req struct {
		Page  int `form:"page" validate:"omitempty,min=1"`
		Limit int `form:"limit" validate:"omitempty,min=1,max=1000"`
	}

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

	// 设置默认值, ?page=1&limit=10
	req.Page, req.Limit = utils.SetDefaultPagination(req.Page, req.Limit)

	// 分页查询数据
	offset := (req.Page - 1) * req.Limit
	mysql := `
	select 
	  id 
		, name 
		, orders
	from image_class
	order by id
	limit ?, ?;
	`

	var imageClassList []ImageClass
	if err := db.Query(&imageClassList, mysql, offset, req.Limit); err != nil {
		utils.BadRequest(c, "获取数据失败")
		return
	}

	// 查询总条数, 用于前端分页
	var total int
	db.Get(&total, "select count(*) from image_class;")

	utils.Success(c, gin.H{
		"list":       imageClassList,
		"totalCount": total,
	})
}

这里对分页查询的重复代码有进行了一定封装, internal/utils/tools.go 中.

// 统一设置分页查询的默认值 ?page=1&limit=10
func SetDefaultPagination(page, limit int) (int, int) {
	if page == 0 {
		page = 1
	}

	if limit == 0 {
		limit = 1
	}

	return page, limit
}

然后对统一验证函数进行了拓展, 按照路径参数, 请求参数, 表单参数, json 参数等进行统一校验.

也是在 internal/utils/tools.go 中.

// 验证和绑定: uri, request, form, json ...等前端参数
func BindAndValidate(c *gin.Context, request interface{}) error {
	// 支持多种验证
	validate := validator.New()

	// 01: 尝试绑定, 路径参数 (URI)
	if err := c.ShouldBindUri(request); err != nil {
		if err := validate.Struct(request); err != nil {
			return err
		}
	}

	// 02: 尝试绑定, 查询参数 (Query)
	if err := c.ShouldBindQuery(request); err != nil {
		if err := validate.Struct(request); err != nil {
			return err
		}
	}

	// 03: 尝试绑定, 表单数据 (post 表单等)
	if err := c.ShouldBind(request); err != nil {
		if err := validate.Struct(request); err != nil {
			return err
		}
	}

	// 还能加其他 ...

	// 最后尝试绑定 json 请求体
	if err := c.ShouldBindJSON(request); err != nil {
		if err := validate.Struct(request); err != nil {
			return err
		}
	}

	return nil
}

POST 新增相册接口

前端: http://localhost:8000/api/image/image_class

前端请求体 json :

{
    "name": "旅游",
    "orders": "",
}

后端: /api/handlers/image/views.go => CreateImageClass

// 接口: 新增相册分类
func CreateImageClass(c *gin.Context) {
	// 验证前端 body 传过来的 json 数据
	var req struct {
		Name   string `json:"name" validate:"required"`
		Orders int    `json:"orders" validate:"omitempty,min=10"`
	}
	if err := utils.BindAndValidate(c, &req); err != nil {
		utils.BadRequest(c, "请求体参数错误")
		return
	}
	// 设置默认值
	if req.Orders == 0 {
		req.Orders = 50
	}

	// 新增数据
	sql := "insert into image_class(name, orders) values (?, ?);"
	if _, err := db.Exec(sql, req.Name, req.Orders); err != nil {
		utils.BadRequest(c, "新增数据失败")
		return
	}

	utils.Success(c, gin.H{
		"msg": "新增成功",
	})
}

也是主要进行参数验证后, 新增数据即可, 没啥难度, 纯体力活.

PUT 修改相册接口

前端: http://localhost:8000/api/image/image_class/3

前端请求体 json :

{
    "name": "去自由",
    "orders": "100"
}

后端: /api/handlers/image/views.go => UpdateImageClass

// 接口: 修改相册分类
func UpdateImageClass(c *gin.Context) {
	var req struct {
		Id     int    `uri:"id" validate:"required,min=1"`
		Name   string `json:"name" validate:"required"`
		Orders int    `json:"orders" validate:"omitempty,min=10"`
	}
	if err := utils.BindAndValidate(c, &req); err != nil {
		utils.BadRequest(c, "请求体参数错误")
		return
	}

	// 设置默认值
	if req.Orders == 0 {
		req.Orders = 50
	}

	sql := `update image_class set name = ?, orders = ? where id = ?`
	if _, err := db.Exec(sql, req.Name, req.Orders, req.Id); err != nil {
		utils.BadRequest(c, "修改失败")
		return
	}

	utils.Success(c, gin.H{
		"msg": "修改成123",
	})
}

DELETE 删除相册接口

前端: http://localhost:8000/api/image/image_class/3/delete

后端: /api/handlers/image/views.go => DeleteImageClass

// 接口: 删除相册分类
func DeleteImageClass(c *gin.Context) {
	// 校验分类id
	var req struct {
		Id int `uri:"id" validate:"required,min=1"`
	}
	if err := utils.BindAndValidate(c, &req); err != nil {
		utils.BadRequest(c, "请求体参数错误")
		return
	}

	// 删除分类前检查如果里面还有图片, 则不让删
  // todo: 看看有无更优化的方式
	var count int
	sql_01 := "select count(*) from image where image_class_id = ?"
	if err := db.Get(&count, sql_01, req.Id); err != nil {
		utils.BadRequest(c, "获取该相册下图片失败")
		return
	}

	if count > 0 {
		utils.BadRequest(c, "请先删除里面图片哦")
		return
	}

	sql_02 := `delete from image_class where id = ?;`
	if _, err := db.Exec(sql_02, req.Id); err != nil {
		utils.BadRequest(c, "删除失败")
		return
	}

	utils.Success(c, gin.H{
		"msg": "删除成功",
		"cj":  count,
	})

}

上篇的相册管理这部分就是开胃小菜, 没啥难度, 唯一的改进点就是参数验证, 后续都统一手动验证 struct + validate 的方式就就行. 下篇的图片上传管理等还是有点复杂的, 接着看.

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