Gin 实现相册模块的 CRUD 接口 (下)
前篇的是 相册管理 比较简单, 几乎没有任何的技能点, 然后本篇就涉及到本地图片的上传, 解析, 校验,
再上传 阿里云OSS 等, 最后用数据库来记录本地图片和远程图片的联系, 算是比较通用和实用的功能吧.
项目目录
我是在 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 %
先不干, 突出一本篇的知识点再说.
OSS 统一封装
再写接口之前, 先突出下 OSS 管理这部分, 就首先还是要自己先买一个 阿里云OSS 服务, 即对象存储, 存图片, 文件啥的都行, 也很便宜, 搞一搞. 然后就是看它提供的 SDK 各主流语言都有, 或者像我这种直接 AI 帮我搞就行, 测一下能用就ok.
放在工具函数模块了, 即 pkg/utils/ossImage.go 中了
package utils
import (
"io"
"path/filepath"
"sync"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)
// 全局变量声明
var (
client *oss.Client
bucket *oss.Bucket
once sync.Once
errInit error
endpoint string = "https://oss-cn-shenzhen.aliyuncs.com"
projectDir string = "shop-admin"
bucketName string = "yyy" // 人工创建的
accessKeyId string = "申请即可"
accessKeySecret string = "也是申请哈"
)
// 初始化 oss 客户端 和 bucket (懒加载)
func initOSS() {
client, errInit = oss.New(endpoint, accessKeyId, accessKeySecret)
if errInit != nil {
return
}
bucket, errInit = client.Bucket(bucketName)
}
// 封装上传图片的方法
// 场景 01: 前端传的是本地文件路径, 后端来读取文件上传
func UploadImageFromFile(localFilePath, objectPath string) error {
// 上传是时才加载一下
once.Do(initOSS)
if errInit != nil {
return errInit
}
// 获取文件名 "/abc/d/cj.png" => cj.png
fileName := filepath.Base(localFilePath)
objectName := filepath.Join(objectPath, fileName)
return bucket.PutObjectFromFile(objectName, localFilePath)
}
// 场景 02: 前端传的是一个二进制流, 表单传递等
func UploadImageFromReader(reader io.Reader, objectPath string) (string, error) {
// 上传时才加载一下
once.Do(initOSS)
if errInit != nil {
return "", errInit
}
// 拼接上项目路径
objectPath = projectDir + "/" + objectPath
// 使用 PutObject 上传
err := bucket.PutObject(objectPath, reader)
// 拼接文件完整的 oss 路径用来存储
fileOssPath := "https://" + bucketName + "." + endpoint[8:] + "/" + objectPath
return fileOssPath, err
}
func DeleteImageFromOss(ossFilePath string) error {
once.Do(initOSS)
if errInit != nil {
return errInit
}
// 删除 oss 文件, 根据oss中, 从 bucket 之后的路径
err := bucket.DeleteObject(ossFilePath)
return err
}
我当前用的基本就是, 从本地上传图片, 即后端来处理; 或者从前端传二级制文件数据流 过来, 后端处理, 还有删除图片这几个方法就可以了. 代码就基本按 AI 写的, 没啥说的.
数据库表
本篇部分主要涉及 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)
// 图片相关
imageGroup.POST("/image/upload", image.UploadImage)
imageGroup.GET("/image_class/:class_id/image/:page", image.GetImagesByclass)
imageGroup.PUT("/image/:id/update", image.UpdateImageName)
imageGroup.DELETE("/image/:id/delete", image.DeleteImage)
}
图片的对应的接口逻辑实现都放在 /api/handlers/image/image.go 里面, 也是不做分层啦.
POST 新增图片接口
前端: http://localhost:8000/api/image/image/upload
前端 FormData, 包含 image_class_id 和一个 二进制流数据
后端: /api/handlers/image/image.go => UploadImage
package image
import (
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"youge.com/internal/db"
"youge.com/pkg/utils"
)
// 02: 图片信息表
type Image struct {
Id int `db:"id" json:"id"`
Url string `db:"url" json:"url"`
Name string `db:"name" json:"name"`
Path string `db:"path" json:"path"`
ImageClassId int `db:"image_class_id" json:"image_class_id"`
}
// 接口: 上传图片到某个相册中
func UploadImage(c *gin.Context) {
// 前端通过 FormData 传递二进制图片数据过来
// 获取相册 class_id
//classId := c.PostForm("class_id")
var req struct {
ClassId int `form:"class_id" validate:"required,min=1"`
}
if err := utils.BindAndValidate(c, &req); err != nil {
utils.BadRequest(c, "获取相册失败")
return
}
// 先获取文件的一些元信息
file, err := c.FormFile("file")
if err != nil {
utils.BadRequest(c, "获取文件失败")
return
}
// 限制上传照片最大为 10M
if file.Size >= (10 * 1024 * 1024) {
utils.BadRequest(c, "图片大小不能超过10M")
return
}
// 分离出文件名和后缀, cjj.png => cjj; png
fileName := filepath.Base(file.Filename)
index := strings.LastIndex(fileName, ".")
if index == -1 {
return
}
fileNameShort := fileName[:index] // cjj
extensition := fileName[index+1:] // png
ossFileName := utils.GetUUID() + "." + extensition // sdfsdfsfs.png
// 读取文件流, 上传 oss
ioReader, err := file.Open()
if err != nil {
utils.BadRequest(c, "图片数据读取错误")
return
}
defer ioReader.Close()
// 上传到 OSS
fileOssPath, err2 := utils.UploadImageFromReader(ioReader, ossFileName)
if err2 != nil {
utils.BadRequest(c, "上传 oss 失败")
return
}
// 硬编码 ossFileName 带上 bucketName, 因为是自己用就不搞匹配了
ossFileName = "shop-admin/" + ossFileName
// 最后记录信息到数据库中
mysql := `
insert into image(url, name, path, image_class_id)
values (?, ?, ?, ?);
`
if _, err := db.Exec(mysql, fileOssPath, fileNameShort, ossFileName, req.ClassId); err != nil {
utils.BadRequest(c, "存数据库失败")
return
}
utils.Success(c, gin.H{
"msg": "上传成功",
"class_id": req.ClassId,
})
}
这个逻辑还是有点长的, 设计文件读取和处理, 还有 oss 上传, 后续可以将文件处理部分单独抽离出来, 不然这样的代码后续很难调试, 然后也不够完善, 比如文件格式验证等都没有做 (因为我前端做了, 后端就偷懒了哈哈哈)
额这里就先不搞了, 下篇优化吧.
GET 查询图片某个相册接口
前端: http://localhost:8000/api/image/image_class/1/image/1?page=1&limit=10
后端: /api/handlers/image/image.go => GetImagesByclass
// 接口: 获取某个相册下的所有图片
func GetImagesByclass(c *gin.Context) {
// 统一验证 uri, query, form, json 等
var req struct {
ClassId int `uri:"class_id" validate:"required,min=1"`
Page int `uri:"page" validate:"required,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
, url
, name
, path
, image_class_id
from image
where image_class_id = ?
limit ?, ?;
`
var imageList []Image
if err := db.Query(&imageList, mysql, req.ClassId, offset, req.Limit); err != nil {
utils.BadRequest(c, "获取数据失败")
return
}
// 查询总条数
var total int
// 这个sql 肯定不会错, 除非表不在, 不校验了
sql2 := "select count(*) as total from image where image_class_id = ?"
db.Get(&total, sql2, req.ClassId)
// 返回结果
utils.Success(c, gin.H{
"list": imageList,
"totalCount": total,
"limit": req.Limit,
})
}
修改图片名称接口
前端: http://localhost:8000/api/image/image/3/update
前端请求体 json :
{
"name": "去自由",
}
后端: /api/handlers/image/image.go => UpdateImageName
// 接口: 修改某张图片的短名称
func UpdateImageName(c *gin.Context) {
// 校验路径参数和请求体参数
var req struct {
Id int `uri:"id" validate:"required,min=1"`
Name string `json:"name" validate:"required"`
}
if err := utils.BindAndValidate(c, &req); err != nil {
utils.BadRequest(c, "获取图片失败")
return
}
// 更新图片名称, 根据id
sql := `update image set name = ? where id = ?;`
if _, err := db.Exec(sql, req.Name, req.Id); err != nil {
utils.BadRequest(c, "修改图片名称失败")
return
}
utils.Success(c, gin.H{
"msg": "修改成功",
})
}
就改一个字段而已, 没啥
DELETE 删除图片接口
前端: http://localhost:8000/api/image/image/6/delete
后端: /api/handlers/image/image.go => DeleteImage
// 接口: 删除某张图片
func DeleteImage(c *gin.Context) {
var req struct {
Id int `uri:"id" validate:"required,min=1"`
}
if err := utils.BindAndValidate(c, &req); err != nil {
utils.BadRequest(c, "获取图片失败")
return
}
// 因为要涉及到 oss 的删除, 所有得先查询到图片的 oss 中 bucket 路径
var imageOssPath string
sql_01 := `select path from image where id = ?;`
if err := db.Get(&imageOssPath, sql_01, req.Id); err != nil {
// 没有查询到也算错哈
utils.BadRequest(c, "获取图片失败")
return
}
// 先删表关系
sql_02 := "delete from image where id = ?"
if _, err := db.Exec(sql_02, req.Id); err != nil {
utils.BadRequest(c, "删除图片失败")
return
}
// 再删除远程的 oss
if err := utils.DeleteImageFromOss(imageOssPath); err != nil {
utils.BadRequest(c, "删除图片 oss 失败")
return
}
utils.Success(c, gin.H{
"msg": "删除成功",
})
}
这里的删除其实还是有点问题的, 就如果本地数据删了, 然后远程删除失败了, 那就没法回滚了, 应该要用事务处理的, 但这个项目毕竟是自己搞着玩就算了吧, 后面用 AI 来搞一下即可.

浙公网安备 33010602011771号