145 lines
4.7 KiB
Go
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
|
|
}
|