Go to file
王性驊 9616969fd0 test(e2e): 加 banner / e2e-list / k6 風格 user journey
讓「我有哪些測試、現在在測什麼」一眼看得到,並補上跨 endpoint 的狀態流測試:

每個測試開頭印中文 banner
- 新增 e2eStep(t, id, method, path, desc) helper(test/e2e/setup_test.go)
- 17 個 contract test 開頭加 banner,go test -v 會逐個顯示
    ▶ [M-01] GET /api/v1/members/me — 讀 profile(tenant/uid/status)
- 對外 ID 與 docs/e2e-testing.md 的測試覆蓋矩陣對齊

新增 make e2e-list
- scripts/e2e-list.sh 掃 _test.go,分兩節印 contract tests + journeys;
  每個 journey 列出所有 step ID + 描述(Step 用 ▶、SkipStep 用 ⊘)

scripts 彩色 step banner + optional MailHog
- scripts/e2e-lib.sh 抽共用 helpers(e2e_step/info/ok/warn、e2e_print_services)
- e2e-run.sh / e2e-up.sh 改用 step banner + 服務面板(執行完印出 Mongo/Redis/
  Gateway/MailHog 的 URL)
- E2E_WITH_SMTP=1 會額外起 MailHog(http://localhost:8025),方便肉眼確認流程

k6 風格 user journey
- 新增 test/e2e/journey.go:NewJourney + Step + SkipStep + Summary,
  任一步 fail 自動 skip 後續,輸出 ▶ [J-x.y] 階層 banner
- J-1 Tenant Owner 入職第一天(12 steps):/me → PATCH → email verify
  → phone verify → TOTP enroll/verify/replay/disable
- J-2 Tenant Admin 建 qa_engineer 角色 → 指派 → 二人視角驗證 → 撤銷(8 steps)
- J-3 Session 生命週期 refresh → /me → logout → 舊 token 401(4 steps,ZZZ 排最後)
- J-4 完整註冊 → 登入(5 steps stub,標 SkipStep;接 ZITADEL container 後改 Step 即可)
- make e2e-journey / make test-e2e-journey 拆獨立 target;e2e-run.sh 透過
  E2E_MODE=journey + E2E_TEST_PATTERN_ZZZ 切換

docs/e2e-testing.md
- 首節改為「我現在有哪些測試?make e2e-list」並附 banner 範例輸出
- 加 Journeys 章節:journey 列表、執行範例、失敗時的輸出、寫新 journey 範本
- 補 e2e-journey / test-e2e-journey / E2E_WITH_SMTP 環境變數

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 17:18:36 +08:00
cmd add member totp 2026-05-22 07:52:39 +08:00
deploy chore: gitignore local gateway.dev.yaml and add example template 2026-05-20 15:14:44 +08:00
docs test(e2e): 加 banner / e2e-list / k6 風格 user journey 2026-05-22 17:18:36 +08:00
etc docs: 統整模組 README ↔ SDD 分工,砍重複內容 2026-05-22 17:18:08 +08:00
generate refactor(middleware): wire AuthJWT + CasbinRBAC via .api middleware directive 2026-05-21 17:30:50 +08:00
internal docs: 統整模組 README ↔ SDD 分工,砍重複內容 2026-05-22 17:18:08 +08:00
scripts test(e2e): 加 banner / e2e-list / k6 風格 user journey 2026-05-22 17:18:36 +08:00
test/e2e test(e2e): 加 banner / e2e-list / k6 風格 user journey 2026-05-22 17:18:36 +08:00
.gitignore add member totp 2026-05-22 07:52:39 +08:00
.golangci.yml add member totp 2026-05-20 21:03:59 +08:00
AGENTS.md refactor(middleware): wire AuthJWT + CasbinRBAC via .api middleware directive 2026-05-21 17:30:50 +08:00
Makefile test(e2e): 加 banner / e2e-list / k6 風格 user journey 2026-05-22 17:18:36 +08:00
README.md docs: 統整模組 README ↔ SDD 分工,砍重複內容 2026-05-22 17:18:08 +08:00
docker-compose.yml fix error msg 2026-05-20 17:32:22 +08:00
gateway.go refactor(middleware): wire AuthJWT + CasbinRBAC via .api middleware directive 2026-05-21 17:30:50 +08:00
go.mod feat(permission): add RBAC module with Casbin enforcement and policy reload 2026-05-21 16:47:35 +08:00
go.sum feat(permission): add RBAC module with Casbin enforcement and policy reload 2026-05-21 16:47:35 +08:00

README.md

Portal API Gateway (PGW)

基於 go-zero 的 API Gateway提供統一 HTTP JSON 回應、8 碼業務錯誤碼,以及由 .api 定義驅動的程式碼與 OpenAPI 3.0 文件生成。

功能概覽

  • REST APIgo-zero rest 服務,預設 http://0.0.0.0:8888
  • 統一回應格式:成功 / 失敗皆為 Status envelopecodemessagedataerror
  • 結構化錯誤8 碼 SSCCCDDDScope + Category + Detail可映射 HTTP / gRPC
  • 程式碼生成goctl + 自訂 handler 模板(自動 response.Write
  • API 文件go-doc 由同一套 .api 產出 OpenAPI 3.0 YAML

環境需求

工具 必要 說明
Go ≥ 1.26.1(見 go.mod
Make 執行 gen-api / gen-doc
goctl make gen-api 用;可 make tools 安裝
goimports 建議 make fmt 用;make tools 安裝
golangci-lint 建議 make lint 用;make tools 安裝v2.golangci.yml
jq 選用 README 範例 curl | jq

請確認 $(go env GOPATH)/bin 在 PATH,否則 go installgoctl 找不到。

快速開始(首次)

cd gateway

# 1. 工具goctl、goimports
make tools

# 2. 依賴
go mod download

# 3. 依 .api 生成程式碼可選repo 內通常已有)
make gen-api

# 4. 啟動
make run
# 或go run gateway.go -f etc/gateway.yaml

本機 Mongo + RedisNotification / Member OTP

需要持久化通知、異步重試或 member 驗證時:

make deps-up          # Mongo :27017、Redis :6379
make mongo-index      # 建立 notifications / notification_dlq 索引
make run-dev          # 使用 etc/gateway.dev.yaml

詳見 deploy/README.mdetc/README.md。選用 MailHogmake deps-up-smtp

產生 OpenAPI會先編譯 generate/doc-generate 內的 go-doc

make gen-doc   # 輸出 docs/openapi/gateway.yaml

健康檢查:

curl -s http://127.0.0.1:8888/api/v1/health | jq

預期回應:

{
  "code": 0,
  "message": "SUCCESS",
  "data": { "pong": "ok" }
}

常用指令

執行 makemake help 可看完整列表。

指令 說明
make tools 首次安裝 goctl、goimports
make gen-api gateway.api 生成 Go自訂 generate/goctl 模板)
make gen-doc 編譯 go-doc 並生成 docs/openapi/gateway.yaml
make test 執行測試
make fmt gofmt + goimports
make lint golangci-lint 靜態檢查(必須 0 issues
make lint-fix 自動修正可修的 lint / import 問題
make fix fmt + lint-fix + lint(提交前建議)
make check fix + testPR / AI 完成前必跑)
make run 啟動 Gateway無 DB
make deps-up Docker 啟動 Mongo + Redis
make run-dev 啟動 Gatewayetc/gateway.dev.yaml,需 Docker
make config-check 驗證 yaml 可載入
make mongo-index 建立 notification Mongo 索引

專案結構

gateway/
├── gateway.go                 # 程式入口
├── etc/gateway.yaml           # 服務設定(埠號等)
├── generate/
│   ├── api/                   # API 定義goctl + go-doc 共用)
│   ├── goctl/api/handler.tpl  # 自訂 handler 模板 → response.Write
│   └── doc-generate/          # go-doc.api → OpenAPI
├── internal/
│   ├── handler/               # HTTP handlergoctl 生成response.Write
│   ├── logic/                 # HTTP 編排types ↔ usecase DTO只回 data + error
│   ├── types/                 # goctl 生成的請求/回應型別
│   ├── response/              # 統一 JSON 回應封裝
│   ├── svc/                   # ServiceContext組裝各模組 UseCase
│   ├── config/                # go-zero RestConf 等服務設定
│   ├── library/errors/        # 全專案唯一結構化錯誤8 碼 SSCCCDDD
│   ├── library/validate/      # struct 驗證go-playground/validator + 翻譯)
│   ├── library/mongo/         # DocumentDB + Redis cachemongo-driver v2
│   ├── library/errlog/        # 可選 slog 日誌輔助
│   └── model/                 # 業務模型根目錄(見下方)
│       └── {module}/          # 例如 member/、order/
└── docs/
    ├── model.md               # model/{module} 內 entity / repository / usecase 細節
    └── openapi/               # gen-doc 輸出(預設 .gitignore

業務模型(internal/model/{module}/

業務領域程式碼一律放在 internal/model/ 底下,再依模組分子目錄(例如 internal/model/member/)。不使用 pkg/。HTTP 層handler / logic / types仍由 goctl 管理;持久化與業務規則在 model/{module}/ 內。

internal/model/
└── member/                  # 模組名稱member、order…
    ├── entity/              # MongoDB documentAccount、User…
    ├── enum/                # 列舉 / 值物件Platform、Status…
    ├── repository/          # Repository 介面 + 實作Mongo / cache
    ├── usecase/             # UseCase 介面、Request/Response DTO、實作
    ├── config/              # 模組用設定 struct不含 go-zero RestConf
    ├── errors.go            # 模組 sentinel如 ErrNotFound非第二套錯誤碼
    ├── const.go             # 模組常數
    ├── redis.go             # Redis key 命名與 helper
    └── mock/                # mockgen 產物,勿手改

依賴方向:

handler → logic → model/{module}/usecase介面
                      ↓
                model/{module}/repository介面
                      ↓
                entity / enum / MongoDB·Redis

entity、enum 不依賴 repository、usecase、logic
logic 不 import model/{module}/repository 或 model/{module}/entity

請求鏈:

HTTP Request
  → handlerresponse.Write
  → logictypes ↔ usecase DTO
  → model/{module}/usecase
  → model/{module}/repository
  → MongoDB / Redis

更細的 entity / repository / usecase 撰寫規則見 docs/model.md

更細的說明見各子目錄 README

開發約定

1. 新增 API 流程

  1. generate/api/ 新增或修改 .api(並在 gateway.api import
  2. make gen-api 生成程式碼( handler 會自動使用 response.Write;已存在檔案不會覆寫,需手動刪除後再生成或自行修改)。
  3. internal/logic/ 做 types 映射並呼叫模組 UseCase只回傳 dataerror
  4. make gen-doc 更新 OpenAPI 文件。
  5. go test ./...

2. Logic 與 Handler

// internal/logic/auth — Auth scope
var errb = errs.For(code.Auth)

// internal/logic/member — Member scope
var errb = errs.For(code.Member)

// internal/handler/... — 由模板生成parse/validate 錯誤用 Facade scoperesponse.RequestErrScope
data, err := l.Ping()
response.Write(r.Context(), w, data, err)

有 request 的 API 會自動包含 httpx.Parse;解析失敗會映射為 400 InputInvalidFormatFacade scope 10101000)。

3. HTTP JSON 格式

成功HTTP 200

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

失敗HTTP 依錯誤類別,如 404Member scope 範例)

{
  "code": 29301000,
  "message": "member not found",
  "error": {
    "biz_code": "29301000",
    "scope": 29,
    "category": 301,
    "detail": 0
  }
}

4. 錯誤處理(全專案單一型別)

對外 API 只使用 gateway/internal/library/errors*errs.Error8 碼 SSCCCDDD)。不要在模組內再維護第二套業務錯誤碼套件。

各層職責:

職責 回傳
repository 忠實反映基礎設施Mongo / Redis / driver *errs.ErrorDB*、ResInvalidMeasureID 等)+ WithCause;可預期「無資料」可回模組 errors.gosentinel
usecase 業務規則(狀態、權限、組合多 repo *errs.ErrorRes*、Auth*、Svc* 等sentinel 轉成對外語意;已是正確的 *errs.Error 可原樣往上傳
logic HTTP 輸入檢查、types 映射 使用該模組 scopecode.Auth / code.Membercross-module 錯誤原樣 return nil, err
handler 序列化 response.Write(內建 errs.FromError

模組頂層 sentinel 範例(internal/model/member/errors.gopackage member

var (
    ErrNotFound        = errors.New("member: not found")
    ErrInvalidObjectID = errors.New("member: invalid object id")
)

Repository 對照建議:

狀況 回傳
mongo.ErrNoDocuments member.ErrNotFound(由 usecase 轉 ResNotFound
ObjectID 格式錯 errb.ResInvalidMeasureID("account_id")
duplicate key errb.DBDuplicate(...)
連線 / 暫時不可用 errb.DBUnavailable(...).WithCause(err)
其他 driver 錯 errb.DBError(...).WithCause(err)

Usecase 範例Member scope

var errb = errs.For(code.Member)

acc, err := uc.Account.FindOne(ctx, id)
if err != nil {
    if errors.Is(err, member.ErrNotFound) {
        return nil, errb.ResNotFound("member", id).WithCause(err)
    }
    if e := errs.FromError(err); e != nil {
        return nil, err // DB* 等基礎設施錯誤原樣傳遞
    }
    return nil, errb.DBError("get member failed").WithCause(err)
}

Logic 只做輸入與映射,錯誤直接往上:

out, err := l.svcCtx.MemberUC.GetByID(l.ctx, &memberusecase.GetByIDRequest{ID: req.Id})
if err != nil {
    return nil, err
}

禁止: repository 回傳裸 fmt.Errorf 給上層logic 再包一層無語意錯誤logic 直接操作 Mongo / repository 實作。

5. Lint 與自動修正

設定檔:.golangci.ymlgolangci-lint v2standard + revive / gosec / errorlint 等)。

make lint-fix   # 自動修goimports、部分 linter auto-fix
make lint       # 僅檢查
make fix        # fmt + lint-fix + lint
make check      # fix + test建議每次改 Go 後執行)

無法自動修的須手改AI / 協作者應以 make check 通過為完成條件(見 .cursor/rules/golangci-lint.mdc)。

6. 請求驗證(library/validate

  • svc.ServiceContext.Validator:啟動時以 validate.NewWithDefaultEN() 建立,可傳入 validate.Option 註冊自訂 tag。
  • Handler 模板generate/goctl/api/handler.tpl)在 httpx.Parse 之後自動 ValidateAll(&req),失敗經 WrapRequestError400 InputInvalidFormatLogic 通常不必再驗證。
  • 自訂規則放在 internal/library/validate/custom/(依業務新增),透過 NewWithDefaultEN(custom.YourOption) 註冊。
  • validate.ValidationErrorsfield / message;勿與 internal/library/errors/validate.go(驗證 8 碼組成)混淆。
if err := l.svcCtx.Validator.ValidateAll(req); err != nil {
    return nil, err // handler: response.WrapRequestError(err)
}

7. 新增業務模組檢查清單

  1. 建立 internal/model/{module}/,依上方目錄放置 entityenumrepositoryusecase
  2. internal/svc/service_context.go 組裝 repository → usecase掛上介面欄位。
  3. generate/api/ 定義路由,make gen-api
  4. internal/logic/ 實作types ↔ usecase DTO呼叫 svcCtx.{Module}UC
  5. make gen-docgo test ./...;介面變更後執行 mockgen 更新 internal/model/{module}/mock/

完整步驟見 docs/model.md 第 11 節。

8. 錯誤碼 API 速查

import (
    errs "gateway/internal/library/errors"
    "gateway/internal/library/errors/code"
)

// logic / usecase依模組選 scope
var authErr = errs.For(code.Auth)    // 28301000 = ResNotFound
var memberErr = errs.For(code.Member) // 29301000 = ResNotFound

return nil, memberErr.ResNotFound("member", id)
return nil, authErr.InputMissingRequired("email").WithCause(err)

Category 與 HTTP 對照見 internal/library/errors/README.md

9. API 文件(@ 註解)

.api 中:

  • returns (FooVO):只描述 data(給 goctl / Logic
  • /* @respdoc-200 (FooOKStatus) ... */:描述實際 HTTP body給 OpenAPI
  • @doc(summary, description):單一接口說明。

範例見 generate/api/normal.api

10. OpenAPI 產物與 Git

預設 .gitignore 會忽略 docs/openapi/*.yaml。若需提交給前端,請在 .gitignore 註解相關規則後執行 make gen-doc 並加入版本控制。

設定

主設定檔:etc/gateway.yaml

Name: gateway
Host: 0.0.0.0
Port: 8888

本地覆寫可新增 etc/gateway-local.yaml(已列入 .gitignore,勿提交機密)。

測試

go test ./...

授權

內部專案,授權依組織規範為準。