// Package openapi ... // // Description : openapi 扩展版本,添加更多标签支持和类型映射 // // Author : go_developer@163.com<白茶清欢> // // Date : 2026-01-05 17:27 package openapi import ( "reflect" "strconv" "strings" "time" "git.zhangdeman.cn/zhangdeman/api-doc/define" "git.zhangdeman.cn/zhangdeman/api-doc/util" "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:"是否可控"` } // 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(define.TagJson) 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 define.TagNameOmitempty: info.OmitEmpty = true } } } if info.JSONName == "" { // 使用结构体字段名作为默认的 json tag info.JSONName = info.Name } // 解析参数描述 info.Description = util.ParseStructFieldTag.GetParamDesc(field) // 解析示例值 info.Example = util.ParseStructFieldTag.GetExampleValue(field) // 解析验证规则 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.Value), } 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.Value), } 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 } schema.Min = info.Min if info.Max != nil { schema.Max = 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()} } }