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 的方式就就行. 下篇的图片上传管理等还是有点复杂的, 接着看.

浙公网安备 33010602011771号