diff --git a/server/go.sum b/server/go.sum index 54f57a3..c1d25d2 100644 --- a/server/go.sum +++ b/server/go.sum @@ -457,7 +457,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.194/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.633 h1:Yj8s35IjbgaHp4Ic9BZLVGWdN2gXBMtwYi1JJ+qYbrc= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.633/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= diff --git a/server/internal/library/hgorm/handler/sorter.go b/server/internal/library/hgorm/handler/sorter.go index 2b8ced0..9e21c53 100644 --- a/server/internal/library/hgorm/handler/sorter.go +++ b/server/internal/library/hgorm/handler/sorter.go @@ -1,30 +1,95 @@ +// Package handler +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE package handler import ( + "fmt" "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gutil" + "hotgo/internal/consts" "hotgo/internal/model/input/form" + "hotgo/utility/convert" + "regexp" ) // ISorter 排序器接口,实现该接口即可使用Handler匹配排序,支持多字段排序 type ISorter interface { - GetSorters() []form.Sorter + GetSorters() []*form.Sorter } // Sorter 排序器 func Sorter(in ISorter) func(m *gdb.Model) *gdb.Model { return func(m *gdb.Model) *gdb.Model { - hasSort := false + masterTable, ts := convert.GetModelTable(m) + fields, err := m.TableFields(masterTable) + if err != nil { + g.Log().Panicf(m.GetCtx(), "failed to sorter TableFields err:%+v", err) + } + sorters := in.GetSorters() + aliases := extractTableAliases(ts) + if len(aliases) > 0 { + var newSorters []*form.Sorter + var removeIndex []int + for as, table := range aliases { + // 关联表 + fds, err := m.TableFields(table) + if err != nil { + g.Log().Panicf(m.GetCtx(), "failed to sorter TableFields err2:%+v", err) + } + + var sorter2 []*form.Sorter + for k, sorter := range sorters { + if gstr.HasPrefix(sorter.ColumnKey, as) { + sorter2 = append(sorter2, &form.Sorter{ + ColumnKey: gstr.Replace(sorter.ColumnKey, as, ""), + Order: sorter.Order, + }) + removeIndex = append(removeIndex, k) + } + } + + if len(sorter2) > 0 { + sorter2 = mappingAndFilterToTableFields(fds, sorter2) + for _, v := range sorter2 { + v.ColumnKey = fmt.Sprintf("`%v`.`%v`", as, v.ColumnKey) + } + newSorters = append(newSorters, sorter2...) + } + } + + // 移除关联表字段 + sorters = mappingAndFilterToTableFields(fields, removeSorterIndexes(sorters, removeIndex)) + for _, v := range sorters { + v.ColumnKey = fmt.Sprintf("`%v`.`%v`", masterTable, v.ColumnKey) + } + + sorters = append(newSorters, sorters...) + } else { + // 单表 + sorters = mappingAndFilterToTableFields(fields, sorters) + for _, v := range sorters { + v.ColumnKey = fmt.Sprintf("`%v`.`%v`", masterTable, v.ColumnKey) + } + } + + hasSort := false for _, sorter := range sorters { - if len(sorter.ColumnKey) == 0 || !sorter.Sorter { + if len(sorter.ColumnKey) == 0 { continue } switch sorter.Order { - case "descend": + case "descend": // 降序 hasSort = true m = m.OrderDesc(sorter.ColumnKey) - case "ascend": + case "ascend": // 升序 hasSort = true m = m.OrderAsc(sorter.ColumnKey) default: @@ -32,10 +97,98 @@ func Sorter(in ISorter) func(m *gdb.Model) *gdb.Model { } } - // 不存在排序条件 - if !hasSort { - // ... + if hasSort { + return m } - return m + + // 不存在排序条件,默认使用主表主键做降序排序 + var pk string + for name, field := range fields { + if gstr.ContainsI(field.Key, consts.GenCodesIndexPK) { + pk = name + break + } + } + + // 没有主键 + if len(pk) == 0 { + return m + } + + // 存在别名,优先匹配别名 + if len(aliases) > 0 { + for as, table := range aliases { + if table == masterTable { + return m.OrderDesc(fmt.Sprintf("`%v`.`%v`", as, pk)) + } + } + } + return m.OrderDesc(fmt.Sprintf("`%v`.`%v`", masterTable, pk)) } } + +// extractTableAliases 解析关联条件中的关联表别名 +func extractTableAliases(ts string) map[string]string { + re := regexp.MustCompile("`?([^`\\s]+)`?\\s+AS\\s+`?([^`\\s]+)`?\\s") + matches := re.FindAllStringSubmatch(ts, -1) + + result := make(map[string]string) + for _, match := range matches { + result[match[2]] = match[1] + } + return result +} + +// removeSorterIndexes 移除指定索引的排序器 +func removeSorterIndexes(slice []*form.Sorter, indexes []int) []*form.Sorter { + removed := make([]*form.Sorter, 0) + indexMap := make(map[int]bool) + + for _, index := range indexes { + indexMap[index] = true + } + + for i, value := range slice { + if !indexMap[i] { + removed = append(removed, value) + } + } + return removed +} + +// mappingAndFilterToTableFields 将排序字段映射为实际的表字段 +func mappingAndFilterToTableFields(fieldsMap map[string]*gdb.TableField, sorters []*form.Sorter) (ser []*form.Sorter) { + if len(fieldsMap) == 0 { + return + } + + var fields []string + for _, v := range sorters { + fields = append(fields, v.ColumnKey) + } + + fieldsKeyMap := make(map[string]interface{}, len(fieldsMap)) + for k := range fieldsMap { + fieldsKeyMap[k] = nil + } + + var inputFieldsArray = gstr.SplitAndTrim(gstr.Join(fields, ","), ",") + for _, field := range inputFieldsArray { + if _, ok := fieldsKeyMap[field]; ok { + continue + } + + if !gregex.IsMatchString(`^[\w\-]+$`, field) { + continue + } + + if foundKey, _ := gutil.MapPossibleItemByKey(fieldsKeyMap, field); foundKey != "" { + for _, v := range sorters { + if v.ColumnKey == field { + v.ColumnKey = foundKey + } + } + } + } + return sorters +} diff --git a/server/internal/library/hgorm/handler/sorter_test.go b/server/internal/library/hgorm/handler/sorter_test.go new file mode 100644 index 0000000..f531a0d --- /dev/null +++ b/server/internal/library/hgorm/handler/sorter_test.go @@ -0,0 +1,96 @@ +// Package handler +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package handler_test + +import ( + _ "github.com/gogf/gf/contrib/drivers/mysql/v2" + "github.com/gogf/gf/v2/os/gctx" + "hotgo/internal/dao" + "hotgo/internal/library/hgorm" + "hotgo/internal/library/hgorm/handler" + "hotgo/internal/model/input/form" + "testing" +) + +type SorterInput struct { + form.Sorters +} + +// TestSorterDefault 默认排序 +func TestSorterDefault(t *testing.T) { + in := &SorterInput{} // 不存在排序条件,默认使用主表主键降序排序 + + _, err := dao.SysGenCurdDemo.Ctx(gctx.New()).Handler(handler.Sorter(in)).All() + if err != nil { + t.Error(err) + return + } +} + +// TestSorter 多字段排序 +func TestSorter(t *testing.T) { + in := &SorterInput{ + Sorters: form.Sorters{ + Sorters: []*form.Sorter{ + { + ColumnKey: "id", + Order: "descend", // 降序 + }, + { + ColumnKey: "categoryId", // 自动转换为下划线。categoryId -> category_id + Order: false, // 不参与排序 + }, + { + ColumnKey: "created_at", + Order: "descend", // 降序 + }, + }, + }, + } + + _, err := dao.SysGenCurdDemo.Ctx(gctx.New()).Handler(handler.Sorter(in)).All() + if err != nil { + t.Error(err) + return + } +} + +// TestSorterJoinTable 关联表多字段排序 +func TestSorterJoinTable(t *testing.T) { + in := &SorterInput{ + Sorters: form.Sorters{ + Sorters: []*form.Sorter{ + { + ColumnKey: "id", + Order: "descend", // 降序 + }, + { + ColumnKey: "categoryId", // 自动转换为下划线。categoryId -> category_id + Order: false, // 不参与排序 + }, + { + ColumnKey: "created_at", + Order: "descend", // 降序 + }, + { + ColumnKey: "testCategoryName", // 自动识别关联表别名。 testCategoryName -> testCategory.name + Order: "ascend", // 升序 + }, + }, + }, + } + + _, err := dao.SysGenCurdDemo.Ctx(gctx.New()). + LeftJoin(hgorm.GenJoinOnRelation( + dao.SysGenCurdDemo.Table(), dao.SysGenCurdDemo.Columns().CategoryId, // 主表表名,关联字段 + dao.TestCategory.Table(), "testCategory", dao.TestCategory.Columns().Id, // 关联表表名,别名,关联字段 + )...). + Handler(handler.Sorter(in)).All() + if err != nil { + t.Error(err) + return + } +} diff --git a/server/internal/model/input/form/sorter.go b/server/internal/model/input/form/sorter.go index 71340db..1cb11e1 100644 --- a/server/internal/model/input/form/sorter.go +++ b/server/internal/model/input/form/sorter.go @@ -7,15 +7,14 @@ package form // Sorter 排序器,兼容naiveUI type Sorter struct { - ColumnKey string `json:"columnKey" dc:"排序字段"` - Sorter bool `json:"sorter" dc:"是否需要排序"` - Order interface{} `json:"order" dc:"排序方式 descend|ascend|false"` + ColumnKey string `json:"columnKey" dc:"排序字段"` + Order interface{} `json:"order" dc:"排序方式 descend|ascend|false"` } type Sorters struct { - Sorters []Sorter `json:"sorters" dc:"排序器"` + Sorters []*Sorter `json:"sorters" dc:"排序器"` } -func (s *Sorters) GetSorters() []Sorter { +func (s *Sorters) GetSorters() []*Sorter { return s.Sorters } diff --git a/server/utility/convert/convert.go b/server/utility/convert/convert.go index 8b13a37..4960e9b 100644 --- a/server/utility/convert/convert.go +++ b/server/utility/convert/convert.go @@ -6,10 +6,13 @@ package convert import ( + "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gstr" "hotgo/utility/validate" "reflect" "unicode" + "unsafe" ) var ( @@ -17,6 +20,33 @@ var ( fieldTags = []string{"json"} // 实体字段名称映射 ) +// GetModelTable 获取模型中的表定义 +func GetModelTable(m *gdb.Model) (tablesInit, tables string) { + if m == nil { + return "", "" + } + + v := reflect.ValueOf(m).Elem() + t := reflect.TypeOf(m).Elem() + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + val := v.Field(i) + + if field.Name == "tablesInit" { + tablesInit = reflect.NewAt(val.Type(), unsafe.Pointer(val.UnsafeAddr())).Elem().String() + tablesInit = gstr.Replace(tablesInit, "`", "") + continue + } + + if field.Name == "tables" { + tables = reflect.NewAt(val.Type(), unsafe.Pointer(val.UnsafeAddr())).Elem().String() + continue + } + } + return +} + // GetMapKeys 获取map的所有key func GetMapKeys[K comparable](m map[K]any) []K { j := 0 diff --git a/web/src/views/addons/hgexample/table/model.ts b/web/src/views/addons/hgexample/table/model.ts index 0b7f514..eee7c4c 100644 --- a/web/src/views/addons/hgexample/table/model.ts +++ b/web/src/views/addons/hgexample/table/model.ts @@ -487,6 +487,7 @@ export const columns = [ render(row) { return formatToDate(row.activityAt); }, + sorter: true, }, ];