167 lines
5.4 KiB
Go
167 lines
5.4 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"
|
|
)
|
|
|
|
// ReportingRollupService computes daily/weekly/monthly rollup aggregations
|
|
// from raw ReportingEvents. Reference: Chatwoot app/services/reporting_events/rollup_service.rb (81行)
|
|
//
|
|
// Rollup computation:
|
|
// 1. Group raw events by (account_id, date, dimension_type, dimension_id, metric)
|
|
// 2. Compute COUNT, SUM(value), SUM(value_in_business_hours) per group
|
|
// 3. Upsert into reporting_events_rollups table
|
|
type ReportingRollupService struct {
|
|
eventRepo *repository.ReportingEventRepo
|
|
rollupRepo *repository.ReportingEventsRollupRepo
|
|
}
|
|
|
|
// NewReportingRollupService creates a new rollup service.
|
|
func NewReportingRollupService(eventRepo *repository.ReportingEventRepo, rollupRepo *repository.ReportingEventsRollupRepo) *ReportingRollupService {
|
|
return &ReportingRollupService{eventRepo: eventRepo, rollupRepo: rollupRepo}
|
|
}
|
|
|
|
// ComputeDailyRollup computes rollup for a specific account and date.
|
|
func (s *ReportingRollupService) ComputeDailyRollup(ctx context.Context, accountID uint, date time.Time) error {
|
|
startUTC := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC)
|
|
endUTC := startUTC.Add(24 * time.Hour)
|
|
|
|
// Get raw events for this time range
|
|
events, err := s.eventRepo.FindByAccountIDAndTimeRange(ctx, accountID, startUTC, endUTC)
|
|
if err != nil {
|
|
applogger.L().Errorf("ComputeDailyRollup find events: %v", err)
|
|
return err
|
|
}
|
|
|
|
if len(events) == 0 {
|
|
return nil // No events to rollup
|
|
}
|
|
|
|
// Build rollup rows using the backfill logic
|
|
rollupRows := s.buildRollupFromEvents(accountID, date, events)
|
|
|
|
// Delete existing rollups for this date and replace
|
|
if err := s.rollupRepo.DeleteByAccountAndDate(ctx, accountID, date); err != nil {
|
|
applogger.L().Errorf("ComputeDailyRollup delete old: %v", err)
|
|
return err
|
|
}
|
|
|
|
if len(rollupRows) > 0 {
|
|
if err := s.rollupRepo.BulkCreate(ctx, rollupRows); err != nil {
|
|
applogger.L().Errorf("ComputeDailyRollup bulk insert: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ComputeRollupForRange computes daily rollups for a date range.
|
|
func (s *ReportingRollupService) ComputeRollupForRange(ctx context.Context, accountID uint, since, until time.Time) error {
|
|
for d := since; !d.After(until); d = d.AddDate(0, 0, 1) {
|
|
if err := s.ComputeDailyRollup(ctx, accountID, d); err != nil {
|
|
applogger.L().Errorf("ComputeRollupForRange date=%s: %v", d.Format("2006-01-02"), err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetSummaryMetrics retrieves aggregated metrics for a given time range and dimensions.
|
|
// Returns metrics matching Chatwoot's summary report API response format.
|
|
func (s *ReportingRollupService) GetSummaryMetrics(ctx context.Context, accountID uint, since, until time.Time, dimensionType model.DimensionType, dimensionID uint) (map[string]float64, error) {
|
|
rollups, err := s.rollupRepo.FindByAccountAndDateRangeWithDimension(ctx, accountID, since, until, dimensionType, dimensionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
metrics := make(map[string]float64)
|
|
for _, rollup := range rollups {
|
|
metricKey := string(rollup.Metric)
|
|
|
|
def := GetReportMetricDefinition(metricKey)
|
|
if def == nil {
|
|
// Unknown metric, still record
|
|
metrics[metricKey] = float64(rollup.Count)
|
|
continue
|
|
}
|
|
|
|
switch def.AggregateType {
|
|
case "count":
|
|
metrics[metricKey] = float64(rollup.Count)
|
|
case "average":
|
|
if rollup.Count > 0 {
|
|
metrics[metricKey] = rollup.SumValue / float64(rollup.Count)
|
|
}
|
|
case "average_biz":
|
|
if rollup.Count > 0 {
|
|
metrics[metricKey] = rollup.SumValueBusinessHours / float64(rollup.Count)
|
|
}
|
|
case "sum":
|
|
metrics[metricKey] = rollup.SumValue
|
|
}
|
|
}
|
|
|
|
return metrics, nil
|
|
}
|
|
|
|
func (s *ReportingRollupService) buildRollupFromEvents(accountID uint, date time.Time, events []model.ReportingEvent) []model.ReportingEventsRollup {
|
|
// Group events by (dimension_type, dimension_id, raw_metric_name)
|
|
type groupKey struct {
|
|
dimType model.DimensionType
|
|
dimID uint
|
|
metric model.RollupMetric
|
|
}
|
|
|
|
groups := make(map[groupKey]*RollupAggregate)
|
|
dims := BackfillDimensions
|
|
|
|
for _, event := range events {
|
|
for _, dim := range dims {
|
|
dimID := accountID
|
|
if dim.GroupColumn == "user_id" && event.UserID != nil {
|
|
dimID = *event.UserID
|
|
} else if dim.GroupColumn == "inbox_id" && event.InboxID != nil {
|
|
dimID = *event.InboxID
|
|
}
|
|
|
|
key := groupKey{dimType: dim.Type, dimID: dimID, metric: model.RollupMetric(event.Name)}
|
|
agg, ok := groups[key]
|
|
if !ok {
|
|
agg = &RollupAggregate{
|
|
DimensionType: dim.Type,
|
|
DimensionID: dimID,
|
|
Metric: model.RollupMetric(event.Name),
|
|
}
|
|
groups[key] = agg
|
|
}
|
|
agg.Count++
|
|
agg.SumValue += event.Value
|
|
agg.SumBizHours += event.ValueInBusinessHours
|
|
}
|
|
}
|
|
|
|
// Expand each group into rollup metrics
|
|
var rollupRows []model.ReportingEventsRollup
|
|
for _, agg := range groups {
|
|
rollupMetrics := ExpandEventToRollupMetrics(agg.Metric, agg.Count, agg.SumValue, agg.SumBizHours)
|
|
for rm, data := range rollupMetrics {
|
|
rollupRows = append(rollupRows, model.ReportingEventsRollup{
|
|
AccountID: accountID,
|
|
Date: date,
|
|
DimensionType: agg.DimensionType,
|
|
DimensionID: agg.DimensionID,
|
|
Metric: rm,
|
|
Count: data.Count,
|
|
SumValue: data.SumValue,
|
|
SumValueBusinessHours: data.SumBizHours,
|
|
})
|
|
}
|
|
}
|
|
|
|
return rollupRows
|
|
} |