Files
kra-new/internal/biz/business/ranking.go
T

236 lines
7.1 KiB
Go

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
}
if f.StartDate == nil || f.EndDate == nil {
day := workbenchWindow(uc.now()).Today
f.StartDate, f.EndDate = &day, &day
}
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
}