gin + es 实践 02

发布于:2025-05-09 ⋅ 阅读:(25) ⋅ 点赞:(0)

领域模型设计

核心领域概念

在 Go-ES 项目中,我们采用了领域驱动设计(DDD)方法论来构建产品管理系统的核心模型。以下是本项目中的核心领域概念:

产品(Product)

产品是本系统的核心聚合根,它包含以下属性:

  • ID: 产品唯一标识符
  • 名称(Name): 产品名称
  • 描述(Description): 产品详细描述
  • 价格(Price): 产品价格
  • 类别(Category): 产品所属类别
  • 标签(Tags): 产品关联的标签集合
  • 创建时间(CreatedAt): 产品创建时间
  • 更新时间(UpdatedAt): 产品最后更新时间

领域实体设计

Product 实体

产品实体是系统的核心,它在 domain/entity/product.go 中定义:

// Product 产品实体
type Product struct {
    ID          string    `json:"id" gorm:"type:varchar(36);primary_key"`
    Name        string    `json:"name" gorm:"type:varchar(255);not null"`
    Description string    `json:"description" gorm:"type:text"`
    Price       float64   `json:"price" gorm:"type:decimal(10,2);not null"`
    Category    string    `json:"category" gorm:"type:varchar(100);index"`
    Tags        []string  `json:"tags" gorm:"-"`
    TagsString  string    `json:"-" gorm:"type:text"` 
    CreatedAt   time.Time `json:"created_at" gorm:"autoCreateTime"`
    UpdatedAt   time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}

实体具有以下特点:

  1. 标识唯一性:使用 UUID 作为产品的唯一标识符
  2. 业务完整性:包含产品的所有业务属性
  3. 持久化映射:通过 GORM 标签定义与数据库的映射关系
  4. 标签处理:将字符串数组与数据库字符串之间进行转换

实体方法

产品实体提供了以下方法:

1. 实体创建

NewProduct 方法用于创建新产品实例:

// NewProduct 创建新产品
func NewProduct(name, description string, price float64, category string, tags []string) *Product {
    return &Product{
        ID:          uuid.New().String(),
        Name:        name,
        Description: description,
        Price:       price,
        Category:    category,
        Tags:        tags,
        CreatedAt:   time.Now(),
        UpdatedAt:   time.Now(),
    }
}
2. 持久化钩子

实体提供了 GORM 钩子方法,在保存和加载时处理标签转换:

// BeforeSave 保存前的钩子
func (p *Product) BeforeSave() error {
    // 将标签数组转换为字符串存储
    if len(p.Tags) > 0 {
        // 简单地使用逗号分隔标签
        tagsStr := ""
        for i, tag := range p.Tags {
            if i > 0 {
                tagsStr += ","
            }
            tagsStr += tag
        }
        p.TagsString = tagsStr
    }
    return nil
}

// AfterFind 查询后的钩子
func (p *Product) AfterFind() error {
    // 将标签字符串转换为数组
    if p.TagsString != "" {
        p.Tags = splitTags(p.TagsString)
    }
    return nil
}
3. ES 文档转换

ToMap 方法将产品实体转换为适合 Elasticsearch 存储的映射:

// ToMap 将产品转换为 map,用于 ES 文档
func (p *Product) ToMap() map[string]interface{} {
    return map[string]interface{}{
        "id":          p.ID,
        "name":        p.Name,
        "description": p.Description,
        "price":       p.Price,
        "category":    p.Category,
        "tags":        p.Tags,
        "created_at":  p.CreatedAt.Format(time.RFC3339),
        "updated_at":  p.UpdatedAt.Format(time.RFC3339),
    }
}

领域服务设计

领域服务封装了跨实体的业务逻辑,在 domain/service/product_service.go 中实现:

// ProductService 产品领域服务
type ProductService struct {
    productRepo repository.ProductRepository
}

领域服务提供以下核心功能:

1. 产品创建

// CreateProduct 创建产品
func (s *ProductService) CreateProduct(ctx context.Context, product *entity.Product) error {
    // 领域验证
    if product.Name == "" || product.Price <= 0 {
        return ErrInvalidProduct
    }

    // 使用仓储保存产品
    err := s.productRepo.Create(ctx, product)
    if err != nil {
        return err
    }

    // 同步保存到 Elasticsearch
    return s.productRepo.IndexProduct(ctx, product)
}

2. 产品更新

// UpdateProduct 更新产品
func (s *ProductService) UpdateProduct(ctx context.Context, product *entity.Product) error {
    // 检查产品是否存在
    _, err := s.productRepo.FindByID(ctx, product.ID)
    if err != nil {
        return ErrProductNotFound
    }

    // 领域验证
    if product.Name == "" || product.Price <= 0 {
        return ErrInvalidProduct
    }

    // 更新产品
    err = s.productRepo.Update(ctx, product)
    if err != nil {
        return err
    }

    // 同步更新到 Elasticsearch
    return s.productRepo.IndexProduct(ctx, product)
}

3. 产品搜索

// SearchProducts 搜索产品
func (s *ProductService) SearchProducts(ctx context.Context, keyword string, category string, page, pageSize int) ([]*entity.Product, int64, error) {
    return s.productRepo.Search(ctx, keyword, category, page, pageSize)
}

仓储接口设计

仓储接口定义了领域实体的持久化操作,在 domain/repository/product_repository.go 中定义:

// ProductRepository 产品仓储接口
type ProductRepository interface {
    // 基本 CRUD 操作
    Create(ctx context.Context, product *entity.Product) error
    FindByID(ctx context.Context, id string) (*entity.Product, error)
    Update(ctx context.Context, product *entity.Product) error
    Delete(ctx context.Context, id string) error

    // 列表和搜索功能
    FindAll(ctx context.Context, page, pageSize int) ([]*entity.Product, int64, error)
    FindByCategory(ctx context.Context, category string, page, pageSize int) ([]*entity.Product, int64, error)

    // 搜索功能
    Search(ctx context.Context, keyword string, category string, page, pageSize int) ([]*entity.Product, int64, error)

    // Elasticsearch 相关操作
    IndexProduct(ctx context.Context, product *entity.Product) error
    DeleteFromIndex(ctx context.Context, id string) error

    // 重建索引
    ReindexAll(ctx context.Context) error
}

仓储接口的设计体现了以下原则:

  1. 依赖倒置原则:领域层定义接口,基础设施层负责实现
  2. 单一职责原则:接口方法专注于单一功能
  3. 完整性:包含所有必要的数据访问操作
  4. 领域无关性:不暴露技术实现细节给领域层

领域模型与数据存储的关系

在本项目中,领域模型与数据存储有两种映射关系:

  1. 关系型数据库(MySQL):通过 GORM 将 Product 实体映射到数据库表
  2. Elasticsearch:通过 ToMap() 方法将 Product 实体转换为 ES 文档

这种双重存储机制实现了:

  • 在关系型数据库中保证数据的完整性和一致性
  • 在 Elasticsearch 中提供高效的全文搜索能力

领域模型与存储之间的同步由仓储实现负责,确保两套存储系统的数据一致性。


网站公告

今日签到

点亮在社区的每一天
去签到