218 lines
9.2 KiB
Go
218 lines
9.2 KiB
Go
package business
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func TestGameIDSortDoesNotFallBackToRechargeAmount(t *testing.T) {
|
||
rows := []*AnalyticsRow{{AnalyticsKey: AnalyticsKey{ID: 20, SourceDB: "lx"}, Pay: "1.00"}, {AnalyticsKey: AnalyticsKey{ID: 3, SourceDB: "lx"}, Pay: "900.00"}}
|
||
if err := sortAnalytics(rows, GameAnalytics, "gameId", true); err != nil || rows[0].ID != 3 {
|
||
t.Fatal("game ID ascending sort failed")
|
||
}
|
||
if err := sortAnalytics(rows, GameAnalytics, "gameId", false); err != nil || rows[0].ID != 20 {
|
||
t.Fatal("game ID descending sort fell back to recharge amount")
|
||
}
|
||
}
|
||
|
||
type analyticsRepoFake struct {
|
||
data *AnalyticsData
|
||
scope AnalyticsScope
|
||
filter AnalyticsFilter
|
||
fillCalls [][]AnalyticsKey
|
||
fill func([]*AnalyticsRow) error
|
||
}
|
||
|
||
func (r *analyticsRepoFake) FillNames(_ context.Context, _ AnalyticsKind, _ AnalyticsScope, _ AnalyticsFilter, rows []*AnalyticsRow) error {
|
||
keys := make([]AnalyticsKey, len(rows))
|
||
for i, row := range rows {
|
||
keys[i] = row.AnalyticsKey
|
||
}
|
||
r.fillCalls = append(r.fillCalls, keys)
|
||
if r.fill != nil {
|
||
return r.fill(rows)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (r *analyticsRepoFake) Aggregate(_ context.Context, _ AnalyticsKind, s AnalyticsScope, f AnalyticsFilter) (*AnalyticsData, error) {
|
||
r.scope = s
|
||
r.filter = f
|
||
copy := *r.data
|
||
copy.Rows = []*AnalyticsRow{}
|
||
for _, row := range r.data.Rows {
|
||
c := *row
|
||
copy.Rows = append(copy.Rows, &c)
|
||
}
|
||
return ©, nil
|
||
}
|
||
func analyticsFixture() (*AnalyticsUsecase, *analyticsRepoFake) {
|
||
a := &accountRepoFake{accounts: map[uint]*Account{1: {ID: 1}, 7: {ID: 7, Level: 1, IDPath: "0"}}}
|
||
r := &analyticsRepoFake{data: &AnalyticsData{Available: true, Rows: []*AnalyticsRow{{AnalyticsKey: AnalyticsKey{SourceDB: "lx", ID: 1}, Name: "甲", Counts: PortraitCounts{Register: 2, Active: 4, Pay: 2, NewPay: 1}, Pay: "20.00"}, {AnalyticsKey: AnalyticsKey{SourceDB: "hw", ID: 2}, Name: "乙", Counts: PortraitCounts{Register: 3, Active: 6, Pay: 1, NewPay: 1}, Pay: "30.00"}}, GlobalCounts: &PortraitCounts{Register: 4, Active: 8, Pay: 3}, GlobalPay: "45.00"}}
|
||
uc := NewAnalyticsUsecase(r, NewAccountUsecase(a), NewUserPreferenceUsecase(&channelPreferenceFake{UserPreference{IncludeSubPromote: true, IDCardDedup: true}}), nil)
|
||
uc.now = func() time.Time { return time.Date(2026, 11, 4, 9, 0, 0, 0, portraitLocation) }
|
||
return uc, r
|
||
}
|
||
func TestGameAnalyticsSummaryUsesAllRowsAndCorrectDenominators(t *testing.T) {
|
||
uc, _ := analyticsFixture()
|
||
page, err := uc.List(accountContext(1, 888), GameAnalytics, AnalyticsFilter{}, 1, 1)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(page.Rows) != 1 || page.Total != 2 || page.Rows[0].ID != 2 || page.Summary.Counts.Register != 5 || page.Summary.Pay != "50.00" || page.Summary.PayRate != "30.00" || page.Summary.ARPU != "5.00" || page.Summary.ARPPU != "16.67" {
|
||
t.Fatalf("游戏汇总、分页或分母错误: %+v", page)
|
||
}
|
||
if page.Begin != "2026-11-04" || page.End != "2026-11-04" {
|
||
t.Fatal("游戏数据缺省统计日期应为当天")
|
||
}
|
||
}
|
||
func TestChannelAnalyticsSummaryDependsOnViewer(t *testing.T) {
|
||
uc, _ := analyticsFixture()
|
||
page, err := uc.List(accountContext(7, 901), ChannelAnalytics, AnalyticsFilter{}, 1, 10)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if page.Summary.Counts.Register != 5 || page.Summary.Pay != "50.00" {
|
||
t.Fatal("普通商务应汇总可见展示行")
|
||
}
|
||
page, err = uc.List(accountContext(1, 888), ChannelAnalytics, AnalyticsFilter{}, 1, 10)
|
||
if err != nil || page.Summary.Counts.Register != 4 || page.Summary.Pay != "45.00" || page.Summary.PayRate != "37.50" {
|
||
t.Fatal("超管应使用筛选范围内全局去重汇总")
|
||
}
|
||
}
|
||
|
||
func TestChannelAnalyticsAllPagesSummaryAndDefaultDateRange(t *testing.T) {
|
||
uc, repo := analyticsFixture()
|
||
repo.data.EffectiveCount = 3
|
||
for _, pageNum := range []int{1, 2, 8} {
|
||
page, err := uc.List(accountContext(7, 901), ChannelAnalytics, AnalyticsFilter{}, pageNum, 1)
|
||
if err != nil || page.Total != 2 || page.Summary.Counts.Register != 5 || page.Summary.Counts.Active != 10 || page.Summary.Counts.Pay != 3 || page.Summary.Counts.NewPay != 2 || page.Summary.Pay != "50.00" || page.Summary.PayRate != "30.00" || page.EffectiveCount != 3 {
|
||
t.Fatalf("翻页不能改变完整合计: page=%d %+v %v", pageNum, page, err)
|
||
}
|
||
if page.Begin != "" || page.End != "2026-11-04" || repo.filter.StartDate != nil {
|
||
t.Fatalf("渠道统计开始留空不应截断历史数据: %+v", repo.filter)
|
||
}
|
||
}
|
||
for _, sample := range []struct {
|
||
field string
|
||
asc bool
|
||
source string
|
||
}{{"totalPay", true, "lx"}, {"totalPay", false, "hw"}, {"", false, "hw"}} {
|
||
page, err := uc.List(accountContext(7, 901), ChannelAnalytics, AnalyticsFilter{SortField: sample.field, Ascending: sample.asc}, 1, 1)
|
||
if err != nil || page.Rows[0].SourceDB != sample.source {
|
||
t.Fatalf("取消排序应恢复稳定原始顺序: %+v %v", page, err)
|
||
}
|
||
}
|
||
}
|
||
func TestAnalyticsSortAndMissingData(t *testing.T) {
|
||
uc, r := analyticsFixture()
|
||
r.data.Rows[0].Pay = "9007199254740993.00"
|
||
r.data.Rows[1].Pay = "9007199254740992.99"
|
||
p, err := uc.List(accountContext(7, 901), GameAnalytics, AnalyticsFilter{SortField: "totalPay"}, 1, 10)
|
||
if err != nil || p.Rows[0].ID != 1 {
|
||
t.Fatal("金额排序不能经浮点数失真")
|
||
}
|
||
r.data.Available = false
|
||
p, err = uc.List(accountContext(7, 901), ChannelAnalytics, AnalyticsFilter{}, 1, 10)
|
||
if err != nil || p.Available || p.Summary != nil {
|
||
t.Fatal("缺汇总表不能伪造零卡片")
|
||
}
|
||
begin := time.Date(2026, 11, 4, 0, 0, 0, 0, portraitLocation)
|
||
end := begin.AddDate(0, 0, -1)
|
||
if _, err := uc.List(accountContext(1, 888), GameAnalytics, AnalyticsFilter{StartDate: &begin, EndDate: &end}, 1, 10); err != ErrAnalyticsDateOrder {
|
||
t.Fatal("倒置日期应被拒绝")
|
||
}
|
||
}
|
||
|
||
func TestGameAnalyticsCancelSortRestoresDefaultOrder(t *testing.T) {
|
||
uc, repo := analyticsFixture()
|
||
repo.data.Rows = []*AnalyticsRow{
|
||
{AnalyticsKey: AnalyticsKey{SourceDB: "lx", ID: 2}, Pay: "5"},
|
||
{AnalyticsKey: AnalyticsKey{SourceDB: "lx", ID: 1}, Pay: "10"},
|
||
{AnalyticsKey: AnalyticsKey{SourceDB: "lx", ID: 3}, Pay: "20"},
|
||
}
|
||
for _, sample := range []struct {
|
||
field string
|
||
ascending bool
|
||
first int64
|
||
}{{"totalPay", true, 2}, {"totalPay", false, 3}, {"", false, 1}} {
|
||
page, err := uc.List(accountContext(7, 901), GameAnalytics, AnalyticsFilter{SortField: sample.field, Ascending: sample.ascending}, 1, 1)
|
||
if err != nil || page.Rows[0].ID != sample.first || page.Summary.Pay != "35.00" {
|
||
t.Fatalf("切换排序不应改变汇总,取消时恢复默认顺序: %+v %v", page, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestAnalyticsLoadsMetadataOnlyForSelectedPage(t *testing.T) {
|
||
for _, kind := range []AnalyticsKind{ChannelAnalytics, GameAnalytics} {
|
||
t.Run(string(kind), func(t *testing.T) {
|
||
uc, repo := analyticsFixture()
|
||
for _, row := range repo.data.Rows {
|
||
row.Name = ""
|
||
}
|
||
repo.fill = func(rows []*AnalyticsRow) error {
|
||
for _, row := range rows {
|
||
row.Name = "本页资料"
|
||
row.BindingAccount = "本页商务"
|
||
}
|
||
return nil
|
||
}
|
||
page, err := uc.List(accountContext(7, 901), kind, AnalyticsFilter{SortField: "totalPay"}, 1, 1)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(repo.fillCalls) != 1 || len(repo.fillCalls[0]) != 1 || repo.fillCalls[0][0].ID != 2 {
|
||
t.Fatalf("应只读取排序后当前页的资料: %+v", repo.fillCalls)
|
||
}
|
||
if page.Total != 2 || page.Summary.Pay != "50.00" || page.Rows[0].Name != "本页资料" || page.Rows[0].BindingAccount != "本页商务" {
|
||
t.Fatalf("延迟读取影响汇总或展示资料: %+v", page)
|
||
}
|
||
repo.fillCalls = nil
|
||
page, err = uc.List(accountContext(7, 901), kind, AnalyticsFilter{}, 3, 1)
|
||
if err != nil || len(page.Rows) != 0 || len(repo.fillCalls) != 0 || page.Summary.Pay != "50.00" {
|
||
t.Fatal("空页不应读取资料,汇总仍须保留")
|
||
}
|
||
repo.fill = func([]*AnalyticsRow) error { return ErrStorage }
|
||
if _, err := uc.List(accountContext(7, 901), kind, AnalyticsFilter{}, 1, 1); err != ErrStorage {
|
||
t.Fatalf("资料读取错误必须向上传递: %v", err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestAnalyticsLoadsMetadataBeforeNameOrTypeSort(t *testing.T) {
|
||
for _, field := range []string{"promoteName", "promoteType"} {
|
||
t.Run(field, func(t *testing.T) {
|
||
uc, repo := analyticsFixture()
|
||
for _, row := range repo.data.Rows {
|
||
row.Name = ""
|
||
}
|
||
repo.fill = func(rows []*AnalyticsRow) error {
|
||
for _, row := range rows {
|
||
name, kind := "Zulu", 9
|
||
if row.ID == 2 {
|
||
name, kind = "Alpha", 1
|
||
}
|
||
row.Name, row.PromoteType = name, &kind
|
||
}
|
||
return nil
|
||
}
|
||
page, err := uc.List(accountContext(7, 901), ChannelAnalytics, AnalyticsFilter{SortField: field, Ascending: true}, 1, 1)
|
||
if err != nil || len(page.Rows) != 1 || page.Rows[0].ID != 2 || len(repo.fillCalls) != 1 || len(repo.fillCalls[0]) != 2 {
|
||
t.Fatalf("按资料字段排序前必须读取全体资料: page=%+v calls=%+v err=%v", page, repo.fillCalls, err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestAnalyticsDeferredNamesPreserveOfficialAndUnknownLabels(t *testing.T) {
|
||
uc, repo := analyticsFixture()
|
||
repo.data.Rows[0].ID, repo.data.Rows[0].Name = 0, ""
|
||
repo.data.Rows[1].Name = ""
|
||
page, err := uc.List(accountContext(1, 888), ChannelAnalytics, AnalyticsFilter{SortField: "promoteId", Ascending: true}, 1, 10)
|
||
if err != nil || page.Rows[0].Name != "官方渠道" || page.Rows[1].Name != "未知渠道(ID:2)" {
|
||
t.Fatalf("默认渠道名丢失: %+v %v", page, err)
|
||
}
|
||
}
|