Files
tyapi-server/internal/application/product/subscription_application_service_impl.go
2025-12-05 14:59:23 +08:00

482 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package product
import (
"context"
"fmt"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"tyapi-server/internal/application/product/dto/commands"
appQueries "tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses"
domain_api_repo "tyapi-server/internal/domains/api/repositories"
"tyapi-server/internal/domains/product/entities"
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
product_service "tyapi-server/internal/domains/product/services"
user_repositories "tyapi-server/internal/domains/user/repositories"
)
// SubscriptionApplicationServiceImpl 订阅应用服务实现
// 负责业务流程编排、事务管理、数据转换,不直接操作仓库
type SubscriptionApplicationServiceImpl struct {
productSubscriptionService *product_service.ProductSubscriptionService
userRepo user_repositories.UserRepository
apiCallRepository domain_api_repo.ApiCallRepository
logger *zap.Logger
}
// NewSubscriptionApplicationService 创建订阅应用服务
func NewSubscriptionApplicationService(
productSubscriptionService *product_service.ProductSubscriptionService,
userRepo user_repositories.UserRepository,
apiCallRepository domain_api_repo.ApiCallRepository,
logger *zap.Logger,
) SubscriptionApplicationService {
return &SubscriptionApplicationServiceImpl{
productSubscriptionService: productSubscriptionService,
userRepo: userRepo,
apiCallRepository: apiCallRepository,
logger: logger,
}
}
// UpdateSubscriptionPrice 更新订阅价格
// 业务流程1. 获取订阅 2. 更新价格 3. 保存订阅
func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error {
return s.productSubscriptionService.UpdateSubscriptionPrice(ctx, cmd.ID, cmd.Price)
}
// 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,
PageSize: 1000,
})
if err != nil {
return err
}
s.logger.Info("获取到订阅列表",
zap.Int("total_subscriptions", len(subscriptions)))
// 根据范围筛选订阅
var targetSubscriptions []*entities.Subscription
for _, sub := range subscriptions {
if cmd.Scope == "all" {
// 所有订阅都修改
targetSubscriptions = append(targetSubscriptions, sub)
} else if cmd.Scope == "undiscounted" {
// 只修改未打折的订阅(价格等于产品原价)
if sub.Product != nil && sub.Price.Equal(sub.Product.Price) {
targetSubscriptions = append(targetSubscriptions, sub)
}
}
}
// 批量更新价格
updatedCount := 0
skippedCount := 0
for _, sub := range targetSubscriptions {
if sub.Product == nil {
skippedCount++
continue
}
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.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
}
// CreateSubscription 创建订阅
// 业务流程1. 创建订阅
func (s *SubscriptionApplicationServiceImpl) CreateSubscription(ctx context.Context, cmd *commands.CreateSubscriptionCommand) error {
_, err := s.productSubscriptionService.CreateSubscription(ctx, cmd.UserID, cmd.ProductID)
return err
}
// GetSubscriptionByID 根据ID获取订阅
// 业务流程1. 获取订阅信息 2. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionByID(ctx context.Context, query *appQueries.GetSubscriptionQuery) (*responses.SubscriptionInfoResponse, error) {
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, query.ID)
if err != nil {
return nil, err
}
return s.convertToSubscriptionInfoResponse(subscription), nil
}
// ListSubscriptions 获取订阅列表(管理员用)
// 业务流程1. 获取订阅列表 2. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) ListSubscriptions(ctx context.Context, query *appQueries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error) {
repoQuery := &repoQueries.ListSubscriptionsQuery{
Page: query.Page,
PageSize: query.PageSize,
UserID: query.UserID, // 管理员可以按用户筛选
Keyword: query.Keyword,
SortBy: query.SortBy,
SortOrder: query.SortOrder,
CompanyName: query.CompanyName,
ProductName: query.ProductName,
StartTime: query.StartTime,
EndTime: query.EndTime,
}
subscriptions, total, err := s.productSubscriptionService.ListSubscriptions(ctx, repoQuery)
if err != nil {
return nil, err
}
items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
for i := range subscriptions {
resp := s.convertToSubscriptionInfoResponseForAdmin(subscriptions[i])
if resp != nil {
items[i] = *resp // 解引用指针
}
}
return &responses.SubscriptionListResponse{
Total: total,
Page: query.Page,
Size: query.PageSize,
Items: items,
}, nil
}
// ListMySubscriptions 获取我的订阅列表(用户用)
// 业务流程1. 获取用户订阅列表 2. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) ListMySubscriptions(ctx context.Context, userID string, query *appQueries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error) {
repoQuery := &repoQueries.ListSubscriptionsQuery{
Page: query.Page,
PageSize: query.PageSize,
UserID: userID, // 强制设置为当前用户ID
Keyword: query.Keyword,
SortBy: query.SortBy,
SortOrder: query.SortOrder,
CompanyName: query.CompanyName,
ProductName: query.ProductName,
StartTime: query.StartTime,
EndTime: query.EndTime,
}
subscriptions, total, err := s.productSubscriptionService.ListSubscriptions(ctx, repoQuery)
if err != nil {
return nil, err
}
items := make([]responses.SubscriptionInfoResponse, len(subscriptions))
for i := range subscriptions {
resp := s.convertToSubscriptionInfoResponse(subscriptions[i])
if resp != nil {
items[i] = *resp // 解引用指针
}
}
return &responses.SubscriptionListResponse{
Total: total,
Page: query.Page,
Size: query.PageSize,
Items: items,
}, nil
}
// GetUserSubscriptions 获取用户订阅
// 业务流程1. 获取用户订阅 2. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) GetUserSubscriptions(ctx context.Context, query *appQueries.GetUserSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error) {
subscriptions, err := s.productSubscriptionService.GetUserSubscriptions(ctx, query.UserID)
if err != nil {
return nil, err
}
// 转换为响应对象
items := make([]*responses.SubscriptionInfoResponse, len(subscriptions))
for i := range subscriptions {
items[i] = s.convertToSubscriptionInfoResponse(subscriptions[i])
}
return items, nil
}
// GetProductSubscriptions 获取产品订阅
// 业务流程1. 获取产品订阅 2. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) GetProductSubscriptions(ctx context.Context, query *appQueries.GetProductSubscriptionsQuery) ([]*responses.SubscriptionInfoResponse, error) {
// 这里需要扩展领域服务来支持按产品查询订阅
// 暂时返回空列表
return []*responses.SubscriptionInfoResponse{}, nil
}
// GetSubscriptionUsage 获取订阅使用情况
// 业务流程1. 获取订阅信息 2. 根据产品ID和用户ID统计API调用次数 3. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Context, subscriptionID string) (*responses.SubscriptionUsageResponse, error) {
// 获取订阅信息
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
if err != nil {
return nil, err
}
// 根据用户ID和产品ID统计API调用次数
apiCallCount, err := s.apiCallRepository.CountByUserIdAndProductId(ctx, subscription.UserID, subscription.ProductID)
if err != nil {
s.logger.Warn("统计API调用次数失败使用订阅记录中的值",
zap.String("subscription_id", subscriptionID),
zap.String("user_id", subscription.UserID),
zap.String("product_id", subscription.ProductID),
zap.Error(err))
// 如果统计失败使用订阅实体中的APIUsed字段作为备选
apiCallCount = subscription.APIUsed
}
return &responses.SubscriptionUsageResponse{
ID: subscription.ID,
ProductID: subscription.ProductID,
APIUsed: apiCallCount,
}, nil
}
// GetSubscriptionStats 获取订阅统计信息
// 业务流程1. 获取订阅统计 2. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error) {
stats, err := s.productSubscriptionService.GetSubscriptionStats(ctx)
if err != nil {
return nil, err
}
return &responses.SubscriptionStatsResponse{
TotalSubscriptions: stats["total_subscriptions"].(int64),
TotalRevenue: stats["total_revenue"].(float64),
}, nil
}
// GetMySubscriptionStats 获取我的订阅统计信息
// 业务流程1. 获取用户订阅统计 2. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error) {
stats, err := s.productSubscriptionService.GetUserSubscriptionStats(ctx, userID)
if err != nil {
return nil, err
}
return &responses.SubscriptionStatsResponse{
TotalSubscriptions: stats["total_subscriptions"].(int64),
TotalRevenue: stats["total_revenue"].(float64),
}, nil
}
// CancelMySubscription 取消我的订阅
// 业务流程1. 验证订阅是否属于当前用户 2. 取消订阅
func (s *SubscriptionApplicationServiceImpl) CancelMySubscription(ctx context.Context, userID string, subscriptionID string) error {
// 1. 获取订阅信息
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, subscriptionID)
if err != nil {
s.logger.Error("获取订阅信息失败", zap.String("subscription_id", subscriptionID), zap.Error(err))
return fmt.Errorf("订阅不存在")
}
// 2. 验证订阅是否属于当前用户
if subscription.UserID != userID {
s.logger.Warn("用户尝试取消不属于自己的订阅",
zap.String("user_id", userID),
zap.String("subscription_id", subscriptionID),
zap.String("subscription_user_id", subscription.UserID))
return fmt.Errorf("无权取消此订阅")
}
// 3. 取消订阅(软删除)
if err := s.productSubscriptionService.CancelSubscription(ctx, subscriptionID); err != nil {
s.logger.Error("取消订阅失败", zap.String("subscription_id", subscriptionID), zap.Error(err))
return fmt.Errorf("取消订阅失败: %w", err)
}
s.logger.Info("用户取消订阅成功",
zap.String("user_id", userID),
zap.String("subscription_id", subscriptionID))
return nil
}
// convertToSubscriptionInfoResponse 转换为订阅信息响应
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(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 productResponse *responses.ProductSimpleResponse
if subscription.Product != nil {
productResponse = s.convertToProductSimpleResponse(subscription.Product)
}
return &responses.SubscriptionInfoResponse{
ID: subscription.ID,
UserID: subscription.UserID,
ProductID: subscription.ProductID,
Price: subscription.Price.InexactFloat64(),
User: userInfo,
Product: productResponse,
APIUsed: subscription.APIUsed,
CreatedAt: subscription.CreatedAt,
UpdatedAt: subscription.UpdatedAt,
}
}
// convertToProductSimpleResponse 转换为产品简单信息响应
func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleResponse(product *entities.Product) *responses.ProductSimpleResponse {
var categoryResponse *responses.CategorySimpleResponse
if product.Category != nil {
categoryResponse = s.convertToCategorySimpleResponse(product.Category)
}
return &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,
}
}
// 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 {
return nil
}
return &responses.CategorySimpleResponse{
ID: category.ID,
Name: category.Name,
}
}