From a14d0f2c72ac53ddb5bb3814f1f098d9123d84da Mon Sep 17 00:00:00 2001 From: what Date: Fri, 21 Aug 2026 08:57:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Condition/ConditionExpr=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20JSON=20=E5=BA=8F=E5=88=97=E5=8C=96/=E5=8F=8D?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 给 Condition/ConditionExpr 加 MarshalJSON/UnmarshalJSON,中间过一层 DTO (conditionDTO/exprDTO)避免 UnmarshalJSON 递归。FromMap(m map[string]any) 方便从已经解析好的 map(比如请求参数)直接构造 Condition,不用先序列化 成 JSON 字符串再解析一遍。 --- condition_json.go | 161 +++++++++++++++++++++++++++++++++++++++++ condition_json_test.go | 126 ++++++++++++++++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 condition_json.go create mode 100644 condition_json_test.go diff --git a/condition_json.go b/condition_json.go new file mode 100644 index 0000000..35fb431 --- /dev/null +++ b/condition_json.go @@ -0,0 +1,161 @@ +package condition + +import ( + "encoding/json" + "strings" +) + +// conditionDTO 是 Condition 的 JSON 中间结构,用于避免 UnmarshalJSON 递归 +type conditionDTO struct { + Type string `json:"type"` + Describe string `json:"describe,omitempty"` + Exprs []*exprDTO `json:"exprs,omitempty"` + Childrens []*conditionDTO `json:"childrens,omitempty"` +} + +type exprDTO struct { + Operator string `json:"operator"` + Field string `json:"field"` + FieldResource string `json:"fieldResource"` + FieldSqlFunc string `json:"fieldSqlFunc,omitempty"` + FieldSqlFuncParam string `json:"fieldSqlFuncParam,omitempty"` + IgnoreEmptyParam bool `json:"ignoreEmptyParam,omitempty"` + TokenType TokenType `json:"tokenType"` + Token string `json:"token"` +} + +func (this *Condition) UnmarshalJSON(data []byte) error { + var dto conditionDTO + if err := json.Unmarshal(data, &dto); err != nil { + return err + } + conditionFromDTO(this, &dto) + return nil +} + +func (this Condition) MarshalJSON() ([]byte, error) { + return json.Marshal(conditionToDTO(&this)) +} + +func (this *ConditionExpr) UnmarshalJSON(data []byte) error { + var dto exprDTO + if err := json.Unmarshal(data, &dto); err != nil { + return err + } + *this = *exprFromDTO(&dto) + return nil +} + +func (this ConditionExpr) MarshalJSON() ([]byte, error) { + return json.Marshal(exprToDTO(&this)) +} + +// FromMap 将 map[string]any 转换为 *Condition +func FromMap(m map[string]any) (*Condition, error) { + data, err := json.Marshal(m) + if err != nil { + return nil, err + } + cond := New() + if err := json.Unmarshal(data, cond); err != nil { + return nil, err + } + return cond, nil +} + +func conditionFromDTO(c *Condition, dto *conditionDTO) { + if strings.EqualFold(dto.Type, "OR") { + c.typ = OR + } else { + c.typ = AND + } + c.describe = dto.Describe + for _, e := range dto.Exprs { + c.SetExpr(exprFromDTO(e)) + } + for _, child := range dto.Childrens { + childCond := &Condition{} + conditionFromDTO(childCond, child) + c.SetCondition(childCond) + } +} + +func conditionToDTO(c *Condition) *conditionDTO { + typ := "AND" + if c.typ == OR { + typ = "OR" + } + dto := &conditionDTO{Type: typ, Describe: c.describe} + for _, e := range c.exprs { + dto.Exprs = append(dto.Exprs, exprToDTO(e)) + } + for _, child := range c.childrens { + dto.Childrens = append(dto.Childrens, conditionToDTO(child)) + } + return dto +} + +func exprFromDTO(dto *exprDTO) *ConditionExpr { + tokenType := dto.TokenType + if tokenType == "" { + tokenType = STRING + } + return &ConditionExpr{ + operator: ToConditionOperator(dto.Operator), + field: dto.Field, + fieldResource: dto.FieldResource, + fieldSqlFunc: dto.FieldSqlFunc, + fieldSqlFuncParam: dto.FieldSqlFuncParam, + ignoreEmptyParma: dto.IgnoreEmptyParam, + tokenType: tokenType, + token: dto.Token, + } +} + +func exprToDTO(e *ConditionExpr) *exprDTO { + return &exprDTO{ + Operator: operatorToString(e.operator), + Field: e.field, + FieldResource: e.fieldResource, + FieldSqlFunc: e.fieldSqlFunc, + FieldSqlFuncParam: e.fieldSqlFuncParam, + IgnoreEmptyParam: e.ignoreEmptyParma, + TokenType: e.tokenType, + Token: e.token, + } +} + +func operatorToString(op ConditionOperator) string { + switch op { + case IS_NULL: + return "IS NULL" + case IS_NOT_NULL: + return "IS NOT NULL" + case EQ: + return "=" + case NE: + return "!=" + case GT: + return ">" + case GE: + return ">=" + case LT: + return "<" + case LE: + return "<=" + case LIKE: + return "LIKE" + case NOT_LIKE: + return "NOT LIKE" + case IN: + return "IN" + case NOT_IN: + return "NOT IN" + case REGEXP: + return "REGEXP" + case NOT_REGEXP: + return "NOT REGEXP" + default: + return "=" + } +} diff --git a/condition_json_test.go b/condition_json_test.go new file mode 100644 index 0000000..8d289c1 --- /dev/null +++ b/condition_json_test.go @@ -0,0 +1,126 @@ +package condition_test + +import ( + "encoding/json" + "testing" + + "git.fsdpf.net/go/condition" + "github.com/stretchr/testify/suite" +) + +type conditionJSONTest struct { + suite.Suite +} + +func TestConditionJSONSuite(t *testing.T) { + suite.Run(t, new(conditionJSONTest)) +} + +// 构造一个有代表性的多层条件 +func buildTestCondition() *condition.Condition { + cond := condition.New(condition.Describe("测试")) + cond.SetExpr(condition.NewExpr("User", "name", + condition.Operator(condition.LIKE), + condition.Token("张三", condition.STRING), + )) + cond.SetExpr(condition.NewExpr("User", "age", + condition.Operator(condition.GE), + condition.Token("18", condition.STRING), + )) + + sub := condition.New(condition.Type(condition.OR)) + sub.SetExpr(condition.NewExpr("User", "status", + condition.Operator(condition.IN), + condition.Token("active", condition.STRING), + )) + sub.SetExpr(condition.NewExpr("User", "roles", + condition.FieldSqlFn("json_contains", ""), + condition.Token("admin", condition.STRING), + )) + cond.SetCondition(sub) + + return cond +} + +func (t *conditionJSONTest) TestMarshalUnmarshal() { + orig := buildTestCondition() + + data, err := json.Marshal(orig) + t.Require().NoError(err) + t.T().Log("JSON:", string(data)) + + got := condition.New() + t.Require().NoError(json.Unmarshal(data, got)) + + // 二次序列化结果应一致 + data2, err := json.Marshal(got) + t.Require().NoError(err) + t.Equal(string(data), string(data2)) +} + +func (t *conditionJSONTest) TestFromMap() { + m := map[string]any{ + "type": "AND", + "describe": "来自Map", + "exprs": []any{ + map[string]any{ + "operator": "=", + "field": "name", + "fieldResource": "User", + "tokenType": "string", + "token": "张三", + }, + map[string]any{ + "operator": ">=", + "field": "age", + "fieldResource": "User", + "tokenType": "string", + "token": "18", + }, + }, + "childrens": []any{ + map[string]any{ + "type": "OR", + "exprs": []any{ + map[string]any{ + "operator": "IS NULL", + "field": "deleted_at", + "fieldResource": "User", + "tokenType": "string", + "token": "", + }, + }, + }, + }, + } + + cond, err := condition.FromMap(m) + t.Require().NoError(err) + t.False(cond.IsEmpty()) + + data, err := json.Marshal(cond) + t.Require().NoError(err) + t.T().Log("FromMap JSON:", string(data)) +} + +func (t *conditionJSONTest) TestRoundTrip() { + orig := buildTestCondition() + + // Condition → JSON → Condition → JSON,两次 JSON 应一致 + data1, err := json.Marshal(orig) + t.Require().NoError(err) + + cond2 := condition.New() + t.Require().NoError(json.Unmarshal(data1, cond2)) + + data2, err := json.Marshal(cond2) + t.Require().NoError(err) + + t.JSONEq(string(data1), string(data2)) +} + +func (t *conditionJSONTest) TestFromMapEmpty() { + cond, err := condition.FromMap(map[string]any{}) + t.Require().NoError(err) + t.True(cond.IsEmpty()) +}