template-monorepo/generate/api/README.md

7.8 KiB
Raw Permalink Blame History

API 定義goctl + go-doc 共用)

檔案

檔案 用途
gateway.api 入口:info() + import
common.api 共用文件型別(APIErrorStatusErrorDetail
auth.api Auth 路由scope 28
member.api Member 路由scope 29
permission.api Permission / RBAC 路由scope 31
normal.api 路由與業務 data 型別

指令

make gen-api   # 生成 handler / logic / types
make gen-doc   # 生成 docs/openapi/gateway.yamlOpenAPI 3.0

註解約定

  • Logic returns:只寫業務 dataPingData
  • 文件 @respdoc:寫實際 HTTP JSONPingOKStatusAPIErrorStatus
  • @doc:單一 API 的 summary / description
  • 多狀態碼用 /* @respdoc-200 ... */ 區塊,放在 @handler
  • Request 驗證:欄位可加 validate:"required,email" 等 tagmake gen-api 後 handler 會自動 ValidateAll(見 generate/goctl/api/handler.tpl

文件分組與欄位說明go-doc 規則)

OpenAPI 由 generate/doc-generatego-doc.api 直接讀取,下列三種寫法必須遵守AI agent 在新增 / 修改 API 時請一併照做:

1. Swagger UI 分組tags

每個 .api 檔的 @server (...) 區塊必須tags:summary:,否則 endpoint 會散落在 "default" 群組。

@server (
    group:   permission        // goctl 用:決定 handler 子目錄
    prefix:  /api/v1/permissions
    tags:    "Permission - 權限"      // ← go-doc 用Swagger UI 分組標題
    summary: "Catalog / 角色 / 使用者角色 / 外部映射 / Policy"
)

一個 .api 內可以開多個 @server 區塊(相同 group / prefix 但不同 tags:),把同一模組再拆成子組。

2. 欄位中文 description

go-doc 把欄位的 description 只認 backtick 結尾的同一行 // 註解,寫在前一行不會被解析。

// ❌ 不會被當 description
RegisterReq {
    // 租戶 slug
    TenantSlug string `json:"tenant_slug" validate:"required"`
}

// ✅ 正確寫法backtick 行末 //
RegisterReq {
    TenantSlug string `json:"tenant_slug" validate:"required"` // 租戶 slug小寫英數
    Email      string `json:"email" validate:"required,email"` // 電子郵件
    Password   string `json:"password" validate:"required,min=8,max=128"` // 密碼8-128 字元)
}

所有 Request 型別(命名 *Req / *Query / *Path 的 struct每個欄位都要加中文 description。Response / Data 型別可選。

3. Enum 列舉值Swagger UI 下拉選單)

validate:"oneof=A B C" 是 runtime 驗證用的,go-doc 不會解析。要讓 Swagger UI 顯示下拉選單,必須在 tag 內加 options=A|B|C(管道分隔):

// ❌ 只有 validate 不會出 enum
Provider string `json:"provider" validate:"required,oneof=google"`

// ✅ 同時保留兩者options= 給 go-docvalidate= 給 runtime
Provider string `json:"provider,options=google" validate:"required,oneof=google"`
Source   string `json:"source,optional,options=manual|zitadel|ldap|scim" validate:"omitempty,oneof=manual zitadel ldap scim"`
Status   string `form:"status,optional,options=open|close" validate:"omitempty,oneof=open close"`

規則:只要有 oneof=...,就一定要在同欄位 tag 加對應 options=...,並把可選值也寫進行末註解。

4. 修改流程

每次改 .api必跑

make gen-api   # 同步 internal/types/types.gojson tag 帶 options= 給 go-zero runtime
make gen-doc   # 同步 docs/openapi/gateway.yamlgitignore本地驗證用
go build ./... # 確保編譯通過

驗證:把 docs/openapi/gateway.yaml 丟進 https://editor.swagger.io 或 Swagger UI確認分組、description、enum 都正確顯示。

Middlewarego-zero 正規手段)

強制原則:所有需要登入 / 授權的 endpoint 都用 .apimiddleware: 宣告,不要gateway.goserver.Use(...) 全域掛載。

設定方式

@servermiddleware: AuthJWTmiddleware: AuthJWT,CasbinRBAC(多個用逗號):

// 公開(不掛)— 註冊 / 登入 / token refresh / health
@server (
    group:  auth
    prefix: /api/v1/auth
    tags:   "Auth - 認證(公開)"
)
service gateway { ... register / login / token/exchange / token/refresh ... }

// 需登入AuthJWT— logout / member 自身資料 / Permission catalog & me
@server (
    group:      auth
    prefix:     /api/v1/auth
    middleware: AuthJWT
    tags:       "Auth - 認證(需 Bearer"
)
service gateway { ... logout ... }

// 需登入 + RBACAuthJWT,CasbinRBAC— Permission 管理類 endpoint
@server (
    group:      permission
    prefix:     /api/v1/permissions
    middleware: AuthJWT,CasbinRBAC
    tags:       "Permission - 權限(管理)"
)
service gateway { ... roles / role-mappings / users / policy/reload ... }

make gen-apiinternal/handler/routes.go 會自動產生:

server.AddRoutes(
    rest.WithMiddlewares(
        []rest.Middleware{serverCtx.AuthJWT, serverCtx.CasbinRBAC},
        []rest.Route{ ... }...,
    ),
    rest.WithPrefix("/api/v1/permissions"),
)

撰寫新 middlewarego-zero 標準 struct 模式)

每個 middleware 必須是 struct + NewXxxMiddleware(...) 建構函式 + Handle(next http.HandlerFunc) http.HandlerFunc 方法。範例:

// internal/middleware/authjwt_middleware.go  ← 檔名由 goctl stringx 決定
package middleware

type AuthJWTMiddleware struct {
    tokens domauth.TokenUseCase
}

func NewAuthJWTMiddleware(tokens domauth.TokenUseCase) *AuthJWTMiddleware {
    return &AuthJWTMiddleware{tokens: tokens}
}

func (m *AuthJWTMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // ...檢查 / 注入 actor / 呼叫 next(w, r)
    }
}

檔名規則goctl 會依 .apimiddleware: 名字產生 stub 檔,命名 = stringx.From(<Name>).ToLower() + "_middleware.go",例如:

  • AuthJWTauthjwt_middleware.go
  • CasbinRBACcasbinrbac_middleware.go

新 middleware 必須直接用這個檔名寫實作goctl 下次 gen-api 才會跳過(看到「已存在」)。

ServiceContext 注入

internal/svc/service_context.go 暴露 rest.Middleware 欄位並在 NewServiceContext 結尾 wire 起來:

type ServiceContext struct {
    ...
    AuthJWT    rest.Middleware
    CasbinRBAC rest.Middleware
}

func NewServiceContext(c config.Config) *ServiceContext {
    ...
    // 結尾,所有 use case 都建好之後
    sc.AuthJWT    = middleware.NewAuthJWTMiddleware(sc.AuthToken).Handle
    sc.CasbinRBAC = middleware.NewCasbinRBACMiddleware(sc.PermissionRBAC, middleware.CasbinRBACOptions{}).Handle
    return sc
}

.Handle 是 method valuefunc(next http.HandlerFunc) http.HandlerFunc),剛好符合 rest.Middleware 簽名。

Actor context 共享

JWT middleware 解析完 token 後用 internal/library/actor.WithActor(ctx, tenantID, uid) 注入 actor。 所有層(logic / usecase / 其他 middleware)讀 actor 都用 actor.ActorFromContext(ctx) —— 不要在各 logic 套件各自定義 actorKey struct{},否則 context value 進不來。

internal/logic/member/actor.gointernal/logic/permission/actor.golibrary/actor 的 thin alias方便既有程式用 member.Actor / member.ActorFromContext,但底層 key 是同一個。

與 runtime 對齊

Handler 使用 response.Write 輸出:

{ "code": 102000, "message": "SUCCESS", "data": { ... } }

失敗時含 error.biz_code / error.scope 等欄位。Handler parse 錯誤為 Facade scope10101000);各模組 logic/usecase 使用對應 scopeAuth=28、Member=29