diff --git a/internal/application/product/dto/commands/subscription_commands.go b/internal/application/product/dto/commands/subscription_commands.go index cbe3faa..c498115 100644 --- a/internal/application/product/dto/commands/subscription_commands.go +++ b/internal/application/product/dto/commands/subscription_commands.go @@ -14,7 +14,9 @@ type UpdateSubscriptionPriceCommand struct { // BatchUpdateSubscriptionPricesCommand 批量更新订阅价格命令 type BatchUpdateSubscriptionPricesCommand struct { - UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"` - Discount float64 `json:"discount" binding:"required,min=0.1,max=10" comment:"折扣比例(0.1-10折)"` - Scope string `json:"scope" binding:"required,oneof=undiscounted all" comment:"改价范围(undiscounted:仅未打折,all:所有)"` + UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"` + AdjustmentType string `json:"adjustment_type" binding:"required,oneof=discount cost_multiple" comment:"调整方式(discount:按售价折扣,cost_multiple:按成本价倍数)"` + Discount float64 `json:"discount,omitempty" binding:"omitempty,min=0.1,max=10" comment:"折扣比例(0.1-10折)"` + CostMultiple float64 `json:"cost_multiple,omitempty" binding:"omitempty,min=0.1" comment:"成本价倍数"` + Scope string `json:"scope" binding:"required,oneof=undiscounted all" comment:"改价范围(undiscounted:仅未打折,all:所有)"` } \ No newline at end of file diff --git a/internal/application/product/dto/responses/product_responses.go b/internal/application/product/dto/responses/product_responses.go index bcc7a45..e72fa52 100644 --- a/internal/application/product/dto/responses/product_responses.go +++ b/internal/application/product/dto/responses/product_responses.go @@ -70,6 +70,12 @@ type ProductSimpleResponse struct { IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"` } +// ProductSimpleAdminResponse 管理员产品简单信息响应(包含成本价) +type ProductSimpleAdminResponse struct { + ProductSimpleResponse + CostPrice float64 `json:"cost_price" comment:"成本价"` +} + // ProductStatsResponse 产品统计响应 type ProductStatsResponse struct { TotalProducts int64 `json:"total_products" comment:"产品总数"` diff --git a/internal/application/product/dto/responses/subscription_responses.go b/internal/application/product/dto/responses/subscription_responses.go index d9e8f8e..2fe520c 100644 --- a/internal/application/product/dto/responses/subscription_responses.go +++ b/internal/application/product/dto/responses/subscription_responses.go @@ -22,6 +22,8 @@ type SubscriptionInfoResponse struct { // 关联信息 User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"` Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"` + // 管理员端使用,包含成本价的产品信息 + ProductAdmin *ProductSimpleAdminResponse `json:"product_admin,omitempty" comment:"产品信息(管理员端,包含成本价)"` CreatedAt time.Time `json:"created_at" comment:"创建时间"` UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` diff --git a/internal/application/product/subscription_application_service_impl.go b/internal/application/product/subscription_application_service_impl.go index 3ab4c08..7d762dd 100644 --- a/internal/application/product/subscription_application_service_impl.go +++ b/internal/application/product/subscription_application_service_impl.go @@ -2,6 +2,7 @@ package product import ( "context" + "fmt" "github.com/shopspring/decimal" "go.uber.org/zap" @@ -45,6 +46,22 @@ func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context // BatchUpdateSubscriptionPrices 一键改价 // 业务流程:1. 获取用户所有订阅 2. 根据范围筛选 3. 批量更新价格 func (s *SubscriptionApplicationServiceImpl) BatchUpdateSubscriptionPrices(ctx context.Context, cmd *commands.BatchUpdateSubscriptionPricesCommand) error { + // 记录请求参数 + s.logger.Info("开始批量更新订阅价格", + zap.String("user_id", cmd.UserID), + zap.String("adjustment_type", cmd.AdjustmentType), + zap.Float64("discount", cmd.Discount), + zap.Float64("cost_multiple", cmd.CostMultiple), + zap.String("scope", cmd.Scope)) + + // 验证调整方式对应的参数 + if cmd.AdjustmentType == "discount" && cmd.Discount <= 0 { + return fmt.Errorf("按售价折扣调整时,折扣比例必须大于0") + } + if cmd.AdjustmentType == "cost_multiple" && cmd.CostMultiple <= 0 { + return fmt.Errorf("按成本价倍数调整时,倍数必须大于0") + } + subscriptions, _, err := s.productSubscriptionService.ListSubscriptions(ctx, &repoQueries.ListSubscriptionsQuery{ UserID: cmd.UserID, Page: 1, @@ -54,6 +71,9 @@ func (s *SubscriptionApplicationServiceImpl) BatchUpdateSubscriptionPrices(ctx c return err } + s.logger.Info("获取到订阅列表", + zap.Int("total_subscriptions", len(subscriptions))) + // 根据范围筛选订阅 var targetSubscriptions []*entities.Subscription for _, sub := range subscriptions { @@ -69,24 +89,64 @@ func (s *SubscriptionApplicationServiceImpl) BatchUpdateSubscriptionPrices(ctx c } // 批量更新价格 + updatedCount := 0 + skippedCount := 0 for _, sub := range targetSubscriptions { - if sub.Product != nil { - // 计算折扣后的价格 - discountRatio := cmd.Discount / 10 - newPrice := sub.Product.Price.Mul(decimal.NewFromFloat(discountRatio)) - // 四舍五入到2位小数 - newPrice = newPrice.Round(2) + if sub.Product == nil { + skippedCount++ + continue + } - err := s.productSubscriptionService.UpdateSubscriptionPrice(ctx, sub.ID, newPrice.InexactFloat64()) - if err != nil { - s.logger.Error("批量更新订阅价格失败", + var newPrice decimal.Decimal + + if cmd.AdjustmentType == "discount" { + // 按售价折扣调整 + discountRatio := cmd.Discount / 10 + newPrice = sub.Product.Price.Mul(decimal.NewFromFloat(discountRatio)) + } else if cmd.AdjustmentType == "cost_multiple" { + // 按成本价倍数调整 + // 检查成本价是否有效(必须大于0) + // 使用严格检查:成本价必须大于0 + if !sub.Product.CostPrice.GreaterThan(decimal.Zero) { + // 跳过没有成本价或成本价为0的产品 + skippedCount++ + s.logger.Info("跳过未设置成本价或成本价为0的订阅", zap.String("subscription_id", sub.ID), - zap.Error(err)) - // 继续处理其他订阅,不中断整个流程 + zap.String("product_id", sub.ProductID), + zap.String("product_name", sub.Product.Name), + zap.String("cost_price", sub.Product.CostPrice.String())) + continue } + // 计算成本价倍数后的价格 + newPrice = sub.Product.CostPrice.Mul(decimal.NewFromFloat(cmd.CostMultiple)) + } else { + s.logger.Warn("未知的调整方式", + zap.String("adjustment_type", cmd.AdjustmentType), + zap.String("subscription_id", sub.ID)) + skippedCount++ + continue + } + + // 四舍五入到2位小数 + newPrice = newPrice.Round(2) + + err := s.productSubscriptionService.UpdateSubscriptionPrice(ctx, sub.ID, newPrice.InexactFloat64()) + if err != nil { + s.logger.Error("批量更新订阅价格失败", + zap.String("subscription_id", sub.ID), + zap.Error(err)) + skippedCount++ + // 继续处理其他订阅,不中断整个流程 + } else { + updatedCount++ } } + s.logger.Info("批量更新订阅价格完成", + zap.Int("total", len(targetSubscriptions)), + zap.Int("updated", updatedCount), + zap.Int("skipped", skippedCount)) + return nil } @@ -129,7 +189,7 @@ func (s *SubscriptionApplicationServiceImpl) ListSubscriptions(ctx context.Conte } items := make([]responses.SubscriptionInfoResponse, len(subscriptions)) for i := range subscriptions { - resp := s.convertToSubscriptionInfoResponse(subscriptions[i]) + resp := s.convertToSubscriptionInfoResponseForAdmin(subscriptions[i]) if resp != nil { items[i] = *resp // 解引用指针 } @@ -300,6 +360,65 @@ func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleResponse(prod } } +// convertToSubscriptionInfoResponseForAdmin 转换为订阅信息响应(管理员端,包含成本价) +func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponseForAdmin(subscription *entities.Subscription) *responses.SubscriptionInfoResponse { + // 查询用户信息 + var userInfo *responses.UserSimpleResponse + if subscription.UserID != "" { + user, err := s.userRepo.GetByIDWithEnterpriseInfo(context.Background(), subscription.UserID) + if err == nil { + companyName := "未知公司" + if user.EnterpriseInfo != nil { + companyName = user.EnterpriseInfo.CompanyName + } + userInfo = &responses.UserSimpleResponse{ + ID: user.ID, + CompanyName: companyName, + Phone: user.Phone, + } + } + } + + var productAdminResponse *responses.ProductSimpleAdminResponse + if subscription.Product != nil { + productAdminResponse = s.convertToProductSimpleAdminResponse(subscription.Product) + } + + return &responses.SubscriptionInfoResponse{ + ID: subscription.ID, + UserID: subscription.UserID, + ProductID: subscription.ProductID, + Price: subscription.Price.InexactFloat64(), + User: userInfo, + ProductAdmin: productAdminResponse, + APIUsed: subscription.APIUsed, + CreatedAt: subscription.CreatedAt, + UpdatedAt: subscription.UpdatedAt, + } +} + +// convertToProductSimpleAdminResponse 转换为管理员产品简单信息响应(包含成本价) +func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleAdminResponse(product *entities.Product) *responses.ProductSimpleAdminResponse { + var categoryResponse *responses.CategorySimpleResponse + if product.Category != nil { + categoryResponse = s.convertToCategorySimpleResponse(product.Category) + } + + return &responses.ProductSimpleAdminResponse{ + ProductSimpleResponse: responses.ProductSimpleResponse{ + ID: product.ID, + OldID: product.OldID, + Name: product.Name, + Code: product.Code, + Description: product.Description, + Price: product.Price.InexactFloat64(), + Category: categoryResponse, + IsPackage: product.IsPackage, + }, + CostPrice: product.CostPrice.InexactFloat64(), + } +} + // convertToCategorySimpleResponse 转换为分类简单信息响应 func (s *SubscriptionApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse { if category == nil { diff --git a/internal/infrastructure/database/repositories/product/gorm_subscription_repository.go b/internal/infrastructure/database/repositories/product/gorm_subscription_repository.go index d4bc834..1bd7e26 100644 --- a/internal/infrastructure/database/repositories/product/gorm_subscription_repository.go +++ b/internal/infrastructure/database/repositories/product/gorm_subscription_repository.go @@ -172,10 +172,10 @@ func (r *GormSubscriptionRepository) ListSubscriptions(ctx context.Context, quer dbQuery = dbQuery.Offset(offset).Limit(query.PageSize) } - // 预加载Product的id、name、code、price、is_package字段,并同时预加载ProductCategory的id、name、code字段 + // 预加载Product的id、name、code、price、cost_price、is_package字段,并同时预加载ProductCategory的id、name、code字段 if err := dbQuery. Preload("Product", func(db *gorm.DB) *gorm.DB { - return db.Select("id", "name", "code", "price", "is_package", "category_id"). + return db.Select("id", "name", "code", "price", "cost_price", "is_package", "category_id"). Preload("Category", func(db2 *gorm.DB) *gorm.DB { return db2.Select("id", "name", "code") })