Commit c452b0729d5ee255b34aafd4d91b1ab4b068591f
1 parent
7c397bbf
fix(pms): 串行化个人排序更新并统一项目删除加锁顺序
Showing
7 changed files
with
261 additions
and
6 deletions
yudao-module-pms/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectService.java
| ... | ... | @@ -45,6 +45,16 @@ public interface PmsProjectService { |
| 45 | 45 | PmsProjectDO getProject(Long id); |
| 46 | 46 | |
| 47 | 47 | /** |
| 48 | + * 获得项目,并锁定项目行 | |
| 49 | + * | |
| 50 | + * 需在事务中调用,锁保持至事务结束 | |
| 51 | + * | |
| 52 | + * @param id 项目编号 | |
| 53 | + * @return 项目 | |
| 54 | + */ | |
| 55 | + PmsProjectDO getProjectForUpdate(Long id); | |
| 56 | + | |
| 57 | + /** | |
| 48 | 58 | * 获得项目详情并记录访问时间 |
| 49 | 59 | * |
| 50 | 60 | * @param id 项目编号 | ... | ... |
yudao-module-pms/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectServiceImpl.java
| ... | ... | @@ -118,6 +118,11 @@ public class PmsProjectServiceImpl implements PmsProjectService { |
| 118 | 118 | } |
| 119 | 119 | |
| 120 | 120 | @Override |
| 121 | + public PmsProjectDO getProjectForUpdate(Long id) { | |
| 122 | + return projectMapper.selectOneForUpdate(PmsProjectDO::getId, id); | |
| 123 | + } | |
| 124 | + | |
| 125 | + @Override | |
| 121 | 126 | public PmsProjectDO getProjectAndUpdateVisitTime(Long id, Long userId) { |
| 122 | 127 | // 1. 校验当前用户可以查看项目 |
| 123 | 128 | PmsProjectDO project = projectMemberService.validateProjectReadable(id, userId); |
| ... | ... | @@ -307,8 +312,11 @@ public class PmsProjectServiceImpl implements PmsProjectService { |
| 307 | 312 | @Override |
| 308 | 313 | @Transactional(rollbackFor = Exception.class) |
| 309 | 314 | public void deleteProject(Long id, Long userId) { |
| 310 | - // 1.1 校验项目存在 | |
| 311 | - PmsProjectDO project = validateProjectExists(id); | |
| 315 | + // 1.1 锁定项目并校验存在,与个人排序保持一致的加锁顺序(https://t.zsxq.com/V48Hl) | |
| 316 | + PmsProjectDO project = getProjectForUpdate(id); | |
| 317 | + if (project == null) { | |
| 318 | + throw exception(PROJECT_NOT_EXISTS); | |
| 319 | + } | |
| 312 | 320 | // 1.2 校验项目位于回收站 |
| 313 | 321 | if (ObjectUtil.notEqual(PmsProjectStatusEnum.RECYCLED.getStatus(), project.getStatus())) { |
| 314 | 322 | throw exception(PROJECT_STATUS_INVALID); | ... | ... |
yudao-module-pms/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortServiceImpl.java
| 1 | 1 | package cn.iocoder.yudao.module.pms.service.pm.workitem; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; | |
| 4 | 5 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; |
| 5 | 6 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO; |
| 6 | 7 | import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemUserSortMapper; |
| 8 | +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; | |
| 9 | +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectService; | |
| 7 | 10 | import jakarta.annotation.Resource; |
| 11 | +import org.springframework.context.annotation.Lazy; | |
| 8 | 12 | import org.springframework.stereotype.Service; |
| 9 | 13 | import org.springframework.transaction.annotation.Transactional; |
| 10 | 14 | import org.springframework.validation.annotation.Validated; |
| ... | ... | @@ -13,7 +17,10 @@ import java.util.Comparator; |
| 13 | 17 | import java.util.List; |
| 14 | 18 | import java.util.Map; |
| 15 | 19 | |
| 20 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | |
| 16 | 21 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; |
| 22 | +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS; | |
| 23 | +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID; | |
| 17 | 24 | |
| 18 | 25 | /** |
| 19 | 26 | * PMS 工作项个人排序 Service 实现类 |
| ... | ... | @@ -27,13 +34,27 @@ public class PmsWorkItemUserSortServiceImpl implements PmsWorkItemUserSortServic |
| 27 | 34 | @Resource |
| 28 | 35 | private PmsWorkItemUserSortMapper workItemUserSortMapper; |
| 29 | 36 | |
| 37 | + @Lazy // 延迟加载,避免循环依赖 | |
| 38 | + @Resource | |
| 39 | + private PmsProjectService projectService; | |
| 40 | + | |
| 30 | 41 | @Override |
| 31 | 42 | @Transactional(rollbackFor = Exception.class) |
| 32 | 43 | public void updateWorkItemUserSort(Long projectId, List<Long> workItemIds, Long userId) { |
| 33 | - // 1. 删除原有个人排序 | |
| 44 | + // 1.1 锁定项目,避免并发更新个人排序产生重复记录(https://t.zsxq.com/V48Hl) | |
| 45 | + PmsProjectDO project = projectService.getProjectForUpdate(projectId); | |
| 46 | + // 1.2 校验锁定后的项目状态,避免项目并发回收、删除后继续写入个人排序 | |
| 47 | + if (project == null) { | |
| 48 | + throw exception(PROJECT_NOT_EXISTS); | |
| 49 | + } | |
| 50 | + if (!PmsProjectStatusEnum.ACTIVE.getStatus().equals(project.getStatus())) { | |
| 51 | + throw exception(PROJECT_STATUS_INVALID); | |
| 52 | + } | |
| 53 | + | |
| 54 | + // 2. 删除原有个人排序 | |
| 34 | 55 | workItemUserSortMapper.deleteByProjectIdAndUserId(projectId, userId); |
| 35 | 56 | |
| 36 | - // 2. 按请求顺序保存个人排序 | |
| 57 | + // 3. 按请求顺序保存个人排序 | |
| 37 | 58 | for (int index = 0; index < workItemIds.size(); index++) { |
| 38 | 59 | workItemUserSortMapper.insert(new PmsWorkItemUserSortDO().setProjectId(projectId) |
| 39 | 60 | .setWorkItemId(workItemIds.get(index)).setUserId(userId).setSort(index + 1)); | ... | ... |
yudao-module-pms/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectServiceImplTest.java
| ... | ... | @@ -33,6 +33,7 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServic |
| 33 | 33 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; |
| 34 | 34 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; |
| 35 | 35 | import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_ADMIN_REQUIRED; |
| 36 | +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS; | |
| 36 | 37 | import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_OWNER_REQUIRED; |
| 37 | 38 | import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID; |
| 38 | 39 | import static java.util.Arrays.asList; |
| ... | ... | @@ -237,6 +238,35 @@ public class PmsProjectServiceImplTest extends BaseDbUnitTest { |
| 237 | 238 | } |
| 238 | 239 | |
| 239 | 240 | @Test |
| 241 | + public void testDeleteProject_success() { | |
| 242 | + // mock 数据 | |
| 243 | + Long userId = randomLongId(); | |
| 244 | + PmsProjectDO project = randomProjectDO(PmsProjectStatusEnum.RECYCLED.getStatus()); | |
| 245 | + projectMapper.insert(project); | |
| 246 | + when(projectMemberService.hasProjectOwnerPermission(project.getId(), userId)).thenReturn(true); | |
| 247 | + | |
| 248 | + // 调用 | |
| 249 | + projectService.deleteProject(project.getId(), userId); | |
| 250 | + | |
| 251 | + // 断言 | |
| 252 | + assertNull(projectMapper.selectById(project.getId())); | |
| 253 | + verify(workItemWorkLogService).deleteWorkItemWorkLogListByProjectId(project.getId()); | |
| 254 | + verify(workItemService).deleteWorkItemListByProjectId(project.getId()); | |
| 255 | + verify(iterationService).deleteIterationListByProjectId(project.getId()); | |
| 256 | + verify(projectMemberService).deleteProjectMemberListByProjectId(project.getId()); | |
| 257 | + } | |
| 258 | + | |
| 259 | + @Test | |
| 260 | + public void testDeleteProject_projectNotExists() { | |
| 261 | + // 准备参数 | |
| 262 | + Long projectId = randomLongId(); | |
| 263 | + Long userId = randomLongId(); | |
| 264 | + | |
| 265 | + // 调用,并断言异常 | |
| 266 | + assertServiceException(() -> projectService.deleteProject(projectId, userId), PROJECT_NOT_EXISTS); | |
| 267 | + } | |
| 268 | + | |
| 269 | + @Test | |
| 240 | 270 | public void testDeleteProject_statusInvalid() { |
| 241 | 271 | // mock 数据 |
| 242 | 272 | Long userId = randomLongId(); | ... | ... |
yudao-module-pms/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemServiceImplTest.java
| ... | ... | @@ -24,6 +24,7 @@ import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemMemberD |
| 24 | 24 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO; |
| 25 | 25 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO; |
| 26 | 26 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemWorkLogDO; |
| 27 | +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectMapper; | |
| 27 | 28 | import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemBoardMapper; |
| 28 | 29 | import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemCommentMapper; |
| 29 | 30 | import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemMapper; |
| ... | ... | @@ -39,9 +40,14 @@ import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemPriorityEnum; |
| 39 | 40 | import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; |
| 40 | 41 | import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; |
| 41 | 42 | import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; |
| 43 | +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectAnnouncementService; | |
| 44 | +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectFavoriteService; | |
| 45 | +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectGroupService; | |
| 42 | 46 | import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; |
| 47 | +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectServiceImpl; | |
| 43 | 48 | import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; |
| 44 | 49 | import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; |
| 50 | +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; | |
| 45 | 51 | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| 46 | 52 | import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; |
| 47 | 53 | import jakarta.annotation.Resource; |
| ... | ... | @@ -88,7 +94,8 @@ import static org.mockito.Mockito.when; |
| 88 | 94 | * |
| 89 | 95 | * @author 芋道源码 |
| 90 | 96 | */ |
| 91 | -@Import({PmsWorkItemServiceImpl.class, PmsWorkItemUserSortServiceImpl.class, PmsWorkItemStatusServiceImpl.class, | |
| 97 | +@Import({PmsWorkItemServiceImpl.class, PmsWorkItemUserSortServiceImpl.class, PmsProjectServiceImpl.class, | |
| 98 | + PmsWorkItemStatusServiceImpl.class, | |
| 92 | 99 | PmsWorkItemBoardServiceImpl.class, |
| 93 | 100 | PmsWorkItemCommentServiceImpl.class, PmsWorkItemWorkLogServiceImpl.class}) |
| 94 | 101 | public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { |
| ... | ... | @@ -110,10 +117,18 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { |
| 110 | 117 | private PmsWorkItemWorkLogMapper workItemWorkLogMapper; |
| 111 | 118 | @Resource |
| 112 | 119 | private PmsWorkItemUserSortMapper workItemUserSortMapper; |
| 120 | + @Resource | |
| 121 | + private PmsProjectMapper projectMapper; | |
| 113 | 122 | |
| 114 | 123 | @MockitoBean |
| 115 | 124 | private PmsProjectMemberService projectMemberService; |
| 116 | 125 | @MockitoBean |
| 126 | + private PmsProjectGroupService projectGroupService; | |
| 127 | + @MockitoBean | |
| 128 | + private PmsProjectFavoriteService projectFavoriteService; | |
| 129 | + @MockitoBean | |
| 130 | + private PmsProjectAnnouncementService projectAnnouncementService; | |
| 131 | + @MockitoBean | |
| 117 | 132 | private PmsIterationService iterationService; |
| 118 | 133 | @MockitoBean |
| 119 | 134 | private PmsWorkItemLabelService workItemLabelService; |
| ... | ... | @@ -126,6 +141,8 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { |
| 126 | 141 | private NotifyMessageSendApi notifyMessageSendApi; |
| 127 | 142 | @MockitoBean |
| 128 | 143 | private AdminUserApi adminUserApi; |
| 144 | + @MockitoBean | |
| 145 | + private PermissionApi permissionApi; | |
| 129 | 146 | |
| 130 | 147 | @Test |
| 131 | 148 | public void testCreateWorkItem_success() { |
| ... | ... | @@ -510,6 +527,8 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { |
| 510 | 527 | // mock 数据 |
| 511 | 528 | Long userId = randomLongId(); |
| 512 | 529 | Long projectId = randomLongId(); |
| 530 | + PmsProjectDO project = randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType()); | |
| 531 | + projectMapper.insert(project); | |
| 513 | 532 | PmsWorkItemStatusDO taskStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), |
| 514 | 533 | PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); |
| 515 | 534 | workItemStatusMapper.insert(taskStatus); |
| ... | ... | @@ -521,7 +540,7 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { |
| 521 | 540 | PmsWorkItemDO second = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.DEFECT.getType(), defectStatus, 2); |
| 522 | 541 | workItemMapper.insert(second); |
| 523 | 542 | when(projectMemberService.validateProjectWritable(projectId, userId)) |
| 524 | - .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); | |
| 543 | + .thenReturn(project); | |
| 525 | 544 | // 准备参数 |
| 526 | 545 | PmsWorkItemPlanningSortReqVO reqVO = new PmsWorkItemPlanningSortReqVO().setProjectId(projectId) |
| 527 | 546 | .setWorkItemIds(Arrays.asList(second.getId(), first.getId())); | ... | ... |
yudao-module-pms/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.pms.service.pm.workitem; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; | |
| 4 | +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; | |
| 5 | +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO; | |
| 6 | +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectMapper; | |
| 7 | +import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemUserSortMapper; | |
| 8 | +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum; | |
| 9 | +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; | |
| 10 | +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectAnnouncementService; | |
| 11 | +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectFavoriteService; | |
| 12 | +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectGroupService; | |
| 13 | +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; | |
| 14 | +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectServiceImpl; | |
| 15 | +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; | |
| 16 | +import jakarta.annotation.Resource; | |
| 17 | +import org.junit.jupiter.api.Test; | |
| 18 | +import org.springframework.context.annotation.Import; | |
| 19 | +import org.springframework.test.context.bean.override.mockito.MockitoBean; | |
| 20 | + | |
| 21 | +import java.util.ArrayList; | |
| 22 | +import java.util.List; | |
| 23 | +import java.util.concurrent.CountDownLatch; | |
| 24 | +import java.util.concurrent.ExecutorService; | |
| 25 | +import java.util.concurrent.Executors; | |
| 26 | +import java.util.concurrent.Future; | |
| 27 | +import java.util.concurrent.TimeUnit; | |
| 28 | + | |
| 29 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | |
| 30 | +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; | |
| 31 | +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS; | |
| 32 | +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID; | |
| 33 | +import static org.junit.jupiter.api.Assertions.assertEquals; | |
| 34 | + | |
| 35 | +/** | |
| 36 | + * {@link PmsWorkItemUserSortServiceImpl} 的单元测试类 | |
| 37 | + * | |
| 38 | + * @author 芋道源码 | |
| 39 | + */ | |
| 40 | +@Import({PmsWorkItemUserSortServiceImpl.class, PmsProjectServiceImpl.class}) | |
| 41 | +public class PmsWorkItemUserSortServiceImplTest extends BaseDbUnitTest { | |
| 42 | + | |
| 43 | + @Resource | |
| 44 | + private PmsWorkItemUserSortServiceImpl workItemUserSortService; | |
| 45 | + | |
| 46 | + @Resource | |
| 47 | + private PmsWorkItemUserSortMapper workItemUserSortMapper; | |
| 48 | + @Resource | |
| 49 | + private PmsProjectMapper projectMapper; | |
| 50 | + | |
| 51 | + @MockitoBean | |
| 52 | + private PmsProjectMemberService projectMemberService; | |
| 53 | + @MockitoBean | |
| 54 | + private PmsProjectGroupService projectGroupService; | |
| 55 | + @MockitoBean | |
| 56 | + private PmsProjectFavoriteService projectFavoriteService; | |
| 57 | + @MockitoBean | |
| 58 | + private PmsProjectAnnouncementService projectAnnouncementService; | |
| 59 | + @MockitoBean | |
| 60 | + private PmsIterationService iterationService; | |
| 61 | + @MockitoBean | |
| 62 | + private PmsWorkItemService workItemService; | |
| 63 | + @MockitoBean | |
| 64 | + private PmsWorkItemStatusService workItemStatusService; | |
| 65 | + @MockitoBean | |
| 66 | + private PmsWorkItemWorkLogService workItemWorkLogService; | |
| 67 | + @MockitoBean | |
| 68 | + private PermissionApi permissionApi; | |
| 69 | + | |
| 70 | + @Test | |
| 71 | + public void testUpdateWorkItemUserSort_success() { | |
| 72 | + // mock 数据 | |
| 73 | + PmsProjectDO project = new PmsProjectDO().setName("测试项目").setType(1).setLevel(1); | |
| 74 | + projectMapper.insert(project); | |
| 75 | + workItemUserSortService.updateWorkItemUserSort(project.getId(), List.of(50L, 51L), 238L); | |
| 76 | + workItemUserSortService.updateWorkItemUserSort(project.getId(), List.of(50L, 51L), 239L); | |
| 77 | + | |
| 78 | + // 调用:连续调整个人排序,并移除不再位于待规划区域的工作项 | |
| 79 | + workItemUserSortService.updateWorkItemUserSort(project.getId(), List.of(51L, 50L), 238L); | |
| 80 | + workItemUserSortService.updateWorkItemUserSort(project.getId(), List.of(50L), 238L); | |
| 81 | + | |
| 82 | + // 断言:仅保留当前顺序,不影响其他用户的个人排序 | |
| 83 | + List<PmsWorkItemUserSortDO> userSorts = workItemUserSortMapper | |
| 84 | + .selectListByProjectIdAndUserId(project.getId(), 238L); | |
| 85 | + assertEquals(List.of(50L), convertList(userSorts, PmsWorkItemUserSortDO::getWorkItemId)); | |
| 86 | + assertEquals(1, userSorts.get(0).getSort()); | |
| 87 | + assertEquals(List.of(50L, 51L), convertList(workItemUserSortMapper | |
| 88 | + .selectListByProjectIdAndUserId(project.getId(), 239L), PmsWorkItemUserSortDO::getWorkItemId)); | |
| 89 | + } | |
| 90 | + | |
| 91 | + @Test | |
| 92 | + public void testUpdateWorkItemUserSort_projectNotExists() { | |
| 93 | + // 调用,并断言异常 | |
| 94 | + assertServiceException(() -> workItemUserSortService.updateWorkItemUserSort(999L, | |
| 95 | + List.of(50L), 238L), PROJECT_NOT_EXISTS); | |
| 96 | + assertEquals(0L, workItemUserSortMapper.selectCount()); | |
| 97 | + } | |
| 98 | + | |
| 99 | + @Test | |
| 100 | + public void testUpdateWorkItemUserSort_projectDeleted() { | |
| 101 | + // mock 数据 | |
| 102 | + PmsProjectDO project = new PmsProjectDO().setName("测试项目").setType(1).setLevel(1); | |
| 103 | + projectMapper.insert(project); | |
| 104 | + projectMapper.deleteById(project.getId()); | |
| 105 | + | |
| 106 | + // 调用,并断言异常 | |
| 107 | + assertServiceException(() -> workItemUserSortService.updateWorkItemUserSort(project.getId(), | |
| 108 | + List.of(50L), 238L), PROJECT_NOT_EXISTS); | |
| 109 | + assertEquals(0L, workItemUserSortMapper.selectCount()); | |
| 110 | + } | |
| 111 | + | |
| 112 | + @Test | |
| 113 | + public void testUpdateWorkItemUserSort_projectStatusInvalid() { | |
| 114 | + // mock 数据 | |
| 115 | + PmsProjectDO project = new PmsProjectDO().setName("测试项目").setType(1).setLevel(1) | |
| 116 | + .setStatus(PmsProjectStatusEnum.RECYCLED.getStatus()); | |
| 117 | + projectMapper.insert(project); | |
| 118 | + workItemUserSortMapper.insert(new PmsWorkItemUserSortDO().setProjectId(project.getId()) | |
| 119 | + .setWorkItemId(50L).setUserId(238L).setSort(1)); | |
| 120 | + | |
| 121 | + // 调用,并断言异常 | |
| 122 | + assertServiceException(() -> workItemUserSortService.updateWorkItemUserSort(project.getId(), | |
| 123 | + List.of(51L), 238L), PROJECT_STATUS_INVALID); | |
| 124 | + // 断言:拒绝更新时保留原有个人排序 | |
| 125 | + assertEquals(List.of(50L), convertList(workItemUserSortMapper | |
| 126 | + .selectListByProjectIdAndUserId(project.getId(), 238L), PmsWorkItemUserSortDO::getWorkItemId)); | |
| 127 | + } | |
| 128 | + | |
| 129 | + @Test | |
| 130 | + public void testUpdateWorkItemUserSort_concurrent() throws Exception { | |
| 131 | + // mock 数据 | |
| 132 | + PmsProjectDO project = new PmsProjectDO().setName("测试项目").setType(1).setLevel(1); | |
| 133 | + projectMapper.insert(project); | |
| 134 | + // 准备参数 | |
| 135 | + CountDownLatch startLatch = new CountDownLatch(1); | |
| 136 | + ExecutorService executor = Executors.newFixedThreadPool(4); | |
| 137 | + List<Future<?>> futures = new ArrayList<>(); | |
| 138 | + | |
| 139 | + // 调用:同一项目、同一用户并发保存个人排序 | |
| 140 | + try { | |
| 141 | + for (int index = 0; index < 4; index++) { | |
| 142 | + futures.add(executor.submit(() -> { | |
| 143 | + startLatch.await(); | |
| 144 | + for (int count = 0; count < 5; count++) { | |
| 145 | + workItemUserSortService.updateWorkItemUserSort(project.getId(), List.of(51L, 50L), 238L); | |
| 146 | + } | |
| 147 | + return null; | |
| 148 | + })); | |
| 149 | + } | |
| 150 | + startLatch.countDown(); | |
| 151 | + for (Future<?> future : futures) { | |
| 152 | + future.get(20, TimeUnit.SECONDS); | |
| 153 | + } | |
| 154 | + } finally { | |
| 155 | + executor.shutdownNow(); | |
| 156 | + executor.awaitTermination(10, TimeUnit.SECONDS); | |
| 157 | + } | |
| 158 | + | |
| 159 | + // 断言:没有重复的有效记录,顺序与最后一次保存一致 | |
| 160 | + List<PmsWorkItemUserSortDO> userSorts = workItemUserSortMapper | |
| 161 | + .selectListByProjectIdAndUserId(project.getId(), 238L); | |
| 162 | + assertEquals(List.of(51L, 50L), convertList(userSorts, PmsWorkItemUserSortDO::getWorkItemId)); | |
| 163 | + assertEquals(List.of(1, 2), convertList(userSorts, PmsWorkItemUserSortDO::getSort)); | |
| 164 | + } | |
| 165 | + | |
| 166 | +} | ... | ... |
yudao-module-pms/src/test/resources/sql/clean.sql
| ... | ... | @@ -5,6 +5,7 @@ DELETE FROM "pms_work_item_comment"; |
| 5 | 5 | DELETE FROM "pms_work_item_label"; |
| 6 | 6 | DELETE FROM "pms_work_item_work_log"; |
| 7 | 7 | DELETE FROM "pms_work_item_member"; |
| 8 | +DELETE FROM "pms_work_item_user_sort"; | |
| 8 | 9 | DELETE FROM "pms_work_item"; |
| 9 | 10 | DELETE FROM "pms_work_item_status"; |
| 10 | 11 | DELETE FROM "pms_iteration"; | ... | ... |