Files
backend/internal/service/user_activity_service.go

243 lines
6.1 KiB
Go
Raw Normal View History

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
}