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/*.
241 lines
8.3 KiB
Go
241 lines
8.3 KiB
Go
package repository
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"with_you/internal/model"
|
||
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
// Redis Key 前缀常量(遵循项目命名规范)
|
||
const (
|
||
// DAUKeyPrefix DAU Key: stats:dau:YYYYMMDD
|
||
DAUKeyPrefix = "stats:dau:"
|
||
// WAUKeyPrefix WAU Key: stats:wau:YYYYWW (年-周)
|
||
WAUKeyPrefix = "stats:wau:"
|
||
// MAUKeyPrefix MAU Key: stats:mau:YYYYMM
|
||
MAUKeyPrefix = "stats:mau:"
|
||
)
|
||
|
||
// TTL 常量
|
||
const (
|
||
DAUTTL = 45 * 24 * time.Hour // DAU: 45天
|
||
WAUTTL = 120 * 24 * time.Hour // WAU: 120天
|
||
MAUTTL = 400 * 24 * time.Hour // MAU: 400天
|
||
)
|
||
|
||
// activityCache 用户活跃缓存接口(避免循环导入)
|
||
type activityCache interface {
|
||
ZAdd(ctx context.Context, key string, score float64, member string) error
|
||
ZCard(ctx context.Context, key string) (int64, error)
|
||
ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error)
|
||
Expire(ctx context.Context, key string, ttl time.Duration) error
|
||
}
|
||
|
||
// UserActivityRepository 用户活跃数据仓储接口
|
||
type UserActivityRepository interface {
|
||
RecordActivity(ctx context.Context, userID, loginType, ip, userAgent string) error
|
||
GetDAU(ctx context.Context, date time.Time) (int64, error)
|
||
GetActiveUserIDs(ctx context.Context, date time.Time) ([]string, error)
|
||
GetUserActivityCount(ctx context.Context, userID string, startDate, endDate time.Time) (int64, error)
|
||
SaveStat(ctx context.Context, stat *model.UserActivityStat) error
|
||
GetStat(ctx context.Context, statType string, date time.Time) (*model.UserActivityStat, error)
|
||
RecordActivityToRedis(ctx context.Context, userID string) error
|
||
GetDAUFromRedis(ctx context.Context, date time.Time) (int64, error)
|
||
GetWAUFromRedis(ctx context.Context, year int, week int) (int64, error)
|
||
GetMAUFromRedis(ctx context.Context, year int, month int) (int64, error)
|
||
GetDAUUserIDsFromRedis(ctx context.Context, date time.Time) ([]string, error)
|
||
}
|
||
|
||
// userActivityRepository 用户活跃数据仓储实现
|
||
type userActivityRepository struct {
|
||
db *gorm.DB
|
||
cache activityCache
|
||
}
|
||
|
||
// NewUserActivityRepository 创建用户活跃数据仓储
|
||
func NewUserActivityRepository(db *gorm.DB, cache activityCache) UserActivityRepository {
|
||
return &userActivityRepository{db: db, cache: cache}
|
||
}
|
||
|
||
// ==================== 数据库操作方法 ====================
|
||
|
||
// RecordActivity 记录用户活跃(写入数据库)
|
||
func (r *userActivityRepository) RecordActivity(ctx context.Context, userID, loginType, ip, userAgent string) error {
|
||
log := &model.UserActiveLog{
|
||
UserID: userID,
|
||
ActiveDate: time.Now(),
|
||
LoginType: loginType,
|
||
LoginIP: ip,
|
||
UserAgent: userAgent,
|
||
}
|
||
return r.db.WithContext(ctx).Create(log).Error
|
||
}
|
||
|
||
// GetDAU 获取指定日期的日活用户数(从数据库)
|
||
func (r *userActivityRepository) GetDAU(ctx context.Context, date time.Time) (int64, error) {
|
||
var count int64
|
||
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||
endOfDay := startOfDay.Add(24 * time.Hour)
|
||
|
||
err := r.db.WithContext(ctx).
|
||
Model(&model.UserActiveLog{}).
|
||
Where("active_date >= ? AND active_date < ?", startOfDay, endOfDay).
|
||
Distinct("user_id").
|
||
Count(&count).Error
|
||
|
||
return count, err
|
||
}
|
||
|
||
// GetActiveUserIDs 获取指定日期的活跃用户ID列表
|
||
func (r *userActivityRepository) GetActiveUserIDs(ctx context.Context, date time.Time) ([]string, error) {
|
||
var userIDs []string
|
||
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||
endOfDay := startOfDay.Add(24 * time.Hour)
|
||
|
||
err := r.db.WithContext(ctx).
|
||
Model(&model.UserActiveLog{}).
|
||
Select("DISTINCT user_id").
|
||
Where("active_date >= ? AND active_date < ?", startOfDay, endOfDay).
|
||
Pluck("user_id", &userIDs).Error
|
||
|
||
return userIDs, err
|
||
}
|
||
|
||
// GetUserActivityCount 获取用户在指定日期范围内的活跃天数
|
||
func (r *userActivityRepository) GetUserActivityCount(ctx context.Context, userID string, startDate, endDate time.Time) (int64, error) {
|
||
var count int64
|
||
startOfDay := time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, startDate.Location())
|
||
endOfDay := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 0, endDate.Location())
|
||
|
||
err := r.db.WithContext(ctx).
|
||
Model(&model.UserActiveLog{}).
|
||
Where("user_id = ? AND active_date >= ? AND active_date <= ?", userID, startOfDay, endOfDay).
|
||
Distinct("DATE(active_date)").
|
||
Count(&count).Error
|
||
|
||
return count, err
|
||
}
|
||
|
||
// SaveStat 保存统计数据快照
|
||
func (r *userActivityRepository) SaveStat(ctx context.Context, stat *model.UserActivityStat) error {
|
||
return r.db.WithContext(ctx).
|
||
Clauses(clause.OnConflict{
|
||
Columns: []clause.Column{{Name: "stat_type"}, {Name: "stat_date"}},
|
||
DoUpdates: clause.AssignmentColumns([]string{"active_count", "new_count", "retention_1", "retention_7", "retention_30", "updated_at"}),
|
||
}).
|
||
Create(stat).Error
|
||
}
|
||
|
||
// GetStat 获取统计数据快照
|
||
func (r *userActivityRepository) GetStat(ctx context.Context, statType string, date time.Time) (*model.UserActivityStat, error) {
|
||
var stat model.UserActivityStat
|
||
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||
|
||
err := r.db.WithContext(ctx).
|
||
Where("stat_type = ? AND stat_date = ?", statType, startOfDay).
|
||
First(&stat).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &stat, nil
|
||
}
|
||
|
||
// ==================== Redis 操作方法 ====================
|
||
|
||
// RecordActivityToRedis 记录用户活跃到 Redis Sorted Set
|
||
// score 为时间戳,member 为用户ID
|
||
func (r *userActivityRepository) RecordActivityToRedis(ctx context.Context, userID string) error {
|
||
now := time.Now()
|
||
score := float64(now.Unix())
|
||
|
||
// 记录到 DAU
|
||
dauKey := formatDAUKey(now)
|
||
if err := r.cache.ZAdd(ctx, dauKey, score, userID); err != nil {
|
||
return fmt.Errorf("failed to record DAU: %w", err)
|
||
}
|
||
r.cache.Expire(ctx, dauKey, DAUTTL)
|
||
|
||
// 记录到 WAU
|
||
year, week := now.ISOWeek()
|
||
wauKey := formatWAUKey(year, week)
|
||
if err := r.cache.ZAdd(ctx, wauKey, score, userID); err != nil {
|
||
return fmt.Errorf("failed to record WAU: %w", err)
|
||
}
|
||
r.cache.Expire(ctx, wauKey, WAUTTL)
|
||
|
||
// 记录到 MAU
|
||
mauKey := formatMAUKey(now.Year(), int(now.Month()))
|
||
if err := r.cache.ZAdd(ctx, mauKey, score, userID); err != nil {
|
||
return fmt.Errorf("failed to record MAU: %w", err)
|
||
}
|
||
r.cache.Expire(ctx, mauKey, MAUTTL)
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetDAUFromRedis 从 Redis 获取日活用户数
|
||
func (r *userActivityRepository) GetDAUFromRedis(ctx context.Context, date time.Time) (int64, error) {
|
||
key := formatDAUKey(date)
|
||
count, err := r.cache.ZCard(ctx, key)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("failed to get DAU from redis: %w", err)
|
||
}
|
||
return count, nil
|
||
}
|
||
|
||
// GetWAUFromRedis 从 Redis 获取周活用户数
|
||
func (r *userActivityRepository) GetWAUFromRedis(ctx context.Context, year int, week int) (int64, error) {
|
||
key := formatWAUKey(year, week)
|
||
count, err := r.cache.ZCard(ctx, key)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("failed to get WAU from redis: %w", err)
|
||
}
|
||
return count, nil
|
||
}
|
||
|
||
// GetMAUFromRedis 从 Redis 获取月活用户数
|
||
func (r *userActivityRepository) GetMAUFromRedis(ctx context.Context, year int, month int) (int64, error) {
|
||
key := formatMAUKey(year, month)
|
||
count, err := r.cache.ZCard(ctx, key)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("failed to get MAU from redis: %w", err)
|
||
}
|
||
return count, nil
|
||
}
|
||
|
||
// GetDAUUserIDsFromRedis 获取日活用户ID列表
|
||
func (r *userActivityRepository) GetDAUUserIDsFromRedis(ctx context.Context, date time.Time) ([]string, error) {
|
||
key := formatDAUKey(date)
|
||
// 获取所有成员(从最小分数到最大分数)
|
||
members, err := r.cache.ZRangeByScore(ctx, key, "-inf", "+inf", 0, -1)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to get DAU user IDs from redis: %w", err)
|
||
}
|
||
return members, nil
|
||
}
|
||
|
||
// ==================== 辅助方法 ====================
|
||
|
||
// formatDAUKey 格式化 DAU Key
|
||
// 格式: stats:dau:YYYYMMDD
|
||
func formatDAUKey(date time.Time) string {
|
||
return fmt.Sprintf("%s%s", DAUKeyPrefix, date.Format("20060102"))
|
||
}
|
||
|
||
// formatWAUKey 格式化 WAU Key
|
||
// 格式: stats:wau:YYYYWW (年-周,周数补零到2位)
|
||
func formatWAUKey(year int, week int) string {
|
||
return fmt.Sprintf("%s%04d%02d", WAUKeyPrefix, year, week)
|
||
}
|
||
|
||
// formatMAUKey 格式化 MAU Key
|
||
// 格式: stats:mau:YYYYMM
|
||
func formatMAUKey(year int, month int) string {
|
||
return fmt.Sprintf("%s%04d%02d", MAUKeyPrefix, year, month)
|
||
}
|