# 7.1 文件上传全流程 :::tip 概述 上传采用两段式:SaveTemp 先入临时目录(白名单+大小+图片嗅探校验),表单保存时 SaveFromTemp 移动到正式子目录。未走第二段的 temp 文件由每日 03:30 清理任务过期回收,不污染正式目录。 ::: ## 上传接口 ```text POST /api/upload/uploadFile (sys:upload:file)multipart 字段名 file POST /api/upload/uploadFiles (sys:upload:files)多文件 入参 type=image 时按图片规则校验,否则按普通文件 ``` multipart 字段约定:`*ghttp.UploadFile` + `p:"file"` 标签。 ## 两段式流程图 ```text 阶段一(上传时,POST /api/upload/uploadFile): 浏览器 ──multipart──▶ SaveTemp │ ① 大小 ≤ file.maxSize(MB) │ ② 扩展名 ∈ file.fileExt / file.imageExt │ ③ type=image:前512B 魔数嗅探须 image/* ▼ 落盘 {uploadDir}/temp/{yyyymmdd}/{32位hex}.{ext} ▼ 返回 {url:"{domainUrl}/temp/...", path:"temp/.../x.png"(入库形态)} → 前端预览 阶段二(表单保存时,spec.saveTransform / SaveFileField): 表单携带 temp URL ──▶ SaveFromTemp(url, saveDir="user") │ 剥离 domainUrl → 相对路径 │ 以 temp/ 开头?──否──▶ 已正式/外站:归一化或原样返回 │ 是 ▼ {uploadDir}/{saveDir}/{yyyymmdd}/{HHmmss}_{4字节hex}.{ext}(Rename,跨卷复制回退) ▼ 库中写入相对路径 "user/20261017/153012_ab12.png" ▼ 读侧 GetFileUrl:domainUrl + "/" + 相对路径 ──▶ 前端展示 ``` ## SaveTemp 校验链(第一段) ```text 1) 大小 ≤ file.maxSize(MB,默认 10) 2) 扩展名白名单:file.fileExt(普通)/ file.imageExt(图片) 3) 读取前 512 字节:type=image 时做魔数嗅探(http.DetectContentType 须 image/*),并据文件头嗅探 MIME 落盘:{file.uploadDir}/{file.tempDir}/{yyyymmdd}/{32位随机hex}.{ext} 返回 Result{Url(可预览完整地址), Path(相对 uploadDir,入库形态), Name, Size, Ext, Mime} ``` ## SaveFromTemp 处理规则(第二段) | 入参形态 | 处理 | |---------|------| | 空值 | 返回 "" | | 本站 URL(匹配 file.domainUrl 前缀) | 剥离域名得相对路径 | | 相对路径以 temp/ 开头 | 移动到 {saveDir}/{yyyymmdd}/{HHmmss}_{4字节hex}.{ext}(os.Rename,跨卷回退"复制后删源");返回新相对路径(入库形态) | | 已是正式相对路径 | 归一化后原样返回(幂等) | | 外站 http(s) URL | 原样返回(不处理外链) | SaveFromTempMulti:按逗号拆分逐个转正(如 article.images 多图字段)。 ## 路径契约 ```text 数据库统一存"相对 uploadDir 的路径"(如 tenant/20260923/x.png) 对外访问地址 = GetFileUrl = file.domainUrl + "/" + 相对路径 (domainUrl 为空时用 /upload/ 相对路径) 静态访问:router SetServerRoot("resource") 提供 /upload/** Response 中间件对 .svg 强制 attachment + nosniff(防存储型 XSS) ``` ## 声明式用法(ModuleSpec) ```go FileFields: []string{"avatar"} + FileSaveDir: "user" // 写侧 saveTransform 自动 temp→正式;读侧 readTransform 自动 GetFileUrl 补域名 // MultiFileFields 处理逗号多文件;ContentFields 走富文本流程 ``` 模块已接入:user(avatar)、article(cover/images/content)、tenant(logo)、example(avatar) 等。 ## 临时目录清理(运营闭环) ```text cmd.go 注册 gcron "0 30 3 * * *"(每日 03:30)→ logic.CleanTemp: 扫描 {uploadDir}/{tempDir} 下目录名为 yyyymmdd 且早于 now-file.tempExpireDays(默认 7 天,0=不清理)的日期目录整目录删除 返回删除数量并记日志 ``` ## 常见问题 | 症状 | 排查 | |------|------| | 上传图片 404 | file.domainUrl 与静态服务根的相对关系;库中存相对路径,展示层必须经 GetFileUrl | | 上传被拒 | 扩展名不在白名单 / 超 maxSize / 图片魔数嗅探不过(改后缀伪造) | | temp 文件堆积 | CleanTemp 的 gcron 已注册?file.tempExpireDays > 0? | | 跨卷移动失败 | os.Rename 失败自动回退"复制后删源";检查磁盘空间与权限 | | 富文本图片不显示 | 读取侧走 Detail/List(基座自动 Expand),不要直接 Struct 输出 |