221 lines
12 KiB
Go
221 lines
12 KiB
Go
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("缺报表表不能回退原始流水冒充同口径")
|
||
}
|
||
}
|