feat: 新增路由配置、交易处理和用户连击功能,并初始化相关服务与处理器
This commit is contained in:
73
internal/handler/streak_handler.go
Normal file
73
internal/handler/streak_handler.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"accounting-app/internal/service"
|
||||
"accounting-app/pkg/api"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// StreakHandler handles HTTP requests for user streaks
|
||||
type StreakHandler struct {
|
||||
streakService *service.StreakService
|
||||
}
|
||||
|
||||
// NewStreakHandler creates a new StreakHandler instance
|
||||
func NewStreakHandler(streakService *service.StreakService) *StreakHandler {
|
||||
return &StreakHandler{
|
||||
streakService: streakService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetStreak handles GET /api/v1/user/streak
|
||||
// Returns the user's current streak information
|
||||
func (h *StreakHandler) GetStreak(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
api.Unauthorized(c, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
// Check and reset streak if needed (in case user hasn't opened app in a while)
|
||||
if err := h.streakService.CheckAndResetStreak(userID.(uint)); err != nil {
|
||||
// Log error but don't fail the request
|
||||
// Just continue to get streak info
|
||||
}
|
||||
|
||||
streakInfo, err := h.streakService.GetStreakInfo(userID.(uint))
|
||||
if err != nil {
|
||||
api.InternalError(c, "Failed to get streak info: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
api.Success(c, streakInfo)
|
||||
}
|
||||
|
||||
// RecalculateStreak handles POST /api/v1/user/streak/recalculate
|
||||
// Recalculates the streak from transaction history (admin/debug use)
|
||||
func (h *StreakHandler) RecalculateStreak(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
api.Unauthorized(c, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.streakService.RecalculateStreak(userID.(uint)); err != nil {
|
||||
api.InternalError(c, "Failed to recalculate streak: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
streakInfo, err := h.streakService.GetStreakInfo(userID.(uint))
|
||||
if err != nil {
|
||||
api.InternalError(c, "Failed to get streak info: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
api.Success(c, streakInfo)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
// TransactionHandler handles HTTP requests for transaction operations
|
||||
type TransactionHandler struct {
|
||||
transactionService *service.TransactionService
|
||||
streakService *service.StreakService // Optional: for updating streak on transaction creation
|
||||
}
|
||||
|
||||
// NewTransactionHandler creates a new TransactionHandler instance
|
||||
@@ -24,6 +25,14 @@ func NewTransactionHandler(transactionService *service.TransactionService) *Tran
|
||||
}
|
||||
}
|
||||
|
||||
// NewTransactionHandlerWithStreak creates a new TransactionHandler with streak tracking support
|
||||
func NewTransactionHandlerWithStreak(transactionService *service.TransactionService, streakService *service.StreakService) *TransactionHandler {
|
||||
return &TransactionHandler{
|
||||
transactionService: transactionService,
|
||||
streakService: streakService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTransactionRequest represents the request body for creating a transaction
|
||||
type CreateTransactionRequest struct {
|
||||
Amount float64 `json:"amount" binding:"required"`
|
||||
@@ -96,6 +105,14 @@ func (h *TransactionHandler) CreateTransaction(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update streak if streak service is configured
|
||||
if h.streakService != nil {
|
||||
// Fire and forget - don't fail the transaction if streak update fails
|
||||
go func() {
|
||||
_ = h.streakService.UpdateStreak(userID.(uint), transactionDate)
|
||||
}()
|
||||
}
|
||||
|
||||
api.Created(c, transaction)
|
||||
}
|
||||
|
||||
|
||||
31
internal/models/user_streak.go
Normal file
31
internal/models/user_streak.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserStreak represents a user's consecutive recording streak
|
||||
type UserStreak struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
UserID uint `gorm:"uniqueIndex;not null" json:"user_id"`
|
||||
CurrentStreak int `gorm:"default:0" json:"current_streak"` // 当前连续天数
|
||||
LongestStreak int `gorm:"default:0" json:"longest_streak"` // 最长连续记录
|
||||
LastRecordDate *time.Time `gorm:"type:date" json:"last_record_date"` // 最后记账日期
|
||||
TotalRecordDays int `gorm:"default:0" json:"total_record_days"` // 累计记账天数
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for UserStreak
|
||||
func (UserStreak) TableName() string {
|
||||
return "user_streaks"
|
||||
}
|
||||
|
||||
// StreakInfo represents the streak information returned to frontend
|
||||
type StreakInfo struct {
|
||||
CurrentStreak int `json:"current_streak"` // 当前连续天数
|
||||
LongestStreak int `json:"longest_streak"` // 最长连续记录
|
||||
TotalRecordDays int `json:"total_record_days"` // 累计记账天数
|
||||
HasRecordToday bool `json:"has_record_today"` // 今天是否已记账
|
||||
Message string `json:"message"` // 提示信息
|
||||
}
|
||||
109
internal/repository/streak_repository.go
Normal file
109
internal/repository/streak_repository.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"accounting-app/internal/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// StreakRepository handles database operations for user streaks
|
||||
type StreakRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewStreakRepository creates a new StreakRepository instance
|
||||
func NewStreakRepository(db *gorm.DB) *StreakRepository {
|
||||
return &StreakRepository{db: db}
|
||||
}
|
||||
|
||||
// GetByUserID retrieves a user's streak record
|
||||
func (r *StreakRepository) GetByUserID(userID uint) (*models.UserStreak, error) {
|
||||
var streak models.UserStreak
|
||||
if err := r.db.Where("user_id = ?", userID).First(&streak).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil // Return nil without error if not found
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &streak, nil
|
||||
}
|
||||
|
||||
// Create creates a new streak record
|
||||
func (r *StreakRepository) Create(streak *models.UserStreak) error {
|
||||
return r.db.Create(streak).Error
|
||||
}
|
||||
|
||||
// Update updates an existing streak record
|
||||
func (r *StreakRepository) Update(streak *models.UserStreak) error {
|
||||
return r.db.Save(streak).Error
|
||||
}
|
||||
|
||||
// GetOrCreate retrieves existing streak record or creates a new one
|
||||
func (r *StreakRepository) GetOrCreate(userID uint) (*models.UserStreak, error) {
|
||||
streak, err := r.GetByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if streak == nil {
|
||||
// Create new streak record
|
||||
streak = &models.UserStreak{
|
||||
UserID: userID,
|
||||
CurrentStreak: 0,
|
||||
LongestStreak: 0,
|
||||
TotalRecordDays: 0,
|
||||
}
|
||||
if err := r.Create(streak); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return streak, nil
|
||||
}
|
||||
|
||||
// HasTransactionOnDate checks if user has any transaction on the given date
|
||||
func (r *StreakRepository) HasTransactionOnDate(userID uint, date time.Time) (bool, error) {
|
||||
var count int64
|
||||
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||||
endOfDay := startOfDay.Add(24 * time.Hour)
|
||||
|
||||
err := r.db.Model(&models.Transaction{}).
|
||||
Where("user_id = ? AND transaction_date >= ? AND transaction_date < ?", userID, startOfDay, endOfDay).
|
||||
Count(&count).Error
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// GetTransactionDatesInRange returns all dates with transactions in a date range
|
||||
func (r *StreakRepository) GetTransactionDatesInRange(userID uint, startDate, endDate time.Time) ([]time.Time, error) {
|
||||
var dates []time.Time
|
||||
|
||||
rows, err := r.db.Model(&models.Transaction{}).
|
||||
Select("DATE(transaction_date) as date").
|
||||
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 date time.Time
|
||||
if err := rows.Scan(&date); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dates = append(dates, date)
|
||||
}
|
||||
|
||||
return dates, nil
|
||||
}
|
||||
@@ -50,6 +50,7 @@ func Setup(db *gorm.DB, yunAPIClient *service.YunAPIClient, cfg *config.Config)
|
||||
userSettingsRepo := repository.NewUserSettingsRepository(db)
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
notificationRepo := repository.NewNotificationRepository(db)
|
||||
streakRepo := repository.NewStreakRepository(db)
|
||||
|
||||
// Initialize auth services
|
||||
authService := service.NewAuthService(userRepo, cfg)
|
||||
@@ -85,6 +86,7 @@ func Setup(db *gorm.DB, yunAPIClient *service.YunAPIClient, cfg *config.Config)
|
||||
refundService := service.NewRefundService(db, transactionRepo, accountRepo)
|
||||
userSettingsService := service.NewUserSettingsService(userSettingsRepo)
|
||||
notificationService := service.NewNotificationService(notificationRepo)
|
||||
streakService := service.NewStreakService(streakRepo)
|
||||
|
||||
// Feature: financial-core-upgrade - Initialize new services
|
||||
subAccountService := service.NewSubAccountService(accountRepo, db)
|
||||
@@ -100,7 +102,8 @@ func Setup(db *gorm.DB, yunAPIClient *service.YunAPIClient, cfg *config.Config)
|
||||
categoryHandler := handler.NewCategoryHandler(categoryService)
|
||||
tagHandler := handler.NewTagHandler(tagService)
|
||||
classificationHandler := handler.NewClassificationHandler(classificationService)
|
||||
transactionHandler := handler.NewTransactionHandler(transactionService)
|
||||
transactionHandler := handler.NewTransactionHandlerWithStreak(transactionService, streakService)
|
||||
|
||||
imageHandler := handler.NewImageHandler(imageService)
|
||||
recurringHandler := handler.NewRecurringTransactionHandler(recurringService)
|
||||
exchangeRateHandler := handler.NewExchangeRateHandlerWithClient(exchangeRateService, yunAPIClient)
|
||||
@@ -128,6 +131,9 @@ func Setup(db *gorm.DB, yunAPIClient *service.YunAPIClient, cfg *config.Config)
|
||||
// Feature: health-score - Initialize health score handler
|
||||
healthScoreHandler := handler.NewHealthScoreHandler(healthScoreService)
|
||||
|
||||
// Feature: streak - Initialize streak handler
|
||||
streakHandler := handler.NewStreakHandler(streakService)
|
||||
|
||||
// AI Bookkeeping Service and Handler
|
||||
aiBookkeepingService := service.NewAIBookkeepingService(
|
||||
cfg,
|
||||
@@ -253,6 +259,9 @@ func Setup(db *gorm.DB, yunAPIClient *service.YunAPIClient, cfg *config.Config)
|
||||
|
||||
// Register health score routes
|
||||
healthScoreHandler.RegisterRoutes(protected)
|
||||
|
||||
// Feature: streak - Register streak routes
|
||||
streakHandler.RegisterRoutes(protected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,6 +323,7 @@ func SetupWithRedis(db *gorm.DB, yunAPIClient *service.YunAPIClient, redisClient
|
||||
userSettingsRepo := repository.NewUserSettingsRepository(db)
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
notificationRepo := repository.NewNotificationRepository(db)
|
||||
streakRepo := repository.NewStreakRepository(db)
|
||||
|
||||
// Initialize auth services
|
||||
authService := service.NewAuthService(userRepo, cfg)
|
||||
@@ -348,6 +358,7 @@ func SetupWithRedis(db *gorm.DB, yunAPIClient *service.YunAPIClient, redisClient
|
||||
refundService := service.NewRefundService(db, transactionRepo, accountRepo)
|
||||
userSettingsService := service.NewUserSettingsService(userSettingsRepo)
|
||||
notificationService := service.NewNotificationService(notificationRepo)
|
||||
streakService := service.NewStreakService(streakRepo)
|
||||
|
||||
// Feature: financial-core-upgrade - Initialize new services
|
||||
subAccountService := service.NewSubAccountService(accountRepo, db)
|
||||
@@ -367,7 +378,7 @@ func SetupWithRedis(db *gorm.DB, yunAPIClient *service.YunAPIClient, redisClient
|
||||
categoryHandler := handler.NewCategoryHandler(categoryService)
|
||||
tagHandler := handler.NewTagHandler(tagService)
|
||||
classificationHandler := handler.NewClassificationHandler(classificationService)
|
||||
transactionHandler := handler.NewTransactionHandler(transactionService)
|
||||
transactionHandler := handler.NewTransactionHandlerWithStreak(transactionService, streakService)
|
||||
imageHandler := handler.NewImageHandler(imageService)
|
||||
recurringHandler := handler.NewRecurringTransactionHandler(recurringService)
|
||||
reportHandler := handler.NewReportHandler(reportService, pdfExportService, excelExportService)
|
||||
@@ -391,6 +402,9 @@ func SetupWithRedis(db *gorm.DB, yunAPIClient *service.YunAPIClient, redisClient
|
||||
defaultAccountHandler := handler.NewDefaultAccountHandler(userSettingsServiceWithAccounts)
|
||||
interestHandler := handler.NewInterestHandler(interestService, nil)
|
||||
|
||||
// Feature: streak - Initialize streak handler
|
||||
streakHandler := handler.NewStreakHandler(streakService)
|
||||
|
||||
// AI Bookkeeping Service and Handler for Redis setup
|
||||
aiBookkeepingServiceRedis := service.NewAIBookkeepingService(
|
||||
cfg,
|
||||
@@ -523,6 +537,9 @@ func SetupWithRedis(db *gorm.DB, yunAPIClient *service.YunAPIClient, redisClient
|
||||
// Register notification routes
|
||||
notificationHandler.RegisterUserRoutes(protected)
|
||||
notificationHandler.RegisterAdminRoutes(v1)
|
||||
|
||||
// Feature: streak - Register streak routes
|
||||
streakHandler.RegisterRoutes(v1)
|
||||
}
|
||||
|
||||
return r, syncScheduler
|
||||
|
||||
243
internal/service/streak_service.go
Normal file
243
internal/service/streak_service.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"accounting-app/internal/models"
|
||||
"accounting-app/internal/repository"
|
||||
)
|
||||
|
||||
// StreakService handles business logic for user streaks
|
||||
type StreakService struct {
|
||||
repo *repository.StreakRepository
|
||||
}
|
||||
|
||||
// NewStreakService creates a new StreakService instance
|
||||
func NewStreakService(repo *repository.StreakRepository) *StreakService {
|
||||
return &StreakService{repo: repo}
|
||||
}
|
||||
|
||||
// GetStreakInfo returns the streak information for a user
|
||||
func (s *StreakService) GetStreakInfo(userID uint) (*models.StreakInfo, error) {
|
||||
streak, err := s.repo.GetOrCreate(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
|
||||
// Check if user has recorded today
|
||||
hasRecordToday, err := s.repo.HasTransactionOnDate(userID, today)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Determine message based on streak status
|
||||
var message string
|
||||
if hasRecordToday {
|
||||
if streak.CurrentStreak >= 7 {
|
||||
message = "太厉害了!连续记账一周以上 🔥"
|
||||
} else if streak.CurrentStreak >= 3 {
|
||||
message = "继续保持,养成好习惯!💪"
|
||||
} else {
|
||||
message = "今日已记账,明天继续哦 ✅"
|
||||
}
|
||||
} else {
|
||||
if streak.CurrentStreak > 0 {
|
||||
message = "今天还没有记账哦,别断了连续记录!"
|
||||
} else {
|
||||
message = "今天还没有记账哦,开始记录吧!"
|
||||
}
|
||||
}
|
||||
|
||||
return &models.StreakInfo{
|
||||
CurrentStreak: streak.CurrentStreak,
|
||||
LongestStreak: streak.LongestStreak,
|
||||
TotalRecordDays: streak.TotalRecordDays,
|
||||
HasRecordToday: hasRecordToday,
|
||||
Message: message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateStreak updates the streak when a transaction is created
|
||||
// This should be called after a transaction is created
|
||||
func (s *StreakService) UpdateStreak(userID uint, transactionDate time.Time) error {
|
||||
streak, err := s.repo.GetOrCreate(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Normalize transaction date to start of day
|
||||
txDate := time.Date(transactionDate.Year(), transactionDate.Month(), transactionDate.Day(), 0, 0, 0, 0, transactionDate.Location())
|
||||
now := time.Now()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
|
||||
// Only update streak for today's date or past dates (not future)
|
||||
if txDate.After(today) {
|
||||
return nil // Don't update streak for future transactions
|
||||
}
|
||||
|
||||
// If this is the first record ever
|
||||
if streak.LastRecordDate == nil {
|
||||
streak.CurrentStreak = 1
|
||||
streak.LongestStreak = 1
|
||||
streak.TotalRecordDays = 1
|
||||
streak.LastRecordDate = &txDate
|
||||
return s.repo.Update(streak)
|
||||
}
|
||||
|
||||
lastDate := *streak.LastRecordDate
|
||||
lastDateNormalized := time.Date(lastDate.Year(), lastDate.Month(), lastDate.Day(), 0, 0, 0, 0, lastDate.Location())
|
||||
|
||||
// If already recorded on this date, no update needed
|
||||
if txDate.Equal(lastDateNormalized) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If recording for a new date
|
||||
if txDate.After(lastDateNormalized) {
|
||||
daysDiff := int(txDate.Sub(lastDateNormalized).Hours() / 24)
|
||||
|
||||
if daysDiff == 1 {
|
||||
// Consecutive day - increment streak
|
||||
streak.CurrentStreak++
|
||||
} else {
|
||||
// Streak broken - reset to 1
|
||||
streak.CurrentStreak = 1
|
||||
}
|
||||
|
||||
// Update longest streak if current is higher
|
||||
if streak.CurrentStreak > streak.LongestStreak {
|
||||
streak.LongestStreak = streak.CurrentStreak
|
||||
}
|
||||
|
||||
// Increment total record days
|
||||
streak.TotalRecordDays++
|
||||
streak.LastRecordDate = &txDate
|
||||
} else {
|
||||
// Recording for a past date - just increment total if it's a new day
|
||||
hasRecord, err := s.repo.HasTransactionOnDate(userID, txDate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// This is a bit tricky - we'd need to check if this specific date already has records
|
||||
// For simplicity, if it's a past date and we're adding a new transaction,
|
||||
// we'll just increment total (this might not be 100% accurate for edits)
|
||||
if !hasRecord {
|
||||
streak.TotalRecordDays++
|
||||
}
|
||||
}
|
||||
|
||||
return s.repo.Update(streak)
|
||||
}
|
||||
|
||||
// RecalculateStreak recalculates the entire streak from transaction history
|
||||
// This is useful for fixing streak data or after bulk imports
|
||||
func (s *StreakService) RecalculateStreak(userID uint) error {
|
||||
streak, err := s.repo.GetOrCreate(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get all transaction dates for the user (last 365 days should be enough)
|
||||
now := time.Now()
|
||||
startDate := now.AddDate(-1, 0, 0) // 1 year ago
|
||||
|
||||
dates, err := s.repo.GetTransactionDatesInRange(userID, startDate, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(dates) == 0 {
|
||||
// No transactions, reset streak
|
||||
streak.CurrentStreak = 0
|
||||
streak.LongestStreak = 0
|
||||
streak.TotalRecordDays = 0
|
||||
streak.LastRecordDate = nil
|
||||
return s.repo.Update(streak)
|
||||
}
|
||||
|
||||
// Calculate streaks
|
||||
streak.TotalRecordDays = len(dates)
|
||||
|
||||
// Find longest streak and current streak
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
|
||||
currentStreak := 1
|
||||
longestStreak := 1
|
||||
tempStreak := 1
|
||||
|
||||
for i := 1; i < len(dates); i++ {
|
||||
prevDate := dates[i-1]
|
||||
currDate := dates[i]
|
||||
daysDiff := int(currDate.Sub(prevDate).Hours() / 24)
|
||||
|
||||
if daysDiff == 1 {
|
||||
tempStreak++
|
||||
if tempStreak > longestStreak {
|
||||
longestStreak = tempStreak
|
||||
}
|
||||
} else {
|
||||
tempStreak = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Determine current streak (must include today or yesterday)
|
||||
lastRecordDate := dates[len(dates)-1]
|
||||
lastRecordNormalized := time.Date(lastRecordDate.Year(), lastRecordDate.Month(), lastRecordDate.Day(), 0, 0, 0, 0, lastRecordDate.Location())
|
||||
|
||||
daysSinceLastRecord := int(today.Sub(lastRecordNormalized).Hours() / 24)
|
||||
|
||||
if daysSinceLastRecord <= 1 {
|
||||
// Calculate current streak going backwards from last record
|
||||
currentStreak = 1
|
||||
for i := len(dates) - 2; i >= 0; i-- {
|
||||
currDate := dates[i+1]
|
||||
prevDate := dates[i]
|
||||
daysDiff := int(currDate.Sub(prevDate).Hours() / 24)
|
||||
|
||||
if daysDiff == 1 {
|
||||
currentStreak++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Streak is broken
|
||||
currentStreak = 0
|
||||
}
|
||||
|
||||
streak.CurrentStreak = currentStreak
|
||||
streak.LongestStreak = longestStreak
|
||||
streak.LastRecordDate = &lastRecordNormalized
|
||||
|
||||
return s.repo.Update(streak)
|
||||
}
|
||||
|
||||
// CheckAndResetStreak checks if streak should be reset (called daily or on app open)
|
||||
func (s *StreakService) CheckAndResetStreak(userID uint) error {
|
||||
streak, err := s.repo.GetOrCreate(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if streak.LastRecordDate == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
lastDate := *streak.LastRecordDate
|
||||
lastDateNormalized := time.Date(lastDate.Year(), lastDate.Month(), lastDate.Day(), 0, 0, 0, 0, lastDate.Location())
|
||||
|
||||
daysDiff := int(today.Sub(lastDateNormalized).Hours() / 24)
|
||||
|
||||
// If more than 1 day has passed without recording, reset streak
|
||||
if daysDiff > 1 {
|
||||
streak.CurrentStreak = 0
|
||||
return s.repo.Update(streak)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user