diff --git a/docs/openapi.yaml b/docs/openapi.yaml index fc3d1f9..6f64c28 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -224,12 +224,12 @@ paths: description: Внутренняя ошибка сервера /promocode/stats: - get: + post: operationId: GetStats summary: Получить статистику промокода tags: - stats - description: Идентификатор промокода + description: Идентификатор промокода и временной интерфал от до unix requestBody: required: true content: @@ -266,37 +266,34 @@ components: PromoCodeStatsReq: type: object properties: - promoCodeID: + id: type: string + from: + type: integer + to: + type: integer required: - - promoCodeID + - id PromoCodeStatsResp: type: object properties: id: type: string - description: Идентификатор промокода + description: id промокода usageCount: type: integer - description: Количество использований промокода + description: общее количество использований промокода + example: 18 usageMap: type: object - description: Карта использования промокода + description: мапа использования промокода ранжированая по способу additionalProperties: - type: array - items: - $ref: '#/components/schemas/Usage' + type: integer + example: + "-": 10 + "fastlinkID1": 5 + "fastlinkID2": 3 - Usage: - type: object - properties: - key: - type: string - description: fastlink или codeword в зависимости от того что применялось - time: - type: string - format: date-time - description: Время использования промокода CreateFastLinkReq: type: object diff --git a/internal/controller/promocode/promocode_controller.go b/internal/controller/promocode/promocode_controller.go index f7ef478..21c0065 100644 --- a/internal/controller/promocode/promocode_controller.go +++ b/internal/controller/promocode/promocode_controller.go @@ -184,9 +184,7 @@ func (p *PromoCodeController) CreateFastLink(c *fiber.Ctx) error { } func (p *PromoCodeController) GetStats(c *fiber.Ctx) error { - var req struct { - PromoCodeID string `json:"id"` - } + var req models.PromoStatReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"}) @@ -196,7 +194,7 @@ func (p *PromoCodeController) GetStats(c *fiber.Ctx) error { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "PromoCode ID is required"}) } - promoStats, err := p.promoCodeService.GetStats(c.Context(), req.PromoCodeID) + promoStats, err := p.promoCodeService.GetStats(c.Context(), req) if err != nil { p.logger.Error("Failed getting promo stats", zap.Error(err)) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) diff --git a/internal/controller/promocode/route.go b/internal/controller/promocode/route.go index af93df7..d89fcdc 100644 --- a/internal/controller/promocode/route.go +++ b/internal/controller/promocode/route.go @@ -9,7 +9,7 @@ func (p *PromoCodeController) Register(router fiber.Router) { router.Post("/activate", p.Activate) router.Delete("/:promocodeID", p.Delete) router.Post("/fastlink", p.CreateFastLink) - router.Get("/stats", p.GetStats) + router.Post("/stats", p.GetStats) } diff --git a/internal/models/bonus.go b/internal/models/bonus.go index 9c3cf6e..13333b5 100644 --- a/internal/models/bonus.go +++ b/internal/models/bonus.go @@ -81,13 +81,24 @@ type ActivateResp struct { Greetings string `json:"greetings"` // поле из активированного промокода } +type PromoStatReq struct { + PromoCodeID string `json:"id,omitempty"` + From uint64 `json:"from"` + To uint64 `json:"to"` +} + type PromoCodeStats struct { - ID string `bson:"_id,omitempty" json:"id,omitempty"` - UsageCount int `bson:"usageCount" json:"usageCount"` - UsageMap map[string][]Usage `bson:"usageMap" json:"usageMap"` + ID string `json:"id,omitempty"` + UsageMap map[string][]Usage `json:"usageMap"` } type Usage struct { - Key string `bson:"key" json:"key"` - Time time.Time `bson:"time" json:"time"` + UserID string `bson:"userID" json:"userID"` + Time uint64 `bson:"time" json:"time"` +} + +type PromoCodeStatsResp struct { + ID string `json:"id,omitempty"` + UsageCount int `json:"usageCount"` + UsageMap map[string]int `json:"usageMap"` } diff --git a/internal/repository/promocode_stats.go b/internal/repository/promocode_stats.go index 1daf023..218709f 100644 --- a/internal/repository/promocode_stats.go +++ b/internal/repository/promocode_stats.go @@ -20,32 +20,36 @@ func NewStatsRepository(deps Deps) *StatsRepository { } func (r *StatsRepository) UpdateStatistics(ctx context.Context, req *models.ActivateReq, promoCode *models.PromoCode, userID string) error { - filter := bson.M{"_id": promoCode.ID, "usageMap." + userID: bson.M{"$exists": true}} - count, err := r.mdb.CountDocuments(ctx, filter) - if err != nil { - return err - } - - if count >= 1 { - return ErrPromoCodeAlreadyActivated - } - var key string if req.FastLink != "" { key = req.FastLink } else { - key = req.Codeword + key = "-" + } + + var promoCodeStats models.PromoCodeStats + err := r.mdb.FindOne(ctx, bson.M{"_id": promoCode.ID}).Decode(&promoCodeStats) + if err != nil && mongo.ErrNoDocuments == nil { + return err + } + + if promoCodeStats.UsageMap != nil { + usageList := promoCodeStats.UsageMap[key] + for _, usage := range usageList { + if usage.UserID == userID { + return ErrPromoCodeAlreadyActivated + } + } } usage := models.Usage{ - Key: key, - Time: time.Now(), + UserID: userID, + Time: uint64(time.Now().Unix()), } update := bson.M{ - "$inc": bson.M{"usageCount": 1}, "$push": bson.M{ - "usageMap." + userID: usage, + "usageMap." + key: usage, }, } @@ -54,10 +58,10 @@ func (r *StatsRepository) UpdateStatistics(ctx context.Context, req *models.Acti return err } -func (r *StatsRepository) GetStatistics(ctx context.Context, promoCodeID string) (*models.PromoCodeStats, error) { +func (r *StatsRepository) GetStatistics(ctx context.Context, promoCodeID string) (models.PromoCodeStats, error) { objID, err := primitive.ObjectIDFromHex(promoCodeID) if err != nil { - return nil, err + return models.PromoCodeStats{}, err } filter := bson.M{"_id": objID} @@ -65,8 +69,8 @@ func (r *StatsRepository) GetStatistics(ctx context.Context, promoCodeID string) var promoCodeStats models.PromoCodeStats err = r.mdb.FindOne(ctx, filter).Decode(&promoCodeStats) if err != nil { - return nil, err + return models.PromoCodeStats{}, err } - return &promoCodeStats, nil + return promoCodeStats, nil } diff --git a/internal/services/promocode_service.go b/internal/services/promocode_service.go index 3947f3e..dcb55a2 100644 --- a/internal/services/promocode_service.go +++ b/internal/services/promocode_service.go @@ -27,7 +27,7 @@ type PromoCodeRepository interface { type PromoStatsRepository interface { UpdateStatistics(ctx context.Context, req *models.ActivateReq, promoCode *models.PromoCode, userID string) error - GetStatistics(ctx context.Context, promoCodeID string) (*models.PromoCodeStats, error) + GetStatistics(ctx context.Context, promoCodeID string) (models.PromoCodeStats, error) } type PromoDeps struct { @@ -201,11 +201,35 @@ func (s *PromoCodeService) CreateFastLink(ctx context.Context, promoCodeID strin return xid, nil } -func (s *PromoCodeService) GetStats(ctx context.Context, promoCodeID string) (*models.PromoCodeStats, error) { - promoStats, err := s.statsRepo.GetStatistics(ctx, promoCodeID) +func (s *PromoCodeService) GetStats(ctx context.Context, req models.PromoStatReq) (models.PromoCodeStatsResp, error) { + promoStats, err := s.statsRepo.GetStatistics(ctx, req.PromoCodeID) if err != nil { s.logger.Error("Failed getting promo stats", zap.Error(err)) - return nil, err + return models.PromoCodeStatsResp{}, err } - return promoStats, nil + + var resp models.PromoCodeStatsResp + + stats := make(map[string]int) + + for key, usageCount := range promoStats.UsageMap { + count := 0 + for _, usage := range usageCount { + if (req.From == 0 || usage.Time >= req.From) && (req.To == 0 || usage.Time <= req.To) { + count++ + } + } + stats[key] = count + } + + totalUsageCount := 0 + for _, count := range stats { + totalUsageCount += count + } + + resp.UsageMap = stats + resp.UsageCount = totalUsageCount + resp.ID = req.PromoCodeID + + return resp, nil }