package dept import ( "context" "errors" "sync" "sync/atomic" "testing" "time" "perms-system-server/internal/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TC-0759: M-5 修复实战回归 —— UpdateWithOptLock 在 10 个 goroutine 同时写同一行时, // 必须精确有 **1 个成功、9 个 ErrUpdateConflict**。 // 旧实现依赖应用层 "先读后写", 高并发下两个 goroutine 都拿到 UpdateTime=t0 就都会写入成功 // (最后一个覆盖前一个, 数据静默丢失). 新实现 WHERE updateTime=? 才更新, affected=0 即冲突。 // 这是"真实并发"断言, 不是 mock, 踩在实际 MySQL 上, 为乐观锁修复提供最强的防退化护栏。 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), "M-5: 10 个并发写必须且仅有 1 个成功, 实际 %d", success) assert.Equal(t, int32(workers-1), atomic.LoadInt32(&conflicts), "M-5: 其余 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*") }