814 lines
24 KiB
Markdown
814 lines
24 KiB
Markdown
|
|
---
|
|||
|
|
name: go-backend-dev
|
|||
|
|
description: "Backend Agent 使用此技能實作 Golang 後端。根據實作計畫和 API 規格,使用 Domain-Driven + go-zero 風格架構和 TDD 流程產出 production-ready 程式碼。觸發時機:Task Breakdown 完成後(Stage 9)。"
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# /go-backend-dev — Golang 後端實作
|
|||
|
|
|
|||
|
|
Backend Agent 使用此技能實作 Golang 後端。
|
|||
|
|
|
|||
|
|
## 職責
|
|||
|
|
|
|||
|
|
1. 根據實作計畫建立專案結構(Domain-Driven + go-zero 風格)
|
|||
|
|
2. 使用 TDD 流程實作功能(Red-Green-Refactor)
|
|||
|
|
3. 按垂直切片逐步交付(端到端,非逐層)
|
|||
|
|
4. 實作 Domain / Usecase / Logic / Repository 各層
|
|||
|
|
5. 撰寫單元測試和整合測試
|
|||
|
|
|
|||
|
|
## 輸入
|
|||
|
|
|
|||
|
|
- 實作計畫 (`./plans/{feature}.md`)
|
|||
|
|
- API 規格 (`docs/api/{date}-{feature}.yaml`)
|
|||
|
|
- DB Schema (`docs/db/{date}-{feature}.sql`)
|
|||
|
|
|
|||
|
|
## 輸出
|
|||
|
|
|
|||
|
|
- Golang 程式碼結構
|
|||
|
|
- 測試程式碼(單元測試 >= 80%,業務邏輯 >= 90%)
|
|||
|
|
- Protobuf 定義(如需 gRPC)
|
|||
|
|
|
|||
|
|
## 流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
讀取實作計畫 + API 規格 + DB Schema
|
|||
|
|
↓
|
|||
|
|
識別垂直切片(每個切片 = 端到端功能)
|
|||
|
|
↓
|
|||
|
|
對每個切片執行 TDD 循環:
|
|||
|
|
├── RED: 寫測試 → 測試失敗
|
|||
|
|
├── GREEN: 寫最少程式碼 → 測試通過
|
|||
|
|
└── REFACTOR: 重構 → 測試仍然通過
|
|||
|
|
↓
|
|||
|
|
切片內建構順序:
|
|||
|
|
domain (entity/value object/interface)
|
|||
|
|
→ pkg/domain/usecase (介面)
|
|||
|
|
→ pkg/domain/repository (介面)
|
|||
|
|
→ pkg/usecase (實作)
|
|||
|
|
→ pkg/mock (mock)
|
|||
|
|
→ internal/logic (handler 邏輯)
|
|||
|
|
→ pkg/repository (基礎設施實作)
|
|||
|
|
↓
|
|||
|
|
所有切片完成 → 執行整合測試
|
|||
|
|
↓
|
|||
|
|
確認交付物檢查清單
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步驟說明
|
|||
|
|
|
|||
|
|
**1. 讀取輸入**
|
|||
|
|
|
|||
|
|
同時閱讀三個文件:
|
|||
|
|
- 實作計畫:了解垂直切片分解和優先順序
|
|||
|
|
- API 規格:了解端點、請求/回應結構
|
|||
|
|
- DB Schema:了解資料表結構和關係
|
|||
|
|
|
|||
|
|
**2. 識別垂直切片**
|
|||
|
|
|
|||
|
|
不是水平切片(一層一層做),而是垂直切片(端到端):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
✅ 正確方式(垂直):
|
|||
|
|
切片 1: 使用者註冊 (domain.entity + domain.usecase介面 + usecase實作 + logic + repository)
|
|||
|
|
切片 2: 使用者登入 (同上)
|
|||
|
|
切片 3: 使用者列表 (同上)
|
|||
|
|
|
|||
|
|
❌ 錯誤方式(水平):
|
|||
|
|
階段 1: 所有 domain entities
|
|||
|
|
階段 2: 所有 usecases
|
|||
|
|
階段 3: 所有 logic handlers
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**3. TDD 循環(每個切片)**
|
|||
|
|
|
|||
|
|
對每個切片,遵循 Red-Green-Refactor:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
RED: 寫一個測試 → 測試失敗
|
|||
|
|
GREEN: 寫最少的程式碼讓測試通過 → 測試通過
|
|||
|
|
REFACTOR: 重構程式碼 → 測試仍然通過
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
切片內建構順序(由內而外):
|
|||
|
|
1. `pkg/domain/entity/` — 定義 Entity 和 Value Object
|
|||
|
|
2. `pkg/domain/member/` — 定義值物件和列舉(含測試)
|
|||
|
|
3. `pkg/domain/usecase/` — 定義 Use Case 介面
|
|||
|
|
4. `pkg/domain/repository/` — 定義 Repository 介面
|
|||
|
|
5. `pkg/usecase/` — 實作業務邏輯(先寫測試)
|
|||
|
|
6. `pkg/mock/` — 產生 mock
|
|||
|
|
7. `internal/logic/` — Handler 邏輯
|
|||
|
|
8. `pkg/repository/` — 基礎設施實作(含 DB 測試)
|
|||
|
|
|
|||
|
|
**4. 測試**
|
|||
|
|
|
|||
|
|
每個切片完成後,確保:
|
|||
|
|
- 單元測試通過(`pkg/usecase/*_test.go`)
|
|||
|
|
- 值物件測試通過(`pkg/domain/member/*_test.go`)
|
|||
|
|
- Repository 測試通過(`pkg/repository/*_test.go`)
|
|||
|
|
- 整合測試通過(關鍵路徑)
|
|||
|
|
- 測試覆蓋率達標
|
|||
|
|
|
|||
|
|
**5. 完成驗證**
|
|||
|
|
|
|||
|
|
最後確認所有交付物完整。
|
|||
|
|
|
|||
|
|
## 專案結構
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
project-root/
|
|||
|
|
├── build/
|
|||
|
|
│ └── Dockerfile # 建置映像
|
|||
|
|
│
|
|||
|
|
├── etc/
|
|||
|
|
│ └── {service}.example.yaml # 範例設定檔
|
|||
|
|
│
|
|||
|
|
├── generate/
|
|||
|
|
│ └── protobuf/
|
|||
|
|
│ └── {service}.proto # Protobuf 定義(如需 gRPC)
|
|||
|
|
│
|
|||
|
|
├── internal/ # 應用層(不對外暴露)
|
|||
|
|
│ ├── config/
|
|||
|
|
│ │ └── config.go # 應用配置
|
|||
|
|
│ ├── logic/
|
|||
|
|
│ │ └── {module}/
|
|||
|
|
│ │ ├── create_{entity}_logic.go # 每個 use case 一個 logic 檔案
|
|||
|
|
│ │ ├── get_{entity}_logic.go
|
|||
|
|
│ │ ├── update_{entity}_logic.go
|
|||
|
|
│ │ └── ...
|
|||
|
|
│ ├── server/
|
|||
|
|
│ │ └── {module}/
|
|||
|
|
│ │ └── {module}_server.go # Server 定義(HTTP/gRPC)
|
|||
|
|
│ └── svc/
|
|||
|
|
│ └── service_context.go # 依賴注入容器
|
|||
|
|
│
|
|||
|
|
├── pkg/ # 領域層(可對外暴露)
|
|||
|
|
│ ├── domain/
|
|||
|
|
│ │ ├── config/
|
|||
|
|
│ │ │ └── config.go # Domain 配置
|
|||
|
|
│ │ ├── entity/
|
|||
|
|
│ │ │ ├── {entity}.go # Entity 定義
|
|||
|
|
│ │ │ ├── {entity}_uid_table.go # UID 對照表
|
|||
|
|
│ │ │ └── auto_id.go # 自動 ID 產生
|
|||
|
|
│ │ ├── {module}/
|
|||
|
|
│ │ │ ├── {value_object}.go # 值物件和列舉
|
|||
|
|
│ │ │ └── {value_object}_test.go # 值物件測試
|
|||
|
|
│ │ ├── repository/
|
|||
|
|
│ │ │ ├── {entity}.go # Repository 介面
|
|||
|
|
│ │ │ └── ...
|
|||
|
|
│ │ ├── usecase/
|
|||
|
|
│ │ │ ├── {module}.go # Use Case 介面
|
|||
|
|
│ │ │ └── ...
|
|||
|
|
│ │ ├── errors.go # Domain sentinel errors
|
|||
|
|
│ │ ├── const.go # Domain 常數
|
|||
|
|
│ │ └── redis.go # Redis domain 定義
|
|||
|
|
│ ├── mock/
|
|||
|
|
│ │ ├── repository/
|
|||
|
|
│ │ │ ├── {entity}.go # Repository mock
|
|||
|
|
│ │ │ └── ...
|
|||
|
|
│ │ └── usecase/
|
|||
|
|
│ │ └── {module}.go # Use Case mock
|
|||
|
|
│ ├── repository/
|
|||
|
|
│ │ ├── {entity}.go # Repository 實作
|
|||
|
|
│ │ ├── {entity}_test.go # Repository 測試
|
|||
|
|
│ │ ├── {entity}_uid.go # UID Repository 實作
|
|||
|
|
│ │ ├── {entity}_uid_test.go
|
|||
|
|
│ │ ├── error.go # Repository 錯誤定義
|
|||
|
|
│ │ └── start_{db}_container_test.go # testcontainers 啟動
|
|||
|
|
│ └── usecase/
|
|||
|
|
│ ├── {module}.go # Use Case 實作
|
|||
|
|
│ ├── {operation}.go # 特定操作
|
|||
|
|
│ ├── {operation}_test.go # Use Case 測試
|
|||
|
|
│ └── {utils}.go # 工具函式
|
|||
|
|
│
|
|||
|
|
├── {service}.go # 應用程式進入點
|
|||
|
|
├── Makefile
|
|||
|
|
├── go.mod
|
|||
|
|
├── go.sum
|
|||
|
|
├── docker-compose.yml
|
|||
|
|
└── readme.md
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 依賴方向規則
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
pkg/domain/ ← 無外部依賴(最內層,純定義)
|
|||
|
|
↑
|
|||
|
|
pkg/domain/usecase/ ← Use Case 介面(只有介面定義)
|
|||
|
|
pkg/domain/repository/ ← Repository 介面(只有介面定義)
|
|||
|
|
↑
|
|||
|
|
pkg/usecase/ ← 依賴 domain 介面(業務邏輯實作)
|
|||
|
|
pkg/mock/ ← 依賴 domain 介面(測試 mock)
|
|||
|
|
↑
|
|||
|
|
internal/logic/ ← 依賴 usecase 實作(handler 邏輯)
|
|||
|
|
internal/server/ ← 依賴 logic(HTTP/gRPC server)
|
|||
|
|
internal/svc/ ← 依賴所有(DI 容器,組裝依賴)
|
|||
|
|
↑
|
|||
|
|
pkg/repository/ ← 依賴 domain 介面(基礎設施實作)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────┐
|
|||
|
|
│ pkg/domain/ │ ← 純定義,無依賴
|
|||
|
|
│ ├── entity/ │
|
|||
|
|
│ ├── {module}/ (value objects)│
|
|||
|
|
│ ├── repository/ (interfaces) │
|
|||
|
|
│ ├── usecase/ (interfaces) │
|
|||
|
|
│ ├── errors.go │
|
|||
|
|
│ └── const.go │
|
|||
|
|
├─────────────────────────────────┤
|
|||
|
|
│ pkg/usecase/ │ ← 依賴 domain 介面
|
|||
|
|
│ pkg/mock/ │ ← 依賴 domain 介面
|
|||
|
|
├─────────────────────────────────┤
|
|||
|
|
│ internal/logic/ │ ← 依賴 usecase
|
|||
|
|
│ internal/server/ │
|
|||
|
|
│ internal/config/ │
|
|||
|
|
│ internal/svc/ │ ← DI 容器
|
|||
|
|
├─────────────────────────────────┤
|
|||
|
|
│ pkg/repository/ │ ← 依賴 domain 介面
|
|||
|
|
└─────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 架構原則
|
|||
|
|
|
|||
|
|
### `pkg/domain/` — 純領域定義
|
|||
|
|
|
|||
|
|
`pkg/domain/` 是核心,只包含**介面和定義**,不包含實作:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/domain/entity/user.go — Entity 定義
|
|||
|
|
package entity
|
|||
|
|
|
|||
|
|
type User struct {
|
|||
|
|
ID string `json:"id"`
|
|||
|
|
Email string `json:"email"`
|
|||
|
|
Name string `json:"name"`
|
|||
|
|
Status string `json:"status"`
|
|||
|
|
CreatedAt time.Time `json:"created_at"`
|
|||
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/domain/member/status.go — 值物件(含測試)
|
|||
|
|
package member
|
|||
|
|
|
|||
|
|
type Status string
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
StatusActive Status = "active"
|
|||
|
|
StatusInactive Status = "inactive"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func (s Status) IsValid() bool {
|
|||
|
|
switch s {
|
|||
|
|
case StatusActive, StatusInactive:
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewStatus(s string) (Status, error) {
|
|||
|
|
status := Status(s)
|
|||
|
|
if !status.IsValid() {
|
|||
|
|
return "", fmt.Errorf("invalid status: %s", s)
|
|||
|
|
}
|
|||
|
|
return status, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/domain/member/status_test.go — 值物件測試
|
|||
|
|
package member
|
|||
|
|
|
|||
|
|
func TestStatus_IsValid(t *testing.T) {
|
|||
|
|
assert.True(t, StatusActive.IsValid())
|
|||
|
|
assert.True(t, StatusInactive.IsValid())
|
|||
|
|
assert.False(t, Status("unknown").IsValid())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestNewStatus(t *testing.T) {
|
|||
|
|
status, err := NewStatus("active")
|
|||
|
|
assert.NoError(t, err)
|
|||
|
|
assert.Equal(t, StatusActive, status)
|
|||
|
|
|
|||
|
|
_, err = NewStatus("unknown")
|
|||
|
|
assert.Error(t, err)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/domain/repository/user.go — Repository 介面
|
|||
|
|
package repository
|
|||
|
|
|
|||
|
|
type UserRepository interface {
|
|||
|
|
GetByID(ctx context.Context, id string) (*entity.User, error)
|
|||
|
|
GetByEmail(ctx context.Context, email string) (*entity.User, error)
|
|||
|
|
Create(ctx context.Context, user *entity.User) error
|
|||
|
|
Update(ctx context.Context, user *entity.User) error
|
|||
|
|
Delete(ctx context.Context, id string) error
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/domain/usecase/user.go — Use Case 介面
|
|||
|
|
package usecase
|
|||
|
|
|
|||
|
|
type UserUsecase interface {
|
|||
|
|
CreateUser(ctx context.Context, input CreateUserInput) (*entity.User, error)
|
|||
|
|
GetUser(ctx context.Context, id string) (*entity.User, error)
|
|||
|
|
UpdateUser(ctx context.Context, id string, input UpdateUserInput) (*entity.User, error)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/domain/errors.go — Domain sentinel errors
|
|||
|
|
package domain
|
|||
|
|
|
|||
|
|
import "errors"
|
|||
|
|
|
|||
|
|
var (
|
|||
|
|
ErrUserNotFound = errors.New("user not found")
|
|||
|
|
ErrInvalidInput = errors.New("invalid input")
|
|||
|
|
ErrDuplicateEmail = errors.New("email already exists")
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### `pkg/usecase/` — 業務邏輯實作
|
|||
|
|
|
|||
|
|
每個檔案一個功能領域,測試檔案同目錄:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/usecase/account.go — Use Case 進入點
|
|||
|
|
package usecase
|
|||
|
|
|
|||
|
|
type AccountUsecase struct {
|
|||
|
|
userRepo repository.UserRepository
|
|||
|
|
accountRepo repository.AccountRepository
|
|||
|
|
redis *redis.Client
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewAccountUsecase(
|
|||
|
|
userRepo repository.UserRepository,
|
|||
|
|
accountRepo repository.AccountRepository,
|
|||
|
|
redis *redis.Client,
|
|||
|
|
) *AccountUsecase {
|
|||
|
|
return &AccountUsecase{
|
|||
|
|
userRepo: userRepo,
|
|||
|
|
accountRepo: accountRepo,
|
|||
|
|
redis: redis,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/usecase/create_user.go — 單一操作
|
|||
|
|
package usecase
|
|||
|
|
|
|||
|
|
func (uc *AccountUsecase) CreateUser(ctx context.Context, input CreateUserInput) (*entity.User, error) {
|
|||
|
|
if err := input.Validate(); err != nil {
|
|||
|
|
return nil, fmt.Errorf("validate input: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
existing, _ := uc.userRepo.GetByEmail(ctx, input.Email)
|
|||
|
|
if existing != nil {
|
|||
|
|
return nil, domain.ErrDuplicateEmail
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
user, err := entity.NewUser(input.Email, input.Password, input.Name)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("create user: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := uc.userRepo.Create(ctx, user); err != nil {
|
|||
|
|
return nil, fmt.Errorf("save user: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return user, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/usecase/create_user_test.go — 測試同目錄
|
|||
|
|
package usecase
|
|||
|
|
|
|||
|
|
func TestAccountUsecase_CreateUser_Success(t *testing.T) {
|
|||
|
|
mockUserRepo := new(mock.UserRepository)
|
|||
|
|
uc := NewAccountUsecase(mockUserRepo, nil, nil)
|
|||
|
|
|
|||
|
|
mockUserRepo.On("GetByEmail", mock.Anything, "test@example.com").Return(nil, nil)
|
|||
|
|
mockUserRepo.On("Create", mock.Anything, mock.AnythingOfType("*entity.User")).Return(nil)
|
|||
|
|
|
|||
|
|
user, err := uc.CreateUser(context.Background(), input)
|
|||
|
|
|
|||
|
|
assert.NoError(t, err)
|
|||
|
|
assert.NotNil(t, user)
|
|||
|
|
mockUserRepo.AssertExpectations(t)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### `pkg/mock/` — 自動產生的 Mock
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/mock/repository/user.go — 由 mockery 產生
|
|||
|
|
//go:generate mockery --name=UserRepository --output=../../mock/repository --outpkg=mock_repository
|
|||
|
|
|
|||
|
|
package mock_repository
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"github.com/stretchr/testify/mock"
|
|||
|
|
"your-project/pkg/domain/repository"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type UserRepository struct {
|
|||
|
|
mock.Mock
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *UserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
|
|||
|
|
args := m.Called(ctx, id)
|
|||
|
|
if args.Get(0) == nil {
|
|||
|
|
return nil, args.Error(1)
|
|||
|
|
}
|
|||
|
|
return args.Get(0).(*entity.User), args.Error(1)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### `internal/logic/` — Handler 邏輯
|
|||
|
|
|
|||
|
|
每個 use case 一個 logic 檔案(go-zero 風格):
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// internal/logic/account/create_user_logic.go
|
|||
|
|
package account
|
|||
|
|
|
|||
|
|
type CreateUserLogic struct {
|
|||
|
|
ctx context.Context
|
|||
|
|
svcCtx *svc.ServiceContext
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewCreateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserLogic {
|
|||
|
|
return &CreateUserLogic{
|
|||
|
|
ctx: ctx,
|
|||
|
|
svcCtx: svcCtx,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (l *CreateUserLogic) CreateUser(req *types.CreateUserReq) (*types.UserResp, error) {
|
|||
|
|
user, err := l.svcCtx.UserUsecase.CreateUser(l.ctx, usecase.CreateUserInput{
|
|||
|
|
Email: req.Email,
|
|||
|
|
Password: req.Password,
|
|||
|
|
Name: req.Name,
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &types.UserResp{
|
|||
|
|
ID: user.ID,
|
|||
|
|
Email: user.Email,
|
|||
|
|
Name: user.Name,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### `internal/svc/` — 依賴注入容器
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// internal/svc/service_context.go
|
|||
|
|
package svc
|
|||
|
|
|
|||
|
|
type ServiceContext struct {
|
|||
|
|
Config config.Config
|
|||
|
|
UserUsecase usecase.UserUsecase
|
|||
|
|
AccountUsecase usecase.AccountUsecase
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewServiceContext(c config.Config) *ServiceContext {
|
|||
|
|
db := mongo.NewClient(c.Mongo.URI)
|
|||
|
|
redisClient := redis.NewClient(c.Redis)
|
|||
|
|
|
|||
|
|
userRepo := repository.NewUserRepository(db)
|
|||
|
|
accountRepo := repository.NewAccountRepository(db)
|
|||
|
|
|
|||
|
|
return &ServiceContext{
|
|||
|
|
Config: c,
|
|||
|
|
UserUsecase: usecase.NewUserUsecase(userRepo, redisClient),
|
|||
|
|
AccountUsecase: usecase.NewAccountUsecase(userRepo, accountRepo, redisClient),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### `pkg/repository/` — 基礎設施實作
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/repository/user.go
|
|||
|
|
package repository
|
|||
|
|
|
|||
|
|
type userRepository struct {
|
|||
|
|
db *mongo.Database
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewUserRepository(db *mongo.Database) domain.Repository.UserRepository {
|
|||
|
|
return &userRepository{db: db}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *userRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
|
|||
|
|
var user entity.User
|
|||
|
|
err := r.db.Collection("users").FindOne(ctx, bson.M{"_id": id}).Decode(&user)
|
|||
|
|
if err != nil {
|
|||
|
|
if err == mongo.ErrNoDocuments {
|
|||
|
|
return nil, domain.ErrUserNotFound
|
|||
|
|
}
|
|||
|
|
return nil, fmt.Errorf("find user by id: %w", err)
|
|||
|
|
}
|
|||
|
|
return &user, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/repository/user_test.go
|
|||
|
|
package repository
|
|||
|
|
|
|||
|
|
func TestUserRepository_GetByID_Success(t *testing.T) {
|
|||
|
|
db := startMongoContainer(t)
|
|||
|
|
defer db.Client().Disconnect(context.Background())
|
|||
|
|
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 編碼規範
|
|||
|
|
|
|||
|
|
### 檔案命名
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
值物件和列舉: pkg/domain/{module}/{name}.go + _test.go
|
|||
|
|
Entity: pkg/domain/entity/{name}.go
|
|||
|
|
Repository 介面:pkg/domain/repository/{name}.go
|
|||
|
|
Usecase 介面: pkg/domain/usecase/{module}.go
|
|||
|
|
Usecase 實作: pkg/usecase/{operation}.go + _test.go
|
|||
|
|
Usecase 工具: pkg/usecase/{module}_utils.go + _test.go
|
|||
|
|
Repository 實作:pkg/repository/{name}.go + _test.go
|
|||
|
|
Mock: pkg/mock/repository/{name}.go
|
|||
|
|
pkg/mock/usecase/{module}.go
|
|||
|
|
Handler 邏輯: internal/logic/{module}/{operation}_logic.go
|
|||
|
|
Server: internal/server/{module}/{module}_server.go
|
|||
|
|
Service Context:internal/svc/service_context.go
|
|||
|
|
Protobuf 定義: generate/protobuf/{module}.proto
|
|||
|
|
設定檔: etc/{service}.yaml
|
|||
|
|
Dockerfile: build/Dockerfile
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 命名規範
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// Package: 小寫,語意明確
|
|||
|
|
package usecase // 不是 usecases
|
|||
|
|
package repository // 不是 repositories
|
|||
|
|
package entity // 不是 entities
|
|||
|
|
|
|||
|
|
// Entity struct: PascalCase,無後綴
|
|||
|
|
type User struct { ... } // 不是 UserModel, UserEntity
|
|||
|
|
|
|||
|
|
// Value Object: 基礎型別別名 + 方法
|
|||
|
|
type Status string // 不是 StatusEnum
|
|||
|
|
|
|||
|
|
// Interface (介面): 放在 pkg/domain/ 下,語意命名
|
|||
|
|
type UserRepository interface { ... } // 不是 UserRepo 或 UserRepositoryI
|
|||
|
|
|
|||
|
|
// Use Case struct: {Module}Usecase
|
|||
|
|
type AccountUsecase struct { ... }
|
|||
|
|
|
|||
|
|
// Use Case 方法: 動詞開頭
|
|||
|
|
func (uc *AccountUsecase) CreateUser(ctx context.Context, ...) (*entity.User, error)
|
|||
|
|
func (uc *AccountUsecase) GetUser(ctx context.Context, id string) (*entity.User, error)
|
|||
|
|
|
|||
|
|
// Logic struct: {Operation}Logic
|
|||
|
|
type CreateUserLogic struct { ... }
|
|||
|
|
|
|||
|
|
// Error: Err 前綴
|
|||
|
|
var ErrUserNotFound = errors.New("user not found")
|
|||
|
|
|
|||
|
|
// Constant: PascalCase(exported)或 camelCase(internal)
|
|||
|
|
const MaxRetryCount = 3
|
|||
|
|
const defaultPageSize = 20
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 錯誤處理
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// Sentinel errors — pkg/domain/errors.go
|
|||
|
|
var (
|
|||
|
|
ErrUserNotFound = errors.New("user not found")
|
|||
|
|
ErrInvalidInput = errors.New("invalid input")
|
|||
|
|
ErrDuplicateEmail = errors.New("email already exists")
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Error wrapping — always use %w
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("create user: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Error checking — always use errors.Is
|
|||
|
|
if errors.Is(err, domain.ErrUserNotFound) {
|
|||
|
|
// handle not found
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Repository errors — pkg/repository/error.go
|
|||
|
|
var (
|
|||
|
|
ErrMongoConnection = errors.New("mongo connection failed")
|
|||
|
|
ErrRedisConnection = errors.New("redis connection failed")
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 介面設計
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 介面定義在 pkg/domain/(消費者端)
|
|||
|
|
// 實作定義在 pkg/ 下(提供者端)
|
|||
|
|
|
|||
|
|
// Accept interfaces, return structs
|
|||
|
|
func NewUserUsecase(repo repository.UserRepository, redis *redis.Client) *UserUsecase {
|
|||
|
|
return &UserUsecase{repo: repo, redis: redis}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Keep interfaces small (1-3 methods)
|
|||
|
|
type UserRepository interface {
|
|||
|
|
GetByID(ctx context.Context, id string) (*entity.User, error)
|
|||
|
|
Create(ctx context.Context, user *entity.User) error
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## TDD 規範
|
|||
|
|
|
|||
|
|
此技能與 `tdd` 技能整合,遵循共同的 TDD 原則。
|
|||
|
|
|
|||
|
|
### 測試金字塔
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
/\
|
|||
|
|
/ \
|
|||
|
|
/ E2E \ <- 少數關鍵流程
|
|||
|
|
/--------\
|
|||
|
|
/Integration\ <- DB + Redis (testcontainers)
|
|||
|
|
/--------------\
|
|||
|
|
/ Unit Tests \ <- 最多,80%+ 覆蓋
|
|||
|
|
/--------------------\
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 測試位置
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
測試檔案跟原始碼同目錄:
|
|||
|
|
|
|||
|
|
pkg/domain/member/status_test.go ← 值物件測試
|
|||
|
|
pkg/usecase/create_user_test.go ← Use Case 測試
|
|||
|
|
pkg/repository/user_test.go ← Repository 測試
|
|||
|
|
pkg/repository/start_mongo_container_test.go ← testcontainers 啟動
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 垂直切片 TDD
|
|||
|
|
|
|||
|
|
每個切片按照以下順序:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
切片: 使用者註冊
|
|||
|
|
1. RED: 寫 TestStatus_IsValid (值物件)
|
|||
|
|
2. GREEN: 寫 Status.IsValid()
|
|||
|
|
3. RED: 寫 TestAccountUsecase_CreateUser_Success
|
|||
|
|
4. GREEN: 寫 domain/entity, domain/usecase介面, pkg/usecase實作, mock
|
|||
|
|
5. RED: 寫 TestAccountUsecase_CreateUser_DuplicateEmail
|
|||
|
|
6. GREEN: 加入重複檢查
|
|||
|
|
7. RED: 寫 TestUserRepository_Create (DB 測試)
|
|||
|
|
8. GREEN: 寫 pkg/repository/user.go
|
|||
|
|
9. RED: 寫 TestCreateUserLogic (handler 測試)
|
|||
|
|
10. GREEN: 寫 internal/logic/account/create_user_logic.go
|
|||
|
|
11. REFACTOR: 清理全部
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Mock 策略
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 使用 mockery 自動產生 mock
|
|||
|
|
// 在接口檔案加上 go:generate 指令
|
|||
|
|
//go:generate mockery --name=UserRepository --output=../../mock/repository --outpkg=mock_repository
|
|||
|
|
|
|||
|
|
// 單元測試使用 mock
|
|||
|
|
func TestAccountUsecase_CreateUser_Success(t *testing.T) {
|
|||
|
|
mockRepo := new(mock_repository.UserRepository)
|
|||
|
|
uc := usecase.NewAccountUsecase(mockRepo, nil, nil)
|
|||
|
|
|
|||
|
|
mockRepo.On("GetByEmail", mock.Anything, "test@example.com").Return(nil, nil)
|
|||
|
|
mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*entity.User")).Return(nil)
|
|||
|
|
|
|||
|
|
user, err := uc.CreateUser(context.Background(), input)
|
|||
|
|
|
|||
|
|
assert.NoError(t, err)
|
|||
|
|
assert.NotNil(t, user)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### testcontainers 策略
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// pkg/repository/start_mongo_container_test.go
|
|||
|
|
func startMongoContainer(t *testing.T) *mongo.Database {
|
|||
|
|
ctx := context.Background()
|
|||
|
|
req := testcontainers.ContainerRequest{
|
|||
|
|
Image: "mongo:7",
|
|||
|
|
ExposedPorts: []string{"27017/tcp"},
|
|||
|
|
WaitingFor: wait.ForListeningPort("27017/tcp"),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mongoC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
|||
|
|
ContainerRequest: req,
|
|||
|
|
Started: true,
|
|||
|
|
})
|
|||
|
|
require.NoError(t, err)
|
|||
|
|
|
|||
|
|
t.Cleanup(func() {
|
|||
|
|
mongoC.Terminate(ctx)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ... return connected database
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 覆蓋率要求
|
|||
|
|
|
|||
|
|
- 值物件 (`pkg/domain/member/`): >= 90%
|
|||
|
|
- Use Case (`pkg/usecase/`): >= 90%
|
|||
|
|
- Repository (`pkg/repository/`): >= 80%
|
|||
|
|
- Logic (`internal/logic/`): >= 80%(整合測試為主)
|
|||
|
|
- Critical paths: Integration tests required
|
|||
|
|
|
|||
|
|
## 垂直切片模板
|
|||
|
|
|
|||
|
|
每個垂直切片的檔案清單:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
切片: {operation}_{entity}
|
|||
|
|
|
|||
|
|
新增/修改的檔案:
|
|||
|
|
├── pkg/domain/entity/{entity}.go ← Entity 定義
|
|||
|
|
├── pkg/domain/member/{value_object}.go ← 值物件(如需)
|
|||
|
|
├── pkg/domain/member/{value_object}_test.go ← 值物件測試
|
|||
|
|
├── pkg/domain/repository/{entity}.go ← Repository 介面
|
|||
|
|
├── pkg/domain/usecase/{module}.go ← Use Case 介面
|
|||
|
|
├── pkg/usecase/{operation}.go ← Use Case 實作
|
|||
|
|
├── pkg/usecase/{operation}_test.go ← Use Case 測試
|
|||
|
|
├── pkg/mock/repository/{entity}.go ← Repository mock
|
|||
|
|
├── pkg/repository/{entity}.go ← Repository 實作
|
|||
|
|
├── pkg/repository/{entity}_test.go ← Repository 測試
|
|||
|
|
├── internal/logic/{module}/{operation}_logic.go ← Handler 邏輯
|
|||
|
|
└── internal/svc/service_context.go ← 更新 DI
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 完成檢查清單
|
|||
|
|
|
|||
|
|
### 每個切片完成後
|
|||
|
|
- [ ] 值物件測試通過
|
|||
|
|
- [ ] Use Case 測試通過
|
|||
|
|
- [ ] Repository 測試通過(含 DB)
|
|||
|
|
- [ ] 錯誤處理完整
|
|||
|
|
- [ ] 依賴方向正確(domain 無外部依賴)
|
|||
|
|
|
|||
|
|
### 全部完成後
|
|||
|
|
- [ ] 專案結構符合 Domain-Driven + go-zero 風格
|
|||
|
|
- [ ] `pkg/domain/` 包含所有 Entity、Value Object、介面定義
|
|||
|
|
- [ ] `pkg/usecase/` 包含所有業務邏輯實作
|
|||
|
|
- [ ] `pkg/repository/` 包含所有基礎設施實作
|
|||
|
|
- [ ] `internal/logic/` 包含所有 Handler 邏輯
|
|||
|
|
- [ ] `internal/svc/` 包含完整的依賴注入設定
|
|||
|
|
- [ ] 單元測試 >= 80% 覆蓋率
|
|||
|
|
- [ ] 業務邏輯 >= 90% 覆蓋率
|
|||
|
|
- [ ] 整合測試通過(關鍵路徑)
|
|||
|
|
- [ ] 錯誤處理一致且使用 `%w` wrapping
|
|||
|
|
|
|||
|
|
## 相依技能
|
|||
|
|
|
|||
|
|
- **前置**: `prd-to-plan` (實作計畫), `be-api-design` (API 規格), `dba-schema` (DB Schema)
|
|||
|
|
- **輔助**: `tdd` (TDD Red-Green-Refactor 流程), `design-an-interface` (介面設計)
|
|||
|
|
- **後續**: `qa` (QA 測試)
|
|||
|
|
|
|||
|
|
## 退回機制
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
QA 失敗 (Stage 10)
|
|||
|
|
↓
|
|||
|
|
Orchestrator 重新分配修復任務
|
|||
|
|
↓
|
|||
|
|
Backend Agent 修復 Bug + 新增回歸測試
|
|||
|
|
↓
|
|||
|
|
重新進入 QA (Stage 10)
|
|||
|
|
|
|||
|
|
Code Review 退回 (Stage 11)
|
|||
|
|
↓
|
|||
|
|
處理 PR 回饋
|
|||
|
|
↓
|
|||
|
|
重新進入 QA (Stage 10) 驗證
|
|||
|
|
|
|||
|
|
實作計畫不可行
|
|||
|
|
↓
|
|||
|
|
退回 Task Breakdown (Stage 8) 重新分解
|
|||
|
|
```
|