package business import ( "context" "strings" "testing" "kra/internal/biz/business" ) func TestMySQLApprovedGameIndexSupportsExistenceLookup(t *testing.T) { db := openRehearsalMySQL(t) for _, sql := range []string{ "CREATE TABLE tab_game(id INT,source_db VARCHAR(10),game_name VARCHAR(64),PRIMARY KEY(id,source_db),KEY(game_name)) ENGINE=InnoDB", "CREATE TABLE tab_promote(id INT,source_db VARCHAR(10),PRIMARY KEY(id,source_db)) ENGINE=InnoDB", "CREATE TABLE tab_promote_apply(id INT PRIMARY KEY,source_db VARCHAR(10),game_id INT,promote_id INT,status INT,payload VARCHAR(500)) ENGINE=InnoDB", "INSERT INTO tab_game VALUES(1,'lx','游戏甲'),(1,'hw','无申请同号游戏')", "INSERT INTO tab_promote VALUES(100,'lx')", "INSERT INTO tab_promote_apply VALUES(1,'lx',1,100,1,'keep'),(2,'lx',1,100,0,'pending')", } { if err := db.Exec(sql).Error; err != nil { t.Fatal(err) } } for attempt := 0; attempt < 2; attempt++ { if err := migrateApprovedGameIndexes(db); err != nil { t.Fatal(err) } } var columns []string if err := db.Raw("SELECT column_name FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='tab_promote_apply' AND index_name='idx_sw_apply_game_scope' ORDER BY seq_in_index").Scan(&columns).Error; err != nil { t.Fatal(err) } if strings.Join(columns, ",") != "game_id,source_db,status,promote_id" { t.Fatal("申请查询需要覆盖完整游戏、平台、状态和渠道条件") } rows, err := NewLedgerRepo(testData{db}).Games(context.Background(), business.UserScope{All: true}, nil, "", 50) if err != nil || len(rows) != 1 || rows[0].SourceDB != "lx" { t.Fatalf("索引不能改变游戏可见范围: %+v %v", rows, err) } var payload string if err := db.Raw("SELECT payload FROM tab_promote_apply WHERE id=1").Scan(&payload).Error; err != nil || payload != "keep" { t.Fatal("索引迁移不能修改申请数据") } } func TestMySQLBusinessQueryIndexesAreAdditiveAndIdempotent(t *testing.T) { db := openRehearsalMySQL(t) definitions := map[string]string{ "tab_promote": "id BIGINT NOT NULL,source_db VARCHAR(10) NOT NULL,create_time BIGINT,PRIMARY KEY(id,source_db)", "tab_spend": "id BIGINT NOT NULL,source_db VARCHAR(10) NOT NULL,promote_id BIGINT,pay_status INT,pay_time BIGINT,pay_amount DECIMAL(20,2),PRIMARY KEY(id,source_db)", "tab_sw_report_day": "dedup_mode INT,stat_date DATE,source_db VARCHAR(10),promote_id BIGINT,game_id BIGINT,total_pay DECIMAL(20,2)", "tab_sw_report_day_key": "dedup_mode INT,stat_date DATE,source_db VARCHAR(10),promote_id BIGINT,game_id BIGINT,metric INT,identity_key VARCHAR(64)", "tab_sw_report_server_day": "game_name VARCHAR(64),dedup_mode INT,stat_date DATE,source_db VARCHAR(10),game_id BIGINT,server_num VARCHAR(32),total_pay DECIMAL(20,2)", "tab_sw_report_server_day_key": "game_name VARCHAR(64),dedup_mode INT,stat_date DATE,source_db VARCHAR(10),game_id BIGINT,server_num VARCHAR(32),metric INT,identity_key VARCHAR(64)", } for table, columns := range definitions { if err := db.Exec("CREATE TABLE `" + table + "` (" + columns + ") ENGINE=InnoDB").Error; err != nil { t.Fatal(err) } } if err := db.Exec("INSERT INTO tab_spend VALUES(77,'lx',55,1,123456,12.30)").Error; err != nil { t.Fatal(err) } for attempt := 0; attempt < 2; attempt++ { if err := migrateBusinessQueryIndexes(db); err != nil { t.Fatal(err) } } for _, index := range businessQueryIndexes { var columns []string if err := db.Raw("SELECT column_name FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name=? AND index_name=? ORDER BY seq_in_index", index.table, index.name).Scan(&columns).Error; err != nil { t.Fatal(err) } if strings.Join(columns, ",") != strings.Join(index.columns, ",") { t.Fatalf("index %s has unexpected columns: %v", index.name, columns) } } var amount string if err := db.Raw("SELECT pay_amount FROM tab_spend WHERE id=77 AND source_db='lx'").Scan(&amount).Error; err != nil || amount != "12.30" { t.Fatalf("index migration changed business data: %s, %v", amount, err) } if err := db.Exec("ALTER TABLE tab_spend DROP INDEX idx_sw_spend_time_summary, ADD INDEX idx_sw_spend_time_summary(pay_time)").Error; err != nil { t.Fatal(err) } if err := migrateBusinessQueryIndexes(db); err == nil || !strings.Contains(err.Error(), "idx_sw_spend_time_summary") { t.Fatalf("must reject a conflicting existing index: %v", err) } } func TestMySQLReportBuildIndexesCoverInputsAndPreserveIdentities(t *testing.T) { db := openRehearsalMySQL(t) for _, sql := range []string{ "CREATE TABLE tab_user(id INT,source_db VARCHAR(10),puid INT,promote_id INT,idcard VARCHAR(20),payload VARCHAR(1000),PRIMARY KEY(id,source_db)) ENGINE=InnoDB", "CREATE TABLE tab_user_play_info(id INT PRIMARY KEY,play_time INT,user_id INT,source_db VARCHAR(10),promote_id INT,game_id INT,server_id VARCHAR(30),payload VARCHAR(1000),KEY(play_time)) ENGINE=InnoDB", } { if err := db.Exec(sql).Error; err != nil { t.Fatal(err) } } users, plays := []map[string]any{}, []map[string]any{} known := map[business.ReportUserKey]bool{} ids := []int64{} for id := int64(1); id <= 4000; id++ { for _, source := range []string{"lx", "hw"} { users = append(users, map[string]any{"id": id, "source_db": source, "puid": 0, "promote_id": id, "idcard": source, "payload": strings.Repeat("x", 300)}) } plays = append(plays, map[string]any{"id": id, "play_time": id, "user_id": id, "source_db": "lx", "promote_id": id, "game_id": 1, "server_id": " 001 ", "payload": strings.Repeat("x", 300)}) if id <= 501 { known[business.ReportUserKey{SourceDB: "lx", ID: id}] = true ids = append(ids, id) } } for table, rows := range map[string][]map[string]any{"tab_user": users, "tab_user_play_info": plays} { if err := db.Table(table).CreateInBatches(rows, 200).Error; err != nil { t.Fatal(err) } } for attempt := 0; attempt < 2; attempt++ { if err := migrateReportBuildQueryIndexes(db); err != nil { t.Fatal(err) } } loaded, err := loadReportUsers(db, known, map[int64]bool{501: true}) if err != nil || len(loaded) != 502 { t.Fatalf("分批读取或跨平台歧义匹配错误: rows=%d error=%v", len(loaded), err) } for key, user := range loaded { if user.IDCard != key.SourceDB || user.PromoteID == nil || *user.PromoteID != key.ID { t.Fatalf("迁移改变用户归属或身份: %+v", key) } } for _, query := range []struct { sql, index string args []any }{ {"SELECT id,source_db,puid,promote_id,idcard FROM tab_user WHERE source_db=? AND id IN ?", "idx_sw_user_report_identity", []any{"lx", ids}}, {"SELECT id,source_db,puid,promote_id,idcard FROM tab_user WHERE id IN ?", "idx_sw_user_report_identity", []any{ids}}, {"SELECT user_id,source_db,promote_id,game_id,server_id FROM tab_user_play_info WHERE server_id<>'' AND play_time BETWEEN ? AND ?", "idx_sw_play_report_active", []any{1, 501}}, } { var plan []struct{ Key, Type, Extra string } if err := db.Raw("EXPLAIN "+query.sql, query.args...).Scan(&plan).Error; err != nil { t.Fatal(err) } if len(plan) != 1 || plan[0].Key != query.index || plan[0].Type != "range" || !strings.Contains(plan[0].Extra, "Using index") { t.Fatalf("任务查询未使用覆盖范围索引: %+v", plan) } } if err := db.Exec("ALTER TABLE tab_user DROP INDEX idx_sw_user_report_identity, ADD INDEX idx_sw_user_report_identity(id)").Error; err != nil { t.Fatal(err) } if err := migrateReportBuildQueryIndexes(db); err == nil || !strings.Contains(err.Error(), "idx_sw_user_report_identity") { t.Fatalf("同名但列不符的索引必须拒绝: %v", err) } }