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

134 lines
5.3 KiB
Go

package business
import (
"context"
"strings"
"testing"
"time"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"kra/internal/biz/business"
)
type alertSQLCapture struct {
logger.Interface
queries []string
elapsed []time.Duration
}
func (l *alertSQLCapture) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
sql, _ := fc()
if strings.Contains(sql, "AS current_value") || strings.Contains(sql, "AS first_unix") || strings.Contains(sql, "AS recent_amount") {
l.queries = append(l.queries, sql)
l.elapsed = append(l.elapsed, time.Since(begin))
}
l.Interface.Trace(ctx, begin, fc, err)
}
func TestMySQLAlertV2MigrationAndRangePlans(t *testing.T) {
db := openRehearsalMySQL(t)
now := prepareAlertV2(t, db)
// 模拟尚未升级的真实旧表,覆盖普通列合并迁移路径。
for _, sql := range []string{
"ALTER TABLE sys_alert_record DROP INDEX uk_alert_record_v2_open",
"ALTER TABLE sys_alert_record DROP COLUMN v2_open_key",
"ALTER TABLE sys_alert_record DROP COLUMN independent_rule, DROP COLUMN total_players, DROP COLUMN total_pay, DROP COLUMN warning_color",
"ALTER TABLE sys_alert_rule DROP COLUMN condition_mode, DROP COLUMN amount_direction, DROP COLUMN amount_threshold",
} {
if err := db.Exec(sql).Error; err != nil {
t.Fatal(err)
}
}
if err := db.Exec("ALTER TABLE sys_alert_record ADD COLUMN unclosed_flag TINYINT GENERATED ALWAYS AS (CASE WHEN status=5 THEN NULL ELSE 1 END) STORED, ADD UNIQUE INDEX uk_alert_record_unclosed(platform_code,target_promote_id,rule_type,unclosed_flag)").Error; err != nil {
t.Fatal(err)
}
if err := migrateAlertV2(db); err != nil {
t.Fatal(err)
}
if err := migrateAlertV2(db); err != nil {
t.Fatalf("重复迁移: %v", err)
}
if db.Migrator().HasIndex("sys_alert_record", "uk_alert_record_unclosed") {
t.Fatal("旧唯一键阻挡已忽略或独立绝对值规则")
}
if err := db.Exec("ALTER TABLE sys_alert_record DROP INDEX uk_alert_record_v2_open, ADD UNIQUE INDEX uk_alert_record_v2_open(platform_code,target_promote_id,rule_type,v2_open_key)").Error; err != nil {
t.Fatal(err)
}
if err := migrateAlertV2UniqueKey(db); err != nil {
t.Fatalf("已有 V2 索引升级失败: %v", err)
}
rows := make([]map[string]any, 10000)
for i := range rows {
rows[i] = map[string]any{"id": 100 + i, "source_db": "lx", "promote_id": 1, "pay_status": 1, "pay_time": now.AddDate(-1, 0, 0).Unix(), "pay_amount": "1.00"}
}
if err := db.Table("tab_spend").CreateInBatches(rows, 500).Error; err != nil {
t.Fatal(err)
}
if err := db.Exec("ANALYZE TABLE tab_spend,tab_promote").Error; err != nil {
t.Fatal(err)
}
capture := &alertSQLCapture{Interface: logger.Default.LogMode(logger.Silent)}
r := NewAlertTriggerRepo(testData{db.Session(&gorm.Session{Logger: capture})})
key := business.ChannelKey{SourceDB: "lx", ID: 1}
values, err := r.Comparison(context.Background(), 2, []business.ChannelKey{key}, now, 1)
if err != nil || values[key].Current != "3000.00" || values[key].Previous != "100.00" {
t.Fatalf("MySQL 周期计算: %+v %v", values, err)
}
if _, err := loadAlertLoss(db.Session(&gorm.Session{Logger: capture}), []business.ChannelKey{key, key}, business.AlertLossAt(now)); err != nil {
t.Fatal(err)
}
for i, sql := range capture.queries {
var plan []struct {
Table string
Type string
Key string
Rows int64
Extra string
}
if err := db.Raw("EXPLAIN " + sql).Scan(&plan).Error; err != nil {
t.Fatal(err)
}
t.Logf("query %d elapsed=%v plan=%+v", i, capture.elapsed[i], plan)
for _, step := range plan {
if (strings.Contains(sql, "AS current_value") || strings.Contains(sql, "AS recent_amount")) && (step.Table == "m" || step.Table == "tab_spend") && (step.Type != "range" || step.Rows > 100) {
t.Fatalf("预警周期统计必须按时间范围读取,不能扫描整个渠道历史: %+v", plan)
}
}
}
hit := &business.AlertRecord{PlatformCode: "lx", TargetPromoteID: 1, RuleID: 1, RuleType: "充值", IndependentRule: true, GapDays: 7, Status: 1, CreatedAt: now, UpdatedAt: now}
decide := func(r *business.AlertRecord) bool { return r == nil || r.Status == 1 }
for _, id := range []uint64{1, 2} {
hit.RuleID = id
if ok, err := r.StoreHit(context.Background(), hit, []int{1, 2, 3, 4}, decide); err != nil || !ok {
t.Fatalf("独立规则被唯一键挡住: %v %v", ok, err)
}
}
if err := db.Model(&alertRecordPO{}).Where("rule_id=1").Update("status", 6).Error; err != nil {
t.Fatal(err)
}
hit.RuleID = 1
hit.CreatedAt = now.AddDate(0, 0, 7)
if ok, err := r.StoreHit(context.Background(), hit, []int{1, 2, 3, 4}, decide); err != nil || !ok {
t.Fatalf("已忽略仍占用唯一键: %v %v", ok, err)
}
}
func TestMySQLAlertMigrationKeepsLegacyConstraintOnUnexpectedIndex(t *testing.T) {
db := openRehearsalMySQL(t)
for _, sql := range []string{
"ALTER TABLE sys_alert_record DROP INDEX uk_alert_record_v2_open, ADD INDEX uk_alert_record_v2_open(platform_code), ADD UNIQUE INDEX uk_alert_record_daily(platform_code,target_promote_id,rule_type,alert_date)",
} {
if err := db.Exec(sql).Error; err != nil {
t.Fatal(err)
}
}
if err := migrateAlertV2UniqueKey(db); err == nil {
t.Fatal("意外的现有索引定义不应被当成迁移成功")
}
if !db.Migrator().HasIndex("sys_alert_record", "uk_alert_record_daily") {
t.Fatal("验证失败前不能移除旧约束")
}
}