5.9.3 商品服务开发

5.9.3 商品服务开发 #

商品服务是电商系统的核心服务之一,负责商品信息管理、分类管理、库存管理和商品搜索功能。本节将详细介绍如何设计和实现一个完整的商品服务。

服务设计概述 #

核心功能 #

商品服务需要支持以下核心功能:

  • 商品管理:商品的增删改查、状态管理
  • 分类管理:商品分类的层级管理
  • 库存管理:库存数量的实时管理
  • 价格管理:商品价格和促销价格管理
  • 搜索功能:基于多维度的商品搜索
  • 图片管理:商品图片的上传和管理

技术架构 #

┌─────────────────────────────────────────────────────────────┐
│                    Product Service                          │
├─────────────────────────────────────────────────────────────┤
│  HTTP API  │  gRPC API  │  Event Handlers  │  Schedulers   │
├─────────────────────────────────────────────────────────────┤
│           Application Layer (CQRS)                          │
│  Commands  │  Queries   │  Event Handlers  │  Sagas        │
├─────────────────────────────────────────────────────────────┤
│                  Domain Layer                               │
│  Entities  │  Value Objects │ Domain Services │ Events     │
├─────────────────────────────────────────────────────────────┤
│               Infrastructure Layer                          │
│ PostgreSQL │   Redis    │ Elasticsearch │  File Storage   │
└─────────────────────────────────────────────────────────────┘

领域模型设计 #

商品聚合 #

// internal/domain/entity/product.go
package entity

import (
    "errors"
    "time"
)

// 商品聚合根
type Product struct {
    ID          string           `json:"id"`
    Name        string           `json:"name"`
    Description string           `json:"description"`
    SKU         string           `json:"sku"`
    Brand       Brand            `json:"brand"`
    Category    Category         `json:"category"`
    Price       Money            `json:"price"`
    SalePrice   *Money           `json:"sale_price,omitempty"`
    Images      []ProductImage   `json:"images"`
    Attributes  []ProductAttribute `json:"attributes"`
    Variants    []ProductVariant `json:"variants"`
    Inventory   Inventory        `json:"inventory"`
    Status      ProductStatus    `json:"status"`
    Tags        []string         `json:"tags"`
    Weight      Weight           `json:"weight"`
    Dimensions  Dimensions       `json:"dimensions"`
    CreatedAt   time.Time        `json:"created_at"`
    UpdatedAt   time.Time        `json:"updated_at"`

    // 领域事件
    events []DomainEvent
}

// 品牌值对象
type Brand struct {
    ID   string `json:"id"`
    Name string `json:"name"`
    Logo string `json:"logo"`
}

// 分类值对象
type Category struct {
    ID       string `json:"id"`
    Name     string `json:"name"`
    Path     string `json:"path"`
    Level    int    `json:"level"`
    ParentID string `json:"parent_id,omitempty"`
}

// 货币值对象
type Money struct {
    Amount   int64  `json:"amount"`   // 以分为单位存储
    Currency string `json:"currency"` // 货币代码,如 CNY, USD
}

// 商品图片值对象
type ProductImage struct {
    ID       string `json:"id"`
    URL      string `json:"url"`
    Alt      string `json:"alt"`
    IsPrimary bool  `json:"is_primary"`
    SortOrder int   `json:"sort_order"`
}

// 商品属性值对象
type ProductAttribute struct {
    Name  string `json:"name"`
    Value string `json:"value"`
    Type  AttributeType `json:"type"`
}

type AttributeType int

const (
    AttributeTypeText AttributeType = iota + 1
    AttributeTypeNumber
    AttributeTypeBoolean
    AttributeTypeDate
)

// 商品变体值对象
type ProductVariant struct {
    ID         string                `json:"id"`
    Name       string                `json:"name"`
    SKU        string                `json:"sku"`
    Price      Money                 `json:"price"`
    Inventory  Inventory             `json:"inventory"`
    Attributes []ProductAttribute    `json:"attributes"`
    Images     []ProductImage        `json:"images"`
    Status     ProductVariantStatus  `json:"status"`
}

type ProductVariantStatus int

const (
    ProductVariantStatusActive ProductVariantStatus = iota + 1
    ProductVariantStatusInactive
    ProductVariantStatusOutOfStock
)

// 库存值对象
type Inventory struct {
    Quantity    int `json:"quantity"`
    Reserved    int `json:"reserved"`
    Available   int `json:"available"`
    MinQuantity int `json:"min_quantity"`
    MaxQuantity int `json:"max_quantity"`
}

// 重量值对象
type Weight struct {
    Value float64 `json:"value"`
    Unit  string  `json:"unit"` // kg, g, lb, oz
}

// 尺寸值对象
type Dimensions struct {
    Length float64 `json:"length"`
    Width  float64 `json:"width"`
    Height float64 `json:"height"`
    Unit   string  `json:"unit"` // cm, m, in, ft
}

type ProductStatus int

const (
    ProductStatusDraft ProductStatus = iota + 1
    ProductStatusActive
    ProductStatusInactive
    ProductStatusDiscontinued
    ProductStatusOutOfStock
)

// 创建新商品
func NewProduct(name, description, sku string, brand Brand, category Category, price Money) (*Product, error) {
    if err := validateProductInput(name, description, sku); err != nil {
        return nil, err
    }

    if err := validatePrice(price); err != nil {
        return nil, err
    }

    product := &Product{
        ID:          generateID(),
        Name:        name,
        Description: description,
        SKU:         sku,
        Brand:       brand,
        Category:    category,
        Price:       price,
        Images:      make([]ProductImage, 0),
        Attributes:  make([]ProductAttribute, 0),
        Variants:    make([]ProductVariant, 0),
        Inventory:   Inventory{Available: 0},
        Status:      ProductStatusDraft,
        Tags:        make([]string, 0),
        CreatedAt:   time.Now(),
        UpdatedAt:   time.Now(),
        events:      make([]DomainEvent, 0),
    }

    product.addEvent(NewProductCreatedEvent(product.ID, product.Name, product.SKU))
    return product, nil
}

// 更新商品价格
func (p *Product) UpdatePrice(newPrice Money) error {
    if err := validatePrice(newPrice); err != nil {
        return err
    }

    if p.Status == ProductStatusDiscontinued {
        return errors.New("cannot update price for discontinued product")
    }

    oldPrice := p.Price
    p.Price = newPrice
    p.UpdatedAt = time.Now()

    p.addEvent(NewProductPriceUpdatedEvent(p.ID, oldPrice, newPrice))
    return nil
}

// 设置促销价格
func (p *Product) SetSalePrice(salePrice Money) error {
    if err := validatePrice(salePrice); err != nil {
        return err
    }

    if salePrice.Amount >= p.Price.Amount {
        return errors.New("sale price must be lower than regular price")
    }

    p.SalePrice = &salePrice
    p.UpdatedAt = time.Now()

    p.addEvent(NewProductSalePriceSetEvent(p.ID, salePrice))
    return nil
}

// 清除促销价格
func (p *Product) ClearSalePrice() {
    if p.SalePrice != nil {
        p.SalePrice = nil
        p.UpdatedAt = time.Now()
        p.addEvent(NewProductSalePriceClearedEvent(p.ID))
    }
}

// 添加商品图片
func (p *Product) AddImage(image ProductImage) error {
    if image.URL == "" {
        return errors.New("image URL is required")
    }

    // 如果是主图,将其他图片设为非主图
    if image.IsPrimary {
        for i := range p.Images {
            p.Images[i].IsPrimary = false
        }
    }

    p.Images = append(p.Images, image)
    p.UpdatedAt = time.Now()

    p.addEvent(NewProductImageAddedEvent(p.ID, image))
    return nil
}

// 移除商品图片
func (p *Product) RemoveImage(imageID string) error {
    for i, img := range p.Images {
        if img.ID == imageID {
            p.Images = append(p.Images[:i], p.Images[i+1:]...)
            p.UpdatedAt = time.Now()
            p.addEvent(NewProductImageRemovedEvent(p.ID, imageID))
            return nil
        }
    }

    return errors.New("image not found")
}

// 更新库存
func (p *Product) UpdateInventory(quantity int) error {
    if quantity < 0 {
        return errors.New("inventory quantity cannot be negative")
    }

    oldQuantity := p.Inventory.Quantity
    p.Inventory.Quantity = quantity
    p.Inventory.Available = quantity - p.Inventory.Reserved

    // 更新商品状态
    if p.Inventory.Available <= 0 && p.Status == ProductStatusActive {
        p.Status = ProductStatusOutOfStock
    } else if p.Inventory.Available > 0 && p.Status == ProductStatusOutOfStock {
        p.Status = ProductStatusActive
    }

    p.UpdatedAt = time.Now()

    p.addEvent(NewProductInventoryUpdatedEvent(p.ID, oldQuantity, quantity))
    return nil
}

// 预留库存
func (p *Product) ReserveInventory(quantity int) error {
    if quantity <= 0 {
        return errors.New("reserve quantity must be positive")
    }

    if p.Inventory.Available < quantity {
        return errors.New("insufficient inventory")
    }

    p.Inventory.Reserved += quantity
    p.Inventory.Available -= quantity
    p.UpdatedAt = time.Now()

    p.addEvent(NewProductInventoryReservedEvent(p.ID, quantity))
    return nil
}

// 释放库存
func (p *Product) ReleaseInventory(quantity int) error {
    if quantity <= 0 {
        return errors.New("release quantity must be positive")
    }

    if p.Inventory.Reserved < quantity {
        return errors.New("insufficient reserved inventory")
    }

    p.Inventory.Reserved -= quantity
    p.Inventory.Available += quantity
    p.UpdatedAt = time.Now()

    p.addEvent(NewProductInventoryReleasedEvent(p.ID, quantity))
    return nil
}

// 激活商品
func (p *Product) Activate() error {
    if p.Status == ProductStatusActive {
        return errors.New("product already active")
    }

    if p.Inventory.Available <= 0 {
        return errors.New("cannot activate product with zero inventory")
    }

    p.Status = ProductStatusActive
    p.UpdatedAt = time.Now()

    p.addEvent(NewProductActivatedEvent(p.ID))
    return nil
}

// 停用商品
func (p *Product) Deactivate() error {
    if p.Status == ProductStatusInactive {
        return errors.New("product already inactive")
    }

    p.Status = ProductStatusInactive
    p.UpdatedAt = time.Now()

    p.addEvent(NewProductDeactivatedEvent(p.ID))
    return nil
}

// 添加商品变体
func (p *Product) AddVariant(variant ProductVariant) error {
    if variant.SKU == "" {
        return errors.New("variant SKU is required")
    }

    // 检查 SKU 是否重复
    for _, v := range p.Variants {
        if v.SKU == variant.SKU {
            return errors.New("variant SKU already exists")
        }
    }

    variant.ID = generateID()
    p.Variants = append(p.Variants, variant)
    p.UpdatedAt = time.Now()

    p.addEvent(NewProductVariantAddedEvent(p.ID, variant))
    return nil
}

// 获取有效价格(考虑促销价格)
func (p *Product) GetEffectivePrice() Money {
    if p.SalePrice != nil {
        return *p.SalePrice
    }
    return p.Price
}

// 检查是否有库存
func (p *Product) IsInStock() bool {
    return p.Inventory.Available > 0
}

// 获取领域事件
func (p *Product) GetEvents() []DomainEvent {
    return p.events
}

// 清除领域事件
func (p *Product) ClearEvents() {
    p.events = make([]DomainEvent, 0)
}

// 添加领域事件
func (p *Product) addEvent(event DomainEvent) {
    p.events = append(p.events, event)
}

// 验证商品输入
func validateProductInput(name, description, sku string) error {
    if name == "" {
        return errors.New("product name is required")
    }
    if len(name) > 255 {
        return errors.New("product name too long")
    }
    if description == "" {
        return errors.New("product description is required")
    }
    if sku == "" {
        return errors.New("product SKU is required")
    }

    return nil
}

// 验证价格
func validatePrice(price Money) error {
    if price.Amount <= 0 {
        return errors.New("price must be positive")
    }
    if price.Currency == "" {
        return errors.New("currency is required")
    }

    return nil
}

分类聚合 #

// internal/domain/entity/category.go
package entity

import (
    "errors"
    "time"
)

// 分类聚合根
type ProductCategory struct {
    ID          string    `json:"id"`
    Name        string    `json:"name"`
    Description string    `json:"description"`
    Slug        string    `json:"slug"`
    ParentID    string    `json:"parent_id,omitempty"`
    Level       int       `json:"level"`
    Path        string    `json:"path"`
    Image       string    `json:"image"`
    Icon        string    `json:"icon"`
    SortOrder   int       `json:"sort_order"`
    Status      CategoryStatus `json:"status"`
    Metadata    map[string]interface{} `json:"metadata"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`

    // 子分类
    Children []*ProductCategory `json:"children,omitempty"`

    // 领域事件
    events []DomainEvent
}

type CategoryStatus int

const (
    CategoryStatusActive CategoryStatus = iota + 1
    CategoryStatusInactive
    CategoryStatusDeleted
)

// 创建新分类
func NewProductCategory(name, description, slug string, parentID string) (*ProductCategory, error) {
    if err := validateCategoryInput(name, slug); err != nil {
        return nil, err
    }

    level := 1
    path := slug

    // 如果有父分类,需要计算层级和路径
    if parentID != "" {
        // 这里需要通过领域服务来获取父分类信息
        level = 2 // 简化处理,实际应该通过父分类计算
        path = "parent/" + slug
    }

    category := &ProductCategory{
        ID:          generateID(),
        Name:        name,
        Description: description,
        Slug:        slug,
        ParentID:    parentID,
        Level:       level,
        Path:        path,
        Status:      CategoryStatusActive,
        Metadata:    make(map[string]interface{}),
        CreatedAt:   time.Now(),
        UpdatedAt:   time.Now(),
        Children:    make([]*ProductCategory, 0),
        events:      make([]DomainEvent, 0),
    }

    category.addEvent(NewCategoryCreatedEvent(category.ID, category.Name, category.Path))
    return category, nil
}

// 更新分类信息
func (c *ProductCategory) Update(name, description string) error {
    if name == "" {
        return errors.New("category name is required")
    }

    c.Name = name
    c.Description = description
    c.UpdatedAt = time.Now()

    c.addEvent(NewCategoryUpdatedEvent(c.ID, name, description))
    return nil
}

// 移动分类
func (c *ProductCategory) MoveTo(newParentID string, newPath string, newLevel int) error {
    oldParentID := c.ParentID
    oldPath := c.Path

    c.ParentID = newParentID
    c.Path = newPath
    c.Level = newLevel
    c.UpdatedAt = time.Now()

    c.addEvent(NewCategoryMovedEvent(c.ID, oldParentID, newParentID, oldPath, newPath))
    return nil
}

// 添加子分类
func (c *ProductCategory) AddChild(child *ProductCategory) error {
    if child.ParentID != c.ID {
        return errors.New("child category parent ID mismatch")
    }

    c.Children = append(c.Children, child)
    c.UpdatedAt = time.Now()

    return nil
}

// 激活分类
func (c *ProductCategory) Activate() error {
    if c.Status == CategoryStatusActive {
        return errors.New("category already active")
    }

    c.Status = CategoryStatusActive
    c.UpdatedAt = time.Now()

    c.addEvent(NewCategoryActivatedEvent(c.ID))
    return nil
}

// 停用分类
func (c *ProductCategory) Deactivate() error {
    if c.Status == CategoryStatusInactive {
        return errors.New("category already inactive")
    }

    c.Status = CategoryStatusInactive
    c.UpdatedAt = time.Now()

    c.addEvent(NewCategoryDeactivatedEvent(c.ID))
    return nil
}

// 获取领域事件
func (c *ProductCategory) GetEvents() []DomainEvent {
    return c.events
}

// 清除领域事件
func (c *ProductCategory) ClearEvents() {
    c.events = make([]DomainEvent, 0)
}

// 添加领域事件
func (c *ProductCategory) addEvent(event DomainEvent) {
    c.events = append(c.events, event)
}

// 验证分类输入
func validateCategoryInput(name, slug string) error {
    if name == "" {
        return errors.New("category name is required")
    }
    if slug == "" {
        return errors.New("category slug is required")
    }
    if !isValidSlug(slug) {
        return errors.New("invalid slug format")
    }

    return nil
}

领域事件 #

// internal/domain/event/product_events.go
package event

import "time"

// 商品创建事件
type ProductCreatedEvent struct {
    BaseEvent
    Name string `json:"name"`
    SKU  string `json:"sku"`
}

func NewProductCreatedEvent(productID, name, sku string) *ProductCreatedEvent {
    return &ProductCreatedEvent{
        BaseEvent: BaseEvent{
            EventID:     generateEventID(),
            EventType:   "product.created",
            AggregateID: productID,
            OccurredAt:  time.Now(),
        },
        Name: name,
        SKU:  sku,
    }
}

// 商品价格更新事件
type ProductPriceUpdatedEvent struct {
    BaseEvent
    OldPrice Money `json:"old_price"`
    NewPrice Money `json:"new_price"`
}

func NewProductPriceUpdatedEvent(productID string, oldPrice, newPrice Money) *ProductPriceUpdatedEvent {
    return &ProductPriceUpdatedEvent{
        BaseEvent: BaseEvent{
            EventID:     generateEventID(),
            EventType:   "product.price.updated",
            AggregateID: productID,
            OccurredAt:  time.Now(),
        },
        OldPrice: oldPrice,
        NewPrice: newPrice,
    }
}

// 商品库存更新事件
type ProductInventoryUpdatedEvent struct {
    BaseEvent
    OldQuantity int `json:"old_quantity"`
    NewQuantity int `json:"new_quantity"`
}

func NewProductInventoryUpdatedEvent(productID string, oldQuantity, newQuantity int) *ProductInventoryUpdatedEvent {
    return &ProductInventoryUpdatedEvent{
        BaseEvent: BaseEvent{
            EventID:     generateEventID(),
            EventType:   "product.inventory.updated",
            AggregateID: productID,
            OccurredAt:  time.Now(),
        },
        OldQuantity: oldQuantity,
        NewQuantity: newQuantity,
    }
}

// 商品库存预留事件
type ProductInventoryReservedEvent struct {
    BaseEvent
    Quantity int `json:"quantity"`
}

func NewProductInventoryReservedEvent(productID string, quantity int) *ProductInventoryReservedEvent {
    return &ProductInventoryReservedEvent{
        BaseEvent: BaseEvent{
            EventID:     generateEventID(),
            EventType:   "product.inventory.reserved",
            AggregateID: productID,
            OccurredAt:  time.Now(),
        },
        Quantity: quantity,
    }
}

// 商品激活事件
type ProductActivatedEvent struct {
    BaseEvent
}

func NewProductActivatedEvent(productID string) *ProductActivatedEvent {
    return &ProductActivatedEvent{
        BaseEvent: BaseEvent{
            EventID:     generateEventID(),
            EventType:   "product.activated",
            AggregateID: productID,
            OccurredAt:  time.Now(),
        },
    }
}

// 分类创建事件
type CategoryCreatedEvent struct {
    BaseEvent
    Name string `json:"name"`
    Path string `json:"path"`
}

func NewCategoryCreatedEvent(categoryID, name, path string) *CategoryCreatedEvent {
    return &CategoryCreatedEvent{
        BaseEvent: BaseEvent{
            EventID:     generateEventID(),
            EventType:   "category.created",
            AggregateID: categoryID,
            OccurredAt:  time.Now(),
        },
        Name: name,
        Path: path,
    }
}

应用服务层 #

商品命令处理 #

// internal/application/command/product_commands.go
package command

import (
    "context"
    "time"
)

// 创建商品命令
type CreateProductCommand struct {
    Name        string                    `json:"name" validate:"required,max=255"`
    Description string                    `json:"description" validate:"required"`
    SKU         string                    `json:"sku" validate:"required,max=100"`
    BrandID     string                    `json:"brand_id" validate:"required"`
    CategoryID  string                    `json:"category_id" validate:"required"`
    Price       MoneyCommand              `json:"price" validate:"required"`
    Images      []ProductImageCommand     `json:"images"`
    Attributes  []ProductAttributeCommand `json:"attributes"`
    Weight      WeightCommand             `json:"weight"`
    Dimensions  DimensionsCommand         `json:"dimensions"`
    Tags        []string                  `json:"tags"`
}

type MoneyCommand struct {
    Amount   int64  `json:"amount" validate:"min=1"`
    Currency string `json:"currency" validate:"required,len=3"`
}

type ProductImageCommand struct {
    URL       string `json:"url" validate:"required,url"`
    Alt       string `json:"alt"`
    IsPrimary bool   `json:"is_primary"`
    SortOrder int    `json:"sort_order"`
}

type ProductAttributeCommand struct {
    Name  string `json:"name" validate:"required"`
    Value string `json:"value" validate:"required"`
    Type  int    `json:"type" validate:"min=1,max=4"`
}

type WeightCommand struct {
    Value float64 `json:"value" validate:"min=0"`
    Unit  string  `json:"unit" validate:"required"`
}

type DimensionsCommand struct {
    Length float64 `json:"length" validate:"min=0"`
    Width  float64 `json:"width" validate:"min=0"`
    Height float64 `json:"height" validate:"min=0"`
    Unit   string  `json:"unit" validate:"required"`
}

// 更新商品价格命令
type UpdateProductPriceCommand struct {
    ProductID string       `json:"product_id" validate:"required"`
    Price     MoneyCommand `json:"price" validate:"required"`
}

// 更新库存命令
type UpdateInventoryCommand struct {
    ProductID string `json:"product_id" validate:"required"`
    Quantity  int    `json:"quantity" validate:"min=0"`
}

// 预留库存命令
type ReserveInventoryCommand struct {
    ProductID string `json:"product_id" validate:"required"`
    Quantity  int    `json:"quantity" validate:"min=1"`
}

// 释放库存命令
type ReleaseInventoryCommand struct {
    ProductID string `json:"product_id" validate:"required"`
    Quantity  int    `json:"quantity" validate:"min=1"`
}

商品命令处理器 #

// internal/application/command/product_command_handler.go
package command

import (
    "context"
    "errors"

    "github.com/ecommerce/product-service/internal/domain/entity"
    "github.com/ecommerce/product-service/internal/domain/repository"
    "github.com/ecommerce/product-service/internal/domain/service"
    "github.com/ecommerce/product-service/pkg/validator"
)

type ProductCommandHandler struct {
    productRepo     repository.ProductRepository
    categoryRepo    repository.CategoryRepository
    brandRepo       repository.BrandRepository
    eventBus        EventBus
    validator       *validator.Validator
    domainService   *service.ProductDomainService
}

func NewProductCommandHandler(
    productRepo repository.ProductRepository,
    categoryRepo repository.CategoryRepository,
    brandRepo repository.BrandRepository,
    eventBus EventBus,
    validator *validator.Validator,
    domainService *service.ProductDomainService,
) *ProductCommandHandler {
    return &ProductCommandHandler{
        productRepo:   productRepo,
        categoryRepo:  categoryRepo,
        brandRepo:     brandRepo,
        eventBus:      eventBus,
        validator:     validator,
        domainService: domainService,
    }
}

// 处理创建商品命令
func (h *ProductCommandHandler) HandleCreateProduct(ctx context.Context, cmd *CreateProductCommand) (*CreateProductResult, error) {
    // 验证命令
    if err := h.validator.Validate(cmd); err != nil {
        return nil, err
    }

    // 检查 SKU 是否已存在
    if exists, err := h.domainService.IsSKUExists(ctx, cmd.SKU); err != nil {
        return nil, err
    } else if exists {
        return nil, errors.New("SKU already exists")
    }

    // 获取品牌信息
    brand, err := h.brandRepo.GetByID(ctx, cmd.BrandID)
    if err != nil {
        return nil, errors.New("brand not found")
    }

    // 获取分类信息
    category, err := h.categoryRepo.GetByID(ctx, cmd.CategoryID)
    if err != nil {
        return nil, errors.New("category not found")
    }

    // 转换价格
    price := entity.Money{
        Amount:   cmd.Price.Amount,
        Currency: cmd.Price.Currency,
    }

    // 创建商品实体
    product, err := entity.NewProduct(
        cmd.Name,
        cmd.Description,
        cmd.SKU,
        h.toBrandEntity(brand),
        h.toCategoryEntity(category),
        price,
    )
    if err != nil {
        return nil, err
    }

    // 添加图片
    for _, imgCmd := range cmd.Images {
        image := entity.ProductImage{
            ID:        generateID(),
            URL:       imgCmd.URL,
            Alt:       imgCmd.Alt,
            IsPrimary: imgCmd.IsPrimary,
            SortOrder: imgCmd.SortOrder,
        }
        if err := product.AddImage(image); err != nil {
            return nil, err
        }
    }

    // 添加属性
    for _, attrCmd := range cmd.Attributes {
        attr := entity.ProductAttribute{
            Name:  attrCmd.Name,
            Value: attrCmd.Value,
            Type:  entity.AttributeType(attrCmd.Type),
        }
        product.Attributes = append(product.Attributes, attr)
    }

    // 设置重量和尺寸
    if cmd.Weight.Value > 0 {
        product.Weight = entity.Weight{
            Value: cmd.Weight.Value,
            Unit:  cmd.Weight.Unit,
        }
    }

    if cmd.Dimensions.Length > 0 || cmd.Dimensions.Width > 0 || cmd.Dimensions.Height > 0 {
        product.Dimensions = entity.Dimensions{
            Length: cmd.Dimensions.Length,
            Width:  cmd.Dimensions.Width,
            Height: cmd.Dimensions.Height,
            Unit:   cmd.Dimensions.Unit,
        }
    }

    // 设置标签
    product.Tags = cmd.Tags

    // 保存商品
    if err := h.productRepo.Save(ctx, product); err != nil {
        return nil, err
    }

    // 发布领域事件
    for _, event := range product.GetEvents() {
        if err := h.eventBus.Publish(ctx, event); err != nil {
            log.Error("failed to publish event", err)
        }
    }

    product.ClearEvents()

    return &CreateProductResult{
        ProductID: product.ID,
        Name:      product.Name,
        SKU:       product.SKU,
    }, nil
}

// 处理更新商品价格命令
func (h *ProductCommandHandler) HandleUpdateProductPrice(ctx context.Context, cmd *UpdateProductPriceCommand) error {
    // 验证命令
    if err := h.validator.Validate(cmd); err != nil {
        return err
    }

    // 获取商品
    product, err := h.productRepo.GetByID(ctx, cmd.ProductID)
    if err != nil {
        return err
    }

    // 更新价格
    newPrice := entity.Money{
        Amount:   cmd.Price.Amount,
        Currency: cmd.Price.Currency,
    }

    if err := product.UpdatePrice(newPrice); err != nil {
        return err
    }

    // 保存更新
    if err := h.productRepo.Update(ctx, product); err != nil {
        return err
    }

    // 发布领域事件
    for _, event := range product.GetEvents() {
        if err := h.eventBus.Publish(ctx, event); err != nil {
            log.Error("failed to publish event", err)
        }
    }

    product.ClearEvents()
    return nil
}

// 处理更新库存命令
func (h *ProductCommandHandler) HandleUpdateInventory(ctx context.Context, cmd *UpdateInventoryCommand) error {
    // 验证命令
    if err := h.validator.Validate(cmd); err != nil {
        return err
    }

    // 获取商品
    product, err := h.productRepo.GetByID(ctx, cmd.ProductID)
    if err != nil {
        return err
    }

    // 更新库存
    if err := product.UpdateInventory(cmd.Quantity); err != nil {
        return err
    }

    // 保存更新
    if err := h.productRepo.Update(ctx, product); err != nil {
        return err
    }

    // 发布领域事件
    for _, event := range product.GetEvents() {
        if err := h.eventBus.Publish(ctx, event); err != nil {
            log.Error("failed to publish event", err)
        }
    }

    product.ClearEvents()
    return nil
}

// 处理预留库存命令
func (h *ProductCommandHandler) HandleReserveInventory(ctx context.Context, cmd *ReserveInventoryCommand) error {
    // 验证命令
    if err := h.validator.Validate(cmd); err != nil {
        return err
    }

    // 获取商品
    product, err := h.productRepo.GetByID(ctx, cmd.ProductID)
    if err != nil {
        return err
    }

    // 预留库存
    if err := product.ReserveInventory(cmd.Quantity); err != nil {
        return err
    }

    // 保存更新
    if err := h.productRepo.Update(ctx, product); err != nil {
        return err
    }

    // 发布领域事件
    for _, event := range product.GetEvents() {
        if err := h.eventBus.Publish(ctx, event); err != nil {
            log.Error("failed to publish event", err)
        }
    }

    product.ClearEvents()
    return nil
}

// 处理释放库存命令
func (h *ProductCommandHandler) HandleReleaseInventory(ctx context.Context, cmd *ReleaseInventoryCommand) error {
    // 验证命令
    if err := h.validator.Validate(cmd); err != nil {
        return err
    }

    // 获取商品
    product, err := h.productRepo.GetByID(ctx, cmd.ProductID)
    if err != nil {
        return err
    }

    // 释放库存
    if err := product.ReleaseInventory(cmd.Quantity); err != nil {
        return err
    }

    // 保存更新
    if err := h.productRepo.Update(ctx, product); err != nil {
        return err
    }

    // 发布领域事件
    for _, event := range product.GetEvents() {
        if err := h.eventBus.Publish(ctx, event); err != nil {
            log.Error("failed to publish event", err)
        }
    }

    product.ClearEvents()
    return nil
}

// 辅助方法
func (h *ProductCommandHandler) toBrandEntity(brand *BrandModel) entity.Brand {
    return entity.Brand{
        ID:   brand.ID,
        Name: brand.Name,
        Logo: brand.Logo,
    }
}

func (h *ProductCommandHandler) toCategoryEntity(category *CategoryModel) entity.Category {
    return entity.Category{
        ID:       category.ID,
        Name:     category.Name,
        Path:     category.Path,
        Level:    category.Level,
        ParentID: category.ParentID,
    }
}

// 结果类型
type CreateProductResult struct {
    ProductID string `json:"product_id"`
    Name      string `json:"name"`
    SKU       string `json:"sku"`
}

商品查询处理 #

// internal/application/query/product_query_handler.go
package query

import (
    "context"

    "github.com/ecommerce/product-service/internal/domain/repository"
)

type ProductQueryHandler struct {
    productRepo  repository.ProductRepository
    categoryRepo repository.CategoryRepository
    searchRepo   repository.ProductSearchRepository
}

func NewProductQueryHandler(
    productRepo repository.ProductRepository,
    categoryRepo repository.CategoryRepository,
    searchRepo repository.ProductSearchRepository,
) *ProductQueryHandler {
    return &ProductQueryHandler{
        productRepo:  productRepo,
        categoryRepo: categoryRepo,
        searchRepo:   searchRepo,
    }
}

// 获取商品详情
func (h *ProductQueryHandler) GetProductByID(ctx context.Context, productID string) (*ProductDTO, error) {
    product, err := h.productRepo.GetByID(ctx, productID)
    if err != nil {
        return nil, err
    }

    return h.toProductDTO(product), nil
}

// 获取商品列表
func (h *ProductQueryHandler) GetProducts(ctx context.Context, req *GetProductsRequest) (*ProductListResult, error) {
    products, err := h.productRepo.List(ctx, req.Offset, req.Limit, req.Filters)
    if err != nil {
        return nil, err
    }

    total, err := h.productRepo.Count(ctx, req.Filters)
    if err != nil {
        return nil, err
    }

    productDTOs := make([]*ProductDTO, len(products))
    for i, product := range products {
        productDTOs[i] = h.toProductDTO(product)
    }

    return &ProductListResult{
        Products: productDTOs,
        Total:    total,
        Offset:   req.Offset,
        Limit:    req.Limit,
    }, nil
}

// 搜索商品
func (h *ProductQueryHandler) SearchProducts(ctx context.Context, req *SearchProductsRequest) (*ProductListResult, error) {
    searchResult, err := h.searchRepo.Search(ctx, req.Query, req.Filters, req.Offset, req.Limit)
    if err != nil {
        return nil, err
    }

    // 根据搜索结果获取完整的商品信息
    productIDs := make([]string, len(searchResult.Products))
    for i, product := range searchResult.Products {
        productIDs[i] = product.ID
    }

    products, err := h.productRepo.GetByIDs(ctx, productIDs)
    if err != nil {
        return nil, err
    }

    productDTOs := make([]*ProductDTO, len(products))
    for i, product := range products {
        productDTOs[i] = h.toProductDTO(product)
    }

    return &ProductListResult{
        Products: productDTOs,
        Total:    searchResult.Total,
        Offset:   req.Offset,
        Limit:    req.Limit,
    }, nil
}

// 获取分类商品
func (h *ProductQueryHandler) GetProductsByCategory(ctx context.Context, categoryID string, offset, limit int) (*ProductListResult, error) {
    filters := map[string]interface{}{
        "category_id": categoryID,
        "status":      entity.ProductStatusActive,
    }

    products, err := h.productRepo.List(ctx, offset, limit, filters)
    if err != nil {
        return nil, err
    }

    total, err := h.productRepo.Count(ctx, filters)
    if err != nil {
        return nil, err
    }

    productDTOs := make([]*ProductDTO, len(products))
    for i, product := range products {
        productDTOs[i] = h.toProductDTO(product)
    }

    return &ProductListResult{
        Products: productDTOs,
        Total:    total,
        Offset:   offset,
        Limit:    limit,
    }, nil
}

// 转换为 DTO
func (h *ProductQueryHandler) toProductDTO(product *entity.Product) *ProductDTO {
    return &ProductDTO{
        ID:          product.ID,
        Name:        product.Name,
        Description: product.Description,
        SKU:         product.SKU,
        Brand: BrandDTO{
            ID:   product.Brand.ID,
            Name: product.Brand.Name,
            Logo: product.Brand.Logo,
        },
        Category: CategoryDTO{
            ID:       product.Category.ID,
            Name:     product.Category.Name,
            Path:     product.Category.Path,
            Level:    product.Category.Level,
            ParentID: product.Category.ParentID,
        },
        Price: MoneyDTO{
            Amount:   product.Price.Amount,
            Currency: product.Price.Currency,
        },
        SalePrice: h.toMoneyDTOPtr(product.SalePrice),
        Images:    h.toImageDTOs(product.Images),
        Attributes: h.toAttributeDTOs(product.Attributes),
        Variants:   h.toVariantDTOs(product.Variants),
        Inventory: InventoryDTO{
            Quantity:    product.Inventory.Quantity,
            Reserved:    product.Inventory.Reserved,
            Available:   product.Inventory.Available,
            MinQuantity: product.Inventory.MinQuantity,
            MaxQuantity: product.Inventory.MaxQuantity,
        },
        Status:    int(product.Status),
        Tags:      product.Tags,
        Weight: WeightDTO{
            Value: product.Weight.Value,
            Unit:  product.Weight.Unit,
        },
        Dimensions: DimensionsDTO{
            Length: product.Dimensions.Length,
            Width:  product.Dimensions.Width,
            Height: product.Dimensions.Height,
            Unit:   product.Dimensions.Unit,
        },
        CreatedAt: product.CreatedAt,
        UpdatedAt: product.UpdatedAt,
    }
}

// DTO 类型定义
type ProductDTO struct {
    ID          string              `json:"id"`
    Name        string              `json:"name"`
    Description string              `json:"description"`
    SKU         string              `json:"sku"`
    Brand       BrandDTO            `json:"brand"`
    Category    CategoryDTO         `json:"category"`
    Price       MoneyDTO            `json:"price"`
    SalePrice   *MoneyDTO           `json:"sale_price,omitempty"`
    Images      []ProductImageDTO   `json:"images"`
    Attributes  []ProductAttributeDTO `json:"attributes"`
    Variants    []ProductVariantDTO `json:"variants"`
    Inventory   InventoryDTO        `json:"inventory"`
    Status      int                 `json:"status"`
    Tags        []string            `json:"tags"`
    Weight      WeightDTO           `json:"weight"`
    Dimensions  DimensionsDTO       `json:"dimensions"`
    CreatedAt   time.Time           `json:"created_at"`
    UpdatedAt   time.Time           `json:"updated_at"`
}

type BrandDTO struct {
    ID   string `json:"id"`
    Name string `json:"name"`
    Logo string `json:"logo"`
}

type CategoryDTO struct {
    ID       string `json:"id"`
    Name     string `json:"name"`
    Path     string `json:"path"`
    Level    int    `json:"level"`
    ParentID string `json:"parent_id,omitempty"`
}

type MoneyDTO struct {
    Amount   int64  `json:"amount"`
    Currency string `json:"currency"`
}

type ProductImageDTO struct {
    ID        string `json:"id"`
    URL       string `json:"url"`
    Alt       string `json:"alt"`
    IsPrimary bool   `json:"is_primary"`
    SortOrder int    `json:"sort_order"`
}

type ProductAttributeDTO struct {
    Name  string `json:"name"`
    Value string `json:"value"`
    Type  int    `json:"type"`
}

type ProductVariantDTO struct {
    ID         string                `json:"id"`
    Name       string                `json:"name"`
    SKU        string                `json:"sku"`
    Price      MoneyDTO              `json:"price"`
    Inventory  InventoryDTO          `json:"inventory"`
    Attributes []ProductAttributeDTO `json:"attributes"`
    Images     []ProductImageDTO     `json:"images"`
    Status     int                   `json:"status"`
}

type InventoryDTO struct {
    Quantity    int `json:"quantity"`
    Reserved    int `json:"reserved"`
    Available   int `json:"available"`
    MinQuantity int `json:"min_quantity"`
    MaxQuantity int `json:"max_quantity"`
}

type WeightDTO struct {
    Value float64 `json:"value"`
    Unit  string  `json:"unit"`
}

type DimensionsDTO struct {
    Length float64 `json:"length"`
    Width  float64 `json:"width"`
    Height float64 `json:"height"`
    Unit   string  `json:"unit"`
}

type ProductListResult struct {
    Products []*ProductDTO `json:"products"`
    Total    int64         `json:"total"`
    Offset   int           `json:"offset"`
    Limit    int           `json:"limit"`
}

type GetProductsRequest struct {
    Offset  int                    `json:"offset"`
    Limit   int                    `json:"limit"`
    Filters map[string]interface{} `json:"filters"`
}

type SearchProductsRequest struct {
    Query   string                 `json:"query"`
    Filters map[string]interface{} `json:"filters"`
    Offset  int                    `json:"offset"`
    Limit   int                    `json:"limit"`
}

基础设施层 #

搜索服务实现 #

// internal/infrastructure/search/elasticsearch_search_repository.go
package search

import (
    "context"
    "encoding/json"
    "fmt"
    "strings"

    "github.com/elastic/go-elasticsearch/v8"
    "github.com/elastic/go-elasticsearch/v8/esapi"
    "github.com/ecommerce/product-service/internal/domain/repository"
)

type elasticsearchSearchRepository struct {
    client *elasticsearch.Client
    index  string
}

func NewElasticsearchSearchRepository(client *elasticsearch.Client, index string) repository.ProductSearchRepository {
    return &elasticsearchSearchRepository{
        client: client,
        index:  index,
    }
}

// 索引商品
func (r *elasticsearchSearchRepository) IndexProduct(ctx context.Context, product *SearchProduct) error {
    data, err := json.Marshal(product)
    if err != nil {
        return err
    }

    req := esapi.IndexRequest{
        Index:      r.index,
        DocumentID: product.ID,
        Body:       strings.NewReader(string(data)),
        Refresh:    "true",
    }

    res, err := req.Do(ctx, r.client)
    if err != nil {
        return err
    }
    defer res.Body.Close()

    if res.IsError() {
        return fmt.Errorf("error indexing product: %s", res.String())
    }

    return nil
}

// 搜索商品
func (r *elasticsearchSearchRepository) Search(ctx context.Context, query string, filters map[string]interface{}, offset, limit int) (*SearchResult, error) {
    searchQuery := r.buildSearchQuery(query, filters, offset, limit)

    data, err := json.Marshal(searchQuery)
    if err != nil {
        return nil, err
    }

    req := esapi.SearchRequest{
        Index: []string{r.index},
        Body:  strings.NewReader(string(data)),
    }

    res, err := req.Do(ctx, r.client)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()

    if res.IsError() {
        return nil, fmt.Errorf("error searching products: %s", res.String())
    }

    var searchResponse ElasticsearchResponse
    if err := json.NewDecoder(res.Body).Decode(&searchResponse); err != nil {
        return nil, err
    }

    return r.parseSearchResponse(&searchResponse), nil
}

// 构建搜索查询
func (r *elasticsearchSearchRepository) buildSearchQuery(query string, filters map[string]interface{}, offset, limit int) map[string]interface{} {
    searchQuery := map[string]interface{}{
        "from": offset,
        "size": limit,
        "query": map[string]interface{}{
            "bool": map[string]interface{}{
                "must": []interface{}{},
                "filter": []interface{}{},
            },
        },
        "sort": []interface{}{
            map[string]interface{}{
                "_score": map[string]interface{}{
                    "order": "desc",
                },
            },
            map[string]interface{}{
                "created_at": map[string]interface{}{
                    "order": "desc",
                },
            },
        },
    }

    boolQuery := searchQuery["query"].(map[string]interface{})["bool"].(map[string]interface{})

    // 添加文本搜索
    if query != "" {
        boolQuery["must"] = append(boolQuery["must"].([]interface{}), map[string]interface{}{
            "multi_match": map[string]interface{}{
                "query": query,
                "fields": []string{
                    "name^3",
                    "description^2",
                    "brand.name^2",
                    "category.name",
                    "tags",
                    "attributes.value",
                },
                "type": "best_fields",
                "fuzziness": "AUTO",
            },
        })
    } else {
        boolQuery["must"] = append(boolQuery["must"].([]interface{}), map[string]interface{}{
            "match_all": map[string]interface{}{},
        })
    }

    // 添加过滤条件
    for key, value := range filters {
        switch key {
        case "category_id":
            boolQuery["filter"] = append(boolQuery["filter"].([]interface{}), map[string]interface{}{
                "term": map[string]interface{}{
                    "category.id": value,
                },
            })
        case "brand_id":
            boolQuery["filter"] = append(boolQuery["filter"].([]interface{}), map[string]interface{}{
                "term": map[string]interface{}{
                    "brand.id": value,
                },
            })
        case "status":
            boolQuery["filter"] = append(boolQuery["filter"].([]interface{}), map[string]interface{}{
                "term": map[string]interface{}{
                    "status": value,
                },
            })
        case "price_range":
            if priceRange, ok := value.(map[string]interface{}); ok {
                rangeQuery := map[string]interface{}{
                    "range": map[string]interface{}{
                        "price.amount": map[string]interface{}{},
                    },
                }

                if min, exists := priceRange["min"]; exists {
                    rangeQuery["range"].(map[string]interface{})["price.amount"].(map[string]interface{})["gte"] = min
                }

                if max, exists := priceRange["max"]; exists {
                    rangeQuery["range"].(map[string]interface{})["price.amount"].(map[string]interface{})["lte"] = max
                }

                boolQuery["filter"] = append(boolQuery["filter"].([]interface{}), rangeQuery)
            }
        case "in_stock":
            if inStock, ok := value.(bool); ok && inStock {
                boolQuery["filter"] = append(boolQuery["filter"].([]interface{}), map[string]interface{}{
                    "range": map[string]interface{}{
                        "inventory.available": map[string]interface{}{
                            "gt": 0,
                        },
                    },
                })
            }
        }
    }

    return searchQuery
}

// 解析搜索响应
func (r *elasticsearchSearchRepository) parseSearchResponse(response *ElasticsearchResponse) *SearchResult {
    products := make([]*SearchProduct, len(response.Hits.Hits))

    for i, hit := range response.Hits.Hits {
        var product SearchProduct
        json.Unmarshal(hit.Source, &product)
        products[i] = &product
    }

    return &SearchResult{
        Products: products,
        Total:    response.Hits.Total.Value,
    }
}

// Elasticsearch 响应结构
type ElasticsearchResponse struct {
    Hits struct {
        Total struct {
            Value int64 `json:"value"`
        } `json:"total"`
        Hits []struct {
            Source json.RawMessage `json:"_source"`
        } `json:"hits"`
    } `json:"hits"`
}

// 搜索商品结构
type SearchProduct struct {
    ID          string      `json:"id"`
    Name        string      `json:"name"`
    Description string      `json:"description"`
    SKU         string      `json:"sku"`
    Brand       SearchBrand `json:"brand"`
    Category    SearchCategory `json:"category"`
    Price       SearchMoney `json:"price"`
    SalePrice   *SearchMoney `json:"sale_price,omitempty"`
    Images      []SearchImage `json:"images"`
    Attributes  []SearchAttribute `json:"attributes"`
    Inventory   SearchInventory `json:"inventory"`
    Status      int         `json:"status"`
    Tags        []string    `json:"tags"`
    CreatedAt   time.Time   `json:"created_at"`
    UpdatedAt   time.Time   `json:"updated_at"`
}

type SearchBrand struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

type SearchCategory struct {
    ID   string `json:"id"`
    Name string `json:"name"`
    Path string `json:"path"`
}

type SearchMoney struct {
    Amount   int64  `json:"amount"`
    Currency string `json:"currency"`
}

type SearchImage struct {
    URL       string `json:"url"`
    IsPrimary bool   `json:"is_primary"`
}

type SearchAttribute struct {
    Name  string `json:"name"`
    Value string `json:"value"`
}

type SearchInventory struct {
    Available int `json:"available"`
}

type SearchResult struct {
    Products []*SearchProduct `json:"products"`
    Total    int64            `json:"total"`
}

通过本节的学习,我们完成了商品服务的核心功能实现,包括商品管理、分类管理、库存管理和搜索功能。下一节将实现订单服务。