签入脚本

This commit is contained in:
小蚂蚁云
2026-10-20 11:37:25 +08:00
commit 7f5e8470af
909 changed files with 4045080 additions and 0 deletions
@@ -0,0 +1,198 @@
================================================================================
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)。