// Package api_doc ... // // Description : api_doc ... // // Author : go_developer@163.com<白茶清欢> // // Date : 2024-07-22 15:55 package api_doc import ( "errors" "fmt" "git.zhangdeman.cn/gateway/api-doc/define" "git.zhangdeman.cn/zhangdeman/consts" "git.zhangdeman.cn/zhangdeman/wrapper" "net/http" "reflect" "strings" ) // NewOpenapiDoc ... // // Author : go_developer@163.com<白茶清欢> // // Date : 15:56 2024/7/22 func NewOpenapiDoc(info *define.Info, servers []*define.ServerItem) *Generate { if nil == info { info = &define.Info{ Description: "openapi接口文档", Title: "openapi接口文档", TermsOfService: "", Contact: nil, License: nil, Version: "0.0.1", } } if len(info.Version) == 0 { info.Version = "0.0.1" } if nil == info.License { info.License = &define.License{ Name: consts.LicenseApache20, Url: consts.LicenseUrlTable[consts.LicenseApache20], } } if nil == info.Contact { info.Contact = &define.Contact{ Name: "研发人员(developer)", Url: "", Email: "", } } if nil == servers { servers = []*define.ServerItem{} } return &Generate{ readMethodList: []string{ http.MethodGet, http.MethodHead, http.MethodConnect, http.MethodOptions, http.MethodTrace, }, docData: &define.OpenapiDoc{ Openapi: consts.SwaggerDocVersion3, Info: info, Servers: servers, Components: &define.Components{Schemas: map[string]*define.Schema{}}, Tags: make([]*define.TagItem, 0), Paths: make(map[string]*define.PathConfig), }, } } // Generate 文档生成实例 // // Author : go_developer@163.com<白茶清欢> // // Date : 15:57 2024/7/22 type Generate struct { docData *define.OpenapiDoc readMethodList []string } func (g *Generate) Doc() *define.OpenapiDoc { return g.docData } // SetLicense 设置文档协议 // // Author : go_developer@163.com<白茶清欢> // // Date : 14:56 2024/8/14 func (g *Generate) SetLicense(name string, url string) { g.docData.Info.License.Name = name g.docData.Info.License.Url = url } // AddTag 新增tag // // Author : go_developer@163.com<白茶清欢> // // Date : 14:23 2024/8/14 func (g *Generate) AddTag(tagName string, tagDesc string) { isHasTag := false for _, item := range g.docData.Tags { if item.Name == tagName { if len(tagDesc) > 0 { item.Description = tagDesc } isHasTag = true break } } if !isHasTag { g.docData.Tags = append(g.docData.Tags, &define.TagItem{ Name: tagName, Description: tagDesc, }) } } // AddServer 添加server // // Author : go_developer@163.com<白茶清欢> // // Date : 14:45 2024/8/14 func (g *Generate) AddServer(serverDomain string, serverDesc string, serverVariable map[string]*define.ServerItemVariable) { if nil == serverVariable { serverVariable = make(map[string]*define.ServerItemVariable) } serverDomain = strings.TrimRight(serverDomain, "/") isHasServer := false for _, item := range g.docData.Servers { if item.Url != serverDomain { continue } isHasServer = true if len(serverDesc) > 0 { item.Description = serverDesc } for varName, varValue := range serverVariable { item.Variables[varName] = varValue } break } if !isHasServer { g.docData.Servers = append(g.docData.Servers, &define.ServerItem{ Url: serverDomain, Description: serverDesc, Variables: serverVariable, }) } } // AddApi 新增Api // // Author : go_developer@163.com<白茶清欢> // // Date : 15:04 2024/8/14 // // baseCfg : 接口基础配置, 示例数据 // // &define.UriBaseConfig{ // Uri: "/foo/bar", // Method: http.MethodPost, // ContentType: ["application/json"], // TagList: []string{"测试标签"}, // Summary: "这是一份示例基础配置", // Description: "这是一份示例基础配置", // } // // paramList : 参数列表 // // resultList : 返回值列表 func (g *Generate) AddApi(baseCfg *define.UriBaseConfig, paramList []*define.ParamConfig, resultList []*define.ResultConfig) error { return nil } // AddApiFromInAndOut 通过请求参数的 // // Author : go_developer@163.com<白茶清欢> // // Date : 14:22 2025/2/9 func (g *Generate) AddApiFromInAndOut(uriPrefix string, paramType reflect.Type, resultType reflect.Type) error { if paramType.Kind() == reflect.Ptr { paramType = paramType.Elem() } if resultType.Kind() == reflect.Ptr { resultType = resultType.Elem() } baseCfg, err := g.parseBaseUriConfig(uriPrefix, paramType) if nil != err { return err } if _, exist := g.docData.Paths[baseCfg.Uri]; !exist { g.docData.Paths[baseCfg.Uri] = &define.PathConfig{} } // 接口文档初始化 cfg := g.getApiDocBaseCfg(baseCfg, paramType) if wrapper.ArrayType(g.readMethodList).Has(baseCfg.Method) >= 0 { cfg.RequestBody = nil // get类请求没有request body // 参数解析 g.ParseReadConfigParam(baseCfg, cfg, paramType) } else { // post类解析 paramSchemaName := g.AddComponentsSchema("", paramType.PkgPath(), paramType) for _, itemType := range baseCfg.ContentType { cfg.RequestBody.Content[itemType] = &define.Media{ Schema: &define.Schema{ Ref: g.getSchemaRef(paramSchemaName), }, Example: "", Examples: nil, Encoding: nil, } } } // 无论什么请求, 对于result解析逻辑一致 resultSchemaName := g.AddComponentsSchema("", resultType.PkgPath(), resultType) for _, itemOutputType := range baseCfg.OutputContentType { cfg.Responses[fmt.Sprintf("%v", http.StatusOK)].Content[itemOutputType] = &define.Media{ Schema: &define.Schema{ Ref: g.getSchemaRef(resultSchemaName), }, Example: "", Examples: nil, Encoding: nil, } } g.setApiDoc(baseCfg, cfg) return nil } // setApiDoc 设置文档配置 // // Author : go_developer@163.com<白茶清欢> // // Date : 16:13 2025/2/14 func (g *Generate) setApiDoc(baseCfg *define.UriBaseConfig, apiDocCfg *define.PathItemOperationConfig) { switch baseCfg.Method { case http.MethodGet: g.docData.Paths[baseCfg.Uri].Get = apiDocCfg case http.MethodHead: g.docData.Paths[baseCfg.Uri].Head = apiDocCfg case http.MethodPost: g.docData.Paths[baseCfg.Uri].Post = apiDocCfg case http.MethodPut: g.docData.Paths[baseCfg.Uri].Put = apiDocCfg case http.MethodPatch: g.docData.Paths[baseCfg.Uri].Patch = apiDocCfg case http.MethodDelete: g.docData.Paths[baseCfg.Uri].Delete = apiDocCfg case http.MethodConnect: g.docData.Paths[baseCfg.Uri].Connect = apiDocCfg case http.MethodOptions: g.docData.Paths[baseCfg.Uri].Options = apiDocCfg case http.MethodTrace: g.docData.Paths[baseCfg.Uri].Trace = apiDocCfg default: panic("unknown method: " + baseCfg.Method) } } // getApiDocBaseCfg 获取接口文档的基础配置 // // Author : go_developer@163.com<白茶清欢> // // Date : 16:10 2025/2/14 func (g *Generate) getApiDocBaseCfg(baseCfg *define.UriBaseConfig, paramType reflect.Type) *define.PathItemOperationConfig { cfg := &define.PathItemOperationConfig{ Tags: baseCfg.TagList, Summary: baseCfg.Summary, Description: baseCfg.Description, ExternalDocs: nil, OperationID: baseCfg.Summary + "(" + strings.ReplaceAll(strings.TrimLeft(baseCfg.Uri, "/"), "/", "-") + ")", Parameters: make([]*define.PathConfigParameter, 0), RequestBody: &define.RequestBody{ Required: true, Description: "", Content: map[string]*define.Media{}, Ref: "", }, Responses: map[string]*define.Response{ fmt.Sprintf("%v", http.StatusOK): &define.Response{ Content: map[string]*define.Media{}, }, }, Callbacks: nil, Deprecated: baseCfg.Deprecated, Security: nil, Servers: nil, } // 解析绑定在url中的参数 if paramList := define.UriParamRegexp.FindAllString(baseCfg.Uri, -1); len(paramList) > 0 { for _, param := range paramList { param = strings.TrimPrefix(param, "{") param = strings.TrimSuffix(param, "}") cfg.Parameters = append(cfg.Parameters, &define.PathConfigParameter{ Name: param, In: consts.SwaggerParameterInPath, Description: param, Required: true, Deprecated: false, Schema: &define.Schema{ Type: consts.SwaggerDataTypeString, Format: consts.DataTypeString.String(), }, AllowEmptyValue: false, }) } } return cfg } // ParseReadConfigParam 解析get类请求参数 // // Author : go_developer@163.com<白茶清欢> // // Date : 11:55 2025/2/14 func (g *Generate) ParseReadConfigParam(requestCfg *define.UriBaseConfig, baseReqCfg *define.PathItemOperationConfig, inputType reflect.Type) { if inputType.Kind() == reflect.Ptr { inputType = inputType.Elem() } if inputType.Kind() != reflect.Struct { panic(requestCfg.Uri + " : request param not struct") } if nil == baseReqCfg.Parameters { baseReqCfg.Parameters = make([]*define.PathConfigParameter, 0) } for i := 0; i < inputType.NumField(); i++ { propertyName := ParseStructField.GetParamName(inputType.Field(i)) if propertyName == "-" { continue } if inputType.Field(i).Type.Kind() == reflect.Struct && (inputType.Field(i).Type.String() == "Meta" || strings.HasSuffix(inputType.Field(i).Type.String(), ".Meta")) && inputType.Field(i).Type.NumField() == 0 { // 空Meta字段认为是用来描述元信息的, 忽略 continue } realInputTypeFormat := inputType.Field(i).Type.String() fieldType := inputType.Field(i).Type /*if inputType.Field(i).Type.Kind() == reflect.Ptr { fieldType = inputType.Field(i).Type.Elem() }*/ if inputType.Field(i).Type.Kind() == reflect.Ptr { // 处理指针 if inputType.Field(i).Type.Elem().Kind() == reflect.Struct { // 结构体指针 schemaNameNext := g.AddComponentsSchema("", propertyName, inputType.Field(i).Type.Elem()) baseReqCfg.Parameters = append(baseReqCfg.Parameters, &define.PathConfigParameter{ Name: propertyName, In: consts.SwaggerParameterInQuery, Description: ParseStructField.GetParamDesc(inputType.Field(i)), Required: ValidateRule.IsRequired(inputType.Field(i)), Deprecated: ParseStructField.Deprecated(inputType.Field(i)), Schema: &define.Schema{ // Format: realInputTypeFormat, Ref: g.getSchemaRef(schemaNameNext), }, AllowEmptyValue: false, Style: "", Explode: false, AllowReserved: false, }) } else { // 当做默认基础类型, 默认不会出现 *map *[] baseReqCfg.Parameters = append(baseReqCfg.Parameters, &define.PathConfigParameter{ Name: propertyName, In: consts.SwaggerParameterInQuery, Description: ParseStructField.GetParamDesc(inputType.Field(i)), Required: ValidateRule.IsRequired(inputType.Field(i)), Deprecated: ParseStructField.Deprecated(inputType.Field(i)), Schema: &define.Schema{ Type: g.realBaseType2SwaggerType(inputType.Field(i).Type.String()), Format: realInputTypeFormat, }, AllowEmptyValue: false, Style: "", Explode: false, AllowReserved: false, }) } continue } if fieldType.Kind() == reflect.Struct || fieldType.Kind() == reflect.Map || fieldType.Kind() == reflect.Array || fieldType.Kind() == reflect.Slice { if convertType := g.realBaseType2SwaggerType(fieldType.String()); !strings.HasPrefix(convertType, "[]") && convertType != inputType.Field(i).Type.Kind().String() { // 针对基础类型指针 baseReqCfg.Parameters = append(baseReqCfg.Parameters, &define.PathConfigParameter{ Name: ParseStructField.GetParamName(inputType.Field(i)), In: consts.SwaggerParameterInQuery, Description: ParseStructField.GetParamDesc(inputType.Field(i)), Required: ValidateRule.IsRequired(inputType.Field(i)), Deprecated: ParseStructField.Deprecated(inputType.Field(i)), Schema: &define.Schema{ Type: g.realBaseType2SwaggerType(inputType.Field(i).Type.String()), Items: nil, Ref: "", Format: realInputTypeFormat, }, AllowEmptyValue: false, Style: "", Explode: false, AllowReserved: false, Ref: "", }) continue } } else { baseReqCfg.Parameters = append(baseReqCfg.Parameters, &define.PathConfigParameter{ Name: ParseStructField.GetParamName(inputType.Field(i)), In: consts.SwaggerParameterInQuery, Description: ParseStructField.GetParamDesc(inputType.Field(i)), Required: ValidateRule.IsRequired(inputType.Field(i)), Deprecated: ParseStructField.Deprecated(inputType.Field(i)), Schema: &define.Schema{ Type: g.realBaseType2SwaggerType(inputType.Field(i).Type.String()), Items: nil, Ref: "", Format: realInputTypeFormat, }, AllowEmptyValue: false, Style: "", Explode: false, AllowReserved: false, Ref: "", }) } } switch requestCfg.Method { case http.MethodGet: g.docData.Paths[requestCfg.Uri].Get = baseReqCfg case http.MethodHead: g.docData.Paths[requestCfg.Uri].Head = baseReqCfg case http.MethodConnect: g.docData.Paths[requestCfg.Uri].Connect = baseReqCfg case http.MethodOptions: g.docData.Paths[requestCfg.Uri].Options = baseReqCfg case http.MethodTrace: g.docData.Paths[requestCfg.Uri].Trace = baseReqCfg } } // AddComponentsSchema 添加schema // // Author : go_developer@163.com<白茶清欢> // // Date : 15:25 2025/2/8 func (g *Generate) AddComponentsSchema(rootSchemaName string, pkgPath string, inputType reflect.Type) string { if inputType.Kind() == reflect.Struct && (inputType.String() == "Meta" || strings.HasSuffix(inputType.String(), ".Meta")) && inputType.NumField() == 0 { // 空Meta字段认为是用来描述元信息的, 忽略 return "-" } inputNameArr := strings.Split(inputType.Name(), ".") inputName := inputNameArr[len(inputNameArr)-1] schemaName := strings.ReplaceAll(pkgPath+"."+inputName, "/", ".") if schemaName == "-" { // 忽略的属性 return schemaName } if _, exist := g.docData.Components.Schemas[schemaName]; exist { // 已存在, 无需重复生成 return schemaName } if _, exist := g.docData.Components.Schemas[schemaName]; !exist { s := &define.Schema{ Nullable: false, Deprecated: false, Properties: make(map[string]*define.Property), Required: make([]string, 0), Enum: make([]any, 0), Type: consts.SwaggerDataTypeObject, // TODO : 区分数组 Ref: g.getSchemaRef(schemaName), } if len(rootSchemaName) == 0 || inputType.Kind() == reflect.Struct { s.Ref = "" } g.docData.Components.Schemas[schemaName] = s } if inputType.Kind() == reflect.Map { // map, 直接添加公共 g.docData.Components.Schemas[schemaName].Type = consts.SwaggerDataTypeObject return schemaName } // 数组 if inputType.Kind() == reflect.Slice || inputType.Kind() == reflect.Array { if len(rootSchemaName) == 0 { g.docData.Components.Schemas[schemaName].Type = consts.SwaggerDataTypeArray sliceItemType := g.parseSliceItem(schemaName, inputType) g.docData.Components.Schemas[schemaName].Items = &define.PropertyXOf{Ref: g.getSchemaRef(sliceItemType)} } else { sliceItemType := g.parseSliceItem(schemaName, inputType) g.docData.Components.Schemas[rootSchemaName].Properties[schemaName] = &define.Property{ Type: consts.SwaggerDataTypeArray, Format: inputType.String(), Items: &define.PropertyXOf{Ref: g.getSchemaRef(sliceItemType)}, } } return schemaName } // 结构体 if inputType.Kind() == reflect.Struct { for i := 0; i < inputType.NumField(); i++ { propertyName := ParseStructField.GetParamName(inputType.Field(i)) if propertyName == "-" { continue } if inputType.Field(i).Type.Kind() == reflect.Ptr { // 处理指针 if inputType.Field(i).Type.Elem().Kind() == reflect.Struct { // 结构体指针 schemaNameNext := g.AddComponentsSchema(schemaName, propertyName, inputType.Field(i).Type.Elem()) g.docData.Components.Schemas[schemaName].Properties[propertyName] = &define.Property{ Ref: g.getSchemaRef(schemaNameNext), } } else { // 当做默认基础类型, 默认不会出现 *map *[] g.docData.Components.Schemas[schemaName].Properties[propertyName] = &define.Property{ Type: g.realBaseType2SwaggerType(g.realBaseType2SwaggerType(inputType.Field(i).Type.String())), Format: inputType.Field(i).Type.String(), Default: ParseStructField.GetDefaultValue(inputType.Field(i)), Description: ParseStructField.GetParamDesc(inputType.Field(i)), } } continue } if inputType.Field(i).Type.Kind() == reflect.Struct || inputType.Field(i).Type.Kind() == reflect.Map || inputType.Field(i).Type.Kind() == reflect.Array || inputType.Field(i).Type.Kind() == reflect.Slice { if inputType.Field(i).Type.Kind() == reflect.Struct || inputType.Field(i).Type.Kind() == reflect.Map { g.docData.Components.Schemas[schemaName].Properties[propertyName] = &define.Property{ Type: consts.SwaggerDataTypeObject, Format: inputType.Field(i).Type.String(), Description: ParseStructField.GetParamDesc(inputType.Field(i)), Properties: map[string]*define.Property{}, } } else if inputType.Field(i).Type.Kind() == reflect.Array || inputType.Field(i).Type.Kind() == reflect.Slice { g.docData.Components.Schemas[schemaName].Properties[propertyName] = &define.Property{ Type: consts.SwaggerDataTypeArray, Format: inputType.Field(i).Type.String(), Description: ParseStructField.GetParamDesc(inputType.Field(i)), Items: &define.PropertyXOf{ Ref: g.getSchemaRef(g.parseSliceItem(schemaName, inputType.Field(i).Type)), }, Properties: map[string]*define.Property{}, } } else { g.AddComponentsSchema(schemaName, propertyName, inputType.Field(i).Type) } } else { g.docData.Components.Schemas[schemaName].Properties[propertyName] = &define.Property{ Type: g.realBaseType2SwaggerType(inputType.Field(i).Type.String()), Format: inputType.Field(i).Type.String(), Default: ParseStructField.GetDefaultValue(inputType.Field(i)), Description: ParseStructField.GetParamDesc(inputType.Field(i)), } } // 设置参数各种属性 g.setStructFieldProperty(schemaName, inputType.Field(i)) } return schemaName } // 指针 if inputType.Kind() == reflect.Ptr { if inputType.Elem().Kind() == reflect.Struct { // 非基础数据类型 return g.AddComponentsSchema(schemaName, inputType.Elem().String(), inputType.Elem()) } else { convertType := g.realBaseType2SwaggerType(inputType.String()) g.docData.Components.Schemas[schemaName].Properties[schemaName] = &define.Property{ Type: convertType, Format: inputType.String(), Properties: map[string]*define.Property{}, } } } return schemaName } // parseSliceItem 解析数组每一项 // // Author : go_developer@163.com<白茶清欢> // // Date : 21:33 2025/2/8 func (g *Generate) parseSliceItem(rootSchemaName string, inputType reflect.Type) string { if inputType.Kind() != reflect.Slice && inputType.Kind() != reflect.Array { // 不是数组 return "" } sliceValue := reflect.MakeSlice(inputType, 1, 1) sliceItemType := sliceValue.Index(0).Type() if sliceItemType.Kind() == reflect.Ptr { sliceItemType = sliceItemType.Elem() } g.AddComponentsSchema(rootSchemaName, sliceItemType.PkgPath(), sliceItemType) if len(sliceItemType.PkgPath()) == 0 { return sliceItemType.String() } return sliceItemType.PkgPath() + "." + sliceItemType.Name() } // getSchemaRef 获取引用的类型 // // Author : go_developer@163.com<白茶清欢> // // Date : 14:25 2025/2/9 func (g *Generate) getSchemaRef(schemaName string) string { if "" == schemaName { return "" } schemaName = strings.ReplaceAll(schemaName, "*", "") // 去除指针类型 * return "#/components/schemas/" + strings.ReplaceAll(schemaName, "/", ".") } // realType2SwaggerType golang 真实数据类型转换为golang数据类型 // // Author : go_developer@163.com<白茶清欢> // // Date : 20:25 2025/2/11 func (g *Generate) realBaseType2SwaggerType(realType string) string { switch realType { case "bool", "*bool": return consts.SwaggerDataTypeBoolean case "string", "*string": return consts.SwaggerDataTypeString case "byte", "*byte": return consts.SwaggerDataTypeByte case "float32", "*float32", "float64", "*float64": return consts.SwaggerDataTypeDouble case "int", "*int", "uint", "*uint", "int64", "*int64", "uint64", "*uint64": return consts.SwaggerDataTypeInteger case "int8", "*int8", "uint8", "*uint8", "int16", "*int16", "uint16", "*uint16", "int32", "*int32", "uint32", "*uint32": return consts.SwaggerDataTypeInteger default: return realType } } // realType2SwaggerType ... // // Author : go_developer@163.com<白茶清欢> // // Date : 21:20 2025/2/11 func (g *Generate) realType2SwaggerType(realType string) string { if strings.HasPrefix(realType, "[]") { return consts.SwaggerDataTypeArray } return g.realBaseType2SwaggerType(realType) } // setStructFieldProperty 添加struct_field各种属性 // // Author : go_developer@163.com<白茶清欢> // // Date : 16:13 2025/2/13 func (g *Generate) setStructFieldProperty(schemaName string, structField reflect.StructField) { paramName := ParseStructField.GetParamName(structField) if ValidateRule.IsRequired(structField) { g.docData.Components.Schemas[schemaName].Required = append(g.docData.Components.Schemas[schemaName].Required, paramName) } g.docData.Components.Schemas[schemaName].Properties[ParseStructField.GetParamName(structField)].Enum = ValidateRule.Enum(structField) } // parseBaseUriConfig 通过Meta字段解析Uri基础配置信息 // // Author : go_developer@163.com<白茶清欢> // // Date : 15:13 2025/2/14 func (g *Generate) parseBaseUriConfig(uriPrefix string, paramType reflect.Type) (*define.UriBaseConfig, error) { // 解析meta信息 metaField, metaFieldExist := paramType.FieldByName("Meta") if !metaFieldExist { return nil, errors.New("Meta field not found") } res := &define.UriBaseConfig{ Uri: "", Method: "", ContentType: nil, OutputContentType: nil, TagList: nil, Summary: "", Description: "", ParamList: nil, ResultList: nil, Deprecated: false, } res.Uri = metaField.Tag.Get(define.TagPath) if len(uriPrefix) > 0 { res.Uri = strings.TrimRight(uriPrefix, "/") + "/" + strings.TrimLeft(res.Uri, "/") } // 保证接口路由以 /开头 res.Uri = "/" + strings.TrimLeft(res.Uri, "/") res.Method = strings.ToUpper(metaField.Tag.Get(define.TagMethod)) res.Description = metaField.Tag.Get(define.TagDesc) res.TagList = strings.Split(metaField.Tag.Get(define.TagUriTag), ",") // 解析第一个返回值, 要求必须是结构体或者是map outputStrictModel := metaField.Tag.Get(define.TagOutputStrict) res.OutputStrict = outputStrictModel == "1" || outputStrictModel == "true" deprecated := metaField.Tag.Get(define.TagDeprecated) res.Deprecated = deprecated == "1" || deprecated == "true" requestContentType := strings.TrimSpace(metaField.Tag.Get(define.TagContentType)) if len(requestContentType) == 0 { if wrapper.ArrayType(g.readMethodList).Has(res.Method) >= 0 { // get类请求 requestContentType = consts.MimeTypeXWWWFormUrlencoded } else { requestContentType = consts.MimeTypeJson } } res.ContentType = strings.Split(requestContentType, ",") responseContentType := strings.TrimSpace(metaField.Tag.Get(define.TagOutputContentType)) if len(responseContentType) == 0 { // 未设置响应类型默认JSON数据 responseContentType = consts.MimeTypeJson } res.OutputContentType = strings.Split(responseContentType, ",") res.Summary = ParseStructField.Summary(metaField) if res.Method == "" { return nil, errors.New("baseCfg.Method is empty") } if res.Uri == "" { return nil, errors.New("baseCfg.Uri is empty") } if nil == g.docData.Tags { g.docData.Tags = make([]*define.TagItem, 0) } // 增加tag for _, itemTag := range res.TagList { exist := false for _, t := range g.docData.Tags { if itemTag == t.Name { exist = true break } } if !exist { g.docData.Tags = append(g.docData.Tags, &define.TagItem{ Name: itemTag, Description: itemTag, }) } } return res, nil }