================================================================================ EasyGoAdmin 后端开发者文档 · 05 共享能力层与文件/富文本处理 ================================================================================ 代码位置:internal/logic/base.go(共享能力层)、utility/upload、 internal/logic/upload.go(上传接口)、manifest/config 的 file.* 配置 -------------------------------------------------------------------------------- 1. base.go 共享能力层总览(横切能力,组合式复用) -------------------------------------------------------------------------------- 架构纪律(文件头约束): 1) 按能力分区:文件字段 / 富文本 / 枚举文本 / 唯一校验 / 事务 / 分块查询 / 树 2) 依赖方向单向:只允许依赖 dao / utility / consts,禁止引用模块文件 3) 导出函数上限 15 个,超出按能力拆 internal/logic/base/ 子包 4) CheckUnique 仅为 UX 快速报错(TOCTOU),并发唯一性靠 uk_* 索引 5) [IMG_URL] 占位符是持久化契约,还原只能走 ExpandContentFields 能力清单: SaveFileFields(ctx, data, fields, saveDir) 单文件字段批量转正(原地更新 map) SaveMultiFileFields(ctx, data, fields, saveDir) 逗号多文件字段批量转正 SaveFileField(ctx, fileUrl, saveDir) 单值便捷版 SaveMultiFileField(ctx, fileUrls, saveDir) 逗号串便捷版 LocalizeContentFields(ctx, content, saveDir) 富文本存库本地化(占位符化) ExpandContentFields(ctx, content) 富文本读取还原(唯一还原出口) DictGetText(ctx, code, value) 字典值 → 显示名(进程内缓存 300s) SerializeMapOnRead(ctx, data, fieldDict) map 行补 {field}Text LogTypeText(v int) 操作日志类型 → 中文(静态表) CheckUnique(ctx, m, column, value, excludeId, label) 唯一性快速校验 WithTrans(ctx, fn) 事务包裹(错误自动回滚) ChunkQuery(ctx, build, size, fn) 分块遍历(导出防 OOM) BuildTree[T](rows, idOf, pidOf, childrenOf, less) 泛型树构建 CleanTemp(ctx, expireDays) 清理上传临时日期目录 fillUserCount(column, list) 按外键统计引用数(内部,level/position 等复用) 注:ModuleSpec 基座(module_spec.go)在其上封装了 readTransform/saveTransform/ checkUniqueRules/FillTextFields,模块优先用基座声明,逃生舱才直接调 base 函数。 -------------------------------------------------------------------------------- 2. 文件上传两段式(utility/upload + logic/upload.go) -------------------------------------------------------------------------------- 2.1 上传接口 POST /api/upload/uploadFile (perm sys:upload:file)multipart 字段名 file POST /api/upload/uploadFiles (perm sys:upload:files)多文件 入参 type=image 时按图片规则校验,否则按普通文件。 2.2 SaveTemp(第一段:落临时目录) 校验顺序: 1) 大小 ≤ file.maxSize(MB,默认 10) 2) 扩展名白名单:file.fileExt(普通)/ file.imageExt(图片) 3) 读取前 512 字节:type=image 时做魔数嗅探(http.DetectContentType 须 image/*), 并据文件头嗅探 MIME(识别不出回退 application/octet-stream) 落盘:{file.uploadDir}/{file.tempDir}/{yyyymmdd}/{32位随机hex}.{ext} 返回 Result{Url(可直接预览的完整地址), Path(相对 uploadDir,入库形态), Name, Size, Ext, Mime}。 2.3 SaveFromTemp(第二段:表单保存时转正) 入参为上传返回的 URL 或相对路径;处理规则: * 空值 → "" * 本站 URL(匹配 file.domainUrl 前缀)→ 剥离域名得相对路径 * 相对路径以 temp/ 开头 → 移动到 {saveDir}/{yyyymmdd}/{HHmmss}_{4字节hex}.{ext} (os.Rename,跨卷回退“复制后删源”);返回新相对路径(入库形态) * 已是正式相对路径 → 归一化后原样返回(幂等) * 外站 http(s) URL → 原样返回(不处理外链) SaveFromTempMulti:按逗号拆分逐个转正(如 article.images 多图字段)。 2.4 路径契约 数据库统一存“相对 uploadDir 的路径”(如 tenant/20260923/x.png); 对外访问地址由 GetFileUrl = file.domainUrl + "/" + 相对路径 生成 (domainUrl 为空时用 /upload/ 相对路径)。 静态访问:router SetServerRoot("resource") 提供 /upload/**; Response 中间件对 .svg 强制 attachment + nosniff(防存储型 XSS)。 2.5 ModuleSpec 中的声明式用法 FileFields: []string{"avatar"} + FileSaveDir: "user" 写侧 saveTransform 自动 temp→正式;读侧 readTransform 自动 GetFileUrl 补域名。 MultiFileFields 处理逗号多文件;ContentFields 走富文本流程。 2.6 临时目录清理(运营闭环) cmd.go 注册 gcron "0 30 3 * * *"(每日 03:30)→ logic.CleanTemp: 扫描 {uploadDir}/{tempDir} 下目录名为 yyyymmdd 且早于 now-file.tempExpireDays(默认 7 天,0=不清理)的日期目录整目录删除, 返回删除数量并记日志。 2.7 上传两段式流程图 阶段一(上传时,POST /api/upload/uploadFile): 浏览器 ──multipart──▶ SaveTemp │ ① 大小 ≤ file.maxSize(MB) │ ② 扩展名 ∈ file.fileExt / file.imageExt │ ③ type=image:前512B 魔数嗅探须 image/* ▼ 落盘 {uploadDir}/temp/{yyyymmdd}/{32位hex}.{ext} ▼ 返回 {originalName, fileExtension, fileType, fileSize, fileName, filePath:"temp/.../x.png"(入库形态), fileUrl:"{domainUrl}/temp/..."} → 前端预览 阶段二(表单保存时,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 + "/" + 相对路径 ──▶ 前端展示 生命周期:temp 文件由 CleanTemp(每日 03:30)按 tempExpireDays 清理; 未走阶段二的 temp 文件自然过期,不污染正式目录。 -------------------------------------------------------------------------------- 3. 富文本 [IMG_URL] 持久化契约 -------------------------------------------------------------------------------- 3.1 存库(LocalizeContentFields) 正文(如 article.content)入库前处理: * 匹配范围:img/video/source/embed 的 src + a 的 href(带文件后缀) * 本站 /temp/ 引用 → SaveFromTemp 转正后替换为 "[IMG_URL]/{相对路径}" * 本站正式 /upload/ 引用 → 剥离访问基址(GetAccessBase)写 "[IMG_URL]{相对路径}" * 外站 URL → 不处理(规避 SSRF/外链下载) [IMG_URL] 是库内 content 字段格式的一部分:content 不供第三方系统直接消费。 3.2 读取(ExpandContentFields) strings.ReplaceAll(content, "[IMG_URL]", GetAccessBase(ctx)), 是唯一还原出口,业务代码禁止手写字符串替换。 ModuleSpec 的 ContentFields 声明后读写两侧自动接入。 3.3 富文本转换流图 写侧 LocalizeContentFields(content, saveDir): 正文 HTML │ 正则提取 img/video/source/embed[src] + a[href(带后缀)] │ 逐 URL 判断: │ 含 /temp/ → SaveFromTemp 转正 → "[IMG_URL]/user/.../x.png" │ 本站正式路径 → 剥离 domainUrl → "[IMG_URL]/user/.../x.png" │ 外站 http(s) → 跳过(不处理,规避 SSRF) ▼ 库存 content(含 [IMG_URL] 占位符;此为持久化格式契约) 读侧 ExpandContentFields(content): "[IMG_URL]" ──ReplaceAll──▶ domainUrl(访问基址)──▶ 前端富文本展示 唯一还原出口;ModuleSpec.ContentFields 声明后两侧自动接入。 -------------------------------------------------------------------------------- 4. 事务(WithTrans) -------------------------------------------------------------------------------- WithTrans(ctx, fn):g.DB().Begin → fn(gdb.WithTX(ctx, tx)) → 出错 Rollback, 否则 Commit。dao.Ctx(ctx) 在事务上下文内自动走事务连接。 纪律:事务内禁止 HTTP/文件 IO;bcrypt 等 CPU 密集操作在事务外预计算 (典型:租户开户 Account、用户 Add)。 -------------------------------------------------------------------------------- 5. 分块查询(ChunkQuery) -------------------------------------------------------------------------------- ChunkQuery(ctx, build, size, fn):build(page, size) 每页重建查询(gdb Model 不可复用执行),默认页大小 500;语义为快照——并发写入可能造成翻页漂移。 典型用途:用户导出(logic/user.go Export)分批读取 → 写 Excel,防大结果集 OOM。 -------------------------------------------------------------------------------- 6. 树构建(BuildTree / ModuleSpec 树形) -------------------------------------------------------------------------------- base.BuildTree[T]:泛型平铺转树,父节点不在集合内则挂根,可传排序函数。 ModuleSpec 路线:IsTree:true + ParentOf +(ChildSetter 或 DTO 的 Children 字段), List 末尾自动建树,叶子 Children 预置空数组。DTO 必须含 Id 字段。 使用模块:menu / dept / category。 -------------------------------------------------------------------------------- 7. Excel 导入导出(utility/excel) -------------------------------------------------------------------------------- 基于 xuri/excelize/v2,支持 xls/xlsx。 ReadSheet(file FileOpener):首行表头,全单元格转字符串(TrimSpace), 空行跳过;FileOpener 接口兼容 *ghttp.UploadFile 与 *multipart.FileHeader。 WriteSheet(dir, baseName, headers, rows [][]any):写文件返回 (fileName, filePath)。 -------------------------------------------------------------------------------- 8. 其他 utility 能力 -------------------------------------------------------------------------------- utility/cache 统一缓存门面:Init 探测 Redis(失败/运行期出错自动降级 gcache 进程内存,UsingMemory() 可查);Set/Get/Del/Exists/SetNX。 注意:内存降级下多实例不共享(限流计数、任务锁退化为单机语义)。 utility/crypto GenerateSalt(8 字节随机 base64url 截 10 位)、 HashPassword/VerifyPassword(bcrypt(password+salt))、IsBcrypt。 utility/ua ParseOS / ParseBrowser(操作日志、登录日志、在线用户用)。 utility/ip2region xdb 懒加载(sync.Once)+ 读锁并发;City(ip) 优先市回退省, 内网/回环返回空;Location(ip) 供日志(失败显示 "-")。 路径配置 ip2region.path(默认 manifest/data/ip2region.xdb)。 utility/captcha 见 03 章。 utility/response 见 01 章。 -------------------------------------------------------------------------------- 9. 常见问题排查 -------------------------------------------------------------------------------- 上传图片 404:检查 file.domainUrl 与静态服务根(SetServerRoot("resource"))的 相对关系;库中存的是相对 uploadDir 路径,展示层必须经 GetFileUrl。 富文本图片不显示:确认读取侧走 Detail/List(基座自动 Expand),不要直接 Struct 输出;外链未本地化属预期(不处理外站 URL)。 temp 文件堆积:确认 CleanTemp 的 gcron 已注册(cmd.go),file.tempExpireDays>0。 归属地显示 "-":检查 ip2region.xdb 是否存在、api.proxyIps 是否包含反代地址 (否则日志 IP 是反代 IP)。