package dept import ( "context" "errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zeromicro/go-zero/core/stores/sqlx" "perms-system-server/internal/testutil" "sync" "sync/atomic" "testing" "time" ) func TestSysDeptModel_CRUD(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() data := &SysDept{ ParentId: 0, Name: "dept_crud_" + testutil.UniqueId(), Path: "/crud/" + testutil.UniqueId() + "/", Sort: 10, Remark: "r", Status: 1, CreateTime: now, UpdateTime: now, } res, err := m.Insert(ctx, data) require.NoError(t, err) id, err := res.LastInsertId() require.NoError(t, err) tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, id) }) found, err := m.FindOne(ctx, id) require.NoError(t, err) require.NotNil(t, found) assert.Equal(t, id, found.Id) assert.Equal(t, data.Name, found.Name) found.Remark = "updated" found.UpdateTime = now + 1 require.NoError(t, m.Update(ctx, found)) after, err := m.FindOne(ctx, id) require.NoError(t, err) assert.Equal(t, "updated", after.Remark) require.NoError(t, m.Delete(ctx, id)) _, err = m.FindOne(ctx, id) require.Error(t, err) assert.True(t, errors.Is(err, ErrNotFound)) } // TC-0442: FindAll 排序 sort asc, id asc func TestSysDeptModel_FindAll_OrderBySortAscIdAsc(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) base := testutil.UniqueId() now := time.Now().Unix() rows := []*SysDept{ {ParentId: 0, Name: "a_" + base, Path: "/fa/" + base + "/a/", Sort: 30, Status: 1, CreateTime: now, UpdateTime: now}, {ParentId: 0, Name: "b_" + base, Path: "/fa/" + base + "/b/", Sort: 10, Status: 1, CreateTime: now, UpdateTime: now}, {ParentId: 0, Name: "c_" + base, Path: "/fa/" + base + "/c/", Sort: 20, Status: 1, CreateTime: now, UpdateTime: now}, } require.NoError(t, m.BatchInsert(ctx, rows)) all, err := m.FindAll(ctx) require.NoError(t, err) nameSet := map[string]struct{}{rows[0].Name: {}, rows[1].Name: {}, rows[2].Name: {}} var picked []*SysDept var ids []int64 for i := range all { if _, ok := nameSet[all[i].Name]; ok { picked = append(picked, all[i]) ids = append(ids, all[i].Id) } } tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, ids...) }) require.Len(t, picked, 3) for i := 1; i < len(picked); i++ { prev, cur := picked[i-1], picked[i] if prev.Sort == cur.Sort { assert.Less(t, prev.Id, cur.Id) } else { assert.Less(t, prev.Sort, cur.Sort) } } assert.Equal(t, int64(10), picked[0].Sort) assert.Equal(t, int64(20), picked[1].Sort) assert.Equal(t, int64(30), picked[2].Sort) } // TC-0336: 批量 Insert/Delete func TestSysDeptModel_BatchInsert_BatchDelete(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() tag := testutil.UniqueId() batch := []*SysDept{ {ParentId: 0, Name: "b1_" + tag, Path: "/bi/" + tag + "/1/", Sort: 1, Status: 1, CreateTime: now, UpdateTime: now}, {ParentId: 0, Name: "b2_" + tag, Path: "/bi/" + tag + "/2/", Sort: 2, Status: 1, CreateTime: now, UpdateTime: now}, } require.NoError(t, m.BatchInsert(ctx, batch)) all, err := m.FindAll(ctx) require.NoError(t, err) wanted := map[string]struct{}{batch[0].Name: {}, batch[1].Name: {}} var ids []int64 for _, row := range all { if _, ok := wanted[row.Name]; ok { ids = append(ids, row.Id) } } require.Len(t, ids, 2) tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, ids...) }) require.NoError(t, m.BatchDelete(ctx, ids)) for _, id := range ids { _, err := m.FindOne(ctx, id) require.Error(t, err) assert.True(t, errors.Is(err, ErrNotFound)) } } // TC-0327: 事务内 Insert/Update func TestSysDeptModel_TransactCtx_InsertWithTx_UpdateWithTx(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() d := &SysDept{ ParentId: 0, Name: "tx_" + testutil.UniqueId(), Path: "/tx/" + testutil.UniqueId() + "/", Sort: 1, Remark: "before", Status: 1, CreateTime: now, UpdateTime: now, } var finalId int64 err := m.TransactCtx(ctx, func(c context.Context, s sqlx.Session) error { res, err := m.InsertWithTx(c, s, d) if err != nil { return err } lid, err := res.LastInsertId() if err != nil { return err } finalId = lid d.Id = finalId d.Remark = "after_tx" d.UpdateTime = now + 2 return m.UpdateWithTx(c, s, d) }) require.NoError(t, err) tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, finalId) }) out, err := m.FindOne(ctx, finalId) require.NoError(t, err) assert.Equal(t, "after_tx", out.Remark) } // TC-0333: 表名 func TestSysDeptModel_TableName(t *testing.T) { conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) assert.Equal(t, "`sys_dept`", m.TableName()) } // TC-0319: FindOne 不存在 func TestSysDeptModel_FindOne_NotFound(t *testing.T) { conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) _, err := m.FindOne(context.Background(), 999999999999) require.ErrorIs(t, err, ErrNotFound) } // TC-0326: Update 不存在行不报错 func TestSysDeptModel_Update_NonExistentRow_NoError(t *testing.T) { conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) err := m.Update(context.Background(), &SysDept{ Id: 999999999999, Name: "ghost", Path: "/x/", Status: 1, CreateTime: time.Now().Unix(), UpdateTime: time.Now().Unix(), }) require.NoError(t, err) } // TC-0329: Delete 不存在行不报错 func TestSysDeptModel_Delete_NonExistentRow_NoError(t *testing.T) { conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) err := m.Delete(context.Background(), 999999999999) require.NoError(t, err) } // TC-0334: BatchInsert 空 func TestSysDeptModel_BatchInsert_Empty(t *testing.T) { conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) require.NoError(t, m.BatchInsert(context.Background(), nil)) require.NoError(t, m.BatchInsert(context.Background(), []*SysDept{})) } // TC-0353: BatchDelete 空 func TestSysDeptModel_BatchDelete_Empty(t *testing.T) { conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) require.NoError(t, m.BatchDelete(context.Background(), nil)) require.NoError(t, m.BatchDelete(context.Background(), []int64{})) } // TC-0316: 事务回滚无数据 func TestSysDeptModel_InsertWithTx_Rollback(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() uniq := "txrb_" + testutil.UniqueId() d := &SysDept{ ParentId: 0, Name: uniq, Path: "/" + uniq + "/", Sort: 1, Status: 1, CreateTime: now, UpdateTime: now, } err := m.TransactCtx(ctx, func(c context.Context, s sqlx.Session) error { if _, e := m.InsertWithTx(c, s, d); e != nil { return e } return errors.New("force rollback") }) require.Error(t, err) all, err := m.FindAll(ctx) require.NoError(t, err) for _, row := range all { assert.NotEqual(t, uniq, row.Name) } } // TC-0330: 事务内删除 func TestSysDeptModel_DeleteWithTx(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() d := &SysDept{ ParentId: 0, Name: "deltx_" + testutil.UniqueId(), Path: "/deltx/" + testutil.UniqueId() + "/", Sort: 1, Status: 1, CreateTime: now, UpdateTime: now, } res, err := m.Insert(ctx, d) require.NoError(t, err) id, _ := res.LastInsertId() tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, id) }) err = m.TransactCtx(ctx, func(c context.Context, s sqlx.Session) error { return m.DeleteWithTx(c, s, id) }) require.NoError(t, err) _, err = m.FindOne(ctx, id) require.ErrorIs(t, err, ErrNotFound) } // TC-0332: TransactCtx fn 返回错误时回滚 func TestSysDeptModel_TransactCtx_Rollback(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() uniq := "txrb2_" + testutil.UniqueId() d := &SysDept{ ParentId: 0, Name: uniq, Path: "/" + uniq + "/", Sort: 1, Status: 1, CreateTime: now, UpdateTime: now, } err := m.TransactCtx(ctx, func(c context.Context, s sqlx.Session) error { if _, e := m.InsertWithTx(c, s, d); e != nil { return e } return errors.New("force rollback") }) require.Error(t, err) require.Contains(t, err.Error(), "force rollback") all, err := m.FindAll(ctx) require.NoError(t, err) for _, row := range all { assert.NotEqual(t, uniq, row.Name) } } // TC-0343: BatchUpdate 空 func TestSysDeptModel_BatchUpdate_Empty(t *testing.T) { conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) require.NoError(t, m.BatchUpdate(context.Background(), nil)) require.NoError(t, m.BatchUpdate(context.Background(), []*SysDept{})) } // TC-0345: BatchUpdate 多条 func TestSysDeptModel_BatchUpdate_Multi(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() tag := testutil.UniqueId() d1 := &SysDept{ParentId: 0, Name: "bu1_" + tag, Path: "/bu/" + tag + "/1/", Sort: 1, Remark: "r1", Status: 1, CreateTime: now, UpdateTime: now} d2 := &SysDept{ParentId: 0, Name: "bu2_" + tag, Path: "/bu/" + tag + "/2/", Sort: 2, Remark: "r2", Status: 1, CreateTime: now, UpdateTime: now} r1, err := m.Insert(ctx, d1) require.NoError(t, err) id1, _ := r1.LastInsertId() r2, err := m.Insert(ctx, d2) require.NoError(t, err) id2, _ := r2.LastInsertId() tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, id1, id2) }) now2 := time.Now().Unix() upd := []*SysDept{ {Id: id1, ParentId: 0, Name: "bu1_upd", Path: d1.Path, Sort: 10, Remark: "updated", Status: 1, CreateTime: now, UpdateTime: now2}, {Id: id2, ParentId: 0, Name: "bu2_upd", Path: d2.Path, Sort: 20, Remark: "updated", Status: 2, CreateTime: now, UpdateTime: now2}, } require.NoError(t, m.BatchUpdate(ctx, upd)) g1, err := m.FindOne(ctx, id1) require.NoError(t, err) assert.Equal(t, "bu1_upd", g1.Name) assert.Equal(t, int64(10), g1.Sort) g2, err := m.FindOne(ctx, id2) require.NoError(t, err) assert.Equal(t, "bu2_upd", g2.Name) assert.Equal(t, int64(2), g2.Status) } // TC-0335: BatchInsert 单条 func TestSysDeptModel_BatchInsert_Single(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() uniq := "bis_" + testutil.UniqueId() d := &SysDept{ParentId: 0, Name: uniq, Path: "/" + uniq + "/", Sort: 1, Status: 1, CreateTime: now, UpdateTime: now} require.NoError(t, m.BatchInsert(ctx, []*SysDept{d})) all, err := m.FindAll(ctx) require.NoError(t, err) var id int64 for _, row := range all { if row.Name == uniq { id = row.Id break } } require.NotZero(t, id) tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, id) }) } // TC-0341: BatchInsertWithTx 正常 func TestSysDeptModel_BatchInsertWithTx_Normal(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() tag := testutil.UniqueId() batch := []*SysDept{ {ParentId: 0, Name: "bitx1_" + tag, Path: "/bitx/" + tag + "/1/", Sort: 1, Status: 1, CreateTime: now, UpdateTime: now}, {ParentId: 0, Name: "bitx2_" + tag, Path: "/bitx/" + tag + "/2/", Sort: 2, Status: 1, CreateTime: now, UpdateTime: now}, } err := m.TransactCtx(ctx, func(c context.Context, s sqlx.Session) error { return m.BatchInsertWithTx(c, s, batch) }) require.NoError(t, err) all, err := m.FindAll(ctx) require.NoError(t, err) wanted := map[string]struct{}{batch[0].Name: {}, batch[1].Name: {}} var ids []int64 for _, row := range all { if _, ok := wanted[row.Name]; ok { ids = append(ids, row.Id) } } require.Len(t, ids, 2) tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, ids...) }) } // TC-0340: BatchInsertWithTx 空 func TestSysDeptModel_BatchInsertWithTx_Empty(t *testing.T) { conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) err := m.TransactCtx(context.Background(), func(c context.Context, s sqlx.Session) error { return m.BatchInsertWithTx(c, s, nil) }) require.NoError(t, err) } // TC-0342: BatchInsertWithTx 回滚 func TestSysDeptModel_BatchInsertWithTx_Rollback(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() uniq := "rbn_" + testutil.UniqueId() batch := []*SysDept{ {ParentId: 0, Name: uniq, Path: "/rbn/" + uniq + "/", Sort: 1, Status: 1, CreateTime: now, UpdateTime: now}, } err := m.TransactCtx(ctx, func(c context.Context, s sqlx.Session) error { if e := m.BatchInsertWithTx(c, s, batch); e != nil { return e } return errors.New("force rollback") }) require.Error(t, err) all, err := m.FindAll(ctx) require.NoError(t, err) for _, row := range all { assert.NotEqual(t, uniq, row.Name) } } // TC-0349: BatchUpdateWithTx 正常 func TestSysDeptModel_BatchUpdateWithTx_Normal(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() tag := testutil.UniqueId() d1 := &SysDept{ParentId: 0, Name: "butx1_" + tag, Path: "/butx/" + tag + "/1/", Sort: 1, Remark: "old", Status: 1, CreateTime: now, UpdateTime: now} d2 := &SysDept{ParentId: 0, Name: "butx2_" + tag, Path: "/butx/" + tag + "/2/", Sort: 2, Remark: "old", Status: 1, CreateTime: now, UpdateTime: now} r1, err := m.Insert(ctx, d1) require.NoError(t, err) id1, _ := r1.LastInsertId() r2, err := m.Insert(ctx, d2) require.NoError(t, err) id2, _ := r2.LastInsertId() tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, id1, id2) }) now2 := time.Now().Unix() err = m.TransactCtx(ctx, func(c context.Context, s sqlx.Session) error { return m.BatchUpdateWithTx(c, s, []*SysDept{ {Id: id1, ParentId: 0, Name: "butx1_new", Path: d1.Path, Sort: 10, Remark: "new", Status: 1, CreateTime: now, UpdateTime: now2}, {Id: id2, ParentId: 0, Name: "butx2_new", Path: d2.Path, Sort: 20, Remark: "new", Status: 1, CreateTime: now, UpdateTime: now2}, }) }) require.NoError(t, err) g1, err := m.FindOne(ctx, id1) require.NoError(t, err) assert.Equal(t, "butx1_new", g1.Name) assert.Equal(t, int64(10), g1.Sort) g2, err := m.FindOne(ctx, id2) require.NoError(t, err) assert.Equal(t, "butx2_new", g2.Name) } // TC-0348: BatchUpdateWithTx 空 func TestSysDeptModel_BatchUpdateWithTx_Empty(t *testing.T) { conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) err := m.TransactCtx(context.Background(), func(c context.Context, s sqlx.Session) error { return m.BatchUpdateWithTx(c, s, nil) }) require.NoError(t, err) } // TC-0354: BatchDelete 单条 func TestSysDeptModel_BatchDelete_Single(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() d := &SysDept{ParentId: 0, Name: "bds_" + testutil.UniqueId(), Path: "/bds/" + testutil.UniqueId() + "/", Sort: 1, Status: 1, CreateTime: now, UpdateTime: now} res, err := m.Insert(ctx, d) require.NoError(t, err) id, _ := res.LastInsertId() tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, id) }) require.NoError(t, m.BatchDelete(ctx, []int64{id})) _, err = m.FindOne(ctx, id) require.ErrorIs(t, err, ErrNotFound) } // TC-0356: BatchDelete 包含不存在 id func TestSysDeptModel_BatchDelete_ContainsNonExist(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() d := &SysDept{ParentId: 0, Name: "bdne_" + testutil.UniqueId(), Path: "/bdne/" + testutil.UniqueId() + "/", Sort: 1, Status: 1, CreateTime: now, UpdateTime: now} res, err := m.Insert(ctx, d) require.NoError(t, err) id, _ := res.LastInsertId() tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, id) }) require.NoError(t, m.BatchDelete(ctx, []int64{id, 999999999})) _, err = m.FindOne(ctx, id) require.ErrorIs(t, err, ErrNotFound) } // TC-0358: BatchDeleteWithTx 正常 func TestSysDeptModel_BatchDeleteWithTx_Normal(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() tag := testutil.UniqueId() d1 := &SysDept{ParentId: 0, Name: "bdtx1_" + tag, Path: "/bdtx/" + tag + "/1/", Sort: 1, Status: 1, CreateTime: now, UpdateTime: now} d2 := &SysDept{ParentId: 0, Name: "bdtx2_" + tag, Path: "/bdtx/" + tag + "/2/", Sort: 2, Status: 1, CreateTime: now, UpdateTime: now} r1, err := m.Insert(ctx, d1) require.NoError(t, err) id1, _ := r1.LastInsertId() r2, err := m.Insert(ctx, d2) require.NoError(t, err) id2, _ := r2.LastInsertId() tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, id1, id2) }) err = m.TransactCtx(ctx, func(c context.Context, s sqlx.Session) error { return m.BatchDeleteWithTx(c, s, []int64{id1, id2}) }) require.NoError(t, err) _, err = m.FindOne(ctx, id1) require.ErrorIs(t, err, ErrNotFound) _, err = m.FindOne(ctx, id2) require.ErrorIs(t, err, ErrNotFound) } // TC-0357: BatchDeleteWithTx 空 func TestSysDeptModel_BatchDeleteWithTx_Empty(t *testing.T) { conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) err := m.TransactCtx(context.Background(), func(c context.Context, s sqlx.Session) error { return m.BatchDeleteWithTx(c, s, nil) }) require.NoError(t, err) } // TC-0323: 事务内 FindOne func TestSysDeptModel_FindOneWithTx_InsertThenFind(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) now := time.Now().Unix() var foundInTx *SysDept var insertedId int64 err := m.TransactCtx(ctx, func(c context.Context, s sqlx.Session) error { res, err := m.InsertWithTx(c, s, &SysDept{ ParentId: 0, Name: "ftx_" + testutil.UniqueId(), Path: "/ftx/" + testutil.UniqueId() + "/", Sort: 1, DeptType: "NORMAL", Status: 1, CreateTime: now, UpdateTime: now, }) if err != nil { return err } insertedId, _ = res.LastInsertId() foundInTx, err = m.FindOneWithTx(c, s, insertedId) return err }) require.NoError(t, err) tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, insertedId) }) require.NotNil(t, foundInTx) assert.Equal(t, insertedId, foundInTx.Id) } // TC-0322: FindOneWithTx 不存在 func TestSysDeptModel_FindOneWithTx_NotFound(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) err := m.TransactCtx(ctx, func(c context.Context, s sqlx.Session) error { _, err := m.FindOneWithTx(c, s, 999999999999) return err }) require.ErrorIs(t, err, ErrNotFound) } func TestSysDeptModel_UpdateWithOptLock_ConcurrentSingleWinner(t *testing.T) { ctx := context.Background() conn := testutil.GetTestSqlConn() m := NewSysDeptModel(conn, testutil.GetTestCacheConf(), testutil.GetTestCachePrefix()) base := time.Now().Unix() row := &SysDept{ ParentId: 0, Name: "dept_optlock_" + testutil.UniqueId(), Path: "/optlock/" + testutil.UniqueId() + "/", Sort: 10, Remark: "orig", Status: 1, CreateTime: base, UpdateTime: base, } res, err := m.Insert(ctx, row) require.NoError(t, err) id, err := res.LastInsertId() require.NoError(t, err) tbl := m.TableName() t.Cleanup(func() { testutil.CleanTable(ctx, conn, tbl, id) }) const workers = 10 var ( wg sync.WaitGroup success int32 conflicts int32 other int32 start = make(chan struct{}) ) for i := 0; i < workers; i++ { wg.Add(1) go func(idx int) { defer wg.Done() <-start err := m.UpdateWithOptLock(ctx, &SysDept{ Id: id, ParentId: 0, Name: row.Name, Path: row.Path, Sort: int64(idx), Remark: "w" + testutil.UniqueId(), DeptType: "NORMAL", Status: 1, CreateTime: base, UpdateTime: base + int64(idx+1), }, base) switch { case err == nil: atomic.AddInt32(&success, 1) case errors.Is(err, ErrUpdateConflict): atomic.AddInt32(&conflicts, 1) default: atomic.AddInt32(&other, 1) t.Errorf("unexpected error: %v", err) } }(i) } close(start) wg.Wait() assert.Equal(t, int32(1), atomic.LoadInt32(&success), "10 个并发写必须且仅有 1 个成功, 实际 %d", success) assert.Equal(t, int32(workers-1), atomic.LoadInt32(&conflicts), "其余 goroutine 必须全部得到 ErrUpdateConflict (无声覆盖即 BUG)") assert.Equal(t, int32(0), atomic.LoadInt32(&other), "不应出现除成功/冲突外的其他错误") after, err := m.FindOne(ctx, id) require.NoError(t, err) assert.NotEqual(t, base, after.UpdateTime, "成功的那一个必须把 UpdateTime 推进, DB 里不允许停留在初值") assert.Equal(t, "orig", row.Remark) assert.NotEqual(t, "orig", after.Remark, "胜出者必须把 Remark 更新为 w*") }