117 lines
4.2 KiB
Go
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
|
|
}
|