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()
g
是啥?
• GoFrame 的核心包,提供全局工具。
• 这里用到的是g.Cfg()
——全局 配置中心。g.Cfg()
做了什么?
• 读取项目根目录里的config.yaml / config.toml / config.json
等配置文件;
• 把它们解析后存在内存里,方便随时取值。
– 举个config.yaml
片段:upload: path: "./upload/" # ← 这就是我们要拿的值
.MustGet(ctx, "upload.path")
•Get
表示“按键名取配置值”,键用 点号 表示层级。
"upload.path"
就等同于 YAML 里的upload: {path: ...}
。
•MustGet
比Get
多了一个“必须要拿到”的语义:
– 找到了 → 正常返回;
– 找不到 → 直接panic
,程序启动时就会报错,防止运行过程中才发现配置缺失。
• 第二参ctx
:为了支持热加载、分环境覆盖等场景,这里仍然要求传context.Context
。初学可理解为“附带一张工单”,此处没有超时之类特殊用法。.String()
•MustGet
返回的是 GoFrame 的万能容器*gvar.Var
,里面可以装任何类型;
• 调.String()
把它安全地转成 字符串。
• 如果 YAML 中path
写的是./upload/
,这里就得到"./upload/"
。uploadPath := …
•:=
短变量声明,把结果赋值给变量uploadPath
。
• 以后代码会拿它去做:file.Save(uploadPath + newFileName)
为什么这么写而不是写死路径?
• 环境差异:本地开发放./upload/
,线上服务器可能放/data/files/
; 用配置文件改一行即可。
• 可读性 & 维护:所有路径、端口、密钥都集中在config.yaml
,新同学一眼就能看到。
• 出错早发现:MustGet
缺值直接 panic,部署阶段就能踩出坑,避免线上才报“路径为空”。“小白级”生活比喻
g.Cfg()
→ 打开家里的“说明书”抽屉;MustGet("upload.path")
→ 找到“上传文件存放在哪个房间”那一行;.String()
→ 把房间号抄成一张便签;uploadPath := …
→ 以后搬箱子(保存文件)都拿这张便签,不用死记硬背。
一句话总结
这行代码就是从配置文件里读取“上传文件存储目录”的字符串,存进变量 uploadPath
。如果配置缺失,程序立即报错,确保后面保存文件时不会因为路径为空而崩溃或把文件乱丢。
代码片段
if in.Name != "" { // ① 如果前端额外传了一个“文件名”字段
in.File.Filename = in.Name // ② 就把它写进上传文件的 Filename 属性里
}
把它当成“给上传的文件改名”来讲,三步就懂。
────────────────────────────────────────
一、先认清每个变量是谁
in
- 整个上传接口的入参结构体,举例(伪代码):
type FileUploadInput struct { Name string // 前端可选:希望保存成什么文件名 File *ghttp.UploadFile // GoFrame 封装的上传文件信息 }
- 整个上传接口的入参结构体,举例(伪代码):
in.Name
- 前端表单里额外传的 想要的文件名,比如
"avatar_123.jpg"
。 - 如果前端没传,就会是空字符串
""
。
- 前端表单里额外传的 想要的文件名,比如
in.File
- GoFrame 帮你把
<input type="file">
上传的文件解析成*ghttp.UploadFile
。 - 其中的
Filename
默认是 客户端原始文件名(如"IMG_0001.JPG"
)。
- GoFrame 帮你把
────────────────────────────────────────
二、代码做了什么?
步骤 | 发生的事 | 目的 |
---|---|---|
① | if in.Name != "" |
判断前端有没有主动指定文件名 |
② | in.File.Filename = in.Name |
如果有,就把默认文件名覆写成指定名字 |
也就是:“前端想改名就尊重前端;前端不管就保持原名。”
────────────────────────────────────────
三、为什么要让前端改名字?
- 业务需要
- 比如头像上传时,后端希望所有头像都叫
avatar_<userId>.jpg
,前端就能在Name
字段里直接传好。
- 比如头像上传时,后端希望所有头像都叫
- 防乱码 / 空格
- 客户端原名可能带空格或中文,存储系统不友好;前端预处理后告诉后端保存为什么名字。
- 避免重名冲突
- 同一个用户多次上传
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()
dao.FileInfo
• “DAO = Data Access Object”,可理解成数据库表的服务窗口。
•FileInfo
这张“表”记录了所有上传文件的日志:谁传的、文件名、什么时候传的…….Ctx(ctx)
—— 带上工单
•ctx
是这次 HTTP 请求的“随身文件夹”。
• 传给 DAO 让数据库操作带上取消/超时/链路信息(这里是占位用)。.Where(dao.FileInfo.Columns().UserId, … )
—— 第 1 条筛选条件
• “只看当前管理员上传的记录”。
•ctx.Value(consts.CtxAdminId)
:在鉴权中间件里,我们把管理员 ID 已写进ctx
;这里把它取出来。
•gconv.Int(...)
:做一次类型转换,确保是真的整数。
• 效果相当于 SQL:WHERE user_id = 12
.WhereGTE(dao.FileInfo.Columns().CreatedAt, gtime.Now().Add(-time.Minute))
—— 第 2 条筛选条件
• “只看最近 1 分钟内的记录”。
•gtime.Now()
拿当前时间 →.Add(-time.Minute)
往前倒 60 秒。
•WhereGTE
= “列值 >= 给定时间”。
• SQL 片段:AND created_at >= (当前时间 - 60秒)
.Count()
—— 真正执行查询
• 让数据库帮我们数一数满足上面两个条件的行数;
• 结果放进count
变量。
• 如果数据库出错,错误信息写入err
。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")
────────────────────────────────────────
gtime.Now()
• GoFrame 的时间工具,等价于time.Now()
,拿到此刻的本地时间。
例如:2024-06-22 15:04:05.Format("Ymd")
• 把时间格式化成字符串。
•"Ymd"
指定样式:Year-Month-Day →20240622
(8 位数字)。
• 结果存在变量dateDirName
。
➜ 作用:得到一个以当天日期命名的子目录名,方便按天归档文件。
────────────────────────────────────────
第 2 行:fileName, err := in.File.Save(...)
────────────────────────────────────────
gfile.Join(uploadPath, dateDirName)
•uploadPath
之前从config.yaml
读到,如"./upload"
。
•gfile.Join
会智能拼接文件路径、自动补/
:
"./upload" + "20240622"
→"./upload/20240622"
• 目标是把当天所有文件放进 “./upload/20240622” 这个文件夹。in.File.Save(目标目录, in.RandomName)
•in.File
是 GoFrame 解析出来的 上传文件对象。
•.Save(dir, useRandomName bool)
dir
:要保存到哪个文件夹。useRandomName
:是否自动给文件起一个随机名字(防止重名)。
• 返回两个值:fileName
:最终保存到磁盘上的完整文件名(含扩展名);err
:保存过程中有没有错误。
fileName, err :=
•fileName
可能是"20240622150405_123abc.jpg"
(日期_随机串+原扩展名)。
• 如果磁盘权限不足或目录不存在,就会写进err
。
────────────────────────────────────────
整体作用(生活比喻)
- 先看日历:今天是 2024-06-22 → 创建或锁定【20240622】这个文件夹。
- 把文件放入当天文件夹:
./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)),
}
逐字段解释(超小白版)
data :=
• 创建一个名叫data
的变量;
• 类型是entity.FileInfo
——它对应数据库里file_info
这张表的一行记录。
• 大括号{...}
写的是这行记录里的各个列值。Name: fileName,
•Name
字段 = “保存到磁盘后的文件名”。
•fileName
来自前面in.File.Save(...)
的返回值,例如20240622150405_abcd.jpg
。Src: gfile.Join(uploadPath, dateDirName, fileName),
•Src
(Source)= 文件在服务器硬盘上的绝对/相对路径。
• 相当于告诉后台 “这份文件放在 ./upload/20240622/xxxxx.jpg”。
• 后台做清理或迁移时会用到。Url: "/upload/" + dateDirName + "/" + fileName,
•Url
= 让浏览器可以访问这张图片的 HTTP 路径。
• 比如http://api.xxx.com/upload/20240622/xxxxx.jpg
。
• 代码里只拼接了相对部分/upload/...
,域名由 Nginx 或前端自己加。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
小白级生活比喻
- 你(代码)拿着文件身份证
data
去档案室窗口(dao.FileInfo
)。 - 把工单
ctx
递给办事员,说明这次是谁在操作。 - “这是资料,请录入。”(
.Data(data)
) - “里面有几栏空着不用管。”(
.OmitEmpty()
) - 办事员敲键盘存档成功,返给你一张小票——档案编号
id
。若电脑罢工就给你一张写着错误原因的纸(err
)。
一句话总结
InsertAndGetId()
这整句代码把准备好的文件信息写入 file_info
表,同时拿到数据库分配的主键 id
,留给后续逻辑使用(例如返回给前端或做日志)。