package business import ( "context" "testing" "time" ) func TestGameIDSortDoesNotFallBackToRechargeAmount(t *testing.T) { rows := []*AnalyticsRow{{AnalyticsKey: AnalyticsKey{ID: 20, SourceDB: "lx"}, Pay: "1.00"}, {AnalyticsKey: AnalyticsKey{ID: 3, SourceDB: "lx"}, Pay: "900.00"}} if err := sortAnalytics(rows, GameAnalytics, "gameId", true); err != nil || rows[0].ID != 3 { t.Fatal("game ID ascending sort failed") } if err := sortAnalytics(rows, GameAnalytics, "gameId", false); err != nil || rows[0].ID != 20 { t.Fatal("game ID descending sort fell back to recharge amount") } } type analyticsRepoFake struct { data *AnalyticsData scope AnalyticsScope filter AnalyticsFilter fillCalls [][]AnalyticsKey fill func([]*AnalyticsRow) error } func (r *analyticsRepoFake) FillNames(_ context.Context, _ AnalyticsKind, _ AnalyticsScope, _ AnalyticsFilter, rows []*AnalyticsRow) error { keys := make([]AnalyticsKey, len(rows)) for i, row := range rows { keys[i] = row.AnalyticsKey } r.fillCalls = append(r.fillCalls, keys) if r.fill != nil { return r.fill(rows) } return nil } func (r *analyticsRepoFake) Aggregate(_ context.Context, _ AnalyticsKind, s AnalyticsScope, f AnalyticsFilter) (*AnalyticsData, error) { r.scope = s r.filter = f copy := *r.data copy.Rows = []*AnalyticsRow{} for _, row := range r.data.Rows { c := *row copy.Rows = append(copy.Rows, &c) } return ©, nil } func analyticsFixture() (*AnalyticsUsecase, *analyticsRepoFake) { a := &accountRepoFake{accounts: map[uint]*Account{1: {ID: 1}, 7: {ID: 7, Level: 1, IDPath: "0"}}} r := &analyticsRepoFake{data: &AnalyticsData{Available: true, Rows: []*AnalyticsRow{{AnalyticsKey: AnalyticsKey{SourceDB: "lx", ID: 1}, Name: "甲", Counts: PortraitCounts{Register: 2, Active: 4, Pay: 2, NewPay: 1}, Pay: "20.00"}, {AnalyticsKey: AnalyticsKey{SourceDB: "hw", ID: 2}, Name: "乙", Counts: PortraitCounts{Register: 3, Active: 6, Pay: 1, NewPay: 1}, Pay: "30.00"}}, GlobalCounts: &PortraitCounts{Register: 4, Active: 8, Pay: 3}, GlobalPay: "45.00"}} uc := NewAnalyticsUsecase(r, NewAccountUsecase(a), NewUserPreferenceUsecase(&channelPreferenceFake{UserPreference{IncludeSubPromote: true, IDCardDedup: true}}), nil) uc.now = func() time.Time { return time.Date(2026, 11, 4, 9, 0, 0, 0, portraitLocation) } return uc, r } func TestGameAnalyticsSummaryUsesAllRowsAndCorrectDenominators(t *testing.T) { uc, _ := analyticsFixture() page, err := uc.List(accountContext(1, 888), GameAnalytics, AnalyticsFilter{}, 1, 1) if err != nil { t.Fatal(err) } if len(page.Rows) != 1 || page.Total != 2 || page.Rows[0].ID != 2 || page.Summary.Counts.Register != 5 || page.Summary.Pay != "50.00" || page.Summary.PayRate != "30.00" || page.Summary.ARPU != "5.00" || page.Summary.ARPPU != "16.67" { t.Fatalf("游戏汇总、分页或分母错误: %+v", page) } if page.Begin != "2026-10-06" || page.End != "2026-11-04" { t.Fatal("后台缺省统计日期应为含今天的近30天") } } func TestChannelAnalyticsSummaryDependsOnViewer(t *testing.T) { uc, _ := analyticsFixture() page, err := uc.List(accountContext(7, 901), ChannelAnalytics, AnalyticsFilter{}, 1, 10) if err != nil { t.Fatal(err) } if page.Summary.Counts.Register != 5 || page.Summary.Pay != "50.00" { t.Fatal("普通商务应汇总可见展示行") } page, err = uc.List(accountContext(1, 888), ChannelAnalytics, AnalyticsFilter{}, 1, 10) if err != nil || page.Summary.Counts.Register != 4 || page.Summary.Pay != "45.00" || page.Summary.PayRate != "37.50" { t.Fatal("超管应使用筛选范围内全局去重汇总") } } func TestChannelAnalyticsAllPagesSummaryAndDefaultDateRange(t *testing.T) { uc, repo := analyticsFixture() repo.data.EffectiveCount = 3 for _, pageNum := range []int{1, 2, 8} { page, err := uc.List(accountContext(7, 901), ChannelAnalytics, AnalyticsFilter{}, pageNum, 1) if err != nil || page.Total != 2 || page.Summary.Counts.Register != 5 || page.Summary.Counts.Active != 10 || page.Summary.Counts.Pay != 3 || page.Summary.Counts.NewPay != 2 || page.Summary.Pay != "50.00" || page.Summary.PayRate != "30.00" || page.EffectiveCount != 3 { t.Fatalf("翻页不能改变完整合计: page=%d %+v %v", pageNum, page, err) } if page.Begin != "2026-10-06" || page.End != "2026-11-04" || repo.filter.StartDate == nil { t.Fatalf("渠道统计缺省应为含今天的近30天: %+v", repo.filter) } } for _, sample := range []struct { field string asc bool source string }{{"totalPay", true, "lx"}, {"totalPay", false, "hw"}, {"", false, "hw"}} { page, err := uc.List(accountContext(7, 901), ChannelAnalytics, AnalyticsFilter{SortField: sample.field, Ascending: sample.asc}, 1, 1) if err != nil || page.Rows[0].SourceDB != sample.source { t.Fatalf("取消排序应恢复稳定原始顺序: %+v %v", page, err) } } } func TestAnalyticsSortAndMissingData(t *testing.T) { uc, r := analyticsFixture() r.data.Rows[0].Pay = "9007199254740993.00" r.data.Rows[1].Pay = "9007199254740992.99" p, err := uc.List(accountContext(7, 901), GameAnalytics, AnalyticsFilter{SortField: "totalPay"}, 1, 10) if err != nil || p.Rows[0].ID != 1 { t.Fatal("金额排序不能经浮点数失真") } r.data.Available = false p, err = uc.List(accountContext(7, 901), ChannelAnalytics, AnalyticsFilter{}, 1, 10) if err != nil || p.Available || p.Summary != nil { t.Fatal("缺汇总表不能伪造零卡片") } begin := time.Date(2026, 11, 4, 0, 0, 0, 0, portraitLocation) end := begin.AddDate(0, 0, -1) if _, err := uc.List(accountContext(1, 888), GameAnalytics, AnalyticsFilter{StartDate: &begin, EndDate: &end}, 1, 10); err != ErrAnalyticsDateOrder { t.Fatal("倒置日期应被拒绝") } } func TestGameAnalyticsCancelSortRestoresDefaultOrder(t *testing.T) { uc, repo := analyticsFixture() repo.data.Rows = []*AnalyticsRow{ {AnalyticsKey: AnalyticsKey{SourceDB: "lx", ID: 2}, Pay: "5"}, {AnalyticsKey: AnalyticsKey{SourceDB: "lx", ID: 1}, Pay: "10"}, {AnalyticsKey: AnalyticsKey{SourceDB: "lx", ID: 3}, Pay: "20"}, } for _, sample := range []struct { field string ascending bool first int64 }{{"totalPay", true, 2}, {"totalPay", false, 3}, {"", false, 1}} { page, err := uc.List(accountContext(7, 901), GameAnalytics, AnalyticsFilter{SortField: sample.field, Ascending: sample.ascending}, 1, 1) if err != nil || page.Rows[0].ID != sample.first || page.Summary.Pay != "35.00" { t.Fatalf("切换排序不应改变汇总,取消时恢复默认顺序: %+v %v", page, err) } } } func TestAnalyticsLoadsMetadataOnlyForSelectedPage(t *testing.T) { for _, kind := range []AnalyticsKind{ChannelAnalytics, GameAnalytics} { t.Run(string(kind), func(t *testing.T) { uc, repo := analyticsFixture() for _, row := range repo.data.Rows { row.Name = "" } repo.fill = func(rows []*AnalyticsRow) error { for _, row := range rows { row.Name = "本页资料" row.BindingAccount = "本页商务" } return nil } page, err := uc.List(accountContext(7, 901), kind, AnalyticsFilter{SortField: "totalPay"}, 1, 1) if err != nil { t.Fatal(err) } if len(repo.fillCalls) != 1 || len(repo.fillCalls[0]) != 1 || repo.fillCalls[0][0].ID != 2 { t.Fatalf("应只读取排序后当前页的资料: %+v", repo.fillCalls) } if page.Total != 2 || page.Summary.Pay != "50.00" || page.Rows[0].Name != "本页资料" || page.Rows[0].BindingAccount != "本页商务" { t.Fatalf("延迟读取影响汇总或展示资料: %+v", page) } repo.fillCalls = nil page, err = uc.List(accountContext(7, 901), kind, AnalyticsFilter{}, 3, 1) if err != nil || len(page.Rows) != 0 || len(repo.fillCalls) != 0 || page.Summary.Pay != "50.00" { t.Fatal("空页不应读取资料,汇总仍须保留") } repo.fill = func([]*AnalyticsRow) error { return ErrStorage } if _, err := uc.List(accountContext(7, 901), kind, AnalyticsFilter{}, 1, 1); err != ErrStorage { t.Fatalf("资料读取错误必须向上传递: %v", err) } }) } } func TestAnalyticsLoadsMetadataBeforeNameOrTypeSort(t *testing.T) { for _, field := range []string{"promoteName", "promoteType"} { t.Run(field, func(t *testing.T) { uc, repo := analyticsFixture() for _, row := range repo.data.Rows { row.Name = "" } repo.fill = func(rows []*AnalyticsRow) error { for _, row := range rows { name, kind := "Zulu", 9 if row.ID == 2 { name, kind = "Alpha", 1 } row.Name, row.PromoteType = name, &kind } return nil } page, err := uc.List(accountContext(7, 901), ChannelAnalytics, AnalyticsFilter{SortField: field, Ascending: true}, 1, 1) if err != nil || len(page.Rows) != 1 || page.Rows[0].ID != 2 || len(repo.fillCalls) != 1 || len(repo.fillCalls[0]) != 2 { t.Fatalf("按资料字段排序前必须读取全体资料: page=%+v calls=%+v err=%v", page, repo.fillCalls, err) } }) } } func TestAnalyticsDeferredNamesPreserveOfficialAndUnknownLabels(t *testing.T) { uc, repo := analyticsFixture() repo.data.Rows[0].ID, repo.data.Rows[0].Name = 0, "" repo.data.Rows[1].Name = "" page, err := uc.List(accountContext(1, 888), ChannelAnalytics, AnalyticsFilter{SortField: "promoteId", Ascending: true}, 1, 10) if err != nil || page.Rows[0].Name != "官方渠道" || page.Rows[1].Name != "未知渠道(ID:2)" { t.Fatalf("默认渠道名丢失: %+v %v", page, err) } }