template-monorepo/generate/api/README.md

212 lines
7.8 KiB
Markdown
Raw Permalink Normal View History

2026-05-19 11:00:28 +00:00
# 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 |
2026-05-19 11:00:28 +00:00
| `normal.api` | 路由與業務 `data` 型別 |
## 指令
```bash
make gen-api # 生成 handler / logic / types
make gen-doc # 生成 docs/openapi/gateway.yamlOpenAPI 3.0
```
## 註解約定
- **Logic `returns`**:只寫業務 data`PingData`
- **文件 `@respdoc`**:寫實際 HTTP JSON`PingOKStatus`、`APIErrorStatus`
- **`@doc`**:單一 API 的 summary / description
- 多狀態碼用 `/* @respdoc-200 ... */` 區塊,放在 `@handler`
2026-05-19 12:56:32 +00:00
- **Request 驗證**:欄位可加 `validate:"required,email"` 等 tag`make gen-api` 後 handler 會自動 `ValidateAll`(見 `generate/goctl/api/handler.tpl`
2026-05-19 11:00:28 +00:00
## 文件分組與欄位說明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-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` 後**必跑**
```bash
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 都正確顯示。
refactor(middleware): wire AuthJWT + CasbinRBAC via .api middleware directive Stop relying on a global server.Use(CloudEPJWT) that was invisible from the .api source. Protected routes now declare middleware explicitly in each @server block and goctl chains them into routes.go — the .api file is the single source of truth for "who needs Bearer / who needs RBAC". Concretely: - Rewrite middleware to go-zero's standard struct + Handle() pattern. AuthJWT becomes strict: missing/invalid Bearer returns 28501000 (was soft passthrough). CasbinRBAC stays nil-tolerant so dev/test boots without a policy. - Files renamed to goctl's stringx convention (authjwt_middleware.go, casbinrbac_middleware.go) so future `make gen-api` runs see them as already-generated and skip the empty stub. - Move actor context helpers (Actor, WithActor, ActorFromContext) into internal/library/actor so middleware and BOTH logic packages share one context key. Previously each logic package had its own private actorKey struct{}, so an actor injected for member was invisible to permission — the permission RBAC chain would always see "missing actor". member/permission actor.go are now thin type-alias shims. - .api files declare middleware per group: auth.api (public) → no middleware (register/login/token/...) auth.api (logout) → middleware: AuthJWT member.api → middleware: AuthJWT permission.api (catalog,me) → middleware: AuthJWT permission.api (admin ops) → middleware: AuthJWT,CasbinRBAC normal.api (/health) → no middleware - ServiceContext exposes AuthJWT / CasbinRBAC as rest.Middleware; the global server.Use(...) in gateway.go is removed. - Document the pattern in AGENTS.md (cross-agent rules) and generate/api/README.md (detailed examples + filename rules) so any future AI agent or human follows the same convention. make gen-api / gen-doc / lint / build all pass. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 09:30:50 +00:00
## Middlewarego-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 ... }
// 需登入 + 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-api``internal/handler/routes.go` 會自動產生:
```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` 方法。範例:
```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(<Name>).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 是同一個。
2026-05-19 11:00:28 +00:00
## 與 runtime 對齊
Handler 使用 `response.Write` 輸出:
```json
{ "code": 102000, "message": "SUCCESS", "data": { ... } }
2026-05-19 11:00:28 +00:00
```
失敗時含 `error.biz_code` / `error.scope` 等欄位。Handler parse 錯誤為 Facade scope`10101000`);各模組 logic/usecase 使用對應 scopeAuth=28、Member=29