From b9c5ed591a488f4320c018d5fe31a48fb48a21fb 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:22:32 +0800 Subject: [PATCH 01/26] =?UTF-8?q?feat:=20=E5=BC=95=E5=85=A5=20github.com/g?= =?UTF-8?q?etkin/kin-openapi/openapi3=20=E5=BA=93,=20=E5=87=86=E5=A4=87?= =?UTF-8?q?=E9=87=8D=E5=86=99=E6=96=87=E6=A1=A3=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 8 ++++++++ go.sum | 18 ++++++++++++++++++ openapi/generate.go | 15 +++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 openapi/generate.go diff --git a/go.mod b/go.mod index d5b2768..e49c936 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect @@ -41,14 +42,20 @@ require ( github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.1 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.58.0 // indirect github.com/sbabiv/xml2map v1.2.1 // indirect @@ -59,6 +66,7 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect + github.com/woodsbury/decimal128 v1.4.0 // indirect go.uber.org/mock v0.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.23.0 // indirect diff --git a/go.sum b/go.sum index 7ffc02b..796f7b6 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= @@ -41,6 +43,7 @@ github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3Zlm github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= @@ -82,6 +85,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -94,6 +99,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -101,8 +108,16 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= @@ -111,6 +126,7 @@ github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4U github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/sbabiv/xml2map v1.2.1 h1:1lT7t0hhUvXZCkdxqtq4n8/ZCnwLWGq4rDuDv5XOoFE= github.com/sbabiv/xml2map v1.2.1/go.mod h1:2TPoAfcaM7+Sd4iriPvzyntb2mx7GY+kkQpB/GQa/eo= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= @@ -148,6 +164,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= +github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= diff --git a/openapi/generate.go b/openapi/generate.go new file mode 100644 index 0000000..85316c9 --- /dev/null +++ b/openapi/generate.go @@ -0,0 +1,15 @@ +// Package openapi ... +// +// Description : openapi ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-05 17:20 +package openapi + +func NewGenerate() *Generate { + return &Generate{} +} + +// Generate 生成 OpenApi 标准规范的文档 +type Generate struct{} -- 2.36.6 From e031f56393b7c79f8391732236553643a787ab3e 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:25:25 +0800 Subject: [PATCH 02/26] =?UTF-8?q?feat:=20=E8=A7=84=E5=88=92=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=8E=A5=E5=8F=A3=E6=96=87=E6=A1=A3=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/generate.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openapi/generate.go b/openapi/generate.go index 85316c9..2bf5a7d 100644 --- a/openapi/generate.go +++ b/openapi/generate.go @@ -13,3 +13,8 @@ func NewGenerate() *Generate { // Generate 生成 OpenApi 标准规范的文档 type Generate struct{} + +// AddApiDoc 添加接口文档 +func (g *Generate) AddApiDoc(request any, response any) error { + return nil +} -- 2.36.6 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 03/26] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20struct=5Ffie?= =?UTF-8?q?ld?= 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() -- 2.36.6 From 2996fc4732522703acf4282df331b516e87c330c 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 18:02:47 +0800 Subject: [PATCH 04/26] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E6=8F=8F=E8=BF=B0=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 ++ go.sum | 4 ++++ openapi/schema.go | 24 ++++++++----------- util/struct_field.go | 55 +++++++++++++++++++++++--------------------- 4 files changed, 44 insertions(+), 41 deletions(-) diff --git a/go.mod b/go.mod index e49c936..240354d 100644 --- a/go.mod +++ b/go.mod @@ -49,9 +49,11 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/mozillazg/go-pinyin v0.21.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/go.sum b/go.sum index 796f7b6..2184c20 100644 --- a/go.sum +++ b/go.sum @@ -103,6 +103,8 @@ github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8 github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -110,6 +112,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/mozillazg/go-pinyin v0.21.0 h1:Wo8/NT45z7P3er/9YSLHA3/kjZzbLz5hR7i+jGeIGao= +github.com/mozillazg/go-pinyin v0.21.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= diff --git a/openapi/schema.go b/openapi/schema.go index 04bf40d..2e8b5b4 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -13,6 +13,8 @@ import ( "strings" "time" + "git.zhangdeman.cn/zhangdeman/api-doc/define" + "git.zhangdeman.cn/zhangdeman/api-doc/util" "github.com/getkin/kin-openapi/openapi3" ) @@ -35,10 +37,6 @@ type StructFieldInfo struct { OmitEmpty bool `json:"omit_empty" dc:"是否可控"` } -func getDesc(field reflect.StructField) string { - -} - // ParseStructField 解析结构体字段信息 func ParseStructField(field reflect.StructField) *StructFieldInfo { if !field.IsExported() { @@ -51,7 +49,7 @@ func ParseStructField(field reflect.StructField) *StructFieldInfo { } // 解析 JSON tag - jsonTag := field.Tag.Get("json") + jsonTag := field.Tag.Get(define.TagJson) if jsonTag != "" { parts := strings.Split(jsonTag, ",") if len(parts) > 0 { @@ -64,7 +62,7 @@ func ParseStructField(field reflect.StructField) *StructFieldInfo { } for _, part := range parts[1:] { switch part { - case "omitempty": + case define.TagNameOmitempty: info.OmitEmpty = true } } @@ -75,15 +73,11 @@ func ParseStructField(field reflect.StructField) *StructFieldInfo { 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) - } - + // 解析参数描述 + info.Description = util.ParseStructFieldTag.GetParamDesc(field) + // 解析示例值 + info.Example = util.ParseStructFieldTag.GetExampleValue(field) + // 解析验证规则 if required := field.Tag.Get("required"); required == "true" { info.Required = true } diff --git a/util/struct_field.go b/util/struct_field.go index fc6f68c..6cc34d1 100644 --- a/util/struct_field.go +++ b/util/struct_field.go @@ -13,6 +13,7 @@ import ( "strings" "git.zhangdeman.cn/zhangdeman/api-doc/define" + utilPkg "git.zhangdeman.cn/zhangdeman/util" ) var ( @@ -154,36 +155,38 @@ func (psf parseStructFieldTag) EnumDescription(structField reflect.StructField) // GetExampleValue 示例值 func (psf parseStructFieldTag) GetExampleValue(structField reflect.StructField) any { - descTagList := []string{define.TagEg, define.TagExample} - fieldType := structField.Type.Kind().String() - for _, tag := range descTagList { - val := strings.TrimSpace(structField.Tag.Get(tag)) - if val == "" { + exampleTagList := []string{define.TagEg, define.TagExample} + structFieldType := structField.Type + if structFieldType.Kind() == reflect.Ptr { + structFieldType = structFieldType.Elem() + } + fieldTypeKind := structFieldType.Kind() + for _, tag := range exampleTagList { + example := strings.TrimSpace(structField.Tag.Get(tag)) + if example == "" { continue } - if strings.HasPrefix(fieldType, "int") { - i, _ := strconv.Atoi(val) - return i - } - if strings.HasPrefix(fieldType, "uint") { - uintVal, _ := strconv.ParseUint(val, 10, 64) - return uintVal - } - if strings.HasPrefix(fieldType, "float") { - floatVal, _ := strconv.ParseFloat(val, 64) - return floatVal - } - if strings.HasPrefix(fieldType, "string") { - return val - } - if strings.HasPrefix(fieldType, "bool") { - if val == "true" { - return true - } else { - return false + switch fieldTypeKind { + case reflect.String: + return example + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + var res int64 + if err := utilPkg.ConvertAssign(&res, example); err == nil { + return res } + case reflect.Float32, reflect.Float64: + var res float64 + if err := utilPkg.ConvertAssign(&res, example); err == nil { + return res + } + case reflect.Bool: + var res bool + if err := utilPkg.ConvertAssign(&res, example); err == nil { + return res + } + default: + return example } - return val } return nil } -- 2.36.6 From 20b6c48640ad5bd23054ac9defc1122be8c1e856 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 18:05:45 +0800 Subject: [PATCH 05/26] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=80=92?= =?UTF-8?q?=E5=BD=92=E8=B0=83=E7=94=A8=E7=B1=BB=E5=9E=8B=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi/schema.go b/openapi/schema.go index 2e8b5b4..4a6f87f 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -208,7 +208,7 @@ func generateSchemaRecursive(t reflect.Type, seen map[string]bool) *openapi3.Sch return nil } return &openapi3.SchemaRef{ - Value: openapi3.NewArraySchema().WithItems(elemSchema), + Value: openapi3.NewArraySchema().WithItems(elemSchema.Value), } case reflect.Map: -- 2.36.6 From 8ce50fd235227175443e6433f63c068ce166c0b6 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 18:16:48 +0800 Subject: [PATCH 06/26] save code --- openapi/schema.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openapi/schema.go b/openapi/schema.go index 4a6f87f..a165863 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -225,7 +225,7 @@ func generateSchemaRecursive(t reflect.Type, seen map[string]bool) *openapi3.Sch } return &openapi3.SchemaRef{ Value: openapi3.NewObjectSchema(). - WithAdditionalProperties(valueSchema), + WithAdditionalProperties(valueSchema.Value), } case reflect.Ptr: @@ -253,11 +253,10 @@ func applyFieldInfoToSchema(schema *openapi3.Schema, info *StructFieldInfo) { if info.Example != nil { schema.Example = info.Example } - if info.Min != nil { - schema.Min = &openapi3.Min{Value: *info.Min} - } + schema.Min = info.Min + if info.Max != nil { - schema.Max = &openapi3.Max{Value: *info.Max} + schema.Max = info.Max } if info.MinLength != nil { schema.MinLength = *info.MinLength -- 2.36.6 From 1157973b655654a547aa28ddaaac041345d2980d 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 18:23:23 +0800 Subject: [PATCH 07/26] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=9E=9A?= =?UTF-8?q?=E4=B8=BE=E5=80=BC=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- define/tag.go | 5 +++++ openapi/schema.go | 38 +++++++++++++++++--------------------- util/struct_field.go | 12 ++++++------ 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/define/tag.go b/define/tag.go index 78ae579..b517731 100644 --- a/define/tag.go +++ b/define/tag.go @@ -36,3 +36,8 @@ const ( TagNameOmitempty = "omitempty" TagNameEnumDescription = "enum-desc" // 枚举值描述: enum1:enum1-desc||enum2:enum2-desc ) + +type EnumValue struct { + Value string `json:"value" dc:"枚举值"` + Description string `json:"description" dc:"枚举值描述"` +} diff --git a/openapi/schema.go b/openapi/schema.go index a165863..a9920d4 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -20,21 +20,21 @@ import ( // 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:"是否可控"` + 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 []define.EnumValue `json:"enum" dc:"枚举值列表"` + OmitEmpty bool `json:"omit_empty" dc:"是否可控"` } // ParseStructField 解析结构体字段信息 @@ -114,12 +114,8 @@ func ParseStructField(field reflect.StructField) *StructFieldInfo { 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)) - } - } + // 解析枚举值 + info.Enum = util.ParseStructFieldTag.EnumDescription(field) return info } diff --git a/util/struct_field.go b/util/struct_field.go index 6cc34d1..7b82fde 100644 --- a/util/struct_field.go +++ b/util/struct_field.go @@ -130,9 +130,9 @@ func (psf parseStructFieldTag) Summary(structField reflect.StructField) string { } // EnumDescription .枚举值详细描述 -func (psf parseStructFieldTag) EnumDescription(structField reflect.StructField) map[string]string { +func (psf parseStructFieldTag) EnumDescription(structField reflect.StructField) []define.EnumValue { defaultTagList := []string{define.TagNameEnumDescription} - res := map[string]string{} + res := make([]define.EnumValue, 0) for _, tag := range defaultTagList { if tagVal, exist := structField.Tag.Lookup(tag); exist && len(tagVal) > 0 { tagVal = strings.ReplaceAll(tagVal, "###", "`") @@ -142,14 +142,14 @@ func (psf parseStructFieldTag) EnumDescription(structField reflect.StructField) if len(enumArr) < 2 { continue } - res[enumArr[0]] = strings.Join(enumArr[1:], ":") + res = append(res, define.EnumValue{ + Value: enumArr[0], + Description: strings.Join(enumArr[1:], ":"), + }) } return res } } - if len(res) == 0 { - return nil - } return res } -- 2.36.6 From f4447d50d66e0f319b9f4a2dcc6d33cab55d7f4d 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 18:28:11 +0800 Subject: [PATCH 08/26] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E6=9E=9A?= =?UTF-8?q?=E4=B8=BE=E5=80=BC=E8=B5=8B=E5=80=BC=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/schema.go | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/openapi/schema.go b/openapi/schema.go index a9920d4..20b192b 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -120,27 +120,6 @@ func ParseStructField(field reflect.StructField) *StructFieldInfo { 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) @@ -267,7 +246,10 @@ func applyFieldInfoToSchema(schema *openapi3.Schema, info *StructFieldInfo) { schema.Format = info.Format } if len(info.Enum) > 0 { - schema.Enum = info.Enum + schema.Enum = make([]any, 0) + for _, item := range info.Enum { + schema.Enum = append(schema.Enum, item.Value) + } } } -- 2.36.6 From 5ff7dac2273c26db18b92f67913ea9173c807a4b 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 18:46:53 +0800 Subject: [PATCH 09/26] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=90=8D=E7=A7=B0=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/schema.go | 26 ++------------------------ util/struct_field.go | 36 +++++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/openapi/schema.go b/openapi/schema.go index 20b192b..6f1dc90 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -10,7 +10,6 @@ package openapi import ( "reflect" "strconv" - "strings" "time" "git.zhangdeman.cn/zhangdeman/api-doc/define" @@ -48,30 +47,9 @@ func ParseStructField(field reflect.StructField) *StructFieldInfo { Type: field.Type, } + util.ParseStructFieldTag.GetParamName(field) // 解析 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.JSONName, info.OmitEmpty = util.ParseStructFieldTag.GetParamName(field) // 解析参数描述 info.Description = util.ParseStructFieldTag.GetParamDesc(field) diff --git a/util/struct_field.go b/util/struct_field.go index 7b82fde..683c2d8 100644 --- a/util/struct_field.go +++ b/util/struct_field.go @@ -24,7 +24,7 @@ type parseStructFieldTag struct { } // GetParamName 获取参数名称 -func (psf parseStructFieldTag) GetParamName(structField reflect.StructField) string { +func (psf parseStructFieldTag) GetParamName(structField reflect.StructField) (string, bool) { paramNameTagList := []string{ define.TagJson, define.TagForm, define.TagXml, define.TagYaml, @@ -32,14 +32,30 @@ func (psf parseStructFieldTag) GetParamName(structField reflect.StructField) str } for _, tag := range paramNameTagList { tagVal := structField.Tag.Get(tag) - tagVal = strings.TrimSuffix(strings.TrimPrefix(tagVal, define.TagNameOmitempty+","), ","+define.TagNameOmitempty) - tagVal = strings.Trim(tagVal, ",") - if tagVal != "" && tagVal != define.TagNameOmitempty { - return tagVal + if tagVal == "" { + continue } + parts := strings.Split(tagVal, ",") + jsonName := "" + omit := false + if len(parts) > 0 { + if parts[0] == "-" { + return "", false // 跳过该字段 + } + if parts[0] != "" { + jsonName = parts[0] + } + } + for _, part := range parts[1:] { + switch part { + case define.TagNameOmitempty: + omit = true + } + } + return jsonName, omit } // 未设置相关字段, 则字段名即为参数名 - return structField.Name + return structField.Name, false } // GetParamDesc ... @@ -52,7 +68,8 @@ func (psf parseStructFieldTag) GetParamDesc(structField reflect.StructField) str } } // 没有显示的设置参数描述, 则使用参数名作为参数描述 - return psf.GetParamName(structField) + paramName, _ := psf.GetParamName(structField) + return paramName } // GetDefaultValue 获取默认值 @@ -122,10 +139,7 @@ func (psf parseStructFieldTag) Summary(structField reflect.StructField) string { return tagVal } } - paramName := psf.GetParamName(structField) - if paramName == "-" { - return "" - } + paramName, _ := psf.GetParamName(structField) return paramName } -- 2.36.6 From b3b090a02003b85172c1340b155e116d5bfc35c6 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 19:00:10 +0800 Subject: [PATCH 10/26] =?UTF-8?q?feat:=20validate=20v10=20=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E8=A7=84=E5=88=99=E7=BB=93=E6=9E=84=E5=8C=96=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- util/validate_v10_parse.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 util/validate_v10_parse.go diff --git a/util/validate_v10_parse.go b/util/validate_v10_parse.go new file mode 100644 index 0000000..11f79de --- /dev/null +++ b/util/validate_v10_parse.go @@ -0,0 +1,34 @@ +// Package util ... +// +// Description : util ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-05 18:48 +package util + +import ( + "reflect" + "strings" +) + +// ParseValidateRule 解析验证规则 +func ParseValidateRule(dataType reflect.Kind, ruleStr string) map[string]any { + ruleList := strings.Split(ruleStr, ",") + return nil +} + +type ValidateRule struct { + Omitempty bool `json:"omitempty" dc:"为空则不校验"` + Required bool `json:"required" dc:"必传校验"` + Lte *float64 `json:"lte" dc:"数字类型小于等于"` + Gte *float64 `json:"gte" dc:"数字类型大于等于"` + Lt *float64 `json:"lt" dc:"数字类型小于"` + Gt *float64 `json:"gt" dc:"数字类型大于"` + Len *uint `json:"len" dc:"长度等于"` + Max *uint `json:"max" dc:"长度小于等于"` + Min *uint `json:"min" dc:"长度大于等于"` + Eq any `json:"eq" dc:"等于"` + Ne any `json:"ne" dc:"不等于"` + Oneof []any `json:"oneof" dc:"枚举值列表"` +} -- 2.36.6 From e40b2959d75994295cc3eff132e333b0b724f248 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 22:50:12 +0800 Subject: [PATCH 11/26] =?UTF-8?q?feat:=20=E9=AA=8C=E8=AF=81=E8=A7=84?= =?UTF-8?q?=E5=88=99=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- util/validate_v10_parse.go | 81 +++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/util/validate_v10_parse.go b/util/validate_v10_parse.go index 11f79de..852acab 100644 --- a/util/validate_v10_parse.go +++ b/util/validate_v10_parse.go @@ -10,12 +10,89 @@ package util import ( "reflect" "strings" + + "git.zhangdeman.cn/zhangdeman/consts" + utilPkg "git.zhangdeman.cn/zhangdeman/util" ) // ParseValidateRule 解析验证规则 -func ParseValidateRule(dataType reflect.Kind, ruleStr string) map[string]any { +func ParseValidateRule(dataType reflect.Type, ruleStr string) ValidateRule { + if dataType.Kind() == reflect.Ptr { + dataType = dataType.Elem() + } + dataKind := dataType.Kind() + rule := ValidateRule{ + Omitempty: false, + Required: false, + Lte: nil, + Gte: nil, + Lt: nil, + Gt: nil, + Len: nil, + Max: nil, + Min: nil, + Eq: nil, + Ne: nil, + Oneof: nil, + } ruleList := strings.Split(ruleStr, ",") - return nil + for _, itemRule := range ruleList { + itemRule = strings.ToLower(strings.TrimSpace(itemRule)) + if len(itemRule) == 0 { + continue + } + if strings.Contains(itemRule, "=") { + // 一定是无需要值的验证规则 + switch itemRule { + case consts.ValidatorRuleCommonRequired.String(): // 必传 + rule.Required = true + case consts.ValidatorRuleCommonOmitempty.String(): // 为空则不校验 + rule.Omitempty = true + } + continue + } + itemRuleArr := strings.Split(itemRule, "=") + ruleType := itemRuleArr[0] + ruleValue := strings.Join(itemRuleArr[1:], "=") + if len(ruleValue) == 0 { + // 未配置值的校验规则 + continue + } + switch ruleType { + case consts.ValidateRuleLte.String(), consts.ValidateRuleGte.String(), consts.ValidateRuleGt.String(), consts.ValidateRuleLt.String(): // 数字取值范围 + var val float64 + if err := utilPkg.ConvertAssign(&val, ruleValue); nil == err { + switch ruleType { + case consts.ValidateRuleLte.String(): + rule.Lte = &val + case consts.ValidateRuleGte.String(): + rule.Gte = &val + case consts.ValidateRuleGt.String(): + rule.Gt = &val + case consts.ValidateRuleLt.String(): + rule.Lt = &val + } + } + case consts.ValidatorRuleCommonLen.String(), consts.ValidatorRuleCommonMin.String(), consts.ValidatorRuleCommonMax.String(): // 长度取值范围 + var val uint + if err := utilPkg.ConvertAssign(&val, ruleValue); nil == err { + switch ruleType { + case consts.ValidatorRuleCommonLen.String(): + rule.Len = &val + case consts.ValidatorRuleCommonMin.String(): + rule.Min = &val + case consts.ValidatorRuleCommonMax.String(): + rule.Max = &val + } + } + case consts.ValidateRuleEq.String(): + rule.Eq = ruleValue + case consts.ValidateRuleNe.String(): + rule.Ne = ruleValue + case consts.ValidatorRuleCommonOneOf.String(): + } + } + return rule } type ValidateRule struct { -- 2.36.6 From 2cad79cf3da46c4a54ea56cd8b8cf1261c020007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 09:59:40 +0800 Subject: [PATCH 12/26] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90validate=2010?= =?UTF-8?q?=20=E9=AA=8C=E8=AF=81=E8=A7=84=E5=88=99=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 2 ++ openapi/schema.go | 53 ++++++++++---------------------------- util/validate_v10_parse.go | 47 +++++++++++++++++++++------------ 4 files changed, 48 insertions(+), 56 deletions(-) diff --git a/go.mod b/go.mod index 240354d..eb91469 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 toolchain go1.24.1 require ( - git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260105014807-92a7dc3bb97f + git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260106015608-42e268aea545 git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20251013044511-86c1a4a3a9dd git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20251225094759-09c64ba2541c github.com/gin-gonic/gin v1.11.0 diff --git a/go.sum b/go.sum index 2184c20..12c2f9f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260105014807-92a7dc3bb97f h1:2p5yxwk472XxY93UTky2Xl2doTU1uqR7vcKara/Dt1E= git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260105014807-92a7dc3bb97f/go.mod h1:5p8CEKGBxi7qPtTXDI3HDmqKAfIm5i/aBWdrbkbdNjc= +git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260106015608-42e268aea545 h1:A6UeeMcSqAlHUmA2coWIuiCS/W9ySylhZMa/0HbFDcQ= +git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260106015608-42e268aea545/go.mod h1:5p8CEKGBxi7qPtTXDI3HDmqKAfIm5i/aBWdrbkbdNjc= git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20251013024601-da007da2fb42 h1:VjYrb4adud7FHeiYS9XA0B/tOaJjfRejzQAlwimrrDc= git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20251013024601-da007da2fb42/go.mod h1:VHb9qmhaPDAQDcS6vUiDCamYjZ4R5lD1XtVsh55KsMI= git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20251013044511-86c1a4a3a9dd h1:kTZOpR8iHx27sUufMWVYhDZx9Q4h80j7RWlaR8GIBiU= diff --git a/openapi/schema.go b/openapi/schema.go index 6f1dc90..783d326 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -28,11 +28,12 @@ type StructFieldInfo struct { 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:"最大长度"` + MinLength *float64 `json:"min_length" dc:"最小长度"` + MaxLength *float64 `json:"max_length" dc:"最大长度"` Pattern string `json:"pattern" dc:"模式"` Format string `json:"format" dc:"格式"` - Enum []define.EnumValue `json:"enum" dc:"枚举值列表"` + Enum []any `json:"enum" dc:"枚举值列表"` + EnumDesc []define.EnumValue `json:"enum_desc" dc:"枚举值详细描述"` OmitEmpty bool `json:"omit_empty" dc:"是否可控"` } @@ -56,44 +57,18 @@ func ParseStructField(field reflect.StructField) *StructFieldInfo { // 解析示例值 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 - } + validateRule := util.ParseValidateRule(field.Type, util.ParseStructFieldTag.GetValidateRule(field)) + info.Required = validateRule.Required + info.Min = validateRule.Min + info.Max = validateRule.Max + info.MinLength = validateRule.Min + info.MaxLength = validateRule.Max + info.Pattern = validateRule.Regexp + info.Format = field.Type.String() // 解析枚举值 - info.Enum = util.ParseStructFieldTag.EnumDescription(field) + info.Enum = validateRule.Oneof + info.EnumDesc = util.ParseStructFieldTag.EnumDescription(field) return info } diff --git a/util/validate_v10_parse.go b/util/validate_v10_parse.go index 852acab..65ca88e 100644 --- a/util/validate_v10_parse.go +++ b/util/validate_v10_parse.go @@ -13,6 +13,7 @@ import ( "git.zhangdeman.cn/zhangdeman/consts" utilPkg "git.zhangdeman.cn/zhangdeman/util" + "git.zhangdeman.cn/zhangdeman/wrapper/op_string" ) // ParseValidateRule 解析验证规则 @@ -59,24 +60,19 @@ func ParseValidateRule(dataType reflect.Type, ruleStr string) ValidateRule { continue } switch ruleType { - case consts.ValidateRuleLte.String(), consts.ValidateRuleGte.String(), consts.ValidateRuleGt.String(), consts.ValidateRuleLt.String(): // 数字取值范围 + case consts.ValidatorRuleLte.String(), consts.ValidatorRuleGte.String(), consts.ValidatorRuleGt.String(), consts.ValidatorRuleLt.String(), // 数字取值范围 + consts.ValidatorRuleCommonLen.String(), consts.ValidatorRuleCommonMin.String(), consts.ValidatorRuleCommonMax.String(): // 长度取值范围 var val float64 if err := utilPkg.ConvertAssign(&val, ruleValue); nil == err { switch ruleType { - case consts.ValidateRuleLte.String(): + case consts.ValidatorRuleLte.String(): rule.Lte = &val - case consts.ValidateRuleGte.String(): + case consts.ValidatorRuleGte.String(): rule.Gte = &val - case consts.ValidateRuleGt.String(): + case consts.ValidatorRuleGt.String(): rule.Gt = &val - case consts.ValidateRuleLt.String(): + case consts.ValidatorRuleLt.String(): rule.Lt = &val - } - } - case consts.ValidatorRuleCommonLen.String(), consts.ValidatorRuleCommonMin.String(), consts.ValidatorRuleCommonMax.String(): // 长度取值范围 - var val uint - if err := utilPkg.ConvertAssign(&val, ruleValue); nil == err { - switch ruleType { case consts.ValidatorRuleCommonLen.String(): rule.Len = &val case consts.ValidatorRuleCommonMin.String(): @@ -85,11 +81,29 @@ func ParseValidateRule(dataType reflect.Type, ruleStr string) ValidateRule { rule.Max = &val } } - case consts.ValidateRuleEq.String(): + case consts.ValidatorRuleEq.String(): rule.Eq = ruleValue - case consts.ValidateRuleNe.String(): + case consts.ValidatorRuleNe.String(): rule.Ne = ruleValue + case consts.ValidatorRuleRegexp.String(): + rule.Regexp = ruleValue case consts.ValidatorRuleCommonOneOf.String(): + rule.Oneof = make([]any, 0) + switch dataKind { + case reflect.String: + valList := strings.Split(ruleValue, " ") + for _, item := range valList { + rule.Oneof = append(rule.Oneof, item) + } + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint, + reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, + reflect.Float32, reflect.Float64: + valList := op_string.ToBaseTypeSlice[float64](ruleValue, " ").Value + for _, item := range valList { + rule.Oneof = append(rule.Oneof, item) + } + default: + } } } return rule @@ -102,10 +116,11 @@ type ValidateRule struct { Gte *float64 `json:"gte" dc:"数字类型大于等于"` Lt *float64 `json:"lt" dc:"数字类型小于"` Gt *float64 `json:"gt" dc:"数字类型大于"` - Len *uint `json:"len" dc:"长度等于"` - Max *uint `json:"max" dc:"长度小于等于"` - Min *uint `json:"min" dc:"长度大于等于"` + Len *float64 `json:"len" dc:"长度等于"` + Max *float64 `json:"max" dc:"长度小于等于"` + Min *float64 `json:"min" dc:"长度大于等于"` Eq any `json:"eq" dc:"等于"` Ne any `json:"ne" dc:"不等于"` Oneof []any `json:"oneof" dc:"枚举值列表"` + Regexp string `json:"regexp" dc:"正则表达式"` } -- 2.36.6 From 9f2059837c0bd9e115bad4fbf9395d5dec4d9e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 10:05:29 +0800 Subject: [PATCH 13/26] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96ParseStructFiel?= =?UTF-8?q?d=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/schema.go | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/openapi/schema.go b/openapi/schema.go index 783d326..ca338f5 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -42,34 +42,29 @@ func ParseStructField(field reflect.StructField) *StructFieldInfo { if !field.IsExported() { return nil } - - info := &StructFieldInfo{ - Name: field.Name, - Type: field.Type, - } - - util.ParseStructFieldTag.GetParamName(field) - // 解析 JSON tag - info.JSONName, info.OmitEmpty = util.ParseStructFieldTag.GetParamName(field) - - // 解析参数描述 - info.Description = util.ParseStructFieldTag.GetParamDesc(field) - // 解析示例值 - info.Example = util.ParseStructFieldTag.GetExampleValue(field) // 解析验证规则 validateRule := util.ParseValidateRule(field.Type, util.ParseStructFieldTag.GetValidateRule(field)) - info.Required = validateRule.Required - info.Min = validateRule.Min - info.Max = validateRule.Max - info.MinLength = validateRule.Min - info.MaxLength = validateRule.Max - info.Pattern = validateRule.Regexp - info.Format = field.Type.String() - - // 解析枚举值 - info.Enum = validateRule.Oneof - info.EnumDesc = util.ParseStructFieldTag.EnumDescription(field) + info := &StructFieldInfo{ + Name: field.Name, + JSONName: "", + Type: field.Type, + Description: util.ParseStructFieldTag.GetParamDesc(field), // 解析参数描述 + Example: util.ParseStructFieldTag.GetExampleValue(field), // 解析示例值 + Default: util.ParseStructFieldTag.GetDefaultValue(field), // 解析默认值 + Required: validateRule.Required, + Min: validateRule.Min, + Max: validateRule.Max, + MinLength: validateRule.Min, + MaxLength: validateRule.Max, + Pattern: validateRule.Regexp, + Format: field.Type.String(), + Enum: validateRule.Oneof, // 解析枚举值 + EnumDesc: util.ParseStructFieldTag.EnumDescription(field), // 解析枚举值描述 + OmitEmpty: false, + } + // 解析 JSON tag + info.JSONName, info.OmitEmpty = util.ParseStructFieldTag.GetParamName(field) return info } -- 2.36.6 From b03fc5acf9672bd89d995a7eecbcb5288c9d6e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 10:55:27 +0800 Subject: [PATCH 14/26] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E4=BD=93=E5=AD=97=E6=AE=B5=E7=B1=BB=E5=9E=8B=E8=A7=A3?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/schema.go | 14 +++++----- openapi/schema_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 openapi/schema_test.go diff --git a/openapi/schema.go b/openapi/schema.go index ca338f5..0fc3a24 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -9,7 +9,6 @@ package openapi import ( "reflect" - "strconv" "time" "git.zhangdeman.cn/zhangdeman/api-doc/define" @@ -69,7 +68,7 @@ func ParseStructField(field reflect.StructField) *StructFieldInfo { } // GenerateOpenAPISchema 生成完整的 OpenAPI Schema -func GenerateOpenAPISchema(s interface{}) *openapi3.SchemaRef { +func GenerateOpenAPISchema(s any) *openapi3.SchemaRef { t := reflect.TypeOf(s) if t.Kind() == reflect.Ptr { t = t.Elem() @@ -78,6 +77,7 @@ func GenerateOpenAPISchema(s interface{}) *openapi3.SchemaRef { return generateSchemaRecursive(t, make(map[string]bool)) } +// 生成 schema func generateSchemaRecursive(t reflect.Type, seen map[string]bool) *openapi3.SchemaRef { // 检查循环引用 typeName := t.PkgPath() + "." + t.Name() @@ -182,10 +182,10 @@ func applyFieldInfoToSchema(schema *openapi3.Schema, info *StructFieldInfo) { schema.Max = info.Max } if info.MinLength != nil { - schema.MinLength = *info.MinLength + schema.MinLength = uint64(*info.MinLength) } if info.MaxLength != nil { - schema.MaxLength = openapi3.Uint64Ptr(*info.MaxLength) + schema.MaxLength = openapi3.Ptr(uint64(*info.MaxLength)) } if info.Pattern != "" { schema.Pattern = info.Pattern @@ -194,10 +194,10 @@ func applyFieldInfoToSchema(schema *openapi3.Schema, info *StructFieldInfo) { schema.Format = info.Format } if len(info.Enum) > 0 { - schema.Enum = make([]any, 0) - for _, item := range info.Enum { + schema.Enum = info.Enum + /*for _, item := range info.Enum { schema.Enum = append(schema.Enum, item.Value) - } + }*/ } } diff --git a/openapi/schema_test.go b/openapi/schema_test.go new file mode 100644 index 0000000..1a9aa61 --- /dev/null +++ b/openapi/schema_test.go @@ -0,0 +1,58 @@ +// Package openapi ... +// +// Description : openapi ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-06 10:50 +package openapi + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/getkin/kin-openapi/openapi3" +) + +func TestParseStructField(t *testing.T) { + type Category struct { + ID int64 `json:"id" description:"分类ID"` + Name string `json:"name" description:"分类名称"` + } + type Product struct { + ID int64 `json:"id" description:"产品ID" example:"1001" required:"true"` + Name string `json:"name" description:"产品名称" example:"iPhone 13" minLength:"2" maxLength:"100" required:"true"` + Price float64 `json:"price" description:"价格" example:"6999.99" min:"0"` + Stock int `json:"stock" description:"库存" example:"100" min:"0"` + Tags []string `json:"tags" description:"标签"` + Attributes map[string]string `json:"attributes" description:"属性"` + CreatedAt time.Time `json:"created_at" description:"创建时间"` + UpdatedAt *time.Time `json:"updated_at,omitempty" description:"更新时间"` + Category *Category `json:"category,omitempty" description:"分类"` + } + + // 生成 Product 的 Schema + productSchema := GenerateOpenAPISchema(Product{}) + + // 创建完整的 OpenAPI 文档 + doc := &openapi3.T{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + Title: "产品服务 API", + Description: "产品管理相关接口", + Version: "1.0.0", + }, + Components: &openapi3.Components{ + Schemas: map[string]*openapi3.SchemaRef{ + "Product": productSchema, + "Category": GenerateOpenAPISchema(Category{}), + }, + }, + } + + // 输出 JSON + data, _ := json.MarshalIndent(doc, "", " ") + fmt.Println(string(data)) +} -- 2.36.6 From 2f4e6851f333ffa1b978b35d8b2bec38026f4d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 11:11:07 +0800 Subject: [PATCH 15/26] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90openapi?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/generate.go | 94 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/openapi/generate.go b/openapi/generate.go index 2bf5a7d..ce5edb9 100644 --- a/openapi/generate.go +++ b/openapi/generate.go @@ -7,12 +7,102 @@ // Date : 2026-01-05 17:20 package openapi +import ( + "git.zhangdeman.cn/zhangdeman/consts" + "github.com/getkin/kin-openapi/openapi3" +) + +// NewGenerate 生成文档实例 func NewGenerate() *Generate { - return &Generate{} + return &Generate{ + doc: &openapi3.T{ + Extensions: map[string]any{}, + OpenAPI: "openapi", + Components: &openapi3.Components{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + Schemas: map[string]*openapi3.SchemaRef{}, + Parameters: map[string]*openapi3.ParameterRef{}, + Headers: map[string]*openapi3.HeaderRef{}, + RequestBodies: map[string]*openapi3.RequestBodyRef{}, + Responses: map[string]*openapi3.ResponseRef{}, + SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, + Examples: map[string]*openapi3.ExampleRef{}, + Links: map[string]*openapi3.LinkRef{}, + Callbacks: map[string]*openapi3.CallbackRef{}, + }, + Info: &openapi3.Info{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + Title: "服务 API接口 文档", + Description: "服务 API接口 文档", + TermsOfService: "", + Contact: &openapi3.Contact{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + Name: "developer", + URL: "", + Email: "developer@test.com", + }, + License: &openapi3.License{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + Name: consts.LicenseApache20, + URL: consts.LicenseUrlTable[consts.LicenseApache20], + }, + Version: "0.0.1", + }, + Paths: &openapi3.Paths{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + }, + Security: []openapi3.SecurityRequirement{}, + Servers: []*openapi3.Server{}, + Tags: []*openapi3.Tag{}, + ExternalDocs: &openapi3.ExternalDocs{ + Extensions: map[string]any{}, + Origin: nil, + Description: "", + URL: "", + }, + }, + } } // Generate 生成 OpenApi 标准规范的文档 -type Generate struct{} +type Generate struct { + doc *openapi3.T +} // AddApiDoc 添加接口文档 func (g *Generate) AddApiDoc(request any, response any) error { -- 2.36.6 From c1d5dd89d40aab709672dbea14e68477c0b02a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 11:57:02 +0800 Subject: [PATCH 16/26] =?UTF-8?q?feat:=20=E5=AE=8C=E8=83=9C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0schema=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/generate.go | 35 +++++++++++++++++++++++++++++++++-- openapi/generate_test.go | 38 ++++++++++++++++++++++++++++++++++++++ openapi/schema.go | 15 +++++++++++---- 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 openapi/generate_test.go diff --git a/openapi/generate.go b/openapi/generate.go index ce5edb9..1ac9de1 100644 --- a/openapi/generate.go +++ b/openapi/generate.go @@ -8,6 +8,8 @@ package openapi import ( + "reflect" + "git.zhangdeman.cn/zhangdeman/consts" "github.com/getkin/kin-openapi/openapi3" ) @@ -17,7 +19,7 @@ func NewGenerate() *Generate { return &Generate{ doc: &openapi3.T{ Extensions: map[string]any{}, - OpenAPI: "openapi", + OpenAPI: "3.1.0", Components: &openapi3.Components{ Extensions: map[string]any{}, Origin: &openapi3.Origin{ @@ -106,5 +108,34 @@ type Generate struct { // AddApiDoc 添加接口文档 func (g *Generate) AddApiDoc(request any, response any) error { - return nil + var ( + err error + requestType reflect.Type + responseType reflect.Type + ok bool + ) + + // 初始化请求数据与响应数据类型 + if requestType, ok = request.(reflect.Type); !ok { + requestType = reflect.TypeOf(request) + if requestType.Kind() == reflect.Ptr { + requestType = requestType.Elem() + } + } + + if responseType, ok = response.(reflect.Type); !ok { + responseType = reflect.TypeOf(response) + if responseType.Kind() == reflect.Ptr { + responseType = responseType.Elem() + } + } + requestTypeStr := requestType.String() + if _, exist := g.doc.Components.Schemas[requestTypeStr]; !exist { + g.doc.Components.Schemas[requestTypeStr] = GenerateOpenAPISchema(requestType) + } + responseTypeStr := requestType.String() + if _, exist := g.doc.Components.Schemas[responseTypeStr]; !exist { + g.doc.Components.Schemas[responseTypeStr] = GenerateOpenAPISchema(responseType) + } + return err } diff --git a/openapi/generate_test.go b/openapi/generate_test.go new file mode 100644 index 0000000..413fc7e --- /dev/null +++ b/openapi/generate_test.go @@ -0,0 +1,38 @@ +// Package openapi ... +// +// Description : openapi ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-06 11:48 +package openapi + +import ( + "encoding/json" + "fmt" + "testing" + "time" +) + +func TestGenerate_AddApiDoc(t *testing.T) { + type Category struct { + ID int64 `json:"id" description:"分类ID"` + Name string `json:"name" description:"分类名称"` + } + type Product struct { + ID int64 `json:"id" description:"产品ID" example:"1001" required:"true"` + Name string `json:"name" description:"产品名称" example:"iPhone 13" minLength:"2" maxLength:"100" required:"true"` + Price float64 `json:"price" description:"价格" example:"6999.99" min:"0"` + Stock int `json:"stock" description:"库存" example:"100" min:"0"` + Tags []string `json:"tags" description:"标签"` + Attributes map[string]string `json:"attributes" description:"属性"` + CreatedAt time.Time `json:"created_at" description:"创建时间"` + UpdatedAt *time.Time `json:"updated_at,omitempty" description:"更新时间"` + Category *Category `json:"category,omitempty" description:"分类"` + } + instance := NewGenerate() + instance.AddApiDoc(Category{}, Product{}) + // 输出 JSON + data, _ := json.MarshalIndent(instance.doc, "", " ") + fmt.Println(string(data)) +} diff --git a/openapi/schema.go b/openapi/schema.go index 0fc3a24..c7ef10d 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -69,12 +69,19 @@ func ParseStructField(field reflect.StructField) *StructFieldInfo { // GenerateOpenAPISchema 生成完整的 OpenAPI Schema func GenerateOpenAPISchema(s any) *openapi3.SchemaRef { - t := reflect.TypeOf(s) - if t.Kind() == reflect.Ptr { - t = t.Elem() + var ( + ok bool + tType reflect.Type + ) + + if tType, ok = s.(reflect.Type); !ok { + tType = reflect.TypeOf(s) + if tType.Kind() == reflect.Ptr { + tType = tType.Elem() + } } - return generateSchemaRecursive(t, make(map[string]bool)) + return generateSchemaRecursive(tType, make(map[string]bool)) } // 生成 schema -- 2.36.6 From 0dd0e8f22f979b69c9b22d5f639fd3951f1cd8ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 18:05:07 +0800 Subject: [PATCH 17/26] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E6=96=87=E6=A1=A3=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/generate.go | 136 +++++++++++++++++++++++++++++++++++++-- openapi/generate_test.go | 12 +++- 2 files changed, 139 insertions(+), 9 deletions(-) diff --git a/openapi/generate.go b/openapi/generate.go index 1ac9de1..920349e 100644 --- a/openapi/generate.go +++ b/openapi/generate.go @@ -8,9 +8,14 @@ package openapi import ( + "fmt" + "net/http" "reflect" + "strings" + "git.zhangdeman.cn/zhangdeman/api-doc/define" "git.zhangdeman.cn/zhangdeman/consts" + "git.zhangdeman.cn/zhangdeman/wrapper/op_array" "github.com/getkin/kin-openapi/openapi3" ) @@ -91,12 +96,12 @@ func NewGenerate() *Generate { Security: []openapi3.SecurityRequirement{}, Servers: []*openapi3.Server{}, Tags: []*openapi3.Tag{}, - ExternalDocs: &openapi3.ExternalDocs{ + /*ExternalDocs: &openapi3.ExternalDocs{ Extensions: map[string]any{}, Origin: nil, Description: "", URL: "", - }, + },*/ }, } } @@ -107,7 +112,7 @@ type Generate struct { } // AddApiDoc 添加接口文档 -func (g *Generate) AddApiDoc(request any, response any) error { +func (g *Generate) AddApiDoc(apiMeta define.UriConfig, request any, response any) error { var ( err error requestType reflect.Type @@ -129,13 +134,130 @@ func (g *Generate) AddApiDoc(request any, response any) error { responseType = responseType.Elem() } } - requestTypeStr := requestType.String() - if _, exist := g.doc.Components.Schemas[requestTypeStr]; !exist { - g.doc.Components.Schemas[requestTypeStr] = GenerateOpenAPISchema(requestType) + + schemaData := GenerateOpenAPISchema(requestType) + apiOperate, isRead := g.initApiConfig(apiMeta) + requestTypeStr := strings.ReplaceAll(requestType.String(), ".", "_") + if isRead { + for paramName, paramConfig := range schemaData.Value.Properties { + apiOperate.Parameters = append(apiOperate.Parameters, &openapi3.ParameterRef{ + Extensions: nil, + Origin: nil, + Ref: "", + Value: &openapi3.Parameter{ + Extensions: nil, + Origin: nil, + Name: paramName, + In: strings.ToLower(consts.RequestDataLocationQuery.String()), + Description: paramConfig.Value.Description, + Style: "", + Explode: nil, + AllowEmptyValue: false, + AllowReserved: false, + Deprecated: false, + Required: op_array.ArrayType(paramConfig.Value.Required).Has(paramName) >= 0, + Schema: paramConfig, + Example: nil, + Examples: nil, + Content: nil, + }, + }) + } + } else { + apiOperate.RequestBody = &openapi3.RequestBodyRef{ + Extensions: nil, + Origin: nil, + Ref: requestTypeStr, + Value: nil, + } } - responseTypeStr := requestType.String() + + // 初始化接口配置 + if _, exist := g.doc.Components.Schemas[requestTypeStr]; !exist { + g.doc.Components.Schemas[requestTypeStr] = schemaData + } + responseTypeStr := strings.ReplaceAll(requestType.String(), ".", "_") if _, exist := g.doc.Components.Schemas[responseTypeStr]; !exist { g.doc.Components.Schemas[responseTypeStr] = GenerateOpenAPISchema(responseType) } + desc := "请求成功" + apiOperate.Responses.Set(fmt.Sprintf("%v", http.StatusOK), &openapi3.ResponseRef{ + Extensions: nil, + Origin: nil, + Ref: "", + Value: &openapi3.Response{ + Extensions: nil, + Origin: nil, + Description: &desc, + Headers: nil, + Content: map[string]*openapi3.MediaType{ + consts.MimeTypeJson: { + Extensions: nil, + Origin: nil, + Schema: g.doc.Components.Schemas[responseTypeStr], + Example: nil, + Examples: nil, + Encoding: nil, + }, + }, + Links: nil, + }, + }) return err } + +// initApiConfig 初始化接口配置, 并返回 operation 以及 是否读请求 +func (g *Generate) initApiConfig(apiMeta define.UriConfig) (*openapi3.Operation, bool) { + if nil == g.doc.Paths.Value(apiMeta.Path) { + g.doc.Paths.Set(apiMeta.Path, &openapi3.PathItem{ + Extensions: nil, + Origin: nil, + Ref: "", + Summary: "", + Description: "", + Connect: nil, + Delete: nil, + Get: nil, + Head: nil, + Options: nil, + Patch: nil, + Post: nil, + Put: nil, + Trace: nil, + Servers: nil, + Parameters: nil, + }) + } + newOperate := openapi3.NewOperation() + newOperate.Parameters = make(openapi3.Parameters, 0) + newOperate.Responses = openapi3.NewResponses() + isRead := false + switch apiMeta.RequestMethod { + case http.MethodGet: + g.doc.Paths.Value(apiMeta.Path).Get = newOperate + isRead = true + case http.MethodHead: + g.doc.Paths.Value(apiMeta.Path).Head = newOperate + isRead = true + case http.MethodConnect: + g.doc.Paths.Value(apiMeta.Path).Connect = newOperate + isRead = true + case http.MethodOptions: + g.doc.Paths.Value(apiMeta.Path).Options = newOperate + isRead = true + case http.MethodTrace: + g.doc.Paths.Value(apiMeta.Path).Trace = newOperate + isRead = true + // 读请求 + case http.MethodPost: + g.doc.Paths.Value(apiMeta.Path).Post = newOperate + case http.MethodPut: + g.doc.Paths.Value(apiMeta.Path).Put = newOperate + case http.MethodPatch: + g.doc.Paths.Value(apiMeta.Path).Patch = newOperate + case http.MethodDelete: + g.doc.Paths.Value(apiMeta.Path).Delete = newOperate + // 写请求 + } + return newOperate, isRead +} diff --git a/openapi/generate_test.go b/openapi/generate_test.go index 413fc7e..1f4ab87 100644 --- a/openapi/generate_test.go +++ b/openapi/generate_test.go @@ -10,8 +10,11 @@ package openapi import ( "encoding/json" "fmt" + "net/http" "testing" "time" + + "git.zhangdeman.cn/zhangdeman/api-doc/define" ) func TestGenerate_AddApiDoc(t *testing.T) { @@ -20,7 +23,7 @@ func TestGenerate_AddApiDoc(t *testing.T) { Name string `json:"name" description:"分类名称"` } type Product struct { - ID int64 `json:"id" description:"产品ID" example:"1001" required:"true"` + ID int64 `json:"id,omitempty" description:"产品ID" example:"1001" required:"true" binding:"required"` Name string `json:"name" description:"产品名称" example:"iPhone 13" minLength:"2" maxLength:"100" required:"true"` Price float64 `json:"price" description:"价格" example:"6999.99" min:"0"` Stock int `json:"stock" description:"库存" example:"100" min:"0"` @@ -31,7 +34,12 @@ func TestGenerate_AddApiDoc(t *testing.T) { Category *Category `json:"category,omitempty" description:"分类"` } instance := NewGenerate() - instance.AddApiDoc(Category{}, Product{}) + instance.AddApiDoc(define.UriConfig{ + Path: "/a/b/c", + RequestMethod: http.MethodGet, + TagList: []string{"test"}, + Desc: "测试接口", + }, Category{}, Product{}) // 输出 JSON data, _ := json.MarshalIndent(instance.doc, "", " ") fmt.Println(string(data)) -- 2.36.6 From 8ed6735d48468ec6cecd658e89e0c3dec0a7ae40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 18:52:57 +0800 Subject: [PATCH 18/26] =?UTF-8?q?feat:=20=E8=B0=83=E8=AF=95=E5=BF=85?= =?UTF-8?q?=E4=BC=A0=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/generate_test.go | 4 ++-- openapi/schema.go | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openapi/generate_test.go b/openapi/generate_test.go index 1f4ab87..2b93cd5 100644 --- a/openapi/generate_test.go +++ b/openapi/generate_test.go @@ -19,8 +19,8 @@ import ( func TestGenerate_AddApiDoc(t *testing.T) { type Category struct { - ID int64 `json:"id" description:"分类ID"` - Name string `json:"name" description:"分类名称"` + ID int64 `json:"id" description:"分类ID" binding:"required,min=10,max=100"` + Name string `json:"name" description:"分类名称" binding:"required,min=8"` } type Product struct { ID int64 `json:"id,omitempty" description:"产品ID" example:"1001" required:"true" binding:"required"` diff --git a/openapi/schema.go b/openapi/schema.go index c7ef10d..6d71d25 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -81,7 +81,8 @@ func GenerateOpenAPISchema(s any) *openapi3.SchemaRef { } } - return generateSchemaRecursive(tType, make(map[string]bool)) + schema := generateSchemaRecursive(tType, make(map[string]bool)) + return schema } // 生成 schema -- 2.36.6 From 708e327ddf0ec6eab54200befcfff635223a8ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 19:01:21 +0800 Subject: [PATCH 19/26] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E5=BF=85?= =?UTF-8?q?=E4=BC=A0=E6=95=B0=E6=8D=AE=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- util/validate_v10_parse.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/validate_v10_parse.go b/util/validate_v10_parse.go index 65ca88e..56d179c 100644 --- a/util/validate_v10_parse.go +++ b/util/validate_v10_parse.go @@ -42,7 +42,7 @@ func ParseValidateRule(dataType reflect.Type, ruleStr string) ValidateRule { if len(itemRule) == 0 { continue } - if strings.Contains(itemRule, "=") { + if !strings.Contains(itemRule, "=") { // 一定是无需要值的验证规则 switch itemRule { case consts.ValidatorRuleCommonRequired.String(): // 必传 -- 2.36.6 From 088b813045d0adb95a3dafb935205c26710bb471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 19:17:22 +0800 Subject: [PATCH 20/26] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E6=95=B0?= =?UTF-8?q?=E5=AD=97=E5=8F=96=E5=80=BC=E8=8C=83=E5=9B=B4=E4=B8=8E=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E9=95=BF=E5=BA=A6=E9=99=90=E5=88=B6=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/generate.go | 6 +++--- openapi/schema.go | 17 +++++++++++++---- util/validate_v10_parse.go | 4 ++++ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/openapi/generate.go b/openapi/generate.go index 920349e..bd5503a 100644 --- a/openapi/generate.go +++ b/openapi/generate.go @@ -137,7 +137,7 @@ func (g *Generate) AddApiDoc(apiMeta define.UriConfig, request any, response any schemaData := GenerateOpenAPISchema(requestType) apiOperate, isRead := g.initApiConfig(apiMeta) - requestTypeStr := strings.ReplaceAll(requestType.String(), ".", "_") + requestTypeStr := requestType.String() if isRead { for paramName, paramConfig := range schemaData.Value.Properties { apiOperate.Parameters = append(apiOperate.Parameters, &openapi3.ParameterRef{ @@ -152,7 +152,7 @@ func (g *Generate) AddApiDoc(apiMeta define.UriConfig, request any, response any Description: paramConfig.Value.Description, Style: "", Explode: nil, - AllowEmptyValue: false, + AllowEmptyValue: paramConfig.Value.AllowEmptyValue, AllowReserved: false, Deprecated: false, Required: op_array.ArrayType(paramConfig.Value.Required).Has(paramName) >= 0, @@ -176,7 +176,7 @@ func (g *Generate) AddApiDoc(apiMeta define.UriConfig, request any, response any if _, exist := g.doc.Components.Schemas[requestTypeStr]; !exist { g.doc.Components.Schemas[requestTypeStr] = schemaData } - responseTypeStr := strings.ReplaceAll(requestType.String(), ".", "_") + responseTypeStr := requestType.String() if _, exist := g.doc.Components.Schemas[responseTypeStr]; !exist { g.doc.Components.Schemas[responseTypeStr] = GenerateOpenAPISchema(responseType) } diff --git a/openapi/schema.go b/openapi/schema.go index 6d71d25..19f15c7 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -18,6 +18,7 @@ import ( // StructFieldInfo 结构体字段信息 type StructFieldInfo struct { + IsString bool `json:"is_string" dc:"是否字符串"` Name string `json:"name" dc:"结构体字段名"` JSONName string `json:"json_name" dc:"json tag"` Type reflect.Type `json:"type" dc:"字段类型"` @@ -44,6 +45,7 @@ func ParseStructField(field reflect.StructField) *StructFieldInfo { // 解析验证规则 validateRule := util.ParseValidateRule(field.Type, util.ParseStructFieldTag.GetValidateRule(field)) info := &StructFieldInfo{ + IsString: validateRule.IsString, Name: field.Name, JSONName: "", Type: field.Type, @@ -51,16 +53,23 @@ func ParseStructField(field reflect.StructField) *StructFieldInfo { Example: util.ParseStructFieldTag.GetExampleValue(field), // 解析示例值 Default: util.ParseStructFieldTag.GetDefaultValue(field), // 解析默认值 Required: validateRule.Required, - Min: validateRule.Min, - Max: validateRule.Max, - MinLength: validateRule.Min, - MaxLength: validateRule.Max, + Min: nil, + Max: nil, + MinLength: nil, + MaxLength: nil, Pattern: validateRule.Regexp, Format: field.Type.String(), Enum: validateRule.Oneof, // 解析枚举值 EnumDesc: util.ParseStructFieldTag.EnumDescription(field), // 解析枚举值描述 OmitEmpty: false, } + if info.IsString { + info.MinLength = validateRule.Min + info.MaxLength = validateRule.Max + } else { + info.Min = validateRule.Min + info.Max = validateRule.Max + } // 解析 JSON tag info.JSONName, info.OmitEmpty = util.ParseStructFieldTag.GetParamName(field) diff --git a/util/validate_v10_parse.go b/util/validate_v10_parse.go index 56d179c..24312ac 100644 --- a/util/validate_v10_parse.go +++ b/util/validate_v10_parse.go @@ -23,6 +23,7 @@ func ParseValidateRule(dataType reflect.Type, ruleStr string) ValidateRule { } dataKind := dataType.Kind() rule := ValidateRule{ + IsString: dataKind == reflect.String, Omitempty: false, Required: false, Lte: nil, @@ -75,8 +76,10 @@ func ParseValidateRule(dataType reflect.Type, ruleStr string) ValidateRule { rule.Lt = &val case consts.ValidatorRuleCommonLen.String(): rule.Len = &val + case consts.ValidatorRuleCommonMin.String(): rule.Min = &val + case consts.ValidatorRuleCommonMax.String(): rule.Max = &val } @@ -110,6 +113,7 @@ func ParseValidateRule(dataType reflect.Type, ruleStr string) ValidateRule { } type ValidateRule struct { + IsString bool `json:"is_string" dc:"是否字符串"` Omitempty bool `json:"omitempty" dc:"为空则不校验"` Required bool `json:"required" dc:"必传校验"` Lte *float64 `json:"lte" dc:"数字类型小于等于"` -- 2.36.6 From b6821e14971b741c8624c44d13367406a0e7a190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 19:37:16 +0800 Subject: [PATCH 21/26] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E6=95=B0=E6=8D=AE=E6=96=87=E6=A1=A3=E7=94=9F=E6=88=90?= =?UTF-8?q?=E7=9A=84BUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/generate.go | 23 +++++++++++++++++++---- openapi/generate_test.go | 6 ++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/openapi/generate.go b/openapi/generate.go index bd5503a..eb89637 100644 --- a/openapi/generate.go +++ b/openapi/generate.go @@ -167,8 +167,23 @@ func (g *Generate) AddApiDoc(apiMeta define.UriConfig, request any, response any apiOperate.RequestBody = &openapi3.RequestBodyRef{ Extensions: nil, Origin: nil, - Ref: requestTypeStr, - Value: nil, + Ref: "", + Value: &openapi3.RequestBody{ + Extensions: nil, + Origin: nil, + Description: "", + Required: false, + Content: map[string]*openapi3.MediaType{ + consts.MimeTypeJson: { + Extensions: nil, + Origin: nil, + Schema: schemaData, + Example: nil, + Examples: nil, + Encoding: nil, + }, + }, + }, } } @@ -176,7 +191,7 @@ func (g *Generate) AddApiDoc(apiMeta define.UriConfig, request any, response any if _, exist := g.doc.Components.Schemas[requestTypeStr]; !exist { g.doc.Components.Schemas[requestTypeStr] = schemaData } - responseTypeStr := requestType.String() + responseTypeStr := responseType.String() if _, exist := g.doc.Components.Schemas[responseTypeStr]; !exist { g.doc.Components.Schemas[responseTypeStr] = GenerateOpenAPISchema(responseType) } @@ -213,7 +228,7 @@ func (g *Generate) initApiConfig(apiMeta define.UriConfig) (*openapi3.Operation, Extensions: nil, Origin: nil, Ref: "", - Summary: "", + Summary: apiMeta.Desc, Description: "", Connect: nil, Delete: nil, diff --git a/openapi/generate_test.go b/openapi/generate_test.go index 2b93cd5..e4b84f6 100644 --- a/openapi/generate_test.go +++ b/openapi/generate_test.go @@ -40,6 +40,12 @@ func TestGenerate_AddApiDoc(t *testing.T) { TagList: []string{"test"}, Desc: "测试接口", }, Category{}, Product{}) + instance.AddApiDoc(define.UriConfig{ + Path: "/a/b/c", + RequestMethod: http.MethodPost, + TagList: []string{"test"}, + Desc: "测试接口", + }, Category{}, Product{}) // 输出 JSON data, _ := json.MarshalIndent(instance.doc, "", " ") fmt.Println(string(data)) -- 2.36.6 From d45d6e7890ab296eb542a6395ecb0b412d86e637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 21:43:08 +0800 Subject: [PATCH 22/26] feat: swagger server --- openapi/generate.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openapi/generate.go b/openapi/generate.go index eb89637..911dacf 100644 --- a/openapi/generate.go +++ b/openapi/generate.go @@ -94,8 +94,16 @@ func NewGenerate() *Generate { }, }, Security: []openapi3.SecurityRequirement{}, - Servers: []*openapi3.Server{}, - Tags: []*openapi3.Tag{}, + Servers: []*openapi3.Server{ + { + Extensions: nil, + Origin: nil, + URL: "http://127.0.0.1:8080/v1", + Description: "开发环境", + Variables: nil, + }, + }, + Tags: []*openapi3.Tag{}, /*ExternalDocs: &openapi3.ExternalDocs{ Extensions: map[string]any{}, Origin: nil, -- 2.36.6 From d1b5e16d3540caa4ecbfc0a23336d26a9de8543e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 21:59:41 +0800 Subject: [PATCH 23/26] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E5=88=86?= =?UTF-8?q?=E7=BB=84=E3=80=81=E6=A0=87=E7=AD=BE=E3=80=81=E6=8F=8F=E8=BF=B0?= =?UTF-8?q?=E4=B8=8D=E7=8E=B0=E5=AE=9E=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/generate.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openapi/generate.go b/openapi/generate.go index 911dacf..36e6b49 100644 --- a/openapi/generate.go +++ b/openapi/generate.go @@ -237,7 +237,7 @@ func (g *Generate) initApiConfig(apiMeta define.UriConfig) (*openapi3.Operation, Origin: nil, Ref: "", Summary: apiMeta.Desc, - Description: "", + Description: apiMeta.Desc, Connect: nil, Delete: nil, Get: nil, @@ -254,6 +254,10 @@ func (g *Generate) initApiConfig(apiMeta define.UriConfig) (*openapi3.Operation, newOperate := openapi3.NewOperation() newOperate.Parameters = make(openapi3.Parameters, 0) newOperate.Responses = openapi3.NewResponses() + newOperate.Summary = apiMeta.Desc + newOperate.Description = apiMeta.Desc + newOperate.Tags = apiMeta.TagList + newOperate.OperationID = fmt.Sprintf("%v_%v", apiMeta.RequestMethod, apiMeta.Path) isRead := false switch apiMeta.RequestMethod { case http.MethodGet: -- 2.36.6 From a0090ad52469009fa72a288eae8cf47d58533f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 22:08:31 +0800 Subject: [PATCH 24/26] =?UTF-8?q?feat:=20=E8=B0=83=E8=AF=95=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/generate_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi/generate_test.go b/openapi/generate_test.go index e4b84f6..be0cbe0 100644 --- a/openapi/generate_test.go +++ b/openapi/generate_test.go @@ -19,8 +19,8 @@ import ( func TestGenerate_AddApiDoc(t *testing.T) { type Category struct { - ID int64 `json:"id" description:"分类ID" binding:"required,min=10,max=100"` - Name string `json:"name" description:"分类名称" binding:"required,min=8"` + ID int64 `json:"id" description:"分类ID" eg:"123" binding:"required,min=10,max=100"` + Name string `json:"name" description:"分类名称" eg:"baichaqinghuan" binding:"required,min=8"` } type Product struct { ID int64 `json:"id,omitempty" description:"产品ID" example:"1001" required:"true" binding:"required"` -- 2.36.6 From 5d86daadeb14f37754c7b59d8d0f1c0833a2e716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 22:42:29 +0800 Subject: [PATCH 25/26] =?UTF-8?q?feat:=20=E6=9E=9A=E4=B8=BE=E5=80=BC?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20x-enum-descriptions=20=E7=94=A8=E4=BA=8E?= =?UTF-8?q?=E6=A0=87=E8=AE=B0=E6=9E=9A=E4=B8=BE=E5=80=BC=E5=90=AB=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/generate_test.go | 1 + openapi/schema.go | 42 ++++++++++++++++++++-------------------- util/struct_field.go | 9 +++------ 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/openapi/generate_test.go b/openapi/generate_test.go index be0cbe0..ca06d11 100644 --- a/openapi/generate_test.go +++ b/openapi/generate_test.go @@ -20,6 +20,7 @@ import ( func TestGenerate_AddApiDoc(t *testing.T) { type Category struct { ID int64 `json:"id" description:"分类ID" eg:"123" binding:"required,min=10,max=100"` + Sex string `json:"sex" dc:"性别" binding:"required,oneof=man woman" enum-desc:"man:男||woman:女"` Name string `json:"name" description:"分类名称" eg:"baichaqinghuan" binding:"required,min=8"` } type Product struct { diff --git a/openapi/schema.go b/openapi/schema.go index 19f15c7..db2a50f 100644 --- a/openapi/schema.go +++ b/openapi/schema.go @@ -11,30 +11,29 @@ import ( "reflect" "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 { - IsString bool `json:"is_string" dc:"是否字符串"` - 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 *float64 `json:"min_length" dc:"最小长度"` - MaxLength *float64 `json:"max_length" dc:"最大长度"` - Pattern string `json:"pattern" dc:"模式"` - Format string `json:"format" dc:"格式"` - Enum []any `json:"enum" dc:"枚举值列表"` - EnumDesc []define.EnumValue `json:"enum_desc" dc:"枚举值详细描述"` - OmitEmpty bool `json:"omit_empty" dc:"是否可控"` + IsString bool `json:"is_string" dc:"是否字符串"` + 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 *float64 `json:"min_length" dc:"最小长度"` + MaxLength *float64 `json:"max_length" dc:"最大长度"` + Pattern string `json:"pattern" dc:"模式"` + Format string `json:"format" dc:"格式"` + Enum []any `json:"enum" dc:"枚举值列表"` + EnumDesc map[string]string `json:"enum_desc" dc:"枚举值详细描述"` + OmitEmpty bool `json:"omit_empty" dc:"是否可控"` } // ParseStructField 解析结构体字段信息 @@ -212,9 +211,10 @@ func applyFieldInfoToSchema(schema *openapi3.Schema, info *StructFieldInfo) { } if len(info.Enum) > 0 { schema.Enum = info.Enum - /*for _, item := range info.Enum { - schema.Enum = append(schema.Enum, item.Value) - }*/ + if nil == schema.Extensions { + schema.Extensions = map[string]any{} + } + schema.Extensions["x-enum-descriptions"] = info.EnumDesc } } diff --git a/util/struct_field.go b/util/struct_field.go index 683c2d8..d5b3603 100644 --- a/util/struct_field.go +++ b/util/struct_field.go @@ -144,9 +144,9 @@ func (psf parseStructFieldTag) Summary(structField reflect.StructField) string { } // EnumDescription .枚举值详细描述 -func (psf parseStructFieldTag) EnumDescription(structField reflect.StructField) []define.EnumValue { +func (psf parseStructFieldTag) EnumDescription(structField reflect.StructField) map[string]string { defaultTagList := []string{define.TagNameEnumDescription} - res := make([]define.EnumValue, 0) + res := make(map[string]string) for _, tag := range defaultTagList { if tagVal, exist := structField.Tag.Lookup(tag); exist && len(tagVal) > 0 { tagVal = strings.ReplaceAll(tagVal, "###", "`") @@ -156,10 +156,7 @@ func (psf parseStructFieldTag) EnumDescription(structField reflect.StructField) if len(enumArr) < 2 { continue } - res = append(res, define.EnumValue{ - Value: enumArr[0], - Description: strings.Join(enumArr[1:], ":"), - }) + res[enumArr[0]] = strings.Join(enumArr[1:], ":") } return res } -- 2.36.6 From 3ea5af0709b9f91b29ce90f7ba70e61925b2510e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E8=8C=B6=E6=B8=85=E6=AC=A2?= Date: Tue, 6 Jan 2026 23:25:14 +0800 Subject: [PATCH 26/26] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=95=B0=E6=8D=AE=E5=88=9D=E5=A7=8B=E5=8C=96=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/generate.go | 246 +++++++++++++++++++++------------------ openapi/generate_test.go | 9 +- openapi/option.go | 33 ++++++ 3 files changed, 172 insertions(+), 116 deletions(-) create mode 100644 openapi/option.go diff --git a/openapi/generate.go b/openapi/generate.go index 36e6b49..23c2219 100644 --- a/openapi/generate.go +++ b/openapi/generate.go @@ -19,108 +19,130 @@ import ( "github.com/getkin/kin-openapi/openapi3" ) -// NewGenerate 生成文档实例 -func NewGenerate() *Generate { - return &Generate{ - doc: &openapi3.T{ - Extensions: map[string]any{}, - OpenAPI: "3.1.0", - Components: &openapi3.Components{ - Extensions: map[string]any{}, - Origin: &openapi3.Origin{ - Key: &openapi3.Location{ - Line: 0, - Column: 0, - }, - Fields: make(map[string]openapi3.Location), - }, - Schemas: map[string]*openapi3.SchemaRef{}, - Parameters: map[string]*openapi3.ParameterRef{}, - Headers: map[string]*openapi3.HeaderRef{}, - RequestBodies: map[string]*openapi3.RequestBodyRef{}, - Responses: map[string]*openapi3.ResponseRef{}, - SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, - Examples: map[string]*openapi3.ExampleRef{}, - Links: map[string]*openapi3.LinkRef{}, - Callbacks: map[string]*openapi3.CallbackRef{}, - }, - Info: &openapi3.Info{ - Extensions: map[string]any{}, - Origin: &openapi3.Origin{ - Key: &openapi3.Location{ - Line: 0, - Column: 0, - }, - Fields: make(map[string]openapi3.Location), - }, - Title: "服务 API接口 文档", - Description: "服务 API接口 文档", - TermsOfService: "", - Contact: &openapi3.Contact{ - Extensions: map[string]any{}, - Origin: &openapi3.Origin{ - Key: &openapi3.Location{ - Line: 0, - Column: 0, - }, - Fields: make(map[string]openapi3.Location), - }, - Name: "developer", - URL: "", - Email: "developer@test.com", - }, - License: &openapi3.License{ - Extensions: map[string]any{}, - Origin: &openapi3.Origin{ - Key: &openapi3.Location{ - Line: 0, - Column: 0, - }, - Fields: make(map[string]openapi3.Location), - }, - Name: consts.LicenseApache20, - URL: consts.LicenseUrlTable[consts.LicenseApache20], - }, - Version: "0.0.1", - }, - Paths: &openapi3.Paths{ - Extensions: map[string]any{}, - Origin: &openapi3.Origin{ - Key: &openapi3.Location{ - Line: 0, - Column: 0, - }, - Fields: make(map[string]openapi3.Location), - }, - }, - Security: []openapi3.SecurityRequirement{}, - Servers: []*openapi3.Server{ - { - Extensions: nil, - Origin: nil, - URL: "http://127.0.0.1:8080/v1", - Description: "开发环境", - Variables: nil, - }, - }, - Tags: []*openapi3.Tag{}, - /*ExternalDocs: &openapi3.ExternalDocs{ - Extensions: map[string]any{}, - Origin: nil, - Description: "", - URL: "", - },*/ - }, +var ( + DocManager = &Generate{ + docTable: make(map[string]*openapi3.T), } +) + +// NewOpenApiDoc 生成文档实例 +func NewOpenApiDoc(optionFunc ...OptionFunc) *openapi3.T { + t := &openapi3.T{ + Extensions: map[string]any{}, + OpenAPI: "3.1.0", + Components: &openapi3.Components{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + Schemas: map[string]*openapi3.SchemaRef{}, + Parameters: map[string]*openapi3.ParameterRef{}, + Headers: map[string]*openapi3.HeaderRef{}, + RequestBodies: map[string]*openapi3.RequestBodyRef{}, + Responses: map[string]*openapi3.ResponseRef{}, + SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, + Examples: map[string]*openapi3.ExampleRef{}, + Links: map[string]*openapi3.LinkRef{}, + Callbacks: map[string]*openapi3.CallbackRef{}, + }, + Info: &openapi3.Info{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + Title: "服务 API接口 文档", + Description: "服务 API接口 文档", + TermsOfService: "", + Contact: &openapi3.Contact{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + Name: "developer", + URL: "", + Email: "developer@test.com", + }, + License: &openapi3.License{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + Name: consts.LicenseApache20, + URL: consts.LicenseUrlTable[consts.LicenseApache20], + }, + Version: "0.0.1", + }, + Paths: &openapi3.Paths{ + Extensions: map[string]any{}, + Origin: &openapi3.Origin{ + Key: &openapi3.Location{ + Line: 0, + Column: 0, + }, + Fields: make(map[string]openapi3.Location), + }, + }, + Security: []openapi3.SecurityRequirement{}, + Servers: []*openapi3.Server{ + { + Extensions: nil, + Origin: nil, + URL: "http://127.0.0.1:8080", + Description: "开发环境", + Variables: nil, + }, + }, + Tags: []*openapi3.Tag{}, + /*ExternalDocs: &openapi3.ExternalDocs{ + Extensions: map[string]any{}, + Origin: nil, + Description: "", + URL: "", + },*/ + } + for _, item := range optionFunc { + item(t) + } + return t } // Generate 生成 OpenApi 标准规范的文档 type Generate struct { - doc *openapi3.T + docTable map[string]*openapi3.T +} + +// DocData 获取一个文档数据 +func (g *Generate) DocData(docFlag string) *openapi3.T { + return g.docTable[docFlag] +} + +func (g *Generate) NewOpenApiDoc(docFlag string, docOption ...OptionFunc) *openapi3.T { + t := NewOpenApiDoc() + for _, item := range docOption { + item(t) + } + g.docTable[docFlag] = t + return t } // AddApiDoc 添加接口文档 -func (g *Generate) AddApiDoc(apiMeta define.UriConfig, request any, response any) error { +func (g *Generate) AddApiDoc(docFlag string, apiMeta define.UriConfig, request any, response any) error { var ( err error requestType reflect.Type @@ -144,7 +166,7 @@ func (g *Generate) AddApiDoc(apiMeta define.UriConfig, request any, response any } schemaData := GenerateOpenAPISchema(requestType) - apiOperate, isRead := g.initApiConfig(apiMeta) + apiOperate, isRead := g.initApiConfig(docFlag, apiMeta) requestTypeStr := requestType.String() if isRead { for paramName, paramConfig := range schemaData.Value.Properties { @@ -196,12 +218,12 @@ func (g *Generate) AddApiDoc(apiMeta define.UriConfig, request any, response any } // 初始化接口配置 - if _, exist := g.doc.Components.Schemas[requestTypeStr]; !exist { - g.doc.Components.Schemas[requestTypeStr] = schemaData + if _, exist := g.docTable[docFlag].Components.Schemas[requestTypeStr]; !exist { + g.docTable[docFlag].Components.Schemas[requestTypeStr] = schemaData } responseTypeStr := responseType.String() - if _, exist := g.doc.Components.Schemas[responseTypeStr]; !exist { - g.doc.Components.Schemas[responseTypeStr] = GenerateOpenAPISchema(responseType) + if _, exist := g.docTable[docFlag].Components.Schemas[responseTypeStr]; !exist { + g.docTable[docFlag].Components.Schemas[responseTypeStr] = GenerateOpenAPISchema(responseType) } desc := "请求成功" apiOperate.Responses.Set(fmt.Sprintf("%v", http.StatusOK), &openapi3.ResponseRef{ @@ -217,7 +239,7 @@ func (g *Generate) AddApiDoc(apiMeta define.UriConfig, request any, response any consts.MimeTypeJson: { Extensions: nil, Origin: nil, - Schema: g.doc.Components.Schemas[responseTypeStr], + Schema: g.docTable[docFlag].Components.Schemas[responseTypeStr], Example: nil, Examples: nil, Encoding: nil, @@ -230,9 +252,9 @@ func (g *Generate) AddApiDoc(apiMeta define.UriConfig, request any, response any } // initApiConfig 初始化接口配置, 并返回 operation 以及 是否读请求 -func (g *Generate) initApiConfig(apiMeta define.UriConfig) (*openapi3.Operation, bool) { - if nil == g.doc.Paths.Value(apiMeta.Path) { - g.doc.Paths.Set(apiMeta.Path, &openapi3.PathItem{ +func (g *Generate) initApiConfig(docFlag string, apiMeta define.UriConfig) (*openapi3.Operation, bool) { + if nil == g.docTable[docFlag].Paths.Value(apiMeta.Path) { + g.docTable[docFlag].Paths.Set(apiMeta.Path, &openapi3.PathItem{ Extensions: nil, Origin: nil, Ref: "", @@ -261,29 +283,29 @@ func (g *Generate) initApiConfig(apiMeta define.UriConfig) (*openapi3.Operation, isRead := false switch apiMeta.RequestMethod { case http.MethodGet: - g.doc.Paths.Value(apiMeta.Path).Get = newOperate + g.docTable[docFlag].Paths.Value(apiMeta.Path).Get = newOperate isRead = true case http.MethodHead: - g.doc.Paths.Value(apiMeta.Path).Head = newOperate + g.docTable[docFlag].Paths.Value(apiMeta.Path).Head = newOperate isRead = true case http.MethodConnect: - g.doc.Paths.Value(apiMeta.Path).Connect = newOperate + g.docTable[docFlag].Paths.Value(apiMeta.Path).Connect = newOperate isRead = true case http.MethodOptions: - g.doc.Paths.Value(apiMeta.Path).Options = newOperate + g.docTable[docFlag].Paths.Value(apiMeta.Path).Options = newOperate isRead = true case http.MethodTrace: - g.doc.Paths.Value(apiMeta.Path).Trace = newOperate + g.docTable[docFlag].Paths.Value(apiMeta.Path).Trace = newOperate isRead = true // 读请求 case http.MethodPost: - g.doc.Paths.Value(apiMeta.Path).Post = newOperate + g.docTable[docFlag].Paths.Value(apiMeta.Path).Post = newOperate case http.MethodPut: - g.doc.Paths.Value(apiMeta.Path).Put = newOperate + g.docTable[docFlag].Paths.Value(apiMeta.Path).Put = newOperate case http.MethodPatch: - g.doc.Paths.Value(apiMeta.Path).Patch = newOperate + g.docTable[docFlag].Paths.Value(apiMeta.Path).Patch = newOperate case http.MethodDelete: - g.doc.Paths.Value(apiMeta.Path).Delete = newOperate + g.docTable[docFlag].Paths.Value(apiMeta.Path).Delete = newOperate // 写请求 } return newOperate, isRead diff --git a/openapi/generate_test.go b/openapi/generate_test.go index ca06d11..985a944 100644 --- a/openapi/generate_test.go +++ b/openapi/generate_test.go @@ -34,20 +34,21 @@ func TestGenerate_AddApiDoc(t *testing.T) { UpdatedAt *time.Time `json:"updated_at,omitempty" description:"更新时间"` Category *Category `json:"category,omitempty" description:"分类"` } - instance := NewGenerate() - instance.AddApiDoc(define.UriConfig{ + docFlag := "demo" + DocManager.NewOpenApiDoc(docFlag) + DocManager.AddApiDoc(docFlag, define.UriConfig{ Path: "/a/b/c", RequestMethod: http.MethodGet, TagList: []string{"test"}, Desc: "测试接口", }, Category{}, Product{}) - instance.AddApiDoc(define.UriConfig{ + DocManager.AddApiDoc(docFlag, define.UriConfig{ Path: "/a/b/c", RequestMethod: http.MethodPost, TagList: []string{"test"}, Desc: "测试接口", }, Category{}, Product{}) // 输出 JSON - data, _ := json.MarshalIndent(instance.doc, "", " ") + data, _ := json.MarshalIndent(DocManager.docTable[docFlag], "", " ") fmt.Println(string(data)) } diff --git a/openapi/option.go b/openapi/option.go new file mode 100644 index 0000000..3603a99 --- /dev/null +++ b/openapi/option.go @@ -0,0 +1,33 @@ +// Package openapi ... +// +// Description : openapi ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-06 22:48 +package openapi + +import "github.com/getkin/kin-openapi/openapi3" + +// OptionFunc 设置文档选项 +type OptionFunc func(t *openapi3.T) + +// WithServers 设置文档服务器 +func WithServers(serverList openapi3.Servers) OptionFunc { + return func(t *openapi3.T) { + if len(serverList) == 0 { + return + } + t.Servers = serverList + } +} + +// WithInfo 文档基础信息 +func WithInfo(info *openapi3.Info) OptionFunc { + return func(t *openapi3.T) { + if nil == info { + return + } + t.Info = info + } +} -- 2.36.6