package main import ( "encoding/json" "net/http" "net/http/httptest" "path/filepath" "strconv" "testing" "time" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "gorm.io/gorm" businessbiz "kra/internal/biz/business" "kra/internal/biz/system" businessdata "kra/internal/data/business" systemdata "kra/internal/data/system" "kra/internal/server/handler" businessservice "kra/internal/service/business" ) func TestChannelPortraitHTTPTotalTiersAndScope(t *testing.T) { db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "channel-portrait.db")), &gorm.Config{}) if err != nil { t.Fatal(err) } pool, _ := db.DB() t.Cleanup(func() { _ = pool.Close() }) for _, step := range append(systemdata.Migrations(), businessdata.Migrations()...) { if err := step.Migrate(db); err != nil { t.Fatal(err) } } for _, sql := range []string{ "INSERT INTO sys_users(id,uuid,username,level,parent_id,id_path,enable) VALUES(1,'root','root',0,0,'0',1),(7,'owner','owner',1,0,'0',1),(9,'leaf','leaf',3,7,'0,7',1),(99,'outside','outside',1,0,'0',1)", "INSERT INTO sys_user_bind_platform(user_id,source_db,platform_busier_id,platform_busier_account,status) VALUES(9,'lx',14,'sdk','0'),(99,'hw',20,'other','0')", "CREATE TABLE tab_promote(id INTEGER,source_db TEXT,account TEXT,nickname TEXT,parent_name TEXT,real_name TEXT,promote_level INTEGER,register_type INTEGER,status INTEGER,lock_status INTEGER,parent_id INTEGER,top_promote_id INTEGER,busier_id INTEGER,create_time INTEGER,last_login_time INTEGER)", "INSERT INTO tab_promote VALUES(100,'lx','root-account','主渠道','','负责人',1,1,-3,0,0,0,14,1000,2000),(101,'lx','child-account','子渠道','旧上级名称','',2,0,1,0,100,100,14,1000,0),(100,'hw','outside-channel','','','',1,0,1,0,0,0,20,1000,0)", "CREATE TABLE tab_promote_apply(source_db TEXT,promote_id INTEGER,game_id INTEGER,status INTEGER)", "INSERT INTO tab_promote_apply VALUES('lx',100,1,1),('lx',100,1,1),('lx',100,2,1)", "CREATE TABLE tab_game(source_db TEXT,id INTEGER,game_name TEXT)", "INSERT INTO tab_game VALUES('lx',1,'游戏甲'),('lx',2,'游戏乙')", "CREATE TABLE tab_sw_report_day(source_db TEXT,promote_id INTEGER,game_id INTEGER,dedup_mode INTEGER,stat_date DATE,total_pay DECIMAL(20,2))", "CREATE TABLE tab_sw_report_day_key(source_db TEXT,promote_id INTEGER,game_id INTEGER,dedup_mode INTEGER,stat_date DATE,metric INTEGER,identity_key TEXT)", "CREATE TABLE tab_user(source_db TEXT,id INTEGER,promote_id INTEGER,puid INTEGER,idcard TEXT,register_time INTEGER)", "CREATE TABLE tab_spend(source_db TEXT,promote_id INTEGER,user_id INTEGER,pay_status INTEGER,pay_time INTEGER,pay_amount DECIMAL(20,2))", } { if err := db.Exec(sql).Error; err != nil { t.Fatal(err) } } now := time.Now().In(time.FixedZone("CST", 8*3600)) today := now.Format("2006-01-02") day20 := now.AddDate(0, 0, -20).Format("2006-01-02") day30 := now.AddDate(0, 0, -30).Format("2006-01-02") previous := now.AddDate(0, 0, -7).Format("2006-01-02") start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix() for _, row := range []struct { promote, mode int date, amount string }{{100, 0, day30, "20.00"}, {100, 1, day20, "777.00"}, {100, 0, day20, "10.00"}, {100, 0, previous, "6.00"}, {100, 0, today, "12.00"}, {101, 0, today, "3.00"}} { if err := db.Exec("INSERT INTO tab_sw_report_day VALUES('lx',?,1,?,?,?)", row.promote, row.mode, row.date, row.amount).Error; err != nil { t.Fatal(err) } } cards := []string{"A-X", " A-x ", "B", "C", "D"} for i, card := range cards { promote := 100 if i == 1 { promote = 101 } if err := db.Exec("INSERT INTO tab_user VALUES('lx',?,?,0,?,?)", i+1, promote, card, start+3600).Error; err != nil { t.Fatal(err) } for _, mode := range []int{0, 1} { key := strconv.Itoa(i + 1) if mode == 1 { key = card if i < 2 { key = "A-x" } } for _, metric := range []int{1, 2} { if err := db.Exec("INSERT INTO tab_sw_report_day_key VALUES('lx',?,1,?,?,?,?)", promote, mode, today, metric, key).Error; err != nil { t.Fatal(err) } } } } if err := db.Exec("INSERT INTO tab_sw_report_day_key VALUES('lx',100,1,1,?,3,'A-x')", today).Error; err != nil { t.Fatal(err) } for _, row := range []struct { promote, user int timestamp int64 amount string }{{100, 1, 1000, "9999.50"}, {101, 2, 2000000000, "0.50"}, {100, 3, start + 3600, "1000.00"}, {100, 4, start + 3600, "0.01"}, {100, 5, start + 3600, "0.00"}} { if err := db.Exec("INSERT INTO tab_spend VALUES('lx',?,?,1,?,?)", row.promote, row.user, row.timestamp, row.amount).Error; err != nil { t.Fatal(err) } } provider := businessHTTPData{db} channels := businessdata.NewChannelRepo(provider) accounts := businessbiz.NewAccountUsecase(businessdata.NewAccountRepo(provider)) prefs := businessbiz.NewUserPreferenceUsecase(businessdata.NewUserPreferenceRepo(provider)) uc := businessbiz.NewChannelPortraitUsecase(businessdata.NewChannelPortraitRepo(provider), channels, businessdata.NewBusinessPortraitRepo(provider, channelHTTPDatabases{}), accounts, prefs) h := handler.NewChannelPortrait(businessservice.NewChannelPortraitService(uc)) engine := gin.New() engine.Use(func(c *gin.Context) { id, _ := strconv.ParseUint(c.GetHeader("X-Test-User"), 10, 64) aid := uint(901) if id == 1 { aid = 888 } c.Request = c.Request.WithContext(system.NewActorContext(c.Request.Context(), system.Actor{UserID: uint(id), AuthorityID: aid})) c.Next() }) engine.GET("/sw/promote/portrait", h.Portrait) server := httptest.NewServer(engine) defer server.Close() call := func(user int, query string, expected int) map[string]any { t.Helper() req, _ := http.NewRequest("GET", server.URL+"/sw/promote/portrait?"+query, nil) req.Header.Set("X-Test-User", strconv.Itoa(user)) res, err := server.Client().Do(req) if err != nil { t.Fatal(err) } defer res.Body.Close() var body map[string]any if err := json.NewDecoder(res.Body).Decode(&body); err != nil { t.Fatal(err) } if body["code"] != float64(expected) { t.Fatalf("画像 %s 用户%d响应错误: %+v", query, user, body) } return body["data"].(map[string]any) } data := call(7, "sourceDb=lx&promoteId=100&period=today", 0) basic := data["basic"].(map[string]any) metrics := data["metrics"].(map[string]any) if basic["statusLabel"] != "商务新增" || basic["gameCount"] != float64(2) || basic["parentPromoteId"] != nil || basic["lastLoginTime"] == nil || basic["busierDisplayName"] != "sdk(leaf)" { t.Fatalf("基本信息错误: %+v", basic) } if metrics["register"].(map[string]any)["value"] != "4" || metrics["active"].(map[string]any)["value"] != "4" || metrics["payRate"].(map[string]any)["value"] != "25.0" || metrics["payAmount"].(map[string]any)["value"] != "15.00" { t.Fatalf("本期指标错误: %+v", metrics) } if _, ok := metrics["arpu"]; ok { t.Fatal("渠道第四项是活跃人数,不是商务 ARPU") } tiers := data["payTiers"].([]any) for _, raw := range tiers { tier := raw.(map[string]any) if tier["count"] != float64(1) || tier["ratio"] != "25.0" { t.Fatalf("累计付费分层错误: %+v", tiers) } } if data["gameDistribution"].([]any)[0].(map[string]any)["payAmount"] != "31.00" { t.Fatal("游戏分布应始终累计,不随顶部今日切换") } data = call(7, "sourceDb=lx&promoteId=100&period=total", 0) metrics = data["metrics"].(map[string]any) if metrics["payAmount"].(map[string]any)["value"] != "31.00" || len(data["registerTrend"].([]any)) != 21 { t.Fatal("累计模式没有使用当前去重口径最早日期") } for _, raw := range metrics { metric := raw.(map[string]any) if metric["compareValue"] != nil || metric["changeRate"] != nil || metric["changeDiff"] != nil || metric["compareLabel"] != nil { t.Fatal("累计模式不应有环比字段值") } } if err := db.Exec("INSERT INTO sys_user_preference(user_id,include_sub_promote,idcard_dedup) VALUES(7,1,0)").Error; err != nil { t.Fatal(err) } data = call(7, "sourceDb=lx&promoteId=100&period=total", 0) tiers = data["payTiers"].([]any) if data["metrics"].(map[string]any)["payAmount"].(map[string]any)["value"] != "51.00" || tiers[0].(map[string]any)["count"] != float64(0) || tiers[1].(map[string]any)["count"] != float64(2) { t.Fatal("模式0应按原始付款账号分类,且使用自己的累计起点") } data = call(7, "sourceDb=lx&promoteId=101&period=last7", 0) basic = data["basic"].(map[string]any) if basic["parentPromoteId"] != float64(100) || basic["parentPromoteName"] != "root-account" || basic["lastLoginTime"] != nil { t.Fatal("上级应读同平台当前账号,不能沿用旧快照名") } call(99, "sourceDb=lx&promoteId=100", 7) call(7, "sourceDb=hw&promoteId=100", 7) if err := db.Exec("DROP TABLE tab_sw_report_day_key").Error; err != nil { t.Fatal(err) } data = call(7, "sourceDb=lx&promoteId=100", 0) if data["available"] != false || data["metrics"] != nil || data["basic"].(map[string]any)["promoteId"] != float64(100) { t.Fatal("缺数据时应保留已授权基本资料并标明未知") } }