package services import ( "errors" "time" "github.com/inchstep/server/internal/models" "gorm.io/gorm" ) type CheckInService struct { db *gorm.DB } func NewCheckInService(db *gorm.DB) *CheckInService { return &CheckInService{db: db} } // CreateItem 创建打卡项 func (s *CheckInService) CreateItem(userID string, item *models.CheckInItem) error { item.ID = generateID() item.UserID = userID item.Status = models.StatusActive item.CreatedAt = time.Now() return s.db.Create(item).Error } // GetUserItems 获取用户打卡项列表 func (s *CheckInService) GetUserItems(userID string) ([]models.CheckInItem, error) { var items []models.CheckInItem err := s.db.Where("user_id = ? AND status != ?", userID, models.StatusDeleted). Order("created_at desc").Find(&items).Error return items, err } // GetItemByID 获取打卡项详情 func (s *CheckInService) GetItemByID(id, userID string) (*models.CheckInItem, error) { var item models.CheckInItem err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&item).Error if err != nil { return nil, err } return &item, nil } // UpdateItem 更新打卡项 func (s *CheckInService) UpdateItem(id, userID string, updates map[string]interface{}) (*models.CheckInItem, error) { allowed := map[string]bool{"name": true, "icon": true, "category": true, "daily_goal": true, "cycle_type": true, "privacy": true} filtered := make(map[string]interface{}) for k, v := range updates { if allowed[k] { filtered[k] = v } } result := s.db.Model(&models.CheckInItem{}).Where("id = ? AND user_id = ?", id, userID).Updates(filtered) if result.RowsAffected == 0 { return nil, errors.New("打卡项不存在或无权限") } return s.GetItemByID(id, userID) } // DeleteItem 软删除打卡项 func (s *CheckInService) DeleteItem(id, userID string) error { return s.db.Model(&models.CheckInItem{}). Where("id = ? AND user_id = ?", id, userID). Update("status", models.StatusDeleted).Error } // PauseItem 暂停打卡项 func (s *CheckInService) PauseItem(id, userID string) error { return s.db.Model(&models.CheckInItem{}). Where("id = ? AND user_id = ?", id, userID). Update("status", models.StatusPaused).Error } // ResumeItem 恢复打卡项 func (s *CheckInService) ResumeItem(id, userID string) error { return s.db.Model(&models.CheckInItem{}). Where("id = ? AND user_id = ?", id, userID). Update("status", models.StatusActive).Error } // CreateRecord 创建打卡记录 func (s *CheckInService) CreateRecord(userID, itemID string, checkDate time.Time, note, images *string) (*models.CheckInRecord, error) { // 验证打卡项 item, err := s.GetItemByID(itemID, userID) if err != nil { return nil, errors.New("打卡项不存在或无权限") } if item.Status != models.StatusActive { return nil, errors.New("打卡项当前不可用(已暂停或归档)") } // 检查当日是否已打卡 startOfDay := time.Date(checkDate.Year(), checkDate.Month(), checkDate.Day(), 0, 0, 0, 0, checkDate.Location()) endOfDay := startOfDay.Add(24 * time.Hour) var count int64 s.db.Model(&models.CheckInRecord{}). Where("item_id = ? AND user_id = ? AND check_date >= ? AND check_date < ?", itemID, userID, startOfDay, endOfDay).Count(&count) if count > 0 { return nil, errors.New("今日已打卡") } record := &models.CheckInRecord{ ID: generateID(), ItemID: itemID, UserID: userID, CheckDate: checkDate, Status: models.RecordNormal, Note: note, Images: images, CreatedAt: time.Now(), } if err := s.db.Create(record).Error; err != nil { return nil, err } return record, nil } // MakeupRecord 补卡(7 天内,每月 3 次免费,超出扣积分) func (s *CheckInService) MakeupRecord(userID, itemID string, checkDate time.Time, costPoints int) (*models.CheckInRecord, error) { now := time.Now() diff := now.Sub(checkDate) if diff.Hours() > 7*24 || diff.Hours() < 0 { return nil, errors.New("只能补签最近 7 天的记录") } // 检查本月已用补卡次数 currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) var makeupCount int64 s.db.Model(&models.CheckInRecord{}). Where("user_id = ? AND status = ? AND created_at >= ?", userID, models.RecordMakeup, currentMonthStart).Count(&makeupCount) // 计算需要消耗的积分 freeCount := int64(3) if makeupCount >= freeCount { var user models.User if err := s.db.First(&user, "id = ?", userID).Error; err != nil { return nil, err } if user.Points < costPoints { return nil, errors.New("积分不足,补卡需要 5 积分") } s.db.Model(&user).Update("points", user.Points-costPoints) } record := &models.CheckInRecord{ ID: generateID(), ItemID: itemID, UserID: userID, CheckDate: checkDate, Status: models.RecordMakeup, CreatedAt: time.Now(), } return record, s.db.Create(record).Error } // GetRecords 获取打卡记录列表 func (s *CheckInService) GetRecords(userID, itemID string, start, end time.Time) ([]models.CheckInRecord, error) { var records []models.CheckInRecord query := s.db.Where("user_id = ?", userID) if itemID != "" { query = query.Where("item_id = ?", itemID) } err := query.Where("check_date >= ? AND check_date < ?", start, end). Order("check_date desc").Find(&records).Error return records, err } // GetStreak 计算连续打卡天数(时区感知) func (s *CheckInService) GetStreak(itemID, userID string) (int, error) { var records []models.CheckInRecord if err := s.db.Where("item_id = ? AND user_id = ?", itemID, userID). Order("check_date desc").Find(&records).Error; err != nil { return 0, err } if len(records) == 0 { return 0, nil } streak := 0 today := time.Now().Truncate(24 * time.Hour) for i, r := range records { recordDate := r.CheckDate.Truncate(24 * time.Hour) if i == 0 { if recordDate.Before(today.Add(-48 * time.Hour)) { return 0, nil // 最近一次打卡超过 2 天前,连续已断 } streak = 1 continue } prevDate := records[i-1].CheckDate.Truncate(24 * time.Hour) if prevDate.Sub(recordDate).Hours() == 24 { streak++ } else { break } } return streak, nil } // GetMonthlyStats 获取月度统计 func (s *CheckInService) GetMonthlyStats(userID string, year, month int) (map[string]interface{}, error) { startOfMonth := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.Local) endOfMonth := startOfMonth.AddDate(0, 1, 0) var totalItems int64 s.db.Model(&models.CheckInItem{}).Where("user_id = ? AND status = ?", userID, models.StatusActive).Count(&totalItems) var totalRecords int64 s.db.Model(&models.CheckInRecord{}). Where("user_id = ? AND check_date >= ? AND check_date < ?", userID, startOfMonth, endOfMonth). Count(&totalRecords) // 按分类统计 type CategoryCount struct { Category string Count int64 } var catStats []CategoryCount s.db.Model(&models.CheckInItem{}). Select("category, count(*) as count"). Where("user_id = ? AND status = ?", userID, models.StatusActive). Group("category").Find(&catStats) return map[string]interface{}{ "totalItems": totalItems, "totalRecords": totalRecords, "byCategory": catStats, }, nil } // GetTodayStatus 获取用户今天已打卡的打卡项 ID 列表 func (s *CheckInService) GetTodayStatus(userID string) (map[string]bool, error) { today := time.Now().Truncate(24 * time.Hour) endOfToday := today.Add(24 * time.Hour) var records []models.CheckInRecord if err := s.db.Where("user_id = ? AND check_date >= ? AND check_date < ?", userID, today, endOfToday).Find(&records).Error; err != nil { return nil, err } result := make(map[string]bool) for _, r := range records { result[r.ItemID] = true } return result, nil }