453 lines
14 KiB
Go
453 lines
14 KiB
Go
|
|
package product
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"tyapi-server/internal/application/product/dto/commands"
|
||
|
|
appQueries "tyapi-server/internal/application/product/dto/queries"
|
||
|
|
"tyapi-server/internal/application/product/dto/responses"
|
||
|
|
"tyapi-server/internal/domains/product/entities"
|
||
|
|
"tyapi-server/internal/domains/product/repositories"
|
||
|
|
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
|
||
|
|
"tyapi-server/internal/domains/product/services"
|
||
|
|
|
||
|
|
"go.uber.org/zap"
|
||
|
|
)
|
||
|
|
|
||
|
|
// ProductApplicationServiceImpl 产品应用服务实现
|
||
|
|
type ProductApplicationServiceImpl struct {
|
||
|
|
productRepo repositories.ProductRepository
|
||
|
|
categoryRepo repositories.ProductCategoryRepository
|
||
|
|
subscriptionRepo repositories.SubscriptionRepository
|
||
|
|
productService *services.ProductService
|
||
|
|
logger *zap.Logger
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewProductApplicationService 创建产品应用服务
|
||
|
|
func NewProductApplicationService(
|
||
|
|
productRepo repositories.ProductRepository,
|
||
|
|
categoryRepo repositories.ProductCategoryRepository,
|
||
|
|
subscriptionRepo repositories.SubscriptionRepository,
|
||
|
|
productService *services.ProductService,
|
||
|
|
logger *zap.Logger,
|
||
|
|
) ProductApplicationService {
|
||
|
|
return &ProductApplicationServiceImpl{
|
||
|
|
productRepo: productRepo,
|
||
|
|
categoryRepo: categoryRepo,
|
||
|
|
subscriptionRepo: subscriptionRepo,
|
||
|
|
productService: productService,
|
||
|
|
logger: logger,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// CreateProduct 创建产品
|
||
|
|
func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) error {
|
||
|
|
// 1. 参数验证
|
||
|
|
if err := s.validateCreateProduct(cmd); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. 验证产品编号唯一性
|
||
|
|
if err := s.productService.ValidateProductCode(cmd.Code, ""); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. 创建产品实体
|
||
|
|
product := &entities.Product{
|
||
|
|
Name: cmd.Name,
|
||
|
|
Code: cmd.Code,
|
||
|
|
Description: cmd.Description,
|
||
|
|
Content: cmd.Content,
|
||
|
|
CategoryID: cmd.CategoryID,
|
||
|
|
Price: cmd.Price,
|
||
|
|
IsEnabled: cmd.IsEnabled,
|
||
|
|
IsVisible: cmd.IsVisible,
|
||
|
|
IsPackage: cmd.IsPackage,
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. 设置SEO信息
|
||
|
|
product.UpdateSEO(cmd.SEOTitle, cmd.SEODescription, cmd.SEOKeywords)
|
||
|
|
|
||
|
|
// 5. 调用领域服务验证
|
||
|
|
if err := s.productService.ValidateProduct(product); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// 6. 保存到仓储
|
||
|
|
if _, err := s.productRepo.Create(ctx, *product); err != nil {
|
||
|
|
s.logger.Error("创建产品失败", zap.Error(err))
|
||
|
|
return fmt.Errorf("创建产品失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
s.logger.Info("创建产品成功", zap.String("id", product.ID), zap.String("name", product.Name))
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// UpdateProduct 更新产品
|
||
|
|
func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd *commands.UpdateProductCommand) error {
|
||
|
|
// 1. 获取现有产品
|
||
|
|
existingProduct, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("产品不存在: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. 参数验证
|
||
|
|
if err := s.validateUpdateProduct(cmd); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. 验证产品编号唯一性(如果修改了编号)
|
||
|
|
if cmd.Code != "" && cmd.Code != existingProduct.Code {
|
||
|
|
if err := s.productService.ValidateProductCode(cmd.Code, cmd.ID); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
existingProduct.Code = cmd.Code
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. 更新字段
|
||
|
|
if cmd.Name != "" {
|
||
|
|
existingProduct.Name = cmd.Name
|
||
|
|
}
|
||
|
|
if cmd.Description != "" {
|
||
|
|
existingProduct.Description = cmd.Description
|
||
|
|
}
|
||
|
|
if cmd.Content != "" {
|
||
|
|
existingProduct.Content = cmd.Content
|
||
|
|
}
|
||
|
|
if cmd.CategoryID != "" {
|
||
|
|
existingProduct.CategoryID = cmd.CategoryID
|
||
|
|
}
|
||
|
|
if cmd.Price >= 0 {
|
||
|
|
existingProduct.Price = cmd.Price
|
||
|
|
}
|
||
|
|
if cmd.IsEnabled != nil {
|
||
|
|
existingProduct.IsEnabled = *cmd.IsEnabled
|
||
|
|
}
|
||
|
|
if cmd.IsVisible != nil {
|
||
|
|
existingProduct.IsVisible = *cmd.IsVisible
|
||
|
|
}
|
||
|
|
if cmd.IsPackage != nil {
|
||
|
|
existingProduct.IsPackage = *cmd.IsPackage
|
||
|
|
}
|
||
|
|
|
||
|
|
// 5. 更新SEO信息
|
||
|
|
if cmd.SEOTitle != "" || cmd.SEODescription != "" || cmd.SEOKeywords != "" {
|
||
|
|
existingProduct.UpdateSEO(cmd.SEOTitle, cmd.SEODescription, cmd.SEOKeywords)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 6. 调用领域服务验证
|
||
|
|
if err := s.productService.ValidateProduct(&existingProduct); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// 7. 保存到仓储
|
||
|
|
if err := s.productRepo.Update(ctx, existingProduct); err != nil {
|
||
|
|
s.logger.Error("更新产品失败", zap.Error(err))
|
||
|
|
return fmt.Errorf("更新产品失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
s.logger.Info("更新产品成功", zap.String("id", existingProduct.ID))
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// DeleteProduct 删除产品
|
||
|
|
func (s *ProductApplicationServiceImpl) DeleteProduct(ctx context.Context, cmd *commands.DeleteProductCommand) error {
|
||
|
|
// 1. 检查产品是否存在
|
||
|
|
product, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("产品不存在: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. 检查是否有活跃订阅
|
||
|
|
subscriptions, err := s.subscriptionRepo.FindByProductID(ctx, cmd.ID)
|
||
|
|
if err == nil && len(subscriptions) > 0 {
|
||
|
|
return errors.New("产品存在订阅,无法删除")
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. 删除产品
|
||
|
|
if err := s.productRepo.Delete(ctx, cmd.ID); err != nil {
|
||
|
|
s.logger.Error("删除产品失败", zap.Error(err))
|
||
|
|
return fmt.Errorf("删除产品失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
s.logger.Info("删除产品成功", zap.String("id", cmd.ID), zap.String("name", product.Name))
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// ListProducts 获取产品列表
|
||
|
|
func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, query *appQueries.ListProductsQuery) (*responses.ProductListResponse, error) {
|
||
|
|
// 构建仓储查询
|
||
|
|
repoQuery := &repoQueries.ListProductsQuery{
|
||
|
|
Keyword: query.Keyword,
|
||
|
|
CategoryID: query.CategoryID,
|
||
|
|
MinPrice: query.MinPrice,
|
||
|
|
MaxPrice: query.MaxPrice,
|
||
|
|
IsEnabled: query.IsEnabled,
|
||
|
|
IsVisible: query.IsVisible,
|
||
|
|
IsPackage: query.IsPackage,
|
||
|
|
Page: query.Page,
|
||
|
|
PageSize: query.PageSize,
|
||
|
|
SortBy: query.SortBy,
|
||
|
|
SortOrder: query.SortOrder,
|
||
|
|
}
|
||
|
|
|
||
|
|
// 调用仓储
|
||
|
|
products, total, err := s.productRepo.ListProducts(ctx, repoQuery)
|
||
|
|
if err != nil {
|
||
|
|
s.logger.Error("获取产品列表失败", zap.Error(err))
|
||
|
|
return nil, fmt.Errorf("获取产品列表失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 转换为响应对象
|
||
|
|
items := make([]responses.ProductInfoResponse, len(products))
|
||
|
|
for i := range products {
|
||
|
|
items[i] = *s.convertToProductInfoResponse(products[i])
|
||
|
|
}
|
||
|
|
|
||
|
|
return &responses.ProductListResponse{
|
||
|
|
Total: total,
|
||
|
|
Page: query.Page,
|
||
|
|
Size: query.PageSize,
|
||
|
|
Items: items,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetProductsByIDs 根据ID列表获取产品
|
||
|
|
func (s *ProductApplicationServiceImpl) GetProductsByIDs(ctx context.Context, query *appQueries.GetProductsByIDsQuery) ([]*responses.ProductInfoResponse, error) {
|
||
|
|
products, err := s.productRepo.GetByIDs(ctx, query.IDs)
|
||
|
|
if err != nil {
|
||
|
|
s.logger.Error("获取产品列表失败", zap.Error(err))
|
||
|
|
return nil, fmt.Errorf("获取产品列表失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 转换为响应对象
|
||
|
|
items := make([]*responses.ProductInfoResponse, len(products))
|
||
|
|
for i := range products {
|
||
|
|
items[i] = s.convertToProductInfoResponse(&products[i])
|
||
|
|
}
|
||
|
|
|
||
|
|
return items, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetSubscribableProducts 获取可订阅的产品
|
||
|
|
func (s *ProductApplicationServiceImpl) GetSubscribableProducts(ctx context.Context, query *appQueries.GetSubscribableProductsQuery) ([]*responses.ProductInfoResponse, error) {
|
||
|
|
products, err := s.productRepo.FindSubscribableProducts(ctx, query.UserID)
|
||
|
|
if err != nil {
|
||
|
|
s.logger.Error("获取可订阅产品失败", zap.Error(err))
|
||
|
|
return nil, fmt.Errorf("获取可订阅产品失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 转换为响应对象
|
||
|
|
items := make([]*responses.ProductInfoResponse, len(products))
|
||
|
|
for i := range products {
|
||
|
|
items[i] = s.convertToProductInfoResponse(products[i])
|
||
|
|
}
|
||
|
|
|
||
|
|
return items, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetProductByID 根据ID获取产品
|
||
|
|
func (s *ProductApplicationServiceImpl) GetProductByID(ctx context.Context, query *appQueries.GetProductQuery) (*responses.ProductInfoResponse, error) {
|
||
|
|
var product *entities.Product
|
||
|
|
var err error
|
||
|
|
|
||
|
|
if query.ID != "" {
|
||
|
|
p, err := s.productRepo.GetByID(ctx, query.ID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("产品不存在: %w", err)
|
||
|
|
}
|
||
|
|
product = &p
|
||
|
|
} else if query.Code != "" {
|
||
|
|
product, err = s.productRepo.FindByCode(ctx, query.Code)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("产品不存在: %w", err)
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
return nil, errors.New("产品ID或编号不能为空")
|
||
|
|
}
|
||
|
|
|
||
|
|
// 转换为响应对象
|
||
|
|
response := s.convertToProductInfoResponse(product)
|
||
|
|
|
||
|
|
// 加载分类信息
|
||
|
|
if product.CategoryID != "" {
|
||
|
|
category, err := s.categoryRepo.GetByID(ctx, product.CategoryID)
|
||
|
|
if err == nil {
|
||
|
|
response.Category = s.convertToCategoryInfoResponse(&category)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return response, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// EnableProduct 启用产品
|
||
|
|
func (s *ProductApplicationServiceImpl) EnableProduct(ctx context.Context, cmd *commands.EnableProductCommand) error {
|
||
|
|
product, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("产品不存在: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
product.Enable()
|
||
|
|
|
||
|
|
if err := s.productRepo.Update(ctx, product); err != nil {
|
||
|
|
s.logger.Error("启用产品失败", zap.Error(err))
|
||
|
|
return fmt.Errorf("启用产品失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
s.logger.Info("启用产品成功", zap.String("id", cmd.ID))
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// DisableProduct 禁用产品
|
||
|
|
func (s *ProductApplicationServiceImpl) DisableProduct(ctx context.Context, cmd *commands.DisableProductCommand) error {
|
||
|
|
product, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("产品不存在: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
product.Disable()
|
||
|
|
|
||
|
|
if err := s.productRepo.Update(ctx, product); err != nil {
|
||
|
|
s.logger.Error("禁用产品失败", zap.Error(err))
|
||
|
|
return fmt.Errorf("禁用产品失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
s.logger.Info("禁用产品成功", zap.String("id", cmd.ID))
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// ShowProduct 显示产品
|
||
|
|
func (s *ProductApplicationServiceImpl) ShowProduct(ctx context.Context, cmd *commands.ShowProductCommand) error {
|
||
|
|
product, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("产品不存在: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
product.Show()
|
||
|
|
|
||
|
|
if err := s.productRepo.Update(ctx, product); err != nil {
|
||
|
|
s.logger.Error("显示产品失败", zap.Error(err))
|
||
|
|
return fmt.Errorf("显示产品失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
s.logger.Info("显示产品成功", zap.String("id", cmd.ID))
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// HideProduct 隐藏产品
|
||
|
|
func (s *ProductApplicationServiceImpl) HideProduct(ctx context.Context, cmd *commands.HideProductCommand) error {
|
||
|
|
product, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("产品不存在: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
product.Hide()
|
||
|
|
|
||
|
|
if err := s.productRepo.Update(ctx, product); err != nil {
|
||
|
|
s.logger.Error("隐藏产品失败", zap.Error(err))
|
||
|
|
return fmt.Errorf("隐藏产品失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
s.logger.Info("隐藏产品成功", zap.String("id", cmd.ID))
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// UpdateProductSEO 更新产品SEO信息
|
||
|
|
func (s *ProductApplicationServiceImpl) UpdateProductSEO(ctx context.Context, cmd *commands.UpdateProductSEOCommand) error {
|
||
|
|
product, err := s.productRepo.GetByID(ctx, cmd.ID)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("产品不存在: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
product.UpdateSEO(cmd.SEOTitle, cmd.SEODescription, cmd.SEOKeywords)
|
||
|
|
|
||
|
|
if err := s.productRepo.Update(ctx, product); err != nil {
|
||
|
|
s.logger.Error("更新产品SEO失败", zap.Error(err))
|
||
|
|
return fmt.Errorf("更新产品SEO失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
s.logger.Info("更新产品SEO成功", zap.String("id", cmd.ID))
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetProductStats 获取产品统计
|
||
|
|
func (s *ProductApplicationServiceImpl) GetProductStats(ctx context.Context) (*responses.ProductStatsResponse, error) {
|
||
|
|
stats, err := s.productService.GetProductStats()
|
||
|
|
if err != nil {
|
||
|
|
s.logger.Error("获取产品统计失败", zap.Error(err))
|
||
|
|
return nil, fmt.Errorf("获取产品统计失败: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return &responses.ProductStatsResponse{
|
||
|
|
TotalProducts: stats["total"],
|
||
|
|
EnabledProducts: stats["enabled"],
|
||
|
|
VisibleProducts: stats["visible"],
|
||
|
|
PackageProducts: 0, // 需要单独统计
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// 私有方法
|
||
|
|
|
||
|
|
func (s *ProductApplicationServiceImpl) validateCreateProduct(cmd *commands.CreateProductCommand) error {
|
||
|
|
if cmd.Name == "" {
|
||
|
|
return errors.New("产品名称不能为空")
|
||
|
|
}
|
||
|
|
if cmd.Code == "" {
|
||
|
|
return errors.New("产品编号不能为空")
|
||
|
|
}
|
||
|
|
if cmd.CategoryID == "" {
|
||
|
|
return errors.New("产品分类不能为空")
|
||
|
|
}
|
||
|
|
if cmd.Price < 0 {
|
||
|
|
return errors.New("产品价格不能为负数")
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *ProductApplicationServiceImpl) validateUpdateProduct(cmd *commands.UpdateProductCommand) error {
|
||
|
|
if cmd.ID == "" {
|
||
|
|
return errors.New("产品ID不能为空")
|
||
|
|
}
|
||
|
|
if cmd.Price < 0 {
|
||
|
|
return errors.New("产品价格不能为负数")
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse {
|
||
|
|
return &responses.ProductInfoResponse{
|
||
|
|
ID: product.ID,
|
||
|
|
Name: product.Name,
|
||
|
|
Code: product.Code,
|
||
|
|
Description: product.Description,
|
||
|
|
Content: product.Content,
|
||
|
|
CategoryID: product.CategoryID,
|
||
|
|
Price: product.Price,
|
||
|
|
IsEnabled: product.IsEnabled,
|
||
|
|
IsVisible: product.IsVisible,
|
||
|
|
IsPackage: product.IsPackage,
|
||
|
|
SEOTitle: product.SEOTitle,
|
||
|
|
SEODescription: product.SEODescription,
|
||
|
|
SEOKeywords: product.SEOKeywords,
|
||
|
|
CreatedAt: product.CreatedAt,
|
||
|
|
UpdatedAt: product.UpdatedAt,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *ProductApplicationServiceImpl) convertToCategoryInfoResponse(category *entities.ProductCategory) *responses.CategoryInfoResponse {
|
||
|
|
return &responses.CategoryInfoResponse{
|
||
|
|
ID: category.ID,
|
||
|
|
Name: category.Name,
|
||
|
|
Code: category.Code,
|
||
|
|
Description: category.Description,
|
||
|
|
ParentID: category.ParentID,
|
||
|
|
Level: category.Level,
|
||
|
|
Sort: category.Sort,
|
||
|
|
IsEnabled: category.IsEnabled,
|
||
|
|
IsVisible: category.IsVisible,
|
||
|
|
CreatedAt: category.CreatedAt,
|
||
|
|
UpdatedAt: category.UpdatedAt,
|
||
|
|
}
|
||
|
|
}
|