Files
2026-10-20 12:34:18 +08:00

866 lines
30 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// +======================================================================
// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2019-2026 半城风雨团队 版权所有
// +----------------------------------------------------------------------
// | 开源协议: Apache-2.0
// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
// +----------------------------------------------------------------------
// | 官方网站: https://www.easygoadmin.vip
// +----------------------------------------------------------------------
// | 研发团队: 小蚂蚁云团队 荣誉出品
// +======================================================================
// | 版权和免责声明:
// | 一、知识产权声明
// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
// | 二、使用限制与禁止条款
// | 您理解并同意,禁止将本产品用于以下任何行为:
// | 1. 任何违反中华人民共和国法律法规的用途;
// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
// | 三、责任豁免
// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
// | 四、违约与法律责任
// | 如发现任何违反本声明的行为,本团队有权:
// | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
// | 3. 保留进一步采取刑事控告等法律措施的权利。
// | 五、许可范围说明
// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
// | 成的一切经济损失及商誉损害。
// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
// | 许可协议》附件为准。
// +======================================================================
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
}