feat: 迁移渠道画像累计指标和玩家付费分层

This commit is contained in:
Yvan
2026-11-03 23:50:05 +08:00
parent 0de822f121
commit f5b7ce21b9
38 changed files with 1424 additions and 37 deletions
+195
View File
@@ -0,0 +1,195 @@
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("缺数据时应保留已授权基本资料并标明未知")
}
}
+6 -1
View File
@@ -82,15 +82,19 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg
auditRecorderUsecase := system2.NewAuditRecorderUsecase(auditRecordRepo)
auditRecorder := system3.NewAuditRecorder(auditRecorderUsecase)
registry := taskRegistry()
channelPortraitRepo := business.NewChannelPortraitRepo(dataData)
channelRepo := business.NewChannelRepo(dataData)
businessPortraitRepo := business.NewBusinessPortraitRepo(dataData, dataData)
accountRepo := business.NewAccountRepo(dataData)
accountUsecase := business2.NewAccountUsecase(accountRepo)
userPreferenceRepo := business.NewUserPreferenceRepo(dataData)
userPreferenceUsecase := business2.NewUserPreferenceUsecase(userPreferenceRepo)
channelPortraitUsecase := business2.NewChannelPortraitUsecase(channelPortraitRepo, channelRepo, businessPortraitRepo, accountUsecase, userPreferenceUsecase)
channelPortraitService := business3.NewChannelPortraitService(channelPortraitUsecase)
channelPortrait := handler.NewChannelPortrait(channelPortraitService)
businessPortraitUsecase := business2.NewBusinessPortraitUsecase(businessPortraitRepo, accountUsecase, userPreferenceUsecase)
businessPortraitService := business3.NewBusinessPortraitService(businessPortraitUsecase)
businessPortrait := handler.NewBusinessPortrait(businessPortraitService)
channelRepo := business.NewChannelRepo(dataData)
channelWriteRepo := business.NewChannelWriteRepo(dataData)
platformPasswordEncoder := gameplatform.NewPasswordEncoder()
channelUsecase := business2.NewChannelUsecase(channelRepo, channelWriteRepo, accountUsecase, userPreferenceUsecase, platformPasswordEncoder)
@@ -223,6 +227,7 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg
integrationConfigService := integration2.NewIntegrationConfigService(integrationConfigUsecase)
integrationConfig := handler.NewIntegrationConfig(integrationConfigService)
set := &handler.Set{
ChannelPortrait: channelPortrait,
BusinessPortrait: businessPortrait,
Channel: channel,
Activation: activation,