4.6 核心基座体系
概述
本项目没有传统"基类继承"——用组合替代继承:ModuleSpec 泛型基座(module_spec.go)承担 CRUD 流水线,base.go 共享能力层提供横切函数。本节解释两者的分工与设计纪律。
组合替代继承的架构
text
┌────────────────────────────────────────────────────┐
│ 模块 logic 文件(example.go) │
│ var exampleSpec = ModuleSpec[...]{ 声明差异 } │──▶ 委托
│ func 方法() { exampleSpec.Page(...) } │
│ func 扩展() { 手写(合法逃生舱)} │
└───────────────────────┬────────────────────────────┘
│ 复用
┌───────────────────────▼────────────────────────────┐
│ module_spec.go:ModuleSpec 泛型基座 │
│ applyFilters / readTransform / saveTransform │
│ checkUniqueRules / FillTextFields / buildTree │
└───────────────────────┬────────────────────────────┘
│ 调用
┌───────────────────────▼────────────────────────────┐
│ base.go 共享能力层(导出上限 15 个) │
│ 文件转正 / 富文本 / 字典文本 / 唯一校验 / │
│ 事务 / 分块查询 / 泛型树 / 临时目录清理 │
└────────────────────────────────────────────────────┘base.go 能力清单(internal/logic/base.go)
| 能力 | 函数 | 一句话职责 |
|---|---|---|
| 文件字段 | SaveFileFields / SaveMultiFileFields | 批量转正(原地更新 map) |
| SaveFileField / SaveMultiFileField | 单值便捷版 | |
| 富文本 | LocalizeContentFields | 存库本地化([IMG_URL] 占位符化) |
| ExpandContentFields | 读取还原(唯一还原出口) | |
| 枚举文本 | DictGetText | 字典值 → 显示名(进程内缓存 300s) |
| SerializeMapOnRead | map 行补 {field}Text | |
| LogTypeText | 操作日志类型 → 中文(静态表) | |
| 唯一校验 | CheckUnique | UX 快速报错(TOCTOU,真值靠 uk_* 索引) |
| 事务 | WithTrans | 事务包裹(错误自动回滚) |
| 分块 | ChunkQuery | 分块遍历(导出防 OOM,默认 500/页) |
| 树 | BuildTree[T] | 泛型平铺转树 |
| 清理 | CleanTemp | 清理上传临时日期目录 |
架构纪律(文件头约束,真实注释):
text
1) 按能力分区:文件字段 / 富文本 / 枚举文本 / 唯一校验 / 事务 / 分块查询 / 树
2) 依赖方向单向:只允许依赖 dao / utility / consts,禁止引用模块文件
3) 导出函数上限 15 个,超出按能力拆 internal/logic/base/ 子包
4) CheckUnique 仅为 UX 快速报错(TOCTOU),并发唯一性靠 uk_* 索引
5) [IMG_URL] 占位符是持久化契约,还原只能走 ExpandContentFieldsModuleSpec 基座流水线(module_spec.go)
text
Page applyFilters → Count → applyOrder.Page → structRows → readTransform
→ AfterPageList
List applyFilters → 排序 → req.limit 上限 → structRows → readTransform
→ IsTree 时 buildTree(叶子 Children 预置 [] 而非 null)
Detail Model.Where(id).One → Struct → readTransform → AfterDetail
Add ToData → BeforeAdd → saveTransform(文件/富文本转正)→ checkUniqueRules
→ Insert → LastInsertId → AfterAdd → 返回 id
Update ToData → BeforeUpdate → saveTransform → checkUniqueRules(排除自身)
→ Update(仅更新 data 中出现的键)→ AfterUpdate
Delete BeforeDelete → Delete(钩子改写软删)→ AfterDelete
BatchDelete BeforeBatchDelete → 逐条 BeforeDelete+软删 → AfterBatchDeletereadTransform 顺序:FileFields 补 URL → MultiFileFields 补 URL → ContentFields 展开 [IMG_URL] → FillTextFields({field}Text)。
设计纪律(为什么基座不会腐化)
基座三铁律
- 基座禁止"按模块分支"——差异只进声明字段与钩子;写不进钩子 → 手写(合法逃生舱)。这条纪律保证基座永远不出现
if module == "user"式代码。 - 钩子用具体函数类型,req any 经 mustAs[T] 断言(失败=编程错误,panic 暴露而非带病运行)。
- 钩子内禁止 append、禁止 HTTP/文件 IO——文件迁移统一在 saveTransform 完成。
applyFilters 固定顺序:LikeFields → EqFields → KeywordFields → Filter → DataScope;租户/软删隔离由 dao.Ctx 先于本链注入(任何模块不可绕过)。
ModuleSpec 声明字段全景
text
Model func(ctx) *gdb.Model 数据源(通常 dao.Xxx.Ctx)
LikeFields []string 逐列 LIKE(值取 req 同名驼峰字段,空跳过)
EqFields []string 等值(零值跳过)
KeywordFields []string req.keyword 多列 OR(基座保留能力)
Filter func(ctx, m, req) *gdb.Model 复杂条件逃生舱
OrderBy / ListOrderBy / OrderableFields 排序(白名单控制前端排序)
IsTree / ParentOf / ChildSetter 树形
SerializeMaps map[字段]字典编码 {field}Text 填充
FileFields / MultiFileFields / ContentFields / FileSaveDir 文件与富文本
UniqueFields []UniqueRule{Field, Group} 唯一性预检(Group 分组唯一)
DataScope DataScopeFunc 行级数据权限(ApplyDataScope 接线)
ToData req → map[string]any 新增/修改共用转换
钩子 Before/After × Add/Update/Delete/Batch + AfterPageList/AfterDetail与"基类方案"的对比
| 维度 | 本项目(泛型基座+组合) | 传统基类继承 |
|---|---|---|
| 差异表达 | 声明字段 + 钩子 | 重写虚方法 |
| 类型安全 | 泛型三类型参数编译期约束 | 常退化 any/map |
| 复用粒度 | 函数级(base.go 自由组合) | 类级(受继承链束缚) |
| 基座污染风险 | 三铁律禁止按模块分支 | 基类易堆积 if-else |
| 逃生舱 | 手写方法(完全自由) | 多重继承/桥接方法 |