From d74f82d0609bf7b725cfa3c21fe1649bb652c6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Mon, 5 Jan 2026 17:47:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20struct=5Ffield?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/schema.go | 306 ++++++++++++++++++++++++ struct_field.go => util/struct_field.go | 36 +-- 2 files changed, 308 insertions(+), 34 deletions(-) create mode 100644 openapi/schema.go rename struct_field.go => util/struct_field.go (88%) diff --git a/openapi/schema.go b/openapi/schema.go new file mode 100644 index 0000000..04bf40d --- /dev/null +++ b/openapi/schema.go @@ -0,0 +1,306 @@ +// Package openapi ... +// +// Description : openapi 扩展版本,添加更多标签支持和类型映射 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-05 17:27 +package openapi + +import ( + "reflect" + "strconv" + "strings" + "time" + + "github.com/getkin/kin-openapi/openapi3" +) + +// StructFieldInfo 结构体字段信息 +type StructFieldInfo struct { + Name string `json:"name" dc:"结构体字段名"` + JSONName string `json:"json_name" dc:"json tag"` + Type reflect.Type `json:"type" dc:"字段类型"` + Description string `json:"description" dc:"参数描述"` + Example any `json:"example" dc:"示例值"` + Default any `json:"default" dc:"默认值"` + Required bool `json:"required" dc:"是否必传"` + Min *float64 `json:"min" dc:"最小值"` + Max *float64 `json:"max" dc:"最大值"` + MinLength *uint64 `json:"min_length" dc:"最小长度"` + MaxLength *uint64 `json:"max_length" dc:"最大长度"` + Pattern string `json:"pattern" dc:"模式"` + Format string `json:"format" dc:"格式"` + Enum []any `json:"enum" dc:"枚举值列表"` + OmitEmpty bool `json:"omit_empty" dc:"是否可控"` +} + +func getDesc(field reflect.StructField) string { + +} + +// ParseStructField 解析结构体字段信息 +func ParseStructField(field reflect.StructField) *StructFieldInfo { + if !field.IsExported() { + return nil + } + + info := &StructFieldInfo{ + Name: field.Name, + Type: field.Type, + } + + // 解析 JSON tag + jsonTag := field.Tag.Get("json") + if jsonTag != "" { + parts := strings.Split(jsonTag, ",") + if len(parts) > 0 { + if parts[0] == "-" { + return nil // 跳过该字段 + } + if parts[0] != "" { + info.JSONName = parts[0] + } + } + for _, part := range parts[1:] { + switch part { + case "omitempty": + info.OmitEmpty = true + } + } + } + + if info.JSONName == "" { + // 使用结构体字段名作为默认的 json tag + info.JSONName = info.Name + } + + // 解析其他标签 + if desc := field.Tag.Get("description"); desc != "" { + info.Description = desc + } + + if example := field.Tag.Get("example"); example != "" { + info.Example = parseExampleValue(example, field.Type) + } + + if required := field.Tag.Get("required"); required == "true" { + info.Required = true + } + + if min := field.Tag.Get("min"); min != "" { + if val, err := strconv.ParseFloat(min, 64); err == nil { + info.Min = &val + } + } + + if max := field.Tag.Get("max"); max != "" { + if val, err := strconv.ParseFloat(max, 64); err == nil { + info.Max = &val + } + } + + if minLen := field.Tag.Get("minLength"); minLen != "" { + if val, err := strconv.ParseUint(minLen, 10, 64); err == nil { + info.MinLength = &val + } + } + + if maxLen := field.Tag.Get("maxLength"); maxLen != "" { + if val, err := strconv.ParseUint(maxLen, 10, 64); err == nil { + info.MaxLength = &val + } + } + + if pattern := field.Tag.Get("pattern"); pattern != "" { + info.Pattern = pattern + } + + if format := field.Tag.Get("format"); format != "" { + info.Format = format + } + + if enum := field.Tag.Get("enum"); enum != "" { + enums := strings.Split(enum, ",") + for _, e := range enums { + info.Enum = append(info.Enum, strings.TrimSpace(e)) + } + } + + return info +} + +func parseExampleValue(example string, t reflect.Type) interface{} { + // 根据类型解析示例值 + switch t.Kind() { + case reflect.String: + return example + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if val, err := strconv.ParseInt(example, 10, 64); err == nil { + return val + } + case reflect.Float32, reflect.Float64: + if val, err := strconv.ParseFloat(example, 64); err == nil { + return val + } + case reflect.Bool: + if val, err := strconv.ParseBool(example); err == nil { + return val + } + } + return example +} + +// GenerateOpenAPISchema 生成完整的 OpenAPI Schema +func GenerateOpenAPISchema(s interface{}) *openapi3.SchemaRef { + t := reflect.TypeOf(s) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + return generateSchemaRecursive(t, make(map[string]bool)) +} + +func generateSchemaRecursive(t reflect.Type, seen map[string]bool) *openapi3.SchemaRef { + // 检查循环引用 + typeName := t.PkgPath() + "." + t.Name() + if seen[typeName] && t.Kind() == reflect.Struct { + return &openapi3.SchemaRef{ + Value: openapi3.NewObjectSchema(), + } + } + seen[typeName] = true + defer delete(seen, typeName) + + // 处理不同类型 + switch t.Kind() { + case reflect.Struct: + // 特殊处理 time.Time + if t == reflect.TypeOf(time.Time{}) { + schema := openapi3.NewDateTimeSchema() + return &openapi3.SchemaRef{Value: schema} + } + + schema := openapi3.NewObjectSchema() + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + info := ParseStructField(field) + if info == nil { + continue + } + + fieldSchema := generateSchemaFromType(info.Type, seen) + if fieldSchema == nil { + continue + } + + // 应用字段信息到 Schema + applyFieldInfoToSchema(fieldSchema.Value, info) + + if info.Required && !info.OmitEmpty { + schema.Required = append(schema.Required, info.JSONName) + } + + schema.Properties[info.JSONName] = fieldSchema + } + + return &openapi3.SchemaRef{Value: schema} + + case reflect.Slice, reflect.Array: + elemType := t.Elem() + elemSchema := generateSchemaRecursive(elemType, seen) + if elemSchema == nil { + return nil + } + return &openapi3.SchemaRef{ + Value: openapi3.NewArraySchema().WithItems(elemSchema), + } + + case reflect.Map: + if t.Key().Kind() != reflect.String { + // OpenAPI 只支持字符串键 + return &openapi3.SchemaRef{ + Value: openapi3.NewObjectSchema(), + } + } + valueType := t.Elem() + valueSchema := generateSchemaRecursive(valueType, seen) + if valueSchema == nil { + return nil + } + return &openapi3.SchemaRef{ + Value: openapi3.NewObjectSchema(). + WithAdditionalProperties(valueSchema), + } + + case reflect.Ptr: + return generateSchemaRecursive(t.Elem(), seen) + + case reflect.Interface: + return &openapi3.SchemaRef{Value: openapi3.NewSchema()} + + default: + return generatePrimitiveSchema(t) + } +} + +func generateSchemaFromType(t reflect.Type, seen map[string]bool) *openapi3.SchemaRef { + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + return generateSchemaRecursive(t, seen) +} + +func applyFieldInfoToSchema(schema *openapi3.Schema, info *StructFieldInfo) { + if info.Description != "" { + schema.Description = info.Description + } + if info.Example != nil { + schema.Example = info.Example + } + if info.Min != nil { + schema.Min = &openapi3.Min{Value: *info.Min} + } + if info.Max != nil { + schema.Max = &openapi3.Max{Value: *info.Max} + } + if info.MinLength != nil { + schema.MinLength = *info.MinLength + } + if info.MaxLength != nil { + schema.MaxLength = openapi3.Uint64Ptr(*info.MaxLength) + } + if info.Pattern != "" { + schema.Pattern = info.Pattern + } + if info.Format != "" { + schema.Format = info.Format + } + if len(info.Enum) > 0 { + schema.Enum = info.Enum + } +} + +func generatePrimitiveSchema(t reflect.Type) *openapi3.SchemaRef { + switch t.Kind() { + case reflect.String: + return &openapi3.SchemaRef{Value: openapi3.NewStringSchema()} + case reflect.Bool: + return &openapi3.SchemaRef{Value: openapi3.NewBoolSchema()} + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: + return &openapi3.SchemaRef{Value: openapi3.NewInt32Schema()} + case reflect.Int64: + return &openapi3.SchemaRef{Value: openapi3.NewInt64Schema()} + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32: + return &openapi3.SchemaRef{Value: openapi3.NewInt64Schema().WithMin(0)} + case reflect.Uint64: + return &openapi3.SchemaRef{Value: openapi3.NewInt64Schema().WithMin(0)} + case reflect.Float32: + return &openapi3.SchemaRef{Value: openapi3.NewFloat64Schema()} + case reflect.Float64: + return &openapi3.SchemaRef{Value: openapi3.NewFloat64Schema()} + default: + return &openapi3.SchemaRef{Value: openapi3.NewSchema()} + } +} diff --git a/struct_field.go b/util/struct_field.go similarity index 88% rename from struct_field.go rename to util/struct_field.go index 73b9dac..fc6f68c 100644 --- a/struct_field.go +++ b/util/struct_field.go @@ -1,11 +1,11 @@ -// Package api_doc ... +// Package util ... // // Description : api_doc ... // // Author : go_developer@163.com<白茶清欢> // // Date : 2025-02-12 22:15 -package api_doc +package util import ( "reflect" @@ -23,10 +23,6 @@ type parseStructFieldTag struct { } // GetParamName 获取参数名称 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 21:58 2025/2/11 func (psf parseStructFieldTag) GetParamName(structField reflect.StructField) string { paramNameTagList := []string{ define.TagJson, define.TagForm, @@ -46,10 +42,6 @@ func (psf parseStructFieldTag) GetParamName(structField reflect.StructField) str } // GetParamDesc ... -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 22:01 2025/2/11 func (psf parseStructFieldTag) GetParamDesc(structField reflect.StructField) string { descTagList := []string{define.TagDc, define.TagDesc, define.TagDescription} for _, tag := range descTagList { @@ -63,10 +55,6 @@ func (psf parseStructFieldTag) GetParamDesc(structField reflect.StructField) str } // GetDefaultValue 获取默认值 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 22:05 2025/2/11 func (psf parseStructFieldTag) GetDefaultValue(structField reflect.StructField) any { defaultTagList := []string{define.TagD, define.TagDefault} fieldType := structField.Type.Kind().String() @@ -104,10 +92,6 @@ func (psf parseStructFieldTag) GetDefaultValue(structField reflect.StructField) } // GetValidateRule 获取验证规则 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 15:30 2025/2/13 func (psf parseStructFieldTag) GetValidateRule(structField reflect.StructField) string { defaultTagList := []string{define.TagValidate, define.TagBinding} for _, tag := range defaultTagList { @@ -119,10 +103,6 @@ func (psf parseStructFieldTag) GetValidateRule(structField reflect.StructField) } // Deprecated 是否弃用 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 21:12 2025/2/13 func (psf parseStructFieldTag) Deprecated(structField reflect.StructField) bool { defaultTagList := []string{define.TagDeprecated} for _, tag := range defaultTagList { @@ -134,10 +114,6 @@ func (psf parseStructFieldTag) Deprecated(structField reflect.StructField) bool } // Summary 摘要信息 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 15:15 2025/2/14 func (psf parseStructFieldTag) Summary(structField reflect.StructField) string { defaultTagList := []string{define.TagSummary} for _, tag := range defaultTagList { @@ -153,10 +129,6 @@ func (psf parseStructFieldTag) Summary(structField reflect.StructField) string { } // EnumDescription .枚举值详细描述 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 22:40 2025/2/18 func (psf parseStructFieldTag) EnumDescription(structField reflect.StructField) map[string]string { defaultTagList := []string{define.TagNameEnumDescription} res := map[string]string{} @@ -181,10 +153,6 @@ func (psf parseStructFieldTag) EnumDescription(structField reflect.StructField) } // GetExampleValue 示例值 -// -// Author : go_developer@163.com<白茶清欢> -// -// Date : 14:42 2025/2/20 func (psf parseStructFieldTag) GetExampleValue(structField reflect.StructField) any { descTagList := []string{define.TagEg, define.TagExample} fieldType := structField.Type.Kind().String()