diff --git a/internal/biz/business/analytics.go b/internal/biz/business/analytics.go index ca4c6ce..35bfa16 100644 --- a/internal/biz/business/analytics.go +++ b/internal/biz/business/analytics.go @@ -64,6 +64,13 @@ func (f AnalyticsFilter) MatchesMetrics(row *AnalyticsRow) (bool, error) { return true, nil } +// Inactive channels match a negative activity filter, unless another selected +// metric explicitly requires activity. +func (f AnalyticsFilter) IncludesInactiveRows() bool { + return (f.HasRegister != nil || f.HasPay != nil) && + (f.HasRegister == nil || !*f.HasRegister) && (f.HasPay == nil || !*f.HasPay) +} + func (r *AnalyticsRow) BusinessName() string { if value := strings.TrimSpace(r.BindingAccount); value != "" { return value diff --git a/internal/data/business/analytics.go b/internal/data/business/analytics.go index c27a5cd..8faefa6 100644 --- a/internal/data/business/analytics.go +++ b/internal/data/business/analytics.go @@ -228,6 +228,11 @@ func (r *analyticsRepo) Aggregate(ctx context.Context, kind business.AnalyticsKi } } if metricFilter { + if f.IncludesInactiveRows() { + if err := includeInactiveAnalyticsRows(db, scope, f, rowMap); err != nil { + return nil, err + } + } for key, row := range rowMap { match, err := f.MatchesMetrics(row) if err != nil { @@ -313,6 +318,28 @@ func (r *analyticsRepo) Aggregate(ctx context.Context, kind business.AnalyticsKi return result, nil } +func includeInactiveAnalyticsRows(db *gorm.DB, scope business.AnalyticsScope, f business.AnalyticsFilter, rows map[business.AnalyticsKey]*business.AnalyticsRow) error { + q := analyticsPromotes(db, scope, f, "p") + if len(f.GameNames) > 0 { + applications := db.Table("tab_promote_apply a").Select("1").Joins("JOIN tab_game g ON g.source_db=a.source_db AND g.id=a.game_id").Where("a.source_db=p.source_db AND a.promote_id=p.id AND a.status=1 AND g.game_name IN ?", f.GameNames) + q = q.Where("EXISTS (?)", applications) + } + id := "p.id" + if scope.IncludeSub { + id = "CASE WHEN p.promote_level=1 THEN p.id WHEN p.top_promote_id>0 THEN p.top_promote_id ELSE p.id END" + } + var keys []business.AnalyticsKey + if err := q.Select("DISTINCT p.source_db," + id + " AS id").Scan(&keys).Error; err != nil { + return business.ErrStorage + } + for _, key := range keys { + if rows[key] == nil { + rows[key] = &business.AnalyticsRow{AnalyticsKey: key, Pay: "0"} + } + } + return nil +} + func (r *analyticsRepo) FillNames(ctx context.Context, kind business.AnalyticsKind, scope business.AnalyticsScope, f business.AnalyticsFilter, rows []*business.AnalyticsRow) error { if len(rows) == 0 { return nil diff --git a/internal/data/business/analytics_test.go b/internal/data/business/analytics_test.go index a3703bb..ff929bc 100644 --- a/internal/data/business/analytics_test.go +++ b/internal/data/business/analytics_test.go @@ -87,6 +87,49 @@ func TestAnalyticsMetricFiltersPreserveGlobalDedupAndEffectiveCount(t *testing.T } } +func TestAnalyticsNegativeFiltersIncludeInactiveAuthorizedChannels(t *testing.T) { + db, repo, scope, filter := prepareAnalyticsData(t) + for _, sql := range []string{ + "INSERT INTO tab_promote(id,source_db,account,promote_level,parent_id,top_promote_id,busier_id) VALUES(200,'lx','inactive',1,0,0,14),(201,'lx','invisible',1,0,0,999),(202,'lx','inactive-child',2,200,200,14)", + "INSERT INTO tab_promote_apply(source_db,promote_id,game_id,status) VALUES('lx',202,1,1)", + } { + if err := db.Exec(sql).Error; err != nil { + t.Fatal(err) + } + } + no := false + filter.HasRegister, filter.HasPay = &no, &no + filter.Sources = []string{"lx"} + for _, games := range [][]string{nil, {"当前游戏甲"}} { + filter.GameNames = games + // Use the actual game name; the report's saved historical name can differ. + if len(games) > 0 { + if err := db.Table("tab_game").Where("source_db='lx' AND id=1").Pluck("game_name", &filter.GameNames).Error; err != nil { + t.Fatal(err) + } + } + data, err := repo.Aggregate(context.Background(), business.ChannelAnalytics, scope, filter) + if err != nil { + t.Fatal(err) + } + found := false + for _, row := range data.Rows { + if row.ID == 201 || row.ID == 202 { + t.Fatalf("permission or tree aggregation leak: %+v", row) + } + if row.ID == 200 { + found = true + if row.Counts.Register != 0 || row.Pay != "0" { + t.Fatalf("inactive metrics: %+v", row) + } + } + } + if !found { + t.Fatalf("inactive authorized channel missing with games %v", filter.GameNames) + } + } +} + func TestAnalyticsRowsOnlyComeFromSelectedPeriod(t *testing.T) { db, repo, scope, filter := prepareAnalyticsData(t) for _, sql := range []string{