202 lines
9.3 KiB
Go
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)
|
|
}
|
|
}
|