1232 lines
37 KiB
Markdown
1232 lines
37 KiB
Markdown
|
|
# DDD领域开发指南
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
本文档描述了在tyapi-server项目中如何按照DDD(领域驱动设计)架构模式新增领域或领域内功能的标准流程和最佳实践。
|
|||
|
|
|
|||
|
|
## 目录结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
tyapi-server-gin/
|
|||
|
|
├── internal/
|
|||
|
|
│ ├── domains/ # 领域层
|
|||
|
|
│ │ └── {domain}/ # 具体领域
|
|||
|
|
│ │ ├── entities/ # 实体
|
|||
|
|
│ │ ├── repositories/ # 仓储接口
|
|||
|
|
│ │ └── services/ # 领域服务
|
|||
|
|
│ ├── application/ # 应用层
|
|||
|
|
│ │ └── {domain}/ # 具体领域
|
|||
|
|
│ │ ├── dto/ # 数据传输对象
|
|||
|
|
│ │ └── {service}_application_service.go
|
|||
|
|
│ ├── infrastructure/ # 基础设施层
|
|||
|
|
│ │ ├── database/repositories/ # 仓储实现
|
|||
|
|
│ │ └── http/ # HTTP层
|
|||
|
|
│ │ ├── handlers/ # 处理器
|
|||
|
|
│ │ └── routes/ # 路由
|
|||
|
|
│ └── container/ # 依赖注入容器
|
|||
|
|
└── docs/ # 文档
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 新增领域的完整流程
|
|||
|
|
|
|||
|
|
### 第一步:创建实体 (Entities)
|
|||
|
|
|
|||
|
|
#### 1.1 主实体文件
|
|||
|
|
```go
|
|||
|
|
// internal/domains/{domain}/entities/{entity}.go
|
|||
|
|
package entities
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"time"
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// {Entity} 实体
|
|||
|
|
type {Entity} struct {
|
|||
|
|
ID string `gorm:"primaryKey;type:varchar(36)" comment:"ID"`
|
|||
|
|
Name string `gorm:"type:varchar(100);not null" comment:"名称"`
|
|||
|
|
Description string `gorm:"type:text" comment:"描述"`
|
|||
|
|
IsEnabled bool `gorm:"default:true" comment:"是否启用"`
|
|||
|
|
IsVisible bool `gorm:"default:true" comment:"是否可见"`
|
|||
|
|
|
|||
|
|
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
|
|||
|
|
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
|
|||
|
|
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 业务方法
|
|||
|
|
func (e *{Entity}) IsValid() bool {
|
|||
|
|
return e.DeletedAt.Time.IsZero() && e.IsEnabled
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (e *{Entity}) Enable() {
|
|||
|
|
e.IsEnabled = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (e *{Entity}) Disable() {
|
|||
|
|
e.IsEnabled = false
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 1.2 枚举文件(如需要)
|
|||
|
|
```go
|
|||
|
|
// internal/domains/{domain}/entities/{entity}_enums.go
|
|||
|
|
package entities
|
|||
|
|
|
|||
|
|
// {Entity}Status 状态枚举
|
|||
|
|
type {Entity}Status string
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
{Entity}StatusActive {Entity}Status = "active"
|
|||
|
|
{Entity}StatusInactive {Entity}Status = "inactive"
|
|||
|
|
{Entity}StatusPending {Entity}Status = "pending"
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 第二步:创建仓储接口 (Repository Interfaces)
|
|||
|
|
|
|||
|
|
#### 2.1 仓储接口定义
|
|||
|
|
```go
|
|||
|
|
// internal/domains/{domain}/repositories/{entity}_repository_interface.go
|
|||
|
|
package repositories
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"tyapi-server/internal/domains/{domain}/entities"
|
|||
|
|
"tyapi-server/internal/shared/interfaces"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// {Entity}Repository 仓储接口
|
|||
|
|
type {Entity}Repository interface {
|
|||
|
|
// 继承基础Repository接口
|
|||
|
|
interfaces.Repository[entities.{Entity}]
|
|||
|
|
|
|||
|
|
// 业务特定方法
|
|||
|
|
FindByStatus(ctx context.Context, status entities.{Entity}Status) ([]*entities.{Entity}, error)
|
|||
|
|
FindByUserID(ctx context.Context, userID string) ([]*entities.{Entity}, error)
|
|||
|
|
CountByStatus(ctx context.Context, status entities.{Entity}Status) (int64, error)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.2 查询对象
|
|||
|
|
```go
|
|||
|
|
// internal/domains/{domain}/repositories/queries/{entity}_queries.go
|
|||
|
|
package queries
|
|||
|
|
|
|||
|
|
import "tyapi-server/internal/domains/{domain}/entities"
|
|||
|
|
|
|||
|
|
// List{Entity}sQuery 列表查询
|
|||
|
|
type List{Entity}sQuery struct {
|
|||
|
|
Page int `form:"page" binding:"min=1" comment:"页码"`
|
|||
|
|
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
|||
|
|
Status entities.{Entity}Status `form:"status" comment:"状态"`
|
|||
|
|
UserID string `form:"user_id" comment:"用户ID"`
|
|||
|
|
SortBy string `form:"sort_by" comment:"排序字段"`
|
|||
|
|
SortOrder string `form:"sort_order" comment:"排序方向"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Get{Entity}Query 获取详情查询
|
|||
|
|
type Get{Entity}Query struct {
|
|||
|
|
ID string `uri:"id" binding:"required" comment:"ID"`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 第三步:创建应用服务 (Application Services)
|
|||
|
|
|
|||
|
|
#### 3.1 应用服务接口
|
|||
|
|
```go
|
|||
|
|
// internal/application/{domain}/{entity}_application_service.go
|
|||
|
|
package {domain}
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"tyapi-server/internal/application/{domain}/dto/commands"
|
|||
|
|
"tyapi-server/internal/application/{domain}/dto/queries"
|
|||
|
|
"tyapi-server/internal/application/{domain}/dto/responses"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// {Entity}ApplicationService 应用服务接口
|
|||
|
|
type {Entity}ApplicationService interface {
|
|||
|
|
// 基础CRUD操作
|
|||
|
|
Create{Entity}(ctx context.Context, cmd *commands.Create{Entity}Command) error
|
|||
|
|
Update{Entity}(ctx context.Context, cmd *commands.Update{Entity}Command) error
|
|||
|
|
Delete{Entity}(ctx context.Context, cmd *commands.Delete{Entity}Command) error
|
|||
|
|
Get{Entity}ByID(ctx context.Context, query *queries.Get{Entity}Query) (*responses.{Entity}InfoResponse, error)
|
|||
|
|
List{Entity}s(ctx context.Context, query *queries.List{Entity}sQuery) (*responses.{Entity}ListResponse, error)
|
|||
|
|
|
|||
|
|
// 业务操作
|
|||
|
|
Enable{Entity}(ctx context.Context, cmd *commands.Enable{Entity}Command) error
|
|||
|
|
Disable{Entity}(ctx context.Context, cmd *commands.Disable{Entity}Command) error
|
|||
|
|
|
|||
|
|
// 统计
|
|||
|
|
Get{Entity}Stats(ctx context.Context) (*responses.{Entity}StatsResponse, error)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.2 应用服务实现
|
|||
|
|
```go
|
|||
|
|
// internal/application/{domain}/{entity}_application_service_impl.go
|
|||
|
|
package {domain}
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"fmt"
|
|||
|
|
"tyapi-server/internal/application/{domain}/dto/commands"
|
|||
|
|
"tyapi-server/internal/application/{domain}/dto/queries"
|
|||
|
|
"tyapi-server/internal/application/{domain}/dto/responses"
|
|||
|
|
"tyapi-server/internal/domains/{domain}/entities"
|
|||
|
|
"tyapi-server/internal/domains/{domain}/repositories"
|
|||
|
|
repoQueries "tyapi-server/internal/domains/{domain}/repositories/queries"
|
|||
|
|
|
|||
|
|
"go.uber.org/zap"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// {Entity}ApplicationServiceImpl 应用服务实现
|
|||
|
|
type {Entity}ApplicationServiceImpl struct {
|
|||
|
|
{entity}Repo repositories.{Entity}Repository
|
|||
|
|
logger *zap.Logger
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// New{Entity}ApplicationService 创建应用服务
|
|||
|
|
func New{Entity}ApplicationService(
|
|||
|
|
{entity}Repo repositories.{Entity}Repository,
|
|||
|
|
logger *zap.Logger,
|
|||
|
|
) {Entity}ApplicationService {
|
|||
|
|
return &{Entity}ApplicationServiceImpl{
|
|||
|
|
{entity}Repo: {entity}Repo,
|
|||
|
|
logger: logger,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Create{Entity} 创建实体
|
|||
|
|
func (s *{Entity}ApplicationServiceImpl) Create{Entity}(ctx context.Context, cmd *commands.Create{Entity}Command) error {
|
|||
|
|
// 1. 参数验证
|
|||
|
|
if err := s.validateCreate{Entity}(cmd); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 创建实体
|
|||
|
|
{entity} := entities.{Entity}{
|
|||
|
|
Name: cmd.Name,
|
|||
|
|
Description: cmd.Description,
|
|||
|
|
IsEnabled: cmd.IsEnabled,
|
|||
|
|
IsVisible: cmd.IsVisible,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 保存到仓储
|
|||
|
|
_, err := s.{entity}Repo.Create(ctx, {entity})
|
|||
|
|
if err != nil {
|
|||
|
|
s.logger.Error("创建{Entity}失败", zap.Error(err))
|
|||
|
|
return fmt.Errorf("创建{Entity}失败: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.logger.Info("创建{Entity}成功", zap.String("name", cmd.Name))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 其他方法实现...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 第四步:创建DTO (Data Transfer Objects)
|
|||
|
|
|
|||
|
|
#### 4.1 命令DTO
|
|||
|
|
```go
|
|||
|
|
// internal/application/{domain}/dto/commands/{entity}_commands.go
|
|||
|
|
package commands
|
|||
|
|
|
|||
|
|
// Create{Entity}Command 创建命令
|
|||
|
|
type Create{Entity}Command struct {
|
|||
|
|
Name string `json:"name" binding:"required" comment:"名称"`
|
|||
|
|
Description string `json:"description" comment:"描述"`
|
|||
|
|
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
|||
|
|
IsVisible bool `json:"is_visible" comment:"是否可见"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update{Entity}Command 更新命令
|
|||
|
|
type Update{Entity}Command struct {
|
|||
|
|
ID string `json:"-"`
|
|||
|
|
Name string `json:"name" comment:"名称"`
|
|||
|
|
Description string `json:"description" comment:"描述"`
|
|||
|
|
IsEnabled *bool `json:"is_enabled" comment:"是否启用"`
|
|||
|
|
IsVisible *bool `json:"is_visible" comment:"是否可见"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Delete{Entity}Command 删除命令
|
|||
|
|
type Delete{Entity}Command struct {
|
|||
|
|
ID string `json:"-"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Enable{Entity}Command 启用命令
|
|||
|
|
type Enable{Entity}Command struct {
|
|||
|
|
ID string `json:"-"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Disable{Entity}Command 禁用命令
|
|||
|
|
type Disable{Entity}Command struct {
|
|||
|
|
ID string `json:"-"`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.2 查询DTO
|
|||
|
|
```go
|
|||
|
|
// internal/application/{domain}/dto/queries/{entity}_queries.go
|
|||
|
|
package queries
|
|||
|
|
|
|||
|
|
import "tyapi-server/internal/domains/{domain}/entities"
|
|||
|
|
|
|||
|
|
// List{Entity}sQuery 列表查询
|
|||
|
|
type List{Entity}sQuery struct {
|
|||
|
|
Page int `form:"page" binding:"min=1" comment:"页码"`
|
|||
|
|
PageSize int `form:"page_size" binding:"min=1,max=100" comment:"每页数量"`
|
|||
|
|
Status entities.{Entity}Status `form:"status" comment:"状态"`
|
|||
|
|
UserID string `form:"user_id" comment:"用户ID"`
|
|||
|
|
SortBy string `form:"sort_by" comment:"排序字段"`
|
|||
|
|
SortOrder string `form:"sort_order" comment:"排序方向"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Get{Entity}Query 获取详情查询
|
|||
|
|
type Get{Entity}Query struct {
|
|||
|
|
ID string `uri:"id" binding:"required" comment:"ID"`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.3 响应DTO
|
|||
|
|
```go
|
|||
|
|
// internal/application/{domain}/dto/responses/{entity}_responses.go
|
|||
|
|
package responses
|
|||
|
|
|
|||
|
|
import "time"
|
|||
|
|
|
|||
|
|
// {Entity}InfoResponse 详情响应
|
|||
|
|
type {Entity}InfoResponse struct {
|
|||
|
|
ID string `json:"id" comment:"ID"`
|
|||
|
|
Name string `json:"name" comment:"名称"`
|
|||
|
|
Description string `json:"description" comment:"描述"`
|
|||
|
|
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
|||
|
|
IsVisible bool `json:"is_visible" comment:"是否可见"`
|
|||
|
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
|||
|
|
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// {Entity}ListResponse 列表响应
|
|||
|
|
type {Entity}ListResponse struct {
|
|||
|
|
Total int64 `json:"total" comment:"总数"`
|
|||
|
|
Page int `json:"page" comment:"页码"`
|
|||
|
|
Size int `json:"size" comment:"每页数量"`
|
|||
|
|
Items []{Entity}InfoResponse `json:"items" comment:"列表"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// {Entity}StatsResponse 统计响应
|
|||
|
|
type {Entity}StatsResponse struct {
|
|||
|
|
Total{Entity}s int64 `json:"total_{entity}s" comment:"总数"`
|
|||
|
|
Enabled{Entity}s int64 `json:"enabled_{entity}s" comment:"启用数"`
|
|||
|
|
Visible{Entity}s int64 `json:"visible_{entity}s" comment:"可见数"`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 第五步:实现仓储 (Repository Implementation)
|
|||
|
|
|
|||
|
|
#### 5.1 GORM仓储实现
|
|||
|
|
```go
|
|||
|
|
// internal/infrastructure/database/repositories/{domain}/gorm_{entity}_repository.go
|
|||
|
|
package repositories
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"tyapi-server/internal/domains/{domain}/entities"
|
|||
|
|
"tyapi-server/internal/domains/{domain}/repositories"
|
|||
|
|
"tyapi-server/internal/domains/{domain}/repositories/queries"
|
|||
|
|
"tyapi-server/internal/shared/interfaces"
|
|||
|
|
|
|||
|
|
"go.uber.org/zap"
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Gorm{Entity}Repository GORM仓储实现
|
|||
|
|
type Gorm{Entity}Repository struct {
|
|||
|
|
db *gorm.DB
|
|||
|
|
logger *zap.Logger
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 编译时检查接口实现
|
|||
|
|
var _ repositories.{Entity}Repository = (*Gorm{Entity}Repository)(nil)
|
|||
|
|
|
|||
|
|
// NewGorm{Entity}Repository 创建GORM仓储
|
|||
|
|
func NewGorm{Entity}Repository(db *gorm.DB, logger *zap.Logger) repositories.{Entity}Repository {
|
|||
|
|
return &Gorm{Entity}Repository{
|
|||
|
|
db: db,
|
|||
|
|
logger: logger,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Create 创建实体
|
|||
|
|
func (r *Gorm{Entity}Repository) Create(ctx context.Context, entity entities.{Entity}) (entities.{Entity}, error) {
|
|||
|
|
r.logger.Info("创建{Entity}", zap.String("id", entity.ID))
|
|||
|
|
err := r.db.WithContext(ctx).Create(&entity).Error
|
|||
|
|
return entity, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetByID 根据ID获取实体
|
|||
|
|
func (r *Gorm{Entity}Repository) GetByID(ctx context.Context, id string) (entities.{Entity}, error) {
|
|||
|
|
var entity entities.{Entity}
|
|||
|
|
err := r.db.WithContext(ctx).Where("id = ?", id).First(&entity).Error
|
|||
|
|
return entity, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update 更新实体
|
|||
|
|
func (r *Gorm{Entity}Repository) Update(ctx context.Context, entity entities.{Entity}) error {
|
|||
|
|
r.logger.Info("更新{Entity}", zap.String("id", entity.ID))
|
|||
|
|
return r.db.WithContext(ctx).Save(&entity).Error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Delete 删除实体
|
|||
|
|
func (r *Gorm{Entity}Repository) Delete(ctx context.Context, id string) error {
|
|||
|
|
r.logger.Info("删除{Entity}", zap.String("id", id))
|
|||
|
|
return r.db.WithContext(ctx).Delete(&entities.{Entity}{}, "id = ?", id).Error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// List{Entity}s 获取列表
|
|||
|
|
func (r *Gorm{Entity}Repository) List{Entity}s(ctx context.Context, query *queries.List{Entity}sQuery) ([]*entities.{Entity}, int64, error) {
|
|||
|
|
var {entity}s []entities.{Entity}
|
|||
|
|
var total int64
|
|||
|
|
|
|||
|
|
dbQuery := r.db.WithContext(ctx).Model(&entities.{Entity}{})
|
|||
|
|
|
|||
|
|
// 应用筛选条件
|
|||
|
|
if query.Status != "" {
|
|||
|
|
dbQuery = dbQuery.Where("status = ?", query.Status)
|
|||
|
|
}
|
|||
|
|
if query.UserID != "" {
|
|||
|
|
dbQuery = dbQuery.Where("user_id = ?", query.UserID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取总数
|
|||
|
|
if err := dbQuery.Count(&total).Error; err != nil {
|
|||
|
|
return nil, 0, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 应用排序
|
|||
|
|
if query.SortBy != "" {
|
|||
|
|
order := query.SortBy
|
|||
|
|
if query.SortOrder == "desc" {
|
|||
|
|
order += " DESC"
|
|||
|
|
} else {
|
|||
|
|
order += " ASC"
|
|||
|
|
}
|
|||
|
|
dbQuery = dbQuery.Order(order)
|
|||
|
|
} else {
|
|||
|
|
dbQuery = dbQuery.Order("created_at DESC")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 应用分页
|
|||
|
|
if query.Page > 0 && query.PageSize > 0 {
|
|||
|
|
offset := (query.Page - 1) * query.PageSize
|
|||
|
|
dbQuery = dbQuery.Offset(offset).Limit(query.PageSize)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取数据
|
|||
|
|
if err := dbQuery.Find(&{entity}s).Error; err != nil {
|
|||
|
|
return nil, 0, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 转换为指针切片
|
|||
|
|
result := make([]*entities.{Entity}, len({entity}s))
|
|||
|
|
for i := range {entity}s {
|
|||
|
|
result[i] = &{entity}s[i]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result, total, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 基础Repository接口方法
|
|||
|
|
func (r *Gorm{Entity}Repository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
|
|||
|
|
var count int64
|
|||
|
|
query := r.db.WithContext(ctx).Model(&entities.{Entity}{})
|
|||
|
|
|
|||
|
|
// 应用筛选条件
|
|||
|
|
if options.Filters != nil {
|
|||
|
|
for key, value := range options.Filters {
|
|||
|
|
query = query.Where(key+" = ?", value)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 应用搜索条件
|
|||
|
|
if options.Search != "" {
|
|||
|
|
query = query.Where("name LIKE ? OR description LIKE ?", "%"+options.Search+"%", "%"+options.Search+"%")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
err := query.Count(&count).Error
|
|||
|
|
return count, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 其他基础方法实现...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 第六步:创建HTTP处理器 (HTTP Handlers)
|
|||
|
|
|
|||
|
|
#### 6.1 HTTP处理器
|
|||
|
|
```go
|
|||
|
|
// internal/infrastructure/http/handlers/{entity}_handler.go
|
|||
|
|
package handlers
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"tyapi-server/internal/application/{domain}"
|
|||
|
|
"tyapi-server/internal/application/{domain}/dto/commands"
|
|||
|
|
"tyapi-server/internal/application/{domain}/dto/queries"
|
|||
|
|
"tyapi-server/internal/shared/interfaces"
|
|||
|
|
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
"go.uber.org/zap"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// {Entity}Handler HTTP处理器
|
|||
|
|
type {Entity}Handler struct {
|
|||
|
|
appService {domain}.{Entity}ApplicationService
|
|||
|
|
responseBuilder interfaces.ResponseBuilder
|
|||
|
|
logger *zap.Logger
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// New{Entity}Handler 创建HTTP处理器
|
|||
|
|
func New{Entity}Handler(
|
|||
|
|
appService {domain}.{Entity}ApplicationService,
|
|||
|
|
responseBuilder interfaces.ResponseBuilder,
|
|||
|
|
logger *zap.Logger,
|
|||
|
|
) *{Entity}Handler {
|
|||
|
|
return &{Entity}Handler{
|
|||
|
|
appService: appService,
|
|||
|
|
responseBuilder: responseBuilder,
|
|||
|
|
logger: logger,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// List{Entity}s 获取列表
|
|||
|
|
// @Summary 获取{Entity}列表
|
|||
|
|
// @Description 分页获取{Entity}列表,支持筛选
|
|||
|
|
// @Tags {Entity}管理
|
|||
|
|
// @Accept json
|
|||
|
|
// @Produce json
|
|||
|
|
// @Param page query int false "页码" default(1)
|
|||
|
|
// @Param page_size query int false "每页数量" default(10)
|
|||
|
|
// @Param status query string false "状态"
|
|||
|
|
// @Param sort_by query string false "排序字段"
|
|||
|
|
// @Param sort_order query string false "排序方向" Enums(asc, desc)
|
|||
|
|
// @Success 200 {object} responses.{Entity}ListResponse "获取列表成功"
|
|||
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|||
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|||
|
|
// @Router /api/v1/{entity}s [get]
|
|||
|
|
func (h *{Entity}Handler) List{Entity}s(c *gin.Context) {
|
|||
|
|
var query queries.List{Entity}sQuery
|
|||
|
|
if err := c.ShouldBindQuery(&query); err != nil {
|
|||
|
|
h.responseBuilder.BadRequest(c, "请求参数错误")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置默认值
|
|||
|
|
if query.Page <= 0 {
|
|||
|
|
query.Page = 1
|
|||
|
|
}
|
|||
|
|
if query.PageSize <= 0 {
|
|||
|
|
query.PageSize = 10
|
|||
|
|
}
|
|||
|
|
if query.PageSize > 100 {
|
|||
|
|
query.PageSize = 100
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result, err := h.appService.List{Entity}s(c.Request.Context(), &query)
|
|||
|
|
if err != nil {
|
|||
|
|
h.logger.Error("获取{Entity}列表失败", zap.Error(err))
|
|||
|
|
h.responseBuilder.InternalError(c, "获取{Entity}列表失败")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
h.responseBuilder.Success(c, result, "获取{Entity}列表成功")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Get{Entity}Detail 获取详情
|
|||
|
|
// @Summary 获取{Entity}详情
|
|||
|
|
// @Description 根据ID获取{Entity}详细信息
|
|||
|
|
// @Tags {Entity}管理
|
|||
|
|
// @Accept json
|
|||
|
|
// @Produce json
|
|||
|
|
// @Param id path string true "{Entity}ID"
|
|||
|
|
// @Success 200 {object} responses.{Entity}InfoResponse "获取详情成功"
|
|||
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|||
|
|
// @Failure 404 {object} map[string]interface{} "{Entity}不存在"
|
|||
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|||
|
|
// @Router /api/v1/{entity}s/{id} [get]
|
|||
|
|
func (h *{Entity}Handler) Get{Entity}Detail(c *gin.Context) {
|
|||
|
|
var query queries.Get{Entity}Query
|
|||
|
|
query.ID = c.Param("id")
|
|||
|
|
|
|||
|
|
if query.ID == "" {
|
|||
|
|
h.responseBuilder.BadRequest(c, "{Entity}ID不能为空")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result, err := h.appService.Get{Entity}ByID(c.Request.Context(), &query)
|
|||
|
|
if err != nil {
|
|||
|
|
h.logger.Error("获取{Entity}详情失败", zap.Error(err), zap.String("{entity}_id", query.ID))
|
|||
|
|
h.responseBuilder.NotFound(c, "{Entity}不存在")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
h.responseBuilder.Success(c, result, "获取{Entity}详情成功")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Create{Entity} 创建{Entity}
|
|||
|
|
// @Summary 创建{Entity}
|
|||
|
|
// @Description 创建新的{Entity}
|
|||
|
|
// @Tags {Entity}管理
|
|||
|
|
// @Accept json
|
|||
|
|
// @Produce json
|
|||
|
|
// @Param request body commands.Create{Entity}Command true "创建请求"
|
|||
|
|
// @Success 200 {object} map[string]interface{} "创建成功"
|
|||
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|||
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|||
|
|
// @Router /api/v1/{entity}s [post]
|
|||
|
|
func (h *{Entity}Handler) Create{Entity}(c *gin.Context) {
|
|||
|
|
var cmd commands.Create{Entity}Command
|
|||
|
|
if err := c.ShouldBindJSON(&cmd); err != nil {
|
|||
|
|
h.responseBuilder.BadRequest(c, "请求参数错误")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := h.appService.Create{Entity}(c.Request.Context(), &cmd); err != nil {
|
|||
|
|
h.logger.Error("创建{Entity}失败", zap.Error(err))
|
|||
|
|
h.responseBuilder.BadRequest(c, err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
h.responseBuilder.Success(c, nil, "创建{Entity}成功")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update{Entity} 更新{Entity}
|
|||
|
|
// @Summary 更新{Entity}
|
|||
|
|
// @Description 更新{Entity}信息
|
|||
|
|
// @Tags {Entity}管理
|
|||
|
|
// @Accept json
|
|||
|
|
// @Produce json
|
|||
|
|
// @Param id path string true "{Entity}ID"
|
|||
|
|
// @Param request body commands.Update{Entity}Command true "更新请求"
|
|||
|
|
// @Success 200 {object} map[string]interface{} "更新成功"
|
|||
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|||
|
|
// @Failure 404 {object} map[string]interface{} "{Entity}不存在"
|
|||
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|||
|
|
// @Router /api/v1/{entity}s/{id} [put]
|
|||
|
|
func (h *{Entity}Handler) Update{Entity}(c *gin.Context) {
|
|||
|
|
id := c.Param("id")
|
|||
|
|
if id == "" {
|
|||
|
|
h.responseBuilder.BadRequest(c, "{Entity}ID不能为空")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var cmd commands.Update{Entity}Command
|
|||
|
|
if err := c.ShouldBindJSON(&cmd); err != nil {
|
|||
|
|
h.responseBuilder.BadRequest(c, "请求参数错误")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cmd.ID = id
|
|||
|
|
|
|||
|
|
if err := h.appService.Update{Entity}(c.Request.Context(), &cmd); err != nil {
|
|||
|
|
h.logger.Error("更新{Entity}失败", zap.Error(err), zap.String("{entity}_id", id))
|
|||
|
|
h.responseBuilder.BadRequest(c, err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
h.responseBuilder.Success(c, nil, "更新{Entity}成功")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Delete{Entity} 删除{Entity}
|
|||
|
|
// @Summary 删除{Entity}
|
|||
|
|
// @Description 删除指定的{Entity}
|
|||
|
|
// @Tags {Entity}管理
|
|||
|
|
// @Accept json
|
|||
|
|
// @Produce json
|
|||
|
|
// @Param id path string true "{Entity}ID"
|
|||
|
|
// @Success 200 {object} map[string]interface{} "删除成功"
|
|||
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|||
|
|
// @Failure 404 {object} map[string]interface{} "{Entity}不存在"
|
|||
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|||
|
|
// @Router /api/v1/{entity}s/{id} [delete]
|
|||
|
|
func (h *{Entity}Handler) Delete{Entity}(c *gin.Context) {
|
|||
|
|
id := c.Param("id")
|
|||
|
|
if id == "" {
|
|||
|
|
h.responseBuilder.BadRequest(c, "{Entity}ID不能为空")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var cmd commands.Delete{Entity}Command
|
|||
|
|
cmd.ID = id
|
|||
|
|
|
|||
|
|
if err := h.appService.Delete{Entity}(c.Request.Context(), &cmd); err != nil {
|
|||
|
|
h.logger.Error("删除{Entity}失败", zap.Error(err), zap.String("{entity}_id", id))
|
|||
|
|
h.responseBuilder.BadRequest(c, err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
h.responseBuilder.Success(c, nil, "删除{Entity}成功")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Enable{Entity} 启用{Entity}
|
|||
|
|
// @Summary 启用{Entity}
|
|||
|
|
// @Description 启用指定的{Entity}
|
|||
|
|
// @Tags {Entity}管理
|
|||
|
|
// @Accept json
|
|||
|
|
// @Produce json
|
|||
|
|
// @Param id path string true "{Entity}ID"
|
|||
|
|
// @Success 200 {object} map[string]interface{} "启用成功"
|
|||
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|||
|
|
// @Failure 404 {object} map[string]interface{} "{Entity}不存在"
|
|||
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|||
|
|
// @Router /api/v1/{entity}s/{id}/enable [post]
|
|||
|
|
func (h *{Entity}Handler) Enable{Entity}(c *gin.Context) {
|
|||
|
|
id := c.Param("id")
|
|||
|
|
if id == "" {
|
|||
|
|
h.responseBuilder.BadRequest(c, "{Entity}ID不能为空")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var cmd commands.Enable{Entity}Command
|
|||
|
|
cmd.ID = id
|
|||
|
|
|
|||
|
|
if err := h.appService.Enable{Entity}(c.Request.Context(), &cmd); err != nil {
|
|||
|
|
h.logger.Error("启用{Entity}失败", zap.Error(err), zap.String("{entity}_id", id))
|
|||
|
|
h.responseBuilder.BadRequest(c, err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
h.responseBuilder.Success(c, nil, "启用{Entity}成功")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Disable{Entity} 禁用{Entity}
|
|||
|
|
// @Summary 禁用{Entity}
|
|||
|
|
// @Description 禁用指定的{Entity}
|
|||
|
|
// @Tags {Entity}管理
|
|||
|
|
// @Accept json
|
|||
|
|
// @Produce json
|
|||
|
|
// @Param id path string true "{Entity}ID"
|
|||
|
|
// @Success 200 {object} map[string]interface{} "禁用成功"
|
|||
|
|
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
|||
|
|
// @Failure 404 {object} map[string]interface{} "{Entity}不存在"
|
|||
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|||
|
|
// @Router /api/v1/{entity}s/{id}/disable [post]
|
|||
|
|
func (h *{Entity}Handler) Disable{Entity}(c *gin.Context) {
|
|||
|
|
id := c.Param("id")
|
|||
|
|
if id == "" {
|
|||
|
|
h.responseBuilder.BadRequest(c, "{Entity}ID不能为空")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var cmd commands.Disable{Entity}Command
|
|||
|
|
cmd.ID = id
|
|||
|
|
|
|||
|
|
if err := h.appService.Disable{Entity}(c.Request.Context(), &cmd); err != nil {
|
|||
|
|
h.logger.Error("禁用{Entity}失败", zap.Error(err), zap.String("{entity}_id", id))
|
|||
|
|
h.responseBuilder.BadRequest(c, err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
h.responseBuilder.Success(c, nil, "禁用{Entity}成功")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Get{Entity}Stats 获取统计信息
|
|||
|
|
// @Summary 获取{Entity}统计信息
|
|||
|
|
// @Description 获取{Entity}相关的统计信息
|
|||
|
|
// @Tags {Entity}管理
|
|||
|
|
// @Accept json
|
|||
|
|
// @Produce json
|
|||
|
|
// @Success 200 {object} responses.{Entity}StatsResponse "获取统计成功"
|
|||
|
|
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
|||
|
|
// @Router /api/v1/{entity}s/stats [get]
|
|||
|
|
func (h *{Entity}Handler) Get{Entity}Stats(c *gin.Context) {
|
|||
|
|
result, err := h.appService.Get{Entity}Stats(c.Request.Context())
|
|||
|
|
if err != nil {
|
|||
|
|
h.logger.Error("获取{Entity}统计失败", zap.Error(err))
|
|||
|
|
h.responseBuilder.InternalError(c, "获取{Entity}统计失败")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
h.responseBuilder.Success(c, result, "获取{Entity}统计成功")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 第七步:创建路由 (Routes)
|
|||
|
|
|
|||
|
|
#### 7.1 路由配置
|
|||
|
|
```go
|
|||
|
|
// internal/infrastructure/http/routes/{entity}_routes.go
|
|||
|
|
package routes
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"tyapi-server/internal/infrastructure/http/handlers"
|
|||
|
|
"tyapi-server/internal/shared/http"
|
|||
|
|
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Register{Entity}Routes 注册{Entity}路由
|
|||
|
|
func Register{Entity}Routes(router *gin.RouterGroup, handler *handlers.{Entity}Handler) {
|
|||
|
|
{entity}Group := router.Group("/{entity}s")
|
|||
|
|
{
|
|||
|
|
// 基础CRUD操作
|
|||
|
|
{entity}Group.GET("", handler.List{Entity}s) // 获取列表
|
|||
|
|
{entity}Group.GET("/:id", handler.Get{Entity}Detail) // 获取详情
|
|||
|
|
{entity}Group.POST("", handler.Create{Entity}) // 创建
|
|||
|
|
{entity}Group.PUT("/:id", handler.Update{Entity}) // 更新
|
|||
|
|
{entity}Group.DELETE("/:id", handler.Delete{Entity}) // 删除
|
|||
|
|
|
|||
|
|
// 业务操作
|
|||
|
|
{entity}Group.POST("/:id/enable", handler.Enable{Entity}) // 启用
|
|||
|
|
{entity}Group.POST("/:id/disable", handler.Disable{Entity}) // 禁用
|
|||
|
|
|
|||
|
|
// 统计信息
|
|||
|
|
{entity}Group.GET("/stats", handler.Get{Entity}Stats) // 统计
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 第八步:配置依赖注入 (Dependency Injection)
|
|||
|
|
|
|||
|
|
#### 8.1 容器配置
|
|||
|
|
```go
|
|||
|
|
// internal/container/container.go 中添加
|
|||
|
|
func (c *Container) register{Entity}Services() {
|
|||
|
|
// 注册仓储
|
|||
|
|
c.{entity}Repository = database.NewGorm{Entity}Repository(c.db, c.logger)
|
|||
|
|
|
|||
|
|
// 注册应用服务
|
|||
|
|
c.{entity}ApplicationService = application.New{Entity}ApplicationService(
|
|||
|
|
c.{entity}Repository,
|
|||
|
|
c.logger,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 注册HTTP处理器
|
|||
|
|
c.{entity}Handler = handlers.New{Entity}Handler(
|
|||
|
|
c.{entity}ApplicationService,
|
|||
|
|
c.responseBuilder,
|
|||
|
|
c.logger,
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 在registerAllServices方法中调用
|
|||
|
|
func (c *Container) registerAllServices() {
|
|||
|
|
// ... 其他服务注册
|
|||
|
|
c.register{Entity}Services()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 在registerAllRoutes方法中注册路由
|
|||
|
|
func (c *Container) registerAllRoutes() {
|
|||
|
|
// ... 其他路由注册
|
|||
|
|
routes.Register{Entity}Routes(c.apiGroup, c.{entity}Handler)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 测试指南
|
|||
|
|
|
|||
|
|
### 单元测试
|
|||
|
|
|
|||
|
|
#### 1. 实体测试
|
|||
|
|
```go
|
|||
|
|
// internal/domains/{domain}/entities/{entity}_test.go
|
|||
|
|
package entities
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"testing"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func Test{Entity}_IsValid(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
{entity} {Entity}
|
|||
|
|
want bool
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "有效的{Entity}",
|
|||
|
|
{entity}: {Entity}{
|
|||
|
|
ID: "test-id",
|
|||
|
|
Name: "测试{Entity}",
|
|||
|
|
IsEnabled: true,
|
|||
|
|
},
|
|||
|
|
want: true,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "禁用的{Entity}",
|
|||
|
|
{entity}: {Entity}{
|
|||
|
|
ID: "test-id",
|
|||
|
|
Name: "测试{Entity}",
|
|||
|
|
IsEnabled: false,
|
|||
|
|
},
|
|||
|
|
want: false,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
if got := tt.{entity}.IsValid(); got != tt.want {
|
|||
|
|
t.Errorf("{Entity}.IsValid() = %v, want %v", got, tt.want)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2. 应用服务测试
|
|||
|
|
```go
|
|||
|
|
// internal/application/{domain}/{entity}_application_service_test.go
|
|||
|
|
package {domain}
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"testing"
|
|||
|
|
"tyapi-server/internal/application/{domain}/dto/commands"
|
|||
|
|
"tyapi-server/internal/domains/{domain}/entities"
|
|||
|
|
|
|||
|
|
"github.com/stretchr/testify/mock"
|
|||
|
|
"go.uber.org/zap"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Mock{Entity}Repository 模拟仓储
|
|||
|
|
type Mock{Entity}Repository struct {
|
|||
|
|
mock.Mock
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *Mock{Entity}Repository) Create(ctx context.Context, entity entities.{Entity}) (entities.{Entity}, error) {
|
|||
|
|
args := m.Called(ctx, entity)
|
|||
|
|
return args.Get(0).(entities.{Entity}), args.Error(1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 其他方法实现...
|
|||
|
|
|
|||
|
|
func Test{Entity}ApplicationService_Create{Entity}(t *testing.T) {
|
|||
|
|
mockRepo := new(Mock{Entity}Repository)
|
|||
|
|
logger := zap.NewNop()
|
|||
|
|
|
|||
|
|
service := &{Entity}ApplicationServiceImpl{
|
|||
|
|
{entity}Repo: mockRepo,
|
|||
|
|
logger: logger,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cmd := &commands.Create{Entity}Command{
|
|||
|
|
Name: "测试{Entity}",
|
|||
|
|
Description: "测试描述",
|
|||
|
|
IsEnabled: true,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
expected{Entity} := entities.{Entity}{
|
|||
|
|
Name: cmd.Name,
|
|||
|
|
Description: cmd.Description,
|
|||
|
|
IsEnabled: cmd.IsEnabled,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mockRepo.On("Create", mock.Anything, expected{Entity}).Return(expected{Entity}, nil)
|
|||
|
|
|
|||
|
|
err := service.Create{Entity}(context.Background(), cmd)
|
|||
|
|
|
|||
|
|
assert.NoError(t, err)
|
|||
|
|
mockRepo.AssertExpectations(t)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 集成测试
|
|||
|
|
|
|||
|
|
#### 1. HTTP处理器测试
|
|||
|
|
```go
|
|||
|
|
// internal/infrastructure/http/handlers/{entity}_handler_test.go
|
|||
|
|
package handlers
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"encoding/json"
|
|||
|
|
"net/http"
|
|||
|
|
"net/http/httptest"
|
|||
|
|
"testing"
|
|||
|
|
"tyapi-server/internal/application/{domain}/dto/commands"
|
|||
|
|
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
"github.com/stretchr/testify/assert"
|
|||
|
|
"github.com/stretchr/testify/mock"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func Test{Entity}Handler_Create{Entity}(t *testing.T) {
|
|||
|
|
gin.SetMode(gin.TestMode)
|
|||
|
|
|
|||
|
|
mockAppService := new(Mock{Entity}ApplicationService)
|
|||
|
|
mockResponseBuilder := new(MockResponseBuilder)
|
|||
|
|
|
|||
|
|
handler := &{Entity}Handler{
|
|||
|
|
appService: mockAppService,
|
|||
|
|
responseBuilder: mockResponseBuilder,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cmd := commands.Create{Entity}Command{
|
|||
|
|
Name: "测试{Entity}",
|
|||
|
|
Description: "测试描述",
|
|||
|
|
IsEnabled: true,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
body, _ := json.Marshal(cmd)
|
|||
|
|
|
|||
|
|
w := httptest.NewRecorder()
|
|||
|
|
c, _ := gin.CreateTestContext(w)
|
|||
|
|
c.Request = httptest.NewRequest("POST", "/{entity}s", bytes.NewBuffer(body))
|
|||
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|||
|
|
|
|||
|
|
mockAppService.On("Create{Entity}", mock.Anything, &cmd).Return(nil)
|
|||
|
|
mockResponseBuilder.On("Success", c, nil, "创建{Entity}成功").Return()
|
|||
|
|
|
|||
|
|
handler.Create{Entity}(c)
|
|||
|
|
|
|||
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|||
|
|
mockAppService.AssertExpectations(t)
|
|||
|
|
mockResponseBuilder.AssertExpectations(t)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 最佳实践
|
|||
|
|
|
|||
|
|
### 1. 命名规范
|
|||
|
|
|
|||
|
|
#### 实体命名
|
|||
|
|
- 实体名称使用单数形式,首字母大写
|
|||
|
|
- 文件名使用小写,下划线分隔
|
|||
|
|
- 示例:`Product` → `product.go`
|
|||
|
|
|
|||
|
|
#### 仓储命名
|
|||
|
|
- 接口名称:`{Entity}Repository`
|
|||
|
|
- 实现名称:`Gorm{Entity}Repository`
|
|||
|
|
- 文件名:`{entity}_repository_interface.go`、`gorm_{entity}_repository.go`
|
|||
|
|
|
|||
|
|
#### 应用服务命名
|
|||
|
|
- 接口名称:`{Entity}ApplicationService`
|
|||
|
|
- 实现名称:`{Entity}ApplicationServiceImpl`
|
|||
|
|
- 文件名:`{entity}_application_service.go`、`{entity}_application_service_impl.go`
|
|||
|
|
|
|||
|
|
#### HTTP处理器命名
|
|||
|
|
- 结构体名称:`{Entity}Handler`
|
|||
|
|
- 文件名:`{entity}_handler.go`
|
|||
|
|
|
|||
|
|
### 2. 错误处理
|
|||
|
|
|
|||
|
|
#### 统一错误响应
|
|||
|
|
```go
|
|||
|
|
// 使用统一的响应构建器
|
|||
|
|
h.responseBuilder.BadRequest(c, "请求参数错误")
|
|||
|
|
h.responseBuilder.NotFound(c, "{Entity}不存在")
|
|||
|
|
h.responseBuilder.InternalError(c, "服务器内部错误")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 业务错误处理
|
|||
|
|
```go
|
|||
|
|
// 在应用服务中抛出业务错误
|
|||
|
|
if entity.IsDeleted() {
|
|||
|
|
return fmt.Errorf("{Entity}已被删除")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if !entity.IsEnabled {
|
|||
|
|
return fmt.Errorf("{Entity}已被禁用")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 日志记录
|
|||
|
|
|
|||
|
|
#### 结构化日志
|
|||
|
|
```go
|
|||
|
|
// 使用结构化字段
|
|||
|
|
logger.Info("创建{Entity}成功",
|
|||
|
|
zap.String("id", entity.ID),
|
|||
|
|
zap.String("name", entity.Name),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
logger.Error("删除{Entity}失败",
|
|||
|
|
zap.Error(err),
|
|||
|
|
zap.String("id", id),
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 日志级别
|
|||
|
|
- `Info`: 业务操作成功
|
|||
|
|
- `Warn`: 业务警告(如重复操作)
|
|||
|
|
- `Error`: 业务错误和系统错误
|
|||
|
|
- `Debug`: 调试信息
|
|||
|
|
|
|||
|
|
### 4. 参数验证
|
|||
|
|
|
|||
|
|
#### 请求参数验证
|
|||
|
|
```go
|
|||
|
|
// 使用binding标签进行验证
|
|||
|
|
type Create{Entity}Command struct {
|
|||
|
|
Name string `json:"name" binding:"required,min=1,max=100" comment:"名称"`
|
|||
|
|
Description string `json:"description" binding:"max=500" comment:"描述"`
|
|||
|
|
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 业务规则验证
|
|||
|
|
```go
|
|||
|
|
// 在应用服务中进行业务规则验证
|
|||
|
|
func (s *{Entity}ApplicationServiceImpl) validateCreate{Entity}(cmd *commands.Create{Entity}Command) error {
|
|||
|
|
if cmd.Name == "" {
|
|||
|
|
return fmt.Errorf("名称不能为空")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(cmd.Name) > 100 {
|
|||
|
|
return fmt.Errorf("名称长度不能超过100个字符")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5. 数据库设计
|
|||
|
|
|
|||
|
|
#### 表结构规范
|
|||
|
|
```go
|
|||
|
|
type {Entity} struct {
|
|||
|
|
ID string `gorm:"primaryKey;type:varchar(36)" comment:"ID"`
|
|||
|
|
Name string `gorm:"type:varchar(100);not null;index" comment:"名称"`
|
|||
|
|
Description string `gorm:"type:text" comment:"描述"`
|
|||
|
|
IsEnabled bool `gorm:"default:true;index" comment:"是否启用"`
|
|||
|
|
IsVisible bool `gorm:"default:true" comment:"是否可见"`
|
|||
|
|
|
|||
|
|
CreatedAt time.Time `gorm:"autoCreateTime;index" comment:"创建时间"`
|
|||
|
|
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
|
|||
|
|
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 索引设计
|
|||
|
|
- 主键:`ID`
|
|||
|
|
- 业务索引:`Name`、`IsEnabled`、`CreatedAt`
|
|||
|
|
- 软删除索引:`DeletedAt`
|
|||
|
|
|
|||
|
|
### 6. API设计
|
|||
|
|
|
|||
|
|
#### RESTful规范
|
|||
|
|
```
|
|||
|
|
GET /api/v1/{entity}s # 获取列表
|
|||
|
|
GET /api/v1/{entity}s/:id # 获取详情
|
|||
|
|
POST /api/v1/{entity}s # 创建
|
|||
|
|
PUT /api/v1/{entity}s/:id # 更新
|
|||
|
|
DELETE /api/v1/{entity}s/:id # 删除
|
|||
|
|
POST /api/v1/{entity}s/:id/enable # 启用
|
|||
|
|
POST /api/v1/{entity}s/:id/disable # 禁用
|
|||
|
|
GET /api/v1/{entity}s/stats # 统计
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 查询参数
|
|||
|
|
```
|
|||
|
|
GET /api/v1/{entity}s?page=1&page_size=10&status=active&sort_by=created_at&sort_order=desc
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 响应格式
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"message": "操作成功",
|
|||
|
|
"data": {
|
|||
|
|
"total": 100,
|
|||
|
|
"page": 1,
|
|||
|
|
"size": 10,
|
|||
|
|
"items": [...]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7. 性能优化
|
|||
|
|
|
|||
|
|
#### 分页查询
|
|||
|
|
```go
|
|||
|
|
// 使用LIMIT和OFFSET进行分页
|
|||
|
|
dbQuery = dbQuery.Offset(offset).Limit(pageSize)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 索引优化
|
|||
|
|
```go
|
|||
|
|
// 为常用查询字段添加索引
|
|||
|
|
`gorm:"index"` // 普通索引
|
|||
|
|
`gorm:"uniqueIndex"` // 唯一索引
|
|||
|
|
`gorm:"index:idx_name_status"` // 复合索引
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 缓存策略
|
|||
|
|
```go
|
|||
|
|
// 对于频繁查询的数据使用缓存
|
|||
|
|
func (r *Gorm{Entity}Repository) GetByID(ctx context.Context, id string) (entities.{Entity}, error) {
|
|||
|
|
// 先从缓存获取
|
|||
|
|
if cached, err := r.cache.Get(ctx, "entity:"+id); err == nil {
|
|||
|
|
return cached, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 从数据库获取
|
|||
|
|
var entity entities.{Entity}
|
|||
|
|
err := r.db.WithContext(ctx).Where("id = ?", id).First(&entity).Error
|
|||
|
|
if err != nil {
|
|||
|
|
return entity, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 存入缓存
|
|||
|
|
r.cache.Set(ctx, "entity:"+id, entity, time.Hour)
|
|||
|
|
return entity, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 常见问题
|
|||
|
|
|
|||
|
|
### 1. 循环依赖
|
|||
|
|
**问题**: 领域层和应用层之间出现循环依赖
|
|||
|
|
**解决**: 使用接口解耦,领域层定义接口,应用层实现接口
|
|||
|
|
|
|||
|
|
### 2. 事务管理
|
|||
|
|
**问题**: 跨仓储操作需要事务支持
|
|||
|
|
**解决**: 在应用服务层使用事务装饰器
|
|||
|
|
```go
|
|||
|
|
func (s *{Entity}ApplicationServiceImpl) Create{Entity}WithTransaction(ctx context.Context, cmd *commands.Create{Entity}Command) error {
|
|||
|
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
|||
|
|
// 事务操作
|
|||
|
|
return nil
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 并发控制
|
|||
|
|
**问题**: 高并发场景下的数据一致性问题
|
|||
|
|
**解决**: 使用乐观锁或悲观锁
|
|||
|
|
```go
|
|||
|
|
// 乐观锁
|
|||
|
|
type {Entity} struct {
|
|||
|
|
Version int `gorm:"default:1" comment:"版本号"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 悲观锁
|
|||
|
|
func (r *Gorm{Entity}Repository) GetByIDForUpdate(ctx context.Context, id string) (entities.{Entity}, error) {
|
|||
|
|
var entity entities.{Entity}
|
|||
|
|
err := r.db.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", id).First(&entity).Error
|
|||
|
|
return entity, err
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 数据迁移
|
|||
|
|
**问题**: 实体结构变更需要数据库迁移
|
|||
|
|
**解决**: 使用GORM的AutoMigrate或手动编写迁移脚本
|
|||
|
|
```go
|
|||
|
|
// 自动迁移
|
|||
|
|
db.AutoMigrate(&entities.{Entity}{})
|
|||
|
|
|
|||
|
|
// 手动迁移
|
|||
|
|
db.Exec("ALTER TABLE {entity}s ADD COLUMN new_field VARCHAR(255)")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 总结
|
|||
|
|
|
|||
|
|
本文档提供了在tyapi-server项目中新增领域或领域内功能的完整指南。遵循DDD架构模式,确保代码的可维护性、可扩展性和可测试性。
|
|||
|
|
|
|||
|
|
关键要点:
|
|||
|
|
1. **分层架构**: 严格遵循领域层、应用层、基础设施层的分层原则
|
|||
|
|
2. **依赖倒置**: 通过接口实现依赖倒置,降低耦合度
|
|||
|
|
3. **单一职责**: 每个组件只负责自己的职责
|
|||
|
|
4. **开闭原则**: 对扩展开放,对修改关闭
|
|||
|
|
5. **测试驱动**: 编写充分的单元测试和集成测试
|
|||
|
|
|
|||
|
|
通过遵循本指南,可以快速、规范地在项目中新增领域功能,保持代码的一致性和质量。
|