2026-07-12 17:38:48 +08:00

145 lines
4.7 KiB
Go

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
}