template-monorepo/docs/model.md

371 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Model 分層規範
本文件定義 Gateway 專案中 **業務模型**`internal/model/{module}/`)的目錄結構與撰寫約定,參考 Clean Architecture 分層。
> Gateway 的 HTTP 層handler / logic / types仍由 goctl 生成;本規範適用於 **`internal/model/{module}/`**(例如 `internal/model/member/`——entity、repository、usecase 等。**不使用 `pkg/`。**
專案總覽與錯誤分層見 [README.md](../README.md)。
## 目錄結構
以下以 `member` 模組為例,路徑前綴為 `internal/model/member/`
```
internal/model/
└── member/
├── entity/ # 持久化資料模型MongoDB document
├── enum/ # 領域值物件 / 列舉Platform、Status…
├── repository/ # Repository 介面 + 實作
├── usecase/ # UseCase 介面、Request/Response DTO、實作
├── config/ # 模組用設定 struct
├── errors.go # 模組 sentinelErrNotFound 等),非第二套 8 碼
├── const.go # 模組常數
├── redis.go # Redis key 命名與 helper
└── mock/ # mockgen 產物
├── repository/
└── usecase/
```
### 依賴方向
```
usecase實作 → repository介面
→ entity、enum
→ usecase介面 + DTO
repository實作 → repository介面
→ entity
entity、enum → 只依賴標準庫或第三方型別
internal/logic → model/{module}/usecase 介面 only不 import repository、entity
```
## 1. Entity`entity/`
每個 MongoDB collection 對應一個 struct放在 `internal/model/{module}/entity/`
**規則:**
- 檔名使用 snake_case 對應 collection 語意,如 `account.go`、`user.go`。
- struct 名稱使用 PascalCase 單數,如 `Account`、`User`。
- 必須實作 `CollectionName() string`,回傳 MongoDB collection 名稱。
- 欄位 tag`bson` 必填;對外 JSON 序列化才加 `json`
- 主鍵使用 `primitive.ObjectID`tag 為 `` `bson:"_id,omitempty" json:"id,omitempty"` ``。
- 時間戳記統一用 `*int64`,欄位名 `CreateAt` / `UpdateAt`,值為 UTC nanoseconds。
- 可選欄位用指標型別(`*string`、`*int64`)。
- 領域列舉引用 `enum/` 下的型別,不在 entity 內重複定義。
**範例:**
```go
package entity
import (
"gateway/internal/model/member/enum"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Account struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
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"
}
```
## 2. 值物件 / 列舉(`enum/`
業務列舉、狀態碼等放在 `internal/model/{module}/enum/`
**規則:**
- 以具名型別包裝底層型別(`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` 驗證轉換邏輯。
## 3. Repository 介面(`repository/`
**規則:**
- 一個 entity 一個 `XxxRepository` interface。
- 方法第一個參數固定為 `context.Context`
- 參數 / 回傳值使用 `entity` 型別,不暴露 driver 細節(除 index migration 等必要場景)。
- Index migration 以獨立 interface 嵌入,命名 `{Entity}IndexUP`,方法名含版本號,如 `Index20241226001UP`
- 介面檔案不含實作、不含 import 基礎設施 package`mon`、`mongo` 實作層等僅在 repository 實作出現)。
**範例:**
```go
package repository
import (
"context"
"gateway/internal/model/member/entity"
"go.mongodb.org/mongo-driver/mongo"
)
type AccountRepository interface {
Insert(ctx context.Context, data *entity.Account) error
FindOne(ctx context.Context, id string) (*entity.Account, error)
Update(ctx context.Context, data *entity.Account) (*mongo.UpdateResult, error)
Delete(ctx context.Context, id string) (int64, error)
AccountIndexUP
}
type AccountIndexUP interface {
Index20241226001UP(ctx context.Context) (*mongo.Cursor, error)
}
```
## 4. Repository 實作(`repository/`
**規則:**
- struct 名稱 `{Entity}Repository`,建構子 `New{Entity}Repository(param {Entity}RepositoryParam)`
- Param struct 集中注入 `Conf`、`CacheConf`、`DBOpts`、`CacheOpts`。
- 建構時以 entity 的 `CollectionName()` 初始化 DocumentDB失敗時 `panic`(啟動期錯誤)。
- CRUD 透過 `mongo.DocumentDBWithCacheUseCase``gateway/internal/library/mongo`)操作,搭配模組 `redis.go` 的 key helper。
- `Insert`ID 為 zero 時自動產生 ObjectID 並寫入 `CreateAt` / `UpdateAt`
- `Update`:自動更新 `UpdateAt`
- `FindOne` / `Delete`:無效 ObjectID → `*errs.Error``ResInvalidMeasureID`)或模組 `ErrInvalidObjectID`;查無資料 → 模組 `ErrNotFound`(見第 7 節錯誤)。
- 整合測試放在同 package 的 `*_test.go`,可用 testcontainer + miniredis。
**範例:**
```go
type AccountRepositoryParam struct {
Conf *mongo.Conf
CacheConf cache.CacheConf
DBOpts []mon.Option
CacheOpts []cache.Option
}
type accountRepository struct {
DB mongo.DocumentDBWithCacheUseCase
}
func NewAccountRepository(param AccountRepositoryParam) AccountRepository {
e := entity.Account{}
documentDB, err := mongo.MustDocumentDBWithCache(
param.Conf, e.CollectionName(), param.CacheConf, param.DBOpts, param.CacheOpts,
)
if err != nil {
panic(err)
}
return &accountRepository{DB: documentDB}
}
```
## 5. UseCase 介面與 DTO`usecase/`
**規則:**
- 業務入口定義為 interface`AccountUseCase`;大介面可拆成多個小 interface 再 compose。
- Request / Response struct 放在同一 package命名 `{Action}Request`、`{Action}Response`。
- DTO 只含 `json` tag 與欄位註解,不含 bson tagDTO 不直接映射 DB
- 更新類 Request 的可選欄位用指標,以便區分「未傳入」與「傳入零值」。
- 共用分頁 struct 放 `common.go`,如 `Pager`
**範例:**
```go
package usecase
import "gateway/internal/model/member/enum"
type AccountUseCase interface {
CreateLoginUser(ctx context.Context, req *CreateLoginUserRequest) (*CreateLoginUserResponse, error)
}
type CreateLoginUserRequest struct {
LoginID string `json:"login_id"`
Platform enum.Platform `json:"platform"`
Token string `json:"token"`
}
```
## 6. UseCase 實作(`usecase/`
**規則:**
- struct 名稱描述業務聚合,如 `MemberUseCase`
-`{Name}UseCaseParam` 注入所有 repository 與 `config.Config`
- 建構子命名 `Must{Name}UseCase(param) AccountUseCase`,回傳 interface 型別。
- 實作 struct 嵌入 Param`type MemberUseCase struct { MemberUseCaseParam }`。
- 方法簽名與 interface 一致;內部組裝 `entity`,呼叫 repository。
- 錯誤一律回傳 `gateway/internal/library/errors``*errs.Error`(見第 7 節)。
- 可測性:將難 mock 的純函式抽成 package 級變數(如 `var HashPasswordFunc = HashPassword`)。
**範例:**
```go
type MemberUseCaseParam struct {
Account repository.AccountRepository
User repository.UserRepository
Config config.Config
}
type MemberUseCase struct {
MemberUseCaseParam
}
func MustMemberUseCase(param MemberUseCaseParam) AccountUseCase {
return &MemberUseCase{param}
}
```
## 7. 錯誤處理
全專案對外只使用 `gateway/internal/library/errors``var errb = errs.For(code.Facade)`)。模組根目錄的 `errors.go` **只放 sentinel**,不另建 8 碼常數表。
### 7.1 模組 sentinel`errors.go`
```go
package member
import "errors"
var (
ErrNotFound = errors.New("member: not found")
ErrInvalidObjectID = errors.New("member: invalid object id")
)
```
### 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. 模組共用檔案
| 檔案 | 用途 |
|------|------|
| `errors.go` | 模組 sentinel`ErrNotFound` 等) |
| `const.go` | 模組字面常數 |
| `redis.go` | Redis key 型別、`With()` 組合、`GetXxxRedisKey()` helper |
| `config/config.go` | UseCase 需要的設定 struct不含 go-zero RestConf |
Redis key 統一帶業務 prefix避免跨服務衝突
```go
type RedisKey string
const AccountRedisKey RedisKey = "member:account"
func (key RedisKey) With(s ...string) RedisKey { /* join with ":" */ }
func GetAccountRedisKey(id string) string {
return AccountRedisKey.With(id).ToString()
}
```
## 9. Mock`mock/`
Repository / UseCase 介面變更後,用 mockgen 重新生成:
```bash
mockgen -source=./internal/model/member/repository/account.go \
-destination=./internal/model/member/mock/repository/account.go \
-package=mockrepository
```
- 產物放在 `mock/repository/``mock/usecase/`**不要手改**。
- UseCase 單元測試注入 mock不啟動真實 DB。
## 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 檢查清單
1. 建立 `internal/model/{module}/` 目錄結構。
2.`entity/` 新增 struct + `CollectionName()`
3. 若有列舉 / 狀態,在 `enum/` 定義值物件。
4.`repository/` 宣告 interface 並實作 CRUD + index migration + `*_test.go`
5.`errors.go` 補充 sentinel若需要`redis.go` 補 cache key若需要
6.`usecase/` 定義 interface、DTO 與實作 + 單元測試。
7. 執行 mockgen 更新 `mock/`
8.`internal/svc/service_context.go` 組裝 repository → usecase。
9.`generate/api/` 定義路由,`make gen-api`。
10.`internal/logic/` 實作 types 映射,**只**呼叫 UseCase interface。
11. `make gen-doc`、`go test ./...`。
## 12. 與 Gateway HTTP 層的關係
```
HTTP Request
handlergoctl 生成)→ response.Write
logicgoctl 生成框架,手寫映射)
↓ 轉換 types ↔ usecase DTO
usecaseinternal/model/{module}/usecase
repositoryinternal/model/{module}/repository
MongoDB / Redis
```
- `internal/types`HTTP 請求 / 回應型別,由 `.api` 生成。
- `internal/model/{module}/usecase` DTO業務層資料結構logic 負責兩者映射。
- 錯誤自 usecase 以 `*errs.Error` 往上冒泡logic 原樣傳遞handler 經 `response.Write` 輸出 8 碼 JSON。