Gin 项目优化相册图片的上传和删除
本篇是一个优化篇, 针对 image.go 中的 crud , 目的还是适量拆分一些代码逻辑, 使其更清晰, 同时这个优化也是能力提升的最佳方式.
封装图片文件处理
封装为单独一个文件: api/handlers/image/dealUploadFile.go
package image
import (
"errors"
"io"
"log"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"youge.com/pkg/utils"
)
// 定义允许的图片后缀
var allowedExtensions = map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".gft": true,
".bmp": true,
".tiff": true,
".tif": true,
".svg": true,
".webp": true,
".heif": true,
".heic": true,
}
// 检查图片拓展名是否合法
func isOkImageExtension(filename string) bool {
ext := strings.ToLower(filepath.Ext(filename))
return allowedExtensions[ext]
}
// 准备图片上传: 包括文件大小检查, 拓展名, 文件处理等
func CheckImageForUpload(c *gin.Context, maxSize int64) (io.ReadCloser, string, error) {
// 先获取文件的一些元信息
file, err := c.FormFile("file")
if err != nil {
return nil, "", errors.New("获取文件失败")
}
// 验证文件扩展名
if !isOkImageExtension(file.Filename) {
return nil, "", errors.New("不支持的图片格式")
}
// 验证照片大小
if file.Size >= maxSize {
return nil, "", errors.New("图片太大了哈")
}
// 分离出文件名和后缀, cjj.png => cjj; png
fileName := filepath.Base(file.Filename)
// index := strings.LastIndex(fileName, ".")
// if index == -1 {
// return nil, "", errors.New("无法识别文件拓展名")
// }
// fileNameShort := fileName[:index]
// extensition := fileName[index+1:]
ext := filepath.Ext(fileName) // .png; .jpeg ....
// 用 uuid 生成新的文件名 => abcde.png
ossFileName := utils.GetUUID() + ext
// 读取文件流
reader, err := file.Open()
if err != nil {
return nil, "", errors.New("图片数据读取错误")
}
log.Println(ossFileName)
return reader, ossFileName, nil
}
这样在调用的时候就变成了:
// 图片上传校验, 调用封装的函数
reader, ossFileName, err := CheckImageForUpload(c, 10*1024*1024)
if err != nil {
utils.Error(c, 500, "", err)
return
}
defer reader.Close()
// 上传到 OSS
fileOssPath, err2 := utils.UploadImageFromReader(reader, ossFileName)
if err2 != nil {
utils.BadRequest(c, "上传 oss 失败")
return
}
// ...
配置信息封装在工具函数中
更常用的方式是用类似 .env 或者 yaml 等文件弄成环境变量来读取. 我不想搞, 就直接放工具函数得了.
pkg/utils/imageOss.go
// ....
// 统一前缀, 这里是 "shop-admin/" + "xxx"
func GetOssFilePath(ossFileName string) string {
return projectDir + "/" + ossFileName
}
这样接口中就由原来的 硬编码 改成了函数调用了.
// "shop-admin/" + "adfsfds.png"
ossFileName = utils.GetOssFilePath(ossFileName)
同理,
pkg/utils/tools.go
// 随机生成 uuid
func GetUUID() string {
newUuid, _ := uuid.NewRandom()
return newUuid.String()
}
数据库封装事务
只是尝试用 ai 搞了一下, 但始终无法完全隐藏细节.
internal/db/db.go
// 事务封装, 自动回滚, 支持返回值 T 泛型
// 事务封装,支持返回值 T
func WithTransaction[T any](fn func(*sqlx.Tx) (T, error)) (T, error) {
var zero T
tx, err := db.Beginx()
if err != nil {
return zero, err
}
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p)
}
}()
result, err := fn(tx)
if err != nil {
tx.Rollback()
return zero, err
}
if err := tx.Commit(); err != nil {
return zero, err
}
return result, nil
}
然后在删除图片的接口 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 路径
imageOssPath, err := db.WithTransaction[string](func(tx *sqlx.Tx) (string, error) {
var ossPath string
sql_01 := `select path from image where id = ?;`
if err := tx.Get(&ossPath, sql_01, req.Id); err != nil {
// 没有查询到也算错哈
utils.BadRequest(c, "获取图片失败")
return "", err
}
// 先删表关系
sql_02 := "delete from image where id = ?"
if _, err := db.Exec(sql_02, req.Id); err != nil {
utils.BadRequest(c, "删除图片失败")
return "", err
}
// 上面事务一致了, 再返回图片路径
return ossPath, nil
})
// 统一处理事务的报错
if 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": "删除成功",
})
}
但似乎在简单场景下, 不考虑多并发情况下, 也不需要用到事务的呀, 这里也是当做尝试一下而已.
基本优化就这些了, 更多优化后续持续展开, 比如这个查询分页的改进 (前端), 数据库查询单独分函数等.
耐心和恒心, 总会获得回报的.

浙公网安备 33010602011771号