Commit 85f2928aa51e1670f1f2f69fa310f18d0872949f

Authored by YunaiV
1 parent 89a6c642

fix(crm): 修复管理员转移数据时旧负责人权限处理

yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java
@@ -206,41 +206,47 @@ public class CrmPermissionServiceImpl implements CrmPermissionService { @@ -206,41 +206,47 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
206 @Override 206 @Override
207 @Transactional(rollbackFor = Exception.class) 207 @Transactional(rollbackFor = Exception.class)
208 public void transferPermission(CrmPermissionTransferReqBO transferReqBO) { 208 public void transferPermission(CrmPermissionTransferReqBO transferReqBO) {
209 - // 1. 校验数据权限:是否是负责人,只有负责人才可以转移  
210 - CrmPermissionDO oldPermission = permissionMapper.selectByBizTypeAndBizIdByUserId(  
211 - transferReqBO.getBizType(), transferReqBO.getBizId(), transferReqBO.getUserId());  
212 String bizTypeName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType()); 209 String bizTypeName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType());
213 - if ((oldPermission == null || !isOwner(oldPermission.getLevel()))  
214 - && !CrmPermissionUtils.isCrmAdmin()) { // 并且不是超管 210 + List<CrmPermissionDO> permissions = permissionMapper.selectByBizTypeAndBizId(
  211 + transferReqBO.getBizType(), transferReqBO.getBizId()); // 获得所有数据权限
  212 + // 1.1 校验数据权限:只有负责人,或者 CRM 管理员,才可以转移
  213 + CrmPermissionDO userPermission = CollUtil.findOne(permissions,
  214 + item -> ObjUtil.equal(item.getUserId(), transferReqBO.getUserId()));
  215 + boolean owner = userPermission != null && isOwner(userPermission.getLevel());
  216 + if (!owner && !CrmPermissionUtils.isCrmAdmin()) {
215 throw exception(CRM_PERMISSION_DENIED, bizTypeName); 217 throw exception(CRM_PERMISSION_DENIED, bizTypeName);
216 } 218 }
217 - // 1.1 校验转移对象是否已经是该负责人  
218 - if (oldPermission != null && ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) { 219 + // 1.2 获得老负责人的权限:操作人是负责人时,就是操作人的权限;否则(CRM 管理员),是数据实际的负责人,而不是管理员自己的团队权限
  220 + CrmPermissionDO oldOwnerPermission = owner ? userPermission
  221 + : CollUtil.findOne(permissions, item -> isOwner(item.getLevel()));
  222 + if (oldOwnerPermission == null) { // 数据缺少负责人时,在修改任何权限前失败,避免部分写入
  223 + throw exception(CRM_PERMISSION_NOT_EXISTS);
  224 + }
  225 + // 1.3 校验转移对象是否已经是该负责人
  226 + if (ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldOwnerPermission.getUserId())) {
219 throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, bizTypeName); 227 throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, bizTypeName);
220 } 228 }
221 - // 1.2 校验新负责人是否存在 229 + // 1.4 校验新负责人是否存在
222 adminUserApi.validateUserList(Collections.singletonList(transferReqBO.getNewOwnerUserId())); 230 adminUserApi.validateUserList(Collections.singletonList(transferReqBO.getNewOwnerUserId()));
223 231
224 - // 2. 修改新负责人的权限  
225 - List<CrmPermissionDO> permissions = permissionMapper.selectByBizTypeAndBizId(  
226 - transferReqBO.getBizType(), transferReqBO.getBizId()); // 获得所有数据权限  
227 - CrmPermissionDO permission = CollUtil.findOne(permissions, 232 + // 2. 修改新负责人的权限:已在团队中时,提升为负责人,不重复新增
  233 + CrmPermissionDO newOwnerPermission = CollUtil.findOne(permissions,
228 item -> ObjUtil.equal(item.getUserId(), transferReqBO.getNewOwnerUserId())); 234 item -> ObjUtil.equal(item.getUserId(), transferReqBO.getNewOwnerUserId()));
229 - if (permission == null) { 235 + if (newOwnerPermission == null) {
230 permissionMapper.insert(new CrmPermissionDO().setBizType(transferReqBO.getBizType()) 236 permissionMapper.insert(new CrmPermissionDO().setBizType(transferReqBO.getBizType())
231 .setBizId(transferReqBO.getBizId()).setUserId(transferReqBO.getNewOwnerUserId()) 237 .setBizId(transferReqBO.getBizId()).setUserId(transferReqBO.getNewOwnerUserId())
232 .setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); 238 .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
233 } else { 239 } else {
234 - permissionMapper.updateById(new CrmPermissionDO().setId(permission.getId()) 240 + permissionMapper.updateById(new CrmPermissionDO().setId(newOwnerPermission.getId())
235 .setLevel(CrmPermissionLevelEnum.OWNER.getLevel())); 241 .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
236 } 242 }
237 243
238 // 3. 修改老负责人的权限 244 // 3. 修改老负责人的权限
239 if (transferReqBO.getOldOwnerPermissionLevel() != null) { 245 if (transferReqBO.getOldOwnerPermissionLevel() != null) {
240 - permissionMapper.updateById(new CrmPermissionDO().setId(oldPermission.getId()) 246 + permissionMapper.updateById(new CrmPermissionDO().setId(oldOwnerPermission.getId())
241 .setLevel(transferReqBO.getOldOwnerPermissionLevel())); 247 .setLevel(transferReqBO.getOldOwnerPermissionLevel()));
242 } else { 248 } else {
243 - permissionMapper.deleteById(oldPermission.getId()); 249 + permissionMapper.deleteById(oldOwnerPermission.getId());
244 } 250 }
245 } 251 }
246 252
yudao-module-crm/src/test/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.service.permission;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
  5 +import cn.iocoder.yudao.framework.security.core.LoginUser;
  6 +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
  7 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  8 +import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
  9 +import cn.iocoder.yudao.module.crm.dal.mysql.permission.CrmPermissionMapper;
  10 +import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
  11 +import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
  12 +import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
  13 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  14 +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum;
  15 +import org.junit.jupiter.api.AfterEach;
  16 +import org.junit.jupiter.api.BeforeEach;
  17 +import org.junit.jupiter.api.Test;
  18 +import org.mockito.InjectMocks;
  19 +import org.mockito.Mock;
  20 +import org.mockito.MockedStatic;
  21 +import org.springframework.mock.web.MockHttpServletRequest;
  22 +import org.springframework.security.core.context.SecurityContextHolder;
  23 +
  24 +import java.util.Arrays;
  25 +import java.util.Collections;
  26 +import java.util.List;
  27 +import java.util.Objects;
  28 +
  29 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  30 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  31 +import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.*;
  32 +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_NOT_EXISTS;
  33 +import static org.mockito.ArgumentMatchers.argThat;
  34 +import static org.mockito.Mockito.*;
  35 +
  36 +/**
  37 + * {@link CrmPermissionServiceImpl} 的单元测试类
  38 + *
  39 + * 重点覆盖 {@link CrmPermissionServiceImpl#transferPermission(CrmPermissionTransferReqBO)}:区分操作人的权限、数据实际的负责人
  40 + *
  41 + * @author 芋道源码
  42 + */
  43 +public class CrmPermissionServiceImplTest extends BaseMockitoUnitTest {
  44 +
  45 + private static final Integer BIZ_TYPE = CrmBizTypeEnum.CRM_CUSTOMER.getType();
  46 + private static final String BIZ_TYPE_NAME = CrmBizTypeEnum.CRM_CUSTOMER.getName();
  47 + private static final Long BIZ_ID = 10L;
  48 +
  49 + private static final Long LOGIN_USER_ID = 100L;
  50 + private static final Long OWNER_USER_ID = 200L;
  51 + private static final Long NEW_OWNER_USER_ID = 300L;
  52 +
  53 + private static final Long OWNER_PERMISSION_ID = 1L;
  54 + private static final Long LOGIN_USER_PERMISSION_ID = 2L;
  55 + private static final Long NEW_OWNER_PERMISSION_ID = 3L;
  56 +
  57 + @InjectMocks
  58 + private CrmPermissionServiceImpl permissionService;
  59 +
  60 + @Mock
  61 + private CrmPermissionMapper permissionMapper;
  62 + @Mock
  63 + private AdminUserApi adminUserApi;
  64 + @Mock
  65 + private PermissionCommonApi permissionCommonApi;
  66 +
  67 + private MockedStatic<SpringUtil> springUtilMockedStatic;
  68 +
  69 + @BeforeEach
  70 + public void setUp() {
  71 + // mock CrmPermissionUtils#isCrmAdmin() 使用的 PermissionCommonApi,在 mockCrmAdmin 方法中按需 mock 返回值
  72 + springUtilMockedStatic = mockStatic(SpringUtil.class);
  73 + // mock 登录用户
  74 + SecurityFrameworkUtils.setLoginUser(new LoginUser().setId(LOGIN_USER_ID), new MockHttpServletRequest());
  75 + }
  76 +
  77 + @AfterEach
  78 + public void tearDown() {
  79 + springUtilMockedStatic.close();
  80 + SecurityContextHolder.clearContext();
  81 + }
  82 +
  83 + // ========== 负责人转移 ==========
  84 +
  85 + @Test
  86 + public void testTransferPermission_ownerRemoveOldOwner() {
  87 + // mock 数据:当前用户是负责人,新负责人不在团队中
  88 + mockPermissions(buildPermission(LOGIN_USER_PERMISSION_ID, LOGIN_USER_ID, CrmPermissionLevelEnum.OWNER));
  89 +
  90 + // 调用:老负责人移除
  91 + permissionService.transferPermission(buildTransferReqBO(NEW_OWNER_USER_ID, null));
  92 + // 断言:新增新负责人;删除老负责人(当前用户)
  93 + verify(adminUserApi).validateUserList(Collections.singletonList(NEW_OWNER_USER_ID));
  94 + verify(permissionMapper).selectByBizTypeAndBizId(BIZ_TYPE, BIZ_ID);
  95 + verifyNewOwnerInserted();
  96 + verify(permissionMapper).deleteById(LOGIN_USER_PERMISSION_ID);
  97 + verifyNoMoreInteractions(permissionMapper);
  98 + }
  99 +
  100 + @Test
  101 + public void testTransferPermission_ownerKeepOldOwnerAndPromoteTeamMember() {
  102 + // mock 数据:当前用户是负责人,新负责人已有 READ 团队权限
  103 + mockPermissions(buildPermission(LOGIN_USER_PERMISSION_ID, LOGIN_USER_ID, CrmPermissionLevelEnum.OWNER),
  104 + buildPermission(NEW_OWNER_PERMISSION_ID, NEW_OWNER_USER_ID, CrmPermissionLevelEnum.READ));
  105 +
  106 + // 调用:老负责人保留为 WRITE
  107 + permissionService.transferPermission(buildTransferReqBO(NEW_OWNER_USER_ID, CrmPermissionLevelEnum.WRITE.getLevel()));
  108 + // 断言:新负责人的团队权限提升为负责人,不重复新增;老负责人(当前用户)改为 WRITE
  109 + verify(permissionMapper).selectByBizTypeAndBizId(BIZ_TYPE, BIZ_ID);
  110 + verifyLevelUpdated(NEW_OWNER_PERMISSION_ID, CrmPermissionLevelEnum.OWNER);
  111 + verifyLevelUpdated(LOGIN_USER_PERMISSION_ID, CrmPermissionLevelEnum.WRITE);
  112 + verifyNoMoreInteractions(permissionMapper);
  113 + }
  114 +
  115 + @Test
  116 + public void testTransferPermission_ownerToSelf() {
  117 + // mock 数据:当前用户是负责人
  118 + mockPermissions(buildPermission(LOGIN_USER_PERMISSION_ID, LOGIN_USER_ID, CrmPermissionLevelEnum.OWNER));
  119 +
  120 + // 调用,并断言异常:转移给自己
  121 + assertServiceException(() -> permissionService.transferPermission(buildTransferReqBO(LOGIN_USER_ID, null)),
  122 + CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, BIZ_TYPE_NAME);
  123 + assertNoPermissionWrite();
  124 + verifyNoInteractions(adminUserApi);
  125 + }
  126 +
  127 + // ========== 非负责人、非 CRM 管理员 ==========
  128 +
  129 + @Test
  130 + public void testTransferPermission_teamMemberDenied() {
  131 + // mock 数据:当前用户只有 WRITE 团队权限,不是 CRM 管理员
  132 + mockPermissions(buildPermission(OWNER_PERMISSION_ID, OWNER_USER_ID, CrmPermissionLevelEnum.OWNER),
  133 + buildPermission(LOGIN_USER_PERMISSION_ID, LOGIN_USER_ID, CrmPermissionLevelEnum.WRITE));
  134 + mockCrmAdmin(false);
  135 +
  136 + // 调用,并断言异常
  137 + assertServiceException(() -> permissionService.transferPermission(buildTransferReqBO(NEW_OWNER_USER_ID, null)),
  138 + CRM_PERMISSION_DENIED, BIZ_TYPE_NAME);
  139 + assertNoPermissionWrite();
  140 + verifyNoInteractions(adminUserApi);
  141 + }
  142 +
  143 + @Test
  144 + public void testTransferPermission_noPermissionDenied() {
  145 + // mock 数据:当前用户没有任何权限,不是 CRM 管理员
  146 + mockPermissions(buildPermission(OWNER_PERMISSION_ID, OWNER_USER_ID, CrmPermissionLevelEnum.OWNER));
  147 + mockCrmAdmin(false);
  148 +
  149 + // 调用,并断言异常
  150 + assertServiceException(() -> permissionService.transferPermission(buildTransferReqBO(NEW_OWNER_USER_ID, null)),
  151 + CRM_PERMISSION_DENIED, BIZ_TYPE_NAME);
  152 + assertNoPermissionWrite();
  153 + verifyNoInteractions(adminUserApi);
  154 + }
  155 +
  156 + // ========== CRM 管理员转移 ==========
  157 +
  158 + @Test
  159 + public void testTransferPermission_crmAdminWithoutPermission() {
  160 + // mock 数据:CRM 管理员没有权限,数据的负责人是其他人(修复前:先新增新负责人,再因 NPE 失败)
  161 + mockPermissions(buildPermission(OWNER_PERMISSION_ID, OWNER_USER_ID, CrmPermissionLevelEnum.OWNER));
  162 + mockCrmAdmin(true);
  163 +
  164 + // 调用:老负责人移除
  165 + permissionService.transferPermission(buildTransferReqBO(NEW_OWNER_USER_ID, null));
  166 + // 断言:新增新负责人;删除实际的老负责人
  167 + verify(adminUserApi).validateUserList(Collections.singletonList(NEW_OWNER_USER_ID));
  168 + verify(permissionMapper).selectByBizTypeAndBizId(BIZ_TYPE, BIZ_ID);
  169 + verifyNewOwnerInserted();
  170 + verify(permissionMapper).deleteById(OWNER_PERMISSION_ID);
  171 + verifyNoMoreInteractions(permissionMapper);
  172 + }
  173 +
  174 + @Test
  175 + public void testTransferPermission_crmAdminWithTeamPermission() {
  176 + // mock 数据:CRM 管理员有 WRITE 团队权限,数据的负责人是其他人
  177 + mockPermissions(buildPermission(OWNER_PERMISSION_ID, OWNER_USER_ID, CrmPermissionLevelEnum.OWNER),
  178 + buildPermission(LOGIN_USER_PERMISSION_ID, LOGIN_USER_ID, CrmPermissionLevelEnum.WRITE));
  179 + mockCrmAdmin(true);
  180 +
  181 + // 调用:老负责人保留为 READ
  182 + permissionService.transferPermission(buildTransferReqBO(NEW_OWNER_USER_ID, CrmPermissionLevelEnum.READ.getLevel()));
  183 + // 断言:新增新负责人;实际的老负责人改为 READ;管理员自己的团队权限不变
  184 + verify(permissionMapper).selectByBizTypeAndBizId(BIZ_TYPE, BIZ_ID);
  185 + verifyNewOwnerInserted();
  186 + verifyLevelUpdated(OWNER_PERMISSION_ID, CrmPermissionLevelEnum.READ);
  187 + verifyNoMoreInteractions(permissionMapper);
  188 + }
  189 +
  190 + @Test
  191 + public void testTransferPermission_crmAdminToSelf() {
  192 + // mock 数据:CRM 管理员有 READ 团队权限,数据的负责人是其他人
  193 + mockPermissions(buildPermission(OWNER_PERMISSION_ID, OWNER_USER_ID, CrmPermissionLevelEnum.OWNER),
  194 + buildPermission(LOGIN_USER_PERMISSION_ID, LOGIN_USER_ID, CrmPermissionLevelEnum.READ));
  195 + mockCrmAdmin(true);
  196 +
  197 + // 调用:转移给自己,老负责人移除
  198 + permissionService.transferPermission(buildTransferReqBO(LOGIN_USER_ID, null));
  199 + // 断言:管理员的团队权限提升为负责人,不重复新增;删除实际的老负责人
  200 + verify(adminUserApi).validateUserList(Collections.singletonList(LOGIN_USER_ID));
  201 + verify(permissionMapper).selectByBizTypeAndBizId(BIZ_TYPE, BIZ_ID);
  202 + verifyLevelUpdated(LOGIN_USER_PERMISSION_ID, CrmPermissionLevelEnum.OWNER);
  203 + verify(permissionMapper).deleteById(OWNER_PERMISSION_ID);
  204 + verifyNoMoreInteractions(permissionMapper);
  205 + }
  206 +
  207 + @Test
  208 + public void testTransferPermission_crmAdminNewOwnerIsOwner() {
  209 + // mock 数据:CRM 管理员没有权限,新负责人已经是负责人
  210 + mockPermissions(buildPermission(OWNER_PERMISSION_ID, OWNER_USER_ID, CrmPermissionLevelEnum.OWNER));
  211 + mockCrmAdmin(true);
  212 +
  213 + // 调用,并断言异常
  214 + assertServiceException(() -> permissionService.transferPermission(buildTransferReqBO(OWNER_USER_ID, null)),
  215 + CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, BIZ_TYPE_NAME);
  216 + assertNoPermissionWrite();
  217 + verifyNoInteractions(adminUserApi);
  218 + }
  219 +
  220 + @Test
  221 + public void testTransferPermission_crmAdminOwnerNotExists() {
  222 + // mock 数据:CRM 管理员有 WRITE 团队权限,但数据缺少负责人
  223 + mockPermissions(buildPermission(LOGIN_USER_PERMISSION_ID, LOGIN_USER_ID, CrmPermissionLevelEnum.WRITE),
  224 + buildPermission(NEW_OWNER_PERMISSION_ID, NEW_OWNER_USER_ID, CrmPermissionLevelEnum.READ));
  225 + mockCrmAdmin(true);
  226 +
  227 + // 调用,并断言异常:在修改任何权限前失败
  228 + assertServiceException(() -> permissionService.transferPermission(buildTransferReqBO(NEW_OWNER_USER_ID, null)),
  229 + CRM_PERMISSION_NOT_EXISTS);
  230 + assertNoPermissionWrite();
  231 + verifyNoInteractions(adminUserApi);
  232 + }
  233 +
  234 + @Test
  235 + public void testTransferPermission_newOwnerNotExists() {
  236 + // mock 数据:CRM 管理员没有权限,数据的负责人是其他人
  237 + mockPermissions(buildPermission(OWNER_PERMISSION_ID, OWNER_USER_ID, CrmPermissionLevelEnum.OWNER));
  238 + mockCrmAdmin(true);
  239 + // mock 方法:新负责人不存在
  240 + doThrow(exception(USER_NOT_EXISTS)).when(adminUserApi)
  241 + .validateUserList(Collections.singletonList(NEW_OWNER_USER_ID));
  242 +
  243 + // 调用,并断言异常:在修改任何权限前失败
  244 + assertServiceException(() -> permissionService.transferPermission(buildTransferReqBO(NEW_OWNER_USER_ID, null)),
  245 + USER_NOT_EXISTS);
  246 + assertNoPermissionWrite();
  247 + }
  248 +
  249 + private void mockCrmAdmin(boolean crmAdmin) {
  250 + springUtilMockedStatic.when(() -> SpringUtil.getBean(PermissionCommonApi.class)).thenReturn(permissionCommonApi);
  251 + when(permissionCommonApi.hasAnyRoles(LOGIN_USER_ID, RoleCodeEnum.CRM_ADMIN.getCode())).thenReturn(crmAdmin);
  252 + }
  253 +
  254 + private void mockPermissions(CrmPermissionDO... permissions) {
  255 + List<CrmPermissionDO> list = Arrays.asList(permissions);
  256 + when(permissionMapper.selectByBizTypeAndBizId(BIZ_TYPE, BIZ_ID)).thenReturn(list);
  257 + }
  258 +
  259 + private void verifyNewOwnerInserted() {
  260 + verify(permissionMapper).insert(argThat((CrmPermissionDO permission) ->
  261 + permission.getId() == null
  262 + && Objects.equals(permission.getBizType(), BIZ_TYPE)
  263 + && Objects.equals(permission.getBizId(), BIZ_ID)
  264 + && Objects.equals(permission.getUserId(), NEW_OWNER_USER_ID)
  265 + && CrmPermissionLevelEnum.isOwner(permission.getLevel())));
  266 + }
  267 +
  268 + private void verifyLevelUpdated(Long id, CrmPermissionLevelEnum level) {
  269 + verify(permissionMapper).updateById(argThat((CrmPermissionDO permission) ->
  270 + Objects.equals(permission.getId(), id) && Objects.equals(permission.getLevel(), level.getLevel())));
  271 + }
  272 +
  273 + private void assertNoPermissionWrite() {
  274 + verify(permissionMapper).selectByBizTypeAndBizId(BIZ_TYPE, BIZ_ID);
  275 + verifyNoMoreInteractions(permissionMapper);
  276 + }
  277 +
  278 + private static CrmPermissionTransferReqBO buildTransferReqBO(Long newOwnerUserId, Integer oldOwnerPermissionLevel) {
  279 + return new CrmPermissionTransferReqBO(LOGIN_USER_ID, BIZ_TYPE, BIZ_ID, newOwnerUserId, oldOwnerPermissionLevel);
  280 + }
  281 +
  282 + private static CrmPermissionDO buildPermission(Long id, Long userId, CrmPermissionLevelEnum level) {
  283 + return new CrmPermissionDO().setId(id).setBizType(BIZ_TYPE).setBizId(BIZ_ID)
  284 + .setUserId(userId).setLevel(level.getLevel());
  285 + }
  286 +
  287 +}