Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
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
|
|
}
|