package user import ( "context" "errors" "testing" "time" "perms-system-server/internal/consts" "perms-system-server/internal/loaders" deptModel "perms-system-server/internal/model/dept" userModel "perms-system-server/internal/model/user" "perms-system-server/internal/response" "perms-system-server/internal/svc" "perms-system-server/internal/testutil" "perms-system-server/internal/testutil/ctxhelper" "perms-system-server/internal/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func insertTestDept(t *testing.T, ctx context.Context, svcCtx *svc.ServiceContext) int64 { t.Helper() now := time.Now().Unix() res, err := svcCtx.SysDeptModel.Insert(ctx, &deptModel.SysDept{ Name: testutil.UniqueId(), ParentId: 0, Path: "/", Sort: 0, DeptType: "NORMAL", Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) id, _ := res.LastInsertId() return id } // TC-0156: 正常更新 func TestUpdateUser_Success(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) deptId := insertTestDept(t, ctx, svcCtx) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Nickname: strPtr("新昵称"), Email: strPtr("new@example.com"), Phone: strPtr("13900139000"), Remark: strPtr("更新备注"), DeptId: int64Ptr(deptId), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, "新昵称", user.Nickname) assert.Equal(t, "new@example.com", user.Email) assert.Equal(t, "13900139000", user.Phone) assert.Equal(t, "更新备注", user.Remark) assert.Equal(t, deptId, user.DeptId) } // TC-0157: 不存在 func TestUpdateUser_NotFound(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: 999999999, Nickname: strPtr("ghost"), }) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 404, codeErr.Code()) assert.Equal(t, "用户不存在", codeErr.Error()) } // TC-0158: 仅传id func TestUpdateUser_OnlyId_NothingChanges(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) // 真实使用场景: 用户打开编辑页面后再提交, 插入与更新之间存在时间间隔, // updateTime 粒度为秒级, 因此在 INSERT 之后等待 1 秒再发起 UPDATE, 避免同秒 no-op SQL 被 MySQL 视为 0 affected rows. time.Sleep(1100 * time.Millisecond) before, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) logic := NewUpdateUserLogic(ctx, svcCtx) err = logic.UpdateUser(&types.UpdateUserReq{Id: userId}) require.NoError(t, err) after, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, before.Nickname, after.Nickname) assert.Equal(t, before.Email, after.Email) assert.Equal(t, before.Phone, after.Phone) assert.Equal(t, before.DeptId, after.DeptId) } // TC-0159: 清空nickname func TestUpdateUser_ClearNickname(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Nickname: strPtr(""), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, "", user.Nickname) } // TC-0160: 清空email func TestUpdateUser_ClearEmail(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Email: strPtr(""), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, "", user.Email) } // TC-0162: 非法email格式 func TestUpdateUser_InvalidEmail(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Email: strPtr("bad-email"), }) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Equal(t, "邮箱格式不正确", codeErr.Error()) } // TC-0166: DeptId设为0(取消部门) func TestUpdateUser_DeptIdZero_Clear(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) deptId := insertTestDept(t, ctx, svcCtx) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, DeptId: int64Ptr(deptId), }) require.NoError(t, err) err = logic.UpdateUser(&types.UpdateUserReq{ Id: userId, DeptId: int64Ptr(0), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, int64(0), user.DeptId) } // TC-0167: DeptId设为正值 func TestUpdateUser_DeptIdSet(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) deptId := insertTestDept(t, ctx, svcCtx) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, DeptId: int64Ptr(deptId), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, deptId, user.DeptId) } // TC-0168: DeptId不传(nil) func TestUpdateUser_NilDeptId_Unchanged(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) deptId := insertTestDept(t, ctx, svcCtx) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, DeptId: int64Ptr(deptId), }) require.NoError(t, err) err = logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Nickname: strPtr("changed"), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, deptId, user.DeptId) assert.Equal(t, "changed", user.Nickname) } // TC-0161: 清空remark func TestUpdateUser_ClearRemark(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Remark: strPtr("some remark"), }) require.NoError(t, err) err = logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Remark: strPtr(""), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, "", user.Remark) } // TC-0164: 合法phone func TestUpdateUser_ValidInternationalPhone(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Phone: strPtr("+8613800138000"), }) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, "+8613800138000", user.Phone) } // TC-0163: 非法phone格式 func TestUpdateUser_InvalidPhone(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Phone: strPtr("12345"), }) require.Error(t, err) var codeErr *response.CodeError require.True(t, errors.As(err, &codeErr)) assert.Equal(t, 400, codeErr.Code()) assert.Equal(t, "手机号格式不正确", codeErr.Error()) } // TC-0165: 不传email(nil) func TestUpdateUser_NilEmail_Unchanged(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) before, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) originalEmail := before.Email logic := NewUpdateUserLogic(ctx, svcCtx) err = logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Nickname: strPtr("changed-nick"), }) require.NoError(t, err) after, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, originalEmail, after.Email) assert.Equal(t, "changed-nick", after.Nickname) } // TC-0542: MEMBER用户尝试修改其他用户被CheckManageAccess拒绝 func TestUpdateUser_MemberCannotManageOtherUser(t *testing.T) { svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() superCtx := ctxhelper.SuperAdminCtx() targetName := testutil.UniqueId() targetId := insertTestUser(t, superCtx, targetName, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(superCtx, conn, "`sys_user`", targetId) }) ctx := ctxhelper.MemberCtx("test_product") logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: targetId, Nickname: strPtr("hacked")}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) } // TC-0170: 产品管理员可以修改其管理范围内的用户信息 func TestUpdateUser_ProductAdminCanManageUser(t *testing.T) { svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() superCtx := ctxhelper.SuperAdminCtx() deptId := insertTestDept(t, superCtx, svcCtx) t.Cleanup(func() { testutil.CleanTable(superCtx, conn, "`sys_dept`", deptId) }) targetName := testutil.UniqueId() targetId := insertTestUser(t, superCtx, targetName, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(superCtx, conn, "`sys_user`", targetId) }) productCode := "test_product" mId := insertTestMember(t, svcCtx, productCode, targetId) t.Cleanup(func() { testutil.CleanTable(superCtx, conn, "`sys_product_member`", mId) }) ctx := ctxhelper.AdminCtx(productCode) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: targetId, Nickname: strPtr("new-nick")}) require.NoError(t, err) user, err := svcCtx.SysUserModel.FindOne(superCtx, targetId) require.NoError(t, err) assert.Equal(t, "new-nick", user.Nickname) } // TC-0171: UpdateUser 昵称超过64字符被拒绝 func TestUpdateUser_NicknameTooLong(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) longNick := string(make([]byte, 65)) for i := range longNick { longNick = longNick[:i] + "a" + longNick[i+1:] } logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: userId, Nickname: &longNick}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 400, ce.Code()) assert.Contains(t, ce.Error(), "昵称长度不能超过64个字符") } // TC-0172: UpdateUser 部门不存在被拒绝 func TestUpdateUser_DeptNotExists(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: userId, DeptId: int64Ptr(999999999)}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 400, ce.Code()) assert.Contains(t, ce.Error(), "部门不存在") } // TC-0543: updateUser自己修改DeptId被拒绝 func TestUpdateUser_SelfEditDeptIdRejected(t *testing.T) { ctx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: 100, Username: "self_user", Status: consts.StatusEnabled, }) svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: 100, DeptId: int64Ptr(5)}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) assert.Equal(t, "不允许修改自己的部门和状态", ce.Error()) } // TC-0544: updateUser自己修改Status被拒绝 func TestUpdateUser_SelfEditStatusRejected(t *testing.T) { ctx := ctxhelper.CustomCtx(&loaders.UserDetails{ UserId: 100, Username: "self_user", Status: consts.StatusEnabled, }) svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: 100, Status: 2}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) assert.Equal(t, "不允许修改自己的部门和状态", ce.Error()) } // TC-0545: updateUser未登录被拒绝 func TestUpdateUser_NotLoggedInRejected(t *testing.T) { ctx := context.Background() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) logic := NewUpdateUserLogic(ctx, svcCtx) err := logic.UpdateUser(&types.UpdateUserReq{Id: 1, Nickname: strPtr("hacked")}) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 401, ce.Code()) assert.Equal(t, "未登录", ce.Error()) } // TC-0169: 超管A通过updateUser修改超管B的状态被拒绝(H-2修复验证) func TestUpdateUser_SuperAdminCannotFreezeOtherSuperAdmin(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() now := time.Now().Unix() uRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{ Username: testutil.UniqueId(), Password: testutil.HashPassword("pass"), Nickname: "super_b", IsSuperAdmin: consts.IsSuperAdminYes, MustChangePassword: 2, Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) require.NoError(t, err) superBId, _ := uRes.LastInsertId() t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", superBId) }) logic := NewUpdateUserLogic(ctx, svcCtx) err = logic.UpdateUser(&types.UpdateUserReq{ Id: superBId, Status: consts.StatusDisabled, }) require.Error(t, err) var ce *response.CodeError require.True(t, errors.As(err, &ce)) assert.Equal(t, 403, ce.Code()) assert.Equal(t, "不能通过此接口修改其他超级管理员的状态和部门", ce.Error()) user, err := svcCtx.SysUserModel.FindOne(ctx, superBId) require.NoError(t, err) assert.Equal(t, int64(consts.StatusEnabled), user.Status, "超管B的状态不应被修改") } // TC-0173: updateUser 修改状态时会递增 tokenVersion(H-1修复验证) func TestUpdateUser_StatusChange_IncrementsTokenVersion(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) before, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) origTv := before.TokenVersion logic := NewUpdateUserLogic(ctx, svcCtx) err = logic.UpdateUser(&types.UpdateUserReq{Id: userId, Status: consts.StatusDisabled}) require.NoError(t, err) after, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, int64(consts.StatusDisabled), after.Status) assert.Equal(t, origTv+1, after.TokenVersion, "状态变化应递增 tokenVersion") } // TC-0174: updateUser 只改 profile 不会递增 tokenVersion func TestUpdateUser_ProfileOnly_NoTokenVersionChange(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) before, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) logic := NewUpdateUserLogic(ctx, svcCtx) err = logic.UpdateUser(&types.UpdateUserReq{ Id: userId, Nickname: strPtr("新名字"), Email: strPtr("new@example.com"), }) require.NoError(t, err) after, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) assert.Equal(t, "新名字", after.Nickname) assert.Equal(t, before.TokenVersion, after.TokenVersion, "不改状态时 tokenVersion 不应变化") } // TC-0175: updateUser 乐观锁冲突 -> 409 // 乐观锁依赖秒级 updateTime, 需在两次更新之间保证 >= 1 秒的间隔, 否则 MySQL 看到的新/旧 updateTime 相同无法生效. func TestUpdateUser_OptimisticLockConflict_Returns409(t *testing.T) { ctx := ctxhelper.SuperAdminCtx() svcCtx := svc.NewServiceContext(testutil.GetTestConfig()) conn := testutil.GetTestSqlConn() username := testutil.UniqueId() userId := insertTestUser(t, ctx, username, testutil.HashPassword("pass")) t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) }) orig, err := svcCtx.SysUserModel.FindOne(ctx, userId) require.NoError(t, err) time.Sleep(1100 * time.Millisecond) logic := NewUpdateUserLogic(ctx, svcCtx) err = logic.UpdateUser(&types.UpdateUserReq{Id: userId, Nickname: strPtr("first")}) require.NoError(t, err) err = svcCtx.SysUserModel.UpdateProfile(ctx, userId, orig.Username, "second", orig.Email, orig.Phone, orig.Remark, orig.DeptId, orig.Status, false, orig.UpdateTime) require.ErrorIs(t, err, userModel.ErrUpdateConflict, "基于旧 updateTime 的更新应失败") }