Files
junhong_cmp_fiber/pkg/openapi/generator.go
huang 93200a9074
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s
开放接口完成
2026-05-11 14:26:10 +08:00

566 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package openapi
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/swaggest/openapi-go/openapi3"
"gopkg.in/yaml.v3"
)
// Generator OpenAPI 文档生成器
type Generator struct {
Reflector *openapi3.Reflector
}
// SecuritySchemeAgentOpenAPI 表示代理开放接口 Header 签名认证方案
const SecuritySchemeAgentOpenAPI = "agent_open_api"
const (
agentOpenAPIAccountSecurityName = "AgentOpenAPIAccount"
agentOpenAPIPasswordSecurityName = "AgentOpenAPIPassword"
agentOpenAPITimestampSecurityName = "AgentOpenAPITimestamp"
agentOpenAPINonceSecurityName = "AgentOpenAPINonce"
agentOpenAPISignSecurityName = "AgentOpenAPISign"
)
// NewGenerator 创建一个新的生成器实例
func NewGenerator(title, version string) *Generator {
reflector := openapi3.Reflector{}
reflector.Spec = &openapi3.Spec{
Openapi: "3.0.3",
Info: openapi3.Info{
Title: title,
Version: version,
},
}
g := &Generator{Reflector: &reflector}
g.addBearerAuth()
g.addAgentOpenAPIAuth()
return g
}
// addBearerAuth 添加 Bearer Token 认证定义
func (g *Generator) addBearerAuth() {
bearerFormat := "JWT"
g.Reflector.Spec.ComponentsEns().SecuritySchemesEns().WithMapOfSecuritySchemeOrRefValuesItem(
"BearerAuth",
openapi3.SecuritySchemeOrRef{
SecurityScheme: &openapi3.SecurityScheme{
HTTPSecurityScheme: &openapi3.HTTPSecurityScheme{
Scheme: "bearer",
BearerFormat: &bearerFormat,
},
},
},
)
g.addErrorResponseSchema()
}
// addAgentOpenAPIAuth 添加代理开放接口 Header 签名认证定义
func (g *Generator) addAgentOpenAPIAuth() {
g.addAPIKeyHeaderAuth(agentOpenAPIAccountSecurityName, "X-Agent-Account", "代理账号标识,填写代理登录平台使用的用户名或手机号,例如 agent001。")
g.addAPIKeyHeaderAuth(agentOpenAPIPasswordSecurityName, "X-Agent-Password", "代理账号当前登录密码,同时作为 HMAC-SHA256 签名密钥;密码变更后必须使用新密码签名。")
g.addAPIKeyHeaderAuth(agentOpenAPITimestampSecurityName, "X-Agent-Timestamp", "调用方生成的请求发起时间,支持 Unix 秒、Unix 毫秒或 RFC3339 时间,必须与签名原文 timestamp 行一致。")
g.addAPIKeyHeaderAuth(agentOpenAPINonceSecurityName, "X-Agent-Nonce", "调用方每次请求自行生成的随机串,不需要平台提前分配;同一账号在 5 分钟内不可重复。")
g.addAPIKeyHeaderAuth(agentOpenAPISignSecurityName, "X-Agent-Sign", "调用方按签名规则实时计算的 HMAC-SHA256 小写十六进制签名,不是固定值。")
}
// addAPIKeyHeaderAuth 添加一个 Header 类型的 API Key 认证定义
func (g *Generator) addAPIKeyHeaderAuth(name, header, description string) {
g.Reflector.Spec.ComponentsEns().SecuritySchemesEns().WithMapOfSecuritySchemeOrRefValuesItem(
name,
openapi3.SecuritySchemeOrRef{
SecurityScheme: &openapi3.SecurityScheme{
APIKeySecurityScheme: &openapi3.APIKeySecurityScheme{
Name: header,
In: openapi3.APIKeySecuritySchemeInHeader,
Description: &description,
},
},
},
)
}
// addErrorResponseSchema 添加错误响应 Schema 定义
func (g *Generator) addErrorResponseSchema() {
objectType := openapi3.SchemaType("object")
integerType := openapi3.SchemaType("integer")
stringType := openapi3.SchemaType("string")
dateTimeFormat := "date-time"
var errorExample interface{} = "参数验证失败"
var codeExample interface{} = 1001
errorSchema := openapi3.SchemaOrRef{
Schema: &openapi3.Schema{
Type: &objectType,
Properties: map[string]openapi3.SchemaOrRef{
"code": {
Schema: &openapi3.Schema{
Type: &integerType,
Description: ptrString("错误码"),
Example: &codeExample,
},
},
"msg": {
Schema: &openapi3.Schema{
Type: &stringType,
Description: ptrString("错误消息"),
Example: &errorExample,
},
},
"data": {
Schema: &openapi3.Schema{
Type: &objectType,
Description: ptrString("错误详情(可选)"),
},
},
"timestamp": {
Schema: &openapi3.Schema{
Type: &stringType,
Format: &dateTimeFormat,
Description: ptrString("时间戳"),
},
},
},
Required: []string{"code", "msg", "timestamp"},
},
}
g.Reflector.Spec.ComponentsEns().SchemasEns().WithMapOfSchemaOrRefValuesItem("ErrorResponse", errorSchema)
}
func ptrString(s string) *string {
return &s
}
// FileUploadField 定义文件上传字段
type FileUploadField struct {
Name string
Description string
Required bool
}
// AddOperation 向 OpenAPI 规范中添加一个操作
// 参数:
// - method: HTTP 方法GET, POST, PUT, DELETE 等)
// - path: API 路径
// - summary: 操作摘要
// - description: 详细说明,支持 Markdown 语法(可为空)
// - input: 请求参数结构体(可为 nil
// - output: 响应结构体(可为 nil
// - tags: 标签列表
// - requiresAuth: 是否需要认证
func (g *Generator) AddOperation(method, path, summary, description string, input interface{}, body interface{}, output interface{}, requiresAuth bool, tags ...string) {
g.AddOperationWithSecurity(method, path, summary, description, input, body, output, requiresAuth, "", tags...)
}
// AddOperationWithSecurity 添加操作并支持指定 OpenAPI 认证方案
func (g *Generator) AddOperationWithSecurity(method, path, summary, description string, input interface{}, body interface{}, output interface{}, requiresAuth bool, securityScheme string, tags ...string) {
op := openapi3.Operation{
Summary: &summary,
Tags: tags,
}
if description != "" {
op.Description = &description
}
// 反射输入 (请求参数/Body)
if input != nil {
// SetRequest 根据结构体标签自动检测 Body、Query 或 Path 参数
if err := g.Reflector.SetRequest(&op, input, method); err != nil {
panic(err) // 生成过程中出错直接 panic以便快速发现问题
}
}
// 显式传入的 JSON body用于 DELETE 等方法不自动生成 requestBody 的场景
if body != nil {
tempOp := openapi3.Operation{}
if err := g.Reflector.SetRequest(&tempOp, body, "POST"); err != nil {
panic(err)
}
if tempOp.RequestBody != nil {
op.RequestBody = tempOp.RequestBody
}
}
// 反射输出 (响应 Body)
if output != nil {
// 将输出包裹在 envelope 中
g.setEnvelopeResponse(&op, output, 200)
}
// 添加认证要求
if requiresAuth {
g.addSecurityRequirement(&op, securityScheme)
}
// 添加标准错误响应
g.addStandardErrorResponses(&op, requiresAuth)
// 将操作添加到规范中
if err := g.Reflector.Spec.AddOperation(method, path, op); err != nil {
panic(err)
}
}
// AddMultipartOperation 添加支持文件上传的 multipart/form-data 操作
func (g *Generator) AddMultipartOperation(method, path, summary, description string, input interface{}, output interface{}, requiresAuth bool, fileFields []FileUploadField, tags ...string) {
op := openapi3.Operation{
Summary: &summary,
Tags: tags,
}
if description != "" {
op.Description = &description
}
objectType := openapi3.SchemaType("object")
stringType := openapi3.SchemaType("string")
integerType := openapi3.SchemaType("integer")
binaryFormat := "binary"
properties := make(map[string]openapi3.SchemaOrRef)
var requiredFields []string
for _, f := range fileFields {
properties[f.Name] = openapi3.SchemaOrRef{
Schema: &openapi3.Schema{
Type: &stringType,
Format: &binaryFormat,
Description: ptrString(f.Description),
},
}
if f.Required {
requiredFields = append(requiredFields, f.Name)
}
}
if input != nil {
formFields := parseFormFields(input)
for _, field := range formFields {
var schemaType *openapi3.SchemaType
switch field.Type {
case "integer":
schemaType = &integerType
default:
schemaType = &stringType
}
schema := &openapi3.Schema{
Type: schemaType,
Description: ptrString(field.Description),
}
if field.Min != nil {
schema.Minimum = field.Min
}
if field.MaxLength != nil {
schema.MaxLength = field.MaxLength
}
properties[field.Name] = openapi3.SchemaOrRef{Schema: schema}
if field.Required {
requiredFields = append(requiredFields, field.Name)
}
}
}
op.RequestBody = &openapi3.RequestBodyOrRef{
RequestBody: &openapi3.RequestBody{
Required: ptrBool(true),
Content: map[string]openapi3.MediaType{
"multipart/form-data": {
Schema: &openapi3.SchemaOrRef{
Schema: &openapi3.Schema{
Type: &objectType,
Properties: properties,
Required: requiredFields,
},
},
},
},
},
}
if output != nil {
// 将输出包裹在 envelope 中
g.setEnvelopeResponse(&op, output, 200)
}
if requiresAuth {
g.addSecurityRequirement(&op, "")
}
g.addStandardErrorResponses(&op, requiresAuth)
if err := g.Reflector.Spec.AddOperation(method, path, op); err != nil {
panic(err)
}
}
func ptrBool(b bool) *bool {
return &b
}
type formFieldInfo struct {
Name string
Type string
Description string
Required bool
Min *float64
MaxLength *int64
}
func parseFormFields(input interface{}) []formFieldInfo {
var fields []formFieldInfo
t := reflect.TypeOf(input)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return fields
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
formTag := field.Tag.Get("form")
if formTag == "" || formTag == "-" {
continue
}
info := formFieldInfo{
Name: formTag,
Description: field.Tag.Get("description"),
}
switch field.Type.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
info.Type = "integer"
default:
info.Type = "string"
}
validateTag := field.Tag.Get("validate")
if strings.Contains(validateTag, "required") {
info.Required = true
}
if minStr := field.Tag.Get("minimum"); minStr != "" {
if min, err := strconv.ParseFloat(minStr, 64); err == nil {
info.Min = &min
}
}
if maxLenStr := field.Tag.Get("maxLength"); maxLenStr != "" {
if maxLen, err := strconv.ParseInt(maxLenStr, 10, 64); err == nil {
info.MaxLength = &maxLen
}
}
fields = append(fields, info)
}
return fields
}
// setEnvelopeResponse 设置包裹在 envelope 中的响应
func (g *Generator) setEnvelopeResponse(op *openapi3.Operation, output interface{}, statusCode int) {
// 首先调用 SetJSONResponse 让 Reflector 处理 DTO schema
tempOp := openapi3.Operation{}
if err := g.Reflector.SetJSONResponse(&tempOp, output, statusCode); err != nil {
panic(err)
}
// 获取生成的 DTO schema
dtoSchemaOrRef := tempOp.Responses.MapOfResponseOrRefValues[strconv.Itoa(statusCode)].Response.Content["application/json"].Schema
objectType := openapi3.SchemaType("object")
integerType := openapi3.SchemaType("integer")
stringType := openapi3.SchemaType("string")
dateTimeFormat := "date-time"
var successCodeExample interface{} = 0
var successMsgExample interface{} = "success"
// 构造 envelope schema
envelopeSchema := &openapi3.SchemaOrRef{
Schema: &openapi3.Schema{
Type: &objectType,
Properties: map[string]openapi3.SchemaOrRef{
"code": {
Schema: &openapi3.Schema{
Type: &integerType,
Description: ptrString("响应码"),
Example: &successCodeExample,
},
},
"msg": {
Schema: &openapi3.Schema{
Type: &stringType,
Description: ptrString("响应消息"),
Example: &successMsgExample,
},
},
"data": *dtoSchemaOrRef,
"timestamp": {
Schema: &openapi3.Schema{
Type: &stringType,
Format: &dateTimeFormat,
Description: ptrString("时间戳"),
},
},
},
Required: []string{"code", "msg", "data", "timestamp"},
},
}
// 设置响应
statusStr := strconv.Itoa(statusCode)
description := "成功"
if op.Responses.MapOfResponseOrRefValues == nil {
op.Responses.MapOfResponseOrRefValues = make(map[string]openapi3.ResponseOrRef)
}
op.Responses.MapOfResponseOrRefValues[statusStr] = openapi3.ResponseOrRef{
Response: &openapi3.Response{
Description: description,
Content: map[string]openapi3.MediaType{
"application/json": {
Schema: envelopeSchema,
},
},
},
}
}
// addSecurityRequirement 为操作添加认证要求
func (g *Generator) addSecurityRequirement(op *openapi3.Operation, securityScheme string) {
if securityScheme == SecuritySchemeAgentOpenAPI {
op.Security = []map[string][]string{
{
agentOpenAPIAccountSecurityName: {},
agentOpenAPIPasswordSecurityName: {},
agentOpenAPITimestampSecurityName: {},
agentOpenAPINonceSecurityName: {},
agentOpenAPISignSecurityName: {},
},
}
return
}
op.Security = []map[string][]string{
{"BearerAuth": {}},
}
}
// addStandardErrorResponses 添加标准错误响应
func (g *Generator) addStandardErrorResponses(op *openapi3.Operation, requiresAuth bool) {
if op.Responses.MapOfResponseOrRefValues == nil {
op.Responses.MapOfResponseOrRefValues = make(map[string]openapi3.ResponseOrRef)
}
// 400 Bad Request - 所有端点都可能返回
desc400 := "请求参数错误"
op.Responses.MapOfResponseOrRefValues["400"] = openapi3.ResponseOrRef{
Response: &openapi3.Response{
Description: desc400,
Content: map[string]openapi3.MediaType{
"application/json": {
Schema: &openapi3.SchemaOrRef{
SchemaReference: &openapi3.SchemaReference{
Ref: "#/components/schemas/ErrorResponse",
},
},
},
},
},
}
// 401 Unauthorized - 仅认证端点返回
if requiresAuth {
desc401 := "未认证或认证已过期"
op.Responses.MapOfResponseOrRefValues["401"] = openapi3.ResponseOrRef{
Response: &openapi3.Response{
Description: desc401,
Content: map[string]openapi3.MediaType{
"application/json": {
Schema: &openapi3.SchemaOrRef{
SchemaReference: &openapi3.SchemaReference{
Ref: "#/components/schemas/ErrorResponse",
},
},
},
},
},
}
// 403 Forbidden - 仅认证端点返回
desc403 := "无权访问"
op.Responses.MapOfResponseOrRefValues["403"] = openapi3.ResponseOrRef{
Response: &openapi3.Response{
Description: desc403,
Content: map[string]openapi3.MediaType{
"application/json": {
Schema: &openapi3.SchemaOrRef{
SchemaReference: &openapi3.SchemaReference{
Ref: "#/components/schemas/ErrorResponse",
},
},
},
},
},
}
}
// 500 Internal Server Error - 所有端点都可能返回
desc500 := "服务器内部错误"
op.Responses.MapOfResponseOrRefValues["500"] = openapi3.ResponseOrRef{
Response: &openapi3.Response{
Description: desc500,
Content: map[string]openapi3.MediaType{
"application/json": {
Schema: &openapi3.SchemaOrRef{
SchemaReference: &openapi3.SchemaReference{
Ref: "#/components/schemas/ErrorResponse",
},
},
},
},
},
}
}
// Save 将规范导出为 YAML 文件
func (g *Generator) Save(filename string) error {
// 确保目录存在
dir := filepath.Dir(filename)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
// 安全的方法MarshalJSON -> Unmarshal -> MarshalYAML
// 这确保了我们遵守 openapi3 库中定义的 `json` 标签
jsonBytes, err := g.Reflector.Spec.MarshalJSON()
if err != nil {
return err
}
var obj interface{}
if err := json.Unmarshal(jsonBytes, &obj); err != nil {
return err
}
yamlBytes, err := yaml.Marshal(obj)
if err != nil {
return err
}
return os.WriteFile(filename, yamlBytes, 0644)
}