2026-05-19 12:56:32 +00:00
# Model 分層規範
本文件定義 Gateway 專案中 **業務模型** ( `internal/model/{module}/`)的目錄結構與撰寫約定,參考 Clean Architecture 分層。
2026-05-20 07:01:08 +00:00
> Gateway 的 HTTP 層( handler / logic / types) 仍由 goctl 生成;本規範適用於 **`internal/model/{module}/`**(例如 `internal/model/member/`、`internal/model/notification/`)。**不使用 `pkg/`。**
2026-05-19 12:56:32 +00:00
專案總覽與錯誤分層見 [README.md ](../README.md )。
2026-05-20 07:01:08 +00:00
## 目錄結構(`domain/` + 外層實作)
2026-05-19 12:56:32 +00:00
2026-05-20 07:01:08 +00:00
**新模組與重構中的模組**( `notification`、`member`、`permission` 等)採 **domain 分包** :契約與領域型別在 `domain/` , Mongo / Redis / Provider / embed 等實作在模組根下對應目錄。對齊 `app-cloudep-notification-service/pkg/` 的 `domain/` 慣例。
路徑前綴:`internal/model/{module}/`
2026-05-19 12:56:32 +00:00
```
internal/model/
2026-05-20 07:01:08 +00:00
└── {module}/ # 例: notification、member、permission
2026-05-20 13:03:59 +00:00
├── domain/ # 純領域: 介面、實體、列舉、DTO、模組級定義
2026-05-20 07:01:08 +00:00
│ ├── entity/ # Mongo document 結構 + CollectionName()
│ ├── enum/ # Channel、Status、Platform…
│ ├── repository/ # Repository / Cache 介面 only
│ ├── usecase/ # UseCase 介面 + Request/Response DTO
2026-05-20 13:03:59 +00:00
│ ├── template/ # 可選:模板 Spec、Registry、Renderer 介面( notification)
│ ├── errors.go # 模組 sentinel( package domain)
│ ├── const.go # BSON 欄位名、模組常數( package domain)
│ └── redis.go # Redis key 命名( package domain)
2026-05-20 07:01:08 +00:00
├── repository/ # domain/repository 的 Mongo / Redis / memory 實作
├── usecase/ # domain/usecase 的實作 + factory 組裝
├── template/ # 可選: go:embed、DefaultRegistry、Renderer 實作
├── provider/ # 可選:僅本模組用的 Sender( email/sms) , 不放 library/
2026-05-20 13:03:59 +00:00
├── totp/、xxx/ # 可選:模組專屬純函式 library( 不放 internal/library/)
2026-05-20 07:01:08 +00:00
├── config/ # 模組設定 struct( 嵌入 gateway Config)
└── mock/ # mockgen( 路徑對應 domain/)
2026-05-19 12:56:32 +00:00
├── repository/
└── usecase/
```
2026-05-20 13:03:59 +00:00
> **定義類( errors / const / redis key) 統一放 `domain/`**: caller 端以 `member "gateway/internal/model/{module}/domain"` 取用,引用形式仍為 `member.ErrXxx` / `member.Get…RedisKey`,但這些 sentinel 與 key helper 都在 `package domain`,與 `domain/entity` 等子套件平行。
>
> **`internal/library/` 只放跨模組真正共用的東西**(如 `library/errors`、`library/mongo`、`library/redis`、`library/crypto`)。僅某個模組會用的純函式 / 演算法(例如 member 的 RFC 6238 TOTP helper) 應落在該模組底下, 例如 `internal/model/member/totp/`,避免污染 library 命名空間。
2026-05-20 07:01:08 +00:00
**參考實作:** [`internal/model/notification/` ](../internal/model/notification/ )( N0– N5 核心已完成;流程圖與設定見 [**notification README** ](../internal/model/notification/README.md ))。
| 層 | 路徑 | 內容 |
|----|------|------|
| 領域契約 | `domain/entity` 、`domain/enum` | 實體、值物件 |
| 領域契約 | `domain/repository` | `XxxRepository` 、`IdempotencyCache` 等 **interface** |
| 領域契約 | `domain/usecase` | `XxxUseCase` 、`SendRequest` 等 **interface + DTO** |
| 基礎設施 | `repository/` | `NewXxxRepository` 、index migration |
| 應用服務 | `usecase/` | `MustXxxUseCase` 、`NewXxxUseCaseFromParam` |
| 模組專用整合 | `provider/` 、`template/` | 不進 `internal/library/` |
2026-05-19 12:56:32 +00:00
### 依賴方向
```
2026-05-20 07:01:08 +00:00
domain/entity、domain/enum → 僅標準庫 / 列舉底層型別
domain/repository、domain/usecase、domain/template
→ domain/entity、domain/enum( 彼此不 import 實作層)
usecase( 實作) → domain/usecase( 實作介面)
→ domain/repository( 介面)
→ repository/、provider/、template/(具體型別)
repository( 實作) → domain/repository( 實作介面)
→ domain/entity
internal/logic、其他 model → domain/usecase 介面 only
(不 import repository 實作、entity、provider)
```
**Import 範例( notification) : **
```go
import (
domusecase "gateway/internal/model/notification/domain/usecase"
"gateway/internal/model/notification/usecase"
)
// 業務模組(如 member) 只依賴介面
var _ domusecase.NotifierUseCase = (*usecase.notifierUseCase)(nil)
// ServiceContext 組裝實作
notifier, err := usecase.NewNotifierUseCaseFromParam(usecase.FactoryParam{...})
```
### 基礎設施連線( Mongo / Redis)
2026-05-19 12:56:32 +00:00
2026-05-20 07:01:08 +00:00
**一個 Pod( 一個進程) 內, 連線在 `internal/svc/service_context.go` 建立一次,再注入各 module。**
2026-05-19 12:56:32 +00:00
2026-05-20 07:01:08 +00:00
| 資源 | 建立位置 | 共用方式 | 模組內 |
|------|----------|----------|--------|
| Mongo | `library/mongo` + go-zero `mon` | 同一 URI → **一個 `*mongo.Client` pool** ( `mon` 的 `clientManager` ) | 每 collection 一個 `DocumentDB` / repository, **不**各自 `Connect` |
| Redis | `library/redis.NewClient` + go-zero `redis.MustNewRedis` | 同一 `Addr` → **一個 connection pool** | 只收 `*redis.Client` , **禁止**在 factory 內 `go-redis.NewClient` |
2026-05-19 12:56:32 +00:00
2026-05-20 07:01:08 +00:00
```go
// internal/svc/service_context.go( 示意)
rds, _ := redislib.NewClient(c.Redis) // 全進程共用
notifier, _ := notifusecase.NewNotifierUseCaseFromParam(notifusecase.FactoryParam{
MongoConf: & c.Mongo,
Redis: rds, // 注入,非 RedisConf
Config: c.Notification,
})
2026-05-19 12:56:32 +00:00
```
2026-05-20 07:01:08 +00:00
- `ServiceContext.Redis` 為 `*library/redis.Client` , `Host` 空則 `nil` (模組可 fallback memory) 。
- 之後 **member / permission** 的 factory、`MustXxxUseCase` **必須**接受 `*redislib.Client` ,不得在 module 內新建 Redis 連線。
- 模組 `redis.go` 只定義 **key 前綴** ,不建立 client。
### 與舊版扁平目錄的關係
早期 scaffold 可能仍為 `member/entity` (無 `domain/` )。**新增 `permission` 、重構 `member` 時請遷移到上表結構**,與 `notification` 一致。遷移步驟:先搬 `entity` /`enum` → `domain/` ,再搬 repository/usecase **介面** → `domain/` ,最後保留外層實作並修正 import。
2026-05-19 12:56:32 +00:00
2026-05-20 07:01:08 +00:00
## 1. Entity( `domain/entity/`)
每個 MongoDB collection 對應一個 struct, 放在 `internal/model/{module}/domain/entity/` 。
2026-05-19 12:56:32 +00:00
**規則:**
- 檔名使用 snake_case 對應 collection 語意,如 `account.go` 、`user.go`。
- struct 名稱使用 PascalCase 單數,如 `Account` 、`User`。
- 必須實作 `CollectionName() string` ,回傳 MongoDB collection 名稱。
- 欄位 tag: `bson` 必填;對外 JSON 序列化才加 `json` 。
2026-05-20 07:01:08 +00:00
- 主鍵使用 MongoDB driver **v2** 的 `bson.ObjectID` ( `go.mongodb.org/mongo-driver/v2/bson`) , tag 為 `` `bson:"_id,omitempty" json:"id,omitempty"` ``。舊模組若仍為 `primitive.ObjectID` ,遷移時一併改為 v2。
2026-05-19 12:56:32 +00:00
- 時間戳記統一用 `*int64` ,欄位名 `CreateAt` / `UpdateAt` ,值為 UTC nanoseconds。
- 可選欄位用指標型別(`*string`、`*int64`)。
2026-05-20 07:01:08 +00:00
- 領域列舉引用 `domain/enum/` 下的型別,不在 entity 內重複定義。
2026-05-19 12:56:32 +00:00
**範例:**
```go
package entity
import (
2026-05-20 07:01:08 +00:00
"gateway/internal/model/member/domain/enum"
"go.mongodb.org/mongo-driver/v2/bson"
2026-05-19 12:56:32 +00:00
)
type Account struct {
2026-05-20 07:01:08 +00:00
ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
2026-05-19 12:56:32 +00:00
LoginID string `bson:"login_id"`
Token string `bson:"token"`
Platform enum.Platform `bson:"platform"`
UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"`
CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"`
}
func (a *Account) CollectionName() string {
return "account"
}
```
2026-05-20 07:01:08 +00:00
## 2. 值物件 / 列舉(`domain/enum/`)
2026-05-19 12:56:32 +00:00
2026-05-20 07:01:08 +00:00
業務列舉、狀態碼等放在 `internal/model/{module}/domain/enum/` 。
2026-05-19 12:56:32 +00:00
**規則:**
- 以具名型別包裝底層型別(`type Status int32`)。
- 常數集中定義,附中文或英文註解說明語意。
- 提供轉換方法:`ToInt32()`、`ToInt64()`、`ToString()`、`CodeToString()` 等。
- 字串 ↔ 列舉映射用 private map + public getter, 未知值回傳零值或 sentinel。
**範例:**
```go
package enum
type Platform int8
const (
Digimon Platform = 1 + iota
Google
Line
Apple
)
func (p Platform) ToInt64() int64 { return int64(p) }
func (p Platform) ToString() string { /* map lookup */ }
```
同一檔案或目錄下可放 `*_test.go` 驗證轉換邏輯。
2026-05-20 07:01:08 +00:00
## 3. Repository 介面(`domain/repository/`)
2026-05-19 12:56:32 +00:00
**規則:**
2026-05-20 07:01:08 +00:00
- 一個 entity 一個 `XxxRepository` interface, 檔案放在 `domain/repository/` 。
2026-05-19 12:56:32 +00:00
- 方法第一個參數固定為 `context.Context` 。
2026-05-20 07:01:08 +00:00
- 參數 / 回傳值使用 `domain/entity` 型別,不暴露 driver 細節(除 index migration 等必要場景)。
2026-05-19 12:56:32 +00:00
- Index migration 以獨立 interface 嵌入,命名 `{Entity}IndexUP` ,方法名含版本號,如 `Index20241226001UP` 。
2026-05-20 07:01:08 +00:00
- **此目錄僅介面**:不含 `NewXxxRepository` 、不含 `mongo` / `redis` import。
2026-05-19 12:56:32 +00:00
**範例:**
```go
package repository
import (
"context"
2026-05-20 07:01:08 +00:00
"gateway/internal/model/member/domain/entity"
2026-05-19 12:56:32 +00:00
)
type AccountRepository interface {
Insert(ctx context.Context, data *entity.Account) error
FindOne(ctx context.Context, id string) (*entity.Account, error)
2026-05-20 07:01:08 +00:00
Update(ctx context.Context, data *entity.Account) error
2026-05-19 12:56:32 +00:00
Delete(ctx context.Context, id string) (int64, error)
}
```
2026-05-20 07:01:08 +00:00
輔助介面(冪等、配額等)亦放在 `domain/repository/` ,由外層 `repository/redis_store.go` 等實作。
2026-05-19 12:56:32 +00:00
## 4. Repository 實作(`repository/`)
**規則:**
2026-05-20 07:01:08 +00:00
- struct 名稱 `{entity}Repository` (小寫)或 `{Entity}Repository` ,建構子 `New{Entity}Repository(param {Entity}RepositoryParam)` , **回傳型別為 `domain/repository` 的 interface**。
- Param struct 集中注入 `*mongo.Conf` 等;實作 import `domain/entity` 、`domain/repository`。
- 建構時以 `domain/entity` 的 `CollectionName()` 初始化 DocumentDB; 失敗時 `panic` (啟動期錯誤)。
- CRUD 透過 `gateway/internal/library/mongo` 的 DocumentDB helper, 搭配模組 `redis.go` 的 key helper。
2026-05-19 12:56:32 +00:00
- `Insert` : ID 為 zero 時自動產生 ObjectID 並寫入 `CreateAt` / `UpdateAt` 。
- `Update` :自動更新 `UpdateAt` 。
- `FindOne` / `Delete` :無效 ObjectID → `*errs.Error` ( `ResInvalidMeasureID`)或模組 `ErrInvalidObjectID` ;查無資料 → 模組 `ErrNotFound` (見第 7 節錯誤)。
- 整合測試放在同 package 的 `*_test.go` ,可用 testcontainer + miniredis。
**範例:**
```go
2026-05-20 07:01:08 +00:00
import (
domentity "gateway/internal/model/member/domain/entity"
domrepo "gateway/internal/model/member/domain/repository"
)
2026-05-19 12:56:32 +00:00
type accountRepository struct {
2026-05-20 07:01:08 +00:00
db mongo.DocumentDBUseCase
2026-05-19 12:56:32 +00:00
}
2026-05-20 07:01:08 +00:00
func NewAccountRepository(param AccountRepositoryParam) domrepo.AccountRepository {
e := domentity.Account{}
documentDB, err := mongo.NewDocumentDB(param.Conf, e.CollectionName())
2026-05-19 12:56:32 +00:00
if err != nil {
panic(err)
}
2026-05-20 07:01:08 +00:00
return & accountRepository{db: documentDB}
2026-05-19 12:56:32 +00:00
}
```
2026-05-20 07:01:08 +00:00
## 5. UseCase 介面與 DTO( `domain/usecase/`)
2026-05-19 12:56:32 +00:00
**規則:**
2026-05-20 07:01:08 +00:00
- 業務入口定義為 interface, 如 `NotifierUseCase` 、`AccountUseCase`,放在 `domain/usecase/` 。
- Request / Response struct 與 interface **同 package** ,命名 `{Action}Request` 、`NotificationDTO`。
- DTO 只含 `json` tag( 若需序列化) , 不含 bson tag。
2026-05-19 12:56:32 +00:00
- 更新類 Request 的可選欄位用指標,以便區分「未傳入」與「傳入零值」。
**範例:**
```go
package usecase
2026-05-20 07:01:08 +00:00
import (
"context"
"gateway/internal/model/notification/domain/enum"
)
2026-05-19 12:56:32 +00:00
2026-05-20 07:01:08 +00:00
type NotifierUseCase interface {
Send(ctx context.Context, req *SendRequest) (*NotificationDTO, error)
Enqueue(ctx context.Context, req *SendRequest) (*NotificationDTO, error)
Get(ctx context.Context, tenantID, id string) (*NotificationDTO, error)
2026-05-19 12:56:32 +00:00
}
2026-05-20 07:01:08 +00:00
type SendRequest struct {
TenantID string
Channel enum.Channel
// ...
2026-05-19 12:56:32 +00:00
}
```
## 6. UseCase 實作(`usecase/`)
**規則:**
2026-05-20 07:01:08 +00:00
- 實作 struct 如 `notifierUseCase` 、`memberUseCase`,放在模組根 `usecase/` 。
- 以 `{Name}UseCaseParam` 注入 `domain/repository` 介面、provider、renderer、`config`。
- 建構子 `Must{Name}UseCase(param) domusecase.XxxUseCase` ,回傳 **domain** interface。
- 跨模組組裝可用 `New{Name}UseCaseFromParam` / `factory.go` (見 `notification/usecase/factory.go` )。
- 方法簽名與 `domain/usecase` interface 一致;內部組裝 `domain/entity` ,呼叫 repository 介面。
2026-05-19 12:56:32 +00:00
- 錯誤一律回傳 `gateway/internal/library/errors` 的 `*errs.Error` (見第 7 節)。
- 可測性:將難 mock 的純函式抽成 package 級變數(如 `var HashPasswordFunc = HashPassword` )。
2026-05-20 13:03:59 +00:00
### 6.1 UseCase 互不呼叫( atomic-only)
> **強制規則**: UseCase 是 **atomic primitive**, **禁止**在 usecase 內部呼叫其他 usecase( 不論是同模組或跨模組) 。
>
> - usecase struct 的依賴**只能**是 `domain/repository` 介面、`provider/`、`template/`、library helper、`config`。
> - **不可**在 `XxxUseCaseParam` 出現另一個 `domain/usecase.XxxUseCase` 欄位。
> - 需要把多個 atomic 串成一個業務流程( 例如「OTP.Generate → Notifier.Send → Profile.SetVerified」) 時, **編排在 `internal/logic/`**; logic handler 持有多個 usecase interface 並負責順序、補償、rate-limit、step-up 守門。
> - CLI / driver( 如 `cmd/notify-test/`)扮演 logic 同等角色:直接組 atomic, 不應該被包成 composite usecase。
>
> 這條規則優先於 [identity-member-design.md §5.2](./identity-member-design.md) 提到的 Composite UseCase; 該節保留為「**邏輯流的描述**」,不代表 `domain/usecase` 會出現 composite interface。
2026-05-19 12:56:32 +00:00
**範例:**
```go
2026-05-20 07:01:08 +00:00
import (
domrepo "gateway/internal/model/member/domain/repository"
domusecase "gateway/internal/model/member/domain/usecase"
)
2026-05-19 12:56:32 +00:00
type MemberUseCaseParam struct {
2026-05-20 07:01:08 +00:00
Account domrepo.AccountRepository
User domrepo.UserRepository
2026-05-19 12:56:32 +00:00
Config config.Config
}
2026-05-20 07:01:08 +00:00
type memberUseCase struct {
2026-05-19 12:56:32 +00:00
MemberUseCaseParam
}
2026-05-20 07:01:08 +00:00
func MustMemberUseCase(param MemberUseCaseParam) domusecase.AccountUseCase {
return & memberUseCase{param}
2026-05-19 12:56:32 +00:00
}
```
## 7. 錯誤處理
2026-05-20 13:03:59 +00:00
全專案對外只使用 `gateway/internal/library/errors` ( `var errb = errs.For(code.Facade)`)。`domain/errors.go` **只放 sentinel** ,不另建 8 碼常數表。
2026-05-19 12:56:32 +00:00
2026-05-20 13:03:59 +00:00
### 7.1 模組 sentinel( `domain/errors.go`)
2026-05-19 12:56:32 +00:00
```go
2026-05-20 13:03:59 +00:00
package domain
2026-05-19 12:56:32 +00:00
2026-05-20 07:01:08 +00:00
import "fmt"
2026-05-19 12:56:32 +00:00
var (
2026-05-20 07:01:08 +00:00
ErrNotFound = fmt.Errorf("member: not found")
ErrInvalidObjectID = fmt.Errorf("member: invalid object id")
2026-05-19 12:56:32 +00:00
)
```
2026-05-20 13:03:59 +00:00
( 專案慣例: sentinel 一律以 `fmt.Errorf` 定義,便於 `%w` 包裝。caller 端 `member "gateway/internal/model/member/domain"` 後即可 `member.ErrNotFound` 。)
2026-05-20 07:01:08 +00:00
2026-05-19 12:56:32 +00:00
### 7.2 Repository
| 狀況 | 回傳 |
|------|------|
| `mongo.ErrNoDocuments` | `ErrNotFound` (由 usecase 轉 `ResNotFound` ) |
| ObjectID 格式錯 | `errb.ResInvalidMeasureID(...)` 或 `ErrInvalidObjectID` |
| duplicate key | `errb.DBDuplicate(...)` |
| 連線 / 暫時不可用 | `errb.DBUnavailable(...).WithCause(err)` |
| 其他 driver 錯 | `errb.DBError(...).WithCause(err)` |
基礎設施錯誤須在 repository 邊界對照完成,並以 `WithCause` 保留原始 err 供 `errlog` 使用。
### 7.3 UseCase
- 業務規則:`errb.ResAlreadyExist`、`errb.ResInvalidState`、`errb.AuthForbidden` 等。
- `errors.Is(err, member.ErrNotFound)` → `errb.ResNotFound("member", id).WithCause(err)` 。
- 已是 `*errs.Error` 且語意正確(如 `DBDuplicate` )→ 原樣 `return err` 。
### 7.4 Logic / Handler
- Logic: 僅 `InputMissingRequired` 等 HTTP 輸入錯誤; usecase 回傳的 err **不二次包裝** 。
- Handler: `response.Write`,由 `errs.FromError` 決定 HTTP 狀態與 body。
## 8. 模組共用檔案
| 檔案 | 用途 |
|------|------|
2026-05-20 13:03:59 +00:00
| `domain/errors.go` | 模組 sentinel( `ErrNotFound` 等,`package domain`) |
| `domain/const.go` | 模組字面常數(`package domain`) |
| `domain/redis.go` | Redis key 型別、`With()` 組合、`GetXxxRedisKey()` helper( `package domain`) |
2026-05-19 12:56:32 +00:00
| `config/config.go` | UseCase 需要的設定 struct( 不含 go-zero RestConf) |
Redis key 統一帶業務 prefix, 避免跨服務衝突:
```go
2026-05-20 13:03:59 +00:00
package domain
2026-05-19 12:56:32 +00:00
type RedisKey string
const AccountRedisKey RedisKey = "member:account"
func (key RedisKey) With(s ...string) RedisKey { /* join with ":" */ }
func GetAccountRedisKey(id string) string {
2026-05-20 13:03:59 +00:00
return AccountRedisKey.With(id).String()
2026-05-19 12:56:32 +00:00
}
```
2026-05-20 13:03:59 +00:00
Caller 端:
```go
import (
member "gateway/internal/model/member/domain"
)
// 使用: member.GetAccountRedisKey(id)、member.ErrNotFound
```
2026-05-20 07:01:08 +00:00
## 9. Mock( `mock/` + gomock)
**方案 A( 本專案採用) : **
| 測試對象 | 做法 |
|----------|------|
| `domain/repository` 契約行為( 冪等重複、FindByIdempotency) | `repository/*_test.go` + **in-memory 實作** (如 `MemoryNotificationRepository` ) |
| `usecase` 編排、分支、配額 | **gomock** `domain/repository` 介面,`EXPECT` / `DoAndReturn` |
| `logic` 呼叫其他模組 | gomock `domain/usecase` |
| Provider Chain | 保留 `provider/*/mock_sender.go` ( 記錄呼叫、failover) |
2026-05-19 12:56:32 +00:00
2026-05-20 07:01:08 +00:00
介面變更後執行:
2026-05-19 12:56:32 +00:00
```bash
2026-05-20 07:01:08 +00:00
make gen-mock
```
`domain/repository/generate.go` 範例:
```go
//go:generate go run go.uber.org/mock/mockgen@latest -typed -destination=../../mock/repository/repository_mock.go -package=mocknotifrepo gateway/internal/model/{module}/domain/repository NotificationRepository,IdempotencyCache,QuotaCounter
2026-05-19 12:56:32 +00:00
```
2026-05-20 07:01:08 +00:00
- 產物在 `mock/repository/` 、`mock/usecase/`, **不要手改**。
- **勿**在 usecase test 手寫 70 行 fake repo; 有狀態的儲存邏輯放在 `repository/memory_*.go` 並由 **repository 層測試** 覆蓋。
2026-05-19 12:56:32 +00:00
## 10. 命名對照表
| 概念 | 命名 |
|------|------|
| Entity struct | `Account` 、`User` |
| Collection 方法 | `CollectionName()` |
| Repository 介面 | `AccountRepository` |
| Repository 實作 struct | `accountRepository` 或 `AccountRepository` |
| Repository 建構子 | `NewAccountRepository` |
| Repository 參數 | `AccountRepositoryParam` |
| UseCase 介面 | `AccountUseCase` |
| UseCase 實作 | `MemberUseCase` |
| UseCase 建構子 | `MustMemberUseCase` |
| 請求 DTO | `CreateLoginUserRequest` |
| 回應 DTO | `GetUIDByAccountResponse` |
| Index migration | `Index{YYYYMMDD}{seq}UP` |
| 測試檔 | 與被測檔案同目錄,`*_test.go` |
## 11. 新增模組 / Model 檢查清單
2026-05-20 07:01:08 +00:00
1. 建立 `internal/model/{module}/domain/{entity,enum,repository,usecase}/` 與外層 `repository/` 、`usecase/`。
2. 在 `domain/entity/` 新增 struct + `CollectionName()` 。
3. 在 `domain/enum/` 定義值物件(若有)。
4. 在 `domain/repository/` 宣告 interface; 於 `repository/` 實作 CRUD + index + `*_test.go` 。
5. 在 `domain/usecase/` 宣告 interface + DTO; 於 `usecase/` 實作 + 單元測試(可 fake `domain/repository` )。
6. 模組專用整合放 `provider/` 、`template/`(勿放入 `library/` )。
7. `errors.go` 、`const.go`、`redis.go`、`config/` 按需補齊。
8. 執行 `make gen-mock` ( `go:generate` 在 `domain/repository/generate.go` 等)。
9. 在 `internal/config/config.go` 嵌入模組 `config` ; `etc/gateway.yaml` 加區塊。
10. 在 `internal/svc/service_context.go` 建立 **共用** `*redislib.Client` ,再注入 `NewXxxUseCaseFromParam` ( Mongo / Redis 未配置時對應欄位可為 `nil` )。
11. 在 `generate/api/` 定義路由,`make gen-api`; `internal/logic/` **只** import `domain/usecase` 。
12. `make gen-doc` 、`go test ./...`。
**Notification 模組進度(參考):** N0– N5 核心 ✅(含 `RetryWorker` 、`AdminNotifierUseCase`);文件見 [notification README ](../internal/model/notification/README.md )。待做: HTTP admin API( goctl) 。
2026-05-20 13:03:59 +00:00
**Member 模組進度( P3.5) : ** atomic primitives `OTPUseCase` ( Generate/Verify/Invalidate) + `TOTPUseCase` ( enroll/verify/backup/disable) + `VerifyRateStore` + `ProfileRepository` ✅。**usecase 之間不互相呼叫**:「業務 email/phone 驗證 = OTP.Generate → Notifier.Send → Profile.SetXxxVerified」的編排由 **logic 層**負責(尚未實作);參考實作見 `cmd/notify-test/main.go::startMemberVerify` ( driver 等同 logic 角色) 。後續: HTTP API( goctl) + logic 層編排(含 rate-limit + step-up 守門)。
2026-05-19 12:56:32 +00:00
## 12. 與 Gateway HTTP 層的關係
```
HTTP Request
↓
handler( goctl 生成)→ response.Write
↓
logic( goctl 生成框架,手寫映射)
↓ 轉換 types ↔ usecase DTO
2026-05-20 07:01:08 +00:00
usecase 介面( internal/model/{module}/domain/usecase)
↓
usecase 實作( internal/model/{module}/usecase)
2026-05-19 12:56:32 +00:00
↓
2026-05-20 07:01:08 +00:00
repository 實作( internal/model/{module}/repository)
↓ 實作 domain/repository 介面
2026-05-19 12:56:32 +00:00
↓
MongoDB / Redis
```
- `internal/types` : HTTP 請求 / 回應型別,由 `.api` 生成。
2026-05-20 07:01:08 +00:00
- `internal/model/{module}/domain/usecase` DTO: 業務層資料結構, logic 負責與 `types` 映射。
2026-05-19 12:56:32 +00:00
- 錯誤自 usecase 以 `*errs.Error` 往上冒泡; logic 原樣傳遞, handler 經 `response.Write` 輸出 8 碼 JSON。