added: Cost price adjustment
This commit is contained in:
@@ -14,7 +14,9 @@ type UpdateSubscriptionPriceCommand struct {
|
|||||||
|
|
||||||
// BatchUpdateSubscriptionPricesCommand 批量更新订阅价格命令
|
// BatchUpdateSubscriptionPricesCommand 批量更新订阅价格命令
|
||||||
type BatchUpdateSubscriptionPricesCommand struct {
|
type BatchUpdateSubscriptionPricesCommand struct {
|
||||||
UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"`
|
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折)"`
|
AdjustmentType string `json:"adjustment_type" binding:"required,oneof=discount cost_multiple" comment:"调整方式(discount:按售价折扣,cost_multiple:按成本价倍数)"`
|
||||||
Scope string `json:"scope" binding:"required,oneof=undiscounted all" comment:"改价范围(undiscounted:仅未打折,all:所有)"`
|
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:所有)"`
|
||||||
}
|
}
|
||||||
@@ -70,6 +70,12 @@ type ProductSimpleResponse struct {
|
|||||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProductSimpleAdminResponse 管理员产品简单信息响应(包含成本价)
|
||||||
|
type ProductSimpleAdminResponse struct {
|
||||||
|
ProductSimpleResponse
|
||||||
|
CostPrice float64 `json:"cost_price" comment:"成本价"`
|
||||||
|
}
|
||||||
|
|
||||||
// ProductStatsResponse 产品统计响应
|
// ProductStatsResponse 产品统计响应
|
||||||
type ProductStatsResponse struct {
|
type ProductStatsResponse struct {
|
||||||
TotalProducts int64 `json:"total_products" comment:"产品总数"`
|
TotalProducts int64 `json:"total_products" comment:"产品总数"`
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ type SubscriptionInfoResponse struct {
|
|||||||
// 关联信息
|
// 关联信息
|
||||||
User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"`
|
User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"`
|
||||||
Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"`
|
Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"`
|
||||||
|
// 管理员端使用,包含成本价的产品信息
|
||||||
|
ProductAdmin *ProductSimpleAdminResponse `json:"product_admin,omitempty" comment:"产品信息(管理员端,包含成本价)"`
|
||||||
|
|
||||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package product
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -45,6 +46,22 @@ func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context
|
|||||||
// BatchUpdateSubscriptionPrices 一键改价
|
// BatchUpdateSubscriptionPrices 一键改价
|
||||||
// 业务流程:1. 获取用户所有订阅 2. 根据范围筛选 3. 批量更新价格
|
// 业务流程:1. 获取用户所有订阅 2. 根据范围筛选 3. 批量更新价格
|
||||||
func (s *SubscriptionApplicationServiceImpl) BatchUpdateSubscriptionPrices(ctx context.Context, cmd *commands.BatchUpdateSubscriptionPricesCommand) error {
|
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{
|
subscriptions, _, err := s.productSubscriptionService.ListSubscriptions(ctx, &repoQueries.ListSubscriptionsQuery{
|
||||||
UserID: cmd.UserID,
|
UserID: cmd.UserID,
|
||||||
Page: 1,
|
Page: 1,
|
||||||
@@ -54,6 +71,9 @@ func (s *SubscriptionApplicationServiceImpl) BatchUpdateSubscriptionPrices(ctx c
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.logger.Info("获取到订阅列表",
|
||||||
|
zap.Int("total_subscriptions", len(subscriptions)))
|
||||||
|
|
||||||
// 根据范围筛选订阅
|
// 根据范围筛选订阅
|
||||||
var targetSubscriptions []*entities.Subscription
|
var targetSubscriptions []*entities.Subscription
|
||||||
for _, sub := range subscriptions {
|
for _, sub := range subscriptions {
|
||||||
@@ -69,24 +89,64 @@ func (s *SubscriptionApplicationServiceImpl) BatchUpdateSubscriptionPrices(ctx c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 批量更新价格
|
// 批量更新价格
|
||||||
|
updatedCount := 0
|
||||||
|
skippedCount := 0
|
||||||
for _, sub := range targetSubscriptions {
|
for _, sub := range targetSubscriptions {
|
||||||
if sub.Product != nil {
|
if sub.Product == nil {
|
||||||
// 计算折扣后的价格
|
skippedCount++
|
||||||
discountRatio := cmd.Discount / 10
|
continue
|
||||||
newPrice := sub.Product.Price.Mul(decimal.NewFromFloat(discountRatio))
|
}
|
||||||
// 四舍五入到2位小数
|
|
||||||
newPrice = newPrice.Round(2)
|
|
||||||
|
|
||||||
err := s.productSubscriptionService.UpdateSubscriptionPrice(ctx, sub.ID, newPrice.InexactFloat64())
|
var newPrice decimal.Decimal
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("批量更新订阅价格失败",
|
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.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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +189,7 @@ func (s *SubscriptionApplicationServiceImpl) ListSubscriptions(ctx context.Conte
|
|||||||
}
|
}
|
||||||
items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
|
items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
|
||||||
for i := range subscriptions {
|
for i := range subscriptions {
|
||||||
resp := s.convertToSubscriptionInfoResponse(subscriptions[i])
|
resp := s.convertToSubscriptionInfoResponseForAdmin(subscriptions[i])
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
items[i] = *resp // 解引用指针
|
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 转换为分类简单信息响应
|
// convertToCategorySimpleResponse 转换为分类简单信息响应
|
||||||
func (s *SubscriptionApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
|
func (s *SubscriptionApplicationServiceImpl) convertToCategorySimpleResponse(category *entities.ProductCategory) *responses.CategorySimpleResponse {
|
||||||
if category == nil {
|
if category == nil {
|
||||||
|
|||||||
@@ -172,10 +172,10 @@ func (r *GormSubscriptionRepository) ListSubscriptions(ctx context.Context, quer
|
|||||||
dbQuery = dbQuery.Offset(offset).Limit(query.PageSize)
|
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.
|
if err := dbQuery.
|
||||||
Preload("Product", func(db *gorm.DB) *gorm.DB {
|
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 {
|
Preload("Category", func(db2 *gorm.DB) *gorm.DB {
|
||||||
return db2.Select("id", "name", "code")
|
return db2.Select("id", "name", "code")
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user