All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s
566 lines
16 KiB
Go
566 lines
16 KiB
Go
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)
|
||
}
|