package main import ( "bytes" "encoding/json" "io" "net/http" "net/http/httptest" "net/url" "path/filepath" "strconv" "strings" "testing" "time" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "github.com/xuri/excelize/v2" "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" "kra/internal/server/router" businessservice "kra/internal/service/business" ) func TestLedgerHTTPQueriesAndExcelExports(t *testing.T) { db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "ledger.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,'other','other',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,promote_level INTEGER,top_promote_id INTEGER,busier_id INTEGER)", "INSERT INTO tab_promote VALUES(100,'lx','root-channel',1,0,14),(101,'lx','child-channel',2,100,14),(100,'hw','outside',1,0,20)", "CREATE TABLE tab_game(id INTEGER,source_db TEXT,game_name TEXT)", "INSERT INTO tab_game VALUES(1,'lx','当前游戏'),(1,'hw','同号其他游戏')", "CREATE TABLE tab_user(id INTEGER,source_db TEXT,register_time TEXT,fgame_id INTEGER,fgame_name TEXT,register_ip TEXT,device_name TEXT,promote_id INTEGER,puid INTEGER)", "CREATE TABLE tab_spend(id INTEGER,source_db TEXT,user_id INTEGER,pay_order_number TEXT,game_id INTEGER,game_name TEXT,server_id TEXT,server_name TEXT,game_player_name TEXT,pay_amount DECIMAL(20,2),pay_time INTEGER,pay_way INTEGER,pay_status INTEGER,spend_ip TEXT,promote_id INTEGER,promote_account TEXT)", } { if err := db.Exec(sql).Error; err != nil { t.Fatal(err) } } now := time.Now().In(time.FixedZone("CST", 8*3600)) day := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location()) stamp := day.Unix() + 43200 if err := db.Exec("INSERT INTO tab_user VALUES(1,'lx',?,1,'历史游戏','192.0.2.1','设备甲',100,0),(2,'lx',?,1,'历史游戏','','设备乙',101,NULL),(3,'lx',?,1,'历史游戏','','设备丙',101,0),(1,'hw',?,1,'外服游戏','','外服',100,0)", strconv.FormatInt(stamp, 10), strconv.FormatInt(stamp, 10), strconv.FormatInt(stamp, 10), strconv.FormatInt(stamp, 10)).Error; err != nil { t.Fatal(err) } if err := db.Exec("INSERT INTO tab_spend VALUES(101,'lx',1,'000000000001',1,'当前游戏','s1','区服','角色',12.34,?,3,1,'192.0.2.2',100,''),(102,'lx',3,'00002',1,'当前游戏','s1','区服','角色',3.00,?,4,1,'',101,''),(103,'lx',1,'fail',1,'当前游戏','s1','区服','角色',20.00,?,3,0,'',100,''),(104,'lx',1,'history',1,'当前游戏','s1','区服','角色',1000,0,3,1,'',999,'历史渠道'),(105,'lx',5,'official',1,'当前游戏','s1','区服','角色',5,?,3,1,'',0,'')", stamp, stamp, stamp, stamp).Error; err != nil { t.Fatal(err) } provider := businessHTTPData{db} uc := businessbiz.NewLedgerUsecase(businessdata.NewLedgerRepo(provider), businessbiz.NewAccountUsecase(businessdata.NewAccountRepo(provider)), businessbiz.NewUserPreferenceUsecase(businessdata.NewUserPreferenceRepo(provider))) 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() }) router.RegisterLedger(engine.Group(""), handler.NewLedger(businessservice.NewLedgerService(uc))) server := httptest.NewServer(engine) defer server.Close() request := func(user int, method, path, body string) ([]byte, http.Header) { t.Helper() req, _ := http.NewRequest(method, server.URL+path, strings.NewReader(body)) req.Header.Set("X-Test-User", strconv.Itoa(user)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") res, err := server.Client().Do(req) if err != nil { t.Fatal(err) } defer res.Body.Close() data, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } return data, res.Header } call := func(user int, method, path, body string, code int) any { t.Helper() raw, _ := request(user, method, path, body) var result map[string]any if err := json.Unmarshal(raw, &result); err != nil { t.Fatal(err) } if result["code"] != float64(code) { t.Fatalf("%s %s: %s", method, path, raw) } return result["data"] } data := call(7, "GET", "/sw/register/list?sourceDb=lx&sortField=totalPayAmount", "", 0).(map[string]any) rows := data["rows"].([]any) first := rows[0].(map[string]any) if data["total"] != float64(2) || first["userId"] != float64(1) || first["totalPayAmount"] != "1012.34" || first["fgameName"] != "历史游戏" || first["busierName"] != "sdk(leaf)" { t.Fatalf("注册明细口径或字段错误: %+v", data) } games := call(7, "GET", "/sw/serverAnalysis/games?sourceDb=lx&limit=50", "", 0).([]any) if len(games) != 1 || games[0].(map[string]any)["gameName"] != "当前游戏" { t.Fatal("游戏下拉平台范围错误") } filtered := call(7, "GET", "/sw/register/list?sourceDb=lx&gameName="+url.QueryEscape("同号其他游戏"), "", 0).(map[string]any) if filtered["total"] != float64(0) { t.Fatal("游戏筛选跨平台串号") } data = call(7, "GET", "/sw/spend/list?sourceDb=lx&payStatus=0", "", 0).(map[string]any) if data["total"] != float64(1) || data["totalPayAmount"] != "15.34" { t.Fatal("失败列表的合计仍须取对应成功订单") } data = call(1, "GET", "/sw/spend/list?sourceDb=lx", "", 0).(map[string]any) if data["total"] != float64(4) || data["totalPayAmount"] != "20.34" { t.Fatal("超管全平台遗漏官方订单") } data = call(99, "GET", "/sw/register/list?sourceDb=lx", "", 0).(map[string]any) if data["total"] != float64(0) { t.Fatal("其他商务看到了 lx 明细") } 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, "GET", "/sw/register/list?sourceDb=lx", "", 0).(map[string]any) if data["total"] != float64(2) { t.Fatal("身份证开关不应改变明细行") } form := url.Values{"sourceDb": {"lx"}, "beginRegisterTime": {day.Format("2006-01-02")}, "endRegisterTime": {day.Format("2006-01-02")}}.Encode() raw, header := request(7, "POST", "/sw/register/export", form) if header.Get("success") != "true" { t.Fatalf("导出失败: %s", raw) } f, err := excelize.OpenReader(bytes.NewReader(raw)) if err != nil { t.Fatal(err) } xlsRows, err := f.GetRows("注册明细") f.Close() if err != nil || len(xlsRows) != 3 || xlsRows[0][7] != "累计充值" { t.Fatal("注册 XLSX 字段或行数错误") } raw, header = request(7, "POST", "/sw/spend/export", "sourceDb=lx&payStatus=1&sortField=payAmount") if header.Get("success") != "true" { t.Fatalf("充值导出失败: %s", raw) } f, err = excelize.OpenReader(bytes.NewReader(raw)) if err != nil { t.Fatal(err) } order, _ := f.GetCellValue("充值明细", "E2") f.Close() if order != "000000000001" { t.Fatal("订单号导出丢失前导零") } call(7, "GET", "/sw/register/list?beginRegisterTime=bad&endRegisterTime=2026-11-04", "", 7) if err := db.Exec("WITH RECURSIVE seq(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n<10001) INSERT INTO tab_user(id,source_db,register_time,fgame_id,fgame_name,promote_id,puid) SELECT 1000+n,'lx',?,1,'游戏',100,0 FROM seq", strconv.FormatInt(stamp, 10)).Error; err != nil { t.Fatal(err) } call(7, "POST", "/sw/register/export", form, 7) if err := db.Exec("DROP TABLE tab_promote").Error; err != nil { t.Fatal(err) } data = call(7, "GET", "/sw/spend/list", "", 0).(map[string]any) if data["available"] != false || data["totalPayAmount"] != nil { t.Fatal("缺表不能返回伪造零合计") } call(7, "POST", "/sw/spend/export", "sourceDb=lx", 7) }