313 lines
9.4 KiB
Go
313 lines
9.4 KiB
Go
package business
|
|
|
|
import (
|
|
"context"
|
|
"math/big"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-kratos/kratos/v3/errors"
|
|
)
|
|
|
|
type ChannelPortraitBasic struct {
|
|
Channel *Channel
|
|
Parent *Channel
|
|
GameCount *int64
|
|
}
|
|
type ChannelPortraitMetrics struct{ Register, PayAmount, PayRate, Active *PortraitMetric }
|
|
type ChannelPayTier struct {
|
|
Tier, Label string
|
|
Count int64
|
|
Ratio string
|
|
}
|
|
type ChannelPortrait struct {
|
|
Basic *ChannelPortraitBasic
|
|
Period string
|
|
Metrics *ChannelPortraitMetrics
|
|
RegisterTrend, PayTrend []*PortraitTrend
|
|
GameDistribution []*ChannelGame
|
|
PayTiers []*ChannelPayTier
|
|
Available bool
|
|
UnavailableReason string
|
|
}
|
|
type ChannelIdentityPay struct {
|
|
UserID *uint64
|
|
IDCard *string
|
|
Amount string
|
|
}
|
|
type ChannelTierData struct {
|
|
RegisterKeys []string
|
|
FallbackCount int64
|
|
Payments []*ChannelIdentityPay
|
|
Available bool
|
|
}
|
|
type ChannelPortraitRepo interface {
|
|
GameCount(context.Context, ChannelKey) (*int64, error)
|
|
EarliestDate(context.Context, ChannelGameScope) (string, bool, error)
|
|
TierData(context.Context, ChannelGameScope) (*ChannelTierData, error)
|
|
}
|
|
type ChannelPortraitUsecase struct {
|
|
repo ChannelPortraitRepo
|
|
channels ChannelRepo
|
|
reports BusinessPortraitRepo
|
|
accounts *AccountUsecase
|
|
preferences *UserPreferenceUsecase
|
|
now func() time.Time
|
|
}
|
|
|
|
func NewChannelPortraitUsecase(repo ChannelPortraitRepo, channels ChannelRepo, reports BusinessPortraitRepo, accounts *AccountUsecase, preferences *UserPreferenceUsecase) *ChannelPortraitUsecase {
|
|
return &ChannelPortraitUsecase{repo: repo, channels: channels, reports: reports, accounts: accounts, preferences: preferences, now: time.Now}
|
|
}
|
|
func normalizeChannelPortraitPeriod(period string) string {
|
|
switch value := strings.ToLower(strings.TrimSpace(period)); value {
|
|
case "today", "yesterday", "last7", "last30", "total":
|
|
return value
|
|
}
|
|
return "today"
|
|
}
|
|
func ChannelLevelLabel(level *int) string {
|
|
if level == nil {
|
|
return ""
|
|
}
|
|
return map[int]string{1: "一级渠道", 2: "二级渠道", 3: "三级渠道"}[*level]
|
|
}
|
|
func ChannelStatusLabel(status *int) string {
|
|
if status == nil {
|
|
return ""
|
|
}
|
|
return map[int]string{0: "待审核", 1: "正常", -2: "驳回", -3: "商务新增"}[*status]
|
|
}
|
|
func ChannelTypeLabel(registerType *int) string {
|
|
if registerType == nil {
|
|
return ""
|
|
}
|
|
if *registerType == 1 {
|
|
return "公会"
|
|
}
|
|
return "个人"
|
|
}
|
|
func ChannelUnixTime(value *int64) *time.Time {
|
|
if value == nil || *value <= 0 {
|
|
return nil
|
|
}
|
|
t := time.Unix(*value, 0).In(portraitLocation)
|
|
return &t
|
|
}
|
|
func unavailableChannelPortrait(p *ChannelPortrait, reason string) *ChannelPortrait {
|
|
p.Available = false
|
|
p.UnavailableReason = reason
|
|
p.Metrics = nil
|
|
p.RegisterTrend = []*PortraitTrend{}
|
|
p.PayTrend = []*PortraitTrend{}
|
|
p.GameDistribution = []*ChannelGame{}
|
|
p.PayTiers = []*ChannelPayTier{}
|
|
return p
|
|
}
|
|
func (uc *ChannelPortraitUsecase) Portrait(ctx context.Context, key ChannelKey, period string) (*ChannelPortrait, error) {
|
|
source, err := NormalizePlatform(key.SourceDB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
key.SourceDB = source
|
|
actor, account, err := uc.accounts.actor(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
viewer := portraitUserScope(account)
|
|
viewer.All = actor.AuthorityID == 888
|
|
root, err := uc.channels.Find(ctx, viewer, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
period = normalizeChannelPortraitPeriod(period)
|
|
profile := &ChannelPortrait{Basic: &ChannelPortraitBasic{Channel: root}, Period: period, Available: true, RegisterTrend: []*PortraitTrend{}, PayTrend: []*PortraitTrend{}, GameDistribution: []*ChannelGame{}, PayTiers: []*ChannelPayTier{}}
|
|
if (root.Level == nil || *root.Level != 1) && root.ParentID > 0 {
|
|
parent, err := uc.channels.Find(ctx, UserScope{All: true}, ChannelKey{SourceDB: source, ID: root.ParentID})
|
|
if err != nil && !errors.IsNotFound(err) {
|
|
return nil, err
|
|
}
|
|
profile.Basic.Parent = parent
|
|
}
|
|
if root.BusierID > 0 {
|
|
bindings, err := uc.reports.Bindings(ctx, viewer, []string{source}, &PlatformKey{SourceDB: source, BusierID: root.BusierID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(bindings) > 0 {
|
|
b := bindings[0]
|
|
root.BusierUserID = &b.UserID
|
|
root.BindingAccount = b.Account
|
|
owner, err := uc.accounts.repo.Find(ctx, b.UserID)
|
|
if err != nil && !errors.IsNotFound(err) {
|
|
return nil, err
|
|
}
|
|
if owner != nil {
|
|
root.BindingUserName = owner.Username
|
|
}
|
|
}
|
|
}
|
|
profile.Basic.GameCount, err = uc.repo.GameCount(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if profile.Basic.GameCount == nil {
|
|
return unavailableChannelPortrait(profile, "推广游戏数据尚未接入,渠道画像指标暂不可用"), nil
|
|
}
|
|
pref, err := uc.preferences.Current(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := uc.now()
|
|
today := now.In(portraitLocation).Format("2006-01-02")
|
|
cumulative := ChannelGameScope{Root: root, IncludeSub: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup, EndDate: today}
|
|
begin, available, err := uc.repo.EarliestDate(ctx, cumulative)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !available {
|
|
return unavailableChannelPortrait(profile, "日汇总数据尚未接入,渠道画像指标暂不可用"), nil
|
|
}
|
|
if begin == "" {
|
|
begin = today
|
|
}
|
|
cumulative.BeginDate = begin
|
|
ranges := portraitRanges(period, now)
|
|
if period == "total" {
|
|
ranges.Begin, err = time.ParseInLocation("2006-01-02", begin, portraitLocation)
|
|
if err != nil {
|
|
return nil, ErrStorage
|
|
}
|
|
ranges.Hourly = false
|
|
ranges.CompareLabel = ""
|
|
}
|
|
stats, err := uc.reports.Statistics(ctx, PortraitReportScope{Root: root, Sources: []string{source}, IncludeSub: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup, Range: ranges, NoCompare: period == "total"})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !stats.Available {
|
|
return unavailableChannelPortrait(profile, "汇总或趋势数据尚未接入,渠道画像指标暂不可用"), nil
|
|
}
|
|
profile.Metrics, err = channelPortraitMetrics(stats, ranges.CompareLabel, period != "total")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
profile.RegisterTrend, err = portraitTrend(stats.RegisterSeries, ranges, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
profile.PayTrend, err = portraitTrend(stats.PaySeries, ranges, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
games, err := uc.channels.Games(ctx, cumulative)
|
|
if err != nil {
|
|
if err == ErrChannelReportsUnavailable {
|
|
return unavailableChannelPortrait(profile, "推广游戏汇总数据尚未接入"), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
profile.GameDistribution, err = normalizeChannelGames(games)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tiers, err := uc.repo.TierData(ctx, cumulative)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !tiers.Available {
|
|
return unavailableChannelPortrait(profile, "付费分层所需的注册或支付数据尚未接入"), nil
|
|
}
|
|
profile.PayTiers, err = buildChannelPayTiers(source, pref.IDCardDedup, tiers)
|
|
return profile, err
|
|
}
|
|
func channelPortraitMetrics(stats *PortraitStatistics, label string, compare bool) (*ChannelPortraitMetrics, error) {
|
|
base, err := portraitMetrics(stats, label)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
active, err := portraitRateCard(strconv.FormatInt(min(stats.Current.Active, int64(2147483647)), 10), strconv.FormatInt(min(stats.Previous.Active, int64(2147483647)), 10), label)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := &ChannelPortraitMetrics{Register: base.Register, PayAmount: base.PayAmount, PayRate: base.PayRate, Active: active}
|
|
if !compare {
|
|
for _, card := range []*PortraitMetric{result.Register, result.PayAmount, result.PayRate, result.Active} {
|
|
card.CompareValue = nil
|
|
card.ChangeDiff = nil
|
|
card.ChangeRate = nil
|
|
card.CompareLabel = ""
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
func buildChannelPayTiers(source string, dedup bool, data *ChannelTierData) ([]*ChannelPayTier, error) {
|
|
keys := map[string]bool{}
|
|
for _, key := range data.RegisterKeys {
|
|
keys[key] = true
|
|
}
|
|
denominator := int64(len(keys))
|
|
if denominator == 0 {
|
|
denominator = min(data.FallbackCount, int64(2147483647))
|
|
}
|
|
counts := []int64{0, 0, 0, 0}
|
|
if len(keys) == 0 {
|
|
counts[3] = denominator
|
|
} else {
|
|
payByKey := map[string]*big.Rat{}
|
|
for _, payment := range data.Payments {
|
|
var key string
|
|
if dedup {
|
|
if payment.IDCard == nil {
|
|
continue
|
|
}
|
|
key = normalizeIDCard(*payment.IDCard)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
} else {
|
|
if payment.UserID == nil {
|
|
continue
|
|
}
|
|
key = source + "|" + strconv.FormatUint(*payment.UserID, 10)
|
|
}
|
|
amount, err := portraitNumber(payment.Amount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
amount, _ = portraitNumber(portraitDecimal(amount, 2))
|
|
if payByKey[key] == nil {
|
|
payByKey[key] = new(big.Rat)
|
|
}
|
|
payByKey[key].Add(payByKey[key], amount)
|
|
}
|
|
for key := range keys {
|
|
pay := payByKey[key]
|
|
if pay == nil {
|
|
pay = new(big.Rat)
|
|
}
|
|
switch {
|
|
case pay.Cmp(big.NewRat(10000, 1)) >= 0:
|
|
counts[0]++
|
|
case pay.Cmp(big.NewRat(1000, 1)) >= 0:
|
|
counts[1]++
|
|
case pay.Sign() > 0:
|
|
counts[2]++
|
|
default:
|
|
counts[3]++
|
|
}
|
|
}
|
|
}
|
|
result := []*ChannelPayTier{}
|
|
codes := []string{"whale", "mid", "small", "free"}
|
|
labels := []string{"大R", "中R", "小R", "免费"}
|
|
for i, code := range codes {
|
|
ratio := "0.0"
|
|
if denominator > 0 {
|
|
ratio = portraitDecimal(new(big.Rat).Mul(big.NewRat(counts[i], denominator), big.NewRat(100, 1)), 1)
|
|
}
|
|
result = append(result, &ChannelPayTier{Tier: code, Label: labels[i], Count: counts[i], Ratio: ratio})
|
|
}
|
|
return result, nil
|
|
}
|