GoFrame电商项目上传文件至七牛云详解

发布于:2025-06-22 ⋅ 阅读:(25) ⋅ 点赞:(0)
func (s *sFile) Upload(ctx context.Context, in model.FileUploadInput) (out *model.FileUploadOutput, err error) {
	//定义图片上传位置
	uploadPath := g.Cfg().MustGet(ctx, "upload.path").String()
	if uploadPath == "" {
		return nil, gerror.New("读取配置文件失败 上传路径不存在")
	}
	if in.Name != "" {
		in.File.Filename = in.Name
	}
	//安全性校验:每个人1分钟内只能上传10次
	count, err := dao.FileInfo.Ctx(ctx).
		Where(dao.FileInfo.Columns().UserId, gconv.Int(ctx.Value(consts.CtxAdminId))).
		WhereGTE(dao.FileInfo.Columns().CreatedAt, gtime.Now().Add(-time.Minute)).Count()
	if err != nil {
		return nil, err
	}
	//避免在代码中写死常量 抽取出去
	if count >= consts.FileMaxUploadCountMinute {
		return nil, gerror.New("上传频繁,1分钟内只能上传10次")
	}
	//	定义年月日 Ymd
	dateDirName := gtime.Now().Format("Ymd")
	//gfile.Join 用"/"拼接
	fileName, err := in.File.Save(gfile.Join(uploadPath, dateDirName), in.RandomName)
	if err != nil {
		return nil, err
	}
	//	4. 入库
	data := entity.FileInfo{
		Name:   fileName,
		Src:    gfile.Join(uploadPath, dateDirName, fileName),
		Url:    "/upload/" + dateDirName + "/" + fileName, //和上面gfile.Join()效果一样
		UserId: gconv.Int(ctx.Value(consts.CtxAdminId)),
	}
	id, err := dao.FileInfo.Ctx(ctx).Data(data).OmitEmpty().InsertAndGetId()
	if err != nil {
		return nil, err
	}
	return &model.FileUploadOutput{
		Id:   uint(id),
		Name: data.Name,
		Src:  data.Src,
		Url:  data.Url,
	}, nil
}

YAML文件:

qiniu:
  bucket: "x"
  accessKey: "x"
  secretKey: "x"
  url: "http://x.x.x/"

upload:
  path: "upload"

拿这一行代码当成一句“去仓库取快递单、拆包、拿出里面写着的上传目录”来看,分豆腐块讲完。

uploadPath := g.Cfg().MustGet(ctx, "upload.path").String()
  1. g 是啥?
    • GoFrame 的核心包,提供全局工具。
    • 这里用到的是 g.Cfg()——全局 配置中心

  2. g.Cfg() 做了什么?
    • 读取项目根目录里的 config.yaml / config.toml / config.json 等配置文件;
    • 把它们解析后存在内存里,方便随时取值。
    – 举个 config.yaml 片段:

    upload:
      path: "./upload/"   # ← 这就是我们要拿的值
    
  3. .MustGet(ctx, "upload.path")
    Get 表示“按键名取配置值”,键用 点号 表示层级。
    "upload.path" 就等同于 YAML 里的 upload: {path: ...}
    MustGetGet 多了一个“必须要拿到”的语义:
    – 找到了 → 正常返回;
    – 找不到 → 直接 panic,程序启动时就会报错,防止运行过程中才发现配置缺失。
    • 第二参 ctx:为了支持热加载、分环境覆盖等场景,这里仍然要求传 context.Context。初学可理解为“附带一张工单”,此处没有超时之类特殊用法。

  4. .String()
    MustGet 返回的是 GoFrame 的万能容器 *gvar.Var,里面可以装任何类型;
    • 调 .String() 把它安全地转成 字符串
    • 如果 YAML 中 path 写的是 ./upload/,这里就得到 "./upload/"

  5. uploadPath := …
    := 短变量声明,把结果赋值给变量 uploadPath
    • 以后代码会拿它去做:

    file.Save(uploadPath + newFileName)
    
  6. 为什么这么写而不是写死路径?
    环境差异:本地开发放 ./upload/,线上服务器可能放 /data/files/; 用配置文件改一行即可。
    可读性 & 维护:所有路径、端口、密钥都集中在 config.yaml,新同学一眼就能看到。
    出错早发现MustGet 缺值直接 panic,部署阶段就能踩出坑,避免线上才报“路径为空”。

  7. “小白级”生活比喻

    • g.Cfg() → 打开家里的“说明书”抽屉;
    • MustGet("upload.path") → 找到“上传文件存放在哪个房间”那一行;
    • .String() → 把房间号抄成一张便签;
    • uploadPath := … → 以后搬箱子(保存文件)都拿这张便签,不用死记硬背。

一句话总结
这行代码就是从配置文件里读取“上传文件存储目录”的字符串,存进变量 uploadPath。如果配置缺失,程序立即报错,确保后面保存文件时不会因为路径为空而崩溃或把文件乱丢。

代码片段

if in.Name != "" {          // ① 如果前端额外传了一个“文件名”字段
    in.File.Filename = in.Name // ② 就把它写进上传文件的 Filename 属性里
}

把它当成“给上传的文件改名”来讲,三步就懂。

────────────────────────────────────────
一、先认清每个变量是谁

  1. in

    • 整个上传接口的入参结构体,举例(伪代码):
      type FileUploadInput struct {
          Name string             // 前端可选:希望保存成什么文件名
          File *ghttp.UploadFile  // GoFrame 封装的上传文件信息
      }
      
  2. in.Name

    • 前端表单里额外传的 想要的文件名,比如 "avatar_123.jpg"
    • 如果前端没传,就会是空字符串 ""
  3. in.File

    • GoFrame 帮你把 <input type="file"> 上传的文件解析成 *ghttp.UploadFile
    • 其中的 Filename 默认是 客户端原始文件名(如 "IMG_0001.JPG")。

────────────────────────────────────────
二、代码做了什么?

步骤 发生的事 目的
if in.Name != "" 判断前端有没有主动指定文件名
in.File.Filename = in.Name 如果有,就把默认文件名覆写成指定名字

也就是:“前端想改名就尊重前端;前端不管就保持原名。”

────────────────────────────────────────
三、为什么要让前端改名字?

  1. 业务需要
    • 比如头像上传时,后端希望所有头像都叫 avatar_<userId>.jpg,前端就能在 Name 字段里直接传好。
  2. 防乱码 / 空格
    • 客户端原名可能带空格或中文,存储系统不友好;前端预处理后告诉后端保存为什么名字。
  3. 避免重名冲突
    • 同一个用户多次上传 logo.png,如果你没加时间戳或随机数,直接覆盖。前端可改成 logo_20240622.png

────────────────────────────────────────
四、生活比喻

  • 你去照相馆洗照片 (File)。
  • 如果你什么都不说,老板会用原始文件名打标签 (IMG_1234.JPG)。
  • 但你可以告诉老板:“帮我写成 毕业合影.jpg 啊!” (Name)
  • 老板听到了,就把贴纸改成你的要求。

一句话总结
这三行代码就是:在保存上传文件前,若调用方(前端/其它服务)指定了想要的文件名,就用它覆盖默认文件名;否则保持原样。这样既灵活又防止后端强行重命名带来的麻烦。

把这一句当成“去账本里数一数:这一分钟内,这位管理员上传了几次文件”来看,按毫无编程基础的节奏一小格一小格解释。

原代码

count, err := dao.FileInfo.Ctx(ctx).
    Where(dao.FileInfo.Columns().UserId, gconv.Int(ctx.Value(consts.CtxAdminId))).
    WhereGTE(dao.FileInfo.Columns().CreatedAt, gtime.Now().Add(-time.Minute)).
    Count()

  1. dao.FileInfo
    • “DAO = Data Access Object”,可理解成数据库表的服务窗口
    FileInfo 这张“表”记录了所有上传文件的日志:谁传的、文件名、什么时候传的……

  2. .Ctx(ctx) —— 带上工单
    ctx 是这次 HTTP 请求的“随身文件夹”。
    • 传给 DAO 让数据库操作带上取消/超时/链路信息(这里是占位用)。

  3. .Where(dao.FileInfo.Columns().UserId, … ) —— 第 1 条筛选条件
    • “只看当前管理员上传的记录”。
    ctx.Value(consts.CtxAdminId):在鉴权中间件里,我们把管理员 ID 已写进 ctx;这里把它取出来。
    gconv.Int(...):做一次类型转换,确保是真的整数。
    • 效果相当于 SQL:WHERE user_id = 12

  4. .WhereGTE(dao.FileInfo.Columns().CreatedAt, gtime.Now().Add(-time.Minute)) —— 第 2 条筛选条件
    • “只看最近 1 分钟内的记录”。
    gtime.Now() 拿当前时间 → .Add(-time.Minute) 往前倒 60 秒。
    WhereGTE = “列值 >= 给定时间”。
    • SQL 片段:AND created_at >= (当前时间 - 60秒)

  5. .Count() —— 真正执行查询
    • 让数据库帮我们数一数满足上面两个条件的行数;
    • 结果放进 count 变量。
    • 如果数据库出错,错误信息写入 err

  6. count, err := …
    := 声明两个变量:

    • count = 查到的条目数量(比如 0、1、5…)
    • err = 执行过程中有无错误(成功则是 nil

这有什么用?

  • 防止刷接口:如果 count 已经 ≥ 10,就说明这个管理员 1 分钟内上传了 10 次文件,可立即返回“操作太频繁”。
  • 统计分析:也能用于后台图表“最近一分钟上传次数”。

一句话总结
这三行链式调用=
“到 file_info 表里查:user_id = 当前管理员created_at 在最近1分钟内 的记录数量,把结果存到 count;如果查询出错写到 err。”

下面把两行代码拆成“做日期文件夹 → 把上传文件存进去”的保姆级步骤。
先放原文:

dateDirName := gtime.Now().Format("Ymd")
fileName, err := in.File.Save(gfile.Join(uploadPath, dateDirName), in.RandomName)

────────────────────────────────────────
第 1 行:dateDirName := gtime.Now().Format("Ymd")
────────────────────────────────────────

  1. gtime.Now()
    • GoFrame 的时间工具,等价于 time.Now(),拿到此刻的本地时间
    例如:2024-06-22 15:04:05

  2. .Format("Ymd")
    • 把时间格式化成字符串。
    "Ymd" 指定样式:Year-Month-Day → 20240622(8 位数字)。
    • 结果存在变量 dateDirName
    ➜ 作用:得到一个以当天日期命名的子目录名,方便按天归档文件。

────────────────────────────────────────
第 2 行:fileName, err := in.File.Save(...)
────────────────────────────────────────

  1. gfile.Join(uploadPath, dateDirName)
    uploadPath 之前从 config.yaml 读到,如 "./upload"
    gfile.Join 会智能拼接文件路径、自动补 /
    "./upload" + "20240622""./upload/20240622"
    • 目标是把当天所有文件放进 “./upload/20240622” 这个文件夹。

  2. in.File.Save(目标目录, in.RandomName)
    in.File 是 GoFrame 解析出来的 上传文件对象
    .Save(dir, useRandomName bool)

    • dir:要保存到哪个文件夹。
    • useRandomName:是否自动给文件起一个随机名字(防止重名)。
      • 返回两个值:
    • fileName:最终保存到磁盘上的完整文件名(含扩展名);
    • err:保存过程中有没有错误。
  3. fileName, err :=
    fileName 可能是 "20240622150405_123abc.jpg"(日期_随机串+原扩展名)。
    • 如果磁盘权限不足或目录不存在,就会写进 err

────────────────────────────────────────
整体作用(生活比喻)

  1. 先看日历:今天是 2024-06-22 → 创建或锁定【20240622】这个文件夹。
  2. 把文件放入当天文件夹./upload/20240622/随机名.jpg
    • 好处:
      • 文件量多时,按天分目录方便管理与清理;
      • 随机文件名解决不同人上传同名 logo.jpg 覆盖的问题。

一句话总结
➜ 这两行代码的功能是“按当天日期创建子目录,并把当前上传文件保存进去,用随机文件名避免冲突,同时返回保存后的文件名与可能出错信息”。

继续沿用“快递贴标签”的思路:文件已经存进硬盘,现在要写一条记录到数据库,描述它的“身份卡”。这段代码就是在做“制卡”。

data := entity.FileInfo{
    Name:   fileName,
    Src:    gfile.Join(uploadPath, dateDirName, fileName),
    Url:    "/upload/" + dateDirName + "/" + fileName, // 和上面 Join() 结果一样
    UserId: gconv.Int(ctx.Value(consts.CtxAdminId)),
}

逐字段解释(超小白版)

  1. data :=
    • 创建一个名叫 data 的变量;
    • 类型是 entity.FileInfo——它对应数据库里 file_info 这张表的一行记录
    • 大括号 {...} 写的是这行记录里的各个列值。

  2. Name: fileName,
    Name 字段 = “保存到磁盘后的文件名”。
    fileName 来自前面 in.File.Save(...) 的返回值,例如 20240622150405_abcd.jpg

  3. Src: gfile.Join(uploadPath, dateDirName, fileName),
    Src(Source)= 文件在服务器硬盘上的绝对/相对路径
    • 相当于告诉后台 “这份文件放在 ./upload/20240622/xxxxx.jpg”。
    • 后台做清理或迁移时会用到。

  4. Url: "/upload/" + dateDirName + "/" + fileName,
    Url = 让浏览器可以访问这张图片的 HTTP 路径
    • 比如 http://api.xxx.com/upload/20240622/xxxxx.jpg
    • 代码里只拼接了相对部分 /upload/...,域名由 Nginx 或前端自己加。

  5. UserId: gconv.Int(ctx.Value(consts.CtxAdminId)),
    • 记录是谁上传的。
    ctx.Value(...):在鉴权时已把管理员 ID 塞进 context,这里取出来。
    gconv.Int(...) 做类型安全转换(防止拿到空指针或字符串)。

整体作用

➜ 把关于这张文件的关键信息(文件名、磁盘路径、URL、上传者)整理好,装进 data 这一行;
下一步代码会 dao.FileInfo.Ctx(ctx).Data(data).Insert() 把它写进数据库,方便:

  • 前端请求“文件列表”时展示;
  • 后台按用户/日期统计上传量;
  • 若要删除文件,先删数据库记录再删硬盘文件。

一句话总结

这段代码就是准备好一条“文件档案”记录:记录文件名、硬盘存储位置、对外访问地址以及上传者 ID,为后续插入数据库做准备。

一句话先说明场景
上一步把文件的“身份证”( data 变量 ) 准备好了;这行代码的任务就是 把这张身份证送进数据库存档,并拿回系统自动生成的编号(id)


原代码

id, err := dao.FileInfo.            // ① 找到“文件信息”这张表的 DAO
          Ctx(ctx).                 // ② 带上本次 HTTP 请求的 context
          Data(data).               // ③ 写入准备好的记录 data
          OmitEmpty().             // ④ 忽略结构体里值为空的字段
          InsertAndGetId()         // ⑤ 执行 INSERT,并把自增主键取回来

下面像“流水线”一样拆解

步骤 片段 通俗解释
dao.FileInfo FileInfo表窗口办理业务(DAO = Data Access Object)。
.Ctx(ctx) 带上“工作单”(context)——里边有超时/用户信息;便于日志、取消操作。
.Data(data) 告诉窗口:“这就是我要存档的那行资料,请看”。data 就是上一行准备的 entity.FileInfo{...}
.OmitEmpty() 如果 data 里有空值字段(零值),就别把这些列写进 SQL,数据库会用默认值。
.InsertAndGetId() 真正执行 SQL INSERT,并立刻把数据库生成的自增主键返回。

结果变量

id  →  新插入的那条文件记录在数据库里的主键,比如 128  
err →  如果写库出错(网络、权限),这里会装着错误信息。成功为 nil

小白级生活比喻

  1. 你(代码)拿着文件身份证 data 去档案室窗口(dao.FileInfo)。
  2. 把工单 ctx 递给办事员,说明这次是谁在操作。
  3. “这是资料,请录入。”(.Data(data))
  4. “里面有几栏空着不用管。”(.OmitEmpty())
  5. 办事员敲键盘存档成功,返给你一张小票——档案编号 id。若电脑罢工就给你一张写着错误原因的纸(err)。

一句话总结
InsertAndGetId() 这整句代码把准备好的文件信息写入 file_info 表,同时拿到数据库分配的主键 id,留给后续逻辑使用(例如返回给前端或做日志)。


网站公告

今日签到

点亮在社区的每一天
去签到