我# Portal API Gateway (PGW)

基於 [go-zero](https://github.com/zeromicro/go-zero) 的 API Gateway，提供統一 HTTP JSON 回應、8 碼業務錯誤碼，以及由 `.api` 定義驅動的程式碼與 OpenAPI 3.0 文件生成。

## 功能概覽

- **REST API**：go-zero `rest` 服務，預設 `http://0.0.0.0:8888`
- **統一回應格式**：成功 / 失敗皆為 `Status` envelope（`code`、`message`、`data`、`error`）
- **結構化錯誤**：8 碼 `SSCCCDDD`（Scope + 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](https://go-zero.dev/docs/tasks/installation/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 install` 的 `goctl` 找不到。

## 快速開始（首次）

```bash
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 + Redis（Notification / Member OTP）

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

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

詳見 [deploy/README.md](deploy/README.md)、[etc/README.md](etc/README.md)。選用 MailHog：`make deps-up-smtp`。

產生 OpenAPI（會先編譯 `generate/doc-generate` 內的 go-doc）：

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

健康檢查：

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

預期回應：

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

## 常用指令

執行 `make` 或 `make 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` + `test`（PR / AI 完成前必跑） |
| `make run` | 啟動 Gateway（無 DB） |
| `make deps-up` | Docker 啟動 Mongo + Redis |
| `make run-dev` | 啟動 Gateway（`etc/gateway.dev.yaml`，需 Docker） |
| `make config-check` | 驗證 yaml 可載入 |
| `make mongo-index` | 建立 notification Mongo 索引 |
| `make dev-up` | **一鍵**本機全套（Docker + ZITADEL + MailHog + Gateway） |
| `make frontend-dev` | 啟動 React 前台 + 管理後台（`frontend/`，:5173） |
| `make dev-down` | 關閉 `dev-up` 啟動的一切 |

## 專案結構

```
gateway/
├── frontend/                  # Vite + React 使用者前台與管理後台
├── 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 handler（goctl 生成，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 cache（mongo-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 document（Account、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
  → handler（response.Write）
  → logic（types ↔ usecase DTO）
  → model/{module}/usecase
  → model/{module}/repository
  → MongoDB / Redis
```

更細的 entity / repository / usecase 撰寫規則見 [docs/model.md](docs/model.md)。

更細的說明見各子目錄 README：

- [`generate/api/README.md`](generate/api/README.md) — `.api`、`@respdoc`、middleware 約定
- [`internal/response/README.md`](internal/response/README.md) — Handler / Logic 分工
- [`internal/library/errors/README.md`](internal/library/errors/README.md) — 錯誤碼與 HTTP 對照
- [`internal/library/mongo/README.md`](internal/library/mongo/README.md) — MongoDB / Redis cache 流程
- [`internal/library/redis/README.md`](internal/library/redis/README.md) — Redis 連線共用
- [`docs/model.md`](docs/model.md) — `internal/model/{module}` 分層規範
- [`docs/identity-member-design.md`](docs/identity-member-design.md) — Identity / Member / Permission 跨模組架構（ZITADEL、LDAP、SCIM、B2B 自定義權限）
- [`docs/auth-unified-registration.md`](docs/auth-unified-registration.md) — 統一註冊／登入完整時序（Email / Social / JWT）
- [`docs/e2e-testing.md`](docs/e2e-testing.md) — E2E 測試 + 一鍵 `make e2e-full`
- [`docs/notification-testing.md`](docs/notification-testing.md) — Notification 本機測試
- 模組 README + SDD：
  - [`auth`](internal/model/auth/README.md) ／ [SDD](internal/model/auth/SDD.md)
  - [`member`](internal/model/member/README.md) ／ [SDD](internal/model/member/SDD.md)
  - [`permission`](internal/model/permission/README.md) ／ [SDD](internal/model/permission/SDD.md)
  - [`notification`](internal/model/notification/README.md) ／ [SDD](internal/model/notification/SDD.md)
  - README = 開發速查；SDD = 規格書（Data Dictionary、API 端點欄位）

## 開發約定

### 1. 新增 API 流程

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

### 2. Logic 與 Handler

```go
// 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 scope（response.RequestErrScope）
data, err := l.Ping()
response.Write(r.Context(), w, data, err)
```

有 request 的 API 會自動包含 `httpx.Parse`；解析失敗會映射為 **400** `InputInvalidFormat`（**Facade scope `10101000`**）。

### 3. HTTP JSON 格式

**成功（HTTP 200）**

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

**失敗（HTTP 依錯誤類別，如 404；Member scope 範例）**

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

### 4. 錯誤處理（全專案單一型別）

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

各層職責：

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

模組頂層 sentinel 範例（`internal/model/member/errors.go`，`package member`）：

```go
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）：

```go
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 只做輸入與映射，錯誤直接往上：

```go
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.yml`（golangci-lint v2，`standard` + revive / gosec / errorlint 等）。

```bash
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)`，失敗經 `WrapRequestError` → **400** `InputInvalidFormat`；Logic 通常不必再驗證。
- 自訂規則放在 `internal/library/validate/custom/`（依業務新增），透過 `NewWithDefaultEN(custom.YourOption)` 註冊。
- `validate.ValidationErrors` 含 `field` / `message`；勿與 `internal/library/errors/validate.go`（驗證 8 碼組成）混淆。

```go
if err := l.svcCtx.Validator.ValidateAll(req); err != nil {
    return nil, err // handler: response.WrapRequestError(err)
}
```

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

1. 建立 `internal/model/{module}/`，依上方目錄放置 `entity`、`enum`、`repository`、`usecase`。
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-doc`、`go test ./...`；介面變更後執行 mockgen 更新 `internal/model/{module}/mock/`。

完整步驟見 [docs/model.md](docs/model.md) 第 11 節。

### 8. 錯誤碼 API 速查

```go
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](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`

```yaml
Name: gateway
Host: 0.0.0.0
Port: 8888
```

本地覆寫可新增 `etc/gateway-local.yaml`（已列入 `.gitignore`，勿提交機密）。

## 測試

```bash
go test ./...
```

## 授權

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