feat: 添加用户记账连击(streak)功能,包括服务、处理器、模型和仓库层。
This commit is contained in:
@@ -66,8 +66,27 @@ func (h *StreakHandler) RecalculateStreak(c *gin.Context) {
|
|||||||
api.Success(c, streakInfo)
|
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
|
// RegisterRoutes registers streak-related routes
|
||||||
func (h *StreakHandler) RegisterRoutes(rg *gin.RouterGroup) {
|
func (h *StreakHandler) RegisterRoutes(rg *gin.RouterGroup) {
|
||||||
rg.GET("/user/streak", h.GetStreak)
|
rg.GET("/user/streak", h.GetStreak)
|
||||||
rg.POST("/user/streak/recalculate", h.RecalculateStreak)
|
rg.POST("/user/streak/recalculate", h.RecalculateStreak)
|
||||||
|
rg.GET("/user/streak/contribution", h.GetContributionData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,3 +29,9 @@ type StreakInfo struct {
|
|||||||
HasRecordToday bool `json:"has_record_today"` // 今天是否已记账
|
HasRecordToday bool `json:"has_record_today"` // 今天是否已记账
|
||||||
Message string `json:"message"` // 提示信息
|
Message string `json:"message"` // 提示信息
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DailyContribution represents transaction count for a specific date
|
||||||
|
type DailyContribution struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -107,3 +107,51 @@ func (r *StreakRepository) GetTransactionDatesInRange(userID uint, startDate, en
|
|||||||
|
|
||||||
return dates, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -241,3 +241,11 @@ func (s *StreakService) CheckAndResetStreak(userID uint) error {
|
|||||||
|
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user