Files
gochat/internal/service/reporting_backfill_service.go
T

117 lines
4.2 KiB
Go

package service
import (
"context"
"time"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
applogger "github.com/gochat/gochat/pkg/logger"
"gorm.io/gorm"
)
// ReportingBackfillService backfills missing rollup data for a given account and date.
// Reference: Chatwoot app/services/reporting_events/backfill_service.rb (142行)
//
// It aggregates raw ReportingEvent rows into ReportingEventsRollup rows per dimension
// (account, agent, inbox) per metric, exactly matching Chatwoot's SQL semantics:
// - COUNT/SUM/DURATION aggregation per (dimension_type, dimension_id, metric)
// - DISTINCT_COUNT for bot_handoff events
// - Business-hours value tracking
type ReportingBackfillService struct {
eventRepo *repository.ReportingEventRepo
rollupRepo *repository.ReportingEventsRollupRepo
}
// NewReportingBackfillService creates a new backfill service.
func NewReportingBackfillService(eventRepo *repository.ReportingEventRepo, rollupRepo *repository.ReportingEventsRollupRepo) *ReportingBackfillService {
return &ReportingBackfillService{eventRepo: eventRepo, rollupRepo: rollupRepo}
}
// DimensionSpec defines a rollup dimension with its group column.
type DimensionSpec struct {
Type model.DimensionType
GroupColumn string // empty for account dimension
}
// BackfillDimensions mirrors Chatwoot DIMENSIONS constant.
var BackfillDimensions = []DimensionSpec{
{Type: model.DimensionAccount, GroupColumn: ""},
{Type: model.DimensionAgent, GroupColumn: "user_id"},
{Type: model.DimensionInbox, GroupColumn: "inbox_id"},
}
// DistinctCountEvents mirrors Chatwoot DISTINCT_COUNT_EVENTS.
// These events require COUNT(DISTINCT conversation_id) instead of simple SUM(count).
var DistinctCountEvents = []string{
"conversation_bot_handoff",
}
// BackfillDate performs backfill for a single account on a single date.
// 1. Build replacement rollups for the account reporting timezone date
// 2. Delete existing rollups for that date and insert replacements transactionally
func (s *ReportingBackfillService) BackfillDate(ctx context.Context, accountID uint, date time.Time) error {
db := reportingServiceDB(s.eventRepo, s.rollupRepo)
loc, enabled, err := accountReportingLocation(ctx, db, accountID)
if err != nil {
return err
}
if !enabled {
return nil
}
startUTC, endUTC, rollupDate := utcBoundariesForReportingDate(date, loc)
rollupRows, err := s.buildRollupRows(ctx, accountID, rollupDate, startUTC, endUTC)
if err != nil {
applogger.L().Errorf("BackfillDate build rollup rows: %v", err)
return err
}
if db == nil {
return nil
}
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Unscoped().Where("account_id = ? AND date = ?", accountID, rollupDate).Delete(&model.ReportingEventsRollup{}).Error; err != nil {
applogger.L().Errorf("BackfillDate delete existing rollups: %v", err)
return err
}
if len(rollupRows) == 0 {
return nil
}
if err := tx.CreateInBatches(rollupRows, 100).Error; err != nil {
applogger.L().Errorf("BackfillDate bulk insert: %v", err)
return err
}
return nil
})
}
// BackfillRange performs backfill for a date range (inclusive).
func (s *ReportingBackfillService) BackfillRange(ctx context.Context, accountID uint, startDate, endDate time.Time) error {
for d := startDate; !d.After(endDate); d = d.AddDate(0, 0, 1) {
if err := s.BackfillDate(ctx, accountID, d); err != nil {
applogger.L().Errorf("BackfillRange date=%s: %v", d.Format("2006-01-02"), err)
// Continue with next date rather than failing the entire range
}
}
return nil
}
// RollupAggregate represents a grouped aggregate from raw events.
type RollupAggregate struct {
DimensionType model.DimensionType
DimensionID uint
Metric model.RollupMetric
Count int64
SumValue float64
SumBizHours float64
}
func (s *ReportingBackfillService) buildRollupRows(ctx context.Context, accountID uint, date time.Time, startUTC, endUTC time.Time) ([]model.ReportingEventsRollup, error) {
events, err := s.eventRepo.FindByAccountIDAndTimeRange(ctx, accountID, startUTC, endUTC)
if err != nil {
return nil, err
}
return rollupRowsFromAggregates(accountID, date, aggregateReportingEvents(accountID, events)), nil
}