# API 定義(goctl + go-doc 共用) ## 檔案 | 檔案 | 用途 | |------|------| | `gateway.api` | 入口:`info()` + `import` | | `common.api` | 共用文件型別(`APIErrorStatus`、`ErrorDetail`) | | `auth.api` | Auth 路由(scope 28) | | `member.api` | Member 路由(scope 29) | | `permission.api` | Permission / RBAC 路由(scope 31) | | `normal.api` | 路由與業務 `data` 型別 | ## 指令 ```bash make gen-api # 生成 handler / logic / types make gen-doc # 生成 docs/openapi/gateway.yaml(OpenAPI 3.0) ``` ## 註解約定 - **Logic `returns`**:只寫業務 data(如 `PingData`) - **文件 `@respdoc`**:寫實際 HTTP JSON(如 `PingOKStatus`、`APIErrorStatus`) - **`@doc`**:單一 API 的 summary / description - 多狀態碼用 `/* @respdoc-200 ... */` 區塊,放在 `@handler` 前 - **Request 驗證**:欄位可加 `validate:"required,email"` 等 tag;`make gen-api` 後 handler 會自動 `ValidateAll`(見 `generate/goctl/api/handler.tpl`) ## 文件分組與欄位說明(go-doc 規則) OpenAPI 由 `generate/doc-generate`(go-doc)從 `.api` 直接讀取,下列三種寫法**必須**遵守,AI agent 在新增 / 修改 API 時請一併照做: ### 1. Swagger UI 分組(tags) 每個 `.api` 檔的 `@server (...)` 區塊**必須**帶 `tags:` 與 `summary:`,否則 endpoint 會散落在 "default" 群組。 ```go @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 結尾的同一行 `//` 註解**,寫在前一行不會被解析。 ```go // ❌ 不會被當 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`(管道分隔): ```go // ❌ 只有 validate 不會出 enum Provider string `json:"provider" validate:"required,oneof=google"` // ✅ 同時保留兩者:options= 給 go-doc,validate= 給 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` 後**必跑**: ```bash make gen-api # 同步 internal/types/types.go(json tag 帶 options= 給 go-zero runtime) make gen-doc # 同步 docs/openapi/gateway.yaml(gitignore,本地驗證用) go build ./... # 確保編譯通過 ``` 驗證:把 `docs/openapi/gateway.yaml` 丟進 https://editor.swagger.io 或 Swagger UI,確認分組、description、enum 都正確顯示。 ## Middleware(go-zero 正規手段) **強制原則**:所有需要登入 / 授權的 endpoint 都用 `.api` 的 `middleware:` 宣告,**不要**在 `gateway.go` 用 `server.Use(...)` 全域掛載。 ### 設定方式 在 `@server` 加 `middleware: AuthJWT` 或 `middleware: AuthJWT,CasbinRBAC`(多個用逗號): ```go // 公開(不掛)— 註冊 / 登入 / 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 ... } // 需登入 + RBAC(AuthJWT,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-api` 後 `internal/handler/routes.go` 會自動產生: ```go server.AddRoutes( rest.WithMiddlewares( []rest.Middleware{serverCtx.AuthJWT, serverCtx.CasbinRBAC}, []rest.Route{ ... }..., ), rest.WithPrefix("/api/v1/permissions"), ) ``` ### 撰寫新 middleware(go-zero 標準 struct 模式) 每個 middleware **必須**是 struct + `NewXxxMiddleware(...)` 建構函式 + `Handle(next http.HandlerFunc) http.HandlerFunc` 方法。範例: ```go // 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 會依 `.api` 的 `middleware:` 名字產生 stub 檔,命名 = `stringx.From().ToLower() + "_middleware.go"`,例如: - `AuthJWT` → `authjwt_middleware.go` - `CasbinRBAC` → `casbinrbac_middleware.go` 新 middleware **必須**直接用這個檔名寫實作,goctl 下次 gen-api 才會跳過(看到「已存在」)。 ### ServiceContext 注入 在 `internal/svc/service_context.go` 暴露 `rest.Middleware` 欄位並在 `NewServiceContext` 結尾 wire 起來: ```go 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 value(`func(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.go` 與 `internal/logic/permission/actor.go` 是 `library/actor` 的 thin alias,方便既有程式用 `member.Actor` / `member.ActorFromContext`,但底層 key 是同一個。 ## 與 runtime 對齊 Handler 使用 `response.Write` 輸出: ```json { "code": 102000, "message": "SUCCESS", "data": { ... } } ``` 失敗時含 `error.biz_code` / `error.scope` 等欄位。Handler parse 錯誤為 Facade scope(`10101000`);各模組 logic/usecase 使用對應 scope(Auth=28、Member=29)。