2025-07-20 20:53:26 +08:00
|
|
|
|
package services
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
2025-07-31 15:41:00 +08:00
|
|
|
|
"time"
|
2025-07-20 20:53:26 +08:00
|
|
|
|
|
2025-07-28 01:46:39 +08:00
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
|
|
2025-07-20 20:53:26 +08:00
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
|
|
|
|
|
|
"tyapi-server/internal/domains/product/entities"
|
|
|
|
|
|
"tyapi-server/internal/domains/product/repositories"
|
2025-07-28 01:46:39 +08:00
|
|
|
|
"tyapi-server/internal/domains/product/repositories/queries"
|
2025-08-02 02:54:21 +08:00
|
|
|
|
"tyapi-server/internal/shared/interfaces"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/shopspring/decimal"
|
2025-07-20 20:53:26 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ProductSubscriptionService 产品订阅领域服务
|
|
|
|
|
|
// 负责产品订阅相关的业务逻辑,包括订阅验证、订阅管理等
|
|
|
|
|
|
type ProductSubscriptionService struct {
|
|
|
|
|
|
productRepo repositories.ProductRepository
|
|
|
|
|
|
subscriptionRepo repositories.SubscriptionRepository
|
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewProductSubscriptionService 创建产品订阅领域服务
|
|
|
|
|
|
func NewProductSubscriptionService(
|
|
|
|
|
|
productRepo repositories.ProductRepository,
|
|
|
|
|
|
subscriptionRepo repositories.SubscriptionRepository,
|
|
|
|
|
|
logger *zap.Logger,
|
|
|
|
|
|
) *ProductSubscriptionService {
|
|
|
|
|
|
return &ProductSubscriptionService{
|
|
|
|
|
|
productRepo: productRepo,
|
|
|
|
|
|
subscriptionRepo: subscriptionRepo,
|
|
|
|
|
|
logger: logger,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 01:46:39 +08:00
|
|
|
|
// UserSubscribedProductByCode 查找用户已订阅的产品
|
|
|
|
|
|
func (s *ProductSubscriptionService) UserSubscribedProductByCode(ctx context.Context, userID string, productCode string) (*entities.Subscription, error) {
|
|
|
|
|
|
product, err := s.productRepo.FindByCode(ctx, productCode)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
subscription, err := s.subscriptionRepo.FindByUserAndProduct(ctx, userID, product.ID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return subscription, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetUserSubscribedProduct 查找用户已订阅的产品
|
|
|
|
|
|
func (s *ProductSubscriptionService) GetUserSubscribedProduct(ctx context.Context, userID string, productID string) (*entities.Subscription, error) {
|
|
|
|
|
|
subscription, err := s.subscriptionRepo.FindByUserAndProduct(ctx, userID, productID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
|
return nil, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return subscription, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-20 20:53:26 +08:00
|
|
|
|
// CanUserSubscribeProduct 检查用户是否可以订阅产品
|
|
|
|
|
|
func (s *ProductSubscriptionService) CanUserSubscribeProduct(ctx context.Context, userID string, productID string) (bool, error) {
|
|
|
|
|
|
// 检查产品是否存在且可订阅
|
|
|
|
|
|
product, err := s.productRepo.GetByID(ctx, productID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false, fmt.Errorf("产品不存在: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !product.CanBeSubscribed() {
|
|
|
|
|
|
return false, errors.New("产品不可订阅")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查用户是否已有该产品的订阅
|
|
|
|
|
|
existingSubscription, err := s.subscriptionRepo.FindByUserAndProduct(ctx, userID, productID)
|
|
|
|
|
|
if err == nil && existingSubscription != nil {
|
|
|
|
|
|
return false, errors.New("用户已有该产品的订阅")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CreateSubscription 创建订阅
|
|
|
|
|
|
func (s *ProductSubscriptionService) CreateSubscription(ctx context.Context, userID, productID string) (*entities.Subscription, error) {
|
|
|
|
|
|
// 检查是否可以订阅
|
|
|
|
|
|
canSubscribe, err := s.CanUserSubscribeProduct(ctx, userID, productID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if !canSubscribe {
|
|
|
|
|
|
return nil, errors.New("无法订阅该产品")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取产品信息以获取价格
|
|
|
|
|
|
product, err := s.productRepo.GetByID(ctx, productID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("产品不存在: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建订阅
|
|
|
|
|
|
subscription := &entities.Subscription{
|
|
|
|
|
|
UserID: userID,
|
|
|
|
|
|
ProductID: productID,
|
|
|
|
|
|
Price: product.Price,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
createdSubscription, err := s.subscriptionRepo.Create(ctx, *subscription)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
s.logger.Error("创建订阅失败", zap.Error(err))
|
|
|
|
|
|
return nil, fmt.Errorf("创建订阅失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s.logger.Info("订阅创建成功",
|
|
|
|
|
|
zap.String("subscription_id", createdSubscription.ID),
|
|
|
|
|
|
zap.String("user_id", userID),
|
|
|
|
|
|
zap.String("product_id", productID),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return &createdSubscription, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 01:46:39 +08:00
|
|
|
|
// ListSubscriptions 获取订阅列表
|
|
|
|
|
|
func (s *ProductSubscriptionService) ListSubscriptions(ctx context.Context, query *queries.ListSubscriptionsQuery) ([]*entities.Subscription, int64, error) {
|
|
|
|
|
|
return s.subscriptionRepo.ListSubscriptions(ctx, query)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-20 20:53:26 +08:00
|
|
|
|
// GetUserSubscriptions 获取用户订阅列表
|
|
|
|
|
|
func (s *ProductSubscriptionService) GetUserSubscriptions(ctx context.Context, userID string) ([]*entities.Subscription, error) {
|
|
|
|
|
|
return s.subscriptionRepo.FindByUserID(ctx, userID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetSubscriptionByID 根据ID获取订阅
|
|
|
|
|
|
func (s *ProductSubscriptionService) GetSubscriptionByID(ctx context.Context, subscriptionID string) (*entities.Subscription, error) {
|
|
|
|
|
|
subscription, err := s.subscriptionRepo.GetByID(ctx, subscriptionID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("订阅不存在: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return &subscription, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CancelSubscription 取消订阅
|
|
|
|
|
|
func (s *ProductSubscriptionService) CancelSubscription(ctx context.Context, subscriptionID string) error {
|
|
|
|
|
|
// 由于订阅实体没有状态字段,这里直接删除订阅
|
|
|
|
|
|
if err := s.subscriptionRepo.Delete(ctx, subscriptionID); err != nil {
|
|
|
|
|
|
s.logger.Error("取消订阅失败", zap.Error(err))
|
|
|
|
|
|
return fmt.Errorf("取消订阅失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s.logger.Info("订阅取消成功",
|
|
|
|
|
|
zap.String("subscription_id", subscriptionID),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetProductStats 获取产品统计信息
|
|
|
|
|
|
func (s *ProductSubscriptionService) GetProductStats(ctx context.Context) (map[string]int64, error) {
|
|
|
|
|
|
stats := make(map[string]int64)
|
|
|
|
|
|
|
|
|
|
|
|
total, err := s.productRepo.CountByCategory(ctx, "")
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
stats["total"] = total
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enabled, err := s.productRepo.CountEnabled(ctx)
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
stats["enabled"] = enabled
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
visible, err := s.productRepo.CountVisible(ctx)
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
stats["visible"] = visible
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return stats, nil
|
2025-07-28 01:46:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *ProductSubscriptionService) SaveSubscription(ctx context.Context, subscription *entities.Subscription) error {
|
|
|
|
|
|
exists, err := s.subscriptionRepo.Exists(ctx, subscription.ID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("检查订阅是否存在失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if exists {
|
|
|
|
|
|
return s.subscriptionRepo.Update(ctx, *subscription)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_, err := s.subscriptionRepo.Create(ctx, *subscription)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("创建订阅失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2025-07-31 15:41:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IncrementSubscriptionAPIUsage 增加订阅API使用次数(使用乐观锁,带重试机制)
|
|
|
|
|
|
func (s *ProductSubscriptionService) IncrementSubscriptionAPIUsage(ctx context.Context, subscriptionID string, increment int64) error {
|
|
|
|
|
|
const maxRetries = 3
|
|
|
|
|
|
const baseDelay = 10 * time.Millisecond
|
|
|
|
|
|
|
|
|
|
|
|
for attempt := 0; attempt < maxRetries; attempt++ {
|
|
|
|
|
|
// 使用乐观锁直接更新数据库
|
|
|
|
|
|
err := s.subscriptionRepo.IncrementAPIUsageWithOptimisticLock(ctx, subscriptionID, increment)
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
// 更新成功
|
|
|
|
|
|
if attempt > 0 {
|
|
|
|
|
|
s.logger.Info("订阅API使用次数更新成功(重试后)",
|
|
|
|
|
|
zap.String("subscription_id", subscriptionID),
|
|
|
|
|
|
zap.Int64("increment", increment),
|
|
|
|
|
|
zap.Int("retry_count", attempt))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
s.logger.Info("订阅API使用次数更新成功",
|
|
|
|
|
|
zap.String("subscription_id", subscriptionID),
|
|
|
|
|
|
zap.Int64("increment", increment))
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否是版本冲突错误
|
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
|
// 版本冲突,等待后重试
|
|
|
|
|
|
if attempt < maxRetries-1 {
|
|
|
|
|
|
delay := time.Duration(attempt+1) * baseDelay
|
|
|
|
|
|
s.logger.Debug("订阅版本冲突,准备重试",
|
|
|
|
|
|
zap.String("subscription_id", subscriptionID),
|
|
|
|
|
|
zap.Int("attempt", attempt+1),
|
|
|
|
|
|
zap.Duration("delay", delay))
|
|
|
|
|
|
time.Sleep(delay)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
// 最后一次重试失败
|
|
|
|
|
|
s.logger.Error("订阅不存在或版本冲突,重试次数已用完",
|
|
|
|
|
|
zap.String("subscription_id", subscriptionID),
|
|
|
|
|
|
zap.Int("max_retries", maxRetries),
|
|
|
|
|
|
zap.Error(err))
|
|
|
|
|
|
return fmt.Errorf("订阅不存在或已被其他操作修改(重试%d次后失败): %w", maxRetries, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 其他错误直接返回,不重试
|
|
|
|
|
|
s.logger.Error("更新订阅API使用次数失败",
|
|
|
|
|
|
zap.String("subscription_id", subscriptionID),
|
|
|
|
|
|
zap.Int64("increment", increment),
|
|
|
|
|
|
zap.Error(err))
|
|
|
|
|
|
return fmt.Errorf("更新订阅API使用次数失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return fmt.Errorf("更新失败,已重试%d次", maxRetries)
|
|
|
|
|
|
}
|
2025-08-02 02:54:21 +08:00
|
|
|
|
|
|
|
|
|
|
// GetSubscriptionStats 获取订阅统计信息
|
|
|
|
|
|
func (s *ProductSubscriptionService) GetSubscriptionStats(ctx context.Context) (map[string]interface{}, error) {
|
|
|
|
|
|
stats := make(map[string]interface{})
|
|
|
|
|
|
|
|
|
|
|
|
// 获取总订阅数
|
|
|
|
|
|
totalSubscriptions, err := s.subscriptionRepo.Count(ctx, interfaces.CountOptions{})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
s.logger.Error("获取订阅总数失败", zap.Error(err))
|
|
|
|
|
|
return nil, fmt.Errorf("获取订阅总数失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
stats["total_subscriptions"] = totalSubscriptions
|
|
|
|
|
|
|
|
|
|
|
|
// 获取总收入
|
|
|
|
|
|
totalRevenue, err := s.subscriptionRepo.GetTotalRevenue(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
s.logger.Error("获取总收入失败", zap.Error(err))
|
|
|
|
|
|
return nil, fmt.Errorf("获取总收入失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
stats["total_revenue"] = totalRevenue
|
|
|
|
|
|
|
|
|
|
|
|
return stats, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetUserSubscriptionStats 获取用户订阅统计信息
|
|
|
|
|
|
func (s *ProductSubscriptionService) GetUserSubscriptionStats(ctx context.Context, userID string) (map[string]interface{}, error) {
|
|
|
|
|
|
stats := make(map[string]interface{})
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户订阅数
|
|
|
|
|
|
userSubscriptions, err := s.subscriptionRepo.FindByUserID(ctx, userID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
s.logger.Error("获取用户订阅失败", zap.Error(err))
|
|
|
|
|
|
return nil, fmt.Errorf("获取用户订阅失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算用户总收入
|
|
|
|
|
|
var totalRevenue float64
|
|
|
|
|
|
for _, subscription := range userSubscriptions {
|
|
|
|
|
|
totalRevenue += subscription.Price.InexactFloat64()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stats["total_subscriptions"] = int64(len(userSubscriptions))
|
|
|
|
|
|
stats["total_revenue"] = totalRevenue
|
|
|
|
|
|
|
|
|
|
|
|
return stats, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UpdateSubscriptionPrice 更新订阅价格
|
|
|
|
|
|
func (s *ProductSubscriptionService) UpdateSubscriptionPrice(ctx context.Context, subscriptionID string, newPrice float64) error {
|
|
|
|
|
|
// 获取订阅
|
|
|
|
|
|
subscription, err := s.subscriptionRepo.GetByID(ctx, subscriptionID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("订阅不存在: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新价格
|
|
|
|
|
|
subscription.Price = decimal.NewFromFloat(newPrice)
|
|
|
|
|
|
subscription.Version++ // 增加版本号
|
|
|
|
|
|
|
|
|
|
|
|
// 保存更新
|
|
|
|
|
|
if err := s.subscriptionRepo.Update(ctx, subscription); err != nil {
|
|
|
|
|
|
s.logger.Error("更新订阅价格失败", zap.Error(err))
|
|
|
|
|
|
return fmt.Errorf("更新订阅价格失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s.logger.Info("订阅价格更新成功",
|
|
|
|
|
|
zap.String("subscription_id", subscriptionID),
|
|
|
|
|
|
zap.Float64("new_price", newPrice))
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|