package host import ( "context" "time" "hyapp/pkg/appcode" "hyapp/pkg/xerr" hostdomain "hyapp/services/user-service/internal/domain/host" ) const validMicDayThresholdMS = int64(time.Hour / time.Millisecond) // GetHostEngagementStats 聚合 user-service 自己拥有的事实表,不从当前 Agency 关系反推历史归属。 func (r *Repository) GetHostEngagementStats(ctx context.Context, query hostdomain.EngagementStatsQuery) (hostdomain.EngagementStats, error) { if r == nil || r.db == nil { return hostdomain.EngagementStats{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") } stats := hostdomain.EngagementStats{} appCode := appcode.FromContext(ctx) startDate := time.UnixMilli(query.StartAtMS).UTC().Format("2006-01-02") endDate := time.UnixMilli(query.EndAtMS - 1).UTC().Format("2006-01-02") if err := r.db.QueryRowContext(ctx, ` SELECT COALESCE(SUM(mic_online_ms), 0), COALESCE(SUM(CASE WHEN mic_online_ms >= ? THEN 1 ELSE 0 END), 0) FROM user_mic_daily_stats WHERE app_code = ? AND user_id = ? AND stat_date BETWEEN ? AND ?`, validMicDayThresholdMS, appCode, query.HostUserID, startDate, endDate, ).Scan(&stats.ValidMicDurationMS, &stats.ValidMicDays); err != nil { return hostdomain.EngagementStats{}, err } if err := r.db.QueryRowContext(ctx, ` SELECT COUNT(DISTINCT sender_user_id) FROM user_private_message_events WHERE app_code = ? AND target_user_id = ? AND occurred_at_ms >= ? AND occurred_at_ms < ?`, appCode, query.HostUserID, query.StartAtMS, query.EndAtMS, ).Scan(&stats.PrivateMessageSenders); err != nil { return hostdomain.EngagementStats{}, err } if err := r.db.QueryRowContext(ctx, ` SELECT COALESCE(SUM(delta), 0) FROM user_follow_events WHERE app_code = ? AND followee_user_id = ? AND occurred_at_ms >= ? AND occurred_at_ms < ?`, appCode, query.HostUserID, query.StartAtMS, query.EndAtMS, ).Scan(&stats.NewFollowers); err != nil { return hostdomain.EngagementStats{}, err } onlineMS, err := r.hostOnlineDuration(ctx, query) if err != nil { return hostdomain.EngagementStats{}, err } stats.OnlineDurationMS = onlineMS return stats, nil } // hostOnlineDuration 按每天“最早会话开始到最后一次心跳”累计;多设备/多 session 不重复相加。 func (r *Repository) hostOnlineDuration(ctx context.Context, query hostdomain.EngagementStatsQuery) (int64, error) { rows, err := r.db.QueryContext(ctx, ` SELECT created_at_ms, last_heartbeat_at_ms FROM auth_sessions WHERE app_code = ? AND user_id = ? AND last_heartbeat_at_ms >= ? AND created_at_ms < ?`, appcode.FromContext(ctx), query.HostUserID, query.StartAtMS, query.EndAtMS, ) if err != nil { return 0, err } defer rows.Close() type daySpan struct{ first, last int64 } spans := map[string]daySpan{} for rows.Next() { var startedAtMS, heartbeatAtMS int64 if err := rows.Scan(&startedAtMS, &heartbeatAtMS); err != nil { return 0, err } start := maxInt64(startedAtMS, query.StartAtMS) end := minInt64(heartbeatAtMS, query.EndAtMS-1) if end < start { continue } for day := utcDayStart(start); day <= utcDayStart(end); day += int64(24 * time.Hour / time.Millisecond) { segmentStart := maxInt64(start, day) segmentEnd := minInt64(end, day+int64(24*time.Hour/time.Millisecond)-1) key := time.UnixMilli(day).UTC().Format("2006-01-02") span, exists := spans[key] if !exists || segmentStart < span.first { span.first = segmentStart } if !exists || segmentEnd > span.last { span.last = segmentEnd } spans[key] = span } } if err := rows.Err(); err != nil { return 0, err } var total int64 for _, span := range spans { if span.last >= span.first { total += span.last - span.first } } return total, nil } // RecordPrivateMessageEvent 用 app_code + event_id 幂等,不保存消息正文或扩展负载。 func (r *Repository) RecordPrivateMessageEvent(ctx context.Context, event hostdomain.PrivateMessageEvent) (bool, error) { result, err := r.db.ExecContext(ctx, ` INSERT IGNORE INTO user_private_message_events ( app_code, event_id, sender_user_id, target_user_id, occurred_at_ms, created_at_ms ) VALUES (?, ?, ?, ?, ?, ?)`, appcode.FromContext(ctx), event.EventID, event.SenderUserID, event.TargetUserID, event.OccurredAtMS, time.Now().UTC().UnixMilli(), ) if err != nil { return false, err } rows, err := result.RowsAffected() return rows > 0, err } func utcDayStart(value int64) int64 { t := time.UnixMilli(value).UTC() return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC).UnixMilli() } func minInt64(left, right int64) int64 { if left < right { return left } return right } func maxInt64(left, right int64) int64 { if left > right { return left } return right }