236 lines
7.1 KiB
Go
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
|
|
}
|