37 KiB
37 KiB
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 主实体文件
// 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 枚举文件(如需要)
// 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 仓储接口定义
// 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 查询对象
// 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 应用服务接口
// 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 应用服务实现
// 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
// 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
// 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
// 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仓储实现
// 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处理器
// 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 路由配置
// 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 容器配置
// 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. 实体测试
// 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. 应用服务测试
// 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处理器测试
// 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. 错误处理
统一错误响应
// 使用统一的响应构建器
h.responseBuilder.BadRequest(c, "请求参数错误")
h.responseBuilder.NotFound(c, "{Entity}不存在")
h.responseBuilder.InternalError(c, "服务器内部错误")
业务错误处理
// 在应用服务中抛出业务错误
if entity.IsDeleted() {
return fmt.Errorf("{Entity}已被删除")
}
if !entity.IsEnabled {
return fmt.Errorf("{Entity}已被禁用")
}
3. 日志记录
结构化日志
// 使用结构化字段
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. 参数验证
请求参数验证
// 使用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:"是否启用"`
}
业务规则验证
// 在应用服务中进行业务规则验证
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. 数据库设计
表结构规范
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
响应格式
{
"code": 200,
"message": "操作成功",
"data": {
"total": 100,
"page": 1,
"size": 10,
"items": [...]
}
}
7. 性能优化
分页查询
// 使用LIMIT和OFFSET进行分页
dbQuery = dbQuery.Offset(offset).Limit(pageSize)
索引优化
// 为常用查询字段添加索引
`gorm:"index"` // 普通索引
`gorm:"uniqueIndex"` // 唯一索引
`gorm:"index:idx_name_status"` // 复合索引
缓存策略
// 对于频繁查询的数据使用缓存
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. 事务管理
问题: 跨仓储操作需要事务支持 解决: 在应用服务层使用事务装饰器
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. 并发控制
问题: 高并发场景下的数据一致性问题 解决: 使用乐观锁或悲观锁
// 乐观锁
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或手动编写迁移脚本
// 自动迁移
db.AutoMigrate(&entities.{Entity}{})
// 手动迁移
db.Exec("ALTER TABLE {entity}s ADD COLUMN new_field VARCHAR(255)")
总结
本文档提供了在tyapi-server项目中新增领域或领域内功能的完整指南。遵循DDD架构模式,确保代码的可维护性、可扩展性和可测试性。
关键要点:
- 分层架构: 严格遵循领域层、应用层、基础设施层的分层原则
- 依赖倒置: 通过接口实现依赖倒置,降低耦合度
- 单一职责: 每个组件只负责自己的职责
- 开闭原则: 对扩展开放,对修改关闭
- 测试驱动: 编写充分的单元测试和集成测试
通过遵循本指南,可以快速、规范地在项目中新增领域功能,保持代码的一致性和质量。