package logic import ( "context" "time" "spark-server/internal/module/energy/entity" "spark-server/internal/pkg/errs" ) // DayStatus is one calendar cell for the sign-in page. type DayStatus struct { Day int64 `json:"day"` SignInStatus string `json:"sign_in_status"` RewardPoint int `json:"reward_point"` IsDouble bool `json:"is_double"` IsSurprise bool `json:"is_surprise"` VideoDouble bool `json:"video_double"` } // CalendarView matches the legacy SignInBean shape used by the Android page. type CalendarView struct { ContinueDays int `json:"continue_days"` StartDate int64 `json:"start_date"` NeedDays int `json:"need_days"` RewardPoint int `json:"reward_point"` TodaySignStatus string `json:"today_sign_status"` TotalRewardPoint int64 `json:"total_reward_point"` DrawPrizeStatus int `json:"draw_prize_status"` Energy int64 `json:"energy"` StatusList []DayStatus `json:"status_list"` } // Calendar returns month status for year/month (1–12). year=0 uses current year. func (s *Service) Calendar(ctx context.Context, userID int64, year, month int) (*CalendarView, error) { if !s.ready() || userID <= 0 { return nil, errs.ErrUnauthorized } now := s.now() if year <= 0 { year = now.Year() } if month < 1 || month > 12 { month = int(now.Month()) } first := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, s.loc) last := first.AddDate(0, 1, -1) fromDay, toDay := dayKeyOf(first), dayKeyOf(last) today := dayKeyOf(now) records, err := s.repo.ListSignsInRange(ctx, userID, fromDay, toDay) if err != nil { return nil, err } byDay := map[int]*entity.SignRecord{} for _, r := range records { byDay[r.DayKey] = r } streak, start := s.computeStreak(ctx, userID, today) need := entity.DefaultNeedDays - (streak % entity.DefaultNeedDays) if need == entity.DefaultNeedDays && streak > 0 && streak%entity.DefaultNeedDays == 0 { need = entity.DefaultNeedDays } if streak == 0 { need = entity.DefaultNeedDays } nextReward := entity.CycleRewards(streak + 1) wallet, err := s.repo.EnsureWallet(ctx, userID) if err != nil { return nil, err } list := make([]DayStatus, 0, last.Day()) todayStatus := entity.StatusUnsigned for d := 1; d <= last.Day(); d++ { day := year*10000 + month*100 + d st := DayStatus{ Day: int64(day), RewardPoint: entity.CycleRewards(1), } if rec, ok := byDay[day]; ok { if rec.Kind == entity.KindMakeup { st.SignInStatus = entity.StatusMakeupDone } else { st.SignInStatus = entity.StatusSigned } st.RewardPoint = rec.Reward } else if day == today { st.SignInStatus = entity.StatusUnsigned st.RewardPoint = nextReward } else if day < today { st.SignInStatus = entity.StatusCanMakeup st.RewardPoint = entity.CycleRewards(1) } else { // Future days: not yet signed (client treats as empty slot). st.SignInStatus = entity.StatusUnsigned st.RewardPoint = entity.CycleRewards(1) } if day == today { todayStatus = st.SignInStatus if todayStatus == entity.StatusUnsigned { st.RewardPoint = nextReward } } list = append(list, st) } return &CalendarView{ ContinueDays: streak, StartDate: int64(start), NeedDays: need, RewardPoint: nextReward, TodaySignStatus: todayStatus, TotalRewardPoint: wallet.Energy, DrawPrizeStatus: 0, Energy: wallet.Energy, StatusList: list, }, nil } func (s *Service) computeStreak(ctx context.Context, userID int64, today int) (streak int, start int) { recent, err := s.repo.ListRecentSigns(ctx, userID, 60) if err != nil || len(recent) == 0 { return 0, 0 } have := map[int]bool{} for _, r := range recent { have[r.DayKey] = true } cursor := today if !have[today] { cursor = addDays(today, -1) } if !have[cursor] { return 0, 0 } start = cursor for have[cursor] { streak++ start = cursor cursor = addDays(cursor, -1) } return streak, start }