feat: 新增路由配置、交易处理和用户连击功能,并初始化相关服务与处理器

This commit is contained in:
2026-01-28 10:08:48 +08:00
parent 4d024eba8e
commit e811256d99
7 changed files with 508 additions and 2 deletions

View 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
}