package service import ( "context" "fmt" "time" "with_you/internal/repository" "go.uber.org/zap" ) // UserActivityService 用户活跃统计服务接口 type UserActivityService interface { // RecordUserActive 记录用户活跃(核心方法) RecordUserActive(ctx context.Context, userID, loginType, ip, userAgent string) error // GetDAU 获取日活用户数 GetDAU(ctx context.Context, date time.Time) (int64, error) // GetWAU 获取周活用户数 GetWAU(ctx context.Context, year int, week int) (int64, error) // GetMAU 获取月活用户数 GetMAU(ctx context.Context, year int, month int) (int64, error) // GetRetention 计算留存率 // day1: 基准日期, dayN: N天后的日期 // 返回: day1的活跃用户在dayN仍然活跃的比例 GetRetention(ctx context.Context, day1, dayN time.Time) (float64, error) // GetActivityStats 获取综合统计数据 GetActivityStats(ctx context.Context) (*ActivityStatsResponse, error) } // userActivityService 实现 type userActivityService struct { repo repository.UserActivityRepository } // NewUserActivityService 创建用户活跃统计服务 func NewUserActivityService(repo repository.UserActivityRepository) UserActivityService { return &userActivityService{repo: repo} } // ActivityStatsResponse 活跃统计响应 type ActivityStatsResponse struct { Today DailyStats `json:"today"` Yesterday DailyStats `json:"yesterday"` ThisWeek PeriodStats `json:"this_week"` ThisMonth PeriodStats `json:"this_month"` Retention RetentionStats `json:"retention"` } // DailyStats 日统计 type DailyStats struct { Date string `json:"date"` DAU int64 `json:"dau"` NewUsers int64 `json:"new_users"` } // PeriodStats 周期统计 type PeriodStats struct { Period string `json:"period"` ActiveUsers int64 `json:"active_users"` } // RetentionStats 留存统计 type RetentionStats struct { Day1 float64 `json:"day_1"` // 次日留存 Day7 float64 `json:"day_7"` // 7日留存 Day30 float64 `json:"day_30"` // 30日留存 } // RecordUserActive 记录用户活跃 func (s *userActivityService) RecordUserActive(ctx context.Context, userID, loginType, ip, userAgent string) error { // 1. 记录到 Redis(同步,快速) if err := s.repo.RecordActivityToRedis(ctx, userID); err != nil { // 记录日志但不阻断流程 zap.L().Warn("failed to record activity to redis", zap.Error(err), ) } // 2. 异步写入数据库(可以使用 goroutine 或消息队列) go func() { bgCtx := context.Background() if err := s.repo.RecordActivity(bgCtx, userID, loginType, ip, userAgent); err != nil { zap.L().Warn("failed to record activity to db", zap.Error(err), ) } }() return nil } // GetDAU 获取日活用户数 func (s *userActivityService) GetDAU(ctx context.Context, date time.Time) (int64, error) { return s.repo.GetDAUFromRedis(ctx, date) } // GetWAU 获取周活用户数 func (s *userActivityService) GetWAU(ctx context.Context, year int, week int) (int64, error) { return s.repo.GetWAUFromRedis(ctx, year, week) } // GetMAU 获取月活用户数 func (s *userActivityService) GetMAU(ctx context.Context, year int, month int) (int64, error) { return s.repo.GetMAUFromRedis(ctx, year, month) } // GetRetention 计算留存率 func (s *userActivityService) GetRetention(ctx context.Context, day1, dayN time.Time) (float64, error) { // 1. 获取 day1 的活跃用户列表 day1Users, err := s.repo.GetDAUUserIDsFromRedis(ctx, day1) if err != nil { return 0, err } if len(day1Users) == 0 { return 0, nil } // 2. 获取 dayN 的活跃用户列表 dayNUsers, err := s.repo.GetDAUUserIDsFromRedis(ctx, dayN) if err != nil { return 0, err } // 3. 计算交集 day1Set := make(map[string]bool) for _, u := range day1Users { day1Set[u] = true } retained := 0 for _, u := range dayNUsers { if day1Set[u] { retained++ } } // 4. 计算留存率(返回百分比形式 0-100) retention := float64(retained) / float64(len(day1Users)) * 100 return retention, nil } // GetActivityStats 获取综合统计数据 func (s *userActivityService) GetActivityStats(ctx context.Context) (*ActivityStatsResponse, error) { now := time.Now() // 获取今日和昨日的日期(零点) today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) yesterday := today.AddDate(0, 0, -1) // 获取当前年周 year, week := now.ISOWeek() // 并发获取各项统计数据 resp := &ActivityStatsResponse{} // 获取今日 DAU todayDAU, err := s.GetDAU(ctx, today) if err != nil { zap.L().Warn("failed to get today DAU", zap.Error(err), ) } resp.Today = DailyStats{ Date: today.Format("2006-01-02"), DAU: todayDAU, } // 获取昨日 DAU yesterdayDAU, err := s.GetDAU(ctx, yesterday) if err != nil { zap.L().Warn("failed to get yesterday DAU", zap.Error(err), ) } resp.Yesterday = DailyStats{ Date: yesterday.Format("2006-01-02"), DAU: yesterdayDAU, } // 获取本周 WAU wau, err := s.GetWAU(ctx, year, week) if err != nil { zap.L().Warn("failed to get WAU", zap.Error(err), ) } resp.ThisWeek = PeriodStats{ Period: fmt.Sprintf("%d-W%02d", year, week), ActiveUsers: wau, } // 获取本月 MAU mau, err := s.GetMAU(ctx, now.Year(), int(now.Month())) if err != nil { zap.L().Warn("failed to get MAU", zap.Error(err), ) } resp.ThisMonth = PeriodStats{ Period: now.Format("2006-01"), ActiveUsers: mau, } // 获取留存率 // 次日留存 day1Retention, err := s.GetRetention(ctx, yesterday, today) if err != nil { zap.L().Warn("failed to get day 1 retention", zap.Error(err), ) } resp.Retention.Day1 = day1Retention // 7日留存 day7Date := today.AddDate(0, 0, -7) day7Retention, err := s.GetRetention(ctx, day7Date, today) if err != nil { zap.L().Warn("failed to get day 7 retention", zap.Error(err), ) } resp.Retention.Day7 = day7Retention // 30日留存 day30Date := today.AddDate(0, 0, -30) day30Retention, err := s.GetRetention(ctx, day30Date, today) if err != nil { zap.L().Warn("failed to get day 30 retention", zap.Error(err), ) } resp.Retention.Day30 = day30Retention return resp, nil }