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

121 lines
5.7 KiB
Go

package business
import (
"context"
"testing"
"time"
)
type channelPortraitRepoFake struct {
count *int64
first string
tiers *ChannelTierData
tierScope ChannelGameScope
}
func (r *channelPortraitRepoFake) GameCount(context.Context, ChannelKey) (*int64, error) {
return r.count, nil
}
func (r *channelPortraitRepoFake) EarliestDate(context.Context, ChannelGameScope) (string, bool, error) {
return r.first, true, nil
}
func (r *channelPortraitRepoFake) TierData(_ context.Context, s ChannelGameScope) (*ChannelTierData, error) {
r.tierScope = s
return r.tiers, nil
}
type portraitChannelReaderFake struct {
ChannelRepo
rows map[ChannelKey]*Channel
gameScope ChannelGameScope
}
func (r *portraitChannelReaderFake) Find(_ context.Context, scope UserScope, key ChannelKey) (*Channel, error) {
if !scope.All && scope.SelfID == 99 {
return nil, ErrChannelScope
}
row := r.rows[key]
if row == nil {
return nil, ErrChannelNotFound
}
copy := *row
return &copy, nil
}
func (r *portraitChannelReaderFake) Games(_ context.Context, s ChannelGameScope) ([]*ChannelGame, error) {
r.gameScope = s
return []*ChannelGame{{ID: 1, Name: "游戏", RegisterCount: 1, PayAmount: "12.00"}}, nil
}
func channelPortraitFixture() (*ChannelPortraitUsecase, *channelPortraitRepoFake, *portraitChannelReaderFake, *portraitRepoFake) {
base, reports := portraitFixture()
key := ChannelKey{SourceDB: "lx", ID: 101}
parent := ChannelKey{SourceDB: "lx", ID: 100}
count := int64(1)
channels := &portraitChannelReaderFake{rows: map[ChannelKey]*Channel{key: {ChannelKey: key, Account: "child", Nickname: "子渠道", Level: intPointer(2), Status: intPointer(-3), ParentID: 100, BusierID: 14}, parent: {ChannelKey: parent, Account: "parent", Level: intPointer(1)}}}
repo := &channelPortraitRepoFake{count: &count, first: "2026-11-01", tiers: &ChannelTierData{Available: true, RegisterKeys: []string{"A"}}}
uc := NewChannelPortraitUsecase(repo, channels, reports, base.accounts, base.preferences)
uc.now = func() time.Time { return time.Date(2026, 11, 3, 12, 0, 0, 0, portraitLocation) }
return uc, repo, channels, reports
}
func TestChannelPortraitTotalHasNoComparisonAndKeepsCumulativeParts(t *testing.T) {
uc, repo, channels, reports := channelPortraitFixture()
p, err := uc.Portrait(accountContext(7, 901), ChannelKey{SourceDB: "lx", ID: 101}, "total")
if err != nil {
t.Fatal(err)
}
if p.Period != "total" || !reports.scope.NoCompare || reports.scope.Root.ID != 101 || len(reports.scope.Pairs) != 0 {
t.Fatal("累计模式或渠道范围错误")
}
if reports.scope.Range.Begin.Format("2006-01-02") != "2026-11-01" || len(p.RegisterTrend) != 3 || p.Metrics.Active.Value != "3" || p.Metrics.Active.CompareValue != nil || p.Metrics.PayAmount.ChangeRate != nil {
t.Fatal("累计卡片不应带环比,趋势应从最早汇总开始")
}
if p.Basic.Parent == nil || p.Basic.Parent.Account != "parent" || p.Basic.Channel.BusinessDisplayName() != "leaf" {
t.Fatal("上级渠道或归属商务资料未正确补齐")
}
p, err = uc.Portrait(accountContext(7, 901), ChannelKey{SourceDB: "lx", ID: 101}, "last7")
if err != nil {
t.Fatal(err)
}
if len(p.RegisterTrend) != 7 || channels.gameScope.BeginDate != "2026-11-01" || repo.tierScope.BeginDate != "2026-11-01" || reports.scope.Range.Begin.Format("2006-01-02") != "2026-10-28" {
t.Fatal("日期切换不应把游戏分布或分层改成当期口径")
}
}
func TestChannelPortraitMissingDataAndVisibility(t *testing.T) {
uc, repo, _, _ := channelPortraitFixture()
repo.count = nil
p, err := uc.Portrait(accountContext(7, 901), ChannelKey{SourceDB: "lx", ID: 101}, "today")
if err != nil || p.Available || p.Metrics != nil || p.Basic.Channel.Account != "child" {
t.Fatal("缺业务数据时应保留已读取资料,不造零指标")
}
if _, err := uc.Portrait(accountContext(99, 901), ChannelKey{SourceDB: "lx", ID: 101}, "today"); err != ErrChannelScope {
t.Fatal("越权渠道画像被放行")
}
if normalizeChannelPortraitPeriod("thismonth") != "today" || normalizeChannelPortraitPeriod("TOTAL") != "total" {
t.Fatal("渠道周期范围与商务画像混用")
}
}
func TestChannelPayTiersUseRegisteredIdentitiesAndExactThresholds(t *testing.T) {
data := &ChannelTierData{RegisterKeys: []string{"A-x", "A-x", "B", "C", "D", "E", "F"}, Payments: []*ChannelIdentityPay{{IDCard: stringPointer(" A-X "), Amount: "9999.50"}, {IDCard: stringPointer("A-x"), Amount: "0.50"}, {IDCard: stringPointer("B"), Amount: "1000.00"}, {IDCard: stringPointer("C"), Amount: "0.01"}, {IDCard: stringPointer("D"), Amount: "0"}, {IDCard: stringPointer("E"), Amount: "-1"}, {IDCard: stringPointer("outside"), Amount: "999999"}}}
tiers, err := buildChannelPayTiers("lx", true, data)
if err != nil {
t.Fatal(err)
}
want := []int64{1, 1, 1, 3}
for i, tier := range tiers {
if tier.Count != want[i] {
t.Fatalf("分层边界、归一或成员范围错误: %+v", tiers)
}
}
if tiers[0].Ratio != "16.7" || tiers[3].Ratio != "50.0" {
t.Fatal("分层比例应独立保留一位,不强行凑整")
}
id1, id2, id4 := uint64(1), uint64(2), uint64(4)
tiers, err = buildChannelPayTiers("lx", false, &ChannelTierData{RegisterKeys: []string{"lx|1", "lx|2", "lx|3"}, Payments: []*ChannelIdentityPay{{UserID: &id1, Amount: "1000"}, {UserID: &id2, Amount: "999.99"}, {UserID: &id4, Amount: "100000"}}})
if err != nil || tiers[0].Count != 0 || tiers[1].Count != 1 || tiers[2].Count != 1 || tiers[3].Count != 1 {
t.Fatal("模式0必须使用带平台前缀的原始用户 ID,未注册身份不参与分层")
}
tiers, err = buildChannelPayTiers("lx", false, &ChannelTierData{FallbackCount: 5})
if err != nil || tiers[3].Count != 5 || tiers[3].Ratio != "100.0" {
t.Fatal("无身份列表时应保留原回退的免费人数")
}
}