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,6 +45,16 @@ public interface PmsProjectService { | ||
| 45 | PmsProjectDO getProject(Long id); | 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 | * @param id 项目编号 | 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,6 +118,11 @@ public class PmsProjectServiceImpl implements PmsProjectService { | ||
| 118 | } | 118 | } |
| 119 | 119 | ||
| 120 | @Override | 120 | @Override |
| 121 | + public PmsProjectDO getProjectForUpdate(Long id) { | ||
| 122 | + return projectMapper.selectOneForUpdate(PmsProjectDO::getId, id); | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + @Override | ||
| 121 | public PmsProjectDO getProjectAndUpdateVisitTime(Long id, Long userId) { | 126 | public PmsProjectDO getProjectAndUpdateVisitTime(Long id, Long userId) { |
| 122 | // 1. 校验当前用户可以查看项目 | 127 | // 1. 校验当前用户可以查看项目 |
| 123 | PmsProjectDO project = projectMemberService.validateProjectReadable(id, userId); | 128 | PmsProjectDO project = projectMemberService.validateProjectReadable(id, userId); |
| @@ -307,8 +312,11 @@ public class PmsProjectServiceImpl implements PmsProjectService { | @@ -307,8 +312,11 @@ public class PmsProjectServiceImpl implements PmsProjectService { | ||
| 307 | @Override | 312 | @Override |
| 308 | @Transactional(rollbackFor = Exception.class) | 313 | @Transactional(rollbackFor = Exception.class) |
| 309 | public void deleteProject(Long id, Long userId) { | 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 | // 1.2 校验项目位于回收站 | 320 | // 1.2 校验项目位于回收站 |
| 313 | if (ObjectUtil.notEqual(PmsProjectStatusEnum.RECYCLED.getStatus(), project.getStatus())) { | 321 | if (ObjectUtil.notEqual(PmsProjectStatusEnum.RECYCLED.getStatus(), project.getStatus())) { |
| 314 | throw exception(PROJECT_STATUS_INVALID); | 322 | throw exception(PROJECT_STATUS_INVALID); |
yudao-module-pms/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortServiceImpl.java
| 1 | package cn.iocoder.yudao.module.pms.service.pm.workitem; | 1 | package cn.iocoder.yudao.module.pms.service.pm.workitem; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO; | ||
| 4 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; | 5 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO; |
| 5 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO; | 6 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO; |
| 6 | import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemUserSortMapper; | 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 | import jakarta.annotation.Resource; | 10 | import jakarta.annotation.Resource; |
| 11 | +import org.springframework.context.annotation.Lazy; | ||
| 8 | import org.springframework.stereotype.Service; | 12 | import org.springframework.stereotype.Service; |
| 9 | import org.springframework.transaction.annotation.Transactional; | 13 | import org.springframework.transaction.annotation.Transactional; |
| 10 | import org.springframework.validation.annotation.Validated; | 14 | import org.springframework.validation.annotation.Validated; |
| @@ -13,7 +17,10 @@ import java.util.Comparator; | @@ -13,7 +17,10 @@ import java.util.Comparator; | ||
| 13 | import java.util.List; | 17 | import java.util.List; |
| 14 | import java.util.Map; | 18 | import java.util.Map; |
| 15 | 19 | ||
| 20 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | ||
| 16 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; | 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 | * PMS 工作项个人排序 Service 实现类 | 26 | * PMS 工作项个人排序 Service 实现类 |
| @@ -27,13 +34,27 @@ public class PmsWorkItemUserSortServiceImpl implements PmsWorkItemUserSortServic | @@ -27,13 +34,27 @@ public class PmsWorkItemUserSortServiceImpl implements PmsWorkItemUserSortServic | ||
| 27 | @Resource | 34 | @Resource |
| 28 | private PmsWorkItemUserSortMapper workItemUserSortMapper; | 35 | private PmsWorkItemUserSortMapper workItemUserSortMapper; |
| 29 | 36 | ||
| 37 | + @Lazy // 延迟加载,避免循环依赖 | ||
| 38 | + @Resource | ||
| 39 | + private PmsProjectService projectService; | ||
| 40 | + | ||
| 30 | @Override | 41 | @Override |
| 31 | @Transactional(rollbackFor = Exception.class) | 42 | @Transactional(rollbackFor = Exception.class) |
| 32 | public void updateWorkItemUserSort(Long projectId, List<Long> workItemIds, Long userId) { | 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 | workItemUserSortMapper.deleteByProjectIdAndUserId(projectId, userId); | 55 | workItemUserSortMapper.deleteByProjectIdAndUserId(projectId, userId); |
| 35 | 56 | ||
| 36 | - // 2. 按请求顺序保存个人排序 | 57 | + // 3. 按请求顺序保存个人排序 |
| 37 | for (int index = 0; index < workItemIds.size(); index++) { | 58 | for (int index = 0; index < workItemIds.size(); index++) { |
| 38 | workItemUserSortMapper.insert(new PmsWorkItemUserSortDO().setProjectId(projectId) | 59 | workItemUserSortMapper.insert(new PmsWorkItemUserSortDO().setProjectId(projectId) |
| 39 | .setWorkItemId(workItemIds.get(index)).setUserId(userId).setSort(index + 1)); | 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,6 +33,7 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServic | ||
| 33 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; | 33 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; |
| 34 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; | 34 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; |
| 35 | import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_ADMIN_REQUIRED; | 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 | import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_OWNER_REQUIRED; | 37 | import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_OWNER_REQUIRED; |
| 37 | import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID; | 38 | import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID; |
| 38 | import static java.util.Arrays.asList; | 39 | import static java.util.Arrays.asList; |
| @@ -237,6 +238,35 @@ public class PmsProjectServiceImplTest extends BaseDbUnitTest { | @@ -237,6 +238,35 @@ public class PmsProjectServiceImplTest extends BaseDbUnitTest { | ||
| 237 | } | 238 | } |
| 238 | 239 | ||
| 239 | @Test | 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 | public void testDeleteProject_statusInvalid() { | 270 | public void testDeleteProject_statusInvalid() { |
| 241 | // mock 数据 | 271 | // mock 数据 |
| 242 | Long userId = randomLongId(); | 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,6 +24,7 @@ import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemMemberD | ||
| 24 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO; | 24 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO; |
| 25 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO; | 25 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO; |
| 26 | import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemWorkLogDO; | 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 | import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemBoardMapper; | 28 | import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemBoardMapper; |
| 28 | import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemCommentMapper; | 29 | import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemCommentMapper; |
| 29 | import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemMapper; | 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,9 +40,14 @@ import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemPriorityEnum; | ||
| 39 | import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; | 40 | import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum; |
| 40 | import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; | 41 | import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum; |
| 41 | import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService; | 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 | import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; | 46 | import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService; |
| 47 | +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectServiceImpl; | ||
| 43 | import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; | 48 | import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; |
| 44 | import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; | 49 | import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; |
| 50 | +import cn.iocoder.yudao.module.system.api.permission.PermissionApi; | ||
| 45 | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; | 51 | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| 46 | import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; | 52 | import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; |
| 47 | import jakarta.annotation.Resource; | 53 | import jakarta.annotation.Resource; |
| @@ -88,7 +94,8 @@ import static org.mockito.Mockito.when; | @@ -88,7 +94,8 @@ import static org.mockito.Mockito.when; | ||
| 88 | * | 94 | * |
| 89 | * @author 芋道源码 | 95 | * @author 芋道源码 |
| 90 | */ | 96 | */ |
| 91 | -@Import({PmsWorkItemServiceImpl.class, PmsWorkItemUserSortServiceImpl.class, PmsWorkItemStatusServiceImpl.class, | 97 | +@Import({PmsWorkItemServiceImpl.class, PmsWorkItemUserSortServiceImpl.class, PmsProjectServiceImpl.class, |
| 98 | + PmsWorkItemStatusServiceImpl.class, | ||
| 92 | PmsWorkItemBoardServiceImpl.class, | 99 | PmsWorkItemBoardServiceImpl.class, |
| 93 | PmsWorkItemCommentServiceImpl.class, PmsWorkItemWorkLogServiceImpl.class}) | 100 | PmsWorkItemCommentServiceImpl.class, PmsWorkItemWorkLogServiceImpl.class}) |
| 94 | public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { | 101 | public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { |
| @@ -110,10 +117,18 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { | @@ -110,10 +117,18 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { | ||
| 110 | private PmsWorkItemWorkLogMapper workItemWorkLogMapper; | 117 | private PmsWorkItemWorkLogMapper workItemWorkLogMapper; |
| 111 | @Resource | 118 | @Resource |
| 112 | private PmsWorkItemUserSortMapper workItemUserSortMapper; | 119 | private PmsWorkItemUserSortMapper workItemUserSortMapper; |
| 120 | + @Resource | ||
| 121 | + private PmsProjectMapper projectMapper; | ||
| 113 | 122 | ||
| 114 | @MockitoBean | 123 | @MockitoBean |
| 115 | private PmsProjectMemberService projectMemberService; | 124 | private PmsProjectMemberService projectMemberService; |
| 116 | @MockitoBean | 125 | @MockitoBean |
| 126 | + private PmsProjectGroupService projectGroupService; | ||
| 127 | + @MockitoBean | ||
| 128 | + private PmsProjectFavoriteService projectFavoriteService; | ||
| 129 | + @MockitoBean | ||
| 130 | + private PmsProjectAnnouncementService projectAnnouncementService; | ||
| 131 | + @MockitoBean | ||
| 117 | private PmsIterationService iterationService; | 132 | private PmsIterationService iterationService; |
| 118 | @MockitoBean | 133 | @MockitoBean |
| 119 | private PmsWorkItemLabelService workItemLabelService; | 134 | private PmsWorkItemLabelService workItemLabelService; |
| @@ -126,6 +141,8 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { | @@ -126,6 +141,8 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { | ||
| 126 | private NotifyMessageSendApi notifyMessageSendApi; | 141 | private NotifyMessageSendApi notifyMessageSendApi; |
| 127 | @MockitoBean | 142 | @MockitoBean |
| 128 | private AdminUserApi adminUserApi; | 143 | private AdminUserApi adminUserApi; |
| 144 | + @MockitoBean | ||
| 145 | + private PermissionApi permissionApi; | ||
| 129 | 146 | ||
| 130 | @Test | 147 | @Test |
| 131 | public void testCreateWorkItem_success() { | 148 | public void testCreateWorkItem_success() { |
| @@ -510,6 +527,8 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { | @@ -510,6 +527,8 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { | ||
| 510 | // mock 数据 | 527 | // mock 数据 |
| 511 | Long userId = randomLongId(); | 528 | Long userId = randomLongId(); |
| 512 | Long projectId = randomLongId(); | 529 | Long projectId = randomLongId(); |
| 530 | + PmsProjectDO project = randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType()); | ||
| 531 | + projectMapper.insert(project); | ||
| 513 | PmsWorkItemStatusDO taskStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), | 532 | PmsWorkItemStatusDO taskStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(), |
| 514 | PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); | 533 | PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1); |
| 515 | workItemStatusMapper.insert(taskStatus); | 534 | workItemStatusMapper.insert(taskStatus); |
| @@ -521,7 +540,7 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { | @@ -521,7 +540,7 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest { | ||
| 521 | PmsWorkItemDO second = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.DEFECT.getType(), defectStatus, 2); | 540 | PmsWorkItemDO second = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.DEFECT.getType(), defectStatus, 2); |
| 522 | workItemMapper.insert(second); | 541 | workItemMapper.insert(second); |
| 523 | when(projectMemberService.validateProjectWritable(projectId, userId)) | 542 | when(projectMemberService.validateProjectWritable(projectId, userId)) |
| 524 | - .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType())); | 543 | + .thenReturn(project); |
| 525 | // 准备参数 | 544 | // 准备参数 |
| 526 | PmsWorkItemPlanningSortReqVO reqVO = new PmsWorkItemPlanningSortReqVO().setProjectId(projectId) | 545 | PmsWorkItemPlanningSortReqVO reqVO = new PmsWorkItemPlanningSortReqVO().setProjectId(projectId) |
| 527 | .setWorkItemIds(Arrays.asList(second.getId(), first.getId())); | 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,6 +5,7 @@ DELETE FROM "pms_work_item_comment"; | ||
| 5 | DELETE FROM "pms_work_item_label"; | 5 | DELETE FROM "pms_work_item_label"; |
| 6 | DELETE FROM "pms_work_item_work_log"; | 6 | DELETE FROM "pms_work_item_work_log"; |
| 7 | DELETE FROM "pms_work_item_member"; | 7 | DELETE FROM "pms_work_item_member"; |
| 8 | +DELETE FROM "pms_work_item_user_sort"; | ||
| 8 | DELETE FROM "pms_work_item"; | 9 | DELETE FROM "pms_work_item"; |
| 9 | DELETE FROM "pms_work_item_status"; | 10 | DELETE FROM "pms_work_item_status"; |
| 10 | DELETE FROM "pms_iteration"; | 11 | DELETE FROM "pms_iteration"; |