diff --git a/internal/handler/streak_handler.go b/internal/handler/streak_handler.go index fd1d588..8360b4e 100644 --- a/internal/handler/streak_handler.go +++ b/internal/handler/streak_handler.go @@ -66,8 +66,27 @@ func (h *StreakHandler) RecalculateStreak(c *gin.Context) { api.Success(c, streakInfo) } +// GetContributionData handles GET /api/v1/user/streak/contribution +// Returns daily contribution data for heat map +func (h *StreakHandler) GetContributionData(c *gin.Context) { + userID, exists := c.Get("user_id") + if !exists { + api.Unauthorized(c, "User not authenticated") + return + } + + contributionData, err := h.streakService.GetContributionData(userID.(uint)) + if err != nil { + api.InternalError(c, "Failed to get contribution data: "+err.Error()) + return + } + + api.Success(c, contributionData) +} + // RegisterRoutes registers streak-related routes func (h *StreakHandler) RegisterRoutes(rg *gin.RouterGroup) { rg.GET("/user/streak", h.GetStreak) rg.POST("/user/streak/recalculate", h.RecalculateStreak) + rg.GET("/user/streak/contribution", h.GetContributionData) } diff --git a/internal/models/user_streak.go b/internal/models/user_streak.go index e2d1341..16e9dd9 100644 --- a/internal/models/user_streak.go +++ b/internal/models/user_streak.go @@ -29,3 +29,9 @@ type StreakInfo struct { HasRecordToday bool `json:"has_record_today"` // 今天是否已记账 Message string `json:"message"` // 提示信息 } + +// DailyContribution represents transaction count for a specific date +type DailyContribution struct { + Date string `json:"date"` + Count int `json:"count"` +} diff --git a/internal/repository/streak_repository.go b/internal/repository/streak_repository.go index c6b22e7..a055ead 100644 --- a/internal/repository/streak_repository.go +++ b/internal/repository/streak_repository.go @@ -107,3 +107,51 @@ func (r *StreakRepository) GetTransactionDatesInRange(userID uint, startDate, en return dates, nil } + +// GetDailyContribution returns daily transaction counts in a date range +func (r *StreakRepository) GetDailyContribution(userID uint, startDate, endDate time.Time) ([]models.DailyContribution, error) { + var results []models.DailyContribution + + // SQLite uses strftime, MySQL/Postgres uses DATE() + // Using a more generic approach compatible with SQLite (which is likely used locally) + // For production readiness with multiple DBs, raw SQL might be safer or check dialect + + rows, err := r.db.Model(&models.Transaction{}). + Select("DATE(transaction_date) as date, COUNT(*) as count"). + Where("user_id = ? AND transaction_date >= ? AND transaction_date <= ?", userID, startDate, endDate). + Group("DATE(transaction_date)"). + Order("date ASC"). + Rows() + + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var dateStr string // Using string to handle potential format differences + var count int + // Scan into generic types then convert + // Some drivers return date as time.Time, some as string/bytes + // Let's try scan into string first (common for DATE() function result) + // Or scan into interface{} to be safe + if err := rows.Scan(&dateStr, &count); err != nil { + // If string scan fails, try time.Time + // Unfortunately we can't rewind rows. scan is one-way. + // But usually drivers handle string conversion for DATE() + // If this fails we might need to adjust based on specific DB driver + return nil, err + } + // Normalize date string to YYYY-MM-DD (take first 10 chars if it includes time) + if len(dateStr) > 10 { + dateStr = dateStr[:10] + } + + results = append(results, models.DailyContribution{ + Date: dateStr, + Count: count, + }) + } + + return results, nil +} diff --git a/internal/service/streak_service.go b/internal/service/streak_service.go index d02dcb1..e35738b 100644 --- a/internal/service/streak_service.go +++ b/internal/service/streak_service.go @@ -241,3 +241,11 @@ func (s *StreakService) CheckAndResetStreak(userID uint) error { return nil } + +// GetContributionData returns daily transaction counts for the past year +func (s *StreakService) GetContributionData(userID uint) ([]models.DailyContribution, error) { + now := time.Now() + startDate := now.AddDate(-1, 0, 0) // 1 year ago + + return s.repo.GetDailyContribution(userID, startDate, now) +}