package business import ( "context" "errors" "kra/pkg/task" "strings" "testing" "time" ) func reportPtr(n int64) *int64 { return &n } func TestReportBuildIdentityRulesAndAnonymousLogins(t *testing.T) { input := &ReportInput{Users: map[ReportUserKey]ReportUser{}, Games: map[AnalyticsKey]string{{SourceDB: "lx", ID: 1}: "游戏"}} for _, u := range []ReportUser{{ReportUserKey: ReportUserKey{"lx", 1}, ParentID: reportPtr(0), PromoteID: reportPtr(10), IDCard: " aaX "}, {ReportUserKey: ReportUserKey{"hw", 1}, ParentID: reportPtr(0), PromoteID: reportPtr(20), IDCard: "b"}, {ReportUserKey: ReportUserKey{"lx", 2}, ParentID: reportPtr(0), PromoteID: reportPtr(10), IDCard: "aax"}, {ReportUserKey: ReportUserKey{"lx", 3}, ParentID: reportPtr(1), PromoteID: reportPtr(10), IDCard: "sub"}, {ReportUserKey: ReportUserKey{"lx", 4}, PromoteID: reportPtr(10)}, {ReportUserKey: ReportUserKey{"lx", 5}, ParentID: reportPtr(0), PromoteID: reportPtr(10)}} { input.Users[u.ReportUserKey] = u } event := func(id int64) ReportEvent { return ReportEvent{SourceDB: "lx", UserID: reportPtr(id), PromoteID: reportPtr(10), GameID: reportPtr(1)} } input.Registers = []ReportEvent{event(1), event(2), event(5)} missing := event(2) missing.SourceDB = "" missing.PromoteID = nil ambiguous := event(1) ambiguous.SourceDB = "" input.Actives = []ReportEvent{event(1), missing, ambiguous, event(3), event(4)} for _, id := range []int64{1, 3, 99} { p := event(id) p.Amount = "10" input.Payments = append(input.Payments, p) } noUser := event(1) noUser.UserID = nil noUser.Amount = "1" input.Payments = append(input.Payments, noUser) rows, err := compileReport(DailyReport, input) if err != nil || len(rows) != 2 { t.Fatalf("汇总分组错误: %+v %v", rows, err) } for _, row := range rows { if row.Pay != "31.00" || row.GameName != "游戏" || row.Counts.NewPay != 1 { t.Fatalf("金额、名称或新增付费错误: %+v", row) } if row.Mode == 0 && (row.Counts.Register != 3 || row.Counts.Active != 2 || row.Counts.Pay != 3) { t.Fatalf("账号口径未保留主账号登录/原始付费账号: %+v", row) } if row.Mode == 1 && (row.Counts.Register != 1 || row.Counts.Active != 1 || row.Counts.Pay != 2) { t.Fatalf("身份证口径、空证件或末尾 X 归一错误: %+v", row) } } input.Registers = []ReportEvent{event(2)} input.Payments = []ReportEvent{event(1)} input.Payments[0].Amount = "10" rows, err = compileReport(DailyReport, input) if err != nil { t.Fatal(err) } for _, row := range rows { if row.Counts.NewPay != 0 { t.Fatal("新增付费必须先取原始账号交集,不能把相同证件的其他付费账号算入") } } server := event(3) server.ServerNum = " 001 " input.Registers = []ReportEvent{server} input.Actives = []ReportEvent{server} input.Payments = []ReportEvent{server} input.Payments[0].Amount = "20" rows, err = compileReport(ServerReport, input) if err != nil || len(rows) != 2 { t.Fatal("区服汇总失败") } for _, row := range rows { if row.ServerNum != "001" || row.Counts.Register != 1 || row.Counts.Active != 1 || row.Counts.Pay != 1 || len(row.Keys[4]) != 0 { t.Fatal("区服创角/活跃不能套用主账号限制,也不能生成新增付费指标") } } } type reportBuildFake struct{ days []string } type failingReportCommit struct{} func (failingReportCommit) Rebuild(_ context.Context, _ ReportBuildKind, _ time.Time, compile func(*ReportInput) ([]*ReportBuiltRow, error)) error { _, err := compile(&ReportInput{Registers: []ReportEvent{{SourceDB: "lx", UserID: reportPtr(1), PromoteID: reportPtr(1), GameID: reportPtr(1)}}}) if err != nil { return err } return errors.New("commit failed") } func TestReportBuildDetailsDoNotClaimRolledBackRows(t *testing.T) { ctx, output := task.WithOutput(context.Background()) uc := NewReportBuildUsecase(failingReportCommit{}) if err := uc.Run(ctx, DailyReport, "backfill", "2026-11-03", "2026-11-03"); err == nil { t.Fatal("commit error was lost") } text := output.String() if !strings.Contains(text, "2026-11-03 未提交") || !strings.Contains(text, "已提交日期=0;汇总行=0;身份键=0") { t.Fatalf("rollback was reported as success: %s", text) } } func (r *reportBuildFake) Rebuild(_ context.Context, _ ReportBuildKind, day time.Time, _ func(*ReportInput) ([]*ReportBuiltRow, error)) error { r.days = append(r.days, day.Format("2006-01-02")) return nil } func TestReportBuildBackfillRangeAndCancellation(t *testing.T) { r := &reportBuildFake{} uc := NewReportBuildUsecase(r) uc.now = func() time.Time { return time.Date(2026, 11, 4, 9, 0, 0, 0, portraitLocation) } pauses := 0 uc.pause = func(context.Context) error { pauses++; return nil } if err := uc.Run(context.Background(), DailyReport, "backfill", "2026-10-31", "2026-11-02"); err != nil || len(r.days) != 3 || pauses != 2 || r.days[2] != "2026-11-02" { t.Fatal("回跑日期应闭区间且仅日间暂停") } if err := uc.Run(context.Background(), DailyReport, "backfill", "bad", ""); err != ErrAnalyticsDate { t.Fatal("非法日期未拒绝") } if err := uc.Run(context.Background(), DailyReport, "backfill", "2026-11-05", "2026-11-04"); err != ErrAnalyticsDateOrder { t.Fatal("倒置区间未拒绝") } ctx, cancel := context.WithCancel(context.Background()) cancel() count := len(r.days) if err := uc.Run(ctx, ServerReport, "today", "", ""); err != context.Canceled || len(r.days) != count { t.Fatal("取消后仍重建报表") } }