Files
backend/internal/service/user_activity_service.go
lafay 2f584c1f2c
All checks were successful
Build Backend / build (push) Successful in 5m10s
Build Backend / build-docker (push) Successful in 2m22s
This is a breaking change that renames the entire project from "Carrot BBS" to "WithYou".
The Go module name has been changed from `carrot_bbs` to `with_you`, which requires
all import paths to be updated throughout the codebase and by external consumers.

BREAKING CHANGE: Module name changed from carrot_bbs to with_you. All imports
and dependencies must be updated from carrot_bbs/* to with_you/*.
2026-04-22 16:01:59 +08:00

243 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}