package base import ( "encoding/json" "sort" "strconv" jschema "github.com/google/jsonschema-go/jsonschema" "github.com/spf13/cast" ) type ResApiParam struct { ID int64 `db:"id" json:"id"` PID int64 `db:"pid" json:"pid"` Code string `db:"code" json:"code"` Name string `db:"name" json:"name"` Category string `db:"category" json:"category"` DataType string `db:"type" json:"type"` Desc string `db:"desc" json:"desc"` IsRequired bool `db:"isRequired" json:"isRequired"` Constraints map[string]any `db:"constraints" json:"constraints"` Rank int `db:"rank" json:"rank"` RefID int64 `db:"ref_id" json:"ref_id"` DefaultVal string `db:"df_val" json:"df_val"` } type ResApiParams []ResApiParam // ToJSONSchema 将扁平参数列表还原为 JSON Schema 对象 func (params ResApiParams) ToJSONSchema() *jschema.Schema { return params.buildObject(0) } // ResApiParamsFromJSONSchema 将 JSON Schema 对象解析为扁平参数列表 func ResApiParamsFromJSONSchema(schema *jschema.Schema) ResApiParams { var params ResApiParams var seq int64 params.parseObject(schema, 0, &seq) return params } func (params ResApiParams) childrenOf(pid int64) ResApiParams { var result ResApiParams for _, p := range params { if p.PID == pid { result = append(result, p) } } sort.Slice(result, func(i, j int) bool { return result[i].Rank < result[j].Rank }) return result } func (params ResApiParams) buildObject(pid int64) *jschema.Schema { s := &jschema.Schema{Type: "object"} props := map[string]*jschema.Schema{} for _, p := range params.childrenOf(pid) { if p.Code == "items" || p.Code == "oneOf" { continue } props[p.Code] = params.buildProperty(p) if p.IsRequired { s.Required = append(s.Required, p.Code) } } if len(props) > 0 { s.Properties = props } sort.Strings(s.Required) return s } func (params ResApiParams) findByID(id int64) (ResApiParam, bool) { for _, p := range params { if p.ID == id { return p, true } } return ResApiParam{}, false } // pathOf 计算参数在生成的 JSON Schema 中的路径 func (params ResApiParams) pathOf(id int64) string { p, ok := params.findByID(id) if !ok { return "" } if p.PID == 0 { return "#/properties/" + p.Code } parent := params.pathOf(p.PID) switch p.Code { case "items": return parent + "/items" case "oneOf": return parent + "/items/oneOf/" + strconv.Itoa(p.Rank) default: return parent + "/properties/" + p.Code } } func (params ResApiParams) buildProperty(p ResApiParam) *jschema.Schema { if p.RefID != 0 { if _, ok := params.findByID(p.RefID); ok { return &jschema.Schema{Ref: params.pathOf(p.RefID), Description: p.Desc} } } // any 类型:不设 type,从 oneOf 子节点构建 if p.DataType == "any" { s := &jschema.Schema{Title: p.Name, Description: p.Desc} if p.DefaultVal != "" { var tmp any if err := json.Unmarshal([]byte(p.DefaultVal), &tmp); err == nil { s.Default = json.RawMessage(p.DefaultVal) } else { s.Default, _ = json.Marshal(p.DefaultVal) } } for _, child := range params.childrenOf(p.ID) { c := child if child.Code == "oneOf" { s.OneOf = append(s.OneOf, params.buildProperty(c)) } } params.applyConstraints(s, p.Constraints) return s } // 归一化 DataType:json → object;空字符串根据子节点推断 dataType := p.DataType switch dataType { case "json": dataType = "object" case "bool": dataType = "boolean" case "": for _, c := range params.childrenOf(p.ID) { if c.Code == "oneOf" || c.Code == "items" { dataType = "array" break } } if dataType == "" && len(params.childrenOf(p.ID)) > 0 { dataType = "object" } } s := &jschema.Schema{ Type: dataType, Title: p.Name, Description: p.Desc, } if p.DefaultVal != "" { var tmp any if err := json.Unmarshal([]byte(p.DefaultVal), &tmp); err == nil { s.Default = json.RawMessage(p.DefaultVal) } else { s.Default, _ = json.Marshal(p.DefaultVal) } } params.applyConstraints(s, p.Constraints) switch dataType { case "object": nested := params.buildObject(p.ID) s.Properties = nested.Properties s.Required = nested.Required case "array": var oneOfSchemas []*jschema.Schema var itemsChild *ResApiParam for _, child := range params.childrenOf(p.ID) { c := child switch child.Code { case "oneOf": oneOfSchemas = append(oneOfSchemas, params.buildProperty(c)) case "items": itemsChild = &c } } if len(oneOfSchemas) > 0 { s.Items = &jschema.Schema{OneOf: oneOfSchemas} } else if itemsChild != nil { s.Items = params.buildItems(*itemsChild) } } return s } func (params ResApiParams) buildItems(p ResApiParam) *jschema.Schema { if ref, ok := p.Constraints["$ref"].(string); ok { return &jschema.Schema{Ref: ref} } if p.RefID != 0 { if ref, ok := params.findByID(p.RefID); ok { path := params.pathOf(p.RefID) if ref.DataType == "array" { path += "/items" } return &jschema.Schema{Ref: path} } } itemType := p.DataType if itemType == "json" { itemType = "object" } else if itemType == "" && len(params.childrenOf(p.ID)) > 0 { itemType = "object" } s := &jschema.Schema{ Type: itemType, Title: p.Name, Description: p.Desc, } if s.Description == "" && s.Title != "" { s.Description = s.Title } if itemType == "object" { nested := params.buildObject(p.ID) s.Properties = nested.Properties s.Required = nested.Required } params.applyConstraints(s, p.Constraints) return s } func (params ResApiParams) applyConstraints(s *jschema.Schema, c map[string]any) { for k, v := range c { switch k { case "$ref": if ref, ok := v.(string); ok { s.Ref = ref } case "enum": if arr, ok := v.([]any); ok { s.Enum = arr } case "format": if f, ok := v.(string); ok { s.Format = f } case "pattern": if p, ok := v.(string); ok { s.Pattern = p } case "minLength": s.MinLength = jschema.Ptr(cast.ToInt(v)) case "maxLength": s.MaxLength = jschema.Ptr(cast.ToInt(v)) case "minimum": s.Minimum = jschema.Ptr(cast.ToFloat64(v)) case "maximum": s.Maximum = jschema.Ptr(cast.ToFloat64(v)) case "multipleOf": s.MultipleOf = jschema.Ptr(cast.ToFloat64(v)) case "minItems": s.MinItems = jschema.Ptr(cast.ToInt(v)) case "maxItems": s.MaxItems = jschema.Ptr(cast.ToInt(v)) case "minProperties": s.MinProperties = jschema.Ptr(cast.ToInt(v)) case "maxProperties": s.MaxProperties = jschema.Ptr(cast.ToInt(v)) case "additionalProperties": switch val := v.(type) { case bool: if val { s.AdditionalProperties = &jschema.Schema{} } else { s.AdditionalProperties = &jschema.Schema{Not: &jschema.Schema{}} } } } } } func (params *ResApiParams) parseObject(schema *jschema.Schema, pid int64, seq *int64) { requiredSet := make(map[string]bool, len(schema.Required)) for _, r := range schema.Required { requiredSet[r] = true } rank := 0 for code, prop := range schema.Properties { *seq++ id := *seq p := ResApiParam{ ID: id, PID: pid, Code: code, DataType: prop.Type, Name: prop.Title, Desc: prop.Description, IsRequired: requiredSet[code], DefaultVal: params.rawToStr(prop.Default), Rank: rank, Constraints: params.extractConstraints(prop), } *params = append(*params, p) rank++ switch prop.Type { case "object": params.parseObject(prop, id, seq) case "array": if prop.Items == nil { break } // items 为纯 oneOf schema:建立 oneOf 子行,不建 items 子行 if len(prop.Items.OneOf) > 0 { for oneOfRank, sub := range prop.Items.OneOf { *seq++ oneOfID := *seq itemType := sub.Type if itemType == "" { itemType = "object" } *params = append(*params, ResApiParam{ ID: oneOfID, PID: id, Code: "oneOf", DataType: itemType, Name: sub.Title, Desc: sub.Description, Rank: oneOfRank, Constraints: params.extractConstraints(sub), }) if itemType == "object" { params.parseObject(sub, oneOfID, seq) } } break } *seq++ itemID := *seq itemType := prop.Items.Type if itemType == "" { itemType = "object" } *params = append(*params, ResApiParam{ ID: itemID, PID: id, Code: "items", DataType: itemType, Name: prop.Items.Title, Desc: prop.Items.Description, Constraints: params.extractConstraints(prop.Items), }) if itemType == "object" { params.parseObject(prop.Items, itemID, seq) } } } } func (params ResApiParams) extractConstraints(s *jschema.Schema) map[string]any { c := map[string]any{} if s.Ref != "" { c["$ref"] = s.Ref } if len(s.Enum) > 0 { c["enum"] = s.Enum } if s.Format != "" { c["format"] = s.Format } if s.Pattern != "" { c["pattern"] = s.Pattern } if s.MinLength != nil { c["minLength"] = *s.MinLength } if s.MaxLength != nil { c["maxLength"] = *s.MaxLength } if s.Minimum != nil { c["minimum"] = *s.Minimum } if s.Maximum != nil { c["maximum"] = *s.Maximum } if s.MultipleOf != nil { c["multipleOf"] = *s.MultipleOf } if s.MinItems != nil { c["minItems"] = *s.MinItems } if s.MaxItems != nil { c["maxItems"] = *s.MaxItems } if s.MinProperties != nil { c["minProperties"] = *s.MinProperties } if s.MaxProperties != nil { c["maxProperties"] = *s.MaxProperties } if s.AdditionalProperties != nil { c["additionalProperties"] = s.AdditionalProperties.Not == nil } if len(c) == 0 { return nil } return c } func (params ResApiParams) rawToStr(raw json.RawMessage) string { if len(raw) == 0 { return "" } var s string if err := json.Unmarshal(raw, &s); err == nil { return s } return string(raw) }