- Remove BOM from all Go source files - Standardize import ordering and whitespace across handlers and services - Replace PostgreSQL full-text search with ILIKE pattern matching in post and user repositories - Enhance JPush client with TLS transport, rate limit monitoring, and simplified API - Refactor PushService to include userID in device management methods - Add MaxDevicesPerUser limit and extract helper functions for push payload construction
243 lines
6.1 KiB
Go
243 lines
6.1 KiB
Go
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
|
||
}
|