251 lines
10 KiB
Go
251 lines
10 KiB
Go
package business
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"kra/internal/biz/business"
|
|
)
|
|
|
|
func prepareAlertRecords(t *testing.T) (*gorm.DB, business.AlertRecordRepo) {
|
|
t.Helper()
|
|
db := prepareAccounts(t)
|
|
for _, sql := range []string{
|
|
"INSERT INTO sys_users(id,username,level,parent_id,id_path) VALUES(7,'owner',1,0,'0'),(8,'child',2,7,'0,7'),(9,'leaf',3,8,'0,7,8'),(10,'sibling',2,7,'0,7'),(99,'other',1,0,'0')",
|
|
"INSERT INTO sys_user_bind_platform(user_id,source_db,platform_busier_id,status) VALUES(9,'lx',14,'0'),(10,'hw',14,'0'),(9,'hw',15,'3'),(99,'yq',14,'0')",
|
|
"INSERT INTO tab_promote VALUES(100,'lx',14,1,0),(101,'lx',14,2,1),(100,'hw',14,1,1),(102,'hw',15,1,1),(100,'yq',14,1,1)",
|
|
"CREATE TABLE media_file_upload_and_downloads(id INTEGER PRIMARY KEY,url TEXT,deleted_at DATETIME)",
|
|
"INSERT INTO media_file_upload_and_downloads(id,url) VALUES(1,'/files/one.png'),(2,'/files/two.png')",
|
|
} {
|
|
if err := db.Exec(sql).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
stamp := time.Date(2026, 11, 3, 10, 0, 0, 0, time.UTC)
|
|
pos := []alertRecordPO{
|
|
{ID: 1, PlatformCode: "lx", TargetPromoteID: 100, BusinessID: 7, RuleName: "注册", RuleType: "register_anomaly", Status: 1, CreatedAt: stamp, UpdatedAt: stamp},
|
|
{ID: 2, PlatformCode: "lx", TargetPromoteID: 100, BusinessID: 7, RuleName: "充值", RuleType: "recharge_anomaly", Status: 3, CreatedAt: stamp, UpdatedAt: stamp},
|
|
{ID: 3, PlatformCode: "hw", TargetPromoteID: 100, BusinessID: 7, Status: 2, CreatedAt: stamp},
|
|
{ID: 4, PlatformCode: "yq", TargetPromoteID: 100, BusinessID: 99, Status: 5, CreatedAt: stamp},
|
|
{ID: 5, PlatformCode: "lx", TargetPromoteID: 100, BusinessID: 99, Status: 1, CreatedAt: stamp},
|
|
{ID: 6, PlatformCode: "lx", TargetPromoteID: 101, BusinessID: 7, Status: 1, CreatedAt: stamp},
|
|
{ID: 7, PlatformCode: "hw", TargetPromoteID: 102, BusinessID: 7, Status: 1, CreatedAt: stamp},
|
|
}
|
|
if err := db.Create(&pos).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return db, NewAlertRecordRepo(testData{db})
|
|
}
|
|
|
|
func alertTestScope(id uint) business.AlertRecordScope {
|
|
return business.AlertRecordScope{Owner: business.LevelOneScope{UserID: 7}, Viewer: business.UserScope{SelfID: id, IncludeSelf: true, OnlySelf: id == 9, DescendantPath: map[uint]string{7: "0,7", 8: "0,7,8", 9: "0,7,8,9"}[id]}}
|
|
}
|
|
|
|
func TestAlertRecordsScopeAndSummaryUseDifferentFilters(t *testing.T) {
|
|
_, repo := prepareAlertRecords(t)
|
|
ctx := context.Background()
|
|
for _, id := range []uint{8, 9} {
|
|
page, err := repo.List(ctx, alertTestScope(id), business.AlertRecordFilter{})
|
|
if err != nil || page.Total != 2 || len(page.Rows) != 2 || page.Rows[0].ID != 2 || page.Rows[1].ID != 1 {
|
|
t.Fatalf("渠道状态、平台隔离、一级归属或排序错误: %+v %v", page, err)
|
|
}
|
|
}
|
|
scope := alertTestScope(7)
|
|
status := 1
|
|
page, err := repo.List(ctx, scope, business.AlertRecordFilter{RuleName: "注册", Status: &status})
|
|
if err != nil || page.Total != 1 || len(page.StatusCount) != 1 || page.StatusCount[1] != 1 {
|
|
t.Fatalf("汇总应继承名称筛选并忽略状态自身: %+v %v", page, err)
|
|
}
|
|
selected := alertTestScope(8).Viewer
|
|
scope.Selected = &selected
|
|
page, err = repo.List(ctx, scope, business.AlertRecordFilter{})
|
|
if err != nil || page.Total != 2 {
|
|
t.Fatalf("二级商务筛选误用了 record.business_id: %+v %v", page, err)
|
|
}
|
|
outside := business.UserScope{SelfID: 99, OnlySelf: true}
|
|
scope.Selected = &outside
|
|
page, err = repo.List(ctx, scope, business.AlertRecordFilter{})
|
|
if err != nil || page.Total != 0 || len(page.StatusCount) != 0 {
|
|
t.Fatalf("指定其他商务绕过查看范围: %+v %v", page, err)
|
|
}
|
|
root := business.AlertRecordScope{Owner: business.LevelOneScope{All: true}, Viewer: business.UserScope{All: true}, Selected: &selected}
|
|
page, err = repo.List(ctx, root, business.AlertRecordFilter{})
|
|
if err != nil || page.Total != 3 {
|
|
t.Fatalf("超管指定商务须收窄渠道但不增加一级归属约束: %+v %v", page, err)
|
|
}
|
|
if _, err := repo.Detail(ctx, alertTestScope(8), 3); err != business.ErrAlertRecordScope {
|
|
t.Fatal("同号不同平台详情被放行")
|
|
}
|
|
if _, err := repo.Detail(ctx, alertTestScope(8), 5); err != business.ErrAlertRecordScope {
|
|
t.Fatal("异常归属预警跨一级商务泄漏")
|
|
}
|
|
}
|
|
|
|
func TestAlertRecordsMissingChannelsFailClosed(t *testing.T) {
|
|
db, repo := prepareAlertRecords(t)
|
|
if err := db.Exec("DROP TABLE tab_promote").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
page, err := repo.List(ctx, alertTestScope(8), business.AlertRecordFilter{})
|
|
if err != nil || page.Available || page.Total != 0 {
|
|
t.Fatalf("缺渠道表未明确不可用: %+v %v", page, err)
|
|
}
|
|
if _, err := repo.Detail(ctx, alertTestScope(8), 1); err != business.ErrAlertChannelsUnavailable {
|
|
t.Fatalf("详情应拒绝未知范围: %v", err)
|
|
}
|
|
root := business.AlertRecordScope{Owner: business.LevelOneScope{All: true}, Viewer: business.UserScope{All: true}}
|
|
page, err = repo.List(ctx, root, business.AlertRecordFilter{})
|
|
if err != nil || !page.Available || page.Total != 7 {
|
|
t.Fatal("不限范围的超管查询不应依赖渠道表")
|
|
}
|
|
root.Selected = &business.UserScope{SelfID: 500, OnlySelf: true}
|
|
page, err = repo.List(ctx, root, business.AlertRecordFilter{})
|
|
if err != nil || !page.Available || page.Total != 0 {
|
|
t.Fatal("无绑定应是确定空集")
|
|
}
|
|
}
|
|
|
|
func TestBindingPortraitAlertsStayWithinSelectedSDKAccount(t *testing.T) {
|
|
db, repo := prepareAlertRecords(t)
|
|
if err := db.Exec("INSERT INTO sys_user_bind_platform(user_id,source_db,platform_busier_id,status) VALUES(9,'lx',16,'0')").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec("INSERT INTO tab_promote VALUES(103,'lx',16,1,1)").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(&alertRecordPO{ID: 8, PlatformCode: "lx", TargetPromoteID: 103, BusinessID: 7, Status: 1, CreatedAt: time.Now()}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
for _, sample := range []struct {
|
|
busier uint64
|
|
want int64
|
|
}{{14, 2}, {16, 1}, {999, 0}} {
|
|
page, err := repo.List(ctx, alertTestScope(9), business.AlertRecordFilter{PlatformCodes: []string{"lx"}, PlatformBusierID: &sample.busier})
|
|
if err != nil || page.Total != sample.want {
|
|
t.Fatalf("binding %d: %+v %v", sample.busier, page, err)
|
|
}
|
|
var total int64
|
|
for _, count := range page.StatusCount {
|
|
total += count
|
|
}
|
|
if total != sample.want {
|
|
t.Fatal("binding filter must also constrain status counts")
|
|
}
|
|
}
|
|
busier := uint64(16)
|
|
page, err := repo.List(ctx, alertTestScope(10), business.AlertRecordFilter{PlatformCodes: []string{"lx"}, PlatformBusierID: &busier})
|
|
if err != nil || page.Total != 0 {
|
|
t.Fatal("binding filter must not expand the viewer's permissions")
|
|
}
|
|
}
|
|
|
|
func TestAlertRecordTransactionRollsBackWhenLogFails(t *testing.T) {
|
|
db, repo := prepareAlertRecords(t)
|
|
if err := db.Exec("CREATE TRIGGER reject_note BEFORE INSERT ON sys_alert_operation_log WHEN NEW.log_type = 2 BEGIN SELECT RAISE(ABORT, 'test failure'); END").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err := repo.WithRecord(context.Background(), alertTestScope(8), 1, func(r *business.AlertRecord) ([]*business.AlertLog, error) {
|
|
r.Status = 2
|
|
return []*business.AlertLog{{LogType: 1, ActionDetail: "start"}, {LogType: 2, NoteContent: "备注"}}, nil
|
|
})
|
|
if err == nil {
|
|
t.Fatal("日志失败应回滚")
|
|
}
|
|
var record alertRecordPO
|
|
if err := db.First(&record, 1).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var count int64
|
|
db.Model(&alertLogPO{}).Count(&count)
|
|
if record.Status != 1 || count != 0 {
|
|
t.Fatal("留下半次状态流转或日志")
|
|
}
|
|
}
|
|
|
|
func TestAlertRecordRepeatedConcurrentActionCreatesOneLog(t *testing.T) {
|
|
db, repo := prepareAlertRecords(t)
|
|
pool, _ := db.DB()
|
|
// SQLite 用单连接串行事务;生产 MySQL 使用同一行锁回调,状态必须在锁内校验。
|
|
pool.SetMaxOpenConns(1)
|
|
var success atomic.Int32
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 12; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
err := repo.WithRecord(context.Background(), alertTestScope(8), 1, func(r *business.AlertRecord) ([]*business.AlertLog, error) {
|
|
if r.Status != 1 {
|
|
return nil, business.ErrAlertRecordChanged
|
|
}
|
|
r.Status = 2
|
|
return []*business.AlertLog{{LogType: 1, ActionDetail: "start"}}, nil
|
|
})
|
|
if err == nil {
|
|
success.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
var count int64
|
|
db.Model(&alertLogPO{}).Count(&count)
|
|
if success.Load() != 1 || count != 1 {
|
|
t.Fatalf("重复请求写入多次: successes=%d logs=%d", success.Load(), count)
|
|
}
|
|
}
|
|
|
|
func TestAlertRecordLogAttachmentsAndStableOrder(t *testing.T) {
|
|
db, repo := prepareAlertRecords(t)
|
|
stamp := time.Now()
|
|
for i, raw := range []string{`[2,"1",999,2]`, `["/legacy/a.png","https://files.invalid/b.png"]`, `{"bad":true}`} {
|
|
value := raw
|
|
if err := db.Create(&alertLogPO{ID: uint64(i + 1), AlertID: 1, LogType: 2, AttachmentIDs: &value, CreatedAt: stamp}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
detail, err := repo.Detail(context.Background(), alertTestScope(8), 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(detail.Logs) != 3 || detail.Logs[0].ID != 3 || len(detail.Logs[0].Attachments) != 0 || len(detail.Logs[1].Attachments) != 2 || len(detail.Logs[2].Attachments) != 2 || detail.Logs[2].Attachments[0] != "/files/two.png" {
|
|
t.Fatalf("附件或顺序错误: %+v", detail.Logs)
|
|
}
|
|
}
|
|
|
|
func TestAlertAttachmentMigrationUsesLegacyBackupAndIsIdempotent(t *testing.T) {
|
|
db, _ := prepareAlertRecords(t)
|
|
for _, sql := range []string{
|
|
"CREATE TABLE bak_sw2kra_20261103_sys_alert_operation_log(id INTEGER,attachment_ids TEXT)",
|
|
"INSERT INTO sw_kra_id_map(entity,old_id,new_id) VALUES('file','9007199254740993',2),('file','1',1)",
|
|
`INSERT INTO sys_alert_operation_log(id,alert_id,attachment_ids) VALUES(10,1,'[9007199254740993,999]'),(11,1,'["/legacy.png"]'),(12,1,'[1]'),(13,1,'[2]')`,
|
|
`INSERT INTO bak_sw2kra_20261103_sys_alert_operation_log VALUES(10,'[9007199254740993,999]'),(11,'["/legacy.png"]'),(13,'[9007199254740993]')`,
|
|
} {
|
|
if err := db.Exec(sql).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for i := 0; i < 2; i++ {
|
|
if err := migrateAlertAttachments(db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
var rows []alertLogPO
|
|
db.Order("id").Find(&rows)
|
|
if len(rows) != 4 {
|
|
t.Fatal("日志丢失")
|
|
}
|
|
var ids []int64
|
|
if json.Unmarshal([]byte(*rows[0].AttachmentIDs), &ids) != nil || len(ids) != 1 || ids[0] != 2 {
|
|
t.Fatal("大 ID 映射或失效引用处理错误")
|
|
}
|
|
if *rows[1].AttachmentIDs != `["/legacy.png"]` || *rows[2].AttachmentIDs != `[1]` || *rows[3].AttachmentIDs != `[2]` {
|
|
t.Fatal("改动了 URL、新日志或已经转换的附件")
|
|
}
|
|
}
|