12 KiB
12 KiB
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。
目錄結構
以下以 member 模組為例,路徑前綴為 internal/model/member/:
internal/model/
└── member/
├── entity/ # 持久化資料模型(MongoDB document)
├── enum/ # 領域值物件 / 列舉(Platform、Status…)
├── repository/ # Repository 介面 + 實作
├── usecase/ # UseCase 介面、Request/Response DTO、實作
├── config/ # 模組用設定 struct
├── errors.go # 模組 sentinel(ErrNotFound 等),非第二套 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 內重複定義。
範例:
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。
範例:
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 一個
XxxRepositoryinterface。 - 方法第一個參數固定為
context.Context。 - 參數 / 回傳值使用
entity型別,不暴露 driver 細節(除 index migration 等必要場景)。 - Index migration 以獨立 interface 嵌入,命名
{Entity}IndexUP,方法名含版本號,如Index20241226001UP。 - 介面檔案不含實作、不含 import 基礎設施 package(
mon、mongo實作層等僅在 repository 實作出現)。
範例:
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。
範例:
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 只含
jsontag 與欄位註解,不含 bson tag(DTO 不直接映射 DB)。 - 更新類 Request 的可選欄位用指標,以便區分「未傳入」與「傳入零值」。
- 共用分頁 struct 放
common.go,如Pager。
範例:
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)。
範例:
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)
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,避免跨服務衝突:
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 重新生成:
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 檢查清單
- 建立
internal/model/{module}/目錄結構。 - 在
entity/新增 struct +CollectionName()。 - 若有列舉 / 狀態,在
enum/定義值物件。 - 在
repository/宣告 interface 並實作 CRUD + index migration +*_test.go。 - 在
errors.go補充 sentinel(若需要);在redis.go補 cache key(若需要)。 - 在
usecase/定義 interface、DTO 與實作 + 單元測試。 - 執行 mockgen 更新
mock/。 - 在
internal/svc/service_context.go組裝 repository → usecase。 - 在
generate/api/定義路由,make gen-api。 - 在
internal/logic/實作 types 映射,只呼叫 UseCase interface。 make gen-doc、go test ./...。
12. 與 Gateway HTTP 層的關係
HTTP Request
↓
handler(goctl 生成)→ response.Write
↓
logic(goctl 生成框架,手寫映射)
↓ 轉換 types ↔ usecase DTO
usecase(internal/model/{module}/usecase)
↓
repository(internal/model/{module}/repository)
↓
MongoDB / Redis
internal/types:HTTP 請求 / 回應型別,由.api生成。internal/model/{module}/usecaseDTO:業務層資料結構,logic 負責兩者映射。- 錯誤自 usecase 以
*errs.Error往上冒泡;logic 原樣傳遞,handler 經response.Write輸出 8 碼 JSON。