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"`
}
通过本节的学习,我们完成了商品服务的核心功能实现,包括商品管理、分类管理、库存管理和搜索功能。下一节将实现订单服务。