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

133 lines
7.4 KiB
Go

package business
import (
"context"
"testing"
"time"
)
type portraitRepoFake struct {
BusinessPortraitRepo
bindings []*Binding
channels []*PortraitChannel
channelsAvailable bool
stats *PortraitStatistics
scope PortraitReportScope
platforms map[uint64]*PortraitPlatformBusiness
}
func (r *portraitRepoFake) Bindings(_ context.Context, scope UserScope, _ []string, _ *PlatformKey) ([]*Binding, error) {
if !scope.All && scope.SelfID == 99 {
return []*Binding{}, nil
}
return r.bindings, nil
}
func (r *portraitRepoFake) Channels(_ context.Context, pairs []PlatformKey, _ bool) ([]*PortraitChannel, bool, error) {
if len(pairs) == 0 {
return []*PortraitChannel{}, true, nil
}
return r.channels, r.channelsAvailable, nil
}
func (r *portraitRepoFake) Statistics(_ context.Context, scope PortraitReportScope) (*PortraitStatistics, error) {
r.scope = scope
return r.stats, nil
}
func (r *portraitRepoFake) PlatformBusiness(_ context.Context, key PlatformKey) (*PortraitPlatformBusiness, error) {
return r.platforms[key.BusierID], nil
}
func portraitFixture() (*BusinessPortraitUsecase, *portraitRepoFake) {
accounts := &accountRepoFake{accounts: map[uint]*Account{7: {ID: 7, Username: "owner", RealName: "商务甲", Level: 1, IDPath: "0"}, 9: {ID: 9, Username: "leaf", Level: 3, ParentID: 7, IDPath: "0,7"}, 99: {ID: 99, Username: "other", Level: 1, IDPath: "0"}}}
created := time.Date(2026, 11, 1, 0, 0, 0, 0, portraitLocation).Unix()
r := &portraitRepoFake{bindings: []*Binding{{UserID: 9, PlatformKey: PlatformKey{SourceDB: "lx", BusierID: 14}, Status: "0"}}, channelsAvailable: true, channels: []*PortraitChannel{{ChannelKey: ChannelKey{SourceDB: "lx", ID: 100}, Level: intPointer(1), Account: "一级渠道", CreatedUnix: &created}}, platforms: map[uint64]*PortraitPlatformBusiness{14: {ID: 14, Account: "platform", RealName: "平台商务", Level: intPointer(3), Status: intPointer(0), Commerce2: portraitUint(12), CreatedUnix: &created}, 12: {ID: 12, Account: "platform-parent", RealName: "平台上级"}}}
r.stats = &PortraitStatistics{Available: true, Current: PortraitCounts{Register: 2, Active: 3, Pay: 2}, Previous: PortraitCounts{Register: 1, Active: 4, Pay: 1}, CurrentPay: "12.00", PreviousPay: "6.00", RegisterSeries: map[string]string{"2026-11-01": "2", "0": "3"}, PaySeries: map[string]string{"0": "1.25"}, RegisterRanks: []*PortraitStatRank{{ChannelKey: ChannelKey{SourceDB: "lx", ID: 100}, Value: "2"}}, PayRanks: []*PortraitStatRank{{ChannelKey: ChannelKey{SourceDB: "lx", ID: 100}, Value: "12.00"}}}
uc := NewBusinessPortraitUsecase(r, NewAccountUsecase(accounts), NewUserPreferenceUsecase(&channelPreferenceFake{UserPreference{IncludeSubPromote: true, IDCardDedup: true}}))
uc.now = func() time.Time { return time.Date(2026, 11, 3, 12, 0, 0, 0, portraitLocation) }
return uc, r
}
func portraitUint(n int64) *int64 { return &n }
func TestPortraitCalendarRangesClampPreviousMonth(t *testing.T) {
today := time.Date(2024, 3, 31, 10, 0, 0, 0, portraitLocation)
cases := []struct{ period, begin, end, prevBegin, prevEnd string }{{"today", "2024-03-31", "2024-03-31", "2024-03-30", "2024-03-30"}, {"yesterday", "2024-03-30", "2024-03-30", "2024-03-29", "2024-03-29"}, {"last7", "2024-03-25", "2024-03-31", "2024-03-18", "2024-03-24"}, {"last30", "2024-03-02", "2024-03-31", "2024-02-01", "2024-03-01"}, {"thismonth", "2024-03-01", "2024-03-31", "2024-02-01", "2024-02-29"}}
for _, tc := range cases {
r := portraitRanges(tc.period, today)
if r.Begin.Format("2006-01-02") != tc.begin || r.End.Format("2006-01-02") != tc.end || r.PreviousBegin.Format("2006-01-02") != tc.prevBegin || r.PreviousEnd.Format("2006-01-02") != tc.prevEnd {
t.Fatalf("日期范围错误: %+v %+v", tc, r)
}
}
if normalizePortraitPeriod(" thisMonth ") != "thismonth" || normalizePortraitPeriod("total") != "today" {
t.Fatal("商务画像与渠道累计模式混用")
}
}
func TestPortraitMetricsUseActiveDenominatorAndPercentagePoints(t *testing.T) {
uc, r := portraitFixture()
portrait, err := uc.ForUser(accountContext(7, 901), 7, []string{"lx"}, "today")
if err != nil {
t.Fatal(err)
}
m := portrait.Metrics
if m.Register.Value != "2" || *m.Register.ChangeRate != "100.0" || m.PayRate.Value != "66.7" || *m.PayRate.CompareValue != "25.0" || *m.PayRate.ChangeDiff != "41.7" || m.ARPU.Value != "6.00" || *m.ARPU.ChangeRate != "0.0" {
t.Fatalf("画像卡片计算错误: %+v", m)
}
if *portrait.Basic.PromoteCount != 1 || *portrait.Basic.MonthNewPromote != 1 || len(portrait.RegisterTrend) != 24 || portrait.RegisterTrend[0].Value != "3" || portrait.PayTrend[0].Value != "1.25" {
t.Fatal("基本渠道计数或小时趋势错误")
}
if r.scope.RankLimit != 5 || !r.scope.IDCardDedup || !r.scope.IncludeSub {
t.Fatal("画像没有使用查看人的全局偏好")
}
r.stats.Previous.Active = 0
portrait, err = uc.ForUser(accountContext(7, 901), 7, []string{"lx"}, "last7")
if err != nil {
t.Fatal(err)
}
if portrait.Metrics.PayRate.CompareValue != nil || portrait.Metrics.PayRate.ChangeDiff != nil || len(portrait.RegisterTrend) != 7 {
t.Fatal("无上期活跃时不应伪造付费率环比")
}
}
func TestPortraitEmptyScopeAndUnavailableDataAreDifferent(t *testing.T) {
uc, r := portraitFixture()
r.channelsAvailable = false
p, err := uc.ForUser(accountContext(7, 901), 7, []string{"lx"}, "today")
if err != nil || p.Available || p.Basic.UserName != "owner" || p.Basic.PromoteCount != nil || p.Metrics != nil {
t.Fatal("缺表时应保留基本资料并标记未知指标")
}
r.bindings = nil
p, err = uc.ForUser(accountContext(7, 901), 7, []string{"lx"}, "today")
if err != nil || !p.Available || *p.Basic.PromoteCount != 0 || p.Metrics.Register.Value != "0" || len(p.RegisterTrend) != 24 {
t.Fatal("无绑定属于确定空数据")
}
if _, err := uc.ForUser(accountContext(99, 901), 7, []string{"lx"}, "today"); err != ErrAccountScope {
t.Fatal("不能查询其他一级商务画像")
}
}
func TestBindingPortraitUsesPlatformHierarchyAndStatus(t *testing.T) {
uc, _ := portraitFixture()
p, err := uc.ForBinding(accountContext(9, 903), PlatformKey{SourceDB: "lx", BusierID: 14}, "today")
if err != nil {
t.Fatal(err)
}
if p.Basic.UserID != 14 || p.Basic.UserName != "platform" || p.Basic.ParentUserID == nil || *p.Basic.ParentUserID != 12 || p.Basic.ParentUserName != "platform-parent" || *p.Basic.Status != "0" || p.Basic.StatusLabel != "待审核" {
t.Fatalf("混淆平台与 SW 的 ID、上级或状态: %+v", p.Basic)
}
if _, err := uc.ForBinding(accountContext(99, 901), PlatformKey{SourceDB: "lx", BusierID: 14}, "today"); err != ErrAccountScope {
t.Fatal("范围外 SDK 画像被放行")
}
}
func TestPlatformPortraitNonpositiveParentIsAbsent(t *testing.T) {
negative := int64(-1)
if parent := (&PortraitPlatformBusiness{Level: intPointer(3), Commerce2: &negative}).ParentID(); parent != nil {
t.Fatal("平台的负数父级占位值不能变成无符号 ID")
}
}
func TestPortraitDecimalRoundingMatchesBigDecimal(t *testing.T) {
card, err := portraitRateCard("1000.00", "1000.01", "较昨日")
if err != nil || card.ChangeRate == nil || *card.ChangeRate != "0.0" {
t.Fatal("极小负环比应舍入为不带负号的零")
}
metrics, err := portraitMetrics(&PortraitStatistics{CurrentPay: "8.005", PreviousPay: "0", Current: PortraitCounts{Register: 2}}, "较昨日")
if err != nil || metrics.PayAmount.Value != "8.01" || metrics.ARPU.Value != "4.01" {
t.Fatal("应先将充值金额保留两位再计算人均充值")
}
}