package business import ( "context" "fmt" "math/big" "sort" "strconv" "strings" "time" ) type RankingKind string const ( ChannelRanking RankingKind = "channel" BusinessRanking RankingKind = "business" GameRanking RankingKind = "game" ) type RankingRange struct { AnalyticsFilter PreviousBegin, PreviousEnd string } type RankingMetrics struct { Counts PortraitCounts Pay, Name string } type RankingBusiness struct { ID uint Name string Level int Channels []AnalyticsKey } type RankingData struct { Current, Previous map[AnalyticsKey]*RankingMetrics Businesses []*RankingBusiness Available bool } type RankingItem struct { AnalyticsKey ObjectID, Name, Value string Rank int MoM *string MoMStatus *int } type RankingPage struct { Register, Recharge, PayRate, Active []*RankingItem Begin, End string Available bool } type RankingRepo interface { Collect(context.Context, RankingKind, AnalyticsScope, RankingRange) (*RankingData, error) } type RankingUsecase struct { repo RankingRepo accounts *AccountUsecase preferences *UserPreferenceUsecase now func() time.Time } func NewRankingUsecase(repo RankingRepo, accounts *AccountUsecase, preferences *UserPreferenceUsecase) *RankingUsecase { return &RankingUsecase{repo: repo, accounts: accounts, preferences: preferences, now: time.Now} } func (uc *RankingUsecase) List(ctx context.Context, kind RankingKind, f AnalyticsFilter) (*RankingPage, error) { actor, account, err := uc.accounts.actor(ctx) if err != nil { return nil, err } if kind != ChannelRanking && kind != BusinessRanking && kind != GameRanking { return nil, ErrAnalyticsInput } f.StartDate, f.EndDate = reportDateDefaults(f.StartDate, f.EndDate, 7, uc.now()) if f.EndDate.Before(*f.StartDate) { return nil, ErrAnalyticsDateOrder } f.Begin, f.End = f.StartDate.Format("2006-01-02"), f.EndDate.Format("2006-01-02") out := &RankingPage{Register: []*RankingItem{}, Recharge: []*RankingItem{}, PayRate: []*RankingItem{}, Active: []*RankingItem{}, Begin: f.Begin, End: f.End, Available: true} if kind == BusinessRanking && actor.AuthorityID != 888 && account.Level >= 3 { return out, nil } pref, err := uc.preferences.Current(ctx) if err != nil { return nil, err } viewer := portraitUserScope(account) viewer.All = actor.AuthorityID == 888 days := int((f.EndDate.Unix()-f.StartDate.Unix())/86400) + 1 rangeFilter := RankingRange{AnalyticsFilter: f, PreviousBegin: f.StartDate.AddDate(0, 0, -days).Format("2006-01-02"), PreviousEnd: f.StartDate.AddDate(0, 0, -1).Format("2006-01-02")} data, err := uc.repo.Collect(ctx, kind, AnalyticsScope{Viewer: viewer, IncludeSub: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup}, rangeFilter) if err != nil { return nil, err } out.Available = data.Available if !data.Available { return out, nil } for _, period := range []map[AnalyticsKey]*RankingMetrics{data.Current, data.Previous} { for _, row := range period { row.Counts = analyticsCap(row.Counts) } } if kind == BusinessRanking { data.Current, err = aggregateRankingBusinesses(data.Businesses, data.Current) if err != nil { return nil, err } data.Previous, err = aggregateRankingBusinesses(data.Businesses, data.Previous) if err != nil { return nil, err } } for _, metric := range []struct { name string target *[]*RankingItem }{{"register", &out.Register}, {"pay", &out.Recharge}, {"rate", &out.PayRate}, {"active", &out.Active}} { rows, err := buildRankingItems(kind, metric.name, data.Current, data.Previous) if err != nil { return nil, err } *metric.target = rows } return out, nil } func aggregateRankingBusinesses(businesses []*RankingBusiness, channels map[AnalyticsKey]*RankingMetrics) (map[AnalyticsKey]*RankingMetrics, error) { out := map[AnalyticsKey]*RankingMetrics{} for _, target := range businesses { if len(target.Channels) == 0 { continue } row := &RankingMetrics{Name: fmt.Sprintf("%s (L%d)", target.Name, target.Level)} pay := new(big.Rat) for _, key := range target.Channels { if c := channels[key]; c != nil { row.Counts = analyticsAdd(row.Counts, c.Counts) n, err := portraitNumber(c.Pay) if err != nil { return nil, err } pay.Add(pay, n) } } // 商务人数沿用各渠道人数相加;使用 int64 避免原 Java int 溢出,不跨渠道重新去重。 row.Pay = pay.RatString() out[AnalyticsKey{ID: int64(target.ID)}] = row } return out, nil } func rankingValue(row *RankingMetrics, metric string) (*big.Rat, bool, error) { if row == nil { return new(big.Rat), false, nil } switch metric { case "register": return big.NewRat(row.Counts.Register, 1), row.Counts.Register > 0, nil case "active": return big.NewRat(row.Counts.Active, 1), row.Counts.Active > 0, nil case "rate": value := analyticsRatio(big.NewRat(row.Counts.Pay, 1), row.Counts.Active, true) n, _ := portraitNumber(value) return n, row.Counts.Active > 0, nil default: n, err := portraitNumber(row.Pay) return n, err == nil && n.Sign() > 0, err } } func buildRankingItems(kind RankingKind, metric string, current, previous map[AnalyticsKey]*RankingMetrics) ([]*RankingItem, error) { rows := []*RankingItem{} values := map[*RankingItem]*big.Rat{} for key, row := range current { value, keep, err := rankingValue(row, metric) if err != nil { return nil, err } if !keep { continue } item := &RankingItem{AnalyticsKey: key, ObjectID: strconv.FormatInt(key.ID, 10), Name: row.Name} if kind == GameRanking { item.ObjectID = key.SourceDB + "|" + item.ObjectID if strings.TrimSpace(item.Name) == "" { item.Name = "游戏" + strconv.FormatInt(key.ID, 10) } } if kind == ChannelRanking { item.Name = (&LedgerNames{PromoteID: &key.ID, PromoteAccount: row.Name}).DisplayPromote() } digits := 0 if metric == "pay" || metric == "rate" { digits = 2 } item.Value = portraitDecimal(value, digits) values[item], _ = portraitNumber(item.Value) prev := previous[key] if prev != nil { prior, valid, err := rankingValue(prev, metric) if err != nil { return nil, err } // 四个榜单统一使用相对上期的增长率。上期为零或无有效 // 活跃基数时不能计算环比,保留 nil 供页面显示无可比数据。 if valid && prior.Sign() > 0 { change := new(big.Rat).Sub(value, prior) change.Mul(change, big.NewRat(100, 1)) change.Quo(change, prior) item.MoM = stringPointer(portraitDecimal(change, 2)) rounded, _ := portraitNumber(*item.MoM) status := 0 if new(big.Rat).Abs(rounded).Cmp(big.NewRat(1, 100)) >= 0 { status = rounded.Sign() } item.MoMStatus = &status } } rows = append(rows, item) } sort.SliceStable(rows, func(i, j int) bool { a, b := rows[i], rows[j] if c := values[a].Cmp(values[b]); c != 0 { return c > 0 } if a.SourceDB != b.SourceDB { return a.SourceDB < b.SourceDB } return a.ID < b.ID }) rows = rows[:min(20, len(rows))] for i, row := range rows { row.Rank = i + 1 } return rows, nil }