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

202 lines
9.3 KiB
Go

package business
import (
"context"
"sync/atomic"
"testing"
"time"
"gorm.io/gorm"
"kra/internal/biz/business"
)
func TestAlertLossSkipsPaymentReadsForExpiredLogins(t *testing.T) {
db := openTestDB(t)
now := prepareAlertV2(t, db)
if err := db.Exec("UPDATE tab_promote SET last_login_time=0,create_time=1").Error; err != nil {
t.Fatal(err)
}
var paymentReads atomic.Int32
if err := db.Callback().Row().Before("gorm:row").Register("count_loss_payment_reads", func(tx *gorm.DB) {
if tx.Statement.Table == "tab_spend" {
paymentReads.Add(1)
}
}); err != nil {
t.Fatal(err)
}
keys := []business.ChannelKey{{SourceDB: "lx", ID: 1}, {SourceDB: "hw", ID: 1}}
window := business.AlertLossAt(now)
states, err := loadAlertLoss(db, keys, window)
if err != nil || paymentReads.Load() != 0 || len(states) != 2 || !*states[keys[0]] || !*states[keys[1]] {
t.Fatalf("expired logins should establish loss without payment reads: %+v %v reads=%d", states, err, paymentReads.Load())
}
if err := db.Exec("UPDATE tab_promote SET last_login_time=? WHERE source_db='hw'", now.Unix()).Error; err != nil {
t.Fatal(err)
}
states, err = loadAlertLoss(db, keys, window)
if err != nil || paymentReads.Load() != 1 || !*states[keys[0]] || *states[keys[1]] {
t.Fatalf("active login must still check payments within its own platform: %+v %v reads=%d", states, err, paymentReads.Load())
}
}
func prepareAlertV2(t *testing.T, db *gorm.DB) time.Time {
t.Helper()
now := time.Date(2026, 11, 5, 0, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600))
for _, sql := range []string{
"CREATE TABLE tab_promote(id BIGINT,source_db VARCHAR(20),busier_id BIGINT,account VARCHAR(100),promote_level INT,status INT,last_login_time BIGINT,create_time BIGINT,parent_id BIGINT,top_promote_id BIGINT,PRIMARY KEY(source_db,id))",
"CREATE TABLE tab_user(id BIGINT,source_db VARCHAR(20),promote_id BIGINT,puid BIGINT,register_time VARCHAR(30),idcard VARCHAR(100),PRIMARY KEY(source_db,id))",
"CREATE TABLE tab_spend(id BIGINT PRIMARY KEY,source_db VARCHAR(20),promote_id BIGINT,pay_status INT,pay_time BIGINT,pay_amount DECIMAL(20,2))",
"INSERT INTO tab_promote VALUES(1,'lx',7,'父渠道',1,1,0,1,0,0),(2,'lx',7,'子渠道',2,1,0,1,1,1),(3,'lx',7,'孙渠道',3,1,0,1,2,1),(1,'hw',7,'同号渠道',1,1,0,1,0,0)",
} {
if err := db.Exec(sql).Error; err != nil {
t.Fatal(err)
}
}
if err := db.Exec("UPDATE tab_promote SET last_login_time=?", now.Unix()).Error; err != nil {
t.Fatal(err)
}
if err := db.Exec("INSERT INTO tab_user VALUES(1,'lx',1,0,?,'card-a'),(2,'lx',2,0,?,'card-a'),(3,'lx',3,0,?,'card-b'),(4,'lx',3,1,?,'card-c'),(1,'hw',1,0,?,'other')", now.AddDate(0, 0, -2).Unix(), now.AddDate(0, 0, -1).Unix(), now.AddDate(0, 0, -1).Unix(), now.AddDate(0, 0, -1).Unix(), now.AddDate(0, 0, -1).Unix()).Error; err != nil {
t.Fatal(err)
}
if err := db.Exec("INSERT INTO tab_spend VALUES(1,'lx',1,1,?,100),(2,'lx',2,1,?,2000),(3,'lx',3,1,?,1000),(4,'lx',3,0,?,999999),(5,'hw',1,1,?,999999)", now.AddDate(0, 0, -2).Unix(), now.AddDate(0, 0, -1).Unix(), now.AddDate(0, 0, -1).Unix(), now.AddDate(0, 0, -1).Unix(), now.AddDate(0, 0, -1).Unix()).Error; err != nil {
t.Fatal(err)
}
return now
}
func TestAlertV2SnapshotsTreeAndBoundedComparison(t *testing.T) {
db := openTestDB(t)
now := prepareAlertV2(t, db)
repo := NewAlertTriggerRepo(testData{db})
key := business.ChannelKey{SourceDB: "lx", ID: 1}
sample, err := repo.Comparison(context.Background(), 2, []business.ChannelKey{key}, now, 1)
if err != nil || sample[key].Current != "3000.00" || sample[key].Previous != "100.00" {
t.Fatalf("子渠道汇总或平台隔离错误: %+v %v", sample, err)
}
for _, dedup := range []bool{true, false} {
totals, err := repo.Snapshots(context.Background(), []business.ChannelKey{key}, now, dedup)
want := int64(3)
if dedup {
want = 2
}
if err != nil || totals[key].Players != want || totals[key].Pay != "3100" {
t.Fatalf("累计快照或去重错误: %+v %v", totals, err)
}
}
}
func TestAlertV2ExemptionHistoryAndAutomaticRemoval(t *testing.T) {
db := openTestDB(t)
now := prepareAlertV2(t, db)
if err := db.AutoMigrate(&accountPO{}); err != nil {
t.Fatal(err)
}
if err := db.Exec("INSERT INTO sys_users(id,username,level,id_path) VALUES(7,'owner',1,'0')").Error; err != nil {
t.Fatal(err)
}
if err := db.Exec("INSERT INTO sys_user_bind_platform(user_id,source_db,platform_busier_id,status) VALUES(7,'lx',7,'0')").Error; err != nil {
t.Fatal(err)
}
r := NewAlertGovernanceRepo(testData{db})
ctx := context.Background()
entry := &business.AlertExemption{ChannelKey: business.ChannelKey{SourceDB: "lx", ID: 1}, ChannelName: "父渠道", OwnerID: 7, AddedBy: 7, AddedName: "一级商务", AddedAt: now, Reason: "长期合作"}
if err := r.AddExemption(ctx, business.UserScope{All: true}, entry.ChannelKey, func(*business.AlertExemptionTarget) (*business.AlertExemption, error) { return entry, nil }); err != nil {
t.Fatal(err)
}
if err := r.AddExemption(ctx, business.UserScope{All: true}, entry.ChannelKey, func(*business.AlertExemptionTarget) (*business.AlertExemption, error) { return entry, nil }); err != business.ErrAlertExemptExists {
t.Fatalf("重复加入: %v", err)
}
rows, total, err := r.Exemptions(ctx, business.LevelOneScope{UserID: 7}, business.AlertExemptionFilter{})
if err != nil || total != 1 {
t.Fatalf("名单: %+v %v", rows, err)
}
if err := r.RemoveExemption(ctx, business.LevelOneScope{UserID: 8}, rows[0].ID, 8, "越权", "手动移出", now); err != business.ErrAlertExemptNotFound {
t.Fatalf("跨商务移出: %v", err)
}
hit := &business.AlertRecord{BusinessID: 7, PlatformCode: "lx", TargetPromoteID: 1, RuleID: 1, RuleType: "充值", Status: 1, CreatedAt: now, UpdatedAt: now}
written, err := NewAlertTriggerRepo(testData{db}).StoreHit(ctx, hit, []int{1, 2, 3, 4}, func(*business.AlertRecord) bool { return true })
if err != nil || written {
t.Fatalf("免预警仍生成: %v %v", written, err)
}
// 父渠道自身近 30 天流水仅 100 元,满足激活池条件;子渠道流水不冒充它自身的流水。
uc := business.NewAlertGovernanceUsecase(r, nil)
if err := uc.Reconcile(ctx, now); err != nil {
t.Fatal(err)
}
rows, _, _ = r.Exemptions(ctx, business.LevelOneScope{All: true}, business.AlertExemptionFilter{})
if rows[0].RemovedAt == nil || rows[0].RemovedName != "系统(自动移出)" || rows[0].RemoveReason != "进入激活池 · 自动移出" {
t.Fatalf("自动移出未留痕: %+v", rows[0])
}
if err := r.AddExemption(ctx, business.UserScope{All: true}, entry.ChannelKey, func(*business.AlertExemptionTarget) (*business.AlertExemption, error) { return entry, nil }); err != nil {
t.Fatal(err)
}
_, total, err = r.Exemptions(ctx, business.LevelOneScope{All: true}, business.AlertExemptionFilter{})
if err != nil || total != 2 {
t.Fatal("重新加入不得覆盖历史")
}
}
func TestAlertV2GapSeparatesAbsoluteRulesAndIgnoredRecords(t *testing.T) {
db := openTestDB(t)
r := NewAlertTriggerRepo(testData{db})
now := time.Now().Truncate(time.Second)
hit := &business.AlertRecord{PlatformCode: "lx", TargetPromoteID: 1, RuleID: 1, RuleType: "充值", Status: 1, CreatedAt: now, UpdatedAt: now, GapDays: 7, IndependentRule: true}
decide := func(current *business.AlertRecord) bool {
return current == nil || current.Status == business.AlertUnprocessed
}
write := func(want bool) {
t.Helper()
got, err := r.StoreHit(context.Background(), hit, []int{1, 2, 3, 4}, decide)
if err != nil || got != want {
t.Fatalf("written=%v want=%v err=%v", got, want, err)
}
}
write(true)
hit.RuleID = 2
write(true)
hit.RuleID = 1
write(false)
if err := db.Model(&alertRecordPO{}).Where("rule_id=1").Update("status", business.AlertIgnored).Error; err != nil {
t.Fatal(err)
}
hit.CreatedAt = now.AddDate(0, 0, 6)
write(false)
hit.CreatedAt = now.AddDate(0, 0, 7)
write(true)
var rows []alertRecordPO
if err := db.Order("id").Find(&rows).Error; err != nil {
t.Fatal(err)
}
if len(rows) != 3 || rows[0].Status != business.AlertIgnored {
t.Fatal("已忽略记录被复活或独立规则被覆盖")
}
}
func TestAlertV2LiveLossDoesNotRewriteSnapshots(t *testing.T) {
db := openTestDB(t)
now := prepareAlertV2(t, db)
// 流失检测使用父渠道的当前值,快照保持触发时的值。
players, pay := int64(2), "3100.00"
if err := db.Create(&alertRecordPO{ID: 1, PlatformCode: "lx", TargetPromoteID: 1, BusinessID: 7, Status: 1, AlertContent: "满2000", TotalPlayers: &players, TotalPay: &pay, CreatedAt: now}).Error; err != nil {
t.Fatal(err)
}
scope := business.AlertRecordScope{Owner: business.LevelOneScope{All: true}, Viewer: business.UserScope{All: true}}
r := NewAlertRecordRepo(testData{db})
lost := true
page, err := r.List(context.Background(), scope, business.AlertRecordFilter{Lost: &lost, AlertContent: "2000"})
if err != nil || page.Total != 1 || page.Rows[0].Lost == nil || !*page.Rows[0].Lost {
t.Fatalf("实时流失筛选: %+v %v", page, err)
}
if err := db.Exec("UPDATE tab_spend SET pay_amount=1000 WHERE id=1").Error; err != nil {
t.Fatal(err)
}
if err := db.Exec("UPDATE tab_promote SET last_login_time=?", time.Now().Unix()).Error; err != nil {
t.Fatal(err)
}
lost = false
page, err = r.List(context.Background(), scope, business.AlertRecordFilter{Lost: &lost})
if err != nil || page.Total != 1 || *page.Rows[0].TotalPlayers != 2 || *page.Rows[0].TotalPay != "3100" {
t.Fatalf("实时状态或冻结快照错误: %+v %v", page, err)
}
}