Quellcode durchsuchen

不要组件,直接写了测试通过

qdy vor 1 Monat
Ursprung
Commit
fc6ebaeddc

BIN
.DS_Store Datei anzeigen


+ 53
- 0
model/request/queryreq/operator_enum.go Datei anzeigen

@@ -0,0 +1,53 @@
1
+package queryreq
2
+
3
+// Operator 查询运算符枚举
4
+type Operator string
5
+
6
+const (
7
+	// OpEquals 等于
8
+	OpEquals Operator = "="
9
+	// OpNotEquals 不等于
10
+	OpNotEquals Operator = "!="
11
+	// OpLike 模糊匹配
12
+	OpLike Operator = "like"
13
+	// OpIn IN筛选
14
+	OpIn Operator = "in"
15
+	// OpGreaterThan 大于
16
+	OpGreaterThan Operator = ">"
17
+	// OpLessThan 小于
18
+	OpLessThan Operator = "<"
19
+	// OpGreaterOrEq 大于等于
20
+	OpGreaterOrEq Operator = ">="
21
+	// OpLessOrEq 小于等于
22
+	OpLessOrEq Operator = "<="
23
+)
24
+
25
+// IsValid 检查运算符是否有效
26
+func (o Operator) IsValid() bool {
27
+	switch o {
28
+	case OpEquals, OpNotEquals, OpLike, OpIn, OpGreaterThan,
29
+		OpLessThan, OpGreaterOrEq, OpLessOrEq:
30
+		return true
31
+	default:
32
+		return false
33
+	}
34
+}
35
+
36
+// ToSQL 将运算符转换为SQL操作符
37
+func (o Operator) ToSQL() string {
38
+	switch o {
39
+	case OpLike:
40
+		return "LIKE"
41
+	case OpIn:
42
+		return "IN"
43
+	default:
44
+		return string(o)
45
+	}
46
+}
47
+
48
+// RequiresValue 检查运算符是否需要值
49
+func (o Operator) RequiresValue() bool {
50
+	// 所有定义的运算符都需要值
51
+	// 未来如果添加IS NULL等不需要值的运算符,可以在此排除
52
+	return true
53
+}

+ 202
- 0
model/request/queryreq/query_builder.go Datei anzeigen

@@ -0,0 +1,202 @@
1
+package queryreq
2
+
3
+import (
4
+	"fmt"
5
+	"strings"
6
+)
7
+
8
+// FieldMapper 字段映射函数类型:前端字段名 -> 数据库列名
9
+type FieldMapper func(string) string
10
+
11
+// DefaultFieldMapper 默认字段映射(直接使用原字段名)
12
+func DefaultFieldMapper(field string) string {
13
+	return field
14
+}
15
+
16
+// BuildWhereClause 安全构建WHERE子句
17
+// 返回: WHERE条件字符串, 参数值数组
18
+func BuildWhereClause(filters []FilterParam, fieldMapper FieldMapper) (string, []interface{}) {
19
+	if len(filters) == 0 {
20
+		return "", nil
21
+	}
22
+
23
+	var conditions []string
24
+	var args []interface{}
25
+
26
+	for _, filter := range filters {
27
+		if !filter.Operator.IsValid() {
28
+			continue
29
+		}
30
+
31
+		// 获取安全的数据库列名
32
+		dbField := fieldMapper(filter.Field)
33
+		if dbField == "" {
34
+			continue // 无效字段,跳过
35
+		}
36
+
37
+		// 根据运算符构建条件
38
+		condition, filterArgs := buildCondition(dbField, filter.Operator, filter.Value)
39
+		if condition != "" {
40
+			conditions = append(conditions, condition)
41
+			args = append(args, filterArgs...)
42
+		}
43
+	}
44
+
45
+	if len(conditions) == 0 {
46
+		return "", nil
47
+	}
48
+
49
+	return "WHERE " + strings.Join(conditions, " AND "), args
50
+}
51
+
52
+// BuildOrderByClause 安全构建ORDER BY子句
53
+// 返回: ORDER BY子句字符串
54
+func BuildOrderByClause(sorts []SortParam, fieldMapper FieldMapper) string {
55
+	if len(sorts) == 0 {
56
+		return ""
57
+	}
58
+
59
+	var orderClauses []string
60
+	for _, sort := range sorts {
61
+		// 获取安全的数据库列名
62
+		dbField := fieldMapper(sort.Field)
63
+		if dbField == "" {
64
+			continue // 无效字段,跳过
65
+		}
66
+
67
+		// 验证排序方向
68
+		order := strings.ToUpper(sort.Order)
69
+		if order != "ASC" && order != "DESC" {
70
+			order = "ASC" // 默认升序
71
+		}
72
+
73
+		orderClauses = append(orderClauses, fmt.Sprintf("%s %s", dbField, order))
74
+	}
75
+
76
+	if len(orderClauses) == 0 {
77
+		return ""
78
+	}
79
+
80
+	return "ORDER BY " + strings.Join(orderClauses, ", ")
81
+}
82
+
83
+// buildCondition 构建单个筛选条件
84
+func buildCondition(field string, operator Operator, value interface{}) (string, []interface{}) {
85
+	switch operator {
86
+	case OpEquals, OpNotEquals, OpGreaterThan, OpLessThan, OpGreaterOrEq, OpLessOrEq:
87
+		return fmt.Sprintf("%s %s ?", field, operator.ToSQL()), []interface{}{value}
88
+
89
+	case OpLike:
90
+		// LIKE操作自动添加通配符
91
+		if strVal, ok := value.(string); ok {
92
+			return fmt.Sprintf("%s LIKE ?", field), []interface{}{"%" + strVal + "%"}
93
+		}
94
+		// 非字符串类型,直接使用值
95
+		return fmt.Sprintf("%s LIKE ?", field), []interface{}{value}
96
+
97
+	case OpIn:
98
+		return buildInCondition(field, value)
99
+
100
+	default:
101
+		return "", nil
102
+	}
103
+}
104
+
105
+// buildInCondition 构建IN条件
106
+func buildInCondition(field string, value interface{}) (string, []interface{}) {
107
+	// 处理IN操作符的值
108
+	values, ok := processInValue(value)
109
+	if !ok || len(values) == 0 {
110
+		return "", nil
111
+	}
112
+
113
+	// 构建占位符 (?, ?, ?)
114
+	placeholders := strings.Repeat("?, ", len(values))
115
+	placeholders = placeholders[:len(placeholders)-2] // 移除最后的逗号和空格
116
+
117
+	return fmt.Sprintf("%s IN (%s)", field, placeholders), values
118
+}
119
+
120
+// processInValue 处理IN操作符的值,转换为参数数组
121
+// 支持逗号分隔的字符串、字符串数组、接口数组等
122
+func processInValue(value interface{}) ([]interface{}, bool) {
123
+	switch v := value.(type) {
124
+	case string:
125
+		// 逗号分隔的字符串
126
+		if strings.Contains(v, ",") {
127
+			parts := strings.Split(v, ",")
128
+			var result []interface{}
129
+			for _, part := range parts {
130
+				trimmed := strings.TrimSpace(part)
131
+				if trimmed != "" {
132
+					// 尝试解析为数值
133
+					if num, err := parseNumber(trimmed); err == nil {
134
+						result = append(result, num)
135
+					} else {
136
+						result = append(result, trimmed)
137
+					}
138
+				}
139
+			}
140
+			return result, len(result) > 0
141
+		}
142
+		// 单个字符串值
143
+		return []interface{}{v}, true
144
+
145
+	case []string:
146
+		// 字符串数组
147
+		var result []interface{}
148
+		for _, item := range v {
149
+			if trimmed := strings.TrimSpace(item); trimmed != "" {
150
+				if num, err := parseNumber(trimmed); err == nil {
151
+					result = append(result, num)
152
+				} else {
153
+					result = append(result, trimmed)
154
+				}
155
+			}
156
+		}
157
+		return result, len(result) > 0
158
+
159
+	case []interface{}:
160
+		// 接口数组
161
+		var result []interface{}
162
+		for _, item := range v {
163
+			if item != nil {
164
+				result = append(result, item)
165
+			}
166
+		}
167
+		return result, len(result) > 0
168
+
169
+	default:
170
+		// 其他类型视为单个值
171
+		return []interface{}{v}, true
172
+	}
173
+}
174
+
175
+// parseNumber 尝试将字符串解析为数值
176
+func parseNumber(s string) (interface{}, error) {
177
+	// 尝试解析为整数
178
+	if intVal, err := parseInt(s); err == nil {
179
+		return intVal, nil
180
+	}
181
+
182
+	// 尝试解析为浮点数
183
+	if floatVal, err := parseFloat(s); err == nil {
184
+		return floatVal, nil
185
+	}
186
+
187
+	return nil, fmt.Errorf("not a number")
188
+}
189
+
190
+// parseInt 解析整数
191
+func parseInt(s string) (int64, error) {
192
+	var val int64
193
+	_, err := fmt.Sscanf(s, "%d", &val)
194
+	return val, err
195
+}
196
+
197
+// parseFloat 解析浮点数
198
+func parseFloat(s string) (float64, error) {
199
+	var val float64
200
+	_, err := fmt.Sscanf(s, "%f", &val)
201
+	return val, err
202
+}

+ 63
- 0
model/request/queryreq/query_request.go Datei anzeigen

@@ -0,0 +1,63 @@
1
+package queryreq
2
+
3
+// SortParam 排序参数
4
+type SortParam struct {
5
+	Field string `json:"field"` // 前端字段名
6
+	Order string `json:"order"` // asc/desc
7
+}
8
+
9
+// FilterParam 筛选参数
10
+type FilterParam struct {
11
+	Field    string      `json:"field"`    // 前端字段名
12
+	Operator Operator    `json:"operator"` // 运算符枚举
13
+	Value    interface{} `json:"value"`    // 值(支持多种类型)
14
+}
15
+
16
+// QueryRequest 通用查询请求
17
+type QueryRequest struct {
18
+	Page     int           `json:"page"`     // 页码(从0开始)
19
+	PageSize int           `json:"pageSize"` // 每页大小
20
+	Sort     []SortParam   `json:"sort,omitempty"`
21
+	Filter   []FilterParam `json:"filter,omitempty"`
22
+}
23
+
24
+// Validate 验证查询请求参数
25
+func (q *QueryRequest) Validate() error {
26
+	// 验证分页参数
27
+	if q.Page < 0 {
28
+		q.Page = 0
29
+	}
30
+	if q.PageSize <= 0 {
31
+		q.PageSize = 10
32
+	} else if q.PageSize > 1000 {
33
+		q.PageSize = 1000 // 限制最大页大小
34
+	}
35
+
36
+	// 验证排序参数
37
+	for i := range q.Sort {
38
+		if q.Sort[i].Order != "asc" && q.Sort[i].Order != "desc" {
39
+			q.Sort[i].Order = "asc" // 默认升序
40
+		}
41
+	}
42
+
43
+	// 验证筛选参数
44
+	for i := range q.Filter {
45
+		if !q.Filter[i].Operator.IsValid() {
46
+			// 无效运算符,移除该筛选条件
47
+			q.Filter = append(q.Filter[:i], q.Filter[i+1:]...)
48
+			continue
49
+		}
50
+	}
51
+
52
+	return nil
53
+}
54
+
55
+// GetOffset 计算分页偏移量
56
+func (q *QueryRequest) GetOffset() int {
57
+	return q.Page * q.PageSize
58
+}
59
+
60
+// GetLimit 获取分页限制
61
+func (q *QueryRequest) GetLimit() int {
62
+	return q.PageSize
63
+}

+ 97
- 0
model/request/tabulatorreq/tabulator_request.go Datei anzeigen

@@ -0,0 +1,97 @@
1
+package tabulatorreq
2
+
3
+import (
4
+	"git.x2erp.com/qdy/go-base/model/request/queryreq"
5
+)
6
+
7
+// TabulatorSorter Tabulator排序器格式
8
+type TabulatorSorter struct {
9
+	Field string `json:"field"` // 排序字段
10
+	Dir   string `json:"dir"`   // 排序方向: asc/desc
11
+}
12
+
13
+// TabulatorFilter Tabulator筛选器格式
14
+type TabulatorFilter struct {
15
+	Field string      `json:"field"` // 筛选字段
16
+	Type  string      `json:"type"`  // 筛选类型: =, !=, like, >, <, in等
17
+	Value interface{} `json:"value"` // 筛选值
18
+}
19
+
20
+// TabulatorRequest Tabulator原生请求格式
21
+type TabulatorRequest struct {
22
+	Page   int               `json:"page"`             // 当前页码(从1开始)
23
+	Size   int               `json:"size"`             // 每页大小
24
+	Sort   []TabulatorSorter `json:"sort,omitempty"`   // 排序器数组(单数,匹配前端)
25
+	Filter []TabulatorFilter `json:"filter,omitempty"` // 筛选器数组(单数,匹配前端)
26
+}
27
+
28
+// Validate 验证请求参数
29
+func (r *TabulatorRequest) Validate() {
30
+	if r.Page < 1 {
31
+		r.Page = 1
32
+	}
33
+	if r.Size <= 0 {
34
+		r.Size = 10
35
+	} else if r.Size > 1000 {
36
+		r.Size = 1000
37
+	}
38
+}
39
+
40
+// ToQueryRequest 转换为通用QueryRequest
41
+func (r *TabulatorRequest) ToQueryRequest(fieldMapper func(string) string) *queryreq.QueryRequest {
42
+	req := &queryreq.QueryRequest{
43
+		Page:     r.Page - 1, // Tabulator从1开始,QueryRequest从0开始
44
+		PageSize: r.Size,
45
+	}
46
+
47
+	// 转换排序器
48
+	for _, sorter := range r.Sort {
49
+		field := fieldMapper(sorter.Field)
50
+		if field == "" {
51
+			continue // 无效字段,跳过
52
+		}
53
+		req.Sort = append(req.Sort, queryreq.SortParam{
54
+			Field: field,
55
+			Order: sorter.Dir,
56
+		})
57
+	}
58
+
59
+	// 转换筛选器
60
+	for _, filter := range r.Filter {
61
+		field := fieldMapper(filter.Field)
62
+		if field == "" {
63
+			continue // 无效字段,跳过
64
+		}
65
+		req.Filter = append(req.Filter, queryreq.FilterParam{
66
+			Field:    field,
67
+			Operator: mapOperator(filter.Type),
68
+			Value:    filter.Value,
69
+		})
70
+	}
71
+
72
+	return req
73
+}
74
+
75
+// mapOperator 映射Tabulator操作符到QueryRequest操作符
76
+func mapOperator(tabulatorOp string) queryreq.Operator {
77
+	switch tabulatorOp {
78
+	case "=", "==":
79
+		return queryreq.OpEquals
80
+	case "!=", "<>":
81
+		return queryreq.OpNotEquals
82
+	case "like", "contains":
83
+		return queryreq.OpLike
84
+	case "in":
85
+		return queryreq.OpIn
86
+	case ">":
87
+		return queryreq.OpGreaterThan
88
+	case "<":
89
+		return queryreq.OpLessThan
90
+	case ">=":
91
+		return queryreq.OpGreaterOrEq
92
+	case "<=":
93
+		return queryreq.OpLessOrEq
94
+	default:
95
+		return queryreq.OpEquals
96
+	}
97
+}

+ 1
- 0
model/response/response_model.go Datei anzeigen

@@ -9,6 +9,7 @@ import (
9 9
 // QueryResult 查询结果
10 10
 type QueryResult[T any] struct {
11 11
 	Success    bool                `json:"success"`
12
+	LastPage   int                 `json:"last_page,omitempty"`
12 13
 	Data       T                   `json:"data,omitempty"`
13 14
 	Error      string              `json:"error,omitempty"`
14 15
 	Count      int                 `json:"count,omitempty"`

+ 109
- 0
model/response/tabulator_response.go Datei anzeigen

@@ -0,0 +1,109 @@
1
+package response
2
+
3
+import (
4
+	"encoding/json"
5
+	"fmt"
6
+	"time"
7
+
8
+	"git.x2erp.com/qdy/go-base/ctx"
9
+)
10
+
11
+// TabulatorResponse Tabulator期望的响应格式
12
+type TabulatorResponse struct {
13
+	LastPage int         `json:"last_page"` // 总页数
14
+	Data     interface{} `json:"data"`      // 数据数组
15
+}
16
+
17
+// CreateTabulatorResponse 创建Tabulator格式的响应
18
+func CreateTabulatorResponse[T any](data T, totalCount int, pageSize int, reqCtx *ctx.RequestContext) *QueryResult[map[string]interface{}] {
19
+	// 计算总页数
20
+	lastPage := 1
21
+	if pageSize > 0 && totalCount > 0 {
22
+		lastPage = (totalCount + pageSize - 1) / pageSize
23
+	}
24
+
25
+	// 构建Tabulator格式数据
26
+	tabulatorData := map[string]interface{}{
27
+		"last_page": lastPage,
28
+		"data":      data,
29
+	}
30
+
31
+	// 创建成功结果
32
+	result := &QueryResult[map[string]interface{}]{
33
+		Success:    true,
34
+		Data:       tabulatorData,
35
+		TotalCount: totalCount,
36
+		Time:       time.Now().Format(time.RFC3339),
37
+		Metadata:   reqCtx,
38
+	}
39
+
40
+	// 如果数据是数组,设置Count
41
+	switch v := any(data).(type) {
42
+	case []interface{}:
43
+		result.Count = len(v)
44
+	case []map[string]interface{}:
45
+		result.Count = len(v)
46
+	default:
47
+		// 尝试通过反射获取长度
48
+		// 简化处理,如果无法获取长度,使用TotalCount
49
+		result.Count = totalCount
50
+	}
51
+
52
+	return result
53
+}
54
+
55
+// AdaptToTabulator 将现有QueryResult适配为Tabulator格式
56
+// 此函数主要用于中间件或处理器直接输出Tabulator格式
57
+func AdaptToTabulator[T any](result *QueryResult[T], pageSize int) []byte {
58
+	if !result.Success {
59
+		// 错误响应格式
60
+		errorResponse := map[string]interface{}{
61
+			"error": result.Error,
62
+		}
63
+		if result.Message != "" {
64
+			errorResponse["message"] = result.Message
65
+		}
66
+		jsonData, _ := json.Marshal(errorResponse)
67
+		return jsonData
68
+	}
69
+
70
+	// 计算总页数
71
+	lastPage := 1
72
+	if pageSize > 0 && result.TotalCount > 0 {
73
+		lastPage = (result.TotalCount + pageSize - 1) / pageSize
74
+	}
75
+
76
+	// 构建Tabulator格式响应
77
+	tabulatorResponse := map[string]interface{}{
78
+		"last_page": lastPage,
79
+		"data":      result.Data,
80
+	}
81
+
82
+	// 编码为JSON
83
+	jsonData, err := json.Marshal(tabulatorResponse)
84
+	if err != nil {
85
+		// 编码失败,返回错误
86
+		errorResponse := map[string]interface{}{
87
+			"error": fmt.Sprintf("JSON编码失败: %v", err),
88
+		}
89
+		errorData, _ := json.Marshal(errorResponse)
90
+		return errorData
91
+	}
92
+
93
+	return jsonData
94
+}
95
+
96
+// CreateTabulatorErrorResponse 创建Tabulator格式的错误响应
97
+func CreateTabulatorErrorResponse(errorMsg string, reqCtx *ctx.RequestContext) *QueryResult[map[string]interface{}] {
98
+	errorResponse := map[string]interface{}{
99
+		"error": errorMsg,
100
+	}
101
+
102
+	return &QueryResult[map[string]interface{}]{
103
+		Success:  false,
104
+		Data:     errorResponse,
105
+		Error:    errorMsg,
106
+		Time:     time.Now().Format(time.RFC3339),
107
+		Metadata: reqCtx,
108
+	}
109
+}

Laden…
Abbrechen
Speichern