Files
tyapi-server/docs/DDD领域开发指南.md
2025-07-15 13:21:34 +08:00

1232 lines
37 KiB
Markdown
Raw Permalink 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.

# 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. **测试驱动**: 编写充分的单元测试和集成测试
通过遵循本指南,可以快速、规范地在项目中新增领域功能,保持代码的一致性和质量。