2026-05-08 18:48:29 +08:00

98 lines
3.1 KiB
Go

// Package mictime runs room mic outbox consumers and exposes mic duration queries.
package mictime
import (
"context"
"log"
"time"
"hyapp/pkg/xerr"
mictimedomain "hyapp/services/user-service/internal/domain/mictime"
)
// Repository applies room mic events and returns user duration aggregates.
type Repository interface {
ProcessRoomMicEvent(ctx context.Context, event mictimedomain.RoomMicEvent) (mictimedomain.ApplyResult, error)
GetLifetimeStats(ctx context.Context, appCode string, userID int64) (mictimedomain.LifetimeStats, error)
}
// RoomOutboxReader scans room-service RoomMicChanged facts in creation order.
type RoomOutboxReader interface {
ListRoomMicEventsAfter(ctx context.Context, cursor mictimedomain.RoomOutboxCursor, batchSize int) ([]mictimedomain.RoomMicEvent, error)
}
// Service owns the user mic duration read model.
type Service struct {
repository Repository
reader RoomOutboxReader
}
// WorkerOptions controls local room outbox polling.
type WorkerOptions struct {
WorkerID string
PollInterval time.Duration
BatchSize int
}
// New creates a mic time service with persistence and optional room outbox reader.
func New(repository Repository, reader RoomOutboxReader) *Service {
return &Service{repository: repository, reader: reader}
}
// GetLifetimeStats returns user-wide accumulated mic duration.
func (s *Service) GetLifetimeStats(ctx context.Context, appCode string, userID int64) (mictimedomain.LifetimeStats, error) {
if s == nil || s.repository == nil {
return mictimedomain.LifetimeStats{}, xerr.New(xerr.Unavailable, "mic time repository is not configured")
}
return s.repository.GetLifetimeStats(ctx, appCode, userID)
}
// RunRoomMicWorker consumes RoomMicChanged facts from room-service outbox.
func (s *Service) RunRoomMicWorker(ctx context.Context, options WorkerOptions) {
if s == nil || s.repository == nil || s.reader == nil {
log.Printf("component=user_mic_time op=room_mic_worker status=disabled reason=missing_dependency")
return
}
if options.PollInterval <= 0 {
options.PollInterval = 5 * time.Second
}
if options.BatchSize <= 0 {
options.BatchSize = 100
}
var cursor mictimedomain.RoomOutboxCursor
for {
processed, err := s.processBatch(ctx, &cursor, options.BatchSize)
if err != nil {
log.Printf("component=user_mic_time op=room_mic_worker worker_id=%s status=failed error=%q", options.WorkerID, err.Error())
}
if ctx.Err() != nil {
return
}
if processed >= options.BatchSize {
// Backlog is still present; continue without sleeping until the cursor catches up.
continue
}
select {
case <-ctx.Done():
return
case <-time.After(options.PollInterval):
}
}
}
func (s *Service) processBatch(ctx context.Context, cursor *mictimedomain.RoomOutboxCursor, batchSize int) (int, error) {
events, err := s.reader.ListRoomMicEventsAfter(ctx, *cursor, batchSize)
if err != nil {
return 0, err
}
for _, event := range events {
if _, err := s.repository.ProcessRoomMicEvent(ctx, event); err != nil {
return 0, err
}
cursor.CreatedAtMs = event.OutboxCreatedAtMs
cursor.EventID = event.EventID
}
return len(events), nil
}