fastGin脚手架——公共功能
图片上传 大小限制 白名单机制 文件名重复判断 package image_api import ( "fast_gin/global" "fast_gin/utils/find" "fas

fastGin脚手架——公共功能

发布时间:2025-02-27 (2025-02-27)
AI 文章总结
用 AI 快速提炼本文核心内容和阅读重点。

图片上传

  1. 大小限制
  2. 白名单机制
  3. 文件名重复判断
package image_api

import (
  "fast_gin/global"
  "fast_gin/utils/find"
  "fast_gin/utils/md5"
  "fast_gin/utils/random"
  "fast_gin/utils/res"
  "fmt"
  "github.com/gin-gonic/gin"
  "os"
  "path"
  "path/filepath"
  "strings"
)

var whiteList = []string{
  ".jpg",
  ".jpeg",
  ".png",
  ".webp",
}

func (ImageApi) UploadView(c *gin.Context) {
  fileHeader, err := c.FormFile("file")
  if err != nil {
    res.FailWithMsg("请选择文件", c)
    return
  }

  // 大小限制
  if fileHeader.Size > global.Config.Upload.Size*1024*1024 {
    res.FailWithMsg("上传文件过大", c)
    return
  }
  // 后缀判断
  ext := strings.ToLower(filepath.Ext(fileHeader.Filename))

  if !find.InList(whiteList, ext) {
    res.FailWithMsg("上传文件后缀非法", c)
    return
  }

  // 处理文件名重复
  // uploads/images/xx.jpg
  // uploads/images/xx_7hf.jpg
  fp := path.Join("uploads", global.Config.Upload.Dir, fileHeader.Filename)
  for {
    _, err1 := os.Stat(fp)
    if os.IsNotExist(err1) {
      break
    }
    // 文件存在
    // 算上传的图片和本身的图片是不是一样的,如果是一样的,那就直接返回之前的地址
    uploadFile, _ := fileHeader.Open()
    oldFile, _ := os.Open(fp)

    uploadFileHash := md5.MD5WithFile(uploadFile)
    oldFileHash := md5.MD5WithFile(oldFile)
    if uploadFileHash == oldFileHash {
      // 上传的图片,名称和内容都是一样的
      res.Ok("/"+fp, "上传成功", c)
      return
    }
    // 上传的图片,名称是一样的,但是内容不一样
    fileNameNotExt := strings.TrimSuffix(fileHeader.Filename, ext)
    newFileName := fmt.Sprintf("%s_%s%s", fileNameNotExt, random.RandStr(3), ext)
    fp = path.Join("uploads", global.Config.Upload.Dir, newFileName)
  }
  c.SaveUploadedFile(fileHeader, fp)

  res.Ok("/"+fp, "上传成功", c)
}

生成图片验证码

可以使用这个第三方库

https://github.com/mojocn/base64Captcha?tab=readme-ov-file

重点讲一下图片验证码后续的验证流程

生成图片验证码的时候,返回一串随机字符串和图片本身的base64编码

后端在内存中存储随机字符串对应正确的图片验证码

前端把图片渲染出来,需要验证的时候,就把用户输入的图片验证码和那一串随机字符串传给后端

验证的时候,根据随机字符串去匹配图片验证码是否正确

package captcha_api

import (
  "fast_gin/utils/captcha"
  "fast_gin/utils/res"
  "github.com/gin-gonic/gin"
  "github.com/mojocn/base64Captcha"
  "github.com/sirupsen/logrus"
)

type GenerateResponse struct {
  CaptchaID string `json:"captchaID"`
  Captcha   string `json:"captcha"`
}

func (CaptchaApi) GenerateView(c *gin.Context) {
  var driver = base64Captcha.DriverString{
    Width:           200,
    Height:          60,
    NoiseCount:      2,
    ShowLineOptions: 4,
    Length:          4,
    Source:          "0123456789",
  }
  cp := base64Captcha.NewCaptcha(&driver, captcha.CaptchaStore)
  id, b64s, _, err := cp.Generate()
  if err != nil {
    logrus.Errorf("图片验证码生成失败 %s", err)
    res.FailWithMsg("图片验证码生成失败", c)
    return
  }
  res.OkWithData(GenerateResponse{
    CaptchaID: id,
    Captcha:   b64s,
  }, c)
}

apifox显示base64的图片


//pm是apifox的内置对象,通过这个对象,可以获取到请求结果(response)
//通过.json()函数,获取到响应体中返回的json数据
let res = pm.response.json()

//定义一个模板,这个模板存的是
const template = `<html><img src="{{imgTemplate}}" /></html>`;

//构建img标签能识别的base64 url,注:如果接口返回的base64 url没有【data:image/png;base64,】则需要拼接进去,否则出不来图片。
//因接口返回的是一个数组,这里打印打一张图片
let img= res.data.captcha;
//console.log(img) //打印

// 设置 visualizer 数据。
//template:模板,上面const定义的template。
// {imgTemplate: img},imgTemplate对应的是template中src的值;img指的是上述定义的img base64 url
pm.visualizer.set(template, {
    imgTemplate: img
})

登录,登出

校验用户名密码,然后颁发token

package user_api

import (
  "fast_gin/global"
  "fast_gin/middleware"
  "fast_gin/models"
  "fast_gin/utils/captcha"
  "fast_gin/utils/jwts"
  "fast_gin/utils/pwd"
  "fast_gin/utils/res"
  "github.com/gin-gonic/gin"
  "github.com/sirupsen/logrus"
)

type LoginRequest struct {
  Username    string `json:"username" binding:"required" label:"用户名"`
  Password    string `json:"password" binding:"required" label:"密码"`
  CaptchaID   string `json:"captchaID"`
  CaptchaCode string `json:"captchaCode"`
}

func (UserApi) LoginView(c *gin.Context) {
  cr := middleware.GetBind[LoginRequest](c)

  if global.Config.Site.Login.Captcha {
    if cr.CaptchaID == "" || cr.CaptchaCode == "" {
      res.FailWithMsg("请输入图片验证码", c)
      return
    }
    if !captcha.CaptchaStore.Verify(cr.CaptchaID, cr.CaptchaCode, true) {
      res.FailWithMsg("图片验证码验证失败", c)
      return
    }
  }

  var user models.UserModel
  err := global.DB.Take(&user, "username = ?", cr.Username).Error
  if err != nil {
    res.FailWithMsg("用户名或密码错误", c)
    return
  }

  if !pwd.CompareHashAndPassword(user.Password, cr.Password) {
    res.FailWithMsg("用户名或密码错误", c)
    return
  }

  token, err := jwts.SetToken(jwts.Claims{
    UserID: user.ID,
    RoleID: user.RoleID,
  })
  if err != nil {
    logrus.Errorf("生成token失败 %s", err)
    res.FailWithMsg("登录失败", c)
    return
  }

  res.OkWithData(token, c)
  return
}

注销

将token放入redis黑名单,过期时间为token的失效时间

package redis_ser

import (
  "context"
  "fast_gin/global"
  "fast_gin/utils/jwts"
  "fmt"
  "github.com/sirupsen/logrus"
  "time"
)

func Logout(token string) {
  claims, err := jwts.CheckToken(token)
  if err != nil {
    return
  }
  key := fmt.Sprintf("logout_%s", token)
  sub := claims.ExpiresAt.Sub(time.Now())

  _, err = global.Redis.Set(context.Background(), key, "", sub).Result()
  if err != nil {
    logrus.Error(err)
  }
}

func HasLogout(token string) (ok bool) {
  key := fmt.Sprintf("logout_%s", token)
  _, err := global.Redis.Get(context.Background(), key).Result()
  if err == nil {
    return true
  }
  return false
}

用户列表

  1. 分页查询
  2. 模糊匹配
  3. 关键字查询
package user_api

import (
  "fast_gin/global"
  "fast_gin/middleware"
  "fast_gin/models"
  "fast_gin/utils/res"
  "github.com/gin-gonic/gin"
)

func (UserApi) UserListView(c *gin.Context) {
  var cr = middleware.GetBind[models.PageInfo](c)
  var list = make([]models.UserModel, 0)
  query := global.DB.Where("")

  if cr.Key != "" {
    query.Where("username like ?", "%"+cr.Key+"%")
  }

  offset := (cr.Page - 1) * cr.Limit

  global.DB.Where(query).Limit(cr.Limit).Offset(offset).Order(cr.Order).Find(&list)

  var count int64
  global.DB.Where(query).Count(&count)

  res.OkWithList(list, count, c)
}

通用列表查询

package common

import (
  "fast_gin/global"
  "fast_gin/models"
  "fmt"
  "gorm.io/gorm"
)

type QueryOption struct {
  models.PageInfo
  Likes    []string
  Where    *gorm.DB
  Preloads []string
  Debug    bool
}

func QueryList[T any](model T, option QueryOption) (list []T, count int64, err error) {
  list = make([]T, 0)
  // 自己身上的
  query := global.DB.Where(model)

  // 模糊匹配
  if option.Key != "" {
    if len(option.Likes) != 0 {
      likeQuery := global.DB.Where("")
      for _, column := range option.Likes {
        likeQuery.Or(fmt.Sprintf("%s like ?", column), fmt.Sprintf("%%%s%%", option.Key))
      }
      query.Where(likeQuery)
    }
  }

  // 预加载
  for _, preload := range option.Preloads {
    query = query.Preload(preload)
  }

  // 分页
  if option.Page <= 0 {
    option.Page = 1
  }
  if option.Limit <= 0 {
    option.Limit = -1
  }

  offset := (option.Page - 1) * option.Limit

  if option.Order == "" {
    option.Order = "created_at desc"
  }

  db := global.DB.Where("")
  if option.Debug {
    db = db.Debug()
  }

  db.Where(query).Limit(option.Limit).Offset(offset).Order(option.Order).Find(&list)
  db.Model(model).Where(query).Count(&count)
  return
}

如何使用

func (UserApi) UserListView(c *gin.Context) {
  var cr = middleware.GetBind[models.PageInfo](c)

  list, count, _ := common.QueryList(models.UserModel{}, common.QueryOption{
    PageInfo: cr,
    Likes:    []string{"username", "nickname"},
    Debug:    true,
  })
  res.OkWithList(list, count, c)
}