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

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
}