From dd02620b5e68669f1d5ec775ec38f88c352443c0 Mon Sep 17 00:00:00 2001 From: Yvan <8574526@qq,com> Date: Thu, 5 Nov 2026 14:45:58 +0800 Subject: [PATCH] fix: limit channel report rows to the selected period --- internal/data/business/analytics.go | 19 +------- internal/data/business/analytics_test.go | 59 ++++++++++++++++++------ 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/internal/data/business/analytics.go b/internal/data/business/analytics.go index 816c127..317b799 100644 --- a/internal/data/business/analytics.go +++ b/internal/data/business/analytics.go @@ -216,23 +216,8 @@ func (r *analyticsRepo) Aggregate(ctx context.Context, kind business.AnalyticsKi putAnalyticsCount(&rowFor(po).Counts, po.Metric, po.Count) } } - // Keep visible channels with no events in the selected period so that - // "no registration / no recharge" can find genuinely inactive channels. - if kind == business.ChannelAnalytics && len(f.GameNames) == 0 { - root := "p.id" - if scope.IncludeSub { - root = "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 channels []analyticsAggPO - if err := analyticsPromotes(db, scope, f, "p").Select("DISTINCT p.source_db," + root + " AS entity_id").Scan(&channels).Error; err != nil { - return nil, business.ErrStorage - } - for _, po := range channels { - if po.ID != nil { - rowFor(po) - } - } - } + // Report rows come from facts in the selected period. Registration/payment + // filters narrow that same set; they must not add the entire channel catalog. if kind == business.ChannelAnalytics && !scope.Viewer.All { if err := filterAnalyticsRows(db, scope, f, rowMap); err != nil { return nil, err diff --git a/internal/data/business/analytics_test.go b/internal/data/business/analytics_test.go index e695e05..9bc9d43 100644 --- a/internal/data/business/analytics_test.go +++ b/internal/data/business/analytics_test.go @@ -61,31 +61,57 @@ func TestAnalyticsChannelTreeAndAdminOfficialRows(t *testing.T) { } } -func TestAnalyticsFiltersIncludeIdleChannelsAndRecalculateTotals(t *testing.T) { +func TestAnalyticsFiltersOnlyNarrowPeriodRowsAndRecalculateTotals(t *testing.T) { db, repo, scope, filter := prepareAnalyticsData(t) - if err := db.Exec("INSERT INTO tab_promote(id,source_db,account,promote_level,parent_id,top_promote_id,busier_id) VALUES(200,'lx','idle',1,0,0,14)").Error; err != nil { - t.Fatal(err) + for _, sql := range []string{ + "INSERT INTO tab_promote(id,source_db,account,promote_level,parent_id,top_promote_id,busier_id) VALUES(200,'lx','outside-period',1,0,0,14),(201,'lx','active-only',1,0,0,14),(202,'lx','never-active',1,0,0,14)", + "INSERT INTO tab_sw_report_day(source_db,promote_id,game_id,dedup_mode,stat_date,total_pay,game_name) VALUES('lx',200,1,0,'2026-10-31',500,'历史游戏甲')", + "INSERT INTO tab_sw_report_day_key(source_db,promote_id,game_id,dedup_mode,stat_date,metric,identity_key,game_name) VALUES('lx',201,1,1,'2026-11-02',2,'active-only','历史游戏甲')", + } { + if err := db.Exec(sql).Error; err != nil { + t.Fatal(err) + } } yes, no := true, false - filter.HasRegister, filter.HasPay = &no, &no for _, viewer := range []business.UserScope{scope.Viewer, {All: true}} { scope.Viewer = viewer - data, err := repo.Aggregate(context.Background(), business.ChannelAnalytics, scope, filter) + filter.HasRegister, filter.HasPay = nil, nil + baseline, err := repo.Aggregate(context.Background(), business.ChannelAnalytics, scope, filter) if err != nil { t.Fatal(err) } - found := false - for _, row := range data.Rows { - if row.Counts.Register != 0 || portraitTestMoney(row.Pay) != "0.00" { - t.Fatalf("筛选后的每行必须同时满足两个条件: %+v", row) + keys := map[business.AnalyticsKey]bool{} + for _, row := range baseline.Rows { + if row.ID == 200 || row.ID == 202 { + t.Fatalf("默认报表不能补入区间外或完全无数据渠道: %+v", row) } - found = found || row.SourceDB == "lx" && row.ID == 200 + keys[row.AnalyticsKey] = true } - if !found { - t.Fatal("整个期间没有事件的可见渠道不能遗漏") + if !keys[business.AnalyticsKey{SourceDB: "lx", ID: 201}] { + t.Fatal("仅有活跃、没有注册或充值的统计行不能遗漏") } - if viewer.All && (data.GlobalCounts.Register != 0 || portraitTestMoney(data.GlobalPay) != "0.00") { - t.Fatalf("管理员合计未跟随过滤: %+v", data) + for _, flags := range [][2]*bool{{&no, &no}, {&no, nil}, {nil, &no}, {&yes, nil}, {nil, &yes}, {&yes, &yes}, {&no, &yes}, {&yes, &no}} { + filter.HasRegister, filter.HasPay = flags[0], flags[1] + data, err := repo.Aggregate(context.Background(), business.ChannelAnalytics, scope, filter) + if err != nil { + t.Fatal(err) + } + for _, row := range data.Rows { + if !keys[row.AnalyticsKey] { + t.Fatalf("筛选不能扩大默认统计范围: %+v", row) + } + if flags[0] != nil && (row.Counts.Register > 0) != *flags[0] || flags[1] != nil && (portraitTestMoney(row.Pay) != "0.00") != *flags[1] { + t.Fatalf("筛选条件没有同时生效: %+v", row) + } + } + if flags[0] == &no && flags[1] == &no { + if len(data.Rows) != 1 || data.Rows[0].ID != 201 { + t.Fatalf("无注册且无充值仍应保留仅活跃渠道: %+v", data.Rows) + } + if viewer.All && (data.GlobalCounts.Register != 0 || data.GlobalCounts.Active != 1 || portraitTestMoney(data.GlobalPay) != "0.00") { + t.Fatalf("管理员合计未跟随过滤: %+v", data) + } + } } } filter.HasRegister, filter.HasPay = &yes, &yes @@ -94,6 +120,11 @@ func TestAnalyticsFiltersIncludeIdleChannelsAndRecalculateTotals(t *testing.T) { if err != nil || len(data.Rows) != 1 || portraitTestMoney(data.GlobalPay) != "60.60" || data.GlobalCounts.Register != data.Rows[0].Counts.Register { t.Fatalf("平台、名称及指标筛选须共同约束列表与全量合计: %+v %v", data, err) } + filter = business.AnalyticsFilter{Sources: []string{"lx", "hw"}, Begin: "2027-01-01", End: "2027-01-02", HasRegister: &no, HasPay: &no} + data, err = repo.Aggregate(context.Background(), business.ChannelAnalytics, scope, filter) + if err != nil || len(data.Rows) != 0 || data.GlobalCounts.Register != 0 || portraitTestMoney(data.GlobalPay) != "0.00" { + t.Fatalf("无报表记录的区间应返回空列表与零合计: %+v %v", data, err) + } } func TestAnalyticsEffectiveCountIncludesQualifiedDescendants(t *testing.T) {