package allocate import ( "context" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity" _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/os/gtime" "testing" ) func TestPersistedAllocateDetailsRequireExactBusinessResult(t *testing.T) { task := &Task{Batch: Batch{BatchNo: "ALC-child", TenantID: "000000", CreatedBy: 133, CreatedDept: 175}} detail := &Detail{ID: 1, ICCID: "89860401790000277308", TargetDeptID: 185} good := entity.BatchDetail{Id: 1, BatchNo: task.Batch.BatchNo, BatchType: consts.BatchTypeAllocate, TenantId: task.Batch.TenantID, DetailStatus: consts.BatchDetailStatusSuccess, UpdatedBy: 133, Snapshot: gjson.New(detail.Snapshot(task.Batch).String())} if err := validatePersistedDetailResults(task, []*Detail{detail}, []*entity.BatchDetail{&good}); err != nil { t.Fatal(err) } for _, name := range []string{"missing", "tenant", "batch", "type", "status", "reason", "snapshot", "operator", "duplicate"} { t.Run(name, func(t *testing.T) { row := good rows := []*entity.BatchDetail{&row} details := []*Detail{detail} switch name { case "missing": rows = nil case "tenant": row.TenantId = "other" case "batch": row.BatchNo = "other" case "type": row.BatchType = 4 case "status": row.DetailStatus = consts.BatchDetailStatusRunning case "reason": row.ErrorReason = "unexpected" case "snapshot": row.Snapshot = gjson.New(map[string]any{"targetDeptId": 999}) case "operator": row.UpdatedBy = 999 case "duplicate": details = append(details, detail) rows = append(rows, &row) } if validatePersistedDetailResults(task, details, rows) == nil { t.Fatal("inconsistent detail accepted") } }) } detail.Reason = "整批校验失败" good.DetailStatus = consts.BatchDetailStatusFailed good.ErrorReason = detail.Reason good.Snapshot = gjson.New(detail.Snapshot(task.Batch).String()) if err := validatePersistedDetailResults(task, []*Detail{detail}, []*entity.BatchDetail{&good}); err != nil { t.Fatal(err) } } func TestDetailChunkZeroAffectedChecksPersistedResult(t *testing.T) { ctx := context.Background() db, err := gdb.New(gdb.ConfigNode{Type: "sqlite", Link: "sqlite::@file(:memory:)"}) if err != nil { t.Fatal(err) } defer db.Close(ctx) for _, sql := range []string{ "CREATE TABLE batch_detail (id INTEGER PRIMARY KEY, batch_no TEXT, batch_type INTEGER, tenant_id TEXT, detail_status INTEGER, error_reason TEXT, snapshot TEXT, updated_by INTEGER, updated_at DATETIME)", "CREATE TABLE card_test (id INTEGER PRIMARY KEY, dept_id INTEGER)", "INSERT INTO card_test VALUES (1,175)", } { if _, err = db.Exec(ctx, sql); err != nil { t.Fatal(err) } } task := &Task{Batch: Batch{BatchNo: "ALC-child", TenantID: "000000", CreatedBy: 133, CreatedDept: 175}} detail := &Detail{ID: 1, ICCID: "CARD", TargetDeptID: 185, Remark: "保留备注"} if _, err = db.Model("batch_detail").Data(do.BatchDetail{Id: 1, BatchNo: task.Batch.BatchNo, BatchType: 3, TenantId: task.Batch.TenantID, DetailStatus: 2, ErrorReason: "", Snapshot: detail.Snapshot(task.Batch), UpdatedBy: 133}).Insert(); err != nil { t.Fatal(err) } // 强制模拟驱动返回零修改行,不依赖 SQLite 与 MySQL 行数语义差异。 if _, err = db.Exec(ctx, "CREATE TRIGGER ignore_update BEFORE UPDATE ON batch_detail BEGIN SELECT RAISE(IGNORE); END"); err != nil { t.Fatal(err) } repo := &Repository{db: db} if err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { return repo.updateDetailChunk(ctx, tx, task, []*Detail{detail}, gtime.Now()) }); err != nil { t.Fatalf("identical persisted result rejected: %v", err) } for _, name := range []string{"mismatch", "missing", "tenant"} { t.Run(name, func(t *testing.T) { changed := *detail changed.Reason = "unexpected" if name == "missing" { changed.ID = 999 } scopedTask := *task if name == "tenant" { scopedTask.Batch.TenantID = "other" } err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { if _, err := tx.Exec("UPDATE card_test SET dept_id=185 WHERE id=1"); err != nil { return err } return repo.updateDetailChunk(ctx, tx, &scopedTask, []*Detail{&changed}, gtime.Now()) }) if err == nil { t.Fatal("unsafe update accepted") } dept, err := db.Model("card_test").Value("dept_id") if err != nil || dept.Int() != 175 { t.Fatalf("transaction failed to rollback ownership: %v %v", dept, err) } }) } }