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 }