template-monorepo/docs/model.md

12 KiB
Raw Blame History

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        # 模組 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. Entityentity/

每個 MongoDB collection 對應一個 struct放在 internal/model/{module}/entity/

規則:

  • 檔名使用 snake_case 對應 collection 語意,如 account.gouser.go
  • struct 名稱使用 PascalCase 單數,如 AccountUser
  • 必須實作 CollectionName() string,回傳 MongoDB collection 名稱。
  • 欄位 tagbson 必填;對外 JSON 序列化才加 json
  • 主鍵使用 primitive.ObjectIDtag 為 `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 一個 XxxRepository interface。
  • 方法第一個參數固定為 context.Context
  • 參數 / 回傳值使用 entity 型別,不暴露 driver 細節(除 index migration 等必要場景)。
  • Index migration 以獨立 interface 嵌入,命名 {Entity}IndexUP,方法名含版本號,如 Index20241226001UP
  • 介面檔案不含實作、不含 import 基礎設施 packagemonmongo 實作層等僅在 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 集中注入 ConfCacheConfDBOptsCacheOpts
  • 建構時以 entity 的 CollectionName() 初始化 DocumentDB失敗時 panic(啟動期錯誤)。
  • CRUD 透過 mongo.DocumentDBWithCacheUseCasegateway/internal/library/mongo)操作,搭配模組 redis.go 的 key helper。
  • InsertID 為 zero 時自動產生 ObjectID 並寫入 CreateAt / UpdateAt
  • Update:自動更新 UpdateAt
  • FindOne / Delete:無效 ObjectID → *errs.ErrorResInvalidMeasureID)或模組 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 介面與 DTOusecase/

規則:

  • 業務入口定義為 interfaceAccountUseCase;大介面可拆成多個小 interface 再 compose。
  • Request / Response struct 放在同一 package命名 {Action}Request{Action}Response
  • DTO 只含 json tag 與欄位註解,不含 bson tagDTO 不直接映射 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 嵌入 Paramtype 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/errorsvar errb = errs.For(code.Facade))。模組根目錄的 errors.go 只放 sentinel,不另建 8 碼常數表。

7.1 模組 sentinelerrors.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.ResAlreadyExisterrb.ResInvalidStateerrb.AuthForbidden 等。
  • errors.Is(err, member.ErrNotFound)errb.ResNotFound("member", id).WithCause(err)
  • 已是 *errs.Error 且語意正確(如 DBDuplicate)→ 原樣 return err

7.4 Logic / Handler

  • LogicInputMissingRequired 等 HTTP 輸入錯誤usecase 回傳的 err 不二次包裝
  • Handlerresponse.Write,由 errs.FromError 決定 HTTP 狀態與 body。

8. 模組共用檔案

檔案 用途
errors.go 模組 sentinelErrNotFound 等)
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. Mockmock/

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 AccountUser
Collection 方法 CollectionName()
Repository 介面 AccountRepository
Repository 實作 struct accountRepositoryAccountRepository
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-docgo test ./...

12. 與 Gateway HTTP 層的關係

HTTP Request
    ↓
handlergoctl 生成)→ response.Write
    ↓
logicgoctl 生成框架,手寫映射)
    ↓ 轉換 types ↔ usecase DTO
usecaseinternal/model/{module}/usecase
    ↓
repositoryinternal/model/{module}/repository
    ↓
MongoDB / Redis
  • internal/typesHTTP 請求 / 回應型別,由 .api 生成。
  • internal/model/{module}/usecase DTO業務層資料結構logic 負責兩者映射。
  • 錯誤自 usecase 以 *errs.Error 往上冒泡logic 原樣傳遞handler 經 response.Write 輸出 8 碼 JSON。