package logic // ============================================================================ // ModuleSpec 泛型 CRUD 基座(声明式) // // 设计原则(纪律): // 1. 基座禁止出现"按模块分支"——差异只进声明字段与钩子;写不进钩子 → 该方法逃逸手写(合法逃生舱) // 2. 钩子/Filter 使用具体函数类型;req any 一律经 mustAs[T] 断言(失败=编程错误,快速 panic) // 3. 钩子内:可修改元素/data,禁止 append、禁止 HTTP/文件 IO(文件迁移统一在 saveTransform) // 4. applyFilters 顺序固定:LikeFields → EqFields → KeywordFields → Filter → DataScope; // 租户/软删隔离由 dao.Ctx(hooks.ApplyCtx)先于本链注入 // 5. ToData:Add=全量必填;Update=全量表单语义(前端 Detail 回显 → 全量 PUT,零值即用户所见) // 6. 错误:钩子错误记 stage 日志后原样上抛(保留 gerror 语义码,不包装) // ============================================================================ import ( "context" "fmt" "reflect" "strings" "sync" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "easygoadmin/internal/consts" "easygoadmin/utility/upload" ) // ============================== 声明类型 ============================== // UniqueRule 唯一性规则 // Group 为空表示全局唯一;非空表示分组内唯一,例如按租户分组 type UniqueRule struct { Field string // 唯一字段 Group string // 分组字段 } // DataScopeFunc 角色数据权限 // 共享工厂为 ApplyDataScope(data_scope.go);为 nil 时不做过滤 type DataScopeFunc func(ctx context.Context, m *gdb.Model, req any) *gdb.Model // ModuleSpec 模块声明 // // Item 为分页行类型,LItem 为列表行类型(可与 Item 相同),DRes 为详情类型。 // 裁剪说明:List 不挂 AfterPageList,因精简列表结构无冗余列与 Text 字段,树形构建在 LItem 上。 type ModuleSpec[Item any, LItem any, DRes any] struct { // ─── 数据源与查询声明(applyFilters 按字段顺序执行 1→5)─── Model func(ctx context.Context) *gdb.Model // 1 数据源(租户/软删已由 dao.Ctx 注入) LikeFields []string // 2 逐列 LIKE(值取 req 同名列,空跳过) EqFields []string // 3 等值(列名→驼峰 json 取值,零值跳过) KeywordFields []string // 4 关键词列(req.keyword,多列 OR 组内配对) Filter func(ctx context.Context, m *gdb.Model, req any) *gdb.Model // 5 复杂条件(禁重复 2-4 已覆盖者) OrderBy string // 默认排序(硬编码,如 "sort, id") ListOrderBy string // List 排序(空=沿用 OrderBy) OrderableFields []string // 前端排序白名单(空=忽略 orderField/orderType 入参) // ─── 输出声明 ─── IsTree bool // 树形(仅影响 List) ParentOf func(*LItem) int64 // IsTree 必填(缺失 fail-fast) ChildSetter func(*LItem, []*LItem) // 可选;缺省反射写 LItem 的 Children SerializeMaps map[string]string // {field}Text 来自 dict_item FileFields []string // 单文件字段名单(保存 temp→正式、读侧补域名) MultiFileFields []string // 逗号多文件字段名单 ContentFields []string // 富文本字段(存 [IMG_URL] 占位,读自动还原) FileSaveDir string // 本模块文件统一转正目录(单文件/多文件/富文本三类共用) FieldLabels map[string]string // 唯一校验提示的显式覆盖 // ─── 业务规则 ─── UniqueFields []UniqueRule DataScope DataScopeFunc // 角色数据权限(nil 跳过;见 ApplyDataScope) // ─── 钩子(具体签名;错误原样上抛)─── BeforeAdd func(ctx context.Context, data map[string]any, req any) error AfterAdd func(ctx context.Context, id int64, req any) error BeforeUpdate func(ctx context.Context, data map[string]any, id int64, req any) error AfterUpdate func(ctx context.Context, id int64, req any) error BeforeDelete func(ctx context.Context, id int64) error AfterDelete func(ctx context.Context, id int64) error BeforeBatchDelete func(ctx context.Context, ids []int64) error AfterBatchDelete func(ctx context.Context, ids []int64) error AfterPageList func(ctx context.Context, list []*Item, req any) error // 可改元素或就地排序,禁 append AfterDetail func(ctx context.Context, item *DRes, req any) error // ─── 数据装配 ─── ToData func(req any) map[string]any // Add=全量必填;Update=全量表单语义 } // ============================================================================ // 通用助手 // ============================================================================ // mustAs 请求类型断言 // // 支持指针与值互转;配置错误属于编程错误,直接 panic 暴露 func mustAs[T any](req any) T { var zero T if v, ok := req.(T); ok { return v } rv := reflect.ValueOf(req) tv := reflect.TypeOf(zero) if rv.IsValid() { // T=值类型、req=*T:解引用转换 if tv.Kind() != reflect.Ptr && rv.Kind() == reflect.Ptr && !rv.IsNil() && rv.Type().Elem() == tv { return rv.Elem().Interface().(T) } // T=*T指针、req=值:装箱新指针 if tv.Kind() == reflect.Ptr && rv.Kind() != reflect.Ptr && rv.Type() == tv.Elem() { p := reflect.New(tv.Elem()) p.Elem().Set(rv) return p.Interface().(T) } } panic(fmt.Sprintf("ModuleSpec: req 类型断言失败,期望 %T,实际 %T", zero, req)) } // reqFieldCache 请求结构字段索引缓存 var reqFieldCache sync.Map // reflect.Type → map[jsonName]fieldIndex // reqValueByColumn 按列名从请求对象取值 // // 列名 dept_id 映射到 json 标签 deptId,再通过反射读取字段,找不到时返回 nil func reqValueByColumn(req any, column string) any { if req == nil { return nil } rv := reflect.ValueOf(req) for rv.Kind() == reflect.Ptr { if rv.IsNil() { return nil } rv = rv.Elem() } rt := rv.Type() idxMap, _ := reqFieldCache.LoadOrStore(rt, buildReqFieldIndex(rt)) fields, _ := idxMap.(map[string]int) jsonName := camelLower(column) idx, ok := fields[jsonName] if !ok { return nil } return rv.Field(idx).Interface() } // buildReqFieldIndex 构建 json 标签名到字段索引的映射 func buildReqFieldIndex(rt reflect.Type) map[string]int { m := make(map[string]int) for i := 0; i < rt.NumField(); i++ { tag := rt.Field(i).Tag.Get("json") if tag == "" { continue } name := strings.Split(tag, ",")[0] if name == "" || name == "-" { continue } m[name] = i } return m } // builtinLabels 常见列名的内置中文标签 var ( builtinLabels = map[string]string{ "username": "用户名", "realname": "姓名", "name": "名称", "code": "编码", "mobile": "手机号", "email": "邮箱", "password": "密码", "title": "标题", "status": "状态", "sort": "排序", "note": "备注", "remark": "备注", "address": "地址", "avatar": "头像", "type": "类型", "content": "内容", } ) // fieldLabel 字段标签三级回退 // // 依次取 FieldLabels、内置常见列名、字段名原文; // 凡声明 UniqueFields 的模块必须显式写 FieldLabels,提示语以此为准,不依赖数据库列注释 func (s *ModuleSpec[Item, LItem, DRes]) fieldLabel(field string) string { if s.FieldLabels != nil { if v, ok := s.FieldLabels[field]; ok && v != "" { return v } } if v, ok := builtinLabels[field]; ok { return v } return field } // textPlanCache 文本填充计划缓存 var textPlanCache sync.Map // reflect.Type → []textFillPlan // textFillPlan 文本填充计划 // 描述源字段索引、目标文本字段索引与字典编码 type textFillPlan struct { srcIdx, dstIdx int code string } // FillTextFields 批量填充结构体中已声明的 {Field}Text 字段 // // 声明即生效:结构里存在 Text 字段就一定被填充;反射计划按类型缓存。 // target 支持 *[]*Item、*Item、*DRes。 func FillTextFields(ctx context.Context, target any, maps map[string]string) { if len(maps) == 0 || target == nil { return } rv := reflect.ValueOf(target) if rv.Kind() != reflect.Ptr || rv.IsNil() { return } switch rv.Elem().Kind() { case reflect.Slice: s := rv.Elem() for i := 0; i < s.Len(); i++ { fillOneText(ctx, s.Index(i), maps) } default: fillOneText(ctx, rv.Elem(), maps) } } // fillOneText 填充单个结构体的文本字段 func fillOneText(ctx context.Context, v reflect.Value, maps map[string]string) { // 解引用 for v.Kind() == reflect.Ptr { if v.IsNil() { return } v = v.Elem() } if v.Kind() != reflect.Struct { return } plans := plansForText(v.Type(), maps) for _, p := range plans { src := v.Field(p.srcIdx) dst := v.Field(p.dstIdx) if !dst.CanSet() || dst.Type().Kind() != reflect.String { continue } dst.SetString(DictGetText(ctx, p.code, src.Interface())) } } // plansForText 生成或读取文本填充计划 func plansForText(rt reflect.Type, maps map[string]string) []textFillPlan { key := rt.String() if v, ok := textPlanCache.Load(key); ok { if plans, ok2 := v.([]textFillPlan); ok2 { return plans } } var plans []textFillPlan for field, code := range maps { base := pascal(field) dst, ok := rt.FieldByName(base + "Text") if !ok || dst.Type.Kind() != reflect.String || len(dst.Index) != 1 { continue } src, ok := rt.FieldByName(base) if !ok || len(src.Index) != 1 { continue } plans = append(plans, textFillPlan{srcIdx: src.Index[0], dstIdx: dst.Index[0], code: code}) } textPlanCache.Store(key, plans) return plans } // hookWarn 钩子错误仅记日志 // 原样返回错误,保留 gerror 语义码 func hookWarn(ctx context.Context, stage string, err error) error { if err != nil { g.Log().Warningf(ctx, "ModuleSpec %s: %v", stage, err) } return err } // ============================================================================ // 查询流水线 // ============================================================================ // applyFilters 应用查询条件 // // 固定顺序:LikeFields → EqFields → KeywordFields → Filter → DataScope func (s *ModuleSpec[Item, LItem, DRes]) applyFilters(ctx context.Context, m *gdb.Model, req any) *gdb.Model { // 2) 逐列 LIKE(req 同名或驼峰列取值) for _, col := range s.LikeFields { if v := reqValueByColumn(req, col); v != nil { if str, ok := v.(string); ok && str != "" { m = m.WhereLike(col, "%"+str+"%") } } } // 3) 等值(零值跳过) for _, col := range s.EqFields { if v := reqValueByColumn(req, col); v != nil { rv := reflect.ValueOf(v) if rv.IsValid() && !rv.IsZero() { m = m.Where(col, v) } } } // 4) 关键词多列 OR(参数化条件,整体作为一个 AND 项,避免 OR 优先级问题) if len(s.KeywordFields) > 0 { if kw, ok := reqValueByColumn(req, "keyword").(string); ok && kw != "" { likes := make([]string, 0, len(s.KeywordFields)) args := make([]any, 0, len(s.KeywordFields)) for _, col := range s.KeywordFields { likes = append(likes, col+" LIKE ?") args = append(args, "%"+kw+"%") } m = m.Where("("+strings.Join(likes, " OR ")+")", args...) } } // 5) 复杂条件 if s.Filter != nil { m = s.Filter(ctx, m, req) } // 6) 数据权限(nil 跳过) if s.DataScope != nil { m = s.DataScope(ctx, m, req) } return m } // applyOrder 应用排序 // 入参 orderField 与 orderType 仅在 OrderableFields 白名单内生效 func (s *ModuleSpec[Item, LItem, DRes]) applyOrder(m *gdb.Model, req any) *gdb.Model { if len(s.OrderableFields) > 0 { field, _ := reqValueByColumn(req, "order_field").(string) if field != "" { for _, allowed := range s.OrderableFields { if allowed == field { dir := "desc" if t, ok := reqValueByColumn(req, "order_type").(string); ok && strings.EqualFold(t, "asc") { dir = "asc" } return m.Order(field, dir) } } } } if s.OrderBy != "" { return m.Order(s.OrderBy) } return m } // readTransform 读侧转换 // // 依次执行文件字段补域名、富文本还原、枚举文本填充; // FileFields 与 MultiFileFields 声明后读侧自动补可访问 URL func (s *ModuleSpec[Item, LItem, DRes]) readTransform(ctx context.Context, target any) { for _, field := range s.FileFields { fillStringField(target, field, func(v string) string { return upload.GetFileUrl(ctx, v) }) } for _, field := range s.MultiFileFields { fillStringField(target, field, func(v string) string { if v == "" { return v } parts := strings.Split(v, ",") for i, p := range parts { parts[i] = upload.GetFileUrl(ctx, strings.TrimSpace(p)) } return strings.Join(parts, ",") }) } for _, f := range s.ContentFields { fillStringField(target, f, func(v string) string { return ExpandContentFields(ctx, v) }) } FillTextFields(ctx, target, s.SerializeMaps) } // saveTransform 写侧转换 // // 单文件、多文件与富文本字段由临时目录转正至统一目录 func (s *ModuleSpec[Item, LItem, DRes]) saveTransform(ctx context.Context, data map[string]any) { for _, field := range s.FileFields { if str, ok := data[field].(string); ok && str != "" { data[field] = upload.SaveFromTemp(ctx, str, s.FileSaveDir) } } for _, field := range s.MultiFileFields { if str, ok := data[field].(string); ok && str != "" { data[field] = upload.SaveFromTempMulti(ctx, str, s.FileSaveDir) } } for _, field := range s.ContentFields { if str, ok := data[field].(string); ok && str != "" { data[field] = LocalizeContentFields(ctx, str, s.FileSaveDir) } } } // checkUniqueRules 唯一性批量校验 // 提示语按字段标签四级回退 func (s *ModuleSpec[Item, LItem, DRes]) checkUniqueRules(ctx context.Context, data map[string]any, excludeId int64) error { for _, rule := range s.UniqueFields { v, ok := data[rule.Field] if !ok || v == nil { continue } if str, isStr := v.(string); isStr && str == "" { continue } m := s.Model(ctx).Where(rule.Field, v) if rule.Group != "" { if gv, ok := data[rule.Group]; ok { m = m.Where(rule.Group, gv) } } if excludeId > 0 { m = m.WhereNot("id", excludeId) } n, err := m.Count() if err != nil { return err } if n > 0 { return gerror.NewCodef(consts.CodeRecordExists, "%s已存在", s.fieldLabel(rule.Field)) } } return nil } // fillStringField 按 json 字段名对结构或指针结构应用字符串变换 func fillStringField(target any, jsonName string, fn func(string) string) { rv := reflect.ValueOf(target) for rv.Kind() == reflect.Ptr { if rv.IsNil() { return } rv = rv.Elem() } applyOnStruct := func(sv reflect.Value) { if sv.Kind() != reflect.Struct { return } rt := sv.Type() for i := 0; i < rt.NumField(); i++ { tag := strings.Split(rt.Field(i).Tag.Get("json"), ",")[0] if tag == jsonName && rt.Field(i).Type.Kind() == reflect.String && sv.Field(i).CanSet() { sv.Field(i).SetString(fn(sv.Field(i).String())) } } } switch rv.Kind() { case reflect.Slice: for i := 0; i < rv.Len(); i++ { elem := rv.Index(i) for elem.Kind() == reflect.Ptr { elem = elem.Elem() } applyOnStruct(elem) } default: applyOnStruct(rv) } } // ============================================================================ // CRUD 流水线 // ============================================================================ // Page 分页查询 // 流水线:过滤 → 计数 → 分页 → 映射 → 读转换 → 钩子 func (s *ModuleSpec[Item, LItem, DRes]) Page(ctx context.Context, req any, pageNo, pageSize int) ([]*Item, int, error) { m := s.applyFilters(ctx, s.Model(ctx), req) total, err := m.Count() if err != nil { return nil, 0, err } rows, err := s.applyOrder(m, req).Page(pageNo, pageSize).All() if err != nil { return nil, 0, err } list, err := structRows[Item](rows) if err != nil { return nil, 0, err } s.readTransform(ctx, &list) if err = hookWarn(ctx, "AfterPageList", s.callAfterPageList(ctx, list, req)); err != nil { return nil, 0, err } return list, total, nil } // List 全量列表 // // 使用 LItem 类型;IsTree 为真时在末尾建树; // 不挂 AfterPageList,因精简结构无冗余列 func (s *ModuleSpec[Item, LItem, DRes]) List(ctx context.Context, req any) ([]*LItem, error) { m := s.applyFilters(ctx, s.Model(ctx), req) if s.ListOrderBy != "" { m = m.Order(s.ListOrderBy) } else { m = s.applyOrder(m, req) } // 条数上限(req.limit,零值表示不限) if v := reqValueByColumn(req, "limit"); v != nil { if n, ok := v.(int); ok && n > 0 { m = m.Limit(n) } } rows, err := m.All() if err != nil { return nil, err } list, err := structRows[LItem](rows) if err != nil { return nil, err } s.readTransform(ctx, &list) if s.IsTree { s.checkTreeDeclared() return s.buildTree(list), nil } return list, nil } // Detail 详情查询 // 流水线:查询 → 映射 → 读转换 → 钩子 func (s *ModuleSpec[Item, LItem, DRes]) Detail(ctx context.Context, id int64, req any) (*DRes, error) { one, err := s.Model(ctx).Where("id", id).One() if err != nil { return nil, err } if len(one) == 0 { return nil, gerror.NewCode(consts.CodeRecordNotFound) } var item DRes if err = one.Struct(&item); err != nil { return nil, err } s.readTransform(ctx, &item) if err = hookWarn(ctx, "AfterDetail", s.callAfterDetail(ctx, &item, req)); err != nil { return nil, err } return &item, nil } // Add 新增 // 流水线:ToData → BeforeAdd → 文件三类 → 唯一校验 → Insert → AfterAdd func (s *ModuleSpec[Item, LItem, DRes]) Add(ctx context.Context, req any) (int64, error) { data := s.ToData(req) if err := hookWarn(ctx, "BeforeAdd", callBeforeAdd(s.BeforeAdd, ctx, data, req)); err != nil { return 0, err } s.saveTransform(ctx, data) if err := s.checkUniqueRules(ctx, data, 0); err != nil { return 0, err } result, err := s.Model(ctx).Data(data).Insert() if err != nil { return 0, err } id, _ := result.LastInsertId() if err = hookWarn(ctx, "AfterAdd", callAfterAdd(s.AfterAdd, ctx, id, req)); err != nil { return 0, err } return id, nil } // Update 更新 // 全量表单语义,仅更新 data 中出现的键 func (s *ModuleSpec[Item, LItem, DRes]) Update(ctx context.Context, id int64, req any) error { data := s.ToData(req) if err := hookWarn(ctx, "BeforeUpdate", callBeforeUpdate(s.BeforeUpdate, ctx, data, id, req)); err != nil { return err } s.saveTransform(ctx, data) if err := s.checkUniqueRules(ctx, data, id); err != nil { return err } if _, err := s.Model(ctx).Where("id", id).Data(data).Update(); err != nil { return err } return hookWarn(ctx, "AfterUpdate", callAfterUpdate(s.AfterUpdate, ctx, id, req)) } // Delete 删除 // 存在性与守卫由模块 BeforeDelete 决定,随后软删并触发 AfterDelete func (s *ModuleSpec[Item, LItem, DRes]) Delete(ctx context.Context, id int64) error { if err := hookWarn(ctx, "BeforeDelete", callBeforeDelete(s.BeforeDelete, ctx, id)); err != nil { return err } if _, err := s.Model(ctx).Where("id", id).Delete(); err != nil { return err } return hookWarn(ctx, "AfterDelete", callAfterDelete(s.AfterDelete, ctx, id)) } // BatchDelete 批量删除 // 流程:BeforeBatchDelete → 逐条执行 BeforeDelete 与软删 → AfterBatchDelete func (s *ModuleSpec[Item, LItem, DRes]) BatchDelete(ctx context.Context, ids []int64) error { if err := hookWarn(ctx, "BeforeBatchDelete", callBeforeBatch(s.BeforeBatchDelete, ctx, ids)); err != nil { return err } for _, id := range ids { if err := hookWarn(ctx, "BeforeDelete", callBeforeDelete(s.BeforeDelete, ctx, id)); err != nil { return err } if _, err := s.Model(ctx).Where("id", id).Delete(); err != nil { return err } } return hookWarn(ctx, "AfterBatchDelete", callAfterBatch(s.AfterBatchDelete, ctx, ids)) } // MergeOld 全量覆盖辅助 // // 将 data 中缺失或零值的键用库中旧值补齐,供 BeforeUpdate 内使用 func (s *ModuleSpec[Item, LItem, DRes]) MergeOld(ctx context.Context, data map[string]any, id int64) { one, err := s.Model(ctx).Where("id", id).One() if err != nil || len(one) == 0 { return } for k, oldV := range one { if v, ok := data[k]; !ok || v == nil || v == "" || v == 0 { data[k] = oldV } } } // ─── 内部:结构映射与树 ──────────────────────────────────────────── // structRows 数据库行转换为指针切片 // 通过 orm 与 json 标签映射 func structRows[Item any](rows gdb.Result) ([]*Item, error) { list := make([]*Item, 0, len(rows)) for _, row := range rows { var item Item if err := row.Struct(&item); err != nil { return nil, err } list = append(list, &item) } return list, nil } // callAfterPageList 调用分页后钩子 func (s *ModuleSpec[Item, LItem, DRes]) callAfterPageList(ctx context.Context, list []*Item, req any) error { if s.AfterPageList == nil { return nil } return s.AfterPageList(ctx, list, req) } // callAfterDetail 调用详情后钩子 func (s *ModuleSpec[Item, LItem, DRes]) callAfterDetail(ctx context.Context, item *DRes, req any) error { if s.AfterDetail == nil { return nil } return s.AfterDetail(ctx, item, req) } // 钩子 nil 安全包装(字段已是具体类型,直接判空调用) // callBeforeAdd 调用新增前钩子 func callBeforeAdd(f func(context.Context, map[string]any, any) error, ctx context.Context, data map[string]any, req any) error { if f == nil { return nil } return f(ctx, data, req) } // callAfterAdd 调用新增后钩子 func callAfterAdd(f func(context.Context, int64, any) error, ctx context.Context, id int64, req any) error { if f == nil { return nil } return f(ctx, id, req) } // callBeforeUpdate 调用更新前钩子 func callBeforeUpdate(f func(context.Context, map[string]any, int64, any) error, ctx context.Context, data map[string]any, id int64, req any) error { if f == nil { return nil } return f(ctx, data, id, req) } // callAfterUpdate 调用更新后钩子 func callAfterUpdate(f func(context.Context, int64, any) error, ctx context.Context, id int64, req any) error { if f == nil { return nil } return f(ctx, id, req) } // callBeforeDelete 调用删除前钩子 func callBeforeDelete(f func(context.Context, int64) error, ctx context.Context, id int64) error { if f == nil { return nil } return f(ctx, id) } // callAfterDelete 调用删除后钩子 func callAfterDelete(f func(context.Context, int64) error, ctx context.Context, id int64) error { if f == nil { return nil } return f(ctx, id) } // callBeforeBatch 调用批量删除前钩子 func callBeforeBatch(f func(context.Context, []int64) error, ctx context.Context, ids []int64) error { if f == nil { return nil } return f(ctx, ids) } // callAfterBatch 调用批量删除后钩子 func callAfterBatch(f func(context.Context, []int64) error, ctx context.Context, ids []int64) error { if f == nil { return nil } return f(ctx, ids) } // ─── 树 ────────────────────────────────────────────────────────── // checkTreeDeclared 校验树形声明的完整性 func (s *ModuleSpec[Item, LItem, DRes]) checkTreeDeclared() { if s.ParentOf == nil { // 泛型类型名作调试标识 var zero LItem panic(fmt.Sprintf("ModuleSpec: IsTree=true 但未声明 ParentOf(%s)", reflect.TypeOf(&zero).Elem().Name())) } } // childrenIdxCache 子节点字段索引缓存 var childrenIdxCache sync.Map // reflect.Type → children field index(-1 表示无) // childrenSetter 获取子节点写入函数 // // 优先使用声明的 ChildSetter,缺省时反射写入 DTO 的 Children 字段 func (s *ModuleSpec[Item, LItem, DRes]) childrenSetter() func(*LItem, []*LItem) { if s.ChildSetter != nil { return s.ChildSetter } // 缺省:反射写 DTO 的 Children 字段(命名约定) var sample LItem rt := reflect.TypeOf(sample) v, _ := childrenIdxCache.LoadOrStore(rt, func() any { f, ok := rt.FieldByName("Children") if !ok || f.Type.Kind() != reflect.Slice || len(f.Index) != 1 { return -1 } return f.Index[0] }()) // 注意:LoadOrStore 存值不存函数,需立即调用 idx, ok := v.(int) if !ok { idx = -1 } if idx < 0 { panic(fmt.Sprintf("ModuleSpec: IsTree 需要 ChildSetter 或 DTO 的 Children 字段(%s)", rt.Name())) } return func(it *LItem, children []*LItem) { rv := reflect.ValueOf(it).Elem().Field(idx) rv.Set(reflect.ValueOf(children)) } } // buildTree 平铺列表转换为树 // // 先按 ParentOf 分组,再写入各父节点的 Children; // 输入顺序稳定,缺少父节点的元素挂到根 func (s *ModuleSpec[Item, LItem, DRes]) buildTree(list []*LItem) []*LItem { return buildTreeBy(list, s.ParentOf, s.childrenSetter()) } // buildTreeBy 通用树构建 // idOf 约定 DTO 包含 Id 字段,父节点不在集合内时挂根 func buildTreeBy[T any](list []*T, parentOf func(*T) int64, setChildren func(*T, []*T)) []*T { // idOf:约定 DTO 含 Id 字段(int64/int 均可,reflect.Int 读取) idOf := func(it *T) int64 { f := reflect.ValueOf(it).Elem().FieldByName("Id") if !f.IsValid() { panic("ModuleSpec: 树形 DTO 必须包含 Id 字段") } return f.Int() } nodes := make(map[int64]*T, len(list)) children := make(map[int64][]*T, len(list)) ids := make([]int64, 0, len(list)) for _, it := range list { setChildren(it, []*T{}) // 预置空(叶子 JSON 输出 [] 而非 null) idv := idOf(it) nodes[idv] = it ids = append(ids, idv) } roots := make([]*T, 0, len(ids)) for _, idv := range ids { node := nodes[idv] pid := parentOf(node) if pid != 0 { if _, ok := nodes[pid]; ok { children[pid] = append(children[pid], node) continue } } roots = append(roots, node) // 顶级或缺父挂根 } for pid, ch := range children { setChildren(nodes[pid], ch) } return roots } // derefSlice 指针切片转换为值切片 // 统一响应值语义:PageData[T] 的 T 为值类型 func derefSlice[T any](in []*T) []T { out := make([]T, 0, len(in)) for _, p := range in { out = append(out, *p) } return out }