199 lines
12 KiB
Plaintext
199 lines
12 KiB
Plaintext
================================================================================
|
||
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)。
|