Files
kra-new/internal/data/business/channel_test.go
T

221 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package business
import (
"context"
"math/big"
"strconv"
"testing"
"time"
"gorm.io/gorm"
"kra/internal/biz/business"
)
func prepareChannels(t *testing.T) (*gorm.DB, business.ChannelRepo) {
t.Helper()
db := openTestDB(t)
if err := db.AutoMigrate(&accountPO{}); err != nil {
t.Fatal(err)
}
for _, sql := range []string{
"INSERT INTO sys_users(id,username,level,parent_id,id_path) VALUES(1,'root',0,0,'0'),(7,'parent',1,0,'0'),(8,'branch',2,7,'0,7'),(9,'leaf',3,8,'0,7,8'),(10,'sibling',3,7,'0,7'),(99,'outsider',1,0,'0')",
"INSERT INTO sys_user_bind_platform(user_id,source_db,platform_busier_id,platform_busier_account,status) VALUES(9,'lx',14,'lx-business','0'),(8,'hw',14,'hw-business','0'),(10,'lx',16,'sibling-business','0'),(99,'yq',14,'other-business','0'),(9,'yq',14,'pending','3')",
"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 DEFAULT 0,status INTEGER DEFAULT 1,lock_status INTEGER DEFAULT 0,parent_id INTEGER,top_promote_id INTEGER,busier_id INTEGER,create_time INTEGER DEFAULT 1000,PRIMARY KEY(source_db,id))",
"INSERT INTO tab_promote(id,source_db,account,promote_level,parent_id,top_promote_id,busier_id) VALUES(100,'lx','root-channel',1,0,0,14),(101,'lx','child-channel',2,100,100,14),(102,'lx','grandchild',3,101,100,14),(103,'lx','missing-top',3,101,0,14),(104,'lx','other-branch',2,999,999,16),(100,'hw','same-id',1,0,0,14),(100,'yq','outside',1,0,0,14)",
"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),('lx',100,3,0),('lx',101,3,1),('hw',100,99,1)",
"CREATE TABLE tab_user(source_db TEXT,id INTEGER,promote_id INTEGER,puid INTEGER,idcard TEXT,PRIMARY KEY(source_db,id))",
"INSERT INTO tab_user VALUES('lx',1,100,0,' abcX '),('lx',2,101,NULL,'abcx'),('lx',3,102,0,''),('lx',4,101,2,'subaccount'),('lx',5,103,0,'thirdX'),('hw',1,100,0,'different')",
"CREATE TABLE tab_spend(source_db TEXT,promote_id INTEGER,pay_status INTEGER,pay_amount DECIMAL(20,2))",
"INSERT INTO tab_spend VALUES('lx',100,1,10.10),('lx',101,1,20.20),('lx',102,1,30.30),('lx',103,1,40.40),('hw',100,1,99.90),('lx',100,0,999)",
"CREATE TABLE tab_game(source_db TEXT,id INTEGER,game_name TEXT)",
"INSERT INTO tab_game VALUES('lx',1,'游戏甲'),('lx',2,'游戏乙'),('hw',1,'同号其他游戏')",
"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))",
"INSERT INTO tab_sw_report_day VALUES('lx',100,1,0,'2026-10-01',1.10),('lx',100,1,0,'2026-11-01',10.10),('lx',101,1,0,'2026-11-02',20.20),('lx',102,2,0,'2026-11-02',30.30),('lx',103,1,0,'2026-11-02',999),('lx',100,1,0,'2026-11-04',999),('lx',100,1,1,'2026-11-01',9999),('hw',100,1,0,'2026-11-01',999)",
"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)",
"INSERT INTO tab_sw_report_day_key VALUES('lx',100,1,1,'2026-11-01',1,'A'),('lx',101,1,1,'2026-11-02',1,'A'),('lx',102,2,1,'2026-11-02',1,'B'),('lx',102,2,1,'2026-11-02',2,'C'),('lx',103,1,1,'2026-11-02',1,'N'),('lx',100,1,1,'2026-11-04',1,'F'),('hw',100,1,1,'2026-11-01',1,'Z'),('lx',100,1,0,'2026-10-01',1,'old'),('lx',100,1,0,'2026-11-01',1,'u1'),('lx',101,1,0,'2026-11-02',1,'u2')",
} {
if err := db.Exec(sql).Error; err != nil {
t.Fatal(err)
}
}
return db, NewChannelRepo(testData{db})
}
func TestChannelMetadataFiltersSortingAndFullStatusCounts(t *testing.T) {
db, repo := prepareChannels(t)
day := time.Date(2026, 11, 1, 0, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600))
for _, sql := range []string{"ALTER TABLE tab_promote ADD COLUMN mobile_phone TEXT", "ALTER TABLE tab_promote ADD COLUMN settlement_day_period INTEGER", "ALTER TABLE tab_promote ADD COLUMN last_login_time INTEGER"} {
if err := db.Exec(sql).Error; err != nil {
t.Fatal(err)
}
}
for i, status := range []int{0, -1, -2} {
if err := db.Exec("UPDATE tab_promote SET create_time=?,last_login_time=?,mobile_phone=?,settlement_day_period=7,lock_status=? WHERE source_db='lx' AND id=?", day.Unix()+int64(i+1)*100, day.Unix()+1000, "1381234001"+strconv.Itoa(i), status, 100+i).Error; err != nil {
t.Fatal(err)
}
}
scope := business.UserScope{SelfID: 9, OnlySelf: true}
seven := 7
filter := business.ChannelFilter{Phone: "4001", SettlementDays: &seven, CreatedBegin: &day, CreatedEnd: &day, LoginBegin: &day, LoginEnd: &day, SortField: "createdAt", Ascending: true}
page, err := repo.List(context.Background(), scope, filter, business.ListLimit(1))
if err != nil || page.Total != 3 || len(page.Rows) != 1 || page.Rows[0].ID != 100 || page.Rows[0].Phone != "13812340010" || page.Rows[0].SettlementDays == nil || *page.Rows[0].SettlementDays != 7 || page.Rows[0].LastLoginUnix == nil {
t.Fatalf("筛选、排序或元数据遗漏: %+v %v", page, err)
}
if page.StatusCounts.Normal != 1 || page.StatusCounts.Locked != 1 || page.StatusCounts.Terminated != 1 {
t.Fatalf("状态汇总必须覆盖全量三条而非当前页: %+v", page.StatusCounts)
}
filter.Ascending = false
page, err = repo.List(context.Background(), scope, filter, business.ListLimit(1))
if err != nil || page.Rows[0].ID != 102 {
t.Fatalf("降序必须作用于所有页: %+v %v", page, err)
}
locked := -1
filter.LockStatus = &locked
page, err = repo.List(context.Background(), scope, filter)
if err != nil || page.Total != 1 || page.StatusCounts.Normal != 0 || page.StatusCounts.Locked != 1 || page.StatusCounts.Terminated != 0 {
t.Fatalf("筛选变化应同步刷新汇总: %+v %v", page, err)
}
}
func channelTestScope() business.UserScope {
return business.UserScope{SelfID: 8, IncludeSelf: true, DescendantPath: "0,7,8"}
}
func TestChannelKeywordMatchesNameAndContactWithinScope(t *testing.T) {
db, repo := prepareChannels(t)
if err := db.Exec("UPDATE tab_promote SET nickname='同名公会', real_name='负责人甲' WHERE id=100").Error; err != nil {
t.Fatal(err)
}
for _, keyword := range []string{"公会", "负责人甲"} {
page, err := repo.List(context.Background(), business.UserScope{SelfID: 9, OnlySelf: true}, business.ChannelFilter{Keyword: keyword})
if err != nil || page.Total != 1 || len(page.Rows) != 1 || page.Rows[0].SourceDB != "lx" {
t.Fatalf("keyword must not escape the business scope: %q %+v %v", keyword, page, err)
}
}
page, err := repo.List(context.Background(), channelTestScope(), business.ChannelFilter{Keyword: "%"})
if err != nil || page.Total != 0 {
t.Fatal("keyword wildcards must be treated literally")
}
}
func TestChannelListMatchesBindingsAndLiteralBusinessKeyword(t *testing.T) {
db, repo := prepareChannels(t)
ctx := context.Background()
scope := channelTestScope()
page, err := repo.List(ctx, scope, business.ChannelFilter{})
if err != nil || page.Total != 5 {
t.Fatalf("本人及下级绑定范围错误: %+v %v", page, err)
}
user := uint(9)
page, err = repo.List(ctx, scope, business.ChannelFilter{BusierUserID: &user})
if err != nil || page.Total != 4 {
t.Fatalf("指定商务应只取本人绑定: %+v %v", page, err)
}
for _, row := range page.Rows {
if row.SourceDB != "lx" || row.BusierUserID == nil || *row.BusierUserID != 9 || row.BusinessDisplayName() != "lx-business(leaf)" {
t.Fatalf("归属展示错误: %+v", row)
}
}
for _, keyword := range []string{"LEAF", "lx-business"} {
page, err = repo.List(ctx, scope, business.ChannelFilter{BusierAccount: keyword})
if err != nil || page.Total != 4 {
t.Fatal("商务账号或 SW 用户名匹配失败")
}
}
page, err = repo.List(ctx, scope, business.ChannelFilter{BusierAccount: "%"})
if err != nil || page.Total != 0 {
t.Fatal("商务关键词被当成 SQL 通配符")
}
page, err = repo.List(ctx, business.UserScope{All: true}, business.ChannelFilter{})
if err != nil || page.Total != 7 {
t.Fatal("超管默认不限绑定")
}
page, err = repo.List(ctx, business.UserScope{All: true}, business.ChannelFilter{BusierAccount: "leaf"})
if err != nil || page.Total != 4 {
t.Fatal("超管输入商务账号后没有收窄")
}
if _, err := repo.Find(ctx, scope, business.ChannelKey{SourceDB: "yq", ID: 100}); err != business.ErrChannelScope {
t.Fatal("同号其他平台未拒绝")
}
if err := db.Exec("DROP TABLE tab_promote").Error; err != nil {
t.Fatal(err)
}
page, err = repo.List(ctx, scope, business.ChannelFilter{})
if err != nil || page.Available {
t.Fatal("缺渠道表未标记不可用")
}
page, err = repo.List(ctx, scope, business.ChannelFilter{BusierAccount: "not-found"})
if err != nil || !page.Available || page.Total != 0 {
t.Fatal("没有绑定是确定空集")
}
}
func TestChannelMetricStorageSeparatesSourcesAndMissingTables(t *testing.T) {
db, repo := prepareChannels(t)
ctx := context.Background()
keys := []business.ChannelKey{{SourceDB: "lx", ID: 100}, {SourceDB: "lx", ID: 101}, {SourceDB: "lx", ID: 102}}
metrics, err := repo.Metrics(ctx, keys[:1], keys, true)
if err != nil || metrics.Games[keys[0]] != 2 || len(metrics.PlayerCards) != 3 || metrics.Pay[keys[0]] != "10.1" {
t.Fatalf("原始指标查询范围错误: %+v %v", metrics, err)
}
var card string
for _, player := range metrics.PlayerCards {
if player.UserID == 1 {
card = player.IDCard
}
}
if card != " abcX " {
t.Fatal("idcard 存储字段未映射,导致身份证去重人数错误")
}
if err := db.Exec("DROP TABLE tab_user").Error; err != nil {
t.Fatal(err)
}
metrics, err = repo.Metrics(ctx, keys[:1], keys, false)
if err != nil || metrics.PlayersAvailable || !metrics.GamesAvailable || !metrics.PayAvailable {
t.Fatal("一个指标缺表不应掩盖其余指标")
}
}
func TestChannelGameDistributionUsesReportsAndCurrentDedupStart(t *testing.T) {
db, repo := prepareChannels(t)
ctx := context.Background()
root, err := repo.Find(ctx, channelTestScope(), business.ChannelKey{SourceDB: "lx", ID: 100})
if err != nil {
t.Fatal(err)
}
scope := business.ChannelGameScope{Root: root, IncludeSub: true, IDCardDedup: true, EndDate: "2026-11-03"}
money := func(raw string) string {
n, ok := new(big.Rat).SetString(raw)
if !ok {
return "invalid"
}
return n.FloatString(2)
}
games, err := repo.Games(ctx, scope)
if err != nil || len(games) != 2 || games[0].ID != 1 || games[0].Name != "游戏甲" || games[0].RegisterCount != 1 || money(games[0].PayAmount) != "30.30" || games[1].RegisterCount != 1 || money(games[1].PayAmount) != "30.30" {
t.Fatalf("报表日期、去重口径、直接推广范围或渠道树错误: %+v %v", games, err)
}
scope.IDCardDedup = false
games, err = repo.Games(ctx, scope)
if err != nil || games[0].RegisterCount != 3 || money(games[0].PayAmount) != "31.40" {
t.Fatal("不去重口径没有使用自己的累计起点")
}
scope.IDCardDedup = true
scope.IncludeSub = false
games, err = repo.Games(ctx, scope)
if err != nil || money(games[0].PayAmount) != "10.10" || money(games[1].PayAmount) != "0.00" {
t.Fatal("不包含下级仍汇总了子渠道")
}
if err := db.Exec("DELETE FROM tab_sw_report_day WHERE dedup_mode=1").Error; err != nil {
t.Fatal(err)
}
games, err = repo.Games(ctx, scope)
if err != nil || games[0].RegisterCount != 0 || money(games[0].PayAmount) != "0.00" {
t.Fatal("无当前口径日汇总时必须沿用旧接口的当天起点")
}
if err := db.Exec("DROP TABLE tab_sw_report_day_key").Error; err != nil {
t.Fatal(err)
}
if _, err := repo.Games(ctx, scope); err != business.ErrChannelReportsUnavailable {
t.Fatal("缺报表表不能回退原始流水冒充同口径")
}
}