package business import ( "context" "testing" "time" ) type rankingRepoFake struct { data *RankingData filter RankingRange calls int } func (r *rankingRepoFake) Collect(_ context.Context, _ RankingKind, _ AnalyticsScope, f RankingRange) (*RankingData, error) { r.filter = f r.calls++ out := &RankingData{Available: r.data.Available, Businesses: r.data.Businesses, Current: map[AnalyticsKey]*RankingMetrics{}, Previous: map[AnalyticsKey]*RankingMetrics{}} for k, v := range r.data.Current { c := *v out.Current[k] = &c } for k, v := range r.data.Previous { c := *v out.Previous[k] = &c } return out, nil } func TestRankingComparisonsAndBusinessCountSemantics(t *testing.T) { k := AnalyticsKey{SourceDB: "lx", ID: 1} zero := AnalyticsKey{SourceDB: "lx", ID: 2} r := &rankingRepoFake{data: &RankingData{Available: true, Current: map[AnalyticsKey]*RankingMetrics{k: {Counts: PortraitCounts{Register: 3, Active: 4, Pay: 2}, Pay: "20.005"}, zero: {Counts: PortraitCounts{Active: 1}}}, Previous: map[AnalyticsKey]*RankingMetrics{k: {Counts: PortraitCounts{Register: 2, Active: 2, Pay: 1}, Pay: "10"}}}} a := &accountRepoFake{accounts: map[uint]*Account{7: {ID: 7, Level: 1, IDPath: "0"}, 9: {ID: 9, Level: 3, IDPath: "0,7"}}} uc := NewRankingUsecase(r, NewAccountUsecase(a), NewUserPreferenceUsecase(&channelPreferenceFake{UserPreference{IncludeSubPromote: true, IDCardDedup: true}})) uc.now = func() time.Time { return time.Date(2026, 11, 4, 9, 0, 0, 0, portraitLocation) } begin, end := time.Date(2026, 10, 29, 0, 0, 0, 0, portraitLocation), time.Date(2026, 11, 4, 0, 0, 0, 0, portraitLocation) p, err := uc.List(accountContext(7, 901), GameRanking, AnalyticsFilter{StartDate: &begin, EndDate: &end}) if err != nil || len(p.Register) != 1 || p.Register[0].ObjectID != "lx|1" || p.Register[0].Name != "游戏1" || *p.Register[0].MoM != "50.00" || p.Recharge[0].Value != "20.01" || *p.Recharge[0].MoM != "100.05" || len(p.PayRate) != 2 || p.PayRate[1].Value != "0.00" || p.PayRate[1].MoM != nil { t.Fatalf("排行筛选、命名或环比错误: %+v %v", p, err) } if *p.PayRate[0].MoM != "0.00" || *p.PayRate[0].MoMStatus != 0 || r.filter.PreviousBegin != "2026-10-22" || r.filter.PreviousEnd != "2026-10-28" { t.Fatal("等长上期或付费率环比错误") } r.data.Businesses = []*RankingBusiness{{ID: 8, Name: "二级商务", Level: 2, Channels: []AnalyticsKey{k, zero}}} p, err = uc.List(accountContext(7, 901), BusinessRanking, AnalyticsFilter{}) if err != nil || p.Active[0].Value != "5" || p.PayRate[0].Value != "40.00" || p.Register[0].Name != "二级商务 (L2)" || p.Register[0].SourceDB != "" { t.Fatal("商务需加总各渠道人数,不全局去重") } if p.Begin != "2026-10-29" || p.End != "2026-11-04" || r.filter.PreviousBegin != "2026-10-22" || r.filter.PreviousEnd != "2026-10-28" { t.Fatal("排行榜缺省为含今天的近7天,上期范围应等长") } calls := r.calls p, err = uc.List(accountContext(9, 903), BusinessRanking, AnalyticsFilter{}) if err != nil || len(p.Register) != 0 || r.calls != calls { t.Fatal("三级商务不得展开商务榜查询") } r.data.Available = false p, err = uc.List(accountContext(7, 901), ChannelRanking, AnalyticsFilter{}) if err != nil || p.Available || len(p.Active) != 0 { t.Fatal("缺数据不能产生伪造排行") } } func TestRankingTopTwentyAndStatusThreshold(t *testing.T) { current, previous := map[AnalyticsKey]*RankingMetrics{}, map[AnalyticsKey]*RankingMetrics{} for i := int64(1); i <= 25; i++ { k := AnalyticsKey{SourceDB: "lx", ID: i} current[k] = &RankingMetrics{Counts: PortraitCounts{Active: i}, Pay: "9007199254740992.99"} previous[k] = &RankingMetrics{Counts: PortraitCounts{Active: i}} } current[AnalyticsKey{SourceDB: "lx", ID: 25}].Pay = "9007199254740993.00" rows, err := buildRankingItems(ChannelRanking, "pay", current, previous) if err != nil || len(rows) != 20 || rows[0].ID != 25 || rows[19].Rank != 20 { t.Fatal("TOP20 或精确金额排序错误") } rows, err = buildRankingItems(ChannelRanking, "active", current, previous) if err != nil || *rows[0].MoMStatus != 0 || *rows[0].MoM != "0.00" { t.Fatal("持平环比错误") } } func TestRankingPayRateRelativeGrowth(t *testing.T) { for _, tc := range []struct { name string current PortraitCounts previous *RankingMetrics want string status int }{ {"increase", PortraitCounts{Pay: 12, Active: 100}, &RankingMetrics{Counts: PortraitCounts{Pay: 10, Active: 100}}, "20.00", 1}, {"decrease", PortraitCounts{Pay: 10, Active: 100}, &RankingMetrics{Counts: PortraitCounts{Pay: 20, Active: 100}}, "-50.00", -1}, {"unchanged", PortraitCounts{Pay: 10, Active: 100}, &RankingMetrics{Counts: PortraitCounts{Pay: 10, Active: 100}}, "0.00", 0}, {"no current payers", PortraitCounts{Active: 100}, &RankingMetrics{Counts: PortraitCounts{Pay: 10, Active: 100}}, "-100.00", -1}, {"zero prior rate", PortraitCounts{Pay: 10, Active: 100}, &RankingMetrics{Counts: PortraitCounts{Active: 100}}, "", 0}, {"no prior active players", PortraitCounts{Pay: 10, Active: 100}, &RankingMetrics{Counts: PortraitCounts{Pay: 1}}, "", 0}, {"no prior row", PortraitCounts{Pay: 10, Active: 100}, nil, "", 0}, } { t.Run(tc.name, func(t *testing.T) { key := AnalyticsKey{SourceDB: "lx", ID: 1} rows, err := buildRankingItems(ChannelRanking, "rate", map[AnalyticsKey]*RankingMetrics{key: {Counts: tc.current}}, map[AnalyticsKey]*RankingMetrics{key: tc.previous}) if err != nil || len(rows) != 1 { t.Fatalf("ranking: %v, %v", rows, err) } row := rows[0] if tc.want == "" { if row.MoM != nil || row.MoMStatus != nil { t.Fatal("missing or zero baseline must not produce a growth percentage") } } else if row.MoM == nil || *row.MoM != tc.want || row.MoMStatus == nil || *row.MoMStatus != tc.status { t.Fatalf("expected %s%% with status %d, got %+v", tc.want, tc.status, row) } }) } }